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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.standard.yml +2 -0
  3. data/.yard-lint.yml +296 -0
  4. data/AGENTS.md +56 -0
  5. data/Appraisals +6 -2
  6. data/CHANGELOG.md +20 -0
  7. data/README.md +101 -11
  8. data/Rakefile +7 -0
  9. data/config/mutant.rb +37 -0
  10. data/config/mutant.yml +25 -0
  11. data/example/Gemfile +5 -6
  12. data/example/Gemfile.lock +260 -141
  13. data/example/app/webhooks/acme_handler.rb +16 -0
  14. data/example/app/webhooks/flaky_handler.rb +8 -0
  15. data/example/app/webhooks/signed_handler.rb +10 -0
  16. data/example/config/initializers/webhukhs.rb +4 -3
  17. data/example/config/routes.rb +3 -0
  18. data/example/db/migrate/20240601000000_add_headers_to_webhukhs_webhooks.rb +7 -0
  19. data/example/db/schema.rb +2 -1
  20. data/example/db/seeds.rb +71 -7
  21. data/gemfiles/{rails_7.1.gemfile → rails_7.2.gemfile} +1 -4
  22. data/gemfiles/rails_7.2.gemfile.lock +477 -0
  23. data/gemfiles/rails_8.0.gemfile +0 -3
  24. data/gemfiles/rails_8.0.gemfile.lock +74 -4
  25. data/gemfiles/rails_8.1.gemfile +0 -3
  26. data/gemfiles/rails_8.1.gemfile.lock +74 -4
  27. data/gemfiles/rails_master.gemfile +7 -0
  28. data/lib/webhukhs/admin/application_controller.rb +32 -0
  29. data/lib/webhukhs/admin/assets/admin.css +476 -0
  30. data/lib/webhukhs/admin/assets/admin.js +111 -0
  31. data/lib/webhukhs/admin/config/routes.rb +6 -0
  32. data/lib/webhukhs/admin/engine.rb +20 -0
  33. data/lib/webhukhs/admin/received_webhook_presenter.rb +63 -0
  34. data/lib/webhukhs/admin/received_webhooks_controller.rb +60 -0
  35. data/lib/webhukhs/admin/view_helper.rb +50 -0
  36. data/lib/webhukhs/admin/views/layouts/webhukhs/admin/application.html.erb +22 -0
  37. data/lib/webhukhs/admin/views/webhukhs/admin/received_webhooks/index.html.erb +50 -0
  38. data/lib/webhukhs/admin/views/webhukhs/admin/received_webhooks/show.html.erb +55 -0
  39. data/lib/webhukhs/admin.rb +17 -0
  40. data/lib/webhukhs/base_handler.rb +9 -6
  41. data/lib/webhukhs/controllers/receive_webhooks_controller.rb +26 -7
  42. data/lib/webhukhs/engine.rb +1 -4
  43. data/lib/webhukhs/install_generator.rb +9 -2
  44. data/lib/webhukhs/jobs/processing_job.rb +13 -12
  45. data/lib/webhukhs/models/received_webhook.rb +21 -9
  46. data/lib/webhukhs/version.rb +1 -1
  47. data/lib/webhukhs.rb +9 -0
  48. data/test/admin_test.rb +334 -0
  49. data/test/base_handler_test.rb +125 -0
  50. data/test/browser/admin_click_through.rb +208 -0
  51. data/test/processing_job_test.rb +152 -12
  52. data/test/received_webhook_test.rb +260 -0
  53. data/test/test_app.rb +39 -15
  54. data/test/test_helper.rb +69 -0
  55. data/test/webhukhs_test.rb +111 -49
  56. metadata +173 -11
  57. data/example/.ruby-version +0 -1
  58. data/gemfiles/rails_7.1.gemfile.lock +0 -412
@@ -1,25 +1,167 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "test_helper"
4
- require_relative "test_app"
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
- teardown { Webhukhs::ReceivedWebhook.delete_all }
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 error when webhook argument is nil" do
10
- assert_error_reported(Webhukhs::ProcessingJob::InvalidWebhookArgument) do
11
- Webhukhs::ProcessingJob.perform_now(nil)
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
- assert_no_enqueued_jobs only: Webhukhs::ProcessingJob
57
+ assert_equal message, error.message
58
+ end
15
59
  end
16
60
 
17
- test "discards job and reports error when webhook argument is not a ReceivedWebhook" do
18
- assert_error_reported(Webhukhs::ProcessingJob::InvalidWebhookArgument) do
19
- Webhukhs::ProcessingJob.perform_now("not a webhook")
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
- assert_no_enqueued_jobs only: Webhukhs::ProcessingJob
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
- database = "development.sqlite3"
9
- ENV["DATABASE_URL"] = "sqlite3:#{database}"
10
- ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: database)
11
- ActiveRecord::Base.logger = Logger.new(nil)
12
- ActiveRecord::Schema.define do
13
- create_table "received_webhooks", force: :cascade do |t|
14
- t.string "handler_event_id", null: false
15
- t.string "handler_module_name", null: false
16
- t.string "status", default: "received", null: false
17
- t.binary "body", null: false
18
- t.json "request_headers", null: true
19
- t.datetime "created_at", null: false
20
- t.datetime "updated_at", null: false
21
- t.index ["handler_module_name", "handler_event_id"], name: "webhook_dedup_idx", unique: true
22
- t.index ["status"], name: "index_received_webhooks_on_status"
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