...

Text file src/github.com/datawire/ambassador/v2/python/tests/kat/t_error_response.py

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

View as plain text