Pulse/internal/config/sso_test.go
2026-04-01 12:00:31 +01:00

765 lines
17 KiB
Go

package config
import (
"testing"
)
func TestNewSSOConfig(t *testing.T) {
cfg := NewSSOConfig()
if cfg == nil {
t.Fatal("NewSSOConfig returned nil")
}
if cfg.Providers == nil {
t.Error("Providers slice should not be nil")
}
if len(cfg.Providers) != 0 {
t.Error("Providers slice should be empty")
}
if !cfg.AllowMultipleProviders {
t.Error("AllowMultipleProviders should default to true")
}
}
func TestSSOConfig_GetProvider(t *testing.T) {
cfg := &SSOConfig{
Providers: []SSOProvider{
{ID: "p1", Name: "Provider 1"},
{ID: "p2", Name: "Provider 2"},
},
}
tests := []struct {
name string
id string
wantName string
wantNil bool
}{
{"existing provider", "p1", "Provider 1", false},
{"second provider", "p2", "Provider 2", false},
{"non-existent", "p3", "", true},
{"empty id", "", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := cfg.GetProvider(tt.id)
if tt.wantNil {
if p != nil {
t.Errorf("GetProvider(%q) = %v, want nil", tt.id, p)
}
} else {
if p == nil {
t.Fatalf("GetProvider(%q) = nil, want non-nil", tt.id)
}
if p.Name != tt.wantName {
t.Errorf("GetProvider(%q).Name = %q, want %q", tt.id, p.Name, tt.wantName)
}
}
})
}
// Test nil config
var nilCfg *SSOConfig
if p := nilCfg.GetProvider("p1"); p != nil {
t.Error("GetProvider on nil config should return nil")
}
}
func TestSSOConfig_GetEnabledProviders(t *testing.T) {
cfg := &SSOConfig{
Providers: []SSOProvider{
{ID: "p1", Name: "Provider 1", Enabled: false, Priority: 10},
{ID: "p2", Name: "Provider 2", Enabled: true, Priority: 20},
{ID: "p3", Name: "Provider 3", Enabled: true, Priority: 5},
{ID: "p4", Name: "Provider 4", Enabled: true, Priority: 15},
},
}
enabled := cfg.GetEnabledProviders()
if len(enabled) != 3 {
t.Fatalf("GetEnabledProviders() returned %d providers, want 3", len(enabled))
}
// Should be sorted by priority (ascending)
expectedOrder := []string{"p3", "p4", "p2"}
for i, p := range enabled {
if p.ID != expectedOrder[i] {
t.Errorf("GetEnabledProviders()[%d].ID = %q, want %q", i, p.ID, expectedOrder[i])
}
}
// Test nil config
var nilCfg *SSOConfig
if result := nilCfg.GetEnabledProviders(); result != nil {
t.Error("GetEnabledProviders on nil config should return nil")
}
}
func TestSSOConfig_GetDefaultProvider(t *testing.T) {
tests := []struct {
name string
cfg *SSOConfig
wantID string
wantNil bool
}{
{
name: "explicit default",
cfg: &SSOConfig{
DefaultProviderID: "p2",
Providers: []SSOProvider{
{ID: "p1", Enabled: true, Priority: 1},
{ID: "p2", Enabled: true, Priority: 2},
},
},
wantID: "p2",
wantNil: false,
},
{
name: "first enabled when no default",
cfg: &SSOConfig{
Providers: []SSOProvider{
{ID: "p1", Enabled: false, Priority: 1},
{ID: "p2", Enabled: true, Priority: 10},
{ID: "p3", Enabled: true, Priority: 5},
},
},
wantID: "p3", // lowest priority among enabled
wantNil: false,
},
{
name: "no enabled providers",
cfg: &SSOConfig{
Providers: []SSOProvider{
{ID: "p1", Enabled: false},
},
},
wantNil: true,
},
{
name: "nil config",
cfg: nil,
wantNil: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := tt.cfg.GetDefaultProvider()
if tt.wantNil {
if p != nil {
t.Errorf("GetDefaultProvider() = %v, want nil", p)
}
} else {
if p == nil {
t.Fatal("GetDefaultProvider() = nil, want non-nil")
}
if p.ID != tt.wantID {
t.Errorf("GetDefaultProvider().ID = %q, want %q", p.ID, tt.wantID)
}
}
})
}
}
func TestSSOConfig_HasEnabledProviders(t *testing.T) {
tests := []struct {
name string
cfg *SSOConfig
want bool
}{
{
name: "has enabled",
cfg: &SSOConfig{
Providers: []SSOProvider{{ID: "p1", Enabled: true}},
},
want: true,
},
{
name: "none enabled",
cfg: &SSOConfig{
Providers: []SSOProvider{{ID: "p1", Enabled: false}},
},
want: false,
},
{
name: "empty providers",
cfg: &SSOConfig{Providers: []SSOProvider{}},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.cfg.HasEnabledProviders(); got != tt.want {
t.Errorf("HasEnabledProviders() = %v, want %v", got, tt.want)
}
})
}
}
func TestSSOConfig_AddProvider(t *testing.T) {
cfg := NewSSOConfig()
// Add first provider
err := cfg.AddProvider(SSOProvider{ID: "p1", Name: "Provider 1"})
if err != nil {
t.Fatalf("AddProvider() error = %v, want nil", err)
}
if len(cfg.Providers) != 1 {
t.Errorf("len(Providers) = %d, want 1", len(cfg.Providers))
}
// Add second provider
err = cfg.AddProvider(SSOProvider{ID: "p2", Name: "Provider 2"})
if err != nil {
t.Fatalf("AddProvider() error = %v, want nil", err)
}
if len(cfg.Providers) != 2 {
t.Errorf("len(Providers) = %d, want 2", len(cfg.Providers))
}
// Duplicate ID should fail
err = cfg.AddProvider(SSOProvider{ID: "p1", Name: "Duplicate"})
if err == nil {
t.Error("AddProvider() with duplicate ID should return error")
}
// Empty ID should fail
err = cfg.AddProvider(SSOProvider{ID: "", Name: "No ID"})
if err == nil {
t.Error("AddProvider() with empty ID should return error")
}
// Nil config should fail
var nilCfg *SSOConfig
err = nilCfg.AddProvider(SSOProvider{ID: "p1"})
if err == nil {
t.Error("AddProvider() on nil config should return error")
}
}
func TestSSOConfig_UpdateProvider(t *testing.T) {
cfg := &SSOConfig{
Providers: []SSOProvider{
{ID: "p1", Name: "Original"},
},
}
// Update existing
err := cfg.UpdateProvider(SSOProvider{ID: "p1", Name: "Updated"})
if err != nil {
t.Fatalf("UpdateProvider() error = %v, want nil", err)
}
if cfg.Providers[0].Name != "Updated" {
t.Errorf("Provider name = %q, want %q", cfg.Providers[0].Name, "Updated")
}
// Update non-existent
err = cfg.UpdateProvider(SSOProvider{ID: "p2", Name: "Non-existent"})
if err == nil {
t.Error("UpdateProvider() for non-existent provider should return error")
}
// Nil config
var nilCfg *SSOConfig
err = nilCfg.UpdateProvider(SSOProvider{ID: "p1"})
if err == nil {
t.Error("UpdateProvider() on nil config should return error")
}
}
func TestSSOConfig_RemoveProvider(t *testing.T) {
cfg := &SSOConfig{
DefaultProviderID: "p1",
Providers: []SSOProvider{
{ID: "p1", Name: "Provider 1"},
{ID: "p2", Name: "Provider 2"},
},
}
// Remove non-existent
err := cfg.RemoveProvider("p3")
if err == nil {
t.Error("RemoveProvider() for non-existent provider should return error")
}
// Remove existing (and default)
err = cfg.RemoveProvider("p1")
if err != nil {
t.Fatalf("RemoveProvider() error = %v, want nil", err)
}
if len(cfg.Providers) != 1 {
t.Errorf("len(Providers) = %d, want 1", len(cfg.Providers))
}
if cfg.DefaultProviderID != "" {
t.Errorf("DefaultProviderID = %q, want empty (cleared)", cfg.DefaultProviderID)
}
// Remove last provider
err = cfg.RemoveProvider("p2")
if err != nil {
t.Fatalf("RemoveProvider() error = %v, want nil", err)
}
if len(cfg.Providers) != 0 {
t.Errorf("len(Providers) = %d, want 0", len(cfg.Providers))
}
// Nil config
var nilCfg *SSOConfig
err = nilCfg.RemoveProvider("p1")
if err == nil {
t.Error("RemoveProvider() on nil config should return error")
}
}
func TestSSOConfig_Validate(t *testing.T) {
tests := []struct {
name string
cfg *SSOConfig
wantErr bool
}{
{
name: "nil config",
cfg: nil,
wantErr: false,
},
{
name: "empty config",
cfg: &SSOConfig{},
wantErr: false,
},
{
name: "valid OIDC provider",
cfg: &SSOConfig{
Providers: []SSOProvider{
{
ID: "oidc1",
Name: "OIDC Provider",
Type: SSOProviderTypeOIDC,
Enabled: true,
OIDC: &OIDCProviderConfig{
IssuerURL: "https://idp.example.com",
ClientID: "client123",
},
},
},
},
wantErr: false,
},
{
name: "valid SAML provider with metadata URL",
cfg: &SSOConfig{
Providers: []SSOProvider{
{
ID: "saml1",
Name: "SAML Provider",
Type: SSOProviderTypeSAML,
Enabled: true,
SAML: &SAMLProviderConfig{
IDPMetadataURL: "https://idp.example.com/metadata",
},
},
},
},
wantErr: false,
},
{
name: "valid SAML provider with SSO URL",
cfg: &SSOConfig{
Providers: []SSOProvider{
{
ID: "saml1",
Name: "SAML Provider",
Type: SSOProviderTypeSAML,
Enabled: true,
SAML: &SAMLProviderConfig{
IDPSSOURL: "https://idp.example.com/sso",
},
},
},
},
wantErr: false,
},
{
name: "missing provider ID",
cfg: &SSOConfig{
Providers: []SSOProvider{
{ID: "", Name: "Test"},
},
},
wantErr: true,
},
{
name: "duplicate provider ID",
cfg: &SSOConfig{
Providers: []SSOProvider{
{ID: "p1", Name: "Provider 1"},
{ID: "p1", Name: "Provider 2"},
},
},
wantErr: true,
},
{
name: "missing provider name",
cfg: &SSOConfig{
Providers: []SSOProvider{
{ID: "p1", Name: ""},
},
},
wantErr: true,
},
{
name: "invalid provider type",
cfg: &SSOConfig{
Providers: []SSOProvider{
{ID: "p1", Name: "Test", Type: "invalid"},
},
},
wantErr: true,
},
{
name: "enabled OIDC without config",
cfg: &SSOConfig{
Providers: []SSOProvider{
{ID: "p1", Name: "Test", Type: SSOProviderTypeOIDC, Enabled: true},
},
},
wantErr: true,
},
{
name: "enabled SAML without config",
cfg: &SSOConfig{
Providers: []SSOProvider{
{ID: "p1", Name: "Test", Type: SSOProviderTypeSAML, Enabled: true},
},
},
wantErr: true,
},
{
name: "OIDC missing issuer URL",
cfg: &SSOConfig{
Providers: []SSOProvider{
{
ID: "p1",
Name: "Test",
Type: SSOProviderTypeOIDC,
Enabled: true,
OIDC: &OIDCProviderConfig{ClientID: "client123"},
},
},
},
wantErr: true,
},
{
name: "OIDC missing client ID",
cfg: &SSOConfig{
Providers: []SSOProvider{
{
ID: "p1",
Name: "Test",
Type: SSOProviderTypeOIDC,
Enabled: true,
OIDC: &OIDCProviderConfig{IssuerURL: "https://idp.example.com"},
},
},
},
wantErr: true,
},
{
name: "SAML missing metadata/SSO URL",
cfg: &SSOConfig{
Providers: []SSOProvider{
{
ID: "p1",
Name: "Test",
Type: SSOProviderTypeSAML,
Enabled: true,
SAML: &SAMLProviderConfig{},
},
},
},
wantErr: true,
},
{
name: "SAML signing without cert/key",
cfg: &SSOConfig{
Providers: []SSOProvider{
{
ID: "p1",
Name: "Test",
Type: SSOProviderTypeSAML,
Enabled: true,
SAML: &SAMLProviderConfig{
IDPMetadataURL: "https://idp.example.com/metadata",
SignRequests: true,
},
},
},
},
wantErr: true,
},
{
name: "invalid default provider ID",
cfg: &SSOConfig{
DefaultProviderID: "nonexistent",
Providers: []SSOProvider{
{ID: "p1", Name: "Test"},
},
},
wantErr: true,
},
{
name: "disabled provider skips validation",
cfg: &SSOConfig{
Providers: []SSOProvider{
{
ID: "p1",
Name: "Test",
Type: SSOProviderTypeOIDC,
Enabled: false,
OIDC: nil, // Would fail if enabled
},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.cfg.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestSSOConfig_Clone(t *testing.T) {
original := &SSOConfig{
DefaultProviderID: "p1",
AllowMultipleProviders: true,
Providers: []SSOProvider{
{
ID: "p1",
Name: "Provider 1",
Type: SSOProviderTypeOIDC,
Enabled: true,
AllowedGroups: []string{"admin", "users"},
AllowedDomains: []string{"example.com"},
GroupRoleMappings: map[string]string{
"admin": "administrator",
},
OIDC: &OIDCProviderConfig{
IssuerURL: "https://idp.example.com",
ClientID: "client123",
ClientSecret: "secret",
Scopes: []string{"openid", "profile"},
},
},
{
ID: "p2",
Name: "Provider 2",
Type: SSOProviderTypeSAML,
Enabled: false,
SAML: &SAMLProviderConfig{
IDPMetadataURL: "https://idp2.example.com/metadata",
},
},
},
}
clone := original.Clone()
// Verify deep copy
if clone == original {
t.Error("Clone() returned same pointer")
}
if clone.DefaultProviderID != original.DefaultProviderID {
t.Error("DefaultProviderID not cloned")
}
if clone.AllowMultipleProviders != original.AllowMultipleProviders {
t.Error("AllowMultipleProviders not cloned")
}
if len(clone.Providers) != len(original.Providers) {
t.Error("Providers slice length mismatch")
}
// Modify clone and verify original unchanged
clone.Providers[0].Name = "Modified"
if original.Providers[0].Name == "Modified" {
t.Error("Modifying clone affected original - not a deep copy")
}
clone.Providers[0].AllowedGroups[0] = "modified"
if original.Providers[0].AllowedGroups[0] == "modified" {
t.Error("Modifying clone's AllowedGroups affected original")
}
clone.Providers[0].GroupRoleMappings["admin"] = "modified"
if original.Providers[0].GroupRoleMappings["admin"] == "modified" {
t.Error("Modifying clone's GroupRoleMappings affected original")
}
clone.Providers[0].OIDC.ClientSecret = "modified"
if original.Providers[0].OIDC.ClientSecret == "modified" {
t.Error("Modifying clone's OIDC config affected original")
}
// Test nil clone
var nilCfg *SSOConfig
if result := nilCfg.Clone(); result != nil {
t.Error("Clone() on nil should return nil")
}
}
// Test URL validation
func TestValidateOIDCProvider(t *testing.T) {
tests := []struct {
name string
cfg *OIDCProviderConfig
wantErr bool
}{
{
name: "valid config",
cfg: &OIDCProviderConfig{
IssuerURL: "https://idp.example.com",
ClientID: "client123",
},
wantErr: false,
},
{
name: "invalid issuer URL",
cfg: &OIDCProviderConfig{
IssuerURL: "not-a-url",
ClientID: "client123",
},
wantErr: true,
},
{
name: "empty issuer URL",
cfg: &OIDCProviderConfig{
IssuerURL: "",
ClientID: "client123",
},
wantErr: true,
},
{
name: "whitespace issuer URL",
cfg: &OIDCProviderConfig{
IssuerURL: " ",
ClientID: "client123",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateOIDCProvider(tt.cfg)
if (err != nil) != tt.wantErr {
t.Errorf("validateOIDCProvider() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestValidateSAMLProvider(t *testing.T) {
tests := []struct {
name string
cfg *SAMLProviderConfig
wantErr bool
}{
{
name: "valid with metadata URL",
cfg: &SAMLProviderConfig{
IDPMetadataURL: "https://idp.example.com/metadata",
},
wantErr: false,
},
{
name: "valid with metadata XML",
cfg: &SAMLProviderConfig{
IDPMetadataXML: "<xml>...</xml>",
},
wantErr: false,
},
{
name: "valid with SSO URL",
cfg: &SAMLProviderConfig{
IDPSSOURL: "https://idp.example.com/sso",
},
wantErr: false,
},
{
name: "missing all required fields",
cfg: &SAMLProviderConfig{},
wantErr: true,
},
{
name: "invalid metadata URL",
cfg: &SAMLProviderConfig{
IDPMetadataURL: "not-a-url",
},
wantErr: true,
},
{
name: "invalid SSO URL",
cfg: &SAMLProviderConfig{
IDPSSOURL: "not-a-url",
},
wantErr: true,
},
{
name: "rejects SSO URL with embedded credentials",
cfg: &SAMLProviderConfig{
IDPSSOURL: "https://user:pass@idp.example.com/sso",
},
wantErr: true,
},
{
name: "rejects invalid SLO URL",
cfg: &SAMLProviderConfig{
IDPSSOURL: "https://idp.example.com/sso",
IDPSLOURL: "https://user:pass@idp.example.com/slo",
},
wantErr: true,
},
{
name: "signing enabled without cert",
cfg: &SAMLProviderConfig{
IDPMetadataURL: "https://idp.example.com/metadata",
SignRequests: true,
},
wantErr: true,
},
{
name: "signing enabled with cert but no key",
cfg: &SAMLProviderConfig{
IDPMetadataURL: "https://idp.example.com/metadata",
SignRequests: true,
SPCertificate: "-----BEGIN CERTIFICATE-----...",
},
wantErr: true,
},
{
name: "signing enabled with cert file and key file",
cfg: &SAMLProviderConfig{
IDPMetadataURL: "https://idp.example.com/metadata",
SignRequests: true,
SPCertFile: "/path/to/cert.pem",
SPKeyFile: "/path/to/key.pem",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateSAMLProvider(tt.cfg)
if (err != nil) != tt.wantErr {
t.Errorf("validateSAMLProvider() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}