package shutdown import ( "context" "errors" "sync/atomic" "syscall" "testing" "time" ) type mockLogger struct { infoMsgs []string errorMsgs []string } func (m *mockLogger) Info(msg string) { m.infoMsgs = append(m.infoMsgs, msg) } func (m *mockLogger) Error(msg string, _ error) { m.errorMsgs = append(m.errorMsgs, msg) } func TestHandle_RunsHooks(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) logger := &mockLogger{} var hookCalled int32 hooks := []Hook{ { Name: "test-hook", Fn: func() error { atomic.StoreInt32(&hookCalled, 1) return nil }, }, } done := make(chan struct{}) go func() { Handle(ctx, cancel, hooks, logger) close(done) }() // Send SIGTERM to trigger shutdown time.Sleep(50 * time.Millisecond) p, _ := syscall.Getpid(), 0 syscall.Kill(p, syscall.SIGTERM) select { case <-done: case <-time.After(3 * time.Second): t.Fatal("Handle did not return within timeout") } if atomic.LoadInt32(&hookCalled) != 1 { t.Error("hook was not called") } } func TestHandle_HookError_ContinuesOtherHooks(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) logger := &mockLogger{} var secondCalled int32 hooks := []Hook{ { Name: "failing-hook", Fn: func() error { return errors.New("hook error") }, }, { Name: "second-hook", Fn: func() error { atomic.StoreInt32(&secondCalled, 1) return nil }, }, } done := make(chan struct{}) go func() { Handle(ctx, cancel, hooks, logger) close(done) }() time.Sleep(50 * time.Millisecond) syscall.Kill(syscall.Getpid(), syscall.SIGTERM) select { case <-done: case <-time.After(3 * time.Second): t.Fatal("Handle did not return within timeout") } if atomic.LoadInt32(&secondCalled) != 1 { t.Error("second hook should still run after first hook error") } if len(logger.errorMsgs) == 0 { t.Error("error should be logged for failing hook") } } func TestHandle_ContextCancel(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) logger := &mockLogger{} var hookCalled int32 hooks := []Hook{ { Name: "ctx-hook", Fn: func() error { atomic.StoreInt32(&hookCalled, 1) return nil }, }, } done := make(chan struct{}) go func() { Handle(ctx, cancel, hooks, logger) close(done) }() // Cancel context directly instead of sending signal time.Sleep(50 * time.Millisecond) cancel() select { case <-done: case <-time.After(3 * time.Second): t.Fatal("Handle did not return within timeout after context cancel") } if atomic.LoadInt32(&hookCalled) != 1 { t.Error("hook should run on context cancel") } }