e2e/profile_test.go

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

238 lines · 10163 bytes

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