From 6a7b4aef5e60ac3cfbac6d735cf6fdd7bfcbbecc Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Thu, 14 Nov 2024 23:40:09 +0000 Subject: [PATCH 1/2] feat: aggregated provider state endpoint # Provider States - Aggregated view by provider Allowed methods: `GET` Path: `/pacts/provider/{provider}/provider-states` This resource returns a aggregated de-duplicated list of all provider states for a given provider. Provider states are collected from the latest pact on the main branch for any dependant consumers. Example response ```json { "providerStates": [ { "name": "an error occurs retrieving an alligator" }, { "name": "there is an alligator named Mary" }, { "name": "there is not an alligator named Mary" } ] } ``` --- lib/pact_broker/api.rb | 5 + .../decorators/provider_states_decorator.rb | 19 ++++ .../api/resources/provider_states.rb | 38 +++++++ .../doc/views/pact/provider-states.markdown | 28 ++++++ lib/pact_broker/pacts/content.rb | 27 ++++- .../pacts/provider_state_service.rb | 22 +++++ lib/pact_broker/pacts/selector.rb | 4 + lib/pact_broker/pacts/selectors.rb | 4 + lib/pact_broker/services.rb | 9 ++ spec/features/list_provider_states_spec.rb | 44 +++++++++ .../api/resources/provider_states_spec.rb | 98 +++++++++++++++++++ spec/lib/pact_broker/pacts/content_spec.rb | 42 ++++++++ 12 files changed, 335 insertions(+), 5 deletions(-) create mode 100644 lib/pact_broker/api/decorators/provider_states_decorator.rb create mode 100644 lib/pact_broker/api/resources/provider_states.rb create mode 100644 lib/pact_broker/doc/views/pact/provider-states.markdown create mode 100644 lib/pact_broker/pacts/provider_state_service.rb create mode 100644 spec/features/list_provider_states_spec.rb create mode 100644 spec/lib/pact_broker/api/resources/provider_states_spec.rb diff --git a/lib/pact_broker/api.rb b/lib/pact_broker/api.rb index a9e5f9ef8..fca93ad20 100644 --- a/lib/pact_broker/api.rb +++ b/lib/pact_broker/api.rb @@ -44,6 +44,11 @@ def self.build_api(application_context = PactBroker::ApplicationContext.default_ add ["pacts", "provider", :provider_name, "consumer", :consumer_name, "version", :consumer_version_number, "diff", "version", :comparison_consumer_version], Api::Resources::PactContentDiff, {resource_name: "pact_version_diff_by_consumer_version"} add ["pacts", "provider", :provider_name, "consumer", :consumer_name, "pact-version", :pact_version_sha, "diff", "pact-version", :comparison_pact_version_sha], Api::Resources::PactContentDiff, {resource_name: "pact_version_diff_by_pact_version_sha"} + # Provider states + + add ["pacts", "provider", :provider_name, "provider-states"], Api::Resources::ProviderStates, { resource_name: "provider_states" } + + # Verifications add ["pacts", "provider", :provider_name, "consumer", :consumer_name, "pact-version", :pact_version_sha, "verification-results"], Api::Resources::Verifications, {resource_name: "verification_results"} add ["pacts", "provider", :provider_name, "consumer", :consumer_name, "pact-version", :pact_version_sha, "metadata", :metadata, "verification-results"], Api::Resources::Verifications, {resource_name: "verification_results"} diff --git a/lib/pact_broker/api/decorators/provider_states_decorator.rb b/lib/pact_broker/api/decorators/provider_states_decorator.rb new file mode 100644 index 000000000..39460a2d5 --- /dev/null +++ b/lib/pact_broker/api/decorators/provider_states_decorator.rb @@ -0,0 +1,19 @@ +require "pact_broker/api/decorators/base_decorator" + +module PactBroker + module Api + module Decorators + class ProviderStateDecorator < BaseDecorator + camelize_property_names + + property :name + property :params + + end + + class ProviderStatesDecorator < BaseDecorator + collection :providerStates, getter: -> (context) { context[:represented].sort_by(&:name) }, :extend => PactBroker::Api::Decorators::ProviderStateDecorator + end + end + end +end diff --git a/lib/pact_broker/api/resources/provider_states.rb b/lib/pact_broker/api/resources/provider_states.rb new file mode 100644 index 000000000..42efff978 --- /dev/null +++ b/lib/pact_broker/api/resources/provider_states.rb @@ -0,0 +1,38 @@ +require "pact_broker/api/resources/base_resource" +require "pact_broker/api/decorators/provider_states_decorator" + +module PactBroker + module Api + module Resources + class ProviderStates < BaseResource + def content_types_provided + [["application/hal+json", :to_json]] + end + + def allowed_methods + ["GET", "OPTIONS"] + end + + def resource_exists? + !!provider + end + + def to_json + decorator_class(:provider_states_decorator).new(provider_states).to_json(decorator_options) + end + + def policy_name + :'pacts::pacts' + end + + private + + # attr_reader :provider_states + + def provider_states + @provider_states ||= provider_state_service.list_provider_states(provider) + end + end + end + end +end \ No newline at end of file diff --git a/lib/pact_broker/doc/views/pact/provider-states.markdown b/lib/pact_broker/doc/views/pact/provider-states.markdown new file mode 100644 index 000000000..72827bb90 --- /dev/null +++ b/lib/pact_broker/doc/views/pact/provider-states.markdown @@ -0,0 +1,28 @@ +# Provider States - Aggregated view by provider + +Allowed methods: `GET` + +Path: `/pacts/provider/{provider}/provider-states` + +This resource returns a aggregated de-duplicated list of all provider states for a given provider. + +Provider states are collected from the latest pact on the main branch for any dependant consumers. + +Example response + +```json +{ + "providerStates": [ + { + "name": "an error occurs retrieving an alligator" + }, + { + "name": "there is an alligator named Mary" + }, + { + "name": "there is not an alligator named Mary" + } + ] +} +``` + diff --git a/lib/pact_broker/pacts/content.rb b/lib/pact_broker/pacts/content.rb index 6d6788dbe..73b98698a 100644 --- a/lib/pact_broker/pacts/content.rb +++ b/lib/pact_broker/pacts/content.rb @@ -5,7 +5,10 @@ module PactBroker module Pacts + ProviderState = Struct.new(:name, :params) class Content + + include GenerateInteractionSha using PactBroker::HashRefinements @@ -33,9 +36,21 @@ def sort Content.from_hash(SortContent.call(pact_hash)) end + def provider_states + messages_or_interaction_or_empty_array.flat_map do | interaction | + if interaction["providerState"].is_a?(String) + [ProviderState.new(interaction["providerState"])] + elsif interaction["providerStates"].is_a?(Array) + interaction["providerStates"].collect do | provider_state | + ProviderState.new(provider_state["name"], provider_state["params"]) + end + end + end.compact + end + def interactions_missing_test_results - return [] unless messages_or_interactions - messages_or_interactions.reject do | interaction | + return [] unless messages_and_or_interactions + messages_and_or_interactions.reject do | interaction | interaction["tests"]&.any? end end @@ -116,12 +131,14 @@ def interactions pact_hash.is_a?(Hash) && pact_hash["interactions"].is_a?(Array) ? pact_hash["interactions"] : nil end - def messages_or_interactions - messages || interactions + def messages_and_or_interactions + if messages || interactions + (messages || []) + (interactions || []) + end end def messages_or_interaction_or_empty_array - messages_or_interactions || [] + messages_and_or_interactions || [] end def pact_specification_version diff --git a/lib/pact_broker/pacts/provider_state_service.rb b/lib/pact_broker/pacts/provider_state_service.rb new file mode 100644 index 000000000..2ac8d251c --- /dev/null +++ b/lib/pact_broker/pacts/provider_state_service.rb @@ -0,0 +1,22 @@ +require "pact_broker/services" +require "pact_broker/pacts/selectors" +require "pact_broker/pacts/pact_publication" +require "pact_broker/repositories" + + +module PactBroker + module Pacts + class ProviderStateService + # extend self + extend PactBroker::Services + extend PactBroker::Repositories::Scopes + + def self.list_provider_states(provider) + query = scope_for(PactPublication).eager_for_domain_with_content.for_provider_and_consumer_version_selector(provider, PactBroker::Pacts::Selector.latest_for_main_branch) + query.all.flat_map do | pact_publication | + pact_publication.to_domain.content_object.provider_states + end + end + end + end +end \ No newline at end of file diff --git a/lib/pact_broker/pacts/selector.rb b/lib/pact_broker/pacts/selector.rb index 108751dd1..acfc48a06 100644 --- a/lib/pact_broker/pacts/selector.rb +++ b/lib/pact_broker/pacts/selector.rb @@ -155,6 +155,10 @@ def self.latest_for_branch(branch) new(latest: true, branch: branch) end + def self.latest_for_main_branch + new(latest: true, main_branch: true) + end + def self.latest_for_tag_with_fallback(tag, fallback_tag) new(latest: true, tag: tag, fallback_tag: fallback_tag) end diff --git a/lib/pact_broker/pacts/selectors.rb b/lib/pact_broker/pacts/selectors.rb index 08992006c..132b197df 100644 --- a/lib/pact_broker/pacts/selectors.rb +++ b/lib/pact_broker/pacts/selectors.rb @@ -27,6 +27,10 @@ def self.create_for_latest_for_branch(branch) Selectors.new([Selector.latest_for_branch(branch)]) end + def self.create_for_latest_from_main_branch + Selectors.new([Selector.latest_for_main_branch]) + end + def self.create_for_overall_latest Selectors.new([Selector.overall_latest]) end diff --git a/lib/pact_broker/services.rb b/lib/pact_broker/services.rb index 0e588f29e..a4564a279 100644 --- a/lib/pact_broker/services.rb +++ b/lib/pact_broker/services.rb @@ -93,6 +93,10 @@ def branch_service get_service(:branch_service) end + def provider_state_service + get_service(:provider_state_service) + end + # rubocop: disable Metrics/MethodLength def register_default_services register_service(:index_service) do @@ -194,6 +198,11 @@ def register_default_services require "pact_broker/versions/branch_service" PactBroker::Versions::BranchService end + + register_service(:provider_state_service) do + require "pact_broker/pacts/provider_state_service" + PactBroker::Pacts::ProviderStateService + end end # rubocop: enable Metrics/MethodLength end diff --git a/spec/features/list_provider_states_spec.rb b/spec/features/list_provider_states_spec.rb new file mode 100644 index 000000000..e3cfaca93 --- /dev/null +++ b/spec/features/list_provider_states_spec.rb @@ -0,0 +1,44 @@ +RSpec.describe "listing the provider states" do + before do + td.create_consumer("Foo", main_branch: "main") + .publish_pact(consumer_name: "Foo", provider_name: "Bar", consumer_version_number: "1", branch: "main", json_content: pact_content_1.to_json) + .publish_pact(consumer_name: "Foo", provider_name: "Bar", consumer_version_number: "2", branch: "not-main") + .create_consumer("Waffle", main_branch: "main") + .publish_pact(consumer_name: "Waffle", provider_name: "Bar", consumer_version_number: "1", branch: "main", json_content: pact_content_2.to_json) + end + + let(:rack_headers) { { "HTTP_ACCEPT" => "application/hal+json" } } + + let(:pact_content_1) do + { + interactions: [ + { + providerState: "state 2" + }, + { + providerState: "state 1" + } + ] + } + end + + let(:pact_content_2) do + { + interactions: [ + { + providerStates: [ { name: "state 3" }, { name: "state 4" } ] + }, + { + providerStates: [ { name: "state 5" } ] + } + ] + } + end + + let(:path) { "/pacts/provider/Bar/provider-states" } + + subject { get(path, nil, rack_headers).tap { |it| puts it.body } } + + it { is_expected.to be_a_hal_json_success_response } + +end \ No newline at end of file diff --git a/spec/lib/pact_broker/api/resources/provider_states_spec.rb b/spec/lib/pact_broker/api/resources/provider_states_spec.rb new file mode 100644 index 000000000..602f506b0 --- /dev/null +++ b/spec/lib/pact_broker/api/resources/provider_states_spec.rb @@ -0,0 +1,98 @@ +require "pact_broker/api/resources/provider_states" +require "pact_broker/application_context" +require "pact_broker/pacts/provider_state_service" + +module PactBroker + module Api + module Resources + describe ProviderStates do + before do + allow(PactBroker::Pacticipants::Service).to receive(:find_pacticipant_by_name).and_return(provider) + allow(PactBroker::Pacts::ProviderStateService).to receive(:list_provider_states).and_return(provider_states) + end + + let(:provider) { double("Example API") } + let(:path) { "/pacts/provider/Example%20API/provider-states" } + let(:json) { + { "providerStates": + [ + {"name":"an error occurs retrieving an alligator"}, + {"name":"there is an alligator named Mary"}, + {"name":"there is not an alligator named Mary"} + ]}.to_json + } + + let(:provider_states) do + [ + PactBroker::Pacts::ProviderState.new(name: "there is an alligator named Mary", params: nil), + PactBroker::Pacts::ProviderState.new(name: "there is not an alligator named Mary", params: nil), + PactBroker::Pacts::ProviderState.new(name: "an error occurs retrieving an alligator", params: nil) + ] + end + + describe "GET - provider states where they exist" do + subject { get path; last_response } + + it "attempts to find the ProviderStates" do + expect(PactBroker::Pacts::ProviderStateService).to receive(:list_provider_states) + subject + end + + it "returns a 200 response status" do + expect(subject.status).to eq 200 + end + + it "returns the correct JSON body" do + expect(subject.body).to eq json + end + + it "returns the correct content type" do + expect(subject.headers["Content-Type"]).to include("application/hal+json") + end + end + describe "GET - provider states where do not exist" do + let(:provider_states) do + [] + end + let(:json) { + { "providerStates": + []}.to_json + } + + subject { get path; last_response } + + it "returns a 200 response status" do + expect(subject.status).to eq 200 + end + + it "returns the correct JSON body" do + expect(subject.body).to eq json + end + + it "returns the correct content type" do + expect(subject.headers["Content-Type"]).to include("application/hal+json") + end + end + describe "GET - where provider does not exist" do + + let(:provider) { nil } + let(:json) { {"error":"No provider with name 'Example API' found"}.to_json } + + subject { get path; last_response } + + it "returns a 404 response status" do + expect(subject.status).to eq 404 + end + + it "returns the correct JSON error body" do + expect(subject.body).to eq json + end + + it "returns the correct content type" do + expect(subject.headers["Content-Type"]).to include("application/hal+json") + end + end + end + end + end +end \ No newline at end of file diff --git a/spec/lib/pact_broker/pacts/content_spec.rb b/spec/lib/pact_broker/pacts/content_spec.rb index d65b38d31..7fc266edd 100644 --- a/spec/lib/pact_broker/pacts/content_spec.rb +++ b/spec/lib/pact_broker/pacts/content_spec.rb @@ -179,6 +179,48 @@ module Pacts end end + + describe "provider_states" do + let(:pact_content_1) do + { + interactions: [ + { + providerState: "state 1" + }, + { + providerStates: [ { name: "state 2" }, { name: "state 3", params: { foo: "bar" } } ] + }, + {} + ], + messages: [ + { + providerStates: [ { name: "state 4" } ] + } + ] + } + end + + let(:content) { Content.from_json(pact_content_1.to_json) } + let(:expected_provider_states) do + [ + ProviderState.new("state 4"), + ProviderState.new("state 1"), + ProviderState.new("state 2"), + ProviderState.new("state 3", { "foo" => "bar" }) + ] + end + + subject { content.provider_states } + + it { is_expected.to eq expected_provider_states } + + context "with a contract with no interactions or messages" do + let(:pact_content_1) do + its(:size) { is_expected.to eq 0 } + end + end + end + describe "#pact_specification_version" do subject { Content.from_hash(json) } context "with pactSpecification.version" do From 96f13283905f33a4acec210832d29cdc9d550910 Mon Sep 17 00:00:00 2001 From: Yousaf Nabi Date: Thu, 14 Nov 2024 23:40:40 +0000 Subject: [PATCH 2/2] chore: ignore sinatra audit as no current fix --- .bundler-audit.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .bundler-audit.yml diff --git a/.bundler-audit.yml b/.bundler-audit.yml new file mode 100644 index 000000000..ecadea15f --- /dev/null +++ b/.bundler-audit.yml @@ -0,0 +1,3 @@ +--- +ignore: + - CVE-2024-21510 \ No newline at end of file