├── .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
--------------------------------------------------------------------------------