From 36efe56d643e9fd3c1ac13e0d83e9fba625ec9f2 Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 19 Sep 2023 11:28:47 -0700 Subject: [PATCH] Expand unit testing Signed-off-by: jamshale --- .github/workflows/controller_unittests.yml | 2 +- oidc-controller/.coveragerc | 6 + oidc-controller/api/core/acapy/client.py | 9 + .../api/core/acapy/tests/__mocks__.py | 50 ++++ .../api/core/acapy/tests/test_client.py | 216 ++++++++++++++++++ .../api/core/acapy/tests/test_config.py | 51 +++++ .../api/core/oidc/issue_token_service.py | 5 +- .../api/core/oidc/tests/__mocks__.py | 122 ++++++++++ .../oidc/tests/test_issue_token_service.py | 167 ++++++++++++++ oidc-controller/api/routers/oidc.py | 3 +- oidc-controller/api/test_utils.py | 8 + oidc-controller/requirements-dev.txt | 5 +- oidc-controller/tox.ini | 16 +- 13 files changed, 643 insertions(+), 17 deletions(-) create mode 100644 oidc-controller/.coveragerc create mode 100644 oidc-controller/api/core/acapy/tests/__mocks__.py create mode 100644 oidc-controller/api/core/acapy/tests/test_client.py create mode 100644 oidc-controller/api/core/acapy/tests/test_config.py create mode 100644 oidc-controller/api/core/oidc/tests/__mocks__.py create mode 100644 oidc-controller/api/core/oidc/tests/test_issue_token_service.py create mode 100644 oidc-controller/api/test_utils.py diff --git a/.github/workflows/controller_unittests.yml b/.github/workflows/controller_unittests.yml index ec5efb3f..d4197ac7 100644 --- a/.github/workflows/controller_unittests.yml +++ b/.github/workflows/controller_unittests.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", "3.11"] + python-version: ["3.11"] steps: - uses: actions/checkout@v3 diff --git a/oidc-controller/.coveragerc b/oidc-controller/.coveragerc new file mode 100644 index 00000000..51e39a7e --- /dev/null +++ b/oidc-controller/.coveragerc @@ -0,0 +1,6 @@ +[run] +omit = + **/tests/* + *test* + *__init__* + diff --git a/oidc-controller/api/core/acapy/client.py b/oidc-controller/api/core/acapy/client.py index 3ba2b342..3ea1f080 100644 --- a/oidc-controller/api/core/acapy/client.py +++ b/oidc-controller/api/core/acapy/client.py @@ -49,7 +49,10 @@ def create_presentation_request( headers=self.agent_config.get_headers(), json=present_proof_payload, ) + + # TODO: Determine if this should assert it received a json object assert resp_raw.status_code == 200, resp_raw.content + resp = json.loads(resp_raw.content) result = CreatePresentationResponse.parse_obj(resp) @@ -66,7 +69,10 @@ def get_presentation_request(self, presentation_exchange_id: Union[UUID, str]): + str(presentation_exchange_id), headers=self.agent_config.get_headers(), ) + + # TODO: Determine if this should assert it received a json object assert resp_raw.status_code == 200, resp_raw.content + resp = json.loads(resp_raw.content) logger.debug(f"<<< get_presentation_request -> {resp}") @@ -102,9 +108,12 @@ def get_wallet_did(self, public=False) -> WalletDid: url, headers=self.agent_config.get_headers(), ) + + # TODO: Determine if this should assert it received a json object assert ( resp_raw.status_code == 200 ), f"{resp_raw.status_code}::{resp_raw.content}" + resp = json.loads(resp_raw.content) if public: diff --git a/oidc-controller/api/core/acapy/tests/__mocks__.py b/oidc-controller/api/core/acapy/tests/__mocks__.py new file mode 100644 index 00000000..1c7d4b08 --- /dev/null +++ b/oidc-controller/api/core/acapy/tests/__mocks__.py @@ -0,0 +1,50 @@ +presentation_request_configuration = { + 'name': 'proof_requested', + 'version': '0.0.1', + 'requested_attributes': { + 'req_attr_0': { + 'names': ['email'], + 'restrictions': [ + {'schema_name': 'verified-email', + 'issuer_did': 'MTYqmTBoLT7KLP5RNfgK3b'} + ], + 'non_revoked': { + 'from': 1695320203, 'to': 1695320203 + } + } + }, + 'requested_predicates': {} +} + +presentation_request = { + 'nonce': '136042354083201173353396', + 'name': 'proof_requested', + 'version': '0.0.1', + 'requested_attributes':{ + 'req_attr_0': { + 'non_revoked': {'from': 1695321803, 'to': 1695321803}, + 'restrictions': [{'schema_name': 'verified-email', 'issuer_did': 'MTYqmTBoLT7KLP5RNfgK3b'}], + 'names': ['email'] + } + }, + 'requested_predicates': {} +} + +create_presentation_response_http = { + 'updated_at': '2023-09-21T18:43:23.470373Z', + 'role': 'verifier', + 'presentation_exchange_id': 'b2945790-79c4-4059-9f93-6bd43b2186f7', + 'created_at': '2023-09-21T18:43:23.470373Z', + 'trace': False, + 'thread_id': 'ab2e3f02-6e16-4e08-8165-5ddc7aad3090', + 'initiator': 'self', + 'state': 'request_sent', + 'presentation_request': presentation_request, + 'auto_verify': True, + 'presentation_request_dict': { + '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/1.0/request-presentation', + '@id': 'ab2e3f02-6e16-4e08-8165-5ddc7aad3090', + 'request_presentations~attach': [{'@id': 'libindy-request-presentation-0', 'mime-type': 'application/json', 'data': {'base64': 'eyJuYW1lIjogInByb29mX3JlcXVlc3RlZCIsICJ2ZXJzaW9uIjogIjAuMC4xIiwgInJlcXVlc3RlZF9hdHRyaWJ1dGVzIjogeyJyZXFfYXR0cl8wIjogeyJuYW1lcyI6IFsiZW1haWwiXSwgInJlc3RyaWN0aW9ucyI6IFt7InNjaGVtYV9uYW1lIjogInZlcmlmaWVkLWVtYWlsIiwgImlzc3Vlcl9kaWQiOiAiTVRZcW1UQm9MVDdLTFA1Uk5mZ0szYiJ9XSwgIm5vbl9yZXZva2VkIjogeyJmcm9tIjogMTY5NTMyMTgwMywgInRvIjogMTY5NTMyMTgwM319fSwgInJlcXVlc3RlZF9wcmVkaWNhdGVzIjoge30sICJub25jZSI6ICIxMzYwNDIzNTQwODMyMDExNzMzNTMzOTYifQ=='}}] + }, + 'auto_present': False +} diff --git a/oidc-controller/api/core/acapy/tests/test_client.py b/oidc-controller/api/core/acapy/tests/test_client.py new file mode 100644 index 00000000..9680f759 --- /dev/null +++ b/oidc-controller/api/core/acapy/tests/test_client.py @@ -0,0 +1,216 @@ +import json + +import mock +import pytest +from api.core.acapy.client import (CREATE_PRESENTATION_REQUEST_URL, + PRESENT_PROOF_RECORDS, + PUBLIC_WALLET_DID_URI, WALLET_DID_URI, + AcapyClient) +from api.core.acapy.config import MultiTenantAcapy, SingleTenantAcapy +from api.core.acapy.models import CreatePresentationResponse, WalletDid +from api.core.acapy.tests.__mocks__ import (create_presentation_response_http, + presentation_request_configuration) +from api.core.config import settings + + +@pytest.mark.asyncio +@mock.patch.object(settings, "ACAPY_TENANCY", None) +async def test_init_no_setting_returns_client_with_single_tenancy_config(): + client = AcapyClient() + assert client is not None + assert isinstance(client.agent_config, SingleTenantAcapy) is True + + +@pytest.mark.asyncio +@mock.patch.object(settings, "ACAPY_TENANCY", "single") +async def test_init_single_returns_client_with_single_tenancy_config(): + client = AcapyClient() + assert client is not None + assert isinstance(client.agent_config, SingleTenantAcapy) is True + + +@pytest.mark.asyncio +@mock.patch.object(settings, "ACAPY_TENANCY", "multi") +async def test_init_multi_returns_client_with_multi_tenancy_config(): + client = AcapyClient() + assert client is not None + assert isinstance(client.agent_config, MultiTenantAcapy) is True + + +@pytest.mark.asyncio +async def test_create_presentation_returns_sucessfully_with_valid_data(requests_mock): + requests_mock.post( + settings.ACAPY_ADMIN_URL + CREATE_PRESENTATION_REQUEST_URL, + headers={}, + json=json.dumps(create_presentation_response_http), + status_code=200, + ) + + with mock.patch.object(CreatePresentationResponse, "parse_obj", return_value={'result': 'success'}): + client = AcapyClient() + client.agent_config.get_headers = mock.MagicMock( + return_value={'x-api-key': ''}) + presentation_request = client.create_presentation_request( + presentation_request_configuration) + assert presentation_request is not None + + +@pytest.mark.asyncio +async def test_create_presentation_throws_assertion_error_with_non_200_response_from_acapy(requests_mock): + requests_mock.post( + settings.ACAPY_ADMIN_URL + CREATE_PRESENTATION_REQUEST_URL, + headers={}, + json=json.dumps(create_presentation_response_http), + status_code=400, + ) + + with mock.patch.object(CreatePresentationResponse, "parse_obj", return_value={'result': 'success'}): + client = AcapyClient() + client.agent_config.get_headers = mock.MagicMock( + return_value={'x-api-key': ''}) + try: + presentation_request = client.create_presentation_request( + presentation_request_configuration) + assert presentation_request is not None + except AssertionError as e: + assert e is not None + +# TODO: determine if this function should assert a valid json response +@pytest.mark.asyncio +async def test_create_presentation_throws_error_with_non_json_from_acapy(requests_mock): + requests_mock.post( + settings.ACAPY_ADMIN_URL + CREATE_PRESENTATION_REQUEST_URL, + headers={}, + status_code=200, + ) + + with mock.patch.object(CreatePresentationResponse, "parse_obj", return_value={'result': 'success'}): + client = AcapyClient() + client.agent_config.get_headers = mock.MagicMock( + return_value={'x-api-key': ''}) + try: + presentation_request = client.create_presentation_request( + presentation_request_configuration) + assert presentation_request is not None + except json.JSONDecodeError as e: + assert e is not None + + +@pytest.mark.asyncio +async def test_get_presentation_returns_sucessfully_with_valid_data(requests_mock): + requests_mock.get( + settings.ACAPY_ADMIN_URL + PRESENT_PROOF_RECORDS + "/" + "1234-567890", + headers={}, + json={"result": "success"}, + status_code=200, + ) + + client = AcapyClient() + client.agent_config.get_headers = mock.MagicMock( + return_value={'x-api-key': ''}) + presentation = client.get_presentation_request("1234-567890") + assert presentation is not None + + +@pytest.mark.asyncio +async def test_get_presentation_throws_assertion_error_for_non_200_response_from_acapy(requests_mock): + requests_mock.get( + settings.ACAPY_ADMIN_URL + PRESENT_PROOF_RECORDS + "/" + "1234-567890", + headers={}, + json={"result": "success"}, + status_code=400, + ) + + client = AcapyClient() + client.agent_config.get_headers = mock.MagicMock( + return_value={'x-api-key': ''}) + try: + client.get_presentation_request("1234-567890") + except AssertionError as e: + assert e is not None + + +@pytest.mark.asyncio +async def test_verify_presentation_returns_sucessfully_with_valid_data(requests_mock): + requests_mock.post( + settings.ACAPY_ADMIN_URL + PRESENT_PROOF_RECORDS + + "/" + "1234-567890" + "/verify-presentation", + headers={}, + json={"result": "success"}, + status_code=200, + ) + + client = AcapyClient() + client.agent_config.get_headers = mock.MagicMock( + return_value={'x-api-key': ''}) + verification = client.verify_presentation("1234-567890") + assert verification is not None + + +@pytest.mark.asyncio +async def test_verify_presentation_throws_assertion_error_for_non_200_response_from_acapy(requests_mock): + requests_mock.post( + settings.ACAPY_ADMIN_URL + PRESENT_PROOF_RECORDS + + "/" + "1234-567890" + "/verify-presentation", + headers={}, + json={"result": "success"}, + status_code=400, + ) + + client = AcapyClient() + client.agent_config.get_headers = mock.MagicMock( + return_value={'x-api-key': ''}) + try: + client.verify_presentation("1234-567890") + except AssertionError as e: + assert e is not None + + +@pytest.mark.asyncio +async def test_get_wallet_did_public_returns_sucessfully_on_public_url_and_simple_resp(requests_mock): + requests_mock.get( + settings.ACAPY_ADMIN_URL + PUBLIC_WALLET_DID_URI, + headers={}, + json={"result": "success"}, + status_code=200, + ) + with mock.patch.object(WalletDid, "parse_obj", return_value={'result': 'success'}): + client = AcapyClient() + client.agent_config.get_headers = mock.MagicMock( + return_value={'x-api-key': ''}) + wallet_resp = client.get_wallet_did(public=True) + assert wallet_resp is not None + + +@pytest.mark.asyncio +async def test_get_wallet_did_public_throws_assertion_error_on_non_200_response(requests_mock): + requests_mock.get( + settings.ACAPY_ADMIN_URL + PUBLIC_WALLET_DID_URI, + headers={}, + json={"result": "success"}, + status_code=400, + ) + with mock.patch.object(WalletDid, "parse_obj", return_value={'result': 'success'}): + client = AcapyClient() + client.agent_config.get_headers = mock.MagicMock( + return_value={'x-api-key': ''}) + try: + client.get_wallet_did(public=True) + except AssertionError as e: + assert e is not None + + +@pytest.mark.asyncio +async def test_get_wallet_did_not_public_returns_sucessfully_on_correct_url_and_processes_array(requests_mock): + requests_mock.get( + settings.ACAPY_ADMIN_URL + WALLET_DID_URI, + headers={}, + json={"results": ["success"]}, + status_code=200, + ) + with mock.patch.object(WalletDid, "parse_obj", return_value={'result': 'success'}): + client = AcapyClient() + client.agent_config.get_headers = mock.MagicMock( + return_value={'x-api-key': ''}) + wallet_resp = client.get_wallet_did(public=False) + assert wallet_resp is not None diff --git a/oidc-controller/api/core/acapy/tests/test_config.py b/oidc-controller/api/core/acapy/tests/test_config.py new file mode 100644 index 00000000..b252710a --- /dev/null +++ b/oidc-controller/api/core/acapy/tests/test_config.py @@ -0,0 +1,51 @@ +import mock +import pytest +from api.core.acapy.config import MultiTenantAcapy, SingleTenantAcapy +from api.core.config import settings + + +@pytest.mark.asyncio +@mock.patch.object(settings, "ST_ACAPY_ADMIN_API_KEY_NAME", 'name') +@mock.patch.object(settings, "ST_ACAPY_ADMIN_API_KEY", 'key') +async def test_single_tenant_has_expected_headers(): + acapy = SingleTenantAcapy() + headers = acapy.get_headers() + assert headers == {'name': 'key'} + + +@pytest.mark.asyncio +async def test_multi_tenant_get_headers_returns_bearer_token_auth(requests_mock): + acapy = MultiTenantAcapy() + acapy.get_wallet_token = mock.MagicMock(return_value='token') + headers = acapy.get_headers() + assert headers == {"Authorization": "Bearer token"} + + +@pytest.mark.asyncio +async def test_multi_tenant_get_wallet_token_returns_token_at_token_key(requests_mock): + requests_mock.post( + settings.ACAPY_ADMIN_URL + "/multitenancy/wallet/wallet_id/token", + headers={}, + json={'token': 'token'}, + status_code=200, + ) + acapy = MultiTenantAcapy() + acapy.wallet_id = 'wallet_id' + token = acapy.get_wallet_token() + assert token == 'token' + + +@pytest.mark.asyncio +async def test_multi_tenant_throws_assertion_error_for_non_200_response(requests_mock): + requests_mock.post( + settings.ACAPY_ADMIN_URL + "/multitenancy/wallet/wallet_id/token", + headers={}, + json={'token': 'token'}, + status_code=400, + ) + acapy = MultiTenantAcapy() + acapy.wallet_id = 'wallet_id' + try: + acapy.get_wallet_token() + except AssertionError as e: + assert e is not None diff --git a/oidc-controller/api/core/oidc/issue_token_service.py b/oidc-controller/api/core/oidc/issue_token_service.py index 6449447c..9b0711e2 100644 --- a/oidc-controller/api/core/oidc/issue_token_service.py +++ b/oidc-controller/api/core/oidc/issue_token_service.py @@ -32,7 +32,7 @@ class Token(BaseModel): @classmethod def get_claims( - cls, pres_exch: Dict, auth_session: AuthSession, ver_config: VerificationConfig + cls, auth_session: AuthSession, ver_config: VerificationConfig ) -> dict[str, str]: """Converts vc presentation values to oidc claims""" oidc_claims: List[Claim] = [ @@ -108,7 +108,8 @@ def get_claims( {c.type: c.value for c in presentation_claims.values()} ) return result - + + # TODO: Determine if this is useful to keep, and remove it if it's not. It is currently unused. # renames and calculates dict members appropriate to # https://openid.net/specs/openid-connect-core-1_0.html#IDToken # and diff --git a/oidc-controller/api/core/oidc/tests/__mocks__.py b/oidc-controller/api/core/oidc/tests/__mocks__.py new file mode 100644 index 00000000..e020a514 --- /dev/null +++ b/oidc-controller/api/core/oidc/tests/__mocks__.py @@ -0,0 +1,122 @@ + + +from datetime import datetime, timedelta + +from api.verificationConfigs.models import VerificationConfig, VerificationProofRequest +from api.authSessions.models import AuthSession + +presentation = { + "thread_id": "428ab5dc-185f-40ba-8714-498c79b822f3", + "created_at": "2023-09-15T17:49:16.397954Z", + "role": "verifier", + "auto_present": False, + "presentation_exchange_id": "ccaac3c5-1606-44fb-ade3-33937dfb6dca", + "presentation_request": { + "nonce": "633368193772519315256591", + "name": "proof_requested", + "version": "0.0.1", + "requested_attributes": "invalid", # Add test requested attributes + "requested_predicates": {}, + }, + "presentation": { + "proof": { + "proofs": [ + { + "primary_proof": { + "eq_proof": { + "revealed_attrs": { + "email": "73814602767252868561268261832462872577293109184327908660400248444458427915643" + }, + "a_prime": "40809417934849907123261471333411809633354074436405510819719547561843004255793023387498104889708571270607131703938696756407487467873368373775514806534499374680059274765067020721416649641170535056242622419292879011504372419414431627060123535951854477609020119038446707071877530649018798857842493513975477809431501443034563297114458359206476293934461316378865994820249592804467758433904174100097026785285885805688715928578812499534293751388422584754995155726212908115142236221995953756086868367889189436621196564054394071524712971126196703085030819194540892286515361206473918715176334283198231106756804249201321110676817", + "e": "132166309846004002298968329630750626534906193461199076364226183288855464715230907038994149984215600411276690314987056469395087923423376034", + "v": "389852336354596234620050863642411241608940316153319185423050526707483503655636070718528707360249027781424214465818771183857876571898645233712049550065615437328753383559647676180932006072233802815800996667626559757294734656704881893512658110558847026240354780006408044914149694965180223127754340423475416000940696265773581100989216553579233198807834530514088943560794612627439000580021272527304012796141460444099028132444095750454921031441117671745673457339631909515144154149520136598843334638440138229207476684776682052027672414220327439299122271829090948693982983191662066627130404544959976637513561609461174550463471636750546362339385232864872096796748538286120598703640917737216094473317639418313144992206875604615452306826430748790105881684965109026881701380306409317275248915954859998470708379642658766566479932465684746033865495941061980422316943701392367332573162799073428328950386460662125690364635698421614848860", + "m": { + "time": "11896542455624181868867605459032062290636202825612483735571300567506766112289589216989585608752835036405824270689163034684130257627062069669940002269643613585904726141499206139998", + "master_secret": "11978669714570126125906674715703732098298259897696576462078203241042378591463388624826815153862705227192729264588296043944542578244619153416802465506231069348686312732305137579470", + }, + "m2": "5273201069177175286302918970464632772021241766353736934881903190951819578898991714048991692651562372414552361008106867369251847950990157935703897032904209115054769991615534757984", + }, + "ge_proofs": [], + } + } + ], + "aggregated_proof": { + "c_hash": "8135055767072243139738404741550925116429855319200452769474586673630216912237", + "c_list": + [[1]], # Not complete data + }, + }, + "requested_proof": { + "revealed_attrs": {}, + "revealed_attr_groups": "invalid", # Add test revealed_attr_groups + "self_attested_attrs": {}, + "unrevealed_attrs": {}, + "predicates": {}, + }, + "identifiers": [ + { + "schema_id": "MTYqmTBoLT7KLP5RNfgK3b:2:verified-email:1.2.3", + "cred_def_id": "MTYqmTBoLT7KLP5RNfgK3b:3:CL:160342:default", + } + ], + }, + "verified": "true", + "state": "verified", + "presentation_request_dict": { + "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/present-proof/1.0/request-presentation", + "@id": "428ab5dc-185f-40ba-8714-498c79b822f3", + "request_presentations~attach": [ + { + "@id": "libindy-request-presentation-0", + "mime-type": "application/json", + "data": { + "base64": "eyJuYW1lIjogInByb29mX3JlcXVlc3RlZCIsICJ2ZXJzaW9uIjogIjAuMC4xIiwgInJlcXVlc3RlZF9hdHRyaWJ1dGVzIjogeyJyZXFfYXR0cl8wIjogeyJuYW1lcyI6IFsiZW1haWwiXSwgInJlc3RyaWN0aW9ucyI6IFt7InNjaGVtYV9uYW1lIjogInZlcmlmaWVkLWVtYWlsIiwgImlzc3Vlcl9kaWQiOiAiTVRZcW1UQm9MVDdLTFA1Uk5mZ0szYiJ9XSwgIm5vbl9yZXZva2VkIjogeyJmcm9tIjogMTY5NDgwMDE1NiwgInRvIjogMTY5NDgwMDE1Nn19fSwgInJlcXVlc3RlZF9wcmVkaWNhdGVzIjoge30sICJub25jZSI6ICI2MzMzNjgxOTM3NzI1MTkzMTUyNTY1OTEifQ==" + }, + } + ], + }, + "initiator": "self", + "updated_at": "2023-09-15T17:49:33.477755Z", + "trace": False, + "auto_verify": True, + "verified_msgs": ["RMV_GLB_NRI", "RMV_RFNT_NRI::req_attr_0"], +} + +auth_session = AuthSession( + pres_exch_id="e444bc3e-346d-47d1-882d-39c014b8978c", + expired_timestamp=datetime.now() + timedelta(seconds=3000), + ver_config_id="verified-email", + request_parameters={ + 'scope': 'openid vc_authn', + 'state': 'oFLNfUyzDtWHmc61dNiQZkVZRsRUUXZ5KZIiQBeQuJQ.xfaKQBh1xfQ.T02DEr3QRTmMUfjegc9fQQ', + 'response_type': 'code', + 'client_id': 'keycloak', + 'redirect_uri': 'http://localhost:8880/auth/realms/vc-authn/broker/vc-authn/endpoint', + 'pres_req_conf_id': 'verified-email', + 'nonce': 'J2o8dDBWAZyov0ipkMPZng' + }, + pyop_auth_code="str", + response_url="str", + proof_status="pending", +) + +ver_config = VerificationConfig( + ver_config_id="verified-email", + subject_identifier="email", + proof_request=VerificationProofRequest( + name="BCGov Verified Email", + version="1.0", + requested_attributes=[ + { + "names": ["email"], + "restrictions": [ + { + "schema_name": "verified-email", + "issuer_did": "MTYqmTBoLT7KLP5RNfgK3b" + } + ] + } + ], + requested_predicates=[] + ) +) diff --git a/oidc-controller/api/core/oidc/tests/test_issue_token_service.py b/oidc-controller/api/core/oidc/tests/test_issue_token_service.py new file mode 100644 index 00000000..96ffb4da --- /dev/null +++ b/oidc-controller/api/core/oidc/tests/test_issue_token_service.py @@ -0,0 +1,167 @@ +import mock +import pytest +from api.authSessions.models import AuthSession +from api.core.oidc.issue_token_service import Token +from api.core.oidc.tests.__mocks__ import auth_session, presentation, ver_config +from api.test_utils import is_valid_uuid + +basic_valid_requested_attributes = { + "req_attr_0": { + "names": ["email"], + "restrictions": [ + { + "schema_name": "verified-email", + "issuer_did": "MTYqmTBoLT7KLP5RNfgK3b", + } + ], + } +} + +basic_valid_revealed_attr_groups = { + "req_attr_0": { + "sub_proof_index": 0, + "values": { + "email": { + "raw": "jamiehalebc@gmail.com", + "encoded": "73814602767252868561268261832462872577293109184327908660400248444458427915643", + } + } + } +} + + +@pytest.mark.asyncio +async def test_valid_proof_presentation_with_one_attribute_returns_claims(): + presentation['presentation_request']['requested_attributes'] = basic_valid_requested_attributes + presentation['presentation']['requested_proof']['revealed_attr_groups'] = basic_valid_revealed_attr_groups + with mock.patch.object(AuthSession, "presentation_exchange", presentation): + claims = Token.get_claims(auth_session, ver_config) + assert claims is not None + + +@pytest.mark.asyncio +async def test_valid_proof_presentation_with_multiple_attributes_returns_claims(): + presentation['presentation_request']['requested_attributes'] = { + "req_attr_0": { + "names": ["email"], + "restrictions": [ + { + "schema_name": "verified-email", + "issuer_did": "MTYqmTBoLT7KLP5RNfgK3b", + } + ], + }, + "req_attr_1": { + "names": ["age"], + "restrictions": [ + { + "schema_name": "verified-age", + "issuer_did": "MTYqmTBoLT7KLP5RNfgK3c", + } + ], + } + } + presentation['presentation']['requested_proof']['revealed_attr_groups'] = { + "req_attr_0": { + "sub_proof_index": 0, + "values": { + "email": { + "raw": "jamiehalebc@gmail.com", + "encoded": "73814602767252868561268261832462872577293109184327908660400248444458427915643", + } + } + }, + "req_attr_1": { + "sub_proof_index": 0, + "values": { + "age": { + "raw": "30", + "encoded": "73814602767252868561268261832462872577293109184327908660400248444458427915644", + } + } + } + } + with mock.patch.object(AuthSession, "presentation_exchange", presentation): + claims = Token.get_claims(auth_session, ver_config) + assert claims is not None + + +@pytest.mark.asyncio +async def test_valid_proof_presentation_with_one_attribute_and_multiple_values_returns_claims(): + presentation['presentation_request']['requested_attributes'] = { + "req_attr_0": { + "names": ["email_1", "age_1"], + "restrictions": [ + { + "schema_name": "verified-email", + "issuer_did": "MTYqmTBoLT7KLP5RNfgK3b", + } + ], + }, + } + presentation['presentation']['requested_proof']['revealed_attr_groups'] = { + "req_attr_0": { + "sub_proof_index": 0, + "values": { + "email_1": { + "raw": "jamiehalebc@gmail.com", + "encoded": "73814602767252868561268261832462872577293109184327908660400248444458427915643", + }, + "age_1": { + "raw": "30", + "encoded": "73814602767252868561268261832462872577293109184327908660400248444458427915644", + } + } + } + } + with mock.patch.object(AuthSession, "presentation_exchange", presentation): + claims = Token.get_claims(auth_session, ver_config) + assert claims is not None + + +@pytest.mark.asyncio +async def test_revealed_attrs_dont_match_requested_attributes_throws_exception(): + presentation['presentation_request']['requested_attributes'] = { + "req_attr_0": { + "names": ["email"], + "restrictions": [ + { + "schema_name": "verified-email", + "issuer_did": "MTYqmTBoLT7KLP5RNfgK3b", + } + ], + } + } + presentation['presentation']['requested_proof']['revealed_attr_groups'] = { + "req_attr_0": { + "sub_proof_index": 0, + "values": { + "email-wrong": { + "raw": "jamiehalebc@gmail.com", + "encoded": "73814602767252868561268261832462872577293109184327908660400248444458427915643", + } + } + } + } + with mock.patch.object(AuthSession, "presentation_exchange", presentation): + with pytest.raises(Exception): + Token.get_claims(auth_session, ver_config) + + +@pytest.mark.asyncio +async def test_valid_presentation_with_matching_subject_identifier_has_identifier_in_claims_sub(): + presentation['presentation_request']['requested_attributes'] = basic_valid_requested_attributes + presentation['presentation']['requested_proof']['revealed_attr_groups'] = basic_valid_revealed_attr_groups + with mock.patch.object(AuthSession, "presentation_exchange", presentation): + claims = Token.get_claims(auth_session, ver_config) + print(claims) + assert claims["sub"] == "jamiehalebc@gmail.com" + +@pytest.mark.asyncio +async def test_valid_presentation_with_non_matching_subject_identifier_and_has_uuid_in_claims_sub(): + presentation['presentation_request']['requested_attributes'] = basic_valid_requested_attributes + presentation['presentation']['requested_proof']['revealed_attr_groups'] = basic_valid_revealed_attr_groups + with mock.patch.object(AuthSession, "presentation_exchange", presentation): + ver_config.subject_identifier = "not-email" + claims = Token.get_claims(auth_session, ver_config) + assert is_valid_uuid(claims["sub"]) is True diff --git a/oidc-controller/api/routers/oidc.py b/oidc-controller/api/routers/oidc.py index 53c411a1..da12b09b 100644 --- a/oidc-controller/api/routers/oidc.py +++ b/oidc-controller/api/routers/oidc.py @@ -165,8 +165,7 @@ async def post_token(request: Request, db: Database = Depends(get_db)): auth_session = await AuthSessionCRUD(db).get_by_pyop_auth_code(model.get("code")) ver_config = await VerificationConfigCRUD(db).get(auth_session.ver_config_id) - presentation = client.get_presentation_request(auth_session.pres_exch_id) - claims = Token.get_claims(presentation, auth_session, ver_config) + claims = Token.get_claims(auth_session, ver_config) # modify subject identifier value to use vc-attribute as configured new_sub = claims.pop("sub") diff --git a/oidc-controller/api/test_utils.py b/oidc-controller/api/test_utils.py new file mode 100644 index 00000000..2a8176f4 --- /dev/null +++ b/oidc-controller/api/test_utils.py @@ -0,0 +1,8 @@ +import uuid + +def is_valid_uuid(value: str): + try: + uuid.UUID(str(value)) + return True + except ValueError: + return False \ No newline at end of file diff --git a/oidc-controller/requirements-dev.txt b/oidc-controller/requirements-dev.txt index ff65b7a0..6986fa9d 100644 --- a/oidc-controller/requirements-dev.txt +++ b/oidc-controller/requirements-dev.txt @@ -1,5 +1,8 @@ black==23.3.0 flake8==6.0.0 +mock==4.0.3 mongomock==4.1.2 -pytest==7.3.1 pytest-asyncio==0.21.1 +pytest-cov==4.1.0 +pytest==7.3.1 +requests-mock==1.11.0 diff --git a/oidc-controller/tox.ini b/oidc-controller/tox.ini index 8dd72d1a..c4cc0ca3 100644 --- a/oidc-controller/tox.ini +++ b/oidc-controller/tox.ini @@ -4,16 +4,12 @@ extend-ignore = E203 [tox] skipsdist = True -envlist = py310 +envlist = py311 [testenv] deps= - flake8 - pytest - pytest_asyncio -r requirements.txt - black - coverage + -r requirements-dev.txt [testenv:lint] skip_install = false @@ -24,15 +20,13 @@ commands = [testenv:test] skip_install = false commands = - pytest tests --asyncio-mode=strict + pytest [testenv:coverage] skip_install = false commands = - coverage --version - coverage run -m pytest tests --asyncio-mode=strict - coverage xml + pytest --cov-config=.coveragerc --cov . --cov-report=html [gh-actions] python = - 3.10: py310 + 3.11: py311