internal/httpd/feed_test.go
156 lines · 5871 bytes
1package httpd
2
3import (
4 "testing"
5 "time"
6
7 "gitbay.org/gitbay/internal/store"
8)
9
10// D04: build events on the same commit fold into one feed line, a "run",
11// whose State is the worst of the folded jobs' outcomes.
12func TestFeedLinesFoldsBuildRunsBySHA(t *testing.T) {
13 events := []store.FeedEvent{
14 {RepoPath: "alice/app", Actor: "alice", Kind: "build.success",
15 Data: `{"number":1,"job":"unit","sha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}`},
16 {RepoPath: "alice/app", Actor: "alice", Kind: "build.failure",
17 Data: `{"number":2,"job":"lint","sha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}`},
18 }
19 lines := feedLines(events)
20 if len(lines) != 1 {
21 t.Fatalf("feedLines returned %d lines, want 1: %+v", len(lines), lines)
22 }
23 l := lines[0]
24 if l.State != "failure" {
25 t.Errorf("State = %q, want failure", l.State)
26 }
27 if l.Verb != "ran 2 jobs on" {
28 t.Errorf("Verb = %q, want %q", l.Verb, "ran 2 jobs on")
29 }
30 if l.Ref != "aaaaaaaaaa" {
31 t.Errorf("Ref = %q, want short sha", l.Ref)
32 }
33 if l.URL != "/alice/app/commit/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" {
34 t.Errorf("URL = %q", l.URL)
35 }
36 if len(l.Jobs) != 2 || l.Jobs[0] != "unit" || l.Jobs[1] != "lint" {
37 t.Errorf("Jobs = %+v", l.Jobs)
38 }
39}
40
41// A single build event with a sha still gets a State, and keeps its
42// ordinary verb/ref/url — a run of one job reads the same as before.
43func TestFeedLinesSingleBuildGetsState(t *testing.T) {
44 events := []store.FeedEvent{
45 {RepoPath: "alice/app", Actor: "alice", Kind: "build.success",
46 Data: `{"number":1,"job":"unit","sha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}`},
47 }
48 lines := feedLines(events)
49 if len(lines) != 1 {
50 t.Fatalf("feedLines returned %d lines, want 1", len(lines))
51 }
52 l := lines[0]
53 if l.State != "success" {
54 t.Errorf("State = %q, want success", l.State)
55 }
56 if l.Verb != "build success" || l.Ref != "unit" || l.URL != "/alice/app/builds/1" {
57 t.Errorf("single build line changed shape: %+v", l)
58 }
59}
60
61// Two different commits never fold, even back to back.
62func TestFeedLinesDoesNotFoldAcrossDifferentSHAs(t *testing.T) {
63 events := []store.FeedEvent{
64 {RepoPath: "alice/app", Actor: "alice", Kind: "build.success",
65 Data: `{"number":1,"job":"unit","sha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}`},
66 {RepoPath: "alice/app", Actor: "alice", Kind: "build.success",
67 Data: `{"number":2,"job":"lint","sha":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}`},
68 }
69 lines := feedLines(events)
70 if len(lines) != 2 {
71 t.Fatalf("feedLines returned %d lines, want 2: %+v", len(lines), lines)
72 }
73}
74
75// An event with no sha (an older event recorded before this field existed)
76// never folds into anything, even when it shares a repo with an adjacent
77// build event.
78func TestFeedLinesNoSHANeverFolds(t *testing.T) {
79 events := []store.FeedEvent{
80 {RepoPath: "alice/app", Actor: "alice", Kind: "build.success", Data: `{"number":1,"job":"unit"}`},
81 {RepoPath: "alice/app", Actor: "alice", Kind: "build.success", Data: `{"number":2,"job":"lint"}`},
82 }
83 lines := feedLines(events)
84 if len(lines) != 2 {
85 t.Fatalf("feedLines returned %d lines, want 2: %+v", len(lines), lines)
86 }
87 if lines[0].State != "" || lines[1].State != "" {
88 t.Errorf("shaless build lines got a State: %+v", lines)
89 }
90}
91
92// A non-build event between two builds of the same commit breaks the
93// fold: only adjacent build events on the same commit combine.
94func TestFeedLinesNonBuildEventBreaksFold(t *testing.T) {
95 events := []store.FeedEvent{
96 {RepoPath: "alice/app", Actor: "alice", Kind: "build.success",
97 Data: `{"number":1,"job":"unit","sha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}`},
98 {RepoPath: "alice/app", Actor: "bob", Kind: "issue.created", Data: `{"number":1}`},
99 {RepoPath: "alice/app", Actor: "alice", Kind: "build.failure",
100 Data: `{"number":2,"job":"lint","sha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}`},
101 }
102 lines := feedLines(events)
103 if len(lines) != 3 {
104 t.Fatalf("feedLines returned %d lines, want 3: %+v", len(lines), lines)
105 }
106}
107
108// worstStatus governs the run's combined State the same way it governs
109// combinedStatus for the builds tab.
110func TestFeedLinesRunStatePrecedence(t *testing.T) {
111 cases := []struct {
112 statuses []string
113 want string
114 }{
115 {[]string{"success", "success"}, "success"},
116 {[]string{"success", "pending"}, "pending"},
117 {[]string{"pending", "running"}, "running"},
118 {[]string{"running", "cancelled"}, "cancelled"},
119 {[]string{"cancelled", "failure"}, "failure"},
120 }
121 for _, tc := range cases {
122 events := make([]store.FeedEvent, len(tc.statuses))
123 for i, s := range tc.statuses {
124 events[i] = store.FeedEvent{RepoPath: "alice/app", Actor: "alice", Kind: "build." + s,
125 Data: `{"number":1,"job":"j","sha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}`}
126 }
127 lines := feedLines(events)
128 if len(lines) != 1 || lines[0].State != tc.want {
129 t.Errorf("statuses %v: got %+v, want State %q", tc.statuses, lines, tc.want)
130 }
131 }
132}
133
134// D05: feedLines parses the stored RFC3339 timestamp into WhenT for the
135// template's relative-time rendering; an unparseable value leaves it zero
136// rather than panicking or guessing.
137func TestFeedLinesParsesWhenT(t *testing.T) {
138 events := []store.FeedEvent{
139 {RepoPath: "alice/app", Actor: "alice", Kind: "issue.created",
140 Data: `{"number":1}`, CreatedAt: "2026-09-10T12:00:00Z"},
141 {RepoPath: "alice/app", Actor: "alice", Kind: "issue.created",
142 Data: `{"number":2}`, CreatedAt: "not-a-time"},
143 }
144 lines := feedLines(events)
145 want, _ := time.Parse(time.RFC3339Nano, "2026-09-10T12:00:00Z")
146 if !lines[0].WhenT.Equal(want) {
147 t.Errorf("WhenT = %v, want %v", lines[0].WhenT, want)
148 }
149 if !lines[1].WhenT.IsZero() {
150 t.Errorf("WhenT for bad timestamp = %v, want zero", lines[1].WhenT)
151 }
152 // When is preserved for anything that still reads the raw string.
153 if lines[0].When != "2026-09-10T12:00:00Z" {
154 t.Errorf("When = %q", lines[0].When)
155 }
156}