Reshape the dead per-user transcripts table (PK videos.id, user_id, RLS-FORCEd — never read or written by app code) into the shared public caption store ADR-021 specifies: keyed by (provider, provider_video_id), no user_id, NOT RLS-scoped. Migration 015 (reversible). Add ports.TranscriptStore + Store.GetTranscript/SaveTranscript via the raw pool (no withUser): public content, shared across users by construction. SaveTranscript persists only terminal outcomes (captions/none) and refuses SourceRateLimited so a transient 429 can never be stored as a false permanent absence (ADR-014). Flip the isolation proof: transcripts leaves the RLS-scoped set; TestTranscriptsTableIsSharedNotRLS asserts it is the SINGLE non-RLS surface (writable/readable with no user scope, no user_id column, RLS off on it alone, still on every user-owned table) — the proof the public-content classification was applied exactly here and leaked nowhere. appPool made idempotent so two tests can build it. Adjust the 010/011/014 up-down migration tests for the new HEAD. account.go: user deletion no longer strips shared transcripts. Reconcile data-model.md + CLAUDE.md. Wiring the engine to read-stored-first is the next commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
88 lines
2.9 KiB
Go
88 lines
2.9 KiB
Go
package store_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"gitea.d-ma.be/mathias/tapir/internal/adapters/store"
|
|
"gitea.d-ma.be/mathias/tapir/internal/domain"
|
|
"gitea.d-ma.be/mathias/tapir/internal/ports"
|
|
)
|
|
|
|
// Static check: Store satisfies the shared TranscriptStore port (ADR-021).
|
|
var _ ports.TranscriptStore = (*store.Store)(nil)
|
|
|
|
func TestSaveAndGetTranscript_RoundTrip(t *testing.T) {
|
|
s := newStore(t)
|
|
resetDB(t, rawPool(t))
|
|
ctx := context.Background()
|
|
|
|
want := domain.Transcript{Source: domain.SourceCaptions, Language: "en", Content: "the words"}
|
|
require.NoError(t, s.SaveTranscript(ctx, "youtube", "vid-1", want))
|
|
|
|
got, ok, err := s.GetTranscript(ctx, "youtube", "vid-1")
|
|
require.NoError(t, err)
|
|
require.True(t, ok, "a saved transcript must be found")
|
|
require.Equal(t, domain.SourceCaptions, got.Source)
|
|
require.Equal(t, "en", got.Language)
|
|
require.Equal(t, "the words", got.Content)
|
|
require.True(t, got.HasText())
|
|
}
|
|
|
|
func TestGetTranscript_Miss(t *testing.T) {
|
|
s := newStore(t)
|
|
resetDB(t, rawPool(t))
|
|
|
|
_, ok, err := s.GetTranscript(context.Background(), "youtube", "absent")
|
|
require.NoError(t, err, "a miss is not an error")
|
|
require.False(t, ok)
|
|
}
|
|
|
|
// A stored "no captions" outcome is a real hit: callers must skip without
|
|
// re-fetching, so ok is true even though there is no text (ADR-021 / ADR-007).
|
|
func TestSaveAndGetTranscript_NoneIsAStoredHit(t *testing.T) {
|
|
s := newStore(t)
|
|
resetDB(t, rawPool(t))
|
|
ctx := context.Background()
|
|
|
|
require.NoError(t, s.SaveTranscript(ctx, "youtube", "vid-none", domain.Transcript{Source: domain.SourceNone}))
|
|
|
|
got, ok, err := s.GetTranscript(ctx, "youtube", "vid-none")
|
|
require.NoError(t, err)
|
|
require.True(t, ok, "a stored SourceNone is a hit, not a miss")
|
|
require.Equal(t, domain.SourceNone, got.Source)
|
|
require.False(t, got.HasText())
|
|
}
|
|
|
|
// A transient 429 must never be persisted as a terminal transcript, or a later
|
|
// read would mask the rate-limit as a permanent "no transcript" (ADR-014).
|
|
func TestSaveTranscript_RejectsRateLimited(t *testing.T) {
|
|
s := newStore(t)
|
|
resetDB(t, rawPool(t))
|
|
|
|
err := s.SaveTranscript(context.Background(), "youtube", "vid-429",
|
|
domain.Transcript{Source: domain.SourceRateLimited})
|
|
require.Error(t, err)
|
|
|
|
_, ok, _ := s.GetTranscript(context.Background(), "youtube", "vid-429")
|
|
require.False(t, ok, "a rejected rate-limited save must leave nothing stored")
|
|
}
|
|
|
|
func TestSaveTranscript_UpsertLastWriteWins(t *testing.T) {
|
|
s := newStore(t)
|
|
resetDB(t, rawPool(t))
|
|
ctx := context.Background()
|
|
|
|
require.NoError(t, s.SaveTranscript(ctx, "youtube", "vid-up", domain.Transcript{Source: domain.SourceNone}))
|
|
require.NoError(t, s.SaveTranscript(ctx, "youtube", "vid-up",
|
|
domain.Transcript{Source: domain.SourceCaptions, Language: "en", Content: "now resolved"}))
|
|
|
|
got, ok, err := s.GetTranscript(ctx, "youtube", "vid-up")
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
require.Equal(t, domain.SourceCaptions, got.Source)
|
|
require.Equal(t, "now resolved", got.Content)
|
|
}
|