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
15 changes: 15 additions & 0 deletions docs/source/differences-to-vws.rst
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,21 @@ signature.
The 404 has not been verified, because no request for a real report has caught
one before it was generated.

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

Real Vuforia gives an empty body with a 404 response only for a request to a
path which does not start with a served path, such as
``/some-random-endpoint``.
For any other request which it does not serve, such as ``DELETE /summary`` or
``GET /targetsfoo``, it gives an HTML "Not Found" page which names the method
and the path of the request.
The Flask and Docker mock gives an empty body for all of these.

The ``requests`` and ``httpx`` backends mock only the paths which the mock
serves, so a request to any other path raises a connection error rather than
giving the 404 response which real Vuforia gives.

Header cases
------------

Expand Down
1 change: 1 addition & 0 deletions newsfragments/unrouted-requests.change
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Return a 404 response from the Flask and Docker mock for a request to a path which it does not serve, and for a request to a served path with a method which that path does not serve, as real Vuforia does, rather than raising an error.
25 changes: 25 additions & 0 deletions src/mock_vws/_flask_server/vws.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from beartype import beartype
from flask import Flask, Response, request
from pydantic_settings import BaseSettings
from werkzeug.exceptions import MethodNotAllowed, NotFound

from mock_vws._constants import (
VUMARK_PDF,
Expand Down Expand Up @@ -199,7 +200,14 @@ def validate_request() -> None:

Reco counts report downloads stand in for presigned URLs, which are not
authorized with VWS credentials.

Flask runs ``before_request`` handlers before it raises a routing error,
so requests which match no route reach this function.
Those requests are left to Flask, which raises the routing error, and
``handle_unrouted_request`` turns that into a response.
"""
if request.url_rule is None:
return
if request.endpoint == "generate_vumark_instance":
return
if (
Expand Down Expand Up @@ -242,6 +250,23 @@ def handle_exceptions(exc: ValidatorError) -> Response:
return response


@VWS_FLASK_APP.errorhandler(code_or_exception=HTTPStatus.NOT_FOUND)
@VWS_FLASK_APP.errorhandler(code_or_exception=HTTPStatus.METHOD_NOT_ALLOWED)
@beartype
def handle_unrouted_request(exc: NotFound | MethodNotAllowed) -> Response:
"""Return a 404 response with no body for a request which no route
serves.

Real Vuforia returns a 404 response for a request to a path which it does
not serve, and for a request to a served path with a method which that
path does not serve.
"""
del exc
response = Response(status=HTTPStatus.NOT_FOUND, response=b"")
del response.headers["Content-Type"]
return response


@VWS_FLASK_APP.route(rule="/oauth2/token", methods=[HTTPMethod.POST])
@beartype
def oauth2_token() -> Response:
Expand Down
21 changes: 21 additions & 0 deletions tests/mock_vws/test_flask_app_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,27 @@ def test_per_endpoint_limits() -> None:
client.get_database_summary_report()


class TestUnroutedRequests:
"""Tests for requests which the Flask app does not route.

Signed requests are covered by
``tests/mock_vws/test_invalid_given_id.py``, which verifies the
responses against real Vuforia.
"""

@staticmethod
def test_unauthenticated_unknown_path() -> None:
"""A request to a path which is not routed returns a 404 even
without credentials.

The Docker health check relies on this request returning a
response.
"""
response = VWS_FLASK_APP.test_client().get("/some-random-endpoint")

assert response.status_code == HTTPStatus.NOT_FOUND


class TestAddCloudDatabase:
"""Tests for adding cloud databases to the mock."""

Expand Down
152 changes: 147 additions & 5 deletions tests/mock_vws/test_invalid_given_id.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,106 @@
"""
Tests for passing invalid target IDs to endpoints which require a target
ID to
be given.
"""Tests for requests which name something that VWS does not serve.

These cover an invalid target ID given to an endpoint which requires one, a
path which VWS does not serve, and a served path with a method which that
path does not serve.

The tests for paths and methods live here, rather than in a file of their
own, because every entry in the CI test matrix uses one of the credentials
files in ``secrets.tar.gpg``, and there are exactly as many of those files as
there are entries.
"""

from http import HTTPStatus
from dataclasses import dataclass
from http import HTTPMethod, HTTPStatus

import pytest
import requests
from beartype import beartype
from vws import VWS
from vws_auth_tools import authorization_header, rfc_1123_date

from mock_vws._constants import ResultCodes
from mock_vws._flask_server.vws import VWS_FLASK_APP
from mock_vws.database import CloudDatabase
from tests.mock_vws.fixtures.vuforia_backends import VuforiaBackend
from tests.mock_vws.utils import Endpoint
from tests.mock_vws.utils.assertions import assert_vws_failure
from tests.mock_vws.utils.too_many_requests import handle_server_errors

_VWS_HOST = "https://vws.vuforia.com"


@beartype
@dataclass(frozen=True, kw_only=True)
class _UnroutedResponse:
"""The parts of a response to a request which no route serves."""

status_code: int
body: bytes
content_type: str | None


@beartype
def _send_unrouted_request(
*,
backend: VuforiaBackend,
vuforia_database: CloudDatabase,
method: HTTPMethod,
request_path: str,
) -> _UnroutedResponse | None:
"""Send a signed request which no route serves and return the response.

``None`` is returned when the backend refuses the connection rather than
returning a response.
"""
date = rfc_1123_date()
headers = {
"Authorization": authorization_header(
access_key=vuforia_database.server_access_key,
secret_key=vuforia_database.server_secret_key,
method=method,
content=b"",
content_type="",
date=date,
request_path=request_path,
),
"Date": date,
}

if backend == VuforiaBackend.DOCKER_IN_MEMORY:
# The ``responses`` library intercepts only the paths and methods
# which the Flask app routes, so requests to any other path never
# reach the app. A running container serves every path, so we drive
# the app with its own test client.
test_client_response = VWS_FLASK_APP.test_client().open(
request_path,
method=method,
headers=headers,
)
return _UnroutedResponse(
status_code=test_client_response.status_code,
body=test_client_response.data,
content_type=test_client_response.headers.get(
key="Content-Type",
),
)

try:
response = requests.request(
method=method,
url=_VWS_HOST + request_path,
headers=headers,
timeout=30,
)
except requests.exceptions.ConnectionError:
return None

return _UnroutedResponse(
status_code=response.status_code,
body=response.content,
content_type=response.headers.get("Content-Type"),
)


@pytest.mark.usefixtures("verify_mock_vuforia")
class TestInvalidGivenID:
Expand Down Expand Up @@ -53,3 +140,58 @@ def test_not_real_id(
status_code=HTTPStatus.NOT_FOUND,
result_code=ResultCodes.UNKNOWN_TARGET,
)


@pytest.mark.usefixtures("verify_mock_vuforia")
class TestUnroutedRequests:
"""Tests for requests which VWS does not serve."""

@staticmethod
def test_unknown_path(
*,
vuforia_database: CloudDatabase,
verify_mock_vuforia: VuforiaBackend,
) -> None:
"""A request to a path which is not served returns a 404 with no
body.
"""
response = _send_unrouted_request(
backend=verify_mock_vuforia,
vuforia_database=vuforia_database,
method=HTTPMethod.GET,
request_path="/some-random-endpoint",
)

if verify_mock_vuforia == VuforiaBackend.MOCK:
# The ``requests`` and ``httpx`` backends mock only the paths
# which they serve, so they give no response at all.
assert response is None
return

assert response is not None
assert response.status_code == HTTPStatus.NOT_FOUND
assert response.body == b""
assert response.content_type is None

@staticmethod
def test_unknown_method(
*,
vuforia_database: CloudDatabase,
verify_mock_vuforia: VuforiaBackend,
) -> None:
"""A request to a served path with a method which that path does
not serve returns a 404, rather than a 405.
"""
response = _send_unrouted_request(
backend=verify_mock_vuforia,
vuforia_database=vuforia_database,
method=HTTPMethod.DELETE,
request_path="/summary",
)

if verify_mock_vuforia == VuforiaBackend.MOCK:
assert response is None
return

assert response is not None
assert response.status_code == HTTPStatus.NOT_FOUND