Files
sub-store/internal/template/builtin_test.go
T
rogee cec95ff4e8 test: add comprehensive unit tests — coverage 20.2% → 91.8%
Add 12 test files covering all internal packages:

- internal/util/util_test.go (97.2%)
- internal/model/model_test.go (100.0%)
- internal/config/config_test.go (88.1%)
- internal/template/builtin_test.go (98.8%)
- internal/middleware/middleware_test.go (98.7%)
- internal/database/repo_extra_test.go (85.5%)
- internal/rules/converter_test.go (99.1%)
- internal/service/subscription_test.go (75.7%)
- internal/handler/handler_test.go (90.6%)
- internal/filter/filter_extra_test.go (91.9%)
- internal/proxy/client_parser_test.go (96.1%)
- internal/render/render_extra_test.go (99.2%)

Overall: 91.8% (4033/4400 statements) — exceeds 85% acceptance threshold.
All tests pass, go vet clean, go build clean.
2026-07-27 16:12:28 +08:00

610 lines
16 KiB
Go

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]
}