diff --git a/.github/instructions/pact-v2.instructions.md b/.github/instructions/pact-v2.instructions.md deleted file mode 100644 index cac47aea6..000000000 --- a/.github/instructions/pact-v2.instructions.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -description: "Pact V2" -applyTo: "/src/pact/v2/**" ---- - -# Pact V2 Legacy Code - -- These files provide backwards compatibility with version 2 of Pact Python. -- They are in maintenance mode with only critical bug fixes being applied - no new features should be added. -- When making changes: - - Preserve existing APIs and behavior to maintain backwards compatibility - - Prioritize minimal, targeted fixes over architectural improvements - - Ensure changes do not break existing user code -- New development should focus on the main V3+ codebase in `/src/pact/` instead. diff --git a/.github/instructions/pact.instructions.md b/.github/instructions/pact.instructions.md index 9a6a35e23..65f8491e1 100644 --- a/.github/instructions/pact.instructions.md +++ b/.github/instructions/pact.instructions.md @@ -13,9 +13,3 @@ applyTo: "/src/pact/**" - `match/` - Matching rules and matchers - `generate/` - Value generators - `interaction/` - Interaction building blocks - -## V2 Legacy Code - -- Files in `v2/` subdirectories implement the legacy version of Pact Python -- This version is in maintenance mode with only critical bug fixes being applied -- New features and active development should focus on the main codebase diff --git a/.github/labels.yml b/.github/labels.yml index d1bd66533..d5b84ff59 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -14,7 +14,3 @@ - name: area:ffi description: Relating to the FFI color: C2E0C6 - -- name: area:v2 - description: Relating to v2 code - color: C2E0C6 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2d7c66682..d94e5a0e2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -100,9 +100,6 @@ jobs: - name: Run tests run: hatch run test.py${{ matrix.python-version }}:test --junit-xml=junit.xml --cov-report=xml:coverage-tests.xml - - name: Run tests (v2) - run: hatch run v2-test.py${{ matrix.python-version }}:test --junit-xml=v2-junit.xml --cov-report=xml:coverage-v2.xml - - name: Run tests (CLI) working-directory: pact-python-cli run: hatch run test.py${{ matrix.python-version }}:test --junit-xml=junit.xml --cov-report=xml:coverage-cli.xml @@ -117,7 +114,7 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} flags: tests - files: coverage-tests.xml,coverage-v2.xml,pact-python-cli/coverage-cli.xml,pact-python-ffi/coverage-ffi.xml + files: coverage-tests.xml,pact-python-cli/coverage-cli.xml,pact-python-ffi/coverage-ffi.xml - name: Upload test results if: ${{ !cancelled() }} diff --git a/.vscode/settings.json b/.vscode/settings.json index 1688b7440..e4b488462 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,8 +2,6 @@ "python.testing.pytestArgs": [ "--reruns=0", "--no-cov", - "--ignore=examples/v2", - "--ignore=tests/v2", "examples/", "pact-python-cli/tests/", "pact-python-ffi/tests/", diff --git a/AGENTS.md b/AGENTS.md index 02a996e16..78aafcbcf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,6 @@ - **Typecheck**: `hatch run typecheck` (all) - **Examples**: `hatch run example` (run example tests) - **All checks**: `hatch run all` (format, lint, test, typecheck) -- **V2 tests**: `hatch run v2-test:test` (legacy v2 compatibility tests) ## Code Style @@ -29,6 +28,5 @@ ## Project Structure - `src/pact/`: Main V3+ codebase (active development) -- `src/pact/v2/`: Legacy V2 code (maintenance only - no new features) - `pact-python-ffi/`: Low-level FFI bindings (memory management, type conversion only) - `pact-python-cli/`: CLI wrapper (bundled binaries with system fallback) diff --git a/MIGRATION.md b/MIGRATION.md index 2975ef23d..de505e39d 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,11 +1,23 @@ # Migration Guide + + > [!INFO] > > This document is best viewed on the [Pact Python docs site](https://pact-foundation.github.io/pact-python/MIGRATION/). This document outlines the key changes and migration steps for users transitioning between major Pact Python versions. It focuses on breaking changes, new features, and best practices to ensure a smooth upgrade. +## Migrating from `3.x` to `4.x` + +### Key Changes + +- The `pact.v2` compatibility module and the `compat-v2` extra have been removed, along with the `pact-verifier` console script. Code still importing from `pact.v2` must move to the `3.x` API described in [Migrating from `2.x` to `3.x`](#migrating-from-2x-to-3x). +- Python 3.11 or newer is required. +- The `compat-v2` extra used to install [`pact-python-cli`](https://pypi.org/project/pact-python-cli/). Install it directly if you rely on the Pact CLI (for example, `pact broker` to publish contracts). + +Projects that cannot migrate yet should pin `pact-python<4`, which still provides `pact.v2` through `pact-python[compat-v2]`. + ## Migrating from `2.x` to `3.x` ### Key Changes @@ -14,35 +26,7 @@ This document outlines the key changes and migration steps for users transitioni - This will help ensure feature parity between different language implementations and improve performance and reliability. This also brings compatibility with the latest Pact Specification (v4). - The bundled CLI is now a separate package: [`pact-python-cli`](https://pypi.org/project/pact-python-cli/). - The programmatic API has been completely overhauled to be more Pythonic and easier to use. Legacy process calls and return code checks have been removed in favour of proper exception handling. -- The old programmatic interface is available in the `pact.v2` module for backwards compatibility. This is deprecated, will not receive new features, and will be removed in a future release. - -### Using the `pact.v2` Compatibility Module - -For teams with larger codebases that need time to fully migrate to the new v3 API, a backwards compatibility module is provided at `pact.v2`. This module contains the same API as Pact Python v2.x and serves as an interim measure to assist gradual migration. - -To use the v2 compatibility module, you must install pact-python with the `compat-v2` feature enabled: - -```bash -pip install pact-python[compat-v2] -``` - -All existing `pact.*` imports need to be updated to use `pact.v2.*` instead. Here are some common examples: - -```python -# Old v2.x imports -from pact import Consumer, Provider -from pact.matchers import Like, EachLike -from pact.verifier import Verifier - -# New v3.x imports using the v2 compatibility module -from pact.v2 import Consumer, Provider -from pact.v2.matchers import Like, EachLike -from pact.v2.verifier import Verifier -``` - -Please note that this compatibility module is intended as a temporary solution. A full migration to the new v3 API is strongly recommended as soon as feasible. The compatibility module will only receive critical bug fixes and no new features. - -Pact, by default, updates existing Pact contracts in place, so migrating consumer tests incrementally should be feasible. However, newer features (e.g., message pacts) will likely require a full migration, and mixed usage of v2 and v3 APIs is not officially supported. +- The old programmatic interface was available in the `pact.v2` module during the `3.x` series. It was removed in `4.0.0`; see [Migrating from `3.x` to `4.x`](#migrating-from-3x-to-4x). ### Consumer Changes diff --git a/docs/scripts/python.py b/docs/scripts/python.py index 16954964d..b037af9d0 100644 --- a/docs/scripts/python.py +++ b/docs/scripts/python.py @@ -211,7 +211,6 @@ def process_python( ("src/pact", "api"), ], python_mapping=[("src.pact", "pact")], - ignore=["src/pact/v2"], ) process_python( "pact-python-cli/src/pact_cli", @@ -227,7 +226,4 @@ def process_python( ("pact-python-ffi/src/pact_ffi", "pact-python-ffi/api"), ], ) - process_python( - "examples", - ignore=["examples/v2"], - ) + process_python("examples") diff --git a/examples/v2/__init__.py b/examples/v2/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/v2/src/__init__.py b/examples/v2/src/__init__.py deleted file mode 100644 index 87b9d2a43..000000000 --- a/examples/v2/src/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -Example Client Code. - -This module defines a simple consumer and a couple of implementation of simple -providers. The general premise here is that the consumers will be fetching user -information from the providers. - -The development of the consumer and provider sides would typically be done in -separate teams (and likely different languages). Within the Pact framework, the -consumer side is the one which defines the contract and the provider side is the -one which must satisfy the contract. -""" diff --git a/examples/v2/src/consumer.py b/examples/v2/src/consumer.py deleted file mode 100644 index aed2f68d1..000000000 --- a/examples/v2/src/consumer.py +++ /dev/null @@ -1,155 +0,0 @@ -""" -Simple Consumer Implementation. - -This modules defines a simple -[consumer](https://docs.pact.io/getting_started/terminology#service-consumer) -which will be tested with Pact in the [consumer -test][examples.tests.test_00_consumer]. As Pact is a consumer-driven framework, -the consumer defines the interactions which the provider must then satisfy. - -The consumer is the application which makes requests to another service (the -provider) and receives a response to process. In this example, we have a simple -[`User`][examples.v2.src.consumer.User] class and the consumer fetches a user's -information from a HTTP endpoint. - -This also showcases how Pact tests differ from merely testing adherence to an -OpenAPI specification. The Pact tests are more concerned with the practical use -of the API, rather than the formally defined specification. So you will see -below that as far as this consumer is concerned, the only information needed -from the provider is the user's ID, name, and creation date. This is despite the -provider having additional fields in the response. - -Note that the code in this module is agnostic of Pact (i.e., this would be your -production code). The `pact-python` dependency only appears in the tests. This -is because the consumer is not concerned with Pact, only the tests are. -""" - -from __future__ import annotations - -from dataclasses import dataclass -from datetime import datetime -from typing import Any - -import requests - - -@dataclass() -class User: - """User data class.""" - - id: int - name: str - created_on: datetime - - def __post_init__(self) -> None: - """ - Validate the User data. - - This performs the following checks: - - - The name cannot be empty - - The id must be a positive integer - - Raises: - ValueError: If any of the above checks fail. - """ - if not self.name: - msg = "User must have a name" - raise ValueError(msg) - - if self.id < 0: - msg = "User ID must be a positive integer" - raise ValueError(msg) - - def __repr__(self) -> str: - """Return the user's name.""" - return f"User({self.id}:{self.name})" - - -class UserConsumer: - """ - Example consumer. - - This class defines a simple consumer which will interact with a provider - over HTTP to fetch a user's information, and then return an instance of the - `User` class. - """ - - def __init__(self, base_uri: str) -> None: - """ - Initialise the consumer. - - Args: - base_uri: The uri of the provider - """ - self.base_uri = base_uri - - def get_user(self, user_id: int) -> User: - """ - Fetch a user by ID from the server. - - Args: - user_id: The ID of the user to fetch. - - Returns: - The user if found. - - In all other cases, an error dictionary is returned with the key - `error` and the value as the error message. - - Raises: - requests.HTTPError: If the server returns a non-200 response. - """ - uri = f"{self.base_uri}/users/{user_id}" - response = requests.get(uri, timeout=5) - response.raise_for_status() - data: dict[str, Any] = response.json() - return User( - id=data["id"], - name=data["name"], - created_on=datetime.fromisoformat(data["created_on"]), - ) - - def create_user( - self, - *, - name: str, - ) -> User: - """ - Create a new user on the server. - - Args: - name: The name of the user to create. - - Returns: - The user, if successfully created. - - Raises: - requests.HTTPError: If the server returns a non-200 response. - """ - uri = f"{self.base_uri}/users/" - response = requests.post(uri, json={"name": name}, timeout=5) - response.raise_for_status() - data: dict[str, Any] = response.json() - return User( - id=data["id"], - name=data["name"], - created_on=datetime.fromisoformat(data["created_on"]), - ) - - def delete_user(self, uid: int | User) -> None: - """ - Delete a user by ID from the server. - - Args: - uid: The user ID or user object to delete. - - Raises: - requests.HTTPError: If the server returns a non-200 response. - """ - if isinstance(uid, User): - uid = uid.id - - uri = f"{self.base_uri}/users/{uid}" - response = requests.delete(uri, timeout=5) - response.raise_for_status() diff --git a/examples/v2/src/message.py b/examples/v2/src/message.py deleted file mode 100644 index aa4f33333..000000000 --- a/examples/v2/src/message.py +++ /dev/null @@ -1,123 +0,0 @@ -""" -Handler for non-HTTP interactions. - -This module implements a very basic handler to handle JSON payloads which might -be sent through a messaging system. Unlike a HTTP interaction, the handler is -solely responsible for processing the message, and does not necessarily need to -send a response. This specific example handles file system events. - -Due to the broad range of possible technologies underpinning message systems -(e.g., Kafka, RabbitMQ, SQS, SNS, etc.), Pact's implementation is agnostic to -the transport mechanism. Instead, Pact Python v3 allows to provide a simple -function (or mapping of functions) to produce messages. Under the hood, Pact -uses HTTP to communicate - -Note that the code in this module is agnostic of Pact (i.e., this would be your -production code). The `pact-python` dependency only appears in the tests. This -is because the consumer is not concerned with Pact, only the tests are. -""" - -from __future__ import annotations - -from pathlib import Path -from typing import Any - - -class Filesystem: - """ - Filesystem interface. - - In practice, the handler would process messages and perform some actions on - other systems, whether that be a database, a filesystem, or some other - service. This capability would typically be offered by some library; - however, when running tests, we typically wish to avoid actually interacting - with this external service. - - In order to avoid side effects while testing, the test setup should mock out - the calls to the external service. - - This class provides a simple dummy filesystem interface (which evidently - would fail if actually used), and serves to demonstrate how to mock out - external services when testing. - """ - - def __init__(self) -> None: - """Initialize the filesystem connection.""" - - def write(self, _file: str, _contents: str) -> None: - """Write contents to a file.""" - raise NotImplementedError - - def read(self, file: str) -> str: - """Read contents from a file.""" - raise NotImplementedError - - -class Handler: - """ - Message queue handler. - - This class is responsible for handling messages from the queue. - """ - - def __init__(self) -> None: - """ - Initialize the handler. - - This ensures the underlying filesystem is ready to be used. - """ - self.fs = Filesystem() - - def process(self, event: dict[str, Any]) -> str | None: - """ - Process an event from the queue. - - The event is expected to be a dictionary with the following mandatory - keys: - - - `action`: The action to be performed, either `READ` or `WRITE`. - - `path`: The path to the file to be read or written. - - The event may also contain an optional `contents` key, which is the - contents to be written to the file. If the `contents` key is not - present, an empty file will be written. - """ - self.validate_event(event) - - if event["action"] == "WRITE": - self.fs.write(event["path"], event.get("contents", "")) - return None - if event["action"] == "READ": - return self.fs.read(event["path"]) - - msg = f"Invalid action: {event['action']!r}" - raise ValueError(msg) - - @staticmethod - def validate_event(event: dict[str, Any] | Any) -> None: # noqa: ANN401 - """ - Validates the event received from the queue. - - The event is expected to be a dictionary with the following mandatory - keys: - - - `action`: The action to be performed, either `READ` or `WRITe`. - - `path`: The path to the file to be read or written. - """ - if not isinstance(event, dict): - msg = "Event must be a dictionary." - raise TypeError(msg) - if "action" not in event: - msg = "Event must contain an 'action' key." - raise ValueError(msg) - if "path" not in event: - msg = "Event must contain a 'path' key." - raise ValueError(msg) - if event["action"] not in ["READ", "WRITE"]: - msg = "Event must contain a valid 'action' key." - raise ValueError(msg) - try: - Path(event["path"]) - except TypeError as err: - msg = "Event must contain a valid 'path' key." - raise ValueError(msg) from err diff --git a/examples/v2/src/message_producer.py b/examples/v2/src/message_producer.py deleted file mode 100644 index fe8efcb26..000000000 --- a/examples/v2/src/message_producer.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -Message producer for non-HTTP interactions. - -This modules implements a very basic message producer which could -send to an eventing system, such as Kafka, or a message queue. - -Note that the code in this module is agnostic of Pact (i.e., this would be your -production code). The `pact-python` dependency only appears in the tests. This -is because the consumer is not concerned with Pact, only the tests are. -""" - -from __future__ import annotations - -import enum -import json -from typing import Literal, NamedTuple - - -class FileSystemAction(enum.Enum): - """ - Represents a file system action. - """ - - READ = "READ" - WRITE = "WRITE" - - -class FileSystemEvent(NamedTuple): - """ - Represents a file system event. - """ - - action: Literal[FileSystemAction.READ, FileSystemAction.WRITE] - path: str - contents: str | None - - -class MockMessageQueue: - """ - A mock message queue. - """ - - def __init__(self) -> None: - """ - Initialize the message queue. - """ - self.messages: list[str] = [] - - def send(self, message: str) -> None: - """ - Send a message to the queue. - - Args: - message: The message to send. - """ - self.messages.append(message) - - -class FileSystemMessageProducer: - """ - A message producer for file system events. - """ - - def __init__(self) -> None: - """ - Initialize the message producer. - """ - self.queue = MockMessageQueue() - - def send_to_queue(self, message: FileSystemEvent) -> None: - """ - Send a message to a message queue. - - :param message: The message to send. - """ - self.queue.send( - json.dumps({ - "action": message.action.value, - "path": message.path, - "contents": message.contents, - }) - ) - - def send_write_event(self, filename: str, contents: str) -> None: - """ - Send a write event to a message queue. - - Args: - filename: The name of the file. - contents: The contents of the file. - """ - message = FileSystemEvent( - action=FileSystemAction.WRITE, - path=filename, - contents=contents, - ) - self.send_to_queue(message) - - def send_read_event(self, filename: str) -> None: - """ - Send a read event to a message queue. - - :param filename: The name of the file. - """ - message = FileSystemEvent( - action=FileSystemAction.READ, - path=filename, - contents=None, - ) - self.send_to_queue(message) diff --git a/examples/v2/tests/__init__.py b/examples/v2/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/v2/tests/test_02_message_consumer.py b/examples/v2/tests/test_02_message_consumer.py deleted file mode 100644 index cd79253a7..000000000 --- a/examples/v2/tests/test_02_message_consumer.py +++ /dev/null @@ -1,139 +0,0 @@ -""" -Test Message Pact consumer. - -Pact was originally designed for HTTP interactions involving a request and a -response. Message Pact is an addition to Pact that allows for testing of -non-HTTP interactions, such as message queues. This example demonstrates how to -use Message Pact to test whether a consumer can handle the messages it. Due to -the large number of possible transports, Message Pact does not provide a mock -provider and the tests only verifies the messages. - -A note on terminology, the _consumer_ for Message Pact is the system that -receives the message, and the _provider_ is the system that sends the message. -Pact is still consumer-driven, and the consumer defines the expected messages it -will receive from the provider. When the provider is being verified, Pact -ensures that the provider sends the expected messages. - -In this example, Pact simply ensures that the consumer is capable of processing -the message. The consumer need not send back a message, and any sideffects of -the message must be verified separately (such as through `assert` statements or -as part of the usual unit testing suite). - -> :warning: There is currently a bug whereby the `given` and -`expects_to_receive` have swapped meanings compared to the reference -implementation. This will be addressed in a future release. - -A good resource for understanding the message pact testing can be found [in the -Pact -documentation](https://docs.pact.io/getting_started/how_pact_works#non-http-testing-message-pact). -""" - -from __future__ import annotations - -import logging -from typing import TYPE_CHECKING, Any -from unittest.mock import MagicMock - -import pytest - -from examples.v2.src.message import Handler -from pact.v2 import MessageConsumer, MessagePact, Provider - -if TYPE_CHECKING: - from collections.abc import Generator - from pathlib import Path - - from yarl import URL - -log = logging.getLogger(__name__) - - -@pytest.fixture(scope="module") -def pact(broker: URL, pacts_path: Path) -> Generator[MessagePact, Any, None]: - """ - Set up Message Pact Consumer. - - This fixtures sets up the Message Pact consumer and the pact it has with a - provider. The consumer defines the expected messages it will receive from - the provider, and the Python test suite verifies that the correct actions - are taken. - - For each interaction, the consumer defines the following: - - ```python - ( - pact.given("a request to write test.txt") - .expects_to_receive("empty filesystem") - .with_content(msg) - .with_metadata({"Content-Type": "application/json"}) - ) - - NOTE: There is currently a bug whereby the `given` and `expects_to_receive` - have swapped meanings. This will be addressed in a future release. - ``` - """ - consumer = MessageConsumer("MessageConsumer") - pact = consumer.has_pact_with( - Provider("MessageProvider"), - pact_dir=pacts_path, - publish_to_broker=True, - # Broker configuration - broker_base_url=str(broker), - broker_username=broker.user, - broker_password=broker.password, - ) - with pact: - yield pact - - -@pytest.fixture -def handler() -> Handler: - """ - Fixture for the Handler. - - This fixture mocks the filesystem calls in the handler, so that we can - verify that the handler is calling the filesystem correctly. - """ - handler = Handler() - handler.fs = MagicMock() - handler.fs.write.return_value = None - handler.fs.read.return_value = "Hello world!" - return handler - - -def test_write_file(pact: MessagePact, handler: Handler) -> None: - """ - Test write file. - - This test will be run against the mock provider. The mock provider will - expect to receive a request to write a file, and will respond with a 200 - status code. - """ - msg = {"action": "WRITE", "path": "test.txt", "contents": "Hello world!"} - ( - pact.given("a request to write test.txt") - .expects_to_receive("empty filesystem") - .with_content(msg) - .with_metadata({"Content-Type": "application/json"}) - ) - - result = handler.process(msg) - handler.fs.write.assert_called_once_with( # type: ignore[attr-defined] - "test.txt", - "Hello world!", - ) - assert result is None - - -def test_read_file(pact: MessagePact, handler: Handler) -> None: - msg = {"action": "READ", "path": "test.txt"} - ( - pact.given("a request to read test.txt") - .expects_to_receive("test.txt exists") - .with_content(msg) - .with_metadata({"Content-Type": "application/json"}) - ) - - result = handler.process(msg) - handler.fs.read.assert_called_once_with("test.txt") # type: ignore[attr-defined] - assert result == "Hello world!" diff --git a/examples/v2/tests/test_03_message_provider.py b/examples/v2/tests/test_03_message_provider.py deleted file mode 100644 index 644b0ae5c..000000000 --- a/examples/v2/tests/test_03_message_provider.py +++ /dev/null @@ -1,76 +0,0 @@ -""" -Test Message Pact provider. - -Pact was originally designed for HTTP interactions involving a request and a -response. Message Pact is an addition to Pact that allows for testing of -non-HTTP interactions, such as message queues. This example demonstrates how to -use Message Pact to test whether a consumer can handle the messages it. Due to -the large number of possible transports, Message Pact does not provide a mock -provider and the tests only verifies the messages. - -A note on terminology, the _consumer_ for Message Pact is the system that -receives the message, and the _provider_ is the system that sends the message. -Pact is still consumer-driven, and the consumer defines the expected messages it -will receive from the provider. When the provider is being verified, Pact -ensures that the provider sends the expected messages. - -The below example verifies that the provider sends the expected messages. The -consumer need not send back a message, and any sideffects of the message must -be verified on the consumer side. - -A good resource for understanding the message pact testing can be found [in the -Pact -documentation](https://docs.pact.io/getting_started/how_pact_works#non-http-testing-message-pact). -""" - -from __future__ import annotations - -from pathlib import Path -from typing import TYPE_CHECKING - -from flask import Flask -from pact.v2 import MessageProvider - -if TYPE_CHECKING: - from yarl import URL - -app = Flask(__name__) -PACT_DIR = (Path(__file__).parent / "pacts").resolve() - - -def generate_write_message() -> dict[str, str]: - return { - "action": "WRITE", - "path": "test.txt", - "contents": "Hello world!", - } - - -def generate_read_message() -> dict[str, str]: - return { - "action": "READ", - "path": "test.txt", - } - - -def test_verify(broker: URL) -> None: - provider = MessageProvider( - provider="MessageProvider", - consumer="MessageConsumer", - pact_dir=str(PACT_DIR), - message_providers={ - "a request to write test.txt": generate_write_message, - "a request to read test.txt": generate_read_message, - }, - ) - - with provider: - provider.verify_with_broker( - broker_url=str(broker), - # Despite the auth being set in the broker URL, we still need to pass - # the username and password to the verify_with_broker method. - broker_username=broker.user, - broker_password=broker.password, - publish_version="0.0.0", - publish_verification_results=True, - ) diff --git a/examples/v2/tests/v3/__init__.py b/examples/v2/tests/v3/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/v2/tests/v3/test_02_message_consumer.py b/examples/v2/tests/v3/test_02_message_consumer.py deleted file mode 100644 index 6e5bd1dcf..000000000 --- a/examples/v2/tests/v3/test_02_message_consumer.py +++ /dev/null @@ -1,191 +0,0 @@ -""" -Consumer test of example message handler using the v3 API. - -This test will create a pact between the message handler -and the message provider. -""" - -from __future__ import annotations - -import json -import logging -from pathlib import Path -from typing import ( - TYPE_CHECKING, - Any, -) -from unittest.mock import MagicMock - -import pytest - -from examples.v2.src.message import Handler -from pact.pact import Pact - -if TYPE_CHECKING: - from collections.abc import Callable, Generator - - -log = logging.getLogger(__name__) - - -@pytest.fixture(scope="module") -def pact(pacts_path: Path) -> Generator[Pact, None, None]: - """ - Set up Message Pact Consumer. - - This fixtures sets up the Message Pact consumer and the pact it has with a - provider. The consumer defines the expected messages it will receive from - the provider, and the Python test suite verifies that the correct actions - are taken. - - The verify method takes a function as an argument. This function - will be called with one or two arguments - the value of `with_body` and - the contents of `with_metadata` if provided. - - If the function under test does not take those parameters, you can create - a wrapper function to convert the pact parameters into the values - expected by your function. - - - For each interaction, the consumer defines the following: - - ```python - ( - pact = Pact("consumer name", "provider name") - processed_messages: list[MessagePact.MessagePactResult] = pact \ - .with_specification("V3") - .upon_receiving("a request", "Async") \ - .given("a request to write test.txt") \ - .with_body(msg) \ - .with_metadata({"Content-Type": "application/json"}) - .verify(pact_handler) - ) - - ``` - """ - pact = Pact("v3_message_consumer", "v3_message_provider") - log.info("Creating Message Pact with V3 specification") - yield pact.with_specification("V3") - pact.write_file(pacts_path, overwrite=True) - - -@pytest.fixture -def handler() -> Handler: - """ - Fixture for the Handler. - - This fixture mocks the filesystem calls in the handler, so that we can - verify that the handler is calling the filesystem correctly. - """ - handler = Handler() - handler.fs = MagicMock() - handler.fs.write.return_value = None - handler.fs.read.return_value = "Hello world!" - return handler - - -@pytest.fixture -def verifier( - handler: Handler, -) -> Generator[Callable[[str | bytes | None, dict[str, Any]], None], Any, None]: - """ - Verifier function for the Pact. - - This function is passed to the `verify` method of the Pact object. It is - responsible for taking in the messages (along with the context/metadata) - and ensuring that the consumer is able to process the message correctly. - - In our case, we deserialize the message and pass it to the (pre-mocked) - handler for processing. We then verify that the underlying filesystem - calls were made as expected. - """ - assert isinstance(handler.fs, MagicMock), "Handler filesystem not mocked" - - def _verifier(msg: str | bytes | None, context: dict[str, Any]) -> None: - assert msg is not None, "Message is None" - data = json.loads(msg) - log.info( - "Processing message: ", - extra={"input": msg, "processed_message": data, "context": context}, - ) - handler.process(data) - - yield _verifier - - assert handler.fs.mock_calls, "Handler did not call the filesystem" - - -def test_async_message_handler_write( - pact: Pact, - handler: Handler, - verifier: Callable[[str | bytes | None, dict[str, Any]], None], -) -> None: - """ - Create a pact between the message handler and the message provider. - """ - assert isinstance(handler.fs, MagicMock), "Handler filesystem not mocked" - - ( - pact.upon_receiving("a write request", "Async") - .given("a request to write test.txt") - .with_body( - json.dumps({ - "action": "WRITE", - "path": "my_file.txt", - "contents": "Hello, world!", - }), - "application/json", - ) - .with_matching_rules( - { - "body": { - "$.path": { - "combine": "AND", - "matchers": [ - {"match": "type"}, - ], - } - } - }, - "Response", - ) - ) - pact.verify(verifier, "Async") - - handler.fs.write.assert_called_once_with("my_file.txt", "Hello, world!") - - -def test_async_message_handler_read( - pact: Pact, - handler: Handler, - verifier: Callable[[str | bytes | None, dict[str, Any]], None], -) -> None: - """ - Create a pact between the message handler and the message provider. - """ - assert isinstance(handler.fs, MagicMock), "Handler filesystem not mocked" - - ( - pact.upon_receiving("a read request", "Async") - .given("a request to read test.txt") - .with_body( - json.dumps({ - "action": "READ", - "path": "my_file.txt", - }), - "application/json", - ) - .with_matching_rules({ - "body": { - "$.path": { - "combine": "AND", - "matchers": [ - {"match": "type"}, - ], - }, - } - }) - ) - pact.verify(verifier, "Async") - - handler.fs.read.assert_called_once_with("my_file.txt") diff --git a/examples/v2/tests/v3/test_03_message_provider.py b/examples/v2/tests/v3/test_03_message_provider.py deleted file mode 100644 index 3d0abcb4d..000000000 --- a/examples/v2/tests/v3/test_03_message_provider.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -Producer test of example message. - -This test will read a pact between the message handler and the message provider -and then validate the pact against the provider. -""" - -from __future__ import annotations - -import json -from pathlib import Path -from typing import Any -from unittest.mock import MagicMock - -from examples.v2.src.message_producer import FileSystemMessageProducer -from pact import Verifier -from pact.types import Message - -PACT_DIR = (Path(__file__).parent.parent.parent / "pacts").resolve() - -RESPONSES: dict[str, dict[str, str]] = { - "a request to write test.txt": { - "function_name": "send_write_event", - }, - "a request to read test.txt": { - "function_name": "send_read_event", - }, -} - - -def message_producer(message: str, metadata: dict[str, Any] | None) -> Message: # noqa: ARG001 - """ - Function to produce a message for the provider. - - This specific implementation is rather simple as it returns static content. - In fact, a straight mapping of the message names to the expected responses - could be given to the message handler directly. However, this function is - provided to demonstrate the capability of the message handler to be very - generic. - - Args: - message: - The message name. - - metadata: - Any metadata associated with the message which can be used to - determine the response. - """ - producer = FileSystemMessageProducer() - producer.queue = MagicMock() - - return Message( - contents=json.dumps(RESPONSES[message]).encode("utf-8"), - content_type="application/json", - metadata=None, - ) - - -def test_producer() -> None: - """ - Test the message producer. - """ - verifier = Verifier("provider").message_handler(message_producer) - verifier.verify() diff --git a/pact-python-cli/pyproject.toml b/pact-python-cli/pyproject.toml index fdedf7e5f..b81add323 100644 --- a/pact-python-cli/pyproject.toml +++ b/pact-python-cli/pyproject.toml @@ -122,10 +122,6 @@ build-backend = "hatchling.build" [[tool.hatch.envs.test.matrix]] python = ["3.11", "3.12", "3.13", "3.14", "3.15"] - [tool.mypy] - # Overwrite the exclusions from the root pyproject.toml. - exclude = "" - ## PyTest Configuration [tool.pytest] addopts = [ @@ -143,5 +139,3 @@ build-backend = "hatchling.build" ## Ruff Configuration [tool.ruff] extend = "../pyproject.toml" - - exclude = [] diff --git a/pact-python-ffi/pyproject.toml b/pact-python-ffi/pyproject.toml index a8ef830ac..068e3ec52 100644 --- a/pact-python-ffi/pyproject.toml +++ b/pact-python-ffi/pyproject.toml @@ -108,10 +108,6 @@ build-backend = "hatchling.build" [[tool.hatch.envs.test.matrix]] python = ["3.11", "3.12", "3.13", "3.14", "3.15"] - [tool.mypy] - # Overwrite the exclusions from the root pyproject.toml. - exclude = "" - ## PyTest Configuration [tool.pytest] addopts = [ @@ -129,5 +125,3 @@ build-backend = "hatchling.build" ## Ruff Configuration [tool.ruff] extend = "../pyproject.toml" - - exclude = [] diff --git a/prek.toml b/prek.toml index 0e2a2dc86..a485cc29c 100644 --- a/prek.toml +++ b/prek.toml @@ -46,12 +46,10 @@ rev = "v0.16.1" [[repos.hooks]] id = "ruff-check" - exclude = '(src/pact|tests|examples)/v2/.*\.pyi?' args = ["--fix", "--exit-non-zero-on-fix"] [[repos.hooks]] id = "ruff-format" - exclude = '(src/pact|tests|examples)/v2/.*\.pyi?' [[repos]] repo = "https://github.com/crate-ci/committed" @@ -93,8 +91,7 @@ repo = "local" language = "system" types = ["python"] exclude = '''(?x)( - ^(src/pact|tests|examples)/v2/.*\.pyi? - | ^pact-python-(cli|ffi)/ + ^pact-python-(cli|ffi)/ | ^examples/plugins/proto/.*_pb2(_grpc)?\.py )''' stages = ["pre-push"] diff --git a/pyproject.toml b/pyproject.toml index 7a7bcb7dc..9094856d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,21 +54,6 @@ dependencies = [ issues = "https://github.com/pact-foundation/pact-python/issues" source = "https://github.com/pact-foundation/pact-python" - [project.scripts] - pact-verifier = "pact.v2.cli.verify:main" - - [project.optional-dependencies] - # Dependencies required for v2 only - compat-v2 = [ - # Pact dependencies - "pact-python-cli~=2.5", - # External dependencies - "click~=8.0", - "psutil~=7.0", - "requests~=2.0", - "six~=1.0", - ] - [dependency-groups] # Linting and formatting tools use a more narrow specification to ensure # developper consistency. All other dependencies are as above. @@ -76,9 +61,7 @@ dev = [ "ruff==0.17.0", { include-group = "docs" }, { include-group = "example" }, - { include-group = "example-v2" }, { include-group = "test" }, - { include-group = "test-v2" }, { include-group = "types" }, ] @@ -101,6 +84,7 @@ example = [ "fastapi~=0.0", "flask[async]~=3.0", "grpcio~=1.0", + "httpx~=0.0", "protobuf~=7.34", "pydantic~=2.0", "pytest~=9.0", @@ -129,26 +113,6 @@ types = [ "types-requests~=2.0", ] -# Dependencies for v2 example and test environments -example-v2 = [ - "fastapi~=0.0", - "flask[async]~=3.0", - "pytest~=9.0", - "pytest-cov~=7.0", - "pytest-rerunfailures~=16.0", - "testcontainers~=4.0", - "uvicorn[standard]~=0.0", -] -test-v2 = [ - "fastapi~=0.0", - "httpx~=0.0", - "mock~=5.0", - "pytest~=9.0", - "pytest-cov~=7.0", - "pytest-rerunfailures~=16.0", - "uvicorn[standard]~=0.0", -] - [build-system] requires = ["hatchling"] build-backend = "hatchling.build" @@ -182,10 +146,10 @@ build-backend = "hatchling.build" all = ["example", "format", "lint", "test", "typecheck"] docs = "mkdocs serve {args}" docs-build = "mkdocs build {args}" - example = "pytest --ignore=examples/v2 examples/ {args}" + example = "pytest examples/ {args}" format = "ruff format {args}" lint = "ruff check --output-format=full --show-fixes {args}" - test = "pytest --ignore=tests/v2 tests/ {args}" + test = "pytest tests/ {args}" typecheck = ["typecheck-examples", "typecheck-src", "typecheck-tests"] typecheck-examples = "mypy examples/ {args}" typecheck-src = "mypy src/ {args}" @@ -208,37 +172,6 @@ build-backend = "hatchling.build" [[tool.hatch.envs.example.matrix]] python = ["3.11", "3.12", "3.13", "3.14", "3.15"] - [tool.hatch.envs.v2-test] - features = ["compat-v2"] - installer = "uv" - path = ".venv/v2-test" - pre-install-commands = ["uv pip install --group test-v2 -e ."] - - [tool.hatch.envs.v2-test.scripts] - all = ["test"] - test = "pytest tests/v2 {args}" - - [[tool.hatch.envs.v2-test.matrix]] - python = ["3.11", "3.12", "3.13", "3.14", "3.15"] - - [tool.hatch.envs.v2-example] - features = ["compat-v2"] - installer = "uv" - path = ".venv/v2-example" - pre-install-commands = ["uv pip install --group example-v2 -e ."] - - [tool.hatch.envs.v2-example.scripts] - all = ["example"] - example = "pytest examples/v2 {args}" - - [[tool.hatch.envs.v2-example.matrix]] - python = ["3.11", "3.12", "3.13", "3.14", "3.15"] - - [tool.mypy] - exclude = """(?x)^( - (src/pact|tests|examples)/v2/.*\\.pyi? - )$""" - [tool.pytest] addopts = [ # Coverage options @@ -273,8 +206,6 @@ build-backend = "hatchling.build" ] [tool.ruff] - extend-exclude = ["src/pact/v2/*", "tests/v2/*", "examples/v2/*"] - [tool.ruff.lint] select = ["ALL"] diff --git a/src/pact/v2/__init__.py b/src/pact/v2/__init__.py deleted file mode 100644 index ebd8a8a85..000000000 --- a/src/pact/v2/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Python methods for interactive with a Pact Mock Service.""" -import warnings - -from pact.v2.broker import Broker -from pact.v2.consumer import Consumer -from pact.v2.matchers import EachLike, Like, SomethingLike, Term, Format -from pact.v2.message_consumer import MessageConsumer -from pact.v2.message_pact import MessagePact -from pact.v2.message_provider import MessageProvider -from pact.v2.pact import Pact -from pact.v2.provider import Provider -from pact.v2.verifier import Verifier - -from pact.__version__ import __version__, __version_tuple__ - -warnings.warn( - "The `pact.v2` module is deprecated.", - stacklevel=1, - category=DeprecationWarning, -) - -__url__ = "https://github.com/pact-foundation/pact-python" -__license__ = "MIT" - -__all__ = [ - '__version__', - '__version_tuple__', - '__url__', - '__license__', - 'Broker', - 'Consumer', - 'EachLike', - 'Like', - 'MessageConsumer', - 'MessagePact', - 'MessageProvider', - 'Pact', - 'Provider', - 'SomethingLike', - 'Term', - 'Format', - 'Verifier', -] diff --git a/src/pact/v2/broker.py b/src/pact/v2/broker.py deleted file mode 100644 index 229c33bbd..000000000 --- a/src/pact/v2/broker.py +++ /dev/null @@ -1,116 +0,0 @@ -"""API for creating a contract and configuring the mock service.""" -from __future__ import unicode_literals - -import fnmatch -import logging -import os -from subprocess import Popen - -from pact_cli import _telemetry_env - -from .constants import BROKER_CLIENT_PATH - -log = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - -class Broker(): - """PactBroker helper functions.""" - - def __init__(self, broker_base_url=None, broker_username=None, broker_password=None, broker_token=None): - """ - Create a Broker instance. - - :param broker_base_url: URL of the pact broker that pacts will be - published to. Can also be supplied through the PACT_BROKER_BASE_URL - environment variable. Defaults to None. - :type broker_base_url: str - :param broker_username: Username to use when connecting to the pact - broker if authentication is required. Can also be supplied through - the PACT_BROKER_USERNAME environment variable. Defaults to None. - :type broker_username: str - :param broker_password: Password to use when connecting to the pact - broker if authentication is required. Strongly recommend supplying - this value through the PACT_BROKER_PASSWORD environment variable - instead. Defaults to None. - :type broker_password: str - :param broker_token: Authentication token to use when connecting to - the pact broker. Strongly recommend supplying this value through - the PACT_BROKER_TOKEN environment variable instead. - Defaults to None. - """ - self.broker_base_url = broker_base_url - self.broker_username = broker_username - self.broker_password = broker_password - self.broker_token = broker_token - - def _get_broker_base_url(self): - return self.broker_base_url or os.environ["PACT_BROKER_BASE_URL"] - - @staticmethod - def _normalize_consumer_name(name): - return name.lower().replace(' ', '_') - - def publish(self, consumer_name, version, pact_dir=None, - tag_with_git_branch=None, consumer_tags=None, branch=None, build_url=None, auto_detect_version_properties=None): - """Publish the generated pact files to the specified pact broker.""" - if self.broker_base_url is None \ - and "PACT_BROKER_BASE_URL" not in os.environ: - raise RuntimeError("No pact broker URL specified. " - + "Did you expect the PACT_BROKER_BASE_URL " - + "environment variable to be set?") - - pact_files = fnmatch.filter( - os.listdir(pact_dir), - self._normalize_consumer_name(consumer_name) + '*.json' - ) - pact_files = list(map(lambda pact_file: f'{pact_dir}/{pact_file}', pact_files)) - command = [ - BROKER_CLIENT_PATH, - 'publish', - '--consumer-app-version={}'.format(version)] - - command.append('--broker-base-url={}'.format(self._get_broker_base_url())) - - if self.broker_username is not None: - command.append('--broker-username={}'.format(self.broker_username)) - if self.broker_password is not None: - command.append('--broker-password={}'.format(self.broker_password)) - if self.broker_token is not None: - command.append('--broker-token={}'.format(self.broker_token)) - - command.extend(pact_files) - - if tag_with_git_branch: - command.append('--tag-with-git-branch') - - if consumer_tags is not None: - for tag in consumer_tags: - command.extend(['-t', tag]) - - if branch: - command.extend(['--branch={}'.format(branch)]) - - if build_url: - command.extend(['--build-url={}'.format(build_url)]) - - if auto_detect_version_properties is True: - command.append('--auto-detect-version-properties') - - if log.isEnabledFor(logging.DEBUG): - log_command: list[str] = [] - for arg in command: - if arg.startswith('--broker-password='): - log_command.append('--broker-password=') - elif arg.startswith('--broker-token='): - log_command.append('--broker-token=') - else: - log_command.append(arg) - - log.debug(f"PactBroker publish command: {log_command}") - - publish_process = Popen(command, env=_telemetry_env()) - publish_process.wait() - if publish_process.returncode != 0: - url = self._get_broker_base_url() - raise RuntimeError( - f"There was an error while publishing to the pact broker at {url}.") diff --git a/src/pact/v2/cli/__init__.py b/src/pact/v2/cli/__init__.py deleted file mode 100644 index dcc9917e8..000000000 --- a/src/pact/v2/cli/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Python methods for interactive with a Pact Mock Service.""" diff --git a/src/pact/v2/cli/verify.py b/src/pact/v2/cli/verify.py deleted file mode 100644 index 8c3f51762..000000000 --- a/src/pact/v2/cli/verify.py +++ /dev/null @@ -1,227 +0,0 @@ -"""Methods to verify previously created pacts.""" -import sys - -from pact.v2.verify_wrapper import path_exists, expand_directories, VerifyWrapper - -import click - - -@click.command() -@click.argument('pacts', nargs=-1) -@click.option( - 'base_url', '--provider-base-url', - help='Base URL of the provider to verify against.', - required=True) -@click.option( - 'pact_url', '--pact-url', - help='DEPRECATED: specify pacts as arguments instead.\n' - 'The URI of the pact to verify.' - ' Can be an HTTP URI, a local file or directory path. ' - ' It can be specified multiple times to verify several pacts.', - multiple=True) # Remove in major version 1.0.0 -@click.option( - 'pact_urls', '--pact-urls', - default=[], - help='DEPRECATED: specify pacts as arguments instead.\n' - 'The URI(s) of the pact to verify.' - ' Can be an HTTP URI(s) or local file path(s).' - ' Provide multiple URI separated by a comma.', - multiple=True) # Remove in major version 1.0.0 -@click.option( - 'states_url', '--provider-states-url', - help='DEPRECATED: URL to fetch the provider states for' - ' the given provider API.') # Remove in major version 1.0.0 -@click.option( - 'states_setup_url', '--provider-states-setup-url', - help='URL to send POST requests to setup a given provider state.') -@click.option( - 'username', '--pact-broker-username', - envvar='PACT_BROKER_USERNAME', - help='Username for Pact Broker basic authentication. Can also be specified' - ' via the environment variable PACT_BROKER_USERNAME.') -@click.option( - 'broker_base_url', '--pact-broker-url', - default='', - envvar='PACT_BROKER_BASE_URL', - help='Base URl for the Pact Broker instance to publish pacts to. Can also be specified' - ' via the environment variable PACT_BROKER_BASE_URL.') -@click.option( - 'consumer_version_tag', '--consumer-version-tag', - default=[], - multiple=True, - help='Retrieve the latest pacts with this consumer version tag. ' - 'Used in conjunction with --provider. May be specified multiple times.') -@click.option( - 'consumer_version_selector', '--consumer-version-selector', - default=[], - multiple=True, - help='Retrieve the latest pacts with this consumer version selector. ' - 'Used in conjunction with --provider. May be specified multiple times.') -@click.option( - 'provider_version_tag', '--provider-version-tag', - default=[], - multiple=True, - help='Tag to apply to the provider application version. ' - 'May be specified multiple times.') -@click.option( - 'provider_version_branch', '--provider-version-branch', - default='', - help='The name of the branch the provider version belongs to.') -@click.option( - 'password', '--pact-broker-password', - envvar='PACT_BROKER_PASSWORD', - help='Password for Pact Broker basic authentication. Can also be specified' - ' via the environment variable PACT_BROKER_PASSWORD.') -@click.option( - 'token', '--pact-broker-token', - envvar='PACT_BROKER_TOKEN', - help='Bearer token for Pact Broker authentication. Can also be specified' - ' via the environment variable PACT_BROKER_TOKEN.') -@click.option( - 'provider', '--provider', - default='', - help='Retrieve the latest pacts for this provider.') -@click.option( - 'headers', '--custom-provider-header', - envvar='CUSTOM_PROVIDER_HEADER', - multiple=True, - help='Header to add to provider state set up and ' - 'pact verification requests. ' - 'eg \'Authorization: Basic cGFjdDpwYWN0\'. ' - 'May be specified multiple times.') -@click.option( - 'timeout', '-t', '--timeout', - default=30, - help='The duration in seconds we should wait to confirm that the verification' - ' process was successful. Defaults to 30.', - type=int) -@click.option( - 'provider_app_version', '-a', '--provider-app-version', - help='The provider application version. ' - 'Required for publishing verification results.') -@click.option( - 'publish_verification_results', '-r', '--publish-verification-results', - default=False, - help='Publish verification results to the broker.', - is_flag=True) -@click.option( - '--verbose/--no-verbose', - default=False, - help='Toggle verbose logging, defaults to False.') -@click.option( - 'log_dir', '--log-dir', - help='The directory for the pact.log file.') -@click.option( - 'log_level', '--log-level', - help='The logging level.') -@click.option( - 'enable_pending', '--enable-pending/--no-enable-pending', - default=False, - help='Allow pacts which are in pending state to be verified without causing the ' - 'overall task to fail. For more information, see https://pact.io/pending', - is_flag=True) -@click.option( - 'include_wip_pacts_since', '--include-wip-pacts-since', - default=None, - help='Automatically include the pending pacts in the verification step. ' - 'For more information, see https://docs.pact.io/pact_broker/advanced_topics/wip_pacts/',) -def main(pacts, base_url, pact_url, pact_urls, states_url, states_setup_url, - username, broker_base_url, consumer_version_tag, consumer_version_selector, - provider_version_tag, password, token, provider, headers, timeout, - provider_app_version, publish_verification_results, verbose, log_dir, - log_level, enable_pending, include_wip_pacts_since, provider_version_branch): - """ - Verify one or more contracts against a provider service. - - Minimal example: - pact-verifier --provider-base-url=http://localhost:8080 ./pacts - """ # NOQA - error = click.style('Error:', fg='red') - warning = click.style('Warning:', fg='yellow') - all_pact_urls = list(pacts) + list(pact_url) - - for urls in pact_urls: # Remove in major version 1.0.0 - all_pact_urls.extend(p for p in urls.split(',') if p) - - if len(pact_urls) > 1: - click.echo( - warning - + ' Multiple --pact-urls arguments are deprecated. ' - + 'Please provide a comma separated list of pacts to --pact-urls, ' - + 'or multiple --pact-url arguments.') - - if not all_pact_urls and broker_not_provided(broker_base_url, provider): - click.echo( - error - + ' You must supply at least one pact file or directory ' - + 'to verify OR a Pact Broker and Provider.') - raise click.Abort() - - all_pact_urls = expand_directories(all_pact_urls) - - missing_files = [path for path in all_pact_urls if not path_exists(path)] - if missing_files: - click.echo( - error - + ' The following Pact files could not be found:\n' - + '\n'.join(missing_files)) - raise click.Abort() - - if publish_verification_results: - validate_publish(error, provider_app_version) - - options = { - 'broker_password': password, - 'broker_username': username, - 'broker_token': token, - 'broker_url': broker_base_url, - 'log_dir': log_dir, - 'log_level': log_level, - 'provider_app_version': provider_app_version, - 'custom_provider_headers': list(headers), - 'publish_verification_results': publish_verification_results, - 'timeout': timeout, - 'verbose': verbose, - 'consumer_tags': list(consumer_version_tag), - 'consumer_selectors': list(consumer_version_selector), - 'provider_tags': list(provider_version_tag), - 'provider_version_branch': provider_version_branch, - 'provider_states_setup_url': states_setup_url, - } - - options = dict(filter(lambda item: item[1] is not None, options.items())) - options = dict(filter(lambda item: item[1] != '', options.items())) - options = dict(filter(lambda item: is_empty_list(item), options.items())) - - success, logs = VerifyWrapper().call_verify(*all_pact_urls, - provider=provider, - provider_base_url=base_url, - enable_pending=enable_pending, - include_wip_pacts_since=include_wip_pacts_since, - **options) - sys.exit(success) - - -def validate_publish(error, provider_app_version): - """Publish results to broker.""" - if not provider_app_version: - click.echo( - error - + 'Provider application version is required ' - + 'to publish verification results to broker' - ) - raise click.Abort() - - -def broker_not_provided(broker_base_url, provider): - """Check if broker not provided.""" - return (broker_base_url == '' or provider == '') - - -def is_empty_list(item): - """Util for is empty lists.""" - return (not isinstance(item[1], list)) or (len(item[1]) != 0) - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/src/pact/v2/constants.py b/src/pact/v2/constants.py deleted file mode 100644 index 2caae7e84..000000000 --- a/src/pact/v2/constants.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -Constant values for the pact-python package. - -The constants in this module are simply re-exported from the `pact_cli` module. -""" - -import pact_cli - -BROKER_CLIENT_PATH = pact_cli.BROKER_CLIENT_PATH or "" -MESSAGE_PATH = pact_cli.MESSAGE_PATH or "" -MOCK_SERVICE_PATH = pact_cli.MOCK_SERVICE_PATH or "" -VERIFIER_PATH = pact_cli.VERIFIER_PATH or "" diff --git a/src/pact/v2/consumer.py b/src/pact/v2/consumer.py deleted file mode 100644 index 64fbec064..000000000 --- a/src/pact/v2/consumer.py +++ /dev/null @@ -1,159 +0,0 @@ -"""Classes and methods to describe contract Consumers.""" -from .pact import Pact -from .provider import Provider - - -class Consumer(object): - """ - A Pact consumer. - - Use this class to describe the service making requests to the provider and - then use `has_pact_with` to create a contract with a specific service: - - >>> from pact.v2 import Consumer, Provider - >>> consumer = Consumer('my-web-front-end') - >>> consumer.has_pact_with(Provider('my-backend-service')) - """ - - def __init__(self, name, service_cls=Pact, tags=None, - tag_with_git_branch=False, version='0.0.0', branch=None, build_url=None, auto_detect_version_properties=False): - """ - Create the Consumer class. - - :param name: The name of this Consumer. This will be shown in the Pact - when it is published. - :type name: str - :param service_cls: Pact, or a sub-class of it, to use when creating - the contracts. This is useful when you have a custom URL or port - for your mock service and want to use the same value on all of - your contracts. - :type service_cls: pact.Pact - :param tags: A list of strings to use as tags to use when publishing - to a pact broker. Defaults to None. - :type tags: list - :param tag_with_git_branch: A flag to determine whether to - automatically tag a pact with the current git branch name. - Defaults to False. - :type tag_with_git_branch: bool - :param version: The version of this Consumer. This will be used when - publishing pacts to a pact broker. Defaults to '0.0.0' - :param branch: The branch of this Consumer. - :type branch: str - :param build_url: The build URL that created the pact. - :type build_url: str - :param auto_detect_version_properties: Automatically detect the repository branch from known CI, - environment variables or git CLI. Supports Buildkite, Circle CI, Travis CI, GitHub Actions, - Jenkins, Hudson, AppVeyor, GitLab, CodeShip, Bitbucket and Azure DevOps.'. - Defaults to False. - :type auto_detect_version_properties: bool - """ - self.name = name - self.service_cls = service_cls - self.tags = tags - self.tag_with_git_branch = tag_with_git_branch - self.version = version - self.branch = branch - self.build_url = build_url - self.auto_detect_version_properties = auto_detect_version_properties - - def has_pact_with(self, provider, host_name='localhost', port=1234, - log_dir=None, ssl=False, sslcert=None, sslkey=None, - cors=False, publish_to_broker=False, - broker_base_url=None, broker_username=None, - broker_password=None, broker_token=None, pact_dir=None, - specification_version='2.0.0', - file_write_mode='overwrite'): - """ - Create a contract between the `provider` and this consumer. - - If you are running the Pact mock service in a non-default location, - you can provide the host name and port here: - - >>> from pact.v2 import Consumer, Provider - >>> consumer = Consumer('my-web-front-end') - >>> consumer.has_pact_with( - ... Provider('my-backend-service'), - ... host_name='192.168.1.1', - ... port=8000) - - :param provider: The provider service for this contract. - :type provider: pact.Provider - :param host_name: An optional host name to use when contacting the - Pact mock service. This will need to be the same host name used by - your code under test to contact the mock service. It defaults to: - `localhost`. - :type host_name: str - :param port: The TCP port to use when contacting the Pact mock service. - This will need to be the same port used by your code under test - to contact the mock service. It defaults to: 1234 - :type port: int - :param log_dir: The directory where logs should be written. Defaults to - the current directory. - :type log_dir: str - :param ssl: Flag to control the use of a self-signed SSL cert to run - the server over HTTPS , defaults to False. - :type ssl: bool - :param sslcert: Path to a custom self-signed SSL cert file, 'ssl' - option must be set to True to use this option. Defaults to None. - :type sslcert: str - :param sslkey: Path to a custom key and self-signed SSL cert key file, - 'ssl' option must be set to True to use this option. - Defaults to None. - :type sslkey: str - :param cors: Allow CORS OPTION requests to be accepted, - defaults to False. - :type cors: bool - :param publish_to_broker: Flag to control automatic publishing of - pacts to a pact broker. Defaults to False. - :type publish_to_broker: bool - :param broker_base_url: URL of the pact broker that pacts will be - published to. Defaults to None. - :type broker_base_url: str - :param broker_username: Username to use when connecting to the pact - broker if authentication is required. Defaults to None. - :type broker_username: str - :param broker_password: Password to use when connecting to the pact - broker if authentication is required. Defaults to None. - :type broker_password: str - :param broker_token: Authentication token to use when connecting to - the pact broker. Defaults to None. - :type broker_token: str - :param pact_dir: Directory where the resulting pact files will be - written. Defaults to the current directory. - :type pact_dir: str - :param specification_version: The Pact Specification version to use. - Defaults to '2.0.0'. - :type version: str - :param file_write_mode: How the mock service should apply multiple - calls to .verify(). Pass 'overwrite' to overwrite the generated - JSON file on every call to .verify() or pass 'merge' to merge all - interactions into the same JSON file. When using 'merge', make - sure to delete any existing JSON file before calling .verify() - for the first time. Defaults to 'overwrite'. - :type version: str - :return: A Pact object which you can use to define the specific - interactions your code will have with the provider. - :rtype: pact.Pact - """ - if not isinstance(provider, (Provider,)): - raise ValueError( - 'provider must be an instance of the Provider class.') - - return self.service_cls( - broker_base_url=broker_base_url, - broker_username=broker_username, - broker_password=broker_password, - broker_token=broker_token, - consumer=self, - provider=provider, - host_name=host_name, - port=port, - log_dir=log_dir, - ssl=ssl, - sslcert=sslcert, - sslkey=sslkey, - cors=cors, - pact_dir=pact_dir, - publish_to_broker=publish_to_broker, - specification_version=specification_version, - file_write_mode=file_write_mode) diff --git a/src/pact/v2/http_proxy.py b/src/pact/v2/http_proxy.py deleted file mode 100644 index 5c85d99dd..000000000 --- a/src/pact/v2/http_proxy.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Http Proxy to be used as provider url in verifier.""" -from fastapi import FastAPI, status, Request, HTTPException -import uvicorn as uvicorn -import logging -log = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - -app = FastAPI() -PROXY_PORT = 1234 -UVICORN_LOGGING_LEVEL = "error" -items = { - "states": None -} - - -def _match_states(payload): - """Match states in payload against stored message handlers.""" - log.debug(f'Find handler from payload: {payload}') - handlers = items["states"] - states = handlers['messageHandlers'] - log.debug(f'Setup states: {handlers}') - provider_states = payload['providerStates'] - - for state in provider_states: - matching_state = state['name'] - if matching_state in states: - return states[matching_state] - raise HTTPException(status_code=500, detail='No matched handler.') - - -@app.post("/") -async def root(request: Request): - """Match states with provided message handlers.""" - payload = await request.json() - message = _match_states(payload) - return {'contents': message} - - -@app.get('/ping', status_code=status.HTTP_200_OK) -def ping(): - """Check whether the server is available before setting up states.""" - return {"ping": "pong"} - - -@app.post("/setup", status_code=status.HTTP_201_CREATED) -async def setup(request: Request): - """Endpoint to setup states. - - Use localstack to store payload. - """ - payload = await request.json() - items["states"] = payload - return items["states"] - - -def run_proxy(): - """Rub HTTP Proxy.""" - uvicorn.run("pact.v2.http_proxy:app", port=PROXY_PORT, log_level=UVICORN_LOGGING_LEVEL) diff --git a/src/pact/v2/matchers.py b/src/pact/v2/matchers.py deleted file mode 100644 index 0ef0bbe37..000000000 --- a/src/pact/v2/matchers.py +++ /dev/null @@ -1,452 +0,0 @@ -"""Classes for defining request and response data that is variable.""" -import six -import datetime - -from enum import Enum - - -class Matcher(object): - """Base class for defining complex contract expectations.""" - - def generate(self): - """ - Get the value that the mock service should use for this Matcher. - - :rtype: any - """ - raise NotImplementedError - - -class EachLike(Matcher): - """ - Expect the data to be a list of similar objects. - - Example: - >>> from pact.v2 import Consumer, Provider - >>> pact = Consumer('consumer').has_pact_with(Provider('provider')) - >>> (pact.given('there are three comments') - ... .upon_receiving('a request for the most recent 2 comments') - ... .with_request('get', '/comment', query={'limit': 2}) - ... .will_respond_with(200, body={ - ... 'comments': EachLike( - ... {'name': SomethingLike('bob'), - ... 'text': SomethingLike('Hello!')}, - ... minimum=2) - ... })) - Would expect the response to be a JSON object, with a comments list. In - that list should be at least 2 items, and each item should be a `dict` - with the keys `name` and `text`, - - """ - - def __init__(self, matcher, minimum=1): - """ - Create a new EachLike. - - :param matcher: The expected value that each item in a list should - look like, this can be other matchers. - :type matcher: None, list, dict, int, float, str, unicode, Matcher - :param minimum: The minimum number of items expected. - Must be greater than or equal to 1. - :type minimum: int - """ - self.matcher = matcher - assert minimum >= 1, 'Minimum must be greater than or equal to 1' - self.minimum = minimum - - def generate(self): - """ - Generate the value the mock service will return. - - :return: A dict containing the information about the contents of the - list and the provided minimum number of items for that list. - :rtype: dict - """ - return { - 'json_class': 'Pact::ArrayLike', - 'contents': from_term(self.matcher), - 'min': self.minimum} - - -class Like(Matcher): - """ - Expect the type of the value to be the same as matcher. - - Example: - >>> from pact.v2 import Consumer, Provider - >>> pact = Consumer('consumer').has_pact_with(Provider('provider')) - >>> (pact - ... .given('there is a random number generator') - ... .upon_receiving('a request for a random number') - ... .with_request('get', '/generate-number') - ... .will_respond_with(200, body={ - ... 'number': Like(1111222233334444) - ... })) - - Would expect the response body to be a JSON object, containing the key - `number`, which would contain an integer. When the consumer runs this - contract, the value `1111222233334444` will be returned by the mock - service, instead of a randomly generated value. - - """ - - def __init__(self, matcher): - """ - Create a new SomethingLike. - - :param matcher: The object that should be expected. The mock service - will return this value. When verified against the provider, the - type of this value will be asserted, while the value will be - ignored. - :type matcher: None, list, dict, int, float, str, unicode, Matcher - """ - valid_types = ( - type(None), list, dict, int, float, six.string_types, Matcher) - - assert isinstance(matcher, valid_types), ( - "matcher must be one of '{}', got '{}'".format( - valid_types, type(matcher))) - - self.matcher = matcher - - def generate(self): - """ - Return the value that should be used in the request/response. - - :return: A dict containing the information about what the contents of - the response should be. - :rtype: dict - """ - return { - 'json_class': 'Pact::SomethingLike', - 'contents': from_term(self.matcher)} - - -# Remove SomethingLike in major version 1.0.0 -SomethingLike = Like - - -class Term(Matcher): - """ - Expect the response to match a specified regular expression. - - Example: - >>> from pact.v2 import Consumer, Provider - >>> pact = Consumer('consumer').has_pact_with(Provider('provider')) - >>> (pact.given('the current user is logged in as `tester`') - ... .upon_receiving('a request for the user profile') - ... .with_request('get', '/profile') - ... .will_respond_with(200, body={ - ... 'name': 'tester', - ... 'theme': Term('light|dark|legacy', 'dark') - ... })) - - Would expect the response body to be a JSON object, containing the key - `name`, which will contain the value `tester`, and `theme` which must be - one of the values: light, dark, or legacy. When the consumer runs this - contract, the value `dark` will be returned by the mock service. - - """ - - def __init__(self, matcher, generate): - """ - Create a new Term. - - :param matcher: A regular expression to find. - :type matcher: basestring - :param generate: A value to be returned by the mock service when - generating the response to the consumer. - :type generate: basestring - """ - self.matcher = matcher - self._generate = generate - - def generate(self): - """ - Return the value that should be used in the request/response. - - :return: A dict containing the information about what the contents of - the response should be, and what should match for the requests. - :rtype: dict - """ - return { - 'json_class': 'Pact::Term', - 'data': { - 'generate': self._generate, - 'matcher': { - 'json_class': 'Regexp', - 'o': 0, - 's': self.matcher}}} - - -def from_term(term): - """ - Parse the provided term into the JSON for the mock service. - - :param term: The term to be parsed. - :type term: None, list, dict, int, float, str, bytes, unicode, Matcher - :return: The JSON representation for this term. - :rtype: dict, list, str - """ - if term is None: - return term - elif isinstance(term, (six.string_types, six.binary_type, int, float)): - return term - elif isinstance(term, dict): - return {k: from_term(v) for k, v in term.items()} - elif isinstance(term, list): - return [from_term(t) for i, t in enumerate(term)] - elif issubclass(term.__class__, (Matcher,)): - return term.generate() - else: - raise ValueError('Unknown type: %s' % type(term)) - - -def get_generated_values(input): - """ - Resolve (nested) Matchers to their generated values for assertion. - - :param input: The input to be resolved to its generated values. - :type input: None, list, dict, int, float, bool, str, unicode, Matcher - :return: The input resolved to its generated value(s) - :rtype: None, list, dict, int, float, bool, str, unicode, Matcher - """ - if input is None: - return input - if isinstance(input, (six.string_types, int, float, bool)): - return input - if isinstance(input, dict): - return {k: get_generated_values(v) for k, v in input.items()} - if isinstance(input, list): - return [get_generated_values(t) for i, t in enumerate(input)] - elif isinstance(input, Like): - return get_generated_values(input.matcher) - elif isinstance(input, EachLike): - return [get_generated_values(input.matcher)] * input.minimum - elif isinstance(input, Term): - return input.generate()['data']['generate'] - else: - raise ValueError('Unknown type: %s' % type(input)) - - -class Format: - """ - Class of regular expressions for common formats. - - Example: - >>> from pact.v2 import Consumer, Provider - >>> from pact.v2.matchers import Format - >>> pact = Consumer('consumer').has_pact_with(Provider('provider')) - >>> (pact.given('the current user is logged in as `tester`') - ... .upon_receiving('a request for the user profile') - ... .with_request('get', '/profile') - ... .will_respond_with(200, body={ - ... 'id': Format().identifier, - ... 'lastUpdated': Format().time - ... })) - - Would expect `id` to be any valid int and `lastUpdated` to be a valid time. - When the consumer runs this contract, the value of that will be returned - is the second value passed to Term in the given function, for the time - example it would be datetime.datetime(2000, 2, 1, 12, 30, 0, 0).time() - - """ - - def __init__(self): - """Create a new Formatter.""" - self.identifier = self.integer_or_identifier() - self.integer = self.integer_or_identifier() - self.decimal = self.decimal() - self.ip_address = self.ip_address() - self.hexadecimal = self.hexadecimal() - self.ipv6_address = self.ipv6_address() - self.uuid = self.uuid() - self.timestamp = self.timestamp() - self.date = self.date() - self.time = self.time() - self.iso_datetime = self.iso_8601_datetime() - self.iso_datetime_ms = self.iso_8601_datetime(with_ms=True) - - def integer_or_identifier(self): - """ - Match any integer. - - :return: a Like object with an integer. - :rtype: Like - """ - return Like(1) - - def decimal(self): - """ - Match any decimal. - - :return: a Like object with a decimal. - :rtype: Like - """ - return Like(1.0) - - def ip_address(self): - """ - Match any ip address. - - :return: a Term object with an ip address regex. - :rtype: Term - """ - return Term(self.Regexes.ip_address.value, '127.0.0.1') - - def hexadecimal(self): - """ - Match any hexadecimal. - - :return: a Term object with a hexadecimal regex. - :rtype: Term - """ - return Term(self.Regexes.hexadecimal.value, '3F') - - def ipv6_address(self): - """ - Match any ipv6 address. - - :return: a Term object with an ipv6 address regex. - :rtype: Term - """ - return Term(self.Regexes.ipv6_address.value, '::ffff:192.0.2.128') - - def uuid(self): - """ - Match any uuid. - - :return: a Term object with a uuid regex. - :rtype: Term - """ - return Term( - self.Regexes.uuid.value, 'fc763eba-0905-41c5-a27f-3934ab26786c' - ) - - def timestamp(self): - """ - Match any timestamp. - - :return: a Term object with a timestamp regex. - :rtype: Term - """ - return Term( - self.Regexes.timestamp.value, datetime.datetime( - 2000, 2, 1, 12, 30, 0, 0 - ).isoformat() - ) - - def date(self): - """ - Match any date. - - :return: a Term object with a date regex. - :rtype: Term - """ - return Term( - self.Regexes.date.value, datetime.datetime( - 2000, 2, 1, 12, 30, 0, 0 - ).date().isoformat() - ) - - def time(self): - """ - Match any time. - - :return: a Term object with a time regex. - :rtype: Term - """ - return Term( - self.Regexes.time_regex.value, datetime.datetime( - 2000, 2, 1, 12, 30, 0, 0 - ).time().isoformat() - ) - - def iso_8601_datetime(self, with_ms=False): - """ - Match a string for a full ISO 8601 Date. - - Does not do any sort of date validation, only checks if the string is - according to the ISO 8601 spec. - - This method differs from :func:`~pact.Format.timestamp`, - :func:`~pact.Format.date` and :func:`~pact.Format.time` implementations - in that it is more stringent and tests the string for exact match to - the ISO 8601 dates format. - - Without `with_ms` will match string containing ISO 8601 formatted dates - as stated bellow: - - * 2016-12-15T20:16:01 - * 2010-05-01T01:14:31.876 - * 2016-05-24T15:54:14.00000Z - * 1994-11-05T08:15:30-05:00 - * 2002-01-31T23:00:00.1234-02:00 - * 1991-02-20T06:35:26.079043+00:00 - - Otherwise, ONLY dates with milliseconds will match the pattern: - - * 2010-05-01T01:14:31.876 - * 2016-05-24T15:54:14.00000Z - * 2002-01-31T23:00:00.1234-02:00 - * 1991-02-20T06:35:26.079043+00:00 - - :param with_ms: Enforcing millisecond precision. - :type with_ms: bool - :return: a Term object with a date regex. - :rtype: Term - """ - date = [1991, 2, 20, 6, 35, 26] - if with_ms: - matcher = self.Regexes.iso_8601_datetime_ms.value - date.append(79043) - else: - matcher = self.Regexes.iso_8601_datetime.value - - return Term( - matcher, - datetime.datetime(*date, tzinfo=datetime.timezone.utc).isoformat() - ) - - class Regexes(Enum): - """Regex Enum for common formats.""" - - ip_address = r'(\d{1,3}\.)+\d{1,3}' - hexadecimal = r'[0-9a-fA-F]+' - ipv6_address = r'(\A([0-9a-f]{1,4}:){1,1}(:[0-9a-f]{1,4}){1,6}\Z)|' \ - r'(\A([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}\Z)|(\A([0-9a-f]' \ - r'{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}\Z)|(\A([0-9a-f]{1,4}:)' \ - r'{1,4}(:[0-9a-f]{1,4}){1,3}\Z)|(\A([0-9a-f]{1,4}:){1,5}(:[0-' \ - r'9a-f]{1,4}){1,2}\Z)|(\A([0-9a-f]{1,4}:){1,6}(:[0-9a-f]{1,4})' \ - r'{1,1}\Z)|(\A(([0-9a-f]{1,4}:){1,7}|:):\Z)|(\A:(:[0-9a-f]{1,4})' \ - r'{1,7}\Z)|(\A((([0-9a-f]{1,4}:){6})(25[0-5]|2[0-4]\d|[0-1]' \ - r'?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})\Z)|(\A(([0-9a-f]' \ - r'{1,4}:){5}[0-9a-f]{1,4}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25' \ - r'[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})\Z)|(\A([0-9a-f]{1,4}:){5}:[' \ - r'0-9a-f]{1,4}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4' \ - r']\d|[0-1]?\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,1}(:[0-9a-f]' \ - r'{1,4}){1,4}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]' \ - r'\d|[0-1]?\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}' \ - r'){1,3}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0' \ - r'-1]?\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,' \ - r'2}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]' \ - r'?\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,1}:' \ - r'(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?' \ - r'\d)){3}\Z)|(\A(([0-9a-f]{1,4}:){1,5}|:):(25[0-5]|2[0-4]\d|[0' \ - r'-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\Z)|(\A:(:[' \ - r'0-9a-f]{1,4}){1,5}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]' \ - r'|2[0-4]\d|[0-1]?\d?\d)){3}\Z)' - uuid = r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' - timestamp = r'^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3(' \ - r'[12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-' \ - r'9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2' \ - r'[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d' \ - r'([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$' - date = r'^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|' \ - r'0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|' \ - r'[12]\d{2}|3([0-5]\d|6[1-6])))?)' - time_regex = r'^(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?$' - iso_8601_datetime = r'^\d{4}-[01]\d-[0-3]\d\x54[0-2]\d:[0-6]\d:' \ - r'[0-6]\d(?:\.\d+)?(?:(?:[+-]\d\d:?\d\d)|\x5A)?$' - iso_8601_datetime_ms = r'^\d{4}-[01]\d-[0-3]\d\x54[0-2]\d:[0-6]\d:' \ - r'[0-6]\d\.\d+(?:(?:[+-]\d\d:?\d\d)|\x5A)?$' diff --git a/src/pact/v2/message_consumer.py b/src/pact/v2/message_consumer.py deleted file mode 100644 index acbb5f141..000000000 --- a/src/pact/v2/message_consumer.py +++ /dev/null @@ -1,146 +0,0 @@ -"""Classes and methods to describe contract Consumers.""" -from .message_pact import MessagePact -from .provider import Provider - - -class MessageConsumer(object): - """ - A Pact message consumer. - - Use this class to describe the service making requests to the provider and - then use `has_pact_with` to create a contract with a specific service: - - >>> from pact.v2 import MessageConsumer, Provider - >>> message_consumer = MessageConsumer('my-web-front-end') - >>> message_consumer.has_pact_with(Provider('my-backend-service')) - """ - - def __init__( - self, - name, - service_cls=MessagePact, - tags=None, - tag_with_git_branch=False, - version="0.0.0", - branch=None, - build_url=None, - auto_detect_version_properties=False - ): - """ - Create the Message Consumer class. - - :param name: The name of this Consumer. This will be shown in the Pact - when it is published. - :type name: str - :param service_cls: Pact, or a sub-class of it, to use when creating - the contracts. This is useful when you have a custom URL or port - for your mock service and want to use the same value on all of - your contracts. - :type service_cls: pact.Pact - :param tags: A list of strings to use as tags to use when publishing - to a pact broker. Defaults to None. - :type tags: list - :param tag_with_git_branch: A flag to determine whether to - automatically tag a pact with the current git branch name. - Defaults to False. - :type tag_with_git_branch: bool - :param version: The version of this Consumer. This will be used when - publishing pacts to a pact broker. Defaults to '0.0.0' - :param branch: The branch of this Consumer. - :type branch: str - :param build_url: The build URL that created the pact. - :type build_url: str - :param auto_detect_version_properties: Automatically detect the repository branch from known CI, - environment variables or git CLI. Supports Buildkite, Circle CI, Travis CI, GitHub Actions, - Jenkins, Hudson, AppVeyor, GitLab, CodeShip, Bitbucket and Azure DevOps.'. - Defaults to False. - :type auto_detect_version_properties: bool - """ - self.name = name - self.service_cls = service_cls - self.tags = tags - self.tag_with_git_branch = tag_with_git_branch - self.version = version - self.branch = branch - self.build_url = build_url - self.auto_detect_version_properties = auto_detect_version_properties - - def has_pact_with( - self, - provider, - publish_to_broker=False, - broker_base_url=None, - broker_username=None, - broker_password=None, - broker_token=None, - pact_dir=None, - version="3.0.0", - file_write_mode="merge", - ): - """ - Create a contract between the `provider` and this consumer. - - If you are running the Pact mock service in a non-default location, - you can provide the host name and port here: - - >>> from pact.v2 import Consumer, Provider - >>> consumer = Consumer('my-web-front-end') - >>> consumer.has_pact_with( - ... Provider('my-backend-service'), - ... host_name='192.168.1.1', - ... port=8000) - - :param provider: The provider service for this contract. - :type provider: pact.Provider - :param host_name: An optional host name to use when contacting the - Pact mock service. This will need to be the same host name used by - your code under test to contact the mock service. It defaults to: - `localhost`. - :type host_name: str - :param publish_to_broker: Flag to control automatic publishing of - pacts to a pact broker. Defaults to False. - :type publish_to_broker: bool - :param broker_base_url: URL of the pact broker that pacts will be - published to. Defaults to None. - :type broker_base_url: str - :param broker_username: Username to use when connecting to the pact - broker if authentication is required. Defaults to None. - :type broker_username: str - :param broker_password: Password to use when connecting to the pact - broker if authentication is required. Defaults to None. - :type broker_password: str - :param broker_token: Authentication token to use when connecting to - the pact broker. Defaults to None. - :type broker_token: str - :param pact_dir: Directory where the resulting pact files will be - written. Defaults to the current directory. - :type pact_dir: str - :param version: The Pact Specification version to use, defaults to - '3.0.0'. - :type version: str - :param file_write_mode: How the mock service should apply multiple - calls to .verify(). Pass 'overwrite' to overwrite the generated - JSON file on every call to .verify() or pass 'merge' to merge all - interactions into the same JSON file. When using 'merge', make - sure to delete any existing JSON file before calling .verify() - for the first time. Defaults to 'merge'. - :type version: str - :return: A Pact object which you can use to define the specific - interactions your code will have with the provider. - :rtype: pact.Pact - """ - if not isinstance(provider, (Provider,)): - raise ValueError("provider must be an instance of the Provider class.") - - return self.service_cls( - broker_base_url=broker_base_url, - broker_username=broker_username, - broker_password=broker_password, - broker_token=broker_token, - consumer=self, - provider=provider, - pact_dir=pact_dir, - publish_to_broker=publish_to_broker, - version=version, - file_write_mode=file_write_mode, - ) diff --git a/src/pact/v2/message_pact.py b/src/pact/v2/message_pact.py deleted file mode 100644 index 2eaee6ff0..000000000 --- a/src/pact/v2/message_pact.py +++ /dev/null @@ -1,225 +0,0 @@ -"""API for creating a contract and configuring the mock service.""" -from __future__ import unicode_literals - -import json -import os -from subprocess import Popen - -from pact_cli import _telemetry_env - -from .broker import Broker -from .constants import MESSAGE_PATH -from .matchers import from_term - - -class MessagePact(Broker): - """ - Represents a contract between a consumer and provider using messages. - - Provides Python context handler to perform tests on a Python consumer. For example: - - >>> from pact.v2 import MessageConsumer, Provider - >>> pact = MessageConsumer('MyMessageConsumer').has_pact_with(Provider('provider')) - >>> (pact - ... .given({"name": "Test provider"}]) - ... .expects_to_receive('Test description') - ... .with_content({'name': 'John', 'document_name': 'sample_document.doc'}) - ... .with_metadata({'contentType': 'application/json'})) - >>> with pact: - ... handler(event, context) - """ - - MANDATORY_FIELDS = {"providerStates", "description", "contents", "metaData"} - - def __init__( - self, - consumer, - provider, - publish_to_broker=False, - broker_base_url=None, - broker_username=None, - broker_password=None, - broker_token=None, - pact_dir=None, - version='3.0.0', - file_write_mode="merge", - ): - """ - Create a Pact instance using messages. - - :param consumer: A consumer for this contract that uses messages. - :type consumer: pact.MessageConsumer - :param provider: The generic provider for this contract. - :type provider: pact.Provider - :param publish_to_broker: Flag to control automatic publishing of - pacts to a pact broker. Defaults to False. - :type publish_to_broker: bool - :param broker_base_url: URL of the pact broker that pacts will be - published to. Can also be supplied through the PACT_BROKER_BASE_URL - environment variable. Defaults to None. - :type broker_base_url: str - :param broker_username: Username to use when connecting to the pact - broker if authentication is required. Can also be supplied through - the PACT_BROKER_USERNAME environment variable. Defaults to None. - :type broker_username: str - :param broker_password: Password to use when connecting to the pact - broker if authentication is required. Strongly recommend supplying - this value through the PACT_BROKER_PASSWORD environment variable - instead. Defaults to None. - :type broker_password: str - :param broker_token: Authentication token to use when connecting to - the pact broker. Strongly recommend supplying this value through - the PACT_BROKER_TOKEN environment variable instead. - Defaults to None. - :type broker_token: str - :param pact_dir: Directory where the resulting pact files will be - written. Defaults to the current directory. - :type pact_dir: str - :param version: The Pact Specification version to use, defaults to - '3.0.0'. - :type version: str - :param file_write_mode: `overwrite` or `merge`. Use `merge` when - running multiple mock service instances in parallel for the same - consumer/provider pair. Ensure the pact file is deleted before - running tests when using this option so that interactions deleted - from the code are not maintained in the file. Defaults to - `merge`. - :type file_write_mode: str - """ - super().__init__( - broker_base_url, broker_username, broker_password, broker_token - ) - - self.consumer = consumer - self.file_write_mode = file_write_mode - self.pact_dir = pact_dir or os.getcwd() - self.provider = provider - self.publish_to_broker = publish_to_broker - self.version = version - self._process = None - self._messages = [] - self._message_process = None - - def given(self, name, params=None): - """ - Define the provider state for this pact. - - When the provider verifies this contract, they will use this field to - setup pre-defined data that will satisfy the response expectations. - - :param name: The short sentence that is unique to describe the provider - state for this contract. - :type name: basestring - :param params: Additional arguments necessary to set the provider state - :type params: dict - :rtype: Pact - """ - self._insert_message_if_complete() - - provider_state = {'name': "{}".format(name)} - if params: - provider_state['params'] = params - - if 'providerStates' not in self._messages[0]: - self._messages[0]['providerStates'] = [provider_state] - else: - self._messages[0]['providerStates'].append(provider_state) - - return self - - def with_metadata(self, metadata): - """ - Define metadata attached to the message. - - :param metadata: dictionary of metadata attached to the message. - :type metadata: dict or None - :rtype: Pact - """ - self._insert_message_if_complete() - self._messages[0]['metaData'] = from_term(metadata) - return self - - def with_content(self, contents): - """ - Define message content (event) that will be use in the message. - - :param contents: dictionary of dictionary used in the message. - :type metadata: dict - :rtype: Pact - """ - self._insert_message_if_complete() - self._messages[0]['contents'] = from_term(contents) - return self - - def expects_to_receive(self, description): - """ - Define the name of this contract (Same as upon_receiving in http pact implementation). - - :param scenario: A unique name for this contract. - :type scenario: basestring - :rtype: Pact - """ - self._insert_message_if_complete() - self._messages[0]['description'] = description - return self - - def write_to_pact_file(self): - """ - Create a pact file based on provided attributes in DSL. - - Return 0 if success, 1 otherwise. - - :rtype: int - """ - command = [ - MESSAGE_PATH, - "update", - json.dumps(self._messages[0]), - "--pact-dir", self.pact_dir, - f"--pact-specification-version={self.version}", - "--consumer", f"{self.consumer.name}", - "--provider", f"{self.provider.name}", - ] - - self._message_process = Popen(command, env=_telemetry_env()) - self._message_process.wait() - - def _insert_message_if_complete(self): - """ - Insert a new message if current message is complete. - - An interaction is complete if it has all the mandatory fields. - If there are no message, a new message will be added. - :rtype: None - """ - if not self._messages: - self._messages.append({}) - elif all(field in self._messages[0] for field in self.MANDATORY_FIELDS): - self._messages.insert(0, {}) - - def __enter__(self): - """Enter a Python context. This function is required for context manager to work.""" - pass - - def __exit__(self, exc_type, exc_val, exc_tb): - """ - Exit a Python context. - - Calls pact-message to write out the contracts to disk. - """ - if (exc_type, exc_val, exc_tb) != (None, None, None): - return - - self.write_to_pact_file() - - if self.publish_to_broker: - self.publish( - self.consumer.name, - self.consumer.version, - pact_dir=self.pact_dir, - tag_with_git_branch=self.consumer.tag_with_git_branch, - consumer_tags=self.consumer.tags, - branch=self.consumer.branch, - build_url=self.consumer.build_url, - auto_detect_version_properties=self.consumer.auto_detect_version_properties - ) diff --git a/src/pact/v2/message_provider.py b/src/pact/v2/message_provider.py deleted file mode 100644 index f132a7db4..000000000 --- a/src/pact/v2/message_provider.py +++ /dev/null @@ -1,163 +0,0 @@ -"""Contract Message Provider.""" -import os -import time - -import requests -from requests.adapters import HTTPAdapter -from requests.packages.urllib3 import Retry -from multiprocessing import Process - -from pact._util import user_agent - -from .verifier import Verifier -from .http_proxy import run_proxy - -import logging -logging.getLogger("urllib3").setLevel(logging.ERROR) - - -class MessageProvider(object): - """ - A Pact message provider. - - provider = MessageProvider( - message_providers = { - "a document created successfully": handler - }, - provider='DocumentService', - pact_dir='pacts', - version='3.0.0' - ) - """ - - HEADERS = {'User-Agent': user_agent()} - - def __init__( - self, - message_providers, - provider, - consumer, - pact_dir=os.getcwd(), - version="3.0.0", - proxy_host='localhost', - proxy_port='1234' - ): - """Create a Message Provider instance.""" - self.message_providers = message_providers - self.provider = provider - self.consumer = consumer - self.version = version - self.pact_dir = pact_dir - self.proxy_host = proxy_host - self.proxy_port = proxy_port - self._process = None - - def _proxy_url(self): - return f'http://{self.proxy_host}:{self.proxy_port}' - - def _pact_file(self): - return f'{self.consumer}-{self.provider}.json'.lower().replace(' ', '_') - - def _setup_states(self): - message_handlers = {} - for key, handler in self.message_providers.items(): - message_handlers[f'{key}'] = handler() - - resp = requests.post(f'{self._proxy_url()}/setup', - headers=self.HEADERS, - verify=False, - json={"messageHandlers": message_handlers}) - assert resp.status_code == 201, resp.text - return message_handlers - - def _wait_for_server_start(self): - """ - Wait for the Http Proxy to be ready. - - :rtype: None - :raises RuntimeError: If there is a problem starting the Http Proxy. - """ - s = requests.Session() - retries = Retry(total=9, backoff_factor=0.5) - http_mount = 'http://' - s.mount(http_mount, HTTPAdapter(max_retries=retries)) - resp = s.get(f'{self._proxy_url()}/ping', headers=self.HEADERS, verify=False) - if resp.status_code != 200: - self._stop_proxy() - raise RuntimeError( - 'There was a problem starting the proxy: %s', resp.text - ) - - def _wait_for_server_stop(self): - """Wait for server to finish, or raise exception after timeout.""" - retry = 20 - while True: - self._process.terminate() - time.sleep(0.1) - try: - assert not self._process.is_alive() - return 0 - except AssertionError: - if retry == 0: - raise RuntimeError("Process timed out") - retry -= 1 - - def _start_proxy(self): - self._process = Process(target=run_proxy, args=(), daemon=True) - self._process.start() - self._wait_for_server_start() - self._setup_states() - - def _stop_proxy(self): - """Stop the Http Proxy.""" - if isinstance(self._process, Process): - self._wait_for_server_stop() - - def verify(self): - """Verify pact files with executable verifier.""" - pact_files = f'{self.pact_dir}/{self._pact_file()}' - verifier = Verifier(provider=self.provider, - provider_base_url=self._proxy_url()) - return_code, _ = verifier.verify_pacts(pact_files, verbose=False) - assert (return_code == 0), f'Expected returned_code = 0, actual = {return_code}' - - def verify_with_broker(self, enable_pending=False, include_wip_pacts_since=None, **kwargs): - """Use Broker to verify. - - Args: - broker_username ([String]): broker username - broker_password ([String]): broker password - broker_url ([String]): url of broker - enable_pending ([Boolean]) - include_wip_pacts_since ([String]) - publish_version ([String]) - - """ - verifier = Verifier(provider=self.provider, - provider_base_url=self._proxy_url()) - - return_code, _ = verifier.verify_with_broker(enable_pending, include_wip_pacts_since, **kwargs) - - assert (return_code == 0), f'Expected returned_code = 0, actual = {return_code}' - - def __enter__(self): - """ - Enter a Python context. - - Sets up the Http Proxy. - """ - self._start_proxy() - - def __exit__(self, exc_type, exc_val, exc_tb): - """ - Exit a Python context. - - Return False to cascade the exception in context manager's body. - Otherwise it will be suppressed and the test will always pass. - """ - if (exc_type, exc_val, exc_tb) != (None, None, None): - if exc_type is not None: - self._stop_proxy() - return False - self._stop_proxy() - return True diff --git a/src/pact/v2/pact.py b/src/pact/v2/pact.py deleted file mode 100644 index b01ab5e1b..000000000 --- a/src/pact/v2/pact.py +++ /dev/null @@ -1,465 +0,0 @@ -"""API for creating a contract and configuring the mock service.""" -from __future__ import unicode_literals - -import os -import platform -from subprocess import Popen - -import psutil -import requests -from requests.adapters import HTTPAdapter -from requests.packages.urllib3 import Retry - -from pact_cli import _telemetry_env - -from pact._util import user_agent - -from .broker import Broker -from .constants import MOCK_SERVICE_PATH -from .matchers import from_term -from .verify_wrapper import PactException - - -class Pact(Broker): - """ - Represents a contract between a consumer and provider. - - Provides Python context handlers to configure the Pact mock service to - perform tests on a Python consumer. For example: - - >>> from pact.v2 import Consumer, Provider - >>> pact = Consumer('consumer').has_pact_with(Provider('provider')) - >>> (pact.given('the echo service is available') - ... .upon_receiving('a request is made to the echo service') - ... .with_request('get', '/echo', query={'text': 'Hello!'}) - ... .will_respond_with(200, body='Hello!')) - >>> with pact: - ... requests.get(pact.uri + '/echo?text=Hello!') - - The GET request is made to the mock service, which will verify that it - was a GET to /echo with a query string with a key named `text` and its - value is `Hello!`. If the request does not match an error is raised, if it - does match the defined interaction, it will respond with the text `Hello!`. - """ - - HEADERS = {'X-Pact-Mock-Service': 'true', 'User-Agent': user_agent()} - - MANDATORY_FIELDS = {'response', 'description', 'request'} - - def __init__( - self, - consumer, - provider, - host_name='localhost', - port=1234, - log_dir=None, - ssl=False, - sslcert=None, - sslkey=None, - cors=False, - publish_to_broker=False, - broker_base_url=None, - broker_username=None, - broker_password=None, - broker_token=None, - pact_dir=None, - specification_version='2.0.0', - file_write_mode='overwrite', - ): - """ - Create a Pact instance. - - :param consumer: The consumer for this contract. - :type consumer: pact.Consumer - :param provider: The provider for this contract. - :type provider: pact.Provider - :param host_name: The host name where the mock service is running. - :type host_name: str - :param port: The port number where the mock service is running. - :type port: int - :param log_dir: The directory where logs should be written. Defaults to - the current directory. - :type log_dir: str - :param ssl: Flag to control the use of a self-signed SSL cert to run - the server over HTTPS , defaults to False. - :type ssl: bool - :param sslcert: Path to a custom self-signed SSL cert file, 'ssl' - option must be set to True to use this option. Defaults to None. - :type sslcert: str - :param sslkey: Path to a custom key and self-signed SSL cert key file, - 'ssl' option must be set to True to use this option. - Defaults to None. - :type sslkey: str - :param cors: Allow CORS OPTION requests to be accepted, - defaults to False. - :type cors: bool - :param publish_to_broker: Flag to control automatic publishing of - pacts to a pact broker. Defaults to False. - :type publish_to_broker: bool - :param broker_base_url: URL of the pact broker that pacts will be - published to. Can also be supplied through the PACT_BROKER_BASE_URL - environment variable. Defaults to None. - :type broker_base_url: str - :param broker_username: Username to use when connecting to the pact - broker if authentication is required. Can also be supplied through - the PACT_BROKER_USERNAME environment variable. Defaults to None. - :type broker_username: str - :param broker_password: Password to use when connecting to the pact - broker if authentication is required. Strongly recommend supplying - this value through the PACT_BROKER_PASSWORD environment variable - instead. Defaults to None. - :type broker_password: str - :param broker_token: Authentication token to use when connecting to - the pact broker. Strongly recommend supplying this value through - the PACT_BROKER_TOKEN environment variable instead. - Defaults to None. - :type broker_token: str - :param pact_dir: Directory where the resulting pact files will be - written. Defaults to the current directory. - :type pact_dir: str - :param specification_version: The Pact Specification version to use, defaults to - '2.0.0'. - :type version: str of the consumer version. - :param file_write_mode: `overwrite` or `merge`. Use `merge` when - running multiple mock service instances in parallel for the same - consumer/provider pair. Ensure the pact file is deleted before - running tests when using this option so that interactions deleted - from the code are not maintained in the file. Defaults to - `overwrite`. - :type file_write_mode: str - """ - super().__init__( - broker_base_url, broker_username, broker_password, broker_token - ) - - scheme = 'https' if ssl else 'http' - self.uri = '{scheme}://{host_name}:{port}'.format( - host_name=host_name, port=port, scheme=scheme) - self.consumer = consumer - self.cors = cors - self.file_write_mode = file_write_mode - self.host_name = host_name - self.log_dir = log_dir or os.getcwd() - self.pact_dir = pact_dir or os.getcwd() - self.port = port - self.provider = provider - self.publish_to_broker = publish_to_broker - self.ssl = ssl - self.sslcert = sslcert - self.sslkey = sslkey - self.specification_version = specification_version - self._interactions = [] - self._process = None - - def given(self, provider_state): - """ - Define the provider state for this pact. - - When the provider verifies this contract, they will use this field to - setup pre-defined data that will satisfy the response expectations. - - :param provider_state: The short sentence that is unique to describe - the provider state for this contract. - :type provider_state: basestring - :rtype: Pact - """ - self._insert_interaction_if_complete() - self._interactions[0]['provider_state'] = provider_state - return self - - def setup(self): - """Configure the Mock Service to ready it for a test.""" - try: - # First, check that the interactions are all complete - for interaction in self._interactions: - missing_fields = [f for f in self.MANDATORY_FIELDS if f not in interaction] - if missing_fields: - raise PactException(f"Interaction incomplete, missing field(s): {', '.join(missing_fields)}") - - interactions_uri = f"{self.uri}/interactions" - resp = requests.delete( - interactions_uri, headers=self.HEADERS, verify=False - ) - - assert resp.status_code == 200, resp.text - resp = requests.put( - interactions_uri, - headers=self.HEADERS, - verify=False, - json={"interactions": self._interactions}, - ) - - assert resp.status_code == 200, resp.text - except AssertionError: - raise - - def start_service(self): - """ - Start the external Mock Service. - - :raises RuntimeError: if there is a problem starting the mock service. - """ - command = [ - MOCK_SERVICE_PATH, - "service", - f"--host={self.host_name}", - f"--port={format(self.port)}", - "--log", f"{self.log_dir}/pact-mock-service.log", - "--pact-dir", self.pact_dir, - "--pact-file-write-mode", self.file_write_mode, - f"--pact-specification-version={self.specification_version}", - "--consumer", self.consumer.name, - "--provider", self.provider.name, - ] - - if self.ssl: - command.append('--ssl') - if self.sslcert: - command.extend(['--sslcert', self.sslcert]) - if self.sslkey: - command.extend(['--sslkey', self.sslkey]) - if self.cors: - command.extend(['--cors']) - - self._process = Popen(command, env=_telemetry_env()) - self._wait_for_server_start() - - def stop_service(self): - """Stop the external Mock Service.""" - is_windows = 'windows' in platform.platform().lower() - if is_windows: - # Send the signal to ruby.exe, not the *.bat process - p = psutil.Process(self._process.pid) - for child in p.children(recursive=True): - child.terminate() - p.wait() - if psutil.pid_exists(self._process.pid): - raise RuntimeError( - 'There was an error when stopping the Pact mock service.') - - else: - self._process.terminate() - - self._process.communicate() - if self._process.returncode != 0: - raise RuntimeError( - 'There was an error when stopping the Pact mock service.' - ) - if self.publish_to_broker: - self.publish( - self.consumer.name, - self.consumer.version, - tag_with_git_branch=self.consumer.tag_with_git_branch, - consumer_tags=self.consumer.tags, - branch=self.consumer.branch, - pact_dir=self.pact_dir, - build_url=self.consumer.build_url, - auto_detect_version_properties=self.consumer.auto_detect_version_properties - ) - - def upon_receiving(self, scenario): - """ - Define the name of this contract. - - :param scenario: A unique name for this contract. - :type scenario: basestring - :rtype: Pact - """ - self._insert_interaction_if_complete() - self._interactions[0]['description'] = scenario - return self - - def verify(self): - """ - Have the mock service verify all interactions occurred. - - Calls the mock service to verify that all interactions occurred as - expected, and has it write out the contracts to disk. - - :raises AssertionError: When not all interactions are found. - """ - self._interactions = [] - resp = requests.get( - self.uri + "/interactions/verification", headers=self.HEADERS, verify=False - ) - assert resp.status_code == 200, resp.text - resp = requests.post(self.uri + "/pact", headers=self.HEADERS, verify=False) - assert resp.status_code == 200, resp.text - - def with_request(self, method, path, body=None, headers=None, query=None): - """ - Define the request that the client is expected to perform. - - :param method: The HTTP method. - :type method: str - :param path: The path portion of the URI the client will access. - :type path: str, Matcher - :param body: The request body, can be a string or an object that will - serialize to JSON, like list or dict, defaults to None. - :type body: list, dict or None - :param headers: The headers the client is expected to include on with - this request. Defaults to None. - :type headers: dict or None - :param query: The query options the client is expected to send. Can be - a dict of keys and values, or a URL encoded string. - Defaults to None. - :type query: dict, basestring, or None - :rtype: Pact - """ - self._insert_interaction_if_complete() - self._interactions[0]['request'] = Request( - method, path, body=body, headers=headers, query=query - ).json() - return self - - def will_respond_with(self, status, headers=None, body=None): - """ - Define the response the server is expected to create. - - :param status: The HTTP status code. - :type status: int - :param headers: All required headers. Defaults to None. - :type headers: dict or None - :param body: The response body, or a collection of Matcher objects to - allow for pattern matching. Defaults to None. - :type body: Matcher, dict, list, basestring, or None - :rtype: Pact - """ - self._insert_interaction_if_complete() - self._interactions[0]['response'] = Response( - status, headers=headers, body=body - ).json() - return self - - def _insert_interaction_if_complete(self): - """ - Insert a new interaction if current interaction is complete. - - An interaction is complete if it has all the mandatory fields. - If there are no interactions, a new interaction will be added. - - :rtype: None - """ - if not self._interactions: - self._interactions.append({}) - elif all(field in self._interactions[0] for field in self.MANDATORY_FIELDS): - self._interactions.insert(0, {}) - - def _wait_for_server_start(self): - """ - Wait for the mock service to be ready for requests. - - :rtype: None - :raises RuntimeError: If there is a problem starting the mock service. - """ - s = requests.Session() - retries = Retry(total=9, backoff_factor=0.1) - http_mount = 'https://' if self.ssl else 'http://' - s.mount(http_mount, HTTPAdapter(max_retries=retries)) - - resp = s.get(self.uri, headers=self.HEADERS, verify=False) - if resp.status_code != 200: - self._process.terminate() - self._process.communicate() - raise RuntimeError( - 'There was a problem starting the mock service: %s', resp.text - ) - - def __enter__(self): - """ - Enter a Python context. - - Sets up the mock service to expect the client requests. - """ - self.setup() - - def __exit__(self, exc_type, exc_val, exc_tb): - """ - Exit a Python context. - - Calls the mock service to verify that all interactions occurred as - expected, and has it write out the contracts to disk. - """ - self._interactions = [] - if (exc_type, exc_val, exc_tb) != (None, None, None): - return - - self.verify() - - -class FromTerms(object): - """Base class for objects built from a collection of Matchers.""" - - def json(self): - """Convert the object to a JSON version of the mock service.""" - raise NotImplementedError - - -class Request(FromTerms): - """Represents an HTTP request and supports Matchers on its properties.""" - - def __init__(self, method, path, body=None, headers=None, query=''): - """ - Create a new instance of Request. - - :param method: The HTTP method that is expected. - :type method: str - :param path: The URI path that is expected on this request. - :type path: str, Matcher - :param body: The contents of the body of the expected request. - :type body: str, dict, list - :param headers: The headers of the expected request. - :type headers: dict - :param query: The URI query of the expected request. - :type query: str or dict - """ - self.method = method - self.path = from_term(path) - self.body = from_term(body) - self.headers = from_term(headers) - self.query = from_term(query) - - def json(self): - """Convert the Request to a JSON version for the mock service.""" - request = {'method': self.method, 'path': self.path} - - if self.headers: - request['headers'] = self.headers - - if self.body is not None: - request['body'] = self.body - - if self.query: - request['query'] = self.query - - return request - - -class Response(FromTerms): - """Represents an HTTP response and supports Matchers on its properties.""" - - def __init__(self, status, headers=None, body=None): - """ - Create a new Response. - - :param status: The expected HTTP status of the response. - :type status: int - :param headers: The expected headers of the response. - :type headers: dict - :param body: The expected body of the response. - :type body: str, dict, or list - """ - self.status = status - self.body = from_term(body) - self.headers = from_term(headers) - - def json(self): - """Convert the Response to a JSON version for the mock service.""" - response = {'status': self.status} - if self.body is not None: - response['body'] = self.body - - if self.headers: - response['headers'] = self.headers - - return response diff --git a/src/pact/v2/provider.py b/src/pact/v2/provider.py deleted file mode 100644 index 543f0152d..000000000 --- a/src/pact/v2/provider.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Classes and methods to describe contract Providers.""" - - -class Provider(object): - """A Pact provider.""" - - def __init__(self, name): - """ - Create a new Provider. - - :param name: The name of this provider. This will be shown in the Pact - when it is published. - :type name: str - """ - self.name = name diff --git a/src/pact/v2/verifier.py b/src/pact/v2/verifier.py deleted file mode 100644 index d675d9539..000000000 --- a/src/pact/v2/verifier.py +++ /dev/null @@ -1,144 +0,0 @@ -"""Classes and methods to verify Contracts.""" -import json - -from pact.v2.verify_wrapper import VerifyWrapper, path_exists, expand_directories - - -class Verifier(object): - """A Pact Verifier.""" - - def __init__(self, provider, provider_base_url, **kwargs): - """Create a new Verifier. - - Args: - provider ([String]): provider name - provider_base_url ([String]): provider url - - """ - self.provider = provider - self.provider_base_url = provider_base_url - - def __str__(self): - """Return string representation. - - Returns: - [String]: verifier description. - - """ - return 'Verifier for {} with url {}'.format(self.provider, self.provider_base_url) - - def validate_publish(self, **kwargs): - """Validate publish has a version.""" - if (kwargs.get('publish') is not None) and (kwargs.get('publish_version') is None): - # do something - raise Exception() - - def verify_pacts(self, *pacts, enable_pending=False, include_wip_pacts_since=None, **kwargs): - """Verify our pacts from the provider. - - Returns: - success: True if no failures - logs: some tbd output of logs - - """ - self.validate_publish(**kwargs) - - missing_files = [path for path in pacts if not path_exists(path)] - if missing_files: - raise Exception("Missing pact files {}".format(missing_files)) - - pacts = expand_directories(pacts) - - options = self.extract_params(**kwargs) - success, logs = VerifyWrapper().call_verify(*pacts, - provider=self.provider, - provider_base_url=self.provider_base_url, - enable_pending=enable_pending, - include_wip_pacts_since=include_wip_pacts_since, - **options) - - return success, logs - - def verify_with_broker(self, enable_pending=False, include_wip_pacts_since=None, **kwargs): - """Use Broker to verify. - - Args: - broker_username ([String]): broker username - broker_password ([String]): broker password - broker_url ([String]): url of broker - enable_pending ([Boolean]) - include_wip_pacts_since ([String]) - publish_version ([String]) - - """ - broker_username = kwargs.get('broker_username', None) - broker_password = kwargs.get('broker_password', None) - broker_url = kwargs.get('broker_url', None) - broker_token = kwargs.get('broker_token', None) - - options = { - 'broker_password': broker_password, - 'broker_username': broker_username, - 'broker_token': broker_token, - 'broker_url': broker_url - } - options.update(self.extract_params(**kwargs)) - - success, logs = VerifyWrapper().call_verify(provider=self.provider, - provider_base_url=self.provider_base_url, - enable_pending=enable_pending, - include_wip_pacts_since=include_wip_pacts_since, - **options) - return success, logs - - def extract_params(self, **kwargs): - """Extract params.""" - log_dir = kwargs.get('log_dir', None) - log_level = kwargs.get('log_level', 'INFO') - headers = kwargs.get('headers', []) - timeout = kwargs.get('timeout', None) - consumer_tags = kwargs.get('consumer_tags', []) - provider_tags = kwargs.get('provider_tags', []) - states_setup_url = kwargs.get('provider_states_setup_url', None) - verbose = kwargs.get('verbose', False) - provider_app_version = kwargs.get('publish_version', None) - publish_verification_results = kwargs.get('publish_verification_results', None) - raw_consumer_selectors = kwargs.get('consumer_version_selectors', []) - consumer_selectors = self._build_consumer_selectors(raw_consumer_selectors) - provider_version_branch = kwargs.get('provider_version_branch') - - options = { - 'log_dir': log_dir, - 'log_level': log_level, - 'provider_app_version': provider_app_version, - 'custom_provider_headers': list(headers), - 'timeout': timeout, - 'consumer_tags': list(consumer_tags), - 'provider_tags': list(provider_tags), - 'provider_states_setup_url': states_setup_url, - 'verbose': verbose, - 'consumer_selectors': consumer_selectors, - 'publish_verification_results': publish_verification_results, - 'provider_version_branch': provider_version_branch - } - return self.filter_empty_options(**options) - - def _build_consumer_selectors(self, consumer_selectors): - """ - Build the consumer_selectors list. - - Turn each dict in the consumer_selectors list into a string with a - json object, as expected by VerifyWrapper. - """ - return [json.dumps(selector) for selector in consumer_selectors] - - def filter_empty_options(self, **kwargs): - """Filter out empty options.""" - kwargs = dict(filter(lambda item: item[1] is not None, kwargs.items())) - kwargs = dict(filter(lambda item: item[1] != '', kwargs.items())) - kwargs = dict(filter(lambda item: self.is_empty_list(item), kwargs.items())) - return kwargs - - def is_empty_list(self, item): - """Util for is empty lists.""" - return (not isinstance(item[1], list)) or (len(item[1]) != 0) diff --git a/src/pact/v2/verify_wrapper.py b/src/pact/v2/verify_wrapper.py deleted file mode 100644 index 7ead8b01d..000000000 --- a/src/pact/v2/verify_wrapper.py +++ /dev/null @@ -1,219 +0,0 @@ -"""Wrapper to verify previously created pacts.""" - -import os -import platform -import subprocess -import sys -from os import listdir -from os.path import isdir, isfile, join - -from pact_cli import VERIFIER_PATH, _telemetry_env - - -def capture_logs(process, verbose): - """Capture logs from ruby process.""" - result = '' - for line in process.stdout: - result = result + line + '\n' - - return result - - -def path_exists(path): - """ - Determine if a particular path exists. - - Can be provided a URL or local path. URLs always result in a True. Local - paths are True only if a file exists at that location. - - :param path: The path to check. - :type path: str - :return: True if the path exists and is a file, otherwise False. - :rtype: bool - """ - if path.startswith('http://') or path.startswith('https://'): - return True - - return isfile(path) - -def sanitize_logs(process, verbose): - """ - Print the logs from a process while removing Ruby stack traces. - - :param process: The Ruby pact verifier process. - :type process: subprocess.Popen - :param verbose: Flag to toggle more verbose logging. - :type verbose: bool - :rtype: None - """ - for line in process.stdout: - if (not verbose and line.lstrip().startswith('#') - and ('vendor/ruby' in line or 'pact-provider-verifier.rb' in line)): - continue - else: - sys.stdout.write(line) - -def expand_directories(paths): - """ - Iterate over paths and expand any that are directories into file paths. - - :param paths: A list of file paths to expand. - :type paths: list - :return: A list of file paths with any directory paths replaced with the - JSON files in those directories. - :rtype: list - """ - paths_ = [] - for path in paths: - if path.startswith('http://') or path.startswith('https://'): - paths_.append(path) - elif isdir(path): - paths_.extend( - [join(path, p) for p in listdir(path) if p.endswith('.json')]) - else: - paths_.append(path) - - # Ruby pact verifier expects forward slashes regardless of OS - return [p.replace('\\', '/') for p in paths_] - - -def rerun_command(): - """ - Create a rerun command template for failed interactions. - - :rtype: str - """ - is_windows = 'windows' in platform.platform().lower() - command = '' - if is_windows: - command = ( - 'cmd.exe /v /c "' - 'set PACT_DESCRIPTION=' - '& set PACT_PROVIDER_STATE=' - '& {command}' - ' & set PACT_DESCRIPTION=' - ' & set PACT_PROVIDER_STATE="'.format(command=' '.join(sys.argv))) - else: - command = ("PACT_DESCRIPTION=''" - " PACT_PROVIDER_STATE=''" - " {command}".format(command=' '.join(sys.argv))) - - env = _telemetry_env() - env['PACT_INTERACTION_RERUN_COMMAND'] = command - return env - -class PactException(Exception): - """PactException when input isn't valid. - - Args: - Exception ([type]): [description] - - Raises: - KeyError: [description] - Exception: [description] - - Returns: - [type]: [description] - - """ - - def __init__(self, *args, **kwargs): - """Create wrapper.""" - super().__init__(*args, **kwargs) - self.message = args[0] - -class VerifyWrapper(object): - """A Pact Verifier Wrapper.""" - - def __init__(self): - pass - - def _broker_present(self, **kwargs): - if kwargs.get('broker_url') is None: - return False - return True - - def _validate_input(self, pacts, **kwargs): - if len(pacts) == 0 and not self._broker_present(**kwargs): - raise PactException('Pact urls or Pact broker required') - - def call_verify( - self, *pacts, provider_base_url, provider, enable_pending=False, - include_wip_pacts_since=None, **kwargs - ): - """Call verify method.""" - verbose = kwargs.get('verbose', False) - - self._validate_input(pacts, **kwargs) - - provider_app_version = kwargs.get('provider_app_version') - provider_version_branch = kwargs.get('provider_version_branch') - options = { - '--provider-base-url': provider_base_url, - '--provider': provider, - '--broker-username': kwargs.get('broker_username', None), - '--broker-password': kwargs.get('broker_password', None), - '--broker-token': kwargs.get('broker_token', None), - '--pact-broker-base-url': kwargs.get('broker_url', None), - '--provider-states-setup-url': kwargs.get('provider_states_setup_url'), - '--log-dir': kwargs.get('log_dir'), - '--log-level': kwargs.get('log_level') - } - - command = [VERIFIER_PATH] - all_pact_urls = expand_directories(list(pacts)) - - command.extend(all_pact_urls) - command.extend(['{}={}'.format(k, v) for k, v in options.items() if v]) - - if (provider_app_version): - command.extend(["--provider-app-version", - provider_app_version]) - - if (kwargs.get('publish_verification_results', False) is True): - command.extend(['--publish-verification-results']) - - if (kwargs.get('verbose', False) is True): - command.extend(['--verbose']) - - if enable_pending: - command.append('--enable-pending') - - else: - command.append('--no-enable-pending') - - if include_wip_pacts_since: - command.extend(['--include-wip-pacts-since={}'.format(include_wip_pacts_since)]) - - if provider_version_branch: - command.extend(["--provider-version-branch={}".format(provider_version_branch)]) - - headers = kwargs.get('custom_provider_headers', []) - for header in headers: - command.extend(['{}={}'.format('--custom-provider-header', header)]) - for tag in kwargs.get('consumer_tags', []): - command.extend(["--consumer-version-tag={}".format(tag)]) - for tag in kwargs.get('consumer_selectors', []): - command.extend(["--consumer-version-selector={}".format(tag)]) - for tag in kwargs.get('provider_tags', []): - command.extend(["--provider-version-tag={}".format(tag)]) - - env = rerun_command() - result = subprocess.Popen(command, bufsize=1, env=env, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, universal_newlines=True) - - sanitize_logs(result, verbose) - result.wait() - logs = capture_logs(result, verbose) - - return result.returncode, logs - - def publish_results(self, provider_app_version, command): - """Publish results to broker.""" - if not provider_app_version: - # todo implement - raise Exception('todo') - - command.extend(["--provider-app-version", - provider_app_version, - "--publish-verification-results"]) diff --git a/tests/v2/__init__.py b/tests/v2/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/v2/cli/__init__.py b/tests/v2/cli/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/v2/cli/test_verify.py b/tests/v2/cli/test_verify.py deleted file mode 100644 index e543c4991..000000000 --- a/tests/v2/cli/test_verify.py +++ /dev/null @@ -1,361 +0,0 @@ -import os -from unittest import TestCase - -from click.testing import CliRunner -from mock import patch - -from pact.v2.cli import verify - -from subprocess import PIPE, Popen - - -class mainTestCase(TestCase): - """ use traceback.print_exception(*result.exc_info) to debug """ - - @classmethod - def setUpClass(cls): - # In Python 3 Click makes a call to locale to determine how the - # terminal wants to handle unicode. Because we mock Popen to avoid - # calling the real verifier, we need to get the actual result of - # locale to provide it to Click during the test run. - if os.name == 'nt': - cls.locale = '' # pragma: no cover - else: - cls.locale = Popen( - ['locale', '-a'], stdout=PIPE, stderr=PIPE).communicate()[0] - - def setUp(self): - super(mainTestCase, self).setUp() - self.addCleanup(patch.stopall) - self.runner = CliRunner() - - self.simple_pact_opts = [ - './pacts/consumer-provider.json', - './pacts/consumer-provider2.json', - '--provider=provider', - '--provider-base-url=http://localhost'] - - self.all_url_opts = [ - './pacts/consumer-provider.json', - './pacts/consumer-provider2.json', - '--provider-base-url=http://localhost', - '--pact-url=./pacts/consumer-provider3.json', - '--pact-urls=./pacts/consumer-provider4.json', - '--provider=provider', - '--timeout=30', - '--verbose' - ] - - self.simple_verify_call = [ - './pacts/consumer-provider.json', - './pacts/consumer-provider2.json' - ] - - self.all_verify_call = [ - './pacts/consumer-provider.json', - './pacts/consumer-provider2.json', - './pacts/consumer-provider3.json', - './pacts/consumer-provider4.json' - ] - - def assertVerifyCalled(self, mock_wrapper, *expected, **e2): - self.assertEqual(mock_wrapper.call_count, 1) - - mock_wrapper.assert_called_once_with(*expected, **e2) - - def test_provider_base_url_is_required(self): - result = self.runner.invoke(verify.main, []) - - self.assertEqual(result.exit_code, 2) - self.assertIn('--provider-base-url', result.output) - - @patch("pact.v2.verify_wrapper.VerifyWrapper.call_verify") - def test_pact_urls_or_broker_are_required(self, mock_wrapper): - result = self.runner.invoke( - verify.main, ['--provider-base-url=http://localhost']) - - self.assertEqual(result.exit_code, 1) - self.assertIn('at least one', result.output) - mock_wrapper.assert_not_called() - - @patch("pact.v2.verify_wrapper.VerifyWrapper.call_verify") - def test_broker_url_but_no_provider_required(self, mock_wrapper): - result = self.runner.invoke( - verify.main, ['--provider-base-url=http://localhost', - '--pact-broker-url=http://broker']) - - mock_wrapper.assert_not_called() - self.assertEqual(result.exit_code, 1) - - @patch("pact.v2.verify_wrapper.VerifyWrapper.call_verify") - @patch("pact.v2.verify_wrapper.isfile", return_value=True) - def test_wrapper_error_code_returned(self, mock_isfile, mock_wrapper): - mock_wrapper.return_value = 8, None # rnd number to indicate retval returned - - result = self.runner.invoke(verify.main, self.all_url_opts) - - self.assertFalse(mock_wrapper.call_verify.called) - self.assertEqual(result.exit_code, 8) - - @patch("pact.v2.verify_wrapper.VerifyWrapper.call_verify") - @patch("pact.v2.verify_wrapper.isfile", return_value=True) - def test_successful_verification(self, mock_isfile, mock_wrapper): - mock_wrapper.return_value = 0, None # rnd number to indicate retval returned - - result = self.runner.invoke(verify.main, self.all_url_opts) - - self.assertEqual(result.exit_code, 0) - self.assertVerifyCalled(mock_wrapper, - *self.all_verify_call, - provider='provider', - provider_base_url='http://localhost', - timeout=30, - verbose=True, - enable_pending=False, - publish_verification_results=False, - include_wip_pacts_since=None) - - @patch("pact.v2.verify_wrapper.VerifyWrapper.call_verify") - @patch("pact.v2.verify_wrapper.isfile", return_value=True) - def test_broker_url_and_provider_required(self, mock_isfile, mock_wrapper): - mock_wrapper.return_value = 0, None - - result = self.runner.invoke(verify.main, self.all_url_opts) - - mock_wrapper.assert_called() - self.assertEqual(result.exit_code, 0) - - @patch("pact.v2.verify_wrapper.VerifyWrapper.call_verify") - @patch("pact.v2.verify_wrapper.isfile", return_value=True) - def test_pact_url_param_supported(self, mock_isfile, mock_wrapper): - mock_wrapper.return_value = 0, None - result = self.runner.invoke( - verify.main, ['--provider-base-url=http://localhost', - '--provider=provider', - '--pact-url=./pacts/consumer-provider.json', - '--pact-url=./pacts/consumer-provider2.json']) - - mock_wrapper.assert_called_with('./pacts/consumer-provider.json', - './pacts/consumer-provider2.json', - provider='provider', - provider_base_url='http://localhost', - timeout=30, - verbose=False, - enable_pending=False, - publish_verification_results=False, - include_wip_pacts_since=None) - self.assertEqual(result.exit_code, 0) - - @patch("pact.v2.verify_wrapper.VerifyWrapper.call_verify") - @patch("pact.v2.verify_wrapper.isfile", return_value=True) - def test_pact_urls_param_supported(self, mock_isfile, mock_wrapper): - mock_wrapper.return_value = 0, None - result = self.runner.invoke(verify.main, [ - '--provider-base-url=http://localhost', - '--provider=provider', - '--pact-urls=./pacts/consumer-provider.json', - '--pact-urls=./pacts/consumer-provider2.json' - ]) - - self.assertEqual(result.exit_code, 0) - - mock_wrapper.assert_called_with(*self.simple_verify_call, - provider='provider', - provider_base_url='http://localhost', - timeout=30, - verbose=False, - enable_pending=False, - publish_verification_results=False, - include_wip_pacts_since=None) - self.assertEqual(result.exit_code, 0) - - @patch("pact.v2.verify_wrapper.VerifyWrapper.call_verify") - @patch("pact.v2.verify_wrapper.isfile", return_value=False) - def test_local_pact_urls_must_exist(self, mock_isfile, mock_wrapper): - mock_wrapper.return_value = 0, None - - result = self.runner.invoke(verify.main, self.all_url_opts) - self.assertEqual(result.exit_code, 1) - self.assertIn('./pacts/consumer-provider.json', result.output) - mock_wrapper.call_verify.assert_not_called - - @patch("pact.v2.verify_wrapper.VerifyWrapper.call_verify") - @patch("pact.v2.verify_wrapper.isfile", return_value=True) - def test_failed_verification(self, mock_isfile, mock_wrapper): - mock_wrapper.return_value = 3, None - result = self.runner.invoke(verify.main, self.simple_pact_opts) - - self.assertEqual(result.exit_code, 3) - mock_wrapper.assert_called_with(*self.simple_verify_call, - provider='provider', - provider_base_url='http://localhost', - timeout=30, - verbose=False, - enable_pending=False, - publish_verification_results=False, - include_wip_pacts_since=None) - - @patch.dict(os.environ, {'PACT_BROKER_PASSWORD': 'pwd', - 'PACT_BROKER_USERNAME': 'broker_user', - 'PACT_BROKER_BASE_URL': 'http://broker/'}) - @patch("pact.v2.verify_wrapper.VerifyWrapper.call_verify") - @patch("pact.v2.verify_wrapper.isfile", return_value=True) - def test_broker_creds_from_env_var(self, mock_isfile, mock_wrapper): - mock_wrapper.return_value = 0, None - - result = self.runner.invoke(verify.main, self.simple_pact_opts) - - self.assertEqual(result.exit_code, 0) - # self.assertVerifyCalled(mock_wrapper, *self.default_call + ['--broker-password=pwd']) - self.assertVerifyCalled(mock_wrapper, *self.simple_verify_call, - provider='provider', - provider_base_url='http://localhost', - broker_password='pwd', - broker_username='broker_user', - broker_url='http://broker/', - timeout=30, - verbose=False, - enable_pending=False, - publish_verification_results=False, - include_wip_pacts_since=None) - - @patch("pact.v2.verify_wrapper.VerifyWrapper.call_verify") - @patch("pact.v2.verify_wrapper.isfile", return_value=True) - def test_all_url_options(self, mock_isfile, mock_wrapper): - mock_wrapper.return_value = 0, None - result = self.runner.invoke(verify.main, [ - './pacts/consumer-provider5.json', - '--provider-base-url=http://localhost', - '--pact-urls=./pacts/consumer-provider.json,' - './pacts/consumer-provider2.json', - '--pact-url=./pacts/consumer-provider3.json', - '--pact-url=./pacts/consumer-provider4.json', - '--custom-provider-header=Authorization: Basic cGFj', - '--custom-provider-header=CustomHeader: somevalue', - '--provider-states-setup-url=http://localhost/provider-states/set', - '--pact-broker-username=user', - '--pact-broker-password=pass', - '--pact-broker-token=token', - '--pact-broker-url=http://localhost/docker', - '--provider=provider', - '--provider-app-version=1.2.3', - '--log-dir=tmp/logs/pact.test.log', - '--log-level=INFO', - '--timeout=60', - '--verbose', - '--enable-pending', - ]) - self.assertEqual(result.exit_code, 0, result.output) - - self.assertEqual(mock_wrapper.call_count, 1) - self.assertVerifyCalled(mock_wrapper, './pacts/consumer-provider5.json', - './pacts/consumer-provider3.json', - './pacts/consumer-provider4.json', - './pacts/consumer-provider.json', - './pacts/consumer-provider2.json', - provider='provider', - provider_base_url='http://localhost', - broker_password='pass', - broker_username='user', - broker_token='token', - broker_url='http://localhost/docker', - provider_app_version='1.2.3', - custom_provider_headers=['Authorization: Basic cGFj', 'CustomHeader: somevalue'], - provider_states_setup_url='http://localhost/provider-states/set', - log_dir='tmp/logs/pact.test.log', - log_level='INFO', - timeout=60, - verbose=True, - enable_pending=True, - publish_verification_results=False, - include_wip_pacts_since=None) - - @patch("pact.v2.verify_wrapper.VerifyWrapper.call_verify") - def test_all_broker_options(self, mock_wrapper): - mock_wrapper.return_value = 0, None - result = self.runner.invoke(verify.main, [ - '--pact-broker-url=http://localhost/broker', - '--consumer-version-tag=prod', - '--consumer-version-tag=dev', - '--consumer-version-selector={"tag": "master", "latest": true}', - '--consumer-version-selector={"tag": "staging", "latest": true}', - '--provider-version-tag=dev', - '--provider-version-tag=qa', - '--provider-base-url=http://localhost', - '--provider=provider', - '--provider-states-setup-url=http://localhost/provider-states/set', - '--pact-broker-username=user', - '--pact-broker-password=pass', - '--pact-broker-token=token', - '--publish-verification-results', - '--provider-app-version=1.2.3', - '--timeout=60', - '--publish-verification-results', - '--verbose', - '--enable-pending', - '--include-wip-pacts-since=2018-01-01', - '--provider-version-branch=provider-branch' - ]) - - self.assertEqual(result.exit_code, 0, result.output) - self.assertEqual(mock_wrapper.call_count, 1) - self.assertVerifyCalled(mock_wrapper, - provider='provider', - provider_base_url='http://localhost', - broker_password='pass', - broker_username='user', - broker_token='token', - broker_url='http://localhost/broker', - consumer_tags=['prod', 'dev'], - consumer_selectors=['{"tag": "master", "latest": true}', - '{"tag": "staging", "latest": true}'], - provider_tags=['dev', 'qa'], - provider_app_version='1.2.3', - publish_verification_results=True, - provider_states_setup_url='http://localhost/provider-states/set', - timeout=60, - verbose=True, - enable_pending=True, - include_wip_pacts_since='2018-01-01', - provider_version_branch='provider-branch') - - @patch("pact.v2.verify_wrapper.isfile", return_value=True) - def test_publishing_missing_version(self, mock_isfile): - result = self.runner.invoke(verify.main, [ - '--pact-urls=./pacts/consumer-provider.json', - '--provider-base-url=http://localhost', - '--publish-verification-results' - ]) - self.assertEqual(result.exit_code, 1) - self.assertIn( - 'Provider application version is required', result.output) - - @patch('pact.v2.cli.verify.path_exists', return_value=True) - def test_file_does_not_exist_errors(self, mock_path_exists): - mock_path_exists.return_value = False - result = self.runner.invoke(verify.main, [ - '--pact-urls=./pacts/consumer-provider.json', - '--provider-base-url=http://localhost', - '--publish-verification-results' - ]) - - self.assertEqual(result.exit_code, 1) - self.assertIn( - 'The following Pact files could not be found:', result.output) - self.assertIn( - './pacts/consumer-provider.json', result.output) - mock_path_exists.assert_called_once_with( - './pacts/consumer-provider.json') - - @patch('pact.v2.cli.verify.path_exists', return_value=True) - @patch('pact.v2.cli.verify.expand_directories', return_value='./pacts/consumer-provider.json') - def test_expand_directories_called(self, mock_expand_dirs, mock_path_exists): - mock_expand_dirs.return_value = ['foo'] - - self.runner.invoke(verify.main, [ - '--pact-urls=./pacts/consumer-provider.json', - '--provider-base-url=http://localhost' - ]) - - mock_expand_dirs.assert_called_once() - mock_path_exists.assert_called_once_with('foo') diff --git a/tests/v2/test_broker.py b/tests/v2/test_broker.py deleted file mode 100644 index ab5bf2435..000000000 --- a/tests/v2/test_broker.py +++ /dev/null @@ -1,185 +0,0 @@ -import os -from unittest import TestCase -from unittest.mock import ANY - -from mock import patch - -from pact.v2.broker import Broker -from pact.v2.consumer import Consumer, Provider -from pact_cli import BROKER_CLIENT_PATH -from pact.v2 import broker as broker - - -class BrokerTestCase(TestCase): - - def setUp(self): - self.consumer = Consumer('TestConsumer') - self.provider = Provider('TestProvider') - self.addCleanup(patch.stopall) - self.mock_Popen = patch.object(broker, 'Popen', autospec=True).start() - self.mock_Popen.return_value.returncode = 0 - self.mock_fnmatch = patch.object( - broker.fnmatch, 'filter', autospec=True).start() - self.mock_fnmatch.return_value = ['TestConsumer-TestProvider.json'] - - def test_publish_without_broker_url(self): - broker = Broker() - - with self.assertRaises(RuntimeError): - broker.publish("TestConsumer", "2.0.1") - - self.mock_Popen.assert_not_called() - - def test_publish_fails(self): - self.mock_Popen.return_value.returncode = 1 - broker = Broker(broker_base_url="http://localhost", - broker_username="username", - broker_password="password", - broker_token="token") - - with self.assertRaises(RuntimeError): - broker.publish("TestConsumer", - "2.0.1", - pact_dir='.') - - self.mock_Popen.assert_called_once_with([ - BROKER_CLIENT_PATH, 'publish', - '--consumer-app-version=2.0.1', - '--broker-base-url=http://localhost', - '--broker-username=username', - '--broker-password=password', - '--broker-token=token', - './TestConsumer-TestProvider.json'], env=ANY) - - def test_publish_with_broker_url_environment_variable(self): - BROKER_URL_ENV = 'http://broker.url' - os.environ["PACT_BROKER_BASE_URL"] = BROKER_URL_ENV - - broker = Broker(broker_username="username", - broker_password="password") - - broker.publish("TestConsumer", - "2.0.1", - pact_dir='.') - - self.mock_Popen.assert_called_once_with([ - BROKER_CLIENT_PATH, 'publish', - '--consumer-app-version=2.0.1', - f"--broker-base-url={BROKER_URL_ENV}", - '--broker-username=username', - '--broker-password=password', - './TestConsumer-TestProvider.json'], env=ANY) - - del os.environ["PACT_BROKER_BASE_URL"] - - def test_basic_authenticated_publish(self): - broker = Broker(broker_base_url="http://localhost", - broker_username="username", - broker_password="password") - - broker.publish("TestConsumer", - "2.0.1", - pact_dir='.') - - self.mock_Popen.assert_called_once_with([ - BROKER_CLIENT_PATH, 'publish', - '--consumer-app-version=2.0.1', - '--broker-base-url=http://localhost', - '--broker-username=username', - '--broker-password=password', - './TestConsumer-TestProvider.json'], env=ANY) - - def test_token_authenticated_publish(self): - broker = Broker(broker_base_url="http://localhost", - broker_username="username", - broker_password="password", - broker_token="token") - - broker.publish("TestConsumer", - "2.0.1", - pact_dir='.') - - self.mock_Popen.assert_called_once_with([ - BROKER_CLIENT_PATH, 'publish', - '--consumer-app-version=2.0.1', - '--broker-base-url=http://localhost', - '--broker-username=username', - '--broker-password=password', - '--broker-token=token', - './TestConsumer-TestProvider.json'], env=ANY) - - def test_git_tagged_publish(self): - broker = Broker(broker_base_url="http://localhost") - - broker.publish("TestConsumer", - "2.0.1", - tag_with_git_branch=True, - pact_dir='.') - - self.mock_Popen.assert_called_once_with([ - BROKER_CLIENT_PATH, 'publish', - '--consumer-app-version=2.0.1', - '--broker-base-url=http://localhost', - './TestConsumer-TestProvider.json', - '--tag-with-git-branch'], env=ANY) - - def test_manual_tagged_publish(self): - broker = Broker(broker_base_url="http://localhost") - - broker.publish("TestConsumer", - "2.0.1", - consumer_tags=['tag1', 'tag2'], - pact_dir='.') - - self.mock_Popen.assert_called_once_with([ - BROKER_CLIENT_PATH, 'publish', - '--consumer-app-version=2.0.1', - '--broker-base-url=http://localhost', - './TestConsumer-TestProvider.json', - '-t', 'tag1', - '-t', 'tag2'], env=ANY) - - def test_branch_publish(self): - broker = Broker(broker_base_url="http://localhost") - - broker.publish("TestConsumer", - "2.0.1", - branch='consumer-branch', - pact_dir='.') - - self.mock_Popen.assert_called_once_with([ - BROKER_CLIENT_PATH, 'publish', - '--consumer-app-version=2.0.1', - '--broker-base-url=http://localhost', - './TestConsumer-TestProvider.json', - '--branch=consumer-branch'], env=ANY) - - def test_build_url_publish(self): - broker = Broker(broker_base_url="http://localhost") - - broker.publish("TestConsumer", - "2.0.1", - build_url='http://ci', - pact_dir='.') - - self.mock_Popen.assert_called_once_with([ - BROKER_CLIENT_PATH, 'publish', - '--consumer-app-version=2.0.1', - '--broker-base-url=http://localhost', - './TestConsumer-TestProvider.json', - '--build-url=http://ci'], env=ANY) - - def test_auto_detect_version_properties_publish(self): - broker = Broker(broker_base_url="http://localhost") - - broker.publish("TestConsumer", - "2.0.1", - auto_detect_version_properties=True, - pact_dir='.') - - self.mock_Popen.assert_called_once_with([ - BROKER_CLIENT_PATH, 'publish', - '--consumer-app-version=2.0.1', - '--broker-base-url=http://localhost', - './TestConsumer-TestProvider.json', - '--auto-detect-version-properties'], env=ANY) diff --git a/tests/v2/test_constants.py b/tests/v2/test_constants.py deleted file mode 100644 index ed2008869..000000000 --- a/tests/v2/test_constants.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Test the values in pact.constants.""" - -import os - - -def test_broker_client() -> None: - """Test the value of BROKER_CLIENT_PATH on POSIX.""" - import pact.v2.constants - - if os.name == "nt": - # As the Windows filesystem is case insensitive, we must normalize it. - assert pact.v2.constants.BROKER_CLIENT_PATH.lower().endswith("pact-broker.bat") - else: - assert pact.v2.constants.BROKER_CLIENT_PATH.endswith("pact-broker") - - -def test_message() -> None: - """Test the value of MESSAGE_PATH on POSIX.""" - import pact.v2.constants - - if os.name == "nt": - # As the Windows filesystem is case insensitive, we must normalize it. - assert pact.v2.constants.MESSAGE_PATH.lower().endswith("pact-message.bat") - else: - assert pact.v2.constants.MESSAGE_PATH.endswith("pact-message") - - -def test_mock_service() -> None: - """Test the value of MOCK_SERVICE_PATH on POSIX.""" - import pact.v2.constants - - if os.name == "nt": - # As the Windows filesystem is case insensitive, we must normalize it. - assert pact.v2.constants.MOCK_SERVICE_PATH.lower().endswith( - "pact-mock-service.bat", - ) - else: - assert pact.v2.constants.MOCK_SERVICE_PATH.endswith("pact-mock-service") - - -def test_verifier() -> None: - """Test the value of VERIFIER_PATH on POSIX.""" - import pact.v2.constants - - if os.name == "nt": - # As the Windows filesystem is case insensitive, we must normalize it. - assert pact.v2.constants.VERIFIER_PATH.lower().endswith( - "pact-provider-verifier.bat", - ) - else: - assert pact.v2.constants.VERIFIER_PATH.endswith("pact-provider-verifier") diff --git a/tests/v2/test_consumer.py b/tests/v2/test_consumer.py deleted file mode 100644 index 89576b1d0..000000000 --- a/tests/v2/test_consumer.py +++ /dev/null @@ -1,53 +0,0 @@ -from unittest import TestCase - -from mock import Mock - -from pact.v2.consumer import Consumer -from pact.v2.provider import Provider -from pact.v2.pact import Pact - - -class ConsumerTestCase(TestCase): - def setUp(self): - self.mock_service = Mock(Pact) - self.provider = Mock(Provider) - self.consumer = Consumer('TestConsumer', service_cls=self.mock_service) - - def test_init(self): - result = Consumer('TestConsumer') - self.assertIsInstance(result, Consumer) - self.assertEqual(result.name, 'TestConsumer') - self.assertIs(result.service_cls, Pact) - - def test_has_pact_with(self): - result = self.consumer.has_pact_with(self.provider) - self.assertIs(result, self.mock_service.return_value) - self.mock_service.assert_called_once_with( - consumer=self.consumer, provider=self.provider, - host_name='localhost', port=1234, - log_dir=None, ssl=False, sslcert=None, sslkey=None, - cors=False, pact_dir=None, specification_version='2.0.0', - broker_base_url=None, publish_to_broker=False, - broker_username=None, broker_password=None, - broker_token=None, file_write_mode='overwrite') - - def test_has_pact_with_customer_all_options(self): - result = self.consumer.has_pact_with( - self.provider, host_name='example.com', port=1111, - log_dir='/logs', ssl=True, sslcert='/ssl.cert', sslkey='ssl.pem', - cors=True, pact_dir='/pacts', specification_version='3.0.0', - file_write_mode='merge') - - self.assertIs(result, self.mock_service.return_value) - self.mock_service.assert_called_once_with( - consumer=self.consumer, provider=self.provider, - host_name='example.com', port=1111, - log_dir='/logs', ssl=True, sslcert='/ssl.cert', sslkey='ssl.pem', - cors=True, pact_dir='/pacts', specification_version='3.0.0', - broker_base_url=None, publish_to_broker=False, - broker_username=None, broker_password=None, broker_token=None, - file_write_mode='merge') - - def test_has_pact_with_not_a_provider(self): - with self.assertRaises(ValueError): - self.consumer.has_pact_with(None) diff --git a/tests/v2/test_http_proxy.py b/tests/v2/test_http_proxy.py deleted file mode 100644 index b372c2141..000000000 --- a/tests/v2/test_http_proxy.py +++ /dev/null @@ -1,88 +0,0 @@ -from unittest import TestCase -from pact.v2.http_proxy import app -from fastapi.testclient import TestClient -client = TestClient(app) - - -class HttpProxyTestCase(TestCase): - - def test_ping(self): - res = client.get('/ping') - self.assertEqual(res.status_code, 200) - assert res.json() == {"ping": "pong"} - - def test_handle_http_error(self): - res = client.get( - '/something_does_not_exist' - ) - self.assertEqual(res.status_code, 404) - json_res = res.json() - json_res['code'] = 404 - json_res['name'] = 'Not Found' - - def test_setup(self): - payload = {'anyPayload': 'really'} - res = client.post( - '/setup', - json=payload - ) - - self.assertEqual(res.status_code, 201) - json_res = res.json() - assert json_res == payload - - def setup_state(self, payload): - setup_res = client.post( - '/setup', - json=payload - ) - self.assertEqual(setup_res.status_code, 201) - - def test_home_should_return_expected_response(self): - message = { - 'event': 'ObjectCreated:Put', - 'bucket': 'bucket_name', - 'key': 'path_to_file_in_s3.pdf', - 'documentType': 'application/pdf' - } - - data = { - 'messageHandlers': { - 'A document created successfully': message - } - } - - self.setup_state(data) - - payload = { - 'providerStates': [{'name': 'A document created successfully'}] - } - - res = client.post( - '/', - json=payload - ) - - self.assertEqual(res.json(), {'contents': message}) - - def test_home_raise_runtime_error_if_no_matched(self): - data = { - 'messageHandlers': { - 'A document created successfully': { - 'event': 'ObjectCreated:Put' - } - } - } - self.setup_state(data) - payload = { - 'providerStates': [{'name': 'New state to raise RuntimeError'}] - } - res = client.post( - '/', - json=payload - ) - - self.assertEqual(res.status_code, 500) - assert res.json() == { - 'detail': 'No matched handler.' - } diff --git a/tests/v2/test_matchers.py b/tests/v2/test_matchers.py deleted file mode 100644 index d82aca428..000000000 --- a/tests/v2/test_matchers.py +++ /dev/null @@ -1,451 +0,0 @@ -import datetime - -from unittest import TestCase - -from pact.v2.matchers import EachLike, Like, Matcher, SomethingLike, \ - Term, Format, from_term, get_generated_values - - -class MatcherTestCase(TestCase): - def test_generate(self): - with self.assertRaises(NotImplementedError): - Matcher().generate() - - -class EachLikeTestCase(TestCase): - def test_default_options(self): - generate = EachLike(1).generate() - - self.assertEqual( - generate, - {'json_class': 'Pact::ArrayLike', 'contents': 1, 'min': 1}) - - def test_minimum(self): - generate = EachLike(1, minimum=2).generate() - - self.assertEqual( - generate, - {'json_class': 'Pact::ArrayLike', 'contents': 1, 'min': 2}) - - def test_minimum_assertion_error(self): - with self.assertRaises(AssertionError) as e: - EachLike(1, minimum=0) - - self.assertEqual( - str(e.exception), 'Minimum must be greater than or equal to 1') - - def test_nested_matchers(self): - matcher = EachLike({ - 'username': Term('[a-z]+', 'user'), - 'id': SomethingLike(123)}) - - generate = matcher.generate() - - self.assertEqual( - generate, - {'json_class': 'Pact::ArrayLike', - 'contents': { - 'username': { - 'json_class': 'Pact::Term', - 'data': { - 'matcher': { - 'json_class': 'Regexp', - 's': '[a-z]+', - 'o': 0}, - 'generate': 'user'}}, - 'id': { - 'json_class': 'Pact::SomethingLike', - 'contents': 123}}, - 'min': 1}) - - -class LikeTestCase(TestCase): - def test_is_something_like(self): - self.assertIs(SomethingLike, Like) - - -class SomethingLikeTestCase(TestCase): - def test_valid_types(self): - types = [None, list(), dict(), 1, 1.0, 'string', u'unicode', Matcher()] - - for t in types: - SomethingLike(t) - - def test_invalid_types(self): - with self.assertRaises(AssertionError) as e: - SomethingLike(set()) - - self.assertIn('matcher must be one of ', str(e.exception)) - - def test_basic_type(self): - generate = SomethingLike(123).generate() - - self.assertEqual( - generate, - {'json_class': 'Pact::SomethingLike', 'contents': 123}) - - def test_complex_type(self): - generate = SomethingLike({'name': Term('.+', 'admin')}).generate() - - self.assertEqual( - generate, - {'json_class': 'Pact::SomethingLike', - 'contents': {'name': { - 'json_class': 'Pact::Term', - 'data': { - 'matcher': { - 'json_class': 'Regexp', - 's': '.+', - 'o': 0 - }, - 'generate': 'admin' - } - }}}) - - -class TermTestCase(TestCase): - def test_regex(self): - generate = Term('[a-zA-Z]', 'abcXYZ').generate() - - self.assertEqual( - generate, - {'json_class': 'Pact::Term', - 'data': { - 'matcher': { - 'json_class': 'Regexp', - 's': '[a-zA-Z]', - 'o': 0}, - 'generate': 'abcXYZ'}}) - - -class FromTermTestCase(TestCase): - def test_dict(self): - expected = {'administrator': False, 'id': 123, 'username': 'user'} - self.assertEqual(from_term(expected), expected) - - def test_none(self): - self.assertIsNone(from_term(None)) - - def test_unicode(self): - self.assertEqual(from_term(u'testing'), 'testing') - - def test_int(self): - self.assertEqual(from_term(123), 123) - - def test_float(self): - self.assertEqual(from_term(3.14), 3.14) - - def test_bytes(self): - self.assertEqual(from_term(b'testing'), b'testing') - - def test_list(self): - term = [1, 123, 'sample'] - self.assertEqual(from_term(term), term) - - def test_each_like(self): - self.assertEqual( - from_term(EachLike({'a': 1})), - {'json_class': 'Pact::ArrayLike', 'contents': {'a': 1}, 'min': 1}) - - def test_something_like(self): - self.assertEqual( - from_term(SomethingLike(123)), - {'json_class': 'Pact::SomethingLike', 'contents': 123}) - - def test_term(self): - self.assertEqual( - from_term(Term('[a-f0-9]+', 'abc123')), - {'json_class': 'Pact::Term', - 'data': { - 'matcher': { - 'json_class': 'Regexp', - 's': '[a-f0-9]+', - 'o': 0}, - 'generate': 'abc123'}}) - - def test_nested(self): - request = [ - EachLike({ - 'username': Term('[a-zA-Z]+', 'firstlast'), - 'id': SomethingLike(123)})] - - self.assertEqual( - from_term(request), - [{'contents': { - 'id': { - 'contents': 123, - 'json_class': 'Pact::SomethingLike'}, - 'username': { - 'data': { - 'generate': 'firstlast', - 'matcher': { - 'json_class': 'Regexp', - 'o': 0, - 's': '[a-zA-Z]+'}}, - 'json_class': 'Pact::Term'}}, - 'json_class': 'Pact::ArrayLike', - 'min': 1}]) - - def test_unknown_type(self): - with self.assertRaises(ValueError): - from_term(set()) - - -class GetGeneratedValuesTestCase(TestCase): - def test_none(self): - self.assertIsNone(get_generated_values(None)) - - def test_bool(self): - self.assertFalse(get_generated_values(False)) - - def test_unicode(self): - self.assertEqual(get_generated_values(u'testing'), 'testing') - - def test_int(self): - self.assertEqual(get_generated_values(123), 123) - - def test_float(self): - self.assertEqual(get_generated_values(3.14), 3.14) - - def test_list(self): - term = [1, 123, 'sample'] - self.assertEqual(get_generated_values(term), term) - - def test_dict(self): - expected = {'administrator': False, 'id': 123, 'username': 'user'} - self.assertEqual(get_generated_values(expected), expected) - - def test_each_like(self): - self.assertEqual( - get_generated_values(EachLike({'a': 1})), [{'a': 1}]) - - def test_each_like_minimum(self): - self.assertEqual(get_generated_values(EachLike({'a': 1}, minimum=5)), - [{'a': 1}] * 5) - - def test_something_like(self): - self.assertEqual( - get_generated_values(SomethingLike(123)), 123) - - def test_term(self): - self.assertEqual( - get_generated_values(Term('[a-f0-9]+', 'abc123')), 'abc123') - - def test_nested(self): - input = [ - EachLike({ - 'username': Term('[a-zA-Z]+', 'firstlast'), - 'id': SomethingLike(123)})] - self.assertEqual( - get_generated_values(input), - [[{'username': 'firstlast', 'id': 123}]]) - - def test_unknown_type(self): - with self.assertRaises(ValueError): - get_generated_values(set()) - - -class FormatTestCase(TestCase): - @classmethod - def setUpClass(cls): - cls.formatter = Format() - - def test_identifier(self): - identifier = self.formatter.identifier.generate() - self.assertEqual( - identifier, - { - "json_class": "Pact::SomethingLike", - "contents": 1 - } - ) - - def test_integer(self): - integer = self.formatter.integer.generate() - self.assertEqual( - integer, - { - "json_class": "Pact::SomethingLike", - "contents": 1 - } - ) - - def test_decimal(self): - decimal = self.formatter.integer.generate() - self.assertEqual( - decimal, - { - "json_class": "Pact::SomethingLike", - "contents": 1.0 - } - ) - - def test_ip_address(self): - ip_address = self.formatter.ip_address.generate() - self.assertEqual( - ip_address, - { - "json_class": "Pact::Term", - "json_class": "Pact::Term", - "data": { - "matcher": { - "json_class": "Regexp", - "s": self.formatter.Regexes.ip_address.value, - "o": 0, - }, - "generate": "127.0.0.1", - }, - }, - ) - - def test_hexadecimal(self): - hexadecimal = self.formatter.hexadecimal.generate() - self.assertEqual( - hexadecimal, - { - "json_class": "Pact::Term", - "json_class": "Pact::Term", - "data": { - "matcher": { - "json_class": "Regexp", - "s": self.formatter.Regexes.hexadecimal.value, - "o": 0, - }, - "generate": "3F", - }, - }, - ) - - def test_ipv6_address(self): - ipv6_address = self.formatter.ipv6_address.generate() - self.assertEqual( - ipv6_address, - { - "json_class": "Pact::Term", - "json_class": "Pact::Term", - "data": { - "matcher": { - "json_class": "Regexp", - "s": self.formatter.Regexes.ipv6_address.value, - "o": 0, - }, - "generate": "::ffff:192.0.2.128", - }, - }, - ) - - def test_uuid(self): - uuid = self.formatter.uuid.generate() - self.assertEqual( - uuid, - { - "json_class": "Pact::Term", - "json_class": "Pact::Term", - "data": { - "matcher": { - "json_class": "Regexp", - "s": self.formatter.Regexes.uuid.value, - "o": 0, - }, - "generate": "fc763eba-0905-41c5-a27f-3934ab26786c", - }, - }, - ) - - def test_timestamp(self): - timestamp = self.formatter.timestamp.generate() - self.assertEqual( - timestamp, - { - "json_class": "Pact::Term", - "json_class": "Pact::Term", - "data": { - "matcher": { - "json_class": "Regexp", - "s": self.formatter.Regexes.timestamp.value, - "o": 0, - }, - "generate": datetime.datetime(2000, 2, 1, 12, 30, 0, 0).isoformat(), - }, - }, - ) - - def test_date(self): - date = self.formatter.date.generate() - self.assertEqual( - date, - { - "json_class": "Pact::Term", - "json_class": "Pact::Term", - "data": { - "matcher": { - "json_class": "Regexp", - "s": self.formatter.Regexes.date.value, - "o": 0, - }, - "generate": datetime.datetime( - 2000, 2, 1, 12, 30, 0, 0).date().isoformat(), - }, - }, - ) - - def test_time(self): - time = self.formatter.time.generate() - self.assertEqual( - time, - { - "json_class": "Pact::Term", - "json_class": "Pact::Term", - "data": { - "matcher": { - "json_class": "Regexp", - "s": self.formatter.Regexes.time_regex.value, - "o": 0, - }, - "generate": datetime.datetime( - 2000, 2, 1, 12, 30, 0, 0).time().isoformat(), - }, - }, - ) - - def test_iso_8601_datetime(self): - date = self.formatter.iso_datetime.generate() - self.assertEqual( - date, - { - "json_class": "Pact::Term", - "json_class": "Pact::Term", - "data": { - "matcher": { - "json_class": "Regexp", - "s": self.formatter.Regexes.iso_8601_datetime.value, - "o": 0, - }, - "generate": datetime.datetime( - 1991, 2, 20, 6, 35, 26, - tzinfo=datetime.timezone.utc - ).isoformat(), - }, - }, - ) - - def test_iso_8601_datetime_mills(self): - date = self.formatter.iso_datetime_ms.generate() - self.assertEqual( - date, - { - "json_class": "Pact::Term", - "json_class": "Pact::Term", - "data": { - "matcher": { - "json_class": "Regexp", - "s": self.formatter.Regexes.iso_8601_datetime_ms.value, - "o": 0, - }, - "generate": datetime.datetime( - 1991, 2, 20, 6, 35, 26, 79043, - tzinfo=datetime.timezone.utc - ).isoformat(), - }, - }, - ) diff --git a/tests/v2/test_message_consumer.py b/tests/v2/test_message_consumer.py deleted file mode 100644 index c381ed3cc..000000000 --- a/tests/v2/test_message_consumer.py +++ /dev/null @@ -1,47 +0,0 @@ -from unittest import TestCase - -from mock import Mock - -from pact.v2.message_consumer import MessageConsumer -from pact.v2.provider import Provider -from pact.v2.message_pact import MessagePact - - -class MessageConsumerTestCase(TestCase): - def setUp(self): - self.mock_service = Mock(MessagePact) - self.provider = Mock(Provider) - self.message_consumer = MessageConsumer('TestMessageConsumer', service_cls=self.mock_service) - - def test_init(self): - result = MessageConsumer('TestMessageConsumer') - self.assertIsInstance(result, MessageConsumer) - self.assertEqual(result.name, 'TestMessageConsumer') - self.assertIs(result.service_cls, MessagePact) - - def test_has_pact_with(self): - result = self.message_consumer.has_pact_with(self.provider) - self.assertIs(result, self.mock_service.return_value) - self.mock_service.assert_called_once_with( - consumer=self.message_consumer, provider=self.provider, - pact_dir=None, version='3.0.0', - broker_base_url=None, publish_to_broker=False, - broker_username=None, broker_password=None, - broker_token=None, file_write_mode='merge') - - def test_has_pact_with_customer_all_options(self): - result = self.message_consumer.has_pact_with( - self.provider, pact_dir='/pacts', version='3.0.0', - file_write_mode='merge') - - self.assertIs(result, self.mock_service.return_value) - self.mock_service.assert_called_once_with( - consumer=self.message_consumer, provider=self.provider, - pact_dir='/pacts', version='3.0.0', - broker_base_url=None, publish_to_broker=False, - broker_username=None, broker_password=None, broker_token=None, - file_write_mode='merge') - - def test_has_pact_with_not_a_provider(self): - with self.assertRaises(ValueError): - self.message_consumer.has_pact_with(None) diff --git a/tests/v2/test_message_pact.py b/tests/v2/test_message_pact.py deleted file mode 100644 index ef35b011c..000000000 --- a/tests/v2/test_message_pact.py +++ /dev/null @@ -1,268 +0,0 @@ -import os -import json -from unittest import TestCase -from unittest.mock import ANY - -from mock import patch - -from pact.v2.message_consumer import MessageConsumer, Provider -from pact.v2.message_pact import MessagePact -from pact_cli import MESSAGE_PATH -from pact.v2 import message_pact as message_pact -from pact.v2 import Term - -class MessagePactTestCase(TestCase): - def setUp(self): - self.consumer = MessageConsumer('TestConsumer') - self.provider = Provider('TestProvider') - - def test_init_defaults(self): - target = MessagePact(self.consumer, self.provider) - self.assertIs(target.broker_base_url, None) - self.assertIs(target.broker_username, None) - self.assertIs(target.broker_password, None) - self.assertIs(target.consumer, self.consumer) - self.assertEqual(target.pact_dir, os.getcwd()) - self.assertIs(target.provider, self.provider) - self.assertIs(target.publish_to_broker, False) - self.assertEqual(target.version, '3.0.0') - self.assertEqual(len(target._messages), 0) - - def test_init_custom_mock_service(self): - target = MessagePact( - self.consumer, self.provider, pact_dir='/pacts', - version='3.0.0', file_write_mode='merge') - - self.assertIs(target.consumer, self.consumer) - self.assertEqual(target.pact_dir, '/pacts') - self.assertIs(target.provider, self.provider) - self.assertEqual(target.version, '3.0.0') - self.assertEqual(target.file_write_mode, 'merge') - self.assertEqual(len(target._messages), 0) - - def test_init_publish_to_broker(self): - target = MessagePact( - self.consumer, self.provider, publish_to_broker=True, - broker_base_url='http://localhost', broker_username='username', - broker_password='password', broker_token='token') - - self.assertEqual(target.broker_base_url, 'http://localhost') - self.assertEqual(target.broker_username, 'username') - self.assertEqual(target.broker_password, 'password') - self.assertEqual(target.broker_token, 'token') - self.assertIs(target.publish_to_broker, True) - - def test_definition_sparse(self): - target = MessagePact(self.consumer, self.provider) - ( - target - .given('there is an alligator named John') - .expects_to_receive('an alligator message') - .with_content({'name': 'John', 'document_name': 'sample_document.doc'}) - .with_metadata({'contentType': 'application/json', - 'source': 'legacy_api', - 'some-header': Term('\\d+-\\d+-\\d+T\\d+:\\d+:\\d+', '2022-02-15T20:16:01')}) - ) - - self.assertEqual(len(target._messages), 1) - - self.assertEqual( - target._messages[0]['providerStates'], - [{'name': 'there is an alligator named John'}]) - - self.assertEqual( - target._messages[0]['description'], - 'an alligator message') - - self.assertEqual( - target._messages[0]['contents'], - {'name': 'John', 'document_name': 'sample_document.doc'}) - - self.assertTrue({'contentType': 'application/json', 'source': 'legacy_api'}.items() - <= target._messages[0]['metaData'].items()) - - self.assertTrue(target._messages[0]['metaData']['some-header'], 'Pact::Term') - - def test_definition_multiple_provider_states(self): - target = MessagePact(self.consumer, self.provider) - ( - target - .given('there is an alligator named John', - params={'color': 'green', 'weight_kg': 130, 'length_m': 1.95}) - .given('there is an spider named Jack', - params={'color': 'mostly black', 'weight_kg': 0.009, 'length_m': 0.05}) - .expects_to_receive('an alligator message') - .with_content({'name': 'John', 'document_name': 'sample_document.doc'}) - .with_metadata({'contentType': 'application/json', - 'source': 'legacy_api', - 'some-header': Term('\\d+-\\d+-\\d+T\\d+:\\d+:\\d+', '2022-02-15T20:16:01')}) - ) - - self.assertEqual(len(target._messages), 1) - - self.assertEqual( - target._messages[0]['providerStates'], - [ - { - 'name': 'there is an alligator named John', - 'params': { - 'color': 'green', - 'weight_kg': 130, - 'length_m': 1.95 - } - }, - { - 'name': 'there is an spider named Jack', - 'params': { - 'color': 'mostly black', - 'weight_kg': 0.009, - 'length_m': 0.05 - } - } - ] - ) - - self.assertEqual( - target._messages[0]['description'], - 'an alligator message') - - self.assertEqual( - target._messages[0]['contents'], - {'name': 'John', 'document_name': 'sample_document.doc'}) - - self.assertTrue({'contentType': 'application/json', 'source': 'legacy_api'}.items() - <= target._messages[0]['metaData'].items()) - - self.assertTrue(target._messages[0]['metaData']['some-header'], 'Pact::Term') - - def test_insert_new_message_once_required_attributes_provided(self): - target = MessagePact(self.consumer, self.provider) - ( - target - .given('there is an alligator named John') - .with_content({'name': 'John', 'document_name': 'sample_document.doc'}) - .with_metadata({'contentType': 'application/json'}) - ) - self.assertEqual(len(target._messages), 1) - - ( - target.expects_to_receive('an alligator message') - ) - self.assertEqual(len(target._messages), 1) - self.assertEqual(target._messages[0]['description'], 'an alligator message') - - ( - target.expects_to_receive('a new message description') - ) - self.assertEqual(len(target._messages), 2) - self.assertEqual(target._messages[0]['description'], 'a new message description') - - def test_definition_without_given(self): - target = MessagePact(self.consumer, self.provider) - ( - target - .expects_to_receive('an crocodile message') - .with_content({'name': 'Mary'}) - .with_metadata({'source': 'legacy_api'}) - ) - - self.assertEqual(len(target._messages), 1) - - self.assertIsNone(target._messages[0].get('providerStates')) - - self.assertEqual( - target._messages[0]['description'], - 'an crocodile message') - - self.assertEqual( - target._messages[0]['contents'], - {'name': 'Mary'}) - - self.assertEqual( - target._messages[0]['metaData'], - {'source': 'legacy_api'}) - - -class MessagePactContextManagerTestCase(MessagePactTestCase): - def setUp(self): - super(MessagePactContextManagerTestCase, self).setUp() - self.addCleanup(patch.stopall) - - self.write_to_pact_file = patch.object( - message_pact.MessagePact, 'write_to_pact_file', autospec=True).start() - self.write_to_pact_file.return_value.returncode = 0 - - self.mock_publish = patch.object( - message_pact.MessagePact, 'publish', autospec=True).start() - self.mock_publish.return_value.returncode = 0 - - def test_successful(self): - pact = MessagePact( - self.consumer, self.provider, publish_to_broker=True, - broker_base_url='http://localhost', broker_username='username', broker_password='password', - pact_dir='some_dir') - - with pact: - pass - - self.write_to_pact_file.assert_called_once() - self.mock_publish.assert_called_once_with( - pact, - 'TestConsumer', - '0.0.0', - auto_detect_version_properties=False, - branch=None, - build_url=None, - consumer_tags=None, - pact_dir='some_dir', - tag_with_git_branch=False - ) - - def test_context_raises_error(self): - pact = MessagePact( - self.consumer, self.provider, publish_to_broker=True, - broker_base_url='http://localhost', broker_username='username', broker_password='password') - - with self.assertRaises(RuntimeError): - with pact: - raise RuntimeError - - self.write_to_pact_file.assert_not_called() - self.mock_publish.assert_not_called() - - -class PactGeneratePactFileTestCase(TestCase): - def setUp(self): - super(PactGeneratePactFileTestCase, self).setUp() - self.addCleanup(patch.stopall) - self.mock_Popen = patch.object(message_pact, 'Popen', autospec=True).start() - self.mock_Popen.return_value.returncode = 0 - - self.consumer = MessageConsumer('TestConsumer') - self.provider = Provider('TestProvider') - - def test_call_pact_message_to_generate_pact_file(self): - target = MessagePact( - self.consumer, self.provider, pact_dir='/pacts', - version='3.0.0', file_write_mode='merge', publish_to_broker=True) - - (target - .given('There is an alligator named John') - .expects_to_receive('an alligator message') - .with_content({ - 'name': 'John', - 'document_name': 'sample_document.doc' - }).with_metadata({ - 'contentType': 'application/json', - 'source': 'legacy_api'})) - - target.write_to_pact_file() - - self.mock_Popen.assert_called_once_with([ - MESSAGE_PATH, 'update', - json.dumps(target._messages[0]), - '--pact-dir', '/pacts', - '--pact-specification-version=3.0.0', - '--consumer', 'TestConsumer', - '--provider', 'TestProvider', - ], env=ANY) diff --git a/tests/v2/test_message_provider.py b/tests/v2/test_message_provider.py deleted file mode 100644 index 308cccf39..000000000 --- a/tests/v2/test_message_provider.py +++ /dev/null @@ -1,168 +0,0 @@ -import os -# import pytest -from mock import patch, Mock -from unittest import TestCase -from unittest.mock import ANY - -from pact.v2.message_provider import MessageProvider -from pact.v2 import message_provider as message_provider - - -class MessageProviderTestCase(TestCase): - def _mock_response( - self, - status=200, - content="fake response", - raise_for_status=None): - - mock_resp = Mock() - mock_resp.raise_for_status = Mock() - if raise_for_status: - mock_resp.raise_for_status.side_effect = raise_for_status - - mock_resp.status_code = status - mock_resp.text = content - return mock_resp - - def message_handler(self): - return {'success': True} - - def setUp(self): - self.provider = MessageProvider( - provider='DocumentService', - consumer='DetectContentLambda', - message_providers={ - 'a document created successfully': self.message_handler - } - ) - self.options = { - 'broker_username': "test", - 'broker_password': "test", - 'broker_url': "http://localhost", - 'publish_version': '3', - 'publish_verification_results': False - } - - def test_init(self): - self.assertIsInstance(self.provider, MessageProvider) - self.assertEqual(self.provider.provider, 'DocumentService') - self.assertEqual(self.provider.consumer, 'DetectContentLambda') - self.assertEqual(self.provider.pact_dir, os.getcwd()) - self.assertEqual(self.provider.version, '3.0.0') - self.assertEqual(self.provider.proxy_host, 'localhost') - self.assertEqual(self.provider.proxy_port, '1234') - - @patch('pact.v2.Verifier.verify_pacts', return_value=(0, 'logs')) - def test_verify(self, mock_verify_pacts): - self.provider.verify() - - assert mock_verify_pacts.call_count == 1 - mock_verify_pacts.assert_called_with(f'{self.provider.pact_dir}/{self.provider._pact_file()}', verbose=False) - - @patch('pact.v2.Verifier.verify_with_broker', return_value=(0, 'logs')) - def test_verify_with_broker(self, mock_verify_pacts): - self.provider.verify_with_broker(**self.options) - - assert mock_verify_pacts.call_count == 1 - mock_verify_pacts.assert_called_with(False, None, broker_username="test", - broker_password="test", - broker_url="http://localhost", - publish_version='3', - publish_verification_results=False) - - -class MessageProviderContextManagerTestCase(MessageProviderTestCase): - def setUp(self): - super(MessageProviderContextManagerTestCase, self).setUp() - - @patch('pact.v2.message_provider.MessageProvider._start_proxy', return_value=0) - @patch('pact.v2.message_provider.MessageProvider._stop_proxy', return_value=0) - def test_successful(self, mock_stop_proxy, mock_start_proxy): - with self.provider: - pass - - mock_start_proxy.assert_called_once() - mock_stop_proxy.assert_called_once() - - @patch('pact.v2.message_provider.MessageProvider._wait_for_server_start', side_effect=RuntimeError('boom!')) - @patch('pact.v2.message_provider.MessageProvider._start_proxy', return_value=0) - @patch('pact.v2.message_provider.MessageProvider._stop_proxy', return_value=0) - def test_stop_proxy_on_runtime_error(self, mock_stop_proxy, mock_start_proxy, mock_wait_for_server_start,): - with self.provider: - pass - - mock_start_proxy.assert_called_once() - mock_stop_proxy.assert_called_once() - - -class StartProxyTestCase(MessageProviderTestCase): - def setUp(self): - super(StartProxyTestCase, self).setUp() - - -class StopProxyTestCase(MessageProviderTestCase): - def setUp(self): - super(StopProxyTestCase, self).setUp() - - @patch('requests.post') - def test_shutdown_successfully(self, mock_requests): - mock_requests.return_value = self._mock_response(content="success") - self.provider._stop_proxy() - - -class SetupStateTestCase(MessageProviderTestCase): - def setUp(self): - super(SetupStateTestCase, self).setUp() - - @patch('requests.post') - def test_shutdown_successfully(self, mock_requests): - mock_requests.return_value = self._mock_response(status=201) - self.provider._setup_states() - expected_payload = { - 'messageHandlers': { - 'a document created successfully': self.message_handler() - } - } - - mock_requests.assert_called_once_with(f'{self.provider._proxy_url()}/setup', headers={'User-Agent': ANY}, verify=False, json=expected_payload) - - -class WaitForServerStartTestCase(MessageProviderTestCase): - def setUp(self): - super(WaitForServerStartTestCase, self).setUp() - - @patch.object(message_provider.requests, 'Session') - @patch.object(message_provider, 'Retry') - @patch.object(message_provider, 'HTTPAdapter') - @patch('pact.v2.message_provider.MessageProvider._stop_proxy') - def test_wait_for_server_start_success(self, mock_stop_proxy, mock_HTTPAdapter, mock_Retry, mock_Session): - mock_Session.return_value.get.return_value.status_code = 200 - self.provider._wait_for_server_start() - - session = mock_Session.return_value - session.mount.assert_called_once_with( - 'http://', mock_HTTPAdapter.return_value) - session.get.assert_called_once_with(f'{self.provider._proxy_url()}/ping', headers={'User-Agent': ANY}, verify=False) - mock_HTTPAdapter.assert_called_once_with( - max_retries=mock_Retry.return_value) - mock_Retry.assert_called_once_with(total=9, backoff_factor=0.5) - mock_stop_proxy.assert_not_called() - - @patch.object(message_provider.requests, 'Session') - @patch.object(message_provider, 'Retry') - @patch.object(message_provider, 'HTTPAdapter') - @patch('pact.v2.message_provider.MessageProvider._stop_proxy') - def test_wait_for_server_start_failure(self, mock_stop_proxy, mock_HTTPAdapter, mock_Retry, mock_Session): - mock_Session.return_value.get.return_value.status_code = 500 - - with self.assertRaises(RuntimeError): - self.provider._wait_for_server_start() - - session = mock_Session.return_value - session.mount.assert_called_once_with( - 'http://', mock_HTTPAdapter.return_value) - session.get.assert_called_once_with(f'{self.provider._proxy_url()}/ping', headers={'User-Agent': ANY}, verify=False) - mock_HTTPAdapter.assert_called_once_with( - max_retries=mock_Retry.return_value) - mock_Retry.assert_called_once_with(total=9, backoff_factor=0.5) - mock_stop_proxy.assert_called_once() diff --git a/tests/v2/test_pact.py b/tests/v2/test_pact.py deleted file mode 100644 index 055de82d3..000000000 --- a/tests/v2/test_pact.py +++ /dev/null @@ -1,664 +0,0 @@ -import os -from subprocess import Popen -from unittest import TestCase -from unittest.mock import ANY - -from mock import patch, call, Mock -from psutil import Process - -from pact.v2.broker import Broker -from pact.v2.consumer import Consumer, Provider -from pact.v2.matchers import Term -from pact_cli import MOCK_SERVICE_PATH -from pact.v2.pact import Pact, FromTerms, Request, Response -from pact.v2 import pact as pact -from pact.v2.verify_wrapper import PactException - - -class PactTestCase(TestCase): - def setUp(self): - self.consumer = Consumer('TestConsumer') - self.provider = Provider('TestProvider') - - def test_init_defaults(self): - target = Pact(self.consumer, self.provider) - self.assertIs(target.broker_base_url, None) - self.assertIs(target.broker_username, None) - self.assertIs(target.broker_password, None) - self.assertIs(target.consumer, self.consumer) - self.assertIs(target.cors, False) - self.assertEqual(target.host_name, 'localhost') - self.assertEqual(target.log_dir, os.getcwd()) - self.assertEqual(target.pact_dir, os.getcwd()) - self.assertEqual(target.port, 1234) - self.assertIs(target.provider, self.provider) - self.assertIs(target.publish_to_broker, False) - self.assertIs(target.ssl, False) - self.assertIsNone(target.sslcert) - self.assertIsNone(target.sslkey) - self.assertEqual(target.uri, 'http://localhost:1234') - self.assertEqual(target.specification_version, '2.0.0') - self.assertEqual(len(target._interactions), 0) - - def test_init_custom_mock_service(self): - target = Pact( - self.consumer, self.provider, host_name='192.168.1.1', port=8000, - log_dir='/logs', ssl=True, sslcert='/ssl.cert', sslkey='/ssl.pem', - cors=True, pact_dir='/pacts', specification_version='3.0.0', - file_write_mode='merge') - - self.assertIs(target.consumer, self.consumer) - self.assertIs(target.cors, True) - self.assertEqual(target.host_name, '192.168.1.1') - self.assertEqual(target.log_dir, '/logs') - self.assertEqual(target.pact_dir, '/pacts') - self.assertEqual(target.port, 8000) - self.assertIs(target.provider, self.provider) - self.assertIs(target.ssl, True) - self.assertEqual(target.sslcert, '/ssl.cert') - self.assertEqual(target.sslkey, '/ssl.pem') - self.assertEqual(target.uri, 'https://192.168.1.1:8000') - self.assertEqual(target.specification_version, '3.0.0') - self.assertEqual(target.file_write_mode, 'merge') - self.assertEqual(len(target._interactions), 0) - - def test_init_publish_to_broker(self): - target = Pact( - self.consumer, self.provider, publish_to_broker=True, - broker_base_url='http://localhost', broker_username='username', - broker_password='password', broker_token='token') - - self.assertEqual(target.broker_base_url, 'http://localhost') - self.assertEqual(target.broker_username, 'username') - self.assertEqual(target.broker_password, 'password') - self.assertEqual(target.broker_token, 'token') - self.assertIs(target.publish_to_broker, True) - - def test_definition_sparse(self): - target = Pact(self.consumer, self.provider) - (target - .given('I am creating a new pact using the Pact class') - .upon_receiving('a specific request to the server') - .with_request('GET', '/path') - .will_respond_with(200, body='success')) - - self.assertEqual(len(target._interactions), 1) - - self.assertEqual( - target._interactions[0]['provider_state'], - 'I am creating a new pact using the Pact class') - - self.assertEqual( - target._interactions[0]['description'], - 'a specific request to the server') - - self.assertEqual(target._interactions[0]['request'], - {'path': '/path', 'method': 'GET'}) - self.assertEqual(target._interactions[0]['response'], - {'status': 200, 'body': 'success'}) - - def test_definition_without_given(self): - target = Pact(self.consumer, self.provider) - (target - .upon_receiving('a specific request to the server') - .with_request('GET', '/path') - .will_respond_with(200, body='success')) - - self.assertEqual(len(target._interactions), 1) - - self.assertIsNone( - target._interactions[0].get('provider_state')) - - self.assertEqual( - target._interactions[0]['description'], - 'a specific request to the server') - - self.assertEqual(target._interactions[0]['request'], - {'path': '/path', 'method': 'GET'}) - self.assertEqual(target._interactions[0]['response'], - {'status': 200, 'body': 'success'}) - - def test_definition_all_options(self): - target = Pact(self.consumer, self.provider) - (target - .given('I am creating a new pact using the Pact class') - .upon_receiving('a specific request to the server') - .with_request('GET', '/path', - body={'key': 'value'}, - headers={'Accept': 'application/json'}, - query={'search': 'test'}) - .will_respond_with( - 200, - body='success', headers={'Content-Type': 'application/json'})) - - self.assertEqual( - target._interactions[0]['provider_state'], - 'I am creating a new pact using the Pact class') - - self.assertEqual( - target._interactions[0]['description'], - 'a specific request to the server') - - self.assertEqual(target._interactions[0]['request'], { - 'path': '/path', - 'method': 'GET', - 'body': {'key': 'value'}, - 'headers': {'Accept': 'application/json'}, - 'query': {'search': 'test'}}) - self.assertEqual(target._interactions[0]['response'], { - 'status': 200, - 'body': 'success', - 'headers': {'Content-Type': 'application/json'}}) - - def test_definition_multiple_interactions(self): - target = Pact(self.consumer, self.provider) - (target - .given('I am creating a new pact using the Pact class') - .upon_receiving('a specific request to the server') - .with_request('GET', '/foo') - .will_respond_with(200, body='success') - .given('I am creating another new pact using the Pact class') - .upon_receiving('a different request to the server') - .with_request('GET', '/bar') - .will_respond_with(200, body='success')) - - self.assertEqual(len(target._interactions), 2) - - self.assertEqual( - target._interactions[1]['provider_state'], - 'I am creating a new pact using the Pact class') - self.assertEqual( - target._interactions[0]['provider_state'], - 'I am creating another new pact using the Pact class') - - self.assertEqual( - target._interactions[1]['description'], - 'a specific request to the server') - self.assertEqual( - target._interactions[0]['description'], - 'a different request to the server') - - self.assertEqual(target._interactions[1]['request'], - {'path': '/foo', 'method': 'GET'}) - self.assertEqual(target._interactions[0]['request'], - {'path': '/bar', 'method': 'GET'}) - - self.assertEqual(target._interactions[1]['response'], - {'status': 200, 'body': 'success'}) - self.assertEqual(target._interactions[0]['response'], - {'status': 200, 'body': 'success'}) - - def test_definition_multiple_interactions_without_given(self): - target = Pact(self.consumer, self.provider) - (target - .upon_receiving('a specific request to the server') - .with_request('GET', '/foo') - .will_respond_with(200, body='success') - .upon_receiving('a different request to the server') - .with_request('GET', '/bar') - .will_respond_with(200, body='success')) - - self.assertEqual(len(target._interactions), 2) - - self.assertIsNone( - target._interactions[1].get('provider_state')) - self.assertIsNone( - target._interactions[0].get('provider_state')) - - self.assertEqual( - target._interactions[1]['description'], - 'a specific request to the server') - self.assertEqual( - target._interactions[0]['description'], - 'a different request to the server') - - self.assertEqual(target._interactions[1]['request'], - {'path': '/foo', 'method': 'GET'}) - self.assertEqual(target._interactions[0]['request'], - {'path': '/bar', 'method': 'GET'}) - - self.assertEqual(target._interactions[1]['response'], - {'status': 200, 'body': 'success'}) - self.assertEqual(target._interactions[0]['response'], - {'status': 200, 'body': 'success'}) - -class PactSetupTestCase(PactTestCase): - def setUp(self): - super(PactSetupTestCase, self).setUp() - self.addCleanup(patch.stopall) - self.mock_requests = patch('requests.api.request').start() - self.target = Pact(self.consumer, self.provider) - (self.target - .given('I am creating a new pact using the Pact class') - .upon_receiving('a specific request to the server') - .with_request('GET', '/path') - .will_respond_with(200, body='success')) - - self.delete_call = call('delete', 'http://localhost:1234/interactions', - headers={'X-Pact-Mock-Service': 'true', 'User-Agent': ANY}, - verify=False) - - self.put_interactions_call = call( - 'put', 'http://localhost:1234/interactions', - data=None, - headers={'X-Pact-Mock-Service': 'true', 'User-Agent': ANY}, - verify=False, - json={'interactions': [{ - 'response': {'status': 200, 'body': 'success'}, - 'request': {'path': '/path', 'method': 'GET'}, - 'description': 'a specific request to the server', - 'provider_state': 'I am creating a new pact using the ' - 'Pact class'}]}) - - def test_error_deleting_interactions(self): - self.mock_requests.side_effect = iter([ - Mock(status_code=500, text='deletion error')]) - - with self.assertRaises(AssertionError) as e: - self.target.setup() - - self.assertEqual(str(e.exception), 'deletion error') - self.assertEqual(self.mock_requests.call_count, 1) - self.mock_requests.assert_has_calls([self.delete_call]) - - def test_error_posting_interactions(self): - self.mock_requests.side_effect = iter([ - Mock(status_code=200), - Mock(status_code=500, text='post interactions error')]) - - with self.assertRaises(AssertionError) as e: - self.target.setup() - - self.assertEqual(str(e.exception), 'post interactions error') - self.assertEqual(self.mock_requests.call_count, 2) - self.mock_requests.assert_has_calls( - [self.delete_call, self.put_interactions_call]) - - def test_successful(self): - self.mock_requests.side_effect = iter([Mock(status_code=200)] * 4) - self.target.setup() - - self.assertEqual(self.mock_requests.call_count, 2) - self.mock_requests.assert_has_calls([ - self.delete_call, self.put_interactions_call]) - - -class PactStartShutdownServerTestCase(TestCase): - def setUp(self): - super(PactStartShutdownServerTestCase, self).setUp() - self.addCleanup(patch.stopall) - self.mock_Popen = patch.object(pact, 'Popen', autospec=True).start() - self.mock_Popen.return_value.returncode = 0 - self.mock_Process = patch.object( - pact.psutil, 'Process', autospec=True).start() - self.mock_platform = patch.object( - pact.platform, 'platform', autospec=True).start() - self.mock_wait_for_server_start = patch.object( - pact.Pact, '_wait_for_server_start', autospec=True).start() - self.mock_Pid_exists = patch.object( - pact.psutil, 'pid_exists', autospec=True).start() - self.mock_publish = patch.object( - Broker, 'publish', autospec=True).start() - - def test_start_fails(self): - self.mock_Popen.return_value.returncode = 1 - self.mock_wait_for_server_start.side_effect = RuntimeError - pact = Pact(Consumer('consumer'), Provider('provider'), - log_dir='/logs', pact_dir='/pacts') - - with self.assertRaises(RuntimeError): - pact.start_service() - - self.mock_Popen.assert_called_once_with([ - MOCK_SERVICE_PATH, 'service', - '--host=localhost', - '--port=1234', - '--log', '/logs/pact-mock-service.log', - '--pact-dir', '/pacts', - '--pact-file-write-mode', 'overwrite', - '--pact-specification-version=2.0.0', - '--consumer', 'consumer', - '--provider', 'provider'], env=ANY) - - def test_start_no_ssl(self): - pact = Pact(Consumer('consumer'), Provider('provider'), - log_dir='/logs', pact_dir='/pacts') - pact.start_service() - - self.mock_Popen.assert_called_once_with([ - MOCK_SERVICE_PATH, 'service', - '--host=localhost', - '--port=1234', - '--log', '/logs/pact-mock-service.log', - '--pact-dir', '/pacts', - '--pact-file-write-mode', 'overwrite', - '--pact-specification-version=2.0.0', - '--consumer', 'consumer', - '--provider', 'provider'], env=ANY) - - def test_start_with_ssl(self): - pact = Pact(Consumer('consumer'), Provider('provider'), - log_dir='/logs', pact_dir='/pacts', - ssl=True, sslcert='/ssl.cert', sslkey='/ssl.key') - pact.start_service() - - self.mock_Popen.assert_called_once_with([ - MOCK_SERVICE_PATH, 'service', - '--host=localhost', - '--port=1234', - '--log', '/logs/pact-mock-service.log', - '--pact-dir', '/pacts', - '--pact-file-write-mode', 'overwrite', - '--pact-specification-version=2.0.0', - '--consumer', 'consumer', - '--provider', 'provider', - '--ssl', - '--sslcert', '/ssl.cert', - '--sslkey', '/ssl.key'], env=ANY) - - def test_stop_posix(self): - self.mock_publish.return_value.returncode = 0 - self.mock_platform.return_value = 'Linux' - pact = Pact(Consumer('consumer'), Provider('provider')) - pact._process = Mock(spec=Popen, pid=999, returncode=0) - pact.stop_service() - - pact._process.terminate.assert_called_once_with() - pact._process.communicate.assert_called_once_with() - self.mock_publish.assert_not_called() - self.assertFalse(self.mock_Process.called) - - def test_stop_windows(self): - self.mock_platform.return_value = 'Windows' - ruby_exe = Mock(spec=Process) - self.mock_Process.return_value.children.return_value = [ruby_exe] - self.mock_Pid_exists.return_value = False - pact = Pact(Consumer('consumer', version='abc'), Provider('provider'), publish_to_broker=True, pact_dir='some_dir') - pact._process = Mock(spec=Popen, pid=999) - pact.stop_service() - - self.assertFalse(pact._process.terminate.called) - self.assertFalse(pact._process.communicate.called) - self.mock_Process.assert_called_once_with(999) - self.mock_Process.return_value.children.assert_called_once_with( - recursive=True) - ruby_exe.terminate.assert_called_once_with() - self.mock_Process.return_value.wait.assert_called_once_with() - self.mock_Pid_exists.assert_called_once_with(999) - self.mock_publish.assert_called_once_with( - pact, - 'consumer', - 'abc', - consumer_tags=None, - tag_with_git_branch=False, - pact_dir='some_dir', - branch=None, - build_url=None, - auto_detect_version_properties=False) - - def test_stop_fails_posix(self): - self.mock_platform.return_value = 'Linux' - self.mock_Popen.return_value.returncode = 1 - pact = Pact(Consumer('consumer'), Provider('provider')) - pact._process = Mock(spec=Popen, pid=999, returncode=1) - with self.assertRaises(RuntimeError): - pact.stop_service() - - pact._process.terminate.assert_called_once_with() - pact._process.communicate.assert_called_once_with() - self.mock_publish.assert_not_called() - - def test_stop_fails_windows(self): - self.mock_platform.return_value = 'Windows' - self.mock_Popen.return_value.returncode = 15 - self.mock_Pid_exists.return_value = True - - pact = Pact(Consumer('consumer'), Provider('provider')) - pact._process = Mock(spec=Popen, pid=999, returncode=15) - with self.assertRaises(RuntimeError): - pact.stop_service() - - self.assertFalse(pact._process.terminate.called) - self.assertFalse(pact._process.communicate.called) - self.mock_Process.assert_called_once_with(999) - self.mock_Process.return_value.children.assert_called_once_with( - recursive=True) - self.mock_Process.return_value.wait.assert_called_once_with() - self.mock_Pid_exists.assert_called_once_with(999) - self.mock_publish.assert_not_called() - - -class PactWaitForServerStartTestCase(TestCase): - def setUp(self): - super(PactWaitForServerStartTestCase, self).setUp() - self.addCleanup(patch.stopall) - self.mock_HTTPAdapter = patch.object( - pact, 'HTTPAdapter', autospec=True).start() - self.mock_Retry = patch.object(pact, 'Retry', autospec=True).start() - self.mock_Session = patch.object( - pact.requests, 'Session', autospec=True).start() - - def test_wait_for_server_start_success(self): - self.mock_Session.return_value.get.return_value.status_code = 200 - pact = Pact(Consumer('consumer'), Provider('provider')) - pact._process = Mock(spec=Popen) - pact._wait_for_server_start() - - session = self.mock_Session.return_value - session.mount.assert_called_once_with( - 'http://', self.mock_HTTPAdapter.return_value) - session.get.assert_called_once_with( - 'http://localhost:1234', - headers={'X-Pact-Mock-Service': 'true', 'User-Agent': ANY}, - verify=False) - self.mock_HTTPAdapter.assert_called_once_with( - max_retries=self.mock_Retry.return_value) - self.mock_Retry.assert_called_once_with(total=9, backoff_factor=0.1) - self.assertFalse(pact._process.communicate.called) - self.assertFalse(pact._process.terminate.called) - - def test_wait_for_server_start_failure(self): - self.mock_Session.return_value.get.return_value.status_code = 500 - pact = Pact(Consumer('consumer'), Provider('provider')) - pact._process = Mock(spec=Popen) - with self.assertRaises(RuntimeError): - pact._wait_for_server_start() - - session = self.mock_Session.return_value - session.mount.assert_called_once_with( - 'http://', self.mock_HTTPAdapter.return_value) - session.get.assert_called_once_with( - 'http://localhost:1234', - headers={'X-Pact-Mock-Service': 'true', 'User-Agent': ANY}, - verify=False) - self.mock_HTTPAdapter.assert_called_once_with( - max_retries=self.mock_Retry.return_value) - self.mock_Retry.assert_called_once_with(total=9, backoff_factor=0.1) - pact._process.communicate.assert_called_once_with() - pact._process.terminate.assert_called_once_with() - - -class PactVerifyTestCase(PactTestCase): - def setUp(self): - super(PactVerifyTestCase, self).setUp() - self.addCleanup(patch.stopall) - self.mock_requests = patch('requests.api.request').start() - self.target = Pact(self.consumer, self.provider) - (self.target - .given('I am creating a new pact using the Pact class') - .upon_receiving('a specific request to the server') - .with_request('GET', '/path') - .will_respond_with(200, body='success')) - self.get_verification_call = call( - 'get', 'http://localhost:1234/interactions/verification', - headers={'X-Pact-Mock-Service': 'true', 'User-Agent': ANY}, - verify=False, - params=None) - - self.post_publish_pacts_call = call( - 'post', 'http://localhost:1234/pact', - data=None, - headers={'X-Pact-Mock-Service': 'true', 'User-Agent': ANY}, - verify=False, - json=None) - - def test_success(self): - self.mock_requests.side_effect = iter([Mock(status_code=200)] * 2) - self.target.verify() - - self.assertEqual(self.mock_requests.call_count, 2) - self.mock_requests.assert_has_calls([ - self.get_verification_call, self.post_publish_pacts_call]) - - def test_error_verifying_interactions(self): - self.mock_requests.side_effect = iter([ - Mock(status_code=500, text='verification error')]) - - with self.assertRaises(AssertionError) as e: - self.target.verify() - - self.assertEqual(str(e.exception), 'verification error') - self.assertEqual(self.mock_requests.call_count, 1) - self.mock_requests.assert_has_calls([ - self.get_verification_call]) - - def test_error_writing_pacts_to_file(self): - self.mock_requests.side_effect = iter([ - Mock(status_code=200), - Mock(status_code=500, text='error writing pact to file')]) - - with self.assertRaises(AssertionError) as e: - self.target.verify() - - self.assertEqual(str(e.exception), 'error writing pact to file') - self.assertEqual(self.mock_requests.call_count, 2) - self.mock_requests.assert_has_calls([ - self.get_verification_call, self.post_publish_pacts_call]) - - -class PactContextManagerTestCase(PactTestCase): - def setUp(self): - super(PactContextManagerTestCase, self).setUp() - self.addCleanup(patch.stopall) - self.mock_setup = patch.object( - pact.Pact, 'setup', autospec=True).start() - - self.mock_verify = patch.object( - pact.Pact, 'verify', autospec=True).start() - - def test_successful(self): - pact = Pact(self.consumer, self.provider) - with pact: - pass - - self.mock_setup.assert_called_once_with(pact) - self.mock_verify.assert_called_once_with(pact) - - def test_context_raises_error(self): - pact = Pact(self.consumer, self.provider) - with self.assertRaises(RuntimeError): - with pact: - raise RuntimeError - - self.mock_setup.assert_called_once_with(pact) - self.assertFalse(self.mock_verify.called) - - def test_does_not_leave_interactions_after_exception(self): - pact = Pact(self.consumer, self.provider) - (pact - .given('I am creating a new pact using the Pact class') - .upon_receiving('a specific request to the server') - .with_request('GET', '/path') - .will_respond_with(200, body='success')) - with self.assertRaises(RuntimeError): - with pact: - raise RuntimeError - - assert pact._interactions == [] - - - - - -class PactContextManagerSetupTestCase(PactTestCase): - def test_definition_without_description(self): - # Description (populated from "given") is listed in the MANDATORY_FIELDS. - # Make sure if it isn't there that an exception is raised - pact = Pact(self.consumer, self.provider) - (pact.given("A request without a description") - .with_request('GET', '/path') - .will_respond_with(200, body='success')) - - self.assertEqual(len(pact._interactions), 1) - - self.assertTrue('description' not in pact._interactions[0]) - - # By using "with", __enter__ will call the setup method that will verify if this is present - with self.assertRaises(PactException): - with pact: - pact.verify() - - -class FromTermsTestCase(TestCase): - def test_json(self): - with self.assertRaises(NotImplementedError): - FromTerms().json() - - -class RequestTestCase(TestCase): - def test_sparse(self): - target = Request('GET', '/path') - result = target.json() - self.assertEqual(result, { - 'method': 'GET', - 'path': '/path'}) - - def test_all_options(self): - target = Request( - 'POST', '/path', - body='the content', - headers={'Accept': 'application/json'}, - query='term=test') - - result = target.json() - self.assertEqual(result, { - 'method': 'POST', - 'path': '/path', - 'body': 'the content', - 'headers': {'Accept': 'application/json'}, - 'query': 'term=test'}) - - def test_falsey_body(self): - target = Request('GET', '/path', body=[]) - result = target.json() - self.assertEqual(result, { - 'method': 'GET', - 'path': '/path', - 'body': []}) - - def test_matcher_in_path_gets_converted(self): - target = Request('GET', Term('\/.+', '/test-path')) # noqa: W605 - result = target.json() - self.assertTrue(isinstance(result['path'], dict)) - - -class ResponseTestCase(TestCase): - def test_sparse(self): - target = Response(200) - result = target.json() - self.assertEqual(result, {'status': 200}) - - def test_all_options(self): - target = Response( - 202, headers={'Content-Type': 'application/json'}, body='the body') - - result = target.json() - self.assertEqual(result, { - 'status': 202, - 'body': 'the body', - 'headers': {'Content-Type': 'application/json'}}) - - def test_falsey_body(self): - target = Response(200, body=[]) - result = target.json() - self.assertEqual(result, {'status': 200, 'body': []}) diff --git a/tests/v2/test_verifier.py b/tests/v2/test_verifier.py deleted file mode 100644 index eab8fd763..000000000 --- a/tests/v2/test_verifier.py +++ /dev/null @@ -1,267 +0,0 @@ -from collections import OrderedDict - -from unittest import TestCase -import unittest -from mock import patch - -from pact.v2.verifier import Verifier -from pact.v2.verify_wrapper import VerifyWrapper - - -def assertVerifyCalled(mock_wrapper, *pacts, **options): - tc = unittest.TestCase() - tc.assertEqual(mock_wrapper.call_count, 1) - - mock_wrapper.assert_called_once_with(*pacts, **options) - - -class VerifierPactsTestCase(TestCase): - - def setUp(self): - super(VerifierPactsTestCase, self).setUp() - self.addCleanup(patch.stopall) - self.verifier = Verifier(provider='test_provider', - provider_base_url="http://localhost:8888") - - self.mock_wrapper = patch.object( - VerifyWrapper, 'call_verify').start() - - @patch("pact.v2.verify_wrapper.VerifyWrapper.call_verify") - @patch('pact.v2.verifier.path_exists', return_value=True) - def test_verifier_with_provider_and_files(self, mock_path_exists, mock_wrapper): - mock_wrapper.return_value = (True, 'some logs') - - output, _ = self.verifier.verify_pacts('path/to/pact1', - 'path/to/pact2', - headers=['header1', 'header2']) - - assertVerifyCalled(mock_wrapper, - 'path/to/pact1', - 'path/to/pact2', - provider='test_provider', - custom_provider_headers=['header1', 'header2'], - provider_base_url='http://localhost:8888', - log_level='INFO', - verbose=False, - enable_pending=False, - include_wip_pacts_since=None) - - @patch("pact.v2.verify_wrapper.VerifyWrapper.call_verify") - @patch('pact.v2.verifier.path_exists', return_value=True) - def test_verifier_with_provider_and_files_passes_consumer_selectors(self, mock_path_exists, mock_wrapper): - mock_wrapper.return_value = (True, 'some logs') - - output, _ = self.verifier.verify_pacts( - 'path/to/pact1', - 'path/to/pact2', - headers=['header1', 'header2'], - consumer_version_selectors=[ - # Using OrderedDict for the sake of testing - OrderedDict([("tag", "main"), ("latest", True)]), - OrderedDict([("tag", "test"), ("latest", False)]), - ] - ) - - assertVerifyCalled(mock_wrapper, - 'path/to/pact1', - 'path/to/pact2', - provider='test_provider', - custom_provider_headers=['header1', 'header2'], - provider_base_url='http://localhost:8888', - log_level='INFO', - verbose=False, - enable_pending=False, - include_wip_pacts_since=None, - consumer_selectors=['{"tag": "main", "latest": true}', - '{"tag": "test", "latest": false}']) - - def test_validate_on_publish_results(self): - self.assertRaises(Exception, self.verifier.verify_pacts, 'path/to/pact1', publish=True) - - @patch("pact.v2.verify_wrapper.VerifyWrapper.call_verify") - @patch('pact.v2.verifier.path_exists', return_value=True) - def test_publish_on_success(self, mock_path_exists, mock_wrapper): - mock_wrapper.return_value = (True, 'some logs') - - output, _ = self.verifier.verify_pacts('path/to/pact1', publish_version='1.0.0') - - assertVerifyCalled(mock_wrapper, - 'path/to/pact1', - provider='test_provider', - provider_base_url='http://localhost:8888', - log_level='INFO', - verbose=False, - provider_app_version='1.0.0', - enable_pending=False, - include_wip_pacts_since=None) - - @patch('pact.v2.verifier.path_exists', return_value=False) - def test_raises_error_on_missing_pact_files(self, mock_path_exists): - self.assertRaises(Exception, - self.verifier.verify_pacts, - 'path/to/pact1', 'path/to/pact2') - - mock_path_exists.assert_called_with('path/to/pact2') - - @patch("pact.v2.verify_wrapper.VerifyWrapper.call_verify", return_value=(0, None)) - @patch('pact.v2.verifier.expand_directories', return_value=['./pacts/pact1', './pacts/pact2']) - @patch('pact.v2.verifier.path_exists', return_value=True) - def test_expand_directories_called_for_pacts(self, mock_path_exists, mock_expand_dir, mock_wrapper): - output, _ = self.verifier.verify_pacts('path/to/pact1', - 'path/to/pact2') - - mock_expand_dir.assert_called_once() - - @patch('pact.v2.verify_wrapper.VerifyWrapper.call_verify', return_value=(0, None)) - def test_passes_enable_pending_flag_value(self, mock_wrapper): - for value in (True, False): - with self.subTest(value=value): - with patch('pact.v2.verifier.path_exists'): - self.verifier.verify_pacts('any.json', enable_pending=value) - self.assertTrue( - ('enable_pending', value) in mock_wrapper.call_args.kwargs.items(), - mock_wrapper.call_args.kwargs, - ) - - @patch('pact.v2.verify_wrapper.VerifyWrapper.call_verify', return_value=(0, None)) - @patch('pact.v2.verifier.path_exists', return_value=True) - def test_passes_include_wip_pacts_since_value(self, mock_path_exists, mock_wrapper): - self.verifier.verify_pacts('any.json', include_wip_pacts_since='2018-01-01') - self.assertTrue( - ('include_wip_pacts_since', '2018-01-01') in mock_wrapper.call_args.kwargs.items(), - mock_wrapper.call_args.kwargs, - ) - - -class VerifierBrokerTestCase(TestCase): - - def setUp(self): - super(VerifierBrokerTestCase, self).setUp() - self.addCleanup(patch.stopall) - self.verifier = Verifier(provider='test_provider', - provider_base_url="http://localhost:8888") - - self.mock_wrapper = patch.object( - VerifyWrapper, 'call_verify').start() - self.broker_username = 'broker_username' - self.broker_password = 'broker_password' - self.broker_url = 'http://broker' - - self.default_opts = { - 'broker_username': self.broker_username, - 'broker_password': self.broker_password, - 'broker_url': self.broker_url, - 'broker_token': 'token' - } - - @patch("pact.v2.verify_wrapper.VerifyWrapper.call_verify") - def test_verifier_with_broker(self, mock_wrapper): - - mock_wrapper.return_value = (True, 'some value') - - output, _ = self.verifier.verify_with_broker(**self.default_opts) - - self.assertTrue(output) - assertVerifyCalled(mock_wrapper, - provider='test_provider', - provider_base_url='http://localhost:8888', - broker_password=self.broker_password, - broker_username=self.broker_username, - broker_token='token', - broker_url=self.broker_url, - log_level='INFO', - verbose=False, - enable_pending=False, - include_wip_pacts_since=None) - - @patch("pact.v2.verify_wrapper.VerifyWrapper.call_verify") - def test_verifier_and_publish_with_broker(self, mock_wrapper): - - mock_wrapper.return_value = (True, 'some value') - - self.default_opts['publish_version'] = '1.0.0' - output, _ = self.verifier.verify_with_broker(**self.default_opts) - - self.assertTrue(output) - assertVerifyCalled(mock_wrapper, - provider='test_provider', - provider_base_url='http://localhost:8888', - broker_password=self.broker_password, - broker_username=self.broker_username, - broker_token='token', - broker_url=self.broker_url, - log_level='INFO', - verbose=False, - enable_pending=False, - include_wip_pacts_since=None, - provider_app_version='1.0.0', - ) - - @patch("pact.v2.verify_wrapper.VerifyWrapper.call_verify") - def test_verifier_with_broker_passes_consumer_selectors(self, mock_wrapper): - - mock_wrapper.return_value = (True, 'some value') - - output, _ = self.verifier.verify_with_broker( - consumer_version_selectors=[ - # Using OrderedDict for the sake of testing - OrderedDict([("tag", "main"), ("latest", True)]), - OrderedDict([("tag", "test"), ("latest", False)]), - ], - **self.default_opts - ) - - self.assertTrue(output) - assertVerifyCalled(mock_wrapper, - provider='test_provider', - provider_base_url='http://localhost:8888', - broker_password=self.broker_password, - broker_username=self.broker_username, - broker_token='token', - broker_url=self.broker_url, - log_level='INFO', - verbose=False, - enable_pending=False, - include_wip_pacts_since=None, - consumer_selectors=['{"tag": "main", "latest": true}', - '{"tag": "test", "latest": false}']) - - @patch("pact.v2.verify_wrapper.VerifyWrapper.call_verify") - @patch('pact.v2.verifier.path_exists', return_value=True) - def test_publish_on_success(self, mock_path_exists, mock_wrapper): - mock_wrapper.return_value = (True, 'some logs') - - self.verifier.verify_with_broker(publish_version='1.0.0', **self.default_opts) - - assertVerifyCalled(mock_wrapper, - provider='test_provider', - provider_base_url='http://localhost:8888', - broker_password=self.broker_password, - broker_username=self.broker_username, - broker_token='token', - broker_url=self.broker_url, - log_level='INFO', - verbose=False, - provider_app_version='1.0.0', - enable_pending=False, - include_wip_pacts_since=None) - - @patch('pact.v2.verify_wrapper.VerifyWrapper.call_verify', return_value=(0, None)) - def test_passes_enable_pending_flag_value(self, mock_wrapper): - for value in (True, False): - with self.subTest(value=value): - with patch('pact.v2.verifier.path_exists'): - self.verifier.verify_with_broker(enable_pending=value) - self.assertTrue( - ('enable_pending', value) in mock_wrapper.call_args.kwargs.items(), - mock_wrapper.call_args.kwargs, - ) - - @patch('pact.v2.verify_wrapper.VerifyWrapper.call_verify', return_value=(0, None)) - @patch('pact.v2.verifier.path_exists', return_value=True) - def test_passes_include_wip_pacts_since_value(self, mock_path_exists, mock_wrapper): - self.verifier.verify_with_broker(include_wip_pacts_since='2018-01-01') - self.assertTrue( - ('include_wip_pacts_since', '2018-01-01') in mock_wrapper.call_args.kwargs.items(), - mock_wrapper.call_args.kwargs, - ) diff --git a/tests/v2/test_verify_wrapper.py b/tests/v2/test_verify_wrapper.py deleted file mode 100644 index f70189d41..000000000 --- a/tests/v2/test_verify_wrapper.py +++ /dev/null @@ -1,363 +0,0 @@ -import os -from unittest import TestCase - -from mock import patch, Mock, call - -from pact_cli import VERIFIER_PATH -from pact.v2.verify_wrapper import VerifyWrapper, PactException, path_exists, sanitize_logs, expand_directories, rerun_command -from pact.v2 import verify_wrapper - - -from subprocess import PIPE, Popen - -class VerifyWrapperTestCase(TestCase): - """ use traceback.print_exception(*result.exc_info) to debug """ - - @classmethod - def setUpClass(cls): - # In Python 3 Click makes a call to locale to determine how the - # terminal wants to handle unicode. Because we mock Popen to avoid - # calling the real verifier, we need to get the actual result of - # locale to provide it to Click during the test run. - if os.name == 'nt': - cls.locale = '' # pragma: no cover - else: - cls.locale = Popen( - ['locale', '-a'], stdout=PIPE, stderr=PIPE).communicate()[0] - - def setUp(self): - super(VerifyWrapperTestCase, self).setUp() - self.addCleanup(patch.stopall) - self.mock_Popen = patch.object( - verify_wrapper.subprocess, 'Popen', spec=verify_wrapper.subprocess.Popen, - stdout=['6 interactions, 0 failures']).start() - - self.mock_Popen.return_value.communicate.return_value = self.locale - - self.mock_rerun_command = patch.object( - verify_wrapper, 'rerun_command', autospec=True).start() - - self.default_call = [ - './pacts/consumer-provider.json', - './pacts/consumer-provider2.json', - '--no-enable-pending', - '--provider=test_provider', - '--provider-base-url=http://localhost'] - - self.broker_call = [ - '--provider=test_provider', - '--provider-base-url=http://localhost', - '--pact-broker-base-url=http://broker', - '--broker-username=username', - '--broker-password=pwd', - '--broker-token=token', - '--consumer-version-tag=prod', - '--consumer-version-tag=dev', - '--no-enable-pending', - '--provider-version-tag=dev', - '--provider-version-tag=qa', - '--provider-version-branch=provider-branch'] - - def assertProcess(self, *expected): - self.assertEqual(self.mock_Popen.call_count, 1) - process_call = self.mock_Popen.mock_calls[0] - - actual = process_call[1][0] - self.assertEqual(actual[0], VERIFIER_PATH) - self.assertEqual(len(actual), len(expected) + 1) - self.assertEqual(set(actual[1:]), set(expected)) - self.assertEqual(set(expected), set(actual[1:])) - self.assertEqual( - process_call[2]['env'], - self.mock_rerun_command.return_value) - self.assertTrue(self.mock_Popen.called) - - def test_pact_urls_or_broker_required(self): - self.mock_Popen.return_value.returncode = 2 - wrapper = VerifyWrapper() - - with self.assertRaises(PactException) as context: - wrapper.call_verify(provider='provider', provider_base_url='http://localhost') - - self.assertTrue('Pact urls or Pact broker required' in context.exception.message) - - def test_broker_without_authentication_can_be_used(self): - self.mock_Popen.return_value.returncode = 0 - wrapper = VerifyWrapper() - wrapper.call_verify( - provider='provider', provider_base_url='http://localhost', broker_url='http://broker.example.com' - ) - self.assertProcess(*[ - '--no-enable-pending', - '--pact-broker-base-url=http://broker.example.com', - '--provider-base-url=http://localhost', - '--provider=provider', - ]) - - def test_pact_urls_provided(self): - self.mock_Popen.return_value.returncode = 0 - wrapper = VerifyWrapper() - - result, output = wrapper.call_verify('./pacts/consumer-provider.json', - './pacts/consumer-provider2.json', - provider='test_provider', - provider_base_url='http://localhost') - - self.assertProcess(*self.default_call) - self.assertEqual(result, 0) - - @patch("pact.v2.verify_wrapper.isfile", return_value=True) - def test_all_url_options(self, mock_isfile): - self.mock_Popen.return_value.returncode = 0 - wrapper = VerifyWrapper() - - result, output = wrapper.call_verify('./pacts/consumer-provider5.json', - './pacts/consumer-provider3.json', - provider_base_url='http://localhost', - provider_states_setup_url='http://localhost/provider-states/set', - provider='provider', - provider_app_version='1.2.3', - custom_provider_headers=['Authorization: Basic cGFj', 'CustomHeader: somevalue'], - log_dir='tmp/logs/pact.test.log', - log_level='INFO', - timeout=60, - verbose=True, - enable_pending=True, - include_wip_pacts_since='2018-01-01') - - self.assertEqual(result, 0) - self.mock_Popen.return_value.wait.assert_called_once_with() - self.assertEqual(self.mock_Popen.call_count, 1) - self.assertProcess( - './pacts/consumer-provider5.json', - './pacts/consumer-provider3.json', - '--custom-provider-header=Authorization: Basic cGFj', - '--custom-provider-header=CustomHeader: somevalue', - '--provider-base-url=http://localhost', - '--provider-states-setup-url=http://localhost/provider-states/set', - '--provider=provider', - '--provider-app-version', '1.2.3', - '--log-dir=tmp/logs/pact.test.log', - '--log-level=INFO', - '--verbose', - '--enable-pending', - '--include-wip-pacts-since=2018-01-01', - ) - - def test_uses_broker_if_no_pacts_and_provider_required(self): - self.mock_Popen.return_value.returncode = 0 - wrapper = VerifyWrapper() - - result, output = wrapper.call_verify(provider='test_provider', - provider_base_url='http://localhost', - broker_username='username', - broker_password='pwd', - broker_token='token', - broker_url='http://broker', - consumer_tags=['prod', 'dev'], - provider_tags=['dev', 'qa'], - provider_version_branch='provider-branch') - - self.assertProcess(*self.broker_call) - self.assertEqual(result, 0) - - @patch('pact.v2.verify_wrapper.path_exists', return_value=True) - @patch('pact.v2.verify_wrapper.sanitize_logs') - @patch('pact.v2.verify_wrapper.expand_directories', return_value='./pacts/consumer-provider.json') - @patch('pact.v2.verify_wrapper.rerun_command') - def test_rerun_command_called(self, mock_rerun_cmd, mock_expand_dirs, mock_sanitize_logs, mock_path_exists): - self.mock_Popen.return_value.returncode = 0 - wrapper = VerifyWrapper() - - result, output = wrapper.call_verify('path/to/pact1', - 'path/to/pact2', - provider_base_url='http://localhost', - provider='provider') - - mock_rerun_cmd.assert_called_once() - - @patch('pact.v2.verify_wrapper.path_exists', return_value=True) - @patch('pact.v2.verify_wrapper.sanitize_logs') - @patch('pact.v2.verify_wrapper.expand_directories', return_value='./pacts/consumer-provider.json') - @patch('pact.v2.verify_wrapper.rerun_command') - def test_sanitize_called(self, mock_rerun_cmd, mock_expand_dirs, mock_sanitize_logs, mock_path_exists): - self.mock_Popen.return_value.returncode = 0 - wrapper = VerifyWrapper() - - result, output = wrapper.call_verify('path/to/pact1', - 'path/to/pact2', - provider_base_url='http://localhost', - provider='provider') - - mock_sanitize_logs.assert_called_with(self.mock_Popen.return_value, False) - - @patch('pact.v2.verify_wrapper.path_exists', return_value=True) - @patch('pact.v2.verify_wrapper.sanitize_logs') - def test_publishing_with_version(self, mock_sanitize_logs, mock_path_exists): - self.mock_Popen.return_value.returncode = 0 - wrapper = VerifyWrapper() - - result, output = wrapper.call_verify('./pacts/consumer-provider.json', - './pacts/consumer-provider2.json', - provider='test_provider', - provider_base_url='http://localhost', - provider_app_version='1.2.3', - publish_verification_results=True) - - self.default_call.extend(['--provider-app-version', '1.2.3', '--publish-verification-results']) - - self.assertProcess(*self.default_call) - self.assertEqual(result, 0) - - @patch('pact.v2.verify_wrapper.path_exists', return_value=True) - @patch('pact.v2.verify_wrapper.sanitize_logs') - @patch('pact.v2.verify_wrapper.expand_directories', return_value='./pacts/consumer-provider.json') - @patch('pact.v2.verify_wrapper.rerun_command') - def test_expand_dirs_called(self, mock_rerun_cmd, mock_expand_dirs, mock_sanitize_logs, mock_path_exists): - self.mock_Popen.return_value.returncode = 0 - wrapper = VerifyWrapper() - - result, output = wrapper.call_verify('path/to/pact1', - 'path/to/pact2', - provider_base_url='http://localhost', - provider='provider') - - mock_expand_dirs.assert_called_with(['path/to/pact1', - 'path/to/pact2']) - - -class path_existsTestCase(TestCase): - def test_path_exists(self): - self.assertTrue(path_exists('README.md')) - - def test_path_not_exists(self): - self.assertFalse(path_exists('not_a_real_file')) - - def test_path_exists_valid_for_http(self): - self.assertTrue(path_exists('http://someurl')) - - def test_path_exists_valid_for_https(self): - self.assertTrue(path_exists('https://someurl')) - - -class sanitize_logsTestCase(TestCase): - def setUp(self): - self.addCleanup(patch.stopall) - self.process = Mock(Popen, stdout=[ - 'Actual: {"username":123,"id":"100","groups":["users","admins"]}', - '# /Users/auser/dev/pact-python/pact/bin/pact/lib/vendor' - '/ruby/2.2.0/gems/pact-provider-verifier-1.6.0/lib/pact/provider' - '_verifier/cli/custom_thor.rb:17:in `start\'', - '# /Users/auser/dev/pact-python/pact/bin/pact/lib/app' - '/pact-provider-verifier.rb:2:in `
\'' - ]) - - @patch('pact.v2.verify_wrapper.sys.stdout.write') - def test_verbose(self, mock_write): - sanitize_logs(self.process, True) - mock_write.assert_has_calls([ - call('Actual: {"username":123,"id":"100","groups":' - '["users","admins"]}'), - call('# /Users/auser/dev/pact-python/pact/bin/pact/lib' - '/vendor/ruby/2.2.0/gems/pact-provider-verifier-1.6.0/lib' - '/pact/provider_verifier/cli/custom_thor.rb:17:in `start\''), - call('# /Users/auser/dev/pact-python/pact/bin/pact/lib' - '/app/pact-provider-verifier.rb:2:in `
\'') - ]) - - @patch('pact.v2.verify_wrapper.sys.stdout.write') - def test_terse(self, mock_write): - sanitize_logs(self.process, False) - mock_write.assert_called_once_with( - 'Actual: {"username":123,"id":"100","groups":["users","admins"]}') - - -class expand_directoriesTestCase(TestCase): - def setUp(self): - super(expand_directoriesTestCase, self).setUp() - - def posix_join(*args): - return '/'.join(args) - - self.addCleanup(patch.stopall) - self.mock_isdir = patch.object(verify_wrapper, 'isdir', autospec=True).start() - self.mock_join = patch.object( - verify_wrapper, 'join', new=posix_join).start() - self.mock_listdir = patch.object( - verify_wrapper, 'listdir', autospec=True).start() - - def test_directory(self): - self.mock_isdir.return_value = True - self.mock_listdir.return_value = [ - 'consumer-provider.json', - 'consumer2-provider.json', - 'unrelated-file.txt'] - - result = expand_directories(['/tmp']) - self.assertEqual(result, [ - '/tmp/consumer-provider.json', - '/tmp/consumer2-provider.json', - ]) - - def test_file(self): - self.mock_isdir.return_value = False - result = expand_directories(['/tmp/consumer-provider.json']) - self.assertEqual(result, ['/tmp/consumer-provider.json']) - - def test_uri(self): - result = expand_directories(['http://broker']) - self.assertEqual(result, ['http://broker']) - - def test_windows_directories(self): - self.mock_isdir.return_value = True - self.mock_listdir.return_value = [ - 'consumer-provider.json', - 'consumer2-provider.json', - 'unrelated-file.txt'] - - result = expand_directories(['C:\\tmp']) - self.assertEqual(result, [ - 'C:/tmp/consumer-provider.json', - 'C:/tmp/consumer2-provider.json', - ]) - - -class rerun_commandTestCase(TestCase): - def setUp(self): - self.addCleanup(patch.stopall) - self.mock_platform = patch.object( - verify_wrapper.platform, 'platform', autospec=True).start() - - @patch.object(verify_wrapper.sys, 'argv', new=[ - 'pact-verifier', '--pact-url=./consumer-provider.json']) - def test_posix(self): - self.mock_platform.return_value = 'linux' - result = rerun_command() - self.assertEqual( - result['PACT_INTERACTION_RERUN_COMMAND'], - "PACT_DESCRIPTION=''" - " PACT_PROVIDER_STATE=''" - " pact-verifier --pact-url=./consumer-provider.json") - - @patch.object(verify_wrapper.sys, 'argv', new=[ - 'pact-verifier.exe', '--pact-url=./consumer-provider.json']) - def test_windows(self): - self.mock_platform.return_value = 'Windows' - result = rerun_command() - self.assertEqual( - result['PACT_INTERACTION_RERUN_COMMAND'], - "cmd.exe /v /c \"" - "set PACT_DESCRIPTION=" - "& set PACT_PROVIDER_STATE=" - "& pact-verifier.exe" - " --pact-url=./consumer-provider.json" - " & set PACT_DESCRIPTION=" - " & set PACT_PROVIDER_STATE=\"") - - @patch('pact.v2.verify_wrapper.os.environ') - def test_env_copied(self, mock_env): - mock_env.copy.return_value = {'foo': 'bar'} - self.mock_platform.return_value = 'linux' - - result = rerun_command() - self.assertEqual(result['foo'], 'bar')