package store_test import ( "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "git.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/domain" ) // These exercise the Store API over uploaded_transcripts. The test Store connects // as the superuser, which BYPASSES row-level security — so the cross-user cases // here prove the query's own user_id predicate (defence in depth), while the // database-enforced guarantee is proven separately, as the non-superuser app // role, in uploaded_transcripts_isolation_test.go. Text is synthetic; this repo is // public and no real transcript may appear in it (#33). func seedUsers(t *testing.T, ids ...string) { t.Helper() super := rawPool(t) resetDB(t, super) for _, id := range ids { _, err := super.Exec(context.Background(), `INSERT INTO users (id) VALUES ($1)`, id) require.NoError(t, err) } } func TestCreateUpload_StartsQueuedAndIsOwnedByTheCreator(t *testing.T) { s := newStore(t) seedUsers(t, userA) ctx := context.Background() id, err := s.CreateUpload(ctx, userA, "Teams export", false) require.NoError(t, err) got, err := s.GetUpload(ctx, userA, id) require.NoError(t, err) assert.Equal(t, id, got.ID) assert.Equal(t, userA, got.UserID) assert.Equal(t, "Teams export", got.Title) assert.Equal(t, domain.UploadQueued, got.Status) assert.Empty(t, got.Content) assert.True(t, got.FinishedAt.IsZero(), "an unfinished upload has no finish time") } func TestUploadLifecycle_CompletedKeepsTextSegmentsEndpointAndDuration(t *testing.T) { s := newStore(t) seedUsers(t, userA) ctx := context.Background() id, err := s.CreateUpload(ctx, userA, "m", false) require.NoError(t, err) require.NoError(t, s.StartTranscribing(ctx, userA, id)) mid, err := s.GetUpload(ctx, userA, id) require.NoError(t, err) assert.Equal(t, domain.UploadTranscribing, mid.Status) want := domain.Transcription{ Text: "God morgon allihopa. Vi går igenom läget.", Segments: []domain.Segment{ {Start: 0.0, End: 1.5, Text: "God morgon allihopa."}, {Start: 1.5, End: 4.25, Text: "Vi går igenom läget."}, }, Endpoint: "iguana/kb-whisper", AudioSeconds: 4.25, } require.NoError(t, s.CompleteUpload(ctx, userA, id, want)) got, err := s.GetUpload(ctx, userA, id) require.NoError(t, err) assert.Equal(t, domain.UploadDone, got.Status) assert.Equal(t, want.Text, got.Content) assert.Equal(t, want.Segments, got.Segments, "segment times must survive the JSONB round trip") assert.Equal(t, "iguana/kb-whisper", got.Endpoint, "the serving endpoint is a quality fact and is recorded per row") assert.InDelta(t, 4.25, got.AudioSeconds, 1e-9) assert.False(t, got.FinishedAt.IsZero()) assert.Empty(t, got.ErrorKind) } func TestUploadLifecycle_FailedRecordsTypedKindAndNoContent(t *testing.T) { s := newStore(t) seedUsers(t, userA) ctx := context.Background() id, err := s.CreateUpload(ctx, userA, "m", false) require.NoError(t, err) require.NoError(t, s.StartTranscribing(ctx, userA, id)) require.NoError(t, s.FailUpload(ctx, userA, id, domain.UploadErrTranscriptionFailed)) got, err := s.GetUpload(ctx, userA, id) require.NoError(t, err) assert.Equal(t, domain.UploadFailed, got.Status) assert.Equal(t, domain.UploadErrTranscriptionFailed, got.ErrorKind) assert.Empty(t, got.Content) assert.False(t, got.FinishedAt.IsZero()) } func TestGetUpload_AnotherUsersIDIsNotFound(t *testing.T) { s := newStore(t) seedUsers(t, userA, userB) ctx := context.Background() id, err := s.CreateUpload(ctx, userA, "A's meeting", false) require.NoError(t, err) _, err = s.GetUpload(ctx, userB, id) assert.ErrorIs(t, err, store.ErrUploadNotFound, "a non-owner must get not-found, not forbidden: existence is not disclosed") } func TestUploadWrites_ByAnotherUserChangeNothing(t *testing.T) { s := newStore(t) seedUsers(t, userA, userB) ctx := context.Background() id, err := s.CreateUpload(ctx, userA, "A's meeting", false) require.NoError(t, err) assert.ErrorIs(t, s.StartTranscribing(ctx, userB, id), store.ErrUploadNotFound) assert.ErrorIs(t, s.CompleteUpload(ctx, userB, id, domain.Transcription{Text: "planted"}), store.ErrUploadNotFound) assert.ErrorIs(t, s.FailUpload(ctx, userB, id, domain.UploadErrTranscriptionFailed), store.ErrUploadNotFound) got, err := s.GetUpload(ctx, userA, id) require.NoError(t, err) assert.Equal(t, domain.UploadQueued, got.Status, "B's writes must leave A's upload exactly as it was") assert.Empty(t, got.Content) } func TestListUploads_ReturnsOnlyTheCallersNewestFirstWithoutTranscriptText(t *testing.T) { s := newStore(t) seedUsers(t, userA, userB) ctx := context.Background() first, err := s.CreateUpload(ctx, userA, "first", false) require.NoError(t, err) second, err := s.CreateUpload(ctx, userA, "second", false) require.NoError(t, err) _, err = s.CreateUpload(ctx, userB, "B's", false) require.NoError(t, err) require.NoError(t, s.StartTranscribing(ctx, userA, first)) require.NoError(t, s.CompleteUpload(ctx, userA, first, domain.Transcription{Text: "long text body", Endpoint: "e"})) got, err := s.ListUploads(ctx, userA) require.NoError(t, err) require.Len(t, got, 2, "A must see only A's uploads") assert.Equal(t, second, got[0].ID, "newest first") assert.Equal(t, first, got[1].ID) assert.Empty(t, got[1].Content, "a list row must not drag the whole transcript along") assert.Equal(t, domain.UploadDone, got[1].Status) } func TestFailInterruptedUploads_FailsOnlyUnfinishedRowsForEveryUser(t *testing.T) { s := newStore(t) seedUsers(t, userA, userB) super := rawPool(t) ctx := context.Background() for subject, uid := range map[string]string{"sub-a": userA, "sub-b": userB} { _, err := super.Exec(ctx, `INSERT INTO user_identities (dex_subject, user_id) VALUES ($1, $2)`, subject, uid) require.NoError(t, err) } queued, err := s.CreateUpload(ctx, userA, "queued", false) require.NoError(t, err) running, err := s.CreateUpload(ctx, userB, "running", false) require.NoError(t, err) require.NoError(t, s.StartTranscribing(ctx, userB, running)) done, err := s.CreateUpload(ctx, userA, "done", false) require.NoError(t, err) require.NoError(t, s.StartTranscribing(ctx, userA, done)) require.NoError(t, s.CompleteUpload(ctx, userA, done, domain.Transcription{Text: "kept", Endpoint: "e"})) n, err := s.FailInterruptedUploads(ctx) require.NoError(t, err) assert.Equal(t, 2, n, "the queued and the transcribing row, across both users") for _, c := range []struct{ user, id string }{{userA, queued}, {userB, running}} { got, err := s.GetUpload(ctx, c.user, c.id) require.NoError(t, err) assert.Equal(t, domain.UploadFailed, got.Status) assert.Equal(t, domain.UploadErrInterrupted, got.ErrorKind, "audio is not retained, so a restart cannot resume: the honest state is failed/interrupted") } kept, err := s.GetUpload(ctx, userA, done) require.NoError(t, err) assert.Equal(t, domain.UploadDone, kept.Status, "a finished transcript must survive the sweep") assert.Equal(t, "kept", kept.Content) } func TestCreateUpload_RecordsWhetherTheFileHadAVideoTrack(t *testing.T) { s := newStore(t) seedUsers(t, userA) ctx := context.Background() withVideo, err := s.CreateUpload(ctx, userA, "camera off", true) require.NoError(t, err) audioOnly, err := s.CreateUpload(ctx, userA, "voice only", false) require.NoError(t, err) gotVideo, err := s.GetUpload(ctx, userA, withVideo) require.NoError(t, err) gotAudio, err := s.GetUpload(ctx, userA, audioOnly) require.NoError(t, err) listed, err := s.ListUploads(ctx, userA) require.NoError(t, err) assert.True(t, gotVideo.HadVideo, "the page must be able to say the video was ignored") assert.False(t, gotAudio.HadVideo) byID := map[string]bool{} for _, u := range listed { byID[u.ID] = u.HadVideo } assert.Equal(t, map[string]bool{withVideo: true, audioOnly: false}, byID, "the list carries it too") } func TestUploadHadVideo_SurvivesCompletionAndFailure(t *testing.T) { s := newStore(t) seedUsers(t, userA) ctx := context.Background() done, err := s.CreateUpload(ctx, userA, "d", true) require.NoError(t, err) failed, err := s.CreateUpload(ctx, userA, "f", true) require.NoError(t, err) require.NoError(t, s.StartTranscribing(ctx, userA, done)) require.NoError(t, s.CompleteUpload(ctx, userA, done, domain.Transcription{Text: "x", Endpoint: "e"})) require.NoError(t, s.FailUpload(ctx, userA, failed, domain.UploadErrTranscriptionFailed)) for _, id := range []string{done, failed} { got, err := s.GetUpload(ctx, userA, id) require.NoError(t, err) assert.True(t, got.HadVideo, "completing or failing must not clear it: the notice outlives the job") } }