e2e/profile_test.go

e9566eed86ebcd185c4b85f63d667b5e671fe787
gitbay/e2e/profile_test.go history · blame · raw

234 lines · 10011 bytes

  1package e2e
  2
  3import (
  4	"os"
  5	"path/filepath"
  6	"strings"
  7	"testing"
  8)
  9
 10func TestOwnerProfiles(t *testing.T) {
 11	inst := startInstance(t)
 12	aliceKey := inst.newKey(t, "alice")
 13	bobKey := inst.newKey(t, "bob")
 14	// A verified address, because the about text is a commit now.
 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
 19	// Self-service user profile; website validated.
 20	if _, errOut, code := inst.ssh(t, aliceKey, "",
 21		"profile", "set", "--description", "'builds small tools'", "--website", "https://alice.example"); code != 0 {
 22		t.Fatalf("profile set: %s", errOut)
 23	}
 24	if _, _, code := inst.ssh(t, aliceKey, "", "profile", "set", "--website", "gopher://nope"); code != 2 {
 25		t.Fatal("bad website scheme accepted")
 26	}
 27	out, _, _ := inst.ssh(t, bobKey, "", "profile", "show", "alice", "--json")
 28	if !strings.Contains(out, `"description":"builds small tools"`) || !strings.Contains(out, `"website":"https://alice.example"`) {
 29		t.Fatalf("profile show: %s", out)
 30	}
 31
 32	// A profile is the whole page's worth: repositories the caller may
 33	// see, org membership, and the activity window — all previously
 34	// readable only by the web, which went straight to the store.
 35	if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/public-tool"); code != 0 {
 36		t.Fatal("repo create")
 37	}
 38	if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/secret", "--private"); code != 0 {
 39		t.Fatal("private repo create")
 40	}
 41	if _, _, code := inst.ssh(t, aliceKey, "", "org", "create", "toolmakers"); code != 0 {
 42		t.Fatal("org create")
 43	}
 44
 45	out, _, _ = inst.ssh(t, aliceKey, "", "profile", "show", "alice", "--json")
 46	for _, want := range []string{`"path":"alice/public-tool"`, `"path":"alice/secret"`,
 47		`"name":"toolmakers"`, `"activity_total"`} {
 48		if !strings.Contains(out, want) {
 49			t.Errorf("own profile missing %q: %s", want, out)
 50		}
 51	}
 52
 53	// A profile's repository rows carry the listing metadata the web
 54	// shows: topics, license, default branch, last commit. Without them
 55	// the web had to decorate the rows itself, which is what kept it
 56	// reading the store (krz/gitbay#47).
 57	if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "topics", "add", "alice/public-tool", "cli", "go"); code != 0 {
 58		t.Fatalf("topics add: %s", errOut)
 59	}
 60	env := inst.gitEnv(aliceKey)
 61	work := t.TempDir()
 62	mustGit(t, work, env, "clone", inst.sshURL("alice/public-tool"), "w")
 63	dir := filepath.Join(work, "w")
 64	os.WriteFile(filepath.Join(dir, "LICENSE"), []byte("MIT License\n\nPermission is hereby granted, free of charge\n"), 0o644)
 65	mustGit(t, dir, env, "checkout", "-q", "-b", "main")
 66	mustGit(t, dir, env, "add", ".")
 67	mustGit(t, dir, env, "commit", "-q", "-m", "add license")
 68	mustGit(t, dir, env, "push", "-q", "origin", "main")
 69
 70	out, _, _ = inst.ssh(t, bobKey, "", "profile", "show", "alice", "--json")
 71	for _, want := range []string{`"cli"`, `"go"`, `"license":"MIT"`, `"default_branch":"main"`, `"updated"`} {
 72		if !strings.Contains(out, want) {
 73			t.Errorf("profile repo row missing %q: %s", want, out)
 74		}
 75	}
 76	// The owner page renders those rows, having dispatched the same
 77	// command rather than assembling them from the store again.
 78	status, body := inst.get(t, "/alice")
 79	if status != 200 {
 80		t.Fatalf("owner page: %d", status)
 81	}
 82	for _, want := range []string{">public-tool<", "?q=cli", "MIT", "updated "} {
 83		if !strings.Contains(body, want) {
 84			t.Errorf("owner page missing %q:\n%s", want, body)
 85		}
 86	}
 87	if strings.Contains(body, "secret") {
 88		t.Fatal("owner page leaks a private repo")
 89	}
 90
 91	// A stranger sees the public repository and not the private one.
 92	out, _, _ = inst.ssh(t, bobKey, "", "profile", "show", "alice", "--json")
 93	if !strings.Contains(out, `"path":"alice/public-tool"`) {
 94		t.Errorf("stranger cannot see a public repo on a profile: %s", out)
 95	}
 96	if strings.Contains(out, `"alice/secret"`) {
 97		t.Errorf("a private repository leaked onto a profile: %s", out)
 98	}
 99
100	// An org profile lists its members rather than org memberships.
101	out, _, _ = inst.ssh(t, aliceKey, "", "profile", "show", "toolmakers", "--json")
102	if !strings.Contains(out, `"kind":"org"`) || !strings.Contains(out, `"name":"alice"`) {
103		t.Errorf("org profile members: %s", out)
104	}
105
106	// Partial update leaves the other field untouched; empty clears.
107	if _, _, code := inst.ssh(t, aliceKey, "", "profile", "set", "--description", "'tinkerer'"); code != 0 {
108		t.Fatal("partial set failed")
109	}
110	out, _, _ = inst.ssh(t, aliceKey, "", "profile", "show", "--json")
111	if !strings.Contains(out, "tinkerer") || !strings.Contains(out, "alice.example") {
112		t.Fatalf("partial update clobbered website: %s", out)
113	}
114	if _, _, code := inst.ssh(t, aliceKey, "", "profile", "set", "--website", "''"); code != 0 {
115		t.Fatal("clear failed")
116	}
117	out, _, _ = inst.ssh(t, aliceKey, "", "profile", "show", "--json")
118	if strings.Contains(out, "alice.example") {
119		t.Fatalf("website not cleared: %s", out)
120	}
121
122	// Org profile: admin sets, member cannot; no flags shows.
123	if _, _, code := inst.ssh(t, aliceKey, "", "org", "create", "workshop"); code != 0 {
124		t.Fatal("org create failed")
125	}
126	if _, _, code := inst.ssh(t, aliceKey, "", "org", "members", "add", "workshop", "bob"); code != 0 {
127		t.Fatal("member add failed")
128	}
129	if _, errOut, code := inst.ssh(t, aliceKey, "",
130		"org", "profile", "workshop", "--description", "'where things get made'", "--website", "https://workshop.example"); code != 0 {
131		t.Fatalf("org profile set: %s", errOut)
132	}
133	if _, _, code := inst.ssh(t, bobKey, "", "org", "profile", "workshop", "--description", "hax"); code != 4 {
134		t.Fatal("member set org profile")
135	}
136	out, _, _ = inst.ssh(t, bobKey, "", "org", "profile", "workshop")
137	if !strings.Contains(out, "where things get made") {
138		t.Fatalf("org profile show: %s", out)
139	}
140
141	// About: a file in <owner>/.gitbay, rendered on the page. The
142	// extension picks the renderer; there is no stored format.
143	if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/.gitbay"); code != 0 {
144		t.Fatalf("creating alice/.gitbay: %s", errOut)
145	}
146	if _, errOut, code := inst.ssh(t, aliceKey, "* Tools\n\nI maintain /small tools/.\n",
147		"repo", "commit-file", "alice/.gitbay", "profile/README.org",
148		"--ref", "main", "--file", "-"); code != 0 {
149		t.Fatalf("org about: %s", errOut)
150	}
151	out, _, _ = inst.ssh(t, aliceKey, "", "profile", "show", "alice", "--json")
152	if !strings.Contains(out, `"about_format":"org"`) {
153		t.Fatalf("about format not read from the extension: %s", out)
154	}
155	if !strings.Contains(out, "tinkerer") {
156		t.Fatalf("about clobbered the description: %s", out)
157	}
158	// Org emphasis parsed, not left as literal slashes the way the
159	// markdown renderer would.
160	_, body = inst.get(t, "/alice")
161	if !strings.Contains(body, "<em>small tools</em>") || strings.Contains(body, "/small tools/") {
162		t.Fatalf("org about not rendered as org: %s", body)
163	}
164
165	// Markdown for the rest of the checks: .md wins the resolution order.
166	if _, errOut, code := inst.ssh(t, aliceKey, "# Hello\n\nI maintain *small tools*.\n",
167		"repo", "commit-file", "alice/.gitbay", "profile/README.md",
168		"--ref", "main", "--file", "-"); code != 0 {
169		t.Fatalf("markdown about: %s", errOut)
170	}
171	out, _, _ = inst.ssh(t, aliceKey, "", "profile", "show", "alice", "--json")
172	if !strings.Contains(out, "I maintain *small tools*.") {
173		t.Fatalf("about not read from the repository: %s", out)
174	}
175
176	// Links: free-form, labelled or bare, capped, cleared by an empty one.
177	if _, errOut, code := inst.ssh(t, aliceKey, "", "profile", "set",
178		"--link", "'Mastodon|https://fosstodon.example/@alice'",
179		"--link", "https://alice.example/now"); code != 0 {
180		t.Fatalf("links: %s", errOut)
181	}
182	out, _, _ = inst.ssh(t, aliceKey, "", "profile", "show", "alice", "--json")
183	if !strings.Contains(out, `"label":"Mastodon"`) ||
184		!strings.Contains(out, `"url":"https://fosstodon.example/@alice"`) ||
185		!strings.Contains(out, `"url":"https://alice.example/now"`) {
186		t.Fatalf("links not stored: %s", out)
187	}
188	if _, _, code := inst.ssh(t, aliceKey, "", "profile", "set", "--link", "'x|gopher://nope'"); code != 2 {
189		t.Fatal("bad link scheme accepted")
190	}
191	sixth := []string{"profile", "set"}
192	for i := 0; i < 6; i++ {
193		sixth = append(sixth, "--link", "https://example.org/"+string(rune('a'+i)))
194	}
195	if _, _, code := inst.ssh(t, aliceKey, "", sixth...); code != 2 {
196		t.Fatal("more than five links accepted")
197	}
198
199	// Owner pages render description and website link.
200	status, body = inst.get(t, "/alice")
201	if status != 200 || !strings.Contains(body, "tinkerer") {
202		t.Fatalf("user page profile: %d", status)
203	}
204	// About renders as markdown between the header and the activity graph;
205	// links render as chips.
206	if !strings.Contains(body, "<em>small tools</em>") {
207		t.Fatalf("about not rendered: %s", body)
208	}
209	if !strings.Contains(body, `href="https://fosstodon.example/@alice"`) ||
210		!strings.Contains(body, ">Mastodon<") {
211		t.Fatalf("links not rendered: %s", body)
212	}
213	if strings.Index(body, "<em>small tools</em>") > strings.Index(body, `class="activity"`) {
214		t.Error("about renders below the activity graph")
215	}
216	if strings.Index(body, `<ul class="repolist"`) < strings.Index(body, `class="activity"`) {
217		t.Error("repositories render above the activity graph")
218	}
219
220	// Clearing works the same way as the other fields. The about is not
221	// among them: it is a file, and it goes the way a file goes.
222	if _, _, code := inst.ssh(t, aliceKey, "", "profile", "set", "--link", "''"); code != 0 {
223		t.Fatal("clear links failed")
224	}
225	out, _, _ = inst.ssh(t, aliceKey, "", "profile", "show", "alice", "--json")
226	if strings.Contains(out, "fosstodon") {
227		t.Fatalf("links not cleared: %s", out)
228	}
229	status, body = inst.get(t, "/workshop")
230	if status != 200 || !strings.Contains(body, "where things get made") ||
231		!strings.Contains(body, `href="https://workshop.example"`) {
232		t.Fatalf("org page profile: %d\n%s", status, body)
233	}
234}