package main import ( "fmt" "net/http" "net/url" "os" "path/filepath" "slices" "strings" "sync" "testing" ) // A page name has to survive the trip out to a URL and back to a path in the // tree, or a page is listed under a link that does not open it. func TestWikiPageNames(t *testing.T) { for _, file := range []string{ "index.md", "docs/index.md", "notes/deep/index.md", "plain.md", "myindex.md", "a/myindex.md", "index.notes.md", } { page := wikiPage(file) files, ok := wikiFiles(page) if !ok { t.Errorf("%s becomes the page %q, which is not a usable name", file, page) continue } if !slices.Contains(files, file) { t.Errorf("%s becomes the page %q, which reads %v instead", file, page, files) } } } // No pages yet is a state, not a failure. func TestEmptyWikiIsNotAnError(t *testing.T) { f := testForge(t) addRepo(t, f, "notes", Repo{Visibility: visPublic, Issues: featDisabled, Wiki: featUsers}) if code := get(f.routes(), "/notes/wiki", nil).Code; code != http.StatusOK { t.Errorf("an empty wiki answered %d", code) } if code := get(f.routes(), "/notes/wiki/nope", nil).Code; code != http.StatusOK { t.Errorf("a missing page in an empty wiki answered %d", code) } } // A push can put a name in the tree that reading refuses, and a link that // answers 404 is worse than no link. func TestWikiIndexOnlyListsPagesItCanOpen(t *testing.T) { f := testForge(t) addRepo(t, f, "notes", Repo{Visibility: visPublic, Issues: featDisabled, Wiki: featUsers}) dir := f.wikiDir("notes") author := f.store.forgeAuthor() err := commitWith(dir, author, author, func(base string) ([]change, string, error) { return []change{ {path: "fine.md", data: []byte("# fine\n")}, {path: "a b.md", data: []byte("# spaces\n")}, {path: ".hidden.md", data: []byte("# a dot\n")}, // Both of these read as a page that opens index.md instead, so // listing them offers a link that never reaches the file. {path: ".md", data: []byte("# a file called .md\n")}, {path: "index.md.md", data: []byte("# doubled\n")}, }, "wiki: written by push", nil }) if err != nil { t.Fatal(err) } head, err := resolveRef(dir, branch) if err != nil { t.Fatal(err) } pages := f.wikiPages(dir, head) if !slices.Contains(pages, "fine") { t.Errorf("a usable page is not listed: %v", pages) } // One entry per file, and every entry opens the file it came from. if len(pages) != 1 { t.Errorf("three files that cannot be opened were listed anyway: %v", pages) } h := f.routes() for _, page := range []string{"a b", ".hidden"} { if slices.Contains(pages, page) { t.Errorf("%q is listed but cannot be opened: %v", page, pages) } if code := get(h, "/notes/wiki/"+url.PathEscape(page), nil).Code; code != http.StatusNotFound { t.Errorf("%q answered %d, so it could have been listed after all", page, code) } } } // Two people saving one page from the same base must not both be told they // saved. Comparing before the commit rather than inside it leaves the whole // commit between the check and the write, so this has to be concurrent. func TestWikiSaveIsACompareAndSwap(t *testing.T) { f := testForge(t) addUser(t, f, "alice", "hunter2hunter2") addRepo(t, f, "notes", Repo{Visibility: visPublic, Issues: featDisabled, Wiki: featUsers}) h := f.routes() session := signIn(t, f, h, "alice") token := f.csrf(withCookies("/", session)) post(h, "/notes/wiki/page", url.Values{ "csrf": {token}, "base": {""}, "action": {"save"}, "body": {"original\n"}}, session) dir := f.wikiDir("notes") base, err := f.wikiBlob(dir, "page.md") if err != nil || base == "" { t.Fatalf("the first page did not land: %q %v", base, err) } const writers = 4 codes := make([]int, writers) var wg sync.WaitGroup for i := range writers { wg.Add(1) go func() { defer wg.Done() codes[i] = post(h, "/notes/wiki/page", url.Values{ "csrf": {token}, "base": {base}, "action": {"save"}, "body": {fmt.Sprintf("rewritten by %d\n", i)}}, session).Code }() } wg.Wait() saved := 0 for i, code := range codes { switch code { case http.StatusSeeOther: saved++ case http.StatusOK: // handed back the editor and told to merge default: t.Errorf("writer %d: status %d", i, code) } } if saved != 1 { t.Errorf("%d of %d writers from one base were told they saved", saved, writers) } // And the one that did save is what the page holds. head, _ := resolveRef(dir, branch) body, err := catFile(dir, head+":page.md") if err != nil || !strings.HasPrefix(string(body), "rewritten by ") { t.Errorf("the page reads %q, %v", body, err) } } // An empty repository and one that is not there must not look the same, or a // bad restore renders as a friendly page. Both at the primitive and at the // page that shows it. func TestMissingRepoIsAnError(t *testing.T) { f := testForge(t) addRepo(t, f, "notes", Repo{Visibility: visPublic, Issues: featUsers, Wiki: featUsers}) h := f.routes() dir := filepath.Join(f.store.reposDir, "notes.wiki.git") if head, err := resolveRef(dir, branch); head != "" || err != nil { t.Fatalf("an empty repository should be empty and fine: %q %v", head, err) } if code := get(h, "/notes/wiki", nil).Code; code != http.StatusOK { t.Fatalf("an empty wiki answered %d", code) } if err := os.RemoveAll(dir); err != nil { t.Fatal(err) } if _, err := resolveRef(dir, branch); err == nil { t.Error("a missing repository still looks like an empty one") } if code := get(h, "/notes/wiki", nil).Code; code != http.StatusInternalServerError { t.Errorf("a wiki with no repository behind it answered %d", code) } // And the tracker, which is the same question at the other door. if err := os.RemoveAll(f.issuesDir("notes")); err != nil { t.Fatal(err) } if code := get(h, "/notes/issues", nil).Code; code != http.StatusInternalServerError { t.Errorf("a tracker with no repository behind it answered %d", code) } }