package uploads_test import ( "context" "os" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "git.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/uploads" ) // The duration limit is a claim in the file's header. The worker measures what was // really converted and refuses both a recording that is longer than the limit (the // header lied low) and one that ends far earlier than the file claimed (it was truncated // and would otherwise yield a partial transcript with no warning) -- adversarial review F2. // sizedConverter writes a (sparse) 16 kHz mono PCM WAV of the given length, without // actually allocating it: 32,000 bytes per second plus a 44-byte header. type sizedConverter struct{ seconds float64 } func (c sizedConverter) ToWAV(_ context.Context, _, dst string) error { f, err := os.Create(dst) if err != nil { return err } defer func() { _ = f.Close() }() return f.Truncate(44 + int64(c.seconds*32000)) } func durationWorker(t *testing.T, r *wrig, wavSeconds float64, max time.Duration) *uploads.Worker { t.Helper() opts := []uploads.WorkerOption{} if max > 0 { opts = append(opts, uploads.WithMaxDuration(max)) } return uploads.NewWorker(r.st, sizedConverter{seconds: wavSeconds}, r.tr, r.tmp, time.Minute, opts...) } func jobWith(r *wrig, t *testing.T, claimed time.Duration) uploads.Job { j := r.spool(t, "a") j.Duration = claimed return j } func TestWorkerDuration_AConvertedLengthOverTheLimitFailsTheJobBeforeAnyEngineSeesIt(t *testing.T) { r := newWRig(t, time.Minute) w := durationWorker(t, r, 100*60, 90*time.Minute) w.Process(context.Background(), jobWith(r, t, 10*time.Second)) _, failed := r.st.snapshot() assert.Equal(t, domain.UploadErrTooLong, failed["a"], "a header that claimed 10 s must not let 100 minutes through") assert.Empty(t, r.tr.paths, "no engine may be sent audio that is over the limit") assert.Empty(t, r.leftovers(t), "the audio is still deleted") } func TestWorkerDuration_JustOverTheLimitWithinTheSlackStillTranscribes(t *testing.T) { r := newWRig(t, time.Minute) w := durationWorker(t, r, 90*60+20, 90*time.Minute) w.Process(context.Background(), jobWith(r, t, 90*time.Minute)) completed, failed := r.st.snapshot() assert.Empty(t, failed) assert.Contains(t, completed, "a", "ffmpeg rounds: 20 s over a 90-minute limit is not a lie") } func TestWorkerDuration_AFileThatEndsFarEarlierThanItClaimedIsRefusedNotHalfTranscribed(t *testing.T) { r := newWRig(t, time.Minute) w := durationWorker(t, r, 20*60, 90*time.Minute) w.Process(context.Background(), jobWith(r, t, 60*time.Minute)) _, failed := r.st.snapshot() assert.Equal(t, domain.UploadErrIncomplete, failed["a"], "a recording truncated at a third must not return a third of a transcript with no warning") assert.Empty(t, r.tr.paths) } func TestWorkerDuration_AnEstimateThatIsMerelyALittleOffIsNotTruncation(t *testing.T) { tests := []struct { name string claimed time.Duration wav float64 }{ {"five percent short", 60 * time.Minute, 57 * 60}, {"under two minutes short of a long file", 60 * time.Minute, 59 * 60}, {"unknown claimed length", 0, 20 * 60}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := newWRig(t, time.Minute) w := durationWorker(t, r, tt.wav, 90*time.Minute) w.Process(context.Background(), jobWith(r, t, tt.claimed)) completed, failed := r.st.snapshot() assert.Empty(t, failed) assert.Contains(t, completed, "a") }) } } func TestWorkerDuration_WithNoMaximumConfiguredNothingIsMeasured(t *testing.T) { r := newWRig(t, time.Minute) w := durationWorker(t, r, 100*60, 0) w.Process(context.Background(), jobWith(r, t, 0)) completed, _ := r.st.snapshot() assert.Contains(t, completed, "a") require.NotNil(t, w) }