Skip to content

Commit 21d7e70

Browse files
Fix path prefix stripping for base URL with path prefix (#2996)
When MockVWS or MockVWSForHttpx is configured with a base_vws_url containing a path prefix (e.g. https://example.com/prefix), strip the prefix from the request path before passing it to validators and route handlers. Previously, validators performing path-length checks (e.g. validate_target_id_exists, validate_keys, validate_name_*) received the full prefixed path and incorrectly parsed it, causing spurious errors such as UnknownTarget for endpoints that take no target ID. Fixes #2995. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 937ea33 commit 21d7e70

4 files changed

Lines changed: 100 additions & 5 deletions

File tree

src/mock_vws/_requests_mock_server/decorators.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ def _wrap_callback(
162162
callback: _MockCallback,
163163
delay_seconds: float,
164164
sleep_fn: Callable[[float], None],
165+
base_path: str,
165166
) -> _ResponsesCallback:
166167
"""Wrap a callback to add a response delay."""
167168

@@ -197,9 +198,13 @@ def wrapped(
197198
else:
198199
body_bytes = raw_body
199200

201+
path = request.path_url
202+
if base_path and path.startswith(base_path):
203+
path = path[len(base_path) :]
204+
200205
request_data = RequestData(
201206
method=request.method or "",
202-
path=request.path_url,
207+
path=path,
203208
headers=dict(request.headers),
204209
body=body_bytes,
205210
)
@@ -221,6 +226,7 @@ def __enter__(self) -> Self:
221226
(self._mock_vws_api, self._base_vws_url),
222227
(self._mock_vwq_api, self._base_vwq_url),
223228
):
229+
base_path = urlparse(url=base_url).path.rstrip("/")
224230
for route in api.routes:
225231
url_pattern = base_url.rstrip("/") + route.path_pattern + "$"
226232
compiled_url_pattern = re.compile(pattern=url_pattern)
@@ -234,6 +240,7 @@ def __enter__(self) -> Self:
234240
callback=original_callback,
235241
delay_seconds=self._response_delay_seconds,
236242
sleep_fn=self._sleep_fn,
243+
base_path=base_path,
237244
),
238245
content_type=None,
239246
)

src/mock_vws/_respx_mock_server/decorators.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,26 @@
3636
_BRISQUE_TRACKING_RATER = BrisqueTargetTrackingRater()
3737

3838

39-
def _to_request_data(request: httpx.Request) -> RequestData:
39+
def _to_request_data(
40+
request: httpx.Request,
41+
*,
42+
base_path: str,
43+
) -> RequestData:
4044
"""Convert an httpx.Request to a RequestData.
4145
4246
Args:
4347
request: The httpx request to convert.
48+
base_path: The base path prefix to strip from the request path.
4449
4550
Returns:
4651
A RequestData with method, path, headers, and body set.
4752
"""
53+
path = request.url.raw_path.decode(encoding="ascii")
54+
if base_path and path.startswith(base_path):
55+
path = path[len(base_path) :]
4856
return RequestData(
4957
method=request.method,
50-
path=request.url.raw_path.decode(encoding="ascii"),
58+
path=path,
5159
headers={k.title(): v for k, v in request.headers.items()},
5260
body=request.content,
5361
)
@@ -155,12 +163,14 @@ def add_vumark_database(self, vumark_database: VuMarkDatabase) -> None:
155163
def _make_callback(
156164
self,
157165
handler: Callable[[RequestData], _ResponseType],
166+
base_path: str,
158167
) -> Callable[[httpx.Request], httpx.Response]:
159168
"""Create a respx-compatible callback from a handler.
160169
161170
Args:
162171
handler: A handler that takes a RequestData and returns a
163172
response tuple.
173+
base_path: The base path prefix to strip from the request path.
164174
165175
Returns:
166176
A callback that takes an httpx.Request and returns an
@@ -183,7 +193,10 @@ def callback(request: httpx.Request) -> httpx.Response:
183193
Exception: A timeout error is raised when the response
184194
delay exceeds the read timeout.
185195
"""
186-
request_data = _to_request_data(request=request)
196+
request_data = _to_request_data(
197+
request=request,
198+
base_path=base_path,
199+
)
187200
timeout_info: dict[str, float | None] = request.extensions.get(
188201
"timeout", {}
189202
)
@@ -237,6 +250,7 @@ def __enter__(self) -> Self:
237250
(self._mock_vws_api, self._base_vws_url),
238251
(self._mock_vwq_api, self._base_vwq_url),
239252
):
253+
base_path = urlparse(url=base_url).path.rstrip("/")
240254
for route in api.routes:
241255
url_pattern = base_url.rstrip("/") + route.path_pattern + "$"
242256
compiled_url_pattern = re.compile(pattern=url_pattern)
@@ -249,6 +263,7 @@ def __enter__(self) -> Self:
249263
).mock(
250264
side_effect=self._make_callback(
251265
handler=original_callback,
266+
base_path=base_path,
252267
),
253268
)
254269

tests/mock_vws/test_requests_mock_usage.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import io
66
import json
77
import socket
8+
from http import HTTPStatus
89
from urllib.parse import urlparse
910

1011
import pytest
@@ -13,7 +14,7 @@
1314
from freezegun import freeze_time
1415
from PIL import Image
1516
from vws import VWS, CloudRecoService
16-
from vws_auth_tools import rfc_1123_date
17+
from vws_auth_tools import authorization_header, rfc_1123_date
1718

1819
from mock_vws import MissingSchemeError, MockVWS
1920
from mock_vws.database import CloudDatabase, VuMarkDatabase
@@ -391,6 +392,42 @@ def test_custom_base_vwq_url_with_path_prefix() -> None:
391392
timeout=30,
392393
)
393394

395+
@staticmethod
396+
def test_vws_operations_work_with_path_prefix() -> None:
397+
"""VWS API operations work correctly with a base URL path
398+
prefix.
399+
"""
400+
database = CloudDatabase()
401+
base_vws_url = "https://vuforia.vws.example.com/prefix"
402+
403+
with MockVWS(base_vws_url=base_vws_url) as mock:
404+
mock.add_cloud_database(cloud_database=database)
405+
406+
request_path = "/targets"
407+
date = rfc_1123_date()
408+
auth = authorization_header(
409+
access_key=database.server_access_key,
410+
secret_key=database.server_secret_key,
411+
method="GET",
412+
content=b"",
413+
content_type="",
414+
date=date,
415+
request_path=request_path,
416+
)
417+
response = requests.get(
418+
url=base_vws_url + request_path,
419+
headers={
420+
"Authorization": auth,
421+
"Date": date,
422+
},
423+
timeout=30,
424+
)
425+
426+
assert response.status_code == HTTPStatus.OK
427+
response_json = response.json()
428+
assert response_json["result_code"] == "Success"
429+
assert response_json["results"] == []
430+
394431
@staticmethod
395432
def test_no_scheme() -> None:
396433
"""An error if raised if a URL is given with no scheme."""

tests/mock_vws/test_respx_mock_usage.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,42 @@ def test_custom_base_vwq_url_with_path_prefix() -> None:
252252
timeout=30,
253253
)
254254

255+
@staticmethod
256+
def test_vws_operations_work_with_path_prefix() -> None:
257+
"""VWS API operations work correctly with a base URL path
258+
prefix.
259+
"""
260+
database = CloudDatabase()
261+
base_vws_url = "https://vuforia.vws.example.com/prefix"
262+
263+
with MockVWSForHttpx(base_vws_url=base_vws_url) as mock:
264+
mock.add_cloud_database(cloud_database=database)
265+
266+
request_path = "/targets"
267+
date = rfc_1123_date()
268+
auth = authorization_header(
269+
access_key=database.server_access_key,
270+
secret_key=database.server_secret_key,
271+
method="GET",
272+
content=b"",
273+
content_type="",
274+
date=date,
275+
request_path=request_path,
276+
)
277+
response = httpx.get(
278+
url=base_vws_url + request_path,
279+
headers={
280+
"Authorization": auth,
281+
"Date": date,
282+
},
283+
timeout=30,
284+
)
285+
286+
assert response.status_code == HTTPStatus.OK
287+
response_json = response.json()
288+
assert response_json["result_code"] == "Success"
289+
assert response_json["results"] == []
290+
255291
@staticmethod
256292
def test_no_scheme() -> None:
257293
"""An error is raised if a URL is given with no scheme."""

0 commit comments

Comments
 (0)