f1bcb8cd3a
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.
165 lines
4.9 KiB
Go
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)
|
|
}
|
|
}
|