Просмотр исходного кода

test(ngrok): Remove over complicated mocks (#6483)

Co-authored-by: Gergely Bräutigam <gergely.brautigam@sap.com>
Signed-off-by: Jonathan Stacks <jonstacks@users.noreply.github.com>
Jonathan Stacks 3 дней назад
Родитель
Сommit
46fecd11a5

+ 1 - 1
providers/v1/ngrok/client.go

@@ -239,7 +239,7 @@ func (c *client) verifyVaultNameStillMatchesID(ctx context.Context) error {
 	}
 
 	vault, err := c.vaultClient.Get(ctx, vaultID)
-	if err != nil || vault.Name != c.vaultName {
+	if err != nil || vault == nil || vault.Name != c.vaultName {
 		return c.refreshVaultID(ctx)
 	}
 

+ 290 - 178
providers/v1/ngrok/client_test.go

@@ -17,6 +17,7 @@ limitations under the License.
 package ngrok
 
 import (
+	"context"
 	"encoding/json"
 	"errors"
 
@@ -45,61 +46,21 @@ func (p pushSecretRemoteRef) GetProperty() string {
 	return p.property
 }
 
-type testClientOpts struct {
-	vaults         []*ngrok.Vault
-	secrets        []*ngrok.Secret
-	secretsListErr error
-	vaultName      string
-}
-
-type testClientOpt func(opts *testClientOpts)
-
-func WithVaults(vaults ...*ngrok.Vault) testClientOpt {
-	return func(opts *testClientOpts) {
-		opts.vaults = vaults
-	}
-}
-
-func WithSecrets(secrets ...*ngrok.Secret) testClientOpt {
-	return func(opts *testClientOpts) {
-		opts.secrets = secrets
-	}
-}
-
-func WithSecretsListError(err error) testClientOpt {
-	return func(opts *testClientOpts) {
-		opts.secretsListErr = err
-	}
-}
-
-func WithVaultName(vaultName string) testClientOpt {
-	return func(opts *testClientOpts) {
-		opts.vaultName = vaultName
-	}
-}
-
 var _ = Describe("client", func() {
 	var (
-		s          *fake.Store
 		c          *client
 		vaultsAPI  *fake.VaultClient
 		secretsAPI *fake.SecretsClient
 		vaultName  string
-
-		listVaultsErr  error
-		listSecretsErr error
 	)
 
 	BeforeEach(func() {
 		vaultName = "test-vault"
-		listSecretsErr = nil
-		listVaultsErr = nil
-		s = fake.NewStore()
+		vaultsAPI = &fake.VaultClient{}
+		secretsAPI = &fake.SecretsClient{}
 	})
 
 	JustBeforeEach(func() {
-		vaultsAPI = s.VaultClient().WithListError(listVaultsErr)
-		secretsAPI = s.SecretsClient().WithListError(listSecretsErr)
 		c = &client{
 			vaultClient:   vaultsAPI,
 			secretsClient: secretsAPI,
@@ -112,10 +73,12 @@ var _ = Describe("client", func() {
 			k8Secret        *corev1.Secret
 			pushData        v1alpha1.PushSecretData
 			vault           *ngrok.Vault
-			secret          *ngrok.Secret
 			ngrokSecretName string
 
 			pushErr error
+
+			createCalledWith *ngrok.SecretCreate
+			updateCalledWith *ngrok.SecretUpdate
 		)
 
 		BeforeEach(func() {
@@ -138,65 +101,120 @@ var _ = Describe("client", func() {
 				},
 			}
 			vault = nil
+			createCalledWith = nil
+			updateCalledWith = nil
 		})
 
 		JustBeforeEach(func(ctx SpecContext) {
 			if vault != nil {
-				// Set the client's vault ID. This is normally initialized by the provider's NewClient method.
 				c.setVaultID(vault.ID)
 			}
 			pushErr = c.PushSecret(ctx, k8Secret, pushData)
 		})
 
-		When("the vault exists", func() {
-			var (
-				getSecretErr error
-			)
+		When("the vault does not exist", func() {
+			BeforeEach(func() {
+				vaultsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Vault] {
+					return fake.NewIter([]*ngrok.Vault{}, nil)
+				}
+			})
+			It("should return an error", func(ctx SpecContext) {
+				Expect(pushErr).To(HaveOccurred())
+				Expect(pushErr.Error()).To(ContainSubstring("failed to verify vault name still matches ID: failed to refresh vault ID: vault does not exist"))
+			})
+		})
 
-			BeforeEach(func(ctx SpecContext) {
-				var vaultCreateErr error
-				vault, vaultCreateErr = s.VaultClient().Create(ctx, &ngrok.VaultCreate{
-					Name: vaultName,
-				})
-				Expect(vaultCreateErr).ToNot(HaveOccurred())
+		When("an error occurs listing vaults", func() {
+			BeforeEach(func() {
+				vaultsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Vault] {
+					return fake.NewIter([]*ngrok.Vault{}, errors.New("failed to list vaults"))
+				}
 			})
+			It("should return an error", func(ctx SpecContext) {
+				Expect(pushErr).To(HaveOccurred())
+				Expect(pushErr.Error()).To(ContainSubstring("failed to list vaults"))
+			})
+		})
 
-			// Re-fetch the secret after the push to verify it was updated
-			JustBeforeEach(func(ctx SpecContext) {
-				secret = nil
-				iter := s.SecretsClient().List(nil)
-				for iter.Next(ctx) {
-					if iter.Item().Name == ngrokSecretName && iter.Item().Vault.ID == vault.ID {
-						secret = iter.Item()
-						break
+		When("the vault exists", func() {
+			BeforeEach(func(ctx SpecContext) {
+				vault = &ngrok.Vault{ID: "vault-123", Name: vaultName}
+				vaultsAPI.GetFn = func(ctx context.Context, id string) (*ngrok.Vault, error) {
+					if id == vault.ID {
+						return vault, nil
 					}
+					return nil, errors.New("not found")
+				}
+				vaultsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Vault] {
+					return fake.NewIter([]*ngrok.Vault{vault}, nil)
 				}
-				getSecretErr = iter.Err()
+			})
+
+			When("an error occurs listing secrets", func() {
+				BeforeEach(func() {
+					secretsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Secret] {
+						return fake.NewIter([]*ngrok.Secret{}, errors.New("failed to list secrets"))
+					}
+				})
+				It("should return an error", func(ctx SpecContext) {
+					Expect(pushErr).To(HaveOccurred())
+					Expect(pushErr.Error()).To(ContainSubstring("failed to get secret: failed to list secrets"))
+				})
 			})
 
 			When("the secret does not exist", func() {
+				BeforeEach(func() {
+					secretsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Secret] {
+						return fake.NewIter([]*ngrok.Secret{}, nil)
+					}
+					secretsAPI.CreateFn = func(ctx context.Context, req *ngrok.SecretCreate) (*ngrok.Secret, error) {
+						createCalledWith = req
+						return &ngrok.Secret{ID: "sec-123", Name: req.Name, Description: req.Description}, nil
+					}
+				})
+
 				It("should not return an error", func(ctx SpecContext) {
 					Expect(pushErr).ToNot(HaveOccurred())
 				})
 
 				It("should create the ngrok secret", func(ctx SpecContext) {
-					Expect(getSecretErr).ToNot(HaveOccurred())
-					Expect(secret).ToNot(BeNil())
-					Expect(secret.Name).To(Equal(ngrokSecretName))
-					Expect(secret.ID).ToNot(BeEmpty())
-					Expect(secret.Description).To(Equal(defaultDescription))
+					Expect(createCalledWith).ToNot(BeNil())
+					Expect(createCalledWith.Name).To(Equal(ngrokSecretName))
+					Expect(createCalledWith.Description).To(Equal(defaultDescription))
+					Expect(createCalledWith.VaultID).To(Equal(vault.ID))
+					Expect(createCalledWith.Value).To(Equal("new value"))
+				})
+
+				When("secret creation fails", func() {
+					BeforeEach(func() {
+						secretsAPI.CreateFn = func(ctx context.Context, req *ngrok.SecretCreate) (*ngrok.Secret, error) {
+							return nil, errors.New("failed to create secret")
+						}
+					})
+					It("should return an error", func(ctx SpecContext) {
+						Expect(pushErr).To(HaveOccurred())
+						Expect(pushErr.Error()).To(ContainSubstring("failed to create secret"))
+					})
 				})
 			})
 
 			When("the secret exists", func() {
+				var existingSecret *ngrok.Secret
 				BeforeEach(func(ctx SpecContext) {
-					var createErr error
-					secret, createErr = s.SecretsClient().Create(ctx, &ngrok.SecretCreate{
-						VaultID: vault.ID,
-						Name:    ngrokSecretName,
-						Value:   "old-value",
-					})
-					Expect(createErr).ToNot(HaveOccurred())
+					existingSecret = &ngrok.Secret{
+						ID:   "sec-123",
+						Name: ngrokSecretName,
+						Vault: ngrok.Ref{
+							ID: vault.ID,
+						},
+					}
+					secretsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Secret] {
+						return fake.NewIter([]*ngrok.Secret{existingSecret}, nil)
+					}
+					secretsAPI.UpdateFn = func(ctx context.Context, req *ngrok.SecretUpdate) (*ngrok.Secret, error) {
+						updateCalledWith = req
+						return &ngrok.Secret{ID: req.ID}, nil
+					}
 				})
 
 				It("should not return an error", func(ctx SpecContext) {
@@ -204,13 +222,28 @@ var _ = Describe("client", func() {
 				})
 
 				It("should update the ngrok secret description", func(ctx SpecContext) {
-					Expect(secret.Description).To(Equal(defaultDescription))
+					Expect(updateCalledWith).ToNot(BeNil())
+					Expect(updateCalledWith.Description).ToNot(BeNil())
+					Expect(*updateCalledWith.Description).To(Equal(defaultDescription))
 				})
 
 				It("should update the ngrok secret metadata", func(ctx SpecContext) {
-					// The metadata should include the sha256 of the new value.
+					Expect(updateCalledWith).ToNot(BeNil())
+					Expect(updateCalledWith.Metadata).ToNot(BeNil())
 					// sha256sum "new value" = 9c51d0b0f64dfb3662ed85ce945dd1e8f6130665c289754e4e9257a58013e61d
-					Expect(secret.Metadata).To(Equal(`{"_sha256":"9c51d0b0f64dfb3662ed85ce945dd1e8f6130665c289754e4e9257a58013e61d"}`))
+					Expect(*updateCalledWith.Metadata).To(Equal(`{"_sha256":"9c51d0b0f64dfb3662ed85ce945dd1e8f6130665c289754e4e9257a58013e61d"}`))
+				})
+
+				When("secret update fails", func() {
+					BeforeEach(func() {
+						secretsAPI.UpdateFn = func(ctx context.Context, req *ngrok.SecretUpdate) (*ngrok.Secret, error) {
+							return nil, errors.New("failed to update secret")
+						}
+					})
+					It("should return an error", func(ctx SpecContext) {
+						Expect(pushErr).To(HaveOccurred())
+						Expect(pushErr.Error()).To(ContainSubstring("failed to update secret"))
+					})
 				})
 
 				When("The secret key is not specified on the push data", func() {
@@ -219,8 +252,11 @@ var _ = Describe("client", func() {
 					})
 
 					It("should marshal the entire secret data as JSON", func(ctx SpecContext) {
+						Expect(updateCalledWith).ToNot(BeNil())
+						Expect(updateCalledWith.Metadata).ToNot(BeNil())
+
 						data := map[string]string{}
-						err := json.Unmarshal([]byte(secret.Metadata), &data)
+						err := json.Unmarshal([]byte(*updateCalledWith.Metadata), &data)
 
 						Expect(err).ToNot(HaveOccurred())
 						Expect(data).To(HaveKeyWithValue("_sha256", "146ed8bb7a977ee78ee11cf262924e3ae93423c413ab6d612a8d159a0ae4e1ad"))
@@ -254,12 +290,16 @@ spec:
 						})
 
 						It("should update the ngrok secret description", func(ctx SpecContext) {
-							Expect(secret.Description).To(Equal("my custom description"))
+							Expect(updateCalledWith).ToNot(BeNil())
+							Expect(updateCalledWith.Description).ToNot(BeNil())
+							Expect(*updateCalledWith.Description).To(Equal("my custom description"))
 						})
 
 						It("should update the ngrok secret metadata", func(ctx SpecContext) {
+							Expect(updateCalledWith).ToNot(BeNil())
+							Expect(updateCalledWith.Metadata).ToNot(BeNil())
 							data := map[string]string{}
-							err := json.Unmarshal([]byte(secret.Metadata), &data)
+							err := json.Unmarshal([]byte(*updateCalledWith.Metadata), &data)
 							Expect(err).ToNot(HaveOccurred())
 							Expect(data).To(HaveKeyWithValue("environment", "production"))
 							Expect(data).To(HaveKeyWithValue("team", "frontend"))
@@ -282,6 +322,33 @@ spec:
 				})
 			})
 		})
+
+		When("the cached vault ID no longer matches the vault name", func() {
+			BeforeEach(func() {
+				// Seed a stale cached vault ID. The vault it points to has since
+				// been renamed, so the client must refresh the ID before pushing.
+				vault = &ngrok.Vault{ID: "stale-vault-id", Name: vaultName}
+				vaultsAPI.GetFn = func(ctx context.Context, id string) (*ngrok.Vault, error) {
+					return &ngrok.Vault{ID: id, Name: "renamed-out-from-under-us"}, nil
+				}
+				vaultsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Vault] {
+					return fake.NewIter([]*ngrok.Vault{{ID: "fresh-vault-id", Name: vaultName}}, nil)
+				}
+				secretsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Secret] {
+					return fake.NewIter([]*ngrok.Secret{}, nil)
+				}
+				secretsAPI.CreateFn = func(ctx context.Context, req *ngrok.SecretCreate) (*ngrok.Secret, error) {
+					createCalledWith = req
+					return &ngrok.Secret{ID: "sec-123"}, nil
+				}
+			})
+
+			It("should refresh the vault ID and push using the refreshed ID", func(ctx SpecContext) {
+				Expect(pushErr).ToNot(HaveOccurred())
+				Expect(createCalledWith).ToNot(BeNil())
+				Expect(createCalledWith.VaultID).To(Equal("fresh-vault-id"))
+			})
+		})
 	})
 
 	Describe("SecretExists", func() {
@@ -303,6 +370,11 @@ spec:
 		})
 
 		When("the vault does not exist", func() {
+			BeforeEach(func() {
+				vaultsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Vault] {
+					return fake.NewIter([]*ngrok.Vault{}, nil)
+				}
+			})
 			It("should return exists as false without an error", func(ctx SpecContext) {
 				Expect(err).ToNot(HaveOccurred())
 				Expect(exists).To(BeFalse())
@@ -310,17 +382,20 @@ spec:
 		})
 
 		When("the vault exists", func() {
-			var (
-				vault *ngrok.Vault
-			)
+			var vault *ngrok.Vault
 			BeforeEach(func(ctx SpecContext) {
-				vault, err = s.VaultClient().Create(ctx, &ngrok.VaultCreate{
-					Name: c.vaultName,
-				})
-				Expect(err).ToNot(HaveOccurred())
+				vault = &ngrok.Vault{ID: "vault-123", Name: vaultName}
+				vaultsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Vault] {
+					return fake.NewIter([]*ngrok.Vault{vault}, nil)
+				}
 			})
 
 			When("the secret does not exist", func() {
+				BeforeEach(func() {
+					secretsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Secret] {
+						return fake.NewIter([]*ngrok.Secret{}, nil)
+					}
+				})
 				It("should return exists as false without an error", func(ctx SpecContext) {
 					Expect(err).ToNot(HaveOccurred())
 					Expect(exists).To(BeFalse())
@@ -329,12 +404,11 @@ spec:
 
 			When("the secret exists", func() {
 				BeforeEach(func(ctx SpecContext) {
-					_, err = s.SecretsClient().Create(ctx, &ngrok.SecretCreate{
-						VaultID: vault.ID,
-						Name:    secretName,
-						Value:   "supersecret",
-					})
-					Expect(err).ToNot(HaveOccurred())
+					secretsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Secret] {
+						return fake.NewIter([]*ngrok.Secret{
+							{ID: "sec-123", Name: secretName, Vault: ngrok.Ref{ID: vault.ID}},
+						}, nil)
+					}
 				})
 
 				It("should return exists as true without an error", func(ctx SpecContext) {
@@ -342,11 +416,26 @@ spec:
 					Expect(exists).To(BeTrue())
 				})
 			})
+
+			When("an error occurs listing secrets", func() {
+				BeforeEach(func(ctx SpecContext) {
+					secretsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Secret] {
+						return fake.NewIter([]*ngrok.Secret{}, errors.New("failed to list secrets"))
+					}
+				})
+
+				It("should return an error", func(ctx SpecContext) {
+					Expect(err).To(HaveOccurred())
+					Expect(err.Error()).To(ContainSubstring("error fetching secret: failed to list secrets"))
+				})
+			})
 		})
 
 		When("an error occurs listing vaults", func() {
 			BeforeEach(func() {
-				listVaultsErr = errors.New("failed to list vaults")
+				vaultsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Vault] {
+					return fake.NewIter([]*ngrok.Vault{}, errors.New("failed to list vaults"))
+				}
 			})
 
 			It("should return exists as false", func() {
@@ -362,22 +451,18 @@ spec:
 
 	Describe("getVaultByName", func() {
 		var (
-			vault   *ngrok.Vault
-			fetched *ngrok.Vault
-			err     error
+			vault      *ngrok.Vault
+			fetched    *ngrok.Vault
+			err        error
+			listPaging *ngrok.FilteredPaging
 		)
 
 		BeforeEach(func(ctx SpecContext) {
-			var createErr error
-			vault, createErr = s.CreateVault(&ngrok.VaultCreate{
-				Name: vaultName,
-			})
-			Expect(createErr).ToNot(HaveOccurred())
-
-			_, createErr = s.CreateVault(&ngrok.VaultCreate{
-				Name: "other-vault",
-			})
-			Expect(createErr).ToNot(HaveOccurred())
+			vault = &ngrok.Vault{ID: "vault-123", Name: vaultName}
+			vaultsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Vault] {
+				listPaging = paging
+				return fake.NewIter([]*ngrok.Vault{vault}, nil)
+			}
 		})
 
 		JustBeforeEach(func(ctx SpecContext) {
@@ -391,77 +476,62 @@ spec:
 		})
 
 		It("should filter vaults by name", func() {
-			Expect(vaultsAPI.LastListPaging()).ToNot(BeNil())
-			Expect(vaultsAPI.LastListPaging().Filter).ToNot(BeNil())
-			Expect(*vaultsAPI.LastListPaging().Filter).To(Equal(`obj.name == "test-vault"`))
+			Expect(listPaging).ToNot(BeNil())
+			Expect(listPaging.Filter).ToNot(BeNil())
+			Expect(*listPaging.Filter).To(Equal(`obj.name == "test-vault"`))
 		})
 	})
 
 	Describe("getSecretByVaultIDAndName", func() {
 		var (
-			targetVault *ngrok.Vault
-			otherVault  *ngrok.Vault
-			found       *ngrok.Secret
-			err         error
-			secretName  string
+			targetVaultID string
+			found         *ngrok.Secret
+			err           error
+			secretName    string
+			listPaging    *ngrok.FilteredPaging
 		)
 
 		BeforeEach(func(ctx SpecContext) {
 			secretName = "shared-name"
-
-			var createErr error
-			targetVault, createErr = s.CreateVault(&ngrok.VaultCreate{Name: vaultName})
-			Expect(createErr).ToNot(HaveOccurred())
-
-			otherVault, createErr = s.CreateVault(&ngrok.VaultCreate{Name: "other-vault"})
-			Expect(createErr).ToNot(HaveOccurred())
+			targetVaultID = "vault-target"
 		})
 
 		JustBeforeEach(func(ctx SpecContext) {
-			found, err = c.getSecretByVaultIDAndName(ctx, targetVault.ID, secretName)
+			found, err = c.getSecretByVaultIDAndName(ctx, targetVaultID, secretName)
 		})
 
 		When("a secret with the same name exists in multiple vaults", func() {
-			var targetSecret *ngrok.Secret
-
 			BeforeEach(func(ctx SpecContext) {
-				_, createErr := s.CreateSecret(&ngrok.SecretCreate{
-					VaultID: otherVault.ID,
-					Name:    secretName,
-					Value:   "other-value",
-				})
-				Expect(createErr).ToNot(HaveOccurred())
-
-				targetSecret, createErr = s.CreateSecret(&ngrok.SecretCreate{
-					VaultID: targetVault.ID,
-					Name:    secretName,
-					Value:   "target-value",
-				})
-				Expect(createErr).ToNot(HaveOccurred())
+				secretsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Secret] {
+					listPaging = paging
+					return fake.NewIter([]*ngrok.Secret{
+						{ID: "sec-1", Name: secretName, Vault: ngrok.Ref{ID: "vault-other"}},
+						{ID: "sec-2", Name: secretName, Vault: ngrok.Ref{ID: targetVaultID}},
+					}, nil)
+				}
 			})
 
 			It("should return the secret from the target vault", func() {
 				Expect(err).ToNot(HaveOccurred())
 				Expect(found).ToNot(BeNil())
-				Expect(found.ID).To(Equal(targetSecret.ID))
-				Expect(found.Vault.ID).To(Equal(targetVault.ID))
+				Expect(found.ID).To(Equal("sec-2"))
+				Expect(found.Vault.ID).To(Equal(targetVaultID))
 			})
 
 			It("should filter secrets by name before checking the vault", func() {
-				Expect(secretsAPI.LastListPaging()).ToNot(BeNil())
-				Expect(secretsAPI.LastListPaging().Filter).ToNot(BeNil())
-				Expect(*secretsAPI.LastListPaging().Filter).To(Equal(`obj.name == "shared-name"`))
+				Expect(listPaging).ToNot(BeNil())
+				Expect(listPaging.Filter).ToNot(BeNil())
+				Expect(*listPaging.Filter).To(Equal(`obj.name == "shared-name"`))
 			})
 		})
 
 		When("only another vault has a secret with that name", func() {
 			BeforeEach(func(ctx SpecContext) {
-				_, createErr := s.CreateSecret(&ngrok.SecretCreate{
-					VaultID: otherVault.ID,
-					Name:    secretName,
-					Value:   "other-value",
-				})
-				Expect(createErr).ToNot(HaveOccurred())
+				secretsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Secret] {
+					return fake.NewIter([]*ngrok.Secret{
+						{ID: "sec-1", Name: secretName, Vault: ngrok.Ref{ID: "vault-other"}},
+					}, nil)
+				}
 			})
 
 			It("should report the secret as missing from the target vault", func() {
@@ -476,12 +546,13 @@ spec:
 	Describe("DeleteSecret", func() {
 		var (
 			secretName string
-
-			err error
+			err        error
+			deletedID  string
 		)
 
 		BeforeEach(func() {
 			secretName = "my-secret"
+			deletedID = ""
 		})
 
 		JustBeforeEach(func(ctx SpecContext) {
@@ -491,6 +562,11 @@ spec:
 		})
 
 		When("the vault does not exist", func() {
+			BeforeEach(func() {
+				vaultsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Vault] {
+					return fake.NewIter([]*ngrok.Vault{}, nil)
+				}
+			})
 			It("should not return an error", func(ctx SpecContext) {
 				Expect(err).ToNot(HaveOccurred())
 			})
@@ -498,10 +574,12 @@ spec:
 
 		When("the vault exists but the secret does not", func() {
 			BeforeEach(func(ctx SpecContext) {
-				_, err := c.vaultClient.Create(ctx, &ngrok.VaultCreate{
-					Name: c.vaultName,
-				})
-				Expect(err).ToNot(HaveOccurred())
+				vaultsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Vault] {
+					return fake.NewIter([]*ngrok.Vault{{ID: "vault-123", Name: vaultName}}, nil)
+				}
+				secretsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Secret] {
+					return fake.NewIter([]*ngrok.Secret{}, nil)
+				}
 			})
 
 			It("should not return an error", func(ctx SpecContext) {
@@ -511,26 +589,60 @@ spec:
 
 		When("the vault and secret both exist", func() {
 			BeforeEach(func(ctx SpecContext) {
-				vault, err := s.VaultClient().Create(ctx, &ngrok.VaultCreate{
-					Name: c.vaultName,
-				})
+				vaultsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Vault] {
+					return fake.NewIter([]*ngrok.Vault{{ID: "vault-123", Name: vaultName}}, nil)
+				}
+				secretsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Secret] {
+					return fake.NewIter([]*ngrok.Secret{
+						{ID: "sec-123", Name: secretName, Vault: ngrok.Ref{ID: "vault-123"}},
+					}, nil)
+				}
+				secretsAPI.DeleteFn = func(ctx context.Context, id string) error {
+					deletedID = id
+					return nil
+				}
+			})
+
+			It("should not return an error and delete is called", func(ctx SpecContext) {
 				Expect(err).ToNot(HaveOccurred())
-				_, err = s.SecretsClient().Create(ctx, &ngrok.SecretCreate{
-					VaultID: vault.ID,
-					Name:    secretName,
-					Value:   "supersecret",
+				Expect(deletedID).To(Equal("sec-123"))
+			})
+
+			When("secret deletion fails", func() {
+				BeforeEach(func(ctx SpecContext) {
+					secretsAPI.DeleteFn = func(ctx context.Context, id string) error {
+						return errors.New("failed to delete secret")
+					}
+				})
+
+				It("should return an error", func(ctx SpecContext) {
+					Expect(err).To(HaveOccurred())
+					Expect(err.Error()).To(ContainSubstring("failed to delete secret"))
 				})
-				Expect(err).ToNot(HaveOccurred())
 			})
+		})
 
-			It("should not return an error", func(ctx SpecContext) {
-				Expect(err).ToNot(HaveOccurred())
+		When("an error occurs listing secrets", func() {
+			BeforeEach(func(ctx SpecContext) {
+				vaultsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Vault] {
+					return fake.NewIter([]*ngrok.Vault{{ID: "vault-123", Name: vaultName}}, nil)
+				}
+				secretsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Secret] {
+					return fake.NewIter([]*ngrok.Secret{}, errors.New("failed to list secrets"))
+				}
+			})
+
+			It("should return an error", func(ctx SpecContext) {
+				Expect(err).To(HaveOccurred())
+				Expect(err.Error()).To(ContainSubstring("failed to list secrets"))
 			})
 		})
 
 		When("an error occurs listing vaults", func() {
 			BeforeEach(func() {
-				listVaultsErr = errors.New("failed to list vaults")
+				vaultsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Vault] {
+					return fake.NewIter([]*ngrok.Vault{}, errors.New("failed to list vaults"))
+				}
 			})
 
 			It("should return the listing error", func() {
@@ -552,6 +664,11 @@ spec:
 
 		When("the client can list secrets", func() {
 			When("there are no secrets", func() {
+				BeforeEach(func() {
+					secretsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Secret] {
+						return fake.NewIter([]*ngrok.Secret{}, nil)
+					}
+				})
 				It("should return ValidationResultReady without an error", func() {
 					Expect(err).To(BeNil())
 					Expect(result).To(Equal(esv1.ValidationResultReady))
@@ -560,16 +677,9 @@ spec:
 
 			When("there are some secrets", func() {
 				BeforeEach(func(ctx SpecContext) {
-					vault, err := s.VaultClient().Create(ctx, &ngrok.VaultCreate{
-						Name: c.vaultName,
-					})
-					Expect(err).ToNot(HaveOccurred())
-					_, err = s.SecretsClient().Create(ctx, &ngrok.SecretCreate{
-						VaultID: vault.ID,
-						Name:    "my-secret",
-						Value:   "supersecret",
-					})
-					Expect(err).ToNot(HaveOccurred())
+					secretsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Secret] {
+						return fake.NewIter([]*ngrok.Secret{{ID: "sec-1"}}, nil)
+					}
 				})
 
 				It("should return ValidationResultReady without an error", func() {
@@ -581,7 +691,9 @@ spec:
 
 		When("the client cannot list secrets", func() {
 			BeforeEach(func() {
-				listSecretsErr = errors.New("failed to list secrets")
+				secretsAPI.ListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Secret] {
+					return fake.NewIter([]*ngrok.Secret{}, errors.New("failed to list secrets"))
+				}
 			})
 
 			It("should return ValidationResultError with the listing error", func() {

+ 54 - 602
providers/v1/ngrok/fake/fake.go

@@ -18,19 +18,20 @@ package fake
 
 import (
 	"context"
+	"errors"
 	"fmt"
-	"maps"
 	"math/rand"
-	"net/http"
-	"slices"
-	"strconv"
 	"strings"
-	"sync"
 	"time"
 
 	"github.com/ngrok/ngrok-api-go/v9"
 )
 
+// errFnNotConfigured is returned by a fake method when it is called without a
+// corresponding function being set. Failing loudly is safer than returning a
+// nil result, which the real ngrok API never does and which callers may deref.
+var errFnNotConfigured = errors.New("fake: method called but no function configured")
+
 func GenerateRandomString(length int) string {
 	const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
 	seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) /* #nosec G404 */
@@ -43,557 +44,84 @@ func GenerateRandomString(length int) string {
 	return sb.String()
 }
 
-func VaultNameEmpty() *ngrok.Error {
-	return &ngrok.Error{
-		ErrorCode:  "ERR_NGROK_23001",
-		StatusCode: http.StatusBadRequest,
-		Msg:        "The vault name cannot be empty.",
-	}
-}
-
-func VaultNamesMustBeUniqueWithinAccount() *ngrok.Error {
-	return &ngrok.Error{
-		ErrorCode:  "ERR_NGROK_23004",
-		StatusCode: http.StatusBadRequest,
-		Msg:        "Vault names must be unique within an account.",
-	}
-}
-
-func VaultNameInvalid(name string) *ngrok.Error {
-	return &ngrok.Error{
-		ErrorCode:  "ERR_NGROK_23002",
-		StatusCode: http.StatusBadRequest,
-		Msg:        fmt.Sprintf("The vault name %q is invalid. Must only contain the characters \"a-zA-Z0-9_/.\".", name),
-	}
-}
-
-func SecretNameEmpty() *ngrok.Error {
-	return &ngrok.Error{
-		ErrorCode:  "ERR_NGROK_24001",
-		StatusCode: http.StatusBadRequest,
-		Msg:        "The secret name cannot be empty.",
-	}
-}
-
-func SecretValueEmpty() *ngrok.Error {
-	return &ngrok.Error{
-		ErrorCode:  "ERR_NGROK_24003",
-		StatusCode: http.StatusBadRequest,
-		Msg:        "The secret value cannot be empty.",
-	}
-}
-
-func SecretNameMustBeUniqueWithinVault() *ngrok.Error {
-	return &ngrok.Error{
-		ErrorCode:  "ERR_NGROK_24005",
-		StatusCode: http.StatusBadRequest,
-		Msg:        "Secret names must be unique within a vault.",
-	}
-}
-
-func SecretVaultNotFound(id string) *ngrok.Error {
-	return &ngrok.Error{
-		ErrorCode:  "ERR_NGROK_24006",
-		StatusCode: http.StatusNotFound,
-		Msg:        fmt.Sprintf("Vault with ID %s not found.", id),
-	}
-}
-
-func NotFound(id string) *ngrok.Error {
-	return &ngrok.Error{
-		StatusCode: http.StatusNotFound,
-		Msg:        fmt.Sprintf("Resource with ID %s not found.", id),
-	}
-}
-
-func VaultNotEmpty() *ngrok.Error {
-	return &ngrok.Error{
-		ErrorCode:  "ERR_NGROK_23003",
-		StatusCode: http.StatusBadRequest,
-		Msg:        "A Vault must be empty before it can be deleted. Please remove all secrets from the vault and try again.",
-	}
-}
-
-type vault struct {
-	vault *ngrok.Vault
-
-	mu          sync.RWMutex
-	secretsByID map[string]*ngrok.Secret
-}
-
-// newVault creates a new vault instance with an empty secrets map.
-// given the ngrok.Vault to wrap.
-func newVault(v *ngrok.Vault) *vault {
-	return &vault{
-		vault:       v,
-		secretsByID: make(map[string]*ngrok.Secret),
-	}
-}
-
-func (v *vault) setSecret(id string, secret *ngrok.Secret) {
-	v.mu.Lock()
-	defer v.mu.Unlock()
-	v.secretsByID[id] = secret
-}
-
-func (v *vault) getSecret(id string) (*ngrok.Secret, bool) {
-	v.mu.RLock()
-	defer v.mu.RUnlock()
-	val, ok := v.secretsByID[id]
-	return val, ok
-}
-
-func (v *vault) deleteSecret(id string) {
-	v.mu.Lock()
-	defer v.mu.Unlock()
-	delete(v.secretsByID, id)
-}
-
-// CreateSecret creates a new secret in the vault.
-func (v *vault) CreateSecret(s *ngrok.SecretCreate) (*ngrok.Secret, error) {
-	if s.Name == "" {
-		return nil, SecretNameEmpty()
-	}
-
-	if s.Value == "" {
-		return nil, SecretValueEmpty()
-	}
-
-	existing := v.GetSecretByName(s.Name)
-	if existing != nil {
-		return nil, SecretNameMustBeUniqueWithinVault()
-	}
-
-	ts := time.Now()
-	newSecret := &ngrok.Secret{
-		ID: "secret_" + GenerateRandomString(20),
-		Vault: ngrok.Ref{
-			ID:  v.vault.ID,
-			URI: v.vault.URI,
-		},
-		Name:        s.Name,
-		Description: s.Description,
-		Metadata:    s.Metadata,
-		CreatedAt:   ts.Format(time.RFC3339),
-		UpdatedAt:   ts.Format(time.RFC3339),
-	}
-
-	v.setSecret(newSecret.ID, newSecret)
-	return newSecret, nil
-}
-
-// DeleteSecret deletes a secret from the vault by ID.
-func (v *vault) DeleteSecret(id string) error {
-	_, exists := v.getSecret(id)
-
-	if exists {
-		v.deleteSecret(id)
-		return nil
-	}
-
-	return NotFound(id)
-}
-
-// ListSecrets returns all secrets in the vault.
-func (v *vault) ListSecrets() []*ngrok.Secret {
-	v.mu.RLock()
-	defer v.mu.RUnlock()
-
-	return slices.Collect(maps.Values(v.secretsByID))
-}
-
-// GetSecretByID returns the secret with the given ID, or nil if not found.
-func (v *vault) GetSecretByID(id string) *ngrok.Secret {
-	val, _ := v.getSecret(id)
-	return val
-}
-
-// GetSecretByName returns the secret with the given name, or nil if not found.
-func (v *vault) GetSecretByName(name string) *ngrok.Secret {
-	for _, secret := range v.ListSecrets() {
-		if secret.Name == name {
-			return secret
-		}
-	}
-	return nil
-}
-
-// UpdateSecret updates an existing secret in the vault.
-func (v *vault) UpdateSecret(s *ngrok.SecretUpdate) (*ngrok.Secret, error) {
-	secret := v.GetSecretByID(s.ID)
-	if secret == nil {
-		return nil, NotFound(s.ID)
-	}
-
-	if s.Name != nil {
-		if *s.Name == "" {
-			return nil, SecretNameEmpty()
-		}
-
-		existing := v.GetSecretByName(*s.Name)
-		if existing != nil && existing.ID != s.ID {
-			return nil, SecretNameMustBeUniqueWithinVault()
-		}
-	}
-
-	if s.Value != nil {
-		if *s.Value == "" {
-			return nil, SecretValueEmpty()
-		}
-	}
-
-	ts := time.Now()
-	secret.UpdatedAt = ts.Format(time.RFC3339)
-	if s.Name != nil {
-		secret.Name = *s.Name
-	}
-	if s.Description != nil {
-		secret.Description = *s.Description
-	}
-	if s.Metadata != nil {
-		secret.Metadata = *s.Metadata
-	}
-
-	return secret, nil
-}
-
-type Store struct {
-	mu         sync.RWMutex
-	vaultsByID map[string]*vault
-}
-
-func NewStore() *Store {
-	return &Store{
-		vaultsByID: make(map[string]*vault),
-	}
-}
-
-func (s *Store) setVault(id string, v *vault) {
-	s.mu.Lock()
-	defer s.mu.Unlock()
-	s.vaultsByID[id] = v
-}
-
-func (s *Store) getVault(id string) (*vault, bool) {
-	s.mu.RLock()
-	defer s.mu.RUnlock()
-	val, ok := s.vaultsByID[id]
-	return val, ok
-}
-
-func (s *Store) deleteVault(id string) {
-	s.mu.Lock()
-	defer s.mu.Unlock()
-	delete(s.vaultsByID, id)
-}
-
-// CreateVault creates a new vault in the store.
-func (s *Store) CreateVault(v *ngrok.VaultCreate) (*ngrok.Vault, error) {
-	if v.Name == "" {
-		return nil, VaultNameEmpty()
-	}
-
-	for _, vault := range s.ListVaults() {
-		if vault.Name == v.Name {
-			return nil, VaultNamesMustBeUniqueWithinAccount()
-		}
-	}
-
-	ts := time.Now()
-	ngrokVault := &ngrok.Vault{
-		ID:          "vault_" + GenerateRandomString(20),
-		Name:        v.Name,
-		Description: v.Description,
-		Metadata:    v.Metadata,
-		CreatedAt:   ts.Format(time.RFC3339),
-		UpdatedAt:   ts.Format(time.RFC3339),
-	}
-
-	s.setVault(ngrokVault.ID, newVault(ngrokVault))
-	return ngrokVault, nil
-}
-
-func (s *Store) CreateSecret(secret *ngrok.SecretCreate) (*ngrok.Secret, error) {
-	v, _ := s.getVault(secret.VaultID)
-
-	if v == nil {
-		return nil, SecretVaultNotFound(secret.VaultID)
-	}
-	return v.CreateSecret(secret)
-}
-
-// DeleteVault deletes a vault from the store by ID.
-func (s *Store) DeleteVault(id string) error {
-	v, _ := s.getVault(id)
-
-	if v == nil {
-		return NotFound(id)
-	}
-
-	if len(v.ListSecrets()) > 0 {
-		return VaultNotEmpty()
-	}
-
-	s.deleteVault(id)
-	return nil
-}
-
-// GetVaultByID returns the vault with the given ID, or nil if not found.
-func (s *Store) GetVaultByID(id string) (*ngrok.Vault, error) {
-	v, _ := s.getVault(id)
-	if v == nil {
-		return nil, NotFound(id)
-	}
-	return v.vault, nil
-}
-
-func (s *Store) GetVaultByName(name string) *ngrok.Vault {
-	for _, v := range s.ListVaults() {
-		if v.Name == name {
-			return v
-		}
-	}
-	return nil
-}
-
-// ListSecrets returns all secrets in the store.
-func (s *Store) ListSecrets() []*ngrok.Secret {
-	s.mu.RLock()
-	defer s.mu.RUnlock()
-
-	secrets := []*ngrok.Secret{}
-	for _, v := range s.vaultsByID {
-		secrets = append(secrets, v.ListSecrets()...)
-	}
-	return secrets
-}
-
-// ListVaults returns all vaults in the store.
-func (s *Store) ListVaults() []*ngrok.Vault {
-	s.mu.RLock()
-	defer s.mu.RUnlock()
-
-	vaults := make([]*ngrok.Vault, 0, len(s.vaultsByID))
-	for _, v := range s.vaultsByID {
-		vaults = append(vaults, v.vault)
-	}
-	return vaults
-}
-
-func (s *Store) ListVaultSecrets(vaultID string) ([]*ngrok.Secret, error) {
-	v, _ := s.getVault(vaultID)
-
-	if v == nil {
-		return nil, NotFound(vaultID)
-	}
-
-	return v.ListSecrets(), nil
-}
-
-func (s *Store) UpdateSecret(secret *ngrok.SecretUpdate) (*ngrok.Secret, error) {
-	var found *ngrok.Secret
-
-	for _, sec := range s.ListSecrets() {
-		if sec.ID == secret.ID {
-			found = sec
-			break
-		}
-	}
-
-	if found == nil {
-		return nil, NotFound(secret.ID)
-	}
-
-	v, ok := s.getVault(found.Vault.ID)
-	if !ok {
-		return nil, SecretVaultNotFound(found.Vault.ID)
-	}
-
-	return v.UpdateSecret(secret)
-}
-
-func (s *Store) DeleteSecret(secretID string) error {
-	secret, vault, err := s.GetSecretAndVaultByID(secretID)
-	if err != nil {
-		return err
-	}
-	if secret == nil || vault == nil {
-		return NotFound(secretID)
-	}
-	return vault.DeleteSecret(secretID)
-}
-
-func (s *Store) GetSecretAndVaultByID(secretID string) (*ngrok.Secret, *vault, error) {
-	s.mu.RLock()
-	defer s.mu.RUnlock()
-	for _, v := range s.vaultsByID {
-		if sec := v.GetSecretByID(secretID); sec != nil {
-			return sec, v, nil
-		}
-	}
-	return nil, nil, NotFound(secretID)
-}
-
-func (s *Store) VaultClient() *VaultClient {
-	return &VaultClient{
-		store: s,
-	}
-}
-
-func (s *Store) SecretsClient() *SecretsClient {
-	return &SecretsClient{
-		store: s,
-	}
-}
-
 // VaultClient is a mock implementation which implements the ngrok.VaultsClient interface.
 type VaultClient struct {
-	store     *Store
-	createErr error
-	listErr   error
-
-	lastListPagingMu sync.Mutex
-	lastListPaging   *ngrok.FilteredPaging
+	CreateFn            func(context.Context, *ngrok.VaultCreate) (*ngrok.Vault, error)
+	GetFn               func(context.Context, string) (*ngrok.Vault, error)
+	GetSecretsByVaultFn func(string, *ngrok.Paging) ngrok.Iter[*ngrok.Secret]
+	ListFn              func(*ngrok.FilteredPaging) ngrok.Iter[*ngrok.Vault]
 }
 
-// WithCreateError sets an error to be returned when Create is called.
-// This is useful for testing error handling in the client.
-func (m *VaultClient) WithCreateError(err error) *VaultClient {
-	m.createErr = err
-	return m
-}
-
-// Create creates a new vault and returns it. If an error is set, it will return that error instead of the vault.
-func (m *VaultClient) Create(_ context.Context, vault *ngrok.VaultCreate) (*ngrok.Vault, error) {
-	if m.createErr != nil {
-		return nil, m.createErr
+func (m *VaultClient) Create(ctx context.Context, vault *ngrok.VaultCreate) (*ngrok.Vault, error) {
+	if m.CreateFn != nil {
+		return m.CreateFn(ctx, vault)
 	}
-	return m.store.CreateVault(vault)
+	return nil, fmt.Errorf("VaultClient.Create: %w", errFnNotConfigured)
 }
 
-// Get retrieves a vault by its ID. If the vault does not exist, it returns an error.
-func (m *VaultClient) Get(_ context.Context, vaultID string) (*ngrok.Vault, error) {
-	return m.store.GetVaultByID(vaultID)
+func (m *VaultClient) Get(ctx context.Context, vaultID string) (*ngrok.Vault, error) {
+	if m.GetFn != nil {
+		return m.GetFn(ctx, vaultID)
+	}
+	return nil, fmt.Errorf("VaultClient.Get: %w", errFnNotConfigured)
 }
 
 func (m *VaultClient) GetSecretsByVault(id string, paging *ngrok.Paging) ngrok.Iter[*ngrok.Secret] {
-	secrets, err := m.store.ListVaultSecrets(id)
-	return NewIter(secrets, err)
-}
-
-// WithListError sets an error to be returned when List is called.
-func (m *VaultClient) WithListError(err error) *VaultClient {
-	m.listErr = err
-	return m
-}
-
-func (m *VaultClient) LastListPaging() *ngrok.FilteredPaging {
-	m.lastListPagingMu.Lock()
-	defer m.lastListPagingMu.Unlock()
-
-	return cloneFilteredPaging(m.lastListPaging)
+	if m.GetSecretsByVaultFn != nil {
+		return m.GetSecretsByVaultFn(id, paging)
+	}
+	return NewIter[*ngrok.Secret](nil, fmt.Errorf("VaultClient.GetSecretsByVault: %w", errFnNotConfigured))
 }
 
-// List returns an iterator over the vaults.
-// If an error is set, it will return that error instead of the vaults.
 func (m *VaultClient) List(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Vault] {
-	m.lastListPagingMu.Lock()
-	defer m.lastListPagingMu.Unlock()
-
-	m.lastListPaging = cloneFilteredPaging(paging)
-	return NewIter(filterVaults(m.store.ListVaults(), paging), m.listErr)
+	if m.ListFn != nil {
+		return m.ListFn(paging)
+	}
+	return NewIter[*ngrok.Vault](nil, fmt.Errorf("VaultClient.List: %w", errFnNotConfigured))
 }
 
 // SecretsClient is a mock implementation of the SecretsClient interface.
-// It allows you to create, update, delete, and list secrets.
-// It can be used to test the client without needing a real ngrok API.
 type SecretsClient struct {
-	store     *Store
-	createErr error
-	updateErr error
-	deleteErr error
-	listErr   error
-
-	lastListPagingMu sync.Mutex
-	lastListPaging   *ngrok.FilteredPaging
-}
-
-// WithCreateError sets an error to be returned when Create is called.
-// This is useful for testing error handling in the client.
-func (m *SecretsClient) WithCreateError(err error) *SecretsClient {
-	m.createErr = err
-	return m
+	CreateFn func(context.Context, *ngrok.SecretCreate) (*ngrok.Secret, error)
+	DeleteFn func(context.Context, string) error
+	GetFn    func(context.Context, string) (*ngrok.Secret, error)
+	ListFn   func(*ngrok.FilteredPaging) ngrok.Iter[*ngrok.Secret]
+	UpdateFn func(context.Context, *ngrok.SecretUpdate) (*ngrok.Secret, error)
 }
 
-// Create creates a new secret and returns it. If an error is set, it will return that error instead of the secret.
-func (m *SecretsClient) Create(_ context.Context, secret *ngrok.SecretCreate) (*ngrok.Secret, error) {
-	if m.createErr != nil {
-		return nil, m.createErr
+func (m *SecretsClient) Create(ctx context.Context, secret *ngrok.SecretCreate) (*ngrok.Secret, error) {
+	if m.CreateFn != nil {
+		return m.CreateFn(ctx, secret)
 	}
-
-	return m.store.CreateSecret(secret)
-}
-
-// WithUpdateError sets an error to be returned when Update is called.
-// This is useful for testing error handling in the client.
-func (m *SecretsClient) WithUpdateError(err error) *SecretsClient {
-	m.updateErr = err
-	return m
+	return nil, fmt.Errorf("SecretsClient.Create: %w", errFnNotConfigured)
 }
 
-// Update updates an existing secret and returns it. If an error is set, it will return that error instead of the secret.
-func (m *SecretsClient) Update(_ context.Context, secret *ngrok.SecretUpdate) (*ngrok.Secret, error) {
-	if m.updateErr != nil {
-		return nil, m.updateErr
+func (m *SecretsClient) Delete(ctx context.Context, secretID string) error {
+	if m.DeleteFn != nil {
+		return m.DeleteFn(ctx, secretID)
 	}
-
-	return m.store.UpdateSecret(secret)
-}
-
-// WithDeleteError sets an error to be returned when Delete is called.
-// This is useful for testing error handling in the client.
-func (m *SecretsClient) WithDeleteError(err error) *SecretsClient {
-	m.deleteErr = err
-	return m
+	return fmt.Errorf("SecretsClient.Delete: %w", errFnNotConfigured)
 }
 
-// Delete deletes a secret by its ID. If an error is set, it will return that error instead of deleting the secret.
-// If the secret does not exist, it returns an error.
-func (m *SecretsClient) Delete(_ context.Context, secretID string) error {
-	if m.deleteErr != nil {
-		return m.deleteErr
+func (m *SecretsClient) Get(ctx context.Context, secretID string) (*ngrok.Secret, error) {
+	if m.GetFn != nil {
+		return m.GetFn(ctx, secretID)
 	}
-	return m.store.DeleteSecret(secretID)
+	return nil, fmt.Errorf("SecretsClient.Get: %w", errFnNotConfigured)
 }
 
-// Get retrieves a secret by its ID. If the secret does not exist, it returns an error.
-func (m *SecretsClient) Get(_ context.Context, secretID string) (*ngrok.Secret, error) {
-	s, _, err := m.store.GetSecretAndVaultByID(secretID) // to check existence
-	return s, err
-}
-
-// WithListError sets an error to be returned when List is called.
-// This is useful for testing error handling in the client.
-func (m *SecretsClient) WithListError(err error) *SecretsClient {
-	m.listErr = err
-	return m
-}
-
-func (m *SecretsClient) LastListPaging() *ngrok.FilteredPaging {
-	m.lastListPagingMu.Lock()
-	defer m.lastListPagingMu.Unlock()
-
-	return cloneFilteredPaging(m.lastListPaging)
-}
-
-// List returns an iterator over the secrets.
-// If an error is set, it will return that error instead of the secrets.
 func (m *SecretsClient) List(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Secret] {
-	m.lastListPagingMu.Lock()
-	defer m.lastListPagingMu.Unlock()
+	if m.ListFn != nil {
+		return m.ListFn(paging)
+	}
+	return NewIter[*ngrok.Secret](nil, fmt.Errorf("SecretsClient.List: %w", errFnNotConfigured))
+}
 
-	m.lastListPaging = cloneFilteredPaging(paging)
-	return NewIter(filterSecrets(m.store.ListSecrets(), paging), m.listErr)
+func (m *SecretsClient) Update(ctx context.Context, secret *ngrok.SecretUpdate) (*ngrok.Secret, error) {
+	if m.UpdateFn != nil {
+		return m.UpdateFn(ctx, secret)
+	}
+	return nil, fmt.Errorf("SecretsClient.Update: %w", errFnNotConfigured)
 }
 
 // Iter is a mock iterator that implements the ngrok.Iter[T] interface.
@@ -604,14 +132,10 @@ type Iter[T any] struct {
 }
 
 func (m *Iter[T]) Next(_ context.Context) bool {
-	// If there is an error, stop iteration
 	if m.err != nil {
 		return false
 	}
-
-	// Increment the index
 	m.n++
-
 	return m.n < len(m.items) && m.n >= 0
 }
 
@@ -633,75 +157,3 @@ func NewIter[T any](items []T, err error) *Iter[T] {
 		n:     -1,
 	}
 }
-
-func cloneFilteredPaging(paging *ngrok.FilteredPaging) *ngrok.FilteredPaging {
-	if paging == nil {
-		return nil
-	}
-
-	clone := *paging
-	if paging.Filter != nil {
-		filter := *paging.Filter
-		clone.Filter = &filter
-	}
-	if paging.BeforeID != nil {
-		beforeID := *paging.BeforeID
-		clone.BeforeID = &beforeID
-	}
-	if paging.Limit != nil {
-		limit := *paging.Limit
-		clone.Limit = &limit
-	}
-	return &clone
-}
-
-func filterVaults(vaults []*ngrok.Vault, paging *ngrok.FilteredPaging) []*ngrok.Vault {
-	if paging == nil || paging.Filter == nil {
-		return vaults
-	}
-
-	name, ok := exactNameFilter(*paging.Filter)
-	if !ok {
-		return vaults
-	}
-
-	filtered := make([]*ngrok.Vault, 0, len(vaults))
-	for _, vault := range vaults {
-		if vault.Name == name {
-			filtered = append(filtered, vault)
-		}
-	}
-	return filtered
-}
-
-func filterSecrets(secrets []*ngrok.Secret, paging *ngrok.FilteredPaging) []*ngrok.Secret {
-	if paging == nil || paging.Filter == nil {
-		return secrets
-	}
-
-	name, ok := exactNameFilter(*paging.Filter)
-	if !ok {
-		return secrets
-	}
-
-	filtered := make([]*ngrok.Secret, 0, len(secrets))
-	for _, secret := range secrets {
-		if secret.Name == name {
-			filtered = append(filtered, secret)
-		}
-	}
-	return filtered
-}
-
-func exactNameFilter(filter string) (string, bool) {
-	rest, ok := strings.CutPrefix(filter, "obj.name == ")
-	if !ok {
-		return "", false
-	}
-
-	name, err := strconv.Unquote(rest)
-	if err != nil {
-		return "", false
-	}
-	return name, true
-}

+ 25 - 20
providers/v1/ngrok/provider_test.go

@@ -100,13 +100,14 @@ var _ = Describe("Provider", func() {
 	Describe("NewClient", func() {
 		var (
 			store            esv1.GenericStore
-			ngrokStore       *fake.Store
 			vaultsClient     *fake.VaultClient
 			namespace        string
 			kubeClient       kubeClient.Client
 			ngrokCredentials *corev1.Secret
 			vaultName        string
 
+			mockVaultsListFn func(*ngrok.FilteredPaging) ngrok.Iter[*ngrok.Vault]
+
 			// Injected errors
 			vaultListErr error
 
@@ -120,17 +121,21 @@ var _ = Describe("Provider", func() {
 			vaultName = "vault-" + fake.GenerateRandomString(5)
 			ngrokCredentials = newNgrokAPICredentials("ngrok-credentials", namespace, "secret-api-key")
 			kubeClient = clientfake.NewClientBuilder().WithObjects(ngrokCredentials).Build()
-			ngrokStore = fake.NewStore()
 			vaultListErr = nil
+
+			mockVaultsListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Vault] {
+				return fake.NewIter([]*ngrok.Vault{}, vaultListErr)
+			}
 		})
 
 		JustBeforeEach(func() {
 			getVaultsClient = func(_ *ngrok.ClientConfig) VaultClient {
-				vaultsClient = ngrokStore.VaultClient().WithListError(vaultListErr)
+				vaultsClient = &fake.VaultClient{}
+				vaultsClient.ListFn = mockVaultsListFn
 				return vaultsClient
 			}
 			getSecretsClient = func(_ *ngrok.ClientConfig) SecretsClient {
-				return ngrokStore.SecretsClient()
+				return &fake.SecretsClient{}
 			}
 			client, err = provider.NewClient(GinkgoT().Context(), store, kubeClient, namespace)
 		})
@@ -186,11 +191,12 @@ var _ = Describe("Provider", func() {
 				})
 
 				When("the vault exists", func() {
+					var listPaging *ngrok.FilteredPaging
 					BeforeEach(func() {
-						_, createErr := ngrokStore.CreateVault(&ngrok.VaultCreate{
-							Name: vaultName,
-						})
-						Expect(createErr).To(BeNil())
+						mockVaultsListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Vault] {
+							listPaging = paging
+							return fake.NewIter([]*ngrok.Vault{{ID: "vault-1", Name: vaultName}}, nil)
+						}
 					})
 
 					It("should not return an error", func() {
@@ -202,10 +208,9 @@ var _ = Describe("Provider", func() {
 					})
 
 					It("should filter vaults by name when listing", func() {
-						Expect(vaultsClient).NotTo(BeNil())
-						Expect(vaultsClient.LastListPaging()).NotTo(BeNil())
-						Expect(vaultsClient.LastListPaging().Filter).NotTo(BeNil())
-						Expect(*vaultsClient.LastListPaging().Filter).To(Equal(fmt.Sprintf("obj.name == %q", vaultName)))
+						Expect(listPaging).NotTo(BeNil())
+						Expect(listPaging.Filter).NotTo(BeNil())
+						Expect(*listPaging.Filter).To(Equal(fmt.Sprintf("obj.name == %q", vaultName)))
 					})
 				})
 
@@ -299,11 +304,12 @@ var _ = Describe("Provider", func() {
 				})
 
 				When("the vault exists", func() {
+					var listPaging *ngrok.FilteredPaging
 					BeforeEach(func() {
-						_, createErr := ngrokStore.CreateVault(&ngrok.VaultCreate{
-							Name: vaultName,
-						})
-						Expect(createErr).To(BeNil())
+						mockVaultsListFn = func(paging *ngrok.FilteredPaging) ngrok.Iter[*ngrok.Vault] {
+							listPaging = paging
+							return fake.NewIter([]*ngrok.Vault{{ID: "vault-1", Name: vaultName}}, nil)
+						}
 					})
 
 					It("should not return an error", func() {
@@ -315,10 +321,9 @@ var _ = Describe("Provider", func() {
 					})
 
 					It("should filter vaults by name when listing", func() {
-						Expect(vaultsClient).NotTo(BeNil())
-						Expect(vaultsClient.LastListPaging()).NotTo(BeNil())
-						Expect(vaultsClient.LastListPaging().Filter).NotTo(BeNil())
-						Expect(*vaultsClient.LastListPaging().Filter).To(Equal(fmt.Sprintf("obj.name == %q", vaultName)))
+						Expect(listPaging).NotTo(BeNil())
+						Expect(listPaging.Filter).NotTo(BeNil())
+						Expect(*listPaging.Filter).To(Equal(fmt.Sprintf("obj.name == %q", vaultName)))
 					})
 				})
 			})