e2e/facts_test.go

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

187 lines · 7186 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	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}