diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 2aa9641..5aea09f 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -92,6 +92,10 @@ func Load() (*Config, error) { return def } + // parseDuration parses a duration env var. Every duration in this config is a + // token TTL, which must be strictly positive — a zero/negative TTL would mint + // already-expired tokens (no login, no media playback) — so reject those here + // rather than fail mysteriously at runtime. parseDuration := func(key, def string) time.Duration { raw := defaultStr(key, def) d, err := time.ParseDuration(raw) @@ -99,6 +103,10 @@ func Load() (*Config, error) { errs = append(errs, fmt.Errorf("%s: invalid duration %q: %w", key, raw, err)) return 0 } + if d <= 0 { + errs = append(errs, fmt.Errorf("%s must be positive, got %q", key, raw)) + return 0 + } return d } diff --git a/backend/internal/config/config_test.go b/backend/internal/config/config_test.go new file mode 100644 index 0000000..60b6d04 --- /dev/null +++ b/backend/internal/config/config_test.go @@ -0,0 +1,57 @@ +package config + +import ( + "strings" + "testing" +) + +// setValidEnv sets every required variable to a valid dummy value, so a test can +// then override one var to exercise a single validation path. +func setValidEnv(t *testing.T) { + t.Helper() + t.Setenv("JWT_SECRET", "test-secret") + t.Setenv("ADMIN_PASSWORD", "test-password") + t.Setenv("DATABASE_URL", "postgres://u:p@localhost:5432/db?sslmode=disable") + t.Setenv("FILES_PATH", "/tmp/files") + t.Setenv("THUMBS_CACHE_PATH", "/tmp/thumbs") + t.Setenv("IMPORT_PATH", "/tmp/import") + // Pin the TTLs to valid values so an ambient env var can't perturb the case + // under test; individual tests override the one they exercise. + t.Setenv("JWT_ACCESS_TTL", "15m") + t.Setenv("JWT_REFRESH_TTL", "720h") + t.Setenv("CONTENT_TOKEN_TTL", "6h") +} + +func TestLoadValid(t *testing.T) { + setValidEnv(t) + cfg, err := Load() + if err != nil { + t.Fatalf("Load: %v", err) + } + if cfg.JWTAccessTTL <= 0 || cfg.JWTRefreshTTL <= 0 || cfg.ContentTokenTTL <= 0 { + t.Fatalf("TTLs should be positive: access=%v refresh=%v content=%v", + cfg.JWTAccessTTL, cfg.JWTRefreshTTL, cfg.ContentTokenTTL) + } +} + +func TestLoadRejectsNonPositiveTTL(t *testing.T) { + cases := []struct{ key, val string }{ + {"JWT_ACCESS_TTL", "0"}, + {"JWT_REFRESH_TTL", "-1h"}, + {"CONTENT_TOKEN_TTL", "0s"}, + } + for _, tc := range cases { + t.Run(tc.key, func(t *testing.T) { + setValidEnv(t) + t.Setenv(tc.key, tc.val) + + _, err := Load() + if err == nil { + t.Fatalf("expected error for %s=%q", tc.key, tc.val) + } + if !strings.Contains(err.Error(), tc.key) || !strings.Contains(err.Error(), "must be positive") { + t.Fatalf("error should name %s and mention positivity, got: %v", tc.key, err) + } + }) + } +}