Skip to content

Commit

Permalink
feat(publish pacts): merge pact files with same consumer/provider bef…
Browse files Browse the repository at this point in the history
…ore publishing
  • Loading branch information
bethesque committed Sep 28, 2017
1 parent b614fa8 commit 1c039a0
Show file tree
Hide file tree
Showing 10 changed files with 219 additions and 43 deletions.
5 changes: 5 additions & 0 deletions lib/pact_broker/client/error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module PactBroker
module Client
class Error < StandardError; end
end
end
57 changes: 57 additions & 0 deletions lib/pact_broker/client/merge_pacts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
require 'json'
require 'pact_broker/client/error'

module PactBroker
module Client

class PactMergeError < PactBroker::Client::Error; end

module MergePacts
extend self

def call pact_hashes
pact_hashes.reduce{|p1, p2| merge(p1, p2) }
end

# Accepts two hashes representing pacts, outputs a merged hash
# Does not make any guarantees about order of interactions
def merge original, additional
new_pact = JSON.parse(original.to_json, symbolize_names: true)

additional[:interactions].each do |new_interaction|
# check to see if this interaction matches an existing interaction
overwrite_index = original[:interactions].find_index do |original_interaction|
same_description_and_state?(original_interaction, new_interaction)
end

# overwrite existing interaction if a match is found, otherwise appends the new interaction
if overwrite_index
if new_interaction == original[:interactions][overwrite_index]
new_pact[:interactions][overwrite_index] = new_interaction
else
raise PactMergeError, almost_duplicate_message(original[:interactions][overwrite_index], new_interaction)
end
else
new_pact[:interactions] << new_interaction
end
end

new_pact
end

private

def almost_duplicate_message(original, new_interaction)
"An interaction with same description (#{new_interaction[:description].inspect}) and provider state (#{new_interaction[:providerState].inspect}) but a different request or response has already been used. " +
"Please use a different description or provider state, or hard-code any random data.\n" +
original.to_json + "\n\n"
new_interaction.to_json
end

def same_description_and_state? original, additional
original[:description] == additional[:description] &&
original[:providerState] == additional[:providerState]
end
end
end
end
15 changes: 10 additions & 5 deletions lib/pact_broker/client/pact_file.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'json'
require 'pact_broker/client/pact_hash'

module PactBroker
module Client
Expand All @@ -7,20 +8,24 @@ def initialize path
@path = path
end

def path
@path
end

def pact_name
"#{consumer_name}/#{provider_name} pact"
pact_hash.pact_name
end

def consumer_name
pact_contents[:consumer][:name]
pact_hash.consumer_name
end

def provider_name
pact_contents[:provider][:name]
pact_hash.provider_name
end

def pact_contents
@contents ||= JSON.parse(read, symbolize_names: true)
def pact_hash
@pact_hash ||= PactHash[JSON.parse(read, symbolize_names: true)]
end

def read
Expand Down
19 changes: 19 additions & 0 deletions lib/pact_broker/client/pact_hash.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require 'json'

module PactBroker
module Client
class PactHash < ::Hash
def pact_name
"#{consumer_name}/#{provider_name} pact"
end

def consumer_name
self[:consumer][:name]
end

def provider_name
self[:provider][:name]
end
end
end
end
13 changes: 7 additions & 6 deletions lib/pact_broker/client/pacts.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
require_relative 'base_client'
require 'pact_broker/client/pact_hash'

module PactBroker
module Client
class Pacts < BaseClient

def publish options
consumer_version = options[:consumer_version]
pact_string = options[:pact_json]
consumer_contract = ::Pact::ConsumerContract.from_json pact_string
url = save_consumer_contract_url consumer_contract, consumer_version
pact_hash = options[:pact_hash]
pact_string = pact_hash.to_json
url = save_consumer_contract_url pact_hash, consumer_version

if @client_options[:write] == :merge
response = self.class.patch(url, body: pact_string, headers: default_patch_headers)
Expand Down Expand Up @@ -126,9 +127,9 @@ def get_consumer_contract_url options
"/pacts/provider/#{provider_name}/consumer/#{consumer_name}/version/#{consumer_version}"
end

def save_consumer_contract_url consumer_contract, consumer_version
consumer_name = encode_param(consumer_contract.consumer.name)
provider_name = encode_param(consumer_contract.provider.name)
def save_consumer_contract_url pact_hash, consumer_version
consumer_name = encode_param(pact_hash.consumer_name)
provider_name = encode_param(pact_hash.provider_name)
version = encode_param(consumer_version)
"/pacts/provider/#{provider_name}/consumer/#{consumer_name}/version/#{version}"
end
Expand Down
27 changes: 16 additions & 11 deletions lib/pact_broker/client/publish_pacts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
require 'pact_broker/client'
require 'pact_broker/client/retry'
require 'pact_broker/client/pact_file'
require 'pact_broker/client/pact_hash'
require 'pact_broker/client/merge_pacts'

module PactBroker
module Client
Expand Down Expand Up @@ -37,22 +39,25 @@ def pact_broker_client

def publish_pacts
pact_files.group_by(&:pact_name).collect do | pact_name, pact_files |
pact_files.collect do | pact_file |
publish_pact(pact_file)
end
end.flatten.all?
$stdout.puts "Merging #{pact_files.collect(&:path).join(", ")}" if pact_files.size > 1
publish_pact(PactHash[merge_contents(pact_files)])
end.all?
end

def merge_contents(pact_files)
MergePacts.call(pact_files.collect(&:pact_hash))
end

def pact_files
@pact_files ||= pact_file_paths.collect{ |pact_file_path| PactFile.new(pact_file_path) }
end

def publish_pact pact_file
def publish_pact pact
begin
$stdout.puts ">> Publishing #{pact_file.pact_name} to pact broker at #{pact_broker_base_url}"
publish_pact_contents pact_file
$stdout.puts "Publishing #{pact.pact_name} to pact broker at #{pact_broker_base_url}"
publish_pact_contents pact
rescue => e
$stderr.puts "Failed to publish #{pact_file.pact_name} due to error: #{e.class} - #{e}"
$stderr.puts "Failed to publish #{pact.pact_name} due to error: #{e.class} - #{e}"
false
end
end
Expand All @@ -76,14 +81,14 @@ def tag_consumer_version tag
false
end

def publish_pact_contents(pact_file)
def publish_pact_contents(pact)
Retry.until_true do
pacts = pact_broker_client.pacticipants.versions.pacts
if pacts.version_published?(consumer: pact_file.consumer_name, provider: pact_file.provider_name, consumer_version: consumer_version)
if pacts.version_published?(consumer: pact.consumer_name, provider: pact.provider_name, consumer_version: consumer_version)
$stdout.puts ::Term::ANSIColor.yellow("The given version of pact is already published. Will Overwrite...")
end

latest_pact_url = pacts.publish(pact_json: pact_file.read, consumer_version: consumer_version)
latest_pact_url = pacts.publish(pact_hash: pact, consumer_version: consumer_version)
$stdout.puts "The latest version of this pact can be accessed at the following URL (use this to configure the provider verification):\n#{latest_pact_url}"
true
end
Expand Down
69 changes: 69 additions & 0 deletions spec/lib/pact_broker/client/merge_pacts_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
require 'pact_broker/client/merge_pacts'

module PactBroker
module Client
describe MergePacts do
describe ".call" do
let(:pact_hash_1) do
{
other: 'info',
interactions: [
{providerState: 1, description: 1, foo: 'bar' }
]
}
end

let(:pact_hash_2) do
{
interactions: [
{providerState: 2, description: 2, foo: 'wiffle' }
]
}
end

let(:pact_hash_3) do
{
interactions: [
{providerState: 3, description: 3, foo: 'meep' },
{providerState: 1, description: 1, foo: 'bar' }
]
}
end

let(:pact_hashes) { [pact_hash_1, pact_hash_2, pact_hash_3] }

let(:expected_merge) do
{
other: 'info',
interactions: [
{providerState: 1, description: 1, foo: 'bar' },
{providerState: 2, description: 2, foo: 'wiffle' },
{providerState: 3, description: 3, foo: 'meep' }
]
}
end

subject { MergePacts.call(pact_hashes) }

it "merges the interactions by consumer/provider" do
expect(subject).to eq expected_merge
end

context "when an interaction is found with the same state and description but has a difference elsewhere" do
let(:pact_hash_3) do
{
interactions: [
{providerState: 3, description: 3, foo: 'meep' },
{providerState: 1, description: 1, foo: 'different' }
]
}
end

it "raises an error" do
expect { subject }.to raise_error PactMergeError
end
end
end
end
end
end
52 changes: 33 additions & 19 deletions spec/lib/pact_broker/client/publish_pacts_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,24 @@ module PactBroker
module Client
describe PublishPacts do

# The amount of stubbing that we have to do here indicates this class is doing
# TOO MUCH and needs to be split up!
before do
FakeFS.activate!
allow(pacts_client).to receive(:publish).and_return(latest_pact_url)
allow(PactBroker::Client::PactBrokerClient).to receive(:new).with(base_url: pact_broker_base_url, client_options: pact_broker_client_options).and_return(pact_broker_client)
allow(PactBroker::Client::PactBrokerClient).to receive(:new)
.with(base_url: pact_broker_base_url, client_options: pact_broker_client_options)
.and_return(pact_broker_client)
allow(pact_broker_client).to receive_message_chain(:pacticipants, :versions).and_return(pact_versions_client)
allow(pact_broker_client).to receive_message_chain(:pacticipants, :versions, :pacts).and_return(pacts_client)
allow(pacts_client).to receive(:version_published?).and_return(false)
allow($stdout).to receive(:puts)
allow(Retry).to receive(:sleep)
allow(MergePacts).to receive(:call) { | pact_hashes | pact_hashes[0] }
FileUtils.mkdir_p "spec/pacts"
File.open("spec/pacts/consumer-provider.json", "w") { |file| file << pact_hash.to_json }
File.open("spec/pacts/consumer-provider-2.json", "w") { |file| file << pact_hash.to_json }
File.open("spec/pacts/foo-bar.json", "w") { |file| file << pact_hash_2.to_json }
end

after do
Expand All @@ -25,6 +37,7 @@ module Client
let(:consumer_version) { "1.2.3" }
let(:tags) { nil }
let(:pact_hash) { {consumer: {name: 'Consumer'}, provider: {name: 'Provider'}, interactions: [] } }
let(:pact_hash_2) { {consumer: {name: 'Foo'}, provider: {name: 'Bar'}, interactions: [] } }
let(:pacts_client) { instance_double("PactBroker::ClientSupport::Pacts")}
let(:pact_versions_client) { instance_double("PactBroker::Client::Versions", tag: false) }
let(:pact_broker_base_url) { 'http://some-host'}
Expand All @@ -39,18 +52,10 @@ module Client

subject { PublishPacts.new(pact_broker_base_url, pact_file_paths, consumer_version, tags, pact_broker_client_options) }

before do
FileUtils.mkdir_p "spec/pacts"
File.open("spec/pacts/consumer-provider.json", "w") { |file| file << pact_hash.to_json }
allow(pact_broker_client).to receive_message_chain(:pacticipants, :versions).and_return(pact_versions_client)
allow(pact_broker_client).to receive_message_chain(:pacticipants, :versions, :pacts).and_return(pacts_client)
allow(pacts_client).to receive(:version_published?).and_return(false)
end

describe "call" do

it "uses the pact_broker client to publish the given pact" do
expect(pacts_client).to receive(:publish).with(pact_json: pact_hash.to_json, consumer_version: consumer_version)
expect(pacts_client).to receive(:publish).with(pact_hash: pact_hash, consumer_version: consumer_version)
subject.call
end

Expand All @@ -66,16 +71,23 @@ module Client
end
end

context "when publishing multiple files with the same consumer/provider" do
let(:pact_file_paths) { ['spec/pacts/consumer-provider.json','spec/pacts/consumer-provider-2.json']}
it "merges the files" do
expect(MergePacts).to receive(:call).with([pact_hash, pact_hash])
subject.call
end
end

context "when publishing one or more pacts fails" do
let(:pact_file_paths) { ['spec/pacts/consumer-provider.json','spec/pacts/consumer-provider.json']}
let(:pact_file_paths) { ['spec/pacts/consumer-provider.json','spec/pacts/foo-bar.json']}

before do
count = 0
allow(pacts_client).to receive(:publish) do | args |
count += 1
raise "test error" if count <= 3
latest_pact_url
end
allow(pacts_client).to receive(:publish).with(
pact_hash: pact_hash,
consumer_version: consumer_version
).and_raise("an error")

allow($stderr).to receive(:puts)
end

Expand All @@ -85,7 +97,8 @@ module Client
end

it "continues publishing the rest" do
expect(pacts_client).to receive(:publish).with(pact_json: pact_hash.to_json, consumer_version: consumer_version)
expect(pacts_client).to receive(:publish).with(
pact_hash: pact_hash_2, consumer_version: consumer_version)
subject.call
end

Expand Down Expand Up @@ -119,7 +132,8 @@ module Client
let(:tags) { ["dev"] }

it "tags the consumer version" do
expect(pact_versions_client).to receive(:tag).with({pacticipant: "Consumer", version: consumer_version, tag: "dev"})
expect(pact_versions_client).to receive(:tag).with({pacticipant: "Consumer",
version: consumer_version, tag: "dev"})
subject.call
end

Expand Down
2 changes: 1 addition & 1 deletion spec/service_providers/pact_broker_client_publish_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module PactBroker::Client

describe "publishing a pact" do

let(:options) { { pact_json: pact_json, consumer_version: consumer_version }}
let(:options) { { pact_hash: pact_hash, consumer_version: consumer_version }}
let(:location) { 'http://example.org/pacts/provider/Pricing%20Service/consumer/Condor/latest' }
context "when the provider already exists in the pact-broker" do

Expand Down
Loading

0 comments on commit 1c039a0

Please sign in to comment.