223 lines
7.8 KiB
Go
223 lines
7.8 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.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)
|
|
}
|
|
})
|
|
}
|
|
}
|