Files
mihomo_injecter/app/tests/test_expander.py
T
urbnywrt 2e1c488bb9 feat: direct subscription fetching, URI parser, FLClashX compatibility
- Fetch subscriptions directly via httpx (mihomo UA → YAML, xray-checker → base64 URI list)
- Add uri_parser.py: vless/vmess/ss/trojan/hysteria2 URI → Mihomo proxy dicts
- Fix YAML quoting for Go parser (strings starting with special chars)
- Remove hidden:true and proxy-providers from delivered configs
- Inject all service groups into GLOBAL proxies for FLClashX group discovery
- Strip placeholder proxy-providers (e.g. "subscription") not in DB
- Fix Mihomo healthcheck: add Bearer auth header
- Add README

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 06:04:03 +03:00

270 lines
7.7 KiB
Python

import yaml
from dataclasses import dataclass
from expander import filter_proxy, expand_config, build_mihomo_config, inject_providers_for_delivery
@dataclass
class FakeSub:
name: str
url: str
PROXY_FULL = {
"name": "node1",
"type": "ss",
"server": "1.2.3.4",
"port": 443,
"password": "secret",
"cipher": "aes-256-gcm",
"alive": True, # runtime field — should be stripped
"history": [], # runtime field — should be stripped
"extra": {"foo": "bar"}, # runtime field — should be stripped
}
BASE_YAML = """
proxies: []
proxy-providers:
provider1:
type: http
url: https://example.com/sub
interval: 3600
proxy-groups:
- name: Proxy
type: select
use:
- provider1
rules:
- MATCH,DIRECT
"""
def test_filter_proxy_removes_runtime_fields():
filtered = filter_proxy(PROXY_FULL)
assert "alive" not in filtered
assert "history" not in filtered
assert "extra" not in filtered
assert filtered["name"] == "node1"
assert filtered["server"] == "1.2.3.4"
def test_expand_config_replaces_providers_with_proxies():
proxies = [
{"name": "node1", "type": "ss", "server": "1.2.3.4", "port": 443,
"password": "pwd", "cipher": "aes-256-gcm"},
]
result = expand_config(BASE_YAML, {"provider1": proxies})
cfg = yaml.safe_load(result)
assert "proxy-providers" not in cfg
assert any(p["name"] == "node1" for p in cfg["proxies"])
def test_expand_config_fills_use_in_proxy_groups():
proxies = [
{"name": "node1", "type": "ss", "server": "1.2.3.4", "port": 443,
"password": "pwd", "cipher": "aes-256-gcm"},
{"name": "node2", "type": "ss", "server": "5.6.7.8", "port": 443,
"password": "pwd", "cipher": "aes-256-gcm"},
]
result = expand_config(BASE_YAML, {"provider1": proxies})
cfg = yaml.safe_load(result)
proxy_group = next(g for g in cfg["proxy-groups"] if g["name"] == "Proxy")
assert "use" not in proxy_group
assert "node1" in proxy_group["proxies"]
assert "node2" in proxy_group["proxies"]
def test_expand_config_preserves_existing_proxies():
base = """
proxies:
- name: static-node
type: direct
proxy-providers:
p1:
type: http
url: https://example.com/sub
interval: 3600
proxy-groups: []
rules: []
"""
result = expand_config(base, {"p1": [{"name": "dynamic-node", "type": "ss",
"server": "1.2.3.4", "port": 443}]})
cfg = yaml.safe_load(result)
names = [p["name"] for p in cfg["proxies"]]
assert "static-node" in names
assert "dynamic-node" in names
def test_expand_config_empty_providers():
result = expand_config(BASE_YAML, {})
cfg = yaml.safe_load(result)
assert "proxy-providers" not in cfg
def test_build_mihomo_config_merges_providers():
yaml1 = """
proxy-providers:
p1:
type: http
url: https://a.example.com/sub
interval: 3600
health-check:
enable: true
url: http://www.gstatic.com/generate_204
interval: 300
"""
yaml2 = """
proxy-providers:
p2:
type: http
url: https://b.example.com/sub
interval: 3600
"""
result = build_mihomo_config([yaml1, yaml2], "mysecret")
cfg = yaml.safe_load(result)
assert cfg["external-controller"] == "0.0.0.0:9090"
assert cfg["secret"] == "mysecret"
assert cfg["mixed-port"] == 7890
assert cfg["allow-lan"] is False
assert "p1" in cfg["proxy-providers"]
assert "p2" in cfg["proxy-providers"]
assert cfg["proxy-providers"]["p1"]["health-check"]["enable"] is True
def test_build_mihomo_config_deduplicates_by_name():
yaml1 = """
proxy-providers:
p1:
type: http
url: https://original.example.com/sub
interval: 3600
"""
yaml2 = """
proxy-providers:
p1:
type: http
url: https://duplicate.example.com/sub
interval: 3600
"""
result = build_mihomo_config([yaml1, yaml2], "s")
cfg = yaml.safe_load(result)
assert cfg["proxy-providers"]["p1"]["url"] == "https://original.example.com/sub"
def test_build_mihomo_config_no_providers():
result = build_mihomo_config(["proxies: []"], "s")
cfg = yaml.safe_load(result)
assert "proxy-providers" not in cfg
def test_expand_config_skips_proxies_without_name():
result = expand_config(BASE_YAML, {"provider1": [
{"type": "ss", "server": "1.2.3.4", "port": 443}, # no "name"
{"name": "valid-node", "type": "ss", "server": "5.6.7.8", "port": 443},
]})
cfg = yaml.safe_load(result)
names = [p["name"] for p in cfg["proxies"]]
assert "valid-node" in names
assert len(names) == 1
def test_build_mihomo_config_skips_malformed_yaml():
valid_yaml = """
proxy-providers:
p1:
type: http
url: https://example.com/sub
interval: 3600
"""
result = build_mihomo_config([valid_yaml, ":: invalid yaml ::: {{{"], "s")
cfg = yaml.safe_load(result)
assert "p1" in cfg["proxy-providers"]
# --- inject_providers_for_delivery tests ---
def test_inject_providers_returns_unchanged_when_no_subs():
base = "proxies: []\nrules:\n - MATCH,DIRECT\n"
result = inject_providers_for_delivery(base, [])
assert result == base
def test_inject_providers_adds_entry_for_missing_sub():
base = "proxies: []\nproxy-groups: []\nrules:\n - MATCH,DIRECT\n"
sub = FakeSub(name="mysub", url="https://example.com/sub")
result = inject_providers_for_delivery(base, [sub])
cfg = yaml.safe_load(result)
assert "mysub" in cfg["proxy-providers"]
entry = cfg["proxy-providers"]["mysub"]
assert entry["type"] == "http"
assert entry["url"] == "https://example.com/sub"
assert entry["interval"] == 3600
assert entry["health-check"]["enable"] is True
def test_inject_providers_adds_use_to_fallback_and_url_test_groups():
base = (
"proxies: []\n"
"proxy-groups:\n"
" - name: Auto\n"
" type: url-test\n"
" proxies: []\n"
" - name: FB\n"
" type: fallback\n"
" proxies: []\n"
" - name: LB\n"
" type: load-balance\n"
" proxies: []\n"
" - name: Manual\n"
" type: select\n"
" proxies: []\n"
"rules:\n"
" - MATCH,DIRECT\n"
)
sub = FakeSub(name="newsub", url="https://example.com/sub")
result = inject_providers_for_delivery(base, [sub])
cfg = yaml.safe_load(result)
groups = {g["name"]: g for g in cfg["proxy-groups"]}
assert groups["Auto"]["use"] == ["newsub"]
assert groups["FB"]["use"] == ["newsub"]
assert groups["LB"]["use"] == ["newsub"]
assert "use" not in groups["Manual"]
def test_inject_providers_does_not_add_use_to_groups_that_already_have_use():
base = (
"proxies: []\n"
"proxy-groups:\n"
" - name: Auto\n"
" type: url-test\n"
" use:\n"
" - existing-provider\n"
"rules:\n"
" - MATCH,DIRECT\n"
)
sub = FakeSub(name="newsub", url="https://example.com/sub")
result = inject_providers_for_delivery(base, [sub])
cfg = yaml.safe_load(result)
groups = {g["name"]: g for g in cfg["proxy-groups"]}
assert groups["Auto"]["use"] == ["existing-provider"]
def test_inject_providers_skips_sub_already_in_base_yaml():
base = (
"proxy-providers:\n"
" existing-sub:\n"
" type: http\n"
" url: https://original.example.com/sub\n"
" interval: 3600\n"
"proxies: []\n"
"proxy-groups: []\n"
"rules:\n"
" - MATCH,DIRECT\n"
)
sub = FakeSub(name="existing-sub", url="https://different.example.com/sub")
result = inject_providers_for_delivery(base, [sub])
# Should return unchanged since no new entries
assert result == base