veza/veza-backend-api/internal/config/upload_limits_test.go

53 lines
1.6 KiB
Go
Raw Normal View History

feat(backend,web): single source of truth for upload-size limits Second item of the v1.0.6 backlog. The "front 500MB vs back 100MB" mismatch flagged in the v1.0.5 audit turned out to be a misread — every live pair was already aligned (tracks 100/100, cloud 500/500, video 500/500). The real bug is architectural: the same byte values were duplicated in five places (`track/service.go`, `handlers/upload.go:GetUploadLimits`, `handlers/education_handler.go`, `upload-modal/constants.ts`, and `CloudUploadModal.tsx`), drifting silently as soon as anyone tuned one. Backend — one canonical spec at `internal/config/upload_limits.go`: * `AudioLimit`, `ImageLimit`, `VideoLimit` expose `Bytes()`, `MB()`, `HumanReadable()`, `AllowedMIMEs` — read lazily from env (`MAX_UPLOAD_AUDIO_MB`, `MAX_UPLOAD_IMAGE_MB`, `MAX_UPLOAD_VIDEO_MB`) with defaults 100/10/500. * Invalid / negative / zero env values fall back to the default; unreadable config can't turn the limit off silently. * `track.Service.maxFileSize`, `track_upload_handler.go` error string, `education_handler.go` video gate, and `upload.go:GetUploadLimits` all read from this single source. Changing `MAX_UPLOAD_AUDIO_MB` retunes every path at once. Frontend — new `useUploadLimits()` hook: * Fetches GET `/api/v1/upload/limits` via react-query (5 min stale, 30 min gc), one retry, then silently falls back to baked-in defaults that match the backend compile-time defaults so the dropzone stays responsive even without the network round-trip. * `useUploadModal.ts` replaces its hardcoded `MAX_FILE_SIZE` constant with `useUploadLimits().audio.maxBytes`, and surfaces `audioMaxHuman` up to `UploadModal` → `UploadModalDropzone` so the "max 100 MB" label and the "too large" error toast both display the live value. * `MAX_FILE_SIZE` constant kept as pure fallback for pre-network render (documented as such). Tests * 4 Go tests on `config.UploadLimit` (defaults, env override, invalid env → fallback, non-empty MIME lists). * 4 Vitest tests on `useUploadLimits` (sync fallback on first render, typed mapping from server payload, partial-payload falls back per-category, network failure keeps fallback). * Existing `trackUpload.integration.test.tsx` (11 cases) still green. Out of scope (tracked for later): * `CloudUploadModal.tsx` still has its own 500MB hardcoded — cloud uploads accept audio+zip+midi with a different category semantic than the three in `/upload/limits`. Unifying those deserves its own design pass, not a drive-by. * No runtime refactor of admin-provided custom category limits — the current tri-category split covers every upload we ship today. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 17:37:37 +00:00
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestUploadLimit_DefaultBytesAndMB(t *testing.T) {
t.Setenv(AudioLimit.EnvVar, "")
t.Setenv(ImageLimit.EnvVar, "")
t.Setenv(VideoLimit.EnvVar, "")
assert.Equal(t, 100, AudioLimit.MB(), "audio default must match historical 100MB value")
assert.Equal(t, int64(100*1024*1024), AudioLimit.Bytes())
assert.Equal(t, "100MB", AudioLimit.HumanReadable())
assert.Equal(t, 10, ImageLimit.MB())
assert.Equal(t, int64(10*1024*1024), ImageLimit.Bytes())
assert.Equal(t, "10MB", ImageLimit.HumanReadable())
assert.Equal(t, 500, VideoLimit.MB())
assert.Equal(t, int64(500*1024*1024), VideoLimit.Bytes())
}
func TestUploadLimit_EnvOverride(t *testing.T) {
t.Setenv(AudioLimit.EnvVar, "250")
assert.Equal(t, 250, AudioLimit.MB())
assert.Equal(t, int64(250*1024*1024), AudioLimit.Bytes())
assert.Equal(t, "250MB", AudioLimit.HumanReadable())
}
func TestUploadLimit_InvalidEnvFallsBackToDefault(t *testing.T) {
t.Setenv(AudioLimit.EnvVar, "not-a-number")
assert.Equal(t, DefaultAudioMaxMB, AudioLimit.MB(),
"non-numeric env must fall back to default")
t.Setenv(AudioLimit.EnvVar, "-50")
assert.Equal(t, DefaultAudioMaxMB, AudioLimit.MB(),
"negative env must fall back to default")
t.Setenv(AudioLimit.EnvVar, "0")
assert.Equal(t, DefaultAudioMaxMB, AudioLimit.MB(),
"zero env must fall back to default")
}
func TestUploadLimit_AllowedMIMEsAreNonEmpty(t *testing.T) {
for _, l := range []UploadLimitMB{AudioLimit, ImageLimit, VideoLimit} {
assert.NotEmpty(t, l.AllowedMIMEs, "category %s must declare its MIME list", l.Category)
}
}