webhook_test.go 7.0 KB

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