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.
610 lines
16 KiB
Go
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]
|
|
}
|