diff --git a/ce/iir/api/iir_client/api/iir_api/iir_api_create_challenges.py b/ce/iir/api/iir_client/api/iir_api/iir_api_create_challenges.py index 5e45258..b7ae0e4 100644 --- a/ce/iir/api/iir_client/api/iir_api/iir_api_create_challenges.py +++ b/ce/iir/api/iir_client/api/iir_api/iir_api_create_challenges.py @@ -28,7 +28,7 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "post", - "url": "/api/iir/createChallenges", + "url": "/iir-api/createChallenges", } if isinstance(body, CreateChallengesRequest): diff --git a/ce/iir/api/iir_client/api/iir_api/iir_api_federation_fetch.py b/ce/iir/api/iir_client/api/iir_api/iir_api_federation_fetch.py index 3c0feb9..817ed73 100644 --- a/ce/iir/api/iir_client/api/iir_api/iir_api_federation_fetch.py +++ b/ce/iir/api/iir_client/api/iir_api/iir_api_federation_fetch.py @@ -15,7 +15,6 @@ def _get_kwargs( *, - ctid: str, sub: str, ) -> dict[str, Any]: @@ -25,8 +24,6 @@ def _get_kwargs( params: dict[str, Any] = {} - params["ctid"] = ctid - params["sub"] = sub @@ -35,7 +32,7 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "get", - "url": "/api/iir/federationFetch", + "url": "/iir-api/federationFetch", "params": params, } @@ -70,13 +67,11 @@ def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Res def sync_detailed( *, client: AuthenticatedClient | Client, - ctid: str, sub: str, ) -> Response[IIRApiFederationFetchResponse200]: """ Args: - ctid (str): sub (str): Raises: @@ -89,8 +84,7 @@ def sync_detailed( kwargs = _get_kwargs( - ctid=ctid, -sub=sub, + sub=sub, ) @@ -103,13 +97,11 @@ def sync_detailed( def sync( *, client: AuthenticatedClient | Client, - ctid: str, sub: str, ) -> IIRApiFederationFetchResponse200 | None: """ Args: - ctid (str): sub (str): Raises: @@ -123,7 +115,6 @@ def sync( return sync_detailed( client=client, -ctid=ctid, sub=sub, ).parsed @@ -131,13 +122,11 @@ def sync( async def asyncio_detailed( *, client: AuthenticatedClient | Client, - ctid: str, sub: str, ) -> Response[IIRApiFederationFetchResponse200]: """ Args: - ctid (str): sub (str): Raises: @@ -150,8 +139,7 @@ async def asyncio_detailed( kwargs = _get_kwargs( - ctid=ctid, -sub=sub, + sub=sub, ) @@ -164,13 +152,11 @@ async def asyncio_detailed( async def asyncio( *, client: AuthenticatedClient | Client, - ctid: str, sub: str, ) -> IIRApiFederationFetchResponse200 | None: """ Args: - ctid (str): sub (str): Raises: @@ -184,7 +170,6 @@ async def asyncio( return (await asyncio_detailed( client=client, -ctid=ctid, sub=sub, )).parsed diff --git a/ce/iir/api/iir_client/api/iir_api/iir_api_get_issuer_by_did.py b/ce/iir/api/iir_client/api/iir_api/iir_api_get_issuer_by_did.py index 9c16209..fa8a96f 100644 --- a/ce/iir/api/iir_client/api/iir_api/iir_api_get_issuer_by_did.py +++ b/ce/iir/api/iir_client/api/iir_api/iir_api_get_issuer_by_did.py @@ -32,7 +32,7 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "get", - "url": "/api/iir/issuerByDid", + "url": "/iir-api/issuerByDid", "params": params, } diff --git a/ce/iir/api/iir_client/api/iir_api/iir_api_get_issuers.py b/ce/iir/api/iir_client/api/iir_api/iir_api_get_issuers.py index 06eae6f..b24f024 100644 --- a/ce/iir/api/iir_client/api/iir_api/iir_api_get_issuers.py +++ b/ce/iir/api/iir_client/api/iir_api/iir_api_get_issuers.py @@ -9,22 +9,41 @@ from ... import errors from ...models.iir_api_get_issuers_response_200 import IIRApiGetIssuersResponse200 +from ...types import UNSET, Unset from typing import cast def _get_kwargs( - + *, + page: int | Unset = UNSET, + page_size: int | Unset = UNSET, + keyword: str | Unset = UNSET, + task: str | Unset = UNSET, + ) -> dict[str, Any]: - + params: dict[str, Any] = {} + + params["page"] = page + + params["pageSize"] = page_size + + params["keyword"] = keyword + + params["task"] = task + + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + _kwargs: dict[str, Any] = { "method": "get", - "url": "/api/iir/issuers", + "url": "/iir-api/issuers", + "params": params, } @@ -58,9 +77,19 @@ def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Res def sync_detailed( *, client: AuthenticatedClient | Client, + page: int | Unset = UNSET, + page_size: int | Unset = UNSET, + keyword: str | Unset = UNSET, + task: str | Unset = UNSET, ) -> Response[IIRApiGetIssuersResponse200]: """ + Args: + page (int | Unset): + page_size (int | Unset): + keyword (str | Unset): + task (str | Unset): + Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. @@ -71,7 +100,11 @@ def sync_detailed( kwargs = _get_kwargs( - + page=page, +page_size=page_size, +keyword=keyword, +task=task, + ) response = client.get_httpx_client().request( @@ -83,9 +116,19 @@ def sync_detailed( def sync( *, client: AuthenticatedClient | Client, + page: int | Unset = UNSET, + page_size: int | Unset = UNSET, + keyword: str | Unset = UNSET, + task: str | Unset = UNSET, ) -> IIRApiGetIssuersResponse200 | None: """ + Args: + page (int | Unset): + page_size (int | Unset): + keyword (str | Unset): + task (str | Unset): + Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. @@ -97,15 +140,29 @@ def sync( return sync_detailed( client=client, +page=page, +page_size=page_size, +keyword=keyword, +task=task, ).parsed async def asyncio_detailed( *, client: AuthenticatedClient | Client, + page: int | Unset = UNSET, + page_size: int | Unset = UNSET, + keyword: str | Unset = UNSET, + task: str | Unset = UNSET, ) -> Response[IIRApiGetIssuersResponse200]: """ + Args: + page (int | Unset): + page_size (int | Unset): + keyword (str | Unset): + task (str | Unset): + Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. @@ -116,7 +173,11 @@ async def asyncio_detailed( kwargs = _get_kwargs( - + page=page, +page_size=page_size, +keyword=keyword, +task=task, + ) response = await client.get_async_httpx_client().request( @@ -128,9 +189,19 @@ async def asyncio_detailed( async def asyncio( *, client: AuthenticatedClient | Client, + page: int | Unset = UNSET, + page_size: int | Unset = UNSET, + keyword: str | Unset = UNSET, + task: str | Unset = UNSET, ) -> IIRApiGetIssuersResponse200 | None: """ + Args: + page (int | Unset): + page_size (int | Unset): + keyword (str | Unset): + task (str | Unset): + Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. httpx.TimeoutException: If the request takes longer than Client.timeout. @@ -142,5 +213,9 @@ async def asyncio( return (await asyncio_detailed( client=client, +page=page, +page_size=page_size, +keyword=keyword, +task=task, )).parsed diff --git a/ce/iir/api/iir_client/api/iir_api/iir_api_get_issuers_by_user.py b/ce/iir/api/iir_client/api/iir_api/iir_api_get_issuers_by_user.py index c64ceeb..8a880e3 100644 --- a/ce/iir/api/iir_client/api/iir_api/iir_api_get_issuers_by_user.py +++ b/ce/iir/api/iir_client/api/iir_api/iir_api_get_issuers_by_user.py @@ -9,6 +9,7 @@ from ... import errors from ...models.iir_api_get_issuers_by_user_response_200 import IIRApiGetIssuersByUserResponse200 +from ...types import UNSET, Unset from typing import cast from uuid import UUID @@ -16,17 +17,35 @@ def _get_kwargs( user_id: UUID, + *, + page: int | Unset = UNSET, + page_size: int | Unset = UNSET, + keyword: str | Unset = UNSET, + task: str | Unset = UNSET, ) -> dict[str, Any]: - + params: dict[str, Any] = {} + + params["page"] = page + + params["pageSize"] = page_size + + params["keyword"] = keyword + + params["task"] = task + + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + _kwargs: dict[str, Any] = { "method": "get", - "url": "/api/iir/users/{user_id}/issuers".format(user_id=quote(str(user_id), safe=""),), + "url": "/iir-api/users/{user_id}/issuers".format(user_id=quote(str(user_id), safe=""),), + "params": params, } @@ -61,11 +80,19 @@ def sync_detailed( user_id: UUID, *, client: AuthenticatedClient | Client, + page: int | Unset = UNSET, + page_size: int | Unset = UNSET, + keyword: str | Unset = UNSET, + task: str | Unset = UNSET, ) -> Response[IIRApiGetIssuersByUserResponse200]: """ Args: user_id (UUID): + page (int | Unset): + page_size (int | Unset): + keyword (str | Unset): + task (str | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -78,6 +105,10 @@ def sync_detailed( kwargs = _get_kwargs( user_id=user_id, +page=page, +page_size=page_size, +keyword=keyword, +task=task, ) @@ -91,11 +122,19 @@ def sync( user_id: UUID, *, client: AuthenticatedClient | Client, + page: int | Unset = UNSET, + page_size: int | Unset = UNSET, + keyword: str | Unset = UNSET, + task: str | Unset = UNSET, ) -> IIRApiGetIssuersByUserResponse200 | None: """ Args: user_id (UUID): + page (int | Unset): + page_size (int | Unset): + keyword (str | Unset): + task (str | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -109,6 +148,10 @@ def sync( return sync_detailed( user_id=user_id, client=client, +page=page, +page_size=page_size, +keyword=keyword, +task=task, ).parsed @@ -116,11 +159,19 @@ async def asyncio_detailed( user_id: UUID, *, client: AuthenticatedClient | Client, + page: int | Unset = UNSET, + page_size: int | Unset = UNSET, + keyword: str | Unset = UNSET, + task: str | Unset = UNSET, ) -> Response[IIRApiGetIssuersByUserResponse200]: """ Args: user_id (UUID): + page (int | Unset): + page_size (int | Unset): + keyword (str | Unset): + task (str | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -133,6 +184,10 @@ async def asyncio_detailed( kwargs = _get_kwargs( user_id=user_id, +page=page, +page_size=page_size, +keyword=keyword, +task=task, ) @@ -146,11 +201,19 @@ async def asyncio( user_id: UUID, *, client: AuthenticatedClient | Client, + page: int | Unset = UNSET, + page_size: int | Unset = UNSET, + keyword: str | Unset = UNSET, + task: str | Unset = UNSET, ) -> IIRApiGetIssuersByUserResponse200 | None: """ Args: user_id (UUID): + page (int | Unset): + page_size (int | Unset): + keyword (str | Unset): + task (str | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -164,5 +227,9 @@ async def asyncio( return (await asyncio_detailed( user_id=user_id, client=client, +page=page, +page_size=page_size, +keyword=keyword, +task=task, )).parsed diff --git a/ce/iir/api/iir_client/api/iir_api/iir_api_get_organization_iir_detail.py b/ce/iir/api/iir_client/api/iir_api/iir_api_get_organization_iir_detail.py new file mode 100644 index 0000000..62ec496 --- /dev/null +++ b/ce/iir/api/iir_client/api/iir_api/iir_api_get_organization_iir_detail.py @@ -0,0 +1,175 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ...client import AuthenticatedClient, Client +from ...types import Response, UNSET +from ... import errors + +from ...models.iir_api_get_organization_iir_detail_response_200 import IIRApiGetOrganizationIIRDetailResponse200 +from typing import cast + + + +def _get_kwargs( + *, + ctid: str, + +) -> dict[str, Any]: + + + + + params: dict[str, Any] = {} + + params["ctid"] = ctid + + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/iir-api/getOrganizationIIRDetail", + "params": params, + } + + + return _kwargs + + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> IIRApiGetOrganizationIIRDetailResponse200 | None: + if response.status_code == 200: + response_200 = IIRApiGetOrganizationIIRDetailResponse200.from_dict(response.json()) + + + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[IIRApiGetOrganizationIIRDetailResponse200]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + ctid: str, + +) -> Response[IIRApiGetOrganizationIIRDetailResponse200]: + """ + Args: + ctid (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[IIRApiGetOrganizationIIRDetailResponse200] + """ + + + kwargs = _get_kwargs( + ctid=ctid, + + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + +def sync( + *, + client: AuthenticatedClient | Client, + ctid: str, + +) -> IIRApiGetOrganizationIIRDetailResponse200 | None: + """ + Args: + ctid (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + IIRApiGetOrganizationIIRDetailResponse200 + """ + + + return sync_detailed( + client=client, +ctid=ctid, + + ).parsed + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + ctid: str, + +) -> Response[IIRApiGetOrganizationIIRDetailResponse200]: + """ + Args: + ctid (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[IIRApiGetOrganizationIIRDetailResponse200] + """ + + + kwargs = _get_kwargs( + ctid=ctid, + + ) + + response = await client.get_async_httpx_client().request( + **kwargs + ) + + return _build_response(client=client, response=response) + +async def asyncio( + *, + client: AuthenticatedClient | Client, + ctid: str, + +) -> IIRApiGetOrganizationIIRDetailResponse200 | None: + """ + Args: + ctid (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + IIRApiGetOrganizationIIRDetailResponse200 + """ + + + return (await asyncio_detailed( + client=client, +ctid=ctid, + + )).parsed diff --git a/ce/iir/api/iir_client/api/iir_api/iir_api_get_registry_resource.py b/ce/iir/api/iir_client/api/iir_api/iir_api_get_registry_resource.py index 527df04..29615e3 100644 --- a/ce/iir/api/iir_client/api/iir_api/iir_api_get_registry_resource.py +++ b/ce/iir/api/iir_client/api/iir_api/iir_api_get_registry_resource.py @@ -32,7 +32,7 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "get", - "url": "/api/iir/getRegistryResource", + "url": "/iir-api/getRegistryResource", "params": params, } diff --git a/ce/iir/api/iir_client/api/iir_api/iir_api_is_user_a_member.py b/ce/iir/api/iir_client/api/iir_api/iir_api_is_user_a_member.py index 354fe29..9bfe7d1 100644 --- a/ce/iir/api/iir_client/api/iir_api/iir_api_is_user_a_member.py +++ b/ce/iir/api/iir_client/api/iir_api/iir_api_is_user_a_member.py @@ -17,6 +17,7 @@ def _get_kwargs( user_id: UUID, *, + task: str, ctid: str, ) -> dict[str, Any]: @@ -26,6 +27,8 @@ def _get_kwargs( params: dict[str, Any] = {} + params["task"] = task + params["ctid"] = ctid @@ -34,7 +37,7 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "get", - "url": "/api/iir/isUserAMember/{user_id}".format(user_id=quote(str(user_id), safe=""),), + "url": "/iir-api/isUserAMember/{user_id}".format(user_id=quote(str(user_id), safe=""),), "params": params, } @@ -70,12 +73,14 @@ def sync_detailed( user_id: UUID, *, client: AuthenticatedClient | Client, + task: str, ctid: str, ) -> Response[IIRApiIsUserAMemberResponse200]: """ Args: user_id (UUID): + task (str): ctid (str): Raises: @@ -89,6 +94,7 @@ def sync_detailed( kwargs = _get_kwargs( user_id=user_id, +task=task, ctid=ctid, ) @@ -103,12 +109,14 @@ def sync( user_id: UUID, *, client: AuthenticatedClient | Client, + task: str, ctid: str, ) -> IIRApiIsUserAMemberResponse200 | None: """ Args: user_id (UUID): + task (str): ctid (str): Raises: @@ -123,6 +131,7 @@ def sync( return sync_detailed( user_id=user_id, client=client, +task=task, ctid=ctid, ).parsed @@ -131,12 +140,14 @@ async def asyncio_detailed( user_id: UUID, *, client: AuthenticatedClient | Client, + task: str, ctid: str, ) -> Response[IIRApiIsUserAMemberResponse200]: """ Args: user_id (UUID): + task (str): ctid (str): Raises: @@ -150,6 +161,7 @@ async def asyncio_detailed( kwargs = _get_kwargs( user_id=user_id, +task=task, ctid=ctid, ) @@ -164,12 +176,14 @@ async def asyncio( user_id: UUID, *, client: AuthenticatedClient | Client, + task: str, ctid: str, ) -> IIRApiIsUserAMemberResponse200 | None: """ Args: user_id (UUID): + task (str): ctid (str): Raises: @@ -184,6 +198,7 @@ async def asyncio( return (await asyncio_detailed( user_id=user_id, client=client, +task=task, ctid=ctid, )).parsed diff --git a/ce/iir/api/iir_client/api/iir_api/iir_api_is_user_a_member_of_org.py b/ce/iir/api/iir_client/api/iir_api/iir_api_is_user_a_member_of_org.py index 61e8cd3..c3679b2 100644 --- a/ce/iir/api/iir_client/api/iir_api/iir_api_is_user_a_member_of_org.py +++ b/ce/iir/api/iir_client/api/iir_api/iir_api_is_user_a_member_of_org.py @@ -16,6 +16,7 @@ def _get_kwargs( *, ctid: str, + task: str, ) -> dict[str, Any]: @@ -26,13 +27,15 @@ def _get_kwargs( params["ctid"] = ctid + params["task"] = task + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} _kwargs: dict[str, Any] = { "method": "get", - "url": "/api/iir/IsUserAMemberOfOrg", + "url": "/iir-api/IsUserAMemberOfOrg", "params": params, } @@ -68,11 +71,13 @@ def sync_detailed( *, client: AuthenticatedClient | Client, ctid: str, + task: str, ) -> Response[IIRApiIsUserAMemberOfOrgResponse200]: """ Args: ctid (str): + task (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -85,6 +90,7 @@ def sync_detailed( kwargs = _get_kwargs( ctid=ctid, +task=task, ) @@ -98,11 +104,13 @@ def sync( *, client: AuthenticatedClient | Client, ctid: str, + task: str, ) -> IIRApiIsUserAMemberOfOrgResponse200 | None: """ Args: ctid (str): + task (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -116,6 +124,7 @@ def sync( return sync_detailed( client=client, ctid=ctid, +task=task, ).parsed @@ -123,11 +132,13 @@ async def asyncio_detailed( *, client: AuthenticatedClient | Client, ctid: str, + task: str, ) -> Response[IIRApiIsUserAMemberOfOrgResponse200]: """ Args: ctid (str): + task (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -140,6 +151,7 @@ async def asyncio_detailed( kwargs = _get_kwargs( ctid=ctid, +task=task, ) @@ -153,11 +165,13 @@ async def asyncio( *, client: AuthenticatedClient | Client, ctid: str, + task: str, ) -> IIRApiIsUserAMemberOfOrgResponse200 | None: """ Args: ctid (str): + task (str): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -171,5 +185,6 @@ async def asyncio( return (await asyncio_detailed( client=client, ctid=ctid, +task=task, )).parsed diff --git a/ce/iir/api/iir_client/api/iir_api/iir_api_save_challenge_token.py b/ce/iir/api/iir_client/api/iir_api/iir_api_save_challenge_token.py index c14bc6f..7542beb 100644 --- a/ce/iir/api/iir_client/api/iir_api/iir_api_save_challenge_token.py +++ b/ce/iir/api/iir_client/api/iir_api/iir_api_save_challenge_token.py @@ -28,7 +28,7 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "post", - "url": "/api/iir/saveChallengeToken", + "url": "/iir-api/saveChallengeToken", } if isinstance(body, SaveChallengeTokenRequest): diff --git a/ce/iir/api/iir_client/api/iir_api/iir_api_submit_to_iir.py b/ce/iir/api/iir_client/api/iir_api/iir_api_submit_to_iir.py index bd5e9cf..a374faf 100644 --- a/ce/iir/api/iir_client/api/iir_api/iir_api_submit_to_iir.py +++ b/ce/iir/api/iir_client/api/iir_api/iir_api_submit_to_iir.py @@ -1,5 +1,6 @@ from http import HTTPStatus -from typing import Any +from typing import Any, cast +from urllib.parse import quote import httpx @@ -8,33 +9,49 @@ from ... import errors from ...models.iir_api_submit_to_iir_response_200 import IIRApiSubmitToIIRResponse200 -from ...models.issuer_dto import IssuerDTO +from ...models.pub_issuer_dto import PubIssuerDTO +from typing import cast + def _get_kwargs( *, - body: IssuerDTO | Unset = UNSET, + body: PubIssuerDTO | PubIssuerDTO | Unset = UNSET, + ) -> dict[str, Any]: headers: dict[str, Any] = {} + + + + + _kwargs: dict[str, Any] = { "method": "post", - "url": "/api/iir/submitToIIR", + "url": "/iir-api/submitToIIR", } - if body is not UNSET: + if isinstance(body, PubIssuerDTO): _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + if isinstance(body, PubIssuerDTO): + _kwargs["data"] = body.to_dict() + + headers["Content-Type"] = "application/x-www-form-urlencoded" _kwargs["headers"] = headers return _kwargs -def _parse_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> IIRApiSubmitToIIRResponse200 | None: + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> IIRApiSubmitToIIRResponse200 | None: if response.status_code == 200: response_200 = IIRApiSubmitToIIRResponse200.from_dict(response.json()) + + + return response_200 if client.raise_on_unexpected_status: @@ -43,9 +60,7 @@ def _parse_response( return None -def _build_response( - *, client: AuthenticatedClient | Client, response: httpx.Response -) -> Response[IIRApiSubmitToIIRResponse200]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[IIRApiSubmitToIIRResponse200]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -57,46 +72,113 @@ def _build_response( def sync_detailed( *, client: AuthenticatedClient | Client, - body: IssuerDTO | Unset = UNSET, + body: PubIssuerDTO | PubIssuerDTO | Unset = UNSET, + ) -> Response[IIRApiSubmitToIIRResponse200]: - kwargs = _get_kwargs(body=body) + """ + Args: + body (PubIssuerDTO): + body (PubIssuerDTO): - response = client.get_httpx_client().request(**kwargs) + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. - return _build_response(client=client, response=response) + Returns: + Response[IIRApiSubmitToIIRResponse200] + """ + + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) def sync( *, client: AuthenticatedClient | Client, - body: IssuerDTO | Unset = UNSET, + body: PubIssuerDTO | PubIssuerDTO | Unset = UNSET, + ) -> IIRApiSubmitToIIRResponse200 | None: + """ + Args: + body (PubIssuerDTO): + body (PubIssuerDTO): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + IIRApiSubmitToIIRResponse200 + """ + + return sync_detailed( client=client, - body=body, - ).parsed +body=body, + ).parsed async def asyncio_detailed( *, client: AuthenticatedClient | Client, - body: IssuerDTO | Unset = UNSET, + body: PubIssuerDTO | PubIssuerDTO | Unset = UNSET, + ) -> Response[IIRApiSubmitToIIRResponse200]: - kwargs = _get_kwargs(body=body) + """ + Args: + body (PubIssuerDTO): + body (PubIssuerDTO): - response = await client.get_async_httpx_client().request(**kwargs) + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. - return _build_response(client=client, response=response) + Returns: + Response[IIRApiSubmitToIIRResponse200] + """ + + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request( + **kwargs + ) + + return _build_response(client=client, response=response) async def asyncio( *, client: AuthenticatedClient | Client, - body: IssuerDTO | Unset = UNSET, + body: PubIssuerDTO | PubIssuerDTO | Unset = UNSET, + ) -> IIRApiSubmitToIIRResponse200 | None: - return ( - await asyncio_detailed( - client=client, - body=body, - ) - ).parsed \ No newline at end of file + """ + Args: + body (PubIssuerDTO): + body (PubIssuerDTO): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + IIRApiSubmitToIIRResponse200 + """ + + + return (await asyncio_detailed( + client=client, +body=body, + + )).parsed diff --git a/ce/iir/api/iir_client/api/iir_api/iir_api_update_issuer.py b/ce/iir/api/iir_client/api/iir_api/iir_api_update_issuer.py index b3eca8d..0bbd8b9 100644 --- a/ce/iir/api/iir_client/api/iir_api/iir_api_update_issuer.py +++ b/ce/iir/api/iir_client/api/iir_api/iir_api_update_issuer.py @@ -5,18 +5,18 @@ import httpx from ...client import AuthenticatedClient, Client -from ...types import Response, UNSET, Unset +from ...types import Response, UNSET from ... import errors from ...models.iir_api_update_issuer_response_200 import IIRApiUpdateIssuerResponse200 -from ...models.issuer_dto import IssuerDTO +from ...models.pub_issuer_dto import PubIssuerDTO from typing import cast def _get_kwargs( *, - body: IssuerDTO | IssuerDTO | Unset = UNSET, + body: PubIssuerDTO | PubIssuerDTO | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} @@ -28,15 +28,15 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "put", - "url": "/api/iir/updateIssuer", + "url": "/iir-api/updateIssuer", } - if isinstance(body, IssuerDTO): + if isinstance(body, PubIssuerDTO): _kwargs["json"] = body.to_dict() headers["Content-Type"] = "application/json" - if isinstance(body, IssuerDTO): + if isinstance(body, PubIssuerDTO): _kwargs["data"] = body.to_dict() headers["Content-Type"] = "application/x-www-form-urlencoded" @@ -72,13 +72,13 @@ def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Res def sync_detailed( *, client: AuthenticatedClient | Client, - body: IssuerDTO | IssuerDTO | Unset = UNSET, + body: PubIssuerDTO | PubIssuerDTO | Unset = UNSET, ) -> Response[IIRApiUpdateIssuerResponse200]: """ Args: - body (IssuerDTO): - body (IssuerDTO): + body (PubIssuerDTO): + body (PubIssuerDTO): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -103,13 +103,13 @@ def sync_detailed( def sync( *, client: AuthenticatedClient | Client, - body: IssuerDTO | IssuerDTO | Unset = UNSET, + body: PubIssuerDTO | PubIssuerDTO | Unset = UNSET, ) -> IIRApiUpdateIssuerResponse200 | None: """ Args: - body (IssuerDTO): - body (IssuerDTO): + body (PubIssuerDTO): + body (PubIssuerDTO): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -129,13 +129,13 @@ def sync( async def asyncio_detailed( *, client: AuthenticatedClient | Client, - body: IssuerDTO | IssuerDTO | Unset = UNSET, + body: PubIssuerDTO | PubIssuerDTO | Unset = UNSET, ) -> Response[IIRApiUpdateIssuerResponse200]: """ Args: - body (IssuerDTO): - body (IssuerDTO): + body (PubIssuerDTO): + body (PubIssuerDTO): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -160,13 +160,13 @@ async def asyncio_detailed( async def asyncio( *, client: AuthenticatedClient | Client, - body: IssuerDTO | IssuerDTO | Unset = UNSET, + body: PubIssuerDTO | PubIssuerDTO | Unset = UNSET, ) -> IIRApiUpdateIssuerResponse200 | None: """ Args: - body (IssuerDTO): - body (IssuerDTO): + body (PubIssuerDTO): + body (PubIssuerDTO): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. diff --git a/ce/iir/api/iir_client/api/iir_api/iir_api_validate_jwt_signature.py b/ce/iir/api/iir_client/api/iir_api/iir_api_validate_jwt_signature.py index f611c17..7ab600f 100644 --- a/ce/iir/api/iir_client/api/iir_api/iir_api_validate_jwt_signature.py +++ b/ce/iir/api/iir_client/api/iir_api/iir_api_validate_jwt_signature.py @@ -28,7 +28,7 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "post", - "url": "/api/iir/validateJwt", + "url": "/iir-api/validateJwt", } if isinstance(body, ValidateJwtRequest): diff --git a/ce/iir/api/iir_client/api/iir_api/iir_api_validate_key.py b/ce/iir/api/iir_client/api/iir_api/iir_api_validate_key.py index e6b219d..b4a1e5b 100644 --- a/ce/iir/api/iir_client/api/iir_api/iir_api_validate_key.py +++ b/ce/iir/api/iir_client/api/iir_api/iir_api_validate_key.py @@ -35,7 +35,7 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "get", - "url": "/api/iir/validateDidKey", + "url": "/iir-api/validateDidKey", "params": params, } diff --git a/ce/iir/api/iir_client/api/iir_api/iir_api_validate_web.py b/ce/iir/api/iir_client/api/iir_api/iir_api_validate_web.py index b557e16..0573ff1 100644 --- a/ce/iir/api/iir_client/api/iir_api/iir_api_validate_web.py +++ b/ce/iir/api/iir_client/api/iir_api/iir_api_validate_web.py @@ -32,7 +32,7 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "get", - "url": "/api/iir/validateDidWeb", + "url": "/iir-api/validateDidWeb", "params": params, } diff --git a/ce/iir/api/iir_client/models/__init__.py b/ce/iir/api/iir_client/models/__init__.py index dc18d1f..4c9f955 100644 --- a/ce/iir/api/iir_client/models/__init__.py +++ b/ce/iir/api/iir_client/models/__init__.py @@ -7,6 +7,7 @@ from .iir_api_get_issuer_by_did_response_200 import IIRApiGetIssuerByDidResponse200 from .iir_api_get_issuers_by_user_response_200 import IIRApiGetIssuersByUserResponse200 from .iir_api_get_issuers_response_200 import IIRApiGetIssuersResponse200 +from .iir_api_get_organization_iir_detail_response_200 import IIRApiGetOrganizationIIRDetailResponse200 from .iir_api_get_registry_resource_response_200 import IIRApiGetRegistryResourceResponse200 from .iir_api_is_user_a_member_of_org_response_200 import IIRApiIsUserAMemberOfOrgResponse200 from .iir_api_is_user_a_member_response_200 import IIRApiIsUserAMemberResponse200 @@ -16,7 +17,7 @@ from .iir_api_validate_jwt_signature_response_200 import IIRApiValidateJwtSignatureResponse200 from .iir_api_validate_key_response_200 import IIRApiValidateKeyResponse200 from .iir_api_validate_web_response_200 import IIRApiValidateWebResponse200 -from .issuer_dto import IssuerDTO +from .pub_issuer_dto import PubIssuerDTO from .safe_wait_handle import SafeWaitHandle from .save_challenge_token_request import SaveChallengeTokenRequest from .validate_jwt_request import ValidateJwtRequest @@ -31,6 +32,7 @@ "IIRApiGetIssuerByDidResponse200", "IIRApiGetIssuersByUserResponse200", "IIRApiGetIssuersResponse200", + "IIRApiGetOrganizationIIRDetailResponse200", "IIRApiGetRegistryResourceResponse200", "IIRApiIsUserAMemberOfOrgResponse200", "IIRApiIsUserAMemberResponse200", @@ -40,7 +42,7 @@ "IIRApiValidateJwtSignatureResponse200", "IIRApiValidateKeyResponse200", "IIRApiValidateWebResponse200", - "IssuerDTO", + "PubIssuerDTO", "SafeWaitHandle", "SaveChallengeTokenRequest", "ValidateJwtRequest", diff --git a/ce/iir/api/iir_client/models/iir_api_get_organization_iir_detail_response_200.py b/ce/iir/api/iir_client/models/iir_api_get_organization_iir_detail_response_200.py new file mode 100644 index 0000000..b3b988a --- /dev/null +++ b/ce/iir/api/iir_client/models/iir_api_get_organization_iir_detail_response_200.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + + + + + + + +T = TypeVar("T", bound="IIRApiGetOrganizationIIRDetailResponse200") + + + +@_attrs_define +class IIRApiGetOrganizationIIRDetailResponse200: + """ + """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + iir_api_get_organization_iir_detail_response_200 = cls( + ) + + + iir_api_get_organization_iir_detail_response_200.additional_properties = d + return iir_api_get_organization_iir_detail_response_200 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/ce/iir/api/iir_client/models/issuer_dto.py b/ce/iir/api/iir_client/models/pub_issuer_dto.py similarity index 90% rename from ce/iir/api/iir_client/models/issuer_dto.py rename to ce/iir/api/iir_client/models/pub_issuer_dto.py index 78b22fb..0be5f14 100644 --- a/ce/iir/api/iir_client/models/issuer_dto.py +++ b/ce/iir/api/iir_client/models/pub_issuer_dto.py @@ -18,12 +18,12 @@ -T = TypeVar("T", bound="IssuerDTO") +T = TypeVar("T", bound="PubIssuerDTO") @_attrs_define -class IssuerDTO: +class PubIssuerDTO: """ Attributes: ctid (str | Unset): @@ -72,12 +72,12 @@ def to_dict(self) -> dict[str, Any]: logo_uri = self.logo_uri valid_from: str | Unset = UNSET - if self.valid_from is not UNSET: - valid_from = self.valid_from.isoformat() if hasattr(self.valid_from, "isoformat") else self.valid_from + if not isinstance(self.valid_from, Unset): + valid_from = self.valid_from.isoformat() valid_until: str | Unset = UNSET - if self.valid_until is not UNSET: - valid_until = self.valid_until.isoformat() if hasattr(self.valid_until, "isoformat") else self.valid_until + if not isinstance(self.valid_until, Unset): + valid_until = self.valid_until.isoformat() field_dict: dict[str, Any] = {} @@ -148,7 +148,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: - issuer_dto = cls( + pub_issuer_dto = cls( ctid=ctid, did=did, name=name, @@ -162,8 +162,8 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: ) - issuer_dto.additional_properties = d - return issuer_dto + pub_issuer_dto.additional_properties = d + return pub_issuer_dto @property def additional_keys(self) -> list[str]: diff --git a/ce/iir/csv_processor.py b/ce/iir/csv_processor.py index 88f4495..a2c2f05 100644 --- a/ce/iir/csv_processor.py +++ b/ce/iir/csv_processor.py @@ -21,14 +21,16 @@ from rich.table import Table, box from ce.iir.did_ops import ( + call_get_organization_iir_detail, call_is_user_member, - call_create_challenge, + call_create_challenges, call_get_registry_resource, call_save_challenge_token, call_validate_did_key, call_validate_did_web, call_verify_jwt_signature, classify_did, + merge_autofill, sign_proof_jwt, validate_ctid, validate_date, @@ -165,7 +167,7 @@ def fail(reason: str) -> None: if not ok: fail(err) else: - api_vm_ids = data.get("verificationMethodIds") or [] + api_vm_ids = data.get("VerificationMethodIds") or data.get("verificationMethodIds") or [] if not error: ok, err = validate_verification_method(vm, did, api_vm_ids) @@ -175,19 +177,25 @@ def fail(reason: str) -> None: challenge_uuid = "" challenge_payload = {} if not error: - ok, challenge, err = call_create_challenge( - ctid, vm, access_token, env.publisher_base, env.ssl_verify + ok, challenges_list, err = call_create_challenges( + ctid, [vm], access_token, env.publisher_base, env.ssl_verify ) if not ok: fail(err) + elif not challenges_list: + fail("createChallenges returned no challenges") else: - challenge_uuid = challenge.get("challenge") or challenge.get("Challenge", "") + challenge = next( + (c for c in challenges_list if (c.get("Did") or c.get("did")) == vm), + challenges_list[0], + ) + challenge_uuid = challenge.get("Challenge") or challenge.get("challenge", "") challenge_payload = { - "did": challenge.get("Did") or challenge.get("did", did), + "did": challenge.get("Did") or challenge.get("did", did), "challenge": challenge_uuid, - "aud": challenge.get("Aud") or challenge.get("aud", ""), - "iat": challenge.get("Iat") or challenge.get("iat", 0), - "exp": challenge.get("Exp") or challenge.get("exp", 0), + "aud": challenge.get("Aud") or challenge.get("aud", ""), + "iat": challenge.get("Iat") or challenge.get("iat", 0), + "exp": challenge.get("Exp") or challenge.get("exp", 0), "ctid": ctid, } @@ -329,13 +337,22 @@ def pub_fail(reason: str) -> None: if not ok: pub_fail(err) else: + ok_acc, accounts_data, acc_err = call_get_organization_iir_detail( + ctid, access_token, env.publisher_base, env.ssl_verify + ) + if not ok_acc: + accounts_data = {} + + merged = merge_autofill(registry_data, accounts_data) ok, err = call_submit_to_iir( ctid, did, - registry_data.get("Name", ""), - registry_data.get("LegalName", ""), - registry_data.get("CredentialRegistryUri", ""), - registry_data.get("SubjectWebpage", ""), + merged["name"], + merged["legal_name"], + merged["registry_uri"], + merged["subject_webpage"], access_token, env.publisher_base, env.ssl_verify, + logo_uri=merged["image"], + logo_base64=merged["logo_base64"], valid_from=valid_from, valid_until=valid_until, ) diff --git a/ce/iir/did_ops.py b/ce/iir/did_ops.py index be1c8de..981f716 100644 --- a/ce/iir/did_ops.py +++ b/ce/iir/did_ops.py @@ -21,17 +21,18 @@ from ce.iir.api.iir_client.models.create_challenges_request import CreateChallengesRequest from ce.iir.api.iir_client.models.save_challenge_token_request import SaveChallengeTokenRequest from ce.iir.api.iir_client.models.validate_jwt_request import ValidateJwtRequest -from ce.iir.api.iir_client.models.issuer_dto import IssuerDTO +from ce.iir.api.iir_client.models.pub_issuer_dto import PubIssuerDTO from ce.iir.api.iir_client.api.iir_api.iir_api_is_user_a_member import sync_detailed as is_user_member_sync from ce.iir.api.iir_client.api.iir_api.iir_api_get_registry_resource import sync_detailed as get_registry_resource_sync +from ce.iir.api.iir_client.api.iir_api.iir_api_get_organization_iir_detail import sync_detailed as get_organization_iir_detail_sync from ce.iir.api.iir_client.api.iir_api.iir_api_validate_key import sync_detailed as validate_did_key_sync from ce.iir.api.iir_client.api.iir_api.iir_api_validate_web import sync_detailed as validate_did_web_sync from ce.iir.api.iir_client.api.iir_api.iir_api_create_challenges import sync_detailed as create_challenges_sync from ce.iir.api.iir_client.api.iir_api.iir_api_validate_jwt_signature import sync_detailed as validate_jwt_sync from ce.iir.api.iir_client.api.iir_api.iir_api_save_challenge_token import sync_detailed as save_challenge_token_sync from ce.iir.api.iir_client.api.iir_api.iir_api_submit_to_iir import sync_detailed as submit_to_iir_sync - +from ce.iir.api.iir_client.types import UNSET CE_GUID_RE = re.compile( r"^ce-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", @@ -47,6 +48,8 @@ "z6LS": "X25519", } +IIR_TASK = "IIR" + def _client(token: str, base_url: str, ssl_verify: bool) -> AuthenticatedClient: return AuthenticatedClient( base_url=base_url.rstrip("/"), @@ -55,6 +58,23 @@ def _client(token: str, base_url: str, ssl_verify: bool) -> AuthenticatedClient: timeout=60.0, ) + +def _parsed_to_dict(parsed: Any) -> dict: + + if not parsed: + return {} + if isinstance(parsed, dict): + return parsed + if hasattr(parsed, "to_dict"): + try: + d = parsed.to_dict() + if isinstance(d, dict): + return d + except Exception: + pass + return getattr(parsed, "additional_properties", {}) or {} + + def validate_ctid(ctid: str) -> tuple[bool, str]: if not ctid: return False, "CTID is empty" @@ -76,6 +96,31 @@ def validate_date(value: str, field_name: str) -> tuple[bool, str, str]: return False, f"{field_name} must be in MM/DD/YYYY format, got '{value}'", "" +def validate_date_range(valid_from: str, valid_until: str) -> tuple[bool, str]: + """Mirror the React page's both-or-neither + vu > vf cross-check. + + Inputs are the raw user values (any format accepted by validate_date). + """ + vf = (valid_from or "").strip() + vu = (valid_until or "").strip() + + if not vf and not vu: + return True, "" + if bool(vf) != bool(vu): + return False, "If you provide dates, both Valid From and Valid Until are required." + + ok_vf, err_vf, iso_vf = validate_date(vf, "Valid From") + if not ok_vf: + return False, err_vf + ok_vu, err_vu, iso_vu = validate_date(vu, "Valid Until") + if not ok_vu: + return False, err_vu + + if iso_vu <= iso_vf: + return False, "Valid Until must be after Valid From." + return True, "" + + def classify_did(did: str) -> tuple[str, str]: if not did: return "unknown", "DID is empty" @@ -116,20 +161,15 @@ def validate_verification_method(vm: str, did: str, api_vm_ids: list[str]) -> tu ) return True, "" -def call_is_user_member(user_id, ctid, token, publisher_base, ssl_verify) -> tuple[bool, str]: + +def call_is_user_member( + user_id, ctid, token, publisher_base, ssl_verify, task: str = IIR_TASK +) -> tuple[bool, str]: try: client = _client(token, publisher_base, ssl_verify) - response = is_user_member_sync(client=client, user_id=user_id, ctid=ctid) - - # Extract data - data = {} - parsed = response.parsed - if parsed: - if isinstance(parsed, dict): - data = parsed - else: - data = getattr(parsed, "additional_properties", {}) + response = is_user_member_sync(user_id, client=client, task=task, ctid=ctid) + data = _parsed_to_dict(response.parsed) valid = data.get("valid", False) if not valid: return False, "User is not a member of this organization" @@ -144,14 +184,7 @@ def call_get_registry_resource(ctid, token, publisher_base, ssl_verify) -> tuple client = _client(token, publisher_base, ssl_verify) response = get_registry_resource_sync(client=client, ctid=ctid) - data = {} - parsed = response.parsed - if parsed: - if isinstance(parsed, dict): - data = parsed - else: - data = getattr(parsed, "additional_properties", {}) - + data = _parsed_to_dict(response.parsed) exists = data.get("ExistsInRegistry", False) if not exists: return False, data, f"CTID '{ctid}' not found in registry" @@ -161,6 +194,56 @@ def call_get_registry_resource(ctid, token, publisher_base, ssl_verify) -> tuple except Exception as e: return False, {}, f"Registry API request failed: {e}" + +def call_get_organization_iir_detail( + ctid, token, publisher_base, ssl_verify +) -> tuple[bool, dict, str]: + try: + client = _client(token, publisher_base, ssl_verify) + response = get_organization_iir_detail_sync(client=client, ctid=ctid) + + envelope = _parsed_to_dict(response.parsed) + if not envelope: + return False, {}, "getOrganizationIIRDetail returned empty" + + if not envelope.get("valid", False): + return False, envelope, "Organization IIR detail not available" + + inner = envelope.get("data") or {} + if not isinstance(inner, dict): + inner = {} + + return True, inner, "" + + except Exception as e: + return False, {}, f"getOrganizationIIRDetail request failed: {e}" + + +def merge_autofill(registry: dict, accounts: dict) -> dict: + + name = registry.get("Name", "") or "" + registry_uri = registry.get("CredentialRegistryUri", "") or "" + subject_webpage = registry.get("SubjectWebpage", "") or "" + image = registry.get("Image", "") or "" + legal_name = registry.get("LegalName", "") or "" + logo_base64 = registry.get("LogoBase64", "") or "" + + if accounts: + if accounts.get("LegalName"): + legal_name = accounts["LegalName"] + if not image and accounts.get("LogoUrl"): + image = accounts["LogoUrl"] + + return { + "name": name, + "legal_name": legal_name, + "registry_uri": registry_uri, + "subject_webpage": subject_webpage, + "image": image, + "logo_base64": logo_base64, + } + + def call_validate_did_key( did: str, alg: str, token: str, publisher_base: str, ssl_verify: bool ) -> tuple[bool, dict, str]: @@ -178,7 +261,7 @@ def call_validate_did_key( if response.status_code >= 400: return False, {}, f"validateDidKey error {response.status_code}" - return True, (response.parsed.to_dict() if response.parsed is not None else {}), "" + return True, _parsed_to_dict(response.parsed), "" except Exception as e: return False, {}, f"validateDidKey request failed: {e}" @@ -202,25 +285,49 @@ def call_validate_did_web( if response.status_code >= 400: return False, {}, f"validateDidWeb error {response.status_code}" - data = response.parsed or {} - if hasattr(data, "to_dict"): - data = data.to_dict() - - return True, data if isinstance(data, dict) else {}, "" + return True, _parsed_to_dict(response.parsed), "" except Exception as e: return False, {}, f"validateDidWeb request failed: {e}" -def call_create_challenge( - ctid: str, vm_id: str, token: str, publisher_base: str, ssl_verify: bool -) -> tuple[bool, dict, str]: +def call_validate_did( + did: str, alg: str, token: str, publisher_base: str, ssl_verify: bool +) -> tuple[bool, list[str], dict, str]: + + kind, kind_err = classify_did(did) + if kind == "unknown": + return False, [], {}, kind_err or "Unsupported DID method" + + if kind == "did:key": + ok_pfx, err_pfx = validate_did_key_prefix(did, alg) + if not ok_pfx: + return False, [], {}, err_pfx + ok, data, err = call_validate_did_key(did, alg, token, publisher_base, ssl_verify) + else: + ok, data, err = call_validate_did_web(did, token, publisher_base, ssl_verify) + + if not ok: + return False, [], data, err + + vm_ids = data.get("VerificationMethodIds") or [] + if not isinstance(vm_ids, list): + vm_ids = [] + return True, vm_ids, data, "" + + +def call_create_challenges( + ctid: str, vm_ids: list[str], token: str, publisher_base: str, ssl_verify: bool +) -> tuple[bool, list[dict], str]: + if not vm_ids: + return False, [], "no verification methods to challenge" + try: client = _client(token, publisher_base, ssl_verify) body = CreateChallengesRequest( ctid=ctid, - verification_method_ids=[vm_id], + verification_method_ids=vm_ids, ) response = create_challenges_sync( @@ -229,22 +336,26 @@ def call_create_challenge( ) if response.status_code == 400: - return False, {}, "createChallenges failed" + return False, [], "createChallenges failed" if response.status_code >= 400: - return False, {}, f"createChallenges error {response.status_code}" + return False, [], f"createChallenges error {response.status_code}" data = response.parsed if not data: - return False, {}, "createChallenges returned empty" + return False, [], "createChallenges returned empty" if isinstance(data, list): - first = data[0] - return True, first.to_dict() if hasattr(first, "to_dict") else first, "" + items = [c.to_dict() if hasattr(c, "to_dict") else dict(c) for c in data] + return True, items, "" - return True, {}, "" + single = data.to_dict() if hasattr(data, "to_dict") else None + if isinstance(single, dict): + return True, [single], "" + + return False, [], "createChallenges returned unexpected shape" except Exception as e: - return False, {}, f"createChallenges request failed: {e}" + return False, [], f"createChallenges request failed: {e}" def call_verify_jwt_signature( @@ -320,26 +431,36 @@ def call_submit_to_iir( valid_from: str = "", valid_until: str = "", ) -> tuple[bool, str]: + from datetime import datetime + + def _to_dt_or_unset(value: str): + if not value: + return UNSET + try: + return datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError: + try: + return datetime.strptime(value, "%Y-%m-%d") + except ValueError: + return UNSET + try: client = _client(token, publisher_base, ssl_verify) - body = IssuerDTO( + body = PubIssuerDTO( ctid=ctid, did=did, name=name, legal_name=legal_name, credential_registry_uri=registry_uri, subject_web_page=subject_web_page, - logo_base_64=logo_base64 or None, - logo_uri=logo_uri or None, - valid_from=valid_from or None, - valid_until=valid_until or None, + logo_base_64=logo_base64 if logo_base64 else UNSET, + logo_uri=logo_uri if logo_uri else UNSET, + valid_from=_to_dt_or_unset(valid_from), + valid_until=_to_dt_or_unset(valid_until), ) - response = submit_to_iir_sync( - client=client, - body=body, - ) + response = submit_to_iir_sync(client=client, body=body) if response.status_code == 409: return False, "Did already exists" @@ -352,8 +473,7 @@ def call_submit_to_iir( except Exception as e: return False, f"submitToIIR request failed: {e}" - - + def _parse_uvarint(data: bytes) -> tuple[int, int]: x, s = 0, 0 for i, b in enumerate(data): diff --git a/ce/iir/openapi.json b/ce/iir/openapi.json index 6c45a80..448411d 100644 --- a/ce/iir/openapi.json +++ b/ce/iir/openapi.json @@ -5,7 +5,7 @@ "title": "CTI Directory API" }, "paths": { - "/api/iir/IsUserAMemberOfOrg": { + "/iir-api/IsUserAMemberOfOrg": { "get": { "tags": [ "IIRApi" @@ -19,6 +19,14 @@ "schema": { "type": "string" } + }, + { + "name": "task", + "in": "query", + "required": true, + "schema": { + "type": "string" + } } ], "responses": { @@ -50,7 +58,7 @@ } } }, - "/api/iir/isUserAMember/{userId}": { + "/iir-api/isUserAMember/{userId}": { "get": { "tags": [ "IIRApi" @@ -66,6 +74,14 @@ "format": "uuid" } }, + { + "name": "task", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, { "name": "ctid", "in": "query", @@ -104,7 +120,52 @@ } } }, - "/api/iir/getRegistryResource": { + "/iir-api/getOrganizationIIRDetail": { + "get": { + "tags": [ + "IIRApi" + ], + "operationId": "IIRApi_GetOrganizationIIRDetail", + "parameters": [ + { + "name": "ctid", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + }, + "text/json": { + "schema": { + "type": "object" + } + }, + "application/xml": { + "schema": { + "type": "object" + } + }, + "text/xml": { + "schema": { + "type": "object" + } + } + } + } + } + } + }, + "/iir-api/getRegistryResource": { "get": { "tags": [ "IIRApi" @@ -149,7 +210,7 @@ } } }, - "/api/iir/validateDidWeb": { + "/iir-api/validateDidWeb": { "get": { "tags": [ "IIRApi" @@ -194,7 +255,7 @@ } } }, - "/api/iir/validateDidKey": { + "/iir-api/validateDidKey": { "get": { "tags": [ "IIRApi" @@ -247,7 +308,7 @@ } } }, - "/api/iir/createChallenges": { + "/iir-api/createChallenges": { "post": { "tags": [ "IIRApi" @@ -312,7 +373,7 @@ } } }, - "/api/iir/validateJwt": { + "/iir-api/validateJwt": { "post": { "tags": [ "IIRApi" @@ -377,7 +438,7 @@ } } }, - "/api/iir/saveChallengeToken": { + "/iir-api/saveChallengeToken": { "post": { "tags": [ "IIRApi" @@ -442,14 +503,14 @@ } } }, - "/api/iir/submitToIIR": { + "/iir-api/submitToIIR": { "post": { "tags": [ "IIRApi" ], "operationId": "IIRApi_SubmitToIIR", "requestBody": { - "$ref": "#/components/requestBodies/IssuerDTO" + "$ref": "#/components/requestBodies/PubIssuerDTO" }, "responses": { "200": { @@ -480,12 +541,48 @@ } } }, - "/api/iir/issuers": { + "/iir-api/issuers": { "get": { "tags": [ "IIRApi" ], "operationId": "IIRApi_GetIssuers", + "parameters": [ + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "keyword", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "task", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "OK", @@ -515,7 +612,7 @@ } } }, - "/api/iir/users/{userId}/issuers": { + "/iir-api/users/{userId}/issuers": { "get": { "tags": [ "IIRApi" @@ -530,6 +627,40 @@ "type": "string", "format": "uuid" } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "keyword", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "task", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } ], "responses": { @@ -561,21 +692,13 @@ } } }, - "/api/iir/federationFetch": { + "/iir-api/federationFetch": { "get": { "tags": [ "IIRApi" ], "operationId": "IIRApi_FederationFetch", "parameters": [ - { - "name": "ctid", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - }, { "name": "sub", "in": "query", @@ -614,7 +737,7 @@ } } }, - "/api/iir/issuerByDid": { + "/iir-api/issuerByDid": { "get": { "tags": [ "IIRApi" @@ -659,14 +782,14 @@ } } }, - "/api/iir/updateIssuer": { + "/iir-api/updateIssuer": { "put": { "tags": [ "IIRApi" ], "operationId": "IIRApi_UpdateIssuer", "requestBody": { - "$ref": "#/components/requestBodies/IssuerDTO" + "$ref": "#/components/requestBodies/PubIssuerDTO" }, "responses": { "200": { @@ -705,31 +828,31 @@ ], "components": { "requestBodies": { - "IssuerDTO": { + "PubIssuerDTO": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IssuerDTO" + "$ref": "#/components/schemas/PubIssuerDTO" } }, "text/json": { "schema": { - "$ref": "#/components/schemas/IssuerDTO" + "$ref": "#/components/schemas/PubIssuerDTO" } }, "application/xml": { "schema": { - "$ref": "#/components/schemas/IssuerDTO" + "$ref": "#/components/schemas/PubIssuerDTO" } }, "text/xml": { "schema": { - "$ref": "#/components/schemas/IssuerDTO" + "$ref": "#/components/schemas/PubIssuerDTO" } }, "application/x-www-form-urlencoded": { "schema": { - "$ref": "#/components/schemas/IssuerDTO" + "$ref": "#/components/schemas/PubIssuerDTO" } } }, @@ -820,7 +943,7 @@ } } }, - "IssuerDTO": { + "PubIssuerDTO": { "type": "object", "properties": { "CTID": { diff --git a/ce/iir/swagger.json b/ce/iir/swagger.json index 5f7d6bb..546034d 100644 --- a/ce/iir/swagger.json +++ b/ce/iir/swagger.json @@ -1,12 +1,19 @@ { "swagger": "2.0", - "info": { "version": "v1", "title": "CTI Directory API" }, + "info": { + "version": "v1", + "title": "CTI Directory API" + }, "host": "localhost:44330", - "schemes": ["https"], + "schemes": [ + "https" + ], "paths": { - "/api/iir/IsUserAMemberOfOrg": { + "/iir-api/IsUserAMemberOfOrg": { "get": { - "tags": ["IIRApi"], + "tags": [ + "IIRApi" + ], "operationId": "IIRApi_IsUserAMemberOfOrg", "consumes": [], "produces": [ @@ -21,19 +28,29 @@ "in": "query", "required": true, "type": "string" + }, + { + "name": "task", + "in": "query", + "required": true, + "type": "string" } ], "responses": { "200": { "description": "OK", - "schema": { "type": "object" } + "schema": { + "type": "object" + } } } } }, - "/api/iir/isUserAMember/{userId}": { + "/iir-api/isUserAMember/{userId}": { "get": { - "tags": ["IIRApi"], + "tags": [ + "IIRApi" + ], "operationId": "IIRApi_IsUserAMember", "consumes": [], "produces": [ @@ -50,6 +67,43 @@ "type": "string", "format": "uuid" }, + { + "name": "task", + "in": "query", + "required": true, + "type": "string" + }, + { + "name": "ctid", + "in": "query", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object" + } + } + } + } + }, + "/iir-api/getOrganizationIIRDetail": { + "get": { + "tags": [ + "IIRApi" + ], + "operationId": "IIRApi_GetOrganizationIIRDetail", + "consumes": [], + "produces": [ + "application/json", + "text/json", + "application/xml", + "text/xml" + ], + "parameters": [ { "name": "ctid", "in": "query", @@ -60,14 +114,18 @@ "responses": { "200": { "description": "OK", - "schema": { "type": "object" } + "schema": { + "type": "object" + } } } } }, - "/api/iir/getRegistryResource": { + "/iir-api/getRegistryResource": { "get": { - "tags": ["IIRApi"], + "tags": [ + "IIRApi" + ], "operationId": "IIRApi_GetRegistryResource", "consumes": [], "produces": [ @@ -87,14 +145,18 @@ "responses": { "200": { "description": "OK", - "schema": { "type": "object" } + "schema": { + "type": "object" + } } } } }, - "/api/iir/validateDidWeb": { + "/iir-api/validateDidWeb": { "get": { - "tags": ["IIRApi"], + "tags": [ + "IIRApi" + ], "operationId": "IIRApi_ValidateWeb", "consumes": [], "produces": [ @@ -114,14 +176,18 @@ "responses": { "200": { "description": "OK", - "schema": { "type": "object" } + "schema": { + "type": "object" + } } } } }, - "/api/iir/validateDidKey": { + "/iir-api/validateDidKey": { "get": { - "tags": ["IIRApi"], + "tags": [ + "IIRApi" + ], "operationId": "IIRApi_ValidateKey", "consumes": [], "produces": [ @@ -147,14 +213,18 @@ "responses": { "200": { "description": "OK", - "schema": { "type": "object" } + "schema": { + "type": "object" + } } } } }, - "/api/iir/createChallenges": { + "/iir-api/createChallenges": { "post": { - "tags": ["IIRApi"], + "tags": [ + "IIRApi" + ], "operationId": "IIRApi_CreateChallenges", "consumes": [ "application/json", @@ -182,14 +252,18 @@ "responses": { "200": { "description": "OK", - "schema": { "type": "object" } + "schema": { + "type": "object" + } } } } }, - "/api/iir/validateJwt": { + "/iir-api/validateJwt": { "post": { - "tags": ["IIRApi"], + "tags": [ + "IIRApi" + ], "operationId": "IIRApi_ValidateJwtSignature", "consumes": [ "application/json", @@ -209,20 +283,26 @@ "name": "model", "in": "body", "required": true, - "schema": { "$ref": "#/definitions/ValidateJwtRequest" } + "schema": { + "$ref": "#/definitions/ValidateJwtRequest" + } } ], "responses": { "200": { "description": "OK", - "schema": { "type": "object" } + "schema": { + "type": "object" + } } } } }, - "/api/iir/saveChallengeToken": { + "/iir-api/saveChallengeToken": { "post": { - "tags": ["IIRApi"], + "tags": [ + "IIRApi" + ], "operationId": "IIRApi_SaveChallengeToken", "consumes": [ "application/json", @@ -250,14 +330,18 @@ "responses": { "200": { "description": "OK", - "schema": { "type": "object" } + "schema": { + "type": "object" + } } } } }, - "/api/iir/submitToIIR": { + "/iir-api/submitToIIR": { "post": { - "tags": ["IIRApi"], + "tags": [ + "IIRApi" + ], "operationId": "IIRApi_SubmitToIIR", "consumes": [ "application/json", @@ -277,20 +361,26 @@ "name": "dto", "in": "body", "required": true, - "schema": { "$ref": "#/definitions/IssuerDTO" } + "schema": { + "$ref": "#/definitions/PubIssuerDTO" + } } ], "responses": { "200": { "description": "OK", - "schema": { "type": "object" } + "schema": { + "type": "object" + } } } } }, - "/api/iir/issuers": { + "/iir-api/issuers": { "get": { - "tags": ["IIRApi"], + "tags": [ + "IIRApi" + ], "operationId": "IIRApi_GetIssuers", "consumes": [], "produces": [ @@ -299,18 +389,49 @@ "application/xml", "text/xml" ], - "parameters": [], + "parameters": [ + { + "name": "page", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "keyword", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "task", + "in": "query", + "required": false, + "type": "string" + } + ], "responses": { "200": { "description": "OK", - "schema": { "type": "object" } + "schema": { + "type": "object" + } } } } }, - "/api/iir/users/{userId}/issuers": { + "/iir-api/users/{userId}/issuers": { "get": { - "tags": ["IIRApi"], + "tags": [ + "IIRApi" + ], "operationId": "IIRApi_GetIssuersByUser", "consumes": [], "produces": [ @@ -326,19 +447,49 @@ "required": true, "type": "string", "format": "uuid" + }, + { + "name": "page", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "keyword", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "task", + "in": "query", + "required": false, + "type": "string" } ], "responses": { "200": { "description": "OK", - "schema": { "type": "object" } + "schema": { + "type": "object" + } } } } }, - "/api/iir/federationFetch": { + "/iir-api/federationFetch": { "get": { - "tags": ["IIRApi"], + "tags": [ + "IIRApi" + ], "operationId": "IIRApi_FederationFetch", "consumes": [], "produces": [ @@ -348,12 +499,6 @@ "text/xml" ], "parameters": [ - { - "name": "ctid", - "in": "query", - "required": true, - "type": "string" - }, { "name": "sub", "in": "query", @@ -364,14 +509,18 @@ "responses": { "200": { "description": "OK", - "schema": { "type": "object" } + "schema": { + "type": "object" + } } } } }, - "/api/iir/issuerByDid": { + "/iir-api/issuerByDid": { "get": { - "tags": ["IIRApi"], + "tags": [ + "IIRApi" + ], "operationId": "IIRApi_GetIssuerByDid", "consumes": [], "produces": [ @@ -391,14 +540,18 @@ "responses": { "200": { "description": "OK", - "schema": { "type": "object" } + "schema": { + "type": "object" + } } } } }, - "/api/iir/updateIssuer": { + "/iir-api/updateIssuer": { "put": { - "tags": ["IIRApi"], + "tags": [ + "IIRApi" + ], "operationId": "IIRApi_UpdateIssuer", "consumes": [ "application/json", @@ -418,13 +571,17 @@ "name": "dto", "in": "body", "required": true, - "schema": { "$ref": "#/definitions/IssuerDTO" } + "schema": { + "$ref": "#/definitions/PubIssuerDTO" + } } ], "responses": { "200": { "description": "OK", - "schema": { "type": "object" } + "schema": { + "type": "object" + } } } } @@ -438,7 +595,10 @@ "type": "boolean", "readOnly": true }, - "CanBeCanceled": { "type": "boolean", "readOnly": true }, + "CanBeCanceled": { + "type": "boolean", + "readOnly": true + }, "WaitHandle": { "$ref": "#/definitions/WaitHandle", "readOnly": true @@ -448,31 +608,47 @@ "WaitHandle": { "type": "object", "properties": { - "Handle": { "type": "object" }, - "SafeWaitHandle": { "$ref": "#/definitions/SafeWaitHandle" } + "Handle": { + "type": "object" + }, + "SafeWaitHandle": { + "$ref": "#/definitions/SafeWaitHandle" + } } }, "SafeWaitHandle": { "type": "object", "properties": { - "IsInvalid": { "type": "boolean", "readOnly": true }, - "IsClosed": { "type": "boolean", "readOnly": true } + "IsInvalid": { + "type": "boolean", + "readOnly": true + }, + "IsClosed": { + "type": "boolean", + "readOnly": true + } } }, "CreateChallengesRequest": { "type": "object", "properties": { - "Ctid": { "type": "string" }, + "Ctid": { + "type": "string" + }, "VerificationMethodIds": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } } } }, "ValidateJwtRequest": { "type": "object", "properties": { - "Jwt": { "type": "string" }, + "Jwt": { + "type": "string" + }, "ChallengeId": { "format": "uuid", "type": "string", @@ -483,29 +659,55 @@ "SaveChallengeTokenRequest": { "type": "object", "properties": { - "Ctid": { "type": "string" }, + "Ctid": { + "type": "string" + }, "ChallengeId": { "format": "uuid", "type": "string", "example": "00000000-0000-0000-0000-000000000000" }, - "Token": { "type": "string" } + "Token": { + "type": "string" + } } }, - "IssuerDTO": { + "PubIssuerDTO": { "type": "object", "properties": { - "CTID": { "type": "string" }, - "DID": { "type": "string" }, - "Name": { "type": "string" }, - "LegalName": { "type": "string" }, - "CredentialRegistryUri": { "type": "string" }, - "SubjectWebPage": { "type": "string" }, - "LogoBase64": { "type": "string" }, - "LogoUri": { "type": "string" }, - "ValidFrom": { "format": "date-time", "type": "string" }, - "ValidUntil": { "format": "date-time", "type": "string" } + "CTID": { + "type": "string" + }, + "DID": { + "type": "string" + }, + "Name": { + "type": "string" + }, + "LegalName": { + "type": "string" + }, + "CredentialRegistryUri": { + "type": "string" + }, + "SubjectWebPage": { + "type": "string" + }, + "LogoBase64": { + "type": "string" + }, + "LogoUri": { + "type": "string" + }, + "ValidFrom": { + "format": "date-time", + "type": "string" + }, + "ValidUntil": { + "format": "date-time", + "type": "string" + } } } } -} +} \ No newline at end of file diff --git a/generate.ps1 b/generate.ps1 index 72b9f8b..b9d98af 100644 --- a/generate.ps1 +++ b/generate.ps1 @@ -1,5 +1,8 @@ +npm install -g swagger2openapi pip install openapi-python-client --user +swagger2openapi ce/iir/swagger.json -o ce/iir/openapi.json + python -m openapi_python_client generate ` --path ce/iir/openapi.json ` --config ce/iir/api/openapi-client-config.yml ` diff --git a/tests/cassettes/TestFullChallengeFlowIntegration.test_create_challenge.yaml b/tests/cassettes/TestFullChallengeFlowIntegration.test_create_challenge.yaml new file mode 100644 index 0000000..839b4ae --- /dev/null +++ b/tests/cassettes/TestFullChallengeFlowIntegration.test_create_challenge.yaml @@ -0,0 +1,46 @@ +interactions: +- request: + body: Ctid=ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73&VerificationMethodIds=did%3Akey%3Az6MkfUvJ9QJ7Gh6y9MXUyByLc3bxhuP9yffH8iHSNBmhhBic%23z6MkfUvJ9QJ7Gh6y9MXUyByLc3bxhuP9yffH8iHSNBmhhBic + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '178' + Content-Type: + - application/x-www-form-urlencoded + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: POST + uri: https://localhost:44330/iir-api/createChallenges + response: + body: + string: '[{"Challenge":"0b006f04-40c3-4641-808e-6de966cc6651","Did":"did:key:z6MkfUvJ9QJ7Gh6y9MXUyByLc3bxhuP9yffH8iHSNBmhhBic#z6MkfUvJ9QJ7Gh6y9MXUyByLc3bxhuP9yffH8iHSNBmhhBic","Aud":"https://issuerregistry.credentialengine.org","Iat":1778189668,"Exp":1778276068,"Ctid":"ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73","CreatedDate":"2026-05-07T21:34:28.7799348+00:00"}]' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '353' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:27 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcY3JlYXRlQ2hhbGxlbmdlcw==?= + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/TestFullChallengeFlowIntegration.test_full_sign_verify_save_flow.yaml b/tests/cassettes/TestFullChallengeFlowIntegration.test_full_sign_verify_save_flow.yaml new file mode 100644 index 0000000..9ffbca0 --- /dev/null +++ b/tests/cassettes/TestFullChallengeFlowIntegration.test_full_sign_verify_save_flow.yaml @@ -0,0 +1,134 @@ +interactions: +- request: + body: Ctid=ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73&VerificationMethodIds=did%3Akey%3Az6MkfUvJ9QJ7Gh6y9MXUyByLc3bxhuP9yffH8iHSNBmhhBic%23z6MkfUvJ9QJ7Gh6y9MXUyByLc3bxhuP9yffH8iHSNBmhhBic + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '178' + Content-Type: + - application/x-www-form-urlencoded + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: POST + uri: https://localhost:44330/iir-api/createChallenges + response: + body: + string: '[{"Challenge":"a7abef34-7b8a-4b98-98ff-8c8c1be8b7d9","Did":"did:key:z6MkfUvJ9QJ7Gh6y9MXUyByLc3bxhuP9yffH8iHSNBmhhBic#z6MkfUvJ9QJ7Gh6y9MXUyByLc3bxhuP9yffH8iHSNBmhhBic","Aud":"https://issuerregistry.credentialengine.org","Iat":1778189668,"Exp":1778276068,"Ctid":"ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73","CreatedDate":"2026-05-07T21:34:28.9188195+00:00"}]' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '353' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:28 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcY3JlYXRlQ2hhbGxlbmdlcw==?= + status: + code: 200 + message: OK +- request: + body: Jwt=eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2ZVdko5UUo3R2g2eTlNWFV5QnlMYzNieGh1UDl5ZmZIOGlIU05CbWhoQmljI3o2TWtmVXZKOVFKN0doNnk5TVhVeUJ5TGMzYnhodVA5eWZmSDhpSFNOQm1oaEJpYyIsInR5cCI6IkpXVCJ9.eyJkaWQiOiJkaWQ6a2V5Ono2TWtmVXZKOVFKN0doNnk5TVhVeUJ5TGMzYnhodVA5eWZmSDhpSFNOQm1oaEJpYyN6Nk1rZlV2SjlRSjdHaDZ5OU1YVXlCeUxjM2J4aHVQOXlmZkg4aUhTTkJtaGhCaWMiLCJjaGFsbGVuZ2UiOiJhN2FiZWYzNC03YjhhLTRiOTgtOThmZi04YzhjMWJlOGI3ZDkiLCJhdWQiOiJodHRwczovL2lzc3VlcnJlZ2lzdHJ5LmNyZWRlbnRpYWxlbmdpbmUub3JnIiwiaWF0IjoxNzc4MTg5NjY4LCJleHAiOjE3NzgyNzYwNjgsImN0aWQiOiJjZS1hNDA0MTk4My1iMWFlLTRhZDQtYTQzZC0yODRhNWI0YjJkNzMifQ.sffkQ2qrMSLJlIc0K-jUIJ71l-uu1TETag8vwUZpUuBd9fEbZP_QrTkkzu485zJpJy3kRgkcVBaAk1D3_yaLCQ&ChallengeId=a7abef34-7b8a-4b98-98ff-8c8c1be8b7d9 + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '731' + Content-Type: + - application/x-www-form-urlencoded + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: POST + uri: https://localhost:44330/iir-api/validateJwt + response: + body: + string: '{"Valid":true}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '14' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:28 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcdmFsaWRhdGVKd3Q=?= + status: + code: 200 + message: OK +- request: + body: Ctid=ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73&ChallengeId=a7abef34-7b8a-4b98-98ff-8c8c1be8b7d9&Token=eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2ZVdko5UUo3R2g2eTlNWFV5QnlMYzNieGh1UDl5ZmZIOGlIU05CbWhoQmljI3o2TWtmVXZKOVFKN0doNnk5TVhVeUJ5TGMzYnhodVA5eWZmSDhpSFNOQm1oaEJpYyIsInR5cCI6IkpXVCJ9.eyJkaWQiOiJkaWQ6a2V5Ono2TWtmVXZKOVFKN0doNnk5TVhVeUJ5TGMzYnhodVA5eWZmSDhpSFNOQm1oaEJpYyN6Nk1rZlV2SjlRSjdHaDZ5OU1YVXlCeUxjM2J4aHVQOXlmZkg4aUhTTkJtaGhCaWMiLCJjaGFsbGVuZ2UiOiJhN2FiZWYzNC03YjhhLTRiOTgtOThmZi04YzhjMWJlOGI3ZDkiLCJhdWQiOiJodHRwczovL2lzc3VlcnJlZ2lzdHJ5LmNyZWRlbnRpYWxlbmdpbmUub3JnIiwiaWF0IjoxNzc4MTg5NjY4LCJleHAiOjE3NzgyNzYwNjgsImN0aWQiOiJjZS1hNDA0MTk4My1iMWFlLTRhZDQtYTQzZC0yODRhNWI0YjJkNzMifQ.sffkQ2qrMSLJlIc0K-jUIJ71l-uu1TETag8vwUZpUuBd9fEbZP_QrTkkzu485zJpJy3kRgkcVBaAk1D3_yaLCQ + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '778' + Content-Type: + - application/x-www-form-urlencoded + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: POST + uri: https://localhost:44330/iir-api/saveChallengeToken + response: + body: + string: '{"ChallengeId":"a7abef34-7b8a-4b98-98ff-8c8c1be8b7d9","Ctid":"ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73","Token":"eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2ZVdko5UUo3R2g2eTlNWFV5QnlMYzNieGh1UDl5ZmZIOGlIU05CbWhoQmljI3o2TWtmVXZKOVFKN0doNnk5TVhVeUJ5TGMzYnhodVA5eWZmSDhpSFNOQm1oaEJpYyIsInR5cCI6IkpXVCJ9.eyJkaWQiOiJkaWQ6a2V5Ono2TWtmVXZKOVFKN0doNnk5TVhVeUJ5TGMzYnhodVA5eWZmSDhpSFNOQm1oaEJpYyN6Nk1rZlV2SjlRSjdHaDZ5OU1YVXlCeUxjM2J4aHVQOXlmZkg4aUhTTkJtaGhCaWMiLCJjaGFsbGVuZ2UiOiJhN2FiZWYzNC03YjhhLTRiOTgtOThmZi04YzhjMWJlOGI3ZDkiLCJhdWQiOiJodHRwczovL2lzc3VlcnJlZ2lzdHJ5LmNyZWRlbnRpYWxlbmdpbmUub3JnIiwiaWF0IjoxNzc4MTg5NjY4LCJleHAiOjE3NzgyNzYwNjgsImN0aWQiOiJjZS1hNDA0MTk4My1iMWFlLTRhZDQtYTQzZC0yODRhNWI0YjJkNzMifQ.sffkQ2qrMSLJlIc0K-jUIJ71l-uu1TETag8vwUZpUuBd9fEbZP_QrTkkzu485zJpJy3kRgkcVBaAk1D3_yaLCQ","IsTokenVerified":true,"CreatedDate":"2026-05-07T21:34:29.1912694+00:00"}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '865' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:28 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcc2F2ZUNoYWxsZW5nZVRva2Vu?= + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/TestGetOrganizationIIRDetailIntegration.test_known_ctid_returns_org_detail_or_skips.yaml b/tests/cassettes/TestGetOrganizationIIRDetailIntegration.test_known_ctid_returns_org_detail_or_skips.yaml new file mode 100644 index 0000000..7c02c57 --- /dev/null +++ b/tests/cassettes/TestGetOrganizationIIRDetailIntegration.test_known_ctid_returns_org_detail_or_skips.yaml @@ -0,0 +1,43 @@ +interactions: +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: GET + uri: https://localhost:44330/iir-api/getOrganizationIIRDetail?ctid=ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73 + response: + body: + string: '{"valid":true,"data":{"CTID":"ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73","LegalName":"Credential + Engine","LogoUrl":"https://credentialengine.org/wp-content/themes/credential-engine/img/logo.png"}}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '194' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:26 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcZ2V0T3JnYW5pemF0aW9uSUlSRGV0YWls?= + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/TestGetOrganizationIIRDetailIntegration.test_unknown_ctid_returns_failure.yaml b/tests/cassettes/TestGetOrganizationIIRDetailIntegration.test_unknown_ctid_returns_failure.yaml new file mode 100644 index 0000000..b4e93fc --- /dev/null +++ b/tests/cassettes/TestGetOrganizationIIRDetailIntegration.test_unknown_ctid_returns_failure.yaml @@ -0,0 +1,43 @@ +interactions: +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: GET + uri: https://localhost:44330/iir-api/getOrganizationIIRDetail?ctid=ce-00000000-0000-0000-0000-999999999999 + response: + body: + string: '{"Error":"{\"data\":null,\"valid\":false,\"status\":\"Organization + not found.\",\"extra\":null}"}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '97' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:26 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcZ2V0T3JnYW5pemF0aW9uSUlSRGV0YWls?= + status: + code: 404 + message: Not Found +version: 1 diff --git a/tests/cassettes/TestGetRegistryResourceIntegration.test_known_ctid_exists_in_registry.yaml b/tests/cassettes/TestGetRegistryResourceIntegration.test_known_ctid_exists_in_registry.yaml new file mode 100644 index 0000000..a849d1b --- /dev/null +++ b/tests/cassettes/TestGetRegistryResourceIntegration.test_known_ctid_exists_in_registry.yaml @@ -0,0 +1,44 @@ +interactions: +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: GET + uri: https://localhost:44330/iir-api/getRegistryResource?ctid=ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73 + response: + body: + string: '{"CTID":"ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73","ExistsInRegistry":true,"RegistryUri":"https://sandbox.credentialengineregistry.org/resources/ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73","CTDLType":"ceterms:CredentialOrganization","Name":"Credential + Engine Administration - Sandbox","LegalName":"Credential Engine Administration + - Sandbox","CredentialRegistryUri":"https://sandbox.credentialengineregistry.org/resources/ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73","SubjectWebpage":"https://sandbox.credentialengine.org/","Image":null}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '529' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:24 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcZ2V0UmVnaXN0cnlSZXNvdXJjZQ==?= + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/TestGetRegistryResourceIntegration.test_unknown_ctid_not_found.yaml b/tests/cassettes/TestGetRegistryResourceIntegration.test_unknown_ctid_not_found.yaml new file mode 100644 index 0000000..93810bd --- /dev/null +++ b/tests/cassettes/TestGetRegistryResourceIntegration.test_unknown_ctid_not_found.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: GET + uri: https://localhost:44330/iir-api/getRegistryResource?ctid=ce-00000000-0000-0000-0000-999999999999 + response: + body: + string: '{"CTID":"ce-00000000-0000-0000-0000-999999999999","ExistsInRegistry":false,"RegistryUri":"","CTDLType":""}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '106' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:26 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcZ2V0UmVnaXN0cnlSZXNvdXJjZQ==?= + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/TestIsUserMemberIntegration.test_bogus_ctid_returns_error.yaml b/tests/cassettes/TestIsUserMemberIntegration.test_bogus_ctid_returns_error.yaml new file mode 100644 index 0000000..e67f60c --- /dev/null +++ b/tests/cassettes/TestIsUserMemberIntegration.test_bogus_ctid_returns_error.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: GET + uri: https://localhost:44330/iir-api/isUserAMember/bf95cac3-ca3d-4e93-838c-257a4cdb46a6?ctid=ce-00000000-0000-0000-0000-999999999999&task=IIR + response: + body: + string: '{"valid":false}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '15' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:23 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcaXNVc2VyQU1lbWJlclxiZjk1Y2FjMy1jYTNkLTRlOTMtODM4Yy0yNTdhNGNkYjQ2YTY=?= + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/TestIsUserMemberIntegration.test_member_check_returns_a_bool.yaml b/tests/cassettes/TestIsUserMemberIntegration.test_member_check_returns_a_bool.yaml new file mode 100644 index 0000000..12b42c8 --- /dev/null +++ b/tests/cassettes/TestIsUserMemberIntegration.test_member_check_returns_a_bool.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: GET + uri: https://localhost:44330/iir-api/isUserAMember/bf95cac3-ca3d-4e93-838c-257a4cdb46a6?ctid=ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73&task=IIR + response: + body: + string: '{"valid":true}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '14' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:23 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcaXNVc2VyQU1lbWJlclxiZjk1Y2FjMy1jYTNkLTRlOTMtODM4Yy0yNTdhNGNkYjQ2YTY=?= + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/TestIsUserMemberIntegration.test_member_of_known_org.yaml b/tests/cassettes/TestIsUserMemberIntegration.test_member_of_known_org.yaml new file mode 100644 index 0000000..12b42c8 --- /dev/null +++ b/tests/cassettes/TestIsUserMemberIntegration.test_member_of_known_org.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: GET + uri: https://localhost:44330/iir-api/isUserAMember/bf95cac3-ca3d-4e93-838c-257a4cdb46a6?ctid=ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73&task=IIR + response: + body: + string: '{"valid":true}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '14' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:23 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcaXNVc2VyQU1lbWJlclxiZjk1Y2FjMy1jYTNkLTRlOTMtODM4Yy0yNTdhNGNkYjQ2YTY=?= + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/TestMergeAutofillEndToEnd.test_accounts_legal_name_overrides_when_present.yaml b/tests/cassettes/TestMergeAutofillEndToEnd.test_accounts_legal_name_overrides_when_present.yaml new file mode 100644 index 0000000..8b19b25 --- /dev/null +++ b/tests/cassettes/TestMergeAutofillEndToEnd.test_accounts_legal_name_overrides_when_present.yaml @@ -0,0 +1,85 @@ +interactions: +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: GET + uri: https://localhost:44330/iir-api/getRegistryResource?ctid=ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73 + response: + body: + string: '{"CTID":"ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73","ExistsInRegistry":true,"RegistryUri":"https://sandbox.credentialengineregistry.org/resources/ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73","CTDLType":"ceterms:CredentialOrganization","Name":"Credential + Engine Administration - Sandbox","LegalName":"Credential Engine Administration + - Sandbox","CredentialRegistryUri":"https://sandbox.credentialengineregistry.org/resources/ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73","SubjectWebpage":"https://sandbox.credentialengine.org/","Image":null}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '529' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:31 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcZ2V0UmVnaXN0cnlSZXNvdXJjZQ==?= + status: + code: 200 + message: OK +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: GET + uri: https://localhost:44330/iir-api/getOrganizationIIRDetail?ctid=ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73 + response: + body: + string: '{"valid":true,"data":{"CTID":"ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73","LegalName":"Credential + Engine","LogoUrl":"https://credentialengine.org/wp-content/themes/credential-engine/img/logo.png"}}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '194' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:31 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcZ2V0T3JnYW5pemF0aW9uSUlSRGV0YWls?= + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/TestMergeAutofillEndToEnd.test_registry_data_flows_through_merge.yaml b/tests/cassettes/TestMergeAutofillEndToEnd.test_registry_data_flows_through_merge.yaml new file mode 100644 index 0000000..de4595c --- /dev/null +++ b/tests/cassettes/TestMergeAutofillEndToEnd.test_registry_data_flows_through_merge.yaml @@ -0,0 +1,85 @@ +interactions: +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: GET + uri: https://localhost:44330/iir-api/getRegistryResource?ctid=ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73 + response: + body: + string: '{"CTID":"ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73","ExistsInRegistry":true,"RegistryUri":"https://sandbox.credentialengineregistry.org/resources/ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73","CTDLType":"ceterms:CredentialOrganization","Name":"Credential + Engine Administration - Sandbox","LegalName":"Credential Engine Administration + - Sandbox","CredentialRegistryUri":"https://sandbox.credentialengineregistry.org/resources/ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73","SubjectWebpage":"https://sandbox.credentialengine.org/","Image":null}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '529' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:30 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcZ2V0UmVnaXN0cnlSZXNvdXJjZQ==?= + status: + code: 200 + message: OK +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: GET + uri: https://localhost:44330/iir-api/getOrganizationIIRDetail?ctid=ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73 + response: + body: + string: '{"valid":true,"data":{"CTID":"ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73","LegalName":"Credential + Engine","LogoUrl":"https://credentialengine.org/wp-content/themes/credential-engine/img/logo.png"}}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '194' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:30 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcZ2V0T3JnYW5pemF0aW9uSUlSRGV0YWls?= + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/TestSubmitToIirIntegration.test_submit_returns_success_or_already_exists.yaml b/tests/cassettes/TestSubmitToIirIntegration.test_submit_returns_success_or_already_exists.yaml new file mode 100644 index 0000000..e36d261 --- /dev/null +++ b/tests/cassettes/TestSubmitToIirIntegration.test_submit_returns_success_or_already_exists.yaml @@ -0,0 +1,46 @@ +interactions: +- request: + body: CTID=ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73&DID=did%3Akey%3Az6MkfUvJ9QJ7Gh6y9MXUyByLc3bxhuP9yffH8iHSNBmhhBic&Name=Acme+Test&LegalName=Acme+Test+Corp&CredentialRegistryUri=https%3A%2F%2Fregistry.example.com&SubjectWebPage=https%3A%2F%2Facme.example.com + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '252' + Content-Type: + - application/x-www-form-urlencoded + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: POST + uri: https://localhost:44330/iir-api/submitToIIR + response: + body: + string: '{"Error":""}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '12' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:28 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcc3VibWl0VG9JSVI=?= + status: + code: 409 + message: Conflict +version: 1 diff --git a/tests/cassettes/TestSubmitToIirIntegration.test_submit_with_logo_uri_is_accepted.yaml b/tests/cassettes/TestSubmitToIirIntegration.test_submit_with_logo_uri_is_accepted.yaml new file mode 100644 index 0000000..29c3f27 --- /dev/null +++ b/tests/cassettes/TestSubmitToIirIntegration.test_submit_with_logo_uri_is_accepted.yaml @@ -0,0 +1,46 @@ +interactions: +- request: + body: CTID=ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73&DID=did%3Akey%3Az6MkfUvJ9QJ7Gh6y9MXUyByLc3bxhuP9yffH8iHSNBmhhBic&Name=Acme+Test&LegalName=Acme+Test+Corp&CredentialRegistryUri=https%3A%2F%2Fregistry.example.com&SubjectWebPage=https%3A%2F%2Facme.example.com&LogoUri=https%3A%2F%2Facme.example.com%2Flogo.png + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '302' + Content-Type: + - application/x-www-form-urlencoded + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: POST + uri: https://localhost:44330/iir-api/submitToIIR + response: + body: + string: '{"Error":""}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '12' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:30 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcc3VibWl0VG9JSVI=?= + status: + code: 409 + message: Conflict +version: 1 diff --git a/tests/cassettes/TestSubmitToIirIntegration.test_submit_with_valid_dates_is_accepted.yaml b/tests/cassettes/TestSubmitToIirIntegration.test_submit_with_valid_dates_is_accepted.yaml new file mode 100644 index 0000000..d774970 --- /dev/null +++ b/tests/cassettes/TestSubmitToIirIntegration.test_submit_with_valid_dates_is_accepted.yaml @@ -0,0 +1,46 @@ +interactions: +- request: + body: CTID=ce-a4041983-b1ae-4ad4-a43d-284a5b4b2d73&DID=did%3Akey%3Az6MkfUvJ9QJ7Gh6y9MXUyByLc3bxhuP9yffH8iHSNBmhhBic&Name=Acme+Test&LegalName=Acme+Test+Corp&CredentialRegistryUri=https%3A%2F%2Fregistry.example.com&SubjectWebPage=https%3A%2F%2Facme.example.com&ValidFrom=2024-01-01T00%3A00%3A00%2B00%3A00&ValidUntil=2025-01-01T00%3A00%3A00%2B00%3A00 + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '341' + Content-Type: + - application/x-www-form-urlencoded + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: POST + uri: https://localhost:44330/iir-api/submitToIIR + response: + body: + string: '{"Error":""}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '12' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:28 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcc3VibWl0VG9JSVI=?= + status: + code: 409 + message: Conflict +version: 1 diff --git a/tests/cassettes/TestValidateDidDispatcherIntegration.test_did_key_returns_verification_method_for_known_did.yaml b/tests/cassettes/TestValidateDidDispatcherIntegration.test_did_key_returns_verification_method_for_known_did.yaml new file mode 100644 index 0000000..f1117f6 --- /dev/null +++ b/tests/cassettes/TestValidateDidDispatcherIntegration.test_did_key_returns_verification_method_for_known_did.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: GET + uri: https://localhost:44330/iir-api/validateDidKey?alg=Ed25519&did=did%3Akey%3Az6MkfUvJ9QJ7Gh6y9MXUyByLc3bxhuP9yffH8iHSNBmhhBic + response: + body: + string: '{"Algorithm":"Ed25519","VerificationMethodIds":["did:key:z6MkfUvJ9QJ7Gh6y9MXUyByLc3bxhuP9yffH8iHSNBmhhBic#z6MkfUvJ9QJ7Gh6y9MXUyByLc3bxhuP9yffH8iHSNBmhhBic"],"PublicKeyBytes":"D0iEqVnPUSkCcPSlMi6tTI8sWRVUs7R98R67EB1UVRU="}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '221' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:31 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcdmFsaWRhdGVEaWRLZXk=?= + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/TestValidateDidDispatcherIntegration.test_did_web_returns_verification_method_for_known_did.yaml b/tests/cassettes/TestValidateDidDispatcherIntegration.test_did_web_returns_verification_method_for_known_did.yaml new file mode 100644 index 0000000..2532497 --- /dev/null +++ b/tests/cassettes/TestValidateDidDispatcherIntegration.test_did_web_returns_verification_method_for_known_did.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: GET + uri: https://localhost:44330/iir-api/validateDidWeb?did=did%3Aweb%3Asandbox.credentialengine.org + response: + body: + string: '{"DidDocumentUrl":"https://sandbox.credentialengine.org/.well-known/did.json","VerificationMethodIds":["did:web:sandbox.credentialengine.org#z6MkwYGVFm6oFReLYwoHLobjVV5PBjnaYwB9VvJahn4nAx4f"]}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '192' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:31 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcdmFsaWRhdGVEaWRXZWI=?= + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/TestValidateDidKeyIntegration.test_malformed_did_key_returns_error.yaml b/tests/cassettes/TestValidateDidKeyIntegration.test_malformed_did_key_returns_error.yaml new file mode 100644 index 0000000..ee751c0 --- /dev/null +++ b/tests/cassettes/TestValidateDidKeyIntegration.test_malformed_did_key_returns_error.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: GET + uri: https://localhost:44330/iir-api/validateDidKey?alg=Ed25519&did=did%3Akey%3Azinvalid + response: + body: + string: '{"Error":""}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '12' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:26 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcdmFsaWRhdGVEaWRLZXk=?= + status: + code: 400 + message: Bad Request +version: 1 diff --git a/tests/cassettes/TestValidateDidKeyIntegration.test_valid_did_key_returns_vm_ids.yaml b/tests/cassettes/TestValidateDidKeyIntegration.test_valid_did_key_returns_vm_ids.yaml new file mode 100644 index 0000000..b9bd9a3 --- /dev/null +++ b/tests/cassettes/TestValidateDidKeyIntegration.test_valid_did_key_returns_vm_ids.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: GET + uri: https://localhost:44330/iir-api/validateDidKey?alg=Ed25519&did=did%3Akey%3Az6MkfUvJ9QJ7Gh6y9MXUyByLc3bxhuP9yffH8iHSNBmhhBic + response: + body: + string: '{"Algorithm":"Ed25519","VerificationMethodIds":["did:key:z6MkfUvJ9QJ7Gh6y9MXUyByLc3bxhuP9yffH8iHSNBmhhBic#z6MkfUvJ9QJ7Gh6y9MXUyByLc3bxhuP9yffH8iHSNBmhhBic"],"PublicKeyBytes":"D0iEqVnPUSkCcPSlMi6tTI8sWRVUs7R98R67EB1UVRU="}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '221' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:26 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcdmFsaWRhdGVEaWRLZXk=?= + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/TestValidateDidWebIntegration.test_nonexistent_domain_returns_error.yaml b/tests/cassettes/TestValidateDidWebIntegration.test_nonexistent_domain_returns_error.yaml new file mode 100644 index 0000000..f4c4edf --- /dev/null +++ b/tests/cassettes/TestValidateDidWebIntegration.test_nonexistent_domain_returns_error.yaml @@ -0,0 +1,43 @@ +interactions: +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: GET + uri: https://localhost:44330/iir-api/validateDidWeb?did=did%3Aweb%3Athis-domain-does-not-exist-xyz.invalid + response: + body: + string: '{"Error":"{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.6.3\",\"title\":\"Bad + Gateway\",\"status\":502,\"traceId\":\"00-7b6306b31f4d9bb0daab52e6d7b5e0d5-2452abf3eceb879f-00\"}"}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '190' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:27 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcdmFsaWRhdGVEaWRXZWI=?= + status: + code: 502 + message: Bad Gateway +version: 1 diff --git a/tests/cassettes/TestValidateDidWebIntegration.test_valid_did_web_resolves.yaml b/tests/cassettes/TestValidateDidWebIntegration.test_valid_did_web_resolves.yaml new file mode 100644 index 0000000..4d5a211 --- /dev/null +++ b/tests/cassettes/TestValidateDidWebIntegration.test_valid_did_web_resolves.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Host: + - localhost:44330 + User-Agent: + - python-httpx/0.28.1 + authorization: + - REDACTED + method: GET + uri: https://localhost:44330/iir-api/validateDidWeb?did=did%3Aweb%3Asandbox.credentialengine.org + response: + body: + string: '{"DidDocumentUrl":"https://sandbox.credentialengine.org/.well-known/did.json","VerificationMethodIds":["did:web:sandbox.credentialengine.org#z6MkwYGVFm6oFReLYwoHLobjVV5PBjnaYwB9VvJahn4nAx4f"]}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Content-Length: + - '192' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 07 May 2026 21:34:27 GMT + Request-Context: + - appId=cid-v1:3de5a21a-3b87-4008-b83b-c667a37cd396 + Server: + - Microsoft-IIS/10.0 + X-Powered-By: + - ASP.NET + X-SourceFiles: + - =?UTF-8?B?RDpcQ3JlZGVudGlhbEVuZ2luZVxQdWJsaXNoZXJcRGlyZWN0b3J5XGlpci1hcGlcdmFsaWRhdGVEaWRXZWI=?= + status: + code: 200 + message: OK +version: 1 diff --git a/tests/conftest.py b/tests/conftest.py index 94579f3..e3e3455 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,17 +2,15 @@ from __future__ import annotations -from unittest.mock import MagicMock +import os from pathlib import Path -from dotenv import load_dotenv - +from unittest.mock import MagicMock import pytest +from dotenv import load_dotenv load_dotenv(Path(__file__).resolve().parents[1] / ".env.test") -import os - PUBLISHER_BASE = os.getenv("CE_PUBLISHER_BASE", "https://localhost:44330") ACCESS_TOKEN = os.getenv("CE_TEST_ACCESS_TOKEN", "") USER_ID = os.getenv("CE_TEST_USER_ID", "") @@ -38,6 +36,10 @@ def pytest_configure(config): "markers", "integration: mark test as an integration test requiring a live server", ) + config.addinivalue_line( + "markers", + "vcr: mark test as using recorded HTTP interactions (pytest-recording)", + ) @pytest.fixture @@ -46,3 +48,32 @@ def mock_env(): env.publisher_base = PUBLISHER_BASE env.ssl_verify = False return env + + + + +@pytest.fixture(scope="session") +def vcr_config(): + return { + "filter_headers": [ + ("authorization", "REDACTED"), + ("cookie", "REDACTED"), + ("x-api-key", "REDACTED"), + ], + "filter_query_parameters": [ + ("access_token", "REDACTED"), + ("token", "REDACTED"), + ], + "filter_post_data_parameters": [ + ("access_token", "REDACTED"), + ("client_secret", "REDACTED"), + ], + "match_on": ["method", "scheme", "host", "port", "path", "query"], + "record_mode": os.getenv("VCR_RECORD_MODE", "none"), + } + + +@pytest.fixture(scope="module") +def vcr_cassette_dir(request): + """Store cassettes alongside the test module under a 'cassettes/' folder.""" + return str(Path(request.module.__file__).parent / "cassettes") \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py deleted file mode 100644 index 54f6790..0000000 --- a/tests/test_cli.py +++ /dev/null @@ -1,543 +0,0 @@ -""" -Unit tests for ce.iir.did_ops. -Run with: - pytest -m "not integration" -s -""" - -from __future__ import annotations -import os -import base64 -import json -import sys -from types import SimpleNamespace -from unittest.mock import MagicMock, patch - -import pytest - - -pytestmark = pytest.mark.unit - -import ce.iir.did_ops as did_ops -from ce.iir.did_ops import ( - _client, - _did_web_to_url, - _extract_ed25519_pub_from_did_key, - _parse_uvarint, - call_create_challenge, - call_get_registry_resource, - call_is_user_member, - call_save_challenge_token, - call_submit_to_iir, - call_validate_did_key, - call_validate_did_web, - call_verify_jwt_signature, - classify_did, - extract_user_id_from_token, - sign_proof_jwt, - validate_ctid, - validate_date, - validate_did_key_prefix, - validate_verification_method, - verify_proof_jwt, -) - - -PUBLISHER_BASE = os.getenv("CE_PUBLISHER_BASE") -ACCESS_TOKEN = os.getenv("CE_TEST_ACCESS_TOKEN") -USER_ID = os.getenv("CE_TEST_USER_ID") -CTID = os.getenv("CE_TEST_CTID") -CHALLENGE_UUID = os.getenv("CE_TEST_CHALLENGE_UUID") -ED25519_DID_KEY = os.getenv("CE_TEST_DID_KEY") -ED25519_KID = ( - f"{ED25519_DID_KEY}#{ED25519_DID_KEY[len('did:key:'):]}" - if ED25519_DID_KEY.startswith("did:key:") - else "" -) - - -def _fake_response(status_code: int, parsed=None) -> SimpleNamespace: - return SimpleNamespace(status_code=status_code, parsed=parsed) - - -class _ParsedObj: - """Simulates the SDK model objects returned by the generated API client. - - The implementation calls `getattr(parsed, "additional_properties", {})` on - non-dict parsed values, so this helper must expose that property. - """ - - def __init__(self, **kwargs): - self._data = kwargs - - @property - def additional_properties(self): - return self._data - - def to_dict(self): - return dict(self._data) - - -class TestClient: - def test_client_strips_trailing_slash(self): - client = _client(ACCESS_TOKEN, PUBLISHER_BASE + "/", ssl_verify=False) - assert client._base_url == PUBLISHER_BASE - assert client.token == ACCESS_TOKEN - assert client._verify_ssl is False - - -class TestValidateCtid: - def test_valid_ctid_passes(self): - ok, err = validate_ctid(CTID) - assert ok - assert err == "" - - def test_empty_string_fails(self): - ok, err = validate_ctid("") - assert not ok - assert "empty" in err.lower() - - def test_wrong_format_fails(self): - ok, err = validate_ctid("not-a-ctid") - assert not ok - assert "ce-guid" in err - - def test_ctid_is_case_insensitive(self): - ok, _ = validate_ctid(CTID.upper()) - assert ok - - -class TestValidateDate: - def test_empty_value_is_treated_as_optional(self): - ok, err, iso = validate_date("", "ValidFrom") - assert ok - assert err == "" and iso == "" - - def test_valid_date_converts_to_iso8601(self): - ok, err, iso = validate_date("01/15/2024", "ValidFrom") - assert ok - assert err == "" - assert iso == "2024-01-15T00:00:00Z" - - def test_iso_format_input_is_rejected(self): - ok, err, _ = validate_date("2024-01-15", "ValidFrom") - assert not ok - assert "MM/DD/YYYY" in err - - def test_impossible_date_fails(self): - ok, err, _ = validate_date("13/99/2024", "ValidFrom") - assert not ok - assert "MM/DD/YYYY" in err - - -class TestClassifyDid: - def test_did_key_recognised(self): - kind, err = classify_did("did:key:z6Mk...") - assert kind == "did:key" and err == "" - - def test_did_web_recognised(self): - kind, err = classify_did("did:web:example.com") - assert kind == "did:web" and err == "" - - def test_unsupported_method_returns_unknown(self): - kind, err = classify_did("did:ethr:0x123") - assert kind == "unknown" - assert "unrecognised" in err - - def test_empty_did_returns_unknown(self): - kind, err = classify_did("") - assert kind == "unknown" - assert "empty" in err.lower() - - -class TestValidateDidKeyPrefix: - def test_ed25519_prefix_matches_algorithm(self): - ok, _ = validate_did_key_prefix("did:key:z6MkhaXgBZ", "Ed25519") - assert ok - - def test_prefix_algorithm_mismatch_fails(self): - ok, err = validate_did_key_prefix("did:key:z6MkhaXgBZ", "P-256") - assert not ok - assert "mismatch" in err - - def test_unsupported_algorithm_fails(self): - ok, err = validate_did_key_prefix("did:key:z6MkhaXgBZ", "RSA") - assert not ok - assert "unsupported" in err - - def test_missing_algorithm_fails(self): - ok, err = validate_did_key_prefix("did:key:z6MkhaXgBZ", "") - assert not ok - assert "required" in err - - def test_unrecognised_key_prefix_fails(self): - ok, err = validate_did_key_prefix("did:key:zXXXunknown", "Ed25519") - assert not ok - assert "unrecognised" in err - - -class TestValidateVerificationMethod: - def test_valid_vm_in_api_list_passes(self): - vm = f"{ED25519_DID_KEY}#key-1" - ok, _ = validate_verification_method(vm, ED25519_DID_KEY, [vm]) - assert ok - - def test_empty_vm_fails(self): - ok, err = validate_verification_method("", ED25519_DID_KEY, []) - assert not ok - assert "empty" in err.lower() - - def test_vm_missing_from_api_list_fails(self): - ok, err = validate_verification_method( - f"{ED25519_DID_KEY}#key-1", ED25519_DID_KEY, [f"{ED25519_DID_KEY}#key-2"] - ) - assert not ok - assert "not found" in err - - def test_vm_belonging_to_wrong_did_fails(self): - ok, err = validate_verification_method("did:key:zOTHER#key-1", ED25519_DID_KEY, []) - assert not ok - assert "does not belong" in err - - def test_empty_api_list_skips_membership_check(self): - ok, _ = validate_verification_method(f"{ED25519_DID_KEY}#key-1", ED25519_DID_KEY, []) - assert ok - - -class TestCallIsUserMember: - def test_returns_true_when_valid_dict_payload(self): - with patch("ce.iir.did_ops.is_user_member_sync") as mock_sync: - mock_sync.return_value = _fake_response(200, {"valid": True}) - ok, err = call_is_user_member(USER_ID, CTID, ACCESS_TOKEN, PUBLISHER_BASE, False) - assert ok and err == "" - - def test_object_payload_with_valid_true_succeeds(self): - # _ParsedObj.additional_properties returns {"valid": True}, which the - # implementation reads via getattr(parsed, "additional_properties", {}). - with patch("ce.iir.did_ops.is_user_member_sync") as mock_sync: - mock_sync.return_value = _fake_response(200, _ParsedObj(valid=True)) - ok, err = call_is_user_member(USER_ID, CTID, ACCESS_TOKEN, PUBLISHER_BASE, False) - assert ok and err == "" - - def test_object_payload_with_valid_false_fails(self): - # additional_properties returns {"valid": False} -> not a member. - with patch("ce.iir.did_ops.is_user_member_sync") as mock_sync: - mock_sync.return_value = _fake_response(200, _ParsedObj(valid=False)) - ok, err = call_is_user_member(USER_ID, CTID, ACCESS_TOKEN, PUBLISHER_BASE, False) - assert not ok - assert "not a member" in err.lower() - - def test_http_error_yields_not_member_message(self): - # The implementation does not inspect status_code after the sync call; - # a 4xx with parsed=None gives data={}, valid=False, and the "not a - # member" message. There is no branch that embeds the status code in err. - with patch("ce.iir.did_ops.is_user_member_sync") as mock_sync: - mock_sync.return_value = _fake_response(403, None) - ok, err = call_is_user_member(USER_ID, CTID, ACCESS_TOKEN, PUBLISHER_BASE, False) - assert not ok - assert err != "" - - def test_handles_exception_gracefully(self): - with patch("ce.iir.did_ops.is_user_member_sync", side_effect=RuntimeError("timeout")): - ok, err = call_is_user_member(USER_ID, CTID, ACCESS_TOKEN, PUBLISHER_BASE, False) - assert not ok and "timeout" in err - - -class TestCallGetRegistryResource: - def test_returns_data_when_ctid_exists(self): - body = _ParsedObj(ExistsInRegistry=True, Name="Acme", LegalName="Acme Corp") - with patch("ce.iir.did_ops.get_registry_resource_sync") as mock_sync: - mock_sync.return_value = _fake_response(200, body) - ok, data, err = call_get_registry_resource(CTID, ACCESS_TOKEN, PUBLISHER_BASE, False) - assert ok and err == "" - assert data["Name"] == "Acme" - - def test_returns_false_when_ctid_not_in_registry_dict_payload(self): - # parsed is a plain dict -> isinstance(parsed, dict) branch sets data = parsed. - # The function returns that dict as-is, not {}. - with patch("ce.iir.did_ops.get_registry_resource_sync") as mock_sync: - mock_sync.return_value = _fake_response(200, {"ExistsInRegistry": False}) - ok, data, err = call_get_registry_resource(CTID, ACCESS_TOKEN, PUBLISHER_BASE, False) - assert not ok - assert data == {"ExistsInRegistry": False} - assert "not found" in err - - def test_400_response_falls_through_to_not_found_message(self): - # There is no explicit 400 branch in call_get_registry_resource. - # parsed=None -> data={} -> ExistsInRegistry missing -> exists=False -> - # returns (False, {}, "CTID '...' not found in registry"). - with patch("ce.iir.did_ops.get_registry_resource_sync") as mock_sync: - mock_sync.return_value = _fake_response(400, None) - ok, data, err = call_get_registry_resource(CTID, ACCESS_TOKEN, PUBLISHER_BASE, False) - assert not ok and data == {} - assert "not found" in err - - def test_exception_returns_api_request_failed_message(self): - with patch( - "ce.iir.did_ops.get_registry_resource_sync", side_effect=RuntimeError("conn refused") - ): - ok, data, err = call_get_registry_resource(CTID, ACCESS_TOKEN, PUBLISHER_BASE, False) - assert not ok and data == {} - assert "conn refused" in err - - -class TestCallValidateDidKey: - def test_returns_verification_method_ids_on_success(self): - body = _ParsedObj(verificationMethodIds=[ED25519_KID]) - with patch("ce.iir.did_ops.validate_did_key_sync") as mock_sync: - mock_sync.return_value = _fake_response(200, body) - ok, data, err = call_validate_did_key( - ED25519_DID_KEY, "Ed25519", ACCESS_TOKEN, PUBLISHER_BASE, False - ) - assert ok and err == "" - assert ED25519_KID in data["verificationMethodIds"] - - def test_400_returns_generic_failure_message(self): - with patch("ce.iir.did_ops.validate_did_key_sync") as mock_sync: - mock_sync.return_value = _fake_response(400, None) - ok, data, err = call_validate_did_key( - "did:key:bad", "Ed25519", ACCESS_TOKEN, PUBLISHER_BASE, False - ) - assert not ok and data == {} - assert err == "DID validation failed" - - -class TestCallValidateDidWeb: - def test_success(self): - body = _ParsedObj(verificationMethodIds=["did:web:example.com#key-1"]) - with patch("ce.iir.did_ops.validate_did_web_sync") as mock_sync: - mock_sync.return_value = _fake_response(200, body) - ok, data, err = call_validate_did_web( - "did:web:example.com", ACCESS_TOKEN, PUBLISHER_BASE, False - ) - assert ok and err == "" - assert data["verificationMethodIds"] == ["did:web:example.com#key-1"] - - def test_502_means_remote_did_document_unreachable(self): - with patch("ce.iir.did_ops.validate_did_web_sync") as mock_sync: - mock_sync.return_value = _fake_response(502, None) - ok, data, err = call_validate_did_web( - "did:web:example.com", ACCESS_TOKEN, PUBLISHER_BASE, False - ) - assert not ok and data == {} - assert "502" in err - - -class TestCallCreateChallenge: - def _challenge_body(self): - return _ParsedObj( - challenge=CHALLENGE_UUID, - Did=ED25519_DID_KEY, - Aud="ce-api", - Iat=1700000000, - Exp=1700003600, - ) - - def test_returns_first_challenge_on_success(self): - with patch("ce.iir.did_ops.create_challenges_sync") as mock_sync: - mock_sync.return_value = _fake_response(200, [self._challenge_body()]) - ok, data, err = call_create_challenge( - CTID, ED25519_KID, ACCESS_TOKEN, PUBLISHER_BASE, False - ) - assert ok and err == "" - assert data["challenge"] == CHALLENGE_UUID - - def test_empty_response_list_is_an_error(self): - # An empty list is falsy -> `if not data` fires -> "returned empty" message. - with patch("ce.iir.did_ops.create_challenges_sync") as mock_sync: - mock_sync.return_value = _fake_response(200, []) - ok, data, err = call_create_challenge( - CTID, ED25519_KID, ACCESS_TOKEN, PUBLISHER_BASE, False - ) - assert not ok and data == {} - assert "empty" in err - - def test_400_returns_generic_failure_message(self): - with patch("ce.iir.did_ops.create_challenges_sync") as mock_sync: - mock_sync.return_value = _fake_response(400, None) - ok, data, err = call_create_challenge( - CTID, "bad-vm", ACCESS_TOKEN, PUBLISHER_BASE, False - ) - assert not ok and data == {} - assert err == "createChallenges failed" - - -class TestCallVerifyJwtSignature: - def test_success(self): - with patch("ce.iir.did_ops.validate_jwt_sync") as mock_sync: - mock_sync.return_value = _fake_response(200, None) - ok, err = call_verify_jwt_signature( - "proof.jwt.token", CHALLENGE_UUID, ACCESS_TOKEN, PUBLISHER_BASE, False - ) - assert ok - assert err == "" - - def test_404_means_challenge_expired_or_missing(self): - with patch("ce.iir.did_ops.validate_jwt_sync") as mock_sync: - mock_sync.return_value = _fake_response(404, None) - ok, err = call_verify_jwt_signature( - "proof.jwt.token", CHALLENGE_UUID, ACCESS_TOKEN, PUBLISHER_BASE, False - ) - assert not ok and err == "Challenge not found" - - def test_400_returns_generic_failure_message(self): - with patch("ce.iir.did_ops.validate_jwt_sync") as mock_sync: - mock_sync.return_value = _fake_response(400, None) - ok, err = call_verify_jwt_signature( - "bad.jwt", CHALLENGE_UUID, ACCESS_TOKEN, PUBLISHER_BASE, False - ) - assert not ok and err == "JWT validation failed" - - -class TestCallSaveChallengeToken: - def test_success(self): - with patch("ce.iir.did_ops.save_challenge_token_sync") as mock_sync: - mock_sync.return_value = _fake_response(200, None) - ok, err = call_save_challenge_token( - CTID, CHALLENGE_UUID, "proof.jwt", ACCESS_TOKEN, PUBLISHER_BASE, False - ) - assert ok and err == "" - - def test_400_returns_generic_failure_message(self): - with patch("ce.iir.did_ops.save_challenge_token_sync") as mock_sync: - mock_sync.return_value = _fake_response(400, None) - ok, err = call_save_challenge_token( - CTID, CHALLENGE_UUID, "bad.jwt", ACCESS_TOKEN, PUBLISHER_BASE, False - ) - assert not ok and err == "saveChallengeToken failed" - - -class TestCallSubmitToIir: - def _base_args(self): - return ( - CTID, ED25519_DID_KEY, "Acme", "Acme Corp", - "https://registry.example.com", "https://acme.example.com", - ACCESS_TOKEN, PUBLISHER_BASE, False, - ) - - def test_success(self): - with patch("ce.iir.did_ops.submit_to_iir_sync") as mock_sync: - mock_sync.return_value = _fake_response(200, None) - ok, err = call_submit_to_iir(*self._base_args()) - assert ok and err == "" - - def test_409_means_did_already_registered(self): - with patch("ce.iir.did_ops.submit_to_iir_sync") as mock_sync: - mock_sync.return_value = _fake_response(409, None) - ok, err = call_submit_to_iir(*self._base_args()) - assert not ok and err == "Did already exists" - - def test_valid_from_and_until_are_passed_as_model_fields(self): - with patch("ce.iir.did_ops.submit_to_iir_sync") as mock_sync: - mock_sync.return_value = _fake_response(200, None) - call_submit_to_iir( - *self._base_args(), - valid_from="2024-01-01T00:00:00Z", - valid_until="2025-01-01T00:00:00Z", - ) - _, kwargs = mock_sync.call_args - body = kwargs["body"] - assert body.valid_from == "2024-01-01T00:00:00Z" - assert body.valid_until == "2025-01-01T00:00:00Z" - - def test_empty_optional_fields_become_none_on_model(self): - with patch("ce.iir.did_ops.submit_to_iir_sync") as mock_sync: - mock_sync.return_value = _fake_response(200, None) - call_submit_to_iir(*self._base_args()) - _, kwargs = mock_sync.call_args - body = kwargs["body"] - assert body.logo_uri is None - assert body.logo_base_64 is None - assert body.valid_from is None - assert body.valid_until is None - - -class TestHelpers: - def test_parse_uvarint_roundtrip_example(self): - value, used = _parse_uvarint(bytes([0xED, 0x01])) - assert value == 0xED - assert used == 2 - - def test_extract_ed25519_pub_from_did_key_returns_32_bytes(self): - pub = _extract_ed25519_pub_from_did_key(ED25519_DID_KEY) - assert isinstance(pub, bytes) - assert len(pub) == 32 - - def test_did_web_to_url_without_path(self): - assert _did_web_to_url("did:web:example.com") == "https://example.com/.well-known/did.json" - - def test_did_web_to_url_with_path(self): - assert _did_web_to_url("did:web:example.com:users:alice") == "https://example.com/users/alice/did.json" - - -@pytest.fixture(scope="session") -def private_key() -> str: - key = os.getenv("CE_TEST_PRIVKEY_MULTIBASE", "").strip() - - if not key: - pytest.skip("CE_TEST_PRIVKEY_MULTIBASE not set in .env.test") - - try: - did_ops._extract_ed25519_seed(key) - except Exception as e: - pytest.skip(f"Invalid CE_TEST_PRIVKEY_MULTIBASE format: {e}") - - return key - - -class TestJwtCrypto: - def _make_payload(self) -> dict: - return { - "did": ED25519_DID_KEY, - "challenge": CHALLENGE_UUID, - "aud": "ce-api", - "iat": 1700000000, - "exp": 1700003600, - "ctid": CTID, - } - - def test_sign_produces_a_three_part_jwt(self, private_key): - token = sign_proof_jwt(private_key, ED25519_KID, self._make_payload()) - assert isinstance(token, str) - assert token.count(".") == 2 - - def test_sign_and_verify_roundtrip(self, private_key): - token = sign_proof_jwt(private_key, ED25519_KID, self._make_payload()) - ok, err = verify_proof_jwt(token, ED25519_KID) - assert ok, err - - def test_tampered_signature_fails_verification(self, private_key): - token = sign_proof_jwt(private_key, ED25519_KID, self._make_payload()) - parts = token.split(".") - first = parts[2][0] - parts[2] = ("B" if first != "B" else "C") + parts[2][1:] - ok, err = verify_proof_jwt(".".join(parts), ED25519_KID) - assert not ok and "signature" in err.lower() - - def test_bad_private_key_raises(self): - with pytest.raises(Exception): - sign_proof_jwt("zNOTVALID", ED25519_KID, self._make_payload()) - - -class TestExtractUserIdFromToken: - def _make_token(self, payload: dict) -> str: - header = base64.urlsafe_b64encode(b'{"alg":"RS256"}').rstrip(b"=").decode() - body = base64.urlsafe_b64encode(json.dumps(payload).encode()).rstrip(b"=").decode() - return f"{header}.{body}.fakesig" - - def test_extracts_uuid_sub_claim(self): - token = self._make_token({"sub": USER_ID}) - assert extract_user_id_from_token(token) == USER_ID - - def test_missing_sub_raises_value_error(self): - token = self._make_token({"iss": "keycloak"}) - with pytest.raises(ValueError, match="'sub' claim missing"): - extract_user_id_from_token(token) - - def test_non_uuid_sub_raises_value_error(self): - token = self._make_token({"sub": "not-a-uuid"}) - with pytest.raises(ValueError, match="not a valid GUID"): - extract_user_id_from_token(token) - - def test_malformed_token_raises(self): - with pytest.raises(ValueError): - extract_user_id_from_token("not.a.jwt.at.all") \ No newline at end of file diff --git a/tests/test_did_ops_integration.py b/tests/test_did_ops_integration.py index f0f9fde..6f45241 100644 --- a/tests/test_did_ops_integration.py +++ b/tests/test_did_ops_integration.py @@ -1,15 +1,17 @@ """ Integration tests for ce.iir.did_ops. -Configuration is loaded from .env.test (never hard-coded here). -Load the file before running, e.g.: +Config comes from .env.test — nothing is hard-coded in this file. Load it +before running, e.g. `set -a && source .env.test && set +a`, or rely on +python-dotenv (it's loaded below if available). - export $(grep -v '^#' .env.test | xargs) && pytest -m integration -s +Two ways to run: -Or with pytest-dotenv / python-dotenv auto-loading if configured in pytest.ini: + # Live: hits the real publisher. Use this to (re-)record cassettes. + VCR_RECORD_MODE=once pytest -m integration -s + # Playback (default): replays cassettes, no network, no secrets needed. pytest -m integration -s - """ from __future__ import annotations @@ -19,55 +21,63 @@ import pytest import requests as req +# Best-effort .env.test loading. If python-dotenv isn't installed we just +# fall back to whatever's already in os.environ. +try: + from dotenv import load_dotenv + + load_dotenv(".env.test") +except ImportError: + pass + from ce.iir.did_ops import ( - call_create_challenge, + call_create_challenges, + call_get_organization_iir_detail, call_get_registry_resource, call_is_user_member, call_save_challenge_token, + call_submit_to_iir, + call_validate_did, call_validate_did_key, call_validate_did_web, call_verify_jwt_signature, extract_user_id_from_token, + merge_autofill, sign_proof_jwt, verify_proof_jwt, ) -pytestmark = pytest.mark.integration +pytestmark = [pytest.mark.integration, pytest.mark.vcr] # --------------------------------------------------------------------------- -# Config — read once from the environment (populated from .env.test) +# Environment / config # --------------------------------------------------------------------------- +VCR_RECORD_MODE = os.environ.get("VCR_RECORD_MODE", "none").lower() +PLAYBACK_ONLY = VCR_RECORD_MODE == "none" + + PUBLISHER_BASE = os.environ.get("CE_PUBLISHER_BASE", "").rstrip("/") +TEST_CTID = os.environ.get("CE_TEST_CTID", "") +TEST_DID_KEY = os.environ.get("CE_TEST_DID_KEY", "") +TEST_DID_WEB = os.environ.get("CE_TEST_DID_WEB", "") +TEST_VM = os.environ.get("CE_TEST_VM", f"{TEST_DID_KEY}#{TEST_DID_KEY.split(':')[-1]}") +TEST_ALG = os.environ.get("CE_TEST_ALG", "Ed25519") +TEST_USER_ID = os.environ.get("CE_TEST_USER_ID", "").strip() -_RAW_DID_KEY = os.environ.get("CE_TEST_DID_KEY", "") -TEST_CTID = os.environ.get("CE_TEST_CTID", "") -TEST_USER_ID = os.environ.get("CE_TEST_USER_ID", "") -TEST_DID_KEY = _RAW_DID_KEY -TEST_DID_WEB = os.environ.get("CE_TEST_DID_WEB") -TEST_ALG = os.environ.get("CE_TEST_ALG", "Ed25519") -TEST_VM = ( - f"{_RAW_DID_KEY}#{_RAW_DID_KEY[len('did:key:'):]}" - if _RAW_DID_KEY.startswith("did:key:") - else os.environ.get("CE_TEST_VM", "") -) # --------------------------------------------------------------------------- -# Session-scoped fixtures +# Fixtures # --------------------------------------------------------------------------- @pytest.fixture(scope="session", autouse=True) def _require_env(): - """Fail fast with a clear message when mandatory variables are missing.""" - missing = [ - name for name in ( - "CE_PUBLISHER_BASE", - "CE_TEST_ACCESS_TOKEN", - "CE_TEST_CTID", - "CE_TEST_DID_KEY", - ) - if not os.environ.get(name, "").strip() - ] + """Bail out early — with a useful message — if live mode is missing config.""" + if PLAYBACK_ONLY: + return + + required = ("CE_PUBLISHER_BASE", "CE_TEST_ACCESS_TOKEN", "CE_TEST_CTID", "CE_TEST_DID_KEY") + missing = [name for name in required if not os.environ.get(name, "").strip()] if missing: pytest.skip( "Missing required environment variables (load .env.test first): " @@ -77,15 +87,19 @@ def _require_env(): @pytest.fixture(scope="session", autouse=True) def server_available(): - """Skip everything if the API server isn't reachable.""" + """In live/record mode, skip everything if the publisher isn't up.""" + if PLAYBACK_ONLY: + return try: - req.get(f"{PUBLISHER_BASE}/api/iir/isUserAMember", timeout=5, verify=False) - except req.exceptions.ConnectionError: + req.get(f"{PUBLISHER_BASE}/api/iir", timeout=5, verify=False) + except (req.exceptions.ConnectionError, req.exceptions.Timeout): pytest.skip(f"API server not reachable at {PUBLISHER_BASE}") @pytest.fixture(scope="session") def access_token() -> str: + if PLAYBACK_ONLY: + return "PLAYBACK_TOKEN" token = os.environ.get("CE_TEST_ACCESS_TOKEN", "").strip() if not token: pytest.skip("CE_TEST_ACCESS_TOKEN not set — skipping integration tests.") @@ -102,11 +116,20 @@ def private_key() -> str: @pytest.fixture(scope="session") def user_id(access_token) -> str: + # Explicit override wins, then playback dummy, then decode the live token. if TEST_USER_ID: return TEST_USER_ID + if PLAYBACK_ONLY: + return "00000000-0000-0000-0000-000000000000" return extract_user_id_from_token(access_token) +@pytest.fixture(scope="module") +def vcr_cassette_dir(request): + """Cassettes sit next to this file in ./cassettes/.""" + return os.path.join(os.path.dirname(request.module.__file__), "cassettes") + + # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- @@ -128,8 +151,11 @@ def test_member_of_known_org(self, access_token, user_id): def test_bogus_ctid_returns_error(self, access_token, user_id): ok, err = call_is_user_member( - user_id, "ce-00000000-0000-0000-0000-999999999999", - access_token, PUBLISHER_BASE, ssl_verify=False, + user_id, + "ce-00000000-0000-0000-0000-999999999999", + access_token, + PUBLISHER_BASE, + ssl_verify=False, ) assert not ok @@ -140,12 +166,38 @@ def test_known_ctid_exists_in_registry(self, access_token): TEST_CTID, access_token, PUBLISHER_BASE, ssl_verify=False ) assert ok, f"Registry lookup failed: {err}" + # Either field is fine — different orgs publish different shapes. assert "Name" in data or "LegalName" in data def test_unknown_ctid_not_found(self, access_token): - ok, data, err = call_get_registry_resource( + ok, _data, err = call_get_registry_resource( + "ce-00000000-0000-0000-0000-999999999999", + access_token, + PUBLISHER_BASE, + ssl_verify=False, + ) + assert not ok + assert err + + +class TestGetOrganizationIIRDetailIntegration: + def test_known_ctid_returns_org_detail_or_skips(self, access_token): + ok, data, err = call_get_organization_iir_detail( + TEST_CTID, access_token, PUBLISHER_BASE, ssl_verify=False + ) + if not ok: + pytest.skip(f"Org has no Accounts detail yet: {err}") + + assert data.get("LegalName") or data.get("LogoUrl"), ( + "getOrganizationIIRDetail returned valid=true but data was empty" + ) + + def test_unknown_ctid_returns_failure(self, access_token): + ok, _data, err = call_get_organization_iir_detail( "ce-00000000-0000-0000-0000-999999999999", - access_token, PUBLISHER_BASE, ssl_verify=False, + access_token, + PUBLISHER_BASE, + ssl_verify=False, ) assert not ok assert err @@ -160,7 +212,7 @@ def test_valid_did_key_returns_vm_ids(self, access_token): assert data def test_malformed_did_key_returns_error(self, access_token): - ok, data, err = call_validate_did_key( + ok, _data, _err = call_validate_did_key( "did:key:zinvalid", "Ed25519", access_token, PUBLISHER_BASE, ssl_verify=False ) assert not ok @@ -168,46 +220,64 @@ def test_malformed_did_key_returns_error(self, access_token): class TestValidateDidWebIntegration: def test_valid_did_web_resolves(self, access_token): - ok, data, err = call_validate_did_web( + if not TEST_DID_WEB: + pytest.skip("CE_TEST_DID_WEB not set — skipping did:web integration test.") + ok, _data, err = call_validate_did_web( TEST_DID_WEB, access_token, PUBLISHER_BASE, ssl_verify=False ) assert ok, f"DID web validation failed: {err}" def test_nonexistent_domain_returns_error(self, access_token): - ok, data, err = call_validate_did_web( + ok, _data, _err = call_validate_did_web( "did:web:this-domain-does-not-exist-xyz.invalid", - access_token, PUBLISHER_BASE, ssl_verify=False, + access_token, + PUBLISHER_BASE, + ssl_verify=False, ) assert not ok class TestFullChallengeFlowIntegration: def test_create_challenge(self, access_token): - ok, challenge, err = call_create_challenge( - TEST_CTID, TEST_VM, access_token, PUBLISHER_BASE, ssl_verify=False + ok, challenges, err = call_create_challenges( + TEST_CTID, [TEST_VM], access_token, PUBLISHER_BASE, ssl_verify=False ) assert ok, f"createChallenges failed: {err}" - assert challenge.get("challenge") or challenge.get("Challenge") + assert isinstance(challenges, list) and challenges + + first = challenges[0] + # The publisher has used both casings over time. + assert first.get("Challenge") or first.get("challenge") def test_full_sign_verify_save_flow(self, access_token, private_key): - ok, challenge, err = call_create_challenge( - TEST_CTID, TEST_VM, access_token, PUBLISHER_BASE, ssl_verify=False + ok, challenges, err = call_create_challenges( + TEST_CTID, [TEST_VM], access_token, PUBLISHER_BASE, ssl_verify=False ) assert ok, f"createChallenges failed: {err}" + assert challenges, "createChallenges returned empty list" + + # Pick the challenge that matches our VM, otherwise fall back to first. + challenge = next( + (c for c in challenges if (c.get("Did") or c.get("did")) == TEST_VM), + challenges[0], + ) + + challenge_uuid = challenge.get("Challenge") or challenge.get("challenge", "") + assert challenge_uuid, "Challenge UUID missing from response" - challenge_uuid = challenge.get("challenge") or challenge.get("Challenge", "") payload = { - "did": challenge.get("Did") or challenge.get("did", TEST_DID_KEY), + "did": challenge.get("Did") or challenge.get("did", TEST_DID_KEY), "challenge": challenge_uuid, - "aud": challenge.get("Aud") or challenge.get("aud", ""), - "iat": challenge.get("Iat") or challenge.get("iat", 0), - "exp": challenge.get("Exp") or challenge.get("exp", 0), - "ctid": TEST_CTID, + "aud": challenge.get("Aud") or challenge.get("aud", ""), + "iat": challenge.get("Iat") or challenge.get("iat", 0), + "exp": challenge.get("Exp") or challenge.get("exp", 0), + "ctid": TEST_CTID, } proof_jwt = sign_proof_jwt(private_key, TEST_VM, payload) assert proof_jwt + # Verify locally first — cheap sanity check before hitting the API. ok, err = verify_proof_jwt(proof_jwt, TEST_VM) assert ok, f"Local JWT verification failed: {err}" @@ -217,7 +287,163 @@ def test_full_sign_verify_save_flow(self, access_token, private_key): assert ok, f"API JWT verification failed: {err}" ok, err = call_save_challenge_token( - TEST_CTID, challenge_uuid, proof_jwt, - access_token, PUBLISHER_BASE, ssl_verify=False, + TEST_CTID, + challenge_uuid, + proof_jwt, + access_token, + PUBLISHER_BASE, + ssl_verify=False, ) - assert ok, f"saveChallengeToken failed: {err}" \ No newline at end of file + assert ok, f"saveChallengeToken failed: {err}" + + +class TestSubmitToIirIntegration: + def test_submit_returns_success_or_already_exists(self, access_token): + # On a fresh registry the submit succeeds; on a re-run it comes back + # as "Did already exists". Either is fine for this test. + ok, err = call_submit_to_iir( + TEST_CTID, + TEST_DID_KEY, + "Acme Test", + "Acme Test Corp", + "https://registry.example.com", + "https://acme.example.com", + access_token, + PUBLISHER_BASE, + ssl_verify=False, + ) + if not ok: + assert err == "Did already exists", ( + f"Expected success or 'Did already exists', got: {err}" + ) + + def test_submit_with_valid_dates_is_accepted(self, access_token): + ok, err = call_submit_to_iir( + TEST_CTID, + TEST_DID_KEY, + "Acme Test", + "Acme Test Corp", + "https://registry.example.com", + "https://acme.example.com", + access_token, + PUBLISHER_BASE, + ssl_verify=False, + valid_from="2024-01-01T00:00:00Z", + valid_until="2025-01-01T00:00:00Z", + ) + if not ok: + assert err == "Did already exists", ( + f"Expected success or 'Did already exists', got: {err}" + ) + + def test_submit_with_logo_uri_is_accepted(self, access_token): + # Just making sure the optional logo_uri field round-trips. + ok, err = call_submit_to_iir( + TEST_CTID, + TEST_DID_KEY, + "Acme Test", + "Acme Test Corp", + "https://registry.example.com", + "https://acme.example.com", + access_token, + PUBLISHER_BASE, + ssl_verify=False, + logo_uri="https://acme.example.com/logo.png", + ) + if not ok: + assert err == "Did already exists", ( + f"Expected success or 'Did already exists', got: {err}" + ) + + +class TestMergeAutofillEndToEnd: + def test_registry_data_flows_through_merge(self, access_token): + ok_reg, registry_data, err_reg = call_get_registry_resource( + TEST_CTID, access_token, PUBLISHER_BASE, ssl_verify=False + ) + assert ok_reg, f"Registry lookup failed: {err_reg}" + + ok_acc, accounts_data, _ = call_get_organization_iir_detail( + TEST_CTID, access_token, PUBLISHER_BASE, ssl_verify=False + ) + accounts = accounts_data if ok_acc else {} + + merged = merge_autofill(registry_data, accounts) + + expected_keys = { + "name", + "legal_name", + "registry_uri", + "subject_webpage", + "image", + "logo_base64", + } + assert set(merged.keys()) == expected_keys, ( + f"merge_autofill output shape changed: {set(merged.keys())}" + ) + + assert merged["name"], ( + f"Expected non-empty name from registry. " + f"Registry data keys: {list(registry_data.keys())}" + ) + + def test_accounts_legal_name_overrides_when_present(self, access_token): + ok_reg, registry_data, _ = call_get_registry_resource( + TEST_CTID, access_token, PUBLISHER_BASE, ssl_verify=False + ) + assert ok_reg + + ok_acc, accounts_data, _ = call_get_organization_iir_detail( + TEST_CTID, access_token, PUBLISHER_BASE, ssl_verify=False + ) + if not ok_acc or not accounts_data.get("LegalName"): + pytest.skip("Test org has no Accounts LegalName — can't verify override") + + merged = merge_autofill(registry_data, accounts_data) + assert merged["legal_name"] == accounts_data["LegalName"] + + +class TestValidateDidDispatcherIntegration: + def test_did_key_returns_verification_method_for_known_did(self, access_token): + ok, vm_ids, _data, err = call_validate_did( + TEST_DID_KEY, + TEST_ALG, + access_token, + PUBLISHER_BASE, + ssl_verify=False, + ) + assert ok, f"call_validate_did failed for did:key: {err}" + assert isinstance(vm_ids, list) + assert vm_ids, "Expected at least one verification method ID" + assert any(TEST_DID_KEY in vm for vm in vm_ids), ( + f"Expected '{TEST_DID_KEY}' in verification method IDs: {vm_ids}" + ) + + def test_did_web_returns_verification_method_for_known_did(self, access_token): + if not TEST_DID_WEB: + pytest.skip("CE_TEST_DID_WEB not set — skipping did:web dispatcher test.") + + ok, vm_ids, _data, err = call_validate_did( + TEST_DID_WEB, + "", # alg isn't used for did:web + access_token, + PUBLISHER_BASE, + ssl_verify=False, + ) + assert ok, f"call_validate_did failed for did:web: {err}" + assert isinstance(vm_ids, list) + assert vm_ids, "Expected at least one verification method ID" + + def test_unsupported_did_method_short_circuits_without_api_call(self, access_token): + # Dispatcher should reject unknown methods locally — no point burning + # an API call on something we know we can't handle. + ok, vm_ids, _data, err = call_validate_did( + "did:ethr:0x123", + "", + access_token, + PUBLISHER_BASE, + ssl_verify=False, + ) + assert not ok + assert vm_ids == [] + assert "unrecognised" in err.lower() \ No newline at end of file diff --git a/tests/test_did_ops_unit.py b/tests/test_did_ops_unit.py index 7ec752d..60eff99 100644 --- a/tests/test_did_ops_unit.py +++ b/tests/test_did_ops_unit.py @@ -1,59 +1,62 @@ """ Unit tests for ce.iir.did_ops. Run with: - pytest -m "not integration" -s + pytest -m unit -s """ from __future__ import annotations -import os import base64 import json -import sys +from datetime import datetime, timezone from types import SimpleNamespace -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest pytestmark = pytest.mark.unit -import ce.iir.did_ops as did_ops + from ce.iir.did_ops import ( + IIR_TASK, _client, _did_web_to_url, _extract_ed25519_pub_from_did_key, _parse_uvarint, - call_create_challenge, + _parsed_to_dict, + call_create_challenges, + call_get_organization_iir_detail, call_get_registry_resource, call_is_user_member, call_save_challenge_token, call_submit_to_iir, + call_validate_did, call_validate_did_key, call_validate_did_web, call_verify_jwt_signature, classify_did, extract_user_id_from_token, + merge_autofill, sign_proof_jwt, validate_ctid, validate_date, + validate_date_range, validate_did_key_prefix, validate_verification_method, verify_proof_jwt, ) +from ce.iir.api.iir_client.types import UNSET -PUBLISHER_BASE = os.getenv("CE_PUBLISHER_BASE") -ACCESS_TOKEN = os.getenv("CE_TEST_ACCESS_TOKEN") -USER_ID = os.getenv("CE_TEST_USER_ID") -CTID = os.getenv("CE_TEST_CTID") -CHALLENGE_UUID = os.getenv("CE_TEST_CHALLENGE_UUID") -ED25519_DID_KEY = os.getenv("CE_TEST_DID_KEY") -ED25519_KID = ( - f"{ED25519_DID_KEY}#{ED25519_DID_KEY[len('did:key:'):]}" - if ED25519_DID_KEY.startswith("did:key:") - else "" -) +#Test parameters +PUBLISHER_BASE = "https://fake-publisher.test" +ACCESS_TOKEN = "fake-token-for-unit-tests" +USER_ID = "00000000-0000-0000-0000-000000000001" +CTID = "ce-12345678-1234-1234-1234-123456789abc" +CHALLENGE_UUID = "11111111-2222-3333-4444-555555555555" +ED25519_DID_KEY = "did:key:z6MkhaXgBZDvf7gKKWXjGmgkbQXyrXYY4uZmTaTzPhRgg9Wm" +ED25519_KID = f"{ED25519_DID_KEY}#{ED25519_DID_KEY[len('did:key:'):]}" def _fake_response(status_code: int, parsed=None) -> SimpleNamespace: return SimpleNamespace(status_code=status_code, parsed=parsed) @@ -69,9 +72,6 @@ class _ParsedObj: def __init__(self, **kwargs): self._data = kwargs - # FIX 1: added so call_is_user_member / call_get_registry_resource can - # read the payload — without this, getattr(parsed, "additional_properties", {}) - # always returned {}, causing every _ParsedObj-based test to fail. @property def additional_properties(self): return self._data @@ -80,6 +80,54 @@ def to_dict(self): return dict(self._data) +@pytest.fixture(scope="session") +def ed25519_keypair(): + """Generate a fresh Ed25519 keypair for crypto tests. + + Returns (private_key_multibase, did_key, kid). The matching public + key is encoded as a did:key, so signatures created with this private + key will verify against the returned kid. + """ + try: + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + except ImportError: + pytest.skip("cryptography package not installed") + + try: + import base58 + except ImportError: + pytest.skip("base58 package not installed") + + sk = Ed25519PrivateKey.generate() + seed = sk.private_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PrivateFormat.Raw, + encryption_algorithm=serialization.NoEncryption(), + ) + pub = sk.public_key().public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) + + # did:key encoding: multicodec prefix 0xed01 (Ed25519 pub) + raw pub bytes, + # then base58btc encoded with 'z' multibase prefix. + pub_multicodec = bytes([0xED, 0x01]) + pub + did_key = "did:key:z" + base58.b58encode(pub_multicodec).decode() + + # Private key in the same multibase form your code expects. + # Adjust the multicodec prefix if did_ops uses a different encoding. + priv_multicodec = bytes([0x80, 0x26]) + seed # 0x8026 = Ed25519 priv multicodec + priv_multibase = "z" + base58.b58encode(priv_multicodec).decode() + + kid = f"{did_key}#{did_key[len('did:key:'):]}" + return priv_multibase, did_key, kid + + +# --------------------------------------------------------------------------- +# Client construction +# --------------------------------------------------------------------------- + class TestClient: def test_client_strips_trailing_slash(self): client = _client(ACCESS_TOKEN, PUBLISHER_BASE + "/", ssl_verify=False) @@ -88,11 +136,32 @@ def test_client_strips_trailing_slash(self): assert client._verify_ssl is False +class TestParsedToDict: + """``_parsed_to_dict`` centralises SDK response unwrapping; verify all 3 shapes.""" + + def test_dict_passthrough(self): + assert _parsed_to_dict({"valid": True}) == {"valid": True} + + def test_object_with_to_dict(self): + assert _parsed_to_dict(_ParsedObj(valid=True, name="x")) == {"valid": True, "name": "x"} + + def test_object_with_only_additional_properties(self): + class Bare: + additional_properties = {"valid": False} + + assert _parsed_to_dict(Bare()) == {"valid": False} + + def test_none_returns_empty_dict(self): + assert _parsed_to_dict(None) == {} + + def test_falsy_returns_empty_dict(self): + assert _parsed_to_dict({}) == {} + + class TestValidateCtid: def test_valid_ctid_passes(self): ok, err = validate_ctid(CTID) - assert ok - assert err == "" + assert ok and err == "" def test_empty_string_fails(self): ok, err = validate_ctid("") @@ -112,13 +181,11 @@ def test_ctid_is_case_insensitive(self): class TestValidateDate: def test_empty_value_is_treated_as_optional(self): ok, err, iso = validate_date("", "ValidFrom") - assert ok - assert err == "" and iso == "" + assert ok and err == "" and iso == "" def test_valid_date_converts_to_iso8601(self): ok, err, iso = validate_date("01/15/2024", "ValidFrom") - assert ok - assert err == "" + assert ok and err == "" assert iso == "2024-01-15T00:00:00Z" def test_iso_format_input_is_rejected(self): @@ -132,6 +199,42 @@ def test_impossible_date_fails(self): assert "MM/DD/YYYY" in err +class TestValidateDateRange: + + def test_both_empty_is_ok(self): + ok, err = validate_date_range("", "") + assert ok and err == "" + + def test_only_from_provided_fails(self): + ok, err = validate_date_range("01/01/2024", "") + assert not ok + assert "both" in err.lower() + + def test_only_until_provided_fails(self): + ok, err = validate_date_range("", "01/01/2024") + assert not ok + assert "both" in err.lower() + + def test_until_before_from_fails(self): + ok, err = validate_date_range("06/01/2024", "01/01/2024") + assert not ok + assert "after" in err.lower() + + def test_until_equal_to_from_fails(self): + ok, err = validate_date_range("01/01/2024", "01/01/2024") + assert not ok + assert "after" in err.lower() + + def test_until_after_from_passes(self): + ok, err = validate_date_range("01/01/2024", "01/02/2024") + assert ok and err == "" + + def test_invalid_format_propagates_error(self): + ok, err = validate_date_range("not-a-date", "01/01/2024") + assert not ok + assert "MM/DD/YYYY" in err + + class TestClassifyDid: def test_did_key_recognised(self): kind, err = classify_did("did:key:z6Mk...") @@ -214,16 +317,11 @@ def test_returns_true_when_valid_dict_payload(self): assert ok and err == "" def test_object_payload_with_valid_true_succeeds(self): - # _ParsedObj.additional_properties returns {"valid": True}, which the - # implementation reads via getattr(parsed, "additional_properties", {}). with patch("ce.iir.did_ops.is_user_member_sync") as mock_sync: mock_sync.return_value = _fake_response(200, _ParsedObj(valid=True)) ok, err = call_is_user_member(USER_ID, CTID, ACCESS_TOKEN, PUBLISHER_BASE, False) assert ok and err == "" - # FIX 2: was "test_current_implementation_treats_object_payload_as_success" - # and incorrectly asserted ok=True. With additional_properties present, - # valid=False is now correctly read, so the call returns (False, "not a member"). def test_object_payload_with_valid_false_fails(self): with patch("ce.iir.did_ops.is_user_member_sync") as mock_sync: mock_sync.return_value = _fake_response(200, _ParsedObj(valid=False)) @@ -231,21 +329,34 @@ def test_object_payload_with_valid_false_fails(self): assert not ok assert "not a member" in err.lower() - # FIX 3: was "test_propagates_http_error_status" asserting "403" in err. - # call_is_user_member has no status-code branch — a 403 with parsed=None - # gives data={}, valid=False, and returns the generic "not a member" message. def test_http_error_yields_not_member_message(self): with patch("ce.iir.did_ops.is_user_member_sync") as mock_sync: mock_sync.return_value = _fake_response(403, None) ok, err = call_is_user_member(USER_ID, CTID, ACCESS_TOKEN, PUBLISHER_BASE, False) - assert not ok - assert err != "" + assert not ok and err != "" def test_handles_exception_gracefully(self): with patch("ce.iir.did_ops.is_user_member_sync", side_effect=RuntimeError("timeout")): ok, err = call_is_user_member(USER_ID, CTID, ACCESS_TOKEN, PUBLISHER_BASE, False) assert not ok and "timeout" in err + def test_default_task_is_iir(self): + with patch("ce.iir.did_ops.is_user_member_sync") as mock_sync: + mock_sync.return_value = _fake_response(200, {"valid": True}) + call_is_user_member(USER_ID, CTID, ACCESS_TOKEN, PUBLISHER_BASE, False) + _, kwargs = mock_sync.call_args + assert kwargs["task"] == "IIR" == IIR_TASK + assert kwargs["ctid"] == CTID + + def test_explicit_task_overrides_default(self): + with patch("ce.iir.did_ops.is_user_member_sync") as mock_sync: + mock_sync.return_value = _fake_response(200, {"valid": True}) + call_is_user_member( + USER_ID, CTID, ACCESS_TOKEN, PUBLISHER_BASE, False, task="OTHER" + ) + _, kwargs = mock_sync.call_args + assert kwargs["task"] == "OTHER" + class TestCallGetRegistryResource: def test_returns_data_when_ctid_exists(self): @@ -256,8 +367,6 @@ def test_returns_data_when_ctid_exists(self): assert ok and err == "" assert data["Name"] == "Acme" - # FIX 4: was asserting data == {}. The implementation returns the parsed dict - # as-is on ExistsInRegistry=False, not an empty dict. def test_returns_false_when_ctid_not_in_registry_dict_payload(self): with patch("ce.iir.did_ops.get_registry_resource_sync") as mock_sync: mock_sync.return_value = _fake_response(200, {"ExistsInRegistry": False}) @@ -266,9 +375,6 @@ def test_returns_false_when_ctid_not_in_registry_dict_payload(self): assert data == {"ExistsInRegistry": False} assert "not found" in err - # FIX 5: was asserting err == "Registry lookup failed". There is no status-code - # branch in call_get_registry_resource — a 400 with parsed=None gives data={}, - # ExistsInRegistry missing → exists=False → returns the "not found" message. def test_400_falls_through_to_not_found_message(self): with patch("ce.iir.did_ops.get_registry_resource_sync") as mock_sync: mock_sync.return_value = _fake_response(400, None) @@ -278,23 +384,133 @@ def test_400_falls_through_to_not_found_message(self): def test_exception_returns_api_request_failed_message(self): with patch( - "ce.iir.did_ops.get_registry_resource_sync", side_effect=RuntimeError("conn refused") + "ce.iir.did_ops.get_registry_resource_sync", + side_effect=RuntimeError("conn refused"), ): ok, data, err = call_get_registry_resource(CTID, ACCESS_TOKEN, PUBLISHER_BASE, False) assert not ok and data == {} assert "conn refused" in err +class TestCallGetOrganizationIIRDetail: + def test_unwraps_envelope_on_success(self): + body = _ParsedObj( + valid=True, + data={"LegalName": "Acme Corp", "LogoUrl": "https://acme/logo.png"}, + ) + with patch("ce.iir.did_ops.get_organization_iir_detail_sync") as mock_sync: + mock_sync.return_value = _fake_response(200, body) + ok, data, err = call_get_organization_iir_detail( + CTID, ACCESS_TOKEN, PUBLISHER_BASE, False + ) + assert ok and err == "" + assert data["LegalName"] == "Acme Corp" + assert data["LogoUrl"] == "https://acme/logo.png" + + def test_valid_false_returns_failure(self): + with patch("ce.iir.did_ops.get_organization_iir_detail_sync") as mock_sync: + mock_sync.return_value = _fake_response(200, {"valid": False}) + ok, data, err = call_get_organization_iir_detail( + CTID, ACCESS_TOKEN, PUBLISHER_BASE, False + ) + assert not ok + assert "not available" in err.lower() + + def test_empty_envelope_returns_failure(self): + with patch("ce.iir.did_ops.get_organization_iir_detail_sync") as mock_sync: + mock_sync.return_value = _fake_response(200, None) + ok, data, err = call_get_organization_iir_detail( + CTID, ACCESS_TOKEN, PUBLISHER_BASE, False + ) + assert not ok and data == {} + assert "empty" in err.lower() + + def test_missing_data_key_returns_empty_dict(self): + with patch("ce.iir.did_ops.get_organization_iir_detail_sync") as mock_sync: + mock_sync.return_value = _fake_response(200, {"valid": True}) + ok, data, err = call_get_organization_iir_detail( + CTID, ACCESS_TOKEN, PUBLISHER_BASE, False + ) + assert ok and err == "" + assert data == {} + + def test_exception_returns_failure(self): + with patch( + "ce.iir.did_ops.get_organization_iir_detail_sync", + side_effect=RuntimeError("network error"), + ): + ok, data, err = call_get_organization_iir_detail( + CTID, ACCESS_TOKEN, PUBLISHER_BASE, False + ) + assert not ok and data == {} + assert "network error" in err + + +class TestMergeAutofill: + def test_accounts_legal_name_overrides_registry(self): + registry = {"Name": "Acme", "LegalName": "Acme Inc.", "Image": "logo.png"} + accounts = {"LegalName": "Acme Corporation"} + merged = merge_autofill(registry, accounts) + assert merged["legal_name"] == "Acme Corporation" + assert merged["name"] == "Acme" + assert merged["image"] == "logo.png" + + def test_accounts_logo_only_fills_when_registry_image_empty(self): + registry = {"Name": "Acme", "LegalName": "", "Image": "registry-logo.png"} + accounts = {"LegalName": "Acme Corp", "LogoUrl": "accounts-logo.png"} + merged = merge_autofill(registry, accounts) + assert merged["image"] == "registry-logo.png" + + def test_accounts_logo_used_when_registry_image_blank(self): + registry = {"Name": "Acme", "LegalName": "", "Image": ""} + accounts = {"LegalName": "Acme Corp", "LogoUrl": "accounts-logo.png"} + merged = merge_autofill(registry, accounts) + assert merged["image"] == "accounts-logo.png" + + def test_no_accounts_data_falls_back_to_registry(self): + registry = { + "Name": "Acme", + "LegalName": "Acme Inc.", + "Image": "logo.png", + "CredentialRegistryUri": "https://registry/x", + "SubjectWebpage": "https://acme.example", + "LogoBase64": "abc", + } + merged = merge_autofill(registry, {}) + assert merged["legal_name"] == "Acme Inc." + assert merged["image"] == "logo.png" + assert merged["registry_uri"] == "https://registry/x" + assert merged["subject_webpage"] == "https://acme.example" + assert merged["logo_base64"] == "abc" + + def test_empty_accounts_legal_name_does_not_clear_registry(self): + registry = {"Name": "Acme", "LegalName": "Acme Inc."} + accounts = {"LegalName": ""} + merged = merge_autofill(registry, accounts) + assert merged["legal_name"] == "Acme Inc." + + def test_handles_missing_registry_keys_gracefully(self): + merged = merge_autofill({}, {}) + assert merged == { + "name": "", + "legal_name": "", + "registry_uri": "", + "subject_webpage": "", + "image": "", + "logo_base64": "", + } + + class TestCallValidateDidKey: def test_returns_verification_method_ids_on_success(self): - body = _ParsedObj(verificationMethodIds=[ED25519_KID]) + body = _ParsedObj(VerificationMethodIds=[ED25519_KID]) with patch("ce.iir.did_ops.validate_did_key_sync") as mock_sync: mock_sync.return_value = _fake_response(200, body) ok, data, err = call_validate_did_key( ED25519_DID_KEY, "Ed25519", ACCESS_TOKEN, PUBLISHER_BASE, False ) assert ok and err == "" - assert ED25519_KID in data["verificationMethodIds"] + assert ED25519_KID in data["VerificationMethodIds"] def test_400_returns_generic_failure_message(self): with patch("ce.iir.did_ops.validate_did_key_sync") as mock_sync: @@ -308,14 +524,14 @@ def test_400_returns_generic_failure_message(self): class TestCallValidateDidWeb: def test_success(self): - body = _ParsedObj(verificationMethodIds=["did:web:example.com#key-1"]) + body = _ParsedObj(VerificationMethodIds=["did:web:example.com#key-1"]) with patch("ce.iir.did_ops.validate_did_web_sync") as mock_sync: mock_sync.return_value = _fake_response(200, body) ok, data, err = call_validate_did_web( "did:web:example.com", ACCESS_TOKEN, PUBLISHER_BASE, False ) assert ok and err == "" - assert data["verificationMethodIds"] == ["did:web:example.com#key-1"] + assert data["VerificationMethodIds"] == ["did:web:example.com#key-1"] def test_502_means_remote_did_document_unreachable(self): with patch("ce.iir.did_ops.validate_did_web_sync") as mock_sync: @@ -327,42 +543,122 @@ def test_502_means_remote_did_document_unreachable(self): assert "502" in err -class TestCallCreateChallenge: - def _challenge_body(self): +class TestCallValidateDid: + def test_did_key_path_returns_vm_ids(self): + body = _ParsedObj(VerificationMethodIds=[ED25519_KID]) + with patch("ce.iir.did_ops.validate_did_key_sync") as mock_sync: + mock_sync.return_value = _fake_response(200, body) + ok, vm_ids, data, err = call_validate_did( + ED25519_DID_KEY, "Ed25519", ACCESS_TOKEN, PUBLISHER_BASE, False + ) + assert ok and err == "" + assert vm_ids == [ED25519_KID] + assert data["VerificationMethodIds"] == [ED25519_KID] + + def test_did_web_path_returns_vm_ids(self): + body = _ParsedObj(VerificationMethodIds=["did:web:example.com#key-1"]) + with patch("ce.iir.did_ops.validate_did_web_sync") as mock_sync: + mock_sync.return_value = _fake_response(200, body) + ok, vm_ids, data, err = call_validate_did( + "did:web:example.com", "", ACCESS_TOKEN, PUBLISHER_BASE, False + ) + assert ok and err == "" + assert vm_ids == ["did:web:example.com#key-1"] + + def test_unsupported_did_method_returns_classify_error(self): + ok, vm_ids, _data, err = call_validate_did( + "did:ethr:0x123", "", ACCESS_TOKEN, PUBLISHER_BASE, False + ) + assert not ok + assert vm_ids == [] + assert "unrecognised" in err + + def test_did_key_with_bad_prefix_short_circuits(self): + with patch("ce.iir.did_ops.validate_did_key_sync") as mock_sync: + ok, vm_ids, _, err = call_validate_did( + "did:key:zXXXunknown", "Ed25519", ACCESS_TOKEN, PUBLISHER_BASE, False + ) + assert not ok and vm_ids == [] + assert "unrecognised" in err.lower() + mock_sync.assert_not_called() + + def test_missing_vm_ids_returns_empty_list_not_none(self): + with patch("ce.iir.did_ops.validate_did_web_sync") as mock_sync: + mock_sync.return_value = _fake_response(200, _ParsedObj(somethingElse=True)) + ok, vm_ids, _, err = call_validate_did( + "did:web:example.com", "", ACCESS_TOKEN, PUBLISHER_BASE, False + ) + assert ok and err == "" + assert vm_ids == [] + + +class TestCallCreateChallenges: + """Batch challenges: takes a list of vm_ids, returns a list of challenge dicts.""" + + def _challenge_body(self, did=None, challenge=None): return _ParsedObj( - challenge=CHALLENGE_UUID, - Did=ED25519_DID_KEY, + Challenge=challenge or CHALLENGE_UUID, + Did=did or ED25519_DID_KEY, Aud="ce-api", Iat=1700000000, Exp=1700003600, ) - def test_returns_first_challenge_on_success(self): + def test_returns_list_on_success(self): with patch("ce.iir.did_ops.create_challenges_sync") as mock_sync: - mock_sync.return_value = _fake_response(200, [self._challenge_body()]) - ok, data, err = call_create_challenge( - CTID, ED25519_KID, ACCESS_TOKEN, PUBLISHER_BASE, False + mock_sync.return_value = _fake_response( + 200, + [ + self._challenge_body(did=f"{ED25519_DID_KEY}#k1"), + self._challenge_body(did=f"{ED25519_DID_KEY}#k2"), + ], + ) + ok, data, err = call_create_challenges( + CTID, + [f"{ED25519_DID_KEY}#k1", f"{ED25519_DID_KEY}#k2"], + ACCESS_TOKEN, PUBLISHER_BASE, False, ) assert ok and err == "" - assert data["challenge"] == CHALLENGE_UUID + assert isinstance(data, list) + assert len(data) == 2 + assert all(c["Challenge"] == CHALLENGE_UUID for c in data) + + def test_passes_all_vm_ids_in_single_request(self): + vm_ids = [f"{ED25519_DID_KEY}#k1", f"{ED25519_DID_KEY}#k2"] + with patch("ce.iir.did_ops.create_challenges_sync") as mock_sync: + mock_sync.return_value = _fake_response(200, [self._challenge_body()]) + call_create_challenges(CTID, vm_ids, ACCESS_TOKEN, PUBLISHER_BASE, False) + _, kwargs = mock_sync.call_args + body = kwargs["body"] + assert mock_sync.call_count == 1 + assert body.verification_method_ids == vm_ids + assert body.ctid == CTID + + def test_empty_vm_ids_short_circuits_without_api_call(self): + with patch("ce.iir.did_ops.create_challenges_sync") as mock_sync: + ok, data, err = call_create_challenges( + CTID, [], ACCESS_TOKEN, PUBLISHER_BASE, False + ) + assert not ok and data == [] + assert "no verification methods" in err.lower() + mock_sync.assert_not_called() def test_empty_response_list_is_an_error(self): - # An empty list is falsy -> `if not data` fires -> "returned empty" message. with patch("ce.iir.did_ops.create_challenges_sync") as mock_sync: mock_sync.return_value = _fake_response(200, []) - ok, data, err = call_create_challenge( - CTID, ED25519_KID, ACCESS_TOKEN, PUBLISHER_BASE, False + ok, data, err = call_create_challenges( + CTID, [ED25519_KID], ACCESS_TOKEN, PUBLISHER_BASE, False ) - assert not ok and data == {} + assert not ok and data == [] assert "empty" in err def test_400_returns_generic_failure_message(self): with patch("ce.iir.did_ops.create_challenges_sync") as mock_sync: mock_sync.return_value = _fake_response(400, None) - ok, data, err = call_create_challenge( - CTID, "bad-vm", ACCESS_TOKEN, PUBLISHER_BASE, False + ok, data, err = call_create_challenges( + CTID, ["bad-vm"], ACCESS_TOKEN, PUBLISHER_BASE, False ) - assert not ok and data == {} + assert not ok and data == [] assert err == "createChallenges failed" @@ -373,8 +669,7 @@ def test_success(self): ok, err = call_verify_jwt_signature( "proof.jwt.token", CHALLENGE_UUID, ACCESS_TOKEN, PUBLISHER_BASE, False ) - assert ok - assert err == "" + assert ok and err == "" def test_404_means_challenge_expired_or_missing(self): with patch("ce.iir.did_ops.validate_jwt_sync") as mock_sync: @@ -431,7 +726,7 @@ def test_409_means_did_already_registered(self): ok, err = call_submit_to_iir(*self._base_args()) assert not ok and err == "Did already exists" - def test_valid_from_and_until_are_passed_as_model_fields(self): + def test_iso_strings_become_datetime_objects_on_dto(self): with patch("ce.iir.did_ops.submit_to_iir_sync") as mock_sync: mock_sync.return_value = _fake_response(200, None) call_submit_to_iir( @@ -441,19 +736,54 @@ def test_valid_from_and_until_are_passed_as_model_fields(self): ) _, kwargs = mock_sync.call_args body = kwargs["body"] - assert body.valid_from == "2024-01-01T00:00:00Z" - assert body.valid_until == "2025-01-01T00:00:00Z" + assert isinstance(body.valid_from, datetime) + assert isinstance(body.valid_until, datetime) + assert body.valid_from == datetime(2024, 1, 1, tzinfo=timezone.utc) + assert body.valid_until == datetime(2025, 1, 1, tzinfo=timezone.utc) + + def test_yyyy_mm_dd_dates_also_parse(self): + with patch("ce.iir.did_ops.submit_to_iir_sync") as mock_sync: + mock_sync.return_value = _fake_response(200, None) + call_submit_to_iir( + *self._base_args(), valid_from="2024-06-15", valid_until="2025-06-15" + ) + _, kwargs = mock_sync.call_args + body = kwargs["body"] + assert isinstance(body.valid_from, datetime) + assert body.valid_from.year == 2024 and body.valid_from.month == 6 + + def test_empty_optional_fields_are_unset_not_none(self): + with patch("ce.iir.did_ops.submit_to_iir_sync") as mock_sync: + mock_sync.return_value = _fake_response(200, None) + call_submit_to_iir(*self._base_args()) + _, kwargs = mock_sync.call_args + body = kwargs["body"] + assert body.logo_uri is UNSET + assert body.logo_base_64 is UNSET + assert body.valid_from is UNSET + assert body.valid_until is UNSET - def test_empty_optional_fields_become_none_on_model(self): + def test_dto_to_dict_does_not_crash_on_empty_optionals(self): with patch("ce.iir.did_ops.submit_to_iir_sync") as mock_sync: mock_sync.return_value = _fake_response(200, None) call_submit_to_iir(*self._base_args()) _, kwargs = mock_sync.call_args body = kwargs["body"] - assert body.logo_uri is None - assert body.logo_base_64 is None - assert body.valid_from is None - assert body.valid_until is None + d = body.to_dict() + assert "ValidFrom" not in d + assert "ValidUntil" not in d + assert "LogoUri" not in d + assert "LogoBase64" not in d + + def test_invalid_date_string_falls_back_to_unset(self): + with patch("ce.iir.did_ops.submit_to_iir_sync") as mock_sync: + mock_sync.return_value = _fake_response(200, None) + call_submit_to_iir( + *self._base_args(), valid_from="garbage-date", valid_until="" + ) + _, kwargs = mock_sync.call_args + body = kwargs["body"] + assert body.valid_from is UNSET class TestHelpers: @@ -471,28 +801,17 @@ def test_did_web_to_url_without_path(self): assert _did_web_to_url("did:web:example.com") == "https://example.com/.well-known/did.json" def test_did_web_to_url_with_path(self): - assert _did_web_to_url("did:web:example.com:users:alice") == "https://example.com/users/alice/did.json" - - -@pytest.fixture(scope="session") -def private_key() -> str: - key = os.getenv("CE_TEST_PRIVKEY_MULTIBASE", "").strip() - - if not key: - pytest.skip("CE_TEST_PRIVKEY_MULTIBASE not set in .env.test") - - try: - did_ops._extract_ed25519_seed(key) - except Exception as e: - pytest.skip(f"Invalid CE_TEST_PRIVKEY_MULTIBASE format: {e}") + assert ( + _did_web_to_url("did:web:example.com:users:alice") + == "https://example.com/users/alice/did.json" + ) - return key class TestJwtCrypto: - def _make_payload(self) -> dict: + def _make_payload(self, did_key: str) -> dict: return { - "did": ED25519_DID_KEY, + "did": did_key, "challenge": CHALLENGE_UUID, "aud": "ce-api", "iat": 1700000000, @@ -500,31 +819,31 @@ def _make_payload(self) -> dict: "ctid": CTID, } - def test_sign_produces_a_three_part_jwt(self, private_key): - token = sign_proof_jwt(private_key, ED25519_KID, self._make_payload()) + def test_sign_produces_a_three_part_jwt(self, ed25519_keypair): + priv, did_key, kid = ed25519_keypair + token = sign_proof_jwt(priv, kid, self._make_payload(did_key)) assert isinstance(token, str) assert token.count(".") == 2 - def test_sign_and_verify_roundtrip(self, private_key): - token = sign_proof_jwt(private_key, ED25519_KID, self._make_payload()) - ok, err = verify_proof_jwt(token, ED25519_KID) + def test_sign_and_verify_roundtrip(self, ed25519_keypair): + priv, did_key, kid = ed25519_keypair + token = sign_proof_jwt(priv, kid, self._make_payload(did_key)) + ok, err = verify_proof_jwt(token, kid) assert ok, err - # FIX 6: the old version mutated the last base64url character, which may be - # padding-ignored (it can contribute 0 bits of actual data), so the decoded - # signature bytes were unchanged and verification still passed. Mutating the - # first character always affects the decoded bytes. - def test_tampered_signature_fails_verification(self, private_key): - token = sign_proof_jwt(private_key, ED25519_KID, self._make_payload()) + def test_tampered_signature_fails_verification(self, ed25519_keypair): + priv, did_key, kid = ed25519_keypair + token = sign_proof_jwt(priv, kid, self._make_payload(did_key)) parts = token.split(".") first = parts[2][0] parts[2] = ("B" if first != "B" else "C") + parts[2][1:] - ok, err = verify_proof_jwt(".".join(parts), ED25519_KID) + ok, err = verify_proof_jwt(".".join(parts), kid) assert not ok and "signature" in err.lower() - def test_bad_private_key_raises(self): + def test_bad_private_key_raises(self, ed25519_keypair): + _, _, kid = ed25519_keypair with pytest.raises(Exception): - sign_proof_jwt("zNOTVALID", ED25519_KID, self._make_payload()) + sign_proof_jwt("zNOTVALID", kid, self._make_payload(ED25519_DID_KEY)) class TestExtractUserIdFromToken: