From 4a06944fec40845e424a726150e2a904edc25dce Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 5 Nov 2026 18:40:41 +0800 Subject: [PATCH] Verify multimodal content boundaries and every material sort --- internal/platform/content_test.go | 39 +++++++++++++++++++++++++++ internal/platform/integration_test.go | 15 +++++++++++ 2 files changed, 54 insertions(+) create mode 100644 internal/platform/content_test.go diff --git a/internal/platform/content_test.go b/internal/platform/content_test.go new file mode 100644 index 0000000..1cd761c --- /dev/null +++ b/internal/platform/content_test.go @@ -0,0 +1,39 @@ +package platform + +import ( + "encoding/json" + "testing" +) + +func TestContentValidation(t *testing.T) { + cases := []struct { + name, kind, body string + valid bool + }{ + {"plain text", "text", `{"text":""}`, true}, + {"empty article draft", "article", `{"article":{"sections":[]}}`, true}, + {"paragraph descriptions", "article", `{"article":{"sections":[{"id":"s","title":"Section","description":"Summary","paragraphs":[{"id":"p","text":"Body","description":"Notes"}]}]}}`, true}, + {"duplicate paragraph IDs", "article", `{"article":{"sections":[{"id":"s","paragraphs":[{"id":"p","text":"One"},{"id":"p","text":"Two"}]}]}}`, false}, + {"markdown field", "article", `{"markdown":"# Heading"}`, false}, + {"unknown schema", "text", `{"schema_version":2,"text":"Text"}`, false}, + {"null content", "board", `null`, false}, + {"nested unknown field", "image", `{"image":{"url":"https://example.com/image.png","secret":"unexpected"}}`, false}, + {"insecure image", "image", `{"image":{"url":"http://example.com/image.png"}}`, false}, + {"URL credentials", "image", `{"image":{"url":"https://user:password@example.com/image.png"}}`, false}, + {"card and text", "card", `{"parts":[{"kind":"text","text":"Caption"}]}`, true}, + {"empty card", "card", `{"parts":[]}`, false}, + {"mismatched part", "card", `{"parts":[{"kind":"text","image":{"url":"https://example.com/image.png"}}]}`, false}, + {"board payload", "board", `{"text":"Wrong type"}`, false}, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + out, ok := normalizeContent(test.kind, json.RawMessage(test.body)) + if ok != test.valid { + t.Fatalf("valid=%v, want %v", ok, test.valid) + } + if ok && !json.Valid(out) { + t.Fatal("normalized content is invalid JSON") + } + }) + } +} diff --git a/internal/platform/integration_test.go b/internal/platform/integration_test.go index 3f2496d..036a5a4 100644 --- a/internal/platform/integration_test.go +++ b/internal/platform/integration_test.go @@ -168,6 +168,21 @@ func TestOnboardingAndSharing(t *testing.T) { call("DELETE", workspacePath+"/tags/"+tag["id"].(string), tokens[0], nil, 204) call("PATCH", textPath, tokens[0], changed, 412, `"3"`) + imageContent := response{"image": response{"url": "https://assets.example.com/image.png", "alt": "Photo"}} + call("POST", workspacePath+"/materials", tokens[0], response{"kind": "image", "title": "Photo", "content": imageContent}, 201) + call("POST", workspacePath+"/materials", tokens[0], response{"kind": "card", "title": "Mixed card", "content": response{"parts": []any{ + response{"kind": "text", "text": "Caption"}, + response{"kind": "article", "article": articleContent["article"]}, + response{"kind": "image", "image": imageContent["image"]}, + }}}, 201) + for _, sort := range []string{"modified", "created", "alphabetical", "opened", "tag"} { + call("GET", workspacePath+"/materials?sort="+sort+"&limit=2", tokens[0], nil, 200) + } + images := call("GET", workspacePath+"/materials?kind=image&q=Photo", tokens[0], nil, 200) + if len(images["items"].([]any)) != 1 { + t.Fatal("kind/title filter returned incorrect materials") + } + email := "fixture@example.invalid" password := "a-long-fixture-password" call("POST", base+"/auth/register", "", response{"email": email, "password": password}, 202)