package config import ( "testing" "time" ) func TestLoadFileManagerUploadPolicyConfigFromEnv_Defaults(t *testing.T) { t.Setenv("FILE_MANAGER_UPLOAD_MAX_FILE_SIZE_BYTES", "") t.Setenv("FILE_MANAGER_UPLOAD_ALLOWED_MIME_TYPES", "") t.Setenv("FILE_MANAGER_UPLOAD_ALLOWED_EXTENSIONS", "") t.Setenv("FILE_MANAGER_UPLOAD_ALLOW_EMPTY_FILE", "") t.Setenv("FILE_MANAGER_UPLOAD_MAX_FILENAME_LENGTH", "") cfg := LoadFileManagerUploadPolicyConfigFromEnv() if cfg.MaxFileSizeBytes != 4*1024*1024 { t.Fatalf("expected default max file size 4194304, got %d", cfg.MaxFileSizeBytes) } if cfg.AllowEmptyFile { t.Fatalf("expected default allow empty file false") } if cfg.MaxFilenameLength != 255 { t.Fatalf("expected default max filename length 255, got %d", cfg.MaxFilenameLength) } if len(cfg.AllowedMimeTypes) != 0 { t.Fatalf("expected empty default allowed mime types, got %v", cfg.AllowedMimeTypes) } if len(cfg.AllowedExtensions) != 0 { t.Fatalf("expected empty default allowed extensions, got %v", cfg.AllowedExtensions) } } func TestLoadFileManagerUploadPolicyConfigFromEnv_Normalize(t *testing.T) { t.Setenv("FILE_MANAGER_UPLOAD_MAX_FILE_SIZE_BYTES", "1048576") t.Setenv("FILE_MANAGER_UPLOAD_ALLOWED_MIME_TYPES", " image/PNG ; charset=utf-8,application/pdf,IMAGE/PNG") t.Setenv("FILE_MANAGER_UPLOAD_ALLOWED_EXTENSIONS", " .PDF, png ,PNG") t.Setenv("FILE_MANAGER_UPLOAD_ALLOW_EMPTY_FILE", "true") t.Setenv("FILE_MANAGER_UPLOAD_MAX_FILENAME_LENGTH", "120") cfg := LoadFileManagerUploadPolicyConfigFromEnv() if cfg.MaxFileSizeBytes != 1048576 { t.Fatalf("expected max file size 1048576, got %d", cfg.MaxFileSizeBytes) } if !cfg.AllowEmptyFile { t.Fatalf("expected allow empty file true") } if cfg.MaxFilenameLength != 120 { t.Fatalf("expected max filename length 120, got %d", cfg.MaxFilenameLength) } if len(cfg.AllowedMimeTypes) != 2 || cfg.AllowedMimeTypes[0] != "image/png" || cfg.AllowedMimeTypes[1] != "application/pdf" { t.Fatalf("unexpected normalized mime types: %v", cfg.AllowedMimeTypes) } if len(cfg.AllowedExtensions) != 2 || cfg.AllowedExtensions[0] != "pdf" || cfg.AllowedExtensions[1] != "png" { t.Fatalf("unexpected normalized extensions: %v", cfg.AllowedExtensions) } } func TestLoadFileManagerTrashPolicyConfigFromEnv_Defaults(t *testing.T) { t.Setenv("FILE_MANAGER_TRASH_AUTO_PURGE_ENABLED", "") t.Setenv("FILE_MANAGER_TRASH_RETENTION", "") t.Setenv("FILE_MANAGER_TRASH_AUTO_PURGE_INTERVAL", "") t.Setenv("FILE_MANAGER_TRASH_AUTO_PURGE_BATCH_SIZE", "") cfg := LoadFileManagerTrashPolicyConfigFromEnv() if !cfg.AutoPurgeEnabled { t.Fatalf("expected default auto purge enabled") } if cfg.Retention != 30*24*time.Hour { t.Fatalf("expected default retention 720h, got %s", cfg.Retention) } if cfg.AutoPurgeInterval != time.Hour { t.Fatalf("expected default auto purge interval 1h, got %s", cfg.AutoPurgeInterval) } if cfg.AutoPurgeBatchSize != 200 { t.Fatalf("expected default batch size 200, got %d", cfg.AutoPurgeBatchSize) } } func TestLoadFileManagerTrashPolicyConfigFromEnv_Custom(t *testing.T) { t.Setenv("FILE_MANAGER_TRASH_AUTO_PURGE_ENABLED", "false") t.Setenv("FILE_MANAGER_TRASH_RETENTION", "168h") t.Setenv("FILE_MANAGER_TRASH_AUTO_PURGE_INTERVAL", "15m") t.Setenv("FILE_MANAGER_TRASH_AUTO_PURGE_BATCH_SIZE", "50") cfg := LoadFileManagerTrashPolicyConfigFromEnv() if cfg.AutoPurgeEnabled { t.Fatalf("expected auto purge disabled") } if cfg.Retention != 168*time.Hour { t.Fatalf("expected retention 168h, got %s", cfg.Retention) } if cfg.AutoPurgeInterval != 15*time.Minute { t.Fatalf("expected auto purge interval 15m, got %s", cfg.AutoPurgeInterval) } if cfg.AutoPurgeBatchSize != 50 { t.Fatalf("expected batch size 50, got %d", cfg.AutoPurgeBatchSize) } } func TestLoadFileManagerLifecycleConfigFromEnv_Defaults(t *testing.T) { t.Setenv("ENABLE_S3_TAGGING", "") t.Setenv("ENABLE_FILE_CLEANUP_JOB", "") t.Setenv("TEMP_UPLOAD_GRACE_HOURS", "") t.Setenv("ORPHAN_GRACE_PERIOD_DAYS", "") t.Setenv("FILE_CLEANUP_BATCH_SIZE", "") t.Setenv("FILE_CLEANUP_DRY_RUN", "") cfg := LoadFileManagerLifecycleConfigFromEnv() if !cfg.EnableS3Tagging { t.Fatalf("expected s3 tagging enabled by default") } if cfg.EnableCleanupJob { t.Fatalf("expected cleanup job disabled by default") } if cfg.TempUploadGraceHours != 2 { t.Fatalf("expected temp upload grace 2 hours, got %d", cfg.TempUploadGraceHours) } if cfg.OrphanGraceDays != 30 { t.Fatalf("expected orphan grace 30 days, got %d", cfg.OrphanGraceDays) } if cfg.CleanupBatchSize != 100 { t.Fatalf("expected cleanup batch size 100, got %d", cfg.CleanupBatchSize) } if !cfg.CleanupDryRun { t.Fatalf("expected cleanup dry run enabled by default") } } func TestLoadFileManagerLifecycleConfigFromEnv_Custom(t *testing.T) { t.Setenv("ENABLE_S3_TAGGING", "false") t.Setenv("ENABLE_FILE_CLEANUP_JOB", "true") t.Setenv("TEMP_UPLOAD_GRACE_HOURS", "6") t.Setenv("ORPHAN_GRACE_PERIOD_DAYS", "45") t.Setenv("FILE_CLEANUP_BATCH_SIZE", "250") t.Setenv("FILE_CLEANUP_DRY_RUN", "false") cfg := LoadFileManagerLifecycleConfigFromEnv() if cfg.EnableS3Tagging { t.Fatalf("expected s3 tagging disabled") } if !cfg.EnableCleanupJob { t.Fatalf("expected cleanup job enabled") } if cfg.TempUploadGraceHours != 6 { t.Fatalf("expected temp upload grace 6 hours, got %d", cfg.TempUploadGraceHours) } if cfg.OrphanGraceDays != 45 { t.Fatalf("expected orphan grace 45 days, got %d", cfg.OrphanGraceDays) } if cfg.CleanupBatchSize != 250 { t.Fatalf("expected cleanup batch size 250, got %d", cfg.CleanupBatchSize) } if cfg.CleanupDryRun { t.Fatalf("expected cleanup dry run disabled") } } func TestLoadFileQueueConfigFromEnv_Defaults(t *testing.T) { t.Setenv("FILE_QUEUE_MESSAGE_SCHEMA_VERSION", "") t.Setenv("FILE_QUEUE_IDEMPOTENCY_KEY_PREFIX", "") t.Setenv("FILE_QUEUE_WORKER_MONITOR_ADDR", "") t.Setenv("FILE_QUEUE_OUTBOX_ENABLED", "") t.Setenv("FILE_QUEUE_OUTBOX_POLL_INTERVAL", "") t.Setenv("FILE_QUEUE_OUTBOX_BATCH_SIZE", "") t.Setenv("FILE_QUEUE_OUTBOX_DISPATCH_TIMEOUT", "") t.Setenv("FILE_QUEUE_OUTBOX_LOCK_TTL", "") t.Setenv("FILE_QUEUE_OUTBOX_MAX_ATTEMPTS", "") cfg := LoadFileQueueConfigFromEnv() if cfg.MessageSchemaVersion != "v1" { t.Fatalf("expected default schema version v1, got %q", cfg.MessageSchemaVersion) } if cfg.IdempotencyKeyPrefix != "queue:idempotency:file:" { t.Fatalf("unexpected default idempotency prefix: %q", cfg.IdempotencyKeyPrefix) } if cfg.WorkerMonitorAddr != "127.0.0.1:9091" { t.Fatalf("unexpected default monitor addr: %q", cfg.WorkerMonitorAddr) } if !cfg.OutboxEnabled { t.Fatalf("expected default file outbox enabled") } if cfg.OutboxPollInterval != time.Second { t.Fatalf("expected default outbox poll interval 1s, got %s", cfg.OutboxPollInterval) } if cfg.OutboxBatchSize != 10 { t.Fatalf("expected default outbox batch size 10, got %d", cfg.OutboxBatchSize) } if cfg.OutboxDispatchTimeout != 10*time.Second { t.Fatalf("expected default outbox dispatch timeout 10s, got %s", cfg.OutboxDispatchTimeout) } if cfg.OutboxLockTTL != time.Minute { t.Fatalf("expected default outbox lock ttl 1m, got %s", cfg.OutboxLockTTL) } if cfg.OutboxMaxAttempts != 20 { t.Fatalf("expected default outbox max attempts 20, got %d", cfg.OutboxMaxAttempts) } } func TestLoadFileSQSConfigFromEnv(t *testing.T) { t.Setenv("AWS_REGION", "ap-southeast-1") t.Setenv("SQS_ENDPOINT", "http://localhost:4566") t.Setenv("SQS_ALLOW_INSECURE_ENDPOINT", "true") t.Setenv("FILE_SQS_REGION", "") t.Setenv("FILE_SQS_ENDPOINT", "") t.Setenv("FILE_SQS_QUEUE_URL", "http://localhost:4566/000000000000/app-file-queue") t.Setenv("FILE_SQS_QUEUE_TYPE", "standard") t.Setenv("FILE_SQS_MESSAGE_GROUP_ID", "file") t.Setenv("FILE_SQS_MAX_MESSAGES", "8") t.Setenv("FILE_SQS_WAIT_TIME_SECONDS", "15") t.Setenv("FILE_SQS_VISIBILITY_TIMEOUT", "90s") t.Setenv("FILE_SQS_ALLOW_INSECURE_ENDPOINT", "") cfg := LoadFileSQSConfigFromEnv() if cfg.Region != "ap-southeast-1" { t.Fatalf("unexpected region: %q", cfg.Region) } if cfg.Endpoint != "http://localhost:4566" { t.Fatalf("unexpected endpoint: %q", cfg.Endpoint) } if cfg.QueueURL == "" { t.Fatalf("expected queue url") } if cfg.MaxMessages != 8 { t.Fatalf("unexpected max messages: %d", cfg.MaxMessages) } if cfg.WaitTimeSeconds != 15 { t.Fatalf("unexpected wait time seconds: %d", cfg.WaitTimeSeconds) } if !cfg.AllowInsecureEndpoint { t.Fatalf("expected insecure endpoint allowed from fallback") } } func TestValidateDistinctQueueURLs(t *testing.T) { t.Run("different queue urls", func(t *testing.T) { err := ValidateDistinctQueueURLs( SQSConfig{QueueURL: "http://localhost:4566/000000000000/app-queue"}, SQSConfig{QueueURL: "http://localhost:4566/000000000000/app-file-queue"}, ) if err != nil { t.Fatalf("expected no error, got %v", err) } }) t.Run("same queue urls with different case and slash", func(t *testing.T) { err := ValidateDistinctQueueURLs( SQSConfig{QueueURL: "HTTP://LOCALHOST:4566/000000000000/app-queue"}, SQSConfig{QueueURL: "http://localhost:4566/000000000000/app-queue/"}, ) if err == nil { t.Fatalf("expected error for same queue urls") } }) t.Run("skip when one queue url empty", func(t *testing.T) { err := ValidateDistinctQueueURLs( SQSConfig{QueueURL: "http://localhost:4566/000000000000/app-queue"}, SQSConfig{QueueURL: ""}, ) if err != nil { t.Fatalf("expected no error when file queue url is empty, got %v", err) } }) } func TestParseHTTPTimeoutOverrides(t *testing.T) { t.Run("invalid format ignored", func(t *testing.T) { parsed := parseHTTPTimeoutOverrides("GET /users/:id 100ms,foo") if len(parsed) != 0 { t.Fatalf("expected no parsed entries, got %v", parsed) } }) t.Run("invalid duration ignored", func(t *testing.T) { parsed := parseHTTPTimeoutOverrides("GET /users/:id=not-a-duration") if len(parsed) != 0 { t.Fatalf("expected no parsed entries, got %v", parsed) } }) t.Run("zero and negative duration ignored", func(t *testing.T) { parsed := parseHTTPTimeoutOverrides("GET /users/:id=0s,POST /users=-1s") if len(parsed) != 0 { t.Fatalf("expected no parsed entries, got %v", parsed) } }) t.Run("valid entries parsed", func(t *testing.T) { parsed := parseHTTPTimeoutOverrides("GET /users/:id=120ms, POST /auth/login = 2s") if len(parsed) != 2 { t.Fatalf("expected 2 parsed entries, got %d", len(parsed)) } if parsed["GET /users/:id"] != 120*time.Millisecond { t.Fatalf("unexpected GET /users/:id timeout: %s", parsed["GET /users/:id"]) } if parsed["POST /auth/login"] != 2*time.Second { t.Fatalf("unexpected POST /auth/login timeout: %s", parsed["POST /auth/login"]) } }) } func TestParseHTTPEndpointRateLimitOverrides(t *testing.T) { t.Run("invalid entries ignored", func(t *testing.T) { parsed := parseHTTPEndpointRateLimitOverrides("POST /auth/login=abc,1m;foo;GET /x=1") if len(parsed) != 0 { t.Fatalf("expected no parsed entries, got %v", parsed) } }) t.Run("zero or negative values ignored", func(t *testing.T) { parsed := parseHTTPEndpointRateLimitOverrides("POST /auth/login=0,1m;POST /auth/totp/verify=5,0s") if len(parsed) != 0 { t.Fatalf("expected no parsed entries, got %v", parsed) } }) t.Run("valid entries parsed", func(t *testing.T) { parsed := parseHTTPEndpointRateLimitOverrides("POST /auth/login=5,1m; POST /auth/totp/verify = 3,30s") if len(parsed) != 2 { t.Fatalf("expected 2 parsed entries, got %d", len(parsed)) } login, ok := parsed["POST /auth/login"] if !ok { t.Fatalf("expected POST /auth/login to be parsed") } if login.Max != 5 || login.Window != time.Minute { t.Fatalf("unexpected login rule: %+v", login) } totp, ok := parsed["POST /auth/totp/verify"] if !ok { t.Fatalf("expected POST /auth/totp/verify to be parsed") } if totp.Max != 3 || totp.Window != 30*time.Second { t.Fatalf("unexpected totp rule: %+v", totp) } }) }