package store_test import ( "context" "testing" "time" "github.com/jackc/pgx/v5/pgxpool" "github.com/stretchr/testify/require" "git.d-ma.be/mathias/tapir/internal/adapters/store" ) // seedBareVideo inserts a videos row with no summary, so the all-videos read and // the manual-queue flag can be exercised without a delivered summary. func seedBareVideo(t *testing.T, p *pgxpool.Pool, userID, videoID, title string) { t.Helper() _, err := p.Exec(context.Background(), `INSERT INTO users (id) VALUES ($1) ON CONFLICT (id) DO NOTHING`, userID) require.NoError(t, err) _, err = p.Exec(context.Background(), `INSERT INTO videos (id, user_id, provider, provider_video_id, title) VALUES ($1, $2, 'youtube', $3, $4)`, videoID, userID, "pv-"+videoID[:8], title) require.NoError(t, err) } func TestAutoSummarizeRoundTripDefaultsFalse(t *testing.T) { ctx := context.Background() s := newStore(t) resetDB(t, rawPool(t)) // Unknown / fresh user defaults to manual (false). got, err := s.GetAutoSummarize(ctx, userA) require.NoError(t, err) require.False(t, got, "default mode is manual") require.NoError(t, s.SetAutoSummarize(ctx, userA, true)) got, err = s.GetAutoSummarize(ctx, userA) require.NoError(t, err) require.True(t, got, "set to automatic round-trips") require.NoError(t, s.SetAutoSummarize(ctx, userA, false)) got, err = s.GetAutoSummarize(ctx, userA) require.NoError(t, err) require.False(t, got, "set back to manual round-trips") } func TestRegisteredUserDefaultsAutoSummarizeOn(t *testing.T) { ctx := context.Background() s := newStore(t) resetDB(t, rawPool(t)) // A real registration creates the users row, so the column default (migration // 011: TRUE) drives the mode — onboarded friends get auto out of the box. id, err := s.RegisterUser(ctx, subjectA, "Alice") require.NoError(t, err) got, err := s.GetAutoSummarize(ctx, id) require.NoError(t, err) require.True(t, got, "new registrations default to auto-summarize (ADR-018)") // The account-page toggle still works: a user can switch to manual. require.NoError(t, s.SetAutoSummarize(ctx, id, false)) got, err = s.GetAutoSummarize(ctx, id) require.NoError(t, err) require.False(t, got, "the manual toggle still flips it off") } func TestRequestSummarizeSetsFlag(t *testing.T) { ctx := context.Background() s := newStore(t) p := rawPool(t) resetDB(t, p) seedBareVideo(t, p, userA, videoX, "X Title") require.NoError(t, s.RequestSummarize(ctx, userA, videoX)) requested, err := s.RequestedVideoIDs(ctx, userA) require.NoError(t, err) require.Equal(t, map[string]bool{videoX: true}, requested) // Clearing drops it from the requested set. require.NoError(t, s.ClearSummarizeRequested(ctx, userA, videoX)) requested, err = s.RequestedVideoIDs(ctx, userA) require.NoError(t, err) require.Empty(t, requested) } func TestRequestSummarizeMissingVideo(t *testing.T) { ctx := context.Background() s := newStore(t) resetDB(t, rawPool(t)) err := s.RequestSummarize(ctx, userA, videoX) require.ErrorIs(t, err, store.ErrNotFound, "queuing a non-existent video reports not found") } func TestListVideosReturnsSummarizedAndUnsummarized(t *testing.T) { ctx := context.Background() s := newStore(t) p := rawPool(t) resetDB(t, p) // videoX: discovered AND summarized. videoY: discovered, not yet summarized. seedBareVideo(t, p, userA, videoX, "Summarized One") seedBareVideo(t, p, userA, videoY, "Pending One") require.NoError(t, s.Deliver(ctx, summary(userA, videoX, "body x"))) require.NoError(t, s.RequestSummarize(ctx, userA, videoY)) rows, err := s.ListVideos(ctx, userA, 50) require.NoError(t, err) require.Len(t, rows, 2, "both summarized and unsummarized videos are listed") byID := map[string]store.SummaryRow{} for _, r := range rows { byID[r.VideoID] = r } require.True(t, byID[videoX].Summarized) require.Equal(t, "body x", byID[videoX].Summary) require.False(t, byID[videoX].SummarizeRequested) require.False(t, byID[videoY].Summarized, "no summary -> Summarized false") require.Empty(t, byID[videoY].Summary, "unsummarized row has empty summary") require.True(t, byID[videoY].SummarizeRequested, "queued video carries the flag") } // TestListVideosOrderedByPublishedDescNullsLast: summarized videos sort first // (regardless of their date), then unsummarized by published_at DESC with // undated (NULL) videos last — the recency-aligned list order (UX review B2). func TestListVideosOrderedByPublishedDescNullsLast(t *testing.T) { ctx := context.Background() s := newStore(t) p := rawPool(t) resetDB(t, p) const ( vSummOld = "cccccccc-cccc-cccc-cccc-cccccccccccc" // summarized, oldest date vNewer = "dddddddd-dddd-dddd-dddd-dddddddddddd" // unsummarized, newest vOlder = "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee" // unsummarized, older vUndated = "ffffffff-ffff-ffff-ffff-ffffffffffff" // unsummarized, no date ) // Deliver first so the userA row exists (the videos FK needs it); the // summarized video carries the OLDEST date yet must still sort first because // it is summarized, proving summarized-first dominates the date sort. require.NoError(t, s.Deliver(ctx, summary(userA, vSummOld, "body"))) seedVideo(t, p, userA, vSummOld, "Summarized Old", "youtube", "https://s", time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)) seedVideo(t, p, userA, vNewer, "Newer", "youtube", "https://n", time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)) seedVideo(t, p, userA, vOlder, "Older", "youtube", "https://o", time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)) seedVideo(t, p, userA, vUndated, "Undated", "youtube", "https://u", time.Time{}) rows, err := s.ListVideos(ctx, userA, 50) require.NoError(t, err) require.Len(t, rows, 4) got := []string{rows[0].VideoID, rows[1].VideoID, rows[2].VideoID, rows[3].VideoID} require.Equal(t, []string{vSummOld, vNewer, vOlder, vUndated}, got, "summarized first, then published_at DESC, NULL dates last") } func TestListVideosIsUserScoped(t *testing.T) { ctx := context.Background() s := newStore(t) p := rawPool(t) resetDB(t, p) seedBareVideo(t, p, userA, videoX, "A only") rows, err := s.ListVideos(ctx, userB, 50) require.NoError(t, err) require.Empty(t, rows, "user B must not see user A's videos") } func TestGetVideoRowNotFound(t *testing.T) { ctx := context.Background() s := newStore(t) resetDB(t, rawPool(t)) _, err := s.GetVideoRow(ctx, userA, videoX) require.ErrorIs(t, err, store.ErrNotFound) }