| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002 |
- /*
- Copyright © The ESO Authors
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- https://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package secretserver
- import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "os"
- "testing"
- "github.com/DelineaXPM/tss-sdk-go/v3/server"
- "github.com/stretchr/testify/assert"
- "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"
- )
- var (
- errNotFound = errors.New("not found")
- )
- type fakeAPI struct {
- secrets []*server.Secret
- secretsErr error
- }
- const (
- usernameSlug = "username"
- passwordSlug = "password"
- )
- func (f *fakeAPI) Secret(id int) (*server.Secret, error) {
- for _, s := range f.secrets {
- if s.ID == id {
- return s, nil
- }
- }
- return nil, errNotFound
- }
- func (f *fakeAPI) Secrets(searchText, _ string) ([]server.Secret, error) {
- if f.secretsErr != nil {
- return nil, f.secretsErr
- }
- // 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 {
- if s.Name == searchText {
- secrets = append(secrets, *s)
- }
- }
- if secrets == nil {
- secrets = []server.Secret{}
- }
- return secrets, nil
- }
- func (f *fakeAPI) SecretByPath(path string) (*server.Secret, error) {
- for _, s := range f.secrets {
- if "/"+s.Name == path || s.Name == path {
- return s, nil
- }
- }
- 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) {
- s, err := jsonData()
- if err != nil {
- return nil, err
- }
- s.ID = id
- s.Fields[0].ItemValue = itemValue
- return s, nil
- }
- func jsonData() (*server.Secret, error) {
- var s = &server.Secret{}
- jsonFile, err := os.Open("test_data.json")
- if err != nil {
- return nil, err
- }
- defer jsonFile.Close()
- byteValue, err := io.ReadAll(jsonFile)
- if err != nil {
- return nil, err
- }
- err = json.Unmarshal(byteValue, &s)
- if err != nil {
- return nil, err
- }
- return s, nil
- }
- func createTestSecretFromCode(id int) *server.Secret {
- s := new(server.Secret)
- s.ID = id
- s.Name = "Secretname"
- s.Fields = make([]server.SecretField, 2)
- s.Fields[0].ItemValue = "usernamevalue"
- s.Fields[0].FieldName = "Username"
- s.Fields[0].Slug = usernameSlug
- s.Fields[1].FieldName = "Password"
- s.Fields[1].Slug = passwordSlug
- s.Fields[1].ItemValue = "passwordvalue"
- return s
- }
- func createTestFolderSecret(id, folderId int) *server.Secret {
- s := new(server.Secret)
- s.FolderID = folderId
- s.ID = id
- s.Name = "FolderSecretname"
- s.Fields = make([]server.SecretField, 2)
- s.Fields[0].ItemValue = "usernamevalue"
- s.Fields[0].FieldName = "Username"
- s.Fields[0].Slug = usernameSlug
- s.Fields[1].FieldName = "Password"
- s.Fields[1].Slug = passwordSlug
- s.Fields[1].ItemValue = "passwordvalue"
- return s
- }
- func createPlainTextSecret(id int) *server.Secret {
- s := new(server.Secret)
- s.ID = id
- s.Name = "PlainTextSecret"
- s.Fields = make([]server.SecretField, 1)
- s.Fields[0].FieldName = "Content"
- s.Fields[0].Slug = "content"
- s.Fields[0].ItemValue = `non-json-secret-value`
- return s
- }
- func createNilFieldsSecret(id int) *server.Secret {
- s := new(server.Secret)
- s.ID = id
- s.Name = "NilFieldsSecret"
- s.Fields = nil
- return s
- }
- func createEmptyFieldsSecret(id int) *server.Secret {
- s := new(server.Secret)
- s.ID = id
- s.Name = "EmptyFieldsSecret"
- s.Fields = []server.SecretField{}
- return s
- }
- func newTestClient(t *testing.T) esv1.SecretsClient {
- // Build secrets list while handling any errors from createSecret
- var secrets []*server.Secret //nolint:prealloc // populated incrementally
- s, err := createSecret(1000, "{ \"user\": \"robertOppenheimer\", \"password\": \"badPassword\",\"server\":\"192.168.1.50\"}")
- require.NoError(t, err)
- s2, err := createSecret(2000, "{ \"user\": \"helloWorld\", \"password\": \"badPassword\",\"server\":[ \"192.168.1.50\",\"192.168.1.51\"] }")
- require.NoError(t, err)
- s3, err := createSecret(3000, "{ \"user\": \"chuckTesta\", \"password\": \"badPassword\",\"server\":\"192.168.1.50\"}")
- require.NoError(t, err)
- secrets = append(secrets, s, s2, s3, createTestSecretFromCode(4000), createPlainTextSecret(5000))
- s6, err := createSecret(6000, "{ \"user\": \"betaTest\", \"password\": \"badPassword\" }")
- require.NoError(t, err)
- 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{
- api: &fakeAPI{
- secrets: secrets,
- },
- }
- }
- func TestGetSecretSecretServer(t *testing.T) {
- ctx := context.Background()
- c := newTestClient(t)
- s, err := jsonData()
- require.NoError(t, err)
- jsonStr, err := json.Marshal(s)
- require.NoError(t, err)
- jsonStr2, err := json.Marshal(createTestSecretFromCode(4000))
- require.NoError(t, err)
- jsonStr3, err := json.Marshal(createPlainTextSecret(5000))
- require.NoError(t, err)
- jsonStr4, err := json.Marshal(createTestFolderSecret(9000, 4))
- require.NoError(t, err)
- testCases := map[string]struct {
- 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": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "0",
- },
- want: []byte(nil),
- errMsg: errMsgNoMatchingSecrets,
- },
- "key = 'secret name' and user property returns a single value": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "ESO-test-secret",
- Property: "user",
- },
- want: []byte(`robertOppenheimer`),
- },
- "Secret from JSON: key and password property returns a single value": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "1000",
- Property: "password",
- },
- want: []byte(`badPassword`),
- },
- "Secret from JSON: key and nested property returns a single value": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "2000",
- Property: "server.1",
- },
- want: []byte(`192.168.1.51`),
- },
- "Secret from JSON: existent key with non-existing property": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "3000",
- Property: "foo.bar",
- },
- err: esv1.NoSecretError{},
- },
- "Secret from JSON: existent 'name' key with no property": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "1000",
- },
- want: jsonStr,
- },
- "Secret from code: existent key with no property": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "4000",
- },
- want: jsonStr2,
- },
- "Secret from code: key and username fieldnamereturns a single value": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "4000",
- Property: "Username",
- },
- want: []byte(`usernamevalue`),
- },
- "Plain text secret: existent key with no property": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "5000",
- },
- want: jsonStr3,
- },
- "Plain text secret: key with property returns expected value": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "5000",
- Property: "Content",
- },
- want: []byte(`non-json-secret-value`),
- },
- "Secret from code: valid ItemValue but incorrect property returns noSecretError": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "6000",
- Property: "missing",
- },
- want: []byte(nil),
- err: esv1.NoSecretError{},
- },
- "Secret from code: nil Fields returns error": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "7000",
- },
- want: []byte(nil),
- errMsg: "secret contains no fields",
- },
- "Secret from code: empty Fields returns error": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "8000",
- },
- want: []byte(nil),
- errMsg: "secret contains no fields",
- },
- "Secret from code: 'name' and password slug returns a single value": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "Secretname",
- Property: "password",
- },
- want: []byte(`passwordvalue`),
- },
- "Secret from code: 'name' not found returns unable to retrieve secret error": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "Secretnameerror",
- Property: "password",
- },
- want: []byte(nil),
- errMsg: errMsgNoMatchingSecrets,
- },
- "Secret from code: 'name' found and non-existent attribute slug returns noSecretError": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "Secretname",
- Property: "passwordkey",
- },
- want: []byte(nil),
- err: esv1.NoSecretError{},
- },
- "Secret by path: valid path returns secret": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "/FolderSecretname",
- },
- want: jsonStr4,
- },
- "Secret by path: invalid path returns error": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "/invalid/secret/path",
- },
- want: []byte(nil),
- errMsg: "not found",
- },
- }
- for name, tc := range testCases {
- t.Run(name, func(t *testing.T) {
- got, err := c.GetSecret(ctx, tc.ref)
- if tc.err == nil && tc.errMsg == "" {
- assert.NoError(t, err)
- assert.Equal(t, tc.want, got)
- } else {
- assert.Nil(t, got)
- if tc.errMsg != "" {
- assert.ErrorContains(t, err, tc.errMsg)
- } else {
- assert.ErrorIs(t, err, tc.err)
- }
- }
- })
- }
- }
- // 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()
- bad := &server.Secret{
- ID: 0,
- Fields: []server.SecretField{},
- }
- c := &client{
- api: &fakeAPI{
- secrets: []*server.Secret{bad},
- },
- }
- bad.Fields = []server.SecretField{
- {
- FieldName: "Foo",
- ItemValue: string([]byte{0xff, 0xfe}), // invalid UTF-8
- },
- }
- // GetSecret with no property returns the full JSON; json.Marshal handles invalid UTF-8.
- _, err := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{Key: "0"})
- require.NoError(t, err)
- }
- // TestGetSecretEmptySecretsList tests GetSecret when the secrets list is empty.
- func TestGetSecretEmptySecretsList(t *testing.T) {
- ctx := context.Background()
- c := &client{
- api: &fakeAPI{secrets: []*server.Secret{}},
- }
- _, err := c.getSecret(ctx, esv1.ExternalSecretDataRemoteRef{Key: "nonexistent"})
- assert.Error(t, err)
- // 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.
- func TestGetSecretWithVersion(t *testing.T) {
- ctx := context.Background()
- c := newTestClient(t)
- testCases := map[string]struct {
- ref esv1.ExternalSecretDataRemoteRef
- wantErr bool
- errMsg string
- }{
- "returns error when version is specified": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "1000",
- Version: "v1",
- },
- wantErr: true,
- errMsg: "specifying a version is not supported",
- },
- }
- for name, tc := range testCases {
- t.Run(name, func(t *testing.T) {
- got, err := c.GetSecret(ctx, tc.ref)
- assert.Error(t, err)
- assert.Nil(t, got)
- assert.Equal(t, tc.errMsg, err.Error())
- })
- }
- }
- // 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.
- func TestPushSecret(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}}`),
- }
- // 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.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.
- func TestDeleteSecret(t *testing.T) {
- ctx := context.Background()
- c := newTestClient(t)
- 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.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.
- func TestSecretExists(t *testing.T) {
- ctx := context.Background()
- c := newTestClient(t)
- 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.
- func TestValidate(t *testing.T) {
- c := newTestClient(t)
- result, err := c.Validate()
- assert.NoError(t, err)
- assert.Equal(t, esv1.ValidationResultReady, result)
- }
- func TestValidateAPIError(t *testing.T) {
- c := &client{api: &fakeAPI{secretsErr: errors.New("401 Unauthorized: invalid credentials")}}
- result, err := c.Validate()
- assert.Error(t, err)
- assert.Equal(t, esv1.ValidationResultError, result)
- assert.Contains(t, err.Error(), "failed to validate Secret Server credentials")
- }
- // TestValidateNilAPI tests the Validate functionality with nil API.
- func TestValidateNilAPI(t *testing.T) {
- c := &client{api: nil}
- result, err := c.Validate()
- assert.Error(t, err)
- assert.Equal(t, esv1.ValidationResultError, result)
- }
- // TestGetSecretMap tests the GetSecretMap functionality.
- func TestGetSecretMap(t *testing.T) {
- ctx := context.Background()
- c := newTestClient(t)
- testCases := map[string]struct {
- ref esv1.ExternalSecretDataRemoteRef
- want map[string][]byte
- wantErr bool
- }{
- "successfully retrieve secret map with valid JSON": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "1000",
- },
- want: map[string][]byte{
- "user": []byte("robertOppenheimer"),
- "password": []byte("badPassword"),
- "server": []byte("192.168.1.50"),
- },
- wantErr: false,
- },
- "successfully retrieve non-JSON data field map": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "9999",
- },
- want: map[string][]byte{
- "data": []byte("simulated error"),
- },
- wantErr: false,
- },
- "error when secret has nil fields": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "7000",
- },
- want: nil,
- wantErr: true,
- },
- "error when secret has empty fields": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "8000",
- },
- want: nil,
- wantErr: true,
- },
- "successfully retrieve secret map with nested values": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "2000",
- },
- want: map[string][]byte{
- "user": []byte("helloWorld"),
- "password": []byte("badPassword"),
- "server": []byte("[\"192.168.1.50\",\"192.168.1.51\"]"),
- },
- wantErr: false,
- },
- "successfully retrieve multi-field template secret map": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "4000",
- },
- want: map[string][]byte{
- "username": []byte("usernamevalue"),
- "password": []byte("passwordvalue"),
- },
- wantErr: false,
- },
- "successfully retrieve plain text field map": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "5000",
- },
- want: map[string][]byte{
- "content": []byte("non-json-secret-value"),
- },
- wantErr: false,
- },
- }
- for name, tc := range testCases {
- t.Run(name, func(t *testing.T) {
- got, err := c.GetSecretMap(ctx, tc.ref)
- if tc.wantErr {
- assert.Error(t, err)
- assert.Nil(t, got)
- } else {
- assert.NoError(t, err)
- assert.Equal(t, tc.want, got)
- }
- })
- }
- }
- func TestGetSecretMapWithProperty(t *testing.T) {
- ctx := context.Background()
- c := &client{api: &fakeAPI{secrets: []*server.Secret{
- {
- ID: 1000,
- Name: "json-secret",
- Fields: []server.SecretField{
- {
- FieldName: "Data",
- Slug: "data",
- ItemValue: `{"credentials":{"username":"alice","password":"secret"},"server":"example.com"}`,
- },
- },
- },
- {
- ID: 2000,
- Name: "multi-field-secret",
- Fields: []server.SecretField{
- {FieldName: "Username", Slug: "username", ItemValue: "bob"},
- {FieldName: "Password", Slug: "password", ItemValue: "secret"},
- },
- },
- }}}
- t.Run("extracts JSON object property with existing gjson behavior", func(t *testing.T) {
- got, err := c.GetSecretMap(ctx, esv1.ExternalSecretDataRemoteRef{
- Key: "1000",
- Property: "credentials",
- })
- assert.NoError(t, err)
- assert.Equal(t, map[string][]byte{
- "username": []byte("alice"),
- "password": []byte("secret"),
- }, got)
- })
- t.Run("returns a single field map for scalar properties", func(t *testing.T) {
- got, err := c.GetSecretMap(ctx, esv1.ExternalSecretDataRemoteRef{
- Key: "2000",
- Property: "username",
- })
- assert.NoError(t, err)
- assert.Equal(t, map[string][]byte{
- "username": []byte("bob"),
- }, got)
- })
- }
- // TestGetSecretMapInvalidJSON verifies that a first field whose value starts
- // with "{" but is not valid JSON is treated as a plain field value (mapped by
- // slug) rather than causing GetSecretMap to fail.
- func TestGetSecretMapInvalidJSON(t *testing.T) {
- ctx := context.Background()
- c := newTestClient(t)
- // Overwrite one secret's value with something that looks like JSON but isn't.
- fake := c.(*client).api.(*fakeAPI)
- fake.secrets[0].Fields[0].ItemValue = "{invalid-json"
- got, err := c.GetSecretMap(ctx, esv1.ExternalSecretDataRemoteRef{Key: "1000"})
- assert.NoError(t, err)
- assert.Equal(t, []byte("{invalid-json"), got["data"])
- }
- // TestGetSecretMapValidJSON tests GetSecretMap with valid JSON data succeeds.
- func TestGetSecretMapValidJSON(t *testing.T) {
- ctx := context.Background()
- c := newTestClient(t)
- // GetSecretMap with valid JSON should succeed
- result, err := c.GetSecretMap(ctx, esv1.ExternalSecretDataRemoteRef{Key: "1000"})
- assert.NoError(t, err)
- assert.NotNil(t, result)
- assert.Equal(t, []byte("robertOppenheimer"), result["user"])
- }
- // TestClose tests the Close functionality.
- func TestClose(t *testing.T) {
- ctx := context.Background()
- c := newTestClient(t)
- err := c.Close(ctx)
- assert.NoError(t, err)
- }
- // TestGetAllSecrets tests the GetAllSecrets functionality.
- func TestGetAllSecrets(t *testing.T) {
- ctx := context.Background()
- c := newTestClient(t)
- testCases := map[string]struct {
- ref esv1.ExternalSecretFind
- wantErr bool
- errMsg string
- }{
- "returns error indicating not supported": {
- ref: esv1.ExternalSecretFind{
- Path: new("some-path"),
- },
- wantErr: true,
- errMsg: "getting all secrets is not supported by Delinea Secret Server",
- },
- "returns error with nil path": {
- ref: esv1.ExternalSecretFind{},
- wantErr: true,
- errMsg: "getting all secrets is not supported by Delinea Secret Server",
- },
- }
- for name, tc := range testCases {
- t.Run(name, func(t *testing.T) {
- got, err := c.GetAllSecrets(ctx, tc.ref)
- assert.Error(t, err)
- assert.Nil(t, got)
- assert.Equal(t, tc.errMsg, err.Error())
- })
- }
- }
- // 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")
- }
|