package game import ( "errors" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestStatusIsKnown(t *testing.T) { for _, status := range []Status{ StatusDraft, StatusEnrollmentOpen, StatusReadyToStart, StatusStarting, StatusStartFailed, StatusRunning, StatusPaused, StatusFinished, StatusCancelled, } { assert.Truef(t, status.IsKnown(), "expected %q known", status) } assert.False(t, Status("").IsKnown()) assert.False(t, Status("unknown").IsKnown()) } func TestStatusIsTerminal(t *testing.T) { assert.True(t, StatusFinished.IsTerminal()) assert.True(t, StatusCancelled.IsTerminal()) for _, status := range []Status{ StatusDraft, StatusEnrollmentOpen, StatusReadyToStart, StatusStarting, StatusStartFailed, StatusRunning, StatusPaused, } { assert.Falsef(t, status.IsTerminal(), "expected %q non-terminal", status) } } func TestTriggerIsKnown(t *testing.T) { for _, trigger := range []Trigger{ TriggerCommand, TriggerManual, TriggerDeadline, TriggerGap, TriggerRuntimeEvent, TriggerExternalBlock, } { assert.Truef(t, trigger.IsKnown(), "expected %q known", trigger) } assert.False(t, Trigger("").IsKnown()) assert.False(t, Trigger("bogus").IsKnown()) } func TestTransitionHappyPathsCoverFrozenTable(t *testing.T) { cases := []struct { from Status to Status triggers []Trigger }{ {StatusDraft, StatusEnrollmentOpen, []Trigger{TriggerCommand}}, {StatusEnrollmentOpen, StatusReadyToStart, []Trigger{TriggerManual, TriggerDeadline, TriggerGap}}, {StatusReadyToStart, StatusStarting, []Trigger{TriggerCommand}}, {StatusStarting, StatusRunning, []Trigger{TriggerRuntimeEvent}}, {StatusStarting, StatusPaused, []Trigger{TriggerRuntimeEvent}}, {StatusStarting, StatusStartFailed, []Trigger{TriggerRuntimeEvent}}, {StatusStartFailed, StatusReadyToStart, []Trigger{TriggerCommand}}, {StatusRunning, StatusPaused, []Trigger{TriggerCommand}}, {StatusRunning, StatusFinished, []Trigger{TriggerRuntimeEvent}}, {StatusPaused, StatusRunning, []Trigger{TriggerCommand}}, {StatusPaused, StatusFinished, []Trigger{TriggerRuntimeEvent}}, {StatusDraft, StatusCancelled, []Trigger{TriggerCommand, TriggerExternalBlock}}, {StatusEnrollmentOpen, StatusCancelled, []Trigger{TriggerCommand, TriggerExternalBlock}}, {StatusReadyToStart, StatusCancelled, []Trigger{TriggerCommand, TriggerExternalBlock}}, {StatusStartFailed, StatusCancelled, []Trigger{TriggerCommand, TriggerExternalBlock}}, {StatusStarting, StatusCancelled, []Trigger{TriggerExternalBlock}}, {StatusRunning, StatusCancelled, []Trigger{TriggerExternalBlock}}, {StatusPaused, StatusCancelled, []Trigger{TriggerExternalBlock}}, } for _, tc := range cases { for _, trigger := range tc.triggers { t.Run(string(tc.from)+"->"+string(tc.to)+"/"+string(trigger), func(t *testing.T) { require.NoError(t, Transition(tc.from, tc.to, trigger)) }) } } } func TestTransitionRejectsUnknownPair(t *testing.T) { err := Transition(StatusDraft, StatusRunning, TriggerCommand) require.Error(t, err) assert.True(t, errors.Is(err, ErrInvalidTransition)) var typed *InvalidTransitionError require.True(t, errors.As(err, &typed)) assert.Equal(t, StatusDraft, typed.From) assert.Equal(t, StatusRunning, typed.To) assert.Equal(t, TriggerCommand, typed.Trigger) } func TestTransitionRejectsWrongTrigger(t *testing.T) { err := Transition(StatusDraft, StatusEnrollmentOpen, TriggerDeadline) require.Error(t, err) assert.True(t, errors.Is(err, ErrInvalidTransition)) } func TestTransitionRejectsUnknownStatusOrTrigger(t *testing.T) { require.Error(t, Transition(Status("bogus"), StatusEnrollmentOpen, TriggerCommand)) require.Error(t, Transition(StatusDraft, Status("bogus"), TriggerCommand)) require.Error(t, Transition(StatusDraft, StatusEnrollmentOpen, Trigger("bogus"))) } func TestTransitionsOutOfTerminalStatusAllRejected(t *testing.T) { triggers := []Trigger{ TriggerCommand, TriggerManual, TriggerDeadline, TriggerGap, TriggerRuntimeEvent, TriggerExternalBlock, } for _, from := range []Status{StatusFinished, StatusCancelled} { for _, to := range []Status{ StatusDraft, StatusEnrollmentOpen, StatusReadyToStart, StatusStarting, StatusStartFailed, StatusRunning, StatusPaused, StatusFinished, StatusCancelled, } { for _, trigger := range triggers { if from == to { continue } err := Transition(from, to, trigger) require.Errorf(t, err, "%s->%s via %s should be rejected", from, to, trigger) } } } } func TestAllowedTransitionsSnapshotMatchesTable(t *testing.T) { snapshot := AllowedTransitions() count := 0 for _, inner := range snapshot { count += len(inner) } assert.Equal(t, len(allowedTransitions), count) for key, triggers := range allowedTransitions { inner, ok := snapshot[key.from] require.Truef(t, ok, "expected from=%s in snapshot", key.from) list, ok := inner[key.to] require.Truef(t, ok, "expected to=%s under from=%s", key.to, key.from) for trigger := range triggers { assert.Containsf(t, list, trigger, "missing trigger %q for %s->%s", trigger, key.from, key.to) } } }