client_test.go 59 KB

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