internal/httpd/feed_test.go

a32f7f2001c1c0bf38ba8c3360e6bc7ca3982fee
gitbay/internal/httpd/feed_test.go history · blame · raw

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}