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
79 changes: 57 additions & 22 deletions docs/source/differences-to-vws.rst
Original file line number Diff line number Diff line change
Expand Up @@ -448,35 +448,70 @@ As real Vuforia does, the mock returns a 401 response with the
server keys but which names any other database, including one named by its
name rather than by its ID.

Real Vuforia returns a presigned URL for cloud storage.
The mock returns a URL served by the mock itself, without the query
parameters of a presigned URL, so the mock's URL never expires where a real
one expires after just under seven days.
Real Vuforia returns a presigned URL for cloud storage, of this form:

.. code-block:: text

https://guacamole-targetstore-production-targets.s3.us-west-1.amazonaws.com/reports/{database_id}/{file_name}.csv
?X-Amz-Security-Token=...
&X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Date=20260808T210052Z
&X-Amz-SignedHeaders=host
&X-Amz-Credential=.../20260808/us-west-1/s3/aws4_request
&X-Amz-Expires=604799
&X-Amz-Signature=...

The mock returns a URL with the same path and the same query parameters,
served by the mock itself rather than by cloud storage.
The URL returned by the Flask and Docker mock is built from the
:envvar:`VWS_BASE_URL` environment variable.
The report takes :paramref:`~mock_vws.MockVWS.processing_time_seconds`
seconds to generate in the mock.
The documentation says a real report takes between a few seconds and one
hour, but a report for a database with no recognitions has been observed
ready within seconds.
The credential, the security token and the signature are placeholders of
the right shape.
The mock does not check the signature, so a URL whose signature or file name
has been changed, which real Vuforia refuses with a ``SignatureDoesNotMatch``
error document, is served by the mock as if it were signed.

Real Vuforia names the report file after the requested month, and does so
differently for each of the two months it accepts.
A report for the current month is named for the date and the hour, such as
A report for the current month is named for the UTC date and hour, such as
``2026-08-08-21.csv``, and a report for the previous month is named for the
month, such as ``2026-07.csv``.
The mock names every report after an opaque report identifier, so the
requested month cannot be recovered from the mock's URL, and two requests for
the same month never give the same URL.

The mock's URL returns a 404 response until the report is ready, and requires
no authorization.
The lack of authorization matches real Vuforia, whose URL carries its own
signature.

.. admonition:: Unverified assumption

:ref:`unverified-reco-counts-report-not-ready`
The mock does the same, so two requests for the same month in the same hour
name the same file, and two requests for the previous month always do.
Real Vuforia does not generate the report again for such a request: the URL
which the second request returns serves the file which the first request
generated, unchanged.
The mock does the same, so recognition counts set between the two requests
are not in the report which the second URL serves.

The URL expires ``X-Amz-Expires`` seconds after its ``X-Amz-Date``, which
is one second under seven days.
Real Vuforia's storage checks that the URL is in date before it checks the
signature, so a URL whose ``X-Amz-Date`` or ``X-Amz-Expires`` has been
edited to put it out of date gives the same 403 response as a URL which has
expired, even though the edit invalidates the signature.
The mock honors those two parameters in the same way, so code which handles
a stale URL can be tested by editing them.
The 403 response is the XML ``AccessDenied`` error document which Amazon S3
gives, with a ``Request has expired`` message, the expiry time and the
server time.
A URL without those parameters gives the ``AccessDenied`` error document
with an ``Access Denied`` message, as it does on real Vuforia.

Until the report is ready, the URL gives a 404 response with the XML
``NoSuchKey`` error document which Amazon S3 gives, naming the file's key.
The mock does the same, and the mock gives the same response for a file
which no request generated.
The mock's error documents carry random request identifiers where Amazon's
carry its own.
The report takes :paramref:`~mock_vws.MockVWS.processing_time_seconds`
seconds to generate in the mock.
The documentation says a real report takes between a few seconds and one
hour, but a real report has been observed ready within a second of the
request, and the 404 response has been observed by fetching the URL straight
after the request.
The download requires no authorization beyond the query parameters of the
URL, as on real Vuforia.

Paths which the mock does not serve
-----------------------------------
Expand Down
13 changes: 0 additions & 13 deletions docs/source/unverified-behavior.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,6 @@ The mock does not implement this, so a user of the mock sees a successful respon

A database with more than a million images would verify this, which a test account cannot hold.

.. _unverified-reco-counts-report-not-ready:

A reco counts report which is not ready
---------------------------------------

:Category: never-attempted
:API: Reco Counts Report API

The URL which the mock returns for a reco counts report gives a 404 response until the report is ready.

A request for a real report which caught it before it was generated would verify this.
Every real report requested so far has been ready by the time it was asked for.

.. _unverified-reco-counts-report-row-order:

The order of reco counts report rows
Expand Down
4 changes: 4 additions & 0 deletions newsfragments/3364.change
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
The URL of a reco counts report now has the shape of the presigned cloud storage URL which real Vuforia returns.
The report file is named ``{date}-{hour}.csv`` for the current month and ``{month}.csv`` for the previous month, so two requests for the same month in the same hour name the same file and serve the report which the first request generated.
The URL carries the query parameters of a presigned URL, and the mock honors ``X-Amz-Date`` and ``X-Amz-Expires`` as real Vuforia's storage does, so a URL which is out of date or which has no query parameters gives the ``403`` XML error document which Amazon S3 gives.
A report which is not yet generated gives the ``404`` ``NoSuchKey`` XML error document which Amazon S3 gives, as real Vuforia has now been observed to do, rather than an empty body.
19 changes: 12 additions & 7 deletions src/mock_vws/_flask_server/vws.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ def __init__(self) -> None:

@property
def reco_counts_reports(self) -> dict[str, RecoCountsReport]:
"""All reco counts reports, keyed by report identifier."""
"""All reco counts reports, keyed by report file path."""
with self._lock:
return dict(self._reports)

Expand All @@ -299,7 +299,7 @@ def add_reco_counts_report(
) -> None:
"""Add a reco counts report."""
with self._lock:
self._reports[reco_counts_report.uuid_] = reco_counts_report
self._reports[reco_counts_report.key] = reco_counts_report


_RECO_COUNTS_REPORT_STORE = _InMemoryRecoCountsReportStore()
Expand Down Expand Up @@ -364,7 +364,7 @@ def validate_request() -> None:
if (
request.path.startswith("/oauth2/")
or request.path.startswith("/modeltargets/")
or request.path.startswith("/reports/recoCounts/")
or request.path.startswith("/reports/")
):
return
_ = run_services_validators(
Expand Down Expand Up @@ -707,20 +707,25 @@ def reco_counts_report(database_id: str) -> Response:


@VWS_FLASK_APP.route(
rule="/reports/recoCounts/<string:report_id>",
rule="/reports/<string:database_id>/<string:file_name>",
methods=[HTTPMethod.GET],
)
@beartype
def download_reco_counts_report(report_id: str) -> Response:
def download_reco_counts_report(database_id: str, file_name: str) -> Response:
"""Download a generated reco counts report.

This stands in for the presigned URL which real Vuforia returns, so it
does not require any authorization.
does not require any authorization beyond the query parameters of that
URL.
"""
# The report is looked up by the whole path, which names the database
# and the file.
del database_id
del file_name
return _to_flask_response(
api_response=download_report(
report_store=_RECO_COUNTS_REPORT_STORE,
report_id=report_id,
request_path=request.full_path,
),
)

Expand Down
10 changes: 8 additions & 2 deletions src/mock_vws/_mock_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,14 @@
RECO_COUNTS_REPORT_PATH_PATTERN = (
f"/imagetargets/databases/{DATABASE_ID_PATTERN}/reports/recoCounts"
)
# The path which stands in for a reco counts report presigned URL.
RECO_COUNTS_DOWNLOAD_PATH_PATTERN = "/reports/recoCounts/[A-Za-z0-9]+"
# The path which stands in for a reco counts report presigned URL, with the
# query string of that URL.
# Any file name is matched, so that a file which no request generated gives
# the response which cloud storage gives for a missing object rather than
# leaving the request unmatched.
RECO_COUNTS_DOWNLOAD_PATH_PATTERN = (
f"/reports/{DATABASE_ID_PATTERN}/[^/?]+(\\?.*)?"
)


@beartype
Expand Down
Loading