1import logging
2import sys
3
4import pytest
5
6logging.basicConfig(
7 level=logging.INFO,
8 format="%(asctime)s test %(levelname)s: %(message)s",
9 datefmt="%Y-%m-%d %H:%M:%S",
10)
11
12logger = logging.getLogger("ambassador")
13
14from ambassador import IR, Config
15from ambassador.fetch import ResourceFetcher
16from ambassador.ir.irerrorresponse import IRErrorResponse
17from ambassador.utils import NullSecretHandler
18
19
20def _status_code_filter_eq_obj(status_code):
21 return {
22 "status_code_filter": {
23 "comparison": {
24 "op": "EQ",
25 "value": {"default_value": f"{status_code}", "runtime_key": "_donotsetthiskey"},
26 }
27 }
28 }
29
30
31def _json_format_obj(json_format, content_type=None):
32 return {"json_format": json_format}
33
34
35def _text_format_obj(text, content_type=None):
36 obj = {"text_format": f"{text}"}
37 if content_type is not None:
38 obj["content_type"] = content_type
39 return obj
40
41
42def _text_format_source_obj(filename, content_type=None):
43 obj = {"text_format_source": {"filename": filename}}
44 if content_type is not None:
45 obj["content_type"] = content_type
46 return obj
47
48
49def _ambassador_module_config():
50 return """
51---
52apiVersion: getambassador.io/v3alpha1
53kind: Module
54name: ambassador
55config:
56"""
57
58
59def _ambassador_module_onemapper(status_code, body_kind, body_value, content_type=None):
60 mod = (
61 _ambassador_module_config()
62 + f"""
63 error_response_overrides:
64 - on_status_code: "{status_code}"
65 body:
66"""
67 )
68 if body_kind == "text_format_source":
69 mod = (
70 mod
71 + f"""
72 {body_kind}:
73 filename: "{body_value}"
74"""
75 )
76 elif body_kind == "json_format":
77 mod = (
78 mod
79 + f"""
80 {body_kind}: {body_value}
81"""
82 )
83 else:
84 mod = (
85 mod
86 + f"""
87 {body_kind}: "{body_value}"
88"""
89 )
90 if content_type is not None:
91 mod = (
92 mod
93 + f"""
94 content_type: "{content_type}"
95"""
96 )
97 return mod
98
99
100def _test_errorresponse(yaml, expectations, expect_fail=False):
101 aconf = Config()
102
103 fetcher = ResourceFetcher(logger, aconf)
104 fetcher.parse_yaml(yaml)
105
106 aconf.load_all(fetcher.sorted())
107
108 secret_handler = NullSecretHandler(logger, None, None, "0")
109
110 ir = IR(aconf, file_checker=lambda path: True, secret_handler=secret_handler)
111
112 error_response = IRErrorResponse(
113 ir, aconf, ir.ambassador_module.get("error_response_overrides", None), ir.ambassador_module
114 )
115
116 error_response.setup(ir, aconf)
117 if aconf.errors:
118 print("errors: %s" % repr(aconf.errors))
119
120 ir_conf = error_response.config()
121 if expect_fail:
122 assert ir_conf is None
123 return
124 assert ir_conf
125
126 # There should be no default body format override
127 body_format = ir_conf.get("body_format", None)
128 assert body_format is None
129
130 mappers = ir_conf.get("mappers", None)
131 assert mappers
132 assert len(mappers) == len(
133 expectations
134 ), f"unexpected len(mappers) {len(expectations)} != len(expectations) {len(expectations)}"
135
136 for i in range(len(expectations)):
137 expected_filter, expected_body_format_override = expectations[i]
138 m = mappers[i]
139
140 print(
141 "checking with expected_body_format_override %s and expected_filter %s"
142 % (expected_body_format_override, expected_filter)
143 )
144 print("checking m: ", m)
145 actual_filter = m["filter"]
146 assert m["filter"] == expected_filter
147 if expected_body_format_override:
148 actual_body_format_override = m["body_format_override"]
149 assert actual_body_format_override == expected_body_format_override
150
151
152def _test_errorresponse_onemapper(yaml, expected_filter, expected_body_format_override, fail=False):
153 return _test_errorresponse(
154 yaml, [(expected_filter, expected_body_format_override)], expect_fail=fail
155 )
156
157
158def _test_errorresponse_twomappers(yaml, expectation1, expectation2, fail=False):
159 return _test_errorresponse(yaml, [expectation1, expectation2], expect_fail=fail)
160
161
162def _test_errorresponse_onemapper_onstatuscode_textformat(status_code, text_format, fail=False):
163 _test_errorresponse_onemapper(
164 _ambassador_module_onemapper(status_code, "text_format", text_format),
165 _status_code_filter_eq_obj(status_code),
166 _text_format_obj(text_format),
167 fail=fail,
168 )
169
170
171def _test_errorresponse_onemapper_onstatuscode_textformat_contenttype(
172 status_code, text_format, content_type
173):
174 _test_errorresponse_onemapper(
175 _ambassador_module_onemapper(
176 status_code, "text_format", text_format, content_type=content_type
177 ),
178 _status_code_filter_eq_obj(status_code),
179 _text_format_obj(text_format, content_type=content_type),
180 )
181
182
183def _test_errorresponse_onemapper_onstatuscode_textformat_datasource(
184 status_code, text_format, source, content_type
185):
186
187 # in order for tests to pass the files (all located in /tmp) need to exist
188 try:
189 open(source, "x").close()
190 except OSError:
191 # the file already exists
192 pass
193
194 _test_errorresponse_onemapper(
195 _ambassador_module_onemapper(
196 status_code, "text_format_source", source, content_type=content_type
197 ),
198 _status_code_filter_eq_obj(status_code),
199 _text_format_source_obj(source, content_type=content_type),
200 )
201
202
203def _sanitize_json(json_format):
204 sanitized = {}
205 for k, v in json_format.items():
206 if isinstance(v, bool):
207 sanitized[k] = str(v).lower()
208 else:
209 sanitized[k] = str(v)
210 return sanitized
211
212
213def _test_errorresponse_onemapper_onstatuscode_jsonformat(status_code, json_format):
214 _test_errorresponse_onemapper(
215 _ambassador_module_onemapper(status_code, "json_format", json_format),
216 _status_code_filter_eq_obj(status_code),
217 # We expect the output json to be sanitized and contain the string representation
218 # of every value. We provide a basic implementation of string sanitizatino in this
219 # test, `sanitize_json`.
220 _json_format_obj(_sanitize_json(json_format)),
221 )
222
223
224def _test_errorresponse_twomappers_onstatuscode_textformat(code1, text1, code2, text2, fail=False):
225 _test_errorresponse_twomappers(
226 f"""
227---
228apiVersion: getambassador.io/v3alpha1
229kind: Module
230name: ambassador
231config:
232 error_response_overrides:
233 - on_status_code: "{code1}"
234 body:
235 text_format: {text1}
236 - on_status_code: {code2}
237 body:
238 text_format: {text2}
239""",
240 (_status_code_filter_eq_obj(code1), _text_format_obj(text1)),
241 (_status_code_filter_eq_obj(code2), _text_format_obj(text2)),
242 fail=fail,
243 )
244
245
246def _test_errorresponse_invalid_configs(yaml):
247 _test_errorresponse(yaml, list(), expect_fail=True)
248
249
250@pytest.mark.compilertest
251def test_errorresponse_twomappers_onstatuscode_textformat():
252 _test_errorresponse_twomappers_onstatuscode_textformat(
253 "400", "bad request my friend", "504", "waited too long for an upstream resonse"
254 )
255 _test_errorresponse_twomappers_onstatuscode_textformat("503", "boom", "403", "go away")
256
257
258@pytest.mark.compilertest
259def test_errorresponse_onemapper_onstatuscode_textformat():
260 _test_errorresponse_onemapper_onstatuscode_textformat(429, "429 the int")
261 _test_errorresponse_onemapper_onstatuscode_textformat("501", "five oh one")
262 _test_errorresponse_onemapper_onstatuscode_textformat("400", "bad req")
263 _test_errorresponse_onemapper_onstatuscode_textformat(
264 "429", "too fast, too furious on host %REQ(:authority)%"
265 )
266
267
268@pytest.mark.compilertest
269def test_errorresponse_invalid_envoy_operator():
270 _test_errorresponse_onemapper_onstatuscode_textformat(404, "%FAILME%", fail=True)
271
272
273@pytest.mark.compilertest
274def test_errorresponse_onemapper_onstatuscode_textformat_contenttype():
275 _test_errorresponse_onemapper_onstatuscode_textformat_contenttype("503", "oops", "text/what")
276 _test_errorresponse_onemapper_onstatuscode_textformat_contenttype(
277 "429", "<html>too fast, too furious on host %REQ(:authority)%</html>", "text/html"
278 )
279 _test_errorresponse_onemapper_onstatuscode_textformat_contenttype(
280 "404", "{'error':'notfound'}", "application/json"
281 )
282
283
284@pytest.mark.compilertest
285def test_errorresponse_onemapper_onstatuscode_jsonformat():
286 _test_errorresponse_onemapper_onstatuscode_jsonformat(
287 "501",
288 {
289 "response_code": "%RESPONSE_CODE%",
290 "upstream_cluster": "%UPSTREAM_CLUSTER%",
291 "badness": "yup",
292 },
293 )
294 # Test both a JSON object whose Python type has non-string primitives...
295 _test_errorresponse_onemapper_onstatuscode_jsonformat(
296 "401",
297 {
298 "unauthorized": "yeah",
299 "your_address": "%DOWNSTREAM_REMOTE_ADDRESS%",
300 "security_level": 9000,
301 "awesome": True,
302 "floaty": 0.75,
303 },
304 )
305 # ...and a JSON object where the Python type already has strings
306 _test_errorresponse_onemapper_onstatuscode_jsonformat(
307 "403",
308 {
309 "whoareyou": "dunno",
310 "your_address": "%DOWNSTREAM_REMOTE_ADDRESS%",
311 "security_level": "11000",
312 "awesome": "false",
313 "floaty": "0.95",
314 },
315 )
316
317
318@pytest.mark.compilertest
319def test_errorresponse_onemapper_onstatuscode_textformatsource():
320 _test_errorresponse_onemapper_onstatuscode_textformat_datasource(
321 "400", "badness", "/tmp/badness", "text/plain"
322 )
323 _test_errorresponse_onemapper_onstatuscode_textformat_datasource(
324 "404", "badness", "/tmp/notfound.dat", "application/specialsauce"
325 )
326 _test_errorresponse_onemapper_onstatuscode_textformat_datasource(
327 "429", "2fast", "/tmp/2fast.html", "text/html"
328 )
329 _test_errorresponse_onemapper_onstatuscode_textformat_datasource(
330 "503", "something went wrong", "/tmp/503.html", "text/html; charset=UTF-8"
331 )
332
333
334@pytest.mark.compilertest
335def test_errorresponse_invalid_configs():
336 # status code must be an int
337 _test_errorresponse_invalid_configs(
338 _ambassador_module_config()
339 + f"""
340 error_response_overrides:
341 - on_status_code: bad
342 body:
343 text_format: 'good'
344"""
345 )
346 # cannot match on code < 400 nor >= 600
347 _test_errorresponse_invalid_configs(
348 _ambassador_module_config()
349 + f"""
350 error_response_overrides:
351 - on_status_code: 200
352 body:
353 text_format: 'good'
354"""
355 )
356 _test_errorresponse_invalid_configs(
357 _ambassador_module_config()
358 + f"""
359 error_response_overrides:
360 - on_status_code: 399
361 body:
362 text_format: 'good'
363"""
364 )
365 _test_errorresponse_invalid_configs(
366 _ambassador_module_config()
367 + f"""
368 error_response_overrides:
369 - on_status_code: 600
370 body:
371 text_format: 'good'
372"""
373 )
374 # body must be a dict
375 _test_errorresponse_invalid_configs(
376 _ambassador_module_config()
377 + f"""
378 error_response_overrides:
379 - on_status_code: 401
380 body: 'bad'
381"""
382 )
383 # body requires a valid format field
384 _test_errorresponse_invalid_configs(
385 _ambassador_module_config()
386 + f"""
387 error_response_overrides:
388 - on_status_code: 401
389 body:
390 bad: 'good'
391"""
392 )
393 # body field must be present
394 _test_errorresponse_invalid_configs(
395 _ambassador_module_config()
396 + f"""
397 error_response_overrides:
398 - on_status_code: 501
399 bad:
400 text_format: 'good'
401"""
402 )
403 # body field cannot be an empty dict
404 _test_errorresponse_invalid_configs(
405 _ambassador_module_config()
406 + f"""
407 error_response_overrides:
408 - on_status_code: 501
409 body: {{}}
410"""
411 )
412 # response override must be a non-empty array
413 _test_errorresponse_invalid_configs(
414 _ambassador_module_config()
415 + f"""
416 error_response_overrides: []
417"""
418 )
419 _test_errorresponse_invalid_configs(
420 _ambassador_module_config()
421 + f"""
422 error_response_overrides: 'great sadness'
423"""
424 )
425 # (not an array, bad)
426 _test_errorresponse_invalid_configs(
427 _ambassador_module_config()
428 + f"""
429 error_response_overrides:
430 on_status_code: 401
431 body:
432 text_format: 'good'
433"""
434 )
435 # text_format_source must have a single string 'filename'
436 _test_errorresponse_invalid_configs(
437 _ambassador_module_config()
438 + f"""
439 error_response_overrides:
440 - on_status_code: 401
441 body:
442 text_format_source: "this obviously cannot be a string"
443"""
444 )
445 _test_errorresponse_invalid_configs(
446 _ambassador_module_config()
447 + f"""
448 error_response_overrides:
449 - on_status_code: 401
450 body:
451 text_format_source:
452 filename: []
453"""
454 )
455 _test_errorresponse_invalid_configs(
456 _ambassador_module_config()
457 + f"""
458 error_response_overrides:
459 - on_status_code: 401
460 body:
461 text_format_source:
462 notfilename: "/tmp/good"
463"""
464 )
465 # json_format field must be an object field
466 _test_errorresponse_invalid_configs(
467 _ambassador_module_config()
468 + f"""
469 error_response_overrides:
470 - on_status_code: 401
471 body:
472 json_format: "this also cannot be a string"
473"""
474 )
475 # json_format cannot have values that do not cast to string trivially
476 _test_errorresponse_invalid_configs(
477 _ambassador_module_config()
478 + f"""
479 error_response_overrides:
480 - on_status_code: 401
481 body:
482 json_format:
483 "x":
484 "yo": 1
485 "field": "good"
486"""
487 )
488 _test_errorresponse_invalid_configs(
489 _ambassador_module_config()
490 + f"""
491 error_response_overrides:
492 - on_status_code: 401
493 body:
494 json_format:
495 "a": []
496 "x": true
497"""
498 )
499 # content type, if it exists, must be a string
500 _test_errorresponse_invalid_configs(
501 _ambassador_module_config()
502 + f"""
503 error_response_overrides:
504 - on_status_code: 401
505 body:
506 text_format: "good"
507 content_type: []
508"""
509 )
510 _test_errorresponse_invalid_configs(
511 _ambassador_module_config()
512 + f"""
513 error_response_overrides:
514 - on_status_code: 401
515 body:
516 text_format: "good"
517 content_type: 4.2
518"""
519 )
520 # only one of text_format, json_format, or text_format_source may be set
521 _test_errorresponse_invalid_configs(
522 _ambassador_module_config()
523 + f"""
524 error_response_overrides:
525 - on_status_code: 401
526 body:
527 text_format: "bad"
528 json_format:
529 "bad": 1
530 "invalid": "bad"
531"""
532 )
533 _test_errorresponse_invalid_configs(
534 _ambassador_module_config()
535 + f"""
536 error_response_overrides:
537 - on_status_code: 401
538 body:
539 text_format: "goodgood"
540 text_format_source:
541 filename: "/etc/issue"
542"""
543 )
544
545
546if __name__ == "__main__":
547 pytest.main(sys.argv)
View as plain text