package routing import ( "encoding/json" "testing" ) // TestValidateEmptyErrorsSerializeAsArray guards the JSON shape API clients // depend on: a valid profile's Validate() must marshal to `[]`, not `null`. func TestValidateEmptyErrorsSerializeAsArray(t *testing.T) { raw, err := json.Marshal(Default().Validate()) if err != nil { t.Fatal(err) } if string(raw) != "[]" { t.Fatalf("want `[]`, got %s", raw) } } func TestValidate(t *testing.T) { ok := Default() ok.Rules = []Rule{{Type: "domain_suffix", Value: "example.com", Action: "direct", Enabled: true}} e := ok.Validate() if len(e) != 0 { t.Fatalf("valid profile got errors %v", e) } if e == nil { t.Fatal("Validate must return a non-nil empty slice (serializes to [] not null)") } bad := Default() bad.Mode = "weird" // 非法 mode bad.Final = "reject" // final 只能 proxy|direct bad.Rules = []Rule{ {Type: "ip_cidr", Value: "not-a-cidr", Action: "proxy", Enabled: true}, // CIDR 非法 {Type: "geosite", Value: "netflix", Action: "direct", Enabled: true}, // 不在白名单(仅 cn) {Type: "bogus", Value: "x", Action: "direct", Enabled: true}, // type 非法 } errs := bad.Validate() if len(errs) < 5 { t.Fatalf("want >=5 field errors, got %d: %v", len(errs), errs) } } func TestValidateDirectIPCIDRReservedGuard(t *testing.T) { // direct 的 ip_cidr 会并入 TUN route_exclude_address(系统层之下),catch-all 或 // 与隧道/DNS 保留段(172.16.0.0/12)重叠会静默旁路 hijack-dns/隧道 → 必须拒绝。 rejected := []Rule{ {Type: "ip_cidr", Value: "0.0.0.0/0", Action: "direct", Enabled: true}, {Type: "ip_cidr", Value: "::/0", Action: "direct", Enabled: true}, {Type: "ip_cidr", Value: "172.16.0.0/12", Action: "direct", Enabled: true}, {Type: "ip_cidr", Value: "172.19.0.0/16", Action: "direct", Enabled: true}, // 含隧道 DNS 172.19.0.2 } for _, r := range rejected { p := Default() p.Rules = []Rule{r} if errs := p.Validate(); len(errs) == 0 { t.Errorf("direct ip_cidr %q 应被拒,却无报错", r.Value) } } // proxy/reject 不入排除表故不受限;不与保留段重叠的 direct 允许。 allowed := []Rule{ {Type: "ip_cidr", Value: "0.0.0.0/0", Action: "proxy", Enabled: true}, {Type: "ip_cidr", Value: "10.0.0.0/8", Action: "direct", Enabled: true}, {Type: "ip_cidr", Value: "8.8.8.8/32", Action: "direct", Enabled: true}, } for _, r := range allowed { p := Default() p.Rules = []Rule{r} if errs := p.Validate(); len(errs) != 0 { t.Errorf("ip_cidr %q action=%s 应允许,却报错 %v", r.Value, r.Action, errs) } } } func TestValidateCountLimit(t *testing.T) { p := Default() for i := 0; i < 201; i++ { p.Rules = append(p.Rules, Rule{Type: "domain", Value: "a.com", Action: "proxy", Enabled: true}) } if e := p.Validate(); len(e) == 0 { t.Fatal("want count-limit error") } } func TestNormalizeDedupAndTrim(t *testing.T) { p := Default() p.Rules = []Rule{ {Type: "domain", Value: " a.com ", Action: "proxy", Enabled: true}, {Type: "domain", Value: "a.com", Action: "proxy", Enabled: false}, {Type: "domain", Value: "b.com", Action: "proxy", Enabled: true}, } p.Normalize() if len(p.Rules) != 2 { t.Fatalf("want 2 rules after dedup, got %d: %v", len(p.Rules), p.Rules) } if p.Rules[0].Value != "a.com" || p.Rules[1].Value != "b.com" { t.Fatalf("want first-occurrence order preserved, got %v", p.Rules) } } // TestNormalizeDedupNoSeparatorCollision guards against the historical // string-concatenation dedup key ("type|value|action"): two distinct rules // whose Value contains "|" could concatenate to the same string even though // (type, value, action) differ. The struct-keyed dedup must tell them apart. func TestNormalizeDedupNoSeparatorCollision(t *testing.T) { p := Default() p.Rules = []Rule{ {Type: "domain", Value: "a|b", Action: "proxy", Enabled: true}, {Type: "domain", Value: "a", Action: "b|proxy", Enabled: true}, } p.Normalize() if len(p.Rules) != 2 { t.Fatalf("want 2 distinct rules preserved (no false collision), got %d: %v", len(p.Rules), p.Rules) } }