From b5795c8f496043f9d33381b19241d553e61327c0 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 2 Aug 2023 07:58:17 -0400 Subject: [PATCH 01/14] feat: define didx problem report Signed-off-by: Daniel Bluhm --- .../didexchange/v1_0/message_types.py | 1 + .../v1_0/messages/problem_report_reason.py | 71 ++++++++++++++++++- 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/aries_cloudagent/protocols/didexchange/v1_0/message_types.py b/aries_cloudagent/protocols/didexchange/v1_0/message_types.py index 02d11ac306..ce0f952848 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/message_types.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/message_types.py @@ -12,6 +12,7 @@ DIDX_REQUEST = f"{ARIES_PROTOCOL}/request" DIDX_RESPONSE = f"{ARIES_PROTOCOL}/response" DIDX_COMPLETE = f"{ARIES_PROTOCOL}/complete" +PROBLEM_REPORT = f"{ARIES_PROTOCOL}/problem_report" PROTOCOL_PACKAGE = "aries_cloudagent.protocols.didexchange.v1_0" diff --git a/aries_cloudagent/protocols/didexchange/v1_0/messages/problem_report_reason.py b/aries_cloudagent/protocols/didexchange/v1_0/messages/problem_report_reason.py index 3370264deb..3daeb86634 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/messages/problem_report_reason.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/messages/problem_report_reason.py @@ -1,6 +1,19 @@ -"""DID Exchange problem report reasons.""" +"""DID Exchange problem report and reasons.""" from enum import Enum +from typing import Optional +from marshmallow import EXCLUDE, fields, validate + +from .....messaging.agent_message import AgentMessage, AgentMessageSchema + +from ..message_types import PROBLEM_REPORT + + +# TODO this should be handled by a didx specific handler to mark the connection +# as abandoned. +HANDLER_CLASS = ( + "aries_cloudagent.protocols.problem_report.v1_0.handler.ProblemReportHandler" +) class ProblemReportReason(Enum): @@ -12,3 +25,59 @@ class ProblemReportReason(Enum): RESPONSE_NOT_ACCEPTED = "response_not_accepted" RESPONSE_PROCESSING_ERROR = "response_processing_error" COMPLETE_NOT_ACCEPTED = "complete_not_accepted" + ABANDONED = "abandoned" + + +class DIDXProblemReport(AgentMessage): + """Base class representing a connection problem report message.""" + + class Meta: + """DID Exchange problem report metadata.""" + + handler_class = HANDLER_CLASS + message_type = PROBLEM_REPORT + schema_class = "DIDXProblemReportSchema" + + def __init__( + self, + *, + problem_code: Optional[str] = None, + explain: Optional[str] = None, + **kwargs + ): + """ + Initialize a ProblemReport message instance. + + Args: + explain: The localized error explanation + problem_code: The standard error identifier + """ + super().__init__(**kwargs) + self.explain = explain + self.problem_code = problem_code + + +class DIDXProblemReportSchema(AgentMessageSchema): + """Schema for DIDXProblemReport model class.""" + + class Meta: + """Metadata for connection problem report schema.""" + + model_class = DIDXProblemReport + unknown = EXCLUDE + + explain = fields.Str( + required=False, + description="Localized error explanation", + example="Invitation not accepted", + ) + problem_code = fields.Str( + data_key="problem-code", + required=False, + description="Standard error identifier", + validate=validate.OneOf( + choices=[prr.value for prr in ProblemReportReason], + error="Value {input} must be one of {choices}.", + ), + example=ProblemReportReason.INVITATION_NOT_ACCEPTED.value, + ) From 915b2e9b5c06a616a51e712466c690486c5a8407 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 2 Aug 2023 08:02:08 -0400 Subject: [PATCH 02/14] refactor: move problem_report_reason to problem_report Signed-off-by: Daniel Bluhm --- .../protocols/didexchange/v1_0/handlers/invitation_handler.py | 2 +- .../didexchange/v1_0/handlers/tests/test_complete_handler.py | 2 +- .../didexchange/v1_0/handlers/tests/test_invitation_handler.py | 2 +- .../didexchange/v1_0/handlers/tests/test_request_handler.py | 2 +- .../didexchange/v1_0/handlers/tests/test_response_handler.py | 2 +- aries_cloudagent/protocols/didexchange/v1_0/manager.py | 2 +- .../messages/{problem_report_reason.py => problem_report.py} | 0 7 files changed, 6 insertions(+), 6 deletions(-) rename aries_cloudagent/protocols/didexchange/v1_0/messages/{problem_report_reason.py => problem_report.py} (100%) diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/invitation_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/invitation_handler.py index ab193a06d1..eed715216a 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/handlers/invitation_handler.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/invitation_handler.py @@ -9,7 +9,7 @@ from ....out_of_band.v1_0.messages.invitation import InvitationMessage from ....problem_report.v1_0.message import ProblemReport -from ..messages.problem_report_reason import ProblemReportReason +from ..messages.problem_report import ProblemReportReason class InvitationHandler(BaseHandler): diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_complete_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_complete_handler.py index 19372e515c..945a6de809 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_complete_handler.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_complete_handler.py @@ -7,7 +7,7 @@ from ...manager import DIDXManagerError from ...messages.complete import DIDXComplete -from ...messages.problem_report_reason import ProblemReportReason +from ...messages.problem_report import ProblemReportReason from .. import complete_handler as test_module from ......wallet.did_method import DIDMethods diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_invitation_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_invitation_handler.py index 279d905863..230221cb46 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_invitation_handler.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_invitation_handler.py @@ -8,7 +8,7 @@ from .....problem_report.v1_0.message import ProblemReport from ...handlers.invitation_handler import InvitationHandler -from ...messages.problem_report_reason import ProblemReportReason +from ...messages.problem_report import ProblemReportReason from ......wallet.did_method import DIDMethods diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_request_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_request_handler.py index 6c7a5892b8..489141bf07 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_request_handler.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_request_handler.py @@ -21,7 +21,7 @@ from ...handlers import request_handler as test_module from ...manager import DIDXManagerError from ...messages.request import DIDXRequest -from ...messages.problem_report_reason import ProblemReportReason +from ...messages.problem_report import ProblemReportReason TEST_DID = "55GkHamhTU1ZbTbV2ab9DE" TEST_VERKEY = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_response_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_response_handler.py index ba81955e36..be279c47a8 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_response_handler.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_response_handler.py @@ -22,7 +22,7 @@ from ...handlers import response_handler as test_module from ...manager import DIDXManagerError from ...messages.response import DIDXResponse -from ...messages.problem_report_reason import ProblemReportReason +from ...messages.problem_report import ProblemReportReason TEST_DID = "55GkHamhTU1ZbTbV2ab9DE" TEST_VERKEY = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" diff --git a/aries_cloudagent/protocols/didexchange/v1_0/manager.py b/aries_cloudagent/protocols/didexchange/v1_0/manager.py index ef739e0ef4..d5c0a19f62 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/manager.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/manager.py @@ -35,7 +35,7 @@ from ...out_of_band.v1_0.messages.service import Service as OOBService from .message_types import ARIES_PROTOCOL as DIDX_PROTO from .messages.complete import DIDXComplete -from .messages.problem_report_reason import ProblemReportReason +from .messages.problem_report import ProblemReportReason from .messages.request import DIDXRequest from .messages.response import DIDXResponse diff --git a/aries_cloudagent/protocols/didexchange/v1_0/messages/problem_report_reason.py b/aries_cloudagent/protocols/didexchange/v1_0/messages/problem_report.py similarity index 100% rename from aries_cloudagent/protocols/didexchange/v1_0/messages/problem_report_reason.py rename to aries_cloudagent/protocols/didexchange/v1_0/messages/problem_report.py From 97cb9a7d472db6158cf8878ca5fc510d709a7638 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 2 Aug 2023 08:52:22 -0400 Subject: [PATCH 03/14] feat: send didexchange problem reports Signed-off-by: Daniel Bluhm --- .../v1_0/handlers/invitation_handler.py | 5 +- .../v1_0/handlers/request_handler.py | 8 ++- .../v1_0/handlers/response_handler.py | 8 ++- .../handlers/tests/test_invitation_handler.py | 13 ++-- .../handlers/tests/test_request_handler.py | 45 +++++++------ .../handlers/tests/test_response_handler.py | 33 ++++++---- .../protocols/didexchange/v1_0/manager.py | 2 +- .../v1_0/messages/problem_report.py | 63 +++++++------------ 8 files changed, 95 insertions(+), 82 deletions(-) diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/invitation_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/invitation_handler.py index eed715216a..1ad96afa1b 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/handlers/invitation_handler.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/invitation_handler.py @@ -7,9 +7,8 @@ ) from ....out_of_band.v1_0.messages.invitation import InvitationMessage -from ....problem_report.v1_0.message import ProblemReport -from ..messages.problem_report import ProblemReportReason +from ..messages.problem_report import DIDXProblemReport, ProblemReportReason class InvitationHandler(BaseHandler): @@ -27,7 +26,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): self._logger.debug(f"InvitationHandler called with context {context}") assert isinstance(context.message, InvitationMessage) - report = ProblemReport( + report = DIDXProblemReport( description={ "code": ProblemReportReason.INVITATION_NOT_ACCEPTED.value, "en": ( diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/request_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/request_handler.py index 766ecb8571..736bf66a7a 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/handlers/request_handler.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/request_handler.py @@ -1,9 +1,11 @@ """Connection request handler under RFC 23 (DID exchange).""" +from aries_cloudagent.protocols.didexchange.v1_0.messages.problem_report import ( + DIDXProblemReport, +) from .....connections.models.conn_record import ConnRecord from .....messaging.base_handler import BaseHandler, BaseResponder, RequestContext from ....coordinate_mediation.v1_0.manager import MediationManager -from ....problem_report.v1_0.message import ProblemReport from ..manager import DIDXManager, DIDXManagerError from ..messages.request import DIDXRequest @@ -75,6 +77,8 @@ async def handle(self, context: RequestContext, responder: BaseResponder): "Error parsing DIDDoc for problem report" ) await responder.send_reply( - ProblemReport(description={"en": e.message, "code": e.error_code}), + DIDXProblemReport( + description={"en": e.message, "code": e.error_code} + ), target_list=targets, ) diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/response_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/response_handler.py index 68a81fca50..ccf17e1b60 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/handlers/response_handler.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/response_handler.py @@ -1,12 +1,14 @@ """DID exchange response handler under RFC 23.""" +from aries_cloudagent.protocols.didexchange.v1_0.messages.problem_report import ( + DIDXProblemReport, +) from .....messaging.base_handler import ( BaseHandler, BaseResponder, RequestContext, ) -from ....problem_report.v1_0.message import ProblemReport from ....trustping.v1_0.messages.ping import Ping from ..manager import DIDXManager, DIDXManagerError @@ -48,7 +50,9 @@ async def handle(self, context: RequestContext, responder: BaseResponder): "Error parsing DIDDoc for problem report" ) await responder.send_reply( - ProblemReport(description={"en": e.message, "code": e.error_code}), + DIDXProblemReport( + description={"en": e.message, "code": e.error_code} + ), target_list=targets, ) return diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_invitation_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_invitation_handler.py index 230221cb46..77013f4fb7 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_invitation_handler.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_invitation_handler.py @@ -5,10 +5,9 @@ from ......transport.inbound.receipt import MessageReceipt from .....out_of_band.v1_0.messages.invitation import InvitationMessage -from .....problem_report.v1_0.message import ProblemReport from ...handlers.invitation_handler import InvitationHandler -from ...messages.problem_report import ProblemReportReason +from ...messages.problem_report import DIDXProblemReport, ProblemReportReason from ......wallet.did_method import DIDMethods @@ -30,8 +29,12 @@ async def test_problem_report(self, request_context): messages = responder.messages assert len(messages) == 1 result, target = messages[0] - assert isinstance(result, ProblemReport) and ( - result.description["code"] - == ProblemReportReason.INVITATION_NOT_ACCEPTED.value + assert ( + isinstance(result, DIDXProblemReport) + and result.description + and ( + result.description["code"] + == ProblemReportReason.INVITATION_NOT_ACCEPTED.value + ) ) assert not target diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_request_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_request_handler.py index 489141bf07..dada5723a3 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_request_handler.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_request_handler.py @@ -1,27 +1,19 @@ from asynctest import mock as async_mock from asynctest import TestCase as AsyncTestCase -from ......connections.models import connection_target, conn_record -from ......connections.models.diddoc import ( - DIDDoc, - PublicKey, - PublicKeyType, - Service, -) +from ......connections.models import conn_record, connection_target +from ......connections.models.diddoc import DIDDoc, PublicKey, PublicKeyType, Service from ......core.in_memory import InMemoryProfile -from ......wallet.did_method import SOV, DIDMethods -from ......wallet.key_type import ED25519 from ......messaging.decorators.attach_decorator import AttachDecorator from ......messaging.request_context import RequestContext from ......messaging.responder import MockResponder from ......transport.inbound.receipt import MessageReceipt - -from .....problem_report.v1_0.message import ProblemReport - +from ......wallet.did_method import DIDMethods, SOV +from ......wallet.key_type import ED25519 from ...handlers import request_handler as test_module from ...manager import DIDXManagerError +from ...messages.problem_report import DIDXProblemReport, ProblemReportReason from ...messages.request import DIDXRequest -from ...messages.problem_report import ProblemReportReason TEST_DID = "55GkHamhTU1ZbTbV2ab9DE" TEST_VERKEY = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" @@ -184,8 +176,13 @@ async def test_problem_report(self, mock_didx_mgr): messages = responder.messages assert len(messages) == 1 result, target = messages[0] - assert isinstance(result, ProblemReport) and ( - result.description["code"] == ProblemReportReason.REQUEST_NOT_ACCEPTED.value + assert ( + isinstance(result, DIDXProblemReport) + and result.description + and ( + result.description["code"] + == ProblemReportReason.REQUEST_NOT_ACCEPTED.value + ) ) assert target == {"target_list": None} @@ -211,8 +208,13 @@ async def test_problem_report_did_doc(self, mock_conn_target, mock_didx_mgr): messages = responder.messages assert len(messages) == 1 result, target = messages[0] - assert isinstance(result, ProblemReport) and ( - result.description["code"] == ProblemReportReason.REQUEST_NOT_ACCEPTED.value + assert ( + isinstance(result, DIDXProblemReport) + and result.description + and ( + result.description["code"] + == ProblemReportReason.REQUEST_NOT_ACCEPTED.value + ) ) assert target == {"target_list": [mock_conn_target]} @@ -242,7 +244,12 @@ async def test_problem_report_did_doc_no_conn_target( messages = responder.messages assert len(messages) == 1 result, target = messages[0] - assert isinstance(result, ProblemReport) and ( - result.description["code"] == ProblemReportReason.REQUEST_NOT_ACCEPTED.value + assert ( + isinstance(result, DIDXProblemReport) + and result.description + and ( + result.description["code"] + == ProblemReportReason.REQUEST_NOT_ACCEPTED.value + ) ) assert target == {"target_list": None} diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_response_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_response_handler.py index be279c47a8..fa4fe5ad12 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_response_handler.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_response_handler.py @@ -16,13 +16,12 @@ from ......wallet.did_method import SOV, DIDMethods from ......wallet.key_type import ED25519 -from .....problem_report.v1_0.message import ProblemReport from .....trustping.v1_0.messages.ping import Ping from ...handlers import response_handler as test_module from ...manager import DIDXManagerError from ...messages.response import DIDXResponse -from ...messages.problem_report import ProblemReportReason +from ...messages.problem_report import DIDXProblemReport, ProblemReportReason TEST_DID = "55GkHamhTU1ZbTbV2ab9DE" TEST_VERKEY = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" @@ -125,9 +124,13 @@ async def test_problem_report(self, mock_didx_mgr): messages = responder.messages assert len(messages) == 1 result, target = messages[0] - assert isinstance(result, ProblemReport) and ( - result.description["code"] - == ProblemReportReason.RESPONSE_NOT_ACCEPTED.value + assert ( + isinstance(result, DIDXProblemReport) + and result.description + and ( + result.description["code"] + == ProblemReportReason.RESPONSE_NOT_ACCEPTED.value + ) ) assert target == {"target_list": None} @@ -157,9 +160,13 @@ async def test_problem_report_did_doc( messages = responder.messages assert len(messages) == 1 result, target = messages[0] - assert isinstance(result, ProblemReport) and ( - result.description["code"] - == ProblemReportReason.RESPONSE_NOT_ACCEPTED.value + assert ( + isinstance(result, DIDXProblemReport) + and result.description + and ( + result.description["code"] + == ProblemReportReason.RESPONSE_NOT_ACCEPTED.value + ) ) assert target == {"target_list": [mock_conn_target]} @@ -189,8 +196,12 @@ async def test_problem_report_did_doc_no_conn_target( messages = responder.messages assert len(messages) == 1 result, target = messages[0] - assert isinstance(result, ProblemReport) and ( - result.description["code"] - == ProblemReportReason.RESPONSE_NOT_ACCEPTED.value + assert ( + isinstance(result, DIDXProblemReport) + and result.description + and ( + result.description["code"] + == ProblemReportReason.RESPONSE_NOT_ACCEPTED.value + ) ) assert target == {"target_list": None} diff --git a/aries_cloudagent/protocols/didexchange/v1_0/manager.py b/aries_cloudagent/protocols/didexchange/v1_0/manager.py index d5c0a19f62..9194ee995b 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/manager.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/manager.py @@ -35,7 +35,7 @@ from ...out_of_band.v1_0.messages.service import Service as OOBService from .message_types import ARIES_PROTOCOL as DIDX_PROTO from .messages.complete import DIDXComplete -from .messages.problem_report import ProblemReportReason +from .messages.problem_report import DIDXProblemReport, ProblemReportReason from .messages.request import DIDXRequest from .messages.response import DIDXResponse diff --git a/aries_cloudagent/protocols/didexchange/v1_0/messages/problem_report.py b/aries_cloudagent/protocols/didexchange/v1_0/messages/problem_report.py index 3daeb86634..651b66725c 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/messages/problem_report.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/messages/problem_report.py @@ -1,11 +1,11 @@ """DID Exchange problem report and reasons.""" from enum import Enum -from typing import Optional -from marshmallow import EXCLUDE, fields, validate +import logging -from .....messaging.agent_message import AgentMessage, AgentMessageSchema +from marshmallow import EXCLUDE, ValidationError, validates_schema +from ....problem_report.v1_0.message import ProblemReport, ProblemReportSchema from ..message_types import PROBLEM_REPORT @@ -15,6 +15,8 @@ "aries_cloudagent.protocols.problem_report.v1_0.handler.ProblemReportHandler" ) +LOGGER = logging.getLogger(__name__) + class ProblemReportReason(Enum): """Supported reason codes.""" @@ -28,7 +30,7 @@ class ProblemReportReason(Enum): ABANDONED = "abandoned" -class DIDXProblemReport(AgentMessage): +class DIDXProblemReport(ProblemReport): """Base class representing a connection problem report message.""" class Meta: @@ -38,26 +40,8 @@ class Meta: message_type = PROBLEM_REPORT schema_class = "DIDXProblemReportSchema" - def __init__( - self, - *, - problem_code: Optional[str] = None, - explain: Optional[str] = None, - **kwargs - ): - """ - Initialize a ProblemReport message instance. - - Args: - explain: The localized error explanation - problem_code: The standard error identifier - """ - super().__init__(**kwargs) - self.explain = explain - self.problem_code = problem_code - - -class DIDXProblemReportSchema(AgentMessageSchema): + +class DIDXProblemReportSchema(ProblemReportSchema): """Schema for DIDXProblemReport model class.""" class Meta: @@ -66,18 +50,19 @@ class Meta: model_class = DIDXProblemReport unknown = EXCLUDE - explain = fields.Str( - required=False, - description="Localized error explanation", - example="Invitation not accepted", - ) - problem_code = fields.Str( - data_key="problem-code", - required=False, - description="Standard error identifier", - validate=validate.OneOf( - choices=[prr.value for prr in ProblemReportReason], - error="Value {input} must be one of {choices}.", - ), - example=ProblemReportReason.INVITATION_NOT_ACCEPTED.value, - ) + @validates_schema + def validate_fields(self, data, **kwargs): + """Validate schema fields.""" + + if not data.get("description", {}).get("code", ""): + raise ValidationError("Value for description.code must be present") + elif data.get("description", {}).get("code", "") not in [ + prr.value for prr in ProblemReportReason + ]: + locales = list(data.get("description").keys()) + locales.remove("code") + LOGGER.warning( + "Unexpected error code received.\n" + f"Code: {data.get('description').get('code')}, " + f"Description: {data.get('description').get(locales[0])}" + ) From eadb7d4898cb0f8e234a16eae5ada6c3605f9551 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 2 Aug 2023 08:52:48 -0400 Subject: [PATCH 04/14] feat: add abandon method to conn record Signed-off-by: Daniel Bluhm --- aries_cloudagent/connections/models/conn_record.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/aries_cloudagent/connections/models/conn_record.py b/aries_cloudagent/connections/models/conn_record.py index 5303c065d0..131f2eff64 100644 --- a/aries_cloudagent/connections/models/conn_record.py +++ b/aries_cloudagent/connections/models/conn_record.py @@ -535,6 +535,13 @@ async def delete_record(self, session: ProfileSession): {"connection_id": self.connection_id}, ) + async def abandon(self, session: ProfileSession, *, reason: Optional[str] = None): + """Set state to abandoned.""" + reason = reason or "Connectin abandoned" + self.state = ConnRecord.State.ABANDONED.rfc23 + self.error_msg = reason + await self.save(session, reason=reason) + async def metadata_get( self, session: ProfileSession, key: str, default: Any = None ) -> Any: From c216d83b306f2439fc95d1fa0c35a2a5c523e57a Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 2 Aug 2023 08:55:06 -0400 Subject: [PATCH 05/14] feat: add didx reject endpoint Signed-off-by: Daniel Bluhm --- .../protocols/didexchange/v1_0/manager.py | 32 ++++++++++++--- .../protocols/didexchange/v1_0/routes.py | 40 +++++++++++++++++++ 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/aries_cloudagent/protocols/didexchange/v1_0/manager.py b/aries_cloudagent/protocols/didexchange/v1_0/manager.py index 9194ee995b..8067b05055 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/manager.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/manager.py @@ -368,10 +368,10 @@ async def receive_request( self, request: DIDXRequest, recipient_did: str, - recipient_verkey: str = None, - my_endpoint: str = None, - alias: str = None, - auto_accept_implicit: bool = None, + recipient_verkey: Optional[str] = None, + my_endpoint: Optional[str] = None, + alias: Optional[str] = None, + auto_accept_implicit: Optional[bool] = None, ) -> ConnRecord: """ Receive and store a connection request. @@ -561,8 +561,8 @@ async def receive_request( async def create_response( self, conn_rec: ConnRecord, - my_endpoint: str = None, - mediation_id: str = None, + my_endpoint: Optional[str] = None, + mediation_id: Optional[str] = None, ) -> DIDXResponse: """ Create a connection response for a received connection request. @@ -856,6 +856,26 @@ async def accept_complete( return conn_rec + async def abandon_exchange( + self, + conn_rec: ConnRecord, + *, + reason: Optional[str] = None, + ) -> DIDXProblemReport: + """Abandon an existing DID exchange.""" + async with self.profile.session() as session: + await conn_rec.abandon(session, reason=reason) + + report = DIDXProblemReport( + description={ + "code": ProblemReportReason.ABANDONED.value, + "en": reason or "Connection abandoned", + }, + ) + + # TODO Delete the record? + return report + async def verify_diddoc( self, wallet: BaseWallet, diff --git a/aries_cloudagent/protocols/didexchange/v1_0/routes.py b/aries_cloudagent/protocols/didexchange/v1_0/routes.py index b284b0394e..325df0d17f 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/routes.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/routes.py @@ -174,6 +174,16 @@ class DIDXConnIdRefIdMatchInfoSchema(OpenAPISchema): ) +class DIDXRejectRequestSchema(OpenAPISchema): + """Parameters and validators for reject-request request string.""" + + reason = fields.Str( + description="Reason for rejecting the DID Exchange", + required=False, + example="Request rejected", + ) + + @docs( tags=["did-exchange"], summary="Accept a stored connection invitation", @@ -362,6 +372,35 @@ async def didx_accept_request(request: web.BaseRequest): return web.json_response(result) +@docs( + tags=["did-exchange"], + summary="Abandon or reject a DID Exchange", +) +@match_info_schema(DIDXConnIdMatchInfoSchema()) +@request_schema(DIDXRejectRequestSchema()) +@response_schema(ConnRecordSchema(), 200, description="") +async def didx_reject(request: web.BaseRequest): + context: AdminRequestContext = request["context"] + outbound_handler = request["outbound_message_router"] + + connection_id = request.match_info["conn_id"] + + profile = context.profile + didx_mgr = DIDXManager(profile) + try: + async with profile.session() as session: + conn_rec = await ConnRecord.retrieve_by_id(session, connection_id) + report = await didx_mgr.abandon_exchange(conn_rec=conn_rec) + except StorageNotFoundError as err: + raise web.HTTPNotFound(reason=err.roll_up) from err + except (StorageError, WalletError, DIDXManagerError, BaseModelError) as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + + await outbound_handler(report, connection_id=conn_rec.connection_id) + result = conn_rec.serialize() + return web.json_response(result) + + async def register(app: web.Application): """Register routes.""" @@ -374,6 +413,7 @@ async def register(app: web.Application): web.post("/didexchange/create-request", didx_create_request_implicit), web.post("/didexchange/receive-request", didx_receive_request_implicit), web.post("/didexchange/{conn_id}/accept-request", didx_accept_request), + web.post("/didexchange/{conn_id}/reject", didx_reject), ] ) From 7a0d675d9aecacda587011cc63205e102bdfcbf7 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 2 Aug 2023 09:10:29 -0400 Subject: [PATCH 06/14] fix: use existing error codes Signed-off-by: Daniel Bluhm --- .../protocols/didexchange/v1_0/manager.py | 16 +++++++++++++--- .../didexchange/v1_0/messages/problem_report.py | 1 - .../protocols/didexchange/v1_0/routes.py | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/aries_cloudagent/protocols/didexchange/v1_0/manager.py b/aries_cloudagent/protocols/didexchange/v1_0/manager.py index 8067b05055..439a1bb1b7 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/manager.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/manager.py @@ -856,7 +856,7 @@ async def accept_complete( return conn_rec - async def abandon_exchange( + async def reject( self, conn_rec: ConnRecord, *, @@ -866,10 +866,20 @@ async def abandon_exchange( async with self.profile.session() as session: await conn_rec.abandon(session, reason=reason) + state_to_reject_code = { + "invitation-received": ProblemReportReason.INVITATION_NOT_ACCEPTED, + "request-received": ProblemReportReason.REQUEST_NOT_ACCEPTED, + } + code = state_to_reject_code.get(conn_rec.rfc23_state) + if not code: + raise DIDXManagerError( + f"Cannot reject connection in state: {conn_rec.state}" + ) + report = DIDXProblemReport( description={ - "code": ProblemReportReason.ABANDONED.value, - "en": reason or "Connection abandoned", + "code": code.value, + "en": reason or "DID exchange rejected", }, ) diff --git a/aries_cloudagent/protocols/didexchange/v1_0/messages/problem_report.py b/aries_cloudagent/protocols/didexchange/v1_0/messages/problem_report.py index 651b66725c..f657461658 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/messages/problem_report.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/messages/problem_report.py @@ -27,7 +27,6 @@ class ProblemReportReason(Enum): RESPONSE_NOT_ACCEPTED = "response_not_accepted" RESPONSE_PROCESSING_ERROR = "response_processing_error" COMPLETE_NOT_ACCEPTED = "complete_not_accepted" - ABANDONED = "abandoned" class DIDXProblemReport(ProblemReport): diff --git a/aries_cloudagent/protocols/didexchange/v1_0/routes.py b/aries_cloudagent/protocols/didexchange/v1_0/routes.py index 325df0d17f..71dff1f40a 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/routes.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/routes.py @@ -390,7 +390,7 @@ async def didx_reject(request: web.BaseRequest): try: async with profile.session() as session: conn_rec = await ConnRecord.retrieve_by_id(session, connection_id) - report = await didx_mgr.abandon_exchange(conn_rec=conn_rec) + report = await didx_mgr.reject(conn_rec=conn_rec) except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err except (StorageError, WalletError, DIDXManagerError, BaseModelError) as err: From 5ddf1cbbbd1f357ba0a51cd12cd76d40afd92ae9 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 2 Aug 2023 13:32:26 -0400 Subject: [PATCH 07/14] feat: add did exchange problem report handler Signed-off-by: Daniel Bluhm --- .../v1_0/handlers/problem_report_handler.py | 33 +++++++++++++++++++ .../protocols/didexchange/v1_0/manager.py | 23 +++++++++++++ .../v1_0/messages/problem_report.py | 5 ++- 3 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 aries_cloudagent/protocols/didexchange/v1_0/handlers/problem_report_handler.py diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/problem_report_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/problem_report_handler.py new file mode 100644 index 0000000000..8039abc946 --- /dev/null +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/problem_report_handler.py @@ -0,0 +1,33 @@ +"""Problem report handler for DID Exchange.""" + +from .....messaging.base_handler import ( + BaseHandler, + BaseResponder, + HandlerException, + RequestContext, +) +from ..manager import DIDXManager, DIDXManagerError +from ..messages.problem_report import DIDXProblemReport + + +class DIDXProblemReportHandler(BaseHandler): + """Handler class for DID Exchange problem report messages.""" + + async def handle(self, context: RequestContext, responder: BaseResponder): + """Handle problem report message.""" + self._logger.debug(f"DIDXProblemReportHandler called with context {context}") + assert isinstance(context.message, DIDXProblemReport) + + self._logger.info("Received problem report: %s", context.message.description) + profile = context.profile + mgr = DIDXManager(profile) + try: + if context.connection_record: + await mgr.receive_problem_report( + context.connection_record, context.message + ) + else: + raise HandlerException("No connection established for problem report") + except DIDXManagerError: + # Unrecognized problem report code + self._logger.exception("Error receiving DID Exchange problem report") diff --git a/aries_cloudagent/protocols/didexchange/v1_0/manager.py b/aries_cloudagent/protocols/didexchange/v1_0/manager.py index 439a1bb1b7..94821f2925 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/manager.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/manager.py @@ -886,6 +886,29 @@ async def reject( # TODO Delete the record? return report + async def receive_problem_report( + self, + conn_rec: ConnRecord, + report: DIDXProblemReport, + ): + """Receive problem report.""" + if not report.description: + raise DIDXManagerError("Missing description in problem report") + + if report.description.get("code") in set( + reason.value for reason in ProblemReportReason + ): + self._logger.info("Problem report indicates connection is abandoned") + async with self.profile.session() as session: + await conn_rec.abandon( + session, + reason=report.description.get("en"), + ) + else: + raise DIDXManagerError( + "Received unrecognized problem report: %s", report.serialize() + ) + async def verify_diddoc( self, wallet: BaseWallet, diff --git a/aries_cloudagent/protocols/didexchange/v1_0/messages/problem_report.py b/aries_cloudagent/protocols/didexchange/v1_0/messages/problem_report.py index f657461658..46e7987ec4 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/messages/problem_report.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/messages/problem_report.py @@ -9,10 +9,9 @@ from ..message_types import PROBLEM_REPORT -# TODO this should be handled by a didx specific handler to mark the connection -# as abandoned. HANDLER_CLASS = ( - "aries_cloudagent.protocols.problem_report.v1_0.handler.ProblemReportHandler" + "aries_cloudagent.protocols.didexchange.v1_0.handlers." + "problem_report_handler.ProblemReportHandler" ) LOGGER = logging.getLogger(__name__) From a727c46a3c552bab8c31158ac92cb0fc1d80fad8 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 2 Aug 2023 13:39:16 -0400 Subject: [PATCH 08/14] style: appease flake8 Signed-off-by: Daniel Bluhm --- aries_cloudagent/protocols/didexchange/v1_0/routes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aries_cloudagent/protocols/didexchange/v1_0/routes.py b/aries_cloudagent/protocols/didexchange/v1_0/routes.py index 71dff1f40a..4734397c4f 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/routes.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/routes.py @@ -380,6 +380,7 @@ async def didx_accept_request(request: web.BaseRequest): @request_schema(DIDXRejectRequestSchema()) @response_schema(ConnRecordSchema(), 200, description="") async def didx_reject(request: web.BaseRequest): + """Abandon or reject a DID Exchange.""" context: AdminRequestContext = request["context"] outbound_handler = request["outbound_message_router"] From a32274cd1c66aee619ad9038d0988be6d30123e3 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 2 Aug 2023 14:06:14 -0400 Subject: [PATCH 09/14] test: problem report handler Signed-off-by: Daniel Bluhm --- .../tests/test_problem_report_handler.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_problem_report_handler.py diff --git a/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_problem_report_handler.py b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_problem_report_handler.py new file mode 100644 index 0000000000..e851c2d8c7 --- /dev/null +++ b/aries_cloudagent/protocols/didexchange/v1_0/handlers/tests/test_problem_report_handler.py @@ -0,0 +1,56 @@ +from asynctest import mock as async_mock +import pytest + +from .. import problem_report_handler as test_module +from ......messaging.base_handler import HandlerException +from ......messaging.request_context import RequestContext +from ......messaging.responder import MockResponder +from ...manager import DIDXManagerError +from ...messages.problem_report import DIDXProblemReport + + +@pytest.fixture() +def request_context(): + ctx = RequestContext.test_context() + yield ctx + + +class TestDIDXProblemReportHandler: + """Unit test problem report handler.""" + + @pytest.mark.asyncio + @async_mock.patch.object(test_module, "DIDXManager") + async def test_called(self, manager, request_context): + manager.return_value.receive_problem_report = async_mock.CoroutineMock() + request_context.message = DIDXProblemReport() + request_context.connection_record = async_mock.MagicMock() + handler_inst = test_module.DIDXProblemReportHandler() + responder = MockResponder() + await handler_inst.handle(request_context, responder) + assert not responder.messages + assert manager.return_value.receive_problem_report.called_once() + + @pytest.mark.asyncio + @async_mock.patch.object(test_module, "DIDXManager") + async def test_called_no_conn(self, manager, request_context): + manager.return_value.receive_problem_report = async_mock.CoroutineMock() + request_context.message = DIDXProblemReport() + handler_inst = test_module.DIDXProblemReportHandler() + responder = MockResponder() + with pytest.raises(HandlerException): + await handler_inst.handle(request_context, responder) + + @pytest.mark.asyncio + @async_mock.patch.object(test_module, "DIDXManager") + async def test_called_unrecognized_report_exception( + self, manager, request_context, caplog + ): + manager.return_value.receive_problem_report = async_mock.CoroutineMock( + side_effect=DIDXManagerError() + ) + request_context.message = DIDXProblemReport() + request_context.connection_record = async_mock.MagicMock() + handler_inst = test_module.DIDXProblemReportHandler() + responder = MockResponder() + await handler_inst.handle(request_context, responder) + assert "Error receiving DID Exchange problem report" in caplog.text From 7b76d00d714879148b0c663754d917d1dbae95b6 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 2 Aug 2023 14:37:06 -0400 Subject: [PATCH 10/14] test: reject and receive problem report in didx manager Signed-off-by: Daniel Bluhm --- .../protocols/didexchange/v1_0/manager.py | 16 ++-- .../didexchange/v1_0/tests/test_manager.py | 87 ++++++++++++++++++- 2 files changed, 94 insertions(+), 9 deletions(-) diff --git a/aries_cloudagent/protocols/didexchange/v1_0/manager.py b/aries_cloudagent/protocols/didexchange/v1_0/manager.py index 94821f2925..d129c019a3 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/manager.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/manager.py @@ -863,19 +863,21 @@ async def reject( reason: Optional[str] = None, ) -> DIDXProblemReport: """Abandon an existing DID exchange.""" - async with self.profile.session() as session: - await conn_rec.abandon(session, reason=reason) - state_to_reject_code = { - "invitation-received": ProblemReportReason.INVITATION_NOT_ACCEPTED, - "request-received": ProblemReportReason.REQUEST_NOT_ACCEPTED, + ConnRecord.State.INVITATION.rfc23 + + "-received": ProblemReportReason.INVITATION_NOT_ACCEPTED, + ConnRecord.State.REQUEST.rfc23 + + "-received": ProblemReportReason.REQUEST_NOT_ACCEPTED, } code = state_to_reject_code.get(conn_rec.rfc23_state) if not code: raise DIDXManagerError( - f"Cannot reject connection in state: {conn_rec.state}" + f"Cannot reject connection in state: {conn_rec.rfc23_state}" ) + async with self.profile.session() as session: + await conn_rec.abandon(session, reason=reason) + report = DIDXProblemReport( description={ "code": code.value, @@ -906,7 +908,7 @@ async def receive_problem_report( ) else: raise DIDXManagerError( - "Received unrecognized problem report: %s", report.serialize() + f"Received unrecognized problem report: {report.description}" ) async def verify_diddoc( diff --git a/aries_cloudagent/protocols/didexchange/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/didexchange/v1_0/tests/test_manager.py index 604f6a4f82..c15bf555d5 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/tests/test_manager.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/tests/test_manager.py @@ -4,6 +4,7 @@ from asynctest import mock as async_mock from pydid import DIDDocument +from .. import manager as test_module from .....cache.base import BaseCache from .....cache.in_memory import InMemoryCache from .....connections.base_manager import BaseConnectionManagerError @@ -24,7 +25,7 @@ from .....storage.error import StorageNotFoundError from .....transport.inbound.receipt import MessageReceipt from .....wallet.did_info import DIDInfo -from .....wallet.did_method import SOV, DIDMethods +from .....wallet.did_method import DIDMethods, SOV from .....wallet.error import WalletError from .....wallet.in_memory import InMemoryWallet from .....wallet.key_type import ED25519 @@ -36,8 +37,8 @@ from ....out_of_band.v1_0.manager import OutOfBandManager from ....out_of_band.v1_0.messages.invitation import HSProto, InvitationMessage from ....out_of_band.v1_0.messages.service import Service as OOBService -from .. import manager as test_module from ..manager import DIDXManager, DIDXManagerError +from ..messages.problem_report import DIDXProblemReport, ProblemReportReason class TestConfig: @@ -1825,6 +1826,88 @@ async def test_accept_complete_x_not_found(self): with self.assertRaises(DIDXManagerError): await self.manager.accept_complete(mock_complete, receipt) + async def test_reject_invited(self): + mock_conn = ConnRecord( + connection_id="dummy", + inbound_connection_id=None, + their_did=TestConfig.test_target_did, + state=ConnRecord.State.INVITATION.rfc23, + their_role=ConnRecord.Role.RESPONDER, + ) + mock_conn.abandon = async_mock.CoroutineMock() + reason = "He doesn't like you!" + report = await self.manager.reject(mock_conn, reason=reason) + assert report + + async def test_reject_requested(self): + mock_conn = ConnRecord( + connection_id="dummy", + inbound_connection_id=None, + their_did=TestConfig.test_target_did, + state=ConnRecord.State.REQUEST.rfc23, + their_role=ConnRecord.Role.REQUESTER, + ) + mock_conn.abandon = async_mock.CoroutineMock() + reason = "I don't like you either! You just watch yourself!" + report = await self.manager.reject(mock_conn, reason=reason) + assert report + + async def test_reject_invalid(self): + mock_conn = ConnRecord( + connection_id="dummy", + inbound_connection_id=None, + their_did=TestConfig.test_target_did, + state=ConnRecord.State.COMPLETED.rfc23, + ) + mock_conn.abandon = async_mock.CoroutineMock() + reason = "I'll be careful." + with self.assertRaises(DIDXManagerError) as context: + await self.manager.reject(mock_conn, reason=reason) + assert "Cannot reject connection in state" in str(context.exception) + + async def test_receive_problem_report(self): + mock_conn = async_mock.MagicMock( + connection_id="dummy", + inbound_connection_id=None, + their_did=TestConfig.test_target_did, + state=ConnRecord.State.COMPLETED.rfc23, + ) + mock_conn.abandon = async_mock.CoroutineMock() + report = DIDXProblemReport( + description={ + "code": ProblemReportReason.REQUEST_NOT_ACCEPTED.value, + "en": "You'll be dead!", + } + ) + await self.manager.receive_problem_report(mock_conn, report) + assert mock_conn.abandon.called_once() + + async def test_receive_problem_report_x_missing_description(self): + mock_conn = async_mock.MagicMock( + connection_id="dummy", + inbound_connection_id=None, + their_did=TestConfig.test_target_did, + state=ConnRecord.State.COMPLETED.rfc23, + ) + mock_conn.abandon = async_mock.CoroutineMock() + report = DIDXProblemReport() + with self.assertRaises(DIDXManagerError) as context: + await self.manager.receive_problem_report(mock_conn, report) + assert "Missing description" in str(context.exception) + + async def test_receive_problem_report_x_unrecognized_code(self): + mock_conn = async_mock.MagicMock( + connection_id="dummy", + inbound_connection_id=None, + their_did=TestConfig.test_target_did, + state=ConnRecord.State.COMPLETED.rfc23, + ) + mock_conn.abandon = async_mock.CoroutineMock() + report = DIDXProblemReport(description={"code": "something random"}) + with self.assertRaises(DIDXManagerError) as context: + await self.manager.receive_problem_report(mock_conn, report) + assert "unrecognized problem report" in str(context.exception) + async def test_create_did_document(self): did_info = DIDInfo( TestConfig.test_did, From 8e7c9a6b53595afa55ab75d4f55203d581b0ca27 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 2 Aug 2023 14:58:31 -0400 Subject: [PATCH 11/14] fix: overloads for base model deserialize Signed-off-by: Daniel Bluhm --- aries_cloudagent/messaging/models/base.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/aries_cloudagent/messaging/models/base.py b/aries_cloudagent/messaging/models/base.py index 79b94016bf..a63bdbe718 100644 --- a/aries_cloudagent/messaging/models/base.py +++ b/aries_cloudagent/messaging/models/base.py @@ -132,6 +132,15 @@ def Schema(self) -> Type["BaseModelSchema"]: """ return self._get_schema_class() + @overload + @classmethod + def deserialize( + cls: Type[ModelType], + obj, + ) -> ModelType: + """Convert from JSON representation to a model instance.""" + ... + @overload @classmethod def deserialize( From 88a2283c2d4d75ffac105d275a3c0d3de02131af Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 2 Aug 2023 14:59:17 -0400 Subject: [PATCH 12/14] test: didx problem report message validation Signed-off-by: Daniel Bluhm --- .../messages/tests/test_problem_report.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_problem_report.py diff --git a/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_problem_report.py b/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_problem_report.py new file mode 100644 index 0000000000..84674baf8b --- /dev/null +++ b/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_problem_report.py @@ -0,0 +1,36 @@ +import pytest + +from ......messaging.models.base import BaseModelError +from .....didcomm_prefix import DIDCommPrefix +from ...message_types import PROBLEM_REPORT +from ..problem_report import DIDXProblemReport + + +THID = "dummy-thid" +PTHID = "dummy-pthid" + + +def test_init_type(): + complete = DIDXProblemReport() + assert complete._type == DIDCommPrefix.qualify_current(PROBLEM_REPORT) + + +def test_serde(): + obj = { + "~thread": {"thid": THID, "pthid": PTHID}, + "description": {"code": "complete_not_accepted", "en": "test"}, + } + report = DIDXProblemReport.deserialize(obj) + assert report._type == DIDCommPrefix.qualify_current(PROBLEM_REPORT) + complete_dict = report.serialize() + assert complete_dict["~thread"] == obj["~thread"] + + +def test_missing_code(): + with pytest.raises(BaseModelError): + DIDXProblemReport.deserialize({"description": {"en": "test"}}) + + +def test_unrecognized_code(caplog): + DIDXProblemReport.deserialize({"description": {"code": "unknown", "en": "test"}}) + assert "Unexpected error code received" in caplog.text From 8b67001c33ded16c847950116013c0289bb47963 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 2 Aug 2023 15:14:01 -0400 Subject: [PATCH 13/14] test: didx reject endpoint Signed-off-by: Daniel Bluhm --- .../protocols/didexchange/v1_0/routes.py | 4 +- .../didexchange/v1_0/tests/test_routes.py | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/aries_cloudagent/protocols/didexchange/v1_0/routes.py b/aries_cloudagent/protocols/didexchange/v1_0/routes.py index 4734397c4f..e072cb5a2f 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/routes.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/routes.py @@ -385,13 +385,15 @@ async def didx_reject(request: web.BaseRequest): outbound_handler = request["outbound_message_router"] connection_id = request.match_info["conn_id"] + body = await request.json() + reason = body.get("reason") profile = context.profile didx_mgr = DIDXManager(profile) try: async with profile.session() as session: conn_rec = await ConnRecord.retrieve_by_id(session, connection_id) - report = await didx_mgr.reject(conn_rec=conn_rec) + report = await didx_mgr.reject(conn_rec=conn_rec, reason=reason) except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err except (StorageError, WalletError, DIDXManagerError, BaseModelError) as err: diff --git a/aries_cloudagent/protocols/didexchange/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/didexchange/v1_0/tests/test_routes.py index 8de2345b31..ff5ad5c52c 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/tests/test_routes.py @@ -262,6 +262,51 @@ async def test_didx_accept_request_x(self): with self.assertRaises(test_module.web.HTTPBadRequest): await test_module.didx_accept_request(self.request) + async def test_didx_reject(self): + self.request.match_info = {"conn_id": "dummy"} + self.request.json = async_mock.CoroutineMock(return_value={"reason": "asdf"}) + + with async_mock.patch.object( + test_module.ConnRecord, "retrieve_by_id", async_mock.CoroutineMock() + ) as mock_conn_rec_retrieve_by_id, async_mock.patch.object( + test_module, "DIDXManager", autospec=True + ) as mock_didx_mgr, async_mock.patch.object( + test_module.web, "json_response" + ) as mock_response: + mock_didx_mgr.return_value.reject = async_mock.CoroutineMock() + + await test_module.didx_reject(self.request) + + async def test_didx_reject_x_not_found(self): + self.request.match_info = {"conn_id": "dummy"} + self.request.json = async_mock.CoroutineMock(return_value={"reason": "asdf"}) + + with async_mock.patch.object( + test_module.ConnRecord, "retrieve_by_id", async_mock.CoroutineMock() + ) as mock_conn_rec_retrieve_by_id: + mock_conn_rec_retrieve_by_id.side_effect = StorageNotFoundError() + + with self.assertRaises(test_module.web.HTTPNotFound): + await test_module.didx_reject(self.request) + + async def test_didx_reject_x_bad_conn_state(self): + self.request.match_info = {"conn_id": "dummy"} + self.request.json = async_mock.CoroutineMock(return_value={"reason": "asdf"}) + + with async_mock.patch.object( + test_module.ConnRecord, "retrieve_by_id", async_mock.CoroutineMock() + ) as mock_conn_rec_retrieve_by_id, async_mock.patch.object( + test_module, "DIDXManager", autospec=True + ) as mock_didx_mgr, async_mock.patch.object( + test_module.web, "json_response" + ) as mock_response: + mock_didx_mgr.return_value.reject = async_mock.CoroutineMock( + side_effect=test_module.DIDXManagerError() + ) + + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.didx_reject(self.request) + async def test_register(self): mock_app = async_mock.MagicMock() mock_app.add_routes = async_mock.MagicMock() From c5d8c1784ff9fa0241d72e89925808973c503471 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Wed, 2 Aug 2023 15:42:15 -0400 Subject: [PATCH 14/14] fix: caplog level for test Signed-off-by: Daniel Bluhm --- .../didexchange/v1_0/messages/tests/test_problem_report.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_problem_report.py b/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_problem_report.py index 84674baf8b..03af260b1a 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_problem_report.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/messages/tests/test_problem_report.py @@ -32,5 +32,8 @@ def test_missing_code(): def test_unrecognized_code(caplog): - DIDXProblemReport.deserialize({"description": {"code": "unknown", "en": "test"}}) + with caplog.at_level("DEBUG"): + DIDXProblemReport.deserialize( + {"description": {"code": "unknown", "en": "test"}} + ) assert "Unexpected error code received" in caplog.text