From 5c8d30dc942b8cf099cb358f9af42715080a524d Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 30 Jun 2026 09:14:52 +1000 Subject: [PATCH] fix: pass http transport first to ffi The underlying FFI assigns special significance to the first transport added, which can cause issues if the end-user configures non-HTTP transports first. Signed-off-by: JP-Ellis --- src/pact/verifier.py | 13 +++++++- tests/test_verifier.py | 76 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/pact/verifier.py b/src/pact/verifier.py index 3a53a476a..7c3bc7a9a 100644 --- a/src/pact/verifier.py +++ b/src/pact/verifier.py @@ -1306,7 +1306,18 @@ def verify(self) -> Self: msg = "No transports have been set" raise RuntimeError(msg) - first, *rest = self._transports + # The first transport has special significance in the Pact FFI, and + # should always be the main HTTP(S) transport. + primary_idx = next( + ( + i + for i, t in enumerate(self._transports) + if t["transport"].startswith("http") + ), + 0, + ) + first = self._transports[primary_idx] + rest = [t for i, t in enumerate(self._transports) if i != primary_idx] pact_ffi.verifier_set_provider_info( self._handle, diff --git a/tests/test_verifier.py b/tests/test_verifier.py index 895b8b2e5..b249d2958 100644 --- a/tests/test_verifier.py +++ b/tests/test_verifier.py @@ -16,6 +16,7 @@ import pytest +from pact.types import Message from pact.verifier import Verifier ASSETS_DIR = Path(__file__).parent / "assets" @@ -162,6 +163,81 @@ def test_verify(verifier: Verifier) -> None: verifier.verify() +@pytest.mark.parametrize( + "message_first", + [ + pytest.param(True, id="message_handler_before_add_transport"), + pytest.param(False, id="add_transport_before_message_handler"), + ], +) +def test_verify_http_primary_regardless_of_order( + verifier: Verifier, + *, + message_first: bool, +) -> None: + """ + The HTTP transport must always be the primary provider info. + + The internal `message` transport added by `message_handler` must never + become the primary transport, regardless of the order in which + `message_handler` and `add_transport` are called. + """ + if message_first: + verifier.message_handler( + lambda name, metadata: Message( # noqa: ARG005 + contents=b"", + metadata=None, + content_type=None, + ) + ) + verifier.add_transport(url="http://localhost:8080") + else: + verifier.add_transport(url="http://localhost:8080") + verifier.message_handler( + lambda name, metadata: Message( # noqa: ARG005 + contents=b"", + metadata=None, + content_type=None, + ) + ) + + with ( + patch("pact_ffi.verifier_set_provider_info") as mock_set_info, + patch("pact_ffi.verifier_add_provider_transport") as mock_add_transport, + patch("pact_ffi.verifier_execute"), + ): + verifier.verify() + + mock_set_info.assert_called_once() + # The primary transport's port (4th positional arg) must be the HTTP one. + assert mock_set_info.call_args[0][4] == 8080 + + # The internal message transport must be added as a secondary transport. + assert mock_add_transport.call_count == 1 + assert mock_add_transport.call_args[0][1] == "message" + + +def test_verify_message_only(verifier: Verifier) -> None: + """A message-only verification must still work.""" + verifier.message_handler( + lambda name, metadata: Message( # noqa: ARG005 + contents=b"", + metadata=None, + content_type=None, + ) + ) + + with ( + patch("pact_ffi.verifier_set_provider_info") as mock_set_info, + patch("pact_ffi.verifier_add_provider_transport") as mock_add_transport, + patch("pact_ffi.verifier_execute"), + ): + verifier.verify() + + mock_set_info.assert_called_once() + mock_add_transport.assert_not_called() + + def test_logs(verifier: Verifier) -> None: logs = verifier.logs assert logs == ""