go-doc/backend/tests/bdd/feedback.feature_test.go

361 lines
12 KiB
Go
Raw Normal View History

2026-10-07 15:03:50 +02:00
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")
}},
},
},
},
})
}