diff --git a/docs/source/differences-to-vws.rst b/docs/source/differences-to-vws.rst index 4ba11998b..9c822331f 100644 --- a/docs/source/differences-to-vws.rst +++ b/docs/source/differences-to-vws.rst @@ -304,6 +304,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 588139921..41b188969 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."""