From 21e6d3d00e48090b47505b2f2bc8c676b46c0542 Mon Sep 17 00:00:00 2001 From: Patrick St-Louis Date: Fri, 9 Jan 2026 10:09:45 -0500 Subject: [PATCH 1/2] add tag to cred def event, add schema event, fix tests Signed-off-by: Patrick St-Louis --- acapy_agent/anoncreds/events.py | 62 ++++++++++++++++++++++ acapy_agent/anoncreds/issuer.py | 39 ++++++++++++-- acapy_agent/anoncreds/tests/test_issuer.py | 33 +++++++++--- 3 files changed, 124 insertions(+), 10 deletions(-) diff --git a/acapy_agent/anoncreds/events.py b/acapy_agent/anoncreds/events.py index eef517a884..fd48a2ccd9 100644 --- a/acapy_agent/anoncreds/events.py +++ b/acapy_agent/anoncreds/events.py @@ -8,6 +8,9 @@ FIRST_REGISTRY_TAG = str(0) # This tag is used to signify it is the first registry +# Schema finished event +SCHEMA_FINISHED_EVENT = "anoncreds::schema::finished" + # Initial credential definition event, kicks off the revocation setup process CRED_DEF_FINISHED_EVENT = "anoncreds::credential-definition::finished" @@ -102,6 +105,7 @@ class CredDefFinishedPayload(NamedTuple): issuer_id: str support_revocation: bool max_cred_num: int + tag: str options: dict @@ -131,6 +135,7 @@ def with_payload( issuer_id: str, support_revocation: bool, max_cred_num: int, + tag: str, options: Optional[dict] = None, ): """With payload.""" @@ -140,6 +145,7 @@ def with_payload( issuer_id=issuer_id, support_revocation=support_revocation, max_cred_num=max_cred_num, + tag=tag, options=options or {}, ) return cls(payload) @@ -150,6 +156,62 @@ def payload(self) -> CredDefFinishedPayload: return self._payload +class SchemaFinishedPayload(NamedTuple): + """Payload of schema finished event.""" + + schema_id: str + issuer_id: str + name: str + version: str + attr_names: list + options: dict + + +class SchemaFinishedEvent(Event): + """Event for schema finished.""" + + event_topic = SCHEMA_FINISHED_EVENT + + def __init__( + self, + payload: SchemaFinishedPayload, + ): + """Initialize an instance. + + Args: + payload: SchemaFinishedPayload + + """ + self._topic = self.event_topic + self._payload = payload + + @classmethod + def with_payload( + cls, + schema_id: str, + issuer_id: str, + name: str, + version: str, + attr_names: list, + options: Optional[dict] = None, + ): + """With payload.""" + payload = SchemaFinishedPayload( + schema_id=schema_id, + issuer_id=issuer_id, + name=name, + version=version, + attr_names=attr_names, + options=options or {}, + ) + return cls(payload) + + @property + def payload(self) -> SchemaFinishedPayload: + """Return payload.""" + return self._payload + + class RevRegDefFinishedPayload(NamedTuple): """Payload of rev reg def finished event.""" diff --git a/acapy_agent/anoncreds/issuer.py b/acapy_agent/anoncreds/issuer.py index aec57c5473..f73eb99c58 100644 --- a/acapy_agent/anoncreds/issuer.py +++ b/acapy_agent/anoncreds/issuer.py @@ -33,7 +33,7 @@ STATE_FINISHED, ) from .error_messages import ANONCREDS_PROFILE_REQUIRED_MSG -from .events import CredDefFinishedEvent +from .events import CredDefFinishedEvent, SchemaFinishedEvent from .models.credential_definition import CredDef, CredDefResult from .models.schema import AnonCredsSchema, GetSchemaResult, SchemaResult, SchemaState from .registry import AnonCredsRegistry @@ -156,6 +156,18 @@ async def store_schema( "state": result.schema_state.state, }, ) + + if result.schema_state.state == STATE_FINISHED: + await self.notify( + SchemaFinishedEvent.with_payload( + schema_id=result.schema_state.schema_id, + issuer_id=result.schema_state.schema.issuer_id, + name=result.schema_state.schema.name, + version=result.schema_state.schema.version, + attr_names=result.schema_state.schema.attr_names, + options={}, + ) + ) except DBError as err: raise AnonCredsIssuerError("Error storing schema") from err @@ -240,9 +252,25 @@ async def create_and_register_schema( async def finish_schema(self, job_id: str, schema_id: str) -> None: """Mark a schema as finished.""" async with self.profile.transaction() as txn: - await self._finish_registration(txn, CATEGORY_SCHEMA, job_id, schema_id) + entry = await self._finish_registration( + txn, CATEGORY_SCHEMA, job_id, schema_id + ) await txn.commit() + from .models.schema import AnonCredsSchema + + schema = AnonCredsSchema.from_json(entry.value) + await self.notify( + SchemaFinishedEvent.with_payload( + schema_id=schema_id, + issuer_id=schema.issuer_id, + name=schema.name, + version=schema.version, + attr_names=schema.attr_names, + options={}, + ) + ) + async def get_created_schemas( self, name: Optional[str] = None, @@ -434,13 +462,17 @@ async def store_credential_definition( await txn.commit() if cred_def_result.credential_definition_state.state == STATE_FINISHED: + cred_def = ( + cred_def_result.credential_definition_state.credential_definition + ) await self.notify( CredDefFinishedEvent.with_payload( schema_id=schema_result.schema_id, cred_def_id=identifier, - issuer_id=cred_def_result.credential_definition_state.credential_definition.issuer_id, + issuer_id=cred_def.issuer_id, support_revocation=support_revocation, max_cred_num=max_cred_num, + tag=cred_def.tag, options=options, ) ) @@ -477,6 +509,7 @@ async def finish_cred_def( issuer_id=cred_def.issuer_id, support_revocation=support_revocation, max_cred_num=max_cred_num, + tag=cred_def.tag, options=options, ) ) diff --git a/acapy_agent/anoncreds/tests/test_issuer.py b/acapy_agent/anoncreds/tests/test_issuer.py index 167d24936e..92df8e04b5 100644 --- a/acapy_agent/anoncreds/tests/test_issuer.py +++ b/acapy_agent/anoncreds/tests/test_issuer.py @@ -156,10 +156,13 @@ async def test_create_and_register_schema_finds_schema_raises_x( attr_names=["attr1", "attr2"], ) + @mock.patch.object(test_module.AnonCredsIssuer, "notify") + @mock.patch.object(test_module.AnonCredsIssuer, "store_schema") @mock.patch.object(AskarAnonCredsProfileSession, "handle") - async def test_create_and_register_schema(self, mock_session_handle): + async def test_create_and_register_schema( + self, mock_session_handle, mock_store_schema, mock_notify + ): mock_session_handle.fetch_all = mock.CoroutineMock(return_value=[]) - mock_session_handle.insert = mock.CoroutineMock(return_value=None) self.profile.inject = mock.Mock( return_value=mock.MagicMock( register_schema=mock.CoroutineMock(return_value=get_mock_schema_result()) @@ -174,11 +177,12 @@ async def test_create_and_register_schema(self, mock_session_handle): assert result is not None mock_session_handle.fetch_all.assert_called_once() - mock_session_handle.insert.assert_called_once() + mock_store_schema.assert_called_once() + @mock.patch.object(test_module.AnonCredsIssuer, "notify") @mock.patch.object(AskarAnonCredsProfileSession, "handle") async def test_create_and_register_schema_missing_schema_id_or_job_id( - self, mock_session_handle + self, mock_session_handle, mock_notify ): mock_session_handle.fetch_all = mock.CoroutineMock(return_value=[]) mock_session_handle.insert = mock.CoroutineMock(return_value=None) @@ -278,9 +282,10 @@ async def test_create_and_register_schema_fail_insert(self, mock_session_handle) mock_session_handle.fetch_all.assert_called_once() mock_session_handle.insert.assert_called_once() + @mock.patch.object(test_module.AnonCredsIssuer, "notify") @mock.patch.object(AskarAnonCredsProfileSession, "handle") async def test_create_and_register_schema_already_exists_but_not_in_wallet( - self, mock_session_handle + self, mock_session_handle, mock_notify ): mock_session_handle.fetch_all = mock.CoroutineMock(return_value=[]) mock_session_handle.insert = mock.CoroutineMock(return_value=None) @@ -308,9 +313,10 @@ async def test_create_and_register_schema_already_exists_but_not_in_wallet( attr_names=["attr1", "attr2"], ) + @mock.patch.object(test_module.AnonCredsIssuer, "notify") @mock.patch.object(AskarAnonCredsProfileSession, "handle") async def test_create_and_register_schema_without_job_id_or_schema_id_raises_x( - self, mock_session_handle + self, mock_session_handle, mock_notify ): mock_session_handle.fetch_all = mock.CoroutineMock(return_value=[]) mock_session_handle.insert = mock.CoroutineMock(return_value=None) @@ -381,7 +387,20 @@ async def test_create_and_register_schema_with_endorsed_transaction_response_doe assert isinstance(result, SchemaResult) assert mock_store_schema.called - async def test_finish_schema(self): + @mock.patch.object(test_module.AnonCredsIssuer, "notify") + @mock.patch.object(test_module.AnonCredsIssuer, "_finish_registration") + async def test_finish_schema(self, mock_finish_registration, mock_notify): + # Mock entry with valid schema JSON + mock_entry = mock.MagicMock() + mock_entry.value = json.dumps( + { + "issuerId": "issuer-id", + "name": "name", + "version": "1.0", + "attrNames": ["attr1", "attr2"], + } + ) + mock_finish_registration.return_value = mock_entry self.profile.transaction = mock.Mock( return_value=mock.MagicMock( commit=mock.CoroutineMock(return_value=None), From c53038a4049adce75cdda40da6b9afc7d84b5cba Mon Sep 17 00:00:00 2001 From: Patrick St-Louis Date: Fri, 9 Jan 2026 10:42:45 -0500 Subject: [PATCH 2/2] add test coverage Signed-off-by: Patrick St-Louis --- acapy_agent/anoncreds/tests/test_issuer.py | 54 ++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/acapy_agent/anoncreds/tests/test_issuer.py b/acapy_agent/anoncreds/tests/test_issuer.py index 92df8e04b5..487e868318 100644 --- a/acapy_agent/anoncreds/tests/test_issuer.py +++ b/acapy_agent/anoncreds/tests/test_issuer.py @@ -33,6 +33,7 @@ from ...tests import mock from ...utils.testing import create_test_profile from .. import issuer as test_module +from ..events import CredDefFinishedEvent, SchemaFinishedEvent class MockSchemaEntry: @@ -408,6 +409,47 @@ async def test_finish_schema(self, mock_finish_registration, mock_notify): ) await self.issuer.finish_schema(job_id="job-id", schema_id="schema-id") + # Verify schema event was emitted with correct payload + mock_notify.assert_called_once() + call_args = mock_notify.call_args + assert isinstance(call_args[0][0], SchemaFinishedEvent) + event = call_args[0][0] + assert event.payload.schema_id == "schema-id" + assert event.payload.issuer_id == "issuer-id" + assert event.payload.name == "name" + assert event.payload.version == "1.0" + assert event.payload.attr_names == ["attr1", "attr2"] + assert event.payload.options == {} + + @mock.patch.object(test_module.AnonCredsIssuer, "notify") + async def test_store_schema_emits_event(self, mock_notify): + """Test that store_schema emits SchemaFinishedEvent when state is finished.""" + # Mock profile.session() for store_schema - it returns an async context manager + mock_session_handle = mock.MagicMock() + mock_session_handle.insert = mock.CoroutineMock(return_value=None) + mock_session = mock.MagicMock() + mock_session.handle = mock_session_handle # Set handle property + # __aenter__ and __aexit__ must be coroutines (async methods) + mock_session.__aenter__ = mock.CoroutineMock(return_value=mock_session) + mock_session.__aexit__ = mock.CoroutineMock(return_value=None) + # profile.session() is a method that returns an async context manager (not a coroutine) + self.profile.session = mock.Mock(return_value=mock_session) + + schema_result = get_mock_schema_result() + await self.issuer.store_schema(schema_result) + + # Verify schema event was emitted + mock_notify.assert_called_once() + call_args = mock_notify.call_args + assert isinstance(call_args[0][0], SchemaFinishedEvent) + event = call_args[0][0] + assert event.payload.schema_id == "schema-id" + assert event.payload.issuer_id == "issuer-id" + assert event.payload.name == "name" + assert event.payload.version == "1.0" + assert event.payload.attr_names == ["attr1", "attr2"] + assert event.payload.options == {} + @mock.patch.object(AskarAnonCredsProfileSession, "handle") async def test_get_created_schemas(self, mock_session_handle): mock_session_handle.fetch_all = mock.CoroutineMock( @@ -692,7 +734,19 @@ async def test_create_and_register_credential_definition_finishes(self, mock_not ) assert isinstance(result, CredDefResult) + # Verify cred def event was emitted with tag mock_notify.assert_called_once() + call_args = mock_notify.call_args + assert isinstance(call_args[0][0], CredDefFinishedEvent) + event = call_args[0][0] + assert event.payload.schema_id == "schema-id" + # When job_id exists, identifier is job_id, not cred_def_id + assert event.payload.cred_def_id == "job-id" + assert event.payload.issuer_id == "did:sov:3avoBCqDMFHFaKUHug9s8W" + assert event.payload.tag == "tag" # Verify tag is included in event + assert event.payload.support_revocation is False + assert event.payload.max_cred_num == 1000 # Default value + assert event.payload.options == {} @mock.patch.object(test_module.AnonCredsIssuer, "notify") async def test_create_and_register_credential_definition_errors(self, mock_notify):