initial commit
This commit is contained in:
commit
23816d00f7
71 changed files with 14497 additions and 0 deletions
295
backend/tests/bdd/documents.feature_test.go
Normal file
295
backend/tests/bdd/documents.feature_test.go
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
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
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue