Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

save_exchange_record should be Optional #1195

Merged
merged 3 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions app/models/issuer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from aries_cloudcontroller import LDProofVCDetail, TxnOrPublishRevocationsResult
from pydantic import BaseModel, Field, ValidationInfo, field_validator, model_validator

from app.util.save_exchange_record import SaveExchangeRecordField
from shared.exceptions import CloudApiValueError


Expand All @@ -18,14 +19,10 @@ class IndyCredential(BaseModel):
attributes: Dict[str, str]


class CredentialBase(BaseModel):
class CredentialBase(SaveExchangeRecordField):
type: CredentialType = CredentialType.INDY
indy_credential_detail: Optional[IndyCredential] = None
ld_credential_detail: Optional[LDProofVCDetail] = None
save_exchange_record: bool = Field(
default=False,
description="Whether the credential exchange record should be saved on completion",
)

@field_validator("indy_credential_detail", mode="before")
@classmethod
Expand Down
16 changes: 6 additions & 10 deletions app/models/verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from aries_cloudcontroller import IndyProofRequest as AcaPyIndyProofRequest
from pydantic import BaseModel, Field, ValidationInfo, field_validator, model_validator

from app.util.save_exchange_record import SaveExchangeRecordField
from shared.exceptions import CloudApiValueError


Expand Down Expand Up @@ -62,11 +63,10 @@ class ProofRequestMetadata(BaseModel):
comment: Optional[str] = None


class CreateProofRequest(ProofRequestBase, ProofRequestMetadata):
save_exchange_record: bool = Field(
default=False,
description="Whether the presentation exchange record should be saved on completion",
)
class CreateProofRequest(
ProofRequestBase, ProofRequestMetadata, SaveExchangeRecordField
):
pass


class SendProofRequest(CreateProofRequest):
Expand All @@ -77,14 +77,10 @@ class ProofId(BaseModel):
proof_id: str


class AcceptProofRequest(ProofId):
class AcceptProofRequest(ProofId, SaveExchangeRecordField):
type: ProofRequestType = ProofRequestType.INDY
indy_presentation_spec: Optional[IndyPresSpec] = None
dif_presentation_spec: Optional[DIFPresSpec] = None
save_exchange_record: bool = Field(
default=False,
description="Whether the presentation exchange record should be saved on completion",
)

@field_validator("indy_presentation_spec", mode="before")
@classmethod
Expand Down
4 changes: 2 additions & 2 deletions app/routes/issuer.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,8 +409,8 @@ async def get_credential(
NB: An issuer and a holder will have distinct credential exchange ids, despite referring to the same exchange.
The `thread_id` is the only record attribute that will be the same for the holder and the issuer.

An exchange record will automatically be deleted after a flow completes (i.e. when state is 'done'),
unless the `save_exchange_record` was set to true.
An exchange record will, by default, automatically be deleted after a flow completes (i.e. when state is 'done'),
unless the `save_exchange_record` was set to true, or the wallet is configured to preserve records by default.

The following parameters can be set to filter the fetched exchange records: connection_id, role, state, thread_id.

Expand Down
4 changes: 2 additions & 2 deletions app/services/issuer/acapy_issuer_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ async def send_credential(

bound_logger.debug("Issue v2 credential (automated)")
request_body = V20CredExFree(
auto_remove=not credential.save_exchange_record,
auto_remove=credential.auto_remove,
connection_id=credential.connection_id,
filter=cred_filter,
credential_preview=credential_preview,
Expand Down Expand Up @@ -113,7 +113,7 @@ async def create_offer(

bound_logger.debug("Creating v2 credential offer")
request_body = V20CredOfferConnFreeRequest(
auto_remove=not credential.save_exchange_record,
auto_remove=credential.auto_remove,
credential_preview=credential_preview,
filter=cred_filter,
)
Expand Down
7 changes: 4 additions & 3 deletions app/services/verifier/acapy_verifier_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ async def create_proof_request(
bound_logger = logger.bind(body=create_proof_request)
bound_logger.debug("Creating v2 proof request")
request_body = V20PresCreateRequestRequest(
auto_remove=not create_proof_request.save_exchange_record,
auto_remove=create_proof_request.auto_remove,
presentation_request=presentation_request,
auto_verify=True,
comment=create_proof_request.comment,
Expand Down Expand Up @@ -95,7 +95,7 @@ async def send_proof_request(

bound_logger = logger.bind(body=send_proof_request)
request_body = V20PresSendRequestRequest(
auto_remove=not send_proof_request.save_exchange_record,
auto_remove=send_proof_request.auto_remove,
connection_id=send_proof_request.connection_id,
presentation_request=presentation_request,
auto_verify=True,
Expand All @@ -121,7 +121,8 @@ async def send_proof_request(
async def accept_proof_request(
cls, controller: AcaPyClient, accept_proof_request: AcceptProofRequest
) -> PresentationExchange:
auto_remove = not accept_proof_request.save_exchange_record
auto_remove = accept_proof_request.auto_remove

if accept_proof_request.type == ProofRequestType.INDY:
presentation_spec = V20PresSpecByFormatRequest(
auto_remove=auto_remove,
Expand Down
5 changes: 3 additions & 2 deletions app/tests/e2e/issuer/test_save_exchange_record.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import json
from typing import Optional

import pytest
from fastapi import HTTPException
Expand All @@ -15,13 +16,13 @@


@pytest.mark.anyio
@pytest.mark.parametrize("save_exchange_record", [False, True])
@pytest.mark.parametrize("save_exchange_record", [None, False, True])
async def test_issue_credential_with_save_exchange_record(
faber_client: RichAsyncClient,
credential_definition_id: str,
faber_and_alice_connection: FaberAliceConnect,
alice_member_client: RichAsyncClient,
save_exchange_record: bool,
save_exchange_record: Optional[bool],
) -> CredentialExchange:
credential = {
"connection_id": faber_and_alice_connection.faber_connection_id,
Expand All @@ -34,7 +35,7 @@

# create and send credential offer- issuer
faber_send_response = (
await faber_client.post(

Check failure on line 38 in app/tests/e2e/issuer/test_save_exchange_record.py

View workflow job for this annotation

GitHub Actions / JUnit Test Report

test_save_exchange_record.test_issue_credential_with_save_exchange_record[clean-clean-clean-clean-clean-None]

fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}
Raw output
self = <shared.util.rich_async_client.RichAsyncClient object at 0x7fb386b27c50>
method = 'post', url = '/v1/issuer/credentials'
kwargs = {'json': {'connection_id': '2a2eb3bb-1d25-4ea1-aad0-b5b04b40e84f', 'indy_credential_detail': {'attributes': {'age': '4...ce', 'speed': '10'}, 'credential_definition_id': '2AszGVdGiN6A2sa6AQPGD5:3:CL:245:tag'}, 'save_exchange_record': None}}
attempt = 0, response = <Response [422 Unprocessable Entity]>, code = 422

    async def _request_with_retries(self, method: str, url: str, **kwargs) -> Response:
        for attempt in range(self.retries):
            try:
                response = await getattr(super(), method)(url, **kwargs)
>               return await self._handle_response(response)

shared/util/rich_async_client.py:67: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
shared/util/rich_async_client.py:50: in _handle_response
    response.raise_for_status()  # Raise exception for 4xx and 5xx status codes
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Response [422 Unprocessable Entity]>

    def raise_for_status(self) -> Response:
        """
        Raise the `HTTPStatusError` if one occurred.
        """
        request = self._request
        if request is None:
            raise RuntimeError(
                "Cannot call `raise_for_status` as the request "
                "instance has not been set on this response."
            )
    
        if self.is_success:
            return self
    
        if self.has_redirect_location:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "Redirect location: '{0.headers[location]}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
        else:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
    
        status_class = self.status_code // 100
        error_types = {
            1: "Informational response",
            3: "Redirect response",
            4: "Client error",
            5: "Server error",
        }
        error_type = error_types.get(status_class, "Invalid status code")
        message = message.format(self, error_type=error_type)
>       raise HTTPStatusError(message, request=request, response=self)
E       httpx.HTTPStatusError: Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/issuer/credentials'
E       For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422

/usr/local/lib/python3.12/site-packages/httpx/_models.py:763: HTTPStatusError

The above exception was the direct cause of the following exception:

faber_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7fb386b27c50>
credential_definition_id = '2AszGVdGiN6A2sa6AQPGD5:3:CL:245:tag'
faber_and_alice_connection = FaberAliceConnect(alice_connection_id='058f4a74-e533-4aca-916d-5bbe40dbca25', faber_connection_id='2a2eb3bb-1d25-4ea1-aad0-b5b04b40e84f')
alice_member_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7fb385f7d370>
save_exchange_record = None

    @pytest.mark.anyio
    @pytest.mark.parametrize("save_exchange_record", [None, False, True])
    async def test_issue_credential_with_save_exchange_record(
        faber_client: RichAsyncClient,
        credential_definition_id: str,
        faber_and_alice_connection: FaberAliceConnect,
        alice_member_client: RichAsyncClient,
        save_exchange_record: Optional[bool],
    ) -> CredentialExchange:
        credential = {
            "connection_id": faber_and_alice_connection.faber_connection_id,
            "indy_credential_detail": {
                "credential_definition_id": credential_definition_id,
                "attributes": sample_credential_attributes,
            },
            "save_exchange_record": save_exchange_record,
        }
    
        # create and send credential offer- issuer
        faber_send_response = (
>           await faber_client.post(
                CREDENTIALS_BASE_PATH,
                json=credential,
            )
        ).json()

app/tests/e2e/issuer/test_save_exchange_record.py:38: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
shared/util/rich_async_client.py:81: in post
    return await self._request_with_retries("post", url, **kwargs)
shared/util/rich_async_client.py:78: in _request_with_retries
    await self._handle_error(e, url, method)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <shared.util.rich_async_client.RichAsyncClient object at 0x7fb386b27c50>
e = HTTPStatusError("Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/issuer/credentials'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422")
url = '/v1/issuer/credentials', method = 'post'

    async def _handle_error(self, e: HTTPStatusError, url: str, method: str) -> None:
        code = e.response.status_code
        message = e.response.text
        log_message = (
            f"{self.name} {method} `{url}` failed. "
            f"Status code: {code}. Response: `{message}`."
        )
        logger.error(log_message)
>       raise HTTPException(status_code=code, detail=message) from e
E       fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}

shared/util/rich_async_client.py:61: HTTPException
CREDENTIALS_BASE_PATH,
json=credential,
)
Expand Down
9 changes: 5 additions & 4 deletions app/tests/e2e/verifier/test_verifier.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio

Check failure on line 1 in app/tests/e2e/verifier/test_verifier.py

View workflow job for this annotation

GitHub Actions / JUnit Test Report Regression

test_verifier.test_regression_proof_valid_credential[reuse-reuse-reuse-reuse-reuse-reuse]

failed on setup with "AssertionError: Fixture is being recreated (regression tests configured to fail on recreating)"
Raw output
anyio_backend = 'asyncio'
request = <SubRequest 'get_or_issue_regression_cred_valid' for <Function test_regression_proof_valid_credential[reuse-reuse-reuse-reuse-reuse-reuse]>>
args = ()
kwargs = {'alice_member_client': <shared.util.rich_async_client.RichAsyncClient object at 0x7f9429b4a4b0>, 'credential_definiti...391-46a9-b93c-7ee276f5149f'), 'faber_client': <shared.util.rich_async_client.RichAsyncClient object at 0x7f9429f4c890>}
local_func = <function get_or_issue_regression_cred_valid at 0x7f942d2a8180>
backend_name = 'asyncio', backend_options = {}
runner = <anyio._backends._asyncio.TestRunner object at 0x7f942ccc4680>

    def wrapper(
        *args: Any, anyio_backend: Any, request: SubRequest, **kwargs: Any
    ) -> Any:
        # Rebind any fixture methods to the request instance
        if (
            request.instance
            and ismethod(func)
            and type(func.__self__) is type(request.instance)
        ):
            local_func = func.__func__.__get__(request.instance)
        else:
            local_func = func
    
        backend_name, backend_options = extract_backend_and_options(anyio_backend)
        if has_backend_arg:
            kwargs["anyio_backend"] = anyio_backend
    
        if has_request_arg:
            kwargs["request"] = request
    
        with get_runner(backend_name, backend_options) as runner:
            if isasyncgenfunction(local_func):
                yield from runner.run_asyncgen_fixture(local_func, kwargs)
            else:
>               yield runner.run_fixture(local_func, kwargs)

/usr/local/lib/python3.12/site-packages/anyio/pytest_plugin.py:100: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/usr/local/lib/python3.12/site-packages/anyio/_backends/_asyncio.py:2237: in run_fixture
    retval = self.get_loop().run_until_complete(
/usr/local/lib/python3.12/asyncio/base_events.py:687: in run_until_complete
    return future.result()
/usr/local/lib/python3.12/site-packages/anyio/_backends/_asyncio.py:2207: in _call_in_runner_task
    return await future
/usr/local/lib/python3.12/site-packages/anyio/_backends/_asyncio.py:2174: in _run_tests_and_fixtures
    retval = await coro
app/tests/fixtures/credentials.py:381: in get_or_issue_regression_cred_valid
    assert_fail_on_recreating_fixtures()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

    def assert_fail_on_recreating_fixtures():
        assert (
>           RegressionTestConfig.fail_on_recreating_fixtures is False
        ), "Fixture is being recreated (regression tests configured to fail on recreating)"
E       AssertionError: Fixture is being recreated (regression tests configured to fail on recreating)

app/tests/util/regression_testing.py:34: AssertionError
import json
import time
from typing import Optional

import pytest
from aries_cloudcontroller import IndyPresSpec, IndyRequestedCredsRequestedAttr
Expand Down Expand Up @@ -129,7 +130,7 @@
),
)

response = await alice_member_client.post(

Check failure on line 133 in app/tests/e2e/verifier/test_verifier.py

View workflow job for this annotation

GitHub Actions / JUnit Test Report

test_verifier.test_accept_proof_request[clean-clean-clean-clean-clean-clean-trust_registry]

fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}
Raw output
self = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203938a40>
method = 'post', url = '/v1/verifier/accept-request'
kwargs = {'json': {'dif_presentation_spec': None, 'indy_presentation_spec': {'requested_attributes': {'0_speed_uuid': {'cred_id...ibutes': {}, 'trace': None}, 'proof_id': 'v2-206e7e91-aded-4089-a6fc-843a9dc77e0e', 'save_exchange_record': None, ...}}
attempt = 0, response = <Response [422 Unprocessable Entity]>, code = 422

    async def _request_with_retries(self, method: str, url: str, **kwargs) -> Response:
        for attempt in range(self.retries):
            try:
                response = await getattr(super(), method)(url, **kwargs)
>               return await self._handle_response(response)

shared/util/rich_async_client.py:67: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
shared/util/rich_async_client.py:50: in _handle_response
    response.raise_for_status()  # Raise exception for 4xx and 5xx status codes
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Response [422 Unprocessable Entity]>

    def raise_for_status(self) -> Response:
        """
        Raise the `HTTPStatusError` if one occurred.
        """
        request = self._request
        if request is None:
            raise RuntimeError(
                "Cannot call `raise_for_status` as the request "
                "instance has not been set on this response."
            )
    
        if self.is_success:
            return self
    
        if self.has_redirect_location:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "Redirect location: '{0.headers[location]}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
        else:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
    
        status_class = self.status_code // 100
        error_types = {
            1: "Informational response",
            3: "Redirect response",
            4: "Client error",
            5: "Server error",
        }
        error_type = error_types.get(status_class, "Invalid status code")
        message = message.format(self, error_type=error_type)
>       raise HTTPStatusError(message, request=request, response=self)
E       httpx.HTTPStatusError: Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/verifier/accept-request'
E       For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422

/usr/local/lib/python3.12/site-packages/httpx/_models.py:763: HTTPStatusError

The above exception was the direct cause of the following exception:

issue_credential_to_alice = {'attributes': {'age': '44', 'name': 'Alice', 'speed': '10'}, 'connection_id': 'd29f3784-1177-425c-b27d-974931bd5cbf', 'created_at': '2024-11-22T17:41:12.836406Z', 'credential_definition_id': '5pAv9UW8haJsdJaDTfuCTN:3:CL:216:tag', ...}
alice_member_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203938a40>
acme_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203d5eff0>
credential_definition_id = '5pAv9UW8haJsdJaDTfuCTN:3:CL:216:tag'
acme_and_alice_connection = AcmeAliceConnect(alice_connection_id='19651091-891f-4949-878d-8dd921ea3c75', acme_connection_id='4f1d7509-a5e8-4500-99f7-57aef256ac60')

    @pytest.mark.anyio
    @pytest.mark.parametrize(
        "acme_and_alice_connection", ["trust_registry", "default"], indirect=True
    )
    async def test_accept_proof_request(
        issue_credential_to_alice: CredentialExchange,  # pylint: disable=unused-argument
        alice_member_client: RichAsyncClient,
        acme_client: RichAsyncClient,
        credential_definition_id: str,
        acme_and_alice_connection: AcmeAliceConnect,
    ):
        request_body = {
            "connection_id": acme_and_alice_connection.acme_connection_id,
            "indy_proof_request": {
                "name": "Proof Request",
                "version": "1.0.0",
                "requested_attributes": {
                    "0_speed_uuid": {
                        "name": "speed",
                        "restrictions": [{"cred_def_id": credential_definition_id}],
                    }
                },
                "requested_predicates": {},
            },
        }
        send_proof_response = await send_proof_request(acme_client, request_body)
    
        acme_proof_id = send_proof_response["proof_id"]
        thread_id = send_proof_response["thread_id"]
    
        alice_payload = await check_webhook_state(
            client=alice_member_client,
            topic="proofs",
            state="request-received",
            filter_map={
                "thread_id": thread_id,
            },
        )
    
        alice_proof_id = alice_payload["proof_id"]
    
        requested_credentials = await alice_member_client.get(
            f"{VERIFIER_BASE_PATH}/proofs/{alice_proof_id}/credentials"
        )
    
        referent = requested_credentials.json()[0]["cred_info"]["referent"]
        indy_request_attrs = IndyRequestedCredsRequestedAttr(
            cred_id=referent, revealed=True
        )
        proof_accept = AcceptProofRequest(
            proof_id=alice_proof_id,
            indy_presentation_spec=IndyPresSpec(
                requested_attributes={"0_speed_uuid": indy_request_attrs},
                requested_predicates={},
                self_attested_attributes={},
            ),
        )
    
>       response = await alice_member_client.post(
            VERIFIER_BASE_PATH + "/accept-request",
            json=proof_accept.model_dump(),
        )

app/tests/e2e/verifier/test_verifier.py:133: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
shared/util/rich_async_client.py:81: in post
    return await self._request_with_retries("post", url, **kwargs)
shared/util/rich_async_client.py:78: in _request_with_retries
    await self._handle_error(e, url, method)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203938a40>
e = HTTPStatusError("Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/verifier/accept-request'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422")
url = '/v1/verifier/accept-request', method = 'post'

    async def _handle_error(self, e: HTTPStatusError, url: str, method: str) -> None:
        code = e.response.status_code
        message = e.response.text
        log_message = (
            f"{self.name} {method} `{url}` failed. "
            f"Status code: {code}. Response: `{message}`."
        )
        logger.error(log_message)
>       raise HTTPException(status_code=code, detail=message) from e
E       fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}

shared/util/rich_async_client.py:61: HTTPException

Check failure on line 133 in app/tests/e2e/verifier/test_verifier.py

View workflow job for this annotation

GitHub Actions / JUnit Test Report

test_verifier.test_accept_proof_request[clean-clean-clean-clean-clean-clean-default]

fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}
Raw output
self = <shared.util.rich_async_client.RichAsyncClient object at 0x7f72039387a0>
method = 'post', url = '/v1/verifier/accept-request'
kwargs = {'json': {'dif_presentation_spec': None, 'indy_presentation_spec': {'requested_attributes': {'0_speed_uuid': {'cred_id...ibutes': {}, 'trace': None}, 'proof_id': 'v2-f4edd59c-a1b4-4151-b13c-0a5bfd9123fc', 'save_exchange_record': None, ...}}
attempt = 0, response = <Response [422 Unprocessable Entity]>, code = 422

    async def _request_with_retries(self, method: str, url: str, **kwargs) -> Response:
        for attempt in range(self.retries):
            try:
                response = await getattr(super(), method)(url, **kwargs)
>               return await self._handle_response(response)

shared/util/rich_async_client.py:67: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
shared/util/rich_async_client.py:50: in _handle_response
    response.raise_for_status()  # Raise exception for 4xx and 5xx status codes
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Response [422 Unprocessable Entity]>

    def raise_for_status(self) -> Response:
        """
        Raise the `HTTPStatusError` if one occurred.
        """
        request = self._request
        if request is None:
            raise RuntimeError(
                "Cannot call `raise_for_status` as the request "
                "instance has not been set on this response."
            )
    
        if self.is_success:
            return self
    
        if self.has_redirect_location:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "Redirect location: '{0.headers[location]}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
        else:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
    
        status_class = self.status_code // 100
        error_types = {
            1: "Informational response",
            3: "Redirect response",
            4: "Client error",
            5: "Server error",
        }
        error_type = error_types.get(status_class, "Invalid status code")
        message = message.format(self, error_type=error_type)
>       raise HTTPStatusError(message, request=request, response=self)
E       httpx.HTTPStatusError: Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/verifier/accept-request'
E       For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422

/usr/local/lib/python3.12/site-packages/httpx/_models.py:763: HTTPStatusError

The above exception was the direct cause of the following exception:

issue_credential_to_alice = {'attributes': {'age': '44', 'name': 'Alice', 'speed': '10'}, 'connection_id': '65e0d1fa-b1a0-4bcb-8605-d5106a92b76b', 'created_at': '2024-11-22T17:41:20.248173Z', 'credential_definition_id': '5pAv9UW8haJsdJaDTfuCTN:3:CL:216:tag', ...}
alice_member_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7f72039387a0>
acme_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203d5eff0>
credential_definition_id = '5pAv9UW8haJsdJaDTfuCTN:3:CL:216:tag'
acme_and_alice_connection = AcmeAliceConnect(alice_connection_id='cee08c62-d6f3-4bf4-82db-aee261e0156c', acme_connection_id='818c5eea-5563-44d7-bbaf-737c63a34805')

    @pytest.mark.anyio
    @pytest.mark.parametrize(
        "acme_and_alice_connection", ["trust_registry", "default"], indirect=True
    )
    async def test_accept_proof_request(
        issue_credential_to_alice: CredentialExchange,  # pylint: disable=unused-argument
        alice_member_client: RichAsyncClient,
        acme_client: RichAsyncClient,
        credential_definition_id: str,
        acme_and_alice_connection: AcmeAliceConnect,
    ):
        request_body = {
            "connection_id": acme_and_alice_connection.acme_connection_id,
            "indy_proof_request": {
                "name": "Proof Request",
                "version": "1.0.0",
                "requested_attributes": {
                    "0_speed_uuid": {
                        "name": "speed",
                        "restrictions": [{"cred_def_id": credential_definition_id}],
                    }
                },
                "requested_predicates": {},
            },
        }
        send_proof_response = await send_proof_request(acme_client, request_body)
    
        acme_proof_id = send_proof_response["proof_id"]
        thread_id = send_proof_response["thread_id"]
    
        alice_payload = await check_webhook_state(
            client=alice_member_client,
            topic="proofs",
            state="request-received",
            filter_map={
                "thread_id": thread_id,
            },
        )
    
        alice_proof_id = alice_payload["proof_id"]
    
        requested_credentials = await alice_member_client.get(
            f"{VERIFIER_BASE_PATH}/proofs/{alice_proof_id}/credentials"
        )
    
        referent = requested_credentials.json()[0]["cred_info"]["referent"]
        indy_request_attrs = IndyRequestedCredsRequestedAttr(
            cred_id=referent, revealed=True
        )
        proof_accept = AcceptProofRequest(
            proof_id=alice_proof_id,
            indy_presentation_spec=IndyPresSpec(
                requested_attributes={"0_speed_uuid": indy_request_attrs},
                requested_predicates={},
                self_attested_attributes={},
            ),
        )
    
>       response = await alice_member_client.post(
            VERIFIER_BASE_PATH + "/accept-request",
            json=proof_accept.model_dump(),
        )

app/tests/e2e/verifier/test_verifier.py:133: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
shared/util/rich_async_client.py:81: in post
    return await self._request_with_retries("post", url, **kwargs)
shared/util/rich_async_client.py:78: in _request_with_retries
    await self._handle_error(e, url, method)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <shared.util.rich_async_client.RichAsyncClient object at 0x7f72039387a0>
e = HTTPStatusError("Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/verifier/accept-request'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422")
url = '/v1/verifier/accept-request', method = 'post'

    async def _handle_error(self, e: HTTPStatusError, url: str, method: str) -> None:
        code = e.response.status_code
        message = e.response.text
        log_message = (
            f"{self.name} {method} `{url}` failed. "
            f"Status code: {code}. Response: `{message}`."
        )
        logger.error(log_message)
>       raise HTTPException(status_code=code, detail=message) from e
E       fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}

shared/util/rich_async_client.py:61: HTTPException
VERIFIER_BASE_PATH + "/accept-request",
json=proof_accept.model_dump(),
)
Expand Down Expand Up @@ -298,7 +299,7 @@
)

# Alice accepts
await alice_member_client.post(

Check failure on line 302 in app/tests/e2e/verifier/test_verifier.py

View workflow job for this annotation

GitHub Actions / JUnit Test Report

test_verifier.test_get_proof_and_get_proofs[clean-clean-clean-clean-clean-clean]

fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}
Raw output
self = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203938cb0>
method = 'post', url = '/v1/verifier/accept-request'
kwargs = {'json': {'dif_presentation_spec': None, 'indy_presentation_spec': {'requested_attributes': {'0_speed_uuid': {'cred_id...ibutes': {}, 'trace': None}, 'proof_id': 'v2-744259b2-facb-47ad-af36-89dde35fea17', 'save_exchange_record': None, ...}}
attempt = 0, response = <Response [422 Unprocessable Entity]>, code = 422

    async def _request_with_retries(self, method: str, url: str, **kwargs) -> Response:
        for attempt in range(self.retries):
            try:
                response = await getattr(super(), method)(url, **kwargs)
>               return await self._handle_response(response)

shared/util/rich_async_client.py:67: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
shared/util/rich_async_client.py:50: in _handle_response
    response.raise_for_status()  # Raise exception for 4xx and 5xx status codes
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Response [422 Unprocessable Entity]>

    def raise_for_status(self) -> Response:
        """
        Raise the `HTTPStatusError` if one occurred.
        """
        request = self._request
        if request is None:
            raise RuntimeError(
                "Cannot call `raise_for_status` as the request "
                "instance has not been set on this response."
            )
    
        if self.is_success:
            return self
    
        if self.has_redirect_location:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "Redirect location: '{0.headers[location]}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
        else:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
    
        status_class = self.status_code // 100
        error_types = {
            1: "Informational response",
            3: "Redirect response",
            4: "Client error",
            5: "Server error",
        }
        error_type = error_types.get(status_class, "Invalid status code")
        message = message.format(self, error_type=error_type)
>       raise HTTPStatusError(message, request=request, response=self)
E       httpx.HTTPStatusError: Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/verifier/accept-request'
E       For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422

/usr/local/lib/python3.12/site-packages/httpx/_models.py:763: HTTPStatusError

The above exception was the direct cause of the following exception:

acme_and_alice_connection = AcmeAliceConnect(alice_connection_id='e9ac6a68-7d7e-4965-8a07-21e8bc41721f', acme_connection_id='d5b67050-51b2-4014-afac-90bcb8bfe065')
issue_credential_to_alice = {'attributes': {'age': '44', 'name': 'Alice', 'speed': '10'}, 'connection_id': '62193549-69db-4f72-b652-39cf15905cd9', 'created_at': '2024-11-22T17:41:28.549191Z', 'credential_definition_id': '5pAv9UW8haJsdJaDTfuCTN:3:CL:216:tag', ...}
credential_definition_id = '5pAv9UW8haJsdJaDTfuCTN:3:CL:216:tag'
acme_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203d5eff0>
alice_member_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203938cb0>

    @pytest.mark.anyio
    async def test_get_proof_and_get_proofs(
        acme_and_alice_connection: AcmeAliceConnect,
        issue_credential_to_alice: CredentialExchange,  # pylint: disable=unused-argument
        credential_definition_id: str,
        acme_client: RichAsyncClient,
        alice_member_client: RichAsyncClient,
    ):
        acme_connection_id = acme_and_alice_connection.acme_connection_id
    
        request_body = {
            "save_exchange_record": True,
            "connection_id": acme_connection_id,
            "indy_proof_request": sample_indy_proof_request(
                restrictions=[{"cred_def_id": credential_definition_id}]
            ).to_dict(),
        }
        send_proof_response = await send_proof_request(acme_client, request_body)
    
        # Assert that getting single proof record works
        acme_proof_id = send_proof_response["proof_id"]
        thread_id = send_proof_response["thread_id"]
    
        response = await acme_client.get(
            f"{VERIFIER_BASE_PATH}/proofs/{acme_proof_id}",
        )
        result = response.json()
        assert "connection_id" in result
        assert "created_at" in result
        assert "updated_at" in result
        assert "presentation" in result
        assert "presentation_request" in result
    
        await asyncio.sleep(0.3)  # allow moment for alice records to update
    
        # Fetch proofs for alice
        alice_payload = await check_webhook_state(
            client=alice_member_client,
            topic="proofs",
            state="request-received",
            filter_map={
                "thread_id": thread_id,
            },
        )
        alice_proof_id = alice_payload["proof_id"]
    
        # Get credential referent for alice to accept request
        referent = (
            await alice_member_client.get(
                f"{VERIFIER_BASE_PATH}/proofs/{alice_proof_id}/credentials"
            )
        ).json()[0]["cred_info"]["referent"]
    
        indy_request_attrs = IndyRequestedCredsRequestedAttr(
            cred_id=referent, revealed=True
        )
    
        proof_accept = AcceptProofRequest(
            proof_id=alice_proof_id,
            indy_presentation_spec=IndyPresSpec(
                requested_attributes={"0_speed_uuid": indy_request_attrs},
                requested_predicates={},
                self_attested_attributes={},
            ),
        )
    
        # Alice accepts
>       await alice_member_client.post(
            VERIFIER_BASE_PATH + "/accept-request",
            json=proof_accept.model_dump(),
        )

app/tests/e2e/verifier/test_verifier.py:302: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
shared/util/rich_async_client.py:81: in post
    return await self._request_with_retries("post", url, **kwargs)
shared/util/rich_async_client.py:78: in _request_with_retries
    await self._handle_error(e, url, method)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203938cb0>
e = HTTPStatusError("Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/verifier/accept-request'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422")
url = '/v1/verifier/accept-request', method = 'post'

    async def _handle_error(self, e: HTTPStatusError, url: str, method: str) -> None:
        code = e.response.status_code
        message = e.response.text
        log_message = (
            f"{self.name} {method} `{url}` failed. "
            f"Status code: {code}. Response: `{message}`."
        )
        logger.error(log_message)
>       raise HTTPException(status_code=code, detail=message) from e
E       fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}

shared/util/rich_async_client.py:61: HTTPException
VERIFIER_BASE_PATH + "/accept-request",
json=proof_accept.model_dump(),
)
Expand Down Expand Up @@ -525,7 +526,7 @@
),
)

response = await alice_member_client.post(

Check failure on line 529 in app/tests/e2e/verifier/test_verifier.py

View workflow job for this annotation

GitHub Actions / JUnit Test Report

test_verifier.test_accept_proof_request_verifier_has_issuer_role[clean-clean-clean-clean-clean-trust_registry]

fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}
Raw output
self = <shared.util.rich_async_client.RichAsyncClient object at 0x7f72041f4200>
method = 'post', url = '/v1/verifier/accept-request'
kwargs = {'json': {'dif_presentation_spec': None, 'indy_presentation_spec': {'requested_attributes': {'0_speed_uuid': {'cred_id...ibutes': {}, 'trace': None}, 'proof_id': 'v2-1a9ded88-7396-4f53-a0ad-6f331143c3ef', 'save_exchange_record': None, ...}}
attempt = 0, response = <Response [422 Unprocessable Entity]>, code = 422

    async def _request_with_retries(self, method: str, url: str, **kwargs) -> Response:
        for attempt in range(self.retries):
            try:
                response = await getattr(super(), method)(url, **kwargs)
>               return await self._handle_response(response)

shared/util/rich_async_client.py:67: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
shared/util/rich_async_client.py:50: in _handle_response
    response.raise_for_status()  # Raise exception for 4xx and 5xx status codes
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Response [422 Unprocessable Entity]>

    def raise_for_status(self) -> Response:
        """
        Raise the `HTTPStatusError` if one occurred.
        """
        request = self._request
        if request is None:
            raise RuntimeError(
                "Cannot call `raise_for_status` as the request "
                "instance has not been set on this response."
            )
    
        if self.is_success:
            return self
    
        if self.has_redirect_location:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "Redirect location: '{0.headers[location]}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
        else:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
    
        status_class = self.status_code // 100
        error_types = {
            1: "Informational response",
            3: "Redirect response",
            4: "Client error",
            5: "Server error",
        }
        error_type = error_types.get(status_class, "Invalid status code")
        message = message.format(self, error_type=error_type)
>       raise HTTPStatusError(message, request=request, response=self)
E       httpx.HTTPStatusError: Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/verifier/accept-request'
E       For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422

/usr/local/lib/python3.12/site-packages/httpx/_models.py:763: HTTPStatusError

The above exception was the direct cause of the following exception:

meld_co_issue_credential_to_alice = {'attributes': {'age': '44', 'name': 'Alice', 'speed': '10'}, 'connection_id': 'fb87b3ab-e3f6-461f-a959-6921e817aa9a', 'created_at': '2024-11-22T17:40:39.958981Z', 'credential_definition_id': 'Vb8URxnLAs2WinBmxywSBW:3:CL:216:tag', ...}
meld_co_credential_definition_id = 'Vb8URxnLAs2WinBmxywSBW:3:CL:216:tag'
alice_member_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7f72041f4200>
meld_co_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7f72041afb30>
meld_co_and_alice_connection = MeldCoAliceConnect(alice_connection_id='fb87b3ab-e3f6-461f-a959-6921e817aa9a', meld_co_connection_id='e67aef05-802b-48b5-ad9e-12c47f7b618f')

    @pytest.mark.anyio
    @pytest.mark.parametrize(
        "meld_co_and_alice_connection", ["trust_registry", "default"], indirect=True
    )
    async def test_accept_proof_request_verifier_has_issuer_role(
        meld_co_issue_credential_to_alice: CredentialExchange,  # pylint: disable=unused-argument
        meld_co_credential_definition_id: str,
        alice_member_client: RichAsyncClient,
        meld_co_client: RichAsyncClient,
        meld_co_and_alice_connection: MeldCoAliceConnect,
    ):
        request_body = {
            "connection_id": meld_co_and_alice_connection.meld_co_connection_id,
            "indy_proof_request": sample_indy_proof_request(
                restrictions=[{"cred_def_id": meld_co_credential_definition_id}]
            ).to_dict(),
        }
        send_proof_response = await send_proof_request(meld_co_client, request_body)
    
        meld_co_proof_id = send_proof_response["proof_id"]
        thread_id = send_proof_response["thread_id"]
    
        alice_payload = await check_webhook_state(
            client=alice_member_client,
            topic="proofs",
            state="request-received",
            filter_map={"thread_id": thread_id},
        )
        alice_proof_id = alice_payload["proof_id"]
    
        requested_credentials = await alice_member_client.get(
            f"{VERIFIER_BASE_PATH}/proofs/{alice_proof_id}/credentials"
        )
    
        assert await check_webhook_state(
            client=alice_member_client,
            topic="proofs",
            state="request-received",
            filter_map={
                "proof_id": alice_proof_id,
            },
        )
    
        referent = requested_credentials.json()[0]["cred_info"]["referent"]
        indy_request_attrs = IndyRequestedCredsRequestedAttr(
            cred_id=referent, revealed=True
        )
    
        proof_accept = AcceptProofRequest(
            proof_id=alice_proof_id,
            indy_presentation_spec=IndyPresSpec(
                requested_attributes={"0_speed_uuid": indy_request_attrs},
                requested_predicates={},
                self_attested_attributes={},
            ),
        )
    
>       response = await alice_member_client.post(
            VERIFIER_BASE_PATH + "/accept-request",
            json=proof_accept.model_dump(),
        )

app/tests/e2e/verifier/test_verifier.py:529: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
shared/util/rich_async_client.py:81: in post
    return await self._request_with_retries("post", url, **kwargs)
shared/util/rich_async_client.py:78: in _request_with_retries
    await self._handle_error(e, url, method)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <shared.util.rich_async_client.RichAsyncClient object at 0x7f72041f4200>
e = HTTPStatusError("Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/verifier/accept-request'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422")
url = '/v1/verifier/accept-request', method = 'post'

    async def _handle_error(self, e: HTTPStatusError, url: str, method: str) -> None:
        code = e.response.status_code
        message = e.response.text
        log_message = (
            f"{self.name} {method} `{url}` failed. "
            f"Status code: {code}. Response: `{message}`."
        )
        logger.error(log_message)
>       raise HTTPException(status_code=code, detail=message) from e
E       fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}

shared/util/rich_async_client.py:61: HTTPException

Check failure on line 529 in app/tests/e2e/verifier/test_verifier.py

View workflow job for this annotation

GitHub Actions / JUnit Test Report

test_verifier.test_accept_proof_request_verifier_has_issuer_role[clean-clean-clean-clean-clean-default]

fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}
Raw output
self = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203d94d10>
method = 'post', url = '/v1/verifier/accept-request'
kwargs = {'json': {'dif_presentation_spec': None, 'indy_presentation_spec': {'requested_attributes': {'0_speed_uuid': {'cred_id...ibutes': {}, 'trace': None}, 'proof_id': 'v2-17cf68cd-5ee7-4a7d-bc71-b4471cdd56ab', 'save_exchange_record': None, ...}}
attempt = 0, response = <Response [422 Unprocessable Entity]>, code = 422

    async def _request_with_retries(self, method: str, url: str, **kwargs) -> Response:
        for attempt in range(self.retries):
            try:
                response = await getattr(super(), method)(url, **kwargs)
>               return await self._handle_response(response)

shared/util/rich_async_client.py:67: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
shared/util/rich_async_client.py:50: in _handle_response
    response.raise_for_status()  # Raise exception for 4xx and 5xx status codes
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Response [422 Unprocessable Entity]>

    def raise_for_status(self) -> Response:
        """
        Raise the `HTTPStatusError` if one occurred.
        """
        request = self._request
        if request is None:
            raise RuntimeError(
                "Cannot call `raise_for_status` as the request "
                "instance has not been set on this response."
            )
    
        if self.is_success:
            return self
    
        if self.has_redirect_location:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "Redirect location: '{0.headers[location]}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
        else:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
    
        status_class = self.status_code // 100
        error_types = {
            1: "Informational response",
            3: "Redirect response",
            4: "Client error",
            5: "Server error",
        }
        error_type = error_types.get(status_class, "Invalid status code")
        message = message.format(self, error_type=error_type)
>       raise HTTPStatusError(message, request=request, response=self)
E       httpx.HTTPStatusError: Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/verifier/accept-request'
E       For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422

/usr/local/lib/python3.12/site-packages/httpx/_models.py:763: HTTPStatusError

The above exception was the direct cause of the following exception:

meld_co_issue_credential_to_alice = {'attributes': {'age': '44', 'name': 'Alice', 'speed': '10'}, 'connection_id': 'd0611fc9-98b0-47c9-9762-a33a89902888', 'created_at': '2024-11-22T17:40:46.384371Z', 'credential_definition_id': 'Vb8URxnLAs2WinBmxywSBW:3:CL:216:tag', ...}
meld_co_credential_definition_id = 'Vb8URxnLAs2WinBmxywSBW:3:CL:216:tag'
alice_member_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203d94d10>
meld_co_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7f72041afb30>
meld_co_and_alice_connection = MeldCoAliceConnect(alice_connection_id='d0611fc9-98b0-47c9-9762-a33a89902888', meld_co_connection_id='222ed84a-14f3-4c7d-8d98-4d70455382a9')

    @pytest.mark.anyio
    @pytest.mark.parametrize(
        "meld_co_and_alice_connection", ["trust_registry", "default"], indirect=True
    )
    async def test_accept_proof_request_verifier_has_issuer_role(
        meld_co_issue_credential_to_alice: CredentialExchange,  # pylint: disable=unused-argument
        meld_co_credential_definition_id: str,
        alice_member_client: RichAsyncClient,
        meld_co_client: RichAsyncClient,
        meld_co_and_alice_connection: MeldCoAliceConnect,
    ):
        request_body = {
            "connection_id": meld_co_and_alice_connection.meld_co_connection_id,
            "indy_proof_request": sample_indy_proof_request(
                restrictions=[{"cred_def_id": meld_co_credential_definition_id}]
            ).to_dict(),
        }
        send_proof_response = await send_proof_request(meld_co_client, request_body)
    
        meld_co_proof_id = send_proof_response["proof_id"]
        thread_id = send_proof_response["thread_id"]
    
        alice_payload = await check_webhook_state(
            client=alice_member_client,
            topic="proofs",
            state="request-received",
            filter_map={"thread_id": thread_id},
        )
        alice_proof_id = alice_payload["proof_id"]
    
        requested_credentials = await alice_member_client.get(
            f"{VERIFIER_BASE_PATH}/proofs/{alice_proof_id}/credentials"
        )
    
        assert await check_webhook_state(
            client=alice_member_client,
            topic="proofs",
            state="request-received",
            filter_map={
                "proof_id": alice_proof_id,
            },
        )
    
        referent = requested_credentials.json()[0]["cred_info"]["referent"]
        indy_request_attrs = IndyRequestedCredsRequestedAttr(
            cred_id=referent, revealed=True
        )
    
        proof_accept = AcceptProofRequest(
            proof_id=alice_proof_id,
            indy_presentation_spec=IndyPresSpec(
                requested_attributes={"0_speed_uuid": indy_request_attrs},
                requested_predicates={},
                self_attested_attributes={},
            ),
        )
    
>       response = await alice_member_client.post(
            VERIFIER_BASE_PATH + "/accept-request",
            json=proof_accept.model_dump(),
        )

app/tests/e2e/verifier/test_verifier.py:529: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
shared/util/rich_async_client.py:81: in post
    return await self._request_with_retries("post", url, **kwargs)
shared/util/rich_async_client.py:78: in _request_with_retries
    await self._handle_error(e, url, method)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203d94d10>
e = HTTPStatusError("Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/verifier/accept-request'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422")
url = '/v1/verifier/accept-request', method = 'post'

    async def _handle_error(self, e: HTTPStatusError, url: str, method: str) -> None:
        code = e.response.status_code
        message = e.response.text
        log_message = (
            f"{self.name} {method} `{url}` failed. "
            f"Status code: {code}. Response: `{message}`."
        )
        logger.error(log_message)
>       raise HTTPException(status_code=code, detail=message) from e
E       fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}

shared/util/rich_async_client.py:61: HTTPException
VERIFIER_BASE_PATH + "/accept-request",
json=proof_accept.model_dump(),
)
Expand All @@ -544,16 +545,16 @@


@pytest.mark.anyio
@pytest.mark.parametrize("acme_save_exchange_record", [False, True])
@pytest.mark.parametrize("alice_save_exchange_record", [False, True])
@pytest.mark.parametrize("acme_save_exchange_record", [None, False, True])
@pytest.mark.parametrize("alice_save_exchange_record", [None, False, True])
async def test_saving_of_presentation_exchange_records(
issue_credential_to_alice: CredentialExchange, # pylint: disable=unused-argument
credential_definition_id: str,
alice_member_client: RichAsyncClient,
acme_client: RichAsyncClient,
acme_and_alice_connection: AcmeAliceConnect,
acme_save_exchange_record: bool,
alice_save_exchange_record: bool,
acme_save_exchange_record: Optional[bool],
alice_save_exchange_record: Optional[bool],
):
request_body = {
"connection_id": acme_and_alice_connection.acme_connection_id,
Expand All @@ -562,7 +563,7 @@
).to_dict(),
"save_exchange_record": acme_save_exchange_record,
}
send_proof_response = await send_proof_request(acme_client, request_body)

Check failure on line 566 in app/tests/e2e/verifier/test_verifier.py

View workflow job for this annotation

GitHub Actions / JUnit Test Report

test_verifier.test_saving_of_presentation_exchange_records[clean-clean-clean-clean-clean-clean-None-None]

fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}
Raw output
self = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203d5eff0>
method = 'post', url = '/v1/verifier/send-request'
kwargs = {'json': {'connection_id': 'bfc3a0ef-7059-4652-a273-a3ea62c42619', 'indy_proof_request': {'name': 'string', 'non_revok...quested_attributes': {'0_speed_uuid': {'name': 'speed', 'restrictions': [{...}]}}, ...}, 'save_exchange_record': None}}
attempt = 0, response = <Response [422 Unprocessable Entity]>, code = 422

    async def _request_with_retries(self, method: str, url: str, **kwargs) -> Response:
        for attempt in range(self.retries):
            try:
                response = await getattr(super(), method)(url, **kwargs)
>               return await self._handle_response(response)

shared/util/rich_async_client.py:67: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
shared/util/rich_async_client.py:50: in _handle_response
    response.raise_for_status()  # Raise exception for 4xx and 5xx status codes
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Response [422 Unprocessable Entity]>

    def raise_for_status(self) -> Response:
        """
        Raise the `HTTPStatusError` if one occurred.
        """
        request = self._request
        if request is None:
            raise RuntimeError(
                "Cannot call `raise_for_status` as the request "
                "instance has not been set on this response."
            )
    
        if self.is_success:
            return self
    
        if self.has_redirect_location:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "Redirect location: '{0.headers[location]}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
        else:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
    
        status_class = self.status_code // 100
        error_types = {
            1: "Informational response",
            3: "Redirect response",
            4: "Client error",
            5: "Server error",
        }
        error_type = error_types.get(status_class, "Invalid status code")
        message = message.format(self, error_type=error_type)
>       raise HTTPStatusError(message, request=request, response=self)
E       httpx.HTTPStatusError: Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/verifier/send-request'
E       For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422

/usr/local/lib/python3.12/site-packages/httpx/_models.py:763: HTTPStatusError

The above exception was the direct cause of the following exception:

issue_credential_to_alice = {'attributes': {'age': '44', 'name': 'Alice', 'speed': '10'}, 'connection_id': 'dbb334c2-632b-44ac-8d26-5b91dd3c2575', 'created_at': '2024-11-22T17:41:39.292442Z', 'credential_definition_id': '5pAv9UW8haJsdJaDTfuCTN:3:CL:216:tag', ...}
credential_definition_id = '5pAv9UW8haJsdJaDTfuCTN:3:CL:216:tag'
alice_member_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7f72041f7d10>
acme_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203d5eff0>
acme_and_alice_connection = AcmeAliceConnect(alice_connection_id='200ca3b1-cbd1-48c4-b364-8caa3d9ac91e', acme_connection_id='bfc3a0ef-7059-4652-a273-a3ea62c42619')
acme_save_exchange_record = None, alice_save_exchange_record = None

    @pytest.mark.anyio
    @pytest.mark.parametrize("acme_save_exchange_record", [None, False, True])
    @pytest.mark.parametrize("alice_save_exchange_record", [None, False, True])
    async def test_saving_of_presentation_exchange_records(
        issue_credential_to_alice: CredentialExchange,  # pylint: disable=unused-argument
        credential_definition_id: str,
        alice_member_client: RichAsyncClient,
        acme_client: RichAsyncClient,
        acme_and_alice_connection: AcmeAliceConnect,
        acme_save_exchange_record: Optional[bool],
        alice_save_exchange_record: Optional[bool],
    ):
        request_body = {
            "connection_id": acme_and_alice_connection.acme_connection_id,
            "indy_proof_request": sample_indy_proof_request(
                restrictions=[{"cred_def_id": credential_definition_id}]
            ).to_dict(),
            "save_exchange_record": acme_save_exchange_record,
        }
>       send_proof_response = await send_proof_request(acme_client, request_body)

app/tests/e2e/verifier/test_verifier.py:566: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
app/tests/util/verifier.py:10: in send_proof_request
    response = await client.post(
shared/util/rich_async_client.py:81: in post
    return await self._request_with_retries("post", url, **kwargs)
shared/util/rich_async_client.py:78: in _request_with_retries
    await self._handle_error(e, url, method)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203d5eff0>
e = HTTPStatusError("Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/verifier/send-request'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422")
url = '/v1/verifier/send-request', method = 'post'

    async def _handle_error(self, e: HTTPStatusError, url: str, method: str) -> None:
        code = e.response.status_code
        message = e.response.text
        log_message = (
            f"{self.name} {method} `{url}` failed. "
            f"Status code: {code}. Response: `{message}`."
        )
        logger.error(log_message)
>       raise HTTPException(status_code=code, detail=message) from e
E       fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}

shared/util/rich_async_client.py:61: HTTPException

Check failure on line 566 in app/tests/e2e/verifier/test_verifier.py

View workflow job for this annotation

GitHub Actions / JUnit Test Report

test_verifier.test_saving_of_presentation_exchange_records[clean-clean-clean-clean-clean-clean-False-None]

fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}
Raw output
self = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203d5eff0>
method = 'post', url = '/v1/verifier/send-request'
kwargs = {'json': {'connection_id': '654cb07d-16c6-4e0f-8523-2db20de78d93', 'indy_proof_request': {'name': 'string', 'non_revok...quested_attributes': {'0_speed_uuid': {'name': 'speed', 'restrictions': [{...}]}}, ...}, 'save_exchange_record': None}}
attempt = 0, response = <Response [422 Unprocessable Entity]>, code = 422

    async def _request_with_retries(self, method: str, url: str, **kwargs) -> Response:
        for attempt in range(self.retries):
            try:
                response = await getattr(super(), method)(url, **kwargs)
>               return await self._handle_response(response)

shared/util/rich_async_client.py:67: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
shared/util/rich_async_client.py:50: in _handle_response
    response.raise_for_status()  # Raise exception for 4xx and 5xx status codes
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Response [422 Unprocessable Entity]>

    def raise_for_status(self) -> Response:
        """
        Raise the `HTTPStatusError` if one occurred.
        """
        request = self._request
        if request is None:
            raise RuntimeError(
                "Cannot call `raise_for_status` as the request "
                "instance has not been set on this response."
            )
    
        if self.is_success:
            return self
    
        if self.has_redirect_location:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "Redirect location: '{0.headers[location]}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
        else:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
    
        status_class = self.status_code // 100
        error_types = {
            1: "Informational response",
            3: "Redirect response",
            4: "Client error",
            5: "Server error",
        }
        error_type = error_types.get(status_class, "Invalid status code")
        message = message.format(self, error_type=error_type)
>       raise HTTPStatusError(message, request=request, response=self)
E       httpx.HTTPStatusError: Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/verifier/send-request'
E       For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422

/usr/local/lib/python3.12/site-packages/httpx/_models.py:763: HTTPStatusError

The above exception was the direct cause of the following exception:

issue_credential_to_alice = {'attributes': {'age': '44', 'name': 'Alice', 'speed': '10'}, 'connection_id': 'b74488f2-c91b-4029-9cde-d5cc6c1fe63f', 'created_at': '2024-11-22T17:41:55.727618Z', 'credential_definition_id': '5pAv9UW8haJsdJaDTfuCTN:3:CL:216:tag', ...}
credential_definition_id = '5pAv9UW8haJsdJaDTfuCTN:3:CL:216:tag'
alice_member_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7202dd0770>
acme_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203d5eff0>
acme_and_alice_connection = AcmeAliceConnect(alice_connection_id='3e42c4af-bb95-4e05-942f-d7aadd97a9a4', acme_connection_id='654cb07d-16c6-4e0f-8523-2db20de78d93')
acme_save_exchange_record = None, alice_save_exchange_record = False

    @pytest.mark.anyio
    @pytest.mark.parametrize("acme_save_exchange_record", [None, False, True])
    @pytest.mark.parametrize("alice_save_exchange_record", [None, False, True])
    async def test_saving_of_presentation_exchange_records(
        issue_credential_to_alice: CredentialExchange,  # pylint: disable=unused-argument
        credential_definition_id: str,
        alice_member_client: RichAsyncClient,
        acme_client: RichAsyncClient,
        acme_and_alice_connection: AcmeAliceConnect,
        acme_save_exchange_record: Optional[bool],
        alice_save_exchange_record: Optional[bool],
    ):
        request_body = {
            "connection_id": acme_and_alice_connection.acme_connection_id,
            "indy_proof_request": sample_indy_proof_request(
                restrictions=[{"cred_def_id": credential_definition_id}]
            ).to_dict(),
            "save_exchange_record": acme_save_exchange_record,
        }
>       send_proof_response = await send_proof_request(acme_client, request_body)

app/tests/e2e/verifier/test_verifier.py:566: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
app/tests/util/verifier.py:10: in send_proof_request
    response = await client.post(
shared/util/rich_async_client.py:81: in post
    return await self._request_with_retries("post", url, **kwargs)
shared/util/rich_async_client.py:78: in _request_with_retries
    await self._handle_error(e, url, method)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203d5eff0>
e = HTTPStatusError("Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/verifier/send-request'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422")
url = '/v1/verifier/send-request', method = 'post'

    async def _handle_error(self, e: HTTPStatusError, url: str, method: str) -> None:
        code = e.response.status_code
        message = e.response.text
        log_message = (
            f"{self.name} {method} `{url}` failed. "
            f"Status code: {code}. Response: `{message}`."
        )
        logger.error(log_message)
>       raise HTTPException(status_code=code, detail=message) from e
E       fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}

shared/util/rich_async_client.py:61: HTTPException

Check failure on line 566 in app/tests/e2e/verifier/test_verifier.py

View workflow job for this annotation

GitHub Actions / JUnit Test Report

test_verifier.test_saving_of_presentation_exchange_records[clean-clean-clean-clean-clean-clean-True-None]

fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}
Raw output
self = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203d5eff0>
method = 'post', url = '/v1/verifier/send-request'
kwargs = {'json': {'connection_id': '1ddcdee2-514a-4f65-a565-1839cf8e3954', 'indy_proof_request': {'name': 'string', 'non_revok...quested_attributes': {'0_speed_uuid': {'name': 'speed', 'restrictions': [{...}]}}, ...}, 'save_exchange_record': None}}
attempt = 0, response = <Response [422 Unprocessable Entity]>, code = 422

    async def _request_with_retries(self, method: str, url: str, **kwargs) -> Response:
        for attempt in range(self.retries):
            try:
                response = await getattr(super(), method)(url, **kwargs)
>               return await self._handle_response(response)

shared/util/rich_async_client.py:67: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
shared/util/rich_async_client.py:50: in _handle_response
    response.raise_for_status()  # Raise exception for 4xx and 5xx status codes
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Response [422 Unprocessable Entity]>

    def raise_for_status(self) -> Response:
        """
        Raise the `HTTPStatusError` if one occurred.
        """
        request = self._request
        if request is None:
            raise RuntimeError(
                "Cannot call `raise_for_status` as the request "
                "instance has not been set on this response."
            )
    
        if self.is_success:
            return self
    
        if self.has_redirect_location:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "Redirect location: '{0.headers[location]}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
        else:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
    
        status_class = self.status_code // 100
        error_types = {
            1: "Informational response",
            3: "Redirect response",
            4: "Client error",
            5: "Server error",
        }
        error_type = error_types.get(status_class, "Invalid status code")
        message = message.format(self, error_type=error_type)
>       raise HTTPStatusError(message, request=request, response=self)
E       httpx.HTTPStatusError: Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/verifier/send-request'
E       For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422

/usr/local/lib/python3.12/site-packages/httpx/_models.py:763: HTTPStatusError

The above exception was the direct cause of the following exception:

issue_credential_to_alice = {'attributes': {'age': '44', 'name': 'Alice', 'speed': '10'}, 'connection_id': '7687bec7-ccf9-4d90-ab13-b4ccddf9d632', 'created_at': '2024-11-22T17:42:15.479790Z', 'credential_definition_id': '5pAv9UW8haJsdJaDTfuCTN:3:CL:216:tag', ...}
credential_definition_id = '5pAv9UW8haJsdJaDTfuCTN:3:CL:216:tag'
alice_member_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203916870>
acme_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203d5eff0>
acme_and_alice_connection = AcmeAliceConnect(alice_connection_id='87b68e73-28b3-4330-8dec-d3f5d1d44789', acme_connection_id='1ddcdee2-514a-4f65-a565-1839cf8e3954')
acme_save_exchange_record = None, alice_save_exchange_record = True

    @pytest.mark.anyio
    @pytest.mark.parametrize("acme_save_exchange_record", [None, False, True])
    @pytest.mark.parametrize("alice_save_exchange_record", [None, False, True])
    async def test_saving_of_presentation_exchange_records(
        issue_credential_to_alice: CredentialExchange,  # pylint: disable=unused-argument
        credential_definition_id: str,
        alice_member_client: RichAsyncClient,
        acme_client: RichAsyncClient,
        acme_and_alice_connection: AcmeAliceConnect,
        acme_save_exchange_record: Optional[bool],
        alice_save_exchange_record: Optional[bool],
    ):
        request_body = {
            "connection_id": acme_and_alice_connection.acme_connection_id,
            "indy_proof_request": sample_indy_proof_request(
                restrictions=[{"cred_def_id": credential_definition_id}]
            ).to_dict(),
            "save_exchange_record": acme_save_exchange_record,
        }
>       send_proof_response = await send_proof_request(acme_client, request_body)

app/tests/e2e/verifier/test_verifier.py:566: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
app/tests/util/verifier.py:10: in send_proof_request
    response = await client.post(
shared/util/rich_async_client.py:81: in post
    return await self._request_with_retries("post", url, **kwargs)
shared/util/rich_async_client.py:78: in _request_with_retries
    await self._handle_error(e, url, method)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203d5eff0>
e = HTTPStatusError("Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/verifier/send-request'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422")
url = '/v1/verifier/send-request', method = 'post'

    async def _handle_error(self, e: HTTPStatusError, url: str, method: str) -> None:
        code = e.response.status_code
        message = e.response.text
        log_message = (
            f"{self.name} {method} `{url}` failed. "
            f"Status code: {code}. Response: `{message}`."
        )
        logger.error(log_message)
>       raise HTTPException(status_code=code, detail=message) from e
E       fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}

shared/util/rich_async_client.py:61: HTTPException

acme_proof_id = send_proof_response["proof_id"]
thread_id = send_proof_response["thread_id"]
Expand Down Expand Up @@ -596,7 +597,7 @@
save_exchange_record=alice_save_exchange_record,
)

response = await alice_member_client.post(

Check failure on line 600 in app/tests/e2e/verifier/test_verifier.py

View workflow job for this annotation

GitHub Actions / JUnit Test Report

test_verifier.test_saving_of_presentation_exchange_records[clean-clean-clean-clean-clean-clean-None-False]

fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}
Raw output
self = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203da3c20>
method = 'post', url = '/v1/verifier/accept-request'
kwargs = {'json': {'dif_presentation_spec': None, 'indy_presentation_spec': {'requested_attributes': {'0_speed_uuid': {'cred_id...ibutes': {}, 'trace': None}, 'proof_id': 'v2-16d5bd13-d156-4711-8307-b23effa28fb1', 'save_exchange_record': None, ...}}
attempt = 0, response = <Response [422 Unprocessable Entity]>, code = 422

    async def _request_with_retries(self, method: str, url: str, **kwargs) -> Response:
        for attempt in range(self.retries):
            try:
                response = await getattr(super(), method)(url, **kwargs)
>               return await self._handle_response(response)

shared/util/rich_async_client.py:67: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
shared/util/rich_async_client.py:50: in _handle_response
    response.raise_for_status()  # Raise exception for 4xx and 5xx status codes
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Response [422 Unprocessable Entity]>

    def raise_for_status(self) -> Response:
        """
        Raise the `HTTPStatusError` if one occurred.
        """
        request = self._request
        if request is None:
            raise RuntimeError(
                "Cannot call `raise_for_status` as the request "
                "instance has not been set on this response."
            )
    
        if self.is_success:
            return self
    
        if self.has_redirect_location:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "Redirect location: '{0.headers[location]}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
        else:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
    
        status_class = self.status_code // 100
        error_types = {
            1: "Informational response",
            3: "Redirect response",
            4: "Client error",
            5: "Server error",
        }
        error_type = error_types.get(status_class, "Invalid status code")
        message = message.format(self, error_type=error_type)
>       raise HTTPStatusError(message, request=request, response=self)
E       httpx.HTTPStatusError: Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/verifier/accept-request'
E       For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422

/usr/local/lib/python3.12/site-packages/httpx/_models.py:763: HTTPStatusError

The above exception was the direct cause of the following exception:

issue_credential_to_alice = {'attributes': {'age': '44', 'name': 'Alice', 'speed': '10'}, 'connection_id': 'e768b912-999e-4f69-b5f4-0d83867a4172', 'created_at': '2024-11-22T17:41:44.426432Z', 'credential_definition_id': '5pAv9UW8haJsdJaDTfuCTN:3:CL:216:tag', ...}
credential_definition_id = '5pAv9UW8haJsdJaDTfuCTN:3:CL:216:tag'
alice_member_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203da3c20>
acme_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203d5eff0>
acme_and_alice_connection = AcmeAliceConnect(alice_connection_id='2b324149-767b-46ac-9f29-556ae0875d3d', acme_connection_id='79060d3e-423b-43d9-8791-6e928a4de16c')
acme_save_exchange_record = False, alice_save_exchange_record = None

    @pytest.mark.anyio
    @pytest.mark.parametrize("acme_save_exchange_record", [None, False, True])
    @pytest.mark.parametrize("alice_save_exchange_record", [None, False, True])
    async def test_saving_of_presentation_exchange_records(
        issue_credential_to_alice: CredentialExchange,  # pylint: disable=unused-argument
        credential_definition_id: str,
        alice_member_client: RichAsyncClient,
        acme_client: RichAsyncClient,
        acme_and_alice_connection: AcmeAliceConnect,
        acme_save_exchange_record: Optional[bool],
        alice_save_exchange_record: Optional[bool],
    ):
        request_body = {
            "connection_id": acme_and_alice_connection.acme_connection_id,
            "indy_proof_request": sample_indy_proof_request(
                restrictions=[{"cred_def_id": credential_definition_id}]
            ).to_dict(),
            "save_exchange_record": acme_save_exchange_record,
        }
        send_proof_response = await send_proof_request(acme_client, request_body)
    
        acme_proof_id = send_proof_response["proof_id"]
        thread_id = send_proof_response["thread_id"]
    
        alice_payload = await check_webhook_state(
            client=alice_member_client,
            topic="proofs",
            state="request-received",
            filter_map={
                "thread_id": thread_id,
            },
        )
        alice_proof_id = alice_payload["proof_id"]
    
        requested_credentials = await alice_member_client.get(
            f"{VERIFIER_BASE_PATH}/proofs/{alice_proof_id}/credentials"
        )
    
        referent = requested_credentials.json()[0]["cred_info"]["referent"]
        indy_request_attrs = IndyRequestedCredsRequestedAttr(
            cred_id=referent, revealed=True
        )
    
        proof_accept = AcceptProofRequest(
            proof_id=alice_proof_id,
            indy_presentation_spec=IndyPresSpec(
                requested_attributes={"0_speed_uuid": indy_request_attrs},
                requested_predicates={},
                self_attested_attributes={},
            ),
            save_exchange_record=alice_save_exchange_record,
        )
    
>       response = await alice_member_client.post(
            VERIFIER_BASE_PATH + "/accept-request",
            json=proof_accept.model_dump(),
        )

app/tests/e2e/verifier/test_verifier.py:600: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
shared/util/rich_async_client.py:81: in post
    return await self._request_with_retries("post", url, **kwargs)
shared/util/rich_async_client.py:78: in _request_with_retries
    await self._handle_error(e, url, method)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203da3c20>
e = HTTPStatusError("Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/verifier/accept-request'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422")
url = '/v1/verifier/accept-request', method = 'post'

    async def _handle_error(self, e: HTTPStatusError, url: str, method: str) -> None:
        code = e.response.status_code
        message = e.response.text
        log_message = (
            f"{self.name} {method} `{url}` failed. "
            f"Status code: {code}. Response: `{message}`."
        )
        logger.error(log_message)
>       raise HTTPException(status_code=code, detail=message) from e
E       fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}

shared/util/rich_async_client.py:61: HTTPException

Check failure on line 600 in app/tests/e2e/verifier/test_verifier.py

View workflow job for this annotation

GitHub Actions / JUnit Test Report

test_verifier.test_saving_of_presentation_exchange_records[clean-clean-clean-clean-clean-clean-None-True]

fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}
Raw output
self = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7202dece60>
method = 'post', url = '/v1/verifier/accept-request'
kwargs = {'json': {'dif_presentation_spec': None, 'indy_presentation_spec': {'requested_attributes': {'0_speed_uuid': {'cred_id...ibutes': {}, 'trace': None}, 'proof_id': 'v2-b192346a-2fd5-473a-afdd-27ec5952b8c6', 'save_exchange_record': None, ...}}
attempt = 0, response = <Response [422 Unprocessable Entity]>, code = 422

    async def _request_with_retries(self, method: str, url: str, **kwargs) -> Response:
        for attempt in range(self.retries):
            try:
                response = await getattr(super(), method)(url, **kwargs)
>               return await self._handle_response(response)

shared/util/rich_async_client.py:67: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
shared/util/rich_async_client.py:50: in _handle_response
    response.raise_for_status()  # Raise exception for 4xx and 5xx status codes
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Response [422 Unprocessable Entity]>

    def raise_for_status(self) -> Response:
        """
        Raise the `HTTPStatusError` if one occurred.
        """
        request = self._request
        if request is None:
            raise RuntimeError(
                "Cannot call `raise_for_status` as the request "
                "instance has not been set on this response."
            )
    
        if self.is_success:
            return self
    
        if self.has_redirect_location:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "Redirect location: '{0.headers[location]}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
        else:
            message = (
                "{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
                "For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
            )
    
        status_class = self.status_code // 100
        error_types = {
            1: "Informational response",
            3: "Redirect response",
            4: "Client error",
            5: "Server error",
        }
        error_type = error_types.get(status_class, "Invalid status code")
        message = message.format(self, error_type=error_type)
>       raise HTTPStatusError(message, request=request, response=self)
E       httpx.HTTPStatusError: Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/verifier/accept-request'
E       For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422

/usr/local/lib/python3.12/site-packages/httpx/_models.py:763: HTTPStatusError

The above exception was the direct cause of the following exception:

issue_credential_to_alice = {'attributes': {'age': '44', 'name': 'Alice', 'speed': '10'}, 'connection_id': '950e904d-58ef-4bcd-b77f-f8f4a6f9a3ad', 'created_at': '2024-11-22T17:41:50.047851Z', 'credential_definition_id': '5pAv9UW8haJsdJaDTfuCTN:3:CL:216:tag', ...}
credential_definition_id = '5pAv9UW8haJsdJaDTfuCTN:3:CL:216:tag'
alice_member_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7202dece60>
acme_client = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7203d5eff0>
acme_and_alice_connection = AcmeAliceConnect(alice_connection_id='8322789e-aa68-4876-8e43-2d41aa40bafe', acme_connection_id='99f28de0-5e45-4592-8791-f3351928b731')
acme_save_exchange_record = True, alice_save_exchange_record = None

    @pytest.mark.anyio
    @pytest.mark.parametrize("acme_save_exchange_record", [None, False, True])
    @pytest.mark.parametrize("alice_save_exchange_record", [None, False, True])
    async def test_saving_of_presentation_exchange_records(
        issue_credential_to_alice: CredentialExchange,  # pylint: disable=unused-argument
        credential_definition_id: str,
        alice_member_client: RichAsyncClient,
        acme_client: RichAsyncClient,
        acme_and_alice_connection: AcmeAliceConnect,
        acme_save_exchange_record: Optional[bool],
        alice_save_exchange_record: Optional[bool],
    ):
        request_body = {
            "connection_id": acme_and_alice_connection.acme_connection_id,
            "indy_proof_request": sample_indy_proof_request(
                restrictions=[{"cred_def_id": credential_definition_id}]
            ).to_dict(),
            "save_exchange_record": acme_save_exchange_record,
        }
        send_proof_response = await send_proof_request(acme_client, request_body)
    
        acme_proof_id = send_proof_response["proof_id"]
        thread_id = send_proof_response["thread_id"]
    
        alice_payload = await check_webhook_state(
            client=alice_member_client,
            topic="proofs",
            state="request-received",
            filter_map={
                "thread_id": thread_id,
            },
        )
        alice_proof_id = alice_payload["proof_id"]
    
        requested_credentials = await alice_member_client.get(
            f"{VERIFIER_BASE_PATH}/proofs/{alice_proof_id}/credentials"
        )
    
        referent = requested_credentials.json()[0]["cred_info"]["referent"]
        indy_request_attrs = IndyRequestedCredsRequestedAttr(
            cred_id=referent, revealed=True
        )
    
        proof_accept = AcceptProofRequest(
            proof_id=alice_proof_id,
            indy_presentation_spec=IndyPresSpec(
                requested_attributes={"0_speed_uuid": indy_request_attrs},
                requested_predicates={},
                self_attested_attributes={},
            ),
            save_exchange_record=alice_save_exchange_record,
        )
    
>       response = await alice_member_client.post(
            VERIFIER_BASE_PATH + "/accept-request",
            json=proof_accept.model_dump(),
        )

app/tests/e2e/verifier/test_verifier.py:600: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
shared/util/rich_async_client.py:81: in post
    return await self._request_with_retries("post", url, **kwargs)
shared/util/rich_async_client.py:78: in _request_with_retries
    await self._handle_error(e, url, method)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <shared.util.rich_async_client.RichAsyncClient object at 0x7f7202dece60>
e = HTTPStatusError("Client error '422 Unprocessable Entity' for url 'https://tenant-web.cloudapi.dev.didxtech.com/tenant/v1/verifier/accept-request'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422")
url = '/v1/verifier/accept-request', method = 'post'

    async def _handle_error(self, e: HTTPStatusError, url: str, method: str) -> None:
        code = e.response.status_code
        message = e.response.text
        log_message = (
            f"{self.name} {method} `{url}` failed. "
            f"Status code: {code}. Response: `{message}`."
        )
        logger.error(log_message)
>       raise HTTPException(status_code=code, detail=message) from e
E       fastapi.exceptions.HTTPException: 422: {"detail":[{"type":"bool_type","loc":["body","save_exchange_record"],"msg":"Input should be a valid boolean","input":null}]}

shared/util/rich_async_client.py:61: HTTPException
VERIFIER_BASE_PATH + "/accept-request",
json=proof_accept.model_dump(),
)
Expand Down
22 changes: 22 additions & 0 deletions app/util/save_exchange_record.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from typing import Optional

from pydantic import BaseModel, Field

save_exchange_record_field = Field(
default=None,
description=(
"Controls exchange record retention after exchange is complete. None uses "
"wallet default (typically to delete), true forces save, false forces delete."
),
)


class SaveExchangeRecordField(BaseModel):
save_exchange_record: Optional[bool] = save_exchange_record_field

@property
def auto_remove(self) -> Optional[bool]:
"""Returns the inverse of save_exchange_record if set, otherwise None."""
if self.save_exchange_record is None:
return None
return not self.save_exchange_record
Loading