munster 0.4.1 → 0.4.2
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/CHANGELOG.md +4 -0
- data/lib/munster/jobs/processing_job.rb +8 -6
- data/lib/munster/version.rb +1 -1
- data/test/munster_test.rb +214 -0
- data/test/test-webhook-handlers/.DS_Store +0 -0
- data/test/test-webhook-handlers/extract_id_handler.rb +5 -0
- data/test/test-webhook-handlers/failing_with_concealed_errors.rb +7 -0
- data/test/test-webhook-handlers/failing_with_exposed_errors.rb +7 -0
- data/test/test-webhook-handlers/inactive_handler.rb +5 -0
- data/test/test-webhook-handlers/invalid_handler.rb +5 -0
- data/test/test-webhook-handlers/private_handler.rb +3 -0
- data/test/test-webhook-handlers/webhook_test_handler.rb +13 -0
- data/test/test_app.rb +47 -0
- data/test/test_helper.rb +50 -0
- metadata +14 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1cc63f1db75010013d7ad528bfcc3d4aa52c8d4242ee93a5baff4ecbe6673d01
|
4
|
+
data.tar.gz: b8458f7196754f2bde9440b37e4fac46f308e713ed21fca28b700c54aa599168
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4a8ee7017935cb805c69f7cf0f31b88356b00466159040d5f67b763d19643844246485b3db0d8f4302dfd3ab02b9ff862cabf1fb541a0b338e237897e1327723
|
7
|
+
data.tar.gz: 6ac65ba57bebe9fce221ae820369b20eb78c69823c021a9d74b58d38122a7ac6e6918ba84e63d16db4b60330684bf358f4ec72c55590afddccfaa3113ce7d841
|
data/CHANGELOG.md
CHANGED
@@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file.
|
|
3
3
|
|
4
4
|
This format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
5
5
|
|
6
|
+
## 0.4.2
|
7
|
+
|
8
|
+
- When processing a webhook, print messages to the ActiveJob logger. This allows for quicker debugging if the app does not have an error tracking service set up
|
9
|
+
|
6
10
|
## 0.4.1
|
7
11
|
|
8
12
|
- Webhook processor now requires `active_job/railtie`, instead of `active_job`. It requires GlobalID to work and that get's required only with railtie.
|
@@ -4,23 +4,25 @@ require "active_job/railtie"
|
|
4
4
|
|
5
5
|
module Munster
|
6
6
|
class ProcessingJob < ActiveJob::Base
|
7
|
-
class WebhookPayloadInvalid < StandardError
|
8
|
-
end
|
9
|
-
|
10
7
|
def perform(webhook)
|
11
8
|
Rails.error.set_context(munster_handler_module_name: webhook.handler_module_name, **Munster.configuration.error_context)
|
12
9
|
|
10
|
+
webhook_details_for_logs = "Munster::ReceivedWebhook#%s (handler: %s)" % [webhook.id, webhook.handler]
|
13
11
|
webhook.with_lock do
|
14
|
-
|
12
|
+
unless webhook.received?
|
13
|
+
logger.info { "#{webhook_details_for_logs} is being processed in a different job or has been processed already, skipping." }
|
14
|
+
return
|
15
|
+
end
|
15
16
|
webhook.processing!
|
16
17
|
end
|
17
18
|
|
18
19
|
if webhook.handler.valid?(webhook.request)
|
20
|
+
logger.info { "#{webhook_details_for_logs} starting to process" }
|
19
21
|
webhook.handler.process(webhook)
|
20
22
|
webhook.processed! if webhook.processing?
|
23
|
+
logger.info { "#{webhook_details_for_logs} processed" }
|
21
24
|
else
|
22
|
-
|
23
|
-
Rails.error.report(e, handled: true, severity: :error)
|
25
|
+
logger.info { "#{webhook_details_for_logs} did not pass validation by the handler. Marking it `failed_validation`." }
|
24
26
|
webhook.failed_validation!
|
25
27
|
end
|
26
28
|
rescue => e
|
data/lib/munster/version.rb
CHANGED
@@ -0,0 +1,214 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
require_relative "test_app"
|
5
|
+
|
6
|
+
class TestMunster < ActionDispatch::IntegrationTest
|
7
|
+
teardown { Munster::ReceivedWebhook.delete_all }
|
8
|
+
|
9
|
+
def test_that_it_has_a_version_number
|
10
|
+
refute_nil ::Munster::VERSION
|
11
|
+
end
|
12
|
+
|
13
|
+
def webhook_body
|
14
|
+
<<~JSON
|
15
|
+
{
|
16
|
+
"provider_id": "musterbank-flyio",
|
17
|
+
"starts_at": "<%= Time.now.utc %>",
|
18
|
+
"external_source": "The Forge Of Downtime",
|
19
|
+
"external_ticket_title": "DOWN-123",
|
20
|
+
"internal_description_markdown": "A test has failed"
|
21
|
+
}
|
22
|
+
JSON
|
23
|
+
end
|
24
|
+
|
25
|
+
Munster.configure do |config|
|
26
|
+
config.active_handlers = {
|
27
|
+
test: WebhookTestHandler,
|
28
|
+
inactive: "InactiveHandler",
|
29
|
+
invalid: "InvalidHandler",
|
30
|
+
private: "PrivateHandler",
|
31
|
+
"failing-with-exposed-errors": "FailingWithExposedErrors",
|
32
|
+
"failing-with-concealed-errors": "FailingWithConcealedErrors",
|
33
|
+
extract_id: "ExtractIdHandler"
|
34
|
+
}
|
35
|
+
end
|
36
|
+
self.app = MunsterTestApp
|
37
|
+
|
38
|
+
def self.xtest(msg)
|
39
|
+
test(msg) { skip }
|
40
|
+
end
|
41
|
+
|
42
|
+
test "ensure webhook is processed only once during creation" do
|
43
|
+
tf = Tempfile.new
|
44
|
+
body = {isValid: true, outputToFilename: tf.path}
|
45
|
+
body_json = body.to_json
|
46
|
+
|
47
|
+
assert_enqueued_jobs 1, only: Munster::ProcessingJob do
|
48
|
+
post "/munster/test", params: body_json, headers: {"CONTENT_TYPE" => "application/json"}
|
49
|
+
assert_response 200
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
test "accepts a webhook, stores and processes it" do
|
54
|
+
tf = Tempfile.new
|
55
|
+
body = {isValid: true, outputToFilename: tf.path}
|
56
|
+
body_json = body.to_json
|
57
|
+
|
58
|
+
post "/munster/test", params: body_json, headers: {"CONTENT_TYPE" => "application/json"}
|
59
|
+
assert_response 200
|
60
|
+
|
61
|
+
webhook = Munster::ReceivedWebhook.last!
|
62
|
+
|
63
|
+
assert_predicate webhook, :received?
|
64
|
+
assert_equal "WebhookTestHandler", webhook.handler_module_name
|
65
|
+
assert_equal webhook.status, "received"
|
66
|
+
assert_equal webhook.body, body_json
|
67
|
+
|
68
|
+
perform_enqueued_jobs
|
69
|
+
assert_predicate webhook.reload, :processed?
|
70
|
+
tf.rewind
|
71
|
+
assert_equal tf.read, body_json
|
72
|
+
end
|
73
|
+
|
74
|
+
test "accepts a webhook but does not process it if it is invalid" do
|
75
|
+
tf = Tempfile.new
|
76
|
+
body = {isValid: false, outputToFilename: tf.path}
|
77
|
+
body_json = body.to_json
|
78
|
+
|
79
|
+
post "/munster/test", params: body_json, headers: {"CONTENT_TYPE" => "application/json"}
|
80
|
+
assert_response 200
|
81
|
+
|
82
|
+
webhook = Munster::ReceivedWebhook.last!
|
83
|
+
|
84
|
+
assert_predicate webhook, :received?
|
85
|
+
assert_equal "WebhookTestHandler", webhook.handler_module_name
|
86
|
+
assert_equal webhook.status, "received"
|
87
|
+
assert_equal webhook.body, body_json
|
88
|
+
|
89
|
+
perform_enqueued_jobs
|
90
|
+
assert_predicate webhook.reload, :failed_validation?
|
91
|
+
|
92
|
+
tf.rewind
|
93
|
+
assert_predicate tf.read, :empty?
|
94
|
+
end
|
95
|
+
|
96
|
+
test "marks a webhook as errored if it raises during processing" do
|
97
|
+
tf = Tempfile.new
|
98
|
+
body = {isValid: true, raiseDuringProcessing: true, outputToFilename: tf.path}
|
99
|
+
body_json = body.to_json
|
100
|
+
|
101
|
+
post "/munster/test", params: body_json, headers: {"CONTENT_TYPE" => "application/json"}
|
102
|
+
assert_response 200
|
103
|
+
|
104
|
+
webhook = Munster::ReceivedWebhook.last!
|
105
|
+
|
106
|
+
assert_predicate webhook, :received?
|
107
|
+
assert_equal "WebhookTestHandler", webhook.handler_module_name
|
108
|
+
assert_equal webhook.status, "received"
|
109
|
+
assert_equal webhook.body, body_json
|
110
|
+
|
111
|
+
assert_raises(StandardError) { perform_enqueued_jobs }
|
112
|
+
assert_predicate webhook.reload, :error?
|
113
|
+
|
114
|
+
tf.rewind
|
115
|
+
assert_predicate tf.read, :empty?
|
116
|
+
end
|
117
|
+
|
118
|
+
test "does not accept a test payload that is larger than the configured maximum size" do
|
119
|
+
oversize = Munster.configuration.request_body_size_limit + 1
|
120
|
+
utf8_junk = Base64.strict_encode64(Random.bytes(oversize))
|
121
|
+
body = {isValid: true, filler: utf8_junk, raiseDuringProcessing: false, outputToFilename: "/tmp/nothing"}
|
122
|
+
body_json = body.to_json
|
123
|
+
|
124
|
+
post "/munster/test", params: body_json, headers: {"CONTENT_TYPE" => "application/json"}
|
125
|
+
assert_raises(ActiveRecord::RecordNotFound) { Munster::ReceivedWebhook.last! }
|
126
|
+
end
|
127
|
+
|
128
|
+
test "does not try to process a webhook if it is not in `received' state" do
|
129
|
+
tf = Tempfile.new
|
130
|
+
body = {isValid: true, raiseDuringProcessing: true, outputToFilename: tf.path}
|
131
|
+
body_json = body.to_json
|
132
|
+
|
133
|
+
post "/munster/test", params: body_json, headers: {"CONTENT_TYPE" => "application/json"}
|
134
|
+
assert_response 200
|
135
|
+
|
136
|
+
webhook = Munster::ReceivedWebhook.last!
|
137
|
+
webhook.processing!
|
138
|
+
|
139
|
+
perform_enqueued_jobs
|
140
|
+
assert_predicate webhook.reload, :processing?
|
141
|
+
|
142
|
+
tf.rewind
|
143
|
+
assert_predicate tf.read, :empty?
|
144
|
+
end
|
145
|
+
|
146
|
+
test "raises an error if the service_id is not known" do
|
147
|
+
post "/munster/missing_service", params: webhook_body, headers: {"CONTENT_TYPE" => "application/json"}
|
148
|
+
assert_response 404
|
149
|
+
end
|
150
|
+
|
151
|
+
test "returns a 503 when a handler is inactive" do
|
152
|
+
post "/munster/inactive", params: webhook_body, headers: {"CONTENT_TYPE" => "application/json"}
|
153
|
+
|
154
|
+
assert_response 503
|
155
|
+
assert_equal 'Webhook handler "inactive" is inactive', response.parsed_body["error"]
|
156
|
+
end
|
157
|
+
|
158
|
+
test "returns a 200 status and error message if the handler does not expose errors" do
|
159
|
+
post "/munster/failing-with-concealed-errors", params: webhook_body, headers: {"CONTENT_TYPE" => "application/json"}
|
160
|
+
|
161
|
+
assert_response 200
|
162
|
+
assert_equal false, response.parsed_body["ok"]
|
163
|
+
assert response.parsed_body["error"]
|
164
|
+
end
|
165
|
+
|
166
|
+
test "returns a 500 status and error message if the handler does not expose errors" do
|
167
|
+
post "/munster/failing-with-exposed-errors", params: webhook_body, headers: {"CONTENT_TYPE" => "application/json"}
|
168
|
+
|
169
|
+
assert_response 500
|
170
|
+
# The response generation in this case is done by Rails, through the
|
171
|
+
# common Rails error page
|
172
|
+
end
|
173
|
+
|
174
|
+
test "deduplicates received webhooks based on the event ID" do
|
175
|
+
body = {event_id: SecureRandom.uuid, body: "test"}.to_json
|
176
|
+
|
177
|
+
assert_changes_by -> { Munster::ReceivedWebhook.count }, exactly: 1 do
|
178
|
+
3.times do
|
179
|
+
post "/munster/extract_id", params: body, headers: {"CONTENT_TYPE" => "application/json"}
|
180
|
+
assert_response 200
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
test "preserves the route params and the request params in the serialised request stored with the webhook" do
|
186
|
+
body = {user_name: "John", number_of_dependents: 14}.to_json
|
187
|
+
|
188
|
+
Munster::ReceivedWebhook.delete_all
|
189
|
+
post "/per-user-munster/123/private", params: body, headers: {"CONTENT_TYPE" => "application/json"}
|
190
|
+
assert_response 200
|
191
|
+
|
192
|
+
received_webhook = Munster::ReceivedWebhook.first!
|
193
|
+
assert_predicate received_webhook, :received?
|
194
|
+
assert_equal body, received_webhook.request.body.read
|
195
|
+
assert_equal "John", received_webhook.request.params["user_name"]
|
196
|
+
assert_equal 14, received_webhook.request.params["number_of_dependents"]
|
197
|
+
assert_equal "123", received_webhook.request.params["user_id"]
|
198
|
+
end
|
199
|
+
|
200
|
+
test "erroneous webhook could be processed again" do
|
201
|
+
webhook = Munster::ReceivedWebhook.create(
|
202
|
+
handler_event_id: "test",
|
203
|
+
handler_module_name: "WebhookTestHandler",
|
204
|
+
status: "error",
|
205
|
+
body: {isValid: true}.to_json
|
206
|
+
)
|
207
|
+
|
208
|
+
assert_enqueued_jobs 1, only: Munster::ProcessingJob do
|
209
|
+
webhook.received!
|
210
|
+
|
211
|
+
assert_equal "received", webhook.status
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
Binary file
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class WebhookTestHandler < Munster::BaseHandler
|
2
|
+
def valid?(request)
|
3
|
+
request.params.fetch(:isValid, false)
|
4
|
+
end
|
5
|
+
|
6
|
+
def process(webhook)
|
7
|
+
raise "Oops, failed" if webhook.request.params[:raiseDuringProcessing]
|
8
|
+
filename = webhook.request.params.fetch(:outputToFilename)
|
9
|
+
File.binwrite(filename, webhook.body)
|
10
|
+
end
|
11
|
+
|
12
|
+
def expose_errors_to_sender? = true
|
13
|
+
end
|
data/test/test_app.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
require "action_pack"
|
5
|
+
require "action_controller"
|
6
|
+
require "rails"
|
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"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require_relative "../lib/munster"
|
27
|
+
require_relative "test-webhook-handlers/webhook_test_handler"
|
28
|
+
|
29
|
+
class MunsterTestApp < Rails::Application
|
30
|
+
config.logger = Logger.new(nil)
|
31
|
+
config.autoload_paths << File.dirname(__FILE__) + "/test-webhook-handlers"
|
32
|
+
config.root = __dir__
|
33
|
+
config.eager_load = false
|
34
|
+
config.consider_all_requests_local = true
|
35
|
+
config.secret_key_base = "i_am_a_secret"
|
36
|
+
config.active_support.cache_format_version = 7.0
|
37
|
+
config.hosts << ->(host) { true } # Permit all hosts
|
38
|
+
|
39
|
+
routes.append do
|
40
|
+
mount Munster::Engine, at: "/munster"
|
41
|
+
post "/per-user-munster/:user_id/:service_id" => "munster/receive_webhooks#create"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
MunsterTestApp.initialize!
|
46
|
+
|
47
|
+
# run MunsterTestApp
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require_relative "test_app"
|
2
|
+
require "rails/test_help"
|
3
|
+
|
4
|
+
class ActiveSupport::TestCase
|
5
|
+
# Same as "assert_changes" in Rails but for countable entities.
|
6
|
+
# @return [*] return value of the block
|
7
|
+
# @example
|
8
|
+
# assert_changes_by("Notification.count", exactly: 2) do
|
9
|
+
# cause_two_notifications_to_get_delivered
|
10
|
+
# end
|
11
|
+
def assert_changes_by(expression, message = nil, exactly: nil, at_least: nil, at_most: nil, &block)
|
12
|
+
# rubocop:disable Security/Eval
|
13
|
+
exp = expression.respond_to?(:call) ? expression : -> { eval(expression.to_s, block.binding) }
|
14
|
+
# rubocop:enable Security/Eval
|
15
|
+
|
16
|
+
raise "either exactly:, at_least: or at_most: must be specified" unless exactly || at_least || at_most
|
17
|
+
raise "exactly: is mutually exclusive with other options" if exactly && (at_least || at_most)
|
18
|
+
raise "at_most: must be larger than at_least:" if at_least && at_most && at_most < at_least
|
19
|
+
|
20
|
+
before = exp.call
|
21
|
+
retval = assert_nothing_raised(&block)
|
22
|
+
|
23
|
+
after = exp.call
|
24
|
+
delta = after - before
|
25
|
+
|
26
|
+
if exactly
|
27
|
+
at_most = exactly
|
28
|
+
at_least = exactly
|
29
|
+
end
|
30
|
+
|
31
|
+
# We do not make these an if/else since we allow both at_most and at_least
|
32
|
+
if at_most
|
33
|
+
error = "#{expression.inspect} changed by #{delta} which is more than #{at_most}"
|
34
|
+
error = "#{error}. It was #{before} and became #{after}"
|
35
|
+
error = "#{message.call}.\n" if message&.respond_to?(:call)
|
36
|
+
error = "#{message}.\n#{error}" if message && !message.respond_to?(:call)
|
37
|
+
assert delta <= at_most, error
|
38
|
+
end
|
39
|
+
|
40
|
+
if at_least
|
41
|
+
error = "#{expression.inspect} changed by #{delta} which is less than #{at_least}"
|
42
|
+
error = "#{error}. It was #{before} and became #{after}"
|
43
|
+
error = "#{message.call}.\n" if message&.respond_to?(:call)
|
44
|
+
error = "#{message}.\n#{error}" if message && !message.respond_to?(:call)
|
45
|
+
assert delta >= at_least, error
|
46
|
+
end
|
47
|
+
|
48
|
+
retval
|
49
|
+
end
|
50
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: munster
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stanislav Katkov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-08-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -186,6 +186,17 @@ files:
|
|
186
186
|
- lib/munster/templates/munster.rb
|
187
187
|
- lib/munster/version.rb
|
188
188
|
- lib/tasks/munster_tasks.rake
|
189
|
+
- test/munster_test.rb
|
190
|
+
- test/test-webhook-handlers/.DS_Store
|
191
|
+
- test/test-webhook-handlers/extract_id_handler.rb
|
192
|
+
- test/test-webhook-handlers/failing_with_concealed_errors.rb
|
193
|
+
- test/test-webhook-handlers/failing_with_exposed_errors.rb
|
194
|
+
- test/test-webhook-handlers/inactive_handler.rb
|
195
|
+
- test/test-webhook-handlers/invalid_handler.rb
|
196
|
+
- test/test-webhook-handlers/private_handler.rb
|
197
|
+
- test/test-webhook-handlers/webhook_test_handler.rb
|
198
|
+
- test/test_app.rb
|
199
|
+
- test/test_helper.rb
|
189
200
|
homepage: https://www.cheddar.me/
|
190
201
|
licenses:
|
191
202
|
- MIT
|
@@ -208,7 +219,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
208
219
|
- !ruby/object:Gem::Version
|
209
220
|
version: '0'
|
210
221
|
requirements: []
|
211
|
-
rubygems_version: 3.
|
222
|
+
rubygems_version: 3.3.7
|
212
223
|
signing_key:
|
213
224
|
specification_version: 4
|
214
225
|
summary: Webhooks processing engine for Rails applications
|