316 lines
11 KiB
Go
316 lines
11 KiB
Go
//nolint:goconst // Settings contract tests intentionally repeat literal keys in input and expected maps.
|
|
package config
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestEffectiveAdminSettingsUsesRuntimeDefaults(t *testing.T) {
|
|
effective := EffectiveAdminSettings(map[string]string{
|
|
"database.max_connections": "",
|
|
"server.log_level": "debug",
|
|
"custom.setting": "kept",
|
|
})
|
|
|
|
if got := effective["database.max_connections"]; got != "20" {
|
|
t.Fatalf("database.max_connections = %q, want 20", got)
|
|
}
|
|
if got := effective["s3.public_path_style"]; got != "true" {
|
|
t.Fatalf("s3.public_path_style = %q, want true", got)
|
|
}
|
|
if got := effective["playback.transcode_enabled"]; got != "true" {
|
|
t.Fatalf("playback.transcode_enabled = %q, want true", got)
|
|
}
|
|
if got := effective["theme.catalog_url"]; got != DefaultThemeCatalogURL {
|
|
t.Fatalf("theme.catalog_url = %q, want %q", got, DefaultThemeCatalogURL)
|
|
}
|
|
if got := effective["server.log_level"]; got != "debug" {
|
|
t.Fatalf("server.log_level = %q, want debug", got)
|
|
}
|
|
if got := effective["custom.setting"]; got != "kept" {
|
|
t.Fatalf("custom.setting = %q, want kept", got)
|
|
}
|
|
}
|
|
|
|
func TestEffectiveAdminSettingsUsesLegacyS3FallbacksBeforeDefaults(t *testing.T) {
|
|
effective := EffectiveAdminSettings(map[string]string{
|
|
"s3.operational_path_style": "false",
|
|
"s3.operational_token_ttl": "3600",
|
|
})
|
|
|
|
if got := effective["s3.public_path_style"]; got != "false" {
|
|
t.Fatalf("s3.public_path_style = %q, want legacy false", got)
|
|
}
|
|
if got := effective["s3.private_path_style"]; got != "false" {
|
|
t.Fatalf("s3.private_path_style = %q, want legacy false", got)
|
|
}
|
|
if got := effective["s3.public_token_ttl"]; got != "3600" {
|
|
t.Fatalf("s3.public_token_ttl = %q, want legacy 3600", got)
|
|
}
|
|
}
|
|
|
|
func TestEffectiveAdminSettingsCanonicalS3ValuesOverrideLegacyFallbacks(t *testing.T) {
|
|
effective := EffectiveAdminSettings(map[string]string{
|
|
"s3.public_path_style": "true",
|
|
"s3.private_path_style": "false",
|
|
"s3.operational_path_style": "true",
|
|
"s3.public_token_ttl": "7200",
|
|
"s3.operational_token_ttl": "3600",
|
|
})
|
|
|
|
if got := effective["s3.public_path_style"]; got != "true" {
|
|
t.Fatalf("s3.public_path_style = %q, want canonical true", got)
|
|
}
|
|
if got := effective["s3.private_path_style"]; got != "false" {
|
|
t.Fatalf("s3.private_path_style = %q, want canonical false", got)
|
|
}
|
|
if got := effective["s3.public_token_ttl"]; got != "7200" {
|
|
t.Fatalf("s3.public_token_ttl = %q, want canonical 7200", got)
|
|
}
|
|
}
|
|
|
|
func TestEffectiveAdminSettingsEmptyCanonicalS3ValuesUseLegacyFallbacks(t *testing.T) {
|
|
effective := EffectiveAdminSettings(map[string]string{
|
|
"s3.public_path_style": "",
|
|
"s3.private_path_style": "",
|
|
"s3.operational_path_style": "false",
|
|
"s3.public_token_ttl": "",
|
|
"s3.operational_token_ttl": "3600",
|
|
})
|
|
|
|
if got := effective["s3.public_path_style"]; got != "false" {
|
|
t.Fatalf("s3.public_path_style = %q, want legacy false", got)
|
|
}
|
|
if got := effective["s3.private_path_style"]; got != "false" {
|
|
t.Fatalf("s3.private_path_style = %q, want legacy false", got)
|
|
}
|
|
if got := effective["s3.public_token_ttl"]; got != "3600" {
|
|
t.Fatalf("s3.public_token_ttl = %q, want legacy 3600", got)
|
|
}
|
|
}
|
|
|
|
func TestEffectiveAdminSettingsProjectsRuntimeLegacyFallbacks(t *testing.T) {
|
|
stored := map[string]string{
|
|
"s3.operational_endpoint": "https://s3.example.invalid",
|
|
"s3.operational_public_endpoint": "https://cdn.example.invalid",
|
|
"s3.operational_region": "us-test-1",
|
|
"s3.operational_path_style": "false",
|
|
"s3.operational_bucket": "legacy-bucket",
|
|
"s3.operational_key_prefix": "legacy-prefix",
|
|
"s3.operational_access_key": "legacy-access",
|
|
"s3.operational_secret_key": "legacy-secret",
|
|
"s3.operational_url_auth": "presigned",
|
|
"s3.operational_token_secret": "legacy-token",
|
|
"s3.operational_token_param": "signature",
|
|
"s3.operational_token_ttl": "3600",
|
|
"subtitle_ai.base_url": "https://legacy-ai.example.invalid",
|
|
"subtitle_ai.api_key": "legacy-ai-key",
|
|
"subtitle_ai.chat_model": "legacy-chat-model",
|
|
"subtitle_ai.max_concurrent_jobs": "7",
|
|
"ai.max_concurrent_jobs": "0",
|
|
}
|
|
|
|
effective := EffectiveAdminSettings(stored)
|
|
expected := map[string]string{
|
|
"s3.public_endpoint": stored["s3.operational_endpoint"],
|
|
"s3.public_read_endpoint": stored["s3.operational_public_endpoint"],
|
|
"s3.public_region": stored["s3.operational_region"],
|
|
"s3.public_path_style": stored["s3.operational_path_style"],
|
|
"s3.public_bucket": stored["s3.operational_bucket"],
|
|
"s3.public_key_prefix": stored["s3.operational_key_prefix"],
|
|
"s3.public_access_key": stored["s3.operational_access_key"],
|
|
"s3.public_secret_key": stored["s3.operational_secret_key"],
|
|
"s3.public_url_auth": stored["s3.operational_url_auth"],
|
|
"s3.public_token_secret": stored["s3.operational_token_secret"],
|
|
"s3.public_token_param": stored["s3.operational_token_param"],
|
|
"s3.public_token_ttl": stored["s3.operational_token_ttl"],
|
|
"s3.private_endpoint": stored["s3.operational_endpoint"],
|
|
"s3.private_region": stored["s3.operational_region"],
|
|
"s3.private_path_style": stored["s3.operational_path_style"],
|
|
"s3.private_bucket": stored["s3.operational_bucket"],
|
|
"s3.private_key_prefix": stored["s3.operational_key_prefix"],
|
|
"s3.private_access_key": stored["s3.operational_access_key"],
|
|
"s3.private_secret_key": stored["s3.operational_secret_key"],
|
|
"ai.base_url": stored["subtitle_ai.base_url"],
|
|
"ai.api_key": stored["subtitle_ai.api_key"],
|
|
"ai.chat_model": stored["subtitle_ai.chat_model"],
|
|
"ai.max_concurrent_jobs": stored["subtitle_ai.max_concurrent_jobs"],
|
|
}
|
|
for key, want := range expected {
|
|
if got := effective[key]; got != want {
|
|
t.Errorf("%s = %q, want legacy fallback %q", key, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEffectiveAdminSettingsUsesRuntimeDefaultForNonpositiveCanonicalAIConcurrency(t *testing.T) {
|
|
for _, canonical := range []string{"0", "-1"} {
|
|
t.Run(canonical, func(t *testing.T) {
|
|
effective := EffectiveAdminSettings(map[string]string{
|
|
"ai.max_concurrent_jobs": canonical,
|
|
})
|
|
if got := effective["ai.max_concurrent_jobs"]; got != "2" {
|
|
t.Fatalf("ai.max_concurrent_jobs = %q, want runtime default 2", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAdminSettingDefaultsAlignWithConfigRuntimeDefaults(t *testing.T) {
|
|
baseline, err := LoadFromDB(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
normalizeEffectiveRuntimeDefaults(baseline)
|
|
|
|
for key, value := range adminSettingDefaults {
|
|
t.Run(key, func(t *testing.T) {
|
|
withExplicitDefault, err := LoadFromDB(map[string]string{key: value})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
normalizeEffectiveRuntimeDefaults(withExplicitDefault)
|
|
if !reflect.DeepEqual(withExplicitDefault, baseline) {
|
|
t.Fatalf("admin default %q does not match the runtime default", value)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestChapterThumbnailSoftwareToneMapDefaultsDisabled(t *testing.T) {
|
|
effective := EffectiveAdminSettings(nil)
|
|
if got := effective[chapterThumbnailSoftwareToneMapKey]; got != "false" {
|
|
t.Fatalf("software tone-map default = %q, want false", got)
|
|
}
|
|
}
|
|
|
|
func normalizeEffectiveRuntimeDefaults(cfg *Config) {
|
|
if cfg.S3.Public.URLAuth == "" {
|
|
cfg.S3.Public.URLAuth = "presigned"
|
|
}
|
|
if cfg.S3.Public.TokenParam == "" {
|
|
cfg.S3.Public.TokenParam = "verify"
|
|
}
|
|
if cfg.S3.Public.TokenTTL <= 0 {
|
|
cfg.S3.Public.TokenTTL = 10800
|
|
}
|
|
// The client IP loader treats an empty value as its built-in private-range
|
|
// default. Normalize formatting as well so equivalent CIDR lists compare.
|
|
cfg.ClientIP.TrustedProxies = strings.ReplaceAll(
|
|
firstConfiguredString(
|
|
map[string]string{"trusted": cfg.ClientIP.TrustedProxies},
|
|
adminSettingDefaults["clientip.trusted_proxies"],
|
|
"trusted",
|
|
),
|
|
" ",
|
|
"",
|
|
)
|
|
}
|
|
|
|
func TestNormalizeAdminSettingRejectsInvalidValues(t *testing.T) {
|
|
tests := []struct {
|
|
key string
|
|
value string
|
|
}{
|
|
{key: "database.max_connections", value: "0"},
|
|
{key: "metadata.cache_images", value: "maybe"},
|
|
{key: chapterThumbnailSoftwareToneMapKey, value: "maybe"},
|
|
{key: "auth.access_token_expiry", value: "forever"},
|
|
{key: "recommendations.embeddings_cron", value: "not a cron"},
|
|
{key: "notifications.server_channels.batch_seconds", value: "119"},
|
|
{key: "catalog.search.meilisearch.semantic_ratio", value: "1.2"},
|
|
{key: "email.smtp_port", value: "70000"},
|
|
{key: "theme.catalog_url", value: "http://raw.githubusercontent.com/Silo-Server/silo-themes/main/catalog.json"},
|
|
{key: "theme.catalog_url", value: "https://example.com/catalog.json"},
|
|
{key: "redis.url", value: "not-a-url"},
|
|
{key: "playback.segment_retention_seconds", value: "119"},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.key, func(t *testing.T) {
|
|
if _, err := NormalizeAdminSetting(tc.key, tc.value); err == nil {
|
|
t.Fatalf("NormalizeAdminSetting(%q, %q) returned nil error", tc.key, tc.value)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormalizeAdminSettingAcceptsSegmentRetentionBounds(t *testing.T) {
|
|
for _, value := range []string{"0", "120", "86400"} {
|
|
t.Run(value, func(t *testing.T) {
|
|
got, err := NormalizeAdminSetting("playback.segment_retention_seconds", value)
|
|
if err != nil {
|
|
t.Fatalf("NormalizeAdminSetting: %v", err)
|
|
}
|
|
if got != value {
|
|
t.Fatalf("normalized retention = %q, want %q", got, value)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormalizeAdminSettingAcceptsApprovedThemeCatalogURL(t *testing.T) {
|
|
got, err := NormalizeAdminSetting(
|
|
"theme.catalog_url",
|
|
"https://raw.githubusercontent.com/Silo-Server/silo-themes/main/catalog.json/",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != "https://raw.githubusercontent.com/Silo-Server/silo-themes/main/catalog.json" {
|
|
t.Fatalf("normalized URL = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestValidateAdminSettingsChecksProspectiveRelationships(t *testing.T) {
|
|
values := map[string]string{
|
|
"auth.access_token_expiry": "48h",
|
|
"auth.refresh_token_expiry": "24h",
|
|
"playback.watched_threshold": "90",
|
|
"playback.min_resume_threshold": "5",
|
|
}
|
|
if err := ValidateAdminSettings(values); err == nil {
|
|
t.Fatal("ValidateAdminSettings() returned nil for refresh shorter than access")
|
|
}
|
|
|
|
values["auth.refresh_token_expiry"] = "72h"
|
|
values["playback.min_resume_threshold"] = "95"
|
|
if err := ValidateAdminSettings(values); err == nil {
|
|
t.Fatal("ValidateAdminSettings() returned nil for resume threshold above watched")
|
|
}
|
|
}
|
|
|
|
func TestValidateAdminSettingsRequiresDurableRedisTransport(t *testing.T) {
|
|
values := map[string]string{"ratelimit.backend": "redis"}
|
|
if err := ValidateAdminSettings(values); err == nil {
|
|
t.Fatal("ValidateAdminSettings() accepted Redis backend without a durable transport")
|
|
}
|
|
|
|
if err := ValidateAdminSettingsWithCapabilities(values, AdminSettingsCapabilities{
|
|
RedisBootstrapAvailable: true,
|
|
}); err != nil {
|
|
t.Fatalf("bootstrap Sentinel transport was rejected: %v", err)
|
|
}
|
|
|
|
values["redis.url"] = "redis://cache.example.invalid:6379"
|
|
if err := ValidateAdminSettings(values); err != nil {
|
|
t.Fatalf("persisted Redis URL was rejected: %v", err)
|
|
}
|
|
|
|
values["redis.url"] = "not-a-url"
|
|
if err := ValidateAdminSettings(values); err == nil {
|
|
t.Fatal("ValidateAdminSettings() accepted a malformed Redis URL")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeAdminSettingCanonicalizesRedisURL(t *testing.T) {
|
|
got, err := NormalizeAdminSetting("redis.url", " rediss://cache.example.invalid:6380/2 ")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != "rediss://cache.example.invalid:6380/2" {
|
|
t.Fatalf("normalized Redis URL = %q", got)
|
|
}
|
|
}
|