yao/openapi/oauth/oauth_test.go
Max 966e0cfd00 Enhance OAuth test setup with reusable certificate management
- Introduced global test certificate paths to avoid redundant certificate generation across tests, improving efficiency.
- Implemented a function to create temporary certificates once for all tests, ensuring consistent usage of signing certificates.
- Updated test configurations to utilize the new certificate management, enhancing clarity and maintainability.
- Added cleanup functionality for global test certificates to ensure proper resource management after tests.
2025-07-21 19:44:00 +08:00

1019 lines
31 KiB
Go

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
// Test certificate paths - created once and reused across tests
var (
testCertPath string
testKeyPath string
)
// 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://localhost/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://localhost/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://localhost/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 test certificates once if not already created
if testCertPath == "" || testKeyPath == "" {
createTestCertificatesOnce(t)
}
// Create OAuth service configuration
oauthConfig := &Config{
Store: mainStore,
Cache: cache,
Signing: types.SigningConfig{
SigningAlgorithm: "RS256",
SigningCertPath: testCertPath,
SigningKeyPath: testKeyPath,
},
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)
}
}
// createTestCertificatesOnce creates temporary certificate pair for all tests
func createTestCertificatesOnce(t *testing.T) {
// Generate temporary certificates (auto-generate with empty paths)
config := &types.SigningConfig{
SigningAlgorithm: "RS256",
SigningCertPath: "",
SigningKeyPath: "",
}
certs, err := LoadSigningCertificates(config)
if err != nil {
t.Fatalf("Failed to generate test certificates: %v", err)
}
testCertPath = certs.SigningCertPath
testKeyPath = certs.SigningKeyPath
t.Logf("Created test certificates: cert=%s, key=%s", testCertPath, testKeyPath)
}
// 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()
// Cleanup global test certificates
cleanupGlobalTestCertificates()
os.Exit(code)
}
// cleanupGlobalTestCertificates removes global test certificates
func cleanupGlobalTestCertificates() {
if testCertPath != "" {
if _, err := os.Stat(testCertPath); !os.IsNotExist(err) {
os.Remove(testCertPath)
}
}
if testKeyPath != "" {
if _, err := os.Stat(testKeyPath); !os.IsNotExist(err) {
os.Remove(testKeyPath)
}
}
}
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: testCertPath,
SigningKeyPath: testKeyPath,
},
}
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: testCertPath,
SigningKeyPath: testKeyPath,
},
}
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: testCertPath,
SigningKeyPath: testKeyPath,
},
}
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: testCertPath,
SigningKeyPath: testKeyPath,
},
}
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: testCertPath,
SigningKeyPath: testKeyPath,
},
}
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: testCertPath,
SigningKeyPath: testKeyPath,
},
Token: types.TokenConfig{
AccessTokenLifetime: time.Hour,
RefreshTokenLifetime: 24 * time.Hour,
AuthorizationCodeLifetime: 10 * time.Minute,
},
}
// Set defaults first (like NewService does)
err := setConfigDefaults(config)
assert.NoError(t, err)
// Then validate
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: testCertPath,
SigningKeyPath: testKeyPath,
},
}
// Set defaults first (like NewService does)
err := setConfigDefaults(config)
assert.NoError(t, err)
// Then validate
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: testCertPath,
SigningKeyPath: testKeyPath,
},
}
// Set defaults first (like NewService does)
err := setConfigDefaults(config)
assert.NoError(t, err)
// Then validate
err = validateConfig(config)
assert.Error(t, err)
assert.Equal(t, types.ErrIssuerURLMissing, err)
})
t.Run("partial certificate configuration", func(t *testing.T) {
config := &Config{
Store: getBadgerStore(t),
IssuerURL: "https://test.example.com",
Signing: types.SigningConfig{
SigningCertPath: testCertPath, // Only cert path, missing key path
SigningKeyPath: "",
},
}
// Set defaults first (like NewService does)
err := setConfigDefaults(config)
assert.NoError(t, err)
// Then validate
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: testCertPath,
SigningKeyPath: testKeyPath,
},
Token: types.TokenConfig{
AccessTokenLifetime: -1 * time.Hour, // Invalid negative lifetime
},
}
// Set defaults first (like NewService does)
err := setConfigDefaults(config)
assert.NoError(t, err)
// Then validate
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)
})
}