package main import ( "bookhoard/internal/database" "bytes" "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // createTestQueueItem creates a complete test fixture: device, library, media item, and sync queue item func createTestQueueItem(t *testing.T, db *database.Queries, userID uuid.UUID) (database.SyncQueue, database.Devices) { ctx := context.Background() // 1. Create device deviceToken := fmt.Sprintf("dev_%s", uuid.New().String()) device, err := db.CreateDevice(ctx, database.CreateDeviceParams{ UserID: pgtype.UUID{Bytes: [16]byte(userID), Valid: true}, DeviceName: "Test Device", DeviceType: "koreader", DeviceIdentifier: "test-device-" + uuid.New().String(), AuthToken: deviceToken, SyncEnabled: pgtype.Bool{Bool: true, Valid: true}, AutoSync: pgtype.Bool{Bool: true, Valid: true}, DeviceMetadata: []byte("{}"), }) require.NoError(t, err, "Should create device") // 2. Get library type libraryType, err := db.GetLibraryTypeByName(ctx, "ebooks") require.NoError(t, err, "Should find library type") // 3. Create library library, err := db.CreateLibrary(ctx, database.CreateLibraryParams{ Name: "Test Queue Library", Description: pgtype.Text{String: "Test library", Valid: true}, LibraryTypeID: libraryType.ID, CreatedByAdminID: pgtype.UUID{Bytes: [16]byte(userID), Valid: true}, }) require.NoError(t, err, "Should create library") // 4. Create media item mediaItem, err := db.CreateMediaItem(ctx, database.CreateMediaItemParams{ LibraryID: library.ID, Title: "Test Book for Queue", Author: pgtype.Text{String: "Test Author", Valid: true}, FilePath: "/tmp/test.epub", FileSize: pgtype.Int8{Int64: 1024, Valid: true}, MimeType: pgtype.Text{String: "application/epub+zip", Valid: true}, }) require.NoError(t, err, "Should create media item") // 5. Create sync queue item queueItem, err := db.CreateSyncQueueItem(ctx, database.CreateSyncQueueItemParams{ DeviceID: device.ID, MediaItemID: mediaItem.ID, SyncType: "progress", SyncData: []byte(`{"percentage": 50}`), Priority: pgtype.Int4{Int32: 5, Valid: true}, }) require.NoError(t, err, "Should create sync queue item") return queueItem, device } func TestListAllQueueItems_Admin(t *testing.T) { setup := setupTestServer(t) token := loginAdminUser(t, setup.Server, setup.DB) req := httptest.NewRequest("GET", "/api/queue/items", nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code, "Should list all queue items") var response map[string]interface{} json.Unmarshal(rec.Body.Bytes(), &response) assert.NotNil(t, response["items"], "Should have queue items") } func TestGetDeviceQueueStats(t *testing.T) { setup := setupTestServer(t) token := loginTestUser(t, setup.Server, setup.DB) userID := getTestUserID(t, setup.DB) deviceToken := fmt.Sprintf("dev_%s", uuid.New().String()) device, err := setup.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}, DeviceMetadata: []byte("{}"), }) require.NoError(t, err, "Should create device") deviceIDBytes := device.ID.Bytes[0:16] deviceID, err := uuid.FromBytes(deviceIDBytes) require.NoError(t, err, "Should parse device ID") req := httptest.NewRequest("GET", fmt.Sprintf("/api/queue/devices/%s/stats", deviceID.String()), nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code, "Should get device queue stats") var response map[string]interface{} json.Unmarshal(rec.Body.Bytes(), &response) assert.Contains(t, response, "total_count", "Should have total_count") assert.Contains(t, response, "pending_count", "Should have pending_count") assert.Contains(t, response, "processing_count", "Should have processing_count") assert.Contains(t, response, "completed_count", "Should have completed_count") assert.Contains(t, response, "failed_count", "Should have failed_count") } func TestListDeviceQueueItems(t *testing.T) { setup := setupTestServer(t) token := loginTestUser(t, setup.Server, setup.DB) userID := getTestUserID(t, setup.DB) deviceToken := fmt.Sprintf("dev_%s", uuid.New().String()) device, err := setup.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}, DeviceMetadata: []byte("{}"), }) require.NoError(t, err, "Should create device") deviceIDBytes := device.ID.Bytes[0:16] deviceID, err := uuid.FromBytes(deviceIDBytes) require.NoError(t, err, "Should parse device ID") req := httptest.NewRequest("GET", fmt.Sprintf("/api/queue/devices/%s/items", deviceID.String()), nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code, "Should list device queue items") var response map[string]interface{} json.Unmarshal(rec.Body.Bytes(), &response) items, ok := response["items"].([]interface{}) assert.True(t, ok, "Should have items array") assert.NotNil(t, items, "Should have queue items") } func TestRetryQueueItem(t *testing.T) { setup := setupTestServer(t) token := loginTestUser(t, setup.Server, setup.DB) // Get the current test user's ID from database user, err := setup.DB.GetUserByEmail(context.Background(), "testuser@example.com") require.NoError(t, err, "Should get test user") userID, err := uuid.FromBytes(user.ID.Bytes[:]) require.NoError(t, err, "Should parse user UUID") queueItem, _ := createTestQueueItem(t, setup.DB, userID) itemID := uuid.UUID(queueItem.ID.Bytes).String() req := httptest.NewRequest("POST", fmt.Sprintf("/api/queue/items/%s/retry", itemID), nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code, "Should retry queue item") } func TestDeleteQueueItem(t *testing.T) { setup := setupTestServer(t) token := loginTestUser(t, setup.Server, setup.DB) // Get the current test user's ID from database user, err := setup.DB.GetUserByEmail(context.Background(), "testuser@example.com") require.NoError(t, err, "Should get test user") userID, err := uuid.FromBytes(user.ID.Bytes[:]) require.NoError(t, err, "Should parse user UUID") queueItem, _ := createTestQueueItem(t, setup.DB, userID) itemID := uuid.UUID(queueItem.ID.Bytes).String() req := httptest.NewRequest("DELETE", fmt.Sprintf("/api/queue/items/%s", itemID), nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusNoContent, rec.Code, "Should delete queue item") } func TestClearDeviceQueue(t *testing.T) { setup := setupTestServer(t) token := loginTestUser(t, setup.Server, setup.DB) userID := getTestUserID(t, setup.DB) deviceToken := fmt.Sprintf("dev_%s", uuid.New().String()) device, err := setup.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}, DeviceMetadata: []byte("{}"), }) require.NoError(t, err, "Should create device") deviceIDBytes := device.ID.Bytes[0:16] deviceID, err := uuid.FromBytes(deviceIDBytes) require.NoError(t, err, "Should parse device ID") req := httptest.NewRequest("DELETE", fmt.Sprintf("/api/queue/devices/%s/clear", deviceID.String()), nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code, "Should clear device queue") } func TestQueueEndpoints_Unauthorized(t *testing.T) { setup := setupTestServer(t) tests := []struct { name string method string endpoint string body []byte expectedStatus int }{ {"ListAllQueueItems", "GET", "/api/queue/items", nil, http.StatusUnauthorized}, {"GetDeviceQueueStats", "GET", "/api/queue/devices/test-device-id/stats", nil, http.StatusUnauthorized}, {"ListDeviceQueueItems", "GET", "/api/queue/devices/test-device-id/items", nil, http.StatusUnauthorized}, {"RetryQueueItem", "POST", "/api/queue/items/test-item-id/retry", nil, http.StatusUnauthorized}, {"DeleteQueueItem", "DELETE", "/api/queue/items/test-item-id", nil, http.StatusUnauthorized}, {"ClearDeviceQueue", "DELETE", "/api/queue/devices/test-device-id/clear", nil, http.StatusUnauthorized}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest(tt.method, tt.endpoint, bytes.NewReader(tt.body)) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, tt.expectedStatus, rec.Code, "Should require authentication") }) } } func loginAdminUser(t *testing.T, ts *httptest.Server, db *database.Queries) string { ctx := context.Background() // Check if admin user exists and delete them first to ensure fresh state // (using database delete directly to bypass "last admin" check) user, err := db.GetUserByEmail(ctx, "admin@example.com") if err == nil { err = db.DeleteUser(ctx, user.ID) if err != nil { t.Logf("Warning: Could not delete existing admin user: %v", err) } } // Create a fresh admin user with known password // Password: "Test@Pass123!" meets complexity requirements // This is the bcrypt hash for "Test@Pass123!" passwordHash := "$2a$10$JjAtK7PPa1WexQC3AUGe8OXLeuseZ/haN1Mz7emMo6CfOvMiTVXWq" adminUser, err := db.CreateUser(ctx, database.CreateUserParams{ Email: "admin@example.com", Username: "admin", PasswordHash: passwordHash, FirstName: pgtype.Text{String: "Admin", Valid: true}, LastName: pgtype.Text{String: "User", Valid: true}, Role: "admin", Theme: pgtype.Text{String: "tokyo-night", Valid: true}, }) require.NoError(t, err, "Failed to create admin user") userUUID, err := uuid.FromBytes(adminUser.ID.Bytes[:]) require.NoError(t, err, "Should parse admin user UUID") return loginUserWithID(t, ts, db, userUUID, "admin@example.com", "Test@Pass123!") } func loginUserWithID(t *testing.T, ts *httptest.Server, db *database.Queries, userID uuid.UUID, 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 }