"""Tests for the default_brand logic module.""" from unittest.mock import MagicMock, patch import flask import pytest from permissions.connectors import neo4j from permissions.exceptions.incomplete_result_error import IncompleteResultError from permissions.logic import default_brand from permissions.models import ( auth0 as auth0_model, identity as identity_model, owsusers as owsusers_model, tenant as tenant_model, ) from permissions.types import Auth0UserMetadata, Auth0UserResponse from tests.unit.conftest import get_transactional_session_mock @pytest.mark.parametrize( 'current_brand,remaining_brands,should_update,expected_new_brand', [ ('sme', ['theorchard', 'columbia'], True, 'theorchard'), ('sme', ['sme', 'rca'], False, None), ('sme', [], True, None), (None, ['sme', 'rca'], False, None), ], ) def test_update_default_brand_if_needed_updates_brand( current_brand: str, remaining_brands: list[str], should_update: bool, expected_new_brand: str, ) -> None: """Test that defaultBrand is updated when user no longer has access to current brand.""" identity_id = 'test-user-123' admin_identity_id = 'admin-user-123' auth0_user_id = 'auth0-user-123' identity_mock = MagicMock( id=identity_id, auth0_user_id=auth0_user_id, default_brand=current_brand ) session_mock = get_transactional_session_mock() with patch.object(neo4j, 'db_session', return_value=session_mock), patch.object( owsusers_model, 'get_auth0_user', return_value=Auth0UserResponse(user_metadata=Auth0UserMetadata(defaultBrand=current_brand)), ), patch.object( tenant_model, 'get_brands_for_identity', return_value=remaining_brands ), patch.object(auth0_model, 'update_user_metadata') as update_metadata_mock, patch.object( identity_model, 'update_identity_default_brand' ) as update_identity_brand_mock: default_brand.update_default_brand_if_needed( identity=identity_mock, admin_identity_id=admin_identity_id, ) owsusers_model.get_auth0_user.assert_called_with(auth0_user_id) tenant_model.get_brands_for_identity.assert_called_with(identity_id) if should_update: update_metadata_mock.assert_called_once_with( {auth0_user_id: {'defaultBrand': expected_new_brand}} ) update_identity_brand_mock.assert_called_once() call_kwargs = update_identity_brand_mock.call_args[1] assert call_kwargs['identity_id'] == identity_id assert call_kwargs['default_brand'] == expected_new_brand assert call_kwargs['admin_identity_id'] == admin_identity_id else: update_metadata_mock.assert_not_called() update_identity_brand_mock.assert_not_called() @pytest.mark.parametrize( 'identity_default_brand,tenant_brands,should_update,expected_new_brand', [ ('drm', ['sme', 'theorchard'], True, 'sme'), ('ma', ['sme', 'rca'], True, 'sme'), ('sme', ['sme', 'rca'], False, None), ('theorchard', [], True, None), (None, ['sme', 'rca'], False, None), ], ) @patch('permissions.logic.default_brand.g') def test_update_default_brand_if_needed_updates_neo4j_when_auth0_404( g_mock: MagicMock, identity_default_brand: str, tenant_brands: list[str], should_update: bool, expected_new_brand: str, app_context, ): """Test that Neo4j identity is updated when Auth0 returns 404 and defaultBrand needs updating.""" identity_id = 'test-user-123' admin_identity_id = 'admin-user-123' auth0_user_id = 'auth0-user-123' session_mock = get_transactional_session_mock() identity_mock = MagicMock( id=identity_id, auth0_user_id=auth0_user_id, default_brand=identity_default_brand ) # Mock Auth0 404 error import requests auth0_error = requests.exceptions.HTTPError() auth0_error.response = MagicMock(status_code=404) with patch.object(neo4j, 'db_session', return_value=session_mock), patch.object( owsusers_model, 'get_auth0_user', side_effect=auth0_error ), patch.object( tenant_model, 'get_brands_for_identity', return_value=tenant_brands ), patch.object(auth0_model, 'update_user_metadata') as update_metadata_mock, patch.object( identity_model, 'update_identity_default_brand' ) as update_identity_brand_mock: default_brand.update_default_brand_if_needed( identity=identity_mock, admin_identity_id=admin_identity_id, ) # Verify Auth0 call was attempted and 404 was logged owsusers_model.get_auth0_user.assert_called_with(auth0_user_id) g_mock.log.info.assert_called_with( 'Auth0 user does not exist yet', resources={ 'identity_id': identity_id, 'auth0_user_id': auth0_user_id, }, ) # Verify brands were fetched tenant_model.get_brands_for_identity.assert_called_with(identity_id) if should_update: # Auth0 should NOT be updated since user doesn't exist update_metadata_mock.assert_not_called() # Neo4j should still be updated update_identity_brand_mock.assert_called_once() call_kwargs = update_identity_brand_mock.call_args[1] assert call_kwargs['identity_id'] == identity_id assert call_kwargs['default_brand'] == expected_new_brand assert call_kwargs['admin_identity_id'] == admin_identity_id else: update_metadata_mock.assert_not_called() update_identity_brand_mock.assert_not_called() @patch('permissions.logic.default_brand.g') def test_update_default_brand_if_needed_updates_when_auth0_error( g_mock: MagicMock, app_context: flask.ctx.AppContext, ) -> None: """Test that Auth0 errors are logged but don't prevent Neo4j update.""" identity_id = 'test-user-123' admin_identity_id = 'admin-user-123' auth0_user_id = 'auth0-user-123' session_mock = get_transactional_session_mock() identity_mock = MagicMock(id=identity_id, auth0_user_id=auth0_user_id) auth0_err = auth0_model.Auth0Error(500, 'Internal Error', 'Auth0 is down') with patch.object(neo4j, 'db_session', return_value=session_mock), patch.object( owsusers_model, 'get_auth0_user', return_value=Auth0UserResponse(user_metadata=Auth0UserMetadata(defaultBrand='sme')), ), patch.object( tenant_model, 'get_brands_for_identity', return_value=['theorchard', 'columbia'] ), patch.object(auth0_model, 'update_user_metadata', side_effect=auth0_err), patch.object( identity_model, 'update_identity_default_brand' ) as update_identity_brand_mock: # Should not raise - error is caught and logged default_brand.update_default_brand_if_needed( identity=identity_mock, admin_identity_id=admin_identity_id, ) # Verify Auth0 error was logged g_mock.log.error.assert_any_call( 'Error updating Auth0 default brand', resources={ 'auth0_user_id': auth0_user_id, 'error': str(auth0_err), }, ) # Verify Neo4j update still happened update_identity_brand_mock.assert_called_once() call_kwargs = update_identity_brand_mock.call_args[1] assert call_kwargs['identity_id'] == identity_id assert call_kwargs['default_brand'] == 'theorchard' assert call_kwargs['admin_identity_id'] == admin_identity_id @patch('permissions.logic.default_brand.g') def test_update_default_brand_if_needed_neo4j_error( g_mock: MagicMock, app_context: flask.ctx.AppContext, ) -> None: """Test that errors updating defaultBrand are logged.""" identity_id = 'test-user-123' admin_identity_id = 'admin-user-123' auth0_user_id = 'auth0-user-123' session_mock = get_transactional_session_mock() identity_mock = MagicMock(id=identity_id, auth0_user_id=auth0_user_id) err = IncompleteResultError(message='Failed to update Identity') with patch.object(neo4j, 'db_session', return_value=session_mock), patch.object( owsusers_model, 'get_auth0_user', return_value=Auth0UserResponse(user_metadata=Auth0UserMetadata(defaultBrand='sme')), ), patch.object( tenant_model, 'get_brands_for_identity', return_value=['theorchard', 'columbia'] ), patch.object(auth0_model, 'update_user_metadata'), patch.object( identity_model, 'update_identity_default_brand', side_effect=err ): with pytest.raises(IncompleteResultError): default_brand.update_default_brand_if_needed( identity=identity_mock, admin_identity_id=admin_identity_id, ) # Verify error was logged g_mock.log.error.assert_called_with( 'Error updating Neo4j default brand', resources={ 'identity_id': identity_id, 'error': str(err), }, ) # ------- Tests for refactored functions ------- @pytest.mark.parametrize( 'current_default,current_brands,expected', [ ('sme', ['sme', 'theorchard'], False), # brand in list ('sme', ['theorchard', 'columbia'], True), # brand not in list ('sme', [], True), # empty list (None, ['sme', 'theorchard'], False), # None default brand (None, [], False), # None default brand, empty list ], ) def test_needs_brand_update(current_default: str | None, current_brands: list[str], expected: bool): """Test the needs_brand_update helper function.""" result = default_brand.needs_brand_update(current_default, current_brands) assert result == expected def test_get_current_brands_with_company_brand(): """When company_brand is provided, use it directly without DB query.""" with patch.object(tenant_model, 'get_brands_for_identity') as mock_get: result = default_brand.get_current_brands('identity-123', company_brand='awal') assert result == ['awal'] mock_get.assert_not_called() def test_get_current_brands_without_company_brand(): """When company_brand is None, query DB for brands.""" with patch.object( tenant_model, 'get_brands_for_identity', return_value=['sme', 'theorchard'] ) as mock_get: result = default_brand.get_current_brands('identity-123') assert result == ['sme', 'theorchard'] mock_get.assert_called_once_with('identity-123') def test_get_auth0_user_metadata_success(): """Returns user metadata when Auth0 user exists.""" identity_mock = MagicMock(id='identity-123', auth0_user_id='auth0|123') expected_metadata = Auth0UserMetadata(defaultBrand='sme') with patch.object( owsusers_model, 'get_auth0_user', return_value=Auth0UserResponse(user_metadata=expected_metadata), ): result = default_brand.get_auth0_user_metadata(identity_mock) assert result == expected_metadata @patch('permissions.logic.default_brand.g') def test_get_auth0_user_metadata_404(g_mock: MagicMock, app_context): """Returns None and logs when Auth0 user doesn't exist.""" import requests identity_mock = MagicMock(id='identity-123', auth0_user_id='auth0|123') error = requests.exceptions.HTTPError() error.response = MagicMock(status_code=404) with patch.object(owsusers_model, 'get_auth0_user', side_effect=error): result = default_brand.get_auth0_user_metadata(identity_mock) assert result is None g_mock.log.info.assert_called_with( 'Auth0 user does not exist yet', resources={'identity_id': 'identity-123', 'auth0_user_id': 'auth0|123'}, ) def test_update_auth0_default_brand_success(): """Returns True on successful update.""" with patch.object(auth0_model, 'update_user_metadata') as mock_update: result = default_brand.update_auth0_default_brand('auth0|123', 'theorchard') assert result is True mock_update.assert_called_once_with({'auth0|123': {'defaultBrand': 'theorchard'}}) @patch('permissions.logic.default_brand.g') def test_update_auth0_default_brand_error(g_mock: MagicMock, app_context): """Returns False and logs on error.""" error = Exception('Auth0 error') with patch.object(auth0_model, 'update_user_metadata', side_effect=error): result = default_brand.update_auth0_default_brand('auth0|123', 'theorchard') assert result is False g_mock.log.error.assert_called_with( 'Error updating Auth0 default brand', resources={'auth0_user_id': 'auth0|123', 'error': 'Auth0 error'}, ) def test_update_neo4j_default_brand_success(): """Successfully updates Neo4j.""" session_mock = get_transactional_session_mock() with patch.object(neo4j, 'db_session', return_value=session_mock), patch.object( identity_model, 'update_identity_default_brand' ) as mock_update: default_brand.update_neo4j_default_brand('identity-123', 'theorchard', 'admin-123') mock_update.assert_called_once_with( session=session_mock.__enter__.return_value, identity_id='identity-123', default_brand='theorchard', admin_identity_id='admin-123', ) @patch('permissions.logic.default_brand.g') def test_update_neo4j_default_brand_error(g_mock: MagicMock, app_context): """Raises and logs on error.""" session_mock = get_transactional_session_mock() error = Exception('Neo4j error') with patch.object(neo4j, 'db_session', return_value=session_mock), patch.object( identity_model, 'update_identity_default_brand', side_effect=error ): with pytest.raises(Exception, match='Neo4j error'): default_brand.update_neo4j_default_brand('identity-123', 'theorchard', 'admin-123') g_mock.log.error.assert_called_with( 'Error updating Neo4j default brand', resources={'identity_id': 'identity-123', 'error': 'Neo4j error'}, ) @patch('permissions.logic.default_brand.identity_model') @patch('permissions.logic.default_brand.neo4j_connector') @patch('permissions.logic.default_brand.auth0_model') @patch('permissions.logic.default_brand.owsusers_model') @patch('permissions.logic.default_brand.tenant_model') def test_update_default_brand_if_needed_with_company_brand_skips_db_query( tenant_model_mock: MagicMock, owsusers_model_mock: MagicMock, auth0_model_mock: MagicMock, neo4j_connector_mock: MagicMock, identity_model_mock: MagicMock, ): """When company_brand is provided, skip DB query and use it directly.""" identity_mock = MagicMock( id='identity-123', auth0_user_id='auth0|123', default_brand='theorchard' ) session_mock = MagicMock() neo4j_connector_mock.db_session.return_value.__enter__.return_value = session_mock owsusers_model_mock.get_auth0_user.return_value = Auth0UserResponse( user_metadata=Auth0UserMetadata(defaultBrand='theorchard') ) default_brand.update_default_brand_if_needed( identity=identity_mock, admin_identity_id='admin-123', company_brand='awal', ) # Should NOT query DB for brands tenant_model_mock.get_brands_for_identity.assert_not_called() # Should update Auth0 with the new brand auth0_model_mock.update_user_metadata.assert_called_with( {'auth0|123': {'defaultBrand': 'awal'}} ) # Should update Neo4j with the new brand identity_model_mock.update_identity_default_brand.assert_called_with( session=session_mock, identity_id='identity-123', default_brand='awal', admin_identity_id='admin-123', ) @patch('permissions.logic.default_brand.identity_model') @patch('permissions.logic.default_brand.neo4j_connector') @patch('permissions.logic.default_brand.auth0_model') @patch('permissions.logic.default_brand.owsusers_model') @patch('permissions.logic.default_brand.tenant_model') def test_update_default_brand_if_needed_no_update_when_brand_still_valid( tenant_model_mock: MagicMock, owsusers_model_mock: MagicMock, auth0_model_mock: MagicMock, neo4j_connector_mock: MagicMock, identity_model_mock: MagicMock, ): """No update when current default brand is still in current brands.""" identity_mock = MagicMock(id='identity-123', auth0_user_id='auth0|123', default_brand='sme') tenant_model_mock.get_brands_for_identity.return_value = ['sme', 'theorchard'] owsusers_model_mock.get_auth0_user.return_value = Auth0UserResponse( user_metadata=Auth0UserMetadata(defaultBrand='sme') ) default_brand.update_default_brand_if_needed( identity=identity_mock, admin_identity_id='admin-123', ) # Should query DB since no company_brand provided tenant_model_mock.get_brands_for_identity.assert_called_once_with('identity-123') # Should NOT update since brand is still valid auth0_model_mock.update_user_metadata.assert_not_called() identity_model_mock.update_identity_default_brand.assert_not_called()