From 630830e9b468012011b59bab4074a3bb6b18b9f9 Mon Sep 17 00:00:00 2001 From: EmadAnwer Date: Sun, 9 Jun 2024 17:41:10 +0300 Subject: [PATCH] ADD: tests for VCDI handler - test_create_offer - test_create_request - test_issue_credential_revocable - test_match_sent_cred_def_id_error - test_store_credential Signed-off-by: EmadAnwer --- .../v2_0/formats/vc_di/tests/test_handler.py | 375 +++++++++++++----- 1 file changed, 283 insertions(+), 92 deletions(-) diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/tests/test_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/tests/test_handler.py index 04d3c552ef..02a99804b2 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/tests/test_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/tests/test_handler.py @@ -2,8 +2,25 @@ from time import time import json +from aries_cloudagent.anoncreds.models.anoncreds_cred_def import ( + CredDef, + GetCredDefResult, +) +from aries_cloudagent.anoncreds.models.anoncreds_revocation import ( + GetRevRegDefResult, + RevRegDef, +) +from aries_cloudagent.anoncreds.registry import AnonCredsRegistry +from aries_cloudagent.askar.profile_anon import AskarAnoncredsProfile +from aries_cloudagent.protocols.issue_credential.v2_0.messages.cred_issue import ( + V20CredIssue, +) +from aries_cloudagent.protocols.issue_credential.v2_0.models.cred_ex_record import ( + V20CredExRecord, +) +from aries_cloudagent.wallet.did_info import DIDInfo import pytest -from .......anoncreds.holder import AnonCredsHolder +from .......anoncreds.holder import AnonCredsHolder, AnonCredsHolderError from .......messaging.credential_definitions.util import ( CRED_DEF_SENT_RECORD_TYPE, ) @@ -176,7 +193,8 @@ "data_model_version": "2.0", "binding_proof": { "anoncreds_link_secret": { - "entropy": "M7PyEDW7WfLDA8UH4BPhVN", + "prover_did": f"did:sov:{TEST_DID}", + "entropy": f"did:sov:{TEST_DID}", "cred_def_id": CRED_DEF_ID, "blinded_ms": { "u": "10047077609650450290609991930929594521921208780899757965398360086992099381832995073955506958821655372681970112562804577530208651675996528617262693958751195285371230790988741041496869140904046414320278189103736789305088489636024127715978439300785989247215275867951013255925809735479471883338351299180591011255281885961242995409072561940244771612447316409017677474822482928698183528232263803799926211692640155689629903898365777273000566450465466723659861801656618726777274689021162957914736922404694190070274236964163273886807208820068271673047750886130307545831668836096290655823576388755329367886670574352063509727295", @@ -234,7 +252,9 @@ async def asyncSetUp(self): self.profile = self.session.profile self.context = self.profile.context setattr(self.profile, "session", mock.MagicMock(return_value=self.session)) - + self.session.wallet.get_public_did = mock.CoroutineMock( + return_value=DIDInfo(TEST_DID, None, None, None, True) + ) # Ledger Ledger = mock.MagicMock() self.ledger = Ledger() @@ -381,7 +401,6 @@ async def test_receive_proposal(self): # Not much to assert. Receive proposal doesn't do anything await self.handler.receive_proposal(cred_ex_record, cred_proposal_message) - @pytest.mark.skip(reason="Anoncreds-break") async def test_create_offer(self): schema_id_parts = SCHEMA_ID.split(":") @@ -423,26 +442,36 @@ async def test_create_offer(self): ) await self.session.storage.add_record(cred_def_record) - self.issuer.create_credential = mock.CoroutineMock( - return_value=json.dumps(VCDI_OFFER) - ) + with mock.patch.object( + test_module, "AnonCredsIssuer", return_value=self.issuer + ) as mock_issuer: + self.issuer.create_credential_offer = mock.CoroutineMock( + return_value=json.dumps( + VCDI_OFFER["binding_method"]["anoncreds_link_secret"] + ) + ) - (cred_format, attachment) = await self.handler.create_offer(cred_proposal) + self.issuer.match_created_credential_definitions = mock.CoroutineMock( + return_value=CRED_DEF_ID + ) - self.issuer.create_credential.assert_called_once_with(CRED_DEF_ID) + (cred_format, attachment) = await self.handler.create_offer(cred_proposal) - # assert identifier match - assert cred_format.attach_id == self.handler.format.api == attachment.ident + self.issuer.create_credential_offer.assert_called_once_with(CRED_DEF_ID) - # assert content of attachment is proposal data - assert attachment.content == VCDI_OFFER + assert cred_format.attach_id == self.handler.format.api == attachment.ident - # assert data is encoded as base64 - assert attachment.data.base64 + assert ( + attachment.content["binding_method"]["anoncreds_link_secret"] + == VCDI_OFFER["binding_method"]["anoncreds_link_secret"] + ) + + # Assert data is encoded as base64 + assert attachment.data.base64 + # TODO: fix this get_public_did return None in the sc + # (cred_format, attachment) = await self.handler.create_offer(cred_proposal) - self.issuer.create_credential_offer.reset_mock() - (cred_format, attachment) = await self.handler.create_offer(cred_proposal) - self.issuer.create_credential_offer.assert_not_called() + # self.issuer.create_credential_offer.assert_not_called() @pytest.mark.skip(reason="Anoncreds-break") async def test_receive_offer(self): @@ -452,9 +481,25 @@ async def test_receive_offer(self): # Not much to assert. Receive offer doesn't do anything await self.handler.receive_offer(cred_ex_record, cred_offer_message) - @pytest.mark.skip(reason="Anoncreds-break") async def test_create_request(self): + # Define your mock credential definition + mock_credential_definition_result = GetCredDefResult( + credential_definition=CredDef( + issuer_id=TEST_DID, schema_id=SCHEMA_ID, type="CL", tag="tag1", value={} + ), + credential_definition_id=CRED_DEF_ID, + resolution_metadata={}, + credential_definition_metadata={}, + ) + mock_creds_registry = mock.MagicMock() + mock_creds_registry.get_credential_definition = mock.AsyncMock( + return_value=mock_credential_definition_result + ) + + # Inject the MagicMock into the context + self.context.injector.bind_instance(AnonCredsRegistry, mock_creds_registry) + holder_did = "did" cred_offer = V20CredOffer( @@ -466,62 +511,67 @@ async def test_create_request(self): ], ) ], - # TODO here offers_attach=[AttachDecorator.data_base64(VCDI_OFFER, ident="0")], ) - cred_ex_record = V20CredExRecordIndy( + cred_ex_record = V20CredExRecord( cred_ex_id="dummy-id", - state=V20CredExRecordIndy.STATE_OFFER_RECEIVED, + state=V20CredExRecord.STATE_OFFER_RECEIVED, cred_offer=cred_offer.serialize(), ) - cred_def = {"cred": "def"} - self.ledger.get_credential_definition = mock.CoroutineMock( - return_value=cred_def - ) - cred_req_meta = {} self.holder.create_credential_request = mock.CoroutineMock( return_value=(json.dumps(VCDI_CRED_REQ), json.dumps(cred_req_meta)) ) - (cred_format, attachment) = await self.handler.create_request( - cred_ex_record, {"holder_did": holder_did} - ) - - self.holder.create_credential_request.assert_called_once_with( - VCDI_OFFER, cred_def, holder_did - ) - - # assert identifier match - assert cred_format.attach_id == self.handler.format.api == attachment.ident + self.profile = mock.MagicMock(AskarAnoncredsProfile) + self.context.injector.bind_instance(AskarAnoncredsProfile, self.profile) + with mock.patch.object( + AnonCredsHolder, "create_credential_request", mock.CoroutineMock() + ) as mock_create: - # assert content of attachment is proposal data - assert attachment.content == VCDI_CRED_REQ + mock_create.return_value = ( + json.dumps(VCDI_CRED_REQ["binding_proof"]["anoncreds_link_secret"]), + json.dumps(cred_req_meta), + ) + (cred_format, attachment) = await self.handler.create_request( + cred_ex_record, {"holder_did": holder_did} + ) - # assert data is encoded as base64 - assert attachment.data.base64 + legacy_offer = await self.handler._prepare_legacy_offer( + VCDI_OFFER, SCHEMA_ID + ) + mock_create.assert_called_once_with( + legacy_offer, + mock_credential_definition_result.credential_definition, + holder_did, + ) + assert cred_format.attach_id == self.handler.format.api == attachment.ident - # cover case with cache (change ID to prevent already exists error) - cred_ex_record._id = "dummy-id2" - await self.handler.create_request(cred_ex_record, {"holder_did": holder_did}) + del VCDI_CRED_REQ["binding_proof"]["anoncreds_link_secret"]["prover_did"] + assert attachment.content == VCDI_CRED_REQ + assert attachment.data.base64 - # cover case with no cache in injection context - self.context.injector.clear_binding(BaseCache) - cred_ex_record._id = "dummy-id3" - self.context.injector.bind_instance( - BaseMultitenantManager, - mock.MagicMock(MultitenantManager, autospec=True), - ) - with mock.patch.object( - IndyLedgerRequestsExecutor, - "get_ledger_for_identifier", - mock.CoroutineMock(return_value=(None, self.ledger)), - ): + cred_ex_record._id = "dummy-id2" await self.handler.create_request( cred_ex_record, {"holder_did": holder_did} ) + self.context.injector.clear_binding(BaseCache) + cred_ex_record._id = "dummy-id3" + self.context.injector.bind_instance( + BaseMultitenantManager, + mock.MagicMock(MultitenantManager, autospec=True), + ) + with mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + mock.CoroutineMock(return_value=(None, self.ledger)), + ): + await self.handler.create_request( + cred_ex_record, {"holder_did": holder_did} + ) + @pytest.mark.skip(reason="Anoncreds-break") async def test_receive_request(self): cred_ex_record = mock.MagicMock() @@ -530,7 +580,6 @@ async def test_receive_request(self): # Not much to assert. Receive request doesn't do anything await self.handler.receive_request(cred_ex_record, cred_request_message) - @pytest.mark.skip(reason="Anoncreds-break") async def test_issue_credential_revocable(self): attr_values = { "legalName": "value", @@ -552,7 +601,6 @@ async def test_issue_credential_revocable(self): ], ) ], - # TODO here offers_attach=[AttachDecorator.data_base64(VCDI_OFFER, ident="0")], ) cred_request = V20CredRequest( @@ -564,59 +612,52 @@ async def test_issue_credential_revocable(self): ], ) ], - # TODO here requests_attach=[AttachDecorator.data_base64(VCDI_CRED_REQ, ident="0")], ) - cred_ex_record = V20CredExRecordIndy( + cred_ex_record = V20CredExRecord( cred_ex_id="dummy-cxid", cred_offer=cred_offer.serialize(), cred_request=cred_request.serialize(), - initiator=V20CredExRecordIndy.INITIATOR_SELF, - role=V20CredExRecordIndy.ROLE_ISSUER, - state=V20CredExRecordIndy.STATE_REQUEST_RECEIVED, + initiator=V20CredExRecord.INITIATOR_SELF, + role=V20CredExRecord.ROLE_ISSUER, + state=V20CredExRecord.STATE_REQUEST_RECEIVED, ) cred_rev_id = "1000" - self.issuer.create_credential = mock.CoroutineMock( - return_value=(json.dumps(VCDI_CRED), cred_rev_id) - ) - - with mock.patch.object(test_module, "IndyRevocation", autospec=True) as revoc: - revoc.return_value.get_or_create_active_registry = mock.CoroutineMock( - return_value=( - mock.MagicMock( # active_rev_reg_rec - revoc_reg_id=REV_REG_ID, - ), - mock.MagicMock( # rev_reg - tails_local_path="dummy-path", - get_or_fetch_local_tails_path=(mock.CoroutineMock()), - max_creds=10, - ), - ) + dummy_registry = "dummy-registry" + expected_credential = json.dumps(VCDI_CRED) + + # Mock AnonCredsRevocation and its method create_credential_w3c + with mock.patch.object( + test_module, "AnonCredsRevocation", autospec=True + ) as MockAnonCredsRevocation: + mock_issuer = MockAnonCredsRevocation.return_value + mock_issuer.create_credential_w3c = mock.CoroutineMock( + return_value=(expected_credential, cred_rev_id, dummy_registry) ) + # Call the method under test (cred_format, attachment) = await self.handler.issue_credential( cred_ex_record, retries=1 ) - - self.issuer.create_credential.assert_called_once_with( - SCHEMA, - VCDI_OFFER, - VCDI_CRED_REQ, + legacy_offer = await self.handler._prepare_legacy_offer( + VCDI_OFFER, SCHEMA_ID + ) + legacy_request = await self.handler._prepare_legacy_request( + VCDI_CRED_REQ, CRED_DEF_ID + ) + # Verify the mocked method was called with the expected parameters + mock_issuer.create_credential_w3c.assert_called_once_with( + legacy_offer, + legacy_request, attr_values, - REV_REG_ID, - "dummy-path", ) - # assert identifier match + # Assert the results are as expected assert cred_format.attach_id == self.handler.format.api == attachment.ident - - # assert content of attachment is proposal data - assert attachment.content == VCDI_CRED - - # assert data is encoded as base64 assert attachment.data.base64 + assert attachment.content == {"credential": VCDI_CRED} @pytest.mark.skip(reason="Anoncreds-break") async def test_issue_credential_non_revocable(self): @@ -702,3 +743,153 @@ async def test_issue_credential_non_revocable(self): # assert data is encoded as base64 assert attachment.data.base64 + + @pytest.mark.asyncio + async def test_match_sent_cred_def_id_error(self): + tag_query = {"tag": "test_tag"} + + with self.assertRaises(V20CredFormatError) as context: + await self.handler._match_sent_cred_def_id(tag_query) + assert "Issuer has no operable cred def for proposal spec " in str( + context.exception + ) + + @pytest.mark.asyncio + async def test_store_credential(self): + attr_values = { + "legalName": "value", + "jurisdictionId": "value", + "incorporationDate": "value", + } + cred_preview = V20CredPreview( + attributes=[ + V20CredAttrSpec(name=k, value=v) for (k, v) in attr_values.items() + ] + ) + cred_offer = V20CredOffer( + credential_preview=cred_preview, + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ + V20CredFormat.Format.VC_DI.api + ], + ) + ], + offers_attach=[AttachDecorator.data_base64(VCDI_OFFER, ident="0")], + ) + cred_request = V20CredRequest( + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ + V20CredFormat.Format.VC_DI.api + ], + ) + ], + requests_attach=[AttachDecorator.data_base64(VCDI_CRED_REQ, ident="0")], + ) + cred_issue = V20CredIssue( + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_ISSUE][ + V20CredFormat.Format.VC_DI.api + ], + ) + ], + credentials_attach=[AttachDecorator.data_base64(VCDI_CRED, ident="0")], + ) + cred_ex_record = V20CredExRecord( + cred_ex_id="dummy-cxid", + cred_offer=cred_offer.serialize(), + cred_request=cred_request.serialize(), + cred_issue=cred_issue.serialize(), + initiator=V20CredExRecord.INITIATOR_SELF, + role=V20CredExRecord.ROLE_ISSUER, + state=V20CredExRecord.STATE_REQUEST_RECEIVED, + ) + cred_id = "dummy-cred-id" + + # Define your mock credential definition + mock_credential_definition_result = GetCredDefResult( + credential_definition=CredDef( + issuer_id=TEST_DID, schema_id=SCHEMA_ID, type="CL", tag="tag1", value={} + ), + credential_definition_id=CRED_DEF_ID, + resolution_metadata={}, + credential_definition_metadata={}, + ) + mock_creds_registry = mock.AsyncMock() + mock_creds_registry.get_credential_definition = mock.AsyncMock( + return_value=mock_credential_definition_result + ) + + revocation_registry = RevRegDef( + cred_def_id=CRED_DEF_ID, + issuer_id=TEST_DID, + tag="tag1", + type="CL_ACCUM", + value={}, + ) + + mock_creds_registry.get_revocation_registry_definition = mock.AsyncMock( + return_value=GetRevRegDefResult( + revocation_registry=revocation_registry, + revocation_registry_id="rr-id", + resolution_metadata={}, + revocation_registry_metadata={}, + ) + ) + # Inject the MagicMock into the context + self.context.injector.bind_instance(AnonCredsRegistry, mock_creds_registry) + self.profile = mock.AsyncMock(AskarAnoncredsProfile) + self.context.injector.bind_instance(AskarAnoncredsProfile, self.profile) + with mock.patch.object( + test_module.AnonCredsRevocation, + "get_or_fetch_local_tails_path", + mock.CoroutineMock(), + ) as mock_get_or_fetch_local_tails_path: + with self.assertRaises(V20CredFormatError) as context: + await self.handler.store_credential(cred_ex_record, cred_id) + assert ( + "No credential exchange didcomm/ detail record found for cred ex id dummy-cxid" + in str(context.exception) + ) + + record = V20CredExRecordIndy( + cred_ex_indy_id="dummy-cxid", + rev_reg_id="rr-id", + cred_ex_id="dummy-cxid", + cred_id_stored=cred_id, + cred_request_metadata="dummy-metadata", + cred_rev_id="0", + ) + + record.save = mock.CoroutineMock() + self.handler.get_detail_record = mock.AsyncMock(return_value=record) + with mock.patch.object( + AnonCredsHolder, + "store_credential_w3c", + mock.AsyncMock(), + ) as mock_store_credential: + # Error case: no cred ex record found + + await self.handler.store_credential(cred_ex_record, cred_id) + + mock_store_credential.assert_called_once_with( + mock_credential_definition_result.credential_definition.serialize(), + VCDI_CRED["credential"], + record.cred_request_metadata, + None, + credential_id=cred_id, + rev_reg_def=revocation_registry.serialize(), + ) + + with mock.patch.object( + AnonCredsHolder, + "store_credential_w3c", + mock.AsyncMock(side_effect=AnonCredsHolderError), + ) as mock_store_credential: + with self.assertRaises(AnonCredsHolderError) as context: + await self.handler.store_credential(cred_ex_record, cred_id)