b7f52be4c9
CI / Run CI (push) Has been cancelled
CI / check-backend (push) Has been cancelled
CI / check-frontend (push) Has been cancelled
CI / tests (push) Has been cancelled
CI / e2e-tests (push) Has been cancelled
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled
1243 lines
46 KiB
Python
1243 lines
46 KiB
Python
import os
|
|
from unittest.mock import AsyncMock, Mock, patch
|
|
|
|
import httpx
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
from chainlit.oauth_providers import (
|
|
ACCESS_TOKEN_MISSING,
|
|
Auth0OAuthProvider,
|
|
AWSCognitoOAuthProvider,
|
|
AzureADHybridOAuthProvider,
|
|
AzureADOAuthProvider,
|
|
DescopeOAuthProvider,
|
|
GenericOAuthProvider,
|
|
GithubOAuthProvider,
|
|
GitlabOAuthProvider,
|
|
GoogleOAuthProvider,
|
|
KeycloakOAuthProvider,
|
|
OAuthProvider,
|
|
OktaOAuthProvider,
|
|
get_configured_oauth_providers,
|
|
get_oauth_provider,
|
|
)
|
|
from chainlit.user import User
|
|
|
|
|
|
class TestOAuthProviderBase:
|
|
"""Test suite for OAuthProvider base class."""
|
|
|
|
def test_oauth_provider_has_required_methods(self):
|
|
"""Test that OAuthProvider defines required methods."""
|
|
provider = OAuthProvider()
|
|
|
|
# These should be methods
|
|
assert hasattr(provider, "is_configured")
|
|
assert hasattr(provider, "get_raw_token_response")
|
|
assert hasattr(provider, "get_token")
|
|
assert hasattr(provider, "get_user_info")
|
|
assert hasattr(provider, "get_env_prefix")
|
|
assert hasattr(provider, "get_prompt")
|
|
|
|
def test_oauth_provider_is_configured_returns_false_when_env_missing(self):
|
|
"""Test is_configured returns False when environment variables are missing."""
|
|
provider = OAuthProvider()
|
|
provider.env = ["MISSING_VAR_1", "MISSING_VAR_2"]
|
|
|
|
assert provider.is_configured() is False
|
|
|
|
def test_oauth_provider_is_configured_returns_true_when_env_present(self):
|
|
"""Test is_configured returns True when all environment variables are present."""
|
|
provider = OAuthProvider()
|
|
provider.env = ["TEST_VAR_1", "TEST_VAR_2"]
|
|
|
|
with patch.dict(os.environ, {"TEST_VAR_1": "value1", "TEST_VAR_2": "value2"}):
|
|
assert provider.is_configured() is True
|
|
|
|
def test_oauth_provider_get_env_prefix(self):
|
|
"""Test get_env_prefix converts id to uppercase with underscores."""
|
|
provider = OAuthProvider()
|
|
provider.id = "azure-ad"
|
|
|
|
assert provider.get_env_prefix() == "AZURE_AD"
|
|
|
|
def test_oauth_provider_get_prompt_returns_provider_specific(self):
|
|
"""Test get_prompt returns provider-specific prompt."""
|
|
provider = OAuthProvider()
|
|
provider.id = "github"
|
|
provider.default_prompt = None
|
|
|
|
with patch.dict(os.environ, {"OAUTH_GITHUB_PROMPT": "consent"}):
|
|
assert provider.get_prompt() == "consent"
|
|
|
|
def test_oauth_provider_get_prompt_returns_global(self):
|
|
"""Test get_prompt returns global prompt when provider-specific not set."""
|
|
provider = OAuthProvider()
|
|
provider.id = "github"
|
|
provider.default_prompt = None
|
|
|
|
with patch.dict(os.environ, {"OAUTH_PROMPT": "select_account"}):
|
|
assert provider.get_prompt() == "select_account"
|
|
|
|
def test_oauth_provider_get_prompt_returns_default(self):
|
|
"""Test get_prompt returns default when no env vars set."""
|
|
provider = OAuthProvider()
|
|
provider.id = "github"
|
|
provider.default_prompt = "login"
|
|
|
|
assert provider.get_prompt() == "login"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_oauth_provider_abstract_methods_raise_not_implemented(self):
|
|
"""Test that abstract methods raise NotImplementedError."""
|
|
provider = OAuthProvider()
|
|
|
|
with pytest.raises(NotImplementedError):
|
|
await provider.get_raw_token_response("code", "url")
|
|
|
|
with pytest.raises(NotImplementedError):
|
|
await provider.get_token("code", "url")
|
|
|
|
with pytest.raises(NotImplementedError):
|
|
await provider.get_user_info("token")
|
|
|
|
|
|
class TestGithubOAuthProvider:
|
|
"""Test suite for GithubOAuthProvider."""
|
|
|
|
def test_github_provider_initialization(self):
|
|
"""Test GithubOAuthProvider initialization."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_GITHUB_CLIENT_ID": "test_client_id",
|
|
"OAUTH_GITHUB_CLIENT_SECRET": "test_secret",
|
|
},
|
|
):
|
|
provider = GithubOAuthProvider()
|
|
|
|
assert provider.id == "github"
|
|
assert provider.client_id == "test_client_id"
|
|
assert provider.client_secret == "test_secret"
|
|
assert "scope" in provider.authorize_params
|
|
assert provider.authorize_params["scope"] == "user:email"
|
|
|
|
def test_github_provider_with_custom_urls(self):
|
|
"""Test GithubOAuthProvider with custom URLs."""
|
|
# Need to set env vars before importing/instantiating since they're class-level
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_GITHUB_CLIENT_ID": "test_id",
|
|
"OAUTH_GITHUB_CLIENT_SECRET": "test_secret",
|
|
"OAUTH_GITHUB_AUTH_URL": "https://custom.github.com/oauth/authorize",
|
|
"OAUTH_GITHUB_TOKEN_URL": "https://custom.github.com/oauth/token",
|
|
"OAUTH_GITHUB_USER_INFO_URL": "https://custom.github.com/api/user",
|
|
},
|
|
clear=False,
|
|
):
|
|
# Re-import to get the updated class-level attributes
|
|
from importlib import reload
|
|
|
|
import chainlit.oauth_providers as oauth_module
|
|
|
|
reload(oauth_module)
|
|
|
|
provider = oauth_module.GithubOAuthProvider()
|
|
|
|
assert provider.authorize_url == "https://custom.github.com/oauth/authorize"
|
|
assert provider.token_url == "https://custom.github.com/oauth/token"
|
|
assert provider.user_info_url == "https://custom.github.com/api/user"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_github_get_raw_token_response_success(self):
|
|
"""Test GitHub get_raw_token_response with successful response."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_GITHUB_CLIENT_ID": "test_id",
|
|
"OAUTH_GITHUB_CLIENT_SECRET": "test_secret",
|
|
},
|
|
):
|
|
provider = GithubOAuthProvider()
|
|
|
|
mock_response = Mock()
|
|
mock_response.text = "access_token=test_token&token_type=bearer"
|
|
mock_response.raise_for_status = Mock()
|
|
|
|
with patch("httpx.AsyncClient") as mock_client:
|
|
mock_client.return_value.__aenter__.return_value.post = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
result = await provider.get_raw_token_response(
|
|
"test_code", "http://localhost"
|
|
)
|
|
|
|
assert "access_token" in result
|
|
assert result["access_token"][0] == "test_token"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_github_get_token_success(self):
|
|
"""Test GitHub get_token with successful response."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_GITHUB_CLIENT_ID": "test_id",
|
|
"OAUTH_GITHUB_CLIENT_SECRET": "test_secret",
|
|
},
|
|
):
|
|
provider = GithubOAuthProvider()
|
|
|
|
mock_response = Mock()
|
|
mock_response.text = "access_token=github_token_123&token_type=bearer"
|
|
mock_response.raise_for_status = Mock()
|
|
|
|
with patch("httpx.AsyncClient") as mock_client:
|
|
mock_client.return_value.__aenter__.return_value.post = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
token = await provider.get_token("test_code", "http://localhost")
|
|
|
|
assert token == "github_token_123"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_github_get_token_missing_access_token(self):
|
|
"""Test GitHub get_token raises error when access_token is missing."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_GITHUB_CLIENT_ID": "test_id",
|
|
"OAUTH_GITHUB_CLIENT_SECRET": "test_secret",
|
|
},
|
|
):
|
|
provider = GithubOAuthProvider()
|
|
|
|
mock_response = Mock()
|
|
mock_response.text = "error=invalid_grant"
|
|
mock_response.raise_for_status = Mock()
|
|
|
|
with patch("httpx.AsyncClient") as mock_client:
|
|
mock_client.return_value.__aenter__.return_value.post = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await provider.get_token("test_code", "http://localhost")
|
|
|
|
assert exc_info.value.status_code == 400
|
|
assert ACCESS_TOKEN_MISSING in str(exc_info.value.detail)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_github_get_user_info_success(self):
|
|
"""Test GitHub get_user_info with successful response."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_GITHUB_CLIENT_ID": "test_id",
|
|
"OAUTH_GITHUB_CLIENT_SECRET": "test_secret",
|
|
},
|
|
):
|
|
provider = GithubOAuthProvider()
|
|
|
|
mock_user_response = Mock()
|
|
mock_user_response.json.return_value = {
|
|
"login": "testuser",
|
|
"avatar_url": "https://github.com/avatar.png",
|
|
"email": "test@example.com",
|
|
}
|
|
mock_user_response.raise_for_status = Mock()
|
|
|
|
mock_emails_response = Mock()
|
|
mock_emails_response.json.return_value = [
|
|
{"email": "test@example.com", "primary": True, "verified": True}
|
|
]
|
|
mock_emails_response.raise_for_status = Mock()
|
|
|
|
with patch("httpx.AsyncClient") as mock_client:
|
|
mock_get = AsyncMock(
|
|
side_effect=[mock_user_response, mock_emails_response]
|
|
)
|
|
mock_client.return_value.__aenter__.return_value.get = mock_get
|
|
|
|
github_user, user = await provider.get_user_info("test_token")
|
|
|
|
assert github_user["login"] == "testuser"
|
|
assert "emails" in github_user
|
|
assert isinstance(user, User)
|
|
assert user.identifier == "testuser"
|
|
assert user.metadata["provider"] == "github"
|
|
assert user.metadata["image"] == "https://github.com/avatar.png"
|
|
|
|
|
|
class TestGoogleOAuthProvider:
|
|
"""Test suite for GoogleOAuthProvider."""
|
|
|
|
def test_google_provider_initialization(self):
|
|
"""Test GoogleOAuthProvider initialization."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_GOOGLE_CLIENT_ID": "google_client_id",
|
|
"OAUTH_GOOGLE_CLIENT_SECRET": "google_secret",
|
|
},
|
|
):
|
|
provider = GoogleOAuthProvider()
|
|
|
|
assert provider.id == "google"
|
|
assert provider.client_id == "google_client_id"
|
|
assert provider.client_secret == "google_secret"
|
|
assert (
|
|
provider.authorize_url == "https://accounts.google.com/o/oauth2/v2/auth"
|
|
)
|
|
assert "scope" in provider.authorize_params
|
|
assert "userinfo.profile" in provider.authorize_params["scope"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_google_get_token_success(self):
|
|
"""Test Google get_token with successful response."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_GOOGLE_CLIENT_ID": "google_id",
|
|
"OAUTH_GOOGLE_CLIENT_SECRET": "google_secret",
|
|
},
|
|
):
|
|
provider = GoogleOAuthProvider()
|
|
|
|
mock_response = Mock()
|
|
mock_response.json.return_value = {
|
|
"access_token": "google_access_token",
|
|
"token_type": "Bearer",
|
|
}
|
|
mock_response.raise_for_status = Mock()
|
|
|
|
with patch("httpx.AsyncClient") as mock_client:
|
|
mock_client.return_value.__aenter__.return_value.post = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
token = await provider.get_token(
|
|
"auth_code", "http://localhost/callback"
|
|
)
|
|
|
|
assert token == "google_access_token"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_google_get_user_info_success(self):
|
|
"""Test Google get_user_info with successful response."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_GOOGLE_CLIENT_ID": "google_id",
|
|
"OAUTH_GOOGLE_CLIENT_SECRET": "google_secret",
|
|
},
|
|
):
|
|
provider = GoogleOAuthProvider()
|
|
|
|
mock_response = Mock()
|
|
mock_response.json.return_value = {
|
|
"email": "user@gmail.com",
|
|
"name": "Test User",
|
|
"picture": "https://google.com/photo.jpg",
|
|
}
|
|
mock_response.raise_for_status = Mock()
|
|
|
|
with patch("httpx.AsyncClient") as mock_client:
|
|
mock_client.return_value.__aenter__.return_value.get = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
google_user, user = await provider.get_user_info("test_token")
|
|
|
|
assert google_user["email"] == "user@gmail.com"
|
|
assert isinstance(user, User)
|
|
assert user.identifier == "user@gmail.com"
|
|
assert user.metadata["provider"] == "google"
|
|
assert user.metadata["image"] == "https://google.com/photo.jpg"
|
|
|
|
|
|
class TestAzureADOAuthProvider:
|
|
"""Test suite for AzureADOAuthProvider."""
|
|
|
|
def test_azure_ad_provider_initialization(self):
|
|
"""Test AzureADOAuthProvider initialization."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_AZURE_AD_CLIENT_ID": "azure_client_id",
|
|
"OAUTH_AZURE_AD_CLIENT_SECRET": "azure_secret",
|
|
"OAUTH_AZURE_AD_TENANT_ID": "tenant_123",
|
|
},
|
|
):
|
|
provider = AzureADOAuthProvider()
|
|
|
|
assert provider.id == "azure-ad"
|
|
assert provider.client_id == "azure_client_id"
|
|
assert provider.client_secret == "azure_secret"
|
|
assert "tenant" in provider.authorize_params
|
|
assert provider.authorize_params["tenant"] == "tenant_123"
|
|
|
|
def test_azure_ad_single_tenant_urls(self):
|
|
"""Test Azure AD uses tenant-specific URLs when single tenant enabled."""
|
|
# Azure AD URLs are set at class definition time, need to reload module
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_AZURE_AD_CLIENT_ID": "azure_id",
|
|
"OAUTH_AZURE_AD_CLIENT_SECRET": "azure_secret",
|
|
"OAUTH_AZURE_AD_TENANT_ID": "tenant_abc",
|
|
"OAUTH_AZURE_AD_ENABLE_SINGLE_TENANT": "true",
|
|
},
|
|
clear=False,
|
|
):
|
|
from importlib import reload
|
|
|
|
import chainlit.oauth_providers as oauth_module
|
|
|
|
reload(oauth_module)
|
|
|
|
provider = oauth_module.AzureADOAuthProvider()
|
|
|
|
assert "tenant_abc" in provider.authorize_url
|
|
assert "tenant_abc" in provider.token_url
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_azure_ad_get_token_with_refresh_token(self):
|
|
"""Test Azure AD get_token stores refresh token."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_AZURE_AD_CLIENT_ID": "azure_id",
|
|
"OAUTH_AZURE_AD_CLIENT_SECRET": "azure_secret",
|
|
"OAUTH_AZURE_AD_TENANT_ID": "tenant_123",
|
|
},
|
|
):
|
|
provider = AzureADOAuthProvider()
|
|
|
|
mock_response = Mock()
|
|
mock_response.json.return_value = {
|
|
"access_token": "azure_access_token",
|
|
"refresh_token": "azure_refresh_token",
|
|
}
|
|
mock_response.raise_for_status = Mock()
|
|
|
|
with patch("httpx.AsyncClient") as mock_client:
|
|
mock_client.return_value.__aenter__.return_value.post = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
token = await provider.get_token(
|
|
"auth_code", "http://localhost/callback"
|
|
)
|
|
|
|
assert token == "azure_access_token"
|
|
assert provider._refresh_token == "azure_refresh_token"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_azure_ad_get_user_info_with_photo(self):
|
|
"""Test Azure AD get_user_info includes photo when available."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_AZURE_AD_CLIENT_ID": "azure_id",
|
|
"OAUTH_AZURE_AD_CLIENT_SECRET": "azure_secret",
|
|
"OAUTH_AZURE_AD_TENANT_ID": "tenant_123",
|
|
},
|
|
):
|
|
provider = AzureADOAuthProvider()
|
|
provider._refresh_token = "refresh_token_123"
|
|
|
|
mock_user_response = Mock()
|
|
mock_user_response.json.return_value = {
|
|
"userPrincipalName": "user@company.com",
|
|
"displayName": "Test User",
|
|
}
|
|
mock_user_response.raise_for_status = Mock()
|
|
|
|
mock_photo_response = Mock()
|
|
mock_photo_response.aread = AsyncMock(return_value=b"photo_data")
|
|
mock_photo_response.headers = {"Content-Type": "image/jpeg"}
|
|
|
|
with patch("httpx.AsyncClient") as mock_client:
|
|
mock_get = AsyncMock(
|
|
side_effect=[mock_user_response, mock_photo_response]
|
|
)
|
|
mock_client.return_value.__aenter__.return_value.get = mock_get
|
|
|
|
azure_user, user = await provider.get_user_info("test_token")
|
|
|
|
assert azure_user["userPrincipalName"] == "user@company.com"
|
|
assert "image" in azure_user
|
|
assert isinstance(user, User)
|
|
assert user.identifier == "user@company.com"
|
|
assert user.metadata["provider"] == "azure-ad"
|
|
assert user.metadata["refresh_token"] == "refresh_token_123"
|
|
|
|
|
|
class TestOktaOAuthProvider:
|
|
"""Test suite for OktaOAuthProvider."""
|
|
|
|
def test_okta_provider_initialization(self):
|
|
"""Test OktaOAuthProvider initialization."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_OKTA_CLIENT_ID": "okta_client_id",
|
|
"OAUTH_OKTA_CLIENT_SECRET": "okta_secret",
|
|
"OAUTH_OKTA_DOMAIN": "dev-12345.okta.com",
|
|
},
|
|
clear=False,
|
|
):
|
|
from importlib import reload
|
|
|
|
import chainlit.oauth_providers as oauth_module
|
|
|
|
reload(oauth_module)
|
|
|
|
provider = oauth_module.OktaOAuthProvider()
|
|
|
|
assert provider.id == "okta"
|
|
assert provider.client_id == "okta_client_id"
|
|
assert "dev-12345.okta.com" in provider.authorize_url
|
|
|
|
def test_okta_authorization_server_path_default(self):
|
|
"""Test Okta uses default authorization server."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_OKTA_CLIENT_ID": "okta_id",
|
|
"OAUTH_OKTA_CLIENT_SECRET": "okta_secret",
|
|
"OAUTH_OKTA_DOMAIN": "dev-12345.okta.com",
|
|
},
|
|
):
|
|
provider = OktaOAuthProvider()
|
|
|
|
assert provider.get_authorization_server_path() == "/default"
|
|
|
|
def test_okta_authorization_server_path_custom(self):
|
|
"""Test Okta uses custom authorization server."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_OKTA_CLIENT_ID": "okta_id",
|
|
"OAUTH_OKTA_CLIENT_SECRET": "okta_secret",
|
|
"OAUTH_OKTA_DOMAIN": "dev-12345.okta.com",
|
|
"OAUTH_OKTA_AUTHORIZATION_SERVER_ID": "custom_server",
|
|
},
|
|
):
|
|
provider = OktaOAuthProvider()
|
|
|
|
assert provider.get_authorization_server_path() == "/custom_server"
|
|
|
|
def test_okta_authorization_server_path_false(self):
|
|
"""Test Okta with no authorization server."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_OKTA_CLIENT_ID": "okta_id",
|
|
"OAUTH_OKTA_CLIENT_SECRET": "okta_secret",
|
|
"OAUTH_OKTA_DOMAIN": "dev-12345.okta.com",
|
|
"OAUTH_OKTA_AUTHORIZATION_SERVER_ID": "false",
|
|
},
|
|
):
|
|
provider = OktaOAuthProvider()
|
|
|
|
assert provider.get_authorization_server_path() == ""
|
|
|
|
|
|
class TestAuth0OAuthProvider:
|
|
"""Test suite for Auth0OAuthProvider."""
|
|
|
|
def test_auth0_provider_initialization(self):
|
|
"""Test Auth0OAuthProvider initialization."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_AUTH0_CLIENT_ID": "auth0_client_id",
|
|
"OAUTH_AUTH0_CLIENT_SECRET": "auth0_secret",
|
|
"OAUTH_AUTH0_DOMAIN": "dev-12345.auth0.com",
|
|
},
|
|
):
|
|
provider = Auth0OAuthProvider()
|
|
|
|
assert provider.id == "auth0"
|
|
assert provider.client_id == "auth0_client_id"
|
|
assert "dev-12345.auth0.com" in provider.domain
|
|
|
|
def test_auth0_with_original_domain(self):
|
|
"""Test Auth0 with separate original domain."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_AUTH0_CLIENT_ID": "auth0_id",
|
|
"OAUTH_AUTH0_CLIENT_SECRET": "auth0_secret",
|
|
"OAUTH_AUTH0_DOMAIN": "custom.domain.com",
|
|
"OAUTH_AUTH0_ORIGINAL_DOMAIN": "dev-12345.auth0.com",
|
|
},
|
|
):
|
|
provider = Auth0OAuthProvider()
|
|
|
|
assert "custom.domain.com" in provider.domain
|
|
assert "dev-12345.auth0.com" in provider.original_domain
|
|
|
|
|
|
class TestGenericOAuthProvider:
|
|
"""Test suite for GenericOAuthProvider."""
|
|
|
|
def test_generic_provider_initialization(self):
|
|
"""Test GenericOAuthProvider initialization."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_GENERIC_CLIENT_ID": "generic_id",
|
|
"OAUTH_GENERIC_CLIENT_SECRET": "generic_secret",
|
|
"OAUTH_GENERIC_AUTH_URL": "https://auth.example.com/oauth/authorize",
|
|
"OAUTH_GENERIC_TOKEN_URL": "https://auth.example.com/oauth/token",
|
|
"OAUTH_GENERIC_USER_INFO_URL": "https://auth.example.com/oauth/userinfo",
|
|
"OAUTH_GENERIC_SCOPES": "openid profile email",
|
|
},
|
|
):
|
|
provider = GenericOAuthProvider()
|
|
|
|
assert provider.id == "generic"
|
|
assert provider.client_id == "generic_id"
|
|
assert provider.authorize_url == "https://auth.example.com/oauth/authorize"
|
|
assert provider.token_url == "https://auth.example.com/oauth/token"
|
|
assert provider.user_info_url == "https://auth.example.com/oauth/userinfo"
|
|
|
|
def test_generic_provider_custom_name(self):
|
|
"""Test GenericOAuthProvider with custom name."""
|
|
# Generic provider id is set at class definition time
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_GENERIC_NAME": "my-custom-provider",
|
|
"OAUTH_GENERIC_CLIENT_ID": "generic_id",
|
|
"OAUTH_GENERIC_CLIENT_SECRET": "generic_secret",
|
|
"OAUTH_GENERIC_AUTH_URL": "https://auth.example.com/oauth/authorize",
|
|
"OAUTH_GENERIC_TOKEN_URL": "https://auth.example.com/oauth/token",
|
|
"OAUTH_GENERIC_USER_INFO_URL": "https://auth.example.com/oauth/userinfo",
|
|
"OAUTH_GENERIC_SCOPES": "openid profile",
|
|
},
|
|
clear=False,
|
|
):
|
|
from importlib import reload
|
|
|
|
import chainlit.oauth_providers as oauth_module
|
|
|
|
reload(oauth_module)
|
|
|
|
provider = oauth_module.GenericOAuthProvider()
|
|
|
|
assert provider.id == "my-custom-provider"
|
|
|
|
def test_generic_provider_custom_user_identifier(self):
|
|
"""Test GenericOAuthProvider with custom user identifier field."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_GENERIC_CLIENT_ID": "generic_id",
|
|
"OAUTH_GENERIC_CLIENT_SECRET": "generic_secret",
|
|
"OAUTH_GENERIC_AUTH_URL": "https://auth.example.com/oauth/authorize",
|
|
"OAUTH_GENERIC_TOKEN_URL": "https://auth.example.com/oauth/token",
|
|
"OAUTH_GENERIC_USER_INFO_URL": "https://auth.example.com/oauth/userinfo",
|
|
"OAUTH_GENERIC_SCOPES": "openid",
|
|
"OAUTH_GENERIC_USER_IDENTIFIER": "username",
|
|
},
|
|
):
|
|
provider = GenericOAuthProvider()
|
|
|
|
assert provider.user_identifier == "username"
|
|
|
|
|
|
class TestAzureADHybridOAuthProvider:
|
|
"""Test suite for AzureADHybridOAuthProvider."""
|
|
|
|
def test_azure_ad_hybrid_provider_initialization(self):
|
|
"""Test AzureADHybridOAuthProvider initialization."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_AZURE_AD_HYBRID_CLIENT_ID": "hybrid_client_id",
|
|
"OAUTH_AZURE_AD_HYBRID_CLIENT_SECRET": "hybrid_secret",
|
|
"OAUTH_AZURE_AD_HYBRID_TENANT_ID": "tenant_456",
|
|
},
|
|
):
|
|
provider = AzureADHybridOAuthProvider()
|
|
|
|
assert provider.id == "azure-ad-hybrid"
|
|
assert provider.client_id == "hybrid_client_id"
|
|
assert provider.client_secret == "hybrid_secret"
|
|
assert "tenant" in provider.authorize_params
|
|
assert provider.authorize_params["tenant"] == "tenant_456"
|
|
assert provider.authorize_params["response_type"] == "code id_token"
|
|
assert provider.authorize_params["response_mode"] == "form_post"
|
|
assert "nonce" in provider.authorize_params
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_azure_ad_hybrid_get_token_success(self):
|
|
"""Test AzureADHybrid get_token with successful response."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_AZURE_AD_HYBRID_CLIENT_ID": "hybrid_id",
|
|
"OAUTH_AZURE_AD_HYBRID_CLIENT_SECRET": "hybrid_secret",
|
|
"OAUTH_AZURE_AD_HYBRID_TENANT_ID": "tenant_789",
|
|
},
|
|
):
|
|
provider = AzureADHybridOAuthProvider()
|
|
|
|
mock_response = Mock()
|
|
mock_response.json.return_value = {
|
|
"access_token": "hybrid_access_token",
|
|
"refresh_token": "hybrid_refresh_token",
|
|
}
|
|
mock_response.raise_for_status = Mock()
|
|
|
|
with patch("httpx.AsyncClient") as mock_client:
|
|
mock_client.return_value.__aenter__.return_value.post = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
token = await provider.get_token(
|
|
"auth_code", "http://localhost/callback"
|
|
)
|
|
|
|
assert token == "hybrid_access_token"
|
|
assert provider._refresh_token == "hybrid_refresh_token"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_azure_ad_hybrid_get_user_info_success(self):
|
|
"""Test AzureADHybrid get_user_info with successful response."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_AZURE_AD_HYBRID_CLIENT_ID": "hybrid_id",
|
|
"OAUTH_AZURE_AD_HYBRID_CLIENT_SECRET": "hybrid_secret",
|
|
"OAUTH_AZURE_AD_HYBRID_TENANT_ID": "tenant_789",
|
|
},
|
|
):
|
|
provider = AzureADHybridOAuthProvider()
|
|
provider._refresh_token = "refresh_token_hybrid"
|
|
|
|
mock_user_response = Mock()
|
|
mock_user_response.json.return_value = {
|
|
"userPrincipalName": "hybrid@company.com",
|
|
"displayName": "Hybrid User",
|
|
}
|
|
mock_user_response.raise_for_status = Mock()
|
|
|
|
mock_photo_response = Mock()
|
|
mock_photo_response.aread = AsyncMock(return_value=b"photo_bytes")
|
|
mock_photo_response.headers = {"Content-Type": "image/png"}
|
|
|
|
with patch("httpx.AsyncClient") as mock_client:
|
|
mock_get = AsyncMock(
|
|
side_effect=[mock_user_response, mock_photo_response]
|
|
)
|
|
mock_client.return_value.__aenter__.return_value.get = mock_get
|
|
|
|
azure_user, user = await provider.get_user_info("test_token")
|
|
|
|
assert azure_user["userPrincipalName"] == "hybrid@company.com"
|
|
assert isinstance(user, User)
|
|
assert user.identifier == "hybrid@company.com"
|
|
assert user.metadata["provider"] == "azure-ad"
|
|
assert user.metadata["refresh_token"] == "refresh_token_hybrid"
|
|
|
|
|
|
class TestDescopeOAuthProvider:
|
|
"""Test suite for DescopeOAuthProvider."""
|
|
|
|
def test_descope_provider_initialization(self):
|
|
"""Test DescopeOAuthProvider initialization."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_DESCOPE_CLIENT_ID": "descope_client_id",
|
|
"OAUTH_DESCOPE_CLIENT_SECRET": "descope_secret",
|
|
},
|
|
):
|
|
provider = DescopeOAuthProvider()
|
|
|
|
assert provider.id == "descope"
|
|
assert provider.client_id == "descope_client_id"
|
|
assert provider.client_secret == "descope_secret"
|
|
assert "openid profile email" in provider.authorize_params["scope"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_descope_get_token_success(self):
|
|
"""Test Descope get_token with successful response."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_DESCOPE_CLIENT_ID": "descope_id",
|
|
"OAUTH_DESCOPE_CLIENT_SECRET": "descope_secret",
|
|
},
|
|
):
|
|
provider = DescopeOAuthProvider()
|
|
|
|
mock_response = Mock()
|
|
mock_response.json.return_value = {
|
|
"access_token": "descope_access_token",
|
|
"token_type": "Bearer",
|
|
}
|
|
mock_response.raise_for_status = Mock()
|
|
|
|
with patch("httpx.AsyncClient") as mock_client:
|
|
mock_client.return_value.__aenter__.return_value.post = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
token = await provider.get_token(
|
|
"auth_code", "http://localhost/callback"
|
|
)
|
|
|
|
assert token == "descope_access_token"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_descope_get_user_info_success(self):
|
|
"""Test Descope get_user_info with successful response."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_DESCOPE_CLIENT_ID": "descope_id",
|
|
"OAUTH_DESCOPE_CLIENT_SECRET": "descope_secret",
|
|
},
|
|
):
|
|
provider = DescopeOAuthProvider()
|
|
|
|
mock_response = Mock()
|
|
mock_response.json.return_value = {
|
|
"email": "user@descope.com",
|
|
"name": "Descope User",
|
|
}
|
|
mock_response.raise_for_status = Mock()
|
|
|
|
with patch("httpx.AsyncClient") as mock_client:
|
|
mock_client.return_value.__aenter__.return_value.get = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
descope_user, user = await provider.get_user_info("test_token")
|
|
|
|
assert descope_user["email"] == "user@descope.com"
|
|
assert isinstance(user, User)
|
|
assert user.identifier == "user@descope.com"
|
|
assert user.metadata["provider"] == "descope"
|
|
|
|
|
|
class TestAWSCognitoOAuthProvider:
|
|
"""Test suite for AWSCognitoOAuthProvider."""
|
|
|
|
def test_cognito_provider_initialization(self):
|
|
"""Test AWSCognitoOAuthProvider initialization."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_COGNITO_CLIENT_ID": "cognito_client_id",
|
|
"OAUTH_COGNITO_CLIENT_SECRET": "cognito_secret",
|
|
"OAUTH_COGNITO_DOMAIN": "my-app.auth.us-east-1.amazoncognito.com",
|
|
},
|
|
):
|
|
provider = AWSCognitoOAuthProvider()
|
|
|
|
assert provider.id == "aws-cognito"
|
|
assert provider.client_id == "cognito_client_id"
|
|
assert provider.client_secret == "cognito_secret"
|
|
assert "openid profile email" in provider.scopes
|
|
|
|
def test_cognito_provider_custom_scopes(self):
|
|
"""Test AWSCognitoOAuthProvider with custom scopes."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_COGNITO_CLIENT_ID": "cognito_id",
|
|
"OAUTH_COGNITO_CLIENT_SECRET": "cognito_secret",
|
|
"OAUTH_COGNITO_DOMAIN": "my-app.auth.us-east-1.amazoncognito.com",
|
|
"OAUTH_COGNITO_SCOPE": "openid email phone",
|
|
},
|
|
):
|
|
provider = AWSCognitoOAuthProvider()
|
|
|
|
assert provider.scopes == "openid email phone"
|
|
assert provider.authorize_params["scope"] == "openid email phone"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cognito_get_token_success(self):
|
|
"""Test Cognito get_token with successful response."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_COGNITO_CLIENT_ID": "cognito_id",
|
|
"OAUTH_COGNITO_CLIENT_SECRET": "cognito_secret",
|
|
"OAUTH_COGNITO_DOMAIN": "my-app.auth.us-east-1.amazoncognito.com",
|
|
},
|
|
):
|
|
provider = AWSCognitoOAuthProvider()
|
|
|
|
mock_response = Mock()
|
|
mock_response.json.return_value = {
|
|
"access_token": "cognito_access_token",
|
|
"token_type": "Bearer",
|
|
}
|
|
mock_response.raise_for_status = Mock()
|
|
|
|
with patch("httpx.AsyncClient") as mock_client:
|
|
mock_client.return_value.__aenter__.return_value.post = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
token = await provider.get_token(
|
|
"auth_code", "http://localhost/callback"
|
|
)
|
|
|
|
assert token == "cognito_access_token"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cognito_get_user_info_success(self):
|
|
"""Test Cognito get_user_info with successful response."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_COGNITO_CLIENT_ID": "cognito_id",
|
|
"OAUTH_COGNITO_CLIENT_SECRET": "cognito_secret",
|
|
"OAUTH_COGNITO_DOMAIN": "my-app.auth.us-east-1.amazoncognito.com",
|
|
},
|
|
):
|
|
provider = AWSCognitoOAuthProvider()
|
|
|
|
mock_response = Mock()
|
|
mock_response.json.return_value = {
|
|
"email": "user@cognito.com",
|
|
"picture": "https://cognito.com/photo.jpg",
|
|
}
|
|
mock_response.raise_for_status = Mock()
|
|
|
|
with patch("httpx.AsyncClient") as mock_client:
|
|
mock_client.return_value.__aenter__.return_value.get = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
cognito_user, user = await provider.get_user_info("test_token")
|
|
|
|
assert cognito_user["email"] == "user@cognito.com"
|
|
assert isinstance(user, User)
|
|
assert user.identifier == "user@cognito.com"
|
|
assert user.metadata["provider"] == "aws-cognito"
|
|
assert user.metadata["image"] == "https://cognito.com/photo.jpg"
|
|
|
|
|
|
class TestGitlabOAuthProvider:
|
|
"""Test suite for GitlabOAuthProvider."""
|
|
|
|
def test_gitlab_provider_initialization(self):
|
|
"""Test GitlabOAuthProvider initialization."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_GITLAB_CLIENT_ID": "gitlab_client_id",
|
|
"OAUTH_GITLAB_CLIENT_SECRET": "gitlab_secret",
|
|
"OAUTH_GITLAB_DOMAIN": "gitlab.example.com",
|
|
},
|
|
):
|
|
provider = GitlabOAuthProvider()
|
|
|
|
assert provider.id == "gitlab"
|
|
assert provider.client_id == "gitlab_client_id"
|
|
assert provider.client_secret == "gitlab_secret"
|
|
assert "gitlab.example.com" in provider.domain
|
|
assert "openid profile email" in provider.authorize_params["scope"]
|
|
|
|
def test_gitlab_provider_strips_trailing_slash(self):
|
|
"""Test GitlabOAuthProvider strips trailing slash from domain."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_GITLAB_CLIENT_ID": "gitlab_id",
|
|
"OAUTH_GITLAB_CLIENT_SECRET": "gitlab_secret",
|
|
"OAUTH_GITLAB_DOMAIN": "gitlab.example.com/",
|
|
},
|
|
):
|
|
provider = GitlabOAuthProvider()
|
|
|
|
assert not provider.domain.endswith("/")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_gitlab_get_token_success(self):
|
|
"""Test Gitlab get_token with successful response."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_GITLAB_CLIENT_ID": "gitlab_id",
|
|
"OAUTH_GITLAB_CLIENT_SECRET": "gitlab_secret",
|
|
"OAUTH_GITLAB_DOMAIN": "gitlab.example.com",
|
|
},
|
|
):
|
|
provider = GitlabOAuthProvider()
|
|
|
|
mock_response = Mock()
|
|
mock_response.json.return_value = {
|
|
"access_token": "gitlab_access_token",
|
|
"token_type": "Bearer",
|
|
}
|
|
mock_response.raise_for_status = Mock()
|
|
|
|
with patch("httpx.AsyncClient") as mock_client:
|
|
mock_client.return_value.__aenter__.return_value.post = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
token = await provider.get_token(
|
|
"auth_code", "http://localhost/callback"
|
|
)
|
|
|
|
assert token == "gitlab_access_token"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_gitlab_get_user_info_success(self):
|
|
"""Test Gitlab get_user_info with successful response."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_GITLAB_CLIENT_ID": "gitlab_id",
|
|
"OAUTH_GITLAB_CLIENT_SECRET": "gitlab_secret",
|
|
"OAUTH_GITLAB_DOMAIN": "gitlab.example.com",
|
|
},
|
|
):
|
|
provider = GitlabOAuthProvider()
|
|
|
|
mock_response = Mock()
|
|
mock_response.json.return_value = {
|
|
"email": "user@gitlab.com",
|
|
"picture": "https://gitlab.com/avatar.png",
|
|
}
|
|
mock_response.raise_for_status = Mock()
|
|
|
|
with patch("httpx.AsyncClient") as mock_client:
|
|
mock_client.return_value.__aenter__.return_value.get = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
gitlab_user, user = await provider.get_user_info("test_token")
|
|
|
|
assert gitlab_user["email"] == "user@gitlab.com"
|
|
assert isinstance(user, User)
|
|
assert user.identifier == "user@gitlab.com"
|
|
assert user.metadata["provider"] == "gitlab"
|
|
assert user.metadata["image"] == "https://gitlab.com/avatar.png"
|
|
|
|
|
|
class TestKeycloakOAuthProvider:
|
|
"""Test suite for KeycloakOAuthProvider."""
|
|
|
|
def test_keycloak_provider_initialization(self):
|
|
"""Test KeycloakOAuthProvider initialization."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_KEYCLOAK_CLIENT_ID": "keycloak_client_id",
|
|
"OAUTH_KEYCLOAK_CLIENT_SECRET": "keycloak_secret",
|
|
"OAUTH_KEYCLOAK_REALM": "my-realm",
|
|
"OAUTH_KEYCLOAK_BASE_URL": "https://keycloak.example.com",
|
|
},
|
|
):
|
|
provider = KeycloakOAuthProvider()
|
|
|
|
assert provider.client_id == "keycloak_client_id"
|
|
assert provider.client_secret == "keycloak_secret"
|
|
assert provider.realm == "my-realm"
|
|
assert provider.base_url == "https://keycloak.example.com"
|
|
assert "profile email openid" in provider.authorize_params["scope"]
|
|
|
|
def test_keycloak_provider_custom_name(self):
|
|
"""Test KeycloakOAuthProvider with custom name."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_KEYCLOAK_NAME": "my-keycloak",
|
|
"OAUTH_KEYCLOAK_CLIENT_ID": "keycloak_id",
|
|
"OAUTH_KEYCLOAK_CLIENT_SECRET": "keycloak_secret",
|
|
"OAUTH_KEYCLOAK_REALM": "my-realm",
|
|
"OAUTH_KEYCLOAK_BASE_URL": "https://keycloak.example.com",
|
|
},
|
|
clear=False,
|
|
):
|
|
from importlib import reload
|
|
|
|
import chainlit.oauth_providers as oauth_module
|
|
|
|
reload(oauth_module)
|
|
|
|
provider = oauth_module.KeycloakOAuthProvider()
|
|
|
|
assert provider.id == "my-keycloak"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_keycloak_get_token_success(self):
|
|
"""Test Keycloak get_token with successful response."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_KEYCLOAK_CLIENT_ID": "keycloak_id",
|
|
"OAUTH_KEYCLOAK_CLIENT_SECRET": "keycloak_secret",
|
|
"OAUTH_KEYCLOAK_REALM": "my-realm",
|
|
"OAUTH_KEYCLOAK_BASE_URL": "https://keycloak.example.com",
|
|
},
|
|
):
|
|
provider = KeycloakOAuthProvider()
|
|
|
|
mock_response = Mock()
|
|
mock_response.json.return_value = {
|
|
"access_token": "keycloak_access_token",
|
|
"refresh_token": "keycloak_refresh_token",
|
|
}
|
|
mock_response.raise_for_status = Mock()
|
|
|
|
with patch("httpx.AsyncClient") as mock_client:
|
|
mock_client.return_value.__aenter__.return_value.post = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
token = await provider.get_token(
|
|
"auth_code", "http://localhost/callback"
|
|
)
|
|
|
|
assert token == "keycloak_access_token"
|
|
assert provider.refresh_token == "keycloak_refresh_token"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_keycloak_get_user_info_success(self):
|
|
"""Test Keycloak get_user_info with successful response."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_KEYCLOAK_CLIENT_ID": "keycloak_id",
|
|
"OAUTH_KEYCLOAK_CLIENT_SECRET": "keycloak_secret",
|
|
"OAUTH_KEYCLOAK_REALM": "my-realm",
|
|
"OAUTH_KEYCLOAK_BASE_URL": "https://keycloak.example.com",
|
|
},
|
|
):
|
|
provider = KeycloakOAuthProvider()
|
|
|
|
mock_response = Mock()
|
|
mock_response.json.return_value = {
|
|
"email": "user@keycloak.com",
|
|
"name": "Keycloak User",
|
|
}
|
|
mock_response.raise_for_status = Mock()
|
|
|
|
with patch("httpx.AsyncClient") as mock_client:
|
|
mock_client.return_value.__aenter__.return_value.get = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
keycloak_user, user = await provider.get_user_info("test_token")
|
|
|
|
assert keycloak_user["email"] == "user@keycloak.com"
|
|
assert isinstance(user, User)
|
|
assert user.identifier == "user@keycloak.com"
|
|
assert user.metadata["provider"] == "keycloak"
|
|
|
|
|
|
class TestHelperFunctions:
|
|
"""Test suite for helper functions."""
|
|
|
|
def test_get_oauth_provider_returns_correct_provider(self):
|
|
"""Test get_oauth_provider returns the correct provider."""
|
|
provider = get_oauth_provider("github")
|
|
|
|
assert provider is not None
|
|
assert provider.id == "github"
|
|
|
|
def test_get_oauth_provider_returns_none_for_unknown(self):
|
|
"""Test get_oauth_provider returns None for unknown provider."""
|
|
provider = get_oauth_provider("unknown_provider")
|
|
|
|
assert provider is None
|
|
|
|
def test_get_configured_oauth_providers_empty_when_none_configured(self):
|
|
"""Test get_configured_oauth_providers returns empty list when none configured."""
|
|
# Clear all OAuth environment variables
|
|
with patch.dict(os.environ, {}, clear=True):
|
|
configured = get_configured_oauth_providers()
|
|
|
|
assert configured == []
|
|
|
|
def test_get_configured_oauth_providers_returns_configured(self):
|
|
"""Test get_configured_oauth_providers returns configured providers."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_GITHUB_CLIENT_ID": "github_id",
|
|
"OAUTH_GITHUB_CLIENT_SECRET": "github_secret",
|
|
"OAUTH_GOOGLE_CLIENT_ID": "google_id",
|
|
"OAUTH_GOOGLE_CLIENT_SECRET": "google_secret",
|
|
},
|
|
):
|
|
configured = get_configured_oauth_providers()
|
|
|
|
assert "github" in configured
|
|
assert "google" in configured
|
|
|
|
|
|
class TestOAuthProviderEdgeCases:
|
|
"""Test suite for OAuth provider edge cases."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_provider_handles_http_error(self):
|
|
"""Test provider handles HTTP errors gracefully."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_GITHUB_CLIENT_ID": "test_id",
|
|
"OAUTH_GITHUB_CLIENT_SECRET": "test_secret",
|
|
},
|
|
):
|
|
provider = GithubOAuthProvider()
|
|
|
|
mock_response = Mock()
|
|
mock_response.raise_for_status.side_effect = httpx.HTTPStatusError(
|
|
"Error", request=Mock(), response=Mock()
|
|
)
|
|
|
|
with patch("httpx.AsyncClient") as mock_client:
|
|
mock_client.return_value.__aenter__.return_value.post = AsyncMock(
|
|
return_value=mock_response
|
|
)
|
|
|
|
with pytest.raises(httpx.HTTPStatusError):
|
|
await provider.get_raw_token_response("code", "url")
|
|
|
|
def test_provider_strips_trailing_slash_from_domain(self):
|
|
"""Test providers strip trailing slashes from domains."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_AUTH0_CLIENT_ID": "auth0_id",
|
|
"OAUTH_AUTH0_CLIENT_SECRET": "auth0_secret",
|
|
"OAUTH_AUTH0_DOMAIN": "dev-12345.auth0.com/",
|
|
},
|
|
):
|
|
provider = Auth0OAuthProvider()
|
|
|
|
assert not provider.domain.endswith("/")
|
|
|
|
def test_provider_with_prompt_parameter(self):
|
|
"""Test provider includes prompt parameter when configured."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"OAUTH_GOOGLE_CLIENT_ID": "google_id",
|
|
"OAUTH_GOOGLE_CLIENT_SECRET": "google_secret",
|
|
"OAUTH_GOOGLE_PROMPT": "consent",
|
|
},
|
|
):
|
|
provider = GoogleOAuthProvider()
|
|
|
|
assert "prompt" in provider.authorize_params
|
|
assert provider.authorize_params["prompt"] == "consent"
|