From c9248e7ba8be2ea2bce29c54d178e4091794a19a Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Mon, 30 Oct 2023 14:03:28 +1100 Subject: [PATCH 01/69] test: fix test file name --- ... => get_released_versions_for_version_and_environment_spec.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/features/{get_released_versions_for_version_and_environment.rb => get_released_versions_for_version_and_environment_spec.rb} (100%) diff --git a/spec/features/get_released_versions_for_version_and_environment.rb b/spec/features/get_released_versions_for_version_and_environment_spec.rb similarity index 100% rename from spec/features/get_released_versions_for_version_and_environment.rb rename to spec/features/get_released_versions_for_version_and_environment_spec.rb From 47f76f12908d2351d714fddd29a8d6392f8cade4 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Tue, 31 Oct 2023 12:55:55 +1100 Subject: [PATCH 02/69] docs: add yardocs for test data builder --- lib/pact_broker/test/test_data_builder.rb | 31 +++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/lib/pact_broker/test/test_data_builder.rb b/lib/pact_broker/test/test_data_builder.rb index 289fca2a2..2cdd4d2b2 100644 --- a/lib/pact_broker/test/test_data_builder.rb +++ b/lib/pact_broker/test/test_data_builder.rb @@ -88,6 +88,12 @@ def create_condor self end + # Creates a consumer, consumer version, provider and pact with the specified JSON content + # Does NOT rely on previous state. + # @param [String] consumer_name + # @param [String] consumer_version_number + # @param [Strig] provider_name + # @param [String] json_content def create_pact_with_hierarchy consumer_name = "Consumer", consumer_version_number = "1.2.3", provider_name = "Provider", json_content = nil use_consumer(consumer_name) create_consumer(consumer_name) if !consumer @@ -99,18 +105,39 @@ def create_pact_with_hierarchy consumer_name = "Consumer", consumer_version_numb self end + # Creates a consumer, consumer version with tag, provider and pact + # Does NOT rely on previous state. + # @param [String] consumer_name + # @param [String] consumer_version_number + # @param [String] consumer_version_tag_name + # @param [Strig] provider_name def create_pact_with_consumer_version_tag consumer_name, consumer_version_number, consumer_version_tag_name, provider_name create_pact_with_hierarchy(consumer_name, consumer_version_number, provider_name) create_consumer_version_tag(consumer_version_tag_name) self end + # Creates a consumer, consumer version, provider, pact and verification + # Does NOT rely on previous state. + # @param [String] consumer_name + # @param [String] consumer_version + # @param [String] provider_name + # @param [String] provider_version + # @param [Boolean] success default true def create_pact_with_verification consumer_name = "Consumer", consumer_version = "1.0.#{model_counter}", provider_name = "Provider", provider_version = "1.0.#{model_counter}", success = true create_pact_with_hierarchy(consumer_name, consumer_version, provider_name) create_verification(number: model_counter, provider_version: provider_version, success: success) self end + # Creates a consumer, consumer version, provider, pact and verification + # Does NOT rely on previous state. + # @param [String] consumer_name + # @param [String] consumer_version + # @param [Array] consumer_version_tags + # @param [String] provider_name + # @param [String] provider_version + # @param [Array] provider_version_tags def create_pact_with_verification_and_tags consumer_name = "Consumer", consumer_version = "1.0.#{model_counter}", consumer_version_tags = [], provider_name = "Provider", provider_version = "1.0.#{model_counter}", provider_version_tags = [] create_pact_with_hierarchy(consumer_name, consumer_version, provider_name) consumer_version_tags.each do | tag | @@ -120,6 +147,10 @@ def create_pact_with_verification_and_tags consumer_name = "Consumer", consumer_ self end + # Create a pacticipant and version + # Does NOT rely on previous state + # @param [String] pacticipant_name + # @param [String] pacticipant_version def create_version_with_hierarchy pacticipant_name, pacticipant_version pacticipant = pacticipant_service.create(:name => pacticipant_name) version = PactBroker::Domain::Version.create(:number => pacticipant_version, :pacticipant => pacticipant) From 2f43590c50c82a26f1d180ab733c9834745d0865 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Thu, 2 Nov 2023 09:49:55 +1100 Subject: [PATCH 03/69] feat: do not include pb:record-deployment or pb:record-release relations for versions embedded in resources --- .../api/decorators/version_decorator.rb | 5 +++-- .../api/decorators/version_decorator_spec.rb | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/lib/pact_broker/api/decorators/version_decorator.rb b/lib/pact_broker/api/decorators/version_decorator.rb index b033a606b..2c7585739 100644 --- a/lib/pact_broker/api/decorators/version_decorator.rb +++ b/lib/pact_broker/api/decorators/version_decorator.rb @@ -1,5 +1,6 @@ require_relative "base_decorator" require_relative "embedded_tag_decorator" +require_relative "embedded_branch_version_decorator" module PactBroker module Api @@ -58,7 +59,7 @@ class VersionDecorator < BaseDecorator end links :'pb:record-deployment' do | context | - context.fetch(:environments, []).collect do | environment | + context[:environments]&.collect do | environment | { title: "Record deployment to #{environment.display_name}", name: environment.name, @@ -68,7 +69,7 @@ class VersionDecorator < BaseDecorator end links :'pb:record-release' do | context | - context.fetch(:environments, []).collect do | environment | + context[:environments]&.collect do | environment | { title: "Record release to #{environment.display_name}", name: environment.name, diff --git a/spec/lib/pact_broker/api/decorators/version_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/version_decorator_spec.rb index 045a6ee01..22c00e4d3 100644 --- a/spec/lib/pact_broker/api/decorators/version_decorator_spec.rb +++ b/spec/lib/pact_broker/api/decorators/version_decorator_spec.rb @@ -23,6 +23,7 @@ module Decorators describe "to_json" do before do allow(decorator).to receive(:deployed_versions_for_version_and_environment_url).and_return("http://deployed-versions") + allow(decorator).to receive(:released_versions_for_version_and_environment_url).and_return("http://released-versions") end let(:version) do @@ -112,6 +113,25 @@ module Decorators href: "http://deployed-versions" ) end + + it "includes a list of environments that this version can be released to" do + expect(decorator).to receive(:released_versions_for_version_and_environment_url).with(version, environments.first, base_url) + expect(subject[:_links][:'pb:record-release']).to be_instance_of(Array) + expect(subject[:_links][:'pb:record-release'].first).to eq( + name: "test", + title: "Record release to Test", + href: "http://released-versions" + ) + end + + context "when the environments option is not present" do + let(:options) { { user_options: { base_url: base_url } } } + + it "does not include the pb:record-deployment or pb:record-release" do + expect(subject[:_links]).to_not have_key(:'pb:record-deployment') + expect(subject[:_links]).to_not have_key(:'pb:record-release') + end + end end end end From d6af0600c0b78bcc21973974e2dcd4080a97c8c2 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Fri, 10 Nov 2023 09:02:59 +1100 Subject: [PATCH 04/69] test: add provider states for deleting branches --- lib/pact_broker/api/resources/index.rb | 6 ++++++ spec/service_consumers/hal_relation_proxy_app.rb | 4 +++- .../provider_states_for_pact_broker_client.rb | 12 ++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/pact_broker/api/resources/index.rb b/lib/pact_broker/api/resources/index.rb index 0a2653f8e..83877f91e 100644 --- a/lib/pact_broker/api/resources/index.rb +++ b/lib/pact_broker/api/resources/index.rb @@ -116,6 +116,12 @@ def links title: "Get, create or delete a tag for a pacticipant version", templated: true }, + "pb:pacticipant-branch" => + { + href: base_url + "/pacticipants/{pacticipant}/branches/{branch}", + title: "Get or delete a pacticipant branch", + templated: true + }, "pb:pacticipant-branch-version" => { href: base_url + "/pacticipants/{pacticipant}/branches/{branch}/versions/{version}", diff --git a/spec/service_consumers/hal_relation_proxy_app.rb b/spec/service_consumers/hal_relation_proxy_app.rb index 57d2123b8..bca43e3d2 100644 --- a/spec/service_consumers/hal_relation_proxy_app.rb +++ b/spec/service_consumers/hal_relation_proxy_app.rb @@ -27,7 +27,9 @@ class HalRelationProxyApp "/PLACEHOLDER-ENVIRONMENT-CURRENTLY-DEPLOYED-16926ef3-590f-4e3f-838e-719717aa88c9" => "/environments/16926ef3-590f-4e3f-838e-719717aa88c9/deployed-versions/currently-deployed", "/HAL-REL-PLACEHOLDER-PB-ENVIRONMENT-16926ef3-590f-4e3f-838e-719717aa88c9" => - "/environments/16926ef3-590f-4e3f-838e-719717aa88c9" + "/environments/16926ef3-590f-4e3f-838e-719717aa88c9", + "/HAL-REL-PLACEHOLDER-PB-PACTICIPANT-BRANCH-Foo-main" => + "/pacticipants/Foo/branches/main" } RESPONSE_BODY_REPLACEMENTS = { diff --git a/spec/service_consumers/provider_states_for_pact_broker_client.rb b/spec/service_consumers/provider_states_for_pact_broker_client.rb index a6e1ce372..1c64cd917 100644 --- a/spec/service_consumers/provider_states_for_pact_broker_client.rb +++ b/spec/service_consumers/provider_states_for_pact_broker_client.rb @@ -338,4 +338,16 @@ .create_deployed_version_for_consumer_version(uuid: "ff3adecf-cfc5-4653-a4e3-f1861092f8e0") end end + + provider_state "the pb:pacticipant-branch relation exists in the index resource" do + no_op + end + + provider_state "a branch named main exists for pacticipant Foo" do + set_up do + TestDataBuilder.new + .create_consumer("Foo") + .create_consumer_version("1", branch: "main") + end + end end From 5a9e1e7f186e92073fde1d4e48673cb83e64e4c7 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Fri, 10 Nov 2023 09:19:04 +1100 Subject: [PATCH 05/69] docs: add HAL docs for pacticipant-branch --- .../views/index/pacticipant-branch.markdown | 25 +++++++++++++++++++ spec/lib/pact_broker/doc/coverage_spec.rb | 2 ++ 2 files changed, 27 insertions(+) create mode 100644 lib/pact_broker/doc/views/index/pacticipant-branch.markdown diff --git a/lib/pact_broker/doc/views/index/pacticipant-branch.markdown b/lib/pact_broker/doc/views/index/pacticipant-branch.markdown new file mode 100644 index 000000000..481d29518 --- /dev/null +++ b/lib/pact_broker/doc/views/index/pacticipant-branch.markdown @@ -0,0 +1,25 @@ +# Pacticipant branch + +Allowed methods: `GET`, `DELETE` + +Path: `/pacticipants/{pacticipant}/branches/{branch}` + +Get or delete a pacticipant branch. + +## Create + +Branches cannot be created via the resource URL. They are created automatically when publishing contracts. + +## Get + +### Example + + curl http://broker/pacticipants/Bar/branches/main -H "Accept: application/hal+json" + +## Delete + +Deletes a pacticipant branch. Does NOT delete the associated pacticipant versions. + +Send a `DELETE` request to the branch resource. + + curl -XDELETE http://broker/pacticipants/Bar/branches/main diff --git a/spec/lib/pact_broker/doc/coverage_spec.rb b/spec/lib/pact_broker/doc/coverage_spec.rb index 95c6704f1..19e6ecc34 100644 --- a/spec/lib/pact_broker/doc/coverage_spec.rb +++ b/spec/lib/pact_broker/doc/coverage_spec.rb @@ -3,6 +3,8 @@ RSpec.describe "the HAL docs for the index" do let(:app) do + require "pact_broker/api" + Rack::Builder.new do map "/docs" do run PactBroker::Doc::Controllers::App From c71089fe9902a35a510c44417c0f673b1a71b925 Mon Sep 17 00:00:00 2001 From: Voon Siong Wong Date: Fri, 10 Nov 2023 11:06:38 +1100 Subject: [PATCH 06/69] feat: suppport `page` + `size` as pagination params (#642) This is required for compatibility with SmartBear pagination standards. PACT-1460 --- .../pagination_query_params_schema.rb | 5 +++ .../api/decorators/branch_decorator.rb | 2 +- .../api/decorators/pagination_links.rb | 4 +- .../api/resources/pagination_methods.rb | 8 +++- spec/features/get_branch_versions_spec.rb | 4 +- spec/features/get_integrations_spec.rb | 2 +- .../features/get_pacticipant_branches_spec.rb | 4 +- spec/features/get_pacticipants_spec.rb | 4 +- spec/features/get_versions_spec.rb | 2 +- ...cticipant_branches_decorator.approved.json | 2 +- .../pagination_query_params_schema_spec.rb | 42 +++++++++++++++---- .../api/resources/dashboard_spec.rb | 4 +- .../api/resources/integrations_spec.rb | 2 +- .../api/resources/pacticipants_spec.rb | 8 ++-- spec/support/shared_examples_for_responses.rb | 4 +- 15 files changed, 67 insertions(+), 30 deletions(-) diff --git a/lib/pact_broker/api/contracts/pagination_query_params_schema.rb b/lib/pact_broker/api/contracts/pagination_query_params_schema.rb index bf2761b3d..7f3e69a48 100644 --- a/lib/pact_broker/api/contracts/pagination_query_params_schema.rb +++ b/lib/pact_broker/api/contracts/pagination_query_params_schema.rb @@ -5,8 +5,13 @@ module Api module Contracts class PaginationQueryParamsSchema < BaseContract params do + # legacy format optional(:pageNumber).maybe(:integer).value(gteq?: 1) optional(:pageSize).maybe(:integer).value(gteq?: 1) + + # desired format + optional(:page).maybe(:integer).value(gteq?: 1) + optional(:size).maybe(:integer).value(gteq?: 1) end end end diff --git a/lib/pact_broker/api/decorators/branch_decorator.rb b/lib/pact_broker/api/decorators/branch_decorator.rb index 915a12975..a53f4d2db 100644 --- a/lib/pact_broker/api/decorators/branch_decorator.rb +++ b/lib/pact_broker/api/decorators/branch_decorator.rb @@ -18,7 +18,7 @@ class BranchDecorator < BaseDecorator link "pb:latest-version" do | user_options | { title: "Latest version for branch", - href: branch_versions_url(represented, user_options.fetch(:base_url)) + "?pageSize=1" + href: branch_versions_url(represented, user_options.fetch(:base_url)) + "?size=1" } end diff --git a/lib/pact_broker/api/decorators/pagination_links.rb b/lib/pact_broker/api/decorators/pagination_links.rb index eba47eeae..39b00091a 100644 --- a/lib/pact_broker/api/decorators/pagination_links.rb +++ b/lib/pact_broker/api/decorators/pagination_links.rb @@ -24,7 +24,7 @@ module PaginationLinks represented.respond_to?(:page_count) && represented.current_page < represented.page_count { - href: context[:resource_url] + "?pageSize=#{represented.page_size}&pageNumber=#{represented.current_page + 1}", + href: context[:resource_url] + "?size=#{represented.page_size}&page=#{represented.current_page + 1}", title: "Next page" } @@ -34,7 +34,7 @@ module PaginationLinks link :previous do | context | if represented.respond_to?(:first_page?) && !represented.first_page? { - href: context[:resource_url] + "?pageSize=#{represented.page_size}&pageNumber=#{represented.current_page - 1}", + href: context[:resource_url] + "?size=#{represented.page_size}&page=#{represented.current_page - 1}", title: "Previous page" } end diff --git a/lib/pact_broker/api/resources/pagination_methods.rb b/lib/pact_broker/api/resources/pagination_methods.rb index 069f71853..a8c2f4a9f 100644 --- a/lib/pact_broker/api/resources/pagination_methods.rb +++ b/lib/pact_broker/api/resources/pagination_methods.rb @@ -2,8 +2,14 @@ module PactBroker module Api module Resources module PaginationMethods + # rubocop: disable Metrics/CyclomaticComplexity def pagination_options - if request.query["pageNumber"] || request.query["pageSize"] + if request.query["page"] || request.query["size"] + { + page_number: request.query["page"]&.to_i || 1, + page_size: request.query["size"]&.to_i || 100 + } + elsif request.query["pageNumber"] || request.query["pageSize"] { page_number: request.query["pageNumber"]&.to_i || 1, page_size: request.query["pageSize"]&.to_i || 100 diff --git a/spec/features/get_branch_versions_spec.rb b/spec/features/get_branch_versions_spec.rb index 31774d407..9884cab86 100644 --- a/spec/features/get_branch_versions_spec.rb +++ b/spec/features/get_branch_versions_spec.rb @@ -27,9 +27,9 @@ end context "with pagination options" do - subject { get(path, { "pageSize" => "2", "pageNumber" => "1" }) } + subject { get(path, { "size" => "2", "page" => "1" }) } - it "only returns the number of items specified in the pageSize" do + it "only returns the number of items specified in the size" do expect(JSON.parse(subject.body).dig("_embedded", "versions").size).to eq 2 end diff --git a/spec/features/get_integrations_spec.rb b/spec/features/get_integrations_spec.rb index 04b01482c..c2389301f 100644 --- a/spec/features/get_integrations_spec.rb +++ b/spec/features/get_integrations_spec.rb @@ -23,7 +23,7 @@ end context "with pagination options" do - let(:query) { { "pageSize" => "2", "pageNumber" => "1" } } + let(:query) { { "size" => "2", "page" => "1" } } it_behaves_like "a paginated response" end diff --git a/spec/features/get_pacticipant_branches_spec.rb b/spec/features/get_pacticipant_branches_spec.rb index 4fb6b4b2b..56fcf41e6 100644 --- a/spec/features/get_pacticipant_branches_spec.rb +++ b/spec/features/get_pacticipant_branches_spec.rb @@ -21,9 +21,9 @@ it_behaves_like "a page" context "with pagination options" do - subject { get(path, { "pageSize" => "2", "pageNumber" => "1" }) } + subject { get(path, { "size" => "2", "number" => "1" }) } - it "only returns the number of items specified in the pageSize" do + it "only returns the number of items specified in the size" do expect(response_body_hash[:_links][:"pb:branches"].size).to eq 2 end diff --git a/spec/features/get_pacticipants_spec.rb b/spec/features/get_pacticipants_spec.rb index 97232bc97..4f10667ca 100644 --- a/spec/features/get_pacticipants_spec.rb +++ b/spec/features/get_pacticipants_spec.rb @@ -23,9 +23,9 @@ end context "with pagination options" do - subject { get(path, { "pageSize" => "2", "pageNumber" => "1" }) } + subject { get(path, { "size" => "2", "page" => "1" }) } - it "only returns the number of items specified in the pageSize" do + it "only returns the number of items specified in the page" do expect(response_body_hash[:_links][:"pacticipants"].size).to eq 2 end diff --git a/spec/features/get_versions_spec.rb b/spec/features/get_versions_spec.rb index f0a6ff1c2..e2c24fb8d 100644 --- a/spec/features/get_versions_spec.rb +++ b/spec/features/get_versions_spec.rb @@ -26,7 +26,7 @@ end context "with pagination options" do - subject { get(path, { "pageSize" => "1", "pageNumber" => "1" }) } + subject { get(path, { "size" => "1", "page" => "1" }) } it "paginates the response" do expect(last_response_body[:_links][:"versions"].size).to eq 1 diff --git a/spec/fixtures/approvals/pacticipant_branches_decorator.approved.json b/spec/fixtures/approvals/pacticipant_branches_decorator.approved.json index def84d52f..6f719defe 100644 --- a/spec/fixtures/approvals/pacticipant_branches_decorator.approved.json +++ b/spec/fixtures/approvals/pacticipant_branches_decorator.approved.json @@ -11,7 +11,7 @@ }, "pb:latest-version": { "title": "Latest version for branch", - "href": "http://example.org/pacticipants/Foo/branches/main/versions?pageSize=1" + "href": "http://example.org/pacticipants/Foo/branches/main/versions?size=1" } } } diff --git a/spec/lib/pact_broker/api/contracts/pagination_query_params_schema_spec.rb b/spec/lib/pact_broker/api/contracts/pagination_query_params_schema_spec.rb index 7e7afc6ae..1277f5352 100644 --- a/spec/lib/pact_broker/api/contracts/pagination_query_params_schema_spec.rb +++ b/spec/lib/pact_broker/api/contracts/pagination_query_params_schema_spec.rb @@ -19,25 +19,51 @@ module Contracts context "with values that are not numeric" do let(:params) do { - "pageNumber" => "a", - "pageSize" => "3.2" + "page" => "a", + "size" => "3.2" } end - its([:pageNumber]) { is_expected.to contain_exactly(match("integer"))} - its([:pageSize]) { is_expected.to contain_exactly(match("integer"))} + its([:page]) { is_expected.to contain_exactly(match("integer"))} + its([:size]) { is_expected.to contain_exactly(match("integer"))} end context "with values that are 0" do let(:params) do { - "pageNumber" => "-0", - "pageSize" => "-0" + "page" => "-0", + "size" => "-0" } end - its([:pageNumber]) { is_expected.to contain_exactly(match(/greater.*1/))} - its([:pageSize]) { is_expected.to contain_exactly(match(/greater.*1/))} + its([:page]) { is_expected.to contain_exactly(match(/greater.*1/))} + its([:size]) { is_expected.to contain_exactly(match(/greater.*1/))} + end + + context "legacy format" do + context "with values that are not numeric" do + let(:params) do + { + "pageNumber" => "a", + "pageSize" => "3.2" + } + end + + its([:pageNumber]) { is_expected.to contain_exactly(match("integer"))} + its([:pageSize]) { is_expected.to contain_exactly(match("integer"))} + end + + context "with values that are 0" do + let(:params) do + { + "pageNumber" => "-0", + "pageSize" => "-0" + } + end + + its([:pageNumber]) { is_expected.to contain_exactly(match(/greater.*1/))} + its([:pageSize]) { is_expected.to contain_exactly(match(/greater.*1/))} + end end end end diff --git a/spec/lib/pact_broker/api/resources/dashboard_spec.rb b/spec/lib/pact_broker/api/resources/dashboard_spec.rb index 5e669cd81..cb50ac5b7 100644 --- a/spec/lib/pact_broker/api/resources/dashboard_spec.rb +++ b/spec/lib/pact_broker/api/resources/dashboard_spec.rb @@ -24,7 +24,7 @@ module Resources end context "with pagination" do - subject { get(path, { pageNumber: 1, pageSize: 1 }) } + subject { get(path, { page: 1, size: 1 }) } it "only returns the items for the page" do expect(response_body_hash["items"].size).to eq 1 @@ -32,7 +32,7 @@ module Resources end context "with invalid pagination" do - subject { get(path, { pageNumber: -1, pageSize: -1 }) } + subject { get(path, { page: -1, size: -1 }) } it_behaves_like "an invalid pagination params response" end diff --git a/spec/lib/pact_broker/api/resources/integrations_spec.rb b/spec/lib/pact_broker/api/resources/integrations_spec.rb index 3aacb1be5..30bf76e20 100644 --- a/spec/lib/pact_broker/api/resources/integrations_spec.rb +++ b/spec/lib/pact_broker/api/resources/integrations_spec.rb @@ -23,7 +23,7 @@ module Resources let(:errors) { {} } let(:path) { "/integrations" } - let(:params) { { "pageNumber" => "1", "pageSize" => "2" } } + let(:params) { { "page" => "1", "size" => "2" } } subject { get(path, params, rack_headers) } diff --git a/spec/lib/pact_broker/api/resources/pacticipants_spec.rb b/spec/lib/pact_broker/api/resources/pacticipants_spec.rb index 6120aacb8..14a30a6a6 100644 --- a/spec/lib/pact_broker/api/resources/pacticipants_spec.rb +++ b/spec/lib/pact_broker/api/resources/pacticipants_spec.rb @@ -7,8 +7,8 @@ module Resources describe "GET" do let(:query) do { - "pageSize" => "10", - "pageNumber" => "1", + "size" => "10", + "page" => "1", "q" => "search" } end @@ -44,8 +44,8 @@ module Resources context "with invalid pagination params" do let(:query) do { - "pageSize" => "0", - "pageNumber" => "0", + "size" => "0", + "page" => "0", } end diff --git a/spec/support/shared_examples_for_responses.rb b/spec/support/shared_examples_for_responses.rb index 7e7a713a5..02885ee93 100644 --- a/spec/support/shared_examples_for_responses.rb +++ b/spec/support/shared_examples_for_responses.rb @@ -45,8 +45,8 @@ end it "includes the parameter validation errors" do - expect(response_body_hash[:errors].has_key?(:pageNumber)).to be_truthy - expect(response_body_hash[:errors].has_key?(:pageSize)).to be_truthy + expect(response_body_hash[:errors].has_key?(:page)).to be_truthy + expect(response_body_hash[:errors].has_key?(:size)).to be_truthy end end From ed14236f9d2f1e92605d97a1ba132e8e7f5b1aba Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 22 Nov 2023 11:11:15 +1100 Subject: [PATCH 07/69] chore: remove redundant embedded objects from deployed and released versions resources (#643) * chore: remove redundant embedded objects from deployed and released versions resources * test: update title expectation * chore: add back in target for v old versions of pact broker client --- .../decorators/deployed_versions_decorator.rb | 4 +-- .../embedded_deployed_version_decorator.rb | 30 +++++++++++++++++++ .../embedded_released_version_decorator.rb | 27 +++++++++++++++++ .../decorators/released_versions_decorator.rb | 4 +-- ...ently_deployed_versions_for_environment.rb | 2 +- ...ntly_supported_versions_for_environment.rb | 2 +- ...ed_versions_for_version_and_environment.rb | 2 +- ...ntly_deployed_versions_for_version_spec.rb | 2 +- 8 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 lib/pact_broker/api/decorators/embedded_deployed_version_decorator.rb create mode 100644 lib/pact_broker/api/decorators/embedded_released_version_decorator.rb diff --git a/lib/pact_broker/api/decorators/deployed_versions_decorator.rb b/lib/pact_broker/api/decorators/deployed_versions_decorator.rb index 61b0e1d18..647f7a504 100644 --- a/lib/pact_broker/api/decorators/deployed_versions_decorator.rb +++ b/lib/pact_broker/api/decorators/deployed_versions_decorator.rb @@ -1,11 +1,11 @@ require "pact_broker/api/decorators/base_decorator" -require "pact_broker/api/decorators/deployed_version_decorator" +require "pact_broker/api/decorators/embedded_deployed_version_decorator" module PactBroker module Api module Decorators class DeployedVersionsDecorator < BaseDecorator - collection :entries, as: :deployedVersions, embedded: true, :extend => PactBroker::Api::Decorators::DeployedVersionDecorator + collection :entries, as: :deployedVersions, embedded: true, :extend => PactBroker::Api::Decorators::EmbeddedDeployedVersionDecorator link :self do | context | href = append_query_if_present(context[:resource_url], context[:query_string]) diff --git a/lib/pact_broker/api/decorators/embedded_deployed_version_decorator.rb b/lib/pact_broker/api/decorators/embedded_deployed_version_decorator.rb new file mode 100644 index 000000000..26be2573e --- /dev/null +++ b/lib/pact_broker/api/decorators/embedded_deployed_version_decorator.rb @@ -0,0 +1,30 @@ +require "pact_broker/api/decorators/base_decorator" +require "pact_broker/api/decorators/embedded_pacticipant_decorator" +require "pact_broker/api/decorators/embedded_version_decorator" +require "pact_broker/api/decorators/environment_decorator" + +module PactBroker + module Api + module Decorators + class EmbeddedDeployedVersionDecorator < BaseDecorator + property :uuid + property :currently_deployed, camelize: true + property :target, camelize: true # deprecated + property :applicationInstance, getter: lambda { |_| target } + property :undeployedAt, getter: lambda { |_| undeployed_at ? FormatDateTime.call(undeployed_at) : nil }, writeable: false + + property :pacticipant, :extend => EmbeddedPacticipantDecorator, writeable: false, embedded: true, if: -> (user_options:, **_other) { user_options[:expand]&.include?(:pacticipant) } + property :version, :extend => EmbeddedVersionDecorator, writeable: false, embedded: true, if: -> (user_options:, **_other) { user_options[:expand]&.include?(:version) } + property :environment, :extend => EnvironmentDecorator, writeable: false, embedded: true, if: -> (user_options:, **_other) { user_options[:expand]&.include?(:environment) } + + include Timestamps + + link :self do | user_options | + { + href: deployed_version_url(represented, user_options.fetch(:base_url)) + } + end + end + end + end +end diff --git a/lib/pact_broker/api/decorators/embedded_released_version_decorator.rb b/lib/pact_broker/api/decorators/embedded_released_version_decorator.rb new file mode 100644 index 000000000..d98cd97fc --- /dev/null +++ b/lib/pact_broker/api/decorators/embedded_released_version_decorator.rb @@ -0,0 +1,27 @@ +require "pact_broker/api/decorators/base_decorator" +require "pact_broker/api/decorators/embedded_pacticipant_decorator" +require "pact_broker/api/decorators/embedded_version_decorator" +require "pact_broker/api/decorators/environment_decorator" + +module PactBroker + module Api + module Decorators + class EmbeddedReleasedVersionDecorator < BaseDecorator + property :uuid + property :currently_supported, camelize: true + include Timestamps + property :supportEndedAt, getter: lambda { |_| support_ended_at ? FormatDateTime.call(support_ended_at) : nil }, writeable: false + + property :pacticipant, :extend => EmbeddedPacticipantDecorator, writeable: false, embedded: true, if: -> (user_options:, **_other) { user_options[:expand]&.include?(:pacticipant) } + property :version, :extend => EmbeddedVersionDecorator, writeable: false, embedded: true, if: -> (user_options:, **_other) { user_options[:expand]&.include?(:version) } + property :environment, :extend => EnvironmentDecorator, writeable: false, embedded: true, if: -> (user_options:, **_other) { user_options[:expand]&.include?(:environment) } + + link :self do | user_options | + { + href: released_version_url(represented, user_options.fetch(:base_url)) + } + end + end + end + end +end diff --git a/lib/pact_broker/api/decorators/released_versions_decorator.rb b/lib/pact_broker/api/decorators/released_versions_decorator.rb index d21e94ab5..083a9598a 100644 --- a/lib/pact_broker/api/decorators/released_versions_decorator.rb +++ b/lib/pact_broker/api/decorators/released_versions_decorator.rb @@ -1,11 +1,11 @@ require "pact_broker/api/decorators/base_decorator" -require "pact_broker/api/decorators/released_version_decorator" +require "pact_broker/api/decorators/embedded_released_version_decorator" module PactBroker module Api module Decorators class ReleasedVersionsDecorator < BaseDecorator - collection :entries, as: :releasedVersions, embedded: true, :extend => PactBroker::Api::Decorators::ReleasedVersionDecorator + collection :entries, as: :releasedVersions, embedded: true, :extend => PactBroker::Api::Decorators::EmbeddedReleasedVersionDecorator link :self do | context | href = append_query_if_present(context[:resource_url], context[:query_string]) diff --git a/lib/pact_broker/api/resources/currently_deployed_versions_for_environment.rb b/lib/pact_broker/api/resources/currently_deployed_versions_for_environment.rb index 1d56f9f7b..b2de08e25 100644 --- a/lib/pact_broker/api/resources/currently_deployed_versions_for_environment.rb +++ b/lib/pact_broker/api/resources/currently_deployed_versions_for_environment.rb @@ -21,7 +21,7 @@ def resource_exists? end def to_json - decorator_class(decorator_name).new(deployed_versions).to_json(**decorator_options(title: title)) + decorator_class(decorator_name).new(deployed_versions).to_json(**decorator_options(title: title, expand: [:pacticipant, :version])) end def policy_name diff --git a/lib/pact_broker/api/resources/currently_supported_versions_for_environment.rb b/lib/pact_broker/api/resources/currently_supported_versions_for_environment.rb index 95dd313b6..d34e6c317 100644 --- a/lib/pact_broker/api/resources/currently_supported_versions_for_environment.rb +++ b/lib/pact_broker/api/resources/currently_supported_versions_for_environment.rb @@ -21,7 +21,7 @@ def resource_exists? end def to_json - decorator_class(decorator_name).new(released_versions).to_json(**decorator_options(title: title)) + decorator_class(decorator_name).new(released_versions).to_json(**decorator_options(title: title, expand: [:pacticipant, :version])) end def policy_name diff --git a/lib/pact_broker/api/resources/deployed_versions_for_version_and_environment.rb b/lib/pact_broker/api/resources/deployed_versions_for_version_and_environment.rb index 9e79be72a..00f8c3e0c 100644 --- a/lib/pact_broker/api/resources/deployed_versions_for_version_and_environment.rb +++ b/lib/pact_broker/api/resources/deployed_versions_for_version_and_environment.rb @@ -78,7 +78,7 @@ def application_instance end def title - "Deployed versions for #{pacticipant_name} version #{pacticipant_version_number}" + "Deployed versions for #{pacticipant_name} version #{pacticipant_version_number} in environment #{environment.display_name}" end end end diff --git a/spec/features/get_currently_deployed_versions_for_version_spec.rb b/spec/features/get_currently_deployed_versions_for_version_spec.rb index 02e25cc67..fed74c744 100644 --- a/spec/features/get_currently_deployed_versions_for_version_spec.rb +++ b/spec/features/get_currently_deployed_versions_for_version_spec.rb @@ -21,7 +21,7 @@ it "returns a list of deployed versions" do expect(response_body_hash[:_embedded][:deployedVersions]).to be_a(Array) expect(response_body_hash[:_embedded][:deployedVersions].size).to eq 1 - expect(response_body_hash[:_links][:self][:title]).to eq "Deployed versions for Foo version 1" + expect(response_body_hash[:_links][:self][:title]).to eq "Deployed versions for Foo version 1 in environment Test" expect(response_body_hash[:_links][:self][:href]).to end_with(path) end end From 7b0033e45dac5560cd416d4336123f17d4093172 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 22 Nov 2023 11:16:31 +1100 Subject: [PATCH 08/69] chore: load runtime error problem+json decorator from configuration object --- .../api/decorators/runtime_error_problem_json_decorator.rb | 2 +- lib/pact_broker/api/resources/error_response_generator.rb | 6 +++++- .../api/resources/error_response_generator_spec.rb | 6 ++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/pact_broker/api/decorators/runtime_error_problem_json_decorator.rb b/lib/pact_broker/api/decorators/runtime_error_problem_json_decorator.rb index a2f92f220..f43f76077 100644 --- a/lib/pact_broker/api/decorators/runtime_error_problem_json_decorator.rb +++ b/lib/pact_broker/api/decorators/runtime_error_problem_json_decorator.rb @@ -3,7 +3,7 @@ module PactBroker module Api module Decorators - class RuntimeErrorProblemJSONDecorator + class RuntimeErrorProblemJsonDecorator # @param message [String] def initialize(message) diff --git a/lib/pact_broker/api/resources/error_response_generator.rb b/lib/pact_broker/api/resources/error_response_generator.rb index d372c5d86..3a6e104cf 100644 --- a/lib/pact_broker/api/resources/error_response_generator.rb +++ b/lib/pact_broker/api/resources/error_response_generator.rb @@ -58,7 +58,7 @@ def self.display_message(error, message, obfuscated_message) end private_class_method def self.problem_json_response_body(message, env) - PactBroker::Api::Decorators::RuntimeErrorProblemJSONDecorator.new(message).to_hash(user_options: { base_url: env["pactbroker.base_url" ] }) + error_decorator_class(env).new(message).to_hash(user_options: { base_url: env["pactbroker.base_url" ] }) end private_class_method def self.obfuscated_error_message(error_reference) @@ -76,6 +76,10 @@ def self.display_message(error, message, obfuscated_message) private_class_method def self.problem_json?(env) env["HTTP_ACCEPT"]&.include?("application/problem+json") end + + private_class_method def self.error_decorator_class(env) + env["pactbroker.application_context"].decorator_configuration.class_for(:runtime_error_problem_json_decorator) + end end end end diff --git a/spec/lib/pact_broker/api/resources/error_response_generator_spec.rb b/spec/lib/pact_broker/api/resources/error_response_generator_spec.rb index 4f66b5537..b3e172ef3 100644 --- a/spec/lib/pact_broker/api/resources/error_response_generator_spec.rb +++ b/spec/lib/pact_broker/api/resources/error_response_generator_spec.rb @@ -1,4 +1,6 @@ require "pact_broker/api/resources/error_response_generator" +require "pact_broker/application_context" +require "pact_broker/api/decorators/runtime_error_problem_json_decorator" module PactBroker module Api @@ -13,7 +15,7 @@ module Resources let(:error_reference) { "bYWfnyWPlf" } let(:headers_and_body) { ErrorResponseGenerator.call(error, error_reference, rack_env) } - let(:rack_env) { { "pactbroker.base_url" => "http://example.org" } } + let(:rack_env) { { "pactbroker.base_url" => "http://example.org", "pactbroker.application_context" => PactBroker::ApplicationContext.default_application_context } } let(:headers) { headers_and_body.first } subject { JSON.parse(headers_and_body.last) } @@ -35,7 +37,7 @@ module Resources end context "when the Accept header includes application/problem+json" do - let(:rack_env) { { "HTTP_ACCEPT" => "application/hal+json, application/problem+json", "pactbroker.base_url" => "http://example.org" } } + let(:rack_env) { { "HTTP_ACCEPT" => "application/hal+json, application/problem+json", "pactbroker.base_url" => "http://example.org", "pactbroker.application_context" => PactBroker::ApplicationContext.default_application_context } } it "returns headers" do expect(headers).to eq("Content-Type" => "application/problem+json;charset=utf-8") From ce7dc703033cbb0a7fad4500cbbf3c69242ab3e7 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 22 Nov 2023 11:31:05 +1100 Subject: [PATCH 09/69] docs: update comments --- lib/pact_broker/api/resources/error_response_generator.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pact_broker/api/resources/error_response_generator.rb b/lib/pact_broker/api/resources/error_response_generator.rb index 3a6e104cf..c4f757b40 100644 --- a/lib/pact_broker/api/resources/error_response_generator.rb +++ b/lib/pact_broker/api/resources/error_response_generator.rb @@ -3,8 +3,9 @@ require "pact_broker/errors" require "pact_broker/messages" -# Generates the response headers and body for use when there is an unexpected -# error when executing a Webmachine resource request. +# Generates the response headers and body for use when there is a runtime +# error in the business logic (services and repositories) when executing a Webmachine resource request. +# Obfuscates any exception messages that might expose vulnerablities in production. # Uses the Accept header to determine whether to return application/problem+json # or application/hal+json, for backwards compatibility. # In the next major version of the Pact Broker, all error responses From 0c32c027713596843e3e83a382ca8591ebccd927 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Sat, 25 Nov 2023 08:42:14 +1100 Subject: [PATCH 10/69] refactor: clean up decorator context creation --- .../api/decorators/base_decorator.rb | 1 - .../api/decorators/decorator_context.rb | 23 --------- .../decorators/decorator_context_creator.rb | 49 ++++++++++++++++++- .../decorators/pact_version_decorator_spec.rb | 4 +- .../api/decorators/webhooks_decorator_spec.rb | 10 ++-- .../api/resources/all_webhooks_spec.rb | 2 +- .../api/resources/integrations_spec.rb | 2 +- .../resources/pacticipant_webhooks_spec.rb | 2 +- .../api/resources/pacticipants_spec.rb | 2 +- 9 files changed, 60 insertions(+), 35 deletions(-) delete mode 100644 lib/pact_broker/api/decorators/decorator_context.rb diff --git a/lib/pact_broker/api/decorators/base_decorator.rb b/lib/pact_broker/api/decorators/base_decorator.rb index ec5a801c4..96bc5892c 100644 --- a/lib/pact_broker/api/decorators/base_decorator.rb +++ b/lib/pact_broker/api/decorators/base_decorator.rb @@ -1,7 +1,6 @@ require "roar/decorator" require "roar/json/hal" require "pact_broker/api/pact_broker_urls" -require "pact_broker/api/decorators/decorator_context" require "pact_broker/api/decorators/format_date_time" require "pact_broker/string_refinements" require "pact_broker/hash_refinements" diff --git a/lib/pact_broker/api/decorators/decorator_context.rb b/lib/pact_broker/api/decorators/decorator_context.rb deleted file mode 100644 index 2bf4eded7..000000000 --- a/lib/pact_broker/api/decorators/decorator_context.rb +++ /dev/null @@ -1,23 +0,0 @@ -module PactBroker - module Api - module Decorators - class DecoratorContext < Hash - attr_reader :base_url, :resource_url, :resource_title, :env, :query_string, :request_url - - def initialize base_url, resource_url, env, options = {} - @base_url = self[:base_url] = base_url - @resource_url = self[:resource_url] = resource_url - @resource_title = self[:resource_title] = options[:resource_title] - @env = self[:env] = env - @query_string = self[:query_string] = (env["QUERY_STRING"] && !env["QUERY_STRING"].empty? ? env["QUERY_STRING"] : nil) - @request_url = self[:request_url] = query_string ? resource_url + "?" + query_string : resource_url - merge!(options) - end - - def to_s - "DecoratorContext #{super}" - end - end - end - end -end diff --git a/lib/pact_broker/api/decorators/decorator_context_creator.rb b/lib/pact_broker/api/decorators/decorator_context_creator.rb index 6d9441a3c..c712941d3 100644 --- a/lib/pact_broker/api/decorators/decorator_context_creator.rb +++ b/lib/pact_broker/api/decorators/decorator_context_creator.rb @@ -1,11 +1,56 @@ -require "pact_broker/api/decorators/decorator_context" +# Builds the Hash that is passed into the Decorator as the `user_options`. It contains the request details, rack env, the (optional) title +# and anything else that is required by the decorator to render the resource (eg. the pacticipant that the versions belong to) module PactBroker module Api module Decorators class DecoratorContextCreator + + # @param [PactBroker::BaseResource] the Pact Broker webmachine resource + # @param [Hash] options any extra options that need to be passed through to the decorator. + # @return [Hash] decorator_context + + # decorator_context [String] :base_url + # The location where the Pact Broker is hosted. + # eg. http://some.host:9292/pact_broker + # Always present + + # decorator_context [String] :resource_url + # The resource URL without any query string. + # eg. http://some.host:9292/pact_broker/pacticipants/Foo/versions + # Always present + + # decorator_context [String] :query_string + # The query string. + # "page=1&size=50" + # May be empty + + # decorator_context [String] :request_url + # The full request URL. + # eg. http://some.host:9292/pact_broker/pacticipants/Foo/versions?page=1&size=50 + # Always present + + # decorator_context [Hash] :env + # The rack env. + # Always present + + # decorator_context [Hash] :resource_title + # eg. "Pacticipant versions for Foo" + # Optional + # Used when a single decorator is being used for multiple resources and the title needs to be + # set from the resource. + def self.call(resource, options) - Decorators::DecoratorContext.new(resource.base_url, resource.resource_url, resource.request.env, options) + env = resource.request.env + decorator_context = {} + decorator_context[:base_url] = resource.base_url + decorator_context[:resource_url] = resource.resource_url + decorator_context[:query_string] = query_string = (env["QUERY_STRING"] && !env["QUERY_STRING"].empty? ? env["QUERY_STRING"] : nil) + decorator_context[:request_url] = query_string ? resource.resource_url + "?" + query_string : resource.resource_url + decorator_context[:env] = env + decorator_context[:resource_title] = options[:resource_title] + decorator_context.merge!(options) + decorator_context end end end diff --git a/spec/lib/pact_broker/api/decorators/pact_version_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/pact_version_decorator_spec.rb index 280c040ec..f7ad6c7d6 100644 --- a/spec/lib/pact_broker/api/decorators/pact_version_decorator_spec.rb +++ b/spec/lib/pact_broker/api/decorators/pact_version_decorator_spec.rb @@ -33,9 +33,9 @@ module Decorators build_url: "http://build" ) end - let(:decorator_context) { DecoratorContext.new(base_url, "", {}) } + let(:user_options) { { base_url: base_url } } - let(:json) { PactVersionDecorator.new(pact).to_json(user_options: decorator_context) } + let(:json) { PactVersionDecorator.new(pact).to_json(user_options: user_options) } subject { JSON.parse(json, symbolize_names: true) } diff --git a/spec/lib/pact_broker/api/decorators/webhooks_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/webhooks_decorator_spec.rb index 320fb4ea7..082abeedb 100644 --- a/spec/lib/pact_broker/api/decorators/webhooks_decorator_spec.rb +++ b/spec/lib/pact_broker/api/decorators/webhooks_decorator_spec.rb @@ -15,15 +15,19 @@ module Decorators let(:base_url) { "http://example.org" } let(:resource_url) { "http://example.org/webhooks" } - let(:decorator_context) do - DecoratorContext.new(base_url, resource_url, {}, resource_title: "Title") + let(:user_options) do + { + base_url: base_url, + resource_url: resource_url, + resource_title: "Title" + } end let(:webhooks) { [webhook] } describe "to_json" do - let(:json) { WebhooksDecorator.new(webhooks).to_json(user_options: decorator_context) } + let(:json) { WebhooksDecorator.new(webhooks).to_json(user_options: user_options) } subject { JSON.parse(json, symbolize_names: true) } diff --git a/spec/lib/pact_broker/api/resources/all_webhooks_spec.rb b/spec/lib/pact_broker/api/resources/all_webhooks_spec.rb index ab048a040..e657c2f5c 100644 --- a/spec/lib/pact_broker/api/resources/all_webhooks_spec.rb +++ b/spec/lib/pact_broker/api/resources/all_webhooks_spec.rb @@ -129,7 +129,7 @@ module Resources it "generates a JSON representation of the webhook" do expect(Decorators::WebhooksDecorator).to receive(:new).with(webhooks) - expect(decorator).to receive(:to_json).with(user_options: instance_of(Decorators::DecoratorContext)) + expect(decorator).to receive(:to_json).with(user_options: instance_of(Hash)) subject end diff --git a/spec/lib/pact_broker/api/resources/integrations_spec.rb b/spec/lib/pact_broker/api/resources/integrations_spec.rb index 30bf76e20..ff0246510 100644 --- a/spec/lib/pact_broker/api/resources/integrations_spec.rb +++ b/spec/lib/pact_broker/api/resources/integrations_spec.rb @@ -41,7 +41,7 @@ module Resources it "renders the integrations" do expect(decorator_class).to receive(:new).with(integrations) - expect(decorator).to receive(:to_json).with(user_options: instance_of(Decorators::DecoratorContext)) + expect(decorator).to receive(:to_json).with(user_options: instance_of(Hash)) expect(subject.body).to eq json end diff --git a/spec/lib/pact_broker/api/resources/pacticipant_webhooks_spec.rb b/spec/lib/pact_broker/api/resources/pacticipant_webhooks_spec.rb index a997c15e4..a58300ac8 100644 --- a/spec/lib/pact_broker/api/resources/pacticipant_webhooks_spec.rb +++ b/spec/lib/pact_broker/api/resources/pacticipant_webhooks_spec.rb @@ -40,7 +40,7 @@ module Resources it "generates a JSON body" do expect(Decorators::WebhooksDecorator).to receive(:new).with(webhooks) - expect(decorator).to receive(:to_json).with(user_options: instance_of(Decorators::DecoratorContext)) + expect(decorator).to receive(:to_json).with(user_options: instance_of(Hash)) subject end diff --git a/spec/lib/pact_broker/api/resources/pacticipants_spec.rb b/spec/lib/pact_broker/api/resources/pacticipants_spec.rb index 14a30a6a6..034306b11 100644 --- a/spec/lib/pact_broker/api/resources/pacticipants_spec.rb +++ b/spec/lib/pact_broker/api/resources/pacticipants_spec.rb @@ -123,7 +123,7 @@ module Resources it "creates a JSON representation of the new pacticipant" do expect(decorator_class).to receive(:new).with(created_model) - expect(decorator).to receive(:to_json).with(user_options: instance_of(Decorators::DecoratorContext)) + expect(decorator).to receive(:to_json).with(user_options: instance_of(Hash)) subject end From 9a637327856114f19715d36c17b6371d1a821501 Mon Sep 17 00:00:00 2001 From: Voon Siong Wong Date: Wed, 6 Dec 2023 13:44:06 +1100 Subject: [PATCH 11/69] feat: add no-cache header PACT-1499 --- lib/pact_broker/app.rb | 2 ++ lib/rack/pact_broker/add_cache_header.rb | 14 ++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 lib/rack/pact_broker/add_cache_header.rb diff --git a/lib/pact_broker/app.rb b/lib/pact_broker/app.rb index af4c0c932..d44c893b1 100644 --- a/lib/pact_broker/app.rb +++ b/lib/pact_broker/app.rb @@ -21,6 +21,7 @@ require "rack/pact_broker/configurable_make_it_later" require "rack/pact_broker/no_auth" require "rack/pact_broker/reset_thread_data" +require "rack/pact_broker/add_cache_header" require "rack/pact_broker/add_vary_header" require "rack/pact_broker/use_when" require "rack/pact_broker/application_context" @@ -183,6 +184,7 @@ def configure_middleware @app_builder.use Rack::PactBroker::InvalidUriProtection @app_builder.use Rack::PactBroker::ResetThreadData @app_builder.use Rack::PactBroker::AddPactBrokerVersionHeader + @app_builder.use Rack::PactBroker::AddCacheHeader @app_builder.use Rack::PactBroker::AddVaryHeader @app_builder.use Rack::Static, :urls => ["/stylesheets", "/css", "/fonts", "/js", "/javascripts", "/images"], :root => PactBroker.project_root.join("public") @app_builder.use Rack::Static, :urls => ["/favicon.ico"], :root => PactBroker.project_root.join("public/images"), header_rules: [[:all, {"Content-Type" => "image/x-icon"}]] diff --git a/lib/rack/pact_broker/add_cache_header.rb b/lib/rack/pact_broker/add_cache_header.rb new file mode 100644 index 000000000..1f17e9cc9 --- /dev/null +++ b/lib/rack/pact_broker/add_cache_header.rb @@ -0,0 +1,14 @@ +module Rack + module PactBroker + class AddCacheHeader + def initialize app + @app = app + end + + def call(env) + status, headers, body = @app.call(env) + [status, { "Cache-Control" => "no-cache" }.merge(headers || {}), body] + end + end + end +end From 930b45cd103f36e1cd9d44ba3b5551391a79055a Mon Sep 17 00:00:00 2001 From: Voon Siong Wong Date: Wed, 6 Dec 2023 13:45:38 +1100 Subject: [PATCH 12/69] fix: raise 404 on paths with missing path segments (#648) PACT-13 --- lib/pact_broker/app.rb | 2 +- .../pact_broker/invalid_uri_protection.rb | 22 ++++++++++++++++--- .../invalid_uri_protection_spec.rb | 12 +++++++++- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/lib/pact_broker/app.rb b/lib/pact_broker/app.rb index d44c893b1..c67d9948e 100644 --- a/lib/pact_broker/app.rb +++ b/lib/pact_broker/app.rb @@ -181,6 +181,7 @@ def configure_middleware @app_builder.use PactBroker::Api::Middleware::HttpDebugLogs if configuration.http_debug_logging_enabled configure_basic_auth configure_rack_protection + @app_builder.use Rack::PactBroker::ApplicationContext, application_context @app_builder.use Rack::PactBroker::InvalidUriProtection @app_builder.use Rack::PactBroker::ResetThreadData @app_builder.use Rack::PactBroker::AddPactBrokerVersionHeader @@ -191,7 +192,6 @@ def configure_middleware @app_builder.use Rack::PactBroker::ConvertFileExtensionToAcceptHeader # Rack::PactBroker::SetBaseUrl needs to be before the Rack::PactBroker::HalBrowserRedirect @app_builder.use Rack::PactBroker::SetBaseUrl, configuration.base_urls - @app_builder.use Rack::PactBroker::ApplicationContext, application_context if configuration.use_hal_browser logger.info "Mounting HAL browser" diff --git a/lib/rack/pact_broker/invalid_uri_protection.rb b/lib/rack/pact_broker/invalid_uri_protection.rb index 536e4fb98..850c6aa7e 100644 --- a/lib/rack/pact_broker/invalid_uri_protection.rb +++ b/lib/rack/pact_broker/invalid_uri_protection.rb @@ -12,6 +12,8 @@ module PactBroker class InvalidUriProtection include ::PactBroker::Messages + CONSECUTIVE_SLASH = /\/{2,}/ + def initialize app @app = app end @@ -19,12 +21,12 @@ def initialize app def call env if (uri = valid_uri?(env)) if (error_message = validate(uri)) - [422, {"Content-Type" => "text/plain"}, [error_message]] + [422, headers, [body(env, error_message, "Unprocessable", "invalid-request-parameter-value", 422)]] else app.call(env) end else - [404, {}, []] + [404, headers, [body(env, "Empty path component found", "Not Found", "not-found", 404)]] end end @@ -34,7 +36,9 @@ def call env def valid_uri? env begin - parse(::Rack::Request.new(env).url) + uri = parse(::Rack::Request.new(env).url) + return nil if CONSECUTIVE_SLASH.match(uri.path) + uri rescue URI::InvalidURIError, ArgumentError nil end @@ -52,6 +56,18 @@ def validate(uri) message("errors.tab_in_url_path") end end + + def headers + {"Content-Type" => "application/problem+json"} + end + + def body(env, detail, title, type, status) + env["pactbroker.application_context"] + .decorator_configuration + .class_for(:custom_error_problem_json_decorator) + .new(detail: detail, title: title, type: type, status: status) + .to_json(user_options: { base_url: env["pactbroker.base_url"] }) + end end end end diff --git a/spec/lib/rack/pact_broker/invalid_uri_protection_spec.rb b/spec/lib/rack/pact_broker/invalid_uri_protection_spec.rb index 73e8e3004..9e2617ade 100644 --- a/spec/lib/rack/pact_broker/invalid_uri_protection_spec.rb +++ b/spec/lib/rack/pact_broker/invalid_uri_protection_spec.rb @@ -1,4 +1,6 @@ require "rack/pact_broker/invalid_uri_protection" +require "pact_broker/application_context" +require "pact_broker/api/decorators/custom_error_problem_json_decorator" module Rack module PactBroker @@ -7,7 +9,7 @@ module PactBroker let(:app) { InvalidUriProtection.new(target_app) } let(:path) { "/foo" } - subject { get(path) } + subject { get(path, {}, {"pactbroker.application_context" => ::PactBroker::ApplicationContext.default_application_context} ) } context "with a URI that the Ruby default URI library cannot parse" do let(:path) { "/badpath" } @@ -27,6 +29,14 @@ module PactBroker expect(subject.status).to eq 200 end + context "when the path contains missing path segments" do + let(:path) { "/foo//bar" } + + it "returns a 404" do + expect(subject.status).to eq 404 + end + end + context "when the URI contains a new line because someone forgot to strip the result of `git rev-parse HEAD`, and I have totally never done this before myself" do let(:path) { "/foo%0A/bar" } From 4adc657f1323c2c64878e582f08e67c1ec69ddf9 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 6 Dec 2023 13:45:52 +1100 Subject: [PATCH 13/69] chore: update the describe routes code and all routes spec (#647) --- lib/pact_broker/api/resources/all_webhooks.rb | 6 +- lib/webmachine/describe_routes.rb | 92 ++++++++++-------- .../api/resources/all_routes_spec.rb | 43 +++++---- spec/support/all_routes_spec_support.yml | 96 +------------------ tasks/development.rake | 12 ++- 5 files changed, 89 insertions(+), 160 deletions(-) diff --git a/lib/pact_broker/api/resources/all_webhooks.rb b/lib/pact_broker/api/resources/all_webhooks.rb index 95b492bee..d8d4f544e 100644 --- a/lib/pact_broker/api/resources/all_webhooks.rb +++ b/lib/pact_broker/api/resources/all_webhooks.rb @@ -38,7 +38,7 @@ def to_json end def from_json - saved_webhook = webhook_service.create(next_uuid, webhook, consumer, provider) + saved_webhook = webhook_service.create(next_uuid, webhook, webhook_consumer, webhook_provider) response.body = decorator_class(:webhook_decorator).new(saved_webhook).to_json(**decorator_options) end @@ -60,11 +60,11 @@ def schema api_contract_class(:webhook_contract) end - def consumer + def webhook_consumer webhook.consumer&.name ? pacticipant_service.find_pacticipant_by_name(webhook.consumer.name) : nil end - def provider + def webhook_provider webhook.provider&.name ? pacticipant_service.find_pacticipant_by_name(webhook.provider.name) : nil end diff --git a/lib/webmachine/describe_routes.rb b/lib/webmachine/describe_routes.rb index 47c3cb2e5..c75970a89 100644 --- a/lib/webmachine/describe_routes.rb +++ b/lib/webmachine/describe_routes.rb @@ -1,6 +1,10 @@ require "webmachine/adapters/rack_mapped" require "pact_broker/string_refinements" +# Code to describe the routes in a Webmachine API, including +# path, resource class, allowed methods, schemas, policy class. +# Used in tests and in the pact_broker:routes task + module Webmachine class DescribeRoutes using PactBroker::StringRefinements @@ -12,18 +16,11 @@ class DescribeRoutes :resource_name, :resource_class_location, :allowed_methods, - :policy_class, + :policy_names, + :policy_classes, # only used by pactflow :schemas, keyword_init: true) do - def [](key) - if respond_to?(key) - send(key) - else - nil - end - end - def path_include?(component) path.include?(component) end @@ -47,7 +44,7 @@ def build_resource(env, application_context, path_param_values) }.merge(path_params) rack_req = ::Rack::Request.new({ "REQUEST_METHOD" => "GET", "rack.input" => StringIO.new("") }.merge(env) ) - dummy_request = Webmachine::Adapters::Rack::RackRequest.new( + request = Webmachine::Adapters::Rack::RackRequest.new( rack_req.env["REQUEST_METHOD"], path, Webmachine::Headers.from_cgi({"HTTP_HOST" => "example.org"}.merge(env)), @@ -56,13 +53,13 @@ def build_resource(env, application_context, path_param_values) {}, rack_req.env ) - dummy_request.path_info = path_info - resource_class.new(dummy_request, Webmachine::Response.new) + request.path_info = path_info + resource_class.new(request, Webmachine::Response.new) end end def self.call(webmachine_applications, search_term: nil) - path_mappings = webmachine_applications.flat_map { | webmachine_application | paths_to_resource_class_mappings(webmachine_application) } + path_mappings = webmachine_applications.flat_map { | webmachine_application | build_routes(webmachine_application) } if search_term path_mappings = path_mappings.select{ |(route, _)| route[:path].include?(search_term) } @@ -71,8 +68,10 @@ def self.call(webmachine_applications, search_term: nil) path_mappings.sort_by{ | mapping | mapping[:path] } end - def self.paths_to_resource_class_mappings(webmachine_application) - webmachine_application.routes.collect do | webmachine_route | + # Build a Route object to describe every Webmachine route defined in the app.routes block + # @return [Array] + def self.build_routes(webmachine_application) + webmachine_routes_to_describe(webmachine_application).collect do | webmachine_route | resource_path_absolute = Pathname.new(source_location_for(webmachine_route.resource)) Route.new({ path: "/" + webmachine_route.path_spec.collect{ |part| part.is_a?(Symbol) ? ":#{part}" : part }.join("/"), @@ -80,19 +79,23 @@ def self.paths_to_resource_class_mappings(webmachine_application) resource_class: webmachine_route.resource, resource_name: webmachine_route.instance_variable_get(:@bindings)[:resource_name], resource_class_location: resource_path_absolute.relative_path_from(Pathname.pwd).to_s - }.merge(info_from_resource_instance(webmachine_route, webmachine_application.application_context))) - end.reject{ | route | route.resource_class == Webmachine::Trace::TraceResource } + }.merge(properties_for_webmachine_route(webmachine_route, webmachine_application.application_context))) + end end - def self.info_from_resource_instance(webmachine_route, application_context) + def self.webmachine_routes_to_describe(webmachine_application) + webmachine_application.routes.reject{ | route | route.resource == Webmachine::Trace::TraceResource }.collect + end + + def self.properties_for_webmachine_route(webmachine_route, application_context) with_no_logging do path_info = { application_context: application_context, pacticipant_name: "foo", pacticipant_version_number: "1", resource_name: "foo" } path_info.default = "1" - dummy_request = dummy_request(http_method: "GET", path_info: path_info) + request = build_request(http_method: "GET", path_info: path_info) - dummy_resource = webmachine_route.resource.new(dummy_request, Webmachine::Response.new) - if dummy_resource - info_from_dummy_resource(dummy_resource, webmachine_route, path_info) + resource = webmachine_route.resource.new(request, Webmachine::Response.new) + if resource + properties_for_resource(resource.allowed_methods - ["OPTIONS"], webmachine_route, application_context) else {} end @@ -102,29 +105,38 @@ def self.info_from_resource_instance(webmachine_route, application_context) {} end - def self.info_from_dummy_resource(dummy_resource, webmachine_route, path_info) + # Return the properties of the resource that can only be determined by instantiating the resource + # @return [Hash] + def self.properties_for_resource(allowed_methods, webmachine_route, application_context) + schemas = [] + policy_names = [] + allowed_methods.each do | http_method | + resource = build_resource(webmachine_route, http_method, application_context) + if (schema_class = resource.respond_to?(:schema, true) && resource.send(:schema)) + schemas << { http_method: http_method, class: schema_class, location: source_location_for(schema_class)} + end + + policy_names << resource.policy_name + end + { - allowed_methods: dummy_resource.allowed_methods, - schemas: dummy_resource.respond_to?(:schema, true) && dummy_resource.send(:schema) ? schemas(dummy_resource.allowed_methods, webmachine_route.resource, path_info) : nil - }.compact + allowed_methods: allowed_methods, + schemas: schemas, + policy_names: policy_names.uniq + } end - # This is not entirely accurate, because some GET requests have schemas too, but we can't tell that statically at the moment - def self.schemas(allowed_methods, resource, path_info) - (allowed_methods - ["GET", "OPTIONS", "DELETE"]).collect do | http_method | - resource.new(dummy_request(http_method: http_method, path_info: path_info), Webmachine::Response.new).send(:schema) - end.uniq.collect do | schema_class | - { - class: schema_class, - location: source_location_for(schema_class) - } - end + def self.build_resource(webmachine_route, http_method, application_context) + path_info = { application_context: application_context, pacticipant_name: "foo", pacticipant_version_number: "1", resource_name: "foo" } + path_info.default = "1" + request = build_request(http_method: http_method, path_info: path_info) + webmachine_route.resource.new(request, Webmachine::Response.new) end - def self.dummy_request(http_method: "GET", path_info: ) - dummy_request = Webmachine::Adapters::Rack::RackRequest.new(http_method, "/", Webmachine::Headers["host" => "example.org"], nil, {}, {}, { "REQUEST_METHOD" => http_method }) - dummy_request.path_info = path_info - dummy_request + def self.build_request(http_method: "GET", path_info: ) + request = Webmachine::Adapters::Rack::RackRequest.new(http_method, "/", Webmachine::Headers["host" => "example.org"], nil, {}, {}, { "REQUEST_METHOD" => http_method }) + request.path_info = path_info + request end def self.source_location_for(clazz) diff --git a/spec/lib/pact_broker/api/resources/all_routes_spec.rb b/spec/lib/pact_broker/api/resources/all_routes_spec.rb index d1b4a5beb..2f7d861c1 100644 --- a/spec/lib/pact_broker/api/resources/all_routes_spec.rb +++ b/spec/lib/pact_broker/api/resources/all_routes_spec.rb @@ -29,27 +29,22 @@ uuid: UUID } -REQUESTS_WHICH_ARE_EXECTED_TO_HAVE_NO_POLICY_RECORD = YAML.safe_load(File.read("spec/support/all_routes_spec_support.yml"))["requests_which_are_exected_to_have_no_policy_record"] - -RSpec.describe "all the routes" do - it "has a name for every route" do - expect(PactBroker.routes.reject(&:resource_name)).to eq [] - end - - it "has a unique name (except for the ones that don't which we can't change now because it would ruin the PF metrics)" do - dupliates = PactBroker.routes.collect(&:resource_name).group_by(&:itself).select { | _, values | values.size > 1 }.keys - expect(dupliates).to eq(["pact_publication", "verification_results", "verification_result"]) - end -end +REQUESTS_WHICH_ARE_EXECTED_TO_HAVE_NO_POLICY_RECORD = YAML.safe_load(File.read("spec/support/all_routes_spec_support.yml"))["requests_which_are_exected_to_have_no_policy_record_or_pacticipant"] +# Ensure that every resource/http method has a way of determining whether or not a user is allowed to call it. +# The actual permissions logic will be performed in Pactflow as the Pact Broker does not support a permissions model. +# Almost every route should either be associated with a pacticipant, or it should have a method to provide the policy_record. +# Some routes, as listed in spec/support/all_routes_spec_support.yml do not need either. +# This test is here to ensure that every new route added to the Pact Broker API has been considered, and explictly whitelisted if necessary. PactBroker.routes.each do | pact_broker_route | describe "#{pact_broker_route.path} (#{pact_broker_route.resource_name})" do - pact_broker_route.allowed_methods.each do | allowed_method | + (pact_broker_route.allowed_methods - ["OPTIONS"]).each do | allowed_method | - if allowed_method != "OPTIONS" && !REQUESTS_WHICH_ARE_EXECTED_TO_HAVE_NO_POLICY_RECORD.include?("#{pact_broker_route.resource_name} #{allowed_method}") + if !REQUESTS_WHICH_ARE_EXECTED_TO_HAVE_NO_POLICY_RECORD.include?("#{pact_broker_route.resource_name} #{allowed_method}") describe allowed_method do before do + # Create test data that matches the POTENTIAL_PARAMS above so that every route has data present for it td.create_consumer("foo") .create_provider("bar") .create_consumer_version("1", branch: "main", tag_names: ["prod"]) @@ -63,12 +58,26 @@ PactBroker::Pacts::PactVersion.first.update(sha: PACT_VERSION_SHA) end - it "has a policy record object" do - dummy_resource = pact_broker_route.build_resource({ "REQUEST_METHOD" => allowed_method }, PactBroker::ApplicationContext.default_application_context, POTENTIAL_PARAMS) - expect(dummy_resource.policy_record).to_not be nil + it "has a policy record object or a consumer/provider/pacticipant in the route" do + resource = pact_broker_route.build_resource({ "REQUEST_METHOD" => allowed_method }, PactBroker::ApplicationContext.default_application_context, POTENTIAL_PARAMS) + + thing = resource.consumer || resource.provider || resource.pacticipant || resource.respond_to?(:policy_record) + + expect(thing).to_not be_falsy end end end end end end + +RSpec.describe "all the routes" do + it "has a name for every route" do + expect(PactBroker.routes.reject(&:resource_name)).to eq [] + end + + it "has a unique name (except for the ones that don't which we can't change now because it would ruin the PF metrics)" do + duplicates = PactBroker.routes.collect(&:resource_name).group_by(&:itself).select { | _, values | values.size > 1 }.keys + expect(duplicates).to eq(["pact_publication", "verification_results", "verification_result"]) + end +end diff --git a/spec/support/all_routes_spec_support.yml b/spec/support/all_routes_spec_support.yml index f99f47597..cbb370047 100644 --- a/spec/support/all_routes_spec_support.yml +++ b/spec/support/all_routes_spec_support.yml @@ -1,114 +1,20 @@ -# Need to change this to use the path because there are resources with the same resource_name (pact_publications, verification_results and verification_result) -requests_which_are_exected_to_have_no_policy_record: - - publish_contracts POST +requests_which_are_exected_to_have_no_policy_record_or_pacticipant: - index GET - can_i_deploy GET - dashboard GET - - integration_dashboard GET - environments GET - environments POST - environment GET - environment PUT - environment DELETE - - group GET - integrations GET - integrations DELETE - - integration DELETE - matrix GET - - matrix_consumer_provider GET - - matrix_tag_badge GET - metrics GET - - latest_pact_publications GET - - pact_publication GET - - pact_publication PUT - - pact_publication DELETE - - pact_publication PATCH - pacticipants GET - pacticipants POST - - pacticipant GET - - pacticipant PUT - - pacticipant PATCH - - pacticipant DELETE - - can_i_deploy_latest_branch_version_to_environment GET - - can_i_deploy_latest_branch_version_to_environment_badge GET - - branch GET - - branch DELETE - - branch_version GET - - branch_version PUT - - branch_version DELETE - - pacticipant_label GET - - pacticipant_label PUT - - pacticipant_label DELETE - - latest_pacticipant_version GET - - latest_tagged_pacticipant_version GET - - can_i_deploy_latest_tagged_version_to_tag GET - - can_i_deploy_latest_tagged_version_to_tag_badge GET - - pacticipant_versions GET - - pacticipant_version GET - - pacticipant_version PUT - - pacticipant_version PATCH - - pacticipant_version DELETE - - pacticipant_version_tag GET - - pacticipant_version_tag PUT - - pacticipant_version_tag DELETE - - pacticipant_branch_versions GET - pacticipants_for_label GET - latest_pacts GET - - provider_pact_publications GET - - pact_publications_for_branch DELETE - - latest_pact_publication GET - - latest_untagged_pact_publication GET - - latest_untagged_pact_badge GET - - latest_pact_publication_for_branch GET - - latest_tagged_pact_publication GET - - latest_tagged_pact_badge GET - - latest_verification_results_for_latest_tagged_pact_publication GET - - latest_verification_results_for_latest_tagged_pact_publication DELETE - - latest_pact_badge GET - - latest_verification_results_for_latest_pact_publication GET - - latest_verification_results_for_latest_pact_publication DELETE - - pact_publication GET - - pact_version_diff_by_pact_version_sha GET - - pact_publication GET - - verification_results POST - - verification_result GET - - verification_result DELETE - - verification_results POST - - verification_result GET - - verification_result DELETE - - latest_verification_results_for_pact_version GET - - latest_verification_results_for_pact_version DELETE - - tagged_pact_publications GET - - tagged_pact_publications DELETE - - pact_publication GET - - pact_publication PUT - - pact_publication DELETE - - pact_publication PATCH - - previous_distinct_pact_version_diff GET - - pact_version_diff_by_consumer_version GET - - previous_distinct_pact_version GET - - latest_verification_results_for_pact_publication GET - - latest_verification_results_for_pact_publication DELETE - - pact_publications GET - - pact_publications DELETE - - pact_publication GET - - pact_publication PUT - - pact_publication DELETE - - pact_publication PATCH - - pact_webhooks POST - - pact_webhooks GET - - pact_webhooks_status GET - - pacts_for_verification GET - - pacts_for_verification POST - - latest_provider_pact_publications GET - - latest_tagged_provider_pact_publications GET - - tagged_provider_pact_publications GET - relationships GET - error_test GET - error_test POST - - verification_results_for_consumer_version GET - - webhooks GET - - consumer_webhooks GET - - provider_webhooks GET - - pacticipant_branches GET - - pacticipant_webhooks GET diff --git a/tasks/development.rake b/tasks/development.rake index 2bcc3efa1..ef62580a2 100644 --- a/tasks/development.rake +++ b/tasks/development.rake @@ -46,12 +46,14 @@ task :'pact_broker:routes', [:search_term] do | _t, args | puts "#{route.path}" puts " allowed_methods: #{route.allowed_methods.join(", ")}" puts " class: #{route.resource_class}" - puts " location: #{route.resource_class_location}" - if route[:schemas] + puts " location: #{route.resource_class_location}" + puts " policy_name: #{route.policy_names.collect(&:to_s).join(", ")}" + if route.schemas.any? puts " schemas:" - route[:schemas].each do | schema | - puts " class: #{schema[:class]}" - puts " location: #{schema[:location]}" + route.schemas.each do | schema | + puts " - method: #{schema[:http_method]}" + puts " class: #{schema[:class]}" + puts " location: #{schema[:location]}" end end From c216bec84c5aa95f0aa86a6c9fe7277eec816b06 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 6 Dec 2023 13:52:14 +1100 Subject: [PATCH 14/69] feat: add latest version for branch endpoint (#644) --- lib/pact_broker/api.rb | 1 + .../api/decorators/branch_decorator.rb | 2 +- .../api/decorators/version_decorator.rb | 3 ++- .../api/decorators/versions_decorator.rb | 16 ++++++++++--- lib/pact_broker/api/pact_broker_urls.rb | 4 ++++ .../api/resources/latest_version.rb | 2 ++ .../get_latest_version_for_branch_spec.rb | 18 +++++++++++++++ ...cticipant_branches_decorator.approved.json | 2 +- .../api/decorators/version_decorator_spec.rb | 6 ++--- .../api/decorators/versions_decorator_spec.rb | 23 ++++++++++--------- 10 files changed, 57 insertions(+), 20 deletions(-) create mode 100644 spec/features/get_latest_version_for_branch_spec.rb diff --git a/lib/pact_broker/api.rb b/lib/pact_broker/api.rb index 3954a1ce4..4e7cefa00 100644 --- a/lib/pact_broker/api.rb +++ b/lib/pact_broker/api.rb @@ -97,6 +97,7 @@ def self.build_api(application_context = PactBroker::ApplicationContext.default_ add ["pacticipants", :pacticipant_name, "versions", :pacticipant_version_number, "tags", :tag_name], Api::Resources::Tag, {resource_name: "pacticipant_version_tag"} add ["pacticipants", :pacticipant_name, "branches"], Api::Resources::PacticipantBranches, {resource_name: "pacticipant_branches"} add ["pacticipants", :pacticipant_name, "branches", :branch_name], Api::Resources::Branch, { resource_name: "branch" } + add ["pacticipants", :pacticipant_name, "branches", :branch_name, "latest-version"], Api::Resources::LatestVersion, { resource_name: "latest_pacticipant_version_for_branch" } add ["pacticipants", :pacticipant_name, "branches", :branch_name, "versions", :version_number], Api::Resources::BranchVersion, { resource_name: "branch_version" } add ["pacticipants", :pacticipant_name, "branches", :branch_name, "latest-version", "can-i-deploy", "to-environment", :environment_name], Api::Resources::CanIDeployPacticipantVersionByBranchToEnvironment, { resource_name: "can_i_deploy_latest_branch_version_to_environment" } add ["pacticipants", :pacticipant_name, "branches", :branch_name, "latest-version", "can-i-deploy", "to-environment", :environment_name, "badge"], Api::Resources::CanIDeployPacticipantVersionByBranchToEnvironmentBadge, { resource_name: "can_i_deploy_latest_branch_version_to_environment_badge" } diff --git a/lib/pact_broker/api/decorators/branch_decorator.rb b/lib/pact_broker/api/decorators/branch_decorator.rb index a53f4d2db..9f7fee210 100644 --- a/lib/pact_broker/api/decorators/branch_decorator.rb +++ b/lib/pact_broker/api/decorators/branch_decorator.rb @@ -18,7 +18,7 @@ class BranchDecorator < BaseDecorator link "pb:latest-version" do | user_options | { title: "Latest version for branch", - href: branch_versions_url(represented, user_options.fetch(:base_url)) + "?size=1" + href: latest_version_for_branch_url(represented, user_options.fetch(:base_url)) } end diff --git a/lib/pact_broker/api/decorators/version_decorator.rb b/lib/pact_broker/api/decorators/version_decorator.rb index 2c7585739..875d95884 100644 --- a/lib/pact_broker/api/decorators/version_decorator.rb +++ b/lib/pact_broker/api/decorators/version_decorator.rb @@ -21,7 +21,8 @@ class VersionDecorator < BaseDecorator { title: "Version", name: represented.number, - href: version_url(options.fetch(:base_url), represented) + # This decorator is used for multiple Version resources, so dynamically fetch the current resource URL + href: options.fetch(:resource_url) } end diff --git a/lib/pact_broker/api/decorators/versions_decorator.rb b/lib/pact_broker/api/decorators/versions_decorator.rb index 5637f7b50..3cb016e61 100644 --- a/lib/pact_broker/api/decorators/versions_decorator.rb +++ b/lib/pact_broker/api/decorators/versions_decorator.rb @@ -6,13 +6,23 @@ module PactBroker module Api module Decorators class VersionsDecorator < BaseDecorator + class VersionInCollectionDecorator < PactBroker::Api::Decorators::VersionDecorator + # VersionDecorator has a dynamic self URL, depending which path the Version resource is mounted at. + # Hardcode the URL of the embedded Versions in this collection to use the canonical URL with the version number. + link :self do | user_options | + { + title: "Version", + name: represented.number, + href: version_url(user_options.fetch(:base_url), represented) + } + end + end - collection :entries, as: :versions, embedded: true, :extend => PactBroker::Api::Decorators::VersionDecorator + collection :entries, as: :versions, embedded: true, :extend => VersionInCollectionDecorator link :self do | user_options | - href = append_query_if_present(user_options[:resource_url], user_options[:query_string]) { - href: href, + href: user_options.fetch(:request_url), title: user_options[:resource_title] || "All application versions of #{user_options[:pacticipant_name]}" } end diff --git a/lib/pact_broker/api/pact_broker_urls.rb b/lib/pact_broker/api/pact_broker_urls.rb index 3ce937d9d..81aa06755 100644 --- a/lib/pact_broker/api/pact_broker_urls.rb +++ b/lib/pact_broker/api/pact_broker_urls.rb @@ -243,6 +243,10 @@ def branch_version_url(branch_version, base_url = "") "#{branch_versions_url(branch_version.branch, base_url)}/#{url_encode(branch_version.version_number)}" end + def latest_version_for_branch_url(branch, base_url = "") + "#{branch_url(branch, base_url)}/latest-version" + end + def templated_tag_url_for_pacticipant pacticipant_name, base_url = "" pacticipant_url_from_params({ pacticipant_name: pacticipant_name }, base_url) + "/versions/{version}/tags/{tag}" end diff --git a/lib/pact_broker/api/resources/latest_version.rb b/lib/pact_broker/api/resources/latest_version.rb index bac4ab7a2..430410fd8 100644 --- a/lib/pact_broker/api/resources/latest_version.rb +++ b/lib/pact_broker/api/resources/latest_version.rb @@ -17,6 +17,8 @@ def allowed_methods def version if identifier_from_path[:tag] @version ||= version_service.find_by_pacticipant_name_and_latest_tag(identifier_from_path[:pacticipant_name], identifier_from_path[:tag]) + elsif identifier_from_path[:branch_name] + @version ||= version_service.find_latest_by_pacticipant_name_and_branch_name(identifier_from_path[:pacticipant_name], identifier_from_path[:branch_name]) else @version ||= version_service.find_latest_by_pacticpant_name(identifier_from_path) end diff --git a/spec/features/get_latest_version_for_branch_spec.rb b/spec/features/get_latest_version_for_branch_spec.rb new file mode 100644 index 000000000..64bdefd52 --- /dev/null +++ b/spec/features/get_latest_version_for_branch_spec.rb @@ -0,0 +1,18 @@ +describe "Get latest version for branch" do + before do + td.create_consumer("Foo") + .create_consumer_version("1", branch: "main") + .create_consumer_version("2", branch: "main") + .create_consumer_version("3", branch: "not-main") + end + let(:path) { PactBroker::Api::PactBrokerUrls.latest_version_for_branch_url(PactBroker::Versions::Branch.order(:id).first) } + let(:rack_env) { { "CONTENT_TYPE" => "application/json" } } + + subject { get(path, {}, rack_env) } + + it { is_expected.to be_a_hal_json_success_response } + + it "returns the latest version for the branch" do + expect(JSON.parse(subject.body)["number"]).to eq "2" + end +end diff --git a/spec/fixtures/approvals/pacticipant_branches_decorator.approved.json b/spec/fixtures/approvals/pacticipant_branches_decorator.approved.json index 6f719defe..4a3204790 100644 --- a/spec/fixtures/approvals/pacticipant_branches_decorator.approved.json +++ b/spec/fixtures/approvals/pacticipant_branches_decorator.approved.json @@ -11,7 +11,7 @@ }, "pb:latest-version": { "title": "Latest version for branch", - "href": "http://example.org/pacticipants/Foo/branches/main/versions?size=1" + "href": "http://example.org/pacticipants/Foo/branches/main/latest-version" } } } diff --git a/spec/lib/pact_broker/api/decorators/version_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/version_decorator_spec.rb index 22c00e4d3..98b5297a9 100644 --- a/spec/lib/pact_broker/api/decorators/version_decorator_spec.rb +++ b/spec/lib/pact_broker/api/decorators/version_decorator_spec.rb @@ -48,13 +48,13 @@ module Decorators end let(:base_url) { "http://example.org" } - let(:options) { { user_options: { base_url: base_url, environments: environments } } } + let(:options) { { user_options: { base_url: base_url, resource_url: "resource_url", environments: environments } } } let(:decorator) { VersionDecorator.new(version) } subject { JSON.parse(decorator.to_json(options), symbolize_names: true) } it "includes a link to itself" do - expect(subject[:_links][:self][:href]).to eq "http://example.org/pacticipants/Consumer/versions/1.2.3" + expect(subject[:_links][:self][:href]).to eq "resource_url" end it "includes the version number in the link" do @@ -125,7 +125,7 @@ module Decorators end context "when the environments option is not present" do - let(:options) { { user_options: { base_url: base_url } } } + let(:options) { { user_options: { base_url: base_url, resource_url: "resource_url" } } } it "does not include the pb:record-deployment or pb:record-release" do expect(subject[:_links]).to_not have_key(:'pb:record-deployment') diff --git a/spec/lib/pact_broker/api/decorators/versions_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/versions_decorator_spec.rb index e4cd7c58b..867b0f20a 100644 --- a/spec/lib/pact_broker/api/decorators/versions_decorator_spec.rb +++ b/spec/lib/pact_broker/api/decorators/versions_decorator_spec.rb @@ -5,21 +5,18 @@ module PactBroker module Api module Decorators describe VersionsDecorator do + before do + allow_any_instance_of(VersionsDecorator::VersionInCollectionDecorator).to receive(:version_url).and_return("version_url") + end - let(:options) { { resource_url: "http://versions", base_url: "http://example.org", pacticipant_name: "Consumer", query_string: query_string}} - let(:query_string) { nil } + let(:options) { { request_url: "http://versions?foo=bar", base_url: "http://example.org", pacticipant_name: "Consumer", resource_url: "http://versions" } } let(:versions) { [] } + let(:decorator) { VersionsDecorator.new(versions) } + let(:json) { decorator.to_json(user_options: options) } - subject { JSON.parse VersionsDecorator.new(versions).to_json(user_options: options), symbolize_names: true } - - context "with no query string" do - its([:_links, :self, :href]) { is_expected.to eq "http://versions" } - end + subject { JSON.parse(json, symbolize_names: true) } - context "with a query string" do - let(:query_string) { "foo=bar" } - its([:_links, :self, :href]) { is_expected.to eq "http://versions?foo=bar" } - end + its([:_links, :self, :href]) { is_expected.to eq "http://versions?foo=bar" } context "with no versions" do it "doesn't blow up" do @@ -41,6 +38,10 @@ module Decorators expect(subject[:_embedded][:versions]).to be_instance_of(Array) expect(subject[:_embedded][:versions].size).to eq 1 end + + it "has the version href with the version number" do + expect(subject[:_embedded][:versions].first[:_links][:self][:href]).to eq "version_url" + end end end end From 89271f1ea61ff741bd42157730a553f035b07206 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Fri, 8 Dec 2023 10:44:00 +1100 Subject: [PATCH 15/69] chore: copy back changes from pactflow --- lib/pact_broker/api/decorators/extended_pact_decorator.rb | 7 ++++++- lib/pact_broker/api/pact_broker_urls.rb | 2 +- lib/pact_broker/api/resources/base_resource.rb | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/pact_broker/api/decorators/extended_pact_decorator.rb b/lib/pact_broker/api/decorators/extended_pact_decorator.rb index aa5568c7b..23daac697 100644 --- a/lib/pact_broker/api/decorators/extended_pact_decorator.rb +++ b/lib/pact_broker/api/decorators/extended_pact_decorator.rb @@ -1,5 +1,10 @@ require "pact_broker/api/decorators/pact_decorator" +# Pactflow notes: +# This decorator was added for Pactflow, but we needed to change it so much that there +# is a separate class in pact_broker_fork/lib/pactflow/api/decorators/extended_pact_decorator.rb now +# and this one isn't used. + module PactBroker module Api module Decorators @@ -47,4 +52,4 @@ def head_tags end end end -end \ No newline at end of file +end diff --git a/lib/pact_broker/api/pact_broker_urls.rb b/lib/pact_broker/api/pact_broker_urls.rb index 81aa06755..c6bd1d72b 100644 --- a/lib/pact_broker/api/pact_broker_urls.rb +++ b/lib/pact_broker/api/pact_broker_urls.rb @@ -287,7 +287,7 @@ def webhooks_url base_url "#{base_url}/webhooks" end - def webhook_url uuid, base_url + def webhook_url uuid, base_url = "" "#{base_url}/webhooks/#{uuid}" end diff --git a/lib/pact_broker/api/resources/base_resource.rb b/lib/pact_broker/api/resources/base_resource.rb index 1b4f5da4c..c91468496 100644 --- a/lib/pact_broker/api/resources/base_resource.rb +++ b/lib/pact_broker/api/resources/base_resource.rb @@ -207,7 +207,7 @@ def invalid_json? def find_pacticipant name, role pacticipant_service.find_pacticipant_by_name(name).tap do | pacticipant | if pacticipant.nil? - set_json_error_message("No #{role} with name '#{name}' found", title: "Not found", type: "not_found", status: 404) + set_json_error_message("No #{role} with name '#{name}' found", title: "Not found", type: "not-found", status: 404) end end end From 14ac33c8afe25a17ab6e299524ad412eda0fb8ba Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Mon, 11 Dec 2023 12:05:28 +1100 Subject: [PATCH 16/69] feat: bulk delete branches (#652) --- .../api/decorators/notices_decorator.rb | 11 ++++ lib/pact_broker/api/resources/after_reply.rb | 15 +++++ .../api/resources/pacticipant_branches.rb | 20 +++++- lib/pact_broker/async/after_reply.rb | 30 +++++++++ lib/pact_broker/locale/en.yml | 2 + lib/pact_broker/versions/branch_repository.rb | 33 ++++++++++ lib/pact_broker/versions/branch_service.rb | 15 ++++- .../delete_pacticipant_branches_spec.rb | 35 ++++++++++ .../versions/branch_repository_spec.rb | 64 +++++++++++++++++++ .../versions/branch_service_spec.rb | 22 +++++++ .../pact_broker/middleware/mock_puma.rb | 26 ++++++++ spec/support/shared_context_for_app.rb | 1 + 12 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 lib/pact_broker/api/decorators/notices_decorator.rb create mode 100644 lib/pact_broker/api/resources/after_reply.rb create mode 100644 lib/pact_broker/async/after_reply.rb create mode 100644 spec/features/delete_pacticipant_branches_spec.rb create mode 100644 spec/support/pact_broker/middleware/mock_puma.rb diff --git a/lib/pact_broker/api/decorators/notices_decorator.rb b/lib/pact_broker/api/decorators/notices_decorator.rb new file mode 100644 index 000000000..fe46659de --- /dev/null +++ b/lib/pact_broker/api/decorators/notices_decorator.rb @@ -0,0 +1,11 @@ +require_relative "base_decorator" + +module PactBroker + module Api + module Decorators + class NoticesDecorator < BaseDecorator + property :entries, as: :notices, getter: ->(represented:, **) { represented.collect(&:to_h) } + end + end + end +end diff --git a/lib/pact_broker/api/resources/after_reply.rb b/lib/pact_broker/api/resources/after_reply.rb new file mode 100644 index 000000000..cbf7da715 --- /dev/null +++ b/lib/pact_broker/api/resources/after_reply.rb @@ -0,0 +1,15 @@ +require "pact_broker/async/after_reply" + +module PactBroker + module Api + module Resources + module AfterReply + + # @param [Callable] block the block to execute after the response has been sent to the user. + def after_reply(&block) + PactBroker::Async::AfterReply.new(request.env).execute(&block) + end + end + end + end +end diff --git a/lib/pact_broker/api/resources/pacticipant_branches.rb b/lib/pact_broker/api/resources/pacticipant_branches.rb index f97da58a2..e30181b45 100644 --- a/lib/pact_broker/api/resources/pacticipant_branches.rb +++ b/lib/pact_broker/api/resources/pacticipant_branches.rb @@ -1,6 +1,8 @@ require "pact_broker/api/resources/base_resource" require "pact_broker/api/resources/pagination_methods" require "pact_broker/api/resources/filter_methods" +require "pact_broker/api/resources/after_reply" +require "rack/utils" module PactBroker module Api @@ -8,13 +10,14 @@ module Resources class PacticipantBranches < BaseResource include PaginationMethods include FilterMethods + include AfterReply def content_types_provided [["application/hal+json", :to_json]] end def allowed_methods - ["GET", "OPTIONS"] + ["GET", "DELETE", "OPTIONS"] end def resource_exists? @@ -29,6 +32,17 @@ def policy_name :'versions::branches' end + # Allows bulk deletion of pacticipant branches, keeping the specified branches and the main branch. + # Deletes the branches asyncronously, after the response has been sent, for performance reasons. + def delete_resource + after_reply do + branch_service.delete_branches_for_pacticipant(pacticipant, exclude: exclude) + end + notices = branch_service.branch_deletion_notices(pacticipant, exclude: exclude) + response.body = decorator_class(:notices_decorator).new(notices).to_json(**decorator_options) + 202 + end + private def branches @@ -40,6 +54,10 @@ def branches ) end + def exclude + Rack::Utils.parse_nested_query(request.uri.query)["exclude"] || [] + end + def eager_load_associations decorator_class(:pacticipant_branches_decorator).eager_load_associations end diff --git a/lib/pact_broker/async/after_reply.rb b/lib/pact_broker/async/after_reply.rb new file mode 100644 index 000000000..f25676fef --- /dev/null +++ b/lib/pact_broker/async/after_reply.rb @@ -0,0 +1,30 @@ +# Saves a block for execution after the HTTP response has been sent to the user. +# When the block is executed, it connects to the database before executing the code. +# This is good for doing things that might take a while and don't have to be done before +# the response is sent, and don't need retries (in which case, it might be better to use a SuckerPunch Job). +# +# This leverages a feature of Puma which I'm not sure is meant to be public or not. +# There are serveral mentions of it on the internet, so I assume it's ok to use it. +# Puma itself uses the rack.after_reply for http request logging. +# +# https://github.com/search?q=repo%3Apuma%2Fpuma%20rack.after_reply&type=code + +module PactBroker + module Async + class AfterReply + def initialize(rack_env) + @rack_env = rack_env + @database_connector = rack_env.fetch("pactbroker.database_connector") + end + + def execute(&block) + dc = @database_connector + @rack_env["rack.after_reply"] << lambda { + dc.call do + block.call + end + } + end + end + end +end diff --git a/lib/pact_broker/locale/en.yml b/lib/pact_broker/locale/en.yml index f3489fc79..d3b149aa2 100644 --- a/lib/pact_broker/locale/en.yml +++ b/lib/pact_broker/locale/en.yml @@ -59,6 +59,8 @@ en: verifications: Add Pact verification tests to the %{provider_name} build. See https://docs.pact.io/go/provider_verification webhooks: Configure separate %{provider_name} pact verification build and webhook to trigger it when the pact content changes. See https://docs.pact.io/go/webhooks version_branch: Configure the version branch to be the value of your repository branch. + branch: + bulk_delete: "Scheduled deletion of %{count} branches for pacticipant %{pacticipant_name}. Remaining branches are: %{remaining}" errors: runtime: with_error_reference: "An error has occurred. The details have been logged with the reference %{error_reference}" diff --git a/lib/pact_broker/versions/branch_repository.rb b/lib/pact_broker/versions/branch_repository.rb index 7e41b00f5..35f69e198 100644 --- a/lib/pact_broker/versions/branch_repository.rb +++ b/lib/pact_broker/versions/branch_repository.rb @@ -38,6 +38,39 @@ def find_branch(pacticipant_name:, branch_name:) def delete_branch(branch) branch.delete end + + # @param [PactBroker::Domain::Pacticipant] pacticipant + # @params [Array] exclude the names of the branches to NOT delete + # @param [Integer] the number of branches that will be deleted + def count_branches_to_delete(pacticipant, exclude: ) + build_query_for_pacticipant_branches(pacticipant, exclude: exclude).count + end + + # Returns the list of branches which will NOT be deleted (the bulk delete is executed async after the request has finished) + # @param [PactBroker::Domain::Pacticipant] pacticipant + # @params [Array] exclude the names of the branches to NOT delete + # @return [Array] + def remaining_branches_after_future_deletion(pacticipant, exclude: ) + exclude_dup = exclude.dup + if pacticipant.main_branch + exclude_dup << pacticipant.main_branch + end + Branch.where(pacticipant_id: pacticipant.id).where(name: exclude_dup) + end + + # @param [PactBroker::Domain::Pacticipant] pacticipant + # @params [Array] exclude the names of the branches to NOT delete + def delete_branches_for_pacticipant(pacticipant, exclude:) + build_query_for_pacticipant_branches(pacticipant, exclude: exclude).delete + end + + def build_query_for_pacticipant_branches(pacticipant, exclude: ) + exclude_dup = exclude.dup + if pacticipant.main_branch + exclude_dup << pacticipant.main_branch + end + Branch.where(pacticipant_id: pacticipant.id).exclude(name: exclude_dup) + end end end end diff --git a/lib/pact_broker/versions/branch_service.rb b/lib/pact_broker/versions/branch_service.rb index 85e616121..fd8ede8d2 100644 --- a/lib/pact_broker/versions/branch_service.rb +++ b/lib/pact_broker/versions/branch_service.rb @@ -1,17 +1,28 @@ +require "forwardable" require "pact_broker/logging" require "pact_broker/repositories" require "pact_broker/messages" -require "forwardable" module PactBroker module Versions class BranchService extend PactBroker::Repositories + extend PactBroker::Messages class << self extend Forwardable delegate [:find_branch_version, :find_or_create_branch_version, :delete_branch_version] => :branch_version_repository - delegate [:find_branch, :delete_branch, :find_all_branches_for_pacticipant] => :branch_repository + delegate [:find_branch, :delete_branch, :find_all_branches_for_pacticipant, :delete_branches_for_pacticipant] => :branch_repository + + # Returns a list of notices to display to the user in the terminal + # @param [PactBroker::Domain::Pacticipant] pacticipant + # @param [Array] exclude the list of branches to NOT delete + # @return [Array] + def branch_deletion_notices(pacticipant, exclude:) + count = branch_repository.count_branches_to_delete(pacticipant, exclude: exclude) + remaining = branch_repository.remaining_branches_after_future_deletion(pacticipant, exclude: exclude).sort_by(&:created_at).collect(&:name).join(", ") + [PactBroker::Contracts::Notice.success(message("messages.branch.bulk_delete", count: count, pacticipant_name: pacticipant.name, remaining: remaining))] + end end end end diff --git a/spec/features/delete_pacticipant_branches_spec.rb b/spec/features/delete_pacticipant_branches_spec.rb new file mode 100644 index 000000000..7930c6274 --- /dev/null +++ b/spec/features/delete_pacticipant_branches_spec.rb @@ -0,0 +1,35 @@ +describe "Delete pacticipant branches" do + before do + td.create_consumer("Bar") + .create_consumer_version("1", branch: "main") + .create_consumer("Foo", main_branch: "main") + .create_consumer_version("1", branch: "main") + .create_consumer_version("2", branch: "feat/bar") + .create_consumer_version("3", branch: "feat/foo") + end + let(:path) { PactBroker::Api::PactBrokerUrls.pacticipant_branches_url(td.and_return(:pacticipant)) } + let(:rack_env) do + { + "pactbroker.database_connector" => lambda { |&block| block.call } + } + end + + subject { delete(path + "?exclude[]=feat%2Fbar", nil, rack_env) } + + its(:status) { is_expected.to eq 202 } + + it "returns a list of notices to be displayed to the user" do + expect(JSON.parse(subject.body)["notices"]).to be_instance_of(Array) + expect(JSON.parse(subject.body)["notices"]).to include(hash_including("text")) + end + + it "after the request, it deletes all except the excluded branches for a pacticipant" do + expect { subject }.to change { + PactBroker::Versions::Branch + .where(pacticipant_id: td.and_return(:pacticipant).id) + .all + .collect(&:name) + .sort + }.from(["feat/bar", "feat/foo", "main"]).to(["feat/bar", "main"]) + end +end diff --git a/spec/lib/pact_broker/versions/branch_repository_spec.rb b/spec/lib/pact_broker/versions/branch_repository_spec.rb index 68ab71d95..e7a2fd44b 100644 --- a/spec/lib/pact_broker/versions/branch_repository_spec.rb +++ b/spec/lib/pact_broker/versions/branch_repository_spec.rb @@ -92,6 +92,70 @@ module Versions expect{ subject }.to_not change { PactBroker::Domain::Version.count } end end + + describe "count_branches_to_delete" do + before do + td.create_consumer("foo") + .create_consumer_version("1", branch: "main") + .create_consumer_version("3", branch: "not-main") + .create_consumer_version("4", branch: "foo") + .create_consumer_version("5", branch: "bar") + .create_consumer_version("6", branch: "not-bar") + .create_consumer("bar") + .create_consumer_version("1", branch: "main") + end + + let(:pacticipant) { td.find_pacticipant("foo") } + + subject { BranchRepository.new.count_branches_to_delete(pacticipant, exclude: ["foo"]) } + + it "returns a count of the number of branches that will be deleted" do + expect(subject).to eq 3 + end + end + + describe "delete_branches_for_pacticipant" do + before do + td.create_consumer("foo") + .create_consumer_version("1", branch: "main") + .create_consumer_version("3", branch: "not-main") + .create_consumer_version("4", branch: "foo") + .create_consumer_version("5", branch: "bar") + .create_consumer_version("6", branch: "not-bar") + .create_consumer("bar") + .create_consumer_version("1", branch: "main") + end + + let(:pacticipant) { td.find_pacticipant("foo") } + + subject { BranchRepository.new.delete_branches_for_pacticipant(pacticipant, exclude: ["foo"]) } + + it "deletes all the branches except for the excluded ones and the main branch" do + subject + expect(Branch.where(pacticipant: pacticipant).collect(&:name)).to contain_exactly("foo", "main") + end + end + + describe "remaining_branches_after_future_deletion" do + before do + td.create_consumer("foo") + .create_consumer_version("1", branch: "main") + .create_consumer_version("3", branch: "not-main") + .create_consumer_version("4", branch: "foo") + .create_consumer_version("5", branch: "bar") + .create_consumer_version("6", branch: "not-bar") + .create_consumer("bar") + .create_consumer_version("1", branch: "main") + end + + let(:pacticipant) { td.find_pacticipant("foo") } + + subject { BranchRepository.new.remaining_branches_after_future_deletion(pacticipant, exclude: ["foo"]) } + + it "returns the branches that will not be deleted" do + expect(subject).to contain_exactly(have_attributes(name: "main"), have_attributes(name: "foo")) + end + end end end end diff --git a/spec/lib/pact_broker/versions/branch_service_spec.rb b/spec/lib/pact_broker/versions/branch_service_spec.rb index e3176e822..3ba433114 100644 --- a/spec/lib/pact_broker/versions/branch_service_spec.rb +++ b/spec/lib/pact_broker/versions/branch_service_spec.rb @@ -91,6 +91,28 @@ module Versions end end end + + describe "#branch_deletion_notices" do + let(:pacticipant) { instance_double(PactBroker::Domain::Pacticipant, name: "some-service") } + let(:exclude) { ["foo", "bar" ] } + let(:branch_repository) { instance_double(PactBroker::Versions::BranchRepository, count_branches_to_delete: 3, remaining_branches_after_future_deletion: remaining_branches) } + let(:remaining_branches) do + [ + instance_double(PactBroker::Versions::Branch, name: "foo", created_at: DateTime.now - 10), + instance_double(PactBroker::Versions::Branch, name: "bar", created_at: DateTime.now - 20) + ] + end + + before do + allow(BranchService).to receive(:branch_repository).and_return(branch_repository) + end + + subject { BranchService.branch_deletion_notices(pacticipant, exclude: exclude) } + + it "returns a list of notices" do + expect(subject).to contain_exactly(have_attributes(text: "Scheduled deletion of 3 branches for pacticipant some-service. Remaining branches are: bar, foo")) + end + end end end end diff --git a/spec/support/pact_broker/middleware/mock_puma.rb b/spec/support/pact_broker/middleware/mock_puma.rb new file mode 100644 index 000000000..a94eadea0 --- /dev/null +++ b/spec/support/pact_broker/middleware/mock_puma.rb @@ -0,0 +1,26 @@ +# Mock out the rack.after_reply functionality provided by Puma +# I'm not sure if this is meant to be a public feature or not, but +# there are several mentions of it on the net, so I assume it's ok to use it. +# Puma itself uses the rack.after_reply for http request logging. +# +# See https://github.com/puma/puma/search?q=rack.after_reply +# This middleware executes the hooks that would normally run after the request +# *before* the request ends, for the purposes of testing. + +module PactBroker + module Middleware + class MockPuma + + def initialize(app) + @app = app + end + + def call(env) + after_reply = [] + response = @app.call({ "rack.after_reply" => after_reply }.merge(env)) + after_reply.each(&:call) + response + end + end + end +end diff --git a/spec/support/shared_context_for_app.rb b/spec/support/shared_context_for_app.rb index 1de3af135..975319e9a 100644 --- a/spec/support/shared_context_for_app.rb +++ b/spec/support/shared_context_for_app.rb @@ -43,6 +43,7 @@ builder.use OpenapiFirst::PactBrokerCoverage, endpoints_to_be_called end + builder.use(PactBroker::Middleware::MockPuma) builder.use(Rack::PactBroker::ApplicationContext, application_context) builder.run(PactBroker.build_api(application_context)) builder.to_app From 321a2291c725d8597cba17afdc76d8aef26540ba Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Thu, 14 Dec 2023 14:42:28 +1100 Subject: [PATCH 17/69] fix: fix performance issues due to contention in the integrations table when publishing a large number of contracts (> 20) per request, in parallel (#654) PACT-1352 --- lib/pact_broker/api.rb | 1 - .../api/resources/event_methods.rb | 15 +++++++ lib/pact_broker/api/resources/pact.rb | 17 +++++--- .../api/resources/verifications.rb | 13 ++++-- lib/pact_broker/contracts/service.rb | 5 +++ lib/pact_broker/initializers/subscriptions.rb | 4 -- lib/pact_broker/integrations/repository.rb | 13 +++++- lib/pact_broker/integrations/service.rb | 7 +++ .../integrations/repository_spec.rb | 43 ++++++++++++++++++- 9 files changed, 101 insertions(+), 17 deletions(-) create mode 100644 lib/pact_broker/api/resources/event_methods.rb delete mode 100644 lib/pact_broker/initializers/subscriptions.rb diff --git a/lib/pact_broker/api.rb b/lib/pact_broker/api.rb index 4e7cefa00..70c1346af 100644 --- a/lib/pact_broker/api.rb +++ b/lib/pact_broker/api.rb @@ -5,7 +5,6 @@ require "pact_broker/api/contracts" require "pact_broker/application_context" require "pact_broker/feature_toggle" -require "pact_broker/initializers/subscriptions" module Webmachine class Request diff --git a/lib/pact_broker/api/resources/event_methods.rb b/lib/pact_broker/api/resources/event_methods.rb new file mode 100644 index 000000000..0dcbe3a0b --- /dev/null +++ b/lib/pact_broker/api/resources/event_methods.rb @@ -0,0 +1,15 @@ +require "pact_broker/events/subscriber" + +module PactBroker + module Api + module Resources + module EventMethods + def subscribe(listener) + PactBroker::Events.subscribe(listener) do + yield + end + end + end + end + end +end diff --git a/lib/pact_broker/api/resources/pact.rb b/lib/pact_broker/api/resources/pact.rb index 441ed1496..226ec827d 100644 --- a/lib/pact_broker/api/resources/pact.rb +++ b/lib/pact_broker/api/resources/pact.rb @@ -9,14 +9,17 @@ require "pact_broker/webhooks/execution_configuration" require "pact_broker/api/resources/webhook_execution_methods" require "pact_broker/api/resources/pact_resource_methods" +require "pact_broker/api/resources/event_methods" +require "pact_broker/integrations/event_listener" module PactBroker module Api module Resources class Pact < BaseResource + include EventMethods include PacticipantResourceMethods - include WebhookExecutionMethods include PactResourceMethods + include WebhookExecutionMethods include PactBroker::Messages def content_types_provided @@ -65,11 +68,13 @@ def resource_exists? def from_json response_code = pact ? 200 : 201 - handle_webhook_events do - if request.patch? && resource_exists? - @pact = pact_service.merge_pact(pact_params) - else - @pact = pact_service.create_or_update_pact(pact_params) + subscribe(PactBroker::Integrations::EventListener.new) do + handle_webhook_events do + if request.patch? && resource_exists? + @pact = pact_service.merge_pact(pact_params) + else + @pact = pact_service.create_or_update_pact(pact_params) + end end end response.body = to_json diff --git a/lib/pact_broker/api/resources/verifications.rb b/lib/pact_broker/api/resources/verifications.rb index ac743214f..a6f8ff27d 100644 --- a/lib/pact_broker/api/resources/verifications.rb +++ b/lib/pact_broker/api/resources/verifications.rb @@ -5,6 +5,8 @@ require "pact_broker/api/decorators/verification_decorator" require "pact_broker/api/resources/webhook_execution_methods" require "pact_broker/api/resources/metadata_resource_methods" +require "pact_broker/api/resources/event_methods" +require "pact_broker/integrations/event_listener" module PactBroker module Api @@ -12,6 +14,7 @@ module Resources class Verifications < BaseResource include WebhookExecutionMethods include MetadataResourceMethods + include EventMethods def content_types_accepted [["application/json", :from_json]] @@ -42,10 +45,12 @@ def create_path end def from_json - handle_webhook_events(build_url: verification_params["buildUrl"]) do - verified_pacts = pact_service.find_for_verification_publication(pact_params, event_context[:consumer_version_selectors]) - verification = verification_service.create(next_verification_number, verification_params, verified_pacts, event_context) - response.body = decorator_for(verification).to_json(**decorator_options) + subscribe(PactBroker::Integrations::EventListener.new) do + handle_webhook_events(build_url: verification_params["buildUrl"]) do + verified_pacts = pact_service.find_for_verification_publication(pact_params, event_context[:consumer_version_selectors]) + verification = verification_service.create(next_verification_number, verification_params, verified_pacts, event_context) + response.body = decorator_for(verification).to_json(**decorator_options) + end end true end diff --git a/lib/pact_broker/contracts/service.rb b/lib/pact_broker/contracts/service.rb index e13bff71c..dd6bd1c52 100644 --- a/lib/pact_broker/contracts/service.rb +++ b/lib/pact_broker/contracts/service.rb @@ -35,6 +35,7 @@ def publish(parsed_contracts, base_url: ) version, version_notices = create_version(parsed_contracts) tags = create_tags(parsed_contracts, version) pacts, pact_notices = create_pacts(parsed_contracts, base_url) + update_integrations(pacts) notices = version_notices + pact_notices ContractsPublicationResults.from_hash( pacticipant: version.pacticipant, @@ -303,6 +304,10 @@ def url_for_triggered_webhook(triggered_webhook, base_url) PactBroker::Api::PactBrokerUrls.triggered_webhook_logs_url(triggered_webhook, base_url) end + def update_integrations(pacts) + integration_service.handle_bulk_contract_data_published(pacts) + end + private :url_for_triggered_webhook end end diff --git a/lib/pact_broker/initializers/subscriptions.rb b/lib/pact_broker/initializers/subscriptions.rb deleted file mode 100644 index 7ac2bc32d..000000000 --- a/lib/pact_broker/initializers/subscriptions.rb +++ /dev/null @@ -1,4 +0,0 @@ -require "pact_broker/events/subscriber" -require "pact_broker/integrations/event_listener" - -PactBroker::Events.subscribe(PactBroker::Integrations::EventListener.new) diff --git a/lib/pact_broker/integrations/repository.rb b/lib/pact_broker/integrations/repository.rb index e790a32da..73f6912b4 100644 --- a/lib/pact_broker/integrations/repository.rb +++ b/lib/pact_broker/integrations/repository.rb @@ -21,7 +21,8 @@ def create_for_pact(consumer_id, provider_id) Integration.new( consumer_id: consumer_id, provider_id: provider_id, - created_at: Sequel.datetime_class.now + created_at: Sequel.datetime_class.now, + contract_data_updated_at: Sequel.datetime_class.now ).insert_ignore end nil @@ -39,6 +40,16 @@ def set_contract_data_updated_at(consumer, provider) .where({ consumer_id: consumer&.id, provider_id: provider.id }.compact ) .update(contract_data_updated_at: Sequel.datetime_class.now) end + + + # Sets the contract_data_updated_at for the integrations as specified by an array of objects which each have a consumer and provider + # @param [Array] where each object has a consumer and a provider + def set_contract_data_updated_at_for_multiple_integrations(objects_with_consumer_and_provider) + consumer_and_provider_ids = objects_with_consumer_and_provider.collect{ | object | [object.consumer.id, object.provider.id] }.uniq + Integration + .where([:consumer_id, :provider_id] => consumer_and_provider_ids) + .update(contract_data_updated_at: Sequel.datetime_class.now) + end end end end diff --git a/lib/pact_broker/integrations/service.rb b/lib/pact_broker/integrations/service.rb index 22a3c8e16..d291180cf 100644 --- a/lib/pact_broker/integrations/service.rb +++ b/lib/pact_broker/integrations/service.rb @@ -24,6 +24,13 @@ def self.handle_contract_data_published(consumer, provider) integration_repository.set_contract_data_updated_at(consumer, provider) end + + # Callback to invoke when a batch of contract data is published (eg. the publish contracts endpoint) + # @param [Array] where each object has a consumer and a provider + def self.handle_bulk_contract_data_published(objects_with_consumer_and_provider) + integration_repository.set_contract_data_updated_at_for_multiple_integrations(objects_with_consumer_and_provider) + end + def self.delete(consumer_name, provider_name) consumer = pacticipant_service.find_pacticipant_by_name!(consumer_name) provider = pacticipant_service.find_pacticipant_by_name!(provider_name) diff --git a/spec/lib/pact_broker/integrations/repository_spec.rb b/spec/lib/pact_broker/integrations/repository_spec.rb index a37d5f98d..243be5fbf 100644 --- a/spec/lib/pact_broker/integrations/repository_spec.rb +++ b/spec/lib/pact_broker/integrations/repository_spec.rb @@ -22,10 +22,11 @@ module Integrations td.create_verification(provider_version: "2") end - # No contract data date + # Nil contract data date td.create_consumer("Dog") .create_provider("Cat") .create_integration + Integration.order(:id).last.update(contract_data_updated_at: nil) end subject { Repository.new.find } @@ -93,6 +94,46 @@ module Integrations end end end + + describe "#set_contract_data_updated_at_for_multiple_integrations" do + before do + Timecop.freeze(date_1) do + td.create_consumer("Foo1") + .create_provider("Bar1") + .create_integration + .create_consumer("Foo2") + .create_provider("Bar2") + .create_integration + .create_consumer("Foo3") + .create_provider("Bar3") + .create_integration + end + end + + let(:date_1) { Time.new(2023, 1, 1).utc.to_datetime } + let(:date_2) { Time.new(2023, 1, 2).utc.to_datetime } + + let(:objects_with_consumer_and_provider) do + [ + OpenStruct.new(consumer: td.find_pacticipant("Foo1"), provider: td.find_pacticipant("Bar1")), + OpenStruct.new(consumer: td.find_pacticipant("Foo2"), provider: td.find_pacticipant("Bar2")) + ] + end + + subject do + Timecop.freeze(date_2) do + Repository.new.set_contract_data_updated_at_for_multiple_integrations(objects_with_consumer_and_provider) + end + end + + it "sets the contract_data_updated_at of the specified integrations" do + subject + integrations = Integration.order(:id).all + expect(integrations[0].contract_data_updated_at).to be_date_time(date_2) + expect(integrations[1].contract_data_updated_at).to be_date_time(date_2) + expect(integrations[2].contract_data_updated_at).to be_date_time(date_1) + end + end end end end From e2b2fcf2feb848e6e1ad9931032e75ea08790a93 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Fri, 15 Dec 2023 13:55:17 +1100 Subject: [PATCH 18/69] chore: move no-cache header after static assets middleware --- lib/pact_broker/app.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pact_broker/app.rb b/lib/pact_broker/app.rb index c67d9948e..d85c46fb8 100644 --- a/lib/pact_broker/app.rb +++ b/lib/pact_broker/app.rb @@ -185,10 +185,10 @@ def configure_middleware @app_builder.use Rack::PactBroker::InvalidUriProtection @app_builder.use Rack::PactBroker::ResetThreadData @app_builder.use Rack::PactBroker::AddPactBrokerVersionHeader - @app_builder.use Rack::PactBroker::AddCacheHeader @app_builder.use Rack::PactBroker::AddVaryHeader @app_builder.use Rack::Static, :urls => ["/stylesheets", "/css", "/fonts", "/js", "/javascripts", "/images"], :root => PactBroker.project_root.join("public") @app_builder.use Rack::Static, :urls => ["/favicon.ico"], :root => PactBroker.project_root.join("public/images"), header_rules: [[:all, {"Content-Type" => "image/x-icon"}]] + @app_builder.use Rack::PactBroker::AddCacheHeader @app_builder.use Rack::PactBroker::ConvertFileExtensionToAcceptHeader # Rack::PactBroker::SetBaseUrl needs to be before the Rack::PactBroker::HalBrowserRedirect @app_builder.use Rack::PactBroker::SetBaseUrl, configuration.base_urls From 9830afc53ddc1b2fa1c7ff455d005d3b5800f564 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Fri, 5 Jan 2024 11:31:26 +1100 Subject: [PATCH 19/69] chore: correct pacticipant name in pact conflict message --- lib/pact_broker/contracts/service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pact_broker/contracts/service.rb b/lib/pact_broker/contracts/service.rb index dd6bd1c52..6847419aa 100644 --- a/lib/pact_broker/contracts/service.rb +++ b/lib/pact_broker/contracts/service.rb @@ -67,7 +67,7 @@ def add_pact_conflict_notices(notices, parsed_contracts) def add_pact_conflict_notice(notices, parsed_contracts, contract_to_publish, existing_json_content, new_json_content) message_params = { - consumer_name: contract_to_publish.provider_name, + consumer_name: contract_to_publish.consumer_name, consumer_version_number: parsed_contracts.pacticipant_version_number, provider_name: contract_to_publish.provider_name } From f76b99351004be8ad7f92e0878cf792185a601d8 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Fri, 5 Jan 2024 14:29:37 +1100 Subject: [PATCH 20/69] fix(metrics): correct the query for pactRevisionsPerConsumerVersion PACT-1606 --- lib/pact_broker/metrics/service.rb | 4 ++-- spec/lib/pact_broker/metrics/service_spec.rb | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/pact_broker/metrics/service.rb b/lib/pact_broker/metrics/service.rb index 0e70ffff7..c0c94461b 100644 --- a/lib/pact_broker/metrics/service.rb +++ b/lib/pact_broker/metrics/service.rb @@ -101,8 +101,8 @@ def interactions_counts end def pact_revision_counts - query = "select revision_count as number_of_revisions, count(consumer_version_id) as consumer_version_count - from (select consumer_version_id, count(*) as revision_count from pact_publications group by consumer_version_id) foo + query = "select revision_count as number_of_revisions, count(*) as consumer_version_count + from (select count(*) as revision_count from pact_publications group by consumer_version_id, provider_id) foo group by revision_count order by 1" PactBroker::Pacts::PactPublication.db[query].all.each_with_object({}) { |row, hash| hash[row[:number_of_revisions]] = row[:consumer_version_count] } diff --git a/spec/lib/pact_broker/metrics/service_spec.rb b/spec/lib/pact_broker/metrics/service_spec.rb index 7fa07f43f..0bdac2994 100644 --- a/spec/lib/pact_broker/metrics/service_spec.rb +++ b/spec/lib/pact_broker/metrics/service_spec.rb @@ -121,7 +121,7 @@ module Service describe "pactRevisionsPerPactPublication" do before do - td.create_pact_with_hierarchy + td.create_pact_with_hierarchy("Foo", "1", "Bar") .comment("this consumer version will have 3 revisions") .revise_pact .revise_pact @@ -129,12 +129,15 @@ module Service .create_pact .comment("this consumer version will have 1 revision") .revise_pact + .create_pact_with_hierarchy("Foo", "1", "Bar2", td.random_json_content("Foo", "Bar2")) + .create_pact_with_hierarchy("Foo", "2", "Bar", td.random_json_content("Foo", "Bar")) end let(:distribution) { subject[:pactRevisionsPerConsumerVersion][:distribution] } it "returns a distribution of pact revisions per consumer version" do - expect(distribution).to eq(2 => 1, 3 => 1) + # numberOfRevisions => countOfPactsWithThisNumberOfRevisions + expect(distribution).to eq({ 1 => 2, 2 => 1, 3 => 1 }) end end end From b13f2c37ca1721c1c6a54205e062f5d50072de3a Mon Sep 17 00:00:00 2001 From: bethesque Date: Fri, 5 Jan 2024 03:51:41 +0000 Subject: [PATCH 21/69] chore(release): version 2.108.0 --- CHANGELOG.md | 55 ++++++++++++++++++++++++++++++++++++++ lib/pact_broker/version.rb | 2 +- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b1e07079..f3ca1436a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,58 @@ + +### v2.108.0 (2024-01-05) + +#### Features + +* bulk delete branches (#652) ([14ac33c8](/../../commit/14ac33c8)) +* add latest version for branch endpoint (#644) ([c216bec8](/../../commit/c216bec8)) +* add no-cache header ([9a637327](/../../commit/9a637327)) +* suppport `page` + `size` as pagination params (#642) ([c71089fe](/../../commit/c71089fe)) +* do not include pb:record-deployment or pb:record-release relations for versions embedded in resources ([2f43590c](/../../commit/2f43590c)) +* remove status from individual error in problem+error response ([a4b3ec58](/../../commit/a4b3ec58)) +* add version_id indexes to deployed_versions and released_versions ([00fc7d10](/../../commit/00fc7d10)) +* add endpoint to list branches for a pacticipant (#638) ([ff7e3a53](/../../commit/ff7e3a53)) +* stop running tests for ruby 2.7 ([034aba3b](/../../commit/034aba3b)) +* update sinatra and rack-protection to ~> 3.0 ([92ebbdd3](/../../commit/92ebbdd3)) +* add branch endpoint supporting GET and DELETE (#635) ([1bb6088d](/../../commit/1bb6088d)) +* optimise matrix by applying specified limit to pact publications before joining to verifications ([c61c324e](/../../commit/c61c324e)) +* optimise matrix query when selectors with pacticipant names only are used ([b98f5d1a](/../../commit/b98f5d1a)) +* include environment name in pact metadata ([e120c4e7](/../../commit/e120c4e7)) +* improve wording of 'no version exits' messaging in can-i-deploy response ([9529c679](/../../commit/9529c679)) +* improve performance of matrix when multiple selectors are specified (#631) ([58a28604](/../../commit/58a28604)) +* add pagination parameter validation for paginated endpoints. (#626) ([abb0a1c6](/../../commit/abb0a1c6)) +* add endpoint to list pacticipant versions by branch ([9b4e3f61](/../../commit/9b4e3f61)) +* add endpoint to return latest pact for consumer, provider and consumer branch ([f77086ef](/../../commit/f77086ef)) +* update required ruby version from 2.2 to 2.7 ([f1b1e906](/../../commit/f1b1e906)) +* add pagination and filtering for integrations endpoint ([68d7cf30](/../../commit/68d7cf30)) +* add contract_data_updated_at to integrations table to speed up dashboard query (#617) ([e43c10f2](/../../commit/e43c10f2)) +* support setting feature toggles via individual environment variables (#609) ([be7d9d52](/../../commit/be7d9d52)) + +* **metrics** + * hardcode matrix count to -1 as calculating it causes performance issues and it has no meaning ([62e121b8](/../../commit/62e121b8)) + +* **matrix** + * optimise identification of the 'latest tag' ([824c516a](/../../commit/824c516a)) + +#### Bug Fixes + +* **metrics** + * correct the query for pactRevisionsPerConsumerVersion ([f76b9935](/../../commit/f76b9935)) + +* fix performance issues due to contention in the integrations table when publishing a large number of contracts (> 20) per request, in parallel (#654) ([321a2291](/../../commit/321a2291)) +* raise 404 on paths with missing path segments (#648) ([930b45cd](/../../commit/930b45cd)) +* do not error when no environment is found by name ([d1501618](/../../commit/d1501618)) +* ensure pact associations are eager loaded when finding a single pact ([c98abda6](/../../commit/c98abda6)) +* gracefully handle validating an array when a hash is expected ([b26ddb46](/../../commit/b26ddb46)) +* fix error occuring when can-i-deploy badge is requested and no version is found ([db7dee3a](/../../commit/db7dee3a)) +* fix bug in error handling for 'can-i-deploy branch to environment' badge ([c23beb6b](/../../commit/c23beb6b)) +* improve performance of network diagram (#614) ([ffd3ec4b](/../../commit/ffd3ec4b)) +* fix error raised when attempting to log warning when webhook_redact_sensitive_data is set to false ([9b66270e](/../../commit/9b66270e)) +* gracefully handle execution of webhooks that are deleted between execution attempts (#613) ([1127b41f](/../../commit/1127b41f)) +* add extra validation to ensure parsed content is a hash when publishing pacts ([913e0a52](/../../commit/913e0a52)) + +* **matrix** + * return only most recent row missing verification when latestby=cp ([b7550e53](/../../commit/b7550e53)) + ### v2.107.1 (2023-05-02) diff --git a/lib/pact_broker/version.rb b/lib/pact_broker/version.rb index 090c220d1..3480711c2 100644 --- a/lib/pact_broker/version.rb +++ b/lib/pact_broker/version.rb @@ -1,3 +1,3 @@ module PactBroker - VERSION = "2.107.1" + VERSION = "2.108.0" end From fb38b856122842c75fafbab941b6d6ccf4d88d89 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 17 Jan 2024 08:29:35 +1100 Subject: [PATCH 22/69] chore: make webhook uuid not writeable in decorator --- lib/pact_broker/api/decorators/webhook_decorator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pact_broker/api/decorators/webhook_decorator.rb b/lib/pact_broker/api/decorators/webhook_decorator.rb index aad276dac..348b70931 100644 --- a/lib/pact_broker/api/decorators/webhook_decorator.rb +++ b/lib/pact_broker/api/decorators/webhook_decorator.rb @@ -16,7 +16,7 @@ class WebhookEventDecorator < BaseDecorator property :name end - property :uuid + property :uuid, writeable: false property :description, getter: lambda { |represented:, **| represented.display_description } From 29c9031994c3e037c7cae667a03df9f06cef80a0 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 17 Jan 2024 11:10:20 +1100 Subject: [PATCH 23/69] chore: measure the time it takes to generate the pact content sha --- lib/pact_broker/logging.rb | 10 ++++++++++ lib/pact_broker/pacts/generate_sha.rb | 13 +++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/pact_broker/logging.rb b/lib/pact_broker/logging.rb index 5e2df2779..3ff159a3c 100644 --- a/lib/pact_broker/logging.rb +++ b/lib/pact_broker/logging.rb @@ -42,6 +42,16 @@ def log_with_tag(tag) end end + def measure_info(message, payload: {}) + if logger.respond_to?(:measure_info) + logger.measure_info(message, payload: payload) do + yield + end + else + yield + end + end + def log_error e, description = nil if logger.instance_of?(SemanticLogger::Logger) if description diff --git a/lib/pact_broker/pacts/generate_sha.rb b/lib/pact_broker/pacts/generate_sha.rb index 65e89c92b..4719763cd 100644 --- a/lib/pact_broker/pacts/generate_sha.rb +++ b/lib/pact_broker/pacts/generate_sha.rb @@ -3,22 +3,27 @@ require "pact_broker/pacts/sort_content" require "pact_broker/pacts/parse" require "pact_broker/pacts/content" +require "pact_broker/logging" module PactBroker module Pacts class GenerateSha + include PactBroker::Logging + # @param [String] json_content - def self.call json_content, _options = {} + def self.call(json_content, _options = {}) content_for_sha = if PactBroker.configuration.base_equality_only_on_content_that_affects_verification_results extract_verifiable_content_for_sha(json_content) else json_content end - Digest::SHA1.hexdigest(content_for_sha) + measure_info("Generating SHA1 hexdigest for pact", payload: { length: content_for_sha.length } ){ Digest::SHA1.hexdigest(content_for_sha) } end - def self.extract_verifiable_content_for_sha json_content - Content.from_json(json_content).sort.content_that_affects_verification_results.to_json + def self.extract_verifiable_content_for_sha(json_content) + objects = Content.from_json(json_content) + sorted_content = measure_info("Sorting content", payload: { length: json_content.length }){ objects.sort } + sorted_content.content_that_affects_verification_results.to_json end end end From a947e40926acc9cb29ddef04ff1e28169e695f78 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 17 Jan 2024 11:23:55 +1100 Subject: [PATCH 24/69] feat: improve performance of publication for very large pacts by calculating the content SHA only once per request --- .../decorators/publish_contract_decorator.rb | 7 +++++- lib/pact_broker/api/resources/pact.rb | 10 +++++--- .../api/resources/publish_contracts.rb | 2 +- .../contracts/contract_to_publish.rb | 9 ++++---- lib/pact_broker/contracts/service.rb | 4 ++-- lib/pact_broker/pacts/pact_params.rb | 2 +- lib/pact_broker/pacts/service.rb | 23 +++++++++---------- lib/pact_broker/test/test_data_builder.rb | 8 ++++++- .../lib/pact_broker/contracts/service_spec.rb | 9 +++++--- spec/lib/pact_broker/pacts/service_spec.rb | 15 ++---------- 10 files changed, 47 insertions(+), 42 deletions(-) diff --git a/lib/pact_broker/api/decorators/publish_contract_decorator.rb b/lib/pact_broker/api/decorators/publish_contract_decorator.rb index 48e3c40f7..ec336372f 100644 --- a/lib/pact_broker/api/decorators/publish_contract_decorator.rb +++ b/lib/pact_broker/api/decorators/publish_contract_decorator.rb @@ -11,8 +11,13 @@ class PublishContractDecorator < BaseDecorator property :provider_name property :specification property :content_type - property :decoded_content + property :decoded_content, setter: -> (fragment:, represented:, user_options:, **) { + represented.decoded_content = fragment + # Set the pact version sha when we set the content + represented.pact_version_sha = user_options.fetch(:sha_generator).call(fragment) + } property :on_conflict, default: "overwrite" + end end end diff --git a/lib/pact_broker/api/resources/pact.rb b/lib/pact_broker/api/resources/pact.rb index 226ec827d..a27c9a4d6 100644 --- a/lib/pact_broker/api/resources/pact.rb +++ b/lib/pact_broker/api/resources/pact.rb @@ -71,9 +71,9 @@ def from_json subscribe(PactBroker::Integrations::EventListener.new) do handle_webhook_events do if request.patch? && resource_exists? - @pact = pact_service.merge_pact(pact_params) + @pact = pact_service.merge_pact(pact_params.merge(pact_version_sha: pact_version_sha)) else - @pact = pact_service.create_or_update_pact(pact_params) + @pact = pact_service.create_or_update_pact(pact_params.merge(pact_version_sha: pact_version_sha)) end end end @@ -114,7 +114,7 @@ def pact end def disallowed_modification? - if request.really_put? && pact_service.disallowed_modification?(pact, pact_params.json_content) + if request.really_put? && pact_service.disallowed_modification?(pact, pact_version_sha) message_params = { consumer_name: pact_params.consumer_name, consumer_version_number: pact_params.consumer_version_number, provider_name: pact_params.provider_name } set_json_error_message(message("errors.validation.pact_content_modification_not_allowed", message_params)) true @@ -126,6 +126,10 @@ def disallowed_modification? def schema api_contract_class(:put_pact_params_contract) end + + def pact_version_sha + @pact_version_sha ||= pact_service.generate_sha(pact_params.json_content) + end end end end diff --git a/lib/pact_broker/api/resources/publish_contracts.rb b/lib/pact_broker/api/resources/publish_contracts.rb index 45b921735..c7e598c6e 100644 --- a/lib/pact_broker/api/resources/publish_contracts.rb +++ b/lib/pact_broker/api/resources/publish_contracts.rb @@ -53,7 +53,7 @@ def policy_record_context private def parsed_contracts - @parsed_contracts ||= decorator_class(:publish_contracts_decorator).new(PactBroker::Contracts::ContractsToPublish.new).from_hash(params) + @parsed_contracts ||= decorator_class(:publish_contracts_decorator).new(PactBroker::Contracts::ContractsToPublish.new).from_hash(params, { user_options: { sha_generator: PactBroker.configuration.sha_generator } } ) end def params diff --git a/lib/pact_broker/contracts/contract_to_publish.rb b/lib/pact_broker/contracts/contract_to_publish.rb index 96a7ad5a5..a710925cf 100644 --- a/lib/pact_broker/contracts/contract_to_publish.rb +++ b/lib/pact_broker/contracts/contract_to_publish.rb @@ -1,11 +1,10 @@ module PactBroker module Contracts - ContractToPublish = Struct.new(:consumer_name, :provider_name, :decoded_content, :content_type, :specification, :on_conflict) do - # rubocop: disable Metrics/ParameterLists - def self.from_hash(consumer_name: nil, provider_name: nil, decoded_content: nil, content_type: nil, specification: nil, on_conflict: nil) - new(consumer_name, provider_name, decoded_content, content_type, specification, on_conflict) + ContractToPublish = Struct.new(:consumer_name, :provider_name, :decoded_content, :content_type, :specification, :on_conflict, :pact_version_sha, keyword_init: true) do + + def self.from_hash(hash) + new(**hash) end - # rubocop: enable Metrics/ParameterLists def pact? specification == "pact" diff --git a/lib/pact_broker/contracts/service.rb b/lib/pact_broker/contracts/service.rb index 6847419aa..7776bcecb 100644 --- a/lib/pact_broker/contracts/service.rb +++ b/lib/pact_broker/contracts/service.rb @@ -57,7 +57,7 @@ def add_pact_conflict_notices(notices, parsed_contracts) parsed_contracts.contracts.collect do | contract_to_publish | pact_params = create_pact_params(parsed_contracts, contract_to_publish) existing_pact = pact_service.find_pact(pact_params) - if existing_pact && pact_service.disallowed_modification?(existing_pact, contract_to_publish.decoded_content) + if existing_pact && pact_service.disallowed_modification?(existing_pact, contract_to_publish.pact_version_sha) add_pact_conflict_notice(notices, parsed_contracts, contract_to_publish, existing_pact.json_content, contract_to_publish.decoded_content) end end @@ -134,7 +134,7 @@ def create_pacts(parsed_contracts, base_url) pact_params = create_pact_params(parsed_contracts, contract_to_publish) existing_pact = pact_service.find_pact(pact_params) listener = TriggeredWebhooksCreatedListener.new - created_pact = create_or_merge_pact(contract_to_publish.merge?, existing_pact, pact_params, listener) + created_pact = create_or_merge_pact(contract_to_publish.merge?, existing_pact, pact_params.merge(pact_version_sha: contract_to_publish.pact_version_sha), listener) notices.concat(notices_for_pact(parsed_contracts, contract_to_publish, existing_pact, created_pact, listener, base_url)) created_pact end diff --git a/lib/pact_broker/pacts/pact_params.rb b/lib/pact_broker/pacts/pact_params.rb index e7f75723c..c6d9cb733 100644 --- a/lib/pact_broker/pacts/pact_params.rb +++ b/lib/pact_broker/pacts/pact_params.rb @@ -20,7 +20,7 @@ def self.from_path_info path_info ) end - def self.from_request request, path_info + def self.from_request(request, path_info) json_content = request.body.to_s parsed_content = begin parsed = JSON.parse(json_content, PACT_PARSING_OPTIONS) diff --git a/lib/pact_broker/pacts/service.rb b/lib/pact_broker/pacts/service.rb index fb59bd7ad..6ec1adacb 100644 --- a/lib/pact_broker/pacts/service.rb +++ b/lib/pact_broker/pacts/service.rb @@ -22,6 +22,10 @@ module Service extend PactBroker::Messages extend SquashPactsForVerification + def generate_sha(json_content) + PactBroker.configuration.sha_generator.call(json_content) + end + def find_latest_pact params pact_repository.find_latest_pact(params[:consumer_name], params[:provider_name], params[:tag], params[:branch_name]) end @@ -154,10 +158,12 @@ def find_for_verification_publication(pact_params, consumer_version_selector_has end # Overwriting an existing pact with the same consumer/provider/consumer version number + # by creating a new revision (that is, a new PactPublication with an incremented revision number) + # Modifing pacts is strongly discouraged now, and support for it will be dropped in the next major version of the Pact Broker def create_pact_revision params, existing_pact logger.info("Updating existing pact publication", params.without(:json_content)) logger.debug("Content #{params[:json_content]}") - pact_version_sha = generate_sha(params[:json_content]) + pact_version_sha = params.fetch(:pact_version_sha) json_content = add_interaction_ids(params[:json_content]) update_params = { pact_version_sha: pact_version_sha, json_content: json_content } updated_pact = pact_repository.update(existing_pact.id, update_params) @@ -178,21 +184,20 @@ def create_pact_revision params, existing_pact private :create_pact_revision - def disallowed_modification?(existing_pact, new_json_content) - !PactBroker.configuration.allow_dangerous_contract_modification && existing_pact && existing_pact.pact_version_sha != generate_sha(new_json_content) + def disallowed_modification?(existing_pact, new_pact_version_sha) + !PactBroker.configuration.allow_dangerous_contract_modification && existing_pact && existing_pact.pact_version_sha != new_pact_version_sha end # When no publication for the given consumer/provider/consumer version number exists - def create_pact params, version, provider + def create_pact(params, version, provider) logger.info("Creating new pact publication", params.without(:json_content)) logger.debug("Content #{params[:json_content]}") - pact_version_sha = generate_sha(params[:json_content]) json_content = add_interaction_ids(params[:json_content]) pact = pact_repository.create( version_id: version.id, provider_id: provider.id, consumer_id: version.pacticipant_id, - pact_version_sha: pact_version_sha, + pact_version_sha: params.fetch(:pact_version_sha), json_content: json_content, version: version ) @@ -212,12 +217,6 @@ def create_pact params, version, provider private :create_pact - def generate_sha(json_content) - PactBroker.configuration.sha_generator.call(json_content) - end - - private :generate_sha - def add_interaction_ids(json_content) Content.from_json(json_content).with_ids.to_json end diff --git a/lib/pact_broker/test/test_data_builder.rb b/lib/pact_broker/test/test_data_builder.rb index 2cdd4d2b2..92d6bbcbc 100644 --- a/lib/pact_broker/test/test_data_builder.rb +++ b/lib/pact_broker/test/test_data_builder.rb @@ -256,7 +256,13 @@ def create_label label_name def publish_pact(consumer_name:, provider_name:, consumer_version_number: , tags: nil, branch: nil, build_url: nil, json_content: nil) json_content = json_content || random_json_content(consumer_name, provider_name) contracts = [ - PactBroker::Contracts::ContractToPublish.from_hash(consumer_name: consumer_name, provider_name: provider_name, decoded_content: json_content, content_type: "application/json", specification: "pact") + PactBroker::Contracts::ContractToPublish.from_hash( + consumer_name: consumer_name, + provider_name: provider_name, + decoded_content: json_content, + content_type: "application/json", + specification: "pact", + pact_version_sha: PactBroker::Pacts::GenerateSha.call(json_content)) ] contracts_to_publish = PactBroker::Contracts::ContractsToPublish.from_hash( pacticipant_name: consumer_name, diff --git a/spec/lib/pact_broker/contracts/service_spec.rb b/spec/lib/pact_broker/contracts/service_spec.rb index fafa7891f..349fa38f7 100644 --- a/spec/lib/pact_broker/contracts/service_spec.rb +++ b/spec/lib/pact_broker/contracts/service_spec.rb @@ -18,10 +18,11 @@ module Contracts let(:branch) { "main" } let(:contracts) { [contract_1] } let(:contract_1) do - ContractToPublish.from_hash( + ContractToPublish.new( consumer_name: "Foo", provider_name: "Bar", decoded_content: decoded_contract, + pact_version_sha: PactBroker::Pacts::GenerateSha.call(decoded_contract), specification: "pact", on_conflict: on_conflict ) @@ -149,17 +150,19 @@ module Contracts let(:branch) { "main" } let(:contracts) { [contract_1] } let(:contract_1) do - ContractToPublish.from_hash( + ContractToPublish.new( consumer_name: "Foo", provider_name: "Bar", decoded_content: decoded_contract, specification: "pact", - on_conflict: on_conflict + on_conflict: on_conflict, + pact_version_sha: new_pact_version_sha ) end let(:contract_hash) { { consumer: { name: "Foo" }, provider: { name: "Bar" }, interactions: [{a: "b"}] } } let(:decoded_contract) { contract_hash.to_json } + let(:new_pact_version_sha) { PactBroker::Pacts::GenerateSha.call(decoded_contract) } subject { Service.conflict_notices(contracts_to_publish, base_url: "base_url") } diff --git a/spec/lib/pact_broker/pacts/service_spec.rb b/spec/lib/pact_broker/pacts/service_spec.rb index a656a710c..3aa0a156c 100644 --- a/spec/lib/pact_broker/pacts/service_spec.rb +++ b/spec/lib/pact_broker/pacts/service_spec.rb @@ -31,7 +31,8 @@ module Pacts consumer_name: "Foo", provider_name: "Bar", consumer_version_number: "1", - json_content: json_content + json_content: json_content, + pact_version_sha: PactBroker::Pacts::GenerateSha.call(json_content) } end let(:content) { double("content") } @@ -48,12 +49,6 @@ module Pacts subject { Service.create_or_update_pact(params) } context "when no pact exists with the same params" do - it "creates the sha before adding the interaction ids" do - expect(PactBroker::Pacts::GenerateSha).to receive(:call).ordered - expect(content).to receive(:with_ids).ordered - subject - end - it "saves the pact interactions/messages with ids added to them" do expect(pact_repository).to receive(:create).with hash_including(json_content: json_content_with_ids) subject @@ -135,12 +130,6 @@ module Pacts let(:expected_event_context) { { consumer_version_tags: ["dev"] } } - it "creates the sha before adding the interaction ids" do - expect(PactBroker::Pacts::GenerateSha).to receive(:call).ordered - expect(content).to receive(:with_ids).ordered - subject - end - it "saves the pact interactions/messages with ids added to them" do expect(pact_repository).to receive(:update).with(anything, hash_including(json_content: json_content_with_ids)) subject From 474df31a1d70b944db9cf68eb1a7722bafceec07 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 17 Jan 2024 11:34:11 +1100 Subject: [PATCH 25/69] test: update migration spec --- spec/migrations/23_pact_versions_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/migrations/23_pact_versions_spec.rb b/spec/migrations/23_pact_versions_spec.rb index 3ecbaa892..f2e4b9701 100644 --- a/spec/migrations/23_pact_versions_spec.rb +++ b/spec/migrations/23_pact_versions_spec.rb @@ -65,12 +65,14 @@ it "allows a new pact to be inserted with no duplicate ID error" do subject + json_content = load_fixture("a_consumer-a_provider.json") PactBroker::Pacts::Service.create_or_update_pact( { consumer_name: consumer[:name], provider_name: provider[:name], consumer_version_number: "1.2.3", - json_content: load_fixture("a_consumer-a_provider.json") + json_content: json_content, + pact_version_sha: PactBroker::Pacts::GenerateSha.call(json_content) } ) end From 9aaa3484ad5d7ecbeb0789b45241d03872b940fe Mon Sep 17 00:00:00 2001 From: Matt Fellows <53900+mefellows@users.noreply.github.com> Date: Wed, 17 Jan 2024 11:40:18 +1100 Subject: [PATCH 26/69] fix: Dockerfile to reduce vulnerabilities (#650) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-ALPINE317-NCURSES-5606599 - https://snyk.io/vuln/SNYK-ALPINE317-NCURSES-5606599 - https://snyk.io/vuln/SNYK-ALPINE317-OPENSSL-6032385 - https://snyk.io/vuln/SNYK-ALPINE317-OPENSSL-6032385 - https://snyk.io/vuln/SNYK-ALPINE317-OPENSSL-6095780 Co-authored-by: snyk-bot --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d30b4ad1f..9314d9183 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:3.2.1-alpine3.17 +FROM ruby:alpine3.17 WORKDIR /home From 80a48bc14aaccfb73e6db141d2c095140dc56816 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 17 Jan 2024 11:40:54 +1100 Subject: [PATCH 27/69] Revert "[Snyk] Security upgrade ruby from 3.2.1-alpine3.17 to alpine3.17" (#660) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9314d9183..d30b4ad1f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:alpine3.17 +FROM ruby:3.2.1-alpine3.17 WORKDIR /home From a2aacd89b10c1f50f8fcad0761836f98f5e14ec2 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 17 Jan 2024 08:29:35 +1100 Subject: [PATCH 28/69] chore: make webhook uuid not writeable in decorator --- lib/pact_broker/api/decorators/webhook_decorator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pact_broker/api/decorators/webhook_decorator.rb b/lib/pact_broker/api/decorators/webhook_decorator.rb index aad276dac..348b70931 100644 --- a/lib/pact_broker/api/decorators/webhook_decorator.rb +++ b/lib/pact_broker/api/decorators/webhook_decorator.rb @@ -16,7 +16,7 @@ class WebhookEventDecorator < BaseDecorator property :name end - property :uuid + property :uuid, writeable: false property :description, getter: lambda { |represented:, **| represented.display_description } From b950e5063da270e92e649157bda092579b0eb7f2 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Thu, 18 Jan 2024 12:49:57 +1100 Subject: [PATCH 29/69] chore: print requests and responses to stdout when DEBUG=true --- spec/spec_helper.rb | 5 ----- spec/support/logging.rb | 26 +++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 46c06871a..40e22bce2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -54,8 +54,3 @@ config.filter_run_excluding skip: true config.include PactBroker::RackHelpers end - -if ENV["DEBUG"] == "true" - SemanticLogger.default_level = :info - SemanticLogger.add_appender(io: $stdout) -end diff --git a/spec/support/logging.rb b/spec/support/logging.rb index 38551529d..0522591e1 100644 --- a/spec/support/logging.rb +++ b/spec/support/logging.rb @@ -3,4 +3,28 @@ FileUtils.mkdir_p("log") SemanticLogger.default_level = :error -SemanticLogger.add_appender(file_name: "log/test.log", formatter: PactBroker::Logging::DefaultFormatter.new) + +if ENV["DEBUG"] == "true" + SemanticLogger.default_level = :info + SemanticLogger.add_appender(io: $stdout) +end + +# Print out the request and response when DEBUG=true +RSpec.configure do | config | + config.after(:each) do | example | + if ENV["DEBUG"] == "true" && defined?(last_response) + last_request.env["rack.input"]&.rewind + puts "------------------------------------------------------------" + puts "Request: #{last_request.request_method} #{last_request.path}\n\n" + puts "Rack env:\n#{last_request.env}\n\n" + puts "Request body:\n#{last_request.env["rack.input"]&.read}" + + puts "\n\n" + puts "Response status: #{last_response.status}\n\n" + puts "Response headers: #{last_response.headers}\n\n" + puts "Response body:\n#{last_response.body}" + puts "------------------------------------------------------------" + puts "" + end + end +end From 5d9d70022f9e6f86c3747f7a2662e1498176319c Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Tue, 23 Jan 2024 14:03:00 +1100 Subject: [PATCH 30/69] feat: use SemanticLogger for Padrino (#662) --- lib/pact_broker/app.rb | 5 ---- lib/pact_broker/ui.rb | 29 +++++++++++++------ .../ui/controllers/base_controller.rb | 6 ++-- .../pact_broker/ui/controllers/index_spec.rb | 6 ++-- 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/lib/pact_broker/app.rb b/lib/pact_broker/app.rb index d85c46fb8..341635492 100644 --- a/lib/pact_broker/app.rb +++ b/lib/pact_broker/app.rb @@ -1,8 +1,3 @@ -# Must be defined before loading Padrino -PADRINO_LOGGER ||= { - ENV.fetch("RACK_ENV", "production").to_sym => { log_level: :error, stream: :stdout, format_datetime: "%Y-%m-%dT%H:%M:%S.000%:z" } -} - require "pact_broker/configuration" require "pact_broker/db" require "pact_broker/initializers/database_connection" diff --git a/lib/pact_broker/ui.rb b/lib/pact_broker/ui.rb index 7c005008a..101af7e15 100644 --- a/lib/pact_broker/ui.rb +++ b/lib/pact_broker/ui.rb @@ -1,14 +1,25 @@ -require "pact_broker/configuration" +# Must be defined before loading Padrino # Stop Padrino creating a log file, as it will try to create it in the gems directory # http://www.padrinorb.com/api/Padrino/Logger.html -unless defined? PADRINO_LOGGER - log_path = File.join(PactBroker.configuration.log_dir, "ui.log") - PADRINO_LOGGER = { - production: { log_level: :error, stream: :to_file, log_path: log_path }, - staging: { log_level: :error, stream: :to_file, log_path: log_path }, - test: { log_level: :warn, stream: :to_file, log_path: log_path }, - development: { log_level: :warn, stream: :to_file, log_path: log_path } - } +# This configuration will be replaced by the SemanticLogger later on. +PADRINO_LOGGER ||= { + ENV.fetch("RACK_ENV", "production").to_sym => { stream: :stdout } +} + +require "padrino-core" + +class PactBrokerPadrinoLogger < SemanticLogger::Logger + include Padrino::Logger::Extensions + + # Padrino expects level to return an integer, not a symbol + def level + Padrino::Logger::Levels[SemanticLogger.default_level] + end end +Padrino.logger = PactBrokerPadrinoLogger.new("Padrino") +# Log a test message to ensure that the logger works properly, as it only +# seems to be used in production. +Padrino.logger.info("Padrino has been configured with SemanticLogger") + require "pact_broker/ui/app" diff --git a/lib/pact_broker/ui/controllers/base_controller.rb b/lib/pact_broker/ui/controllers/base_controller.rb index 6125e0bfa..d3657a5ce 100644 --- a/lib/pact_broker/ui/controllers/base_controller.rb +++ b/lib/pact_broker/ui/controllers/base_controller.rb @@ -10,8 +10,10 @@ class Base < Padrino::Application using PactBroker::StringRefinements set :root, File.join(File.dirname(__FILE__), "..") - set :show_exceptions, ENV["RACK_ENV"] != "production" - set :dump_errors, false # The padrino logger logs these for us. If this is enabled we get duplicate logging. + set :show_exceptions, ENV["RACK_ENV"] == "development" + # The padrino logger logs these for us, but only in production. If this is enabled we get duplicate logging. + set :dump_errors, ENV["RACK_ENV"] != "production" + set :raise_errors, ENV["RACK_ENV"] == "test" def base_url # Using the X-Forwarded headers in the UI can leave the app vulnerable diff --git a/spec/lib/pact_broker/ui/controllers/index_spec.rb b/spec/lib/pact_broker/ui/controllers/index_spec.rb index 83371fbaf..196d3b6c2 100644 --- a/spec/lib/pact_broker/ui/controllers/index_spec.rb +++ b/spec/lib/pact_broker/ui/controllers/index_spec.rb @@ -40,7 +40,7 @@ module Controllers context "when pagination parameters are present" do it "passes through pagination parameters to the search" do - expect(PactBroker::Index::Service).to receive(:find_index_items).with(hash_including(page_number: 2, page_size: 40)) + expect(PactBroker::Index::Service).to receive(:find_index_items).with(hash_including(page_number: 2, page_size: 40)).and_call_original get "/", { page: "2", pageSize: "40" } end end @@ -48,14 +48,14 @@ module Controllers context "when pagination parameters are not present" do context "when tags=true" do it "passes through default pagination parameters to the search with page_size=30" do - expect(PactBroker::Index::Service).to receive(:find_index_items).with(hash_including(page_number: 1, page_size: 30)) + expect(PactBroker::Index::Service).to receive(:find_index_items).with(hash_including(page_number: 1, page_size: 30)).and_call_original get "/", { tags: "true" } end end context "when not tags=true" do it "passes through default pagination parameters to the search with page_size=100" do - expect(PactBroker::Index::Service).to receive(:find_index_items).with(hash_including(page_number: 1, page_size: 100)) + expect(PactBroker::Index::Service).to receive(:find_index_items).with(hash_including(page_number: 1, page_size: 100)).and_call_original get "/" end end From 4f486ae7b0882f6050cc54497bccb0f8c0c531a1 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 24 Jan 2024 14:24:30 +1100 Subject: [PATCH 31/69] style: rubocop --- spec/support/logging.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/support/logging.rb b/spec/support/logging.rb index 0522591e1..b1a31ee49 100644 --- a/spec/support/logging.rb +++ b/spec/support/logging.rb @@ -11,7 +11,7 @@ # Print out the request and response when DEBUG=true RSpec.configure do | config | - config.after(:each) do | example | + config.after(:each) do if ENV["DEBUG"] == "true" && defined?(last_response) last_request.env["rack.input"]&.rewind puts "------------------------------------------------------------" From 5c386a436b91c4cc68281fcf5f7114c532b1e9d9 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Thu, 1 Feb 2024 16:51:49 +1100 Subject: [PATCH 32/69] fix: pass in environment to environment policy when getting an individual environment --- lib/pact_broker/api/resources/environment.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/pact_broker/api/resources/environment.rb b/lib/pact_broker/api/resources/environment.rb index 80b1d7707..5f92cd843 100644 --- a/lib/pact_broker/api/resources/environment.rb +++ b/lib/pact_broker/api/resources/environment.rb @@ -42,6 +42,10 @@ def policy_name :'deployments::environment' end + def policy_record + environment + end + def to_json decorator_class(:environment_decorator).new(environment).to_json(**decorator_options) end From d126419a269bd5a6c0b450ebacbbb56a9974bd2f Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Fri, 2 Feb 2024 20:11:28 +0000 Subject: [PATCH 33/69] chore(deps): update sucker_punch to 3.x post merge of https://github.com/brandonhilkert/sucker_punch/pull/253 --- Gemfile | 2 -- pact_broker.gemspec | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 5b0a2a94c..467bcc449 100644 --- a/Gemfile +++ b/Gemfile @@ -2,8 +2,6 @@ source "https://rubygems.org" gemspec -# While https://github.com/brandonhilkert/sucker_punch/pull/253 is outstanding -gem "sucker_punch", git: "https://github.com/pact-foundation/sucker_punch.git", ref: "fix/rename-is-singleton-class-method-2" gem "rake", "~>12.3.3" gem "sqlite3", "~>1.3" diff --git a/pact_broker.gemspec b/pact_broker.gemspec index 8221f7de1..bb28101bf 100644 --- a/pact_broker.gemspec +++ b/pact_broker.gemspec @@ -64,7 +64,7 @@ Gem::Specification.new do |gem| gem.add_runtime_dependency "padrino-core", ">= 0.14.3", "~> 0.14" gem.add_runtime_dependency "sinatra", "~> 3.0" gem.add_runtime_dependency "haml", "~>5.0" - gem.add_runtime_dependency "sucker_punch", "~>2.0" + gem.add_runtime_dependency "sucker_punch", "~>3.0" gem.add_runtime_dependency "rack-protection", "~> 3.0" gem.add_runtime_dependency "table_print", "~> 1.5" gem.add_runtime_dependency "semantic_logger", "~> 4.11" From 6a62441a5b5bca41aeee38e441434a939d9b36b8 Mon Sep 17 00:00:00 2001 From: YOU54F Date: Mon, 5 Feb 2024 17:36:50 +0000 Subject: [PATCH 34/69] chore(release): version 2.109.0 --- CHANGELOG.md | 13 +++++++++++++ lib/pact_broker/version.rb | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3ca1436a..438d2e2ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ + +### v2.109.0 (2024-02-01) + +#### Features + +* use SemanticLogger for Padrino (#662) ([5d9d7002](/../../commit/5d9d7002)) +* improve performance of publication for very large pacts by calculating the content SHA only once per request ([a947e409](/../../commit/a947e409)) + +#### Bug Fixes + +* pass in environment to environment policy when getting an individual environment ([5c386a43](/../../commit/5c386a43)) +* Dockerfile to reduce vulnerabilities (#650) ([9aaa3484](/../../commit/9aaa3484)) + ### v2.108.0 (2024-01-05) diff --git a/lib/pact_broker/version.rb b/lib/pact_broker/version.rb index 3480711c2..1b05011a1 100644 --- a/lib/pact_broker/version.rb +++ b/lib/pact_broker/version.rb @@ -1,3 +1,3 @@ module PactBroker - VERSION = "2.108.0" + VERSION = "2.109.0" end From 25406678725d6ac5f2b35ae7a815ce648e43e660 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Mon, 5 Feb 2024 17:37:23 +0000 Subject: [PATCH 35/69] Revert "chore(examples): pin suckerpunch path in example config.ru" This reverts commit f3a616771e5d7a7dd656d884b6deb80642e036bb. --- example/Gemfile | 1 - 1 file changed, 1 deletion(-) diff --git a/example/Gemfile b/example/Gemfile index 4d05a180a..3c9aa1199 100644 --- a/example/Gemfile +++ b/example/Gemfile @@ -4,4 +4,3 @@ gem "pact_broker" # Would pin this to a known minor version when using properly gem "puma", "~> 5.3" # Can be replaced with your choice of application server gem "sqlite3", "~>1.3" # Sqlite is just for testing. Replace this with "pg" for production. # gem "pg" # Production database gem -gem "sucker_punch", git: "https://github.com/pact-foundation/sucker_punch.git", ref: "fix/rename-is-singleton-class-method-2" From 4a895e58e42a135e5a88e355d31ca6262cdbf3ec Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 7 Feb 2024 11:49:18 +1100 Subject: [PATCH 36/69] chore: update link to CI test config file --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b6d61974a..cba54ee3e 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ You can use the [Pact Broker Docker image][docker] or [Terraform on AWS][terrafo * Are you sure you don't just want to use the [Pact Broker Docker image][docker]? No Docker at your company yet? Ah well, keep reading. * Create a PostgreSQL database. - * To ensure you're on a supported version of the database that you choose, check the [travis.yml][travisyml] file to see which versions we're currently running our tests against. + * To ensure you're on a supported version of the database that you choose, check the [.github/workflows/test.yml](.github/workflows/test.yml) file to see which versions we're currently running our tests against. * MySQL was supported for the native Ruby application until around 2021, but the official `pactfoundation/pact-broker` Docker image does not support it. New features will not be optimised for MySQL, and some new features may not even be supported on it (eg. the database clean feature). * You'll find a sample database creation script in the [example/config.ru](https://github.com/pact-foundation/pact_broker/blob/master/example/config.ru). * Install ruby 2.7 and the latest version of bundler (if you've come this far, I'm assuming you know how to do both of these. Did I mention there was a [Docker][docker] image?) From 7766eda65e3d2d0c132ca7bb07c38a883066519b Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 7 Feb 2024 11:50:01 +1100 Subject: [PATCH 37/69] chore: remove badge for hakari as it doesn't exist any more --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index cba54ee3e..baecc231d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![Gem Version](https://badge.fury.io/rb/pact_broker.svg)](http://badge.fury.io/rb/pact_broker) ![Build status](https://github.com/pact-foundation/pact_broker/workflows/Test/badge.svg) [![Join the chat at https://pact-foundation.slack.com/](https://img.shields.io/badge/chat-on%20slack-blue.svg?logo=slack)](https://slack.pact.io) - [![security](https://hakiri.io/github/pact-foundation/pact_broker/master.svg)](https://hakiri.io/github/pact-foundation/pact_broker/master) The Pact Broker is an application for sharing of consumer driven contracts and verification results. It is optimised for use with "pacts" (contracts created by the [Pact][pact-docs] framework), but can be used for any type of contract that can be serialized to JSON. From ae62ae7abfd4f6a6420d3f2b73617d98af6ec1ec Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Mon, 12 Feb 2024 13:18:45 +1100 Subject: [PATCH 38/69] fix: correct spelling in message when pact is modified --- lib/pact_broker/locale/en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pact_broker/locale/en.yml b/lib/pact_broker/locale/en.yml index d3b149aa2..5204919a8 100644 --- a/lib/pact_broker/locale/en.yml +++ b/lib/pact_broker/locale/en.yml @@ -50,7 +50,7 @@ en: contract: pact_published: Pact successfully published for %{consumer_name} version %{consumer_version_number} and provider %{provider_name}. same_pact_content_published: Pact successfully republished for %{consumer_name} version %{consumer_version_number} and provider %{provider_name} with no content changes. - pact_modified_for_same_version: Pact with changed content published over existing content for %{consumer_name} version %{consumer_version_number} and provider %{provider_name}. This is not recommended in normal cicumstances and may indicate that you have not configured your Pact pipeline correctly. For more information see https://docs.pact.io/go/versioning + pact_modified_for_same_version: Pact with changed content published over existing content for %{consumer_name} version %{consumer_version_number} and provider %{provider_name}. This is not recommended in normal circumstances and may indicate that you have not configured your Pact pipeline correctly. For more information see https://docs.pact.io/go/versioning pact_merged: Pact content merged with existing content for %{consumer_name} version %{consumer_version_number} and provider %{provider_name}. events: pact_published_unchanged_with_single_tag: pact content is the same as previous version with tag %{tag_name} and no new tags were applied From 37905808dc2df61ede1ca22491fe5d50d6bf7e70 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 21 Feb 2024 13:36:30 +1100 Subject: [PATCH 39/69] chore: add missing requires --- lib/pact_broker/pacts/pacts_for_verification_repository.rb | 1 + lib/pact_broker/versions/branch_service.rb | 1 + spec/lib/pact_broker/pacts/pact_params_spec.rb | 3 ++- .../pact_publication_clean_selector_dataset_module_spec.rb | 2 ++ 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/pact_broker/pacts/pacts_for_verification_repository.rb b/lib/pact_broker/pacts/pacts_for_verification_repository.rb index 8cd4190b5..2c5ed84db 100644 --- a/lib/pact_broker/pacts/pacts_for_verification_repository.rb +++ b/lib/pact_broker/pacts/pacts_for_verification_repository.rb @@ -7,6 +7,7 @@ require "pact_broker/pacts/selectors" require "pact_broker/feature_toggle" require "pact_broker/repositories/scopes" +require "pact_broker/matrix/unresolved_selector" module PactBroker module Pacts diff --git a/lib/pact_broker/versions/branch_service.rb b/lib/pact_broker/versions/branch_service.rb index fd8ede8d2..f787280da 100644 --- a/lib/pact_broker/versions/branch_service.rb +++ b/lib/pact_broker/versions/branch_service.rb @@ -2,6 +2,7 @@ require "pact_broker/logging" require "pact_broker/repositories" require "pact_broker/messages" +require "pact_broker/contracts/notice" module PactBroker module Versions diff --git a/spec/lib/pact_broker/pacts/pact_params_spec.rb b/spec/lib/pact_broker/pacts/pact_params_spec.rb index ca9990277..490745811 100644 --- a/spec/lib/pact_broker/pacts/pact_params_spec.rb +++ b/spec/lib/pact_broker/pacts/pact_params_spec.rb @@ -1,4 +1,5 @@ require "pact_broker/pacts/pact_params" +require "webmachine/request" module PactBroker module Pacts @@ -45,7 +46,7 @@ module Pacts describe "from_request" do context "from a PUT request" do - let(:request) { Webmachine::Request.new("PUT", "/", headers, body)} + let(:request) { ::Webmachine::Request.new("PUT", "/", headers, body)} subject { PactParams.from_request(request, path_info) } diff --git a/spec/lib/pact_broker/pacts/pact_publication_clean_selector_dataset_module_spec.rb b/spec/lib/pact_broker/pacts/pact_publication_clean_selector_dataset_module_spec.rb index 1c6dbf0a0..bfe251e0e 100644 --- a/spec/lib/pact_broker/pacts/pact_publication_clean_selector_dataset_module_spec.rb +++ b/spec/lib/pact_broker/pacts/pact_publication_clean_selector_dataset_module_spec.rb @@ -1,3 +1,5 @@ +require "pact_broker/db/clean/selector" + module PactBroker module Pacts describe PactPublicationCleanSelectorDatasetModule do From 9ef03f6c3c8d8e52d8de25914ec262996b90e083 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 21 Feb 2024 13:37:37 +1100 Subject: [PATCH 40/69] test: update approval --- ...publish_contract_verification_already_exists.approved.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/fixtures/approvals/publish_contract_verification_already_exists.approved.json b/spec/fixtures/approvals/publish_contract_verification_already_exists.approved.json index 62ebb0f77..8c3dfc3bc 100644 --- a/spec/fixtures/approvals/publish_contract_verification_already_exists.approved.json +++ b/spec/fixtures/approvals/publish_contract_verification_already_exists.approved.json @@ -40,7 +40,7 @@ }, { "level": "warning", - "message": "Pact with changed content published over existing content for Foo version 1 and provider Bar. This is not recommended in normal cicumstances and may indicate that you have not configured your Pact pipeline correctly. For more information see https://docs.pact.io/go/versioning", + "message": "Pact with changed content published over existing content for Foo version 1 and provider Bar. This is not recommended in normal circumstances and may indicate that you have not configured your Pact pipeline correctly. For more information see https://docs.pact.io/go/versioning", "deprecationWarning": "Replaced by notices" }, { @@ -71,7 +71,7 @@ }, { "type": "warning", - "text": "Pact with changed content published over existing content for Foo version 1 and provider Bar. This is not recommended in normal cicumstances and may indicate that you have not configured your Pact pipeline correctly. For more information see https://docs.pact.io/go/versioning" + "text": "Pact with changed content published over existing content for Foo version 1 and provider Bar. This is not recommended in normal circumstances and may indicate that you have not configured your Pact pipeline correctly. For more information see https://docs.pact.io/go/versioning" }, { "type": "debug", From 299a6abe8c6dfd747e67326da4f4935d98f6927e Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 21 Feb 2024 16:53:54 +1100 Subject: [PATCH 41/69] fix: improve performance for 'pacts for verification' queries --- .../pacts/pact_publication_dataset_module.rb | 1 + .../pact_publication_wip_dataset_module.rb | 138 ++++++++---------- 2 files changed, 60 insertions(+), 79 deletions(-) diff --git a/lib/pact_broker/pacts/pact_publication_dataset_module.rb b/lib/pact_broker/pacts/pact_publication_dataset_module.rb index e4260f955..bdfb5c431 100644 --- a/lib/pact_broker/pacts/pact_publication_dataset_module.rb +++ b/lib/pact_broker/pacts/pact_publication_dataset_module.rb @@ -57,6 +57,7 @@ def for_consumer_name_and_maybe_version_number(consumer_name, consumer_version_n end end + # TODO use the branch heads here def latest_by_consumer_branch branch_versions_join = { Sequel[:pact_publications][:consumer_version_id] => Sequel[:branch_versions][:version_id] diff --git a/lib/pact_broker/pacts/pact_publication_wip_dataset_module.rb b/lib/pact_broker/pacts/pact_publication_wip_dataset_module.rb index eb6ade199..9613089d4 100644 --- a/lib/pact_broker/pacts/pact_publication_wip_dataset_module.rb +++ b/lib/pact_broker/pacts/pact_publication_wip_dataset_module.rb @@ -1,35 +1,76 @@ module PactBroker module Pacts module PactPublicationWipDatasetModule + + # Use a cut down model of the verifications table just for the WIP calculations. + # Don't need all the associations and normal domain methods. + class VerificationForWipCalculations < Sequel::Model(:verifications) + dataset_module do + def successful_non_wip_by_provider(provider_id) + distinct.where(success: true, wip: false, provider_id: provider_id) + end + + def verified_before_creation_date_of(record) + if record + verified_before_date(record.created_at) + else + self + end + end + + def join_branch_versions_excluding_branch(provider_id, branch_name) + branch_versions_join = { + Sequel[:verifications][:provider_version_id] => Sequel[:branch_versions][:version_id], + Sequel[:branch_versions][:pacticipant_id] => provider_id + } + join(:branch_versions, branch_versions_join) do + Sequel.lit("branch_versions.branch_name != ?", branch_name) + end + end + + def join_provider_versions_for_provider_id_and_branch(provider_id, provider_version_branch) + branch_versions_join = { + Sequel[:verifications][:provider_version_id] => Sequel[:branch_versions][:version_id], + Sequel[:branch_versions][:pacticipant_id] => provider_id, + Sequel[:branch_versions][:branch_name] => provider_version_branch + } + + join(:branch_versions, branch_versions_join) + end + + def verified_before_date(date) + where { Sequel[:verifications][:execution_date] < date } + end + end + end + def successfully_verified_by_provider_branch_when_not_wip(provider_id, provider_version_branch) + successful_verifications = VerificationForWipCalculations + .select(:pact_version_id) + .distinct + .successful_non_wip_by_provider(provider_id) + .join_provider_versions_for_provider_id_and_branch(provider_id, provider_version_branch) + + from_self(alias: :pp) .select(Sequel[:pp].*) - .where(Sequel[:pp][:provider_id] => provider_id) - .join_successful_non_wip_verifications_for_provider_id(provider_id) - .join_provider_versions_for_provider_id_and_branch(provider_id, provider_version_branch) .distinct + .join(successful_verifications, { Sequel[:pp][:pact_version_id] => Sequel[:v][:pact_version_id] }, { table_alias: :v }) end def successfully_verified_by_provider_another_branch_before_this_branch_first_created(provider_id, provider_version_branch) first_version_for_branch = PactBroker::Domain::Version.first_for_pacticipant_id_and_branch(provider_id, provider_version_branch) + successful_verifications = VerificationForWipCalculations + .select(:pact_version_id) + .distinct + .successful_non_wip_by_provider(provider_id) + .join_branch_versions_excluding_branch(provider_id, provider_version_branch) + .verified_before_creation_date_of(first_version_for_branch) + from_self(alias: :pp) .select(Sequel[:pp].*) - .join_successful_non_wip_verifications_for_provider_id(provider_id) - .join_provider_versions_for_provider_id(provider_id) - .join_branch_versions_excluding_branch(provider_version_branch) - .where(Sequel[:pp][:provider_id] => provider_id) - .verified_before_creation_date_of(first_version_for_branch) - .distinct - end - - def join_branch_versions_excluding_branch(branch_name) - branch_versions_join = { - Sequel[:provider_versions][:id] => Sequel[:branch_versions][:version_id] - } - join(:branch_versions, branch_versions_join) do - Sequel.lit("branch_versions.branch_name != ?", branch_name) - end + .join(successful_verifications, { Sequel[:pp][:pact_version_id] => Sequel[:v][:pact_version_id] }, { table_alias: :v }) end def successfully_verified_by_provider_tag_when_not_wip(provider_tag) @@ -43,7 +84,6 @@ def successfully_verified_by_provider_tag_when_not_wip(provider_tag) .select(Sequel[:pp].*) .join(:pact_version_provider_tag_successful_verifications, pact_version_provider_tag_verifications_join, { table_alias: :sv }) .distinct - end def successfully_verified_by_provider_another_tag_before_this_tag_first_created(provider_id, provider_tag) @@ -71,66 +111,6 @@ def successfully_verified_by_provider_another_tag_before_this_tag_first_created( end .distinct end - - protected - - def verified_before_date(date) - where { Sequel[:verifications][:execution_date] < date } - end - - def join_successful_non_wip_verifications_for_provider_id(provider_id, &block) - verifications_join = { - pact_version_id: :pact_version_id, - Sequel[:verifications][:success] => true, - Sequel[:verifications][:wip] => false, - Sequel[:verifications][:provider_id] => provider_id - } - join(:verifications, verifications_join, {}, &block) - end - - def join_provider_version_tags &block - tags_join = { - Sequel[:verifications][:provider_version_id] => Sequel[:provider_tags][:version_id], - } - join(:tags, tags_join, { table_alias: :provider_tags }, &block) - end - - def join_provider_version_tags_for_tag(tag) - tags_join = { - Sequel[:verifications][:provider_version_id] => Sequel[:provider_tags][:version_id], - Sequel[:provider_tags][:name] => tag - } - join(:tags, tags_join, { table_alias: :provider_tags } ) - end - - def join_provider_versions_for_provider_id_and_branch(provider_id, provider_version_branch) - versions_join = { - Sequel[:verifications][:provider_version_id] => Sequel[:provider_versions][:id], - Sequel[:provider_versions][:pacticipant_id] => provider_id - } - branch_versions_join = { - Sequel[:provider_versions][:id] => Sequel[:branch_versions][:version_id], - Sequel[:branch_versions][:branch_name] => provider_version_branch - } - join(:versions, versions_join, { table_alias: :provider_versions } ) - .join(:branch_versions, branch_versions_join) - end - - def join_provider_versions_for_provider_id(provider_id, &block) - versions_join = { - Sequel[:verifications][:provider_version_id] => Sequel[:provider_versions][:id], - Sequel[:provider_versions][:pacticipant_id] => provider_id - } - join(:versions, versions_join, { table_alias: :provider_versions }, &block) - end - - def verified_before_creation_date_of(record) - if record - verified_before_date(record.created_at) - else - self - end - end end end end From 0c885d1d847a3f6a12a3596375e4aadbcd61cb4b Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 21 Feb 2024 16:54:07 +1100 Subject: [PATCH 42/69] style: whitespace --- lib/pact_broker/matrix/matrix_row.rb | 1 - lib/pact_broker/matrix/resolved_selector.rb | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/pact_broker/matrix/matrix_row.rb b/lib/pact_broker/matrix/matrix_row.rb index abf66a3e3..8a00da013 100644 --- a/lib/pact_broker/matrix/matrix_row.rb +++ b/lib/pact_broker/matrix/matrix_row.rb @@ -55,7 +55,6 @@ class Verification < Sequel::Model(Sequel.as(:latest_verification_id_for_pact_ve Sequel[:v][:verification_id], Sequel[:v][:provider_version_id], Sequel[:v][:provider_version_created_at] - ] # Must be kept in sync with PactBroker::Matrix::EveryRow diff --git a/lib/pact_broker/matrix/resolved_selector.rb b/lib/pact_broker/matrix/resolved_selector.rb index ad7051a83..e490aa284 100644 --- a/lib/pact_broker/matrix/resolved_selector.rb +++ b/lib/pact_broker/matrix/resolved_selector.rb @@ -11,7 +11,6 @@ module PactBroker module Matrix class ResolvedSelector < Hash - using PactBroker::HashRefinements # A version ID of -1 will not match any rows, which is what we want to ensure that From 62017edbca4b0cd7fa9bf0259d896a6b55c8c2ca Mon Sep 17 00:00:00 2001 From: bethesque Date: Wed, 21 Feb 2024 05:59:01 +0000 Subject: [PATCH 43/69] chore(release): version 2.109.1 --- CHANGELOG.md | 8 ++++++++ lib/pact_broker/version.rb | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 438d2e2ab..3b85e8f62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ + +### v2.109.1 (2024-02-21) + +#### Bug Fixes + +* improve performance for 'pacts for verification' queries ([299a6abe](/../../commit/299a6abe)) +* correct spelling in message when pact is modified ([ae62ae7a](/../../commit/ae62ae7a)) + ### v2.109.0 (2024-02-01) diff --git a/lib/pact_broker/version.rb b/lib/pact_broker/version.rb index 1b05011a1..beb535b35 100644 --- a/lib/pact_broker/version.rb +++ b/lib/pact_broker/version.rb @@ -1,3 +1,3 @@ module PactBroker - VERSION = "2.109.0" + VERSION = "2.109.1" end From 34334ca839c259e856ae79d8fd59e62c16b484a4 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Thu, 29 Feb 2024 13:34:59 +1100 Subject: [PATCH 44/69] feat: support consumer version selector for all branches (#667) * feat: support consumer version selector for all branches * test: add test for latest_for_all_consumer_branches --- .../consumer_version_selector_contract.rb | 2 +- .../provider_pacts_for_verification.rb | 5 ++ .../pacts/pact_publication_dataset_module.rb | 29 ++++++++- ...act_publication_selector_dataset_module.rb | 1 + lib/pact_broker/pacts/selector.rb | 10 ++- ...for_verification_json_query_schema_spec.rb | 30 +++++++++ .../pact_publication_dataset_module_spec.rb | 65 +++++++++++++++++++ 7 files changed, 137 insertions(+), 5 deletions(-) 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") From f9705583f2eb596b799619416e5a15352950a539 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Thu, 29 Feb 2024 13:53:29 +1100 Subject: [PATCH 45/69] fix: improve performance of WIP pacts by using branch heads instead of calculating latest pact for branch --- .../pacts/pact_publication_dataset_module.rb | 34 +++++++++---------- ...act_publication_selector_dataset_module.rb | 2 +- .../pacts_for_verification_repository.rb | 2 +- .../pact_publication_dataset_module_spec.rb | 4 +-- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/pact_broker/pacts/pact_publication_dataset_module.rb b/lib/pact_broker/pacts/pact_publication_dataset_module.rb index 851a8637b..f8ef35ee6 100644 --- a/lib/pact_broker/pacts/pact_publication_dataset_module.rb +++ b/lib/pact_broker/pacts/pact_publication_dataset_module.rb @@ -118,6 +118,23 @@ def overall_latest_for_consumer_id_and_provider_id(consumer_id, provider_id) .limit(1) end + # Returns the pacts (if they exist) for all the branch heads. + # If the version for the branch head does not have a pact, then no pact is returned, + # (unlike latest_by_consumer_branch) + # This is much more performant than latest_by_consumer_branch and should be used + # for the 'pacts for verification' response + # @return [Dataset] + def for_all_branch_heads + base_query = self + base_query = base_query.join(:branch_heads, { Sequel[:bh][:version_id] => Sequel[:pact_publications][:consumer_version_id] }, { table_alias: :bh }) + + if no_columns_selected? + base_query = base_query.select_all_qualified.select_append(Sequel[:bh][:branch_name].as(:branch_name)) + end + + base_query.remove_overridden_revisions + end + # 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 @@ -141,23 +158,6 @@ 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 1e624215e..017821090 100644 --- a/lib/pact_broker/pacts/pact_publication_selector_dataset_module.rb +++ b/lib/pact_broker/pacts/pact_publication_selector_dataset_module.rb @@ -15,7 +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.for_all_branch_heads 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/pacts_for_verification_repository.rb b/lib/pact_broker/pacts/pacts_for_verification_repository.rb index 2c5ed84db..cb8533d9c 100644 --- a/lib/pact_broker/pacts/pacts_for_verification_repository.rb +++ b/lib/pact_broker/pacts/pacts_for_verification_repository.rb @@ -229,7 +229,7 @@ def find_wip_pact_versions_for_provider_by_provider_branch(provider_name, provid provider = pacticipant_repository.find_by_name(provider_name) wip_start_date = options.fetch(:include_wip_pacts_since) - potential_wip_by_consumer_branch = PactPublication.for_provider(provider).created_after(wip_start_date).latest_by_consumer_branch + potential_wip_by_consumer_branch = PactPublication.for_provider(provider).created_after(wip_start_date).for_all_branch_heads potential_wip_by_consumer_tag = PactPublication.for_provider(provider).created_after(wip_start_date).latest_by_consumer_tag log_debug_for_wip do 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 ac8b50883..2f496f05b 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,7 +131,7 @@ module Pacts end end - describe "latest_for_all_consumer_branches" do + describe "for_all_branch_heads" do before do td.create_consumer("Foo") .create_provider("Bar") @@ -150,7 +150,7 @@ module Pacts .create_pact end - subject { PactPublication.latest_for_all_consumer_branches } + subject { PactPublication.for_all_branch_heads } 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 } From 871209e1dab94cbdf4eb56f2b48cd276c9a8e1b1 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Thu, 29 Feb 2024 17:24:07 +1100 Subject: [PATCH 46/69] fix: optimise WIP pacts by using branch/tag heads (#668) --- .../pacts/pact_publication_dataset_module.rb | 30 +++++++++++- .../pacts_for_verification_repository.rb | 4 +- .../pact_publication_dataset_module_spec.rb | 47 +++++++++++++++++++ 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/lib/pact_broker/pacts/pact_publication_dataset_module.rb b/lib/pact_broker/pacts/pact_publication_dataset_module.rb index f8ef35ee6..d6c8f6a5c 100644 --- a/lib/pact_broker/pacts/pact_publication_dataset_module.rb +++ b/lib/pact_broker/pacts/pact_publication_dataset_module.rb @@ -199,7 +199,10 @@ def old_latest_for_consumer_branch(branch_name) # The latest pact publication for each tag # This uses the old logic of "the latest pact for a version that has a tag" (which always returns a pact) # rather than "the pact for the latest version with a tag" - # Need to see about updating this. + # + # For 'pacts for verification' this has been replaced by for_all_tag_heads + # This should only be used for the UI + # @return [Sequel::Dataset] def latest_by_consumer_tag tags_join = { Sequel[:pact_publications][:consumer_version_id] => Sequel[:tags][:version_id], @@ -228,6 +231,7 @@ def latest_by_consumer_tag # This uses the old logic of "the latest pact for a version that has a tag" (which always returns a pact) # rather than "the pact for the latest version with a tag" # Need to see about updating this. + # @return [Sequel::Dataset] def latest_for_consumer_tag(tag_name) tags_join = { Sequel[:pact_publications][:consumer_version_id] => Sequel[:tags][:version_id], @@ -278,6 +282,30 @@ def for_latest_consumer_versions_with_tag(tag_name) .remove_overridden_revisions_from_complete_query end + # The pacts for the latest versions for each tag. + # Will not return a pact if the pact is no longer published for a particular tag + # NEW LOGIC + # @return [Sequel::Dataset] + def for_all_tag_heads + head_tags = PactBroker::Domain::Tag + .select_group(:pacticipant_id, :name) + .select_append{ max(version_order).as(:latest_version_order) } + + head_tags_join = { + Sequel[:pact_publications][:consumer_id] => Sequel[:head_tags][:pacticipant_id], + Sequel[:pact_publications][:consumer_version_order] => Sequel[:head_tags][:latest_version_order] + } + + base_query = self + if no_columns_selected? + base_query = base_query.select_all_qualified.select_append(Sequel[:head_tags][:name].as(:tag_name)) + end + + base_query + .join(head_tags, head_tags_join, { table_alias: :head_tags }) + .remove_overridden_revisions_from_complete_query + end + def in_environments currently_deployed_join = { Sequel[:pact_publications][:consumer_version_id] => Sequel[:currently_deployed_version_ids][:version_id] diff --git a/lib/pact_broker/pacts/pacts_for_verification_repository.rb b/lib/pact_broker/pacts/pacts_for_verification_repository.rb index cb8533d9c..5c226812b 100644 --- a/lib/pact_broker/pacts/pacts_for_verification_repository.rb +++ b/lib/pact_broker/pacts/pacts_for_verification_repository.rb @@ -64,7 +64,7 @@ def find_wip(provider_name, provider_version_branch, provider_tags_names, explic provider_tags_names, wip_start_date, explicitly_specified_verifiable_pacts, - :latest_by_consumer_tag + :for_all_tag_heads ) wip_by_consumer_branches = find_wip_pact_versions_for_provider_by_provider_tags( @@ -72,7 +72,7 @@ def find_wip(provider_name, provider_version_branch, provider_tags_names, explic provider_tags_names, wip_start_date, explicitly_specified_verifiable_pacts, - :latest_by_consumer_branch + :for_all_branch_heads ) deduplicate_verifiable_pacts(wip_by_consumer_tags + wip_by_consumer_branches).sort 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 2f496f05b..d9b892e59 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 @@ -244,6 +244,53 @@ module Pacts end end + describe "for_all_tag_heads" do + before do + td.create_consumer("Foo") + .create_provider("Bar") + .create_consumer_version("1", tag_names: ["main"]) + .create_pact + .create_consumer_version("2", tag_names: ["feat/x"]) + .create_pact + .create_consumer_version("3", tag_names: ["main"], comment: "latest") + .create_pact + .create_consumer_version("4", tag_names: ["feat/x"], comment: "latest") + .create_pact + .create_consumer("FooZ") + .create_consumer_version("6", tag_names: ["main"], comment: "Different consumer") + .create_pact + .create_consumer_version("7", comment: "No branch") + .create_pact + .create_consumer_version("8", tag_names: ["main"], comment: "No pact") + end + + subject { PactPublication.for_all_tag_heads.all_allowing_lazy_load } + + let(:foo) { PactBroker::Domain::Pacticipant.where(name: "Foo").single_record } + let(:bar) { PactBroker::Domain::Pacticipant.where(name: "Bar").single_record } + let(:foo_z) { PactBroker::Domain::Pacticipant.where(name: "FooZ").single_record } + + it "returns the pacts belonging to the latest tagged version for each tag" do + expect(subject.size).to eq 2 + subject.collect(&:values) + + expect(subject.find { |pp| pp.consumer_id == foo.id && pp[:tag_name] == "main" }.consumer_version.number).to eq "3" + expect(subject.find { |pp| pp.consumer_id == foo.id && pp[:tag_name] == "feat/x" }.consumer_version.number).to eq "4" + end + + it "does not return extra columns" do + expect(subject.first.values.keys.sort).to eq (PactPublication.columns + [:tag_name]).sort + end + + context "when columns are already selected" do + subject { PactPublication.select(Sequel[:pact_publications][:id]).latest_by_consumer_tag } + + it "does not override them" do + expect(subject.all.first.values.keys).to eq [:id] + end + end + end + describe "overall_latest" do before do td.create_consumer("Foo") From 14148a343d68a73f953fb3a378f3f74332a70165 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Thu, 29 Feb 2024 18:43:36 +1100 Subject: [PATCH 47/69] fix: use for_all_tag_heads instead of latest_by_consumer_tag when fetching wip by branch --- lib/pact_broker/pacts/pacts_for_verification_repository.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pact_broker/pacts/pacts_for_verification_repository.rb b/lib/pact_broker/pacts/pacts_for_verification_repository.rb index 5c226812b..4f72dc455 100644 --- a/lib/pact_broker/pacts/pacts_for_verification_repository.rb +++ b/lib/pact_broker/pacts/pacts_for_verification_repository.rb @@ -230,7 +230,7 @@ def find_wip_pact_versions_for_provider_by_provider_branch(provider_name, provid wip_start_date = options.fetch(:include_wip_pacts_since) potential_wip_by_consumer_branch = PactPublication.for_provider(provider).created_after(wip_start_date).for_all_branch_heads - potential_wip_by_consumer_tag = PactPublication.for_provider(provider).created_after(wip_start_date).latest_by_consumer_tag + potential_wip_by_consumer_tag = PactPublication.for_provider(provider).created_after(wip_start_date).for_all_tag_heads log_debug_for_wip do log_pact_publications_from_query("Potential WIP pacts for provider branch #{provider_version_branch} created after #{wip_start_date} by consumer branch", potential_wip_by_consumer_branch) From 07084be21be8c6ddbca8aacadcd2ea3b369972c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:53:15 +1100 Subject: [PATCH 48/69] chore(deps): bump peter-evans/repository-dispatch from 2 to 3 (#663) Bumps [peter-evans/repository-dispatch](https://github.com/peter-evans/repository-dispatch) from 2 to 3. - [Release notes](https://github.com/peter-evans/repository-dispatch/releases) - [Commits](https://github.com/peter-evans/repository-dispatch/compare/v2...v3) --- updated-dependencies: - dependency-name: peter-evans/repository-dispatch dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release_gem.yml | 2 +- .github/workflows/trigger_pact_docs_update.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release_gem.yml b/.github/workflows/release_gem.yml index 2ee570fec..85d037f7a 100644 --- a/.github/workflows/release_gem.yml +++ b/.github/workflows/release_gem.yml @@ -48,7 +48,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify ${{ matrix.repository }} of gem release - uses: peter-evans/repository-dispatch@v2 + uses: peter-evans/repository-dispatch@v3 with: token: ${{ secrets.GHTOKENNOTIFYPBRELEASED }} repository: ${{ matrix.repository }} diff --git a/.github/workflows/trigger_pact_docs_update.yml b/.github/workflows/trigger_pact_docs_update.yml index c916c25a8..5b0a7fe60 100644 --- a/.github/workflows/trigger_pact_docs_update.yml +++ b/.github/workflows/trigger_pact_docs_update.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Trigger docs.pact.io update workflow - uses: peter-evans/repository-dispatch@v2 + uses: peter-evans/repository-dispatch@v3 with: token: ${{ secrets.GHTOKENFORTRIGGERINGPACTDOCSUPDATE }} repository: pact-foundation/docs.pact.io From 075f5db22dd2f6eb4f8706f1af3858f91e72fdaf Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 20 Mar 2024 10:19:02 +1100 Subject: [PATCH 49/69] refactor: remove old method for loading versions collection (#669) * refactor: remove old method for loading versions collection * chore: fix argument --- ...n_pattern_for_eager_loading_collections.md | 23 +++++++++++++++++ .../api/decorators/base_decorator.rb | 11 +++++++- .../api/decorators/version_decorator.rb | 14 +++++++++++ .../api/resources/branch_versions.rb | 2 +- lib/pact_broker/api/resources/versions.rb | 2 +- lib/pact_broker/versions/repository.rb | 22 ++-------------- lib/pact_broker/versions/service.rb | 8 ++---- spec/features/get_branch_versions_spec.rb | 2 +- .../pact_broker/versions/repository_spec.rb | 25 ------------------- 9 files changed, 54 insertions(+), 55 deletions(-) create mode 100644 docs/developer/design_pattern_for_eager_loading_collections.md diff --git a/docs/developer/design_pattern_for_eager_loading_collections.md b/docs/developer/design_pattern_for_eager_loading_collections.md new file mode 100644 index 000000000..ebc759490 --- /dev/null +++ b/docs/developer/design_pattern_for_eager_loading_collections.md @@ -0,0 +1,23 @@ +# Design pattern for eager loading collections + +For collection resources (eg. `/versions` ), associations included in the items (eg. branch versions) must be eager loaded for performance reasons. + +The responsiblities of each class used to render a collection resource are as follows: + +* collection decorator (eg. `VersionsDecorator`) - delegate each item in the collection to be rendered by the decorator for the individual item, render pagination links +* item decorator (eg. `VersionDecorator`) - render the JSON for each item +* resource (eg. `PactBroker::Api::Resources::Versions`) - coordinate between, and delegate to, the service and the decorator +* service (eg. `PactBroker::Versions::Service`) - just delegate to repository, as there is no business logic required +* repository (eg. `PactBroker::Versions::Repository`) - load the domain objects from the database + +If the associations for a model are not eager loaded, then each individual association will be lazy loaded when the decorator for the item calls the association method to render it. This results in at least ` * ` calls to the database, and potentially more if any of the associations have their own associations that are required to render the item. This can cause significant performance issues. + +To efficiently render a collection resource, associations must be eager loaded when the collection items are loaded from the database in the repository. Since the repository method for loading the collection may be used in multiple places, and the eager loaded associations required for each of those places may be different (some may not require any associations to be eager loaded), we do not want to hard code the repository to load a fixed set of associations. The list of associations to eager load is therefore passed in to the repository finder method as an argument `eager_load_associations`. + +The decorator is the class that knows what associations are going to be called on the model to render the JSON, so following the design guideline of "put things together that change together", the best place for the declaration of "what associations should be eager loaded for this decorator" is in the decorator itself. The `PactBroker::Api::Decorators::BaseDecorator` has a default implementation of this method called `eager_load_associations` which attempts to automatically identify the required associations, but this can be overridden when necessary. + +We can therefore add the following responsiblities to our previous list: + +* item decorator - return a list of all the associations (including nested associations) that should be eager loaded in order to render its item +* repository - eager load the associations that have been passed into it +* resource - pass in the eager load associations to the repository from the decorator diff --git a/lib/pact_broker/api/decorators/base_decorator.rb b/lib/pact_broker/api/decorators/base_decorator.rb index 96bc5892c..b14524cc8 100644 --- a/lib/pact_broker/api/decorators/base_decorator.rb +++ b/lib/pact_broker/api/decorators/base_decorator.rb @@ -34,7 +34,16 @@ def self.property(name, options={}, &block) end end - # Returns the names of the model associations to eager load for use with this decorator + # Returns the names of the model associations to eager load for use with this decorator. + # The default implementation attempts to do an "auto detect" of the associations. + # For single item decorators, it attempts to identify the attributes that are themselves models. + # For collection decorators, it delegates to the eager_load_associations + # method of the single item decorator used to decorate the collection. + # + # The "auto detect" logic can only go so far. It cannot identify when a child object needs its own + # child object(s) to render the attribute. + # This method should be overridden when the "auto detect" logic cannot identify the correct associations + # to load. eg VersionDecorator # @return [Array] def self.eager_load_associations if is_collection_resource? diff --git a/lib/pact_broker/api/decorators/version_decorator.rb b/lib/pact_broker/api/decorators/version_decorator.rb index 875d95884..7b94dbc59 100644 --- a/lib/pact_broker/api/decorators/version_decorator.rb +++ b/lib/pact_broker/api/decorators/version_decorator.rb @@ -17,6 +17,20 @@ class VersionDecorator < BaseDecorator include Timestamps + # Returns the list of associations that must be eager loaded to efficiently render a version + # when this decorator is used in a collection (eg. VersionsDecorator) + # The associations that need to be eager loaded for the VersionDecorator + # are hand coded + # @return + def self.eager_load_associations + [ + :pacticipant, + :pact_publications, + { branch_versions: [:version, :branch_head, { branch: :pacticipant }] }, + { tags: :head_tag } + ] + end + link :self do | options | { title: "Version", diff --git a/lib/pact_broker/api/resources/branch_versions.rb b/lib/pact_broker/api/resources/branch_versions.rb index 6bb1b10ea..0a25d9d7a 100644 --- a/lib/pact_broker/api/resources/branch_versions.rb +++ b/lib/pact_broker/api/resources/branch_versions.rb @@ -31,7 +31,7 @@ def to_json end def versions - @versions ||= version_service.find_pacticipant_versions_in_reverse_order(pacticipant_name, { branch_name: identifier_from_path[:branch_name] }, pagination_options) + @versions ||= version_service.find_pacticipant_versions_in_reverse_order(pacticipant_name, { branch_name: identifier_from_path[:branch_name] }, pagination_options, decorator_class(:versions_decorator).eager_load_associations) end def policy_name diff --git a/lib/pact_broker/api/resources/versions.rb b/lib/pact_broker/api/resources/versions.rb index e03b2d76d..4e91ca18a 100644 --- a/lib/pact_broker/api/resources/versions.rb +++ b/lib/pact_broker/api/resources/versions.rb @@ -31,7 +31,7 @@ def to_json end def versions - @versions ||= version_service.find_all_pacticipant_versions_in_reverse_order(pacticipant_name, pagination_options) + @versions ||= version_service.find_pacticipant_versions_in_reverse_order(pacticipant_name, {}, pagination_options, decorator_class(:versions_decorator).eager_load_associations) end def policy_name diff --git a/lib/pact_broker/versions/repository.rb b/lib/pact_broker/versions/repository.rb index 28091d084..4ae4c25ed 100644 --- a/lib/pact_broker/versions/repository.rb +++ b/lib/pact_broker/versions/repository.rb @@ -57,29 +57,11 @@ def find_by_pacticipant_name_and_number pacticipant_name, number .single_record end - # The eager loaded relations are hardcoded here to support the PactBroker::Api::Decorators::VersionDecorator - # Newer "find all" implementations for other models pass the relations to eager load in - # from the decorator via the resource. - def find_all_pacticipant_versions_in_reverse_order name, pagination_options = {} - pacticipant = pacticipant_repository.find_by_name!(name) - query = PactBroker::Domain::Version - .where(pacticipant: pacticipant) - .eager(:pacticipant) - .eager(branch_versions: [:version, :branch_head, { branch: :pacticipant }]) - .eager(tags: :head_tag) - .eager(:pact_publications) - .reverse_order(:order) - query.all_with_pagination_options(pagination_options) - end - - def find_pacticipant_versions_in_reverse_order(pacticipant_name, options = {}, pagination_options = {}) + def find_pacticipant_versions_in_reverse_order(pacticipant_name, options = {}, pagination_options = {}, eager_load_associations = []) pacticipant = pacticipant_repository.find_by_name!(pacticipant_name) query = PactBroker::Domain::Version .where(pacticipant: pacticipant) - .eager(:pacticipant) - .eager(branch_versions: [:version, :branch_head, { branch: :pacticipant }]) - .eager(tags: :head_tag) - .eager(:pact_publications) + .eager(*eager_load_associations) .reverse_order(:order) if options[:branch_name] diff --git a/lib/pact_broker/versions/service.rb b/lib/pact_broker/versions/service.rb index 439133509..2f5d876c2 100644 --- a/lib/pact_broker/versions/service.rb +++ b/lib/pact_broker/versions/service.rb @@ -26,12 +26,8 @@ def self.find_latest_by_pacticipant_name_and_branch_name(pacticipant_name, branc version_repository.find_latest_by_pacticipant_name_and_branch_name(pacticipant_name, branch_name) end - def self.find_all_pacticipant_versions_in_reverse_order(name, pagination_options = {}) - version_repository.find_all_pacticipant_versions_in_reverse_order(name, pagination_options) - end - - def self.find_pacticipant_versions_in_reverse_order(pacticipant_name, options, pagination_options = {}) - version_repository.find_pacticipant_versions_in_reverse_order(pacticipant_name, options, pagination_options) + def self.find_pacticipant_versions_in_reverse_order(pacticipant_name, options, pagination_options = {}, eager_load_associations = []) + version_repository.find_pacticipant_versions_in_reverse_order(pacticipant_name, options, pagination_options, eager_load_associations) end def self.create_or_overwrite(pacticipant_name, version_number, version) diff --git a/spec/features/get_branch_versions_spec.rb b/spec/features/get_branch_versions_spec.rb index 9884cab86..dadb3eb18 100644 --- a/spec/features/get_branch_versions_spec.rb +++ b/spec/features/get_branch_versions_spec.rb @@ -1,4 +1,4 @@ -describe "Get a branch version" do +describe "Get versions for branch" do before do td.create_consumer("Foo") .create_consumer_version("1", branch: "main") diff --git a/spec/lib/pact_broker/versions/repository_spec.rb b/spec/lib/pact_broker/versions/repository_spec.rb index 356a664d6..3df5ecebb 100644 --- a/spec/lib/pact_broker/versions/repository_spec.rb +++ b/spec/lib/pact_broker/versions/repository_spec.rb @@ -24,31 +24,6 @@ module Versions end end - describe "#find_all_pacticipant_versions_in_reverse_order" do - before do - td - .create_consumer("Foo") - .create_consumer_version("1.2.3") - .create_consumer_version("4.5.6") - .create_consumer("Bar") - .create_consumer_version("8.9.0") - end - - subject { Repository.new.find_all_pacticipant_versions_in_reverse_order "Foo" } - - it "returns all the application versions for the given consumer" do - expect(subject.collect(&:number)).to eq ["4.5.6", "1.2.3"] - end - - context "with pagination options" do - subject { Repository.new.find_all_pacticipant_versions_in_reverse_order "Foo", page_number: 1, page_size: 1 } - - it "paginates the query" do - expect(subject.collect(&:number)).to eq ["4.5.6"] - end - end - end - describe "#find_pacticipant_versions_in_reverse_order" do before do td From 92a97d5b7aa04b51747049c39869cf39b04c3d14 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Thu, 21 Mar 2024 11:47:57 +1100 Subject: [PATCH 50/69] refactor: create/update integrations in batch at end of transaction (#670) * refactor: create/update integrations in batch at end of transaction * docs: add comments --- lib/pact_broker/contracts/service.rb | 8 +++- lib/pact_broker/integrations/integration.rb | 9 ++++- lib/pact_broker/integrations/repository.rb | 14 +++++++ lib/pact_broker/integrations/service.rb | 2 + lib/pact_broker/pacts/repository.rb | 4 +- lib/pact_broker/test/test_data_builder.rb | 11 +++-- .../integrations/repository_spec.rb | 40 +++++++++++++++++++ spec/lib/pact_broker/pacts/repository_spec.rb | 6 --- 8 files changed, 80 insertions(+), 14 deletions(-) diff --git a/lib/pact_broker/contracts/service.rb b/lib/pact_broker/contracts/service.rb index 7776bcecb..698430a65 100644 --- a/lib/pact_broker/contracts/service.rb +++ b/lib/pact_broker/contracts/service.rb @@ -35,7 +35,7 @@ def publish(parsed_contracts, base_url: ) version, version_notices = create_version(parsed_contracts) tags = create_tags(parsed_contracts, version) pacts, pact_notices = create_pacts(parsed_contracts, base_url) - update_integrations(pacts) + create_or_update_integrations(pacts) notices = version_notices + pact_notices ContractsPublicationResults.from_hash( pacticipant: version.pacticipant, @@ -304,7 +304,11 @@ def url_for_triggered_webhook(triggered_webhook, base_url) PactBroker::Api::PactBrokerUrls.triggered_webhook_logs_url(triggered_webhook, base_url) end - def update_integrations(pacts) + # Creating/updating the integrations all at once at the end of the transaction instead + # of one by one, as each pact is created, reduces the amount of time that + # a lock is held on the integrations table, therefore reducing contention + # and potential for deadlocks when there are many pacts being published at once. + def create_or_update_integrations(pacts) integration_service.handle_bulk_contract_data_published(pacts) end diff --git a/lib/pact_broker/integrations/integration.rb b/lib/pact_broker/integrations/integration.rb index 78c6750ab..860d7b9ce 100644 --- a/lib/pact_broker/integrations/integration.rb +++ b/lib/pact_broker/integrations/integration.rb @@ -7,7 +7,14 @@ module PactBroker module Integrations - class Integration < Sequel::Model(Sequel::Model.db[:integrations].select(:id, :consumer_id, :provider_id, :contract_data_updated_at)) + # The columns are explicitly specified for the Integration object so that the consumer_name and provider_name columns aren't included + # in the model. + # Those columns exist in the integrations table because the integrations table used to be an integrations view based on the + # pact_publications table, and those columns existed in the view. + # When the view was migrated to be a table (in db/migrations/20211102_create_table_temp_integrations.rb and the following migrations) + # the columns had to be maintained for backwards compatiblity. + # They are not used by the current code, however. + class Integration < Sequel::Model(Sequel::Model.db[:integrations].select(:id, :consumer_id, :provider_id, :created_at, :contract_data_updated_at)) set_primary_key :id plugin :insert_ignore, identifying_columns: [:consumer_id, :provider_id] associate(:many_to_one, :consumer, :class => "PactBroker::Domain::Pacticipant", :key => :consumer_id, :primary_key => :id) diff --git a/lib/pact_broker/integrations/repository.rb b/lib/pact_broker/integrations/repository.rb index 73f6912b4..7568eed66 100644 --- a/lib/pact_broker/integrations/repository.rb +++ b/lib/pact_broker/integrations/repository.rb @@ -28,6 +28,20 @@ def create_for_pact(consumer_id, provider_id) nil end + # Ensure an Integration exists for each consumer/provider pair. + # Using SELECT ... INSERT IGNORE rather than just INSERT IGNORE so that we do not + # need to lock the table at all when the integrations already exist, which will + # be the most common use case. New integrations get created incredibly rarely. + # The INSERT IGNORE is used rather than just INSERT to handle race conditions + # when requests come in parallel. + # @param [Array] where each object has a consumer and a provider + def create_for_pacts(objects_with_consumer_and_provider) + published_integrations = objects_with_consumer_and_provider.collect{ |i| { consumer_id: i.consumer.id, provider_id: i.provider.id } } + existing_integrations = Sequel::Model.db[:integrations].select(:consumer_id, :provider_id).where(Sequel.|(*published_integrations) ).all + new_integrations = (published_integrations - existing_integrations).collect{ |i| i.merge(created_at: Sequel.datetime_class.now, contract_data_updated_at: Sequel.datetime_class.now) } + Integration.dataset.insert_ignore.multi_insert(new_integrations) + end + def delete(consumer_id, provider_id) Integration.where(consumer_id: consumer_id, provider_id: provider_id).delete end diff --git a/lib/pact_broker/integrations/service.rb b/lib/pact_broker/integrations/service.rb index d291180cf..5f11c92b0 100644 --- a/lib/pact_broker/integrations/service.rb +++ b/lib/pact_broker/integrations/service.rb @@ -21,6 +21,7 @@ def self.find_all(filter_options = {}, pagination_options = {}, eager_load_assoc # @param [PactBroker::Domain::Pacticipant] consumer or nil # @param [PactBroker::Domain::Pacticipant] provider def self.handle_contract_data_published(consumer, provider) + integration_repository.create_for_pact(consumer.id, provider.id) integration_repository.set_contract_data_updated_at(consumer, provider) end @@ -28,6 +29,7 @@ def self.handle_contract_data_published(consumer, provider) # Callback to invoke when a batch of contract data is published (eg. the publish contracts endpoint) # @param [Array] where each object has a consumer and a provider def self.handle_bulk_contract_data_published(objects_with_consumer_and_provider) + integration_repository.create_for_pacts(objects_with_consumer_and_provider) integration_repository.set_contract_data_updated_at_for_multiple_integrations(objects_with_consumer_and_provider) end diff --git a/lib/pact_broker/pacts/repository.rb b/lib/pact_broker/pacts/repository.rb index c5f816b8f..f1e05291d 100644 --- a/lib/pact_broker/pacts/repository.rb +++ b/lib/pact_broker/pacts/repository.rb @@ -33,8 +33,8 @@ def unscoped(scope) scope end - def create params - integration_repository.create_for_pact(params.fetch(:consumer_id), params.fetch(:provider_id)) + # @return [PactBroker::Domain::Pact] + def create(params) pact_version = find_or_create_pact_version( params.fetch(:consumer_id), params.fetch(:provider_id), diff --git a/lib/pact_broker/test/test_data_builder.rb b/lib/pact_broker/test/test_data_builder.rb index 92d6bbcbc..59a2270ea 100644 --- a/lib/pact_broker/test/test_data_builder.rb +++ b/lib/pact_broker/test/test_data_builder.rb @@ -43,7 +43,6 @@ class TestDataBuilder include PactBroker::Services using PactBroker::StringRefinements - attr_reader :pacticipant attr_reader :consumer attr_reader :provider @@ -192,8 +191,11 @@ def create_provider provider_name = "Provider #{model_counter}", params = {} self end + # Create an Integration object for the current consumer and provider + # @return [PactBroker::Test::TestDataBuilder] def create_integration - PactBroker::Integrations::Repository.new.create_for_pact(consumer.id, provider.id) + @integration = PactBroker::Integrations::Repository.new.create_for_pact(consumer.id, provider.id) + set_created_at_if_set(@now, :integrations, { consumer_id: consumer.id, provider_id: provider.id }) self end @@ -280,7 +282,9 @@ def publish_pact(consumer_name:, provider_name:, consumer_version_number: , tags self end - def create_pact params = {} + # Creates a pact (and integration if one does not already exist) from the given params + # @return [PactBroker::Test::TestDataBuilder] + def create_pact(params = {}) params.delete(:comment) json_content = params[:json_content] || default_json_content pact_version_sha = params[:pact_version_sha] || generate_pact_version_sha(json_content) @@ -293,6 +297,7 @@ def create_pact params = {} json_content: prepare_json_content(json_content), version: @consumer_version ) + integration_service.handle_bulk_contract_data_published([@pact]) pact_versions_count_after = PactBroker::Pacts::PactVersion.count set_created_at_if_set(params[:created_at], :pact_publications, id: @pact.id) set_created_at_if_set(params[:created_at], :pact_versions, sha: @pact.pact_version_sha) if pact_versions_count_after > pact_versions_count_before diff --git a/spec/lib/pact_broker/integrations/repository_spec.rb b/spec/lib/pact_broker/integrations/repository_spec.rb index 243be5fbf..dd21a882a 100644 --- a/spec/lib/pact_broker/integrations/repository_spec.rb +++ b/spec/lib/pact_broker/integrations/repository_spec.rb @@ -38,6 +38,46 @@ module Integrations end end + describe "#create_for_pacts" do + before do + Timecop.freeze(Date.today - 5) do + td.create_consumer("A") + .create_provider("B") + .create_integration + .create_pacticipant("C") + .create_pacticipant("D") + end + end + + let(:objects_with_consumer_and_provider) do + [ + double("i1", consumer: td.find_pacticipant("A"), provider: td.find_pacticipant("B")), + double("i2", consumer: td.find_pacticipant("C"), provider: td.find_pacticipant("D")) + ] + end + + subject { Repository.new.create_for_pacts(objects_with_consumer_and_provider) } + + it "inserts any missing integrations" do + now = Time.utc(2024) + Timecop.freeze(now) do + subject + end + + integrations = Integration.eager(:consumer, :provider).order(:id).all + expect(integrations).to contain_exactly( + have_attributes(consumer_name: "A", provider_name: "B"), + have_attributes(consumer_name: "C", provider_name: "D") + ) + expect(integrations.last.created_at).to be_date_time(now) + expect(integrations.last.contract_data_updated_at).to be_date_time(now) + end + + it "does not change the created_at or contract_data_updated_at of the existing integrations" do + expect { subject }.to_not change { Integration.order(:id).select(:created_at, :contract_data_updated_at).first.created_at } + end + end + describe "#set_contract_data_updated_at" do before do # A -> B diff --git a/spec/lib/pact_broker/pacts/repository_spec.rb b/spec/lib/pact_broker/pacts/repository_spec.rb index 8a9fe65a9..68952e257 100644 --- a/spec/lib/pact_broker/pacts/repository_spec.rb +++ b/spec/lib/pact_broker/pacts/repository_spec.rb @@ -73,12 +73,6 @@ module Pacts expect(PactVersion.order(:id).last.messages_count).to eq 0 end - it "creates an integration" do - expect { subject }.to change { - PactBroker::Integrations::Integration.where(consumer_id: consumer.id, provider_id: provider.id).count - }.from(0).to(1) - end - context "when a pact already exists with exactly the same content" do let(:another_version) { Versions::Repository.new.create number: "2.0.0", pacticipant_id: consumer.id } From 637c25fa0d7f40473eb04382daa71abf44707496 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 27 Mar 2024 12:00:05 +1100 Subject: [PATCH 51/69] feat(clean): use postgres advisory locks to ensure only one process can run a clean at a time (#672) --- lib/pact_broker/app.rb | 33 ++++-- lib/pact_broker/db/advisory_lock.rb | 58 ++++++++++ lib/pact_broker/tasks/clean_task.rb | 94 +++++++++++----- lib/sequel/extensions/pg_advisory_lock.rb | 101 ++++++++++++++++++ script/docker/db-migrate.sh | 3 + .../lib/sequel/extensions/lock_names_keys.yml | 100 +++++++++++++++++ .../extensions/register_advisory_lock_test.rb | 89 +++++++++++++++ 7 files changed, 443 insertions(+), 35 deletions(-) create mode 100644 lib/pact_broker/db/advisory_lock.rb create mode 100644 lib/sequel/extensions/pg_advisory_lock.rb create mode 100755 script/docker/db-migrate.sh create mode 100644 spec/lib/sequel/extensions/lock_names_keys.yml create mode 100644 spec/lib/sequel/extensions/register_advisory_lock_test.rb diff --git a/lib/pact_broker/app.rb b/lib/pact_broker/app.rb index 341635492..2f6be0006 100644 --- a/lib/pact_broker/app.rb +++ b/lib/pact_broker/app.rb @@ -27,6 +27,7 @@ require "pact_broker/api/authorization/resource_access_policy" require "pact_broker/api/middleware/http_debug_logs" require "pact_broker/application_context" +require "pact_broker/db/advisory_lock" module PactBroker @@ -101,22 +102,19 @@ def post_configure def prepare_database logger.info "Database schema version is #{PactBroker::DB.version(configuration.database_connection)}" + lock = PactBroker::DB::AdvisoryLock.new(configuration.database_connection, :migrate, :pg_advisory_lock) if configuration.auto_migrate_db - migration_options = { allow_missing_migration_files: configuration.allow_missing_migration_files } - if PactBroker::DB.is_current?(configuration.database_connection, migration_options) - logger.info "Skipping database migrations as the latest migration has already been applied" - else - logger.info "Migrating database schema" - PactBroker::DB.run_migrations configuration.database_connection, migration_options - logger.info "Database schema version is now #{PactBroker::DB.version(configuration.database_connection)}" + lock.with_lock do + ensure_all_database_migrations_are_applied end else logger.info "Skipping database schema migrations as database auto migrate is disabled" end if configuration.auto_migrate_db_data - logger.info "Migrating data" - PactBroker::DB.run_data_migrations configuration.database_connection + lock.with_lock do + run_data_migrations + end else logger.info "Skipping data migrations" end @@ -125,6 +123,23 @@ def prepare_database PactBroker::Webhooks::Service.fail_retrying_triggered_webhooks end + def ensure_all_database_migrations_are_applied + migration_options = { allow_missing_migration_files: configuration.allow_missing_migration_files } + + if PactBroker::DB.is_current?(configuration.database_connection, migration_options) + logger.info "Skipping database migrations as the latest migration has already been applied" + else + logger.info "Migrating database schema" + PactBroker::DB.run_migrations(configuration.database_connection, migration_options) + logger.info "Database schema version is now #{PactBroker::DB.version(configuration.database_connection)}" + end + end + + def run_data_migrations + logger.info "Migrating data" + PactBroker::DB.run_data_migrations(configuration.database_connection) + end + def load_configuration_from_database configuration.load_from_database! end diff --git a/lib/pact_broker/db/advisory_lock.rb b/lib/pact_broker/db/advisory_lock.rb new file mode 100644 index 000000000..d59e1a257 --- /dev/null +++ b/lib/pact_broker/db/advisory_lock.rb @@ -0,0 +1,58 @@ +require "pact_broker/logging" + +# Uses a Postgres advisory lock to ensure that a given block of code can only have ONE +# thread in excution at a time against a given database. +# When the database is not Postgres, the block will yield without any locks, allowing +# this class to be used safely with other database types, but without the locking +# functionality. +# +# This is a wrapper around the actual implementation code in the Sequel extension from https://github.com/yuryroot/sequel-pg_advisory_lock +# which was copied into this codebase and modified for usage in this codebase. +# +# See https://www.postgresql.org/docs/16/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS for docs on lock types +# + +module PactBroker + module DB + class AdvisoryLock + include PactBroker::Logging + + def initialize(database_connection, name, type = :pg_try_advisory_lock) + @database_connection = database_connection + @name = name + @type = type + @lock_obtained = false + register_advisory_lock if postgres? + end + + def with_lock + if postgres? + @database_connection.with_advisory_lock(@name) do + logger.debug("Lock #{@name} obtained") + @lock_obtained = true + yield + end + else + logger.warn("Executing block without lock as this is not a Postgres database") + @lock_obtained = true + yield + end + end + + def lock_obtained? + @lock_obtained + end + + private + + def postgres? + @database_connection.adapter_scheme.to_s =~ /postgres/ + end + + def register_advisory_lock + @database_connection.extension :pg_advisory_lock + @database_connection.register_advisory_lock(@name, @type) + end + end + end +end diff --git a/lib/pact_broker/tasks/clean_task.rb b/lib/pact_broker/tasks/clean_task.rb index d356a8c5a..699c2886c 100644 --- a/lib/pact_broker/tasks/clean_task.rb +++ b/lib/pact_broker/tasks/clean_task.rb @@ -1,3 +1,8 @@ +# This task is used to clean up old data in a Pact Broker database +# to stop performance issues from slowing down responses when there is +# too much data. +# See https://docs.pact.io/pact_broker/administration/maintenance + module PactBroker module DB class CleanTask < ::Rake::TaskLib @@ -7,11 +12,13 @@ class CleanTask < ::Rake::TaskLib attr_accessor :version_deletion_limit attr_accessor :logger attr_accessor :dry_run + attr_accessor :use_lock # allow disabling of postgres lock if it is causing problems def initialize &block require "pact_broker/db/clean_incremental" @version_deletion_limit = 1000 @dry_run = false + @use_lock = true @keep_version_selectors = PactBroker::DB::CleanIncremental::DEFAULT_KEEP_SELECTORS rake_task(&block) end @@ -28,42 +35,77 @@ def rake_task &block namespace :db do desc "Clean unnecessary pacts and verifications from database" task :clean do | _t, _args | - instance_eval(&block) - require "pact_broker/db/clean_incremental" - require "pact_broker/error" - require "yaml" - require "benchmark" + with_lock do + perform_clean + end + end + end + end + end + + def perform_clean + require "pact_broker/db/clean_incremental" + require "pact_broker/error" + require "yaml" + require "benchmark" - raise PactBroker::Error.new("You must specify the version_deletion_limit") unless version_deletion_limit + raise PactBroker::Error.new("You must specify the version_deletion_limit") unless version_deletion_limit - prefix = dry_run ? "[DRY RUN] " : "" + if keep_version_selectors.nil? || keep_version_selectors.empty? + raise PactBroker::Error.new("You must specify which versions to keep") + else + add_defaults_to_keep_selectors + output "Deleting oldest #{version_deletion_limit} versions, keeping versions that match the configured selectors", keep_version_selectors.collect(&:to_hash) + end - if keep_version_selectors.nil? || keep_version_selectors.empty? - raise PactBroker::Error.new("You must specify which versions to keep") - else - add_defaults_to_keep_selectors - output "#{prefix}Deleting oldest #{version_deletion_limit} versions, keeping versions that match the configured selectors", keep_version_selectors.collect(&:to_hash) - end + start_time = Time.now + results = PactBroker::DB::CleanIncremental.call(database_connection, + keep: keep_version_selectors, + limit: version_deletion_limit, + logger: logger, + dry_run: dry_run + ) + end_time = Time.now + elapsed_seconds = (end_time - start_time).to_i + output "Results (#{elapsed_seconds} seconds)", results + end - start_time = Time.now - results = PactBroker::DB::CleanIncremental.call(database_connection, - keep: keep_version_selectors, - limit: version_deletion_limit, - logger: logger, - dry_run: dry_run - ) - end_time = Time.now - elapsed_seconds = (end_time - start_time).to_i - output "Results (#{elapsed_seconds} seconds)", results - end + # Use a Postgres advisory lock to ensure that only one clean can run at a time. + # This allows a cron schedule to be used on the Pact Broker Docker image when deployed + # on a multi-instance architecture, without all the instances stepping on each other's toes. + # + # Any tasks that attempt to run while a clean job is running will skip the clean + # and exit with a message and a success code. + # + # To test that the lock works, run: + # script/docker/db-start.sh + # script/docker/db-migrate.sh + # for i in {0..3}; do PACT_BROKER_TEST_DATABASE_URL=postgres://postgres:postgres@localhost/postgres bundle exec rake pact_broker:db:clean &; done; + # + # There will be 3 messages saying "Clean was not performed" and output from one thread showing the clean is being done. + def with_lock + if use_lock + require "pact_broker/db/advisory_lock" + + lock = PactBroker::DB::AdvisoryLock.new(database_connection, :clean, :pg_try_advisory_lock) + results = lock.with_lock do + yield + end + + if !lock.lock_obtained? + output("Clean was not performed as a clean is already in progress. Exiting.") end + results + else + yield end end - def output string, payload = {} - logger ? logger.info(string, payload) : puts("#{string} #{payload.to_json}") + def output(string, payload = {}) + prefix = dry_run ? "[DRY RUN] " : "" + logger ? logger.info("#{prefix}#{string}") : puts("#{prefix}#{string} #{payload.to_json}") end def add_defaults_to_keep_selectors diff --git a/lib/sequel/extensions/pg_advisory_lock.rb b/lib/sequel/extensions/pg_advisory_lock.rb new file mode 100644 index 000000000..b9a1ea1b1 --- /dev/null +++ b/lib/sequel/extensions/pg_advisory_lock.rb @@ -0,0 +1,101 @@ +# Copied with thanks from https://github.com/yuryroot/sequel-pg_advisory_lock/blob/d7509aa/lib/sequel/extensions/pg_advisory_lock.rb +# The reason this is copy/pasted and modified is that I wanted to allow exact duplicate +# locks to be registered because different threads running the same code +# should not cause a Sequel::Error to be raised. +# Also, I wanted it to use Concurrent::Hash for multi-threaded environments. + +require "sequel" +require "zlib" +require "concurrent/hash" + +module Sequel + module Postgres + module PgAdvisoryLock + + SESSION_LEVEL_LOCKS = [ + :pg_advisory_lock, + :pg_try_advisory_lock + ].freeze + + TRANSACTION_LEVEL_LOCKS = [ + :pg_advisory_xact_lock, + :pg_try_advisory_xact_lock + ].freeze + + LOCK_FUNCTIONS = (SESSION_LEVEL_LOCKS + TRANSACTION_LEVEL_LOCKS).freeze + + DEFAULT_LOCK_FUNCTION = :pg_advisory_lock + UNLOCK_FUNCTION = :pg_advisory_unlock + + class LockAlreadyRegistered < Sequel::Error; end + + def registered_advisory_locks + @registered_advisory_locks ||= Concurrent::Hash.new + end + + def with_advisory_lock(name, id = nil) + options = registered_advisory_locks.fetch(name.to_sym) + + lock_key = options.fetch(:key) + function_params = [lock_key, id].compact + + lock_function = options.fetch(:lock_function) + transaction_level_lock = TRANSACTION_LEVEL_LOCKS.include?(lock_function) + + if transaction_level_lock + # TODO: It's allowed to specify additional options (in particular, :server) + # while opening database transaction. + # That's why this check must be smarter. + unless in_transaction? + raise Error, "Transaction must be manually opened before using transaction level lock '#{lock_function}'" + end + + if get(Sequel.function(lock_function, *function_params)) + yield + end + else + synchronize do + if get(Sequel.function(lock_function, *function_params)) + begin + result = yield + ensure + get(Sequel.function(UNLOCK_FUNCTION, *function_params)) + result + end + end + end + end + end + + # Beth: not sure how much extra value this registration provides. + # It turns the name into a number, and makes sure the name/number is unique, + # and that you don't try and use a different lock function with the same name. + def register_advisory_lock(name, lock_function = DEFAULT_LOCK_FUNCTION) + name = name.to_sym + + if registered_advisory_locks.key?(name) && registered_advisory_locks[name][:lock_function] != lock_function + raise LockAlreadyRegistered, "Lock with name :#{name} is already registered with a different lock function (#{registered_advisory_locks[name][:lock_function]})" + end + + key = advisory_lock_key_for(name) + name_for_key = registered_advisory_locks.keys.find { |n| registered_advisory_locks[n].fetch(:key) == key } + if name_for_key && name_for_key != name + raise Error, "Lock key #{key} is already taken" + end + + function = lock_function.to_sym + unless LOCK_FUNCTIONS.include?(function) + raise Error, "Invalid lock function :#{function}" + end + + registered_advisory_locks[name] = { key: key, lock_function: function } + end + + def advisory_lock_key_for(lock_name) + Zlib.crc32(lock_name.to_s) % 2 ** 31 + end + end + end + + Database.register_extension(:pg_advisory_lock, Postgres::PgAdvisoryLock) +end diff --git a/script/docker/db-migrate.sh b/script/docker/db-migrate.sh new file mode 100755 index 000000000..5be8db83e --- /dev/null +++ b/script/docker/db-migrate.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +PACT_BROKER_TEST_DATABASE_URL=postgres://postgres:postgres@localhost/postgres bundle exec rake pact_broker:db:migrate \ No newline at end of file diff --git a/spec/lib/sequel/extensions/lock_names_keys.yml b/spec/lib/sequel/extensions/lock_names_keys.yml new file mode 100644 index 000000000..3893a6d13 --- /dev/null +++ b/spec/lib/sequel/extensions/lock_names_keys.yml @@ -0,0 +1,100 @@ +--- +5b25e9d56cf531395d7eb91474acf01c600235d24090ecbf107a: 922798582 +9fb20fc10a71bc70f7556ee2cb2d9fd73a14d4ea21533cb57f4e6a84d0b7a4d247f7c64fa09aeb750ef5fe51bf9d: 897625924 +45780a5ee58571f5a2feac03913dd4597ed5bcbafb357231f1e6bc9e40b02afc45f219200f8ff6bbcb: 1473933586 +54afaac7d7c7ecc0734c301c11a26adc7ce2bc053024: 55786877 +41b3170b207e66b1eae1a84a03db26a45ffad81c013c81c388c1ac0a: 483987601 +316c92: 1995393960 +01213c6faa1234eaef8d5ee0b927ddd8605a8de4f4d720c444e096722e6c8767595ef3820526: 1182101158 +d670c662978e4c5ece07962b93053352b169f3d4b4545b53d0857ab8cb4b2d2496e4f4536d5c0c7108e72de62b3b26bc: 1119328736 +287ec6811624273828d5cbdc3ce6: 365621557 +3ab555ecc1b687ea68b7966637cb8b6aae6ea087f573f1f54c63f9b6191cf5985794ef1fc2: 1637375338 +4922ba19c153457e4077e12b6f42c049e9fe2888e4c263357a9f6e43: 1039229035 +8ba8cf151a952b8e86acc8397bcbc6f6c0f22d7b2d09de2977c6de095e7d: 76296185 +9a2aad22a067750f65336ad874f28f6b8fe6660fffb9abee57a52ea856e9db84346363401d3082e3cc38d1: 2012818523 +1df37db5ca4759a41cab515d4f8c7be4864c76b647fa60a8397ae72bd2: 511628245 +448501e579e9cf595961317530eecd55b32559d1b9c889b06440e9903cf5c0b6e6f648aa6304e451: 1570896729 +e595ee62d9e5253dc404615149ef5ce7: 1194816858 +038aca8adbfa2379e2785daf82adb536234acd9606ca4b3c3a: 732478070 +6eb1: 1007515829 +73653d90fc4a86080ecc3aa7595dccbf29c642cfebadb5e3402f113b9237b48027079324f4c1ae: 660466134 +3d500f41c70beef0184fc70ef0f6c462c45071bc2f68fdce97e607418d55813db7ce926194776c5be6: 220327056 +d51387ab1a5b20d1160b063f073dc2853faa7c05da11e5bd0f6c26b1210c718bb7f1f6cfd4a55d6dc8ff: 182882809 +9a13a82a50433497728dc2edb2: 666506043 +afd607619128a8a18471a6c64456eeb947187c8d8c9d831d319c22e3ca91: 1585000330 +88758d3f4db23cefae7e9c1ed1d1da: 353123979 +589ef5e96046a05a08f073e8715d94d932a2343b8c: 241791474 +a15059545c772be2fa34123b9d8a91ff9065232eb23163f5e3b406f5a1617024d36efe06b5a6735d71c5: 501631426 +12fa2ef6582fe8c237ad87124f3c5230d2cd: 182175882 +56328a1fa9ff3a7ffbbeca7bd6b2b3128d5bc87bc9cf: 78183177 +4f72: 1250424524 +78b5b7520420abd83de7d4d8a04b1d778b4879cffbb9037d9a8352c9e8: 1441815315 +6136fb4240624b722118a06eece42a5d9a2c: 1751735639 +3091db355a8bf8ed0a5e: 456038127 +f96dcc2843cafdd4a34c780e1a01338ecda8fcd6655a3d796f243194a5dc63da83541971f1cd5eb7ba35: 212906644 +c0e650b6139390b0c145b71fe8ee: 1717118197 +ccb8845ac4b34d23be99: 414287445 +'49': 636905728 +303ff5f54e: 539528939 +478b8f89622f42d3e52337ef4466202ac98a3f1376924075db75ffcb752655d5e8420468022d2ac1: 40606646 +f599f96da671fc62cefa106dfeffa25e2d68f48e6027087ba6870a9d0393970b3920be: 1567333467 +3cb95e3bf12bde6957569d0dca890a22cd5e4afa94f2d15b1e61e01d381508fc0537fcb2eb: 1862269908 +70da2cacbdc197ce12be3d3eb480fe01a13ebcff83d26ce98e49d2: 128376115 +5c253269f8952c0e2676: 1782203278 +0bcdd137e90916568b: 2039072251 +af0872c2f276902384779c81cf3febfdc72b2566e42aba3aba2738ec8102d693b7032ebc239a8b9471602fa50f1d488b1979: 459687685 +a9f1f80b4b9faac29173d8beef5220e4ad9993f05be5f950245e4b0035a98f58d830defce2dc: 1696241422 +d2ee666571ece020bd8137c39580b1abc9fb4a89991ce0aa7822b5d960d3e8ca09: 91351839 +3fdda8adf582eaf51ea40b5b9122515502612470c2bb66a9c5558201e2c84694a8cbe092fcde6a6dd942dc5eb8ede08b: 656571790 +ae: 15195598 +269741a9d8b944cb815bc9fd6db15d3f5106b7811541043a7448a32b7d8f17: 974353071 +603770f07d4571f83f2974aae0be42041c18e2be7ce0138df8d0: 1366051146 +e07d82b7f9cd8c5fdc: 874617480 +8d83a1f774bcd95a8aae: 1687774301 +ea5f6d68f75dbc5fdddffc2752f33ce4cfdcc5aeb5ce03851e61a61c6724d55ff4dc66442a: 1377712884 +93b0bfedc1910576f173b5646490474d2042a2387f6279416e7faa7df15aa56ea0da4cd8416a: 154691814 +bedea59011b243b84bda7466c37e88df8b82: 694569716 +7c79b01e18f95a30cd57b9fc: 683762260 +5ab349cb2b167c9760365e0aac391c5496d76ae7: 1689745541 +438328ca1f61a489cd6e: 1219960715 +969b44ec30748b198eda45256b3de53cff82485e5bc3f107c23b009440fe2fa40d1e8bd7aa: 1151588521 +e374274d01c6d3a83cc4: 1515901746 +ea9533b35a1c83ba4e259c5a56e36b8a32c632408210ddef474e: 245084129 +6a6b1b7b0f427cfbc2afbd9b7ec3: 1838557419 +7afe477c5fbc7d0ef167bec6b0309fb2996f172e06b176: 210109733 +189ed9a4acf0d419045b67dec4: 399970956 +aa9f60bd2ff23370628570a6f9cb7a57e06509920e3cbf8a: 1050621436 +daf9a25a8fe0775b99e2e394e03e95da53dc59403edf08ecbec72c4491411fabdb5f4410049c46f919fe48ad9dc6d6: 935832483 +7299937593973d8b988198116f77b0c41f8b80009d1755604925783aee66bdc5ad0b6a81dcc4c7763819dce5201c: 38658330 +254dd86414a411e4dd5bd74a21dfbebf397fdcfa56c802: 56596741 +42a11ea77233f9115bbd5e2b12a7fa171fdb05fbe511d67d94000e9724e17608cc798a1afe: 727298738 +2f79708c54e9e2f16dd82f57789c: 1608939254 +1fdafdbf1129dbe52b4e6570e120cc2dac025b42c9a486bd6b: 1938076612 +6b532b8d568c44711b7f918c2e360781600142de1b1164a32297d52fd7ff65e985360348b90201ab: 347982673 +a65245aeee7900178b1bbc3cc87191e72db441cfd5b49f400763982d906e32ab92b4de417f: 1991069376 +d839d6da17be0b45a1734153a1b5f8e5d02ea0c28ff299234526b0ccb15c72f5775dff8dd7: 1487367662 +2156419fc3adc5178b6d679619f08da1a90fb8f4500c5be52cf840428f1229884032f3fcd242e58e: 548015352 +efc22626e5621243: 1359916413 +04ed6ee27eb54a7bb15edb1f45fdc7035b1bbcaabeac8213d5bc9dacbc0d8c70: 1585988197 +'7199': 948999441 +0a0472f5087f23a623b5: 454820883 +6e4a104a6eb44bc3a6bf1107c06058ac9ca9fc7288e73cfad82b98d66feafc06df182ac732a1d11f9feec8: 1024970306 +42e22b983db5ec1fcd3a945d2fae53: 1649388059 +749ae1c0355da123926654c40885aab25eb5: 1046362064 +1dc4a8c65d67aa3863d6f2: 1072437948 +4e: 1194030303 +bd7fc32f3e1bdcefed3ebd8359b2eea1fc8b25486609aa86d87abe0e37aaa0: 2116128184 +c5aadfa7468e6a82f99c62876a1047bfebba9d36b4d4f97aff51b2540db8: 1415072913 +c450: 794638503 +8796f9ddabaf101ffb34542bd424bcc8a2b47822bc4b2010cc5e8799d19e3e7e438618: 1744306497 +7ce00be84fd51986bfb7faec522c7eb2c3e1a4e084d5: 1088708292 +89a600cf778d1ad0713e28cc660c6756507b87a4e6f347d430f333623f: 695948817 +c50b8ae10dcc2e29: 1193146886 +424793cb6fc0c9cb653d2bdd9248: 1395788399 +c979e881144c0c33b9: 1933771153 +185d70e249be25267445fb3ba544898e277384d09feaf021e89b44e1a01ba6eb743c: 1568080655 +3cc0192772e43af59ea642542a5cf830584ed6efb2122b66: 28535105 +6f8db868686de7c175: 277293610 +30985c01c3a94f39f4616be2a013a95b94b5b4f0a302f1595b8f589cce6da8: 1741848048 +d907ab072c35fd33: 2141142045 +7e8e3d3587c4adaa1c4bcb5b78b7c5c337c4b4492f4a071283fd649c47cd7e: 1010112831 diff --git a/spec/lib/sequel/extensions/register_advisory_lock_test.rb b/spec/lib/sequel/extensions/register_advisory_lock_test.rb new file mode 100644 index 000000000..3115c64be --- /dev/null +++ b/spec/lib/sequel/extensions/register_advisory_lock_test.rb @@ -0,0 +1,89 @@ +require "sequel/extensions/pg_advisory_lock" + +describe Sequel::Postgres::PgAdvisoryLock do + subject { Sequel::Model.db } + + describe "#register_advisory_lock" do + let(:supported_lock_functions) do + [ + :pg_advisory_lock, + :pg_try_advisory_lock, + :pg_advisory_xact_lock, + :pg_try_advisory_xact_lock + ] + end + + let(:default_lock_function) { :pg_advisory_lock } + + before :all do + Sequel::Model.db.extension(:pg_advisory_lock) + end + + before do + subject.registered_advisory_locks.clear + end + + it "base check" do + lock_name = :test_lock + + expect(subject.registered_advisory_locks[lock_name]).to be nil + subject.register_advisory_lock(lock_name) + expect(default_lock_function).to eq subject.registered_advisory_locks[lock_name].fetch(:lock_function) + end + + it "should register locks for all supported PostgreSQL functions" do + supported_lock_functions.each do |lock_function| + lock_name = "#{lock_function}_test".to_sym + + expect(subject.registered_advisory_locks[lock_name]).to be nil + subject.register_advisory_lock(lock_name, lock_function) + expect(lock_function).to eq subject.registered_advisory_locks[lock_name].fetch(:lock_function) + end + end + + it "should prevent specifying not supported PostgreSQL function as lock type" do + lock_name = :not_supported_lock_function_test + lock_function = :not_supported_lock_function + + expect { subject.register_advisory_lock(lock_name, lock_function) }.to raise_error(Sequel::Error, /Invalid lock function/) + end + + it "should prevent registering multiple locks with same name and different functions" do + lock_name = :multiple_locks_with_same_name_test + subject.register_advisory_lock(lock_name, supported_lock_functions[0]) + + expect { subject.register_advisory_lock(lock_name, supported_lock_functions[1]) }.to raise_error(Sequel::Error, /Lock with name .+ is already registered/) + end + + it "should allow registering multiple locks with same name and same functions" do + lock_name = :multiple_locks_with_same_name_test + subject.register_advisory_lock(lock_name, supported_lock_functions[0]) + + expect { subject.register_advisory_lock(lock_name, supported_lock_functions[0]) }.to_not raise_error + end + + it "registered locks must have different lock keys" do + quantity = 100 + quantity.times do |index| + lock_name = "test_lock_#{index}".to_sym + subject.register_advisory_lock(lock_name) + end + + expect(quantity).to eq subject.registered_advisory_locks.size + all_keys = subject.registered_advisory_locks.values.map { |v| v.fetch(:key) } + expect(all_keys.size).to eq all_keys.uniq.size + end + + it "mapping between lock name and lock key must be constant" do + expect(subject.registered_advisory_locks).to be_empty + + lock_names_keys_mapping = YAML.load_file(File.join(File.dirname(__FILE__), "lock_names_keys.yml")) + + lock_names_keys_mapping.each do |lock_name, valid_lock_key| + lock_name = lock_name.to_sym + subject.register_advisory_lock(lock_name) + expect(valid_lock_key).to eq subject.registered_advisory_locks[lock_name].fetch(:key) + end + end + end +end From dfa457a3affcb0283ce4fbf158f19da9ff5a2856 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Tue, 2 Apr 2024 11:55:25 +1100 Subject: [PATCH 52/69] chore: use semantic logger for console --- script/dev/console.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/script/dev/console.rb b/script/dev/console.rb index fca2f77fb..16b095d39 100755 --- a/script/dev/console.rb +++ b/script/dev/console.rb @@ -3,7 +3,7 @@ Bundler.require require "sequel" -require "logger" +require "semantic_logger" require "fileutils" require "pact_broker/initializers/database_connection" @@ -24,13 +24,14 @@ FileUtils.mkdir_p(File.absolute_path(File.dirname(URI(database_connection_string).path))) end -logger = Logger.new($stdout) -logger.level = Logger::DEBUG +SemanticLogger.default_level = :info +SemanticLogger.add_appender(io: $stdout) +logger = SemanticLogger["console"] database_opts = { logger: logger, encoding: "utf8", - sql_log_level: "debug" + sql_log_level: "trace" } puts "Connecting to #{database_connection_string}" From ff72d03c567b76939db8d7450fad2a1d20879b5b Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Tue, 2 Apr 2024 11:57:59 +1100 Subject: [PATCH 53/69] feat: reduce contention when updating the contract_data_updated_at field for integrations (#671) --- lib/pact_broker/integrations/repository.rb | 39 ++++++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/lib/pact_broker/integrations/repository.rb b/lib/pact_broker/integrations/repository.rb index 7568eed66..9890171a6 100644 --- a/lib/pact_broker/integrations/repository.rb +++ b/lib/pact_broker/integrations/repository.rb @@ -50,18 +50,43 @@ def delete(consumer_id, provider_id) # @param [PactBroker::Domain::Pacticipant, nil] consumer the consumer for the integration, or nil if for a provider-only event (eg. Pactflow provider contract published) # @param [PactBroker::Domain::Pacticipant] provider the provider for the integration def set_contract_data_updated_at(consumer, provider) - Integration - .where({ consumer_id: consumer&.id, provider_id: provider.id }.compact ) - .update(contract_data_updated_at: Sequel.datetime_class.now) + set_contract_data_updated_at_for_multiple_integrations([OpenStruct.new(consumer: consumer, provider: provider)]) end - # Sets the contract_data_updated_at for the integrations as specified by an array of objects which each have a consumer and provider - # @param [Array] where each object has a consumer and a provider + # Sets the contract_data_updated_at for the integrations as specified by an array of objects which each have a consumer and provider. + # + # The contract_data_updated_at attribute is only ever used for ordering the list of integrations on the index page of the *Pact Broker* UI, + # so that the most recently updated integrations (the ones you're likely working on) are showed at the top of the first page. + # There is often contention around updating it however, which can cause deadlocks, and slow down API responses. + # Because it's not a critical field (eg. it won't change any can-i-deploy results), the easiest way to reduce this contention + # is to just not update it if the row is locked, because if it is locked, the value of contract_data_updated_at is already + # going to be a date from a few seconds ago, which is perfectly fine for the purposes for which we are using the value. + # + # Notes on SKIP LOCKED: + # SKIP LOCKED is only supported by Postgres. + # When executing SELECT ... FOR UPDATE SKIP LOCKED, the SELECT will run immediately, not waiting for any other transactions, + # and only return rows that are not already locked by another transaction. + # The FOR UPDATE is required to make it work this way - SKIP LOCKED on its own does not work. + # + # @param [Array] where each object MAY have a consumer and does have a provider (for Pactflow provider contract published there is no consumer) def set_contract_data_updated_at_for_multiple_integrations(objects_with_consumer_and_provider) - consumer_and_provider_ids = objects_with_consumer_and_provider.collect{ | object | [object.consumer.id, object.provider.id] }.uniq + consumer_and_provider_ids = objects_with_consumer_and_provider.collect{ | object | { consumer_id: object.consumer&.id, provider_id: object.provider.id }.compact }.uniq + + # MySQL doesn't support an UPDATE with a subquery. FFS. Really need to do a major version release and delete the support code. + criteria = if Integration.dataset.supports_skip_locked? + integration_ids_to_update = Integration + .select(:id) + .where(Sequel.|(*consumer_and_provider_ids)) + .for_update + .skip_locked + { id: integration_ids_to_update } + else + Sequel.|(*consumer_and_provider_ids) + end + Integration - .where([:consumer_id, :provider_id] => consumer_and_provider_ids) + .where(criteria) .update(contract_data_updated_at: Sequel.datetime_class.now) end end From 571e7db841a159c736c016ed7b8cde6a6de0d112 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 24 Apr 2024 16:59:55 +1000 Subject: [PATCH 54/69] chore: move matrix docs to docs dir --- MATRIX.md => docs/developer/matrix.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename MATRIX.md => docs/developer/matrix.md (100%) diff --git a/MATRIX.md b/docs/developer/matrix.md similarity index 100% rename from MATRIX.md rename to docs/developer/matrix.md From 60245c9e37c13905eef2f4f5b90a08e80c43f49b Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Wed, 24 Apr 2024 17:01:28 +1000 Subject: [PATCH 55/69] chore: add rack docs --- docs/developer/rack.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 docs/developer/rack.md diff --git a/docs/developer/rack.md b/docs/developer/rack.md new file mode 100644 index 000000000..ff3d5e613 --- /dev/null +++ b/docs/developer/rack.md @@ -0,0 +1,11 @@ +# Rack + +https://medium.com/quick-code/rack-middleware-vs-rack-application-vs-rack-the-gem-vs-rack-the-architecture-912cd583ed24 +https://github.com/rack/rack/blob/main/SPEC.rdoc +https://www.rubyguides.com/2018/09/rack-middleware/ + + +* Responds to `call` +* Accepts a hash of parameters +* Returns an array where the first item is the http status, the second is a hash of headers, and the third is an object that responds to `each` (or `call`) that provides the body (99% of the time it's an array of length 1 with a string) + From c43d9184763f503bbf697112eac04fe12b72afb0 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Tue, 30 Apr 2024 04:13:41 +0100 Subject: [PATCH 56/69] chore(deps): update psych to 5.x (#674) --- pact_broker.gemspec | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pact_broker.gemspec b/pact_broker.gemspec index bb28101bf..17d2bf5c6 100644 --- a/pact_broker.gemspec +++ b/pact_broker.gemspec @@ -50,7 +50,8 @@ Gem::Specification.new do |gem| gem.license = "MIT" gem.add_runtime_dependency "json", "~> 2.3" - gem.add_runtime_dependency "psych", "~> 4.0" # TODO identify breaking changes and see if we can use 5 + # gem.add_runtime_dependency "psych", "~> 4.0" # TODO identify breaking changes and see if we can use 5 + gem.add_runtime_dependency "psych", "~> 5.0" # TODO identify breaking changes and see if we can use 5 gem.add_runtime_dependency "roar", "~> 1.1" gem.add_runtime_dependency "dry-validation", "~> 1.8" gem.add_runtime_dependency "reform", "~> 2.6" From 5ac4adf0ca7a3729d1f9bdd28e3692d2a348309b Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Tue, 30 Apr 2024 13:14:22 +1000 Subject: [PATCH 57/69] chore: clean comments --- pact_broker.gemspec | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pact_broker.gemspec b/pact_broker.gemspec index 17d2bf5c6..3fc710787 100644 --- a/pact_broker.gemspec +++ b/pact_broker.gemspec @@ -50,8 +50,7 @@ Gem::Specification.new do |gem| gem.license = "MIT" gem.add_runtime_dependency "json", "~> 2.3" - # gem.add_runtime_dependency "psych", "~> 4.0" # TODO identify breaking changes and see if we can use 5 - gem.add_runtime_dependency "psych", "~> 5.0" # TODO identify breaking changes and see if we can use 5 + gem.add_runtime_dependency "psych", "~> 5.0" gem.add_runtime_dependency "roar", "~> 1.1" gem.add_runtime_dependency "dry-validation", "~> 1.8" gem.add_runtime_dependency "reform", "~> 2.6" From 0b17d05a349761be49197a6873a0a434cb372528 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Thu, 2 May 2024 12:27:20 +1000 Subject: [PATCH 58/69] refactor: declare custom sequel models by excluding columns rather than including columns (#676) --- lib/pact_broker/domain/version.rb | 5 +++-- lib/pact_broker/integrations/integration.rb | 5 ++++- lib/pact_broker/webhooks/execution.rb | 16 ++++++++-------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/pact_broker/domain/version.rb b/lib/pact_broker/domain/version.rb index 7e053710a..7dc3733c2 100644 --- a/lib/pact_broker/domain/version.rb +++ b/lib/pact_broker/domain/version.rb @@ -4,9 +4,10 @@ module PactBroker module Domain - VERSION_COLUMNS = [:id, :number, :repository_ref, :pacticipant_id, :order, :created_at, :updated_at, :build_url] + class Version < Sequel::Model + VERSION_COLUMNS = Sequel::Model.db.schema(:versions).collect(&:first) - [:branch] # do not include the branch column, as we now have a branches table + set_dataset(Sequel::Model.db[:versions].select(*VERSION_COLUMNS.collect{ | column | Sequel.qualify(:versions, column) })) - class Version < Sequel::Model(Sequel::Model.db[:versions].select(*VERSION_COLUMNS.collect{ | column | Sequel.qualify(:versions, column) })) set_primary_key :id plugin :timestamps, update_on_create: true diff --git a/lib/pact_broker/integrations/integration.rb b/lib/pact_broker/integrations/integration.rb index 860d7b9ce..9f5fa4ea1 100644 --- a/lib/pact_broker/integrations/integration.rb +++ b/lib/pact_broker/integrations/integration.rb @@ -14,7 +14,10 @@ module Integrations # When the view was migrated to be a table (in db/migrations/20211102_create_table_temp_integrations.rb and the following migrations) # the columns had to be maintained for backwards compatiblity. # They are not used by the current code, however. - class Integration < Sequel::Model(Sequel::Model.db[:integrations].select(:id, :consumer_id, :provider_id, :created_at, :contract_data_updated_at)) + class Integration < Sequel::Model + INTEGRATION_COLUMNS = Sequel::Model.db.schema(:integrations).collect(&:first) - [:consumer_name, :provider_name] + set_dataset(Sequel::Model.db[:integrations].select(*INTEGRATION_COLUMNS)) + set_primary_key :id plugin :insert_ignore, identifying_columns: [:consumer_id, :provider_id] associate(:many_to_one, :consumer, :class => "PactBroker::Domain::Pacticipant", :key => :consumer_id, :primary_key => :id) diff --git a/lib/pact_broker/webhooks/execution.rb b/lib/pact_broker/webhooks/execution.rb index a54d7e1e6..e91de2b63 100644 --- a/lib/pact_broker/webhooks/execution.rb +++ b/lib/pact_broker/webhooks/execution.rb @@ -2,14 +2,14 @@ module PactBroker module Webhooks - class Execution < Sequel::Model( - Sequel::Model.db[:webhook_executions].select( - Sequel[:webhook_executions][:id], - :triggered_webhook_id, - :success, - :logs, - Sequel[:webhook_executions][:created_at]) - ) + class Execution < Sequel::Model(:webhook_executions) + # Ignore the columns that were used before the TriggeredWebhook class existed. + # It used to go Webhook -> Execution, and now it goes Webhook -> TriggeredWebhook -> Execution + # If we ever release a major version where we drop unused columns, those columns could be deleted. + EXECUTION_COLUMNS = Sequel::Model.db.schema(:webhook_executions).collect(&:first) - [:webhook_id, :pact_publication_id, :consumer_id, :provider_id] + + set_dataset(Sequel::Model.db[:webhook_executions].select(*EXECUTION_COLUMNS)) + set_primary_key :id plugin :timestamps From 079d555761e7a7542702f3e07d7bf1f5293b2b7a Mon Sep 17 00:00:00 2001 From: bethesque Date: Fri, 10 May 2024 05:50:17 +0000 Subject: [PATCH 59/69] chore(release): version 2.110.0 --- CHANGELOG.md | 17 +++++++++++++++++ lib/pact_broker/version.rb | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b85e8f62..8d14d7d0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ + +### v2.110.0 (2024-04-02) + +#### Features + +* reduce contention when updating the contract_data_updated_at field for integrations (#671) ([ff72d03c](/../../commit/ff72d03c)) +* support consumer version selector for all branches (#667) ([34334ca8](/../../commit/34334ca8)) + +* **clean** + * use postgres advisory locks to ensure only one process can run a clean at a time (#672) ([637c25fa](/../../commit/637c25fa)) + +#### Bug Fixes + +* use for_all_tag_heads instead of latest_by_consumer_tag when fetching wip by branch ([14148a34](/../../commit/14148a34)) +* optimise WIP pacts by using branch/tag heads (#668) ([871209e1](/../../commit/871209e1)) +* improve performance of WIP pacts by using branch heads instead of calculating latest pact for branch ([f9705583](/../../commit/f9705583)) + ### v2.109.1 (2024-02-21) diff --git a/lib/pact_broker/version.rb b/lib/pact_broker/version.rb index beb535b35..bd46862c7 100644 --- a/lib/pact_broker/version.rb +++ b/lib/pact_broker/version.rb @@ -1,3 +1,3 @@ module PactBroker - VERSION = "2.109.1" + VERSION = "2.110.0" end From 758fef7f8894d45324f27d87cf747bb48aa7be1c Mon Sep 17 00:00:00 2001 From: Sauparna Gupta Date: Tue, 21 May 2024 09:55:10 +0530 Subject: [PATCH 60/69] chore: updating dependencies (#677) --- .ruby-version | 2 +- Dockerfile | 2 +- docker-compose-dev-postgres.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.ruby-version b/.ruby-version index a3ec5a4bd..351227fca 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2 +3.2.4 diff --git a/Dockerfile b/Dockerfile index d30b4ad1f..34402ad3d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:3.2.1-alpine3.17 +FROM ruby:3.2.4-alpine3.18 WORKDIR /home diff --git a/docker-compose-dev-postgres.yml b/docker-compose-dev-postgres.yml index b992d2d30..e48828360 100644 --- a/docker-compose-dev-postgres.yml +++ b/docker-compose-dev-postgres.yml @@ -34,7 +34,7 @@ services: - ./Rakefile:/home/Rakefile shell: - image: ruby:2.5.3-alpine + image: ruby:3.2.4-alpine depends_on: - pact-broker entrypoint: /bin/sh From c59458018b6b8f238681701965bbc5d056b632c9 Mon Sep 17 00:00:00 2001 From: Voon Wong Date: Fri, 21 Jun 2024 10:04:47 +1000 Subject: [PATCH 61/69] feat: search pacticipants by display_name --- lib/pact_broker/pacticipants/repository.rb | 11 ++++++++++- spec/lib/pact_broker/pacticipants/repository_spec.rb | 12 ++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/pact_broker/pacticipants/repository.rb b/lib/pact_broker/pacticipants/repository.rb index 4b1da03f5..26f0e74e8 100644 --- a/lib/pact_broker/pacticipants/repository.rb +++ b/lib/pact_broker/pacticipants/repository.rb @@ -102,7 +102,16 @@ def handle_multiple_pacticipants_found(name, pacticipants) def search_by_name(pacticipant_name) terms = pacticipant_name.split.map { |v| v.gsub("_", "\\_") } - string_match_query = Sequel.|( *terms.map { |term| Sequel.ilike(Sequel[:pacticipants][:name], "%#{term}%") }) + columns = [:name, :display_name] + string_match_query = Sequel.|( + *terms.map do |term| + Sequel.|( + *columns.map do |column| + Sequel.ilike(Sequel[:pacticipants][column], "%#{term}%") + end + ) + end + ) scope_for(PactBroker::Domain::Pacticipant).where(string_match_query) end diff --git a/spec/lib/pact_broker/pacticipants/repository_spec.rb b/spec/lib/pact_broker/pacticipants/repository_spec.rb index 448971a27..7509c479f 100644 --- a/spec/lib/pact_broker/pacticipants/repository_spec.rb +++ b/spec/lib/pact_broker/pacticipants/repository_spec.rb @@ -216,8 +216,8 @@ module Pacticipants before do td - .create_consumer(consumer_name) - .create_consumer(provider_name) + .create_consumer(consumer_name, { display_name: "Pretty Consumer" }) + .create_consumer(provider_name, { display_name: "Fancy Provider" }) end context "when there is a consumer/provider name which matches the search term" do @@ -242,6 +242,14 @@ module Pacticipants searched_dataset = Repository.new.search_by_name "TEST" expect(searched_dataset.collect(&:name)).to include(*[consumer_name, provider_name]) end + + it "searches by display_name" do + searched_dataset = Repository.new.search_by_name "Pretty" + expect(searched_dataset.collect(&:name)).to eq([consumer_name]) + + searched_dataset = Repository.new.search_by_name "Fancy" + expect(searched_dataset.collect(&:name)).to eq([provider_name]) + end end context "when there is NO consumer/provider name which matches the search term" do From ff3f84e2a620346e95a52a3cab2f62bd47946d02 Mon Sep 17 00:00:00 2001 From: Sauparna Gupta Date: Wed, 3 Jul 2024 12:32:12 +0530 Subject: [PATCH 62/69] feat: add new label api (#703) * chore: wip/adding a new labels resource to list all labels * chore: wip/added pagination to labels response and refactored the response * chore: missed a file * chore: updated the spec * chore: updated the decorator and wrote a spec for repository * chore: added labels decorator spec * fix: rubocop failure * fix: fixed the failing test * fix: fixed the wrong syntax * test: fixed another failing test * chore: added test for service layer of Label service * chore: added spacing * chore: added spec for paginated response --- lib/pact_broker/api.rb | 3 ++ .../api/decorators/label_decorator.rb | 32 ++++++++------ .../api/decorators/labels_decorator.rb | 23 ++++++++++ lib/pact_broker/api/resources/labels.rb | 37 ++++++++++++++++ lib/pact_broker/labels/repository.rb | 5 +++ lib/pact_broker/labels/service.rb | 4 ++ spec/features/get_labels_spec.rb | 36 ++++++++++++++++ .../api/decorators/labels_decorator_spec.rb | 43 +++++++++++++++++++ .../lib/pact_broker/labels/repository_spec.rb | 35 +++++++++++++++ spec/lib/pact_broker/labels/service_spec.rb | 12 +++++- spec/support/all_routes_spec_support.yml | 1 + 11 files changed, 217 insertions(+), 14 deletions(-) create mode 100644 lib/pact_broker/api/decorators/labels_decorator.rb create mode 100644 lib/pact_broker/api/resources/labels.rb create mode 100644 spec/features/get_labels_spec.rb create mode 100644 spec/lib/pact_broker/api/decorators/labels_decorator_spec.rb diff --git a/lib/pact_broker/api.rb b/lib/pact_broker/api.rb index 70c1346af..26a674a00 100644 --- a/lib/pact_broker/api.rb +++ b/lib/pact_broker/api.rb @@ -85,6 +85,9 @@ def self.build_api(application_context = PactBroker::ApplicationContext.default_ add ["pacticipants", :pacticipant_name], Api::Resources::Pacticipant, {resource_name: "pacticipant"} add ["pacticipants", :pacticipant_name, "labels", :label_name], Api::Resources::Label, {resource_name: "pacticipant_label"} + # Labels + add ["labels"], Api::Resources::Labels, {resource_name: "labels"} + # Versions add ["pacticipants", :pacticipant_name, "versions"], Api::Resources::Versions, {resource_name: "pacticipant_versions"} add ["pacticipants", :pacticipant_name, "branches", :branch_name, "versions"], Api::Resources::BranchVersions, {resource_name: "pacticipant_branch_versions"} diff --git a/lib/pact_broker/api/decorators/label_decorator.rb b/lib/pact_broker/api/decorators/label_decorator.rb index 009df5d1c..098c0bcc1 100644 --- a/lib/pact_broker/api/decorators/label_decorator.rb +++ b/lib/pact_broker/api/decorators/label_decorator.rb @@ -11,20 +11,26 @@ class LabelDecorator < BaseDecorator include Timestamps - link :self do | options | - { - title: "Label", - name: represented.name, - href: label_url(represented, options[:base_url]) - } - end + # This method is overridden to conditionally render the links based on the user_options + def to_hash(options) + hash = super + + unless options.dig(:user_options, :hide_label_decorator_links) + hash[:_links] = { + self: { + title: "Label", + name: represented.name, + href: label_url(represented, options.dig(:user_options, :base_url)) + }, + pacticipant: { + title: "Pacticipant", + name: represented.pacticipant.name, + href: pacticipant_url(options.dig(:user_options, :base_url), represented.pacticipant) + } + } + end - link :pacticipant do | options | - { - title: "Pacticipant", - name: represented.pacticipant.name, - href: pacticipant_url(options.fetch(:base_url), represented.pacticipant) - } + hash end end end diff --git a/lib/pact_broker/api/decorators/labels_decorator.rb b/lib/pact_broker/api/decorators/labels_decorator.rb new file mode 100644 index 000000000..b668f1c92 --- /dev/null +++ b/lib/pact_broker/api/decorators/labels_decorator.rb @@ -0,0 +1,23 @@ +require_relative "base_decorator" +require_relative "pagination_links" +require_relative "label_decorator" +require "pact_broker/domain/label" + +module PactBroker + module Api + module Decorators + class LabelsDecorator < BaseDecorator + collection :entries, :as => :labels, :class => PactBroker::Domain::Label, :extend => PactBroker::Api::Decorators::LabelDecorator, embedded: true + + include PaginationLinks + + link :self do | options | + { + title: "Labels", + href: options.fetch(:resource_url) + } + end + end + end + end +end diff --git a/lib/pact_broker/api/resources/labels.rb b/lib/pact_broker/api/resources/labels.rb new file mode 100644 index 000000000..d6620969a --- /dev/null +++ b/lib/pact_broker/api/resources/labels.rb @@ -0,0 +1,37 @@ +require "pact_broker/api/resources/base_resource" +require "pact_broker/api/decorators/labels_decorator" +require "pact_broker/api/resources/pagination_methods" + +module PactBroker + module Api + module Resources + class Labels < BaseResource + include PaginationMethods + + def content_types_provided + [["application/hal+json", :to_json]] + end + + def allowed_methods + ["GET", "OPTIONS"] + end + + def policy_name + :'labels::labels' + end + + def to_json + decorator_class(:labels_decorator).new(labels).to_json( + **decorator_options( + hide_label_decorator_links: true, + ) + ) + end + + def labels + label_service.get_all_unique_labels(pagination_options) + end + end + end + end +end diff --git a/lib/pact_broker/labels/repository.rb b/lib/pact_broker/labels/repository.rb index 7efe0df3f..aee665261 100644 --- a/lib/pact_broker/labels/repository.rb +++ b/lib/pact_broker/labels/repository.rb @@ -3,6 +3,11 @@ module PactBroker module Labels class Repository + + def get_all_unique_labels pagination_options = {} + PactBroker::Domain::Label.distinct.select(:name).all_with_pagination_options(pagination_options) + end + def create args Domain::Label.new(name: args.fetch(:name), pacticipant: args.fetch(:pacticipant)).save end diff --git a/lib/pact_broker/labels/service.rb b/lib/pact_broker/labels/service.rb index 02ae239fa..af9bc5e6f 100644 --- a/lib/pact_broker/labels/service.rb +++ b/lib/pact_broker/labels/service.rb @@ -9,6 +9,10 @@ module Service extend PactBroker::Repositories + def get_all_unique_labels pagination_options = {} + label_repository.get_all_unique_labels(pagination_options) + end + def create args pacticipant = pacticipant_repository.find_by_name_or_create args.fetch(:pacticipant_name) label_repository.create pacticipant: pacticipant, name: args.fetch(:label_name) diff --git a/spec/features/get_labels_spec.rb b/spec/features/get_labels_spec.rb new file mode 100644 index 000000000..a63bca7b4 --- /dev/null +++ b/spec/features/get_labels_spec.rb @@ -0,0 +1,36 @@ +describe "Get labels" do + + let(:path) { "/labels" } + let(:response_body_hash) { JSON.parse(subject.body, symbolize_names: true) } + + subject { get(path) } + + context "when labels exists" do + before do + td.create_pacticipant("foo") + .create_label("ios") + .create_label("consumer") + .create_pacticipant("bar") + .create_label("ios") + .create_label("consumer") + end + + it "returns a 200 OK" do + expect(subject).to be_a_hal_json_success_response + end + + it "returns the labels in the body" do + expect(response_body_hash[:_embedded][:labels].map { |label| label[:name] }).to contain_exactly("ios", "consumer") + end + + context "with pagination options" do + subject { get(path, { "size" => "1", "page" => "1" }) } + + it "only returns the number of items specified in the page" do + expect(response_body_hash[:_embedded][:labels].size).to eq 1 + end + + it_behaves_like "a paginated response" + end + end +end diff --git a/spec/lib/pact_broker/api/decorators/labels_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/labels_decorator_spec.rb new file mode 100644 index 000000000..f61302e6b --- /dev/null +++ b/spec/lib/pact_broker/api/decorators/labels_decorator_spec.rb @@ -0,0 +1,43 @@ +require "pact_broker/api/decorators/labels_decorator" +require "pact_broker/labels/service" + +module PactBroker + module Api + module Decorators + + describe LabelsDecorator do + before do + td.create_consumer("Foo") + .create_label("consumer") + .create_consumer("Bar") + .create_label("provider") + .create_consumer("Wiffle") + .create_label("provider") + end + + let(:label) do + PactBroker::Labels::Service.get_all_unique_labels + end + + let(:options) { { user_options: { resource_url: "http://example.org/labels", hide_label_decorator_links: true } } } + subject { JSON.parse LabelsDecorator.new(label).to_json(options), symbolize_names: true } + + it "includes the label names" do + expect(subject[:_embedded][:labels].map { |label| label[:name] }).to contain_exactly("provider", "consumer") + end + + it "includes the resource url" do + expect(subject[:_links][:self][:href]).to eq "http://example.org/labels" + end + + it "labels field doest not include any links" do + expect(subject[:_embedded][:labels][0][:_links]).to be_nil + end + + it "doest not include createdAt" do + expect(subject[:_embedded][:labels][0][:createdAt]).to be_nil + end + end + end + end +end diff --git a/spec/lib/pact_broker/labels/repository_spec.rb b/spec/lib/pact_broker/labels/repository_spec.rb index b3e8824e8..41a29d006 100644 --- a/spec/lib/pact_broker/labels/repository_spec.rb +++ b/spec/lib/pact_broker/labels/repository_spec.rb @@ -4,6 +4,41 @@ module PactBroker module Labels describe Repository do + describe ".get_all_unique_labels" do + before do + td.create_pacticipant("bar") + .create_label("ios") + .create_pacticipant("foo") + .create_label("android") + .create_pacticipant("wiffle") + .create_label("ios") + end + + let(:labels_repository) { Repository.new } + + context "when there are no pagination options" do + subject { labels_repository.get_all_unique_labels } + + it "returns all the unique labels" do + expect(subject.collect(&:name)).to contain_exactly("ios", "android") + end + end + + context "when there are pagination options" do + let(:pagination_options) do + { + :page_number => 1, + :page_size => 1 + } + end + subject { labels_repository.get_all_unique_labels pagination_options } + + it "returns paginated unique labels" do + expect(subject.collect(&:name)).to contain_exactly("ios") + end + end + end + describe ".find" do let(:pacticipant_name) { "foo" } diff --git a/spec/lib/pact_broker/labels/service_spec.rb b/spec/lib/pact_broker/labels/service_spec.rb index 39e242a3c..dd8f45970 100644 --- a/spec/lib/pact_broker/labels/service_spec.rb +++ b/spec/lib/pact_broker/labels/service_spec.rb @@ -5,7 +5,17 @@ module Labels describe Service do let(:pacticipant_name) { "foo" } let(:label_name) { "ios" } - let(:options) { {pacticipant_name: pacticipant_name, label_name: label_name}} + let(:options) { { pacticipant_name: pacticipant_name, label_name: label_name } } + let(:pagination_options) { { page_number: 1, page_size: 1 } } + + describe ".get_all_unique_labels" do + subject { Service.get_all_unique_labels(pagination_options) } + + it "calls the labels repository" do + expect_any_instance_of(Labels::Repository).to receive(:get_all_unique_labels).with(pagination_options) + subject + end + end describe ".create" do subject { Service.create(options) } diff --git a/spec/support/all_routes_spec_support.yml b/spec/support/all_routes_spec_support.yml index cbb370047..03e748698 100644 --- a/spec/support/all_routes_spec_support.yml +++ b/spec/support/all_routes_spec_support.yml @@ -9,6 +9,7 @@ requests_which_are_exected_to_have_no_policy_record_or_pacticipant: - environment DELETE - integrations GET - integrations DELETE + - labels GET - matrix GET - metrics GET - pacticipants GET From cb9c668f9c484bd59fbb6bf7baf1e255b0755aeb Mon Sep 17 00:00:00 2001 From: Prateek Sharma Date: Thu, 11 Jul 2024 10:46:21 +0530 Subject: [PATCH 63/69] Fix/no method for pacticipant patch (#704) * test: update pacticipant with embedded labels * fix: added writeable false to pacticipant labels * test: ensuring labels collection is not modified --- .../api/decorators/pacticipant_decorator.rb | 2 +- spec/features/update_pacticipant_spec.rb | 29 +++++++++++++++++++ .../decorators/pacticipant_decorator_spec.rb | 11 +++++-- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/lib/pact_broker/api/decorators/pacticipant_decorator.rb b/lib/pact_broker/api/decorators/pacticipant_decorator.rb index c13814743..b2c97eb0b 100644 --- a/lib/pact_broker/api/decorators/pacticipant_decorator.rb +++ b/lib/pact_broker/api/decorators/pacticipant_decorator.rb @@ -20,7 +20,7 @@ class PacticipantDecorator < BaseDecorator property :main_branch property :latest_version, as: :latestVersion, :class => PactBroker::Domain::Version, extend: PactBroker::Api::Decorators::EmbeddedVersionDecorator, embedded: true, writeable: false - collection :labels, :class => PactBroker::Domain::Label, extend: PactBroker::Api::Decorators::EmbeddedLabelDecorator, embedded: true + collection :labels, :class => PactBroker::Domain::Label, extend: PactBroker::Api::Decorators::EmbeddedLabelDecorator, embedded: true, writeable: false include Timestamps diff --git a/spec/features/update_pacticipant_spec.rb b/spec/features/update_pacticipant_spec.rb index 5f4cc39b7..efd24a525 100644 --- a/spec/features/update_pacticipant_spec.rb +++ b/spec/features/update_pacticipant_spec.rb @@ -72,6 +72,35 @@ it "returns a json body with the updated pacticipant" do expect(subject.headers["Content-Type"]).to eq "application/hal+json;charset=utf-8" end + + context "when request body contains embedded labels" do + context "when labels has values" do + let(:request_body) { { "displayName": "Updated Consumer Name", "_embedded": { "labels": [{ "name": "ios" }, { "name": "consumer" }] } } } + + it "returns a 200 response" do + expect(subject.status).to be 200 + end + + it "should not create the labels for the pacticipant" do + expect(response_body_hash[:_embedded][:labels]).to be_empty + end + + it "only updates pacticipant attribute ignoring the labels" do + expect(response_body_hash[:displayName]).to eq "Updated Consumer Name" + expect{ subject }.to change { + PactBroker::Domain::Label.where(name: "ios").count + }.by(0) + end + end + + context "when labels is empty" do + let(:request_body) {{"displayName": "Updated Consumer Name", "_embedded": { "labels": []}}} + + it "returns a 200 OK" do + expect(subject.status).to be 200 + end + end + end end context "with application/merge-patch+json" do diff --git a/spec/lib/pact_broker/api/decorators/pacticipant_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/pacticipant_decorator_spec.rb index df5562e0e..274b5278d 100644 --- a/spec/lib/pact_broker/api/decorators/pacticipant_decorator_spec.rb +++ b/spec/lib/pact_broker/api/decorators/pacticipant_decorator_spec.rb @@ -12,12 +12,15 @@ module Decorators end describe "from_json" do - let(:pacticipant) { OpenStruct.new } + let(:pacticipant) { OpenStruct.new(labels: [OpenStruct.new(name: "existing_label")]) } let(:decorator) { PacticipantDecorator.new(pacticipant) } let(:hash) do { name: "Foo", - mainBranch: "main" + mainBranch: "main", + labels: [ + {name: "new_label"} + ] } end @@ -25,6 +28,10 @@ module Decorators its(:name) { is_expected.to eq "Foo" } its(:main_branch) { is_expected.to eq "main" } + + it "does not modify the labels collection" do + expect(subject.labels.map(&:name)).to contain_exactly("existing_label") + end end describe "to_json" do From 6729b7f849a2797b8a820569e48cd97157425d24 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Fri, 26 Jul 2024 13:58:26 +0100 Subject: [PATCH 64/69] fix(docs): Update OAS with correct ref to Notice schema --- pact_broker_oas.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pact_broker_oas.yaml b/pact_broker_oas.yaml index 2fc8b397f..3b21373fe 100644 --- a/pact_broker_oas.yaml +++ b/pact_broker_oas.yaml @@ -53,7 +53,7 @@ paths: notices: type: array items: - $ref: '#/components/schemas/notice' + $ref: '#/components/schemas/Notice' "400": description: Validation errror content: From 2f09ef350adf54c0d884feeaed924670435423c3 Mon Sep 17 00:00:00 2001 From: phanindra srungavarapu Date: Mon, 5 Aug 2024 18:59:48 +0530 Subject: [PATCH 65/69] feat/can-i-merge badge (#707) * Implementing the can-i-merge logic and can-i-merge build badge logic * Add badge resource * Fix test build failing due to inconsistent data setup Fixed rubocop warnings * Minor code cleanup * Trying to fix github build * Force installing docker compose, picked the current latest version But maybe it should be picking up based on latest available at runtime * Correctly using docker compose command --------- Co-authored-by: Phanindra Srungavarapu --- lib/pact_broker/api.rb | 1 + lib/pact_broker/api/paths.rb | 3 +- .../api/resources/can_i_merge_badge.rb | 39 ++++++++++++ lib/pact_broker/badges/service.rb | 14 +++++ lib/pact_broker/matrix/service.rb | 26 ++++++++ lib/pact_broker/test/test_data_builder.rb | 11 +++- .../test/run-rake-on-docker-compose-mysql.sh | 4 +- .../run-rake-on-docker-compose-postgres.sh | 4 +- .../api/resources/can_i_merge_badge_spec.rb | 61 +++++++++++++++++++ spec/lib/pact_broker/badges/service_spec.rb | 15 +++++ spec/lib/pact_broker/matrix/service_spec.rb | 40 ++++++++++++ 11 files changed, 211 insertions(+), 7 deletions(-) create mode 100644 lib/pact_broker/api/resources/can_i_merge_badge.rb create mode 100644 spec/lib/pact_broker/api/resources/can_i_merge_badge_spec.rb diff --git a/lib/pact_broker/api.rb b/lib/pact_broker/api.rb index 26a674a00..a9e5f9ef8 100644 --- a/lib/pact_broker/api.rb +++ b/lib/pact_broker/api.rb @@ -95,6 +95,7 @@ def self.build_api(application_context = PactBroker::ApplicationContext.default_ add ["pacticipants", :pacticipant_name, "latest-version", :tag], Api::Resources::LatestVersion, {resource_name: "latest_tagged_pacticipant_version"} add ["pacticipants", :pacticipant_name, "latest-version", :tag, "can-i-deploy", "to", :to], Api::Resources::CanIDeployPacticipantVersionByTagToTag, { resource_name: "can_i_deploy_latest_tagged_version_to_tag" } add ["pacticipants", :pacticipant_name, "latest-version", :tag, "can-i-deploy", "to", :to, "badge"], Api::Resources::CanIDeployPacticipantVersionByTagToTagBadge, { resource_name: "can_i_deploy_latest_tagged_version_to_tag_badge" } + add ["pacticipants", :pacticipant_name, "main-branch", "can-i-merge", "badge"], Api::Resources::CanIMergeBadge, { resource_name: "can_i_merge_badge" } add ["pacticipants", :pacticipant_name, "latest-version"], Api::Resources::LatestVersion, {resource_name: "latest_pacticipant_version"} add ["pacticipants", :pacticipant_name, "versions", :pacticipant_version_number, "tags", :tag_name], Api::Resources::Tag, {resource_name: "pacticipant_version_tag"} add ["pacticipants", :pacticipant_name, "branches"], Api::Resources::PacticipantBranches, {resource_name: "pacticipant_branches"} diff --git a/lib/pact_broker/api/paths.rb b/lib/pact_broker/api/paths.rb index f1ab12e38..81ceb8443 100644 --- a/lib/pact_broker/api/paths.rb +++ b/lib/pact_broker/api/paths.rb @@ -3,11 +3,12 @@ module Api module Paths PACT_BADGE_PATH = %r{^/pacts/provider/[^/]+/consumer/.*/badge(?:\.[A-Za-z]+)?$}.freeze MATRIX_BADGE_PATH = %r{^/matrix/provider/[^/]+/latest/[^/]+/consumer/[^/]+/latest/[^/]+/badge(?:\.[A-Za-z]+)?$}.freeze + CAN_I_MERGE_BADGE_PATH = %r{^/pacticipants/[^/]+/main-branch/can-i-merge/badge(?:\.[A-Za-z]+)?$}.freeze CAN_I_DEPLOY_TAG_BADGE_PATH = %r{^/pacticipants/[^/]+/latest-version/[^/]+/can-i-deploy/to/[^/]+/badge(?:\.[A-Za-z]+)?$}.freeze CAN_I_DEPLOY_BRANCH_ENV_BADGE_PATH = %r{^/pacticipants/[^/]+/branches/[^/]+/latest-version/can-i-deploy/to-environment/[^/]+/badge(?:\.[A-Za-z]+)?$}.freeze VERIFICATION_RESULTS = %r{^/pacts/provider/[^/]+/consumer/[^/]+/pact-version/[^/]+/verification-results/[^/]+} - BADGE_PATHS = [PACT_BADGE_PATH, MATRIX_BADGE_PATH, CAN_I_DEPLOY_TAG_BADGE_PATH, CAN_I_DEPLOY_BRANCH_ENV_BADGE_PATH] + BADGE_PATHS = [PACT_BADGE_PATH, MATRIX_BADGE_PATH, CAN_I_DEPLOY_TAG_BADGE_PATH, CAN_I_DEPLOY_BRANCH_ENV_BADGE_PATH, CAN_I_MERGE_BADGE_PATH] extend self diff --git a/lib/pact_broker/api/resources/can_i_merge_badge.rb b/lib/pact_broker/api/resources/can_i_merge_badge.rb new file mode 100644 index 000000000..846724eda --- /dev/null +++ b/lib/pact_broker/api/resources/can_i_merge_badge.rb @@ -0,0 +1,39 @@ +require "pact_broker/api/resources/base_resource" +require "pact_broker/api/resources/badge_methods" + +module PactBroker + module Api + module Resources + class CanIMergeBadge < BaseResource + include BadgeMethods # This module contains all necessary webmachine methods for badge implementation + + def badge_url + if pacticipant.nil? + # if the pacticipant is nil, we return an error badge url + badge_service.error_badge_url("pacticipant", "not found") + elsif version.nil? + # when there is no main branch version, we return an error badge url + badge_service.error_badge_url("main branch version", "not found") + else + # we call badge_service to build the badge url + badge_service.can_i_merge_badge_url( + version_number: version.number, + deployable: results + ) + end + end + + private + + def results + # can_i_merge returns true or false if the main branch version is compatible with all the integrations + @results ||= matrix_service.can_i_merge(pacticipant: pacticipant, latest_main_branch_version: version) + end + + def version + @version ||= version_service.find_latest_version_from_main_branch(pacticipant) + end + end + end + end +end diff --git a/lib/pact_broker/badges/service.rb b/lib/pact_broker/badges/service.rb index 5a79a5e22..a835585bc 100644 --- a/lib/pact_broker/badges/service.rb +++ b/lib/pact_broker/badges/service.rb @@ -40,6 +40,20 @@ def can_i_deploy_badge_url(tag, environment_tag, label, deployable) build_shield_io_uri(title, status, color) end + def can_i_merge_badge_url(version_number: nil, deployable: nil) + title = "can-i-merge" + status = deployable ? "yes" : "no" + if deployable.nil? + color = "lightgrey" + status = "unknown" + else + color = deployable ? "brightgreen" : "red" + status = version_number + end + # left text is "can-i-merge", right text is the version number + build_shield_io_uri(title, status, color) + end + def error_badge_url(left_text, right_text) build_shield_io_uri(left_text, right_text, "lightgrey") end diff --git a/lib/pact_broker/matrix/service.rb b/lib/pact_broker/matrix/service.rb index 398b6c81c..687be95c5 100644 --- a/lib/pact_broker/matrix/service.rb +++ b/lib/pact_broker/matrix/service.rb @@ -22,6 +22,32 @@ def can_i_deploy(selectors, options = {}) QueryResultsWithDeploymentStatusSummary.new(query_results, DeploymentStatusSummary.new(query_results)) end + def can_i_merge(pacticipant_name: nil, pacticipant: nil, latest_main_branch_version: nil) + # first we find the pacticipant by name (or use the one passed in) if pacticipant is nil + if pacticipant.nil? + pacticipant = pacticipant_service.find_pacticipant_by_name(pacticipant_name) + raise PactBroker::Error.new("No pacticipant found with name '#{pacticipant_name}'") unless pacticipant + else + pacticipant_name = pacticipant.name + end + + # then we find the latest version from the main branch if not passed in + if latest_main_branch_version.nil? + latest_main_branch_version = version_service.find_latest_version_from_main_branch(pacticipant) + raise PactBroker::Error.new("No main branch version found for pacticipant '#{pacticipant_name}'") unless latest_main_branch_version + end + + selectors = PactBroker::Matrix::UnresolvedSelector.from_hash( + pacticipant_name: pacticipant_name, + pacticipant_version_number: latest_main_branch_version.number + ) + + options = { main_branch: true, latest: true, latestby: "cvp" } + query_results = can_i_deploy([selectors], options) + + query_results.deployable? + end + def find selectors, options = {} logger.info "Querying matrix", selectors: selectors, options: options matrix_repository.find(selectors, options) diff --git a/lib/pact_broker/test/test_data_builder.rb b/lib/pact_broker/test/test_data_builder.rb index 59a2270ea..b9126817d 100644 --- a/lib/pact_broker/test/test_data_builder.rb +++ b/lib/pact_broker/test/test_data_builder.rb @@ -165,9 +165,16 @@ def create_tag_with_hierarchy pacticipant_name, pacticipant_version, tag_name def create_pacticipant pacticipant_name, params = {} params.delete(:comment) + version_to_create = params.delete(:version) + repository_url = "https://github.com/#{params[:repository_namespace] || "example-organization"}/#{params[:repository_name] || pacticipant_name}" merged_params = { name: pacticipant_name, repository_url: repository_url }.merge(params) @pacticipant = PactBroker::Domain::Pacticipant.create(merged_params) + + version = create_pacticipant_version(version_to_create, @pacticipant) if version_to_create + main_branch = params[:main_branch] + PactBroker::Versions::BranchVersionRepository.new.add_branch(version, main_branch) if version && main_branch + self end @@ -639,8 +646,6 @@ def fixed_json_content(consumer_name, provider_name, differentiator) }.to_json end - private - def create_pacticipant_version(version_number, pacticipant, params = {}) params.delete(:comment) tag_names = [params.delete(:tag_names), params.delete(:tag_name)].flatten.compact @@ -665,6 +670,8 @@ def create_pacticipant_version(version_number, pacticipant, params = {}) version end + private + def create_deployed_version(uuid: , currently_deployed: , version:, environment_name: , target: nil, created_at: nil) env = find_environment(environment_name) @deployed_version = PactBroker::Deployments::DeployedVersionService.find_or_create(uuid, version, env, target) diff --git a/script/test/run-rake-on-docker-compose-mysql.sh b/script/test/run-rake-on-docker-compose-mysql.sh index e7a3e73c7..48d65faa5 100755 --- a/script/test/run-rake-on-docker-compose-mysql.sh +++ b/script/test/run-rake-on-docker-compose-mysql.sh @@ -1,8 +1,8 @@ #/bin/sh cleanup() { - docker-compose -f docker-compose-ci-mysql.yml down + docker compose -f docker-compose-ci-mysql.yml down } trap cleanup EXIT -docker-compose -f docker-compose-ci-mysql.yml up --exit-code-from tests --abort-on-container-exit --remove-orphans +docker compose -f docker-compose-ci-mysql.yml up --exit-code-from tests --abort-on-container-exit --remove-orphans diff --git a/script/test/run-rake-on-docker-compose-postgres.sh b/script/test/run-rake-on-docker-compose-postgres.sh index 6e88f9bf5..a7076f1ef 100755 --- a/script/test/run-rake-on-docker-compose-postgres.sh +++ b/script/test/run-rake-on-docker-compose-postgres.sh @@ -1,8 +1,8 @@ #/bin/sh cleanup() { - docker-compose -f docker-compose-ci-postgres.yml down + docker compose -f docker-compose-ci-postgres.yml down } trap cleanup EXIT -docker-compose -f docker-compose-ci-postgres.yml up --exit-code-from tests --abort-on-container-exit --remove-orphans +docker compose -f docker-compose-ci-postgres.yml up --exit-code-from tests --abort-on-container-exit --remove-orphans diff --git a/spec/lib/pact_broker/api/resources/can_i_merge_badge_spec.rb b/spec/lib/pact_broker/api/resources/can_i_merge_badge_spec.rb new file mode 100644 index 000000000..06faf32d4 --- /dev/null +++ b/spec/lib/pact_broker/api/resources/can_i_merge_badge_spec.rb @@ -0,0 +1,61 @@ +require "pact_broker/api/resources/can_i_merge_badge" +require "pact_broker/api/resources/base_resource" + +module PactBroker + module Api + module Resources + describe CanIMergeBadge do + before do + allow_any_instance_of(described_class).to receive(:badge_service).and_return(badge_service) + + allow(badge_service). to receive(:can_i_merge_badge_url).and_return("http://badge_url") + allow(badge_service). to receive(:error_badge_url).and_return("http://error_badge_url") + + allow_any_instance_of(CanIMergeBadge).to receive(:pacticipant).and_return(pacticipant) + allow_any_instance_of(CanIMergeBadge).to receive(:version).and_return(version) + allow_any_instance_of(CanIMergeBadge).to receive(:results).and_return(results) + end + + let(:branch_service) { class_double("PactBroker::Versions::BranchService").as_stubbed_const } + let(:badge_service) { class_double("PactBroker::Badges::Service").as_stubbed_const } + + let(:pacticipant) { double("pacticipant") } + let(:version) { double("version", number: "1") } + let(:results) { true } + + let(:path) { "/pacticipants/Foo/main-branch/can-i-merge/badge" } + + subject { get(path) } + + context "when everything is found" do + it "returns a 307" do + expect(subject.status).to eq 307 + end + + it "return the badge URL" do + expect(badge_service). to receive(:can_i_merge_badge_url).with(version_number: "1", deployable: true) + expect(subject.headers["Location"]).to eq "http://badge_url" + end + end + + context "when the pacticipant is not found" do + let(:pacticipant) { nil } + + it "returns an error badge URL" do + expect(badge_service).to receive(:error_badge_url).with("pacticipant", "not found") + expect(subject.headers["Location"]).to eq "http://error_badge_url" + end + end + + context "when the version is not found" do + let(:version) { nil } + + it "returns an error badge URL" do + expect(badge_service).to receive(:error_badge_url).with("main branch version", "not found") + expect(subject.headers["Location"]).to eq "http://error_badge_url" + end + end + end + end + end +end diff --git a/spec/lib/pact_broker/badges/service_spec.rb b/spec/lib/pact_broker/badges/service_spec.rb index f66d71152..209e88ad6 100644 --- a/spec/lib/pact_broker/badges/service_spec.rb +++ b/spec/lib/pact_broker/badges/service_spec.rb @@ -29,6 +29,21 @@ module Badges allow(Service).to receive(:logger).and_return(logger) end + describe "can_i_merge_badge_url" do + let(:version_number) { "abcd1234" } + let(:deployable) { true } + + subject { Service.can_i_merge_badge_url(version_number: version_number, deployable: deployable) } + context "when deployable is true" do + it { is_expected.to eq URI("https://img.shields.io/badge/can--i--merge-abcd1234-brightgreen.svg") } + end + + context "when deployable is false" do + let(:deployable) { false } + it { is_expected.to eq URI("https://img.shields.io/badge/can--i--merge-abcd1234-red.svg") } + end + end + describe "can_i_deploy_badge_url" do subject { Service.can_i_deploy_badge_url("main", "prod", nil, true) } diff --git a/spec/lib/pact_broker/matrix/service_spec.rb b/spec/lib/pact_broker/matrix/service_spec.rb index 277470bea..ac68c2597 100644 --- a/spec/lib/pact_broker/matrix/service_spec.rb +++ b/spec/lib/pact_broker/matrix/service_spec.rb @@ -4,6 +4,46 @@ module PactBroker module Matrix describe Service do + describe "can-i-merge" do + before do + td.create_consumer("A", main_branch: "main_branch", version: "1") + .create_provider("B", main_branch: "main_branch", version: "1") + .create_pact_with_hierarchy("A", "1", "B") + .create_verification(provider_version: "1", number: 1, success: false, branch: "main_branch") + .create_verification(provider_version: "1", number: 2, success: true, branch: "main_branch") + .create_verification(provider_version: "2", number: 3, success: true, branch: "dev") + end + + let(:pacticipant_name_param) { "B" } + + subject { Service.can_i_merge(pacticipant_name: pacticipant_name_param) } + + context "for pacticipant that has verification on it's main branch" do + let(:options) { + { + latest: true, + main_branch: true, + latestby: "cvp" + } + } + + let(:unresolved_selectors) { + [ + PactBroker::Matrix::UnresolvedSelector.new(pacticipant_name: "B", pacticipant_version_number: "1") + ] + } + + it "returns true because the mergeble status is true" do + expect(subject).to be_truthy + end + + it "calls the can_i_deploy method" do + expect(Service).to receive(:can_i_deploy).with(unresolved_selectors, options).and_call_original + subject + end + end + end + describe "validate_selectors" do before do allow(PactBroker::Deployments::EnvironmentService).to receive(:find_by_name).and_return(environment) From c10304116d5cb58c3d9f54dd072204cbe6d256a6 Mon Sep 17 00:00:00 2001 From: phanindra srungavarapu Date: Wed, 7 Aug 2024 12:55:42 +0530 Subject: [PATCH 66/69] chore: updated rubocop.yml to use EnforcedStyleAlignWith: variable ``` # @example EnforcedStyleAlignWith: variable # # bad # # variable = if true # end # # # good # # variable = if true # end ``` --- .rubocop.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.rubocop.yml b/.rubocop.yml index 82b2b8169..468cf638f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -72,6 +72,7 @@ Lint/EachWithObjectArgument: Lint/ElseLayout: Description: 'Check for odd code arrangement in an else block.' Enabled: true + EnforcedStyleAlignWith: variable Lint/EmptyEnsure: Description: 'Checks for empty ensure block.' From b25a6167c5c3e0ee239395f0d8bdeb3e5bb61297 Mon Sep 17 00:00:00 2001 From: Phanindra Srungavarapu Date: Wed, 7 Aug 2024 13:20:34 +0530 Subject: [PATCH 67/69] chore: updated rubocop.yml to use EnforcedStyleAlignWith: variable --- .rubocop.yml | 1 - .../api/resources/can_i_merge_badge.rb | 7 ++----- lib/pact_broker/badges/service.rb | 18 +++++++++++------- .../api/resources/can_i_merge_badge_spec.rb | 2 +- spec/lib/pact_broker/badges/service_spec.rb | 7 ++++--- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 468cf638f..82b2b8169 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -72,7 +72,6 @@ Lint/EachWithObjectArgument: Lint/ElseLayout: Description: 'Check for odd code arrangement in an else block.' Enabled: true - EnforcedStyleAlignWith: variable Lint/EmptyEnsure: Description: 'Checks for empty ensure block.' diff --git a/lib/pact_broker/api/resources/can_i_merge_badge.rb b/lib/pact_broker/api/resources/can_i_merge_badge.rb index 846724eda..964b74957 100644 --- a/lib/pact_broker/api/resources/can_i_merge_badge.rb +++ b/lib/pact_broker/api/resources/can_i_merge_badge.rb @@ -8,7 +8,7 @@ class CanIMergeBadge < BaseResource include BadgeMethods # This module contains all necessary webmachine methods for badge implementation def badge_url - if pacticipant.nil? + if pacticipant.nil? # pacticipant method is defined in BaseResource # if the pacticipant is nil, we return an error badge url badge_service.error_badge_url("pacticipant", "not found") elsif version.nil? @@ -16,10 +16,7 @@ def badge_url badge_service.error_badge_url("main branch version", "not found") else # we call badge_service to build the badge url - badge_service.can_i_merge_badge_url( - version_number: version.number, - deployable: results - ) + badge_service.can_i_merge_badge_url(deployable: results) end end diff --git a/lib/pact_broker/badges/service.rb b/lib/pact_broker/badges/service.rb index a835585bc..a96b53220 100644 --- a/lib/pact_broker/badges/service.rb +++ b/lib/pact_broker/badges/service.rb @@ -40,16 +40,20 @@ def can_i_deploy_badge_url(tag, environment_tag, label, deployable) build_shield_io_uri(title, status, color) end - def can_i_merge_badge_url(version_number: nil, deployable: nil) + def can_i_merge_badge_url(deployable: nil) title = "can-i-merge" - status = deployable ? "yes" : "no" - if deployable.nil? - color = "lightgrey" - status = "unknown" + + # rubocop:disable Layout/EndAlignment + color, status = case deployable + when nil + [ "lightgrey", "unknown" ] + when true + [ "brightgreen", "success" ] else - color = deployable ? "brightgreen" : "red" - status = version_number + [ "red", "failed" ] end + # rubocop:enable Layout/EndAlignment + # left text is "can-i-merge", right text is the version number build_shield_io_uri(title, status, color) end diff --git a/spec/lib/pact_broker/api/resources/can_i_merge_badge_spec.rb b/spec/lib/pact_broker/api/resources/can_i_merge_badge_spec.rb index 06faf32d4..b0ebb8e4d 100644 --- a/spec/lib/pact_broker/api/resources/can_i_merge_badge_spec.rb +++ b/spec/lib/pact_broker/api/resources/can_i_merge_badge_spec.rb @@ -33,7 +33,7 @@ module Resources end it "return the badge URL" do - expect(badge_service). to receive(:can_i_merge_badge_url).with(version_number: "1", deployable: true) + expect(badge_service). to receive(:can_i_merge_badge_url).with(deployable: true) expect(subject.headers["Location"]).to eq "http://badge_url" end end diff --git a/spec/lib/pact_broker/badges/service_spec.rb b/spec/lib/pact_broker/badges/service_spec.rb index 209e88ad6..fc2b451ff 100644 --- a/spec/lib/pact_broker/badges/service_spec.rb +++ b/spec/lib/pact_broker/badges/service_spec.rb @@ -33,14 +33,15 @@ module Badges let(:version_number) { "abcd1234" } let(:deployable) { true } - subject { Service.can_i_merge_badge_url(version_number: version_number, deployable: deployable) } + subject { Service.can_i_merge_badge_url(deployable: deployable) } + context "when deployable is true" do - it { is_expected.to eq URI("https://img.shields.io/badge/can--i--merge-abcd1234-brightgreen.svg") } + it { is_expected.to eq URI("https://img.shields.io/badge/can--i--merge-success-brightgreen.svg") } end context "when deployable is false" do let(:deployable) { false } - it { is_expected.to eq URI("https://img.shields.io/badge/can--i--merge-abcd1234-red.svg") } + it { is_expected.to eq URI("https://img.shields.io/badge/can--i--merge-failed-red.svg") } end end From 1ece72e09172515e06fd08fcc6d861673cd3280c Mon Sep 17 00:00:00 2001 From: YOU54F Date: Mon, 12 Aug 2024 13:57:02 +0000 Subject: [PATCH 68/69] chore(release): version 2.111.0 --- CHANGELOG.md | 13 +++++++++++++ lib/pact_broker/version.rb | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d14d7d0c..bd3ee1c49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ + +### v2.111.0 (2024-07-26) + +#### Features + +* add new label api (#703) ([ff3f84e2](/../../commit/ff3f84e2)) +* search pacticipants by display_name ([c5945801](/../../commit/c5945801)) + +#### Bug Fixes + +* **docs** + * Update OAS with correct ref to Notice schema ([6729b7f8](/../../commit/6729b7f8)) + ### v2.110.0 (2024-04-02) diff --git a/lib/pact_broker/version.rb b/lib/pact_broker/version.rb index bd46862c7..d214aa505 100644 --- a/lib/pact_broker/version.rb +++ b/lib/pact_broker/version.rb @@ -1,3 +1,3 @@ module PactBroker - VERSION = "2.110.0" + VERSION = "2.111.0" end From e84de4f482acac06bfc7fc145a326ab08af92afc Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Mon, 19 Aug 2024 14:39:28 +1000 Subject: [PATCH 69/69] feat: add application/hal+json content type for webhook logs (#679) --- .../triggered_webhook_logs_decorator.rb | 38 +++++++++++++++++++ .../api/resources/triggered_webhook_logs.rb | 6 ++- ...ogs_of_triggered_webhook_get.approved.json | 3 +- ...gered_webhook_logs_decorator.approved.json | 24 ++++++++++++ .../triggered_webhook_logs_decorator_spec.rb | 31 +++++++++++++++ 5 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 lib/pact_broker/api/decorators/triggered_webhook_logs_decorator.rb create mode 100644 spec/fixtures/approvals/triggered_webhook_logs_decorator.approved.json create mode 100644 spec/lib/pact_broker/api/decorators/triggered_webhook_logs_decorator_spec.rb diff --git a/lib/pact_broker/api/decorators/triggered_webhook_logs_decorator.rb b/lib/pact_broker/api/decorators/triggered_webhook_logs_decorator.rb new file mode 100644 index 000000000..c416ca779 --- /dev/null +++ b/lib/pact_broker/api/decorators/triggered_webhook_logs_decorator.rb @@ -0,0 +1,38 @@ +require_relative "base_decorator" + +module PactBroker + module Api + module Decorators + class TriggeredWebhookLogsDecorator < BaseDecorator + class WebhookExecutionDecorator < BaseDecorator + property :success + property :logs + property :created_at, as: :createdAt + end + + + nested :triggeredWebhook, embedded: true do + property :uuid + end + + collection :webhook_executions, as: :executions, :class => PactBroker::Webhooks::Execution, :extend => WebhookExecutionDecorator + + link :self do | options | + { + title: "Triggered webhook logs", + href: options[:resource_url] + } + end + + link :'pb:webhook' do | context | + if represented.webhook + { + href: webhook_url(represented.webhook.uuid, context[:base_url]), + title: "Webhook" + } + end + end + end + end + end +end diff --git a/lib/pact_broker/api/resources/triggered_webhook_logs.rb b/lib/pact_broker/api/resources/triggered_webhook_logs.rb index 5925e8db1..097788bff 100644 --- a/lib/pact_broker/api/resources/triggered_webhook_logs.rb +++ b/lib/pact_broker/api/resources/triggered_webhook_logs.rb @@ -7,7 +7,7 @@ module Resources class TriggeredWebhookLogs < BaseResource def content_types_provided - [["text/plain", :to_text]] + [["text/plain", :to_text], ["application/hal+json", :to_json]] end def allowed_methods @@ -27,6 +27,10 @@ def to_text end end + def to_json + decorator_class(:triggered_webhook_logs_decorator).new(triggered_webhook).to_json(**decorator_options) + end + def policy_name :'webhooks::triggered_webhook' end diff --git a/spec/fixtures/approvals/docs_webhooks_logs_of_triggered_webhook_get.approved.json b/spec/fixtures/approvals/docs_webhooks_logs_of_triggered_webhook_get.approved.json index 4a380ecf9..71523f368 100644 --- a/spec/fixtures/approvals/docs_webhooks_logs_of_triggered_webhook_get.approved.json +++ b/spec/fixtures/approvals/docs_webhooks_logs_of_triggered_webhook_get.approved.json @@ -13,7 +13,8 @@ "response": { "status": 200, "headers": { - "Content-Type": "text/plain;charset=utf-8" + "Content-Type": "text/plain;charset=utf-8", + "Vary": "Accept" }, "body": "logs" } diff --git a/spec/fixtures/approvals/triggered_webhook_logs_decorator.approved.json b/spec/fixtures/approvals/triggered_webhook_logs_decorator.approved.json new file mode 100644 index 000000000..4202297ea --- /dev/null +++ b/spec/fixtures/approvals/triggered_webhook_logs_decorator.approved.json @@ -0,0 +1,24 @@ +{ + "executions": [ + { + "success": true, + "logs": "foo", + "createdAt": "2024-01-01T00:00:00+00:00" + } + ], + "_embedded": { + "triggeredWebhook": { + "uuid": "1234" + } + }, + "_links": { + "self": { + "title": "Triggered webhook logs", + "href": null + }, + "pb:webhook": { + "href": "http://example.org/webhooks/1234", + "title": "Webhook" + } + } +} diff --git a/spec/lib/pact_broker/api/decorators/triggered_webhook_logs_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/triggered_webhook_logs_decorator_spec.rb new file mode 100644 index 000000000..3f7c7c568 --- /dev/null +++ b/spec/lib/pact_broker/api/decorators/triggered_webhook_logs_decorator_spec.rb @@ -0,0 +1,31 @@ +require "pact_broker/api/decorators/triggered_webhook_logs_decorator" + +module PactBroker + module Api + module Decorators + describe TriggeredWebhookLogsDecorator do + let(:triggered_webhook) do + double("PactBroker::Webhooks::TriggeredWebhook", + uuid: "1234", + webhook_executions: [webhook_execution], + webhook: webhook + ) + end + + let(:webhook) { double("webhook", uuid: "1234") } + + let(:webhook_execution) do + instance_double(PactBroker::Webhooks::Execution, logs: "foo", success: true, created_at: td.in_utc { DateTime.new(2024, 1, 1) } ) + end + + let(:user_options) { { base_url: "http://example.org" } } + + subject { TriggeredWebhookLogsDecorator.new(triggered_webhook).to_json(user_options: user_options) } + + it { + Approvals.verify(subject, :name => "triggered_webhook_logs_decorator", format: :json) + } + end + end + end +end