"""Tests for the Auth0Client class.""" from typing import Any from unittest.mock import MagicMock, patch import pytest import requests from auth0_client import Auth0Client def make_auth0_client() -> Auth0Client: """Create a sample Auth0Client for testing.""" return Auth0Client( domain='example.auth0.com', client_id='cid', client_secret='secret', ) def returns_valid_token(*args: Any, **kwargs: Any) -> object: """Return a valid access token response.""" class Resp: def raise_for_status(self) -> None: pass def json(self) -> dict[str, Any]: return {'access_token': 'token123', 'expires_in': 3600} return Resp() def returns_user_list(*args: Any, **kwargs: Any) -> object: """Return a user list containing one user.""" class Resp: def raise_for_status(self) -> None: pass def json(self) -> list[dict[str, Any]]: return [{'user_id': 'abc', 'email': 'foo@bar.com'}] return Resp() def returns_empty_list(*args: Any, **kwargs: Any) -> object: """Return an empty user list.""" class Resp: def raise_for_status(self) -> None: pass def json(self) -> list[Any]: return [] return Resp() def returns_delete_success(*args: Any, **kwargs: Any) -> object: """Return a successful delete response.""" class Resp: def raise_for_status(self) -> None: pass def json(self) -> dict[str, Any]: return {} @property def status_code(self) -> int: return 204 return Resp() def returns_delete_failure(*args: Any, **kwargs: Any) -> object: """Return a failed delete response.""" class Resp: def raise_for_status(self) -> None: pass def json(self) -> dict[str, Any]: return {} @property def status_code(self) -> int: return 404 return Resp() def raises_http_error(*args: Any, **kwargs: Any) -> object: """Raise an HTTP error.""" class Resp: def raise_for_status(self) -> None: raise Exception('HTTP error') return Resp() def returns_block_success(*args: Any, **kwargs: Any) -> object: """Return a successful block (PATCH) response.""" class Resp: def raise_for_status(self) -> None: pass def json(self) -> dict[str, Any]: return {'blocked': True} @property def status_code(self) -> int: return 200 return Resp() def returns_block_unexpected_status(*args: Any, **kwargs: Any) -> object: """Return a non-200, non-error PATCH response (unexpected status).""" class Resp: def raise_for_status(self) -> None: pass def json(self) -> dict[str, Any]: return {} @property def status_code(self) -> int: return 202 return Resp() def raises_http_error_404(*args: Any, **kwargs: Any) -> object: """Return a PATCH response that raises a real HTTPError with a 404 status.""" class Resp: status_code = 404 def raise_for_status(self) -> None: err = requests.exceptions.HTTPError('404 error') err.response = self # type: ignore[assignment] raise err def json(self) -> dict[str, Any]: return {} return Resp() @patch('requests.Session.post', side_effect=returns_valid_token) @patch('requests.Session.get', side_effect=returns_user_list) def test_returns_user_list_when_email_exists( mock_get: MagicMock, mock_post: MagicMock ) -> None: """When the email exists, the user list is returned. :param mock_get: :param mock_post: :return: """ client = make_auth0_client() result = client.search_user_by_email('foo@bar.com') assert result == [{'user_id': 'abc', 'email': 'foo@bar.com'}] @patch('requests.Session.post', side_effect=returns_valid_token) @patch('requests.Session.get', side_effect=returns_empty_list) def test_returns_empty_list_when_email_not_found( mock_get: MagicMock, mock_post: MagicMock ) -> None: """When the email does not exist, an empty list is returned. :param mock_get: :param mock_post: :return: """ client = make_auth0_client() result = client.search_user_by_email('notfound@bar.com') assert result == [] @patch('requests.Session.post', side_effect=returns_valid_token) @patch('requests.Session.get', side_effect=raises_http_error) def test_raises_exception_on_search_http_error( mock_get: MagicMock, mock_post: MagicMock ) -> None: """HTTP errors during user search are propagated. :param mock_get: :param mock_post: :return: """ client = make_auth0_client() with pytest.raises(Exception): client.search_user_by_email('foo@bar.com') @patch('requests.Session.post', side_effect=raises_http_error) def test_raises_exception_on_token_http_error(mock_post: MagicMock) -> None: """HTTP errors during token retrieval are propagated. :param mock_post: :return: """ client = make_auth0_client() with pytest.raises(Exception): client.search_user_by_email('foo@bar.com') @patch('requests.Session.post', side_effect=returns_valid_token) @patch('requests.Session.get', side_effect=returns_user_list) def test_uses_cached_token_if_not_expired( mock_get: MagicMock, mock_post: MagicMock ) -> None: """If a valid cached token exists, it is used instead of requesting a new one. :param mock_get: :param mock_post: :return: """ client = make_auth0_client() client._access_token = 'cached_token' client._token_expiry = 9999999999 # far in the future result = client.search_user_by_email('foo@bar.com') assert result == [{'user_id': 'abc', 'email': 'foo@bar.com'}] assert mock_post.call_count == 0 @patch('requests.Session.post', side_effect=returns_valid_token) @patch('requests.Session.delete', side_effect=returns_delete_success) def test_delete_user_by_id_success( mock_delete: MagicMock, mock_post: MagicMock ) -> None: """Test successful user deletion. :param mock_delete: :param mock_post: :return: """ client = make_auth0_client() client.delete_user_by_id('abc') # Verify that delete was called with correct URL and headers assert mock_delete.called args, kwargs = mock_delete.call_args assert 'https://example.auth0.com/api/v2/users/abc' in args[0] assert 'Authorization' in kwargs['headers'] assert kwargs['headers']['Authorization'] == 'Bearer token123' # Verify that post was called for token retrieval assert mock_post.called @patch('requests.Session.post', side_effect=returns_valid_token) @patch('requests.Session.delete', side_effect=returns_delete_failure) def test_delete_user_by_id_failure( mock_delete: MagicMock, mock_post: MagicMock ) -> None: """Test user deletion failure with non-204 status code. :param mock_delete: :param mock_post: :return: """ client = make_auth0_client() with pytest.raises(Exception) as exc_info: client.delete_user_by_id('abc') assert 'Unexpected status code: 404' in str(exc_info.value) @patch('requests.Session.post', side_effect=raises_http_error) def test_delete_user_by_id_token_failure(mock_post: MagicMock) -> None: """Test user deletion fails when token retrieval fails. :param mock_post: :return: """ client = make_auth0_client() with pytest.raises(Exception): client.delete_user_by_id('abc') @patch('requests.Session.post', side_effect=returns_valid_token) @patch('requests.Session.delete', side_effect=raises_http_error) def test_delete_user_by_id_http_error( mock_delete: MagicMock, mock_post: MagicMock ) -> None: """Test user deletion propagates HTTP errors. :param mock_delete: :param mock_post: :return: """ client = make_auth0_client() with pytest.raises(Exception): client.delete_user_by_id('abc') @patch('requests.Session.post', side_effect=returns_valid_token) @patch('requests.Session.delete', side_effect=returns_delete_success) def test_delete_user_by_id_uses_cached_token( mock_delete: MagicMock, mock_post: MagicMock ) -> None: """Test that delete_user_by_id uses cached token when valid. :param mock_delete: :param mock_post: :return: """ client = make_auth0_client() client._access_token = 'cached_token' client._token_expiry = 9999999999 # far in the future client.delete_user_by_id('abc') # Verify that delete was called but post was not (token was cached) assert mock_delete.called assert mock_post.call_count == 0 @patch('requests.Session.post', side_effect=returns_valid_token) @patch('requests.Session.patch', side_effect=returns_block_success) def test_block_user_by_id_success(mock_patch: MagicMock, mock_post: MagicMock) -> None: """block_user_by_id PATCHes blocked:true with the bearer token.""" client = make_auth0_client() client.block_user_by_id('abc') assert mock_patch.called args, kwargs = mock_patch.call_args assert 'https://example.auth0.com/api/v2/users/abc' in args[0] assert kwargs['headers']['Authorization'] == 'Bearer token123' assert kwargs['json'] == {'blocked': True} assert mock_post.called @patch('requests.Session.post', side_effect=returns_valid_token) @patch('requests.Session.patch', side_effect=raises_http_error_404) def test_block_user_by_id_404_raises_http_error( mock_patch: MagicMock, mock_post: MagicMock ) -> None: """A 404 surfaces as an HTTPError for the handler to treat as idempotent.""" client = make_auth0_client() with pytest.raises(requests.exceptions.HTTPError): client.block_user_by_id('abc') @patch('requests.Session.post', side_effect=returns_valid_token) @patch('requests.Session.patch', side_effect=returns_block_unexpected_status) def test_block_user_by_id_unexpected_status_raises( mock_patch: MagicMock, mock_post: MagicMock ) -> None: """A non-200, non-error status raises a ValueError.""" client = make_auth0_client() with pytest.raises(ValueError) as exc_info: client.block_user_by_id('abc') assert 'Unexpected status code: 202' in str(exc_info.value) @patch('requests.Session.post', side_effect=returns_valid_token) @patch('requests.Session.patch', side_effect=returns_block_success) def test_block_user_by_id_uses_cached_token( mock_patch: MagicMock, mock_post: MagicMock ) -> None: """block_user_by_id reuses a valid cached token without a new token request.""" client = make_auth0_client() client._access_token = 'cached_token' client._token_expiry = 9999999999 # far in the future client.block_user_by_id('abc') assert mock_patch.called assert mock_post.call_count == 0