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: allow webhooks to specify consumer version matchers for when they are triggered #403

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Sequel.migration do
change do
alter_table(:webhooks) do
add_column(:consumer_version_matchers, String)
end
end
end
7 changes: 7 additions & 0 deletions lib/pact_broker/api/decorators/webhook_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'pact_broker/api/decorators/timestamps'
require 'pact_broker/webhooks/webhook_request_template'
require 'pact_broker/webhooks/webhook_event'
require 'pact_broker/webhooks/version_matcher'
require 'pact_broker/api/decorators/basic_pacticipant_decorator'
require_relative 'pact_pacticipant_decorator'
require_relative 'pacticipant_decorator'
Expand All @@ -15,6 +16,11 @@ class WebhookEventDecorator < BaseDecorator
property :name
end

class VersionMatcherDecorator < BaseDecorator
property :branch
property :tag
end

property :description, getter: lambda { |context| context[:represented].display_description }

property :consumer, :class => PactBroker::Domain::Pacticipant, default: nil do
Expand All @@ -29,6 +35,7 @@ class WebhookEventDecorator < BaseDecorator

property :request, :class => PactBroker::Webhooks::WebhookRequestTemplate, extend: WebhookRequestTemplateDecorator
collection :events, :class => PactBroker::Webhooks::WebhookEvent, extend: WebhookEventDecorator
collection :consumer_version_matchers, camelize: true, :class => PactBroker::Webhooks::VersionMatcher, extend: VersionMatcherDecorator

include Timestamps

Expand Down
12 changes: 12 additions & 0 deletions lib/pact_broker/domain/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,18 @@ def latest_for_branch?
def latest_for_pacticipant?
latest_version_for_pacticipant == self
end

def matches_webhook_matcher?(version_matcher)
if version_matcher.branch && version_matcher.branch != branch
return false
end

if version_matcher.tag
return tags.any?{ |tag| tag.name == version_matcher.tag }
end

true
end
end
end
end
Expand Down
13 changes: 12 additions & 1 deletion lib/pact_broker/domain/webhook.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Webhook
include Logging

# request is actually a request_template
attr_accessor :uuid, :consumer, :provider, :request, :created_at, :updated_at, :events, :enabled, :description
attr_accessor :uuid, :consumer, :provider, :request, :created_at, :updated_at, :events, :enabled, :description, :consumer_version_matchers
attr_reader :attributes

def initialize attributes = {}
Expand All @@ -24,6 +24,7 @@ def initialize attributes = {}
@consumer = attributes[:consumer]
@provider = attributes[:provider]
@events = attributes[:events]
@consumer_version_matchers = attributes[:consumer_version_matchers]
@enabled = attributes[:enabled]
@created_at = attributes[:created_at]
@updated_at = attributes[:updated_at]
Expand Down Expand Up @@ -101,6 +102,16 @@ def expand_currently_deployed_provider_versions?
request.uses_parameter?(PactBroker::Webhooks::PactAndVerificationParameters::CURRENTLY_DEPLOYED_PROVIDER_VERSION_NUMBER)
end

def version_matches_consumer_version_matchers?(version)
if consumer_version_matchers&.any?
consumer_version_matchers.any? do | matcher |
version.matches_webhook_matcher?(matcher)
end
else
true
end
end

private

def execute_request(webhook_request)
Expand Down
6 changes: 4 additions & 2 deletions lib/pact_broker/test/test_data_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require 'pact_broker/webhooks/repository'
require 'pact_broker/webhooks/service'
require 'pact_broker/webhooks/webhook_execution_result'
require 'pact_broker/webhooks/version_matcher'
require 'pact_broker/pacts/repository'
require 'pact_broker/pacts/service'
require 'pact_broker/pacts/content'
Expand Down Expand Up @@ -286,14 +287,15 @@ def create_webhook parameters = {}
provider = params.key?(:provider) ? params.delete(:provider) : @provider
uuid = params[:uuid] || PactBroker::Webhooks::Service.next_uuid
event_params = if params[:event_names]
params[:event_names].collect{ |event_name| {name: event_name} }
params[:event_names].collect{ |event_name| { name: event_name } }
else
params[:events] || [{ name: PactBroker::Webhooks::WebhookEvent::DEFAULT_EVENT_NAME }]
end
consumer_version_matchers = params.delete(:consumer_version_matchers)&.collect{ |m| PactBroker::Webhooks::VersionMatcher.from_hash(m) } || []
events = event_params.collect{ |e| PactBroker::Webhooks::WebhookEvent.new(e) }
template_params = { method: 'POST', url: 'http://example.org', headers: {'Content-Type' => 'application/json'}, username: params[:username], password: params[:password]}
request = PactBroker::Webhooks::WebhookRequestTemplate.new(template_params.merge(params))
@webhook = PactBroker::Webhooks::Repository.new.create uuid, PactBroker::Domain::Webhook.new(request: request, events: events, description: params[:description]), consumer, provider
@webhook = PactBroker::Webhooks::Repository.new.create uuid, PactBroker::Domain::Webhook.new(request: request, events: events, consumer_version_matchers: consumer_version_matchers, description: params[:description]), consumer, provider
self
end

Expand Down
23 changes: 19 additions & 4 deletions lib/pact_broker/webhooks/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,22 @@ def self.find_by_consumer_and_provider consumer, provider
webhook_repository.find_by_consumer_and_provider consumer, provider
end

# this method is a mess.
def self.trigger_webhooks pact, verification, event_name, event_context, options
webhooks = webhook_repository.find_by_consumer_and_or_provider_and_event_name pact.consumer, pact.provider, event_name

matching_webhooks = filter_webhooks(webhooks, pact)

if webhooks.any?
webhook_execution_configuration = options.fetch(:webhook_execution_configuration).with_webhook_context(event_name: event_name)
# bit messy to merge in base_url here, but easier than a big refactor
base_url = options.fetch(:webhook_execution_configuration).webhook_context.fetch(:base_url)
if matching_webhooks.any?
webhook_execution_configuration = options.fetch(:webhook_execution_configuration).with_webhook_context(event_name: event_name)
# bit messy to merge in base_url here, but easier than a big refactor
base_url = options.fetch(:webhook_execution_configuration).webhook_context.fetch(:base_url)

run_webhooks_later(webhooks, pact, verification, event_name, event_context.merge(event_name: event_name, base_url: base_url), options.merge(webhook_execution_configuration: webhook_execution_configuration))
run_webhooks_later(matching_webhooks, pact, verification, event_name, event_context.merge(event_name: event_name, base_url: base_url), options.merge(webhook_execution_configuration: webhook_execution_configuration))
else
logger.info "No enabled webhooks found for consumer \"#{pact.consumer.name}\" and provider \"#{pact.provider.name}\" and event #{event_name} that match the webhook's consumer version matchers"
end
else
logger.info "No enabled webhooks found for consumer \"#{pact.consumer.name}\" and provider \"#{pact.provider.name}\" and event #{event_name}"
end
Expand Down Expand Up @@ -193,6 +200,14 @@ def self.parameters

private

def self.filter_webhooks(webhooks, pact)
# The consumer_version on the pact domain object is an OpenStruct - need to get the domain object
consumer_version = PactBroker::Domain::Version.for(pact.consumer.name, pact.consumer_version.number)
webhooks.select do | webhook |
webhook.version_matches_consumer_version_matchers?(consumer_version)
end
end

# Dirty hack to maintain existing password or Authorization header if it is submitted with value ****
# This is required because the password and Authorization header is **** out in the API response
# for security purposes, so it would need to be re-entered with every response.
Expand Down
33 changes: 33 additions & 0 deletions lib/pact_broker/webhooks/version_matcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
require 'pact_broker/hash_refinements'

module PactBroker
module Webhooks
class VersionMatcher < Hash
using PactBroker::HashRefinements

def initialize(options = {})
merge!(options)
end

def self.from_hash(hash)
new(hash.symbolize_keys)
end

def branch
self[:branch]
end

def branch= branch
self[:branch] = branch
end

def tag
self[:tag]
end

def tag= tag
self[:tag] = tag
end
end
end
end
6 changes: 5 additions & 1 deletion lib/pact_broker/webhooks/webhook.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
require 'pact_broker/domain/webhook'
require 'pact_broker/webhooks/webhook_request_template'
require 'pact_broker/domain/pacticipant'
require 'pact_broker/webhooks/version_matcher'

module PactBroker
module Webhooks
class Webhook < Sequel::Model
set_primary_key :id
plugin :serialization, :json, :headers
plugin :timestamps, update_on_create: true
plugin :serialization, :json, :consumer_version_matchers

associate(:many_to_one, :provider, :class => "PactBroker::Domain::Pacticipant", :key => :provider_id, :primary_key => :id)
associate(:many_to_one, :consumer, :class => "PactBroker::Domain::Pacticipant", :key => :consumer_id, :primary_key => :id)
Expand Down Expand Up @@ -46,6 +48,7 @@ def to_domain
consumer: consumer,
provider: provider,
events: events,
consumer_version_matchers: consumer_version_matchers&.collect{ |m| VersionMatcher.from_hash(m) } || [],
request: Webhooks::WebhookRequestTemplate.new(request_attributes),
enabled: enabled,
created_at: created_at,
Expand Down Expand Up @@ -85,7 +88,8 @@ def self.properties_hash_from_domain webhook
enabled: webhook.enabled.nil? ? true : webhook.enabled,
body: (is_json_request_body ? webhook.request.body.to_json : webhook.request.body),
is_json_request_body: is_json_request_body,
headers: webhook.request.headers
headers: webhook.request.headers,
consumer_version_matchers: webhook.consumer_version_matchers || []
}
end
end
Expand Down
1 change: 1 addition & 0 deletions spec/features/create_webhook_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
events: [{
name: 'contract_content_changed'
}],
consumerVersionMatchers: [{ branch: "main" }],
request: {
method: 'POST',
url: 'https://example.org',
Expand Down
13 changes: 11 additions & 2 deletions spec/lib/pact_broker/api/decorators/webhook_decorator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,17 @@ module Decorators
end

describe 'from_json' do
let(:hash) { { request: request, events: [event] } }
let(:event) { {name: 'something_happened'} }
let(:hash) do
{
request: request,
events: [event],
consumerVersionSelectors: consumer_version_selectors
}
end
let(:consumer_version_selectors) do
[{ branch: "main" }]
end
let(:event) { { name: 'something_happened' } }
let(:json) { hash.to_json }
let(:webhook) { Domain::Webhook.new }
let(:parsed_object) { subject.from_json(json) }
Expand Down
57 changes: 57 additions & 0 deletions spec/lib/pact_broker/domain/version_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,63 @@ def version_numbers
expect(all.first.associations[:current_deployed_versions].first.environment.name).to eq "prod"
end
end

describe "matches_webhook_matcher?" do
let(:version) do
td.create_consumer("Foo")
.create_consumer_version("1", branch: "right-branch", tag_names: ["right-tag"])
.and_return(:consumer_version)
end
let(:matcher) { PactBroker::Webhooks::VersionMatcher.new(branch: "right-branch") }

subject { version.matches_webhook_matcher?(matcher) }

context "when the matcher matches the branch" do
it { is_expected.to be true }
end

context "when the matcher does not match the branch" do
let(:matcher) { PactBroker::Webhooks::VersionMatcher.new(branch: "foo") }

it { is_expected.to be false }
end

context "when the matcher matches the tag" do
let(:matcher) { PactBroker::Webhooks::VersionMatcher.new(tag: "right-tag") }

it { is_expected.to be true }
end

context "when the matcher does not match the tag" do
let(:matcher) { PactBroker::Webhooks::VersionMatcher.new(tag: "bar") }

it { is_expected.to be false }
end

context "when the matcher matches the branch and tag" do
let(:matcher) { PactBroker::Webhooks::VersionMatcher.new(tag: "right-tag", branch: "right-branch") }

it { is_expected.to be true }
end

context "when the matcher matches the branch and not tag" do
let(:matcher) { PactBroker::Webhooks::VersionMatcher.new(tag: "bar", branch: "right-branch") }

it { is_expected.to be false }
end

context "when the matcher matches the tag and not branch" do
let(:matcher) { PactBroker::Webhooks::VersionMatcher.new(tag: "right-tag", branch: "wrong-branch") }

it { is_expected.to be false }
end

context "when the matcher has no properties" do
let(:matcher) { PactBroker::Webhooks::VersionMatcher.new({}) }

it { is_expected.to be true }
end
end
end
end
end
50 changes: 49 additions & 1 deletion spec/lib/pact_broker/webhooks/service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,11 @@ module Webhooks
let(:provider) { PactBroker::Domain::Pacticipant.new(name: 'Provider') }
let(:webhooks) { [webhook]}
let(:webhook) do
instance_double(PactBroker::Domain::Webhook, description: 'description', uuid: '1244', expand_currently_deployed_provider_versions?: expand_currently_deployed)
instance_double(PactBroker::Domain::Webhook,
description: 'description',
uuid: '1244',
expand_currently_deployed_provider_versions?: expand_currently_deployed,
version_matches_consumer_version_matchers?: true)
end
let(:expand_currently_deployed) { false }
let(:triggered_webhook) { instance_double(PactBroker::Webhooks::TriggeredWebhook) }
Expand Down Expand Up @@ -274,6 +278,50 @@ module Webhooks
end
end

describe ".trigger_webhooks integration" do
before do
allow(Job).to receive(:perform_in)
end

let(:pact) do
td.create_provider("Bar")
.create_consumer("Foo")
.create_webhook(
event_names: [PactBroker::Webhooks::WebhookEvent::CONTRACT_PUBLISHED],
consumer_version_matchers: [{ branch: "main" }]
)
.create_consumer_version("1", branch: branch)
.create_pact
.and_return(:pact)
end
let(:branch) { "main" }
let(:event_context) { {} }
let(:options) { { webhook_execution_configuration: webhook_execution_configuration } }
let(:webhook_execution_configuration) do
PactBroker::Webhooks::ExecutionConfiguration.new
.with_webhook_context(base_url: 'http://example.org')

end

subject { Service.trigger_webhooks(pact, nil, PactBroker::Webhooks::WebhookEvent::CONTRACT_PUBLISHED, event_context, options) }

context "when the webhook has a consumer version matcher that matches the pact's version" do
it "schedules a job" do
expect(Job).to receive(:perform_in)
subject
end
end

context "when the webhook has a consumer version matcher that does not match the pact's version" do
let(:branch) { "foo" }

it "does not schedule a job" do
expect(Job).to_not receive(:perform_in)
subject
end
end
end

describe ".test_execution" do
let(:webhook) do
instance_double(PactBroker::Domain::Webhook,
Expand Down
3 changes: 0 additions & 3 deletions spec/lib/pact_broker/webhooks/webhook_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
module PactBroker
module Webhooks
describe Webhook do

let(:td) { TestDataBuilder.new }

before do
td.create_consumer("Foo")
.create_provider("Bar")
Expand Down