webhukhs 0.6.0 → 0.8.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 +296 -0
- data/AGENTS.md +56 -0
- data/Appraisals +6 -2
- data/CHANGELOG.md +20 -0
- data/README.md +101 -11
- data/Rakefile +7 -0
- data/config/mutant.rb +37 -0
- data/config/mutant.yml +25 -0
- data/example/Gemfile +5 -6
- data/example/Gemfile.lock +260 -141
- data/example/app/webhooks/acme_handler.rb +16 -0
- data/example/app/webhooks/flaky_handler.rb +8 -0
- data/example/app/webhooks/signed_handler.rb +10 -0
- data/example/config/initializers/webhukhs.rb +4 -3
- data/example/config/routes.rb +3 -0
- data/example/db/migrate/20240601000000_add_headers_to_webhukhs_webhooks.rb +7 -0
- data/example/db/schema.rb +2 -1
- data/example/db/seeds.rb +71 -7
- data/gemfiles/{rails_7.1.gemfile → rails_7.2.gemfile} +1 -4
- data/gemfiles/rails_7.2.gemfile.lock +477 -0
- data/gemfiles/rails_8.0.gemfile +0 -3
- data/gemfiles/rails_8.0.gemfile.lock +74 -4
- data/gemfiles/rails_8.1.gemfile +0 -3
- data/gemfiles/rails_8.1.gemfile.lock +74 -4
- data/gemfiles/rails_master.gemfile +7 -0
- data/lib/webhukhs/admin/application_controller.rb +32 -0
- data/lib/webhukhs/admin/assets/admin.css +476 -0
- data/lib/webhukhs/admin/assets/admin.js +111 -0
- data/lib/webhukhs/admin/config/routes.rb +6 -0
- data/lib/webhukhs/admin/engine.rb +20 -0
- data/lib/webhukhs/admin/received_webhook_presenter.rb +63 -0
- data/lib/webhukhs/admin/received_webhooks_controller.rb +60 -0
- data/lib/webhukhs/admin/view_helper.rb +50 -0
- data/lib/webhukhs/admin/views/layouts/webhukhs/admin/application.html.erb +22 -0
- data/lib/webhukhs/admin/views/webhukhs/admin/received_webhooks/index.html.erb +50 -0
- data/lib/webhukhs/admin/views/webhukhs/admin/received_webhooks/show.html.erb +55 -0
- data/lib/webhukhs/admin.rb +17 -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 -4
- data/lib/webhukhs/install_generator.rb +9 -2
- data/lib/webhukhs/jobs/processing_job.rb +13 -12
- data/lib/webhukhs/models/received_webhook.rb +21 -9
- data/lib/webhukhs/version.rb +1 -1
- data/lib/webhukhs.rb +9 -0
- data/test/admin_test.rb +334 -0
- data/test/base_handler_test.rb +125 -0
- data/test/browser/admin_click_through.rb +208 -0
- data/test/processing_job_test.rb +152 -12
- data/test/received_webhook_test.rb +260 -0
- data/test/test_app.rb +39 -15
- data/test/test_helper.rb +69 -0
- data/test/webhukhs_test.rb +111 -49
- metadata +173 -11
- data/example/.ruby-version +0 -1
- data/gemfiles/rails_7.1.gemfile.lock +0 -412
data/test/processing_job_test.rb
CHANGED
|
@@ -1,25 +1,167 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "test_helper"
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
class SkipDuringProcessingHandler < Webhukhs::BaseHandler
|
|
6
|
+
def process(webhook)
|
|
7
|
+
webhook.skipped!
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class LoggingHandler < Webhukhs::BaseHandler
|
|
12
|
+
class_attribute :processed_webhook_ids, default: []
|
|
13
|
+
|
|
14
|
+
def self.reset!
|
|
15
|
+
self.processed_webhook_ids = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_s = self.class.name
|
|
19
|
+
|
|
20
|
+
def valid?(_request) = true
|
|
21
|
+
|
|
22
|
+
def process(webhook)
|
|
23
|
+
self.class.processed_webhook_ids += [webhook.id]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class InvalidLoggingHandler < LoggingHandler
|
|
28
|
+
def valid?(_request) = false
|
|
29
|
+
end
|
|
5
30
|
|
|
6
31
|
class ProcessingJobTest < ActiveJob::TestCase
|
|
7
|
-
|
|
32
|
+
cover "Webhukhs::ProcessingJob*"
|
|
33
|
+
|
|
34
|
+
teardown do
|
|
35
|
+
Webhukhs::ReceivedWebhook.delete_all
|
|
36
|
+
LoggingHandler.reset!
|
|
37
|
+
InvalidLoggingHandler.reset!
|
|
38
|
+
end
|
|
8
39
|
|
|
9
|
-
test "discards job and reports
|
|
10
|
-
|
|
11
|
-
Webhukhs::ProcessingJob
|
|
40
|
+
test "discards job and reports invalid webhook arguments" do
|
|
41
|
+
[nil, "not a webhook"].each do |argument|
|
|
42
|
+
assert_error_reported(Webhukhs::ProcessingJob::InvalidWebhookArgument) do
|
|
43
|
+
Webhukhs::ProcessingJob.perform_now(argument)
|
|
44
|
+
end
|
|
12
45
|
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
test "raises specific invalid webhook argument messages" do
|
|
49
|
+
{
|
|
50
|
+
nil => "ProcessingJob received nil webhook argument",
|
|
51
|
+
"not a webhook" => "ProcessingJob expected Webhukhs::ReceivedWebhook, got String"
|
|
52
|
+
}.each do |argument, message|
|
|
53
|
+
error = assert_raises(Webhukhs::ProcessingJob::InvalidWebhookArgument) do
|
|
54
|
+
Webhukhs::ProcessingJob.new.perform(argument)
|
|
55
|
+
end
|
|
13
56
|
|
|
14
|
-
|
|
57
|
+
assert_equal message, error.message
|
|
58
|
+
end
|
|
15
59
|
end
|
|
16
60
|
|
|
17
|
-
test "
|
|
18
|
-
|
|
19
|
-
|
|
61
|
+
test "processes webhooks under a lock" do
|
|
62
|
+
webhook = Webhukhs::ReceivedWebhook.create!(
|
|
63
|
+
handler_event_id: "lock-test",
|
|
64
|
+
handler_module_name: "WebhookTestHandler",
|
|
65
|
+
status: "received",
|
|
66
|
+
body: {isValid: false}.to_json,
|
|
67
|
+
request_headers: {
|
|
68
|
+
"CONTENT_TYPE" => "application/json",
|
|
69
|
+
"action_dispatch.request.path_parameters" => {}
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
with_lock_called = false
|
|
73
|
+
|
|
74
|
+
webhook.define_singleton_method(:with_lock) do |&block|
|
|
75
|
+
with_lock_called = true
|
|
76
|
+
block.call
|
|
20
77
|
end
|
|
21
78
|
|
|
22
|
-
|
|
79
|
+
Webhukhs::ProcessingJob.new.perform(webhook)
|
|
80
|
+
|
|
81
|
+
assert_true with_lock_called
|
|
82
|
+
assert_predicate webhook.reload, :failed_validation?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
test "logs when processing is skipped because the webhook is not received" do
|
|
86
|
+
webhook = Webhukhs::ReceivedWebhook.create!(
|
|
87
|
+
handler_event_id: "skip-log-test",
|
|
88
|
+
handler_module_name: "LoggingHandler",
|
|
89
|
+
status: "processing",
|
|
90
|
+
body: {}.to_json,
|
|
91
|
+
request_headers: {}
|
|
92
|
+
)
|
|
93
|
+
job = Webhukhs::ProcessingJob.new
|
|
94
|
+
details = "Webhukhs::ReceivedWebhook##{webhook.id} (handler: LoggingHandler)"
|
|
95
|
+
|
|
96
|
+
with_captured_info_logs(Webhukhs::ProcessingJob) do |messages|
|
|
97
|
+
job.perform(webhook)
|
|
98
|
+
|
|
99
|
+
assert_equal ["#{details} is being processed in a different job or has been processed already, skipping."], messages
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
test "logs when processing starts and completes" do
|
|
104
|
+
webhook = Webhukhs::ReceivedWebhook.create!(
|
|
105
|
+
handler_event_id: "success-log-test",
|
|
106
|
+
handler_module_name: "LoggingHandler",
|
|
107
|
+
status: "received",
|
|
108
|
+
body: {}.to_json,
|
|
109
|
+
request_headers: {
|
|
110
|
+
"CONTENT_TYPE" => "application/json",
|
|
111
|
+
"action_dispatch.request.path_parameters" => {}
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
job = Webhukhs::ProcessingJob.new
|
|
115
|
+
details = "Webhukhs::ReceivedWebhook##{webhook.id} (handler: LoggingHandler)"
|
|
116
|
+
|
|
117
|
+
with_captured_info_logs(Webhukhs::ProcessingJob) do |messages|
|
|
118
|
+
job.perform(webhook)
|
|
119
|
+
|
|
120
|
+
assert_equal ["#{details} starting to process", "#{details} processed"], messages
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
assert_predicate webhook.reload, :processed?
|
|
124
|
+
assert_equal [webhook.id], LoggingHandler.processed_webhook_ids
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
test "logs when validation fails" do
|
|
128
|
+
webhook = Webhukhs::ReceivedWebhook.create!(
|
|
129
|
+
handler_event_id: "failed-validation-log-test",
|
|
130
|
+
handler_module_name: "InvalidLoggingHandler",
|
|
131
|
+
status: "received",
|
|
132
|
+
body: {}.to_json,
|
|
133
|
+
request_headers: {
|
|
134
|
+
"CONTENT_TYPE" => "application/json",
|
|
135
|
+
"action_dispatch.request.path_parameters" => {}
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
job = Webhukhs::ProcessingJob.new
|
|
139
|
+
details = "Webhukhs::ReceivedWebhook##{webhook.id} (handler: InvalidLoggingHandler)"
|
|
140
|
+
|
|
141
|
+
with_captured_info_logs(Webhukhs::ProcessingJob) do |messages|
|
|
142
|
+
job.perform(webhook)
|
|
143
|
+
|
|
144
|
+
assert_equal ["#{details} did not pass validation by the handler. Marking it `failed_validation`."], messages
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
assert_predicate webhook.reload, :failed_validation?
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
test "does not overwrite webhook state changed by the handler" do
|
|
151
|
+
webhook = Webhukhs::ReceivedWebhook.create!(
|
|
152
|
+
handler_event_id: "skip-test",
|
|
153
|
+
handler_module_name: "SkipDuringProcessingHandler",
|
|
154
|
+
status: "received",
|
|
155
|
+
body: {}.to_json,
|
|
156
|
+
request_headers: {
|
|
157
|
+
"CONTENT_TYPE" => "application/json",
|
|
158
|
+
"action_dispatch.request.path_parameters" => {}
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
Webhukhs::ProcessingJob.new.perform(webhook)
|
|
163
|
+
|
|
164
|
+
assert_predicate webhook.reload, :skipped?
|
|
23
165
|
end
|
|
24
166
|
|
|
25
167
|
test "discards job and reports error when webhook record has been deleted (DeserializationError)" do
|
|
@@ -36,7 +178,5 @@ class ProcessingJobTest < ActiveJob::TestCase
|
|
|
36
178
|
assert_error_reported(ActiveJob::DeserializationError) do
|
|
37
179
|
perform_enqueued_jobs
|
|
38
180
|
end
|
|
39
|
-
|
|
40
|
-
assert_no_enqueued_jobs only: Webhukhs::ProcessingJob
|
|
41
181
|
end
|
|
42
182
|
end
|
|
@@ -0,0 +1,260 @@
|
|
|
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 stores the whole body even if the body io was already read" do
|
|
69
|
+
body_io = StringIO.new("event=paid")
|
|
70
|
+
body_io.read
|
|
71
|
+
request = ActionDispatch::Request.new(
|
|
72
|
+
"CONTENT_TYPE" => "application/x-www-form-urlencoded",
|
|
73
|
+
"rack.input" => body_io,
|
|
74
|
+
"action_dispatch.request.path_parameters" => {}
|
|
75
|
+
)
|
|
76
|
+
webhook = Webhukhs::ReceivedWebhook.new
|
|
77
|
+
|
|
78
|
+
webhook.request = request
|
|
79
|
+
|
|
80
|
+
assert_equal "event=paid".b, webhook.body
|
|
81
|
+
assert_equal 0, body_io.pos
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
test "request writer coerces the stored body to binary before type casting" do
|
|
85
|
+
body_io = StringIO.new("payload")
|
|
86
|
+
request = ActionDispatch::Request.new(
|
|
87
|
+
"CONTENT_TYPE" => "application/json",
|
|
88
|
+
"rack.input" => body_io,
|
|
89
|
+
"action_dispatch.request.path_parameters" => {}
|
|
90
|
+
)
|
|
91
|
+
webhook = Webhukhs::ReceivedWebhook.new
|
|
92
|
+
captured_body = nil
|
|
93
|
+
|
|
94
|
+
with_overridden_singleton_method(webhook, :write_attribute, ->(name, value) {
|
|
95
|
+
captured_body = value if name == "body"
|
|
96
|
+
}) do
|
|
97
|
+
webhook.request = request
|
|
98
|
+
|
|
99
|
+
assert_equal "payload".b, captured_body
|
|
100
|
+
assert_equal Encoding::ASCII_8BIT, captured_body.encoding
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
test "request rejects oversized bodies and rewinds the body io" do
|
|
105
|
+
original_limit = Webhukhs.configuration.request_body_size_limit
|
|
106
|
+
Webhukhs.configuration.request_body_size_limit = 3
|
|
107
|
+
|
|
108
|
+
body_io = StringIO.new("four")
|
|
109
|
+
request = ActionDispatch::Request.new(
|
|
110
|
+
"CONTENT_TYPE" => "application/json",
|
|
111
|
+
"rack.input" => body_io,
|
|
112
|
+
"action_dispatch.request.path_parameters" => {}
|
|
113
|
+
)
|
|
114
|
+
webhook = Webhukhs::ReceivedWebhook.new
|
|
115
|
+
|
|
116
|
+
error = assert_raises(RuntimeError) { webhook.request = request }
|
|
117
|
+
|
|
118
|
+
assert_equal "Cannot accept the webhook as the request body is larger than 3 bytes", error.message
|
|
119
|
+
assert_equal 0, body_io.pos
|
|
120
|
+
ensure
|
|
121
|
+
Webhukhs.configuration.request_body_size_limit = original_limit
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
test "request reconstructs the body without mutating stored headers" do
|
|
125
|
+
request_headers = {
|
|
126
|
+
"CONTENT_TYPE" => "application/json",
|
|
127
|
+
"action_dispatch.request.path_parameters" => {"user_id" => "123"}
|
|
128
|
+
}
|
|
129
|
+
webhook = Webhukhs::ReceivedWebhook.new(
|
|
130
|
+
handler_event_id: "request-read",
|
|
131
|
+
handler_module_name: "WebhookTestHandler",
|
|
132
|
+
status: "received",
|
|
133
|
+
body: "payload",
|
|
134
|
+
request_headers: request_headers.deep_dup
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
request = webhook.request
|
|
138
|
+
request_body = request.body.read
|
|
139
|
+
|
|
140
|
+
assert_equal "payload".b, request_body.b
|
|
141
|
+
assert_equal Encoding::ASCII_8BIT, request_body.encoding
|
|
142
|
+
assert_equal request_headers, webhook.request_headers
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
test "request reader coerces the reconstructed body to binary" do
|
|
146
|
+
webhook = Webhukhs::ReceivedWebhook.new
|
|
147
|
+
|
|
148
|
+
request_headers = {
|
|
149
|
+
"CONTENT_TYPE" => "application/json",
|
|
150
|
+
"action_dispatch.request.path_parameters" => {}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
with_overridden_singleton_methods(webhook, {
|
|
154
|
+
body: -> { "payload" },
|
|
155
|
+
request_headers: -> {
|
|
156
|
+
request_headers
|
|
157
|
+
}
|
|
158
|
+
}) do
|
|
159
|
+
request_body = webhook.request.body.read
|
|
160
|
+
|
|
161
|
+
assert_equal "payload".b, request_body.b
|
|
162
|
+
assert_equal Encoding::ASCII_8BIT, request_body.encoding
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
test "assigns uuid ids for string primary keys" do
|
|
167
|
+
with_overridden_singleton_methods(SecureRandom, {
|
|
168
|
+
respond_to?: ->(method_name, include_all = false) {
|
|
169
|
+
method_name == :uuid_v7 || super(method_name, include_all)
|
|
170
|
+
},
|
|
171
|
+
uuid_v7: -> {
|
|
172
|
+
"00000000-0000-7000-8000-000000000001"
|
|
173
|
+
},
|
|
174
|
+
uuid: -> {
|
|
175
|
+
"00000000-0000-4000-8000-000000000002"
|
|
176
|
+
}
|
|
177
|
+
}) do
|
|
178
|
+
webhook = StringIdReceivedWebhook.create!(
|
|
179
|
+
handler_event_id: "generated-id-event",
|
|
180
|
+
handler_module_name: "WebhookTestHandler",
|
|
181
|
+
status: "received",
|
|
182
|
+
body: {}.to_json,
|
|
183
|
+
request_headers: {}
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
assert_equal "00000000-0000-7000-8000-000000000001", webhook.id
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
test "falls back to uuid when uuid_v7 is unavailable" do
|
|
191
|
+
with_overridden_singleton_methods(SecureRandom, {
|
|
192
|
+
respond_to?: ->(method_name, include_all = false) {
|
|
193
|
+
(method_name == :uuid_v7) ? false : super(method_name, include_all)
|
|
194
|
+
},
|
|
195
|
+
uuid_v7: -> {
|
|
196
|
+
raise "uuid_v7 should not be called when unavailable"
|
|
197
|
+
},
|
|
198
|
+
uuid: -> {
|
|
199
|
+
"00000000-0000-4000-8000-000000000002"
|
|
200
|
+
}
|
|
201
|
+
}) do
|
|
202
|
+
webhook = StringIdReceivedWebhook.create!(
|
|
203
|
+
handler_event_id: "fallback-id-event",
|
|
204
|
+
handler_module_name: "WebhookTestHandler",
|
|
205
|
+
status: "received",
|
|
206
|
+
body: {}.to_json,
|
|
207
|
+
request_headers: {}
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
assert_equal "00000000-0000-4000-8000-000000000002", webhook.id
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
test "does not call uuid generation for integer primary keys" do
|
|
215
|
+
with_overridden_singleton_methods(SecureRandom, {
|
|
216
|
+
uuid_v7: -> {
|
|
217
|
+
raise "uuid_v7 should not be called for integer primary keys"
|
|
218
|
+
},
|
|
219
|
+
uuid: -> {
|
|
220
|
+
raise "uuid should not be called for integer primary keys"
|
|
221
|
+
}
|
|
222
|
+
}) do
|
|
223
|
+
webhook = Webhukhs::ReceivedWebhook.create!(
|
|
224
|
+
handler_event_id: "integer-id-event",
|
|
225
|
+
handler_module_name: "WebhookTestHandler",
|
|
226
|
+
status: "received",
|
|
227
|
+
body: {}.to_json,
|
|
228
|
+
request_headers: {}
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
assert_kind_of Integer, webhook.id
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
test "does not overwrite explicitly assigned string ids" do
|
|
236
|
+
webhook = StringIdReceivedWebhook.create!(
|
|
237
|
+
id: "manual-id",
|
|
238
|
+
handler_event_id: "manual-id-event",
|
|
239
|
+
handler_module_name: "WebhookTestHandler",
|
|
240
|
+
status: "received",
|
|
241
|
+
body: {}.to_json,
|
|
242
|
+
request_headers: {}
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
assert_equal "manual-id", webhook.id
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
test "assigns a uuid when a string id is blank" do
|
|
249
|
+
webhook = StringIdReceivedWebhook.create!(
|
|
250
|
+
id: "",
|
|
251
|
+
handler_event_id: "blank-id-event",
|
|
252
|
+
handler_module_name: "WebhookTestHandler",
|
|
253
|
+
status: "received",
|
|
254
|
+
body: {}.to_json,
|
|
255
|
+
request_headers: {}
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
assert_match(/\A[0-9a-f-]{36}\z/, webhook.id)
|
|
259
|
+
end
|
|
260
|
+
end
|
data/test/test_app.rb
CHANGED
|
@@ -5,25 +5,48 @@ 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"
|
|
49
|
+
require_relative "../lib/webhukhs/admin"
|
|
27
50
|
require_relative "test-webhook-handlers/webhook_test_handler"
|
|
28
51
|
require_relative "test-webhook-handlers/inactive_handler"
|
|
29
52
|
require_relative "test-webhook-handlers/invalid_handler"
|
|
@@ -58,6 +81,7 @@ class WebhukhsTestApp < Rails::Application
|
|
|
58
81
|
routes.append do
|
|
59
82
|
mount Webhukhs::Engine, at: "/webhukhs"
|
|
60
83
|
post "/per-user-webhukhs/:user_id/:service_id" => "webhukhs/receive_webhooks#create"
|
|
84
|
+
mount Webhukhs::Admin::Engine, at: "/admin"
|
|
61
85
|
end
|
|
62
86
|
end
|
|
63
87
|
|
data/test/test_helper.rb
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
|
+
if ENV["SIMPLECOV"]
|
|
2
|
+
require "simplecov"
|
|
3
|
+
|
|
4
|
+
SimpleCov.start do
|
|
5
|
+
add_filter "/spec/"
|
|
6
|
+
add_filter "/test/"
|
|
7
|
+
add_filter "/lib/webhukhs/install_generator.rb"
|
|
8
|
+
minimum_coverage 100
|
|
9
|
+
minimum_coverage_by_file 100
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
1
13
|
require_relative "test_app"
|
|
2
14
|
require "rails/test_help"
|
|
15
|
+
require "minitest/strict"
|
|
16
|
+
require "mutant/minitest/coverage"
|
|
17
|
+
require "active_support/logger"
|
|
18
|
+
require "stringio"
|
|
3
19
|
|
|
4
20
|
class ActiveSupport::TestCase
|
|
5
21
|
# Same as "assert_changes" in Rails but for countable entities.
|
|
@@ -47,4 +63,57 @@ class ActiveSupport::TestCase
|
|
|
47
63
|
|
|
48
64
|
retval
|
|
49
65
|
end
|
|
66
|
+
|
|
67
|
+
def with_overridden_singleton_method(target, method_name, implementation, &block)
|
|
68
|
+
with_overridden_singleton_methods(target, {method_name => implementation}, &block)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def with_captured_info_logs(logger_owner)
|
|
72
|
+
messages = []
|
|
73
|
+
original_logger = logger_owner.logger
|
|
74
|
+
test_logger = ActiveSupport::Logger.new(StringIO.new)
|
|
75
|
+
test_logger.level = Logger::INFO
|
|
76
|
+
test_logger.formatter = lambda do |_severity, _time, _progname, message|
|
|
77
|
+
messages << message
|
|
78
|
+
""
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
logger_owner.logger = test_logger
|
|
82
|
+
yield messages
|
|
83
|
+
ensure
|
|
84
|
+
logger_owner.logger = original_logger
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def with_overridden_singleton_methods(target, methods)
|
|
88
|
+
singleton_class = target.singleton_class
|
|
89
|
+
original_names = methods.keys.each_with_index.to_h do |name, index|
|
|
90
|
+
sanitized_name = name.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
|
|
91
|
+
[name, :"__original_#{sanitized_name}_for_test_#{object_id}_#{index}"]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
silence_warnings do
|
|
95
|
+
singleton_class.class_eval do
|
|
96
|
+
original_names.each do |name, original_name|
|
|
97
|
+
alias_method original_name, name
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
methods.each do |name, override|
|
|
101
|
+
define_method(name, &override)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
yield
|
|
107
|
+
ensure
|
|
108
|
+
silence_warnings do
|
|
109
|
+
singleton_class.class_eval do
|
|
110
|
+
original_names.each do |name, original_name|
|
|
111
|
+
next unless method_defined?(original_name)
|
|
112
|
+
|
|
113
|
+
alias_method name, original_name
|
|
114
|
+
remove_method original_name
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
50
119
|
end
|