package app import ( "context" "os" "syscall" "testing" "time" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // mockHandler is a mock implementation of the Handler interface for testing type mockHandler struct { startSchedulerCalled bool stopSchedulerCalled bool startDelay time.Duration stopDelay time.Duration startError error stopError error } func (m *mockHandler) StartScheduler() { m.startSchedulerCalled = true if m.startDelay > 0 { time.Sleep(m.startDelay) } if m.startError != nil { panic(m.startError) } } func (m *mockHandler) StopScheduler() { m.stopSchedulerCalled = true if m.stopDelay > 0 { time.Sleep(m.stopDelay) } if m.stopError != nil { panic(m.stopError) } } // reset resets the mock handler state func (m *mockHandler) reset() { m.startSchedulerCalled = false m.stopSchedulerCalled = false } // TestApp_New tests App constructor func TestApp_New(t *testing.T) { e := echo.New() handler := &mockHandler{} app := New(e, handler) assert.NotNil(t, app, "App should not be nil") assert.Equal(t, e, app.echo, "Echo instance should be stored") assert.Equal(t, handler, app.handler, "Handler should be stored") assert.Equal(t, 30*time.Second, app.shutdownTimeout, "Default shutdown timeout should be 30 seconds") assert.NotNil(t, app.shutdownDone, "Shutdown done channel should be initialized") } // TestApp_SetShutdownTimeout tests configurable shutdown timeout func TestApp_SetShutdownTimeout(t *testing.T) { e := echo.New() handler := &mockHandler{} app := New(e, handler) customTimeout := 15 * time.Second app.SetShutdownTimeout(customTimeout) assert.Equal(t, customTimeout, app.shutdownTimeout, "Shutdown timeout should be updated") } // TestApp_ShutdownDone tests shutdown done channel func TestApp_ShutdownDone(t *testing.T) { e := echo.New() handler := &mockHandler{} app := New(e, handler) channel := app.ShutdownDone() assert.NotNil(t, channel, "ShutdownDone should return a channel") // Verify it's the same channel by checking if it's readable select { case <-channel: // Channel should not be closed yet t.Error("ShutdownDone channel should not be closed immediately") default: // Expected - channel is open but not ready } } // TestApp_Start_BackgroundServices tests background service startup func TestApp_Start_BackgroundServices(t *testing.T) { e := echo.New() handler := &mockHandler{ startDelay: 100 * time.Millisecond, // Short delay to verify async startup } app := New(e, handler) // Start the app in a goroutine (simulating main.go) done := make(chan error, 1) go func() { // This will block until shutdown signal err := app.Start() done <- err }() // Wait a bit for background services to start time.Sleep(150 * time.Millisecond) // Verify scheduler was started assert.True(t, handler.startSchedulerCalled, "StartScheduler should be called") // Send shutdown signal to clean up done2 := make(chan bool) go func() { // Send shutdown signal process, err := os.FindProcess(os.Getpid()) require.NoError(t, err, "Should find current process") err = process.Signal(syscall.SIGTERM) done2 <- (err == nil) }() // Wait for app to finish select { case err := <-done: assert.NoError(t, err, "App should shut down without error") case <-time.After(5 * time.Second): t.Fatal("App did not shut down within timeout") } <-done2 } // TestApp_Shutdown_GracefulShutdown tests graceful shutdown sequence func TestApp_Shutdown_GracefulShutdown(t *testing.T) { e := echo.New() handler := &mockHandler{ stopDelay: 50 * time.Millisecond, } app := New(e, handler) // Test shutdown err := app.Shutdown() assert.NoError(t, err, "Shutdown should complete without error") assert.True(t, handler.stopSchedulerCalled, "StopScheduler should be called") } // TestApp_Shutdown_ThreadSafety tests thread-safe shutdown func TestApp_Shutdown_ThreadSafety(t *testing.T) { e := echo.New() handler := &mockHandler{} app := New(e, handler) // Call shutdown multiple times concurrently errors := make(chan error, 3) for i := 0; i < 3; i++ { go func() { errors <- app.Shutdown() }() } // Collect results for i := 0; i < 3; i++ { err := <-errors // First call should succeed, subsequent calls should also succeed (no-op) assert.NoError(t, err, "Concurrent shutdown calls should not error") } // Verify handler was stopped only once assert.True(t, handler.stopSchedulerCalled, "StopScheduler should be called at least once") } // TestApp_Shutdown_Timeout tests shutdown timeout handling func TestApp_Shutdown_Timeout(t *testing.T) { e := echo.New() handler := &mockHandler{ stopDelay: 2 * time.Second, // Longer than shutdown timeout } app := New(e, handler) app.SetShutdownTimeout(100 * time.Millisecond) // Set very short timeout // Test shutdown with timeout err := app.Shutdown() assert.Error(t, err, "Shutdown should timeout and return error") assert.True(t, handler.stopSchedulerCalled, "StopScheduler should still be called") } // TestApp_Shutdown_ClosesEchoServer tests HTTP server shutdown func TestApp_Shutdown_ClosesEchoServer(t *testing.T) { e := echo.New() handler := &mockHandler{} // Start a simple server go func() { e.Start(":0") // Use random port }() // Give server time to start time.Sleep(100 * time.Millisecond) app := New(e, handler) // Shutdown should close the echo server err := app.Shutdown() assert.NoError(t, err, "Shutdown should complete") assert.True(t, handler.stopSchedulerCalled, "StopScheduler should be called") } // TestApp_SignalHandling tests signal handling (SIGINT, SIGTERM, SIGQUIT) func TestApp_SignalHandling(t *testing.T) { tests := []struct { name string signal os.Signal }{ {"SIGINT", syscall.SIGINT}, {"SIGTERM", syscall.SIGTERM}, {"SIGQUIT", syscall.SIGQUIT}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { e := echo.New() handler := &mockHandler{} app := New(e, handler) // Start app in goroutine done := make(chan error, 1) go func() { done <- app.Start() }() // Wait for background services to start time.Sleep(100 * time.Millisecond) assert.True(t, handler.startSchedulerCalled, "StartScheduler should be called") // Send signal process, err := os.FindProcess(os.Getpid()) require.NoError(t, err, "Should find current process") err = process.Signal(tt.signal) require.NoError(t, err, "Should send signal") // Wait for shutdown select { case err := <-done: assert.NoError(t, err, "App should shut down without error") assert.True(t, handler.stopSchedulerCalled, "StopScheduler should be called") case <-time.After(5 * time.Second): t.Fatal("App did not shut down within timeout") } }) } } // TestApp_Integration_StartupSequence tests complete startup sequence func TestApp_Integration_StartupSequence(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } e := echo.New() handler := &mockHandler{} app := New(e, handler) // This test simulates the sequence in cmd/server/main.go: // 1. Create app // 2. Start HTTP server in background // 3. Call app.Start() which blocks until signal // Step 1: App already created above assert.NotNil(t, app, "App should be created") // Step 2: Start HTTP server (simulated) serverReady := make(chan bool) go func() { // In real main.go, this would be: e.Start(":" + cfg.ServerPort) // For testing, we don't actually start the server serverReady <- true <-context.Background().Done() // Block until done }() <-serverReady // Step 3: Start app lifecycle in background appDone := make(chan error, 1) go func() { appDone <- app.Start() }() // Verify background services started time.Sleep(150 * time.Millisecond) assert.True(t, handler.startSchedulerCalled, "Scheduler should be started") // Send shutdown signal (simulating user pressing Ctrl+C) process, err := os.FindProcess(os.Getpid()) require.NoError(t, err) err = process.Signal(syscall.SIGINT) require.NoError(t, err) // Wait for graceful shutdown select { case err := <-appDone: assert.NoError(t, err, "App should shut down gracefully") assert.True(t, handler.stopSchedulerCalled, "Scheduler should be stopped") case <-time.After(5 * time.Second): t.Fatal("App did not complete shutdown within timeout") } } // TestApp_HandlerInterface tests that Handler interface is properly implemented func TestApp_HandlerInterface(t *testing.T) { // This test verifies that the mock handler satisfies the Handler interface var _ Handler = &mockHandler{} } // TestApp_AutoStartVerification tests auto-start functionality func TestApp_AutoStartVerification(t *testing.T) { t.Run("Auto-start runs asynchronously", func(t *testing.T) { e := echo.New() handler := &mockHandler{ startDelay: 100 * time.Millisecond, } app := New(e, handler) // Start the app - it should start background services and return immediately // Then we send a shutdown signal right away done := make(chan error, 1) go func() { // This will start services in background, then wait for signal err := app.Start() done <- err }() // Wait a bit for background services to start time.Sleep(150 * time.Millisecond) // Verify scheduler was started in background assert.True(t, handler.startSchedulerCalled, "StartScheduler should be called in background") // Send shutdown signal process, err := os.FindProcess(os.Getpid()) require.NoError(t, err) _ = process.Signal(syscall.SIGTERM) // Wait for clean shutdown select { case err := <-done: assert.NoError(t, err, "App should shut down gracefully") case <-time.After(5 * time.Second): t.Fatal("App did not shut down within timeout") } }) } // BenchmarkApp_Shutdown benchmarks the shutdown process func BenchmarkApp_Shutdown(b *testing.B) { e := echo.New() handler := &mockHandler{} b.ResetTimer() for i := 0; i < b.N; i++ { handler.reset() app := New(e, handler) app.Shutdown() } }