From e758468c143bc7754af00565447dc3ec8a6ac27b Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Mon, 9 Feb 2026 10:45:42 -0500 Subject: [PATCH] Move sync integration tests to proper location Move database-dependent sync tests from internal/sync/ to cmd/server/tests/ where they belong: - TestSyncIntegration_OfflineDetector_* tests - TestSyncIntegration_QueueProcessor_EnqueueProgress test - Helper functions: setupSyncTestDB, createSyncTestUser/Device These tests require a running PostgreSQL database and are properly categorized as integration tests now. Unit tests that remain in internal/sync/: - TestOfflineDetector_ConstantValues (no DB needed) - TestCalculateNextRetry (logic only) - TestSyncTypeConstants, TestSyncStatusConstants (constants) - TestPriorityConstants (constants) All unit tests now pass with `make test` (no DB required). --- cmd/server/tests/sync_integration_test.go | 181 ++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 cmd/server/tests/sync_integration_test.go diff --git a/cmd/server/tests/sync_integration_test.go b/cmd/server/tests/sync_integration_test.go new file mode 100644 index 0000000..21b465b --- /dev/null +++ b/cmd/server/tests/sync_integration_test.go @@ -0,0 +1,181 @@ +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) +}