"""Integration tests for the auth0_cli Lambda handler.""" from typing import Any from unittest.mock import MagicMock, patch import pytest from auth0_client.app import handler from tests.integration.conftest import ( make_delete_event, make_search_event, seed_tenant_secret, ) def _token_resp() -> MagicMock: m = MagicMock() m.raise_for_status.return_value = None m.json.return_value = {'access_token': 'tok', 'expires_in': 3600} return m def _users_resp(users: list[dict[str, Any]]) -> MagicMock: m = MagicMock() m.raise_for_status.return_value = None m.json.return_value = users return m def _delete_resp(status_code: int = 204) -> MagicMock: m = MagicMock() m.status_code = status_code if status_code == 204: m.raise_for_status.return_value = None else: from requests.exceptions import HTTPError mock_response = MagicMock() mock_response.status_code = status_code m.raise_for_status.side_effect = HTTPError( f'{status_code}', response=mock_response ) return m @pytest.mark.integration class TestSearchUserByEmail: """Tests for the 'search-user-by-email' action of the handler.""" def test_user_found_in_seeded_tenant( self, secrets_manager: Any, monkeypatch: pytest.MonkeyPatch ) -> None: """User found in the one seeded tenant → matches list populated.""" monkeypatch.setenv('AUTH0_TENANTS', 'test-tenant') seed_tenant_secret( secrets_manager, 'test-tenant', domain='test-tenant.auth0.com' ) with ( patch('requests.Session.post', return_value=_token_resp()), patch( 'requests.Session.get', return_value=_users_resp( [{'user_id': 'auth0|abc', 'email': 'jane@example.com'}] ), ), ): result = handler(make_search_event('jane@example.com', 'SYS-1'), {}) assert result['email'] == 'jane@example.com' assert result['ticket_id'] == 'SYS-1' assert result['matches'] == [{'tenant': 'test-tenant', 'user_id': 'auth0|abc'}] def test_user_not_found( self, secrets_manager: Any, monkeypatch: pytest.MonkeyPatch ) -> None: """User not found in any tenant → matches: [].""" monkeypatch.setenv('AUTH0_TENANTS', 'test-tenant') seed_tenant_secret(secrets_manager, 'test-tenant') with ( patch('requests.Session.post', return_value=_token_resp()), patch('requests.Session.get', return_value=_users_resp([])), ): result = handler(make_search_event('nobody@example.com'), {}) assert result['matches'] == [] def test_missing_tenant_secret_skipped( self, secrets_manager: Any, monkeypatch: pytest.MonkeyPatch ) -> None: """Tenant whose secret is absent is skipped; other tenants still searched.""" monkeypatch.setenv('AUTH0_TENANTS', 'missing-tenant,good-tenant') # Only seed the second tenant seed_tenant_secret(secrets_manager, 'good-tenant', domain='good.auth0.com') with ( patch('requests.Session.post', return_value=_token_resp()), patch( 'requests.Session.get', return_value=_users_resp( [{'user_id': 'auth0|xyz', 'email': 'x@example.com'}] ), ), ): result = handler(make_search_event('x@example.com'), {}) # missing-tenant is skipped; good-tenant match is returned assert result['matches'] == [{'tenant': 'good-tenant', 'user_id': 'auth0|xyz'}] def test_unknown_action_raises(self, secrets_manager: Any) -> None: """Unknown action raises ValueError.""" with pytest.raises(ValueError, match='Unknown action'): handler({'action': 'bogus', 'dry_run': False}, {}) @pytest.mark.integration class TestDeleteUserById: """Tests for the delete_user_by_id action of the handler.""" def test_delete_succeeds( self, secrets_manager: Any, monkeypatch: pytest.MonkeyPatch ) -> None: """Successful deletion returns deleted=True.""" monkeypatch.setenv('AUTH0_TENANTS', 'prod-orchard') seed_tenant_secret( secrets_manager, 'prod-orchard', domain='prod-orchard.auth0.com' ) with ( patch('requests.Session.post', return_value=_token_resp()), patch('requests.Session.delete', return_value=_delete_resp(204)), ): result = handler( make_delete_event('prod-orchard', 'auth0|abc123', 'SYS-1'), {} ) assert result['deleted'] is True assert result['dry_run'] is False assert result['tenant'] == 'prod-orchard' assert result['user_id'] == 'auth0|abc123' def test_delete_dry_run(self, secrets_manager: Any) -> None: """Dry run returns deleted=False, no HTTP DELETE called.""" with patch('requests.Session.delete') as mock_del: result = handler( make_delete_event('prod-orchard', 'auth0|abc123', dry_run=True), {} ) assert result['deleted'] is False assert result['dry_run'] is True mock_del.assert_not_called() def test_delete_404_is_idempotent( self, secrets_manager: Any, monkeypatch: pytest.MonkeyPatch ) -> None: """Auth0 404 on delete → deleted=False without raising (idempotent).""" monkeypatch.setenv('AUTH0_TENANTS', 'prod-orchard') seed_tenant_secret( secrets_manager, 'prod-orchard', domain='prod-orchard.auth0.com' ) from requests.exceptions import HTTPError mock_404_response = MagicMock() mock_404_response.status_code = 404 mock_404_response.text = 'Not Found' def raise_404(*args: Any, **kwargs: Any) -> None: raise HTTPError('404 Not Found', response=mock_404_response) delete_mock = MagicMock() delete_mock.status_code = 404 delete_mock.raise_for_status.side_effect = raise_404 with ( patch('requests.Session.post', return_value=_token_resp()), patch('requests.Session.delete', return_value=delete_mock), ): result = handler( make_delete_event('prod-orchard', 'auth0|gone', 'SYS-1'), {} ) assert result['deleted'] is False assert 'already_absent' not in result def test_delete_missing_credentials_raises( self, secrets_manager: Any, monkeypatch: pytest.MonkeyPatch ) -> None: """Missing Secrets Manager credentials in delete flow raises ValueError.""" monkeypatch.setenv('AUTH0_TENANTS', 'prod-orchard') # Note: intentionally NOT seeding the secret for 'prod-orchard' with pytest.raises(ValueError, match='Cannot delete user'): handler(make_delete_event('prod-orchard', 'auth0|abc123', 'SYS-1'), {}) def test_delete_url_encodes_user_id( self, secrets_manager: Any, monkeypatch: pytest.MonkeyPatch ) -> None: """user_id is URL-encoded when constructing the delete request URL.""" monkeypatch.setenv('AUTH0_TENANTS', 'prod-orchard') seed_tenant_secret( secrets_manager, 'prod-orchard', domain='prod-orchard.auth0.com' ) with ( patch('requests.Session.post', return_value=_token_resp()), patch( 'requests.Session.delete', return_value=_delete_resp(204) ) as mock_del, ): handler(make_delete_event('prod-orchard', 'auth0|abc/special', 'SYS-1'), {}) call_url = mock_del.call_args[0][0] assert 'auth0%7Cabc%2Fspecial' in call_url