...

Text file src/github.com/datawire/ambassador/v2/python/tests/unit/test_error_response.py

Documentation: github.com/datawire/ambassador/v2/python/tests/unit

     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