discourse/spec/jobs/emit_web_hook_event_spec.rb
Jeff Wong 75e159f0ed
FEATURE: add support for like webhooks (#12917)
* FEATURE: add support for like webhooks

Add support for like webhooks. Webhook events only send on user membership
in the defined webhook group filters.

This also fixes group webhook events, as before this was never used, and
the logic was not correct.
2021-04-30 17:08:38 -07:00

337 lines
11 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: post_hook.id, 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: post_hook.id, 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
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
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