├── .gitattributes ├── .gitignore ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── app ├── assets │ ├── config │ │ └── manifest.js │ ├── images │ │ └── .keep │ └── stylesheets │ │ └── application.css ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── controllers │ ├── application_controller.rb │ ├── concerns │ │ └── .keep │ └── webhooks │ │ ├── base_controller.rb │ │ ├── movies_controller.rb │ │ └── stripe_controller.rb ├── helpers │ ├── application_helper.rb │ └── webhooks │ │ ├── base_helper.rb │ │ ├── movies_helper.rb │ │ └── stripe_helper.rb ├── javascript │ ├── application.js │ └── controllers │ │ ├── application.js │ │ ├── hello_controller.js │ │ └── index.js ├── jobs │ ├── application_job.rb │ └── webhooks │ │ ├── movies_job.rb │ │ └── stripe_job.rb ├── mailers │ └── application_mailer.rb ├── models │ ├── application_record.rb │ ├── concerns │ │ └── .keep │ └── inbound_webhook.rb └── views │ └── layouts │ ├── application.html.erb │ ├── mailer.html.erb │ └── mailer.text.erb ├── bin ├── bundle ├── importmap ├── rails ├── rake └── setup ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── credentials.yml.enc ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── importmap.rb ├── initializers │ ├── assets.rb │ ├── content_security_policy.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ └── permissions_policy.rb ├── locales │ └── en.yml ├── puma.rb ├── routes.rb └── storage.yml ├── db ├── migrate │ └── 20230422190352_create_inbound_webhooks.rb ├── schema.rb └── seeds.rb ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── log └── .keep ├── public ├── 404.html ├── 422.html ├── 500.html ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── favicon.ico └── robots.txt ├── slides ├── railsconf.md └── railsconf.pdf ├── storage └── .keep ├── test ├── application_system_test_case.rb ├── channels │ └── application_cable │ │ └── connection_test.rb ├── controllers │ ├── .keep │ └── webhooks │ │ ├── base_controller_test.rb │ │ ├── movies_controller_test.rb │ │ └── stripe_controller_test.rb ├── fixtures │ ├── files │ │ └── .keep │ ├── inbound_webhooks.yml │ └── webhooks │ │ └── movie.json ├── helpers │ └── .keep ├── integration │ └── .keep ├── jobs │ └── webhooks │ │ ├── movies_job_test.rb │ │ └── stripe_job_test.rb ├── mailers │ └── .keep ├── models │ ├── .keep │ └── inbound_webhook_test.rb ├── system │ └── .keep └── test_helper.rb ├── tmp ├── .keep ├── pids │ └── .keep └── storage │ └── .keep └── vendor ├── .keep └── javascript └── .keep /.gitattributes: -------------------------------------------------------------------------------- 1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files. 2 | 3 | # Mark the database schema as having been generated. 4 | db/schema.rb linguist-generated 5 | 6 | # Mark any vendored files as having been vendored. 7 | vendor/* linguist-vendored 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-* 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/* 16 | /tmp/* 17 | !/log/.keep 18 | !/tmp/.keep 19 | 20 | # Ignore pidfiles, but keep the directory. 21 | /tmp/pids/* 22 | !/tmp/pids/ 23 | !/tmp/pids/.keep 24 | 25 | # Ignore uploaded files in development. 26 | /storage/* 27 | !/storage/.keep 28 | /tmp/storage/* 29 | !/tmp/storage/ 30 | !/tmp/storage/.keep 31 | 32 | /public/assets 33 | 34 | # Ignore master key for decrypting credentials and more. 35 | /config/master.key 36 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.0.3 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | ruby "3.0.3" 5 | 6 | # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" 7 | gem "rails", "~> 7.0.4", ">= 7.0.4.3" 8 | 9 | # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] 10 | gem "sprockets-rails" 11 | 12 | # Use sqlite3 as the database for Active Record 13 | gem "sqlite3", "~> 1.4" 14 | 15 | # Use the Puma web server [https://github.com/puma/puma] 16 | gem "puma", "~> 5.0" 17 | 18 | # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] 19 | gem "importmap-rails" 20 | 21 | # Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] 22 | gem "turbo-rails" 23 | 24 | # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] 25 | gem "stimulus-rails" 26 | 27 | # Build JSON APIs with ease [https://github.com/rails/jbuilder] 28 | gem "jbuilder" 29 | 30 | # Use Redis adapter to run Action Cable in production 31 | gem "redis", "~> 4.0" 32 | 33 | # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] 34 | # gem "kredis" 35 | 36 | # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] 37 | # gem "bcrypt", "~> 3.1.7" 38 | 39 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 40 | gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ] 41 | 42 | # Reduces boot times through caching; required in config/boot.rb 43 | gem "bootsnap", require: false 44 | 45 | # Add the Stripe gem 46 | gem 'stripe', '~> 8.5' 47 | 48 | # Use Sass to process CSS 49 | # gem "sassc-rails" 50 | 51 | # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] 52 | # gem "image_processing", "~> 1.2" 53 | 54 | group :development, :test do 55 | # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem 56 | gem "debug", platforms: %i[ mri mingw x64_mingw ] 57 | end 58 | 59 | group :development do 60 | # Use console on exceptions pages [https://github.com/rails/web-console] 61 | gem "web-console" 62 | 63 | # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] 64 | # gem "rack-mini-profiler" 65 | 66 | # Speed up commands on slow machines / big apps [https://github.com/rails/spring] 67 | # gem "spring" 68 | end 69 | 70 | group :test do 71 | # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] 72 | gem "capybara" 73 | gem "selenium-webdriver" 74 | gem "webdrivers" 75 | end 76 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actioncable (7.0.4.3) 5 | actionpack (= 7.0.4.3) 6 | activesupport (= 7.0.4.3) 7 | nio4r (~> 2.0) 8 | websocket-driver (>= 0.6.1) 9 | actionmailbox (7.0.4.3) 10 | actionpack (= 7.0.4.3) 11 | activejob (= 7.0.4.3) 12 | activerecord (= 7.0.4.3) 13 | activestorage (= 7.0.4.3) 14 | activesupport (= 7.0.4.3) 15 | mail (>= 2.7.1) 16 | net-imap 17 | net-pop 18 | net-smtp 19 | actionmailer (7.0.4.3) 20 | actionpack (= 7.0.4.3) 21 | actionview (= 7.0.4.3) 22 | activejob (= 7.0.4.3) 23 | activesupport (= 7.0.4.3) 24 | mail (~> 2.5, >= 2.5.4) 25 | net-imap 26 | net-pop 27 | net-smtp 28 | rails-dom-testing (~> 2.0) 29 | actionpack (7.0.4.3) 30 | actionview (= 7.0.4.3) 31 | activesupport (= 7.0.4.3) 32 | rack (~> 2.0, >= 2.2.0) 33 | rack-test (>= 0.6.3) 34 | rails-dom-testing (~> 2.0) 35 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 36 | actiontext (7.0.4.3) 37 | actionpack (= 7.0.4.3) 38 | activerecord (= 7.0.4.3) 39 | activestorage (= 7.0.4.3) 40 | activesupport (= 7.0.4.3) 41 | globalid (>= 0.6.0) 42 | nokogiri (>= 1.8.5) 43 | actionview (7.0.4.3) 44 | activesupport (= 7.0.4.3) 45 | builder (~> 3.1) 46 | erubi (~> 1.4) 47 | rails-dom-testing (~> 2.0) 48 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 49 | activejob (7.0.4.3) 50 | activesupport (= 7.0.4.3) 51 | globalid (>= 0.3.6) 52 | activemodel (7.0.4.3) 53 | activesupport (= 7.0.4.3) 54 | activerecord (7.0.4.3) 55 | activemodel (= 7.0.4.3) 56 | activesupport (= 7.0.4.3) 57 | activestorage (7.0.4.3) 58 | actionpack (= 7.0.4.3) 59 | activejob (= 7.0.4.3) 60 | activerecord (= 7.0.4.3) 61 | activesupport (= 7.0.4.3) 62 | marcel (~> 1.0) 63 | mini_mime (>= 1.1.0) 64 | activesupport (7.0.4.3) 65 | concurrent-ruby (~> 1.0, >= 1.0.2) 66 | i18n (>= 1.6, < 2) 67 | minitest (>= 5.1) 68 | tzinfo (~> 2.0) 69 | addressable (2.8.4) 70 | public_suffix (>= 2.0.2, < 6.0) 71 | bindex (0.8.1) 72 | bootsnap (1.16.0) 73 | msgpack (~> 1.2) 74 | builder (3.2.4) 75 | capybara (3.39.0) 76 | addressable 77 | matrix 78 | mini_mime (>= 0.1.3) 79 | nokogiri (~> 1.8) 80 | rack (>= 1.6.0) 81 | rack-test (>= 0.6.3) 82 | regexp_parser (>= 1.5, < 3.0) 83 | xpath (~> 3.2) 84 | concurrent-ruby (1.2.2) 85 | crass (1.0.6) 86 | date (3.3.3) 87 | debug (1.7.2) 88 | irb (>= 1.5.0) 89 | reline (>= 0.3.1) 90 | erubi (1.12.0) 91 | globalid (1.1.0) 92 | activesupport (>= 5.0) 93 | i18n (1.12.0) 94 | concurrent-ruby (~> 1.0) 95 | importmap-rails (1.1.5) 96 | actionpack (>= 6.0.0) 97 | railties (>= 6.0.0) 98 | io-console (0.6.0) 99 | irb (1.6.4) 100 | reline (>= 0.3.0) 101 | jbuilder (2.11.5) 102 | actionview (>= 5.0.0) 103 | activesupport (>= 5.0.0) 104 | loofah (2.20.0) 105 | crass (~> 1.0.2) 106 | nokogiri (>= 1.5.9) 107 | mail (2.8.1) 108 | mini_mime (>= 0.1.1) 109 | net-imap 110 | net-pop 111 | net-smtp 112 | marcel (1.0.2) 113 | matrix (0.4.2) 114 | method_source (1.0.0) 115 | mini_mime (1.1.2) 116 | minitest (5.18.0) 117 | msgpack (1.7.0) 118 | net-imap (0.3.4) 119 | date 120 | net-protocol 121 | net-pop (0.1.2) 122 | net-protocol 123 | net-protocol (0.2.1) 124 | timeout 125 | net-smtp (0.3.3) 126 | net-protocol 127 | nio4r (2.5.9) 128 | nokogiri (1.14.3-arm64-darwin) 129 | racc (~> 1.4) 130 | public_suffix (5.0.1) 131 | puma (5.6.5) 132 | nio4r (~> 2.0) 133 | racc (1.6.2) 134 | rack (2.2.6.4) 135 | rack-test (2.1.0) 136 | rack (>= 1.3) 137 | rails (7.0.4.3) 138 | actioncable (= 7.0.4.3) 139 | actionmailbox (= 7.0.4.3) 140 | actionmailer (= 7.0.4.3) 141 | actionpack (= 7.0.4.3) 142 | actiontext (= 7.0.4.3) 143 | actionview (= 7.0.4.3) 144 | activejob (= 7.0.4.3) 145 | activemodel (= 7.0.4.3) 146 | activerecord (= 7.0.4.3) 147 | activestorage (= 7.0.4.3) 148 | activesupport (= 7.0.4.3) 149 | bundler (>= 1.15.0) 150 | railties (= 7.0.4.3) 151 | rails-dom-testing (2.0.3) 152 | activesupport (>= 4.2.0) 153 | nokogiri (>= 1.6) 154 | rails-html-sanitizer (1.5.0) 155 | loofah (~> 2.19, >= 2.19.1) 156 | railties (7.0.4.3) 157 | actionpack (= 7.0.4.3) 158 | activesupport (= 7.0.4.3) 159 | method_source 160 | rake (>= 12.2) 161 | thor (~> 1.0) 162 | zeitwerk (~> 2.5) 163 | rake (13.0.6) 164 | redis (4.8.1) 165 | regexp_parser (2.8.0) 166 | reline (0.3.3) 167 | io-console (~> 0.5) 168 | rexml (3.2.5) 169 | rubyzip (2.3.2) 170 | selenium-webdriver (4.9.0) 171 | rexml (~> 3.2, >= 3.2.5) 172 | rubyzip (>= 1.2.2, < 3.0) 173 | websocket (~> 1.0) 174 | sprockets (4.2.0) 175 | concurrent-ruby (~> 1.0) 176 | rack (>= 2.2.4, < 4) 177 | sprockets-rails (3.4.2) 178 | actionpack (>= 5.2) 179 | activesupport (>= 5.2) 180 | sprockets (>= 3.0.0) 181 | sqlite3 (1.6.2-arm64-darwin) 182 | stimulus-rails (1.2.1) 183 | railties (>= 6.0.0) 184 | stripe (8.5.0) 185 | thor (1.2.1) 186 | timeout (0.3.2) 187 | turbo-rails (1.4.0) 188 | actionpack (>= 6.0.0) 189 | activejob (>= 6.0.0) 190 | railties (>= 6.0.0) 191 | tzinfo (2.0.6) 192 | concurrent-ruby (~> 1.0) 193 | web-console (4.2.0) 194 | actionview (>= 6.0.0) 195 | activemodel (>= 6.0.0) 196 | bindex (>= 0.4.0) 197 | railties (>= 6.0.0) 198 | webdrivers (5.2.0) 199 | nokogiri (~> 1.6) 200 | rubyzip (>= 1.3.0) 201 | selenium-webdriver (~> 4.0) 202 | websocket (1.2.9) 203 | websocket-driver (0.7.5) 204 | websocket-extensions (>= 0.1.0) 205 | websocket-extensions (0.1.5) 206 | xpath (3.2.0) 207 | nokogiri (~> 1.8) 208 | zeitwerk (2.6.7) 209 | 210 | PLATFORMS 211 | arm64-darwin-20 212 | arm64-darwin-22 213 | 214 | DEPENDENCIES 215 | bootsnap 216 | capybara 217 | debug 218 | importmap-rails 219 | jbuilder 220 | puma (~> 5.0) 221 | rails (~> 7.0.4, >= 7.0.4.3) 222 | redis (~> 4.0) 223 | selenium-webdriver 224 | sprockets-rails 225 | sqlite3 (~> 1.4) 226 | stimulus-rails 227 | stripe (~> 8.5) 228 | turbo-rails 229 | tzinfo-data 230 | web-console 231 | webdrivers 232 | 233 | RUBY VERSION 234 | ruby 3.0.3p157 235 | 236 | BUNDLED WITH 237 | 2.2.32 238 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RailsConf 2023: Webhooks Workshop 2 | 3 | ### Catch Me If You Can: Learning to Process Webhooks in Your Rails App 4 | 5 | This workshop will be taught on on Tuesday, April 25th at [RailsConf 2023](https://railsconf.org/) in Atlanta. 6 | 7 | > In this workshop, you’ll learn how to catch and process webhooks like a pro. You’ll build a Rails app that’s both robust and low-latency so you keep up in real-time like a champ. Come ready to level up your skills and leave with the expertise you need to become a true webhook wizard! 8 | > 9 | > We will begin by exploring the fundamentals of webhooks: how they work, why they are useful, and how they differ from other approaches. We will then dive into the hard-won lessons learned for consuming and processing webhooks, including routing, handling payloads, and responding to events. 10 | > 11 | > Along the way, we will explore best practices like error handling, authentication, retries, idempotency, and scaling. You’ll walk away with a solid understanding for how to build a resilient and robust system to handle webhook notifications from a wide range of external services and APIs. Attendees will leave the workshop with a working webhook processor running on Rails! 12 | 13 | ## How To Use This Repository 14 | 15 | There are many ways to support webhooks in your application. For the interest of this workshop, we will be covering a straightforward approach that uses common Rails patterns from end to end. 16 | 17 | As you evolve your own webhook handling and develop your own style, you will find areas that might benefit from middleware and more intermediate/advanced methods. 18 | 19 | ### Branches 20 | 21 | To allow you to follow along, we've set up each step as a branch so you can see the difference from each step. `main` contains the final project with all the PRs merged but you can checkout the `start-here` branch to move to the starting point. We have included instructions below under Getting Started. 22 | 23 | You can check out each subsequent step with the following branches: 24 | 25 | - `git checkout start-here` 26 | - `git checkout step1-routes` 27 | - `git checkout step2-webhook-model` 28 | - `git checkout step3-background-job` 29 | - `git checkout step4-verification` 30 | - `git checkout step5-tests` 31 | 32 | # Webhooks Tutorial 33 | 34 | Star this repository so we know you have taken this workshop! 35 | 36 | ## Pull down the workshop repository 37 | 38 | Pull down the repository and switch to the `start-here` remote branch 39 | 40 | ```bash 41 | # clone from Github 42 | git clone git@github.com:colinloretz/railsconf-webhooks.git 43 | 44 | # move into directory 45 | cd railsconf-webhooks 46 | 47 | # fetch all the upstream branches that include the steps of this workshop 48 | git fetch --all 49 | 50 | # checkout the start-here branch 51 | git checkout start-here 52 | 53 | # install dependencies 54 | bundle install 55 | 56 | # start server 57 | rails server 58 | ``` 59 | 60 | You should now have a brand new Rails app up and running. You can verify by visiting [http://localhost:3000](http://localhost:3000) 61 | 62 | ## Step 1: Setting Up a Controller and Rails Routes 63 | 64 | We'll start by creating a controller and an HTTP route to catch the webhook events. 65 | 66 | ### 1A) Creating a Base Controller 67 | 68 | We'll start by creating a base controller that all of our webhook controllers will inherit from. This will allow us to add common functionality to all of our webhook controllers. 69 | 70 | ```bash 71 | rails generate controller Webhooks::BaseController 72 | ``` 73 | 74 | ```ruby 75 | # app/controllers/webhooks/base_controller.rb 76 | 77 | class Webhooks::BaseController < ApplicationController 78 | # Disable CSRF checking on webhooks because they do not originate from the browser 79 | skip_before_action :verify_authenticity_token 80 | 81 | def create 82 | head :ok 83 | end 84 | end 85 | ``` 86 | 87 | For this workshop we are going to catch webhooks from two sources: a fake webhook provider `Movies` and `Stripe`. We'll create a controller for each of these webhook sources and inherit the `Webhook::BaseController` 88 | 89 | ### 1B) Creating the Movies Webhook Controller 90 | 91 | This fake Movies controller also includes an example of how to send a webhook to your application. You can use this to test your webhook processor. 92 | 93 | ```bash 94 | rails generate controller Webhooks::MoviesController 95 | ``` 96 | 97 | ```ruby 98 | # app/controllers/webhooks/movies_controller.rb 99 | 100 | class Webhooks::MoviesController < Webhooks::BaseController 101 | # A controller for catching new movie webhooks 102 | # 103 | # To send a sample webhook locally: 104 | # 105 | # curl -X POST http://localhost:3000/webhooks/movies 106 | # -H 'Content-Type: application/json' 107 | # -d '{"title":"Dungeons & Dragons: Honor Among Thieves"}' 108 | # 109 | # If you'd like to override the base controller's behavior, you can do so here 110 | # def create 111 | # head :ok 112 | # end 113 | end 114 | ``` 115 | 116 | Let's create the Stripe webhook controller while we are here. 117 | 118 | ### 1C) Creating the Stripe Webhook Controller 119 | 120 | ```bash 121 | rails generate controller Webhooks::StripeController 122 | ``` 123 | 124 | ```ruby 125 | # app/controllers/webhooks/stripe_controller.rb 126 | 127 | class Webhooks::StripeController < Webhooks::BaseController 128 | # If you'd like to override the base controller's behavior, you can do so here 129 | # def create 130 | # head :ok 131 | # end 132 | end 133 | ``` 134 | 135 | ### 1D) Adding Routes for the Webhook Controllers 136 | 137 | Finally, we need to add routes for these webhook controllers. 138 | 139 | We are going to set up routes that are namespaced to `/webhooks` and then have a route for each webhook controller. We will only allow the `create` action on these routes. 140 | 141 | _As you add more webhook providers, you can also create a dynamic route that will catch all webhooks and route them to a single controller. This is a common pattern for webhook processors._ 142 | 143 | ```ruby 144 | # config/routes.rb 145 | 146 | Rails.application.routes.draw do 147 | namespace :webhooks do 148 | # /webhooks/movies routed to our Webhooks::MoviesController 149 | resource :movies, controller: :movies, only: [:create] 150 | 151 | # /webhooks/stripe routed to our Webhooks::StripeController 152 | resource :stripe, controller: :stripe, only: [:create] 153 | end 154 | 155 | # your other routes here 156 | end 157 | ``` 158 | 159 | ### 1E) Testing our new routes 160 | 161 | We can test our new routes by sending a request to our new webhook routes. We can use `curl` to send a request to our new webhook routes. 162 | 163 | Let's boot up the server and send a request to our new webhook routes. 164 | 165 | This should be successful and return a `200` status code. 166 | 167 | ```bash 168 | curl -X POST 'http://localhost:3000/webhooks/movies' -H 'Content-Type: application/json' -d '{"title":"Dungeons & Dragons: Honor Among Thieves"}' -v 169 | ``` 170 | 171 | We can't easily test our Stripe webhooks without setting up a Stripe account and configuring it to send webhooks to our application. We'll include instructions for this in a later step. 172 | 173 | Now that we have our routes set up, we can move on to creating a webhook model. 174 | 175 | 💡 You can checkout the branch `step1-routes` using `git checkout step1-routes` to get caught up to this step before continuing. 176 | 177 | ## Step 2: Creating a Webhook Model 178 | 179 | This model will be responsible for storing the inbound webhook until a background job can process it. 180 | 181 | The reason we want to do this is because we want our controller to respond as fast as possible for heavy traffic (like Black Friday for example) and we want to store the record in the database to safely store it for retries if the service is having trouble for any reason. 182 | 183 | ### 2A) Creating the Webhook Model 184 | 185 | We can use the Rails generator to create our model and the associated helper files. 186 | 187 | ```bash 188 | rails generate model InboundWebhook status:string body:text 189 | ``` 190 | 191 | We should now have a pretty empty model file and a migration file. 192 | 193 | ```ruby 194 | # app/models/inbound_webhook.rb 195 | 196 | class InboundWebhook < ApplicationRecord 197 | end 198 | ``` 199 | 200 | ```ruby 201 | # db/migrate/20230401162628_create_inbound_webhooks.rb 202 | 203 | class CreateInboundWebhooks < ActiveRecord::Migration[7.0] 204 | def change 205 | create_table :inbound_webhooks do |t| 206 | t.string :status, default: :pending 207 | t.text :body 208 | 209 | t.timestamps 210 | end 211 | end 212 | end 213 | ``` 214 | 215 | We can run our migration to create the table in the database. 216 | 217 | ```bash 218 | rails db:migrate 219 | ``` 220 | 221 | Now that we have a model and a backing table in the database, we can save our webhook to the database. Let's update our `Webhook::BaseController` or each of our webhook controllers to save the webhook to the database. 222 | 223 | ```ruby 224 | # app/controllers/webhooks/base_controller.rb 225 | 226 | class Webhooks::BaseController < ApplicationController 227 | # Disable CSRF checking on webhooks because they do not originate from the browser 228 | skip_before_action :verify_authenticity_token 229 | 230 | def create 231 | InboundWebhook.create(body: payload) 232 | head :ok 233 | end 234 | 235 | private 236 | 237 | def payload 238 | @payload ||= request.body.read 239 | end 240 | end 241 | ``` 242 | 243 | Now if you test your Movies webhook route, you should see a new record in the database. 244 | 245 | ```bash 246 | curl -X POST 'http://localhost:3000/webhooks/movies' -H 'Content-Type: application/json' -d '{"foo":"bar"}' 247 | ``` 248 | 249 | 💡 You can checkout the branch `step2-webhook-model` using `git checkout step2-webhook-model` to get caught up to this step before continuing. 250 | 251 | ## Step 3: Creating a Background Worker 252 | 253 | Now that we have a record in our database, we need to process it. We don't want to process it in the controller because that would slow down our response time. Instead, we want to process it in a background job. 254 | 255 | ### 3A) Creating the Background Job 256 | 257 | Let's use another Rails generator to create a background job. 258 | 259 | In thise case, we are going to use the `ActiveJob` framework that comes with Rails. This will allow us to easily switch between background job providers like Sidekiq, Resque, DelayedJob, etc. 260 | 261 | Let's make a job for each of our webhook providers within a `Webhooks` namespace. 262 | 263 | ```bash 264 | rails generate job Webhooks::MoviesJob 265 | ``` 266 | 267 | ```ruby 268 | # app/jobs/webhooks/movies_job.rb 269 | 270 | class Webhooks::MoviesJob < ApplicationJob 271 | queue_as :default 272 | 273 | def perform(inbound_webhook) 274 | webhook_payload = JSON.parse(inbound_webhook.body, symbolize_names: true) 275 | 276 | # do whatever you'd like here with your webhook payload 277 | # call another service, update a record, etc. 278 | 279 | inbound_webhook.update!(status: :processed) 280 | end 281 | end 282 | ``` 283 | 284 | ```bash 285 | rails generate job Webhooks::StripeJob 286 | ``` 287 | 288 | ```ruby 289 | # app/jobs/webhooks/stripe_job.rb 290 | 291 | class Webhooks::StripeJob < ApplicationJob 292 | queue_as :default 293 | 294 | def perform(inbound_webhook) 295 | json = JSON.parse(inbound_webhook.body, symbolize_names: true) 296 | event = Stripe::Event.construct_from(json) 297 | case event.type 298 | when 'customer.updated' 299 | # Find customer and save changes 300 | inbound_webhook.update!(status: :processed) 301 | else 302 | inbound_webhook.update!(status: :skipped) 303 | end 304 | end 305 | end 306 | ``` 307 | 308 | ### 3B) Updating our Webhook Controllers to enqueue jobs 309 | 310 | Now that we have our jobs defined, we need to enqueue them in our webhook controllers. 311 | 312 | Let's update both of our controllers to process these webhooks by overriding the `create` method in both controllers. 313 | 314 | ```ruby 315 | # app/controllers/webhooks/movies_controller.rb 316 | 317 | class Webhooks::MoviesController < Webhooks::BaseController 318 | def create 319 | # Save webhook to database 320 | record = InboundWebhook.create!(body: payload) 321 | 322 | # Queue database record for processing 323 | Webhooks::MoviesJob.perform_later(record) 324 | 325 | head :ok 326 | end 327 | 328 | private 329 | 330 | def payload 331 | @payload ||= request.body.read 332 | end 333 | end 334 | ``` 335 | 336 | And we can set up our Stripe controller, similarly. 337 | 338 | ```ruby 339 | # app/controllers/webhooks/stripe_controller.rb 340 | 341 | class Webhooks::StripeController < Webhooks::BaseController 342 | def create 343 | # Save webhook to database 344 | record = InboundWebhook.create!(body: payload) 345 | 346 | # Queue database record for processing 347 | Webhooks::StripeJob.perform_later(record) 348 | 349 | # Tell Stripe everything was successful 350 | head :ok 351 | end 352 | 353 | private 354 | 355 | def payload 356 | @payload ||= request.body.read 357 | end 358 | end 359 | ``` 360 | 361 | 💡 You can checkout the branch `step3-background-job` using `git checkout step3-background-job` to get caught up to this step before continuing. 362 | 363 | ## Step 4: Verifying Webhooks 364 | 365 | Verifying webhooks is an important step to make sure that the event is a real event that you expect from the Webhook Provider. 366 | 367 | In our controllers, we have created a `verify_event` method that is called as a before_action in our base controller. 368 | 369 | ```ruby 370 | # app/controllers/webhooks/base_controller.rb 371 | 372 | class Webhooks::BaseController < ApplicationController 373 | # Disable CSRF checking on webhooks because they do not originate from the browser 374 | skip_before_action :verify_authenticity_token 375 | 376 | before_action :verify_event 377 | 378 | def create 379 | InboundWebhook.create(body: payload) 380 | head :ok 381 | end 382 | 383 | private 384 | 385 | def verify_event 386 | head :bad_request 387 | end 388 | 389 | def payload 390 | @payload ||= request.body.read 391 | end 392 | end 393 | ``` 394 | 395 | For testing purposes with our Movie webhooks, we have added a `verify_event` method that is always true unless you pass in a `fail_verification` url parameter on the webhook endpoint. 396 | 397 | ```ruby 398 | # app/controllers/webhooks/movies_controller.rb 399 | # ... rest of controller 400 | 401 | private 402 | 403 | def verify_event 404 | head :bad_request if params[:fail_verification] 405 | end 406 | ``` 407 | 408 | To verify Stripe webhooks in `Webhooks::StripeController`, Stripe provides a method in their ruby gem that uses a combination of a Webhook Signature, your Stripe signing secret and the JSON payload to verify the event. 409 | 410 | ```ruby 411 | # app/controllers/webhooks/stripe_controller.rb 412 | # ... rest of controller 413 | 414 | private 415 | 416 | def verify_event 417 | signature = request.headers['Stripe-Signature'] 418 | secret = Rails.application.credentials.dig(:stripe, :webhook_signing_secret) 419 | 420 | ::Stripe::Webhook::Signature.verify_header( 421 | payload, 422 | signature, 423 | secret.to_s, 424 | tolerance: Stripe::Webhook::DEFAULT_TOLERANCE, 425 | ) 426 | rescue ::Stripe::SignatureVerificationError 427 | head :bad_request 428 | end 429 | ``` 430 | 431 | When consuming webhooks from other sources, you would customize `verify_event` to match the signature verification method that the Webhook Provider has in their webhook documentation. 432 | 433 | For webhooks that don't provide verification methods, you can optionally call to the source API to check that the event has happened or that data has been created/updated as the event suggests. 434 | 435 | However, it is recommended that you do this in the background job that processes the webhook so you don't make unnecessary API calls while returning a status back to the Webhook Provider. 436 | 437 | 💡 You can checkout the branch `step4-verification` using `git checkout step4-verification` to get caught up to this step before continuing. 438 | 439 | ## Step 5: Writing tests for our Webhook Processor 440 | 441 | Now that we have our webhook processor set up, we can write some tests to make sure that it works as expected. 442 | 443 | We will start with a simple 200 OK status test on our MoviesController. 444 | 445 | First, let's create a JSON file to represent our webhook. If you have a lot of webhooks, you can create a folder for all of your webhook fixtures and separate them by provider. 446 | 447 | We'll start by creating a folder for our webhooks in `test/fixtures` and then create a JSON file for our Movie webhook. 448 | 449 | ```bash 450 | mkdir test/fixtures/webhooks 451 | ``` 452 | 453 | And create a file for our Movie webhook. 454 | 455 | ```bash 456 | touch test/fixtures/webhooks/movie.json 457 | ``` 458 | 459 | ```json 460 | { 461 | "title": "Dungeons & Dragons: Honor Among Thieves", 462 | "release_date": "2023-03-31" 463 | } 464 | ``` 465 | 466 | ```ruby 467 | # spec/requests/webhooks/movies_controller_spec.rb 468 | require 'test_helper' 469 | 470 | class Webhooks::MoviesControllerTest < ActionDispatch::IntegrationTest 471 | def setup 472 | # Load the webhook data from the JSON file 473 | file_path = Rails.root.join('test', 'fixtures', 'webhooks', 'movie.json') 474 | @webhook = JSON.parse(File.read(file_path)) 475 | end 476 | 477 | test 'should consume webhook' do 478 | # Send the POST request to the create action with the prepared data 479 | post webhooks_movies_url, params: @webhook 480 | 481 | # Check if the response status is 200 OK 482 | assert_response :ok 483 | 484 | # You can create other test files for each job or service that is called 485 | # For example, check if a record was created/updated in the database 486 | end 487 | end 488 | ``` 489 | 490 | You should have a successful test run if you run the following command: 491 | 492 | ```bash 493 | rails test 494 | ``` 495 | 496 | You can also use more advanced testing frameworks like Rspec and FactoryBot to write tests for your webhook processor. 497 | 498 | 💡 You can checkout the branch `step5-tests` using `git checkout step5-tests` to get caught up to this step before continuing. 499 | 500 | ## Other Topics To Consider 501 | 502 | ### Retries 503 | 504 | Usually retries are done on the Webhook Producer side of things. However, if you are receiving a lot of webhooks and you are having trouble processing them all, you may want to implement background job retries in your application. When doing this, you will also want to make sure you have an idempotency method in place to ensure that you don't process the same webhook event more than once. 505 | 506 | ### Preventing Replay Attacks: Idempotency & Deduplication 507 | 508 | As a Webhook Consumer, you want to make sure that you don't process the same webhook event multiple times. This is especially important if you are doing something like updating a record in your database. 509 | 510 | Most Webhook Providers offer a unique identifier or idempotency method for each webhook event. You can use this to ensure that you don't process the same webhook event more than once. 511 | 512 | ### Backfilling Missing Events 513 | 514 | The beauty of webhooks is that it is an evented-architecture. However, they are not always 100% reliable and a provider may not be able to deliver a webhook event to your application. 515 | 516 | If a webhook provider is unable to deliver a webhook to your application, most will retry a certain number of times over a given time period (usually with exponential backoff). However, if after that time, they still were not able to deliver the webhook payload OR had some sort of service outage, you might be missing important webhook events. 517 | 518 | You can implement other API calls to the service that runs on an interval or when other events are received to check if any other events happened since the last event was received. 519 | 520 | # Webhooks Best Practices & Tools 521 | 522 | - [Webhooks.fyi: Best Practices for Webhook Consumers](https://webhooks.fyi/best-practices/webhook-consumers) 523 | - [Stripe: Using incoming webhooks to get real-time updates](https://stripe.com/docs/webhooks) 524 | - [Twilio: What is a Webhook?](https://www.twilio.com/docs/glossary/what-is-a-webhook) 525 | - [Zapier: What are Webhooks?](https://zapier.com/blog/what-are-webhooks/) 526 | 527 | **Popular Options for safe ingress into your application from the outside world** 528 | 529 | - [ngrok](https://ngrok.com) 530 | - [Cloudflare tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/) 531 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | //= link_tree ../../javascript .js 4 | //= link_tree ../../../vendor/javascript .js 5 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's 6 | * vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/webhooks/base_controller.rb: -------------------------------------------------------------------------------- 1 | class Webhooks::BaseController < ActionController::API 2 | before_action :verify_event 3 | 4 | def create 5 | InboundWebhook.create(body: payload) 6 | head :ok 7 | end 8 | 9 | private 10 | 11 | def verify_event 12 | head :bad_request 13 | end 14 | 15 | def payload 16 | @payload ||= request.body.read 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/controllers/webhooks/movies_controller.rb: -------------------------------------------------------------------------------- 1 | class Webhooks::MoviesController < Webhooks::BaseController 2 | # A controller for catching new movie webhooks 3 | # 4 | # To send a sample webhook locally: 5 | # 6 | # curl -X POST http://localhost:3000/webhooks/movies 7 | # -H 'Content-Type: application/json' 8 | # -d '{"title":"Dungeons & Dragons: Honor Among Thieves"}' 9 | # 10 | # Pass ?fail_verification=1 to simulate a webhook verification failure 11 | 12 | def create 13 | # Save webhook to database 14 | record = InboundWebhook.create!(body: payload) 15 | 16 | # Queue database record for processing 17 | Webhooks::MoviesJob.perform_later(record) 18 | 19 | head :ok 20 | end 21 | 22 | private 23 | 24 | # Pass ?fail_verification=1 to simulate a webhook verification failure 25 | def verify_event 26 | head :bad_request if params[:fail_verification] 27 | end 28 | 29 | def payload 30 | @payload ||= request.body.read 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/controllers/webhooks/stripe_controller.rb: -------------------------------------------------------------------------------- 1 | class Webhooks::StripeController < Webhooks::BaseController 2 | def create 3 | # Save webhook to database 4 | record = InboundWebhook.create!(body: payload) 5 | 6 | # Queue database record for processing 7 | Webhooks::StripeJob.perform_later(record) 8 | 9 | # Tell Stripe everything was successful 10 | head :ok 11 | end 12 | 13 | private 14 | 15 | # Verifies the event came from Stripe 16 | def verify_event 17 | signature = request.headers['Stripe-Signature'] 18 | secret = Rails.application.credentials.dig(:stripe, :webhook_signing_secret) 19 | 20 | ::Stripe::Webhook::Signature.verify_header( 21 | payload, 22 | signature, 23 | secret.to_s, 24 | tolerance: Stripe::Webhook::DEFAULT_TOLERANCE, 25 | ) 26 | rescue ::Stripe::SignatureVerificationError 27 | head :bad_request 28 | end 29 | 30 | def payload 31 | @payload ||= request.body.read 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/webhooks/base_helper.rb: -------------------------------------------------------------------------------- 1 | module Webhooks::BaseHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/webhooks/movies_helper.rb: -------------------------------------------------------------------------------- 1 | module Webhooks::MoviesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/webhooks/stripe_helper.rb: -------------------------------------------------------------------------------- 1 | module Webhooks::StripeHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/javascript/application.js: -------------------------------------------------------------------------------- 1 | // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails 2 | import "@hotwired/turbo-rails" 3 | import "controllers" 4 | -------------------------------------------------------------------------------- /app/javascript/controllers/application.js: -------------------------------------------------------------------------------- 1 | import { Application } from "@hotwired/stimulus" 2 | 3 | const application = Application.start() 4 | 5 | // Configure Stimulus development experience 6 | application.debug = false 7 | window.Stimulus = application 8 | 9 | export { application } 10 | -------------------------------------------------------------------------------- /app/javascript/controllers/hello_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | connect() { 5 | this.element.textContent = "Hello World!" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/javascript/controllers/index.js: -------------------------------------------------------------------------------- 1 | // Import and register all your controllers from the importmap under controllers/* 2 | 3 | import { application } from "controllers/application" 4 | 5 | // Eager load all controllers defined in the import map under controllers/**/*_controller 6 | import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" 7 | eagerLoadControllersFrom("controllers", application) 8 | 9 | // Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!) 10 | // import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading" 11 | // lazyLoadControllersFrom("controllers", application) 12 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /app/jobs/webhooks/movies_job.rb: -------------------------------------------------------------------------------- 1 | class Webhooks::MoviesJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(inbound_webhook) 5 | webhook_payload = JSON.parse(inbound_webhook.body, symbolize_names: true) 6 | # do whatever you'd like here with your webhook payload 7 | # call another service, update a record, etc. 8 | 9 | inbound_webhook.update!(status: :processed) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/jobs/webhooks/stripe_job.rb: -------------------------------------------------------------------------------- 1 | class Webhooks::StripeJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(inbound_webhook) 5 | json = JSON.parse(inbound_webhook.body, symbolize_names: true) 6 | event = Stripe::Event.construct_from(json) 7 | case event.type 8 | when 'customer.updated' 9 | # Find customer and save changes 10 | inbound_webhook.update!(status: :processed) 11 | else 12 | inbound_webhook.update!(status: :skipped) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: "from@example.com" 3 | layout "mailer" 4 | end 5 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | primary_abstract_class 3 | end 4 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/inbound_webhook.rb: -------------------------------------------------------------------------------- 1 | class InboundWebhook < ApplicationRecord 2 | end 3 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RailsconfWebhooks 5 | 6 | <%= csrf_meta_tags %> 7 | <%= csp_meta_tag %> 8 | 9 | <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> 10 | <%= javascript_importmap_tags %> 11 | 12 | 13 | 14 | <%= yield %> 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | bundler_version = $1 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../../Gemfile", __FILE__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_requirement 64 | @bundler_requirement ||= 65 | env_var_version || cli_arg_version || 66 | bundler_requirement_for(lockfile_version) 67 | end 68 | 69 | def bundler_requirement_for(version) 70 | return "#{Gem::Requirement.default}.a" unless version 71 | 72 | bundler_gem_version = Gem::Version.new(version) 73 | 74 | requirement = bundler_gem_version.approximate_recommendation 75 | 76 | return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0") 77 | 78 | requirement += ".a" if bundler_gem_version.prerelease? 79 | 80 | requirement 81 | end 82 | 83 | def load_bundler! 84 | ENV["BUNDLE_GEMFILE"] ||= gemfile 85 | 86 | activate_bundler 87 | end 88 | 89 | def activate_bundler 90 | gem_error = activation_error_handling do 91 | gem "bundler", bundler_requirement 92 | end 93 | return if gem_error.nil? 94 | require_error = activation_error_handling do 95 | require "bundler/version" 96 | end 97 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 98 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" 99 | exit 42 100 | end 101 | 102 | def activation_error_handling 103 | yield 104 | nil 105 | rescue StandardError, LoadError => e 106 | e 107 | end 108 | end 109 | 110 | m.load_bundler! 111 | 112 | if m.invoked_as_script? 113 | load Gem.bin_path("bundler", "bundle") 114 | end 115 | -------------------------------------------------------------------------------- /bin/importmap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "../config/application" 4 | require "importmap/commands" 5 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path("..", __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts "== Installing dependencies ==" 17 | system! "gem install bundler --conservative" 18 | system("bundle check") || system!("bundle install") 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?("config/database.yml") 22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! "bin/rails db:prepare" 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! "bin/rails log:clear tmp:clear" 30 | 31 | puts "\n== Restarting application server ==" 32 | system! "bin/rails restart" 33 | end 34 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails/all" 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module RailsconfWebhooks 10 | class Application < Rails::Application 11 | # Initialize configuration defaults for originally generated Rails version. 12 | config.load_defaults 7.0 13 | 14 | # Configuration for the application, engines, and railties goes here. 15 | # 16 | # These settings can be overridden in specific environments using the files 17 | # in config/environments, which are processed later. 18 | # 19 | # config.time_zone = "Central Time (US & Canada)" 20 | # config.eager_load_paths << Rails.root.join("extras") 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: redis 3 | url: redis://localhost:6379/1 4 | 5 | test: 6 | adapter: test 7 | 8 | production: 9 | adapter: redis 10 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 11 | channel_prefix: railsconf_webhooks_production 12 | -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | gzY/vIzaW4AKn3gfy/nvC6MgDn1OBLZywYmItbCKSx+7dHiQlo/0PLLG9MeD/cIZ7hKhUg336Ce+9VpIJFnA3WbnvxNg+9kUlsWe4rUxGJVLud/cPr6lAZ7W80oS3a4q1ONOAMgSk4NPhfscc4Zx7ea/yxWlkhujMuJgnygjyMtYeYTJHjCnOgwMjJLnRp5VG1kfl7a05I2rhnBYKSdBQNqhmpcJ4SJPSaStWX2pP1jXe2N7F1Er/Ikehq1i0vpbfJ7hQjVcxbhnEzZoJNJR/xW5JpbKEF7IZkkCT9HYAmEHs4miA+HvE/Tit4VX9ZpJmwLo++SNuRf0/x+sRvaRJHRPH7zsXabmidSQiF/poHYelnZD4x/LTgOwohr2/QdvA68mi/6dk2LxYRDUJv6fI6DBiC9WbOoyqt+i--fIoAHOFFXQMZH9S8--ZsGZ0+dBjcMIFrNBKx1M8g== -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem "sqlite3" 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # In the development environment your application's code is reloaded any time 7 | # it changes. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. 12 | config.eager_load = false 13 | 14 | # Show full error reports. 15 | config.consider_all_requests_local = true 16 | 17 | # Enable server timing 18 | config.server_timing = true 19 | 20 | # Enable/disable caching. By default caching is disabled. 21 | # Run rails dev:cache to toggle caching. 22 | if Rails.root.join("tmp/caching-dev.txt").exist? 23 | config.action_controller.perform_caching = true 24 | config.action_controller.enable_fragment_cache_logging = true 25 | 26 | config.cache_store = :memory_store 27 | config.public_file_server.headers = { 28 | "Cache-Control" => "public, max-age=#{2.days.to_i}" 29 | } 30 | else 31 | config.action_controller.perform_caching = false 32 | 33 | config.cache_store = :null_store 34 | end 35 | 36 | # Store uploaded files on the local file system (see config/storage.yml for options). 37 | config.active_storage.service = :local 38 | 39 | # Don't care if the mailer can't send. 40 | config.action_mailer.raise_delivery_errors = false 41 | 42 | config.action_mailer.perform_caching = false 43 | 44 | # Print deprecation notices to the Rails logger. 45 | config.active_support.deprecation = :log 46 | 47 | # Raise exceptions for disallowed deprecations. 48 | config.active_support.disallowed_deprecation = :raise 49 | 50 | # Tell Active Support which deprecation messages to disallow. 51 | config.active_support.disallowed_deprecation_warnings = [] 52 | 53 | # Raise an error on page load if there are pending migrations. 54 | config.active_record.migration_error = :page_load 55 | 56 | # Highlight code that triggered database queries in logs. 57 | config.active_record.verbose_query_logs = true 58 | 59 | # Suppress logger output for asset requests. 60 | config.assets.quiet = true 61 | 62 | # Raises error for missing translations. 63 | # config.i18n.raise_on_missing_translations = true 64 | 65 | # Annotate rendered view with file names. 66 | # config.action_view.annotate_rendered_view_with_filenames = true 67 | 68 | # Uncomment if you wish to allow Action Cable access from any origin. 69 | # config.action_cable.disable_request_forgery_protection = true 70 | end 71 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.cache_classes = true 8 | 9 | # Eager load code on boot. This eager loads most of Rails and 10 | # your application in memory, allowing both threaded web servers 11 | # and those relying on copy on write to perform better. 12 | # Rake tasks automatically ignore this option for performance. 13 | config.eager_load = true 14 | 15 | # Full error reports are disabled and caching is turned on. 16 | config.consider_all_requests_local = false 17 | config.action_controller.perform_caching = true 18 | 19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 21 | # config.require_master_key = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? 26 | 27 | # Compress CSS using a preprocessor. 28 | # config.assets.css_compressor = :sass 29 | 30 | # Do not fallback to assets pipeline if a precompiled asset is missed. 31 | config.assets.compile = false 32 | 33 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 34 | # config.asset_host = "http://assets.example.com" 35 | 36 | # Specifies the header that your server uses for sending files. 37 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache 38 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX 39 | 40 | # Store uploaded files on the local file system (see config/storage.yml for options). 41 | config.active_storage.service = :local 42 | 43 | # Mount Action Cable outside main process or domain. 44 | # config.action_cable.mount_path = nil 45 | # config.action_cable.url = "wss://example.com/cable" 46 | # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] 47 | 48 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 49 | # config.force_ssl = true 50 | 51 | # Include generic and useful information about system operation, but avoid logging too much 52 | # information to avoid inadvertent exposure of personally identifiable information (PII). 53 | config.log_level = :info 54 | 55 | # Prepend all log lines with the following tags. 56 | config.log_tags = [ :request_id ] 57 | 58 | # Use a different cache store in production. 59 | # config.cache_store = :mem_cache_store 60 | 61 | # Use a real queuing backend for Active Job (and separate queues per environment). 62 | # config.active_job.queue_adapter = :resque 63 | # config.active_job.queue_name_prefix = "railsconf_webhooks_production" 64 | 65 | config.action_mailer.perform_caching = false 66 | 67 | # Ignore bad email addresses and do not raise email delivery errors. 68 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 69 | # config.action_mailer.raise_delivery_errors = false 70 | 71 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 72 | # the I18n.default_locale when a translation cannot be found). 73 | config.i18n.fallbacks = true 74 | 75 | # Don't log any deprecations. 76 | config.active_support.report_deprecations = false 77 | 78 | # Use default logging formatter so that PID and timestamp are not suppressed. 79 | config.log_formatter = ::Logger::Formatter.new 80 | 81 | # Use a different logger for distributed setups. 82 | # require "syslog/logger" 83 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") 84 | 85 | if ENV["RAILS_LOG_TO_STDOUT"].present? 86 | logger = ActiveSupport::Logger.new(STDOUT) 87 | logger.formatter = config.log_formatter 88 | config.logger = ActiveSupport::TaggedLogging.new(logger) 89 | end 90 | 91 | # Do not dump schema after migrations. 92 | config.active_record.dump_schema_after_migration = false 93 | end 94 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | 8 | Rails.application.configure do 9 | # Settings specified here will take precedence over those in config/application.rb. 10 | 11 | # Turn false under Spring and add config.action_view.cache_template_loading = true. 12 | config.cache_classes = true 13 | 14 | # Eager loading loads your whole application. When running a single test locally, 15 | # this probably isn't necessary. It's a good idea to do in a continuous integration 16 | # system, or in some way before deploying your code. 17 | config.eager_load = ENV["CI"].present? 18 | 19 | # Configure public file server for tests with Cache-Control for performance. 20 | config.public_file_server.enabled = true 21 | config.public_file_server.headers = { 22 | "Cache-Control" => "public, max-age=#{1.hour.to_i}" 23 | } 24 | 25 | # Show full error reports and disable caching. 26 | config.consider_all_requests_local = true 27 | config.action_controller.perform_caching = false 28 | config.cache_store = :null_store 29 | 30 | # Raise exceptions instead of rendering exception templates. 31 | config.action_dispatch.show_exceptions = false 32 | 33 | # Disable request forgery protection in test environment. 34 | config.action_controller.allow_forgery_protection = false 35 | 36 | # Store uploaded files on the local file system in a temporary directory. 37 | config.active_storage.service = :test 38 | 39 | config.action_mailer.perform_caching = false 40 | 41 | # Tell Action Mailer not to deliver emails to the real world. 42 | # The :test delivery method accumulates sent emails in the 43 | # ActionMailer::Base.deliveries array. 44 | config.action_mailer.delivery_method = :test 45 | 46 | # Print deprecation notices to the stderr. 47 | config.active_support.deprecation = :stderr 48 | 49 | # Raise exceptions for disallowed deprecations. 50 | config.active_support.disallowed_deprecation = :raise 51 | 52 | # Tell Active Support which deprecation messages to disallow. 53 | config.active_support.disallowed_deprecation_warnings = [] 54 | 55 | # Raises error for missing translations. 56 | # config.i18n.raise_on_missing_translations = true 57 | 58 | # Annotate rendered view with file names. 59 | # config.action_view.annotate_rendered_view_with_filenames = true 60 | end 61 | -------------------------------------------------------------------------------- /config/importmap.rb: -------------------------------------------------------------------------------- 1 | # Pin npm packages by running ./bin/importmap 2 | 3 | pin "application", preload: true 4 | pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true 5 | pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true 6 | pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true 7 | pin_all_from "app/javascript/controllers", under: "controllers" 8 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = "1.0" 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in the app/assets 11 | # folder are already added. 12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 13 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy. 4 | # See the Securing Rails Applications Guide for more information: 5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header 6 | 7 | # Rails.application.configure do 8 | # config.content_security_policy do |policy| 9 | # policy.default_src :self, :https 10 | # policy.font_src :self, :https, :data 11 | # policy.img_src :self, :https, :data 12 | # policy.object_src :none 13 | # policy.script_src :self, :https 14 | # policy.style_src :self, :https 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | # 19 | # # Generate session nonces for permitted importmap and inline scripts 20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 21 | # config.content_security_policy_nonce_directives = %w(script-src) 22 | # 23 | # # Report violations without enforcing the policy. 24 | # # config.content_security_policy_report_only = true 25 | # end 26 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be filtered from the log file. Use this to limit dissemination of 4 | # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported 5 | # notations and behaviors. 6 | Rails.application.config.filter_parameters += [ 7 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn 8 | ] 9 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular "person", "people" 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym "RESTful" 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Define an application-wide HTTP permissions policy. For further 2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 3 | # 4 | # Rails.application.config.permissions_policy do |f| 5 | # f.camera :none 6 | # f.gyroscope :none 7 | # f.microphone :none 8 | # f.usb :none 9 | # f.fullscreen :self 10 | # f.payment :self, "https://secure.example.com" 11 | # end 12 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t "hello" 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t("hello") %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # "true": "foo" 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 9 | threads min_threads_count, max_threads_count 10 | 11 | # Specifies the `worker_timeout` threshold that Puma will use to wait before 12 | # terminating a worker in development environments. 13 | # 14 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" 15 | 16 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 17 | # 18 | port ENV.fetch("PORT") { 3000 } 19 | 20 | # Specifies the `environment` that Puma will run in. 21 | # 22 | environment ENV.fetch("RAILS_ENV") { "development" } 23 | 24 | # Specifies the `pidfile` that Puma will use. 25 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 26 | 27 | # Specifies the number of `workers` to boot in clustered mode. 28 | # Workers are forked web server processes. If using threads and workers together 29 | # the concurrency of the application would be max `threads` * `workers`. 30 | # Workers do not work on JRuby or Windows (both of which do not support 31 | # processes). 32 | # 33 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 34 | 35 | # Use the `preload_app!` method when specifying a `workers` number. 36 | # This directive tells Puma to first boot the application and load code 37 | # before forking the application. This takes advantage of Copy On Write 38 | # process behavior so workers use less memory. 39 | # 40 | # preload_app! 41 | 42 | # Allow puma to be restarted by `bin/rails restart` command. 43 | plugin :tmp_restart 44 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | namespace :webhooks do 3 | # /webhooks/movies routed to our Webhooks::MoviesController 4 | resource :movies, controller: :movies, only: [:create] 5 | 6 | # /webhooks/stripe routed to our Webhooks::StripeController 7 | resource :stripe, controller: :stripe, only: [:create] 8 | end 9 | 10 | # root "articles#index" 11 | end 12 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket-<%= Rails.env %> 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket-<%= Rails.env %> 23 | 24 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name-<%= Rails.env %> 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /db/migrate/20230422190352_create_inbound_webhooks.rb: -------------------------------------------------------------------------------- 1 | class CreateInboundWebhooks < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :inbound_webhooks do |t| 4 | t.string :status 5 | t.text :body 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema[7.0].define(version: 2023_04_22_190352) do 14 | create_table "inbound_webhooks", force: :cascade do |t| 15 | t.string "status" 16 | t.text "body" 17 | t.datetime "created_at", null: false 18 | t.datetime "updated_at", null: false 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }]) 7 | # Character.create(name: "Luke", movie: movies.first) 8 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/lib/assets/.keep -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/lib/tasks/.keep -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/log/.keep -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /slides/railsconf.md: -------------------------------------------------------------------------------- 1 | --- 2 | marp: true 3 | --- 4 | 5 | # :hook: Catch Me If You Can 6 | 7 | ## Consuming Webhooks in Ruby on Rails 8 | 9 | ### :wave: @colinloretz and @excid3 10 | 11 | --- 12 | 13 | # :wave: Hello! Welcome! 14 | 15 | ### Setting the Stage 16 | 17 | --- 18 | 19 | # Why Webhooks? 20 | 21 | --- 22 | 23 | # Github Repository 24 | 25 | ### https://github.com/colinloretz/railsconf-webhooks 26 | 27 | You can take a look at each code addition in the GitHub PRs. 28 | 29 | --- 30 | 31 | # Doing it live 32 | 33 | - Step 1: Controllers & Routes 34 | - Step 2: Adding A Model 35 | - Step 3: Background Jobs 36 | - Step 4: Adding Verification 37 | - Step 5: Adding Some Tests 38 | 39 | --- 40 | 41 | # Step 1: Creating our Routes & Controllers 42 | 43 | --- 44 | 45 | ## Creating a Webhooks::BaseController 46 | 47 | ```bash 48 | rails generate controller Webhooks::BaseController 49 | ``` 50 | 51 | ```ruby 52 | # app/controllers/webhooks/base_controller.rb 53 | 54 | class Webhooks::BaseController < ApplicationController 55 | # Disable CSRF checking on webhooks because they do not originate from the browser 56 | skip_before_action :verify_authenticity_token 57 | 58 | def create 59 | head :ok 60 | end 61 | end 62 | ``` 63 | 64 | --- 65 | 66 | ## Creating a Movies Controller 67 | 68 | ```bash 69 | rails generate controller Webhooks::MoviesController 70 | ``` 71 | 72 | ```ruby 73 | # app/controllers/webhooks/movies_controller.rb 74 | 75 | class Webhooks::MoviesController < Webhooks::BaseController 76 | # A controller for catching new movie webhooks 77 | # 78 | # To send a sample webhook locally: 79 | # 80 | # curl -X POST http://localhost:3000/webhooks/movies 81 | # -H 'Content-Type: application/json' 82 | # -d '{"title":"Titanic"}' 83 | # 84 | # If you'd like to override the base controller's behavior, you can do so here 85 | # def create 86 | # head :ok 87 | # end 88 | end 89 | ``` 90 | 91 | --- 92 | 93 | ## Creating a Stripe Controller 94 | 95 | ```bash 96 | rails generate controller Webhooks::StripeController 97 | ``` 98 | 99 | ```ruby 100 | # app/controllers/webhooks/stripe_controller.rb 101 | 102 | class Webhooks::StripeController < Webhooks::BaseController 103 | # If you'd like to override the base controller's behavior, you can do so here 104 | # def create 105 | # head :ok 106 | # end 107 | end 108 | ``` 109 | 110 | --- 111 | 112 | ```bash 113 | curl -X POST http://localhost:3000/webhooks/movies 114 | -H 'Content-Type: application/json' 115 | -d '{"title":"Dungeons & Dragons: Honor Among Thieves"}' 116 | ``` 117 | 118 | --- 119 | 120 | ## Let's Add Some Routes! 121 | 122 | ```ruby 123 | # config/routes.rb 124 | 125 | Rails.application.routes.draw do 126 | namespace :webhooks do 127 | # /webhooks/movies routed to our Webhooks::MoviesController 128 | resource :movies, controller: :movies, only: [:create] 129 | 130 | # /webhooks/stripe routed to our Webhooks::StripeController 131 | resource :stripe, controller: :stripe, only: [:create] 132 | end 133 | 134 | # your other routes here 135 | end 136 | ``` 137 | 138 | --- 139 | 140 | # Testing Our Routes 141 | 142 | ```bash 143 | curl -X POST http://localhost:3000/webhooks/movies 144 | -H 'Content-Type: application/json' 145 | -d '{"title":"The Fifth Element"}' 146 | ``` 147 | 148 | --- 149 | 150 | # Step 2: Adding A Model 151 | 152 | ```bash 153 | rails generate model InboundWebhook status:string body:text 154 | ``` 155 | 156 | ```bash 157 | rails db:migrate 158 | ``` 159 | 160 | --- 161 | 162 | ## Updating Our Controller 163 | 164 | ```ruby 165 | # app/controllers/webhooks/base_controller.rb 166 | 167 | class Webhooks::BaseController < ApplicationController 168 | skip_before_action :verify_authenticity_token 169 | 170 | def create 171 | InboundWebhook.create(body: payload) 172 | head :ok 173 | end 174 | 175 | private 176 | 177 | def payload 178 | @payload ||= request.body.read 179 | end 180 | end 181 | ``` 182 | 183 | --- 184 | 185 | ## Let's create a database record 186 | 187 | ```bash 188 | curl -X POST http://localhost:3000/webhooks/movies 189 | -H 'Content-Type: application/json' 190 | -d '{"title":"Fellowship of the Ring"}' 191 | ``` 192 | 193 | You should now have an InboundWebhook record in your database. 194 | 195 | ```bash 196 | rails c 197 | ``` 198 | 199 | ```bash 200 | InboundWebhook.count 201 | ``` 202 | 203 | --- 204 | 205 | # Step 3: Processing Webhooks in the Background 206 | 207 | ```bash 208 | rails generate job Webhooks::MoviesJob 209 | ``` 210 | 211 | ```bash 212 | rails generate job Webhooks::StripeJob 213 | ``` 214 | 215 | --- 216 | 217 | ## A job to process Movies webhooks 218 | 219 | ```ruby 220 | # app/jobs/webhooks/movies_job.rb 221 | 222 | class Webhooks::MoviesJob < ApplicationJob 223 | queue_as :default 224 | 225 | def perform(inbound_webhook) 226 | webhook_payload = JSON.parse(inbound_webhook.body, symbolize_names: true) 227 | 228 | # do whatever you'd like here with your webhook payload 229 | # call another service, update a record, etc. 230 | 231 | inbound_webhook.update!(status: :processed) 232 | end 233 | end 234 | ``` 235 | 236 | --- 237 | 238 | ## A job to process our Stripe webhooks 239 | 240 | ```ruby 241 | # app/jobs/webhooks/stripe_job.rb 242 | 243 | class Webhooks::StripeJob < ApplicationJob 244 | queue_as :default 245 | 246 | def perform(inbound_webhook) 247 | json = JSON.parse(inbound_webhook.body, symbolize_names: true) 248 | event = Stripe::Event.construct_from(json) 249 | case event.type 250 | when 'customer.updated' 251 | # Find customer and save changes 252 | inbound_webhook.update!(status: :processed) 253 | else 254 | inbound_webhook.update!(status: :skipped) 255 | end 256 | end 257 | end 258 | ``` 259 | 260 | --- 261 | 262 | # Now we need to trigger our jobs 263 | 264 | ### Let's update our controllers 265 | 266 | --- 267 | 268 | # Our Webhooks::MoviesController 269 | 270 | ```ruby 271 | # app/controllers/webhooks/movies_controller.rb 272 | 273 | def create 274 | # Save webhook to database 275 | record = InboundWebhook.create!(body: payload) 276 | 277 | # Queue database record for processing 278 | Webhooks::MoviesJob.perform_later(record) 279 | 280 | head :ok 281 | end 282 | ``` 283 | 284 | --- 285 | 286 | # Our Webhooks::StripeController 287 | 288 | ```ruby 289 | # app/controllers/webhooks/stripe_controller.rb 290 | 291 | def create 292 | # Save webhook to database 293 | record = InboundWebhook.create!(body: payload) 294 | 295 | # Queue database record for processing 296 | Webhooks::StripeJob.perform_later(record) 297 | 298 | head :ok 299 | end 300 | ``` 301 | 302 | --- 303 | 304 | # Step 4: Verifying Our Webhook Payloads 305 | 306 | - Verify that the webhook is real and from the provider 307 | 308 | --- 309 | 310 | ## Let's add a `verify_event` method to our BaseController 311 | 312 | ```ruby 313 | # app/controllers/webhooks/base_controller.rb 314 | 315 | class Webhooks::BaseController < ApplicationController 316 | skip_before_action :verify_authenticity_token 317 | 318 | before_action :verify_event 319 | 320 | def create 321 | InboundWebhook.create(body: payload) 322 | head :ok 323 | end 324 | 325 | private 326 | 327 | def verify_event 328 | head :bad_request 329 | end 330 | 331 | def payload 332 | @payload ||= request.body.read 333 | end 334 | end 335 | ``` 336 | 337 | --- 338 | 339 | # Now we can implement `verify_event` specifically for each provider 340 | 341 | --- 342 | 343 | ## Movies Webhook Verification 344 | 345 | ```ruby 346 | # app/controllers/webhooks/movies_controller.rb 347 | # ... rest of controller 348 | 349 | private 350 | 351 | def verify_event 352 | head :bad_request if params[:fail_verification] 353 | end 354 | ``` 355 | 356 | ```bash 357 | 358 | curl -X POST http://localhost:3000/webhooks/movies?fail_verification=1 359 | -H 'Content-Type: application/json' 360 | -d '{"title":"Hackers"}' 361 | ``` 362 | 363 | --- 364 | 365 | ## Stripe Webhook Verification 366 | 367 | ```ruby 368 | # app/controllers/webhooks/stripe_controller.rb 369 | # ... rest of controller 370 | 371 | private 372 | 373 | def verify_event 374 | signature = request.headers['Stripe-Signature'] 375 | secret = Rails.application.credentials.dig(:stripe, :webhook_signing_secret) 376 | 377 | ::Stripe::Webhook::Signature.verify_header( 378 | payload, 379 | signature, 380 | secret.to_s, 381 | tolerance: Stripe::Webhook::DEFAULT_TOLERANCE, 382 | ) 383 | rescue ::Stripe::SignatureVerificationError 384 | head :bad_request 385 | end 386 | ``` 387 | 388 | --- 389 | 390 | # Step 5: Adding Some Tests 391 | 392 | ```bash 393 | mkdir test/fixtures/webhooks 394 | ``` 395 | 396 | ```bash 397 | touch test/fixtures/webhooks/movie.json 398 | ``` 399 | 400 | ```json 401 | { 402 | "title": "Dungeons & Dragons: Honor Among Thieves", 403 | "release_date": "2023-03-31" 404 | } 405 | ``` 406 | 407 | --- 408 | 409 | ## Our First Webhook Test 410 | 411 | ```ruby 412 | # spec/requests/webhooks/movies_controller_spec.rb 413 | require 'test_helper' 414 | 415 | class Webhooks::MoviesControllerTest < ActionDispatch::IntegrationTest 416 | def setup 417 | # Load the webhook data from the JSON file 418 | file_path = Rails.root.join('test', 'fixtures', 'webhooks', 'movie.json') 419 | @webhook = JSON.parse(File.read(file_path)) 420 | end 421 | 422 | test 'should consume webhook' do 423 | # Send the POST request to the create action with the prepared data 424 | post webhooks_movies_url, params: @webhook 425 | 426 | # Check if the response status is 200 OK 427 | assert_response :ok 428 | 429 | # You can create other test files for each job or service that is called 430 | # For example, check if a record was created/updated in the database 431 | end 432 | end 433 | ``` 434 | 435 | --- 436 | 437 | # Similarity to Action Mailbox 438 | 439 | --- 440 | 441 | # Security 442 | 443 | - Verification methods 444 | - TLS, Oauth, Asymetric keys, HMAC 445 | - Replay Attacks 446 | - Dataless notifications 447 | 448 | --- 449 | 450 | # Scaling 451 | 452 | - Background processing 453 | - AWS Eventbridge 454 | 455 | --- 456 | 457 | # Refactoring As Your Codebase Grows 458 | 459 | - Middleware 460 | - Webhook Handler 461 | 462 | --- 463 | 464 | # Common Gotchas 465 | 466 | - Provider could fail to send a webhook 467 | - Provider might not retry sending webhooks 468 | - Your application needs to be up to receive a webhook 469 | - Webhooks may arrive out of order 470 | - Data in webhook payload may be stale 471 | - Webhooks may be duplicated 472 | 473 | --- 474 | 475 | # Tools 476 | 477 | - Webhooks.fyi 478 | - ngrok and other localtunnels 479 | - Hookrelay 480 | 481 | --- 482 | 483 | # :hook: Questions? 484 | 485 | --- 486 | 487 | # Thank You! 488 | 489 | ### :bird: @colinloretz and @excid3 490 | -------------------------------------------------------------------------------- /slides/railsconf.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/slides/railsconf.pdf -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/storage/.keep -------------------------------------------------------------------------------- /test/application_system_test_case.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 4 | driven_by :selenium, using: :chrome, screen_size: [1400, 1400] 5 | end 6 | -------------------------------------------------------------------------------- /test/channels/application_cable/connection_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase 4 | # test "connects with cookies" do 5 | # cookies.signed[:user_id] = 42 6 | # 7 | # connect 8 | # 9 | # assert_equal connection.user_id, "42" 10 | # end 11 | end 12 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/test/controllers/.keep -------------------------------------------------------------------------------- /test/controllers/webhooks/base_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Webhooks::BaseControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/webhooks/movies_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Webhooks::MoviesControllerTest < ActionDispatch::IntegrationTest 4 | def setup 5 | # Load the webhook data from the JSON file 6 | file_path = Rails.root.join('test', 'fixtures', 'webhooks', 'movie.json') 7 | @webhook = JSON.parse(File.read(file_path)) 8 | end 9 | 10 | test 'should consume webhook' do 11 | # Send the POST request to the create action with the prepared data 12 | post webhooks_movies_url, params: @webhook 13 | 14 | # Check if the response status is 200 OK 15 | assert_response :ok 16 | 17 | # You can create other test files for each job or service that is called 18 | # For example, check if a record was created/updated in the database 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/controllers/webhooks/stripe_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Webhooks::StripeControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/test/fixtures/files/.keep -------------------------------------------------------------------------------- /test/fixtures/inbound_webhooks.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | status: MyString 5 | body: MyText 6 | 7 | two: 8 | status: MyString 9 | body: MyText 10 | -------------------------------------------------------------------------------- /test/fixtures/webhooks/movie.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Dungeons & Dragons: Honor Among Thieves" 3 | } -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/test/helpers/.keep -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/test/integration/.keep -------------------------------------------------------------------------------- /test/jobs/webhooks/movies_job_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Webhooks::MoviesJobTest < ActiveJob::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/jobs/webhooks/stripe_job_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Webhooks::StripeJobTest < ActiveJob::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/test/mailers/.keep -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/test/models/.keep -------------------------------------------------------------------------------- /test/models/inbound_webhook_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class InboundWebhookTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/system/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/test/system/.keep -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= "test" 2 | require_relative "../config/environment" 3 | require "rails/test_help" 4 | 5 | class ActiveSupport::TestCase 6 | # Run tests in parallel with specified workers 7 | parallelize(workers: :number_of_processors) 8 | 9 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 10 | fixtures :all 11 | 12 | # Add more helper methods to be used by all tests here... 13 | end 14 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/tmp/.keep -------------------------------------------------------------------------------- /tmp/pids/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/tmp/pids/.keep -------------------------------------------------------------------------------- /tmp/storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/tmp/storage/.keep -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/vendor/.keep -------------------------------------------------------------------------------- /vendor/javascript/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinloretz/railsconf-webhooks/c96675a842ae2b6acc9cec0f43b4dcb18684ca1e/vendor/javascript/.keep --------------------------------------------------------------------------------