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 }}, }, }, }, }) }