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) } }