refactor(tests): Create TestServerSetup struct with proper resource cleanup
BREAKING CHANGE: setupTestServer() now returns *TestServerSetup instead of (*httptest.Server, *database.Queries, *config.Config) This fixes the database connection and goroutine leak issues where: - Each test created a new pgxpool (default max_conns = 4) - connManager.StartCleanupTask() goroutine was never stopped - queueProcessor.Start() goroutine was never stopped - ~160 tests = potential 640+ leaked connections New TestServerSetup struct provides: - Automatic cleanup via t.Cleanup() - Proper goroutine cancellation - Database pool closing - Thread-safe close() method with mutex Phase 1 of test cleanup refactor.
This commit is contained in:
@@ -8,7 +8,7 @@ import (
|
|||||||
ratelimit "bookhoard/internal/middleware"
|
ratelimit "bookhoard/internal/middleware"
|
||||||
"bookhoard/internal/router"
|
"bookhoard/internal/router"
|
||||||
"bookhoard/internal/services"
|
"bookhoard/internal/services"
|
||||||
"bookhoard/internal/sync"
|
wsync "bookhoard/internal/sync"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -65,6 +66,58 @@ type DeviceTestData struct {
|
|||||||
PGType database.Devices
|
PGType database.Devices
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestServerSetup manages the lifecycle of a test server with proper resource cleanup
|
||||||
|
type TestServerSetup struct {
|
||||||
|
Server *httptest.Server
|
||||||
|
DB *database.Queries
|
||||||
|
DBPool *pgxpool.Pool
|
||||||
|
Config *config.Config
|
||||||
|
ConnManager *wsync.ConnectionManager
|
||||||
|
QueueProcessor *wsync.SyncQueueProcessor
|
||||||
|
CleanupCancel context.CancelFunc
|
||||||
|
QueueCtx context.Context
|
||||||
|
QueueCancel context.CancelFunc
|
||||||
|
mu sync.Mutex
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close cleans up all resources in the correct order
|
||||||
|
func (s *TestServerSetup) Close() error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if s.closed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop queue processor first
|
||||||
|
if s.QueueCancel != nil {
|
||||||
|
s.QueueCancel()
|
||||||
|
s.QueueCancel = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop connection manager cleanup task
|
||||||
|
if s.CleanupCancel != nil {
|
||||||
|
s.CleanupCancel()
|
||||||
|
s.CleanupCancel = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close HTTP server
|
||||||
|
if s.Server != nil {
|
||||||
|
s.Server.Close()
|
||||||
|
s.Server = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close database pool (this waits for all connections to be released)
|
||||||
|
if s.DBPool != nil {
|
||||||
|
s.DBPool.Close()
|
||||||
|
s.DBPool = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s.closed = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Helper functions for testing
|
// Helper functions for testing
|
||||||
func containsPrefix(s, prefix string) bool {
|
func containsPrefix(s, prefix string) bool {
|
||||||
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
|
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
|
||||||
@@ -130,18 +183,18 @@ func getCachePath() string {
|
|||||||
|
|
||||||
// setupDeviceTest creates a complete test environment for device tests
|
// setupDeviceTest creates a complete test environment for device tests
|
||||||
func setupDeviceTest(t *testing.T) *TestDeviceSetup {
|
func setupDeviceTest(t *testing.T) *TestDeviceSetup {
|
||||||
ts, db, cfg := setupTestServer(t)
|
serverSetup := setupTestServer(t)
|
||||||
|
|
||||||
// Create user ONCE with known credentials
|
// Create user ONCE with known credentials
|
||||||
user := createTestUserOnce(t, db)
|
user := createTestUserOnce(t, serverSetup.DB)
|
||||||
|
|
||||||
// Login to get token
|
// Login to get token
|
||||||
token := loginUserWithCredentials(t, ts, user.Email, user.Password)
|
token := loginUserWithCredentials(t, serverSetup.Server, user.Email, user.Password)
|
||||||
|
|
||||||
return &TestDeviceSetup{
|
return &TestDeviceSetup{
|
||||||
Server: ts,
|
Server: serverSetup.Server,
|
||||||
DB: db,
|
DB: serverSetup.DB,
|
||||||
Config: cfg,
|
Config: serverSetup.Config,
|
||||||
User: user,
|
User: user,
|
||||||
UserToken: token,
|
UserToken: token,
|
||||||
}
|
}
|
||||||
@@ -241,8 +294,8 @@ func (s *TestDeviceSetup) CreateDevice(t *testing.T, deviceName, deviceType, dev
|
|||||||
}
|
}
|
||||||
|
|
||||||
// setupTestServer creates a test server with a test database
|
// setupTestServer creates a test server with a test database
|
||||||
// Returns: (*httptest.Server, *database.Queries, *config.Config)
|
// Returns: *TestServerSetup with automatic cleanup via t.Cleanup
|
||||||
func setupTestServer(t *testing.T) (*httptest.Server, *database.Queries, *config.Config) {
|
func setupTestServer(t *testing.T) *TestServerSetup {
|
||||||
// Load configuration using the same method as main application
|
// Load configuration using the same method as main application
|
||||||
cfg := config.LoadConfig()
|
cfg := config.LoadConfig()
|
||||||
|
|
||||||
@@ -271,12 +324,13 @@ func setupTestServer(t *testing.T) (*httptest.Server, *database.Queries, *config
|
|||||||
deviceAuthMiddleware := middleware.NewDeviceAuthMiddleware(queries)
|
deviceAuthMiddleware := middleware.NewDeviceAuthMiddleware(queries)
|
||||||
|
|
||||||
// Create WebSocket connection manager
|
// Create WebSocket connection manager
|
||||||
connManager := sync.NewConnectionManager()
|
connManager := wsync.NewConnectionManager()
|
||||||
connManager.StartCleanupTask()
|
cleanupCancel := connManager.StartCleanupTask()
|
||||||
|
|
||||||
// Create sync queue processor
|
// Create sync queue processor with cancellable context
|
||||||
queueProcessor := sync.NewSyncQueueProcessor(queries)
|
queueProcessor := wsync.NewSyncQueueProcessor(queries)
|
||||||
go queueProcessor.Start(context.Background())
|
queueCtx, queueCancel := context.WithCancel(context.Background())
|
||||||
|
go queueProcessor.Start(queueCtx)
|
||||||
|
|
||||||
koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor)
|
koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor)
|
||||||
wsHandler := handlers.NewWSHandler(queries, connManager, cfg.JWTSecret, deviceAuthMiddleware)
|
wsHandler := handlers.NewWSHandler(queries, connManager, cfg.JWTSecret, deviceAuthMiddleware)
|
||||||
@@ -341,8 +395,27 @@ func setupTestServer(t *testing.T) (*httptest.Server, *database.Queries, *config
|
|||||||
// Create test server
|
// Create test server
|
||||||
ts := httptest.NewServer(e)
|
ts := httptest.NewServer(e)
|
||||||
|
|
||||||
// Return server, queries, and config
|
// Create TestServerSetup struct with all resources
|
||||||
return ts, queries, cfg
|
setup := &TestServerSetup{
|
||||||
|
Server: ts,
|
||||||
|
DB: queries,
|
||||||
|
DBPool: dbPool,
|
||||||
|
Config: cfg,
|
||||||
|
ConnManager: connManager,
|
||||||
|
QueueProcessor: queueProcessor,
|
||||||
|
CleanupCancel: cleanupCancel,
|
||||||
|
QueueCtx: queueCtx,
|
||||||
|
QueueCancel: queueCancel,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register cleanup function to run automatically when test completes
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := setup.Close(); err != nil {
|
||||||
|
t.Errorf("Failed to cleanup test server: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return setup
|
||||||
}
|
}
|
||||||
|
|
||||||
// loginTestUser logs in a test user and returns the JWT token
|
// loginTestUser logs in a test user and returns the JWT token
|
||||||
|
|||||||
Reference in New Issue
Block a user