e2e/wiki_test.go
184 lines · 8112 bytes
1package e2e
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8)
9
10func TestWikis(t *testing.T) {
11 inst := startInstance(t)
12 aliceKey := inst.newKey(t, "alice")
13 bobKey := inst.newKey(t, "bob")
14 inst.admin(t, "admin", "user", "create", "alice",
15 "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
16 inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
17 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 {
18 t.Fatal("repo create failed")
19 }
20 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/secretive", "--private"); code != 0 {
21 t.Fatal("private repo create failed")
22 }
23
24 // No wiki yet: the tab is absent, the page shows the missing hint, and
25 // listing reports no wiki rather than erroring.
26 _, body := inst.get(t, "/alice/app")
27 if strings.Contains(body, ">Wiki<") {
28 t.Fatal("wiki tab shown with no wiki")
29 }
30 _, body = inst.get(t, "/alice/app/wiki")
31 if !strings.Contains(body, "no wiki yet") {
32 t.Fatal("missing-wiki hint absent")
33 }
34 if out, errOut, code := inst.ssh(t, aliceKey, "", "wiki", "list", "alice/app", "--json"); code != 0 {
35 t.Fatalf("wiki list on a repo without one: %s", errOut)
36 } else if !strings.Contains(out, `"pages":[]`) {
37 t.Errorf("wiki list on a repo without one returned pages: %s", out)
38 }
39
40 // Pages are ordinary files: pushing them creates the wiki.
41 env := inst.gitEnv(aliceKey)
42 work := t.TempDir()
43 mustGit(t, work, env, "init", "-q", "-b", "main", "w")
44 dir := filepath.Join(work, "w")
45 os.MkdirAll(filepath.Join(dir, ".gitbay", "wiki"), 0o755)
46 os.WriteFile(filepath.Join(dir, ".gitbay", "wiki", "Home.md"), []byte(
47 "# welcome\n\nsee [Setup](Setup.md) and \n"), 0o644)
48 os.WriteFile(filepath.Join(dir, ".gitbay", "wiki", "Setup.org"), []byte("* setup\n\nsteps here\n"), 0o644)
49 os.WriteFile(filepath.Join(dir, ".gitbay", "wiki", "shot.png"), []byte{0x89, 0x50, 0x4e, 0x47}, 0o644)
50 os.WriteFile(filepath.Join(dir, "top.txt"), []byte("not part of the wiki\n"), 0o644)
51 mustGit(t, dir, env, "add", ".")
52 mustGit(t, dir, env, "commit", "-q", "-m", "wiki start")
53 mustGit(t, dir, env, "push", "-q", inst.sshURL("alice/app"), "main")
54
55 benv := inst.gitEnv(bobKey)
56 if out, code := gitRun(t, dir, benv, "push", inst.sshURL("alice/app"), "main"); code == 0 && !strings.Contains(out, "denied") {
57 t.Fatalf("reader pushed the repository: %d\n%s", code, out)
58 }
59
60 // Rendering: home resolves, tab appears, links rewrite to wiki pages
61 // and images to the wiki raw route; org pages render too.
62 _, body = inst.get(t, "/alice/app")
63 if !strings.Contains(body, ">Wiki<") {
64 t.Fatal("wiki tab missing after push")
65 }
66 _, body = inst.get(t, "/alice/app/wiki")
67 if !strings.Contains(body, "welcome") ||
68 !strings.Contains(body, `href="/alice/app/wiki/Setup"`) ||
69 !strings.Contains(body, `src="/alice/app/wiki/_raw/shot.png"`) {
70 t.Fatalf("wiki home rendering:\n%s", body)
71 }
72 _, body = inst.get(t, "/alice/app/wiki/Setup")
73 if !strings.Contains(body, "steps here") {
74 t.Fatal("org wiki page missing")
75 }
76 if status, _ := inst.get(t, "/alice/app/wiki/Nope"); status != 404 {
77 t.Fatalf("missing page: %d", status)
78 }
79 // The raw route serves the image bytes.
80 status, raw := inst.get(t, "/alice/app/wiki/_raw/shot.png")
81 if status != 200 || !strings.HasPrefix(raw, "\x89PNG") {
82 t.Fatalf("wiki raw: %d", status)
83 }
84 // The raw route cannot climb out of .gitbay/wiki. A literal ".." is
85 // caught by the mux's own path cleaning, which would make this pass
86 // vacuously; percent-encoding it reaches the handler with real ".."
87 // segments in PathValue, which is what the guard has to refuse.
88 if status, body := inst.get(t, "/alice/app/wiki/_raw/%2e%2e/%2e%2e/top.txt"); status == 200 {
89 t.Fatalf("wiki raw escaped .gitbay/wiki: %d\n%s", status, body)
90 }
91
92 // A wiki is readable from every surface, not just a browser: the
93 // commands are what the web dispatches, and what the CLI and the
94 // JSON API reach.
95 out, errOut, code := inst.ssh(t, aliceKey, "", "wiki", "list", "alice/app", "--json")
96 if code != 0 {
97 t.Fatalf("wiki list: %s", errOut)
98 }
99 if !strings.Contains(out, `"Home"`) || !strings.Contains(out, `"Setup"`) {
100 t.Errorf("wiki list pages: %s", out)
101 }
102 if !strings.Contains(out, `"home":"Home"`) {
103 t.Errorf("wiki list did not name the landing page: %s", out)
104 }
105 // shot.png is not a page.
106 if strings.Contains(out, "shot") {
107 t.Errorf("wiki list included a non-page file: %s", out)
108 }
109
110 // Named page, and the landing page when none is named.
111 out, _, code = inst.ssh(t, aliceKey, "", "wiki", "show", "alice/app", "Setup", "--json")
112 if code != 0 || !strings.Contains(out, "steps here") {
113 t.Errorf("wiki show Setup: %s", out)
114 }
115 out, _, code = inst.ssh(t, aliceKey, "", "wiki", "show", "alice/app", "--json")
116 if code != 0 || !strings.Contains(out, "welcome") {
117 t.Errorf("wiki show default page: %s", out)
118 }
119 // An extension is accepted and ignored, as the web's routes do.
120 if _, _, code := inst.ssh(t, aliceKey, "", "wiki", "show", "alice/app", "Setup.org"); code != 0 {
121 t.Error("wiki show rejected a page named with its extension")
122 }
123 if _, _, code := inst.ssh(t, aliceKey, "", "wiki", "show", "alice/app", "Nope"); code == 0 {
124 t.Error("a missing wiki page resolved")
125 }
126 // A page name cannot climb out of the wiki.
127 if _, _, code := inst.ssh(t, aliceKey, "", "wiki", "show", "alice/app", "../../etc/passwd"); code == 0 {
128 t.Error("wiki show escaped the repository")
129 }
130 // A repository with no wiki says so rather than failing oddly, even
131 // for its owner.
132 if out, errOut, code := inst.ssh(t, aliceKey, "", "wiki", "list", "alice/secretive", "--json"); code != 0 {
133 t.Fatalf("wiki list on a repo without one: %s", errOut)
134 } else if !strings.Contains(out, `"pages":[]`) {
135 t.Errorf("wiki list on a repo without one returned pages: %s", out)
136 }
137 // Wiki access derives from the parent: a stranger gets nothing.
138 if _, _, code := inst.ssh(t, bobKey, "", "wiki", "list", "alice/secretive"); code == 0 {
139 t.Error("a stranger listed a private repository's wiki")
140 }
141
142 // 404-parity: a private repo's wiki is invisible, over web and git.
143 if status, _ := inst.get(t, "/alice/secretive/wiki"); status != 404 {
144 t.Fatalf("private wiki page: %d", status)
145 }
146
147 // Pushing to <name>.wiki.git is refused now that the companion route
148 // is gone; there is no such repository.
149 if out, code := gitRun(t, t.TempDir(), env, "clone", inst.sshURL("alice/app.wiki"), "x"); code == 0 {
150 t.Fatalf("cloned a nonexistent companion: %s", out)
151 } else if !strings.Contains(out, "not found") {
152 t.Fatalf("clone of alice/app.wiki: %s", out)
153 }
154
155 // A repository may now be named something.wiki: the suffix is no
156 // longer reserved.
157 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/notes.wiki"); code != 0 {
158 t.Fatalf("something.wiki repo name refused: %s", errOut)
159 }
160
161 // repo commit-file writes a page on a repository that permits
162 // server-authored commits: it is the command behind the web editor,
163 // and there is no wiki-specific write command.
164 if _, errOut, code := inst.ssh(t, aliceKey, "written by commit-file\n",
165 "repo", "commit-file", "alice/app", ".gitbay/wiki/Extra.md",
166 "--ref", "main", "--message", "'add a page'", "--file", "-"); code != 0 {
167 t.Fatalf("repo commit-file: %s", errOut)
168 }
169 out, _, code = inst.ssh(t, aliceKey, "", "wiki", "show", "alice/app", "Extra", "--json")
170 if code != 0 || !strings.Contains(out, "written by commit-file") {
171 t.Errorf("wiki show Extra: %s", out)
172 }
173
174 // A repository requiring verified signatures refuses repo commit-file,
175 // since the server cannot sign on the user's behalf.
176 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "settings", "require-signed", "alice/app", "on"); code != 0 {
177 t.Fatal("require-signed failed")
178 }
179 if _, errOut, code := inst.ssh(t, aliceKey, "blocked\n",
180 "repo", "commit-file", "alice/app", ".gitbay/wiki/Blocked.md",
181 "--ref", "main", "--file", "-"); code == 0 || !strings.Contains(errOut, "requires signed commits") {
182 t.Errorf("repo commit-file not refused on a signed-commits repo: %d %s", code, errOut)
183 }
184}