feat(store): UpsertVideo to persist video metadata

The Sink port carries only a Summary, so video title/url/published_at would
never reach the store. UpsertVideo (new file, store.go untouched) persists them
and returns the durable videos.id UUID, idempotent on
(user_id, provider, provider_video_id). The run loop uses that id as v.ID, so
it equals summaries.video_id and SeenVideoIDs dedup survives restarts.
subscription_id stays NULL: the YouTube resource id is not a UUID (Stage 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 21:00:04 +02:00
co-authored by Claude Opus 4.8
parent 460f4bb1de
commit 6beb779df6
2 changed files with 160 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
package store_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/domain"
)
func ytVideo(userID, provVideoID, title string) domain.Video {
return domain.Video{
UserID: userID,
Provider: domain.ProviderYouTube,
ProviderVideoID: provVideoID,
Title: title,
URL: "https://www.youtube.com/watch?v=" + provVideoID,
PublishedAt: time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC),
// SubscriptionID is a provider resource id, not a UUID — must not be
// written to the UUID column. Set it to prove UpsertVideo ignores it.
SubscriptionID: "yt-subscription-resource-id",
}
}
func TestUpsertVideo_ReturnsStableID(t *testing.T) {
ctx := context.Background()
s := newStore(t)
resetDB(t, rawPool(t))
id1, err := s.UpsertVideo(ctx, ytVideo(userA, "dQw4w9WgXcQ", "first title"))
require.NoError(t, err)
require.NotEmpty(t, id1)
// Same (user, provider, provider_video_id) -> same row, same id, updated meta.
id2, err := s.UpsertVideo(ctx, ytVideo(userA, "dQw4w9WgXcQ", "updated title"))
require.NoError(t, err)
require.Equal(t, id1, id2, "idempotent upsert must return the same durable id")
p := rawPool(t)
var (
title string
count int
)
require.NoError(t, p.QueryRow(ctx,
`SELECT title FROM videos WHERE id = $1`, id1).Scan(&title))
require.Equal(t, "updated title", title, "second upsert must update metadata in place")
require.NoError(t, p.QueryRow(ctx,
`SELECT count(*) FROM videos WHERE user_id = $1`, userA).Scan(&count))
require.Equal(t, 1, count, "must not duplicate the row")
}
func TestUpsertVideo_IDMatchesSummaryDedup(t *testing.T) {
ctx := context.Background()
s := newStore(t)
resetDB(t, rawPool(t))
// Upsert assigns the durable video id; a summary delivered under that id
// must then show up in SeenVideoIDs — this is the cross-restart dedup chain.
id, err := s.UpsertVideo(ctx, ytVideo(userA, "abc123", "t"))
require.NoError(t, err)
require.NoError(t, s.Deliver(ctx, summary(userA, id, "the summary")))
seen, err := s.SeenVideoIDs(ctx, userA)
require.NoError(t, err)
require.True(t, seen[id], "the upserted video id must match the summary dedup key")
}
func TestUpsertVideo_PerUserIsolation(t *testing.T) {
ctx := context.Background()
s := newStore(t)
resetDB(t, rawPool(t))
idA, err := s.UpsertVideo(ctx, ytVideo(userA, "same-provider-id", "a"))
require.NoError(t, err)
idB, err := s.UpsertVideo(ctx, ytVideo(userB, "same-provider-id", "b"))
require.NoError(t, err)
require.NotEqual(t, idA, idB, "same provider video for two users must be two distinct rows")
}