2019-04-29 20:27:42 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2019-03-31 06:58:40 -04:00
|
|
|
require 'excon'
|
2016-06-15 13:49:57 -04:00
|
|
|
|
2022-07-27 22:27:38 -04:00
|
|
|
RSpec.describe Jobs::EmitWebHookEvent do
|
2019-05-06 23:12:20 -04:00
|
|
|
fab!(:post_hook) { Fabricate(:web_hook) }
|
|
|
|
fab!(:inactive_hook) { Fabricate(:inactive_web_hook) }
|
|
|
|
fab!(:post) { Fabricate(:post) }
|
|
|
|
fab!(:user) { Fabricate(:user) }
|
2016-06-15 13:49:57 -04:00
|
|
|
|
|
|
|
it 'raises an error when there is no web hook record' do
|
2018-05-21 04:23:09 -04:00
|
|
|
expect do
|
|
|
|
subject.execute(event_type: 'post', payload: {})
|
|
|
|
end.to raise_error(Discourse::InvalidParameters)
|
2016-06-15 13:49:57 -04:00
|
|
|
end
|
|
|
|
|
2016-12-22 11:08:35 -05:00
|
|
|
it 'raises an error when there is no event type' do
|
2018-05-21 04:23:09 -04:00
|
|
|
expect do
|
2020-04-16 16:24:09 -04:00
|
|
|
subject.execute(web_hook_id: post_hook.id, payload: {})
|
2018-05-21 04:23:09 -04:00
|
|
|
end.to raise_error(Discourse::InvalidParameters)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'raises an error when there is no payload' do
|
|
|
|
expect do
|
2020-04-16 16:24:09 -04:00
|
|
|
subject.execute(web_hook_id: post_hook.id, event_type: 'post')
|
2018-05-21 04:23:09 -04:00
|
|
|
end.to raise_error(Discourse::InvalidParameters)
|
2016-06-15 13:49:57 -04:00
|
|
|
end
|
|
|
|
|
2019-03-31 06:58:40 -04:00
|
|
|
it "should not destroy webhook event in case of error" do
|
2019-03-31 21:46:39 -04:00
|
|
|
stub_request(:post, post_hook.payload_url).to_return(status: 500)
|
|
|
|
|
2019-03-31 06:58:40 -04:00
|
|
|
subject.execute(
|
|
|
|
web_hook_id: post_hook.id,
|
|
|
|
payload: { id: post.id }.to_json,
|
|
|
|
event_type: WebHookEventType::POST
|
|
|
|
)
|
2019-03-31 21:46:39 -04:00
|
|
|
|
2019-03-31 06:58:40 -04:00
|
|
|
expect(WebHookEvent.last.web_hook_id).to eq(post_hook.id)
|
|
|
|
end
|
|
|
|
|
2018-05-30 07:27:40 -04:00
|
|
|
context 'when the web hook is failed' do
|
|
|
|
before do
|
|
|
|
SiteSetting.retry_web_hook_events = true
|
|
|
|
end
|
|
|
|
|
2019-04-18 07:36:37 -04:00
|
|
|
context 'when the webhook has failed for 404 or 410' do
|
|
|
|
before do
|
|
|
|
stub_request(:post, post_hook.payload_url).to_return(body: 'Invalid Access', status: response_status)
|
|
|
|
end
|
2018-05-30 07:27:40 -04:00
|
|
|
|
2019-04-18 07:36:37 -04:00
|
|
|
let(:response_status) { 410 }
|
|
|
|
|
|
|
|
it 'disables the webhook' do
|
|
|
|
expect do
|
|
|
|
subject.execute(
|
|
|
|
web_hook_id: post_hook.id,
|
|
|
|
event_type: described_class::PING_EVENT,
|
|
|
|
retry_count: described_class::MAX_RETRY_COUNT
|
|
|
|
)
|
|
|
|
end.to change { post_hook.reload.active }.to(false)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'logs webhook deactivation reason' do
|
2018-05-30 07:27:40 -04:00
|
|
|
subject.execute(
|
|
|
|
web_hook_id: post_hook.id,
|
|
|
|
event_type: described_class::PING_EVENT,
|
|
|
|
retry_count: described_class::MAX_RETRY_COUNT
|
|
|
|
)
|
2019-04-18 07:36:37 -04:00
|
|
|
user_history = UserHistory.find_by(action: UserHistory.actions[:web_hook_deactivate], acting_user: Discourse.system_user)
|
|
|
|
expect(user_history).to be_present
|
|
|
|
expect(user_history.context).to eq([
|
|
|
|
"webhook_id: #{post_hook.id}",
|
|
|
|
"webhook_response_status: #{response_status}"
|
|
|
|
].to_s)
|
|
|
|
end
|
2018-05-30 07:27:40 -04:00
|
|
|
end
|
|
|
|
|
2019-04-18 07:36:37 -04:00
|
|
|
context 'when the webhook has failed' do
|
|
|
|
before do
|
|
|
|
stub_request(:post, post_hook.payload_url).to_return(body: 'Invalid Access', status: 403)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'retry if site setting is enabled' do
|
|
|
|
expect do
|
|
|
|
subject.execute(
|
|
|
|
web_hook_id: post_hook.id,
|
|
|
|
event_type: described_class::PING_EVENT
|
|
|
|
)
|
|
|
|
end.to change { Jobs::EmitWebHookEvent.jobs.size }.by(1)
|
|
|
|
end
|
|
|
|
|
2020-02-21 09:59:00 -05:00
|
|
|
it 'retries at most 5 times' do
|
|
|
|
Jobs.run_immediately!
|
|
|
|
|
|
|
|
expect(Jobs::EmitWebHookEvent::MAX_RETRY_COUNT + 1).to eq(5)
|
|
|
|
|
|
|
|
expect do
|
|
|
|
subject.execute(
|
|
|
|
web_hook_id: post_hook.id,
|
|
|
|
event_type: described_class::PING_EVENT
|
|
|
|
)
|
|
|
|
end.to change { WebHookEvent.count }.by(Jobs::EmitWebHookEvent::MAX_RETRY_COUNT + 1)
|
|
|
|
end
|
|
|
|
|
2019-04-18 07:36:37 -04:00
|
|
|
it 'does not retry for more than maximum allowed times' do
|
|
|
|
expect do
|
|
|
|
subject.execute(
|
|
|
|
web_hook_id: post_hook.id,
|
|
|
|
event_type: described_class::PING_EVENT,
|
|
|
|
retry_count: described_class::MAX_RETRY_COUNT
|
|
|
|
)
|
|
|
|
end.to_not change { Jobs::EmitWebHookEvent.jobs.size }
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'does not retry if site setting is disabled' do
|
|
|
|
SiteSetting.retry_web_hook_events = false
|
|
|
|
|
|
|
|
expect do
|
|
|
|
subject.execute(
|
|
|
|
web_hook_id: post_hook.id,
|
|
|
|
event_type: described_class::PING_EVENT
|
|
|
|
)
|
2022-07-19 10:03:03 -04:00
|
|
|
end.not_to change { Jobs::EmitWebHookEvent.jobs.size }
|
2019-04-18 07:36:37 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
it 'properly logs error on rescue' do
|
|
|
|
stub_request(:post, post_hook.payload_url).to_raise("connection error")
|
2018-05-30 07:27:40 -04:00
|
|
|
subject.execute(
|
|
|
|
web_hook_id: post_hook.id,
|
|
|
|
event_type: described_class::PING_EVENT
|
|
|
|
)
|
2019-04-15 02:49:48 -04:00
|
|
|
|
2019-04-18 07:36:37 -04:00
|
|
|
event = WebHookEvent.last
|
|
|
|
expect(event.payload).to eq(MultiJson.dump(ping: 'OK'))
|
|
|
|
expect(event.status).to eq(-1)
|
|
|
|
expect(MultiJson.load(event.response_headers)['error']).to eq('connection error')
|
|
|
|
end
|
2019-04-15 02:49:48 -04:00
|
|
|
end
|
2018-05-30 07:27:40 -04:00
|
|
|
end
|
|
|
|
|
2018-05-24 03:16:52 -04:00
|
|
|
it 'does not raise an error for a ping event without payload' do
|
2019-03-31 21:46:39 -04:00
|
|
|
stub_request(:post, post_hook.payload_url)
|
2018-05-24 03:16:52 -04:00
|
|
|
.to_return(body: 'OK', status: 200)
|
|
|
|
|
|
|
|
subject.execute(
|
|
|
|
web_hook_id: post_hook.id,
|
|
|
|
event_type: described_class::PING_EVENT
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2016-06-15 13:49:57 -04:00
|
|
|
it "doesn't emit when the hook is inactive" do
|
2018-05-21 04:23:09 -04:00
|
|
|
subject.execute(
|
|
|
|
web_hook_id: inactive_hook.id,
|
|
|
|
event_type: 'post',
|
|
|
|
payload: { test: "some payload" }.to_json
|
|
|
|
)
|
2016-06-15 13:49:57 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
it 'emits normally with sufficient arguments' do
|
2019-03-31 21:46:39 -04:00
|
|
|
stub_request(:post, post_hook.payload_url)
|
2018-05-21 04:23:09 -04:00
|
|
|
.with(body: "{\"post\":{\"test\":\"some payload\"}}")
|
|
|
|
.to_return(body: 'OK', status: 200)
|
|
|
|
|
|
|
|
subject.execute(
|
|
|
|
web_hook_id: post_hook.id,
|
|
|
|
event_type: 'post',
|
|
|
|
payload: { test: "some payload" }.to_json
|
|
|
|
)
|
2016-06-15 13:49:57 -04:00
|
|
|
end
|
|
|
|
|
2022-11-01 12:33:17 -04:00
|
|
|
it "doesn't emit if the payload URL resolves to a disallowed IP" do
|
|
|
|
FinalDestination::TestHelper.stub_to_fail do
|
|
|
|
subject.execute(
|
|
|
|
web_hook_id: post_hook.id,
|
|
|
|
event_type: 'post',
|
|
|
|
payload: { test: "some payload" }.to_json
|
|
|
|
)
|
|
|
|
end
|
|
|
|
event = post_hook.web_hook_events.last
|
|
|
|
expect(event.response_headers).to eq({ error: I18n.t("webhooks.payload_url.blocked_or_internal") }.to_json)
|
|
|
|
expect(event.response_body).to eq(nil)
|
|
|
|
expect(event.status).to eq(-1)
|
|
|
|
end
|
|
|
|
|
2016-06-15 13:49:57 -04:00
|
|
|
context 'with category filters' do
|
2019-05-06 23:12:20 -04:00
|
|
|
fab!(:category) { Fabricate(:category) }
|
|
|
|
fab!(:topic) { Fabricate(:topic) }
|
|
|
|
fab!(:topic_with_category) { Fabricate(:topic, category_id: category.id) }
|
|
|
|
fab!(:topic_hook) { Fabricate(:topic_web_hook, categories: [category]) }
|
2016-06-15 13:49:57 -04:00
|
|
|
|
|
|
|
it "doesn't emit when event is not related with defined categories" do
|
2018-05-21 04:23:09 -04:00
|
|
|
subject.execute(
|
|
|
|
web_hook_id: topic_hook.id,
|
|
|
|
event_type: 'topic',
|
|
|
|
category_id: topic.category.id,
|
|
|
|
payload: { test: "some payload" }.to_json
|
|
|
|
)
|
2016-06-15 13:49:57 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
it 'emit when event is related with defined categories' do
|
2019-03-31 21:46:39 -04:00
|
|
|
stub_request(:post, post_hook.payload_url)
|
2018-05-21 04:23:09 -04:00
|
|
|
.with(body: "{\"topic\":{\"test\":\"some payload\"}}")
|
|
|
|
.to_return(body: 'OK', status: 200)
|
2016-06-15 13:49:57 -04:00
|
|
|
|
2018-05-21 04:23:09 -04:00
|
|
|
subject.execute(
|
|
|
|
web_hook_id: topic_hook.id,
|
|
|
|
event_type: 'topic',
|
|
|
|
category_id: topic_with_category.category.id,
|
|
|
|
payload: { test: "some payload" }.to_json
|
|
|
|
)
|
2016-06-15 13:49:57 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-12-05 04:14:06 -05:00
|
|
|
context 'with tag filters' do
|
2019-05-06 23:12:20 -04:00
|
|
|
fab!(:tag) { Fabricate(:tag) }
|
|
|
|
fab!(:topic) { Fabricate(:topic, tags: [tag]) }
|
|
|
|
fab!(:topic_hook) { Fabricate(:topic_web_hook, tags: [tag]) }
|
2018-12-05 04:14:06 -05:00
|
|
|
|
|
|
|
it "doesn't emit when event is not included any tags" do
|
|
|
|
subject.execute(
|
|
|
|
web_hook_id: topic_hook.id,
|
|
|
|
event_type: 'topic',
|
|
|
|
payload: { test: "some payload" }.to_json
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "doesn't emit when event is not related with defined tags" do
|
|
|
|
subject.execute(
|
|
|
|
web_hook_id: topic_hook.id,
|
|
|
|
event_type: 'topic',
|
|
|
|
tag_ids: [Fabricate(:tag).id],
|
|
|
|
payload: { test: "some payload" }.to_json
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'emit when event is related with defined tags' do
|
2019-03-31 21:46:39 -04:00
|
|
|
stub_request(:post, post_hook.payload_url)
|
2018-12-05 04:14:06 -05:00
|
|
|
.with(body: "{\"topic\":{\"test\":\"some payload\"}}")
|
|
|
|
.to_return(body: 'OK', status: 200)
|
|
|
|
|
|
|
|
subject.execute(
|
|
|
|
web_hook_id: topic_hook.id,
|
|
|
|
event_type: 'topic',
|
|
|
|
tag_ids: topic.tags.pluck(:id),
|
|
|
|
payload: { test: "some payload" }.to_json
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-04-30 20:08:38 -04:00
|
|
|
context 'with group filters' do
|
|
|
|
fab!(:group) { Fabricate(:group) }
|
|
|
|
fab!(:user) { Fabricate(:user, groups: [group]) }
|
|
|
|
fab!(:like_hook) { Fabricate(:like_web_hook, groups: [group]) }
|
|
|
|
|
|
|
|
it "doesn't emit when event is not included any groups" do
|
|
|
|
subject.execute(
|
|
|
|
web_hook_id: like_hook.id,
|
|
|
|
event_type: 'like',
|
|
|
|
payload: { test: "some payload" }.to_json
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "doesn't emit when event is not related with defined groups" do
|
|
|
|
subject.execute(
|
|
|
|
web_hook_id: like_hook.id,
|
|
|
|
event_type: 'like',
|
|
|
|
group_ids: [Fabricate(:group).id],
|
|
|
|
payload: { test: "some payload" }.to_json
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'emit when event is related with defined groups' do
|
|
|
|
stub_request(:post, like_hook.payload_url)
|
|
|
|
.with(body: "{\"like\":{\"test\":\"some payload\"}}")
|
|
|
|
.to_return(body: 'OK', status: 200)
|
|
|
|
|
|
|
|
subject.execute(
|
|
|
|
web_hook_id: like_hook.id,
|
|
|
|
event_type: 'like',
|
|
|
|
group_ids: user.groups.pluck(:id),
|
|
|
|
payload: { test: "some payload" }.to_json
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-04-17 05:03:23 -04:00
|
|
|
describe '#send_webhook!' do
|
2016-06-15 13:49:57 -04:00
|
|
|
it 'creates delivery event record' do
|
2019-03-31 21:46:39 -04:00
|
|
|
stub_request(:post, post_hook.payload_url)
|
2017-05-26 03:19:09 -04:00
|
|
|
.to_return(body: 'OK', status: 200)
|
2016-06-15 13:49:57 -04:00
|
|
|
|
2020-03-06 12:16:19 -05:00
|
|
|
topic_event_type = WebHookEventType.all.first
|
|
|
|
web_hook_id = Fabricate("#{topic_event_type.name}_web_hook").id
|
2018-03-28 08:10:29 -04:00
|
|
|
|
2020-03-06 12:16:19 -05:00
|
|
|
expect do
|
|
|
|
subject.execute(
|
|
|
|
web_hook_id: web_hook_id,
|
|
|
|
event_type: topic_event_type.name,
|
|
|
|
payload: { test: "some payload" }.to_json
|
|
|
|
)
|
|
|
|
end.to change(WebHookEvent, :count).by(1)
|
2016-06-15 13:49:57 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
it 'sets up proper request headers' do
|
2019-03-31 21:46:39 -04:00
|
|
|
stub_request(:post, post_hook.payload_url)
|
2017-05-26 03:19:09 -04:00
|
|
|
.to_return(headers: { test: 'string' }, body: 'OK', status: 200)
|
2016-06-15 13:49:57 -04:00
|
|
|
|
2018-05-21 04:23:09 -04:00
|
|
|
subject.execute(
|
|
|
|
web_hook_id: post_hook.id,
|
|
|
|
event_type: described_class::PING_EVENT,
|
|
|
|
event_name: described_class::PING_EVENT,
|
2018-05-24 03:16:52 -04:00
|
|
|
payload: { test: "this payload shouldn't appear" }.to_json
|
2018-05-21 04:23:09 -04:00
|
|
|
)
|
|
|
|
|
2016-06-15 13:49:57 -04:00
|
|
|
event = WebHookEvent.last
|
|
|
|
headers = MultiJson.load(event.headers)
|
2022-11-01 12:33:17 -04:00
|
|
|
expect(headers['Content-Length']).to eq("13")
|
2016-06-15 13:49:57 -04:00
|
|
|
expect(headers['Host']).to eq("meta.discourse.org")
|
2022-11-01 12:33:17 -04:00
|
|
|
expect(headers['X-Discourse-Event-Id']).to eq(event.id.to_s)
|
2018-05-21 04:23:09 -04:00
|
|
|
expect(headers['X-Discourse-Event-Type']).to eq(described_class::PING_EVENT)
|
|
|
|
expect(headers['X-Discourse-Event']).to eq(described_class::PING_EVENT)
|
2016-06-15 13:49:57 -04:00
|
|
|
expect(headers['X-Discourse-Event-Signature']).to eq('sha256=162f107f6b5022353274eb1a7197885cfd35744d8d08e5bcea025d309386b7d6')
|
|
|
|
expect(event.payload).to eq(MultiJson.dump(ping: 'OK'))
|
|
|
|
expect(event.status).to eq(200)
|
2022-11-01 12:33:17 -04:00
|
|
|
expect(MultiJson.load(event.response_headers)['test']).to eq('string')
|
2016-06-15 13:49:57 -04:00
|
|
|
expect(event.response_body).to eq('OK')
|
|
|
|
end
|
2019-06-20 15:15:35 -04:00
|
|
|
|
|
|
|
it 'sets up proper request headers when an error raised' do
|
2022-11-01 12:33:17 -04:00
|
|
|
stub_request(:post, post_hook.payload_url).to_raise("error")
|
2019-06-20 15:15:35 -04:00
|
|
|
|
|
|
|
subject.execute(
|
|
|
|
web_hook_id: post_hook.id,
|
|
|
|
event_type: described_class::PING_EVENT,
|
|
|
|
event_name: described_class::PING_EVENT,
|
|
|
|
payload: { test: "this payload shouldn't appear" }.to_json
|
|
|
|
)
|
|
|
|
|
|
|
|
event = WebHookEvent.last
|
|
|
|
headers = MultiJson.load(event.headers)
|
2022-11-01 12:33:17 -04:00
|
|
|
expect(headers['Content-Length']).to eq("13")
|
2019-06-20 15:15:35 -04:00
|
|
|
expect(headers['Host']).to eq("meta.discourse.org")
|
2022-11-01 12:33:17 -04:00
|
|
|
expect(headers['X-Discourse-Event-Id']).to eq(event.id.to_s)
|
2019-06-20 15:15:35 -04:00
|
|
|
expect(headers['X-Discourse-Event-Type']).to eq(described_class::PING_EVENT)
|
|
|
|
expect(headers['X-Discourse-Event']).to eq(described_class::PING_EVENT)
|
|
|
|
expect(headers['X-Discourse-Event-Signature']).to eq('sha256=162f107f6b5022353274eb1a7197885cfd35744d8d08e5bcea025d309386b7d6')
|
|
|
|
expect(event.payload).to eq(MultiJson.dump(ping: 'OK'))
|
|
|
|
end
|
2016-06-15 13:49:57 -04:00
|
|
|
end
|
|
|
|
end
|