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