From 1bc229dc8c6e876cccec9cba9d9084ec33a673eb Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Fri, 10 May 2024 20:45:15 +0100 Subject: [PATCH] test: test plugins --- script/ci/download-standalone-and-test.sh | 1 + script/download-libs.sh | 3 +- script/download-plugins.sh | 6 + script/install-plugin-cli.sh | 86 +++++ script/lib/export-binary-versions.sh | 3 +- test/integration/plugin.proto | 421 ++++++++++++++++++++++ 6 files changed, 518 insertions(+), 2 deletions(-) create mode 100755 script/download-plugins.sh create mode 100755 script/install-plugin-cli.sh create mode 100644 test/integration/plugin.proto diff --git a/script/ci/download-standalone-and-test.sh b/script/ci/download-standalone-and-test.sh index 65639d43..e4e2bd39 100755 --- a/script/ci/download-standalone-and-test.sh +++ b/script/ci/download-standalone-and-test.sh @@ -6,4 +6,5 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)" # Figure out where the . "$SCRIPT_DIR"/../lib/robust-bash.sh ./script/download-standalone.sh +./script/download-plugins.sh ./script/ci/build-and-test.sh \ No newline at end of file diff --git a/script/download-libs.sh b/script/download-libs.sh index a94f09b5..53d68ef9 100755 --- a/script/download-libs.sh +++ b/script/download-libs.sh @@ -3,4 +3,5 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)" # Figure out where the . "${SCRIPT_DIR}/lib/export-binary-versions.sh" "${SCRIPT_DIR}/lib/download-ffi.sh" -"${SCRIPT_DIR}/lib/download-standalone.sh" \ No newline at end of file +"${SCRIPT_DIR}/lib/download-standalone.sh" +"${SCRIPT_DIR}/lib/download-plugins.sh" \ No newline at end of file diff --git a/script/download-plugins.sh b/script/download-plugins.sh new file mode 100755 index 00000000..76b850e3 --- /dev/null +++ b/script/download-plugins.sh @@ -0,0 +1,6 @@ +#!/bin/bash -eu +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)" # Figure out where the script is running + +. "${SCRIPT_DIR}/lib/export-binary-versions.sh" +"${SCRIPT_DIR}/install-plugin-cli.sh" +$HOME/.pact/bin/pact-plugin-cli install -y https://github.com/mefellows/pact-matt-plugin/releases/tag/$PACT_PLUGIN_MATT_VERSION \ No newline at end of file diff --git a/script/install-plugin-cli.sh b/script/install-plugin-cli.sh new file mode 100755 index 00000000..2007a300 --- /dev/null +++ b/script/install-plugin-cli.sh @@ -0,0 +1,86 @@ +#!/bin/sh -e +# +# Usage: +# $ curl -fsSL https://raw.githubusercontent.com/pact-foundation/pact-plugins/master/install-cli.sh | bash +# or +# $ wget -q https://raw.githubusercontent.com/pact-foundation/pact-plugins/master/install-cli.sh -O- | bash +# +set -e # Needed for Windows bash, which doesn't read the shebang + + + + + +detect_osarch() { + # detect_musl + case $(uname -sm) in + 'Linux x86_64') + if ldd /bin/ls >/dev/null 2>&1; then + ldd_output=$(ldd /bin/ls) + case "$ldd_output" in + *musl*) + os='linux' + arch='x86_64-musl' + ;; + *) + os='linux' + arch='x86_64' + ;; + esac + else + os='linux' + arch='x86_64' + fi + ;; + 'Linux aarch64') + if ldd /bin/ls >/dev/null 2>&1; then + ldd_output=$(ldd /bin/ls) + case "$ldd_output" in + *musl*) + os='linux' + arch='aarch64-musl' + ;; + *) + os='linux' + arch='aarch64' + ;; + esac + else + os='linux' + arch='aarch64' + fi + ;; + 'Darwin x86' | 'Darwin x86_64') + os='osx' + arch='x86_64' + ;; + 'Darwin arm64') + os='osx' + arch='aarch64' + ;; + CYGWIN*|MINGW32*|MSYS*|MINGW*) + os="windows" + arch='x86_64' + ext='.exe' + ;; + *) + echo "Sorry, you'll need to install the plugin CLI manually." + exit 1 + ;; + esac +} + + +VERSION="0.1.2" +detect_osarch + +if [ ! -f ~/.pact/bin/pact-plugin-cli ]; then + echo "--- 🐿 Installing plugins CLI version '${VERSION}' (from tag ${TAG})" + mkdir -p ~/.pact/bin + DOWNLOAD_LOCATION=https://github.com/pact-foundation/pact-plugins/releases/download/pact-plugin-cli-v${VERSION}/pact-plugin-cli-${os}-${arch}${ext}.gz + echo " Downloading from: ${DOWNLOAD_LOCATION}" + curl -L -o ~/.pact/bin/pact-plugin-cli-${os}-${arch}.gz "${DOWNLOAD_LOCATION}" + echo " Downloaded $(file ~/.pact/bin/pact-plugin-cli-${os}-${arch}.gz)" + gunzip -N -f ~/.pact/bin/pact-plugin-cli-${os}-${arch}.gz + chmod +x ~/.pact/bin/pact-plugin-cli +fi \ No newline at end of file diff --git a/script/lib/export-binary-versions.sh b/script/lib/export-binary-versions.sh index 066d2464..40d6e246 100644 --- a/script/lib/export-binary-versions.sh +++ b/script/lib/export-binary-versions.sh @@ -3,4 +3,5 @@ LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)" # Figure out where the scr PROJECT_DIR="${LIB_DIR}"/../../ export STANDALONE_VERSION=$(grep "PACT_STANDALONE_VERSION = '" "$PROJECT_DIR"/standalone/install.ts | grep -E -o "'(.*)'" | cut -d"'" -f2) -export FFI_VERSION=v$(grep "PACT_FFI_VERSION = '" "$PROJECT_DIR"/src/ffi/index.ts | grep -E -o "'(.*)'" | cut -d"'" -f2) \ No newline at end of file +export FFI_VERSION=v$(grep "PACT_FFI_VERSION = '" "$PROJECT_DIR"/src/ffi/index.ts | grep -E -o "'(.*)'" | cut -d"'" -f2) +export PACT_PLUGIN_MATT_VERSION=v0.1.1 \ No newline at end of file diff --git a/test/integration/plugin.proto b/test/integration/plugin.proto new file mode 100644 index 00000000..5ba54f1a --- /dev/null +++ b/test/integration/plugin.proto @@ -0,0 +1,421 @@ +// Proto file for Pact plugin interface V1 + +syntax = "proto3"; + +import "google/protobuf/struct.proto"; +import "google/protobuf/wrappers.proto"; +import "google/protobuf/empty.proto"; + +package io.pact.plugin; +option go_package = "io.pact.plugin"; + +// Request to verify the plugin has loaded OK +message InitPluginRequest { + // Implementation calling the plugin + string implementation = 1; + // Version of the implementation + string version = 2; +} + +// Entry to be added to the core catalogue. Each entry describes one of the features the plugin provides. +// Entries will be stored in the catalogue under the key "plugin/$name/$type/$key". +message CatalogueEntry { + enum EntryType { + // Matcher for contents of messages, requests or response bodies + CONTENT_MATCHER = 0; + // Generator for contents of messages, requests or response bodies + CONTENT_GENERATOR = 1; + // Transport for a network protocol + TRANSPORT = 2; + // Matching rule for content field/values + MATCHER = 3; + // Type of interaction + INTERACTION = 4; + } + // Entry type + EntryType type = 1; + // Entry key + string key = 2; + // Associated data required for the entry. For CONTENT_MATCHER and CONTENT_GENERATOR types, a "content-types" + // value (separated by semi-colons) is required for all the content types the plugin supports. + map values = 3; +} + +// Response to init plugin, providing the catalogue entries the plugin provides +message InitPluginResponse { + // List of entries the plugin supports + repeated CatalogueEntry catalogue = 1; +} + +// Catalogue of Core Pact + Plugin features +message Catalogue { + // List of entries from the core catalogue + repeated CatalogueEntry catalogue = 1; +} + +// Message representing a request, response or message body +message Body { + // The content type of the body in MIME format (i.e. application/json) + string contentType = 1; + // Bytes of the actual content + google.protobuf.BytesValue content = 2; + // Enum of content type override. This is a hint on how the content type should be treated. + enum ContentTypeHint { + // Determine the form of the content using the default rules of the Pact implementation + DEFAULT = 0; + // Contents must always be treated as a text form + TEXT = 1; + // Contents must always be treated as a binary form + BINARY = 2; + } + // Content type override to apply (if required). If omitted, the default rules of the Pact implementation + // will be used + ContentTypeHint contentTypeHint = 3; +} + +// Request to preform a comparison on an actual body given the expected one +message CompareContentsRequest { + // Expected body from the Pact interaction + Body expected = 1; + // Actual received body + Body actual = 2; + // If unexpected keys or attributes should be allowed. Setting this to false results in additional keys or fields + // will cause a mismatch + bool allow_unexpected_keys = 3; + // Map of expressions to matching rules. The expressions follow the documented Pact matching rule expressions + map rules = 4; + // Additional data added to the Pact/Interaction by the plugin + PluginConfiguration pluginConfiguration = 5; +} + +// Indicates that there was a mismatch with the content type +message ContentTypeMismatch { + // Expected content type (MIME format) + string expected = 1; + // Actual content type received (MIME format) + string actual = 2; +} + +// A mismatch for an particular item of content +message ContentMismatch { + // Expected data bytes + google.protobuf.BytesValue expected = 1; + // Actual data bytes + google.protobuf.BytesValue actual = 2; + // Description of the mismatch + string mismatch = 3; + // Path to the item that was matched. This is the value as per the documented Pact matching rule expressions. + string path = 4; + // Optional diff of the contents + string diff = 5; + // Part of the interaction that the mismatch is for: body, headers, metadata, etc. + string mismatchType = 6; +} + +// List of content mismatches +message ContentMismatches { + repeated ContentMismatch mismatches = 1; +} + +// Response to the CompareContentsRequest with the results of the comparison +message CompareContentsResponse { + // Error message if an error occurred. If this field is set, the remaining fields will be ignored and the + // verification marked as failed + string error = 1; + // There was a mismatch with the types of content. If this is set, the results may not be set. + ContentTypeMismatch typeMismatch = 2; + // Results of the match, keyed by matching rule expression + map results = 3; +} + +// Request to configure/setup an interaction so that it can be verified later +message ConfigureInteractionRequest { + // Content type of the interaction (MIME format) + string contentType = 1; + // This is data specified by the user in the consumer test + google.protobuf.Struct contentsConfig = 2; +} + +// Represents a matching rule +message MatchingRule { + // Type of the matching rule + string type = 1; + // Associated data for the matching rule + google.protobuf.Struct values = 2; +} + +// List of matching rules +message MatchingRules { + repeated MatchingRule rule = 1; +} + +// Example generator +message Generator { + // Type of generator + string type = 1; + // Associated data for the generator + google.protobuf.Struct values = 2; +} + +// Plugin configuration added to the pact file by the ConfigureInteraction step +message PluginConfiguration { + // Data to be persisted against the interaction + google.protobuf.Struct interactionConfiguration = 1; + // Data to be persisted in the Pact file metadata (Global data) + google.protobuf.Struct pactConfiguration = 2; +} + +// Response to the configure/setup an interaction request +message InteractionResponse { + // Contents for the interaction + Body contents = 1; + // All matching rules to apply + map rules = 2; + // Generators to apply + map generators = 3; + // For message interactions, any metadata to be applied + google.protobuf.Struct messageMetadata = 4; + // Plugin specific data to be persisted in the pact file + PluginConfiguration pluginConfiguration = 5; + // Markdown/HTML formatted text representation of the interaction + string interactionMarkup = 6; + // Type of markup used + enum MarkupType { + // CommonMark format + COMMON_MARK = 0; + // HTML format + HTML = 1; + } + MarkupType interactionMarkupType = 7; + // Description of what part this interaction belongs to (in the case of there being more than one, for instance, + // request/response messages) + string partName = 8; + // All matching rules to apply to any message metadata + map metadata_rules = 9; + // Generators to apply to any message metadata + map metadata_generators = 10; +} + +// Response to the configure/setup an interaction request +message ConfigureInteractionResponse { + // If an error occurred. In this case, the other fields will be ignored/not set + string error = 1; + // The actual response if no error occurred. + repeated InteractionResponse interaction = 2; + // Plugin specific data to be persisted in the pact file + PluginConfiguration pluginConfiguration = 3; +} + +// Request to generate the contents using any defined generators +message GenerateContentRequest { + // Original contents + Body contents = 1; + // Generators to apply + map generators = 2; + // Additional data added to the Pact/Interaction by the plugin + PluginConfiguration pluginConfiguration = 3; + // Context data provided by the test framework + google.protobuf.Struct testContext = 4; + + // The mode of the generation, if running from a consumer test or during provider verification + enum TestMode { + Unknown = 0; + // Running on the consumer side + Consumer = 1; + // Running on the provider side + Provider = 2; + } + TestMode testMode = 5; + + // Which part the content is for + enum ContentFor { + Request = 0; + Response = 1; + } + ContentFor contentFor = 6; +} + +// Generated body/message response +message GenerateContentResponse { + Body contents = 1; +} + +// Request to start a mock server +message StartMockServerRequest { + // Interface to bind to. Will default to the loopback adapter + string hostInterface = 1; + // Port to bind to. Default (or a value of 0) get the OS to open a random port + uint32 port = 2; + // If TLS should be used (if supported by the mock server) + bool tls = 3; + // Pact as JSON to use for the mock server behaviour + string pact = 4; + // Context data provided by the test framework + google.protobuf.Struct testContext = 5; +} + +// Response to the start mock server request +message StartMockServerResponse { + oneof response { + // If an error occurred + string error = 1; + + // Mock server details + MockServerDetails details = 2; + } +} + +// Details on a running mock server +message MockServerDetails { + // Mock server unique ID + string key = 1; + // Port the mock server is running on + uint32 port = 2; + // IP address the mock server is bound to. Probably an IP6 address, but may be IP4 + string address = 3; +} + +// Request to shut down a running mock server +// TODO: replace this with MockServerRequest in the next major version +message ShutdownMockServerRequest { + // The server ID to shutdown + string serverKey = 1; +} + +// Request for a running mock server by ID +message MockServerRequest { + // The server ID to shutdown + string serverKey = 1; +} + +// Result of a request that the mock server received +message MockServerResult { + // service + method that was requested + string path = 1; + // If an error occurred trying to handle the request + string error = 2; + // Any mismatches that occurred + repeated ContentMismatch mismatches = 3; +} + +// Response to the shut down mock server request +// TODO: replace this with MockServerResults in the next major version +message ShutdownMockServerResponse { + // If the mock status is all ok + bool ok = 1; + // The results of the test run, will contain an entry for each request received by the mock server + repeated MockServerResult results = 2; +} + +// Matching results of the mock server. +message MockServerResults { + // If the mock status is all ok + bool ok = 1; + // The results of the test run, will contain an entry for each request received by the mock server + repeated MockServerResult results = 2; +} + +// Request to prepare an interaction for verification +message VerificationPreparationRequest { + // Pact as JSON to use for the verification + string pact = 1; + // Interaction key for the interaction from the Pact that is being verified + string interactionKey = 2; + // Any data supplied by the user to verify the interaction + google.protobuf.Struct config = 3; +} + +// Request metadata value. Will either be a JSON-like value, or binary data +message MetadataValue { + oneof value { + google.protobuf.Value nonBinaryValue = 1; + bytes binaryValue = 2; + } +} + +// Interaction request data to be sent or received for verification +message InteractionData { + // Request/Response body as bytes + Body body = 1; + // Metadata associated with the request/response + map metadata = 2; +} + +// Response for the prepare an interaction for verification request +message VerificationPreparationResponse { + oneof response { + // If an error occurred + string error = 1; + + // Interaction data required to construct any request + InteractionData interactionData = 2; + } +} + +// Request data to verify an interaction +message VerifyInteractionRequest { + // Interaction data required to construct the request + InteractionData interactionData = 1; + // Any data supplied by the user to verify the interaction + google.protobuf.Struct config = 2; + // Pact as JSON to use for the verification + string pact = 3; + // Interaction key for the interaction from the Pact that is being verified + string interactionKey = 4; +} + +message VerificationResultItem { + oneof result { + string error = 1; + ContentMismatch mismatch = 2; + } +} + +// Result of running the verification +message VerificationResult { + // Was the verification successful? + bool success = 1; + // Interaction data retrieved from the provider (optional) + InteractionData responseData = 2; + // Any mismatches that occurred + repeated VerificationResultItem mismatches = 3; + // Output for the verification to display to the user + repeated string output = 4; +} + +// Result of running the verification +message VerifyInteractionResponse { + oneof response { + // If an error occurred trying to run the verification + string error = 1; + + VerificationResult result = 2; + } +} + +service PactPlugin { + // Check that the plugin loaded OK. Returns the catalogue entries describing what the plugin provides + rpc InitPlugin(InitPluginRequest) returns (InitPluginResponse); + // Updated catalogue. This will be sent when the core catalogue has been updated (probably by a plugin loading). + rpc UpdateCatalogue(Catalogue) returns (google.protobuf.Empty); + // Request to perform a comparison of some contents (matching request) + rpc CompareContents(CompareContentsRequest) returns (CompareContentsResponse); + // Request to configure/setup the interaction for later verification. Data returned will be persisted in the pact file. + rpc ConfigureInteraction(ConfigureInteractionRequest) returns (ConfigureInteractionResponse); + // Request to generate the content using any defined generators + rpc GenerateContent(GenerateContentRequest) returns (GenerateContentResponse); + + // Start a mock server + rpc StartMockServer(StartMockServerRequest) returns (StartMockServerResponse); + // Shutdown a running mock server + // TODO: Replace the message types with MockServerRequest and MockServerResults in the next major version + rpc ShutdownMockServer(ShutdownMockServerRequest) returns (ShutdownMockServerResponse); + // Get the matching results from a running mock server + rpc GetMockServerResults(MockServerRequest) returns (MockServerResults); + + // Prepare an interaction for verification. This should return any data required to construct any request + // so that it can be amended before the verification is run + rpc PrepareInteractionForVerification(VerificationPreparationRequest) returns (VerificationPreparationResponse); + // Execute the verification for the interaction. + rpc VerifyInteraction(VerifyInteractionRequest) returns (VerifyInteractionResponse); +} \ No newline at end of file