mirror of
https://github.com/discourse/discourse.git
synced 2024-12-15 21:13:53 +08:00
75e159f0ed
* 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.
337 lines
11 KiB
Ruby
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
|