client_test.go 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002
  1. /*
  2. Copyright © The ESO Authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package secretserver
  14. import (
  15. "context"
  16. "encoding/json"
  17. "errors"
  18. "fmt"
  19. "io"
  20. "os"
  21. "testing"
  22. "github.com/DelineaXPM/tss-sdk-go/v3/server"
  23. "github.com/stretchr/testify/assert"
  24. "github.com/stretchr/testify/require"
  25. corev1 "k8s.io/api/core/v1"
  26. apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
  27. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  28. )
  29. var (
  30. errNotFound = errors.New("not found")
  31. )
  32. type fakeAPI struct {
  33. secrets []*server.Secret
  34. secretsErr error
  35. }
  36. const (
  37. usernameSlug = "username"
  38. passwordSlug = "password"
  39. )
  40. func (f *fakeAPI) Secret(id int) (*server.Secret, error) {
  41. for _, s := range f.secrets {
  42. if s.ID == id {
  43. return s, nil
  44. }
  45. }
  46. return nil, errNotFound
  47. }
  48. func (f *fakeAPI) Secrets(searchText, _ string) ([]server.Secret, error) {
  49. if f.secretsErr != nil {
  50. return nil, f.secretsErr
  51. }
  52. // Match real SDK behavior: return ([]Secret{}, nil) for zero matches,
  53. // NOT (nil, errNotFound). The real SDK's searchResources returns an empty
  54. // SearchResult.Records slice and make([]Secret, 0).
  55. var secrets []server.Secret
  56. for _, s := range f.secrets {
  57. if s.Name == searchText {
  58. secrets = append(secrets, *s)
  59. }
  60. }
  61. if secrets == nil {
  62. secrets = []server.Secret{}
  63. }
  64. return secrets, nil
  65. }
  66. func (f *fakeAPI) SecretByPath(path string) (*server.Secret, error) {
  67. for _, s := range f.secrets {
  68. if "/"+s.Name == path || s.Name == path {
  69. return s, nil
  70. }
  71. }
  72. return nil, errNotFound
  73. }
  74. // CreateSecret is a mock implementation of the Secret Server API CreateSecret method.
  75. // It returns a predefined secret based on the SecretTemplateID provided.
  76. func (f *fakeAPI) CreateSecret(secret server.Secret) (*server.Secret, error) {
  77. if secret.Name == "simulate-create-error" {
  78. return nil, errors.New("simulated create error")
  79. }
  80. secret.ID = len(f.secrets) + 10000
  81. // Simulate populating FieldName and Slug based on FieldID
  82. template, _ := f.SecretTemplate(secret.SecretTemplateID)
  83. if template != nil {
  84. for i, field := range secret.Fields {
  85. for _, tField := range template.Fields {
  86. if tField.SecretTemplateFieldID == field.FieldID {
  87. secret.Fields[i].Slug = tField.FieldSlugName
  88. secret.Fields[i].FieldName = tField.Name
  89. }
  90. }
  91. }
  92. }
  93. f.secrets = append(f.secrets, &secret)
  94. return &secret, nil
  95. }
  96. // UpdateSecret is a mock implementation of the Secret Server API UpdateSecret method.
  97. // It returns an error if a predefined test condition is met, otherwise it simulates success.
  98. func (f *fakeAPI) UpdateSecret(secret server.Secret) (*server.Secret, error) {
  99. for i, s := range f.secrets {
  100. if s.ID == secret.ID {
  101. f.secrets[i] = &secret
  102. return &secret, nil
  103. }
  104. }
  105. return nil, errNotFound
  106. }
  107. // DeleteSecret is a mock implementation of the Secret Server API DeleteSecret method.
  108. // It returns an error if the id corresponds to a simulated failure case.
  109. func (f *fakeAPI) DeleteSecret(id int) error {
  110. if id == 9999 {
  111. return errors.New("simulated backend deletion error")
  112. }
  113. for i, s := range f.secrets {
  114. if s.ID == id {
  115. f.secrets = append(f.secrets[:i], f.secrets[i+1:]...)
  116. return nil
  117. }
  118. }
  119. return errNotFound
  120. }
  121. // SecretTemplate is a mock implementation of the Secret Server API SecretTemplate method.
  122. // It returns a predefined template or an error based on the requested id.
  123. func (f *fakeAPI) SecretTemplate(id int) (*server.SecretTemplate, error) {
  124. if id == 999 {
  125. return nil, errors.New("template not found")
  126. }
  127. return &server.SecretTemplate{
  128. ID: id,
  129. Name: "Test Template",
  130. Fields: []server.SecretTemplateField{
  131. {
  132. SecretTemplateFieldID: 1,
  133. FieldSlugName: "username",
  134. Name: "Username",
  135. },
  136. {
  137. SecretTemplateFieldID: 2,
  138. FieldSlugName: "password",
  139. Name: "Password",
  140. },
  141. {
  142. SecretTemplateFieldID: 3,
  143. FieldSlugName: "notes",
  144. Name: "Notes",
  145. },
  146. },
  147. }, nil
  148. }
  149. func createSecret(id int, itemValue string) (*server.Secret, error) {
  150. s, err := jsonData()
  151. if err != nil {
  152. return nil, err
  153. }
  154. s.ID = id
  155. s.Fields[0].ItemValue = itemValue
  156. return s, nil
  157. }
  158. func jsonData() (*server.Secret, error) {
  159. var s = &server.Secret{}
  160. jsonFile, err := os.Open("test_data.json")
  161. if err != nil {
  162. return nil, err
  163. }
  164. defer jsonFile.Close()
  165. byteValue, err := io.ReadAll(jsonFile)
  166. if err != nil {
  167. return nil, err
  168. }
  169. err = json.Unmarshal(byteValue, &s)
  170. if err != nil {
  171. return nil, err
  172. }
  173. return s, nil
  174. }
  175. func createTestSecretFromCode(id int) *server.Secret {
  176. s := new(server.Secret)
  177. s.ID = id
  178. s.Name = "Secretname"
  179. s.Fields = make([]server.SecretField, 2)
  180. s.Fields[0].ItemValue = "usernamevalue"
  181. s.Fields[0].FieldName = "Username"
  182. s.Fields[0].Slug = usernameSlug
  183. s.Fields[1].FieldName = "Password"
  184. s.Fields[1].Slug = passwordSlug
  185. s.Fields[1].ItemValue = "passwordvalue"
  186. return s
  187. }
  188. func createTestFolderSecret(id, folderId int) *server.Secret {
  189. s := new(server.Secret)
  190. s.FolderID = folderId
  191. s.ID = id
  192. s.Name = "FolderSecretname"
  193. s.Fields = make([]server.SecretField, 2)
  194. s.Fields[0].ItemValue = "usernamevalue"
  195. s.Fields[0].FieldName = "Username"
  196. s.Fields[0].Slug = usernameSlug
  197. s.Fields[1].FieldName = "Password"
  198. s.Fields[1].Slug = passwordSlug
  199. s.Fields[1].ItemValue = "passwordvalue"
  200. return s
  201. }
  202. func createPlainTextSecret(id int) *server.Secret {
  203. s := new(server.Secret)
  204. s.ID = id
  205. s.Name = "PlainTextSecret"
  206. s.Fields = make([]server.SecretField, 1)
  207. s.Fields[0].FieldName = "Content"
  208. s.Fields[0].Slug = "content"
  209. s.Fields[0].ItemValue = `non-json-secret-value`
  210. return s
  211. }
  212. func createNilFieldsSecret(id int) *server.Secret {
  213. s := new(server.Secret)
  214. s.ID = id
  215. s.Name = "NilFieldsSecret"
  216. s.Fields = nil
  217. return s
  218. }
  219. func createEmptyFieldsSecret(id int) *server.Secret {
  220. s := new(server.Secret)
  221. s.ID = id
  222. s.Name = "EmptyFieldsSecret"
  223. s.Fields = []server.SecretField{}
  224. return s
  225. }
  226. func newTestClient(t *testing.T) esv1.SecretsClient {
  227. // Build secrets list while handling any errors from createSecret
  228. var secrets []*server.Secret //nolint:prealloc // populated incrementally
  229. s, err := createSecret(1000, "{ \"user\": \"robertOppenheimer\", \"password\": \"badPassword\",\"server\":\"192.168.1.50\"}")
  230. require.NoError(t, err)
  231. s2, err := createSecret(2000, "{ \"user\": \"helloWorld\", \"password\": \"badPassword\",\"server\":[ \"192.168.1.50\",\"192.168.1.51\"] }")
  232. require.NoError(t, err)
  233. s3, err := createSecret(3000, "{ \"user\": \"chuckTesta\", \"password\": \"badPassword\",\"server\":\"192.168.1.50\"}")
  234. require.NoError(t, err)
  235. secrets = append(secrets, s, s2, s3, createTestSecretFromCode(4000), createPlainTextSecret(5000))
  236. s6, err := createSecret(6000, "{ \"user\": \"betaTest\", \"password\": \"badPassword\" }")
  237. require.NoError(t, err)
  238. secrets = append(secrets, s6, createNilFieldsSecret(7000), createEmptyFieldsSecret(8000), createTestFolderSecret(9000, 4), createTestFolderSecret(9001, 5))
  239. // Create a secret for path-based test
  240. pathSecret := &server.Secret{
  241. ID: 9002,
  242. Name: "/some/path/secret",
  243. FolderID: 6,
  244. Fields: []server.SecretField{
  245. {FieldName: "Password", Slug: "password", ItemValue: "old_path_value"},
  246. },
  247. }
  248. secrets = append(secrets, pathSecret)
  249. s9999, err := createSecret(9999, "simulated error")
  250. require.NoError(t, err)
  251. secrets = append(secrets, s9999)
  252. return &client{
  253. api: &fakeAPI{
  254. secrets: secrets,
  255. },
  256. }
  257. }
  258. func TestGetSecretSecretServer(t *testing.T) {
  259. ctx := context.Background()
  260. c := newTestClient(t)
  261. s, err := jsonData()
  262. require.NoError(t, err)
  263. jsonStr, err := json.Marshal(s)
  264. require.NoError(t, err)
  265. jsonStr2, err := json.Marshal(createTestSecretFromCode(4000))
  266. require.NoError(t, err)
  267. jsonStr3, err := json.Marshal(createPlainTextSecret(5000))
  268. require.NoError(t, err)
  269. jsonStr4, err := json.Marshal(createTestFolderSecret(9000, 4))
  270. require.NoError(t, err)
  271. testCases := map[string]struct {
  272. ref esv1.ExternalSecretDataRemoteRef
  273. want []byte
  274. err error
  275. errMsg string // when set, asserts Contains(err.Error(), errMsg) instead of exact error match
  276. }{
  277. "incorrect key returns nil and error": {
  278. ref: esv1.ExternalSecretDataRemoteRef{
  279. Key: "0",
  280. },
  281. want: []byte(nil),
  282. errMsg: errMsgNoMatchingSecrets,
  283. },
  284. "key = 'secret name' and user property returns a single value": {
  285. ref: esv1.ExternalSecretDataRemoteRef{
  286. Key: "ESO-test-secret",
  287. Property: "user",
  288. },
  289. want: []byte(`robertOppenheimer`),
  290. },
  291. "Secret from JSON: key and password property returns a single value": {
  292. ref: esv1.ExternalSecretDataRemoteRef{
  293. Key: "1000",
  294. Property: "password",
  295. },
  296. want: []byte(`badPassword`),
  297. },
  298. "Secret from JSON: key and nested property returns a single value": {
  299. ref: esv1.ExternalSecretDataRemoteRef{
  300. Key: "2000",
  301. Property: "server.1",
  302. },
  303. want: []byte(`192.168.1.51`),
  304. },
  305. "Secret from JSON: existent key with non-existing property": {
  306. ref: esv1.ExternalSecretDataRemoteRef{
  307. Key: "3000",
  308. Property: "foo.bar",
  309. },
  310. err: esv1.NoSecretError{},
  311. },
  312. "Secret from JSON: existent 'name' key with no property": {
  313. ref: esv1.ExternalSecretDataRemoteRef{
  314. Key: "1000",
  315. },
  316. want: jsonStr,
  317. },
  318. "Secret from code: existent key with no property": {
  319. ref: esv1.ExternalSecretDataRemoteRef{
  320. Key: "4000",
  321. },
  322. want: jsonStr2,
  323. },
  324. "Secret from code: key and username fieldnamereturns a single value": {
  325. ref: esv1.ExternalSecretDataRemoteRef{
  326. Key: "4000",
  327. Property: "Username",
  328. },
  329. want: []byte(`usernamevalue`),
  330. },
  331. "Plain text secret: existent key with no property": {
  332. ref: esv1.ExternalSecretDataRemoteRef{
  333. Key: "5000",
  334. },
  335. want: jsonStr3,
  336. },
  337. "Plain text secret: key with property returns expected value": {
  338. ref: esv1.ExternalSecretDataRemoteRef{
  339. Key: "5000",
  340. Property: "Content",
  341. },
  342. want: []byte(`non-json-secret-value`),
  343. },
  344. "Secret from code: valid ItemValue but incorrect property returns noSecretError": {
  345. ref: esv1.ExternalSecretDataRemoteRef{
  346. Key: "6000",
  347. Property: "missing",
  348. },
  349. want: []byte(nil),
  350. err: esv1.NoSecretError{},
  351. },
  352. "Secret from code: nil Fields returns error": {
  353. ref: esv1.ExternalSecretDataRemoteRef{
  354. Key: "7000",
  355. },
  356. want: []byte(nil),
  357. errMsg: "secret contains no fields",
  358. },
  359. "Secret from code: empty Fields returns error": {
  360. ref: esv1.ExternalSecretDataRemoteRef{
  361. Key: "8000",
  362. },
  363. want: []byte(nil),
  364. errMsg: "secret contains no fields",
  365. },
  366. "Secret from code: 'name' and password slug returns a single value": {
  367. ref: esv1.ExternalSecretDataRemoteRef{
  368. Key: "Secretname",
  369. Property: "password",
  370. },
  371. want: []byte(`passwordvalue`),
  372. },
  373. "Secret from code: 'name' not found returns unable to retrieve secret error": {
  374. ref: esv1.ExternalSecretDataRemoteRef{
  375. Key: "Secretnameerror",
  376. Property: "password",
  377. },
  378. want: []byte(nil),
  379. errMsg: errMsgNoMatchingSecrets,
  380. },
  381. "Secret from code: 'name' found and non-existent attribute slug returns noSecretError": {
  382. ref: esv1.ExternalSecretDataRemoteRef{
  383. Key: "Secretname",
  384. Property: "passwordkey",
  385. },
  386. want: []byte(nil),
  387. err: esv1.NoSecretError{},
  388. },
  389. "Secret by path: valid path returns secret": {
  390. ref: esv1.ExternalSecretDataRemoteRef{
  391. Key: "/FolderSecretname",
  392. },
  393. want: jsonStr4,
  394. },
  395. "Secret by path: invalid path returns error": {
  396. ref: esv1.ExternalSecretDataRemoteRef{
  397. Key: "/invalid/secret/path",
  398. },
  399. want: []byte(nil),
  400. errMsg: "not found",
  401. },
  402. }
  403. for name, tc := range testCases {
  404. t.Run(name, func(t *testing.T) {
  405. got, err := c.GetSecret(ctx, tc.ref)
  406. if tc.err == nil && tc.errMsg == "" {
  407. assert.NoError(t, err)
  408. assert.Equal(t, tc.want, got)
  409. } else {
  410. assert.Nil(t, got)
  411. if tc.errMsg != "" {
  412. assert.ErrorContains(t, err, tc.errMsg)
  413. } else {
  414. assert.ErrorIs(t, err, tc.err)
  415. }
  416. }
  417. })
  418. }
  419. }
  420. // TestGetSecretWithInvalidUTF8ItemValue tests GetSecret with invalid UTF-8 in ItemValue.
  421. // json.Marshal in Go handles invalid UTF-8 strings without error, so this verifies
  422. // that GetSecret succeeds in this edge case.
  423. func TestGetSecretWithInvalidUTF8ItemValue(t *testing.T) {
  424. ctx := t.Context()
  425. bad := &server.Secret{
  426. ID: 0,
  427. Fields: []server.SecretField{},
  428. }
  429. c := &client{
  430. api: &fakeAPI{
  431. secrets: []*server.Secret{bad},
  432. },
  433. }
  434. bad.Fields = []server.SecretField{
  435. {
  436. FieldName: "Foo",
  437. ItemValue: string([]byte{0xff, 0xfe}), // invalid UTF-8
  438. },
  439. }
  440. // GetSecret with no property returns the full JSON; json.Marshal handles invalid UTF-8.
  441. _, err := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{Key: "0"})
  442. require.NoError(t, err)
  443. }
  444. // TestGetSecretEmptySecretsList tests GetSecret when the secrets list is empty.
  445. func TestGetSecretEmptySecretsList(t *testing.T) {
  446. ctx := context.Background()
  447. c := &client{
  448. api: &fakeAPI{secrets: []*server.Secret{}},
  449. }
  450. _, err := c.getSecret(ctx, esv1.ExternalSecretDataRemoteRef{Key: "nonexistent"})
  451. assert.Error(t, err)
  452. // fakeAPI.Secrets now returns ([]Secret{}, nil) for zero matches (matching real SDK),
  453. // so getSecretByName returns errMsgNoMatchingSecrets.
  454. assert.Contains(t, err.Error(), errMsgNoMatchingSecrets)
  455. }
  456. // TestGetSecretWithVersion tests that specifying a version returns an error.
  457. func TestGetSecretWithVersion(t *testing.T) {
  458. ctx := context.Background()
  459. c := newTestClient(t)
  460. testCases := map[string]struct {
  461. ref esv1.ExternalSecretDataRemoteRef
  462. wantErr bool
  463. errMsg string
  464. }{
  465. "returns error when version is specified": {
  466. ref: esv1.ExternalSecretDataRemoteRef{
  467. Key: "1000",
  468. Version: "v1",
  469. },
  470. wantErr: true,
  471. errMsg: "specifying a version is not supported",
  472. },
  473. }
  474. for name, tc := range testCases {
  475. t.Run(name, func(t *testing.T) {
  476. got, err := c.GetSecret(ctx, tc.ref)
  477. assert.Error(t, err)
  478. assert.Nil(t, got)
  479. assert.Equal(t, tc.errMsg, err.Error())
  480. })
  481. }
  482. }
  483. // fakePushSecretData implements esv1.PushSecretData for testing.
  484. type fakePushSecretData struct {
  485. remoteKey string
  486. property string
  487. secretKey string
  488. metadata *apiextensionsv1.JSON
  489. }
  490. // GetRemoteKey returns the remote key for the fake push secret data.
  491. func (f fakePushSecretData) GetRemoteKey() string { return f.remoteKey }
  492. // GetProperty returns the property for the fake push secret data.
  493. func (f fakePushSecretData) GetProperty() string { return f.property }
  494. // GetSecretKey returns the secret key for the fake push secret data.
  495. func (f fakePushSecretData) GetSecretKey() string { return f.secretKey }
  496. // GetMetadata returns the metadata for the fake push secret data.
  497. func (f fakePushSecretData) GetMetadata() *apiextensionsv1.JSON { return f.metadata }
  498. // fakePushSecretRemoteRef implements esv1.PushSecretRemoteRef for testing.
  499. type fakePushSecretRemoteRef struct {
  500. remoteKey string
  501. property string
  502. }
  503. // GetRemoteKey returns the remote key for the fake remote ref.
  504. func (f fakePushSecretRemoteRef) GetRemoteKey() string { return f.remoteKey }
  505. // GetProperty returns the property for the fake remote ref.
  506. func (f fakePushSecretRemoteRef) GetProperty() string { return f.property }
  507. // TestPushSecret tests the PushSecret functionality.
  508. func TestPushSecret(t *testing.T) {
  509. ctx := context.Background()
  510. c := newTestClient(t)
  511. secret := &corev1.Secret{
  512. Data: map[string][]byte{
  513. "my-key": []byte("my-value"),
  514. },
  515. }
  516. metadataJSON := apiextensionsv1.JSON{
  517. Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 1}}`),
  518. }
  519. // Create a new secret
  520. data := fakePushSecretData{
  521. remoteKey: "new-secret",
  522. property: "username",
  523. secretKey: "my-key",
  524. metadata: &metadataJSON,
  525. }
  526. err := c.PushSecret(ctx, secret, data)
  527. assert.NoError(t, err)
  528. // Verify the secret was created
  529. createdSecret, _ := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{Key: "new-secret", Property: "username"})
  530. assert.Equal(t, []byte("my-value"), createdSecret)
  531. // Create a new secret with path-like key and folderId
  532. dataPathCreate := fakePushSecretData{
  533. remoteKey: "/some/new/path/secretname",
  534. property: "username",
  535. secretKey: "my-key",
  536. metadata: &metadataJSON,
  537. }
  538. err = c.PushSecret(ctx, secret, dataPathCreate)
  539. assert.NoError(t, err)
  540. // verify that the created secret has just the basename "secretname"
  541. // and since it's the 10th secret created by fakeAPI, its ID would be 10000 + len(secrets)
  542. foundSecrets, _ := c.(*client).api.Secrets("secretname", "Name")
  543. assert.Len(t, foundSecrets, 1)
  544. assert.Equal(t, "secretname", foundSecrets[0].Name)
  545. assert.Equal(t, 1, foundSecrets[0].FolderID)
  546. // Update an existing secret
  547. dataUpdate := fakePushSecretData{
  548. remoteKey: "4000",
  549. property: "password",
  550. secretKey: "my-key", // "my-value" will replace the badPassword
  551. }
  552. err = c.PushSecret(ctx, secret, dataUpdate)
  553. assert.NoError(t, err)
  554. // Verify update
  555. updatedSecret, _ := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{Key: "4000", Property: "password"})
  556. assert.Equal(t, []byte("my-value"), updatedSecret)
  557. // Missing metadata for new secret
  558. dataMissingMeta := fakePushSecretData{
  559. remoteKey: "new-secret-no-meta",
  560. property: "username",
  561. secretKey: "my-key",
  562. metadata: nil,
  563. }
  564. err = c.PushSecret(ctx, secret, dataMissingMeta)
  565. assert.Error(t, err)
  566. assert.Contains(t, err.Error(), "folderId and secretTemplateId must be provided in metadata to create a new secret")
  567. // Invalid secretTemplateId in metadata
  568. invalidMetadataJSON := apiextensionsv1.JSON{
  569. Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 999}}`), // non-existent template
  570. }
  571. dataInvalidMeta := fakePushSecretData{
  572. remoteKey: "new-secret-invalid-meta",
  573. property: "username",
  574. secretKey: "my-key",
  575. metadata: &invalidMetadataJSON,
  576. }
  577. err = c.PushSecret(ctx, secret, dataInvalidMeta)
  578. assert.Error(t, err)
  579. assert.Contains(t, err.Error(), "failed to get secret template")
  580. // Simulate create error
  581. // Requires modifying fakeAPI to return an error when Name == "simulate-create-error"
  582. dataCreateError := fakePushSecretData{
  583. remoteKey: "simulate-create-error",
  584. property: "username",
  585. secretKey: "my-key",
  586. metadata: &metadataJSON,
  587. }
  588. err = c.PushSecret(ctx, secret, dataCreateError)
  589. assert.Error(t, err)
  590. assert.Contains(t, err.Error(), "failed to create secret")
  591. // Update with non-existent property
  592. dataUpdateInvalidProp := fakePushSecretData{
  593. remoteKey: "4000",
  594. property: "non-existent-property",
  595. secretKey: "my-key",
  596. }
  597. err = c.PushSecret(ctx, secret, dataUpdateInvalidProp)
  598. assert.Error(t, err)
  599. assert.Contains(t, err.Error(), "field non-existent-property not found in secret")
  600. // Update duplicate-named secret in specific folder (ID 9001 in FolderID 5)
  601. metadataFolder5 := apiextensionsv1.JSON{
  602. Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 5, "secretTemplateId": 1}}`),
  603. }
  604. dataFolderUpdate := fakePushSecretData{
  605. remoteKey: "FolderSecretname",
  606. property: "password",
  607. secretKey: "my-key",
  608. metadata: &metadataFolder5,
  609. }
  610. err = c.PushSecret(ctx, secret, dataFolderUpdate)
  611. assert.NoError(t, err)
  612. // Verify only the secret in folder 5 was updated
  613. s9001, _ := c.(*client).api.Secret(9001)
  614. s9000, _ := c.(*client).api.Secret(9000)
  615. // Check the password field
  616. var s9001PW, s9000PW string
  617. for _, f := range s9001.Fields {
  618. if f.Slug == passwordSlug {
  619. s9001PW = f.ItemValue
  620. }
  621. }
  622. for _, f := range s9000.Fields {
  623. if f.Slug == passwordSlug {
  624. s9000PW = f.ItemValue
  625. }
  626. }
  627. assert.Equal(t, "my-value", s9001PW)
  628. assert.Equal(t, "passwordvalue", s9000PW) // Unchanged
  629. // Update path-based key secret
  630. dataPathUpdate := fakePushSecretData{
  631. remoteKey: "/some/path/secret",
  632. property: "password",
  633. secretKey: "my-key",
  634. }
  635. err = c.PushSecret(ctx, secret, dataPathUpdate)
  636. assert.NoError(t, err)
  637. sPath, _ := c.(*client).api.Secret(9002)
  638. var sPathPW string
  639. for _, f := range sPath.Fields {
  640. if f.Slug == passwordSlug {
  641. sPathPW = f.ItemValue
  642. }
  643. }
  644. assert.Equal(t, "my-value", sPathPW)
  645. // Push invalid UTF-8 secret
  646. invalidUtf8Secret := &corev1.Secret{
  647. Data: map[string][]byte{
  648. "invalid-utf8": {0xff, 0xfe, 0xfd},
  649. },
  650. }
  651. dataInvalidUtf8 := fakePushSecretData{
  652. remoteKey: "new-secret-utf8",
  653. property: "username",
  654. secretKey: "invalid-utf8",
  655. metadata: &metadataJSON,
  656. }
  657. err = c.PushSecret(ctx, invalidUtf8Secret, dataInvalidUtf8)
  658. assert.Error(t, err)
  659. assert.Contains(t, err.Error(), "secret value is not valid UTF-8")
  660. }
  661. // TestDeleteSecret tests the DeleteSecret functionality.
  662. func TestDeleteSecret(t *testing.T) {
  663. ctx := context.Background()
  664. c := newTestClient(t)
  665. ref := fakePushSecretRemoteRef{
  666. remoteKey: "1000",
  667. }
  668. // Should exist initially
  669. exists, err := c.SecretExists(ctx, ref)
  670. assert.NoError(t, err)
  671. assert.True(t, exists)
  672. // Delete it
  673. err = c.DeleteSecret(ctx, ref)
  674. assert.NoError(t, err)
  675. // Should not exist now
  676. exists, err = c.SecretExists(ctx, ref)
  677. assert.NoError(t, err)
  678. assert.False(t, exists)
  679. // Test idempotency: delete again should not error
  680. err = c.DeleteSecret(ctx, ref)
  681. assert.NoError(t, err)
  682. // Test path-based key deletion
  683. pathRef := fakePushSecretRemoteRef{
  684. remoteKey: "/some/path/secret",
  685. }
  686. exists, err = c.SecretExists(ctx, pathRef)
  687. assert.NoError(t, err)
  688. assert.True(t, exists)
  689. err = c.DeleteSecret(ctx, pathRef)
  690. assert.NoError(t, err)
  691. exists, err = c.SecretExists(ctx, pathRef)
  692. assert.NoError(t, err)
  693. assert.False(t, exists)
  694. }
  695. // TestDeleteSecret_Error tests that an error from the backend during DeleteSecret is propagated.
  696. func TestDeleteSecret_Error(t *testing.T) {
  697. ctx := context.Background()
  698. c := newTestClient(t)
  699. ref := fakePushSecretRemoteRef{
  700. remoteKey: "9999",
  701. }
  702. // Should exist initially
  703. exists, err := c.SecretExists(ctx, ref)
  704. assert.NoError(t, err)
  705. assert.True(t, exists)
  706. // Attempt to delete it, expecting an error
  707. err = c.DeleteSecret(ctx, ref)
  708. assert.Error(t, err)
  709. assert.Contains(t, err.Error(), "failed to delete secret")
  710. // Verify it still exists
  711. exists, err = c.SecretExists(ctx, ref)
  712. assert.NoError(t, err)
  713. assert.True(t, exists)
  714. }
  715. // TestSecretExists tests the SecretExists functionality.
  716. func TestSecretExists(t *testing.T) {
  717. ctx := context.Background()
  718. c := newTestClient(t)
  719. testCases := map[string]struct {
  720. ref esv1.PushSecretRemoteRef
  721. want bool
  722. wantErr bool
  723. }{
  724. "existing secret": {
  725. ref: fakePushSecretRemoteRef{remoteKey: "1000"},
  726. want: true,
  727. wantErr: false,
  728. },
  729. "non-existing secret": {
  730. ref: fakePushSecretRemoteRef{remoteKey: "does-not-exist"},
  731. want: false,
  732. wantErr: false,
  733. },
  734. }
  735. for name, tc := range testCases {
  736. t.Run(name, func(t *testing.T) {
  737. got, err := c.SecretExists(ctx, tc.ref)
  738. if tc.wantErr {
  739. assert.Error(t, err)
  740. } else {
  741. assert.NoError(t, err)
  742. assert.Equal(t, tc.want, got)
  743. }
  744. })
  745. }
  746. }
  747. // TestValidate tests the Validate functionality.
  748. func TestValidate(t *testing.T) {
  749. c := newTestClient(t)
  750. result, err := c.Validate()
  751. assert.NoError(t, err)
  752. assert.Equal(t, esv1.ValidationResultReady, result)
  753. }
  754. func TestValidateAPIError(t *testing.T) {
  755. c := &client{api: &fakeAPI{secretsErr: errors.New("401 Unauthorized: invalid credentials")}}
  756. result, err := c.Validate()
  757. assert.Error(t, err)
  758. assert.Equal(t, esv1.ValidationResultError, result)
  759. assert.Contains(t, err.Error(), "failed to validate Secret Server credentials")
  760. }
  761. // TestValidateNilAPI tests the Validate functionality with nil API.
  762. func TestValidateNilAPI(t *testing.T) {
  763. c := &client{api: nil}
  764. result, err := c.Validate()
  765. assert.Error(t, err)
  766. assert.Equal(t, esv1.ValidationResultError, result)
  767. }
  768. // TestGetSecretMap tests the GetSecretMap functionality.
  769. func TestGetSecretMap(t *testing.T) {
  770. ctx := context.Background()
  771. c := newTestClient(t)
  772. testCases := map[string]struct {
  773. ref esv1.ExternalSecretDataRemoteRef
  774. want map[string][]byte
  775. wantErr bool
  776. }{
  777. "successfully retrieve secret map with valid JSON": {
  778. ref: esv1.ExternalSecretDataRemoteRef{
  779. Key: "1000",
  780. },
  781. want: map[string][]byte{
  782. "user": []byte("robertOppenheimer"),
  783. "password": []byte("badPassword"),
  784. "server": []byte("192.168.1.50"),
  785. },
  786. wantErr: false,
  787. },
  788. "successfully retrieve non-JSON data field map": {
  789. ref: esv1.ExternalSecretDataRemoteRef{
  790. Key: "9999",
  791. },
  792. want: map[string][]byte{
  793. "data": []byte("simulated error"),
  794. },
  795. wantErr: false,
  796. },
  797. "error when secret has nil fields": {
  798. ref: esv1.ExternalSecretDataRemoteRef{
  799. Key: "7000",
  800. },
  801. want: nil,
  802. wantErr: true,
  803. },
  804. "error when secret has empty fields": {
  805. ref: esv1.ExternalSecretDataRemoteRef{
  806. Key: "8000",
  807. },
  808. want: nil,
  809. wantErr: true,
  810. },
  811. "successfully retrieve secret map with nested values": {
  812. ref: esv1.ExternalSecretDataRemoteRef{
  813. Key: "2000",
  814. },
  815. want: map[string][]byte{
  816. "user": []byte("helloWorld"),
  817. "password": []byte("badPassword"),
  818. "server": []byte("[\"192.168.1.50\",\"192.168.1.51\"]"),
  819. },
  820. wantErr: false,
  821. },
  822. "successfully retrieve multi-field template secret map": {
  823. ref: esv1.ExternalSecretDataRemoteRef{
  824. Key: "4000",
  825. },
  826. want: map[string][]byte{
  827. "username": []byte("usernamevalue"),
  828. "password": []byte("passwordvalue"),
  829. },
  830. wantErr: false,
  831. },
  832. "successfully retrieve plain text field map": {
  833. ref: esv1.ExternalSecretDataRemoteRef{
  834. Key: "5000",
  835. },
  836. want: map[string][]byte{
  837. "content": []byte("non-json-secret-value"),
  838. },
  839. wantErr: false,
  840. },
  841. }
  842. for name, tc := range testCases {
  843. t.Run(name, func(t *testing.T) {
  844. got, err := c.GetSecretMap(ctx, tc.ref)
  845. if tc.wantErr {
  846. assert.Error(t, err)
  847. assert.Nil(t, got)
  848. } else {
  849. assert.NoError(t, err)
  850. assert.Equal(t, tc.want, got)
  851. }
  852. })
  853. }
  854. }
  855. func TestGetSecretMapWithProperty(t *testing.T) {
  856. ctx := context.Background()
  857. c := &client{api: &fakeAPI{secrets: []*server.Secret{
  858. {
  859. ID: 1000,
  860. Name: "json-secret",
  861. Fields: []server.SecretField{
  862. {
  863. FieldName: "Data",
  864. Slug: "data",
  865. ItemValue: `{"credentials":{"username":"alice","password":"secret"},"server":"example.com"}`,
  866. },
  867. },
  868. },
  869. {
  870. ID: 2000,
  871. Name: "multi-field-secret",
  872. Fields: []server.SecretField{
  873. {FieldName: "Username", Slug: "username", ItemValue: "bob"},
  874. {FieldName: "Password", Slug: "password", ItemValue: "secret"},
  875. },
  876. },
  877. }}}
  878. t.Run("extracts JSON object property with existing gjson behavior", func(t *testing.T) {
  879. got, err := c.GetSecretMap(ctx, esv1.ExternalSecretDataRemoteRef{
  880. Key: "1000",
  881. Property: "credentials",
  882. })
  883. assert.NoError(t, err)
  884. assert.Equal(t, map[string][]byte{
  885. "username": []byte("alice"),
  886. "password": []byte("secret"),
  887. }, got)
  888. })
  889. t.Run("returns a single field map for scalar properties", func(t *testing.T) {
  890. got, err := c.GetSecretMap(ctx, esv1.ExternalSecretDataRemoteRef{
  891. Key: "2000",
  892. Property: "username",
  893. })
  894. assert.NoError(t, err)
  895. assert.Equal(t, map[string][]byte{
  896. "username": []byte("bob"),
  897. }, got)
  898. })
  899. }
  900. // TestGetSecretMapInvalidJSON verifies that a first field whose value starts
  901. // with "{" but is not valid JSON is treated as a plain field value (mapped by
  902. // slug) rather than causing GetSecretMap to fail.
  903. func TestGetSecretMapInvalidJSON(t *testing.T) {
  904. ctx := context.Background()
  905. c := newTestClient(t)
  906. // Overwrite one secret's value with something that looks like JSON but isn't.
  907. fake := c.(*client).api.(*fakeAPI)
  908. fake.secrets[0].Fields[0].ItemValue = "{invalid-json"
  909. got, err := c.GetSecretMap(ctx, esv1.ExternalSecretDataRemoteRef{Key: "1000"})
  910. assert.NoError(t, err)
  911. assert.Equal(t, []byte("{invalid-json"), got["data"])
  912. }
  913. // TestGetSecretMapValidJSON tests GetSecretMap with valid JSON data succeeds.
  914. func TestGetSecretMapValidJSON(t *testing.T) {
  915. ctx := context.Background()
  916. c := newTestClient(t)
  917. // GetSecretMap with valid JSON should succeed
  918. result, err := c.GetSecretMap(ctx, esv1.ExternalSecretDataRemoteRef{Key: "1000"})
  919. assert.NoError(t, err)
  920. assert.NotNil(t, result)
  921. assert.Equal(t, []byte("robertOppenheimer"), result["user"])
  922. }
  923. // TestClose tests the Close functionality.
  924. func TestClose(t *testing.T) {
  925. ctx := context.Background()
  926. c := newTestClient(t)
  927. err := c.Close(ctx)
  928. assert.NoError(t, err)
  929. }
  930. // TestGetAllSecrets tests the GetAllSecrets functionality.
  931. func TestGetAllSecrets(t *testing.T) {
  932. ctx := context.Background()
  933. c := newTestClient(t)
  934. testCases := map[string]struct {
  935. ref esv1.ExternalSecretFind
  936. wantErr bool
  937. errMsg string
  938. }{
  939. "returns error indicating not supported": {
  940. ref: esv1.ExternalSecretFind{
  941. Path: new("some-path"),
  942. },
  943. wantErr: true,
  944. errMsg: "getting all secrets is not supported by Delinea Secret Server",
  945. },
  946. "returns error with nil path": {
  947. ref: esv1.ExternalSecretFind{},
  948. wantErr: true,
  949. errMsg: "getting all secrets is not supported by Delinea Secret Server",
  950. },
  951. }
  952. for name, tc := range testCases {
  953. t.Run(name, func(t *testing.T) {
  954. got, err := c.GetAllSecrets(ctx, tc.ref)
  955. assert.Error(t, err)
  956. assert.Nil(t, got)
  957. assert.Equal(t, tc.errMsg, err.Error())
  958. })
  959. }
  960. }
  961. // TestIsNotFoundError tests the isNotFoundError function with various error formats.
  962. func TestIsNotFoundError(t *testing.T) {
  963. testCases := map[string]struct {
  964. err error
  965. want bool
  966. }{
  967. "nil error": {
  968. err: nil,
  969. want: false,
  970. },
  971. "exact lowercase not found": {
  972. err: errors.New("not found"),
  973. want: true,
  974. },
  975. "SDK HTTP 404 format": {
  976. err: errors.New("404 Not Found: no secret was found"),
  977. want: true,
  978. },
  979. "SDK HTTP 404 with empty body": {
  980. err: errors.New("404 Not Found: "),
  981. want: true,
  982. },
  983. "unable to retrieve secret at this time": {
  984. err: errors.New("unable to retrieve secret at this time"),
  985. want: true,
  986. },
  987. "unrelated error": {
  988. err: errors.New("connection refused"),
  989. want: false,
  990. },
  991. "field not found in secret (false positive excluded)": {
  992. // This error from updateSecret should NOT be treated as not-found.
  993. err: fmt.Errorf("field password not found in secret"),
  994. want: false,
  995. },
  996. "field not found in secret template (false positive excluded)": {
  997. // This error from createSecret should NOT be treated as not-found.
  998. err: fmt.Errorf("field username not found in secret template"),
  999. want: false,
  1000. },
  1001. "wrapped field not found in secret": {
  1002. // Even when wrapped, the false-positive exclusion applies.
  1003. err: fmt.Errorf("failed to update secret: %w", fmt.Errorf("field password not found in secret")),
  1004. want: false,
  1005. },
  1006. "mixed case Not Found": {
  1007. err: errors.New("Not Found"),
  1008. want: true,
  1009. },
  1010. "SDK HTTP 401 with not found in body": {
  1011. // Auth errors that happen to contain "not found" in the body should NOT
  1012. // be treated as secret-not-found errors. Only 404 is a true not-found.
  1013. err: errors.New("401 Unauthorized: user not found"),
  1014. want: false,
  1015. },
  1016. "SDK HTTP 500 error": {
  1017. err: errors.New("500 Internal Server Error: something went wrong"),
  1018. want: false,
  1019. },
  1020. "our errMsgNotFound sentinel": {
  1021. // From getSecretByName folder mismatch: errors.New(errMsgNotFound)
  1022. err: errors.New(errMsgNotFound),
  1023. want: true,
  1024. },
  1025. "errMsgAmbiguousName is not a not-found error": {
  1026. err: errors.New(errMsgAmbiguousName),
  1027. want: false,
  1028. },
  1029. }
  1030. for name, tc := range testCases {
  1031. t.Run(name, func(t *testing.T) {
  1032. got := isNotFoundError(tc.err)
  1033. assert.Equal(t, tc.want, got)
  1034. })
  1035. }
  1036. }
  1037. // TestPushSecretInvalidPathKeys tests that PushSecret rejects path-style keys with
  1038. // empty final segments (root slash, double slash, etc.) that would produce an empty secret name.
  1039. func TestPushSecretInvalidPathKeys(t *testing.T) {
  1040. ctx := context.Background()
  1041. c := newTestClient(t)
  1042. secret := &corev1.Secret{
  1043. Data: map[string][]byte{
  1044. "my-key": []byte("my-value"),
  1045. },
  1046. }
  1047. metadataJSON := apiextensionsv1.JSON{
  1048. Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 1}}`),
  1049. }
  1050. testCases := map[string]struct {
  1051. remoteKey string
  1052. errMsg string
  1053. }{
  1054. "root slash only": {
  1055. remoteKey: "/",
  1056. errMsg: "invalid secret name",
  1057. },
  1058. "double slash": {
  1059. remoteKey: "//",
  1060. errMsg: "invalid secret name",
  1061. },
  1062. "triple slash": {
  1063. remoteKey: "///",
  1064. errMsg: "invalid secret name",
  1065. },
  1066. "trailing slash on path": {
  1067. remoteKey: "/Folder/Subfolder/",
  1068. errMsg: "invalid secret name",
  1069. },
  1070. }
  1071. for name, tc := range testCases {
  1072. t.Run(name, func(t *testing.T) {
  1073. data := fakePushSecretData{
  1074. remoteKey: tc.remoteKey,
  1075. property: "username",
  1076. secretKey: "my-key",
  1077. metadata: &metadataJSON,
  1078. }
  1079. err := c.PushSecret(ctx, secret, data)
  1080. assert.Error(t, err)
  1081. assert.Contains(t, err.Error(), tc.errMsg)
  1082. })
  1083. }
  1084. }
  1085. // TestParseFolderPrefix tests the parseFolderPrefix helper function.
  1086. func TestParseFolderPrefix(t *testing.T) {
  1087. testCases := map[string]struct {
  1088. key string
  1089. wantFolderID int
  1090. wantName string
  1091. wantHasFolderPfx bool
  1092. }{
  1093. "valid prefix": {
  1094. key: "folderId:73/my-secret",
  1095. wantFolderID: 73,
  1096. wantName: "my-secret",
  1097. wantHasFolderPfx: true,
  1098. },
  1099. "valid prefix with large folder ID": {
  1100. key: "folderId:99999/secret-name",
  1101. wantFolderID: 99999,
  1102. wantName: "secret-name",
  1103. wantHasFolderPfx: true,
  1104. },
  1105. "valid prefix with name containing slashes": {
  1106. key: "folderId:73/sub/path/secret",
  1107. wantFolderID: 73,
  1108. wantName: "sub/path/secret",
  1109. wantHasFolderPfx: true,
  1110. },
  1111. "no prefix - plain name": {
  1112. key: "my-secret",
  1113. wantFolderID: 0,
  1114. wantName: "my-secret",
  1115. wantHasFolderPfx: false,
  1116. },
  1117. "no prefix - numeric key": {
  1118. key: "12345",
  1119. wantFolderID: 0,
  1120. wantName: "12345",
  1121. wantHasFolderPfx: false,
  1122. },
  1123. "no prefix - path key": {
  1124. key: "/Folder/SecretName",
  1125. wantFolderID: 0,
  1126. wantName: "/Folder/SecretName",
  1127. wantHasFolderPfx: false,
  1128. },
  1129. "prefix without slash": {
  1130. key: "folderId:73",
  1131. wantFolderID: 0,
  1132. wantName: "folderId:73",
  1133. wantHasFolderPfx: false,
  1134. },
  1135. "prefix with empty name": {
  1136. key: "folderId:73/",
  1137. wantFolderID: 0,
  1138. wantName: "folderId:73/",
  1139. wantHasFolderPfx: false,
  1140. },
  1141. "prefix with non-numeric ID": {
  1142. key: "folderId:abc/my-secret",
  1143. wantFolderID: 0,
  1144. wantName: "folderId:abc/my-secret",
  1145. wantHasFolderPfx: false,
  1146. },
  1147. "prefix with zero ID": {
  1148. key: "folderId:0/my-secret",
  1149. wantFolderID: 0,
  1150. wantName: "folderId:0/my-secret",
  1151. wantHasFolderPfx: false,
  1152. },
  1153. "prefix with negative ID": {
  1154. key: "folderId:-1/my-secret",
  1155. wantFolderID: 0,
  1156. wantName: "folderId:-1/my-secret",
  1157. wantHasFolderPfx: false,
  1158. },
  1159. "empty key": {
  1160. key: "",
  1161. wantFolderID: 0,
  1162. wantName: "",
  1163. wantHasFolderPfx: false,
  1164. },
  1165. }
  1166. for name, tc := range testCases {
  1167. t.Run(name, func(t *testing.T) {
  1168. folderID, secretName, hasFolderPrefix := parseFolderPrefix(tc.key)
  1169. assert.Equal(t, tc.wantFolderID, folderID)
  1170. assert.Equal(t, tc.wantName, secretName)
  1171. assert.Equal(t, tc.wantHasFolderPfx, hasFolderPrefix)
  1172. })
  1173. }
  1174. }
  1175. // TestPushSecretWithFolderPrefix tests PushSecret with the "folderId:<id>/<name>" key format.
  1176. func TestPushSecretWithFolderPrefix(t *testing.T) {
  1177. ctx := context.Background()
  1178. c := newTestClient(t)
  1179. secret := &corev1.Secret{
  1180. Data: map[string][]byte{
  1181. "my-key": []byte("folder-prefix-value"),
  1182. },
  1183. }
  1184. metadataJSON := apiextensionsv1.JSON{
  1185. Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 5, "secretTemplateId": 1}}`),
  1186. }
  1187. // Update an existing secret using folderId prefix — should target folder 5 (ID 9001)
  1188. dataUpdate := fakePushSecretData{
  1189. remoteKey: "folderId:5/FolderSecretname",
  1190. property: "password",
  1191. secretKey: "my-key",
  1192. metadata: &metadataJSON,
  1193. }
  1194. err := c.PushSecret(ctx, secret, dataUpdate)
  1195. assert.NoError(t, err)
  1196. // Verify only the secret in folder 5 was updated
  1197. s9001, _ := c.(*client).api.Secret(9001)
  1198. s9000, _ := c.(*client).api.Secret(9000)
  1199. var s9001PW, s9000PW string
  1200. for _, f := range s9001.Fields {
  1201. if f.Slug == passwordSlug {
  1202. s9001PW = f.ItemValue
  1203. }
  1204. }
  1205. for _, f := range s9000.Fields {
  1206. if f.Slug == passwordSlug {
  1207. s9000PW = f.ItemValue
  1208. }
  1209. }
  1210. assert.Equal(t, "folder-prefix-value", s9001PW)
  1211. assert.Equal(t, "passwordvalue", s9000PW) // Unchanged
  1212. // Create a new secret using folderId prefix
  1213. metadataCreate := apiextensionsv1.JSON{
  1214. Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 42, "secretTemplateId": 1}}`),
  1215. }
  1216. dataCreate := fakePushSecretData{
  1217. remoteKey: "folderId:42/brand-new-secret",
  1218. property: "username",
  1219. secretKey: "my-key",
  1220. metadata: &metadataCreate,
  1221. }
  1222. err = c.PushSecret(ctx, secret, dataCreate)
  1223. assert.NoError(t, err)
  1224. // Verify the created secret has the plain name (prefix stripped)
  1225. foundSecrets, _ := c.(*client).api.Secrets("brand-new-secret", "Name")
  1226. assert.Len(t, foundSecrets, 1)
  1227. assert.Equal(t, "brand-new-secret", foundSecrets[0].Name)
  1228. assert.Equal(t, 42, foundSecrets[0].FolderID)
  1229. // Test precedence: remoteKey folderId overrides metadata folderId for lookups.
  1230. // Metadata says folderId:4, but remoteKey says folderId:5 — should target folder 5.
  1231. metadataFolder4 := apiextensionsv1.JSON{
  1232. Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 4, "secretTemplateId": 1}}`),
  1233. }
  1234. dataPrecedence := fakePushSecretData{
  1235. remoteKey: "folderId:5/FolderSecretname",
  1236. property: "username",
  1237. secretKey: "my-key",
  1238. metadata: &metadataFolder4,
  1239. }
  1240. err = c.PushSecret(ctx, secret, dataPrecedence)
  1241. assert.NoError(t, err)
  1242. // Verify the secret in folder 5 was updated (not folder 4)
  1243. s9001, _ = c.(*client).api.Secret(9001)
  1244. var s9001User string
  1245. for _, f := range s9001.Fields {
  1246. if f.Slug == usernameSlug {
  1247. s9001User = f.ItemValue
  1248. }
  1249. }
  1250. assert.Equal(t, "folder-prefix-value", s9001User)
  1251. }
  1252. // TestDeleteSecretWithFolderPrefix tests that DeleteSecret correctly uses the
  1253. // folderId prefix in the remote key to target the right secret.
  1254. func TestDeleteSecretWithFolderPrefix(t *testing.T) {
  1255. ctx := context.Background()
  1256. c := newTestClient(t)
  1257. // Both secrets 9000 (folder 4) and 9001 (folder 5) have name "FolderSecretname".
  1258. // Delete only the one in folder 5.
  1259. ref := fakePushSecretRemoteRef{
  1260. remoteKey: "folderId:5/FolderSecretname",
  1261. }
  1262. // Should exist initially
  1263. exists, err := c.SecretExists(ctx, ref)
  1264. assert.NoError(t, err)
  1265. assert.True(t, exists)
  1266. // Delete it
  1267. err = c.DeleteSecret(ctx, ref)
  1268. assert.NoError(t, err)
  1269. // Should not exist now
  1270. exists, err = c.SecretExists(ctx, ref)
  1271. assert.NoError(t, err)
  1272. assert.False(t, exists)
  1273. // The secret in folder 4 should still exist
  1274. refFolder4 := fakePushSecretRemoteRef{
  1275. remoteKey: "folderId:4/FolderSecretname",
  1276. }
  1277. exists, err = c.SecretExists(ctx, refFolder4)
  1278. assert.NoError(t, err)
  1279. assert.True(t, exists)
  1280. }
  1281. // TestSecretExistsWithFolderPrefix tests that SecretExists correctly uses the
  1282. // folderId prefix in the remote key.
  1283. func TestSecretExistsWithFolderPrefix(t *testing.T) {
  1284. ctx := context.Background()
  1285. c := newTestClient(t)
  1286. testCases := map[string]struct {
  1287. ref esv1.PushSecretRemoteRef
  1288. want bool
  1289. }{
  1290. "existing secret in folder 4": {
  1291. ref: fakePushSecretRemoteRef{remoteKey: "folderId:4/FolderSecretname"},
  1292. want: true,
  1293. },
  1294. "existing secret in folder 5": {
  1295. ref: fakePushSecretRemoteRef{remoteKey: "folderId:5/FolderSecretname"},
  1296. want: true,
  1297. },
  1298. "non-existing secret in wrong folder": {
  1299. ref: fakePushSecretRemoteRef{remoteKey: "folderId:99/FolderSecretname"},
  1300. want: false,
  1301. },
  1302. "non-existing secret name": {
  1303. ref: fakePushSecretRemoteRef{remoteKey: "folderId:4/does-not-exist"},
  1304. want: false,
  1305. },
  1306. }
  1307. for name, tc := range testCases {
  1308. t.Run(name, func(t *testing.T) {
  1309. got, err := c.SecretExists(ctx, tc.ref)
  1310. assert.NoError(t, err)
  1311. assert.Equal(t, tc.want, got)
  1312. })
  1313. }
  1314. }
  1315. // TestDeleteSecretAmbiguousName tests that DeleteSecret returns an error when a
  1316. // plain name matches multiple secrets across different folders.
  1317. func TestDeleteSecretAmbiguousName(t *testing.T) {
  1318. ctx := context.Background()
  1319. c := newTestClient(t)
  1320. // "FolderSecretname" exists in both folder 4 (ID 9000) and folder 5 (ID 9001).
  1321. // Using just the plain name should fail with an ambiguous error.
  1322. ref := fakePushSecretRemoteRef{
  1323. remoteKey: "FolderSecretname",
  1324. }
  1325. err := c.DeleteSecret(ctx, ref)
  1326. assert.Error(t, err)
  1327. assert.Contains(t, err.Error(), "multiple secrets found with the same name")
  1328. assert.Contains(t, err.Error(), "folderId:")
  1329. // Both secrets should still exist (nothing was deleted).
  1330. s9000, err := c.(*client).api.Secret(9000)
  1331. assert.NoError(t, err)
  1332. assert.NotNil(t, s9000)
  1333. s9001, err := c.(*client).api.Secret(9001)
  1334. assert.NoError(t, err)
  1335. assert.NotNil(t, s9001)
  1336. }
  1337. // TestSecretExistsAmbiguousName tests that SecretExists returns an error when a
  1338. // plain name matches multiple secrets across different folders.
  1339. func TestSecretExistsAmbiguousName(t *testing.T) {
  1340. ctx := context.Background()
  1341. c := newTestClient(t)
  1342. // "FolderSecretname" exists in both folder 4 (ID 9000) and folder 5 (ID 9001).
  1343. // Using just the plain name should fail with an ambiguous error.
  1344. ref := fakePushSecretRemoteRef{
  1345. remoteKey: "FolderSecretname",
  1346. }
  1347. exists, err := c.SecretExists(ctx, ref)
  1348. assert.Error(t, err)
  1349. assert.False(t, exists)
  1350. assert.Contains(t, err.Error(), "multiple secrets found with the same name")
  1351. }
  1352. // TestDeleteSecretUniqueName tests that DeleteSecret still works with a plain
  1353. // name when only one secret has that name (no ambiguity).
  1354. func TestDeleteSecretUniqueName(t *testing.T) {
  1355. ctx := context.Background()
  1356. c := newTestClient(t)
  1357. // "Secretname" is unique (only ID 4000 has this name).
  1358. ref := fakePushSecretRemoteRef{
  1359. remoteKey: "Secretname",
  1360. }
  1361. exists, err := c.SecretExists(ctx, ref)
  1362. assert.NoError(t, err)
  1363. assert.True(t, exists)
  1364. err = c.DeleteSecret(ctx, ref)
  1365. assert.NoError(t, err)
  1366. exists, err = c.SecretExists(ctx, ref)
  1367. assert.NoError(t, err)
  1368. assert.False(t, exists)
  1369. }
  1370. // TestGetSecretByNameStrict tests the getSecretByNameStrict helper directly.
  1371. func TestGetSecretByNameStrict(t *testing.T) {
  1372. c := newTestClient(t).(*client)
  1373. testCases := map[string]struct {
  1374. name string
  1375. wantErr bool
  1376. errMsg string
  1377. }{
  1378. "unique name returns secret": {
  1379. name: "Secretname",
  1380. wantErr: false,
  1381. },
  1382. "duplicate name returns ambiguous error": {
  1383. name: "FolderSecretname",
  1384. wantErr: true,
  1385. errMsg: "multiple secrets found with the same name",
  1386. },
  1387. "non-existent name returns unable to retrieve secret error": {
  1388. name: "does-not-exist",
  1389. wantErr: true,
  1390. errMsg: errMsgNoMatchingSecrets,
  1391. },
  1392. }
  1393. for name, tc := range testCases {
  1394. t.Run(name, func(t *testing.T) {
  1395. secret, err := c.getSecretByNameStrict(tc.name)
  1396. if tc.wantErr {
  1397. assert.Error(t, err)
  1398. assert.Nil(t, secret)
  1399. assert.Contains(t, err.Error(), tc.errMsg)
  1400. } else {
  1401. assert.NoError(t, err)
  1402. assert.NotNil(t, secret)
  1403. }
  1404. })
  1405. }
  1406. }
  1407. // TestPushSecretEmptyProperty tests PushSecret with an empty property, which
  1408. // should target the first field of the secret/template.
  1409. func TestPushSecretEmptyProperty(t *testing.T) {
  1410. ctx := context.Background()
  1411. c := newTestClient(t)
  1412. secret := &corev1.Secret{
  1413. Data: map[string][]byte{
  1414. "my-key": []byte("whole-value"),
  1415. },
  1416. }
  1417. metadataJSON := apiextensionsv1.JSON{
  1418. Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 1}}`),
  1419. }
  1420. // Create new secret with empty property → uses first template field
  1421. data := fakePushSecretData{
  1422. remoteKey: "empty-prop-secret",
  1423. property: "",
  1424. secretKey: "my-key",
  1425. metadata: &metadataJSON,
  1426. }
  1427. err := c.PushSecret(ctx, secret, data)
  1428. assert.NoError(t, err)
  1429. // Verify: the first field should have the value
  1430. foundSecrets, _ := c.(*client).api.Secrets("empty-prop-secret", "Name")
  1431. require.Len(t, foundSecrets, 1)
  1432. require.Len(t, foundSecrets[0].Fields, 1)
  1433. assert.Equal(t, "whole-value", foundSecrets[0].Fields[0].ItemValue)
  1434. // Update existing secret with empty property → updates first field
  1435. data2 := fakePushSecretData{
  1436. remoteKey: "4000",
  1437. property: "",
  1438. secretKey: "my-key",
  1439. }
  1440. err = c.PushSecret(ctx, secret, data2)
  1441. assert.NoError(t, err)
  1442. s4000, _ := c.(*client).api.Secret(4000)
  1443. assert.Equal(t, "whole-value", s4000.Fields[0].ItemValue)
  1444. }
  1445. // TestPushSecretConflictingFolderIDs tests that when the remoteKey has a folderId
  1446. // prefix, it overrides the metadata folderId for both lookup AND creation.
  1447. func TestPushSecretConflictingFolderIDs(t *testing.T) {
  1448. ctx := context.Background()
  1449. c := newTestClient(t)
  1450. secret := &corev1.Secret{
  1451. Data: map[string][]byte{
  1452. "my-key": []byte("prefix-wins"),
  1453. },
  1454. }
  1455. // Metadata says folderId:99, but prefix says folderId:42.
  1456. // The prefix should win for creation.
  1457. metadataJSON := apiextensionsv1.JSON{
  1458. Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 99, "secretTemplateId": 1}}`),
  1459. }
  1460. data := fakePushSecretData{
  1461. remoteKey: "folderId:42/conflict-test",
  1462. property: "username",
  1463. secretKey: "my-key",
  1464. metadata: &metadataJSON,
  1465. }
  1466. err := c.PushSecret(ctx, secret, data)
  1467. assert.NoError(t, err)
  1468. // Verify: the secret was created in folder 42, not 99.
  1469. foundSecrets, _ := c.(*client).api.Secrets("conflict-test", "Name")
  1470. require.Len(t, foundSecrets, 1)
  1471. assert.Equal(t, 42, foundSecrets[0].FolderID)
  1472. }
  1473. // TestPushSecretAmbiguousPlainName tests that PushSecret returns an error when
  1474. // a plain name (no prefix, no path, no numeric ID) matches multiple secrets.
  1475. func TestPushSecretAmbiguousPlainName(t *testing.T) {
  1476. ctx := context.Background()
  1477. c := newTestClient(t)
  1478. secret := &corev1.Secret{
  1479. Data: map[string][]byte{
  1480. "my-key": []byte("value"),
  1481. },
  1482. }
  1483. // "FolderSecretname" exists in both folder 4 (ID 9000) and folder 5 (ID 9001).
  1484. // Without a folderId prefix or metadata folderId, this should fail.
  1485. data := fakePushSecretData{
  1486. remoteKey: "FolderSecretname",
  1487. property: "password",
  1488. secretKey: "my-key",
  1489. }
  1490. err := c.PushSecret(ctx, secret, data)
  1491. assert.Error(t, err)
  1492. assert.Contains(t, err.Error(), "multiple secrets found with the same name")
  1493. }
  1494. // TestPushSecretEmptyRemoteKey tests that PushSecret rejects empty remote keys.
  1495. func TestPushSecretEmptyRemoteKey(t *testing.T) {
  1496. ctx := context.Background()
  1497. c := newTestClient(t)
  1498. secret := &corev1.Secret{
  1499. Data: map[string][]byte{
  1500. "my-key": []byte("value"),
  1501. },
  1502. }
  1503. data := fakePushSecretData{
  1504. remoteKey: "",
  1505. property: "username",
  1506. secretKey: "my-key",
  1507. }
  1508. err := c.PushSecret(ctx, secret, data)
  1509. assert.Error(t, err)
  1510. assert.Contains(t, err.Error(), "remote key must be defined")
  1511. }
  1512. // TestCreateSecretFolderPrefixWithSlashes tests that createSecret rejects
  1513. // folderId prefixed names that contain slashes after prefix stripping.
  1514. func TestCreateSecretFolderPrefixWithSlashes(t *testing.T) {
  1515. ctx := context.Background()
  1516. c := newTestClient(t)
  1517. secret := &corev1.Secret{
  1518. Data: map[string][]byte{
  1519. "my-key": []byte("value"),
  1520. },
  1521. }
  1522. metadataJSON := apiextensionsv1.JSON{
  1523. Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 73, "secretTemplateId": 1}}`),
  1524. }
  1525. data := fakePushSecretData{
  1526. remoteKey: "folderId:73/sub/path/secret",
  1527. property: "username",
  1528. secretKey: "my-key",
  1529. metadata: &metadataJSON,
  1530. }
  1531. err := c.PushSecret(ctx, secret, data)
  1532. assert.Error(t, err)
  1533. assert.Contains(t, err.Error(), "must not contain path separators")
  1534. }
  1535. // TestCreateSecretEmptyTemplateFields tests createSecret when the template has
  1536. // no fields and no property is specified.
  1537. func TestCreateSecretEmptyTemplateFields(t *testing.T) {
  1538. // Create a fakeAPI that returns a template with no fields
  1539. fake := &fakeAPI{secrets: []*server.Secret{}}
  1540. // Override SecretTemplate to return empty fields (template ID 888)
  1541. c := &client{api: &emptyTemplateAPI{fakeAPI: fake}}
  1542. err := c.createSecret("test-secret", "", "value", PushSecretMetadataSpec{
  1543. FolderID: 1,
  1544. SecretTemplateID: 888,
  1545. })
  1546. assert.Error(t, err)
  1547. assert.Contains(t, err.Error(), "secret template has no fields")
  1548. }
  1549. // emptyTemplateAPI wraps fakeAPI but returns an empty template for ID 888.
  1550. type emptyTemplateAPI struct {
  1551. *fakeAPI
  1552. }
  1553. func (e *emptyTemplateAPI) SecretTemplate(id int) (*server.SecretTemplate, error) {
  1554. if id == 888 {
  1555. return &server.SecretTemplate{
  1556. ID: 888,
  1557. Name: "Empty Template",
  1558. Fields: []server.SecretTemplateField{},
  1559. }, nil
  1560. }
  1561. return e.fakeAPI.SecretTemplate(id)
  1562. }
  1563. // TestGetSecretGjsonPriorityOverField tests that gjson extraction from
  1564. // Fields[0].ItemValue takes priority over field Slug/FieldName matching.
  1565. // This preserves backward compatibility: existing users relying on gjson
  1566. // extraction from the first field's JSON blob are not broken.
  1567. func TestGetSecretGjsonPriorityOverField(t *testing.T) {
  1568. ctx := context.Background()
  1569. // Create a secret where:
  1570. // - Fields[0].ItemValue is JSON containing key "password"
  1571. // - Fields[1] has Slug "password" with a DIFFERENT value
  1572. // gjson should win because it is checked first (backward compat).
  1573. s := &server.Secret{
  1574. ID: 100,
  1575. Name: "priority-test",
  1576. Fields: []server.SecretField{
  1577. {
  1578. FieldName: "Data",
  1579. Slug: "data",
  1580. ItemValue: `{"password": "from-json-blob"}`,
  1581. },
  1582. {
  1583. FieldName: "Password",
  1584. Slug: "password",
  1585. ItemValue: "from-field-slug",
  1586. },
  1587. },
  1588. }
  1589. c := &client{api: &fakeAPI{secrets: []*server.Secret{s}}}
  1590. got, err := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{
  1591. Key: "100",
  1592. Property: "password",
  1593. })
  1594. assert.NoError(t, err)
  1595. // gjson extraction should return "from-json-blob" (backward compat takes precedence)
  1596. assert.Equal(t, []byte("from-json-blob"), got)
  1597. }
  1598. // TestGetSecretGjsonFallback tests that gjson extraction from Fields[0].ItemValue
  1599. // works as a fallback when no field slug/name matches.
  1600. func TestGetSecretGjsonFallback(t *testing.T) {
  1601. ctx := context.Background()
  1602. s := &server.Secret{
  1603. ID: 101,
  1604. Name: "gjson-fallback-test",
  1605. Fields: []server.SecretField{
  1606. {
  1607. FieldName: "Data",
  1608. Slug: "data",
  1609. ItemValue: `{"nested": {"key": "deep-value"}}`,
  1610. },
  1611. },
  1612. }
  1613. c := &client{api: &fakeAPI{secrets: []*server.Secret{s}}}
  1614. // "nested.key" doesn't match any field slug/name, so gjson fallback kicks in
  1615. got, err := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{
  1616. Key: "101",
  1617. Property: "nested.key",
  1618. })
  1619. assert.NoError(t, err)
  1620. assert.Equal(t, []byte("deep-value"), got)
  1621. }
  1622. // TestLookupSecretNon404Error tests that lookupSecret and lookupSecretStrict
  1623. // correctly propagate non-404 API errors instead of falling through.
  1624. func TestLookupSecretNon404Error(t *testing.T) {
  1625. // Create an API that returns a non-404 error for Secret()
  1626. fake := &errorAPI{
  1627. fakeAPI: &fakeAPI{secrets: []*server.Secret{}},
  1628. secretErr: errors.New("500 Internal Server Error: database connection failed"),
  1629. }
  1630. c := &client{api: fake}
  1631. // lookupSecret with numeric key: Secret() returns non-404 error, should propagate
  1632. _, err := c.lookupSecret("42", 0)
  1633. assert.Error(t, err)
  1634. assert.Contains(t, err.Error(), "database connection failed")
  1635. // lookupSecretStrict with numeric key: same behavior
  1636. _, err = c.lookupSecretStrict("42")
  1637. assert.Error(t, err)
  1638. assert.Contains(t, err.Error(), "database connection failed")
  1639. }
  1640. // errorAPI wraps fakeAPI but returns a configurable error for Secret().
  1641. type errorAPI struct {
  1642. *fakeAPI
  1643. secretErr error
  1644. }
  1645. func (e *errorAPI) Secret(_ int) (*server.Secret, error) {
  1646. if e.secretErr != nil {
  1647. return nil, e.secretErr
  1648. }
  1649. return e.fakeAPI.Secret(0)
  1650. }
  1651. // TestFakeAPISecretsReturnsEmptySlice verifies the fakeAPI mock matches real SDK
  1652. // behavior: Secrets() returns ([]Secret{}, nil) for zero matches, not an error.
  1653. func TestFakeAPISecretsReturnsEmptySlice(t *testing.T) {
  1654. fake := &fakeAPI{secrets: []*server.Secret{}}
  1655. secrets, err := fake.Secrets("nonexistent", "Name")
  1656. assert.NoError(t, err)
  1657. assert.NotNil(t, secrets)
  1658. assert.Empty(t, secrets)
  1659. }
  1660. // TestPushSecretMetadataNoFolderID tests that PushSecret requires a folderId
  1661. // for creation when the prefix doesn't provide one.
  1662. func TestPushSecretMetadataNoFolderID(t *testing.T) {
  1663. ctx := context.Background()
  1664. c := newTestClient(t)
  1665. secret := &corev1.Secret{
  1666. Data: map[string][]byte{
  1667. "my-key": []byte("value"),
  1668. },
  1669. }
  1670. // Metadata has secretTemplateId but no folderId
  1671. metadataJSON := apiextensionsv1.JSON{
  1672. Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 0, "secretTemplateId": 1}}`),
  1673. }
  1674. data := fakePushSecretData{
  1675. remoteKey: "no-folder-secret",
  1676. property: "username",
  1677. secretKey: "my-key",
  1678. metadata: &metadataJSON,
  1679. }
  1680. err := c.PushSecret(ctx, secret, data)
  1681. assert.Error(t, err)
  1682. assert.Contains(t, err.Error(), "folderId and secretTemplateId must be provided")
  1683. }
  1684. // TestPushSecretCreateWithFolderPrefixNoMetadataFolder tests that PushSecret can
  1685. // create a secret when the folderId comes from the prefix even if metadata has
  1686. // folderId: 0, as long as secretTemplateId is provided.
  1687. func TestPushSecretCreateWithFolderPrefixNoMetadataFolder(t *testing.T) {
  1688. ctx := context.Background()
  1689. c := newTestClient(t)
  1690. secret := &corev1.Secret{
  1691. Data: map[string][]byte{
  1692. "my-key": []byte("prefix-folder-value"),
  1693. },
  1694. }
  1695. // Metadata has secretTemplateId but folderId is 0; prefix provides the folder.
  1696. metadataJSON := apiextensionsv1.JSON{
  1697. Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 0, "secretTemplateId": 1}}`),
  1698. }
  1699. data := fakePushSecretData{
  1700. remoteKey: "folderId:55/prefix-only-folder",
  1701. property: "username",
  1702. secretKey: "my-key",
  1703. metadata: &metadataJSON,
  1704. }
  1705. err := c.PushSecret(ctx, secret, data)
  1706. assert.NoError(t, err)
  1707. // Verify: created in folder 55
  1708. foundSecrets, _ := c.(*client).api.Secrets("prefix-only-folder", "Name")
  1709. require.Len(t, foundSecrets, 1)
  1710. assert.Equal(t, 55, foundSecrets[0].FolderID)
  1711. }
  1712. // TestGetSecretByFolderPrefix tests GetSecret with the folderId prefix format.
  1713. func TestGetSecretByFolderPrefix(t *testing.T) {
  1714. ctx := context.Background()
  1715. c := newTestClient(t)
  1716. // Secret 9000 is in folder 4, secret 9001 is in folder 5, both named "FolderSecretname"
  1717. got, err := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{
  1718. Key: "folderId:4/FolderSecretname",
  1719. Property: "username",
  1720. })
  1721. assert.NoError(t, err)
  1722. assert.Equal(t, []byte("usernamevalue"), got)
  1723. // Non-existent folder
  1724. _, err = c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{
  1725. Key: "folderId:99/FolderSecretname",
  1726. Property: "username",
  1727. })
  1728. assert.Error(t, err)
  1729. assert.Contains(t, err.Error(), "not found")
  1730. }
  1731. // TestPushSecretUpdateSecretNoFields tests updating a secret that has no fields.
  1732. func TestPushSecretUpdateSecretNoFields(t *testing.T) {
  1733. ctx := context.Background()
  1734. // Create a secret with no fields
  1735. s := &server.Secret{
  1736. ID: 200,
  1737. Name: "no-fields-secret",
  1738. Fields: []server.SecretField{},
  1739. }
  1740. c := &client{api: &fakeAPI{secrets: []*server.Secret{s}}}
  1741. secret := &corev1.Secret{
  1742. Data: map[string][]byte{
  1743. "my-key": []byte("value"),
  1744. },
  1745. }
  1746. // Update with empty property → tries to write to first field, but there are none
  1747. data := fakePushSecretData{
  1748. remoteKey: "200",
  1749. property: "",
  1750. secretKey: "my-key",
  1751. }
  1752. err := c.PushSecret(ctx, secret, data)
  1753. assert.Error(t, err)
  1754. assert.Contains(t, err.Error(), "secret has no fields to update")
  1755. }
  1756. // TestPushSecretNonExistentTemplateField tests creating a secret with a property that
  1757. // doesn't match any template field.
  1758. func TestPushSecretNonExistentTemplateField(t *testing.T) {
  1759. ctx := context.Background()
  1760. c := newTestClient(t)
  1761. secret := &corev1.Secret{
  1762. Data: map[string][]byte{
  1763. "my-key": []byte("value"),
  1764. },
  1765. }
  1766. metadataJSON := apiextensionsv1.JSON{
  1767. Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 1}}`),
  1768. }
  1769. data := fakePushSecretData{
  1770. remoteKey: "nonexistent-field-secret",
  1771. property: "nonexistent-field",
  1772. secretKey: "my-key",
  1773. metadata: &metadataJSON,
  1774. }
  1775. err := c.PushSecret(ctx, secret, data)
  1776. assert.Error(t, err)
  1777. assert.Contains(t, err.Error(), "field nonexistent-field not found in secret template")
  1778. }