Skip to content

Commit

Permalink
feat: aggregated provider state endpoint
Browse files Browse the repository at this point in the history
# 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"
        }
    ]
}
```
  • Loading branch information
YOU54F committed Nov 14, 2024
1 parent 9653212 commit 6a7b4ae
Show file tree
Hide file tree
Showing 12 changed files with 335 additions and 5 deletions.
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

0 comments on commit 6a7b4ae

Please sign in to comment.