From b27f6590ad85e9d2b55bf73a7c6311bc9797823e Mon Sep 17 00:00:00 2001 From: jamshale Date: Mon, 24 Jun 2024 11:41:30 -0700 Subject: [PATCH 1/5] Use anoncreds registry for holder credential endpoints Signed-off-by: jamshale --- aries_cloudagent/anoncreds/base.py | 6 +- .../anoncreds/default/did_indy/registry.py | 6 +- .../anoncreds/default/legacy_indy/registry.py | 16 ++- aries_cloudagent/anoncreds/holder.py | 33 +++-- aries_cloudagent/anoncreds/registry.py | 16 ++- aries_cloudagent/anoncreds/verifier.py | 2 +- aries_cloudagent/holder/routes.py | 123 ++++++++++++------ 7 files changed, 134 insertions(+), 68 deletions(-) diff --git a/aries_cloudagent/anoncreds/base.py b/aries_cloudagent/anoncreds/base.py index 2e0fc87c34..d3e32c8ea6 100644 --- a/aries_cloudagent/anoncreds/base.py +++ b/aries_cloudagent/anoncreds/base.py @@ -124,7 +124,11 @@ async def get_revocation_registry_definition( @abstractmethod async def get_revocation_list( - self, profile: Profile, revocation_registry_id: str, timestamp: int + self, + profile: Profile, + revocation_registry_id: str, + timestamp_from: Optional[int] = 0, + timestamp_to: Optional[int] = None, ) -> GetRevListResult: """Get a revocation list from the registry.""" diff --git a/aries_cloudagent/anoncreds/default/did_indy/registry.py b/aries_cloudagent/anoncreds/default/did_indy/registry.py index 6099ed574b..567d90ccce 100644 --- a/aries_cloudagent/anoncreds/default/did_indy/registry.py +++ b/aries_cloudagent/anoncreds/default/did_indy/registry.py @@ -88,7 +88,11 @@ async def register_revocation_registry_definition( raise NotImplementedError() async def get_revocation_list( - self, profile: Profile, revocation_registry_id: str, timestamp: int + self, + profile: Profile, + revocation_registry_id: str, + timestamp_from: Optional[int] = 0, + timestamp_to: Optional[int] = None, ) -> GetRevListResult: """Get a revocation list from the registry.""" raise NotImplementedError() diff --git a/aries_cloudagent/anoncreds/default/legacy_indy/registry.py b/aries_cloudagent/anoncreds/default/legacy_indy/registry.py index ba9e2f3cc1..1e3381f742 100644 --- a/aries_cloudagent/anoncreds/default/legacy_indy/registry.py +++ b/aries_cloudagent/anoncreds/default/legacy_indy/registry.py @@ -740,14 +740,18 @@ async def _get_ledger(self, profile: Profile, rev_reg_def_id: str): return ledger_id, ledger async def get_revocation_registry_delta( - self, profile: Profile, rev_reg_def_id: str, timestamp: None + self, + profile: Profile, + rev_reg_def_id: str, + timestamp_from: Optional[int] = 0, + timestamp_to: Optional[int] = None, ) -> Tuple[dict, int]: """Fetch the revocation registry delta.""" ledger_id, ledger = await self._get_ledger(profile, rev_reg_def_id) async with ledger: delta, timestamp = await ledger.get_revoc_reg_delta( - rev_reg_def_id, timestamp_to=timestamp + rev_reg_def_id, timestamp_from=timestamp_from, timestamp_to=timestamp_to ) if delta is None: @@ -759,13 +763,17 @@ async def get_revocation_registry_delta( return delta, timestamp async def get_revocation_list( - self, profile: Profile, rev_reg_def_id: str, timestamp: int + self, + profile: Profile, + rev_reg_def_id: str, + timestamp_from: Optional[int] = 0, + timestamp_to: Optional[int] = None, ) -> GetRevListResult: """Get the revocation registry list.""" _, ledger = await self._get_ledger(profile, rev_reg_def_id) delta, timestamp = await self.get_revocation_registry_delta( - profile, rev_reg_def_id, timestamp + profile, rev_reg_def_id, timestamp_from, timestamp_to ) async with ledger: diff --git a/aries_cloudagent/anoncreds/holder.py b/aries_cloudagent/anoncreds/holder.py index 2c08bbf188..7e177d0cfb 100644 --- a/aries_cloudagent/anoncreds/holder.py +++ b/aries_cloudagent/anoncreds/holder.py @@ -23,10 +23,10 @@ from ..askar.profile_anon import AskarAnoncredsProfile from ..core.error import BaseError from ..core.profile import Profile -from ..ledger.base import BaseLedger from ..wallet.error import WalletNotFoundError from .error_messages import ANONCREDS_PROFILE_REQUIRED_MSG from .models.anoncreds_cred_def import CredDef +from .registry import AnonCredsRegistry LOGGER = logging.getLogger(__name__) @@ -477,7 +477,7 @@ async def _get_credential(self, credential_id: str) -> Credential: raise AnonCredsHolderError("Error loading requested credential") from err async def credential_revoked( - self, ledger: BaseLedger, credential_id: str, fro: int = None, to: int = None + self, credential_id: str, fro: int = None, to: int = None ) -> bool: """Check ledger for revocation status of credential by cred id. @@ -488,18 +488,18 @@ async def credential_revoked( cred = await self._get_credential(credential_id) rev_reg_id = cred.rev_reg_id - # TODO Use anoncreds registry - # check if cred.rev_reg_id is returning None or 'None' - if rev_reg_id: - cred_rev_id = cred.rev_reg_index - (rev_reg_delta, _) = await ledger.get_revoc_reg_delta( - rev_reg_id, - fro, - to, + anoncreds_registry = self.profile.inject(AnonCredsRegistry) + rev_list = ( + await anoncreds_registry.get_revocation_list( + self.profile, rev_reg_id, fro, to ) - return cred_rev_id in rev_reg_delta["value"].get("revoked", []) - else: - return False + ).revocation_list + + set_revoked = { + index for index, value in enumerate(rev_list.revocation_list) if value == 1 + } + + return cred.rev_reg_index in set_revoked async def delete_credential(self, credential_id: str): """Remove a credential stored in the wallet. @@ -515,10 +515,9 @@ async def delete_credential(self, credential_id: str): AnonCredsHolder.RECORD_TYPE_MIME_TYPES, credential_id ) except AskarError as err: - if err.code == AskarErrorCode.NOT_FOUND: - pass - else: - raise AnonCredsHolderError("Error deleting credential") from err + raise AnonCredsHolderError( + "Error deleting credential", error_code=err.code + ) from err # noqa: E501 async def get_mime_type( self, credential_id: str, attr: str = None diff --git a/aries_cloudagent/anoncreds/registry.py b/aries_cloudagent/anoncreds/registry.py index 38535e1bfe..8a920412d2 100644 --- a/aries_cloudagent/anoncreds/registry.py +++ b/aries_cloudagent/anoncreds/registry.py @@ -11,11 +11,7 @@ BaseAnonCredsRegistrar, BaseAnonCredsResolver, ) -from .models.anoncreds_cred_def import ( - CredDef, - CredDefResult, - GetCredDefResult, -) +from .models.anoncreds_cred_def import CredDef, CredDefResult, GetCredDefResult from .models.anoncreds_revocation import ( GetRevListResult, GetRevRegDefResult, @@ -149,11 +145,17 @@ async def register_revocation_registry_definition( ) async def get_revocation_list( - self, profile: Profile, rev_reg_def_id: str, timestamp: int + self, + profile: Profile, + rev_reg_def_id: str, + timestamp_from: Optional[int] = 0, + timestamp_to: Optional[int] = None, ) -> GetRevListResult: """Get a revocation list from the registry.""" resolver = await self._resolver_for_identifier(rev_reg_def_id) - return await resolver.get_revocation_list(profile, rev_reg_def_id, timestamp) + return await resolver.get_revocation_list( + profile, rev_reg_def_id, timestamp_from, timestamp_to + ) async def register_revocation_list( self, diff --git a/aries_cloudagent/anoncreds/verifier.py b/aries_cloudagent/anoncreds/verifier.py index f320d82013..2dbae52f24 100644 --- a/aries_cloudagent/anoncreds/verifier.py +++ b/aries_cloudagent/anoncreds/verifier.py @@ -423,7 +423,7 @@ async def process_pres_identifiers( result = await anoncreds_registry.get_revocation_list( self.profile, identifier["rev_reg_id"], - identifier["timestamp"], + timestamp_to=identifier["timestamp"], ) rev_lists[identifier["rev_reg_id"]][ identifier["timestamp"] diff --git a/aries_cloudagent/holder/routes.py b/aries_cloudagent/holder/routes.py index 97d354497d..a8e3834e2c 100644 --- a/aries_cloudagent/holder/routes.py +++ b/aries_cloudagent/holder/routes.py @@ -1,6 +1,7 @@ """Holder admin routes.""" import json +from profile import Profile from aiohttp import web from aiohttp_apispec import ( @@ -10,10 +11,12 @@ request_schema, response_schema, ) +from aries_askar import AskarErrorCode from marshmallow import fields from ..admin.decorators.auth import tenant_authentication from ..admin.request_context import AdminRequestContext +from ..anoncreds.holder import AnonCredsHolder, AnonCredsHolderError from ..indy.holder import IndyHolder, IndyHolderError from ..indy.models.cred_precis import IndyCredInfoSchema from ..ledger.base import BaseLedger @@ -207,7 +210,11 @@ async def credentials_get(request: web.BaseRequest): context: AdminRequestContext = request["context"] credential_id = request.match_info["credential_id"] - holder = context.profile.inject(IndyHolder) + if context.settings.get("wallet.type") == "askar-anoncreds": + holder = AnonCredsHolder(context.profile) + else: + holder = context.profile.inject(IndyHolder) + try: credential = await holder.get_credential(credential_id) except WalletNotFoundError as err: @@ -236,28 +243,43 @@ async def credentials_revoked(request: web.BaseRequest): credential_id = request.match_info["credential_id"] fro = request.query.get("from") to = request.query.get("to") + profile = context.profile + wallet_type = profile.settings.get_value("wallet.type") + + async def get_revoked_using_anoncreds(profile: Profile): + holder = AnonCredsHolder(profile) + return await holder.credential_revoked( + credential_id, + int(fro) if fro else None, + int(to) if to else None, + ) - async with context.profile.session() as session: - ledger = session.inject_or(BaseLedger) - if not ledger: - reason = "No ledger available" - if not context.settings.get_value("wallet.type"): - reason += ": missing wallet-type?" - raise web.HTTPForbidden(reason=reason) - - async with ledger: - try: - holder = session.inject(IndyHolder) - revoked = await holder.credential_revoked( - ledger, - credential_id, - int(fro) if fro else None, - int(to) if to else None, - ) - except WalletNotFoundError as err: - raise web.HTTPNotFound(reason=err.roll_up) from err - except LedgerError as err: - raise web.HTTPBadRequest(reason=err.roll_up) from err + async def get_revoked_using_indy(profile: Profile): + async with profile.session() as session: + holder = session.inject(IndyHolder) + + ledger = session.inject_or(BaseLedger) + if not ledger: + raise web.HTTPForbidden(reason="No ledger available") + + async with ledger: + try: + return await holder.credential_revoked( + ledger, + credential_id, + int(fro) if fro else None, + int(to) if to else None, + ) + except LedgerError as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + + try: + if wallet_type == "askar-anoncreds": + revoked = await get_revoked_using_anoncreds(profile) + else: + revoked = await get_revoked_using_indy(profile) + except WalletNotFoundError as err: + raise web.HTTPNotFound(reason=err.roll_up) from err return web.json_response({"revoked": revoked}) @@ -279,9 +301,14 @@ async def credentials_attr_mime_types_get(request: web.BaseRequest): context: AdminRequestContext = request["context"] credential_id = request.match_info["credential_id"] - async with context.profile.session() as session: - holder = session.inject(IndyHolder) + if context.settings.get("wallet.type") == "askar-anoncreds": + holder = AnonCredsHolder(context.profile) mime_types = await holder.get_mime_type(credential_id) + else: + async with context.profile.session() as session: + holder = session.inject(IndyHolder) + mime_types = await holder.get_mime_type(credential_id) + return web.json_response({"results": mime_types}) @@ -301,15 +328,33 @@ async def credentials_remove(request: web.BaseRequest): """ context: AdminRequestContext = request["context"] credential_id = request.match_info["credential_id"] + profile: Profile = context.profile - try: - async with context.profile.session() as session: - holder = session.inject(IndyHolder) + async def delete_credential_using_anoncreds(profile: Profile): + try: + holder = AnonCredsHolder(profile) await holder.delete_credential(credential_id) - topic = "acapy::record::credential::delete" - await context.profile.notify(topic, {"id": credential_id, "state": "deleted"}) - except WalletNotFoundError as err: - raise web.HTTPNotFound(reason=err.roll_up) from err + except AnonCredsHolderError as err: + if err.error_code == AskarErrorCode.NOT_FOUND: + raise web.HTTPNotFound(reason=err.roll_up) from err + raise web.HTTPBadRequest(reason=err.roll_up) from err + + async def delete_credential_using_indy(profile: Profile): + async with profile.session() as session: + try: + holder = session.inject(IndyHolder) + await holder.delete_credential(credential_id) + except WalletNotFoundError as err: + raise web.HTTPNotFound(reason=err.roll_up) from err + + if context.settings.get("wallet.type") == "askar-anoncreds": + await delete_credential_using_anoncreds(profile) + else: + await delete_credential_using_indy(profile) + + # Notify event subscribers + topic = "acapy::record::credential::delete" + await profile.notify(topic, {"id": credential_id, "state": "deleted"}) return web.json_response({}) @@ -343,12 +388,16 @@ async def credentials_list(request: web.BaseRequest): start = int(start) if isinstance(start, str) else 0 count = int(count) if isinstance(count, str) else 10 - async with context.profile.session() as session: - holder = session.inject(IndyHolder) - try: - credentials = await holder.get_credentials(start, count, wql) - except IndyHolderError as err: - raise web.HTTPBadRequest(reason=err.roll_up) from err + if context.settings.get("wallet.type") == "askar-anoncreds": + holder = AnonCredsHolder(context.profile) + credentials = await holder.get_credentials(start, count, wql) + else: + async with context.profile.session() as session: + holder = session.inject(IndyHolder) + try: + credentials = await holder.get_credentials(start, count, wql) + except IndyHolderError as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err return web.json_response({"results": credentials}) From 1e9a8906e7c4136fa0dfe321596b96ee2acee3f0 Mon Sep 17 00:00:00 2001 From: jamshale Date: Mon, 24 Jun 2024 13:06:24 -0700 Subject: [PATCH 2/5] Fix unit tests Signed-off-by: jamshale --- .../anoncreds/tests/test_holder.py | 40 ++++++++++++------- aries_cloudagent/holder/routes.py | 4 +- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/aries_cloudagent/anoncreds/tests/test_holder.py b/aries_cloudagent/anoncreds/tests/test_holder.py index 549a5777d8..3cb6143e93 100644 --- a/aries_cloudagent/anoncreds/tests/test_holder.py +++ b/aries_cloudagent/anoncreds/tests/test_holder.py @@ -18,7 +18,6 @@ ) from aries_askar import AskarError, AskarErrorCode -from ..holder import AnonCredsHolder, AnonCredsHolderError from aries_cloudagent.anoncreds.tests.mock_objects import ( MOCK_CRED, MOCK_CRED_DEF, @@ -35,7 +34,10 @@ from aries_cloudagent.wallet.error import WalletNotFoundError from .. import holder as test_module +from ..holder import AnonCredsHolder, AnonCredsHolderError from ..models.anoncreds_cred_def import CredDef, CredDefValue, CredDefValuePrimary +from ..models.anoncreds_revocation import GetRevListResult, RevList +from ..registry import AnonCredsRegistry class MockCredReceived: @@ -84,6 +86,8 @@ def __init__(self, rev_reg=False) -> None: mock_cred = deepcopy(MOCK_CRED) if rev_reg: mock_cred["rev_reg_id"] = "rev-reg-id" + + mock_cred["rev_reg_index"] = 1 self.name = "name" self.value = mock_cred self.raw_value = mock_cred @@ -377,22 +381,27 @@ async def test_get_credential(self, mock_handle): @mock.patch.object(InMemoryProfileSession, "handle") async def test_credential_revoked(self, mock_handle): - mock_ledger = mock.MagicMock( - get_revoc_reg_delta=mock.CoroutineMock( - return_value=( - { - "value": { - "revoked": [100], - } - }, - 0, - ), - ) + self.profile.context.injector.bind_instance( + AnonCredsRegistry, + mock.MagicMock( + get_revocation_list=mock.CoroutineMock( + return_value=GetRevListResult( + revocation_list=RevList( + issuer_id="CsQY9MGeD3CQP4EyuVFo5m", + current_accumulator="21 124C594B6B20E41B681E92B2C43FD165EA9E68BC3C9D63A82C8893124983CAE94 21 124C5341937827427B0A3A32113BD5E64FB7AB39BD3E5ABDD7970874501CA4897 6 5438CB6F442E2F807812FD9DC0C39AFF4A86B1E6766DBB5359E86A4D70401B0F 4 39D1CA5C4716FFC4FE0853C4FF7F081DFD8DF8D2C2CA79705211680AC77BF3A1 6 70504A5493F89C97C225B68310811A41AD9CD889301F238E93C95AD085E84191 4 39582252194D756D5D86D0EED02BF1B95CE12AED2FA5CD3C53260747D891993C", + revocation_list=[0, 1, 1, 0], + timestamp=1669640864487, + rev_reg_def_id="4xE68b6S5VRFrKMMG1U95M:4:4xE68b6S5VRFrKMMG1U95M:3:CL:59232:default:CL_ACCUM:4ae1cc6c-f6bd-486c-8057-88f2ce74e960", + ), + resolution_metadata={}, + revocation_registry_metadata={}, + ) + ) + ), ) mock_handle.fetch = mock.CoroutineMock(return_value=MockCredEntry()) assert ( await self.holder.credential_revoked( - ledger=mock_ledger, credential_id="cred-id", to=None, fro=None, @@ -415,8 +424,9 @@ async def test_delete_credential(self, mock_handle): mock_handle.remove.call_args_list[0].args == ("credential", "cred-id") mock_handle.remove.call_args_list[0].args == ("attribute-mime-types", "cred-id") - # not found, don't raise error - await self.holder.delete_credential("cred-id") + # not found + with self.assertRaises(AnonCredsHolderError): + await self.holder.delete_credential("cred-id") # other asker error, raise error with self.assertRaises(AnonCredsHolderError): await self.holder.delete_credential("cred-id") diff --git a/aries_cloudagent/holder/routes.py b/aries_cloudagent/holder/routes.py index a8e3834e2c..3c4f29e5e8 100644 --- a/aries_cloudagent/holder/routes.py +++ b/aries_cloudagent/holder/routes.py @@ -256,12 +256,12 @@ async def get_revoked_using_anoncreds(profile: Profile): async def get_revoked_using_indy(profile: Profile): async with profile.session() as session: - holder = session.inject(IndyHolder) - ledger = session.inject_or(BaseLedger) if not ledger: raise web.HTTPForbidden(reason="No ledger available") + holder = session.inject(IndyHolder) + async with ledger: try: return await holder.credential_revoked( From 536ab6091b3677e61960fb6c39091f8e4b8371d1 Mon Sep 17 00:00:00 2001 From: jamshale Date: Mon, 24 Jun 2024 15:59:30 -0700 Subject: [PATCH 3/5] Add unit test coverage Signed-off-by: jamshale --- aries_cloudagent/holder/tests/test_routes.py | 215 ++++++++++++++++++- 1 file changed, 214 insertions(+), 1 deletion(-) diff --git a/aries_cloudagent/holder/tests/test_routes.py b/aries_cloudagent/holder/tests/test_routes.py index be0f941d07..4a25c67218 100644 --- a/aries_cloudagent/holder/tests/test_routes.py +++ b/aries_cloudagent/holder/tests/test_routes.py @@ -1,9 +1,14 @@ import json from unittest import IsolatedAsyncioTestCase +from aries_askar import AskarErrorCode + from aries_cloudagent.tests import mock -from ...core.in_memory import InMemoryProfile +from ...admin.request_context import AdminRequestContext +from ...anoncreds.holder import AnonCredsHolder, AnonCredsHolderError +from ...askar.profile_anon import AskarAnoncredsProfile +from ...core.in_memory.profile import InMemoryProfile from ...indy.holder import IndyHolder from ...ledger.base import BaseLedger from ...storage.vc_holder.base import VCHolder @@ -66,6 +71,44 @@ async def test_credentials_get(self): json_response.assert_called_once_with({"hello": "world"}) assert result is json_response.return_value + @mock.patch.object(AnonCredsHolder, "get_credential") + async def test_credentials_get_with_anoncreds(self, mock_get_credential): + self.session_inject = {} + self.profile = InMemoryProfile.test_profile( + settings={ + "wallet.type": "askar-anoncreds", + "admin.admin_api_key": "secret-key", + }, + profile_class=AskarAnoncredsProfile, + ) + self.context = AdminRequestContext.test_context( + self.session_inject, self.profile + ) + self.request_dict = { + "context": self.context, + } + self.request = mock.MagicMock( + match_info={"credential_id": "dummy"}, + __getitem__=lambda _, k: self.request_dict[k], + context=self.context, + headers={"x-api-key": "secret-key"}, + ) + self.context.inject = mock.Mock( + return_value=mock.MagicMock( + get_credential=mock.CoroutineMock(return_value="test-credential") + ) + ) + + mock_get_credential.return_value = json.dumps({"hello": "world"}) + + with mock.patch.object( + test_module.web, "json_response", mock.Mock() + ) as json_response: + result = await test_module.credentials_get(self.request) + json_response.assert_called_once_with({"hello": "world"}) + assert result is json_response.return_value + assert mock_get_credential.called + async def test_credentials_get_not_found(self): self.request.match_info = {"credential_id": "dummy"} self.profile.context.injector.bind_instance( @@ -97,6 +140,43 @@ async def test_credentials_revoked(self): json_response.assert_called_once_with({"revoked": False}) assert result is json_response.return_value + @mock.patch.object(AnonCredsHolder, "credential_revoked") + async def test_credentials_revoked_with_anoncreds(self, mock_credential_revoked): + self.session_inject = {} + self.profile = InMemoryProfile.test_profile( + settings={ + "wallet.type": "askar-anoncreds", + "admin.admin_api_key": "secret-key", + }, + profile_class=AskarAnoncredsProfile, + ) + self.context = AdminRequestContext.test_context( + self.session_inject, self.profile + ) + self.request_dict = { + "context": self.context, + } + self.request = mock.MagicMock( + match_info={"credential_id": "dummy"}, + __getitem__=lambda _, k: self.request_dict[k], + context=self.context, + headers={"x-api-key": "secret-key"}, + ) + self.context.inject = mock.Mock( + return_value=mock.MagicMock( + get_credential=mock.CoroutineMock(return_value="test-credential") + ) + ) + + mock_credential_revoked.return_value = False + + with mock.patch.object( + test_module.web, "json_response", mock.Mock() + ) as json_response: + result = await test_module.credentials_revoked(self.request) + json_response.assert_called_once_with({"revoked": False}) + assert result is json_response.return_value + async def test_credentials_revoked_no_ledger(self): self.request.match_info = {"credential_id": "dummy"} @@ -159,6 +239,47 @@ async def test_attribute_mime_types_get(self): {"results": {"a": "application/jpeg"}} ) + @mock.patch.object(AnonCredsHolder, "get_mime_type") + async def test_attribute_mime_types_get_with_anoncreds(self, mock_get_mime_type): + self.session_inject = {} + self.profile = InMemoryProfile.test_profile( + settings={ + "wallet.type": "askar-anoncreds", + "admin.admin_api_key": "secret-key", + }, + profile_class=AskarAnoncredsProfile, + ) + self.context = AdminRequestContext.test_context( + self.session_inject, self.profile + ) + self.request_dict = { + "context": self.context, + } + self.request = mock.MagicMock( + match_info={"credential_id": "dummy"}, + __getitem__=lambda _, k: self.request_dict[k], + context=self.context, + headers={"x-api-key": "secret-key"}, + ) + self.context.inject = mock.Mock( + return_value=mock.MagicMock( + get_credential=mock.CoroutineMock(return_value="test-credential") + ) + ) + + mock_get_mime_type.side_effect = [None, {"a": "application/jpeg"}] + + with mock.patch.object(test_module.web, "json_response") as mock_response: + await test_module.credentials_attr_mime_types_get(self.request) + mock_response.assert_called_once_with({"results": None}) + + with mock.patch.object(test_module.web, "json_response") as mock_response: + await test_module.credentials_attr_mime_types_get(self.request) + mock_response.assert_called_once_with( + {"results": {"a": "application/jpeg"}} + ) + assert mock_get_mime_type.called + async def test_credentials_remove(self): self.request.match_info = {"credential_id": "dummy"} self.profile.context.injector.bind_instance( @@ -173,6 +294,57 @@ async def test_credentials_remove(self): json_response.assert_called_once_with({}) assert result is json_response.return_value + @mock.patch.object(AnonCredsHolder, "delete_credential") + async def test_credentials_remove_with_anoncreds(self, mock_delete_credential): + self.session_inject = {} + self.profile = InMemoryProfile.test_profile( + settings={ + "wallet.type": "askar-anoncreds", + "admin.admin_api_key": "secret-key", + }, + profile_class=AskarAnoncredsProfile, + ) + self.context = AdminRequestContext.test_context( + self.session_inject, self.profile + ) + self.request_dict = { + "context": self.context, + } + self.request = mock.MagicMock( + match_info={"credential_id": "dummy"}, + __getitem__=lambda _, k: self.request_dict[k], + context=self.context, + headers={"x-api-key": "secret-key"}, + ) + self.context.inject = mock.Mock( + return_value=mock.MagicMock( + get_credential=mock.CoroutineMock(return_value="test-credential") + ) + ) + + mock_delete_credential.side_effect = [ + None, + AnonCredsHolderError( + "anoncreds error", error_code=AskarErrorCode.NOT_FOUND + ), + AnonCredsHolderError( + "anoncreds error", error_code=AskarErrorCode.UNEXPECTED + ), + ] + + with mock.patch.object( + test_module.web, "json_response", mock.Mock() + ) as json_response: + result = await test_module.credentials_remove(self.request) + json_response.assert_called_once_with({}) + assert result is json_response.return_value + assert mock_delete_credential.called + + with self.assertRaises(test_module.web.HTTPNotFound): + await test_module.credentials_remove(self.request) + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.credentials_remove(self.request) + async def test_credentials_remove_not_found(self): self.request.match_info = {"credential_id": "dummy"} self.profile.context.injector.bind_instance( @@ -202,6 +374,47 @@ async def test_credentials_list(self): json_response.assert_called_once_with({"results": [{"hello": "world"}]}) assert result is json_response.return_value + @mock.patch.object(AnonCredsHolder, "get_credentials") + async def test_credentials_list_with_anoncreds(self, mock_get_credentials): + self.session_inject = {} + self.profile = InMemoryProfile.test_profile( + settings={ + "wallet.type": "askar-anoncreds", + "admin.admin_api_key": "secret-key", + }, + profile_class=AskarAnoncredsProfile, + ) + self.context = AdminRequestContext.test_context( + self.session_inject, self.profile + ) + self.request_dict = { + "context": self.context, + } + self.request = mock.MagicMock( + match_info={"credential_id": "dummy"}, + query={ + "start": "0", + "count": "10", + }, + __getitem__=lambda _, k: self.request_dict[k], + context=self.context, + headers={"x-api-key": "secret-key"}, + ) + self.context.inject = mock.Mock( + return_value=mock.MagicMock( + get_credential=mock.CoroutineMock(return_value="test-credential") + ) + ) + + mock_get_credentials.return_value = [{"hello": "world"}] + + with mock.patch.object( + test_module.web, "json_response", mock.Mock() + ) as json_response: + result = await test_module.credentials_list(self.request) + json_response.assert_called_once_with({"results": [{"hello": "world"}]}) + assert result is json_response.return_value + async def test_credentials_list_x_holder(self): self.request.query = {"start": "0", "count": "10"} self.profile.context.injector.bind_instance( From e72704d0a6d6e69f6629af03ab9a15ee5955f26d Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 25 Jun 2024 12:43:32 -0700 Subject: [PATCH 4/5] Use constant for common config Signed-off-by: jamshale --- aries_cloudagent/holder/routes.py | 37 ++++++++++++------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/aries_cloudagent/holder/routes.py b/aries_cloudagent/holder/routes.py index 3c4f29e5e8..f8b81c7dfc 100644 --- a/aries_cloudagent/holder/routes.py +++ b/aries_cloudagent/holder/routes.py @@ -4,13 +4,8 @@ from profile import Profile from aiohttp import web -from aiohttp_apispec import ( - docs, - match_info_schema, - querystring_schema, - request_schema, - response_schema, -) +from aiohttp_apispec import (docs, match_info_schema, querystring_schema, + request_schema, response_schema) from aries_askar import AskarErrorCode from marshmallow import fields @@ -22,22 +17,18 @@ from ..ledger.base import BaseLedger from ..ledger.error import LedgerError from ..messaging.models.openapi import OpenAPISchema -from ..messaging.valid import ( - ENDPOINT_EXAMPLE, - ENDPOINT_VALIDATE, - INDY_WQL_EXAMPLE, - INDY_WQL_VALIDATE, - NUM_STR_NATURAL_EXAMPLE, - NUM_STR_NATURAL_VALIDATE, - NUM_STR_WHOLE_EXAMPLE, - NUM_STR_WHOLE_VALIDATE, - UUID4_EXAMPLE, -) +from ..messaging.valid import (ENDPOINT_EXAMPLE, ENDPOINT_VALIDATE, + INDY_WQL_EXAMPLE, INDY_WQL_VALIDATE, + NUM_STR_NATURAL_EXAMPLE, + NUM_STR_NATURAL_VALIDATE, NUM_STR_WHOLE_EXAMPLE, + NUM_STR_WHOLE_VALIDATE, UUID4_EXAMPLE) from ..storage.error import StorageError, StorageNotFoundError from ..storage.vc_holder.base import VCHolder from ..storage.vc_holder.vc_record import VCRecordSchema from ..wallet.error import WalletNotFoundError +wallet_type_config = "wallet.type" + class HolderModuleResponseSchema(OpenAPISchema): """Response schema for Holder Module.""" @@ -210,7 +201,7 @@ async def credentials_get(request: web.BaseRequest): context: AdminRequestContext = request["context"] credential_id = request.match_info["credential_id"] - if context.settings.get("wallet.type") == "askar-anoncreds": + if context.settings.get(wallet_type_config) == "askar-anoncreds": holder = AnonCredsHolder(context.profile) else: holder = context.profile.inject(IndyHolder) @@ -244,7 +235,7 @@ async def credentials_revoked(request: web.BaseRequest): fro = request.query.get("from") to = request.query.get("to") profile = context.profile - wallet_type = profile.settings.get_value("wallet.type") + wallet_type = profile.settings.get_value(wallet_type_config) async def get_revoked_using_anoncreds(profile: Profile): holder = AnonCredsHolder(profile) @@ -301,7 +292,7 @@ async def credentials_attr_mime_types_get(request: web.BaseRequest): context: AdminRequestContext = request["context"] credential_id = request.match_info["credential_id"] - if context.settings.get("wallet.type") == "askar-anoncreds": + if context.settings.get(wallet_type_config) == "askar-anoncreds": holder = AnonCredsHolder(context.profile) mime_types = await holder.get_mime_type(credential_id) else: @@ -347,7 +338,7 @@ async def delete_credential_using_indy(profile: Profile): except WalletNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err - if context.settings.get("wallet.type") == "askar-anoncreds": + if context.settings.get(wallet_type_config) == "askar-anoncreds": await delete_credential_using_anoncreds(profile) else: await delete_credential_using_indy(profile) @@ -388,7 +379,7 @@ async def credentials_list(request: web.BaseRequest): start = int(start) if isinstance(start, str) else 0 count = int(count) if isinstance(count, str) else 10 - if context.settings.get("wallet.type") == "askar-anoncreds": + if context.settings.get(wallet_type_config) == "askar-anoncreds": holder = AnonCredsHolder(context.profile) credentials = await holder.get_credentials(start, count, wql) else: From 1742eaf64434598dd57a6205f74f2f63365d9549 Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 25 Jun 2024 12:44:38 -0700 Subject: [PATCH 5/5] Fix formatting Signed-off-by: jamshale --- aries_cloudagent/holder/routes.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/aries_cloudagent/holder/routes.py b/aries_cloudagent/holder/routes.py index f8b81c7dfc..4d85b4323a 100644 --- a/aries_cloudagent/holder/routes.py +++ b/aries_cloudagent/holder/routes.py @@ -4,8 +4,13 @@ from profile import Profile from aiohttp import web -from aiohttp_apispec import (docs, match_info_schema, querystring_schema, - request_schema, response_schema) +from aiohttp_apispec import ( + docs, + match_info_schema, + querystring_schema, + request_schema, + response_schema, +) from aries_askar import AskarErrorCode from marshmallow import fields @@ -17,11 +22,17 @@ from ..ledger.base import BaseLedger from ..ledger.error import LedgerError from ..messaging.models.openapi import OpenAPISchema -from ..messaging.valid import (ENDPOINT_EXAMPLE, ENDPOINT_VALIDATE, - INDY_WQL_EXAMPLE, INDY_WQL_VALIDATE, - NUM_STR_NATURAL_EXAMPLE, - NUM_STR_NATURAL_VALIDATE, NUM_STR_WHOLE_EXAMPLE, - NUM_STR_WHOLE_VALIDATE, UUID4_EXAMPLE) +from ..messaging.valid import ( + ENDPOINT_EXAMPLE, + ENDPOINT_VALIDATE, + INDY_WQL_EXAMPLE, + INDY_WQL_VALIDATE, + NUM_STR_NATURAL_EXAMPLE, + NUM_STR_NATURAL_VALIDATE, + NUM_STR_WHOLE_EXAMPLE, + NUM_STR_WHOLE_VALIDATE, + UUID4_EXAMPLE, +) from ..storage.error import StorageError, StorageNotFoundError from ..storage.vc_holder.base import VCHolder from ..storage.vc_holder.vc_record import VCRecordSchema