From e16feef6db3965d0ba1f40bda75e7a50ec114da8 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Thu, 30 Jan 2020 13:30:55 +1100 Subject: [PATCH] feat(pacts for verification): allow all versions for a particular tag to be verified (eg. all prod versions of a mobile consumer) --- .../verifiable_pacts_json_query_schema.rb | 2 +- .../verifiable_pacts_query_schema.rb | 2 +- lib/pact_broker/pacts/pact_publication.rb | 53 +++++++++ lib/pact_broker/pacts/repository.rb | 108 +++++++++++++----- lib/pact_broker/repositories/helpers.rb | 4 + ...verifiable_pacts_json_query_schema_spec.rb | 2 +- .../verifiable_pacts_query_schema_spec.rb | 2 +- .../repository_find_for_verification_spec.rb | 67 +++++++++-- 8 files changed, 197 insertions(+), 43 deletions(-) diff --git a/lib/pact_broker/api/contracts/verifiable_pacts_json_query_schema.rb b/lib/pact_broker/api/contracts/verifiable_pacts_json_query_schema.rb index b8d450472..0f19cfddd 100644 --- a/lib/pact_broker/api/contracts/verifiable_pacts_json_query_schema.rb +++ b/lib/pact_broker/api/contracts/verifiable_pacts_json_query_schema.rb @@ -18,7 +18,7 @@ class VerifiablePactsJSONQuerySchema optional(:consumerVersionSelectors).each do schema do required(:tag).filled(:str?) - required(:latest).filled(included_in?: [true]) + optional(:latest).filled(included_in?: [true, false]) end end optional(:includePendingStatus).filled(included_in?: [true, false]) diff --git a/lib/pact_broker/api/contracts/verifiable_pacts_query_schema.rb b/lib/pact_broker/api/contracts/verifiable_pacts_query_schema.rb index 3e5b77110..55e48823f 100644 --- a/lib/pact_broker/api/contracts/verifiable_pacts_query_schema.rb +++ b/lib/pact_broker/api/contracts/verifiable_pacts_query_schema.rb @@ -17,7 +17,7 @@ class VerifiablePactsQuerySchema optional(:consumer_version_selectors).each do schema do required(:tag).filled(:str?) - required(:latest).filled(included_in?: ["true"]) + optional(:latest).filled(included_in?: ["true", "false"]) end end optional(:include_pending_status).filled(included_in?: ["true", "false"]) diff --git a/lib/pact_broker/pacts/pact_publication.rb b/lib/pact_broker/pacts/pact_publication.rb index 1e4a0cb41..3ca1aeba6 100644 --- a/lib/pact_broker/pacts/pact_publication.rb +++ b/lib/pact_broker/pacts/pact_publication.rb @@ -25,10 +25,63 @@ class PactPublication < Sequel::Model(:pact_publications) dataset_module do include PactBroker::Repositories::Helpers + def remove_overridden_revisions + join(:latest_pact_publication_ids_for_consumer_versions, { Sequel[:lp][:pact_publication_id] => Sequel[:pact_publications][:id] }, { table_alias: :lp}) + end + + def join_consumer_versions(table_alias = :cv) + join(:versions, { Sequel[:pact_publications][:consumer_version_id] => Sequel[table_alias][:id] }, { table_alias: table_alias }) + end + + def join_consumer_version_tags(table_alias = :ct) + join(:tags, { Sequel[table_alias][:version_id] => Sequel[:pact_publications][:consumer_version_id]}, { table_alias: table_alias }) + end + + def join_consumer_version_tags_with_names(consumer_version_tag_names) + join(:tags, { + Sequel[:ct][:version_id] => Sequel[:pact_publications][:consumer_version_id], + Sequel[:ct][:name] => consumer_version_tag_names + }, { + table_alias: :ct + }) + end + + def join_providers(table_alias = :providers) + join(:pacticipants, { Sequel[:pact_publications][:provider_id] => Sequel[table_alias][:id] }, { table_alias: table_alias }) + end + + def join_consumers(table_alias = :consumers) + join(:pacticipants, { Sequel[:pact_publications][:consumer_id] => Sequel[table_alias][:id] }, { table_alias: table_alias }) + end + + def join_pact_versions + join(:pact_versions, { Sequel[:pact_publications][:pact_version_id] => Sequel[:pact_versions][:id] }) + end + + def eager_load_pact_versions + eager(:pact_versions) + end + def tag tag_name filter = name_like(Sequel.qualify(:tags, :name), tag_name) join(:tags, {version_id: :consumer_version_id}).where(filter) end + + def provider_name_like(name) + where(name_like(Sequel[:providers][:name], name)) + end + + def consumer_version_tag(tag) + where(Sequel[:ct][:name] => tag) + end + + def order_by_consumer_name + order_append_ignore_case(Sequel[:consumers][:name]) + end + + def order_by_consumer_version_order + order_append(Sequel[:cv][:order]) + end end def before_create diff --git a/lib/pact_broker/pacts/repository.rb b/lib/pact_broker/pacts/repository.rb index 29ac64c94..f2e35a278 100644 --- a/lib/pact_broker/pacts/repository.rb +++ b/lib/pact_broker/pacts/repository.rb @@ -13,6 +13,7 @@ require 'pact_broker/matrix/head_row' require 'pact_broker/pacts/latest_pact_publication_id_for_consumer_version' require 'pact_broker/pacts/verifiable_pact' +require 'pact_broker/repositories/helpers' module PactBroker module Pacts @@ -20,6 +21,7 @@ class Repository include PactBroker::Logging include PactBroker::Repositories + include PactBroker::Repositories::Helpers def create params pact_version = find_or_create_pact_version( @@ -125,36 +127,60 @@ def find_latest_pact_versions_for_provider provider_name, tag = nil end end + def find_all_pact_versions_for_provider_with_tags provider_name, consumer_version_tag_names + provider = pacticipant_repository.find_by_name(provider_name) + + PactPublication + .select_all_qualified + .select_append(Sequel[:cv][:order].as(:consumer_version_order)) + .remove_overridden_revisions + .join_consumer_versions(:cv) + .join_consumer_version_tags_with_names(consumer_version_tag_names) + .where(provider: provider) + .eager(:consumer) + .eager(:consumer_version) + .eager(:provider) + .eager(:pact_version) + .all + .group_by(&:pact_version_id) + .values + .collect{ | pacts| pacts.sort_by{|pact| pact.values.fetch(:consumer_version_order) }.last } + .collect(&:to_domain) + end + # To find the work in progress pacts for this verification execution: # For each provider tag that will be applied to this verification result (usually there will just be one, but # we have to allow for multiple tags), # find the head pacts (the pacts that are the latest for their tag) that have been successfully # verified against the provider tag. # Then, find all the head pacts, and remove the ones that have been successfully verified by ALL - # of the provider tags supplied. + # of the provider tags supplied, and the ones that were published before the include_wip_pacts_since date. # Then, for all of the head pacts that are remaining (these are the WIP ones) work out which # provider tags they are pending for. - # Don't include pact publications that were created + # Don't include pact publications that were created before the provider tag was first used + # (that is, before the provider's git branch was created). def find_wip_pact_versions_for_provider provider_name, provider_tags_names = [], options = {} + # TODO not sure about this return [] if provider_tags_names.empty? provider = pacticipant_repository.find_by_name(provider_name) - # Hash of provider tag names => list of head pacts - successfully_verified_head_pacts_for_provider_tags = find_successfully_verified_head_pacts_by_provider_tag(provider_name, provider_tags_names, options) + # Hash of provider tag name => list of head pacts that have been successfully verified by that tag + successfully_verified_head_pacts_for_provider_tags = find_successfully_verified_head_pacts_by_provider_tag(provider.id, provider_tags_names, options) + # Create hash of provider tag name => list of pact publication ids successfully_verified_head_pact_publication_ids_for_each_provider_tag = successfully_verified_head_pacts_for_provider_tags.each_with_object({}) do | (provider_tag_name, head_pacts), hash | - hash[provider_tag_name] = head_pacts.collect(&:id) + hash[provider_tag_name] = head_pacts.collect(&:id).uniq end - # list of pact_publication_ids that are NOT work in progress - head_pact_publication_ids_successully_verified_by_all_provider_tags = successfully_verified_head_pacts_for_provider_tags.values.collect{ |head_pacts| head_pacts.collect(&:id) }.reduce(:&) + # list of head pact_publication_ids that are NOT work in progress because they've been verified by all of the provider version tags supplied + non_wip_pact_publication_ids = successfully_verified_head_pacts_for_provider_tags.values.collect{ |head_pacts| head_pacts.collect(&:id) }.reduce(:&) - pact_publication_ids = find_head_pacts_that_have_not_been_successfully_verified_by_all_provider_tags( - provider_name, - head_pact_publication_ids_successully_verified_by_all_provider_tags, + wip_pact_publication_ids = find_head_pacts_that_have_not_been_successfully_verified_by_all_provider_tags( + provider.id, + non_wip_pact_publication_ids, options) - pacts = AllPactPublications.where(id: pact_publication_ids).order_ignore_case(:consumer_name).order_append(:consumer_version_order) + wip_pacts = AllPactPublications.where(id: wip_pact_publication_ids).order_ignore_case(:consumer_name).order_append(:consumer_version_order) # The first instance (by date) of each provider tag with that name provider_tag_collection = PactBroker::Domain::Tag @@ -166,7 +192,7 @@ def find_wip_pact_versions_for_provider provider_name, provider_tags_names = [], .where(name: provider_tags_names) .all - pacts.collect do | pact| + wip_pacts.collect do | pact| pending_tag_names = find_provider_tags_for_which_pact_publication_id_is_pending(pact, successfully_verified_head_pact_publication_ids_for_each_provider_tag) pre_existing_tag_names = find_provider_tag_names_that_were_first_used_before_pact_published(pact, provider_tag_collection) @@ -313,13 +339,33 @@ def find_previous_pacts pact # Returns a list of Domain::Pact objects the represent pact publications def find_for_verification(provider_name, consumer_version_selectors) + find_pacts_for_which_the_latest_version_or_latest_version_for_the_tag_is_required(provider_name, consumer_version_selectors) + + find_pacts_for_which_all_versions_for_the_tag_are_required(provider_name, consumer_version_selectors) + end + + private + + def find_pacts_for_which_the_latest_version_or_latest_version_for_the_tag_is_required(provider_name, consumer_version_selectors) + # The tags for which only the latest version is specified 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_pacts_for_which_all_versions_for_the_tag_are_required(provider_name, consumer_version_selectors) + # The tags for which all versions are specified + all_tags = consumer_version_selectors.any? ? + consumer_version_selectors.reject(&:latest).collect(&:tag) : + nil + + if all_tags + find_all_pact_versions_for_provider_with_tags(provider_name, all_tags) + else + [] + end + end def find_previous_distinct_pact_by_sha pact current_pact_content_sha = @@ -389,27 +435,35 @@ def to_datetime string_or_datetime end 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) + def find_head_pacts_that_have_not_been_successfully_verified_by_all_provider_tags(provider_id, 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) + LatestTaggedPactPublications + .where(provider_id: provider_id) .where(Sequel.lit('latest_tagged_pact_publications.created_at > ?', options.fetch(:include_wip_pacts_since))) + .exclude(id: pact_publication_ids_successfully_verified_by_all_provider_tags) .select_for_subquery(:id) end - # Find the head pacts that have been successfully verified by a provider version with the specified tags - # Returns a Hash of provider_tag => LatestTaggedPactPublications with only id and tag_name populated - def find_successfully_verified_head_pacts_by_provider_tag(provider_name, provider_tags, options) + # Find the head pacts that have been successfully verified by a provider version with the specified + # provider version tags. + # Returns a Hash of provider_tag => LatestTaggedPactPublications with only pact publication id and tag_name populated + # This is the list of pacts we are EXCLUDING from the WIP list because they have already been verified successfully + def find_successfully_verified_head_pacts_by_provider_tag(provider_id, provider_tags, options) provider_tags.compact.each_with_object({}) do | provider_tag, hash | + verifications_join = { + pact_version_id: :pact_version_id, + Sequel[:verifications][:success] => true, + Sequel[:verifications][:provider_id] => provider_id + } + tags_join = { + Sequel[:verifications][:provider_version_id] => Sequel[:provider_tags][:version_id], + Sequel[:provider_tags][:name] => provider_tag + } head_pacts = 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) - .or(Sequel.lit('latest_tagged_pact_publications.created_at < ?', options.fetch(:include_wip_pacts_since))) - .select(Sequel[:latest_tagged_pact_publications][:id].as(:id), :tag_name) + .select(Sequel[:latest_tagged_pact_publications][:id].as(:id)) + .join(:verifications, verifications_join) + .join(:tags, tags_join, { table_alias: :provider_tags } ) + .where(Sequel[:latest_tagged_pact_publications][:provider_id] => provider_id) .all hash[provider_tag] = head_pacts end diff --git a/lib/pact_broker/repositories/helpers.rb b/lib/pact_broker/repositories/helpers.rb index 8fc873d6b..710cca713 100644 --- a/lib/pact_broker/repositories/helpers.rb +++ b/lib/pact_broker/repositories/helpers.rb @@ -23,6 +23,10 @@ def order_ignore_case column_name = :name order(Sequel.function(:lower, column_name)) end + def order_append_ignore_case column_name = :name + order_append(Sequel.function(:lower, column_name)) + end + def mysql? Sequel::Model.db.adapter_scheme.to_s =~ /mysql/ end diff --git a/spec/lib/pact_broker/api/contracts/verifiable_pacts_json_query_schema_spec.rb b/spec/lib/pact_broker/api/contracts/verifiable_pacts_json_query_schema_spec.rb index 86c6fb4ef..5930d1d11 100644 --- a/spec/lib/pact_broker/api/contracts/verifiable_pacts_json_query_schema_spec.rb +++ b/spec/lib/pact_broker/api/contracts/verifiable_pacts_json_query_schema_spec.rb @@ -57,7 +57,7 @@ module Contracts }] end - it { is_expected.to have_key(:consumerVersionSelectors) } + it { is_expected.to be_empty } end context "when includeWipPactsSince key exists" do 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 index 163bb5d72..4847d5fbb 100644 --- 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 @@ -51,7 +51,7 @@ module Contracts }] end - it { is_expected.to have_key(:consumer_version_selectors) } + it { is_expected.to be_empty } end context "when include_wip_pacts_since key exists" do 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 index 4417b15c1..c802009c0 100644 --- a/spec/lib/pact_broker/pacts/repository_find_for_verification_spec.rb +++ b/spec/lib/pact_broker/pacts/repository_find_for_verification_spec.rb @@ -11,21 +11,35 @@ def find_by_consumer_version_number(consumer_version_number) subject.find{ |pact| pact.consumer_version_number == consumer_version_number } end + def find_by_consumer_name_and_consumer_version_number(consumer_name, consumer_version_number) + subject.find{ |pact| pact.consumer_name == consumer_name && pact.consumer_version_number == consumer_version_number } + end + before do - td.create_pact_with_hierarchy("Foo", "bar-latest-prod", "Bar") + td.create_pact_with_hierarchy("Foo", "foo-latest-prod-version", "Bar") .create_consumer_version_tag("prod") - .create_consumer_version("not-latest-dev", tag_names: ["dev"]) + .create_consumer_version("not-latest-dev-version", tag_names: ["dev"]) .comment("next pact not selected") .create_pact - .create_consumer_version("bar-latest-dev", tag_names: ["dev"]) + .create_consumer_version("foo-latest-dev-version", tag_names: ["dev"]) .create_pact .create_consumer("Baz") - .create_consumer_version("baz-latest-dev", tag_names: ["dev"]) + .create_consumer_version("baz-latest-dev-version", tag_names: ["dev"]) .create_pact end subject { Repository.new.find_for_verification("Bar", consumer_version_selectors) } + context "when there are no selectors" do + let(:consumer_version_selectors) { [] } + + it "returns the latest pact for each consumer" do + expect(subject.size).to eq 2 + expect(find_by_consumer_name_and_consumer_version_number("Foo", "foo-latest-dev-version")).to_not be nil + expect(find_by_consumer_name_and_consumer_version_number("Baz", "baz-latest-dev-version")).to_not be nil + end + end + 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) } @@ -34,14 +48,43 @@ def find_by_consumer_version_number(consumer_version_number) 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(find_by_consumer_version_number("foo-latest-prod-version")).to_not be nil + expect(find_by_consumer_version_number("foo-latest-dev-version")).to_not be nil + expect(find_by_consumer_version_number("baz-latest-dev-version")).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' + expect(find_by_consumer_version_number("foo-latest-prod-version").tag).to eq 'prod' + end + + context "when all versions with a given tag are requested" do + before do + td.create_pact_with_hierarchy("Foo2", "prod-version-1", "Bar2") + .create_consumer_version_tag("prod") + .create_consumer_version("not-prod-version", tag_names: %w[master]) + .create_pact + .create_consumer_version("prod-version-2", tag_names: %w[prod]) + .create_pact + end + + let(:consumer_version_selectors) { [pact_selector_1] } + let(:pact_selector_1) { double('selector', tag: 'prod', latest: nil) } + + subject { Repository.new.find_for_verification("Bar2", consumer_version_selectors) } + + it "returns all the versions with the specified tag" do + expect(subject.size).to be 2 + expect(find_by_consumer_version_number("prod-version-1")).to_not be nil + expect(find_by_consumer_version_number("prod-version-2")).to_not be nil + end + + it "dedupes them to ensure that each pact version is only verified once" do + td.create_consumer_version("prod-version-3", tag_names: %w[prod]) + .republish_same_pact + expect(subject.size).to be 2 + expect(subject.collect(&:consumer_version_number)).to eq %w[prod-version-1 prod-version-3] + end end end @@ -49,14 +92,14 @@ def find_by_consumer_version_number(consumer_version_number) 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(find_by_consumer_version_number("foo-latest-dev-version")).to_not be nil + expect(find_by_consumer_version_number("baz-latest-dev-version")).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 + expect(find_by_consumer_version_number("foo-latest-dev-version").tag).to be nil + expect(find_by_consumer_version_number("foo-latest-dev-version").overall_latest?).to be true end end end