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
         |