From 61f386a253033f795ca9857971ff30dd786b0b15 Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Sun, 9 Aug 2026 18:19:29 +0100 Subject: [PATCH 1/5] Return 404 for requests to unrouted paths The Flask app's ``validate_request`` before_request hook ran for requests which match no route, because Flask runs before_request handlers before it raises the routing error. ``validate_keys`` then unpacked an empty generator and raised a ``ValueError``, so any authenticated request to an unknown path, or to a known path with a method it does not serve, crashed the Flask and Docker backends. Skip validation when Flask has matched no route, so Flask raises its own routing error: 404 for an unknown path, as real Vuforia returns, and 405 for an unserved method. Closes #3368 Co-Authored-By: Claude Opus 5 (1M context) --- docs/source/differences-to-vws.rst | 15 +++++ newsfragments/unrouted-requests.change | 1 + src/mock_vws/_flask_server/vws.py | 6 ++ tests/mock_vws/test_flask_app_usage.py | 82 ++++++++++++++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 newsfragments/unrouted-requests.change diff --git a/docs/source/differences-to-vws.rst b/docs/source/differences-to-vws.rst index 3281974b7..13f5f006b 100644 --- a/docs/source/differences-to-vws.rst +++ b/docs/source/differences-to-vws.rst @@ -318,6 +318,21 @@ signature. The 404 has not been verified, because no request for a real report has caught one before it was generated. +Paths which the mock does not serve +----------------------------------- + +Real Vuforia returns a 404 response for a request to a path which it does not +serve. + +The Flask and Docker mock does the same, with a Flask error page as the body, +and it returns a 405 response for a request to a served path with a method +which that path does not serve. +Neither response body has been verified against real Vuforia. + +The ``requests`` and ``httpx`` backends mock only the paths which the mock +serves, so a request to any other path raises a connection error rather than +returning a response. + Header cases ------------ diff --git a/newsfragments/unrouted-requests.change b/newsfragments/unrouted-requests.change new file mode 100644 index 000000000..38346a83a --- /dev/null +++ b/newsfragments/unrouted-requests.change @@ -0,0 +1 @@ +Return a 404 response from the Flask and Docker mock for a request to a path which it does not serve, and a 405 response for a request to a served path with a method which that path does not serve, rather than raising an error. diff --git a/src/mock_vws/_flask_server/vws.py b/src/mock_vws/_flask_server/vws.py index 7933be0b9..67145e486 100644 --- a/src/mock_vws/_flask_server/vws.py +++ b/src/mock_vws/_flask_server/vws.py @@ -199,7 +199,13 @@ def validate_request() -> None: Reco counts report downloads stand in for presigned URLs, which are not authorized with VWS credentials. + + Flask runs ``before_request`` handlers before it raises a routing error, + so requests which match no route reach this function. + Those requests are left to Flask, which raises the routing error itself. """ + if request.url_rule is None: + return if request.endpoint == "generate_vumark_instance": return if ( diff --git a/tests/mock_vws/test_flask_app_usage.py b/tests/mock_vws/test_flask_app_usage.py index 60742fc46..37d18cfb3 100644 --- a/tests/mock_vws/test_flask_app_usage.py +++ b/tests/mock_vws/test_flask_app_usage.py @@ -246,6 +246,88 @@ def test_per_endpoint_limits() -> None: client.get_database_summary_report() +class TestUnroutedRequests: + """Tests for requests which the Flask app does not route. + + These tests use the Flask test client because the ``responses`` + library intercepts only the paths and methods which the app routes, + so requests to any other path never reach the app. + """ + + @staticmethod + def _signed_headers( + *, + database: CloudDatabase, + method: HTTPMethod, + request_path: str, + ) -> dict[str, str]: + """Return headers which sign a request with valid server keys.""" + date = rfc_1123_date() + authorization_string = authorization_header( + access_key=database.server_access_key, + secret_key=database.server_secret_key, + method=method, + content=b"", + content_type="", + date=date, + request_path=request_path, + ) + return {"Authorization": authorization_string, "Date": date} + + def test_unknown_path(self) -> None: + """A request to a path which is not routed returns a 404.""" + database = CloudDatabase() + databases_url = _EXAMPLE_URL_FOR_TARGET_MANAGER + "/cloud_databases" + requests.post(url=databases_url, json=database.to_dict(), timeout=30) + + request_path = "/some-random-endpoint" + headers = self._signed_headers( + database=database, + method=HTTPMethod.GET, + request_path=request_path, + ) + + response = VWS_FLASK_APP.test_client().get( + request_path, + headers=headers, + ) + + assert response.status_code == HTTPStatus.NOT_FOUND + + def test_unknown_method(self) -> None: + """A request to a routed path with a method which that path does + not serve returns a 405. + """ + database = CloudDatabase() + databases_url = _EXAMPLE_URL_FOR_TARGET_MANAGER + "/cloud_databases" + requests.post(url=databases_url, json=database.to_dict(), timeout=30) + + request_path = "/summary" + headers = self._signed_headers( + database=database, + method=HTTPMethod.POST, + request_path=request_path, + ) + + response = VWS_FLASK_APP.test_client().post( + request_path, + headers=headers, + ) + + assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED + + @staticmethod + def test_unauthenticated_unknown_path() -> None: + """A request to a path which is not routed returns a 404 even + without credentials. + + The Docker health check relies on this request not erroring. + """ + response = VWS_FLASK_APP.test_client().get("/some-random-endpoint") + + assert response.status_code == HTTPStatus.NOT_FOUND + + class TestAddCloudDatabase: """Tests for adding cloud databases to the mock.""" From fd73dd98da2cefaea56277d0d45229f8ec11dbef Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Sun, 9 Aug 2026 22:23:52 +0100 Subject: [PATCH 2/5] Reword docstring to satisfy the pylint spelling check Co-Authored-By: Claude Opus 5 (1M context) --- tests/mock_vws/test_flask_app_usage.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/mock_vws/test_flask_app_usage.py b/tests/mock_vws/test_flask_app_usage.py index 37d18cfb3..1bd92e218 100644 --- a/tests/mock_vws/test_flask_app_usage.py +++ b/tests/mock_vws/test_flask_app_usage.py @@ -321,7 +321,8 @@ def test_unauthenticated_unknown_path() -> None: """A request to a path which is not routed returns a 404 even without credentials. - The Docker health check relies on this request not erroring. + The Docker health check relies on this request returning a + response. """ response = VWS_FLASK_APP.test_client().get("/some-random-endpoint") From 29d9f56e3b4819e1bd4db6dba282844259e9f851 Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Sun, 9 Aug 2026 22:41:07 +0100 Subject: [PATCH 3/5] Verify the unrouted request responses against real Vuforia Real Vuforia returns a 404 response both for a request to a path which it does not serve and for a request to a served path with a method which that path does not serve; it does not return a 405. Make the Flask app return a 404 with no body in both cases, rather than Flask's 404 page or a 405. Add verified fake tests which run against real Vuforia and the mocks, and record in the differences documentation which bodies real Vuforia gives. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/test.yml | 1 + docs/source/differences-to-vws.rst | 18 ++- newsfragments/unrouted-requests.change | 2 +- src/mock_vws/_flask_server/vws.py | 21 +++- tests/mock_vws/test_flask_app_usage.py | 68 +---------- tests/mock_vws/test_unrouted_requests.py | 146 +++++++++++++++++++++++ 6 files changed, 183 insertions(+), 73 deletions(-) create mode 100644 tests/mock_vws/test_unrouted_requests.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c1723a289..0e8046a87 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -92,6 +92,7 @@ jobs: - tests/mock_vws/test_target_raters.py - tests/mock_vws/test_target_summary.py - tests/mock_vws/test_unexpected_json.py + - tests/mock_vws/test_unrouted_requests.py - tests/mock_vws/test_update_target.py::TestActiveFlag - tests/mock_vws/test_update_target.py::TestApplicationMetadata::test_base64_encoded - tests/mock_vws/test_update_target.py::TestApplicationMetadata::test_invalid_type diff --git a/docs/source/differences-to-vws.rst b/docs/source/differences-to-vws.rst index 13f5f006b..be1b93fd1 100644 --- a/docs/source/differences-to-vws.rst +++ b/docs/source/differences-to-vws.rst @@ -322,12 +322,18 @@ Paths which the mock does not serve ----------------------------------- Real Vuforia returns a 404 response for a request to a path which it does not -serve. - -The Flask and Docker mock does the same, with a Flask error page as the body, -and it returns a 405 response for a request to a served path with a method -which that path does not serve. -Neither response body has been verified against real Vuforia. +serve, and for a request to a served path with a method which that path does +not serve. +It does not return a 405 response. +The Flask and Docker mock does the same, with an empty body and no +``Content-Type`` header. + +Real Vuforia gives an empty body only for a request to a path which does not +start with a served path, such as ``/some-random-endpoint``. +For any other request which it does not serve, such as ``DELETE /summary`` or +``GET /targetsfoo``, it gives an HTML "Not Found" page which names the method +and the path of the request. +The mock gives an empty body for all of these. The ``requests`` and ``httpx`` backends mock only the paths which the mock serves, so a request to any other path raises a connection error rather than diff --git a/newsfragments/unrouted-requests.change b/newsfragments/unrouted-requests.change index 38346a83a..28ab5d718 100644 --- a/newsfragments/unrouted-requests.change +++ b/newsfragments/unrouted-requests.change @@ -1 +1 @@ -Return a 404 response from the Flask and Docker mock for a request to a path which it does not serve, and a 405 response for a request to a served path with a method which that path does not serve, rather than raising an error. +Return a 404 response from the Flask and Docker mock for a request to a path which it does not serve, and for a request to a served path with a method which that path does not serve, as real Vuforia does, rather than raising an error. diff --git a/src/mock_vws/_flask_server/vws.py b/src/mock_vws/_flask_server/vws.py index 67145e486..f0548de2d 100644 --- a/src/mock_vws/_flask_server/vws.py +++ b/src/mock_vws/_flask_server/vws.py @@ -18,6 +18,7 @@ from beartype import beartype from flask import Flask, Response, request from pydantic_settings import BaseSettings +from werkzeug.exceptions import MethodNotAllowed, NotFound from mock_vws._constants import ( VUMARK_PDF, @@ -202,7 +203,8 @@ def validate_request() -> None: Flask runs ``before_request`` handlers before it raises a routing error, so requests which match no route reach this function. - Those requests are left to Flask, which raises the routing error itself. + Those requests are left to Flask, which raises the routing error, and + ``handle_unrouted_request`` turns that into a response. """ if request.url_rule is None: return @@ -248,6 +250,23 @@ def handle_exceptions(exc: ValidatorError) -> Response: return response +@VWS_FLASK_APP.errorhandler(code_or_exception=HTTPStatus.NOT_FOUND) +@VWS_FLASK_APP.errorhandler(code_or_exception=HTTPStatus.METHOD_NOT_ALLOWED) +@beartype +def handle_unrouted_request(exc: NotFound | MethodNotAllowed) -> Response: + """Return a 404 response with no body for a request which no route + serves. + + Real Vuforia returns a 404 response for a request to a path which it does + not serve, and for a request to a served path with a method which that + path does not serve. + """ + del exc + response = Response(status=HTTPStatus.NOT_FOUND, response=b"") + del response.headers["Content-Type"] + return response + + @VWS_FLASK_APP.route(rule="/oauth2/token", methods=[HTTPMethod.POST]) @beartype def oauth2_token() -> Response: diff --git a/tests/mock_vws/test_flask_app_usage.py b/tests/mock_vws/test_flask_app_usage.py index 1bd92e218..29b700df1 100644 --- a/tests/mock_vws/test_flask_app_usage.py +++ b/tests/mock_vws/test_flask_app_usage.py @@ -249,73 +249,11 @@ def test_per_endpoint_limits() -> None: class TestUnroutedRequests: """Tests for requests which the Flask app does not route. - These tests use the Flask test client because the ``responses`` - library intercepts only the paths and methods which the app routes, - so requests to any other path never reach the app. + Requests which are routed are covered by + ``tests/mock_vws/test_unrouted_requests.py``, which verifies the + responses against real Vuforia. """ - @staticmethod - def _signed_headers( - *, - database: CloudDatabase, - method: HTTPMethod, - request_path: str, - ) -> dict[str, str]: - """Return headers which sign a request with valid server keys.""" - date = rfc_1123_date() - authorization_string = authorization_header( - access_key=database.server_access_key, - secret_key=database.server_secret_key, - method=method, - content=b"", - content_type="", - date=date, - request_path=request_path, - ) - return {"Authorization": authorization_string, "Date": date} - - def test_unknown_path(self) -> None: - """A request to a path which is not routed returns a 404.""" - database = CloudDatabase() - databases_url = _EXAMPLE_URL_FOR_TARGET_MANAGER + "/cloud_databases" - requests.post(url=databases_url, json=database.to_dict(), timeout=30) - - request_path = "/some-random-endpoint" - headers = self._signed_headers( - database=database, - method=HTTPMethod.GET, - request_path=request_path, - ) - - response = VWS_FLASK_APP.test_client().get( - request_path, - headers=headers, - ) - - assert response.status_code == HTTPStatus.NOT_FOUND - - def test_unknown_method(self) -> None: - """A request to a routed path with a method which that path does - not serve returns a 405. - """ - database = CloudDatabase() - databases_url = _EXAMPLE_URL_FOR_TARGET_MANAGER + "/cloud_databases" - requests.post(url=databases_url, json=database.to_dict(), timeout=30) - - request_path = "/summary" - headers = self._signed_headers( - database=database, - method=HTTPMethod.POST, - request_path=request_path, - ) - - response = VWS_FLASK_APP.test_client().post( - request_path, - headers=headers, - ) - - assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED - @staticmethod def test_unauthenticated_unknown_path() -> None: """A request to a path which is not routed returns a 404 even diff --git a/tests/mock_vws/test_unrouted_requests.py b/tests/mock_vws/test_unrouted_requests.py new file mode 100644 index 000000000..3a822bca4 --- /dev/null +++ b/tests/mock_vws/test_unrouted_requests.py @@ -0,0 +1,146 @@ +"""Verified fake tests for requests which VWS does not serve. + +These cover requests to a path which VWS does not serve, and requests to a +served path with a method which that path does not serve. +""" + +from dataclasses import dataclass +from http import HTTPMethod, HTTPStatus + +import pytest +import requests +from beartype import beartype +from vws_auth_tools import authorization_header, rfc_1123_date + +from mock_vws._flask_server.vws import VWS_FLASK_APP +from mock_vws.database import CloudDatabase +from tests.mock_vws.fixtures.vuforia_backends import VuforiaBackend + +_VWS_HOST = "https://vws.vuforia.com" + + +@beartype +@dataclass(frozen=True, kw_only=True) +class _UnroutedResponse: + """The parts of a response to a request which no route serves.""" + + status_code: int + body: bytes + content_type: str | None + + +@beartype +def _send_unrouted_request( + *, + backend: VuforiaBackend, + vuforia_database: CloudDatabase, + method: HTTPMethod, + request_path: str, +) -> _UnroutedResponse | None: + """Send a signed request which no route serves and return the response. + + ``None`` is returned when the backend refuses the connection rather than + returning a response. + """ + date = rfc_1123_date() + headers = { + "Authorization": authorization_header( + access_key=vuforia_database.server_access_key, + secret_key=vuforia_database.server_secret_key, + method=method, + content=b"", + content_type="", + date=date, + request_path=request_path, + ), + "Date": date, + } + + if backend == VuforiaBackend.DOCKER_IN_MEMORY: + # The ``responses`` library intercepts only the paths and methods + # which the Flask app routes, so requests to any other path never + # reach the app. A running container serves every path, so we drive + # the app with its own test client. + test_client_response = VWS_FLASK_APP.test_client().open( + request_path, + method=method, + headers=headers, + ) + return _UnroutedResponse( + status_code=test_client_response.status_code, + body=test_client_response.data, + content_type=test_client_response.headers.get( + key="Content-Type", + ), + ) + + try: + response = requests.request( + method=method, + url=_VWS_HOST + request_path, + headers=headers, + timeout=30, + ) + except requests.exceptions.ConnectionError: + return None + + return _UnroutedResponse( + status_code=response.status_code, + body=response.content, + content_type=response.headers.get("Content-Type"), + ) + + +@pytest.mark.usefixtures("verify_mock_vuforia") +class TestUnroutedRequests: + """Tests for requests which VWS does not serve.""" + + @staticmethod + def test_unknown_path( + *, + vuforia_database: CloudDatabase, + verify_mock_vuforia: VuforiaBackend, + ) -> None: + """A request to a path which is not served returns a 404 with no + body. + """ + response = _send_unrouted_request( + backend=verify_mock_vuforia, + vuforia_database=vuforia_database, + method=HTTPMethod.GET, + request_path="/some-random-endpoint", + ) + + if verify_mock_vuforia == VuforiaBackend.MOCK: + # The ``requests`` and ``httpx`` backends mock only the paths + # which they serve, so they give no response at all. + assert response is None + return + + assert response is not None + assert response.status_code == HTTPStatus.NOT_FOUND + assert response.body == b"" + assert response.content_type is None + + @staticmethod + def test_unknown_method( + *, + vuforia_database: CloudDatabase, + verify_mock_vuforia: VuforiaBackend, + ) -> None: + """A request to a served path with a method which that path does + not serve returns a 404, rather than a 405. + """ + response = _send_unrouted_request( + backend=verify_mock_vuforia, + vuforia_database=vuforia_database, + method=HTTPMethod.DELETE, + request_path="/summary", + ) + + if verify_mock_vuforia == VuforiaBackend.MOCK: + assert response is None + return + + assert response is not None + assert response.status_code == HTTPStatus.NOT_FOUND From b20a1ca4c5ad6f158f93cc148c61352a85ede04d Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Sun, 9 Aug 2026 23:32:43 +0100 Subject: [PATCH 4/5] Document only the differences for unserved paths Co-Authored-By: Claude Opus 5 (1M context) --- docs/source/differences-to-vws.rst | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/docs/source/differences-to-vws.rst b/docs/source/differences-to-vws.rst index be1b93fd1..d6a56b9ad 100644 --- a/docs/source/differences-to-vws.rst +++ b/docs/source/differences-to-vws.rst @@ -321,23 +321,17 @@ one before it was generated. Paths which the mock does not serve ----------------------------------- -Real Vuforia returns a 404 response for a request to a path which it does not -serve, and for a request to a served path with a method which that path does -not serve. -It does not return a 405 response. -The Flask and Docker mock does the same, with an empty body and no -``Content-Type`` header. - -Real Vuforia gives an empty body only for a request to a path which does not -start with a served path, such as ``/some-random-endpoint``. +Real Vuforia gives an empty body with a 404 response only for a request to a +path which does not start with a served path, such as +``/some-random-endpoint``. For any other request which it does not serve, such as ``DELETE /summary`` or ``GET /targetsfoo``, it gives an HTML "Not Found" page which names the method and the path of the request. -The mock gives an empty body for all of these. +The Flask and Docker mock gives an empty body for all of these. The ``requests`` and ``httpx`` backends mock only the paths which the mock serves, so a request to any other path raises a connection error rather than -returning a response. +giving the 404 response which real Vuforia gives. Header cases ------------ From d0fe0e14bf2b0a12e004df1ba6c8b0686fe227fc Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Mon, 10 Aug 2026 10:33:44 +0100 Subject: [PATCH 5/5] Fold the unrouted request tests into an existing test file Every entry in the CI test matrix uses one of the credentials files in secrets.tar.gpg, and there are exactly as many of those files as there are entries, so a new entry has no database to use and its job fails while copying the file. Move the tests into tests/mock_vws/test_invalid_given_id.py, which already covers requests which name something the API does not serve, rather than adding an entry. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/test.yml | 1 - tests/mock_vws/test_flask_app_usage.py | 4 +- tests/mock_vws/test_invalid_given_id.py | 152 ++++++++++++++++++++++- tests/mock_vws/test_unrouted_requests.py | 146 ---------------------- 4 files changed, 149 insertions(+), 154 deletions(-) delete mode 100644 tests/mock_vws/test_unrouted_requests.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0e8046a87..c1723a289 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -92,7 +92,6 @@ jobs: - tests/mock_vws/test_target_raters.py - tests/mock_vws/test_target_summary.py - tests/mock_vws/test_unexpected_json.py - - tests/mock_vws/test_unrouted_requests.py - tests/mock_vws/test_update_target.py::TestActiveFlag - tests/mock_vws/test_update_target.py::TestApplicationMetadata::test_base64_encoded - tests/mock_vws/test_update_target.py::TestApplicationMetadata::test_invalid_type diff --git a/tests/mock_vws/test_flask_app_usage.py b/tests/mock_vws/test_flask_app_usage.py index 29b700df1..9ff6e2a1a 100644 --- a/tests/mock_vws/test_flask_app_usage.py +++ b/tests/mock_vws/test_flask_app_usage.py @@ -249,8 +249,8 @@ def test_per_endpoint_limits() -> None: class TestUnroutedRequests: """Tests for requests which the Flask app does not route. - Requests which are routed are covered by - ``tests/mock_vws/test_unrouted_requests.py``, which verifies the + Signed requests are covered by + ``tests/mock_vws/test_invalid_given_id.py``, which verifies the responses against real Vuforia. """ diff --git a/tests/mock_vws/test_invalid_given_id.py b/tests/mock_vws/test_invalid_given_id.py index 1d3b49e8c..1081429de 100644 --- a/tests/mock_vws/test_invalid_given_id.py +++ b/tests/mock_vws/test_invalid_given_id.py @@ -1,19 +1,106 @@ -""" -Tests for passing invalid target IDs to endpoints which require a target -ID to -be given. +"""Tests for requests which name something that VWS does not serve. + +These cover an invalid target ID given to an endpoint which requires one, a +path which VWS does not serve, and a served path with a method which that +path does not serve. + +The tests for paths and methods live here, rather than in a file of their +own, because every entry in the CI test matrix uses one of the credentials +files in ``secrets.tar.gpg``, and there are exactly as many of those files as +there are entries. """ -from http import HTTPStatus +from dataclasses import dataclass +from http import HTTPMethod, HTTPStatus import pytest +import requests +from beartype import beartype from vws import VWS +from vws_auth_tools import authorization_header, rfc_1123_date from mock_vws._constants import ResultCodes +from mock_vws._flask_server.vws import VWS_FLASK_APP +from mock_vws.database import CloudDatabase +from tests.mock_vws.fixtures.vuforia_backends import VuforiaBackend from tests.mock_vws.utils import Endpoint from tests.mock_vws.utils.assertions import assert_vws_failure from tests.mock_vws.utils.too_many_requests import handle_server_errors +_VWS_HOST = "https://vws.vuforia.com" + + +@beartype +@dataclass(frozen=True, kw_only=True) +class _UnroutedResponse: + """The parts of a response to a request which no route serves.""" + + status_code: int + body: bytes + content_type: str | None + + +@beartype +def _send_unrouted_request( + *, + backend: VuforiaBackend, + vuforia_database: CloudDatabase, + method: HTTPMethod, + request_path: str, +) -> _UnroutedResponse | None: + """Send a signed request which no route serves and return the response. + + ``None`` is returned when the backend refuses the connection rather than + returning a response. + """ + date = rfc_1123_date() + headers = { + "Authorization": authorization_header( + access_key=vuforia_database.server_access_key, + secret_key=vuforia_database.server_secret_key, + method=method, + content=b"", + content_type="", + date=date, + request_path=request_path, + ), + "Date": date, + } + + if backend == VuforiaBackend.DOCKER_IN_MEMORY: + # The ``responses`` library intercepts only the paths and methods + # which the Flask app routes, so requests to any other path never + # reach the app. A running container serves every path, so we drive + # the app with its own test client. + test_client_response = VWS_FLASK_APP.test_client().open( + request_path, + method=method, + headers=headers, + ) + return _UnroutedResponse( + status_code=test_client_response.status_code, + body=test_client_response.data, + content_type=test_client_response.headers.get( + key="Content-Type", + ), + ) + + try: + response = requests.request( + method=method, + url=_VWS_HOST + request_path, + headers=headers, + timeout=30, + ) + except requests.exceptions.ConnectionError: + return None + + return _UnroutedResponse( + status_code=response.status_code, + body=response.content, + content_type=response.headers.get("Content-Type"), + ) + @pytest.mark.usefixtures("verify_mock_vuforia") class TestInvalidGivenID: @@ -53,3 +140,58 @@ def test_not_real_id( status_code=HTTPStatus.NOT_FOUND, result_code=ResultCodes.UNKNOWN_TARGET, ) + + +@pytest.mark.usefixtures("verify_mock_vuforia") +class TestUnroutedRequests: + """Tests for requests which VWS does not serve.""" + + @staticmethod + def test_unknown_path( + *, + vuforia_database: CloudDatabase, + verify_mock_vuforia: VuforiaBackend, + ) -> None: + """A request to a path which is not served returns a 404 with no + body. + """ + response = _send_unrouted_request( + backend=verify_mock_vuforia, + vuforia_database=vuforia_database, + method=HTTPMethod.GET, + request_path="/some-random-endpoint", + ) + + if verify_mock_vuforia == VuforiaBackend.MOCK: + # The ``requests`` and ``httpx`` backends mock only the paths + # which they serve, so they give no response at all. + assert response is None + return + + assert response is not None + assert response.status_code == HTTPStatus.NOT_FOUND + assert response.body == b"" + assert response.content_type is None + + @staticmethod + def test_unknown_method( + *, + vuforia_database: CloudDatabase, + verify_mock_vuforia: VuforiaBackend, + ) -> None: + """A request to a served path with a method which that path does + not serve returns a 404, rather than a 405. + """ + response = _send_unrouted_request( + backend=verify_mock_vuforia, + vuforia_database=vuforia_database, + method=HTTPMethod.DELETE, + request_path="/summary", + ) + + if verify_mock_vuforia == VuforiaBackend.MOCK: + assert response is None + return + + assert response is not None + assert response.status_code == HTTPStatus.NOT_FOUND diff --git a/tests/mock_vws/test_unrouted_requests.py b/tests/mock_vws/test_unrouted_requests.py deleted file mode 100644 index 3a822bca4..000000000 --- a/tests/mock_vws/test_unrouted_requests.py +++ /dev/null @@ -1,146 +0,0 @@ -"""Verified fake tests for requests which VWS does not serve. - -These cover requests to a path which VWS does not serve, and requests to a -served path with a method which that path does not serve. -""" - -from dataclasses import dataclass -from http import HTTPMethod, HTTPStatus - -import pytest -import requests -from beartype import beartype -from vws_auth_tools import authorization_header, rfc_1123_date - -from mock_vws._flask_server.vws import VWS_FLASK_APP -from mock_vws.database import CloudDatabase -from tests.mock_vws.fixtures.vuforia_backends import VuforiaBackend - -_VWS_HOST = "https://vws.vuforia.com" - - -@beartype -@dataclass(frozen=True, kw_only=True) -class _UnroutedResponse: - """The parts of a response to a request which no route serves.""" - - status_code: int - body: bytes - content_type: str | None - - -@beartype -def _send_unrouted_request( - *, - backend: VuforiaBackend, - vuforia_database: CloudDatabase, - method: HTTPMethod, - request_path: str, -) -> _UnroutedResponse | None: - """Send a signed request which no route serves and return the response. - - ``None`` is returned when the backend refuses the connection rather than - returning a response. - """ - date = rfc_1123_date() - headers = { - "Authorization": authorization_header( - access_key=vuforia_database.server_access_key, - secret_key=vuforia_database.server_secret_key, - method=method, - content=b"", - content_type="", - date=date, - request_path=request_path, - ), - "Date": date, - } - - if backend == VuforiaBackend.DOCKER_IN_MEMORY: - # The ``responses`` library intercepts only the paths and methods - # which the Flask app routes, so requests to any other path never - # reach the app. A running container serves every path, so we drive - # the app with its own test client. - test_client_response = VWS_FLASK_APP.test_client().open( - request_path, - method=method, - headers=headers, - ) - return _UnroutedResponse( - status_code=test_client_response.status_code, - body=test_client_response.data, - content_type=test_client_response.headers.get( - key="Content-Type", - ), - ) - - try: - response = requests.request( - method=method, - url=_VWS_HOST + request_path, - headers=headers, - timeout=30, - ) - except requests.exceptions.ConnectionError: - return None - - return _UnroutedResponse( - status_code=response.status_code, - body=response.content, - content_type=response.headers.get("Content-Type"), - ) - - -@pytest.mark.usefixtures("verify_mock_vuforia") -class TestUnroutedRequests: - """Tests for requests which VWS does not serve.""" - - @staticmethod - def test_unknown_path( - *, - vuforia_database: CloudDatabase, - verify_mock_vuforia: VuforiaBackend, - ) -> None: - """A request to a path which is not served returns a 404 with no - body. - """ - response = _send_unrouted_request( - backend=verify_mock_vuforia, - vuforia_database=vuforia_database, - method=HTTPMethod.GET, - request_path="/some-random-endpoint", - ) - - if verify_mock_vuforia == VuforiaBackend.MOCK: - # The ``requests`` and ``httpx`` backends mock only the paths - # which they serve, so they give no response at all. - assert response is None - return - - assert response is not None - assert response.status_code == HTTPStatus.NOT_FOUND - assert response.body == b"" - assert response.content_type is None - - @staticmethod - def test_unknown_method( - *, - vuforia_database: CloudDatabase, - verify_mock_vuforia: VuforiaBackend, - ) -> None: - """A request to a served path with a method which that path does - not serve returns a 404, rather than a 405. - """ - response = _send_unrouted_request( - backend=verify_mock_vuforia, - vuforia_database=vuforia_database, - method=HTTPMethod.DELETE, - request_path="/summary", - ) - - if verify_mock_vuforia == VuforiaBackend.MOCK: - assert response is None - return - - assert response is not None - assert response.status_code == HTTPStatus.NOT_FOUND