e2e/wiki_test.go
149 lines · 6260 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", "--key", aliceKey+".pub")
15 inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
16 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 {
17 t.Fatal("repo create failed")
18 }
19 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/secretive", "--private"); code != 0 {
20 t.Fatal("private repo create failed")
21 }
22
23 // No wiki yet: the tab is absent, the page shows the push hint, and
24 // cloning the companion says so.
25 _, body := inst.get(t, "/alice/app")
26 if strings.Contains(body, ">Wiki<") {
27 t.Fatal("wiki tab shown with no wiki")
28 }
29 _, body = inst.get(t, "/alice/app/wiki")
30 if !strings.Contains(body, "no wiki yet") {
31 t.Fatal("missing-wiki hint absent")
32 }
33 env := inst.gitEnv(aliceKey)
34 if out, code := gitRun(t, t.TempDir(), env, "clone", inst.sshURL("alice/app.wiki"), "w"); code == 0 || !strings.Contains(out, "no wiki yet") {
35 t.Fatalf("clone of absent wiki: %d\n%s", code, out)
36 }
37
38 // First push creates the wiki. A reader without write cannot push it.
39 work := t.TempDir()
40 mustGit(t, work, env, "init", "-q", "-b", "main", "w")
41 dir := filepath.Join(work, "w")
42 os.WriteFile(filepath.Join(dir, "Home.md"), []byte(
43 "# welcome\n\nsee [Setup](Setup.md) and \n"), 0o644)
44 os.WriteFile(filepath.Join(dir, "Setup.org"), []byte("* setup\n\nsteps here\n"), 0o644)
45 os.WriteFile(filepath.Join(dir, "shot.png"), []byte{0x89, 0x50, 0x4e, 0x47}, 0o644)
46 mustGit(t, dir, env, "add", ".")
47 mustGit(t, dir, env, "commit", "-q", "-m", "wiki start")
48 mustGit(t, dir, env, "push", "-q", inst.sshURL("alice/app.wiki"), "main")
49
50 benv := inst.gitEnv(bobKey)
51 if out, code := gitRun(t, dir, benv, "push", inst.sshURL("alice/app.wiki"), "main"); code == 0 && !strings.Contains(out, "denied") {
52 t.Fatalf("reader pushed the wiki: %d\n%s", code, out)
53 }
54
55 // Rendering: home resolves, tab appears, links rewrite to wiki pages
56 // and images to the wiki raw route; org pages render too.
57 _, body = inst.get(t, "/alice/app")
58 if !strings.Contains(body, ">Wiki<") {
59 t.Fatal("wiki tab missing after push")
60 }
61 _, body = inst.get(t, "/alice/app/wiki")
62 if !strings.Contains(body, "welcome") ||
63 !strings.Contains(body, `href="/alice/app/wiki/Setup"`) ||
64 !strings.Contains(body, `src="/alice/app/wiki/_raw/shot.png"`) {
65 t.Fatalf("wiki home rendering:\n%s", body)
66 }
67 _, body = inst.get(t, "/alice/app/wiki/Setup")
68 if !strings.Contains(body, "steps here") {
69 t.Fatal("org wiki page missing")
70 }
71 if status, _ := inst.get(t, "/alice/app/wiki/Nope"); status != 404 {
72 t.Fatalf("missing page: %d", status)
73 }
74 // The raw route serves the image bytes.
75 status, raw := inst.get(t, "/alice/app/wiki/_raw/shot.png")
76 if status != 200 || !strings.HasPrefix(raw, "\x89PNG") {
77 t.Fatalf("wiki raw: %d", status)
78 }
79
80 // A wiki is readable from every surface, not just a browser: the
81 // commands are what the web dispatches, and what the CLI and the
82 // JSON API reach.
83 out, errOut, code := inst.ssh(t, aliceKey, "", "wiki", "list", "alice/app", "--json")
84 if code != 0 {
85 t.Fatalf("wiki list: %s", errOut)
86 }
87 if !strings.Contains(out, `"Home"`) || !strings.Contains(out, `"Setup"`) {
88 t.Errorf("wiki list pages: %s", out)
89 }
90 if !strings.Contains(out, `"home":"Home"`) {
91 t.Errorf("wiki list did not name the landing page: %s", out)
92 }
93 // shot.png is not a page.
94 if strings.Contains(out, "shot") {
95 t.Errorf("wiki list included a non-page file: %s", out)
96 }
97
98 // Named page, and the landing page when none is named.
99 out, _, code = inst.ssh(t, aliceKey, "", "wiki", "show", "alice/app", "Setup", "--json")
100 if code != 0 || !strings.Contains(out, "steps here") {
101 t.Errorf("wiki show Setup: %s", out)
102 }
103 out, _, code = inst.ssh(t, aliceKey, "", "wiki", "show", "alice/app", "--json")
104 if code != 0 || !strings.Contains(out, "welcome") {
105 t.Errorf("wiki show default page: %s", out)
106 }
107 // An extension is accepted and ignored, as the web's routes do.
108 if _, _, code := inst.ssh(t, aliceKey, "", "wiki", "show", "alice/app", "Setup.org"); code != 0 {
109 t.Error("wiki show rejected a page named with its extension")
110 }
111 if _, _, code := inst.ssh(t, aliceKey, "", "wiki", "show", "alice/app", "Nope"); code == 0 {
112 t.Error("a missing wiki page resolved")
113 }
114 // A page name cannot climb out of the wiki.
115 if _, _, code := inst.ssh(t, aliceKey, "", "wiki", "show", "alice/app", "../../etc/passwd"); code == 0 {
116 t.Error("wiki show escaped the repository")
117 }
118 // A repository with no wiki says so rather than failing oddly.
119 if _, errOut, code := inst.ssh(t, aliceKey, "", "wiki", "list", "alice/secretive"); code == 0 ||
120 !strings.Contains(errOut, "no wiki") {
121 t.Errorf("wiki list on a repo without one: %d %s", code, errOut)
122 }
123 // Wiki access derives from the parent: a stranger gets nothing.
124 if _, _, code := inst.ssh(t, bobKey, "", "wiki", "list", "alice/secretive"); code == 0 {
125 t.Error("a stranger listed a private repository's wiki")
126 }
127
128 // 404-parity: a private repo's wiki is invisible, over web and git.
129 mustGit(t, dir, env, "push", "-q", inst.sshURL("alice/secretive.wiki"), "main")
130 if status, _ := inst.get(t, "/alice/secretive/wiki"); status != 404 {
131 t.Fatalf("private wiki page: %d", status)
132 }
133 if out, code := gitRun(t, t.TempDir(), benv, "clone", inst.sshURL("alice/secretive.wiki"), "x"); code == 0 || !strings.Contains(out, "not found") {
134 t.Fatalf("private wiki clone by outsider: %d\n%s", code, out)
135 }
136
137 // Repo names ending .wiki are refused (companion namespace).
138 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/notes.wiki"); code != 2 || !strings.Contains(errOut, "reserved") {
139 t.Fatalf(".wiki name allowed: %d %s", code, errOut)
140 }
141
142 // Deleting the repo removes the wiki companion.
143 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "delete", "alice/secretive", "--yes"); code != 0 {
144 t.Fatal("repo delete failed")
145 }
146 if _, err := os.Stat(filepath.Join(inst.root, "repos", "alice", "secretive.wiki.git")); err == nil {
147 t.Fatal("wiki survived repo delete")
148 }
149}