1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
|
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)
}
}
|