package store import ( "context" "os" "testing" "time" authStore "git.nakama.town/fmartingr/hako/internal/auth/store" "git.nakama.town/fmartingr/hako/internal/database" "git.nakama.town/fmartingr/hako/internal/model" "github.com/google/uuid" "github.com/stretchr/testify/require" ) func setupTestArchiveStore(t *testing.T) (*ArchiveStore, string, string, func()) { t.Helper() // Create a temporary database file tmpDB, err := os.CreateTemp("", "test_archive_store_*.db") require.NoError(t, err) _ = tmpDB.Close() dbURL := "sqlite:" + tmpDB.Name() // Create database connections conns, err := database.NewConnections(dbURL, dbURL) require.NoError(t, err) // Run migrations err = database.InitSchema(conns) require.NoError(t, err) // Create stores userStore := authStore.NewUserStore(conns.Read, conns.Write) linkStore := NewLinkStore(conns.Read, conns.Write) archiveStore := NewArchiveStore(conns.Read, conns.Write) // Create a test user ctx := context.Background() userUUID := uuid.New() user := &authStore.User{ ID: userUUID, Email: "test@example.com", PasswordHash: "test-hash", CreatedAt: time.Now(), UpdatedAt: time.Now(), } err = userStore.Create(user) require.NoError(t, err) // Create a test link userID := userUUID.String() linkID := uuid.New().String() link := &model.Link{ ID: linkID, URL: "https://example.com", UserID: userID, CreatedAt: time.Now(), UpdatedAt: time.Now(), } err = linkStore.Create(ctx, link) require.NoError(t, err) cleanup := func() { _ = conns.Close() _ = os.Remove(tmpDB.Name()) _ = os.Remove(tmpDB.Name() + "-wal") _ = os.Remove(tmpDB.Name() + "-shm") } return archiveStore, userID, linkID, cleanup } func TestArchiveStore_Create(t *testing.T) { store, userID, linkID, cleanup := setupTestArchiveStore(t) defer cleanup() ctx := context.Background() now := time.Now() archive := &model.Archive{ ID: uuid.New().String(), LinkID: linkID, UserID: userID, Status: model.ArchiveStatusPending, ErrorMessage: "", CreatedAt: now, } err := store.Create(ctx, archive) require.NoError(t, err) // Verify it was created retrieved, err := store.GetByID(ctx, archive.ID) require.NoError(t, err) require.Equal(t, archive.ID, retrieved.ID) require.Equal(t, archive.LinkID, retrieved.LinkID) require.Equal(t, archive.UserID, retrieved.UserID) require.Equal(t, archive.Status, retrieved.Status) } func TestArchiveStore_UpdateStatus_ToCompleted(t *testing.T) { store, userID, linkID, cleanup := setupTestArchiveStore(t) defer cleanup() ctx := context.Background() now := time.Now() // Create an archive in pending status archive := &model.Archive{ ID: uuid.New().String(), LinkID: linkID, UserID: userID, Status: model.ArchiveStatusPending, ErrorMessage: "", CreatedAt: now, } err := store.Create(ctx, archive) require.NoError(t, err) // Update to processing err = store.UpdateStatus(ctx, archive.ID, model.ArchiveStatusProcessing, "") require.NoError(t, err) // Verify processing status retrieved, err := store.GetByID(ctx, archive.ID) require.NoError(t, err) require.Equal(t, model.ArchiveStatusProcessing, retrieved.Status) require.Empty(t, retrieved.ErrorMessage) require.Nil(t, retrieved.CompletedAt, "CompletedAt should be nil for processing status") // Update to completed err = store.UpdateStatus(ctx, archive.ID, model.ArchiveStatusCompleted, "") require.NoError(t, err) // Verify completed status - THIS IS THE KEY TEST THAT WOULD HAVE CAUGHT THE BUG retrieved, err = store.GetByID(ctx, archive.ID) require.NoError(t, err) require.Equal(t, model.ArchiveStatusCompleted, retrieved.Status, "Status should be completed") require.Empty(t, retrieved.ErrorMessage, "Error message should be empty") require.NotNil(t, retrieved.CompletedAt, "CompletedAt should be set for completed status") // Verify the completed_at timestamp is recent (within last 2 hours to account for timezone differences) // The key point is that it's set at all, not the exact timestamp require.WithinDuration(t, time.Now().UTC(), retrieved.CompletedAt.UTC(), 2*time.Hour) } func TestArchiveStore_UpdateStatus_ToFailed(t *testing.T) { store, userID, linkID, cleanup := setupTestArchiveStore(t) defer cleanup() ctx := context.Background() now := time.Now() // Create an archive archive := &model.Archive{ ID: uuid.New().String(), LinkID: linkID, UserID: userID, Status: model.ArchiveStatusPending, ErrorMessage: "", CreatedAt: now, } err := store.Create(ctx, archive) require.NoError(t, err) // Update to failed with error message errorMsg := "extractor not found: test_extractor" err = store.UpdateStatus(ctx, archive.ID, model.ArchiveStatusFailed, errorMsg) require.NoError(t, err) // Verify failed status retrieved, err := store.GetByID(ctx, archive.ID) require.NoError(t, err) require.Equal(t, model.ArchiveStatusFailed, retrieved.Status, "Status should be failed") require.Equal(t, errorMsg, retrieved.ErrorMessage, "Error message should be set") require.NotNil(t, retrieved.CompletedAt, "CompletedAt should be set for failed status") } func TestArchiveStore_UpdateStatus_ClearsErrorMessage(t *testing.T) { store, userID, linkID, cleanup := setupTestArchiveStore(t) defer cleanup() ctx := context.Background() now := time.Now() // Create an archive with an error archive := &model.Archive{ ID: uuid.New().String(), LinkID: linkID, UserID: userID, Status: model.ArchiveStatusFailed, ErrorMessage: "previous error", CreatedAt: now, } err := store.Create(ctx, archive) require.NoError(t, err) // Update to completed (should clear error message) err = store.UpdateStatus(ctx, archive.ID, model.ArchiveStatusCompleted, "") require.NoError(t, err) // Verify error message was cleared retrieved, err := store.GetByID(ctx, archive.ID) require.NoError(t, err) require.Equal(t, model.ArchiveStatusCompleted, retrieved.Status) require.Empty(t, retrieved.ErrorMessage, "Error message should be cleared when updating to completed") } func TestArchiveStore_ListByLinkID(t *testing.T) { store, userID, linkID, cleanup := setupTestArchiveStore(t) defer cleanup() ctx := context.Background() // Create multiple archives for the same link archive1 := &model.Archive{ ID: uuid.New().String(), LinkID: linkID, UserID: userID, Status: model.ArchiveStatusCompleted, CreatedAt: time.Now().Add(-2 * time.Hour), } archive2 := &model.Archive{ ID: uuid.New().String(), LinkID: linkID, UserID: userID, Status: model.ArchiveStatusPending, CreatedAt: time.Now().Add(-1 * time.Hour), } archive3 := &model.Archive{ ID: uuid.New().String(), LinkID: linkID, UserID: userID, Status: model.ArchiveStatusProcessing, CreatedAt: time.Now(), } err := store.Create(ctx, archive1) require.NoError(t, err) err = store.Create(ctx, archive2) require.NoError(t, err) err = store.Create(ctx, archive3) require.NoError(t, err) // List archives opts := ArchiveListOptions{LinkID: linkID} archives, err := store.ListByLinkID(ctx, opts) require.NoError(t, err) require.Len(t, archives, 3) // Should be ordered by created_at DESC (newest first) require.Equal(t, archive3.ID, archives[0].ID) require.Equal(t, archive2.ID, archives[1].ID) require.Equal(t, archive1.ID, archives[2].ID) } func TestArchiveStore_GetByID_NotFound(t *testing.T) { store, _, _, cleanup := setupTestArchiveStore(t) defer cleanup() ctx := context.Background() _, err := store.GetByID(ctx, "non-existent-id") require.Error(t, err) require.Contains(t, err.Error(), "archive not found") }