Skip to content

Commit ea2cf18

Browse files
adamtheturtleclaude
andcommitted
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) <noreply@anthropic.com>
1 parent 2e456dc commit ea2cf18

6 files changed

Lines changed: 183 additions & 73 deletions

File tree

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ jobs:
9191
- tests/mock_vws/test_target_raters.py
9292
- tests/mock_vws/test_target_summary.py
9393
- tests/mock_vws/test_unexpected_json.py
94+
- tests/mock_vws/test_unrouted_requests.py
9495
- tests/mock_vws/test_update_target.py::TestActiveFlag
9596
- tests/mock_vws/test_update_target.py::TestApplicationMetadata::test_base64_encoded
9697
- tests/mock_vws/test_update_target.py::TestApplicationMetadata::test_invalid_type

docs/source/differences-to-vws.rst

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -308,12 +308,18 @@ Paths which the mock does not serve
308308
-----------------------------------
309309

310310
Real Vuforia returns a 404 response for a request to a path which it does not
311-
serve.
312-
313-
The Flask and Docker mock does the same, with a Flask error page as the body,
314-
and it returns a 405 response for a request to a served path with a method
315-
which that path does not serve.
316-
Neither response body has been verified against real Vuforia.
311+
serve, and for a request to a served path with a method which that path does
312+
not serve.
313+
It does not return a 405 response.
314+
The Flask and Docker mock does the same, with an empty body and no
315+
``Content-Type`` header.
316+
317+
Real Vuforia gives an empty body only for a request to a path which does not
318+
start with a served path, such as ``/some-random-endpoint``.
319+
For any other request which it does not serve, such as ``DELETE /summary`` or
320+
``GET /targetsfoo``, it gives an HTML "Not Found" page which names the method
321+
and the path of the request.
322+
The mock gives an empty body for all of these.
317323

318324
The ``requests`` and ``httpx`` backends mock only the paths which the mock
319325
serves, so a request to any other path raises a connection error rather than
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +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.
1+
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.

src/mock_vws/_flask_server/vws.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from beartype import beartype
1919
from flask import Flask, Response, request
2020
from pydantic_settings import BaseSettings
21+
from werkzeug.exceptions import MethodNotAllowed, NotFound
2122

2223
from mock_vws._constants import (
2324
VUMARK_PDF,
@@ -202,7 +203,8 @@ def validate_request() -> None:
202203
203204
Flask runs ``before_request`` handlers before it raises a routing error,
204205
so requests which match no route reach this function.
205-
Those requests are left to Flask, which raises the routing error itself.
206+
Those requests are left to Flask, which raises the routing error, and
207+
``handle_unrouted_request`` turns that into a response.
206208
"""
207209
if request.url_rule is None:
208210
return
@@ -248,6 +250,23 @@ def handle_exceptions(exc: ValidatorError) -> Response:
248250
return response
249251

250252

253+
@VWS_FLASK_APP.errorhandler(code_or_exception=HTTPStatus.NOT_FOUND)
254+
@VWS_FLASK_APP.errorhandler(code_or_exception=HTTPStatus.METHOD_NOT_ALLOWED)
255+
@beartype
256+
def handle_unrouted_request(exc: NotFound | MethodNotAllowed) -> Response:
257+
"""Return a 404 response with no body for a request which no route
258+
serves.
259+
260+
Real Vuforia returns a 404 response for a request to a path which it does
261+
not serve, and for a request to a served path with a method which that
262+
path does not serve.
263+
"""
264+
del exc
265+
response = Response(status=HTTPStatus.NOT_FOUND, response=b"")
266+
del response.headers["Content-Type"]
267+
return response
268+
269+
251270
@VWS_FLASK_APP.route(rule="/oauth2/token", methods=[HTTPMethod.POST])
252271
@beartype
253272
def oauth2_token() -> Response:

tests/mock_vws/test_flask_app_usage.py

Lines changed: 3 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -249,73 +249,11 @@ def test_per_endpoint_limits() -> None:
249249
class TestUnroutedRequests:
250250
"""Tests for requests which the Flask app does not route.
251251
252-
These tests use the Flask test client because the ``responses``
253-
library intercepts only the paths and methods which the app routes,
254-
so requests to any other path never reach the app.
252+
Requests which are routed are covered by
253+
``tests/mock_vws/test_unrouted_requests.py``, which verifies the
254+
responses against real Vuforia.
255255
"""
256256

257-
@staticmethod
258-
def _signed_headers(
259-
*,
260-
database: CloudDatabase,
261-
method: HTTPMethod,
262-
request_path: str,
263-
) -> dict[str, str]:
264-
"""Return headers which sign a request with valid server keys."""
265-
date = rfc_1123_date()
266-
authorization_string = authorization_header(
267-
access_key=database.server_access_key,
268-
secret_key=database.server_secret_key,
269-
method=method,
270-
content=b"",
271-
content_type="",
272-
date=date,
273-
request_path=request_path,
274-
)
275-
return {"Authorization": authorization_string, "Date": date}
276-
277-
def test_unknown_path(self) -> None:
278-
"""A request to a path which is not routed returns a 404."""
279-
database = CloudDatabase()
280-
databases_url = _EXAMPLE_URL_FOR_TARGET_MANAGER + "/cloud_databases"
281-
requests.post(url=databases_url, json=database.to_dict(), timeout=30)
282-
283-
request_path = "/some-random-endpoint"
284-
headers = self._signed_headers(
285-
database=database,
286-
method=HTTPMethod.GET,
287-
request_path=request_path,
288-
)
289-
290-
response = VWS_FLASK_APP.test_client().get(
291-
request_path,
292-
headers=headers,
293-
)
294-
295-
assert response.status_code == HTTPStatus.NOT_FOUND
296-
297-
def test_unknown_method(self) -> None:
298-
"""A request to a routed path with a method which that path does
299-
not serve returns a 405.
300-
"""
301-
database = CloudDatabase()
302-
databases_url = _EXAMPLE_URL_FOR_TARGET_MANAGER + "/cloud_databases"
303-
requests.post(url=databases_url, json=database.to_dict(), timeout=30)
304-
305-
request_path = "/summary"
306-
headers = self._signed_headers(
307-
database=database,
308-
method=HTTPMethod.POST,
309-
request_path=request_path,
310-
)
311-
312-
response = VWS_FLASK_APP.test_client().post(
313-
request_path,
314-
headers=headers,
315-
)
316-
317-
assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED
318-
319257
@staticmethod
320258
def test_unauthenticated_unknown_path() -> None:
321259
"""A request to a path which is not routed returns a 404 even
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""Verified fake tests for requests which VWS does not serve.
2+
3+
These cover requests to a path which VWS does not serve, and requests to a
4+
served path with a method which that path does not serve.
5+
"""
6+
7+
from dataclasses import dataclass
8+
from http import HTTPMethod, HTTPStatus
9+
10+
import pytest
11+
import requests
12+
from beartype import beartype
13+
from vws_auth_tools import authorization_header, rfc_1123_date
14+
15+
from mock_vws._flask_server.vws import VWS_FLASK_APP
16+
from mock_vws.database import CloudDatabase
17+
from tests.mock_vws.fixtures.vuforia_backends import VuforiaBackend
18+
19+
_VWS_HOST = "https://vws.vuforia.com"
20+
21+
22+
@beartype
23+
@dataclass(frozen=True, kw_only=True)
24+
class _UnroutedResponse:
25+
"""The parts of a response to a request which no route serves."""
26+
27+
status_code: int
28+
body: bytes
29+
content_type: str | None
30+
31+
32+
@beartype
33+
def _send_unrouted_request(
34+
*,
35+
backend: VuforiaBackend,
36+
vuforia_database: CloudDatabase,
37+
method: HTTPMethod,
38+
request_path: str,
39+
) -> _UnroutedResponse | None:
40+
"""Send a signed request which no route serves and return the response.
41+
42+
``None`` is returned when the backend refuses the connection rather than
43+
returning a response.
44+
"""
45+
date = rfc_1123_date()
46+
headers = {
47+
"Authorization": authorization_header(
48+
access_key=vuforia_database.server_access_key,
49+
secret_key=vuforia_database.server_secret_key,
50+
method=method,
51+
content=b"",
52+
content_type="",
53+
date=date,
54+
request_path=request_path,
55+
),
56+
"Date": date,
57+
}
58+
59+
if backend == VuforiaBackend.DOCKER_IN_MEMORY:
60+
# The ``responses`` library intercepts only the paths and methods
61+
# which the Flask app routes, so requests to any other path never
62+
# reach the app. A running container serves every path, so we drive
63+
# the app with its own test client.
64+
test_client_response = VWS_FLASK_APP.test_client().open(
65+
request_path,
66+
method=method,
67+
headers=headers,
68+
)
69+
return _UnroutedResponse(
70+
status_code=test_client_response.status_code,
71+
body=test_client_response.data,
72+
content_type=test_client_response.headers.get(
73+
key="Content-Type",
74+
),
75+
)
76+
77+
try:
78+
response = requests.request(
79+
method=method,
80+
url=_VWS_HOST + request_path,
81+
headers=headers,
82+
timeout=30,
83+
)
84+
except requests.exceptions.ConnectionError:
85+
return None
86+
87+
return _UnroutedResponse(
88+
status_code=response.status_code,
89+
body=response.content,
90+
content_type=response.headers.get("Content-Type"),
91+
)
92+
93+
94+
@pytest.mark.usefixtures("verify_mock_vuforia")
95+
class TestUnroutedRequests:
96+
"""Tests for requests which VWS does not serve."""
97+
98+
@staticmethod
99+
def test_unknown_path(
100+
*,
101+
vuforia_database: CloudDatabase,
102+
verify_mock_vuforia: VuforiaBackend,
103+
) -> None:
104+
"""A request to a path which is not served returns a 404 with no
105+
body.
106+
"""
107+
response = _send_unrouted_request(
108+
backend=verify_mock_vuforia,
109+
vuforia_database=vuforia_database,
110+
method=HTTPMethod.GET,
111+
request_path="/some-random-endpoint",
112+
)
113+
114+
if verify_mock_vuforia == VuforiaBackend.MOCK:
115+
# The ``requests`` and ``httpx`` backends mock only the paths
116+
# which they serve, so they give no response at all.
117+
assert response is None
118+
return
119+
120+
assert response is not None
121+
assert response.status_code == HTTPStatus.NOT_FOUND
122+
assert response.body == b""
123+
assert response.content_type is None
124+
125+
@staticmethod
126+
def test_unknown_method(
127+
*,
128+
vuforia_database: CloudDatabase,
129+
verify_mock_vuforia: VuforiaBackend,
130+
) -> None:
131+
"""A request to a served path with a method which that path does
132+
not serve returns a 404, rather than a 405.
133+
"""
134+
response = _send_unrouted_request(
135+
backend=verify_mock_vuforia,
136+
vuforia_database=vuforia_database,
137+
method=HTTPMethod.DELETE,
138+
request_path="/summary",
139+
)
140+
141+
if verify_mock_vuforia == VuforiaBackend.MOCK:
142+
assert response is None
143+
return
144+
145+
assert response is not None
146+
assert response.status_code == HTTPStatus.NOT_FOUND

0 commit comments

Comments
 (0)