diff --git a/pkg/client/vcwallet/client.go b/pkg/client/vcwallet/client.go index efbe1024a..152e6328f 100644 --- a/pkg/client/vcwallet/client.go +++ b/pkg/client/vcwallet/client.go @@ -62,8 +62,9 @@ var noAuth walletAuth = func() (string, error) { return "", ErrWalletLocked } // Client enable access to verifiable credential wallet features. type Client struct { - wallet *wallet.Wallet - auth walletAuth + wallet *wallet.Wallet + didComm *wallet.DidComm + auth walletAuth } // New returns new verifiable credential wallet client for given user. @@ -83,7 +84,12 @@ func New(userID string, ctx provider, options ...wallet.UnlockOptions) (*Client, return nil, err } - client := &Client{wallet: w, auth: noAuth} + didComm, err := wallet.NewDidComm(w, ctx) + if err != nil { + return nil, err + } + + client := &Client{wallet: w, didComm: didComm, auth: noAuth} if len(options) > 0 { if client.Close() { @@ -370,7 +376,7 @@ func (c *Client) Connect(invitation *outofband.Invitation, options ...wallet.Con return "", err } - return c.wallet.Connect(auth, invitation, options...) + return c.didComm.Connect(auth, invitation, options...) } // ProposePresentation accepts out-of-band invitation and sends message proposing presentation @@ -395,7 +401,7 @@ func (c *Client) ProposePresentation(invitation *wallet.GenericInvitation, optio return nil, err } - return c.wallet.ProposePresentation(auth, invitation, options...) + return c.didComm.ProposePresentation(auth, invitation, options...) } // PresentProof sends message present proof message from wallet to relying party. @@ -418,7 +424,7 @@ func (c *Client) PresentProof(thID string, presentProofFrom ...wallet.ConcludeIn return nil, err } - return c.wallet.PresentProof(auth, thID, presentProofFrom...) + return c.didComm.PresentProof(auth, thID, presentProofFrom...) } // ProposeCredential sends propose credential message from wallet to issuer. @@ -441,7 +447,7 @@ func (c *Client) ProposeCredential(invitation *wallet.GenericInvitation, options return nil, err } - return c.wallet.ProposeCredential(auth, invitation, options...) + return c.didComm.ProposeCredential(auth, invitation, options...) } // RequestCredential sends request credential message from wallet to issuer and @@ -465,7 +471,7 @@ func (c *Client) RequestCredential(thID string, options ...wallet.ConcludeIntera return nil, err } - return c.wallet.RequestCredential(auth, thID, options...) + return c.didComm.RequestCredential(auth, thID, options...) } // ResolveCredentialManifest resolves given credential manifest by credential fulfillment or credential. diff --git a/pkg/controller/command/vcwallet/command.go b/pkg/controller/command/vcwallet/command.go index 429dbf07a..3b618c42e 100644 --- a/pkg/controller/command/vcwallet/command.go +++ b/pkg/controller/command/vcwallet/command.go @@ -761,7 +761,14 @@ func (o *Command) Connect(rw io.Writer, req io.Reader) command.Error { return command.NewExecuteError(DIDConnectErrorCode, err) } - connectionID, err := vcWallet.Connect(request.Auth, request.Invitation, + didComm, err := wallet.NewDidComm(vcWallet, o.ctx) + if err != nil { + logutil.LogInfo(logger, CommandName, ConnectMethod, err.Error()) + + return command.NewExecuteError(DIDConnectErrorCode, err) + } + + connectionID, err := didComm.Connect(request.Auth, request.Invitation, wallet.WithConnectTimeout(request.Timeout), wallet.WithReuseDID(request.ReuseConnection), wallet.WithReuseAnyConnection(request.ReuseAnyConnection), wallet.WithMyLabel(request.MyLabel), wallet.WithRouterConnections(request.RouterConnections...)) @@ -805,7 +812,14 @@ func (o *Command) ProposePresentation(rw io.Writer, req io.Reader) command.Error return command.NewExecuteError(ProposePresentationErrorCode, err) } - msg, err := vcWallet.ProposePresentation(request.Auth, request.Invitation, + didComm, err := wallet.NewDidComm(vcWallet, o.ctx) + if err != nil { + logutil.LogInfo(logger, CommandName, ProposePresentationMethod, err.Error()) + + return command.NewExecuteError(ProposePresentationErrorCode, err) + } + + msg, err := didComm.ProposePresentation(request.Auth, request.Invitation, wallet.WithFromDID(request.FromDID), wallet.WithInitiateTimeout(request.Timeout), wallet.WithConnectOptions(wallet.WithConnectTimeout(request.ConnectionOpts.Timeout), wallet.WithReuseDID(request.ConnectionOpts.ReuseConnection), @@ -849,7 +863,14 @@ func (o *Command) PresentProof(rw io.Writer, req io.Reader) command.Error { return command.NewExecuteError(PresentProofErrorCode, err) } - status, err := vcWallet.PresentProof(request.Auth, request.ThreadID, + didComm, err := wallet.NewDidComm(vcWallet, o.ctx) + if err != nil { + logutil.LogInfo(logger, CommandName, PresentProofMethod, err.Error()) + + return command.NewExecuteError(PresentProofErrorCode, err) + } + + status, err := didComm.PresentProof(request.Auth, request.ThreadID, prepareConcludeInteractionOpts(request.WaitForDone, request.Timeout, request.Presentation)...) if err != nil { logutil.LogInfo(logger, CommandName, PresentProofMethod, err.Error()) @@ -888,7 +909,14 @@ func (o *Command) ProposeCredential(rw io.Writer, req io.Reader) command.Error { return command.NewExecuteError(ProposeCredentialErrorCode, err) } - msg, err := vcWallet.ProposeCredential(request.Auth, request.Invitation, + didComm, err := wallet.NewDidComm(vcWallet, o.ctx) + if err != nil { + logutil.LogInfo(logger, CommandName, ProposeCredentialMethod, err.Error()) + + return command.NewExecuteError(ProposeCredentialErrorCode, err) + } + + msg, err := didComm.ProposeCredential(request.Auth, request.Invitation, wallet.WithFromDID(request.FromDID), wallet.WithInitiateTimeout(request.Timeout), wallet.WithConnectOptions(wallet.WithConnectTimeout(request.ConnectionOpts.Timeout), wallet.WithReuseDID(request.ConnectionOpts.ReuseConnection), @@ -933,7 +961,14 @@ func (o *Command) RequestCredential(rw io.Writer, req io.Reader) command.Error { return command.NewExecuteError(RequestCredentialErrorCode, err) } - status, err := vcWallet.RequestCredential(request.Auth, request.ThreadID, + didComm, err := wallet.NewDidComm(vcWallet, o.ctx) + if err != nil { + logutil.LogInfo(logger, CommandName, RequestCredentialMethod, err.Error()) + + return command.NewExecuteError(RequestCredentialErrorCode, err) + } + + status, err := didComm.RequestCredential(request.Auth, request.ThreadID, prepareConcludeInteractionOpts(request.WaitForDone, request.Timeout, request.Presentation)...) if err != nil { logutil.LogInfo(logger, CommandName, RequestCredentialMethod, err.Error()) diff --git a/pkg/wallet/didcomm.go b/pkg/wallet/didcomm.go new file mode 100644 index 000000000..3c14da862 --- /dev/null +++ b/pkg/wallet/didcomm.go @@ -0,0 +1,665 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package wallet + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + + "github.com/hyperledger/aries-framework-go/pkg/client/didexchange" + "github.com/hyperledger/aries-framework-go/pkg/client/issuecredential" + "github.com/hyperledger/aries-framework-go/pkg/client/outofband" + "github.com/hyperledger/aries-framework-go/pkg/client/outofbandv2" + "github.com/hyperledger/aries-framework-go/pkg/client/presentproof" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/common/model" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/decorator" + didexchangeSvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/didexchange" + issuecredentialsvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/issuecredential" + outofbandv2svc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/outofbandv2" + "github.com/hyperledger/aries-framework-go/pkg/kms" + "github.com/hyperledger/aries-framework-go/pkg/store/connection" + "github.com/hyperledger/aries-framework-go/spi/storage" +) + +// miscellaneous constants. +const ( + msgEventBufferSize = 10 + ldJSONMimeType = "application/ld+json" + + // protocol states. + stateNameAbandoned = "abandoned" + stateNameAbandoning = "abandoning" + stateNameDone = "done" + + // timeout constants. + defaultDIDExchangeTimeOut = 120 * time.Second + defaultWaitForRequestPresentationTimeOut = 120 * time.Second + defaultWaitForPresentProofDone = 120 * time.Second + retryDelay = 500 * time.Millisecond +) + +// didCommProvider to be used only if wallet needs to be participated in DIDComm operation. +// TODO: using wallet KMS instead of provider KMS. +// TODO: reconcile Protocol storage with wallet store. +type didCommProvider interface { + KMS() kms.KeyManager + ServiceEndpoint() string + ProtocolStateStorageProvider() storage.Provider + Service(id string) (interface{}, error) + KeyType() kms.KeyType + KeyAgreementType() kms.KeyType +} + +type combinedDidCommWalletProvider interface { + provider + didCommProvider +} + +// DidComm enables access to verifiable credential wallet features. +type DidComm struct { + // wallet implementation + wallet *Wallet + + // present proof client + presentProofClient *presentproof.Client + + // issue credential client + issueCredentialClient *issuecredential.Client + + // out of band client + oobClient *outofband.Client + + // out of band v2 client + oobV2Client *outofbandv2.Client + + // did-exchange client + didexchangeClient *didexchange.Client + + // connection lookup + connectionLookup *connection.Lookup +} + +// NewDidComm returns new verifiable credential wallet for given user. +// returns error if wallet profile is not found. +// To create a new wallet profile, use `CreateProfile()`. +// To update an existing profile, use `UpdateProfile()`. +func NewDidComm(wallet *Wallet, ctx combinedDidCommWalletProvider) (*DidComm, error) { + presentProofClient, err := presentproof.New(ctx) + if err != nil { + return nil, fmt.Errorf("failed to initialize present proof client: %w", err) + } + + issueCredentialClient, err := issuecredential.New(ctx) + if err != nil { + return nil, fmt.Errorf("failed to initialize issue credential client: %w", err) + } + + oobClient, err := outofband.New(ctx) + if err != nil { + return nil, fmt.Errorf("failed to initialize out-of-band client: %w", err) + } + + oobV2Client, err := outofbandv2.New(ctx) + if err != nil { + return nil, fmt.Errorf("failed to initialize out-of-band v2 client: %w", err) + } + + connectionLookup, err := connection.NewLookup(ctx) + if err != nil { + return nil, fmt.Errorf("failed to initialize connection lookup: %w", err) + } + + didexchangeClient, err := didexchange.New(ctx) + if err != nil { + return nil, fmt.Errorf("failed to initialize didexchange client: %w", err) + } + + return &DidComm{ + wallet: wallet, + presentProofClient: presentProofClient, + issueCredentialClient: issueCredentialClient, + oobClient: oobClient, + oobV2Client: oobV2Client, + didexchangeClient: didexchangeClient, + connectionLookup: connectionLookup, + }, nil +} + +// Connect accepts out-of-band invitations and performs DID exchange. +// +// Args: +// - authToken: authorization for performing create key pair operation. +// - invitation: out-of-band invitation. +// - options: connection options. +// +// Returns: +// - connection ID if DID exchange is successful. +// - error if operation false. +// +func (c *DidComm) Connect(authToken string, invitation *outofband.Invitation, options ...ConnectOptions) (string, error) { //nolint: lll + statusCh := make(chan service.StateMsg, msgEventBufferSize) + + err := c.didexchangeClient.RegisterMsgEvent(statusCh) + if err != nil { + return "", fmt.Errorf("failed to register msg event : %w", err) + } + + defer func() { + e := c.didexchangeClient.UnregisterMsgEvent(statusCh) + if e != nil { + logger.Warnf("Failed to unregister msg event for connect: %w", e) + } + }() + + opts := &connectOpts{} + for _, opt := range options { + opt(opts) + } + + connID, err := c.oobClient.AcceptInvitation(invitation, opts.Label, getOobMessageOptions(opts)...) + if err != nil { + return "", fmt.Errorf("failed to accept invitation : %w", err) + } + + if opts.timeout == 0 { + opts.timeout = defaultDIDExchangeTimeOut + } + + ctx, cancel := context.WithTimeout(context.Background(), opts.timeout) + defer cancel() + + err = waitForConnect(ctx, statusCh, connID) + if err != nil { + return "", fmt.Errorf("wallet connect failed : %w", err) + } + + return connID, nil +} + +// ProposePresentation accepts out-of-band invitation and sends message proposing presentation +// from wallet to relying party. +// https://w3c-ccg.github.io/universal-wallet-interop-spec/#proposepresentation +// +// Currently Supporting +// [0454-present-proof-v2](https://github.com/hyperledger/aries-rfcs/tree/master/features/0454-present-proof-v2) +// +// Args: +// - authToken: authorization for performing operation. +// - invitation: out-of-band invitation from relying party. +// - options: options for accepting invitation and send propose presentation message. +// +// Returns: +// - DIDCommMsgMap containing request presentation message if operation is successful. +// - error if operation fails. +// +func (c *DidComm) ProposePresentation(authToken string, invitation *GenericInvitation, options ...InitiateInteractionOption) (*service.DIDCommMsgMap, error) { //nolint: lll + opts := &initiateInteractionOpts{} + for _, opt := range options { + opt(opts) + } + + var ( + connID string + err error + ) + + switch invitation.Version() { + default: + fallthrough + case service.V1: + connID, err = c.Connect(authToken, (*outofband.Invitation)(invitation.AsV1()), opts.connectOpts...) + if err != nil { + return nil, fmt.Errorf("failed to perform did connection : %w", err) + } + case service.V2: + connOpts := &connectOpts{} + + for _, opt := range opts.connectOpts { + opt(connOpts) + } + + connID, err = c.oobV2Client.AcceptInvitation( + invitation.AsV2(), + outofbandv2svc.WithRouterConnections(connOpts.Connections), + ) + if err != nil { + return nil, fmt.Errorf("failed to accept OOB v2 invitation : %w", err) + } + } + + connRecord, err := c.connectionLookup.GetConnectionRecord(connID) + if err != nil { + return nil, fmt.Errorf("failed to lookup connection for propose presentation : %w", err) + } + + opts = prepareInteractionOpts(connRecord, opts) + + _, err = c.presentProofClient.SendProposePresentation(&presentproof.ProposePresentation{}, connRecord) + if err != nil { + return nil, fmt.Errorf("failed to propose presentation from wallet: %w", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), opts.timeout) + defer cancel() + + return c.waitForRequestPresentation(ctx, connRecord) +} + +// PresentProof sends message present proof message from wallet to relying party. +// https://w3c-ccg.github.io/universal-wallet-interop-spec/#presentproof +// +// Currently Supporting +// [0454-present-proof-v2](https://github.com/hyperledger/aries-rfcs/tree/master/features/0454-present-proof-v2) +// +// Args: +// - authToken: authorization for performing operation. +// - thID: thread ID (action ID) of request presentation. +// - presentProofFrom: presentation to be sent. +// +// Returns: +// - Credential interaction status containing status, redirectURL. +// - error if operation fails. +// +func (c *DidComm) PresentProof(authToken, thID string, options ...ConcludeInteractionOptions) (*CredentialInteractionStatus, error) { //nolint: lll + opts := &concludeInteractionOpts{} + + for _, option := range options { + option(opts) + } + + var presentation interface{} + if opts.presentation != nil { + presentation = opts.presentation + } else { + presentation = opts.rawPresentation + } + + err := c.presentProofClient.AcceptRequestPresentation(thID, &presentproof.Presentation{ + Attachments: []decorator.GenericAttachment{{ + ID: uuid.New().String(), + Data: decorator.AttachmentData{ + JSON: presentation, + }, + }}, + }, nil) + if err != nil { + return nil, err + } + + // wait for ack or problem-report. + if opts.waitForDone { + statusCh := make(chan service.StateMsg, msgEventBufferSize) + + err = c.presentProofClient.RegisterMsgEvent(statusCh) + if err != nil { + return nil, fmt.Errorf("failed to register present proof msg event : %w", err) + } + + defer func() { + e := c.presentProofClient.UnregisterMsgEvent(statusCh) + if e != nil { + logger.Warnf("Failed to unregister msg event for present proof: %w", e) + } + }() + + ctx, cancel := context.WithTimeout(context.Background(), opts.timeout) + defer cancel() + + return waitCredInteractionCompletion(ctx, statusCh, thID) + } + + return &CredentialInteractionStatus{Status: model.AckStatusPENDING}, nil +} + +// ProposeCredential sends propose credential message from wallet to issuer. +// https://w3c-ccg.github.io/universal-wallet-interop-spec/#proposecredential +// +// Currently Supporting : 0453-issueCredentialV2 +// https://github.com/hyperledger/aries-rfcs/blob/main/features/0453-issue-credential-v2/README.md +// +// Args: +// - authToken: authorization for performing operation. +// - invitation: out-of-band invitation from issuer. +// - options: options for accepting invitation and send propose credential message. +// +// Returns: +// - DIDCommMsgMap containing offer credential message if operation is successful. +// - error if operation fails. +// +func (c *DidComm) ProposeCredential(authToken string, invitation *GenericInvitation, options ...InitiateInteractionOption) (*service.DIDCommMsgMap, error) { //nolint: lll + opts := &initiateInteractionOpts{} + for _, opt := range options { + opt(opts) + } + + var ( + connID string + err error + ) + + switch invitation.Version() { + default: + fallthrough + case service.V1: + connID, err = c.Connect(authToken, (*outofband.Invitation)(invitation.AsV1()), opts.connectOpts...) + if err != nil { + return nil, fmt.Errorf("failed to perform did connection : %w", err) + } + case service.V2: + connOpts := &connectOpts{} + + for _, opt := range opts.connectOpts { + opt(connOpts) + } + + connID, err = c.oobV2Client.AcceptInvitation( + invitation.AsV2(), + outofbandv2svc.WithRouterConnections(connOpts.Connections), + ) + if err != nil { + return nil, fmt.Errorf("failed to accept OOB v2 invitation : %w", err) + } + } + + connRecord, err := c.connectionLookup.GetConnectionRecord(connID) + if err != nil { + return nil, fmt.Errorf("failed to lookup connection for propose presentation : %w", err) + } + + opts = prepareInteractionOpts(connRecord, opts) + + _, err = c.issueCredentialClient.SendProposal( + &issuecredential.ProposeCredential{InvitationID: invitation.ID}, + connRecord, + ) + if err != nil { + return nil, fmt.Errorf("failed to propose credential from wallet: %w", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), opts.timeout) + defer cancel() + + return c.waitForOfferCredential(ctx, connRecord) +} + +// RequestCredential sends request credential message from wallet to issuer and +// optionally waits for credential fulfillment. +// https://w3c-ccg.github.io/universal-wallet-interop-spec/#requestcredential +// +// Currently Supporting : 0453-issueCredentialV2 +// https://github.com/hyperledger/aries-rfcs/blob/main/features/0453-issue-credential-v2/README.md +// +// Args: +// - authToken: authorization for performing operation. +// - thID: thread ID (action ID) of offer credential message previously received. +// - concludeInteractionOptions: options to conclude interaction like presentation to be shared etc. +// +// Returns: +// - Credential interaction status containing status, redirectURL. +// - error if operation fails. +// +func (c *DidComm) RequestCredential(authToken, thID string, options ...ConcludeInteractionOptions) (*CredentialInteractionStatus, error) { //nolint: lll + opts := &concludeInteractionOpts{} + + for _, option := range options { + option(opts) + } + + var presentation interface{} + if opts.presentation != nil { + presentation = opts.presentation + } else { + presentation = opts.rawPresentation + } + + attachmentID := uuid.New().String() + + err := c.issueCredentialClient.AcceptOffer(thID, &issuecredential.RequestCredential{ + Type: issuecredentialsvc.RequestCredentialMsgTypeV2, + Formats: []issuecredentialsvc.Format{{ + AttachID: attachmentID, + Format: ldJSONMimeType, + }}, + Attachments: []decorator.GenericAttachment{{ + ID: attachmentID, + Data: decorator.AttachmentData{ + JSON: presentation, + }, + }}, + }) + if err != nil { + return nil, err + } + + // wait for credential fulfillment. + if opts.waitForDone { + statusCh := make(chan service.StateMsg, msgEventBufferSize) + + err = c.issueCredentialClient.RegisterMsgEvent(statusCh) + if err != nil { + return nil, fmt.Errorf("failed to register issue credential action event : %w", err) + } + + defer func() { + e := c.issueCredentialClient.UnregisterMsgEvent(statusCh) + if e != nil { + logger.Warnf("Failed to unregister action event for issue credential: %w", e) + } + }() + + ctx, cancel := context.WithTimeout(context.Background(), opts.timeout) + defer cancel() + + return waitCredInteractionCompletion(ctx, statusCh, thID) + } + + return &CredentialInteractionStatus{Status: model.AckStatusPENDING}, nil +} + +// currently correlating response action by connection due to limitation in current present proof V1 implementation. +func (c *DidComm) waitForRequestPresentation(ctx context.Context, record *connection.Record) (*service.DIDCommMsgMap, error) { //nolint: lll + done := make(chan *service.DIDCommMsgMap) + + go func() { + for { + actions, err := c.presentProofClient.Actions() + if err != nil { + continue + } + + if len(actions) > 0 { + for _, action := range actions { + if action.MyDID == record.MyDID && action.TheirDID == record.TheirDID { + done <- &action.Msg + return + } + } + } + + select { + default: + time.Sleep(retryDelay) + case <-ctx.Done(): + return + } + } + }() + + select { + case msg := <-done: + return msg, nil + case <-ctx.Done(): + return nil, fmt.Errorf("timeout waiting for request presentation message") + } +} + +// currently correlating response action by connection due to limitation in current issue credential V1 implementation. +func (c *DidComm) waitForOfferCredential(ctx context.Context, record *connection.Record) (*service.DIDCommMsgMap, error) { //nolint: lll + done := make(chan *service.DIDCommMsgMap) + + go func() { + for { + actions, err := c.issueCredentialClient.Actions() + if err != nil { + continue + } + + if len(actions) > 0 { + for _, action := range actions { + if action.MyDID == record.MyDID && action.TheirDID == record.TheirDID { + done <- &action.Msg + return + } + } + } + + select { + default: + time.Sleep(retryDelay) + case <-ctx.Done(): + return + } + } + }() + + select { + case msg := <-done: + return msg, nil + case <-ctx.Done(): + return nil, fmt.Errorf("timeout waiting for offer credential message") + } +} + +func waitForConnect(ctx context.Context, didStateMsgs chan service.StateMsg, connID string) error { + done := make(chan struct{}) + + go func() { + for msg := range didStateMsgs { + if msg.Type != service.PostState || msg.StateID != didexchangeSvc.StateIDCompleted { + continue + } + + var event didexchangeSvc.Event + + switch p := msg.Properties.(type) { + case didexchangeSvc.Event: + event = p + default: + logger.Warnf("failed to cast didexchange event properties") + + continue + } + + if event.ConnectionID() == connID { + logger.Debugf( + "Received connection complete event for invitationID=%s connectionID=%s", + event.InvitationID(), event.ConnectionID()) + + close(done) + + break + } + } + }() + + select { + case <-done: + return nil + case <-ctx.Done(): + return fmt.Errorf("time out waiting for did exchange state 'completed'") + } +} + +// wait for credential interaction to be completed (done or abandoned protocol state). +func waitCredInteractionCompletion(ctx context.Context, didStateMsgs chan service.StateMsg, thID string) (*CredentialInteractionStatus, error) { // nolint:gocognit,gocyclo,lll + done := make(chan *CredentialInteractionStatus) + + go func() { + for msg := range didStateMsgs { + // match post state. + if msg.Type != service.PostState { + continue + } + + // invalid state msg. + if msg.Msg == nil { + continue + } + + msgThID, err := msg.Msg.ThreadID() + if err != nil { + continue + } + + // match parent thread ID. + if msg.Msg.ParentThreadID() != thID && msgThID != thID { + continue + } + + // match protocol state. + if msg.StateID != stateNameDone && msg.StateID != stateNameAbandoned && msg.StateID != stateNameAbandoning { + continue + } + + properties := msg.Properties.All() + + response := &CredentialInteractionStatus{} + response.RedirectURL, response.Status = getWebRedirectInfo(properties) + + // if redirect status missing, then use protocol state, done -> OK, abandoned -> FAIL. + if response.Status == "" { + if msg.StateID == stateNameAbandoned || msg.StateID == stateNameAbandoning { + response.Status = model.AckStatusFAIL + } else { + response.Status = model.AckStatusOK + } + } + + done <- response + + return + } + }() + + select { + case status := <-done: + return status, nil + case <-ctx.Done(): + return nil, fmt.Errorf("time out waiting for credential interaction to get completed") + } +} + +func prepareInteractionOpts(connRecord *connection.Record, opts *initiateInteractionOpts) *initiateInteractionOpts { + if opts.from == "" { + opts.from = connRecord.TheirDID + } + + if opts.timeout == 0 { + opts.timeout = defaultWaitForRequestPresentationTimeOut + } + + return opts +} + +// getWebRedirectInfo reads web redirect info from properties. +func getWebRedirectInfo(properties map[string]interface{}) (string, string) { + var redirect, status string + + if redirectURL, ok := properties[webRedirectURLKey]; ok { + redirect = redirectURL.(string) //nolint: errcheck, forcetypeassert + } + + if redirectStatus, ok := properties[webRedirectStatusKey]; ok { + status = redirectStatus.(string) //nolint: errcheck, forcetypeassert + } + + return redirect, status +} diff --git a/pkg/wallet/didcomm_test.go b/pkg/wallet/didcomm_test.go new file mode 100644 index 000000000..b0ab7ca0d --- /dev/null +++ b/pkg/wallet/didcomm_test.go @@ -0,0 +1,1880 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package wallet + +import ( + _ "embed" + "encoding/json" + "errors" + "fmt" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/hyperledger/aries-framework-go/pkg/client/outofband" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/common/model" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/didexchange" + issuecredentialsvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/issuecredential" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/mediator" + outofbandSvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/outofband" + oobv2 "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/outofbandv2" + presentproofSvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/presentproof" + "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" + mockoutofbandv2 "github.com/hyperledger/aries-framework-go/pkg/internal/gomocks/client/outofbandv2" + "github.com/hyperledger/aries-framework-go/pkg/internal/ldtestutil" + mockdidexchange "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/didexchange" + mockissuecredential "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/issuecredential" + mockmediator "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/mediator" + mockoutofband "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/outofband" + mockpresentproof "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/presentproof" + mockprovider "github.com/hyperledger/aries-framework-go/pkg/mock/provider" + mockstorage "github.com/hyperledger/aries-framework-go/pkg/mock/storage" + "github.com/hyperledger/aries-framework-go/pkg/store/connection" +) + +const ( + exampleWebRedirect = "http://example.com/sample" + sampleMsgComment = "sample mock msg" +) + +func TestNewDidComm(t *testing.T) { + t.Run("test get wallet failure - present proof client initialize error", func(t *testing.T) { + mockctx := newDidCommMockProvider(t) + delete(mockctx.ServiceMap, presentproofSvc.Name) + + err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + wallet, err := New(sampleUserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.Error(t, err) + require.Empty(t, didcomm) + + require.Contains(t, err.Error(), "failed to initialize present proof client") + }) + + t.Run("test get wallet failure - oob client initialize error", func(t *testing.T) { + mockctx := newDidCommMockProvider(t) + delete(mockctx.ServiceMap, outofbandSvc.Name) + + err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + wallet, err := New(sampleUserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.Error(t, err) + require.Empty(t, didcomm) + require.Contains(t, err.Error(), "failed to initialize out-of-band client") + }) + + t.Run("test get wallet failure - oob client initialize error", func(t *testing.T) { + mockctx := newDidCommMockProvider(t) + delete(mockctx.ServiceMap, didexchange.DIDExchange) + + err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + wallet, err := New(sampleUserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.Error(t, err) + require.Empty(t, didcomm) + require.Contains(t, err.Error(), "failed to initialize didexchange client") + }) + + t.Run("test get wallet failure - connection lookup initialize error", func(t *testing.T) { + mockctx := newDidCommMockProvider(t) + mockStoreProvider := mockstorage.NewMockStoreProvider() + mockStoreProvider.FailNamespace = "didexchange" + mockctx.StorageProviderValue = mockStoreProvider + + err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + wallet, err := New(sampleUserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.Error(t, err) + require.Empty(t, didcomm) + require.Contains(t, err.Error(), "failed to initialize connection lookup") + }) +} + +func TestWallet_Connect(t *testing.T) { + sampleDIDCommUser := uuid.New().String() + mockctx := newDidCommMockProvider(t) + err := CreateProfile(sampleDIDCommUser, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + t.Run("test did connect success", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + } + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + didexSvc := &mockdidexchange.MockDIDExchangeSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + connectionID, err := didcomm.Connect(token, &outofband.Invitation{}) + require.NoError(t, err) + require.Equal(t, sampleConnID, connectionID) + }) + + t.Run("test did connect failure - accept invitation failure", func(t *testing.T) { + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return "", fmt.Errorf(sampleWalletErr) + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + connectionID, err := didcomm.Connect(token, &outofband.Invitation{}, WithConnectTimeout(1*time.Millisecond)) + require.Error(t, err) + require.Contains(t, err.Error(), sampleWalletErr) + require.Contains(t, err.Error(), "failed to accept invitation") + require.Empty(t, connectionID) + }) + + t.Run("test did connect failure - register event failure", func(t *testing.T) { + didexSvc := &mockdidexchange.MockDIDExchangeSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + return fmt.Errorf(sampleWalletErr) + }, + } + mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + connectionID, err := didcomm.Connect(token, &outofband.Invitation{}, WithConnectTimeout(1*time.Millisecond)) + require.Error(t, err) + require.Contains(t, err.Error(), sampleWalletErr) + require.Contains(t, err.Error(), "failed to register msg event") + require.Empty(t, connectionID) + }) + + t.Run("test did connect failure - state not completed", func(t *testing.T) { + mockctx.ServiceMap[outofbandSvc.Name] = &mockoutofband.MockOobService{} + mockctx.ServiceMap[didexchange.DIDExchange] = &mockdidexchange.MockDIDExchangeSvc{} + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + connectionID, err := didcomm.Connect(token, &outofband.Invitation{}, WithConnectTimeout(1*time.Millisecond)) + require.Error(t, err) + require.Contains(t, err.Error(), "time out waiting for did exchange state 'completed'") + require.Empty(t, connectionID) + }) + + t.Run("test did connect success - with warnings", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + } + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + didexSvc := &mockdidexchange.MockDIDExchangeSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PreState, + StateID: didexchange.StateIDCompleted, + } + + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + } + + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + UnregisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + return fmt.Errorf(sampleWalletErr) + }, + } + mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + connectionID, err := didcomm.Connect(token, &outofband.Invitation{}) + require.NoError(t, err) + require.Equal(t, sampleConnID, connectionID) + }) + + t.Run("test oob connect options", func(t *testing.T) { + options := []ConnectOptions{ + WithConnectTimeout(10 * time.Second), + WithRouterConnections("sample-conn"), + WithMyLabel("sample-label"), + WithReuseAnyConnection(true), + WithReuseDID("sample-did"), + } + + opts := &connectOpts{} + for _, opt := range options { + opt(opts) + } + + require.Equal(t, opts.timeout, 10*time.Second) + require.Equal(t, opts.Connections[0], "sample-conn") + require.Equal(t, opts.MyLabel(), "sample-label") + require.Equal(t, opts.ReuseDID, "sample-did") + require.True(t, opts.ReuseAny) + + require.Len(t, getOobMessageOptions(opts), 3) + }) +} + +func TestWallet_ProposePresentation(t *testing.T) { + sampleDIDCommUser := uuid.New().String() + mockctx := newDidCommMockProvider(t) + err := CreateProfile(sampleDIDCommUser, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + const ( + myDID = "did:mydid:123" + theirDID = "did:theirdid:123" + ) + + t.Run("test propose presentation success - didcomm v1", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + } + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + didexSvc := &mockdidexchange.MockDIDExchangeSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc + + thID := uuid.New().String() + + ppSvc := &mockpresentproof.MockPresentProofSvc{ + ActionsFunc: func() ([]presentproofSvc.Action, error) { + return []presentproofSvc.Action{ + { + PIID: thID, + Msg: service.NewDIDCommMsgMap(&presentproofSvc.RequestPresentationV2{ + Comment: "mock msg", + }), + MyDID: myDID, + TheirDID: theirDID, + }, + }, nil + }, + HandleFunc: func(service.DIDCommMsg) (string, error) { + return thID, nil + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + mockctx.ServiceMap[presentproofSvc.Name] = ppSvc + + store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) + require.NoError(t, err) + + record := &connection.Record{ + ConnectionID: sampleConnID, + MyDID: myDID, + TheirDID: theirDID, + } + recordBytes, err := json.Marshal(record) + require.NoError(t, err) + require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + invitation := GenericInvitation{} + err = json.Unmarshal([]byte(`{ + "@id": "abc123", + "@type": "https://didcomm.org/out-of-band/1.0/invitation" + }`), &invitation) + require.NoError(t, err) + + msg, err := didcomm.ProposePresentation(token, &invitation, + WithConnectOptions(WithConnectTimeout(1*time.Millisecond))) + require.NoError(t, err) + require.NotEmpty(t, msg) + + // empty invitation defaults to DIDComm v1 + msg, err = didcomm.ProposePresentation(token, &GenericInvitation{}, + WithConnectOptions(WithConnectTimeout(1*time.Millisecond))) + require.NoError(t, err) + require.NotEmpty(t, msg) + + // invitation with unknown version defaults to DIDComm v1 + msg, err = didcomm.ProposePresentation(token, &GenericInvitation{version: "unknown"}, + WithConnectOptions(WithConnectTimeout(1*time.Millisecond))) + require.NoError(t, err) + require.NotEmpty(t, msg) + }) + + t.Run("test propose presentation success - didcomm v2", func(t *testing.T) { + sampleConnID := uuid.New().String() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + oobv2Svc := mockoutofbandv2.NewMockOobService(ctrl) + oobv2Svc.EXPECT().AcceptInvitation(gomock.Any(), gomock.Any()).Return(sampleConnID, nil).AnyTimes() + + mockctx.ServiceMap[oobv2.Name] = oobv2Svc + + thID := uuid.New().String() + + ppSvc := &mockpresentproof.MockPresentProofSvc{ + ActionsFunc: func() ([]presentproofSvc.Action, error) { + return []presentproofSvc.Action{ + { + PIID: thID, + Msg: service.NewDIDCommMsgMap(&presentproofSvc.RequestPresentationV3{ + Body: presentproofSvc.RequestPresentationV3Body{ + Comment: "mock msg", + }, + }), + MyDID: myDID, + TheirDID: theirDID, + }, + }, nil + }, + HandleFunc: func(service.DIDCommMsg) (string, error) { + return thID, nil + }, + } + + mockctx.ServiceMap[presentproofSvc.Name] = ppSvc + + connRec, err := connection.NewRecorder(mockctx) + require.NoError(t, err) + + record := &connection.Record{ + ConnectionID: sampleConnID, + MyDID: myDID, + TheirDID: theirDID, + DIDCommVersion: service.V2, + State: connection.StateNameCompleted, + } + + err = connRec.SaveConnectionRecord(record) + require.NoError(t, err) + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + invitation := GenericInvitation{} + err = json.Unmarshal([]byte(`{ + "id": "abc123", + "type": "https://didcomm.org/out-of-band/2.0/invitation" + }`), &invitation) + require.NoError(t, err) + + msg, err := didcomm.ProposePresentation(token, &invitation, + WithConnectOptions(WithConnectTimeout(1*time.Millisecond))) + require.NoError(t, err) + require.NotEmpty(t, msg) + }) + + t.Run("test propose presentation failure - did connect failure", func(t *testing.T) { + didexSvc := &mockdidexchange.MockDIDExchangeSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + return fmt.Errorf(sampleWalletErr) + }, + } + mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + msg, err := didcomm.ProposePresentation(token, &GenericInvitation{}) + require.Error(t, err) + require.Contains(t, err.Error(), sampleWalletErr) + require.Contains(t, err.Error(), "failed to perform did connection") + require.Empty(t, msg) + }) + + t.Run("test propose presentation failure - no connection found", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + } + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + didexSvc := &mockdidexchange.MockDIDExchangeSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + msg, err := didcomm.ProposePresentation(token, &GenericInvitation{}) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to lookup connection") + require.Empty(t, msg) + }) + + t.Run("test propose presentation failure - failed to send", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + } + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + didexSvc := &mockdidexchange.MockDIDExchangeSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc + + ppSvc := &mockpresentproof.MockPresentProofSvc{ + HandleFunc: func(service.DIDCommMsg) (string, error) { + return "", fmt.Errorf(sampleWalletErr) + }, + HandleOutboundFunc: func(service.DIDCommMsg, string, string) (string, error) { + return "", fmt.Errorf(sampleWalletErr) + }, + } + mockctx.ServiceMap[presentproofSvc.Name] = ppSvc + + store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) + require.NoError(t, err) + + record := &connection.Record{ + ConnectionID: sampleConnID, + MyDID: myDID, + TheirDID: theirDID, + } + recordBytes, err := json.Marshal(record) + require.NoError(t, err) + require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + msg, err := didcomm.ProposePresentation(token, &GenericInvitation{}) + require.Error(t, err) + require.Contains(t, err.Error(), sampleWalletErr) + require.Contains(t, err.Error(), "failed to propose presentation from wallet") + require.Empty(t, msg) + }) + + t.Run("test propose presentation failure - timeout waiting for presentation request", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + } + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + didexSvc := &mockdidexchange.MockDIDExchangeSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc + + ppSvc := &mockpresentproof.MockPresentProofSvc{ + HandleFunc: func(service.DIDCommMsg) (string, error) { + return uuid.New().String(), nil + }, + } + mockctx.ServiceMap[presentproofSvc.Name] = ppSvc + + store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) + require.NoError(t, err) + + record := &connection.Record{ + ConnectionID: sampleConnID, + MyDID: myDID, + TheirDID: theirDID, + } + recordBytes, err := json.Marshal(record) + require.NoError(t, err) + require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + msg, err := didcomm.ProposePresentation(token, &GenericInvitation{}, WithInitiateTimeout(600*time.Millisecond)) + require.Error(t, err) + require.Contains(t, err.Error(), "timeout waiting for request presentation message") + require.Empty(t, msg) + }) + + t.Run("test propose presentation failure - action error", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + } + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + didexSvc := &mockdidexchange.MockDIDExchangeSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc + + ppSvc := &mockpresentproof.MockPresentProofSvc{ + HandleFunc: func(service.DIDCommMsg) (string, error) { + return uuid.New().String(), nil + }, + ActionsFunc: func() ([]presentproofSvc.Action, error) { + return nil, fmt.Errorf(sampleWalletErr) + }, + } + mockctx.ServiceMap[presentproofSvc.Name] = ppSvc + + store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) + require.NoError(t, err) + + record := &connection.Record{ + ConnectionID: sampleConnID, + TheirDID: theirDID, + } + recordBytes, err := json.Marshal(record) + require.NoError(t, err) + require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + msg, err := didcomm.ProposePresentation(token, &GenericInvitation{}, WithInitiateTimeout(1*time.Millisecond), + WithFromDID("did:sample:from")) + require.Error(t, err) + require.Contains(t, err.Error(), "timeout waiting for request presentation message") + require.Empty(t, msg) + }) + + t.Run("test propose presentation failure - oob v2 accept error", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + expectErr := fmt.Errorf("expected error") + + oobv2Svc := mockoutofbandv2.NewMockOobService(ctrl) + oobv2Svc.EXPECT().AcceptInvitation(gomock.Any(), gomock.Any()).Return("", expectErr).AnyTimes() + + mockctx.ServiceMap[oobv2.Name] = oobv2Svc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + invitation := GenericInvitation{} + err = json.Unmarshal([]byte(`{ + "id": "abc123", + "type": "https://didcomm.org/out-of-band/2.0/invitation" + }`), &invitation) + require.NoError(t, err) + + _, err = didcomm.ProposePresentation(token, &invitation, + WithConnectOptions(WithConnectTimeout(1*time.Millisecond))) + require.Error(t, err) + require.ErrorIs(t, err, expectErr) + require.Contains(t, err.Error(), "failed to accept OOB v2 invitation") + }) +} + +func TestWallet_PresentProof(t *testing.T) { + sampleDIDCommUser := uuid.New().String() + mockctx := newDidCommMockProvider(t) + err := CreateProfile(sampleDIDCommUser, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + t.Run("test present proof success", func(t *testing.T) { + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + response, err := didcomm.PresentProof(token, uuid.New().String(), FromPresentation(&verifiable.Presentation{})) + require.NoError(t, err) + require.NotEmpty(t, response) + require.Equal(t, model.AckStatusPENDING, response.Status) + }) + + t.Run("test present proof success - wait for done with redirect", func(t *testing.T) { + thID := uuid.New().String() + mockPresentProofSvc := &mockpresentproof.MockPresentProofSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: presentproofSvc.StateNameDone, + Properties: &mockdidexchange.MockEventProperties{ + Properties: map[string]interface{}{ + webRedirectStatusKey: model.AckStatusOK, + webRedirectURLKey: exampleWebRedirect, + }, + }, + Msg: &mockMsg{thID: thID}, + } + + return nil + }, + } + mockctx.ServiceMap[presentproofSvc.Name] = mockPresentProofSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + response, err := didcomm.PresentProof(token, thID, FromPresentation(&verifiable.Presentation{}), + WaitForDone(1*time.Millisecond)) + require.NoError(t, err) + require.NotEmpty(t, response) + require.Equal(t, model.AckStatusOK, response.Status) + require.Equal(t, exampleWebRedirect, response.RedirectURL) + }) + + t.Run("test present proof success - wait for abandoned with redirect", func(t *testing.T) { + thID := uuid.New().String() + mockPresentProofSvc := &mockpresentproof.MockPresentProofSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: presentproofSvc.StateNameAbandoned, + Properties: &mockdidexchange.MockEventProperties{ + Properties: map[string]interface{}{ + webRedirectStatusKey: model.AckStatusFAIL, + webRedirectURLKey: exampleWebRedirect, + }, + }, + Msg: &mockMsg{thID: thID}, + } + + return nil + }, + } + mockctx.ServiceMap[presentproofSvc.Name] = mockPresentProofSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + response, err := didcomm.PresentProof(token, thID, FromPresentation(&verifiable.Presentation{}), + WaitForDone(1*time.Millisecond)) + require.NoError(t, err) + require.NotEmpty(t, response) + require.Equal(t, model.AckStatusFAIL, response.Status) + require.Equal(t, exampleWebRedirect, response.RedirectURL) + }) + + t.Run("test present proof success - wait for done no redirect", func(t *testing.T) { + thID := uuid.New().String() + mockPresentProofSvc := &mockpresentproof.MockPresentProofSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: presentproofSvc.StateNameDone, + Properties: &mockdidexchange.MockEventProperties{}, + Msg: &mockMsg{thID: thID}, + } + + return nil + }, + } + mockctx.ServiceMap[presentproofSvc.Name] = mockPresentProofSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + response, err := didcomm.PresentProof(token, thID, FromPresentation(&verifiable.Presentation{}), + WaitForDone(1*time.Millisecond)) + require.NoError(t, err) + require.NotEmpty(t, response) + require.Equal(t, model.AckStatusOK, response.Status) + require.Empty(t, response.RedirectURL) + }) + + t.Run("test present proof failure - wait for abandoned no redirect", func(t *testing.T) { + thID := uuid.New().String() + mockPresentProofSvc := &mockpresentproof.MockPresentProofSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: presentproofSvc.StateNameAbandoned, + Properties: &mockdidexchange.MockEventProperties{}, + Msg: &mockMsg{thID: thID}, + } + + return nil + }, + } + mockctx.ServiceMap[presentproofSvc.Name] = mockPresentProofSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + response, err := didcomm.PresentProof(token, thID, FromPresentation(&verifiable.Presentation{}), + WaitForDone(1*time.Millisecond)) + require.NoError(t, err) + require.NotEmpty(t, response) + require.Equal(t, model.AckStatusFAIL, response.Status) + require.Empty(t, response.RedirectURL) + }) + + t.Run("test present proof failure", func(t *testing.T) { + ppSvc := &mockpresentproof.MockPresentProofSvc{ + ActionContinueFunc: func(string, ...presentproofSvc.Opt) error { + return fmt.Errorf(sampleWalletErr) + }, + } + + mockctx.ServiceMap[presentproofSvc.Name] = ppSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + response, err := didcomm.PresentProof(token, uuid.New().String(), FromRawPresentation([]byte("{}"))) + require.Error(t, err) + require.Contains(t, err.Error(), sampleWalletErr) + require.Empty(t, response) + }) + + t.Run("test present proof failure - failed to register message event", func(t *testing.T) { + thID := uuid.New().String() + mockPresentProofSvc := &mockpresentproof.MockPresentProofSvc{ + RegisterMsgEventErr: errors.New(sampleWalletErr), + } + mockctx.ServiceMap[presentproofSvc.Name] = mockPresentProofSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + response, err := didcomm.PresentProof(token, thID, FromPresentation(&verifiable.Presentation{}), + WaitForDone(1*time.Millisecond)) + require.Error(t, err) + require.Contains(t, err.Error(), sampleWalletErr) + require.Empty(t, response) + }) + + t.Run("test present proof failure - wait for done timeout", func(t *testing.T) { + thID := uuid.New().String() + mockPresentProofSvc := &mockpresentproof.MockPresentProofSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PreState, + } + + ch <- service.StateMsg{ + Type: service.PostState, + } + + ch <- service.StateMsg{ + Type: service.PostState, + Msg: &mockMsg{thID: "invalid"}, + } + + ch <- service.StateMsg{ + Type: service.PostState, + StateID: "invalid", + Msg: &mockMsg{thID: thID, fail: errors.New(sampleWalletErr)}, + } + + ch <- service.StateMsg{ + Type: service.PostState, + StateID: "invalid", + Msg: &mockMsg{thID: thID}, + } + + return nil + }, + UnregisterMsgEventErr: errors.New(sampleWalletErr), + } + mockctx.ServiceMap[presentproofSvc.Name] = mockPresentProofSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + response, err := didcomm.PresentProof(token, thID, FromPresentation(&verifiable.Presentation{}), + WaitForDone(1*time.Millisecond)) + require.Error(t, err) + require.Contains(t, err.Error(), "time out waiting for credential interaction to get completed") + require.Empty(t, response) + }) +} + +func TestWallet_ProposeCredential(t *testing.T) { + sampleDIDCommUser := uuid.New().String() + mockctx := newDidCommMockProvider(t) + err := CreateProfile(sampleDIDCommUser, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + const ( + myDID = "did:mydid:123" + theirDID = "did:theirdid:123" + ) + + t.Run("test propose credential success", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + } + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + didexSvc := &mockdidexchange.MockDIDExchangeSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc + + thID := uuid.New().String() + + icSvc := &mockissuecredential.MockIssueCredentialSvc{ + ActionsFunc: func() ([]issuecredentialsvc.Action, error) { + return []issuecredentialsvc.Action{ + { + PIID: thID, + Msg: service.NewDIDCommMsgMap(&issuecredentialsvc.OfferCredentialV2{ + Comment: sampleMsgComment, + }), + MyDID: myDID, + TheirDID: theirDID, + }, + }, nil + }, + HandleFunc: func(service.DIDCommMsg) (string, error) { + return thID, nil + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc + + store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) + require.NoError(t, err) + + record := &connection.Record{ + ConnectionID: sampleConnID, + MyDID: myDID, + TheirDID: theirDID, + } + recordBytes, err := json.Marshal(record) + require.NoError(t, err) + require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + msg, err := didcomm.ProposeCredential(token, &GenericInvitation{}, + WithConnectOptions(WithConnectTimeout(1*time.Millisecond))) + require.NoError(t, err) + require.NotEmpty(t, msg) + + offer := &issuecredentialsvc.OfferCredentialV2{} + + err = msg.Decode(offer) + require.NoError(t, err) + require.NotEmpty(t, offer) + require.Equal(t, sampleMsgComment, offer.Comment) + }) + + t.Run("test propose credential failure - did connect failure", func(t *testing.T) { + didexSvc := &mockdidexchange.MockDIDExchangeSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + return fmt.Errorf(sampleWalletErr) + }, + } + mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + msg, err := didcomm.ProposeCredential(token, &GenericInvitation{}) + require.Error(t, err) + require.Contains(t, err.Error(), sampleWalletErr) + require.Contains(t, err.Error(), "failed to perform did connection") + require.Empty(t, msg) + }) + + t.Run("test propose credential failure - oobv2 accept error", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + expectErr := fmt.Errorf("expected error") + + oobv2Svc := mockoutofbandv2.NewMockOobService(ctrl) + oobv2Svc.EXPECT().AcceptInvitation(gomock.Any(), gomock.Any()).Return("", expectErr).AnyTimes() + + mockctx.ServiceMap[oobv2.Name] = oobv2Svc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + invitation := GenericInvitation{} + err = json.Unmarshal([]byte(`{ + "id": "abc123", + "type": "https://didcomm.org/out-of-band/2.0/invitation" + }`), &invitation) + require.NoError(t, err) + + _, err = didcomm.ProposeCredential(token, &invitation, + WithConnectOptions(WithConnectTimeout(1*time.Millisecond))) + require.Error(t, err) + require.ErrorIs(t, err, expectErr) + require.Contains(t, err.Error(), "failed to accept OOB v2 invitation") + }) + + t.Run("test propose credential failure - no connection found", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + } + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + didexSvc := &mockdidexchange.MockDIDExchangeSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + msg, err := didcomm.ProposeCredential(token, &GenericInvitation{}) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to lookup connection") + require.Empty(t, msg) + }) + + t.Run("test propose credential failure - failed to send", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + } + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + didexSvc := &mockdidexchange.MockDIDExchangeSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc + + icSvc := &mockissuecredential.MockIssueCredentialSvc{ + HandleFunc: func(service.DIDCommMsg) (string, error) { + return "", fmt.Errorf(sampleWalletErr) + }, + HandleOutboundFunc: func(service.DIDCommMsg, string, string) (string, error) { + return "", fmt.Errorf(sampleWalletErr) + }, + } + mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc + + store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) + require.NoError(t, err) + + record := &connection.Record{ + ConnectionID: sampleConnID, + MyDID: myDID, + TheirDID: theirDID, + } + recordBytes, err := json.Marshal(record) + require.NoError(t, err) + require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + msg, err := didcomm.ProposeCredential(token, &GenericInvitation{}) + require.Error(t, err) + require.Contains(t, err.Error(), sampleWalletErr) + require.Contains(t, err.Error(), "failed to propose credential from wallet") + require.Empty(t, msg) + }) + + t.Run("test propose credential failure - timeout waiting for offer credential msg", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + } + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + didexSvc := &mockdidexchange.MockDIDExchangeSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc + + icSvc := &mockissuecredential.MockIssueCredentialSvc{ + HandleFunc: func(service.DIDCommMsg) (string, error) { + return uuid.New().String(), nil + }, + } + mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc + + store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) + require.NoError(t, err) + + record := &connection.Record{ + ConnectionID: sampleConnID, + MyDID: myDID, + TheirDID: theirDID, + } + recordBytes, err := json.Marshal(record) + require.NoError(t, err) + require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + msg, err := didcomm.ProposeCredential(token, &GenericInvitation{}, WithInitiateTimeout(600*time.Millisecond)) + require.Error(t, err) + require.Contains(t, err.Error(), "timeout waiting for offer credential message") + require.Empty(t, msg) + }) + + t.Run("test propose presentation failure - action error", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + } + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + didexSvc := &mockdidexchange.MockDIDExchangeSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc + + icSvc := &mockissuecredential.MockIssueCredentialSvc{ + HandleFunc: func(service.DIDCommMsg) (string, error) { + return uuid.New().String(), nil + }, + ActionsFunc: func() ([]issuecredentialsvc.Action, error) { + return nil, fmt.Errorf(sampleWalletErr) + }, + } + mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc + + store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) + require.NoError(t, err) + + record := &connection.Record{ + ConnectionID: sampleConnID, + TheirDID: theirDID, + } + recordBytes, err := json.Marshal(record) + require.NoError(t, err) + require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + msg, err := didcomm.ProposeCredential(token, &GenericInvitation{}, WithInitiateTimeout(1*time.Millisecond), + WithFromDID("did:sample:from")) + require.Error(t, err) + require.Contains(t, err.Error(), "timeout waiting for offer credential message") + require.Empty(t, msg) + }) +} + +func TestWallet_RequestCredential(t *testing.T) { + sampleDIDCommUser := uuid.New().String() + mockctx := newDidCommMockProvider(t) + err := CreateProfile(sampleDIDCommUser, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + t.Run("test request credential success", func(t *testing.T) { + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + response, err := didcomm.RequestCredential(token, uuid.New().String(), FromPresentation(&verifiable.Presentation{})) + require.NoError(t, err) + require.NotEmpty(t, response) + require.Equal(t, model.AckStatusPENDING, response.Status) + }) + + t.Run("test request credential success - wait for done with redirect", func(t *testing.T) { + thID := uuid.New().String() + + icSvc := &mockissuecredential.MockIssueCredentialSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: stateNameDone, + Properties: &mockdidexchange.MockEventProperties{ + Properties: map[string]interface{}{ + webRedirectStatusKey: model.AckStatusOK, + webRedirectURLKey: exampleWebRedirect, + }, + }, + Msg: &mockMsg{thID: thID}, + } + + return nil + }, + } + mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + response, err := didcomm.RequestCredential(token, thID, FromPresentation(&verifiable.Presentation{}), + WaitForDone(0)) + require.NoError(t, err) + require.NotEmpty(t, response) + require.Equal(t, model.AckStatusOK, response.Status) + require.Equal(t, exampleWebRedirect, response.RedirectURL) + }) + + t.Run("test for request credential - wait for problem report with redirect", func(t *testing.T) { + thID := uuid.New().String() + + icSvc := &mockissuecredential.MockIssueCredentialSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: stateNameAbandoned, + Properties: &mockdidexchange.MockEventProperties{ + Properties: map[string]interface{}{ + webRedirectStatusKey: model.AckStatusFAIL, + webRedirectURLKey: exampleWebRedirect, + }, + }, + Msg: &mockMsg{thID: thID}, + } + + return nil + }, + } + mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + response, err := didcomm.RequestCredential(token, thID, FromPresentation(&verifiable.Presentation{}), + WaitForDone(1*time.Millisecond)) + require.NoError(t, err) + require.NotEmpty(t, response) + require.Equal(t, model.AckStatusFAIL, response.Status) + require.Equal(t, exampleWebRedirect, response.RedirectURL) + }) + + t.Run("test request credential success - wait for done no redirect", func(t *testing.T) { + thID := uuid.New().String() + + icSvc := &mockissuecredential.MockIssueCredentialSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: stateNameDone, + Properties: &mockdidexchange.MockEventProperties{}, + Msg: &mockMsg{thID: thID}, + } + + return nil + }, + } + mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + response, err := didcomm.RequestCredential(token, thID, FromPresentation(&verifiable.Presentation{}), + WaitForDone(10*time.Millisecond)) + require.NoError(t, err) + require.NotEmpty(t, response) + require.Equal(t, model.AckStatusOK, response.Status) + require.Empty(t, response.RedirectURL) + }) + + t.Run("test request credential failure - wait for problem report no redirect", func(t *testing.T) { + thID := uuid.New().String() + + icSvc := &mockissuecredential.MockIssueCredentialSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: stateNameAbandoned, + Properties: &mockdidexchange.MockEventProperties{}, + Msg: &mockMsg{thID: thID}, + } + + return nil + }, + } + mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + response, err := didcomm.RequestCredential(token, thID, FromPresentation(&verifiable.Presentation{}), + WaitForDone(1*time.Millisecond)) + require.NoError(t, err) + require.NotEmpty(t, response) + require.Equal(t, model.AckStatusFAIL, response.Status) + require.Empty(t, response.RedirectURL) + }) + + t.Run("test request credential failure", func(t *testing.T) { + icSvc := &mockissuecredential.MockIssueCredentialSvc{ + ActionContinueFunc: func(string, ...issuecredentialsvc.Opt) error { + return fmt.Errorf(sampleWalletErr) + }, + } + mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + response, err := didcomm.RequestCredential(token, uuid.New().String(), FromRawPresentation([]byte("{}"))) + require.Error(t, err) + require.Contains(t, err.Error(), sampleWalletErr) + require.Empty(t, response) + }) + + t.Run("test request credential failure - failed to register msg event", func(t *testing.T) { + thID := uuid.New().String() + icSvc := &mockissuecredential.MockIssueCredentialSvc{ + RegisterMsgEventErr: errors.New(sampleWalletErr), + } + mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + response, err := didcomm.RequestCredential(token, thID, FromPresentation(&verifiable.Presentation{}), + WaitForDone(1*time.Millisecond)) + require.Error(t, err) + require.Contains(t, err.Error(), sampleWalletErr) + require.Empty(t, response) + }) + + t.Run("test request credential success - wait for done timeout", func(t *testing.T) { + thID := uuid.New().String() + + icSvc := &mockissuecredential.MockIssueCredentialSvc{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PreState, + } + + ch <- service.StateMsg{ + Type: service.PostState, + } + + ch <- service.StateMsg{ + Type: service.PostState, + Msg: &mockMsg{thID: "invalid"}, + } + + ch <- service.StateMsg{ + Type: service.PostState, + StateID: "invalid", + Msg: &mockMsg{thID: thID, fail: errors.New(sampleWalletErr)}, + } + + ch <- service.StateMsg{ + Type: service.PostState, + StateID: "invalid", + Msg: &mockMsg{thID: thID}, + } + + return nil + }, + UnregisterMsgEventErr: errors.New(sampleWalletErr), + } + mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + didcomm, err := NewDidComm(wallet, mockctx) + require.NoError(t, err) + require.NotEmpty(t, didcomm) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + response, err := didcomm.RequestCredential(token, thID, FromPresentation(&verifiable.Presentation{}), + WaitForDone(700*time.Millisecond)) + require.Error(t, err) + require.Contains(t, err.Error(), "time out waiting for credential interaction to get completed") + require.Empty(t, response) + }) +} + +func newDidCommMockProvider(t *testing.T) *mockprovider.Provider { + t.Helper() + + loader, err := ldtestutil.DocumentLoader() + require.NoError(t, err) + + serviceMap := map[string]interface{}{ + presentproofSvc.Name: &mockpresentproof.MockPresentProofSvc{}, + outofbandSvc.Name: &mockoutofband.MockOobService{}, + didexchange.DIDExchange: &mockdidexchange.MockDIDExchangeSvc{}, + mediator.Coordination: &mockmediator.MockMediatorSvc{}, + issuecredentialsvc.Name: &mockissuecredential.MockIssueCredentialSvc{}, + oobv2.Name: &mockoutofbandv2.MockOobService{}, + } + + return &mockprovider.Provider{ + StorageProviderValue: mockstorage.NewMockStoreProvider(), + ProtocolStateStorageProviderValue: mockstorage.NewMockStoreProvider(), + DocumentLoaderValue: loader, + ServiceMap: serviceMap, + } +} + +// mockMsg containing custom parent thread ID. +type mockMsg struct { + *service.DIDCommMsgMap + thID string + fail error + msgType string +} + +func (m *mockMsg) ParentThreadID() string { + return m.thID +} + +func (m *mockMsg) ThreadID() (string, error) { + return m.thID, m.fail +} + +func (m *mockMsg) Type() string { + if m.msgType != "" { + return m.msgType + } + + if m.DIDCommMsgMap != nil { + return m.DIDCommMsgMap.Type() + } + + return "" +} diff --git a/pkg/wallet/wallet.go b/pkg/wallet/wallet.go index a50ec5dc4..2b5b8ddd3 100644 --- a/pkg/wallet/wallet.go +++ b/pkg/wallet/wallet.go @@ -7,29 +7,15 @@ SPDX-License-Identifier: Apache-2.0 package wallet import ( - "context" "encoding/base64" "encoding/json" "errors" "fmt" - "time" - "github.com/google/uuid" "github.com/piprate/json-gold/ld" - "github.com/hyperledger/aries-framework-go/pkg/client/didexchange" - "github.com/hyperledger/aries-framework-go/pkg/client/issuecredential" - "github.com/hyperledger/aries-framework-go/pkg/client/outofband" - "github.com/hyperledger/aries-framework-go/pkg/client/outofbandv2" - "github.com/hyperledger/aries-framework-go/pkg/client/presentproof" "github.com/hyperledger/aries-framework-go/pkg/common/log" "github.com/hyperledger/aries-framework-go/pkg/crypto" - "github.com/hyperledger/aries-framework-go/pkg/didcomm/common/model" - "github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service" - "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/decorator" - didexchangeSvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/didexchange" - issuecredentialsvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/issuecredential" - outofbandv2svc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/outofbandv2" "github.com/hyperledger/aries-framework-go/pkg/doc/cm" "github.com/hyperledger/aries-framework-go/pkg/doc/did" "github.com/hyperledger/aries-framework-go/pkg/doc/signature/jsonld" @@ -41,7 +27,6 @@ import ( "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr" "github.com/hyperledger/aries-framework-go/pkg/kms" - "github.com/hyperledger/aries-framework-go/pkg/store/connection" "github.com/hyperledger/aries-framework-go/spi/storage" ) @@ -57,25 +42,12 @@ const ( // miscellaneous constants. const ( - bbsContext = "https://w3id.org/security/bbs/v1" - emptyRawLength = 4 - msgEventBufferSize = 10 - ldJSONMimeType = "application/ld+json" - - // protocol states. - stateNameAbandoned = "abandoned" - stateNameAbandoning = "abandoning" - stateNameDone = "done" + bbsContext = "https://w3id.org/security/bbs/v1" + emptyRawLength = 4 // web redirect constants. webRedirectStatusKey = "status" webRedirectURLKey = "url" - - // timeout constants. - defaultDIDExchangeTimeOut = 120 * time.Second - defaultWaitForRequestPresentationTimeOut = 120 * time.Second - defaultWaitForPresentProofDone = 120 * time.Second - retryDelay = 500 * time.Millisecond ) // proof options. @@ -98,19 +70,6 @@ type provider interface { Crypto() crypto.Crypto JSONLDDocumentLoader() ld.DocumentLoader MediaTypeProfiles() []string - didCommProvider // to be used only if wallet needs to be participated in DIDComm. -} - -// didCommProvider to be used only if wallet needs to be participated in DIDComm operation. -// TODO: using wallet KMS instead of provider KMS. -// TODO: reconcile Protocol storage with wallet store. -type didCommProvider interface { - KMS() kms.KeyManager - ServiceEndpoint() string - ProtocolStateStorageProvider() storage.Provider - Service(id string) (interface{}, error) - KeyType() kms.KeyType - KeyAgreementType() kms.KeyType } type provable interface { @@ -139,24 +98,6 @@ type Wallet struct { // document loader for JSON-LD contexts jsonldDocumentLoader ld.DocumentLoader - - // present proof client - presentProofClient *presentproof.Client - - // issue credential client - issueCredentialClient *issuecredential.Client - - // out of band client - oobClient *outofband.Client - - // out of band v2 client - oobV2Client *outofbandv2.Client - - // did-exchange client - didexchangeClient *didexchange.Client - - // connection lookup - connectionLookup *connection.Lookup } // New returns new verifiable credential wallet for given user. @@ -174,50 +115,14 @@ func New(userID string, ctx provider) (*Wallet, error) { return nil, fmt.Errorf("failed to get VC wallet profile: %w", err) } - presentProofClient, err := presentproof.New(ctx) - if err != nil { - return nil, fmt.Errorf("failed to initialize present proof client: %w", err) - } - - issueCredentialClient, err := issuecredential.New(ctx) - if err != nil { - return nil, fmt.Errorf("failed to initialize issue credential client: %w", err) - } - - oobClient, err := outofband.New(ctx) - if err != nil { - return nil, fmt.Errorf("failed to initialize out-of-band client: %w", err) - } - - oobV2Client, err := outofbandv2.New(ctx) - if err != nil { - return nil, fmt.Errorf("failed to initialize out-of-band v2 client: %w", err) - } - - connectionLookup, err := connection.NewLookup(ctx) - if err != nil { - return nil, fmt.Errorf("failed to initialize connection lookup: %w", err) - } - - didexchangeClient, err := didexchange.New(ctx) - if err != nil { - return nil, fmt.Errorf("failed to initialize didexchange client: %w", err) - } - return &Wallet{ - userID: userID, - profile: profile, - storeProvider: ctx.StorageProvider(), - walletCrypto: ctx.Crypto(), - contents: newContentStore(ctx.StorageProvider(), ctx.JSONLDDocumentLoader(), profile), - vdr: ctx.VDRegistry(), - jsonldDocumentLoader: ctx.JSONLDDocumentLoader(), - presentProofClient: presentProofClient, - issueCredentialClient: issueCredentialClient, - oobClient: oobClient, - oobV2Client: oobV2Client, - didexchangeClient: didexchangeClient, - connectionLookup: connectionLookup, + userID: userID, + profile: profile, + storeProvider: ctx.StorageProvider(), + walletCrypto: ctx.Crypto(), + contents: newContentStore(ctx.StorageProvider(), ctx.JSONLDDocumentLoader(), profile), + vdr: ctx.VDRegistry(), + jsonldDocumentLoader: ctx.JSONLDDocumentLoader(), }, nil } @@ -646,337 +551,6 @@ func (c *Wallet) CreateKeyPair(authToken string, keyType kms.KeyType) (*KeyPair, }, nil } -// Connect accepts out-of-band invitations and performs DID exchange. -// -// Args: -// - authToken: authorization for performing create key pair operation. -// - invitation: out-of-band invitation. -// - options: connection options. -// -// Returns: -// - connection ID if DID exchange is successful. -// - error if operation false. -// -func (c *Wallet) Connect(authToken string, invitation *outofband.Invitation, options ...ConnectOptions) (string, error) { //nolint: lll - statusCh := make(chan service.StateMsg, msgEventBufferSize) - - err := c.didexchangeClient.RegisterMsgEvent(statusCh) - if err != nil { - return "", fmt.Errorf("failed to register msg event : %w", err) - } - - defer func() { - e := c.didexchangeClient.UnregisterMsgEvent(statusCh) - if e != nil { - logger.Warnf("Failed to unregister msg event for connect: %w", e) - } - }() - - opts := &connectOpts{} - for _, opt := range options { - opt(opts) - } - - connID, err := c.oobClient.AcceptInvitation(invitation, opts.Label, getOobMessageOptions(opts)...) - if err != nil { - return "", fmt.Errorf("failed to accept invitation : %w", err) - } - - if opts.timeout == 0 { - opts.timeout = defaultDIDExchangeTimeOut - } - - ctx, cancel := context.WithTimeout(context.Background(), opts.timeout) - defer cancel() - - err = waitForConnect(ctx, statusCh, connID) - if err != nil { - return "", fmt.Errorf("wallet connect failed : %w", err) - } - - return connID, nil -} - -// ProposePresentation accepts out-of-band invitation and sends message proposing presentation -// from wallet to relying party. -// https://w3c-ccg.github.io/universal-wallet-interop-spec/#proposepresentation -// -// Currently Supporting -// [0454-present-proof-v2](https://github.com/hyperledger/aries-rfcs/tree/master/features/0454-present-proof-v2) -// -// Args: -// - authToken: authorization for performing operation. -// - invitation: out-of-band invitation from relying party. -// - options: options for accepting invitation and send propose presentation message. -// -// Returns: -// - DIDCommMsgMap containing request presentation message if operation is successful. -// - error if operation fails. -// -func (c *Wallet) ProposePresentation(authToken string, invitation *GenericInvitation, options ...InitiateInteractionOption) (*service.DIDCommMsgMap, error) { //nolint: lll - opts := &initiateInteractionOpts{} - for _, opt := range options { - opt(opts) - } - - var ( - connID string - err error - ) - - switch invitation.Version() { - default: - fallthrough - case service.V1: - connID, err = c.Connect(authToken, (*outofband.Invitation)(invitation.AsV1()), opts.connectOpts...) - if err != nil { - return nil, fmt.Errorf("failed to perform did connection : %w", err) - } - case service.V2: - connOpts := &connectOpts{} - - for _, opt := range opts.connectOpts { - opt(connOpts) - } - - connID, err = c.oobV2Client.AcceptInvitation( - invitation.AsV2(), - outofbandv2svc.WithRouterConnections(connOpts.Connections), - ) - if err != nil { - return nil, fmt.Errorf("failed to accept OOB v2 invitation : %w", err) - } - } - - connRecord, err := c.connectionLookup.GetConnectionRecord(connID) - if err != nil { - return nil, fmt.Errorf("failed to lookup connection for propose presentation : %w", err) - } - - opts = prepareInteractionOpts(connRecord, opts) - - _, err = c.presentProofClient.SendProposePresentation(&presentproof.ProposePresentation{}, connRecord) - if err != nil { - return nil, fmt.Errorf("failed to propose presentation from wallet: %w", err) - } - - ctx, cancel := context.WithTimeout(context.Background(), opts.timeout) - defer cancel() - - return c.waitForRequestPresentation(ctx, connRecord) -} - -// PresentProof sends message present proof message from wallet to relying party. -// https://w3c-ccg.github.io/universal-wallet-interop-spec/#presentproof -// -// Currently Supporting -// [0454-present-proof-v2](https://github.com/hyperledger/aries-rfcs/tree/master/features/0454-present-proof-v2) -// -// Args: -// - authToken: authorization for performing operation. -// - thID: thread ID (action ID) of request presentation. -// - presentProofFrom: presentation to be sent. -// -// Returns: -// - Credential interaction status containing status, redirectURL. -// - error if operation fails. -// -func (c *Wallet) PresentProof(authToken, thID string, options ...ConcludeInteractionOptions) (*CredentialInteractionStatus, error) { //nolint: lll - opts := &concludeInteractionOpts{} - - for _, option := range options { - option(opts) - } - - var presentation interface{} - if opts.presentation != nil { - presentation = opts.presentation - } else { - presentation = opts.rawPresentation - } - - err := c.presentProofClient.AcceptRequestPresentation(thID, &presentproof.Presentation{ - Attachments: []decorator.GenericAttachment{{ - ID: uuid.New().String(), - Data: decorator.AttachmentData{ - JSON: presentation, - }, - }}, - }, nil) - if err != nil { - return nil, err - } - - // wait for ack or problem-report. - if opts.waitForDone { - statusCh := make(chan service.StateMsg, msgEventBufferSize) - - err = c.presentProofClient.RegisterMsgEvent(statusCh) - if err != nil { - return nil, fmt.Errorf("failed to register present proof msg event : %w", err) - } - - defer func() { - e := c.presentProofClient.UnregisterMsgEvent(statusCh) - if e != nil { - logger.Warnf("Failed to unregister msg event for present proof: %w", e) - } - }() - - ctx, cancel := context.WithTimeout(context.Background(), opts.timeout) - defer cancel() - - return waitCredInteractionCompletion(ctx, statusCh, thID) - } - - return &CredentialInteractionStatus{Status: model.AckStatusPENDING}, nil -} - -// ProposeCredential sends propose credential message from wallet to issuer. -// https://w3c-ccg.github.io/universal-wallet-interop-spec/#proposecredential -// -// Currently Supporting : 0453-issueCredentialV2 -// https://github.com/hyperledger/aries-rfcs/blob/main/features/0453-issue-credential-v2/README.md -// -// Args: -// - authToken: authorization for performing operation. -// - invitation: out-of-band invitation from issuer. -// - options: options for accepting invitation and send propose credential message. -// -// Returns: -// - DIDCommMsgMap containing offer credential message if operation is successful. -// - error if operation fails. -// -func (c *Wallet) ProposeCredential(authToken string, invitation *GenericInvitation, options ...InitiateInteractionOption) (*service.DIDCommMsgMap, error) { //nolint: lll - opts := &initiateInteractionOpts{} - for _, opt := range options { - opt(opts) - } - - var ( - connID string - err error - ) - - switch invitation.Version() { - default: - fallthrough - case service.V1: - connID, err = c.Connect(authToken, (*outofband.Invitation)(invitation.AsV1()), opts.connectOpts...) - if err != nil { - return nil, fmt.Errorf("failed to perform did connection : %w", err) - } - case service.V2: - connOpts := &connectOpts{} - - for _, opt := range opts.connectOpts { - opt(connOpts) - } - - connID, err = c.oobV2Client.AcceptInvitation( - invitation.AsV2(), - outofbandv2svc.WithRouterConnections(connOpts.Connections), - ) - if err != nil { - return nil, fmt.Errorf("failed to accept OOB v2 invitation : %w", err) - } - } - - connRecord, err := c.connectionLookup.GetConnectionRecord(connID) - if err != nil { - return nil, fmt.Errorf("failed to lookup connection for propose presentation : %w", err) - } - - opts = prepareInteractionOpts(connRecord, opts) - - _, err = c.issueCredentialClient.SendProposal( - &issuecredential.ProposeCredential{InvitationID: invitation.ID}, - connRecord, - ) - if err != nil { - return nil, fmt.Errorf("failed to propose credential from wallet: %w", err) - } - - ctx, cancel := context.WithTimeout(context.Background(), opts.timeout) - defer cancel() - - return c.waitForOfferCredential(ctx, connRecord) -} - -// RequestCredential sends request credential message from wallet to issuer and -// optionally waits for credential fulfillment. -// https://w3c-ccg.github.io/universal-wallet-interop-spec/#requestcredential -// -// Currently Supporting : 0453-issueCredentialV2 -// https://github.com/hyperledger/aries-rfcs/blob/main/features/0453-issue-credential-v2/README.md -// -// Args: -// - authToken: authorization for performing operation. -// - thID: thread ID (action ID) of offer credential message previously received. -// - concludeInteractionOptions: options to conclude interaction like presentation to be shared etc. -// -// Returns: -// - Credential interaction status containing status, redirectURL. -// - error if operation fails. -// -func (c *Wallet) RequestCredential(authToken, thID string, options ...ConcludeInteractionOptions) (*CredentialInteractionStatus, error) { //nolint: lll - opts := &concludeInteractionOpts{} - - for _, option := range options { - option(opts) - } - - var presentation interface{} - if opts.presentation != nil { - presentation = opts.presentation - } else { - presentation = opts.rawPresentation - } - - attachmentID := uuid.New().String() - - err := c.issueCredentialClient.AcceptOffer(thID, &issuecredential.RequestCredential{ - Type: issuecredentialsvc.RequestCredentialMsgTypeV2, - Formats: []issuecredentialsvc.Format{{ - AttachID: attachmentID, - Format: ldJSONMimeType, - }}, - Attachments: []decorator.GenericAttachment{{ - ID: attachmentID, - Data: decorator.AttachmentData{ - JSON: presentation, - }, - }}, - }) - if err != nil { - return nil, err - } - - // wait for credential fulfillment. - if opts.waitForDone { - statusCh := make(chan service.StateMsg, msgEventBufferSize) - - err = c.issueCredentialClient.RegisterMsgEvent(statusCh) - if err != nil { - return nil, fmt.Errorf("failed to register issue credential action event : %w", err) - } - - defer func() { - e := c.issueCredentialClient.UnregisterMsgEvent(statusCh) - if e != nil { - logger.Warnf("Failed to unregister action event for issue credential: %w", e) - } - }() - - ctx, cancel := context.WithTimeout(context.Background(), opts.timeout) - defer cancel() - - return waitCredInteractionCompletion(ctx, statusCh, thID) - } - - return &CredentialInteractionStatus{Status: model.AckStatusPENDING}, nil -} - // ResolveCredentialManifest resolves given credential manifest by credential fulfillment or credential. // Supports: https://identity.foundation/credential-manifest/ // @@ -1272,179 +846,6 @@ func (c *Wallet) validateVerificationMethod(didDoc *did.Doc, opts *ProofOptions, return fmt.Errorf("unable to find '%s' for given verification method", supportedRelationships[relationship]) } -// currently correlating response action by connection due to limitation in current present proof V1 implementation. -func (c *Wallet) waitForRequestPresentation(ctx context.Context, record *connection.Record) (*service.DIDCommMsgMap, error) { //nolint: lll - done := make(chan *service.DIDCommMsgMap) - - go func() { - for { - actions, err := c.presentProofClient.Actions() - if err != nil { - continue - } - - if len(actions) > 0 { - for _, action := range actions { - if action.MyDID == record.MyDID && action.TheirDID == record.TheirDID { - done <- &action.Msg - return - } - } - } - - select { - default: - time.Sleep(retryDelay) - case <-ctx.Done(): - return - } - } - }() - - select { - case msg := <-done: - return msg, nil - case <-ctx.Done(): - return nil, fmt.Errorf("timeout waiting for request presentation message") - } -} - -// currently correlating response action by connection due to limitation in current issue credential V1 implementation. -func (c *Wallet) waitForOfferCredential(ctx context.Context, record *connection.Record) (*service.DIDCommMsgMap, error) { //nolint: lll - done := make(chan *service.DIDCommMsgMap) - - go func() { - for { - actions, err := c.issueCredentialClient.Actions() - if err != nil { - continue - } - - if len(actions) > 0 { - for _, action := range actions { - if action.MyDID == record.MyDID && action.TheirDID == record.TheirDID { - done <- &action.Msg - return - } - } - } - - select { - default: - time.Sleep(retryDelay) - case <-ctx.Done(): - return - } - } - }() - - select { - case msg := <-done: - return msg, nil - case <-ctx.Done(): - return nil, fmt.Errorf("timeout waiting for offer credential message") - } -} - -func waitForConnect(ctx context.Context, didStateMsgs chan service.StateMsg, connID string) error { - done := make(chan struct{}) - - go func() { - for msg := range didStateMsgs { - if msg.Type != service.PostState || msg.StateID != didexchangeSvc.StateIDCompleted { - continue - } - - var event didexchangeSvc.Event - - switch p := msg.Properties.(type) { - case didexchangeSvc.Event: - event = p - default: - logger.Warnf("failed to cast didexchange event properties") - - continue - } - - if event.ConnectionID() == connID { - logger.Debugf( - "Received connection complete event for invitationID=%s connectionID=%s", - event.InvitationID(), event.ConnectionID()) - - close(done) - - break - } - } - }() - - select { - case <-done: - return nil - case <-ctx.Done(): - return fmt.Errorf("time out waiting for did exchange state 'completed'") - } -} - -// wait for credential interaction to be completed (done or abandoned protocol state). -func waitCredInteractionCompletion(ctx context.Context, didStateMsgs chan service.StateMsg, thID string) (*CredentialInteractionStatus, error) { // nolint:gocognit,gocyclo,lll - done := make(chan *CredentialInteractionStatus) - - go func() { - for msg := range didStateMsgs { - // match post state. - if msg.Type != service.PostState { - continue - } - - // invalid state msg. - if msg.Msg == nil { - continue - } - - msgThID, err := msg.Msg.ThreadID() - if err != nil { - continue - } - - // match parent thread ID. - if msg.Msg.ParentThreadID() != thID && msgThID != thID { - continue - } - - // match protocol state. - if msg.StateID != stateNameDone && msg.StateID != stateNameAbandoned && msg.StateID != stateNameAbandoning { - continue - } - - properties := msg.Properties.All() - - response := &CredentialInteractionStatus{} - response.RedirectURL, response.Status = getWebRedirectInfo(properties) - - // if redirect status missing, then use protocol state, done -> OK, abandoned -> FAIL. - if response.Status == "" { - if msg.StateID == stateNameAbandoned || msg.StateID == stateNameAbandoning { - response.Status = model.AckStatusFAIL - } else { - response.Status = model.AckStatusOK - } - } - - done <- response - - return - } - }() - - select { - case status := <-done: - return status, nil - case <-ctx.Done(): - return nil, fmt.Errorf("time out waiting for credential interaction to get completed") - } -} - // addContext adds context if not found in given data model. func addContext(v interface{}, ldcontext string) { if vc, ok := v.(*verifiable.Credential); ok { @@ -1472,30 +873,3 @@ func updateProfile(keyManager kms.KeyManager, profile *profile) error { return nil } - -func prepareInteractionOpts(connRecord *connection.Record, opts *initiateInteractionOpts) *initiateInteractionOpts { - if opts.from == "" { - opts.from = connRecord.TheirDID - } - - if opts.timeout == 0 { - opts.timeout = defaultWaitForRequestPresentationTimeOut - } - - return opts -} - -// getWebRedirectInfo reads web redirect info from properties. -func getWebRedirectInfo(properties map[string]interface{}) (string, string) { - var redirect, status string - - if redirectURL, ok := properties[webRedirectURLKey]; ok { - redirect = redirectURL.(string) //nolint: errcheck, forcetypeassert - } - - if redirectStatus, ok := properties[webRedirectStatusKey]; ok { - status = redirectStatus.(string) //nolint: errcheck, forcetypeassert - } - - return redirect, status -} diff --git a/pkg/wallet/wallet_test.go b/pkg/wallet/wallet_test.go index 4385b3e46..d2dfb368b 100644 --- a/pkg/wallet/wallet_test.go +++ b/pkg/wallet/wallet_test.go @@ -20,45 +20,28 @@ import ( "time" "github.com/btcsuite/btcutil/base58" - "github.com/golang/mock/gomock" "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/hyperledger/aries-framework-go/component/storage/edv" "github.com/hyperledger/aries-framework-go/internal/testdata" - "github.com/hyperledger/aries-framework-go/pkg/client/outofband" "github.com/hyperledger/aries-framework-go/pkg/crypto/primitive/bbs12381g2pub" "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto" - "github.com/hyperledger/aries-framework-go/pkg/didcomm/common/model" - "github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service" - "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/didexchange" - issuecredentialsvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/issuecredential" - "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/mediator" - outofbandSvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/outofband" - oobv2 "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/outofbandv2" - presentproofSvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/presentproof" "github.com/hyperledger/aries-framework-go/pkg/doc/did" "github.com/hyperledger/aries-framework-go/pkg/doc/presexch" "github.com/hyperledger/aries-framework-go/pkg/doc/util" "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" vdrapi "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr" - mockoutofbandv2 "github.com/hyperledger/aries-framework-go/pkg/internal/gomocks/client/outofbandv2" "github.com/hyperledger/aries-framework-go/pkg/internal/ldtestutil" "github.com/hyperledger/aries-framework-go/pkg/kms" "github.com/hyperledger/aries-framework-go/pkg/kms/webkms" cryptomock "github.com/hyperledger/aries-framework-go/pkg/mock/crypto" - mockdidexchange "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/didexchange" - mockissuecredential "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/issuecredential" - mockmediator "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/mediator" - mockoutofband "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/outofband" - mockpresentproof "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/presentproof" mockkms "github.com/hyperledger/aries-framework-go/pkg/mock/kms" mockprovider "github.com/hyperledger/aries-framework-go/pkg/mock/provider" "github.com/hyperledger/aries-framework-go/pkg/mock/secretlock" mockstorage "github.com/hyperledger/aries-framework-go/pkg/mock/storage" mockvdr "github.com/hyperledger/aries-framework-go/pkg/mock/vdr" "github.com/hyperledger/aries-framework-go/pkg/secretlock/local/masterlock/pbkdf2" - "github.com/hyperledger/aries-framework-go/pkg/store/connection" "github.com/hyperledger/aries-framework-go/pkg/vdr/key" "github.com/hyperledger/aries-framework-go/spi/storage" ) @@ -88,8 +71,6 @@ const ( sampleEDVVaultID = "sample-edv-vault-id" sampleEDVEncryptionKID = "sample-edv-encryption-kid" sampleEDVMacKID = "sample-edv-mac-kid" - exampleWebRedirect = "http://example.com/sample" - sampleMsgComment = "sample mock msg" ) func TestCreate(t *testing.T) { @@ -461,60 +442,6 @@ func TestNew(t *testing.T) { require.Empty(t, wallet) require.Contains(t, err.Error(), sampleWalletErr) }) - - t.Run("test get wallet failure - present proof client initialize error", func(t *testing.T) { - mockctx := newMockProvider(t) - delete(mockctx.ServiceMap, presentproofSvc.Name) - - err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) - require.NoError(t, err) - - wallet, err := New(sampleUserID, mockctx) - require.Error(t, err) - require.Empty(t, wallet) - require.Contains(t, err.Error(), "failed to initialize present proof client") - }) - - t.Run("test get wallet failure - oob client initialize error", func(t *testing.T) { - mockctx := newMockProvider(t) - delete(mockctx.ServiceMap, outofbandSvc.Name) - - err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) - require.NoError(t, err) - - wallet, err := New(sampleUserID, mockctx) - require.Error(t, err) - require.Empty(t, wallet) - require.Contains(t, err.Error(), "failed to initialize out-of-band client") - }) - - t.Run("test get wallet failure - oob client initialize error", func(t *testing.T) { - mockctx := newMockProvider(t) - delete(mockctx.ServiceMap, didexchange.DIDExchange) - - err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) - require.NoError(t, err) - - wallet, err := New(sampleUserID, mockctx) - require.Error(t, err) - require.Empty(t, wallet) - require.Contains(t, err.Error(), "failed to initialize didexchange client") - }) - - t.Run("test get wallet failure - connection lookup initialize error", func(t *testing.T) { - mockctx := newMockProvider(t) - mockStoreProvider := mockstorage.NewMockStoreProvider() - mockStoreProvider.FailNamespace = "didexchange" - mockctx.StorageProviderValue = mockStoreProvider - - err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) - require.NoError(t, err) - - wallet, err := New(sampleUserID, mockctx) - require.Error(t, err) - require.Empty(t, wallet) - require.Contains(t, err.Error(), "failed to initialize connection lookup") - }) } func TestWallet_OpenClose(t *testing.T) { @@ -2501,1745 +2428,169 @@ func TestWallet_CreateKeyPair(t *testing.T) { }) } -func TestWallet_Connect(t *testing.T) { - sampleDIDCommUser := uuid.New().String() +func TestWallet_ResolveCredentialManifest(t *testing.T) { mockctx := newMockProvider(t) - err := CreateProfile(sampleDIDCommUser, mockctx, WithPassphrase(samplePassPhrase)) - require.NoError(t, err) - - t.Run("test did connect success", func(t *testing.T) { - sampleConnID := uuid.New().String() + user := uuid.New().String() - oobSvc := &mockoutofband.MockOobService{ - AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { - return sampleConnID, nil - }, - } - mockctx.ServiceMap[outofbandSvc.Name] = oobSvc - - didexSvc := &mockdidexchange.MockDIDExchangeSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PostState, - StateID: didexchange.StateIDCompleted, - Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, - } + // create a wallet + err := CreateProfile(user, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) - return nil - }, - } - mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc + wallet, err := New(user, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) + // get token + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase), WithUnlockExpiry(500*time.Millisecond)) + require.NoError(t, err) + require.NotEmpty(t, token) - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) + fulfillmentVP, err := verifiable.ParsePresentation(testdata.CredentialFulfillmentWithMultipleVCs, + verifiable.WithPresDisabledProofCheck(), + verifiable.WithPresJSONLDDocumentLoader(mockctx.JSONLDDocumentLoader())) + require.NoError(t, err) - defer wallet.Close() + vc, err := verifiable.ParseCredential(testdata.SampleUDCVC, + verifiable.WithJSONLDDocumentLoader(mockctx.JSONLDDocumentLoader())) + require.NoError(t, err) - connectionID, err := wallet.Connect(token, &outofband.Invitation{}) - require.NoError(t, err) - require.Equal(t, sampleConnID, connectionID) - }) + require.NoError(t, wallet.Add(token, Credential, testdata.SampleUDCVC)) - t.Run("test did connect failure - accept invitation failure", func(t *testing.T) { - oobSvc := &mockoutofband.MockOobService{ - AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { - return "", fmt.Errorf(sampleWalletErr) + t.Run("Test Resolving credential manifests", func(t *testing.T) { + testTable := map[string]struct { + manifest []byte + resolve ResolveManifestOption + resultCount int + error string + }{ + "testing resolve by raw credential fulfillment": { + manifest: testdata.CredentialManifestMultipleVCs, + resolve: ResolveRawFulfillment(testdata.CredentialFulfillmentWithMultipleVCs), + resultCount: 2, }, - } - - mockctx.ServiceMap[outofbandSvc.Name] = oobSvc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - connectionID, err := wallet.Connect(token, &outofband.Invitation{}, WithConnectTimeout(1*time.Millisecond)) - require.Error(t, err) - require.Contains(t, err.Error(), sampleWalletErr) - require.Contains(t, err.Error(), "failed to accept invitation") - require.Empty(t, connectionID) - }) - - t.Run("test did connect failure - register event failure", func(t *testing.T) { - didexSvc := &mockdidexchange.MockDIDExchangeSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - return fmt.Errorf(sampleWalletErr) + "testing resolve by credential fulfillment": { + manifest: testdata.CredentialManifestMultipleVCs, + resolve: ResolveFulfillment(fulfillmentVP), + resultCount: 2, }, - } - mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - connectionID, err := wallet.Connect(token, &outofband.Invitation{}, WithConnectTimeout(1*time.Millisecond)) - require.Error(t, err) - require.Contains(t, err.Error(), sampleWalletErr) - require.Contains(t, err.Error(), "failed to register msg event") - require.Empty(t, connectionID) - }) - - t.Run("test did connect failure - state not completed", func(t *testing.T) { - mockctx.ServiceMap[outofbandSvc.Name] = &mockoutofband.MockOobService{} - mockctx.ServiceMap[didexchange.DIDExchange] = &mockdidexchange.MockDIDExchangeSvc{} - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - connectionID, err := wallet.Connect(token, &outofband.Invitation{}, WithConnectTimeout(1*time.Millisecond)) - require.Error(t, err) - require.Contains(t, err.Error(), "time out waiting for did exchange state 'completed'") - require.Empty(t, connectionID) - }) - - t.Run("test did connect success - with warnings", func(t *testing.T) { - sampleConnID := uuid.New().String() - - oobSvc := &mockoutofband.MockOobService{ - AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { - return sampleConnID, nil + "testing resolve by raw credential": { + manifest: testdata.CredentialManifestMultipleVCs, + resolve: ResolveRawCredential("udc_output", testdata.SampleUDCVC), + resultCount: 1, }, - } - mockctx.ServiceMap[outofbandSvc.Name] = oobSvc - - didexSvc := &mockdidexchange.MockDIDExchangeSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PreState, - StateID: didexchange.StateIDCompleted, - } - - ch <- service.StateMsg{ - Type: service.PostState, - StateID: didexchange.StateIDCompleted, - } - - ch <- service.StateMsg{ - Type: service.PostState, - StateID: didexchange.StateIDCompleted, - Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, - } - - return nil + "testing resolve by credential": { + manifest: testdata.CredentialManifestMultipleVCs, + resolve: ResolveCredential("udc_output", vc), + resultCount: 1, + }, + "testing resolve by credential ID": { + manifest: testdata.CredentialManifestMultipleVCs, + resolve: ResolveCredentialID("udc_output", vc.ID), + resultCount: 1, + }, + "testing failure - resolve by empty resolve option": { + manifest: testdata.CredentialManifestMultipleVCs, + resolve: ResolveCredential("udc_output", nil), + resultCount: 0, + error: "invalid option", + }, + "testing failure - resolve by invalid raw fulfillment": { + manifest: testdata.CredentialManifestMultipleVCs, + resolve: ResolveRawFulfillment([]byte("{}")), + resultCount: 0, + error: "verifiable presentation is not valid", }, - UnregisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - return fmt.Errorf(sampleWalletErr) + "testing failure - resolve by invalid raw credential": { + manifest: testdata.CredentialManifestMultipleVCs, + resolve: ResolveRawCredential("", []byte("{}")), + resultCount: 0, + error: "credential type of unknown structure", + }, + "testing failure - invalid credential manifest": { + manifest: []byte("{}"), + resolve: ResolveFulfillment(fulfillmentVP), + resultCount: 0, + error: "invalid credential manifest", + }, + "testing failure - resolve raw credential by invalid descriptor ID": { + manifest: testdata.CredentialManifestMultipleVCs, + resolve: ResolveRawCredential("invalid", testdata.SampleUDCVC), + resultCount: 0, + error: "unable to find matching descriptor", + }, + "testing failure - resolve credential by invalid descriptor ID": { + manifest: testdata.CredentialManifestMultipleVCs, + resolve: ResolveCredential("invalid", vc), + resultCount: 0, + error: "unable to find matching descriptor", + }, + "testing failure - resolve credential by invalid credential ID": { + manifest: testdata.CredentialManifestMultipleVCs, + resolve: ResolveCredentialID("udc_output", "incorrect"), + resultCount: 0, + error: "failed to get credential to resolve from wallet", }, } - mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) + t.Parallel() - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) + for testName, testData := range testTable { + t.Run(testName, func(t *testing.T) { + resolved, err := wallet.ResolveCredentialManifest(token, testData.manifest, testData.resolve) - defer wallet.Close() + if testData.error != "" { + require.Error(t, err) + require.Contains(t, err.Error(), testData.error) + require.Len(t, resolved, testData.resultCount) - connectionID, err := wallet.Connect(token, &outofband.Invitation{}) - require.NoError(t, err) - require.Equal(t, sampleConnID, connectionID) - }) + return + } - t.Run("test oob connect options", func(t *testing.T) { - options := []ConnectOptions{ - WithConnectTimeout(10 * time.Second), - WithRouterConnections("sample-conn"), - WithMyLabel("sample-label"), - WithReuseAnyConnection(true), - WithReuseDID("sample-did"), - } + require.NoError(t, err) + require.NotEmpty(t, resolved) + require.Len(t, resolved, testData.resultCount) - opts := &connectOpts{} - for _, opt := range options { - opt(opts) + for _, result := range resolved { + require.NotEmpty(t, result.DescriptorID) + require.NotEmpty(t, result.Title) + require.NotEmpty(t, result.Properties) + } + }) } - - require.Equal(t, opts.timeout, 10*time.Second) - require.Equal(t, opts.Connections[0], "sample-conn") - require.Equal(t, opts.MyLabel(), "sample-label") - require.Equal(t, opts.ReuseDID, "sample-did") - require.True(t, opts.ReuseAny) - - require.Len(t, getOobMessageOptions(opts), 3) }) } -func TestWallet_ProposePresentation(t *testing.T) { - sampleDIDCommUser := uuid.New().String() - mockctx := newMockProvider(t) - err := CreateProfile(sampleDIDCommUser, mockctx, WithPassphrase(samplePassPhrase)) - require.NoError(t, err) - - const ( - myDID = "did:mydid:123" - theirDID = "did:theirdid:123" - ) - - t.Run("test propose presentation success - didcomm v1", func(t *testing.T) { - sampleConnID := uuid.New().String() +func newMockProvider(t *testing.T) *mockprovider.Provider { + t.Helper() - oobSvc := &mockoutofband.MockOobService{ - AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { - return sampleConnID, nil - }, - } - mockctx.ServiceMap[outofbandSvc.Name] = oobSvc - - didexSvc := &mockdidexchange.MockDIDExchangeSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PostState, - StateID: didexchange.StateIDCompleted, - Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, - } + loader, err := ldtestutil.DocumentLoader() + require.NoError(t, err) - return nil - }, - } - mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc - - thID := uuid.New().String() - - ppSvc := &mockpresentproof.MockPresentProofSvc{ - ActionsFunc: func() ([]presentproofSvc.Action, error) { - return []presentproofSvc.Action{ - { - PIID: thID, - Msg: service.NewDIDCommMsgMap(&presentproofSvc.RequestPresentationV2{ - Comment: "mock msg", - }), - MyDID: myDID, - TheirDID: theirDID, - }, - }, nil - }, - HandleFunc: func(service.DIDCommMsg) (string, error) { - return thID, nil - }, - } + return &mockprovider.Provider{ + StorageProviderValue: mockstorage.NewMockStoreProvider(), + ProtocolStateStorageProviderValue: mockstorage.NewMockStoreProvider(), + DocumentLoaderValue: loader, + } +} - mockctx.ServiceMap[outofbandSvc.Name] = oobSvc - mockctx.ServiceMap[presentproofSvc.Name] = ppSvc +func createSampleProfile(t *testing.T, mockctx *mockprovider.Provider) { + err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) - store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) - require.NoError(t, err) + wallet, err := New(sampleUserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + require.NotEmpty(t, wallet.profile.MasterLockCipher) +} - record := &connection.Record{ - ConnectionID: sampleConnID, - MyDID: myDID, - TheirDID: theirDID, - } - recordBytes, err := json.Marshal(record) +// adds credentials to wallet and returns handle for cleanup. +func addCredentialsToWallet(t *testing.T, walletInstance *Wallet, auth string, vcs ...*verifiable.Credential) func() { + for _, vc := range vcs { + vcBytes, err := vc.MarshalJSON() require.NoError(t, err) - require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - invitation := GenericInvitation{} - err = json.Unmarshal([]byte(`{ - "@id": "abc123", - "@type": "https://didcomm.org/out-of-band/1.0/invitation" - }`), &invitation) - require.NoError(t, err) - - msg, err := wallet.ProposePresentation(token, &invitation, - WithConnectOptions(WithConnectTimeout(1*time.Millisecond))) - require.NoError(t, err) - require.NotEmpty(t, msg) - - // empty invitation defaults to DIDComm v1 - msg, err = wallet.ProposePresentation(token, &GenericInvitation{}, - WithConnectOptions(WithConnectTimeout(1*time.Millisecond))) - require.NoError(t, err) - require.NotEmpty(t, msg) - - // invitation with unknown version defaults to DIDComm v1 - msg, err = wallet.ProposePresentation(token, &GenericInvitation{version: "unknown"}, - WithConnectOptions(WithConnectTimeout(1*time.Millisecond))) - require.NoError(t, err) - require.NotEmpty(t, msg) - }) - - t.Run("test propose presentation success - didcomm v2", func(t *testing.T) { - sampleConnID := uuid.New().String() - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - oobv2Svc := mockoutofbandv2.NewMockOobService(ctrl) - oobv2Svc.EXPECT().AcceptInvitation(gomock.Any(), gomock.Any()).Return(sampleConnID, nil).AnyTimes() - - mockctx.ServiceMap[oobv2.Name] = oobv2Svc - - thID := uuid.New().String() - - ppSvc := &mockpresentproof.MockPresentProofSvc{ - ActionsFunc: func() ([]presentproofSvc.Action, error) { - return []presentproofSvc.Action{ - { - PIID: thID, - Msg: service.NewDIDCommMsgMap(&presentproofSvc.RequestPresentationV3{ - Body: presentproofSvc.RequestPresentationV3Body{ - Comment: "mock msg", - }, - }), - MyDID: myDID, - TheirDID: theirDID, - }, - }, nil - }, - HandleFunc: func(service.DIDCommMsg) (string, error) { - return thID, nil - }, - } - - mockctx.ServiceMap[presentproofSvc.Name] = ppSvc - - connRec, err := connection.NewRecorder(mockctx) - require.NoError(t, err) - - record := &connection.Record{ - ConnectionID: sampleConnID, - MyDID: myDID, - TheirDID: theirDID, - DIDCommVersion: service.V2, - State: connection.StateNameCompleted, - } - - err = connRec.SaveConnectionRecord(record) - require.NoError(t, err) - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - invitation := GenericInvitation{} - err = json.Unmarshal([]byte(`{ - "id": "abc123", - "type": "https://didcomm.org/out-of-band/2.0/invitation" - }`), &invitation) - require.NoError(t, err) - - msg, err := wallet.ProposePresentation(token, &invitation, - WithConnectOptions(WithConnectTimeout(1*time.Millisecond))) - require.NoError(t, err) - require.NotEmpty(t, msg) - }) - - t.Run("test propose presentation failure - did connect failure", func(t *testing.T) { - didexSvc := &mockdidexchange.MockDIDExchangeSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - return fmt.Errorf(sampleWalletErr) - }, - } - mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - msg, err := wallet.ProposePresentation(token, &GenericInvitation{}) - require.Error(t, err) - require.Contains(t, err.Error(), sampleWalletErr) - require.Contains(t, err.Error(), "failed to perform did connection") - require.Empty(t, msg) - }) - - t.Run("test propose presentation failure - no connection found", func(t *testing.T) { - sampleConnID := uuid.New().String() - - oobSvc := &mockoutofband.MockOobService{ - AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { - return sampleConnID, nil - }, - } - mockctx.ServiceMap[outofbandSvc.Name] = oobSvc - - didexSvc := &mockdidexchange.MockDIDExchangeSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PostState, - StateID: didexchange.StateIDCompleted, - Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, - } - - return nil - }, - } - mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - msg, err := wallet.ProposePresentation(token, &GenericInvitation{}) - require.Error(t, err) - require.Contains(t, err.Error(), "failed to lookup connection") - require.Empty(t, msg) - }) - - t.Run("test propose presentation failure - failed to send", func(t *testing.T) { - sampleConnID := uuid.New().String() - - oobSvc := &mockoutofband.MockOobService{ - AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { - return sampleConnID, nil - }, - } - mockctx.ServiceMap[outofbandSvc.Name] = oobSvc - - didexSvc := &mockdidexchange.MockDIDExchangeSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PostState, - StateID: didexchange.StateIDCompleted, - Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, - } - - return nil - }, - } - mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc - - ppSvc := &mockpresentproof.MockPresentProofSvc{ - HandleFunc: func(service.DIDCommMsg) (string, error) { - return "", fmt.Errorf(sampleWalletErr) - }, - HandleOutboundFunc: func(service.DIDCommMsg, string, string) (string, error) { - return "", fmt.Errorf(sampleWalletErr) - }, - } - mockctx.ServiceMap[presentproofSvc.Name] = ppSvc - - store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) - require.NoError(t, err) - - record := &connection.Record{ - ConnectionID: sampleConnID, - MyDID: myDID, - TheirDID: theirDID, - } - recordBytes, err := json.Marshal(record) - require.NoError(t, err) - require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - msg, err := wallet.ProposePresentation(token, &GenericInvitation{}) - require.Error(t, err) - require.Contains(t, err.Error(), sampleWalletErr) - require.Contains(t, err.Error(), "failed to propose presentation from wallet") - require.Empty(t, msg) - }) - - t.Run("test propose presentation failure - timeout waiting for presentation request", func(t *testing.T) { - sampleConnID := uuid.New().String() - - oobSvc := &mockoutofband.MockOobService{ - AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { - return sampleConnID, nil - }, - } - mockctx.ServiceMap[outofbandSvc.Name] = oobSvc - - didexSvc := &mockdidexchange.MockDIDExchangeSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PostState, - StateID: didexchange.StateIDCompleted, - Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, - } - - return nil - }, - } - mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc - - ppSvc := &mockpresentproof.MockPresentProofSvc{ - HandleFunc: func(service.DIDCommMsg) (string, error) { - return uuid.New().String(), nil - }, - } - mockctx.ServiceMap[presentproofSvc.Name] = ppSvc - - store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) - require.NoError(t, err) - - record := &connection.Record{ - ConnectionID: sampleConnID, - MyDID: myDID, - TheirDID: theirDID, - } - recordBytes, err := json.Marshal(record) - require.NoError(t, err) - require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - msg, err := wallet.ProposePresentation(token, &GenericInvitation{}, WithInitiateTimeout(600*time.Millisecond)) - require.Error(t, err) - require.Contains(t, err.Error(), "timeout waiting for request presentation message") - require.Empty(t, msg) - }) - - t.Run("test propose presentation failure - action error", func(t *testing.T) { - sampleConnID := uuid.New().String() - - oobSvc := &mockoutofband.MockOobService{ - AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { - return sampleConnID, nil - }, - } - mockctx.ServiceMap[outofbandSvc.Name] = oobSvc - - didexSvc := &mockdidexchange.MockDIDExchangeSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PostState, - StateID: didexchange.StateIDCompleted, - Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, - } - - return nil - }, - } - mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc - - ppSvc := &mockpresentproof.MockPresentProofSvc{ - HandleFunc: func(service.DIDCommMsg) (string, error) { - return uuid.New().String(), nil - }, - ActionsFunc: func() ([]presentproofSvc.Action, error) { - return nil, fmt.Errorf(sampleWalletErr) - }, - } - mockctx.ServiceMap[presentproofSvc.Name] = ppSvc - - store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) - require.NoError(t, err) - - record := &connection.Record{ - ConnectionID: sampleConnID, - TheirDID: theirDID, - } - recordBytes, err := json.Marshal(record) - require.NoError(t, err) - require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - msg, err := wallet.ProposePresentation(token, &GenericInvitation{}, WithInitiateTimeout(1*time.Millisecond), - WithFromDID("did:sample:from")) - require.Error(t, err) - require.Contains(t, err.Error(), "timeout waiting for request presentation message") - require.Empty(t, msg) - }) - - t.Run("test propose presentation failure - oob v2 accept error", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - expectErr := fmt.Errorf("expected error") - - oobv2Svc := mockoutofbandv2.NewMockOobService(ctrl) - oobv2Svc.EXPECT().AcceptInvitation(gomock.Any(), gomock.Any()).Return("", expectErr).AnyTimes() - - mockctx.ServiceMap[oobv2.Name] = oobv2Svc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - invitation := GenericInvitation{} - err = json.Unmarshal([]byte(`{ - "id": "abc123", - "type": "https://didcomm.org/out-of-band/2.0/invitation" - }`), &invitation) - require.NoError(t, err) - - _, err = wallet.ProposePresentation(token, &invitation, - WithConnectOptions(WithConnectTimeout(1*time.Millisecond))) - require.Error(t, err) - require.ErrorIs(t, err, expectErr) - require.Contains(t, err.Error(), "failed to accept OOB v2 invitation") - }) -} - -func TestWallet_PresentProof(t *testing.T) { - sampleDIDCommUser := uuid.New().String() - mockctx := newMockProvider(t) - err := CreateProfile(sampleDIDCommUser, mockctx, WithPassphrase(samplePassPhrase)) - require.NoError(t, err) - - t.Run("test present proof success", func(t *testing.T) { - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - response, err := wallet.PresentProof(token, uuid.New().String(), FromPresentation(&verifiable.Presentation{})) - require.NoError(t, err) - require.NotEmpty(t, response) - require.Equal(t, model.AckStatusPENDING, response.Status) - }) - - t.Run("test present proof success - wait for done with redirect", func(t *testing.T) { - thID := uuid.New().String() - mockPresentProofSvc := &mockpresentproof.MockPresentProofSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PostState, - StateID: presentproofSvc.StateNameDone, - Properties: &mockdidexchange.MockEventProperties{ - Properties: map[string]interface{}{ - webRedirectStatusKey: model.AckStatusOK, - webRedirectURLKey: exampleWebRedirect, - }, - }, - Msg: &mockMsg{thID: thID}, - } - - return nil - }, - } - mockctx.ServiceMap[presentproofSvc.Name] = mockPresentProofSvc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - response, err := wallet.PresentProof(token, thID, FromPresentation(&verifiable.Presentation{}), - WaitForDone(1*time.Millisecond)) - require.NoError(t, err) - require.NotEmpty(t, response) - require.Equal(t, model.AckStatusOK, response.Status) - require.Equal(t, exampleWebRedirect, response.RedirectURL) - }) - - t.Run("test present proof success - wait for abandoned with redirect", func(t *testing.T) { - thID := uuid.New().String() - mockPresentProofSvc := &mockpresentproof.MockPresentProofSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PostState, - StateID: presentproofSvc.StateNameAbandoned, - Properties: &mockdidexchange.MockEventProperties{ - Properties: map[string]interface{}{ - webRedirectStatusKey: model.AckStatusFAIL, - webRedirectURLKey: exampleWebRedirect, - }, - }, - Msg: &mockMsg{thID: thID}, - } - - return nil - }, - } - mockctx.ServiceMap[presentproofSvc.Name] = mockPresentProofSvc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - response, err := wallet.PresentProof(token, thID, FromPresentation(&verifiable.Presentation{}), - WaitForDone(1*time.Millisecond)) - require.NoError(t, err) - require.NotEmpty(t, response) - require.Equal(t, model.AckStatusFAIL, response.Status) - require.Equal(t, exampleWebRedirect, response.RedirectURL) - }) - - t.Run("test present proof success - wait for done no redirect", func(t *testing.T) { - thID := uuid.New().String() - mockPresentProofSvc := &mockpresentproof.MockPresentProofSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PostState, - StateID: presentproofSvc.StateNameDone, - Properties: &mockdidexchange.MockEventProperties{}, - Msg: &mockMsg{thID: thID}, - } - - return nil - }, - } - mockctx.ServiceMap[presentproofSvc.Name] = mockPresentProofSvc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - response, err := wallet.PresentProof(token, thID, FromPresentation(&verifiable.Presentation{}), - WaitForDone(1*time.Millisecond)) - require.NoError(t, err) - require.NotEmpty(t, response) - require.Equal(t, model.AckStatusOK, response.Status) - require.Empty(t, response.RedirectURL) - }) - - t.Run("test present proof failure - wait for abandoned no redirect", func(t *testing.T) { - thID := uuid.New().String() - mockPresentProofSvc := &mockpresentproof.MockPresentProofSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PostState, - StateID: presentproofSvc.StateNameAbandoned, - Properties: &mockdidexchange.MockEventProperties{}, - Msg: &mockMsg{thID: thID}, - } - - return nil - }, - } - mockctx.ServiceMap[presentproofSvc.Name] = mockPresentProofSvc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - response, err := wallet.PresentProof(token, thID, FromPresentation(&verifiable.Presentation{}), - WaitForDone(1*time.Millisecond)) - require.NoError(t, err) - require.NotEmpty(t, response) - require.Equal(t, model.AckStatusFAIL, response.Status) - require.Empty(t, response.RedirectURL) - }) - - t.Run("test present proof failure", func(t *testing.T) { - ppSvc := &mockpresentproof.MockPresentProofSvc{ - ActionContinueFunc: func(string, ...presentproofSvc.Opt) error { - return fmt.Errorf(sampleWalletErr) - }, - } - - mockctx.ServiceMap[presentproofSvc.Name] = ppSvc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - response, err := wallet.PresentProof(token, uuid.New().String(), FromRawPresentation([]byte("{}"))) - require.Error(t, err) - require.Contains(t, err.Error(), sampleWalletErr) - require.Empty(t, response) - }) - - t.Run("test present proof failure - failed to register message event", func(t *testing.T) { - thID := uuid.New().String() - mockPresentProofSvc := &mockpresentproof.MockPresentProofSvc{ - RegisterMsgEventErr: errors.New(sampleWalletErr), - } - mockctx.ServiceMap[presentproofSvc.Name] = mockPresentProofSvc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - response, err := wallet.PresentProof(token, thID, FromPresentation(&verifiable.Presentation{}), - WaitForDone(1*time.Millisecond)) - require.Error(t, err) - require.Contains(t, err.Error(), sampleWalletErr) - require.Empty(t, response) - }) - - t.Run("test present proof failure - wait for done timeout", func(t *testing.T) { - thID := uuid.New().String() - mockPresentProofSvc := &mockpresentproof.MockPresentProofSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PreState, - } - - ch <- service.StateMsg{ - Type: service.PostState, - } - - ch <- service.StateMsg{ - Type: service.PostState, - Msg: &mockMsg{thID: "invalid"}, - } - - ch <- service.StateMsg{ - Type: service.PostState, - StateID: "invalid", - Msg: &mockMsg{thID: thID, fail: errors.New(sampleWalletErr)}, - } - - ch <- service.StateMsg{ - Type: service.PostState, - StateID: "invalid", - Msg: &mockMsg{thID: thID}, - } - - return nil - }, - UnregisterMsgEventErr: errors.New(sampleWalletErr), - } - mockctx.ServiceMap[presentproofSvc.Name] = mockPresentProofSvc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - response, err := wallet.PresentProof(token, thID, FromPresentation(&verifiable.Presentation{}), - WaitForDone(1*time.Millisecond)) - require.Error(t, err) - require.Contains(t, err.Error(), "time out waiting for credential interaction to get completed") - require.Empty(t, response) - }) -} - -func TestWallet_ProposeCredential(t *testing.T) { - sampleDIDCommUser := uuid.New().String() - mockctx := newMockProvider(t) - err := CreateProfile(sampleDIDCommUser, mockctx, WithPassphrase(samplePassPhrase)) - require.NoError(t, err) - - const ( - myDID = "did:mydid:123" - theirDID = "did:theirdid:123" - ) - - t.Run("test propose credential success", func(t *testing.T) { - sampleConnID := uuid.New().String() - - oobSvc := &mockoutofband.MockOobService{ - AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { - return sampleConnID, nil - }, - } - mockctx.ServiceMap[outofbandSvc.Name] = oobSvc - - didexSvc := &mockdidexchange.MockDIDExchangeSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PostState, - StateID: didexchange.StateIDCompleted, - Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, - } - - return nil - }, - } - mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc - - thID := uuid.New().String() - - icSvc := &mockissuecredential.MockIssueCredentialSvc{ - ActionsFunc: func() ([]issuecredentialsvc.Action, error) { - return []issuecredentialsvc.Action{ - { - PIID: thID, - Msg: service.NewDIDCommMsgMap(&issuecredentialsvc.OfferCredentialV2{ - Comment: sampleMsgComment, - }), - MyDID: myDID, - TheirDID: theirDID, - }, - }, nil - }, - HandleFunc: func(service.DIDCommMsg) (string, error) { - return thID, nil - }, - } - - mockctx.ServiceMap[outofbandSvc.Name] = oobSvc - mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc - - store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) - require.NoError(t, err) - - record := &connection.Record{ - ConnectionID: sampleConnID, - MyDID: myDID, - TheirDID: theirDID, - } - recordBytes, err := json.Marshal(record) - require.NoError(t, err) - require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - msg, err := wallet.ProposeCredential(token, &GenericInvitation{}, - WithConnectOptions(WithConnectTimeout(1*time.Millisecond))) - require.NoError(t, err) - require.NotEmpty(t, msg) - - offer := &issuecredentialsvc.OfferCredentialV2{} - - err = msg.Decode(offer) - require.NoError(t, err) - require.NotEmpty(t, offer) - require.Equal(t, sampleMsgComment, offer.Comment) - }) - - t.Run("test propose credential failure - did connect failure", func(t *testing.T) { - didexSvc := &mockdidexchange.MockDIDExchangeSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - return fmt.Errorf(sampleWalletErr) - }, - } - mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - msg, err := wallet.ProposeCredential(token, &GenericInvitation{}) - require.Error(t, err) - require.Contains(t, err.Error(), sampleWalletErr) - require.Contains(t, err.Error(), "failed to perform did connection") - require.Empty(t, msg) - }) - - t.Run("test propose credential failure - oobv2 accept error", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - expectErr := fmt.Errorf("expected error") - - oobv2Svc := mockoutofbandv2.NewMockOobService(ctrl) - oobv2Svc.EXPECT().AcceptInvitation(gomock.Any(), gomock.Any()).Return("", expectErr).AnyTimes() - - mockctx.ServiceMap[oobv2.Name] = oobv2Svc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - invitation := GenericInvitation{} - err = json.Unmarshal([]byte(`{ - "id": "abc123", - "type": "https://didcomm.org/out-of-band/2.0/invitation" - }`), &invitation) - require.NoError(t, err) - - _, err = wallet.ProposeCredential(token, &invitation, - WithConnectOptions(WithConnectTimeout(1*time.Millisecond))) - require.Error(t, err) - require.ErrorIs(t, err, expectErr) - require.Contains(t, err.Error(), "failed to accept OOB v2 invitation") - }) - - t.Run("test propose credential failure - no connection found", func(t *testing.T) { - sampleConnID := uuid.New().String() - - oobSvc := &mockoutofband.MockOobService{ - AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { - return sampleConnID, nil - }, - } - mockctx.ServiceMap[outofbandSvc.Name] = oobSvc - - didexSvc := &mockdidexchange.MockDIDExchangeSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PostState, - StateID: didexchange.StateIDCompleted, - Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, - } - - return nil - }, - } - mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - msg, err := wallet.ProposeCredential(token, &GenericInvitation{}) - require.Error(t, err) - require.Contains(t, err.Error(), "failed to lookup connection") - require.Empty(t, msg) - }) - - t.Run("test propose credential failure - failed to send", func(t *testing.T) { - sampleConnID := uuid.New().String() - - oobSvc := &mockoutofband.MockOobService{ - AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { - return sampleConnID, nil - }, - } - mockctx.ServiceMap[outofbandSvc.Name] = oobSvc - - didexSvc := &mockdidexchange.MockDIDExchangeSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PostState, - StateID: didexchange.StateIDCompleted, - Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, - } - - return nil - }, - } - mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc - - icSvc := &mockissuecredential.MockIssueCredentialSvc{ - HandleFunc: func(service.DIDCommMsg) (string, error) { - return "", fmt.Errorf(sampleWalletErr) - }, - HandleOutboundFunc: func(service.DIDCommMsg, string, string) (string, error) { - return "", fmt.Errorf(sampleWalletErr) - }, - } - mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc - - store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) - require.NoError(t, err) - - record := &connection.Record{ - ConnectionID: sampleConnID, - MyDID: myDID, - TheirDID: theirDID, - } - recordBytes, err := json.Marshal(record) - require.NoError(t, err) - require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - msg, err := wallet.ProposeCredential(token, &GenericInvitation{}) - require.Error(t, err) - require.Contains(t, err.Error(), sampleWalletErr) - require.Contains(t, err.Error(), "failed to propose credential from wallet") - require.Empty(t, msg) - }) - - t.Run("test propose credential failure - timeout waiting for offer credential msg", func(t *testing.T) { - sampleConnID := uuid.New().String() - - oobSvc := &mockoutofband.MockOobService{ - AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { - return sampleConnID, nil - }, - } - mockctx.ServiceMap[outofbandSvc.Name] = oobSvc - - didexSvc := &mockdidexchange.MockDIDExchangeSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PostState, - StateID: didexchange.StateIDCompleted, - Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, - } - - return nil - }, - } - mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc - - icSvc := &mockissuecredential.MockIssueCredentialSvc{ - HandleFunc: func(service.DIDCommMsg) (string, error) { - return uuid.New().String(), nil - }, - } - mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc - - store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) - require.NoError(t, err) - - record := &connection.Record{ - ConnectionID: sampleConnID, - MyDID: myDID, - TheirDID: theirDID, - } - recordBytes, err := json.Marshal(record) - require.NoError(t, err) - require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - msg, err := wallet.ProposeCredential(token, &GenericInvitation{}, WithInitiateTimeout(600*time.Millisecond)) - require.Error(t, err) - require.Contains(t, err.Error(), "timeout waiting for offer credential message") - require.Empty(t, msg) - }) - - t.Run("test propose presentation failure - action error", func(t *testing.T) { - sampleConnID := uuid.New().String() - - oobSvc := &mockoutofband.MockOobService{ - AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { - return sampleConnID, nil - }, - } - mockctx.ServiceMap[outofbandSvc.Name] = oobSvc - - didexSvc := &mockdidexchange.MockDIDExchangeSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PostState, - StateID: didexchange.StateIDCompleted, - Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, - } - - return nil - }, - } - mockctx.ServiceMap[didexchange.DIDExchange] = didexSvc - - icSvc := &mockissuecredential.MockIssueCredentialSvc{ - HandleFunc: func(service.DIDCommMsg) (string, error) { - return uuid.New().String(), nil - }, - ActionsFunc: func() ([]issuecredentialsvc.Action, error) { - return nil, fmt.Errorf(sampleWalletErr) - }, - } - mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc - - store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) - require.NoError(t, err) - - record := &connection.Record{ - ConnectionID: sampleConnID, - TheirDID: theirDID, - } - recordBytes, err := json.Marshal(record) - require.NoError(t, err) - require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - msg, err := wallet.ProposeCredential(token, &GenericInvitation{}, WithInitiateTimeout(1*time.Millisecond), - WithFromDID("did:sample:from")) - require.Error(t, err) - require.Contains(t, err.Error(), "timeout waiting for offer credential message") - require.Empty(t, msg) - }) -} - -func TestWallet_RequestCredential(t *testing.T) { - sampleDIDCommUser := uuid.New().String() - mockctx := newMockProvider(t) - err := CreateProfile(sampleDIDCommUser, mockctx, WithPassphrase(samplePassPhrase)) - require.NoError(t, err) - - t.Run("test request credential success", func(t *testing.T) { - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - response, err := wallet.RequestCredential(token, uuid.New().String(), FromPresentation(&verifiable.Presentation{})) - require.NoError(t, err) - require.NotEmpty(t, response) - require.Equal(t, model.AckStatusPENDING, response.Status) - }) - - t.Run("test request credential success - wait for done with redirect", func(t *testing.T) { - thID := uuid.New().String() - - icSvc := &mockissuecredential.MockIssueCredentialSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PostState, - StateID: stateNameDone, - Properties: &mockdidexchange.MockEventProperties{ - Properties: map[string]interface{}{ - webRedirectStatusKey: model.AckStatusOK, - webRedirectURLKey: exampleWebRedirect, - }, - }, - Msg: &mockMsg{thID: thID}, - } - - return nil - }, - } - mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - response, err := wallet.RequestCredential(token, thID, FromPresentation(&verifiable.Presentation{}), - WaitForDone(0)) - require.NoError(t, err) - require.NotEmpty(t, response) - require.Equal(t, model.AckStatusOK, response.Status) - require.Equal(t, exampleWebRedirect, response.RedirectURL) - }) - - t.Run("test for request credential - wait for problem report with redirect", func(t *testing.T) { - thID := uuid.New().String() - - icSvc := &mockissuecredential.MockIssueCredentialSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PostState, - StateID: stateNameAbandoned, - Properties: &mockdidexchange.MockEventProperties{ - Properties: map[string]interface{}{ - webRedirectStatusKey: model.AckStatusFAIL, - webRedirectURLKey: exampleWebRedirect, - }, - }, - Msg: &mockMsg{thID: thID}, - } - - return nil - }, - } - mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - response, err := wallet.RequestCredential(token, thID, FromPresentation(&verifiable.Presentation{}), - WaitForDone(1*time.Millisecond)) - require.NoError(t, err) - require.NotEmpty(t, response) - require.Equal(t, model.AckStatusFAIL, response.Status) - require.Equal(t, exampleWebRedirect, response.RedirectURL) - }) - - t.Run("test request credential success - wait for done no redirect", func(t *testing.T) { - thID := uuid.New().String() - - icSvc := &mockissuecredential.MockIssueCredentialSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PostState, - StateID: stateNameDone, - Properties: &mockdidexchange.MockEventProperties{}, - Msg: &mockMsg{thID: thID}, - } - - return nil - }, - } - mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - response, err := wallet.RequestCredential(token, thID, FromPresentation(&verifiable.Presentation{}), - WaitForDone(10*time.Millisecond)) - require.NoError(t, err) - require.NotEmpty(t, response) - require.Equal(t, model.AckStatusOK, response.Status) - require.Empty(t, response.RedirectURL) - }) - - t.Run("test request credential failure - wait for problem report no redirect", func(t *testing.T) { - thID := uuid.New().String() - - icSvc := &mockissuecredential.MockIssueCredentialSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PostState, - StateID: stateNameAbandoned, - Properties: &mockdidexchange.MockEventProperties{}, - Msg: &mockMsg{thID: thID}, - } - - return nil - }, - } - mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - response, err := wallet.RequestCredential(token, thID, FromPresentation(&verifiable.Presentation{}), - WaitForDone(1*time.Millisecond)) - require.NoError(t, err) - require.NotEmpty(t, response) - require.Equal(t, model.AckStatusFAIL, response.Status) - require.Empty(t, response.RedirectURL) - }) - - t.Run("test request credential failure", func(t *testing.T) { - icSvc := &mockissuecredential.MockIssueCredentialSvc{ - ActionContinueFunc: func(string, ...issuecredentialsvc.Opt) error { - return fmt.Errorf(sampleWalletErr) - }, - } - mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - response, err := wallet.RequestCredential(token, uuid.New().String(), FromRawPresentation([]byte("{}"))) - require.Error(t, err) - require.Contains(t, err.Error(), sampleWalletErr) - require.Empty(t, response) - }) - - t.Run("test request credential failure - failed to register msg event", func(t *testing.T) { - thID := uuid.New().String() - icSvc := &mockissuecredential.MockIssueCredentialSvc{ - RegisterMsgEventErr: errors.New(sampleWalletErr), - } - mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - response, err := wallet.RequestCredential(token, thID, FromPresentation(&verifiable.Presentation{}), - WaitForDone(1*time.Millisecond)) - require.Error(t, err) - require.Contains(t, err.Error(), sampleWalletErr) - require.Empty(t, response) - }) - - t.Run("test request credential success - wait for done timeout", func(t *testing.T) { - thID := uuid.New().String() - - icSvc := &mockissuecredential.MockIssueCredentialSvc{ - RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { - ch <- service.StateMsg{ - Type: service.PreState, - } - - ch <- service.StateMsg{ - Type: service.PostState, - } - - ch <- service.StateMsg{ - Type: service.PostState, - Msg: &mockMsg{thID: "invalid"}, - } - - ch <- service.StateMsg{ - Type: service.PostState, - StateID: "invalid", - Msg: &mockMsg{thID: thID, fail: errors.New(sampleWalletErr)}, - } - - ch <- service.StateMsg{ - Type: service.PostState, - StateID: "invalid", - Msg: &mockMsg{thID: thID}, - } - - return nil - }, - UnregisterMsgEventErr: errors.New(sampleWalletErr), - } - mockctx.ServiceMap[issuecredentialsvc.Name] = icSvc - - wallet, err := New(sampleDIDCommUser, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) - require.NoError(t, err) - require.NotEmpty(t, token) - - defer wallet.Close() - - response, err := wallet.RequestCredential(token, thID, FromPresentation(&verifiable.Presentation{}), - WaitForDone(700*time.Millisecond)) - require.Error(t, err) - require.Contains(t, err.Error(), "time out waiting for credential interaction to get completed") - require.Empty(t, response) - }) -} - -func TestWallet_ResolveCredentialManifest(t *testing.T) { - mockctx := newMockProvider(t) - user := uuid.New().String() - - // create a wallet - err := CreateProfile(user, mockctx, WithPassphrase(samplePassPhrase)) - require.NoError(t, err) - - wallet, err := New(user, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - - // get token - token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase), WithUnlockExpiry(500*time.Millisecond)) - require.NoError(t, err) - require.NotEmpty(t, token) - - fulfillmentVP, err := verifiable.ParsePresentation(testdata.CredentialFulfillmentWithMultipleVCs, - verifiable.WithPresDisabledProofCheck(), - verifiable.WithPresJSONLDDocumentLoader(mockctx.JSONLDDocumentLoader())) - require.NoError(t, err) - - vc, err := verifiable.ParseCredential(testdata.SampleUDCVC, - verifiable.WithJSONLDDocumentLoader(mockctx.JSONLDDocumentLoader())) - require.NoError(t, err) - - require.NoError(t, wallet.Add(token, Credential, testdata.SampleUDCVC)) - - t.Run("Test Resolving credential manifests", func(t *testing.T) { - testTable := map[string]struct { - manifest []byte - resolve ResolveManifestOption - resultCount int - error string - }{ - "testing resolve by raw credential fulfillment": { - manifest: testdata.CredentialManifestMultipleVCs, - resolve: ResolveRawFulfillment(testdata.CredentialFulfillmentWithMultipleVCs), - resultCount: 2, - }, - "testing resolve by credential fulfillment": { - manifest: testdata.CredentialManifestMultipleVCs, - resolve: ResolveFulfillment(fulfillmentVP), - resultCount: 2, - }, - "testing resolve by raw credential": { - manifest: testdata.CredentialManifestMultipleVCs, - resolve: ResolveRawCredential("udc_output", testdata.SampleUDCVC), - resultCount: 1, - }, - "testing resolve by credential": { - manifest: testdata.CredentialManifestMultipleVCs, - resolve: ResolveCredential("udc_output", vc), - resultCount: 1, - }, - "testing resolve by credential ID": { - manifest: testdata.CredentialManifestMultipleVCs, - resolve: ResolveCredentialID("udc_output", vc.ID), - resultCount: 1, - }, - "testing failure - resolve by empty resolve option": { - manifest: testdata.CredentialManifestMultipleVCs, - resolve: ResolveCredential("udc_output", nil), - resultCount: 0, - error: "invalid option", - }, - "testing failure - resolve by invalid raw fulfillment": { - manifest: testdata.CredentialManifestMultipleVCs, - resolve: ResolveRawFulfillment([]byte("{}")), - resultCount: 0, - error: "verifiable presentation is not valid", - }, - "testing failure - resolve by invalid raw credential": { - manifest: testdata.CredentialManifestMultipleVCs, - resolve: ResolveRawCredential("", []byte("{}")), - resultCount: 0, - error: "credential type of unknown structure", - }, - "testing failure - invalid credential manifest": { - manifest: []byte("{}"), - resolve: ResolveFulfillment(fulfillmentVP), - resultCount: 0, - error: "invalid credential manifest", - }, - "testing failure - resolve raw credential by invalid descriptor ID": { - manifest: testdata.CredentialManifestMultipleVCs, - resolve: ResolveRawCredential("invalid", testdata.SampleUDCVC), - resultCount: 0, - error: "unable to find matching descriptor", - }, - "testing failure - resolve credential by invalid descriptor ID": { - manifest: testdata.CredentialManifestMultipleVCs, - resolve: ResolveCredential("invalid", vc), - resultCount: 0, - error: "unable to find matching descriptor", - }, - "testing failure - resolve credential by invalid credential ID": { - manifest: testdata.CredentialManifestMultipleVCs, - resolve: ResolveCredentialID("udc_output", "incorrect"), - resultCount: 0, - error: "failed to get credential to resolve from wallet", - }, - } - - t.Parallel() - - for testName, testData := range testTable { - t.Run(testName, func(t *testing.T) { - resolved, err := wallet.ResolveCredentialManifest(token, testData.manifest, testData.resolve) - - if testData.error != "" { - require.Error(t, err) - require.Contains(t, err.Error(), testData.error) - require.Len(t, resolved, testData.resultCount) - - return - } - - require.NoError(t, err) - require.NotEmpty(t, resolved) - require.Len(t, resolved, testData.resultCount) - - for _, result := range resolved { - require.NotEmpty(t, result.DescriptorID) - require.NotEmpty(t, result.Title) - require.NotEmpty(t, result.Properties) - } - }) - } - }) -} - -func newMockProvider(t *testing.T) *mockprovider.Provider { - t.Helper() - - loader, err := ldtestutil.DocumentLoader() - require.NoError(t, err) - - serviceMap := map[string]interface{}{ - presentproofSvc.Name: &mockpresentproof.MockPresentProofSvc{}, - outofbandSvc.Name: &mockoutofband.MockOobService{}, - didexchange.DIDExchange: &mockdidexchange.MockDIDExchangeSvc{}, - mediator.Coordination: &mockmediator.MockMediatorSvc{}, - issuecredentialsvc.Name: &mockissuecredential.MockIssueCredentialSvc{}, - oobv2.Name: &mockoutofbandv2.MockOobService{}, - } - - return &mockprovider.Provider{ - StorageProviderValue: mockstorage.NewMockStoreProvider(), - ProtocolStateStorageProviderValue: mockstorage.NewMockStoreProvider(), - DocumentLoaderValue: loader, - ServiceMap: serviceMap, - } -} - -func createSampleProfile(t *testing.T, mockctx *mockprovider.Provider) { - err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) - require.NoError(t, err) - - wallet, err := New(sampleUserID, mockctx) - require.NoError(t, err) - require.NotEmpty(t, wallet) - require.NotEmpty(t, wallet.profile.MasterLockCipher) -} - -// adds credentials to wallet and returns handle for cleanup. -func addCredentialsToWallet(t *testing.T, walletInstance *Wallet, auth string, vcs ...*verifiable.Credential) func() { - for _, vc := range vcs { - vcBytes, err := vc.MarshalJSON() - require.NoError(t, err) - require.NoError(t, walletInstance.Remove(auth, Credential, vc.ID)) - require.NoError(t, walletInstance.Add(auth, Credential, vcBytes)) - } + require.NoError(t, walletInstance.Remove(auth, Credential, vc.ID)) + require.NoError(t, walletInstance.Add(auth, Credential, vcBytes)) + } return func() { for _, vc := range vcs { @@ -4250,31 +2601,3 @@ func addCredentialsToWallet(t *testing.T, walletInstance *Wallet, auth string, v } } } - -// mockMsg containing custom parent thread ID. -type mockMsg struct { - *service.DIDCommMsgMap - thID string - fail error - msgType string -} - -func (m *mockMsg) ParentThreadID() string { - return m.thID -} - -func (m *mockMsg) ThreadID() (string, error) { - return m.thID, m.fail -} - -func (m *mockMsg) Type() string { - if m.msgType != "" { - return m.msgType - } - - if m.DIDCommMsgMap != nil { - return m.DIDCommMsgMap.Type() - } - - return "" -}