package bdd import ( "strings" "testing" "go-doc/storage" ) // TestFeatureBadgedCommentInjection covers: // // Given an existing documentation page // When a developer adds a comment with a developer badge // Then it should be appended to the JSON array and rendered with a red visual badge func TestFeatureBadgedCommentInjection(t *testing.T) { Run(t, Feature{ Name: "Badged comment injection", Scenarios: []Scenario{ { Name: "a developer comment is appended to the JSON array with a red badge", Given: Steps{ {Text: "an existing documentation page for Core Service API", Do: func(c *Context) error { _, err := c.Engine.SaveDoc("Core Service API", "# Core Service API\n\nBilling endpoints.\n") return err }}, }, When: Steps{ {Text: "a developer posts a comment through POST /api/docs/core-service-api/comments", Do: func(c *Context) error { response, payload, err := c.Do( "POST", "/api/docs/core-service-api/comments", "application/json", `{"author":"ana","role":"Developer","content":"The retry budget is undocumented."}`, ) if err != nil { return err } if err := ExpectStatus(response, 201); err != nil { return errf("%w (body: %s)", err, payload) } created, err := Decode[struct { Comment storage.Comment `json:"comment"` Badge string `json:"badge"` }](payload) if err != nil { return err } c.Remember("comment", created.Comment) c.Remember("badge", created.Badge) return nil }}, }, Then: Steps{ {Text: "the comment is stored with the developer role", Do: func(c *Context) error { comment := c.RecallAs[storage.Comment]("comment") return ExpectEqual(comment.Role, storage.RoleDeveloper, "stored role") }}, {Text: "the comment carries the red 🔴 developer badge", Do: func(c *Context) error { return ExpectEqual(c.RecallAs[string]("badge"), badgeContract[storage.RoleDeveloper], "response badge") }}, {Text: "data/comments/core-service-api-comments.json is a JSON array containing it", Do: func(c *Context) error { raw, err := c.RawJSON("core-service-api") if err != nil { return err } if !strings.HasPrefix(strings.TrimLeft(string(raw), " \n\t"), JSONArrayPrefix) { return errf("feedback file is not a JSON array: %q", string(raw)) } comments, err := c.RawComments("core-service-api") if err != nil { return err } if len(comments) != 1 { return errf("expected 1 stored comment, got %d", len(comments)) } stored := comments[0] if stored.ID == "" { return errf("stored comment has no ID") } if stored.Author != "ana" { return errf("expected author ana, got %q", stored.Author) } if !strings.Contains(stored.Content, "retry budget") { return errf("stored content mismatch: %q", stored.Content) } return ExpectEqual(stored.Badge, badgeContract[storage.RoleDeveloper], "stored badge") }}, {Text: "the badge renders in the red developer colour #ef4444", Do: func(c *Context) error { role := c.RecallAs[storage.Comment]("comment").Role return ExpectEqual(role.Hex(), badgeHex[storage.RoleDeveloper], "badge colour") }}, {Text: "GET /api/docs/core-service-api/comments returns the badged comment", Do: func(c *Context) error { response, payload, err := c.Do("GET", "/api/docs/core-service-api/comments", "", "") if err != nil { return err } if err := ExpectStatus(response, 200); err != nil { return err } listed, err := Decode[struct { Comments []storage.Comment `json:"comments"` Count int `json:"count"` }](payload) if err != nil { return err } if listed.Count != 1 { return errf("expected count 1, got %d", listed.Count) } return ExpectEqual(listed.Comments[0].Badge, "🔴", "listed badge") }}, }, }, { Name: "all three audience badges are applied to their own roles", Given: Steps{ {Text: "a documentation page with an existing user comment", Do: func(c *Context) error { if _, err := c.Engine.SaveDoc("deploy-pipeline", "# Deploy Pipeline\n"); err != nil { return err } _, err := appendComment(c, "deploy-pipeline", storage.RoleUser, "sam", "The wizard is confusing") return err }}, }, When: Steps{ {Text: "an operator and a developer comment are added", Do: func(c *Context) error { if _, err := appendComment(c, "deploy-pipeline", storage.RoleOperator, "robin", "Rollback takes 20 minutes"); err != nil { return err } _, err := appendComment(c, "deploy-pipeline", storage.RoleDeveloper, "kit", "Add a --dry-run flag") return err }}, }, Then: Steps{ {Text: "the JSON array holds all three comments in insertion order", Do: func(c *Context) error { comments, err := c.RawComments("deploy-pipeline") if err != nil { return err } if len(comments) != 3 { return errf("expected 3 comments, got %d", len(comments)) } want := []storage.Role{storage.RoleUser, storage.RoleOperator, storage.RoleDeveloper} for i, role := range want { if comments[i].Role != role { return errf("comment %d: expected role %s, got %s", i, role, comments[i].Role) } if comments[i].Badge != badgeContract[role] { return errf("comment %d: expected badge %s, got %s", i, badgeContract[role], comments[i].Badge) } } return nil }}, {Text: "each badge matches its role colour", Do: func(c *Context) error { comments, err := c.RawComments("deploy-pipeline") if err != nil { return err } for _, comment := range comments { if comment.Role.Hex() != badgeHex[comment.Role] { return errf("unexpected colour for %s", comment.Role) } } return nil }}, {Text: "GET /api/docs/deploy-pipeline/stats aggregates by role", Do: func(c *Context) error { response, payload, err := c.Do("GET", "/api/docs/deploy-pipeline/stats", "", "") if err != nil { return err } if err := ExpectStatus(response, 200); err != nil { return err } stats, err := Decode[storage.CommentStats](payload) if err != nil { return err } if stats.Total != 3 { return errf("expected total 3, got %d", stats.Total) } for _, role := range []string{"user", "operator", "developer"} { if stats.ByRole[role] != 1 { return errf("expected 1 %s comment, got %d", role, stats.ByRole[role]) } } return nil }}, }, }, { Name: "the badge is derived server-side and cannot be spoofed by the client", Given: Steps{ {Text: "a documentation page", Do: func(c *Context) error { _, err := c.Engine.SaveDoc("trust-boundary", "# Trust Boundary\n") return err }}, }, When: Steps{ {Text: `a developer posts a comment claiming the user badge "🟢"`, Do: func(c *Context) error { response, payload, err := c.Do( "POST", "/api/docs/trust-boundary/comments", "application/json", `{"author":"mallory","role":"developer","content":"I am a user, honest","badge":"🟢"}`, ) if err != nil { return err } if err := ExpectStatus(response, 201); err != nil { return errf("%w (body: %s)", err, payload) } c.Remember("payload", string(payload)) return nil }}, }, Then: Steps{ {Text: "the server overrides the claim with the developer badge", Do: func(c *Context) error { comments, err := c.RawComments("trust-boundary") if err != nil { return err } if len(comments) != 1 { return errf("expected 1 comment, got %d", len(comments)) } return ExpectEqual(comments[0].Badge, "🔴", "server-assigned badge") }}, }, }, { Name: "an unknown role is rejected with a 400", Given: Steps{ {Text: "a documentation page", Do: func(c *Context) error { _, err := c.Engine.SaveDoc("role-guard", "# Role Guard\n") return err }}, }, When: Steps{ {Text: `a comment is posted with role "root"`, Do: func(c *Context) error { response, payload, err := c.Do( "POST", "/api/docs/role-guard/comments", "application/json", `{"author":"mallory","role":"root","content":"let me in"}`, ) if err != nil { return err } c.Remember("status", response.StatusCode) c.Remember("body", string(payload)) return nil }}, }, Then: Steps{ {Text: "the API answers 400 with the invalid_role code", Do: func(c *Context) error { if err := ExpectEqual(c.RecallAs[int]("status"), 400, "status"); err != nil { return err } return ExpectContains(c.RecallAs[string]("body"), "invalid_role", "error body") }}, {Text: "no feedback file is created", Do: func(c *Context) error { exists, err := c.FileExists(c.CommentsPath("role-guard")) if err != nil { return err } if exists { return errf("a feedback file was created for a rejected comment") } return nil }}, }, }, { Name: "an empty comment is rejected", Given: Steps{ {Text: "a documentation page", Do: func(c *Context) error { _, err := c.Engine.SaveDoc("empty-guard", "# Empty Guard\n") return err }}, }, When: Steps{ {Text: `a comment with blank content is posted`, Do: func(c *Context) error { response, _, err := c.Do( "POST", "/api/docs/empty-guard/comments", "application/json", `{"author":"ana","role":"operator","content":" "}`, ) if err != nil { return err } c.Remember("status", response.StatusCode) return nil }}, }, Then: Steps{ {Text: "the API answers 400", Do: func(c *Context) error { return ExpectEqual(c.RecallAs[int]("status"), 400, "status") }}, {Text: "no feedback file is created", Do: func(c *Context) error { exists, err := c.FileExists(c.CommentsPath("empty-guard")) if err != nil { return err } if exists { return errf("a feedback file was created for a rejected comment") } return nil }}, }, }, { Name: "a comment on an unknown slug still creates the feedback file", Given: Steps{ {Text: "no document exists for the slug new-service", Do: func(c *Context) error { exists, err := c.FileExists(c.DocPath("new-service")) if err != nil { return err } if exists { return errf("the document should not exist yet") } return nil }}, }, When: Steps{ {Text: "the first user comment is posted for new-service", Do: func(c *Context) error { response, _, err := c.Do( "POST", "/api/docs/new-service/comments", "application/json", `{"author":"jo","role":"user","content":"Is this documented?"}`, ) if err != nil { return err } return ExpectStatus(response, 201) }}, }, Then: Steps{ {Text: "the feedback file holds the user badge 🟢", Do: func(c *Context) error { comments, err := c.RawComments("new-service") if err != nil { return err } if len(comments) != 1 { return errf("expected 1 comment, got %d", len(comments)) } return ExpectEqual(comments[0].Badge, "🟢", "badge") }}, {Text: "reading the comments of a slug with no feedback returns an empty list", Do: func(c *Context) error { response, payload, err := c.Do("GET", "/api/docs/never-seen/comments", "", "") if err != nil { return err } if err := ExpectStatus(response, 200); err != nil { return err } listed, err := Decode[struct { Comments []storage.Comment `json:"comments"` }](payload) if err != nil { return err } return ExpectEqual(len(listed.Comments), 0, "comment count") }}, }, }, }, }) }