client_test.go 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558
  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 onepasswordsdk
  14. import (
  15. "context"
  16. "errors"
  17. "fmt"
  18. "testing"
  19. "time"
  20. "github.com/1password/onepassword-sdk-go"
  21. "github.com/hashicorp/golang-lru/v2/expirable"
  22. "github.com/stretchr/testify/assert"
  23. "github.com/stretchr/testify/require"
  24. corev1 "k8s.io/api/core/v1"
  25. apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
  26. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  27. v1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  28. "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
  29. )
  30. func TestProviderGetSecret(t *testing.T) {
  31. tests := []struct {
  32. name string
  33. ref v1.ExternalSecretDataRemoteRef
  34. want []byte
  35. assertError func(t *testing.T, err error)
  36. client func() *onepassword.Client
  37. }{
  38. {
  39. name: "get secret successfully",
  40. client: func() *onepassword.Client {
  41. fc := &fakeClient{
  42. resolveResult: "secret",
  43. }
  44. return &onepassword.Client{
  45. SecretsAPI: fc,
  46. VaultsAPI: fc,
  47. }
  48. },
  49. assertError: func(t *testing.T, err error) {
  50. require.NoError(t, err)
  51. },
  52. ref: v1.ExternalSecretDataRemoteRef{
  53. Key: "secret",
  54. },
  55. want: []byte("secret"),
  56. },
  57. {
  58. name: "get secret with error",
  59. client: func() *onepassword.Client {
  60. fc := &fakeClient{
  61. resolveError: errors.New("fobar"),
  62. }
  63. return &onepassword.Client{
  64. SecretsAPI: fc,
  65. VaultsAPI: fc,
  66. }
  67. },
  68. assertError: func(t *testing.T, err error) {
  69. require.ErrorContains(t, err, "fobar")
  70. },
  71. ref: v1.ExternalSecretDataRemoteRef{
  72. Key: "secret",
  73. },
  74. },
  75. {
  76. name: "get secret version not implemented",
  77. client: func() *onepassword.Client {
  78. fc := &fakeClient{
  79. resolveResult: "secret",
  80. }
  81. return &onepassword.Client{
  82. SecretsAPI: fc,
  83. VaultsAPI: fc,
  84. }
  85. },
  86. ref: v1.ExternalSecretDataRemoteRef{
  87. Key: "secret",
  88. Version: "1",
  89. },
  90. assertError: func(t *testing.T, err error) {
  91. require.ErrorContains(t, err, "is not implemented in the 1Password SDK provider")
  92. },
  93. },
  94. }
  95. for _, tt := range tests {
  96. t.Run(tt.name, func(t *testing.T) {
  97. p := &SecretsClient{
  98. client: tt.client(),
  99. vaultPrefix: "op://vault/",
  100. }
  101. got, err := p.GetSecret(t.Context(), tt.ref)
  102. tt.assertError(t, err)
  103. require.Equal(t, string(got), string(tt.want))
  104. })
  105. }
  106. }
  107. func TestProviderGetSecretMap(t *testing.T) {
  108. tests := []struct {
  109. name string
  110. ref v1.ExternalSecretDataRemoteRef
  111. want map[string][]byte
  112. assertError func(t *testing.T, err error)
  113. client func() *onepassword.Client
  114. }{
  115. {
  116. name: "get secret successfully for files",
  117. client: func() *onepassword.Client {
  118. fc := &fakeClient{}
  119. fl := &fakeLister{
  120. listAllResult: []onepassword.ItemOverview{
  121. {
  122. ID: "test-item-id",
  123. Title: "key",
  124. Category: "login",
  125. VaultID: "vault-id",
  126. },
  127. },
  128. getResult: onepassword.Item{
  129. ID: "test-item-id",
  130. Title: "key",
  131. Category: "login",
  132. VaultID: "vault-id",
  133. Files: []onepassword.ItemFile{
  134. {
  135. Attributes: onepassword.FileAttributes{
  136. Name: "name",
  137. ID: "id",
  138. },
  139. FieldID: "field-id",
  140. },
  141. },
  142. },
  143. fileLister: &fakeFileLister{
  144. readContent: []byte("content"),
  145. },
  146. }
  147. return &onepassword.Client{
  148. SecretsAPI: fc,
  149. ItemsAPI: fl,
  150. VaultsAPI: fc,
  151. }
  152. },
  153. assertError: func(t *testing.T, err error) {
  154. require.NoError(t, err)
  155. },
  156. ref: v1.ExternalSecretDataRemoteRef{
  157. Key: "key",
  158. Property: "file/name",
  159. },
  160. want: map[string][]byte{
  161. "name": []byte("content"),
  162. },
  163. },
  164. {
  165. name: "get secret successfully for fields",
  166. client: func() *onepassword.Client {
  167. fc := &fakeClient{}
  168. fl := &fakeLister{
  169. listAllResult: []onepassword.ItemOverview{
  170. {
  171. ID: "test-item-id",
  172. Title: "key",
  173. Category: "login",
  174. VaultID: "vault-id",
  175. },
  176. },
  177. getResult: onepassword.Item{
  178. ID: "test-item-id",
  179. Title: "key",
  180. Category: "login",
  181. VaultID: "vault-id",
  182. Fields: []onepassword.ItemField{
  183. {
  184. ID: "field-id",
  185. Title: "name",
  186. FieldType: onepassword.ItemFieldTypeConcealed,
  187. Value: "value",
  188. },
  189. },
  190. },
  191. fileLister: &fakeFileLister{
  192. readContent: []byte("content"),
  193. },
  194. }
  195. return &onepassword.Client{
  196. SecretsAPI: fc,
  197. ItemsAPI: fl,
  198. VaultsAPI: fc,
  199. }
  200. },
  201. assertError: func(t *testing.T, err error) {
  202. require.NoError(t, err)
  203. },
  204. ref: v1.ExternalSecretDataRemoteRef{
  205. Key: "key",
  206. Property: "field/name",
  207. },
  208. want: map[string][]byte{
  209. "name": []byte("value"),
  210. },
  211. },
  212. {
  213. name: "get secret fails with fields with same title",
  214. client: func() *onepassword.Client {
  215. fc := &fakeClient{}
  216. fl := &fakeLister{
  217. listAllResult: []onepassword.ItemOverview{
  218. {
  219. ID: "test-item-id",
  220. Title: "key",
  221. Category: "login",
  222. VaultID: "vault-id",
  223. },
  224. },
  225. getResult: onepassword.Item{
  226. ID: "test-item-id",
  227. Title: "key",
  228. Category: "login",
  229. VaultID: "vault-id",
  230. Fields: []onepassword.ItemField{
  231. {
  232. ID: "field-id",
  233. Title: "name",
  234. FieldType: onepassword.ItemFieldTypeConcealed,
  235. Value: "value",
  236. },
  237. {
  238. ID: "field-id",
  239. Title: "name",
  240. FieldType: onepassword.ItemFieldTypeConcealed,
  241. Value: "value",
  242. },
  243. },
  244. },
  245. fileLister: &fakeFileLister{
  246. readContent: []byte("content"),
  247. },
  248. }
  249. return &onepassword.Client{
  250. SecretsAPI: fc,
  251. ItemsAPI: fl,
  252. VaultsAPI: fc,
  253. }
  254. },
  255. assertError: func(t *testing.T, err error) {
  256. require.ErrorContains(t, err, "found more than 1 fields with title 'name' in 'key', got 2")
  257. },
  258. ref: v1.ExternalSecretDataRemoteRef{
  259. Key: "key",
  260. Property: "field/name",
  261. },
  262. },
  263. }
  264. for _, tt := range tests {
  265. t.Run(tt.name, func(t *testing.T) {
  266. p := &SecretsClient{
  267. client: tt.client(),
  268. vaultPrefix: "op://vault/",
  269. }
  270. got, err := p.GetSecretMap(t.Context(), tt.ref)
  271. tt.assertError(t, err)
  272. require.Equal(t, tt.want, got)
  273. })
  274. }
  275. }
  276. func TestProviderValidate(t *testing.T) {
  277. tests := []struct {
  278. name string
  279. want v1.ValidationResult
  280. assertError func(t *testing.T, err error)
  281. client func() *onepassword.Client
  282. vaultPrefix string
  283. }{
  284. {
  285. name: "validate successfully",
  286. client: func() *onepassword.Client {
  287. fc := &fakeClient{
  288. listAllResult: []onepassword.VaultOverview{
  289. {
  290. ID: "test",
  291. Title: "test",
  292. },
  293. },
  294. }
  295. return &onepassword.Client{
  296. SecretsAPI: fc,
  297. VaultsAPI: fc,
  298. }
  299. },
  300. want: v1.ValidationResultReady,
  301. assertError: func(t *testing.T, err error) {
  302. require.NoError(t, err)
  303. },
  304. vaultPrefix: "op://vault/",
  305. },
  306. }
  307. for _, tt := range tests {
  308. t.Run(tt.name, func(t *testing.T) {
  309. p := &SecretsClient{
  310. client: tt.client(),
  311. vaultPrefix: tt.vaultPrefix,
  312. }
  313. got, err := p.Validate()
  314. tt.assertError(t, err)
  315. require.Equal(t, got, tt.want)
  316. })
  317. }
  318. }
  319. func TestPushSecret(t *testing.T) {
  320. fc := &fakeClient{
  321. listAllResult: []onepassword.VaultOverview{
  322. {
  323. ID: "test",
  324. Title: "test",
  325. },
  326. },
  327. }
  328. tests := []struct {
  329. name string
  330. ref v1alpha1.PushSecretData
  331. secret *corev1.Secret
  332. assertError func(t *testing.T, err error)
  333. lister func() *fakeLister
  334. assertLister func(t *testing.T, lister *fakeLister)
  335. }{
  336. {
  337. name: "create is called",
  338. lister: func() *fakeLister {
  339. return &fakeLister{
  340. listAllResult: []onepassword.ItemOverview{},
  341. }
  342. },
  343. secret: &corev1.Secret{
  344. Data: map[string][]byte{
  345. "foo": []byte("bar"),
  346. },
  347. ObjectMeta: metav1.ObjectMeta{
  348. Name: "secret",
  349. Namespace: "default",
  350. },
  351. },
  352. ref: v1alpha1.PushSecretData{
  353. Match: v1alpha1.PushSecretMatch{
  354. SecretKey: "foo",
  355. RemoteRef: v1alpha1.PushSecretRemoteRef{
  356. RemoteKey: "key",
  357. },
  358. },
  359. },
  360. assertError: func(t *testing.T, err error) {
  361. require.NoError(t, err)
  362. },
  363. assertLister: func(t *testing.T, lister *fakeLister) {
  364. assert.True(t, lister.createCalled)
  365. },
  366. },
  367. {
  368. name: "update is called",
  369. lister: func() *fakeLister {
  370. return &fakeLister{
  371. listAllResult: []onepassword.ItemOverview{
  372. {
  373. ID: "test-item-id",
  374. Title: "key",
  375. Category: "login",
  376. VaultID: "vault-id",
  377. },
  378. },
  379. }
  380. },
  381. secret: &corev1.Secret{
  382. Data: map[string][]byte{
  383. "foo": []byte("bar"),
  384. },
  385. ObjectMeta: metav1.ObjectMeta{
  386. Name: "secret",
  387. Namespace: "default",
  388. },
  389. },
  390. ref: v1alpha1.PushSecretData{
  391. Match: v1alpha1.PushSecretMatch{
  392. SecretKey: "foo",
  393. RemoteRef: v1alpha1.PushSecretRemoteRef{
  394. RemoteKey: "key",
  395. },
  396. },
  397. },
  398. assertError: func(t *testing.T, err error) {
  399. require.NoError(t, err)
  400. },
  401. assertLister: func(t *testing.T, lister *fakeLister) {
  402. assert.True(t, lister.putCalled)
  403. },
  404. },
  405. }
  406. for _, tt := range tests {
  407. t.Run(tt.name, func(t *testing.T) {
  408. ctx := t.Context()
  409. lister := tt.lister()
  410. p := &SecretsClient{
  411. client: &onepassword.Client{
  412. SecretsAPI: fc,
  413. VaultsAPI: fc,
  414. ItemsAPI: lister,
  415. },
  416. }
  417. err := p.PushSecret(ctx, tt.secret, tt.ref)
  418. tt.assertError(t, err)
  419. tt.assertLister(t, lister)
  420. })
  421. }
  422. }
  423. func TestDeleteItemField(t *testing.T) {
  424. fc := &fakeClient{
  425. listAllResult: []onepassword.VaultOverview{
  426. {
  427. ID: "test",
  428. Title: "test",
  429. },
  430. },
  431. }
  432. testCases := []struct {
  433. name string
  434. lister func() *fakeLister
  435. ref *v1alpha1.PushSecretRemoteRef
  436. assertError func(t *testing.T, err error)
  437. assertLister func(t *testing.T, lister *fakeLister)
  438. }{
  439. {
  440. name: "update is called",
  441. ref: &v1alpha1.PushSecretRemoteRef{
  442. RemoteKey: "key",
  443. Property: "password",
  444. },
  445. assertLister: func(t *testing.T, lister *fakeLister) {
  446. require.True(t, lister.putCalled)
  447. },
  448. lister: func() *fakeLister {
  449. fl := &fakeLister{
  450. listAllResult: []onepassword.ItemOverview{
  451. {
  452. ID: "test-item-id",
  453. Title: "key",
  454. Category: "login",
  455. VaultID: "vault-id",
  456. },
  457. },
  458. getResult: onepassword.Item{
  459. ID: "test-item-id",
  460. Title: "key",
  461. Category: "login",
  462. VaultID: "vault-id",
  463. Fields: []onepassword.ItemField{
  464. {
  465. ID: "field-1",
  466. Title: "password",
  467. FieldType: onepassword.ItemFieldTypeConcealed,
  468. Value: "password",
  469. },
  470. {
  471. ID: "field-2",
  472. Title: "other-field",
  473. FieldType: onepassword.ItemFieldTypeConcealed,
  474. Value: "username",
  475. },
  476. },
  477. },
  478. }
  479. return fl
  480. },
  481. assertError: func(t *testing.T, err error) {
  482. require.NoError(t, err)
  483. },
  484. },
  485. {
  486. name: "delete is called",
  487. ref: &v1alpha1.PushSecretRemoteRef{
  488. RemoteKey: "key",
  489. Property: "password",
  490. },
  491. assertLister: func(t *testing.T, lister *fakeLister) {
  492. require.True(t, lister.deleteCalled, "delete should have been called as the item should have existed")
  493. },
  494. lister: func() *fakeLister {
  495. fl := &fakeLister{
  496. listAllResult: []onepassword.ItemOverview{
  497. {
  498. ID: "test-item-id",
  499. Title: "key",
  500. Category: "login",
  501. VaultID: "vault-id",
  502. },
  503. },
  504. getResult: onepassword.Item{
  505. ID: "test-item-id",
  506. Title: "key",
  507. Category: "login",
  508. VaultID: "vault-id",
  509. Fields: []onepassword.ItemField{
  510. {
  511. ID: "field-1",
  512. Title: "password",
  513. FieldType: onepassword.ItemFieldTypeConcealed,
  514. Value: "password",
  515. },
  516. },
  517. },
  518. }
  519. return fl
  520. },
  521. assertError: func(t *testing.T, err error) {
  522. require.NoError(t, err)
  523. },
  524. },
  525. }
  526. for _, testCase := range testCases {
  527. t.Run(testCase.name, func(t *testing.T) {
  528. ctx := t.Context()
  529. lister := testCase.lister()
  530. p := &SecretsClient{
  531. client: &onepassword.Client{
  532. SecretsAPI: fc,
  533. VaultsAPI: fc,
  534. ItemsAPI: lister,
  535. },
  536. }
  537. testCase.assertError(t, p.DeleteSecret(ctx, testCase.ref))
  538. testCase.assertLister(t, lister)
  539. })
  540. }
  541. }
  542. func TestGetVault(t *testing.T) {
  543. fc := &fakeClient{
  544. listAllResult: []onepassword.VaultOverview{
  545. {
  546. ID: "vault-id",
  547. Title: "vault-title",
  548. },
  549. },
  550. }
  551. p := &SecretsClient{
  552. client: &onepassword.Client{
  553. VaultsAPI: fc,
  554. },
  555. }
  556. titleOrUuids := []string{"vault-title", "vault-id"}
  557. for _, titleOrUuid := range titleOrUuids {
  558. t.Run(titleOrUuid, func(t *testing.T) {
  559. vaultID, err := p.GetVault(t.Context(), titleOrUuid)
  560. require.NoError(t, err)
  561. require.Equal(t, fc.listAllResult[0].ID, vaultID)
  562. })
  563. }
  564. }
  565. type fakeLister struct {
  566. listAllResult []onepassword.ItemOverview
  567. createCalled bool
  568. createdFieldType onepassword.ItemFieldType
  569. createdParams onepassword.ItemCreateParams
  570. putCalled bool
  571. putItem onepassword.Item
  572. deleteCalled bool
  573. getResult onepassword.Item
  574. fileLister onepassword.ItemsFilesAPI
  575. }
  576. func (f *fakeLister) Create(ctx context.Context, params onepassword.ItemCreateParams) (onepassword.Item, error) {
  577. f.createCalled = true
  578. f.createdParams = params
  579. if len(params.Fields) > 0 {
  580. f.createdFieldType = params.Fields[0].FieldType
  581. }
  582. return onepassword.Item{}, nil
  583. }
  584. func (f *fakeLister) Get(ctx context.Context, vaultID, itemID string) (onepassword.Item, error) {
  585. return f.getResult, nil
  586. }
  587. func (f *fakeLister) Put(ctx context.Context, item onepassword.Item) (onepassword.Item, error) {
  588. f.putCalled = true
  589. f.putItem = item
  590. return onepassword.Item{}, nil
  591. }
  592. func (f *fakeLister) Delete(ctx context.Context, vaultID, itemID string) error {
  593. f.deleteCalled = true
  594. return nil
  595. }
  596. func (f *fakeLister) Archive(ctx context.Context, vaultID, itemID string) error {
  597. return nil
  598. }
  599. func (f *fakeLister) List(ctx context.Context, vaultID string, opts ...onepassword.ItemListFilter) ([]onepassword.ItemOverview, error) {
  600. return f.listAllResult, nil
  601. }
  602. func (f *fakeLister) Shares() onepassword.ItemsSharesAPI {
  603. return nil
  604. }
  605. func (f *fakeLister) Files() onepassword.ItemsFilesAPI {
  606. return f.fileLister
  607. }
  608. type fakeFileLister struct {
  609. readContent []byte
  610. }
  611. func (f *fakeFileLister) Attach(ctx context.Context, item onepassword.Item, fileParams onepassword.FileCreateParams) (onepassword.Item, error) {
  612. return onepassword.Item{}, nil
  613. }
  614. func (f *fakeFileLister) Read(ctx context.Context, vaultID, itemID string, attr onepassword.FileAttributes) ([]byte, error) {
  615. return f.readContent, nil
  616. }
  617. func (f *fakeFileLister) Delete(ctx context.Context, item onepassword.Item, sectionID, fieldID string) (onepassword.Item, error) {
  618. return onepassword.Item{}, nil
  619. }
  620. func (f *fakeFileLister) ReplaceDocument(ctx context.Context, item onepassword.Item, docParams onepassword.DocumentCreateParams) (onepassword.Item, error) {
  621. return onepassword.Item{}, nil
  622. }
  623. var _ onepassword.ItemsFilesAPI = (*fakeFileLister)(nil)
  624. type statefulFakeLister struct {
  625. listAllResult []onepassword.ItemOverview
  626. items map[string]onepassword.Item
  627. deletedItems map[string]bool
  628. createCalled bool
  629. putCalled bool
  630. deleteCalled bool
  631. fileLister onepassword.ItemsFilesAPI
  632. }
  633. func (f *statefulFakeLister) Create(ctx context.Context, params onepassword.ItemCreateParams) (onepassword.Item, error) {
  634. f.createCalled = true
  635. return onepassword.Item{}, nil
  636. }
  637. func (f *statefulFakeLister) Get(ctx context.Context, vaultID, itemID string) (onepassword.Item, error) {
  638. if f.deletedItems != nil && f.deletedItems[itemID] {
  639. return onepassword.Item{}, fmt.Errorf("item not found")
  640. }
  641. if item, ok := f.items[itemID]; ok {
  642. return item, nil
  643. }
  644. return onepassword.Item{}, fmt.Errorf("item not found")
  645. }
  646. func (f *statefulFakeLister) Put(ctx context.Context, item onepassword.Item) (onepassword.Item, error) {
  647. f.putCalled = true
  648. if f.items == nil {
  649. f.items = make(map[string]onepassword.Item)
  650. }
  651. f.items[item.ID] = item
  652. return item, nil
  653. }
  654. func (f *statefulFakeLister) Delete(ctx context.Context, vaultID, itemID string) error {
  655. f.deleteCalled = true
  656. if f.deletedItems == nil {
  657. f.deletedItems = make(map[string]bool)
  658. }
  659. f.deletedItems[itemID] = true
  660. delete(f.items, itemID)
  661. f.listAllResult = nil
  662. return nil
  663. }
  664. func (f *statefulFakeLister) Archive(ctx context.Context, vaultID, itemID string) error {
  665. return nil
  666. }
  667. func (f *statefulFakeLister) List(ctx context.Context, vaultID string, opts ...onepassword.ItemListFilter) ([]onepassword.ItemOverview, error) {
  668. return f.listAllResult, nil
  669. }
  670. func (f *statefulFakeLister) Shares() onepassword.ItemsSharesAPI {
  671. return nil
  672. }
  673. func (f *statefulFakeLister) Files() onepassword.ItemsFilesAPI {
  674. return f.fileLister
  675. }
  676. var _ onepassword.ItemsAPI = (*statefulFakeLister)(nil)
  677. type fakeClient struct {
  678. resolveResult string
  679. resolveError error
  680. resolveAll onepassword.ResolveAllResponse
  681. resolveAllError error
  682. listAllResult []onepassword.VaultOverview
  683. listAllError error
  684. }
  685. func (f *fakeClient) List(ctx context.Context) ([]onepassword.VaultOverview, error) {
  686. return f.listAllResult, f.listAllError
  687. }
  688. func (f *fakeClient) Resolve(ctx context.Context, secretReference string) (string, error) {
  689. return f.resolveResult, f.resolveError
  690. }
  691. func (f *fakeClient) ResolveAll(ctx context.Context, secretReferences []string) (onepassword.ResolveAllResponse, error) {
  692. return f.resolveAll, f.resolveAllError
  693. }
  694. func TestDeleteMultipleFieldsFromSameItem(t *testing.T) {
  695. fc := &fakeClient{
  696. listAllResult: []onepassword.VaultOverview{
  697. {
  698. ID: "test",
  699. Title: "test",
  700. },
  701. },
  702. }
  703. t.Run("deleting second field after item was deleted should not error", func(t *testing.T) {
  704. fl := &statefulFakeLister{
  705. listAllResult: []onepassword.ItemOverview{
  706. {
  707. ID: "test-item-id",
  708. Title: "key",
  709. Category: "login",
  710. VaultID: "vault-id",
  711. },
  712. },
  713. items: map[string]onepassword.Item{
  714. "test-item-id": {
  715. ID: "test-item-id",
  716. Title: "key",
  717. Category: "login",
  718. VaultID: "vault-id",
  719. Fields: []onepassword.ItemField{
  720. {
  721. ID: "field-1",
  722. Title: "username",
  723. FieldType: onepassword.ItemFieldTypeConcealed,
  724. Value: "testuser",
  725. },
  726. {
  727. ID: "field-2",
  728. Title: "password",
  729. FieldType: onepassword.ItemFieldTypeConcealed,
  730. Value: "testpass",
  731. },
  732. },
  733. },
  734. },
  735. }
  736. p := &SecretsClient{
  737. client: &onepassword.Client{
  738. SecretsAPI: fc,
  739. VaultsAPI: fc,
  740. ItemsAPI: fl,
  741. },
  742. }
  743. ctx := t.Context()
  744. err := p.DeleteSecret(ctx, &v1alpha1.PushSecretRemoteRef{
  745. RemoteKey: "key",
  746. Property: "username",
  747. })
  748. require.NoError(t, err, "first field deletion should succeed")
  749. assert.True(t, fl.putCalled, "Put should have been called to update the item")
  750. assert.False(t, fl.deleteCalled, "Delete should not have been called yet")
  751. fl.putCalled = false
  752. err = p.DeleteSecret(ctx, &v1alpha1.PushSecretRemoteRef{
  753. RemoteKey: "key",
  754. Property: "password",
  755. })
  756. require.NoError(t, err, "second field deletion should succeed")
  757. assert.True(t, fl.deleteCalled, "Delete should have been called to remove the item")
  758. fl.listAllResult = nil
  759. err = p.DeleteSecret(ctx, &v1alpha1.PushSecretRemoteRef{
  760. RemoteKey: "key",
  761. Property: "some-other-field",
  762. })
  763. require.NoError(t, err, "deleting a field from an already-deleted item should not error (this is the bug!)")
  764. })
  765. }
  766. func TestCachingGetSecret(t *testing.T) {
  767. t.Run("cache hit returns cached value", func(t *testing.T) {
  768. fcWithCounter := &fakeClientWithCounter{
  769. fakeClient: &fakeClient{
  770. resolveResult: "secret-value",
  771. },
  772. }
  773. p := &SecretsClient{
  774. client: &onepassword.Client{
  775. SecretsAPI: fcWithCounter,
  776. VaultsAPI: fcWithCounter.fakeClient,
  777. },
  778. vaultPrefix: "op://vault/",
  779. }
  780. // Initialize cache
  781. p.cache = expirable.NewLRU[string, []byte](100, nil, time.Minute)
  782. ref := v1.ExternalSecretDataRemoteRef{Key: "item/field"}
  783. // First call - cache miss
  784. val1, err := p.GetSecret(t.Context(), ref)
  785. require.NoError(t, err)
  786. assert.Equal(t, []byte("secret-value"), val1)
  787. assert.Equal(t, 1, fcWithCounter.resolveCallCount)
  788. // Second call - cache hit, should not call API
  789. val2, err := p.GetSecret(t.Context(), ref)
  790. require.NoError(t, err)
  791. assert.Equal(t, []byte("secret-value"), val2)
  792. assert.Equal(t, 1, fcWithCounter.resolveCallCount, "API should not be called on cache hit")
  793. })
  794. t.Run("cache disabled works normally", func(t *testing.T) {
  795. fcWithCounter := &fakeClientWithCounter{
  796. fakeClient: &fakeClient{
  797. resolveResult: "secret-value",
  798. },
  799. }
  800. p := &SecretsClient{
  801. client: &onepassword.Client{
  802. SecretsAPI: fcWithCounter,
  803. VaultsAPI: fcWithCounter.fakeClient,
  804. },
  805. vaultPrefix: "op://vault/",
  806. cache: nil, // Cache disabled
  807. }
  808. ref := v1.ExternalSecretDataRemoteRef{Key: "item/field"}
  809. // Multiple calls should always hit API
  810. _, err := p.GetSecret(t.Context(), ref)
  811. require.NoError(t, err)
  812. assert.Equal(t, 1, fcWithCounter.resolveCallCount)
  813. _, err = p.GetSecret(t.Context(), ref)
  814. require.NoError(t, err)
  815. assert.Equal(t, 2, fcWithCounter.resolveCallCount)
  816. })
  817. }
  818. func TestCachingGetSecretMap(t *testing.T) {
  819. t.Run("cache hit returns cached map", func(t *testing.T) {
  820. fc := &fakeClient{}
  821. flWithCounter := &fakeListerWithCounter{
  822. fakeLister: &fakeLister{
  823. listAllResult: []onepassword.ItemOverview{
  824. {
  825. ID: "item-id",
  826. Title: "item",
  827. Category: "login",
  828. VaultID: "vault-id",
  829. },
  830. },
  831. getResult: onepassword.Item{
  832. ID: "item-id",
  833. Title: "item",
  834. Category: "login",
  835. VaultID: "vault-id",
  836. Fields: []onepassword.ItemField{
  837. {Title: "username", Value: "user1"},
  838. {Title: "password", Value: "pass1"},
  839. },
  840. },
  841. },
  842. }
  843. p := &SecretsClient{
  844. client: &onepassword.Client{
  845. SecretsAPI: fc,
  846. VaultsAPI: fc,
  847. ItemsAPI: flWithCounter,
  848. },
  849. vaultPrefix: "op://vault/",
  850. vaultID: "vault-id",
  851. cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
  852. }
  853. ref := v1.ExternalSecretDataRemoteRef{Key: "item"}
  854. // First call - cache miss
  855. val1, err := p.GetSecretMap(t.Context(), ref)
  856. require.NoError(t, err)
  857. assert.Equal(t, map[string][]byte{
  858. "username": []byte("user1"),
  859. "password": []byte("pass1"),
  860. }, val1)
  861. assert.Equal(t, 1, flWithCounter.getCallCount)
  862. // Second call - cache hit
  863. val2, err := p.GetSecretMap(t.Context(), ref)
  864. require.NoError(t, err)
  865. assert.Equal(t, val1, val2)
  866. assert.Equal(t, 1, flWithCounter.getCallCount, "API should not be called on cache hit")
  867. })
  868. }
  869. func TestCacheInvalidationPushSecret(t *testing.T) {
  870. t.Run("push secret invalidates cache", func(t *testing.T) {
  871. fcWithCounter := &fakeClientWithCounter{
  872. fakeClient: &fakeClient{
  873. resolveResult: "secret-value",
  874. },
  875. }
  876. fl := &fakeLister{
  877. listAllResult: []onepassword.ItemOverview{
  878. {ID: "item-id", Title: "item", VaultID: "vault-id"},
  879. },
  880. getResult: onepassword.Item{
  881. ID: "item-id",
  882. Title: "item",
  883. VaultID: "vault-id",
  884. Fields: []onepassword.ItemField{{Title: "password", Value: "old"}},
  885. },
  886. }
  887. p := &SecretsClient{
  888. client: &onepassword.Client{
  889. SecretsAPI: fcWithCounter,
  890. VaultsAPI: fcWithCounter.fakeClient,
  891. ItemsAPI: fl,
  892. },
  893. vaultPrefix: "op://vault/",
  894. vaultID: "vault-id",
  895. cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
  896. }
  897. ref := v1.ExternalSecretDataRemoteRef{Key: "item/password"}
  898. // Populate cache
  899. val1, err := p.GetSecret(t.Context(), ref)
  900. require.NoError(t, err)
  901. assert.Equal(t, []byte("secret-value"), val1)
  902. assert.Equal(t, 1, fcWithCounter.resolveCallCount)
  903. // Push new value (should invalidate cache)
  904. pushRef := v1alpha1.PushSecretData{
  905. Match: v1alpha1.PushSecretMatch{
  906. SecretKey: "key",
  907. RemoteRef: v1alpha1.PushSecretRemoteRef{
  908. RemoteKey: "item",
  909. Property: "password",
  910. },
  911. },
  912. }
  913. secret := &corev1.Secret{
  914. Data: map[string][]byte{"key": []byte("new-value")},
  915. }
  916. err = p.PushSecret(t.Context(), secret, pushRef)
  917. require.NoError(t, err)
  918. // Next GetSecret should fetch fresh value (cache was invalidated)
  919. val2, err := p.GetSecret(t.Context(), ref)
  920. require.NoError(t, err)
  921. assert.Equal(t, []byte("secret-value"), val2)
  922. assert.Equal(t, 2, fcWithCounter.resolveCallCount, "Cache should have been invalidated")
  923. })
  924. }
  925. func TestCacheInvalidationDeleteSecret(t *testing.T) {
  926. t.Run("delete secret invalidates cache", func(t *testing.T) {
  927. fcWithCounter := &fakeClientWithCounter{
  928. fakeClient: &fakeClient{
  929. resolveResult: "cached-value",
  930. },
  931. }
  932. fl := &fakeLister{
  933. listAllResult: []onepassword.ItemOverview{
  934. {ID: "item-id", Title: "item", VaultID: "vault-id"},
  935. },
  936. getResult: onepassword.Item{
  937. ID: "item-id",
  938. Title: "item",
  939. VaultID: "vault-id",
  940. Fields: []onepassword.ItemField{
  941. {Title: "field1", Value: "val1"},
  942. {Title: "field2", Value: "val2"},
  943. },
  944. },
  945. }
  946. p := &SecretsClient{
  947. client: &onepassword.Client{
  948. SecretsAPI: fcWithCounter,
  949. VaultsAPI: fcWithCounter.fakeClient,
  950. ItemsAPI: fl,
  951. },
  952. vaultPrefix: "op://vault/",
  953. vaultID: "vault-id",
  954. cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
  955. }
  956. ref := v1.ExternalSecretDataRemoteRef{Key: "item/field1"}
  957. // Populate cache
  958. _, err := p.GetSecret(t.Context(), ref)
  959. require.NoError(t, err)
  960. assert.Equal(t, 1, fcWithCounter.resolveCallCount)
  961. // Delete field (should invalidate cache)
  962. deleteRef := v1alpha1.PushSecretRemoteRef{
  963. RemoteKey: "item",
  964. Property: "field1",
  965. }
  966. err = p.DeleteSecret(t.Context(), deleteRef)
  967. require.NoError(t, err)
  968. // Next GetSecret should miss cache
  969. _, err = p.GetSecret(t.Context(), ref)
  970. require.NoError(t, err)
  971. assert.Equal(t, 2, fcWithCounter.resolveCallCount, "Cache should have been invalidated")
  972. })
  973. }
  974. func TestInvalidateCacheByPrefix(t *testing.T) {
  975. t.Run("invalidates all entries with prefix", func(t *testing.T) {
  976. p := &SecretsClient{
  977. vaultPrefix: "op://vault/",
  978. cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
  979. }
  980. // Add multiple cache entries
  981. p.cache.Add("op://vault/item1/field1", []byte("val1"))
  982. p.cache.Add("op://vault/item1/field2", []byte("val2"))
  983. p.cache.Add("op://vault/item2/field1", []byte("val3"))
  984. // Invalidate item1 entries
  985. p.invalidateCacheByPrefix("op://vault/item1")
  986. // item1 entries should be gone
  987. _, ok1 := p.cache.Get("op://vault/item1/field1")
  988. assert.False(t, ok1)
  989. _, ok2 := p.cache.Get("op://vault/item1/field2")
  990. assert.False(t, ok2)
  991. // item2 entry should still exist
  992. val3, ok3 := p.cache.Get("op://vault/item2/field1")
  993. assert.True(t, ok3)
  994. assert.Equal(t, []byte("val3"), val3)
  995. })
  996. t.Run("handles nil cache gracefully", func(t *testing.T) {
  997. p := &SecretsClient{
  998. vaultPrefix: "op://vault/",
  999. cache: nil,
  1000. }
  1001. // Should not panic
  1002. p.invalidateCacheByPrefix("op://vault/item1")
  1003. })
  1004. t.Run("does not invalidate entries with similar prefixes", func(t *testing.T) {
  1005. p := &SecretsClient{
  1006. vaultPrefix: "op://vault/",
  1007. cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
  1008. }
  1009. p.cache.Add("op://vault/item/field1", []byte("val1"))
  1010. p.cache.Add("op://vault/item/field2", []byte("val2"))
  1011. p.cache.Add("op://vault/item|property", []byte("val3"))
  1012. p.cache.Add("op://vault/item-backup/field1", []byte("val4"))
  1013. p.cache.Add("op://vault/prod-db/secret", []byte("val5"))
  1014. p.cache.Add("op://vault/prod-db-replica/secret", []byte("val6"))
  1015. p.cache.Add("op://vault/prod-db-replica/secret|property", []byte("val7"))
  1016. p.invalidateCacheByPrefix("op://vault/item")
  1017. _, ok1 := p.cache.Get("op://vault/item/field1")
  1018. assert.False(t, ok1)
  1019. _, ok2 := p.cache.Get("op://vault/item/field2")
  1020. assert.False(t, ok2)
  1021. _, ok3 := p.cache.Get("op://vault/item|property")
  1022. assert.False(t, ok3)
  1023. val4, ok4 := p.cache.Get("op://vault/item-backup/field1")
  1024. assert.True(t, ok4, "item-backup should not be invalidated")
  1025. assert.Equal(t, []byte("val4"), val4)
  1026. p.invalidateCacheByPrefix("op://vault/prod-db")
  1027. _, ok5 := p.cache.Get("op://vault/prod-db/secret")
  1028. assert.False(t, ok5)
  1029. val6, ok6 := p.cache.Get("op://vault/prod-db-replica/secret")
  1030. assert.True(t, ok6, "prod-db-replica/secret should not be invalidated")
  1031. assert.Equal(t, []byte("val6"), val6)
  1032. val7, ok7 := p.cache.Get("op://vault/prod-db-replica/secret|property")
  1033. assert.True(t, ok7, "prod-db-replica/secret|property should not be invalidated")
  1034. assert.Equal(t, []byte("val7"), val7)
  1035. })
  1036. }
  1037. // fakeClientWithCounter wraps fakeClient and tracks Resolve call count.
  1038. type fakeClientWithCounter struct {
  1039. *fakeClient
  1040. resolveCallCount int
  1041. }
  1042. func (f *fakeClientWithCounter) Resolve(ctx context.Context, secretReference string) (string, error) {
  1043. f.resolveCallCount++
  1044. return f.fakeClient.Resolve(ctx, secretReference)
  1045. }
  1046. // fakeListerWithCounter wraps fakeLister and tracks Get call count.
  1047. type fakeListerWithCounter struct {
  1048. *fakeLister
  1049. getCallCount int
  1050. }
  1051. func (f *fakeListerWithCounter) Get(ctx context.Context, vaultID, itemID string) (onepassword.Item, error) {
  1052. f.getCallCount++
  1053. return f.fakeLister.Get(ctx, vaultID, itemID)
  1054. }
  1055. func (f *fakeListerWithCounter) Put(ctx context.Context, item onepassword.Item) (onepassword.Item, error) {
  1056. return f.fakeLister.Put(ctx, item)
  1057. }
  1058. func (f *fakeListerWithCounter) Delete(ctx context.Context, vaultID, itemID string) error {
  1059. return f.fakeLister.Delete(ctx, vaultID, itemID)
  1060. }
  1061. func (f *fakeListerWithCounter) Archive(ctx context.Context, vaultID, itemID string) error {
  1062. return f.fakeLister.Archive(ctx, vaultID, itemID)
  1063. }
  1064. func (f *fakeListerWithCounter) List(ctx context.Context, vaultID string, opts ...onepassword.ItemListFilter) ([]onepassword.ItemOverview, error) {
  1065. return f.fakeLister.List(ctx, vaultID, opts...)
  1066. }
  1067. func (f *fakeListerWithCounter) Shares() onepassword.ItemsSharesAPI {
  1068. return f.fakeLister.Shares()
  1069. }
  1070. func (f *fakeListerWithCounter) Files() onepassword.ItemsFilesAPI {
  1071. return f.fakeLister.Files()
  1072. }
  1073. func (f *fakeListerWithCounter) Create(ctx context.Context, item onepassword.ItemCreateParams) (onepassword.Item, error) {
  1074. return f.fakeLister.Create(ctx, item)
  1075. }
  1076. var _ onepassword.SecretsAPI = &fakeClient{}
  1077. var _ onepassword.VaultsAPI = &fakeClient{}
  1078. var _ onepassword.ItemsAPI = &fakeLister{}
  1079. var _ onepassword.SecretsAPI = &fakeClientWithCounter{}
  1080. var _ onepassword.ItemsAPI = &fakeListerWithCounter{}
  1081. func TestSecretExists(t *testing.T) {
  1082. fc := &fakeClient{
  1083. listAllResult: []onepassword.VaultOverview{
  1084. {ID: "vault-id", Title: "vault"},
  1085. },
  1086. }
  1087. itemWithPassword := &fakeLister{
  1088. listAllResult: []onepassword.ItemOverview{
  1089. {ID: "item-id", Title: "key", VaultID: "vault-id"},
  1090. },
  1091. getResult: onepassword.Item{
  1092. ID: "item-id", Title: "key", VaultID: "vault-id",
  1093. Fields: []onepassword.ItemField{
  1094. {Title: "password", Value: "s3cr3t"},
  1095. },
  1096. },
  1097. }
  1098. tests := []struct {
  1099. name string
  1100. ref v1alpha1.PushSecretRemoteRef
  1101. lister *fakeLister
  1102. wantExists bool
  1103. assertError func(t *testing.T, err error)
  1104. }{
  1105. {
  1106. name: "item does not exist returns false",
  1107. ref: v1alpha1.PushSecretRemoteRef{RemoteKey: "missing"},
  1108. lister: &fakeLister{listAllResult: []onepassword.ItemOverview{}},
  1109. wantExists: false,
  1110. assertError: func(t *testing.T, err error) { require.NoError(t, err) },
  1111. },
  1112. {
  1113. name: "item exists no property returns true",
  1114. ref: v1alpha1.PushSecretRemoteRef{RemoteKey: "key"},
  1115. lister: itemWithPassword,
  1116. wantExists: true,
  1117. assertError: func(t *testing.T, err error) { require.NoError(t, err) },
  1118. },
  1119. {
  1120. name: "item exists field present returns true",
  1121. ref: v1alpha1.PushSecretRemoteRef{RemoteKey: "key", Property: "password"},
  1122. lister: itemWithPassword,
  1123. wantExists: true,
  1124. assertError: func(t *testing.T, err error) { require.NoError(t, err) },
  1125. },
  1126. {
  1127. name: "item exists field absent returns false",
  1128. ref: v1alpha1.PushSecretRemoteRef{RemoteKey: "key", Property: "api-token"},
  1129. lister: itemWithPassword,
  1130. wantExists: false,
  1131. assertError: func(t *testing.T, err error) { require.NoError(t, err) },
  1132. },
  1133. {
  1134. name: "pushAllKeys scenario: item exists with no fields returns true",
  1135. ref: v1alpha1.PushSecretRemoteRef{RemoteKey: "key"},
  1136. lister: &fakeLister{
  1137. listAllResult: []onepassword.ItemOverview{
  1138. {ID: "item-id", Title: "key", VaultID: "vault-id"},
  1139. },
  1140. getResult: onepassword.Item{
  1141. ID: "item-id", Title: "key", VaultID: "vault-id",
  1142. Fields: []onepassword.ItemField{},
  1143. },
  1144. },
  1145. wantExists: true,
  1146. assertError: func(t *testing.T, err error) { require.NoError(t, err) },
  1147. },
  1148. }
  1149. for _, tt := range tests {
  1150. t.Run(tt.name, func(t *testing.T) {
  1151. p := &SecretsClient{
  1152. client: &onepassword.Client{
  1153. SecretsAPI: fc,
  1154. VaultsAPI: fc,
  1155. ItemsAPI: tt.lister,
  1156. },
  1157. vaultID: "vault-id",
  1158. }
  1159. exists, err := p.SecretExists(t.Context(), tt.ref)
  1160. tt.assertError(t, err)
  1161. assert.Equal(t, tt.wantExists, exists)
  1162. })
  1163. }
  1164. }
  1165. func TestResolveFieldType(t *testing.T) {
  1166. tests := []struct {
  1167. input string
  1168. expected onepassword.ItemFieldType
  1169. }{
  1170. {"text", onepassword.ItemFieldTypeText},
  1171. {"Text", onepassword.ItemFieldTypeText},
  1172. {"TEXT", onepassword.ItemFieldTypeText},
  1173. {"concealed", onepassword.ItemFieldTypeConcealed},
  1174. {"Concealed", onepassword.ItemFieldTypeConcealed},
  1175. {"url", onepassword.ItemFieldTypeURL},
  1176. {"URL", onepassword.ItemFieldTypeURL},
  1177. {"email", onepassword.ItemFieldTypeEmail},
  1178. {"Email", onepassword.ItemFieldTypeEmail},
  1179. {"phone", onepassword.ItemFieldTypePhone},
  1180. {"date", onepassword.ItemFieldTypeDate},
  1181. {"monthYear", onepassword.ItemFieldTypeMonthYear},
  1182. {"monthyear", onepassword.ItemFieldTypeMonthYear},
  1183. {"MONTHYEAR", onepassword.ItemFieldTypeMonthYear},
  1184. {"", onepassword.ItemFieldTypeConcealed},
  1185. {"unknown", onepassword.ItemFieldTypeConcealed},
  1186. {"otp", onepassword.ItemFieldTypeConcealed},
  1187. {"file", onepassword.ItemFieldTypeConcealed},
  1188. }
  1189. for _, tt := range tests {
  1190. t.Run(tt.input, func(t *testing.T) {
  1191. got := resolveFieldType(tt.input)
  1192. assert.Equal(t, tt.expected, got)
  1193. })
  1194. }
  1195. }
  1196. func TestPushSecretFieldType(t *testing.T) {
  1197. fc := &fakeClient{
  1198. listAllResult: []onepassword.VaultOverview{
  1199. {ID: "vault-id", Title: "vault"},
  1200. },
  1201. }
  1202. tests := []struct {
  1203. name string
  1204. metadataJSON string
  1205. wantFieldType onepassword.ItemFieldType
  1206. }{
  1207. {
  1208. name: "no metadata defaults to Concealed",
  1209. metadataJSON: "",
  1210. wantFieldType: onepassword.ItemFieldTypeConcealed,
  1211. },
  1212. {
  1213. name: "fieldType text creates Text field",
  1214. metadataJSON: `{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"fieldType":"text"}}`,
  1215. wantFieldType: onepassword.ItemFieldTypeText,
  1216. },
  1217. {
  1218. name: "fieldType URL case-insensitive",
  1219. metadataJSON: `{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"fieldType":"URL"}}`,
  1220. wantFieldType: onepassword.ItemFieldTypeURL,
  1221. },
  1222. }
  1223. for _, tt := range tests {
  1224. t.Run(tt.name, func(t *testing.T) {
  1225. fl := &fakeLister{
  1226. listAllResult: []onepassword.ItemOverview{},
  1227. }
  1228. p := &SecretsClient{
  1229. client: &onepassword.Client{
  1230. SecretsAPI: fc,
  1231. VaultsAPI: fc,
  1232. ItemsAPI: fl,
  1233. },
  1234. vaultID: "vault-id",
  1235. }
  1236. ref := v1alpha1.PushSecretData{
  1237. Match: v1alpha1.PushSecretMatch{
  1238. SecretKey: "key",
  1239. RemoteRef: v1alpha1.PushSecretRemoteRef{RemoteKey: "item", Property: "field"},
  1240. },
  1241. }
  1242. if tt.metadataJSON != "" {
  1243. raw := apiextensionsv1.JSON{Raw: []byte(tt.metadataJSON)}
  1244. ref.Metadata = &raw
  1245. }
  1246. secret := &corev1.Secret{
  1247. Data: map[string][]byte{"key": []byte("value")},
  1248. }
  1249. err := p.PushSecret(t.Context(), secret, ref)
  1250. require.NoError(t, err)
  1251. require.True(t, fl.createCalled, "Create should have been called")
  1252. assert.Equal(t, tt.wantFieldType, fl.createdFieldType)
  1253. })
  1254. }
  1255. }
  1256. func TestUpdateFieldValueChangesFieldType(t *testing.T) {
  1257. // Regression test: updateFieldValue must update FieldType when spec.fieldType changes,
  1258. // not only when Value changes.
  1259. fields := []onepassword.ItemField{
  1260. {Title: "myfield", Value: "secret", FieldType: onepassword.ItemFieldTypeConcealed},
  1261. }
  1262. updated, err := updateFieldValue(fields, "myfield", "secret", onepassword.ItemFieldTypeText)
  1263. require.NoError(t, err)
  1264. require.Len(t, updated, 1)
  1265. assert.Equal(t, onepassword.ItemFieldTypeText, updated[0].FieldType, "FieldType should be updated even when Value is unchanged")
  1266. assert.Equal(t, "secret", updated[0].Value)
  1267. }
  1268. func TestGenerateNewItemFieldHasNonEmptyID(t *testing.T) {
  1269. // Regression test: fields created without an ID cause "duplicate field ids" errors
  1270. // when two PushSecret data entries target the same 1Password item.
  1271. // See: generateNewItemField must always produce a non-empty ID.
  1272. tests := []struct {
  1273. title string
  1274. fieldType onepassword.ItemFieldType
  1275. }{
  1276. {"password", onepassword.ItemFieldTypeConcealed},
  1277. {"api-endpoint", onepassword.ItemFieldTypeURL},
  1278. {"username", onepassword.ItemFieldTypeText},
  1279. }
  1280. for _, tt := range tests {
  1281. field := generateNewItemField(tt.title, "value", tt.fieldType)
  1282. assert.NotEmpty(t, field.ID, "field ID must be non-empty to avoid duplicate ID errors on Put")
  1283. assert.Equal(t, tt.title, field.Title)
  1284. assert.Equal(t, "value", field.Value)
  1285. }
  1286. }
  1287. func TestNormalizeItemFields(t *testing.T) {
  1288. // Regression test: fields fetched from 1Password can have SectionID pointing to ""
  1289. // instead of nil. The SDK rejects Put when a field references a section ID that
  1290. // doesn't exist in item.Sections — even an empty-string pointer triggers this.
  1291. emptyStr := ""
  1292. realSection := "extra"
  1293. fields := []onepassword.ItemField{
  1294. {ID: "a", Title: "a", SectionID: &emptyStr},
  1295. {ID: "b", Title: "b", SectionID: nil},
  1296. {ID: "c", Title: "c", SectionID: &realSection},
  1297. }
  1298. got := normalizeItemFields(fields)
  1299. assert.Nil(t, got[0].SectionID, "empty-string SectionID should be normalized to nil")
  1300. assert.Nil(t, got[1].SectionID, "nil SectionID should remain nil")
  1301. assert.Equal(t, &realSection, got[2].SectionID, "non-empty SectionID should be unchanged")
  1302. }
  1303. func TestIsNativeItemID(t *testing.T) {
  1304. tests := []struct {
  1305. name string
  1306. input string
  1307. expected bool
  1308. }{
  1309. {"valid native ID", "gdpvdudxrico74msloimk7qjna", true},
  1310. {"valid native ID all letters", "abcdefghijklmnopqrstuvwxyz", true},
  1311. {"valid native ID with digits", "abcdefghij0123456789abcdef", true},
  1312. {"too short", "gdpvdudxrico74msloimk7qjn", false},
  1313. {"too long", "gdpvdudxrico74msloimk7qjnaa", false},
  1314. {"empty string", "", false},
  1315. {"contains uppercase", "Gdpvdudxrico74msloimk7qjna", false},
  1316. {"contains special char", "gdpvdudxrico7-msloimk7qjna", false},
  1317. {"RFC 4122 UUID", "687adbe7-e6d2-4059-9a62-dbb95d291143", false},
  1318. {"item title", "My App (Production)", false},
  1319. }
  1320. for _, tt := range tests {
  1321. t.Run(tt.name, func(t *testing.T) {
  1322. got := isNativeItemID(tt.input)
  1323. if got != tt.expected {
  1324. t.Errorf("isNativeItemID(%q) = %v, want %v", tt.input, got, tt.expected)
  1325. }
  1326. })
  1327. }
  1328. }
  1329. func TestPushAllKeys(t *testing.T) {
  1330. const (
  1331. testExistingItem = "existing-item"
  1332. testOldKey = "old-key"
  1333. )
  1334. fc := &fakeClient{listAllResult: []onepassword.VaultOverview{{ID: "vault-id", Title: "vault"}}}
  1335. existingItem := onepassword.Item{
  1336. ID: "item-id", Title: testExistingItem, VaultID: "vault-id",
  1337. Fields: []onepassword.ItemField{
  1338. {ID: testOldKey, Title: testOldKey, Value: "old-val", FieldType: onepassword.ItemFieldTypeConcealed},
  1339. },
  1340. }
  1341. newLister := func(existing ...onepassword.Item) *fakeLister {
  1342. fl := &fakeLister{listAllResult: []onepassword.ItemOverview{}}
  1343. if len(existing) > 0 {
  1344. fl.getResult = existing[0]
  1345. fl.listAllResult = []onepassword.ItemOverview{{ID: existing[0].ID, Title: existing[0].Title, VaultID: existing[0].VaultID}}
  1346. }
  1347. return fl
  1348. }
  1349. fieldsMap := func(fields []onepassword.ItemField) map[string]onepassword.ItemField {
  1350. m := make(map[string]onepassword.ItemField, len(fields))
  1351. for _, f := range fields {
  1352. m[f.Title] = f
  1353. }
  1354. return m
  1355. }
  1356. ref := func(key, remoteKey string, meta ...string) v1alpha1.PushSecretData {
  1357. d := v1alpha1.PushSecretData{Match: v1alpha1.PushSecretMatch{SecretKey: key, RemoteRef: v1alpha1.PushSecretRemoteRef{RemoteKey: remoteKey}}}
  1358. if len(meta) > 0 {
  1359. raw := apiextensionsv1.JSON{Raw: []byte(meta[0])}
  1360. d.Metadata = &raw
  1361. }
  1362. return d
  1363. }
  1364. secret := func(kv ...string) *corev1.Secret {
  1365. s := &corev1.Secret{Data: map[string][]byte{}}
  1366. for i := 0; i+1 < len(kv); i += 2 {
  1367. s.Data[kv[i]] = []byte(kv[i+1])
  1368. }
  1369. return s
  1370. }
  1371. t.Run("creates new item with all secret keys as concealed fields", func(t *testing.T) {
  1372. fl := newLister()
  1373. p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, vaultID: "vault-id"}
  1374. require.NoError(t, p.PushSecret(t.Context(), secret("alpha", "val-alpha", "beta", "val-beta"), ref("", "my-item")))
  1375. require.True(t, fl.createCalled)
  1376. assert.False(t, fl.putCalled)
  1377. fm := fieldsMap(fl.createdParams.Fields)
  1378. assert.Equal(t, "val-alpha", fm["alpha"].Value)
  1379. assert.Equal(t, onepassword.ItemFieldTypeConcealed, fm["alpha"].FieldType)
  1380. assert.Equal(t, "val-beta", fm["beta"].Value)
  1381. })
  1382. t.Run("updates existing item with all secret keys", func(t *testing.T) {
  1383. fl := newLister(onepassword.Item{ID: "item-id", Title: testExistingItem, VaultID: "vault-id"})
  1384. p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, vaultID: "vault-id"}
  1385. require.NoError(t, p.PushSecret(t.Context(), secret("key1", "value1", "key2", "value2"), ref("", testExistingItem)))
  1386. assert.False(t, fl.createCalled)
  1387. require.True(t, fl.putCalled)
  1388. fm := fieldsMap(fl.putItem.Fields)
  1389. assert.Equal(t, "value1", fm["key1"].Value)
  1390. assert.Equal(t, "value2", fm["key2"].Value)
  1391. })
  1392. t.Run("applies tags from metadata on create", func(t *testing.T) {
  1393. fl := newLister()
  1394. p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, vaultID: "vault-id"}
  1395. meta := `{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"tags":["env:prod","team:backend"]}}`
  1396. require.NoError(t, p.PushSecret(t.Context(), secret("k", "v"), ref("", "tagged-item", meta)))
  1397. require.True(t, fl.createCalled)
  1398. assert.Equal(t, []string{"env:prod", "team:backend"}, fl.createdParams.Tags)
  1399. })
  1400. t.Run("removes fields deleted from the secret", func(t *testing.T) {
  1401. fl := newLister(existingItem) // existingItem has field testOldKey
  1402. p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, vaultID: "vault-id"}
  1403. // secret no longer contains testOldKey, only "new-key"
  1404. require.NoError(t, p.PushSecret(t.Context(), secret("new-key", "new-val"), ref("", testExistingItem)))
  1405. require.True(t, fl.putCalled)
  1406. fm := fieldsMap(fl.putItem.Fields)
  1407. assert.Equal(t, "new-val", fm["new-key"].Value, "new field must be added")
  1408. _, stillThere := fm[testOldKey]
  1409. assert.False(t, stillThere, "deleted key must be removed from the 1Password item")
  1410. })
  1411. }