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

Connection and DIDX Problem Reports #2653

Merged
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
10 changes: 5 additions & 5 deletions aries_cloudagent/messaging/responder.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,26 @@
"""
import asyncio
import json

from abc import ABC, abstractmethod
from typing import List, Sequence, Union, Optional, Tuple
from typing import List, Optional, Sequence, Tuple, Union

from ..cache.base import BaseCache
from ..connections.models.connection_target import ConnectionTarget
from ..connections.models.conn_record import ConnRecord
from ..connections.models.connection_target import ConnectionTarget
from ..core.error import BaseError
from ..core.profile import Profile
from ..transport.outbound.message import OutboundMessage

from .base_message import BaseMessage
from ..transport.outbound.status import OutboundSendStatus
from .base_message import BaseMessage

SKIP_ACTIVE_CONN_CHECK_MSG_TYPES = [
"didexchange/1.0/request",
"didexchange/1.0/response",
"didexchange/1.0/problem_report",
"connections/1.0/invitation",
"connections/1.0/request",
"connections/1.0/response",
"connections/1.0/problem_report",
]


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ async def handle(self, context: RequestContext, responder: BaseResponder):
),
}
)
report.assign_thread_from(context.message)
# client likely needs to be using direct responses to receive the problem report
await responder.send_reply(report)
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from ....coordinate_mediation.v1_0.manager import MediationManager
from ..manager import ConnectionManager, ConnectionManagerError
from ..messages.connection_request import ConnectionRequest
from ..messages.problem_report import ConnectionProblemReport


class ConnectionRequestHandler(BaseHandler):
Expand Down Expand Up @@ -49,22 +48,11 @@ async def handle(self, context: RequestContext, responder: BaseResponder):
else:
self._logger.debug("Connection request will await acceptance")
except ConnectionManagerError as e:
self._logger.exception("Error receiving connection request")
if e.error_code:
targets = None
if context.message.connection and context.message.connection.did_doc:
try:
targets = mgr.diddoc_connection_targets(
context.message.connection.did_doc,
context.message_receipt.recipient_verkey,
)
except ConnectionManagerError:
self._logger.exception(
"Error parsing DIDDoc for problem report"
)
report, targets = mgr.manager_error_to_problem_report(
e, context.message, context.message_receipt
)
if report and targets:
await responder.send_reply(
ConnectionProblemReport(
description={"en": e.message, "code": e.error_code}
),
message=report,
target_list=targets,
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from .....protocols.trustping.v1_0.messages.ping import Ping
from ..manager import ConnectionManager, ConnectionManagerError
from ..messages.connection_response import ConnectionResponse
from ..messages.problem_report import ConnectionProblemReport


class ConnectionResponseHandler(BaseHandler):
Expand All @@ -31,27 +30,16 @@ async def handle(self, context: RequestContext, responder: BaseResponder):
context.message, context.message_receipt
)
except ConnectionManagerError as e:
self._logger.exception("Error receiving connection response")
if e.error_code:
targets = None
if context.message.connection and context.message.connection.did_doc:
try:
targets = mgr.diddoc_connection_targets(
context.message.connection.did_doc,
context.message_receipt.recipient_verkey,
)
except ConnectionManagerError:
self._logger.exception(
"Error parsing DIDDoc for problem report"
)
report, targets = mgr.manager_error_to_problem_report(
e, context.message, context.message_receipt
)
if report and targets:
await responder.send_reply(
ConnectionProblemReport(
description={"en": e.message, "code": e.error_code}
),
message=report,
target_list=targets,
)
return

# send trust ping in response
if context.settings.get("auto_ping_connection"):
await responder.send(Ping(), connection_id=connection.connection_id)
await responder.send(Ping(), connection_id=connection.connection_id),
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"""Problem report handler for Connection Protocol."""

from .....connections.models.conn_record import ConnRecord
from .....messaging.base_handler import (
BaseHandler,
BaseResponder,
HandlerException,
RequestContext,
)
from .....storage.error import StorageNotFoundError
from ..manager import ConnectionManager, ConnectionManagerError
from ..messages.problem_report import ConnectionProblemReport

Expand All @@ -24,10 +26,19 @@ async def handle(self, context: RequestContext, responder: BaseResponder):
profile = context.profile
mgr = ConnectionManager(profile)
try:
if context.connection_record:
await mgr.receive_problem_report(
context.connection_record, context.message
)
conn_rec = context.connection_record
if not conn_rec:
# try to find connection by thread_id/request_id
try:
async with profile.session() as session:
conn_rec = await ConnRecord.retrieve_by_request_id(
session, context.message._thread_id
)
except StorageNotFoundError:
pass

if conn_rec:
await mgr.receive_problem_report(conn_rec, context.message)
else:
raise HandlerException("No connection established for problem report")
except ConnectionManagerError:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,11 +159,25 @@ async def test_connection_record_without_mediation_metadata(

@pytest.mark.asyncio
@mock.patch.object(handler, "ConnectionManager")
async def test_problem_report(self, mock_conn_mgr, request_context):
@mock.patch.object(connection_target, "ConnectionTarget")
async def test_problem_report(
self, mock_conn_target, mock_conn_mgr, request_context
):
mock_conn_mgr.return_value.receive_request = mock.CoroutineMock()
mock_conn_mgr.return_value.receive_request.side_effect = ConnectionManagerError(
error_code=ProblemReportReason.REQUEST_NOT_ACCEPTED.value
)
mock_conn_mgr.return_value.manager_error_to_problem_report = mock.MagicMock(
return_value=(
ConnectionProblemReport(
description={
"en": "test error",
"code": ProblemReportReason.REQUEST_NOT_ACCEPTED.value,
}
),
[mock_conn_target],
)
)
request_context.message = ConnectionRequest()
handler_inst = handler.ConnectionRequestHandler()
responder = MockResponder()
Expand All @@ -179,7 +193,7 @@ async def test_problem_report(self, mock_conn_mgr, request_context):
== ProblemReportReason.REQUEST_NOT_ACCEPTED.value
)
)
assert target == {"target_list": None}
assert target == {"target_list": [mock_conn_target]}

@pytest.mark.asyncio
@mock.patch.object(handler, "ConnectionManager")
Expand All @@ -194,6 +208,17 @@ async def test_problem_report_did_doc(
mock_conn_mgr.return_value.diddoc_connection_targets = mock.MagicMock(
return_value=[mock_conn_target]
)
mock_conn_mgr.return_value.manager_error_to_problem_report = mock.MagicMock(
return_value=(
ConnectionProblemReport(
description={
"en": "test error",
"code": ProblemReportReason.REQUEST_NOT_ACCEPTED.value,
}
),
[mock_conn_target],
)
)
request_context.message = ConnectionRequest(
connection=ConnectionDetail(did=TEST_DID, did_doc=did_doc),
label=TEST_LABEL,
Expand Down Expand Up @@ -228,6 +253,17 @@ async def test_problem_report_did_doc_no_conn_target(
mock_conn_mgr.return_value.diddoc_connection_targets = mock.MagicMock(
side_effect=ConnectionManagerError("no targets")
)
mock_conn_mgr.return_value.manager_error_to_problem_report = mock.MagicMock(
return_value=(
ConnectionProblemReport(
description={
"en": "test error",
"code": ProblemReportReason.REQUEST_NOT_ACCEPTED.value,
}
),
None,
)
)
request_context.message = ConnectionRequest(
connection=ConnectionDetail(did=TEST_DID, did_doc=did_doc),
label=TEST_LABEL,
Expand All @@ -237,14 +273,4 @@ async def test_problem_report_did_doc_no_conn_target(
responder = MockResponder()
await handler_inst.handle(request_context, responder)
messages = responder.messages
assert len(messages) == 1
result, target = messages[0]
assert (
isinstance(result, ConnectionProblemReport)
and result.description
and (
result.description["code"]
== ProblemReportReason.REQUEST_NOT_ACCEPTED.value
)
)
assert target == {"target_list": None}
assert len(messages) == 0 # messages require a target!
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,25 @@ async def test_called_auto_ping(self, mock_conn_mgr, request_context):

@pytest.mark.asyncio
@mock.patch.object(handler, "ConnectionManager")
async def test_problem_report(self, mock_conn_mgr, request_context):
@mock.patch.object(connection_target, "ConnectionTarget")
async def test_problem_report(
self, mock_conn_target, mock_conn_mgr, request_context
):
mock_conn_mgr.return_value.accept_response = mock.CoroutineMock()
mock_conn_mgr.return_value.accept_response.side_effect = ConnectionManagerError(
error_code=ProblemReportReason.RESPONSE_NOT_ACCEPTED.value,
)
mock_conn_mgr.return_value.manager_error_to_problem_report = mock.MagicMock(
return_value=(
ConnectionProblemReport(
description={
"en": "test error",
"code": ProblemReportReason.RESPONSE_NOT_ACCEPTED.value,
}
),
[mock_conn_target],
)
)
request_context.message = ConnectionResponse()
handler_inst = handler.ConnectionResponseHandler()
responder = MockResponder()
Expand All @@ -116,7 +130,7 @@ async def test_problem_report(self, mock_conn_mgr, request_context):
== ProblemReportReason.RESPONSE_NOT_ACCEPTED.value
)
)
assert target == {"target_list": None}
assert target == {"target_list": [mock_conn_target]}

@pytest.mark.asyncio
@mock.patch.object(handler, "ConnectionManager")
Expand All @@ -131,6 +145,17 @@ async def test_problem_report_did_doc(
mock_conn_mgr.return_value.diddoc_connection_targets = mock.MagicMock(
return_value=[mock_conn_target]
)
mock_conn_mgr.return_value.manager_error_to_problem_report = mock.MagicMock(
return_value=(
ConnectionProblemReport(
description={
"en": "test error",
"code": ProblemReportReason.RESPONSE_NOT_ACCEPTED.value,
}
),
[mock_conn_target],
)
)
request_context.message = ConnectionResponse(
connection=ConnectionDetail(did=TEST_DID, did_doc=did_doc)
)
Expand Down Expand Up @@ -163,21 +188,22 @@ async def test_problem_report_did_doc_no_conn_target(
mock_conn_mgr.return_value.diddoc_connection_targets = mock.MagicMock(
side_effect=ConnectionManagerError("no target")
)
mock_conn_mgr.return_value.manager_error_to_problem_report = mock.MagicMock(
return_value=(
ConnectionProblemReport(
description={
"en": "test error",
"code": ProblemReportReason.RESPONSE_NOT_ACCEPTED.value,
}
),
None,
)
)
request_context.message = ConnectionResponse(
connection=ConnectionDetail(did=TEST_DID, did_doc=did_doc)
)
handler_inst = handler.ConnectionResponseHandler()
responder = MockResponder()
await handler_inst.handle(request_context, responder)
messages = responder.messages
assert len(messages) == 1
result, target = messages[0]
assert (
isinstance(result, ConnectionProblemReport)
and result.description
and (
result.description["code"]
== ProblemReportReason.RESPONSE_NOT_ACCEPTED.value
)
)
assert target == {"target_list": None}
assert len(messages) == 0 # need a connection target to send message
29 changes: 28 additions & 1 deletion aries_cloudagent/protocols/connections/v1_0/manager.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Classes to manage connections."""

import logging
from typing import Optional, Sequence, Tuple, cast
from typing import Optional, Sequence, Tuple, Union, cast

from ....connections.base_manager import BaseConnectionManager
from ....connections.models.conn_record import ConnRecord
from ....connections.models.connection_target import ConnectionTarget
from ....core.error import BaseError
from ....core.oob_processor import OobMessageProcessor
from ....core.profile import Profile
Expand Down Expand Up @@ -780,3 +781,29 @@ async def receive_problem_report(
raise ConnectionManagerError(
f"Received unrecognized problem report: {report.description}"
)

def manager_error_to_problem_report(
self,
e: ConnectionManagerError,
message: Union[ConnectionRequest, ConnectionResponse],
message_receipt,
) -> tuple[ConnectionProblemReport, Sequence[ConnectionTarget]]:
"""Convert ConnectionManagerError to problem report."""
self._logger.exception("Error receiving connection request")
targets = None
report = None
if e.error_code:
report = ConnectionProblemReport(
description={"en": e.message, "code": e.error_code}
)
report.assign_thread_from(message)
if message.connection and message.connection.did_doc:
try:
targets = self.diddoc_connection_targets(
message.connection.did_doc,
message_receipt.recipient_verkey,
)
except ConnectionManagerError:
self._logger.exception("Error parsing DIDDoc for problem report")

return report, targets
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
BaseResponder,
RequestContext,
)

from ....out_of_band.v1_0.messages.invitation import InvitationMessage

from ..messages.problem_report import DIDXProblemReport, ProblemReportReason


Expand All @@ -34,6 +32,6 @@ async def handle(self, context: RequestContext, responder: BaseResponder):
),
}
)

report.assign_thread_from(context.message)
# client likely needs to be using direct responses to receive the problem report
await responder.send_reply(report)
Loading