package app import ( "context" "errors" "strings" "sync/atomic" "testing" "time" "galaxy/gamemaster/internal/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type fakeComponent struct { runErr error shutdownErr error runHook func(context.Context) error shutdownHook func(context.Context) error runCount atomic.Int32 downCount atomic.Int32 blockForCtx bool } func (component *fakeComponent) Run(ctx context.Context) error { component.runCount.Add(1) if component.runHook != nil { return component.runHook(ctx) } if component.blockForCtx { <-ctx.Done() return ctx.Err() } return component.runErr } func (component *fakeComponent) Shutdown(ctx context.Context) error { component.downCount.Add(1) if component.shutdownHook != nil { return component.shutdownHook(ctx) } return component.shutdownErr } func newCfg() config.Config { return config.Config{ShutdownTimeout: time.Second} } func TestAppRunWithoutComponentsBlocksUntilContextDone(t *testing.T) { t.Parallel() app := New(newCfg()) ctx, cancel := context.WithCancel(context.Background()) cancel() require.NoError(t, app.Run(ctx)) } func TestAppRunReturnsOnContextCancel(t *testing.T) { t.Parallel() component := &fakeComponent{blockForCtx: true} app := New(newCfg(), component) ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(10 * time.Millisecond) cancel() }() require.NoError(t, app.Run(ctx)) assert.EqualValues(t, 1, component.runCount.Load()) assert.EqualValues(t, 1, component.downCount.Load()) } func TestAppRunPropagatesComponentFailure(t *testing.T) { t.Parallel() failure := errors.New("boom") component := &fakeComponent{runErr: failure} app := New(newCfg(), component) err := app.Run(context.Background()) require.Error(t, err) require.ErrorIs(t, err, failure) assert.EqualValues(t, 1, component.downCount.Load()) } func TestAppRunFailsOnNilContext(t *testing.T) { t.Parallel() app := New(newCfg()) var ctx context.Context require.Error(t, app.Run(ctx)) } func TestAppRunFailsOnNonPositiveShutdownTimeout(t *testing.T) { t.Parallel() app := New(config.Config{}, &fakeComponent{}) require.Error(t, app.Run(context.Background())) } func TestAppRunFailsOnNilComponent(t *testing.T) { t.Parallel() app := New(newCfg(), nil) require.Error(t, app.Run(context.Background())) } func TestAppRunFlagsCleanExitBeforeShutdown(t *testing.T) { t.Parallel() component := &fakeComponent{} app := New(newCfg(), component) err := app.Run(context.Background()) require.Error(t, err) require.True(t, strings.Contains(err.Error(), "exited without error")) }