Skip to content

Commit 37a62be

Browse files
committed
feat(consumer or provider webhooks): allow a webhook to be defined for either a consumer OR provider
1 parent 803c025 commit 37a62be

File tree

15 files changed

+423
-116
lines changed

15 files changed

+423
-116
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
Sequel.migration do
2+
up do
3+
alter_table(:webhooks) do
4+
set_column_allow_null(:consumer_id)
5+
set_column_allow_null(:provider_id)
6+
end
7+
8+
alter_table(:triggered_webhooks) do
9+
set_column_allow_null(:consumer_id)
10+
set_column_allow_null(:provider_id)
11+
end
12+
13+
create_or_replace_view(:latest_triggered_webhooks,
14+
"select tw.*
15+
from triggered_webhooks tw
16+
inner join latest_triggered_webhook_ids ltwi
17+
on tw.consumer_id = ltwi.consumer_id
18+
and tw.provider_id = ltwi.provider_id
19+
and tw.webhook_uuid = ltwi.webhook_uuid
20+
and tw.created_at = ltwi.latest_triggered_webhook_created_at
21+
22+
union
23+
24+
select tw.*
25+
from triggered_webhooks tw
26+
inner join latest_triggered_webhook_ids ltwi
27+
on tw.consumer_id = ltwi.consumer_id
28+
and tw.webhook_uuid = ltwi.webhook_uuid
29+
and tw.created_at = ltwi.latest_triggered_webhook_created_at
30+
where tw.provider_id is null
31+
and ltwi.provider_id is null
32+
33+
union
34+
35+
select tw.*
36+
from triggered_webhooks tw
37+
inner join latest_triggered_webhook_ids ltwi
38+
on tw.provider_id = ltwi.provider_id
39+
and tw.webhook_uuid = ltwi.webhook_uuid
40+
and tw.created_at = ltwi.latest_triggered_webhook_created_at
41+
where tw.consumer_id is null
42+
and ltwi.consumer_id is null"
43+
)
44+
end
45+
46+
down do
47+
end
48+
end

lib/pact_broker/api.rb

+2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ module PactBroker
5656

5757
# Webhooks
5858
add ['webhooks', 'provider', :provider_name, 'consumer', :consumer_name ], Api::Resources::PactWebhooks, {resource_name: "pact_webhooks"}
59+
add ['webhooks', 'provider', :provider_name], Api::Resources::PactWebhooks, {resource_name: "provider_webhooks"}
60+
add ['webhooks', 'consumer', :consumer_name], Api::Resources::PactWebhooks, {resource_name: "consumer_webhooks"}
5961
add ['webhooks', 'provider', :provider_name, 'consumer', :consumer_name, 'status' ], Api::Resources::PactWebhooksStatus, {resource_name: "pact_webhooks_status"}
6062

6163
add ['webhooks', :uuid ], Api::Resources::Webhook, {resource_name: "webhook"}

lib/pact_broker/api/decorators/webhook_decorator.rb

+20-14
Original file line numberDiff line numberDiff line change
@@ -38,26 +38,32 @@ class WebhookEventDecorator < BaseDecorator
3838

3939

4040
link :'pb:consumer' do | options |
41-
{
42-
title: "Consumer",
43-
name: represented.consumer.name,
44-
href: pacticipant_url(options.fetch(:base_url), represented.consumer)
45-
}
41+
if represented.consumer
42+
{
43+
title: "Consumer",
44+
name: represented.consumer.name,
45+
href: pacticipant_url(options.fetch(:base_url), represented.consumer)
46+
}
47+
end
4648
end
4749

4850
link :'pb:provider' do | options |
49-
{
50-
title: "Provider",
51-
name: represented.provider.name,
52-
href: pacticipant_url(options.fetch(:base_url), represented.provider)
53-
}
51+
if represented.provider
52+
{
53+
title: "Provider",
54+
name: represented.provider.name,
55+
href: pacticipant_url(options.fetch(:base_url), represented.provider)
56+
}
57+
end
5458
end
5559

5660
link :'pb:pact-webhooks' do | options |
57-
{
58-
title: "All webhooks for consumer #{represented.consumer.name} and provider #{represented.provider.name}",
59-
href: webhooks_for_pact_url(represented.consumer, represented.provider, options[:base_url])
60-
}
61+
if represented.consumer && represented.provider
62+
{
63+
title: "All webhooks for consumer #{represented.consumer.name} and provider #{represented.provider.name}",
64+
href: webhooks_for_pact_url(represented.consumer, represented.provider, options[:base_url])
65+
}
66+
end
6167
end
6268

6369
link :'pb:webhooks' do | options |

lib/pact_broker/api/resources/pact_webhooks.rb

+4-3
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ def content_types_accepted
2424
end
2525

2626
def resource_exists?
27-
consumer && provider
27+
(identifier_from_path[:consumer_name].nil? || consumer) &&
28+
(identifier_from_path[:provider_name].nil? || provider)
2829
end
2930

3031
def malformed_request?
@@ -77,11 +78,11 @@ def next_uuid
7778
end
7879

7980
def consumer
80-
@consumer ||= find_pacticipant(identifier_from_path[:consumer_name], "consumer")
81+
@consumer ||= identifier_from_path[:consumer_name] && find_pacticipant(identifier_from_path[:consumer_name], "consumer")
8182
end
8283

8384
def provider
84-
@provider ||= find_pacticipant(identifier_from_path[:provider_name], "provider")
85+
@provider ||= identifier_from_path[:provider_name] && find_pacticipant(identifier_from_path[:provider_name], "provider")
8586
end
8687

8788
def find_pacticipant name, role

lib/pact_broker/domain/webhook.rb

+7-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@ def initialize attributes = {}
2525
end
2626

2727
def description
28-
"A webhook for the pact between #{consumer.name} and #{provider.name}"
28+
if consumer && provider
29+
"A webhook for the pact between #{consumer.name} and #{provider.name}"
30+
elsif provider
31+
"A webhook for all pacts with provider #{provider.name}"
32+
else
33+
"A webhook for all pacts with consumer #{consumer.name}"
34+
end
2935
end
3036

3137
def request_description

lib/pact_broker/webhooks/repository.rb

+38-3
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,38 @@ def find_all
5959
Webhook.all.collect(&:to_domain)
6060
end
6161

62+
def find_for_pact pact
63+
find_by_consumer_and_provider(pact.consumer, pact.provider) +
64+
find_by_consumer_and_provider(nil, pact.provider) +
65+
find_by_consumer_and_provider(pact.consumer, nil)
66+
end
67+
6268
def find_by_consumer_and_provider consumer, provider
63-
Webhook.where(consumer_id: consumer.id, provider_id: provider.id).collect(&:to_domain)
69+
criteria = {
70+
consumer_id: (consumer ? consumer.id : nil),
71+
provider_id: (provider ? provider.id : nil)
72+
}
73+
Webhook.where(criteria).collect(&:to_domain)
74+
end
75+
76+
def find_for_pact_and_event_name pact, event_name
77+
find_by_consumer_and_or_provider_and_event_name(pact.consumer, pact.provider, event_name)
78+
end
79+
80+
def find_by_consumer_and_or_provider_and_event_name consumer, provider, event_name
81+
find_by_consumer_and_provider_and_event_name(consumer, provider, event_name) +
82+
find_by_consumer_and_provider_and_event_name(nil, provider, event_name) +
83+
find_by_consumer_and_provider_and_event_name(consumer, nil, event_name)
6484
end
6585

6686
def find_by_consumer_and_provider_and_event_name consumer, provider, event_name
87+
criteria = {
88+
consumer_id: (consumer ? consumer.id : nil),
89+
provider_id: (provider ? provider.id : nil)
90+
}
6791
Webhook
6892
.select_all_qualified
69-
.where(consumer_id: consumer.id, provider_id: provider.id)
93+
.where(criteria)
7094
.join(:webhook_events, { webhook_id: :id })
7195
.where(Sequel[:webhook_events][:name] => event_name)
7296
.collect(&:to_domain)
@@ -136,9 +160,20 @@ def delete_triggered_webhooks_by_pact_publication_ids pact_publication_ids
136160
DeprecatedExecution.where(pact_publication_id: pact_publication_ids).delete
137161
end
138162

163+
def find_latest_triggered_webhooks_for_pact pact
164+
find_latest_triggered_webhooks(pact.consumer, pact.provider) +
165+
find_latest_triggered_webhooks(nil, pact.provider) +
166+
find_latest_triggered_webhooks(pact.consumer, nil)
167+
end
168+
139169
def find_latest_triggered_webhooks consumer, provider
170+
# The manual grouping is just to get rid of any webhooks that triggered at the same time
171+
criteria = {
172+
consumer_id: consumer ? consumer.id : nil,
173+
provider_id: provider ? provider.id : nil
174+
}
140175
LatestTriggeredWebhook
141-
.where(consumer: consumer, provider: provider)
176+
.where(criteria)
142177
.order(:id)
143178
.all
144179
end

lib/pact_broker/webhooks/service.rb

+12
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ def self.update_triggered_webhook_status triggered_webhook, status
8181
webhook_repository.update_triggered_webhook_status triggered_webhook, status
8282
end
8383

84+
def self.find_for_pact pact
85+
webhook_repository.find_for_pact(pact)
86+
end
87+
88+
def self.find_by_consumer_and_or_provider consumer, provider
89+
webhook_repository.find_by_consumer_and_or_provider(consumer, provider)
90+
end
91+
8492
def self.find_by_consumer_and_provider consumer, provider
8593
webhook_repository.find_by_consumer_and_provider consumer, provider
8694
end
@@ -109,6 +117,10 @@ def self.run_later webhooks, pact, verification, event_name
109117
end
110118
end
111119

120+
def self.find_latest_triggered_webhooks_for_pact pact
121+
webhook_repository.find_latest_triggered_webhooks_for_pact pact
122+
end
123+
112124
def self.find_latest_triggered_webhooks consumer, provider
113125
webhook_repository.find_latest_triggered_webhooks consumer, provider
114126
end

lib/pact_broker/webhooks/webhook.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ def self.from_domain webhook, consumer, provider
2828
new(
2929
properties_hash_from_domain(webhook).merge(uuid: webhook.uuid)
3030
).tap do | db_webhook |
31-
db_webhook.consumer_id = consumer.id
32-
db_webhook.provider_id = provider.id
31+
db_webhook.consumer_id = consumer.id if consumer
32+
db_webhook.provider_id = provider.id if provider
3333
end
3434
end
3535

spec/features/create_webhook_spec.rb

+64-37
Original file line numberDiff line numberDiff line change
@@ -6,74 +6,101 @@
66
TestDataBuilder.new.create_pact_with_hierarchy("Some Consumer", "1", "Some Provider")
77
end
88

9-
let(:path) { "/webhooks/provider/Some%20Provider/consumer/Some%20Consumer" }
109
let(:headers) { {'CONTENT_TYPE' => 'application/json'} }
1110
let(:response_body) { JSON.parse(last_response.body, symbolize_names: true)}
1211
let(:webhook_json) { webhook_hash.to_json }
12+
let(:webhook_hash) do
13+
{
14+
events: [{
15+
name: 'something_happened'
16+
}],
17+
request: {
18+
method: 'POST',
19+
url: 'http://example.org',
20+
headers: {
21+
:"Content-Type" => "application/json"
22+
},
23+
body: {
24+
a: 'body'
25+
}
26+
}
27+
}
28+
end
1329

1430
subject { post path, webhook_json, headers }
1531

16-
context "with invalid attributes" do
32+
context "for a consumer and provider" do
33+
let(:path) { "/webhooks/provider/Some%20Provider/consumer/Some%20Consumer" }
1734

18-
let(:webhook_hash) { {} }
35+
context "with invalid attributes" do
36+
let(:webhook_hash) { {} }
1937

20-
it "returns a 400" do
21-
subject
22-
expect(last_response.status).to be 400
23-
end
38+
it "returns a 400" do
39+
subject
40+
expect(last_response.status).to be 400
41+
end
2442

2543
it "returns a JSON content type" do
2644
subject
2745
expect(last_response.headers['Content-Type']).to eq 'application/hal+json;charset=utf-8'
2846
end
2947

30-
it "returns the validation errors" do
31-
subject
32-
expect(response_body[:errors]).to_not be_empty
48+
it "returns the validation errors" do
49+
subject
50+
expect(response_body[:errors]).to_not be_empty
51+
end
3352
end
3453

35-
end
36-
37-
context "with valid attributes" do
38-
39-
let(:webhook_hash) do
40-
{
41-
events: [{
42-
name: 'something_happened'
43-
}],
44-
request: {
45-
method: 'POST',
46-
url: 'https://example.org',
47-
headers: {
48-
:"Content-Type" => "application/json"
49-
},
50-
body: {
51-
a: 'body'
52-
}
53-
}
54-
}
54+
context "with valid attributes" do
55+
it "returns a 201 response" do
56+
subject
57+
expect(last_response.status).to be 201
58+
end
59+
60+
it "returns the Location header" do
61+
subject
62+
expect(last_response.headers['Location']).to match(%r{http://example.org/webhooks/.+})
63+
end
64+
65+
it "returns a JSON Content Type" do
66+
subject
67+
expect(last_response.headers['Content-Type']).to eq 'application/hal+json;charset=utf-8'
68+
end
69+
70+
it "returns the newly created webhook" do
71+
subject
72+
expect(response_body).to include webhook_hash
73+
end
5574
end
75+
end
5676

57-
let(:webhook_json) { webhook_hash.to_json }
77+
context "for a provider" do
78+
let(:path) { "/webhooks/provider/Some%20Provider" }
5879

5980
it "returns a 201 response" do
6081
subject
6182
expect(last_response.status).to be 201
6283
end
6384

64-
it "returns the Location header" do
85+
it "creates a webhook without a consumer" do
6586
subject
66-
expect(last_response.headers['Location']).to match(%r{http://example.org/webhooks/.+})
87+
expect(PactBroker::Webhooks::Webhook.first.provider).to_not be nil
88+
expect(PactBroker::Webhooks::Webhook.first.consumer).to be nil
6789
end
90+
end
6891

69-
it "returns a JSON Content Type" do
92+
context "for a consumer" do
93+
let(:path) { "/webhooks/consumer/Some%20Consumer" }
94+
95+
it "returns a 201 response" do
7096
subject
71-
expect(last_response.headers['Content-Type']).to eq 'application/hal+json;charset=utf-8'
97+
expect(last_response.status).to be 201
7298
end
7399

74-
it "returns the newly created webhook" do
100+
it "creates a webhook without a provider" do
75101
subject
76-
expect(response_body).to include webhook_hash
102+
expect(PactBroker::Webhooks::Webhook.first.consumer).to_not be nil
103+
expect(PactBroker::Webhooks::Webhook.first.provider).to be nil
77104
end
78105
end
79106
end

0 commit comments

Comments
 (0)