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