api_test.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. /*
  2. Licensed under the Apache License, Version 2.0 (the "License");
  3. you may not use this file except in compliance with the License.
  4. You may obtain a copy of the License at
  5. http://www.apache.org/licenses/LICENSE-2.0
  6. Unless required by applicable law or agreed to in writing, software
  7. distributed under the License is distributed on an "AS IS" BASIS,
  8. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. See the License for the specific language governing permissions and
  10. limitations under the License.
  11. */
  12. package api
  13. import (
  14. "errors"
  15. "reflect"
  16. "testing"
  17. "github.com/stretchr/testify/assert"
  18. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  19. )
  20. const (
  21. fakeClientID = "client-id"
  22. fakeClientSecret = "client-secret"
  23. fakeToken = "token"
  24. fakeProjectSlug = "first-project"
  25. fakeEnvironmentSlug = "dev"
  26. )
  27. func TestAPIClientDo(t *testing.T) {
  28. apiURL := "foo"
  29. httpMethod := "bar"
  30. testCases := []struct {
  31. Name string
  32. MockStatusCode int
  33. MockResponse any
  34. ExpectedResponse any
  35. ExpectedError error
  36. }{
  37. {
  38. Name: "Success",
  39. MockStatusCode: 200,
  40. MockResponse: MachineIdentityDetailsResponse{
  41. AccessToken: "foobar",
  42. },
  43. ExpectedResponse: MachineIdentityDetailsResponse{
  44. AccessToken: "foobar",
  45. },
  46. ExpectedError: nil,
  47. },
  48. {
  49. Name: "Error when response cannot be unmarshalled",
  50. MockStatusCode: 500,
  51. MockResponse: []byte("not-json"),
  52. ExpectedError: errors.New("API error (500), could not unmarshal error response: json: cannot unmarshal string into Go value of type api.InfisicalAPIErrorResponse"),
  53. },
  54. {
  55. Name: "Error when non-Infisical error response received",
  56. MockStatusCode: 500,
  57. MockResponse: map[string]string{"foo": "bar"},
  58. ExpectedError: errors.New("API error (500): {\"foo\":\"bar\"}"),
  59. },
  60. {
  61. Name: "Do: Error when non-200 response received",
  62. MockStatusCode: 401,
  63. MockResponse: InfisicalAPIErrorResponse{
  64. StatusCode: 401,
  65. Error: "Unauthorized",
  66. },
  67. ExpectedError: &InfisicalAPIError{StatusCode: 401, Err: "Unauthorized", Message: ""},
  68. },
  69. {
  70. Name: "Error when arbitrary details are returned",
  71. MockStatusCode: 401,
  72. MockResponse: InfisicalAPIErrorResponse{
  73. StatusCode: 401,
  74. Error: "Unauthorized",
  75. Details: map[string]string{"foo": "details"},
  76. },
  77. ExpectedError: &InfisicalAPIError{StatusCode: 401, Err: "Unauthorized", Message: "", Details: map[string]string{"foo": "details"}},
  78. },
  79. }
  80. for _, tc := range testCases {
  81. t.Run(tc.Name, func(t *testing.T) {
  82. apiClient, closeFunc := NewMockClient(tc.MockStatusCode, tc.MockResponse)
  83. defer closeFunc()
  84. // Automatically pluck out the expected response type using reflection to create a new empty value for unmarshalling.
  85. var actualResponse any
  86. if tc.ExpectedResponse != nil {
  87. actualResponse = reflect.New(reflect.TypeOf(tc.ExpectedResponse)).Interface()
  88. }
  89. err := apiClient.do(apiURL, httpMethod, nil, nil, actualResponse)
  90. if tc.ExpectedError != nil {
  91. assert.Error(t, err)
  92. assert.Equal(t, tc.ExpectedError.Error(), err.Error())
  93. } else {
  94. assert.NoError(t, err)
  95. assert.Equal(t, tc.ExpectedResponse, reflect.ValueOf(actualResponse).Elem().Interface())
  96. }
  97. })
  98. }
  99. }
  100. // TestAPIClientDoInvalidResponse tests the case where the response is a 200 but does not unmarshal
  101. // correctly.
  102. func TestAPIClientDoInvalidResponse(t *testing.T) {
  103. apiClient, closeFunc := NewMockClient(200, []byte("not-json"))
  104. defer closeFunc()
  105. err := apiClient.do("foo", "bar", nil, nil, nil)
  106. assert.ErrorIs(t, err, errJSONUnmarshal)
  107. }
  108. func TestSetTokenViaMachineIdentity(t *testing.T) {
  109. t.Run("Success", func(t *testing.T) {
  110. apiClient, closeFunc := NewMockClient(200, MachineIdentityDetailsResponse{
  111. AccessToken: "foobar",
  112. })
  113. defer closeFunc()
  114. err := apiClient.SetTokenViaMachineIdentity(fakeClientID, fakeClientSecret)
  115. assert.NoError(t, err)
  116. assert.Equal(t, apiClient.token, "foobar")
  117. })
  118. t.Run("SetTokenViaMachineIdentity: Error when non-200 response received", func(t *testing.T) {
  119. apiClient, closeFunc := NewMockClient(401, InfisicalAPIErrorResponse{
  120. StatusCode: 401,
  121. Error: "Unauthorized",
  122. })
  123. defer closeFunc()
  124. err := apiClient.SetTokenViaMachineIdentity(fakeClientID, fakeClientSecret)
  125. assert.Error(t, err)
  126. var apiErr *InfisicalAPIError
  127. assert.True(t, errors.As(err, &apiErr))
  128. assert.Equal(t, 401, apiErr.StatusCode)
  129. assert.Equal(t, "Unauthorized", apiErr.Err)
  130. })
  131. t.Run("Error when token already set", func(t *testing.T) {
  132. apiClient, closeFunc := NewMockClient(401, nil)
  133. defer closeFunc()
  134. apiClient.token = fakeToken
  135. err := apiClient.SetTokenViaMachineIdentity(fakeClientID, fakeClientSecret)
  136. assert.ErrorIs(t, err, errAccessTokenAlreadyRetrieved)
  137. })
  138. }
  139. func TestRevokeAccessToken(t *testing.T) {
  140. t.Run("Success", func(t *testing.T) {
  141. apiClient, closeFunc := NewMockClient(200, RevokeMachineIdentityAccessTokenResponse{
  142. Message: "Success",
  143. })
  144. defer closeFunc()
  145. apiClient.token = fakeToken
  146. err := apiClient.RevokeAccessToken()
  147. assert.NoError(t, err)
  148. // Verify that the access token was unset.
  149. assert.Equal(t, apiClient.token, "")
  150. })
  151. t.Run("RevokeAccessToken: Error when non-200 response received", func(t *testing.T) {
  152. apiClient, closeFunc := NewMockClient(401, InfisicalAPIErrorResponse{
  153. StatusCode: 401,
  154. Error: "Unauthorized",
  155. })
  156. defer closeFunc()
  157. apiClient.token = fakeToken
  158. err := apiClient.RevokeAccessToken()
  159. assert.Error(t, err)
  160. var apiErr *InfisicalAPIError
  161. assert.True(t, errors.As(err, &apiErr))
  162. assert.Equal(t, 401, apiErr.StatusCode)
  163. assert.Equal(t, "Unauthorized", apiErr.Err)
  164. })
  165. t.Run("Error when no access token is set", func(t *testing.T) {
  166. apiClient, closeFunc := NewMockClient(401, nil)
  167. defer closeFunc()
  168. err := apiClient.RevokeAccessToken()
  169. assert.ErrorIs(t, err, errNoAccessToken)
  170. })
  171. }
  172. func TestGetSecretsV3(t *testing.T) {
  173. t.Run("Works with secrets", func(t *testing.T) {
  174. apiClient, closeFunc := NewMockClient(200, GetSecretsV3Response{
  175. Secrets: []SecretsV3{
  176. {SecretKey: "foo", SecretValue: "bar"},
  177. },
  178. })
  179. defer closeFunc()
  180. secrets, err := apiClient.GetSecretsV3(GetSecretsV3Request{
  181. ProjectSlug: fakeProjectSlug,
  182. EnvironmentSlug: fakeEnvironmentSlug,
  183. SecretPath: "/",
  184. Recursive: true,
  185. })
  186. assert.NoError(t, err)
  187. assert.Equal(t, secrets, map[string]string{"foo": "bar"})
  188. })
  189. t.Run("Works with imported secrets", func(t *testing.T) {
  190. apiClient, closeFunc := NewMockClient(200, GetSecretsV3Response{
  191. ImportedSecrets: []ImportedSecretV3{{
  192. Secrets: []SecretsV3{{SecretKey: "foo", SecretValue: "bar"}},
  193. }},
  194. })
  195. defer closeFunc()
  196. secrets, err := apiClient.GetSecretsV3(GetSecretsV3Request{
  197. ProjectSlug: fakeProjectSlug,
  198. EnvironmentSlug: fakeEnvironmentSlug,
  199. SecretPath: "/",
  200. Recursive: true,
  201. })
  202. assert.NoError(t, err)
  203. assert.Equal(t, secrets, map[string]string{"foo": "bar"})
  204. })
  205. t.Run("GetSecretsV3: Error when non-200 response received", func(t *testing.T) {
  206. apiClient, closeFunc := NewMockClient(401, InfisicalAPIErrorResponse{
  207. StatusCode: 401,
  208. Error: "Unauthorized",
  209. })
  210. defer closeFunc()
  211. _, err := apiClient.GetSecretsV3(GetSecretsV3Request{
  212. ProjectSlug: fakeProjectSlug,
  213. EnvironmentSlug: fakeEnvironmentSlug,
  214. SecretPath: "/",
  215. Recursive: true,
  216. })
  217. assert.Error(t, err)
  218. var apiErr *InfisicalAPIError
  219. assert.True(t, errors.As(err, &apiErr))
  220. assert.Equal(t, 401, apiErr.StatusCode)
  221. assert.Equal(t, "Unauthorized", apiErr.Err)
  222. })
  223. }
  224. func TestGetSecretByKeyV3(t *testing.T) {
  225. t.Run("Works", func(t *testing.T) {
  226. apiClient, closeFunc := NewMockClient(200, GetSecretByKeyV3Response{
  227. Secret: SecretsV3{
  228. SecretKey: "foo",
  229. SecretValue: "bar",
  230. },
  231. })
  232. defer closeFunc()
  233. secret, err := apiClient.GetSecretByKeyV3(GetSecretByKeyV3Request{
  234. ProjectSlug: fakeProjectSlug,
  235. EnvironmentSlug: fakeEnvironmentSlug,
  236. SecretPath: "/",
  237. SecretKey: "foo",
  238. })
  239. assert.NoError(t, err)
  240. assert.Equal(t, "bar", secret)
  241. })
  242. t.Run("Error when secret is not found", func(t *testing.T) {
  243. apiClient, closeFunc := NewMockClient(404, InfisicalAPIErrorResponse{
  244. StatusCode: 404,
  245. Error: "Not Found",
  246. })
  247. defer closeFunc()
  248. _, err := apiClient.GetSecretByKeyV3(GetSecretByKeyV3Request{
  249. ProjectSlug: fakeProjectSlug,
  250. EnvironmentSlug: fakeEnvironmentSlug,
  251. SecretPath: "/",
  252. SecretKey: "foo",
  253. })
  254. assert.Error(t, err)
  255. // Importantly, we return the standard error for no secrets found.
  256. assert.ErrorIs(t, err, esv1.NoSecretError{})
  257. })
  258. // Test case where the request is unauthorized
  259. t.Run("ErrorHandlingUnauthorized", func(t *testing.T) {
  260. apiClient, closeFunc := NewMockClient(401, InfisicalAPIErrorResponse{
  261. StatusCode: 401,
  262. Error: "Unauthorized",
  263. })
  264. defer closeFunc()
  265. _, err := apiClient.GetSecretByKeyV3(GetSecretByKeyV3Request{
  266. ProjectSlug: fakeProjectSlug,
  267. EnvironmentSlug: fakeEnvironmentSlug,
  268. SecretPath: "/",
  269. SecretKey: "foo",
  270. })
  271. assert.Error(t, err)
  272. var apiErr *InfisicalAPIError
  273. assert.True(t, errors.As(err, &apiErr))
  274. assert.Equal(t, 401, apiErr.StatusCode)
  275. assert.Equal(t, "Unauthorized", apiErr.Err)
  276. })
  277. }