| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919 |
- /*
- 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
- }
- 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) {
- // 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)
- }
- // TestValidateNilAPI tests the Validate functionality with nil API.
- func TestValidateNilAPI(t *testing.T) {
- c := &client{api: nil}
- result, err := c.Validate()
- // Validate always succeeds and returns ValidationResultReady regardless of API state
- assert.NoError(t, err)
- assert.Equal(t, esv1.ValidationResultReady, 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,
- },
- // 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": {
- ref: esv1.ExternalSecretDataRemoteRef{
- Key: "9999",
- },
- want: nil,
- wantErr: true,
- },
- "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,
- },
- }
- 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)
- }
- })
- }
- }
- // TestGetSecretMapInvalidJSON tests GetSecretMap with invalid JSON in secret.
- func TestGetSecretMapInvalidJSON(t *testing.T) {
- ctx := context.Background()
- c := newTestClient(t)
- // Overwrite one secret's value with invalid JSON
- fake := c.(*client).api.(*fakeAPI)
- fake.secrets[0].Fields[0].ItemValue = "{invalid-json"
- _, err := c.GetSecretMap(ctx, esv1.ExternalSecretDataRemoteRef{Key: "1000"})
- assert.Error(t, err)
- }
- // 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")
- }
|