diff --git a/lib/pact/provider/pact_spec_runner.rb b/lib/pact/provider/pact_spec_runner.rb index d76df0dc..5757d030 100644 --- a/lib/pact/provider/pact_spec_runner.rb +++ b/lib/pact/provider/pact_spec_runner.rb @@ -8,6 +8,7 @@ require 'pact/provider/pact_source' require 'pact/provider/help/write' require 'pact/provider/verification_results/publish_all' +require 'pact/provider/rspec/pact_broker_formatter' require_relative 'rspec' @@ -61,6 +62,8 @@ def configure_rspec config.output_stream = Pact.configuration.output_stream end + ::RSpec.configuration.add_formatter Pact::Provider::RSpec::PactBrokerFormatter, StringIO.new + if options[:format] ::RSpec.configuration.add_formatter options[:format] # Don't want to mess up the JSON parsing with messages to stdout, so send it to stderr @@ -82,14 +85,12 @@ def configure_rspec Pact.configuration.provider.app end + # For the Pact::Provider::RSpec::PactBrokerFormatter + Pact.provider_world.pact_sources = pact_sources jsons = pact_jsons - sources = pact_sources config.after(:suite) do | suite | Pact::Provider::Help::Write.call(jsons) - Pact::RSpec.with_rspec_3 do - Pact::Provider::VerificationResults::PublishAll.call(sources, ::RSpec.configuration.reporter.failed_examples) - end end end diff --git a/lib/pact/provider/rspec.rb b/lib/pact/provider/rspec.rb index 3a4c2d83..dec09a91 100644 --- a/lib/pact/provider/rspec.rb +++ b/lib/pact/provider/rspec.rb @@ -25,7 +25,7 @@ def honour_pactfile pact_uri, pact_json, options Pact.configuration.output_stream.puts "INFO: Filtering interactions by: #{options[:criteria]}" if options[:criteria] && options[:criteria].any? consumer_contract = Pact::ConsumerContract.from_json(pact_json) ::RSpec.describe "Verifying a pact between #{consumer_contract.consumer.name} and #{consumer_contract.provider.name}", pactfile_uri: pact_uri do - honour_consumer_contract consumer_contract, options.merge(pact_json: pact_json) + honour_consumer_contract consumer_contract, options.merge(pact_json: pact_json, pact_uri: pact_uri) end end @@ -61,11 +61,16 @@ def describe_interaction_with_provider_state interaction, options def describe_interaction interaction, options + # pact_uri, pact_provider_state and pact_description are used by + # Pact::Provider::RSpec::PactBrokerFormatter metadata = { pact: :verify, pact_interaction: interaction, pact_interaction_example_description: interaction_description_for_rerun_command(interaction), - pact_json: options[:pact_json] + pact_json: options[:pact_json], + pact_uri: options[:pact_uri], + pact_provider_state: interaction.provider_state, + pact_description: interaction.description } describe description_for(interaction), metadata do diff --git a/lib/pact/provider/rspec/pact_broker_formatter.rb b/lib/pact/provider/rspec/pact_broker_formatter.rb new file mode 100644 index 00000000..e026e1de --- /dev/null +++ b/lib/pact/provider/rspec/pact_broker_formatter.rb @@ -0,0 +1,66 @@ +require 'rspec/core/formatters' +require 'pact/provider/verification_results/publish_all' + +module Pact + module Provider + module RSpec + class PactBrokerFormatter < ::RSpec::Core::Formatters::BaseFormatter + Pact::RSpec.with_rspec_3 do + ::RSpec::Core::Formatters.register self, :message, :dump_summary, :stop, :seed, :close + end + + attr_reader :output_hash + + def initialize(output) + super + @output_hash = { + :version => ::RSpec::Core::Version::STRING + } + end + + def message(notification) + (@output_hash[:messages] ||= []) << notification.message + end + + def dump_summary(summary) + end + + def stop(notification) + @output_hash[:examples] = notification.examples.map do |example| + format_example(example).tap do |hash| + e = example.exception + if e + hash[:exception] = { + class: e.class.name, + message: e.message + } + end + end + end + end + + def seed(notification) + return unless notification.seed_used? + @output_hash[:seed] = notification.seed + end + + def close(_notification) + Pact::Provider::VerificationResults::PublishAll.call(Pact.provider_world.pact_sources, output_hash) + end + + private + + def format_example(example) + { + exampleDescription: example.description, + fullDescription: example.full_description, + status: example.execution_result.status.to_s, + interactionProviderState: example.metadata[:pact_provider_state], + interactionDescription: example.metadata[:pact_description], + pact_uri: example.metadata[:pact_uri] + } + end + end + end + end +end diff --git a/lib/pact/provider/verification_results/create.rb b/lib/pact/provider/verification_results/create.rb index 0d214f9f..62e5b195 100644 --- a/lib/pact/provider/verification_results/create.rb +++ b/lib/pact/provider/verification_results/create.rb @@ -4,38 +4,54 @@ module Provider module VerificationResults class Create - def self.call pact_json, failed_examples - new(pact_json, failed_examples).call + def self.call pact_source, test_results_hash + new(pact_source, test_results_hash).call end - def initialize pact_json, failed_examples - @pact_json = pact_json - @failed_examples = failed_examples + def initialize pact_source, test_results_hash + @pact_source = pact_source + @test_results_hash = test_results_hash end def call - VerificationResult.new(!any_failures?, Pact.configuration.provider.application_version) + VerificationResult.new(!any_failures?, Pact.configuration.provider.application_version, test_results_hash_for_pact_uri) end private - def pact_hash - @pact_hash ||= json_load(pact_json) + def pact_uri + @pact_uri ||= pact_source.uri end - def json_load json - JSON.load(json, nil, { max_nesting: 50 }) + def any_failures? + count_failures_for_pact_uri > 0 end - def count_failures_for_pact_json - failed_examples.collect{ |e| e.metadata[:pact_json] == pact_json }.uniq.size + def examples_for_pact_uri + @examples_for_pact_uri ||= test_results_hash[:examples] + .select{ |e| e[:pact_uri] == pact_uri } + .collect{ |e| clean_example(e) } end - def any_failures? - count_failures_for_pact_json > 0 + def count_failures_for_pact_uri + examples_for_pact_uri.count{ |e| e[:status] != 'passed' } + end + + def test_results_hash_for_pact_uri + { + examples: examples_for_pact_uri, + summary: { + exampleCount: examples_for_pact_uri.size, + failureCount: count_failures_for_pact_uri + } + } + end + + def clean_example(example) + example.reject{ |k, v| k == :pact_uri } end - attr_reader :pact_json, :failed_examples + attr_reader :pact_source, :test_results_hash end end end diff --git a/lib/pact/provider/verification_results/publish.rb b/lib/pact/provider/verification_results/publish.rb index 9eb3a0a2..e4c654af 100644 --- a/lib/pact/provider/verification_results/publish.rb +++ b/lib/pact/provider/verification_results/publish.rb @@ -73,7 +73,7 @@ def tag_versions def publish uri = URI(publication_url) - request = build_request('Post', uri, verification_result.to_json, "Publishing verification result #{verification_result.to_json} to") + request = build_request('Post', uri, verification_result.to_json, "Publishing verification result #{verification_result} to") response = nil begin options = {:use_ssl => uri.scheme == 'https'} @@ -81,12 +81,17 @@ def publish http.request request end rescue StandardError => e - error_message = "Failed to publish verification result due to: #{e.class} #{e.message}" + error_message = "Failed to publish verification results due to: #{e.class} #{e.message}" raise PublicationError.new(error_message) end - unless response.code.start_with?("2") - raise PublicationError.new("Error returned from verification result publication #{response.code} #{response.body}") + + + if response.code.start_with?("2") + new_resource_url = JSON.parse(response.body)['_links']['self']['href'] + $stdout.puts "INFO: Verification results published to #{new_resource_url}" + else + raise PublicationError.new("Error returned from verification results publication #{response.code} #{response.body}") end end diff --git a/lib/pact/provider/verification_results/publish_all.rb b/lib/pact/provider/verification_results/publish_all.rb index 7c2721fe..0c17ac43 100644 --- a/lib/pact/provider/verification_results/publish_all.rb +++ b/lib/pact/provider/verification_results/publish_all.rb @@ -6,13 +6,13 @@ module Provider module VerificationResults class PublishAll - def self.call pact_sources, rspec_summary - new(pact_sources, rspec_summary).call + def self.call pact_sources, test_results_hash + new(pact_sources, test_results_hash).call end - def initialize pact_sources, rspec_summary + def initialize pact_sources, test_results_hash @pact_sources = pact_sources - @rspec_summary = rspec_summary + @test_results_hash = test_results_hash end # TODO do not publish unless all interactions have been run @@ -26,11 +26,11 @@ def call def verification_results pact_sources.collect do | pact_source | - [pact_source, Create.call(pact_source.pact_json, rspec_summary)] + [pact_source, Create.call(pact_source, test_results_hash)] end end - attr_reader :pact_sources, :rspec_summary + attr_reader :pact_sources, :test_results_hash end end end diff --git a/lib/pact/provider/verification_results/verification_result.rb b/lib/pact/provider/verification_results/verification_result.rb index f5a4deae..9e29008f 100644 --- a/lib/pact/provider/verification_results/verification_result.rb +++ b/lib/pact/provider/verification_results/verification_result.rb @@ -1,11 +1,14 @@ +require 'json' + module Pact module Provider module VerificationResults class VerificationResult - def initialize success, provider_application_version + def initialize success, provider_application_version, test_results_hash @success = success @provider_application_version = provider_application_version + @test_results_hash = test_results_hash end def provider_application_version_set? @@ -15,13 +18,18 @@ def provider_application_version_set? def to_json { success: success, - providerApplicationVersion: provider_application_version + providerApplicationVersion: provider_application_version, + testResults: test_results_hash }.to_json end + def to_s + "[success: #{success}, providerApplicationVersion: #{provider_application_version}]" + end + private - attr_reader :success, :provider_application_version + attr_reader :success, :provider_application_version, :test_results_hash end end end diff --git a/lib/pact/provider/world.rb b/lib/pact/provider/world.rb index 1792a51c..4f812d00 100644 --- a/lib/pact/provider/world.rb +++ b/lib/pact/provider/world.rb @@ -14,6 +14,8 @@ def self.clear_provider_world module Provider class World + attr_accessor :pact_sources + def provider_states @provider_states_proxy ||= Pact::Provider::State::ProviderStateProxy.new end @@ -29,7 +31,6 @@ def pact_verifications def pact_urls pact_verifications.collect(&:uri) end - end end end \ No newline at end of file diff --git a/spec/integration/publish_verification_spec.rb b/spec/integration/publish_verification_spec.rb new file mode 100644 index 00000000..666a942e --- /dev/null +++ b/spec/integration/publish_verification_spec.rb @@ -0,0 +1,68 @@ +require 'pact/provider/verification_results/publish_all' +require 'pact/provider/pact_uri' + +describe "publishing verifications" do + + before do + allow(Pact.configuration).to receive(:provider).and_return(provider_configuration) + allow($stdout).to receive(:puts) + end + + let(:provider_configuration) do + double('provider_configuration', + application_version: '1.2.3', + publish_verification_results?: true, + tags: []) + end + + let(:pact_sources) do + [instance_double('Pact::Provider::PactSource', pact_hash: pact_hash, uri: pact_uri)] + end + + let(:pact_uri) do + instance_double('Pact::Provider::PactURI', uri: 'pact.json', basic_auth?: false) + end + + let(:pact_hash) do + { + '_links' => { + 'pb:publish-verification-results' => { + 'href' => 'http://publish/' + } + } + } + end + + let(:created_verification_body) do + { + '_links' => { + 'self' => { + 'href' => 'http://created' + } + } + }.to_json + end + + let(:test_results_hash) do + { + examples: [ + { + exampleDescription: '1', + status: 'passed', + pact_uri: pact_uri + } + ] + } + end + + subject { Pact::Provider::VerificationResults::PublishAll.call(pact_sources, test_results_hash) } + + let!(:request) do + stub_request(:post, 'http://publish').to_return(status: 200, body: created_verification_body) + end + + it "publishes the results" do + subject + expect(request).to have_been_made + end +end diff --git a/spec/lib/pact/provider/verification_results/publish_spec.rb b/spec/lib/pact/provider/verification_results/publish_spec.rb index cd7988b1..58265240 100644 --- a/spec/lib/pact/provider/verification_results/publish_spec.rb +++ b/spec/lib/pact/provider/verification_results/publish_spec.rb @@ -23,6 +23,15 @@ module VerificationResults } } end + let(:created_verification_body) do + { + '_links' => { + 'self' => { + 'href' => 'http://broker/new-verification' + } + } + }.to_json + end let(:app_version_set) { false } let(:verification_json) { '{"foo": "bar"}' } let(:publish_verification_results) { false } @@ -41,11 +50,11 @@ module VerificationResults before do allow($stdout).to receive(:puts) allow(Pact.configuration).to receive(:provider).and_return(provider_configuration) - stub_request(:post, 'http://broker/verifications') + stub_request(:post, 'http://broker/verifications').to_return(status: 200, body: created_verification_body) stub_request(:put, /tag-me/) end - subject { Publish.call(pact_source, verification)} + subject { Publish.call(pact_source, verification) } context "when publish_verification_results is false" do it "does not publish the verification" do @@ -121,7 +130,7 @@ module VerificationResults context "with https" do before do - stub_request(:post, publish_verification_url) + stub_request(:post, publish_verification_url).to_return(status: 200, body: created_verification_body) end let(:publish_verification_url) { 'https://broker/verifications' } diff --git a/spec/support/bar_fail_pact_helper.rb b/spec/support/bar_fail_pact_helper.rb new file mode 100644 index 00000000..3917ef47 --- /dev/null +++ b/spec/support/bar_fail_pact_helper.rb @@ -0,0 +1,27 @@ +require 'json' +require 'pact/provider/rspec' + +module Pact + module Test + class BarApp + def call env + [200, {'Content-Type' => 'application/json'}, [].to_json] + end + end + + Pact.configure do | config | + config.logger.level = Logger::DEBUG + end + + Pact.service_provider "Bar" do + app { BarApp.new } + app_version '1.2.3' + app_version_tags ['master'] + publish_verification_results true + + honours_pact_with 'Foo' do + pact_uri './spec/support/foo-bar.json' + end + end + end +end diff --git a/tasks/foo-bar.rake b/tasks/foo-bar.rake index b359f6d8..d6fc8385 100644 --- a/tasks/foo-bar.rake +++ b/tasks/foo-bar.rake @@ -30,6 +30,10 @@ Pact::VerificationTask.new(:foobar_using_broker) do | pact | pact.uri "#{BROKER_BASE_URL}/pacts/provider/Bar/consumer/Foo/version/1.0.0", :pact_helper => './spec/support/bar_pact_helper.rb' end +Pact::VerificationTask.new('foobar_using_broker:fail') do | pact | + pact.uri "#{BROKER_BASE_URL}/pacts/provider/Bar/consumer/Foo/version/1.0.0", :pact_helper => './spec/support/bar_fail_pact_helper.rb' +end + task 'pact:verify:foobar' => ['pact:foobar:create'] task 'pact:verify:foobar_using_broker' => ['pact:foobar:create', 'pact:foobar:publish']