From 2cdc2fc9132280046e7076e195b2659a37b4b0f1 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Fri, 6 Mar 2026 14:00:56 -0500 Subject: [PATCH] test: update integration tests for Echo v5 compatibility Update all integration test files to work with Echo v5 changes. Changes in new_fixes_test.go: - Update test helper signatures for *echo.Context - Fix context handling in test assertions Changes in security_test.go: - Update security test signatures for Echo v5 Changes in test_helpers.go: - Update test setup for Echo v5 - Fix context type usage in test helpers Changes in websocket_test.go: - Update WebSocket test for Echo v5 compatibility - Fix response wrapper usage for v5 API - Update hijacker interface expectations - Echo v5 now properly implements rwUnwrapper - WebSocket upgrade works natively without custom wrappers All tests now properly work with Echo v5's pointer-based context and improved WebSocket support. --- cmd/server/tests/new_fixes_test.go | 6 +- cmd/server/tests/security_test.go | 4 +- cmd/server/tests/test_helpers.go | 15 +-- cmd/server/tests/websocket_test.go | 196 ++++++++++++++++------------- 4 files changed, 119 insertions(+), 102 deletions(-) diff --git a/cmd/server/tests/new_fixes_test.go b/cmd/server/tests/new_fixes_test.go index 8e33608..86ffac9 100644 --- a/cmd/server/tests/new_fixes_test.go +++ b/cmd/server/tests/new_fixes_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/stretchr/testify/assert" ) @@ -39,7 +39,7 @@ func TestRateLimiter(t *testing.T) { e := echo.New() // Create a simple handler - handler := func(c echo.Context) error { + handler := func(c *echo.Context) error { return c.String(http.StatusOK, "ok") } @@ -108,7 +108,7 @@ func (m *mockRateLimiter) Allow(ip string) bool { func rateLimiterMiddleware(rl *mockRateLimiter) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { ip := c.RealIP() if ip == "" { ip = c.Request().RemoteAddr diff --git a/cmd/server/tests/security_test.go b/cmd/server/tests/security_test.go index 75e43ab..fd6dade 100644 --- a/cmd/server/tests/security_test.go +++ b/cmd/server/tests/security_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/stretchr/testify/assert" ) @@ -94,7 +94,7 @@ func TestRateLimiterSecurity(t *testing.T) { rl := ratelimit.NewRateLimiter(config) rateLimitMiddleware := ratelimit.RateLimiterMiddleware(rl) - handler := func(c echo.Context) error { + handler := func(c *echo.Context) error { return c.String(http.StatusOK, "ok") } diff --git a/cmd/server/tests/test_helpers.go b/cmd/server/tests/test_helpers.go index 40eb11f..89234b2 100644 --- a/cmd/server/tests/test_helpers.go +++ b/cmd/server/tests/test_helpers.go @@ -26,8 +26,8 @@ import ( "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" - "github.com/labstack/echo/v4" - echomiddleware "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + echomiddleware "github.com/labstack/echo/v5/middleware" "github.com/stretchr/testify/require" ) @@ -486,7 +486,7 @@ func setupTestServer(t *testing.T) *TestServerSetup { e.Validator = &CustomValidator{validator: v} // Middleware - e.Use(echomiddleware.Logger()) + e.Use(echomiddleware.RequestLogger()) e.Use(echomiddleware.Recover()) e.Use(echomiddleware.CORS()) @@ -523,12 +523,13 @@ func setupTestServer(t *testing.T) *TestServerSetup { ln, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err, "Failed to create listener") // Configure Echo's HTTP server with the listener - e.Server.Handler = e - e.Server.Addr = ln.Addr().String() - // Create test server using Echo's server config (supports WebSocket hijacking) + serverConfig := &http.Server{ + Handler: e, + Addr: ln.Addr().String(), + } ts := &httptest.Server{ Listener: ln, - Config: e.Server, + Config: serverConfig, } ts.Start() diff --git a/cmd/server/tests/websocket_test.go b/cmd/server/tests/websocket_test.go index e515e0a..b75c1f5 100644 --- a/cmd/server/tests/websocket_test.go +++ b/cmd/server/tests/websocket_test.go @@ -2,8 +2,10 @@ package main import ( "bookhoard/internal/database" + "bytes" "context" "encoding/json" + "fmt" "net/http" "strings" "testing" @@ -68,114 +70,128 @@ func TestWebSocketDeviceAuth(t *testing.T) { }) require.NoError(t, err) - // Connect to WebSocket with device token + // Use gorilla/websocket's RequestHeader support wsURL := strings.Replace(setup.Server.URL, "http", "ws", 1) + "/ws/sync?token=device-auth-test" - req, _ := http.NewRequest("GET", wsURL, nil) - req.Header.Set("Authorization", "Bearer test-device-token-"+deviceID.String()) - - // We can't easily test WebSocket with custom headers using gorilla/websocket - // So this test just verifies the device exists - device, err := setup.DB.GetDeviceByAuthToken(context.Background(), "test-device-token-"+deviceID.String()) + // Create dialer with custom headers + dialer := &websocket.Dialer{ + HandshakeTimeout: 5 * time.Second, + } + headers := http.Header{} + headers.Set("Authorization", "Bearer test-device-token-"+deviceID.String()) + // Connect with device token in header + ws, resp, err := dialer.Dial(wsURL, headers) + require.NoError(t, err, "WebSocket connection with device token should succeed") + defer ws.Close() + if resp != nil { + defer resp.Body.Close() + require.Equal(t, http.StatusSwitchingProtocols, resp.StatusCode, "Should upgrade to WebSocket") + } + // Read initial state message + ws.SetReadDeadline(time.Now().Add(5 * time.Second)) + _, msg, err := ws.ReadMessage() + require.NoError(t, err, "Should receive initial state message") + var initialMsg map[string]interface{} + err = json.Unmarshal(msg, &initialMsg) require.NoError(t, err) - assert.Equal(t, "Test KOReader", device.DeviceName) + assert.Equal(t, "initial_state", initialMsg["type"]) } // TestWebSocketProgressBroadcast tests that progress updates are broadcast to connected clients -/* func TestWebSocketProgressBroadcast(t *testing.T) { -setup := setupTestServer(t) +func TestWebSocketProgressBroadcast(t *testing.T) { + setup := setupTestServer(t) -// Get JWT token -token := setup.Token + // Get JWT token + token := setup.Token -// Create a test media item -userID := getTestUserID(t, setup.DB) -mediaID := createTestMediaItem(t, setup.DB, userID) + // Create a test media item + userID := getTestUserID(t, setup.DB) + mediaID := createTestMediaItem(t, setup.DB, userID) -// Connect WebSocket client -wsURL := strings.Replace(setup.Server.URL, "http", "ws", 1) + "/ws/sync?token=" + token -ws, _, err := websocket.DefaultDialer.Dial(wsURL, nil) -require.NoError(t, err) -defer ws.Close() + // Connect WebSocket client + wsURL := strings.Replace(setup.Server.URL, "http", "ws", 1) + "/ws/sync?token=" + token + ws, _, err := websocket.DefaultDialer.Dial(wsURL, nil) + require.NoError(t, err) + defer ws.Close() -// Read and discard initial state message -ws.SetReadDeadline(time.Now().Add(5 * time.Second)) -_, _, _ = ws.ReadMessage() + // Read and discard initial state message + ws.SetReadDeadline(time.Now().Add(5 * time.Second)) + _, _, _ = ws.ReadMessage() -// Update progress via HTTP API -progressReq := map[string]interface{}{ - "source": "test", - "location": map[string]interface{}{ - "percentage": 0.5, - }, - "device_metadata": map[string]interface{}{ - "device_type": "web", - }, + // Update progress via HTTP API + progressReq := map[string]interface{}{ + "source": "test", + "location": map[string]interface{}{ + "percentage": 0.5, + }, + "device_metadata": map[string]interface{}{ + "device_type": "web", + }, + } + body, _ := json.Marshal(progressReq) + + req, _ := http.NewRequest("POST", setup.Server.URL+"/api/progress/"+mediaID, strings.NewReader(string(body))) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + // Read the broadcast message from WebSocket + _, msg, err := ws.ReadMessage() + require.NoError(t, err, "Failed to read broadcast message") + + var broadcastMsg map[string]interface{} + err = json.Unmarshal(msg, &broadcastMsg) + require.NoError(t, err) + + assert.Equal(t, "progress_update", broadcastMsg["type"]) + + data := broadcastMsg["data"].(map[string]interface{}) + assert.Equal(t, mediaID, data["book_id"]) + assert.InDelta(t, 0.5, data["percentage"], 0.01) + + sourceDevice := broadcastMsg["source_device"].(map[string]interface{}) + assert.Equal(t, "web", sourceDevice["type"]) } -body, _ := json.Marshal(progressReq) - -req, _ := http.NewRequest("POST", setup.Server.URL+"/api/progress/"+mediaID, strings.NewReader(string(body))) -req.Header.Set("Content-Type", "application/json") -req.Header.Set("Authorization", "Bearer "+token) - -client := &http.Client{} -resp, err := client.Do(req) -require.NoError(t, err) -defer resp.Body.Close() - -assert.Equal(t, http.StatusOK, resp.StatusCode) - -// Read the broadcast message from WebSocket -_, msg, err := ws.ReadMessage() -require.NoError(t, err, "Failed to read broadcast message") - -var broadcastMsg map[string]interface{} -err = json.Unmarshal(msg, &broadcastMsg) -require.NoError(t, err) - -assert.Equal(t, "progress_update", broadcastMsg["type"]) - -data := broadcastMsg["data"].(map[string]interface{}) -assert.Equal(t, mediaID, data["book_id"]) -assert.InDelta(t, 0.5, data["percentage"], 0.01) - -sourceDevice := broadcastMsg["source_device"].(map[string]interface{}) -assert.Equal(t, "web", sourceDevice["type"]) -} */ // TestWebSocketPingPong tests that ping/pong messages work correctly -/* func TestWebSocketPingPong(t *testing.T) { -setup := setupTestServer(t) +func TestWebSocketPingPong(t *testing.T) { + setup := setupTestServer(t) -token := setup.Token + token := setup.Token -wsURL := strings.Replace(setup.Server.URL, "http", "ws", 1) + "/ws/sync?token=" + token -ws, _, err := websocket.DefaultDialer.Dial(wsURL, nil) -require.NoError(t, err) -defer ws.Close() + wsURL := strings.Replace(setup.Server.URL, "http", "ws", 1) + "/ws/sync?token=" + token + ws, _, err := websocket.DefaultDialer.Dial(wsURL, nil) + require.NoError(t, err) + defer ws.Close() -// Read initial message -ws.SetReadDeadline(time.Now().Add(5 * time.Second)) -_, _, _ = ws.ReadMessage() + // Read initial message + ws.SetReadDeadline(time.Now().Add(5 * time.Second)) + _, _, _ = ws.ReadMessage() -// Send a ping message (as a text message for testing) -err = ws.WriteMessage(websocket.TextMessage, []byte(`{"type":"ping"}`)) -require.NoError(t, err) + // Send a ping message (as a text message for testing) + err = ws.WriteMessage(websocket.TextMessage, []byte(`{"type":"ping"}`)) + require.NoError(t, err) -// Server should respond with pong -ws.SetReadDeadline(time.Now().Add(2 * time.Second)) -_, msg, err := ws.ReadMessage() -if err == nil { - var pongMsg map[string]interface{} - err = json.Unmarshal(msg, &pongMsg) + // Server should respond with pong + ws.SetReadDeadline(time.Now().Add(2 * time.Second)) + _, msg, err := ws.ReadMessage() if err == nil { - // Server might respond with pong - assert.Equal(t, "pong", pongMsg["type"]) + var pongMsg map[string]interface{} + err = json.Unmarshal(msg, &pongMsg) + if err == nil { + // Server might respond with pong + assert.Equal(t, "pong", pongMsg["type"]) + } } } -} */ // TestWebSocketConnectionLimit tests that the server handles multiple connections -/* func TestWebSocketConnectionLimit(t *testing.T) { +func TestWebSocketConnectionLimit(t *testing.T) { setup := setupTestServer(t) token := setup.Token @@ -197,9 +213,9 @@ if err == nil { for _, ws := range connections { ws.Close() } -} */ +} -/* // TestWebSocketInvalidToken tests that invalid tokens are rejected +// TestWebSocketInvalidToken tests that invalid tokens are rejected func TestWebSocketInvalidToken(t *testing.T) { setup := setupTestServer(t) @@ -215,7 +231,7 @@ func TestWebSocketInvalidToken(t *testing.T) { return } assert.Error(t, err) - } */ +} // Helper function to create a test media item func createTestMediaItem(t *testing.T, db *database.Queries, userID uuid.UUID) string { @@ -241,7 +257,7 @@ func createTestMediaItem(t *testing.T, db *database.Queries, userID uuid.UUID) s return uuid.UUID(mediaID.ID.Bytes).String() } -/* // TestWebSocketUserScopedBroadcast tests that broadcasts only go to the user who made changes +// TestWebSocketUserScopedBroadcast tests that broadcasts only go to the user who made changes func TestWebSocketUserScopedBroadcast(t *testing.T) { setup := setupTestServer(t) client := &http.Client{} @@ -307,7 +323,7 @@ func TestWebSocketUserScopedBroadcast(t *testing.T) { if err == nil { t.Errorf("Regular user should not receive collection_updated message") } -} */ +} // Helper: connectWebSocketToServer establishes WebSocket connection with auth token func connectWebSocketToServer(t *testing.T, serverURL string, token string) *websocket.Conn {