client_test.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  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 infisical
  14. import (
  15. "context"
  16. "io"
  17. "net/http"
  18. "net/http/httptest"
  19. "net/url"
  20. "testing"
  21. infisical "github.com/infisical/go-sdk"
  22. "github.com/stretchr/testify/assert"
  23. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  24. )
  25. func TestGetSecretAddress(t *testing.T) {
  26. t.Run("when the key is not addressing a path and uses the default path", func(t *testing.T) {
  27. path, key := getSecretAddress("/", "foo")
  28. assert.Equal(t, "/", path)
  29. assert.Equal(t, "foo", key)
  30. path, key = getSecretAddress("/foo", "bar")
  31. assert.Equal(t, "/foo", path)
  32. assert.Equal(t, "bar", key)
  33. })
  34. t.Run("when the key is addressing a path", func(t *testing.T) {
  35. path, key := getSecretAddress("/", "/foo/bar")
  36. assert.Equal(t, path, "/foo")
  37. assert.Equal(t, key, "bar")
  38. })
  39. t.Run("when the key is addressing a path and ignores the default path", func(t *testing.T) {
  40. path, key := getSecretAddress("/foo", "/bar/baz")
  41. assert.Equal(t, "/bar", path)
  42. assert.Equal(t, "baz", key)
  43. })
  44. t.Run("works with a nested directory", func(t *testing.T) {
  45. path, key := getSecretAddress("/", "/foo/bar/baz")
  46. assert.Equal(t, "/foo/bar", path)
  47. assert.Equal(t, "baz", key, "baz")
  48. })
  49. t.Run("relative key joins onto the default path", func(t *testing.T) {
  50. path, key := getSecretAddress("/secrets/mysql-core", "azure/admin-users")
  51. assert.Equal(t, "/secrets/mysql-core/azure", path)
  52. assert.Equal(t, "admin-users", key)
  53. })
  54. t.Run("relative key with default root path", func(t *testing.T) {
  55. path, key := getSecretAddress("/", "azure/admin-users")
  56. assert.Equal(t, "/azure", path)
  57. assert.Equal(t, "admin-users", key)
  58. })
  59. t.Run("relative key with nested folders", func(t *testing.T) {
  60. path, key := getSecretAddress("/scope", "a/b/c/name")
  61. assert.Equal(t, "/scope/a/b/c", path)
  62. assert.Equal(t, "name", key)
  63. })
  64. }
  65. // listStub answers the Infisical list endpoint with body and keeps the query it
  66. // was called with, so tests can assert on the request instead of the response.
  67. type listStub struct {
  68. query url.Values
  69. calls int
  70. }
  71. func (s *listStub) provider(t *testing.T, scope *ClientScope, body string, status int) *Provider {
  72. t.Helper()
  73. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  74. s.query = r.URL.Query()
  75. s.calls++
  76. w.Header().Set("Content-Type", "application/json")
  77. w.WriteHeader(status)
  78. _, _ = io.WriteString(w, body)
  79. }))
  80. t.Cleanup(srv.Close)
  81. return &Provider{
  82. sdkClient: infisical.NewInfisicalClient(context.Background(), infisical.Config{SiteUrl: srv.URL}),
  83. apiScope: scope,
  84. }
  85. }
  86. func (s *listStub) get(key string) string {
  87. return s.query.Get(key)
  88. }
  89. func TestGetAllSecretsListRequest(t *testing.T) {
  90. const emptyList = `{"secrets":[],"imports":[]}`
  91. storeScope := func(path string, recursive bool) *ClientScope {
  92. return &ClientScope{
  93. EnvironmentSlug: "dev",
  94. ProjectSlug: "proj",
  95. SecretPath: path,
  96. Recursive: recursive,
  97. ExpandSecretReferences: true,
  98. }
  99. }
  100. tests := []struct {
  101. name string
  102. scope *ClientScope
  103. find esv1.ExternalSecretFind
  104. wantPath string
  105. wantRecursive string
  106. }{
  107. {
  108. name: "find path becomes the request root",
  109. scope: storeScope("/store-default", false),
  110. find: esv1.ExternalSecretFind{Path: new("/app")},
  111. wantPath: "/app",
  112. wantRecursive: "false",
  113. },
  114. {
  115. name: "without a find path the store scope is left alone",
  116. scope: storeScope("/store-default", false),
  117. find: esv1.ExternalSecretFind{},
  118. wantPath: "/store-default",
  119. wantRecursive: "false",
  120. },
  121. {
  122. name: "a recursive store stays recursive",
  123. scope: storeScope("/store-default", true),
  124. find: esv1.ExternalSecretFind{},
  125. wantPath: "/store-default",
  126. wantRecursive: "true",
  127. },
  128. {
  129. name: "a recursive store stays recursive under a find path",
  130. scope: storeScope("/store-default", true),
  131. find: esv1.ExternalSecretFind{Path: new("/app")},
  132. wantPath: "/app",
  133. wantRecursive: "true",
  134. },
  135. {
  136. name: "the project root is a valid find path",
  137. scope: storeScope("/store-default", false),
  138. find: esv1.ExternalSecretFind{Path: new("/")},
  139. wantPath: "/",
  140. wantRecursive: "false",
  141. },
  142. {
  143. // The SDK turns an empty SecretPath into "/", so an empty find path
  144. // would widen the request to the whole project if it were passed on.
  145. name: "an empty find path keeps the store scope",
  146. scope: storeScope("/store-default", false),
  147. find: esv1.ExternalSecretFind{Path: new("")},
  148. wantPath: "/store-default",
  149. wantRecursive: "false",
  150. },
  151. }
  152. for _, tt := range tests {
  153. t.Run(tt.name, func(t *testing.T) {
  154. stub := &listStub{}
  155. p := stub.provider(t, tt.scope, emptyList, http.StatusOK)
  156. _, err := p.GetAllSecrets(context.Background(), tt.find)
  157. assert.NoError(t, err)
  158. assert.Equal(t, tt.wantPath, stub.get("secretPath"))
  159. assert.Equal(t, tt.wantRecursive, stub.get("recursive"))
  160. assert.Equal(t, "dev", stub.get("environment"))
  161. assert.Equal(t, "proj", stub.get("workspaceSlug"))
  162. assert.Equal(t, "true", stub.get("include_imports"))
  163. assert.Equal(t, "true", stub.get("expandSecretReferences"))
  164. })
  165. }
  166. }
  167. func TestGetAllSecretsResults(t *testing.T) {
  168. scope := &ClientScope{EnvironmentSlug: "dev", ProjectSlug: "proj", SecretPath: "/store-default"}
  169. t.Run("the path scopes the request and the name filters what comes back", func(t *testing.T) {
  170. body := `{"secrets":[
  171. {"secretKey":"DB_HOST","secretValue":"db","secretPath":"/app"},
  172. {"secretKey":"API_KEY","secretValue":"key","secretPath":"/app/nested"}
  173. ],"imports":[]}`
  174. stub := &listStub{}
  175. p := stub.provider(t, scope, body, http.StatusOK)
  176. got, err := p.GetAllSecrets(context.Background(), esv1.ExternalSecretFind{
  177. Path: new("/app"),
  178. Name: &esv1.FindName{RegExp: "^DB_"},
  179. })
  180. assert.NoError(t, err)
  181. assert.Equal(t, "/app", stub.get("secretPath"))
  182. assert.Equal(t, map[string][]byte{"DB_HOST": []byte("db")}, got)
  183. })
  184. t.Run("imported secrets are returned even without a path of their own", func(t *testing.T) {
  185. body := `{"secrets":[
  186. {"secretKey":"LOCAL","secretValue":"local","secretPath":"/app"}
  187. ],"imports":[
  188. {"secretPath":"/shared","environment":"dev","folderId":"f1","secrets":[
  189. {"secretKey":"IMPORTED","secretValue":"imported"}
  190. ]}
  191. ]}`
  192. stub := &listStub{}
  193. p := stub.provider(t, scope, body, http.StatusOK)
  194. got, err := p.GetAllSecrets(context.Background(), esv1.ExternalSecretFind{Path: new("/app")})
  195. assert.NoError(t, err)
  196. assert.Equal(t, map[string][]byte{
  197. "LOCAL": []byte("local"),
  198. "IMPORTED": []byte("imported"),
  199. }, got)
  200. })
  201. t.Run("an api error is not swallowed", func(t *testing.T) {
  202. stub := &listStub{}
  203. p := stub.provider(t, scope, `{"message":"forbidden"}`, http.StatusForbidden)
  204. _, err := p.GetAllSecrets(context.Background(), esv1.ExternalSecretFind{Path: new("/app")})
  205. assert.Error(t, err)
  206. })
  207. t.Run("an invalid name regexp fails before anything is selected", func(t *testing.T) {
  208. stub := &listStub{}
  209. p := stub.provider(t, scope, `{"secrets":[],"imports":[]}`, http.StatusOK)
  210. _, err := p.GetAllSecrets(context.Background(), esv1.ExternalSecretFind{
  211. Name: &esv1.FindName{RegExp: "("},
  212. })
  213. assert.Error(t, err)
  214. })
  215. t.Run("finding by tags is still unsupported", func(t *testing.T) {
  216. stub := &listStub{}
  217. p := stub.provider(t, scope, `{"secrets":[],"imports":[]}`, http.StatusOK)
  218. _, err := p.GetAllSecrets(context.Background(), esv1.ExternalSecretFind{Tags: map[string]string{"env": "dev"}})
  219. assert.ErrorIs(t, err, errTagsNotImplemented)
  220. assert.Zero(t, stub.calls)
  221. })
  222. }