package settings import ( "io" "log/slog" "os" "path/filepath" "strings" "testing" ) func TestReadYMLSettingsRejectsUnknownField(t *testing.T) { tmpDir := t.TempDir() settingsPath := filepath.Join(tmpDir, "vctp.yml") content := `settings: log_level: "info" unknown_field: true ` if err := os.WriteFile(settingsPath, []byte(content), 0o600); err != nil { t.Fatalf("failed to write settings file: %v", err) } logger := slog.New(slog.NewTextHandler(io.Discard, nil)) s := New(logger, settingsPath) err := s.ReadYMLSettings() if err == nil { t.Fatal("expected unknown field decode error") } if !strings.Contains(strings.ToLower(err.Error()), "unknown_field") { t.Fatalf("expected error to mention unknown field, got: %v", err) } } func TestReadYMLSettingsAppliesAuthDefaults(t *testing.T) { tmpDir := t.TempDir() settingsPath := filepath.Join(tmpDir, "vctp.yml") content := `settings: log_level: "info" ` if err := os.WriteFile(settingsPath, []byte(content), 0o600); err != nil { t.Fatalf("failed to write settings file: %v", err) } logger := slog.New(slog.NewTextHandler(io.Discard, nil)) s := New(logger, settingsPath) if err := s.ReadYMLSettings(); err != nil { t.Fatalf("expected settings to load, got error: %v", err) } got := s.Values.Settings if got.AuthMode != authModeDisabled { t.Fatalf("expected default auth_mode=%q, got %q", authModeDisabled, got.AuthMode) } if got.AuthTokenLifespanMinutes != defaultAuthTokenLifespanMinutes { t.Fatalf("expected default auth_token_lifespan_minutes=%d, got %d", defaultAuthTokenLifespanMinutes, got.AuthTokenLifespanMinutes) } if got.AuthJWTIssuer != defaultAuthJWTIssuer { t.Fatalf("expected default auth_jwt_issuer=%q, got %q", defaultAuthJWTIssuer, got.AuthJWTIssuer) } if got.AuthJWTAudience != defaultAuthJWTAudience { t.Fatalf("expected default auth_jwt_audience=%q, got %q", defaultAuthJWTAudience, got.AuthJWTAudience) } if got.AuthClockSkewSeconds != defaultAuthClockSkewSeconds { t.Fatalf("expected default auth_clock_skew_seconds=%d, got %d", defaultAuthClockSkewSeconds, got.AuthClockSkewSeconds) } if got.CaptureWriteBatchSize != 1000 { t.Fatalf("expected default capture_write_batch_size=1000, got %d", got.CaptureWriteBatchSize) } if got.SnapshotTableCompatMode == nil || !*got.SnapshotTableCompatMode { t.Fatalf("expected default snapshot_table_compat_mode=true, got %#v", got.SnapshotTableCompatMode) } if got.AsyncReportGeneration == nil || !*got.AsyncReportGeneration { t.Fatalf("expected default async_report_generation=true, got %#v", got.AsyncReportGeneration) } if got.PostgresVmHourlyPartitioning == nil || *got.PostgresVmHourlyPartitioning { t.Fatalf("expected default postgres_vm_hourly_partitioning_enabled=false, got %#v", got.PostgresVmHourlyPartitioning) } if got.ScheduledAggregationEngine != scheduledAggregationEngineGo { t.Fatalf("expected default scheduled_aggregation_engine=%q, got %q", scheduledAggregationEngineGo, got.ScheduledAggregationEngine) } if got.MonthlyAggregationGranularity != "daily" { t.Fatalf("expected default monthly_aggregation_granularity=daily, got %q", got.MonthlyAggregationGranularity) } } func TestReadYMLSettingsRejectsInvalidScheduledAggregationEngine(t *testing.T) { tmpDir := t.TempDir() settingsPath := filepath.Join(tmpDir, "vctp.yml") content := `settings: scheduled_aggregation_engine: "hybrid" ` if err := os.WriteFile(settingsPath, []byte(content), 0o600); err != nil { t.Fatalf("failed to write settings file: %v", err) } logger := slog.New(slog.NewTextHandler(io.Discard, nil)) s := New(logger, settingsPath) err := s.ReadYMLSettings() if err == nil { t.Fatal("expected invalid scheduled_aggregation_engine to fail") } if !strings.Contains(strings.ToLower(err.Error()), "scheduled_aggregation_engine") { t.Fatalf("expected error to mention scheduled_aggregation_engine, got: %v", err) } } func TestReadYMLSettingsRejectsInvalidAuthMode(t *testing.T) { tmpDir := t.TempDir() settingsPath := filepath.Join(tmpDir, "vctp.yml") content := `settings: auth_mode: "sometimes" ` if err := os.WriteFile(settingsPath, []byte(content), 0o600); err != nil { t.Fatalf("failed to write settings file: %v", err) } logger := slog.New(slog.NewTextHandler(io.Discard, nil)) s := New(logger, settingsPath) err := s.ReadYMLSettings() if err == nil { t.Fatal("expected invalid auth_mode to fail") } if !strings.Contains(strings.ToLower(err.Error()), "auth_mode") { t.Fatalf("expected error to mention auth_mode, got: %v", err) } } func TestReadYMLSettingsRejectsAuthEnabledWithoutSigningKey(t *testing.T) { tmpDir := t.TempDir() settingsPath := filepath.Join(tmpDir, "vctp.yml") content := `settings: auth_enabled: true auth_mode: "required" ldap_bind_address: "ldaps://ldap.example.com:636" ldap_base_dn: "dc=example,dc=com" auth_group_role_mappings: "cn=vctp-admin,ou=groups,dc=example,dc=com": "admin" ` if err := os.WriteFile(settingsPath, []byte(content), 0o600); err != nil { t.Fatalf("failed to write settings file: %v", err) } logger := slog.New(slog.NewTextHandler(io.Discard, nil)) s := New(logger, settingsPath) err := s.ReadYMLSettings() if err == nil { t.Fatal("expected auth_enabled=true without signing key to fail") } if !strings.Contains(strings.ToLower(err.Error()), "auth_jwt_signing_key") { t.Fatalf("expected error to mention auth_jwt_signing_key, got: %v", err) } } func TestReadYMLSettingsAcceptsValidAuthConfigAndNormalizesMappings(t *testing.T) { tmpDir := t.TempDir() settingsPath := filepath.Join(tmpDir, "vctp.yml") content := `settings: auth_enabled: true auth_mode: "REQUIRED" auth_jwt_signing_key: "c2VjcmV0" auth_token_lifespan_minutes: 90 auth_jwt_issuer: " custom-issuer " auth_jwt_audience: " custom-audience " auth_clock_skew_seconds: 15 ldap_bind_address: "ldaps://ldap.example.com:636" ldap_base_dn: "dc=example,dc=com" ldap_groups: - " cn=vctp-viewers,ou=groups,dc=example,dc=com " auth_group_role_mappings: " cn=vctp-admins,ou=groups,dc=example,dc=com ": " ADMIN " "cn=vctp-viewers,ou=groups,dc=example,dc=com": "viewer" ` if err := os.WriteFile(settingsPath, []byte(content), 0o600); err != nil { t.Fatalf("failed to write settings file: %v", err) } logger := slog.New(slog.NewTextHandler(io.Discard, nil)) s := New(logger, settingsPath) if err := s.ReadYMLSettings(); err != nil { t.Fatalf("expected valid auth config, got error: %v", err) } got := s.Values.Settings if got.AuthMode != authModeRequired { t.Fatalf("expected normalized auth_mode=%q, got %q", authModeRequired, got.AuthMode) } if got.AuthJWTIssuer != "custom-issuer" { t.Fatalf("expected trimmed auth_jwt_issuer, got %q", got.AuthJWTIssuer) } if got.AuthJWTAudience != "custom-audience" { t.Fatalf("expected trimmed auth_jwt_audience, got %q", got.AuthJWTAudience) } if len(got.LDAPGroups) != 1 || got.LDAPGroups[0] != "cn=vctp-viewers,ou=groups,dc=example,dc=com" { t.Fatalf("expected ldap_groups to be compacted+trimmed, got %#v", got.LDAPGroups) } if got.LDAPUserBaseDN != "dc=example,dc=com" { t.Fatalf("expected default ldap_user_base_dn to fall back to ldap_base_dn, got %q", got.LDAPUserBaseDN) } if got.LDAPGroupBaseDN != "dc=example,dc=com" { t.Fatalf("expected default ldap_group_base_dn to fall back to ldap_base_dn, got %q", got.LDAPGroupBaseDN) } if got.AuthGroupRoleMappings["cn=vctp-admins,ou=groups,dc=example,dc=com"] != authRoleAdmin { t.Fatalf("expected admin mapping to normalize role to %q, got %#v", authRoleAdmin, got.AuthGroupRoleMappings) } } func TestSecureSettingsFileMode(t *testing.T) { cases := []struct { name string in os.FileMode want os.FileMode }{ {name: "already strict", in: 0o600, want: 0o600}, {name: "group read allowed", in: 0o640, want: 0o640}, {name: "too open world", in: 0o666, want: 0o660}, {name: "exec bits stripped", in: 0o755, want: 0o640}, {name: "no perms gets owner rw", in: 0o000, want: 0o600}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := secureSettingsFileMode(tc.in) if got != tc.want { t.Fatalf("unexpected mode conversion: in=%#o got=%#o want=%#o", tc.in, got, tc.want) } }) } }