discourse/spec/jobs/emit_web_hook_event_spec.rb
Blake Erickson 43b54c631d
DEV: Reserve webhook event types to be used in plugins (#9110)
* DEV: Reserve webhook event types to be used in plugins

Based on feedback on the following PR's:

https://github.com/discourse/discourse-solved/pull/85

https://github.com/discourse/discourse-assign/pull/61

This commit reserves ID's to be used for webhook event types to ensure
that some other webhook or plugin doesn't end up using the same ID.

* Fix broken test

I don't think this test has to test ALL event types to verify that this
feature is working. Now that we added some event types that plugins are
using this test was failing for missing fabricators that exist in the
respective plugins.

* remove loop and just test first record
2020-03-06 10:16:19 -07:00

301 lines
9.7 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
require 'excon'
describe Jobs::EmitWebHookEvent do
fab!(:post_hook) { Fabricate(:web_hook) }
fab!(:inactive_hook) { Fabricate(:inactive_web_hook) }
fab!(:post) { Fabricate(:post) }
fab!(:user) { Fabricate(:user) }
it 'raises an error when there is no web hook record' do
expect do
subject.execute(event_type: 'post', payload: {})
end.to raise_error(Discourse::InvalidParameters)
end
it 'raises an error when there is no event type' do
expect do
subject.execute(web_hook_id: 1, payload: {})
end.to raise_error(Discourse::InvalidParameters)
end
it 'raises an error when there is no payload' do
expect do
subject.execute(web_hook_id: 1, event_type: 'post')
end.to raise_error(Discourse::InvalidParameters)
end
it "should not destroy webhook event in case of error" do
stub_request(:post, post_hook.payload_url).to_return(status: 500)
subject.execute(
web_hook_id: post_hook.id,
payload: { id: post.id }.to_json,
event_type: WebHookEventType::POST
)
expect(WebHookEvent.last.web_hook_id).to eq(post_hook.id)
end
context 'when the web hook is failed' do
before do
SiteSetting.retry_web_hook_events = true
end
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
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
subject.execute(
web_hook_id: post_hook.id,
event_type: described_class::PING_EVENT,
retry_count: described_class::MAX_RETRY_COUNT
)
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
end
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
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
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
)
end.to change { Jobs::EmitWebHookEvent.jobs.size }.by(0)
end
it 'properly logs error on rescue' do
stub_request(:post, post_hook.payload_url).to_raise("connection error")
subject.execute(
web_hook_id: post_hook.id,
event_type: described_class::PING_EVENT
)
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
end
end
it 'does not raise an error for a ping event without payload' do
stub_request(:post, post_hook.payload_url)
.to_return(body: 'OK', status: 200)
subject.execute(
web_hook_id: post_hook.id,
event_type: described_class::PING_EVENT
)
end
it "doesn't emit when the hook is inactive" do
subject.execute(
web_hook_id: inactive_hook.id,
event_type: 'post',
payload: { test: "some payload" }.to_json
)
end
it 'emits normally with sufficient arguments' do
stub_request(:post, post_hook.payload_url)
.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
)
end
context 'with category filters' do
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]) }
it "doesn't emit when event is not related with defined categories" do
subject.execute(
web_hook_id: topic_hook.id,
event_type: 'topic',
category_id: topic.category.id,
payload: { test: "some payload" }.to_json
)
end
it 'emit when event is related with defined categories' do
stub_request(:post, post_hook.payload_url)
.with(body: "{\"topic\":{\"test\":\"some payload\"}}")
.to_return(body: 'OK', status: 200)
subject.execute(
web_hook_id: topic_hook.id,
event_type: 'topic',
category_id: topic_with_category.category.id,
payload: { test: "some payload" }.to_json
)
end
end
context 'with tag filters' do
fab!(:tag) { Fabricate(:tag) }
fab!(:topic) { Fabricate(:topic, tags: [tag]) }
fab!(:topic_hook) { Fabricate(:topic_web_hook, tags: [tag]) }
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
stub_request(:post, post_hook.payload_url)
.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
describe '#send_webhook!' do
it 'creates delivery event record' do
stub_request(:post, post_hook.payload_url)
.to_return(body: 'OK', status: 200)
topic_event_type = WebHookEventType.all.first
web_hook_id = Fabricate("#{topic_event_type.name}_web_hook").id
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)
end
it 'sets up proper request headers' do
stub_request(:post, post_hook.payload_url)
.to_return(headers: { test: 'string' }, body: 'OK', status: 200)
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)
expect(headers['Content-Length']).to eq(13)
expect(headers['Host']).to eq("meta.discourse.org")
expect(headers['X-Discourse-Event-Id']).to eq(event.id)
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'))
expect(event.status).to eq(200)
expect(MultiJson.load(event.response_headers)['Test']).to eq('string')
expect(event.response_body).to eq('OK')
end
it 'sets up proper request headers when an error raised' do
Excon::Connection.any_instance.expects(:post).raises("error")
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)
expect(headers['Content-Length']).to eq(13)
expect(headers['Host']).to eq("meta.discourse.org")
expect(headers['X-Discourse-Event-Id']).to eq(event.id)
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
end
end