package audio_test import ( "context" "errors" "os" "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "git.d-ma.be/mathias/tapir/internal/adapters/audio" "git.d-ma.be/mathias/tapir/internal/uploads" ) func TestParseProbe(t *testing.T) { tests := []struct { name string json string want uploads.Probe }{ { name: "audio-only m4a", json: `{"streams":[{"codec_type":"audio","disposition":{"attached_pic":0}}],"format":{"duration":"2701.5"}}`, want: uploads.Probe{Duration: 2701*time.Second + 500*time.Millisecond, HasAudio: true}, }, { name: "mp4 with a real video stream", json: `{"streams":[{"codec_type":"video","disposition":{"attached_pic":0}},{"codec_type":"audio"}],"format":{"duration":"240.0"}}`, want: uploads.Probe{Duration: 240 * time.Second, HasAudio: true, HasVideo: true}, }, { name: "mp3 whose only video stream is attached cover art is NOT video", json: `{"streams":[{"codec_type":"audio"},{"codec_type":"video","disposition":{"attached_pic":1}}],"format":{"duration":"60"}}`, want: uploads.Probe{Duration: 60 * time.Second, HasAudio: true}, }, { name: "no streams at all", json: `{"streams":[],"format":{"duration":"10"}}`, want: uploads.Probe{Duration: 10 * time.Second}, }, { name: "duration absent means zero, which Validate rejects as unreadable", json: `{"streams":[{"codec_type":"audio"}],"format":{}}`, want: uploads.Probe{HasAudio: true}, }, { name: "duration reported as N/A means zero", json: `{"streams":[{"codec_type":"audio"}],"format":{"duration":"N/A"}}`, want: uploads.Probe{HasAudio: true}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := audio.ParseProbe([]byte(tt.json)) require.NoError(t, err) assert.Equal(t, tt.want, got) }) } } func TestParseProbe_GarbageIsAnError(t *testing.T) { _, err := audio.ParseProbe([]byte("not json")) assert.Error(t, err) } // stub writes an executable shell script standing in for ffprobe. This is a fake // at the process boundary: the real binary is not on every host (it is not on // koala), and what is under test is how Probe drives and interprets a process. func stub(t *testing.T, body string) string { t.Helper() p := filepath.Join(t.TempDir(), "ffprobe") require.NoError(t, os.WriteFile(p, []byte("#!/bin/sh\n"+body+"\n"), 0o755)) return p } func TestFFprobeProbe_ParsesWhatTheProcessPrints(t *testing.T) { bin := stub(t, `echo '{"streams":[{"codec_type":"audio"}],"format":{"duration":"90"}}'`) got, err := (&audio.FFprobe{Bin: bin}).Probe(context.Background(), "/ignored") require.NoError(t, err) assert.Equal(t, uploads.Probe{Duration: 90 * time.Second, HasAudio: true}, got) } func TestFFprobeProbe_PassesTheFilePathAsTheLastArgument(t *testing.T) { out := filepath.Join(t.TempDir(), "args") bin := stub(t, `for a; do last="$a"; done; printf '%s' "$last" > `+out+` echo '{"streams":[{"codec_type":"audio"}],"format":{"duration":"1"}}'`) _, err := (&audio.FFprobe{Bin: bin}).Probe(context.Background(), "/spool/upload-123") require.NoError(t, err) got, err := os.ReadFile(out) require.NoError(t, err) assert.Equal(t, "/spool/upload-123", string(got)) } func TestFFprobeProbe_NonzeroExitMeansTheFileIsUnreadable(t *testing.T) { bin := stub(t, `echo "Invalid data found when processing input" >&2; exit 1`) _, err := (&audio.FFprobe{Bin: bin}).Probe(context.Background(), "/x") assert.ErrorIs(t, err, uploads.ErrUnreadable) } func TestFFprobeProbe_MissingBinaryIsOurFaultNotAnUnreadableFile(t *testing.T) { _, err := (&audio.FFprobe{Bin: filepath.Join(t.TempDir(), "no-such-ffprobe")}).Probe(context.Background(), "/x") require.Error(t, err) assert.False(t, errors.Is(err, uploads.ErrUnreadable), "a missing ffprobe must not be reported to the user as their file being bad") } func TestFFprobeProbe_StopsWhenTheContextIsCancelled(t *testing.T) { bin := stub(t, `sleep 30`) ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() start := time.Now() _, err := (&audio.FFprobe{Bin: bin}).Probe(ctx, "/x") require.Error(t, err) assert.Less(t, time.Since(start), 5*time.Second) assert.False(t, errors.Is(err, uploads.ErrUnreadable), "a timeout is not a verdict on the file") }