// SPDX-License-Identifier: AGPL-3.0-or-later package config import ( "testing" "time" ) func TestLoadDefaults(t *testing.T) { // Clear all FAVORITTER_ env vars to test defaults. for _, key := range []string{ "FAVORITTER_DB_PATH", "FAVORITTER_LISTEN", "FAVORITTER_BASE_PATH", "FAVORITTER_EXTERNAL_URL", "FAVORITTER_UPLOAD_DIR", "FAVORITTER_MAX_UPLOAD_SIZE", "FAVORITTER_SESSION_LIFETIME", "FAVORITTER_ARGON2_MEMORY", "FAVORITTER_ARGON2_TIME", "FAVORITTER_ARGON2_PARALLELISM", "FAVORITTER_RATE_LIMIT", "FAVORITTER_ADMIN_USERNAME", "FAVORITTER_ADMIN_PASSWORD", "FAVORITTER_SITE_NAME", "FAVORITTER_DEV_MODE", "FAVORITTER_TRUSTED_PROXIES", } { t.Setenv(key, "") } cfg := Load() if cfg.DBPath != "./data/favoritter.db" { t.Errorf("DBPath = %q, want default", cfg.DBPath) } if cfg.Listen != ":8080" { t.Errorf("Listen = %q, want :8080", cfg.Listen) } if cfg.BasePath != "" { t.Errorf("BasePath = %q, want empty (root)", cfg.BasePath) } if cfg.MaxUploadSize != 10<<20 { t.Errorf("MaxUploadSize = %d, want %d", cfg.MaxUploadSize, 10<<20) } if cfg.SessionLifetime != 720*time.Hour { t.Errorf("SessionLifetime = %v, want 720h", cfg.SessionLifetime) } if cfg.SiteName != "Favoritter" { t.Errorf("SiteName = %q, want Favoritter", cfg.SiteName) } if cfg.DevMode { t.Error("DevMode should be false by default") } if cfg.RateLimit != 60 { t.Errorf("RateLimit = %d, want 60", cfg.RateLimit) } } func TestLoadFromEnv(t *testing.T) { t.Setenv("FAVORITTER_DB_PATH", "/custom/db.sqlite") t.Setenv("FAVORITTER_LISTEN", ":9090") t.Setenv("FAVORITTER_BASE_PATH", "/faves") t.Setenv("FAVORITTER_EXTERNAL_URL", "https://faves.example.com/") t.Setenv("FAVORITTER_UPLOAD_DIR", "/custom/uploads") t.Setenv("FAVORITTER_MAX_UPLOAD_SIZE", "20971520") t.Setenv("FAVORITTER_SESSION_LIFETIME", "48h") t.Setenv("FAVORITTER_ARGON2_MEMORY", "131072") t.Setenv("FAVORITTER_ARGON2_TIME", "5") t.Setenv("FAVORITTER_ARGON2_PARALLELISM", "4") t.Setenv("FAVORITTER_RATE_LIMIT", "100") t.Setenv("FAVORITTER_ADMIN_USERNAME", "admin") t.Setenv("FAVORITTER_ADMIN_PASSWORD", "secret") t.Setenv("FAVORITTER_SITE_NAME", "Mine Favoritter") t.Setenv("FAVORITTER_DEV_MODE", "true") t.Setenv("FAVORITTER_TRUSTED_PROXIES", "10.0.0.0/8,192.168.1.0/24") cfg := Load() if cfg.DBPath != "/custom/db.sqlite" { t.Errorf("DBPath = %q", cfg.DBPath) } if cfg.Listen != ":9090" { t.Errorf("Listen = %q", cfg.Listen) } if cfg.BasePath != "/faves" { t.Errorf("BasePath = %q, want /faves", cfg.BasePath) } // External URL should have trailing slash stripped. if cfg.ExternalURL != "https://faves.example.com" { t.Errorf("ExternalURL = %q, want trailing slash stripped", cfg.ExternalURL) } if cfg.MaxUploadSize != 20971520 { t.Errorf("MaxUploadSize = %d", cfg.MaxUploadSize) } if cfg.SessionLifetime != 48*time.Hour { t.Errorf("SessionLifetime = %v", cfg.SessionLifetime) } if cfg.Argon2Memory != 131072 { t.Errorf("Argon2Memory = %d", cfg.Argon2Memory) } if cfg.Argon2Time != 5 { t.Errorf("Argon2Time = %d", cfg.Argon2Time) } if cfg.Argon2Parallelism != 4 { t.Errorf("Argon2Parallelism = %d", cfg.Argon2Parallelism) } if cfg.RateLimit != 100 { t.Errorf("RateLimit = %d", cfg.RateLimit) } if cfg.AdminUsername != "admin" { t.Errorf("AdminUsername = %q", cfg.AdminUsername) } if cfg.AdminPassword != "secret" { t.Errorf("AdminPassword = %q", cfg.AdminPassword) } if cfg.SiteName != "Mine Favoritter" { t.Errorf("SiteName = %q", cfg.SiteName) } if !cfg.DevMode { t.Error("DevMode should be true") } if len(cfg.TrustedProxies) != 2 { t.Errorf("TrustedProxies: got %d, want 2", len(cfg.TrustedProxies)) } } func TestTrustedProxiesParsing(t *testing.T) { t.Setenv("FAVORITTER_TRUSTED_PROXIES", "10.0.0.0/8, 192.168.0.0/16, 127.0.0.1") cfg := Load() if len(cfg.TrustedProxies) != 3 { t.Fatalf("TrustedProxies: got %d, want 3", len(cfg.TrustedProxies)) } // 127.0.0.1 without CIDR should become 127.0.0.1/32. last := cfg.TrustedProxies[2] if ones, _ := last.Mask.Size(); ones != 32 { t.Errorf("bare IP mask = /%d, want /32", ones) } } func TestTrustedProxiesInvalid(t *testing.T) { t.Setenv("FAVORITTER_TRUSTED_PROXIES", "not-an-ip, 10.0.0.0/8") cfg := Load() // Invalid entries are skipped; valid ones remain. if len(cfg.TrustedProxies) != 1 { t.Errorf("TrustedProxies: got %d, want 1 (invalid skipped)", len(cfg.TrustedProxies)) } } func TestBasePathNormalization(t *testing.T) { tests := []struct { input string want string }{ {"/", ""}, {"", ""}, {"/faves", "/faves"}, {"/faves/", "/faves"}, {"faves", "/faves"}, {"/sub/path/", "/sub/path"}, } for _, tt := range tests { got := normalizePath(tt.input) if got != tt.want { t.Errorf("normalizePath(%q) = %q, want %q", tt.input, got, tt.want) } } } func TestDevModeFlag(t *testing.T) { t.Setenv("FAVORITTER_DEV_MODE", "true") cfg := Load() if !cfg.DevMode { t.Error("DevMode should be true when env is 'true'") } t.Setenv("FAVORITTER_DEV_MODE", "false") cfg = Load() if cfg.DevMode { t.Error("DevMode should be false when env is 'false'") } } func TestExternalHostname(t *testing.T) { cfg := &Config{ExternalURL: "https://faves.example.com/base"} if got := cfg.ExternalHostname(); got != "faves.example.com" { t.Errorf("ExternalHostname = %q, want faves.example.com", got) } cfg = &Config{} if got := cfg.ExternalHostname(); got != "" { t.Errorf("empty ExternalURL: ExternalHostname = %q, want empty", got) } } func TestBaseURL(t *testing.T) { // With external URL configured. cfg := &Config{ExternalURL: "https://faves.example.com"} if got := cfg.BaseURL("localhost:8080"); got != "https://faves.example.com" { t.Errorf("BaseURL with external = %q", got) } // Without external URL — falls back to request host. cfg = &Config{BasePath: "/faves"} if got := cfg.BaseURL("myhost.local:8080"); got != "https://myhost.local:8080/faves" { t.Errorf("BaseURL fallback = %q", got) } }