test: implement mock OAuth2 server infrastructure for auth testing
- Add MockOAuth2ServerContainer using testcontainers - Create conftest_oauth.py with OAuth test fixtures - Add comprehensive OAuth integration tests - Support both mock (default) and real (CI secrets) OAuth modes - Add documentation for OAuth testing setup Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
"""
|
||||
Pytest fixtures for OAuth/OIDC testing.
|
||||
|
||||
Provides fixtures for:
|
||||
- Mock OAuth2 server (using testcontainers)
|
||||
- Real OAuth credentials (from environment/GitHub Actions secrets)
|
||||
- OAuth test helpers
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Dict, Generator, Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.mock_oauth_server import MockOAuth2ServerContainer, create_test_userinfo
|
||||
|
||||
# Check if we should use real OAuth credentials from environment
|
||||
_REAL_OAUTH_AVAILABLE = all([
|
||||
os.environ.get("AUTHENTIK_CLIENT_ID") not in {"", "NOT_SET", "test-key", None},
|
||||
os.environ.get("AUTHENTIK_CLIENT_SECRET") not in {"", "NOT_SET", "test-key", None},
|
||||
os.environ.get("AUTHENTIK_CONFIG_URL") not in {"", "NOT_SET", "test-key", None},
|
||||
])
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def use_real_oauth() -> bool:
|
||||
"""
|
||||
Determine if tests should use real OAuth credentials.
|
||||
|
||||
Returns True if valid OAuth credentials are available in the environment
|
||||
(typically from GitHub Actions secrets).
|
||||
|
||||
Returns:
|
||||
bool: True if real OAuth should be used, False for mock
|
||||
"""
|
||||
# Can be overridden with environment variable
|
||||
if os.environ.get("USE_REAL_OAUTH", "").lower() in ("true", "1", "yes"):
|
||||
return True
|
||||
if os.environ.get("USE_MOCK_OAUTH", "").lower() in ("true", "1", "yes"):
|
||||
return False
|
||||
|
||||
return _REAL_OAUTH_AVAILABLE
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def mock_oauth_server() -> Generator[MockOAuth2ServerContainer, None, None]:
|
||||
"""
|
||||
Provide a mock OAuth2/OIDC server for testing.
|
||||
|
||||
This fixture starts a mock-oauth2-server container that provides
|
||||
a complete OIDC provider with all necessary endpoints.
|
||||
|
||||
Yields:
|
||||
MockOAuth2ServerContainer: Running mock OAuth server
|
||||
"""
|
||||
# Only start if we're not using real OAuth
|
||||
if not _REAL_OAUTH_AVAILABLE or os.environ.get("USE_MOCK_OAUTH", "").lower() in ("true", "1", "yes"):
|
||||
container = MockOAuth2ServerContainer()
|
||||
container.start()
|
||||
|
||||
try:
|
||||
# Wait for the server to be ready
|
||||
container.wait_for_ready()
|
||||
yield container
|
||||
finally:
|
||||
container.stop()
|
||||
else:
|
||||
pytest.skip("Using real OAuth credentials, mock server not needed")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def oauth_config(mock_oauth_server: Optional[MockOAuth2ServerContainer], use_real_oauth: bool) -> Dict[str, str]:
|
||||
"""
|
||||
Provide OAuth configuration for tests.
|
||||
|
||||
Returns either mock OAuth config or real OAuth config based on availability.
|
||||
|
||||
Args:
|
||||
mock_oauth_server: Mock OAuth server fixture (may be None if using real)
|
||||
use_real_oauth: Whether to use real OAuth credentials
|
||||
|
||||
Returns:
|
||||
Dictionary with OAuth configuration
|
||||
"""
|
||||
if use_real_oauth and _REAL_OAUTH_AVAILABLE:
|
||||
# Use real OAuth credentials from environment
|
||||
return {
|
||||
"client_id": os.environ["AUTHENTIK_CLIENT_ID"],
|
||||
"client_secret": os.environ["AUTHENTIK_CLIENT_SECRET"],
|
||||
"server_metadata_url": os.environ["AUTHENTIK_CONFIG_URL"],
|
||||
"issuer": os.environ["AUTHENTIK_CONFIG_URL"].replace("/.well-known/openid-configuration", ""),
|
||||
"mode": "real",
|
||||
}
|
||||
else:
|
||||
# Use mock OAuth server
|
||||
if mock_oauth_server is None:
|
||||
pytest.fail("Mock OAuth server not available and real credentials not configured")
|
||||
|
||||
config = mock_oauth_server.get_config()
|
||||
return {
|
||||
"client_id": "test-client-id",
|
||||
"client_secret": "test-client-secret",
|
||||
"server_metadata_url": config["well_known_url"],
|
||||
"issuer": config["issuer"],
|
||||
"token_endpoint": config["token_endpoint"],
|
||||
"authorization_endpoint": config["authorization_endpoint"],
|
||||
"userinfo_endpoint": config["userinfo_endpoint"],
|
||||
"jwks_uri": config["jwks_uri"],
|
||||
"mode": "mock",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_user_info() -> Dict:
|
||||
"""
|
||||
Provide test user information for OAuth flows.
|
||||
|
||||
Returns:
|
||||
Dictionary with test user claims
|
||||
"""
|
||||
return create_test_userinfo(
|
||||
sub="test-user-123",
|
||||
email="testuser@example.com",
|
||||
name="Test User",
|
||||
preferred_username="testuser",
|
||||
groups=["admin"],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def oauth_test_token(
|
||||
mock_oauth_server: Optional[MockOAuth2ServerContainer],
|
||||
test_user_info: Dict,
|
||||
use_real_oauth: bool,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Generate a test OAuth token.
|
||||
|
||||
For mock mode: Creates a valid JWT from the mock server.
|
||||
For real mode: Skips (would need real authentication flow).
|
||||
|
||||
Args:
|
||||
mock_oauth_server: Mock OAuth server
|
||||
test_user_info: User information to include in token
|
||||
use_real_oauth: Whether using real OAuth
|
||||
|
||||
Returns:
|
||||
JWT token string or None if using real OAuth
|
||||
"""
|
||||
if use_real_oauth:
|
||||
# Can't generate tokens for real OAuth - would need actual auth flow
|
||||
return None
|
||||
|
||||
if mock_oauth_server is None:
|
||||
pytest.fail("Mock OAuth server not available")
|
||||
|
||||
# Create a token with the test user info
|
||||
return mock_oauth_server.create_token(
|
||||
subject=test_user_info["sub"],
|
||||
claims={
|
||||
"email": test_user_info["email"],
|
||||
"name": test_user_info["name"],
|
||||
"preferred_username": test_user_info["preferred_username"],
|
||||
"groups": test_user_info["groups"],
|
||||
},
|
||||
audience="test-client-id",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def oauth_enabled_app(oauth_config: Dict[str, str]):
|
||||
"""
|
||||
Configure the FastAPI app with OAuth enabled for testing.
|
||||
|
||||
This fixture temporarily enables OAuth and configures it with the
|
||||
test OAuth provider (mock or real).
|
||||
|
||||
Args:
|
||||
oauth_config: OAuth configuration
|
||||
|
||||
Yields:
|
||||
Configured test client
|
||||
"""
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
# Save original values
|
||||
original_auth_enabled = os.environ.get("AUTH_ENABLED")
|
||||
original_client_id = os.environ.get("AUTHENTIK_CLIENT_ID")
|
||||
original_client_secret = os.environ.get("AUTHENTIK_CLIENT_SECRET")
|
||||
original_config_url = os.environ.get("AUTHENTIK_CONFIG_URL")
|
||||
|
||||
try:
|
||||
# Enable auth and configure OAuth
|
||||
os.environ["AUTH_ENABLED"] = "True"
|
||||
os.environ["AUTHENTIK_CLIENT_ID"] = oauth_config["client_id"]
|
||||
os.environ["AUTHENTIK_CLIENT_SECRET"] = oauth_config["client_secret"]
|
||||
os.environ["AUTHENTIK_CONFIG_URL"] = oauth_config["server_metadata_url"]
|
||||
|
||||
# Need to reload the app module to pick up new config
|
||||
import importlib
|
||||
from app import auth
|
||||
importlib.reload(auth)
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from app.main import app
|
||||
|
||||
# Create test client
|
||||
client = TestClient(app)
|
||||
|
||||
yield client
|
||||
|
||||
finally:
|
||||
# Restore original values
|
||||
if original_auth_enabled is not None:
|
||||
os.environ["AUTH_ENABLED"] = original_auth_enabled
|
||||
else:
|
||||
os.environ.pop("AUTH_ENABLED", None)
|
||||
|
||||
if original_client_id is not None:
|
||||
os.environ["AUTHENTIK_CLIENT_ID"] = original_client_id
|
||||
else:
|
||||
os.environ.pop("AUTHENTIK_CLIENT_ID", None)
|
||||
|
||||
if original_client_secret is not None:
|
||||
os.environ["AUTHENTIK_CLIENT_SECRET"] = original_client_secret
|
||||
else:
|
||||
os.environ.pop("AUTHENTIK_CLIENT_SECRET", None)
|
||||
|
||||
if original_config_url is not None:
|
||||
os.environ["AUTHENTIK_CONFIG_URL"] = original_config_url
|
||||
else:
|
||||
os.environ.pop("AUTHENTIK_CONFIG_URL", None)
|
||||
|
||||
# Reload auth module to restore original state
|
||||
import importlib
|
||||
from app import auth
|
||||
importlib.reload(auth)
|
||||
Reference in New Issue
Block a user