webhukhs 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.standard.yml +2 -0
- data/.yard-lint.yml +295 -0
- data/AGENTS.md +58 -0
- data/Appraisals +4 -0
- data/CHANGELOG.md +15 -0
- data/README.md +9 -7
- data/config/mutant.rb +37 -0
- data/config/mutant.yml +25 -0
- data/gemfiles/rails_master.gemfile +10 -0
- data/lib/webhukhs/base_handler.rb +9 -6
- data/lib/webhukhs/controllers/receive_webhooks_controller.rb +26 -7
- data/lib/webhukhs/engine.rb +1 -0
- data/lib/webhukhs/install_generator.rb +9 -2
- data/lib/webhukhs/jobs/processing_job.rb +24 -4
- data/lib/webhukhs/models/received_webhook.rb +33 -6
- data/lib/webhukhs/version.rb +1 -1
- data/lib/webhukhs.rb +9 -0
- data/test/base_handler_test.rb +125 -0
- data/test/processing_job_test.rb +195 -0
- data/test/received_webhook_test.rb +244 -0
- data/test/test_app.rb +37 -15
- data/test/test_helper.rb +66 -0
- data/test/webhukhs_test.rb +126 -10
- metadata +11 -5
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class StringIdReceivedWebhook < Webhukhs::ReceivedWebhook
|
|
6
|
+
self.table_name = "string_id_received_webhooks"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
unless ActiveRecord::Base.connection.data_source_exists?("string_id_received_webhooks")
|
|
10
|
+
original_migration_verbose = ActiveRecord::Migration.verbose
|
|
11
|
+
ActiveRecord::Migration.verbose = false
|
|
12
|
+
|
|
13
|
+
begin
|
|
14
|
+
ActiveRecord::Schema.define do
|
|
15
|
+
create_table "string_id_received_webhooks", id: :string, force: true do |t|
|
|
16
|
+
t.string "handler_event_id", null: false
|
|
17
|
+
t.string "handler_module_name", null: false
|
|
18
|
+
t.string "status", default: "received", null: false
|
|
19
|
+
t.binary "body", null: false
|
|
20
|
+
t.json "request_headers", null: true
|
|
21
|
+
t.datetime "created_at", null: false
|
|
22
|
+
t.datetime "updated_at", null: false
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
ensure
|
|
26
|
+
ActiveRecord::Migration.verbose = original_migration_verbose
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class ReceivedWebhookTest < ActiveSupport::TestCase
|
|
31
|
+
cover "Webhukhs::ReceivedWebhook*"
|
|
32
|
+
|
|
33
|
+
teardown do
|
|
34
|
+
Webhukhs::ReceivedWebhook.delete_all
|
|
35
|
+
StringIdReceivedWebhook.delete_all if ActiveRecord::Base.connection.data_source_exists?("string_id_received_webhooks")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
test "request stores uppercase string headers and rewinds the body io" do
|
|
39
|
+
body_io = StringIO.new('{"ok":true}')
|
|
40
|
+
request = ActionDispatch::Request.new(
|
|
41
|
+
"CONTENT_TYPE" => "application/json",
|
|
42
|
+
"HTTP_X_SIGNATURE" => "sig",
|
|
43
|
+
"lowercase" => "skip",
|
|
44
|
+
"Mixed-Case" => "skip",
|
|
45
|
+
"HTTP_X_NUMBER" => 123,
|
|
46
|
+
123 => "skip",
|
|
47
|
+
"RAW_POST_DATA" => "legacy",
|
|
48
|
+
"rack.input" => body_io,
|
|
49
|
+
"action_dispatch.request.path_parameters" => {"user_id" => "123"}
|
|
50
|
+
)
|
|
51
|
+
webhook = Webhukhs::ReceivedWebhook.new
|
|
52
|
+
|
|
53
|
+
webhook.request = request
|
|
54
|
+
|
|
55
|
+
assert_equal 0, body_io.pos
|
|
56
|
+
assert_equal body_io.string.b, webhook.body
|
|
57
|
+
assert_equal Encoding::ASCII_8BIT, webhook.body.encoding
|
|
58
|
+
assert_equal(
|
|
59
|
+
{
|
|
60
|
+
"CONTENT_TYPE" => "application/json",
|
|
61
|
+
"HTTP_X_SIGNATURE" => "sig",
|
|
62
|
+
"action_dispatch.request.path_parameters" => {"user_id" => "123"}
|
|
63
|
+
},
|
|
64
|
+
webhook.request_headers
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
test "request writer coerces the stored body to binary before type casting" do
|
|
69
|
+
body_io = StringIO.new("payload")
|
|
70
|
+
request = ActionDispatch::Request.new(
|
|
71
|
+
"CONTENT_TYPE" => "application/json",
|
|
72
|
+
"rack.input" => body_io,
|
|
73
|
+
"action_dispatch.request.path_parameters" => {}
|
|
74
|
+
)
|
|
75
|
+
webhook = Webhukhs::ReceivedWebhook.new
|
|
76
|
+
captured_body = nil
|
|
77
|
+
|
|
78
|
+
with_overridden_singleton_method(webhook, :write_attribute, ->(name, value) {
|
|
79
|
+
captured_body = value if name == "body"
|
|
80
|
+
}) do
|
|
81
|
+
webhook.request = request
|
|
82
|
+
|
|
83
|
+
assert_equal "payload".b, captured_body
|
|
84
|
+
assert_equal Encoding::ASCII_8BIT, captured_body.encoding
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
test "request rejects oversized bodies and rewinds the body io" do
|
|
89
|
+
original_limit = Webhukhs.configuration.request_body_size_limit
|
|
90
|
+
Webhukhs.configuration.request_body_size_limit = 3
|
|
91
|
+
|
|
92
|
+
body_io = StringIO.new("four")
|
|
93
|
+
request = ActionDispatch::Request.new(
|
|
94
|
+
"CONTENT_TYPE" => "application/json",
|
|
95
|
+
"rack.input" => body_io,
|
|
96
|
+
"action_dispatch.request.path_parameters" => {}
|
|
97
|
+
)
|
|
98
|
+
webhook = Webhukhs::ReceivedWebhook.new
|
|
99
|
+
|
|
100
|
+
error = assert_raises(RuntimeError) { webhook.request = request }
|
|
101
|
+
|
|
102
|
+
assert_equal "Cannot accept the webhook as the request body is larger than 3 bytes", error.message
|
|
103
|
+
assert_equal 0, body_io.pos
|
|
104
|
+
ensure
|
|
105
|
+
Webhukhs.configuration.request_body_size_limit = original_limit
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
test "request reconstructs the body without mutating stored headers" do
|
|
109
|
+
request_headers = {
|
|
110
|
+
"CONTENT_TYPE" => "application/json",
|
|
111
|
+
"action_dispatch.request.path_parameters" => {"user_id" => "123"}
|
|
112
|
+
}
|
|
113
|
+
webhook = Webhukhs::ReceivedWebhook.new(
|
|
114
|
+
handler_event_id: "request-read",
|
|
115
|
+
handler_module_name: "WebhookTestHandler",
|
|
116
|
+
status: "received",
|
|
117
|
+
body: "payload",
|
|
118
|
+
request_headers: request_headers.deep_dup
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
request = webhook.request
|
|
122
|
+
request_body = request.body.read
|
|
123
|
+
|
|
124
|
+
assert_equal "payload".b, request_body.b
|
|
125
|
+
assert_equal Encoding::ASCII_8BIT, request_body.encoding
|
|
126
|
+
assert_equal request_headers, webhook.request_headers
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
test "request reader coerces the reconstructed body to binary" do
|
|
130
|
+
webhook = Webhukhs::ReceivedWebhook.new
|
|
131
|
+
|
|
132
|
+
request_headers = {
|
|
133
|
+
"CONTENT_TYPE" => "application/json",
|
|
134
|
+
"action_dispatch.request.path_parameters" => {}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
with_overridden_singleton_methods(webhook, {
|
|
138
|
+
body: -> { "payload" },
|
|
139
|
+
request_headers: -> {
|
|
140
|
+
request_headers
|
|
141
|
+
}
|
|
142
|
+
}) do
|
|
143
|
+
request_body = webhook.request.body.read
|
|
144
|
+
|
|
145
|
+
assert_equal "payload".b, request_body.b
|
|
146
|
+
assert_equal Encoding::ASCII_8BIT, request_body.encoding
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
test "assigns uuid ids for string primary keys" do
|
|
151
|
+
with_overridden_singleton_methods(SecureRandom, {
|
|
152
|
+
respond_to?: ->(method_name, include_all = false) {
|
|
153
|
+
method_name == :uuid_v7 || super(method_name, include_all)
|
|
154
|
+
},
|
|
155
|
+
uuid_v7: -> {
|
|
156
|
+
"00000000-0000-7000-8000-000000000001"
|
|
157
|
+
},
|
|
158
|
+
uuid: -> {
|
|
159
|
+
"00000000-0000-4000-8000-000000000002"
|
|
160
|
+
}
|
|
161
|
+
}) do
|
|
162
|
+
webhook = StringIdReceivedWebhook.create!(
|
|
163
|
+
handler_event_id: "generated-id-event",
|
|
164
|
+
handler_module_name: "WebhookTestHandler",
|
|
165
|
+
status: "received",
|
|
166
|
+
body: {}.to_json,
|
|
167
|
+
request_headers: {}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
assert_equal "00000000-0000-7000-8000-000000000001", webhook.id
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
test "falls back to uuid when uuid_v7 is unavailable" do
|
|
175
|
+
with_overridden_singleton_methods(SecureRandom, {
|
|
176
|
+
respond_to?: ->(method_name, include_all = false) {
|
|
177
|
+
(method_name == :uuid_v7) ? false : super(method_name, include_all)
|
|
178
|
+
},
|
|
179
|
+
uuid_v7: -> {
|
|
180
|
+
raise "uuid_v7 should not be called when unavailable"
|
|
181
|
+
},
|
|
182
|
+
uuid: -> {
|
|
183
|
+
"00000000-0000-4000-8000-000000000002"
|
|
184
|
+
}
|
|
185
|
+
}) do
|
|
186
|
+
webhook = StringIdReceivedWebhook.create!(
|
|
187
|
+
handler_event_id: "fallback-id-event",
|
|
188
|
+
handler_module_name: "WebhookTestHandler",
|
|
189
|
+
status: "received",
|
|
190
|
+
body: {}.to_json,
|
|
191
|
+
request_headers: {}
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
assert_equal "00000000-0000-4000-8000-000000000002", webhook.id
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
test "does not call uuid generation for integer primary keys" do
|
|
199
|
+
with_overridden_singleton_methods(SecureRandom, {
|
|
200
|
+
uuid_v7: -> {
|
|
201
|
+
raise "uuid_v7 should not be called for integer primary keys"
|
|
202
|
+
},
|
|
203
|
+
uuid: -> {
|
|
204
|
+
raise "uuid should not be called for integer primary keys"
|
|
205
|
+
}
|
|
206
|
+
}) do
|
|
207
|
+
webhook = Webhukhs::ReceivedWebhook.create!(
|
|
208
|
+
handler_event_id: "integer-id-event",
|
|
209
|
+
handler_module_name: "WebhookTestHandler",
|
|
210
|
+
status: "received",
|
|
211
|
+
body: {}.to_json,
|
|
212
|
+
request_headers: {}
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
assert_kind_of Integer, webhook.id
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
test "does not overwrite explicitly assigned string ids" do
|
|
220
|
+
webhook = StringIdReceivedWebhook.create!(
|
|
221
|
+
id: "manual-id",
|
|
222
|
+
handler_event_id: "manual-id-event",
|
|
223
|
+
handler_module_name: "WebhookTestHandler",
|
|
224
|
+
status: "received",
|
|
225
|
+
body: {}.to_json,
|
|
226
|
+
request_headers: {}
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
assert_equal "manual-id", webhook.id
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
test "assigns a uuid when a string id is blank" do
|
|
233
|
+
webhook = StringIdReceivedWebhook.create!(
|
|
234
|
+
id: "",
|
|
235
|
+
handler_event_id: "blank-id-event",
|
|
236
|
+
handler_module_name: "WebhookTestHandler",
|
|
237
|
+
status: "received",
|
|
238
|
+
body: {}.to_json,
|
|
239
|
+
request_headers: {}
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
assert_match(/\A[0-9a-f-]{36}\z/, webhook.id)
|
|
243
|
+
end
|
|
244
|
+
end
|
data/test/test_app.rb
CHANGED
|
@@ -5,24 +5,46 @@ require "action_pack"
|
|
|
5
5
|
require "action_controller"
|
|
6
6
|
require "rails"
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
8
|
+
module Test
|
|
9
|
+
DEFAULT_DATABASE_PATH = File.expand_path("development.sqlite3", __dir__)
|
|
10
|
+
DEFAULT_DATABASE_URL = "sqlite3:#{DEFAULT_DATABASE_PATH}"
|
|
11
|
+
|
|
12
|
+
def self.database_url
|
|
13
|
+
ENV.fetch("DATABASE_URL", DEFAULT_DATABASE_URL)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.database_path
|
|
17
|
+
File.expand_path(database_url.delete_prefix("sqlite3:"))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.establish_database_connection
|
|
21
|
+
database = database_path
|
|
22
|
+
|
|
23
|
+
ENV["DATABASE_URL"] = database_url
|
|
24
|
+
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: database)
|
|
25
|
+
ActiveRecord::Base.logger = Logger.new(nil)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.define_test_schema
|
|
29
|
+
ActiveRecord::Schema.define do
|
|
30
|
+
create_table "received_webhooks", force: :cascade do |t|
|
|
31
|
+
t.string "handler_event_id", null: false
|
|
32
|
+
t.string "handler_module_name", null: false
|
|
33
|
+
t.string "status", default: "received", null: false
|
|
34
|
+
t.binary "body", null: false
|
|
35
|
+
t.json "request_headers", null: true
|
|
36
|
+
t.datetime "created_at", null: false
|
|
37
|
+
t.datetime "updated_at", null: false
|
|
38
|
+
t.index ["handler_module_name", "handler_event_id"], name: "webhook_dedup_idx", unique: true
|
|
39
|
+
t.index ["status"], name: "index_received_webhooks_on_status"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
23
42
|
end
|
|
24
43
|
end
|
|
25
44
|
|
|
45
|
+
Test.establish_database_connection
|
|
46
|
+
Test.define_test_schema
|
|
47
|
+
|
|
26
48
|
require_relative "../lib/webhukhs"
|
|
27
49
|
require_relative "test-webhook-handlers/webhook_test_handler"
|
|
28
50
|
require_relative "test-webhook-handlers/inactive_handler"
|
data/test/test_helper.rb
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
require_relative "test_app"
|
|
2
2
|
require "rails/test_help"
|
|
3
|
+
require "mutant/minitest/coverage"
|
|
4
|
+
require "active_support/logger"
|
|
5
|
+
require "stringio"
|
|
6
|
+
|
|
7
|
+
if ENV["SIMPLECOV"]
|
|
8
|
+
require "simplecov"
|
|
9
|
+
|
|
10
|
+
SimpleCov.start do
|
|
11
|
+
add_filter "/spec/"
|
|
12
|
+
minimum_coverage 100
|
|
13
|
+
minimum_coverage_by_file 100
|
|
14
|
+
end
|
|
15
|
+
end
|
|
3
16
|
|
|
4
17
|
class ActiveSupport::TestCase
|
|
5
18
|
# Same as "assert_changes" in Rails but for countable entities.
|
|
@@ -47,4 +60,57 @@ class ActiveSupport::TestCase
|
|
|
47
60
|
|
|
48
61
|
retval
|
|
49
62
|
end
|
|
63
|
+
|
|
64
|
+
def with_overridden_singleton_method(target, method_name, implementation, &block)
|
|
65
|
+
with_overridden_singleton_methods(target, {method_name => implementation}, &block)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def with_captured_info_logs(logger_owner)
|
|
69
|
+
messages = []
|
|
70
|
+
original_logger = logger_owner.logger
|
|
71
|
+
test_logger = ActiveSupport::Logger.new(StringIO.new)
|
|
72
|
+
test_logger.level = Logger::INFO
|
|
73
|
+
test_logger.formatter = lambda do |_severity, _time, _progname, message|
|
|
74
|
+
messages << message
|
|
75
|
+
""
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
logger_owner.logger = test_logger
|
|
79
|
+
yield messages
|
|
80
|
+
ensure
|
|
81
|
+
logger_owner.logger = original_logger
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def with_overridden_singleton_methods(target, methods)
|
|
85
|
+
singleton_class = target.singleton_class
|
|
86
|
+
original_names = methods.keys.each_with_index.to_h do |name, index|
|
|
87
|
+
sanitized_name = name.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
|
|
88
|
+
[name, :"__original_#{sanitized_name}_for_test_#{object_id}_#{index}"]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
silence_warnings do
|
|
92
|
+
singleton_class.class_eval do
|
|
93
|
+
original_names.each do |name, original_name|
|
|
94
|
+
alias_method original_name, name
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
methods.each do |name, override|
|
|
98
|
+
define_method(name, &override)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
yield
|
|
104
|
+
ensure
|
|
105
|
+
silence_warnings do
|
|
106
|
+
singleton_class.class_eval do
|
|
107
|
+
original_names.each do |name, original_name|
|
|
108
|
+
next unless method_defined?(original_name)
|
|
109
|
+
|
|
110
|
+
alias_method name, original_name
|
|
111
|
+
remove_method original_name
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
50
116
|
end
|
data/test/webhukhs_test.rb
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "test_helper"
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
class FailingLookupReceiveWebhooksController < Webhukhs::ReceiveWebhooksController
|
|
6
|
+
def initialize(error)
|
|
7
|
+
super()
|
|
8
|
+
@error = error
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def service_id = "broken"
|
|
12
|
+
|
|
13
|
+
def lookup_handler(_service_id)
|
|
14
|
+
raise @error
|
|
15
|
+
end
|
|
16
|
+
end
|
|
5
17
|
|
|
6
18
|
class TestWebhukhs < ActionDispatch::IntegrationTest
|
|
19
|
+
cover "Webhukhs*"
|
|
20
|
+
cover "Webhukhs::BaseHandler*"
|
|
21
|
+
cover "Webhukhs::Configuration*"
|
|
22
|
+
cover "Webhukhs::ProcessingJob*"
|
|
23
|
+
cover "Webhukhs::ReceiveWebhooksController*"
|
|
24
|
+
cover "Webhukhs::ReceivedWebhook*"
|
|
25
|
+
|
|
7
26
|
teardown { Webhukhs::ReceivedWebhook.delete_all }
|
|
8
27
|
|
|
9
28
|
def test_that_it_has_a_version_number
|
|
@@ -28,6 +47,39 @@ class TestWebhukhs < ActionDispatch::IntegrationTest
|
|
|
28
47
|
test(msg) { skip }
|
|
29
48
|
end
|
|
30
49
|
|
|
50
|
+
test "returns a memoized configuration object and yields it to configure" do
|
|
51
|
+
original_configuration = Webhukhs.instance_variable_get(:@configuration)
|
|
52
|
+
Webhukhs.remove_instance_variable(:@configuration) if Webhukhs.instance_variable_defined?(:@configuration)
|
|
53
|
+
|
|
54
|
+
configuration = Webhukhs.configuration
|
|
55
|
+
|
|
56
|
+
assert_instance_of Webhukhs::Configuration, configuration
|
|
57
|
+
assert_same configuration, Webhukhs.configuration
|
|
58
|
+
|
|
59
|
+
yielded = nil
|
|
60
|
+
Webhukhs.configure { |config| yielded = config }
|
|
61
|
+
|
|
62
|
+
assert_same configuration, yielded
|
|
63
|
+
ensure
|
|
64
|
+
Webhukhs.instance_variable_set(:@configuration, original_configuration)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
test "applies the configured error context before handling requests" do
|
|
68
|
+
original_error_context = Webhukhs.configuration.error_context
|
|
69
|
+
|
|
70
|
+
Webhukhs.configuration.error_context = {component: "webhooks"}
|
|
71
|
+
|
|
72
|
+
report = assert_error_reported(RuntimeError) do
|
|
73
|
+
post "/webhukhs/failing-with-concealed-errors", params: webhook_body, headers: {"CONTENT_TYPE" => "application/json"}
|
|
74
|
+
|
|
75
|
+
assert_response 200
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
assert_equal "webhooks", report.context[:component]
|
|
79
|
+
ensure
|
|
80
|
+
Webhukhs.configuration.error_context = original_error_context
|
|
81
|
+
end
|
|
82
|
+
|
|
31
83
|
test "ensure webhook is processed only once during creation" do
|
|
32
84
|
tf = Tempfile.new
|
|
33
85
|
body = {isValid: true, outputToFilename: tf.path}
|
|
@@ -36,6 +88,7 @@ class TestWebhukhs < ActionDispatch::IntegrationTest
|
|
|
36
88
|
assert_enqueued_jobs 1, only: Webhukhs::ProcessingJob do
|
|
37
89
|
post "/webhukhs/test", params: body_json, headers: {"CONTENT_TYPE" => "application/json"}
|
|
38
90
|
assert_response 200
|
|
91
|
+
assert_equal({"ok" => true, "error" => nil}, response.parsed_body)
|
|
39
92
|
end
|
|
40
93
|
end
|
|
41
94
|
|
|
@@ -133,23 +186,40 @@ class TestWebhukhs < ActionDispatch::IntegrationTest
|
|
|
133
186
|
end
|
|
134
187
|
|
|
135
188
|
test "raises an error if the service_id is not known" do
|
|
136
|
-
|
|
137
|
-
|
|
189
|
+
report = assert_error_reported(Webhukhs::ReceiveWebhooksController::UnknownHandler) do
|
|
190
|
+
post "/webhukhs/missing_service", params: webhook_body, headers: {"CONTENT_TYPE" => "application/json"}
|
|
191
|
+
|
|
192
|
+
assert_response 404
|
|
193
|
+
assert_equal "No handler found for \"missing_service\"", response.parsed_body["error"]
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
assert_predicate report, :handled?
|
|
197
|
+
assert_equal :error, report.severity
|
|
138
198
|
end
|
|
139
199
|
|
|
140
200
|
test "returns a 503 when a handler is inactive" do
|
|
141
|
-
|
|
201
|
+
report = assert_error_reported(Webhukhs::ReceiveWebhooksController::HandlerInactive) do
|
|
202
|
+
post "/webhukhs/inactive", params: webhook_body, headers: {"CONTENT_TYPE" => "application/json"}
|
|
203
|
+
|
|
204
|
+
assert_response 503
|
|
205
|
+
assert_equal 'Webhook handler "inactive" is inactive', response.parsed_body["error"]
|
|
206
|
+
end
|
|
142
207
|
|
|
143
|
-
|
|
144
|
-
assert_equal
|
|
208
|
+
assert_predicate report, :handled?
|
|
209
|
+
assert_equal :error, report.severity
|
|
145
210
|
end
|
|
146
211
|
|
|
147
212
|
test "returns a 200 status and error message if the handler does not expose errors" do
|
|
148
|
-
|
|
213
|
+
report = assert_error_reported(RuntimeError) do
|
|
214
|
+
post "/webhukhs/failing-with-concealed-errors", params: webhook_body, headers: {"CONTENT_TYPE" => "application/json"}
|
|
149
215
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
216
|
+
assert_response 200
|
|
217
|
+
assert_equal false, response.parsed_body["ok"]
|
|
218
|
+
assert_equal "Internal error (oops)", response.parsed_body["error"]
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
assert_predicate report, :handled?
|
|
222
|
+
assert_equal :error, report.severity
|
|
153
223
|
end
|
|
154
224
|
|
|
155
225
|
test "returns a 500 status and error message if the handler does not expose errors" do
|
|
@@ -160,6 +230,40 @@ class TestWebhukhs < ActionDispatch::IntegrationTest
|
|
|
160
230
|
# common Rails error page
|
|
161
231
|
end
|
|
162
232
|
|
|
233
|
+
test "re-raises lookup errors before a handler has been assigned" do
|
|
234
|
+
original_active_handlers = Webhukhs.configuration.active_handlers
|
|
235
|
+
Webhukhs.configuration.active_handlers = original_active_handlers.merge(broken: "MissingHandler")
|
|
236
|
+
|
|
237
|
+
post "/webhukhs/broken", params: webhook_body, headers: {"CONTENT_TYPE" => "application/json"}
|
|
238
|
+
|
|
239
|
+
assert_response 500
|
|
240
|
+
assert_includes response.body, "NameError"
|
|
241
|
+
assert_includes response.body, "MissingHandler"
|
|
242
|
+
ensure
|
|
243
|
+
Webhukhs.configuration.active_handlers = original_active_handlers
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
test "re-raises the original error when lookup fails before assigning a handler" do
|
|
247
|
+
original_error = NameError.new("uninitialized constant MissingHandler")
|
|
248
|
+
controller = FailingLookupReceiveWebhooksController.new(original_error)
|
|
249
|
+
|
|
250
|
+
raised_error = assert_raises(NameError) { controller.create }
|
|
251
|
+
|
|
252
|
+
assert_same original_error, raised_error
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
test "accepts handlers configured as class constants" do
|
|
256
|
+
original_active_handlers = Webhukhs.configuration.active_handlers
|
|
257
|
+
Webhukhs.configuration.active_handlers = original_active_handlers.merge(class_handler: WebhookTestHandler)
|
|
258
|
+
|
|
259
|
+
post "/webhukhs/class_handler", params: {isValid: false, outputToFilename: "/tmp/unused"}.to_json, headers: {"CONTENT_TYPE" => "application/json"}
|
|
260
|
+
|
|
261
|
+
assert_response 200
|
|
262
|
+
assert_equal({"ok" => true, "error" => nil}, response.parsed_body)
|
|
263
|
+
ensure
|
|
264
|
+
Webhukhs.configuration.active_handlers = original_active_handlers
|
|
265
|
+
end
|
|
266
|
+
|
|
163
267
|
test "deduplicates received webhooks based on the event ID" do
|
|
164
268
|
body = {event_id: SecureRandom.uuid, body: "test"}.to_json
|
|
165
269
|
|
|
@@ -200,4 +304,16 @@ class TestWebhukhs < ActionDispatch::IntegrationTest
|
|
|
200
304
|
assert_equal "received", webhook.status
|
|
201
305
|
end
|
|
202
306
|
end
|
|
307
|
+
|
|
308
|
+
test "ensure_id does not override id for integer primary key columns" do
|
|
309
|
+
webhook = Webhukhs::ReceivedWebhook.create!(
|
|
310
|
+
handler_event_id: SecureRandom.uuid,
|
|
311
|
+
handler_module_name: "WebhookTestHandler",
|
|
312
|
+
status: "received",
|
|
313
|
+
body: {isValid: true}.to_json
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
assert_kind_of Integer, webhook.id
|
|
317
|
+
assert webhook.id > 0
|
|
318
|
+
end
|
|
203
319
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: webhukhs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stanislav Katkov
|
|
@@ -108,9 +108,7 @@ dependencies:
|
|
|
108
108
|
- !ruby/object:Gem::Version
|
|
109
109
|
version: '5.0'
|
|
110
110
|
description: Webhukhs is a Rails engine for processing webhooks from various services.
|
|
111
|
-
Engine saves webhook in database first and later processes
|
|
112
|
-
Supports custom handlers with Webhukhs::BaseHandler and integrates with Rails common
|
|
113
|
-
error reporter.
|
|
111
|
+
Engine saves webhook in database first and later processes in async job.
|
|
114
112
|
email:
|
|
115
113
|
- github@skatkov.com
|
|
116
114
|
executables: []
|
|
@@ -120,11 +118,15 @@ files:
|
|
|
120
118
|
- ".editorconfig"
|
|
121
119
|
- ".rubocop.yml"
|
|
122
120
|
- ".standard.yml"
|
|
121
|
+
- ".yard-lint.yml"
|
|
122
|
+
- AGENTS.md
|
|
123
123
|
- Appraisals
|
|
124
124
|
- CHANGELOG.md
|
|
125
125
|
- LICENSE
|
|
126
126
|
- README.md
|
|
127
127
|
- Rakefile
|
|
128
|
+
- config/mutant.rb
|
|
129
|
+
- config/mutant.yml
|
|
128
130
|
- config/routes.rb
|
|
129
131
|
- example/.gitattributes
|
|
130
132
|
- example/.gitignore
|
|
@@ -193,6 +195,7 @@ files:
|
|
|
193
195
|
- gemfiles/rails_8.0.gemfile.lock
|
|
194
196
|
- gemfiles/rails_8.1.gemfile
|
|
195
197
|
- gemfiles/rails_8.1.gemfile.lock
|
|
198
|
+
- gemfiles/rails_master.gemfile
|
|
196
199
|
- handler-examples/customer_io_handler.rb
|
|
197
200
|
- handler-examples/revolut_business_v1_handler.rb
|
|
198
201
|
- handler-examples/revolut_business_v2_handler.rb
|
|
@@ -209,6 +212,9 @@ files:
|
|
|
209
212
|
- lib/webhukhs/templates/create_webhukhs_tables.rb.erb
|
|
210
213
|
- lib/webhukhs/templates/webhukhs.rb
|
|
211
214
|
- lib/webhukhs/version.rb
|
|
215
|
+
- test/base_handler_test.rb
|
|
216
|
+
- test/processing_job_test.rb
|
|
217
|
+
- test/received_webhook_test.rb
|
|
212
218
|
- test/test-webhook-handlers/.DS_Store
|
|
213
219
|
- test/test-webhook-handlers/extract_id_handler.rb
|
|
214
220
|
- test/test-webhook-handlers/failing_with_concealed_errors.rb
|
|
@@ -241,7 +247,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
241
247
|
- !ruby/object:Gem::Version
|
|
242
248
|
version: '0'
|
|
243
249
|
requirements: []
|
|
244
|
-
rubygems_version: 4.0.
|
|
250
|
+
rubygems_version: 4.0.10
|
|
245
251
|
specification_version: 4
|
|
246
252
|
summary: Webhooks processing engine for Rails applications
|
|
247
253
|
test_files: []
|