Skip to content

Commit

Permalink
feat(pacts for verification): include WIP pacts in list of pacts to v…
Browse files Browse the repository at this point in the history
…erify
  • Loading branch information
bethesque committed Nov 21, 2019
1 parent a80f2fd commit 04a0f40
Show file tree
Hide file tree
Showing 19 changed files with 428 additions and 47 deletions.
15 changes: 15 additions & 0 deletions lib/pact_broker/api/contracts/dry_validation_predicates.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require 'dry-validation'

module PactBroker
module Api
module Contracts
module DryValidationPredicates
include Dry::Logic::Predicates

predicate(:date?) do |value|
DateTime.parse(value) rescue false
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'dry-validation'
require 'pact_broker/hash_refinements'
require 'pact_broker/api/contracts/dry_validation_workarounds'
require 'pact_broker/api/contracts/dry_validation_predicates'

module PactBroker
module Api
Expand All @@ -10,6 +11,9 @@ class VerifiablePactsJSONQuerySchema
using PactBroker::HashRefinements

SCHEMA = Dry::Validation.Schema do
configure do
predicates(DryValidationPredicates)
end
optional(:providerVersionTags).maybe(:array?)
optional(:consumerVersionSelectors).each do
schema do
Expand All @@ -18,6 +22,7 @@ class VerifiablePactsJSONQuerySchema
end
end
optional(:includePendingStatus).filled(included_in?: [true, false])
optional(:includeWipPactsSince).filled(:date?)
end

def self.call(params)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'dry-validation'
require 'pact_broker/api/contracts/dry_validation_workarounds'
require 'pact_broker/api/contracts/dry_validation_predicates'

module PactBroker
module Api
Expand All @@ -9,6 +10,9 @@ class VerifiablePactsQuerySchema
using PactBroker::HashRefinements

SCHEMA = Dry::Validation.Schema do
configure do
predicates(DryValidationPredicates)
end
optional(:provider_version_tags).maybe(:array?)
optional(:consumer_version_selectors).each do
schema do
Expand All @@ -17,6 +21,7 @@ class VerifiablePactsQuerySchema
end
end
optional(:include_pending_status).filled(included_in?: ["true", "false"])
optional(:include_wip_pacts_since).filled(:date?)
end

def self.call(params)
Expand Down
11 changes: 6 additions & 5 deletions lib/pact_broker/api/decorators/verifiable_pact_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ def initialize(verifiable_pact)
end

property :verification_properties, as: :verificationProperties do
property :pending,
if: ->(context) { context[:options][:user_options][:include_pending_status] }
property :pending_reason, as: :pendingReason, exec_context: :decorator,
if: ->(context) { context[:options][:user_options][:include_pending_status] }
property :inclusion_reason, as: :inclusionReason, exec_context: :decorator
property :pending,
if: ->(context) { context[:options][:user_options][:include_pending_status] }
property :wip, if: -> (context) { context[:represented].wip }
property :inclusion_reason, as: :inclusionReason, exec_context: :decorator
property :pending_reason, as: :pendingReason, exec_context: :decorator,
if: ->(context) { context[:options][:user_options][:include_pending_status] }

def inclusion_reason
PactBroker::Pacts::VerifiablePactMessages.new(represented).inclusion_reason
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ class VerifiablePactsQueryDecorator < BaseDecorator
represented.include_pending_status = (fragment == 'true' || fragment == true)
}

property :include_wip_pacts_since, default: nil,
setter: ->(fragment:, represented:, **) {
represented.include_wip_pacts_since = fragment ? DateTime.parse(fragment) : nil
}

def from_hash(hash)
# This handles both the snakecase keys from the GET query and the camelcase JSON POST body
super(hash&.snakecase_keys)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ def pacts
pact_service.find_for_verification(
provider_name,
parsed_query_params.provider_version_tags,
parsed_query_params.consumer_version_selectors
parsed_query_params.consumer_version_selectors,
{ include_wip_pacts_since: parsed_query_params.include_wip_pacts_since }
)
end

Expand Down
59 changes: 41 additions & 18 deletions lib/pact_broker/pacts/repository.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,29 +125,21 @@ def find_latest_pact_versions_for_provider provider_name, tag = nil
end
end

def find_wip_pact_versions_for_provider provider_name, provider_tags = []
def find_wip_pact_versions_for_provider provider_name, provider_tags = [], options = {}
return [] if provider_tags.empty?
successfully_verified_pact_publication_ids_for_each_tag = provider_tags.collect do | provider_tag |
ids = LatestTaggedPactPublications
.join(:verifications, { pact_version_id: :pact_version_id })
.join(:tags, { Sequel[:verifications][:provider_version_id] => Sequel[:provider_tags][:version_id] }, {table_alias: :provider_tags})
.where(Sequel[:provider_tags][:name] => provider_tag)
.provider(provider_name)
.where(Sequel[:verifications][:success] => true)
.select(Sequel[:latest_tagged_pact_publications][:id].as(:id))
.collect(&:id)
[provider_tag, ids]
end

successfully_verified_pact_publication_ids_for_all_tags = successfully_verified_pact_publication_ids_for_each_tag.collect(&:last).reduce(:&)
pact_publication_ids = LatestTaggedPactPublications.provider(provider_name).exclude(id: successfully_verified_pact_publication_ids_for_all_tags).select_for_subquery(:id)
# Hash of provider tag names => list of pact_publication_ids
successfully_verified_head_pact_publication_ids_for_each_provider_tag = find_successfully_verified_head_pacts_by_provider_tag(provider_name, provider_tags, options)

pact_publication_ids = find_head_pacts_that_have_not_been_successfully_verified_by_all_provider_tags(
provider_name,
successfully_verified_head_pact_publication_ids_for_each_provider_tag.values.reduce(:&),
options)

pacts = AllPactPublications.where(id: pact_publication_ids).order_ignore_case(:consumer_name).order_append(:consumer_version_order).collect(&:to_domain)
pacts.collect do | pact|
pending_tags = successfully_verified_pact_publication_ids_for_each_tag.select do | (provider_tag, pact_publication_ids) |
!pact_publication_ids.include?(pact.id)
end.collect(&:first)
VerifiablePact.new(pact, true, pending_tags, [], pact.consumer_version_tag_names)
pending_tags = find_provider_tags_for_which_pact_publication_id_is_pending(pact.id, successfully_verified_head_pact_publication_ids_for_each_provider_tag)
VerifiablePact.new(pact, true, pending_tags, [], pact.consumer_version_tag_names, nil, true)
end
end

Expand Down Expand Up @@ -340,6 +332,37 @@ def find_all_database_versions_between(consumer_name, options, base_class = Late
query = query.tag(options[:tag]) if options[:tag]
query
end

def find_provider_tags_for_which_pact_publication_id_is_pending(pact_publication_id, successfully_verified_head_pact_publication_ids_for_each_provider_tag)
successfully_verified_head_pact_publication_ids_for_each_provider_tag
.select do | provider_tag, pact_publication_ids |
!pact_publication_ids.include?(pact_publication_id)
end.keys
end

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)
# Exclude the head pacts that have been successfully verified by all the specified provider tags
pact_publication_ids = LatestTaggedPactPublications
.provider(provider_name)
.exclude(id: pact_publication_ids_successfully_verified_by_all_provider_tags)
.where(Sequel.lit('latest_tagged_pact_publications.created_at > ?', options.fetch(:include_wip_pacts_since)))
.select_for_subquery(:id)
end

def find_successfully_verified_head_pacts_by_provider_tag(provider_name, provider_tags, options)
provider_tags.compact.each_with_object({}) do | provider_tag, tag_to_ids_hash |
ids = LatestTaggedPactPublications
.join(:verifications, { pact_version_id: :pact_version_id })
.join(:tags, { Sequel[:verifications][:provider_version_id] => Sequel[:provider_tags][:version_id] }, {table_alias: :provider_tags})
.where(Sequel[:provider_tags][:name] => provider_tag)
.provider(provider_name)
.where(Sequel[:verifications][:success] => true)
.where(Sequel.lit('latest_tagged_pact_publications.created_at > ?', options.fetch(:include_wip_pacts_since)))
.select(Sequel[:latest_tagged_pact_publications][:id].as(:id))
.collect(&:id)
tag_to_ids_hash[provider_tag] = ids
end
end
end
end
end
22 changes: 20 additions & 2 deletions lib/pact_broker/pacts/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -115,18 +115,36 @@ def find_distinct_pacts_between consumer, options
distinct
end

def find_for_verification(provider_name, provider_version_tags, consumer_version_selectors)
pact_repository
def find_for_verification(provider_name, provider_version_tags, consumer_version_selectors, options)
verifiable_pacts_specified_in_request = pact_repository
.find_for_verification(provider_name, consumer_version_selectors)
.group_by(&:pact_version_sha)
.values
.collect do | head_pacts |
squash_pacts_for_verification(provider_version_tags, head_pacts)
end

verifiable_wip_pacts = if options[:include_wip_pacts_since]
exclude_specified_pacts(
pact_repository.find_wip_pact_versions_for_provider(provider_name, provider_version_tags, options),
verifiable_pacts_specified_in_request)
else
[]
end

verifiable_pacts_specified_in_request + verifiable_wip_pacts
end

private

def exclude_specified_pacts(wip_pacts, specified_pacts)
wip_pacts.select do | wip_pact |
!specified_pacts.any? do | specified_pacts |
wip_pact.pact_version_sha == specified_pacts.pact_version_sha
end
end
end

# Overwriting an existing pact with the same consumer/provider/consumer version number
def update_pact params, existing_pact, webhook_options
logger.info "Updating existing pact publication with params #{params.reject{ |k, v| k == :json_content}}"
Expand Down
10 changes: 8 additions & 2 deletions lib/pact_broker/pacts/verifiable_pact.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
module PactBroker
module Pacts
class VerifiablePact < SimpleDelegator
attr_reader :pending, :pending_provider_tags, :non_pending_provider_tags, :head_consumer_tags
attr_reader :pending, :pending_provider_tags, :non_pending_provider_tags, :head_consumer_tags, :wip

def initialize(pact, pending, pending_provider_tags = [], non_pending_provider_tags = [], head_consumer_tags = [], overall_latest = false)
# TODO refactor this constructor
def initialize(pact, pending, pending_provider_tags = [], non_pending_provider_tags = [], head_consumer_tags = [], overall_latest = false, wip = false)
super(pact)
@pending = pending
@pending_provider_tags = pending_provider_tags
@non_pending_provider_tags = non_pending_provider_tags
@head_consumer_tags = head_consumer_tags
@overall_latest = overall_latest
@wip = wip
end

def consumer_tags
Expand All @@ -25,6 +27,10 @@ def overall_latest?
def pending?
pending
end

def wip?
wip
end
end
end
end
22 changes: 14 additions & 8 deletions lib/pact_broker/pacts/verifiable_pact_messages.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,34 @@ module Pacts
class VerifiablePactMessages
extend Forwardable

READ_MORE = "Read more at https://pact.io/pending"
READ_MORE_PENDING = "Read more at https://pact.io/pending"
READ_MORE_WIP = "Read more at https://pact.io/wip"

delegate [:consumer_name, :provider_name, :head_consumer_tags, :pending_provider_tags, :non_pending_provider_tags, :pending?] => :verifiable_pact
delegate [:consumer_name, :provider_name, :head_consumer_tags, :pending_provider_tags, :non_pending_provider_tags, :pending?, :wip?] => :verifiable_pact

def initialize(verifiable_pact)
@verifiable_pact = verifiable_pact
end

def inclusion_reason
if head_consumer_tags.any?
version_text = head_consumer_tags.size == 1 ? "version" : "versions"
"This pact is being verified because it is the pact for the latest #{version_text} of Foo tagged with #{joined_head_consumer_tags}"
version_text = head_consumer_tags.size == 1 ? "version" : "versions"
if wip?
# WIP pacts will always have tags, because it is part of the definition of being a WIP pact
"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}"
else
"This pact is being verified because it is the latest pact between #{consumer_name} and #{provider_name}."
if head_consumer_tags.any?
"This pact is being verified because it is the pact for the latest #{version_text} of Foo tagged with #{joined_head_consumer_tags}"
else
"This pact is being verified because it is the latest pact between #{consumer_name} and #{provider_name}."
end
end
end

def pending_reason
if pending?
"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}"
"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}"
else
"This pact has previously been successfully verified by #{non_pending_provider_tags_description}. If this verification fails, it will fail the build. #{READ_MORE}"
"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}"
end
end

Expand Down
Loading

0 comments on commit 04a0f40

Please sign in to comment.