webhook_test.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  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 webhook
  14. import (
  15. "bytes"
  16. "context"
  17. "encoding/json"
  18. "errors"
  19. "io"
  20. "net/http"
  21. "net/http/httptest"
  22. "strings"
  23. "testing"
  24. "time"
  25. "gopkg.in/yaml.v3"
  26. apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
  27. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  28. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  29. genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
  30. )
  31. type testCase struct {
  32. Case string `json:"case,omitempty"`
  33. Args args `json:"args"`
  34. Want want `json:"want"`
  35. }
  36. type args struct {
  37. URL string `json:"url,omitempty"`
  38. Body string `json:"body,omitempty"`
  39. Timeout string `json:"timeout,omitempty"`
  40. Key string `json:"key,omitempty"`
  41. Property string `json:"property,omitempty"`
  42. Version string `json:"version,omitempty"`
  43. JSONPath string `json:"jsonpath,omitempty"`
  44. Response string `json:"response,omitempty"`
  45. StatusCode int `json:"statuscode,omitempty"`
  46. }
  47. type want struct {
  48. Path string `json:"path,omitempty"`
  49. Err string `json:"err,omitempty"`
  50. Result string `json:"result,omitempty"`
  51. ResultMap map[string]string `json:"resultmap,omitempty"`
  52. }
  53. var testCases = `
  54. case: error url
  55. args:
  56. url: /api/getsecret?id={{ .unclosed.template
  57. want:
  58. err: failed to parse url
  59. ---
  60. case: error body
  61. args:
  62. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  63. body: Body error {{ .unclosed.template
  64. want:
  65. err: failed to parse body
  66. ---
  67. case: error connection
  68. args:
  69. url: 1/api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  70. want:
  71. err: failed to call endpoint
  72. ---
  73. case: error no secret err
  74. args:
  75. url: /api/getsecret?id=testkey&version=1
  76. key: testkey
  77. version: 1
  78. statuscode: 404
  79. response: not found
  80. want:
  81. path: /api/getsecret?id=testkey&version=1
  82. err: ` + esv1.NoSecretErr.Error() + `
  83. ---
  84. case: error server error
  85. args:
  86. url: /api/getsecret?id=testkey&version=1
  87. key: testkey
  88. version: 1
  89. statuscode: 500
  90. response: server error
  91. want:
  92. path: /api/getsecret?id=testkey&version=1
  93. err: endpoint gave error 500
  94. ---
  95. case: error bad json
  96. args:
  97. url: /api/getsecret?id=testkey&version=1
  98. key: testkey
  99. version: 1
  100. jsonpath: $.result.thesecret
  101. response: '{"result":{"thesecret":"secret-value"}'
  102. want:
  103. path: /api/getsecret?id=testkey&version=1
  104. err: failed to parse response json
  105. ---
  106. case: error bad jsonpath
  107. args:
  108. url: /api/getsecret?id=testkey&version=1
  109. key: testkey
  110. version: 1
  111. jsonpath: $.result.thesecret
  112. response: '{"result":{"nosecret":"secret-value"}}'
  113. want:
  114. path: /api/getsecret?id=testkey&version=1
  115. err: failed to get response path
  116. ---
  117. case: pull data out of map
  118. args:
  119. url: /api/getsecret?id=testkey&version=1
  120. key: testkey
  121. version: 1
  122. jsonpath: $.result.thesecret
  123. response: '{"result":{"thesecret":{"one":"secret-value"}}}'
  124. want:
  125. path: /api/getsecret?id=testkey&version=1
  126. err: ''
  127. result: '{"one":"secret-value"}'
  128. ---
  129. case: not valid response path
  130. args:
  131. url: /api/getsecret?id=testkey&version=1
  132. key: testkey
  133. version: 1
  134. jsonpath: $.result.unexisting
  135. response: '{"result":{"thesecret":{"one":"secret-value"}}}'
  136. want:
  137. path: /api/getsecret?id=testkey&version=1
  138. err: 'failed to get response path'
  139. result: ''
  140. ---
  141. case: response path not json
  142. args:
  143. url: /api/getsecret?id=testkey&version=1
  144. key: testkey
  145. version: 1
  146. jsonpath: $.result.thesecret
  147. response: '{"result":{"thesecret":[{"one":"secret-value"}]}}'
  148. want:
  149. path: /api/getsecret?id=testkey&version=1
  150. err: 'failed to get response (wrong type:'
  151. result: ''
  152. `
  153. func TestWebhookGetSecret(t *testing.T) {
  154. ydec := yaml.NewDecoder(bytes.NewReader([]byte(testCases)))
  155. for {
  156. var tc testCase
  157. if err := ydec.Decode(&tc); err != nil {
  158. if !errors.Is(err, io.EOF) {
  159. t.Errorf("testcase decode error %v", err)
  160. }
  161. break
  162. }
  163. runTestCase(tc, t)
  164. }
  165. }
  166. func testCaseServer(tc testCase, t *testing.T) *httptest.Server {
  167. // Start a new server for every test case because the server wants to check the expected api path
  168. return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
  169. if tc.Want.Path != "" && req.URL.String() != tc.Want.Path {
  170. t.Errorf("%s: unexpected api path: %s, expected %s", tc.Case, req.URL.String(), tc.Want.Path)
  171. }
  172. if tc.Args.StatusCode != 0 {
  173. rw.WriteHeader(tc.Args.StatusCode)
  174. }
  175. rw.Write([]byte(tc.Args.Response))
  176. }))
  177. }
  178. func parseTimeout(timeout string) (*metav1.Duration, error) {
  179. if timeout == "" {
  180. return nil, nil
  181. }
  182. dur, err := time.ParseDuration(timeout)
  183. if err != nil {
  184. return nil, err
  185. }
  186. return &metav1.Duration{Duration: dur}, nil
  187. }
  188. func runTestCase(tc testCase, t *testing.T) {
  189. ts := testCaseServer(tc, t)
  190. defer ts.Close()
  191. testStore := makeGenerator(ts.URL, tc.Args)
  192. jsonRes, err := json.Marshal(testStore)
  193. if err != nil {
  194. t.Errorf("%s: error parsing timeout '%s': %s", tc.Case, tc.Args.Timeout, err.Error())
  195. return
  196. }
  197. genSpec := &apiextensions.JSON{Raw: jsonRes}
  198. timeout, err := parseTimeout(tc.Args.Timeout)
  199. if err != nil {
  200. t.Errorf("%s: error parsing timeout '%s': %s", tc.Case, tc.Args.Timeout, err.Error())
  201. return
  202. }
  203. testStore.Spec.Timeout = timeout
  204. testProv := &Webhook{}
  205. testGenerate(tc, t, testProv, genSpec)
  206. }
  207. func testGenerate(tc testCase, t *testing.T, client genv1alpha1.Generator, testStore *apiextensions.JSON) {
  208. secretmap, _, err := client.Generate(context.Background(), testStore, nil, "testnamespace")
  209. errStr := ""
  210. if err != nil {
  211. errStr = err.Error()
  212. }
  213. if (tc.Want.Err == "") != (errStr == "") || !strings.Contains(errStr, tc.Want.Err) {
  214. t.Errorf("%s: unexpected error: '%s' (expected '%s')", tc.Case, errStr, tc.Want.Err)
  215. }
  216. if err == nil {
  217. for wantkey, wantval := range tc.Want.ResultMap {
  218. gotval, ok := secretmap[wantkey]
  219. if !ok {
  220. t.Errorf("%s: unexpected response: wanted key '%s' not found", tc.Case, wantkey)
  221. } else if string(gotval) != wantval {
  222. t.Errorf("%s: unexpected response: key '%s' = '%s' (expected '%s')", tc.Case, wantkey, wantval, gotval)
  223. }
  224. }
  225. }
  226. }
  227. func makeGenerator(url string, args args) *genv1alpha1.Webhook {
  228. store := &genv1alpha1.Webhook{
  229. TypeMeta: metav1.TypeMeta{
  230. Kind: "Webhook",
  231. },
  232. ObjectMeta: metav1.ObjectMeta{
  233. Name: "wehbook-store",
  234. Namespace: "default",
  235. },
  236. Spec: genv1alpha1.WebhookSpec{
  237. URL: url + args.URL,
  238. Body: args.Body,
  239. Headers: map[string]string{
  240. "Content-Type": "application.json",
  241. "X-SecretKey": "{{ .remoteRef.key }}",
  242. },
  243. Result: genv1alpha1.WebhookResult{
  244. JSONPath: args.JSONPath,
  245. },
  246. },
  247. }
  248. return store
  249. }