296 lines
9.4 KiB
Go
296 lines
9.4 KiB
Go
|
|
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
|
||
|
|
}},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
})
|
||
|
|
}
|