e2e/profile_test.go

v1.12.0
gitbay/e2e/profile_test.go history · blame · raw

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