e2e/activity_test.go
122 lines · 4264 bytes
4 symbols in this file
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}