diff --git a/db/migrations/20210914_add_labels_to_webhooks.rb b/db/migrations/20210914_add_labels_to_webhooks.rb new file mode 100644 index 000000000..8527308bd --- /dev/null +++ b/db/migrations/20210914_add_labels_to_webhooks.rb @@ -0,0 +1,14 @@ +Sequel.migration do + change do + alter_table(:webhooks) do + add_column(:consumer_label, String) + add_column(:provider_label, String) + end + + # SQLite workaround - with one `alter_table` block it adds only last constraint. + alter_table(:webhooks) do + add_constraint(:consumer_label_exclusion, "consumer_id IS NULL OR (consumer_id IS NOT NULL AND consumer_label IS NULL)") + add_constraint(:provider_label_exclusion, "provider_id IS NULL OR (provider_id IS NOT NULL AND provider_label IS NULL)") + end + end +end diff --git a/lib/pact_broker/api/contracts/webhook_contract.rb b/lib/pact_broker/api/contracts/webhook_contract.rb index 76971dd45..9c220d6d4 100644 --- a/lib/pact_broker/api/contracts/webhook_contract.rb +++ b/lib/pact_broker/api/contracts/webhook_contract.rb @@ -40,6 +40,7 @@ def self.errors; @first_errors end property :consumer do property :name + property :label validation do configure do @@ -50,12 +51,23 @@ def pacticipant_exists?(name) end end - required(:name).filled(:pacticipant_exists?) + optional(:name) + .maybe(:pacticipant_exists?) + .when(:none?) { value(:label).filled? } + + optional(:label) + .maybe(:str?) + .when(:none?) { value(:name).filled? } + + rule(label: [:name, :label]) do |name, label| + (name.filled? & label.filled?) > label.none? + end end end property :provider do property :name + property :label validation do configure do @@ -66,7 +78,17 @@ def pacticipant_exists?(name) end end - required(:name).filled(:pacticipant_exists?) + optional(:name) + .maybe(:pacticipant_exists?) + .when(:none?) { value(:label).filled? } + + optional(:label) + .maybe(:str?) + .when(:none?) { value(:name).filled? } + + rule(label: [:name, :label]) do |name, label| + (name.filled? & label.filled?) > label.none? + end end end diff --git a/lib/pact_broker/api/decorators/webhook_decorator.rb b/lib/pact_broker/api/decorators/webhook_decorator.rb index abca36dc8..013ee8d7f 100644 --- a/lib/pact_broker/api/decorators/webhook_decorator.rb +++ b/lib/pact_broker/api/decorators/webhook_decorator.rb @@ -1,4 +1,5 @@ require_relative "base_decorator" +require "pact_broker/domain/webhook_pacticipant" require "pact_broker/api/decorators/webhook_request_template_decorator" require "pact_broker/api/decorators/timestamps" require "pact_broker/webhooks/webhook_request_template" @@ -19,12 +20,14 @@ class WebhookEventDecorator < BaseDecorator property :description, getter: lambda { |context| context[:represented].display_description } - property :consumer, :class => PactBroker::Domain::Pacticipant, default: nil do + property :consumer, class: Domain::WebhookPacticipant, default: nil do property :name + property :label end - property :provider, :class => PactBroker::Domain::Pacticipant, default: nil do + property :provider, class: Domain::WebhookPacticipant, default: nil do property :name + property :label end property :enabled, default: true @@ -50,7 +53,7 @@ class WebhookEventDecorator < BaseDecorator end link :'pb:consumer' do | options | - if represented.consumer + if represented.consumer&.name { title: "Consumer", name: represented.consumer.name, @@ -59,8 +62,18 @@ class WebhookEventDecorator < BaseDecorator end end + link :'pb:consumers' do | options | + if represented.consumer&.label + { + title: "Consumers by label", + name: represented.consumer.label, + href: pacticipants_with_label_url(options.fetch(:base_url), represented.consumer.label) + } + end + end + link :'pb:provider' do | options | - if represented.provider + if represented.provider&.name { title: "Provider", name: represented.provider.name, @@ -69,6 +82,16 @@ class WebhookEventDecorator < BaseDecorator end end + link :'pb:providers' do | options | + if represented.provider&.label + { + title: "Providers by label", + name: represented.provider.label, + href: pacticipants_with_label_url(options.fetch(:base_url), represented.provider.label) + } + end + end + link :'pb:pact-webhooks' do | options | if represented.consumer && represented.provider { diff --git a/lib/pact_broker/api/pact_broker_urls.rb b/lib/pact_broker/api/pact_broker_urls.rb index d0be22a2e..7b3113de9 100644 --- a/lib/pact_broker/api/pact_broker_urls.rb +++ b/lib/pact_broker/api/pact_broker_urls.rb @@ -22,6 +22,10 @@ def pacticipant_url base_url, pacticipant "#{pacticipants_url(base_url)}/#{url_encode(pacticipant.name)}" end + def pacticipants_with_label_url base_url, label_name + "#{pacticipants_url(base_url)}/label/#{url_encode(label_name)}" + end + def pacticipant_url_from_params params, base_url = "" [ base_url, diff --git a/lib/pact_broker/api/resources/all_webhooks.rb b/lib/pact_broker/api/resources/all_webhooks.rb index 2a32fd816..1a6ca1478 100644 --- a/lib/pact_broker/api/resources/all_webhooks.rb +++ b/lib/pact_broker/api/resources/all_webhooks.rb @@ -70,11 +70,11 @@ def validation_errors? webhook end def consumer - webhook.consumer ? pacticipant_service.find_pacticipant_by_name(webhook.consumer.name) : nil + webhook.consumer&.name ? pacticipant_service.find_pacticipant_by_name(webhook.consumer.name) : nil end def provider - webhook.provider ? pacticipant_service.find_pacticipant_by_name(webhook.provider.name) : nil + webhook.provider&.name ? pacticipant_service.find_pacticipant_by_name(webhook.provider.name) : nil end def webhooks diff --git a/lib/pact_broker/doc/views/pacticipant/label.markdown b/lib/pact_broker/doc/views/pacticipant/label.markdown new file mode 100644 index 000000000..8c42c6a50 --- /dev/null +++ b/lib/pact_broker/doc/views/pacticipant/label.markdown @@ -0,0 +1,12 @@ + +# Pacticipant labels + +Allowed methods: `GET`, `PUT`, `DELETE` + +Path: `/pacticipants/{pacticipant}/labels/{label}` + +Get, create or delete pacticipant labels. + +Pacticipants can be queried by label with `/pacticipants/label/{label}`. + +Labels are also used to create generic webhooks that are triggered for subset of pacticipants with label. diff --git a/lib/pact_broker/doc/views/webhooks.markdown b/lib/pact_broker/doc/views/webhooks.markdown index 6f8c83b13..7369463b7 100644 --- a/lib/pact_broker/doc/views/webhooks.markdown +++ b/lib/pact_broker/doc/views/webhooks.markdown @@ -79,6 +79,23 @@ To specify an XML body, you will need to use a correctly escaped string (or use **BEWARE** While the basic auth password, and any header containing the word `authorization` or `token` will be redacted from the UI and the logs, the password could be reverse engineered from the database, so make a separate account for the Pact Broker to use in your webhooks. Don't use your personal account! +#### Consumer or provider label matching + +Webhooks can be created to match events of certain set of [consumers or providers by label](/doc/label?context=pacticipant). Use `label` attribute for either `provider` or `consumer`. Both are optional, but they cannot be provided when `name` attribute is present. Following example would trigger a webhook when any contract with `async` labeled provider changed its content: + + { + "provider": { + "label": "async" + }, + "events": [{ + "name": "contract_content_changed" + }], + "request": { + "method": "POST", + "url": "http://master.ci.my.domain:8085/rest/api/latest/queue/SOME-PROJECT" + } + } + #### Event types `contract_published:` triggered every time a contract is published. It is not recommended to trigger your provider verification build every time a contract is published - see `contract_content_changed` below. diff --git a/lib/pact_broker/domain/pacticipant.rb b/lib/pact_broker/domain/pacticipant.rb index ca9c5bd8e..19e7297bf 100644 --- a/lib/pact_broker/domain/pacticipant.rb +++ b/lib/pact_broker/domain/pacticipant.rb @@ -76,6 +76,10 @@ def any_versions? def branch_head_for(branch_name) branch_heads.find{ | branch_head | branch_head.branch_name == branch_name } end + + def label?(name) + labels.any? { |label| label.name == name } + end end end end diff --git a/lib/pact_broker/domain/webhook.rb b/lib/pact_broker/domain/webhook.rb index 0bc1f29e5..7cbd966ac 100644 --- a/lib/pact_broker/domain/webhook.rb +++ b/lib/pact_broker/domain/webhook.rb @@ -33,11 +33,11 @@ def display_description def scope_description if consumer && provider - "A webhook for the pact between #{consumer.name} and #{provider.name}" + "A webhook for the pact between #{consumer_name} and #{provider_name}" elsif provider - "A webhook for all pacts with provider #{provider.name}" + "A webhook for all pacts with provider #{provider_name}" elsif consumer - "A webhook for all pacts with consumer #{consumer.name}" + "A webhook for all pacts with consumer #{consumer_name}" else "A webhook for all pacts" end @@ -63,11 +63,11 @@ def to_s end def consumer_name - consumer && consumer.name + consumer && (consumer.name || (consumer.label && "labeled `#{consumer.label}`")) end def provider_name - provider && provider.name + provider && (provider.name || (provider.label && "labeled `#{provider.label}`")) end def trigger_on_contract_content_changed? diff --git a/lib/pact_broker/domain/webhook_pacticipant.rb b/lib/pact_broker/domain/webhook_pacticipant.rb new file mode 100644 index 000000000..f15d7d4a6 --- /dev/null +++ b/lib/pact_broker/domain/webhook_pacticipant.rb @@ -0,0 +1,6 @@ + +module PactBroker + module Domain + WebhookPacticipant = Struct.new(:name, :label, keyword_init: true) + end +end diff --git a/lib/pact_broker/locale/en.yml b/lib/pact_broker/locale/en.yml index 47f203a7d..ac672e962 100644 --- a/lib/pact_broker/locale/en.yml +++ b/lib/pact_broker/locale/en.yml @@ -13,6 +13,7 @@ en: single_line?: "cannot contain multiple lines" no_spaces?: "cannot contain spaces" environment_with_name_exists?: "with name '%{value}' does not exist" + none?: "cannot be provided" pact_broker: messages: diff --git a/lib/pact_broker/matrix/row.rb b/lib/pact_broker/matrix/row.rb index 7ac31e534..ad0c71092 100644 --- a/lib/pact_broker/matrix/row.rb +++ b/lib/pact_broker/matrix/row.rb @@ -150,11 +150,11 @@ def summary end def consumer - @consumer ||= OpenStruct.new(name: consumer_name, id: consumer_id) + @consumer ||= Domain::Pacticipant.new(name: consumer_name).tap { |pacticipant| pacticipant.id = consumer_id } end def provider - @provider ||= OpenStruct.new(name: provider_name, id: provider_id) + @provider ||= Domain::Pacticipant.new(name: provider_name).tap { |pacticipant| pacticipant.id = provider_id } end def consumer_version diff --git a/lib/pact_broker/test/http_test_data_builder.rb b/lib/pact_broker/test/http_test_data_builder.rb index 53d505ccc..c77828e3a 100644 --- a/lib/pact_broker/test/http_test_data_builder.rb +++ b/lib/pact_broker/test/http_test_data_builder.rb @@ -102,6 +102,13 @@ def create_pacticipant(name, main_branch: nil) self end + def create_label(name, label) + puts "Creating label `#{label}` for #{name}" + client.put("pacticipants/#{encode(name)}/labels/#{encode(label)}", {}).tap { |response| check_for_error(response) } + separate + self + end + def publish_contract(consumer: last_consumer_name, consumer_version:, provider: last_provider_name, content_id:, tag: nil, branch: nil) content = generate_content(consumer, provider, content_id) request_body_hash = { @@ -206,26 +213,35 @@ def verify_pact(index: 0, success: true, provider: last_provider_name, provider_ self end - def create_global_webhook_for_event(uuid: nil, url: "https://postman-echo.com/post", body: nil, event_name: ) - puts "Creating global webhook for contract changed event with uuid #{uuid}" + def create_global_webhook_for_event(**kwargs) + create_webhook_for_event(**kwargs) + end + + def create_webhook_for_event(uuid: nil, url: "https://postman-echo.com/post", body: nil, provider: nil, consumer: nil, event_name:) + require "securerandom" + webhook_prefix = "global " if provider.nil? && consumer.nil? + puts "Creating #{webhook_prefix}webhook for contract changed event with uuid #{uuid}" uuid ||= SecureRandom.uuid default_body = { - "providerVersionNumber" => "${pactbroker.providerVersionNumber}", - "providerVersionBranch" => "${pactbroker.providerVersionBranch}", + "eventName" => "${pactbroker.eventName}", + "consumerName" => "${pactbroker.consumerName}", "consumerVersionNumber" => "${pactbroker.consumerVersionNumber}", - "consumerVersionBranch" => "${pactbroker.consumerVersionBranch}" + "providerVersionBranch" => "${pactbroker.providerVersionBranch}", + "providerName" => "${pactbroker.providerName}", + "providerVersionNumber" => "${pactbroker.providerVersionNumber}", + "consumerVersionBranch" => "${pactbroker.consumerVersionBranch}", } request_body = { - "description" => "A webhook for all consumers and providers", - "events" => [{ - "name" => event_name - }], + "consumer" => consumer, + "provider" => provider, + "description" => webhook_description(consumer, provider), + "events" => Array(event_name).map { |name| {"name" => name} }, "request" => { "method" => "POST", "url" => url, "body" => body || default_body } - } + }.compact path = "webhooks/#{uuid}" client.put(path, request_body.to_json).tap { |response| check_for_error(response) } separate @@ -333,6 +349,17 @@ def generate_content(consumer_name, provider_name, content_id) private + def webhook_description(consumer, provider) + return "A webhook for all consumers and providers" if consumer.nil? && provider.nil? + + suffix = {consumer: consumer, provider: provider}.compact.map do |name, pacticipant| + desc = pacticipant.compact.map { |k, v| "#{k}: `#{v}`"}.first + "#{name}s by #{desc}" + end + + "A webhook for #{suffix.join(' and ')}" + end + def publish_verification_results(url_of_pact_to_verify, provider, provider_version, provider_version_tag, provider_version_branch, success) [*provider_version_tag].each do | tag | create_tag(pacticipant: provider, version: provider_version, tag: tag) diff --git a/lib/pact_broker/test/test_data_builder.rb b/lib/pact_broker/test/test_data_builder.rb index ef2f0af5a..b3412a873 100644 --- a/lib/pact_broker/test/test_data_builder.rb +++ b/lib/pact_broker/test/test_data_builder.rb @@ -269,11 +269,10 @@ def create_pact_version_without_publication(json_content = nil ) self end - # rubocop: disable Metrics/CyclomaticComplexity def create_webhook parameters = {} params = parameters.dup - consumer = params.key?(:consumer) ? params.delete(:consumer) : @consumer - provider = params.key?(:provider) ? params.delete(:provider) : @provider + consumer, webhook_consumer = webhook_pacticipant(:consumer, params) + provider, webhook_provider = webhook_pacticipant(:provider, params) uuid = params[:uuid] || PactBroker::Webhooks::Service.next_uuid enabled = params.key?(:enabled) ? params.delete(:enabled) : true event_params = if params[:event_names] @@ -284,7 +283,15 @@ def create_webhook parameters = {} 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], enabled: enabled), consumer, provider + new_webhook = PactBroker::Domain::Webhook.new( + request: request, + events: events, + description: params[:description], + enabled: enabled, + consumer: webhook_consumer, + provider: webhook_provider + ) + @webhook = PactBroker::Webhooks::Repository.new.create uuid, new_webhook, consumer, provider self end # rubocop: enable Metrics/CyclomaticComplexity @@ -597,6 +604,16 @@ def default_json_content "random" => rand }.to_json end + + def webhook_pacticipant(name, params) + pacticipant = params.key?(name) ? params.delete(name) : instance_variable_get(:"@#{name}") + label = params.delete(:"#{name}_label") + if pacticipant + [pacticipant, Domain::WebhookPacticipant.new(name: pacticipant.name)] + elsif label + [nil, Domain::WebhookPacticipant.new(label: label)] + end + end end end end diff --git a/lib/pact_broker/webhooks/repository.rb b/lib/pact_broker/webhooks/repository.rb index d90eb9909..540b824c8 100644 --- a/lib/pact_broker/webhooks/repository.rb +++ b/lib/pact_broker/webhooks/repository.rb @@ -17,8 +17,8 @@ class Repository include Repositories def create uuid, webhook, consumer, provider - consumer = pacticipant_repository.find_by_name(webhook.consumer.name) if webhook.consumer - provider = pacticipant_repository.find_by_name(webhook.provider.name) if webhook.provider + consumer = find_pacticipant_by_name(webhook.consumer) || consumer + provider = find_pacticipant_by_name(webhook.provider) || provider db_webhook = Webhook.from_domain webhook, consumer, provider db_webhook.uuid = uuid db_webhook.save @@ -36,8 +36,8 @@ def find_by_uuid uuid # policy applied at resource level def update_by_uuid uuid, webhook existing_webhook = deliberately_unscoped(Webhook).find(uuid: uuid) - existing_webhook.consumer_id = webhook.consumer ? pacticipant_repository.find_by_name(webhook.consumer.name).id : nil - existing_webhook.provider_id = webhook.provider ? pacticipant_repository.find_by_name(webhook.provider.name).id : nil + existing_webhook.consumer_id = find_pacticipant_by_name(webhook.consumer)&.id + existing_webhook.provider_id = find_pacticipant_by_name(webhook.provider)&.id existing_webhook.update_from_domain(webhook).save existing_webhook.events.collect(&:delete) (webhook.events || []).each do | webhook_event | @@ -187,6 +187,12 @@ def fail_retrying_triggered_webhooks private + def find_pacticipant_by_name(pacticipant) + return unless pacticipant&.name + + pacticipant_repository.find_by_name(pacticipant.name) + end + def deliberately_unscoped(scope) scope end diff --git a/lib/pact_broker/webhooks/webhook.rb b/lib/pact_broker/webhooks/webhook.rb index 8a3f55bd1..70c5b93e5 100644 --- a/lib/pact_broker/webhooks/webhook.rb +++ b/lib/pact_broker/webhooks/webhook.rb @@ -30,12 +30,14 @@ def for_event_name(event_name) end def find_by_consumer_and_or_provider consumer, provider + where( Sequel.|( { consumer_id: consumer.id, provider_id: provider.id }, - { consumer_id: nil, provider_id: provider.id }, - { consumer_id: consumer.id, provider_id: nil }, - { consumer_id: nil, provider_id: nil} + { consumer_id: nil, provider_id: provider.id, consumer_label: nil }, + { consumer_id: consumer.id, provider_id: nil, provider_label: nil }, + { consumer_id: nil, provider_id: nil, consumer_label: nil, provider_label: nil }, + *labels_criteria_for_consumer_or_provider(consumer, provider) ) ) end @@ -51,6 +53,32 @@ def find_by_consumer_and_provider consumer, provider def enabled where(enabled: true) end + + private + + def labels_criteria_for_consumer_or_provider(consumer, provider) + consumer_labels = consumer.labels.map(&:name) + provider_labels = provider.labels.map(&:name) + + [].then do |criteria| + next criteria if consumer_labels.empty? + criteria + [ + { consumer_label: consumer_labels, provider_label: nil, provider_id: nil }, + { consumer_label: consumer_labels, provider_label: nil, provider_id: provider.id } + ] + end.then do |criteria| + next criteria if provider_labels.empty? + criteria + [ + { provider_label: provider_labels, consumer_label: nil, consumer_id: nil }, + { provider_label: provider_labels, consumer_label: nil, consumer_id: consumer.id } + ] + end.then do |criteria| + next criteria if consumer_labels.empty? || provider_labels.empty? + criteria + [ + { consumer_label: consumer_labels, provider_label: provider_labels } + ] + end + end end def update_from_domain webhook @@ -74,8 +102,8 @@ def to_domain Domain::Webhook.new( uuid: uuid, description: description, - consumer: consumer, - provider: provider, + consumer: webhook_consumer, + provider: webhook_provider, events: events, request: Webhooks::WebhookRequestTemplate.new(request_attributes), enabled: enabled, @@ -100,7 +128,15 @@ def parsed_body end def is_for? integration - (consumer_id == integration.consumer_id || !consumer_id) && (provider_id == integration.provider_id || !provider_id) + ( + consumer_id == integration.consumer_id || + match_label?(:consumer, integration) || + match_all?(:consumer) + ) && ( + provider_id == integration.provider_id || + match_label?(:provider, integration) || + match_all?(:provider) + ) end # Keep the triggered webhooks after the webhook has been deleted @@ -110,7 +146,6 @@ def delete super end - def self.properties_hash_from_domain webhook is_json_request_body = !(String === webhook.request.body || webhook.request.body.nil?) # Can't rely on people to set content type { @@ -122,9 +157,32 @@ 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_label: webhook.consumer&.label, + provider_label: webhook.provider&.label } end + + def webhook_consumer + return if consumer.nil? && consumer_label.nil? + + Domain::WebhookPacticipant.new(name: consumer&.name, label: consumer_label) + end + + def webhook_provider + return if provider.nil? && provider_label.nil? + + Domain::WebhookPacticipant.new(name: provider&.name, label: provider_label) + end + + def match_all?(name) + public_send(:"#{name}_id").nil? && public_send(:"#{name}_label").nil? + end + + def match_label?(name, integration) + label = public_send(:"#{name}_label") + public_send(:"#{name}_id").nil? && integration.public_send(name).label?(label) + end end end end diff --git a/spec/features/create_webhook_spec.rb b/spec/features/create_webhook_spec.rb index f2b0611ff..51199a071 100644 --- a/spec/features/create_webhook_spec.rb +++ b/spec/features/create_webhook_spec.rb @@ -6,10 +6,14 @@ let(:headers) { {"CONTENT_TYPE" => "application/json"} } let(:response_body) { JSON.parse(subject.body, symbolize_names: true)} let(:webhook_json) { webhook_hash.to_json } + let(:provider) { nil } + let(:consumer) { nil } let(:webhook_hash) do { description: "trigger build", enabled: false, + provider: provider, + consumer: consumer, events: [{ name: "contract_content_changed" }], @@ -23,7 +27,7 @@ a: "body" } } - } + }.compact end subject { post(path, webhook_json, headers) } @@ -64,33 +68,74 @@ context "for a provider" do let(:path) { "/webhooks" } + let(:provider) { { name: "Some Provider" } } - before do - webhook_hash[:provider] = { name: "Some Provider" } - end - - its(:status) { is_expected.to be 201 } + its(:status) { is_expected.to eq 201 } it "creates a webhook without a consumer" do subject expect(PactBroker::Webhooks::Webhook.first.provider).to_not be nil expect(PactBroker::Webhooks::Webhook.first.consumer).to be nil end + + context "with label" do + let(:provider) { { label: "my_label" } } + + its(:status) { is_expected.to eq 201 } + + it "creates a webhook without explicit consumer and provider with provider label" do + subject + expect(PactBroker::Webhooks::Webhook.first.provider).to be nil + expect(PactBroker::Webhooks::Webhook.first.consumer).to be nil + expect(PactBroker::Webhooks::Webhook.first.provider_label).to eq "my_label" + end + end + + context "with both label and name" do + let(:provider) { { name: "Some Provider", label: "my_label" } } + + its(:status) { is_expected.to eq 400 } + + it "returns the validation errors" do + expect(response_body[:errors]).to_not be_empty + end + end end context "for a consumer" do let(:path) { "/webhooks" } - before do - webhook_hash[:consumer] = { name: "Some Consumer" } - end + let(:consumer) { { name: "Some Consumer" } } - its(:status) { is_expected.to be 201 } + its(:status) { is_expected.to eq 201 } it "creates a webhook without a provider" do subject expect(PactBroker::Webhooks::Webhook.first.consumer).to_not be nil expect(PactBroker::Webhooks::Webhook.first.provider).to be nil end + + context "with label" do + let(:consumer) { { label: "my_label" } } + + its(:status) { is_expected.to eq 201 } + + it "creates a webhook without explicit consumer and provider with consumer label" do + subject + expect(PactBroker::Webhooks::Webhook.first.provider).to be nil + expect(PactBroker::Webhooks::Webhook.first.consumer).to be nil + expect(PactBroker::Webhooks::Webhook.first.consumer_label).to eq "my_label" + end + end + + context "with both label and name" do + let(:consumer) { { name: "Some Consumer", label: "my_label" } } + + its(:status) { is_expected.to eq 400 } + + it "returns the validation errors" do + expect(response_body[:errors]).to_not be_empty + end + end end context "with no consumer or provider" do diff --git a/spec/lib/pact_broker/api/contracts/webhook_contract_spec.rb b/spec/lib/pact_broker/api/contracts/webhook_contract_spec.rb index 6ac97d59c..18b3cbaea 100644 --- a/spec/lib/pact_broker/api/contracts/webhook_contract_spec.rb +++ b/spec/lib/pact_broker/api/contracts/webhook_contract_spec.rb @@ -85,6 +85,31 @@ def valid_webhook_with end end + context "with a consumer label" do + let(:json) do + valid_webhook_with do |hash| + hash["consumer"].delete("name") + hash["consumer"]["label"] = "my_label" + end + end + + it "contains no errors" do + expect(subject.errors).to be_empty + end + end + + context "with a consumer label and name provided" do + let(:json) do + valid_webhook_with do |hash| + hash["consumer"]["label"] = "my_label" + end + end + + it "contains consumer.label error" do + expect(subject.errors.messages).to eq({:'consumer.label' => ["cannot be provided"]}) + end + end + context "with a nil provider name" do let(:json) do valid_webhook_with do |hash| @@ -131,6 +156,31 @@ def valid_webhook_with end end + context "with provider label" do + let(:json) do + valid_webhook_with do |hash| + hash["provider"].delete("name") + hash["provider"]["label"] = "my_label" + end + end + + it "contains no errors" do + expect(subject.errors).to be_empty + end + end + + context "with a provider label and name" do + let(:json) do + valid_webhook_with do |hash| + hash["provider"]["label"] = "my_label" + end + end + + it "contains provider.label error" do + expect(subject.errors.messages).to eq({:'provider.label' => ["cannot be provided"]}) + end + end + context "with no request defined" do let(:json) { {}.to_json } diff --git a/spec/lib/pact_broker/api/decorators/webhook_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/webhook_decorator_spec.rb index a744fa087..0a72fc8ff 100644 --- a/spec/lib/pact_broker/api/decorators/webhook_decorator_spec.rb +++ b/spec/lib/pact_broker/api/decorators/webhook_decorator_spec.rb @@ -20,8 +20,8 @@ module Decorators Webhooks::WebhookRequestTemplate.new(request) end - let(:consumer) { Domain::Pacticipant.new(name: "Consumer") } - let(:provider) { Domain::Pacticipant.new(name: "Provider") } + let(:consumer) { Domain::WebhookPacticipant.new(name: "Consumer") } + let(:provider) { Domain::WebhookPacticipant.new(name: "Provider") } let(:event) { Webhooks::WebhookEvent.new(name: "something_happened") } let(:created_at) { DateTime.now } let(:updated_at) { created_at + 1 } @@ -193,8 +193,8 @@ module Decorators context "when the decorated object has a consumer/provider but the incoming JSON does not" do let(:webhook) do Domain::Webhook.new( - consumer: Domain::Pacticipant.new(name: "consumer"), - provider: Domain::Pacticipant.new(name: "provider") + consumer: Domain::WebhookPacticipant.new(name: "consumer"), + provider: Domain::WebhookPacticipant.new(name: "provider") ) end diff --git a/spec/lib/pact_broker/matrix/head_row_spec.rb b/spec/lib/pact_broker/matrix/head_row_spec.rb index 604b4a3bc..ebcf8e12a 100644 --- a/spec/lib/pact_broker/matrix/head_row_spec.rb +++ b/spec/lib/pact_broker/matrix/head_row_spec.rb @@ -13,18 +13,22 @@ module Matrix .create_provider("Bar") .create_consumer_version .create_pact - .create_global_webhook - .create_consumer_webhook - .create_provider_webhook + .create_global_webhook(description: "global") + .create_consumer_webhook(description: "consumer") + .create_provider_webhook(description: "provider") .create_provider("Wiffle") - .create_provider_webhook + .create_provider_webhook(description: "wiffle") end let(:row) { HeadRow.where(consumer_name: "Foo", provider_name: "Bar").single_record } it "returns all the webhooks" do rows = HeadRow.eager(:webhooks).all - expect(rows.first.webhooks.count).to eq 3 + expect(rows.first.webhooks).to contain_exactly( + have_attributes(description: "global"), + have_attributes(description: "provider"), + have_attributes(description: "consumer") + ) end end diff --git a/spec/lib/pact_broker/webhooks/repository_spec.rb b/spec/lib/pact_broker/webhooks/repository_spec.rb index 58befe520..8752fa9bb 100644 --- a/spec/lib/pact_broker/webhooks/repository_spec.rb +++ b/spec/lib/pact_broker/webhooks/repository_spec.rb @@ -23,6 +23,8 @@ module Webhooks let(:webhook) { Domain::Webhook.new(request: request, events: events) } let(:consumer) { td.create_pacticipant("Consumer").and_return(:pacticipant) } let(:provider) { td.create_pacticipant("Provider").and_return(:pacticipant) } + let(:webhook_consumer) { Domain::WebhookPacticipant.new(name: consumer.name) } + let(:webhook_provider) { Domain::WebhookPacticipant.new(name: provider.name) } let(:uuid) { "the-uuid" } let(:created_webhook_record) { ::DB::PACT_BROKER_DB[:webhooks].order(:id).last } let(:created_events) { ::DB::PACT_BROKER_DB[:webhook_events].where(webhook_id: created_webhook_record[:id]).order(:name).all } @@ -57,13 +59,13 @@ module Webhooks end context "when consumer and provider domain objects are set on the object rather than passed in" do - let(:webhook) { Domain::Webhook.new(request: request, events: events, consumer: consumer, provider: provider) } + let(:webhook) { Domain::Webhook.new(request: request, events: events, consumer: webhook_consumer, provider: webhook_provider) } subject { Repository.new.create(uuid, webhook, nil, nil) } it "sets the consumer and provider relationships" do - expect(subject.consumer.id).to eq consumer.id - expect(subject.provider.id).to eq provider.id + expect(subject.consumer.name).to eq consumer.name + expect(subject.provider.name).to eq provider.name end end end @@ -185,12 +187,10 @@ module Webhooks end it "returns a webhook with the consumer set" do - expect(subject.consumer.id).to eq consumer.id expect(subject.consumer.name).to eq consumer.name end it "returns a webhook with the provider set" do - expect(subject.provider.id).to eq provider.id expect(subject.provider.name).to eq provider.name end @@ -286,7 +286,7 @@ module Webhooks let(:new_event) do PactBroker::Webhooks::WebhookEvent.new(name: "something_else") end - let(:new_consumer) { PactBroker::Domain::Pacticipant.new(name: "Foo2") } + let(:new_consumer) { Domain::WebhookPacticipant.new(name: "Foo2") } let(:new_webhook) do PactBroker::Domain::Webhook.new( consumer: new_consumer, @@ -423,11 +423,13 @@ module Webhooks subject { Repository.new.find_webhooks_to_trigger(consumer: td.consumer, provider: td.provider, event_name: "contract_published") } it "does not use a policy" do - td.create_webhook(event_names: ["contract_published"], enabled: enabled) + td.create_webhook(event_names: ["contract_published"], enabled: enabled, description: "Enabled webhook") .create_consumer("Foo") .create_provider("Bar") expect(PactBroker).to_not receive(:policy_scope!) - expect(subject.size).to eq 1 + is_expected.to contain_exactly( + have_attributes(description: "Enabled webhook") + ) end context "when the webhook is disabled" do @@ -437,21 +439,25 @@ module Webhooks .create_provider("Bar") end let(:enabled) { false } - its(:size) { is_expected.to eq 0 } + + it "finds no webhooks to trigger" do + is_expected.to be_empty + end end context "when the webhook is specified for a consumer and all providers" do before do td.create_consumer("Foo1") .create_provider("Bar1") - .create_webhook(provider: nil, event_names: ["contract_published"]) + .create_webhook(provider: nil, event_names: ["contract_published"], description: "Right webhook") end - its(:size) { is_expected.to eq 1 } + let(:webhook_consumer) { Domain::WebhookPacticipant.new(name: td.consumer.name) } - it "returns the right webhook" do - expect(subject.first.consumer).to eq td.consumer - expect(subject.first.provider).to be nil + it "finds one webhook to trigger" do + is_expected.to contain_exactly( + have_attributes(description: "Right webhook") + ) end end @@ -463,7 +469,144 @@ module Webhooks .create_provider("Bar3") end - its(:size) { is_expected.to eq 0 } + it "finds no webhooks to trigger" do + is_expected.to be_empty + end + end + + context "when the webhook is specified for matching consumer label" do + before do + td.create_webhook( + event_names: ["contract_published"], + consumer_label: "my_label", + description: "Labeled webhook" + ) + .create_consumer("Consumer") + .create_label("my_label") + .create_provider("Provider") + end + + it "finds one webhook to trigger" do + is_expected.to contain_exactly( + have_attributes(description: "Labeled webhook") + ) + end + end + + context "when the webhook is specified for matching consumer label and specific provider" do + before do + td.create_provider("Provider") + .create_webhook( + event_names: ["contract_published"], + consumer_label: "my_label", + description: "Labeled webhook" + ) + .create_consumer("Consumer") + .create_label("my_label") + end + + it "finds one webhook to trigger" do + is_expected.to contain_exactly( + have_attributes(description: "Labeled webhook") + ) + end + end + + context "when the webhook is specified for consumer label that does not match" do + before do + td.create_webhook(event_names: ["contract_published"], consumer_label: "my_label") + .create_consumer("Consumer") + .create_label("other_label") + .create_provider("Provider") + end + + it "finds no webhooks to trigger" do + is_expected.to be_empty + end + end + + context "when the webhook is specified for matching provider label" do + before do + td.create_webhook( + event_names: ["contract_published"], + provider_label: "my_label", + description: "Labeled webhook" + ) + .create_consumer("Consumer") + .create_provider("Provider") + .create_label("my_label") + end + + it "finds one webhook to trigger" do + is_expected.to contain_exactly( + have_attributes(description: "Labeled webhook") + ) + end + end + + context "when the webhook is specified for matching provider label and specific consumer" do + before do + td.create_consumer("Consumer") + .create_webhook( + event_names: ["contract_published"], + provider_label: "my_label", + description: "Labeled webhook" + ) + .create_provider("Provider") + .create_label("my_label") + end + + it "finds one webhook to trigger" do + is_expected.to contain_exactly( + have_attributes(description: "Labeled webhook") + ) + end + end + + context "when the webhook is specified for provider label that does not match" do + before do + td.create_webhook(event_names: ["contract_published"], provider_label: "my_label") + .create_consumer("Consumer") + .create_provider("Provider") + .create_label("other_label") + end + + it "find no webhooks to trigger" do + is_expected.to be_empty + end + end + + context "when the webhook is specified for consumer and provider label" do + before do + td.create_webhook( + event_names: ["contract_published"], + consumer_label: "clabel", + provider_label: "plabel", + description: "Labeled webhook" + ) + .create_webhook( + event_names: ["contract_published"], + consumer_label: "clabel", + provider_label: "plabel2", + description: "Labeled consumer webhook" + ) + .create_webhook( + event_names: ["contract_published"], + consumer_label: "clabel2", + provider_label: "plabel", + description: "Labeled provider webhook" + ) + .create_consumer("Consumer") + .create_label("clabel") + .create_provider("Provider") + .create_label("plabel") + end + + it "finds one webhook to trigger" do + is_expected.to contain_exactly( + have_attributes(description: "Labeled webhook") + ) + end end end diff --git a/spec/lib/pact_broker/webhooks/webhook_spec.rb b/spec/lib/pact_broker/webhooks/webhook_spec.rb index 6d150aca7..aa2cfef14 100644 --- a/spec/lib/pact_broker/webhooks/webhook_spec.rb +++ b/spec/lib/pact_broker/webhooks/webhook_spec.rb @@ -6,6 +6,7 @@ module Webhooks before do td.create_consumer("Foo") .create_provider("Bar") + .create_label("label1") .create_consumer_version .create_pact .create_global_webhook @@ -13,11 +14,13 @@ module Webhooks .create_provider_webhook .create_provider("Wiffle") .create_provider_webhook + .create_webhook(provider: nil, consumer: nil, provider_label: "label1") + .create_webhook(provider: nil, consumer: nil, consumer_label: "label2", provider_label: "label1") end let(:consumer) { PactBroker::Domain::Pacticipant.find(name: "Foo") } let(:provider) { PactBroker::Domain::Pacticipant.find(name: "Bar") } - let(:pact) { double(consumer_id: consumer.id, provider_id: provider.id).as_null_object } + let(:pact) { PactBroker::Pacts::PactPublication.find(id: td.pact.id) } describe "#is_for?" do let(:matching_webhook_uuids) { Webhooks::Webhook.find_by_consumer_and_or_provider(consumer, provider).collect(&:uuid) } @@ -25,10 +28,10 @@ module Webhooks let(:non_matching_webhooks) { Webhooks::Webhook.exclude(uuid: matching_webhook_uuids) } it "matches the implementation of Webhook::Repository#find_by_consumer_and_or_provider" do - expect(matching_webhooks.count).to be > 0 - expect(non_matching_webhooks.count).to be > 0 - expect(matching_webhooks.all?{|w| w.is_for?(pact)}).to be true - expect(non_matching_webhooks.all?{|w| !w.is_for?(pact)}).to be true + expect(matching_webhooks).not_to be_empty + expect(non_matching_webhooks).not_to be_empty + expect(matching_webhooks.reject{|w| w.is_for?(pact)}).to be_empty + expect(non_matching_webhooks.reject{|w| !w.is_for?(pact)}).to be_empty end end end