e2e/activity_test.go

f8b976a97290a20d552056a999511f5d27d8e8ec
gitbay/e2e/activity_test.go history · blame · raw

122 lines · 4264 bytes

  1package e2e
  2
  3import (
  4	"os"
  5	"path/filepath"
  6	"regexp"
  7	"slices"
  8	"strconv"
  9	"strings"
 10	"testing"
 11)
 12
 13func TestActivityGraph(t *testing.T) {
 14	t.Parallel()
 15	inst := startInstance(t)
 16	aliceKey := inst.newKey(t, "alice")
 17	bobKey := inst.newKey(t, "bob")
 18	inst.admin(t, "admin", "user", "create", "alice",
 19		"--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
 20	// bob's email is NOT verified: his commits must not attribute.
 21	inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
 22
 23	if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 {
 24		t.Fatal("repo create failed")
 25	}
 26	if _, _, code := inst.ssh(t, aliceKey, "", "repo", "access", "grant", "alice/app", "bob", "write"); code != 0 {
 27		t.Fatal("grant failed")
 28	}
 29
 30	work := t.TempDir()
 31	env := inst.gitEnv(aliceKey)
 32	// gitEnv pins GIT_AUTHOR_EMAIL; author identity comes from the env.
 33	// Clone before appending: sharing env's backing array would let the
 34	// second append clobber the first.
 35	aliceEnv := append(slices.Clone(env), "GIT_AUTHOR_EMAIL=alice@example.test", "GIT_AUTHOR_NAME=Alice")
 36	bobEnv := append(slices.Clone(env), "GIT_AUTHOR_EMAIL=bob@nowhere.test", "GIT_AUTHOR_NAME=Bob")
 37	mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
 38	dir := filepath.Join(work, "w")
 39	os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a\n"), 0o644)
 40	mustGit(t, dir, env, "checkout", "-q", "-b", "main")
 41	mustGit(t, dir, env, "add", ".")
 42	mustGit(t, dir, aliceEnv, "commit", "-q", "-m", "one")
 43	os.WriteFile(filepath.Join(dir, "b.txt"), []byte("b\n"), 0o644)
 44	mustGit(t, dir, env, "add", ".")
 45	mustGit(t, dir, bobEnv, "commit", "-q", "-m", "two")
 46	mustGit(t, dir, env, "push", "-q", "origin", "main")
 47
 48	// Today's cell counts alice's commit; bob's unverified authorship
 49	// contributes nothing and issue creation counts as an event.
 50	if _, _, code := inst.ssh(t, aliceKey, "", "issue", "create", "alice/app", "--title", "'x'"); code != 0 {
 51		t.Fatal("issue create failed")
 52	}
 53	status, body := inst.get(t, "/alice")
 54	if status != 200 || !strings.Contains(body, `class="actgraph"`) {
 55		t.Fatalf("graph missing: %d", status)
 56	}
 57	// alice: 1 attributed commit + events (issue.created, ...). The exact
 58	// cells depend on author-date timezone vs event UTC, so assert the
 59	// year total instead.
 60	total := activityTotal(t, body)
 61	if total < 2 {
 62		t.Fatalf("alice total = %d, want >= 2", total)
 63	}
 64	// bob authored a commit but his email is unverified: zero activity.
 65	_, bobBody := inst.get(t, "/bob")
 66	if bt := activityTotal(t, bobBody); bt != 0 {
 67		t.Fatalf("unverified author got credit: total %d", bt)
 68	}
 69
 70	// Re-pushing the same history (force) does not double-count.
 71	mustGit(t, dir, env, "push", "-q", "--force", "origin", "main")
 72	_, body2 := inst.get(t, "/alice")
 73	if body2 != body {
 74		// Counts must be identical; compare just the graph cells.
 75		if excerpt(body, "actgraph") != excerpt(body2, "actgraph") {
 76			t.Fatal("re-push changed activity counts")
 77		}
 78	}
 79
 80	// Org pages aggregate their repos' activity.
 81	if _, _, code := inst.ssh(t, aliceKey, "", "org", "create", "theorg"); code != 0 {
 82		t.Fatal("org create failed")
 83	}
 84	if _, _, code := inst.ssh(t, aliceKey, "", "repo", "transfer", "alice/app", "theorg"); code != 0 {
 85		t.Fatal("transfer failed")
 86	}
 87	_, orgBody := inst.get(t, "/theorg")
 88	if !strings.Contains(orgBody, `class="actgraph"`) || strings.Contains(orgBody, "0 in the last year") {
 89		t.Fatalf("org graph empty:\n%s", excerpt(orgBody, "activity"))
 90	}
 91
 92	// Backfill is idempotent and attributes only verified authors.
 93	out := inst.admin(t, "admin", "backfill-activity")
 94	if !strings.Contains(out, "theorg/app\t0 attributed") {
 95		t.Fatalf("backfill re-attributed existing commits: %s", out)
 96	}
 97}
 98
 99// excerpt returns ~600 bytes around the first occurrence of marker.
100func excerpt(s, marker string) string {
101	i := strings.Index(s, marker)
102	if i < 0 {
103		return "(marker missing)"
104	}
105	end := i + 600
106	if end > len(s) {
107		end = len(s)
108	}
109	return s[i:end]
110}
111
112var totalPat = regexp.MustCompile(`([0-9]+) in the last year`)
113
114func activityTotal(t *testing.T, body string) int {
115	t.Helper()
116	m := totalPat.FindStringSubmatch(body)
117	if m == nil {
118		t.Fatal("activity total missing from page")
119	}
120	n, _ := strconv.Atoi(m[1])
121	return n
122}