Skip to content

Commit 625d047

Browse files
authored
Merge pull request #4058 from OpSecId/backport-oob-records-1.2.lts
feat: backport list endpoint for out-of-band records to 1.2.lts
2 parents c045ad8 + 5fe7a96 commit 625d047

File tree

2 files changed

+178
-1
lines changed

2 files changed

+178
-1
lines changed

acapy_agent/protocols/out_of_band/v1_0/routes.py

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from ....admin.request_context import AdminRequestContext
1919
from ....messaging.models.base import BaseModelError
2020
from ....messaging.models.openapi import OpenAPISchema
21+
from ....messaging.models.paginated_query import PaginatedQuerySchema, get_limit_offset
2122
from ....messaging.valid import UUID4_EXAMPLE, UUID4_VALIDATE
2223
from ....storage.error import StorageError, StorageNotFoundError
2324
from ...didcomm_prefix import DIDCommPrefix
@@ -26,7 +27,7 @@
2627
from .message_types import SPEC_URI
2728
from .messages.invitation import HSProto, InvitationMessage, InvitationMessageSchema
2829
from .models.invitation import InvitationRecordSchema
29-
from .models.oob_record import OobRecordSchema
30+
from .models.oob_record import OobRecord, OobRecordSchema
3031

3132
LOGGER = logging.getLogger(__name__)
3233

@@ -217,6 +218,106 @@ class InvitationRecordMatchInfoSchema(OpenAPISchema):
217218
)
218219

219220

221+
class OobRecordListQueryStringSchema(PaginatedQuerySchema):
222+
"""Parameters and validators for OOB record list request query string."""
223+
224+
state = fields.Str(
225+
required=False,
226+
validate=validate.OneOf(
227+
OobRecord.get_attributes_by_prefix("STATE_", walk_mro=True)
228+
),
229+
metadata={
230+
"description": "OOB record state",
231+
"example": OobRecord.STATE_INITIAL,
232+
},
233+
)
234+
role = fields.Str(
235+
required=False,
236+
validate=validate.OneOf(
237+
OobRecord.get_attributes_by_prefix("ROLE_", walk_mro=False)
238+
),
239+
metadata={
240+
"description": "OOB record role",
241+
"example": OobRecord.ROLE_SENDER,
242+
},
243+
)
244+
connection_id = fields.Str(
245+
required=False,
246+
validate=UUID4_VALIDATE,
247+
metadata={
248+
"description": "Connection identifier",
249+
"example": UUID4_EXAMPLE,
250+
},
251+
)
252+
invi_msg_id = fields.Str(
253+
required=False,
254+
validate=UUID4_VALIDATE,
255+
metadata={
256+
"description": "Invitation message identifier",
257+
"example": UUID4_EXAMPLE,
258+
},
259+
)
260+
261+
262+
class OobRecordListSchema(OpenAPISchema):
263+
"""Result schema for OOB record list."""
264+
265+
results = fields.List(
266+
fields.Nested(OobRecordSchema()),
267+
required=True,
268+
metadata={"description": "List of OOB records"},
269+
)
270+
271+
272+
@docs(
273+
tags=["out-of-band"],
274+
summary="Query OOB records",
275+
)
276+
@querystring_schema(OobRecordListQueryStringSchema())
277+
@response_schema(OobRecordListSchema(), 200, description="")
278+
@tenant_authentication
279+
async def oob_records_list(request: web.BaseRequest):
280+
"""Request handler for searching OOB records.
281+
282+
Args:
283+
request: aiohttp request object
284+
285+
Returns:
286+
The OOB record list response
287+
288+
"""
289+
context: AdminRequestContext = request["context"]
290+
291+
tag_filter = {
292+
k: request.query[k]
293+
for k in ("connection_id", "invi_msg_id")
294+
if request.query.get(k, "") != ""
295+
}
296+
post_filter = {
297+
k: request.query[k]
298+
for k in ("state", "role")
299+
if request.query.get(k, "") != ""
300+
}
301+
302+
limit, offset = get_limit_offset(request)
303+
304+
profile = context.profile
305+
try:
306+
async with profile.session() as session:
307+
records = await OobRecord.query(
308+
session,
309+
tag_filter,
310+
limit=limit,
311+
offset=offset,
312+
post_filter_positive=post_filter,
313+
)
314+
results = [record.serialize() for record in records]
315+
except (StorageError, BaseModelError) as err:
316+
raise web.HTTPBadRequest(reason=err.roll_up) from err
317+
318+
return web.json_response({"results": results})
319+
320+
220321
@docs(
221322
tags=["out-of-band"],
222323
summary="Create a new connection invitation",
@@ -365,6 +466,11 @@ async def register(app: web.Application):
365466
[
366467
web.post("/out-of-band/create-invitation", invitation_create),
367468
web.post("/out-of-band/receive-invitation", invitation_receive),
469+
web.get(
470+
"/out-of-band/records",
471+
oob_records_list,
472+
allow_head=False,
473+
),
368474
web.delete("/out-of-band/invitations/{invi_msg_id}", invitation_remove),
369475
]
370476
)

acapy_agent/protocols/out_of_band/v1_0/tests/test_routes.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from .....admin.request_context import AdminRequestContext
44
from .....connections.models.conn_record import ConnRecord
5+
from .....storage.error import StorageError
56
from .....tests import mock
67
from .....utils.testing import create_test_profile
78
from .. import routes as test_module
@@ -211,6 +212,76 @@ async def test_invitation_receive_x(self):
211212
with self.assertRaises(test_module.web.HTTPBadRequest):
212213
await test_module.invitation_receive(self.request)
213214

215+
async def test_oob_records_list(self):
216+
mock_record = mock.MagicMock(
217+
serialize=mock.MagicMock(return_value={"oob_id": "test"})
218+
)
219+
with (
220+
mock.patch.object(
221+
test_module.OobRecord,
222+
"query",
223+
mock.CoroutineMock(return_value=[mock_record]),
224+
),
225+
mock.patch.object(
226+
test_module.web, "json_response", mock.Mock()
227+
) as mock_json_response,
228+
):
229+
await test_module.oob_records_list(self.request)
230+
mock_json_response.assert_called_once_with(
231+
{"results": [{"oob_id": "test"}]}
232+
)
233+
234+
async def test_oob_records_list_with_filters(self):
235+
self.request.query = {
236+
"state": "initial",
237+
"role": "sender",
238+
"connection_id": "test-conn-id",
239+
"invi_msg_id": "test-invi-id",
240+
}
241+
with (
242+
mock.patch.object(
243+
test_module.OobRecord,
244+
"query",
245+
mock.CoroutineMock(return_value=[]),
246+
) as mock_query,
247+
mock.patch.object(
248+
test_module.web, "json_response", mock.Mock()
249+
) as mock_json_response,
250+
):
251+
await test_module.oob_records_list(self.request)
252+
mock_query.assert_called_once()
253+
call_kwargs = mock_query.call_args
254+
tag_filter = call_kwargs[0][1]
255+
assert "connection_id" in tag_filter
256+
assert "invi_msg_id" in tag_filter
257+
assert "state" not in tag_filter
258+
mock_json_response.assert_called_once_with({"results": []})
259+
260+
async def test_oob_records_list_with_pagination(self):
261+
self.request.query = {"limit": "10", "offset": "5"}
262+
with (
263+
mock.patch.object(
264+
test_module.OobRecord,
265+
"query",
266+
mock.CoroutineMock(return_value=[]),
267+
) as mock_query,
268+
mock.patch.object(test_module.web, "json_response", mock.Mock()),
269+
):
270+
await test_module.oob_records_list(self.request)
271+
mock_query.assert_called_once()
272+
call_kwargs = mock_query.call_args[1]
273+
assert call_kwargs["limit"] == 10
274+
assert call_kwargs["offset"] == 5
275+
276+
async def test_oob_records_list_storage_error(self):
277+
with mock.patch.object(
278+
test_module.OobRecord,
279+
"query",
280+
mock.CoroutineMock(side_effect=StorageError("test error")),
281+
):
282+
with self.assertRaises(test_module.web.HTTPBadRequest):
283+
await test_module.oob_records_list(self.request)
284+
214285
async def test_register(self):
215286
mock_app = mock.MagicMock()
216287
mock_app.add_routes = mock.MagicMock()

0 commit comments

Comments
 (0)