package automation import ( "testing" "gopkg.in/yaml.v3" ) func TestParseConfig_FullSchema(t *testing.T) { raw := map[string]interface{}{ "proxy-groups": []interface{}{ map[string]interface{}{ "name": "HK Nodes", "type": "url-test", "match": map[string]interface{}{ "subscriptions": []interface{}{"home-sub"}, "name-pattern": "(?i)HK|Hong Kong", }, "url": "http://www.gstatic.com/generate_204", "interval": 300, }, }, "filters": []interface{}{ map[string]interface{}{ "name": "Cheap", "compares": []interface{}{ map[string]interface{}{ "pattern": `([\d.]+)x`, "operator": "<=", "value": 1.0, }, }, }, }, "patches": []interface{}{ map[string]interface{}{ "path": "dns.nameserver", "op": "append", "value": []interface{}{"1.1.1.1"}, }, }, } cfg, err := ParseConfig(raw) if err != nil { t.Fatalf("ParseConfig() error = %v", err) } if len(cfg.ProxyGroups) != 1 { t.Fatalf("expected 1 proxy-group rule, got %d", len(cfg.ProxyGroups)) } pg := cfg.ProxyGroups[0] if pg.Name != "HK Nodes" || pg.Type != "url-test" { t.Fatalf("unexpected proxy-group rule: %#v", pg) } if len(pg.Match.Subscriptions) != 1 || pg.Match.Subscriptions[0] != "home-sub" { t.Fatalf("unexpected match.subscriptions: %#v", pg.Match.Subscriptions) } if pg.Match.NamePattern != "(?i)HK|Hong Kong" { t.Fatalf("unexpected match.name-pattern: %#v", pg.Match.NamePattern) } // "url" and "interval" aren't named fields on ProxyGroupRule — they must // land in Extra via the inline tag, not get silently dropped. if pg.Extra["url"] != "http://www.gstatic.com/generate_204" { t.Fatalf("expected url to be captured in Extra, got %#v", pg.Extra) } if _, ok := pg.Extra["name"]; ok { t.Fatalf("named field 'name' leaked into Extra: %#v", pg.Extra) } if _, ok := pg.Extra["match"]; ok { t.Fatalf("named field 'match' leaked into Extra: %#v", pg.Extra) } if len(cfg.Filters) != 1 || len(cfg.Filters[0].Compares) != 1 || cfg.Filters[0].Compares[0].Operator != "<=" || cfg.Filters[0].Compares[0].Value != 1.0 { t.Fatalf("unexpected filters: %#v", cfg.Filters) } if len(cfg.Patches) != 1 || cfg.Patches[0].Path != "dns.nameserver" || cfg.Patches[0].Op != "append" { t.Fatalf("unexpected patches: %#v", cfg.Patches) } } func TestParseConfig_RealWorldYAMLRoundTrip(t *testing.T) { // Simulates the actual path: core-config.yaml is loaded via // yamlutil.Unmarshal into map[string]interface{}, and the "ssm" key's // raw value is what gets handed to ParseConfig. yamlSrc := []byte(` ssm: proxy-groups: - name: HK Nodes type: select match: name-pattern: "HK" patches: - path: rules op: prepend value: ["DOMAIN,example.com,DIRECT"] `) var full map[string]interface{} if err := yaml.Unmarshal(yamlSrc, &full); err != nil { t.Fatalf("yaml.Unmarshal() error = %v", err) } cfg, err := ParseConfig(full["ssm"]) if err != nil { t.Fatalf("ParseConfig() error = %v", err) } if len(cfg.ProxyGroups) != 1 || cfg.ProxyGroups[0].Name != "HK Nodes" { t.Fatalf("unexpected proxy-groups: %#v", cfg.ProxyGroups) } if len(cfg.Patches) != 1 || cfg.Patches[0].Op != "prepend" { t.Fatalf("unexpected patches: %#v", cfg.Patches) } } func TestApply_EndToEnd(t *testing.T) { proxies := []ProxyRef{ {OriginalName: "HK 01 | 1.0x", DisplayName: "sub-a | HK 01 | 1.0x", Subscription: "sub-a"}, {OriginalName: "HK 02 | 2.5x", DisplayName: "sub-a | HK 02 | 2.5x", Subscription: "sub-a"}, {OriginalName: "SG 01 | 0.5x", DisplayName: "sub-b | SG 01 | 0.5x", Subscription: "sub-b"}, } cfg := &Config{ ProxyGroups: []ProxyGroupRule{ {Name: "HK Group", Type: "select", Match: MatchRule{NamePattern: "HK"}}, }, Filters: []FilterRule{ {Name: "Remove Expensive", Compares: []CompareRule{ {Pattern: `([\d.]+)x`, Operator: ">", Value: 1.0}, }}, }, Patches: []PatchRule{ {Path: "rules", Op: "append", Value: []interface{}{"MATCH,DIRECT"}}, }, } // Simulates a proxy-group already merged in from a subscription, which // filters must also scrub even though proxy-groups rules never touch it. finalConfig := map[string]interface{}{ "rules": []interface{}{}, "proxy-groups": []interface{}{ map[string]interface{}{ "name": "sub-a | Auto", "type": "url-test", "proxies": []interface{}{ "sub-a | HK 01 | 1.0x", "sub-a | HK 02 | 2.5x", }, }, }, } errs := Apply(finalConfig, cfg, proxies) if len(errs) != 0 { t.Fatalf("unexpected errors: %v", errs) } groups, ok := finalConfig["proxy-groups"].([]interface{}) if !ok || len(groups) != 2 { t.Fatalf("expected 2 proxy-groups, got %#v", finalConfig["proxy-groups"]) } subGroup := groups[0].(map[string]interface{}) if subGroup["name"] != "sub-a | Auto" { t.Fatalf("unexpected first group: %#v", subGroup) } subProxies := subGroup["proxies"].([]interface{}) if len(subProxies) != 1 || subProxies[0] != "sub-a | HK 01 | 1.0x" { t.Fatalf("expected the expensive proxy scrubbed from the subscription group, got %#v", subProxies) } hkGroup := groups[1].(map[string]interface{}) if hkGroup["name"] != "HK Group" { t.Fatalf("unexpected second group: %#v", hkGroup) } hkProxies := hkGroup["proxies"].([]interface{}) if len(hkProxies) != 1 || hkProxies[0] != "sub-a | HK 01 | 1.0x" { t.Fatalf("expected the expensive proxy scrubbed from the built HK Group too, got %#v", hkProxies) } if rules, _ := finalConfig["rules"].([]interface{}); len(rules) != 1 || rules[0] != "MATCH,DIRECT" { t.Fatalf("unexpected rules after patch: %#v", finalConfig["rules"]) } } func TestApply_InvalidFilterDoesNotBlockOthers(t *testing.T) { proxies := []ProxyRef{ {OriginalName: "HK 01", DisplayName: "sub-a | HK 01", Subscription: "sub-a"}, } cfg := &Config{ ProxyGroups: []ProxyGroupRule{ {Name: "All", Type: "select"}, }, Filters: []FilterRule{ {Name: "Bad", Compares: []CompareRule{{Pattern: "(", Operator: "<="}}}, }, } finalConfig := map[string]interface{}{} errs := Apply(finalConfig, cfg, proxies) if len(errs) != 1 { t.Fatalf("expected exactly 1 error for the bad regexp, got %v", errs) } groups, ok := finalConfig["proxy-groups"].([]interface{}) if !ok || len(groups) != 1 { t.Fatalf("expected the valid proxy-groups rule to still run: %#v", finalConfig["proxy-groups"]) } } func TestApplyFilters_NameOnlyActsAsDenylist(t *testing.T) { proxies := []ProxyRef{ {OriginalName: "HK 01", DisplayName: "sub-a | HK 01", Subscription: "sub-a"}, {OriginalName: "SG 01", DisplayName: "sub-b | SG 01", Subscription: "sub-b"}, } finalConfig := map[string]interface{}{ "proxy-groups": []interface{}{ map[string]interface{}{ "name": "Auto", "proxies": []interface{}{"sub-a | HK 01", "sub-b | SG 01", "DIRECT"}, }, }, } errs := applyFilters(finalConfig, []FilterRule{ {Name: "No HK", Match: MatchRule{NamePattern: "^HK"}}, }, proxies) if len(errs) != 0 { t.Fatalf("unexpected errors: %v", errs) } names := finalConfig["proxy-groups"].([]interface{})[0].(map[string]interface{})["proxies"].([]interface{}) want := []interface{}{"sub-b | SG 01", "DIRECT"} if len(names) != 2 || names[0] != want[0] || names[1] != want[1] { t.Fatalf("expected HK proxy removed, DIRECT and SG left alone, got %#v", names) } } func TestApplyFilters_MultipleComparesAreANDed(t *testing.T) { proxies := []ProxyRef{ {OriginalName: "HK 01 | 1.0x | 50ms", DisplayName: "sub-a | HK 01 | 1.0x | 50ms", Subscription: "sub-a"}, {OriginalName: "HK 02 | 1.0x | 200ms", DisplayName: "sub-a | HK 02 | 1.0x | 200ms", Subscription: "sub-a"}, {OriginalName: "SG 01 | 2.5x | 50ms", DisplayName: "sub-b | SG 01 | 2.5x | 50ms", Subscription: "sub-b"}, } finalConfig := map[string]interface{}{ "proxy-groups": []interface{}{ map[string]interface{}{ "name": "Auto", "proxies": []interface{}{ "sub-a | HK 01 | 1.0x | 50ms", "sub-a | HK 02 | 1.0x | 200ms", "sub-b | SG 01 | 2.5x | 50ms", }, }, }, } errs := applyFilters(finalConfig, []FilterRule{ {Name: "Remove Cheap and Fast", Compares: []CompareRule{ {Pattern: `([\d.]+)x`, Operator: "<=", Value: 1.0}, {Pattern: `(\d+)ms`, Operator: "<=", Value: 100}, }}, }, proxies) if len(errs) != 0 { t.Fatalf("unexpected errors: %v", errs) } names := finalConfig["proxy-groups"].([]interface{})[0].(map[string]interface{})["proxies"].([]interface{}) want := []interface{}{"sub-a | HK 02 | 1.0x | 200ms", "sub-b | SG 01 | 2.5x | 50ms"} if len(names) != 2 || names[0] != want[0] || names[1] != want[1] { t.Fatalf("expected only the cheap+fast proxy removed (both conditions must hold), got %#v", names) } }