e2e/facts_test.go

c72da83f1a127ce5d5310bddea7240344b0dc11f
gitbay/e2e/facts_test.go history · blame · raw

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}