"""Test M2M config schemas.""" import uuid from typing import Any import pytest from pydantic import ValidationError from m2mconfig.schemas import ( ClientCredentials, ClientGrants, ClientGrantsValidator, ClientMetadata, NoAwsAccountException, RegistryEntry, ) @pytest.mark.parametrize( "payload, expect_exception, expected", [ ( {}, True, None, ), ( {"m2m_identity_uuid": "not.a.uuid"}, True, None, ), ( { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26", "can_impersonate": 123, }, True, None, ), ( {"m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26"}, False, { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26", "can_impersonate": False, }, ), ( { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26", "can_impersonate": False, }, False, { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26", "can_impersonate": False, }, ), ( { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26", "can_impersonate": True, }, False, { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26", "can_impersonate": True, }, ), ], ) def test_client_metadata( payload: dict[str, Any], expect_exception: bool, expected: dict[str, Any], ) -> None: """Test ClientMetadata schema.""" if expect_exception: with pytest.raises(ValidationError): ClientMetadata.model_validate(payload) else: assert ClientMetadata.model_validate(payload) == ClientMetadata.model_validate( expected ) @pytest.mark.parametrize( "payload, expected", [ pytest.param( { "m2m_identity_uuid": uuid.UUID("0faf537c-851c-4d04-bd52-1633016a6f26"), "can_impersonate": False, }, { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26", "can_impersonate": "false", }, id="uuid and bool are converted to string", ), pytest.param( {"m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26"}, { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26", "can_impersonate": "false", }, id="uuid and bool are still converted to string", ), pytest.param( { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26", "can_impersonate": True, }, { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26", "can_impersonate": "true", }, id="when bool is true, it is still converted to string", ), ], ) def test_client_metadata_model_dump( payload: dict[str, Any], expected: dict[str, Any], ) -> None: """Test ClientMetadata schema's behavior for model_dump().""" client_metadata = ClientMetadata.model_validate(payload) assert client_metadata.model_dump() == expected @pytest.mark.parametrize( "payload, expect_exception, expected, description", [ ( { "name": "", "prod_aws_account": "437795906767", "qa_aws_account": "437795906767", "client_metadata": { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26" }, }, True, None, "Should fail due to empty name field.", ), ( { "name": "pp-m2m-config-test-machine-to-machine", "prod_aws_account": "43779--", "qa_aws_account": "437795906767", "client_metadata": { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26" }, }, True, None, "Should fail due to prod_aws_account shorter than 12 chars.", ), ( { "name": "pp-m2m-config-test-machine-to-machine", "prod_aws_account": "437795906767", "qa_aws_account": "437795906767437795906767437795906767437795906767437795906767", "client_metadata": { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26" }, }, True, None, "Should fail due to qa_aws_account longer than 12 chars.", ), ( { "name": "pp-m2m-config-test-machine-to-machine", "prod_aws_account": "437795906767", "qa_aws_account": "437795906767", }, True, None, "Should fail due to missing client_metadata attribute.", ), ( { "name": "pp-m2m-config-test-machine-to-machine", "prod_aws_account": "437795906767", "qa_aws_account": "437795906767", "uat_aws_account": "43779590676", "client_metadata": { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26" }, }, True, None, "Should fail if uat_aws_account is invalid length.", ), ( { "name": "pp-m2m-config-test-machine-to-machine", "prod_aws_account": "437795906767", "qa_aws_account": "437795906767", "client_metadata": { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26" }, }, False, { "name": "pp-m2m-config-test-machine-to-machine", "prod_aws_account": "437795906767", "qa_aws_account": "437795906767", "client_metadata": { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26" }, }, "Should succeed when all fields are present and valid.", ), ( { "name": "pp-m2m-config-test-machine-to-machine", "prod_aws_account": "437795906767", "qa_aws_account": "437795906767", "uat_aws_account": " 285943604611 ", "client_metadata": { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26" }, }, False, { "name": "pp-m2m-config-test-machine-to-machine", "prod_aws_account": "437795906767", "qa_aws_account": "437795906767", "uat_aws_account": "285943604611", "client_metadata": { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26" }, }, "Should succeed when optional uat_aws_account is present, and if it has whitespace.", ), ( { "name": " pp-m2m-config-test-machine-to-machine", "prod_aws_account": "437795906767 ", "qa_aws_account": " 437795906767", "client_metadata": { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26" }, }, False, { "name": "pp-m2m-config-test-machine-to-machine", "prod_aws_account": "437795906767", "qa_aws_account": "437795906767", "client_metadata": { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26" }, }, "Should trim whitespace on string fields.", ), ( { "name": " pp-m2m-config-test-machine-to-machine", "prod_aws_account": "031099521156", "qa_aws_account": " 591204808501", "client_metadata": { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26" }, }, False, { "name": "pp-m2m-config-test-machine-to-machine", "prod_aws_account": "031099521156", "qa_aws_account": "591204808501", "client_metadata": { "m2m_identity_uuid": "0faf537c-851c-4d04-bd52-1633016a6f26" }, }, "Should succeed when prod_aws_account and qa_aws_account are different values.", ), ], ) def test_registry_entry( payload: dict[str, Any], expect_exception: bool, expected: dict[str, Any], description: str, ) -> None: """Test RegistryEntry schema.""" if expect_exception: with pytest.raises(ValidationError): RegistryEntry.model_validate(payload) else: assert RegistryEntry.model_validate(payload) == RegistryEntry.model_validate( expected ) @pytest.mark.parametrize( "environment, expected", [ pytest.param("qa", "qaqaqaqaqaqa", id="qa returns the qa aws account"), pytest.param("prod", "prodprodprod", id="prod returns the prod aws account"), pytest.param("PROD", "qaqaqaqaqaqa", id="environment check is case-sensitive"), pytest.param("dev", "qaqaqaqaqaqa", id="dev returns the qa aws account"), pytest.param("uat", "uatuatuatuat", id="uat returns the uat aws account"), pytest.param( "really anything", "qaqaqaqaqaqa", id="anything but prod returns the qa aws account", ), ], ) def test_config_entry_get_aws_account( environment: str, expected: str, mock_registry_entry: RegistryEntry, ) -> None: """Test RegistryEntry get_aws_account.""" actual = mock_registry_entry.get_aws_account(environment) assert actual == expected def test_config_entry_get_uat_aws_account_raises( mock_entry_name: str, mock_registry_entry: RegistryEntry, ) -> None: """Test RegistryEntry get_aws_account raises when uat is not configured.""" mock_registry_entry.uat_aws_account = None with pytest.raises(NoAwsAccountException) as exc: mock_registry_entry.get_aws_account("uat") assert exc.value.args == (f"{mock_entry_name} has no uat_aws_account configured.",) @pytest.mark.parametrize( "payload, expect_exception, expected", [ pytest.param( {"client_id": "cid", "client_secret": "shh"}, True, None, id="audience is required", ), pytest.param( {"audience": "you", "client_secret": "shh"}, True, None, id="client_id is required", ), pytest.param( {"audience": "you", "client_id": "cid"}, True, NotImplementedError, id="client_secret is required", ), pytest.param( {"audience": "you", "client_id": "cid", "client_secret": "shh"}, False, { "audience": "you", "client_id": "cid", "client_secret": "shh", "grant_type": "client_credentials", }, id="grant_type has default value", ), pytest.param( { "audience": "you", "client_id": "cid", "client_secret": "shh", "grant_type": "service_creds", }, False, { "audience": "you", "client_id": "cid", "client_secret": "shh", "grant_type": "service_creds", }, id="provided key values are used", ), ], ) def test_client_credentials( payload: dict[str, Any], expect_exception: bool, expected: dict[str, Any], ) -> None: """Test ClientCredentials schema.""" if expect_exception: with pytest.raises(ValidationError): ClientCredentials.model_validate(payload) else: assert ClientCredentials.model_validate( payload ) == ClientCredentials.model_validate(expected) @pytest.mark.parametrize( "as_string, expect_exception, expected", [ pytest.param( "", True, None, id="empty string is not valid", ), pytest.param( "some words", True, None, id="some words are not going to cut it.", ), pytest.param( "{}", True, None, id="stringified empty dict is not valid", ), pytest.param( '{"grant_type":"client_credentials"}', True, None, id="missing required fields is not valid", ), pytest.param( '{"audience":"TODO","client_id":"123","client_secret":"456","grant_type":"client_credentials"}', False, ClientCredentials( audience="TODO", client_id="123", client_secret="456", grant_type="client_credentials", ), id="all values are provided", ), pytest.param( '{"audience":"TODO","client_id":"123","client_secret":"456"}', False, ClientCredentials( audience="TODO", client_id="123", client_secret="456", grant_type="client_credentials", ), id="valid even when no grant_type", ), ], ) def test_client_credentials_model_validate_json( as_string: str, expect_exception: bool, expected: ClientCredentials, ) -> None: """Test ClientCredentials schema can validate a string.""" if expect_exception: with pytest.raises(ValidationError): ClientCredentials.model_validate_json(as_string) else: actual = ClientCredentials.model_validate_json(as_string) assert actual == expected @pytest.mark.parametrize( "payload, expect_exception, expected", [ pytest.param( { "id": "test-id", "client_id": "test-client-id", "audience": "https://my-audience.co", "scope": [], }, False, { "id": "test-id", "client_id": "test-client-id", "audience": "https://my-audience.co", "scope": [], }, id="No error when the object has all fields.", ), pytest.param( { "client_id": "test-client-id", "audience": "https://my-audience.co", "scope": [], }, True, None, id="Should fail when the id field is missing.", ), pytest.param( { "id": "test-id", "audience": "https://my-audience.co", "scope": [], }, True, None, id="should fail when the client_id field is missing.", ), pytest.param( { "id": "test-id", "client_id": "test-client-id", "scope": [], }, True, None, id="Should fail when then audience field is missing.", ), pytest.param( { "id": "test-id", "client_id": "test-client-id", "audience": "https://my-audience.co", }, True, None, id="Should fail when the scopes field is missing.", ), ], ) def test_client_grants( payload: dict[str, Any], expect_exception: bool, expected: dict[str, Any] | None, ) -> None: """Test the ClientGrants schema.""" if expect_exception: with pytest.raises(ValidationError): ClientGrants.model_validate(payload) else: assert ClientGrants.model_validate(payload) == ClientGrants.model_validate( expected ) @pytest.mark.parametrize( "payload,expected", [ pytest.param( [ { "id": "cgr_LaIA1RevCFJ73b91", "client_id": "fake-client-id", "audience": "https://workstation.qaorch.com/api", "scope": [], }, { "id": "cgr_WghAaLaWK0qqRH0v", "client_id": "fake-client-id", "audience": "https://qa-ows.theorchard.io", "scope": [], }, ], [ ClientGrants( id="cgr_LaIA1RevCFJ73b91", client_id="fake-client-id", audience="https://workstation.qaorch.com/api", scope=[], ), ClientGrants( id="cgr_WghAaLaWK0qqRH0v", client_id="fake-client-id", audience="https://qa-ows.theorchard.io", scope=[], ), ], id="Non empty list validates OK.", ), pytest.param( [], [], id="Empty list validates OK.", ), ], ) def test_client_grant_validator( payload: list[dict[str, Any]], expected: list[ClientGrants], ) -> None: """Test the ClientGrantsValidator TypeAdapter schema.""" assert ClientGrantsValidator.validate_python(payload) == expected