package template import ( "testing" ) func TestBuiltinTemplateIDs(t *testing.T) { expectedIDs := []string{ "mihomo-basic", "acl4ssr-mihomo", "acl4ssr-mihomo-no-emoji", "loyalsoldier-whitelist", "loyalsoldier-blacklist", "ai-streaming-mihomo", } if len(BuiltinTemplateIDs) != len(expectedIDs) { t.Errorf("BuiltinTemplateIDs has %d entries, want %d", len(BuiltinTemplateIDs), len(expectedIDs)) } for _, id := range expectedIDs { if !BuiltinTemplateIDs[id] { t.Errorf("BuiltinTemplateIDs[%q] = false, want true", id) } } // Non-builtin should be false if BuiltinTemplateIDs["nonexistent"] { t.Error("BuiltinTemplateIDs[nonexistent] = true, want false") } } func TestDefaultTemplateID(t *testing.T) { if DefaultTemplateID != "acl4ssr-mihomo" { t.Errorf("DefaultTemplateID = %q, want acl4ssr-mihomo", DefaultTemplateID) } } func TestBuiltinTemplates(t *testing.T) { if len(BuiltinTemplates) != 6 { t.Fatalf("len(BuiltinTemplates) = %d, want 6", len(BuiltinTemplates)) } seenIDs := map[string]bool{} for _, tmpl := range BuiltinTemplates { if tmpl.ID == "" { t.Error("found template with empty ID") } if seenIDs[tmpl.ID] { t.Errorf("duplicate template ID: %s", tmpl.ID) } seenIDs[tmpl.ID] = true if tmpl.Name == "" { t.Errorf("template %s has empty Name", tmpl.ID) } if tmpl.Target != "mihomo" { t.Errorf("template %s Target = %q, want mihomo", tmpl.ID, tmpl.Target) } if tmpl.Config == nil { t.Errorf("template %s has nil Config", tmpl.ID) } // All builtin template IDs should be in BuiltinTemplateIDs if !BuiltinTemplateIDs[tmpl.ID] { t.Errorf("template %s not in BuiltinTemplateIDs", tmpl.ID) } // All configs should have proxyGroups if _, ok := tmpl.Config["proxyGroups"]; !ok { t.Errorf("template %s Config missing proxyGroups", tmpl.ID) } // All configs should have rules if _, ok := tmpl.Config["rules"]; !ok { t.Errorf("template %s Config missing rules", tmpl.ID) } // All configs should have ruleProviders if _, ok := tmpl.Config["ruleProviders"]; !ok { t.Errorf("template %s Config missing ruleProviders", tmpl.ID) } } } func TestDefaultProxyGroups(t *testing.T) { groups := DefaultProxyGroups() if len(groups) != 3 { t.Fatalf("DefaultProxyGroups() returned %d groups, want 3", len(groups)) } // First group: 节ç‚č选择 g0 := groups[0] if g0["name"] != "🚀 节ç‚č选择" { t.Errorf("groups[0].name = %v, want 🚀 节ç‚č选择", g0["name"]) } if g0["type"] != "select" { t.Errorf("groups[0].type = %v, want select", g0["type"]) } proxies0, ok := g0["proxies"].([]any) if !ok { t.Fatalf("groups[0].proxies is not []any") } if len(proxies0) != 3 { t.Errorf("groups[0].proxies len = %d, want 3", len(proxies0)) } // Second group: è‡ȘćŠšé€‰æ‹© (url-test) g1 := groups[1] if g1["name"] != "♻ è‡ȘćŠšé€‰æ‹©" { t.Errorf("groups[1].name = %v, want ♻ è‡ȘćŠšé€‰æ‹©", g1["name"]) } if g1["type"] != "url-test" { t.Errorf("groups[1].type = %v, want url-test", g1["type"]) } if g1["url"] != testURL { t.Errorf("groups[1].url = %v, want %s", g1["url"], testURL) } if g1["interval"] != 300 { t.Errorf("groups[1].interval = %v, want 300", g1["interval"]) } if g1["tolerance"] != 50 { t.Errorf("groups[1].tolerance = %v, want 50", g1["tolerance"]) } // Third group: æ‰‹ćŠšćˆ‡æą g2 := groups[2] if g2["name"] != "🚀 æ‰‹ćŠšćˆ‡æą" { t.Errorf("groups[2].name = %v, want 🚀 æ‰‹ćŠšćˆ‡æą", g2["name"]) } if g2["type"] != "select" { t.Errorf("groups[2].type = %v, want select", g2["type"]) } } func TestNormalizeMihomoTemplateConfig(t *testing.T) { t.Run("copies kebab-case aliases to camelCase", func(t *testing.T) { input := map[string]any{ "mixed-port": 7890, "allow-lan": true, "log-level": "info", "proxy-groups": []any{"group1"}, "rule-providers": map[string]any{"rp1": "val"}, "mode": "rule", } output := NormalizeMihomoTemplateConfig(input) // camelCase keys should be set if output["mixedPort"] != 7890 { t.Errorf("mixedPort = %v, want 7890", output["mixedPort"]) } if output["allowLan"] != true { t.Errorf("allowLan = %v, want true", output["allowLan"]) } if output["logLevel"] != "info" { t.Errorf("logLevel = %v, want info", output["logLevel"]) } if output["proxyGroups"] == nil { t.Error("proxyGroups should be set") } if output["ruleProviders"] == nil { t.Error("ruleProviders should be set") } // kebab-case keys should be deleted if _, ok := output["mixed-port"]; ok { t.Error("mixed-port should be deleted") } if _, ok := output["allow-lan"]; ok { t.Error("allow-lan should be deleted") } if _, ok := output["log-level"]; ok { t.Error("log-level should be deleted") } if _, ok := output["proxy-groups"]; ok { t.Error("proxy-groups should be deleted") } if _, ok := output["rule-providers"]; ok { t.Error("rule-providers should be deleted") } // Non-aliased keys should be preserved if output["mode"] != "rule" { t.Errorf("mode = %v, want rule", output["mode"]) } }) t.Run("does not overwrite existing camelCase", func(t *testing.T) { input := map[string]any{ "mixed-port": 7890, "mixedPort": 8080, // already set, should not be overwritten } output := NormalizeMihomoTemplateConfig(input) if output["mixedPort"] != 8080 { t.Errorf("mixedPort = %v, want 8080 (existing should be preserved)", output["mixedPort"]) } }) t.Run("empty input", func(t *testing.T) { output := NormalizeMihomoTemplateConfig(map[string]any{}) if len(output) != 0 { t.Errorf("empty input should produce empty output, got %d keys", len(output)) } }) t.Run("input without aliases preserved as-is", func(t *testing.T) { input := map[string]any{ "mode": "rule", "mixedPort": 7890, } output := NormalizeMihomoTemplateConfig(input) if output["mode"] != "rule" { t.Errorf("mode = %v, want rule", output["mode"]) } if output["mixedPort"] != 7890 { t.Errorf("mixedPort = %v, want 7890", output["mixedPort"]) } }) } func TestStripEmojiViaNoEmojiConfig(t *testing.T) { // Find the no-emoji template var noEmojiCfg map[string]any for _, tmpl := range BuiltinTemplates { if tmpl.ID == "acl4ssr-mihomo-no-emoji" { noEmojiCfg = tmpl.Config break } } if noEmojiCfg == nil { t.Fatal("acl4ssr-mihomo-no-emoji template not found") } // proxyGroups names should not contain emoji groups, ok := noEmojiCfg["proxyGroups"].([]map[string]any) if !ok { t.Fatal("proxyGroups is not []map[string]any") } for _, g := range groups { name, ok := g["name"].(string) if !ok { t.Error("group name is not a string") continue } for from := range emojiLabelMap { if contains(name, from) { t.Errorf("group name %q still contains emoji label %q", name, from) } } // Check that the stripped version exists in the map if stripped, ok := emojiLabelMap[from(name)]; ok && stripped != "" { _ = stripped // just ensuring map access } } // Rules should not contain emoji labels either rules, ok := noEmojiCfg["rules"].([]any) if !ok { t.Fatal("rules is not []any") } for _, r := range rules { if s, ok := r.(string); ok { for from := range emojiLabelMap { if contains(s, from) { t.Errorf("rule %q still contains emoji label %q", s, from) } } } } } func TestStripEmojiDirectly(t *testing.T) { // Test stripEmoji on various types t.Run("string with emoji", func(t *testing.T) { got := stripEmoji("🚀 节ç‚č选择") if got != "节ç‚č选择" { t.Errorf("stripEmoji(🚀 节ç‚č选择) = %q, want 节ç‚č选择", got) } }) t.Run("string without emoji", func(t *testing.T) { got := stripEmoji("plain text") if got != "plain text" { t.Errorf("stripEmoji(plain text) = %q, want plain text", got) } }) t.Run("map", func(t *testing.T) { m := map[string]any{"name": "🚀 节ç‚č选择", "other": "keep"} got := stripEmoji(m).(map[string]any) if got["name"] != "节ç‚č选择" { t.Errorf("map name = %v, want 节ç‚č选择", got["name"]) } if got["other"] != "keep" { t.Errorf("map other = %v, want keep", got["other"]) } }) t.Run("[]any", func(t *testing.T) { arr := []any{"🚀 节ç‚č选择", "plain"} got := stripEmoji(arr).([]any) if got[0] != "节ç‚č选择" { t.Errorf("arr[0] = %v, want 节ç‚č选择", got[0]) } if got[1] != "plain" { t.Errorf("arr[1] = %v, want plain", got[1]) } }) t.Run("[]map[string]any", func(t *testing.T) { arr := []map[string]any{{"name": "🚀 节ç‚č选择"}} got := stripEmoji(arr).([]map[string]any) if got[0]["name"] != "节ç‚č选择" { t.Errorf("arr[0].name = %v, want 节ç‚č选择", got[0]["name"]) } }) t.Run("other type passthrough", func(t *testing.T) { got := stripEmoji(42) if got != 42 { t.Errorf("stripEmoji(42) = %v, want 42", got) } got = stripEmoji(true) if got != true { t.Errorf("stripEmoji(true) = %v, want true", got) } }) } func TestProvider(t *testing.T) { t.Run("default behavior classical", func(t *testing.T) { p := provider("https://example.com/rules.yaml", "") if p["type"] != "http" { t.Errorf("type = %v, want http", p["type"]) } if p["behavior"] != "classical" { t.Errorf("behavior = %v, want classical (default)", p["behavior"]) } if p["url"] != "https://example.com/rules.yaml" { t.Errorf("url = %v, want https://example.com/rules.yaml", p["url"]) } if p["interval"] != 86400 { t.Errorf("interval = %v, want 86400", p["interval"]) } if p["path"] != "./ruleset/rules.yaml" { t.Errorf("path = %v, want ./ruleset/rules.yaml", p["path"]) } }) t.Run("custom behavior domain", func(t *testing.T) { p := provider("https://example.com/rules.txt", "domain") if p["behavior"] != "domain" { t.Errorf("behavior = %v, want domain", p["behavior"]) } }) } func TestLastPathSegment(t *testing.T) { tests := []struct { input string want string }{ {"https://example.com/rules.yaml", "rules.yaml"}, {"https://example.com/path/to/file.txt", "file.txt"}, // No slash → returns "ruleset" (the fallback) {"noslash", "ruleset"}, {"", "ruleset"}, {"https://example.com/", ""}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { got := lastPathSegment(tt.input) if got != tt.want { t.Errorf("lastPathSegment(%q) = %q, want %q", tt.input, got, tt.want) } }) } } func TestURLBuilders(t *testing.T) { // acl4ssrRaw got := acl4ssrRaw("BanAD") want := "https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/BanAD.list" if got != want { t.Errorf("acl4ssrRaw(BanAD) = %q, want %q", got, want) } // loyalSoldier got = loyalSoldier("reject") want = "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/reject.txt" if got != want { t.Errorf("loyalSoldier(reject) = %q, want %q", got, want) } // blackmatrix got = blackmatrix("OpenAI") want = "https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Clash/OpenAI/OpenAI.yaml" if got != want { t.Errorf("blackmatrix(OpenAI) = %q, want %q", got, want) } } func TestMihomoBasicConfig(t *testing.T) { cfg := mihomoBasicConfig() if cfg["mixedPort"] != 7890 { t.Errorf("mixedPort = %v, want 7890", cfg["mixedPort"]) } if cfg["mode"] != "rule" { t.Errorf("mode = %v, want rule", cfg["mode"]) } if cfg["logLevel"] != "info" { t.Errorf("logLevel = %v, want info", cfg["logLevel"]) } rules, ok := cfg["rules"].([]any) if !ok { t.Fatal("rules not []any") } if len(rules) == 0 { t.Error("rules should not be empty") } // Should contain GEOIP and MATCH foundGeoIP := false foundMatch := false for _, r := range rules { if s, ok := r.(string); ok { if contains(s, "GEOIP") { foundGeoIP = true } if contains(s, "MATCH") { foundMatch = true } } } if !foundGeoIP { t.Error("rules should contain GEOIP") } if !foundMatch { t.Error("rules should contain MATCH") } } func TestAcl4ssrConfig(t *testing.T) { cfg := acl4ssrConfig() rps, ok := cfg["ruleProviders"].(map[string]any) if !ok { t.Fatal("ruleProviders not a map") } // Should have multiple providers if len(rps) < 10 { t.Errorf("ruleProviders len = %d, want >= 10", len(rps)) } // Check specific providers if _, ok := rps["BanAD"]; !ok { t.Error("ruleProviders missing BanAD") } if _, ok := rps["ProxyGFWlist"]; !ok { t.Error("ruleProviders missing ProxyGFWlist") } } func TestLoyalsoldierConfigs(t *testing.T) { t.Run("whitelist", func(t *testing.T) { cfg := loyalsoldierWhitelistConfig() rps := cfg["ruleProviders"].(map[string]any) if _, ok := rps["reject"]; !ok { t.Error("whitelist ruleProviders missing reject") } rules := cfg["rules"].([]any) // Last rule should be MATCH last := rules[len(rules)-1].(string) if !contains(last, "MATCH") { t.Errorf("whitelist last rule = %q, want MATCH", last) } }) t.Run("blacklist", func(t *testing.T) { cfg := loyalsoldierBlacklistConfig() rps := cfg["ruleProviders"].(map[string]any) if _, ok := rps["reject"]; !ok { t.Error("blacklist ruleProviders missing reject") } rules := cfg["rules"].([]any) last := rules[len(rules)-1].(string) if !contains(last, "MATCH") { t.Errorf("blacklist last rule = %q, want MATCH", last) } }) } func TestAIStreamingConfig(t *testing.T) { cfg := aiStreamingConfig() rps := cfg["ruleProviders"].(map[string]any) if _, ok := rps["OpenAI"]; !ok { t.Error("ai-streaming ruleProviders missing OpenAI") } if _, ok := rps["Claude"]; !ok { t.Error("ai-streaming ruleProviders missing Claude") } if _, ok := rps["Gemini"]; !ok { t.Error("ai-streaming ruleProviders missing Gemini") } if _, ok := rps["Netflix"]; !ok { t.Error("ai-streaming ruleProviders missing Netflix") } } func TestReplaceAll(t *testing.T) { tests := []struct { s, old, new, want string }{ {"hello world", "world", "go", "hello go"}, {"aaa", "a", "b", "bbb"}, {"no match", "xyz", "abc", "no match"}, {"", "a", "b", ""}, {"ababab", "ab", "x", "xxx"}, } for _, tt := range tests { t.Run(tt.s, func(t *testing.T) { got := replaceAll(tt.s, tt.old, tt.new) if got != tt.want { t.Errorf("replaceAll(%q, %q, %q) = %q, want %q", tt.s, tt.old, tt.new, got, tt.want) } }) } } func TestIndexOf(t *testing.T) { tests := []struct { s, sub string want int }{ {"hello", "ll", 2}, {"hello", "x", -1}, {"hello", "hello", 0}, {"hello", "o", 4}, {"", "a", -1}, {"abc", "abcd", -1}, } for _, tt := range tests { t.Run(tt.s+"_"+tt.sub, func(t *testing.T) { got := indexOf(tt.s, tt.sub) if got != tt.want { t.Errorf("indexOf(%q, %q) = %d, want %d", tt.s, tt.sub, got, tt.want) } }) } } func TestBaseGroups(t *testing.T) { groups := baseGroups() if len(groups) < 3 { t.Errorf("baseGroups len = %d, want >= 3", len(groups)) } // Check first group has the expected name if groups[0]["name"] != "🚀 节ç‚č选择" { t.Errorf("baseGroups[0].name = %v, want 🚀 节ç‚č选择", groups[0]["name"]) } } func TestDefaultDNS(t *testing.T) { dns := defaultDNS() if dns["enable"] != true { t.Errorf("enable = %v, want true", dns["enable"]) } if dns["ipv6"] != false { t.Errorf("ipv6 = %v, want false", dns["ipv6"]) } if dns["enhanced-mode"] != "fake-ip" { t.Errorf("enhanced-mode = %v, want fake-ip", dns["enhanced-mode"]) } ns, ok := dns["nameserver"].([]any) if !ok { t.Fatal("nameserver not []any") } if len(ns) != 2 { t.Errorf("nameserver len = %d, want 2", len(ns)) } } func TestMihomoBase(t *testing.T) { base := mihomoBase() if base["mixedPort"] != 7890 { t.Errorf("mixedPort = %v, want 7890", base["mixedPort"]) } if base["allowLan"] != false { t.Errorf("allowLan = %v, want false", base["allowLan"]) } if base["mode"] != "rule" { t.Errorf("mode = %v, want rule", base["mode"]) } if _, ok := base["dns"].(map[string]any); !ok { t.Error("dns should be a map") } if _, ok := base["proxyGroups"].([]map[string]any); !ok { t.Error("proxyGroups should be []map[string]any") } } // contains helper func contains(s, substr string) bool { return len(s) >= len(substr) && indexOf(s, substr) >= 0 } // from helper — reverse lookup for emojiLabelMap values var emojiLabelMapReverse map[string]string func init() { emojiLabelMapReverse = make(map[string]string, len(emojiLabelMap)) for k, v := range emojiLabelMap { emojiLabelMapReverse[v] = k } } func from(s string) string { return emojiLabelMapReverse[s] }