...

Text file src/github.com/emissary-ingress/emissary/v3/python/tests/kat/t_error_response.py

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

     1from typing import Generator, Tuple, Union
     2
     3from abstract_tests import HTTP, AmbassadorTest, Node
     4from kat.harness import Query
     5
     6
     7class ErrorResponseOnStatusCode(AmbassadorTest):
     8    """
     9    Check that we can return a customized error response where the body is built as a formatted string.
    10    """
    11
    12    def init(self):
    13        self.target = HTTP()
    14
    15    def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
    16        yield self, f"""
    17---
    18apiVersion: getambassador.io/v3alpha1
    19kind: Module
    20name: ambassador
    21ambassador_id: ["{self.ambassador_id}"]
    22config:
    23  error_response_overrides:
    24  - on_status_code: 401
    25    body:
    26      text_format: 'you get a 401'
    27  - on_status_code: 403
    28    body:
    29      text_format: 'and you get a 403'
    30  - on_status_code: 404
    31    body:
    32      text_format: 'cannot find the thing'
    33  - on_status_code: 418
    34    body:
    35      text_format: '2teapot2reply'
    36  - on_status_code: 500
    37    body:
    38      text_format: 'a five hundred happened'
    39  - on_status_code: 501
    40    body:
    41      text_format: 'very not implemented'
    42  - on_status_code: 503
    43    body:
    44      text_format: 'the upstream probably died'
    45  - on_status_code: 504
    46    body:
    47      text_format: 'took too long, sorry'
    48      content_type: 'apology'
    49---
    50apiVersion: getambassador.io/v3alpha1
    51kind: Mapping
    52name:  {self.target.path.k8s}
    53ambassador_id: ["{self.ambassador_id}"]
    54hostname: "*"
    55prefix: /target/
    56service: {self.target.path.fqdn}
    57---
    58apiVersion: getambassador.io/v3alpha1
    59kind: Mapping
    60name:  {self.target.path.k8s}-invalidservice
    61ambassador_id: ["{self.ambassador_id}"]
    62hostname: "*"
    63prefix: /target/invalidservice
    64service: {self.target.path.fqdn}-invalidservice
    65---
    66apiVersion: getambassador.io/v3alpha1
    67kind: Mapping
    68name:  {self.target.path.k8s}-invalidservice-empty
    69ambassador_id: ["{self.ambassador_id}"]
    70hostname: "*"
    71prefix: /target/invalidservice/empty
    72service: {self.target.path.fqdn}-invalidservice-empty
    73error_response_overrides:
    74- on_status_code: 503
    75  body:
    76    text_format: ''
    77"""
    78
    79    def queries(self):
    80        # [0]
    81        yield Query(self.url("does-not-exist/"), expected=404)
    82        # [1]
    83        yield Query(
    84            self.url("target/"), headers={"kat-req-http-requested-status": "401"}, expected=401
    85        )
    86        # [2]
    87        yield Query(
    88            self.url("target/"), headers={"kat-req-http-requested-status": "403"}, expected=403
    89        )
    90        # [3]
    91        yield Query(
    92            self.url("target/"), headers={"kat-req-http-requested-status": "404"}, expected=404
    93        )
    94        # [4]
    95        yield Query(
    96            self.url("target/"), headers={"kat-req-http-requested-status": "418"}, expected=418
    97        )
    98        # [5]
    99        yield Query(
   100            self.url("target/"), headers={"kat-req-http-requested-status": "500"}, expected=500
   101        )
   102        # [6]
   103        yield Query(
   104            self.url("target/"), headers={"kat-req-http-requested-status": "501"}, expected=501
   105        )
   106        # [7]
   107        yield Query(
   108            self.url("target/"), headers={"kat-req-http-requested-status": "503"}, expected=503
   109        )
   110        # [8]
   111        yield Query(
   112            self.url("target/"), headers={"kat-req-http-requested-status": "504"}, expected=504
   113        )
   114        # [9]
   115        yield Query(self.url("target/"))
   116        # [10]
   117        yield Query(self.url("target/invalidservice"), expected=503)
   118        # [11]
   119        yield Query(self.url("target/invalidservice/empty"), expected=503)
   120
   121    def check(self):
   122        # [0]
   123        assert (
   124            self.results[0].text == "cannot find the thing"
   125        ), f"unexpected response body: {self.results[0].text}"
   126
   127        # [1]
   128        assert (
   129            self.results[1].text == "you get a 401"
   130        ), f"unexpected response body: {self.results[1].text}"
   131
   132        # [2]
   133        assert (
   134            self.results[2].text == "and you get a 403"
   135        ), f"unexpected response body: {self.results[2].text}"
   136
   137        # [3]
   138        assert (
   139            self.results[3].text == "cannot find the thing"
   140        ), f"unexpected response body: {self.results[3].text}"
   141
   142        # [4]
   143        assert (
   144            self.results[4].text == "2teapot2reply"
   145        ), f"unexpected response body: {self.results[4].text}"
   146
   147        # [5]
   148        assert (
   149            self.results[5].text == "a five hundred happened"
   150        ), f"unexpected response body: {self.results[5].text}"
   151
   152        # [6]
   153        assert (
   154            self.results[6].text == "very not implemented"
   155        ), f"unexpected response body: {self.results[6].text}"
   156
   157        # [7]
   158        assert (
   159            self.results[7].text == "the upstream probably died"
   160        ), f"unexpected response body: {self.results[7].text}"
   161
   162        # [8]
   163        assert (
   164            self.results[8].text == "took too long, sorry"
   165        ), f"unexpected response body: {self.results[8].text}"
   166        assert self.results[8].headers["Content-Type"] == [
   167            "apology"
   168        ], f"unexpected Content-Type: {self.results[8].headers}"
   169
   170        # [9] should just succeed
   171        assert self.results[9].text == None, f"unexpected response body: {self.results[9].text}"
   172
   173        # [10] envoy-generated 503, since the upstream is 'invalidservice'.
   174        assert (
   175            self.results[10].text == "the upstream probably died"
   176        ), f"unexpected response body: {self.results[10].text}"
   177
   178        # [11] envoy-generated 503, with an empty body override
   179        assert self.results[11].text == "", f"unexpected response body: {self.results[11].text}"
   180
   181
   182class ErrorResponseOnStatusCodeMappingCRD(AmbassadorTest):
   183    """
   184    Check that we can return a customized error response where the body is built as a formatted string.
   185    """
   186
   187    def init(self):
   188        self.target = HTTP()
   189
   190    def manifests(self) -> str:
   191        return (
   192            super().manifests()
   193            + f"""
   194---
   195apiVersion: getambassador.io/v3alpha1
   196kind: Mapping
   197metadata:
   198  name:  {self.target.path.k8s}-crd
   199spec:
   200  ambassador_id: ["{self.ambassador_id}"]
   201  hostname: "*"
   202  prefix: /target/
   203  service: {self.target.path.fqdn}
   204  error_response_overrides:
   205  - on_status_code: 401
   206    body:
   207      text_format: 'you get a 401'
   208  - on_status_code: 403
   209    body:
   210      text_format: 'and you get a 403'
   211  - on_status_code: 404
   212    body:
   213      text_format: 'cannot find the thing'
   214  - on_status_code: 418
   215    body:
   216      text_format: '2teapot2reply'
   217  - on_status_code: 500
   218    body:
   219      text_format: 'a five hundred happened'
   220  - on_status_code: 501
   221    body:
   222      text_format: 'very not implemented'
   223  - on_status_code: 503
   224    body:
   225      text_format: 'the upstream probably died'
   226  - on_status_code: 504
   227    body:
   228      text_format: 'took too long, sorry'
   229      content_type: 'apology'
   230---
   231apiVersion: getambassador.io/v3alpha1
   232kind: Mapping
   233metadata:
   234  name: {self.target.path.k8s}-invalidservice-crd
   235spec:
   236  ambassador_id: ["{self.ambassador_id}"]
   237  hostname: "*"
   238  prefix: /target/invalidservice
   239  service: {self.target.path.fqdn}-invalidservice
   240---
   241apiVersion: getambassador.io/v3alpha1
   242kind: Mapping
   243metadata:
   244  name: {self.target.path.k8s}-invalidservice-override-crd
   245spec:
   246  ambassador_id: ["{self.ambassador_id}"]
   247  hostname: "*"
   248  prefix: /target/invalidservice/override
   249  service: {self.target.path.fqdn}-invalidservice
   250  error_response_overrides:
   251  - on_status_code: 503
   252    body:
   253      text_format_source:
   254        filename: /etc/issue
   255"""
   256        )
   257
   258    def queries(self):
   259        # [0]
   260        yield Query(self.url("does-not-exist/"), expected=404)
   261        # [1]
   262        yield Query(
   263            self.url("target/"), headers={"kat-req-http-requested-status": "401"}, expected=401
   264        )
   265        # [2]
   266        yield Query(
   267            self.url("target/"), headers={"kat-req-http-requested-status": "403"}, expected=403
   268        )
   269        # [3]
   270        yield Query(
   271            self.url("target/"), headers={"kat-req-http-requested-status": "404"}, expected=404
   272        )
   273        # [4]
   274        yield Query(
   275            self.url("target/"), headers={"kat-req-http-requested-status": "418"}, expected=418
   276        )
   277        # [5]
   278        yield Query(
   279            self.url("target/"), headers={"kat-req-http-requested-status": "500"}, expected=500
   280        )
   281        # [6]
   282        yield Query(
   283            self.url("target/"), headers={"kat-req-http-requested-status": "501"}, expected=501
   284        )
   285        # [7]
   286        yield Query(
   287            self.url("target/"), headers={"kat-req-http-requested-status": "503"}, expected=503
   288        )
   289        # [8]
   290        yield Query(
   291            self.url("target/"), headers={"kat-req-http-requested-status": "504"}, expected=504
   292        )
   293        # [9]
   294        yield Query(self.url("target/"))
   295        # [10]
   296        yield Query(self.url("target/invalidservice"), expected=503)
   297        # [11]
   298        yield Query(self.url("target/invalidservice/override"), expected=503)
   299
   300    def check(self):
   301        # [0] does not match the error response mapping, so no 404 response.
   302        # when envoy directly replies with 404, we see it as an empty string.
   303        assert self.results[0].text == "", f"unexpected response body: {self.results[0].text}"
   304
   305        # [1]
   306        assert (
   307            self.results[1].text == "you get a 401"
   308        ), f"unexpected response body: {self.results[1].text}"
   309
   310        # [2]
   311        assert (
   312            self.results[2].text == "and you get a 403"
   313        ), f"unexpected response body: {self.results[2].text}"
   314
   315        # [3]
   316        assert (
   317            self.results[3].text == "cannot find the thing"
   318        ), f"unexpected response body: {self.results[3].text}"
   319
   320        # [4]
   321        assert (
   322            self.results[4].text == "2teapot2reply"
   323        ), f"unexpected response body: {self.results[4].text}"
   324
   325        # [5]
   326        assert (
   327            self.results[5].text == "a five hundred happened"
   328        ), f"unexpected response body: {self.results[5].text}"
   329
   330        # [6]
   331        assert (
   332            self.results[6].text == "very not implemented"
   333        ), f"unexpected response body: {self.results[6].text}"
   334
   335        # [7]
   336        assert (
   337            self.results[7].text == "the upstream probably died"
   338        ), f"unexpected response body: {self.results[7].text}"
   339
   340        # [8]
   341        assert (
   342            self.results[8].text == "took too long, sorry"
   343        ), f"unexpected response body: {self.results[8].text}"
   344        assert self.results[8].headers["Content-Type"] == [
   345            "apology"
   346        ], f"unexpected Content-Type: {self.results[8].headers}"
   347
   348        # [9] should just succeed
   349        assert self.results[9].text == None, f"unexpected response body: {self.results[9].text}"
   350
   351        # [10] envoy-generated 503, since the upstream is 'invalidservice'.
   352        # this response body comes unmodified from envoy, since it goes through
   353        # a mapping with no error response overrides and there's no overrides
   354        # on the Ambassador module
   355        assert (
   356            self.results[10].text == "no healthy upstream"
   357        ), f"unexpected response body: {self.results[10].text}"
   358
   359        # [11] envoy-generated 503, since the upstream is 'invalidservice'.
   360        # this response body should be matched by the `text_format_source` override
   361        # sorry for using /etc/issue, by the way.
   362        assert (
   363            "Welcome to Alpine Linux" in self.results[11].text
   364        ), f"unexpected response body: {self.results[11].text}"
   365
   366
   367class ErrorResponseReturnBodyFormattedText(AmbassadorTest):
   368    """
   369    Check that we can return a customized error response where the body is built as a formatted string.
   370    """
   371
   372    def init(self):
   373        self.target = HTTP()
   374
   375    def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
   376        yield self, f"""
   377---
   378apiVersion: getambassador.io/v3alpha1
   379kind: Module
   380name: ambassador
   381ambassador_id: ["{self.ambassador_id}"]
   382config:
   383  error_response_overrides:
   384  - on_status_code: 404
   385    body:
   386      text_format: 'there has been an error: %RESPONSE_CODE%'
   387  - on_status_code: 429
   388    body:
   389      text_format: '<html>2fast %PROTOCOL%</html>'
   390      content_type: 'text/html'
   391  - on_status_code: 504
   392    body:
   393      text_format: '<html>2slow %PROTOCOL%</html>'
   394      content_type: 'text/html; charset="utf-8"'
   395---
   396apiVersion: getambassador.io/v3alpha1
   397kind: Mapping
   398name:  {self.target.path.k8s}
   399ambassador_id: ["{self.ambassador_id}"]
   400hostname: "*"
   401prefix: /target/
   402service: {self.target.path.fqdn}
   403"""
   404
   405    def queries(self):
   406        # [0]
   407        yield Query(self.url("does-not-exist/"), expected=404)
   408
   409        # [1]
   410        yield Query(
   411            self.url("target/"), headers={"kat-req-http-requested-status": "429"}, expected=429
   412        )
   413
   414        # [2]
   415        yield Query(
   416            self.url("target/"), headers={"kat-req-http-requested-status": "504"}, expected=504
   417        )
   418
   419    def check(self):
   420        # [0]
   421        assert (
   422            self.results[0].text == "there has been an error: 404"
   423        ), f"unexpected response body: {self.results[0].text}"
   424        assert self.results[0].headers["Content-Type"] == [
   425            "text/plain"
   426        ], f"unexpected Content-Type: {self.results[0].headers}"
   427
   428        # [1]
   429        assert (
   430            self.results[1].text == "<html>2fast HTTP/1.1</html>"
   431        ), f"unexpected response body: {self.results[1].text}"
   432        assert self.results[1].headers["Content-Type"] == [
   433            "text/html"
   434        ], f"unexpected Content-type: {self.results[1].headers}"
   435
   436        # [2]
   437        assert (
   438            self.results[2].text == "<html>2slow HTTP/1.1</html>"
   439        ), f"unexpected response body: {self.results[2].text}"
   440        assert self.results[2].headers["Content-Type"] == [
   441            'text/html; charset="utf-8"'
   442        ], f"unexpected Content-Type: {self.results[2].headers}"
   443
   444
   445class ErrorResponseReturnBodyFormattedJson(AmbassadorTest):
   446    """
   447    Check that we can return a customized error response where the body is built from a text source.
   448    """
   449
   450    def init(self):
   451        self.target = HTTP()
   452
   453    def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
   454        yield self, f"""
   455---
   456apiVersion: getambassador.io/v3alpha1
   457kind: Module
   458name: ambassador
   459ambassador_id: ["{self.ambassador_id}"]
   460config:
   461  error_response_overrides:
   462  - on_status_code: 401
   463    body:
   464      json_format:
   465        error: 'unauthorized'
   466  - on_status_code: 404
   467    body:
   468      json_format:
   469        custom_error: 'truth'
   470        code: '%RESPONSE_CODE%'
   471  - on_status_code: 429
   472    body:
   473      json_format:
   474        custom_error: 'yep'
   475        toofast: 'definitely'
   476        code: 'code was %RESPONSE_CODE%'
   477---
   478apiVersion: getambassador.io/v3alpha1
   479kind: Mapping
   480name:  {self.target.path.k8s}
   481ambassador_id: ["{self.ambassador_id}"]
   482hostname: "*"
   483prefix: /target/
   484service: {self.target.path.fqdn}
   485"""
   486
   487    def queries(self):
   488        yield Query(self.url("does-not-exist/"), expected=404)
   489        yield Query(
   490            self.url("target/"), headers={"kat-req-http-requested-status": "429"}, expected=429
   491        )
   492        yield Query(
   493            self.url("target/"), headers={"kat-req-http-requested-status": "401"}, expected=401
   494        )
   495
   496    def check(self):
   497        # [0]
   498        # Strange gotcha: it looks like we always get an integer code here
   499        # even though the field specifier above is wrapped in single quotes.
   500        assert self.results[0].json == {
   501            "custom_error": "truth",
   502            "code": 404,
   503        }, f"unexpected response body: {self.results[0].json}"
   504        assert self.results[0].headers["Content-Type"] == [
   505            "application/json"
   506        ], f"unexpected Content-Type: {self.results[0].headers}"
   507
   508        # [1]
   509        assert self.results[1].json == {
   510            "custom_error": "yep",
   511            "toofast": "definitely",
   512            "code": "code was 429",
   513        }, f"unexpected response body: {self.results[1].json}"
   514        assert self.results[1].headers["Content-Type"] == [
   515            "application/json"
   516        ], f"unexpected Content-Type: {self.results[1].headers}"
   517
   518        # [2]
   519        assert self.results[2].json == {
   520            "error": "unauthorized"
   521        }, f"unexpected response body: {self.results[2].json}"
   522        assert self.results[2].headers["Content-Type"] == [
   523            "application/json"
   524        ], f"unexpected Content-Type: {self.results[2].headers}"
   525
   526
   527class ErrorResponseReturnBodyTextSource(AmbassadorTest):
   528    """
   529    Check that we can return a customized error response where the body is built as a formatted string.
   530    """
   531
   532    def init(self):
   533        self.target = HTTP()
   534
   535    def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
   536        yield self, f"""
   537---
   538apiVersion: getambassador.io/v3alpha1
   539kind: Module
   540name: ambassador
   541ambassador_id: ["{self.ambassador_id}"]
   542config:
   543  error_response_overrides:
   544  - on_status_code: 500
   545    body:
   546      text_format_source:
   547        filename: '/etc/issue'
   548      content_type: 'application/etcissue'
   549  - on_status_code: 503
   550    body:
   551      text_format_source:
   552        filename: '/etc/motd'
   553      content_type: 'application/motd'
   554  - on_status_code: 504
   555    body:
   556      text_format_source:
   557        filename: '/etc/shells'
   558---
   559apiVersion: getambassador.io/v3alpha1
   560kind: Mapping
   561name:  {self.target.path.k8s}
   562ambassador_id: ["{self.ambassador_id}"]
   563hostname: "*"
   564prefix: /target/
   565service: {self.target.path.fqdn}
   566"""
   567
   568    def queries(self):
   569        # [0]
   570        yield Query(
   571            self.url("target/"), headers={"kat-req-http-requested-status": "500"}, expected=500
   572        )
   573
   574        # [1]
   575        yield Query(
   576            self.url("target/"), headers={"kat-req-http-requested-status": "503"}, expected=503
   577        )
   578
   579        # [2]
   580        yield Query(
   581            self.url("target/"), headers={"kat-req-http-requested-status": "504"}, expected=504
   582        )
   583
   584    def check(self):
   585        # [0] Sorry for using /etc/issue...
   586        print("headers = %s" % self.results[0].headers)
   587        assert (
   588            "Welcome to Alpine Linux" in self.results[0].text
   589        ), f"unexpected response body: {self.results[0].text}"
   590        assert self.results[0].headers["Content-Type"] == [
   591            "application/etcissue"
   592        ], f"unexpected Content-Type: {self.results[0].headers}"
   593
   594        # [1] ...and sorry for using /etc/motd...
   595        assert (
   596            "You may change this message by editing /etc/motd." in self.results[1].text
   597        ), f"unexpected response body: {self.results[1].text}"
   598        assert self.results[1].headers["Content-Type"] == [
   599            "application/motd"
   600        ], f"unexpected Content-Type: {self.results[1].headers}"
   601
   602        # [2] ...and sorry for using /etc/shells
   603        assert (
   604            "# valid login shells" in self.results[2].text
   605        ), f"unexpected response body: {self.results[2].text}"
   606        assert self.results[2].headers["Content-Type"] == [
   607            "text/plain"
   608        ], f"unexpected Content-Type: {self.results[2].headers}"
   609
   610
   611class ErrorResponseMappingBypass(AmbassadorTest):
   612    """
   613    Check that we can return a bypass custom error responses at the mapping level
   614    """
   615
   616    def init(self):
   617        self.target = HTTP()
   618
   619    def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
   620        yield self, f"""
   621---
   622apiVersion: getambassador.io/v3alpha1
   623kind: Module
   624name: ambassador
   625ambassador_id: ["{self.ambassador_id}"]
   626config:
   627  error_response_overrides:
   628  - on_status_code: 404
   629    body:
   630      text_format: 'this is a custom 404 response'
   631      content_type: 'text/custom'
   632  - on_status_code: 418
   633    body:
   634      text_format: 'bad teapot request'
   635  - on_status_code: 503
   636    body:
   637      text_format: 'the upstream is not happy'
   638---
   639apiVersion: getambassador.io/v3alpha1
   640kind: Mapping
   641name:  {self.target.path.k8s}
   642ambassador_id: ["{self.ambassador_id}"]
   643hostname: "*"
   644prefix: /target/
   645service: {self.target.path.fqdn}
   646---
   647apiVersion: getambassador.io/v3alpha1
   648kind: Mapping
   649name:  {self.target.path.k8s}-invalidservice
   650ambassador_id: ["{self.ambassador_id}"]
   651hostname: "*"
   652prefix: /target/invalidservice
   653service: {self.target.path.fqdn}-invalidservice
   654---
   655apiVersion: getambassador.io/v3alpha1
   656kind: Mapping
   657name:  {self.target.path.k8s}-bypass
   658ambassador_id: ["{self.ambassador_id}"]
   659hostname: "*"
   660prefix: /bypass/
   661service: {self.target.path.fqdn}
   662bypass_error_response_overrides: true
   663---
   664apiVersion: getambassador.io/v3alpha1
   665kind: Mapping
   666name:  {self.target.path.k8s}-target-bypass
   667ambassador_id: ["{self.ambassador_id}"]
   668hostname: "*"
   669prefix: /target/bypass/
   670service: {self.target.path.fqdn}
   671bypass_error_response_overrides: true
   672---
   673apiVersion: getambassador.io/v3alpha1
   674kind: Mapping
   675name:  {self.target.path.k8s}-bypass-invalidservice
   676ambassador_id: ["{self.ambassador_id}"]
   677hostname: "*"
   678prefix: /bypass/invalidservice
   679service: {self.target.path.fqdn}-invalidservice
   680bypass_error_response_overrides: true
   681"""
   682
   683    def queries(self):
   684        # [0]
   685        yield Query(
   686            self.url("bypass/"), headers={"kat-req-http-requested-status": "404"}, expected=404
   687        )
   688        # [1]
   689        yield Query(
   690            self.url("target/"), headers={"kat-req-http-requested-status": "404"}, expected=404
   691        )
   692        # [2]
   693        yield Query(
   694            self.url("target/bypass/"),
   695            headers={"kat-req-http-requested-status": "418"},
   696            expected=418,
   697        )
   698        # [3]
   699        yield Query(
   700            self.url("target/"), headers={"kat-req-http-requested-status": "418"}, expected=418
   701        )
   702        # [4]
   703        yield Query(self.url("target/invalidservice"), expected=503)
   704        # [5]
   705        yield Query(self.url("bypass/invalidservice"), expected=503)
   706        # [6]
   707        yield Query(
   708            self.url("bypass/"), headers={"kat-req-http-requested-status": "503"}, expected=503
   709        )
   710        # [7]
   711        yield Query(
   712            self.url("target/"), headers={"kat-req-http-requested-status": "503"}, expected=503
   713        )
   714        # [8]
   715        yield Query(self.url("bypass/"), headers={"kat-req-http-requested-status": "200"})
   716        # [9]
   717        yield Query(self.url("target/"), headers={"kat-req-http-requested-status": "200"})
   718
   719    def check(self):
   720        # [0]
   721        assert self.results[0].text is None, f"unexpected response body: {self.results[0].text}"
   722
   723        # [1]
   724        assert (
   725            self.results[1].text == "this is a custom 404 response"
   726        ), f"unexpected response body: {self.results[1].text}"
   727        assert self.results[1].headers["Content-Type"] == [
   728            "text/custom"
   729        ], f"unexpected Content-Type: {self.results[1].headers}"
   730
   731        # [2]
   732        assert self.results[2].text is None, f"unexpected response body: {self.results[2].text}"
   733
   734        # [3]
   735        assert (
   736            self.results[3].text == "bad teapot request"
   737        ), f"unexpected response body: {self.results[3].text}"
   738
   739        # [4]
   740        assert (
   741            self.results[4].text == "the upstream is not happy"
   742        ), f"unexpected response body: {self.results[4].text}"
   743
   744        # [5]
   745        assert (
   746            self.results[5].text == "no healthy upstream"
   747        ), f"unexpected response body: {self.results[5].text}"
   748        assert self.results[5].headers["Content-Type"] == [
   749            "text/plain"
   750        ], f"unexpected Content-Type: {self.results[5].headers}"
   751
   752        # [6]
   753        assert self.results[6].text is None, f"unexpected response body: {self.results[6].text}"
   754
   755        # [7]
   756        assert (
   757            self.results[7].text == "the upstream is not happy"
   758        ), f"unexpected response body: {self.results[7].text}"
   759
   760        # [8]
   761        assert self.results[8].text is None, f"unexpected response body: {self.results[8].text}"
   762
   763        # [9]
   764        assert self.results[9].text is None, f"unexpected response body: {self.results[9].text}"
   765
   766
   767class ErrorResponseMappingBypassAlternate(AmbassadorTest):
   768    """
   769    Check that we can alternate between serving a custom error response and not
   770    serving one. This is a baseline sanity check against Envoy's response map
   771    filter incorrectly persisting state across filter chain iterations.
   772    """
   773
   774    def init(self):
   775        self.target = HTTP()
   776
   777    def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
   778        yield self, f"""
   779---
   780apiVersion: getambassador.io/v3alpha1
   781kind: Module
   782name: ambassador
   783ambassador_id: ["{self.ambassador_id}"]
   784config:
   785  error_response_overrides:
   786  - on_status_code: 404
   787    body:
   788      text_format: 'this is a custom 404 response'
   789      content_type: 'text/custom'
   790---
   791apiVersion: getambassador.io/v3alpha1
   792kind: Mapping
   793name:  {self.target.path.k8s}
   794ambassador_id: ["{self.ambassador_id}"]
   795hostname: "*"
   796prefix: /target/
   797service: {self.target.path.fqdn}
   798---
   799apiVersion: getambassador.io/v3alpha1
   800kind: Mapping
   801name:  {self.target.path.k8s}-invalidservice
   802ambassador_id: ["{self.ambassador_id}"]
   803hostname: "*"
   804prefix: /target/invalidservice
   805service: {self.target.path.fqdn}-invalidservice
   806---
   807apiVersion: getambassador.io/v3alpha1
   808kind: Mapping
   809name:  {self.target.path.k8s}-bypass
   810ambassador_id: ["{self.ambassador_id}"]
   811hostname: "*"
   812prefix: /bypass/
   813service: {self.target.path.fqdn}
   814bypass_error_response_overrides: true
   815"""
   816
   817    def queries(self):
   818        # [0]
   819        yield Query(
   820            self.url("target/"), headers={"kat-req-http-requested-status": "404"}, expected=404
   821        )
   822        # [1]
   823        yield Query(
   824            self.url("bypass/"), headers={"kat-req-http-requested-status": "404"}, expected=404
   825        )
   826        # [2]
   827        yield Query(
   828            self.url("target/"), headers={"kat-req-http-requested-status": "404"}, expected=404
   829        )
   830
   831    def check(self):
   832        # [0]
   833        assert (
   834            self.results[0].text == "this is a custom 404 response"
   835        ), f"unexpected response body: {self.results[0].text}"
   836        assert self.results[0].headers["Content-Type"] == [
   837            "text/custom"
   838        ], f"unexpected Content-Type: {self.results[0].headers}"
   839
   840        # [1]
   841        assert self.results[1].text is None, f"unexpected response body: {self.results[1].text}"
   842
   843        # [2]
   844        assert (
   845            self.results[2].text == "this is a custom 404 response"
   846        ), f"unexpected response body: {self.results[2].text}"
   847        assert self.results[2].headers["Content-Type"] == [
   848            "text/custom"
   849        ], f"unexpected Content-Type: {self.results[2].headers}"
   850
   851
   852class ErrorResponseMapping404Body(AmbassadorTest):
   853    """
   854    Check that a 404 body is consistent whether error response overrides exist or not
   855    """
   856
   857    def init(self):
   858        self.target = HTTP()
   859
   860    def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
   861        yield self, f"""
   862---
   863apiVersion: getambassador.io/v3alpha1
   864kind: Module
   865name: ambassador
   866ambassador_id: ["{self.ambassador_id}"]
   867config:
   868  error_response_overrides:
   869  - on_status_code: 401
   870    body:
   871      text_format: 'this is a custom 401 response'
   872      content_type: 'text/custom'
   873---
   874apiVersion: getambassador.io/v3alpha1
   875kind: Mapping
   876name:  {self.target.path.k8s}
   877ambassador_id: ["{self.ambassador_id}"]
   878hostname: "*"
   879prefix: /target/
   880service: {self.target.path.fqdn}
   881---
   882apiVersion: getambassador.io/v3alpha1
   883kind: Mapping
   884name:  {self.target.path.k8s}-bypass
   885ambassador_id: ["{self.ambassador_id}"]
   886hostname: "*"
   887prefix: /bypass/
   888service: {self.target.path.fqdn}
   889bypass_error_response_overrides: true
   890---
   891apiVersion: getambassador.io/v3alpha1
   892kind: Mapping
   893name:  {self.target.path.k8s}-overrides
   894ambassador_id: ["{self.ambassador_id}"]
   895hostname: "*"
   896prefix: /overrides/
   897service: {self.target.path.fqdn}
   898error_response_overrides:
   899- on_status_code: 503
   900  body:
   901    text_format: 'custom 503'
   902"""
   903
   904    def queries(self):
   905        # [0]
   906        yield Query(self.url("does-not-exist/"), expected=404)
   907        # [1]
   908        yield Query(
   909            self.url("target/"), headers={"kat-req-http-requested-status": "404"}, expected=404
   910        )
   911        # [2]
   912        yield Query(
   913            self.url("bypass/"), headers={"kat-req-http-requested-status": "404"}, expected=404
   914        )
   915        # [3]
   916        yield Query(
   917            self.url("overrides/"), headers={"kat-req-http-requested-status": "404"}, expected=404
   918        )
   919
   920    def check(self):
   921        # [0] does not match the error response mapping, so no 404 response.
   922        # when envoy directly replies with 404, we see it as an empty string.
   923        assert self.results[0].text == "", f"unexpected response body: {self.results[0].text}"
   924
   925        # [1]
   926        assert self.results[1].text is None, f"unexpected response body: {self.results[1].text}"
   927
   928        # [2]
   929        assert self.results[2].text is None, f"unexpected response body: {self.results[2].text}"
   930
   931        # [3]
   932        assert self.results[3].text is None, f"unexpected response body: {self.results[3].text}"
   933
   934
   935class ErrorResponseMappingOverride(AmbassadorTest):
   936    """
   937    Check that we can return a custom error responses at the mapping level
   938    """
   939
   940    def init(self):
   941        self.target = HTTP()
   942
   943    def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
   944        yield self, f"""
   945---
   946apiVersion: getambassador.io/v3alpha1
   947kind: Module
   948name: ambassador
   949ambassador_id: ["{self.ambassador_id}"]
   950config:
   951  error_response_overrides:
   952  - on_status_code: 401
   953    body:
   954      text_format: 'this is a custom 401 response'
   955      content_type: 'text/custom'
   956  - on_status_code: 503
   957    body:
   958      text_format: 'the upstream is not happy'
   959  - on_status_code: 504
   960    body:
   961      text_format: 'the upstream took a really long time'
   962---
   963apiVersion: getambassador.io/v3alpha1
   964kind: Mapping
   965name:  {self.target.path.k8s}
   966ambassador_id: ["{self.ambassador_id}"]
   967hostname: "*"
   968prefix: /target/
   969service: {self.target.path.fqdn}
   970---
   971apiVersion: getambassador.io/v3alpha1
   972kind: Mapping
   973name:  {self.target.path.k8s}-override-401
   974ambassador_id: ["{self.ambassador_id}"]
   975hostname: "*"
   976prefix: /override/401/
   977service: {self.target.path.fqdn}
   978error_response_overrides:
   979- on_status_code: 401
   980  body:
   981    json_format:
   982      x: "1"
   983      status: '%RESPONSE_CODE%'
   984---
   985apiVersion: getambassador.io/v3alpha1
   986kind: Mapping
   987name:  {self.target.path.k8s}-override-503
   988ambassador_id: ["{self.ambassador_id}"]
   989hostname: "*"
   990prefix: /override/503/
   991service: {self.target.path.fqdn}
   992error_response_overrides:
   993- on_status_code: 503
   994  body:
   995    json_format:
   996      "y": "2"
   997      status: '%RESPONSE_CODE%'
   998"""
   999
  1000    def queries(self):
  1001        # [0] Should match module's on_response_code 401
  1002        yield Query(
  1003            self.url("target/"), headers={"kat-req-http-requested-status": "401"}, expected=401
  1004        )
  1005
  1006        # [1] Should match mapping-specific on_response_code 401
  1007        yield Query(
  1008            self.url("override/401/"),
  1009            headers={"kat-req-http-requested-status": "401"},
  1010            expected=401,
  1011        )
  1012
  1013        # [2] Should match mapping-specific on_response_code 503
  1014        yield Query(
  1015            self.url("override/503/"),
  1016            headers={"kat-req-http-requested-status": "503"},
  1017            expected=503,
  1018        )
  1019
  1020        # [3] Should not match mapping-specific rule, therefore no rewrite
  1021        yield Query(
  1022            self.url("override/401/"),
  1023            headers={"kat-req-http-requested-status": "503"},
  1024            expected=503,
  1025        )
  1026
  1027        # [4] Should not match mapping-specific rule, therefore no rewrite
  1028        yield Query(
  1029            self.url("override/503/"),
  1030            headers={"kat-req-http-requested-status": "401"},
  1031            expected=401,
  1032        )
  1033
  1034        # [5] Should not match mapping-specific rule, therefore no rewrite
  1035        yield Query(
  1036            self.url("override/401/"),
  1037            headers={"kat-req-http-requested-status": "504"},
  1038            expected=504,
  1039        )
  1040
  1041        # [6] Should not match mapping-specific rule, therefore no rewrite
  1042        yield Query(
  1043            self.url("override/503/"),
  1044            headers={"kat-req-http-requested-status": "504"},
  1045            expected=504,
  1046        )
  1047
  1048        # [7] Should match module's on_response_code 503
  1049        yield Query(
  1050            self.url("target/"), headers={"kat-req-http-requested-status": "503"}, expected=503
  1051        )
  1052
  1053        # [8] Should match module's on_response_code 504
  1054        yield Query(
  1055            self.url("target/"), headers={"kat-req-http-requested-status": "504"}, expected=504
  1056        )
  1057
  1058    def check(self):
  1059        # [0] Module's 401 rule with custom header
  1060        assert (
  1061            self.results[0].text == "this is a custom 401 response"
  1062        ), f"unexpected response body: {self.results[0].text}"
  1063        assert self.results[0].headers["Content-Type"] == [
  1064            "text/custom"
  1065        ], f"unexpected Content-Type: {self.results[0].headers}"
  1066
  1067        # [1] Mapping's 401 rule with json response
  1068        assert self.results[1].json == {
  1069            "x": "1",
  1070            "status": 401,
  1071        }, f"unexpected response body: {self.results[1].json}"
  1072        assert self.results[1].headers["Content-Type"] == [
  1073            "application/json"
  1074        ], f"unexpected Content-Type: {self.results[1].headers}"
  1075
  1076        # [2] Mapping's 503 rule with json response
  1077        assert self.results[2].json == {
  1078            "y": "2",
  1079            "status": 503,
  1080        }, f"unexpected response body: {self.results[2].json}"
  1081        assert self.results[2].headers["Content-Type"] == [
  1082            "application/json"
  1083        ], f"unexpected Content-Type: {self.results[2].headers}"
  1084
  1085        # [3] Mapping has 401 rule, but response code is 503, no rewrite.
  1086        assert self.results[3].text is None, f"unexpected response body: {self.results[3].text}"
  1087
  1088        # [4] Mapping has 503 rule, but response code is 401, no rewrite.
  1089        assert self.results[4].text is None, f"unexpected response body: {self.results[4].text}"
  1090
  1091        # [5] Mapping has 401 rule, but response code is 504, no rewrite.
  1092        assert self.results[5].text is None, f"unexpected response body: {self.results[5].text}"
  1093
  1094        # [6] Mapping has 503 rule, but response code is 504, no rewrite.
  1095        assert self.results[6].text is None, f"unexpected response body: {self.results[6].text}"
  1096
  1097        # [7] Module's 503 rule, no custom header
  1098        assert (
  1099            self.results[7].text == "the upstream is not happy"
  1100        ), f"unexpected response body: {self.results[7].text}"
  1101        assert self.results[7].headers["Content-Type"] == [
  1102            "text/plain"
  1103        ], f"unexpected Content-Type: {self.results[7].headers}"
  1104
  1105        # [8] Module's 504 rule, no custom header
  1106        assert (
  1107            self.results[8].text == "the upstream took a really long time"
  1108        ), f"unexpected response body: {self.results[8].text}"
  1109        assert self.results[8].headers["Content-Type"] == [
  1110            "text/plain"
  1111        ], f"unexpected Content-Type: {self.results[8].headers}"
  1112
  1113
  1114class ErrorResponseSeveralMappings(AmbassadorTest):
  1115    """
  1116    Check that we can specify separate error response overrides on two mappings with no Module
  1117    config
  1118    """
  1119
  1120    def init(self):
  1121        self.target = HTTP()
  1122
  1123    def manifests(self) -> str:
  1124        return (
  1125            super().manifests()
  1126            + f"""
  1127---
  1128apiVersion: getambassador.io/v3alpha1
  1129kind: Mapping
  1130metadata:
  1131  name:  {self.target.path.k8s}-one
  1132spec:
  1133  ambassador_id: ["{self.ambassador_id}"]
  1134  hostname: "*"
  1135  prefix: /target-one/
  1136  service: {self.target.path.fqdn}
  1137  error_response_overrides:
  1138  - on_status_code: 404
  1139    body:
  1140      text_format: '%RESPONSE_CODE% from first mapping'
  1141  - on_status_code: 504
  1142    body:
  1143      text_format: 'a custom 504 response'
  1144---
  1145apiVersion: getambassador.io/v3alpha1
  1146kind: Mapping
  1147metadata:
  1148  name: {self.target.path.k8s}-two
  1149spec:
  1150  ambassador_id: ["{self.ambassador_id}"]
  1151  hostname: "*"
  1152  prefix: /target-two/
  1153  service: {self.target.path.fqdn}
  1154  error_response_overrides:
  1155  - on_status_code: 404
  1156    body:
  1157      text_format: '%RESPONSE_CODE% from second mapping'
  1158  - on_status_code: 429
  1159    body:
  1160      text_format: 'a custom 429 response'
  1161---
  1162apiVersion: getambassador.io/v3alpha1
  1163kind: Mapping
  1164metadata:
  1165  name: {self.target.path.k8s}-three
  1166spec:
  1167  ambassador_id: ["{self.ambassador_id}"]
  1168  hostname: "*"
  1169  prefix: /target-three/
  1170  service: {self.target.path.fqdn}
  1171---
  1172apiVersion: getambassador.io/v3alpha1
  1173kind: Mapping
  1174metadata:
  1175  name: {self.target.path.k8s}-four
  1176spec:
  1177  ambassador_id: ["{self.ambassador_id}"]
  1178  hostname: "*"
  1179  prefix: /target-four/
  1180  service: {self.target.path.fqdn}
  1181  error_response_overrides:
  1182  - on_status_code: 500
  1183    body:
  1184      text_format: '500 is a bad status code'
  1185"""
  1186        )
  1187
  1188    _queries = [
  1189        {"url": "does-not-exist/", "status": 404, "text": ""},
  1190        {"url": "target-one/", "status": 404, "text": "404 from first mapping"},
  1191        {"url": "target-one/", "status": 429, "text": None},
  1192        {"url": "target-one/", "status": 504, "text": "a custom 504 response"},
  1193        {"url": "target-two/", "status": 404, "text": "404 from second mapping"},
  1194        {"url": "target-two/", "status": 429, "text": "a custom 429 response"},
  1195        {"url": "target-two/", "status": 504, "text": None},
  1196        {"url": "target-three/", "status": 404, "text": None},
  1197        {"url": "target-three/", "status": 429, "text": None},
  1198        {"url": "target-three/", "status": 504, "text": None},
  1199        {"url": "target-four/", "status": 404, "text": None},
  1200        {"url": "target-four/", "status": 429, "text": None},
  1201        {"url": "target-four/", "status": 504, "text": None},
  1202    ]
  1203
  1204    def queries(self):
  1205        for x in self._queries:
  1206            yield Query(
  1207                self.url(x["url"]),
  1208                headers={"kat-req-http-requested-status": str(x["status"])},
  1209                expected=x["status"],
  1210            )
  1211
  1212    def check(self):
  1213        for i in range(len(self._queries)):
  1214            expected = self._queries[i]["text"]
  1215            res = self.results[i]
  1216            assert (
  1217                res.text == expected
  1218            ), f'unexpected response body on query {i}: "{res.text}", wanted "{expected}"'

View as plain text