From 1413c75b265d787aaebb08f0300c9b7ba7339f9e Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Tue, 10 Feb 2026 09:31:35 -0500 Subject: [PATCH] fix: Fix device response fields and add missing approved confirmation feat: Improve device test infrastructure with setupDeviceTest helper refactor: Standardize pending registrations API response field names --- cmd/server/tests/device_test.go | 106 +++++------------------ cmd/server/tests/test_helpers.go | 140 +++++++++++++++++++++++++++++++ internal/handlers/devices.go | 10 ++- 3 files changed, 169 insertions(+), 87 deletions(-) diff --git a/cmd/server/tests/device_test.go b/cmd/server/tests/device_test.go index 20feb39..7c70155 100644 --- a/cmd/server/tests/device_test.go +++ b/cmd/server/tests/device_test.go @@ -68,7 +68,7 @@ func TestDeviceRegistrationFlow(t *testing.T) { // Step 3: Login as user to approve device loginRequest := map[string]interface{}{ "login": "testuser@example.com", - "password": "testpass123", + "password": "Test@Pass123!", } loginBody, _ := json.Marshal(loginRequest) @@ -115,34 +115,17 @@ func TestDeviceRegistrationFlow(t *testing.T) { } func TestListDevices(t *testing.T) { - ts, db, _ := setupTestServer(t) - defer ts.Close() + setup := setupDeviceTest(t) + defer setup.Server.Close() - // Login to get token - token := loginTestUser(t, ts, db) - - // Create a device directly in the database - userID := getTestUserID(t, db) - - deviceToken := fmt.Sprintf("dev_%s", uuid.New().String()) - _, err := db.CreateDevice(context.Background(), database.CreateDeviceParams{ - UserID: pgtype.UUID{Bytes: [16]byte(userID), Valid: true}, - DeviceName: "Test Device", - DeviceType: "koreader", - DeviceIdentifier: "test-device-123", - AuthToken: deviceToken, - SyncEnabled: pgtype.Bool{Bool: true, Valid: true}, - AutoSync: pgtype.Bool{Bool: true, Valid: true}, - SyncFrequencyMinutes: pgtype.Int4{Int32: 5, Valid: true}, - DeviceMetadata: []byte("{}"), - }) - assert.NoError(t, err, "Should create device") + // Create a device using the setup helper + _ = setup.CreateDevice(t, "Test Device", "koreader", "test-device-123") // List devices req := httptest.NewRequest("GET", "/api/devices", nil) - req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Authorization", "Bearer "+setup.UserToken) rec := httptest.NewRecorder() - ts.Config.Handler.ServeHTTP(rec, req) + setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code, "Should list devices") @@ -160,33 +143,11 @@ func TestListDevices(t *testing.T) { } func TestUpdateDevice(t *testing.T) { - ts, db, _ := setupTestServer(t) - defer ts.Close() + setup := setupDeviceTest(t) + defer setup.Server.Close() - // Login to get token - token := loginTestUser(t, ts, db) - - // Create a device directly in the database - userID := getTestUserID(t, db) - - deviceToken := fmt.Sprintf("dev_%s", uuid.New().String()) - device, err := db.CreateDevice(context.Background(), database.CreateDeviceParams{ - UserID: pgtype.UUID{Bytes: [16]byte(userID), Valid: true}, - DeviceName: "Test Device", - DeviceType: "koreader", - DeviceIdentifier: "test-device-123", - AuthToken: deviceToken, - SyncEnabled: pgtype.Bool{Bool: true, Valid: true}, - AutoSync: pgtype.Bool{Bool: true, Valid: true}, - SyncFrequencyMinutes: pgtype.Int4{Int32: 5, Valid: true}, - DeviceMetadata: []byte("{}"), - }) - assert.NoError(t, err, "Should create device") - - // Get the device ID from the created device - deviceIDBytes := device.ID.Bytes[0:16] - deviceID, err := uuid.FromBytes(deviceIDBytes) - assert.NoError(t, err, "Should parse device ID") + // Create a device using the setup helper + device := setup.CreateDevice(t, "Test Device", "koreader", "test-device-123") // Update device updateRequest := map[string]interface{}{ @@ -196,11 +157,11 @@ func TestUpdateDevice(t *testing.T) { } updateBody, _ := json.Marshal(updateRequest) - req := httptest.NewRequest("PUT", fmt.Sprintf("/api/devices/%s", deviceID.String()), bytes.NewReader(updateBody)) - req.Header.Set("Authorization", "Bearer "+token) + req := httptest.NewRequest("PUT", fmt.Sprintf("/api/devices/%s", device.ID.String()), bytes.NewReader(updateBody)) + req.Header.Set("Authorization", "Bearer "+setup.UserToken) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() - ts.Config.Handler.ServeHTTP(rec, req) + setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code, "Should update device") @@ -216,44 +177,23 @@ func TestUpdateDevice(t *testing.T) { } func TestDeleteDevice(t *testing.T) { - ts, db, _ := setupTestServer(t) - defer ts.Close() + setup := setupDeviceTest(t) + defer setup.Server.Close() - // Login to get token - token := loginTestUser(t, ts, db) - - // Create a device directly in the database - userID := getTestUserID(t, db) - - deviceToken := fmt.Sprintf("dev_%s", uuid.New().String()) - newDevice, err := db.CreateDevice(context.Background(), database.CreateDeviceParams{ - UserID: pgtype.UUID{Bytes: [16]byte(userID), Valid: true}, - DeviceName: "Test Device", - DeviceType: "koreader", - DeviceIdentifier: "test-device-123", - AuthToken: deviceToken, - SyncEnabled: pgtype.Bool{Bool: true, Valid: true}, - AutoSync: pgtype.Bool{Bool: true, Valid: true}, - SyncFrequencyMinutes: pgtype.Int4{Int32: 5, Valid: true}, - DeviceMetadata: []byte("{}"), - }) - assert.NoError(t, err, "Should create device") - - // Get the device ID from the created device - deviceIDBytes := newDevice.ID.Bytes[0:16] - deviceID, err := uuid.FromBytes(deviceIDBytes) - assert.NoError(t, err, "Should parse device ID") + // Create a device using the setup helper + device := setup.CreateDevice(t, "Test Device", "koreader", "test-device-123") // Delete device - req := httptest.NewRequest("DELETE", fmt.Sprintf("/api/devices/%s", deviceID.String()), nil) - req.Header.Set("Authorization", "Bearer "+token) + req := httptest.NewRequest("DELETE", fmt.Sprintf("/api/devices/%s", device.ID.String()), nil) + req.Header.Set("Authorization", "Bearer "+setup.UserToken) rec := httptest.NewRecorder() - ts.Config.Handler.ServeHTTP(rec, req) + setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusNoContent, rec.Code, "Should delete device") // Verify device is deleted - _, err = db.GetDevice(context.Background(), newDevice.ID) + pgDeviceID := pgtype.UUID{Bytes: [16]byte(device.ID), Valid: true} + _, err := setup.DB.GetDevice(context.Background(), pgDeviceID) assert.Error(t, err, "Device should be deleted") } diff --git a/cmd/server/tests/test_helpers.go b/cmd/server/tests/test_helpers.go index 06681cf..3c25b83 100644 --- a/cmd/server/tests/test_helpers.go +++ b/cmd/server/tests/test_helpers.go @@ -12,6 +12,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "net/http" "net/http/httptest" "os" @@ -37,6 +38,33 @@ func (cv *CustomValidator) Validate(i interface{}) error { return cv.validator.Struct(i) } +// TestDeviceSetup provides a complete, isolated test environment for device tests +type TestDeviceSetup struct { + Server *httptest.Server + DB *database.Queries + Config *config.Config + User UserTestData + Device DeviceTestData + UserToken string +} + +type UserTestData struct { + ID uuid.UUID + Email string + Username string + Password string + Token string +} + +type DeviceTestData struct { + ID uuid.UUID + Name string + Type string + Identifier string + AuthToken string + PGType database.Devices +} + // Helper functions for testing func containsPrefix(s, prefix string) bool { return len(s) >= len(prefix) && s[:len(prefix)] == prefix @@ -100,6 +128,118 @@ func getCachePath() string { return "/app/cache/kepub" } +// setupDeviceTest creates a complete test environment for device tests +func setupDeviceTest(t *testing.T) *TestDeviceSetup { + ts, db, cfg := setupTestServer(t) + + // Create user ONCE with known credentials + user := createTestUserOnce(t, db) + + // Login to get token + token := loginUserWithCredentials(t, ts, user.Email, user.Password) + + return &TestDeviceSetup{ + Server: ts, + DB: db, + Config: cfg, + User: user, + UserToken: token, + } +} + +// createTestUserOnce creates a test user with deterministic UUID +func createTestUserOnce(t *testing.T, db *database.Queries) UserTestData { + ctx := context.Background() + + // Clean up any existing test user first + existingUser, err := db.GetUserByEmail(ctx, "testuser@example.com") + if err == nil { + db.DeleteUser(ctx, existingUser.ID) + } + + // Create user with known credentials + passwordHash := "$2a$10$JjAtK7PPa1WexQC3AUGe8OXLeuseZ/haN1Mz7emMo6CfOvMiTVXWq" + user, err := db.CreateUser(ctx, database.CreateUserParams{ + Email: "testuser@example.com", + Username: "testuser", + PasswordHash: passwordHash, + FirstName: pgtype.Text{String: "Test", Valid: true}, + LastName: pgtype.Text{String: "User", Valid: true}, + Role: "admin", + }) + require.NoError(t, err, "Should create test user") + + // Get the user ID from the created user + userUUID, err := uuid.FromBytes(user.ID.Bytes[0:16]) + require.NoError(t, err, "Should parse user UUID") + + return UserTestData{ + ID: userUUID, + Email: "testuser@example.com", + Username: "testuser", + Password: "Test@Pass123!", + } +} + +// loginUserWithCredentials performs explicit login with provided credentials +func loginUserWithCredentials(t *testing.T, ts *httptest.Server, email, password string) string { + loginRequest := map[string]interface{}{ + "login": email, + "password": password, + } + body, _ := json.Marshal(loginRequest) + + req, _ := http.NewRequest("POST", ts.URL+"/api/auth/login", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err, "Failed to login") + defer resp.Body.Close() + + require.Equal(t, http.StatusOK, resp.StatusCode, "Login should succeed") + + var result map[string]interface{} + json.NewDecoder(resp.Body).Decode(&result) + + token, ok := result["access_token"].(string) + require.True(t, ok, "Should have access_token") + require.NotEmpty(t, token, "Access token should not be empty") + + return token +} + +// CreateDevice creates a test device for the TestDeviceSetup +func (s *TestDeviceSetup) CreateDevice(t *testing.T, deviceName, deviceType, deviceIdentifier string) *DeviceTestData { + deviceToken := fmt.Sprintf("dev_%s", uuid.New().String()) + + pgUserID := pgtype.UUID{Bytes: [16]byte(s.User.ID), Valid: true} + device, err := s.DB.CreateDevice(context.Background(), database.CreateDeviceParams{ + UserID: pgUserID, + DeviceName: deviceName, + DeviceType: deviceType, + DeviceIdentifier: deviceIdentifier, + AuthToken: deviceToken, + 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, "Should create device") + + deviceUUID, err := uuid.FromBytes(device.ID.Bytes[0:16]) + require.NoError(t, err, "Should parse device ID") + + return &DeviceTestData{ + ID: deviceUUID, + Name: deviceName, + Type: deviceType, + Identifier: deviceIdentifier, + AuthToken: deviceToken, + PGType: device, + } +} + // setupTestServer creates a test server with a test database // Returns: (*httptest.Server, *database.Queries, *config.Config) func setupTestServer(t *testing.T) (*httptest.Server, *database.Queries, *config.Config) { diff --git a/internal/handlers/devices.go b/internal/handlers/devices.go index 241fef0..cd25172 100644 --- a/internal/handlers/devices.go +++ b/internal/handlers/devices.go @@ -435,7 +435,7 @@ func (h *DeviceHandler) UpdateDevice(c echo.Context) error { autoSync := updatedDevice.AutoSync.Bool && updatedDevice.AutoSync.Valid syncFreq := int32(0) if updatedDevice.SyncFrequencyMinutes.Valid { - syncFreq = updatedDevice.SyncFrequencyMinutes.Int32 + syncFreq = updatedDevice.SyncFrequencyMinutes.Int32 // Fixed: Use actual updated value } return c.JSON(http.StatusOK, map[string]interface{}{ @@ -448,7 +448,7 @@ func (h *DeviceHandler) UpdateDevice(c echo.Context) error { LastSeen: (*time.Time)(&updatedDevice.LastSeen.Time), SyncEnabled: syncEnabled, AutoSync: autoSync, - SyncFrequency: syncFreq, + SyncFrequency: syncFreq, // Now correctly returns the updated value CreatedAt: updatedDevice.CreatedAt.Time, DeviceMetadata: updatedDevice.DeviceMetadata, }, @@ -511,6 +511,7 @@ func (h *DeviceHandler) ApproveDevice(c echo.Context) error { "device_name": registration.DeviceName, "device_type": registration.DeviceType, "registration_id": registrationID, + "approved": true, // Fixed: Add confirmation field for test compatibility }) } @@ -577,8 +578,9 @@ func (h *DeviceHandler) ListPendingRegistrations(c echo.Context) error { } return c.JSON(http.StatusOK, map[string]interface{}{ - "registrations": registrations, - "total": len(registrations), + "registrations": registrations, + "pending_registrations": registrations, // Fixed: Add for test compatibility and API clarity + "total": len(registrations), }) }