|
@@ -19,6 +19,7 @@ import (
|
|
|
"context"
|
|
"context"
|
|
|
"encoding/json"
|
|
"encoding/json"
|
|
|
"errors"
|
|
"errors"
|
|
|
|
|
+ "fmt"
|
|
|
"io"
|
|
"io"
|
|
|
"os"
|
|
"os"
|
|
|
"testing"
|
|
"testing"
|
|
@@ -26,6 +27,8 @@ import (
|
|
|
"github.com/DelineaXPM/tss-sdk-go/v3/server"
|
|
"github.com/DelineaXPM/tss-sdk-go/v3/server"
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/assert"
|
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
+ corev1 "k8s.io/api/core/v1"
|
|
|
|
|
+ apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
|
|
|
|
|
|
|
esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
|
|
esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
|
|
|
)
|
|
)
|
|
@@ -53,25 +56,111 @@ func (f *fakeAPI) Secret(id int) (*server.Secret, error) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (f *fakeAPI) Secrets(searchText, _ string) ([]server.Secret, error) {
|
|
func (f *fakeAPI) Secrets(searchText, _ string) ([]server.Secret, error) {
|
|
|
- secret := make([]server.Secret, 1)
|
|
|
|
|
|
|
+ // Match real SDK behavior: return ([]Secret{}, nil) for zero matches,
|
|
|
|
|
+ // NOT (nil, errNotFound). The real SDK's searchResources returns an empty
|
|
|
|
|
+ // SearchResult.Records slice and make([]Secret, 0).
|
|
|
|
|
+ var secrets []server.Secret
|
|
|
for _, s := range f.secrets {
|
|
for _, s := range f.secrets {
|
|
|
if s.Name == searchText {
|
|
if s.Name == searchText {
|
|
|
- secret[0] = *s
|
|
|
|
|
- return secret, nil
|
|
|
|
|
|
|
+ secrets = append(secrets, *s)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- return nil, errNotFound
|
|
|
|
|
|
|
+ if secrets == nil {
|
|
|
|
|
+ secrets = []server.Secret{}
|
|
|
|
|
+ }
|
|
|
|
|
+ return secrets, nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (f *fakeAPI) SecretByPath(path string) (*server.Secret, error) {
|
|
func (f *fakeAPI) SecretByPath(path string) (*server.Secret, error) {
|
|
|
for _, s := range f.secrets {
|
|
for _, s := range f.secrets {
|
|
|
- if "/"+s.Name == path {
|
|
|
|
|
|
|
+ if "/"+s.Name == path || s.Name == path {
|
|
|
return s, nil
|
|
return s, nil
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
return nil, errNotFound
|
|
return nil, errNotFound
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// CreateSecret is a mock implementation of the Secret Server API CreateSecret method.
|
|
|
|
|
+// It returns a predefined secret based on the SecretTemplateID provided.
|
|
|
|
|
+func (f *fakeAPI) CreateSecret(secret server.Secret) (*server.Secret, error) {
|
|
|
|
|
+ if secret.Name == "simulate-create-error" {
|
|
|
|
|
+ return nil, errors.New("simulated create error")
|
|
|
|
|
+ }
|
|
|
|
|
+ secret.ID = len(f.secrets) + 10000
|
|
|
|
|
+
|
|
|
|
|
+ // Simulate populating FieldName and Slug based on FieldID
|
|
|
|
|
+ template, _ := f.SecretTemplate(secret.SecretTemplateID)
|
|
|
|
|
+ if template != nil {
|
|
|
|
|
+ for i, field := range secret.Fields {
|
|
|
|
|
+ for _, tField := range template.Fields {
|
|
|
|
|
+ if tField.SecretTemplateFieldID == field.FieldID {
|
|
|
|
|
+ secret.Fields[i].Slug = tField.FieldSlugName
|
|
|
|
|
+ secret.Fields[i].FieldName = tField.Name
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ f.secrets = append(f.secrets, &secret)
|
|
|
|
|
+ return &secret, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// UpdateSecret is a mock implementation of the Secret Server API UpdateSecret method.
|
|
|
|
|
+// It returns an error if a predefined test condition is met, otherwise it simulates success.
|
|
|
|
|
+func (f *fakeAPI) UpdateSecret(secret server.Secret) (*server.Secret, error) {
|
|
|
|
|
+ for i, s := range f.secrets {
|
|
|
|
|
+ if s.ID == secret.ID {
|
|
|
|
|
+ f.secrets[i] = &secret
|
|
|
|
|
+ return &secret, nil
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return nil, errNotFound
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// DeleteSecret is a mock implementation of the Secret Server API DeleteSecret method.
|
|
|
|
|
+// It returns an error if the id corresponds to a simulated failure case.
|
|
|
|
|
+func (f *fakeAPI) DeleteSecret(id int) error {
|
|
|
|
|
+ if id == 9999 {
|
|
|
|
|
+ return errors.New("simulated backend deletion error")
|
|
|
|
|
+ }
|
|
|
|
|
+ for i, s := range f.secrets {
|
|
|
|
|
+ if s.ID == id {
|
|
|
|
|
+ f.secrets = append(f.secrets[:i], f.secrets[i+1:]...)
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return errNotFound
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// SecretTemplate is a mock implementation of the Secret Server API SecretTemplate method.
|
|
|
|
|
+// It returns a predefined template or an error based on the requested id.
|
|
|
|
|
+func (f *fakeAPI) SecretTemplate(id int) (*server.SecretTemplate, error) {
|
|
|
|
|
+ if id == 999 {
|
|
|
|
|
+ return nil, errors.New("template not found")
|
|
|
|
|
+ }
|
|
|
|
|
+ return &server.SecretTemplate{
|
|
|
|
|
+ ID: id,
|
|
|
|
|
+ Name: "Test Template",
|
|
|
|
|
+ Fields: []server.SecretTemplateField{
|
|
|
|
|
+ {
|
|
|
|
|
+ SecretTemplateFieldID: 1,
|
|
|
|
|
+ FieldSlugName: "username",
|
|
|
|
|
+ Name: "Username",
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ SecretTemplateFieldID: 2,
|
|
|
|
|
+ FieldSlugName: "password",
|
|
|
|
|
+ Name: "Password",
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ SecretTemplateFieldID: 3,
|
|
|
|
|
+ FieldSlugName: "notes",
|
|
|
|
|
+ Name: "Notes",
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ }, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
func createSecret(id int, itemValue string) (*server.Secret, error) {
|
|
func createSecret(id int, itemValue string) (*server.Secret, error) {
|
|
|
s, err := jsonData()
|
|
s, err := jsonData()
|
|
|
if err != nil {
|
|
if err != nil {
|
|
@@ -175,7 +264,22 @@ func newTestClient(t *testing.T) esv1.SecretsClient {
|
|
|
s6, err := createSecret(6000, "{ \"user\": \"betaTest\", \"password\": \"badPassword\" }")
|
|
s6, err := createSecret(6000, "{ \"user\": \"betaTest\", \"password\": \"badPassword\" }")
|
|
|
require.NoError(t, err)
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
- secrets = append(secrets, s6, createNilFieldsSecret(7000), createEmptyFieldsSecret(8000), createTestFolderSecret(9000, 4))
|
|
|
|
|
|
|
+ secrets = append(secrets, s6, createNilFieldsSecret(7000), createEmptyFieldsSecret(8000), createTestFolderSecret(9000, 4), createTestFolderSecret(9001, 5))
|
|
|
|
|
+
|
|
|
|
|
+ // Create a secret for path-based test
|
|
|
|
|
+ pathSecret := &server.Secret{
|
|
|
|
|
+ ID: 9002,
|
|
|
|
|
+ Name: "/some/path/secret",
|
|
|
|
|
+ FolderID: 6,
|
|
|
|
|
+ Fields: []server.SecretField{
|
|
|
|
|
+ {FieldName: "Password", Slug: "password", ItemValue: "old_path_value"},
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+ secrets = append(secrets, pathSecret)
|
|
|
|
|
+
|
|
|
|
|
+ s9999, err := createSecret(9999, "simulated error")
|
|
|
|
|
+ require.NoError(t, err)
|
|
|
|
|
+ secrets = append(secrets, s9999)
|
|
|
|
|
|
|
|
return &client{
|
|
return &client{
|
|
|
api: &fakeAPI{
|
|
api: &fakeAPI{
|
|
@@ -199,16 +303,17 @@ func TestGetSecretSecretServer(t *testing.T) {
|
|
|
require.NoError(t, err)
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
testCases := map[string]struct {
|
|
testCases := map[string]struct {
|
|
|
- ref esv1.ExternalSecretDataRemoteRef
|
|
|
|
|
- want []byte
|
|
|
|
|
- err error
|
|
|
|
|
|
|
+ ref esv1.ExternalSecretDataRemoteRef
|
|
|
|
|
+ want []byte
|
|
|
|
|
+ err error
|
|
|
|
|
+ errMsg string // when set, asserts Contains(err.Error(), errMsg) instead of exact error match
|
|
|
}{
|
|
}{
|
|
|
"incorrect key returns nil and error": {
|
|
"incorrect key returns nil and error": {
|
|
|
ref: esv1.ExternalSecretDataRemoteRef{
|
|
ref: esv1.ExternalSecretDataRemoteRef{
|
|
|
Key: "0",
|
|
Key: "0",
|
|
|
},
|
|
},
|
|
|
- want: []byte(nil),
|
|
|
|
|
- err: errNotFound,
|
|
|
|
|
|
|
+ want: []byte(nil),
|
|
|
|
|
+ errMsg: errMsgNoMatchingSecrets,
|
|
|
},
|
|
},
|
|
|
"key = 'secret name' and user property returns a single value": {
|
|
"key = 'secret name' and user property returns a single value": {
|
|
|
ref: esv1.ExternalSecretDataRemoteRef{
|
|
ref: esv1.ExternalSecretDataRemoteRef{
|
|
@@ -278,19 +383,19 @@ func TestGetSecretSecretServer(t *testing.T) {
|
|
|
want: []byte(nil),
|
|
want: []byte(nil),
|
|
|
err: esv1.NoSecretError{},
|
|
err: esv1.NoSecretError{},
|
|
|
},
|
|
},
|
|
|
- "Secret from code: valid ItemValue but nil Fields returns nil": {
|
|
|
|
|
|
|
+ "Secret from code: nil Fields returns error": {
|
|
|
ref: esv1.ExternalSecretDataRemoteRef{
|
|
ref: esv1.ExternalSecretDataRemoteRef{
|
|
|
Key: "7000",
|
|
Key: "7000",
|
|
|
},
|
|
},
|
|
|
- want: []byte(nil),
|
|
|
|
|
|
|
+ want: []byte(nil),
|
|
|
|
|
+ errMsg: "secret contains no fields",
|
|
|
},
|
|
},
|
|
|
- "Secret from code: empty Fields returns noSecretError": {
|
|
|
|
|
|
|
+ "Secret from code: empty Fields returns error": {
|
|
|
ref: esv1.ExternalSecretDataRemoteRef{
|
|
ref: esv1.ExternalSecretDataRemoteRef{
|
|
|
- Key: "8000",
|
|
|
|
|
- Property: "missing",
|
|
|
|
|
|
|
+ Key: "8000",
|
|
|
},
|
|
},
|
|
|
- want: []byte(nil),
|
|
|
|
|
- err: esv1.NoSecretError{},
|
|
|
|
|
|
|
+ want: []byte(nil),
|
|
|
|
|
+ errMsg: "secret contains no fields",
|
|
|
},
|
|
},
|
|
|
"Secret from code: 'name' and password slug returns a single value": {
|
|
"Secret from code: 'name' and password slug returns a single value": {
|
|
|
ref: esv1.ExternalSecretDataRemoteRef{
|
|
ref: esv1.ExternalSecretDataRemoteRef{
|
|
@@ -299,13 +404,13 @@ func TestGetSecretSecretServer(t *testing.T) {
|
|
|
},
|
|
},
|
|
|
want: []byte(`passwordvalue`),
|
|
want: []byte(`passwordvalue`),
|
|
|
},
|
|
},
|
|
|
- "Secret from code: 'name' not found and password slug returns error": {
|
|
|
|
|
|
|
+ "Secret from code: 'name' not found returns unable to retrieve secret error": {
|
|
|
ref: esv1.ExternalSecretDataRemoteRef{
|
|
ref: esv1.ExternalSecretDataRemoteRef{
|
|
|
Key: "Secretnameerror",
|
|
Key: "Secretnameerror",
|
|
|
Property: "password",
|
|
Property: "password",
|
|
|
},
|
|
},
|
|
|
- want: []byte(nil),
|
|
|
|
|
- err: errNotFound,
|
|
|
|
|
|
|
+ want: []byte(nil),
|
|
|
|
|
+ errMsg: errMsgNoMatchingSecrets,
|
|
|
},
|
|
},
|
|
|
"Secret from code: 'name' found and non-existent attribute slug returns noSecretError": {
|
|
"Secret from code: 'name' found and non-existent attribute slug returns noSecretError": {
|
|
|
ref: esv1.ExternalSecretDataRemoteRef{
|
|
ref: esv1.ExternalSecretDataRemoteRef{
|
|
@@ -325,8 +430,8 @@ func TestGetSecretSecretServer(t *testing.T) {
|
|
|
ref: esv1.ExternalSecretDataRemoteRef{
|
|
ref: esv1.ExternalSecretDataRemoteRef{
|
|
|
Key: "/invalid/secret/path",
|
|
Key: "/invalid/secret/path",
|
|
|
},
|
|
},
|
|
|
- want: []byte(nil),
|
|
|
|
|
- err: errNotFound,
|
|
|
|
|
|
|
+ want: []byte(nil),
|
|
|
|
|
+ errMsg: "not found",
|
|
|
},
|
|
},
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -334,28 +439,31 @@ func TestGetSecretSecretServer(t *testing.T) {
|
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Run(name, func(t *testing.T) {
|
|
|
got, err := c.GetSecret(ctx, tc.ref)
|
|
got, err := c.GetSecret(ctx, tc.ref)
|
|
|
|
|
|
|
|
- if tc.err == nil {
|
|
|
|
|
|
|
+ if tc.err == nil && tc.errMsg == "" {
|
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, err)
|
|
|
assert.Equal(t, tc.want, got)
|
|
assert.Equal(t, tc.want, got)
|
|
|
} else {
|
|
} else {
|
|
|
assert.Nil(t, got)
|
|
assert.Nil(t, got)
|
|
|
- assert.ErrorIs(t, err, tc.err)
|
|
|
|
|
- assert.Equal(t, tc.err, err)
|
|
|
|
|
|
|
+ if tc.errMsg != "" {
|
|
|
|
|
+ assert.ErrorContains(t, err, tc.errMsg)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ assert.ErrorIs(t, err, tc.err)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// TestGetSecretJSONMarshalFailure tests GetSecret when json.Marshal fails.
|
|
|
|
|
-func TestGetSecretJSONMarshalFailure(t *testing.T) {
|
|
|
|
|
|
|
+// TestGetSecretWithInvalidUTF8ItemValue tests GetSecret with invalid UTF-8 in ItemValue.
|
|
|
|
|
+// json.Marshal in Go handles invalid UTF-8 strings without error, so this verifies
|
|
|
|
|
+// that GetSecret succeeds in this edge case.
|
|
|
|
|
+func TestGetSecretWithInvalidUTF8ItemValue(t *testing.T) {
|
|
|
ctx := t.Context()
|
|
ctx := t.Context()
|
|
|
|
|
|
|
|
bad := &server.Secret{
|
|
bad := &server.Secret{
|
|
|
ID: 0,
|
|
ID: 0,
|
|
|
Fields: []server.SecretField{},
|
|
Fields: []server.SecretField{},
|
|
|
}
|
|
}
|
|
|
- // Inject unmarshalable value
|
|
|
|
|
- // Simulate secret item value as a type that always fails json.Marshal
|
|
|
|
|
c := &client{
|
|
c := &client{
|
|
|
api: &fakeAPI{
|
|
api: &fakeAPI{
|
|
|
secrets: []*server.Secret{bad},
|
|
secrets: []*server.Secret{bad},
|
|
@@ -364,13 +472,12 @@ func TestGetSecretJSONMarshalFailure(t *testing.T) {
|
|
|
bad.Fields = []server.SecretField{
|
|
bad.Fields = []server.SecretField{
|
|
|
{
|
|
{
|
|
|
FieldName: "Foo",
|
|
FieldName: "Foo",
|
|
|
- ItemValue: string([]byte{0xff, 0xfe}), // invalid UTF-8 → forces marshal failure
|
|
|
|
|
|
|
+ ItemValue: string([]byte{0xff, 0xfe}), // invalid UTF-8
|
|
|
},
|
|
},
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // GetSecret calls getSecret which returns the secret, so no error expected
|
|
|
|
|
|
|
+ // GetSecret with no property returns the full JSON; json.Marshal handles invalid UTF-8.
|
|
|
_, err := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{Key: "0"})
|
|
_, err := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{Key: "0"})
|
|
|
- // The secret is found but ItemValue is invalid; fail-fast if error
|
|
|
|
|
require.NoError(t, err)
|
|
require.NoError(t, err)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -384,8 +491,9 @@ func TestGetSecretEmptySecretsList(t *testing.T) {
|
|
|
|
|
|
|
|
_, err := c.getSecret(ctx, esv1.ExternalSecretDataRemoteRef{Key: "nonexistent"})
|
|
_, err := c.getSecret(ctx, esv1.ExternalSecretDataRemoteRef{Key: "nonexistent"})
|
|
|
assert.Error(t, err)
|
|
assert.Error(t, err)
|
|
|
- // When secret not found, the fakeAPI returns errNotFound
|
|
|
|
|
- assert.Contains(t, err.Error(), "not found")
|
|
|
|
|
|
|
+ // fakeAPI.Secrets now returns ([]Secret{}, nil) for zero matches (matching real SDK),
|
|
|
|
|
+ // so getSecretByName returns errMsgNoMatchingSecrets.
|
|
|
|
|
+ assert.Contains(t, err.Error(), errMsgNoMatchingSecrets)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// TestGetSecretWithVersion tests that specifying a version returns an error.
|
|
// TestGetSecretWithVersion tests that specifying a version returns an error.
|
|
@@ -419,15 +527,208 @@ func TestGetSecretWithVersion(t *testing.T) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// fakePushSecretData implements esv1.PushSecretData for testing.
|
|
|
|
|
+type fakePushSecretData struct {
|
|
|
|
|
+ remoteKey string
|
|
|
|
|
+ property string
|
|
|
|
|
+ secretKey string
|
|
|
|
|
+ metadata *apiextensionsv1.JSON
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// GetRemoteKey returns the remote key for the fake push secret data.
|
|
|
|
|
+func (f fakePushSecretData) GetRemoteKey() string { return f.remoteKey }
|
|
|
|
|
+
|
|
|
|
|
+// GetProperty returns the property for the fake push secret data.
|
|
|
|
|
+func (f fakePushSecretData) GetProperty() string { return f.property }
|
|
|
|
|
+
|
|
|
|
|
+// GetSecretKey returns the secret key for the fake push secret data.
|
|
|
|
|
+func (f fakePushSecretData) GetSecretKey() string { return f.secretKey }
|
|
|
|
|
+
|
|
|
|
|
+// GetMetadata returns the metadata for the fake push secret data.
|
|
|
|
|
+func (f fakePushSecretData) GetMetadata() *apiextensionsv1.JSON { return f.metadata }
|
|
|
|
|
+
|
|
|
|
|
+// fakePushSecretRemoteRef implements esv1.PushSecretRemoteRef for testing.
|
|
|
|
|
+type fakePushSecretRemoteRef struct {
|
|
|
|
|
+ remoteKey string
|
|
|
|
|
+ property string
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// GetRemoteKey returns the remote key for the fake remote ref.
|
|
|
|
|
+func (f fakePushSecretRemoteRef) GetRemoteKey() string { return f.remoteKey }
|
|
|
|
|
+
|
|
|
|
|
+// GetProperty returns the property for the fake remote ref.
|
|
|
|
|
+func (f fakePushSecretRemoteRef) GetProperty() string { return f.property }
|
|
|
|
|
+
|
|
|
// TestPushSecret tests the PushSecret functionality.
|
|
// TestPushSecret tests the PushSecret functionality.
|
|
|
func TestPushSecret(t *testing.T) {
|
|
func TestPushSecret(t *testing.T) {
|
|
|
ctx := context.Background()
|
|
ctx := context.Background()
|
|
|
c := newTestClient(t)
|
|
c := newTestClient(t)
|
|
|
|
|
|
|
|
- var data esv1.PushSecretData
|
|
|
|
|
- err := c.PushSecret(ctx, nil, data)
|
|
|
|
|
|
|
+ secret := &corev1.Secret{
|
|
|
|
|
+ Data: map[string][]byte{
|
|
|
|
|
+ "my-key": []byte("my-value"),
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ metadataJSON := apiextensionsv1.JSON{
|
|
|
|
|
+ Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 1}}`),
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Create a new secret
|
|
|
|
|
+ data := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "new-secret",
|
|
|
|
|
+ property: "username",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ metadata: &metadataJSON,
|
|
|
|
|
+ }
|
|
|
|
|
+ err := c.PushSecret(ctx, secret, data)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+
|
|
|
|
|
+ // Verify the secret was created
|
|
|
|
|
+ createdSecret, _ := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{Key: "new-secret", Property: "username"})
|
|
|
|
|
+ assert.Equal(t, []byte("my-value"), createdSecret)
|
|
|
|
|
+
|
|
|
|
|
+ // Create a new secret with path-like key and folderId
|
|
|
|
|
+ dataPathCreate := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "/some/new/path/secretname",
|
|
|
|
|
+ property: "username",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ metadata: &metadataJSON,
|
|
|
|
|
+ }
|
|
|
|
|
+ err = c.PushSecret(ctx, secret, dataPathCreate)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+
|
|
|
|
|
+ // verify that the created secret has just the basename "secretname"
|
|
|
|
|
+ // and since it's the 10th secret created by fakeAPI, its ID would be 10000 + len(secrets)
|
|
|
|
|
+ foundSecrets, _ := c.(*client).api.Secrets("secretname", "Name")
|
|
|
|
|
+ assert.Len(t, foundSecrets, 1)
|
|
|
|
|
+ assert.Equal(t, "secretname", foundSecrets[0].Name)
|
|
|
|
|
+ assert.Equal(t, 1, foundSecrets[0].FolderID)
|
|
|
|
|
+
|
|
|
|
|
+ // Update an existing secret
|
|
|
|
|
+ dataUpdate := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "4000",
|
|
|
|
|
+ property: "password",
|
|
|
|
|
+ secretKey: "my-key", // "my-value" will replace the badPassword
|
|
|
|
|
+ }
|
|
|
|
|
+ err = c.PushSecret(ctx, secret, dataUpdate)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+
|
|
|
|
|
+ // Verify update
|
|
|
|
|
+ updatedSecret, _ := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{Key: "4000", Property: "password"})
|
|
|
|
|
+ assert.Equal(t, []byte("my-value"), updatedSecret)
|
|
|
|
|
+
|
|
|
|
|
+ // Missing metadata for new secret
|
|
|
|
|
+ dataMissingMeta := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "new-secret-no-meta",
|
|
|
|
|
+ property: "username",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ metadata: nil,
|
|
|
|
|
+ }
|
|
|
|
|
+ err = c.PushSecret(ctx, secret, dataMissingMeta)
|
|
|
|
|
+ assert.Error(t, err)
|
|
|
|
|
+ assert.Contains(t, err.Error(), "folderId and secretTemplateId must be provided in metadata to create a new secret")
|
|
|
|
|
+
|
|
|
|
|
+ // Invalid secretTemplateId in metadata
|
|
|
|
|
+ invalidMetadataJSON := apiextensionsv1.JSON{
|
|
|
|
|
+ Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 999}}`), // non-existent template
|
|
|
|
|
+ }
|
|
|
|
|
+ dataInvalidMeta := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "new-secret-invalid-meta",
|
|
|
|
|
+ property: "username",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ metadata: &invalidMetadataJSON,
|
|
|
|
|
+ }
|
|
|
|
|
+ err = c.PushSecret(ctx, secret, dataInvalidMeta)
|
|
|
|
|
+ assert.Error(t, err)
|
|
|
|
|
+ assert.Contains(t, err.Error(), "failed to get secret template")
|
|
|
|
|
+
|
|
|
|
|
+ // Simulate create error
|
|
|
|
|
+ // Requires modifying fakeAPI to return an error when Name == "simulate-create-error"
|
|
|
|
|
+ dataCreateError := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "simulate-create-error",
|
|
|
|
|
+ property: "username",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ metadata: &metadataJSON,
|
|
|
|
|
+ }
|
|
|
|
|
+ err = c.PushSecret(ctx, secret, dataCreateError)
|
|
|
assert.Error(t, err)
|
|
assert.Error(t, err)
|
|
|
- assert.Contains(t, err.Error(), "not supported")
|
|
|
|
|
|
|
+ assert.Contains(t, err.Error(), "failed to create secret")
|
|
|
|
|
+
|
|
|
|
|
+ // Update with non-existent property
|
|
|
|
|
+ dataUpdateInvalidProp := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "4000",
|
|
|
|
|
+ property: "non-existent-property",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ }
|
|
|
|
|
+ err = c.PushSecret(ctx, secret, dataUpdateInvalidProp)
|
|
|
|
|
+ assert.Error(t, err)
|
|
|
|
|
+ assert.Contains(t, err.Error(), "field non-existent-property not found in secret")
|
|
|
|
|
+
|
|
|
|
|
+ // Update duplicate-named secret in specific folder (ID 9001 in FolderID 5)
|
|
|
|
|
+ metadataFolder5 := apiextensionsv1.JSON{
|
|
|
|
|
+ Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 5, "secretTemplateId": 1}}`),
|
|
|
|
|
+ }
|
|
|
|
|
+ dataFolderUpdate := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "FolderSecretname",
|
|
|
|
|
+ property: "password",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ metadata: &metadataFolder5,
|
|
|
|
|
+ }
|
|
|
|
|
+ err = c.PushSecret(ctx, secret, dataFolderUpdate)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+
|
|
|
|
|
+ // Verify only the secret in folder 5 was updated
|
|
|
|
|
+ s9001, _ := c.(*client).api.Secret(9001)
|
|
|
|
|
+ s9000, _ := c.(*client).api.Secret(9000)
|
|
|
|
|
+ // Check the password field
|
|
|
|
|
+ var s9001PW, s9000PW string
|
|
|
|
|
+ for _, f := range s9001.Fields {
|
|
|
|
|
+ if f.Slug == passwordSlug {
|
|
|
|
|
+ s9001PW = f.ItemValue
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, f := range s9000.Fields {
|
|
|
|
|
+ if f.Slug == passwordSlug {
|
|
|
|
|
+ s9000PW = f.ItemValue
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ assert.Equal(t, "my-value", s9001PW)
|
|
|
|
|
+ assert.Equal(t, "passwordvalue", s9000PW) // Unchanged
|
|
|
|
|
+
|
|
|
|
|
+ // Update path-based key secret
|
|
|
|
|
+ dataPathUpdate := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "/some/path/secret",
|
|
|
|
|
+ property: "password",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ }
|
|
|
|
|
+ err = c.PushSecret(ctx, secret, dataPathUpdate)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+
|
|
|
|
|
+ sPath, _ := c.(*client).api.Secret(9002)
|
|
|
|
|
+ var sPathPW string
|
|
|
|
|
+ for _, f := range sPath.Fields {
|
|
|
|
|
+ if f.Slug == passwordSlug {
|
|
|
|
|
+ sPathPW = f.ItemValue
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ assert.Equal(t, "my-value", sPathPW)
|
|
|
|
|
+
|
|
|
|
|
+ // Push invalid UTF-8 secret
|
|
|
|
|
+ invalidUtf8Secret := &corev1.Secret{
|
|
|
|
|
+ Data: map[string][]byte{
|
|
|
|
|
+ "invalid-utf8": {0xff, 0xfe, 0xfd},
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+ dataInvalidUtf8 := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "new-secret-utf8",
|
|
|
|
|
+ property: "username",
|
|
|
|
|
+ secretKey: "invalid-utf8",
|
|
|
|
|
+ metadata: &metadataJSON,
|
|
|
|
|
+ }
|
|
|
|
|
+ err = c.PushSecret(ctx, invalidUtf8Secret, dataInvalidUtf8)
|
|
|
|
|
+ assert.Error(t, err)
|
|
|
|
|
+ assert.Contains(t, err.Error(), "secret value is not valid UTF-8")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// TestDeleteSecret tests the DeleteSecret functionality.
|
|
// TestDeleteSecret tests the DeleteSecret functionality.
|
|
@@ -435,10 +736,68 @@ func TestDeleteSecret(t *testing.T) {
|
|
|
ctx := context.Background()
|
|
ctx := context.Background()
|
|
|
c := newTestClient(t)
|
|
c := newTestClient(t)
|
|
|
|
|
|
|
|
- var data esv1.PushSecretRemoteRef
|
|
|
|
|
- err := c.DeleteSecret(ctx, data)
|
|
|
|
|
|
|
+ ref := fakePushSecretRemoteRef{
|
|
|
|
|
+ remoteKey: "1000",
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Should exist initially
|
|
|
|
|
+ exists, err := c.SecretExists(ctx, ref)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+ assert.True(t, exists)
|
|
|
|
|
+
|
|
|
|
|
+ // Delete it
|
|
|
|
|
+ err = c.DeleteSecret(ctx, ref)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+
|
|
|
|
|
+ // Should not exist now
|
|
|
|
|
+ exists, err = c.SecretExists(ctx, ref)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+ assert.False(t, exists)
|
|
|
|
|
+
|
|
|
|
|
+ // Test idempotency: delete again should not error
|
|
|
|
|
+ err = c.DeleteSecret(ctx, ref)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+
|
|
|
|
|
+ // Test path-based key deletion
|
|
|
|
|
+ pathRef := fakePushSecretRemoteRef{
|
|
|
|
|
+ remoteKey: "/some/path/secret",
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ exists, err = c.SecretExists(ctx, pathRef)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+ assert.True(t, exists)
|
|
|
|
|
+
|
|
|
|
|
+ err = c.DeleteSecret(ctx, pathRef)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+
|
|
|
|
|
+ exists, err = c.SecretExists(ctx, pathRef)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+ assert.False(t, exists)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestDeleteSecret_Error tests that an error from the backend during DeleteSecret is propagated.
|
|
|
|
|
+func TestDeleteSecret_Error(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ c := newTestClient(t)
|
|
|
|
|
+
|
|
|
|
|
+ ref := fakePushSecretRemoteRef{
|
|
|
|
|
+ remoteKey: "9999",
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Should exist initially
|
|
|
|
|
+ exists, err := c.SecretExists(ctx, ref)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+ assert.True(t, exists)
|
|
|
|
|
+
|
|
|
|
|
+ // Attempt to delete it, expecting an error
|
|
|
|
|
+ err = c.DeleteSecret(ctx, ref)
|
|
|
assert.Error(t, err)
|
|
assert.Error(t, err)
|
|
|
- assert.Contains(t, err.Error(), "not supported")
|
|
|
|
|
|
|
+ assert.Contains(t, err.Error(), "failed to delete secret")
|
|
|
|
|
+
|
|
|
|
|
+ // Verify it still exists
|
|
|
|
|
+ exists, err = c.SecretExists(ctx, ref)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+ assert.True(t, exists)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// TestSecretExists tests the SecretExists functionality.
|
|
// TestSecretExists tests the SecretExists functionality.
|
|
@@ -446,11 +805,34 @@ func TestSecretExists(t *testing.T) {
|
|
|
ctx := context.Background()
|
|
ctx := context.Background()
|
|
|
c := newTestClient(t)
|
|
c := newTestClient(t)
|
|
|
|
|
|
|
|
- var data esv1.PushSecretRemoteRef
|
|
|
|
|
- exists, err := c.SecretExists(ctx, data)
|
|
|
|
|
- assert.False(t, exists)
|
|
|
|
|
- assert.Error(t, err)
|
|
|
|
|
- assert.Contains(t, err.Error(), "not implemented")
|
|
|
|
|
|
|
+ testCases := map[string]struct {
|
|
|
|
|
+ ref esv1.PushSecretRemoteRef
|
|
|
|
|
+ want bool
|
|
|
|
|
+ wantErr bool
|
|
|
|
|
+ }{
|
|
|
|
|
+ "existing secret": {
|
|
|
|
|
+ ref: fakePushSecretRemoteRef{remoteKey: "1000"},
|
|
|
|
|
+ want: true,
|
|
|
|
|
+ wantErr: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ "non-existing secret": {
|
|
|
|
|
+ ref: fakePushSecretRemoteRef{remoteKey: "does-not-exist"},
|
|
|
|
|
+ want: false,
|
|
|
|
|
+ wantErr: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for name, tc := range testCases {
|
|
|
|
|
+ t.Run(name, func(t *testing.T) {
|
|
|
|
|
+ got, err := c.SecretExists(ctx, tc.ref)
|
|
|
|
|
+ if tc.wantErr {
|
|
|
|
|
+ assert.Error(t, err)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+ assert.Equal(t, tc.want, got)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// TestValidate tests the Validate functionality.
|
|
// TestValidate tests the Validate functionality.
|
|
@@ -492,6 +874,9 @@ func TestGetSecretMap(t *testing.T) {
|
|
|
},
|
|
},
|
|
|
wantErr: false,
|
|
wantErr: false,
|
|
|
},
|
|
},
|
|
|
|
|
+ // The following test case expects an error because the secret with Key "9999"
|
|
|
|
|
+ // contains invalid JSON ("simulated error") which causes unmarshalling to fail
|
|
|
|
|
+ // in GetSecretMap, rather than because the secret is missing.
|
|
|
"error when secret not found": {
|
|
"error when secret not found": {
|
|
|
ref: esv1.ExternalSecretDataRemoteRef{
|
|
ref: esv1.ExternalSecretDataRemoteRef{
|
|
|
Key: "9999",
|
|
Key: "9999",
|
|
@@ -554,15 +939,17 @@ func TestGetSecretMapInvalidJSON(t *testing.T) {
|
|
|
assert.Error(t, err)
|
|
assert.Error(t, err)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// TestGetSecretMapGetByteValueError tests GetSecretMap when GetByteValue fails.
|
|
|
|
|
-func TestGetSecretMapGetByteValueError(t *testing.T) {
|
|
|
|
|
|
|
+// TestGetSecretMapValidJSON tests GetSecretMap with valid JSON data succeeds.
|
|
|
|
|
+func TestGetSecretMapValidJSON(t *testing.T) {
|
|
|
ctx := context.Background()
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
c := newTestClient(t)
|
|
c := newTestClient(t)
|
|
|
|
|
|
|
|
// GetSecretMap with valid JSON should succeed
|
|
// GetSecretMap with valid JSON should succeed
|
|
|
- _, err := c.GetSecretMap(ctx, esv1.ExternalSecretDataRemoteRef{Key: "1000"})
|
|
|
|
|
|
|
+ result, err := c.GetSecretMap(ctx, esv1.ExternalSecretDataRemoteRef{Key: "1000"})
|
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, err)
|
|
|
|
|
+ assert.NotNil(t, result)
|
|
|
|
|
+ assert.Equal(t, []byte("robertOppenheimer"), result["user"])
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// TestClose tests the Close functionality.
|
|
// TestClose tests the Close functionality.
|
|
@@ -589,12 +976,12 @@ func TestGetAllSecrets(t *testing.T) {
|
|
|
Path: new("some-path"),
|
|
Path: new("some-path"),
|
|
|
},
|
|
},
|
|
|
wantErr: true,
|
|
wantErr: true,
|
|
|
- errMsg: "getting all secrets is not supported by Delinea Secret Server at this time",
|
|
|
|
|
|
|
+ errMsg: "getting all secrets is not supported by Delinea Secret Server",
|
|
|
},
|
|
},
|
|
|
"returns error with nil path": {
|
|
"returns error with nil path": {
|
|
|
ref: esv1.ExternalSecretFind{},
|
|
ref: esv1.ExternalSecretFind{},
|
|
|
wantErr: true,
|
|
wantErr: true,
|
|
|
- errMsg: "getting all secrets is not supported by Delinea Secret Server at this time",
|
|
|
|
|
|
|
+ errMsg: "getting all secrets is not supported by Delinea Secret Server",
|
|
|
},
|
|
},
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -608,3 +995,925 @@ func TestGetAllSecrets(t *testing.T) {
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// TestIsNotFoundError tests the isNotFoundError function with various error formats.
|
|
|
|
|
+func TestIsNotFoundError(t *testing.T) {
|
|
|
|
|
+ testCases := map[string]struct {
|
|
|
|
|
+ err error
|
|
|
|
|
+ want bool
|
|
|
|
|
+ }{
|
|
|
|
|
+ "nil error": {
|
|
|
|
|
+ err: nil,
|
|
|
|
|
+ want: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ "exact lowercase not found": {
|
|
|
|
|
+ err: errors.New("not found"),
|
|
|
|
|
+ want: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ "SDK HTTP 404 format": {
|
|
|
|
|
+ err: errors.New("404 Not Found: no secret was found"),
|
|
|
|
|
+ want: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ "SDK HTTP 404 with empty body": {
|
|
|
|
|
+ err: errors.New("404 Not Found: "),
|
|
|
|
|
+ want: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ "unable to retrieve secret at this time": {
|
|
|
|
|
+ err: errors.New("unable to retrieve secret at this time"),
|
|
|
|
|
+ want: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ "unrelated error": {
|
|
|
|
|
+ err: errors.New("connection refused"),
|
|
|
|
|
+ want: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ "field not found in secret (false positive excluded)": {
|
|
|
|
|
+ // This error from updateSecret should NOT be treated as not-found.
|
|
|
|
|
+ err: fmt.Errorf("field password not found in secret"),
|
|
|
|
|
+ want: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ "field not found in secret template (false positive excluded)": {
|
|
|
|
|
+ // This error from createSecret should NOT be treated as not-found.
|
|
|
|
|
+ err: fmt.Errorf("field username not found in secret template"),
|
|
|
|
|
+ want: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ "wrapped field not found in secret": {
|
|
|
|
|
+ // Even when wrapped, the false-positive exclusion applies.
|
|
|
|
|
+ err: fmt.Errorf("failed to update secret: %w", fmt.Errorf("field password not found in secret")),
|
|
|
|
|
+ want: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ "mixed case Not Found": {
|
|
|
|
|
+ err: errors.New("Not Found"),
|
|
|
|
|
+ want: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ "SDK HTTP 401 with not found in body": {
|
|
|
|
|
+ // Auth errors that happen to contain "not found" in the body should NOT
|
|
|
|
|
+ // be treated as secret-not-found errors. Only 404 is a true not-found.
|
|
|
|
|
+ err: errors.New("401 Unauthorized: user not found"),
|
|
|
|
|
+ want: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ "SDK HTTP 500 error": {
|
|
|
|
|
+ err: errors.New("500 Internal Server Error: something went wrong"),
|
|
|
|
|
+ want: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ "our errMsgNotFound sentinel": {
|
|
|
|
|
+ // From getSecretByName folder mismatch: errors.New(errMsgNotFound)
|
|
|
|
|
+ err: errors.New(errMsgNotFound),
|
|
|
|
|
+ want: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ "errMsgAmbiguousName is not a not-found error": {
|
|
|
|
|
+ err: errors.New(errMsgAmbiguousName),
|
|
|
|
|
+ want: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for name, tc := range testCases {
|
|
|
|
|
+ t.Run(name, func(t *testing.T) {
|
|
|
|
|
+ got := isNotFoundError(tc.err)
|
|
|
|
|
+ assert.Equal(t, tc.want, got)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestPushSecretInvalidPathKeys tests that PushSecret rejects path-style keys with
|
|
|
|
|
+// empty final segments (root slash, double slash, etc.) that would produce an empty secret name.
|
|
|
|
|
+func TestPushSecretInvalidPathKeys(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ c := newTestClient(t)
|
|
|
|
|
+
|
|
|
|
|
+ secret := &corev1.Secret{
|
|
|
|
|
+ Data: map[string][]byte{
|
|
|
|
|
+ "my-key": []byte("my-value"),
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ metadataJSON := apiextensionsv1.JSON{
|
|
|
|
|
+ Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 1}}`),
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ testCases := map[string]struct {
|
|
|
|
|
+ remoteKey string
|
|
|
|
|
+ errMsg string
|
|
|
|
|
+ }{
|
|
|
|
|
+ "root slash only": {
|
|
|
|
|
+ remoteKey: "/",
|
|
|
|
|
+ errMsg: "invalid secret name",
|
|
|
|
|
+ },
|
|
|
|
|
+ "double slash": {
|
|
|
|
|
+ remoteKey: "//",
|
|
|
|
|
+ errMsg: "invalid secret name",
|
|
|
|
|
+ },
|
|
|
|
|
+ "triple slash": {
|
|
|
|
|
+ remoteKey: "///",
|
|
|
|
|
+ errMsg: "invalid secret name",
|
|
|
|
|
+ },
|
|
|
|
|
+ "trailing slash on path": {
|
|
|
|
|
+ remoteKey: "/Folder/Subfolder/",
|
|
|
|
|
+ errMsg: "invalid secret name",
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for name, tc := range testCases {
|
|
|
|
|
+ t.Run(name, func(t *testing.T) {
|
|
|
|
|
+ data := fakePushSecretData{
|
|
|
|
|
+ remoteKey: tc.remoteKey,
|
|
|
|
|
+ property: "username",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ metadata: &metadataJSON,
|
|
|
|
|
+ }
|
|
|
|
|
+ err := c.PushSecret(ctx, secret, data)
|
|
|
|
|
+ assert.Error(t, err)
|
|
|
|
|
+ assert.Contains(t, err.Error(), tc.errMsg)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestParseFolderPrefix tests the parseFolderPrefix helper function.
|
|
|
|
|
+func TestParseFolderPrefix(t *testing.T) {
|
|
|
|
|
+ testCases := map[string]struct {
|
|
|
|
|
+ key string
|
|
|
|
|
+ wantFolderID int
|
|
|
|
|
+ wantName string
|
|
|
|
|
+ wantHasFolderPfx bool
|
|
|
|
|
+ }{
|
|
|
|
|
+ "valid prefix": {
|
|
|
|
|
+ key: "folderId:73/my-secret",
|
|
|
|
|
+ wantFolderID: 73,
|
|
|
|
|
+ wantName: "my-secret",
|
|
|
|
|
+ wantHasFolderPfx: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ "valid prefix with large folder ID": {
|
|
|
|
|
+ key: "folderId:99999/secret-name",
|
|
|
|
|
+ wantFolderID: 99999,
|
|
|
|
|
+ wantName: "secret-name",
|
|
|
|
|
+ wantHasFolderPfx: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ "valid prefix with name containing slashes": {
|
|
|
|
|
+ key: "folderId:73/sub/path/secret",
|
|
|
|
|
+ wantFolderID: 73,
|
|
|
|
|
+ wantName: "sub/path/secret",
|
|
|
|
|
+ wantHasFolderPfx: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ "no prefix - plain name": {
|
|
|
|
|
+ key: "my-secret",
|
|
|
|
|
+ wantFolderID: 0,
|
|
|
|
|
+ wantName: "my-secret",
|
|
|
|
|
+ wantHasFolderPfx: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ "no prefix - numeric key": {
|
|
|
|
|
+ key: "12345",
|
|
|
|
|
+ wantFolderID: 0,
|
|
|
|
|
+ wantName: "12345",
|
|
|
|
|
+ wantHasFolderPfx: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ "no prefix - path key": {
|
|
|
|
|
+ key: "/Folder/SecretName",
|
|
|
|
|
+ wantFolderID: 0,
|
|
|
|
|
+ wantName: "/Folder/SecretName",
|
|
|
|
|
+ wantHasFolderPfx: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ "prefix without slash": {
|
|
|
|
|
+ key: "folderId:73",
|
|
|
|
|
+ wantFolderID: 0,
|
|
|
|
|
+ wantName: "folderId:73",
|
|
|
|
|
+ wantHasFolderPfx: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ "prefix with empty name": {
|
|
|
|
|
+ key: "folderId:73/",
|
|
|
|
|
+ wantFolderID: 0,
|
|
|
|
|
+ wantName: "folderId:73/",
|
|
|
|
|
+ wantHasFolderPfx: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ "prefix with non-numeric ID": {
|
|
|
|
|
+ key: "folderId:abc/my-secret",
|
|
|
|
|
+ wantFolderID: 0,
|
|
|
|
|
+ wantName: "folderId:abc/my-secret",
|
|
|
|
|
+ wantHasFolderPfx: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ "prefix with zero ID": {
|
|
|
|
|
+ key: "folderId:0/my-secret",
|
|
|
|
|
+ wantFolderID: 0,
|
|
|
|
|
+ wantName: "folderId:0/my-secret",
|
|
|
|
|
+ wantHasFolderPfx: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ "prefix with negative ID": {
|
|
|
|
|
+ key: "folderId:-1/my-secret",
|
|
|
|
|
+ wantFolderID: 0,
|
|
|
|
|
+ wantName: "folderId:-1/my-secret",
|
|
|
|
|
+ wantHasFolderPfx: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ "empty key": {
|
|
|
|
|
+ key: "",
|
|
|
|
|
+ wantFolderID: 0,
|
|
|
|
|
+ wantName: "",
|
|
|
|
|
+ wantHasFolderPfx: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for name, tc := range testCases {
|
|
|
|
|
+ t.Run(name, func(t *testing.T) {
|
|
|
|
|
+ folderID, secretName, hasFolderPrefix := parseFolderPrefix(tc.key)
|
|
|
|
|
+ assert.Equal(t, tc.wantFolderID, folderID)
|
|
|
|
|
+ assert.Equal(t, tc.wantName, secretName)
|
|
|
|
|
+ assert.Equal(t, tc.wantHasFolderPfx, hasFolderPrefix)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestPushSecretWithFolderPrefix tests PushSecret with the "folderId:<id>/<name>" key format.
|
|
|
|
|
+func TestPushSecretWithFolderPrefix(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ c := newTestClient(t)
|
|
|
|
|
+
|
|
|
|
|
+ secret := &corev1.Secret{
|
|
|
|
|
+ Data: map[string][]byte{
|
|
|
|
|
+ "my-key": []byte("folder-prefix-value"),
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ metadataJSON := apiextensionsv1.JSON{
|
|
|
|
|
+ Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 5, "secretTemplateId": 1}}`),
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Update an existing secret using folderId prefix — should target folder 5 (ID 9001)
|
|
|
|
|
+ dataUpdate := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "folderId:5/FolderSecretname",
|
|
|
|
|
+ property: "password",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ metadata: &metadataJSON,
|
|
|
|
|
+ }
|
|
|
|
|
+ err := c.PushSecret(ctx, secret, dataUpdate)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+
|
|
|
|
|
+ // Verify only the secret in folder 5 was updated
|
|
|
|
|
+ s9001, _ := c.(*client).api.Secret(9001)
|
|
|
|
|
+ s9000, _ := c.(*client).api.Secret(9000)
|
|
|
|
|
+ var s9001PW, s9000PW string
|
|
|
|
|
+ for _, f := range s9001.Fields {
|
|
|
|
|
+ if f.Slug == passwordSlug {
|
|
|
|
|
+ s9001PW = f.ItemValue
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, f := range s9000.Fields {
|
|
|
|
|
+ if f.Slug == passwordSlug {
|
|
|
|
|
+ s9000PW = f.ItemValue
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ assert.Equal(t, "folder-prefix-value", s9001PW)
|
|
|
|
|
+ assert.Equal(t, "passwordvalue", s9000PW) // Unchanged
|
|
|
|
|
+
|
|
|
|
|
+ // Create a new secret using folderId prefix
|
|
|
|
|
+ metadataCreate := apiextensionsv1.JSON{
|
|
|
|
|
+ Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 42, "secretTemplateId": 1}}`),
|
|
|
|
|
+ }
|
|
|
|
|
+ dataCreate := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "folderId:42/brand-new-secret",
|
|
|
|
|
+ property: "username",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ metadata: &metadataCreate,
|
|
|
|
|
+ }
|
|
|
|
|
+ err = c.PushSecret(ctx, secret, dataCreate)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+
|
|
|
|
|
+ // Verify the created secret has the plain name (prefix stripped)
|
|
|
|
|
+ foundSecrets, _ := c.(*client).api.Secrets("brand-new-secret", "Name")
|
|
|
|
|
+ assert.Len(t, foundSecrets, 1)
|
|
|
|
|
+ assert.Equal(t, "brand-new-secret", foundSecrets[0].Name)
|
|
|
|
|
+ assert.Equal(t, 42, foundSecrets[0].FolderID)
|
|
|
|
|
+
|
|
|
|
|
+ // Test precedence: remoteKey folderId overrides metadata folderId for lookups.
|
|
|
|
|
+ // Metadata says folderId:4, but remoteKey says folderId:5 — should target folder 5.
|
|
|
|
|
+ metadataFolder4 := apiextensionsv1.JSON{
|
|
|
|
|
+ Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 4, "secretTemplateId": 1}}`),
|
|
|
|
|
+ }
|
|
|
|
|
+ dataPrecedence := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "folderId:5/FolderSecretname",
|
|
|
|
|
+ property: "username",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ metadata: &metadataFolder4,
|
|
|
|
|
+ }
|
|
|
|
|
+ err = c.PushSecret(ctx, secret, dataPrecedence)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+
|
|
|
|
|
+ // Verify the secret in folder 5 was updated (not folder 4)
|
|
|
|
|
+ s9001, _ = c.(*client).api.Secret(9001)
|
|
|
|
|
+ var s9001User string
|
|
|
|
|
+ for _, f := range s9001.Fields {
|
|
|
|
|
+ if f.Slug == usernameSlug {
|
|
|
|
|
+ s9001User = f.ItemValue
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ assert.Equal(t, "folder-prefix-value", s9001User)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestDeleteSecretWithFolderPrefix tests that DeleteSecret correctly uses the
|
|
|
|
|
+// folderId prefix in the remote key to target the right secret.
|
|
|
|
|
+func TestDeleteSecretWithFolderPrefix(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ c := newTestClient(t)
|
|
|
|
|
+
|
|
|
|
|
+ // Both secrets 9000 (folder 4) and 9001 (folder 5) have name "FolderSecretname".
|
|
|
|
|
+ // Delete only the one in folder 5.
|
|
|
|
|
+ ref := fakePushSecretRemoteRef{
|
|
|
|
|
+ remoteKey: "folderId:5/FolderSecretname",
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Should exist initially
|
|
|
|
|
+ exists, err := c.SecretExists(ctx, ref)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+ assert.True(t, exists)
|
|
|
|
|
+
|
|
|
|
|
+ // Delete it
|
|
|
|
|
+ err = c.DeleteSecret(ctx, ref)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+
|
|
|
|
|
+ // Should not exist now
|
|
|
|
|
+ exists, err = c.SecretExists(ctx, ref)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+ assert.False(t, exists)
|
|
|
|
|
+
|
|
|
|
|
+ // The secret in folder 4 should still exist
|
|
|
|
|
+ refFolder4 := fakePushSecretRemoteRef{
|
|
|
|
|
+ remoteKey: "folderId:4/FolderSecretname",
|
|
|
|
|
+ }
|
|
|
|
|
+ exists, err = c.SecretExists(ctx, refFolder4)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+ assert.True(t, exists)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestSecretExistsWithFolderPrefix tests that SecretExists correctly uses the
|
|
|
|
|
+// folderId prefix in the remote key.
|
|
|
|
|
+func TestSecretExistsWithFolderPrefix(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ c := newTestClient(t)
|
|
|
|
|
+
|
|
|
|
|
+ testCases := map[string]struct {
|
|
|
|
|
+ ref esv1.PushSecretRemoteRef
|
|
|
|
|
+ want bool
|
|
|
|
|
+ }{
|
|
|
|
|
+ "existing secret in folder 4": {
|
|
|
|
|
+ ref: fakePushSecretRemoteRef{remoteKey: "folderId:4/FolderSecretname"},
|
|
|
|
|
+ want: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ "existing secret in folder 5": {
|
|
|
|
|
+ ref: fakePushSecretRemoteRef{remoteKey: "folderId:5/FolderSecretname"},
|
|
|
|
|
+ want: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ "non-existing secret in wrong folder": {
|
|
|
|
|
+ ref: fakePushSecretRemoteRef{remoteKey: "folderId:99/FolderSecretname"},
|
|
|
|
|
+ want: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ "non-existing secret name": {
|
|
|
|
|
+ ref: fakePushSecretRemoteRef{remoteKey: "folderId:4/does-not-exist"},
|
|
|
|
|
+ want: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for name, tc := range testCases {
|
|
|
|
|
+ t.Run(name, func(t *testing.T) {
|
|
|
|
|
+ got, err := c.SecretExists(ctx, tc.ref)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+ assert.Equal(t, tc.want, got)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestDeleteSecretAmbiguousName tests that DeleteSecret returns an error when a
|
|
|
|
|
+// plain name matches multiple secrets across different folders.
|
|
|
|
|
+func TestDeleteSecretAmbiguousName(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ c := newTestClient(t)
|
|
|
|
|
+
|
|
|
|
|
+ // "FolderSecretname" exists in both folder 4 (ID 9000) and folder 5 (ID 9001).
|
|
|
|
|
+ // Using just the plain name should fail with an ambiguous error.
|
|
|
|
|
+ ref := fakePushSecretRemoteRef{
|
|
|
|
|
+ remoteKey: "FolderSecretname",
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ err := c.DeleteSecret(ctx, ref)
|
|
|
|
|
+ assert.Error(t, err)
|
|
|
|
|
+ assert.Contains(t, err.Error(), "multiple secrets found with the same name")
|
|
|
|
|
+ assert.Contains(t, err.Error(), "folderId:")
|
|
|
|
|
+
|
|
|
|
|
+ // Both secrets should still exist (nothing was deleted).
|
|
|
|
|
+ s9000, err := c.(*client).api.Secret(9000)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+ assert.NotNil(t, s9000)
|
|
|
|
|
+
|
|
|
|
|
+ s9001, err := c.(*client).api.Secret(9001)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+ assert.NotNil(t, s9001)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestSecretExistsAmbiguousName tests that SecretExists returns an error when a
|
|
|
|
|
+// plain name matches multiple secrets across different folders.
|
|
|
|
|
+func TestSecretExistsAmbiguousName(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ c := newTestClient(t)
|
|
|
|
|
+
|
|
|
|
|
+ // "FolderSecretname" exists in both folder 4 (ID 9000) and folder 5 (ID 9001).
|
|
|
|
|
+ // Using just the plain name should fail with an ambiguous error.
|
|
|
|
|
+ ref := fakePushSecretRemoteRef{
|
|
|
|
|
+ remoteKey: "FolderSecretname",
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ exists, err := c.SecretExists(ctx, ref)
|
|
|
|
|
+ assert.Error(t, err)
|
|
|
|
|
+ assert.False(t, exists)
|
|
|
|
|
+ assert.Contains(t, err.Error(), "multiple secrets found with the same name")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestDeleteSecretUniqueName tests that DeleteSecret still works with a plain
|
|
|
|
|
+// name when only one secret has that name (no ambiguity).
|
|
|
|
|
+func TestDeleteSecretUniqueName(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ c := newTestClient(t)
|
|
|
|
|
+
|
|
|
|
|
+ // "Secretname" is unique (only ID 4000 has this name).
|
|
|
|
|
+ ref := fakePushSecretRemoteRef{
|
|
|
|
|
+ remoteKey: "Secretname",
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ exists, err := c.SecretExists(ctx, ref)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+ assert.True(t, exists)
|
|
|
|
|
+
|
|
|
|
|
+ err = c.DeleteSecret(ctx, ref)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+
|
|
|
|
|
+ exists, err = c.SecretExists(ctx, ref)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+ assert.False(t, exists)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestGetSecretByNameStrict tests the getSecretByNameStrict helper directly.
|
|
|
|
|
+func TestGetSecretByNameStrict(t *testing.T) {
|
|
|
|
|
+ c := newTestClient(t).(*client)
|
|
|
|
|
+
|
|
|
|
|
+ testCases := map[string]struct {
|
|
|
|
|
+ name string
|
|
|
|
|
+ wantErr bool
|
|
|
|
|
+ errMsg string
|
|
|
|
|
+ }{
|
|
|
|
|
+ "unique name returns secret": {
|
|
|
|
|
+ name: "Secretname",
|
|
|
|
|
+ wantErr: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ "duplicate name returns ambiguous error": {
|
|
|
|
|
+ name: "FolderSecretname",
|
|
|
|
|
+ wantErr: true,
|
|
|
|
|
+ errMsg: "multiple secrets found with the same name",
|
|
|
|
|
+ },
|
|
|
|
|
+ "non-existent name returns unable to retrieve secret error": {
|
|
|
|
|
+ name: "does-not-exist",
|
|
|
|
|
+ wantErr: true,
|
|
|
|
|
+ errMsg: errMsgNoMatchingSecrets,
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for name, tc := range testCases {
|
|
|
|
|
+ t.Run(name, func(t *testing.T) {
|
|
|
|
|
+ secret, err := c.getSecretByNameStrict(tc.name)
|
|
|
|
|
+ if tc.wantErr {
|
|
|
|
|
+ assert.Error(t, err)
|
|
|
|
|
+ assert.Nil(t, secret)
|
|
|
|
|
+ assert.Contains(t, err.Error(), tc.errMsg)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+ assert.NotNil(t, secret)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestPushSecretEmptyProperty tests PushSecret with an empty property, which
|
|
|
|
|
+// should target the first field of the secret/template.
|
|
|
|
|
+func TestPushSecretEmptyProperty(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ c := newTestClient(t)
|
|
|
|
|
+
|
|
|
|
|
+ secret := &corev1.Secret{
|
|
|
|
|
+ Data: map[string][]byte{
|
|
|
|
|
+ "my-key": []byte("whole-value"),
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ metadataJSON := apiextensionsv1.JSON{
|
|
|
|
|
+ Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 1}}`),
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Create new secret with empty property → uses first template field
|
|
|
|
|
+ data := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "empty-prop-secret",
|
|
|
|
|
+ property: "",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ metadata: &metadataJSON,
|
|
|
|
|
+ }
|
|
|
|
|
+ err := c.PushSecret(ctx, secret, data)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+
|
|
|
|
|
+ // Verify: the first field should have the value
|
|
|
|
|
+ foundSecrets, _ := c.(*client).api.Secrets("empty-prop-secret", "Name")
|
|
|
|
|
+ require.Len(t, foundSecrets, 1)
|
|
|
|
|
+ require.Len(t, foundSecrets[0].Fields, 1)
|
|
|
|
|
+ assert.Equal(t, "whole-value", foundSecrets[0].Fields[0].ItemValue)
|
|
|
|
|
+
|
|
|
|
|
+ // Update existing secret with empty property → updates first field
|
|
|
|
|
+ data2 := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "4000",
|
|
|
|
|
+ property: "",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ }
|
|
|
|
|
+ err = c.PushSecret(ctx, secret, data2)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+
|
|
|
|
|
+ s4000, _ := c.(*client).api.Secret(4000)
|
|
|
|
|
+ assert.Equal(t, "whole-value", s4000.Fields[0].ItemValue)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestPushSecretConflictingFolderIDs tests that when the remoteKey has a folderId
|
|
|
|
|
+// prefix, it overrides the metadata folderId for both lookup AND creation.
|
|
|
|
|
+func TestPushSecretConflictingFolderIDs(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ c := newTestClient(t)
|
|
|
|
|
+
|
|
|
|
|
+ secret := &corev1.Secret{
|
|
|
|
|
+ Data: map[string][]byte{
|
|
|
|
|
+ "my-key": []byte("prefix-wins"),
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Metadata says folderId:99, but prefix says folderId:42.
|
|
|
|
|
+ // The prefix should win for creation.
|
|
|
|
|
+ metadataJSON := apiextensionsv1.JSON{
|
|
|
|
|
+ Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 99, "secretTemplateId": 1}}`),
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ data := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "folderId:42/conflict-test",
|
|
|
|
|
+ property: "username",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ metadata: &metadataJSON,
|
|
|
|
|
+ }
|
|
|
|
|
+ err := c.PushSecret(ctx, secret, data)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+
|
|
|
|
|
+ // Verify: the secret was created in folder 42, not 99.
|
|
|
|
|
+ foundSecrets, _ := c.(*client).api.Secrets("conflict-test", "Name")
|
|
|
|
|
+ require.Len(t, foundSecrets, 1)
|
|
|
|
|
+ assert.Equal(t, 42, foundSecrets[0].FolderID)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestPushSecretAmbiguousPlainName tests that PushSecret returns an error when
|
|
|
|
|
+// a plain name (no prefix, no path, no numeric ID) matches multiple secrets.
|
|
|
|
|
+func TestPushSecretAmbiguousPlainName(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ c := newTestClient(t)
|
|
|
|
|
+
|
|
|
|
|
+ secret := &corev1.Secret{
|
|
|
|
|
+ Data: map[string][]byte{
|
|
|
|
|
+ "my-key": []byte("value"),
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // "FolderSecretname" exists in both folder 4 (ID 9000) and folder 5 (ID 9001).
|
|
|
|
|
+ // Without a folderId prefix or metadata folderId, this should fail.
|
|
|
|
|
+ data := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "FolderSecretname",
|
|
|
|
|
+ property: "password",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ }
|
|
|
|
|
+ err := c.PushSecret(ctx, secret, data)
|
|
|
|
|
+ assert.Error(t, err)
|
|
|
|
|
+ assert.Contains(t, err.Error(), "multiple secrets found with the same name")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestPushSecretEmptyRemoteKey tests that PushSecret rejects empty remote keys.
|
|
|
|
|
+func TestPushSecretEmptyRemoteKey(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ c := newTestClient(t)
|
|
|
|
|
+
|
|
|
|
|
+ secret := &corev1.Secret{
|
|
|
|
|
+ Data: map[string][]byte{
|
|
|
|
|
+ "my-key": []byte("value"),
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ data := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "",
|
|
|
|
|
+ property: "username",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ }
|
|
|
|
|
+ err := c.PushSecret(ctx, secret, data)
|
|
|
|
|
+ assert.Error(t, err)
|
|
|
|
|
+ assert.Contains(t, err.Error(), "remote key must be defined")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestCreateSecretFolderPrefixWithSlashes tests that createSecret rejects
|
|
|
|
|
+// folderId prefixed names that contain slashes after prefix stripping.
|
|
|
|
|
+func TestCreateSecretFolderPrefixWithSlashes(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ c := newTestClient(t)
|
|
|
|
|
+
|
|
|
|
|
+ secret := &corev1.Secret{
|
|
|
|
|
+ Data: map[string][]byte{
|
|
|
|
|
+ "my-key": []byte("value"),
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ metadataJSON := apiextensionsv1.JSON{
|
|
|
|
|
+ Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 73, "secretTemplateId": 1}}`),
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ data := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "folderId:73/sub/path/secret",
|
|
|
|
|
+ property: "username",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ metadata: &metadataJSON,
|
|
|
|
|
+ }
|
|
|
|
|
+ err := c.PushSecret(ctx, secret, data)
|
|
|
|
|
+ assert.Error(t, err)
|
|
|
|
|
+ assert.Contains(t, err.Error(), "must not contain path separators")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestCreateSecretEmptyTemplateFields tests createSecret when the template has
|
|
|
|
|
+// no fields and no property is specified.
|
|
|
|
|
+func TestCreateSecretEmptyTemplateFields(t *testing.T) {
|
|
|
|
|
+ // Create a fakeAPI that returns a template with no fields
|
|
|
|
|
+ fake := &fakeAPI{secrets: []*server.Secret{}}
|
|
|
|
|
+ // Override SecretTemplate to return empty fields (template ID 888)
|
|
|
|
|
+ c := &client{api: &emptyTemplateAPI{fakeAPI: fake}}
|
|
|
|
|
+
|
|
|
|
|
+ err := c.createSecret("test-secret", "", "value", PushSecretMetadataSpec{
|
|
|
|
|
+ FolderID: 1,
|
|
|
|
|
+ SecretTemplateID: 888,
|
|
|
|
|
+ })
|
|
|
|
|
+ assert.Error(t, err)
|
|
|
|
|
+ assert.Contains(t, err.Error(), "secret template has no fields")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// emptyTemplateAPI wraps fakeAPI but returns an empty template for ID 888.
|
|
|
|
|
+type emptyTemplateAPI struct {
|
|
|
|
|
+ *fakeAPI
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *emptyTemplateAPI) SecretTemplate(id int) (*server.SecretTemplate, error) {
|
|
|
|
|
+ if id == 888 {
|
|
|
|
|
+ return &server.SecretTemplate{
|
|
|
|
|
+ ID: 888,
|
|
|
|
|
+ Name: "Empty Template",
|
|
|
|
|
+ Fields: []server.SecretTemplateField{},
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ }
|
|
|
|
|
+ return e.fakeAPI.SecretTemplate(id)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestGetSecretGjsonPriorityOverField tests that gjson extraction from
|
|
|
|
|
+// Fields[0].ItemValue takes priority over field Slug/FieldName matching.
|
|
|
|
|
+// This preserves backward compatibility: existing users relying on gjson
|
|
|
|
|
+// extraction from the first field's JSON blob are not broken.
|
|
|
|
|
+func TestGetSecretGjsonPriorityOverField(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+
|
|
|
|
|
+ // Create a secret where:
|
|
|
|
|
+ // - Fields[0].ItemValue is JSON containing key "password"
|
|
|
|
|
+ // - Fields[1] has Slug "password" with a DIFFERENT value
|
|
|
|
|
+ // gjson should win because it is checked first (backward compat).
|
|
|
|
|
+ s := &server.Secret{
|
|
|
|
|
+ ID: 100,
|
|
|
|
|
+ Name: "priority-test",
|
|
|
|
|
+ Fields: []server.SecretField{
|
|
|
|
|
+ {
|
|
|
|
|
+ FieldName: "Data",
|
|
|
|
|
+ Slug: "data",
|
|
|
|
|
+ ItemValue: `{"password": "from-json-blob"}`,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ FieldName: "Password",
|
|
|
|
|
+ Slug: "password",
|
|
|
|
|
+ ItemValue: "from-field-slug",
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ c := &client{api: &fakeAPI{secrets: []*server.Secret{s}}}
|
|
|
|
|
+
|
|
|
|
|
+ got, err := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{
|
|
|
|
|
+ Key: "100",
|
|
|
|
|
+ Property: "password",
|
|
|
|
|
+ })
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+ // gjson extraction should return "from-json-blob" (backward compat takes precedence)
|
|
|
|
|
+ assert.Equal(t, []byte("from-json-blob"), got)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestGetSecretGjsonFallback tests that gjson extraction from Fields[0].ItemValue
|
|
|
|
|
+// works as a fallback when no field slug/name matches.
|
|
|
|
|
+func TestGetSecretGjsonFallback(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+
|
|
|
|
|
+ s := &server.Secret{
|
|
|
|
|
+ ID: 101,
|
|
|
|
|
+ Name: "gjson-fallback-test",
|
|
|
|
|
+ Fields: []server.SecretField{
|
|
|
|
|
+ {
|
|
|
|
|
+ FieldName: "Data",
|
|
|
|
|
+ Slug: "data",
|
|
|
|
|
+ ItemValue: `{"nested": {"key": "deep-value"}}`,
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ c := &client{api: &fakeAPI{secrets: []*server.Secret{s}}}
|
|
|
|
|
+
|
|
|
|
|
+ // "nested.key" doesn't match any field slug/name, so gjson fallback kicks in
|
|
|
|
|
+ got, err := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{
|
|
|
|
|
+ Key: "101",
|
|
|
|
|
+ Property: "nested.key",
|
|
|
|
|
+ })
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+ assert.Equal(t, []byte("deep-value"), got)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestLookupSecretNon404Error tests that lookupSecret and lookupSecretStrict
|
|
|
|
|
+// correctly propagate non-404 API errors instead of falling through.
|
|
|
|
|
+func TestLookupSecretNon404Error(t *testing.T) {
|
|
|
|
|
+ // Create an API that returns a non-404 error for Secret()
|
|
|
|
|
+ fake := &errorAPI{
|
|
|
|
|
+ fakeAPI: &fakeAPI{secrets: []*server.Secret{}},
|
|
|
|
|
+ secretErr: errors.New("500 Internal Server Error: database connection failed"),
|
|
|
|
|
+ }
|
|
|
|
|
+ c := &client{api: fake}
|
|
|
|
|
+
|
|
|
|
|
+ // lookupSecret with numeric key: Secret() returns non-404 error, should propagate
|
|
|
|
|
+ _, err := c.lookupSecret("42", 0)
|
|
|
|
|
+ assert.Error(t, err)
|
|
|
|
|
+ assert.Contains(t, err.Error(), "database connection failed")
|
|
|
|
|
+
|
|
|
|
|
+ // lookupSecretStrict with numeric key: same behavior
|
|
|
|
|
+ _, err = c.lookupSecretStrict("42")
|
|
|
|
|
+ assert.Error(t, err)
|
|
|
|
|
+ assert.Contains(t, err.Error(), "database connection failed")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// errorAPI wraps fakeAPI but returns a configurable error for Secret().
|
|
|
|
|
+type errorAPI struct {
|
|
|
|
|
+ *fakeAPI
|
|
|
|
|
+ secretErr error
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *errorAPI) Secret(_ int) (*server.Secret, error) {
|
|
|
|
|
+ if e.secretErr != nil {
|
|
|
|
|
+ return nil, e.secretErr
|
|
|
|
|
+ }
|
|
|
|
|
+ return e.fakeAPI.Secret(0)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestFakeAPISecretsReturnsEmptySlice verifies the fakeAPI mock matches real SDK
|
|
|
|
|
+// behavior: Secrets() returns ([]Secret{}, nil) for zero matches, not an error.
|
|
|
|
|
+func TestFakeAPISecretsReturnsEmptySlice(t *testing.T) {
|
|
|
|
|
+ fake := &fakeAPI{secrets: []*server.Secret{}}
|
|
|
|
|
+
|
|
|
|
|
+ secrets, err := fake.Secrets("nonexistent", "Name")
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+ assert.NotNil(t, secrets)
|
|
|
|
|
+ assert.Empty(t, secrets)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestPushSecretMetadataNoFolderID tests that PushSecret requires a folderId
|
|
|
|
|
+// for creation when the prefix doesn't provide one.
|
|
|
|
|
+func TestPushSecretMetadataNoFolderID(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ c := newTestClient(t)
|
|
|
|
|
+
|
|
|
|
|
+ secret := &corev1.Secret{
|
|
|
|
|
+ Data: map[string][]byte{
|
|
|
|
|
+ "my-key": []byte("value"),
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Metadata has secretTemplateId but no folderId
|
|
|
|
|
+ metadataJSON := apiextensionsv1.JSON{
|
|
|
|
|
+ Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 0, "secretTemplateId": 1}}`),
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ data := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "no-folder-secret",
|
|
|
|
|
+ property: "username",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ metadata: &metadataJSON,
|
|
|
|
|
+ }
|
|
|
|
|
+ err := c.PushSecret(ctx, secret, data)
|
|
|
|
|
+ assert.Error(t, err)
|
|
|
|
|
+ assert.Contains(t, err.Error(), "folderId and secretTemplateId must be provided")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestPushSecretCreateWithFolderPrefixNoMetadataFolder tests that PushSecret can
|
|
|
|
|
+// create a secret when the folderId comes from the prefix even if metadata has
|
|
|
|
|
+// folderId: 0, as long as secretTemplateId is provided.
|
|
|
|
|
+func TestPushSecretCreateWithFolderPrefixNoMetadataFolder(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ c := newTestClient(t)
|
|
|
|
|
+
|
|
|
|
|
+ secret := &corev1.Secret{
|
|
|
|
|
+ Data: map[string][]byte{
|
|
|
|
|
+ "my-key": []byte("prefix-folder-value"),
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Metadata has secretTemplateId but folderId is 0; prefix provides the folder.
|
|
|
|
|
+ metadataJSON := apiextensionsv1.JSON{
|
|
|
|
|
+ Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 0, "secretTemplateId": 1}}`),
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ data := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "folderId:55/prefix-only-folder",
|
|
|
|
|
+ property: "username",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ metadata: &metadataJSON,
|
|
|
|
|
+ }
|
|
|
|
|
+ err := c.PushSecret(ctx, secret, data)
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+
|
|
|
|
|
+ // Verify: created in folder 55
|
|
|
|
|
+ foundSecrets, _ := c.(*client).api.Secrets("prefix-only-folder", "Name")
|
|
|
|
|
+ require.Len(t, foundSecrets, 1)
|
|
|
|
|
+ assert.Equal(t, 55, foundSecrets[0].FolderID)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestGetSecretByFolderPrefix tests GetSecret with the folderId prefix format.
|
|
|
|
|
+func TestGetSecretByFolderPrefix(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ c := newTestClient(t)
|
|
|
|
|
+
|
|
|
|
|
+ // Secret 9000 is in folder 4, secret 9001 is in folder 5, both named "FolderSecretname"
|
|
|
|
|
+ got, err := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{
|
|
|
|
|
+ Key: "folderId:4/FolderSecretname",
|
|
|
|
|
+ Property: "username",
|
|
|
|
|
+ })
|
|
|
|
|
+ assert.NoError(t, err)
|
|
|
|
|
+ assert.Equal(t, []byte("usernamevalue"), got)
|
|
|
|
|
+
|
|
|
|
|
+ // Non-existent folder
|
|
|
|
|
+ _, err = c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{
|
|
|
|
|
+ Key: "folderId:99/FolderSecretname",
|
|
|
|
|
+ Property: "username",
|
|
|
|
|
+ })
|
|
|
|
|
+ assert.Error(t, err)
|
|
|
|
|
+ assert.Contains(t, err.Error(), "not found")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestPushSecretUpdateSecretNoFields tests updating a secret that has no fields.
|
|
|
|
|
+func TestPushSecretUpdateSecretNoFields(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+
|
|
|
|
|
+ // Create a secret with no fields
|
|
|
|
|
+ s := &server.Secret{
|
|
|
|
|
+ ID: 200,
|
|
|
|
|
+ Name: "no-fields-secret",
|
|
|
|
|
+ Fields: []server.SecretField{},
|
|
|
|
|
+ }
|
|
|
|
|
+ c := &client{api: &fakeAPI{secrets: []*server.Secret{s}}}
|
|
|
|
|
+
|
|
|
|
|
+ secret := &corev1.Secret{
|
|
|
|
|
+ Data: map[string][]byte{
|
|
|
|
|
+ "my-key": []byte("value"),
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Update with empty property → tries to write to first field, but there are none
|
|
|
|
|
+ data := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "200",
|
|
|
|
|
+ property: "",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ }
|
|
|
|
|
+ err := c.PushSecret(ctx, secret, data)
|
|
|
|
|
+ assert.Error(t, err)
|
|
|
|
|
+ assert.Contains(t, err.Error(), "secret has no fields to update")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestPushSecretNonExistentTemplateField tests creating a secret with a property that
|
|
|
|
|
+// doesn't match any template field.
|
|
|
|
|
+func TestPushSecretNonExistentTemplateField(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ c := newTestClient(t)
|
|
|
|
|
+
|
|
|
|
|
+ secret := &corev1.Secret{
|
|
|
|
|
+ Data: map[string][]byte{
|
|
|
|
|
+ "my-key": []byte("value"),
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ metadataJSON := apiextensionsv1.JSON{
|
|
|
|
|
+ Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 1}}`),
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ data := fakePushSecretData{
|
|
|
|
|
+ remoteKey: "nonexistent-field-secret",
|
|
|
|
|
+ property: "nonexistent-field",
|
|
|
|
|
+ secretKey: "my-key",
|
|
|
|
|
+ metadata: &metadataJSON,
|
|
|
|
|
+ }
|
|
|
|
|
+ err := c.PushSecret(ctx, secret, data)
|
|
|
|
|
+ assert.Error(t, err)
|
|
|
|
|
+ assert.Contains(t, err.Error(), "field nonexistent-field not found in secret template")
|
|
|
|
|
+}
|