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
1 change: 1 addition & 0 deletions admin/create_secrets_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def _generate_secrets_file_content(
return textwrap.dedent(
text=f"""\
VUFORIA_TARGET_MANAGER_DATABASE_NAME={cloud_database_details["database_name"]}
VUFORIA_DATABASE_ID={cloud_database_details["database_id"]}
VUFORIA_SERVER_ACCESS_KEY={cloud_database_details["server_access_key"]}
VUFORIA_SERVER_SECRET_KEY={cloud_database_details["server_secret_key"]}
VUFORIA_CLIENT_ACCESS_KEY={cloud_database_details["client_access_key"]}
Expand Down
55 changes: 36 additions & 19 deletions docs/source/differences-to-vws.rst
Original file line number Diff line number Diff line change
Expand Up @@ -259,33 +259,50 @@ Reco counts reports
-------------------

The mock does not count recognitions, so a generated reco counts report
contains only the ``target_id,reco_count`` header row.
contains only the ``target_id,reco_count`` header row, ending with a carriage
return and a line feed.
That is what real Vuforia returns for a database with no recognitions.
The mock returns the same report for the current month and the previous month.
As with real Vuforia, the report is served with a ``text/plain`` content type
rather than a CSV one.

The mock does not use the database ID in the request path.
It uses the database which matches the request's server keys, and it accepts
any database ID.
Real Vuforia returns a 401 response for a request which is signed with valid
server keys but which names a database ID that those keys do not belong to.

Real Vuforia returns a presigned URL for cloud storage, and the report takes
between a few seconds and one hour to generate.
Real Vuforia returns a 401 response with the ``AuthenticationFailure`` result
code for a request which is signed with valid server keys but which names a
database ID that those keys do not belong to.
That includes naming the database by its name rather than by its ID.
:class:`mock_vws.database.CloudDatabase` has no database ID, so the mock
cannot make the same check.

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, and the report takes
:paramref:`~mock_vws.MockVWS.processing_time_seconds` seconds to generate.
parameters of a presigned URL, so the mock's URL never expires where a real
one expires after just under seven days.
The URL returned by the Flask and Docker mock is built from the
:envvar:`VWS_BASE_URL` environment variable.
As with real Vuforia, the URL returns a 404 response until the report is
ready, and it requires no authorization.

The whole endpoint is mock-only in
``tests/mock_vws/test_reco_counts_report.py``, because the test credentials do
not include a database ID and so a request cannot be made which real Vuforia
authenticates.
Nothing about it has been verified against real Vuforia: not the ``Fail``
result code returned for a ``month`` which is not in the ``YYYY-mm`` form or
which is neither the current month nor the previous month, not the columns of
the CSV report, and not the headers of either response.
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.

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
``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.
The 404 has not been verified, because no request for a real report has caught
one before it was generated.

Header cases
------------
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ optional-dependencies.dev = [
"vulture==2.16",
"vws-python==2026.2.25.1",
"vws-test-fixtures==2023.3.5",
"vws-web-tools==2026.5.21",
"vws-web-tools==2026.8.7",
"yamlfix==1.19.1",
"zizmor==1.29.0",
]
Expand Down
Binary file modified secrets.tar.gpg
Binary file not shown.
4 changes: 3 additions & 1 deletion src/mock_vws/_reco_counts_web_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,10 @@ def download_reco_counts_report(
)

body = report.csv_content
# Real Vuforia serves the report from S3 with a ``text/plain`` content
# type, not ``text/csv``.
headers = _download_headers(
content_type="text/csv",
content_type="text/plain",
content_length=len(body),
)
return HTTPStatus.OK, headers, body
3 changes: 2 additions & 1 deletion src/mock_vws/reco_counts.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@

# The mock does not count recognitions, so a generated report never has any
# rows for targets.
_CSV_CONTENT = "target_id,reco_count\n"
# Real Vuforia ends the header row with a carriage return and a line feed.
_CSV_CONTENT = "target_id,reco_count\r\n"


@beartype
Expand Down
19 changes: 19 additions & 0 deletions tests/mock_vws/fixtures/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ class _CloudDatabaseSettings(BaseSettings):
)


class _WorkingCloudDatabaseSettings(_CloudDatabaseSettings):
"""Settings for the working Vuforia database.

Only the working database has an ID, because only endpoints which name
a database in their path need one.
"""

database_id: str


class _InactiveCloudDatabaseSettings(_CloudDatabaseSettings):
"""Settings for an inactive Vuforia database."""

Expand Down Expand Up @@ -137,6 +147,15 @@ def vuforia_database() -> CloudDatabase:
)


@pytest.fixture
def vuforia_database_id() -> str:
"""Return the ID of the working database from environment
variables.
"""
settings = _WorkingCloudDatabaseSettings.model_validate(obj={})
return settings.database_id


@pytest.fixture
def inactive_cloud_database() -> CloudDatabase:
"""
Expand Down
9 changes: 5 additions & 4 deletions tests/mock_vws/fixtures/vuforia_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,15 +367,15 @@ def fixture_verify_mock_vuforia(
vumark_vuforia_database: VuMarkCloudDatabase,
inactive_vumark_database: InactiveVuMarkCloudDatabase,
monkeypatch: pytest.MonkeyPatch,
) -> Generator[None]:
) -> Generator[VuforiaBackend]:
"""Test functions which use this fixture are run multiple times. Once
with
the real Vuforia, and once with each mock.

This is useful for verifying the mocks.

Yields:
``None``.
The backend which the test is running against.
"""
backend: VuforiaBackend = request.param
should_skip = request.config.getoption(
Expand All @@ -390,13 +390,14 @@ def fixture_verify_mock_vuforia(
VuforiaBackend.DOCKER_IN_MEMORY: _enable_use_docker_in_memory,
}[backend]

yield from enable_function(
with contextlib.contextmanager(func=enable_function)(
working_database=vuforia_database,
inactive_cloud_database=inactive_cloud_database,
vumark_vuforia_database=vumark_vuforia_database,
inactive_vumark_database=inactive_vumark_database,
monkeypatch=monkeypatch,
)
):
yield backend


@pytest.fixture(
Expand Down
52 changes: 40 additions & 12 deletions tests/mock_vws/test_reco_counts_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from mock_vws._constants import ResultCodes
from mock_vws.database import CloudDatabase
from tests.mock_vws.fixtures.vuforia_backends import VuforiaBackend

_VWS_HOST = "https://vws.vuforia.com"
# The number of seconds which the mocks take to generate a report.
Expand All @@ -37,12 +38,10 @@ def _month_offset_from_now(*, months: int) -> str:
def _request_reco_counts_report(
*,
vuforia_database: CloudDatabase,
database_id: str,
month: str | int,
) -> requests.Response:
"""Request a reco counts report and return the response."""
# The mocks accept any database ID, and the test credentials do not
# include the ID of the real database.
database_id = uuid.uuid4().hex
request_path = f"/imagetargets/databases/{database_id}/reports/recoCounts"
content_type = "application/json"
content = json.dumps(obj={"month": month}).encode(encoding="utf-8")
Expand Down Expand Up @@ -70,15 +69,25 @@ def _request_reco_counts_report(
)


@pytest.mark.usefixtures("mock_only_vuforia")
class TestRecoCountsReport:
"""Tests for requesting a reco counts report.
@beartype
def _database_id_for_backend(
*,
backend: VuforiaBackend,
vuforia_database_id: str,
) -> str:
"""Return the database ID to name in the request path.

These are tested against the mocks only.
Real Vuforia returns a 401 response for a request which is signed with
valid server keys but which names a database ID that the keys do not
belong to, and the test credentials do not include a database ID.
Real Vuforia requires the ID to be the ID of the database which the
request's server keys belong to. The mocks accept any ID.
"""
if backend != VuforiaBackend.REAL:
return uuid.uuid4().hex

return vuforia_database_id


class TestRecoCountsReport:
"""Tests for requesting a reco counts report."""

@staticmethod
@pytest.mark.parametrize(
Expand All @@ -88,14 +97,20 @@ class TestRecoCountsReport:
)
def test_reco_counts_report(
*,
verify_mock_vuforia: VuforiaBackend,
vuforia_database: CloudDatabase,
vuforia_database_id: str,
months_ago: int,
) -> None:
"""A report can be requested for the current and previous
month.
"""
response = _request_reco_counts_report(
vuforia_database=vuforia_database,
database_id=_database_id_for_backend(
backend=verify_mock_vuforia,
vuforia_database_id=vuforia_database_id,
),
month=_month_offset_from_now(months=-months_ago),
)

Expand All @@ -119,12 +134,18 @@ def test_reco_counts_report(
)
def test_month_out_of_range(
*,
verify_mock_vuforia: VuforiaBackend,
vuforia_database: CloudDatabase,
vuforia_database_id: str,
months_ago: int,
) -> None:
"""Only the current and the previous month can be requested."""
response = _request_reco_counts_report(
vuforia_database=vuforia_database,
database_id=_database_id_for_backend(
backend=verify_mock_vuforia,
vuforia_database_id=vuforia_database_id,
),
month=_month_offset_from_now(months=-months_ago),
)

Expand All @@ -140,12 +161,18 @@ def test_month_out_of_range(
)
def test_malformed_month(
*,
verify_mock_vuforia: VuforiaBackend,
vuforia_database: CloudDatabase,
vuforia_database_id: str,
month: str | int,
) -> None:
"""The month must be given in the ``YYYY-mm`` form."""
response = _request_reco_counts_report(
vuforia_database=vuforia_database,
database_id=_database_id_for_backend(
backend=verify_mock_vuforia,
vuforia_database_id=vuforia_database_id,
),
month=month,
)

Expand All @@ -168,6 +195,7 @@ def test_download_report(*, vuforia_database: CloudDatabase) -> None:
"""The report is available from the given URL once it is ready."""
response = _request_reco_counts_report(
vuforia_database=vuforia_database,
database_id=uuid.uuid4().hex,
month=_month_offset_from_now(months=0),
)
presigned_url = json.loads(s=response.text)["presigned_url"]
Expand All @@ -179,8 +207,8 @@ def test_download_report(*, vuforia_database: CloudDatabase) -> None:

ready_response = requests.get(url=presigned_url, timeout=30)
assert ready_response.status_code == HTTPStatus.OK
assert ready_response.headers["Content-Type"] == "text/csv"
assert ready_response.text == "target_id,reco_count\n"
assert ready_response.headers["Content-Type"] == "text/plain"
assert ready_response.text == "target_id,reco_count\r\n"

@staticmethod
def test_unknown_report() -> None:
Expand Down
1 change: 1 addition & 0 deletions vuforia_secrets.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
VUFORIA_TARGET_MANAGER_DATABASE_NAME=example_database_name
VUFORIA_DATABASE_ID=example_database_id

VUFORIA_SERVER_ACCESS_KEY=example_server_access_key
VUFORIA_SERVER_SECRET_KEY=example_server_secret_key
Expand Down
Loading