package app import ( "context" "sync" "testing" "time" "galaxy/mail/internal/config" "github.com/stretchr/testify/require" ) func TestAppRunStopsComponentsOnContextCancellation(t *testing.T) { t.Parallel() component := &blockingComponent{} app := New(config.Config{ShutdownTimeout: time.Second}, component) ctx, cancel := context.WithCancel(context.Background()) done := make(chan error, 1) go func() { done <- app.Run(ctx) }() require.Eventually(t, func() bool { component.mu.Lock() defer component.mu.Unlock() return component.runStarted }, time.Second, 10*time.Millisecond) cancel() require.Eventually(t, func() bool { select { case err := <-done: return err == nil default: return false } }, time.Second, 10*time.Millisecond) require.Equal(t, 1, component.shutdownCalls) } func TestAppRunReportsEarlyComponentExit(t *testing.T) { t.Parallel() app := New(config.Config{ShutdownTimeout: time.Second}, componentFunc(func(context.Context) error { return nil })) err := app.Run(context.Background()) require.Error(t, err) require.Contains(t, err.Error(), "exited without error before shutdown") } type blockingComponent struct { mu sync.Mutex runStarted bool shutdownCalls int } func (component *blockingComponent) Run(ctx context.Context) error { component.mu.Lock() component.runStarted = true component.mu.Unlock() <-ctx.Done() return ctx.Err() } func (component *blockingComponent) Shutdown(context.Context) error { component.shutdownCalls++ return nil } type componentFunc func(context.Context) error func (fn componentFunc) Run(ctx context.Context) error { return fn(ctx) } func (fn componentFunc) Shutdown(context.Context) error { return nil }