client_test.go 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862
  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 dvls
  14. import (
  15. "context"
  16. "errors"
  17. "fmt"
  18. "net/http"
  19. "testing"
  20. "github.com/Devolutions/go-dvls"
  21. "github.com/stretchr/testify/assert"
  22. corev1 "k8s.io/api/core/v1"
  23. apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
  24. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  25. )
  26. const (
  27. testVaultUUID = "00000000-0000-0000-0000-000000000001"
  28. testEntryUUID = "00000000-0000-0000-0000-000000000002"
  29. testEntryUUID3 = "00000000-0000-0000-0000-000000000003"
  30. testEntryUUID4 = "00000000-0000-0000-0000-000000000004"
  31. testEntryUUID5 = "00000000-0000-0000-0000-000000000005"
  32. testEntryName = "my-entry"
  33. testVaultName = "my-vault"
  34. testSecretName = "my-secret"
  35. testNonExistName = "some-name"
  36. )
  37. // --- Mock credential client ---
  38. type mockCredentialClient struct {
  39. entries map[string]dvls.Entry
  40. getErr error
  41. getEntriesErr error
  42. updateErr error
  43. deleteErr error
  44. lastUpdated dvls.Entry
  45. lastDeleted string
  46. }
  47. func newMockCredentialClient(entries map[string]dvls.Entry) *mockCredentialClient {
  48. if entries == nil {
  49. entries = make(map[string]dvls.Entry)
  50. }
  51. return &mockCredentialClient{entries: entries}
  52. }
  53. func (m *mockCredentialClient) GetByID(_ context.Context, _, entryID string) (dvls.Entry, error) {
  54. if m.getErr != nil {
  55. return dvls.Entry{}, m.getErr
  56. }
  57. if entry, ok := m.entries[entryID]; ok {
  58. return entry, nil
  59. }
  60. return dvls.Entry{}, &dvls.RequestError{Err: fmt.Errorf("unexpected status code %d", http.StatusNotFound), Url: entryID, StatusCode: http.StatusNotFound}
  61. }
  62. func (m *mockCredentialClient) GetEntries(_ context.Context, _ string, opts dvls.GetEntriesOptions) ([]dvls.Entry, error) {
  63. if m.getEntriesErr != nil {
  64. return nil, m.getEntriesErr
  65. }
  66. if opts.Name == nil {
  67. return nil, nil
  68. }
  69. var matches []dvls.Entry
  70. for _, e := range m.entries {
  71. if e.Name == *opts.Name {
  72. if opts.Path != nil && e.Path != *opts.Path {
  73. continue
  74. }
  75. matches = append(matches, e)
  76. }
  77. }
  78. return matches, nil
  79. }
  80. func (m *mockCredentialClient) Update(_ context.Context, entry dvls.Entry) (dvls.Entry, error) {
  81. if m.updateErr != nil {
  82. return entry, m.updateErr
  83. }
  84. m.entries[entry.Id] = entry
  85. m.lastUpdated = entry
  86. return entry, nil
  87. }
  88. func (m *mockCredentialClient) DeleteByID(_ context.Context, _, entryID string) error {
  89. if m.deleteErr != nil {
  90. return m.deleteErr
  91. }
  92. delete(m.entries, entryID)
  93. m.lastDeleted = entryID
  94. return nil
  95. }
  96. // --- Mock vault client ---
  97. type mockVaultClient struct {
  98. vaults map[string]dvls.Vault
  99. getErr error
  100. }
  101. func newMockVaultClient(vaults map[string]dvls.Vault) *mockVaultClient {
  102. if vaults == nil {
  103. vaults = make(map[string]dvls.Vault)
  104. }
  105. return &mockVaultClient{vaults: vaults}
  106. }
  107. func (m *mockVaultClient) GetByName(_ context.Context, name string) (dvls.Vault, error) {
  108. if m.getErr != nil {
  109. return dvls.Vault{}, m.getErr
  110. }
  111. if v, ok := m.vaults[name]; ok {
  112. return v, nil
  113. }
  114. return dvls.Vault{}, dvls.ErrVaultNotFound
  115. }
  116. // --- Test stubs ---
  117. type pushSecretDataStub struct {
  118. remoteKey string
  119. secretKey string
  120. property string
  121. }
  122. func (p pushSecretDataStub) GetMetadata() *apiextensionsv1.JSON { return nil }
  123. func (p pushSecretDataStub) GetSecretKey() string { return p.secretKey }
  124. func (p pushSecretDataStub) GetRemoteKey() string { return p.remoteKey }
  125. func (p pushSecretDataStub) GetProperty() string { return p.property }
  126. type pushSecretRemoteRefStub struct {
  127. remoteKey string
  128. property string
  129. }
  130. func (p pushSecretRemoteRefStub) GetRemoteKey() string { return p.remoteKey }
  131. func (p pushSecretRemoteRefStub) GetProperty() string { return p.property }
  132. // --- Helper to create a test client ---
  133. func newTestClient(entries map[string]dvls.Entry) (*Client, *mockCredentialClient) {
  134. mockCred := newMockCredentialClient(entries)
  135. c := NewClient(mockCred, testVaultUUID)
  136. return c, mockCred
  137. }
  138. // --- Tests: parseEntryRef ---
  139. func TestParseEntryRef(t *testing.T) {
  140. t.Run("name only", func(t *testing.T) {
  141. name, path := parseEntryRef(testEntryName)
  142. assert.Equal(t, testEntryName, name)
  143. assert.Equal(t, "", path)
  144. })
  145. t.Run("forward slash path", func(t *testing.T) {
  146. name, path := parseEntryRef("folder/my-entry")
  147. assert.Equal(t, testEntryName, name)
  148. assert.Equal(t, "folder", path)
  149. })
  150. t.Run("backslash path", func(t *testing.T) {
  151. name, path := parseEntryRef(`folder\my-entry`)
  152. assert.Equal(t, testEntryName, name)
  153. assert.Equal(t, "folder", path)
  154. })
  155. t.Run("nested forward slashes", func(t *testing.T) {
  156. name, path := parseEntryRef("go-dvls/folders/server/123")
  157. assert.Equal(t, "123", name)
  158. assert.Equal(t, `go-dvls\folders\server`, path)
  159. })
  160. t.Run("nested backslashes", func(t *testing.T) {
  161. name, path := parseEntryRef(`go-dvls\folders\server\123`)
  162. assert.Equal(t, "123", name)
  163. assert.Equal(t, `go-dvls\folders\server`, path)
  164. })
  165. t.Run("mixed separators", func(t *testing.T) {
  166. name, path := parseEntryRef(`go-dvls/folders\server/123`)
  167. assert.Equal(t, "123", name)
  168. assert.Equal(t, `go-dvls\folders\server`, path)
  169. })
  170. t.Run("trailing separator", func(t *testing.T) {
  171. name, path := parseEntryRef("folder/")
  172. assert.Equal(t, "", name)
  173. assert.Equal(t, "folder", path)
  174. })
  175. }
  176. // --- Tests: isUUID ---
  177. func TestIsUUID(t *testing.T) {
  178. t.Run("valid UUID", func(t *testing.T) {
  179. assert.True(t, isUUID("00000000-0000-0000-0000-000000000001"))
  180. })
  181. t.Run("valid UUID v4", func(t *testing.T) {
  182. assert.True(t, isUUID("550e8400-e29b-41d4-a716-446655440000"))
  183. })
  184. t.Run("name string", func(t *testing.T) {
  185. assert.False(t, isUUID("my-vault-name"))
  186. })
  187. t.Run("empty string", func(t *testing.T) {
  188. assert.False(t, isUUID(""))
  189. })
  190. t.Run("malformed UUID", func(t *testing.T) {
  191. assert.False(t, isUUID("00000000-0000-0000-000000000001"))
  192. })
  193. }
  194. // --- Tests: resolveVaultRef ---
  195. func TestResolveVaultRef(t *testing.T) {
  196. t.Run("UUID passthrough", func(t *testing.T) {
  197. id, err := resolveVaultRef(context.Background(), testVaultUUID, newMockVaultClient(nil))
  198. assert.NoError(t, err)
  199. assert.Equal(t, testVaultUUID, id)
  200. })
  201. t.Run("name resolved", func(t *testing.T) {
  202. mockVault := newMockVaultClient(map[string]dvls.Vault{
  203. testVaultName: {Id: testVaultUUID, Name: testVaultName},
  204. })
  205. id, err := resolveVaultRef(context.Background(), testVaultName, mockVault)
  206. assert.NoError(t, err)
  207. assert.Equal(t, testVaultUUID, id)
  208. })
  209. t.Run("name not found", func(t *testing.T) {
  210. _, err := resolveVaultRef(context.Background(), "nonexistent", newMockVaultClient(nil))
  211. assert.Error(t, err)
  212. assert.ErrorIs(t, err, dvls.ErrVaultNotFound)
  213. })
  214. }
  215. // --- Tests: resolveEntryRef ---
  216. func TestResolveEntryRef(t *testing.T) {
  217. entry := dvls.Entry{
  218. Id: testEntryUUID,
  219. Name: testEntryName,
  220. Type: dvls.EntryCredentialType,
  221. SubType: dvls.EntryCredentialSubTypeDefault,
  222. }
  223. t.Run("UUID passthrough", func(t *testing.T) {
  224. c, _ := newTestClient(nil)
  225. entryID, err := c.resolveEntryRef(context.Background(), testEntryUUID)
  226. assert.NoError(t, err)
  227. assert.Equal(t, testEntryUUID, entryID)
  228. })
  229. t.Run("name resolved", func(t *testing.T) {
  230. c, _ := newTestClient(map[string]dvls.Entry{testEntryUUID: entry})
  231. entryID, err := c.resolveEntryRef(context.Background(), testEntryName)
  232. assert.NoError(t, err)
  233. assert.Equal(t, testEntryUUID, entryID)
  234. })
  235. t.Run("name with path resolved", func(t *testing.T) {
  236. entryInPath := dvls.Entry{
  237. Id: testEntryUUID,
  238. Name: testEntryName,
  239. Path: `go-dvls\folders`,
  240. Type: dvls.EntryCredentialType,
  241. SubType: dvls.EntryCredentialSubTypeDefault,
  242. }
  243. c, _ := newTestClient(map[string]dvls.Entry{testEntryUUID: entryInPath})
  244. entryID, err := c.resolveEntryRef(context.Background(), "go-dvls/folders/my-entry")
  245. assert.NoError(t, err)
  246. assert.Equal(t, testEntryUUID, entryID)
  247. })
  248. t.Run("path filters out other paths", func(t *testing.T) {
  249. entryA := dvls.Entry{Id: testEntryUUID, Name: "db", Path: `prod`, Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeDefault}
  250. entryB := dvls.Entry{Id: testEntryUUID5, Name: "db", Path: `staging`, Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeDefault}
  251. c, _ := newTestClient(map[string]dvls.Entry{testEntryUUID: entryA, testEntryUUID5: entryB})
  252. entryID, err := c.resolveEntryRef(context.Background(), "prod/db")
  253. assert.NoError(t, err)
  254. assert.Equal(t, testEntryUUID, entryID)
  255. })
  256. t.Run("name not found", func(t *testing.T) {
  257. c, _ := newTestClient(nil)
  258. _, err := c.resolveEntryRef(context.Background(), "nonexistent")
  259. assert.Error(t, err)
  260. assert.ErrorIs(t, err, dvls.ErrEntryNotFound)
  261. })
  262. t.Run("multiple entries found", func(t *testing.T) {
  263. entry2 := dvls.Entry{Id: testEntryUUID3, Name: "dup", Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeDefault}
  264. entry3 := dvls.Entry{Id: testEntryUUID4, Name: "dup", Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeConnectionString}
  265. c, _ := newTestClient(map[string]dvls.Entry{
  266. testEntryUUID3: entry2,
  267. testEntryUUID4: entry3,
  268. })
  269. _, err := c.resolveEntryRef(context.Background(), "dup")
  270. assert.Error(t, err)
  271. assert.Contains(t, err.Error(), "found 2 credential entries")
  272. assert.Contains(t, err.Error(), testEntryUUID3)
  273. assert.Contains(t, err.Error(), testEntryUUID4)
  274. })
  275. t.Run("empty key", func(t *testing.T) {
  276. c, _ := newTestClient(nil)
  277. _, err := c.resolveEntryRef(context.Background(), "")
  278. assert.Error(t, err)
  279. assert.Contains(t, err.Error(), "cannot be empty")
  280. })
  281. t.Run("trailing separator produces empty name", func(t *testing.T) {
  282. c, _ := newTestClient(nil)
  283. _, err := c.resolveEntryRef(context.Background(), "folder/")
  284. assert.Error(t, err)
  285. assert.Contains(t, err.Error(), "entry name cannot be empty")
  286. })
  287. t.Run("GetEntries API error", func(t *testing.T) {
  288. c, mockCred := newTestClient(nil)
  289. mockCred.getEntriesErr = errors.New("network error")
  290. _, err := c.resolveEntryRef(context.Background(), testNonExistName)
  291. assert.Error(t, err)
  292. assert.Contains(t, err.Error(), "failed to resolve entry")
  293. assert.Contains(t, err.Error(), "network error")
  294. })
  295. t.Run("vault not found during name resolution", func(t *testing.T) {
  296. c, mockCred := newTestClient(nil)
  297. mockCred.getEntriesErr = dvls.ErrVaultNotFound
  298. _, err := c.resolveEntryRef(context.Background(), testNonExistName)
  299. assert.Error(t, err)
  300. assert.ErrorIs(t, err, dvls.ErrVaultNotFound)
  301. })
  302. t.Run("cache hit avoids second GetEntries call", func(t *testing.T) {
  303. entry := dvls.Entry{
  304. Id: testEntryUUID,
  305. Name: testEntryName,
  306. Type: dvls.EntryCredentialType,
  307. SubType: dvls.EntryCredentialSubTypeDefault,
  308. }
  309. c, mockCred := newTestClient(map[string]dvls.Entry{testEntryUUID: entry})
  310. // First call populates the cache.
  311. id1, err := c.resolveEntryRef(context.Background(), testEntryName)
  312. assert.NoError(t, err)
  313. assert.Equal(t, testEntryUUID, id1)
  314. // Remove entries from mock so only the cache can satisfy the lookup.
  315. mockCred.entries = map[string]dvls.Entry{}
  316. id2, err := c.resolveEntryRef(context.Background(), testEntryName)
  317. assert.NoError(t, err)
  318. assert.Equal(t, testEntryUUID, id2)
  319. })
  320. }
  321. // --- Tests: parseLegacyRef ---
  322. func TestParseLegacyRef(t *testing.T) {
  323. t.Run("valid format", func(t *testing.T) {
  324. vaultID, entryID, err := parseLegacyRef(testVaultUUID + "/" + testEntryUUID)
  325. assert.NoError(t, err)
  326. assert.Equal(t, testVaultUUID, vaultID)
  327. assert.Equal(t, testEntryUUID, entryID)
  328. })
  329. t.Run("no separator", func(t *testing.T) {
  330. _, _, err := parseLegacyRef(testEntryUUID)
  331. assert.Error(t, err)
  332. assert.Contains(t, err.Error(), "invalid key format")
  333. })
  334. t.Run("empty vault", func(t *testing.T) {
  335. _, _, err := parseLegacyRef("/" + testEntryUUID)
  336. assert.Error(t, err)
  337. assert.Contains(t, err.Error(), "vault ID cannot be empty")
  338. })
  339. t.Run("empty entry", func(t *testing.T) {
  340. _, _, err := parseLegacyRef(testVaultUUID + "/")
  341. assert.Error(t, err)
  342. assert.Contains(t, err.Error(), "entry ID cannot be empty")
  343. })
  344. t.Run("invalid vault UUID", func(t *testing.T) {
  345. _, _, err := parseLegacyRef("not-a-uuid/" + testEntryUUID)
  346. assert.Error(t, err)
  347. assert.Contains(t, err.Error(), "invalid vault UUID")
  348. })
  349. t.Run("invalid entry UUID", func(t *testing.T) {
  350. _, _, err := parseLegacyRef(testVaultUUID + "/not-a-uuid")
  351. assert.Error(t, err)
  352. assert.Contains(t, err.Error(), "invalid entry UUID")
  353. })
  354. }
  355. // --- Tests: resolveRef legacy mode ---
  356. func TestResolveRef_LegacyMode(t *testing.T) {
  357. entry := dvls.Entry{
  358. Id: testEntryUUID, Name: "test",
  359. Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeDefault,
  360. Data: &dvls.EntryCredentialDefaultData{Password: "pass"},
  361. }
  362. t.Run("legacy format when vaultID is empty", func(t *testing.T) {
  363. mockCred := newMockCredentialClient(map[string]dvls.Entry{testEntryUUID: entry})
  364. c := NewClient(mockCred, "")
  365. vaultID, entryID, err := c.resolveRef(context.Background(), testVaultUUID+"/"+testEntryUUID)
  366. assert.NoError(t, err)
  367. assert.Equal(t, testVaultUUID, vaultID)
  368. assert.Equal(t, testEntryUUID, entryID)
  369. })
  370. t.Run("new format with name when vaultID is set", func(t *testing.T) {
  371. c, _ := newTestClient(map[string]dvls.Entry{testEntryUUID: entry})
  372. vaultID, entryID, err := c.resolveRef(context.Background(), "test")
  373. assert.NoError(t, err)
  374. assert.Equal(t, testVaultUUID, vaultID)
  375. assert.Equal(t, testEntryUUID, entryID)
  376. })
  377. t.Run("new format when vaultID is set", func(t *testing.T) {
  378. c, _ := newTestClient(map[string]dvls.Entry{testEntryUUID: entry})
  379. vaultID, entryID, err := c.resolveRef(context.Background(), testEntryUUID)
  380. assert.NoError(t, err)
  381. assert.Equal(t, testVaultUUID, vaultID)
  382. assert.Equal(t, testEntryUUID, entryID)
  383. })
  384. }
  385. // --- Tests: Validate ---
  386. func TestClient_Validate(t *testing.T) {
  387. t.Run("nil cred client", func(t *testing.T) {
  388. c := &Client{cred: nil, vaultID: testVaultUUID}
  389. result, err := c.Validate()
  390. assert.Error(t, err)
  391. assert.Equal(t, esv1.ValidationResultError, result)
  392. })
  393. t.Run("empty vault ID is valid (legacy mode)", func(t *testing.T) {
  394. c := &Client{cred: newMockCredentialClient(nil), vaultID: ""}
  395. result, err := c.Validate()
  396. assert.NoError(t, err)
  397. assert.Equal(t, esv1.ValidationResultReady, result)
  398. })
  399. t.Run("initialized client", func(t *testing.T) {
  400. c := NewClient(newMockCredentialClient(nil), testVaultUUID)
  401. result, err := c.Validate()
  402. assert.NoError(t, err)
  403. assert.Equal(t, esv1.ValidationResultReady, result)
  404. })
  405. }
  406. func TestNewClient(t *testing.T) {
  407. c := NewClient(nil, "")
  408. assert.NotNil(t, c)
  409. assert.Nil(t, c.cred)
  410. assert.Empty(t, c.vaultID)
  411. }
  412. // --- Tests: entryToSecretMap ---
  413. func TestEntryToSecretMap(t *testing.T) {
  414. t.Run("Default credential type", func(t *testing.T) {
  415. entry := dvls.Entry{
  416. Id: "entry-id-123", Name: "test-entry",
  417. Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeDefault,
  418. Data: &dvls.EntryCredentialDefaultData{Username: "testuser", Password: "testpass", Domain: "testdomain"},
  419. }
  420. secretMap, err := entryToSecretMap(entry)
  421. assert.NoError(t, err)
  422. assert.Equal(t, "testuser", string(secretMap["username"]))
  423. assert.Equal(t, "testpass", string(secretMap["password"]))
  424. assert.Equal(t, "testdomain", string(secretMap["domain"]))
  425. })
  426. t.Run("AccessCode credential type", func(t *testing.T) {
  427. entry := dvls.Entry{
  428. Id: "entry-id-456", Name: "access-code-entry",
  429. Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeAccessCode,
  430. Data: &dvls.EntryCredentialAccessCodeData{Password: "access-code-123"},
  431. }
  432. secretMap, err := entryToSecretMap(entry)
  433. assert.NoError(t, err)
  434. assert.Equal(t, "access-code-123", string(secretMap["password"]))
  435. })
  436. t.Run("ApiKey credential type", func(t *testing.T) {
  437. entry := dvls.Entry{
  438. Id: "entry-id-789", Name: "api-key-entry",
  439. Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeApiKey,
  440. Data: &dvls.EntryCredentialApiKeyData{ApiId: "api-id-123", ApiKey: "api-key-secret", TenantId: "tenant-123"},
  441. }
  442. secretMap, err := entryToSecretMap(entry)
  443. assert.NoError(t, err)
  444. assert.Equal(t, "api-id-123", string(secretMap["api-id"]))
  445. assert.Equal(t, "api-key-secret", string(secretMap["api-key"]))
  446. })
  447. t.Run("AzureServicePrincipal credential type", func(t *testing.T) {
  448. entry := dvls.Entry{
  449. Id: "entry-id-azure", Name: "azure-sp-entry",
  450. Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeAzureServicePrincipal,
  451. Data: &dvls.EntryCredentialAzureServicePrincipalData{ClientId: "client-id-123", ClientSecret: "client-secret-456", TenantId: "tenant-id-789"},
  452. }
  453. secretMap, err := entryToSecretMap(entry)
  454. assert.NoError(t, err)
  455. assert.Equal(t, "client-id-123", string(secretMap["client-id"]))
  456. assert.Equal(t, "client-secret-456", string(secretMap["client-secret"]))
  457. assert.Equal(t, "tenant-id-789", string(secretMap["tenant-id"]))
  458. })
  459. t.Run("ConnectionString credential type", func(t *testing.T) {
  460. entry := dvls.Entry{
  461. Id: "entry-id-conn", Name: "connection-string-entry",
  462. Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeConnectionString,
  463. Data: &dvls.EntryCredentialConnectionStringData{ConnectionString: "Server=localhost;Database=mydb"},
  464. }
  465. secretMap, err := entryToSecretMap(entry)
  466. assert.NoError(t, err)
  467. assert.Equal(t, "Server=localhost;Database=mydb", string(secretMap["connection-string"]))
  468. })
  469. t.Run("PrivateKey credential type", func(t *testing.T) {
  470. entry := dvls.Entry{
  471. Id: "entry-id-pk",
  472. Name: "private-key-entry",
  473. Type: dvls.EntryCredentialType,
  474. SubType: dvls.EntryCredentialSubTypePrivateKey,
  475. Data: &dvls.EntryCredentialPrivateKeyData{
  476. Username: "ssh-user",
  477. Password: "key-password",
  478. PrivateKey: "-----BEGIN RSA PRIVATE KEY-----",
  479. PublicKey: "ssh-rsa AAAA",
  480. Passphrase: "my-passphrase",
  481. },
  482. }
  483. secretMap, err := entryToSecretMap(entry)
  484. assert.NoError(t, err)
  485. assert.Equal(t, "ssh-user", string(secretMap["username"]))
  486. assert.Equal(t, "key-password", string(secretMap["password"]))
  487. assert.Equal(t, "-----BEGIN RSA PRIVATE KEY-----", string(secretMap["private-key"]))
  488. assert.Equal(t, "ssh-rsa AAAA", string(secretMap["public-key"]))
  489. assert.Equal(t, "my-passphrase", string(secretMap["passphrase"]))
  490. })
  491. t.Run("Default credential with partial data", func(t *testing.T) {
  492. entry := dvls.Entry{
  493. Id: "entry-id-partial", Name: "partial-entry",
  494. Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeDefault,
  495. Data: &dvls.EntryCredentialDefaultData{Username: "onlyuser"},
  496. }
  497. secretMap, err := entryToSecretMap(entry)
  498. assert.NoError(t, err)
  499. assert.Equal(t, "onlyuser", string(secretMap["username"]))
  500. _, hasPassword := secretMap["password"]
  501. _, hasDomain := secretMap["domain"]
  502. assert.False(t, hasPassword)
  503. assert.False(t, hasDomain)
  504. })
  505. t.Run("Unsupported credential type", func(t *testing.T) {
  506. entry := dvls.Entry{Id: "x", Name: "x", Type: dvls.EntryCredentialType, SubType: "UnknownType"}
  507. _, err := entryToSecretMap(entry)
  508. assert.Error(t, err)
  509. })
  510. }
  511. // --- Tests: GetSecret ---
  512. func TestClient_GetSecret_NotFound(t *testing.T) {
  513. c, _ := newTestClient(nil)
  514. _, err := c.GetSecret(context.Background(), esv1.ExternalSecretDataRemoteRef{Key: testEntryUUID})
  515. assert.ErrorIs(t, err, esv1.NoSecretErr)
  516. }
  517. func TestClient_GetSecret_Success(t *testing.T) {
  518. entry := dvls.Entry{
  519. Id: testEntryUUID, Name: "test-entry",
  520. Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeDefault,
  521. Data: &dvls.EntryCredentialDefaultData{Password: "super-secret"},
  522. }
  523. c, _ := newTestClient(map[string]dvls.Entry{testEntryUUID: entry})
  524. val, err := c.GetSecret(context.Background(), esv1.ExternalSecretDataRemoteRef{Key: testEntryUUID, Property: "password"})
  525. assert.NoError(t, err)
  526. assert.Equal(t, "super-secret", string(val))
  527. }
  528. func TestClient_GetSecret_ByName(t *testing.T) {
  529. entry := dvls.Entry{
  530. Id: testEntryUUID, Name: testSecretName,
  531. Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeDefault,
  532. Data: &dvls.EntryCredentialDefaultData{Password: "name-resolved"},
  533. }
  534. c, _ := newTestClient(map[string]dvls.Entry{testEntryUUID: entry})
  535. val, err := c.GetSecret(context.Background(), esv1.ExternalSecretDataRemoteRef{Key: testSecretName, Property: "password"})
  536. assert.NoError(t, err)
  537. assert.Equal(t, "name-resolved", string(val))
  538. }
  539. func TestClient_GetSecret_ByNameNotFound(t *testing.T) {
  540. c, _ := newTestClient(nil)
  541. _, err := c.GetSecret(context.Background(), esv1.ExternalSecretDataRemoteRef{Key: "nonexistent"})
  542. assert.ErrorIs(t, err, esv1.NoSecretErr)
  543. }
  544. func TestClient_GetSecret_WithPath(t *testing.T) {
  545. entry := dvls.Entry{
  546. Id: testEntryUUID, Name: "db", Path: `prod\databases`,
  547. Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeDefault,
  548. Data: &dvls.EntryCredentialDefaultData{Password: "prod-pass"},
  549. }
  550. c, _ := newTestClient(map[string]dvls.Entry{testEntryUUID: entry})
  551. val, err := c.GetSecret(context.Background(), esv1.ExternalSecretDataRemoteRef{Key: "prod/databases/db", Property: "password"})
  552. assert.NoError(t, err)
  553. assert.Equal(t, "prod-pass", string(val))
  554. }
  555. func TestClient_GetSecret_VaultNotFound(t *testing.T) {
  556. c, mockCred := newTestClient(nil)
  557. mockCred.getErr = dvls.ErrVaultNotFound
  558. // UUID key bypasses name resolution, so GetByID is called directly.
  559. _, err := c.GetSecret(context.Background(), esv1.ExternalSecretDataRemoteRef{Key: testEntryUUID})
  560. assert.Error(t, err)
  561. assert.ErrorIs(t, err, dvls.ErrVaultNotFound)
  562. }
  563. func TestClient_GetSecret_VaultNotFoundDuringNameResolution(t *testing.T) {
  564. c, mockCred := newTestClient(nil)
  565. mockCred.getEntriesErr = dvls.ErrVaultNotFound
  566. _, err := c.GetSecret(context.Background(), esv1.ExternalSecretDataRemoteRef{Key: testNonExistName})
  567. assert.Error(t, err)
  568. assert.ErrorIs(t, err, dvls.ErrVaultNotFound)
  569. }
  570. // --- Tests: GetSecretMap ---
  571. func TestClient_GetSecretMap_ByName(t *testing.T) {
  572. entry := dvls.Entry{
  573. Id: testEntryUUID, Name: testSecretName,
  574. Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeDefault,
  575. Data: &dvls.EntryCredentialDefaultData{Username: "user", Password: "pass"},
  576. }
  577. c, _ := newTestClient(map[string]dvls.Entry{testEntryUUID: entry})
  578. secretMap, err := c.GetSecretMap(context.Background(), esv1.ExternalSecretDataRemoteRef{Key: testSecretName})
  579. assert.NoError(t, err)
  580. assert.Equal(t, "user", string(secretMap["username"]))
  581. assert.Equal(t, "pass", string(secretMap["password"]))
  582. }
  583. func TestClient_GetSecretMap_NotFoundByUUID(t *testing.T) {
  584. c, _ := newTestClient(nil)
  585. _, err := c.GetSecretMap(context.Background(), esv1.ExternalSecretDataRemoteRef{Key: testEntryUUID})
  586. assert.ErrorIs(t, err, esv1.NoSecretErr)
  587. }
  588. func TestClient_GetSecretMap_NotFoundByName(t *testing.T) {
  589. c, _ := newTestClient(nil)
  590. _, err := c.GetSecretMap(context.Background(), esv1.ExternalSecretDataRemoteRef{Key: "nonexistent"})
  591. assert.ErrorIs(t, err, esv1.NoSecretErr)
  592. }
  593. // --- Tests: SecretExists ---
  594. func TestClient_SecretExists(t *testing.T) {
  595. c, mockCred := newTestClient(nil)
  596. exists, err := c.SecretExists(context.Background(), pushSecretRemoteRefStub{remoteKey: testEntryUUID})
  597. assert.NoError(t, err)
  598. assert.False(t, exists)
  599. mockCred.entries[testEntryUUID] = dvls.Entry{Id: testEntryUUID, Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeDefault}
  600. exists, err = c.SecretExists(context.Background(), pushSecretRemoteRefStub{remoteKey: testEntryUUID})
  601. assert.NoError(t, err)
  602. assert.True(t, exists)
  603. mockCred.getErr = errors.New("boom")
  604. _, err = c.SecretExists(context.Background(), pushSecretRemoteRefStub{remoteKey: testEntryUUID})
  605. assert.Error(t, err)
  606. }
  607. func TestClient_SecretExists_ByName(t *testing.T) {
  608. entry := dvls.Entry{Id: testEntryUUID, Name: testEntryName, Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeDefault}
  609. c, _ := newTestClient(map[string]dvls.Entry{testEntryUUID: entry})
  610. exists, err := c.SecretExists(context.Background(), pushSecretRemoteRefStub{remoteKey: testEntryName})
  611. assert.NoError(t, err)
  612. assert.True(t, exists)
  613. exists, err = c.SecretExists(context.Background(), pushSecretRemoteRefStub{remoteKey: "nonexistent"})
  614. assert.NoError(t, err)
  615. assert.False(t, exists)
  616. }
  617. // --- Tests: DeleteSecret ---
  618. func TestClient_DeleteSecret(t *testing.T) {
  619. c, mockCred := newTestClient(map[string]dvls.Entry{testEntryUUID: {Id: testEntryUUID, Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeAccessCode}})
  620. err := c.DeleteSecret(context.Background(), pushSecretRemoteRefStub{remoteKey: testEntryUUID})
  621. assert.NoError(t, err)
  622. assert.Equal(t, testEntryUUID, mockCred.lastDeleted)
  623. }
  624. func TestClient_DeleteSecret_ByName(t *testing.T) {
  625. entry := dvls.Entry{Id: testEntryUUID, Name: testEntryName, Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeDefault}
  626. c, mockCred := newTestClient(map[string]dvls.Entry{testEntryUUID: entry})
  627. err := c.DeleteSecret(context.Background(), pushSecretRemoteRefStub{remoteKey: testEntryName})
  628. assert.NoError(t, err)
  629. assert.Equal(t, testEntryUUID, mockCred.lastDeleted)
  630. }
  631. func TestClient_DeleteSecret_ByNameNotFound(t *testing.T) {
  632. c, _ := newTestClient(nil)
  633. err := c.DeleteSecret(context.Background(), pushSecretRemoteRefStub{remoteKey: "nonexistent"})
  634. assert.NoError(t, err)
  635. }
  636. // --- Tests: PushSecret ---
  637. func TestClient_PushSecret_UpdateDefault(t *testing.T) {
  638. c, mockCred := newTestClient(map[string]dvls.Entry{
  639. testEntryUUID: {Id: testEntryUUID, Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeDefault},
  640. })
  641. secret := &corev1.Secret{Data: map[string][]byte{"password": []byte("new-value")}}
  642. data := pushSecretDataStub{remoteKey: testEntryUUID, secretKey: "password"}
  643. err := c.PushSecret(context.Background(), secret, data)
  644. assert.NoError(t, err)
  645. credData, ok := mockCred.entries[testEntryUUID].Data.(*dvls.EntryCredentialDefaultData)
  646. assert.True(t, ok)
  647. assert.Equal(t, "new-value", credData.Password)
  648. }
  649. func TestClient_PushSecret_ByName(t *testing.T) {
  650. entry := dvls.Entry{Id: testEntryUUID, Name: testEntryName, Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeDefault}
  651. c, mockCred := newTestClient(map[string]dvls.Entry{testEntryUUID: entry})
  652. secret := &corev1.Secret{Data: map[string][]byte{"password": []byte("pushed-via-name")}}
  653. data := pushSecretDataStub{remoteKey: testEntryName, secretKey: "password"}
  654. err := c.PushSecret(context.Background(), secret, data)
  655. assert.NoError(t, err)
  656. credData, ok := mockCred.entries[testEntryUUID].Data.(*dvls.EntryCredentialDefaultData)
  657. assert.True(t, ok)
  658. assert.Equal(t, "pushed-via-name", credData.Password)
  659. }
  660. func TestClient_PushSecret_UpdateAccessCode(t *testing.T) {
  661. c, mockCred := newTestClient(map[string]dvls.Entry{
  662. testEntryUUID: {Id: testEntryUUID, Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeAccessCode},
  663. })
  664. secret := &corev1.Secret{Data: map[string][]byte{"code": []byte("code-value")}}
  665. data := pushSecretDataStub{remoteKey: testEntryUUID, secretKey: "code"}
  666. err := c.PushSecret(context.Background(), secret, data)
  667. assert.NoError(t, err)
  668. credData, ok := mockCred.entries[testEntryUUID].Data.(*dvls.EntryCredentialAccessCodeData)
  669. assert.True(t, ok)
  670. assert.Equal(t, "code-value", credData.Password)
  671. }
  672. func TestClient_PushSecret_UnsupportedSubtype(t *testing.T) {
  673. c, _ := newTestClient(map[string]dvls.Entry{
  674. testEntryUUID: {Id: testEntryUUID, Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeApiKey},
  675. })
  676. secret := &corev1.Secret{Data: map[string][]byte{"password": []byte("pw")}}
  677. data := pushSecretDataStub{remoteKey: testEntryUUID, secretKey: "password"}
  678. err := c.PushSecret(context.Background(), secret, data)
  679. assert.Error(t, err)
  680. assert.Contains(t, err.Error(), "cannot set secret for credential subtype")
  681. }
  682. func TestClient_PushSecret_NotFound(t *testing.T) {
  683. c, _ := newTestClient(nil)
  684. secret := &corev1.Secret{Data: map[string][]byte{"password": []byte("pw")}}
  685. data := pushSecretDataStub{remoteKey: "00000000-0000-0000-0000-000000000099", secretKey: "password"}
  686. err := c.PushSecret(context.Background(), secret, data)
  687. assert.Error(t, err)
  688. assert.Contains(t, err.Error(), "not found")
  689. }
  690. func TestClient_PushSecret_VaultNotFoundDuringNameResolution(t *testing.T) {
  691. c, mockCred := newTestClient(nil)
  692. mockCred.getEntriesErr = dvls.ErrVaultNotFound
  693. secret := &corev1.Secret{Data: map[string][]byte{"password": []byte("pw")}}
  694. data := pushSecretDataStub{remoteKey: testNonExistName, secretKey: "password"}
  695. err := c.PushSecret(context.Background(), secret, data)
  696. assert.Error(t, err)
  697. assert.ErrorIs(t, err, dvls.ErrVaultNotFound)
  698. }
  699. func TestClient_PushSecret_ByNameNotFound(t *testing.T) {
  700. c, _ := newTestClient(nil)
  701. secret := &corev1.Secret{Data: map[string][]byte{"password": []byte("pw")}}
  702. data := pushSecretDataStub{remoteKey: "nonexistent-entry", secretKey: "password"}
  703. err := c.PushSecret(context.Background(), secret, data)
  704. assert.Error(t, err)
  705. assert.Contains(t, err.Error(), "entry must exist before pushing secrets")
  706. }
  707. // --- Tests: isNotFoundError ---
  708. func TestIsNotFoundError(t *testing.T) {
  709. assert.False(t, isNotFoundError(nil))
  710. assert.True(t, isNotFoundError(&dvls.RequestError{Err: fmt.Errorf("not found"), StatusCode: http.StatusNotFound}))
  711. assert.True(t, isNotFoundError(dvls.ErrEntryNotFound))
  712. assert.True(t, isNotFoundError(fmt.Errorf("wrapped: %w", dvls.ErrEntryNotFound)))
  713. assert.False(t, isNotFoundError(dvls.ErrMultipleEntriesFound))
  714. assert.False(t, isNotFoundError(errors.New("some other error")))
  715. }
  716. func TestIsVaultNotFoundError(t *testing.T) {
  717. assert.False(t, isVaultNotFoundError(nil))
  718. assert.True(t, isVaultNotFoundError(dvls.ErrVaultNotFound))
  719. assert.True(t, isVaultNotFoundError(fmt.Errorf("wrapped: %w", dvls.ErrVaultNotFound)))
  720. assert.False(t, isVaultNotFoundError(errors.New("some other error")))
  721. }