client_test.go 54 KB

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