Skip to content

Commit 31fb8aa

Browse files
authored
feat: add 'pacts for verification' endpoint (pact-foundation#308)
* 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
1 parent f0f1a25 commit 31fb8aa

35 files changed

+1200
-171
lines changed

lib/pact_broker/api.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ module PactBroker
4747
add ['pacts', 'provider', :provider_name, 'latest', :tag], Api::Resources::LatestProviderPacts, {resource_name: "latest_tagged_provider_pact_publications"}
4848
add ['pacts', 'latest'], Api::Resources::LatestPacts, {resource_name: "latest_pacts"}
4949

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

5353
# Deprecated pact
5454
add ['pact', 'provider', :provider_name, 'consumer', :consumer_name, 'version', :consumer_version_number], Api::Resources::Pact, {resource_name: "pact_publications", deprecated: "true"} # Deprecate, singular /pact
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
require 'dry-validation'
2+
3+
module PactBroker
4+
module Api
5+
module Contracts
6+
class VerifiablePactsQuerySchema
7+
SCHEMA = Dry::Validation.Schema do
8+
optional(:provider_version_tags).maybe(:array?)
9+
# optional(:exclude_other_pending).filled(included_in?: ["true", "false"])
10+
optional(:consumer_version_selectors).each do
11+
schema do
12+
required(:tag).filled(:str?)
13+
optional(:latest).filled(included_in?: ["true", "false"])
14+
end
15+
end
16+
end
17+
18+
def self.call(params)
19+
select_first_message(flatten_index_messages(SCHEMA.call(params).messages(full: true)))
20+
end
21+
22+
def self.select_first_message(messages)
23+
messages.each_with_object({}) do | (key, value), new_messages |
24+
new_messages[key] = [value.first]
25+
end
26+
end
27+
28+
def self.flatten_index_messages(messages)
29+
if messages[:consumer_version_selectors]
30+
new_messages = messages[:consumer_version_selectors].collect do | index, value |
31+
value.values.flatten.collect { | text | "#{text} at index #{index}"}
32+
end.flatten
33+
messages.merge(consumer_version_selectors: new_messages)
34+
else
35+
messages
36+
end
37+
end
38+
end
39+
end
40+
end
41+
end

lib/pact_broker/api/decorators/verifiable_pact_decorator.rb

+34
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,45 @@
11
require_relative 'base_decorator'
22
require 'pact_broker/api/pact_broker_urls'
3+
require 'delegate'
4+
require 'pact_broker/pacts/verifiable_pact_messages'
35

46
module PactBroker
57
module Api
68
module Decorators
79
class VerifiablePactDecorator < BaseDecorator
810

11+
# Allows a "flat" VerifiablePact to look like it has
12+
# a nested verification_properties object for Reform
13+
class Reshaper < SimpleDelegator
14+
def verification_properties
15+
__getobj__()
16+
end
17+
end
18+
19+
def initialize(verifiable_pact)
20+
super(Reshaper.new(verifiable_pact))
21+
end
22+
23+
property :verification_properties, as: :verificationProperties do
24+
property :pending
25+
property :pending_reason, as: :pendingReason, exec_context: :decorator
26+
property :inclusion_reason, as: :inclusionReason, exec_context: :decorator
27+
28+
def inclusion_reason
29+
PactBroker::Pacts::VerifiablePactMessages.new(represented).inclusion_reason
30+
end
31+
32+
def pending_reason
33+
PactBroker::Pacts::VerifiablePactMessages.new(represented).pending_reason
34+
end
35+
end
36+
37+
link :self do | context |
38+
{
39+
href: pact_version_url(represented, context[:base_url]),
40+
name: represented.name
41+
}
42+
end
943
end
1044
end
1145
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
require_relative 'base_decorator'
2+
require_relative 'verifiable_pact_decorator'
3+
require 'pact_broker/api/pact_broker_urls'
4+
5+
module PactBroker
6+
module Api
7+
module Decorators
8+
class VerifiablePactsQueryDecorator < BaseDecorator
9+
collection :provider_version_tags
10+
11+
collection :consumer_version_selectors, class: OpenStruct do
12+
property :tag
13+
property :latest, setter: ->(fragment:, represented:, **) { represented.latest = (fragment == 'true') }
14+
end
15+
16+
17+
def from_hash(*args)
18+
# Should remember how to do this via Representable...
19+
result = super
20+
result.consumer_version_selectors = [] if result.consumer_version_selectors.nil?
21+
result.provider_version_tags = [] if result.provider_version_tags.nil?
22+
result
23+
end
24+
end
25+
end
26+
end
27+
end

lib/pact_broker/api/resources/index.rb

+12-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'pact_broker/api/resources/base_resource'
2+
require 'pact_broker/feature_toggle'
23
require 'json'
34

45
module PactBroker
@@ -18,7 +19,7 @@ def to_json
1819
end
1920

2021
def links
21-
{
22+
links_hash = {
2223
'self' =>
2324
{
2425
href: base_url,
@@ -109,12 +110,6 @@ def links
109110
href: base_url + '/metrics',
110111
title: "Get Pact Broker metrics",
111112
},
112-
'beta:pending-provider-pacts' =>
113-
{
114-
href: base_url + '/pacts/provider/{provider}/pending',
115-
title: 'Pending pact versions for the specified provider',
116-
templated: true
117-
},
118113
'curies' =>
119114
[{
120115
name: 'pb',
@@ -126,6 +121,16 @@ def links
126121
templated: true
127122
}]
128123
}
124+
125+
if PactBroker.feature_enabled?(:pacts_for_verification)
126+
links_hash['beta:provider-pacts-for-verification'] = {
127+
href: base_url + '/pacts/provider/{provider}/for-verification',
128+
title: 'Pact versions to be verified for the specified provider',
129+
templated: true
130+
}
131+
end
132+
133+
links_hash
129134
end
130135
end
131136
end

lib/pact_broker/api/resources/pending_provider_pacts.rb

-21
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
require 'pact_broker/api/resources/provider_pacts'
2+
require 'pact_broker/api/decorators/verifiable_pacts_decorator'
3+
require 'pact_broker/api/contracts/verifiable_pacts_query_schema'
4+
require 'pact_broker/api/decorators/verifiable_pacts_query_decorator'
5+
6+
module PactBroker
7+
module Api
8+
module Resources
9+
class ProviderPactsForVerification < ProviderPacts
10+
def initialize
11+
@query = Rack::Utils.parse_nested_query(request.uri.query)
12+
end
13+
14+
def malformed_request?
15+
if (errors = query_schema.call(query)).any?
16+
set_json_validation_error_messages(errors)
17+
true
18+
else
19+
false
20+
end
21+
end
22+
23+
private
24+
25+
attr_reader :query
26+
27+
def pacts
28+
pact_service.find_for_verification(
29+
provider_name,
30+
parsed_query_params.provider_version_tags,
31+
parsed_query_params.consumer_version_selectors
32+
)
33+
end
34+
35+
def resource_title
36+
"Pacts to be verified by provider #{provider_name}"
37+
end
38+
39+
def to_json
40+
PactBroker::Api::Decorators::VerifiablePactsDecorator.new(pacts).to_json(to_json_options)
41+
end
42+
43+
44+
def query_schema
45+
PactBroker::Api::Contracts::VerifiablePactsQuerySchema
46+
end
47+
48+
def parsed_query_params
49+
@parsed_query_params ||= PactBroker::Api::Decorators::VerifiablePactsQueryDecorator.new(OpenStruct.new).from_hash(query)
50+
end
51+
end
52+
end
53+
end
54+
end

lib/pact_broker/domain/pact.rb

+28-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
require 'pact_broker/db'
22
require 'pact_broker/json'
33

4+
=begin
5+
This class most accurately represents a PactPublication
6+
=end
7+
48
module PactBroker
59

610
module Domain
711
class Pact
812

13+
# The ID is the pact_publication ID
914
attr_accessor :id, :provider, :consumer_version, :consumer, :created_at, :json_content, :consumer_version_number, :revision_number, :pact_version_sha, :latest_verification, :head_tag_names
10-
1115
def initialize attributes
1216
attributes.each_pair do | key, value |
1317
self.send(key.to_s + "=", value)
@@ -30,6 +34,10 @@ def consumer_version_tag_names
3034
consumer_version.tags.collect(&:name)
3135
end
3236

37+
def latest_consumer_version_tag_names= latest_consumer_version_tag_names
38+
@latest_consumer_version_tag_names = latest_consumer_version_tag_names
39+
end
40+
3341
def to_s
3442
"Pact: consumer=#{consumer.name} provider=#{provider.name}"
3543
end
@@ -53,6 +61,25 @@ def content_hash
5361
def pact_publication_id
5462
id
5563
end
64+
65+
def select_pending_provider_version_tags(provider_version_tags)
66+
provider_version_tags - db_model.pact_version.select_provider_tags_with_successful_verifications(provider_version_tags)
67+
end
68+
69+
def pending?
70+
!pact_version.verified_successfully_by_any_provider_version?
71+
end
72+
73+
private
74+
75+
attr_accessor :db_model
76+
77+
# Really not sure about mixing Sequel model class into this PORO...
78+
# But it's much nicer than using a repository to find out the pending information :(
79+
def pact_version
80+
db_model.pact_version
81+
end
5682
end
83+
5784
end
5885
end

lib/pact_broker/pacts/all_pact_publications.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ def to_domain_without_tags
102102
revision_number: revision_number,
103103
pact_version_sha: pact_version_sha,
104104
created_at: created_at,
105-
head_tag_names: head_tag_names)
105+
head_tag_names: head_tag_names,
106+
db_model: self)
106107
end
107108

108109
def head_tag_names

lib/pact_broker/pacts/head_pact.rb

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
require 'delegate'
2+
3+
# A head pact is the pact for the latest consumer version with the specified tag
4+
# (ignoring later versions that might have the specified tag but no pact)
5+
6+
module PactBroker
7+
module Pacts
8+
class HeadPact < SimpleDelegator
9+
attr_reader :tag, :consumer_version_number
10+
11+
def initialize(pact, consumer_version_number, tag)
12+
super(pact)
13+
@consumer_version_number = consumer_version_number
14+
@tag = tag
15+
end
16+
17+
# The underlying pact publication may well be the overall latest as well, but
18+
# this row does not know that, as there will be a row with a nil tag
19+
# if it is the overall latest as well as a row with the
20+
# tag set, as the data is denormalised in the LatestTaggedPactPublications table.
21+
def overall_latest?
22+
tag.nil?
23+
end
24+
25+
def pact
26+
__getobj__()
27+
end
28+
end
29+
end
30+
end

lib/pact_broker/pacts/latest_pact_publications.rb

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
require 'pact_broker/pacts/latest_pact_publications_by_consumer_version'
2+
require 'pact_broker/pacts/head_pact'
23

34
module PactBroker
45
module Pacts
56

7+
# latest pact for each consumer/provider pair
68
class LatestPactPublications < LatestPactPublicationsByConsumerVersion
79
set_dataset(:latest_pact_publications)
8-
end
910

11+
# This pact may well be the latest for certain tags, but in this query
12+
# we don't know what they are
13+
def to_domain
14+
HeadPact.new(super, consumer_version_number, nil)
15+
end
16+
end
1017
end
1118
end
1219

lib/pact_broker/pacts/latest_tagged_pact_publications.rb

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
require 'pact_broker/pacts/latest_pact_publications_by_consumer_version'
2+
require 'pact_broker/pacts/head_pact'
23

34
module PactBroker
45
module Pacts
56

67
class LatestTaggedPactPublications < LatestPactPublicationsByConsumerVersion
78
set_dataset(:latest_tagged_pact_publications)
8-
end
99

10+
def to_domain
11+
HeadPact.new(super, consumer_version_number, tag_name)
12+
end
13+
end
1014
end
1115
end
1216

lib/pact_broker/pacts/pact_publication.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ def to_domain
5757
pact_version_sha: pact_version.sha,
5858
latest_verification: latest_verification,
5959
created_at: created_at,
60-
head_tag_names: head_tag_names
60+
head_tag_names: head_tag_names,
61+
db_model: self
6162
)
6263
end
6364

0 commit comments

Comments
 (0)