Skip to content

Commit 04a0f40

Browse files
committed
feat(pacts for verification): include WIP pacts in list of pacts to verify
1 parent a80f2fd commit 04a0f40

19 files changed

+428
-47
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
require 'dry-validation'
2+
3+
module PactBroker
4+
module Api
5+
module Contracts
6+
module DryValidationPredicates
7+
include Dry::Logic::Predicates
8+
9+
predicate(:date?) do |value|
10+
DateTime.parse(value) rescue false
11+
end
12+
end
13+
end
14+
end
15+
end

lib/pact_broker/api/contracts/verifiable_pacts_json_query_schema.rb

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require 'dry-validation'
22
require 'pact_broker/hash_refinements'
33
require 'pact_broker/api/contracts/dry_validation_workarounds'
4+
require 'pact_broker/api/contracts/dry_validation_predicates'
45

56
module PactBroker
67
module Api
@@ -10,6 +11,9 @@ class VerifiablePactsJSONQuerySchema
1011
using PactBroker::HashRefinements
1112

1213
SCHEMA = Dry::Validation.Schema do
14+
configure do
15+
predicates(DryValidationPredicates)
16+
end
1317
optional(:providerVersionTags).maybe(:array?)
1418
optional(:consumerVersionSelectors).each do
1519
schema do
@@ -18,6 +22,7 @@ class VerifiablePactsJSONQuerySchema
1822
end
1923
end
2024
optional(:includePendingStatus).filled(included_in?: [true, false])
25+
optional(:includeWipPactsSince).filled(:date?)
2126
end
2227

2328
def self.call(params)

lib/pact_broker/api/contracts/verifiable_pacts_query_schema.rb

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'dry-validation'
22
require 'pact_broker/api/contracts/dry_validation_workarounds'
3+
require 'pact_broker/api/contracts/dry_validation_predicates'
34

45
module PactBroker
56
module Api
@@ -9,6 +10,9 @@ class VerifiablePactsQuerySchema
910
using PactBroker::HashRefinements
1011

1112
SCHEMA = Dry::Validation.Schema do
13+
configure do
14+
predicates(DryValidationPredicates)
15+
end
1216
optional(:provider_version_tags).maybe(:array?)
1317
optional(:consumer_version_selectors).each do
1418
schema do
@@ -17,6 +21,7 @@ class VerifiablePactsQuerySchema
1721
end
1822
end
1923
optional(:include_pending_status).filled(included_in?: ["true", "false"])
24+
optional(:include_wip_pacts_since).filled(:date?)
2025
end
2126

2227
def self.call(params)

lib/pact_broker/api/decorators/verifiable_pact_decorator.rb

+6-5
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ def initialize(verifiable_pact)
2121
end
2222

2323
property :verification_properties, as: :verificationProperties do
24-
property :pending,
25-
if: ->(context) { context[:options][:user_options][:include_pending_status] }
26-
property :pending_reason, as: :pendingReason, exec_context: :decorator,
27-
if: ->(context) { context[:options][:user_options][:include_pending_status] }
28-
property :inclusion_reason, as: :inclusionReason, exec_context: :decorator
24+
property :pending,
25+
if: ->(context) { context[:options][:user_options][:include_pending_status] }
26+
property :wip, if: -> (context) { context[:represented].wip }
27+
property :inclusion_reason, as: :inclusionReason, exec_context: :decorator
28+
property :pending_reason, as: :pendingReason, exec_context: :decorator,
29+
if: ->(context) { context[:options][:user_options][:include_pending_status] }
2930

3031
def inclusion_reason
3132
PactBroker::Pacts::VerifiablePactMessages.new(represented).inclusion_reason

lib/pact_broker/api/decorators/verifiable_pacts_query_decorator.rb

+5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ class VerifiablePactsQueryDecorator < BaseDecorator
2424
represented.include_pending_status = (fragment == 'true' || fragment == true)
2525
}
2626

27+
property :include_wip_pacts_since, default: nil,
28+
setter: ->(fragment:, represented:, **) {
29+
represented.include_wip_pacts_since = fragment ? DateTime.parse(fragment) : nil
30+
}
31+
2732
def from_hash(hash)
2833
# This handles both the snakecase keys from the GET query and the camelcase JSON POST body
2934
super(hash&.snakecase_keys)

lib/pact_broker/api/resources/provider_pacts_for_verification.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ def pacts
3939
pact_service.find_for_verification(
4040
provider_name,
4141
parsed_query_params.provider_version_tags,
42-
parsed_query_params.consumer_version_selectors
42+
parsed_query_params.consumer_version_selectors,
43+
{ include_wip_pacts_since: parsed_query_params.include_wip_pacts_since }
4344
)
4445
end
4546

lib/pact_broker/pacts/repository.rb

+41-18
Original file line numberDiff line numberDiff line change
@@ -125,29 +125,21 @@ def find_latest_pact_versions_for_provider provider_name, tag = nil
125125
end
126126
end
127127

128-
def find_wip_pact_versions_for_provider provider_name, provider_tags = []
128+
def find_wip_pact_versions_for_provider provider_name, provider_tags = [], options = {}
129129
return [] if provider_tags.empty?
130-
successfully_verified_pact_publication_ids_for_each_tag = provider_tags.collect do | provider_tag |
131-
ids = LatestTaggedPactPublications
132-
.join(:verifications, { pact_version_id: :pact_version_id })
133-
.join(:tags, { Sequel[:verifications][:provider_version_id] => Sequel[:provider_tags][:version_id] }, {table_alias: :provider_tags})
134-
.where(Sequel[:provider_tags][:name] => provider_tag)
135-
.provider(provider_name)
136-
.where(Sequel[:verifications][:success] => true)
137-
.select(Sequel[:latest_tagged_pact_publications][:id].as(:id))
138-
.collect(&:id)
139-
[provider_tag, ids]
140-
end
141130

142-
successfully_verified_pact_publication_ids_for_all_tags = successfully_verified_pact_publication_ids_for_each_tag.collect(&:last).reduce(:&)
143-
pact_publication_ids = LatestTaggedPactPublications.provider(provider_name).exclude(id: successfully_verified_pact_publication_ids_for_all_tags).select_for_subquery(:id)
131+
# Hash of provider tag names => list of pact_publication_ids
132+
successfully_verified_head_pact_publication_ids_for_each_provider_tag = find_successfully_verified_head_pacts_by_provider_tag(provider_name, provider_tags, options)
133+
134+
pact_publication_ids = find_head_pacts_that_have_not_been_successfully_verified_by_all_provider_tags(
135+
provider_name,
136+
successfully_verified_head_pact_publication_ids_for_each_provider_tag.values.reduce(:&),
137+
options)
144138

145139
pacts = AllPactPublications.where(id: pact_publication_ids).order_ignore_case(:consumer_name).order_append(:consumer_version_order).collect(&:to_domain)
146140
pacts.collect do | pact|
147-
pending_tags = successfully_verified_pact_publication_ids_for_each_tag.select do | (provider_tag, pact_publication_ids) |
148-
!pact_publication_ids.include?(pact.id)
149-
end.collect(&:first)
150-
VerifiablePact.new(pact, true, pending_tags, [], pact.consumer_version_tag_names)
141+
pending_tags = find_provider_tags_for_which_pact_publication_id_is_pending(pact.id, successfully_verified_head_pact_publication_ids_for_each_provider_tag)
142+
VerifiablePact.new(pact, true, pending_tags, [], pact.consumer_version_tag_names, nil, true)
151143
end
152144
end
153145

@@ -340,6 +332,37 @@ def find_all_database_versions_between(consumer_name, options, base_class = Late
340332
query = query.tag(options[:tag]) if options[:tag]
341333
query
342334
end
335+
336+
def find_provider_tags_for_which_pact_publication_id_is_pending(pact_publication_id, successfully_verified_head_pact_publication_ids_for_each_provider_tag)
337+
successfully_verified_head_pact_publication_ids_for_each_provider_tag
338+
.select do | provider_tag, pact_publication_ids |
339+
!pact_publication_ids.include?(pact_publication_id)
340+
end.keys
341+
end
342+
343+
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)
344+
# Exclude the head pacts that have been successfully verified by all the specified provider tags
345+
pact_publication_ids = LatestTaggedPactPublications
346+
.provider(provider_name)
347+
.exclude(id: pact_publication_ids_successfully_verified_by_all_provider_tags)
348+
.where(Sequel.lit('latest_tagged_pact_publications.created_at > ?', options.fetch(:include_wip_pacts_since)))
349+
.select_for_subquery(:id)
350+
end
351+
352+
def find_successfully_verified_head_pacts_by_provider_tag(provider_name, provider_tags, options)
353+
provider_tags.compact.each_with_object({}) do | provider_tag, tag_to_ids_hash |
354+
ids = LatestTaggedPactPublications
355+
.join(:verifications, { pact_version_id: :pact_version_id })
356+
.join(:tags, { Sequel[:verifications][:provider_version_id] => Sequel[:provider_tags][:version_id] }, {table_alias: :provider_tags})
357+
.where(Sequel[:provider_tags][:name] => provider_tag)
358+
.provider(provider_name)
359+
.where(Sequel[:verifications][:success] => true)
360+
.where(Sequel.lit('latest_tagged_pact_publications.created_at > ?', options.fetch(:include_wip_pacts_since)))
361+
.select(Sequel[:latest_tagged_pact_publications][:id].as(:id))
362+
.collect(&:id)
363+
tag_to_ids_hash[provider_tag] = ids
364+
end
365+
end
343366
end
344367
end
345368
end

lib/pact_broker/pacts/service.rb

+20-2
Original file line numberDiff line numberDiff line change
@@ -115,18 +115,36 @@ def find_distinct_pacts_between consumer, options
115115
distinct
116116
end
117117

118-
def find_for_verification(provider_name, provider_version_tags, consumer_version_selectors)
119-
pact_repository
118+
def find_for_verification(provider_name, provider_version_tags, consumer_version_selectors, options)
119+
verifiable_pacts_specified_in_request = pact_repository
120120
.find_for_verification(provider_name, consumer_version_selectors)
121121
.group_by(&:pact_version_sha)
122122
.values
123123
.collect do | head_pacts |
124124
squash_pacts_for_verification(provider_version_tags, head_pacts)
125125
end
126+
127+
verifiable_wip_pacts = if options[:include_wip_pacts_since]
128+
exclude_specified_pacts(
129+
pact_repository.find_wip_pact_versions_for_provider(provider_name, provider_version_tags, options),
130+
verifiable_pacts_specified_in_request)
131+
else
132+
[]
133+
end
134+
135+
verifiable_pacts_specified_in_request + verifiable_wip_pacts
126136
end
127137

128138
private
129139

140+
def exclude_specified_pacts(wip_pacts, specified_pacts)
141+
wip_pacts.select do | wip_pact |
142+
!specified_pacts.any? do | specified_pacts |
143+
wip_pact.pact_version_sha == specified_pacts.pact_version_sha
144+
end
145+
end
146+
end
147+
130148
# Overwriting an existing pact with the same consumer/provider/consumer version number
131149
def update_pact params, existing_pact, webhook_options
132150
logger.info "Updating existing pact publication with params #{params.reject{ |k, v| k == :json_content}}"

lib/pact_broker/pacts/verifiable_pact.rb

+8-2
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@
33
module PactBroker
44
module Pacts
55
class VerifiablePact < SimpleDelegator
6-
attr_reader :pending, :pending_provider_tags, :non_pending_provider_tags, :head_consumer_tags
6+
attr_reader :pending, :pending_provider_tags, :non_pending_provider_tags, :head_consumer_tags, :wip
77

8-
def initialize(pact, pending, pending_provider_tags = [], non_pending_provider_tags = [], head_consumer_tags = [], overall_latest = false)
8+
# TODO refactor this constructor
9+
def initialize(pact, pending, pending_provider_tags = [], non_pending_provider_tags = [], head_consumer_tags = [], overall_latest = false, wip = false)
910
super(pact)
1011
@pending = pending
1112
@pending_provider_tags = pending_provider_tags
1213
@non_pending_provider_tags = non_pending_provider_tags
1314
@head_consumer_tags = head_consumer_tags
1415
@overall_latest = overall_latest
16+
@wip = wip
1517
end
1618

1719
def consumer_tags
@@ -25,6 +27,10 @@ def overall_latest?
2527
def pending?
2628
pending
2729
end
30+
31+
def wip?
32+
wip
33+
end
2834
end
2935
end
3036
end

lib/pact_broker/pacts/verifiable_pact_messages.rb

+14-8
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,34 @@ module Pacts
33
class VerifiablePactMessages
44
extend Forwardable
55

6-
READ_MORE = "Read more at https://pact.io/pending"
6+
READ_MORE_PENDING = "Read more at https://pact.io/pending"
7+
READ_MORE_WIP = "Read more at https://pact.io/wip"
78

8-
delegate [:consumer_name, :provider_name, :head_consumer_tags, :pending_provider_tags, :non_pending_provider_tags, :pending?] => :verifiable_pact
9+
delegate [:consumer_name, :provider_name, :head_consumer_tags, :pending_provider_tags, :non_pending_provider_tags, :pending?, :wip?] => :verifiable_pact
910

1011
def initialize(verifiable_pact)
1112
@verifiable_pact = verifiable_pact
1213
end
1314

1415
def inclusion_reason
15-
if head_consumer_tags.any?
16-
version_text = head_consumer_tags.size == 1 ? "version" : "versions"
17-
"This pact is being verified because it is the pact for the latest #{version_text} of Foo tagged with #{joined_head_consumer_tags}"
16+
version_text = head_consumer_tags.size == 1 ? "version" : "versions"
17+
if wip?
18+
# WIP pacts will always have tags, because it is part of the definition of being a WIP pact
19+
"This pact is being verified because it is a 'work in progress' pact (ie. it is the pact for the latest #{version_text} of Foo tagged with #{joined_head_consumer_tags} and is still in pending state). #{READ_MORE_WIP}"
1820
else
19-
"This pact is being verified because it is the latest pact between #{consumer_name} and #{provider_name}."
21+
if head_consumer_tags.any?
22+
"This pact is being verified because it is the pact for the latest #{version_text} of Foo tagged with #{joined_head_consumer_tags}"
23+
else
24+
"This pact is being verified because it is the latest pact between #{consumer_name} and #{provider_name}."
25+
end
2026
end
2127
end
2228

2329
def pending_reason
2430
if pending?
25-
"This pact is in pending state because it has not yet been successfully verified by #{pending_provider_tags_description}. If this verification fails, it will not cause the overall build to fail. #{READ_MORE}"
31+
"This pact is in pending state because it has not yet been successfully verified by #{pending_provider_tags_description}. If this verification fails, it will not cause the overall build to fail. #{READ_MORE_PENDING}"
2632
else
27-
"This pact has previously been successfully verified by #{non_pending_provider_tags_description}. If this verification fails, it will fail the build. #{READ_MORE}"
33+
"This pact has previously been successfully verified by #{non_pending_provider_tags_description}. If this verification fails, it will fail the build. #{READ_MORE_PENDING}"
2834
end
2935
end
3036

0 commit comments

Comments
 (0)