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 newsfragments/3544.change
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix nested ``MockVWS`` instances on the ``httpx`` backend: an inner mock is now the only one which answers while it is running, matching the ``requests`` and ``httpx2`` backends. Previously the outer mock kept answering and requests to the inner mock's URL were refused.
17 changes: 17 additions & 0 deletions src/mock_vws/_respx_mock_server/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import httpx
import respx
from respx.mocks import Mocker

from mock_vws._mock_common import RequestData, Route

Expand Down Expand Up @@ -183,4 +184,20 @@ def start_respx_router(
router.route().mock(side_effect=_block_unmatched)

router.start()

# ``respx`` keeps every started router in a single process-wide list and
# tries them in the order they were started, so an outer mock would
# answer requests which an inner, more recently started, mock was created
# for. The catch-all route above means the outer mock always answers, so
# the inner one would never be consulted at all.
#
# Moving the newest router to the front of that list makes nested mocks
# resolve innermost-first, matching the ``requests`` and ``httpx2``
# backends, whose patches form a LIFO stack. ``respx.Router.start``
# looks its mocker up by name, and this router is created with the
# default name, so the lookup cannot fail.
mocker = Mocker.registry[router.using or ""]
mocker.routers.remove(router)
mocker.routers.insert(0, router)

return router
25 changes: 25 additions & 0 deletions tests/mock_vws/test_respx_mock_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,31 @@ def test_add_get_and_delete_target(
with pytest.raises(expected_exception=UnknownTargetError):
client.get_target_record(target_id=target_id)

@staticmethod
def test_nested_mocks() -> None:
"""A mock inside another mock leaves the outer one working.

The innermost mock is the only one which answers while it is
running, which is what the ``requests`` and ``httpx2`` backends do
too, and the outer mock answers again once the inner one has
stopped.
"""
outer_url = "https://vws.vuforia.com/summary"
inner_url = "https://vuforia.vws.example.com/summary"

with MockVWS():
with MockVWS(base_vws_url="https://vuforia.vws.example.com"):
inner_response = httpx.get(url=inner_url, timeout=30)
with pytest.raises(expected_exception=httpx.ConnectError):
httpx.get(url=outer_url, timeout=30)
outer_response = httpx.get(url=outer_url, timeout=30)

with pytest.raises(expected_exception=httpx.ConnectError):
httpx.get(url=inner_url, timeout=30)

assert inner_response.status_code == HTTPStatus.UNAUTHORIZED
assert outer_response.status_code == HTTPStatus.UNAUTHORIZED


class TestCloudRecoService:
"""Synchronous cloud query usage through the mock via ``httpx``."""
Expand Down