From d2df24f05af4989918a20e54d57cdc9c8f8a0531 Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Tue, 8 Oct 2024 11:51:36 +0200 Subject: [PATCH] stop refreshing when a subject has no active DID Documents (#3461) --- cmd/root.go | 2 +- discovery/client.go | 24 ++- discovery/client_test.go | 377 +++++++++++++++++---------------------- discovery/module.go | 7 +- discovery/module_test.go | 20 ++- 5 files changed, 208 insertions(+), 222 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index ff1a64421..708876377 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -198,7 +198,7 @@ func CreateSystem(shutdownCallback context.CancelFunc) *core.System { vdrInstance := vdr.NewVDR(cryptoInstance, networkInstance, didStore, eventManager, storageInstance) credentialInstance := vcr.NewVCRInstance(cryptoInstance, vdrInstance, networkInstance, jsonld, eventManager, storageInstance, pkiInstance) didmanInstance := didman.NewDidmanInstance(vdrInstance, credentialInstance, jsonld) - discoveryInstance := discovery.New(storageInstance, credentialInstance, vdrInstance) + discoveryInstance := discovery.New(storageInstance, credentialInstance, vdrInstance, vdrInstance) authInstance := auth.NewAuthInstance(auth.DefaultConfig(), vdrInstance, vdrInstance, credentialInstance, cryptoInstance, didmanInstance, jsonld, pkiInstance) statusEngine := status.NewStatusEngine(system) metricsEngine := core.NewMetricsEngine() diff --git a/discovery/client.go b/discovery/client.go index 6b4a7b4d1..11a4665ce 100644 --- a/discovery/client.go +++ b/discovery/client.go @@ -34,6 +34,7 @@ import ( "github.com/nuts-foundation/nuts-node/vcr/pe" "github.com/nuts-foundation/nuts-node/vcr/signature/proof" "github.com/nuts-foundation/nuts-node/vdr/didsubject" + "github.com/nuts-foundation/nuts-node/vdr/resolver" "slices" "strings" "time" @@ -56,15 +57,17 @@ type defaultClientRegistrationManager struct { client client.HTTPClient vcr vcr.VCR subjectManager didsubject.Manager + didResolver resolver.DIDResolver } -func newRegistrationManager(services map[string]ServiceDefinition, store *sqlStore, client client.HTTPClient, vcr vcr.VCR, subjectManager didsubject.Manager) *defaultClientRegistrationManager { +func newRegistrationManager(services map[string]ServiceDefinition, store *sqlStore, client client.HTTPClient, vcr vcr.VCR, subjectManager didsubject.Manager, didResolver resolver.DIDResolver) *defaultClientRegistrationManager { return &defaultClientRegistrationManager{ services: services, store: store, client: client, vcr: vcr, subjectManager: subjectManager, + didResolver: didResolver, } } @@ -87,9 +90,26 @@ func (r *defaultClientRegistrationManager) activate(ctx context.Context, service } } subjectDIDs = subjectDIDs[:j] + + if len(subjectDIDs) == 0 { + return fmt.Errorf("%w: %w for %s", ErrPresentationRegistrationFailed, ErrDIDMethodsNotSupported, subjectID) + } } + + // and filter by deactivated status + j := 0 + for i, did := range subjectDIDs { + _, _, err := r.didResolver.Resolve(did, nil) + // any temporary error, like db errors should not cause a deregister action, only ErrDeactivated + if err == nil || !errors.Is(err, resolver.ErrDeactivated) { + subjectDIDs[j] = subjectDIDs[i] + j++ + } + } + subjectDIDs = subjectDIDs[:j] + if len(subjectDIDs) == 0 { - return fmt.Errorf("%w: %w for %s", ErrPresentationRegistrationFailed, ErrDIDMethodsNotSupported, subjectID) + return fmt.Errorf("%w: %w for %s", ErrPresentationRegistrationFailed, didsubject.ErrSubjectNotFound, subjectID) } log.Logger().Debugf("Registering Verifiable Presentation on Discovery Service (service=%s, subject=%s)", service.ID, subjectID) diff --git a/discovery/client_test.go b/discovery/client_test.go index 76e147d1a..4098df696 100644 --- a/discovery/client_test.go +++ b/discovery/client_test.go @@ -32,6 +32,7 @@ import ( "github.com/nuts-foundation/nuts-node/vcr/holder" "github.com/nuts-foundation/nuts-node/vcr/pe" "github.com/nuts-foundation/nuts-node/vdr/didsubject" + "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -41,17 +42,53 @@ import ( var nextRefresh = time.Now().Add(-1 * time.Hour) +type testContext struct { + ctrl *gomock.Controller + didResolver *resolver.MockDIDResolver + invoker *client.MockHTTPClient + vcr *vcr.MockVCR + wallet *holder.MockWallet + subjectManager *didsubject.MockManager + store *sqlStore + manager *defaultClientRegistrationManager +} + +func newTestContext(t *testing.T) testContext { + t.Helper() + storageEngine := storage.NewTestStorageEngine(t) + require.NoError(t, storageEngine.Start()) + ctrl := gomock.NewController(t) + didResolver := resolver.NewMockDIDResolver(ctrl) + invoker := client.NewMockHTTPClient(ctrl) + vcr := vcr.NewMockVCR(ctrl) + wallet := holder.NewMockWallet(ctrl) + subjectManager := didsubject.NewMockManager(ctrl) + store := setupStore(t, storageEngine.GetSQLDatabase()) + manager := newRegistrationManager(testDefinitions(), store, invoker, vcr, subjectManager, didResolver) + vcr.EXPECT().Wallet().Return(wallet).AnyTimes() + + return testContext{ + ctrl: ctrl, + didResolver: didResolver, + invoker: invoker, + vcr: vcr, + wallet: wallet, + subjectManager: subjectManager, + store: store, + manager: manager, + } +} + func Test_defaultClientRegistrationManager_activate(t *testing.T) { storageEngine := storage.NewTestStorageEngine(t) require.NoError(t, storageEngine.Start()) t.Run("immediate registration", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - invoker.EXPECT().Register(gomock.Any(), "http://example.com/usecase", vpAlice) - wallet := holder.NewMockWallet(ctrl) - wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{vcAlice}, nil) - wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).DoAndReturn(func(_ interface{}, credentials []vc.VerifiableCredential, options holder.PresentationOptions, _ interface{}, _ interface{}) (*vc.VerifiablePresentation, error) { + ctx := newTestContext(t) + ctx.invoker.EXPECT().Register(gomock.Any(), "http://example.com/usecase", vpAlice) + ctx.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, nil) + ctx.wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{vcAlice}, nil) + ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).DoAndReturn(func(_ interface{}, credentials []vc.VerifiableCredential, options holder.PresentationOptions, _ interface{}, _ interface{}) (*vc.VerifiablePresentation, error) { // check if two credentials are given // check if the DiscoveryRegistrationCredential is added with an authServerURL assert.Len(t, credentials, 2) @@ -61,89 +98,65 @@ func Test_defaultClientRegistrationManager_activate(t *testing.T) { assert.Equal(t, aliceDID.String(), options.Holder.String()) return &vpAlice, nil }) - mockVCR := vcr.NewMockVCR(ctrl) - mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - err := manager.activate(audit.TestContext(), testServiceID, aliceSubject, defaultRegistrationParams(aliceSubject)) + err := ctx.manager.activate(audit.TestContext(), testServiceID, aliceSubject, defaultRegistrationParams(aliceSubject)) assert.NoError(t, err) }) t.Run("registration fails", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("invoker error")) - wallet := holder.NewMockWallet(ctrl) - wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{vcAlice}, nil) - wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil) - mockVCR := vcr.NewMockVCR(ctrl) - mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) + ctx := newTestContext(t) + ctx.invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("invoker error")) + ctx.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, nil) + ctx.wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{vcAlice}, nil) + ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - err := manager.activate(audit.TestContext(), testServiceID, aliceSubject, defaultRegistrationParams(aliceSubject)) + err := ctx.manager.activate(audit.TestContext(), testServiceID, aliceSubject, defaultRegistrationParams(aliceSubject)) require.ErrorIs(t, err, ErrPresentationRegistrationFailed) assert.ErrorContains(t, err, "invoker error") // check no refresh records are added - record, err := store.getPresentationRefreshRecord(testServiceID, aliceSubject) + record, err := ctx.store.getPresentationRefreshRecord(testServiceID, aliceSubject) require.NoError(t, err) assert.Nil(t, record) }) t.Run("DID method not supported", func(t *testing.T) { - ctrl := gomock.NewController(t) - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - manager := newRegistrationManager(testDefinitions(), nil, nil, nil, mockSubjectManager) + ctx := newTestContext(t) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - err := manager.activate(audit.TestContext(), unsupportedServiceID, aliceSubject, defaultRegistrationParams(aliceSubject)) + err := ctx.manager.activate(audit.TestContext(), unsupportedServiceID, aliceSubject, defaultRegistrationParams(aliceSubject)) assert.ErrorIs(t, err, ErrDIDMethodsNotSupported) }) t.Run("no matching credentials", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - wallet := holder.NewMockWallet(ctrl) - wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, nil) - mockVCR := vcr.NewMockVCR(ctrl) - mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) + ctx := newTestContext(t) + ctx.wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, nil) + ctx.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, nil) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - err := manager.activate(audit.TestContext(), testServiceID, aliceSubject, nil) + err := ctx.manager.activate(audit.TestContext(), testServiceID, aliceSubject, nil) require.ErrorIs(t, err, ErrPresentationRegistrationFailed) require.ErrorIs(t, err, pe.ErrNoCredentials) }) t.Run("subject with 2 DIDs, one registers and other fails", func(t *testing.T) { + ctx := newTestContext(t) subjectDIDs := []did.DID{aliceDID, bobDID} - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - invoker.EXPECT().Register(gomock.Any(), "http://example.com/usecase", vpAlice) - wallet := holder.NewMockWallet(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return(subjectDIDs, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) + ctx.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, nil) + ctx.didResolver.EXPECT().Resolve(bobDID, gomock.Any()).Return(nil, nil, nil) + ctx.invoker.EXPECT().Register(gomock.Any(), "http://example.com/usecase", vpAlice) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return(subjectDIDs, nil) // aliceDID registers - wallet.EXPECT().List(gomock.Any(), aliceDID).Return([]vc.VerifiableCredential{vcAlice}, nil) - wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), &aliceDID, false).Return(&vpAlice, nil) + ctx.wallet.EXPECT().List(gomock.Any(), aliceDID).Return([]vc.VerifiableCredential{vcAlice}, nil) + ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), &aliceDID, false).Return(&vpAlice, nil) // bobDID has no credentials, so builds no presentation - wallet.EXPECT().List(gomock.Any(), bobDID).Return(nil, nil) + ctx.wallet.EXPECT().List(gomock.Any(), bobDID).Return(nil, nil) - err := manager.activate(audit.TestContext(), testServiceID, aliceSubject, defaultRegistrationParams(aliceSubject)) + err := ctx.manager.activate(audit.TestContext(), testServiceID, aliceSubject, defaultRegistrationParams(aliceSubject)) assert.NoError(t, err) }) @@ -158,48 +171,34 @@ func Test_defaultClientRegistrationManager_activate(t *testing.T) { PresentationMaxValidity: int((24 * time.Hour).Seconds()), }, } - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - invoker.EXPECT().Register(gomock.Any(), "http://example.com/usecase", vpAlice) - mockVCR := vcr.NewMockVCR(ctrl) - wallet := holder.NewMockWallet(ctrl) - wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, nil) - wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).DoAndReturn(func(_ interface{}, credentials []vc.VerifiableCredential, _ interface{}, _ interface{}, _ interface{}) (*vc.VerifiablePresentation, error) { + ctx := newTestContext(t) + ctx.invoker.EXPECT().Register(gomock.Any(), "http://example.com/usecase", vpAlice) + ctx.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, nil) + ctx.wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, nil) + ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).DoAndReturn(func(_ interface{}, credentials []vc.VerifiableCredential, _ interface{}, _ interface{}, _ interface{}) (*vc.VerifiablePresentation, error) { // expect registration credential assert.Len(t, credentials, 1) return &vpAlice, nil }) - mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(emptyDefinition, store, invoker, mockVCR, mockSubjectManager) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + ctx.manager = newRegistrationManager(emptyDefinition, ctx.store, ctx.invoker, ctx.vcr, ctx.subjectManager, ctx.didResolver) - err := manager.activate(audit.TestContext(), testServiceID, aliceSubject, nil) + err := ctx.manager.activate(audit.TestContext(), testServiceID, aliceSubject, nil) assert.NoError(t, err) }) t.Run("unknown service", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, nil) + ctx := newTestContext(t) - err := manager.activate(audit.TestContext(), "unknown", aliceSubject, nil) + err := ctx.manager.activate(audit.TestContext(), "unknown", aliceSubject, nil) assert.EqualError(t, err, "discovery service not found") }) t.Run("unknown subject", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - store := setupStore(t, storageEngine.GetSQLDatabase()) - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{}, didsubject.ErrSubjectNotFound) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) + ctx := newTestContext(t) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{}, didsubject.ErrSubjectNotFound) - err := manager.activate(audit.TestContext(), testServiceID, aliceSubject, nil) + err := ctx.manager.activate(audit.TestContext(), testServiceID, aliceSubject, nil) assert.ErrorIs(t, err, didsubject.ErrSubjectNotFound) }) @@ -210,114 +209,74 @@ func Test_defaultClientRegistrationManager_deactivate(t *testing.T) { require.NoError(t, storageEngine.Start()) t.Run("not registered", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) + ctx := newTestContext(t) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - err := manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) + err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) assert.NoError(t, err) }) t.Run("registered", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()) - wallet := holder.NewMockWallet(ctrl) - wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil) - mockVCR := vcr.NewMockVCR(ctrl) - mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) - require.NoError(t, store.add(testServiceID, vpAlice, 1)) + ctx := newTestContext(t) + ctx.invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()) + ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + require.NoError(t, ctx.store.add(testServiceID, vpAlice, 1)) - err := manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) + err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) assert.NoError(t, err) }) t.Run("already deactivated", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - mockVCR.EXPECT().Wallet().Return(holder.NewMockWallet(ctrl)).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) + ctx := newTestContext(t) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) vpAliceDeactivated := createPresentationCustom(aliceDID, func(claims map[string]interface{}, vp *vc.VerifiablePresentation) { claims[jwt.AudienceKey] = []string{testServiceID} claims["retract_jti"] = vpAlice.ID.String() vp.Type = append(vp.Type, retractionPresentationType) }, vcAlice) - require.NoError(t, store.add(testServiceID, vpAliceDeactivated, 1)) + require.NoError(t, ctx.store.add(testServiceID, vpAliceDeactivated, 1)) - err := manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) + err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) assert.NoError(t, err) }) t.Run("DID method not supported", func(t *testing.T) { - ctrl := gomock.NewController(t) - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, nil, nil, mockSubjectManager) + ctx := newTestContext(t) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - err := manager.deactivate(audit.TestContext(), unsupportedServiceID, aliceSubject) + err := ctx.manager.deactivate(audit.TestContext(), unsupportedServiceID, aliceSubject) assert.ErrorIs(t, err, ErrDIDMethodsNotSupported) }) t.Run("deregistering from Discovery Service fails", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("remote error")) - wallet := holder.NewMockWallet(ctrl) - wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil) - mockVCR := vcr.NewMockVCR(ctrl) - mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) - require.NoError(t, store.add(testServiceID, vpAlice, 1)) + ctx := newTestContext(t) + ctx.invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("remote error")) + ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(&vpAlice, nil) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + require.NoError(t, ctx.store.add(testServiceID, vpAlice, 1)) - err := manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) + err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) require.ErrorIs(t, err, ErrPresentationRegistrationFailed) require.ErrorContains(t, err, "remote error") }) t.Run("building presentation fails", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - wallet := holder.NewMockWallet(ctrl) - wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(nil, assert.AnError) - mockVCR := vcr.NewMockVCR(ctrl) - mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) - require.NoError(t, store.add(testServiceID, vpAlice, 1)) + ctx := newTestContext(t) + ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), false).Return(nil, assert.AnError) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + require.NoError(t, ctx.store.add(testServiceID, vpAlice, 1)) - err := manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) + err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) assert.ErrorIs(t, err, assert.AnError) }) t.Run("unknown subject", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - store := setupStore(t, storageEngine.GetSQLDatabase()) - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{}, didsubject.ErrSubjectNotFound) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) + ctx := newTestContext(t) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{}, didsubject.ErrSubjectNotFound) - err := manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) + err := ctx.manager.deactivate(audit.TestContext(), testServiceID, aliceSubject) assert.ErrorIs(t, err, didsubject.ErrSubjectNotFound) }) @@ -328,109 +287,95 @@ func Test_defaultClientRegistrationManager_refresh(t *testing.T) { require.NoError(t, storageEngine.Start()) t.Run("no registrations", func(t *testing.T) { - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - mockSubjectManager := didsubject.NewMockManager(ctrl) - store := setupStore(t, storageEngine.GetSQLDatabase()) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) + ctx := newTestContext(t) - err := manager.refresh(audit.TestContext(), time.Now()) + err := ctx.manager.refresh(audit.TestContext(), time.Now()) require.NoError(t, err) }) t.Run("2 VPs to refresh, first one fails, second one succeeds", func(t *testing.T) { - store := setupStore(t, storageEngine.GetSQLDatabase()) - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) + ctx := newTestContext(t) gomock.InOrder( - invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()), - invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("remote error")), + ctx.invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()), + ctx.invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("remote error")), + ) + gomock.InOrder( + ctx.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, nil), + ctx.didResolver.EXPECT().Resolve(bobDID, gomock.Any()).Return(nil, nil, nil), ) - wallet := holder.NewMockWallet(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) // Alice - _ = store.updatePresentationRefreshTime(testServiceID, aliceSubject, defaultRegistrationParams(aliceSubject), &nextRefresh) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), &aliceDID, false).Return(&vpAlice, nil) - wallet.EXPECT().List(gomock.Any(), aliceDID).Return([]vc.VerifiableCredential{vcAlice}, nil) + _ = ctx.store.updatePresentationRefreshTime(testServiceID, aliceSubject, defaultRegistrationParams(aliceSubject), &nextRefresh) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), &aliceDID, false).Return(&vpAlice, nil) + ctx.wallet.EXPECT().List(gomock.Any(), aliceDID).Return([]vc.VerifiableCredential{vcAlice}, nil) // Bob - _ = store.updatePresentationRefreshTime(testServiceID, bobSubject, defaultRegistrationParams(aliceSubject), &nextRefresh) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), bobSubject).Return([]did.DID{bobDID}, nil) - wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), &bobDID, false).Return(&vpBob, nil) - wallet.EXPECT().List(gomock.Any(), bobDID).Return([]vc.VerifiableCredential{vcBob}, nil) + _ = ctx.store.updatePresentationRefreshTime(testServiceID, bobSubject, defaultRegistrationParams(aliceSubject), &nextRefresh) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), bobSubject).Return([]did.DID{bobDID}, nil) + ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), &bobDID, false).Return(&vpBob, nil) + ctx.wallet.EXPECT().List(gomock.Any(), bobDID).Return([]vc.VerifiableCredential{vcBob}, nil) - err := manager.refresh(audit.TestContext(), time.Now()) + err := ctx.manager.refresh(audit.TestContext(), time.Now()) errStr := "failed to refresh Verifiable Presentation (service=usecase_v1, subject=bob): registration of Verifiable Presentation on remote Discovery Service failed: did:example:bob: remote error" assert.EqualError(t, err, errStr) // check for presentationRefreshError - refreshError := getPresentationRefreshError(t, store.db, testServiceID, bobSubject) + refreshError := getPresentationRefreshError(t, ctx.store.db, testServiceID, bobSubject) assert.Contains(t, refreshError.Error, errStr) }) t.Run("deactivate unknown subject", func(t *testing.T) { - store := setupStore(t, storageEngine.GetSQLDatabase()) - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return(nil, didsubject.ErrSubjectNotFound) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) - _ = store.updatePresentationRefreshTime(testServiceID, aliceSubject, nil, &nextRefresh) + ctx := newTestContext(t) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return(nil, didsubject.ErrSubjectNotFound) + _ = ctx.store.updatePresentationRefreshTime(testServiceID, aliceSubject, nil, &nextRefresh) + + err := ctx.manager.refresh(audit.TestContext(), time.Now()) + + assert.EqualError(t, err, "removed unknown subject (service=usecase_v1, subject=alice)") + }) + t.Run("deactivate deactivated DID", func(t *testing.T) { + ctx := newTestContext(t) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + ctx.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, resolver.ErrDeactivated) + _ = ctx.store.updatePresentationRefreshTime(testServiceID, aliceSubject, nil, &nextRefresh) - err := manager.refresh(audit.TestContext(), time.Now()) + err := ctx.manager.refresh(audit.TestContext(), time.Now()) assert.EqualError(t, err, "removed unknown subject (service=usecase_v1, subject=alice)") }) t.Run("deactivate unsupported DID method", func(t *testing.T) { - store := setupStore(t, storageEngine.GetSQLDatabase()) - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - mockSubjectManager := didsubject.NewMockManager(ctrl) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) - _ = store.updatePresentationRefreshTime(unsupportedServiceID, aliceSubject, defaultRegistrationParams(aliceSubject), &nextRefresh) + ctx := newTestContext(t) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + _ = ctx.store.updatePresentationRefreshTime(unsupportedServiceID, aliceSubject, defaultRegistrationParams(aliceSubject), &nextRefresh) - err := manager.refresh(audit.TestContext(), time.Now()) + err := ctx.manager.refresh(audit.TestContext(), time.Now()) // refresh clears the registration require.NoError(t, err) - record, err := store.getPresentationRefreshRecord(unsupportedServiceID, aliceSubject) + record, err := ctx.store.getPresentationRefreshRecord(unsupportedServiceID, aliceSubject) assert.NoError(t, err) assert.Nil(t, record) }) t.Run("remove presentationRefreshError on success", func(t *testing.T) { - store := setupStore(t, storageEngine.GetSQLDatabase()) - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) + ctx := newTestContext(t) gomock.InOrder( - invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()), + ctx.invoker.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()), ) - wallet := holder.NewMockWallet(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() - mockSubjectManager := didsubject.NewMockManager(ctrl) - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR, mockSubjectManager) + ctx.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, nil) // Alice - _ = store.setPresentationRefreshError(testServiceID, aliceSubject, assert.AnError) - _ = store.updatePresentationRefreshTime(testServiceID, aliceSubject, defaultRegistrationParams(aliceSubject), &time.Time{}) - mockSubjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) - wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), &aliceDID, false).Return(&vpAlice, nil) - wallet.EXPECT().List(gomock.Any(), aliceDID).Return([]vc.VerifiableCredential{vcAlice}, nil) + _ = ctx.store.setPresentationRefreshError(testServiceID, aliceSubject, assert.AnError) + _ = ctx.store.updatePresentationRefreshTime(testServiceID, aliceSubject, defaultRegistrationParams(aliceSubject), &time.Time{}) + ctx.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + ctx.wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), &aliceDID, false).Return(&vpAlice, nil) + ctx.wallet.EXPECT().List(gomock.Any(), aliceDID).Return([]vc.VerifiableCredential{vcAlice}, nil) - err := manager.refresh(audit.TestContext(), time.Now()) + err := ctx.manager.refresh(audit.TestContext(), time.Now()) require.NoError(t, err) // check for presentationRefreshError - refreshError := getPresentationRefreshError(t, store.db, testServiceID, aliceSubject) + refreshError := getPresentationRefreshError(t, ctx.store.db, testServiceID, aliceSubject) assert.Nil(t, refreshError) }) } diff --git a/discovery/module.go b/discovery/module.go index c39cc75d8..0ad5f38e5 100644 --- a/discovery/module.go +++ b/discovery/module.go @@ -32,6 +32,7 @@ import ( "github.com/nuts-foundation/nuts-node/vcr" "github.com/nuts-foundation/nuts-node/vcr/credential" "github.com/nuts-foundation/nuts-node/vdr/didsubject" + "github.com/nuts-foundation/nuts-node/vdr/resolver" "net/url" "os" "path" @@ -67,11 +68,12 @@ var _ Client = &Module{} var retractionPresentationType = ssi.MustParseURI("RetractedVerifiablePresentation") // New creates a new Module. -func New(storageInstance storage.Engine, vcrInstance vcr.VCR, subjectManager didsubject.Manager) *Module { +func New(storageInstance storage.Engine, vcrInstance vcr.VCR, subjectManager didsubject.Manager, didResolver resolver.DIDResolver) *Module { m := &Module{ storageInstance: storageInstance, vcrInstance: vcrInstance, subjectManager: subjectManager, + didResolver: didResolver, } m.ctx, m.cancel = context.WithCancel(context.Background()) m.routines = new(sync.WaitGroup) @@ -89,6 +91,7 @@ type Module struct { allDefinitions map[string]ServiceDefinition vcrInstance vcr.VCR subjectManager didsubject.Manager + didResolver resolver.DIDResolver clientUpdater *clientUpdater ctx context.Context cancel context.CancelFunc @@ -149,7 +152,7 @@ func (m *Module) Start() error { return err } m.clientUpdater = newClientUpdater(m.allDefinitions, m.store, m.verifyRegistration, m.httpClient) - m.registrationManager = newRegistrationManager(m.allDefinitions, m.store, m.httpClient, m.vcrInstance, m.subjectManager) + m.registrationManager = newRegistrationManager(m.allDefinitions, m.store, m.httpClient, m.vcrInstance, m.subjectManager, m.didResolver) if m.config.Client.RefreshInterval > 0 { m.routines.Add(1) go func() { diff --git a/discovery/module_test.go b/discovery/module_test.go index 4e781ef78..8e7d317bc 100644 --- a/discovery/module_test.go +++ b/discovery/module_test.go @@ -35,6 +35,7 @@ import ( "github.com/nuts-foundation/nuts-node/vcr/pe" "github.com/nuts-foundation/nuts-node/vcr/verifier" "github.com/nuts-foundation/nuts-node/vdr/didsubject" + "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -309,6 +310,7 @@ type mockContext struct { ctrl *gomock.Controller subjectManager *didsubject.MockManager verifier *verifier.MockVerifier + didResolver *resolver.MockDIDResolver } func setupModule(t *testing.T, storageInstance storage.Engine, visitors ...func(module *Module)) (*Module, mockContext) { @@ -318,7 +320,8 @@ func setupModule(t *testing.T, storageInstance storage.Engine, visitors ...func( mockVCR := vcr.NewMockVCR(ctrl) mockVCR.EXPECT().Verifier().Return(mockVerifier).AnyTimes() mockSubjectManager := didsubject.NewMockManager(ctrl) - m := New(storageInstance, mockVCR, mockSubjectManager) + mockDIDResolver := resolver.NewMockDIDResolver(ctrl) + m := New(storageInstance, mockVCR, mockSubjectManager, mockDIDResolver) m.config = DefaultConfig() m.publicURL = test.MustParseURL("https://example.com") require.NoError(t, m.Configure(core.TestServerConfig())) @@ -344,6 +347,7 @@ func setupModule(t *testing.T, storageInstance storage.Engine, visitors ...func( ctrl: ctrl, verifier: mockVerifier, subjectManager: mockSubjectManager, + didResolver: mockDIDResolver, } } @@ -494,6 +498,7 @@ func TestModule_ActivateServiceForSubject(t *testing.T) { wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{vcAlice}, nil) wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&vpAlice, nil) testContext.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + testContext.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, nil) err := m.ActivateServiceForSubject(context.Background(), testServiceID, aliceSubject, nil) @@ -526,6 +531,7 @@ func TestModule_ActivateServiceForSubject(t *testing.T) { return &vpAlice, nil }) testContext.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + testContext.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, nil) err := m.ActivateServiceForSubject(context.Background(), testServiceID, aliceSubject, map[string]interface{}{"test": "value"}) @@ -541,6 +547,17 @@ func TestModule_ActivateServiceForSubject(t *testing.T) { require.EqualError(t, err, "subject not found") }) + t.Run("deactivated", func(t *testing.T) { + storageEngine := storage.NewTestStorageEngine(t) + require.NoError(t, storageEngine.Start()) + m, testContext := setupModule(t, storageEngine) + testContext.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + testContext.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, resolver.ErrDeactivated) + + err := m.ActivateServiceForSubject(context.Background(), testServiceID, aliceSubject, nil) + + assert.ErrorIs(t, err, didsubject.ErrSubjectNotFound) + }) t.Run("ok, but couldn't register presentation -> maps to ErrRegistrationFailed", func(t *testing.T) { storageEngine := storage.NewTestStorageEngine(t) require.NoError(t, storageEngine.Start()) @@ -549,6 +566,7 @@ func TestModule_ActivateServiceForSubject(t *testing.T) { m.vcrInstance.(*vcr.MockVCR).EXPECT().Wallet().Return(wallet).MinTimes(1) wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, errors.New("failed")).MinTimes(1) testContext.subjectManager.EXPECT().ListDIDs(gomock.Any(), aliceSubject).Return([]did.DID{aliceDID}, nil) + testContext.didResolver.EXPECT().Resolve(aliceDID, gomock.Any()).Return(nil, nil, nil) err := m.ActivateServiceForSubject(context.Background(), testServiceID, aliceSubject, nil)