e2e/wiki_test.go

v1.36.0
gitbay/e2e/wiki_test.go history · blame · raw

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 ![shot](shot.png)\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}