"""Unit tests for owsclient related to header operations.""" from __future__ import annotations from dataclasses import dataclass from unittest.mock import AsyncMock, MagicMock import pytest from _pytest.monkeypatch import MonkeyPatch from owsclient import AsyncOwsClient, OwsClient from owsclient.base import BaseOwsClient, make_new_correlation_id from owsclient.protocols import RequestContext GET_CORRELATION_ID_FUNC_VALUE = "60bd9aae-2271-11ef-a7c2-3e17271fba71" MAKE_NEW_CORRELATION_ID_VALUE = "e4b3f742-226f-11ef-b312-3e17271fba71" @dataclass class TestRequestContext: """TestRequestContext for unit testing.""" authorization: str | None = None identity_id: str | None = None profile_id: int | None = None profile_type: str | None = None def test_make_new_correlation_id(monkeypatch: MonkeyPatch) -> None: """Test make_new_correlation_id.""" monkeypatch.setattr( "owsclient.base.uuid1", lambda: MAKE_NEW_CORRELATION_ID_VALUE, ) actual = make_new_correlation_id() assert actual == MAKE_NEW_CORRELATION_ID_VALUE @pytest.mark.parametrize( "provided_correlation_id, correlation_id_return_value, expected_correlation_id", [ ( pytest.param( "", None, MAKE_NEW_CORRELATION_ID_VALUE, id="make_new_correlation_id is used when correlation id is empty string and correlation_id_getter returns None", ) ), ( pytest.param( "", GET_CORRELATION_ID_FUNC_VALUE, GET_CORRELATION_ID_FUNC_VALUE, id="get_correlation_id_func is used when correlation id is empty string", ) ), ( pytest.param( "", "", MAKE_NEW_CORRELATION_ID_VALUE, id="make_new_correlation_id is used when correlation id is empty string and correlation_id_getter returns empty", ) ), ( pytest.param( None, None, MAKE_NEW_CORRELATION_ID_VALUE, id="make_new_correlation_id is used when correlation id is None and correlation_id_getter returns None", ) ), ( pytest.param( None, GET_CORRELATION_ID_FUNC_VALUE, GET_CORRELATION_ID_FUNC_VALUE, id="get_correlation_id_func is used when correlation id is None", ) ), ( pytest.param( None, "", MAKE_NEW_CORRELATION_ID_VALUE, id="make_new_correlation_id is used when correlation id is None and correlation_id_getter returns empty", ) ), ( pytest.param( None, None, MAKE_NEW_CORRELATION_ID_VALUE, id="make_new_correlation_id is used when correlation id is None and correlation_id_getter returns None", ) ), ( pytest.param( "19e1f36a-2270-11ef-a911-3e17271fba71", None, "19e1f36a-2270-11ef-a911-3e17271fba71", id="correlation_id is used when correlation id is non-empty string", ) ), ( pytest.param( "19e1f36a-2270-11ef-a911-3e17271fba71", GET_CORRELATION_ID_FUNC_VALUE, "19e1f36a-2270-11ef-a911-3e17271fba71", id="correlation_id is used when correlation id is non-empty string", ) ), ( pytest.param( "19e1f36a-2270-11ef-a911-3e17271fba71", "", "19e1f36a-2270-11ef-a911-3e17271fba71", id="correlation_id is used when correlation id is non-empty string", ) ), ], ) def test_ows_client_prepare_headers_for_correlation_id( provided_correlation_id: str | None, correlation_id_return_value: str | None, expected_correlation_id: str, monkeypatch: MonkeyPatch, ) -> None: """Test prepare_headers populates correlation_id.""" mock_correlation_id_getter = MagicMock(return_value=correlation_id_return_value) ows_client = OwsClient( environment="test", service_name="ows-test", correlation_id_getter=mock_correlation_id_getter, ) monkeypatch.setattr( "owsclient.base.make_new_correlation_id", lambda: MAKE_NEW_CORRELATION_ID_VALUE, ) prepared_headers = ows_client.prepare_headers( headers={}, correlation_id=provided_correlation_id ) assert prepared_headers == {"Correlation-Id": expected_correlation_id} @pytest.mark.parametrize( "provided_correlation_id, expected_correlation_id", [ ( pytest.param( "", MAKE_NEW_CORRELATION_ID_VALUE, id="make_new_correlation_id is used when correlation id is empty string and correlation_id_getter is None", ) ), ( pytest.param( "19e1f36a-2270-11ef-a911-3e17271fba71", "19e1f36a-2270-11ef-a911-3e17271fba71", id="correlation_id is used when correlation_id_getter is None", ) ), ], ) def test_ows_client_prepare_headers_for_correlation_id_null_getter( provided_correlation_id: str | None, expected_correlation_id: str, monkeypatch: MonkeyPatch, ) -> None: """Test OwsClient prepare_headers populates correlation_id when correlation_id_getter is None.""" ows_client = OwsClient( environment="test", service_name="ows-test", ) monkeypatch.setattr( "owsclient.base.make_new_correlation_id", lambda: MAKE_NEW_CORRELATION_ID_VALUE, ) prepared_headers = ows_client.prepare_headers( headers={}, correlation_id=provided_correlation_id ) assert prepared_headers == {"Correlation-Id": expected_correlation_id} @pytest.mark.anyio @pytest.mark.parametrize( "provided_correlation_id, correlation_id_return_value, expected_correlation_id", [ ( pytest.param( "", None, MAKE_NEW_CORRELATION_ID_VALUE, id="make_new_correlation_id is used when correlation id is empty string and correlation_id_getter returns None", ) ), ( pytest.param( "", GET_CORRELATION_ID_FUNC_VALUE, GET_CORRELATION_ID_FUNC_VALUE, id="get_correlation_id_func is used when correlation id is empty string", ) ), ( pytest.param( "", "", MAKE_NEW_CORRELATION_ID_VALUE, id="make_new_correlation_id is used when correlation id is empty string and correlation_id_getter returns empty", ) ), ( pytest.param( None, None, MAKE_NEW_CORRELATION_ID_VALUE, id="make_new_correlation_id is used when correlation id is None and correlation_id_getter returns None", ) ), ( pytest.param( None, GET_CORRELATION_ID_FUNC_VALUE, GET_CORRELATION_ID_FUNC_VALUE, id="get_correlation_id_func is used when correlation id is None", ) ), ( pytest.param( None, "", MAKE_NEW_CORRELATION_ID_VALUE, id="make_new_correlation_id is used when correlation id is None and correlation_id_getter returns empty", ) ), ( pytest.param( None, None, MAKE_NEW_CORRELATION_ID_VALUE, id="make_new_correlation_id is used when correlation id is None and correlation_id_getter returns None", ) ), ( pytest.param( "19e1f36a-2270-11ef-a911-3e17271fba71", None, "19e1f36a-2270-11ef-a911-3e17271fba71", id="correlation_id is used when correlation id is non-empty string", ) ), ( pytest.param( "19e1f36a-2270-11ef-a911-3e17271fba71", GET_CORRELATION_ID_FUNC_VALUE, "19e1f36a-2270-11ef-a911-3e17271fba71", id="correlation_id is used when correlation id is non-empty string", ) ), ( pytest.param( "19e1f36a-2270-11ef-a911-3e17271fba71", "", "19e1f36a-2270-11ef-a911-3e17271fba71", id="correlation_id is used when correlation id is non-empty string", ) ), ], ) async def test_async_ows_client_prepare_headers_for_correlation_id( provided_correlation_id: str | None, correlation_id_return_value: str | None, expected_correlation_id: str, monkeypatch: MonkeyPatch, ) -> None: """Test prepare_headers populates correlation_id.""" mock_correlation_id_getter = MagicMock(return_value=correlation_id_return_value) async_ows_client = AsyncOwsClient( environment="test", service_name="ows-test", correlation_id_getter=mock_correlation_id_getter, ) monkeypatch.setattr( "owsclient.base.make_new_correlation_id", lambda: MAKE_NEW_CORRELATION_ID_VALUE, ) prepared_headers = await async_ows_client.prepare_headers( headers={}, correlation_id=provided_correlation_id ) assert prepared_headers == {"Correlation-Id": expected_correlation_id} @pytest.mark.anyio @pytest.mark.parametrize( "provided_correlation_id, expected_correlation_id", [ ( pytest.param( "", MAKE_NEW_CORRELATION_ID_VALUE, id="make_new_correlation_id is used when correlation id is empty string and correlation_id_getter is None", ) ), ( pytest.param( "19e1f36a-2270-11ef-a911-3e17271fba71", "19e1f36a-2270-11ef-a911-3e17271fba71", id="correlation_id is used when correlation_id_getter is None", ) ), ], ) async def test_async_ows_client_prepare_headers_for_correlation_id_null_getter( provided_correlation_id: str | None, expected_correlation_id: str, monkeypatch: MonkeyPatch, ) -> None: """Test AsyncOwsClient prepare_headers populates correlation_id when correlation_id_getter is None.""" async_ows_client = AsyncOwsClient( environment="test", service_name="ows-test", ) monkeypatch.setattr( "owsclient.base.make_new_correlation_id", lambda: MAKE_NEW_CORRELATION_ID_VALUE, ) prepared_headers = await async_ows_client.prepare_headers( headers={}, correlation_id=provided_correlation_id ) assert prepared_headers == {"Correlation-Id": expected_correlation_id} @pytest.mark.parametrize( "provided_headers, provided_identity_id, provided_profile_id, provided_profile_type, mock_request_context, expect_exception, expected", [ ( pytest.param( {}, None, None, None, None, True, None, id="identity id, profile id, and profile type are required when request context is None", ) ), ( pytest.param( {}, "an identity_id", None, None, None, True, None, id="profile id and profile type are required when request context is None", ) ), ( pytest.param( {}, "an identity_id", 123, None, None, True, None, id="profile type is required when request context is None", ) ), ( pytest.param( {}, "an identity_id", None, "a profile type", None, True, None, id="profile id is required when request context is None", ) ), ( pytest.param( {}, None, 123, "a profile type", None, True, None, id="identity id is required when request context is None", ) ), ( pytest.param( {}, "an identity id", 123, "a profile type", None, False, { "Apollographql-Client-Name": "ows-test", "Orchard-Identity-Id": "an identity id", "Orchard-Profile-Id": "123", "Orchard-Profile-Type": "a profile type", }, id="identity id, profile id, profile type fields are used when request context is None", ) ), ( pytest.param( {}, "an identity id", 123, "a profile type", TestRequestContext(identity_id="some identity"), False, { "Apollographql-Client-Name": "ows-test", "Orchard-Identity-Id": "an identity id", "Orchard-Profile-Id": "123", "Orchard-Profile-Type": "a profile type", }, id="identity id is used instead of RequestContext.identity_id", ) ), ( pytest.param( {}, "an identity id", 123, "a profile type", TestRequestContext(profile_id=888), False, { "Apollographql-Client-Name": "ows-test", "Orchard-Identity-Id": "an identity id", "Orchard-Profile-Id": "123", "Orchard-Profile-Type": "a profile type", }, id="profile id is used instead of RequestContext.profile_id", ) ), ( pytest.param( {}, "an identity id", 123, "a profile type", TestRequestContext(profile_type="another profile type"), False, { "Apollographql-Client-Name": "ows-test", "Orchard-Identity-Id": "an identity id", "Orchard-Profile-Id": "123", "Orchard-Profile-Type": "a profile type", }, id="profile type is used instead of RequestContext.profile_type", ) ), ( pytest.param( {}, None, None, None, TestRequestContext(), True, None, id="empty request context raises error", ) ), ( pytest.param( {}, None, None, None, TestRequestContext(identity_id="an identity"), True, None, id="request context with only identity_id raises error", ) ), ( pytest.param( {}, None, None, None, TestRequestContext(profile_id=111), True, None, id="request context with only profile_id raises error", ) ), ( pytest.param( {}, None, None, None, TestRequestContext(profile_type="this profile type"), True, None, id="request context with only profile_type raises error", ) ), ( pytest.param( {}, None, None, None, TestRequestContext( identity_id="an identity", profile_id=111, profile_type="this profile type", ), False, { "Apollographql-Client-Name": "ows-test", "Orchard-Identity-Id": "an identity", "Orchard-Profile-Id": "111", "Orchard-Profile-Type": "this profile type", }, id="request context is used", ) ), ( pytest.param( { "some-other-header": "import stuff here", "Apollographql-Client-Name": "overridden", }, None, None, None, TestRequestContext( identity_id="an identity", profile_id=111, profile_type="this profile type", ), False, { "some-other-header": "import stuff here", "Apollographql-Client-Name": "ows-test", "Orchard-Identity-Id": "an identity", "Orchard-Profile-Id": "111", "Orchard-Profile-Type": "this profile type", }, id="headers are additive", ) ), ], ) def test_base_ows_client_pass_graphql_headers( provided_headers: dict[str, str], provided_identity_id: str | None, provided_profile_id: int | None, provided_profile_type: str | None, mock_request_context: RequestContext | None, expect_exception: bool, expected: dict[str, str] | None, ) -> None: """Test BaseOwsClient returns headers for graphql.""" mock_request_context_func = MagicMock(return_value=mock_request_context) base_ows_client = BaseOwsClient( environment="test", service_name="ows-test", request_context_getter=mock_request_context_func, ) if expect_exception: with pytest.raises(ValueError): base_ows_client.pass_graphql_headers( headers=provided_headers, identity_id=provided_identity_id, profile_id=provided_profile_id, profile_type=provided_profile_type, ) else: actual = base_ows_client.pass_graphql_headers( headers=provided_headers, identity_id=provided_identity_id, profile_id=provided_profile_id, profile_type=provided_profile_type, ) assert actual == expected mock_request_context_func.assert_called_once() @pytest.mark.parametrize( "provided_headers, mock_request_context, mock_get_token_string, expected", [ ( pytest.param( {}, None, "validjwt", {"authorization": "Bearer validjwt"}, id="m2m_token_manager is used", ) ), ( pytest.param( {"authorization": "overwritten", "other-header": "retained"}, None, "validjwt", {"authorization": "Bearer validjwt", "other-header": "retained"}, id="authorization in provided headers is overwritten", ) ), ( pytest.param( {}, None, Exception("no token to get"), {}, id="m2m_token_manager exception is handled", ) ), ( pytest.param( {"authorization": "overwritten", "other-header": "retained"}, None, Exception("no token to get"), {"authorization": "overwritten", "other-header": "retained"}, id="provided headers still returned even when m2m_token_manager exception", ) ), ( pytest.param( {}, TestRequestContext(authorization="Bearer tokenfromrequestcontext"), "validjwt", {"authorization": "Bearer tokenfromrequestcontext"}, id="request context is used instead of m2m_token_manager", ) ), ], ) def test_ows_client_pass_authorization_header( provided_headers: dict[str, str], mock_request_context: RequestContext | None, mock_get_token_string: str | Exception, expected: dict[str, str], ) -> None: """Test OwsClient pass_authorization_header.""" mock_request_context_func = MagicMock(return_value=mock_request_context) mock_m2m_token_manager = MagicMock() mock_m2m_token_manager.get_token_string = MagicMock( side_effect=[mock_get_token_string] ) ows_client = OwsClient( environment="test", service_name="ows-test", request_context_getter=mock_request_context_func, m2m_token_manager=mock_m2m_token_manager, ) actual = ows_client.pass_authorization_header(provided_headers) assert actual == expected @pytest.mark.anyio @pytest.mark.parametrize( "provided_headers, mock_request_context, mock_get_token_string, expected", [ ( pytest.param( {}, None, "validjwt", {"authorization": "Bearer validjwt"}, id="m2m_token_manager is used", ) ), ( pytest.param( {"authorization": "overwritten", "other-header": "retained"}, None, "validjwt", {"authorization": "Bearer validjwt", "other-header": "retained"}, id="authorization in provided headers is overwritten", ) ), ( pytest.param( {}, None, Exception("no token to get"), {}, id="m2m_token_manager exception is handled", ) ), ( pytest.param( {"authorization": "overwritten", "other-header": "retained"}, None, Exception("no token to get"), {"authorization": "overwritten", "other-header": "retained"}, id="provided headers still returned even when m2m_token_manager exception", ) ), ( pytest.param( {}, TestRequestContext(authorization="Bearer tokenfromrequestcontext"), "validjwt", {"authorization": "Bearer tokenfromrequestcontext"}, id="request context is used instead of m2m_token_manager", ) ), ], ) async def test_async_ows_client_pass_authorization_header( provided_headers: dict[str, str], mock_request_context: RequestContext | None, mock_get_token_string: str | Exception, expected: dict[str, str], ) -> None: """Test AsyncOwsClient pass_authorization_header.""" mock_request_context_func = MagicMock(return_value=mock_request_context) mock_async_m2m_token_manager = MagicMock() mock_async_m2m_token_manager.get_token_string = AsyncMock( side_effect=[mock_get_token_string] ) async_ows_client = AsyncOwsClient( environment="test", service_name="ows-test", request_context_getter=mock_request_context_func, m2m_token_manager=mock_async_m2m_token_manager, ) actual = await async_ows_client.pass_authorization_header(provided_headers) assert actual == expected