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 := setup.Token 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 := setup.Token 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 := setup.Token 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 := setup.Token // Get the current test user's ID from database user, err := setup.DB.GetUserByEmail(context.Background(), "testuser@tests.bookhoard.internal") 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 := setup.Token // Get the current test user's ID from database user, err := setup.DB.GetUserByEmail(context.Background(), "testuser@tests.bookhoard.internal") 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 := setup.Token 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") }) } }