webhukhs 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +14 -0
  3. data/.rubocop.yml +9 -0
  4. data/.standard.yml +3 -0
  5. data/Appraisals +13 -0
  6. data/CHANGELOG.md +54 -0
  7. data/LICENSE +21 -0
  8. data/README.md +71 -0
  9. data/Rakefile +26 -0
  10. data/config/routes.rb +3 -0
  11. data/example/.gitattributes +7 -0
  12. data/example/.gitignore +29 -0
  13. data/example/.ruby-version +1 -0
  14. data/example/Gemfile +32 -0
  15. data/example/Gemfile.lock +228 -0
  16. data/example/README.md +24 -0
  17. data/example/Rakefile +8 -0
  18. data/example/app/assets/images/.keep +0 -0
  19. data/example/app/assets/stylesheets/application.css +1 -0
  20. data/example/app/controllers/application_controller.rb +4 -0
  21. data/example/app/controllers/concerns/.keep +0 -0
  22. data/example/app/helpers/application_helper.rb +4 -0
  23. data/example/app/models/application_record.rb +5 -0
  24. data/example/app/models/concerns/.keep +0 -0
  25. data/example/app/views/layouts/application.html.erb +15 -0
  26. data/example/app/webhooks/webhook_test_handler.rb +13 -0
  27. data/example/bin/bundle +109 -0
  28. data/example/bin/rails +4 -0
  29. data/example/bin/rake +4 -0
  30. data/example/bin/setup +33 -0
  31. data/example/config/application.rb +39 -0
  32. data/example/config/boot.rb +5 -0
  33. data/example/config/credentials.yml.enc +1 -0
  34. data/example/config/database.yml +25 -0
  35. data/example/config/environment.rb +7 -0
  36. data/example/config/environments/development.rb +61 -0
  37. data/example/config/environments/production.rb +71 -0
  38. data/example/config/environments/test.rb +52 -0
  39. data/example/config/initializers/assets.rb +14 -0
  40. data/example/config/initializers/content_security_policy.rb +27 -0
  41. data/example/config/initializers/filter_parameter_logging.rb +10 -0
  42. data/example/config/initializers/generators.rb +7 -0
  43. data/example/config/initializers/inflections.rb +18 -0
  44. data/example/config/initializers/permissions_policy.rb +13 -0
  45. data/example/config/initializers/webhukhs.rb +9 -0
  46. data/example/config/locales/en.yml +33 -0
  47. data/example/config/puma.rb +45 -0
  48. data/example/config/routes.rb +5 -0
  49. data/example/config.ru +8 -0
  50. data/example/db/migrate/20240523125859_create_webhukhs_tables.rb +22 -0
  51. data/example/db/schema.rb +24 -0
  52. data/example/db/seeds.rb +9 -0
  53. data/example/lib/assets/.keep +0 -0
  54. data/example/lib/tasks/.keep +0 -0
  55. data/example/log/.keep +0 -0
  56. data/example/public/404.html +67 -0
  57. data/example/public/422.html +67 -0
  58. data/example/public/500.html +66 -0
  59. data/example/public/apple-touch-icon-precomposed.png +0 -0
  60. data/example/public/apple-touch-icon.png +0 -0
  61. data/example/public/favicon.ico +0 -0
  62. data/example/public/robots.txt +1 -0
  63. data/example/test/controllers/.keep +0 -0
  64. data/example/test/fixtures/files/.keep +0 -0
  65. data/example/test/helpers/.keep +0 -0
  66. data/example/test/integration/.keep +0 -0
  67. data/example/test/models/.keep +0 -0
  68. data/example/test/test_helper.rb +15 -0
  69. data/example/tmp/.keep +0 -0
  70. data/example/tmp/pids/.keep +0 -0
  71. data/example/vendor/.keep +0 -0
  72. data/gemfiles/rails_7.1.gemfile +10 -0
  73. data/gemfiles/rails_7.1.gemfile.lock +412 -0
  74. data/gemfiles/rails_8.0.gemfile +10 -0
  75. data/gemfiles/rails_8.0.gemfile.lock +405 -0
  76. data/gemfiles/rails_8.1.gemfile +10 -0
  77. data/gemfiles/rails_8.1.gemfile.lock +405 -0
  78. data/handler-examples/customer_io_handler.rb +36 -0
  79. data/handler-examples/revolut_business_v1_handler.rb +29 -0
  80. data/handler-examples/revolut_business_v2_handler.rb +42 -0
  81. data/handler-examples/starling_payments_handler.rb +25 -0
  82. data/lib/tasks/webhukhs_tasks.rake +4 -0
  83. data/lib/webhukhs/base_handler.rb +100 -0
  84. data/lib/webhukhs/controllers/receive_webhooks_controller.rb +55 -0
  85. data/lib/webhukhs/engine.rb +24 -0
  86. data/lib/webhukhs/install_generator.rb +31 -0
  87. data/lib/webhukhs/jobs/processing_job.rb +33 -0
  88. data/lib/webhukhs/models/received_webhook.rb +99 -0
  89. data/lib/webhukhs/templates/add_headers_to_webhukhs_webhooks.rb.erb +5 -0
  90. data/lib/webhukhs/templates/create_webhukhs_tables.rb.erb +23 -0
  91. data/lib/webhukhs/templates/webhukhs.rb +40 -0
  92. data/lib/webhukhs/version.rb +5 -0
  93. data/lib/webhukhs.rb +23 -0
  94. data/test/test-webhook-handlers/.DS_Store +0 -0
  95. data/test/test-webhook-handlers/extract_id_handler.rb +5 -0
  96. data/test/test-webhook-handlers/failing_with_concealed_errors.rb +7 -0
  97. data/test/test-webhook-handlers/failing_with_exposed_errors.rb +7 -0
  98. data/test/test-webhook-handlers/inactive_handler.rb +5 -0
  99. data/test/test-webhook-handlers/invalid_handler.rb +5 -0
  100. data/test/test-webhook-handlers/private_handler.rb +3 -0
  101. data/test/test-webhook-handlers/webhook_test_handler.rb +13 -0
  102. data/test/test_app.rb +66 -0
  103. data/test/test_helper.rb +50 -0
  104. data/test/webhukhs_test.rb +203 -0
  105. metadata +247 -0
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require_relative "test_app"
5
+
6
+ class TestWebhukhs < ActionDispatch::IntegrationTest
7
+ teardown { Webhukhs::ReceivedWebhook.delete_all }
8
+
9
+ def test_that_it_has_a_version_number
10
+ refute_nil ::Webhukhs::VERSION
11
+ end
12
+
13
+ def webhook_body
14
+ <<~JSON
15
+ {
16
+ "provider_id": "musterbank-flyio",
17
+ "starts_at": "<%= Time.now.utc %>",
18
+ "external_source": "The Forge Of Downtime",
19
+ "external_ticket_title": "DOWN-123",
20
+ "internal_description_markdown": "A test has failed"
21
+ }
22
+ JSON
23
+ end
24
+
25
+ self.app = WebhukhsTestApp
26
+
27
+ def self.xtest(msg)
28
+ test(msg) { skip }
29
+ end
30
+
31
+ test "ensure webhook is processed only once during creation" do
32
+ tf = Tempfile.new
33
+ body = {isValid: true, outputToFilename: tf.path}
34
+ body_json = body.to_json
35
+
36
+ assert_enqueued_jobs 1, only: Webhukhs::ProcessingJob do
37
+ post "/webhukhs/test", params: body_json, headers: {"CONTENT_TYPE" => "application/json"}
38
+ assert_response 200
39
+ end
40
+ end
41
+
42
+ test "accepts a webhook, stores and processes it" do
43
+ tf = Tempfile.new
44
+ body = {isValid: true, outputToFilename: tf.path}
45
+ body_json = body.to_json
46
+
47
+ post "/webhukhs/test", params: body_json, headers: {"CONTENT_TYPE" => "application/json"}
48
+ assert_response 200
49
+
50
+ webhook = Webhukhs::ReceivedWebhook.last!
51
+
52
+ assert_predicate webhook, :received?
53
+ assert_equal "WebhookTestHandler", webhook.handler_module_name
54
+ assert_equal webhook.status, "received"
55
+ assert_equal webhook.body, body_json
56
+
57
+ perform_enqueued_jobs
58
+ assert_predicate webhook.reload, :processed?
59
+ tf.rewind
60
+ assert_equal tf.read, body_json
61
+ end
62
+
63
+ test "accepts a webhook but does not process it if it is invalid" do
64
+ tf = Tempfile.new
65
+ body = {isValid: false, outputToFilename: tf.path}
66
+ body_json = body.to_json
67
+
68
+ post "/webhukhs/test", params: body_json, headers: {"CONTENT_TYPE" => "application/json"}
69
+ assert_response 200
70
+
71
+ webhook = Webhukhs::ReceivedWebhook.last!
72
+
73
+ assert_predicate webhook, :received?
74
+ assert_equal "WebhookTestHandler", webhook.handler_module_name
75
+ assert_equal webhook.status, "received"
76
+ assert_equal webhook.body, body_json
77
+
78
+ perform_enqueued_jobs
79
+ assert_predicate webhook.reload, :failed_validation?
80
+
81
+ tf.rewind
82
+ assert_predicate tf.read, :empty?
83
+ end
84
+
85
+ test "marks a webhook as errored if it raises during processing" do
86
+ tf = Tempfile.new
87
+ body = {isValid: true, raiseDuringProcessing: true, outputToFilename: tf.path}
88
+ body_json = body.to_json
89
+
90
+ post "/webhukhs/test", params: body_json, headers: {"CONTENT_TYPE" => "application/json"}
91
+ assert_response 200
92
+
93
+ webhook = Webhukhs::ReceivedWebhook.last!
94
+
95
+ assert_predicate webhook, :received?
96
+ assert_equal "WebhookTestHandler", webhook.handler_module_name
97
+ assert_equal webhook.status, "received"
98
+ assert_equal webhook.body, body_json
99
+
100
+ assert_raises(StandardError) { perform_enqueued_jobs }
101
+ assert_predicate webhook.reload, :error?
102
+
103
+ tf.rewind
104
+ assert_predicate tf.read, :empty?
105
+ end
106
+
107
+ test "does not accept a test payload that is larger than the configured maximum size" do
108
+ oversize = Webhukhs.configuration.request_body_size_limit + 1
109
+ utf8_junk = Base64.strict_encode64(Random.bytes(oversize))
110
+ body = {isValid: true, filler: utf8_junk, raiseDuringProcessing: false, outputToFilename: "/tmp/nothing"}
111
+ body_json = body.to_json
112
+
113
+ post "/webhukhs/test", params: body_json, headers: {"CONTENT_TYPE" => "application/json"}
114
+ assert_raises(ActiveRecord::RecordNotFound) { Webhukhs::ReceivedWebhook.last! }
115
+ end
116
+
117
+ test "does not try to process a webhook if it is not in `received' state" do
118
+ tf = Tempfile.new
119
+ body = {isValid: true, raiseDuringProcessing: true, outputToFilename: tf.path}
120
+ body_json = body.to_json
121
+
122
+ post "/webhukhs/test", params: body_json, headers: {"CONTENT_TYPE" => "application/json"}
123
+ assert_response 200
124
+
125
+ webhook = Webhukhs::ReceivedWebhook.last!
126
+ webhook.processing!
127
+
128
+ perform_enqueued_jobs
129
+ assert_predicate webhook.reload, :processing?
130
+
131
+ tf.rewind
132
+ assert_predicate tf.read, :empty?
133
+ end
134
+
135
+ test "raises an error if the service_id is not known" do
136
+ post "/webhukhs/missing_service", params: webhook_body, headers: {"CONTENT_TYPE" => "application/json"}
137
+ assert_response 404
138
+ end
139
+
140
+ test "returns a 503 when a handler is inactive" do
141
+ post "/webhukhs/inactive", params: webhook_body, headers: {"CONTENT_TYPE" => "application/json"}
142
+
143
+ assert_response 503
144
+ assert_equal 'Webhook handler "inactive" is inactive', response.parsed_body["error"]
145
+ end
146
+
147
+ test "returns a 200 status and error message if the handler does not expose errors" do
148
+ post "/webhukhs/failing-with-concealed-errors", params: webhook_body, headers: {"CONTENT_TYPE" => "application/json"}
149
+
150
+ assert_response 200
151
+ assert_equal false, response.parsed_body["ok"]
152
+ assert response.parsed_body["error"]
153
+ end
154
+
155
+ test "returns a 500 status and error message if the handler does not expose errors" do
156
+ post "/webhukhs/failing-with-exposed-errors", params: webhook_body, headers: {"CONTENT_TYPE" => "application/json"}
157
+
158
+ assert_response 500
159
+ # The response generation in this case is done by Rails, through the
160
+ # common Rails error page
161
+ end
162
+
163
+ test "deduplicates received webhooks based on the event ID" do
164
+ body = {event_id: SecureRandom.uuid, body: "test"}.to_json
165
+
166
+ assert_changes_by -> { Webhukhs::ReceivedWebhook.count }, exactly: 1 do
167
+ 3.times do
168
+ post "/webhukhs/extract_id", params: body, headers: {"CONTENT_TYPE" => "application/json"}
169
+ assert_response 200
170
+ end
171
+ end
172
+ end
173
+
174
+ test "preserves the route params and the request params in the serialised request stored with the webhook" do
175
+ body = {user_name: "John", number_of_dependents: 14}.to_json
176
+
177
+ Webhukhs::ReceivedWebhook.delete_all
178
+ post "/per-user-webhukhs/123/private", params: body, headers: {"CONTENT_TYPE" => "application/json"}
179
+ assert_response 200
180
+
181
+ received_webhook = Webhukhs::ReceivedWebhook.first!
182
+ assert_predicate received_webhook, :received?
183
+ assert_equal body, received_webhook.request.body.read
184
+ assert_equal "John", received_webhook.request.params["user_name"]
185
+ assert_equal 14, received_webhook.request.params["number_of_dependents"]
186
+ assert_equal "123", received_webhook.request.params["user_id"]
187
+ end
188
+
189
+ test "erroneous webhook could be processed again" do
190
+ webhook = Webhukhs::ReceivedWebhook.create(
191
+ handler_event_id: "test",
192
+ handler_module_name: "WebhookTestHandler",
193
+ status: "error",
194
+ body: {isValid: true}.to_json
195
+ )
196
+
197
+ assert_enqueued_jobs 1, only: Webhukhs::ProcessingJob do
198
+ webhook.received!
199
+
200
+ assert_equal "received", webhook.status
201
+ end
202
+ end
203
+ end
metadata ADDED
@@ -0,0 +1,247 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: webhukhs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Stanislav Katkov
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: state_machine_enum
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: appraisal
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rake
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '13.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '13.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: standard
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: magic_frozen_string_literal
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: minitest
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '5.0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '5.0'
110
+ description: Webhukhs is a Rails engine for processing webhooks from various services.
111
+ Engine saves webhook in database first and later processes webhook in async process.
112
+ Supports custom handlers with Webhukhs::BaseHandler and integrates with Rails common
113
+ error reporter.
114
+ email:
115
+ - github@skatkov.com
116
+ executables: []
117
+ extensions: []
118
+ extra_rdoc_files: []
119
+ files:
120
+ - ".editorconfig"
121
+ - ".rubocop.yml"
122
+ - ".standard.yml"
123
+ - Appraisals
124
+ - CHANGELOG.md
125
+ - LICENSE
126
+ - README.md
127
+ - Rakefile
128
+ - config/routes.rb
129
+ - example/.gitattributes
130
+ - example/.gitignore
131
+ - example/.ruby-version
132
+ - example/Gemfile
133
+ - example/Gemfile.lock
134
+ - example/README.md
135
+ - example/Rakefile
136
+ - example/app/assets/images/.keep
137
+ - example/app/assets/stylesheets/application.css
138
+ - example/app/controllers/application_controller.rb
139
+ - example/app/controllers/concerns/.keep
140
+ - example/app/helpers/application_helper.rb
141
+ - example/app/models/application_record.rb
142
+ - example/app/models/concerns/.keep
143
+ - example/app/views/layouts/application.html.erb
144
+ - example/app/webhooks/webhook_test_handler.rb
145
+ - example/bin/bundle
146
+ - example/bin/rails
147
+ - example/bin/rake
148
+ - example/bin/setup
149
+ - example/config.ru
150
+ - example/config/application.rb
151
+ - example/config/boot.rb
152
+ - example/config/credentials.yml.enc
153
+ - example/config/database.yml
154
+ - example/config/environment.rb
155
+ - example/config/environments/development.rb
156
+ - example/config/environments/production.rb
157
+ - example/config/environments/test.rb
158
+ - example/config/initializers/assets.rb
159
+ - example/config/initializers/content_security_policy.rb
160
+ - example/config/initializers/filter_parameter_logging.rb
161
+ - example/config/initializers/generators.rb
162
+ - example/config/initializers/inflections.rb
163
+ - example/config/initializers/permissions_policy.rb
164
+ - example/config/initializers/webhukhs.rb
165
+ - example/config/locales/en.yml
166
+ - example/config/puma.rb
167
+ - example/config/routes.rb
168
+ - example/db/migrate/20240523125859_create_webhukhs_tables.rb
169
+ - example/db/schema.rb
170
+ - example/db/seeds.rb
171
+ - example/lib/assets/.keep
172
+ - example/lib/tasks/.keep
173
+ - example/log/.keep
174
+ - example/public/404.html
175
+ - example/public/422.html
176
+ - example/public/500.html
177
+ - example/public/apple-touch-icon-precomposed.png
178
+ - example/public/apple-touch-icon.png
179
+ - example/public/favicon.ico
180
+ - example/public/robots.txt
181
+ - example/test/controllers/.keep
182
+ - example/test/fixtures/files/.keep
183
+ - example/test/helpers/.keep
184
+ - example/test/integration/.keep
185
+ - example/test/models/.keep
186
+ - example/test/test_helper.rb
187
+ - example/tmp/.keep
188
+ - example/tmp/pids/.keep
189
+ - example/vendor/.keep
190
+ - gemfiles/rails_7.1.gemfile
191
+ - gemfiles/rails_7.1.gemfile.lock
192
+ - gemfiles/rails_8.0.gemfile
193
+ - gemfiles/rails_8.0.gemfile.lock
194
+ - gemfiles/rails_8.1.gemfile
195
+ - gemfiles/rails_8.1.gemfile.lock
196
+ - handler-examples/customer_io_handler.rb
197
+ - handler-examples/revolut_business_v1_handler.rb
198
+ - handler-examples/revolut_business_v2_handler.rb
199
+ - handler-examples/starling_payments_handler.rb
200
+ - lib/tasks/webhukhs_tasks.rake
201
+ - lib/webhukhs.rb
202
+ - lib/webhukhs/base_handler.rb
203
+ - lib/webhukhs/controllers/receive_webhooks_controller.rb
204
+ - lib/webhukhs/engine.rb
205
+ - lib/webhukhs/install_generator.rb
206
+ - lib/webhukhs/jobs/processing_job.rb
207
+ - lib/webhukhs/models/received_webhook.rb
208
+ - lib/webhukhs/templates/add_headers_to_webhukhs_webhooks.rb.erb
209
+ - lib/webhukhs/templates/create_webhukhs_tables.rb.erb
210
+ - lib/webhukhs/templates/webhukhs.rb
211
+ - lib/webhukhs/version.rb
212
+ - test/test-webhook-handlers/.DS_Store
213
+ - test/test-webhook-handlers/extract_id_handler.rb
214
+ - test/test-webhook-handlers/failing_with_concealed_errors.rb
215
+ - test/test-webhook-handlers/failing_with_exposed_errors.rb
216
+ - test/test-webhook-handlers/inactive_handler.rb
217
+ - test/test-webhook-handlers/invalid_handler.rb
218
+ - test/test-webhook-handlers/private_handler.rb
219
+ - test/test-webhook-handlers/webhook_test_handler.rb
220
+ - test/test_app.rb
221
+ - test/test_helper.rb
222
+ - test/webhukhs_test.rb
223
+ homepage: https://github.com/skatkov/webhukhs
224
+ licenses:
225
+ - MIT
226
+ metadata:
227
+ homepage_uri: https://github.com/skatkov/webhukhs
228
+ source_code_uri: https://github.com/skatkov/webhukhs
229
+ changelog_uri: https://github.com/skatkov/webhukhs/blob/main/CHANGELOG.md
230
+ rdoc_options: []
231
+ require_paths:
232
+ - lib
233
+ required_ruby_version: !ruby/object:Gem::Requirement
234
+ requirements:
235
+ - - ">="
236
+ - !ruby/object:Gem::Version
237
+ version: '3.0'
238
+ required_rubygems_version: !ruby/object:Gem::Requirement
239
+ requirements:
240
+ - - ">="
241
+ - !ruby/object:Gem::Version
242
+ version: '0'
243
+ requirements: []
244
+ rubygems_version: 4.0.1
245
+ specification_version: 4
246
+ summary: Webhooks processing engine for Rails applications
247
+ test_files: []