diff --git a/lib/pact_broker/api/contracts/consumer_version_selector_contract.rb b/lib/pact_broker/api/contracts/consumer_version_selector_contract.rb index dda334b59..6ce831fc4 100644 --- a/lib/pact_broker/api/contracts/consumer_version_selector_contract.rb +++ b/lib/pact_broker/api/contracts/consumer_version_selector_contract.rb @@ -13,7 +13,7 @@ class ConsumerVersionSelectorContract < BaseContract json do optional(:mainBranch).filled(included_in?: [true]) optional(:tag).filled(:str?) - optional(:branch).filled(:str?) + optional(:branch).filled { str? | eql?(true) } optional(:matchingBranch).filled(included_in?: [true]) optional(:latest).filled(included_in?: [true, false]) optional(:fallbackTag).filled(:str?) diff --git a/lib/pact_broker/api/resources/provider_pacts_for_verification.rb b/lib/pact_broker/api/resources/provider_pacts_for_verification.rb index 852e143d4..822bd4310 100644 --- a/lib/pact_broker/api/resources/provider_pacts_for_verification.rb +++ b/lib/pact_broker/api/resources/provider_pacts_for_verification.rb @@ -15,6 +15,10 @@ def content_types_provided [["application/hal+json", :to_json]] end + # TODO drop support for GET in next major version. + # GET was only used by the very first Ruby Pact clients that supported the 'pacts for verification' + # feature, until it became clear that the parameters for the request were going to get nested and complex, + # at which point the POST was added. def allowed_methods ["GET", "POST", "OPTIONS"] end @@ -32,6 +36,7 @@ def process_post end end + # For this endoint, the POST is a "read" action (used for Pactflow) def read_methods super + %w{POST} end diff --git a/lib/pact_broker/pacts/pact_publication_dataset_module.rb b/lib/pact_broker/pacts/pact_publication_dataset_module.rb index bdfb5c431..851a8637b 100644 --- a/lib/pact_broker/pacts/pact_publication_dataset_module.rb +++ b/lib/pact_broker/pacts/pact_publication_dataset_module.rb @@ -57,7 +57,13 @@ def for_consumer_name_and_maybe_version_number(consumer_name, consumer_version_n end end - # TODO use the branch heads here + # Returns the latest pact for each branch, returning a pact for every branch, even if + # the most recent version of that branch does not have a pact. + # This is different from for_all_branch_heads, which will find the branch head versions, + # and return the pacts associated with those versions. + # This method should not be used for 'pacts for verification', because it will return + # a pact for branches where that integration should no longer exist. + # @return [Dataset] def latest_by_consumer_branch branch_versions_join = { Sequel[:pact_publications][:consumer_version_id] => Sequel[:branch_versions][:version_id] @@ -112,10 +118,12 @@ def overall_latest_for_consumer_id_and_provider_id(consumer_id, provider_id) .limit(1) end - # Return the pacts (if they exist) for the branch heads. + # Return the pacts (if they exist) for the branch heads of the given branch names # This uses the new logic of finding the branch head and returning any associated pacts, # rather than the old logic of returning the pact for the latest version # on the branch that had a pact. + # @param [String] branch_name + # @return [Sequel::Dataset] def for_branch_heads(branch_name) branch_head_join = { Sequel[:pact_publications][:consumer_version_id] => Sequel[:branch_heads][:version_id], @@ -133,6 +141,23 @@ def for_branch_heads(branch_name) .remove_overridden_revisions_from_complete_query end + # Return the pacts (if they exist) for all the branch heads. + # @return [Sequel::Dataset] + def latest_for_all_consumer_branches + branch_head_join = { + Sequel[:pact_publications][:consumer_version_id] => Sequel[:branch_heads][:version_id], + } + + base_query = self + if no_columns_selected? + base_query = base_query.select_all_qualified.select_append(Sequel[:branch_heads][:branch_name].as(:branch_name)) + end + + base_query + .join(:branch_heads, branch_head_join) + .remove_overridden_revisions_from_complete_query + end + # The pact that belongs to the branch head. # May return nil if the branch head does not have a pact published for it. def latest_for_consumer_branch(branch_name) diff --git a/lib/pact_broker/pacts/pact_publication_selector_dataset_module.rb b/lib/pact_broker/pacts/pact_publication_selector_dataset_module.rb index 8822a6094..1e624215e 100644 --- a/lib/pact_broker/pacts/pact_publication_selector_dataset_module.rb +++ b/lib/pact_broker/pacts/pact_publication_selector_dataset_module.rb @@ -15,6 +15,7 @@ def for_provider_and_consumer_version_selector provider, selector # Do the "latest" logic last so that the provider/consumer criteria get included in the "latest" query before the join, rather than after query = query.latest_for_main_branches if selector.latest_for_main_branch? + query = query.latest_for_all_consumer_branches if selector.latest_for_each_branch? query = query.latest_for_consumer_branch(selector.branch) if selector.latest_for_branch? query = query.for_latest_consumer_versions_with_tag(selector.tag) if selector.latest_for_tag? query = query.overall_latest if selector.overall_latest? diff --git a/lib/pact_broker/pacts/selector.rb b/lib/pact_broker/pacts/selector.rb index eb886b39e..108751dd1 100644 --- a/lib/pact_broker/pacts/selector.rb +++ b/lib/pact_broker/pacts/selector.rb @@ -31,6 +31,8 @@ def resolve_for_environment(consumer_version, environment, target = nil) def type if latest_for_main_branch? :latest_for_main_branch + elsif latest_for_each_branch? + :latest_for_each_branch elsif latest_for_branch? :latest_for_branch elsif matching_branch? @@ -265,12 +267,16 @@ def latest_for_tag? potential_tag = nil # Not sure if the fallback_tag logic is needed def latest_for_branch? potential_branch = nil if potential_branch - !!(latest && branch == potential_branch) + latest == true && branch == potential_branch else - !!(latest && !!branch) + latest == true && branch.is_a?(String) end end + def latest_for_each_branch? + latest == true && branch == true + end + def all_for_tag_and_consumer? !!(tag && !latest? && consumer) end diff --git a/spec/lib/pact_broker/api/contracts/pacts_for_verification_json_query_schema_spec.rb b/spec/lib/pact_broker/api/contracts/pacts_for_verification_json_query_schema_spec.rb index da4205546..da0fca3fa 100644 --- a/spec/lib/pact_broker/api/contracts/pacts_for_verification_json_query_schema_spec.rb +++ b/spec/lib/pact_broker/api/contracts/pacts_for_verification_json_query_schema_spec.rb @@ -523,6 +523,36 @@ module Contracts it { is_expected.to_not have_key(:consumerVersionSelectors) } end + + context "when branch is true, and latest is true" do + let(:params) do + { + consumerVersionSelectors: [ { branch: true, latest: true }] + } + end + + it { is_expected.to_not have_key(:consumerVersionSelectors) } + end + + context "when branch is true, and latest is false" do + let(:params) do + { + consumerVersionSelectors: [ { branch: true, latest: false }] + } + end + + its([:consumerVersionSelectors, 0]) { is_expected.to match("cannot specify") } + end + + context "when branch is false" do + let(:params) do + { + consumerVersionSelectors: [ { branch: false }] + } + end + + its([:consumerVersionSelectors, 0]) { is_expected.to match("branch must be a string or branch must be equal to true") } + end end end end diff --git a/spec/lib/pact_broker/pacts/pact_publication_dataset_module_spec.rb b/spec/lib/pact_broker/pacts/pact_publication_dataset_module_spec.rb index 476e4b4de..ac8b50883 100644 --- a/spec/lib/pact_broker/pacts/pact_publication_dataset_module_spec.rb +++ b/spec/lib/pact_broker/pacts/pact_publication_dataset_module_spec.rb @@ -131,6 +131,71 @@ module Pacts end end + describe "latest_for_all_consumer_branches" do + before do + td.create_consumer("Foo") + .create_provider("Bar") + .create_consumer_version("1", branch: "main") + .create_pact + .create_consumer_version("2", branch: "main") + .create_pact + .revise_pact + .create_consumer_version("3", branch: "feat-x") + .create_pact + .create_consumer("Foo2") + .create_provider("Bar2") + .create_consumer_version("10", branch: "main") + .create_pact + .create_consumer_version("11", branch: "main") + .create_pact + end + + subject { PactPublication.latest_for_all_consumer_branches } + + it "returns the pacts for all the branch heads" do + all = subject.all_allowing_lazy_load.sort_by{ |pact_publication| pact_publication.consumer_version.order } + expect(all.size).to eq 3 + expect(all.first.consumer.name).to eq "Foo" + expect(all.first.provider.name).to eq "Bar" + expect(all.first.consumer_version.number).to eq "2" + expect(all.first.revision_number).to eq 2 + + expect(all.last.consumer.name).to eq "Foo2" + expect(all.last.provider.name).to eq "Bar2" + expect(all.last.consumer_version.number).to eq "11" + end + + it "does not return extra columns" do + expect(subject.first.values.keys.sort).to eq (PactPublication.columns + [:branch_name]).sort + end + + context "when there is no pact for the branch head" do + before do + td.create_consumer_version("12", branch: "main") + end + + it "does not return a pact" do + all = subject.all_allowing_lazy_load + expect(all.size).to eq 2 + end + end + + context "when columns are already selected" do + subject { PactPublication.select(Sequel[:pact_publications][:id]).latest_for_consumer_branch("main") } + + it "does not override them" do + expect(subject.all.first.values.keys).to eq [:id] + end + end + + context "when chained" do + it "works" do + all = PactPublication.for_provider(td.find_pacticipant("Bar")).latest_for_consumer_branch("main").all_allowing_lazy_load + expect(all.collect(&:provider_name).uniq).to eq ["Bar"] + end + end + end + describe "latest_by_consumer_tag" do before do td.create_consumer("Foo")