client_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. /*
  2. Copyright © 2025 ESO Maintainer Team
  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. "testing"
  18. "github.com/1password/onepassword-sdk-go"
  19. "github.com/stretchr/testify/assert"
  20. "github.com/stretchr/testify/require"
  21. corev1 "k8s.io/api/core/v1"
  22. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  23. v1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  24. "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
  25. )
  26. func TestProviderGetSecret(t *testing.T) {
  27. tests := []struct {
  28. name string
  29. ref v1.ExternalSecretDataRemoteRef
  30. want []byte
  31. assertError func(t *testing.T, err error)
  32. client func() *onepassword.Client
  33. }{
  34. {
  35. name: "get secret successfully",
  36. client: func() *onepassword.Client {
  37. fc := &fakeClient{
  38. resolveResult: "secret",
  39. }
  40. return &onepassword.Client{
  41. SecretsAPI: fc,
  42. VaultsAPI: fc,
  43. }
  44. },
  45. assertError: func(t *testing.T, err error) {
  46. require.NoError(t, err)
  47. },
  48. ref: v1.ExternalSecretDataRemoteRef{
  49. Key: "secret",
  50. },
  51. want: []byte("secret"),
  52. },
  53. {
  54. name: "get secret with error",
  55. client: func() *onepassword.Client {
  56. fc := &fakeClient{
  57. resolveError: errors.New("fobar"),
  58. }
  59. return &onepassword.Client{
  60. SecretsAPI: fc,
  61. VaultsAPI: fc,
  62. }
  63. },
  64. assertError: func(t *testing.T, err error) {
  65. require.ErrorContains(t, err, "fobar")
  66. },
  67. ref: v1.ExternalSecretDataRemoteRef{
  68. Key: "secret",
  69. },
  70. },
  71. {
  72. name: "get secret version not implemented",
  73. client: func() *onepassword.Client {
  74. fc := &fakeClient{
  75. resolveResult: "secret",
  76. }
  77. return &onepassword.Client{
  78. SecretsAPI: fc,
  79. VaultsAPI: fc,
  80. }
  81. },
  82. ref: v1.ExternalSecretDataRemoteRef{
  83. Key: "secret",
  84. Version: "1",
  85. },
  86. assertError: func(t *testing.T, err error) {
  87. require.ErrorContains(t, err, "is not implemented in the 1Password SDK provider")
  88. },
  89. },
  90. }
  91. for _, tt := range tests {
  92. t.Run(tt.name, func(t *testing.T) {
  93. p := &Provider{
  94. client: tt.client(),
  95. vaultPrefix: "op://vault/",
  96. }
  97. got, err := p.GetSecret(context.Background(), tt.ref)
  98. tt.assertError(t, err)
  99. require.Equal(t, string(got), string(tt.want))
  100. })
  101. }
  102. }
  103. func TestProviderGetSecretMap(t *testing.T) {
  104. tests := []struct {
  105. name string
  106. ref v1.ExternalSecretDataRemoteRef
  107. want map[string][]byte
  108. assertError func(t *testing.T, err error)
  109. client func() *onepassword.Client
  110. }{
  111. {
  112. name: "get secret successfully for files",
  113. client: func() *onepassword.Client {
  114. fc := &fakeClient{}
  115. fl := &fakeLister{
  116. listAllResult: []onepassword.ItemOverview{
  117. {
  118. ID: "test-item-id",
  119. Title: "key",
  120. Category: "login",
  121. VaultID: "vault-id",
  122. },
  123. },
  124. getResult: onepassword.Item{
  125. ID: "test-item-id",
  126. Title: "key",
  127. Category: "login",
  128. VaultID: "vault-id",
  129. Files: []onepassword.ItemFile{
  130. {
  131. Attributes: onepassword.FileAttributes{
  132. Name: "name",
  133. ID: "id",
  134. },
  135. FieldID: "field-id",
  136. },
  137. },
  138. },
  139. fileLister: &fakeFileLister{
  140. readContent: []byte("content"),
  141. },
  142. }
  143. return &onepassword.Client{
  144. SecretsAPI: fc,
  145. ItemsAPI: fl,
  146. VaultsAPI: fc,
  147. }
  148. },
  149. assertError: func(t *testing.T, err error) {
  150. require.NoError(t, err)
  151. },
  152. ref: v1.ExternalSecretDataRemoteRef{
  153. Key: "key",
  154. Property: "file/name",
  155. },
  156. want: map[string][]byte{
  157. "name": []byte("content"),
  158. },
  159. },
  160. {
  161. name: "get secret successfully for fields",
  162. client: func() *onepassword.Client {
  163. fc := &fakeClient{}
  164. fl := &fakeLister{
  165. listAllResult: []onepassword.ItemOverview{
  166. {
  167. ID: "test-item-id",
  168. Title: "key",
  169. Category: "login",
  170. VaultID: "vault-id",
  171. },
  172. },
  173. getResult: onepassword.Item{
  174. ID: "test-item-id",
  175. Title: "key",
  176. Category: "login",
  177. VaultID: "vault-id",
  178. Fields: []onepassword.ItemField{
  179. {
  180. ID: "field-id",
  181. Title: "name",
  182. FieldType: onepassword.ItemFieldTypeConcealed,
  183. Value: "value",
  184. },
  185. },
  186. },
  187. fileLister: &fakeFileLister{
  188. readContent: []byte("content"),
  189. },
  190. }
  191. return &onepassword.Client{
  192. SecretsAPI: fc,
  193. ItemsAPI: fl,
  194. VaultsAPI: fc,
  195. }
  196. },
  197. assertError: func(t *testing.T, err error) {
  198. require.NoError(t, err)
  199. },
  200. ref: v1.ExternalSecretDataRemoteRef{
  201. Key: "key",
  202. Property: "field/name",
  203. },
  204. want: map[string][]byte{
  205. "name": []byte("value"),
  206. },
  207. },
  208. {
  209. name: "get secret fails with fields with same title",
  210. client: func() *onepassword.Client {
  211. fc := &fakeClient{}
  212. fl := &fakeLister{
  213. listAllResult: []onepassword.ItemOverview{
  214. {
  215. ID: "test-item-id",
  216. Title: "key",
  217. Category: "login",
  218. VaultID: "vault-id",
  219. },
  220. },
  221. getResult: onepassword.Item{
  222. ID: "test-item-id",
  223. Title: "key",
  224. Category: "login",
  225. VaultID: "vault-id",
  226. Fields: []onepassword.ItemField{
  227. {
  228. ID: "field-id",
  229. Title: "name",
  230. FieldType: onepassword.ItemFieldTypeConcealed,
  231. Value: "value",
  232. },
  233. {
  234. ID: "field-id",
  235. Title: "name",
  236. FieldType: onepassword.ItemFieldTypeConcealed,
  237. Value: "value",
  238. },
  239. },
  240. },
  241. fileLister: &fakeFileLister{
  242. readContent: []byte("content"),
  243. },
  244. }
  245. return &onepassword.Client{
  246. SecretsAPI: fc,
  247. ItemsAPI: fl,
  248. VaultsAPI: fc,
  249. }
  250. },
  251. assertError: func(t *testing.T, err error) {
  252. require.ErrorContains(t, err, "found more than 1 fields with title 'name' in 'key', got 2")
  253. },
  254. ref: v1.ExternalSecretDataRemoteRef{
  255. Key: "key",
  256. Property: "field/name",
  257. },
  258. },
  259. }
  260. for _, tt := range tests {
  261. t.Run(tt.name, func(t *testing.T) {
  262. p := &Provider{
  263. client: tt.client(),
  264. vaultPrefix: "op://vault/",
  265. }
  266. got, err := p.GetSecretMap(context.Background(), tt.ref)
  267. tt.assertError(t, err)
  268. require.Equal(t, tt.want, got)
  269. })
  270. }
  271. }
  272. func TestProviderValidate(t *testing.T) {
  273. tests := []struct {
  274. name string
  275. want v1.ValidationResult
  276. assertError func(t *testing.T, err error)
  277. client func() *onepassword.Client
  278. vaultPrefix string
  279. }{
  280. {
  281. name: "validate successfully",
  282. client: func() *onepassword.Client {
  283. fc := &fakeClient{
  284. listAllResult: []onepassword.VaultOverview{
  285. {
  286. ID: "test",
  287. Title: "test",
  288. },
  289. },
  290. }
  291. return &onepassword.Client{
  292. SecretsAPI: fc,
  293. VaultsAPI: fc,
  294. }
  295. },
  296. want: v1.ValidationResultReady,
  297. assertError: func(t *testing.T, err error) {
  298. require.NoError(t, err)
  299. },
  300. vaultPrefix: "op://vault/",
  301. },
  302. }
  303. for _, tt := range tests {
  304. t.Run(tt.name, func(t *testing.T) {
  305. p := &Provider{
  306. client: tt.client(),
  307. vaultPrefix: tt.vaultPrefix,
  308. }
  309. got, err := p.Validate()
  310. tt.assertError(t, err)
  311. require.Equal(t, got, tt.want)
  312. })
  313. }
  314. }
  315. func TestPushSecret(t *testing.T) {
  316. fc := &fakeClient{
  317. listAllResult: []onepassword.VaultOverview{
  318. {
  319. ID: "test",
  320. Title: "test",
  321. },
  322. },
  323. }
  324. tests := []struct {
  325. name string
  326. ref v1alpha1.PushSecretData
  327. secret *corev1.Secret
  328. assertError func(t *testing.T, err error)
  329. lister func() *fakeLister
  330. assertLister func(t *testing.T, lister *fakeLister)
  331. }{
  332. {
  333. name: "create is called",
  334. lister: func() *fakeLister {
  335. return &fakeLister{
  336. listAllResult: []onepassword.ItemOverview{},
  337. }
  338. },
  339. secret: &corev1.Secret{
  340. Data: map[string][]byte{
  341. "foo": []byte("bar"),
  342. },
  343. ObjectMeta: metav1.ObjectMeta{
  344. Name: "secret",
  345. Namespace: "default",
  346. },
  347. },
  348. ref: v1alpha1.PushSecretData{
  349. Match: v1alpha1.PushSecretMatch{
  350. SecretKey: "foo",
  351. RemoteRef: v1alpha1.PushSecretRemoteRef{
  352. RemoteKey: "key",
  353. },
  354. },
  355. },
  356. assertError: func(t *testing.T, err error) {
  357. require.NoError(t, err)
  358. },
  359. assertLister: func(t *testing.T, lister *fakeLister) {
  360. assert.True(t, lister.createCalled)
  361. },
  362. },
  363. {
  364. name: "update is called",
  365. lister: func() *fakeLister {
  366. return &fakeLister{
  367. listAllResult: []onepassword.ItemOverview{
  368. {
  369. ID: "test-item-id",
  370. Title: "key",
  371. Category: "login",
  372. VaultID: "vault-id",
  373. },
  374. },
  375. }
  376. },
  377. secret: &corev1.Secret{
  378. Data: map[string][]byte{
  379. "foo": []byte("bar"),
  380. },
  381. ObjectMeta: metav1.ObjectMeta{
  382. Name: "secret",
  383. Namespace: "default",
  384. },
  385. },
  386. ref: v1alpha1.PushSecretData{
  387. Match: v1alpha1.PushSecretMatch{
  388. SecretKey: "foo",
  389. RemoteRef: v1alpha1.PushSecretRemoteRef{
  390. RemoteKey: "key",
  391. },
  392. },
  393. },
  394. assertError: func(t *testing.T, err error) {
  395. require.NoError(t, err)
  396. },
  397. assertLister: func(t *testing.T, lister *fakeLister) {
  398. assert.True(t, lister.putCalled)
  399. },
  400. },
  401. }
  402. for _, tt := range tests {
  403. t.Run(tt.name, func(t *testing.T) {
  404. ctx := context.Background()
  405. lister := tt.lister()
  406. p := &Provider{
  407. client: &onepassword.Client{
  408. SecretsAPI: fc,
  409. VaultsAPI: fc,
  410. ItemsAPI: lister,
  411. },
  412. }
  413. err := p.PushSecret(ctx, tt.secret, tt.ref)
  414. tt.assertError(t, err)
  415. tt.assertLister(t, lister)
  416. })
  417. }
  418. }
  419. func TestDeleteItemField(t *testing.T) {
  420. fc := &fakeClient{
  421. listAllResult: []onepassword.VaultOverview{
  422. {
  423. ID: "test",
  424. Title: "test",
  425. },
  426. },
  427. }
  428. testCases := []struct {
  429. name string
  430. lister func() *fakeLister
  431. ref *v1alpha1.PushSecretRemoteRef
  432. assertError func(t *testing.T, err error)
  433. assertLister func(t *testing.T, lister *fakeLister)
  434. }{
  435. {
  436. name: "update is called",
  437. ref: &v1alpha1.PushSecretRemoteRef{
  438. RemoteKey: "key",
  439. Property: "password",
  440. },
  441. assertLister: func(t *testing.T, lister *fakeLister) {
  442. require.True(t, lister.putCalled)
  443. },
  444. lister: func() *fakeLister {
  445. fl := &fakeLister{
  446. listAllResult: []onepassword.ItemOverview{
  447. {
  448. ID: "test-item-id",
  449. Title: "key",
  450. Category: "login",
  451. VaultID: "vault-id",
  452. },
  453. },
  454. getResult: onepassword.Item{
  455. ID: "test-item-id",
  456. Title: "key",
  457. Category: "login",
  458. VaultID: "vault-id",
  459. Fields: []onepassword.ItemField{
  460. {
  461. ID: "field-1",
  462. Title: "password",
  463. FieldType: onepassword.ItemFieldTypeConcealed,
  464. Value: "password",
  465. },
  466. {
  467. ID: "field-2",
  468. Title: "other-field",
  469. FieldType: onepassword.ItemFieldTypeConcealed,
  470. Value: "username",
  471. },
  472. },
  473. },
  474. }
  475. return fl
  476. },
  477. assertError: func(t *testing.T, err error) {
  478. require.NoError(t, err)
  479. },
  480. },
  481. {
  482. name: "delete is called",
  483. ref: &v1alpha1.PushSecretRemoteRef{
  484. RemoteKey: "key",
  485. Property: "password",
  486. },
  487. assertLister: func(t *testing.T, lister *fakeLister) {
  488. require.True(t, lister.deleteCalled, "delete should have been called as the item should have existed")
  489. },
  490. lister: func() *fakeLister {
  491. fl := &fakeLister{
  492. listAllResult: []onepassword.ItemOverview{
  493. {
  494. ID: "test-item-id",
  495. Title: "key",
  496. Category: "login",
  497. VaultID: "vault-id",
  498. },
  499. },
  500. getResult: onepassword.Item{
  501. ID: "test-item-id",
  502. Title: "key",
  503. Category: "login",
  504. VaultID: "vault-id",
  505. Fields: []onepassword.ItemField{
  506. {
  507. ID: "field-1",
  508. Title: "password",
  509. FieldType: onepassword.ItemFieldTypeConcealed,
  510. Value: "password",
  511. },
  512. },
  513. },
  514. }
  515. return fl
  516. },
  517. assertError: func(t *testing.T, err error) {
  518. require.NoError(t, err)
  519. },
  520. },
  521. }
  522. for _, testCase := range testCases {
  523. t.Run(testCase.name, func(t *testing.T) {
  524. ctx := context.Background()
  525. lister := testCase.lister()
  526. p := &Provider{
  527. client: &onepassword.Client{
  528. SecretsAPI: fc,
  529. VaultsAPI: fc,
  530. ItemsAPI: lister,
  531. },
  532. }
  533. testCase.assertError(t, p.DeleteSecret(ctx, testCase.ref))
  534. testCase.assertLister(t, lister)
  535. })
  536. }
  537. }
  538. func TestGetVault(t *testing.T) {
  539. fc := &fakeClient{
  540. listAllResult: []onepassword.VaultOverview{
  541. {
  542. ID: "vault-id",
  543. Title: "vault-title",
  544. },
  545. },
  546. }
  547. p := &Provider{
  548. client: &onepassword.Client{
  549. VaultsAPI: fc,
  550. },
  551. }
  552. titleOrUuids := []string{"vault-title", "vault-id"}
  553. for _, titleOrUuid := range titleOrUuids {
  554. t.Run(titleOrUuid, func(t *testing.T) {
  555. vaultID, err := p.GetVault(context.Background(), titleOrUuid)
  556. require.NoError(t, err)
  557. require.Equal(t, fc.listAllResult[0].ID, vaultID)
  558. })
  559. }
  560. }
  561. type fakeLister struct {
  562. listAllResult []onepassword.ItemOverview
  563. createCalled bool
  564. putCalled bool
  565. deleteCalled bool
  566. getResult onepassword.Item
  567. fileLister onepassword.ItemsFilesAPI
  568. }
  569. func (f *fakeLister) Create(ctx context.Context, params onepassword.ItemCreateParams) (onepassword.Item, error) {
  570. f.createCalled = true
  571. return onepassword.Item{}, nil
  572. }
  573. func (f *fakeLister) Get(ctx context.Context, vaultID, itemID string) (onepassword.Item, error) {
  574. return f.getResult, nil
  575. }
  576. func (f *fakeLister) Put(ctx context.Context, item onepassword.Item) (onepassword.Item, error) {
  577. f.putCalled = true
  578. return onepassword.Item{}, nil
  579. }
  580. func (f *fakeLister) Delete(ctx context.Context, vaultID, itemID string) error {
  581. f.deleteCalled = true
  582. return nil
  583. }
  584. func (f *fakeLister) Archive(ctx context.Context, vaultID, itemID string) error {
  585. return nil
  586. }
  587. func (f *fakeLister) List(ctx context.Context, vaultID string, opts ...onepassword.ItemListFilter) ([]onepassword.ItemOverview, error) {
  588. return f.listAllResult, nil
  589. }
  590. func (f *fakeLister) Shares() onepassword.ItemsSharesAPI {
  591. return nil
  592. }
  593. func (f *fakeLister) Files() onepassword.ItemsFilesAPI {
  594. return f.fileLister
  595. }
  596. type fakeFileLister struct {
  597. readContent []byte
  598. }
  599. func (f *fakeFileLister) Attach(ctx context.Context, item onepassword.Item, fileParams onepassword.FileCreateParams) (onepassword.Item, error) {
  600. return onepassword.Item{}, nil
  601. }
  602. func (f *fakeFileLister) Read(ctx context.Context, vaultID, itemID string, attr onepassword.FileAttributes) ([]byte, error) {
  603. return f.readContent, nil
  604. }
  605. func (f *fakeFileLister) Delete(ctx context.Context, item onepassword.Item, sectionID, fieldID string) (onepassword.Item, error) {
  606. return onepassword.Item{}, nil
  607. }
  608. func (f *fakeFileLister) ReplaceDocument(ctx context.Context, item onepassword.Item, docParams onepassword.DocumentCreateParams) (onepassword.Item, error) {
  609. return onepassword.Item{}, nil
  610. }
  611. var _ onepassword.ItemsFilesAPI = (*fakeFileLister)(nil)
  612. type fakeClient struct {
  613. resolveResult string
  614. resolveError error
  615. resolveAll onepassword.ResolveAllResponse
  616. resolveAllError error
  617. listAllResult []onepassword.VaultOverview
  618. listAllError error
  619. }
  620. func (f *fakeClient) List(ctx context.Context) ([]onepassword.VaultOverview, error) {
  621. return f.listAllResult, f.listAllError
  622. }
  623. func (f *fakeClient) Resolve(ctx context.Context, secretReference string) (string, error) {
  624. return f.resolveResult, f.resolveError
  625. }
  626. func (f *fakeClient) ResolveAll(ctx context.Context, secretReferences []string) (onepassword.ResolveAllResponse, error) {
  627. return f.resolveAll, f.resolveAllError
  628. }
  629. var _ onepassword.SecretsAPI = &fakeClient{}
  630. var _ onepassword.VaultsAPI = &fakeClient{}
  631. var _ onepassword.ItemsAPI = &fakeLister{}