e2e/profile_test.go
237 lines · 10028 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 if strings.Index(body, `<ul class="repolist"`) < strings.Index(body, `class="activity"`) {
218 t.Error("repositories render above the activity graph")
219 }
220
221 // Clearing works the same way as the other fields.
222 if _, _, code := inst.ssh(t, aliceKey, "", "profile", "set", "--link", "''"); code != 0 {
223 t.Fatal("clear links failed")
224 }
225 if _, _, code := inst.ssh(t, aliceKey, "", "profile", "set", "--about", "''"); code != 0 {
226 t.Fatal("clear about failed")
227 }
228 out, _, _ = inst.ssh(t, aliceKey, "", "profile", "show", "alice", "--json")
229 if strings.Contains(out, "fosstodon") || strings.Contains(out, "small tools") {
230 t.Fatalf("about or links not cleared: %s", out)
231 }
232 status, body = inst.get(t, "/workshop")
233 if status != 200 || !strings.Contains(body, "where things get made") ||
234 !strings.Contains(body, `href="https://workshop.example"`) {
235 t.Fatalf("org page profile: %d\n%s", status, body)
236 }
237}