From 31fb8aa7026745ca8c7a34b8ee93966012452ecd Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Thu, 26 Sep 2019 16:42:34 +1000 Subject: [PATCH] feat: add 'pacts for verification' endpoint (#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 --- lib/pact_broker/api.rb | 4 +- .../verifiable_pacts_query_schema.rb | 41 +++++++ .../decorators/verifiable_pact_decorator.rb | 34 ++++++ .../verifiable_pacts_query_decorator.rb | 27 +++++ lib/pact_broker/api/resources/index.rb | 19 +-- .../api/resources/pending_provider_pacts.rb | 21 ---- .../provider_pacts_for_verification.rb | 54 +++++++++ lib/pact_broker/domain/pact.rb | 29 ++++- .../pacts/all_pact_publications.rb | 3 +- lib/pact_broker/pacts/head_pact.rb | 30 +++++ .../pacts/latest_pact_publications.rb | 9 +- .../pacts/latest_tagged_pact_publications.rb | 6 +- lib/pact_broker/pacts/pact_publication.rb | 3 +- lib/pact_broker/pacts/pact_version.rb | 20 ++++ lib/pact_broker/pacts/repository.rb | 37 +++++- lib/pact_broker/pacts/service.rb | 17 ++- .../pacts/squash_pacts_for_verification.rb | 37 ++++++ lib/pact_broker/pacts/verifiable_pact.rb | 30 +++++ .../pacts/verifiable_pact_messages.rb | 75 ++++++++++++ spec/features/get_pacts_to_verify_spec.rb | 41 ------- ...et_provider_pacts_for_verification_spec.rb | 39 +++++++ spec/features/get_wip_provider_pacts_spec.rb | 26 ----- spec/features/pending_pacts_spec.rb | 109 ++++++++++++++++++ .../verifiable_pacts_query_schema_spec.rb | 62 ++++++++++ .../verifiable_pact_decorator_spec.rb | 35 +++++- .../verifiable_pacts_query_decorator_spec.rb | 46 ++++++++ .../resources/pending_provider_pacts_spec.rb | 34 ------ .../provider_pacts_for_verification_spec.rb | 35 ++++++ .../pact_broker/pacts/pact_version_spec.rb | 69 +++++++++++ .../repository_find_for_verification_spec.rb | 65 +++++++++++ ...ind_wip_pact_versions_for_provider_spec.rb | 78 +++++++++---- .../service_find_for_verification_spec.rb | 51 ++++++++ .../squash_pacts_for_verification_spec.rb | 92 +++++++++++++++ .../pacts/verifiable_pact_messages_spec.rb | 93 +++++++++++++++ .../pact_broker/pacts/verifiable_pact_spec.rb | 0 35 files changed, 1200 insertions(+), 171 deletions(-) create mode 100644 lib/pact_broker/api/contracts/verifiable_pacts_query_schema.rb create mode 100644 lib/pact_broker/api/decorators/verifiable_pacts_query_decorator.rb delete mode 100644 lib/pact_broker/api/resources/pending_provider_pacts.rb create mode 100644 lib/pact_broker/api/resources/provider_pacts_for_verification.rb create mode 100644 lib/pact_broker/pacts/head_pact.rb create mode 100644 lib/pact_broker/pacts/squash_pacts_for_verification.rb create mode 100644 lib/pact_broker/pacts/verifiable_pact.rb create mode 100644 lib/pact_broker/pacts/verifiable_pact_messages.rb delete mode 100644 spec/features/get_pacts_to_verify_spec.rb create mode 100644 spec/features/get_provider_pacts_for_verification_spec.rb delete mode 100644 spec/features/get_wip_provider_pacts_spec.rb create mode 100644 spec/features/pending_pacts_spec.rb create mode 100644 spec/lib/pact_broker/api/contracts/verifiable_pacts_query_schema_spec.rb create mode 100644 spec/lib/pact_broker/api/decorators/verifiable_pacts_query_decorator_spec.rb delete mode 100644 spec/lib/pact_broker/api/resources/pending_provider_pacts_spec.rb create mode 100644 spec/lib/pact_broker/api/resources/provider_pacts_for_verification_spec.rb create mode 100644 spec/lib/pact_broker/pacts/repository_find_for_verification_spec.rb create mode 100644 spec/lib/pact_broker/pacts/service_find_for_verification_spec.rb create mode 100644 spec/lib/pact_broker/pacts/squash_pacts_for_verification_spec.rb create mode 100644 spec/lib/pact_broker/pacts/verifiable_pact_messages_spec.rb create mode 100644 spec/lib/pact_broker/pacts/verifiable_pact_spec.rb diff --git a/lib/pact_broker/api.rb b/lib/pact_broker/api.rb index ab5730760..3d35b0c71 100644 --- a/lib/pact_broker/api.rb +++ b/lib/pact_broker/api.rb @@ -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 diff --git a/lib/pact_broker/api/contracts/verifiable_pacts_query_schema.rb b/lib/pact_broker/api/contracts/verifiable_pacts_query_schema.rb new file mode 100644 index 000000000..e514f709b --- /dev/null +++ b/lib/pact_broker/api/contracts/verifiable_pacts_query_schema.rb @@ -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 diff --git a/lib/pact_broker/api/decorators/verifiable_pact_decorator.rb b/lib/pact_broker/api/decorators/verifiable_pact_decorator.rb index 423f2344f..acc042232 100644 --- a/lib/pact_broker/api/decorators/verifiable_pact_decorator.rb +++ b/lib/pact_broker/api/decorators/verifiable_pact_decorator.rb @@ -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 diff --git a/lib/pact_broker/api/decorators/verifiable_pacts_query_decorator.rb b/lib/pact_broker/api/decorators/verifiable_pacts_query_decorator.rb new file mode 100644 index 000000000..96774596f --- /dev/null +++ b/lib/pact_broker/api/decorators/verifiable_pacts_query_decorator.rb @@ -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 diff --git a/lib/pact_broker/api/resources/index.rb b/lib/pact_broker/api/resources/index.rb index a8fe64a8a..76c190c57 100644 --- a/lib/pact_broker/api/resources/index.rb +++ b/lib/pact_broker/api/resources/index.rb @@ -1,4 +1,5 @@ require 'pact_broker/api/resources/base_resource' +require 'pact_broker/feature_toggle' require 'json' module PactBroker @@ -18,7 +19,7 @@ def to_json end def links - { + links_hash = { 'self' => { href: base_url, @@ -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', @@ -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 diff --git a/lib/pact_broker/api/resources/pending_provider_pacts.rb b/lib/pact_broker/api/resources/pending_provider_pacts.rb deleted file mode 100644 index 0edfae11d..000000000 --- a/lib/pact_broker/api/resources/pending_provider_pacts.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'pact_broker/api/resources/provider_pacts' -require 'pact_broker/configuration' -require 'pact_broker/api/decorators/provider_pacts_decorator' - -module PactBroker - module Api - module Resources - class PendingProviderPacts < ProviderPacts - private - - def pacts - pact_service.find_pending_pact_versions_for_provider provider_name - end - - def resource_title - "Pending pact versions for the provider #{provider_name}" - end - end - end - end -end diff --git a/lib/pact_broker/api/resources/provider_pacts_for_verification.rb b/lib/pact_broker/api/resources/provider_pacts_for_verification.rb new file mode 100644 index 000000000..bf23b1189 --- /dev/null +++ b/lib/pact_broker/api/resources/provider_pacts_for_verification.rb @@ -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 diff --git a/lib/pact_broker/domain/pact.rb b/lib/pact_broker/domain/pact.rb index de53e2a0d..5bccff880 100644 --- a/lib/pact_broker/domain/pact.rb +++ b/lib/pact_broker/domain/pact.rb @@ -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) @@ -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 @@ -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 diff --git a/lib/pact_broker/pacts/all_pact_publications.rb b/lib/pact_broker/pacts/all_pact_publications.rb index 8924128ff..3ee222e9d 100644 --- a/lib/pact_broker/pacts/all_pact_publications.rb +++ b/lib/pact_broker/pacts/all_pact_publications.rb @@ -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 diff --git a/lib/pact_broker/pacts/head_pact.rb b/lib/pact_broker/pacts/head_pact.rb new file mode 100644 index 000000000..877397c1e --- /dev/null +++ b/lib/pact_broker/pacts/head_pact.rb @@ -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 diff --git a/lib/pact_broker/pacts/latest_pact_publications.rb b/lib/pact_broker/pacts/latest_pact_publications.rb index 1c82cab9f..ccf652c9d 100644 --- a/lib/pact_broker/pacts/latest_pact_publications.rb +++ b/lib/pact_broker/pacts/latest_pact_publications.rb @@ -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 diff --git a/lib/pact_broker/pacts/latest_tagged_pact_publications.rb b/lib/pact_broker/pacts/latest_tagged_pact_publications.rb index 913785a21..32148e0b1 100644 --- a/lib/pact_broker/pacts/latest_tagged_pact_publications.rb +++ b/lib/pact_broker/pacts/latest_tagged_pact_publications.rb @@ -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 diff --git a/lib/pact_broker/pacts/pact_publication.rb b/lib/pact_broker/pacts/pact_publication.rb index d66e730cb..9e4a7efbb 100644 --- a/lib/pact_broker/pacts/pact_publication.rb +++ b/lib/pact_broker/pacts/pact_publication.rb @@ -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 diff --git a/lib/pact_broker/pacts/pact_version.rb b/lib/pact_broker/pacts/pact_version.rb index a7e8457ed..c2e1a8a09 100644 --- a/lib/pact_broker/pacts/pact_version.rb +++ b/lib/pact_broker/pacts/pact_version.rb @@ -50,6 +50,26 @@ def latest_consumer_version_number latest_consumer_version.number end + def select_provider_tags_with_successful_verifications(tags) + tags.select do | tag | + PactVersion.where(Sequel[:pact_versions][:id] => id) + .join(:verifications, Sequel[:verifications][:pact_version_id] => Sequel[:pact_versions][:id]) + .join(:versions, Sequel[:versions][:id] => Sequel[:verifications][:provider_version_id]) + .join(:tags, Sequel[:tags][:version_id] => Sequel[:versions][:id]) + .where(Sequel[:tags][:name] => tag) + .where(Sequel[:verifications][:success] => true) + .any? + end + end + + def verified_successfully_by_any_provider_version? + PactVersion.where(Sequel[:pact_versions][:id] => id) + .join(:verifications, Sequel[:verifications][:pact_version_id] => Sequel[:pact_versions][:id]) + .join(:versions, Sequel[:versions][:id] => Sequel[:verifications][:provider_version_id]) + .where(Sequel[:verifications][:success] => true) + .any? + end + def upsert self.class.upsert(to_hash, [:consumer_id, :provider_id, :sha]) end diff --git a/lib/pact_broker/pacts/repository.rb b/lib/pact_broker/pacts/repository.rb index 40b2095fe..f5020f828 100644 --- a/lib/pact_broker/pacts/repository.rb +++ b/lib/pact_broker/pacts/repository.rb @@ -12,6 +12,7 @@ require 'pact_broker/pacts/parse' require 'pact_broker/matrix/head_row' require 'pact_broker/pacts/latest_pact_publication_id_by_consumer_version' +require 'pact_broker/pacts/verifiable_pact' module PactBroker module Pacts @@ -124,10 +125,30 @@ def find_latest_pact_versions_for_provider provider_name, tag = nil end end - def find_pending_pact_versions_for_provider provider_name - provider_id = pacticipant_repository.find_by_name(provider_name).id - pact_publication_ids = PactBroker::Matrix::HeadRow.where(provider_id: provider_id).exclude(success: true).select_for_subquery(:pact_publication_id) - AllPactPublications.where(id: pact_publication_ids).order_ignore_case(:consumer_name).order_append(:consumer_version_order).collect(&:to_domain) + def find_wip_pact_versions_for_provider provider_name, provider_tags = [] + 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]) + .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) + + 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) + end end def find_pact_versions_for_provider provider_name, tag = nil @@ -263,6 +284,14 @@ def find_previous_pacts pact end end + # Returns a list of Domain::Pact objects the represent pact publications + def find_for_verification(provider_name, consumer_version_selectors) + latest_tags = consumer_version_selectors.any? ? + consumer_version_selectors.select(&:latest).collect(&:tag) : + nil + find_latest_pact_versions_for_provider(provider_name, latest_tags) + end + private def find_previous_distinct_pact_by_sha pact diff --git a/lib/pact_broker/pacts/service.rb b/lib/pact_broker/pacts/service.rb index 1370112c1..bd8c997df 100644 --- a/lib/pact_broker/pacts/service.rb +++ b/lib/pact_broker/pacts/service.rb @@ -2,6 +2,8 @@ require 'pact_broker/services' require 'pact_broker/logging' require 'pact_broker/pacts/merger' +require 'pact_broker/pacts/verifiable_pact' +require 'pact_broker/pacts/squash_pacts_for_verification' module PactBroker module Pacts @@ -12,6 +14,7 @@ module Service extend PactBroker::Repositories extend PactBroker::Services include PactBroker::Logging + extend SquashPactsForVerification def find_latest_pact params pact_repository.find_latest_pact(params[:consumer_name], params[:provider_name], params[:tag]) @@ -80,8 +83,8 @@ def find_latest_pact_versions_for_provider provider_name, options = {} pact_repository.find_latest_pact_versions_for_provider provider_name, options[:tag] end - def find_pending_pact_versions_for_provider provider_name - pact_repository.find_pending_pact_versions_for_provider provider_name + def find_wip_pact_versions_for_provider provider_name + pact_repository.find_wip_pact_versions_for_provider provider_name end def find_pact_versions_for_provider provider_name, options = {} @@ -110,6 +113,16 @@ def find_distinct_pacts_between consumer, options distinct end + def find_for_verification(provider_name, provider_version_tags, consumer_version_selectors) + 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 + end + private # Overwriting an existing pact with the same consumer/provider/consumer version number diff --git a/lib/pact_broker/pacts/squash_pacts_for_verification.rb b/lib/pact_broker/pacts/squash_pacts_for_verification.rb new file mode 100644 index 000000000..64fd93a23 --- /dev/null +++ b/lib/pact_broker/pacts/squash_pacts_for_verification.rb @@ -0,0 +1,37 @@ +# All of these pacts have the same underlying pact_version_sha (content) +# No point verifying them multiple times, so squash all the relevant info into one +# "verifiable pact" + +module PactBroker + module Pacts + module SquashPactsForVerification + def self.call(provider_version_tags, head_pacts) + domain_pact = head_pacts.first.pact + pending_provider_tags = [] + pending = nil + if provider_version_tags.any? + pending_provider_tags = domain_pact.select_pending_provider_version_tags(provider_version_tags) + pending = pending_provider_tags.any? + else + pending = domain_pact.pending? + end + + non_pending_provider_tags = provider_version_tags - pending_provider_tags + + head_consumer_tags = head_pacts.collect(&:tag) + overall_latest = head_consumer_tags.include?(nil) + VerifiablePact.new(domain_pact, + pending, + pending_provider_tags, + non_pending_provider_tags, + head_consumer_tags.compact, + overall_latest + ) + end + + def squash_pacts_for_verification(*args) + SquashPactsForVerification.call(*args) + end + end + end +end diff --git a/lib/pact_broker/pacts/verifiable_pact.rb b/lib/pact_broker/pacts/verifiable_pact.rb new file mode 100644 index 000000000..bebc7f1a5 --- /dev/null +++ b/lib/pact_broker/pacts/verifiable_pact.rb @@ -0,0 +1,30 @@ +require 'delegate' + +module PactBroker + module Pacts + class VerifiablePact < SimpleDelegator + attr_reader :pending, :pending_provider_tags, :non_pending_provider_tags, :head_consumer_tags + + def initialize(pact, pending, pending_provider_tags = [], non_pending_provider_tags = [], head_consumer_tags = [], overall_latest = 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 + end + + def consumer_tags + head_consumer_tags + end + + def overall_latest? + @overall_latest + end + + def pending? + pending + end + end + end +end diff --git a/lib/pact_broker/pacts/verifiable_pact_messages.rb b/lib/pact_broker/pacts/verifiable_pact_messages.rb new file mode 100644 index 000000000..8d595bddc --- /dev/null +++ b/lib/pact_broker/pacts/verifiable_pact_messages.rb @@ -0,0 +1,75 @@ +module PactBroker + module Pacts + class VerifiablePactMessages + extend Forwardable + + READ_MORE = "Read more at https://pact.io/pending" + + delegate [:consumer_name, :provider_name, :head_consumer_tags, :pending_provider_tags, :non_pending_provider_tags, :pending?] => :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}" + else + "This pact is being verified because it is the latest pact between #{consumer_name} and #{provider_name}." + 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}" + 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}" + end + end + + private + + attr_reader :verifiable_pact + + def join(list, last_joiner = " and ") + quoted_list = list.collect { | tag | "'#{tag}'" } + comma_joined = quoted_list[0..-3] || [] + and_joined = quoted_list[-2..-1] || quoted_list + if comma_joined.any? + "#{comma_joined.join(', ')}, #{and_joined.join(last_joiner)}" + else + and_joined.join(last_joiner) + end + end + + def joined_head_consumer_tags + join(head_consumer_tags) + same_content_note + end + + def same_content_note + case head_consumer_tags.size + when 1 then "" + when 2 then " (both have the same content)" + else " (all have the same content)" + end + end + + def pending_provider_tags_description + case pending_provider_tags.size + when 0 then provider_name + when 1 then "any version of #{provider_name} with tag '#{pending_provider_tags.first}'" + else "any versions of #{provider_name} with tag #{join(pending_provider_tags)}" + end + end + + def non_pending_provider_tags_description + case non_pending_provider_tags.size + when 0 then provider_name + when 1 then "a version of #{provider_name} with tag '#{non_pending_provider_tags.first}'" + else "a version of #{provider_name} with tag #{join(non_pending_provider_tags)}" + end + end + end + end +end diff --git a/spec/features/get_pacts_to_verify_spec.rb b/spec/features/get_pacts_to_verify_spec.rb deleted file mode 100644 index bfb02535d..000000000 --- a/spec/features/get_pacts_to_verify_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -describe "fetching pacts to verify", pending: 'not yet implemented' do - before do - # td.create_pact_with_hierarchy("Foo", "1", "Bar") - # .create_consumer_version_tag("feat-1") - # .create_provider_version_tag("master") - end - let(:path) { "/pacts/provider/Bar/verifiable" } - let(:query) do - # need to provide the provider tags that will be used when publishing the - # verification results, as whether a pact - # is pending or not depends on which provider tag we're talking about - # eg. if content has been verified on git branch (broker tag) feat-2, - # it's still pending on master, and shouldn't fail the build - { - protocol: "http1", # other option is "message" - include_other_pending: true, # whether or not to include pending pacts not already specified by the consumer_version_tags('head' pacts that have not yet been successfully verified) - provider_version_tags: [{ name: "feat-2" }], # the provider tags that will be applied to this app version when the results are published - consumer_version_tags: [ - { name: "feat-1", fallback: "master" }, # allow a fallback to be provided for the "branch mirroring" workflow - { name: "test", required: true }, # default to optional or required??? Ron? - { name: "prod", all: true } # by default return latest, but allow "all" to be specified for things like mobile apps - ] - } - end - let(:rack_env) { { 'HTTP_ACCEPT' => 'application/hal+json' } } - let(:response_body_hash) { JSON.parse(subject.body, symbolize_names: true) } - - subject { get(path, query, rack_env) } - - it "returns a 200 HAL JSON response" do - expect(subject).to be_a_hal_json_success_response - end - - it "returns a list of links to the pacts" do - expect(response_body_hash[:_embedded][:'pacts']).to be_instance_of(Array) - end - - it "indicates whether a pact is pending or not" do - expect(response_body_hash[:_embedded][:'pacts'].first[:pending]).to be true - end -end diff --git a/spec/features/get_provider_pacts_for_verification_spec.rb b/spec/features/get_provider_pacts_for_verification_spec.rb new file mode 100644 index 000000000..29fb62ec6 --- /dev/null +++ b/spec/features/get_provider_pacts_for_verification_spec.rb @@ -0,0 +1,39 @@ +describe "Get provider pacts for verification" do + let(:last_response_body) { JSON.parse(subject.body, symbolize_names: true) } + let(:pacts) { last_response_body[:_embedded][:'pacts'] } + subject { get path; last_response } + + context "when the provider exists" do + before do + TestDataBuilder.new + .create_provider("Provider") + .create_consumer("Consumer") + .create_consumer_version("0.0.1") + .create_pact + .create_consumer("Consumer 2") + .create_consumer_version("4.5.6") + .create_consumer_version_tag("prod") + .create_pact + end + + context "with no tag specified" do + let(:path) { "/pacts/provider/Provider/for-verification" } + + it "returns a 200 HAL JSON response" do + expect(subject).to be_a_hal_json_success_response + end + + it "returns a list of links to the pacts" do + expect(pacts.size).to eq 2 + end + end + end + + context "when the provider does not exist" do + let(:path) { "/pacts/provider/Provider" } + + it "returns a 404 response" do + expect(subject).to be_a_404_response + end + end +end diff --git a/spec/features/get_wip_provider_pacts_spec.rb b/spec/features/get_wip_provider_pacts_spec.rb deleted file mode 100644 index b1a1824ed..000000000 --- a/spec/features/get_wip_provider_pacts_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -describe "Get pending provider pacts" do - subject { get path; last_response } - - let(:last_response_body) { JSON.parse(subject.body, symbolize_names: true) } - let(:pact_links) { last_response_body[:_links][:'pb:pacts'] } - - context "when the provider exists" do - before do - TestDataBuilder.new - .create_provider("Provider") - .create_consumer("Consumer") - .create_consumer_version("0.0.1") - .create_pact - end - - let(:path) { "/pacts/provider/Provider/pending" } - - it "returns a 200 HAL JSON response" do - expect(subject).to be_a_hal_json_success_response - end - - it "returns a list of links to the pacts" do - expect(pact_links.size).to eq 1 - end - end -end diff --git a/spec/features/pending_pacts_spec.rb b/spec/features/pending_pacts_spec.rb new file mode 100644 index 000000000..18b4ba0bb --- /dev/null +++ b/spec/features/pending_pacts_spec.rb @@ -0,0 +1,109 @@ +RSpec.describe "the pending lifecycle of a pact (with no tags)" do + let(:pact_content_1) { { some: "interactions" }.to_json } + let(:pact_content_2) { { some: "other interactions" }.to_json } + let(:request_headers) { { "CONTENT_TYPE" => "application/json", "HTTP_ACCEPT" => "application/hal+json"} } + let(:failed_verification_results) do + { + providerApplicationVersion: "2", + success: false + }.to_json + end + let(:successful_verification_results) do + { + providerApplicationVersion: "2", + success: true + }.to_json + end + + def publish_pact + put("/pacts/provider/Bar/consumer/Foo/version/1", pact_content_1, request_headers) + end + + def get_pacts_for_verification + get("/pacts/provider/Bar/for-verification", nil, request_headers) + end + + def pact_url_from(pacts_for_verification_response) + JSON.parse(pacts_for_verification_response.body)["_embedded"]["pacts"][0]["_links"]["self"]["href"] + end + + def get_pact(pact_url) + get pact_url, nil, request_headers + end + + def verification_results_url_from(pact_response) + JSON.parse(pact_response.body)["_links"]["pb:publish-verification-results"]["href"] + end + + def publish_verification_results(verification_results_url, results) + post(verification_results_url, results, request_headers) + end + + def pending_status_from(pacts_for_verification_response) + JSON.parse(pacts_for_verification_response.body)["_embedded"]["pacts"][0]["verificationProperties"]["pending"] + end + + + context "a pact" do + describe "when it is first published" do + it "is pending" do + publish_pact + pacts_for_verification_response = get_pacts_for_verification + expect(pending_status_from(pacts_for_verification_response)).to be true + end + end + + describe "when it is verified unsuccessfully" do + it "is still pending" do + # CONSUMER BUILD + # publish pact + publish_pact + + # PROVIDER BUILD + # fetch pacts to verify + pacts_for_verification_response = get_pacts_for_verification + pact_url = pact_url_from(pacts_for_verification_response) + pact_response = get_pact(pact_url) + + # verify pact... failure... + + # publish failure verification results + verification_results_url = verification_results_url_from(pact_response) + publish_verification_results(verification_results_url, failed_verification_results) + + # ANOTHER PROVIDER BUILD + # get pacts for verification + pacts_for_verification_response = get_pacts_for_verification + # still pending + expect(pending_status_from(pacts_for_verification_response)).to be true + end + end + + describe "when it is verified successfully" do + it "is no longer pending" do + # CONSUMER BUILD + publish_pact + + # PROVIDER BUILD + pacts_for_verification_response = get_pacts_for_verification + + # fetch pact + pact_url = pact_url_from(pacts_for_verification_response) + pact_response = get_pact(pact_url) + + # verify pact... success! + + # publish failure verification results + verification_results_url = verification_results_url_from(pact_response) + publish_verification_results(verification_results_url, successful_verification_results) + + # ANOTHER PROVIDER BUILD 2 + # get pacts for verification + # publish successful verification results + pacts_for_verification_response = get_pacts_for_verification + # not pending any more + expect(pending_status_from(pacts_for_verification_response)).to be false + end + end + end +end diff --git a/spec/lib/pact_broker/api/contracts/verifiable_pacts_query_schema_spec.rb b/spec/lib/pact_broker/api/contracts/verifiable_pacts_query_schema_spec.rb new file mode 100644 index 000000000..e427156b3 --- /dev/null +++ b/spec/lib/pact_broker/api/contracts/verifiable_pacts_query_schema_spec.rb @@ -0,0 +1,62 @@ +require 'pact_broker/api/contracts/verifiable_pacts_query_schema' + +module PactBroker + module Api + module Contracts + describe VerifiablePactsQuerySchema do + + let(:params) do + { + provider_version_tags: provider_version_tags, + consumer_version_selectors: consumer_version_selectors + } + end + + let(:provider_version_tags) { %w[master] } + + let(:consumer_version_selectors) do + [{ + tag: "master", + latest: "true" + }] + end + + subject { VerifiablePactsQuerySchema.(params) } + + context "when the params are valid" do + it "has no errors" do + expect(subject).to eq({}) + end + end + + context "when provider_version_tags is not an array" do + let(:provider_version_tags) { "foo" } + + it { is_expected.to have_key(:provider_version_tags) } + end + + context "when the consumer_version_selector is missing a tag" do + let(:consumer_version_selectors) do + [{}] + end + + it "flattens the messages" do + expect(subject[:consumer_version_selectors].first).to eq "tag is missing at index 0" + end + end + + context "whne the consumer_version_selectors is missing the latest" do + let(:consumer_version_selectors) do + [{ + tag: "master" + }] + end + + it "has no errors" do + expect(subject).to eq({}) + end + end + end + end + end +end diff --git a/spec/lib/pact_broker/api/decorators/verifiable_pact_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/verifiable_pact_decorator_spec.rb index fc2616fb0..687b91c31 100644 --- a/spec/lib/pact_broker/api/decorators/verifiable_pact_decorator_spec.rb +++ b/spec/lib/pact_broker/api/decorators/verifiable_pact_decorator_spec.rb @@ -4,26 +4,51 @@ module PactBroker module Api module Decorators describe VerifiablePactDecorator do - + before do + allow(decorator).to receive(:pact_version_url).and_return('/pact-version-url') + allow_any_instance_of(PactBroker::Pacts::VerifiablePactMessages).to receive(:inclusion_reason).and_return("inclusion_reason") + allow_any_instance_of(PactBroker::Pacts::VerifiablePactMessages).to receive(:pending_reason).and_return("pending_reason") + end let(:expected_hash) do { - "pending" => true, + "verificationProperties" => { + "pending" => true, + "pendingReason" => "pending_reason", + "inclusionReason" => "inclusion_reason" + }, "_links" => { - "self" => "http://pact" + "self" => { + "href" => "/pact-version-url", + "name" => "name" + } } } end let(:decorator) { VerifiablePactDecorator.new(pact) } - let(:pact) { double('pact') } + let(:pact) do + double('pact', + pending: true, + name: "name", + provider_name: "Bar", + pending_provider_tags: pending_provider_tags, + consumer_tags: consumer_tags) + end + let(:pending_provider_tags) { %w[dev] } + let(:consumer_tags) { %w[dev] } let(:json) { decorator.to_json(options) } let(:options) { { user_options: { base_url: 'http://example.org' } } } subject { JSON.parse(json) } - it "generates a matching hash", pending: true do + it "generates a matching hash" do expect(subject).to match_pact(expected_hash) end + + it "creates the pact version url" do + expect(decorator).to receive(:pact_version_url).with(pact, 'http://example.org') + subject + end end end end diff --git a/spec/lib/pact_broker/api/decorators/verifiable_pacts_query_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/verifiable_pacts_query_decorator_spec.rb new file mode 100644 index 000000000..ebeb4a328 --- /dev/null +++ b/spec/lib/pact_broker/api/decorators/verifiable_pacts_query_decorator_spec.rb @@ -0,0 +1,46 @@ +require 'pact_broker/api/decorators/verifiable_pacts_query_decorator' + +module PactBroker + module Api + module Decorators + describe VerifiablePactsQueryDecorator do + let(:params) do + { + "provider_version_tags" => provider_version_tags, + "consumer_version_selectors" => consumer_version_selectors + } + end + let(:provider_version_tags) { %w[dev] } + let(:consumer_version_selectors) do + [{"tag" => "dev", "ignored" => "foo"}] + end + + subject { VerifiablePactsQueryDecorator.new(OpenStruct.new).from_hash(params) } + + context "when latest is not specified" do + it "defaults to nil" do + expect(subject.consumer_version_selectors.first.latest).to be nil + end + end + + context "when latest is a string" do + let(:consumer_version_selectors) do + [{"tag" => "dev", "latest" => "true"}] + end + + it "casts it to a boolean" do + expect(subject.consumer_version_selectors.first.latest).to be true + end + end + + context "when there are no consumer_version_selectors" do + let(:params) { {} } + + it "returns an empty array" do + expect(subject.consumer_version_selectors).to eq [] + end + end + end + end + end +end diff --git a/spec/lib/pact_broker/api/resources/pending_provider_pacts_spec.rb b/spec/lib/pact_broker/api/resources/pending_provider_pacts_spec.rb deleted file mode 100644 index b40b578c9..000000000 --- a/spec/lib/pact_broker/api/resources/pending_provider_pacts_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'pact_broker/api/resources/latest_provider_pacts' - -module PactBroker - module Api - module Resources - describe PendingProviderPacts do - before do - allow(PactBroker::Pacts::Service).to receive(:find_pending_pact_versions_for_provider).and_return(pacts) - allow(PactBroker::Api::Decorators::ProviderPactsDecorator).to receive(:new).and_return(decorator) - allow_any_instance_of(PendingProviderPacts).to receive(:resource_exists?).and_return(provider) - end - - let(:provider) { double('provider') } - let(:pacts) { double('pacts') } - let(:path) { '/pacts/provider/Bar/pending' } - let(:decorator) { instance_double('PactBroker::Api::Decorators::ProviderPactsDecorator') } - - subject { get path; last_response } - - it "finds the pending pacts for the provider" do - expect(PactBroker::Pacts::Service).to receive(:find_pending_pact_versions_for_provider).with("Bar") - subject - end - - it "sets the correct resource title" do - expect(decorator).to receive(:to_json) do | options | - expect(options[:user_options][:title]).to eq "Pending pact versions for the provider Bar" - end - subject - end - end - end - end -end diff --git a/spec/lib/pact_broker/api/resources/provider_pacts_for_verification_spec.rb b/spec/lib/pact_broker/api/resources/provider_pacts_for_verification_spec.rb new file mode 100644 index 000000000..4cc2f8b47 --- /dev/null +++ b/spec/lib/pact_broker/api/resources/provider_pacts_for_verification_spec.rb @@ -0,0 +1,35 @@ +require 'pact_broker/api/resources/provider_pacts_for_verification' + +module PactBroker + module Api + module Resources + describe ProviderPactsForVerification do + before do + allow(PactBroker::Pacts::Service).to receive(:find_for_verification).and_return(pacts) + allow(PactBroker::Api::Decorators::VerifiablePactsDecorator).to receive(:new).and_return(decorator) + allow_any_instance_of(ProviderPactsForVerification).to receive(:resource_exists?).and_return(provider) + end + + let(:provider) { double('provider') } + let(:pacts) { double('pacts') } + let(:path) { '/pacts/provider/Bar/for-verification' } + let(:decorator) { instance_double('PactBroker::Api::Decorators::VerifiablePactsDecorator') } + + subject { get(path, provider_version_tags: ['master'], consumer_version_selectors: [ { tag: "dev", latest: true}]) } + + it "finds the pacts for verification by the provider" do + # Naughty not mocking out the query parsing... + expect(PactBroker::Pacts::Service).to receive(:find_for_verification).with("Bar", ["master"], [ OpenStruct.new(tag: "dev", latest: true)]) + subject + end + + it "sets the correct resource title" do + expect(decorator).to receive(:to_json) do | options | + expect(options[:user_options][:title]).to eq "Pacts to be verified by provider Bar" + end + subject + end + end + end + end +end diff --git a/spec/lib/pact_broker/pacts/pact_version_spec.rb b/spec/lib/pact_broker/pacts/pact_version_spec.rb index 11ead67c7..3476ce16d 100644 --- a/spec/lib/pact_broker/pacts/pact_version_spec.rb +++ b/spec/lib/pact_broker/pacts/pact_version_spec.rb @@ -108,6 +108,75 @@ module Pacts expect(subject.number).to eq 3 end end + + describe "select_provider_tags_with_successful_verifications" do + before do + td.create_pact_with_hierarchy("Foo", "1", "Bar") + .create_verification(provider_version: "20", tag_names: ['dev'], success: true) + .create_verification(provider_version: "21", number: 2) + end + + let(:pact_version) { PactVersion.last } + let(:tags) { %w[dev] } + + subject { pact_version.select_provider_tags_with_successful_verifications(tags) } + + context "when the pact version has been successfully verified by all the specified tags" do + let(:tags) { %w[dev] } + + it { is_expected.to eq tags } + end + + context "when the pact version has been verified successfully by one the two specified tags" do + let(:tags) { %w[dev feat-foo] } + + it { is_expected.to eq %w[dev] } + end + + context "when the pact version has been verified unsuccessfully by all of the specified tags" do + before do + td.create_verification(provider_version: "30", number: 10, tag_names: ['feat-bar'], success: false) + end + + let(:tags) { %w[feat-bar] } + + it { is_expected.to eq [] } + end + end + + describe "#verified_successfully_by_any_provider_version?" do + + let(:pact_version) { PactVersion.last } + + subject { pact_version.verified_successfully_by_any_provider_version? } + + context "when the pact version has been successfully verified before" do + before do + td.create_pact_with_hierarchy("Foo", "1", "Bar") + .create_verification(provider_version: "20", success: true) + .create_verification(provider_version: "21", number: 2, success: false) + end + + it { is_expected.to be true } + end + + context "when the pact version has been unsuccessfully verified before" do + before do + td.create_pact_with_hierarchy("Foo", "1", "Bar") + .create_verification(provider_version: "21", number: 2, success: false) + end + + it { is_expected.to be false } + end + + context "when the pact version has not been verified ever before" do + before do + td.create_pact_with_hierarchy("Foo", "1", "Bar") + end + + it { is_expected.to be false } + end + end end end end diff --git a/spec/lib/pact_broker/pacts/repository_find_for_verification_spec.rb b/spec/lib/pact_broker/pacts/repository_find_for_verification_spec.rb new file mode 100644 index 000000000..4417b15c1 --- /dev/null +++ b/spec/lib/pact_broker/pacts/repository_find_for_verification_spec.rb @@ -0,0 +1,65 @@ +require 'pact_broker/pacts/repository' + +module PactBroker + module Pacts + describe Repository do + let(:td) { TestDataBuilder.new } + + describe "#find_for_verification" do + + def find_by_consumer_version_number(consumer_version_number) + subject.find{ |pact| pact.consumer_version_number == consumer_version_number } + end + + before do + td.create_pact_with_hierarchy("Foo", "bar-latest-prod", "Bar") + .create_consumer_version_tag("prod") + .create_consumer_version("not-latest-dev", tag_names: ["dev"]) + .comment("next pact not selected") + .create_pact + .create_consumer_version("bar-latest-dev", tag_names: ["dev"]) + .create_pact + .create_consumer("Baz") + .create_consumer_version("baz-latest-dev", tag_names: ["dev"]) + .create_pact + end + + subject { Repository.new.find_for_verification("Bar", consumer_version_selectors) } + + context "when consumer tag names are specified" do + let(:pact_selector_1) { double('selector', tag: 'dev', latest: true) } + let(:pact_selector_2) { double('selector', tag: 'prod', latest: true) } + let(:consumer_version_selectors) do + [pact_selector_1, pact_selector_2] + end + + it "returns the latest pact with the specified tags for each consumer" do + expect(find_by_consumer_version_number("bar-latest-prod")).to_not be nil + expect(find_by_consumer_version_number("bar-latest-dev")).to_not be nil + expect(find_by_consumer_version_number("baz-latest-dev")).to_not be nil + expect(subject.size).to eq 3 + end + + it "sets the latest_consumer_version_tag_names" do + expect(find_by_consumer_version_number("bar-latest-prod").tag).to eq 'prod' + end + end + + context "when no selectors are specified" do + let(:consumer_version_selectors) { [] } + + it "returns the latest pact for each provider" do + expect(find_by_consumer_version_number("bar-latest-dev")).to_not be nil + expect(find_by_consumer_version_number("baz-latest-dev")).to_not be nil + expect(subject.size).to eq 2 + end + + it "does not set the tag name" do + expect(find_by_consumer_version_number("bar-latest-dev").tag).to be nil + expect(find_by_consumer_version_number("bar-latest-dev").overall_latest?).to be true + end + end + end + end + end +end diff --git a/spec/lib/pact_broker/pacts/repository_find_wip_pact_versions_for_provider_spec.rb b/spec/lib/pact_broker/pacts/repository_find_wip_pact_versions_for_provider_spec.rb index a79b3f8d5..da17a7852 100644 --- a/spec/lib/pact_broker/pacts/repository_find_wip_pact_versions_for_provider_spec.rb +++ b/spec/lib/pact_broker/pacts/repository_find_wip_pact_versions_for_provider_spec.rb @@ -5,77 +5,107 @@ module Pacts describe Repository do let(:td) { TestDataBuilder.new } - describe "find_pending_pact_versions_for_provider" do - subject { Repository.new.find_pending_pact_versions_for_provider("bar") } + describe "find_wip_pact_versions_for_provider" do - context "when the latest pact for a tag has been successfully verified" do + let(:provider_tags) { %w[dev] } + subject { Repository.new.find_wip_pact_versions_for_provider("bar", provider_tags) } + + context "when there are no tags" do + let(:provider_tags) { [] } + + it "returns an empty list" do + expect(subject).to eq [] + end + end + + context "when the latest pact for a tag has been successfully verified by the given provider tag" do before do td.create_pact_with_hierarchy("foo", "1", "bar") .comment("above not included because it's not the latest prod") .create_consumer_version("2") .create_consumer_version_tag("prod") .create_pact - .create_verification(provider_version: "3", comment: "not included because already verified") + .create_verification(provider_version: "3", tag_names: %w[dev], comment: "not included because already verified") end + let(:provider_tags) { %w[dev] } + it "is not included" do expect(subject.size).to be 0 end end - context "when the latest pact without a tag has failed verification" do + context "when the latest pact for a tag has been successfully verified by one of the given provider tags, but not the other" do before do td.create_pact_with_hierarchy("foo", "1", "bar") - .create_verification(provider_version: "3", success: false) + .create_consumer_version_tag("prod") + .create_verification(provider_version: "3", tag_names: %w[dev], comment: "not included because already verified") end + let(:provider_tags) { %w[dev feat-1] } + it "is included" do expect(subject.size).to be 1 end + + it "sets the pending tags to the tag that has not yet been verified" do + expect(subject.first.pending_provider_tags).to eq %w[feat-1] + end end - context "when the latest pact without a tag has not been verified" do + context "when the latest pact for a tag has failed verification from the specified provider version" do before do td.create_pact_with_hierarchy("foo", "1", "bar") - .create_consumer_version("2") - .create_pact + .create_consumer_version_tag("feat-1") + .create_verification(provider_version: "3", success: false, tag_names: %[dev]) end it "is included" do - expect(subject.first.consumer_version_number).to eq "2" expect(subject.size).to be 1 end + + it "sets the pending tags" do + expect(subject.first.pending_provider_tags).to eq %w[dev] + end end - context "when the latest pact for a tag has failed verification" do + context "when there are no consumer tags" do before do td.create_pact_with_hierarchy("foo", "1", "bar") - .create_consumer_version_tag("prod") - .create_verification(provider_version: "3", success: true) - .create_consumer_version("2", tag_names: ["prod"]) - .create_pact - .create_verification(provider_version: "5", success: false) + .create_verification(provider_version: "3", success: false, tag_names: %[dev]) end - it "is included" do - expect(subject.first.consumer_version_number).to eq "2" - expect(subject.size).to be 1 + it "returns an empty list" do + expect(subject).to eq [] + end + end + + context "when the latest pact for a tag has successful and failed verifications" do + before do + td.create_pact_with_hierarchy("foo", "1", "bar") + .create_consumer_version_tag("dev") + .create_verification(provider_version: "3", success: true, tag_names: %[dev]) + .create_verification(provider_version: "5", success: false, number: 2, tag_names: %[dev]) + end + + it "is not included, but maybe it should be? can't really work out a scenario where this is likely to happen" do + expect(subject).to eq [] end end context "when the latest pact for a tag has not been verified" do before do td.create_pact_with_hierarchy("foo", "1", "bar") - .create_consumer_version_tag("prod") - .create_verification(provider_version: "5") - .create_consumer_version("2", tag_names: ["prod"]) - .create_pact + .create_consumer_version_tag("dev") end it "is included" do - expect(subject.first.consumer_version_number).to eq "2" expect(subject.size).to be 1 end + + it "sets the pending tags" do + expect(subject.first.pending_provider_tags).to eq %w[dev] + end end context "when the provider name does not match the given provider name" do diff --git a/spec/lib/pact_broker/pacts/service_find_for_verification_spec.rb b/spec/lib/pact_broker/pacts/service_find_for_verification_spec.rb new file mode 100644 index 000000000..a2794a308 --- /dev/null +++ b/spec/lib/pact_broker/pacts/service_find_for_verification_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' +require 'pact_broker/pacts/service' +require 'pact_broker/pacts/pact_params' + + +module PactBroker + + module Pacts + describe Service do + let(:td) { TestDataBuilder.new } + + describe "find_for_verification" do + include_context "stubbed repositories" + + let(:head_pacts) { [pact_1, pact_2] } + let(:head_tag_1) { "dev" } + let(:head_tag_2) { "feat-x" } + let(:pact_version_sha_1) { "1" } + let(:pact_version_sha_2) { "2" } + let(:domain_pact_1) { double('pact1', pending?: true) } + let(:domain_pact_2) { double('pact2', pending?: true) } + + let(:pact_1) do + double("HeadPact", + head_tag: head_tag_1, + pact_version_sha: pact_version_sha_1, + pact: domain_pact_1 + ) + end + + let(:pact_2) do + double("HeadPact", + head_tag: head_tag_2, + pact_version_sha: pact_version_sha_2, + pact: domain_pact_2 + ) + end + + let(:provider_name) { "Bar" } + let(:provider_version_tags) { [] } + let(:consumer_version_selectors) { [] } + + before do + allow(pact_repository).to receive(:find_for_verification).and_return(head_pacts) + end + + subject { Service.find_for_verification(provider_name, provider_version_tags, consumer_version_selectors) } + end + end + end +end diff --git a/spec/lib/pact_broker/pacts/squash_pacts_for_verification_spec.rb b/spec/lib/pact_broker/pacts/squash_pacts_for_verification_spec.rb new file mode 100644 index 000000000..859b6a280 --- /dev/null +++ b/spec/lib/pact_broker/pacts/squash_pacts_for_verification_spec.rb @@ -0,0 +1,92 @@ +require 'pact_broker/pacts/squash_pacts_for_verification' + +module PactBroker + module Pacts + module SquashPactsForVerification + describe ".call" do + let(:head_pacts) { [pact_1, pact_2] } + let(:head_tag_1) { "dev" } + let(:head_tag_2) { "feat-x" } + let(:pact_version_sha_1) { "1" } + let(:pact_version_sha_2) { "2" } + let(:domain_pact_1) do + double('pact1', + pending?: pending_1, + select_pending_provider_version_tags: pending_provider_version_tags + ) + end + let(:domain_pact_2) { double('pact2', pending?: pending_2) } + let(:pending_1) { false } + let(:pending_2) { false } + let(:pending_provider_version_tags) { [] } + + let(:pact_1) do + double("HeadPact", + tag: head_tag_1, + pact_version_sha: pact_version_sha_1, + pact: domain_pact_1 + ) + end + + let(:pact_2) do + double("HeadPact", + tag: head_tag_2, + pact_version_sha: pact_version_sha_2, + pact: domain_pact_2 + ) + end + + let(:provider_name) { "Bar" } + let(:provider_version_tags) { [] } + + subject { SquashPactsForVerification.call(provider_version_tags, head_pacts) } + + context "when all of the consumer tags are not nil" do + its(:head_consumer_tags) { is_expected.to eq %w[dev feat-x] } + its(:overall_latest?) { is_expected.to be false } + end + + context "when one of the consumer tags is nil" do + let(:head_tag_2) { nil } + its(:head_consumer_tags) { is_expected.to eq %w[dev] } + its(:overall_latest?) { is_expected.to be true } + end + + context "when there are no provider tags" do + context "when the pact version is not pending" do + its(:pending) { is_expected.to be false } + its(:pending_provider_tags) { is_expected.to eq [] } + its(:non_pending_provider_tags) { is_expected.to eq [] } + end + + context "when the pact version is pending" do + let(:pending_1) { true } + its(:pending) { is_expected.to be true } + its(:pending_provider_tags) { is_expected.to eq [] } + its(:non_pending_provider_tags) { is_expected.to eq [] } + end + end + + context "when there are provider version tags" do + let(:provider_version_tags) { %w[dev feat-x] } + + context "when a pact is pending for any of the provider tags" do + let(:pending_provider_version_tags) { %w[dev] } + + its(:pending) { is_expected.to be true } + its(:pending_provider_tags) { is_expected.to eq %w[dev] } + its(:non_pending_provider_tags) { is_expected.to eq %w[feat-x] } + end + + context "when a pact is not pending for any of the provider tags" do + let(:pending_provider_version_tags) { [] } + + its(:pending) { is_expected.to be false } + its(:pending_provider_tags) { is_expected.to eq [] } + its(:non_pending_provider_tags) { is_expected.to eq %w[dev feat-x] } + end + end + end + end + end +end diff --git a/spec/lib/pact_broker/pacts/verifiable_pact_messages_spec.rb b/spec/lib/pact_broker/pacts/verifiable_pact_messages_spec.rb new file mode 100644 index 000000000..d5382cb13 --- /dev/null +++ b/spec/lib/pact_broker/pacts/verifiable_pact_messages_spec.rb @@ -0,0 +1,93 @@ +require 'pact_broker/pacts/verifiable_pact_messages' +require 'pact_broker/pacts/verifiable_pact' + +module PactBroker + module Pacts + describe VerifiablePactMessages do + let(:head_consumer_tags) { [] } + let(:pending_provider_tags) { [] } + let(:non_pending_provider_tags) { [] } + let(:pending) { false } + let(:verifiable_pact) do + double(VerifiablePact, + head_consumer_tags: head_consumer_tags, + consumer_name: "Foo", + provider_name: "Bar", + pending_provider_tags: pending_provider_tags, + non_pending_provider_tags: non_pending_provider_tags, + pending?: pending + ) + end + + subject { VerifiablePactMessages.new(verifiable_pact) } + + describe "#inclusion_reason" do + context "when there are no head consumer tags" do + its(:inclusion_reason) { is_expected.to include "This pact is being verified because it is the latest pact between Foo and Bar." } + end + + context "when there is 1 head consumer tags" do + let(:head_consumer_tags) { %w[dev] } + its(:inclusion_reason) { is_expected.to include "This pact is being verified because it is the pact for the latest version of Foo tagged with 'dev'" } + end + + context "when there are 2 head consumer tags" do + let(:head_consumer_tags) { %w[dev prod] } + its(:inclusion_reason) { is_expected.to include "This pact is being verified because it is the pact for the latest versions of Foo tagged with 'dev' and 'prod' (both have the same content)" } + end + + context "when there are 3 head consumer tags" do + let(:head_consumer_tags) { %w[dev prod feat-x] } + its(:inclusion_reason) { is_expected.to include "This pact is being verified because it is the pact for the latest versions of Foo tagged with 'dev', 'prod' and 'feat-x' (all have the same content)" } + end + + context "when there are 4 head consumer tags" do + let(:head_consumer_tags) { %w[dev prod feat-x feat-y] } + its(:inclusion_reason) { is_expected.to include "'dev', 'prod', 'feat-x' and 'feat-y'" } + end + end + + describe "#pending_reason" do + context "when the pact is not pending" do + context "when there are no non_pending_provider_tags" do + its(:pending_reason) { is_expected.to include "This pact has previously been successfully verified by Bar. If this verification fails, it will fail the build." } + end + + context "when there is 1 non_pending_provider_tag" do + let(:non_pending_provider_tags) { %w[dev] } + + its(:pending_reason) { is_expected.to include "This pact has previously been successfully verified by a version of Bar with tag 'dev'. If this verification fails, it will fail the build."} + end + end + + context "when the pact is pending" do + let(:pending) { true } + + context "when there are no pending_provider_tags" do + context "when there are no non_pending_provider_tags" do + its(:pending_reason) { is_expected.to include "This pact is in pending state because it has not yet been successfully verified by Bar. If this verification fails, it will not cause the overall build to fail." } + end + end + + context "when there is 1 pending_provider_tag" do + let(:pending_provider_tags) { %w[dev] } + + its(:pending_reason) { is_expected.to include "This pact is in pending state because it has not yet been successfully verified by any version of Bar with tag 'dev'. If this verification fails, it will not cause the overall build to fail." } + end + + context "when there are 2 pending_provider_tags" do + let(:pending_provider_tags) { %w[dev feat-x] } + + its(:pending_reason) { is_expected.to include "This pact is in pending state because it has not yet been successfully verified by any versions of Bar with tag 'dev' and 'feat-x'." } + end + + context "when there are 3 pending_provider_tags" do + let(:pending_provider_tags) { %w[dev feat-x feat-y] } + + its(:pending_reason) { is_expected.to include "'dev', 'feat-x' and 'feat-y'" } + end + end + end + end + end +end diff --git a/spec/lib/pact_broker/pacts/verifiable_pact_spec.rb b/spec/lib/pact_broker/pacts/verifiable_pact_spec.rb new file mode 100644 index 000000000..e69de29bb