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