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

296 lines
9.4 KiB
Go
Raw Normal View History

2026-10-07 15:03:50 +02:00
package bdd
import (
"strings"
"testing"
"go-doc/storage"
)
// TestFeatureSlugifiedMarkdownStorage covers:
//
// Given a new application title
// When it is saved
// Then it should be slugified and written to a Markdown file
func TestFeatureSlugifiedMarkdownStorage(t *testing.T) {
Run(t, Feature{
Name: "Slugified Markdown storage",
Scenarios: []Scenario{
{
Name: "a new application title is slugified and written to a Markdown file",
Given: Steps{
{Text: `a new application title "Core Service API"`, Do: func(c *Context) error {
return ExpectEqual(storage.Slugify("Core Service API"), "core-service-api", "slugified title")
}},
},
When: Steps{
{Text: "the title is saved", Do: func(c *Context) error {
_, err := c.Engine.SaveDoc("Core Service API", "# Core Service API\n\nHandles billing.\n")
return err
}},
},
Then: Steps{
{Text: "the engine reports the canonical slug", Do: func(c *Context) error {
meta, err := c.Engine.Stat("Core Service API")
if err != nil {
return err
}
return ExpectEqual(meta.Slug, "core-service-api", "canonical slug")
}},
{Text: "data/docs/core-service-api.md exists", Do: func(c *Context) error {
exists, err := c.FileExists(c.DocPath("core-service-api"))
if err != nil {
return err
}
if !exists {
return errf("expected %s to exist", c.DocPath("core-service-api"))
}
return nil
}},
{Text: "the Markdown content is stored verbatim", Do: func(c *Context) error {
content, err := c.RawDoc("core-service-api")
if err != nil {
return err
}
return ExpectContains(content, "# Core Service API", "stored markdown")
}},
{Text: "no file is created for the un-slugified title", Do: func(c *Context) error {
exists, err := c.FileExists(c.DocPath("Core Service API"))
if err != nil {
return err
}
if exists {
return errf("a non-slugified filename was created")
}
return nil
}},
},
},
{
Name: "titles with punctuation, casing and diacritics are normalised",
Given: Steps{
{Text: "a batch of awkward application titles", Do: func(c *Context) error {
cases := map[string]string{
" Core Service API ": "core-service-api",
"Deploy & Rollback": "deploy-rollback",
"Überwachen der Dienste": "uberwachen-der-dienste",
"API_v2 (Internal)": "api-v2-internal",
"!!!": "",
"Release 2024/Q1 -- Notes": "release-2024-q1-notes",
"User Facing / End Users": "user-facing-end-users",
"already-a-canonical-slug-1": "already-a-canonical-slug-1",
}
for input, want := range cases {
if got := storage.Slugify(input); got != want {
return errf("Slugify(%q) = %q, want %q", input, got, want)
}
}
return nil
}},
},
When: Steps{
{Text: "each title is saved", Do: func(c *Context) error {
for _, title := range []string{"Deploy & Rollback", "Überwachen der Dienste", "API_v2 (Internal)"} {
if _, err := c.Engine.SaveDoc(title, "# "+title+"\n"); err != nil {
return err
}
}
return nil
}},
},
Then: Steps{
{Text: "each slugified filename exists on disk", Do: func(c *Context) error {
for _, slug := range []string{"deploy-rollback", "uberwachen-der-dienste", "api-v2-internal"} {
exists, err := c.FileExists(c.DocPath(slug))
if err != nil {
return err
}
if !exists {
return errf("expected data/docs/%s.md to exist", slug)
}
}
return nil
}},
},
},
{
Name: "saving through the API slugifies the slug taken from the URL",
Given: Steps{
{Text: "the API is running against a temporary data directory", Do: func(c *Context) error {
return nil
}},
},
When: Steps{
{Text: `a document is POSTed to /api/docs/Core Service API`, Do: func(c *Context) error {
response, payload, err := c.Do(
"POST", "/api/docs/Core%20Service%20API", "text/markdown", "# Core Service API\n",
)
if err != nil {
return err
}
if err := ExpectStatus(response, 201); err != nil {
return errf("%w (body: %s)", err, payload)
}
saved, err := Decode[struct {
Slug string `json:"slug"`
}](payload)
if err != nil {
return err
}
c.Remember("slug", saved.Slug)
return nil
}},
},
Then: Steps{
{Text: "the response reports the slugified slug", Do: func(c *Context) error {
return ExpectEqual(c.RecallAs[string]("slug"), "core-service-api", "response slug")
}},
{Text: "the Markdown file is on disk", Do: func(c *Context) error {
exists, err := c.FileExists(c.DocPath("core-service-api"))
if err != nil {
return err
}
if !exists {
return errf("data/docs/core-service-api.md was not created")
}
return nil
}},
{Text: "GET /api/docs/core-service-api returns the raw Markdown", Do: func(c *Context) error {
response, payload, err := c.Do("GET", "/api/docs/core-service-api", "", "")
if err != nil {
return err
}
if err := ExpectStatus(response, 200); err != nil {
return err
}
if contentType := response.Header.Get("Content-Type"); !strings.HasPrefix(contentType, "text/markdown") {
return errf("expected text/markdown, got %q", contentType)
}
return ExpectContains(string(payload), "# Core Service API", "markdown response")
}},
},
},
{
Name: "saving an existing document replaces its content wholesale",
Given: Steps{
{Text: "an existing document with two sections", Do: func(c *Context) error {
_, err := c.Engine.SaveDoc("release-runbook", "# Runbook\n\n## Deploy\n\nstep\n\n## Rollback\n\nstep\n")
return err
}},
},
When: Steps{
{Text: "the document is rewritten with a single section", Do: func(c *Context) error {
response, _, err := c.Do("POST", "/api/docs/release-runbook", "text/markdown", "# Runbook v2\n\n## Deploy only\n")
if err != nil {
return err
}
return ExpectStatus(response, 200)
}},
},
Then: Steps{
{Text: "the previous sections are gone", Do: func(c *Context) error {
content, err := c.RawDoc("release-runbook")
if err != nil {
return err
}
if strings.Contains(content, "## Rollback") {
return errf("stale section survived the update: %q", content)
}
return ExpectContains(content, "## Deploy only", "updated markdown")
}},
},
},
{
Name: "deleting a document removes the file and its feedback",
Given: Steps{
{Text: "a document that already has comments", Do: func(c *Context) error {
if _, err := c.Engine.SaveDoc("obsolete-service", "# Obsolete\n"); err != nil {
return err
}
_, err := c.Engine.AppendComment("obsolete-service", storage.CommentInput{
Author: "ana",
Role: "user",
Content: "is this still needed?",
})
return err
}},
},
When: Steps{
{Text: "DELETE /api/docs/obsolete-service is called", Do: func(c *Context) error {
response, _, err := c.Do("DELETE", "/api/docs/obsolete-service", "", "")
if err != nil {
return err
}
return ExpectStatus(response, 204)
}},
},
Then: Steps{
{Text: "the Markdown file is gone", Do: func(c *Context) error {
exists, err := c.FileExists(c.DocPath("obsolete-service"))
if err != nil {
return err
}
if exists {
return errf("data/docs/obsolete-service.md still exists")
}
return nil
}},
{Text: "the comment file is gone", Do: func(c *Context) error {
exists, err := c.FileExists(c.CommentsPath("obsolete-service"))
if err != nil {
return err
}
if exists {
return errf("the feedback file was not cascaded")
}
return nil
}},
{Text: "reading the deleted document returns 404", Do: func(c *Context) error {
response, _, err := c.Do("GET", "/api/docs/obsolete-service", "", "")
if err != nil {
return err
}
return ExpectStatus(response, 404)
}},
},
},
{
Name: "path traversal in a slug is rejected",
Given: Steps{
{Text: "a request for a traversal slug", Do: func(c *Context) error {
return nil
}},
},
When: Steps{
{Text: `POST /api/docs/..%2F..%2Fetc%2Fpasswd is sent`, Do: func(c *Context) error {
response, payload, err := c.Do("POST", "/api/docs/..%2F..%2Fetc%2Fpasswd", "text/markdown", "nope")
if err != nil {
return err
}
c.Remember("status", response.StatusCode)
c.Remember("body", string(payload))
return nil
}},
},
Then: Steps{
{Text: "the request is rejected with 400", Do: func(c *Context) error {
return ExpectEqual(c.RecallAs[int]("status"), 400, "status code")
}},
{Text: "nothing is written outside data/docs", Do: func(c *Context) error {
for _, name := range []string{"etc", "passwd", ".."} {
exists, err := c.FileExists(c.DocPath(name))
if err != nil {
return err
}
if exists {
return errf("unexpected file created for slug %q", name)
}
}
return nil
}},
},
},
},
})
}