Files
bookhoard/cmd/server/tests/websocket_test.go
T
john-okeefe b2a3955c1e fix(tests): complete TestServerSetup migration for remaining test files
Finish migrating all test files to the new TestServerSetup pattern
introduced by the goroutine cleanup refactoring. This resolves all
remaining compilation errors in the test suite.

Changes:
- device_cap_test.go: Fix undefined ts references (7 instances)
  * Replace ts.URL with setup.Server.URL in all test functions
  * Fix URL references in t.Run subtest closures

- queue_test.go: Fix undefined db and helper function issues (5 instances)
  * Replace db.CreateDevice with setup.DB.CreateDevice
  * Fix loginAdminUser() to use ts/db parameters instead of setup
  * Fix loginUserWithID() to use ts parameter instead of setup

- websocket_test.go: Convert 5 tests to new TestServerSetup pattern
  * Replace old pattern (ts, queries, _) with new pattern (setup)
  * Update all resource references to use setup.Server and setup.DB
  * Fix getTestUserID calls to include t parameter

Build Impact:
- All compilation errors resolved
- Integration tests now compile successfully
- No functional changes to test logic

Related: TestServerSetup cleanup pattern (TEST_CLEANUP_PATTERN.md)
2026-02-10 13:24:08 -05:00

244 lines
7.8 KiB
Go

package main
import (
"bookhoard/internal/database"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"github.com/jackc/pgx/v5/pgtype"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestWebSocketConnection tests basic WebSocket connection and authentication
func TestWebSocketConnection(t *testing.T) {
// Setup test server with WebSocket
setup := setupTestServer(t)
// Get JWT token for a test user
token := loginTestUser(t, setup.Server, setup.DB)
// Connect to WebSocket endpoint
wsURL := strings.Replace(setup.Server.URL, "http", "ws", 1) + "/ws/sync?token=" + token
ws, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
require.NoError(t, err, "Failed to connect to WebSocket")
defer ws.Close()
// Set read deadline
ws.SetReadDeadline(time.Now().Add(5 * time.Second))
// Wait for initial state message
_, msg, err := ws.ReadMessage()
require.NoError(t, err, "Failed to read initial message")
var initialMsg map[string]interface{}
err = json.Unmarshal(msg, &initialMsg)
require.NoError(t, err)
assert.Equal(t, "initial_state", initialMsg["type"])
assert.Contains(t, initialMsg["data"], "progress")
assert.Contains(t, initialMsg["data"], "devices")
}
// TestWebSocketDeviceAuth tests device authentication via WebSocket
func TestWebSocketDeviceAuth(t *testing.T) {
setup := setupTestServer(t)
// Create a test device
userID := getTestUserID(t, setup.DB)
deviceID := uuid.New()
_, err := setup.DB.CreateDevice(context.Background(), database.CreateDeviceParams{
UserID: pgtype.UUID{Bytes: [16]byte(userID), Valid: true},
DeviceName: "Test KOReader",
DeviceType: "koreader",
DeviceIdentifier: "test-device-123",
AuthToken: "test-device-token-" + deviceID.String(),
SyncEnabled: pgtype.Bool{Bool: true, Valid: true},
AutoSync: pgtype.Bool{Bool: true, Valid: true},
SyncFrequencyMinutes: pgtype.Int4{Int32: 5, Valid: true},
DeviceMetadata: []byte("{}"),
})
require.NoError(t, err)
// Connect to WebSocket with device token
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())
require.NoError(t, err)
assert.Equal(t, "Test KOReader", device.DeviceName)
}
// TestWebSocketProgressBroadcast tests that progress updates are broadcast to connected clients
func TestWebSocketProgressBroadcast(t *testing.T) {
setup := setupTestServer(t)
// Get JWT token
token := loginTestUser(t, setup.Server, setup.DB)
// 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()
// 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",
},
}
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)
token := loginTestUser(t, setup.Server, setup.DB)
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()
// 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)
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) {
setup := setupTestServer(t)
token := loginTestUser(t, setup.Server, setup.DB)
// Create multiple connections
connections := make([]*websocket.Conn, 5)
for i := 0; i < 5; i++ {
wsURL := strings.Replace(setup.Server.URL, "http", "ws", 1) + fmt.Sprintf("/ws/sync?token=%s&conn=%d", token, i)
ws, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
require.NoError(t, err, "Failed to create connection %d", i)
connections[i] = ws
// Read initial message
ws.SetReadDeadline(time.Now().Add(5 * time.Second))
_, _, _ = ws.ReadMessage()
}
// Close all connections
for _, ws := range connections {
ws.Close()
}
}
// TestWebSocketInvalidToken tests that invalid tokens are rejected
func TestWebSocketInvalidToken(t *testing.T) {
setup := setupTestServer(t)
// Try to connect with invalid token
wsURL := strings.Replace(setup.Server.URL, "http", "ws", 1) + "/ws/sync?token=invalid-token"
_, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
assert.Error(t, err, "Expected error when connecting with invalid token")
// Check if it's a websocket close error
if websocket.IsCloseError(err, 1000, 1001, 1002, 1003, 1005, 1006, 1007, 1008, 1009, 1010, 1011) {
// Expected close error
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 {
// First create a test library
libID, err := db.CreateLibrary(context.Background(), database.CreateLibraryParams{
Name: "Test Library",
LibraryTypeID: pgtype.UUID{Bytes: [16]byte(uuid.UUID{}), Valid: true},
CreatedByAdminID: pgtype.UUID{Bytes: [16]byte(userID), Valid: true},
})
require.NoError(t, err)
// Create a test media item
mediaID, err := db.CreateMediaItem(context.Background(), database.CreateMediaItemParams{
LibraryID: libID.ID,
Title: "Test Book",
FilePath: "/tmp/test.epub",
FileSize: pgtype.Int8{Int64: 1024, Valid: true},
MimeType: pgtype.Text{String: "application/epub+zip", Valid: true},
AddedByAdminID: pgtype.UUID{Bytes: [16]byte(userID), Valid: true},
})
require.NoError(t, err)
return uuid.UUID(mediaID.ID.Bytes).String()
}