diff --git a/doc/pacts/markdown/Pact Broker Client - Pactflow.md b/doc/pacts/markdown/Pact Broker Client - Pactflow.md index 491a95e4..2aa4759b 100644 --- a/doc/pacts/markdown/Pact Broker Client - Pactflow.md +++ b/doc/pacts/markdown/Pact Broker Client - Pactflow.md @@ -4,12 +4,16 @@ * [A request for the index resource](#a_request_for_the_index_resource) +* [A request for the index resource](#a_request_for_the_index_resource_given_the_pb:publish-provider-contract_relation_exists_in_the_index_resource) given the pb:publish-provider-contract relation exists in the index resource + * [A request to create a provider contract](#a_request_to_create_a_provider_contract) * [A request to create a provider contract](#a_request_to_create_a_provider_contract_given_there_is_a_pf:ui_href_in_the_response) given there is a pf:ui href in the response * [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 +* [A request to publish a provider contract](#a_request_to_publish_a_provider_contract) + #### Interactions @@ -45,6 +49,33 @@ PactFlow will respond with: } } ``` + +Given **the pb:publish-provider-contract relation exists in the index resource**, upon receiving **a request for the index resource** from Pact Broker Client, with +```json +{ + "method": "GET", + "path": "/", + "headers": { + "Accept": "application/hal+json" + } +} +``` +PactFlow will respond with: +```json +{ + "status": 200, + "headers": { + "Content-Type": "application/hal+json;charset=utf-8" + }, + "body": { + "_links": { + "pf:publish-provider-contract": { + "href": "http://localhost:1235/HAL-REL-PLACEHOLDER-PF-PUBLISH-PROVIDER-CONTRACT-{provider}" + } + } + } +} +``` Upon receiving **a request to create a provider contract** from Pact Broker Client, with ```json @@ -171,3 +202,66 @@ PactFlow will respond with: } } ``` + +Upon receiving **a request to publish a provider contract** from Pact Broker Client, with +```json +{ + "method": "post", + "path": "/HAL-REL-PLACEHOLDER-PF-PUBLISH-PROVIDER-CONTRACT-Bar", + "headers": { + "Content-Type": "application/json", + "Accept": "application/hal+json" + }, + "body": { + "pacticipantVersionNumber": "1", + "tags": [ + "dev" + ], + "branch": "main", + "buildUrl": "http://build", + "contract": { + "content": "LS0tCnNvbWU6IGNvbnRyYWN0Cg==", + "contentType": "application/yaml", + "specification": "oas", + "selfVerificationResults": { + "success": true, + "content": "c29tZSByZXN1bHRz", + "contentType": "text/plain", + "format": "text", + "verifier": "my custom tool", + "verifierVersion": "1.0" + } + } + } +} +``` +PactFlow will respond with: +```json +{ + "status": 200, + "headers": { + "Content-Type": "application/hal+json;charset=utf-8" + }, + "body": { + "notices": [ + { + "text": "some notice", + "type": "info" + } + ], + "_embedded": { + "version": { + "number": "1" + } + }, + "_links": { + "pb:pacticipant-version-tags": [ + { + } + ], + "pb:branch-version": { + } + } + } +} +``` diff --git a/lib/pactflow/client/cli/provider_contract_commands.rb b/lib/pactflow/client/cli/provider_contract_commands.rb index 66e82569..aa666bb0 100644 --- a/lib/pactflow/client/cli/provider_contract_commands.rb +++ b/lib/pactflow/client/cli/provider_contract_commands.rb @@ -26,7 +26,7 @@ def self.included(thor) 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" + method_option :build_url, desc: "The build URL that created the provider contract" output_option_json_or_text shared_authentication_options @@ -70,6 +70,7 @@ def publish_provider_contract_command_params(provider_contract_path) provider_version_number: options.provider_app_version.strip, branch_name: options.branch && options.branch.strip, tags: (options.tag && options.tag.collect(&:strip)) || [], + build_url: options.build_url, contract: { content: File.read(provider_contract_path), content_type: options.content_type, diff --git a/lib/pactflow/client/provider_contracts/publish.rb b/lib/pactflow/client/provider_contracts/publish.rb index dfb5acf0..1a793ec0 100644 --- a/lib/pactflow/client/provider_contracts/publish.rb +++ b/lib/pactflow/client/provider_contracts/publish.rb @@ -1,13 +1,14 @@ -require "pact_broker/client/base_command" -require "pact_broker/client/versions/create" -require 'pact_broker/client/colorize_notices' require "base64" +require "pact_broker/client/base_command" +require "pact_broker/client/colorize_notices" +require "pactflow/client/provider_contracts/publish_the_old_way" module Pactflow module Client module ProviderContracts class Publish < PactBroker::Client::BaseCommand - attr_reader :branch_name, :tags, :provider_name, :provider_version_number, :contract, :verification_results + PUBLISH_RELATION = "pf:publish-provider-contract" + def initialize(params, options, pact_broker_client_options) super @@ -15,62 +16,30 @@ def initialize(params, options, pact_broker_client_options) @provider_version_number = params[:provider_version_number] @branch_name = params[:branch_name] @tags = params[:tags] || [] + @build_url = params[:build_url] @contract = params[:contract] @verification_results = params[:verification_results] end private - def do_call - create_branch_version_and_tags - render_response(create_contract) - end + attr_reader :provider_name, :provider_version_number, :branch_name, :tags, :build_url, :contract, :verification_results - def render_response(res) - notices = [ - { type: 'success', text: "Successfully published provider contract for #{provider_name} version #{provider_version_number} to PactFlow"}, - ] - if res.body && res.body['_links'] && res.body['_links']['pf:ui']['href'] - notices.concat([{ text: "View the uploaded contract at #{res.body['_links']['pf:ui']['href']}" }]) + def do_call + if enabled? && index_resource.assert_success!.can?(PUBLISH_RELATION) + publish_provider_contracts + PactBroker::Client::CommandResult.new(success?, message) + else + PublishTheOldWay.call(params, options, pact_broker_client_options) end - notices.concat(next_steps) - PactBroker::Client::CommandResult.new(true, PactBroker::Client::ColorizeNotices.call(notices.collect do |n| - OpenStruct.new(n) - end).join("\n")) - end - - def next_steps - [ - { type: 'prompt', text: 'Next steps:' }, - { type: 'prompt', - text: ' * Check your application is safe to deploy - https://docs.pact.io/can_i_deploy' }, - { text: " pact-broker can-i-deploy --pacticipant #{provider_name} --version #{provider_version_number} --to-environment " }, - { type: 'prompt', - text: ' * Record deployment or release to specified environment (choose one) - https://docs.pact.io/go/record-deployment' }, - { text: " pact-broker record-deployment --pacticipant #{provider_name} --version #{provider_version_number} --environment " }, - { text: " pact-broker record-release --pacticipant #{provider_name} --version #{provider_version_number} --environment " } - ] 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 + def enabled? + ENV.fetch("PACT_BROKER_FEATURES", "").include?("publish_provider_contracts_all_in_one") 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).response + def publish_provider_contracts + @response_entity = index_resource._link(PUBLISH_RELATION).expand(provider: provider_name).post!(contract_params, headers: { "Accept" => "application/hal+json,application/problem+json" }) end def contract_params @@ -82,22 +51,45 @@ def contract_params 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 + + contract_params = { + content: encode_content(contract[:content]), + specification: contract[:specification], + contentType: contract[:content_type] + }.compact if verification_results_params.any? - body_params[:verificationResults] = verification_results_params + contract_params[:selfVerificationResults] = verification_results_params end - body_params + + { + pacticipantVersionNumber: provider_version_number, + tags: tags, + branch: branch_name, + buildUrl: build_url, + contract: contract_params + } end def encode_content oas Base64.strict_encode64(oas) end + + def message + if options[:output] == "json" + @response_entity.response.raw_body + else + text_message + end + end + + def success? + @response_entity.success? + end + + def text_message + PactBroker::Client::ColorizeNotices.call(@response_entity.notices.collect{ |n| OpenStruct.new(n) } ) + end end end end diff --git a/lib/pactflow/client/provider_contracts/publish_the_old_way.rb b/lib/pactflow/client/provider_contracts/publish_the_old_way.rb new file mode 100644 index 00000000..44acaf39 --- /dev/null +++ b/lib/pactflow/client/provider_contracts/publish_the_old_way.rb @@ -0,0 +1,104 @@ +require "pact_broker/client/base_command" +require "pact_broker/client/versions/create" +require 'pact_broker/client/colorize_notices' +require "base64" + +module Pactflow + module Client + module ProviderContracts + class PublishTheOldWay < 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 + render_response(create_contract) + end + + def render_response(res) + notices = [ + { type: 'success', text: "Successfully published provider contract for #{provider_name} version #{provider_version_number} to PactFlow"}, + ] + if res.body && res.body['_links'] && res.body['_links']['pf:ui']['href'] + notices.concat([{ text: "View the uploaded contract at #{res.body['_links']['pf:ui']['href']}" }]) + end + notices.concat(next_steps) + PactBroker::Client::CommandResult.new(true, PactBroker::Client::ColorizeNotices.call(notices.collect do |n| + OpenStruct.new(n) + end).join("\n")) + end + + def next_steps + [ + { type: 'prompt', text: 'Next steps:' }, + { type: 'prompt', + text: ' * Check your application is safe to deploy - https://docs.pact.io/can_i_deploy' }, + { text: " pact-broker can-i-deploy --pacticipant #{provider_name} --version #{provider_version_number} --to-environment " }, + { type: 'prompt', + text: ' * Record deployment or release to specified environment (choose one) - https://docs.pact.io/go/record-deployment' }, + { text: " pact-broker record-deployment --pacticipant #{provider_name} --version #{provider_version_number} --environment " }, + { text: " pact-broker record-release --pacticipant #{provider_name} --version #{provider_version_number} --environment " } + ] + 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).response + 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/foo-bar.json b/script/foo-bar.json new file mode 100644 index 00000000..f15ac596 --- /dev/null +++ b/script/foo-bar.json @@ -0,0 +1,81 @@ +{ + "consumer": { + "name": "Foo" + }, + "provider": { + "name": "Bar" + }, + "interactions": [ + { + "description": "a request to list the latest pacts", + "providerState": "a pact between Condor and the Pricing Service exists", + "request": { + "method": "get", + "path": "/pacts/latest", + "headers": { + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/hal+json" + }, + "body": { + "_links": { + "self": { + "href": "http://example.org/pacts/latest" + } + }, + "pacts": [ + { + "_links": { + "self": [ + { + "href": "http://example.org/pacts/provider/Pricing%20Service/consumer/Condor/latest" + }, + { + "href": "http://example.org/pacts/provider/Pricing%20Service/consumer/Condor/version/1.3.0" + } + ] + }, + "_embedded": { + "consumer": { + "name": "Condor", + "_links": { + "self": { + "href": "http://example.org/pacticipants/Condor" + } + }, + "_embedded": { + "version": { + "number": "1.3.0" + } + } + }, + "provider": { + "_links": { + "self": { + "href": "http://example.org/pacticipants/Pricing%20Service" + } + }, + "name": "Pricing Service" + } + } + } + ] + }, + "matchingRules": { + "$.headers.Content-Type": { + "match": "regex", + "regex": "application\\/hal\\+json" + } + } + } + } + ], + "metadata": { + "pactSpecification": { + "version": "2.0.0" + } + } +} \ No newline at end of file diff --git a/script/publish-pact.sh b/script/publish-pact.sh index f60ba5c3..aa6c1c97 100755 --- a/script/publish-pact.sh +++ b/script/publish-pact.sh @@ -1,5 +1,5 @@ -export PACT_BROKER_BASE_URL="http://localhost:9292" -export PACT_BROKER_TOKEN="localhost" +export PACT_BROKER_BASE_URL=${PACT_BROKER_BASE_URL:-"http://localhost:9292"} +export PACT_BROKER_TOKEN=${PACT_BROKER_TOKEN:-"localhost"} #export PACT_BROKER_FEATURES=publish_pacts_using_old_api # bundle exec bin/pact-broker create-or-update-webhook http://localhost:9393 \ @@ -8,11 +8,11 @@ export PACT_BROKER_TOKEN="localhost" # --description "foo webhook" \ # --contract-published -bundle exec bin/pact-broker create-or-update-webhook http://localhost:9393 \ - --uuid d40f38c3-aaa3-47f5-9161-95csfadfsd7 \ - --description "This is quite a long description for a webhook that I hope will be truncated" \ - --request POST \ - --contract-published +# bundle exec bin/pact-broker create-or-update-webhook http://localhost:9393 \ +# --uuid d40f38c3-aaa3-47f5-9161-95csfadfsd7 \ +# --description "This is quite a long description for a webhook that I hope will be truncated" \ +# --request POST \ +# --contract-published # bundle exec bin/pact-broker publish spec/pacts/pact_broker_client-pact_broker.json spec/fixtures/foo-bar.json \ # --consumer-app-version 1.2.12 \ @@ -29,10 +29,8 @@ bundle exec bin/pact-broker create-or-update-webhook http://localhost:9393 \ # --contract-published -bundle exec bin/pact-broker publish spec/pacts/pact_broker_client-pact_broker.json \ +bundle exec bin/pact-broker publish scripts/foo-bar.json \ --consumer-app-version 1.2.26 \ - --broker-base-url http://localhost:9292 \ - --broker-token localhost \ --auto-detect-version-properties \ --build-url http://mybuild \ --branch master --tag foo5 --tag foo6 diff --git a/script/publish-provider-contract.sh b/script/publish-provider-contract.sh index df4981cc..99c4fff3 100755 --- a/script/publish-provider-contract.sh +++ b/script/publish-provider-contract.sh @@ -1,7 +1,7 @@ export PACT_BROKER_BASE_URL=${PACT_BROKER_BASE_URL:-"http://localhost:9292"} bundle exec bin/pactflow publish-provider-contract \ script/oas.yml \ - --provider Foo \ + --provider Bar \ --provider-app-version 1013b5650d61214e19f10558f97fb5a3bb082d44 \ --branch main \ --tag dev \ diff --git a/spec/fixtures/approvals/publish_provider_contract.approved.txt b/spec/fixtures/approvals/publish_provider_contract.approved.txt new file mode 100644 index 00000000..28346172 --- /dev/null +++ b/spec/fixtures/approvals/publish_provider_contract.approved.txt @@ -0,0 +1,2 @@ +some notice +some other notice diff --git a/spec/integration/publish_provider_contract_spec.rb b/spec/integration/publish_provider_contract_spec.rb new file mode 100644 index 00000000..8cae54bf --- /dev/null +++ b/spec/integration/publish_provider_contract_spec.rb @@ -0,0 +1,57 @@ +require "pactflow/client/cli/pactflow" + +RSpec.describe "publish-provider-contract" do + before do + allow(ENV).to receive(:fetch).and_call_original + allow(ENV).to receive(:fetch).with("PACT_BROKER_FEATURES", "").and_return("publish_provider_contracts_all_in_one") + end + let(:index_body_hash) do + { + _links: { + "pf:publish-provider-contract" => { + href: "http://broker/some-publish/{provider}" + } + } + } + end + + let(:post_response_body) do + { + "notices"=>[{"text"=>"some notice", "type"=>"info"}, {"text"=>"some other notice", "type"=>"info"}] + } + end + + let!(:index_request) do + stub_request(:get, "http://broker").to_return(status: 200, body: index_body_hash.to_json, headers: { "Content-Type" => "application/hal+json" } ) + end + + let!(:publish_request) do + stub_request(:post, "http://broker/some-publish/Bar").to_return(status: 200, body: post_response_body.to_json, headers: { "Content-Type" => "application/hal+json" } ) + end + + let(:parameters) do + %w{ + publish-provider-contract + script/oas.yml + --provider Bar + --broker-base-url http://broker + --provider-app-version 1013b5650d61214e19f10558f97fb5a3bb082d44 + --branch main + --tag dev + --specification oas + --content-type application/yml + --verification-exit-code 0 + --verification-results script/verification-results.txt + --verification-results-content-type text/plain + --verification-results-format text + --verifier my-custom-tool + --verifier-version "1.0" + } + end + + subject { capture(:stdout) { Pactflow::Client::CLI::Pactflow.start(parameters) } } + + it "prints the notices" do + Approvals.verify(subject, :name => "publish_provider_contract", format: :txt) + end +end diff --git a/spec/lib/pactflow/client/provider_contracts/publish_spec.rb b/spec/lib/pactflow/client/provider_contracts/publish_spec.rb new file mode 100644 index 00000000..91e841f8 --- /dev/null +++ b/spec/lib/pactflow/client/provider_contracts/publish_spec.rb @@ -0,0 +1,179 @@ +require "pactflow/client/provider_contracts/publish" + +module Pactflow + module Client + module ProviderContracts + describe Publish do + before do + allow_any_instance_of(PactBroker::Client::Hal::HttpClient).to receive(:sleep) + allow_any_instance_of(PactBroker::Client::Hal::HttpClient).to receive(:default_max_tries).and_return(1) + allow(ENV).to receive(:fetch).and_call_original + allow(ENV).to receive(:fetch).with("PACT_BROKER_FEATURES", "").and_return("publish_provider_contracts_all_in_one") + end + + let(:command_params) do + { + provider_name: "Bar", + provider_version_number: "1", + branch_name: "main", + tags: ["dev"], + build_url: "http://build", + 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(:options) do + { + verbose: false + } + end + + let(:pact_broker_client_options) do + { pact_broker_base_url: "http://pactflow" } + end + + let(:index_body_hash) do + { + _links: { + "pf:publish-provider-contract" => { + href: "http://pactflow/some-publish/{provider}" + } + } + } + end + + let(:post_response_body) do + { + "notices"=>[{"text"=>"some notice", "type"=>"info"}] + } + end + + let!(:index_request) do + stub_request(:get, "http://pactflow") + .to_return( + status: index_status, + body: index_body_hash.to_json, + headers: { "Content-Type" => "application/hal+json" } + ) + end + let(:index_status) { 200 } + + let!(:publish_request) do + stub_request(:post, "http://pactflow/some-publish/Bar") + .to_return( + status: publish_status, + body: post_response_body.to_json, + headers: { "Content-Type" => "application/hal+json" } + ) + end + let(:publish_status) { 200 } + + subject { Publish.call(command_params, options, pact_broker_client_options) } + + context "when there is no relation pf:publish-provider-contract" do + before do + allow(PublishTheOldWay).to receive(:call).with(command_params, options, pact_broker_client_options).and_return(instance_double(PactBroker::Client::CommandResult)) + end + + let(:index_body_hash) do + { + _links: {} + } + end + + it "publishes the provider contracts the old way" do + expect(PublishTheOldWay).to receive(:call).with(command_params, options, pact_broker_client_options) + subject + end + end + + context "when the feature is not enabled" do + before do + allow(ENV).to receive(:fetch).with("PACT_BROKER_FEATURES", "").and_return("") + end + + it "publishes the provider contracts the old way" do + expect(PublishTheOldWay).to receive(:call).with(command_params, options, pact_broker_client_options) + subject + end + end + + it "returns a result and message" do + expect(subject.success).to be true + expect(subject.message).to include("some notice") + end + + it "colourises the notices" do + expect(PactBroker::Client::ColorizeNotices).to receive(:call).with([OpenStruct.new(text: "some notice", type: "info")]).and_return("coloured notices") + expect(subject.message).to eq "coloured notices" + end + + context "when the output is json" do + let(:options) { { output: "json" } } + + it "returns the raw response" do + expect(subject.message).to eq post_response_body.to_json + end + end + + context "when there is an error retrieving the index" do + let(:index_status) { 500 } + let(:index_body_hash) { { "some" => "error" }} + + it "returns an error result with the response body" do + expect(subject.success).to be false + expect(subject.message).to match(/some.*error/) + end + end + + context "when there is an error response from publishing" do + let(:publish_status) { 400 } + let(:post_response_body) do + { + "some" => "error" + } + end + + it "returns an error result with the response body" do + expect(subject.success).to be false + expect(subject.message).to match(/some.*error/) + end + + context "when the output is json" do + let(:options) { { output: "json" } } + + it "returns the raw response" do + expect(subject.message).to eq post_response_body.to_json + end + end + end + + context "when there is an error response from publishing" do + let(:publish_status) { 400 } + let(:post_response_body) do + { + "some" => "error" + } + end + + it "returns an error result with the response body" do + expect(subject.success).to be false + expect(subject.message).to match(/some.*error/) + end + end + end + end + end +end diff --git a/spec/pacts/pact_broker_client-pactflow.json b/spec/pacts/pact_broker_client-pactflow.json index bffebb21..b5ef8b41 100644 --- a/spec/pacts/pact_broker_client-pactflow.json +++ b/spec/pacts/pact_broker_client-pactflow.json @@ -6,6 +6,103 @@ "name": "PactFlow" }, "interactions": [ + { + "description": "a request for the index resource", + "providerState": "the pb:publish-provider-contract relation exists in the index resource", + "request": { + "method": "GET", + "path": "/", + "headers": { + "Accept": "application/hal+json" + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/hal+json;charset=utf-8" + }, + "body": { + "_links": { + "pf:publish-provider-contract": { + "href": "http://localhost:1235/HAL-REL-PLACEHOLDER-PF-PUBLISH-PROVIDER-CONTRACT-{provider}" + } + } + }, + "matchingRules": { + "$.body._links.pf:publish-provider-contract.href": { + "match": "regex", + "regex": "http:\\/\\/.*{provider}" + } + } + } + }, + { + "description": "a request to publish a provider contract", + "request": { + "method": "post", + "path": "/HAL-REL-PLACEHOLDER-PF-PUBLISH-PROVIDER-CONTRACT-Bar", + "headers": { + "Content-Type": "application/json", + "Accept": "application/hal+json" + }, + "body": { + "pacticipantVersionNumber": "1", + "tags": [ + "dev" + ], + "branch": "main", + "buildUrl": "http://build", + "contract": { + "content": "LS0tCnNvbWU6IGNvbnRyYWN0Cg==", + "contentType": "application/yaml", + "specification": "oas", + "selfVerificationResults": { + "success": true, + "content": "c29tZSByZXN1bHRz", + "contentType": "text/plain", + "format": "text", + "verifier": "my custom tool", + "verifierVersion": "1.0" + } + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/hal+json;charset=utf-8" + }, + "body": { + "notices": [ + { + "text": "some notice", + "type": "info" + } + ], + "_embedded": { + "version": { + "number": "1" + } + }, + "_links": { + "pb:pacticipant-version-tags": [ + { + } + ], + "pb:branch-version": { + } + } + }, + "matchingRules": { + "$.body.notices": { + "min": 1 + }, + "$.body.notices[*].*": { + "match": "type" + } + } + } + }, { "description": "a request to create a provider contract", "request": { diff --git a/spec/service_providers/pact_helper.rb b/spec/service_providers/pact_helper.rb index 8b22f83e..eb3d710b 100644 --- a/spec/service_providers/pact_helper.rb +++ b/spec/service_providers/pact_helper.rb @@ -25,10 +25,12 @@ module PactBrokerPactHelperMethods + # @param [String] relation eg "pb:pacticipant" + # @param [Array] params eg ["Foo"] def placeholder_path(relation, params = []) path = "/HAL-REL-PLACEHOLDER-#{relation.gsub(':', '-').upcase}" if params.any? - joined_params = params.collect{ |param| "{#{param}}"}.join("-") + joined_params = params.join("-") path = "#{path}-#{joined_params}" end @@ -36,7 +38,19 @@ def placeholder_path(relation, params = []) end def placeholder_url(relation, params = [], mock_service = pact_broker) - "#{mock_service.mock_service_base_url}#{placeholder_path(relation, params)}" + "#{mock_service.mock_service_base_url}#{placeholder_path_for_term(relation, params)}" + end + + # @param [String] relation eg "pb:pacticipants" + # @param [Array] params eg ["pacticipant"] + def placeholder_path_for_term(relation, params = []) + path = "/HAL-REL-PLACEHOLDER-#{relation.gsub(':', '-').upcase}" + if params.any? + joined_params = params.collect{ |param| "{#{param}}"}.join("-") + path = "#{path}-#{joined_params}" + end + + path end def placeholder_url_term(relation, params = [], mock_service = pact_broker) diff --git a/spec/service_providers/pactflow_publish_provider_contract_spec.rb b/spec/service_providers/pactflow_publish_provider_contract_spec.rb index b15d875e..b3d417a6 100644 --- a/spec/service_providers/pactflow_publish_provider_contract_spec.rb +++ b/spec/service_providers/pactflow_publish_provider_contract_spec.rb @@ -1,11 +1,13 @@ +require "yaml" 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)) + allow_any_instance_of(PactBroker::Client::Hal::HttpClient).to receive(:sleep) + allow_any_instance_of(PactBroker::Client::Hal::HttpClient).to receive(:default_max_tries).and_return(1) + allow(ENV).to receive(:fetch).and_call_original + allow(ENV).to receive(:fetch).with("PACT_BROKER_FEATURES", "").and_return("publish_provider_contracts_all_in_one") end include_context "pact broker" @@ -17,6 +19,7 @@ provider_version_number: "1", branch_name: "main", tags: ["dev"], + build_url: "http://build", contract: { content: { "some" => "contract" }.to_yaml, content_type: "application/yaml", @@ -33,14 +36,17 @@ } end - let(:body) { { some: "body" }.to_json } - let(:request_body) do { + "pacticipantVersionNumber" => "1", + "tags" => ["dev"], + "branch" => "main", + "buildUrl" => "http://build", + "contract" => { "content" => "LS0tCnNvbWU6IGNvbnRyYWN0Cg==", - "contractType" => "oas", "contentType" => "application/yaml", - "verificationResults" => { + "specification" => "oas", + "selfVerificationResults" => { "success" => true, "content" => "c29tZSByZXN1bHRz", "contentType" => "text/plain", @@ -48,14 +54,36 @@ "verifier" => "my custom tool", "verifierVersion" => "1.0" } + } } end - let(:response_status) { 201 } + let(:response_status) { 200 } + + # Can't tell from the response if the buildUrl was correct, but it's not that important + # Add some assertions to the body to ensure we have called the endpoint correctly, + # not because we use the properties in the CLI output. + # There is unfortunately no good way to determine from the response whether or not + # we have correctly published the self verification results. let(:success_response) do { status: response_status, - headers: pact_broker_response_headers + headers: pact_broker_response_headers, + body: { + "notices" => Pact.each_like("text" => "some notice", "type" => "info"), + "_embedded" => { + "version" => { + # This tells us we have set the version number correctly + "number" => "1" + } + }, + "_links" => { + # The links tell us we have successfully created the tags, but we don't care about the contents + "pb:pacticipant-version-tags" => [{}], + # The link tells us we have successfully created the branch version, but we don't care about the contents + "pb:branch-version" => {}, + } + } } end @@ -74,54 +102,38 @@ context "creating a provider contract with valid parameters" do before do pactflow - .upon_receiving("a request to create a provider contract") + .given("the pb:publish-provider-contract relation exists in the index resource") + .upon_receiving("a request for the index resource") .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" - expect(subject.message).not_to include pactflow.mock_service_base_url - end - end - - context "creating a provider contract with valid parameters with pf:ui return results" do - let(:success_response_with_pf_ui_url) do - { - status: response_status, - headers: pact_broker_response_headers, - body: { "_links": { - "pf:ui": { - "href": Pact.like("some-url") + method: "GET", + path: "/", + headers: get_request_headers + ).will_respond_with( + status: 200, + headers: pact_broker_response_headers, + body: { + _links: { + :'pf:publish-provider-contract' => { + href: placeholder_url_term("pf:publish-provider-contract", ['provider'], pactflow) + } + } } - } } - } - end - before do + ) + pactflow - .given("there is a pf:ui href in the response") - .upon_receiving("a request to create a provider contract") + .upon_receiving("a request to publish a provider contract") .with( - method: :put, - path: "/contracts/provider/Bar/version/1", - headers: put_request_headers, - body: request_body - ) - .will_respond_with(success_response_with_pf_ui_url) + method: :post, + path: placeholder_path("pf:publish-provider-contract", ["Bar"]), + headers: post_request_headers, + body: request_body + ).will_respond_with(success_response) end - it "returns a CommandResult with success = true and a provider contract ui url" do + 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" - expect(subject.message).to include "Next steps:" - expect(subject.message).to include "some-url" + expect(subject.message).to include "some notice" end end -end \ No newline at end of file +end diff --git a/spec/service_providers/pactflow_publish_provider_contract_the_old_way_spec.rb b/spec/service_providers/pactflow_publish_provider_contract_the_old_way_spec.rb new file mode 100644 index 00000000..0a08c823 --- /dev/null +++ b/spec/service_providers/pactflow_publish_provider_contract_the_old_way_spec.rb @@ -0,0 +1,129 @@ +require_relative "pact_helper" +require "yaml" +require "pactflow/client/provider_contracts/publish_the_old_way" +require "pact_broker/client/versions/create" + + +RSpec.describe "publishing a provider contract to PactFlow the old way", 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" => "LS0tCnNvbWU6IGNvbnRyYWN0Cg==", + "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::PublishTheOldWay.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" + expect(subject.message).not_to include pactflow.mock_service_base_url + end + end + + context "creating a provider contract with valid parameters with pf:ui return results" do + let(:success_response_with_pf_ui_url) do + { + status: response_status, + headers: pact_broker_response_headers, + body: { "_links": { + "pf:ui": { + "href": Pact.like("some-url") + } + } } + } + end + before do + pactflow + .given("there is a pf:ui href in the response") + .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_with_pf_ui_url) + end + + it "returns a CommandResult with success = true and a provider contract ui url" 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" + expect(subject.message).to include "Next steps:" + expect(subject.message).to include "some-url" + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9e40586c..113e155f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -33,6 +33,10 @@ def capture(stream) eval "$#{stream} = StringIO.new" yield result = eval("$#{stream}").string + rescue SystemExit => e + puts "CAUGHT SYSTEM EXIT" + puts e + puts e.backtrace ensure eval("$#{stream} = #{stream.upcase}") end