Skip to content

Commit f9ba9ab

Browse files
committed
feat(webhooks): support upsert of webhook via a PUT to /webhooks/{uuid}
1 parent bd74b82 commit f9ba9ab

File tree

8 files changed

+120
-21
lines changed

8 files changed

+120
-21
lines changed

lib/pact_broker/api/resources/index.rb

+5
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ def links
9494
title: 'Webhooks',
9595
templated: false
9696
},
97+
'pb:webhook' => {
98+
href: base_url + '/webhooks/{uuid}',
99+
title: 'Webhook',
100+
templated: true
101+
},
97102
'pb:integrations' => {
98103
href: base_url + '/integrations',
99104
title: 'Integrations',

lib/pact_broker/api/resources/webhook.rb

+6-4
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def resource_exists?
2828

2929
def malformed_request?
3030
if request.put?
31-
return invalid_json? || webhook_validation_errors?(new_webhook)
31+
return invalid_json? || webhook_validation_errors?(parsed_webhook, uuid)
3232
end
3333
false
3434
end
@@ -38,7 +38,9 @@ def from_json
3838
@webhook = webhook_service.update_by_uuid uuid, params_with_string_keys
3939
response.body = to_json
4040
else
41-
404
41+
@webhook = webhook_service.create(uuid, parsed_webhook, consumer, provider)
42+
response.body = to_json
43+
201
4244
end
4345
end
4446

@@ -57,8 +59,8 @@ def webhook
5759
@webhook ||= webhook_service.find_by_uuid uuid
5860
end
5961

60-
def new_webhook
61-
@new_webhook ||= Decorators::WebhookDecorator.new(PactBroker::Domain::Webhook.new).from_json(request_body)
62+
def parsed_webhook
63+
@parsed_webhook ||= Decorators::WebhookDecorator.new(PactBroker::Domain::Webhook.new).from_json(request_body)
6264
end
6365

6466
def uuid

lib/pact_broker/api/resources/webhook_resource_methods.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ module PactBroker
22
module Api
33
module Resources
44
module WebhookResourceMethods
5-
def webhook_validation_errors? webhook
6-
errors = webhook_service.errors(webhook)
5+
def webhook_validation_errors?(webhook, uuid = nil)
6+
errors = webhook_service.errors(webhook, uuid)
77
if !errors.empty?
88
set_json_validation_error_messages(errors.messages)
99
true

lib/pact_broker/locale/en.yml

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ en:
4343
consumer_version_number_invalid: "Consumer version number '%{consumer_version_number}' cannot be parsed to a version number. The expected format (unless this configuration has been overridden) is a semantic version. eg. 1.3.0 or 2.0.4.rc1"
4444
pacticipant_name_mismatch: "in pact ('%{name_in_pact}') does not match %{pacticipant} name in path ('%{name}')."
4545
connection_encoding_not_utf8: "The Sequel connection encoding (%{encoding}) is strongly recommended to be \"utf8\". If you need to set it to %{encoding} for some particular reason, then disable this check by setting config.validate_database_connection_config = false"
46+
invalid_webhook_uuid: The UUID can only contain the characters A-Z, a-z, 0-9, _ and -, and must be 16 or more characters.
4647
duplicate_pacticipant: |
4748
This is the first time a pact has been published for "%{new_name}".
4849
The name "%{new_name}" is very similar to the following existing consumers/providers:

lib/pact_broker/webhooks/service.rb

+13-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
require 'pact_broker/webhooks/execution_configuration'
1515
require 'pact_broker/messages'
1616
require 'pact_broker/webhooks/pact_and_verification_parameters'
17+
require 'reform/contract/errors'
1718

1819
module PactBroker
1920
module Webhooks
@@ -28,14 +29,24 @@ class Service
2829
include Logging
2930
extend PactBroker::Messages
3031

32+
# Not actually a UUID. Ah well.
33+
def self.valid_uuid_format?(uuid)
34+
!!(uuid =~ /^[A-Za-z0-9_\-]{16,}$/)
35+
end
36+
3137
def self.next_uuid
3238
SecureRandom.urlsafe_base64
3339
end
3440

35-
def self.errors webhook
41+
def self.errors webhook, uuid = nil
3642
contract = PactBroker::Api::Contracts::WebhookContract.new(webhook)
3743
contract.validate(webhook.attributes)
38-
contract.errors
44+
errors = contract.errors
45+
46+
if uuid && !valid_uuid_format?(uuid)
47+
errors.add("uuid", message("errors.validation.invalid_webhook_uuid"))
48+
end
49+
errors
3950
end
4051

4152
def self.create uuid, webhook, consumer, provider

spec/features/create_webhook_spec.rb

+8
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,12 @@
127127
expect(PactBroker::Webhooks::Webhook.first.provider).to be nil
128128
end
129129
end
130+
131+
context "with a UUID" do
132+
let(:path) { "/webhooks/1234123412341234" }
133+
134+
subject { put(path, webhook_json, headers) }
135+
136+
its(:status) { is_expected.to be 201 }
137+
end
130138
end

spec/lib/pact_broker/api/resources/webhook_spec.rb

+55-12
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@
22
require 'pact_broker/api/resources/webhook'
33

44
module PactBroker::Api
5-
65
module Resources
7-
86
describe Webhook do
9-
107
before do
118
allow(PactBroker::Webhooks::Service).to receive(:find_by_uuid).and_return(webhook)
129
end
@@ -23,15 +20,14 @@ module Resources
2320
end
2421

2522
context "when the webhook exists" do
23+
before do
24+
allow(Decorators::WebhookDecorator).to receive(:new).and_return(decorator)
25+
end
2626

2727
let(:webhook) { double("webhook") }
2828
let(:decorator) { double(Decorators::WebhookDecorator, to_json: json)}
2929
let(:json) { {some: 'json'}.to_json }
3030

31-
before do
32-
allow(Decorators::WebhookDecorator).to receive(:new).and_return(decorator)
33-
end
34-
3531
it "finds the webhook by UUID" do
3632
expect(PactBroker::Webhooks::Service).to receive(:find_by_uuid).with('some-uuid')
3733
subject
@@ -56,14 +52,61 @@ module Resources
5652
end
5753

5854
describe "PUT" do
55+
before do
56+
allow(Decorators::WebhookDecorator).to receive(:new).and_return(decorator)
57+
allow(PactBroker::Webhooks::Service).to receive(:create).and_return(created_webhook)
58+
allow_any_instance_of(Webhook).to receive(:consumer).and_return(consumer)
59+
allow_any_instance_of(Webhook).to receive(:provider).and_return(provider)
60+
allow_any_instance_of(Webhook).to receive(:webhook_validation_errors?).and_return(false)
61+
end
62+
63+
let(:consumer) { double('consumer') }
64+
let(:provider) { double('provider') }
65+
let(:webhook) { double("webhook") }
66+
let(:decorator) { double(Decorators::WebhookDecorator, from_json: parsed_webhook, to_json: json)}
67+
let(:json) { {some: 'json'}.to_json }
68+
69+
let(:parsed_webhook) { double('parsed_webhook') }
70+
let(:created_webhook) { double('created_webhook') }
71+
let(:webhook) { nil }
72+
let(:webhook_json) { load_fixture('webhook_valid.json') }
73+
let(:uuid) { 'some-uuid' }
74+
75+
subject { put("/webhooks/#{uuid}", webhook_json, 'CONTENT_TYPE' => 'application/json') }
76+
77+
it "validates the UUID" do
78+
expect_any_instance_of(Webhook).to receive(:webhook_validation_errors?).with(parsed_webhook, uuid)
79+
subject
80+
end
81+
5982
context "when the webhook does not exist" do
60-
let(:webhook) { nil }
61-
let(:webhook_json) { load_fixture('webhook_valid.json') }
83+
it "creates the webhook" do
84+
expect(PactBroker::Webhooks::Service).to receive(:create).with(uuid, parsed_webhook, consumer, provider)
85+
subject
86+
end
6287

63-
subject { put '/webhooks/some-uuid', webhook_json, {'CONTENT_TYPE' => 'application/json'}; last_response }
88+
its(:status) { is_expected.to eq 201 }
6489

65-
it "returns a 404" do
66-
expect(subject).to be_a_404_response
90+
it "returns the JSON respresentation of the webhook" do
91+
expect(subject.body).to eq json
92+
end
93+
end
94+
95+
context "when the webhook does exist" do
96+
before do
97+
allow(PactBroker::Webhooks::Service).to receive(:update_by_uuid).and_return(created_webhook)
98+
end
99+
let(:webhook) { double('existing webhook') }
100+
101+
its(:status) { is_expected.to eq 200 }
102+
103+
it "updates the webhook" do
104+
expect(PactBroker::Webhooks::Service).to receive(:update_by_uuid).with(uuid, JSON.parse(webhook_json))
105+
subject
106+
end
107+
108+
it "returns the JSON respresentation of the webhook" do
109+
expect(subject.body).to eq json
67110
end
68111
end
69112
end

spec/lib/pact_broker/webhooks/service_spec.rb

+30-1
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,38 @@ module Webhooks
1414
allow(Service).to receive(:logger).and_return(logger)
1515
end
1616

17-
let(:td) { TestDataBuilder.new }
1817
let(:logger) { double('logger').as_null_object }
1918

19+
describe "validate - integration test" do
20+
let(:invalid_webhook) { PactBroker::Domain::Webhook.new }
21+
22+
context "with no uuid" do
23+
subject { Service.errors(invalid_webhook) }
24+
25+
it "does not contain an error for the uuid" do
26+
expect(subject.messages).to_not have_key('uuid')
27+
end
28+
end
29+
30+
context "with a uuid" do
31+
subject { Service.errors(invalid_webhook, '') }
32+
33+
it "merges the uuid errors with the webhook errors" do
34+
expect(subject.messages['uuid'].first).to include "can only contain"
35+
end
36+
end
37+
end
38+
39+
describe ".valid_uuid_format?" do
40+
it 'does something' do
41+
expect(Service.valid_uuid_format?("_-bcdefghigHIJKLMNOP")).to be true
42+
expect(Service.valid_uuid_format?("HIJKLMNOP")).to be false
43+
expect(Service.valid_uuid_format?("abcdefghigHIJKLMNOP\\")).to be false
44+
expect(Service.valid_uuid_format?("abcdefghigHIJKLMNOP#")).to be false
45+
expect(Service.valid_uuid_format?("abcdefghigHIJKLMNOP$")).to be false
46+
end
47+
end
48+
2049
describe ".delete_by_uuid" do
2150
before do
2251
td.create_pact_with_hierarchy

0 commit comments

Comments
 (0)