Skip to content

Commit

Permalink
feat: add 'pacts for verification' endpoint (#308)
Browse files Browse the repository at this point in the history
* feat: add endpoint for "verifiable pacts" which returns a list of pacts to be verified and marks which ones should be considered "pending" and not fail the build

* feat: add beta:provider-pacts-for-verification rel to index

* feat: squash pacts with the same pact version sha into one 'pact'

* feat: compose messages to explain why pacts are in pending/non pending state, and why they have been included in the verification step

* feat: use pending and inclusion messages in the pacts for verification response

* feat: add 'read more' link to pending reason

* feat: add feature flag to turn on pact for verifications relation
  • Loading branch information
bethesque authored Sep 26, 2019
1 parent f0f1a25 commit 31fb8aa
Show file tree
Hide file tree
Showing 35 changed files with 1,200 additions and 171 deletions.
4 changes: 2 additions & 2 deletions lib/pact_broker/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ module PactBroker
add ['pacts', 'provider', :provider_name, 'latest', :tag], Api::Resources::LatestProviderPacts, {resource_name: "latest_tagged_provider_pact_publications"}
add ['pacts', 'latest'], Api::Resources::LatestPacts, {resource_name: "latest_pacts"}

# Pending pacts
add ['pacts', 'provider', :provider_name, 'pending'], Api::Resources::PendingProviderPacts, {resource_name: "pending_provider_pact_publications"}
# Pacts for verification
add ['pacts', 'provider', :provider_name, 'for-verification'], Api::Resources::ProviderPactsForVerification, {resource_name: "pacts_for_verification"}

# Deprecated pact
add ['pact', 'provider', :provider_name, 'consumer', :consumer_name, 'version', :consumer_version_number], Api::Resources::Pact, {resource_name: "pact_publications", deprecated: "true"} # Deprecate, singular /pact
Expand Down
41 changes: 41 additions & 0 deletions lib/pact_broker/api/contracts/verifiable_pacts_query_schema.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
require 'dry-validation'

module PactBroker
module Api
module Contracts
class VerifiablePactsQuerySchema
SCHEMA = Dry::Validation.Schema do
optional(:provider_version_tags).maybe(:array?)
# optional(:exclude_other_pending).filled(included_in?: ["true", "false"])
optional(:consumer_version_selectors).each do
schema do
required(:tag).filled(:str?)
optional(:latest).filled(included_in?: ["true", "false"])
end
end
end

def self.call(params)
select_first_message(flatten_index_messages(SCHEMA.call(params).messages(full: true)))
end

def self.select_first_message(messages)
messages.each_with_object({}) do | (key, value), new_messages |
new_messages[key] = [value.first]
end
end

def self.flatten_index_messages(messages)
if messages[:consumer_version_selectors]
new_messages = messages[:consumer_version_selectors].collect do | index, value |
value.values.flatten.collect { | text | "#{text} at index #{index}"}
end.flatten
messages.merge(consumer_version_selectors: new_messages)
else
messages
end
end
end
end
end
end
34 changes: 34 additions & 0 deletions lib/pact_broker/api/decorators/verifiable_pact_decorator.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,45 @@
require_relative 'base_decorator'
require 'pact_broker/api/pact_broker_urls'
require 'delegate'
require 'pact_broker/pacts/verifiable_pact_messages'

module PactBroker
module Api
module Decorators
class VerifiablePactDecorator < BaseDecorator

# Allows a "flat" VerifiablePact to look like it has
# a nested verification_properties object for Reform
class Reshaper < SimpleDelegator
def verification_properties
__getobj__()
end
end

def initialize(verifiable_pact)
super(Reshaper.new(verifiable_pact))
end

property :verification_properties, as: :verificationProperties do
property :pending
property :pending_reason, as: :pendingReason, exec_context: :decorator
property :inclusion_reason, as: :inclusionReason, exec_context: :decorator

def inclusion_reason
PactBroker::Pacts::VerifiablePactMessages.new(represented).inclusion_reason
end

def pending_reason
PactBroker::Pacts::VerifiablePactMessages.new(represented).pending_reason
end
end

link :self do | context |
{
href: pact_version_url(represented, context[:base_url]),
name: represented.name
}
end
end
end
end
Expand Down
27 changes: 27 additions & 0 deletions lib/pact_broker/api/decorators/verifiable_pacts_query_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
require_relative 'base_decorator'
require_relative 'verifiable_pact_decorator'
require 'pact_broker/api/pact_broker_urls'

module PactBroker
module Api
module Decorators
class VerifiablePactsQueryDecorator < BaseDecorator
collection :provider_version_tags

collection :consumer_version_selectors, class: OpenStruct do
property :tag
property :latest, setter: ->(fragment:, represented:, **) { represented.latest = (fragment == 'true') }
end


def from_hash(*args)
# Should remember how to do this via Representable...
result = super
result.consumer_version_selectors = [] if result.consumer_version_selectors.nil?
result.provider_version_tags = [] if result.provider_version_tags.nil?
result
end
end
end
end
end
19 changes: 12 additions & 7 deletions lib/pact_broker/api/resources/index.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'pact_broker/api/resources/base_resource'
require 'pact_broker/feature_toggle'
require 'json'

module PactBroker
Expand All @@ -18,7 +19,7 @@ def to_json
end

def links
{
links_hash = {
'self' =>
{
href: base_url,
Expand Down Expand Up @@ -109,12 +110,6 @@ def links
href: base_url + '/metrics',
title: "Get Pact Broker metrics",
},
'beta:pending-provider-pacts' =>
{
href: base_url + '/pacts/provider/{provider}/pending',
title: 'Pending pact versions for the specified provider',
templated: true
},
'curies' =>
[{
name: 'pb',
Expand All @@ -126,6 +121,16 @@ def links
templated: true
}]
}

if PactBroker.feature_enabled?(:pacts_for_verification)
links_hash['beta:provider-pacts-for-verification'] = {
href: base_url + '/pacts/provider/{provider}/for-verification',
title: 'Pact versions to be verified for the specified provider',
templated: true
}
end

links_hash
end
end
end
Expand Down
21 changes: 0 additions & 21 deletions lib/pact_broker/api/resources/pending_provider_pacts.rb

This file was deleted.

54 changes: 54 additions & 0 deletions lib/pact_broker/api/resources/provider_pacts_for_verification.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
require 'pact_broker/api/resources/provider_pacts'
require 'pact_broker/api/decorators/verifiable_pacts_decorator'
require 'pact_broker/api/contracts/verifiable_pacts_query_schema'
require 'pact_broker/api/decorators/verifiable_pacts_query_decorator'

module PactBroker
module Api
module Resources
class ProviderPactsForVerification < ProviderPacts
def initialize
@query = Rack::Utils.parse_nested_query(request.uri.query)
end

def malformed_request?
if (errors = query_schema.call(query)).any?
set_json_validation_error_messages(errors)
true
else
false
end
end

private

attr_reader :query

def pacts
pact_service.find_for_verification(
provider_name,
parsed_query_params.provider_version_tags,
parsed_query_params.consumer_version_selectors
)
end

def resource_title
"Pacts to be verified by provider #{provider_name}"
end

def to_json
PactBroker::Api::Decorators::VerifiablePactsDecorator.new(pacts).to_json(to_json_options)
end


def query_schema
PactBroker::Api::Contracts::VerifiablePactsQuerySchema
end

def parsed_query_params
@parsed_query_params ||= PactBroker::Api::Decorators::VerifiablePactsQueryDecorator.new(OpenStruct.new).from_hash(query)
end
end
end
end
end
29 changes: 28 additions & 1 deletion lib/pact_broker/domain/pact.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
require 'pact_broker/db'
require 'pact_broker/json'

=begin
This class most accurately represents a PactPublication
=end

module PactBroker

module Domain
class Pact

# The ID is the pact_publication ID
attr_accessor :id, :provider, :consumer_version, :consumer, :created_at, :json_content, :consumer_version_number, :revision_number, :pact_version_sha, :latest_verification, :head_tag_names

def initialize attributes
attributes.each_pair do | key, value |
self.send(key.to_s + "=", value)
Expand All @@ -30,6 +34,10 @@ def consumer_version_tag_names
consumer_version.tags.collect(&:name)
end

def latest_consumer_version_tag_names= latest_consumer_version_tag_names
@latest_consumer_version_tag_names = latest_consumer_version_tag_names
end

def to_s
"Pact: consumer=#{consumer.name} provider=#{provider.name}"
end
Expand All @@ -53,6 +61,25 @@ def content_hash
def pact_publication_id
id
end

def select_pending_provider_version_tags(provider_version_tags)
provider_version_tags - db_model.pact_version.select_provider_tags_with_successful_verifications(provider_version_tags)
end

def pending?
!pact_version.verified_successfully_by_any_provider_version?
end

private

attr_accessor :db_model

# Really not sure about mixing Sequel model class into this PORO...
# But it's much nicer than using a repository to find out the pending information :(
def pact_version
db_model.pact_version
end
end

end
end
3 changes: 2 additions & 1 deletion lib/pact_broker/pacts/all_pact_publications.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ def to_domain_without_tags
revision_number: revision_number,
pact_version_sha: pact_version_sha,
created_at: created_at,
head_tag_names: head_tag_names)
head_tag_names: head_tag_names,
db_model: self)
end

def head_tag_names
Expand Down
30 changes: 30 additions & 0 deletions lib/pact_broker/pacts/head_pact.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require 'delegate'

# A head pact is the pact for the latest consumer version with the specified tag
# (ignoring later versions that might have the specified tag but no pact)

module PactBroker
module Pacts
class HeadPact < SimpleDelegator
attr_reader :tag, :consumer_version_number

def initialize(pact, consumer_version_number, tag)
super(pact)
@consumer_version_number = consumer_version_number
@tag = tag
end

# The underlying pact publication may well be the overall latest as well, but
# this row does not know that, as there will be a row with a nil tag
# if it is the overall latest as well as a row with the
# tag set, as the data is denormalised in the LatestTaggedPactPublications table.
def overall_latest?
tag.nil?
end

def pact
__getobj__()
end
end
end
end
9 changes: 8 additions & 1 deletion lib/pact_broker/pacts/latest_pact_publications.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
require 'pact_broker/pacts/latest_pact_publications_by_consumer_version'
require 'pact_broker/pacts/head_pact'

module PactBroker
module Pacts

# latest pact for each consumer/provider pair
class LatestPactPublications < LatestPactPublicationsByConsumerVersion
set_dataset(:latest_pact_publications)
end

# This pact may well be the latest for certain tags, but in this query
# we don't know what they are
def to_domain
HeadPact.new(super, consumer_version_number, nil)
end
end
end
end

Expand Down
6 changes: 5 additions & 1 deletion lib/pact_broker/pacts/latest_tagged_pact_publications.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
require 'pact_broker/pacts/latest_pact_publications_by_consumer_version'
require 'pact_broker/pacts/head_pact'

module PactBroker
module Pacts

class LatestTaggedPactPublications < LatestPactPublicationsByConsumerVersion
set_dataset(:latest_tagged_pact_publications)
end

def to_domain
HeadPact.new(super, consumer_version_number, tag_name)
end
end
end
end

Expand Down
3 changes: 2 additions & 1 deletion lib/pact_broker/pacts/pact_publication.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ def to_domain
pact_version_sha: pact_version.sha,
latest_verification: latest_verification,
created_at: created_at,
head_tag_names: head_tag_names
head_tag_names: head_tag_names,
db_model: self
)
end

Expand Down
Loading

0 comments on commit 31fb8aa

Please sign in to comment.