Files
mathiasandClaude Sonnet 4.6 38f222c931
CI / Lint / Test / Vet (push) Successful in 11s
CI / Build & Import (push) Successful in 12s
chore: rename Go module path gitea.d-ma.be → git.d-ma.be
Infra ADR-0004 renamed the Gitea host. Bulk replace across go.mod and
all .go import paths. Build and tests pass unchanged.

Closes #20

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dt6aHEDWRjkK14Voi6HnGh
2026-07-02 14:37:33 +02:00

149 lines
4.9 KiB
Go

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")
}