from unittest import mock import faker import pytest from fastapi.security import HTTPAuthorizationCredentials from jwtauth import JWTAuth from jwtauth.exceptions import JWTAuthError from starlette.requests import Request from fansifter_common.api.security import ( INTERNAL_EMPLOYEES, authenticate_identity, get_fan_response_jwt_auth, validate_fan_response_jwt, ) from fansifter_common.auth.exceptions import NotAuthenticated from fansifter_common.constants import ( DEFAULT_BRAND, FAN_RESPONSE_PROD_AUDIENCE, FAN_RESPONSE_QA_AUDIENCE, ) @pytest.fixture def jwt_auth_mock() -> mock.MagicMock: return mock.MagicMock(spec=JWTAuth) @pytest.mark.anyio async def test_authenticate_identity_from_request_scope( faker: faker.Faker, jwt_auth_mock: mock.MagicMock ) -> None: identity_id = str(faker.uuid4(cast_to=str)) identity = await authenticate_identity( Request( scope={ "type": "http", "token": { "https://grass.theorchard.com/user_metadata": { "orchardIdentityId": identity_id, }, }, } ), credentials=None, jwt_auth=jwt_auth_mock, ) assert identity.id == identity_id assert identity.brand == DEFAULT_BRAND assert identity.is_internal_employee is False @pytest.mark.anyio async def test_authenticate_identity_hardcoded_internal_employee( jwt_auth_mock: mock.MagicMock, ) -> None: identity_id = INTERNAL_EMPLOYEES[0] identity = await authenticate_identity( Request( scope={ "type": "http", "token": { "https://grass.theorchard.com/user_metadata": { "orchardIdentityId": identity_id, }, "https://grass.theorchard.com/internal_employee": False, }, } ), credentials=None, jwt_auth=jwt_auth_mock, ) assert identity.id == identity_id assert identity.brand == DEFAULT_BRAND assert identity.is_internal_employee is True @pytest.mark.anyio async def test_authenticate_identity_from_request_scope_invalid_metadata( jwt_auth_mock: mock.MagicMock, ) -> None: with pytest.raises(NotAuthenticated) as exc_info: await authenticate_identity( Request( scope={ "type": "http", "token": {"aud": "any"}, } ), credentials=None, jwt_auth=jwt_auth_mock, ) assert exc_info.value.code == "authorization_error" assert exc_info.value.message == "Missing token orchardIdentityId claim." @pytest.mark.anyio async def test_authenticate_identity_from_credentials( faker: faker.Faker, jwt_auth_mock: mock.MagicMock ) -> None: identity_id = str(faker.uuid4(cast_to=str)) brand = "sme" is_internal_employee = True jwt_auth_mock.aget_token.return_value = { "https://grass.theorchard.com/user_metadata": { "orchardIdentityId": identity_id, }, "https://grass.theorchard.com/brand": brand, "https://grass.theorchard.com/internal_employee": is_internal_employee, } identity = await authenticate_identity( Request(scope={"type": "http"}), credentials=HTTPAuthorizationCredentials(credentials="token", scheme="bearer"), jwt_auth=jwt_auth_mock, ) assert identity.id == identity_id assert identity.brand == brand assert identity.is_internal_employee is is_internal_employee @pytest.mark.anyio async def test_authenticate_identity_from_credentials_invalid_token( jwt_auth_mock: mock.MagicMock, ) -> None: jwt_auth_mock.aget_token.side_effect = JWTAuthError(message="invalid") with pytest.raises(NotAuthenticated) as exc_info: await authenticate_identity( Request(scope={"type": "http"}), credentials=HTTPAuthorizationCredentials( credentials="token", scheme="bearer" ), jwt_auth=jwt_auth_mock, ) assert exc_info.value.code == "authorization_error" assert exc_info.value.message == "invalid" @pytest.mark.anyio async def test_authenticate_identity_missing_token( jwt_auth_mock: mock.MagicMock, ) -> None: with pytest.raises(NotAuthenticated) as exc_info: await authenticate_identity( Request(scope={"type": "http"}), credentials=None, jwt_auth=jwt_auth_mock, ) assert exc_info.value.code == "authorization_error" assert exc_info.value.message == "Missing Authorization header." @pytest.mark.anyio async def test_validate_fan_response_jwt_success( jwt_auth_mock: mock.MagicMock, ) -> None: jwt_auth_mock.aget_token.return_value = {"sub": "test"} await validate_fan_response_jwt( credentials=HTTPAuthorizationCredentials(credentials="token", scheme="bearer"), fan_response_jwt_auth=jwt_auth_mock, ) jwt_auth_mock.aget_token.assert_awaited_once_with("token") @pytest.mark.anyio async def test_validate_fan_response_jwt_missing_credentials( jwt_auth_mock: mock.MagicMock, ) -> None: with pytest.raises(NotAuthenticated) as exc_info: await validate_fan_response_jwt( credentials=None, fan_response_jwt_auth=jwt_auth_mock, ) assert exc_info.value.code == "authorization_error" assert exc_info.value.message == "Missing Authorization header." @pytest.mark.anyio async def test_validate_fan_response_jwt_invalid_token( jwt_auth_mock: mock.MagicMock, ) -> None: jwt_auth_mock.aget_token.side_effect = Exception("invalid") with pytest.raises(NotAuthenticated) as exc_info: await validate_fan_response_jwt( credentials=HTTPAuthorizationCredentials( credentials="token", scheme="bearer" ), fan_response_jwt_auth=jwt_auth_mock, ) assert exc_info.value.code == "authorization_error" assert exc_info.value.message == "Invalid JWT token." def test_get_fan_response_jwt_auth_prod() -> None: jwt_auth = get_fan_response_jwt_auth("prod") assert jwt_auth.audience == [FAN_RESPONSE_PROD_AUDIENCE] def test_get_fan_response_jwt_auth_qa() -> None: jwt_auth = get_fan_response_jwt_auth("qa") assert jwt_auth.audience == [FAN_RESPONSE_QA_AUDIENCE]