Files
repospawner/internal/jobs/state_test.go
T
unkin-agent f1bcb8cd3a
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Add the initial repospawner service
repospawner turns JSON new-repo requests into terraform-git pull requests
via kubernetes Jobs, follows those PRs to merge and optionally activates
the repository in Woodpecker.
2026-08-30 14:33:31 +10:00

165 lines
4.9 KiB
Go

package jobs
import (
"strings"
"testing"
"git.unkin.net/unkin/repospawner/internal/store"
)
func view(t Type, succeeded, failed bool, result string) View {
return View{Type: t, Request: "abc123", Succeeded: succeeded, Failed: failed, Result: []byte(result)}
}
func TestAdvance(t *testing.T) {
base := store.Request{ID: "abc123", Name: "widget", State: store.StateOpeningPR}
withCI := base
withCI.Woodpecker = true
prDone := `{"pr_number":42,"pr_url":"https://git.unkin.net/unkin/terraform-git/pulls/42"}`
cases := []struct {
name string
in store.Request
views map[Type]View
wantState store.State
wantAction Action
wantPR int
wantErrSub string
}{
{
name: "pr job still running",
in: base,
views: map[Type]View{TypePR: view(TypePR, false, false, "")},
wantState: store.StateOpeningPR,
wantAction: ActionNone,
},
{
name: "pr opened starts the watch",
in: base,
views: map[Type]View{TypePR: view(TypePR, true, false, prDone)},
wantState: store.StatePROpen,
wantAction: ActionCreateWatch,
wantPR: 42,
},
{
name: "pr job failed",
in: base,
views: map[Type]View{TypePR: view(TypePR, false, true, `{"error":"create branch: status 403"}`)},
wantState: store.StateFailed,
wantAction: ActionNone,
wantErrSub: "status 403",
},
{
name: "pr job succeeded but said nothing",
in: base,
views: map[Type]View{TypePR: view(TypePR, true, false, "")},
wantState: store.StateFailed,
wantAction: ActionNone,
wantErrSub: "without reporting",
},
{
name: "watch running keeps the pr open",
in: base,
views: map[Type]View{
TypePR: view(TypePR, true, false, prDone),
TypeWatch: view(TypeWatch, false, false, ""),
},
wantState: store.StatePROpen,
wantAction: ActionNone,
wantPR: 42,
},
{
name: "merge without woodpecker is ready",
in: base,
views: map[Type]View{
TypePR: view(TypePR, true, false, prDone),
TypeWatch: view(TypeWatch, true, false, `{"merged":true}`),
},
wantState: store.StateReady,
wantAction: ActionNone,
wantPR: 42,
},
{
name: "merge with woodpecker enables ci",
in: withCI,
views: map[Type]View{
TypePR: view(TypePR, true, false, prDone),
TypeWatch: view(TypeWatch, true, false, `{"merged":true}`),
},
wantState: store.StateEnablingCI,
wantAction: ActionCreateWoodpecker,
wantPR: 42,
},
{
name: "closed pull request stops everything",
in: withCI,
views: map[Type]View{
TypePR: view(TypePR, true, false, prDone),
TypeWatch: view(TypeWatch, true, false, `{"closed":true}`),
},
wantState: store.StateClosed,
wantAction: ActionNone,
},
{
name: "woodpecker enablement completes the request",
in: withCI,
views: map[Type]View{
TypeWatch: view(TypeWatch, true, false, `{"merged":true}`),
TypeWoodpecker: view(TypeWoodpecker, true, false, `{"enabled":true,"repo_id":9}`),
},
wantState: store.StateReady,
wantAction: ActionNone,
},
{
name: "woodpecker enablement failure surfaces its error",
in: withCI,
views: map[Type]View{
TypeWatch: view(TypeWatch, true, false, `{"merged":true}`),
TypeWoodpecker: view(TypeWoodpecker, false, true, `{"error":"enable in woodpecker: status 401"}`),
},
wantState: store.StateFailed,
wantAction: ActionNone,
wantErrSub: "status 401",
},
{
name: "watch job failure fails the request",
in: base,
views: map[Type]View{TypeWatch: view(TypeWatch, false, true, "")},
wantState: store.StateFailed,
wantAction: ActionNone,
wantErrSub: "watch job failed",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, action := Advance(tc.in, tc.views)
if got.State != tc.wantState {
t.Errorf("state = %q, want %q", got.State, tc.wantState)
}
if action != tc.wantAction {
t.Errorf("action = %q, want %q", action, tc.wantAction)
}
if tc.wantPR != 0 && got.PRNumber != tc.wantPR {
t.Errorf("prNumber = %d, want %d", got.PRNumber, tc.wantPR)
}
if tc.wantErrSub != "" && !strings.Contains(got.Error, tc.wantErrSub) {
t.Errorf("error = %q, want it to mention %q", got.Error, tc.wantErrSub)
}
if tc.wantState == store.StateReady && tc.in.Woodpecker && !got.CIEnabled {
t.Errorf("ciEnabled = false for a completed woodpecker request")
}
})
}
}
// TestAdvanceIsStableAfterTTL covers the window where finished Jobs have been
// garbage collected: a terminal request must not be re-driven.
func TestAdvanceIsStableAfterTTL(t *testing.T) {
done := store.Request{ID: "abc123", Name: "widget", Woodpecker: true, State: store.StateReady, CIEnabled: true, PRNumber: 42}
got, action := Advance(done, map[Type]View{})
if got.State != store.StateReady || action != ActionNone {
t.Errorf("state = %q action = %q, want ready/none", got.State, action)
}
}