feat(backend): reject non-positive token TTLs at config load
Every duration in the config is a token TTL (access, refresh, content). A zero or negative value mints already-expired tokens — no login, no media playback — and previously loaded silently. parseDuration now rejects <= 0 with a clear error, so misconfiguration fails fast at startup instead of mysteriously at runtime. The AuthService itself stays permissive (it's constructed directly in tests with arbitrary TTLs); config load is the gate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -92,6 +92,10 @@ func Load() (*Config, error) {
|
|||||||
return def
|
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 {
|
parseDuration := func(key, def string) time.Duration {
|
||||||
raw := defaultStr(key, def)
|
raw := defaultStr(key, def)
|
||||||
d, err := time.ParseDuration(raw)
|
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))
|
errs = append(errs, fmt.Errorf("%s: invalid duration %q: %w", key, raw, err))
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
if d <= 0 {
|
||||||
|
errs = append(errs, fmt.Errorf("%s must be positive, got %q", key, raw))
|
||||||
|
return 0
|
||||||
|
}
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user