e2e/profile_test.go
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}