Files
traefikoidc/internal/providers/validation_test.go
T
lukaszraczylo c3f23cb99b Release 0.7.5 (#70)
* Resolve issue with opaque tokens not being parsed correctly

* Increase test coverage

* Further improvements to test coverage and code quality

* Add new providers.

* fixup! Add new providers.

* Cleanup.

* fixup! Cleanup.

* fixup! fixup! Cleanup.

* fixup! fixup! fixup! Cleanup.

* fixup! fixup! fixup! fixup! Cleanup.

* Memory management optimisation

24 bytes per Put < 256-4096 bytes per buffer allocation avoided (10-170x difference)

* Pooling cleanup.
2025-10-01 12:13:10 +01:00

564 lines
14 KiB
Go

package providers
import (
"net/url"
"strings"
"testing"
"time"
)
// TestNewConfigValidator tests the creation of a ConfigValidator
func TestNewConfigValidator(t *testing.T) {
validator := NewConfigValidator()
if validator == nil {
t.Error("expected non-nil validator")
}
}
// TestValidateIssuerURL tests the ValidateIssuerURL function
func TestValidateIssuerURL(t *testing.T) {
tests := []struct {
name string
issuerURL string
wantErr bool
errMsg string
}{
{
name: "valid https URL",
issuerURL: "https://accounts.google.com",
wantErr: false,
},
{
name: "valid http URL",
issuerURL: "http://localhost:8080",
wantErr: false,
},
{
name: "valid URL with path",
issuerURL: "https://login.microsoftonline.com/tenant-id/v2.0",
wantErr: false,
},
{
name: "empty URL",
issuerURL: "",
wantErr: true,
errMsg: "issuer URL cannot be empty",
},
{
name: "URL without scheme",
issuerURL: "accounts.google.com",
wantErr: true,
errMsg: "issuer URL must include scheme",
},
{
name: "URL with invalid scheme",
issuerURL: "ftp://example.com",
wantErr: true,
errMsg: "issuer URL scheme must be http or https",
},
{
name: "URL without host",
issuerURL: "https://",
wantErr: true,
errMsg: "issuer URL must include host",
},
{
name: "malformed URL",
issuerURL: "ht!tp://[invalid",
wantErr: true,
errMsg: "invalid issuer URL format",
},
{
name: "URL with port",
issuerURL: "https://auth.example.com:443/oauth",
wantErr: false,
},
{
name: "URL with query parameters",
issuerURL: "https://auth.example.com?tenant=123",
wantErr: false,
},
}
validator := NewConfigValidator()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validator.ValidateIssuerURL(tt.issuerURL)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
} else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
t.Errorf("expected error containing %q, got %q", tt.errMsg, err.Error())
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
})
}
}
// TestValidateClientID tests the ValidateClientID function
func TestValidateClientID(t *testing.T) {
tests := []struct {
name string
clientID string
wantErr bool
errMsg string
}{
{
name: "valid client ID",
clientID: "my-application-client",
wantErr: false,
},
{
name: "valid UUID client ID",
clientID: "123e4567-e89b-12d3-a456-426614174000",
wantErr: false,
},
{
name: "empty client ID",
clientID: "",
wantErr: true,
errMsg: "client ID cannot be empty",
},
{
name: "too short client ID",
clientID: "ab",
wantErr: true,
errMsg: "client ID appears to be too short",
},
{
name: "minimum length client ID",
clientID: "abc",
wantErr: false,
},
{
name: "client ID with special characters",
clientID: "client-id_123.app",
wantErr: false,
},
{
name: "long client ID",
clientID: strings.Repeat("a", 255),
wantErr: false,
},
}
validator := NewConfigValidator()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validator.ValidateClientID(tt.clientID)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
} else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
t.Errorf("expected error containing %q, got %q", tt.errMsg, err.Error())
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
})
}
}
// TestValidateScopes tests the ValidateScopes function
func TestValidateScopes(t *testing.T) {
tests := []struct {
name string
scopes []string
wantErr bool
errMsg string
}{
{
name: "valid scopes with openid",
scopes: []string{"openid", "email", "profile"},
wantErr: false,
},
{
name: "only openid scope",
scopes: []string{"openid"},
wantErr: false,
},
{
name: "openid with whitespace",
scopes: []string{" openid ", "email"},
wantErr: false,
},
{
name: "empty scopes",
scopes: []string{},
wantErr: true,
errMsg: "at least one scope must be provided",
},
{
name: "nil scopes",
scopes: nil,
wantErr: true,
errMsg: "at least one scope must be provided",
},
{
name: "missing openid scope",
scopes: []string{"email", "profile"},
wantErr: true,
errMsg: "'openid' scope is required",
},
{
name: "duplicate openid scope",
scopes: []string{"openid", "openid", "email"},
wantErr: false,
},
{
name: "custom scopes with openid",
scopes: []string{"openid", "api:read", "api:write"},
wantErr: false,
},
}
validator := NewConfigValidator()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validator.ValidateScopes(tt.scopes)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
} else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
t.Errorf("expected error containing %q, got %q", tt.errMsg, err.Error())
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
})
}
}
// TestValidateRedirectURL tests the ValidateRedirectURL function
func TestValidateRedirectURL(t *testing.T) {
tests := []struct {
name string
redirectURL string
wantErr bool
errMsg string
}{
{
name: "valid https redirect URL",
redirectURL: "https://example.com/callback",
wantErr: false,
},
{
name: "valid http redirect URL",
redirectURL: "http://localhost:3000/auth/callback",
wantErr: false,
},
{
name: "empty redirect URL",
redirectURL: "",
wantErr: true,
errMsg: "redirect URL cannot be empty",
},
{
name: "redirect URL without scheme",
redirectURL: "example.com/callback",
wantErr: true,
errMsg: "redirect URL must include scheme",
},
{
name: "malformed redirect URL",
redirectURL: "ht!tp://[invalid",
wantErr: true,
errMsg: "invalid redirect URL format",
},
{
name: "redirect URL with query parameters",
redirectURL: "https://example.com/callback?state=abc",
wantErr: false,
},
{
name: "redirect URL with fragment",
redirectURL: "https://example.com/callback#section",
wantErr: false,
},
}
validator := NewConfigValidator()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validator.ValidateRedirectURL(tt.redirectURL)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
} else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
t.Errorf("expected error containing %q, got %q", tt.errMsg, err.Error())
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
})
}
}
// TestValidateProviderSpecificConfig tests provider-specific configuration validation
func TestValidateProviderSpecificConfig(t *testing.T) {
tests := []struct {
name string
provider OIDCProvider
config map[string]interface{}
wantErr bool
errMsg string
}{
{
name: "valid Google config",
provider: NewGoogleProvider(),
config: map[string]interface{}{
"issuer_url": "https://accounts.google.com",
},
wantErr: false,
},
{
name: "invalid Google config - wrong issuer",
provider: NewGoogleProvider(),
config: map[string]interface{}{
"issuer_url": "https://example.com",
},
wantErr: true,
errMsg: "google provider requires issuer URL to contain accounts.google.com",
},
{
name: "valid Azure config with tenant ID",
provider: NewAzureProvider(),
config: map[string]interface{}{
"issuer_url": "https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/v2.0",
},
wantErr: false,
},
{
name: "invalid Azure config - wrong domain",
provider: NewAzureProvider(),
config: map[string]interface{}{
"issuer_url": "https://example.com/tenant",
},
wantErr: true,
errMsg: "azure provider requires issuer URL to contain login.microsoftonline.com",
},
{
name: "Azure config with sts.windows.net",
provider: NewAzureProvider(),
config: map[string]interface{}{
"issuer_url": "https://sts.windows.net/12345678-1234-1234-1234-123456789012",
},
wantErr: false,
},
{
name: "Azure config without tenant ID",
provider: NewAzureProvider(),
config: map[string]interface{}{
"issuer_url": "https://login.microsoftonline.com/common",
},
wantErr: true,
errMsg: "azure issuer URL should include tenant ID",
},
{
name: "valid generic provider config",
provider: NewGenericProvider(),
config: map[string]interface{}{
"issuer_url": "https://auth.example.com",
},
wantErr: false,
},
{
name: "empty config for generic provider",
provider: NewGenericProvider(),
config: map[string]interface{}{},
wantErr: false,
},
}
validator := NewConfigValidator()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validator.ValidateProviderSpecificConfig(tt.provider, tt.config)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
} else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
t.Errorf("expected error containing %q, got %q", tt.errMsg, err.Error())
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
})
}
}
// TestValidateProviderSpecificConfig_UnknownProvider tests handling of unknown provider types
func TestValidateProviderSpecificConfig_UnknownProvider(t *testing.T) {
validator := NewConfigValidator()
// Create a mock provider with invalid type
mockProvider := &mockUnknownProvider{}
err := validator.ValidateProviderSpecificConfig(mockProvider, map[string]interface{}{})
if err == nil {
t.Error("expected error for unknown provider type")
}
if !strings.Contains(err.Error(), "unknown provider type") {
t.Errorf("expected 'unknown provider type' error, got: %v", err)
}
}
// mockUnknownProvider is a test provider with an invalid type
type mockUnknownProvider struct{}
func (m *mockUnknownProvider) GetType() ProviderType {
return ProviderType(999) // Invalid type
}
func (m *mockUnknownProvider) GetCapabilities() ProviderCapabilities {
return ProviderCapabilities{}
}
func (m *mockUnknownProvider) ValidateTokens(session Session, verifier TokenVerifier, tokenCache TokenCache, refreshGracePeriod time.Duration) (*ValidationResult, error) {
return &ValidationResult{}, nil
}
func (m *mockUnknownProvider) BuildAuthParams(baseParams url.Values, scopes []string) (*AuthParams, error) {
return &AuthParams{}, nil
}
func (m *mockUnknownProvider) HandleTokenRefresh(tokenData *TokenResult) error {
return nil
}
func (m *mockUnknownProvider) ValidateConfig() error {
return nil
}
// TestValidateGoogleConfig_EdgeCases tests edge cases for Google config validation
func TestValidateGoogleConfig_EdgeCases(t *testing.T) {
validator := NewConfigValidator()
googleProvider := NewGoogleProvider()
tests := []struct {
name string
config map[string]interface{}
wantErr bool
}{
{
name: "config without issuer_url",
config: map[string]interface{}{},
wantErr: false, // Should pass as issuer_url is not present
},
{
name: "config with non-string issuer_url",
config: map[string]interface{}{
"issuer_url": 123,
},
wantErr: false, // Should pass as type assertion fails
},
{
name: "config with accounts.google.com in path",
config: map[string]interface{}{
"issuer_url": "https://example.com/accounts.google.com",
},
wantErr: false, // Should pass as it contains the required string
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validator.ValidateProviderSpecificConfig(googleProvider, tt.config)
if tt.wantErr && err == nil {
t.Error("expected error, got nil")
} else if !tt.wantErr && err != nil {
t.Errorf("unexpected error: %v", err)
}
})
}
}
// TestValidateAzureConfig_EdgeCases tests edge cases for Azure config validation
func TestValidateAzureConfig_EdgeCases(t *testing.T) {
validator := NewConfigValidator()
azureProvider := NewAzureProvider()
tests := []struct {
name string
config map[string]interface{}
wantErr bool
errMsg string
}{
{
name: "valid tenant ID format",
config: map[string]interface{}{
"issuer_url": "https://login.microsoftonline.com/a1b2c3d4-e5f6-7890-abcd-ef1234567890/v2.0",
},
wantErr: false,
},
{
name: "tenant ID in different position",
config: map[string]interface{}{
"issuer_url": "https://login.microsoftonline.com/v2.0/a1b2c3d4-e5f6-7890-abcd-ef1234567890/oauth",
},
wantErr: false,
},
{
name: "malformed URL for parsing",
config: map[string]interface{}{
"issuer_url": "https://login.microsoftonline.com/[invalid",
},
wantErr: true,
errMsg: "azure issuer URL should include tenant ID",
},
{
name: "config without issuer_url",
config: map[string]interface{}{},
wantErr: false,
},
{
name: "config with non-string issuer_url",
config: map[string]interface{}{
"issuer_url": []string{"https://login.microsoftonline.com"},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validator.ValidateProviderSpecificConfig(azureProvider, tt.config)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
} else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
t.Errorf("expected error containing %q, got %q", tt.errMsg, err.Error())
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
})
}
}