# frozen_string_literal: true RSpec.describe Admin::WebHooksController do fab!(:web_hook) { Fabricate(:web_hook) } fab!(:admin) { Fabricate(:admin) } fab!(:moderator) { Fabricate(:moderator) } fab!(:user) { Fabricate(:user) } describe "#create" do context "when logged in as admin" do before { sign_in(admin) } it "creates a webhook" do post "/admin/api/web_hooks.json", params: { web_hook: { payload_url: "https://meta.discourse.org/", content_type: 1, secret: "a_secret_for_webhooks", wildcard_web_hook: false, active: true, verify_certificate: true, web_hook_event_type_ids: [1], group_ids: [], category_ids: [], }, } expect(response.status).to eq(200) json = response.parsed_body expect(json["web_hook"]["payload_url"]).to eq("https://meta.discourse.org/") expect( UserHistory.where( acting_user_id: admin.id, action: UserHistory.actions[:web_hook_create], ).count, ).to eq(1) end it "returns error when field is not filled correctly" do post "/admin/api/web_hooks.json", params: { web_hook: { content_type: 1, secret: "a_secret_for_webhooks", wildcard_web_hook: false, active: true, verify_certificate: true, web_hook_event_type_ids: [1], group_ids: [], category_ids: [], }, } expect(response.status).to eq(422) response_body = response.parsed_body expect(response_body["errors"]).to be_present end end shared_examples "webhook creation not allowed" do it "prevents creation with a 404 response" do post "/admin/api/web_hooks.json", params: { web_hook: { payload_url: "https://meta.discourse.org/", content_type: 1, secret: "a_secret_for_webhooks", wildcard_web_hook: false, active: true, verify_certificate: true, web_hook_event_type_ids: [1], group_ids: [], category_ids: [], }, } expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(response.parsed_body["web_hook"]).to be_nil end end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "webhook creation not allowed" end context "when logged in as a non-staff user" do before { sign_in(user) } include_examples "webhook creation not allowed" end end describe "#update" do context "when logged in as admin" do before { sign_in(admin) } it "logs webhook update" do put "/admin/api/web_hooks/#{web_hook.id}.json", params: { web_hook: { active: false, payload_url: "https://test.com", }, } expect(response.status).to eq(200) expect( UserHistory.where( acting_user_id: admin.id, action: UserHistory.actions[:web_hook_update], new_value: "active: false, payload_url: https://test.com", ).exists?, ).to eq(true) end end shared_examples "webhook update not allowed" do it "prevents updates with a 404 response" do current_payload_url = web_hook.payload_url put "/admin/api/web_hooks/#{web_hook.id}.json", params: { web_hook: { active: false, payload_url: "https://test.com", }, } web_hook.reload expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(web_hook.payload_url).to eq(current_payload_url) end end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "webhook update not allowed" end context "when logged in as a non-staff user" do before { sign_in(user) } include_examples "webhook update not allowed" end end describe "#destroy" do context "when logged in as admin" do before { sign_in(admin) } it "logs webhook destroy" do delete "/admin/api/web_hooks/#{web_hook.id}.json", params: { web_hook: { active: false, payload_url: "https://test.com", }, } expect(response.status).to eq(200) expect( UserHistory.where( acting_user_id: admin.id, action: UserHistory.actions[:web_hook_destroy], ).exists?, ).to eq(true) end end shared_examples "webhook deletion not allowed" do it "prevents deletion with a 404 response" do delete "/admin/api/web_hooks/#{web_hook.id}.json" expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(web_hook.reload).to be_present end end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "webhook deletion not allowed" end context "when logged in as a non-staff user" do before { sign_in(user) } include_examples "webhook deletion not allowed" end end describe "#ping" do context "when logged in as admin" do before { sign_in(admin) } it "enqueues the ping event" do expect do post "/admin/api/web_hooks/#{web_hook.id}/ping.json" end.to change { Jobs::EmitWebHookEvent.jobs.size }.by(1) expect(response.status).to eq(200) job_args = Jobs::EmitWebHookEvent.jobs.first["args"].first expect(job_args["web_hook_id"]).to eq(web_hook.id) expect(job_args["event_type"]).to eq("ping") end end shared_examples "webhook ping not allowed" do it "fails to enqueue a ping with 404 response" do expect do post "/admin/api/web_hooks/#{web_hook.id}/ping.json" end.not_to change { Jobs::EmitWebHookEvent.jobs.size } expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) end end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "webhook ping not allowed" end context "when logged in as a non-staff user" do before { sign_in(user) } include_examples "webhook ping not allowed" end end describe "#redeliver_event" do let!(:web_hook_event) do WebHookEvent.create!(web_hook: web_hook, payload: "abc", headers: JSON.dump(aa: "1", bb: "2")) end before { sign_in(admin) } it "emits the web hook and updates the response headers and body" do stub_request(:post, web_hook.payload_url).with( body: "abc", headers: { "aa" => 1, "bb" => 2, }, ).to_return( status: 402, body: "efg", headers: { "Content-Type" => "application/json", "yoo" => "man", }, ) post "/admin/api/web_hooks/#{web_hook.id}/events/#{web_hook_event.id}/redeliver.json" expect(response.status).to eq(200) parsed_event = response.parsed_body["web_hook_event"] expect(parsed_event["id"]).to eq(web_hook_event.id) expect(parsed_event["status"]).to eq(402) expect(JSON.parse(parsed_event["headers"])).to eq({ "aa" => "1", "bb" => "2" }) expect(parsed_event["payload"]).to eq("abc") expect(JSON.parse(parsed_event["response_headers"])).to eq( { "content-type" => "application/json", "yoo" => "man" }, ) expect(parsed_event["response_body"]).to eq("efg") end it "doesn't emit the web hook if the payload URL resolves to an internal IP" do FinalDestination::TestHelper.stub_to_fail do post "/admin/api/web_hooks/#{web_hook.id}/events/#{web_hook_event.id}/redeliver.json" end expect(response.status).to eq(200) parsed_event = response.parsed_body["web_hook_event"] expect(parsed_event["id"]).to eq(web_hook_event.id) expect(parsed_event["response_headers"]).to eq( { error: I18n.t("webhooks.payload_url.blocked_or_internal") }.to_json, ) expect(parsed_event["status"]).to eq(-1) expect(parsed_event["response_body"]).to eq(nil) end it "doesn't emit the web hook if the payload URL resolves to a blocked IP" do FinalDestination::TestHelper.stub_to_fail do post "/admin/api/web_hooks/#{web_hook.id}/events/#{web_hook_event.id}/redeliver.json" end expect(response.status).to eq(200) parsed_event = response.parsed_body["web_hook_event"] expect(parsed_event["id"]).to eq(web_hook_event.id) expect(parsed_event["response_headers"]).to eq( { error: I18n.t("webhooks.payload_url.blocked_or_internal") }.to_json, ) expect(parsed_event["status"]).to eq(-1) expect(parsed_event["response_body"]).to eq(nil) end end end