e2e/facts_test.go
185 lines · 7188 bytes
1package e2e
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strconv"
8 "strings"
9 "testing"
10)
11
12// TestRepoFacts covers the repository summary: counts, license, languages,
13// and contributors resolved to accounts where the email is verified.
14func TestRepoFacts(t *testing.T) {
15 inst := startInstance(t)
16 aliceKey := inst.newKey(t, "alice")
17 inst.admin(t, "admin", "user", "create", "alice",
18 "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
19 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 {
20 t.Fatalf("repo create: %s", errOut)
21 }
22
23 work := t.TempDir()
24 env := inst.gitEnv(aliceKey)
25 aliceEnv := append(append([]string{}, env...),
26 "GIT_AUTHOR_NAME=Alice", "GIT_AUTHOR_EMAIL=alice@example.test")
27 mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
28 dir := filepath.Join(work, "w")
29
30 os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n\nfunc main() {}\n"), 0o644)
31 os.WriteFile(filepath.Join(dir, "run.sh"), []byte("#!/bin/sh\necho hi\n"), 0o644)
32 os.WriteFile(filepath.Join(dir, "LICENSE"), []byte(zeroBSD), 0o644)
33 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
34 mustGit(t, dir, env, "add", ".")
35 mustGit(t, dir, aliceEnv, "commit", "-q", "-m", "one")
36 // A second commit by someone with no account here.
37 os.WriteFile(filepath.Join(dir, "extra.go"), []byte("package main\n"), 0o644)
38 mustGit(t, dir, env, "add", ".")
39 mustGit(t, dir, env, "commit", "-q", "-m", "two")
40 mustGit(t, dir, env, "push", "-q", "origin", "main")
41 mustGit(t, dir, env, "tag", "v0.1.0")
42 mustGit(t, dir, env, "push", "-q", "origin", "v0.1.0")
43
44 status, body := inst.get(t, "/alice/app")
45 if status != 200 {
46 t.Fatalf("repo home: %d", status)
47 }
48 for _, want := range []string{
49 "<strong>2</strong> commit", // both commits counted
50 "<strong>1</strong> branch",
51 "<strong>1</strong> tag",
52 "0BSD", // license detected and surfaced
53 "Go", // language census
54 "Shell", // and it is not single-language
55 "2 contributors",
56 } {
57 if !strings.Contains(body, want) {
58 t.Errorf("repo home missing %q", want)
59 }
60 }
61 // The contributor with a verified email links to their profile; the
62 // other is named without inventing an account for them.
63 if !strings.Contains(body, `href="/alice"`) {
64 t.Error("verified contributor not linked to their profile")
65 }
66 if strings.Contains(body, `href="/t"`) {
67 t.Error("unknown contributor linked to a profile that does not exist")
68 }
69
70 // Subdirectory listings are about the directory, not the repository.
71 mustGit(t, dir, env, "rm", "-q", "extra.go")
72 os.MkdirAll(filepath.Join(dir, "sub"), 0o755)
73 os.WriteFile(filepath.Join(dir, "sub", "x.go"), []byte("package sub\n"), 0o644)
74 mustGit(t, dir, env, "add", "-A")
75 mustGit(t, dir, env, "commit", "-q", "-m", "sub")
76 mustGit(t, dir, env, "push", "-q", "origin", "main")
77 if _, body := inst.get(t, "/alice/app/tree/main/sub"); strings.Contains(body, "contributor") {
78 t.Error("facts bar rendered on a subdirectory listing")
79 }
80
81 // A repository with no commits has no facts to state, and must still
82 // render — the empty case renders through the same page type.
83 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/empty"); code != 0 {
84 t.Fatal("repo create failed")
85 }
86 if status, body := inst.get(t, "/alice/empty"); status != 200 {
87 t.Fatalf("empty repo home: %d", status)
88 } else if strings.Contains(body, "contributor") {
89 t.Error("empty repository claims contributors")
90 }
91}
92
93const zeroBSD = `Permission to use, copy, modify, and/or distribute this software for any
94purpose with or without fee is hereby granted.
95
96THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
97WITH REGARD TO THIS SOFTWARE.
98`
99
100// TestContributorIdentity covers the two ways several addresses fold into
101// one contributor: the repository's own .mailmap, and the addresses an
102// account has verified here. Addresses with neither stay distinct, even
103// when they carry the same name.
104func TestContributorIdentity(t *testing.T) {
105 smtp := startFakeSMTP(t)
106 inst := startInstanceWith(t, fmt.Sprintf(
107 "[mail]\nsmtp_host = %q\nfrom = \"noreply@gitbay.test\"\n", smtp.addr))
108 aliceKey := inst.newKey(t, "alice")
109 inst.admin(t, "admin", "user", "create", "alice",
110 "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
111 // A second address on the same account, verified without the mail
112 // round trip — this test is about what the addresses mean, not how
113 // they got proven.
114 if _, errOut, code := inst.ssh(t, aliceKey, "", "email", "add", "alice2@example.test"); code != 0 {
115 t.Fatalf("email add: %s", errOut)
116 }
117 inst.admin(t, "admin", "email", "verify", "alice", "alice2@example.test")
118 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/ident"); code != 0 {
119 t.Fatalf("repo create: %s", errOut)
120 }
121
122 work := t.TempDir()
123 env := inst.gitEnv(aliceKey)
124 as := func(name, email string) []string {
125 return append(append([]string{}, env...),
126 "GIT_AUTHOR_NAME="+name, "GIT_AUTHOR_EMAIL="+email)
127 }
128 mustGit(t, work, env, "clone", inst.sshURL("alice/ident"), "w")
129 dir := filepath.Join(work, "w")
130 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
131
132 // The mailmap claims the old address; nothing else does.
133 os.WriteFile(filepath.Join(dir, ".mailmap"),
134 []byte("Alice <alice@example.test> <alice@old.test>\n"), 0o644)
135 mustGit(t, dir, env, "add", ".mailmap")
136 mustGit(t, dir, as("Alice", "alice@example.test"), "commit", "-q", "-m", "mailmap")
137
138 commits := []struct{ name, email string }{
139 {"Alice", "alice@example.test"},
140 {"Alice", "alice@example.test"},
141 {"A. Cleberg", "alice2@example.test"}, // verified here, not in the mailmap
142 {"A. Cleberg", "alice2@example.test"},
143 {"Alice Old", "alice@old.test"}, // in the mailmap, no account
144 {"Alice Old", "alice@old.test"},
145 {"Pat", "pat1@example.test"}, // same name, different people
146 {"Pat", "pat2@example.test"},
147 }
148 for i, c := range commits {
149 mustGit(t, dir, as(c.name, c.email),
150 "commit", "-q", "--allow-empty", "-m", strconv.Itoa(i))
151 }
152 mustGit(t, dir, env, "push", "-q", "origin", "main")
153
154 status, body := inst.get(t, "/alice/ident")
155 if status != 200 {
156 t.Fatalf("repo home: %d", status)
157 }
158 // Scope the positives to the contributors bar: the tip bar names an
159 // author too, and links it to the same profile.
160 _, contribs, ok := strings.Cut(body, `<p class="contribs">`)
161 if !ok {
162 t.Fatal("no contributors bar")
163 }
164 contribs, _, _ = strings.Cut(contribs, "</p>")
165 for _, want := range []string{
166 "3 contributors",
167 // Three addresses, one account, one row, named by the account.
168 `title="alice@example.test · 7 commits">alice</a>`,
169 // One commit is one commit.
170 `title="pat1@example.test · 1 commit"`,
171 `title="pat2@example.test · 1 commit"`,
172 } {
173 if !strings.Contains(contribs, want) {
174 t.Errorf("contributors bar missing %q", want)
175 }
176 }
177 if n := strings.Count(contribs, `href="/alice"`); n != 1 {
178 t.Errorf("account listed as %d contributors, want 1", n)
179 }
180 for _, gone := range []string{"alice@old.test", "alice2@example.test", "A. Cleberg", "Alice Old"} {
181 if strings.Contains(body, gone) {
182 t.Errorf("collapsed identity %q still shown", gone)
183 }
184 }
185}