583 lines
17 KiB
Go
583 lines
17 KiB
Go
package helpers
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"io"
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// ---------- formatFeedTime unit tests ----------
|
||
|
||
func TestCrossPlatformCoverageFormatFeedTimeInvalidJSON(t *testing.T) {
|
||
// JSON 解析失败 → 原样返回原始字符串
|
||
raw := "not-json"
|
||
got := formatFeedTime(raw)
|
||
s, ok := got.(string)
|
||
if !ok || s != raw {
|
||
t.Fatalf("formatFeedTime(invalid) = %v (%T), want raw string %q", got, got, raw)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageFormatFeedTimeEmptyInput(t *testing.T) {
|
||
// 空字符串 → JSON 解析失败 → 原样返回
|
||
got := formatFeedTime("")
|
||
s, ok := got.(string)
|
||
if !ok || s != "" {
|
||
t.Fatalf("formatFeedTime(\"\") = %v (%T), want \"\"", got, got)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageFormatFeedTimeNoFeedsKey(t *testing.T) {
|
||
// 合法 JSON 但没有 feeds 字段 → 返回解析后的 map
|
||
raw := `{"status":"ok","count":0}`
|
||
got := formatFeedTime(raw)
|
||
m, ok := got.(map[string]any)
|
||
if !ok {
|
||
t.Fatalf("formatFeedTime(no feeds) = %T, want map[string]any", got)
|
||
}
|
||
if m["status"] != "ok" {
|
||
t.Fatalf("status = %v, want ok", m["status"])
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageFormatFeedTimeFeedsNotArray(t *testing.T) {
|
||
// feeds 不是数组 → 返回解析后的 map
|
||
raw := `{"feeds":"unexpected-string"}`
|
||
got := formatFeedTime(raw)
|
||
m, ok := got.(map[string]any)
|
||
if !ok {
|
||
t.Fatalf("formatFeedTime(feeds string) = %T, want map[string]any", got)
|
||
}
|
||
if m["feeds"] != "unexpected-string" {
|
||
t.Fatalf("feeds = %v, want unexpected-string", m["feeds"])
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageFormatFeedTimeFeedsNull(t *testing.T) {
|
||
// feeds 为 null → 类型断言失败 → 返回解析后的 map
|
||
raw := `{"feeds":null}`
|
||
got := formatFeedTime(raw)
|
||
_, ok := got.(map[string]any)
|
||
if !ok {
|
||
t.Fatalf("formatFeedTime(feeds null) = %T, want map[string]any", got)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageFormatFeedTimeNonMapItem(t *testing.T) {
|
||
// feeds 数组元素不是 object → continue 跳过
|
||
raw := `{"feeds":["string-item", 42, null]}`
|
||
got := formatFeedTime(raw)
|
||
m, ok := got.(map[string]any)
|
||
if !ok {
|
||
t.Fatalf("expected map, got %T", got)
|
||
}
|
||
feeds := m["feeds"].([]any)
|
||
if len(feeds) != 3 {
|
||
t.Fatalf("feeds length = %d, want 3", len(feeds))
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageFormatFeedTimeTimeMissingOrInvalid(t *testing.T) {
|
||
// time 字段缺失或不是 float64 或 <= 0 → 跳过格式化
|
||
raw := `{"feeds":[
|
||
{"id":"a"},
|
||
{"id":"b","time":"not-a-number"},
|
||
{"id":"c","time":0},
|
||
{"id":"d","time":-1000}
|
||
]}`
|
||
got := formatFeedTime(raw)
|
||
m, ok := got.(map[string]any)
|
||
if !ok {
|
||
t.Fatalf("expected map, got %T", got)
|
||
}
|
||
feeds := m["feeds"].([]any)
|
||
for i, f := range feeds {
|
||
item := f.(map[string]any)
|
||
if _, hasTimeMs := item["timeMs"]; hasTimeMs {
|
||
t.Fatalf("feed[%d] should not have timeMs, got %v", i, item)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageFormatFeedTimeNormalPath(t *testing.T) {
|
||
// 正常路径:time 保留原始毫秒时间戳,timeFormatted 被格式化为北京时间
|
||
// 1750067400000 ms = 2025-06-16 17:50 Beijing time (UTC+8)
|
||
tsMs := float64(1750067400000)
|
||
raw := buildFeedJSON(t, []map[string]any{
|
||
{"time": tsMs, "type": float64(1), "id": "f1"},
|
||
})
|
||
|
||
got := formatFeedTime(raw)
|
||
m, ok := got.(map[string]any)
|
||
if !ok {
|
||
t.Fatalf("expected map, got %T", got)
|
||
}
|
||
feeds := m["feeds"].([]any)
|
||
if len(feeds) != 1 {
|
||
t.Fatalf("feeds length = %d, want 1", len(feeds))
|
||
}
|
||
item := feeds[0].(map[string]any)
|
||
|
||
// time 保留原始毫秒时间戳不变
|
||
if ms, ok := item["time"].(float64); !ok || ms != tsMs {
|
||
t.Fatalf("time = %v (%T), want original ms %v", item["time"], item["time"], tsMs)
|
||
}
|
||
|
||
// timeFormatted 被格式化为北京时间字符串(硬编码期望值)
|
||
timeStr, ok := item["timeFormatted"].(string)
|
||
if !ok {
|
||
t.Fatalf("timeFormatted = %T, want string", item["timeFormatted"])
|
||
}
|
||
if timeStr != "2025-06-16 17:50" {
|
||
t.Fatalf("timeFormatted = %q, want %q", timeStr, "2025-06-16 17:50")
|
||
}
|
||
|
||
// typeLabel 被填充
|
||
if item["typeLabel"] != "更新文档" {
|
||
t.Fatalf("typeLabel = %v, want 更新文档", item["typeLabel"])
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageFormatFeedTimeMultipleFeeds(t *testing.T) {
|
||
// 多个 feed,混合有效/无效时间戳
|
||
ts1 := float64(1750067400000)
|
||
ts2 := float64(1700000000000)
|
||
raw := buildFeedJSON(t, []map[string]any{
|
||
{"time": ts1, "type": float64(0), "id": "f1"},
|
||
{"time": ts2, "type": float64(12), "id": "f2"},
|
||
{"id": "f3"}, // 没有 time → 跳过
|
||
})
|
||
|
||
got := formatFeedTime(raw)
|
||
m := got.(map[string]any)
|
||
feeds := m["feeds"].([]any)
|
||
|
||
// 前两个有时间戳
|
||
for i := 0; i < 2; i++ {
|
||
item := feeds[i].(map[string]any)
|
||
if _, ok := item["time"].(float64); !ok {
|
||
t.Fatalf("feed[%d] time should remain float64, got %T", i, item["time"])
|
||
}
|
||
if _, ok := item["timeFormatted"].(string); !ok {
|
||
t.Fatalf("feed[%d] timeFormatted not string", i)
|
||
}
|
||
}
|
||
|
||
// 第三个没有时间戳
|
||
item3 := feeds[2].(map[string]any)
|
||
if _, hasTimeFormatted := item3["timeFormatted"]; hasTimeFormatted {
|
||
t.Fatalf("feed[2] should not have timeFormatted")
|
||
}
|
||
}
|
||
|
||
// ---------- enrichFeedFields unit tests ----------
|
||
|
||
func TestCrossPlatformCoverageEnrichFeedFieldsPreservesAllFields(t *testing.T) {
|
||
// enrichFeedFields 不删除任何字段,仅新增 typeLabel
|
||
item := map[string]any{
|
||
"parentDoc": map[string]any{"name": "parent"},
|
||
"userInfo": map[string]any{"nick": "user1"},
|
||
"id": "f1",
|
||
"type": float64(1),
|
||
}
|
||
enrichFeedFields(item)
|
||
if _, ok := item["parentDoc"]; !ok {
|
||
t.Fatal("parentDoc should be preserved")
|
||
}
|
||
if _, ok := item["userInfo"]; !ok {
|
||
t.Fatal("userInfo should be preserved")
|
||
}
|
||
if item["id"] != "f1" {
|
||
t.Fatal("id should be preserved")
|
||
}
|
||
if item["typeLabel"] != "更新文档" {
|
||
t.Fatalf("typeLabel = %v, want 更新文档", item["typeLabel"])
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageEnrichFeedFieldsTypeLabelKnown(t *testing.T) {
|
||
// 已知 type → 添加 typeLabel
|
||
for typeNum, expectedLabel := range feedTypeLabels {
|
||
item := map[string]any{"type": float64(typeNum)}
|
||
enrichFeedFields(item)
|
||
if item["typeLabel"] != expectedLabel {
|
||
t.Fatalf("type %d: typeLabel = %v, want %q", typeNum, item["typeLabel"], expectedLabel)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageEnrichFeedFieldsTypeLabelUnknown(t *testing.T) {
|
||
// 未知 type → 不添加 typeLabel
|
||
item := map[string]any{"type": float64(999)}
|
||
enrichFeedFields(item)
|
||
if _, ok := item["typeLabel"]; ok {
|
||
t.Fatal("typeLabel should not be set for unknown type 999")
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageEnrichFeedFieldsTypeNotFloat(t *testing.T) {
|
||
// type 不是 float64 → 不添加 typeLabel
|
||
item := map[string]any{"type": "string-type"}
|
||
enrichFeedFields(item)
|
||
if _, ok := item["typeLabel"]; ok {
|
||
t.Fatal("typeLabel should not be set for non-float type")
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageEnrichFeedFieldsUsersPreserved(t *testing.T) {
|
||
// users 数组中的 user 对象所有字段保留不变
|
||
item := map[string]any{
|
||
"users": []any{
|
||
map[string]any{"nick": "Alice", "avatarUrl": "http://img", "userId": "u1"},
|
||
map[string]any{"nick": "Bob", "email": "bob@test"},
|
||
},
|
||
}
|
||
enrichFeedFields(item)
|
||
users := item["users"].([]any)
|
||
u0 := users[0].(map[string]any)
|
||
if u0["avatarUrl"] != "http://img" {
|
||
t.Fatalf("users[0].avatarUrl should be preserved, got %v", u0["avatarUrl"])
|
||
}
|
||
if u0["userId"] != "u1" {
|
||
t.Fatalf("users[0].userId should be preserved, got %v", u0["userId"])
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageEnrichFeedFieldsUsersNonMapItem(t *testing.T) {
|
||
// users 数组中有非 map 元素 → 不 panic
|
||
item := map[string]any{
|
||
"users": []any{"not-a-map", 42, nil},
|
||
}
|
||
enrichFeedFields(item) // 不应 panic
|
||
users := item["users"].([]any)
|
||
if len(users) != 3 {
|
||
t.Fatalf("users length changed: %d", len(users))
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageEnrichFeedFieldsContentPreserved(t *testing.T) {
|
||
// content 和 content.doc 所有字段保留不变
|
||
item := map[string]any{
|
||
"content": map[string]any{
|
||
"doc": map[string]any{
|
||
"name": "测试文档",
|
||
"extension": "adoc",
|
||
"docKey": "dk123",
|
||
"url": "http://example",
|
||
},
|
||
"dentryKey": "entry1",
|
||
"workspaceKey": "ws1",
|
||
},
|
||
}
|
||
enrichFeedFields(item)
|
||
content := item["content"].(map[string]any)
|
||
if content["dentryKey"] != "entry1" {
|
||
t.Fatalf("content.dentryKey = %v, want entry1", content["dentryKey"])
|
||
}
|
||
if content["workspaceKey"] != "ws1" {
|
||
t.Fatalf("content.workspaceKey = %v, want ws1", content["workspaceKey"])
|
||
}
|
||
doc := content["doc"].(map[string]any)
|
||
if doc["name"] != "测试文档" {
|
||
t.Fatalf("doc.name = %v, want 测试文档", doc["name"])
|
||
}
|
||
if doc["docKey"] != "dk123" {
|
||
t.Fatalf("doc.docKey = %v, want dk123", doc["docKey"])
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageEnrichFeedFieldsNoContentNoUsers(t *testing.T) {
|
||
// 没有 content / users / type → 所有字段保留
|
||
item := map[string]any{
|
||
"id": "f1",
|
||
"parentDoc": map[string]any{"x": 1},
|
||
}
|
||
enrichFeedFields(item)
|
||
if _, ok := item["parentDoc"]; !ok {
|
||
t.Fatal("parentDoc should be preserved")
|
||
}
|
||
if item["id"] != "f1" {
|
||
t.Fatal("id should be preserved")
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageEnrichFeedFieldsEmptyItem(t *testing.T) {
|
||
// 空 map → 不 panic
|
||
item := map[string]any{}
|
||
enrichFeedFields(item)
|
||
if len(item) != 0 {
|
||
t.Fatalf("empty item changed: %v", item)
|
||
}
|
||
}
|
||
|
||
// ---------- RunE integration: feed list with formatFeedTime ----------
|
||
|
||
func TestCrossPlatformCoverageWikiFeedListFormatIntegration(t *testing.T) {
|
||
// 通过 scriptedToolCaller 返回真实结构的 feed JSON,验证 RunE 中
|
||
// callMCPToolReturnText → formatFeedTime → PrintJSON 完整路径(仅 --format json)
|
||
caller := &scriptedToolCaller{format: "json"}
|
||
ts := float64(1750067400000)
|
||
feedPayload := map[string]any{
|
||
"feeds": []any{
|
||
map[string]any{
|
||
"time": ts,
|
||
"type": float64(0),
|
||
"id": "feed1",
|
||
"parentDoc": map[string]any{"name": "parent"},
|
||
"userInfo": map[string]any{"nick": "user1", "avatarUrl": "http://img"},
|
||
"users": []any{
|
||
map[string]any{"nick": "Alice", "userId": "u1", "avatarUrl": "http://avatar"},
|
||
},
|
||
"content": map[string]any{
|
||
"doc": map[string]any{
|
||
"name": "测试文档",
|
||
"extension": "adoc",
|
||
"docKey": "dk123",
|
||
},
|
||
"dentryKey": "entry1",
|
||
"workspaceKey": "ws1",
|
||
},
|
||
},
|
||
map[string]any{
|
||
"time": float64(1700000000000),
|
||
"type": float64(7),
|
||
"id": "feed2",
|
||
},
|
||
},
|
||
}
|
||
payloadBytes, err := json.Marshal(feedPayload)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
caller.steps = []scriptedToolStep{{text: string(payloadBytes)}}
|
||
|
||
var out bytes.Buffer
|
||
oldDeps := deps
|
||
oldArgs := os.Args
|
||
InitDeps(caller)
|
||
deps.Out.w = &out
|
||
deps.Out.errW = io.Discard
|
||
t.Cleanup(func() {
|
||
deps = oldDeps
|
||
os.Args = oldArgs
|
||
})
|
||
|
||
root := newWikiCommand()
|
||
installExampleGlobalFlags(root)
|
||
root.SilenceErrors = true
|
||
root.SilenceUsage = true
|
||
root.SetOut(io.Discard)
|
||
root.SetErr(io.Discard)
|
||
root.SetIn(os.Stdin)
|
||
|
||
args := []string{"feed", "list", "--workspace", "ws1", "--exclude-file", "--format", "json"}
|
||
root.SetArgs(args)
|
||
os.Args = append([]string{"dws", "wiki"}, args...)
|
||
|
||
if err := root.Execute(); err != nil {
|
||
t.Fatalf("Execute: %v", err)
|
||
}
|
||
|
||
// 验证输出是合法 JSON 且包含格式化后的 feed
|
||
var parsed map[string]any
|
||
if err := json.Unmarshal(out.Bytes(), &parsed); err != nil {
|
||
t.Fatalf("output is not valid JSON: %v\noutput: %s", err, out.String())
|
||
}
|
||
feeds, ok := parsed["feeds"].([]any)
|
||
if !ok || len(feeds) != 2 {
|
||
t.Fatalf("feeds length unexpected: %v", parsed["feeds"])
|
||
}
|
||
|
||
// 第一个 feed 验证 time 保留、timeFormatted 格式化、所有原始字段保留
|
||
f0 := feeds[0].(map[string]any)
|
||
// time 保留原始毫秒时间戳
|
||
if f0["time"] != ts {
|
||
t.Fatalf("time = %v, want %v", f0["time"], ts)
|
||
}
|
||
// timeFormatted 被填充
|
||
if _, ok := f0["timeFormatted"].(string); !ok {
|
||
t.Fatalf("timeFormatted should be string, got %T", f0["timeFormatted"])
|
||
}
|
||
// 所有原始字段保留(不删除任何字段)
|
||
if _, ok := f0["parentDoc"]; !ok {
|
||
t.Fatal("parentDoc should be preserved")
|
||
}
|
||
if _, ok := f0["userInfo"]; !ok {
|
||
t.Fatal("userInfo should be preserved")
|
||
}
|
||
if f0["typeLabel"] != "创建文档" {
|
||
t.Fatalf("typeLabel = %v, want 创建文档", f0["typeLabel"])
|
||
}
|
||
|
||
// users 保留验证:avatarUrl / userId 均保留
|
||
if users, ok := f0["users"].([]any); ok {
|
||
u0 := users[0].(map[string]any)
|
||
if u0["avatarUrl"] != "http://avatar" {
|
||
t.Fatalf("users[0].avatarUrl should be preserved, got %v", u0["avatarUrl"])
|
||
}
|
||
if u0["userId"] != "u1" {
|
||
t.Fatalf("users[0].userId should be preserved, got %v", u0["userId"])
|
||
}
|
||
}
|
||
|
||
// content 结构保留验证
|
||
if content, ok := f0["content"].(map[string]any); ok {
|
||
if content["dentryKey"] != "entry1" {
|
||
t.Fatalf("content.dentryKey should be preserved, got %v", content["dentryKey"])
|
||
}
|
||
if content["workspaceKey"] != "ws1" {
|
||
t.Fatalf("content.workspaceKey should be preserved, got %v", content["workspaceKey"])
|
||
}
|
||
doc := content["doc"].(map[string]any)
|
||
if doc["docKey"] != "dk123" {
|
||
t.Fatalf("content.doc.docKey should be preserved, got %v", doc["docKey"])
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageWikiFeedListRawFormat(t *testing.T) {
|
||
// --format raw: 输出原始 MCP 文本,不经过 formatFeedTime 后处理
|
||
rawPayload := `{"feeds":[{"time":1750067400000,"type":1}]}`
|
||
caller := &scriptedToolCaller{
|
||
format: "raw",
|
||
steps: []scriptedToolStep{{text: rawPayload}},
|
||
}
|
||
|
||
var out bytes.Buffer
|
||
oldDeps := deps
|
||
oldArgs := os.Args
|
||
InitDeps(caller)
|
||
deps.Out.w = &out
|
||
deps.Out.errW = io.Discard
|
||
t.Cleanup(func() {
|
||
deps = oldDeps
|
||
os.Args = oldArgs
|
||
})
|
||
|
||
root := newWikiCommand()
|
||
installExampleGlobalFlags(root)
|
||
root.SilenceErrors = true
|
||
root.SilenceUsage = true
|
||
root.SetOut(io.Discard)
|
||
root.SetErr(io.Discard)
|
||
root.SetIn(os.Stdin)
|
||
|
||
args := []string{"feed", "list", "--workspace", "ws1", "--format", "raw"}
|
||
root.SetArgs(args)
|
||
os.Args = append([]string{"dws", "wiki"}, args...)
|
||
|
||
if err := root.Execute(); err != nil {
|
||
t.Fatalf("Execute: %v", err)
|
||
}
|
||
|
||
// raw 输出应包含原始文本,不经过 formatFeedTime 格式化
|
||
got := out.String()
|
||
if !bytes.Contains([]byte(got), []byte(`"feeds"`)) {
|
||
t.Fatalf("raw output should contain original payload, got: %s", got)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageWikiFeedListTableFormatMatchesDispatcher(t *testing.T) {
|
||
// --format table 回归:与共享 dispatcher (callMCPToolInternalOpts) 契约一致,
|
||
// 输出原始紧凑 MCP 文本,不做 pretty 化,也不注入 timeFormatted / typeLabel
|
||
rawPayload := `{"feeds":[{"time":1750067400000,"type":1,"id":"f1"}]}`
|
||
caller := &scriptedToolCaller{
|
||
format: "table",
|
||
steps: []scriptedToolStep{{text: rawPayload}},
|
||
}
|
||
|
||
var out bytes.Buffer
|
||
oldDeps := deps
|
||
oldArgs := os.Args
|
||
InitDeps(caller)
|
||
deps.Out.w = &out
|
||
deps.Out.errW = io.Discard
|
||
t.Cleanup(func() {
|
||
deps = oldDeps
|
||
os.Args = oldArgs
|
||
})
|
||
|
||
root := newWikiCommand()
|
||
installExampleGlobalFlags(root)
|
||
root.SilenceErrors = true
|
||
root.SilenceUsage = true
|
||
root.SetOut(io.Discard)
|
||
root.SetErr(io.Discard)
|
||
root.SetIn(os.Stdin)
|
||
|
||
args := []string{"feed", "list", "--workspace", "ws1", "--format", "table"}
|
||
root.SetArgs(args)
|
||
os.Args = append([]string{"dws", "wiki"}, args...)
|
||
|
||
if err := root.Execute(); err != nil {
|
||
t.Fatalf("Execute: %v", err)
|
||
}
|
||
|
||
// 与共享 dispatcher 一致:输出 = 原始 MCP 文本原文
|
||
if got := strings.TrimSpace(out.String()); got != rawPayload {
|
||
t.Fatalf("table output should equal raw MCP payload,\ngot: %s\nwant: %s", got, rawPayload)
|
||
}
|
||
// 不注入任何增强字段
|
||
for _, injected := range []string{"timeFormatted", "typeLabel"} {
|
||
if strings.Contains(out.String(), injected) {
|
||
t.Fatalf("table output should not inject %s, got: %s", injected, out.String())
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageWikiFeedListEmptyResponse(t *testing.T) {
|
||
// MCP 返回空结果 → formatFeedTime 处理空字符串 → 不报错
|
||
caller := &scriptedToolCaller{} // 无 steps → 空结果
|
||
|
||
var out bytes.Buffer
|
||
oldDeps := deps
|
||
oldArgs := os.Args
|
||
InitDeps(caller)
|
||
deps.Out.w = &out
|
||
deps.Out.errW = io.Discard
|
||
t.Cleanup(func() {
|
||
deps = oldDeps
|
||
os.Args = oldArgs
|
||
})
|
||
|
||
root := newWikiCommand()
|
||
installExampleGlobalFlags(root)
|
||
root.SilenceErrors = true
|
||
root.SilenceUsage = true
|
||
root.SetOut(io.Discard)
|
||
root.SetErr(io.Discard)
|
||
root.SetIn(os.Stdin)
|
||
|
||
args := []string{"feed", "list", "--workspace", "ws1"}
|
||
root.SetArgs(args)
|
||
os.Args = append([]string{"dws", "wiki"}, args...)
|
||
|
||
if err := root.Execute(); err != nil {
|
||
t.Fatalf("Execute: %v", err)
|
||
}
|
||
}
|
||
|
||
// ---------- helpers ----------
|
||
|
||
func buildFeedJSON(t *testing.T, feeds []map[string]any) string {
|
||
t.Helper()
|
||
payload := map[string]any{"feeds": feeds}
|
||
b, err := json.Marshal(payload)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return string(b)
|
||
}
|
||
|
||
func mustMarshal(t *testing.T, v any) []byte {
|
||
t.Helper()
|
||
b, err := json.Marshal(v)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return b
|
||
}
|