package service import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "time" "github.com/peterqiu0516/sub-store/internal/model" ) // --- BuildSubscriptionResult with local sources --- func TestBuildSubscriptionResult_LocalSource(t *testing.T) { source := &model.SourceRecord{ ID: "test-src", Name: "Test", Type: "local", Content: "ss://pass@host:80#Node1\nss://pass@host:81#Node2", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}, } result, err := BuildSubscriptionResult(context.Background(), BuildOptions{ Source: source, Sources: []model.SourceRecord{*source}, Target: "json", }) if err != nil { t.Fatalf("build failed: %v", err) } if result.Nodes != 2 { t.Errorf("expected 2 nodes, got %d", result.Nodes) } if result.Body == "" { t.Error("expected non-empty body") } } func TestBuildSubscriptionResult_RealityVlessSubscription(t *testing.T) { content := strings.Join([]string{ "vless://00000000-0000-0000-0000-000000000001@8.220.220.41:443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=dl.google.com&fp=chrome&pbk=public-key-1&sid=short-id-1&spx=%2F&type=tcp&headerType=none#ali-seoul", "vless://00000000-0000-0000-0000-000000000002@8.216.16.28:443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=www.yahoo.co.jp&fp=chrome&pbk=public-key-2&sid=short-id-2&spx=%2F&type=tcp&headerType=none#Ali-Tokyo", "vless://00000000-0000-0000-0000-000000000003@67.215.229.50:443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=www.cloudflare.com&fp=chrome&pbk=public-key-3&sid=short-id-3&spx=%2F&type=tcp&headerType=none#racknerd-la", }, "\n") source := &model.SourceRecord{ID: "reality", Type: "local", Content: content, Enabled: true} result, err := BuildSubscriptionResult(context.Background(), BuildOptions{Source: source, Target: "json"}) if err != nil { t.Fatalf("build failed: %v", err) } if result.OriginalNodes != 3 || result.Nodes != 3 { t.Fatalf("nodes = %d/%d, want 3/3", result.OriginalNodes, result.Nodes) } var output struct { Proxies []model.ProxyNode `json:"proxies"` } if err := json.Unmarshal([]byte(result.Body), &output); err != nil { t.Fatalf("decode output: %v", err) } for _, node := range output.Proxies { if node["type"] != "vless" || node["flow"] != "xtls-rprx-vision" || node["network"] != "tcp" { t.Fatalf("VLESS fields lost: %#v", node) } if reality, ok := node["reality-opts"].(map[string]any); !ok || reality["public-key"] == "" || reality["short-id"] == "" { t.Fatalf("Reality fields lost: %#v", node["reality-opts"]) } } } func TestBuildSubscriptionResult_RemoteFetchError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { http.Error(w, "blocked", http.StatusForbidden) })) defer server.Close() source := &model.SourceRecord{ID: "blocked", Name: "blocked", Type: "remote", URL: server.URL, Enabled: true} _, err := BuildSubscriptionResult(context.Background(), BuildOptions{Source: source, Target: "json"}) if err == nil || !strings.Contains(err.Error(), "Remote source blocked failed: 403") { t.Fatalf("error = %v, want upstream 403", err) } } func TestBuildSubscriptionResult_NoEnabledSources(t *testing.T) { source := &model.SourceRecord{ ID: "test-src", Name: "Test", Type: "local", Content: "ss://pass@host:80#Node1", Enabled: false, Filters: []model.FilterRule{}, Meta: map[string]any{}, } _, err := BuildSubscriptionResult(context.Background(), BuildOptions{ Source: source, Sources: []model.SourceRecord{*source}, Target: "json", }) if err == nil { t.Error("expected error for no enabled sources") } } func TestBuildSubscriptionResult_EmptyContent(t *testing.T) { source := &model.SourceRecord{ ID: "test-src", Name: "Test", Type: "local", Content: "", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}, } _, err := BuildSubscriptionResult(context.Background(), BuildOptions{ Source: source, Sources: []model.SourceRecord{*source}, Target: "json", }) if err == nil { t.Error("expected error for empty content (no nodes)") } } func TestBuildSubscriptionResult_Collection(t *testing.T) { src1 := model.SourceRecord{ ID: "src1", Name: "Src1", Type: "local", Content: "ss://pass@host:80#Node1", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}, } src2 := model.SourceRecord{ ID: "src2", Name: "Src2", Type: "local", Content: "ss://pass@host:81#Node2", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}, } collection := &model.CollectionRecord{ ID: "col1", Name: "Collection", SourceIds: []string{"src1", "src2"}, TemplateId: "acl4ssr-mihomo", Enabled: true, Meta: map[string]any{}, } result, err := BuildSubscriptionResult(context.Background(), BuildOptions{ Collection: collection, Sources: []model.SourceRecord{src1, src2}, Target: "json", }) if err != nil { t.Fatalf("build failed: %v", err) } if result.Nodes != 2 { t.Errorf("expected 2 nodes, got %d", result.Nodes) } } func TestBuildSubscriptionResult_CollectionIgnoreFailed(t *testing.T) { src1 := model.SourceRecord{ ID: "src1", Name: "Src1", Type: "local", Content: "ss://pass@host:80#Node1", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}, } src2 := model.SourceRecord{ ID: "src2", Name: "Src2", Type: "local", Content: "", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}, } collection := &model.CollectionRecord{ ID: "col1", Name: "Collection", SourceIds: []string{"src1", "src2"}, TemplateId: "acl4ssr-mihomo", IgnoreFailed: true, Enabled: true, Meta: map[string]any{}, } result, err := BuildSubscriptionResult(context.Background(), BuildOptions{ Collection: collection, Sources: []model.SourceRecord{src1, src2}, Target: "json", }) if err != nil { t.Fatalf("build failed: %v", err) } // src2 has empty content so produces 0 nodes, but IgnoreFailed should still return src1's node if result.Nodes != 1 { t.Errorf("expected 1 node, got %d", result.Nodes) } } func TestBuildSubscriptionResult_WithFilters(t *testing.T) { source := &model.SourceRecord{ ID: "test-src", Name: "Test", Type: "local", Content: "ss://pass@host:80#HK-Node\nss://pass@host:81#US-Node", Enabled: true, Filters: []model.FilterRule{ {Type: "include", Field: "name", Pattern: "HK"}, }, Meta: map[string]any{}, } result, err := BuildSubscriptionResult(context.Background(), BuildOptions{ Source: source, Sources: []model.SourceRecord{*source}, Target: "json", }) if err != nil { t.Fatalf("build failed: %v", err) } if result.Nodes != 1 { t.Errorf("expected 1 node after filter, got %d", result.Nodes) } if result.OriginalNodes != 2 { t.Errorf("expected 2 original nodes, got %d", result.OriginalNodes) } } func TestBuildSubscriptionResult_MihomoTarget(t *testing.T) { source := &model.SourceRecord{ ID: "test-src", Name: "Test", Type: "local", Content: "ss://pass@host:80#Node1", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}, } result, err := BuildSubscriptionResult(context.Background(), BuildOptions{ Source: source, Sources: []model.SourceRecord{*source}, Target: "mihomo", TemplateConfig: map[string]any{}, }) if err != nil { t.Fatalf("build failed: %v", err) } if result.Body == "" { t.Error("expected non-empty body") } } // --- getSources --- func TestGetSources_WithSource(t *testing.T) { src := &model.SourceRecord{ID: "s1", Name: "S1"} opts := BuildOptions{Source: src} result := getSources(opts) if len(result) != 1 || result[0].ID != "s1" { t.Errorf("expected [s1], got %v", result) } } func TestGetSources_NoSourceNoCollection(t *testing.T) { opts := BuildOptions{} result := getSources(opts) if len(result) != 0 { t.Errorf("expected empty, got %v", result) } } func TestGetSources_CollectionWithSourceIds(t *testing.T) { col := &model.CollectionRecord{ID: "c1", SourceIds: []string{"s1", "s2"}} sources := []model.SourceRecord{ {ID: "s1", Name: "Source1"}, {ID: "s2", Name: "Source2"}, {ID: "s3", Name: "Source3"}, } opts := BuildOptions{Collection: col, Sources: sources} result := getSources(opts) if len(result) != 2 { t.Errorf("expected 2 sources, got %d", len(result)) } } func TestGetSources_CollectionByName(t *testing.T) { col := &model.CollectionRecord{ID: "c1", SourceIds: []string{"Source1"}} sources := []model.SourceRecord{ {ID: "s1", Name: "Source1"}, } opts := BuildOptions{Collection: col, Sources: sources} result := getSources(opts) if len(result) != 1 { t.Errorf("expected 1 source matched by name, got %d", len(result)) } } func TestGetSources_CollectionEmptySourceIds(t *testing.T) { col := &model.CollectionRecord{ID: "c1", SourceIds: []string{}} sources := []model.SourceRecord{{ID: "s1"}} opts := BuildOptions{Collection: col, Sources: sources} result := getSources(opts) if len(result) != 1 { t.Errorf("expected 1 source (fallback to all), got %d", len(result)) } } // --- Helper functions --- func TestGetInt(t *testing.T) { if toInt(42) != 42 { t.Error("expected 42 for int") } if toInt(int64(42)) != 42 { t.Error("expected 42 for int64") } if toInt(float64(42.7)) != 42 { t.Error("expected 42 for float64") } if toInt("42") != 42 { t.Error("expected 42 for string") } if toInt("abc") != 0 { t.Error("expected 0 for invalid string") } if toInt(nil) != 0 { t.Error("expected 0 for nil") } } func TestClamp(t *testing.T) { if clamp(5, 1, 10) != 5 { t.Error("expected 5") } if clamp(0, 1, 10) != 1 { t.Error("expected 1 (min)") } if clamp(20, 1, 10) != 10 { t.Error("expected 10 (max)") } } func TestGetString(t *testing.T) { if getString("hello", "default") != "hello" { t.Error("expected hello") } if getString("", "default") != "default" { t.Error("expected default") } if getString(nil, "default") != "default" { t.Error("expected default for nil") } if getString(42, "default") != "default" { t.Error("expected default for non-string") } } func TestGetStringFromMap(t *testing.T) { m := map[string]any{"key": "value", "num": 42} if getStringFromMap(m, "key") != "value" { t.Error("expected value") } if getStringFromMap(m, "num") != "" { t.Error("expected empty for non-string") } if getStringFromMap(m, "missing") != "" { t.Error("expected empty for missing") } } func TestGetSourceUserAgent(t *testing.T) { // From sub.Meta["ua"] ua := getSourceUserAgent(model.SourceRecord{Meta: map[string]any{"ua": "custom-ua"}}, BuildOptions{}) if ua != "custom-ua" { t.Errorf("expected custom-ua, got %s", ua) } // From sub.Meta["userAgent"] ua = getSourceUserAgent(model.SourceRecord{Meta: map[string]any{"userAgent": "ua2"}}, BuildOptions{}) if ua != "ua2" { t.Errorf("expected ua2, got %s", ua) } // From settings ua = getSourceUserAgent(model.SourceRecord{Meta: map[string]any{}}, BuildOptions{Settings: map[string]any{"defaultUserAgent": "settings-ua"}}) if ua != "settings-ua" { t.Errorf("expected settings-ua, got %s", ua) } // Default ua = getSourceUserAgent(model.SourceRecord{Meta: map[string]any{}}, BuildOptions{}) if ua != "clash.meta/v1.19.24" { t.Errorf("expected default ua, got %s", ua) } } func TestGetCacheTtl(t *testing.T) { // From sub.Meta ttl := getCacheTtl(model.SourceRecord{Meta: map[string]any{"cacheTtl": 600}}, BuildOptions{}) if ttl != 600 { t.Errorf("expected 600, got %d", ttl) } // From settings ttl = getCacheTtl(model.SourceRecord{Meta: map[string]any{}}, BuildOptions{Settings: map[string]any{"remoteCacheTtl": 120}}) if ttl != 120 { t.Errorf("expected 120, got %d", ttl) } // Default ttl = getCacheTtl(model.SourceRecord{Meta: map[string]any{}}, BuildOptions{}) if ttl != 300 { t.Errorf("expected 300, got %d", ttl) } // Clamp to max 3600 ttl = getCacheTtl(model.SourceRecord{Meta: map[string]any{"cacheTtl": 99999}}, BuildOptions{}) if ttl != 3600 { t.Errorf("expected 3600 (clamped), got %d", ttl) } // Zero or negative -> default ttl = getCacheTtl(model.SourceRecord{Meta: map[string]any{"cacheTtl": 0}}, BuildOptions{}) if ttl != 300 { t.Errorf("expected 300 default, got %d", ttl) } } func TestGetTimeout(t *testing.T) { if d := getTimeout(map[string]any{"defaultTimeout": 5000}); d != 5*time.Second { t.Errorf("expected 5s, got %v", d) } if d := getTimeout(map[string]any{"defaultTimeout": 100}); d != 1*time.Second { t.Errorf("expected 1s (clamped), got %v", d) } if d := getTimeout(map[string]any{"defaultTimeout": 999999}); d != 120*time.Second { t.Errorf("expected 120s (clamped), got %v", d) } if d := getTimeout(nil); d != 30*time.Second { t.Errorf("expected 30s default, got %v", d) } } func TestGetConcurrency(t *testing.T) { if c := getConcurrency(map[string]any{"backendRequestConcurrency": 5}); c != 5 { t.Errorf("expected 5, got %d", c) } if c := getConcurrency(map[string]any{"backendRequestConcurrency": 99}); c != 12 { t.Errorf("expected 12 (clamped), got %d", c) } if c := getConcurrency(nil); c != 3 { t.Errorf("expected 3 default, got %d", c) } } func TestGetConcurrencyWait(t *testing.T) { if w := getConcurrencyWait(map[string]any{"backendRequestConcurrencyWaitTime": 100}); w != 100*time.Millisecond { t.Errorf("expected 100ms, got %v", w) } if w := getConcurrencyWait(nil); w != 0 { t.Errorf("expected 0, got %v", w) } } // --- splitSourceUrls --- func TestSplitSourceUrls(t *testing.T) { urls := splitSourceUrls("https://a.com\nhttp://b.com\nnot-a-url\nhttps://c.com") if len(urls) != 3 { t.Errorf("expected 3 urls, got %d", len(urls)) } } func TestSplitSourceUrls_Empty(t *testing.T) { urls := splitSourceUrls("") if len(urls) != 0 { t.Errorf("expected 0 urls, got %d", len(urls)) } } // --- Metadata helpers --- func TestMetadataFromSource(t *testing.T) { meta := metadataFromSource(model.SourceRecord{ Meta: map[string]any{ "subUserinfo": "upload=1;download=2;total=3", "profileWebPageUrl": "https://example.com", "profileUpdateInterval": "24", }, }) if meta.SubscriptionUserinfo != "upload=1;download=2;total=3" { t.Errorf("expected userinfo, got %s", meta.SubscriptionUserinfo) } if meta.ProfileWebPageUrl != "https://example.com" { t.Errorf("expected url, got %s", meta.ProfileWebPageUrl) } if meta.CacheStatus != "disabled" { t.Errorf("expected disabled, got %s", meta.CacheStatus) } } func TestMetadataFromSource_NilMeta(t *testing.T) { meta := metadataFromSource(model.SourceRecord{}) if meta.CacheStatus != "disabled" { t.Errorf("expected disabled, got %s", meta.CacheStatus) } } func TestMetadataFromSource_AlternateKeys(t *testing.T) { meta := metadataFromSource(model.SourceRecord{ Meta: map[string]any{ "subscriptionUserinfo": "alt-info", "appUrl": "https://app.example.com", }, }) if meta.SubscriptionUserinfo != "alt-info" { t.Errorf("expected alt-info, got %s", meta.SubscriptionUserinfo) } if meta.ProfileWebPageUrl != "https://app.example.com" { t.Errorf("expected app url, got %s", meta.ProfileWebPageUrl) } } func TestMetadataToMap(t *testing.T) { m := model.SubscriptionResponseMetadata{ SubscriptionUserinfo: "info", ProfileWebPageUrl: "url", ProfileUpdateInterval: "6", ContentDisposition: "disp", Etag: "etag", LastModified: "mod", } result := metadataToMap(m) if result["subscriptionUserinfo"] != "info" { t.Error("expected info") } if result["etag"] != "etag" { t.Error("expected etag") } } func TestSelectResponseMetadata(t *testing.T) { sources := []model.SourceRecord{{ID: "s1"}, {ID: "s2"}} metaMap := map[string]model.SubscriptionResponseMetadata{ "s1": {SubscriptionUserinfo: "from-s1"}, "s2": {SubscriptionUserinfo: "from-s2"}, } meta := selectResponseMetadata(sources, metaMap) if meta.SubscriptionUserinfo != "from-s1" { t.Errorf("expected from-s1, got %s", meta.SubscriptionUserinfo) } } func TestSelectResponseMetadata_Empty(t *testing.T) { meta := selectResponseMetadata([]model.SourceRecord{}, map[string]model.SubscriptionResponseMetadata{}) if meta.SubscriptionUserinfo != "" { t.Error("expected empty") } } // --- Concurrency --- func TestRunWithConcurrency(t *testing.T) { tasks := make([]func() (int, error), 10) for i := range tasks { i := i tasks[i] = func() (int, error) { return i, nil } } results, err := RunWithConcurrency(tasks, 3, 0) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(results) != 10 { t.Fatalf("expected 10 results, got %d", len(results)) } for i, v := range results { if v != i { t.Errorf("expected %d, got %d", i, v) } } } func TestRunWithConcurrency_WithErrors(t *testing.T) { taskErr := errors.New("task error") tasks := make([]func() (int, error), 5) for i := range tasks { i := i if i == 2 { tasks[i] = func() (int, error) { return 0, taskErr } } else { tasks[i] = func() (int, error) { return i, nil } } } results, err := RunWithConcurrency(tasks, 2, 0) if !errors.Is(err, taskErr) { t.Fatalf("error = %v, want task error", err) } if len(results) != 5 { t.Fatalf("expected 5 results, got %d", len(results)) } } func TestRunWithConcurrency_Empty(t *testing.T) { results, err := RunWithConcurrency([]func() (int, error){}, 3, 0) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(results) != 0 { t.Errorf("expected 0 results, got %d", len(results)) } } func TestRunWithConcurrency_ConcurrencyOne(t *testing.T) { var counter int32 tasks := make([]func() (int, error), 5) for i := range tasks { i := i tasks[i] = func() (int, error) { atomic.AddInt32(&counter, 1) return i, nil } } results, _ := RunWithConcurrency(tasks, 1, 0) if len(results) != 5 { t.Errorf("expected 5 results, got %d", len(results)) } if atomic.LoadInt32(&counter) != 5 { t.Errorf("expected counter 5, got %d", counter) } } func TestRunSettledWithConcurrency(t *testing.T) { tasks := make([]func() (int, error), 5) for i := range tasks { i := i if i == 2 { tasks[i] = func() (int, error) { return 0, errors.New("rejected") } } else { tasks[i] = func() (int, error) { return i, nil } } } results := RunSettledWithConcurrency(tasks, 2, 0) if len(results) != 5 { t.Fatalf("expected 5 results, got %d", len(results)) } if results[2].Status != "rejected" { t.Errorf("expected rejected at index 2, got %s", results[2].Status) } fulfilled := 0 for _, r := range results { if r.Status == "fulfilled" { fulfilled++ } } if fulfilled != 4 { t.Errorf("expected 4 fulfilled, got %d", fulfilled) } } func TestRunSettledWithConcurrency_Empty(t *testing.T) { results := RunSettledWithConcurrency([]func() (int, error){}, 3, 0) if len(results) != 0 { t.Errorf("expected 0 results, got %d", len(results)) } } // --- AsyncWriter --- func TestAsyncWriter_WriteAndWait(t *testing.T) { aw := NewAsyncWriter() var counter int32 for i := 0; i < 5; i++ { aw.Write(func() { atomic.AddInt32(&counter, 1) }) } // Give goroutines time to execute before Wait cancels the context time.Sleep(100 * time.Millisecond) aw.Wait() if atomic.LoadInt32(&counter) != 5 { t.Errorf("expected counter 5, got %d", counter) } } func TestAsyncWriter_WaitWithTimeout(t *testing.T) { aw := NewAsyncWriter() var counter int32 for i := 0; i < 5; i++ { aw.Write(func() { atomic.AddInt32(&counter, 1) }) } time.Sleep(100 * time.Millisecond) aw.WaitWithTimeout(5 * time.Second) if atomic.LoadInt32(&counter) != 5 { t.Errorf("expected counter 5, got %d", counter) } } func TestAsyncWriter_WaitWithTimeout_TimedOut(t *testing.T) { aw := NewAsyncWriter() // Write a task that sleeps longer than timeout aw.Write(func() { time.Sleep(2 * time.Second) }) // WaitWithTimeout should return without hanging aw.WaitWithTimeout(50 * time.Millisecond) // Test passes if it doesn't hang } // --- loadSubscriptionRaw --- func TestLoadSubscriptionRaw_LocalSource(t *testing.T) { sub := model.SourceRecord{ Type: "local", Content: "ss://pass@host:80#Node1", } raw, meta, err := loadSubscriptionRaw(context.Background(), sub, BuildOptions{}) if err != nil { t.Fatalf("unexpected error: %v", err) } if raw != "ss://pass@host:80#Node1" { t.Errorf("expected content, got %s", raw) } if meta.CacheStatus != "disabled" { t.Errorf("expected disabled, got %s", meta.CacheStatus) } } func TestLoadSubscriptionRaw_RemoteNoUrls(t *testing.T) { sub := model.SourceRecord{ Type: "remote", URL: "", } raw, _, err := loadSubscriptionRaw(context.Background(), sub, BuildOptions{}) if err != nil { t.Fatalf("unexpected error: %v", err) } if raw != "" { t.Errorf("expected empty, got %s", raw) } } func TestLoadSubscriptionRaw_ContentPresent(t *testing.T) { // Content present takes priority even for remote sub := model.SourceRecord{ Type: "remote", Content: "ss://pass@host:80#Node1", URL: "https://example.com/sub", } raw, _, err := loadSubscriptionRaw(context.Background(), sub, BuildOptions{}) if err != nil { t.Fatalf("unexpected error: %v", err) } if raw != "ss://pass@host:80#Node1" { t.Errorf("expected content only, got %s", raw) } } // --- Render via BuildSubscriptionResult for various targets --- func TestBuildSubscriptionResult_URITarget(t *testing.T) { source := &model.SourceRecord{ ID: "test-src", Name: "Test", Type: "local", Content: "ss://pass@host:80#Node1", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}, } result, err := BuildSubscriptionResult(context.Background(), BuildOptions{ Source: source, Sources: []model.SourceRecord{*source}, Target: "uri", }) if err != nil { t.Fatalf("build failed: %v", err) } if result.Body == "" { t.Error("expected non-empty body") } } func TestBuildSubscriptionResult_V2rayTarget(t *testing.T) { source := &model.SourceRecord{ ID: "test-src", Name: "Test", Type: "local", Content: "ss://pass@host:80#Node1", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}, } result, err := BuildSubscriptionResult(context.Background(), BuildOptions{ Source: source, Sources: []model.SourceRecord{*source}, Target: "v2ray", }) if err != nil { t.Fatalf("build failed: %v", err) } if result.Body == "" { t.Error("expected non-empty body") } } func TestBuildSubscriptionResult_SingBoxTarget(t *testing.T) { source := &model.SourceRecord{ ID: "test-src", Name: "Test", Type: "local", Content: "ss://pass@host:80#Node1", Enabled: true, Filters: []model.FilterRule{}, Meta: map[string]any{}, } result, err := BuildSubscriptionResult(context.Background(), BuildOptions{ Source: source, Sources: []model.SourceRecord{*source}, Target: "sing-box", }) if err != nil { t.Fatalf("build failed: %v", err) } if result.Body == "" { t.Error("expected non-empty body") } }