diff --git a/aries_cloudagent/messaging/credentials/manager.py b/aries_cloudagent/messaging/credentials/manager.py index 52a00d4992..113b196a6e 100644 --- a/aries_cloudagent/messaging/credentials/manager.py +++ b/aries_cloudagent/messaging/credentials/manager.py @@ -65,10 +65,18 @@ async def create_offer( """ - issuer: BaseIssuer = await self.context.inject(BaseIssuer) - credential_offer = await issuer.create_credential_offer( - credential_definition_id - ) + cache_key = f"credential_offer::{credential_definition_id}" + cached = await CredentialExchange.get_cached_key(self.context, cache_key) + if cached: + credential_offer = cached["offer"] + else: + issuer: BaseIssuer = await self.context.inject(BaseIssuer) + credential_offer = await issuer.create_credential_offer( + credential_definition_id + ) + await CredentialExchange.set_cached_key( + self.context, cache_key, {"offer": credential_offer}, 3600 + ) credential_offer_message = CredentialOffer( offer_json=json.dumps(credential_offer) @@ -146,19 +154,41 @@ async def create_request( credential_exchange_record.credential_exchange_id, ) else: - ledger: BaseLedger = await self.context.inject(BaseLedger) - async with ledger: - credential_definition = await ledger.get_credential_definition( - credential_definition_id - ) - - holder: BaseHolder = await self.context.inject(BaseHolder) - ( - credential_exchange_record.credential_request, - credential_exchange_record.credential_request_metadata, - ) = await holder.create_credential_request( - credential_offer, credential_definition, did + nonce = credential_offer["nonce"] + cache_key = ( + f"credential_request::{credential_definition_id}::{did}::{nonce}" ) + cached = await CredentialExchange.get_cached_key(self.context, cache_key) + if cached: + ( + credential_exchange_record.credential_request, + credential_exchange_record.credential_request_metadata, + ) = (cached["request"], cached["metadata"]) + else: + ledger: BaseLedger = await self.context.inject(BaseLedger) + async with ledger: + credential_definition = await ledger.get_credential_definition( + credential_definition_id + ) + + holder: BaseHolder = await self.context.inject(BaseHolder) + ( + credential_exchange_record.credential_request, + credential_exchange_record.credential_request_metadata, + ) = await holder.create_credential_request( + credential_offer, credential_definition, did + ) + await CredentialExchange.set_cached_key( + self.context, + cache_key, + { + "request": credential_exchange_record.credential_request, + "metadata": ( + credential_exchange_record.credential_request_metadata + ), + }, + 7200, + ) credential_request_message = CredentialRequest( request=json.dumps(credential_exchange_record.credential_request) diff --git a/aries_cloudagent/messaging/issue_credential/v1_0/handlers/credential_offer_handler.py b/aries_cloudagent/messaging/issue_credential/v1_0/handlers/credential_offer_handler.py index b277ebbd83..d9b24067ca 100644 --- a/aries_cloudagent/messaging/issue_credential/v1_0/handlers/credential_offer_handler.py +++ b/aries_cloudagent/messaging/issue_credential/v1_0/handlers/credential_offer_handler.py @@ -1,7 +1,5 @@ """Basic message handler.""" - -from .....storage.error import StorageNotFoundError from ....base_handler import ( BaseHandler, BaseResponder, @@ -10,8 +8,6 @@ ) from ..manager import CredentialManager from ..messages.credential_offer import CredentialOffer -from ..messages.credential_proposal import CredentialProposal -from ..models.credential_exchange import V10CredentialExchange class CredentialOfferHandler(BaseHandler): @@ -38,43 +34,7 @@ async def handle(self, context: RequestContext, responder: BaseResponder): credential_manager = CredentialManager(context) - indy_offer = context.message.indy_offer(0) - credential_proposal_dict = CredentialProposal( - comment=context.message.comment, - credential_proposal=context.message.credential_preview, - cred_def_id=indy_offer["cred_def_id"], - schema_id=indy_offer["schema_id"], - ).serialize() - - # Get credential exchange record (holder sent proposal first) - # or create it (issuer sent offer first) - try: - ( - credential_exchange_record - ) = await V10CredentialExchange.retrieve_by_tag_filter( - context, - {"thread_id": context.message._thread_id}, - {"connection_id": context.connection_record.connection_id}, - ) - credential_exchange_record.credential_proposal_dict = ( - credential_proposal_dict - ) - except StorageNotFoundError: # issuer sent this offer free of any proposal - credential_exchange_record = V10CredentialExchange( - connection_id=context.connection_record.connection_id, - thread_id=context.message._thread_id, - initiator=V10CredentialExchange.INITIATOR_EXTERNAL, - role=V10CredentialExchange.ROLE_HOLDER, - credential_definition_id=indy_offer["cred_def_id"], - schema_id=indy_offer["schema_id"], - credential_proposal_dict=credential_proposal_dict, - ) - - credential_exchange_record.credential_offer = indy_offer - - credential_exchange_record = await credential_manager.receive_offer( - credential_exchange_record - ) + credential_exchange_record = await credential_manager.receive_offer() # If auto respond is turned on, automatically reply with credential request if context.settings.get("debug.auto_respond_credential_offer"): diff --git a/aries_cloudagent/messaging/issue_credential/v1_0/manager.py b/aries_cloudagent/messaging/issue_credential/v1_0/manager.py index fab88ae8fa..95a96ede72 100644 --- a/aries_cloudagent/messaging/issue_credential/v1_0/manager.py +++ b/aries_cloudagent/messaging/issue_credential/v1_0/manager.py @@ -1,14 +1,14 @@ """Classes to manage credentials.""" import logging +from typing import Tuple from ....config.injection_context import InjectionContext from ....error import BaseError from ....holder.base import BaseHolder from ....issuer.base import BaseIssuer from ....ledger.base import BaseLedger - -from ...decorators.attach_decorator import AttachDecorator +from ....storage.error import StorageNotFoundError from .messages.credential_issue import CredentialIssue from .messages.credential_offer import CredentialOffer @@ -16,12 +16,6 @@ from .messages.credential_request import CredentialRequest from .messages.credential_stored import CredentialStored from .messages.inner.credential_preview import CredentialPreview -from .message_types import ( - ATTACH_DECO_IDS, - CREDENTIAL_ISSUE, - CREDENTIAL_OFFER, - CREDENTIAL_REQUEST, -) from .models.credential_exchange import V10CredentialExchange @@ -54,16 +48,12 @@ def context(self) -> InjectionContext: return self._context async def prepare_send( - self, - credential_definition_id: str, - connection_id: str, - credential_proposal: CredentialProposal, + self, connection_id: str, credential_proposal: CredentialProposal ) -> V10CredentialExchange: """ Set up a new credential exchange for an automated send. Args: - credential_definition_id: Credential definition id for offer connection_id: Connection to create offer for credential_proposal: The credential proposal with preview on attribute values to use if auto_issue is enabled @@ -73,6 +63,12 @@ async def prepare_send( """ + credential_definition_id = credential_proposal.cred_def_id + if not credential_definition_id: + raise CredentialManagerError( + "Proposal credential definition ID is required" + ) + credential_exchange = V10CredentialExchange( auto_issue=True, connection_id=connection_id, @@ -95,7 +91,7 @@ async def create_proposal( comment: str = None, credential_preview: CredentialPreview = None, credential_definition_id: str, - ): + ) -> V10CredentialExchange: """ Create a credential proposal. @@ -150,7 +146,7 @@ async def create_proposal( ) return credential_exchange_record - async def receive_proposal(self,): + async def receive_proposal(self) -> V10CredentialExchange: """ Receive a credential proposal from message in context on manager creation. @@ -195,7 +191,7 @@ async def receive_proposal(self,): async def create_offer( self, credential_exchange_record: V10CredentialExchange, comment: str = None - ): + ) -> Tuple[V10CredentialExchange, CredentialOffer]: """ Create a credential offer, update credential exchange record. @@ -208,23 +204,30 @@ async def create_offer( """ credential_definition_id = credential_exchange_record.credential_definition_id + if credential_exchange_record.credential_proposal_dict: + cred_preview = CredentialProposal.deserialize( + credential_exchange_record.credential_proposal_dict + ).credential_proposal + else: + cred_preview = None - issuer: BaseIssuer = await self.context.inject(BaseIssuer) - credential_offer = await issuer.create_credential_offer( - credential_definition_id - ) + cache_key = f"credential_offer::{credential_definition_id}" + cached = await V10CredentialExchange.get_cached_key(self.context, cache_key) + if cached: + credential_offer = cached["offer"] + else: + issuer: BaseIssuer = await self.context.inject(BaseIssuer) + credential_offer = await issuer.create_credential_offer( + credential_definition_id + ) + await V10CredentialExchange.set_cached_key( + self.context, cache_key, {"offer": credential_offer}, 3600 + ) - cred_preview = CredentialProposal.deserialize( - credential_exchange_record.credential_proposal_dict - ).credential_proposal credential_offer_message = CredentialOffer( comment=comment, credential_preview=cred_preview, - offers_attach=[ - AttachDecorator.from_indy_dict( - indy_dict=credential_offer, ident=ATTACH_DECO_IDS[CREDENTIAL_OFFER] - ) - ], + offers_attach=[CredentialOffer.wrap_indy_offer(credential_offer)], ) credential_offer_message._thread = { @@ -244,18 +247,55 @@ async def create_offer( return (credential_exchange_record, credential_offer_message) - async def receive_offer(self, credential_exchange_record: V10CredentialExchange): + async def receive_offer(self) -> V10CredentialExchange: """ Receive a credential offer. - Args: - credential_exchange_record: Credential exchange record with offer to receive - Returns: The credential exchange record, updated """ + credential_offer_message: CredentialOffer = self.context.message + connection_id = self.context.connection_record.connection_id + + credential_preview = credential_offer_message.credential_preview + indy_offer = credential_offer_message.indy_offer(0) + + if credential_preview: + credential_proposal_dict = CredentialProposal( + comment=credential_offer_message.comment, + credential_proposal=credential_preview, + cred_def_id=indy_offer["cred_def_id"], + schema_id=indy_offer["schema_id"], + ).serialize() + else: + credential_proposal_dict = None + + # Get credential exchange record (holder sent proposal first) + # or create it (issuer sent offer first) + try: + ( + credential_exchange_record + ) = await V10CredentialExchange.retrieve_by_connection_and_thread( + self.context, connection_id, credential_offer_message._thread_id + ) + credential_exchange_record.credential_proposal_dict = ( + credential_proposal_dict + ) + except StorageNotFoundError: # issuer sent this offer free of any proposal + credential_exchange_record = V10CredentialExchange( + connection_id=connection_id, + thread_id=credential_offer_message._thread_id, + initiator=V10CredentialExchange.INITIATOR_EXTERNAL, + role=V10CredentialExchange.ROLE_HOLDER, + credential_definition_id=indy_offer["cred_def_id"], + schema_id=indy_offer["schema_id"], + credential_proposal_dict=credential_proposal_dict, + ) + + credential_exchange_record.credential_offer = indy_offer credential_exchange_record.state = V10CredentialExchange.STATE_OFFER_RECEIVED + await credential_exchange_record.save( self.context, reason="receive credential offer" ) @@ -264,7 +304,7 @@ async def receive_offer(self, credential_exchange_record: V10CredentialExchange) async def create_request( self, credential_exchange_record: V10CredentialExchange, holder_did: str - ): + ) -> Tuple[V10CredentialExchange, CredentialRequest]: """ Create a credential request. @@ -286,25 +326,49 @@ async def create_request( credential_exchange_record.credential_exchange_id, ) else: - ledger: BaseLedger = await self.context.inject(BaseLedger) - async with ledger: - credential_definition = await ledger.get_credential_definition( - credential_definition_id + if "nonce" not in credential_offer: + raise CredentialManagerError("Missing nonce in credential offer") + nonce = credential_offer["nonce"] + cache_key = ( + f"credential_request::{credential_definition_id}::{holder_did}::{nonce}" + ) + cached = await V10CredentialExchange.get_cached_key(self.context, cache_key) + if cached: + ( + credential_exchange_record.credential_request, + credential_exchange_record.credential_request_metadata, + ) = (cached["request"], cached["metadata"]) + else: + ledger: BaseLedger = await self.context.inject(BaseLedger) + async with ledger: + credential_definition = await ledger.get_credential_definition( + credential_definition_id + ) + + holder: BaseHolder = await self.context.inject(BaseHolder) + ( + credential_exchange_record.credential_request, + credential_exchange_record.credential_request_metadata, + ) = await holder.create_credential_request( + credential_offer, credential_definition, holder_did ) - holder: BaseHolder = await self.context.inject(BaseHolder) - ( - credential_exchange_record.credential_request, - credential_exchange_record.credential_request_metadata, - ) = await holder.create_credential_request( - credential_offer, credential_definition, holder_did - ) + await V10CredentialExchange.set_cached_key( + self.context, + cache_key, + { + "request": credential_exchange_record.credential_request, + "metadata": ( + credential_exchange_record.credential_request_metadata + ), + }, + 7200, + ) credential_request_message = CredentialRequest( requests_attach=[ - AttachDecorator.from_indy_dict( - indy_dict=credential_exchange_record.credential_request, - ident=ATTACH_DECO_IDS[CREDENTIAL_REQUEST], + CredentialRequest.wrap_indy_cred_req( + credential_exchange_record.credential_request ) ] ) @@ -334,10 +398,12 @@ async def receive_request(self): assert len(credential_request_message.requests_attach or []) == 1 credential_request = credential_request_message.indy_cred_req(0) - credential_exchange_record = await V10CredentialExchange.retrieve_by_tag_filter( + ( + credential_exchange_record + ) = await V10CredentialExchange.retrieve_by_connection_and_thread( self.context, - {"thread_id": credential_request_message._thread_id}, - {"connection_id": self.context.connection_record.connection_id}, + self.context.connection_record.connection_id, + credential_request_message._thread_id, ) credential_exchange_record.credential_request = credential_request credential_exchange_record.state = V10CredentialExchange.STATE_REQUEST_RECEIVED @@ -353,7 +419,7 @@ async def issue_credential( *, comment: str = None, credential_values: dict, - ): + ) -> Tuple[V10CredentialExchange, CredentialIssue]: """ Issue a credential. @@ -397,19 +463,16 @@ async def issue_credential( credential_message = CredentialIssue( comment=comment, credentials_attach=[ - AttachDecorator.from_indy_dict( - indy_dict=credential_exchange_record.credential, - ident=ATTACH_DECO_IDS[CREDENTIAL_ISSUE], + CredentialIssue.wrap_indy_credential( + credential_exchange_record.credential ) ], ) - credential_message._thread = { - "thid": credential_exchange_record.thread_id - } + credential_message._thread = {"thid": credential_exchange_record.thread_id} return (credential_exchange_record, credential_message) - async def receive_credential(self): + async def receive_credential(self) -> V10CredentialExchange: """ Receive a credential from an issuer. @@ -423,12 +486,12 @@ async def receive_credential(self): assert len(credential_message.credentials_attach or []) == 1 raw_credential = credential_message.indy_credential(0) - credential_exchange_record = await ( - V10CredentialExchange.retrieve_by_tag_filter( - self.context, - {"thread_id": credential_message._thread_id}, - {"connection_id": self.context.connection_record.connection_id}, - ) + ( + credential_exchange_record + ) = await V10CredentialExchange.retrieve_by_connection_and_thread( + self.context, + self.context.connection_record.connection_id, + credential_message._thread_id, ) credential_exchange_record.raw_credential = raw_credential @@ -439,7 +502,9 @@ async def receive_credential(self): await credential_exchange_record.save(self.context, reason="receive credential") return credential_exchange_record - async def store_credential(self, credential_exchange_record: V10CredentialExchange): + async def store_credential( + self, credential_exchange_record: V10CredentialExchange + ) -> Tuple[V10CredentialExchange, CredentialStored]: """ Store a credential in the wallet. @@ -486,7 +551,7 @@ async def store_credential(self, credential_exchange_record: V10CredentialExchan return credential_exchange_record, credential_stored_message - async def credential_stored(self): + async def credential_stored(self) -> V10CredentialExchange: """ Receive confirmation that holder stored credential. @@ -495,10 +560,12 @@ async def credential_stored(self): """ credential_stored_message = self.context.message - credential_exchange_record = await V10CredentialExchange.retrieve_by_tag_filter( + ( + credential_exchange_record + ) = await V10CredentialExchange.retrieve_by_connection_and_thread( self.context, - {"thread_id": credential_stored_message._thread_id}, - {"connection_id": self.context.connection_record.connection_id}, + self.context.connection_record.connection_id, + credential_stored_message._thread_id, ) credential_exchange_record.state = V10CredentialExchange.STATE_STORED @@ -506,3 +573,5 @@ async def credential_stored(self): # We're done with the exchange so delete await credential_exchange_record.delete_record(self.context) + + return credential_exchange_record diff --git a/aries_cloudagent/messaging/issue_credential/v1_0/messages/credential_issue.py b/aries_cloudagent/messaging/issue_credential/v1_0/messages/credential_issue.py index 614273d0b1..21bef37849 100644 --- a/aries_cloudagent/messaging/issue_credential/v1_0/messages/credential_issue.py +++ b/aries_cloudagent/messaging/issue_credential/v1_0/messages/credential_issue.py @@ -7,7 +7,7 @@ from ....agent_message import AgentMessage, AgentMessageSchema from ....decorators.attach_decorator import AttachDecorator, AttachDecoratorSchema -from ..message_types import CREDENTIAL_ISSUE +from ..message_types import ATTACH_DECO_IDS, CREDENTIAL_ISSUE HANDLER_CLASS = ( @@ -57,6 +57,13 @@ def indy_credential(self, index: int = 0): """ return self.credentials_attach[index].indy_dict + @classmethod + def wrap_indy_credential(cls, indy_cred: dict) -> AttachDecorator: + """Convert an indy credential offer to an attachment decorator.""" + return AttachDecorator.from_indy_dict( + indy_dict=indy_cred, ident=ATTACH_DECO_IDS[CREDENTIAL_ISSUE] + ) + class CredentialIssueSchema(AgentMessageSchema): """Credential schema.""" @@ -68,8 +75,5 @@ class Meta: comment = fields.Str(comment="Human-readable comment", required=False) credentials_attach = fields.Nested( - AttachDecoratorSchema, - required=True, - many=True, - data_key="credentials~attach" + AttachDecoratorSchema, required=True, many=True, data_key="credentials~attach" ) diff --git a/aries_cloudagent/messaging/issue_credential/v1_0/messages/credential_offer.py b/aries_cloudagent/messaging/issue_credential/v1_0/messages/credential_offer.py index 827a10c042..3fc8f8651d 100644 --- a/aries_cloudagent/messaging/issue_credential/v1_0/messages/credential_offer.py +++ b/aries_cloudagent/messaging/issue_credential/v1_0/messages/credential_offer.py @@ -7,13 +7,13 @@ from ....agent_message import AgentMessage, AgentMessageSchema from ....decorators.attach_decorator import AttachDecorator, AttachDecoratorSchema -from ..message_types import CREDENTIAL_OFFER +from ..message_types import ATTACH_DECO_IDS, CREDENTIAL_OFFER from .inner.credential_preview import CredentialPreview, CredentialPreviewSchema HANDLER_CLASS = ( "aries_cloudagent.messaging.issue_credential.v1_0.handlers." - + "credential_offer_handler.CredentialOfferHandler" + "credential_offer_handler.CredentialOfferHandler" ) @@ -52,7 +52,7 @@ def __init__( ) self.offers_attach = list(offers_attach) if offers_attach else [] - def indy_offer(self, index: int = 0): + def indy_offer(self, index: int = 0) -> dict: """ Retrieve and decode indy offer from attachment. @@ -63,6 +63,13 @@ def indy_offer(self, index: int = 0): """ return self.offers_attach[index].indy_dict + @classmethod + def wrap_indy_offer(cls, indy_offer: dict) -> AttachDecorator: + """Convert an indy credential offer to an attachment decorator.""" + return AttachDecorator.from_indy_dict( + indy_dict=indy_offer, ident=ATTACH_DECO_IDS[CREDENTIAL_OFFER] + ) + class CredentialOfferSchema(AgentMessageSchema): """Credential offer schema.""" @@ -75,8 +82,5 @@ class Meta: comment = fields.Str(required=False, allow_none=False) credential_preview = fields.Nested(CredentialPreviewSchema, required=False) offers_attach = fields.Nested( - AttachDecoratorSchema, - required=True, - many=True, - data_key="offers~attach" + AttachDecoratorSchema, required=True, many=True, data_key="offers~attach" ) diff --git a/aries_cloudagent/messaging/issue_credential/v1_0/messages/credential_request.py b/aries_cloudagent/messaging/issue_credential/v1_0/messages/credential_request.py index cdeedbd99d..6fa7e6668d 100644 --- a/aries_cloudagent/messaging/issue_credential/v1_0/messages/credential_request.py +++ b/aries_cloudagent/messaging/issue_credential/v1_0/messages/credential_request.py @@ -7,7 +7,7 @@ from ....agent_message import AgentMessage, AgentMessageSchema from ....decorators.attach_decorator import AttachDecorator, AttachDecoratorSchema -from ..message_types import CREDENTIAL_REQUEST +from ..message_types import ATTACH_DECO_IDS, CREDENTIAL_REQUEST HANDLER_CLASS = ( @@ -57,6 +57,13 @@ def indy_cred_req(self, index: int = 0): """ return self.requests_attach[index].indy_dict + @classmethod + def wrap_indy_cred_req(cls, indy_cred_req: dict) -> AttachDecorator: + """Convert an indy credential request to an attachment decorator.""" + return AttachDecorator.from_indy_dict( + indy_dict=indy_cred_req, ident=ATTACH_DECO_IDS[CREDENTIAL_REQUEST] + ) + class CredentialRequestSchema(AgentMessageSchema): """Credential request schema.""" @@ -68,8 +75,5 @@ class Meta: comment = fields.Str(required=False) requests_attach = fields.Nested( - AttachDecoratorSchema, - required=True, - many=True, - data_key="requests~attach" + AttachDecoratorSchema, required=True, many=True, data_key="requests~attach" ) diff --git a/aries_cloudagent/messaging/issue_credential/v1_0/models/credential_exchange.py b/aries_cloudagent/messaging/issue_credential/v1_0/models/credential_exchange.py index 495b4c0d1c..29af5f4919 100644 --- a/aries_cloudagent/messaging/issue_credential/v1_0/models/credential_exchange.py +++ b/aries_cloudagent/messaging/issue_credential/v1_0/models/credential_exchange.py @@ -3,6 +3,8 @@ from marshmallow import fields from marshmallow.validate import OneOf +from .....config.injection_context import InjectionContext + from ....models.base_record import BaseRecord, BaseRecordSchema from ....valid import INDY_CRED_DEF_ID, INDY_SCHEMA_ID, UUIDFour @@ -57,7 +59,7 @@ def __init__( auto_offer: bool = False, auto_issue: bool = False, error_msg: str = None, - **kwargs + **kwargs, ): """Initialize a new V10CredentialExchange.""" super().__init__(credential_exchange_id, state, **kwargs) @@ -112,6 +114,22 @@ def record_value(self) -> dict: ) } + @classmethod + async def retrieve_by_connection_and_thread( + cls, context: InjectionContext, connection_id: str, thread_id: str + ) -> "V10CredentialExchange": + """Retrieve a credential exchange record by connection and thread ID.""" + cache_key = f"credential_exchange_ctidx::{connection_id}::{thread_id}" + record_id = await cls.get_cached_key(context, cache_key) + if record_id: + record = await cls.retrieve_by_id(context, record_id) + else: + record = await cls.retrieve_by_tag_filter( + context, {"thread_id": thread_id}, {"connection_id": connection_id} + ) + await cls.set_cached_key(context, cache_key, record.credential_exchange_id) + return record + class V10CredentialExchangeSchema(BaseRecordSchema): """Schema to allow serialization/deserialization of credential exchange records.""" @@ -155,7 +173,7 @@ class Meta: credential_definition_id = fields.Str( required=False, description="Credential definition identifier", - **INDY_CRED_DEF_ID + **INDY_CRED_DEF_ID, ) schema_id = fields.Str( required=False, description="Schema identifier", **INDY_SCHEMA_ID diff --git a/aries_cloudagent/messaging/issue_credential/v1_0/routes.py b/aries_cloudagent/messaging/issue_credential/v1_0/routes.py index 3f864e35f7..6c3d2abc07 100644 --- a/aries_cloudagent/messaging/issue_credential/v1_0/routes.py +++ b/aries_cloudagent/messaging/issue_credential/v1_0/routes.py @@ -223,7 +223,7 @@ async def credential_exchange_send(request: web.BaseRequest): raise web.HTTPForbidden() credential_exchange_record = await credential_manager.prepare_send( - credential_definition_id, connection_id, credential_proposal=credential_proposal + connection_id, credential_proposal=credential_proposal ) ( @@ -264,20 +264,13 @@ async def credential_exchange_send_proposal(request: web.BaseRequest): connection_id = body.get("connection_id") credential_definition_id = body.get("credential_definition_id") comment = body.get("comment") - credential_preview = CredentialPreview( - attributes=[ - CredAttrSpec( - name=attr_preview["name"], - mime_type=attr_preview.get("mime-type", None), - value=attr_preview["value"], - ) - for attr_preview in body.get("credential_proposal")["attributes"] - ] - ) + proposal_spec = body.get("credential_proposal") - if not credential_preview: + if not proposal_spec: raise web.HTTPBadRequest(reason="credential_proposal must be provided.") + credential_preview = CredentialPreview.deserialize(proposal_spec) + credential_manager = CredentialManager(context) try: @@ -338,28 +331,17 @@ async def credential_exchange_send_free_offer(request: web.BaseRequest): auto_issue = body.get( "auto_issue", context.settings.get("debug.auto_respond_credential_request") ) - comment = body.get("comment", None) - credential_preview = CredentialPreview( - attributes=[ - CredAttrSpec( - name=attr_preview["name"], - value=attr_preview["value"], - mime_type=attr_preview.get("mime_type", None), - ) - for attr_preview in body.get("credential_preview")["attributes"] - ] - ) + comment = body.get("comment") + proposal_spec = body.get("credential_proposal") + + if not credential_definition_id: + raise web.HTTPBadRequest(reason="credential_definition_id is required") - if auto_issue and not credential_preview: + if auto_issue and not proposal_spec: raise web.HTTPBadRequest( reason="If auto_issue is set to" + " true then credential_preview must also be provided." ) - credential_proposal = CredentialProposal( - comment=comment, - credential_proposal=credential_preview, - cred_def_id=credential_definition_id, - ) credential_manager = CredentialManager(context) @@ -373,11 +355,22 @@ async def credential_exchange_send_free_offer(request: web.BaseRequest): if not connection_record.is_ready: raise web.HTTPForbidden() + if proposal_spec: + credential_preview = CredentialPreview.deserialize(proposal_spec) + credential_proposal = CredentialProposal( + comment=comment, + credential_proposal=credential_preview, + cred_def_id=credential_definition_id, + ) + credential_proposal_dict = credential_proposal.serialize() + else: + credential_proposal_dict = None + credential_exchange_record = V10CredentialExchange( connection_id=connection_id, initiator=V10CredentialExchange.INITIATOR_SELF, credential_definition_id=credential_definition_id, - credential_proposal_dict=credential_proposal.serialize(), + credential_proposal_dict=credential_proposal_dict, auto_issue=auto_issue, ) diff --git a/aries_cloudagent/messaging/issue_credential/v1_0/tests/test_manager.py b/aries_cloudagent/messaging/issue_credential/v1_0/tests/test_manager.py new file mode 100644 index 0000000000..5c90552116 --- /dev/null +++ b/aries_cloudagent/messaging/issue_credential/v1_0/tests/test_manager.py @@ -0,0 +1,595 @@ +from asynctest import TestCase as AsyncTestCase +from asynctest import mock as async_mock + +from .....config.injection_context import InjectionContext +from .....holder.base import BaseHolder +from .....issuer.base import BaseIssuer +from .....messaging.request_context import RequestContext +from .....ledger.base import BaseLedger +from .....storage.error import StorageNotFoundError + +from ..manager import CredentialManager, CredentialManagerError +from ..messages.credential_issue import CredentialIssue +from ..messages.credential_offer import CredentialOffer +from ..messages.credential_proposal import CredentialProposal +from ..messages.credential_request import CredentialRequest +from ..messages.credential_stored import CredentialStored +from ..messages.inner.credential_preview import CredentialPreview, CredAttrSpec +from ..models.credential_exchange import V10CredentialExchange + + +class TestCredentialManager(AsyncTestCase): + async def setUp(self): + self.context = RequestContext( + base_context=InjectionContext(enforce_typing=False) + ) + Ledger = async_mock.MagicMock(BaseLedger, autospec=True) + self.ledger = Ledger() + self.context.injector.bind_instance(BaseLedger, self.ledger) + self.manager = CredentialManager(self.context) + + async def test_prepare_send(self): + schema_id = "LjgpST2rjsoxYegQDRm7EL:2:bc-reg:1.0" + cred_def_id = "LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag" + connection_id = "test_conn_id" + preview = CredentialPreview( + attributes=(CredAttrSpec(name="attr", value="value"),) + ) + proposal = CredentialProposal( + credential_proposal=preview, cred_def_id=cred_def_id, schema_id=schema_id + ) + with async_mock.patch.object( + self.manager, "create_offer", autospec=True + ) as create_offer: + create_offer.return_value = (object(), None) + ret_exchange = await self.manager.prepare_send(connection_id, proposal) + create_offer.assert_called_once() + assert ret_exchange is create_offer.return_value[0] + exchange: V10CredentialExchange = create_offer.call_args[1][ + "credential_exchange_record" + ] + assert exchange.auto_issue + assert exchange.connection_id == connection_id + assert exchange.credential_definition_id == cred_def_id + assert exchange.role == exchange.ROLE_ISSUER + assert exchange.credential_proposal_dict == proposal.serialize() + + async def test_create_proposal(self): + schema_id = "LjgpST2rjsoxYegQDRm7EL:2:bc-reg:1.0" + cred_def_id = "LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag" + connection_id = "test_conn_id" + comment = "comment" + preview = CredentialPreview( + attributes=(CredAttrSpec(name="attr", value="value"),) + ) + + self.ledger.credential_definition_id2schema_id = async_mock.CoroutineMock( + return_value=schema_id + ) + + with self.assertRaises(CredentialManagerError): + await self.manager.create_proposal( + connection_id, + auto_offer=True, + comment=comment, + credential_preview=preview, + credential_definition_id=None, + ) + + with self.assertRaises(CredentialManagerError): + await self.manager.create_proposal( + connection_id, + auto_offer=True, + comment=comment, + credential_preview=None, + credential_definition_id=cred_def_id, + ) + + with async_mock.patch.object( + V10CredentialExchange, "save", autospec=True + ) as save_ex: + exchange: V10CredentialExchange = await self.manager.create_proposal( + connection_id, + auto_offer=True, + comment=comment, + credential_preview=preview, + credential_definition_id=cred_def_id, + ) + save_ex.assert_called_once() + proposal = CredentialProposal.deserialize(exchange.credential_proposal_dict) + + assert exchange.auto_offer + assert exchange.connection_id == connection_id + assert exchange.credential_definition_id == cred_def_id + assert exchange.schema_id == schema_id + assert exchange.thread_id == proposal._thread_id + assert exchange.role == exchange.ROLE_HOLDER + assert exchange.state == V10CredentialExchange.STATE_PROPOSAL_SENT + + async def test_receive_proposal(self): + schema_id = "LjgpST2rjsoxYegQDRm7EL:2:bc-reg:1.0" + cred_def_id = "LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag" + connection_id = "test_conn_id" + comment = "comment" + + preview = CredentialPreview( + attributes=(CredAttrSpec(name="attr", value="value"),) + ) + self.context.connection_record = async_mock.MagicMock() + self.context.connection_record.connection_id = connection_id + + self.ledger.credential_definition_id2schema_id = async_mock.CoroutineMock( + return_value=schema_id + ) + + with self.assertRaises(CredentialManagerError): + self.context.message = CredentialProposal( + credential_proposal=preview, cred_def_id=None, schema_id=None + ) + await self.manager.receive_proposal() + + with async_mock.patch.object( + V10CredentialExchange, "save", autospec=True + ) as save_ex: + proposal = CredentialProposal( + credential_proposal=preview, cred_def_id=cred_def_id, schema_id=None + ) + self.context.message = proposal + + exchange = await self.manager.receive_proposal() + save_ex.assert_called_once() + + assert exchange.connection_id == connection_id + assert exchange.credential_definition_id == cred_def_id + assert exchange.role == V10CredentialExchange.ROLE_ISSUER + assert exchange.state == V10CredentialExchange.STATE_PROPOSAL_RECEIVED + assert exchange.schema_id == schema_id + assert exchange.thread_id == proposal._thread_id + + ret_proposal: CredentialProposal = CredentialProposal.deserialize( + exchange.credential_proposal_dict + ) + attrs = ret_proposal.credential_proposal.attributes + assert attrs == preview.attributes + + async def test_create_free_offer(self): + schema_id = "LjgpST2rjsoxYegQDRm7EL:2:bc-reg:1.0" + cred_def_id = "LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag" + connection_id = "test_conn_id" + comment = "comment" + + exchange = V10CredentialExchange( + credential_definition_id=cred_def_id, role=V10CredentialExchange.ROLE_ISSUER + ) + + with async_mock.patch.object( + V10CredentialExchange, "save", autospec=True + ) as save_ex, async_mock.patch.object( + V10CredentialExchange, "get_cached_key", autospec=True + ) as get_cached_key, async_mock.patch.object( + V10CredentialExchange, "set_cached_key", autospec=True + ) as set_cached_key: + get_cached_key.return_value = None + cred_offer = {"cred_def_id": cred_def_id, "schema_id": schema_id} + issuer = async_mock.MagicMock() + issuer.create_credential_offer = async_mock.CoroutineMock( + return_value=cred_offer + ) + self.context.injector.bind_instance(BaseIssuer, issuer) + + (ret_exchange, ret_offer) = await self.manager.create_offer( + credential_exchange_record=exchange, comment=comment + ) + assert ret_exchange is exchange + save_ex.assert_called_once() + + issuer.create_credential_offer.assert_called_once_with(cred_def_id) + + assert exchange.thread_id == ret_offer._thread_id + assert exchange.credential_definition_id == cred_def_id + assert exchange.role == V10CredentialExchange.ROLE_ISSUER + assert exchange.schema_id == schema_id + assert exchange.state == V10CredentialExchange.STATE_OFFER_SENT + assert exchange.credential_offer == cred_offer + + async def test_create_bound_offer(self): + schema_id = "LjgpST2rjsoxYegQDRm7EL:2:bc-reg:1.0" + cred_def_id = "LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag" + connection_id = "test_conn_id" + comment = "comment" + + preview = CredentialPreview( + attributes=(CredAttrSpec(name="attr", value="value"),) + ) + proposal = CredentialProposal(credential_proposal=preview) + exchange = V10CredentialExchange( + credential_definition_id=cred_def_id, + credential_proposal_dict=proposal.serialize(), + role=V10CredentialExchange.ROLE_ISSUER, + ) + + with async_mock.patch.object( + V10CredentialExchange, "save", autospec=True + ) as save_ex, async_mock.patch.object( + V10CredentialExchange, "get_cached_key", autospec=True + ) as get_cached_key, async_mock.patch.object( + V10CredentialExchange, "set_cached_key", autospec=True + ) as set_cached_key: + get_cached_key.return_value = None + cred_offer = {"cred_def_id": cred_def_id, "schema_id": schema_id} + issuer = async_mock.MagicMock() + issuer.create_credential_offer = async_mock.CoroutineMock( + return_value=cred_offer + ) + self.context.injector.bind_instance(BaseIssuer, issuer) + + (ret_exchange, ret_offer) = await self.manager.create_offer( + credential_exchange_record=exchange, comment=comment + ) + assert ret_exchange is exchange + save_ex.assert_called_once() + + issuer.create_credential_offer.assert_called_once_with(cred_def_id) + + assert exchange.thread_id == ret_offer._thread_id + assert exchange.schema_id == schema_id + assert exchange.credential_definition_id == cred_def_id + assert exchange.role == V10CredentialExchange.ROLE_ISSUER + assert exchange.state == V10CredentialExchange.STATE_OFFER_SENT + assert exchange.credential_offer == cred_offer + + # additionally check that credential preview was passed through + assert ret_offer.credential_preview.attributes == preview.attributes + + async def test_receive_offer_proposed(self): + schema_id = "LjgpST2rjsoxYegQDRm7EL:2:bc-reg:1.0" + cred_def_id = "LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag" + connection_id = "test_conn_id" + indy_offer = {"schema_id": schema_id, "cred_def_id": cred_def_id} + thread_id = "thread-id" + + preview = CredentialPreview( + attributes=(CredAttrSpec(name="attr", value="value"),) + ) + proposal = CredentialProposal(credential_proposal=preview) + + offer = CredentialOffer( + credential_preview=preview, + offers_attach=[CredentialOffer.wrap_indy_offer(indy_offer)], + ) + offer.assign_thread_id(thread_id) + + self.context.message = offer + self.context.connection_record = async_mock.MagicMock() + self.context.connection_record.connection_id = connection_id + + stored_exchange = V10CredentialExchange( + connection_id=connection_id, + credential_definition_id=cred_def_id, + credential_proposal_dict=proposal.serialize(), + initiator=V10CredentialExchange.INITIATOR_EXTERNAL, + role=V10CredentialExchange.ROLE_HOLDER, + schema_id=schema_id, + thread_id=thread_id, + ) + + with async_mock.patch.object( + V10CredentialExchange, "save", autospec=True + ) as save_ex, async_mock.patch.object( + V10CredentialExchange, + "retrieve_by_connection_and_thread", + async_mock.CoroutineMock(return_value=stored_exchange), + ) as retrieve_ex: + exchange = await self.manager.receive_offer() + + assert exchange.connection_id == connection_id + assert exchange.credential_definition_id == cred_def_id + assert exchange.schema_id == schema_id + assert exchange.thread_id == offer._thread_id + assert exchange.role == V10CredentialExchange.ROLE_HOLDER + assert exchange.state == V10CredentialExchange.STATE_OFFER_RECEIVED + assert exchange.credential_offer == indy_offer + + proposal = CredentialProposal.deserialize(exchange.credential_proposal_dict) + assert proposal.credential_proposal.attributes == preview.attributes + + async def test_receive_offer_non_proposed(self): + schema_id = "LjgpST2rjsoxYegQDRm7EL:2:bc-reg:1.0" + cred_def_id = "LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag" + connection_id = "test_conn_id" + indy_offer = {"schema_id": schema_id, "cred_def_id": cred_def_id} + preview = CredentialPreview( + attributes=(CredAttrSpec(name="attr", value="value"),) + ) + offer = CredentialOffer( + credential_preview=preview, + offers_attach=[CredentialOffer.wrap_indy_offer(indy_offer)], + ) + self.context.message = offer + self.context.connection_record = async_mock.MagicMock() + self.context.connection_record.connection_id = connection_id + + with async_mock.patch.object( + V10CredentialExchange, "save", autospec=True + ) as save_ex, async_mock.patch.object( + V10CredentialExchange, + "retrieve_by_connection_and_thread", + async_mock.CoroutineMock(side_effect=StorageNotFoundError), + ) as retrieve_ex: + exchange = await self.manager.receive_offer() + + assert exchange.connection_id == connection_id + assert exchange.credential_definition_id == cred_def_id + assert exchange.schema_id == schema_id + assert exchange.thread_id == offer._thread_id + assert exchange.role == V10CredentialExchange.ROLE_HOLDER + assert exchange.state == V10CredentialExchange.STATE_OFFER_RECEIVED + assert exchange.credential_offer == indy_offer + + proposal = CredentialProposal.deserialize(exchange.credential_proposal_dict) + assert proposal.credential_proposal.attributes == preview.attributes + + async def test_create_request(self): + schema_id = "LjgpST2rjsoxYegQDRm7EL:2:bc-reg:1.0" + cred_def_id = "LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag" + connection_id = "test_conn_id" + indy_offer = {"schema_id": schema_id, "cred_def_id": cred_def_id, "nonce": "0"} + indy_cred_req = {"schema_id": schema_id, "cred_def_id": cred_def_id} + thread_id = "thread-id" + holder_did = "did" + + stored_exchange = V10CredentialExchange( + connection_id=connection_id, + credential_definition_id=cred_def_id, + credential_offer=indy_offer, + initiator=V10CredentialExchange.INITIATOR_SELF, + role=V10CredentialExchange.ROLE_HOLDER, + schema_id=schema_id, + thread_id=thread_id, + ) + + with async_mock.patch.object( + V10CredentialExchange, "save", autospec=True + ) as save_ex, async_mock.patch.object( + V10CredentialExchange, "get_cached_key", autospec=True + ) as get_cached_key, async_mock.patch.object( + V10CredentialExchange, "set_cached_key", autospec=True + ) as set_cached_key: + get_cached_key.return_value = None + + cred_def = {"cred": "def"} + self.ledger.get_credential_definition = async_mock.CoroutineMock( + return_value=cred_def + ) + + cred_req_meta = object() + holder = async_mock.MagicMock() + holder.create_credential_request = async_mock.CoroutineMock( + return_value=(indy_cred_req, cred_req_meta) + ) + self.context.injector.bind_instance(BaseHolder, holder) + + ret_exchange, ret_request = await self.manager.create_request( + stored_exchange, holder_did + ) + + holder.create_credential_request.assert_called_once_with( + indy_offer, cred_def, holder_did + ) + + assert ret_request.indy_cred_req() == indy_cred_req + assert ret_request._thread_id == thread_id + + assert ret_exchange.state == V10CredentialExchange.STATE_REQUEST_SENT + + async def test_receive_request(self): + schema_id = "LjgpST2rjsoxYegQDRm7EL:2:bc-reg:1.0" + cred_def_id = "LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag" + connection_id = "test_conn_id" + indy_cred_req = {"schema_id": schema_id, "cred_def_id": cred_def_id} + + stored_exchange = V10CredentialExchange( + connection_id=connection_id, + initiator=V10CredentialExchange.INITIATOR_EXTERNAL, + role=V10CredentialExchange.ROLE_ISSUER, + ) + + request = CredentialRequest( + requests_attach=[CredentialRequest.wrap_indy_cred_req(indy_cred_req)] + ) + self.context.message = request + self.context.connection_record = async_mock.MagicMock() + self.context.connection_record.connection_id = connection_id + + with async_mock.patch.object( + V10CredentialExchange, "save", autospec=True + ) as save_ex, async_mock.patch.object( + V10CredentialExchange, + "retrieve_by_connection_and_thread", + async_mock.CoroutineMock(return_value=stored_exchange), + ) as retrieve_ex: + exchange = await self.manager.receive_request() + + retrieve_ex.assert_called_once_with( + self.context, connection_id, request._thread_id + ) + save_ex.assert_called_once() + + assert exchange.state == V10CredentialExchange.STATE_REQUEST_RECEIVED + assert exchange.credential_request == indy_cred_req + + async def test_issue_credential(self): + schema_id = "LjgpST2rjsoxYegQDRm7EL:2:bc-reg:1.0" + cred_def_id = "LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag" + connection_id = "test_conn_id" + comment = "comment" + cred_values = {"attr": "value"} + indy_offer = {"schema_id": schema_id, "cred_def_id": cred_def_id, "nonce": "0"} + indy_cred_req = {"schema_id": schema_id, "cred_def_id": cred_def_id} + thread_id = "thread-id" + + stored_exchange = V10CredentialExchange( + connection_id=connection_id, + credential_offer=indy_offer, + credential_request=indy_cred_req, + initiator=V10CredentialExchange.INITIATOR_SELF, + role=V10CredentialExchange.ROLE_ISSUER, + thread_id=thread_id, + ) + + schema = object() + self.ledger.get_schema = async_mock.CoroutineMock(return_value=schema) + + issuer = async_mock.MagicMock() + cred = {"indy": "credential"} + cred_revoc = object() + issuer.create_credential = async_mock.CoroutineMock( + return_value=(cred, cred_revoc) + ) + self.context.injector.bind_instance(BaseIssuer, issuer) + + with async_mock.patch.object( + V10CredentialExchange, "save", autospec=True + ) as save_ex: + + ret_exchange, ret_cred_issue = await self.manager.issue_credential( + stored_exchange, comment=comment, credential_values=cred_values + ) + + save_ex.assert_called_once() + + issuer.create_credential.assert_called_once_with( + schema, indy_offer, indy_cred_req, cred_values + ) + + assert ret_exchange.credential == cred + assert ret_cred_issue.indy_credential() == cred + assert ret_exchange.state == V10CredentialExchange.STATE_ISSUED + assert ret_cred_issue._thread_id == thread_id + + async def test_receive_credential(self): + schema_id = "LjgpST2rjsoxYegQDRm7EL:2:bc-reg:1.0" + cred_def_id = "LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag" + connection_id = "test_conn_id" + indy_cred = {"indy": "credential"} + + stored_exchange = V10CredentialExchange( + connection_id=connection_id, + initiator=V10CredentialExchange.INITIATOR_EXTERNAL, + role=V10CredentialExchange.ROLE_ISSUER, + ) + + issue = CredentialIssue( + credentials_attach=[CredentialIssue.wrap_indy_credential(indy_cred)] + ) + self.context.message = issue + self.context.connection_record = async_mock.MagicMock() + self.context.connection_record.connection_id = connection_id + + with async_mock.patch.object( + V10CredentialExchange, "save", autospec=True + ) as save_ex, async_mock.patch.object( + V10CredentialExchange, + "retrieve_by_connection_and_thread", + async_mock.CoroutineMock(return_value=stored_exchange), + ) as retrieve_ex: + exchange = await self.manager.receive_credential() + + retrieve_ex.assert_called_once_with( + self.context, connection_id, issue._thread_id + ) + save_ex.assert_called_once() + + assert exchange.raw_credential == indy_cred + assert exchange.state == V10CredentialExchange.STATE_CREDENTIAL_RECEIVED + + async def test_store_credential(self): + schema_id = "LjgpST2rjsoxYegQDRm7EL:2:bc-reg:1.0" + cred_def_id = "LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag" + connection_id = "test_conn_id" + cred = {"cred_def_id": cred_def_id} + cred_req_meta = {"req": "meta"} + thread_id = "thread-id" + + stored_exchange = V10CredentialExchange( + connection_id=connection_id, + credential_definition_id=cred_def_id, + credential_request_metadata=cred_req_meta, + credential_proposal_dict={"credential_proposal": {}}, + raw_credential=cred, + initiator=V10CredentialExchange.INITIATOR_EXTERNAL, + role=V10CredentialExchange.ROLE_HOLDER, + thread_id=thread_id, + ) + + cred_def = object() + self.ledger.get_credential_definition = async_mock.CoroutineMock( + return_value=cred_def + ) + + cred_id = "cred-id" + holder = async_mock.MagicMock() + holder.store_credential = async_mock.CoroutineMock(return_value=cred_id) + stored_cred = {"stored": "cred"} + holder.get_credential = async_mock.CoroutineMock(return_value=stored_cred) + self.context.injector.bind_instance(BaseHolder, holder) + + with async_mock.patch.object( + V10CredentialExchange, "save", autospec=True + ) as save_ex, async_mock.patch.object( + CredentialPreview, "deserialize", autospec=True + ) as mock_preview_deserialize: + + ret_exchange, ret_cred_stored = await self.manager.store_credential( + stored_exchange + ) + + save_ex.assert_called_once() + + self.ledger.get_credential_definition.assert_called_once_with(cred_def_id) + + holder.store_credential.assert_called_once_with( + cred_def, + cred, + cred_req_meta, + mock_preview_deserialize.return_value.mime_types.return_value, + ) + + holder.get_credential.assert_called_once_with(cred_id) + + assert ret_exchange.credential_id == cred_id + assert ret_exchange.credential == stored_cred + assert ret_exchange.state == V10CredentialExchange.STATE_STORED + assert ret_cred_stored._thread_id == thread_id + + async def test_credential_stored(self): + connection_id = "connection-id" + stored_exchange = V10CredentialExchange( + connection_id=connection_id, + initiator=V10CredentialExchange.INITIATOR_SELF, + role=V10CredentialExchange.ROLE_ISSUER, + ) + + stored = CredentialStored() + self.context.message = stored + self.context.connection_record = async_mock.MagicMock() + self.context.connection_record.connection_id = connection_id + + with async_mock.patch.object( + V10CredentialExchange, "save", autospec=True + ) as save_ex, async_mock.patch.object( + V10CredentialExchange, "delete_record", autospec=True + ) as delete_ex, async_mock.patch.object( + V10CredentialExchange, + "retrieve_by_connection_and_thread", + async_mock.CoroutineMock(return_value=stored_exchange), + ) as retrieve_ex: + ret_exchange = await self.manager.credential_stored() + + retrieve_ex.assert_called_once_with( + self.context, connection_id, stored._thread_id + ) + save_ex.assert_called_once() + + assert ret_exchange.state == V10CredentialExchange.STATE_STORED + delete_ex.assert_called_once() diff --git a/aries_cloudagent/messaging/issue_credential/v1_0/tests/test_routes.py b/aries_cloudagent/messaging/issue_credential/v1_0/tests/test_routes.py index b8173f6316..cf3dc5c9f5 100644 --- a/aries_cloudagent/messaging/issue_credential/v1_0/tests/test_routes.py +++ b/aries_cloudagent/messaging/issue_credential/v1_0/tests/test_routes.py @@ -18,13 +18,9 @@ async def test_credential_exchange_send(self): } with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True + test_module, "CredentialManager", autospec=True ) as mock_credential_manager: test_module.web.json_response = async_mock.CoroutineMock() @@ -46,7 +42,9 @@ async def test_credential_exchange_send(self): await test_module.credential_exchange_send(mock) test_module.web.json_response.assert_called_once_with( - mock_credential_manager.return_value.create_offer.return_value[0].serialize.return_value + mock_credential_manager.return_value.create_offer.return_value[ + 0 + ].serialize.return_value ) async def test_credential_exchange_send_no_conn_record(self): @@ -59,14 +57,10 @@ async def test_credential_exchange_send_no_conn_record(self): } with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager: + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager: test_module.web.json_response = async_mock.CoroutineMock() @@ -75,12 +69,12 @@ async def test_credential_exchange_send_no_conn_record(self): side_effect=StorageNotFoundError ) - mock_connection_manager.return_value.create_offer = ( + mock_credential_manager.return_value.create_offer = ( async_mock.CoroutineMock() ) - mock_connection_manager.return_value.create_offer.return_value = ( + mock_credential_manager.return_value.create_offer.return_value = ( + async_mock.MagicMock(), async_mock.MagicMock(), - async_mock.MagicMock() ) with self.assertRaises(test_module.web.HTTPBadRequest): @@ -96,14 +90,10 @@ async def test_credential_exchange_send_not_ready(self): } with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager: + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager: test_module.web.json_response = async_mock.CoroutineMock() @@ -111,62 +101,59 @@ async def test_credential_exchange_send_not_ready(self): mock_connection_record.retrieve_by_id = async_mock.CoroutineMock() mock_connection_record.retrieve_by_id.return_value.is_ready = False - mock_connection_manager.return_value.create_offer = ( + mock_credential_manager.return_value.create_offer = ( async_mock.CoroutineMock() ) - mock_connection_manager.return_value.create_offer.return_value = ( + mock_credential_manager.return_value.create_offer.return_value = ( + async_mock.MagicMock(), async_mock.MagicMock(), - async_mock.MagicMock() ) with self.assertRaises(test_module.web.HTTPForbidden): await test_module.credential_exchange_send(mock) async def test_credential_exchange_send_proposal(self): - mock = async_mock.MagicMock() - mock.json = async_mock.CoroutineMock() + conn_id = "connection-id" + proposal_spec = {"attributes": [{"name": "attr", "value": "value"}]} + mock = async_mock.MagicMock() + mock.json = async_mock.CoroutineMock( + return_value={ + "connection_id": conn_id, + "credential_proposal": proposal_spec, + } + ) mock.app = { "outbound_message_router": async_mock.CoroutineMock(), "request_context": "context", } with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager, async_mock.patch.object( - test_module, - "CredentialProposal", - autospec=True - ) as mock_cred_proposal: + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager, async_mock.patch.object( + test_module.CredentialProposal, "deserialize", autospec=True + ) as mock_proposal_deserialize: test_module.web.json_response = async_mock.CoroutineMock() - mock_connection_manager.return_value.create_proposal = ( - async_mock.CoroutineMock() - ) - mock_cred_ex_record = async_mock.MagicMock() - mock_connection_manager.return_value.create_proposal.return_value = ( + mock_credential_manager.return_value.create_proposal.return_value = ( mock_cred_ex_record ) - mock_cred_proposal.return_value.deserialize.return_value = ( - async_mock.MagicMock() - ) - await test_module.credential_exchange_send_proposal(mock) test_module.web.json_response.assert_called_once_with( mock_cred_ex_record.serialize.return_value ) + mock.app["outbound_message_router"].assert_called_once_with( + mock_proposal_deserialize.return_value, connection_id=conn_id + ) + async def test_credential_exchange_send_proposal_no_conn_record(self): mock = async_mock.MagicMock() mock.json = async_mock.CoroutineMock() @@ -177,14 +164,12 @@ async def test_credential_exchange_send_proposal_no_conn_record(self): } with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager: + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager, async_mock.patch.object( + test_module.CredentialPreview, "deserialize", autospec=True + ) as mock_preview_deserialize: test_module.web.json_response = async_mock.CoroutineMock() @@ -193,10 +178,7 @@ async def test_credential_exchange_send_proposal_no_conn_record(self): side_effect=StorageNotFoundError ) - mock_connection_manager.return_value.create_proposal = ( - async_mock.CoroutineMock() - ) - mock_connection_manager.return_value.create_proposal.return_value = ( + mock_credential_manager.return_value.create_proposal.return_value = ( async_mock.MagicMock() ) @@ -213,14 +195,12 @@ async def test_credential_exchange_send_proposal_not_ready(self): } with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager: + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager, async_mock.patch.object( + test_module.CredentialPreview, "deserialize", autospec=True + ) as mock_preview_deserialize: test_module.web.json_response = async_mock.CoroutineMock() @@ -228,10 +208,7 @@ async def test_credential_exchange_send_proposal_not_ready(self): mock_connection_record.retrieve_by_id = async_mock.CoroutineMock() mock_connection_record.retrieve_by_id.return_value.is_ready = False - mock_connection_manager.return_value.create_proposal = ( - async_mock.CoroutineMock() - ) - mock_connection_manager.return_value.create_proposal.return_value = ( + mock_credential_manager.return_value.create_proposal.return_value = ( async_mock.MagicMock() ) @@ -240,40 +217,35 @@ async def test_credential_exchange_send_proposal_not_ready(self): async def test_credential_exchange_send_free_offer(self): mock = async_mock.MagicMock() - mock.json = async_mock.CoroutineMock() - mock.json.return_value["auto_issue"] = True + mock.json = async_mock.CoroutineMock( + return_value={"auto_issue": False, "credential_definition_id": "cred-def-id"} + ) mock.app = { "outbound_message_router": async_mock.CoroutineMock(), "request_context": async_mock.patch.object( - aio_web, - "BaseRequest", - autospec=True - ) + aio_web, "BaseRequest", autospec=True + ), } mock.app["request_context"].settings = {} with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager: + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager: test_module.web.json_response = async_mock.CoroutineMock() - mock_connection_manager.return_value.create_offer = ( + mock_credential_manager.return_value.create_offer = ( async_mock.CoroutineMock() ) mock_cred_ex_record = async_mock.MagicMock() - mock_connection_manager.return_value.create_offer.return_value = ( + mock_credential_manager.return_value.create_offer.return_value = ( mock_cred_ex_record, - async_mock.MagicMock() + async_mock.MagicMock(), ) await test_module.credential_exchange_send_free_offer(mock) @@ -284,53 +256,23 @@ async def test_credential_exchange_send_free_offer(self): async def test_credential_exchange_send_free_offer_no_conn_record(self): mock = async_mock.MagicMock() - mock.json = async_mock.CoroutineMock() - mock.json.return_value["auto_issue"] = True + mock.json = async_mock.CoroutineMock( + return_value={"auto_issue": False, "credential_definition_id": "cred-def-id"} + ) mock.app = { "outbound_message_router": async_mock.CoroutineMock(), "request_context": async_mock.patch.object( - aio_web, - "BaseRequest", - autospec=True - ) + aio_web, "BaseRequest", autospec=True + ), } mock.app["request_context"].settings = {} with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True - ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager: - - test_module.web.json_response = async_mock.CoroutineMock() - - mock_connection_manager.return_value.create_offer = ( - async_mock.CoroutineMock() - ) - - mock_cred_ex_record = async_mock.MagicMock() - - mock_connection_manager.return_value.create_offer.return_value = ( - mock_cred_ex_record, - async_mock.MagicMock() - ) - - await test_module.credential_exchange_send_free_offer(mock) - - with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager: + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager: test_module.web.json_response = async_mock.CoroutineMock() @@ -339,12 +281,12 @@ async def test_credential_exchange_send_free_offer_no_conn_record(self): side_effect=StorageNotFoundError ) - mock_connection_manager.return_value.create_offer = ( + mock_credential_manager.return_value.create_offer = ( async_mock.CoroutineMock() ) - mock_connection_manager.return_value.create_offer.return_value = ( + mock_credential_manager.return_value.create_offer.return_value = ( + async_mock.MagicMock(), async_mock.MagicMock(), - async_mock.MagicMock() ) with self.assertRaises(test_module.web.HTTPBadRequest): @@ -358,22 +300,16 @@ async def test_credential_exchange_send_free_offer_not_ready(self): mock.app = { "outbound_message_router": async_mock.CoroutineMock(), "request_context": async_mock.patch.object( - aio_web, - "BaseRequest", - autospec=True - ) + aio_web, "BaseRequest", autospec=True + ), } mock.app["request_context"].settings = {} with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager: + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager: test_module.web.json_response = async_mock.CoroutineMock() @@ -381,12 +317,12 @@ async def test_credential_exchange_send_free_offer_not_ready(self): mock_connection_record.retrieve_by_id = async_mock.CoroutineMock() mock_connection_record.retrieve_by_id.return_value.is_ready = False - mock_connection_manager.return_value.create_offer = ( + mock_credential_manager.return_value.create_offer = ( async_mock.CoroutineMock() ) - mock_connection_manager.return_value.create_offer.return_value = ( + mock_credential_manager.return_value.create_offer.return_value = ( + async_mock.MagicMock(), async_mock.MagicMock(), - async_mock.MagicMock() ) with self.assertRaises(test_module.web.HTTPForbidden): @@ -402,17 +338,11 @@ async def test_credential_exchange_send_bound_offer(self): } with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager, async_mock.patch.object( - test_module, - "V10CredentialExchange", - autospec=True + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager, async_mock.patch.object( + test_module, "V10CredentialExchange", autospec=True ) as mock_cred_ex: mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock() @@ -422,13 +352,13 @@ async def test_credential_exchange_send_bound_offer(self): test_module.web.json_response = async_mock.CoroutineMock() - mock_connection_manager.return_value.create_offer = ( + mock_credential_manager.return_value.create_offer = ( async_mock.CoroutineMock() ) mock_cred_ex_record = async_mock.MagicMock() - mock_connection_manager.return_value.create_offer.return_value = ( + mock_credential_manager.return_value.create_offer.return_value = ( mock_cred_ex_record, async_mock.MagicMock(), ) @@ -449,17 +379,11 @@ async def test_credential_exchange_send_bound_offer_no_conn_record(self): } with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager, async_mock.patch.object( - test_module, - "V10CredentialExchange", - autospec=True + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager, async_mock.patch.object( + test_module, "V10CredentialExchange", autospec=True ) as mock_cred_ex: mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock() @@ -474,12 +398,12 @@ async def test_credential_exchange_send_bound_offer_no_conn_record(self): side_effect=StorageNotFoundError ) - mock_connection_manager.return_value.create_offer = ( + mock_credential_manager.return_value.create_offer = ( async_mock.CoroutineMock() ) - mock_connection_manager.return_value.create_offer.return_value = ( + mock_credential_manager.return_value.create_offer.return_value = ( + async_mock.MagicMock(), async_mock.MagicMock(), - async_mock.MagicMock() ) with self.assertRaises(test_module.web.HTTPBadRequest): @@ -495,17 +419,11 @@ async def test_credential_exchange_send_bound_offer_not_ready(self): } with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager, async_mock.patch.object( - test_module, - "V10CredentialExchange", - autospec=True + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager, async_mock.patch.object( + test_module, "V10CredentialExchange", autospec=True ) as mock_cred_ex: mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock() @@ -519,12 +437,12 @@ async def test_credential_exchange_send_bound_offer_not_ready(self): mock_connection_record.retrieve_by_id = async_mock.CoroutineMock() mock_connection_record.retrieve_by_id.return_value.is_ready = False - mock_connection_manager.return_value.create_offer = ( + mock_credential_manager.return_value.create_offer = ( async_mock.CoroutineMock() ) - mock_connection_manager.return_value.create_offer.return_value = ( + mock_credential_manager.return_value.create_offer.return_value = ( + async_mock.MagicMock(), async_mock.MagicMock(), - async_mock.MagicMock() ) with self.assertRaises(test_module.web.HTTPForbidden): @@ -540,17 +458,11 @@ async def test_credential_exchange_send_request(self): } with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager, async_mock.patch.object( - test_module, - "V10CredentialExchange", - autospec=True + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager, async_mock.patch.object( + test_module, "V10CredentialExchange", autospec=True ) as mock_cred_ex: mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock() @@ -562,7 +474,7 @@ async def test_credential_exchange_send_request(self): mock_cred_ex_record = async_mock.MagicMock() - mock_connection_manager.return_value.create_request.return_value = ( + mock_credential_manager.return_value.create_request.return_value = ( mock_cred_ex_record, async_mock.MagicMock(), ) @@ -583,17 +495,11 @@ async def test_credential_exchange_send_request_no_conn_record(self): } with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager, async_mock.patch.object( - test_module, - "V10CredentialExchange", - autospec=True + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager, async_mock.patch.object( + test_module, "V10CredentialExchange", autospec=True ) as mock_cred_ex: test_module.web.json_response = async_mock.CoroutineMock() @@ -608,12 +514,12 @@ async def test_credential_exchange_send_request_no_conn_record(self): side_effect=StorageNotFoundError ) - mock_connection_manager.return_value.create_offer = ( + mock_credential_manager.return_value.create_offer = ( async_mock.CoroutineMock() ) - mock_connection_manager.return_value.create_offer.return_value = ( + mock_credential_manager.return_value.create_offer.return_value = ( + async_mock.MagicMock(), async_mock.MagicMock(), - async_mock.MagicMock() ) with self.assertRaises(test_module.web.HTTPBadRequest): @@ -629,17 +535,11 @@ async def test_credential_exchange_send_request_not_ready(self): } with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager, async_mock.patch.object( - test_module, - "V10CredentialExchange", - autospec=True + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager, async_mock.patch.object( + test_module, "V10CredentialExchange", autospec=True ) as mock_cred_ex: mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock() @@ -653,12 +553,12 @@ async def test_credential_exchange_send_request_not_ready(self): mock_connection_record.retrieve_by_id = async_mock.CoroutineMock() mock_connection_record.retrieve_by_id.return_value.is_ready = False - mock_connection_manager.return_value.create_offer = ( + mock_credential_manager.return_value.create_offer = ( async_mock.CoroutineMock() ) - mock_connection_manager.return_value.create_offer.return_value = ( + mock_credential_manager.return_value.create_offer.return_value = ( + async_mock.MagicMock(), async_mock.MagicMock(), - async_mock.MagicMock() ) with self.assertRaises(test_module.web.HTTPForbidden): @@ -670,25 +570,17 @@ async def test_credential_exchange_issue(self): mock.app = { "outbound_message_router": async_mock.CoroutineMock(), - "request_context": "context" + "request_context": "context", } with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager, async_mock.patch.object( - test_module, - "V10CredentialExchange", - autospec=True + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager, async_mock.patch.object( + test_module, "V10CredentialExchange", autospec=True ) as mock_cred_ex, async_mock.patch.object( - test_module, - "CredentialPreview", - autospec=True + test_module, "CredentialPreview", autospec=True ) as mock_cred_preview: mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock() @@ -700,9 +592,9 @@ async def test_credential_exchange_issue(self): mock_cred_ex_record = async_mock.MagicMock() - mock_connection_manager.return_value.issue_credential.return_value = ( + mock_credential_manager.return_value.issue_credential.return_value = ( mock_cred_ex_record, - async_mock.MagicMock() + async_mock.MagicMock(), ) mock_cred_preview.return_value.deserialize.return_value = ( @@ -728,21 +620,13 @@ async def test_credential_exchange_issue_no_conn_record(self): } with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager, async_mock.patch.object( - test_module, - "V10CredentialExchange", - autospec=True + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager, async_mock.patch.object( + test_module, "V10CredentialExchange", autospec=True ) as mock_cred_ex, async_mock.patch.object( - test_module, - "CredentialPreview", - autospec=True + test_module, "CredentialPreview", autospec=True ) as mock_cred_preview: test_module.web.json_response = async_mock.CoroutineMock() @@ -757,12 +641,12 @@ async def test_credential_exchange_issue_no_conn_record(self): side_effect=StorageNotFoundError ) - mock_connection_manager.return_value.issue_credential = ( + mock_credential_manager.return_value.issue_credential = ( async_mock.CoroutineMock() ) - mock_connection_manager.return_value.issue_credential.return_value = ( + mock_credential_manager.return_value.issue_credential.return_value = ( + async_mock.MagicMock(), async_mock.MagicMock(), - async_mock.MagicMock() ) mock_cred_preview.return_value.deserialize.return_value = ( @@ -785,21 +669,13 @@ async def test_credential_exchange_issue_not_ready(self): } with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager, async_mock.patch.object( - test_module, - "V10CredentialExchange", - autospec=True + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager, async_mock.patch.object( + test_module, "V10CredentialExchange", autospec=True ) as mock_cred_ex, async_mock.patch.object( - test_module, - "CredentialPreview", - autospec=True + test_module, "CredentialPreview", autospec=True ) as mock_cred_preview: mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock() @@ -813,12 +689,12 @@ async def test_credential_exchange_issue_not_ready(self): mock_connection_record.retrieve_by_id = async_mock.CoroutineMock() mock_connection_record.retrieve_by_id.return_value.is_ready = False - mock_connection_manager.return_value.issue_credential = ( + mock_credential_manager.return_value.issue_credential = ( async_mock.CoroutineMock() ) - mock_connection_manager.return_value.issue_credential.return_value = ( + mock_credential_manager.return_value.issue_credential.return_value = ( + async_mock.MagicMock(), async_mock.MagicMock(), - async_mock.MagicMock() ) mock_cred_preview.return_value.deserialize.return_value = ( @@ -841,17 +717,11 @@ async def test_credential_exchange_store(self): } with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager, async_mock.patch.object( - test_module, - "V10CredentialExchange", - autospec=True + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager, async_mock.patch.object( + test_module, "V10CredentialExchange", autospec=True ) as mock_cred_ex: mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock() @@ -863,9 +733,9 @@ async def test_credential_exchange_store(self): mock_cred_ex_record = async_mock.MagicMock() - mock_connection_manager.return_value.store_credential.return_value = ( + mock_credential_manager.return_value.store_credential.return_value = ( mock_cred_ex_record, - async_mock.MagicMock() + async_mock.MagicMock(), ) await test_module.credential_exchange_store(mock) @@ -884,17 +754,11 @@ async def test_credential_exchange_store_no_conn_record(self): } with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager, async_mock.patch.object( - test_module, - "V10CredentialExchange", - autospec=True + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager, async_mock.patch.object( + test_module, "V10CredentialExchange", autospec=True ) as mock_cred_ex: test_module.web.json_response = async_mock.CoroutineMock() @@ -909,9 +773,9 @@ async def test_credential_exchange_store_no_conn_record(self): side_effect=StorageNotFoundError ) - mock_connection_manager.return_value.store_credential.return_value = ( + mock_credential_manager.return_value.store_credential.return_value = ( mock_cred_ex, - async_mock.MagicMock() + async_mock.MagicMock(), ) with self.assertRaises(test_module.web.HTTPBadRequest): @@ -927,17 +791,11 @@ async def test_credential_exchange_store_not_ready(self): } with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager, async_mock.patch.object( - test_module, - "V10CredentialExchange", - autospec=True + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager, async_mock.patch.object( + test_module, "V10CredentialExchange", autospec=True ) as mock_cred_ex: mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock() @@ -951,9 +809,9 @@ async def test_credential_exchange_store_not_ready(self): mock_connection_record.retrieve_by_id = async_mock.CoroutineMock() mock_connection_record.retrieve_by_id.return_value.is_ready = False - mock_connection_manager.return_value.store_credential.return_value = ( + mock_credential_manager.return_value.store_credential.return_value = ( mock_cred_ex, - async_mock.MagicMock() + async_mock.MagicMock(), ) with self.assertRaises(test_module.web.HTTPForbidden): @@ -971,21 +829,13 @@ async def test_credential_exchange_problem_report(self): } with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager, async_mock.patch.object( - test_module, - "V10CredentialExchange", - autospec=True + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager, async_mock.patch.object( + test_module, "V10CredentialExchange", autospec=True ) as mock_cred_ex, async_mock.patch.object( - test_module, - "ProblemReport", - autospec=True + test_module, "ProblemReport", autospec=True ) as mock_prob_report: mock_cred_ex.retrieve_by_id = async_mock.CoroutineMock() @@ -1013,21 +863,13 @@ async def test_credential_exchange_problem_report_no_cred_record(self): } with async_mock.patch.object( - test_module, - "ConnectionRecord", - autospec=True + test_module, "ConnectionRecord", autospec=True ) as mock_connection_record, async_mock.patch.object( - test_module, - "CredentialManager", - autospec=True - ) as mock_connection_manager, async_mock.patch.object( - test_module, - "V10CredentialExchange", - autospec=True + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager, async_mock.patch.object( + test_module, "V10CredentialExchange", autospec=True ) as mock_cred_ex, async_mock.patch.object( - test_module, - "ProblemReport", - autospec=True + test_module, "ProblemReport", autospec=True ) as mock_prob_report: # Emulate storage not found (bad connection id) diff --git a/demo/runners/performance.py b/demo/runners/performance.py index 68140825a0..e992c09689 100644 --- a/demo/runners/performance.py +++ b/demo/runners/performance.py @@ -26,6 +26,8 @@ def __init__( super().__init__(ident, port, port + 1, prefix=prefix, **kwargs) self._connection_id = None self._connection_ready = None + self.credential_state = {} + self.credential_event = asyncio.Event() @property def connection_id(self) -> str: @@ -69,31 +71,32 @@ async def handle_connections(self, payload): self.log("Connected") self._connection_ready.set_result(True) - -class AliceAgent(BaseAgent): - def __init__(self, port: int, **kwargs): - super().__init__("Alice", port, seed=None, **kwargs) - self.credential_state = {} - self.credential_event = asyncio.Event() - self.extra_args = ["--auto-respond-credential-offer", "--auto-store-credential"] - async def handle_credentials(self, payload): cred_id = payload["credential_exchange_id"] self.credential_state[cred_id] = payload["state"] self.credential_event.set() - def check_received_creds(self) -> (int, int): - self.credential_event.clear() - pending = 0 - total = len(self.credential_state) - for result in self.credential_state.values(): - if result != "stored": - pending += 1 - return pending, total + async def check_received_creds(self) -> (int, int): + while True: + self.credential_event.clear() + pending = 0 + total = len(self.credential_state) + for result in self.credential_state.values(): + if result != "stored": + pending += 1 + if self.credential_event.is_set(): + continue + return pending, total async def update_creds(self): await self.credential_event.wait() + +class AliceAgent(BaseAgent): + def __init__(self, port: int, **kwargs): + super().__init__("Alice", port, seed=None, **kwargs) + self.extra_args = ["--auto-respond-credential-offer", "--auto-store-credential"] + async def set_tag_policy(self, cred_def_id, taggables): req_body = {"taggables": taggables} await self.admin_POST(f"/wallet/tag-policy/{cred_def_id}", req_body) @@ -102,28 +105,9 @@ async def set_tag_policy(self, cred_def_id, taggables): class FaberAgent(BaseAgent): def __init__(self, port: int, **kwargs): super().__init__("Faber", port, **kwargs) - self.credential_state = {} - self.credential_event = asyncio.Event() self.schema_id = None self.credential_definition_id = None - async def handle_credentials(self, payload): - cred_id = payload["credential_exchange_id"] - self.credential_state[cred_id] = payload["state"] - self.credential_event.set() - - def check_received_creds(self) -> (int, int): - self.credential_event.clear() - pending = 0 - total = len(self.credential_state) - for result in self.credential_state.values(): - if result != "stored": - pending += 1 - return pending, total - - async def update_creds(self): - await self.credential_event.wait() - async def publish_defs(self): # create a schema self.log("Publishing test schema") @@ -173,7 +157,12 @@ def __init__(self, port: int, **kwargs): super().__init__("Router", port, **kwargs) -async def main(start_port: int, show_timing: bool = False, routing: bool = False): +async def main( + start_port: int, + show_timing: bool = False, + routing: bool = False, + issue_count: int = 300, +): genesis = await default_genesis_txns() if not genesis: @@ -235,7 +224,6 @@ async def main(start_port: int, show_timing: bool = False, routing: bool = False if routing: await alice_router.reset_timing() - issue_count = 300 batch_size = 100 semaphore = asyncio.Semaphore(10) @@ -254,18 +242,23 @@ async def send(): async def check_received(agent, issue_count, pb): reported = 0 iter_pb = iter(pb) if pb else None + prev = -1 while True: - pending, total = agent.check_received_creds() - if iter_pb and total > reported: + pending, total = await agent.check_received_creds() + complete = total - pending + if prev == complete: + await asyncio.wait_for(agent.update_creds(), 30) + continue + prev = complete + if iter_pb and complete > reported: try: - while next(iter_pb) < total: + while next(iter_pb) < complete: pass except StopIteration: iter_pb = None - reported = total - if total == issue_count and not pending: + reported = complete + if reported == issue_count: break - await asyncio.wait_for(agent.update_creds(), 30) with progress() as pb: receive_task = None @@ -298,11 +291,11 @@ async def check_received(agent, issue_count, pb): alice.log(f"Average time per credential: {avg:.2f}s ({1/avg:.2f}/s)") if alice.postgres: - await alice.collect_postgres_stats(str(issue_count) + " creds") + await alice.collect_postgres_stats(f"{issue_count} creds") for line in alice.format_postgres_stats(): alice.log(line) if faber.postgres: - await faber.collect_postgres_stats(str(issue_count) + " creds") + await faber.collect_postgres_stats(f"{issue_count} creds") for line in faber.format_postgres_stats(): faber.log(line) @@ -356,6 +349,13 @@ async def check_received(agent, issue_count, pb): parser = argparse.ArgumentParser( description="Runs an automated credential issuance performance demo." ) + parser.add_argument( + "-c", + "--count", + type=int, + default=300, + help="Set the number of credentials to issue", + ) parser.add_argument( "-p", "--port", @@ -376,7 +376,7 @@ async def check_received(agent, issue_count, pb): try: asyncio.get_event_loop().run_until_complete( - main(args.port, args.timing, args.routing) + main(args.port, args.timing, args.routing, args.count) ) except KeyboardInterrupt: os._exit(1)