e2e/wiki_test.go

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

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