Skip to content

Commit cb5b468

Browse files
Match real Vuforia's reco counts report URL, expiry and not-ready response (#3570)
* Match real Vuforia's reco counts report URL, expiry and not-ready response Closes #3364. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Keep report tests branch-free across hour boundaries and clear the target manager before Flask usage tests Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent f68ca20 commit cb5b468

12 files changed

Lines changed: 890 additions & 120 deletions

File tree

docs/source/differences-to-vws.rst

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -471,35 +471,70 @@ As real Vuforia does, the mock returns a 401 response with the
471471
server keys but which names any other database, including one named by its
472472
name rather than by its ID.
473473

474-
Real Vuforia returns a presigned URL for cloud storage.
475-
The mock returns a URL served by the mock itself, without the query
476-
parameters of a presigned URL, so the mock's URL never expires where a real
477-
one expires after just under seven days.
474+
Real Vuforia returns a presigned URL for cloud storage, of this form:
475+
476+
.. code-block:: text
477+
478+
https://guacamole-targetstore-production-targets.s3.us-west-1.amazonaws.com/reports/{database_id}/{file_name}.csv
479+
?X-Amz-Security-Token=...
480+
&X-Amz-Algorithm=AWS4-HMAC-SHA256
481+
&X-Amz-Date=20260808T210052Z
482+
&X-Amz-SignedHeaders=host
483+
&X-Amz-Credential=.../20260808/us-west-1/s3/aws4_request
484+
&X-Amz-Expires=604799
485+
&X-Amz-Signature=...
486+
487+
The mock returns a URL with the same path and the same query parameters,
488+
served by the mock itself rather than by cloud storage.
478489
The URL returned by the Flask and Docker mock is built from the
479490
:envvar:`VWS_BASE_URL` environment variable.
480-
The report takes :paramref:`~mock_vws.MockVWS.processing_time_seconds`
481-
seconds to generate in the mock.
482-
The documentation says a real report takes between a few seconds and one
483-
hour, but a report for a database with no recognitions has been observed
484-
ready within seconds.
491+
The credential, the security token and the signature are placeholders of
492+
the right shape.
493+
The mock does not check the signature, so a URL whose signature or file name
494+
has been changed, which real Vuforia refuses with a ``SignatureDoesNotMatch``
495+
error document, is served by the mock as if it were signed.
485496

486497
Real Vuforia names the report file after the requested month, and does so
487498
differently for each of the two months it accepts.
488-
A report for the current month is named for the date and the hour, such as
499+
A report for the current month is named for the UTC date and hour, such as
489500
``2026-08-08-21.csv``, and a report for the previous month is named for the
490501
month, such as ``2026-07.csv``.
491-
The mock names every report after an opaque report identifier, so the
492-
requested month cannot be recovered from the mock's URL, and two requests for
493-
the same month never give the same URL.
494-
495-
The mock's URL returns a 404 response until the report is ready, and requires
496-
no authorization.
497-
The lack of authorization matches real Vuforia, whose URL carries its own
498-
signature.
499-
500-
.. admonition:: Unverified assumption
501-
502-
:ref:`unverified-reco-counts-report-not-ready`
502+
The mock does the same, so two requests for the same month in the same hour
503+
name the same file, and two requests for the previous month always do.
504+
Real Vuforia does not generate the report again for such a request: the URL
505+
which the second request returns serves the file which the first request
506+
generated, unchanged.
507+
The mock does the same, so recognition counts set between the two requests
508+
are not in the report which the second URL serves.
509+
510+
The URL expires ``X-Amz-Expires`` seconds after its ``X-Amz-Date``, which
511+
is one second under seven days.
512+
Real Vuforia's storage checks that the URL is in date before it checks the
513+
signature, so a URL whose ``X-Amz-Date`` or ``X-Amz-Expires`` has been
514+
edited to put it out of date gives the same 403 response as a URL which has
515+
expired, even though the edit invalidates the signature.
516+
The mock honors those two parameters in the same way, so code which handles
517+
a stale URL can be tested by editing them.
518+
The 403 response is the XML ``AccessDenied`` error document which Amazon S3
519+
gives, with a ``Request has expired`` message, the expiry time and the
520+
server time.
521+
A URL without those parameters gives the ``AccessDenied`` error document
522+
with an ``Access Denied`` message, as it does on real Vuforia.
523+
524+
Until the report is ready, the URL gives a 404 response with the XML
525+
``NoSuchKey`` error document which Amazon S3 gives, naming the file's key.
526+
The mock does the same, and the mock gives the same response for a file
527+
which no request generated.
528+
The mock's error documents carry random request identifiers where Amazon's
529+
carry its own.
530+
The report takes :paramref:`~mock_vws.MockVWS.processing_time_seconds`
531+
seconds to generate in the mock.
532+
The documentation says a real report takes between a few seconds and one
533+
hour, but a real report has been observed ready within a second of the
534+
request, and the 404 response has been observed by fetching the URL straight
535+
after the request.
536+
The download requires no authorization beyond the query parameters of the
537+
URL, as on real Vuforia.
503538

504539
Paths which the mock does not serve
505540
-----------------------------------

docs/source/unverified-behavior.rst

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -85,19 +85,6 @@ The mock does not implement this, so a user of the mock sees a successful respon
8585

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

88-
.. _unverified-reco-counts-report-not-ready:
89-
90-
A reco counts report which is not ready
91-
---------------------------------------
92-
93-
:Category: never-attempted
94-
:API: Reco Counts Report API
95-
96-
The URL which the mock returns for a reco counts report gives a 404 response until the report is ready.
97-
98-
A request for a real report which caught it before it was generated would verify this.
99-
Every real report requested so far has been ready by the time it was asked for.
100-
10188
.. _unverified-reco-counts-report-row-order:
10289

10390
The order of reco counts report rows

newsfragments/3364.change

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
The URL of a reco counts report now has the shape of the presigned cloud storage URL which real Vuforia returns.
2+
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.
3+
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.
4+
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.

src/mock_vws/_flask_server/vws.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ def __init__(self) -> None:
286286

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

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

304304

305305
_RECO_COUNTS_REPORT_STORE = _InMemoryRecoCountsReportStore()
@@ -364,7 +364,7 @@ def validate_request() -> None:
364364
if (
365365
request.path.startswith("/oauth2/")
366366
or request.path.startswith("/modeltargets/")
367-
or request.path.startswith("/reports/recoCounts/")
367+
or request.path.startswith("/reports/")
368368
):
369369
return
370370
_ = run_services_validators(
@@ -707,20 +707,25 @@ def reco_counts_report(database_id: str) -> Response:
707707

708708

709709
@VWS_FLASK_APP.route(
710-
rule="/reports/recoCounts/<string:report_id>",
710+
rule="/reports/<string:database_id>/<string:file_name>",
711711
methods=[HTTPMethod.GET],
712712
)
713713
@beartype
714-
def download_reco_counts_report(report_id: str) -> Response:
714+
def download_reco_counts_report(database_id: str, file_name: str) -> Response:
715715
"""Download a generated reco counts report.
716716
717717
This stands in for the presigned URL which real Vuforia returns, so it
718-
does not require any authorization.
718+
does not require any authorization beyond the query parameters of that
719+
URL.
719720
"""
721+
# The report is looked up by the whole path, which names the database
722+
# and the file.
723+
del database_id
724+
del file_name
720725
return _to_flask_response(
721726
api_response=download_report(
722727
report_store=_RECO_COUNTS_REPORT_STORE,
723-
report_id=report_id,
728+
request_path=request.full_path,
724729
),
725730
)
726731

src/mock_vws/_mock_common.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@
1919
RECO_COUNTS_REPORT_PATH_PATTERN = (
2020
f"/imagetargets/databases/{DATABASE_ID_PATTERN}/reports/recoCounts"
2121
)
22-
# The path which stands in for a reco counts report presigned URL.
23-
RECO_COUNTS_DOWNLOAD_PATH_PATTERN = "/reports/recoCounts/[A-Za-z0-9]+"
22+
# The path which stands in for a reco counts report presigned URL, with the
23+
# query string of that URL.
24+
# Any file name is matched, so that a file which no request generated gives
25+
# the response which cloud storage gives for a missing object rather than
26+
# leaving the request unmatched.
27+
RECO_COUNTS_DOWNLOAD_PATH_PATTERN = (
28+
f"/reports/{DATABASE_ID_PATTERN}/[^/?]+(\\?.*)?"
29+
)
2430

2531

2632
@beartype

0 commit comments

Comments
 (0)