Go · 8124 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package checks_test
4
5 import (
6 "context"
7 "io"
8 "log/slog"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/jackc/pgx/v5/pgtype"
14 "github.com/jackc/pgx/v5/pgxpool"
15
16 "github.com/tenseleyFlow/shithub/internal/checks"
17 checksdb "github.com/tenseleyFlow/shithub/internal/checks/sqlc"
18 reposdb "github.com/tenseleyFlow/shithub/internal/repos/sqlc"
19 "github.com/tenseleyFlow/shithub/internal/testing/dbtest"
20 usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc"
21 )
22
23 const fixtureHash = "$argon2id$v=19$m=16384,t=1,p=1$" +
24 "AAAAAAAAAAAAAAAA$" +
25 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
26
27 type fx struct {
28 pool *pgxpool.Pool
29 deps checks.Deps
30 repoID int64
31 }
32
33 func setup(t *testing.T) fx {
34 t.Helper()
35 pool := dbtest.NewTestDB(t)
36 ctx := context.Background()
37
38 user, err := usersdb.New().CreateUser(ctx, pool, usersdb.CreateUserParams{
39 Username: "alice", DisplayName: "Alice", PasswordHash: fixtureHash,
40 })
41 if err != nil {
42 t.Fatalf("CreateUser: %v", err)
43 }
44 repo, err := reposdb.New().CreateRepo(ctx, pool, reposdb.CreateRepoParams{
45 OwnerUserID: pgtype.Int8{Int64: user.ID, Valid: true},
46 Name: "demo",
47 DefaultBranch: "trunk",
48 Visibility: reposdb.RepoVisibilityPublic,
49 })
50 if err != nil {
51 t.Fatalf("CreateRepo: %v", err)
52 }
53 return fx{
54 pool: pool,
55 deps: checks.Deps{Pool: pool, Logger: slog.New(slog.NewTextHandler(io.Discard, nil))},
56 repoID: repo.ID,
57 }
58 }
59
60 func TestCreate_AutoCreatesSuite(t *testing.T) {
61 f := setup(t)
62 ctx := context.Background()
63 run, err := checks.Create(ctx, f.deps, checks.CreateParams{
64 RepoID: f.repoID,
65 HeadSHA: strings.Repeat("a", 40),
66 Name: "lint",
67 })
68 if err != nil {
69 t.Fatalf("Create: %v", err)
70 }
71 if run.SuiteID == 0 {
72 t.Errorf("SuiteID should be set")
73 }
74 if run.Status != checksdb.CheckStatusQueued {
75 t.Errorf("default status: got %s, want queued", run.Status)
76 }
77 suites, _ := checksdb.New().ListCheckSuitesForCommit(ctx, f.pool, checksdb.ListCheckSuitesForCommitParams{
78 RepoID: f.repoID, HeadSha: run.HeadSha,
79 })
80 if len(suites) != 1 || suites[0].AppSlug != "external" {
81 t.Errorf("expected one external suite, got %v", suites)
82 }
83 }
84
85 func TestCreate_IdempotentByExternalID(t *testing.T) {
86 f := setup(t)
87 ctx := context.Background()
88 args := checks.CreateParams{
89 RepoID: f.repoID,
90 HeadSHA: strings.Repeat("a", 40),
91 Name: "lint",
92 ExternalID: "ci-job-42",
93 }
94 a, err := checks.Create(ctx, f.deps, args)
95 if err != nil {
96 t.Fatalf("first Create: %v", err)
97 }
98 b, err := checks.Create(ctx, f.deps, args)
99 if err != nil {
100 t.Fatalf("second Create: %v", err)
101 }
102 if a.ID != b.ID {
103 t.Errorf("expected same id (idempotent), got %d vs %d", a.ID, b.ID)
104 }
105 }
106
107 func TestCreate_RequiresConclusionWhenCompleted(t *testing.T) {
108 f := setup(t)
109 _, err := checks.Create(context.Background(), f.deps, checks.CreateParams{
110 RepoID: f.repoID,
111 HeadSHA: strings.Repeat("a", 40),
112 Name: "lint",
113 Status: "completed",
114 })
115 if err == nil {
116 t.Errorf("expected ErrCompletedNeedsConclusion, got nil")
117 }
118 }
119
120 func TestCreate_RejectsShortHeadSHA(t *testing.T) {
121 f := setup(t)
122 _, err := checks.Create(context.Background(), f.deps, checks.CreateParams{
123 RepoID: f.repoID,
124 HeadSHA: "abc",
125 Name: "lint",
126 })
127 if err == nil {
128 t.Errorf("expected ErrShortHeadSHA, got nil")
129 }
130 }
131
132 func TestCreate_RejectsTooLargeOutput(t *testing.T) {
133 f := setup(t)
134 big := strings.Repeat("x", checks.MaxOutputTextBytes+1)
135 _, err := checks.Create(context.Background(), f.deps, checks.CreateParams{
136 RepoID: f.repoID,
137 HeadSHA: strings.Repeat("a", 40),
138 Name: "lint",
139 Output: checks.Output{Text: big},
140 })
141 if err == nil {
142 t.Errorf("expected ErrOutputTextTooLarge, got nil")
143 }
144 }
145
146 func TestUpdate_RollsUpSuiteConclusion(t *testing.T) {
147 f := setup(t)
148 ctx := context.Background()
149 sha := strings.Repeat("a", 40)
150 a, _ := checks.Create(ctx, f.deps, checks.CreateParams{
151 RepoID: f.repoID, HeadSHA: sha, Name: "lint",
152 })
153 b, _ := checks.Create(ctx, f.deps, checks.CreateParams{
154 RepoID: f.repoID, HeadSHA: sha, Name: "test",
155 })
156 // Complete both as success.
157 for _, id := range []int64{a.ID, b.ID} {
158 if _, err := checks.Update(ctx, f.deps, checks.UpdateParams{
159 RunID: id,
160 HasStatus: true,
161 Status: "completed",
162 HasConclusion: true,
163 Conclusion: "success",
164 }); err != nil {
165 t.Fatalf("Update %d: %v", id, err)
166 }
167 }
168 suite, _ := checksdb.New().GetCheckSuite(ctx, f.pool, a.SuiteID)
169 if suite.Status != checksdb.CheckStatusCompleted {
170 t.Errorf("suite status: got %s, want completed", suite.Status)
171 }
172 if !suite.Conclusion.Valid || suite.Conclusion.CheckConclusion != checksdb.CheckConclusionSuccess {
173 t.Errorf("suite conclusion: got %+v, want success", suite.Conclusion)
174 }
175 }
176
177 func TestEvaluateRequiredChecks_NoRequired(t *testing.T) {
178 f := setup(t)
179 got, err := checks.EvaluateRequiredChecks(context.Background(), f.pool, checks.GateInputs{
180 RepoID: f.repoID, HeadSHA: strings.Repeat("a", 40),
181 })
182 if err != nil || !got.Satisfied {
183 t.Errorf("no required → satisfied; got %+v err=%v", got, err)
184 }
185 }
186
187 func TestEvaluateRequiredChecks_BlocksThenSatisfies(t *testing.T) {
188 f := setup(t)
189 ctx := context.Background()
190 sha := strings.Repeat("a", 40)
191 in := checks.GateInputs{
192 RepoID: f.repoID,
193 HeadSHA: sha,
194 RequiredNames: []string{"lint"},
195 }
196
197 // No run yet → blocked.
198 got, _ := checks.EvaluateRequiredChecks(ctx, f.pool, in)
199 if got.Satisfied {
200 t.Errorf("no run yet, expected blocked")
201 }
202 // Queued run → still blocked.
203 run, _ := checks.Create(ctx, f.deps, checks.CreateParams{
204 RepoID: f.repoID, HeadSHA: sha, Name: "lint",
205 })
206 got, _ = checks.EvaluateRequiredChecks(ctx, f.pool, in)
207 if got.Satisfied {
208 t.Errorf("queued run, expected blocked")
209 }
210 // Complete with failure → still blocked.
211 _, _ = checks.Update(ctx, f.deps, checks.UpdateParams{
212 RunID: run.ID, HasStatus: true, Status: "completed",
213 HasConclusion: true, Conclusion: "failure",
214 })
215 got, _ = checks.EvaluateRequiredChecks(ctx, f.pool, in)
216 if got.Satisfied {
217 t.Errorf("failure, expected blocked")
218 }
219 // Switch to success → satisfied.
220 _, _ = checks.Update(ctx, f.deps, checks.UpdateParams{
221 RunID: run.ID, HasStatus: true, Status: "completed",
222 HasConclusion: true, Conclusion: "success",
223 })
224 got, _ = checks.EvaluateRequiredChecks(ctx, f.pool, in)
225 if !got.Satisfied {
226 t.Errorf("success, expected satisfied; reason=%q missing=%v", got.Reason, got.Missing)
227 }
228 }
229
230 func TestStaleOnPush_MarksSuites(t *testing.T) {
231 f := setup(t)
232 ctx := context.Background()
233 prev := strings.Repeat("a", 40)
234 // Queue a run on prev → suite status=queued.
235 if _, err := checks.Create(ctx, f.deps, checks.CreateParams{
236 RepoID: f.repoID, HeadSHA: prev, Name: "lint",
237 }); err != nil {
238 t.Fatalf("Create: %v", err)
239 }
240 n, err := checks.MarkStaleForPreviousHead(ctx, f.deps, f.repoID, prev)
241 if err != nil {
242 t.Fatalf("MarkStaleForPreviousHead: %v", err)
243 }
244 if n != 1 {
245 t.Errorf("expected 1 suite marked stale, got %d", n)
246 }
247 suites, _ := checksdb.New().ListCheckSuitesForCommit(ctx, f.pool, checksdb.ListCheckSuitesForCommitParams{
248 RepoID: f.repoID, HeadSha: prev,
249 })
250 if len(suites) != 1 || !suites[0].Conclusion.Valid ||
251 suites[0].Conclusion.CheckConclusion != checksdb.CheckConclusionStale {
252 t.Errorf("post-stale: %+v", suites)
253 }
254 }
255
256 func TestUpdate_TimestampsRoundTrip(t *testing.T) {
257 f := setup(t)
258 ctx := context.Background()
259 when := time.Date(2026, 5, 8, 12, 0, 0, 0, time.UTC)
260 run, _ := checks.Create(ctx, f.deps, checks.CreateParams{
261 RepoID: f.repoID, HeadSHA: strings.Repeat("a", 40), Name: "lint",
262 })
263 if _, err := checks.Update(ctx, f.deps, checks.UpdateParams{
264 RunID: run.ID,
265 HasStatus: true, Status: "completed",
266 HasConclusion: true, Conclusion: "success",
267 HasStartedAt: true, StartedAt: when,
268 HasCompletedAt: true, CompletedAt: when.Add(30 * time.Second),
269 }); err != nil {
270 t.Fatalf("Update: %v", err)
271 }
272 got, _ := checksdb.New().GetCheckRun(ctx, f.pool, run.ID)
273 if !got.StartedAt.Valid || !got.CompletedAt.Valid {
274 t.Errorf("timestamps not set: %+v", got)
275 }
276 }
277