Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/mock_vws/_mock_common.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
"""Common utilities for creating mock routes."""

import json
from collections.abc import Iterable
from collections.abc import Iterable, Mapping
from dataclasses import dataclass
from typing import Any

from beartype import beartype


@dataclass(frozen=True)
class RequestData:
"""A library-agnostic representation of an HTTP request.

Args:
method: The HTTP method of the request.
path: The path of the request.
headers: The headers sent with the request.
body: The body of the request.
"""

method: str
path: str
headers: Mapping[str, str]
body: bytes


@dataclass(frozen=True)
class Route:
"""A representation of a VWS route.
Expand Down
24 changes: 20 additions & 4 deletions src/mock_vws/_requests_mock_server/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from requests import PreparedRequest
from responses import RequestsMock

from mock_vws._mock_common import RequestData
from mock_vws.database import CloudDatabase, VuMarkDatabase
from mock_vws.image_matchers import (
ImageMatcher,
Expand All @@ -27,7 +28,8 @@
from .mock_web_services_api import MockVuforiaWebServicesAPI

_ResponseType = tuple[int, Mapping[str, str], str | bytes]
_Callback = Callable[[PreparedRequest], _ResponseType]
_MockCallback = Callable[[RequestData], _ResponseType]
_ResponsesCallback = Callable[[PreparedRequest], _ResponseType]

_STRUCTURAL_SIMILARITY_MATCHER = StructuralSimilarityMatcher()
_BRISQUE_TRACKING_RATER = BrisqueTargetTrackingRater()
Expand Down Expand Up @@ -156,10 +158,10 @@ def add_vumark_database(self, vumark_database: VuMarkDatabase) -> None:

@staticmethod
def _wrap_callback(
callback: _Callback,
callback: _MockCallback,
delay_seconds: float,
sleep_fn: Callable[[float], None],
) -> _Callback:
) -> _ResponsesCallback:
"""Wrap a callback to add a response delay."""

def wrapped(
Expand All @@ -186,7 +188,21 @@ def wrapped(
sleep_fn(effective)
raise requests.exceptions.Timeout

result = callback(request)
raw_body = request.body
if raw_body is None:
body_bytes = b""
elif isinstance(raw_body, str):
body_bytes = raw_body.encode(encoding="utf-8")
else:
body_bytes = raw_body
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Query API body normalization behavior silently changed

Low Severity

The PR describes the two removed _body_bytes() helpers as "duplicate," but they had different behavior for str bodies. The old query API version returned b"" for string bodies (treating them same as None), while the services API version encoded them via .encode("utf-8"). The unified adapter in _wrap_callback now always encodes str to UTF-8, silently changing the query API's body handling semantics. If a str body ever reaches the query path, it will now be processed as real content rather than treated as empty.

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behavioral change is intentional. The old query `_body_bytes()` silently discarding string bodies (returning `b""`) was arguably a bug — if a string body arrives, encoding it to UTF-8 is strictly more correct than throwing it away.

In practice this is a no-op: the `responses` library always provides `bytes` or `None` for the query endpoint's multipart form body, so the `str` branch is never reached for query requests.


request_data = RequestData(
method=request.method or "",
path=request.path_url,
headers=dict(request.headers),
body=body_bytes,
)
result = callback(request_data)
sleep_fn(delay_seconds)
return result

Expand Down
31 changes: 9 additions & 22 deletions src/mock_vws/_requests_mock_server/mock_web_query_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@
from typing import ParamSpec, Protocol, runtime_checkable

from beartype import beartype
from requests.models import PreparedRequest

from mock_vws._mock_common import Route
from mock_vws._mock_common import RequestData, Route
from mock_vws._query_tools import (
get_query_match_response_text,
)
Expand Down Expand Up @@ -78,21 +77,9 @@ def decorator(
return decorator


@beartype
def _body_bytes(request: PreparedRequest) -> bytes:
"""Return the body of a request as bytes."""
if request.body is None or isinstance(request.body, str):
return b""

return request.body


@beartype
class MockVuforiaWebQueryAPI:
"""A fake implementation of the Vuforia Web Query API.

This implementation is tied to the implementation of ``responses``.
"""
"""A fake implementation of the Vuforia Web Query API."""

def __init__(
self,
Expand All @@ -114,24 +101,24 @@ def __init__(
self._query_match_checker = query_match_checker

@route(path_pattern="/v1/query", http_methods={HTTPMethod.POST})
def query(self, request: PreparedRequest) -> _ResponseType:
def query(self, request: RequestData) -> _ResponseType:
"""Perform an image recognition query."""
try:
run_query_validators(
request_path=request.path_url,
request_path=request.path,
request_headers=request.headers,
request_body=_body_bytes(request=request),
request_method=request.method or "",
request_body=request.body,
request_method=request.method,
databases=self._target_manager.cloud_databases,
)
except ValidatorError as exc:
return exc.status_code, exc.headers, exc.response_text

response_text = get_query_match_response_text(
request_headers=request.headers,
request_body=_body_bytes(request=request),
request_method=request.method or "",
request_path=request.path_url,
request_body=request.body,
request_method=request.method,
request_path=request.path,
databases=self._target_manager.cloud_databases,
query_match_checker=self._query_match_checker,
)
Expand Down
Loading