Skip to content
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
111 changes: 110 additions & 1 deletion acapy_agent/protocols/out_of_band/v1_0/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
from ....admin.request_context import AdminRequestContext
from ....messaging.models.base import BaseModelError
from ....messaging.models.openapi import OpenAPISchema
from ....messaging.models.paginated_query import (
PaginatedQuerySchema,
get_paginated_query_params,
)
from ....messaging.valid import UUID4_EXAMPLE, UUID4_VALIDATE
from ....storage.error import StorageError, StorageNotFoundError
from ...didcomm_prefix import DIDCommPrefix
Expand All @@ -26,7 +30,7 @@
from .message_types import SPEC_URI
from .messages.invitation import HSProto, InvitationMessage, InvitationMessageSchema
from .models.invitation import InvitationRecordSchema
from .models.oob_record import OobRecordSchema
from .models.oob_record import OobRecord, OobRecordSchema

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -240,6 +244,106 @@ class OobInvitationRecordMatchInfoSchema(OpenAPISchema):
)


class OobRecordListQueryStringSchema(PaginatedQuerySchema):
"""Parameters and validators for OOB record list request query string."""

state = fields.Str(
required=False,
validate=validate.OneOf(
OobRecord.get_attributes_by_prefix("STATE_", walk_mro=True)
),
metadata={
"description": "OOB record state",
"example": OobRecord.STATE_INITIAL,
},
)
role = fields.Str(
required=False,
validate=validate.OneOf(
OobRecord.get_attributes_by_prefix("ROLE_", walk_mro=False)
),
metadata={
"description": "OOB record role",
"example": OobRecord.ROLE_SENDER,
},
)
connection_id = fields.Str(
required=False,
validate=UUID4_VALIDATE,
metadata={
"description": "Connection identifier",
"example": UUID4_EXAMPLE,
},
)
invi_msg_id = fields.Str(
required=False,
validate=UUID4_VALIDATE,
metadata={
"description": "Invitation message identifier",
"example": UUID4_EXAMPLE,
},
)


class OobRecordListSchema(OpenAPISchema):
"""Result schema for OOB record list."""

results = fields.List(
fields.Nested(OobRecordSchema()),
required=True,
metadata={"description": "List of OOB records"},
)


@docs(
tags=["out-of-band"],
summary="Query OOB records",
)
@querystring_schema(OobRecordListQueryStringSchema())
@response_schema(OobRecordListSchema(), 200, description="")
@tenant_authentication
async def oob_records_list(request: web.BaseRequest):
"""Request handler for searching OOB records.

Args:
request: aiohttp request object

Returns:
The OOB record list response

"""
context: AdminRequestContext = request["context"]

tag_filter = {
k: request.query[k]
for k in ("state", "connection_id", "invi_msg_id")
if request.query.get(k, "") != ""
}
post_filter = {
k: request.query[k] for k in ("role",) if request.query.get(k, "") != ""
}

limit, offset, order_by, descending = get_paginated_query_params(request)

profile = context.profile
try:
async with profile.session() as session:
records = await OobRecord.query(
session,
tag_filter,
limit=limit,
offset=offset,
order_by=order_by,
descending=descending,
post_filter_positive=post_filter,
)
results = [record.serialize() for record in records]
except (StorageError, BaseModelError) as err:
raise web.HTTPBadRequest(reason=err.roll_up) from err

return web.json_response({"results": results})


@docs(tags=["out-of-band"], summary="Fetch an existing Out-of-Band invitation.")
@querystring_schema(OobIdQueryStringSchema())
@response_schema(InvitationRecordResponseSchema(), description="")
Expand Down Expand Up @@ -413,6 +517,11 @@ async def register(app: web.Application):
[
web.post("/out-of-band/create-invitation", invitation_create),
web.post("/out-of-band/receive-invitation", invitation_receive),
web.get(
"/out-of-band/records",
oob_records_list,
allow_head=False,
),
web.get(
"/out-of-band/invitations",
invitation_fetch,
Expand Down
81 changes: 81 additions & 0 deletions acapy_agent/protocols/out_of_band/v1_0/tests/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,87 @@ async def test_invitation_create(self):
)
mock_json_response.assert_called_once_with({"abc": "123"})

async def test_oob_records_list(self):
with (
mock.patch.object(
test_module.OobRecord, "query", mock.CoroutineMock()
) as mock_query,
mock.patch.object(
test_module.web, "json_response", mock.Mock()
) as mock_json_response,
):
mock_query.return_value = [
mock.MagicMock(serialize=mock.MagicMock(return_value={"oob_id": "1"})),
mock.MagicMock(serialize=mock.MagicMock(return_value={"oob_id": "2"})),
]

await test_module.oob_records_list(self.request)
mock_query.assert_called_once()
mock_json_response.assert_called_once_with(
{"results": [{"oob_id": "1"}, {"oob_id": "2"}]}
)

async def test_oob_records_list_with_filters(self):
self.request.query = {
"state": "initial",
"role": "sender",
"connection_id": "test-conn-id",
"invi_msg_id": "test-invi-id",
}

with (
mock.patch.object(
test_module.OobRecord, "query", mock.CoroutineMock()
) as mock_query,
mock.patch.object(
test_module.web, "json_response", mock.Mock()
) as mock_json_response,
):
mock_query.return_value = []

await test_module.oob_records_list(self.request)
mock_query.assert_called_once()
call_kwargs = mock_query.call_args
# tag_filter is the second positional arg
tag_filter = call_kwargs[0][1]
assert tag_filter == {
"state": "initial",
"connection_id": "test-conn-id",
"invi_msg_id": "test-invi-id",
}
assert call_kwargs[1]["post_filter_positive"] == {
"role": "sender",
}
mock_json_response.assert_called_once_with({"results": []})

async def test_oob_records_list_with_pagination(self):
self.request.query = {
"limit": "10",
"offset": "5",
}

with (
mock.patch.object(
test_module.OobRecord, "query", mock.CoroutineMock()
) as mock_query,
mock.patch.object(test_module.web, "json_response", mock.Mock()),
):
mock_query.return_value = []

await test_module.oob_records_list(self.request)
call_kwargs = mock_query.call_args[1]
assert call_kwargs["limit"] == 10
assert call_kwargs["offset"] == 5

async def test_oob_records_list_storage_error(self):
with mock.patch.object(
test_module.OobRecord,
"query",
mock.CoroutineMock(side_effect=test_module.StorageError("test error")),
):
with self.assertRaises(test_module.web.HTTPBadRequest):
await test_module.oob_records_list(self.request)

async def test_invitation_fetch(self):
self.request.query = {"oob_id": "dummy"}

Expand Down
Loading