Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: aggregated provider states #734

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .bundler-audit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
ignore:
- CVE-2024-21510
5 changes: 5 additions & 0 deletions lib/pact_broker/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
19 changes: 19 additions & 0 deletions lib/pact_broker/api/decorators/provider_states_decorator.rb
Original file line number Diff line number Diff line change
@@ -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
38 changes: 38 additions & 0 deletions lib/pact_broker/api/resources/provider_states.rb
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions lib/pact_broker/doc/views/pact/provider-states.markdown
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
```

27 changes: 22 additions & 5 deletions lib/pact_broker/pacts/content.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@

module PactBroker
module Pacts
ProviderState = Struct.new(:name, :params)
class Content


include GenerateInteractionSha
using PactBroker::HashRefinements

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions lib/pact_broker/pacts/provider_state_service.rb
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions lib/pact_broker/pacts/selector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions lib/pact_broker/pacts/selectors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions lib/pact_broker/services.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions spec/features/list_provider_states_spec.rb
Original file line number Diff line number Diff line change
@@ -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
98 changes: 98 additions & 0 deletions spec/lib/pact_broker/api/resources/provider_states_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Loading