package main import ( "bookhoard/internal/database" "context" "net/http" "net/http/httptest" "testing" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // setupSyncTestDB creates a database connection for sync integration tests func setupSyncTestDB(t *testing.T) *database.Queries { ctx := context.Background() dbURL := "postgresql://postgres@db:5432/bookhoard?sslmode=disable" dbConfig, err := pgxpool.ParseConfig(dbURL) require.NoError(t, err, "Failed to parse database URL") dbConfig.MaxConns = 1 dbPool, err := pgxpool.NewWithConfig(ctx, dbConfig) require.NoError(t, err, "Failed to connect to test database") db := database.New(dbPool) t.Cleanup(func() { _, _ = dbPool.Exec(ctx, "DELETE FROM sync_queue WHERE true") _, _ = dbPool.Exec(ctx, "DELETE FROM reading_history WHERE true") _, _ = dbPool.Exec(ctx, "DELETE FROM media_items WHERE title LIKE 'Test %'") _, _ = dbPool.Exec(ctx, "DELETE FROM libraries WHERE name LIKE 'Test %'") _, _ = dbPool.Exec(ctx, "DELETE FROM devices WHERE device_name LIKE 'Test %'") _, _ = dbPool.Exec(ctx, "DELETE FROM users WHERE email LIKE 'test%'") dbPool.Close() }) return db } func createSyncTestUser(t *testing.T, db *database.Queries) pgtype.UUID { ctx := context.Background() userID := uuid.New() hashedPassword := "$2a$10$rKvZHX3lIJ6CpH1lOukQ/xU8j5cH8mYHYP5YGfXllq5hG8y0Ou" _, err := db.CreateUser(ctx, database.CreateUserParams{ Email: "test-sync@example.com", Username: "testsyncuser", PasswordHash: hashedPassword, FirstName: pgtype.Text{String: "Test", Valid: true}, LastName: pgtype.Text{String: "Sync", Valid: true}, Role: "user", }) require.NoError(t, err) return pgtype.UUID{Bytes: [16]byte(userID), Valid: true} } func createSyncTestDevice(t *testing.T, db *database.Queries, userID pgtype.UUID) pgtype.UUID { ctx := context.Background() deviceID := uuid.New() _, err := db.CreateDevice(ctx, database.CreateDeviceParams{ UserID: userID, DeviceName: "Test Sync Device", DeviceType: "koreader", DeviceIdentifier: deviceID.String(), AuthToken: "test-sync-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) return pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true} } func createSyncTestMedia(t *testing.T, db *database.Queries, libraryID pgtype.UUID) pgtype.UUID { ctx := context.Background() mediaID := uuid.New() _, err := db.CreateMediaItem(ctx, database.CreateMediaItemParams{ LibraryID: libraryID, Title: "Sync Test Book", 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) return pgtype.UUID{Bytes: [16]byte(mediaID), Valid: true} } func createSyncTestLibrary(t *testing.T, db *database.Queries, userID pgtype.UUID) pgtype.UUID { ctx := context.Background() libraryType, err := db.GetLibraryTypeByName(ctx, "ebooks") require.NoError(t, err, "Should find library type") library, err := db.CreateLibrary(ctx, database.CreateLibraryParams{ Name: "Test Sync Library", Description: pgtype.Text{String: "Test library for sync", Valid: true}, LibraryTypeID: libraryType.ID, CreatedByAdminID: userID, }) require.NoError(t, err) return library.ID } // TestSyncFull_Initial tests full sync with initial device state func TestSyncFull_Initial(t *testing.T) { ctx := context.Background() db := setupSyncTestDB(t) userID := createSyncTestUser(t, db) libraryID := createSyncTestLibrary(t, db, userID) deviceID := createSyncTestDevice(t, db, userID) mediaID := createSyncTestMedia(t, db, libraryID) // Create reading history entry now := time.Now() _, err := db.CreateReadingHistory(ctx, database.CreateReadingHistoryParams{ UserID: userID, MediaItemID: mediaID, DeviceID: deviceID, ProgressPercentage: pgtype.Float8{Float64: 25.0, Valid: true}, ReadingSessionStart: pgtype.Timestamptz{Time: now, Valid: true}, PagesRead: pgtype.Int4{Int32: 50, Valid: true}, TimeSpentSeconds: pgtype.Int4{Int32: 300, Valid: true}, }) require.NoError(t, err) } // TestSyncIncremental tests incremental sync func TestSyncIncremental(t *testing.T) { ctx := context.Background() db := setupSyncTestDB(t) userID := createSyncTestUser(t, db) libraryID := createSyncTestLibrary(t, db, userID) deviceID := createSyncTestDevice(t, db, userID) mediaID := createSyncTestMedia(t, db, libraryID) // Create initial reading history entry now := time.Now() _, err := db.CreateReadingHistory(ctx, database.CreateReadingHistoryParams{ UserID: userID, MediaItemID: mediaID, DeviceID: deviceID, ProgressPercentage: pgtype.Float8{Float64: 50.0, Valid: true}, ReadingSessionStart: pgtype.Timestamptz{Time: now, Valid: true}, PagesRead: pgtype.Int4{Int32: 100, Valid: true}, TimeSpentSeconds: pgtype.Int4{Int32: 600, Valid: true}, }) require.NoError(t, err) } // TestSyncQueueProcessor tests the sync queue processor func TestSyncQueueProcessor(t *testing.T) { ctx := context.Background() db := setupSyncTestDB(t) userID := createSyncTestUser(t, db) libraryID := createSyncTestLibrary(t, db, userID) deviceID := createSyncTestDevice(t, db, userID) mediaID := createSyncTestMedia(t, db, libraryID) // Create sync queue item _, err := db.CreateSyncQueueItem(ctx, database.CreateSyncQueueItemParams{ DeviceID: deviceID, MediaItemID: mediaID, SyncType: "progress", SyncData: []byte(`{"percentage": 50}`), Priority: pgtype.Int4{Int32: 5, Valid: true}, }) require.NoError(t, err) // Verify queue item was created items, err := db.ListPendingSyncQueueItems(ctx, database.ListPendingSyncQueueItemsParams{ Limit: int32(10), }) require.NoError(t, err) assert.Equal(t, 1, len(items), "Should have one queue item") assert.Equal(t, "pending", items[0].Status) } // TestSyncConflictDetection tests conflict detection func TestSyncConflictDetection(t *testing.T) { ctx := context.Background() db := setupSyncTestDB(t) userID := createSyncTestUser(t, db) libraryID := createSyncTestLibrary(t, db, userID) deviceID := createSyncTestDevice(t, db, userID) mediaID := createSyncTestMedia(t, db, libraryID) // Create reading history entry _, err := db.CreateReadingHistory(ctx, database.CreateReadingHistoryParams{ UserID: userID, MediaItemID: mediaID, DeviceID: deviceID, ProgressPercentage: pgtype.Float8{Float64: 50.0, Valid: true}, ReadingSessionStart: pgtype.Timestamptz{Time: time.Now().Add(-1 * time.Hour), Valid: true}, PagesRead: pgtype.Int4{Int32: 100, Valid: true}, TimeSpentSeconds: pgtype.Int4{Int32: 600, Valid: true}, }) require.NoError(t, err) // Create sync queue item with different percentage (simulating conflict) _, err = db.CreateSyncQueueItem(ctx, database.CreateSyncQueueItemParams{ DeviceID: deviceID, MediaItemID: mediaID, SyncType: "progress", SyncData: []byte(`{"percentage": 75}`), Priority: pgtype.Int4{Int32: 5, Valid: true}, }) require.NoError(t, err) // Verify both exist (conflict detection would happen during processing) items, err := db.ListPendingSyncQueueItems(ctx, database.ListPendingSyncQueueItemsParams{ Limit: int32(10), }) require.NoError(t, err) assert.Equal(t, 1, len(items), "Should have one queue item") } // TestSyncWithDeviceAuth tests device authentication in sync func TestSyncWithDeviceAuth(t *testing.T) { setup := setupDeviceTest(t) defer setup.Server.Close() // Create a device device := setup.CreateDevice(t, "Test Sync Device", "koreader", "sync-test-123") // Verify device exists in database ctx := context.Background() pgDeviceID := pgtype.UUID{Bytes: [16]byte(device.ID), Valid: true} dbDevice, err := setup.DB.GetDevice(ctx, pgDeviceID) require.NoError(t, err) assert.Equal(t, "Test Sync Device", dbDevice.DeviceName) } // TestSyncEndpoint tests sync endpoint with authentication func TestSyncEndpoint(t *testing.T) { setup := setupDeviceTest(t) defer setup.Server.Close() device := setup.CreateDevice(t, "Test Sync Device", "koreader", "sync-test-123") // Test sync endpoint with device token req := httptest.NewRequest("POST", "/api/koreader/sync", nil) req.Header.Set("Authorization", "Bearer "+device.AuthToken) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) // Should get a response (may be success or error depending on payload) assert.NotEqual(t, http.StatusUnauthorized, rec.Code, "Should not be unauthorized with valid device token") }