...

Text file src/github.com/emissary-ingress/emissary/v3/python/tests/unit/test_error_response.py

Documentation: github.com/emissary-ingress/emissary/v3/python/tests/unit

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

View as plain text