From 751e416b3d7f95606e0e481b0d93e5b3cb947bf8 Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Fri, 7 Aug 2026 09:16:10 +0100 Subject: [PATCH 1/2] Record the ProjectHasNoAPIAccess casing ambiguity Closes #3349 Co-Authored-By: Claude Opus 5 (1M context) --- docs/source/differences-to-vws.rst | 6 ++++++ newsfragments/project-has-no-api-access-casing.change | 1 + src/mock_vws/_constants.py | 4 ++++ 3 files changed, 11 insertions(+) create mode 100644 newsfragments/project-has-no-api-access-casing.change diff --git a/docs/source/differences-to-vws.rst b/docs/source/differences-to-vws.rst index a73698264..387539232 100644 --- a/docs/source/differences-to-vws.rst +++ b/docs/source/differences-to-vws.rst @@ -140,6 +140,12 @@ against real databases in the corresponding states: :attr:`mock_vws.states.States.PROJECT_SUSPENDED` state. * ``ProjectHasNoAPIAccess`` is returned by VWS endpoints when a database uses the :attr:`mock_vws.states.States.PROJECT_HAS_NO_API_ACCESS` state. + Vuforia's own result codes table spells this ``ProjectHasNoApiAccess``, with + ``Api`` rather than ``API``. + We have never seen a response from a real database in this state, so we + cannot tell which spelling real Vuforia returns. + The mock uses ``ProjectHasNoAPIAccess`` to match the spelling used by + ``vws-python`` and ``vws-cli``. * ``TooManyRequests`` is returned when a :class:`mock_vws.database.CloudDatabase` exceeds its ``requests_per_second_limit``. Set the limit to ``0`` to return this result diff --git a/newsfragments/project-has-no-api-access-casing.change b/newsfragments/project-has-no-api-access-casing.change new file mode 100644 index 000000000..e522db1a7 --- /dev/null +++ b/newsfragments/project-has-no-api-access-casing.change @@ -0,0 +1 @@ +Document that the casing of the ``ProjectHasNoAPIAccess`` result code is unconfirmed, as Vuforia's documentation spells it ``ProjectHasNoApiAccess`` and no response from a real database in that state has been observed. diff --git a/src/mock_vws/_constants.py b/src/mock_vws/_constants.py index 0c5080101..1b3744e33 100644 --- a/src/mock_vws/_constants.py +++ b/src/mock_vws/_constants.py @@ -61,6 +61,10 @@ class ResultCodes(Enum): TARGET_QUOTA_REACHED = "TargetQuotaReached" PROJECT_SUSPENDED = "ProjectSuspended" PROJECT_INACTIVE = "ProjectInactive" + # Vuforia's result codes table spells this "ProjectHasNoApiAccess". We have + # never seen a real response for a database in this state, so the casing is + # unconfirmed. We keep "ProjectHasNoAPIAccess" to match ``vws-python`` and + # ``vws-cli``, which map this result code by that spelling. PROJECT_HAS_NO_API_ACCESS = "ProjectHasNoAPIAccess" INACTIVE_PROJECT = "InactiveProject" TOO_MANY_REQUESTS = "TooManyRequests" From 73618be3e6e0d569b52a0b2a67abc1c440686d95 Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Fri, 7 Aug 2026 09:25:04 +0100 Subject: [PATCH 2/2] Match the result codes table casing for ProjectHasNoApiAccess Closes #3349 Co-Authored-By: Claude Opus 5 (1M context) --- docs/source/differences-to-vws.rst | 13 ++-- .../project-has-no-api-access-casing.change | 2 +- src/mock_vws/_constants.py | 9 ++- .../_services_validators/exceptions.py | 4 +- .../project_state_validators.py | 4 +- tests/mock_vws/test_requests_mock_usage.py | 69 +++++++++++-------- 6 files changed, 57 insertions(+), 44 deletions(-) diff --git a/docs/source/differences-to-vws.rst b/docs/source/differences-to-vws.rst index 387539232..b219edcf0 100644 --- a/docs/source/differences-to-vws.rst +++ b/docs/source/differences-to-vws.rst @@ -138,14 +138,13 @@ against real databases in the corresponding states: ``target_quota`` targets. * ``ProjectSuspended`` is returned by VWS endpoints when a database uses the :attr:`mock_vws.states.States.PROJECT_SUSPENDED` state. -* ``ProjectHasNoAPIAccess`` is returned by VWS endpoints when a database uses +* ``ProjectHasNoApiAccess`` is returned by VWS endpoints when a database uses the :attr:`mock_vws.states.States.PROJECT_HAS_NO_API_ACCESS` state. - Vuforia's own result codes table spells this ``ProjectHasNoApiAccess``, with - ``Api`` rather than ``API``. - We have never seen a response from a real database in this state, so we - cannot tell which spelling real Vuforia returns. - The mock uses ``ProjectHasNoAPIAccess`` to match the spelling used by - ``vws-python`` and ``vws-cli``. + This casing comes from Vuforia's result codes table, as no response from a + real database in this state has been seen. + ``vws-python`` and ``vws-cli`` map this result code by the + ``ProjectHasNoAPIAccess`` spelling, so they do not recognize this response + until they are updated. * ``TooManyRequests`` is returned when a :class:`mock_vws.database.CloudDatabase` exceeds its ``requests_per_second_limit``. Set the limit to ``0`` to return this result diff --git a/newsfragments/project-has-no-api-access-casing.change b/newsfragments/project-has-no-api-access-casing.change index e522db1a7..ae617f67e 100644 --- a/newsfragments/project-has-no-api-access-casing.change +++ b/newsfragments/project-has-no-api-access-casing.change @@ -1 +1 @@ -Document that the casing of the ``ProjectHasNoAPIAccess`` result code is unconfirmed, as Vuforia's documentation spells it ``ProjectHasNoApiAccess`` and no response from a real database in that state has been observed. +Change the ``ProjectHasNoAPIAccess`` result code to ``ProjectHasNoApiAccess``, matching Vuforia's result codes table. diff --git a/src/mock_vws/_constants.py b/src/mock_vws/_constants.py index 1b3744e33..906cd4c7b 100644 --- a/src/mock_vws/_constants.py +++ b/src/mock_vws/_constants.py @@ -61,11 +61,10 @@ class ResultCodes(Enum): TARGET_QUOTA_REACHED = "TargetQuotaReached" PROJECT_SUSPENDED = "ProjectSuspended" PROJECT_INACTIVE = "ProjectInactive" - # Vuforia's result codes table spells this "ProjectHasNoApiAccess". We have - # never seen a real response for a database in this state, so the casing is - # unconfirmed. We keep "ProjectHasNoAPIAccess" to match ``vws-python`` and - # ``vws-cli``, which map this result code by that spelling. - PROJECT_HAS_NO_API_ACCESS = "ProjectHasNoAPIAccess" + # We have never seen a real response for a database in this state, so this + # casing comes from Vuforia's result codes table rather than from an + # observed response. + PROJECT_HAS_NO_API_ACCESS = "ProjectHasNoApiAccess" INACTIVE_PROJECT = "InactiveProject" TOO_MANY_REQUESTS = "TooManyRequests" INVALID_ACCEPT_HEADER = "InvalidAcceptHeader" diff --git a/src/mock_vws/_services_validators/exceptions.py b/src/mock_vws/_services_validators/exceptions.py index 974c3e349..aca038805 100644 --- a/src/mock_vws/_services_validators/exceptions.py +++ b/src/mock_vws/_services_validators/exceptions.py @@ -239,11 +239,11 @@ def __init__(self) -> None: @beartype -class ProjectHasNoAPIAccessError(ValidatorError): +class ProjectHasNoApiAccessError(ValidatorError): """Exception raised when a database cannot make API requests.""" def __init__(self) -> None: - """Initialize a ``ProjectHasNoAPIAccess`` response.""" + """Initialize a ``ProjectHasNoApiAccess`` response.""" super().__init__() self.status_code = HTTPStatus.FORBIDDEN body = { diff --git a/src/mock_vws/_services_validators/project_state_validators.py b/src/mock_vws/_services_validators/project_state_validators.py index fef236338..c22d6a2f8 100644 --- a/src/mock_vws/_services_validators/project_state_validators.py +++ b/src/mock_vws/_services_validators/project_state_validators.py @@ -11,7 +11,7 @@ get_database_matching_server_keys, ) from mock_vws._services_validators.exceptions import ( - ProjectHasNoAPIAccessError, + ProjectHasNoApiAccessError, ProjectInactiveError, ProjectSuspendedError, ValidatorError, @@ -53,7 +53,7 @@ def validate_project_state( ) state_errors: dict[States, type[ValidatorError]] = { - States.PROJECT_HAS_NO_API_ACCESS: ProjectHasNoAPIAccessError, + States.PROJECT_HAS_NO_API_ACCESS: ProjectHasNoApiAccessError, States.PROJECT_SUSPENDED: ProjectSuspendedError, } if error := state_errors.get(database.state): diff --git a/tests/mock_vws/test_requests_mock_usage.py b/tests/mock_vws/test_requests_mock_usage.py index ec9714f49..80aecf3ab 100644 --- a/tests/mock_vws/test_requests_mock_usage.py +++ b/tests/mock_vws/test_requests_mock_usage.py @@ -16,9 +16,7 @@ from freezegun import freeze_time from PIL import Image from vws import VWS, CloudRecoService -from vws.exceptions.base_exceptions import VWSError from vws.exceptions.vws_exceptions import ( - ProjectHasNoAPIAccessError, ProjectSuspendedError, RequestQuotaReachedError, TargetQuotaReachedError, @@ -461,29 +459,9 @@ def test_target_quota_reached( ) @staticmethod - @pytest.mark.parametrize( - argnames=("state", "expected_exception", "result_code"), - argvalues=[ - ( - States.PROJECT_SUSPENDED, - ProjectSuspendedError, - ResultCodes.PROJECT_SUSPENDED, - ), - ( - States.PROJECT_HAS_NO_API_ACCESS, - ProjectHasNoAPIAccessError, - ResultCodes.PROJECT_HAS_NO_API_ACCESS, - ), - ], - ) - def test_project_state_result_codes( - *, - state: States, - expected_exception: type[VWSError], - result_code: ResultCodes, - ) -> None: - """Configured project states reject VWS requests.""" - database = CloudDatabase(state=state) + def test_project_suspended() -> None: + """A suspended project rejects VWS requests.""" + database = CloudDatabase(state=States.PROJECT_SUSPENDED) client = VWS( server_access_key=database.server_access_key, server_secret_key=database.server_secret_key, @@ -491,15 +469,52 @@ def test_project_state_result_codes( with MockVWS() as mock: mock.add_cloud_database(cloud_database=database) - with pytest.raises(expected_exception=expected_exception) as exc: + with pytest.raises( + expected_exception=ProjectSuspendedError, + ) as exc: client.list_targets() assert_vws_failure( response=exc.value.response, status_code=HTTPStatus.FORBIDDEN, - result_code=result_code, + result_code=ResultCodes.PROJECT_SUSPENDED, ) + @staticmethod + def test_project_has_no_api_access() -> None: + """A project with no API access rejects VWS requests. + + This does not use ``vws-python`` because that library maps this + result code by the ``ProjectHasNoAPIAccess`` spelling, which + Vuforia's result codes table does not use. + """ + database = CloudDatabase(state=States.PROJECT_HAS_NO_API_ACCESS) + request_path = "/targets" + + with MockVWS() as mock: + mock.add_cloud_database(cloud_database=database) + date = rfc_1123_date() + auth = authorization_header( + access_key=database.server_access_key, + secret_key=database.server_secret_key, + method="GET", + content=b"", + content_type="", + date=date, + request_path=request_path, + ) + response = requests.get( + url="https://vws.vuforia.com" + request_path, + headers={ + "Authorization": auth, + "Date": date, + }, + timeout=30, + ) + + assert response.status_code == HTTPStatus.FORBIDDEN + assert response.json()["result_code"] == "ProjectHasNoApiAccess" + class TestCustomBaseURLs: """Tests for using custom base URLs."""