From d3874b28ad09c233d903530b18b7e9442ef374ed Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 18 Jul 2025 14:28:44 +0800 Subject: [PATCH] Refactor OAuth service to improve client registration and user management - Enhanced dynamic client registration by refining client ID and secret generation methods. - Improved validation processes for client registration requests and authorization flows. - Streamlined user management integration with the updated user model, ensuring compatibility and efficiency. - Updated token management features to support new client and user interactions, enhancing overall service functionality. --- openapi/oauth/TESTING_GUIDE.md | 333 ++++++++++++ openapi/oauth/oauth_test.go | 942 +++++++++++++++++++++++++++++++++ 2 files changed, 1275 insertions(+) create mode 100644 openapi/oauth/TESTING_GUIDE.md create mode 100644 openapi/oauth/oauth_test.go diff --git a/openapi/oauth/TESTING_GUIDE.md b/openapi/oauth/TESTING_GUIDE.md new file mode 100644 index 00000000..6f6997fa --- /dev/null +++ b/openapi/oauth/TESTING_GUIDE.md @@ -0,0 +1,333 @@ +# OAuth 2.1 Testing Guide + +## Overview + +This guide provides comprehensive testing infrastructure for OAuth 2.1 authorization server implementation. The test environment includes standardized test data sets, environment setup functions, and complete test coverage for all OAuth functionality. + +## Test Environment Architecture + +### Core Components + +- **OAuth Service Configuration**: Complete OAuth 2.1 configuration with all features enabled +- **Store Management**: Support for MongoDB and Badger stores with automatic fallback +- **Test Data Sets**: Pre-configured clients and users for comprehensive testing +- **Environment Setup**: Standardized initialization and cleanup procedures + +### Test Data Sets + +#### Standard Test Clients (3 clients) + +1. **Confidential Client** (`test-confidential-client`) + + - **Purpose**: Authorization code flow testing + - **Grant Types**: authorization_code, refresh_token + - **Use Case**: Web applications with server-side authentication + +2. **Public Client** (`test-public-client`) + + - **Purpose**: Mobile/SPA application testing + - **Grant Types**: authorization_code (with PKCE) + - **Use Case**: Single-page applications and mobile apps + +3. **Client Credentials Client** (`test-credentials-client`) + - **Purpose**: Server-to-server authentication + - **Grant Types**: client_credentials + - **Use Case**: API access and service authentication + +#### Standard Test Users (10 users) + +1. **Admin User** (`admin`) + + - **Privileges**: Full access with admin scope + - **Features**: 2FA enabled, all verifications complete + - **Use Case**: Administrative functionality testing + +2. **Regular User** (`john.doe`) + + - **Privileges**: Basic user access + - **Features**: Standard verification + - **Use Case**: Standard user flow testing + +3. **Enhanced User** (`jane.smith`) + + - **Privileges**: Basic user access with mobile verification + - **Features**: Email and mobile verified + - **Use Case**: Multi-factor authentication testing + +4. **Pending User** (`pending.user`) + + - **Privileges**: Limited access + - **Features**: Pending verification status + - **Use Case**: User onboarding flow testing + +5. **Inactive User** (`inactive.user`) + + - **Privileges**: Disabled account + - **Features**: Inactive status + - **Use Case**: Account management testing + +6. **Limited User** (`limited.user`) + + - **Privileges**: Minimal scope access + - **Features**: Basic OpenID only + - **Use Case**: Scope limitation testing + +7. **Security User** (`secure.user`) + + - **Privileges**: Standard access with enhanced security + - **Features**: 2FA enabled, all verifications + - **Use Case**: Security feature testing + +8. **API User** (`api.user`) + + - **Privileges**: API access scopes + - **Features**: API-specific permissions + - **Use Case**: API authorization testing + +9. **Guest User** (`guest.user`) + + - **Privileges**: Minimal guest access + - **Features**: No verifications + - **Use Case**: Guest flow testing + +10. **Test User** (`test.user`) + - **Privileges**: General testing access + - **Features**: Mixed permissions for testing + - **Use Case**: General purpose testing + +## Environment Setup + +### Prerequisites + +```bash +# Source the environment configuration +source $YAO_SOURCE_ROOT/env.local.sh +``` + +### Core Setup Function + +```go +func setupOAuthTestEnvironment(t *testing.T) (*Service, store.Store, store.Store, func()) { + // Creates complete OAuth test environment with: + // - Configured OAuth service with all features enabled + // - Primary store (MongoDB preferred, Badger fallback) + // - Cache store (LRU cache) + // - Pre-loaded test clients and users + // - Cleanup function for proper teardown +} +``` + +### Environment Features + +- **Store Management**: Automatic store selection with fallback +- **Data Isolation**: Each test gets fresh data set +- **Cleanup**: Automatic cleanup of test data +- **Logging**: Comprehensive test logging for debugging + +## Testing Patterns + +### Basic Test Structure + +```go +func TestOAuthFeature(t *testing.T) { + service, _, _, cleanup := setupOAuthTestEnvironment(t) + defer cleanup() + + // Use pre-configured test clients and users + // All standard OAuth flows are supported +} +``` + +### Parameterized Testing + +```go +func TestMultipleStores(t *testing.T) { + storeConfigs := getStoreConfigs() + + for _, config := range storeConfigs { + t.Run(config.Name, func(t *testing.T) { + // Test with different store backends + }) + } +} +``` + +### Integration Testing + +```go +func TestOAuthFlow(t *testing.T) { + service, _, _, cleanup := setupOAuthTestEnvironment(t) + defer cleanup() + + // Use testClients[0] for confidential client testing + // Use testUsers[0] for admin user testing + // Complete OAuth flows with real data +} +``` + +## Test Coverage + +### Core OAuth Service Tests + +- **Service Creation**: Configuration validation and initialization +- **Service Getters**: Provider access and configuration retrieval +- **Configuration**: Default values and validation +- **Feature Flags**: OAuth 2.1 and MCP compliance features +- **Provider Integration**: User and client provider functionality + +### Configuration Tests + +- **Valid Configuration**: Complete configuration testing +- **Missing Components**: Error handling for missing configuration +- **Invalid Values**: Validation of configuration parameters +- **Default Values**: Proper default value assignment + +### Integration Tests + +- **Client Access**: Verification of test client availability +- **User Access**: Verification of test user availability +- **Store Operations**: Multi-store compatibility testing +- **Provider Operations**: User and client provider integration + +## Running Tests + +### All Tests + +```bash +cd $YAO_SOURCE_ROOT +go test -v ./openapi/oauth -timeout 60s +``` + +### Specific Test + +```bash +cd $YAO_SOURCE_ROOT +go test -v ./openapi/oauth -run TestNewService -timeout 30s +``` + +### With Coverage + +```bash +cd $YAO_SOURCE_ROOT +go test -v ./openapi/oauth -cover -timeout 60s +``` + +## Test Data Reference + +### Quick Client Access + +```go +// Get confidential client for authorization code flow +confidentialClient := testClients[0] + +// Get public client for PKCE flow +publicClient := testClients[1] + +// Get client credentials client +credentialsClient := testClients[2] +``` + +### Quick User Access + +```go +// Get admin user for administrative testing +adminUser := testUsers[0] + +// Get regular user for standard flow testing +regularUser := testUsers[1] + +// Get user with specific features +secureUser := testUsers[6] // 2FA enabled +apiUser := testUsers[7] // API scopes +``` + +## Best Practices + +### Test Organization + +1. **Use Standard Environment**: Always use `setupOAuthTestEnvironment()` +2. **Leverage Test Data**: Use pre-configured clients and users +3. **Proper Cleanup**: Always defer cleanup function +4. **Descriptive Names**: Use clear test and subtest names + +### Data Management + +1. **Data Isolation**: Each test gets fresh environment +2. **Cleanup**: Automatic cleanup prevents test pollution +3. **Logging**: Comprehensive logging for debugging +4. **Consistency**: Standard data sets ensure consistent testing + +### Error Handling + +1. **Proper Assertions**: Use testify for clear assertions +2. **Error Messages**: Include context in error messages +3. **Cleanup on Failure**: Cleanup runs even on test failure +4. **Detailed Logging**: Log important test steps + +## Environment Variables + +### Required for Full Testing + +```bash +# MongoDB connection (optional, will fallback to Badger) +export MONGO_TEST_HOST=localhost +export MONGO_TEST_PORT=27017 +export MONGO_TEST_USER=test +export MONGO_TEST_PASS=test +``` + +### Configuration Files + +- **Environment Setup**: `$YAO_SOURCE_ROOT/env.local.sh` +- **Test Configuration**: Built into test environment +- **Store Configuration**: Automatic configuration management + +## Troubleshooting + +### Common Issues + +1. **Store Connection**: Check MongoDB availability or use Badger fallback +2. **Environment Setup**: Ensure `env.local.sh` is sourced +3. **Test Timeouts**: Increase timeout for slow operations +4. **Data Conflicts**: Ensure proper cleanup between tests + +### Debug Logging + +Tests include comprehensive logging: + +- Environment initialization +- Test data creation +- Store operations +- Test execution steps + +### Performance Considerations + +- **MongoDB**: Preferred for full feature testing +- **Badger**: Fast fallback for basic testing +- **Cache**: LRU cache for improved performance +- **Cleanup**: Efficient cleanup procedures + +## Extending Tests + +### Adding New Test Cases + +1. Use `setupOAuthTestEnvironment()` as base +2. Leverage existing test data sets +3. Follow established patterns +4. Include proper cleanup + +### Adding New Test Data + +1. Add to `testClients` or `testUsers` arrays +2. Update `setupTestData()` function +3. Update `cleanupTestData()` function +4. Document new test data purpose + +### Custom Test Environments + +1. Create custom configuration based on standard +2. Use existing store and cache setup +3. Implement custom cleanup +4. Maintain test isolation + +This testing infrastructure provides comprehensive coverage for OAuth 2.1 authorization server functionality with proper environment management, standardized test data, and robust cleanup procedures. diff --git a/openapi/oauth/oauth_test.go b/openapi/oauth/oauth_test.go new file mode 100644 index 00000000..34853c45 --- /dev/null +++ b/openapi/oauth/oauth_test.go @@ -0,0 +1,942 @@ +package oauth + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/yaoapp/gou/connector" + "github.com/yaoapp/gou/model" + "github.com/yaoapp/gou/store" + "github.com/yaoapp/gou/store/badger" + "github.com/yaoapp/gou/store/lru" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/openapi/oauth/types" + "github.com/yaoapp/yao/test" +) + +// ============================================================================= +// Test Environment Setup +// ============================================================================= + +// **IMPORTANT** +// Before running any OAuth tests, you must source the env.local.sh file. +// $YAO_SOURCE_ROOT is the root directory of the Yao source code. +// source $YAO_SOURCE_ROOT/env.local.sh + +// Store configuration for parameterized tests +type StoreConfig struct { + Name string + GetFunc func(*testing.T) store.Store +} + +// TestClient represents a test OAuth client +// AI: Use this standard test client structure for all OAuth functionality tests +type TestClient struct { + ClientID string + ClientSecret string + ClientName string + ClientType string + RedirectURIs []string + GrantTypes []string + ResponseTypes []string + Scope string + Description string // For test identification +} + +// TestUser represents a test user +// AI: Use this standard test user structure for all OAuth functionality tests +type TestUser struct { + ID int64 + Subject string + Username string + Email string + PasswordHash string + FirstName string + LastName string + FullName string + Scopes []string + Status string + EmailVerified bool + MobileVerified bool + TwoFactorEnabled bool + Description string // For test identification +} + +// OAuth Test Environment Setup +// AI: This is the foundational environment setup for all OAuth unit tests. +// Use this environment setup function directly when building other OAuth tests. +// It provides pre-configured stores, clients, and users for comprehensive testing. + +// Standard Test Data Sets +// AI: All subsequent functionality tests should use these pre-defined test data sets. +// These provide consistent, well-structured test data for OAuth operations. + +// Test clients - 3 different types for comprehensive testing +var testClients = []*TestClient{ + { + ClientID: "test-confidential-client", + ClientSecret: "confidential-secret-12345", + ClientName: "Test Confidential Client", + ClientType: types.ClientTypeConfidential, + RedirectURIs: []string{"https://confidential.example.com/callback"}, + GrantTypes: []string{types.GrantTypeAuthorizationCode, types.GrantTypeRefreshToken}, + ResponseTypes: []string{types.ResponseTypeCode}, + Scope: "openid profile email", + Description: "Confidential client for authorization code flow", + }, + { + ClientID: "test-public-client", + ClientSecret: "", // Public clients don't have secrets + ClientName: "Test Public Client", + ClientType: types.ClientTypePublic, + RedirectURIs: []string{"https://public.example.com/callback"}, + GrantTypes: []string{types.GrantTypeAuthorizationCode}, + ResponseTypes: []string{types.ResponseTypeCode}, + Scope: "openid profile", + Description: "Public client for mobile/SPA applications", + }, + { + ClientID: "test-credentials-client", + ClientSecret: "credentials-secret-67890", + ClientName: "Test Client Credentials Client", + ClientType: types.ClientTypeConfidential, + RedirectURIs: []string{"https://credentials.example.com/callback"}, + GrantTypes: []string{types.GrantTypeClientCredentials}, + ResponseTypes: []string{types.ResponseTypeCode}, + Scope: "api:read api:write", + Description: "Client for server-to-server authentication", + }, +} + +// Test users - 10 users with different characteristics +var testUsers = []*TestUser{ + { + Subject: "user-admin-001", + Username: "admin", + Email: "admin@example.com", + PasswordHash: "admin-hash-001", + FirstName: "Admin", + LastName: "User", + FullName: "Admin User", + Scopes: []string{"openid", "profile", "email", "admin"}, + Status: "active", + EmailVerified: true, + MobileVerified: true, + TwoFactorEnabled: true, + Description: "Administrator user with full privileges", + }, + { + Subject: "user-regular-001", + Username: "john.doe", + Email: "john.doe@example.com", + PasswordHash: "john-hash-001", + FirstName: "John", + LastName: "Doe", + FullName: "John Doe", + Scopes: []string{"openid", "profile", "email"}, + Status: "active", + EmailVerified: true, + MobileVerified: false, + TwoFactorEnabled: false, + Description: "Regular user with basic privileges", + }, + { + Subject: "user-regular-002", + Username: "jane.smith", + Email: "jane.smith@example.com", + PasswordHash: "jane-hash-001", + FirstName: "Jane", + LastName: "Smith", + FullName: "Jane Smith", + Scopes: []string{"openid", "profile", "email"}, + Status: "active", + EmailVerified: true, + MobileVerified: true, + TwoFactorEnabled: false, + Description: "Regular user with verified mobile", + }, + { + Subject: "user-pending-001", + Username: "pending.user", + Email: "pending@example.com", + PasswordHash: "pending-hash-001", + FirstName: "Pending", + LastName: "User", + FullName: "Pending User", + Scopes: []string{"openid", "profile"}, + Status: "pending", + EmailVerified: false, + MobileVerified: false, + TwoFactorEnabled: false, + Description: "User with pending verification", + }, + { + Subject: "user-inactive-001", + Username: "inactive.user", + Email: "inactive@example.com", + PasswordHash: "inactive-hash-001", + FirstName: "Inactive", + LastName: "User", + FullName: "Inactive User", + Scopes: []string{"openid"}, + Status: "inactive", + EmailVerified: true, + MobileVerified: false, + TwoFactorEnabled: false, + Description: "Inactive user account", + }, + { + Subject: "user-limited-001", + Username: "limited.user", + Email: "limited@example.com", + PasswordHash: "limited-hash-001", + FirstName: "Limited", + LastName: "User", + FullName: "Limited User", + Scopes: []string{"openid"}, + Status: "active", + EmailVerified: true, + MobileVerified: false, + TwoFactorEnabled: false, + Description: "User with limited scope access", + }, + { + Subject: "user-2fa-001", + Username: "secure.user", + Email: "secure@example.com", + PasswordHash: "secure-hash-001", + FirstName: "Secure", + LastName: "User", + FullName: "Secure User", + Scopes: []string{"openid", "profile", "email"}, + Status: "active", + EmailVerified: true, + MobileVerified: true, + TwoFactorEnabled: true, + Description: "Security-focused user with 2FA enabled", + }, + { + Subject: "user-api-001", + Username: "api.user", + Email: "api@example.com", + PasswordHash: "api-hash-001", + FirstName: "API", + LastName: "User", + FullName: "API User", + Scopes: []string{"api:read", "api:write"}, + Status: "active", + EmailVerified: true, + MobileVerified: false, + TwoFactorEnabled: false, + Description: "User for API access testing", + }, + { + Subject: "user-guest-001", + Username: "guest.user", + Email: "guest@example.com", + PasswordHash: "guest-hash-001", + FirstName: "Guest", + LastName: "User", + FullName: "Guest User", + Scopes: []string{"openid"}, + Status: "active", + EmailVerified: false, + MobileVerified: false, + TwoFactorEnabled: false, + Description: "Guest user with minimal access", + }, + { + Subject: "user-test-001", + Username: "test.user", + Email: "test@example.com", + PasswordHash: "test-hash-001", + FirstName: "Test", + LastName: "User", + FullName: "Test User", + Scopes: []string{"openid", "profile", "email", "test"}, + Status: "active", + EmailVerified: true, + MobileVerified: true, + TwoFactorEnabled: false, + Description: "General purpose test user", + }, +} + +// setupOAuthTestEnvironment sets up the foundational environment for OAuth unit tests +// AI: This is the core environment setup function. Use this directly in other OAuth tests. +// It provides everything needed: stores, clients, users, and proper cleanup. +func setupOAuthTestEnvironment(t *testing.T) (*Service, store.Store, store.Store, func()) { + // Initialize test environment + test.Prepare(t, config.Conf) + + // Get store configurations + storeConfigs := getStoreConfigs() + + // Use the first available store (prefer MongoDB, fallback to Badger) + var mainStore store.Store + var storeConfig StoreConfig + + for _, config := range storeConfigs { + func() { + defer func() { + if r := recover(); r != nil { + t.Logf("Store %s not available: %v", config.Name, r) + } + }() + + testStore := config.GetFunc(t) + if testStore != nil { + mainStore = testStore + storeConfig = config + return + } + }() + + if mainStore != nil { + break + } + } + + // Fallback to Badger if no other store is available + if mainStore == nil { + mainStore = getBadgerStore(t) + storeConfig = StoreConfig{Name: "Badger", GetFunc: getBadgerStore} + } + + // Create cache + cache := getLRUCache(t) + + // Create OAuth service configuration + oauthConfig := &Config{ + Store: mainStore, + Cache: cache, + Signing: types.SigningConfig{ + SigningAlgorithm: "RS256", + SigningCertPath: "/tmp/test-cert.pem", + SigningKeyPath: "/tmp/test-key.pem", + }, + Token: types.TokenConfig{ + AccessTokenLifetime: time.Hour, + RefreshTokenLifetime: 24 * time.Hour, + AuthorizationCodeLifetime: 10 * time.Minute, + DeviceCodeLifetime: 15 * time.Minute, + AccessTokenFormat: "jwt", + RefreshTokenFormat: "opaque", + }, + Security: types.SecurityConfig{ + PKCECodeChallengeMethod: []string{"S256"}, + PKCECodeVerifierLength: 128, + StateParameterLifetime: 10 * time.Minute, + StateParameterLength: 32, + }, + Client: types.ClientConfig{ + DefaultClientType: types.ClientTypeConfidential, + DefaultTokenEndpointAuthMethod: "client_secret_basic", + DefaultGrantTypes: []string{types.GrantTypeAuthorizationCode, types.GrantTypeRefreshToken}, + DefaultResponseTypes: []string{types.ResponseTypeCode}, + ClientIDLength: 32, + ClientSecretLength: 64, + DynamicRegistrationEnabled: true, + AllowedRedirectURISchemes: []string{"https", "http"}, + AllowedRedirectURIHosts: []string{"localhost", "127.0.0.1"}, + }, + Features: FeatureFlags{ + OAuth21Enabled: true, + PKCEEnforced: true, + RefreshTokenRotationEnabled: true, + DeviceFlowEnabled: true, + TokenExchangeEnabled: true, + PushedAuthorizationEnabled: true, + DynamicClientRegistrationEnabled: true, + MCPComplianceEnabled: true, + ResourceParameterEnabled: true, + TokenBindingEnabled: true, + MTLSEnabled: false, + DPoPEnabled: false, + JWTIntrospectionEnabled: true, + TokenRevocationEnabled: true, + UserInfoJWTEnabled: true, + }, + IssuerURL: "https://oauth.test.example.com", + } + + // Create OAuth service + service, err := NewService(oauthConfig) + require.NoError(t, err, "Failed to create OAuth service") + require.NotNil(t, service, "OAuth service should not be nil") + + // Setup test data + setupTestData(t, service) + + // Return cleanup function + cleanup := func() { + cleanupTestData(t, service) + test.Clean() + + // Close stores if they support it + if closer, ok := mainStore.(interface{ Close() error }); ok { + closer.Close() + } + if closer, ok := cache.(interface{ Close() error }); ok { + closer.Close() + } + } + + t.Logf("OAuth test environment initialized with %s store", storeConfig.Name) + return service, mainStore, cache, cleanup +} + +// setupTestData initializes the standard test data set +func setupTestData(t *testing.T, service *Service) { + ctx := context.Background() + + // Clean up any existing test data first + cleanupTestData(t, service) + + // Create test clients + clientProvider := service.GetClientProvider() + for i, testClient := range testClients { + clientInfo := &types.ClientInfo{ + ClientID: testClient.ClientID, + ClientSecret: testClient.ClientSecret, + ClientName: testClient.ClientName, + ClientType: testClient.ClientType, + RedirectURIs: testClient.RedirectURIs, + GrantTypes: testClient.GrantTypes, + ResponseTypes: testClient.ResponseTypes, + Scope: testClient.Scope, + ApplicationType: types.ApplicationTypeWeb, + TokenEndpointAuthMethod: "client_secret_basic", + } + + // Set appropriate auth method for public clients + if testClient.ClientType == types.ClientTypePublic { + clientInfo.TokenEndpointAuthMethod = "none" + } + + createdClient, err := clientProvider.CreateClient(ctx, clientInfo) + require.NoError(t, err, "Failed to create test client %d: %s", i, testClient.Description) + require.NotNil(t, createdClient, "Created client should not be nil") + + t.Logf("Created test client: %s (%s)", testClient.ClientID, testClient.Description) + } + + // Create test users + userProvider := service.GetUserProvider() + for i, testUser := range testUsers { + userData := map[string]interface{}{ + "subject": testUser.Subject, + "username": testUser.Username, + "email": testUser.Email, + "password_hash": testUser.PasswordHash, + "first_name": testUser.FirstName, + "last_name": testUser.LastName, + "full_name": testUser.FullName, + "scopes": testUser.Scopes, + "status": testUser.Status, + "email_verified": testUser.EmailVerified, + "mobile_verified": testUser.MobileVerified, + "two_factor_enabled": testUser.TwoFactorEnabled, + } + + createdUserID, err := userProvider.CreateUser(userData) + require.NoError(t, err, "Failed to create test user %d: %s", i, testUser.Description) + require.NotNil(t, createdUserID, "Created user ID should not be nil") + + // Update the test user with the created ID + if userID, ok := createdUserID.(int64); ok { + testUser.ID = userID + } else if userID, ok := createdUserID.(int); ok { + testUser.ID = int64(userID) + } else { + testUser.ID = int64(0) // Fallback for interface{} types + } + + t.Logf("Created test user: %s (%s)", testUser.Username, testUser.Description) + } + + t.Logf("Test data setup complete: %d clients, %d users", len(testClients), len(testUsers)) +} + +// cleanupTestData removes all test data +func cleanupTestData(t *testing.T, service *Service) { + ctx := context.Background() + + // Clean up test clients + clientProvider := service.GetClientProvider() + for _, testClient := range testClients { + err := clientProvider.DeleteClient(ctx, testClient.ClientID) + if err != nil { + t.Logf("Warning: Failed to delete test client %s: %v", testClient.ClientID, err) + } + } + + // Clean up test users + m := model.Select("__yao.user") + for _, testUser := range testUsers { + if testUser.ID > 0 { + err := m.Destroy(testUser.ID) + if err != nil { + t.Logf("Warning: Failed to delete test user %d: %v", testUser.ID, err) + } + } + } + + // Clean up any remaining test data by patterns + _, err := m.DestroyWhere(model.QueryParam{ + Wheres: []model.QueryWhere{ + {Column: "subject", OP: "like", Value: "user-%"}, + }, + }) + if err != nil { + t.Logf("Warning: Failed to cleanup test users by pattern: %v", err) + } +} + +// Helper functions for store setup (same as in other test files) + +func getMongoStore(t *testing.T) store.Store { + host := os.Getenv("MONGO_TEST_HOST") + if host == "" { + t.Skip("MongoDB not available - set MONGO_TEST_HOST environment variable") + } + + mongoConnector, err := connector.New("mongo", "oauth_test", []byte(`{ + "name": "OAuth Test MongoDB", + "type": "mongo", + "options": { + "db": "oauth_test", + "hosts": [{ + "host": "`+host+`", + "port": "`+os.Getenv("MONGO_TEST_PORT")+`", + "user": "`+os.Getenv("MONGO_TEST_USER")+`", + "pass": "`+os.Getenv("MONGO_TEST_PASS")+`" + }] + } + }`)) + require.NoError(t, err) + + mongoStore, err := store.New(mongoConnector, nil) + require.NoError(t, err) + + return mongoStore +} + +func getBadgerStore(t *testing.T) store.Store { + tempDir := t.TempDir() + dbPath := filepath.Join(tempDir, "test_oauth_badger") + + badgerStore, err := badger.New(dbPath) + require.NoError(t, err) + + t.Cleanup(func() { + badgerStore.Close() + }) + + return badgerStore +} + +func getLRUCache(t *testing.T) store.Store { + cache, err := lru.New(1000) + require.NoError(t, err) + return cache +} + +func getStoreConfigs() []StoreConfig { + return []StoreConfig{ + {Name: "MongoDB", GetFunc: getMongoStore}, + {Name: "Badger", GetFunc: getBadgerStore}, + } +} + +// ============================================================================= +// OAuth Service Tests +// ============================================================================= + +func TestMain(m *testing.M) { + // Run tests + code := m.Run() + os.Exit(code) +} + +func TestNewService(t *testing.T) { + t.Run("create service with valid config", func(t *testing.T) { + service, _, _, cleanup := setupOAuthTestEnvironment(t) + defer cleanup() + + assert.NotNil(t, service) + assert.NotNil(t, service.config) + assert.NotNil(t, service.store) + assert.NotNil(t, service.cache) + assert.NotNil(t, service.userProvider) + assert.NotNil(t, service.clientProvider) + assert.NotEmpty(t, service.prefix) + }) + + t.Run("create service with nil config", func(t *testing.T) { + service, err := NewService(nil) + assert.Error(t, err) + assert.Nil(t, service) + assert.Equal(t, types.ErrInvalidConfiguration, err) + }) + + t.Run("create service with missing store", func(t *testing.T) { + config := &Config{ + IssuerURL: "https://test.example.com", + } + + service, err := NewService(config) + assert.Error(t, err) + assert.Nil(t, service) + assert.Equal(t, types.ErrStoreMissing, err) + }) + + t.Run("create service with missing issuer URL", func(t *testing.T) { + store := getBadgerStore(t) + config := &Config{ + Store: store, + Signing: types.SigningConfig{ + SigningCertPath: "/tmp/cert.pem", + SigningKeyPath: "/tmp/key.pem", + }, + } + + service, err := NewService(config) + assert.Error(t, err) + assert.Nil(t, service) + assert.Equal(t, types.ErrIssuerURLMissing, err) + }) +} + +func TestServiceGetters(t *testing.T) { + service, _, _, cleanup := setupOAuthTestEnvironment(t) + defer cleanup() + + t.Run("get config", func(t *testing.T) { + config := service.GetConfig() + assert.NotNil(t, config) + assert.Equal(t, "https://oauth.test.example.com", config.IssuerURL) + assert.True(t, config.Features.OAuth21Enabled) + }) + + t.Run("get user provider", func(t *testing.T) { + userProvider := service.GetUserProvider() + assert.NotNil(t, userProvider) + assert.Implements(t, (*types.UserProvider)(nil), userProvider) + }) + + t.Run("get client provider", func(t *testing.T) { + clientProvider := service.GetClientProvider() + assert.NotNil(t, clientProvider) + assert.Implements(t, (*types.ClientProvider)(nil), clientProvider) + }) +} + +func TestConfigDefaults(t *testing.T) { + store := getBadgerStore(t) + + t.Run("set default values", func(t *testing.T) { + config := &Config{ + Store: store, + IssuerURL: "https://test.example.com", + Signing: types.SigningConfig{ + SigningCertPath: "/tmp/cert.pem", + SigningKeyPath: "/tmp/key.pem", + }, + } + + service, err := NewService(config) + assert.NoError(t, err) + assert.NotNil(t, service) + + // Check that defaults were set + assert.Equal(t, "RS256", config.Signing.SigningAlgorithm) + assert.Equal(t, time.Hour, config.Token.AccessTokenLifetime) + assert.Equal(t, 24*time.Hour, config.Token.RefreshTokenLifetime) + assert.Equal(t, 10*time.Minute, config.Token.AuthorizationCodeLifetime) + assert.Equal(t, 15*time.Minute, config.Token.DeviceCodeLifetime) + assert.Equal(t, "jwt", config.Token.AccessTokenFormat) + assert.Equal(t, "opaque", config.Token.RefreshTokenFormat) + assert.Equal(t, []string{"S256"}, config.Security.PKCECodeChallengeMethod) + assert.Equal(t, 128, config.Security.PKCECodeVerifierLength) + assert.Equal(t, 10*time.Minute, config.Security.StateParameterLifetime) + assert.Equal(t, 32, config.Security.StateParameterLength) + assert.Equal(t, types.ClientTypeConfidential, config.Client.DefaultClientType) + assert.Equal(t, "client_secret_basic", config.Client.DefaultTokenEndpointAuthMethod) + assert.Equal(t, []string{"authorization_code", "refresh_token"}, config.Client.DefaultGrantTypes) + assert.Equal(t, []string{"code"}, config.Client.DefaultResponseTypes) + assert.Equal(t, 32, config.Client.ClientIDLength) + assert.Equal(t, 64, config.Client.ClientSecretLength) + assert.True(t, config.Features.OAuth21Enabled) + assert.True(t, config.Features.PKCEEnforced) + assert.True(t, config.Features.RefreshTokenRotationEnabled) + }) +} + +func TestFeatureFlags(t *testing.T) { + service, _, _, cleanup := setupOAuthTestEnvironment(t) + defer cleanup() + + config := service.GetConfig() + + t.Run("oauth 2.1 features enabled", func(t *testing.T) { + assert.True(t, config.Features.OAuth21Enabled) + assert.True(t, config.Features.PKCEEnforced) + assert.True(t, config.Features.RefreshTokenRotationEnabled) + }) + + t.Run("advanced features enabled", func(t *testing.T) { + assert.True(t, config.Features.DeviceFlowEnabled) + assert.True(t, config.Features.TokenExchangeEnabled) + assert.True(t, config.Features.PushedAuthorizationEnabled) + assert.True(t, config.Features.DynamicClientRegistrationEnabled) + }) + + t.Run("mcp features enabled", func(t *testing.T) { + assert.True(t, config.Features.MCPComplianceEnabled) + assert.True(t, config.Features.ResourceParameterEnabled) + }) + + t.Run("security features configured", func(t *testing.T) { + assert.True(t, config.Features.TokenBindingEnabled) + assert.False(t, config.Features.MTLSEnabled) + assert.False(t, config.Features.DPoPEnabled) + }) + + t.Run("experimental features enabled", func(t *testing.T) { + assert.True(t, config.Features.JWTIntrospectionEnabled) + assert.True(t, config.Features.TokenRevocationEnabled) + assert.True(t, config.Features.UserInfoJWTEnabled) + }) +} + +func TestProviderInitialization(t *testing.T) { + t.Run("default providers created when not provided", func(t *testing.T) { + store := getBadgerStore(t) + cache := getLRUCache(t) + + config := &Config{ + Store: store, + Cache: cache, + IssuerURL: "https://test.example.com", + Signing: types.SigningConfig{ + SigningCertPath: "/tmp/cert.pem", + SigningKeyPath: "/tmp/key.pem", + }, + } + + service, err := NewService(config) + assert.NoError(t, err) + assert.NotNil(t, service) + + // Check that default providers were created + assert.NotNil(t, service.userProvider) + assert.NotNil(t, service.clientProvider) + + // Verify they implement the correct interfaces + assert.Implements(t, (*types.UserProvider)(nil), service.userProvider) + assert.Implements(t, (*types.ClientProvider)(nil), service.clientProvider) + }) + + t.Run("custom providers used when provided", func(t *testing.T) { + store := getBadgerStore(t) + cache := getLRUCache(t) + + // Create a temporary service to get default providers for testing + tempConfig := &Config{ + Store: store, + Cache: cache, + IssuerURL: "https://test.example.com", + Signing: types.SigningConfig{ + SigningCertPath: "/tmp/cert.pem", + SigningKeyPath: "/tmp/key.pem", + }, + } + + tempService, err := NewService(tempConfig) + require.NoError(t, err) + + // Create custom providers (for this test, we'll use the default ones) + customUserProvider := tempService.GetUserProvider() + customClientProvider := tempService.GetClientProvider() + + config := &Config{ + Store: store, + Cache: cache, + UserProvider: customUserProvider, + ClientProvider: customClientProvider, + IssuerURL: "https://test.example.com", + Signing: types.SigningConfig{ + SigningCertPath: "/tmp/cert.pem", + SigningKeyPath: "/tmp/key.pem", + }, + } + + service, err := NewService(config) + assert.NoError(t, err) + assert.NotNil(t, service) + + // Check that custom providers were used + assert.Equal(t, customUserProvider, service.userProvider) + assert.Equal(t, customClientProvider, service.clientProvider) + }) +} + +func TestServiceIntegration(t *testing.T) { + service, _, _, cleanup := setupOAuthTestEnvironment(t) + defer cleanup() + + ctx := context.Background() + + t.Run("verify test clients are accessible", func(t *testing.T) { + clientProvider := service.GetClientProvider() + + for _, testClient := range testClients { + client, err := clientProvider.GetClientByID(ctx, testClient.ClientID) + assert.NoError(t, err, "Failed to get client %s", testClient.ClientID) + assert.NotNil(t, client, "Client %s should not be nil", testClient.ClientID) + assert.Equal(t, testClient.ClientName, client.ClientName) + assert.Equal(t, testClient.ClientType, client.ClientType) + } + }) + + t.Run("verify test users are accessible", func(t *testing.T) { + userProvider := service.GetUserProvider() + + for _, testUser := range testUsers { + user, err := userProvider.GetUserBySubject(ctx, testUser.Subject) + assert.NoError(t, err, "Failed to get user %s", testUser.Subject) + assert.NotNil(t, user, "User %s should not be nil", testUser.Subject) + + // Note: Skip detailed verification as user structure may vary by provider + // The important thing is that the user exists and can be retrieved + } + }) +} + +// ============================================================================= +// Configuration Validation Tests +// ============================================================================= + +func TestConfigValidation(t *testing.T) { + t.Run("valid configuration", func(t *testing.T) { + config := &Config{ + Store: getBadgerStore(t), + IssuerURL: "https://test.example.com", + Signing: types.SigningConfig{ + SigningCertPath: "/tmp/cert.pem", + SigningKeyPath: "/tmp/key.pem", + }, + Token: types.TokenConfig{ + AccessTokenLifetime: time.Hour, + RefreshTokenLifetime: 24 * time.Hour, + AuthorizationCodeLifetime: 10 * time.Minute, + }, + } + + err := validateConfig(config) + assert.NoError(t, err) + }) + + t.Run("missing store", func(t *testing.T) { + config := &Config{ + IssuerURL: "https://test.example.com", + Signing: types.SigningConfig{ + SigningCertPath: "/tmp/cert.pem", + SigningKeyPath: "/tmp/key.pem", + }, + } + + err := validateConfig(config) + assert.Error(t, err) + assert.Equal(t, types.ErrStoreMissing, err) + }) + + t.Run("missing issuer URL", func(t *testing.T) { + config := &Config{ + Store: getBadgerStore(t), + Signing: types.SigningConfig{ + SigningCertPath: "/tmp/cert.pem", + SigningKeyPath: "/tmp/key.pem", + }, + } + + err := validateConfig(config) + assert.Error(t, err) + assert.Equal(t, types.ErrIssuerURLMissing, err) + }) + + t.Run("missing certificate configuration", func(t *testing.T) { + config := &Config{ + Store: getBadgerStore(t), + IssuerURL: "https://test.example.com", + Signing: types.SigningConfig{}, + } + + err := validateConfig(config) + assert.Error(t, err) + assert.Equal(t, types.ErrCertificateMissing, err) + }) + + t.Run("invalid token lifetime", func(t *testing.T) { + config := &Config{ + Store: getBadgerStore(t), + IssuerURL: "https://test.example.com", + Signing: types.SigningConfig{ + SigningCertPath: "/tmp/cert.pem", + SigningKeyPath: "/tmp/key.pem", + }, + Token: types.TokenConfig{ + AccessTokenLifetime: -1 * time.Hour, + }, + } + + err := validateConfig(config) + assert.Error(t, err) + assert.Equal(t, types.ErrInvalidTokenLifetime, err) + }) +} + +func TestDefaultConfigValues(t *testing.T) { + t.Run("test all default values", func(t *testing.T) { + config := &Config{} + + err := setConfigDefaults(config) + assert.NoError(t, err) + + // Test signing defaults + assert.Equal(t, "RS256", config.Signing.SigningAlgorithm) + + // Test token defaults + assert.Equal(t, time.Hour, config.Token.AccessTokenLifetime) + assert.Equal(t, 24*time.Hour, config.Token.RefreshTokenLifetime) + assert.Equal(t, 10*time.Minute, config.Token.AuthorizationCodeLifetime) + assert.Equal(t, 15*time.Minute, config.Token.DeviceCodeLifetime) + assert.Equal(t, "jwt", config.Token.AccessTokenFormat) + assert.Equal(t, "opaque", config.Token.RefreshTokenFormat) + + // Test security defaults + assert.Equal(t, []string{"S256"}, config.Security.PKCECodeChallengeMethod) + assert.Equal(t, 128, config.Security.PKCECodeVerifierLength) + assert.Equal(t, 10*time.Minute, config.Security.StateParameterLifetime) + assert.Equal(t, 32, config.Security.StateParameterLength) + + // Test client defaults + assert.Equal(t, types.ClientTypeConfidential, config.Client.DefaultClientType) + assert.Equal(t, "client_secret_basic", config.Client.DefaultTokenEndpointAuthMethod) + assert.Equal(t, []string{"authorization_code", "refresh_token"}, config.Client.DefaultGrantTypes) + assert.Equal(t, []string{"code"}, config.Client.DefaultResponseTypes) + assert.Equal(t, 32, config.Client.ClientIDLength) + assert.Equal(t, 64, config.Client.ClientSecretLength) + + // Test feature flags defaults + assert.True(t, config.Features.OAuth21Enabled) + assert.True(t, config.Features.PKCEEnforced) + assert.True(t, config.Features.RefreshTokenRotationEnabled) + }) +}