Skip to content

Commit e16feef

Browse files
committed
feat(pacts for verification): allow all versions for a particular tag to be verified (eg. all prod versions of a mobile consumer)
1 parent cd63be0 commit e16feef

8 files changed

+197
-43
lines changed

lib/pact_broker/api/contracts/verifiable_pacts_json_query_schema.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class VerifiablePactsJSONQuerySchema
1818
optional(:consumerVersionSelectors).each do
1919
schema do
2020
required(:tag).filled(:str?)
21-
required(:latest).filled(included_in?: [true])
21+
optional(:latest).filled(included_in?: [true, false])
2222
end
2323
end
2424
optional(:includePendingStatus).filled(included_in?: [true, false])

lib/pact_broker/api/contracts/verifiable_pacts_query_schema.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class VerifiablePactsQuerySchema
1717
optional(:consumer_version_selectors).each do
1818
schema do
1919
required(:tag).filled(:str?)
20-
required(:latest).filled(included_in?: ["true"])
20+
optional(:latest).filled(included_in?: ["true", "false"])
2121
end
2222
end
2323
optional(:include_pending_status).filled(included_in?: ["true", "false"])

lib/pact_broker/pacts/pact_publication.rb

+53
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,63 @@ class PactPublication < Sequel::Model(:pact_publications)
2525
dataset_module do
2626
include PactBroker::Repositories::Helpers
2727

28+
def remove_overridden_revisions
29+
join(:latest_pact_publication_ids_for_consumer_versions, { Sequel[:lp][:pact_publication_id] => Sequel[:pact_publications][:id] }, { table_alias: :lp})
30+
end
31+
32+
def join_consumer_versions(table_alias = :cv)
33+
join(:versions, { Sequel[:pact_publications][:consumer_version_id] => Sequel[table_alias][:id] }, { table_alias: table_alias })
34+
end
35+
36+
def join_consumer_version_tags(table_alias = :ct)
37+
join(:tags, { Sequel[table_alias][:version_id] => Sequel[:pact_publications][:consumer_version_id]}, { table_alias: table_alias })
38+
end
39+
40+
def join_consumer_version_tags_with_names(consumer_version_tag_names)
41+
join(:tags, {
42+
Sequel[:ct][:version_id] => Sequel[:pact_publications][:consumer_version_id],
43+
Sequel[:ct][:name] => consumer_version_tag_names
44+
}, {
45+
table_alias: :ct
46+
})
47+
end
48+
49+
def join_providers(table_alias = :providers)
50+
join(:pacticipants, { Sequel[:pact_publications][:provider_id] => Sequel[table_alias][:id] }, { table_alias: table_alias })
51+
end
52+
53+
def join_consumers(table_alias = :consumers)
54+
join(:pacticipants, { Sequel[:pact_publications][:consumer_id] => Sequel[table_alias][:id] }, { table_alias: table_alias })
55+
end
56+
57+
def join_pact_versions
58+
join(:pact_versions, { Sequel[:pact_publications][:pact_version_id] => Sequel[:pact_versions][:id] })
59+
end
60+
61+
def eager_load_pact_versions
62+
eager(:pact_versions)
63+
end
64+
2865
def tag tag_name
2966
filter = name_like(Sequel.qualify(:tags, :name), tag_name)
3067
join(:tags, {version_id: :consumer_version_id}).where(filter)
3168
end
69+
70+
def provider_name_like(name)
71+
where(name_like(Sequel[:providers][:name], name))
72+
end
73+
74+
def consumer_version_tag(tag)
75+
where(Sequel[:ct][:name] => tag)
76+
end
77+
78+
def order_by_consumer_name
79+
order_append_ignore_case(Sequel[:consumers][:name])
80+
end
81+
82+
def order_by_consumer_version_order
83+
order_append(Sequel[:cv][:order])
84+
end
3285
end
3386

3487
def before_create

lib/pact_broker/pacts/repository.rb

+81-27
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@
1313
require 'pact_broker/matrix/head_row'
1414
require 'pact_broker/pacts/latest_pact_publication_id_for_consumer_version'
1515
require 'pact_broker/pacts/verifiable_pact'
16+
require 'pact_broker/repositories/helpers'
1617

1718
module PactBroker
1819
module Pacts
1920
class Repository
2021

2122
include PactBroker::Logging
2223
include PactBroker::Repositories
24+
include PactBroker::Repositories::Helpers
2325

2426
def create params
2527
pact_version = find_or_create_pact_version(
@@ -125,36 +127,60 @@ def find_latest_pact_versions_for_provider provider_name, tag = nil
125127
end
126128
end
127129

130+
def find_all_pact_versions_for_provider_with_tags provider_name, consumer_version_tag_names
131+
provider = pacticipant_repository.find_by_name(provider_name)
132+
133+
PactPublication
134+
.select_all_qualified
135+
.select_append(Sequel[:cv][:order].as(:consumer_version_order))
136+
.remove_overridden_revisions
137+
.join_consumer_versions(:cv)
138+
.join_consumer_version_tags_with_names(consumer_version_tag_names)
139+
.where(provider: provider)
140+
.eager(:consumer)
141+
.eager(:consumer_version)
142+
.eager(:provider)
143+
.eager(:pact_version)
144+
.all
145+
.group_by(&:pact_version_id)
146+
.values
147+
.collect{ | pacts| pacts.sort_by{|pact| pact.values.fetch(:consumer_version_order) }.last }
148+
.collect(&:to_domain)
149+
end
150+
128151
# To find the work in progress pacts for this verification execution:
129152
# For each provider tag that will be applied to this verification result (usually there will just be one, but
130153
# we have to allow for multiple tags),
131154
# find the head pacts (the pacts that are the latest for their tag) that have been successfully
132155
# verified against the provider tag.
133156
# Then, find all the head pacts, and remove the ones that have been successfully verified by ALL
134-
# of the provider tags supplied.
157+
# of the provider tags supplied, and the ones that were published before the include_wip_pacts_since date.
135158
# Then, for all of the head pacts that are remaining (these are the WIP ones) work out which
136159
# provider tags they are pending for.
137-
# Don't include pact publications that were created
160+
# Don't include pact publications that were created before the provider tag was first used
161+
# (that is, before the provider's git branch was created).
138162
def find_wip_pact_versions_for_provider provider_name, provider_tags_names = [], options = {}
163+
# TODO not sure about this
139164
return [] if provider_tags_names.empty?
140165

141166
provider = pacticipant_repository.find_by_name(provider_name)
142167

143-
# Hash of provider tag names => list of head pacts
144-
successfully_verified_head_pacts_for_provider_tags = find_successfully_verified_head_pacts_by_provider_tag(provider_name, provider_tags_names, options)
168+
# Hash of provider tag name => list of head pacts that have been successfully verified by that tag
169+
successfully_verified_head_pacts_for_provider_tags = find_successfully_verified_head_pacts_by_provider_tag(provider.id, provider_tags_names, options)
170+
# Create hash of provider tag name => list of pact publication ids
145171
successfully_verified_head_pact_publication_ids_for_each_provider_tag = successfully_verified_head_pacts_for_provider_tags.each_with_object({}) do | (provider_tag_name, head_pacts), hash |
146-
hash[provider_tag_name] = head_pacts.collect(&:id)
172+
hash[provider_tag_name] = head_pacts.collect(&:id).uniq
147173
end
148174

149-
# list of pact_publication_ids that are NOT work in progress
150-
head_pact_publication_ids_successully_verified_by_all_provider_tags = successfully_verified_head_pacts_for_provider_tags.values.collect{ |head_pacts| head_pacts.collect(&:id) }.reduce(:&)
175+
# list of head pact_publication_ids that are NOT work in progress because they've been verified by all of the provider version tags supplied
176+
non_wip_pact_publication_ids = successfully_verified_head_pacts_for_provider_tags.values.collect{ |head_pacts| head_pacts.collect(&:id) }.reduce(:&)
151177

152-
pact_publication_ids = find_head_pacts_that_have_not_been_successfully_verified_by_all_provider_tags(
153-
provider_name,
154-
head_pact_publication_ids_successully_verified_by_all_provider_tags,
178+
wip_pact_publication_ids = find_head_pacts_that_have_not_been_successfully_verified_by_all_provider_tags(
179+
provider.id,
180+
non_wip_pact_publication_ids,
155181
options)
156182

157-
pacts = AllPactPublications.where(id: pact_publication_ids).order_ignore_case(:consumer_name).order_append(:consumer_version_order)
183+
wip_pacts = AllPactPublications.where(id: wip_pact_publication_ids).order_ignore_case(:consumer_name).order_append(:consumer_version_order)
158184

159185
# The first instance (by date) of each provider tag with that name
160186
provider_tag_collection = PactBroker::Domain::Tag
@@ -166,7 +192,7 @@ def find_wip_pact_versions_for_provider provider_name, provider_tags_names = [],
166192
.where(name: provider_tags_names)
167193
.all
168194

169-
pacts.collect do | pact|
195+
wip_pacts.collect do | pact|
170196
pending_tag_names = find_provider_tags_for_which_pact_publication_id_is_pending(pact, successfully_verified_head_pact_publication_ids_for_each_provider_tag)
171197
pre_existing_tag_names = find_provider_tag_names_that_were_first_used_before_pact_published(pact, provider_tag_collection)
172198

@@ -313,13 +339,33 @@ def find_previous_pacts pact
313339

314340
# Returns a list of Domain::Pact objects the represent pact publications
315341
def find_for_verification(provider_name, consumer_version_selectors)
342+
find_pacts_for_which_the_latest_version_or_latest_version_for_the_tag_is_required(provider_name, consumer_version_selectors) +
343+
find_pacts_for_which_all_versions_for_the_tag_are_required(provider_name, consumer_version_selectors)
344+
end
345+
346+
private
347+
348+
def find_pacts_for_which_the_latest_version_or_latest_version_for_the_tag_is_required(provider_name, consumer_version_selectors)
349+
# The tags for which only the latest version is specified
316350
latest_tags = consumer_version_selectors.any? ?
317351
consumer_version_selectors.select(&:latest).collect(&:tag) :
318352
nil
353+
319354
find_latest_pact_versions_for_provider(provider_name, latest_tags)
320355
end
321356

322-
private
357+
def find_pacts_for_which_all_versions_for_the_tag_are_required(provider_name, consumer_version_selectors)
358+
# The tags for which all versions are specified
359+
all_tags = consumer_version_selectors.any? ?
360+
consumer_version_selectors.reject(&:latest).collect(&:tag) :
361+
nil
362+
363+
if all_tags
364+
find_all_pact_versions_for_provider_with_tags(provider_name, all_tags)
365+
else
366+
[]
367+
end
368+
end
323369

324370
def find_previous_distinct_pact_by_sha pact
325371
current_pact_content_sha =
@@ -389,27 +435,35 @@ def to_datetime string_or_datetime
389435
end
390436
end
391437

392-
def find_head_pacts_that_have_not_been_successfully_verified_by_all_provider_tags(provider_name, pact_publication_ids_successfully_verified_by_all_provider_tags, options)
438+
def find_head_pacts_that_have_not_been_successfully_verified_by_all_provider_tags(provider_id, pact_publication_ids_successfully_verified_by_all_provider_tags, options)
393439
# Exclude the head pacts that have been successfully verified by all the specified provider tags
394-
pact_publication_ids = LatestTaggedPactPublications
395-
.provider(provider_name)
396-
.exclude(id: pact_publication_ids_successfully_verified_by_all_provider_tags)
440+
LatestTaggedPactPublications
441+
.where(provider_id: provider_id)
397442
.where(Sequel.lit('latest_tagged_pact_publications.created_at > ?', options.fetch(:include_wip_pacts_since)))
443+
.exclude(id: pact_publication_ids_successfully_verified_by_all_provider_tags)
398444
.select_for_subquery(:id)
399445
end
400446

401-
# Find the head pacts that have been successfully verified by a provider version with the specified tags
402-
# Returns a Hash of provider_tag => LatestTaggedPactPublications with only id and tag_name populated
403-
def find_successfully_verified_head_pacts_by_provider_tag(provider_name, provider_tags, options)
447+
# Find the head pacts that have been successfully verified by a provider version with the specified
448+
# provider version tags.
449+
# Returns a Hash of provider_tag => LatestTaggedPactPublications with only pact publication id and tag_name populated
450+
# This is the list of pacts we are EXCLUDING from the WIP list because they have already been verified successfully
451+
def find_successfully_verified_head_pacts_by_provider_tag(provider_id, provider_tags, options)
404452
provider_tags.compact.each_with_object({}) do | provider_tag, hash |
453+
verifications_join = {
454+
pact_version_id: :pact_version_id,
455+
Sequel[:verifications][:success] => true,
456+
Sequel[:verifications][:provider_id] => provider_id
457+
}
458+
tags_join = {
459+
Sequel[:verifications][:provider_version_id] => Sequel[:provider_tags][:version_id],
460+
Sequel[:provider_tags][:name] => provider_tag
461+
}
405462
head_pacts = LatestTaggedPactPublications
406-
.join(:verifications, { pact_version_id: :pact_version_id })
407-
.join(:tags, { Sequel[:verifications][:provider_version_id] => Sequel[:provider_tags][:version_id] }, {table_alias: :provider_tags})
408-
.where(Sequel[:provider_tags][:name] => provider_tag)
409-
.provider(provider_name)
410-
.where(Sequel[:verifications][:success] => true)
411-
.or(Sequel.lit('latest_tagged_pact_publications.created_at < ?', options.fetch(:include_wip_pacts_since)))
412-
.select(Sequel[:latest_tagged_pact_publications][:id].as(:id), :tag_name)
463+
.select(Sequel[:latest_tagged_pact_publications][:id].as(:id))
464+
.join(:verifications, verifications_join)
465+
.join(:tags, tags_join, { table_alias: :provider_tags } )
466+
.where(Sequel[:latest_tagged_pact_publications][:provider_id] => provider_id)
413467
.all
414468
hash[provider_tag] = head_pacts
415469
end

lib/pact_broker/repositories/helpers.rb

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ def order_ignore_case column_name = :name
2323
order(Sequel.function(:lower, column_name))
2424
end
2525

26+
def order_append_ignore_case column_name = :name
27+
order_append(Sequel.function(:lower, column_name))
28+
end
29+
2630
def mysql?
2731
Sequel::Model.db.adapter_scheme.to_s =~ /mysql/
2832
end

spec/lib/pact_broker/api/contracts/verifiable_pacts_json_query_schema_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ module Contracts
5757
}]
5858
end
5959

60-
it { is_expected.to have_key(:consumerVersionSelectors) }
60+
it { is_expected.to be_empty }
6161
end
6262

6363
context "when includeWipPactsSince key exists" do

spec/lib/pact_broker/api/contracts/verifiable_pacts_query_schema_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ module Contracts
5151
}]
5252
end
5353

54-
it { is_expected.to have_key(:consumer_version_selectors) }
54+
it { is_expected.to be_empty }
5555
end
5656

5757
context "when include_wip_pacts_since key exists" do

spec/lib/pact_broker/pacts/repository_find_for_verification_spec.rb

+55-12
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,35 @@ def find_by_consumer_version_number(consumer_version_number)
1111
subject.find{ |pact| pact.consumer_version_number == consumer_version_number }
1212
end
1313

14+
def find_by_consumer_name_and_consumer_version_number(consumer_name, consumer_version_number)
15+
subject.find{ |pact| pact.consumer_name == consumer_name && pact.consumer_version_number == consumer_version_number }
16+
end
17+
1418
before do
15-
td.create_pact_with_hierarchy("Foo", "bar-latest-prod", "Bar")
19+
td.create_pact_with_hierarchy("Foo", "foo-latest-prod-version", "Bar")
1620
.create_consumer_version_tag("prod")
17-
.create_consumer_version("not-latest-dev", tag_names: ["dev"])
21+
.create_consumer_version("not-latest-dev-version", tag_names: ["dev"])
1822
.comment("next pact not selected")
1923
.create_pact
20-
.create_consumer_version("bar-latest-dev", tag_names: ["dev"])
24+
.create_consumer_version("foo-latest-dev-version", tag_names: ["dev"])
2125
.create_pact
2226
.create_consumer("Baz")
23-
.create_consumer_version("baz-latest-dev", tag_names: ["dev"])
27+
.create_consumer_version("baz-latest-dev-version", tag_names: ["dev"])
2428
.create_pact
2529
end
2630

2731
subject { Repository.new.find_for_verification("Bar", consumer_version_selectors) }
2832

33+
context "when there are no selectors" do
34+
let(:consumer_version_selectors) { [] }
35+
36+
it "returns the latest pact for each consumer" do
37+
expect(subject.size).to eq 2
38+
expect(find_by_consumer_name_and_consumer_version_number("Foo", "foo-latest-dev-version")).to_not be nil
39+
expect(find_by_consumer_name_and_consumer_version_number("Baz", "baz-latest-dev-version")).to_not be nil
40+
end
41+
end
42+
2943
context "when consumer tag names are specified" do
3044
let(:pact_selector_1) { double('selector', tag: 'dev', latest: true) }
3145
let(:pact_selector_2) { double('selector', tag: 'prod', latest: true) }
@@ -34,29 +48,58 @@ def find_by_consumer_version_number(consumer_version_number)
3448
end
3549

3650
it "returns the latest pact with the specified tags for each consumer" do
37-
expect(find_by_consumer_version_number("bar-latest-prod")).to_not be nil
38-
expect(find_by_consumer_version_number("bar-latest-dev")).to_not be nil
39-
expect(find_by_consumer_version_number("baz-latest-dev")).to_not be nil
51+
expect(find_by_consumer_version_number("foo-latest-prod-version")).to_not be nil
52+
expect(find_by_consumer_version_number("foo-latest-dev-version")).to_not be nil
53+
expect(find_by_consumer_version_number("baz-latest-dev-version")).to_not be nil
4054
expect(subject.size).to eq 3
4155
end
4256

4357
it "sets the latest_consumer_version_tag_names" do
44-
expect(find_by_consumer_version_number("bar-latest-prod").tag).to eq 'prod'
58+
expect(find_by_consumer_version_number("foo-latest-prod-version").tag).to eq 'prod'
59+
end
60+
61+
context "when all versions with a given tag are requested" do
62+
before do
63+
td.create_pact_with_hierarchy("Foo2", "prod-version-1", "Bar2")
64+
.create_consumer_version_tag("prod")
65+
.create_consumer_version("not-prod-version", tag_names: %w[master])
66+
.create_pact
67+
.create_consumer_version("prod-version-2", tag_names: %w[prod])
68+
.create_pact
69+
end
70+
71+
let(:consumer_version_selectors) { [pact_selector_1] }
72+
let(:pact_selector_1) { double('selector', tag: 'prod', latest: nil) }
73+
74+
subject { Repository.new.find_for_verification("Bar2", consumer_version_selectors) }
75+
76+
it "returns all the versions with the specified tag" do
77+
expect(subject.size).to be 2
78+
expect(find_by_consumer_version_number("prod-version-1")).to_not be nil
79+
expect(find_by_consumer_version_number("prod-version-2")).to_not be nil
80+
end
81+
82+
it "dedupes them to ensure that each pact version is only verified once" do
83+
td.create_consumer_version("prod-version-3", tag_names: %w[prod])
84+
.republish_same_pact
85+
expect(subject.size).to be 2
86+
expect(subject.collect(&:consumer_version_number)).to eq %w[prod-version-1 prod-version-3]
87+
end
4588
end
4689
end
4790

4891
context "when no selectors are specified" do
4992
let(:consumer_version_selectors) { [] }
5093

5194
it "returns the latest pact for each provider" do
52-
expect(find_by_consumer_version_number("bar-latest-dev")).to_not be nil
53-
expect(find_by_consumer_version_number("baz-latest-dev")).to_not be nil
95+
expect(find_by_consumer_version_number("foo-latest-dev-version")).to_not be nil
96+
expect(find_by_consumer_version_number("baz-latest-dev-version")).to_not be nil
5497
expect(subject.size).to eq 2
5598
end
5699

57100
it "does not set the tag name" do
58-
expect(find_by_consumer_version_number("bar-latest-dev").tag).to be nil
59-
expect(find_by_consumer_version_number("bar-latest-dev").overall_latest?).to be true
101+
expect(find_by_consumer_version_number("foo-latest-dev-version").tag).to be nil
102+
expect(find_by_consumer_version_number("foo-latest-dev-version").overall_latest?).to be true
60103
end
61104
end
62105
end

0 commit comments

Comments
 (0)