package sync import ( "bookhoard/internal/database" "context" "encoding/json" "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" ) func TestSyncQueueProcessor_EnqueueProgress(t *testing.T) { ctx := context.Background() db := setupTestDB(t) defer teardownTestDB(t, db) processor := 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 := &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, SyncStatusPending, item.Status.String) assert.Equal(t, int32(PriorityPageTurn), item.Priority.Int32) var syncData map[string]interface{} err = json.Unmarshal(item.SyncData, &syncData) require.NoError(t, err) assert.Equal(t, percentage, syncData["percentage"]) assert.Equal(t, "koreader", syncData["source"]) } func TestCalculateNextRetry(t *testing.T) { processor := &SyncQueueProcessor{} tests := []struct { name string attempts int32 minDelay time.Duration maxDelay time.Duration }{ {"Attempt 0", 0, 0, 1 * time.Second}, {"Attempt 1", 1, 59 * time.Second, 61 * time.Second}, {"Attempt 2", 2, 4*time.Minute + 59*time.Second, 5*time.Minute + 1*time.Second}, {"Attempt 3", 3, 14*time.Minute + 59*time.Second, 15*time.Minute + 1*time.Second}, {"Attempt 4", 4, 59*time.Minute + 59*time.Second, 60*time.Minute + 1*time.Second}, {"Attempt 5", 5, 23*time.Hour + 59*time.Minute, 24*time.Hour + 1*time.Minute}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { nextRetry := processor.calculateNextRetry(tt.attempts) delay := nextRetry.Sub(time.Now()) assert.GreaterOrEqual(t, delay, tt.minDelay, "delay should be at least minDelay") assert.LessOrEqual(t, delay, tt.maxDelay, "delay should be at most maxDelay") }) } } func TestSyncTypeConstants(t *testing.T) { assert.Equal(t, "progress", SyncTypeProgress) assert.Equal(t, "note", SyncTypeNote) assert.Equal(t, "highlight", SyncTypeHighlight) assert.Equal(t, "bookmark", SyncTypeBookmark) } func TestSyncStatusConstants(t *testing.T) { assert.Equal(t, "pending", SyncStatusPending) assert.Equal(t, "processing", SyncStatusProcessing) assert.Equal(t, "completed", SyncStatusCompleted) assert.Equal(t, "failed", SyncStatusFailed) } func TestPriorityConstants(t *testing.T) { assert.Equal(t, int32(1), PriorityUserInitiated) assert.Equal(t, int32(2), PriorityBookCompletion) assert.Equal(t, int32(3), PriorityCriticalNote) assert.Equal(t, int32(5), PriorityPageTurn) assert.Equal(t, int32(7), PriorityCheckpoint) assert.Equal(t, int32(10), PriorityBackgroundSync) } func setupTestDB(t *testing.T) *database.Queries { ctx := context.Background() dbURL := "postgresql://postgres:postgres@localhost:5432/bookmann?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 teardownTestDB(t *testing.T, db *database.Queries) { // Cleanup is handled in setupTestDB via t.Cleanup }