Skip to content

Commit 21caed1

Browse files
committed
feat: add consumer_version method
The new `consumer_version` replaces the (now deprecated) `consumer_versions` method within the broker source selector class. This replaces the explicit JSON-serialisation of an untyped dictionary into a much more structured call with documented keyword argument. Signed-off-by: JP-Ellis <[email protected]>
1 parent da29864 commit 21caed1

File tree

2 files changed

+289
-3
lines changed

2 files changed

+289
-3
lines changed

src/pact/verifier.py

Lines changed: 172 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
from datetime import date
8282
from pathlib import Path
8383
from typing import TYPE_CHECKING, Any, Literal, TypedDict, overload
84+
from warnings import deprecated
8485

8586
from yarl import URL
8687

@@ -1386,7 +1387,7 @@ def __init__(
13861387
self._provider_branch: str | None = None
13871388
"The provider branch."
13881389

1389-
self._consumer_versions: list[str] | None = None
1390+
self._consumer_versions: list[str | dict[str, Any]] | None = None
13901391
"List of consumer version regex patterns."
13911392

13921393
self._consumer_tags: list[str] | None = None
@@ -1442,11 +1443,174 @@ def provider_branch(self, branch: str) -> Self:
14421443
self._verifier._branch = branch # type: ignore # noqa: PGH003, SLF001
14431444
return self
14441445

1446+
def consumer_version( # noqa: PLR0913
1447+
self,
1448+
*,
1449+
consumer: str | None = None,
1450+
tag: str | None = None,
1451+
fallback_tag: str | None = None,
1452+
latest: bool | None = None,
1453+
deployed_or_released: Literal[True] | None = None,
1454+
deployed: Literal[True] | None = None,
1455+
released: Literal[True] | None = None,
1456+
environment: str | None = None,
1457+
main_branch: Literal[True] | None = None,
1458+
branch: str | None = None,
1459+
matching_branch: Literal[True] | None = None,
1460+
fallback_branch: str | None = None,
1461+
) -> Self:
1462+
"""
1463+
Add a consumer version selector.
1464+
1465+
This method allows specifying consumer version selection criteria to
1466+
filter which consumer pacts are verified from the broker.
1467+
1468+
This function can be called multiple times to add multiple selectors.
1469+
The resulting selectors are combined with a logical OR, meaning that
1470+
pacts matching any of the selectors will be included in the
1471+
verification.
1472+
1473+
Args:
1474+
consumer:
1475+
Application name to filter the results on.
1476+
1477+
Allows a selector to only be applied to a certain consumer.
1478+
1479+
tag:
1480+
The tag name(s) of the consumer versions to get the pacts for.
1481+
1482+
This field is still supported but it is recommended to use the
1483+
`branch` in preference now.
1484+
1485+
fallback_tag:
1486+
The name of the tag to fallback to if the specified `tag` does
1487+
not exist.
1488+
1489+
This is useful when the consumer and provider use matching
1490+
branch names to coordinate the development of new features. This
1491+
field is still supported but it is recommended to use two
1492+
separate selectors - one with the main branch name and one with
1493+
the feature branch name.
1494+
1495+
latest:
1496+
Only select the latest (if false, this selects all pacts for a
1497+
tag).
1498+
1499+
Used in conjunction with the tag property. If a tag is
1500+
specified, and latest is true, then the latest pact for each of
1501+
the consumers with that tag will be returned. If a tag is
1502+
specified and the latest flag is not set to true, all the pacts
1503+
with the specified tag will be returned.
1504+
1505+
deployed_or_released:
1506+
Applications that have been deployed or released.
1507+
1508+
If the key is specified, can only be set to `True`. Returns the
1509+
pacts for all versions of the consumer that are currently
1510+
deployed or released and currently supported in any environment.
1511+
Use of this selector requires that the deployment of the
1512+
consumer application is recorded in the Pact Broker using the
1513+
`pact-broker record-deployment` or `pact-broker record-release`
1514+
CLI.
1515+
1516+
deployed:
1517+
Applications that have been deployed.
1518+
1519+
If the key is specified, can only be set to `True`. Returns the
1520+
pacts for all versions of the consumer that are currently
1521+
deployed to any environment. Use of this selector requires that
1522+
the deployment of the consumer application is recorded in the
1523+
Pact Broker using the `pact-broker record-deployment` CLI.
1524+
1525+
released:
1526+
Applications that have been released.
1527+
1528+
If the key is specified, can only be set to `True`. Returns the
1529+
pacts for all versions of the consumer that are released and
1530+
currently supported in any environment. Use of this selector
1531+
requires that the deployment of the consumer application is
1532+
recorded in the Pact Broker using the `pact-broker
1533+
record-release` CLI.
1534+
1535+
environment:
1536+
Applications in a given environment.
1537+
1538+
The name of the environment containing the consumer versions for
1539+
which to return the pacts. Used to further qualify `{
1540+
"deployed": true }` or `{ "released": true }`. Normally, this
1541+
would not be needed, as it is recommended to verify the pacts
1542+
for all currently deployed/currently supported released
1543+
versions.
1544+
1545+
main_branch:
1546+
Applications with the default branch set in the broker.
1547+
1548+
If the key is specified, can only be set to `True`. Return the
1549+
pacts for the configured `mainBranch` of each consumer. Use of
1550+
this selector requires that the consumer has configured the
1551+
`mainBranch` property, and has set a branch name when publishing
1552+
the pacts.
1553+
1554+
branch:
1555+
Applications with the given branch.
1556+
1557+
The branch name of the consumer versions to get the pacts for.
1558+
Use of this selector requires that the consumer has configured a
1559+
branch name when publishing the pacts.
1560+
1561+
matching_branch:
1562+
Applications that match the provider version branch sent during
1563+
verification.
1564+
1565+
If the key is specified, can only be set to `True`. When true,
1566+
returns the latest pact for any branch with the same name as the
1567+
specified `provider_version_branch`.
1568+
1569+
fallback_branch:
1570+
Fallback branch if branch doesn't exist.
1571+
1572+
The name of the branch to fallback to if the specified branch
1573+
does not exist. Use of this property is discouraged as it may
1574+
allow a pact to pass on a feature branch while breaking
1575+
backwards compatibility with the main branch, which is generally
1576+
not desired. It is better to use two separate consumer version
1577+
selectors, one with the main branch name, and one with the
1578+
feature branch name, rather than use this property.
1579+
1580+
Returns:
1581+
The builder instance for method chaining.
1582+
"""
1583+
if self._consumer_versions is None:
1584+
self._consumer_versions = []
1585+
1586+
param_mapping = [
1587+
("consumer", consumer),
1588+
("tag", tag),
1589+
("fallbackTag", fallback_tag),
1590+
("latest", latest),
1591+
("deployedOrReleased", deployed_or_released),
1592+
("deployed", deployed),
1593+
("released", released),
1594+
("environment", environment),
1595+
("mainBranch", main_branch),
1596+
("branch", branch),
1597+
("matchingBranch", matching_branch),
1598+
("fallbackBranch", fallback_branch),
1599+
]
1600+
1601+
self._consumer_versions.append({
1602+
key: value for key, value in param_mapping if value is not None
1603+
})
1604+
return self
1605+
1606+
@deprecated("Use `consumer_version` method with keyword arguments instead.")
14451607
def consumer_versions(self, *versions: str) -> Self:
14461608
"""
14471609
Set the consumer versions.
14481610
"""
1449-
self._consumer_versions = list(versions)
1611+
if self._consumer_versions is None:
1612+
self._consumer_versions = []
1613+
self._consumer_versions.extend(versions)
14501614
return self
14511615

14521616
def consumer_tags(self, *tags: str) -> Self:
@@ -1463,6 +1627,11 @@ def build(self) -> Verifier:
14631627
Returns:
14641628
The Verifier instance with the broker source added.
14651629
"""
1630+
consumer_versions = [
1631+
json.dumps(cv) if not isinstance(cv, str) else cv
1632+
for cv in (self._consumer_versions or [])
1633+
]
1634+
14661635
self._verifier._broker_source_hook = ( # noqa: SLF001
14671636
lambda: pact_ffi.verifier_broker_source_with_selectors(
14681637
self._verifier._handle, # noqa: SLF001
@@ -1474,7 +1643,7 @@ def build(self) -> Verifier:
14741643
self._include_wip_since,
14751644
self._provider_tags or [],
14761645
self._provider_branch or self._verifier._branch, # noqa: SLF001
1477-
self._consumer_versions or [],
1646+
consumer_versions,
14781647
self._consumer_tags or [],
14791648
)
14801649
)

tests/test_verifier.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88

99
from __future__ import annotations
1010

11+
import json
1112
import re
1213
from pathlib import Path
14+
from typing import Any
15+
from unittest.mock import patch
1316

1417
import pytest
1518

@@ -166,3 +169,117 @@ def test_logs(verifier: Verifier) -> None:
166169
def test_output(verifier: Verifier) -> None:
167170
output = verifier.output()
168171
assert output == ""
172+
173+
174+
@pytest.mark.parametrize(
175+
("selector_calls", "expected_selectors"),
176+
[
177+
pytest.param(
178+
[{"consumer": "test-consumer"}],
179+
[{"consumer": "test-consumer"}],
180+
id="single_parameter",
181+
),
182+
pytest.param(
183+
[{"consumer": "test-consumer", "branch": "main", "latest": True}],
184+
[{"consumer": "test-consumer", "branch": "main", "latest": True}],
185+
id="multiple_parameters",
186+
),
187+
pytest.param(
188+
[{"deployed_or_released": True, "fallback_tag": "latest"}],
189+
[{"deployedOrReleased": True, "fallbackTag": "latest"}],
190+
id="camelcase_conversion",
191+
),
192+
pytest.param(
193+
[
194+
{"branch": "main", "latest": True},
195+
{"branch": "feature-branch", "latest": True},
196+
{"deployed": True},
197+
],
198+
[
199+
{"branch": "main", "latest": True},
200+
{"branch": "feature-branch", "latest": True},
201+
{"deployed": True},
202+
],
203+
id="multiple_selectors",
204+
),
205+
pytest.param(
206+
[
207+
{
208+
"consumer": "test-consumer",
209+
"tag": "v1.0",
210+
"fallback_tag": "latest",
211+
"latest": True,
212+
"deployed_or_released": True,
213+
"deployed": True,
214+
"released": True,
215+
"environment": "staging",
216+
"main_branch": True,
217+
"branch": "feature-123",
218+
"matching_branch": True,
219+
"fallback_branch": "develop",
220+
}
221+
],
222+
[
223+
{
224+
"consumer": "test-consumer",
225+
"tag": "v1.0",
226+
"fallbackTag": "latest",
227+
"latest": True,
228+
"deployedOrReleased": True,
229+
"deployed": True,
230+
"released": True,
231+
"environment": "staging",
232+
"mainBranch": True,
233+
"branch": "feature-123",
234+
"matchingBranch": True,
235+
"fallbackBranch": "develop",
236+
}
237+
],
238+
id="all_parameters",
239+
),
240+
pytest.param(
241+
[
242+
{
243+
"consumer": "test-consumer",
244+
"branch": "main",
245+
"tag": None,
246+
"latest": None,
247+
}
248+
],
249+
[{"consumer": "test-consumer", "branch": "main"}],
250+
id="none_values_excluded",
251+
),
252+
],
253+
)
254+
def test_consumer_version(
255+
verifier: Verifier,
256+
selector_calls: list[dict[str, Any]],
257+
expected_selectors: list[dict[str, Any]],
258+
) -> None:
259+
"""Test consumer_version with various parameter combinations and selector counts."""
260+
with patch("pact_ffi.verifier_broker_source_with_selectors") as mock_ffi:
261+
selector_builder = verifier.broker_source(
262+
"http://localhost:8080",
263+
selector=True,
264+
)
265+
266+
# Call consumer_version for each set of parameters
267+
for params in selector_calls:
268+
selector_builder.consumer_version(**params)
269+
270+
selector_builder.build()
271+
# We call the hook explicitly to trigger the FFI call
272+
assert verifier._broker_source_hook is not None # noqa: SLF001
273+
verifier._broker_source_hook() # noqa: SLF001
274+
275+
# Verify FFI was called with correct selectors
276+
mock_ffi.assert_called_once()
277+
selectors = [json.loads(s) for s in mock_ffi.call_args[0][9]]
278+
279+
assert len(selectors) == len(expected_selectors)
280+
for actual, expected in zip(selectors, expected_selectors, strict=True):
281+
assert actual == expected
282+
# For None value test case, verify excluded keys
283+
if "tag" not in expected and "latest" not in expected:
284+
assert "tag" not in actual
285+
assert "latest" not in actual

0 commit comments

Comments
 (0)