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.
600 lines
17 KiB
Go
600 lines
17 KiB
Go
package model
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
// ---------- target.go ----------
|
|
|
|
func TestNormalizeTarget(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
target string
|
|
ua string
|
|
want string
|
|
}{
|
|
// Explicit target takes priority
|
|
{"explicit mihomo", "mihomo", "sing-box/1.0", TargetMihomo},
|
|
{"explicit clash alias", "clash", "", TargetMihomo},
|
|
{"explicit clashmeta alias", "clashmeta", "", TargetMihomo},
|
|
{"explicit meta alias", "meta", "", TargetMihomo},
|
|
{"explicit stash", "stash", "", TargetStash},
|
|
{"explicit surge", "surge", "", TargetSurge},
|
|
{"explicit surge-mac", "surge-mac", "", TargetSurgeMac},
|
|
{"explicit surfboard", "surfboard", "", TargetSurfboard},
|
|
{"explicit loon", "loon", "", TargetLoon},
|
|
{"explicit egern", "egern", "", TargetEgern},
|
|
{"explicit shadowrocket", "shadowrocket", "", TargetShadowrocket},
|
|
{"explicit sr alias", "sr", "", TargetShadowrocket},
|
|
{"explicit qx", "qx", "", TargetQX},
|
|
{"explicit quanx alias", "quanx", "", TargetQX},
|
|
{"explicit sing-box", "sing-box", "", TargetSingBox},
|
|
{"explicit singbox alias", "singbox", "", TargetSingBox},
|
|
{"explicit sfa alias", "sfa", "", TargetSingBox},
|
|
{"explicit karing alias", "karing", "", TargetSingBox},
|
|
{"explicit v2ray", "v2ray", "", TargetV2ray},
|
|
{"explicit v2rayn alias", "v2rayn", "", TargetV2ray},
|
|
{"explicit v2rayng alias", "v2rayng", "", TargetV2ray},
|
|
{"explicit base64 alias", "base64", "", TargetV2ray},
|
|
{"explicit uri", "uri", "", TargetURI},
|
|
{"explicit uris alias", "uris", "", TargetURI},
|
|
{"explicit plain alias", "plain", "", TargetURI},
|
|
{"explicit text alias", "text", "", TargetURI},
|
|
{"explicit json", "json", "", TargetJSON},
|
|
{"explicit raw alias", "raw", "", TargetJSON},
|
|
|
|
// UA inference (no explicit target)
|
|
{"ua sing-box", "", "sing-box/1.7", TargetSingBox},
|
|
{"ua singbox", "", "SINGBOX", TargetSingBox},
|
|
{"ua v2ray", "", "v2rayN/6.0", TargetV2ray},
|
|
{"ua surge mac", "", "Surge/4.5 Mac", TargetSurgeMac},
|
|
{"ua surge no mac", "", "Surge/4.5", TargetSurge},
|
|
{"ua loon", "", "Loon/3.0", TargetLoon},
|
|
{"ua egern", "", "Egern/1.0", TargetEgern},
|
|
{"ua shadowrocket", "", "Shadowrocket/2.0", TargetShadowrocket},
|
|
{"ua quantumult", "", "Quantumult%20X/1.0", TargetQX},
|
|
{"ua stash", "", "Stash/2.0", TargetStash},
|
|
{"ua unknown defaults mihomo", "", "UnknownBrowser/1.0", TargetMihomo},
|
|
{"both empty", "", "", TargetMihomo},
|
|
|
|
// Case-insensitive target
|
|
{"uppercase target", "SURGE", "", TargetSurge},
|
|
{"mixed case target", "Sing-Box", "", TargetSingBox},
|
|
|
|
// Unknown target falls through to UA
|
|
{"unknown target with ua", "unknown", "surge/1.0", TargetSurge},
|
|
{"unknown target no ua", "unknown", "", TargetMihomo},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := NormalizeTarget(tt.target, tt.ua)
|
|
if got != tt.want {
|
|
t.Errorf("NormalizeTarget(%q, %q) = %q, want %q", tt.target, tt.ua, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormalizeTargetAlias(t *testing.T) {
|
|
// All aliases should map correctly
|
|
aliases := map[string]string{
|
|
"clash": TargetMihomo,
|
|
"clashmeta": TargetMihomo,
|
|
"clash-meta": TargetMihomo,
|
|
"meta": TargetMihomo,
|
|
"mihomo": TargetMihomo,
|
|
"stash": TargetStash,
|
|
"surge": TargetSurge,
|
|
"surgemac": TargetSurgeMac,
|
|
"surge-mac": TargetSurgeMac,
|
|
"surge_mac": TargetSurgeMac,
|
|
"surfboard": TargetSurfboard,
|
|
"loon": TargetLoon,
|
|
"egern": TargetEgern,
|
|
"shadowrocket": TargetShadowrocket,
|
|
"sr": TargetShadowrocket,
|
|
"qx": TargetQX,
|
|
"quanx": TargetQX,
|
|
"quantumult": TargetQX,
|
|
"quantumultx": TargetQX,
|
|
"quantumult-x": TargetQX,
|
|
"singbox": TargetSingBox,
|
|
"sing-box": TargetSingBox,
|
|
"sfa": TargetSingBox,
|
|
"karing": TargetSingBox,
|
|
"v2ray": TargetV2ray,
|
|
"v2rayn": TargetV2ray,
|
|
"v2rayng": TargetV2ray,
|
|
"base64": TargetV2ray,
|
|
"uri": TargetURI,
|
|
"uris": TargetURI,
|
|
"plain": TargetURI,
|
|
"text": TargetURI,
|
|
"json": TargetJSON,
|
|
"raw": TargetJSON,
|
|
}
|
|
for alias, want := range aliases {
|
|
t.Run("alias_"+alias, func(t *testing.T) {
|
|
if got := NormalizeTargetAlias(alias); got != want {
|
|
t.Errorf("NormalizeTargetAlias(%q) = %q, want %q", alias, got, want)
|
|
}
|
|
})
|
|
}
|
|
|
|
// nil / empty / unknown
|
|
if got := NormalizeTargetAlias(nil); got != "" {
|
|
t.Errorf("NormalizeTargetAlias(nil) = %q, want empty", got)
|
|
}
|
|
if got := NormalizeTargetAlias(""); got != "" {
|
|
t.Errorf("NormalizeTargetAlias(\"\") = %q, want empty", got)
|
|
}
|
|
if got := NormalizeTargetAlias("unknownalias"); got != "" {
|
|
t.Errorf("NormalizeTargetAlias(unknown) = %q, want empty", got)
|
|
}
|
|
// non-string type
|
|
if got := NormalizeTargetAlias(123); got != "" {
|
|
t.Errorf("NormalizeTargetAlias(123) = %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestIsTargetCompatible(t *testing.T) {
|
|
// mihomo/stash/json accept everything
|
|
if !IsTargetCompatible("ss", TargetMihomo) {
|
|
t.Error("mihomo should be compatible with ss")
|
|
}
|
|
if !IsTargetCompatible("anything", TargetStash) {
|
|
t.Error("stash should be compatible with anything")
|
|
}
|
|
if !IsTargetCompatible("anything", TargetJSON) {
|
|
t.Error("json should be compatible with anything")
|
|
}
|
|
|
|
// URI/V2ray/Shadowrocket
|
|
uriTypes := []string{"ss", "ssr", "vmess", "vless", "trojan", "hysteria", "hysteria2", "tuic", "anytls", "http", "socks5", "wireguard"}
|
|
for _, pt := range uriTypes {
|
|
if !IsTargetCompatible(pt, TargetURI) {
|
|
t.Errorf("URI should be compatible with %s", pt)
|
|
}
|
|
if !IsTargetCompatible(pt, TargetV2ray) {
|
|
t.Errorf("V2ray should be compatible with %s", pt)
|
|
}
|
|
if !IsTargetCompatible(pt, TargetShadowrocket) {
|
|
t.Errorf("Shadowrocket should be compatible with %s", pt)
|
|
}
|
|
}
|
|
if IsTargetCompatible("unknown", TargetURI) {
|
|
t.Error("URI should not be compatible with unknown")
|
|
}
|
|
|
|
// SingBox — note: no ssr in singbox list
|
|
if !IsTargetCompatible("ss", TargetSingBox) {
|
|
t.Error("sing-box should be compatible with ss")
|
|
}
|
|
if IsTargetCompatible("ssr", TargetSingBox) {
|
|
t.Error("sing-box should NOT be compatible with ssr")
|
|
}
|
|
|
|
// Surge
|
|
if !IsTargetCompatible("ss", TargetSurge) {
|
|
t.Error("surge should be compatible with ss")
|
|
}
|
|
if !IsTargetCompatible("snell", TargetSurge) {
|
|
t.Error("surge should be compatible with snell")
|
|
}
|
|
if IsTargetCompatible("ssr", TargetSurge) {
|
|
t.Error("surge should NOT be compatible with ssr")
|
|
}
|
|
|
|
// SurgeMac — has ssh and h2-connect
|
|
if !IsTargetCompatible("ssh", TargetSurgeMac) {
|
|
t.Error("surge-mac should be compatible with ssh")
|
|
}
|
|
if !IsTargetCompatible("h2-connect", TargetSurgeMac) {
|
|
t.Error("surge-mac should be compatible with h2-connect")
|
|
}
|
|
|
|
// Surfboard
|
|
if !IsTargetCompatible("ss", TargetSurfboard) {
|
|
t.Error("surfboard should be compatible with ss")
|
|
}
|
|
if IsTargetCompatible("hysteria2", TargetSurfboard) {
|
|
t.Error("surfboard should NOT be compatible with hysteria2")
|
|
}
|
|
|
|
// Loon
|
|
if !IsTargetCompatible("ssr", TargetLoon) {
|
|
t.Error("loon should be compatible with ssr")
|
|
}
|
|
if !IsTargetCompatible("wireguard", TargetLoon) {
|
|
t.Error("loon should be compatible with wireguard")
|
|
}
|
|
|
|
// QX
|
|
if !IsTargetCompatible("ssr", TargetQX) {
|
|
t.Error("qx should be compatible with ssr")
|
|
}
|
|
if !IsTargetCompatible("anytls", TargetQX) {
|
|
t.Error("qx should be compatible with anytls")
|
|
}
|
|
|
|
// Egern
|
|
if !IsTargetCompatible("ss", TargetEgern) {
|
|
t.Error("egern should be compatible with ss")
|
|
}
|
|
if IsTargetCompatible("ssr", TargetEgern) {
|
|
t.Error("egern should NOT be compatible with ssr")
|
|
}
|
|
|
|
// Unknown target
|
|
if IsTargetCompatible("ss", "unknown-target") {
|
|
t.Error("unknown target should not be compatible")
|
|
}
|
|
}
|
|
|
|
func TestGetTargetContentType(t *testing.T) {
|
|
tests := []struct {
|
|
target string
|
|
want string
|
|
}{
|
|
{TargetSingBox, "application/json; charset=utf-8"},
|
|
{TargetJSON, "application/json; charset=utf-8"},
|
|
{TargetV2ray, "text/plain; charset=utf-8"},
|
|
{TargetURI, "text/plain; charset=utf-8"},
|
|
{TargetSurge, "text/plain; charset=utf-8"},
|
|
{TargetSurgeMac, "text/plain; charset=utf-8"},
|
|
{TargetSurfboard, "text/plain; charset=utf-8"},
|
|
{TargetLoon, "text/plain; charset=utf-8"},
|
|
{TargetShadowrocket, "text/plain; charset=utf-8"},
|
|
{TargetQX, "text/plain; charset=utf-8"},
|
|
{TargetMihomo, "text/yaml; charset=utf-8"},
|
|
{TargetStash, "text/yaml; charset=utf-8"},
|
|
{"unknown", "text/yaml; charset=utf-8"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.target, func(t *testing.T) {
|
|
if got := GetTargetContentType(tt.target); got != tt.want {
|
|
t.Errorf("GetTargetContentType(%q) = %q, want %q", tt.target, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------- response.go ----------
|
|
|
|
func TestSuccessResponse(t *testing.T) {
|
|
resp := SuccessResponse("hello")
|
|
if resp.Status != "success" {
|
|
t.Errorf("Status = %q, want success", resp.Status)
|
|
}
|
|
if resp.Data != "hello" {
|
|
t.Errorf("Data = %v, want hello", resp.Data)
|
|
}
|
|
if resp.Error != nil {
|
|
t.Error("Error should be nil for success")
|
|
}
|
|
}
|
|
|
|
func TestFailedResponse(t *testing.T) {
|
|
resp := FailedResponse("bad request", 400)
|
|
if resp.Status != "failed" {
|
|
t.Errorf("Status = %q, want failed", resp.Status)
|
|
}
|
|
if resp.Data != nil {
|
|
t.Error("Data should be nil for failed")
|
|
}
|
|
if resp.Error == nil {
|
|
t.Fatal("Error should not be nil")
|
|
}
|
|
if resp.Error.Code != 400 {
|
|
t.Errorf("Error.Code = %d, want 400", resp.Error.Code)
|
|
}
|
|
if resp.Error.Message != "bad request" {
|
|
t.Errorf("Error.Message = %q, want bad request", resp.Error.Message)
|
|
}
|
|
}
|
|
|
|
func TestSuccessResponseJSON(t *testing.T) {
|
|
// Verify it serializes correctly
|
|
resp := SuccessResponse(map[string]int{"count": 3})
|
|
data, err := json.Marshal(resp)
|
|
if err != nil {
|
|
t.Fatalf("marshal error: %v", err)
|
|
}
|
|
var parsed APIResponse
|
|
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
t.Fatalf("unmarshal error: %v", err)
|
|
}
|
|
if parsed.Status != "success" {
|
|
t.Errorf("Status = %q, want success", parsed.Status)
|
|
}
|
|
}
|
|
|
|
func TestFailedResponseJSON(t *testing.T) {
|
|
resp := FailedResponse("not found", 404)
|
|
data, err := json.Marshal(resp)
|
|
if err != nil {
|
|
t.Fatalf("marshal error: %v", err)
|
|
}
|
|
s := string(data)
|
|
if !containsStr(s, "not found") {
|
|
t.Errorf("JSON should contain message: %s", s)
|
|
}
|
|
if !containsStr(s, "404") {
|
|
t.Errorf("JSON should contain code: %s", s)
|
|
}
|
|
}
|
|
|
|
// ---------- UnmarshalFilterRules ----------
|
|
|
|
func TestUnmarshalFilterRules(t *testing.T) {
|
|
t.Run("empty string", func(t *testing.T) {
|
|
rules := UnmarshalFilterRules("")
|
|
if len(rules) != 0 {
|
|
t.Errorf("empty string should return empty slice, got %d", len(rules))
|
|
}
|
|
})
|
|
|
|
t.Run("empty array", func(t *testing.T) {
|
|
rules := UnmarshalFilterRules("[]")
|
|
if len(rules) != 0 {
|
|
t.Errorf("[] should return empty slice, got %d", len(rules))
|
|
}
|
|
})
|
|
|
|
t.Run("valid rules", func(t *testing.T) {
|
|
data := `[{"type":"name","pattern":"HK"},{"type":"sort","direction":"asc"}]`
|
|
rules := UnmarshalFilterRules(data)
|
|
if len(rules) != 2 {
|
|
t.Fatalf("expected 2 rules, got %d", len(rules))
|
|
}
|
|
if rules[0].Type != "name" {
|
|
t.Errorf("rules[0].Type = %q, want name", rules[0].Type)
|
|
}
|
|
if rules[0].Pattern != "HK" {
|
|
t.Errorf("rules[0].Pattern = %q, want HK", rules[0].Pattern)
|
|
}
|
|
if rules[1].Type != "sort" {
|
|
t.Errorf("rules[1].Type = %q, want sort", rules[1].Type)
|
|
}
|
|
if rules[1].Direction != "asc" {
|
|
t.Errorf("rules[1].Direction = %q, want asc", rules[1].Direction)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid json", func(t *testing.T) {
|
|
rules := UnmarshalFilterRules("!!!invalid")
|
|
if len(rules) != 0 {
|
|
t.Errorf("invalid json should return empty slice, got %d", len(rules))
|
|
}
|
|
})
|
|
|
|
t.Run("null json", func(t *testing.T) {
|
|
rules := UnmarshalFilterRules("null")
|
|
if len(rules) != 0 {
|
|
t.Errorf("null should return empty slice, got %d", len(rules))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestUnmarshalMeta(t *testing.T) {
|
|
t.Run("empty string", func(t *testing.T) {
|
|
m := UnmarshalMeta("")
|
|
if len(m) != 0 {
|
|
t.Errorf("empty string should return empty map, got %d", len(m))
|
|
}
|
|
})
|
|
|
|
t.Run("empty object", func(t *testing.T) {
|
|
m := UnmarshalMeta("{}")
|
|
if len(m) != 0 {
|
|
t.Errorf("{} should return empty map, got %d", len(m))
|
|
}
|
|
})
|
|
|
|
t.Run("valid meta", func(t *testing.T) {
|
|
data := `{"key":"value","count":3}`
|
|
m := UnmarshalMeta(data)
|
|
if m["key"] != "value" {
|
|
t.Errorf("m[key] = %v, want value", m["key"])
|
|
}
|
|
// JSON numbers become float64
|
|
if c, ok := m["count"].(float64); !ok || c != 3 {
|
|
t.Errorf("m[count] = %v, want 3", m["count"])
|
|
}
|
|
})
|
|
|
|
t.Run("invalid json", func(t *testing.T) {
|
|
m := UnmarshalMeta("!!!invalid")
|
|
if len(m) != 0 {
|
|
t.Errorf("invalid json should return empty map, got %d", len(m))
|
|
}
|
|
})
|
|
|
|
t.Run("null json", func(t *testing.T) {
|
|
m := UnmarshalMeta("null")
|
|
if len(m) != 0 {
|
|
t.Errorf("null should return empty map, got %d", len(m))
|
|
}
|
|
})
|
|
}
|
|
|
|
// ---------- types.go (struct round-trips) ----------
|
|
|
|
func TestProxyNodeRoundTrip(t *testing.T) {
|
|
node := ProxyNode{
|
|
"type": "ss",
|
|
"name": "Test",
|
|
"server": "1.2.3.4",
|
|
"port": 8388,
|
|
}
|
|
data, err := json.Marshal(node)
|
|
if err != nil {
|
|
t.Fatalf("marshal error: %v", err)
|
|
}
|
|
var parsed ProxyNode
|
|
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
t.Fatalf("unmarshal error: %v", err)
|
|
}
|
|
if parsed["type"] != "ss" {
|
|
t.Errorf("type = %v, want ss", parsed["type"])
|
|
}
|
|
if parsed["name"] != "Test" {
|
|
t.Errorf("name = %v, want Test", parsed["name"])
|
|
}
|
|
}
|
|
|
|
func TestFilterRuleJSON(t *testing.T) {
|
|
rule := FilterRule{
|
|
Type: "name",
|
|
Pattern: "HK",
|
|
Args: map[string]any{"key": "val"},
|
|
Rules: []CustomRule{{Action: "set", Field: "name"}},
|
|
}
|
|
data, err := json.Marshal(rule)
|
|
if err != nil {
|
|
t.Fatalf("marshal error: %v", err)
|
|
}
|
|
var parsed FilterRule
|
|
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
t.Fatalf("unmarshal error: %v", err)
|
|
}
|
|
if parsed.Type != "name" {
|
|
t.Errorf("Type = %q, want name", parsed.Type)
|
|
}
|
|
if parsed.Pattern != "HK" {
|
|
t.Errorf("Pattern = %q, want HK", parsed.Pattern)
|
|
}
|
|
if len(parsed.Rules) != 1 {
|
|
t.Errorf("Rules len = %d, want 1", len(parsed.Rules))
|
|
}
|
|
}
|
|
|
|
func TestRecordStructsJSON(t *testing.T) {
|
|
// Verify SourceRecord, CollectionRecord, TemplateRecord, DownloadGrantRecord, RecycleBinRecord
|
|
// can marshal/unmarshal without errors.
|
|
t.Run("SourceRecord", func(t *testing.T) {
|
|
r := SourceRecord{ID: "s1", Name: "test", Type: "remote", URL: "http://example.com"}
|
|
data, err := json.Marshal(r)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
var parsed SourceRecord
|
|
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if parsed.ID != "s1" {
|
|
t.Errorf("ID = %q, want s1", parsed.ID)
|
|
}
|
|
})
|
|
|
|
t.Run("CollectionRecord", func(t *testing.T) {
|
|
r := CollectionRecord{ID: "c1", Name: "coll", SourceIds: []string{"s1", "s2"}}
|
|
data, err := json.Marshal(r)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
var parsed CollectionRecord
|
|
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if len(parsed.SourceIds) != 2 {
|
|
t.Errorf("SourceIds len = %d, want 2", len(parsed.SourceIds))
|
|
}
|
|
})
|
|
|
|
t.Run("TemplateRecord", func(t *testing.T) {
|
|
r := TemplateRecord{ID: "t1", Name: "tmpl", Target: "mihomo", Config: map[string]any{"k": "v"}}
|
|
data, err := json.Marshal(r)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
var parsed TemplateRecord
|
|
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if parsed.Target != "mihomo" {
|
|
t.Errorf("Target = %q, want mihomo", parsed.Target)
|
|
}
|
|
})
|
|
|
|
t.Run("DownloadGrantRecord", func(t *testing.T) {
|
|
r := DownloadGrantRecord{ID: "g1", ResourceType: "source", ResourceId: "s1"}
|
|
data, err := json.Marshal(r)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
var parsed DownloadGrantRecord
|
|
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if parsed.ResourceType != "source" {
|
|
t.Errorf("ResourceType = %q, want source", parsed.ResourceType)
|
|
}
|
|
})
|
|
|
|
t.Run("RecycleBinRecord", func(t *testing.T) {
|
|
r := RecycleBinRecord{ID: "r1", ResourceType: "source", ResourceId: "s1", Snapshot: map[string]any{"k": "v"}}
|
|
data, err := json.Marshal(r)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
var parsed RecycleBinRecord
|
|
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if parsed.ResourceType != "source" {
|
|
t.Errorf("ResourceType = %q, want source", parsed.ResourceType)
|
|
}
|
|
})
|
|
|
|
t.Run("AppConfig", func(t *testing.T) {
|
|
r := AppConfig{
|
|
Sources: []SourceRecord{{ID: "s1"}},
|
|
Collections: []CollectionRecord{{ID: "c1"}},
|
|
Templates: []TemplateRecord{{ID: "t1"}},
|
|
Settings: map[string]any{"k": "v"},
|
|
}
|
|
data, err := json.Marshal(r)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
var parsed AppConfig
|
|
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if len(parsed.Sources) != 1 {
|
|
t.Errorf("Sources len = %d, want 1", len(parsed.Sources))
|
|
}
|
|
})
|
|
|
|
t.Run("CustomRule", func(t *testing.T) {
|
|
r := CustomRule{Action: "set", Field: "name", Value: "newname"}
|
|
data, err := json.Marshal(r)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
var parsed CustomRule
|
|
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if parsed.Action != "set" {
|
|
t.Errorf("Action = %q, want set", parsed.Action)
|
|
}
|
|
})
|
|
}
|
|
|
|
// containsStr helper for string-in-string checks (avoids collision with target.go's contains)
|
|
func containsStr(s, substr string) bool {
|
|
return len(s) >= len(substr) && (s == substr || indexOfStr(s, substr) >= 0)
|
|
}
|
|
|
|
func indexOfStr(s, sub string) int {
|
|
for i := 0; i <= len(s)-len(sub); i++ {
|
|
if s[i:i+len(sub)] == sub {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|