Files
vctp2/internal/settings/settings_strict_test.go
T
Nathan Coad 35840697fa
continuous-integration/drone/push Build is passing
improve ldap
2026-04-21 14:40:10 +10:00

226 lines
7.9 KiB
Go

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.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)
}
})
}
}