package main import ( "bookhoard/internal/database" "bookhoard/internal/sync" "context" "testing" "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:postgres@db:5432/bookhoard?sslmode=disable" dbPool, err := pgxpool.New(ctx, dbURL) 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_progress 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$rKvZ.HZx3lLJ6IQCpH1lOukQ/xU8j5cH8mYhPY5YGfXllq5hG8y0Ou" _, 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: userID, Valid: true} } func createSyncTestDevice(t *testing.T, db *database.Queries, userID pgtype.UUID) pgtype.UUID { ctx := context.Background() deviceID := uuid.New() authToken := "test-sync-token-" + deviceID.String() _, err := db.CreateDevice(ctx, database.CreateDeviceParams{ UserID: userID, DeviceName: "Test Sync Device", DeviceType: "koreader", DeviceIdentifier: deviceID.String(), AuthToken: authToken, SyncEnabled: pgtype.Bool{Bool: true, Valid: true}, }) require.NoError(t, err) return pgtype.UUID{Bytes: deviceID, Valid: true} } func TestSyncIntegration_OfflineDetector_DeviceStatusDetection(t *testing.T) { ctx := context.Background() db := setupSyncTestDB(t) userID := createSyncTestUser(t, db) deviceID := createSyncTestDevice(t, db, userID) detector := sync.NewOfflineDetector(db, nil) status, err := detector.GetDeviceStatus(ctx, deviceID) require.NoError(t, err) assert.True(t, status.IsOnline, "device should be online initially") assert.Equal(t, "Test Sync Device", status.DeviceName) assert.Equal(t, "koreader", status.DeviceType) } func TestSyncIntegration_OfflineDetector_OfflineThreshold(t *testing.T) { ctx := context.Background() db := setupSyncTestDB(t) userID := createSyncTestUser(t, db) deviceID := createSyncTestDevice(t, db, userID) _, err := db.UpdateDeviceLastSeen(ctx, deviceID) require.NoError(t, err) detector := sync.NewOfflineDetector(db, nil) // Get device status status, err := detector.GetDeviceStatus(ctx, deviceID) require.NoError(t, err) assert.True(t, status.IsOnline, "device should be online initially") } func TestSyncIntegration_OfflineDetector_GetDeviceStatus(t *testing.T) { ctx := context.Background() db := setupSyncTestDB(t) userID := createSyncTestUser(t, db) deviceID := createSyncTestDevice(t, db, userID) detector := sync.NewOfflineDetector(db, nil) status, err := detector.GetDeviceStatus(ctx, deviceID) require.NoError(t, err) assert.NotNil(t, status) assert.Equal(t, "Test Sync Device", status.DeviceName) assert.Equal(t, "koreader", status.DeviceType) } func TestSyncIntegration_OfflineDetector_ForceReconnectDevice(t *testing.T) { ctx := context.Background() db := setupSyncTestDB(t) userID := createSyncTestUser(t, db) deviceID := createSyncTestDevice(t, db, userID) _, err := db.UpdateDeviceLastSeen(ctx, deviceID) require.NoError(t, err) detector := sync.NewOfflineDetector(db, nil) err = detector.ForceReconnectDevice(ctx, deviceID) require.NoError(t, err) device, err := db.GetDevice(ctx, deviceID) require.NoError(t, err) assert.True(t, device.SyncEnabled.Bool, "device should be re-enabled after force reconnect") } func TestSyncIntegration_QueueProcessor_EnqueueProgress(t *testing.T) { ctx := context.Background() db := setupSyncTestDB(t) processor := sync.NewSyncQueueProcessor(db) userID := pgtype.UUID{Bytes: uuid.New(), Valid: true} deviceID := pgtype.UUID{Bytes: uuid.New(), Valid: true} mediaItemID := pgtype.UUID{Bytes: uuid.New(), Valid: true} percentage := 0.45 chapter := 3 update := &sync.ProgressUpdate{ DeviceID: deviceID, MediaItemID: mediaItemID, UserID: userID, Percentage: percentage, Chapter: &chapter, Source: "koreader", SyncMode: "immediate", } err := processor.EnqueueProgress(update) require.NoError(t, err, "should enqueue progress update") items, err := db.ListPendingSyncQueueItems(ctx, database.ListPendingSyncQueueItemsParams{ DeviceID: deviceID, Limit: 10, }) require.NoError(t, err) assert.Len(t, items, 1, "should have one queue item") item := items[0] assert.Equal(t, "progress", item.SyncType) assert.Equal(t, sync.SyncStatusPending, item.Status.String) assert.Equal(t, int32(sync.PriorityPageTurn), item.Priority.Int32) }