Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:48:47 +08:00

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"