From f42b8e23fcddc5fba5ad1d9fad12f22226c384de Mon Sep 17 00:00:00 2001 From: Mathias Date: Thu, 1 Oct 2026 16:39:20 +0200 Subject: [PATCH] test(uploads): validation policy -- video, no-audio, too-long, unreadable (red) Probe-based, not extension-based: a Teams voice-only export is typically an .mp4 with no video stream, indistinguishable from video by name (ADR-033). Red: package uploads does not exist. Co-Authored-By: Claude Sonnet 5.5 --- internal/uploads/validate_test.go | 95 +++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 internal/uploads/validate_test.go diff --git a/internal/uploads/validate_test.go b/internal/uploads/validate_test.go new file mode 100644 index 0000000..b8dc4c6 --- /dev/null +++ b/internal/uploads/validate_test.go @@ -0,0 +1,95 @@ +package uploads_test + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "git.d-ma.be/mathias/tapir/internal/uploads" +) + +var testLimits = uploads.Limits{MaxBytes: 200 << 20, MaxDuration: 90 * time.Minute} + +func TestValidate(t *testing.T) { + tests := []struct { + name string + probe uploads.Probe + wantReason uploads.Reason // "" = accepted + wantInMsg []string + }{ + { + name: "audio-only file within the duration limit is accepted", + probe: uploads.Probe{Duration: 45 * time.Minute, HasAudio: true}, + }, + { + name: "a recording exactly at the limit is accepted", + probe: uploads.Probe{Duration: 90 * time.Minute, HasAudio: true}, + }, + { + name: "a file with a video stream is rejected and names issue 28", + probe: uploads.Probe{Duration: 4 * time.Minute, HasAudio: true, HasVideo: true}, + wantReason: uploads.ReasonVideo, + wantInMsg: []string{"video", "#28"}, + }, + { + name: "video with no audio is still rejected as video, not as missing audio", + probe: uploads.Probe{Duration: 4 * time.Minute, HasVideo: true}, + wantReason: uploads.ReasonVideo, + wantInMsg: []string{"#28"}, + }, + { + name: "a file with no audio stream is rejected", + probe: uploads.Probe{Duration: 4 * time.Minute}, + wantReason: uploads.ReasonNoAudio, + wantInMsg: []string{"no audio"}, + }, + { + name: "a recording over the limit is rejected, stating both lengths", + probe: uploads.Probe{Duration: 102 * time.Minute, HasAudio: true}, + wantReason: uploads.ReasonTooLong, + wantInMsg: []string{"1 h 42 min", "1 h 30 min"}, + }, + { + name: "zero duration is unreadable, not accepted as an empty recording", + probe: uploads.Probe{HasAudio: true}, + wantReason: uploads.ReasonUnreadable, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := uploads.Validate(tt.probe, testLimits) + + if tt.wantReason == "" { + require.NoError(t, err) + return + } + var rej *uploads.RejectError + require.True(t, errors.As(err, &rej), "want a *RejectError, got %v", err) + assert.Equal(t, tt.wantReason, rej.Reason) + for _, s := range tt.wantInMsg { + assert.Contains(t, rej.Message, s) + } + }) + } +} + +func TestFormatDuration(t *testing.T) { + tests := []struct { + d time.Duration + want string + }{ + {90 * time.Minute, "1 h 30 min"}, + {102 * time.Minute, "1 h 42 min"}, + {45 * time.Minute, "45 min"}, + {2 * time.Hour, "2 h"}, + {30 * time.Second, "30 s"}, + } + for _, tt := range tests { + t.Run(tt.want, func(t *testing.T) { + assert.Equal(t, tt.want, uploads.FormatDuration(tt.d)) + }) + } +}