From 80cc10e4df98c4c2d898ded2a9f49147c2ba1a14 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Tue, 10 May 2022 12:54:31 +1000 Subject: [PATCH] feat: add command to publish provider contracts to pactflow (feature toggle required) (#107) --- bin/pactflow | 4 + .../markdown/Pact Broker Client - Pactflow.md | 36 ++++++++ lib/pact_broker/client/hal_client_methods.rb | 4 + lib/pactflow/client/cli/pactflow.rb | 12 +++ .../client/cli/provider_contract_commands.rb | 68 ++++++++++++++ .../client/provider_contracts/publish.rb | 79 ++++++++++++++++ script/oas.yml | 49 ++++++++++ script/publish-provider-contract.sh | 27 ++++++ script/verification-results.txt | 1 + spec/pacts/pact_broker_client-pactflow.json | 30 ++++++ ...pactflow_publish_provider_contract_spec.rb | 92 +++++++++++++++++++ 11 files changed, 402 insertions(+) create mode 100755 bin/pactflow create mode 100644 lib/pactflow/client/cli/pactflow.rb create mode 100644 lib/pactflow/client/cli/provider_contract_commands.rb create mode 100644 lib/pactflow/client/provider_contracts/publish.rb create mode 100644 script/oas.yml create mode 100755 script/publish-provider-contract.sh create mode 100644 script/verification-results.txt create mode 100644 spec/service_providers/pactflow_publish_provider_contract_spec.rb diff --git a/bin/pactflow b/bin/pactflow new file mode 100755 index 00000000..e5d64d24 --- /dev/null +++ b/bin/pactflow @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require "pactflow/client/cli/pactflow" + +Pactflow::Client::CLI::Pactflow.start diff --git a/doc/pacts/markdown/Pact Broker Client - Pactflow.md b/doc/pacts/markdown/Pact Broker Client - Pactflow.md index 6339731b..d1a2e672 100644 --- a/doc/pacts/markdown/Pact Broker Client - Pactflow.md +++ b/doc/pacts/markdown/Pact Broker Client - Pactflow.md @@ -4,6 +4,8 @@ * [A request for the index resource](#a_request_for_the_index_resource) +* [A request to create a provider contract](#a_request_to_create_a_provider_contract) + * [A request to create a webhook for a team](#a_request_to_create_a_webhook_for_a_team_given_a_team_with_UUID_2abbc12a-427d-432a-a521-c870af1739d9_exists) given a team with UUID 2abbc12a-427d-432a-a521-c870af1739d9 exists #### Interactions @@ -41,6 +43,40 @@ Pactflow will respond with: } } ``` + +Upon receiving **a request to create a provider contract** from Pact Broker Client, with +```json +{ + "method": "put", + "path": "/contracts/provider/Bar/version/1", + "headers": { + "Content-Type": "application/json", + "Accept": "application/hal+json" + }, + "body": { + "content": "LS0tCjpzb21lOiBjb250cmFjdAo=", + "contractType": "oas", + "contentType": "application/yaml", + "verificationResults": { + "success": true, + "content": "c29tZSByZXN1bHRz", + "contentType": "text/plain", + "format": "text", + "verifier": "my custom tool", + "verifierVersion": "1.0" + } + } +} +``` +Pactflow will respond with: +```json +{ + "status": 201, + "headers": { + "Content-Type": "application/hal+json;charset=utf-8" + } +} +``` Given **a team with UUID 2abbc12a-427d-432a-a521-c870af1739d9 exists**, upon receiving **a request to create a webhook for a team** from Pact Broker Client, with ```json diff --git a/lib/pact_broker/client/hal_client_methods.rb b/lib/pact_broker/client/hal_client_methods.rb index a952c266..dc6ba6b3 100644 --- a/lib/pact_broker/client/hal_client_methods.rb +++ b/lib/pact_broker/client/hal_client_methods.rb @@ -8,6 +8,10 @@ def create_index_entry_point(pact_broker_base_url, pact_broker_client_options) PactBroker::Client::Hal::EntryPoint.new(pact_broker_base_url, create_http_client(pact_broker_client_options)) end + def create_entry_point(entry_point, pact_broker_client_options) + PactBroker::Client::Hal::EntryPoint.new(entry_point, create_http_client(pact_broker_client_options)) + end + def create_http_client(pact_broker_client_options) PactBroker::Client::Hal::HttpClient.new(pact_broker_client_options.merge(pact_broker_client_options[:basic_auth] || {})) end diff --git a/lib/pactflow/client/cli/pactflow.rb b/lib/pactflow/client/cli/pactflow.rb new file mode 100644 index 00000000..29290784 --- /dev/null +++ b/lib/pactflow/client/cli/pactflow.rb @@ -0,0 +1,12 @@ +require "pactflow/client/cli/provider_contract_commands" +require "pact_broker/client/cli/custom_thor" + +module Pactflow + module Client + module CLI + class Pactflow < PactBroker::Client::CLI::CustomThor + include ::Pactflow::Client::CLI::ProviderContractCommands + end + end + end +end diff --git a/lib/pactflow/client/cli/provider_contract_commands.rb b/lib/pactflow/client/cli/provider_contract_commands.rb new file mode 100644 index 00000000..c4955f18 --- /dev/null +++ b/lib/pactflow/client/cli/provider_contract_commands.rb @@ -0,0 +1,68 @@ +require "pact_broker/client/hash_refinements" + +module Pactflow + module Client + module CLI + module ProviderContractCommands + using PactBroker::Client::HashRefinements + + def self.included(thor) + thor.class_eval do + + if ENV.fetch("PACTFLOW_FEATURES", "").include?("publish-provider-contract") + + desc 'publish-provider-contract CONTRACT_FILE ...', "Publish provider contract to Pactflow" + method_option :provider, required: true, desc: "The provider name" + method_option :provider_app_version, required: true, aliases: "-a", desc: "The provider application version" + method_option :branch, aliases: "-h", desc: "Repository branch of the provider version" + #method_option :auto_detect_version_properties, hidden: true, type: :boolean, default: false, desc: "Automatically detect the repository branch from known CI environment variables or git CLI." + method_option :tag, aliases: "-t", type: :array, banner: "TAG", desc: "Tag name for provider version. Can be specified multiple times." + #method_option :tag_with_git_branch, aliases: "-g", type: :boolean, default: false, required: false, desc: "Tag consumer version with the name of the current git branch. Default: false" + method_option :specification, default: "oas", desc: "The contract specification" + method_option :content_type, desc: "The content type. eg. application/yml" + method_option :verification_success, type: :boolean + method_option :verification_results, desc: "The path to the file containing the output from the verification process" + method_option :verification_results_content_type, desc: "The content type of the verification output eg. text/plain, application/yaml" + method_option :verification_results_format, desc: "The format of the verification output eg. junit, text" + method_option :verifier, desc: "The tool used to verify the provider contract" + method_option :verifier_version, desc: "The version of the tool used to verify the provider contract" + #method_option :build_url, desc: "The build URL that created the pact" + + output_option_json_or_text + shared_authentication_options + + def publish_provider_contract(provider_contract_path) + require "pactflow/client/provider_contracts/publish" + + params = params = { + provider_name: options.provider.strip, + provider_version_number: options.provider_app_version.strip, + branch_name: options.branch && options.branch.strip, + tags: (options.tag && options.tag.collect(&:strip)) || [], + contract: { + content: File.read(provider_contract_path), + content_type: options.content_type, + specification: options.specification + }, + verification_results: { + success: options.verification_success, + content: options.verification_results ? File.read(options.verification_results) : nil, + content_type: options.verification_results_content_type, + format: options.verification_results_format, + verifier: options.verifier, + verifier_version: options.verifier_version + } + } + + command_options = { verbose: options.verbose, output: options.output } + result = ::Pactflow::Client::ProviderContracts::Publish.call(params, command_options, pact_broker_client_options) + $stdout.puts result.message + exit(1) unless result.success + end + end + end + end + end + end + end +end diff --git a/lib/pactflow/client/provider_contracts/publish.rb b/lib/pactflow/client/provider_contracts/publish.rb new file mode 100644 index 00000000..2afce6c8 --- /dev/null +++ b/lib/pactflow/client/provider_contracts/publish.rb @@ -0,0 +1,79 @@ +require "pact_broker/client/base_command" +require "pact_broker/client/versions/create" +require "base64" + +module Pactflow + module Client + module ProviderContracts + class Publish < PactBroker::Client::BaseCommand + attr_reader :branch_name, :tags, :provider_name, :provider_version_number, :contract, :verification_results + + def initialize(params, options, pact_broker_client_options) + super + @provider_name = params[:provider_name] + @provider_version_number = params[:provider_version_number] + @branch_name = params[:branch_name] + @tags = params[:tags] || [] + @contract = params[:contract] + @verification_results = params[:verification_results] + end + + private + + def do_call + create_branch_version_and_tags + create_contract + PactBroker::Client::CommandResult.new(true, green("Successfully published provider contract for #{provider_name} version #{provider_version_number}")) + end + + def create_branch_version_and_tags + if branch_name || tags.any? + pacticipant_version_params = { + pacticipant_name: provider_name, + version_number: provider_version_number, + branch_name: branch_name, + tags: tags + } + result = PactBroker::Client::Versions::Create.call(pacticipant_version_params, options, pact_broker_client_options) + if !result.success + raise PactBroker::Client::Error.new(result.message) + end + end + end + + def create_contract + contract_path = "#{pact_broker_base_url}/contracts/provider/{provider}/version/{version}" + entrypoint = create_entry_point(contract_path, pact_broker_client_options) + entrypoint.expand(provider: provider_name, version: provider_version_number).put!(contract_params) + end + + def contract_params + verification_results_params = { + success: verification_results[:success], + content: verification_results[:content] ? encode_content(verification_results[:content]) : nil, + contentType: verification_results[:content_type], + format: verification_results[:format], + verifier: verification_results[:verifier], + verifierVersion: verification_results[:verifier_version] + }.compact + + body_params = { + content: encode_content(contract[:content]), + contractType: contract[:specification], + contentType: contract[:content_type], + }.compact + + if verification_results_params.any? + body_params[:verificationResults] = verification_results_params + end + + body_params + end + + def encode_content oas + Base64.strict_encode64(oas) + end + end + end + end +end diff --git a/script/oas.yml b/script/oas.yml new file mode 100644 index 00000000..7f21c18e --- /dev/null +++ b/script/oas.yml @@ -0,0 +1,49 @@ +openapi: 3.0.0 +info: + title: Sample API + description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML. + version: 0.1.9 +servers: + - url: https://test.pactflow.io + description: Prod + - url: https://test.test.pactflow.io + description: Test +paths: + /admin/teams/{uuid}: + get: + summary: Returns a team + parameters: + - name: uuid + in: path + description: The UUID of the team to return + required: true + example: 85ad09f5-e014-4e0f-a146-4377fa64b5ef + schema: + type: string + responses: + '200': + description: A JSON team + content: + application/json: + schema: + type: object + additionalProperties: false + properties: + name: + type: string + example: Team Awesome + uuid: + type: string + example: 85ad09f5-e014-4e0f-a146-4377fa64b5ef + numberOfMembers: + type: integer + example: 4 + _links: + type: object + properties: + self: + type: object + properties: + href: + type: string + example: http://test.pactflow.io/admin/teams/85ad09f5-e014-4e0f-a146-4377fa64b5ef diff --git a/script/publish-provider-contract.sh b/script/publish-provider-contract.sh new file mode 100755 index 00000000..c365b6b3 --- /dev/null +++ b/script/publish-provider-contract.sh @@ -0,0 +1,27 @@ +export PACT_BROKER_BASE_URL="http://localhost:9292" +export PACTFLOW_FEATURES=publish-provider-contract +# bundle exec bin/pactflow publish-provider-contract \ +# script/oas.yml \ +# --provider Foo \ +# --provider-app-version 1013b5650d61214e19f10558f97fb5a3bb082d44 \ +# --branch main \ +# --tag dev \ +# --specification oas \ +# --content-type application/yml \ +# --no-verification-success \ +# --verification-results script/verification-results.txt \ +# --verification-results-content-type text/plain \ +# --verification-results-format text \ +# --verifier my-custom-tool \ +# --verifier-version "1.0" \ +# --verbose + + + bundle exec bin/pactflow publish-provider-contract \ + script/oas.yml \ + --provider Foo \ + --provider-app-version 1013b5650d61214e19f10558f97fb5a3bb082d44 \ + --branch main \ + --tag dev \ + --specification oas \ + --content-type application/yml diff --git a/script/verification-results.txt b/script/verification-results.txt new file mode 100644 index 00000000..014fd4c1 --- /dev/null +++ b/script/verification-results.txt @@ -0,0 +1 @@ +These are some results. diff --git a/spec/pacts/pact_broker_client-pactflow.json b/spec/pacts/pact_broker_client-pactflow.json index 9d18f910..0ca7124f 100644 --- a/spec/pacts/pact_broker_client-pactflow.json +++ b/spec/pacts/pact_broker_client-pactflow.json @@ -6,6 +6,36 @@ "name": "Pactflow" }, "interactions": [ + { + "description": "a request to create a provider contract", + "request": { + "method": "put", + "path": "/contracts/provider/Bar/version/1", + "headers": { + "Content-Type": "application/json", + "Accept": "application/hal+json" + }, + "body": { + "content": "LS0tCjpzb21lOiBjb250cmFjdAo=", + "contractType": "oas", + "contentType": "application/yaml", + "verificationResults": { + "success": true, + "content": "c29tZSByZXN1bHRz", + "contentType": "text/plain", + "format": "text", + "verifier": "my custom tool", + "verifierVersion": "1.0" + } + } + }, + "response": { + "status": 201, + "headers": { + "Content-Type": "application/hal+json;charset=utf-8" + } + } + }, { "description": "a request for the index resource", "request": { diff --git a/spec/service_providers/pactflow_publish_provider_contract_spec.rb b/spec/service_providers/pactflow_publish_provider_contract_spec.rb new file mode 100644 index 00000000..8ae3ae8d --- /dev/null +++ b/spec/service_providers/pactflow_publish_provider_contract_spec.rb @@ -0,0 +1,92 @@ +require_relative "pact_helper" +require "pactflow/client/provider_contracts/publish" +require "yaml" + +RSpec.describe "publishing a provider contract to Pactflow", pact: true do + before do + # no point re-testing this + allow(PactBroker::Client::Versions::Create).to receive(:call).and_return(double("result", success: true)) + end + + include_context "pact broker" + include PactBrokerPactHelperMethods + + let(:command_params) do + { + provider_name: "Bar", + provider_version_number: "1", + branch_name: "main", + tags: ["dev"], + contract: { + content: { some: "contract" }.to_yaml, + content_type: "application/yaml", + specification: "oas" + }, + verification_results: { + success: true, + content: "some results", + content_type: "text/plain", + format: "text", + verifier: "my custom tool", + verifier_version: "1.0" + } + } + end + + let(:body) { { some: "body" }.to_json } + + let(:request_body) do + { + "content" => "LS0tCjpzb21lOiBjb250cmFjdAo=", + "contractType" => "oas", + "contentType" => "application/yaml", + "verificationResults" => { + "success" => true, + "content" => "c29tZSByZXN1bHRz", + "contentType" => "text/plain", + "format" => "text", + "verifier" => "my custom tool", + "verifierVersion" => "1.0" + } + } + end + + let(:response_status) { 201 } + let(:success_response) do + { + status: response_status, + headers: pact_broker_response_headers + } + end + + let(:options) do + { + verbose: false + } + end + + let(:pact_broker_client_options) do + { pact_broker_base_url: pactflow.mock_service_base_url } + end + + subject { Pactflow::Client::ProviderContracts::Publish.call(command_params, options, pact_broker_client_options) } + + context "creating a provider contract with valid parameters" do + before do + pactflow + .upon_receiving("a request to create a provider contract") + .with( + method: :put, + path: "/contracts/provider/Bar/version/1", + headers: put_request_headers, + body: request_body) + .will_respond_with(success_response) + end + + it "returns a CommandResult with success = true" do + expect(subject).to be_a PactBroker::Client::CommandResult + expect(subject.success).to be true + expect(subject.message).to include "Successfully published provider contract for Bar version 1" + end + end +end