From a8bb28a70be8bdb042eb81686c2add68e238b703 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 1 Mar 2024 10:36:02 +1100 Subject: [PATCH 1/3] chore(ffi): implement verifier handle Signed-off-by: JP-Ellis --- src/pact/v3/ffi.py | 88 ++++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 45 deletions(-) diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index 52cfbc5e2f..c6893c7549 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -528,7 +528,40 @@ class SynchronousHttp: ... class SynchronousMessage: ... -class VerifierHandle: ... +class VerifierHandle: + """ + Handle to a Verifier. + + [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/verifier/handle/struct.VerifierHandle.html) + """ + + def __init__(self, ref: int) -> None: + """ + Initialise a new Verifier Handle. + + Args: + ref: + Rust library reference to the Verifier Handle. + """ + self._ref: int = ref + + def __del__(self) -> None: + """ + Destructor for the Verifier Handle. + """ + verifier_shutdown(self) + + def __str__(self) -> str: + """ + String representation of the Verifier Handle. + """ + return f"PactHandle({self._ref})" + + def __repr__(self) -> str: + """ + String representation of the Verifier Handle. + """ + return f"PactHandle({self._ref!r})" class ExpressionValueType(Enum): @@ -6328,55 +6361,20 @@ def verify(args: str) -> int: raise NotImplementedError -def verifier_new() -> VerifierHandle: - """ - Get a Handle to a newly created verifier. - - You should call `pactffi_verifier_shutdown` when done with the verifier to - free all allocated resources. - - [Rust - `pactffi_verifier_new`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_new) - - Deprecated: This function is deprecated. Use - `pactffi_verifier_new_for_application` which allows the calling - application/framework name and version to be specified. - - # Safety - - This function is safe. - - # Error Handling - - Returns NULL on error. - """ - warnings.warn( - "This function is deprecated, use verifier_new_for_application instead", - DeprecationWarning, - stacklevel=2, - ) - raise NotImplementedError - - -def verifier_new_for_application(name: str, version: str) -> VerifierHandle: +def verifier_new_for_application() -> VerifierHandle: """ Get a Handle to a newly created verifier. - You should call `pactffi_verifier_shutdown` when done with the verifier to - free all allocated resources - [Rust `pactffi_verifier_new_for_application`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_new_for_application) - - # Safety - - This function is safe. - - # Error Handling - - Returns NULL on error. """ - raise NotImplementedError + from pact import __version__ + + result: int = lib.pactffi_verifier_new_for_application( + b"pact-python", + __version__.encode("utf-8"), + ) + return VerifierHandle(result) def verifier_shutdown(handle: VerifierHandle) -> None: @@ -6385,7 +6383,7 @@ def verifier_shutdown(handle: VerifierHandle) -> None: [Rust `pactffi_verifier_shutdown`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_shutdown) """ - raise NotImplementedError + lib.pactffi_verifier_shutdown(handle._ref) def verifier_set_provider_info( # noqa: PLR0913 From 8919f5bb4f943d65ad687ce2f037bcb043706e35 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Tue, 5 Mar 2024 10:05:58 +1100 Subject: [PATCH 2/3] feat(v3): add verifier class Signed-off-by: JP-Ellis --- src/pact/v3/__init__.py | 2 + src/pact/v3/ffi.py | 552 +++++++++++++++---------- src/pact/v3/verifier.py | 881 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1229 insertions(+), 206 deletions(-) create mode 100644 src/pact/v3/verifier.py diff --git a/src/pact/v3/__init__.py b/src/pact/v3/__init__.py index 20701bd7ff..826a0d7906 100644 --- a/src/pact/v3/__init__.py +++ b/src/pact/v3/__init__.py @@ -23,9 +23,11 @@ import warnings from pact.v3.pact import Pact +from pact.v3.verifier import Verifier __all__ = [ "Pact", + "Verifier", ] warnings.warn( diff --git a/src/pact/v3/ffi.py b/src/pact/v3/ffi.py index c6893c7549..e809e173db 100644 --- a/src/pact/v3/ffi.py +++ b/src/pact/v3/ffi.py @@ -83,6 +83,7 @@ import gc import json +import logging import typing import warnings from enum import Enum @@ -91,11 +92,15 @@ from pact.v3._ffi import ffi, lib # type: ignore[import] if TYPE_CHECKING: + import datetime + from collections.abc import Collection from pathlib import Path import cffi from typing_extensions import Self +logger = logging.getLogger(__name__) + # The follow types are classes defined in the Rust code. Ultimately, a Python # alternative should be implemented, but for now, the follow lines only serve # to inform the type checker of the existence of these types. @@ -535,7 +540,7 @@ class VerifierHandle: [Rust `VerifierHandle`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/verifier/handle/struct.VerifierHandle.html) """ - def __init__(self, ref: int) -> None: + def __init__(self, ref: cffi.FFI.CData) -> None: """ Initialise a new Verifier Handle. @@ -543,7 +548,7 @@ def __init__(self, ref: int) -> None: ref: Rust library reference to the Verifier Handle. """ - self._ref: int = ref + self._ref = ref def __del__(self) -> None: """ @@ -555,13 +560,13 @@ def __str__(self) -> str: """ String representation of the Verifier Handle. """ - return f"PactHandle({self._ref})" + return f"VerifierHandle({hex(id(self._ref))})" def __repr__(self) -> str: """ String representation of the Verifier Handle. """ - return f"PactHandle({self._ref!r})" + return f"" class ExpressionValueType(Enum): @@ -6370,7 +6375,7 @@ def verifier_new_for_application() -> VerifierHandle: """ from pact import __version__ - result: int = lib.pactffi_verifier_new_for_application( + result: cffi.FFI.CData = lib.pactffi_verifier_new_for_application( b"pact-python", __version__.encode("utf-8"), ) @@ -6388,61 +6393,98 @@ def verifier_shutdown(handle: VerifierHandle) -> None: def verifier_set_provider_info( # noqa: PLR0913 handle: VerifierHandle, - name: str, - scheme: str, - host: str, - port: int, - path: str, + name: str | None, + scheme: str | None, + host: str | None, + port: int | None, + path: str | None, ) -> None: """ Set the provider details for the Pact verifier. - Passing a NULL for any field will use the default value for that field. - [Rust `pactffi_verifier_set_provider_info`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_provider_info) - # Safety + Args: + handle: + The verifier handle to update. - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. + name: + A user-friendly name to describe the provider. + + scheme: + Determine the scheme to use, typically one of `HTTP` or `HTTPS`. + + host: + The host of the provider. This may be either a hostname to resolve, + or an IP address. + + port: + The port of the provider. + + path: + The path of the provider. + + If any value is `None`, the default value as determined by the underlying + FFI library will be used. """ - raise NotImplementedError + lib.pactffi_verifier_set_provider_info( + handle._ref, + name.encode("utf-8") if name else ffi.NULL, + scheme.encode("utf-8") if scheme else ffi.NULL, + host.encode("utf-8") if host else ffi.NULL, + port, + path.encode("utf-8") if path else ffi.NULL, + ) def verifier_add_provider_transport( handle: VerifierHandle, - protocol: str, + protocol: str | None, port: int, - path: str, - scheme: str, + path: str | None, + scheme: str | None, ) -> None: """ Adds a new transport for the given provider. - Passing a NULL for any field will use the default value for that field. - [Rust `pactffi_verifier_add_provider_transport`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_add_provider_transport) - For non-plugin based message interactions, set protocol to "message" and set - scheme to an empty string or "https" if secure HTTP is required. - Communication to the calling application will be over HTTP to the default - provider hostname. + Args: + handle: + The verifier handle to update. - # Safety + protocol: + In this context, the kind of - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. + port: + The port of the provider. + + path: + The path of the provider. + + scheme: + The scheme to use, typically one of `HTTP` or `HTTPS`. + + If any value is `None`, the default value as determined by the underlying + FFI library will be used. """ - raise NotImplementedError + lib.pactffi_verifier_add_provider_transport( + handle._ref, + protocol.encode("utf-8") if protocol else ffi.NULL, + port, + path.encode("utf-8") if path else ffi.NULL, + scheme.encode("utf-8") if scheme else ffi.NULL, + ) def verifier_set_filter_info( handle: VerifierHandle, - filter_description: str, - filter_state: str, - filter_no_state: int, + filter_description: str | None, + filter_state: str | None, + *, + filter_no_state: bool, ) -> None: """ Set the filters for the Pact verifier. @@ -6450,26 +6492,35 @@ def verifier_set_filter_info( [Rust `pactffi_verifier_set_filter_info`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_filter_info) - If `filter_description` is not empty, it needs to be as a regular - expression. + Set filters to narrow down the interactions to verify. - `filter_no_state` is a boolean value. Set it to greater than zero to turn - the option on. + Args: + handle: + The verifier handle to update. - # Safety + filter_description: + A regular expression to filter the interactions by description. - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. + filter_state: + A regular expression to filter the interactions by state. + filter_no_state: + If `True`, the option to filter by state will be turned on. """ - raise NotImplementedError + lib.pactffi_verifier_set_filter_info( + handle._ref, + filter_description.encode("utf-8") if filter_description else ffi.NULL, + filter_state.encode("utf-8") if filter_state else ffi.NULL, + filter_no_state, + ) def verifier_set_provider_state( handle: VerifierHandle, url: str, - teardown: int, - body: int, + *, + teardown: bool, + body: bool, ) -> None: """ Set the provider state URL for the Pact verifier. @@ -6477,134 +6528,170 @@ def verifier_set_provider_state( [Rust `pactffi_verifier_set_provider_state`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_provider_state) - `teardown` is a boolean value. If teardown state change requests should be - made after an interaction is validated (default is false). Set it to greater - than zero to turn the option on. `body` is a boolean value. Sets if state - change request data should be sent in the body (> 0, true) or as query - parameters (== 0, false). Set it to greater than zero to turn the option on. + Args: + handle: + The verifier handle to update. - # Safety + url: + The URL to use for the provider state. - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. + teardown: + If teardown state change requests should be made after an + interaction is validated. - """ - raise NotImplementedError + body: + If state change request data should be sent in the body or the + query. + """ + lib.pactffi_verifier_set_provider_state( + handle._ref, + url.encode("utf-8"), + teardown, + body, + ) def verifier_set_verification_options( handle: VerifierHandle, - disable_ssl_verification: int, + *, + disable_ssl_verification: bool, request_timeout: int, -) -> int: +) -> None: """ Set the options used by the verifier when calling the provider. [Rust `pactffi_verifier_set_verification_options`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_verification_options) - `disable_ssl_verification` is a boolean value. Set it to greater than zero - to turn the option on. - - # Safety + Args: + handle: + The verifier handle to update. - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. + disable_ssl_verification: + If SSL verification should be disabled. + request_timeout: + The timeout for the request in milliseconds. """ - raise NotImplementedError + retval: int = lib.pactffi_verifier_set_verification_options( + handle._ref, + disable_ssl_verification, + request_timeout, + ) + if retval != 0: + msg = f"Failed to set verification options for {handle}." + raise RuntimeError(msg) -def verifier_set_coloured_output(handle: VerifierHandle, coloured_output: int) -> int: +def verifier_set_coloured_output( + handle: VerifierHandle, + *, + enabled: bool, +) -> None: """ Enables or disables coloured output using ANSI escape codes. - By default, coloured output is enabled. - [Rust `pactffi_verifier_set_coloured_output`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_coloured_output) - `coloured_output` is a boolean value. Set it to greater than zero to turn - the option on. - - # Safety + By default, coloured output is enabled. - This function is safe as long as the handle pointer points to a valid - handle. + Args: + handle: + The verifier handle to update. + enabled: + A boolean value to enable or disable coloured output. """ - raise NotImplementedError + retval: int = lib.pactffi_verifier_set_coloured_output( + handle._ref, + enabled, + ) + if retval != 0: + msg = f"Failed to set coloured output for {handle}." + raise RuntimeError(msg) -def verifier_set_no_pacts_is_error(handle: VerifierHandle, is_error: int) -> int: +def verifier_set_no_pacts_is_error(handle: VerifierHandle, *, enabled: bool) -> None: """ Enables or disables if no pacts are found to verify results in an error. [Rust `pactffi_verifier_set_no_pacts_is_error`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_no_pacts_is_error) - `is_error` is a boolean value. Set it to greater than zero to enable an - error when no pacts are found to verify, and set it to zero to disable this. - - # Safety - - This function is safe as long as the handle pointer points to a valid - handle. + Args: + handle: + The verifier handle to update. + enabled: + If `True`, an error will be raised when no pacts are found to verify. """ - raise NotImplementedError + retval: int = lib.pactffi_verifier_set_no_pacts_is_error( + handle._ref, + enabled, + ) + if retval != 0: + msg = f"Failed to set no pacts is error for {handle}." + raise RuntimeError(msg) -def verifier_set_publish_options( # noqa: PLR0913 +def verifier_set_publish_options( handle: VerifierHandle, provider_version: str, build_url: str, provider_tags: List[str], - provider_tags_len: int, provider_branch: str, -) -> int: +) -> None: """ Set the options used when publishing verification results to the Broker. [Rust `pactffi_verifier_set_publish_options`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_publish_options) - # Args + Args: + handle: + The verifier handle to update. - - `handle` - The pact verifier handle to update - - `provider_version` - Version of the provider to publish - - `build_url` - URL to the build which ran the verification - - `provider_tags` - Collection of tags for the provider - - `provider_tags_len` - Number of provider tags supplied - - `provider_branch` - Name of the branch used for verification + provider_version: + Version of the provider to publish. - # Safety + build_url: + URL to the build which ran the verification. - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. + provider_tags: + Collection of tags for the provider. + provider_branch: + Name of the branch used for verification. """ - raise NotImplementedError + retval: int = lib.pactffi_verifier_set_publish_options( + handle._ref, + provider_version.encode("utf-8"), + build_url.encode("utf-8"), + [ffi.new("char[]", t.encode("utf-8")) for t in provider_tags or []], + len(provider_tags), + provider_branch.encode("utf-8"), + ) + if retval != 0: + msg = f"Failed to set publish options for {handle}." + raise RuntimeError(msg) def verifier_set_consumer_filters( handle: VerifierHandle, - consumer_filters: List[str], - consumer_filters_len: int, + consumer_filters: Collection[str], ) -> None: """ Set the consumer filters for the Pact verifier. [Rust `pactffi_verifier_set_consumer_filters`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_set_consumer_filters) - - # Safety - - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. - """ - raise NotImplementedError + lib.pactffi_verifier_set_consumer_filters( + handle._ref, + [ffi.new("char[]", f.encode("utf-8")) for f in consumer_filters], + len(consumer_filters), + ) def verifier_add_custom_header( @@ -6617,13 +6704,12 @@ def verifier_add_custom_header( [Rust `pactffi_verifier_add_custom_header`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_add_custom_header) - - # Safety - - The header name and value must point to a valid NULL terminated string and - must contain valid UTF-8. """ - raise NotImplementedError + lib.pactffi_verifier_add_custom_header( + handle._ref, + header_name.encode("utf-8"), + header_value.encode("utf-8"), + ) def verifier_add_file_source(handle: VerifierHandle, file: str) -> None: @@ -6632,14 +6718,8 @@ def verifier_add_file_source(handle: VerifierHandle, file: str) -> None: [Rust `pactffi_verifier_add_file_source`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_add_file_source) - - # Safety - - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. - """ - raise NotImplementedError + lib.pactffi_verifier_add_file_source(handle._ref, file.encode("utf-8")) def verifier_add_directory_source(handle: VerifierHandle, directory: str) -> None: @@ -6657,124 +6737,179 @@ def verifier_add_directory_source(handle: VerifierHandle, directory: str) -> Non with U+FFFD REPLACEMENT CHARACTER. """ - raise NotImplementedError + lib.pactffi_verifier_add_directory_source(handle._ref, directory.encode("utf-8")) def verifier_url_source( handle: VerifierHandle, url: str, - username: str, - password: str, - token: str, + username: str | None, + password: str | None, + token: str | None, ) -> None: """ Adds a URL as a source to verify. - The Pact file will be fetched from the URL. - [Rust `pactffi_verifier_url_source`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_url_source) - If a username and password is given, then basic authentication will be used - when fetching the pact file. If a token is provided, then bearer token - authentication will be used. + Args: + handle: + The verifier handle to update. - # Safety + url: + The URL to use as a source for the verifier. - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. + username: + The username to use when fetching pacts from the URL. + password: + The password to use when fetching pacts from the URL. + + token: + The token to use when fetching pacts from the URL. This will be used + as a bearer token. It is mutually exclusive with the username and + password. """ - raise NotImplementedError + lib.pactffi_verifier_url_source( + handle._ref, + url.encode("utf-8"), + username.encode("utf-8") if username else ffi.NULL, + password.encode("utf-8") if password else ffi.NULL, + token.encode("utf-8") if token else ffi.NULL, + ) def verifier_broker_source( handle: VerifierHandle, url: str, - username: str, - password: str, - token: str, + username: str | None, + password: str | None, + token: str | None, ) -> None: """ Adds a Pact broker as a source to verify. + [Rust + `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_broker_source) + This will fetch all the pact files from the broker that match the provider name. - [Rust - `pactffi_verifier_broker_source`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_broker_source) + Args: + handle: + The verifier handle to update. - If a username and password is given, then basic authentication will be used - when fetching the pact file. If a token is provided, then bearer token - authentication will be used. + url: + The URL to use as a source for the verifier. - # Safety + username: + The username to use when fetching pacts from the broker. - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. + password: + The password to use when fetching pacts from the broker. + token: + The token to use when fetching pacts from the broker. This will be + used as a bearer token. """ - raise NotImplementedError + lib.pactffi_verifier_broker_source( + handle._ref, + url.encode("utf-8"), + username.encode("utf-8") if username else ffi.NULL, + password.encode("utf-8") if password else ffi.NULL, + token.encode("utf-8") if token else ffi.NULL, + ) def verifier_broker_source_with_selectors( # noqa: PLR0913 handle: VerifierHandle, url: str, - username: str, - password: str, - token: str, + username: str | None, + password: str | None, + token: str | None, enable_pending: int, - include_wip_pacts_since: str, + include_wip_pacts_since: datetime.date | None, provider_tags: List[str], - provider_tags_len: int, - provider_branch: str, + provider_branch: str | None, consumer_version_selectors: List[str], - consumer_version_selectors_len: int, consumer_version_tags: List[str], - consumer_version_tags_len: int, ) -> None: """ Adds a Pact broker as a source to verify. - This will fetch all the pact files from the broker that match the provider - name and the consumer version selectors (See - `https://docs.pact.io/pact_broker/advanced_topics/consumer_version_selectors/`). - [Rust `pactffi_verifier_broker_source_with_selectors`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_broker_source_with_selectors) - The consumer version selectors must be passed in in JSON format. - - `enable_pending` is a boolean value. Set it to greater than zero to turn the - option on. - - If the `include_wip_pacts_since` option is provided, it needs to be a date - formatted in ISO format (YYYY-MM-DD). - - If a username and password is given, then basic authentication will be used - when fetching the pact file. If a token is provided, then bearer token - authentication will be used. - - # Safety - - All string fields must contain valid UTF-8. Invalid UTF-8 will be replaced - with U+FFFD REPLACEMENT CHARACTER. + This will fetch all the pact files from the broker that match the provider + name and the consumer version selectors (See [Consumer Version + Selectors](https://docs.pact.io/pact_broker/advanced_topics/consumer_version_selectors/)). - """ - raise NotImplementedError + Args: + handle: + The verifier handle to update. + + url: + The URL to use as a source for the verifier. + + username: + The username to use when fetching pacts from the broker. + + password: + The password to use when fetching pacts from the broker. + + token: + The token to use when fetching pacts from the broker. This will be + used as a bearer token. + + enable_pending: + If pending pacts should be included in the verification process. + + include_wip_pacts_since: + The date to use to filter out WIP pacts. + + provider_tags: + The tags to use to filter the provider pacts. + + provider_branch: + The branch to use to filter the provider pacts. + + consumer_version_selectors: + The consumer version selectors to use to filter the consumer pacts. + + consumer_version_tags: + The tags to use to filter the consumer pacts. + """ + lib.pactffi_verifier_broker_source_with_selectors( + handle._ref, + url.encode("utf-8"), + username.encode("utf-8") if username else ffi.NULL, + password.encode("utf-8") if password else ffi.NULL, + token.encode("utf-8") if token else ffi.NULL, + enable_pending, + include_wip_pacts_since.isoformat().encode("utf-8") + if include_wip_pacts_since + else ffi.NULL, + [ffi.new("char[]", t.encode("utf-8")) for t in provider_tags], + len(provider_tags), + provider_branch.encode("utf-8") if provider_branch else ffi.NULL, + [ffi.new("char[]", s.encode("utf-8")) for s in consumer_version_selectors], + len(consumer_version_selectors), + [ffi.new("char[]", t.encode("utf-8")) for t in consumer_version_tags], + len(consumer_version_tags), + ) -def verifier_execute(handle: VerifierHandle) -> int: +def verifier_execute(handle: VerifierHandle) -> None: """ Runs the verification. - [Rust `pactffi_verifier_execute`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_execute) - - # Error Handling - - Errors will be reported with a non-zero return value. + (https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_execute) """ - raise NotImplementedError + success: int = lib.pactffi_verifier_execute(handle._ref) + if success != 0: + msg = f"Failed to execute verifier for {handle}." + raise RuntimeError(msg) def verifier_cli_args() -> str: @@ -6840,68 +6975,73 @@ def verifier_logs(handle: VerifierHandle) -> OwnedString: """ Extracts the logs for the verification run. - This needs the memory buffer log sink to be setup before the verification is - executed. The returned string will need to be freed with the `free_string` - function call to avoid leaking memory. - [Rust `pactffi_verifier_logs`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_logs) - Will return a NULL pointer if the logs for the verification can not be - retrieved. + This needs the memory buffer log sink to be setup before the verification is + executed. The returned string will need to be freed with the `free_string` + function call to avoid leaking memory. """ - raise NotImplementedError + ptr = lib.pactffi_verifier_logs(handle._ref) + if ptr == ffi.NULL: + msg = f"Failed to get logs for {handle}." + raise RuntimeError(msg) + return OwnedString(ptr) def verifier_logs_for_provider(provider_name: str) -> OwnedString: """ Extracts the logs for the verification run for the provider name. - This needs the memory buffer log sink to be setup before the verification is - executed. The returned string will need to be freed with the `free_string` - function call to avoid leaking memory. - [Rust `pactffi_verifier_logs_for_provider`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_logs_for_provider) - Will return a NULL pointer if the logs for the verification can not be - retrieved. + This needs the memory buffer log sink to be setup before the verification is + executed. The returned string will need to be freed with the `free_string` + function call to avoid leaking memory. """ - raise NotImplementedError + ptr = lib.pactffi_verifier_logs_for_provider(provider_name.encode("utf-8")) + if ptr == ffi.NULL: + msg = f"Failed to get logs for {provider_name}." + raise RuntimeError(msg) + return OwnedString(ptr) def verifier_output(handle: VerifierHandle, strip_ansi: int) -> OwnedString: """ Extracts the standard output for the verification run. - The returned string will need to be freed with the `free_string` function - call to avoid leaking memory. - [Rust `pactffi_verifier_output`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_output) - * `strip_ansi` - This parameter controls ANSI escape codes. Setting it to a - non-zero value - will cause the ANSI control codes to be stripped from the output. + Args: + handle: + The verifier handle to update. - Will return a NULL pointer if the handle is invalid. + strip_ansi: + This parameter controls ANSI escape codes. Setting it to a non-zero + value will cause the ANSI control codes to be stripped from the + output. """ - raise NotImplementedError + ptr = lib.pactffi_verifier_output(handle._ref, strip_ansi) + if ptr == ffi.NULL: + msg = f"Failed to get output for {handle}." + raise RuntimeError(msg) + return OwnedString(ptr) def verifier_json(handle: VerifierHandle) -> OwnedString: """ Extracts the verification result as a JSON document. - The returned string will need to be freed with the `free_string` function - call to avoid leaking memory. - [Rust `pactffi_verifier_json`](https://docs.rs/pact_ffi/0.4.18/pact_ffi/?search=pactffi_verifier_json) - - Will return a NULL pointer if the handle is invalid. """ - raise NotImplementedError + ptr = lib.pactffi_verifier_json(handle._ref) + if ptr == ffi.NULL: + msg = f"Failed to get JSON for {handle}." + raise RuntimeError(msg) + return OwnedString(ptr) def using_plugin( diff --git a/src/pact/v3/verifier.py b/src/pact/v3/verifier.py new file mode 100644 index 0000000000..c6cf8f4364 --- /dev/null +++ b/src/pact/v3/verifier.py @@ -0,0 +1,881 @@ +""" +Verifier for Pact. + +The Verifier is used to verify that a provider meets the expectations of a +consumer. This is done by replaying interactions from the consumer against the +provider, and ensuring that the provider's responses match the expectations set +by the consumer. +""" + +from __future__ import annotations + +import json +from datetime import date +from pathlib import Path +from typing import TYPE_CHECKING, Any, Literal, overload + +from typing_extensions import Self +from yarl import URL + +import pact.v3.ffi + +if TYPE_CHECKING: + from collections.abc import Iterable + + +class Verifier: + """ + A Verifier between a consumer and a provider. + + This class encapsulates the logic for verifying that a provider meets the + expectations of a consumer. This is done by replaying interactions from the + consumer against the provider, and ensuring that the provider's responses + match the expectations set by the consumer. + """ + + def __init__(self) -> None: + """ + Create a new Verifier. + """ + self._handle: pact.v3.ffi.VerifierHandle = ( + pact.v3.ffi.verifier_new_for_application() + ) + + # In order to provide a fluent interface, we remember some options which + # are set using the same FFI method. + self._disable_ssl_verification = False + self._request_timeout = 5000 + + def __str__(self) -> str: + """ + Informal string representation of the Verifier. + """ + return "Verifier" + + def __repr__(self) -> str: + """ + Information-rish string representation of the Verifier. + """ + return f"" + + def set_info( # noqa: PLR0913 + self, + name: str, + *, + url: str | URL | None = None, + scheme: str | None = None, + host: str | None = None, + port: int | None = None, + path: str | None = None, + ) -> Self: + """ + Set the provider information. + + This sets up information about the provider as well as the way it + communicates with the consumer. Note that for historical reasons, a + HTTP(S) transport method is always added. + + For a provider which uses other protocols (such as message queues), the + [`add_provider_transport`][pact.v3.verifier.Verifier.add_provider_transport] + must be used. This method can be called multiple times to add multiple + transport methods. + + Args: + name: + A user-friendly name for the provider. + + url: + The URL on which requests are made to the provider by Pact. + + It is recommended to use this parameter to set the provider URL. + If the port is not explicitly set, the default port for the + scheme will be used. + + This parameter is mutually exclusive with the individual + parameters. + + scheme: + The provider scheme. This must be one of `http` or `https`. + + host: + The provider hostname or IP address. If the provider is running + on the same machine as the verifier, `localhost` can be used. + + port: + The provider port. If not specified, the default port for the + schema will be used. + + path: + The provider context path. If not specified, the root path will + be used. + + If a non-root path is used, the path given here will be + prepended to the path in the interaction. For example, if the + path is `/api`, and the interaction path is `/users`, the + request will be made to `/api/users`. + """ + if url is not None: + if any(param is not None for param in (scheme, host, port, path)): + msg = "Cannot specify both `url` and individual parameters" + raise ValueError(msg) + + url = URL(url) + scheme = url.scheme + host = url.host + port = url.explicit_port + path = url.path + + if port is None: + msg = "Unable to determine default port for scheme {scheme}" + raise ValueError(msg) + + pact.v3.ffi.verifier_set_provider_info( + self._handle, + name, + scheme, + host, + port, + path, + ) + return self + + url = URL.build( + scheme=scheme or "http", + host=host or "localhost", + port=port, + path=path or "", + ) + return self.set_info(name, url=url) + + def add_transport( + self, + *, + protocol: str, + port: int | None = None, + path: str | None = None, + scheme: str | None = None, + ) -> Self: + """ + Add a provider transport method. + + If the provider supports multiple transport methods, or non-HTTP(S) + methods, this method allows these additional transport methods to be + added. It can be called multiple times to add multiple transport methods. + + As some transport methods may not use ports, paths or schemes, these + parameters are optional. + + Args: + protocol: + The protocol to use. This will typically be one of: + + - `http` for communications over HTTP(S). Note that when + setting up the provider information in + [`set_provider_info`][pact.v3.verifier.Verifier.set_provider_info], + a HTTP transport method is always added and it is unlikely + that an additional HTTP transport method will be needed + unless the provider is running on additional ports. + + - `message` for non-plugin synchronous message-based + communications. + + Any other protocol will be treated as a custom protocol and will + be handled by a plugin. + + port: + The provider port. + + If the protocol does not use ports, this parameter should be + `None`. If not specified, the default port for the scheme will + be used (provided the scheme is known). + + path: + The provider context path. + + For protocols which do not use paths, this parameter should be + `None`. + + For protocols which do use paths, this parameter should be + specified to avoid any ambiguity, though if left unspecified, + the root path will be used. + + If a non-root path is used, the path given here will be + prepended to the path in the interaction. For example, if the + path is `/api`, and the interaction path is `/users`, the + request will be made to `/api/users`. + + scheme: + The provider scheme, if applicable to the protocol. + + This is typically only used for the `http` protocol, where this + value can either be `http` (the default) or `https`. + """ + if port is None and scheme: + if scheme.lower() == "http": + port = 80 + elif scheme.lower() == "https": + port = 443 + + pact.v3.ffi.verifier_add_provider_transport( + self._handle, + protocol, + port or 0, + path, + scheme, + ) + return self + + def filter( + self, + description: str | None = None, + *, + state: str | None = None, + no_state: bool = False, + ) -> Self: + """ + Set the filter for the interactions. + + This method can be used to filter interactions based on their + description and state. Repeated calls to this method will replace the + previous filter. + + Args: + description: + The interaction description. This should be a regular + expression. If unspecified, no filtering will be done based on + the description. + + state: + The interaction state. This should be a regular expression. If + unspecified, no filtering will be done based on the state. + + no_state: + Whether to include interactions with no state. + """ + pact.v3.ffi.verifier_set_filter_info( + self._handle, + description, + state, + filter_no_state=no_state, + ) + return self + + def set_state( + self, + url: str | URL, + *, + teardown: bool = False, + body: bool = False, + ) -> Self: + """ + Set the provider state URL. + + The URL is used when the provider's internal state needs to be changed. + For example, a consumer might have an interaction that requires a + specific user to be present in the database. The provider state URL is + used to change the provider's internal state to include the required + user. + + Args: + url: + The URL to which a `POST` request will be made to change the + provider's internal state. + + teardown: + Whether to teardown the provider state after an interaction is + validated. + + body: + Whether to include the state change request in the body (`True`) + or in the query string (`False`). + """ + pact.v3.ffi.verifier_set_provider_state( + self._handle, + url if isinstance(url, str) else str(url), + teardown=teardown, + body=body, + ) + return self + + def disable_ssl_verification(self) -> Self: + """ + Disable SSL verification. + """ + self._disable_ssl_verification = True + pact.v3.ffi.verifier_set_verification_options( + self._handle, + disable_ssl_verification=self._disable_ssl_verification, + request_timeout=self._request_timeout, + ) + return self + + def set_request_timeout(self, timeout: int) -> Self: + """ + Set the request timeout. + + Args: + timeout: + The request timeout in milliseconds. + """ + if timeout < 0: + msg = "Request timeout must be a positive integer" + raise ValueError(msg) + + self._request_timeout = timeout + pact.v3.ffi.verifier_set_verification_options( + self._handle, + disable_ssl_verification=self._disable_ssl_verification, + request_timeout=self._request_timeout, + ) + return self + + def set_coloured_output(self, *, enabled: bool = True) -> Self: + """ + Toggle coloured output. + """ + pact.v3.ffi.verifier_set_coloured_output(self._handle, enabled=enabled) + return self + + def set_error_on_empty_pact(self, *, enabled: bool = True) -> Self: + """ + Toggle error on empty pact. + + If enabled, a Pact file with no interactions will cause the verifier to + return an error. If disabled, a Pact file with no interactions will be + ignored. + """ + pact.v3.ffi.verifier_set_no_pacts_is_error(self._handle, enabled=enabled) + return self + + def set_publish_options( + self, + version: str, + url: str, + branch: str, + tags: list[str] | None = None, + ) -> Self: + """ + Set options used when publishing results to the Broker. + + Args: + version: + The provider version. + + url: + URL to the build which ran the verification. + + tags: + Collection of tags for the provider. + + branch: + Name of the branch used for verification. + """ + pact.v3.ffi.verifier_set_publish_options( + self._handle, + version, + url, + tags or [], + branch, + ) + return self + + def filter_consumers(self, *filters: str) -> Self: + """ + Filter the consumers. + + Args: + filters: + Filters to apply to the consumers. + """ + pact.v3.ffi.verifier_set_consumer_filters(self._handle, filters) + return self + + def add_custom_header(self, name: str, value: str) -> Self: + """ + Add a customer header to the request. + + These headers are added to every request made to the provider. + + Args: + name: + The key of the header. + + value: + The value of the header. + """ + pact.v3.ffi.verifier_add_custom_header(self._handle, name, value) + return self + + def add_custom_headers( + self, + headers: dict[str, str] | Iterable[tuple[str, str]], + ) -> Self: + """ + Add multiple customer headers to the request. + + These headers are added to every request made to the provider. + + Args: + headers: + The headers to add. This can be a dictionary or an iterable of + key-value pairs. The iterable is preferred as it ensures that + repeated headers are not lost. + """ + if isinstance(headers, dict): + headers = headers.items() + for name, value in headers: + self.add_custom_header(name, value) + return self + + @overload + def add_source( + self, + source: str | URL, + *, + username: str | None = None, + password: str | None = None, + ) -> Self: ... + + @overload + def add_source(self, source: str | URL, *, token: str | None = None) -> Self: ... + + @overload + def add_source(self, source: str | Path) -> Self: ... + + def add_source( + self, + source: str | Path | URL, + *, + username: str | None = None, + password: str | None = None, + token: str | None = None, + ) -> Self: + """ + Adds a source to the verifier. + + This will use one or more Pact files as the source of interactions to + verify. + + Args: + source: + The source of the interactions. This may be either of the + following: + + - A local file path to a Pact file. + - A local file path to a directory containing Pact files. + - A URL to a Pact file. + + If using a URL, the `username` and `password` parameters can be + used to provide basic HTTP authentication, or the `token` + parameter can be used to provide bearer token authentication. + The `username` and `password` parameters can also be passed as + part of the URL. + + username: + The username to use for basic HTTP authentication. This is only + used when the source is a URL. + + password: + The password to use for basic HTTP authentication. This is only + used when the source is a URL. + + token: + The token to use for bearer token authentication. This is only + used when the source is a URL. Note that this is mutually + exclusive with `username` and `password`. + """ + if isinstance(source, Path): + return self._add_source_local(source) + + if isinstance(source, URL): + if source.scheme == "file": + return self._add_source_local(source.path) + + if source.scheme in ("http", "https"): + return self._add_source_remote( + source, + username=username, + password=password, + token=token, + ) + + msg = f"Invalid source scheme: {source.scheme}" + raise ValueError(msg) + + # Strings are ambiguous, so we need identify them as either local or + # remote. + if "://" in source: + return self._add_source_remote( + URL(source), + username=username, + password=password, + token=token, + ) + return self._add_source_local(source) + + def _add_source_local(self, source: str | Path) -> Self: + """ + Adds a local source to the verifier. + + This will use one or more Pact files as the source of interactions to + verify. + + Args: + source: + The source of the interactions. This may be either of the + following: + + - A local file path to a Pact file. + - A local file path to a directory containing Pact files. + """ + source = Path(source) + if source.is_dir(): + pact.v3.ffi.verifier_add_directory_source(self._handle, str(source)) + return self + if source.is_file(): + pact.v3.ffi.verifier_add_file_source(self._handle, str(source)) + return self + msg = f"Invalid source: {source}" + raise ValueError(msg) + + def _add_source_remote( + self, + url: str | URL, + *, + username: str | None = None, + password: str | None = None, + token: str | None = None, + ) -> Self: + """ + Add a remote source to the verifier. + + This will use a Pact file accessible over HTTP or HTTPS as the source of + interactions to verify. + + Args: + url: + The source of the interactions. This must be a URL to a Pact + file. The URL may contain a username and password for basic HTTP + authentication. + + username: + The username to use for basic HTTP authentication. If the source + is a URL containing a username, this parameter must be `None`. + + password: + The password to use for basic HTTP authentication. If the source + is a URL containing a password, this parameter must be `None`. + + token: + The token to use for bearer token authentication. This is + mutually exclusive with `username` and `password` (whether they + be specified through arguments, or embedded in the URL). + """ + url = URL(url) + + if url.user and username: + msg = "Cannot specify both `username` and a username in the URL" + raise ValueError(msg) + username = url.user or username + + if url.password and password: + msg = "Cannot specify both `password` and a password in the URL" + raise ValueError(msg) + password = url.password or password + + if token and (username or password): + msg = "Cannot specify both `token` and `username`/`password`" + raise ValueError(msg) + + pact.v3.ffi.verifier_url_source( + self._handle, + str(url), + username, + password, + token, + ) + return self + + @overload + def broker_source( + self, + url: str | URL, + *, + username: str | None = None, + password: str | None = None, + selector: Literal[False] = False, + ) -> Self: ... + + @overload + def broker_source( + self, + url: str | URL, + *, + token: str | None = None, + selector: Literal[False] = False, + ) -> Self: ... + + @overload + def broker_source( + self, + url: str | URL, + *, + username: str | None = None, + password: str | None = None, + selector: Literal[True], + ) -> BrokerSelectorBuilder: ... + + @overload + def broker_source( + self, + url: str | URL, + *, + token: str | None = None, + selector: Literal[True], + ) -> BrokerSelectorBuilder: ... + + def broker_source( # noqa: PLR0913 + self, + url: str | URL, + *, + username: str | None = None, + password: str | None = None, + token: str | None = None, + selector: bool = False, + ) -> BrokerSelectorBuilder | Self: + """ + Adds a broker source to the verifier. + + Args: + url: + The broker URL. TThe URL may contain a username and password for + basic HTTP authentication. + + username: + The username to use for basic HTTP authentication. If the source + is a URL containing a username, this parameter must be `None`. + + password: + The password to use for basic HTTP authentication. If the source + is a URL containing a password, this parameter must be `None`. + + token: + The token to use for bearer token authentication. This is + mutually exclusive with `username` and `password` (whether they + be specified through arguments, or embedded in the URL). + + selector: + Whether to return a BrokerSelectorBuilder instance. + """ + url = URL(url) + + if url.user and username: + msg = "Cannot specify both `username` and a username in the URL" + raise ValueError(msg) + username = url.user or username + + if url.password and password: + msg = "Cannot specify both `password` and a password in the URL" + raise ValueError(msg) + password = url.password or password + + if token and (username or password): + msg = "Cannot specify both `token` and `username`/`password`" + raise ValueError(msg) + + if selector: + return BrokerSelectorBuilder( + self, + str(url), + username, + password, + token, + ) + pact.v3.ffi.verifier_broker_source( + self._handle, + str(url), + username, + password, + token, + ) + return self + + def verify(self) -> Self: + """ + Verify the interactions. + + Returns: + Whether the interactions were verified successfully. + """ + pact.v3.ffi.verifier_execute(self._handle) + return self + + @property + def logs(self) -> str: + """ + Get the logs. + """ + return pact.v3.ffi.verifier_logs(self._handle) + + @classmethod + def logs_for_provider(cls, provider: str) -> str: + """ + Get the logs for a provider. + """ + return pact.v3.ffi.verifier_logs_for_provider(provider) + + def output(self, *, strip_ansi: bool = False) -> str: + """ + Get the output. + """ + return pact.v3.ffi.verifier_output(self._handle, strip_ansi=strip_ansi) + + @property + def results(self) -> dict[str, Any]: + """ + Get the results. + """ + return json.loads(pact.v3.ffi.verifier_json(self._handle)) + + +class BrokerSelectorBuilder: + """ + A Broker selector. + + This class encapsulates the logic for selecting Pacts from a Pact broker. + """ + + def __init__( # noqa: PLR0913 + self, + verifier: Verifier, + url: str, + username: str | None, + password: str | None, + token: str | None, + ) -> None: + """ + Instantiate a new Broker Selector. + + This constructor should not be called directly. Instead, use the + `broker_source` method of the `Verifier` class with `selector=True`. + """ + self._verifier = verifier + self._url = url + self._username = username + self._password = password + self._token = token + + # If the instance is dropped without having the `build()` method called, + # raise a warning. + self._built = False + + self._include_pending: bool = False + "Whether to include pending Pacts." + + self._include_wip_since: date | None = None + "Whether to include work in progress Pacts since a given date." + + self._provider_tags: list[str] | None = None + "List of provider tags to match." + + self._provider_branch: str | None = None + "The provider branch." + + self._consumer_versions: list[str] | None = None + "List of consumer version regex patterns." + + self._consumer_tags: list[str] | None = None + "List of consumer tags to match." + + def include_pending(self) -> Self: + """ + Include pending Pacts. + """ + self._include_pending = True + return self + + def exclude_pending(self) -> Self: + """ + Exclude pending Pacts. + """ + self._include_pending = False + return self + + def include_wip_since(self, d: str | date) -> Self: + """ + Include work in progress Pacts since a given date. + """ + if isinstance(d, str): + d = date.fromisoformat(d) + self._include_wip_since = d + return self + + def exclude_wip(self) -> Self: + """ + Exclude work in progress Pacts. + """ + self._include_wip_since = None + return self + + def provider_tags(self, *tags: str) -> Self: + """ + Set the provider tags. + """ + self._provider_tags = list(tags) + return self + + def provider_branch(self, branch: str) -> Self: + """ + Set the provider branch. + """ + self._provider_branch = branch + return self + + def consumer_versions(self, *versions: str) -> Self: + """ + Set the consumer versions. + """ + self._consumer_versions = list(versions) + return self + + def consumer_tags(self, *tags: str) -> Self: + """ + Set the consumer tags. + """ + self._consumer_tags = list(tags) + return self + + def build(self) -> Verifier: + """ + Build the Broker Selector. + + Returns: + The Verifier instance with the broker source added. + """ + pact.v3.ffi.verifier_broker_source_with_selectors( + self._verifier._handle, # noqa: SLF001 + self._url, + self._username, + self._password, + self._token, + self._include_pending, + self._include_wip_since, + self._provider_tags or [], + self._provider_branch, + self._consumer_versions or [], + self._consumer_tags or [], + ) + self._built = True + return self._verifier + + def __del__(self) -> None: + """ + Destructor for the Broker Selector. + + This destructor will raise a warning if the instance is dropped without + having the [`build()`][pact.v3.verifier.BrokerSelectorBuilder.build] + method called. + """ + if not self._built: + msg = "BrokerSelectorBuilder was dropped before being built." + raise Warning(msg) From 6d753bb8d42cc8aacf9cfcf044e459acda2ec454 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Fri, 8 Mar 2024 15:07:00 +1100 Subject: [PATCH 3/3] chore(v3): add basic verifier tests These tests are purely meant to detect any issues with the FFI and changes in any of the verifier methods. The compatibility suite will ensure that the verifier is functioning correctly. Signed-off-by: JP-Ellis --- tests/v3/assets/pacts/basic.json | 8 ++ tests/v3/test_verifier.py | 176 +++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 tests/v3/assets/pacts/basic.json create mode 100644 tests/v3/test_verifier.py diff --git a/tests/v3/assets/pacts/basic.json b/tests/v3/assets/pacts/basic.json new file mode 100644 index 0000000000..5c8fb325bd --- /dev/null +++ b/tests/v3/assets/pacts/basic.json @@ -0,0 +1,8 @@ +{ + "consumer": { "name": "Example Consumer" }, + "provider": { "name": "Example Producer" }, + "metadata": { + "pactSpecification": { "version": "2.0" } + }, + "interactions": [] +} diff --git a/tests/v3/test_verifier.py b/tests/v3/test_verifier.py new file mode 100644 index 0000000000..cedb665119 --- /dev/null +++ b/tests/v3/test_verifier.py @@ -0,0 +1,176 @@ +""" +Unit tests for the pact.v3.verifier module. + +These tests perform only very basic checks to ensure that the FFI module is +working correctly. They are not intended to test the Verifier API much, as +that is handled by the compatibility suite. +""" + +import re +from pathlib import Path + +import pytest + +from pact.v3.verifier import Verifier + +ASSETS_DIR = Path(__file__).parent / "assets" + + +@pytest.fixture() +def verifier() -> Verifier: + return Verifier() + + +def test_str_repr(verifier: Verifier) -> None: + assert str(verifier) == "Verifier" + assert re.match(r"", repr(verifier)) + + +def test_set_provider_info(verifier: Verifier) -> None: + name = "test_provider" + url = "http://localhost:8888/api" + verifier.set_info(name, url=url) + + scheme = "http" + host = "localhost" + port = 8888 + path = "/api" + verifier.set_info( + name, + scheme=scheme, + host=host, + port=port, + path=path, + ) + + +def test_add_provider_transport(verifier: Verifier) -> None: + # HTTP + verifier.add_transport( + protocol="http", + port=1234, + path="/api", + scheme="http", + ) + + # HTTPS + verifier.add_transport( + protocol="http", + port=4321, + path="/api", + scheme="https", + ) + + # message + verifier.add_transport( + protocol="message", + ) + + # gRPC + verifier.add_transport( + protocol="grpc", + port=1234, + ) + + +def test_set_filter(verifier: Verifier) -> None: + verifier.filter("test_filter") + verifier.filter("test_filter", state="test_value") + verifier.filter("no_state", no_state=True) + + +def test_set_state(verifier: Verifier) -> None: + verifier.set_state("test_state") + verifier.set_state("test_state", teardown=True) + verifier.set_state("test_state", body=True) + + +def test_disable_ssl_verification(verifier: Verifier) -> None: + verifier.disable_ssl_verification() + + +def test_set_request_timeout(verifier: Verifier) -> None: + verifier.set_request_timeout(1000) + + +def test_set_coloured_output(verifier: Verifier) -> None: + verifier.set_coloured_output(enabled=True) + verifier.set_coloured_output(enabled=False) + + +def test_set_error_on_empty_pact(verifier: Verifier) -> None: + verifier.set_error_on_empty_pact(enabled=True) + verifier.set_error_on_empty_pact(enabled=False) + + +def test_set_publish_options(verifier: Verifier) -> None: + verifier.set_publish_options( + version="1.0.0", + url="http://localhost:8080/build/1234", + branch="main", + tags=["main", "test", "prod"], + ) + + +def test_filter_consumers(verifier: Verifier) -> None: + verifier.filter_consumers("consumer1") + verifier.filter_consumers("consumer1", "consumer2") + + +def test_add_custom_header(verifier: Verifier) -> None: + verifier.add_custom_header("Authorization", "Bearer: 1234") + + +def test_add_custom_headers(verifier: Verifier) -> None: + verifier.add_custom_headers({ + "Authorization": "Bearer: 1234", + "Content-Type": "application/json", + }) + + +def test_add_source(verifier: Verifier) -> None: + # URL + verifier.add_source("http://localhost:8080/pact.json") + + # File + verifier.add_source(ASSETS_DIR / "pacts" / "basic.json") + + # Directory + verifier.add_source(ASSETS_DIR / "pacts") + + +def test_broker_source(verifier: Verifier) -> None: + verifier.broker_source("http://localhost:8080") + verifier.broker_source( + "http://localhost:8080", + username="user", + password="password", # noqa: S106 + ) + verifier.broker_source( + "http://localhost:8080", + token="1234", # noqa: S106 + ) + + +def test_broker_source_selector(verifier: Verifier) -> None: + ( + verifier.broker_source("http://localhost:8080", selector=True) + .consumer_tags("main", "test") + .provider_tags("main", "test") + .consumer_versions("1.2.3") + .build() + ) + + +def test_verify(verifier: Verifier) -> None: + verifier.verify() + + +def test_logs(verifier: Verifier) -> None: + logs = verifier.logs + assert logs == "" + + +def test_output(verifier: Verifier) -> None: + output = verifier.output() + assert output == ""