"""Test service selection OAuth flow using standardized framework."""
import pytest
from .base_test_config import TEST_EMAIL
@pytest.mark.service("auth")
class TestServiceSelection:
"""Tests for service selection OAuth flow functionality."""
@pytest.mark.asyncio
async def test_service_catalog_available(self, client):
"""Test that service catalog can be retrieved."""
from auth.scope_registry import ScopeRegistry
# Test the new service catalog method
catalog = ScopeRegistry.get_service_catalog()
# Validate catalog structure
assert isinstance(catalog, dict), "Service catalog should be a dictionary"
assert len(catalog) > 0, "Service catalog should not be empty"
# Check required services are present
assert "userinfo" in catalog, "Basic Profile service should be in catalog"
assert "drive" in catalog, "Google Drive should be in catalog"
assert "gmail" in catalog, "Gmail should be in catalog"
# Validate service structure
for service_key, service_info in catalog.items():
assert "name" in service_info, f"Service {service_key} should have name"
assert (
"description" in service_info
), f"Service {service_key} should have description"
assert (
"category" in service_info
), f"Service {service_key} should have category"
assert (
"required" in service_info
), f"Service {service_key} should have required flag"
assert "scopes" in service_info, f"Service {service_key} should have scopes"
assert isinstance(
service_info["scopes"], list
), f"Service {service_key} scopes should be a list"
@pytest.mark.asyncio
async def test_scopes_for_services_combination(self, client):
"""Test that scope combination works correctly for selected services."""
from auth.scope_registry import ScopeRegistry
# Test with multiple services
selected_services = ["drive", "gmail", "calendar"]
combined_scopes = ScopeRegistry.get_scopes_for_services(selected_services)
# Validate combined scopes
assert isinstance(combined_scopes, list), "Combined scopes should be a list"
assert len(combined_scopes) > 0, "Combined scopes should not be empty"
# Should include base scopes (required)
base_scopes = ScopeRegistry.resolve_scope_group("base")
for base_scope in base_scopes:
assert (
base_scope in combined_scopes
), f"Base scope {base_scope} should be included"
# Should include service-specific scopes
drive_scopes = ScopeRegistry.get_service_scopes("drive", "basic")
gmail_scopes = ScopeRegistry.get_service_scopes("gmail", "basic")
calendar_scopes = ScopeRegistry.get_service_scopes("calendar", "basic")
# At least some service scopes should be present
drive_present = any(scope in combined_scopes for scope in drive_scopes)
gmail_present = any(scope in combined_scopes for scope in gmail_scopes)
calendar_present = any(scope in combined_scopes for scope in calendar_scopes)
assert drive_present, "Drive scopes should be included"
assert gmail_present, "Gmail scopes should be included"
assert calendar_present, "Calendar scopes should be included"
@pytest.mark.asyncio
async def test_oauth_with_service_selection_flow(self, client):
"""Test OAuth flow with service selection (without actual authentication)."""
from auth.google_auth import _create_service_selection_url, initiate_oauth_flow
test_email = TEST_EMAIL
# Test service selection URL creation
selection_url = await _create_service_selection_url(test_email, "custom")
# Validate URL structure
assert isinstance(selection_url, str), "Selection URL should be a string"
assert (
"/auth/services/select" in selection_url
), "URL should contain service selection path"
assert "state=" in selection_url, "URL should contain state parameter"
assert "flow_type=" in selection_url, "URL should contain flow_type parameter"
# Test OAuth flow with service selection enabled
oauth_url = await initiate_oauth_flow(
user_email=test_email,
service_name="Test Service",
show_service_selection=True,
)
# Should return service selection URL when no services pre-selected
assert (
"/auth/services/select" in oauth_url
), "Should return service selection URL"
# Test OAuth flow with pre-selected services
oauth_url_direct = await initiate_oauth_flow(
user_email=test_email,
service_name="Test Service",
selected_services=["drive", "gmail"],
show_service_selection=False,
)
# Should return Google OAuth URL when services pre-selected
assert (
"accounts.google.com" in oauth_url_direct
), "Should return Google OAuth URL for pre-selected services"
@pytest.mark.asyncio
async def test_service_selection_callback_handling(self, client):
"""Test service selection callback handling."""
from auth.google_auth import (
_create_service_selection_url,
_service_selection_cache,
handle_service_selection_callback,
)
test_email = TEST_EMAIL
# Create a service selection URL (which populates the cache)
selection_url = await _create_service_selection_url(test_email, "custom")
# Extract state from URL
import urllib.parse
parsed_url = urllib.parse.urlparse(selection_url)
query_params = urllib.parse.parse_qs(parsed_url.query)
state = query_params.get("state", [None])[0]
assert state is not None, "State should be present in selection URL"
assert state in _service_selection_cache, "State should be in selection cache"
# Test handling service selection callback
selected_services = ["drive", "gmail", "calendar"]
try:
oauth_url = await handle_service_selection_callback(
state, selected_services
)
# Should return a Google OAuth URL
assert isinstance(oauth_url, str), "Callback should return OAuth URL"
assert "accounts.google.com" in oauth_url, "Should return Google OAuth URL"
# State should be consumed (removed from cache)
assert (
state not in _service_selection_cache
), "State should be removed from cache after use"
except Exception as e:
# This is expected in test environment without full OAuth setup
assert "GoogleAuthError" in str(type(e)) or "OAuth" in str(
e
), f"Should get OAuth-related error: {e}"
@pytest.mark.asyncio
async def test_service_selection_cache_cleanup(self, client):
"""Test that service selection cache cleans up expired entries."""
from datetime import datetime, timedelta
from auth.google_auth import (
_cleanup_service_selection_cache,
_service_selection_cache,
)
# Add an expired entry manually
expired_state = "expired_test_state"
expired_time = datetime.now() - timedelta(
minutes=35
) # 35 minutes ago (should be cleaned)
_service_selection_cache[expired_state] = {
"user_email": TEST_EMAIL,
"flow_type": "test",
"timestamp": expired_time.isoformat(),
}
# Add a fresh entry
fresh_state = "fresh_test_state"
_service_selection_cache[fresh_state] = {
"user_email": TEST_EMAIL,
"flow_type": "test",
"timestamp": datetime.now().isoformat(),
}
assert (
len(_service_selection_cache) >= 2
), "Cache should have at least 2 entries"
# Run cleanup
_cleanup_service_selection_cache()
# Expired entry should be removed, fresh entry should remain
assert (
expired_state not in _service_selection_cache
), "Expired entry should be removed"
assert fresh_state in _service_selection_cache, "Fresh entry should remain"
# Clean up test data
_service_selection_cache.pop(fresh_state, None)
@pytest.mark.asyncio
async def test_auth_middleware_service_selection_integration(self, client):
"""Test AuthMiddleware service selection integration."""
# This tests the context functions we added
from auth.context import (
get_google_provider,
set_google_provider,
)
from auth.middleware import AuthMiddleware
# Test GoogleProvider context management
test_provider = "test_provider_instance"
set_google_provider(test_provider)
retrieved_provider = get_google_provider()
assert (
retrieved_provider == test_provider
), "Should retrieve the same GoogleProvider instance"
# Test AuthMiddleware service selection methods
middleware = AuthMiddleware()
# Test enable/disable service selection
middleware.enable_service_selection(True)
assert (
middleware._enable_service_selection == True
), "Service selection should be enabled"
middleware.enable_service_selection(False)
assert (
middleware._enable_service_selection == False
), "Service selection should be disabled"
# Clean up test data
set_google_provider(None)