Skip to content

Commit 1a75971

Browse files
adamtheturtleclaude
andcommitted
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>
1 parent 359d5a3 commit 1a75971

11 files changed

Lines changed: 844 additions & 111 deletions

File tree

‎docs/source/differences-to-vws.rst‎

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

441-
Real Vuforia returns a presigned URL for cloud storage.
442-
The mock returns a URL served by the mock itself, without the query
443-
parameters of a presigned URL, so the mock's URL never expires where a real
444-
one expires after just under seven days.
441+
Real Vuforia returns a presigned URL for cloud storage, of this form:
442+
443+
.. code-block:: text
444+
445+
https://guacamole-targetstore-production-targets.s3.us-west-1.amazonaws.com/reports/{database_id}/{file_name}.csv
446+
?X-Amz-Security-Token=...
447+
&X-Amz-Algorithm=AWS4-HMAC-SHA256
448+
&X-Amz-Date=20260808T210052Z
449+
&X-Amz-SignedHeaders=host
450+
&X-Amz-Credential=.../20260808/us-west-1/s3/aws4_request
451+
&X-Amz-Expires=604799
452+
&X-Amz-Signature=...
453+
454+
The mock returns a URL with the same path and the same query parameters,
455+
served by the mock itself rather than by cloud storage.
445456
The URL returned by the Flask and Docker mock is built from the
446457
:envvar:`VWS_BASE_URL` environment variable.
447-
The report takes :paramref:`~mock_vws.MockVWS.processing_time_seconds`
448-
seconds to generate in the mock.
449-
The documentation says a real report takes between a few seconds and one
450-
hour, but a report for a database with no recognitions has been observed
451-
ready within seconds.
458+
The credential, the security token and the signature are placeholders of
459+
the right shape.
460+
The mock does not check the signature, so a URL whose signature or file name
461+
has been changed, which real Vuforia refuses with a ``SignatureDoesNotMatch``
462+
error document, is served by the mock as if it were signed.
452463

453464
Real Vuforia names the report file after the requested month, and does so
454465
differently for each of the two months it accepts.
455-
A report for the current month is named for the date and the hour, such as
466+
A report for the current month is named for the UTC date and hour, such as
456467
``2026-08-08-21.csv``, and a report for the previous month is named for the
457468
month, such as ``2026-07.csv``.
458-
The mock names every report after an opaque report identifier, so the
459-
requested month cannot be recovered from the mock's URL, and two requests for
460-
the same month never give the same URL.
461-
462-
The mock's URL returns a 404 response until the report is ready, and requires
463-
no authorization.
464-
The lack of authorization matches real Vuforia, whose URL carries its own
465-
signature.
466-
467-
.. admonition:: Unverified assumption
468-
469-
:ref:`unverified-reco-counts-report-not-ready`
469+
The mock does the same, so two requests for the same month in the same hour
470+
name the same file, and two requests for the previous month always do.
471+
Real Vuforia does not generate the report again for such a request: the URL
472+
which the second request returns serves the file which the first request
473+
generated, unchanged.
474+
The mock does the same, so recognition counts set between the two requests
475+
are not in the report which the second URL serves.
476+
477+
The URL expires ``X-Amz-Expires`` seconds after its ``X-Amz-Date``, which
478+
is one second under seven days.
479+
Real Vuforia's storage checks that the URL is in date before it checks the
480+
signature, so a URL whose ``X-Amz-Date`` or ``X-Amz-Expires`` has been
481+
edited to put it out of date gives the same 403 response as a URL which has
482+
expired, even though the edit invalidates the signature.
483+
The mock honors those two parameters in the same way, so code which handles
484+
a stale URL can be tested by editing them.
485+
The 403 response is the XML ``AccessDenied`` error document which Amazon S3
486+
gives, with a ``Request has expired`` message, the expiry time and the
487+
server time.
488+
A URL without those parameters gives the ``AccessDenied`` error document
489+
with an ``Access Denied`` message, as it does on real Vuforia.
490+
491+
Until the report is ready, the URL gives a 404 response with the XML
492+
``NoSuchKey`` error document which Amazon S3 gives, naming the file's key.
493+
The mock does the same, and the mock gives the same response for a file
494+
which no request generated.
495+
The mock's error documents carry random request identifiers where Amazon's
496+
carry its own.
497+
The report takes :paramref:`~mock_vws.MockVWS.processing_time_seconds`
498+
seconds to generate in the mock.
499+
The documentation says a real report takes between a few seconds and one
500+
hour, but a real report has been observed ready within a second of the
501+
request, and the 404 response has been observed by fetching the URL straight
502+
after the request.
503+
The download requires no authorization beyond the query parameters of the
504+
URL, as on real Vuforia.
470505

471506
Paths which the mock does not serve
472507
-----------------------------------

‎docs/source/unverified-behavior.rst‎

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -90,19 +90,6 @@ The mock does not implement this, and no test sends such a request to either.
9090

9191
Sending a request with a header larger than 8 KiB to a real database would verify this.
9292

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

10895
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
@@ -18,8 +18,14 @@
1818
RECO_COUNTS_REPORT_PATH_PATTERN = (
1919
f"/imagetargets/databases/{DATABASE_ID_PATTERN}/reports/recoCounts"
2020
)
21-
# The path which stands in for a reco counts report presigned URL.
22-
RECO_COUNTS_DOWNLOAD_PATH_PATTERN = "/reports/recoCounts/[A-Za-z0-9]+"
21+
# The path which stands in for a reco counts report presigned URL, with the
22+
# query string of that URL.
23+
# Any file name is matched, so that a file which no request generated gives
24+
# the response which cloud storage gives for a missing object rather than
25+
# leaving the request unmatched.
26+
RECO_COUNTS_DOWNLOAD_PATH_PATTERN = (
27+
f"/reports/{DATABASE_ID_PATTERN}/[^/?]+(\\?.*)?"
28+
)
2329

2430

2531
@beartype

0 commit comments

Comments
 (0)