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" ) // seedVideo inserts a videos row whose id matches a summary's video_id, so the // read-side LEFT JOIN has metadata to attach. published_at may be the zero time // to exercise the NULL path. func seedVideo(t *testing.T, p *pgxpool.Pool, userID, videoID, title, provider, url string, published time.Time) { t.Helper() var pub any if !published.IsZero() { pub = published } _, err := p.Exec(context.Background(), `INSERT INTO videos (id, user_id, provider, provider_video_id, title, url, published_at) VALUES ($1, $2, $3, $4, $5, $6, $7)`, videoID, userID, provider, "pv-"+videoID[:8], title, url, pub) require.NoError(t, err) } // setCreatedAt forces a summary's created_at so ordering is deterministic. func setCreatedAt(t *testing.T, p *pgxpool.Pool, userID, videoID string, at time.Time) { t.Helper() _, err := p.Exec(context.Background(), `UPDATE summaries SET created_at = $3 WHERE user_id = $1 AND video_id = $2`, userID, videoID, at) require.NoError(t, err) } func TestListSummariesRecentFirstWithVideoMetadata(t *testing.T) { ctx := context.Background() s := newStore(t) p := rawPool(t) resetDB(t, p) require.NoError(t, s.Deliver(ctx, summary(userA, videoX, "older"))) require.NoError(t, s.Deliver(ctx, summary(userA, videoY, "newer"))) seedVideo(t, p, userA, videoX, "X Title", "youtube", "https://x", time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)) seedVideo(t, p, userA, videoY, "Y Title", "youtube", "https://y", time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC)) // Force ordering: videoY is the most recent. setCreatedAt(t, p, userA, videoX, time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)) setCreatedAt(t, p, userA, videoY, time.Date(2026, 3, 2, 0, 0, 0, 0, time.UTC)) rows, err := s.ListSummaries(ctx, userA, 50) require.NoError(t, err) require.Len(t, rows, 2) require.Equal(t, videoY, rows[0].VideoID, "most recent created_at first") require.Equal(t, "Y Title", rows[0].Title) require.Equal(t, "youtube", rows[0].Channel) require.Equal(t, "https://y", rows[0].URL) require.Equal(t, 2026, rows[0].PublishedAt.Year()) require.Equal(t, videoX, rows[1].VideoID) require.Equal(t, "X Title", rows[1].Title) } func TestListSummariesWithoutVideoRowIsNullSafe(t *testing.T) { ctx := context.Background() s := newStore(t) resetDB(t, rawPool(t)) // Summary exists but no matching videos row (run loop hasn't populated it). require.NoError(t, s.Deliver(ctx, summary(userA, videoX, "orphan"))) rows, err := s.ListSummaries(ctx, userA, 50) require.NoError(t, err) require.Len(t, rows, 1) require.Empty(t, rows[0].Title, "no videos row -> empty title (caller falls back to id)") require.Empty(t, rows[0].Channel) require.Empty(t, rows[0].URL) require.True(t, rows[0].PublishedAt.IsZero(), "absent published_at -> zero time") require.Equal(t, "orphan", rows[0].Summary) } func TestListSummariesRespectsLimit(t *testing.T) { ctx := context.Background() s := newStore(t) p := rawPool(t) resetDB(t, p) require.NoError(t, s.Deliver(ctx, summary(userA, videoX, "x"))) require.NoError(t, s.Deliver(ctx, summary(userA, videoY, "y"))) setCreatedAt(t, p, userA, videoX, time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)) setCreatedAt(t, p, userA, videoY, time.Date(2026, 3, 2, 0, 0, 0, 0, time.UTC)) rows, err := s.ListSummaries(ctx, userA, 1) require.NoError(t, err) require.Len(t, rows, 1) require.Equal(t, videoY, rows[0].VideoID, "limit keeps the most recent") } func TestListSummariesIsUserScoped(t *testing.T) { ctx := context.Background() s := newStore(t) resetDB(t, rawPool(t)) require.NoError(t, s.Deliver(ctx, summary(userA, videoX, "a-only"))) rows, err := s.ListSummaries(ctx, userB, 50) require.NoError(t, err) require.Empty(t, rows, "user B must not see user A's summaries") } func TestGetSummaryByVideoReturnsFullRow(t *testing.T) { ctx := context.Background() s := newStore(t) resetDB(t, rawPool(t)) require.NoError(t, s.Deliver(ctx, summary(userA, videoX, "full body"))) row, err := s.GetSummaryByVideo(ctx, userA, videoX) require.NoError(t, err) require.Equal(t, "full body", row.Summary) require.Equal(t, []string{"h1", "h2"}, row.Highlights) require.Equal(t, []string{"t1"}, row.Takeaways) require.Equal(t, "local", row.AIProvider) require.Equal(t, "qwen", row.AIModel) } func TestGetSummaryByVideoNotFound(t *testing.T) { ctx := context.Background() s := newStore(t) resetDB(t, rawPool(t)) _, err := s.GetSummaryByVideo(ctx, userA, videoX) require.ErrorIs(t, err, store.ErrNotFound) } func TestGetSummaryByVideoIsUserScoped(t *testing.T) { ctx := context.Background() s := newStore(t) resetDB(t, rawPool(t)) require.NoError(t, s.Deliver(ctx, summary(userA, videoX, "a-owns"))) _, err := s.GetSummaryByVideo(ctx, userB, videoX) require.ErrorIs(t, err, store.ErrNotFound, "the same video under another user is invisible") }