fix webhook specs

This commit is contained in:
Markus Kahl
2026-03-09 15:54:16 +00:00
parent a52f5e4469
commit 47021aa605
8 changed files with 81 additions and 12 deletions
+1 -1
View File
@@ -75,7 +75,7 @@ module OpenProject
# http_options: { open_timeout: 5, read_timeout: 10 }
# )
def post(url, options = {}, &)
super(url, { max_redirects: 0 }.merge(options), &)
super(url, { max_redirects: 0, resolver: resolver }.merge(options), &)
end
##
@@ -53,7 +53,7 @@ module Webhooks
def response_attributes(response:, exception:)
{
response_code: response&.code&.to_i || -1,
response_headers: response&.to_hash&.transform_keys { |k| k.underscore.to_sym },
response_headers: response&.to_hash&.transform_keys { |k| k.underscore.to_sym }.transform_values(&:first),
response_body: response&.body || exception&.message
}
end
@@ -29,6 +29,8 @@
require "spec_helper"
RSpec.describe AttachmentWebhookJob, :webmock, type: :job do
include_context "with ssrf webhook stubs"
shared_let(:user) { create(:admin) }
shared_let(:request_url) { "http://example.net/test/42" }
shared_let(:project) { create(:project, name: "Foo Bar") }
@@ -50,7 +52,7 @@ RSpec.describe AttachmentWebhookJob, :webmock, type: :job do
end
let(:stub) do
stub_request(:post, stubbed_url.sub("http://", ""))
stub_request(:post, ssrf_resolved_url(stubbed_url))
.with(
body: hash_including(
"action" => event,
@@ -59,7 +61,7 @@ RSpec.describe AttachmentWebhookJob, :webmock, type: :job do
"id" => attachment.id
)
),
headers: request_headers
headers: request_headers.merge(host: "example.net")
)
.to_return(
status: response_code,
@@ -29,6 +29,8 @@
require "spec_helper"
RSpec.describe ProjectWebhookJob, :webmock, type: :job do
include_context "with ssrf webhook stubs"
shared_let(:request_url) { "http://example.net/test/42" }
shared_let(:project) { create(:project, name: "Foo Bar") }
shared_let(:webhook) { create(:webhook, all_projects: true, url: request_url, secret: nil) }
@@ -54,7 +56,7 @@ RSpec.describe ProjectWebhookJob, :webmock, type: :job do
end
let(:stub) do
stub_request(:post, stubbed_url.sub("http://", ""))
stub_request(:post, ssrf_resolved_url(stubbed_url))
.with(
body: hash_including(
"action" => event,
@@ -64,7 +66,7 @@ RSpec.describe ProjectWebhookJob, :webmock, type: :job do
**expected_payload
)
),
headers: request_headers
headers: request_headers.merge(host: "example.net")
)
.to_return(
status: response_code,
@@ -29,6 +29,8 @@
require "spec_helper"
RSpec.describe TimeEntryWebhookJob, :webmock, type: :job do
include_context "with ssrf webhook stubs"
shared_let(:user) { create(:admin) }
shared_let(:request_url) { "http://example.net/test/42" }
shared_let(:time_entry) { create(:time_entry, hours: 10) }
@@ -51,7 +53,7 @@ RSpec.describe TimeEntryWebhookJob, :webmock, type: :job do
end
let(:stub) do
stub_request(:post, stubbed_url.sub("http://", ""))
stub_request(:post, ssrf_resolved_url(stubbed_url))
.with(
body: hash_including(
"action" => event,
@@ -60,7 +62,7 @@ RSpec.describe TimeEntryWebhookJob, :webmock, type: :job do
"hours" => "PT10H"
)
),
headers: request_headers
headers: request_headers.merge(host: "example.net")
)
.to_return(
status: response_code,
@@ -31,6 +31,8 @@
require "spec_helper"
RSpec.describe WorkPackageCommentWebhookJob, :webmock, type: :model do
include_context "with ssrf webhook stubs"
let(:user) { create(:admin) }
let(:request_url) { "http://example.net/test/42" }
let(:journal) { work_package.journals.last }
@@ -51,7 +53,7 @@ RSpec.describe WorkPackageCommentWebhookJob, :webmock, type: :model do
end
let(:stub) do
stub_request(:post, stubbed_url.sub("http://", "")).with(
stub_request(:post, ssrf_resolved_url(stubbed_url)).with(
body: hash_including(
"action" => event_name,
"activity" => hash_including(
@@ -59,7 +61,7 @@ RSpec.describe WorkPackageCommentWebhookJob, :webmock, type: :model do
"comment" => hash_including("raw" => notes)
)
),
headers: request_headers
headers: request_headers.merge(host: "example.net")
).to_return(
status: response_code,
body: response_body,
@@ -31,6 +31,8 @@
require "spec_helper"
RSpec.describe WorkPackageWebhookJob, :webmock, type: :model do
include_context "with ssrf webhook stubs"
shared_let(:user) { create(:admin) }
shared_let(:title) { "Some workpackage subject" }
shared_let(:request_url) { "http://example.net/test/42" }
@@ -54,7 +56,7 @@ RSpec.describe WorkPackageWebhookJob, :webmock, type: :model do
end
let(:stub) do
stub_request(:post, stubbed_url.sub("http://", ""))
stub_request(:post, ssrf_resolved_url(stubbed_url))
.with(
body: hash_including(
"action" => event,
@@ -63,7 +65,7 @@ RSpec.describe WorkPackageWebhookJob, :webmock, type: :model do
"subject" => title
)
),
headers: request_headers
headers: request_headers.merge(host: "example.net")
)
.to_return(
status: response_code,
@@ -0,0 +1,59 @@
# frozen_string_literal: true
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
module WithSsrfWebhookStubsMixin
##
# A safe public IP returned by the stubbed resolver for any hostname.
# It is not in SsrfFilter's private-address blocklist, so SSRF validation passes,
# and WebMock stubs using this IP will match the actual Net::HTTP request.
SSRF_TEST_IP = "93.184.216.34"
##
# Translates a webhook URL containing a hostname to the IP-based URL that
# SsrfFilter will use when making the actual HTTP request. Use this when
# setting up WebMock stubs so that they match the resolved request.
#
# URLs that already contain an IP address are returned unchanged.
def ssrf_resolved_url(url)
uri = URI.parse(url)
return url if [Resolv::IPv4::Regex, Resolv::IPv6::Regex].any? { uri.host.match?(_1) }
url.sub(uri.host, SSRF_TEST_IP)
end
end
RSpec.shared_context "with ssrf webhook stubs" do
include WithSsrfWebhookStubsMixin
before do
safe_ip = IPAddr.new(WithSsrfWebhookStubsMixin::SSRF_TEST_IP)
allow(OpenProject::SsrfProtection).to receive(:resolver).and_return(
->(_hostname) { [safe_ip] }
)
end
end