diff --git a/acapy_agent/revocation/manager.py b/acapy_agent/revocation/manager.py index ed0312dd92..b97731b369 100644 --- a/acapy_agent/revocation/manager.py +++ b/acapy_agent/revocation/manager.py @@ -2,7 +2,7 @@ import json import logging -from typing import Mapping, Optional, Sequence, Text, Tuple +from typing import Mapping, NamedTuple, Optional, Sequence, Text, Tuple from ..connections.models.conn_record import ConnRecord from ..core.error import BaseError @@ -26,6 +26,17 @@ class RevocationManagerError(BaseError): """Revocation manager error.""" +class RevocationNotificationInfo(NamedTuple): + """Revocation notification information.""" + + rev_reg_id: str + cred_rev_id: str + thread_id: Optional[str] + connection_id: Optional[str] + comment: Optional[str] + notify_version: Optional[str] + + class RevocationManager: """Class for managing revocation operations.""" @@ -107,6 +118,46 @@ async def revoke_credential_by_cred_ex_id( write_ledger=write_ledger, ) + async def _prepare_revocation_notification( + self, + revoc_notif_info: RevocationNotificationInfo, + ): + """Saves the revocation notification record, and thread_id if not provided.""" + thread_id = ( + revoc_notif_info.thread_id + or f"indy::{revoc_notif_info.rev_reg_id}::{revoc_notif_info.cred_rev_id}" + ) + rev_notify_rec = RevNotificationRecord( + rev_reg_id=revoc_notif_info.rev_reg_id, + cred_rev_id=revoc_notif_info.cred_rev_id, + thread_id=thread_id, + connection_id=revoc_notif_info.connection_id, + comment=revoc_notif_info.comment, + version=revoc_notif_info.notify_version, + ) + async with self._profile.session() as session: + await rev_notify_rec.save(session, reason="New revocation notification") + + async def _get_endorsement_txn_for_revocation( + self, endorser_conn_id: str, issuer_rr_upd: IssuerRevRegRecord + ): + async with self._profile.session() as session: + try: + connection_record = await ConnRecord.retrieve_by_id( + session, endorser_conn_id + ) + except StorageNotFoundError: + raise RevocationManagerError( + "No endorser connection record found " f"for id: {endorser_conn_id}" + ) + endorser_info = await connection_record.metadata_get(session, "endorser_info") + endorser_did = endorser_info["endorser_did"] + return await issuer_rr_upd.send_entry( + self._profile, + write_ledger=False, + endorser_did=endorser_did, + ) + async def revoke_credential( self, rev_reg_id: str, @@ -150,8 +201,8 @@ async def revoke_credential( write_ledger is True, otherwise None. """ issuer = self._profile.inject(IndyIssuer) - revoc = IndyRevocation(self._profile) + issuer_rr_rec = await revoc.get_issuer_rev_reg_record(rev_reg_id) if not issuer_rr_rec: raise RevocationManagerError( @@ -159,72 +210,63 @@ async def revoke_credential( ) if notify: - thread_id = thread_id or f"indy::{rev_reg_id}::{cred_rev_id}" - rev_notify_rec = RevNotificationRecord( - rev_reg_id=rev_reg_id, - cred_rev_id=cred_rev_id, - thread_id=thread_id, - connection_id=connection_id, - comment=comment, - version=notify_version, - ) - async with self._profile.session() as session: - await rev_notify_rec.save(session, reason="New revocation notification") - - if publish: - rev_reg = await revoc.get_ledger_registry(rev_reg_id) - await rev_reg.get_or_fetch_local_tails_path() - # pick up pending revocations on input revocation registry - crids = (issuer_rr_rec.pending_pub or []) + [cred_rev_id] - (delta_json, _) = await issuer.revoke_credentials( - issuer_rr_rec.cred_def_id, - issuer_rr_rec.revoc_reg_id, - issuer_rr_rec.tails_local_path, - crids, + await self._prepare_revocation_notification( + RevocationNotificationInfo( + rev_reg_id=rev_reg_id, + cred_rev_id=cred_rev_id, + thread_id=thread_id, + connection_id=connection_id, + comment=comment, + notify_version=notify_version, + ), ) - async with self._profile.transaction() as txn: - issuer_rr_upd = await IssuerRevRegRecord.retrieve_by_id( - txn, issuer_rr_rec.record_id, for_update=True - ) - if delta_json: - issuer_rr_upd.revoc_reg_entry = json.loads(delta_json) - await issuer_rr_upd.clear_pending(txn, crids) - await txn.commit() - await self.set_cred_revoked_state(rev_reg_id, crids) - if delta_json: - if write_ledger: - rev_entry_resp = await issuer_rr_upd.send_entry(self._profile) - await notify_revocation_published_event( - self._profile, rev_reg_id, [cred_rev_id] - ) - return rev_entry_resp - else: - async with self._profile.session() as session: - try: - connection_record = await ConnRecord.retrieve_by_id( - session, endorser_conn_id - ) - except StorageNotFoundError: - raise RevocationManagerError( - "No endorser connection record found " - f"for id: {endorser_conn_id}" - ) - endorser_info = await connection_record.metadata_get( - session, "endorser_info" - ) - endorser_did = endorser_info["endorser_did"] - rev_entry_resp = await issuer_rr_upd.send_entry( - self._profile, - write_ledger=write_ledger, - endorser_did=endorser_did, - ) - return rev_entry_resp - else: + + if not publish: + # If not publishing, just mark the revocation as pending. async with self._profile.transaction() as txn: await issuer_rr_rec.mark_pending(txn, cred_rev_id) await txn.commit() return None + rev_reg = await revoc.get_ledger_registry(rev_reg_id) + await rev_reg.get_or_fetch_local_tails_path() + # pick up pending revocations on input revocation registry + crids = (issuer_rr_rec.pending_pub or []) + [cred_rev_id] + (delta_json, _) = await issuer.revoke_credentials( + issuer_rr_rec.cred_def_id, + issuer_rr_rec.revoc_reg_id, + issuer_rr_rec.tails_local_path, + crids, + ) + + # Update the revocation registry record with the new delta + # and clear pending revocations + async with self._profile.transaction() as txn: + issuer_rr_upd = await IssuerRevRegRecord.retrieve_by_id( + txn, issuer_rr_rec.record_id, for_update=True + ) + if delta_json: + issuer_rr_upd.revoc_reg_entry = json.loads(delta_json) + await issuer_rr_upd.clear_pending(txn, crids) + await txn.commit() + + await self.set_cred_revoked_state(rev_reg_id, crids) + + # Revocation list needs to be updated on ledger + if delta_json: + # Can write to ledger directly + if write_ledger: + rev_entry_resp = await issuer_rr_upd.send_entry(self._profile) + await notify_revocation_published_event( + self._profile, rev_reg_id, [cred_rev_id] + ) + return rev_entry_resp + # Author --> Need endorsed transaction for revocation + else: + return await self._get_endorsement_txn_for_revocation( + endorser_conn_id, issuer_rr_upd + ) + async def update_rev_reg_revoked_state( self, apply_ledger_update: bool, diff --git a/acapy_agent/revocation/models/issuer_rev_reg_record.py b/acapy_agent/revocation/models/issuer_rev_reg_record.py index 25c5b5f52a..bed21031a2 100644 --- a/acapy_agent/revocation/models/issuer_rev_reg_record.py +++ b/acapy_agent/revocation/models/issuer_rev_reg_record.py @@ -14,7 +14,11 @@ from uuid_utils import uuid4 from ...core.profile import Profile, ProfileSession -from ...indy.credx.issuer import CATEGORY_CRED_DEF, CATEGORY_REV_REG_DEF_PRIVATE +from ...indy.credx.issuer import ( + CATEGORY_CRED_DEF, + CATEGORY_REV_REG, + CATEGORY_REV_REG_DEF_PRIVATE, +) from ...indy.issuer import IndyIssuer, IndyIssuerError from ...indy.models.revocation import ( IndyRevRegDef, @@ -358,6 +362,19 @@ async def send_entry( return rev_entry_res + def _get_revoked_discrepancies( + self, recs: Sequence[IssuerCredRevRecord], rev_reg_delta: dict + ) -> Tuple[list, int]: + revoked_ids = [] + rec_count = 0 + for rec in recs: + if rec.state == IssuerCredRevRecord.STATE_REVOKED: + revoked_ids.append(int(rec.cred_rev_id)) + if int(rec.cred_rev_id) not in rev_reg_delta["value"]["revoked"]: + rec_count += 1 + + return revoked_ids, rec_count + async def fix_ledger_entry( self, profile: Profile, @@ -365,84 +382,88 @@ async def fix_ledger_entry( genesis_transactions: str, ) -> Tuple[dict, dict, dict]: """Fix the ledger entry to match wallet-recorded credentials.""" + recovery_txn = {} + applied_txn = {} + # get rev reg delta (revocations published to ledger) ledger = profile.inject(BaseLedger) async with ledger: (rev_reg_delta, _) = await ledger.get_revoc_reg_delta(self.revoc_reg_id) # get rev reg records from wallet (revocations and status) - recs = [] - rec_count = 0 - accum_count = 0 - recovery_txn = {} - applied_txn = {} async with profile.session() as session: recs = await IssuerCredRevRecord.query_by_ids( session, rev_reg_id=self.revoc_reg_id ) - revoked_ids = [] - for rec in recs: - if rec.state == IssuerCredRevRecord.STATE_REVOKED: - revoked_ids.append(int(rec.cred_rev_id)) - if int(rec.cred_rev_id) not in rev_reg_delta["value"]["revoked"]: - # await rec.set_state(session, IssuerCredRevRecord.STATE_ISSUED) - rec_count += 1 + revoked_ids, rec_count = self._get_revoked_discrepancies(recs, rev_reg_delta) + + LOGGER.debug(f"Fixed entry recs count = {rec_count}") + LOGGER.debug(f"Rev reg entry value: {self.revoc_reg_entry.value}") + LOGGER.debug(f'Rev reg delta: {rev_reg_delta.get("value")}') + + # No update required if no discrepancies + if rec_count == 0: + return (rev_reg_delta, {}, {}) + + # We have revocation discrepancies, generate the recovery txn + async with profile.session() as session: + # We need the cred_def and rev_reg_def_private to generate the recovery txn + issuer_rev_reg_record = await IssuerRevRegRecord.retrieve_by_revoc_reg_id( + session, self.revoc_reg_id + ) + cred_def_id = issuer_rev_reg_record.cred_def_id + cred_def = await session.handle.fetch(CATEGORY_CRED_DEF, cred_def_id) + rev_reg_def_private = await session.handle.fetch( + CATEGORY_REV_REG_DEF_PRIVATE, self.revoc_reg_id + ) - LOGGER.debug(">>> fixed entry recs count = %s", rec_count) - LOGGER.debug( - ">>> rev_reg_record.revoc_reg_entry.value: %s", - self.revoc_reg_entry.value, + credx_module = importlib.import_module("indy_credx") + cred_defn = credx_module.CredentialDefinition.load(cred_def.value_json) + rev_reg_defn_private = credx_module.RevocationRegistryDefinitionPrivate.load( + rev_reg_def_private.value_json ) - LOGGER.debug('>>> rev_reg_delta.get("value"): %s', rev_reg_delta.get("value")) - - # if we had any revocation discrepancies, check the accumulator value - if rec_count > 0: - if (self.revoc_reg_entry.value and rev_reg_delta.get("value")) and not ( - self.revoc_reg_entry.value.accum == rev_reg_delta["value"]["accum"] - ): - # self.revoc_reg_entry = rev_reg_delta["value"] - # await self.save(session) - accum_count += 1 + calculated_txn = await generate_ledger_rrrecovery_txn( + genesis_transactions, + self.revoc_reg_id, + revoked_ids, + cred_defn, + rev_reg_defn_private, + ) + recovery_txn = json.loads(calculated_txn.to_json()) + + LOGGER.debug(f"Applying ledger update: {apply_ledger_update}") + if apply_ledger_update: async with profile.session() as session: - issuer_rev_reg_record = await IssuerRevRegRecord.retrieve_by_revoc_reg_id( - session, self.revoc_reg_id + ledger = session.inject_or(BaseLedger) + if not ledger: + reason = "No ledger available" + if not session.context.settings.get_value("wallet.type"): + reason += ": missing wallet-type?" + raise LedgerError(reason=reason) + + async with ledger: + ledger_response = await ledger.send_revoc_reg_entry( + self.revoc_reg_id, "CL_ACCUM", recovery_txn + ) + + applied_txn = ledger_response["result"] + + # Update the local wallets rev reg entry with the new accumulator value + async with profile.session() as session: + rev_reg = await session.handle.fetch( + CATEGORY_REV_REG, self.revoc_reg_id, for_update=True ) - cred_def_id = issuer_rev_reg_record.cred_def_id - _cred_def = await session.handle.fetch(CATEGORY_CRED_DEF, cred_def_id) - _rev_reg_def_private = await session.handle.fetch( - CATEGORY_REV_REG_DEF_PRIVATE, self.revoc_reg_id + new_value_json = rev_reg.value_json + new_value_json["value"]["accum"] = applied_txn["txn"]["data"]["value"][ + "accum" + ] + await session.handle.replace( + CATEGORY_REV_REG, + rev_reg.name, + json.dumps(new_value_json), + rev_reg.tags, ) - credx_module = importlib.import_module("indy_credx") - cred_defn = credx_module.CredentialDefinition.load(_cred_def.value_json) - rev_reg_defn_private = credx_module.RevocationRegistryDefinitionPrivate.load( - _rev_reg_def_private.value_json - ) - calculated_txn = await generate_ledger_rrrecovery_txn( - genesis_transactions, - self.revoc_reg_id, - revoked_ids, - cred_defn, - rev_reg_defn_private, - ) - recovery_txn = json.loads(calculated_txn.to_json()) - - LOGGER.debug(">>> apply_ledger_update = %s", apply_ledger_update) - if apply_ledger_update: - async with profile.session() as session: - ledger = session.inject_or(BaseLedger) - if not ledger: - reason = "No ledger available" - if not session.context.settings.get_value("wallet.type"): - reason += ": missing wallet-type?" - raise LedgerError(reason=reason) - - async with ledger: - ledger_response = await ledger.send_revoc_reg_entry( - self.revoc_reg_id, "CL_ACCUM", recovery_txn - ) - - applied_txn = ledger_response["result"] return (rev_reg_delta, recovery_txn, applied_txn) diff --git a/acapy_agent/revocation/models/tests/test_issuer_rev_reg_record.py b/acapy_agent/revocation/models/tests/test_issuer_rev_reg_record.py index 705ed5a49f..6aa6cdac7c 100644 --- a/acapy_agent/revocation/models/tests/test_issuer_rev_reg_record.py +++ b/acapy_agent/revocation/models/tests/test_issuer_rev_reg_record.py @@ -1,14 +1,12 @@ import importlib import json from os.path import join -from typing import Any, Mapping, Optional, Type from unittest import IsolatedAsyncioTestCase from acapy_agent.tests import mock -from ....config.injection_context import InjectionContext -from ....core.in_memory import InMemoryProfile, InMemoryProfileSession -from ....core.profile import Profile, ProfileSession +from ....core.in_memory import InMemoryProfile +from ....core.in_memory.profile import InMemoryProfileSession from ....indy.issuer import IndyIssuer, IndyIssuerError from ....indy.util import indy_client_dir from ....ledger.base import BaseLedger @@ -40,6 +38,7 @@ }, "tailsHash": TAILS_HASH, "tailsLocation": TAILS_URI, + "accum": "21 11792B036AED0AAA12A46CF39347EB35C865DAC99F767B286F6E37FF0FF4F1CBE 21 12571556D2A1B4475E81295FC8A4F0B66D00FB78EE8C7E15C29C2CA862D0217D4 6 92166D2C2A3BC621AD615136B7229AF051AB026704BF8874F9F0B0106122BF4F 4 2C47BCBBC32904161E2A2926F120AD8F40D94C09D1D97DA735191D27370A68F8F 6 8CC19FDA63AB16BEA45050D72478115BC1CCB8E47A854339D2DD5E112976FFF7 4 298B2571FFC63A737B79C131AC7048A1BD474BF907AF13BC42E533C79FB502C7", }, } REV_REG_ENTRY = { @@ -80,7 +79,8 @@ async def test_order(self): await rec1.save(session, reason="another record") assert rec0 < rec1 - async def test_fix_ledger_entry(self): + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_fix_ledger_entry(self, mock_handle): mock_cred_def = { "ver": "1.0", "id": "55GkHamhTU1ZbTbV2ab9DE:3:CL:15:tag", @@ -119,73 +119,14 @@ async def test_fix_ledger_entry(self): } } - class TestProfile(InMemoryProfile): - def session( - self, context: Optional[InjectionContext] = None - ) -> "ProfileSession": - return TestProfileSession(self, context=context) - - @classmethod - def test_profile( - cls, settings: Mapping[str, Any] = None, bind: Mapping[Type, Any] = None - ) -> "TestProfile": - profile = TestProfile( - context=InjectionContext(enforce_typing=False, settings=settings), - name=InMemoryProfile.TEST_PROFILE_NAME, - ) - if bind: - for k, v in bind.items(): - if v: - profile.context.injector.bind_instance(k, v) - else: - profile.context.injector.clear_binding(k) - return profile - - @classmethod - def test_session( - cls, settings: Mapping[str, Any] = None, bind: Mapping[Type, Any] = None - ) -> "TestProfileSession": - session = TestProfileSession(cls.test_profile(), settings=settings) - session._active = True - session._init_context() - if bind: - for k, v in bind.items(): - if v: - session.context.injector.bind_instance(k, v) - else: - session.context.injector.clear_binding(k) - return session - - class TestProfileSession(InMemoryProfileSession): - def __init__( - self, - profile: Profile, - *, - context: Optional[InjectionContext] = None, - settings: Mapping[str, Any] = None, - ): - super().__init__(profile=profile, context=context, settings=settings) - self.handle_counter = 0 - - @property - def handle(self): - if self.handle_counter == 0: - self.handle_counter = self.handle_counter + 1 - return mock.MagicMock( - fetch=mock.CoroutineMock( - return_value=mock.MagicMock( - value_json=json.dumps(mock_cred_def) - ) - ) - ) - else: - return mock.MagicMock( - fetch=mock.CoroutineMock( - return_value=mock.MagicMock( - value_json=json.dumps(mock_reg_rev_def_private), - ), - ) - ) + mock_handle.fetch = mock.CoroutineMock( + side_effect=[ + mock.MagicMock(value_json=json.dumps(mock_cred_def)), + mock.MagicMock(value_json=json.dumps(mock_reg_rev_def_private)), + mock.MagicMock(value_json=REV_REG_DEF), + ] + ) + mock_handle.replace = mock.CoroutineMock() credx_module = importlib.import_module("indy_credx") rev_reg_delta_json = json.dumps( @@ -225,10 +166,18 @@ def handle(self): ) self.ledger.send_revoc_reg_entry = mock.CoroutineMock( return_value={ - "result": {"...": "..."}, + "result": { + "txn": { + "data": { + "value": { + "accum": "1 0792BD1C8C1A529173FDF54A5B30AC90C2472956622E9F04971D36A9BF77C2C5 1 13B18B6B68AD62605C74FD61088814338EDEEB41C2195F96EC0E83B2B3D0258F 1 102ED0DDE96F6367199CE1C0B138F172BC913B65E37250581606974034F4CA20 1 1C53786D2C15190B57167CDDD2A046CAD63970B5DE43F4D492D4F46B8EEE6FF1 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000" + } + } + } + }, }, ) - _test_session = TestProfile.test_session( + _test_session = InMemoryProfile.test_session( settings={"tails_server_base_url": "http://1.2.3.4:8088"}, ) _test_profile = _test_session.profile @@ -264,7 +213,15 @@ def handle(self): "accum": "1 0792BD1C8C1A529173FDF54A5B30AC90C2472956622E9F04971D36A9BF77C2C5 1 13B18B6B68AD62605C74FD61088814338EDEEB41C2195F96EC0E83B2B3D0258F 1 102ED0DDE96F6367199CE1C0B138F172BC913B65E37250581606974034F4CA20 1 1C53786D2C15190B57167CDDD2A046CAD63970B5DE43F4D492D4F46B8EEE6FF1 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000" }, }, - {"...": "..."}, + { + "txn": { + "data": { + "value": { + "accum": "1 0792BD1C8C1A529173FDF54A5B30AC90C2472956622E9F04971D36A9BF77C2C5 1 13B18B6B68AD62605C74FD61088814338EDEEB41C2195F96EC0E83B2B3D0258F 1 102ED0DDE96F6367199CE1C0B138F172BC913B65E37250581606974034F4CA20 1 1C53786D2C15190B57167CDDD2A046CAD63970B5DE43F4D492D4F46B8EEE6FF1 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000" + }, + } + } + }, ) == await rec.fix_ledger_entry( profile=_test_profile, apply_ledger_update=True, diff --git a/acapy_agent/revocation/routes.py b/acapy_agent/revocation/routes.py index 0885cfc108..4899e729a8 100644 --- a/acapy_agent/revocation/routes.py +++ b/acapy_agent/revocation/routes.py @@ -1011,9 +1011,15 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): rev_reg_id = request.match_info["rev_reg_id"] - apply_ledger_update_json = request.query.get("apply_ledger_update", "false") - LOGGER.debug(">>> apply_ledger_update_json = %s", apply_ledger_update_json) apply_ledger_update = json.loads(request.query.get("apply_ledger_update", "false")) + LOGGER.debug( + f"/revocation/registry/{rev_reg_id}/fix-revocation-entry-state request = {apply_ledger_update}" # noqa: E501 + ) + + def _log_ledger_info(available_write_ledgers, write_ledger, pool): + LOGGER.debug(f"available write_ledgers = {available_write_ledgers}") + LOGGER.debug(f"write_ledger = {write_ledger}") + LOGGER.debug(f"write_ledger pool = {pool}") rev_reg_record = None genesis_transactions = None @@ -1030,12 +1036,9 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): ledger_manager = context.injector.inject(BaseMultipleLedgerManager) write_ledger = context.injector.inject(BaseLedger) available_write_ledgers = await ledger_manager.get_write_ledgers() - LOGGER.debug(f"available write_ledgers = {available_write_ledgers}") - LOGGER.debug(f"write_ledger = {write_ledger}") pool = write_ledger.pool - LOGGER.debug(f"write_ledger pool = {pool}") - genesis_transactions = pool.genesis_txns + _log_ledger_info(available_write_ledgers, write_ledger, pool) if not genesis_transactions: raise web.HTTPInternalServerError( @@ -1045,10 +1048,9 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): if apply_ledger_update: ledger = session.inject_or(BaseLedger) if not ledger: - reason = "No ledger available" - if not session.context.settings.get_value("wallet.type"): - reason += ": missing wallet-type?" - raise web.HTTPInternalServerError(reason=reason) + raise web.HTTPInternalServerError( + reason="No ledger available when requesting ledger update" + ) rev_manager = RevocationManager(profile) try: diff --git a/acapy_agent/revocation/tests/test_manager.py b/acapy_agent/revocation/tests/test_manager.py index 801f14f941..7ce9dca582 100644 --- a/acapy_agent/revocation/tests/test_manager.py +++ b/acapy_agent/revocation/tests/test_manager.py @@ -93,6 +93,69 @@ async def test_revoke_credential_publish(self): ["2", "1"], ) + async def test_revoke_credential_notify(self): + CRED_EX_ID = "dummy-cxid" + CRED_REV_ID = "1" + mock_issuer_rev_reg_record = mock.MagicMock( + revoc_reg_id=REV_REG_ID, + tails_local_path=TAILS_LOCAL, + send_entry=mock.CoroutineMock(), + clear_pending=mock.CoroutineMock(), + pending_pub=["2"], + ) + issuer = mock.MagicMock(IndyIssuer, autospec=True) + issuer.revoke_credentials = mock.CoroutineMock( + return_value=( + json.dumps( + { + "ver": "1.0", + "value": { + "prevAccum": "1 ...", + "accum": "21 ...", + "issued": [1], + }, + } + ), + [], + ) + ) + self.profile.context.injector.bind_instance(IndyIssuer, issuer) + + with mock.patch.object( + test_module.IssuerCredRevRecord, + "retrieve_by_cred_ex_id", + mock.CoroutineMock(), + ) as mock_retrieve, mock.patch.object( + test_module, "IndyRevocation", autospec=True + ) as revoc, mock.patch.object( + test_module.IssuerRevRegRecord, + "retrieve_by_id", + mock.CoroutineMock(return_value=mock_issuer_rev_reg_record), + ): + mock_retrieve.return_value = mock.MagicMock( + rev_reg_id="dummy-rr-id", cred_rev_id=CRED_REV_ID + ) + mock_rev_reg = mock.MagicMock( + get_or_fetch_local_tails_path=mock.CoroutineMock() + ) + revoc.return_value.get_issuer_rev_reg_record = mock.CoroutineMock( + return_value=mock_issuer_rev_reg_record + ) + revoc.return_value.get_ledger_registry = mock.CoroutineMock( + return_value=mock_rev_reg + ) + + await self.manager.revoke_credential_by_cred_ex_id( + CRED_EX_ID, publish=True, notify=True + ) + + issuer.revoke_credentials.assert_awaited_once_with( + mock_issuer_rev_reg_record.cred_def_id, + mock_issuer_rev_reg_record.revoc_reg_id, + mock_issuer_rev_reg_record.tails_local_path, + ["2", "1"], + ) + async def test_revoke_credential_publish_endorser(self): conn_record = ConnRecord( their_label="Hello", diff --git a/acapy_agent/revocation/tests/test_routes.py b/acapy_agent/revocation/tests/test_routes.py index 216e770964..f48d48b93c 100644 --- a/acapy_agent/revocation/tests/test_routes.py +++ b/acapy_agent/revocation/tests/test_routes.py @@ -11,10 +11,14 @@ from ...admin.request_context import AdminRequestContext from ...askar.profile_anon import AskarAnoncredsProfile +from ...ledger.base import BaseLedger +from ...ledger.multiple_ledger.base_manager import BaseMultipleLedgerManager from ...protocols.endorse_transaction.v1_0.manager import TransactionManagerError +from ...revocation.manager import RevocationManager from ...storage.error import StorageError from ...storage.in_memory import InMemoryStorage from .. import routes as test_module +from ..models.issuer_rev_reg_record import IssuerRevRegRecord class TestRevocationRoutes(IsolatedAsyncioTestCase): @@ -1189,6 +1193,29 @@ async def test_post_process_routes(self): assert "tags" in mock_app._state["swagger_dict"] + @mock.patch.object( + IssuerRevRegRecord, "retrieve_by_revoc_reg_id", return_value=mock.MagicMock() + ) + @mock.patch.object( + RevocationManager, "update_rev_reg_revoked_state", return_value=(True, True, True) + ) + async def test_update_rev_reg_revoked_state(self, *_): + self.request.query = {"apply_ledger_update": "true"} + self.request.match_info = {"rev_reg_id": "rev_reg_id"} + + mock_ledger_manager = mock.MagicMock(BaseMultipleLedgerManager, autospec=True) + mock_ledger_manager.get_write_ledgers = mock.CoroutineMock("ledger") + self.context.injector.bind_instance( + BaseMultipleLedgerManager, mock_ledger_manager + ) + + mock_ledger = mock.MagicMock(BaseLedger, autospec=True) + mock_ledger.pool = mock.MagicMock(genesis_txns="genesis_txns") + self.context.injector.bind_instance(BaseLedger, mock_ledger) + + result = await test_module.update_rev_reg_revoked_state(self.request) + assert result.status == 200 + class TestDeleteTails(IsolatedAsyncioTestCase): def setUp(self):