devise-otp 0.8.0 → 1.1.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +14 -3
  3. data/.gitignore +3 -3
  4. data/Appraisals +22 -0
  5. data/CHANGELOG.md +34 -1
  6. data/Gemfile +3 -1
  7. data/README.md +22 -7
  8. data/Rakefile +0 -11
  9. data/app/assets/stylesheets/devise-otp.css +4 -0
  10. data/app/controllers/devise_otp/devise/otp_credentials_controller.rb +4 -6
  11. data/app/controllers/devise_otp/devise/otp_tokens_controller.rb +1 -2
  12. data/config/locales/en.yml +0 -2
  13. data/devise-otp.gemspec +9 -5
  14. data/gemfiles/rails_7.1.gemfile +21 -0
  15. data/gemfiles/rails_7.2.gemfile +17 -0
  16. data/gemfiles/rails_8.0.gemfile +17 -0
  17. data/lib/devise-otp/version.rb +1 -1
  18. data/lib/devise_otp_authenticatable/controllers/helpers.rb +5 -28
  19. data/lib/devise_otp_authenticatable/hooks/refreshable.rb +3 -1
  20. data/test/dummy/app/assets/javascripts/application.js +0 -1
  21. data/test/dummy/app/assets/stylesheets/application.css +1 -0
  22. data/test/dummy/app/controllers/admin_posts_controller.rb +0 -72
  23. data/test/dummy/app/controllers/non_otp_posts_controller.rb +13 -0
  24. data/test/dummy/app/controllers/posts_controller.rb +8 -2
  25. data/test/dummy/app/models/admin.rb +1 -13
  26. data/test/dummy/app/models/non_otp_user.rb +4 -0
  27. data/test/dummy/app/models/post.rb +1 -1
  28. data/test/dummy/app/models/user.rb +1 -13
  29. data/test/dummy/app/views/admin_posts/index.html.erb +0 -7
  30. data/test/dummy/app/views/layouts/application.html.erb +7 -1
  31. data/test/dummy/app/views/non_otp_posts/index.html.erb +18 -0
  32. data/test/dummy/config/application.rb +0 -7
  33. data/test/dummy/config/database.yml +20 -13
  34. data/test/dummy/config/routes.rb +2 -0
  35. data/test/dummy/db/migrate/20240604000001_create_admins.rb +1 -1
  36. data/test/dummy/db/migrate/20250718092451_create_non_otp_users.rb +9 -0
  37. data/test/dummy/db/migrate/20250718092536_add_devise_to_non_otp_users.rb +52 -0
  38. data/test/dummy/db/schema.rb +118 -0
  39. data/test/dummy/db/seeds.rb +24 -0
  40. data/test/integration/disable_token_test.rb +3 -0
  41. data/test/integration/enable_otp_form_test.rb +17 -0
  42. data/test/integration/non_otp_user_models_test.rb +21 -0
  43. data/test/integration/persistence_test.rb +3 -0
  44. data/test/integration/refresh_test.rb +9 -0
  45. data/test/integration/reset_token_test.rb +3 -0
  46. data/test/integration/sign_in_test.rb +30 -0
  47. data/test/integration_tests_helper.rb +11 -0
  48. data/test/test_helper.rb +0 -5
  49. metadata +39 -23
  50. data/app/assets/javascripts/devise-otp.js +0 -1
  51. data/app/assets/javascripts/qrcode.js +0 -609
  52. data/docs/QR_CODES.md +0 -9
  53. data/test/dummy/app/views/admin_posts/_form.html.erb +0 -25
  54. data/test/dummy/app/views/admin_posts/edit.html.erb +0 -6
  55. data/test/dummy/app/views/admin_posts/new.html.erb +0 -5
  56. data/test/dummy/app/views/admin_posts/show.html.erb +0 -15
  57. data/test/orm/active_record.rb +0 -11
@@ -8,7 +8,13 @@
8
8
  </head>
9
9
  <body>
10
10
 
11
- <%= yield %>
11
+ <div id="alerts">
12
+ <% flash.keys.each do |key| %>
13
+ <%= content_tag :p, flash[key], :id => key %>
14
+ <% end %>
15
+ </div>
16
+
17
+ <%= yield %>
12
18
 
13
19
  </body>
14
20
  </html>
@@ -0,0 +1,18 @@
1
+ <h1>Listing posts</h1>
2
+
3
+ <table>
4
+ <tr>
5
+ <th>Title</th>
6
+ <th>Body</th>
7
+ <th></th>
8
+ <th></th>
9
+ <th></th>
10
+ </tr>
11
+
12
+ <% @posts.each do |post| %>
13
+ <tr>
14
+ <td><%= post.title %></td>
15
+ <td><%= post.body %></td>
16
+ </tr>
17
+ <% end %>
18
+ </table>
@@ -9,13 +9,6 @@ require "sprockets/railtie"
9
9
  # require "rails/test_unit/railtie"
10
10
 
11
11
  Bundler.require
12
- Bundler.require(:default, DEVISE_ORM) if defined?(Bundler)
13
-
14
- begin
15
- require "#{DEVISE_ORM}/railtie"
16
- rescue LoadError
17
- end
18
- PARENT_MODEL_CLASS = (DEVISE_ORM == :active_record) ? ActiveRecord::Base : Object
19
12
 
20
13
  require "devise"
21
14
  require "devise-otp"
@@ -1,25 +1,32 @@
1
- # SQLite version 3.x
1
+ # SQLite. Versions 3.8.0 and up are supported.
2
2
  # gem install sqlite3
3
3
  #
4
4
  # Ensure the SQLite 3 gem is defined in your Gemfile
5
- # gem 'sqlite3'
6
- development:
5
+ # gem "sqlite3"
6
+ #
7
+ default: &default
7
8
  adapter: sqlite3
8
- database: ":memory:"
9
- pool: 5
9
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
10
10
  timeout: 5000
11
11
 
12
+ development:
13
+ <<: *default
14
+ database: storage/development.sqlite3
15
+
12
16
  # Warning: The database defined as "test" will be erased and
13
17
  # re-generated from your development database when you run "rake".
14
18
  # Do not set this db to the same as development or production.
15
19
  test:
16
- adapter: sqlite3
17
- database: db/test.sqlite3
18
- pool: 5
19
- timeout: 5000
20
+ <<: *default
21
+ database: storage/test.sqlite3
22
+
20
23
 
24
+ # SQLite3 write its data on the local filesystem, as such it requires
25
+ # persistent disks. If you are deploying to a managed service, you should
26
+ # make sure it provides disk persistence, as many don't.
27
+ #
28
+ # Similarly, if you deploy your application as a Docker container, you must
29
+ # ensure the database is located in a persisted volume.
21
30
  production:
22
- adapter: sqlite3
23
- database: db/production.sqlite3
24
- pool: 5
25
- timeout: 5000
31
+ <<: *default
32
+ # database: path/to/persistent/storage/production.sqlite3
@@ -1,9 +1,11 @@
1
1
  Dummy::Application.routes.draw do
2
2
  devise_for :admins
3
3
  devise_for :users
4
+ devise_for :non_otp_users
4
5
 
5
6
  resources :posts
6
7
  resources :admin_posts
8
+ resources :non_otp_posts
7
9
 
8
10
  root to: "base#home"
9
11
  end
@@ -1,4 +1,4 @@
1
- class CreateAdmins < ActiveRecord::Migration[7.1]
1
+ class CreateAdmins < ActiveRecord::Migration[5.0]
2
2
  def change
3
3
  create_table :admins do |t|
4
4
  t.string :name
@@ -0,0 +1,9 @@
1
+ class CreateNonOtpUsers < ActiveRecord::Migration[5.0]
2
+ def change
3
+ create_table :non_otp_users do |t|
4
+ t.string :name
5
+
6
+ t.timestamps
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,52 @@
1
+ class AddDeviseToNonOtpUsers < ActiveRecord::Migration[5.0]
2
+ def self.up
3
+ change_table(:non_otp_users) do |t|
4
+ ## Database authenticatable
5
+ t.string :email, null: false, default: ""
6
+ t.string :encrypted_password, null: false, default: ""
7
+
8
+ ## Recoverable
9
+ t.string :reset_password_token
10
+ t.datetime :reset_password_sent_at
11
+
12
+ ## Rememberable
13
+ t.datetime :remember_created_at
14
+
15
+ ## Trackable
16
+ t.integer :sign_in_count, default: 0
17
+ t.datetime :current_sign_in_at
18
+ t.datetime :last_sign_in_at
19
+ t.string :current_sign_in_ip
20
+ t.string :last_sign_in_ip
21
+
22
+ ## Confirmable
23
+ # t.string :confirmation_token
24
+ # t.datetime :confirmed_at
25
+ # t.datetime :confirmation_sent_at
26
+ # t.string :unconfirmed_email # Only if using reconfirmable
27
+
28
+ ## Lockable
29
+ t.integer :failed_attempts, default: 0 # Only if lock strategy is :failed_attempts
30
+ t.string :unlock_token # Only if unlock strategy is :email or :both
31
+ t.datetime :locked_at
32
+
33
+ ## Token authenticatable
34
+ t.string :authentication_token
35
+
36
+ # Uncomment below if timestamps were not included in your original model.
37
+ # t.timestamps
38
+ end
39
+
40
+ add_index :non_otp_users, :email, unique: true
41
+ add_index :non_otp_users, :reset_password_token, unique: true
42
+ # add_index :non_otp_users, :confirmation_token, :unique => true
43
+ add_index :non_otp_users, :unlock_token, unique: true
44
+ add_index :non_otp_users, :authentication_token, unique: true
45
+ end
46
+
47
+ def self.down
48
+ # By default, we don't want to make any assumption about how to roll back a migration when your
49
+ # model already existed. Please edit below which fields you would like to remove in this migration.
50
+ raise ActiveRecord::IrreversibleMigration
51
+ end
52
+ end
@@ -0,0 +1,118 @@
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[8.0].define(version: 2025_07_18_092536) do
14
+ create_table "admins", force: :cascade do |t|
15
+ t.string "name"
16
+ t.datetime "created_at", precision: nil, null: false
17
+ t.datetime "updated_at", precision: nil, null: false
18
+ t.string "email", default: "", null: false
19
+ t.string "encrypted_password", default: "", null: false
20
+ t.string "reset_password_token"
21
+ t.datetime "reset_password_sent_at", precision: nil
22
+ t.datetime "remember_created_at", precision: nil
23
+ t.integer "sign_in_count", default: 0
24
+ t.datetime "current_sign_in_at", precision: nil
25
+ t.datetime "last_sign_in_at", precision: nil
26
+ t.string "current_sign_in_ip"
27
+ t.string "last_sign_in_ip"
28
+ t.integer "failed_attempts", default: 0
29
+ t.string "unlock_token"
30
+ t.datetime "locked_at", precision: nil
31
+ t.string "authentication_token"
32
+ t.string "otp_auth_secret"
33
+ t.string "otp_recovery_secret"
34
+ t.boolean "otp_enabled", default: false, null: false
35
+ t.boolean "otp_mandatory", default: false, null: false
36
+ t.datetime "otp_enabled_on", precision: nil
37
+ t.integer "otp_time_drift", default: 0, null: false
38
+ t.integer "otp_failed_attempts", default: 0, null: false
39
+ t.integer "otp_recovery_counter", default: 0, null: false
40
+ t.string "otp_persistence_seed"
41
+ t.string "otp_session_challenge"
42
+ t.datetime "otp_challenge_expires", precision: nil
43
+ t.index ["authentication_token"], name: "index_admins_on_authentication_token", unique: true
44
+ t.index ["email"], name: "index_admins_on_email", unique: true
45
+ t.index ["otp_challenge_expires"], name: "index_admins_on_otp_challenge_expires"
46
+ t.index ["otp_session_challenge"], name: "index_admins_on_otp_session_challenge", unique: true
47
+ t.index ["reset_password_token"], name: "index_admins_on_reset_password_token", unique: true
48
+ t.index ["unlock_token"], name: "index_admins_on_unlock_token", unique: true
49
+ end
50
+
51
+ create_table "non_otp_users", force: :cascade do |t|
52
+ t.string "name"
53
+ t.datetime "created_at", null: false
54
+ t.datetime "updated_at", null: false
55
+ t.string "email", default: "", null: false
56
+ t.string "encrypted_password", default: "", null: false
57
+ t.string "reset_password_token"
58
+ t.datetime "reset_password_sent_at"
59
+ t.datetime "remember_created_at"
60
+ t.integer "sign_in_count", default: 0
61
+ t.datetime "current_sign_in_at"
62
+ t.datetime "last_sign_in_at"
63
+ t.string "current_sign_in_ip"
64
+ t.string "last_sign_in_ip"
65
+ t.integer "failed_attempts", default: 0
66
+ t.string "unlock_token"
67
+ t.datetime "locked_at"
68
+ t.string "authentication_token"
69
+ t.index ["authentication_token"], name: "index_non_otp_users_on_authentication_token", unique: true
70
+ t.index ["email"], name: "index_non_otp_users_on_email", unique: true
71
+ t.index ["reset_password_token"], name: "index_non_otp_users_on_reset_password_token", unique: true
72
+ t.index ["unlock_token"], name: "index_non_otp_users_on_unlock_token", unique: true
73
+ end
74
+
75
+ create_table "posts", force: :cascade do |t|
76
+ t.string "title"
77
+ t.text "body"
78
+ t.datetime "created_at", precision: nil, null: false
79
+ t.datetime "updated_at", precision: nil, null: false
80
+ end
81
+
82
+ create_table "users", force: :cascade do |t|
83
+ t.string "name"
84
+ t.datetime "created_at", precision: nil, null: false
85
+ t.datetime "updated_at", precision: nil, null: false
86
+ t.string "email", default: "", null: false
87
+ t.string "encrypted_password", default: "", null: false
88
+ t.string "reset_password_token"
89
+ t.datetime "reset_password_sent_at", precision: nil
90
+ t.datetime "remember_created_at", precision: nil
91
+ t.integer "sign_in_count", default: 0
92
+ t.datetime "current_sign_in_at", precision: nil
93
+ t.datetime "last_sign_in_at", precision: nil
94
+ t.string "current_sign_in_ip"
95
+ t.string "last_sign_in_ip"
96
+ t.integer "failed_attempts", default: 0
97
+ t.string "unlock_token"
98
+ t.datetime "locked_at", precision: nil
99
+ t.string "authentication_token"
100
+ t.string "otp_auth_secret"
101
+ t.string "otp_recovery_secret"
102
+ t.boolean "otp_enabled", default: false, null: false
103
+ t.boolean "otp_mandatory", default: false, null: false
104
+ t.datetime "otp_enabled_on", precision: nil
105
+ t.integer "otp_time_drift", default: 0, null: false
106
+ t.integer "otp_failed_attempts", default: 0, null: false
107
+ t.integer "otp_recovery_counter", default: 0, null: false
108
+ t.string "otp_persistence_seed"
109
+ t.string "otp_session_challenge"
110
+ t.datetime "otp_challenge_expires", precision: nil
111
+ t.index ["authentication_token"], name: "index_users_on_authentication_token", unique: true
112
+ t.index ["email"], name: "index_users_on_email", unique: true
113
+ t.index ["otp_challenge_expires"], name: "index_users_on_otp_challenge_expires"
114
+ t.index ["otp_session_challenge"], name: "index_users_on_otp_session_challenge", unique: true
115
+ t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
116
+ t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true
117
+ end
118
+ end
@@ -0,0 +1,24 @@
1
+ Admin.create(
2
+ name: "Test Admin",
3
+ email: "admin@devise-otp.local",
4
+ password: "Pass1234"
5
+ )
6
+
7
+ User.create(
8
+ name: "Test User",
9
+ email: "user@devise-otp.local",
10
+ password: "Pass1234"
11
+ )
12
+
13
+ NonOtpUser.create(
14
+ name: "Non OTP User",
15
+ email: "non-otp-user@devise-otp.local",
16
+ password: "Pass1234"
17
+ )
18
+
19
+ 5.times do |n|
20
+ Post.create(
21
+ title: "Post #{n + 1}",
22
+ body: "This is post #{n + 1}."
23
+ )
24
+ end
@@ -23,6 +23,9 @@ class DisableTokenTest < ActionDispatch::IntegrationTest
23
23
  disable_otp
24
24
 
25
25
  assert page.has_content? "Disabled"
26
+ within "#alerts" do
27
+ assert page.has_content? 'Two-Factor Authentication has been disabled.'
28
+ end
26
29
 
27
30
  # logout
28
31
  sign_out
@@ -20,6 +20,10 @@ class EnableOtpFormTest < ActionDispatch::IntegrationTest
20
20
  assert_equal user_otp_token_path, current_path
21
21
  assert page.has_content?("Enabled")
22
22
 
23
+ within "#alerts" do
24
+ assert page.has_content? 'Your Two-Factor Authentication settings have been updated.'
25
+ end
26
+
23
27
  user.reload
24
28
  assert user.otp_enabled?
25
29
  end
@@ -37,6 +41,15 @@ class EnableOtpFormTest < ActionDispatch::IntegrationTest
37
41
 
38
42
  user.reload
39
43
  assert_not user.otp_enabled?
44
+
45
+ within "#alerts" do
46
+ assert page.has_content? 'The Confirmation Code you entered did not match the QR code shown below.'
47
+ end
48
+
49
+ visit "/"
50
+ within "#alerts" do
51
+ assert !page.has_content?('The Confirmation Code you entered did not match the QR code shown below.')
52
+ end
40
53
  end
41
54
 
42
55
  test "a user should not be able enable their OTP authentication with a blank confirmation code" do
@@ -50,6 +63,10 @@ class EnableOtpFormTest < ActionDispatch::IntegrationTest
50
63
 
51
64
  assert page.has_content?("To Enable Two-Factor Authentication")
52
65
 
66
+ within "#alerts" do
67
+ assert page.has_content? 'The Confirmation Code you entered did not match the QR code shown below.'
68
+ end
69
+
53
70
  user.reload
54
71
  assert_not user.otp_enabled?
55
72
  end
@@ -0,0 +1,21 @@
1
+ require "test_helper"
2
+ require "integration_tests_helper"
3
+
4
+ class NonOtpUserModelsTest < ActionDispatch::IntegrationTest
5
+
6
+ def teardown
7
+ Capybara.reset_sessions!
8
+ end
9
+
10
+ test "a non-OTP user should be able to sign in without error" do
11
+ create_non_otp_user
12
+
13
+ visit non_otp_posts_path
14
+ fill_in "non_otp_user_email", with: "non-otp-user@email.invalid"
15
+ fill_in "non_otp_user_password", with: "12345678"
16
+ page.has_content?("Log in") ? click_button("Log in") : click_button("Sign in")
17
+
18
+ assert_equal non_otp_posts_path, current_path
19
+ end
20
+
21
+ end
@@ -36,6 +36,9 @@ class PersistenceTest < ActionDispatch::IntegrationTest
36
36
 
37
37
  click_link("Trust this browser")
38
38
  assert_text "Your browser is trusted."
39
+ within "#alerts" do
40
+ assert page.has_content? 'Your device is now trusted.'
41
+ end
39
42
  sign_out
40
43
 
41
44
  sign_user_in
@@ -60,6 +60,15 @@ class RefreshTest < ActionDispatch::IntegrationTest
60
60
  fill_in "user_refresh_password", with: "12345670"
61
61
  click_button "Continue..."
62
62
  assert_equal refresh_user_otp_credential_path, current_path
63
+
64
+ within "#alerts" do
65
+ assert page.has_content? 'Sorry, you provided the wrong credentials.'
66
+ end
67
+
68
+ visit "/"
69
+ within "#alerts" do
70
+ assert !page.has_content?('Sorry, you provided the wrong credentials.')
71
+ end
63
72
  end
64
73
 
65
74
  test "user should be finally be able to access their settings, and just password is enough" do
@@ -23,6 +23,9 @@ class ResetTokenTest < ActionDispatch::IntegrationTest
23
23
  reset_otp
24
24
 
25
25
  assert_equal "/users/otp/token/edit", current_path
26
+ within "#alerts" do
27
+ assert page.has_content? 'Your token secret has been reset. Please confirm your new token secret below.'
28
+ end
26
29
  end
27
30
 
28
31
  test "generates new token secrets" do
@@ -43,6 +43,7 @@ class SignInTest < ActionDispatch::IntegrationTest
43
43
  click_button "Submit Token"
44
44
 
45
45
  assert_equal user_otp_credential_path, current_path
46
+ assert page.has_content? "The token you provided was invalid."
46
47
  end
47
48
 
48
49
  test "fail blank token authentication" do
@@ -53,6 +54,7 @@ class SignInTest < ActionDispatch::IntegrationTest
53
54
  click_button "Submit Token"
54
55
 
55
56
  assert_equal user_otp_credential_path, current_path
57
+ assert page.has_content? "You need to type in the token you generated with your device."
56
58
  end
57
59
 
58
60
  test "successful token authentication" do
@@ -78,4 +80,32 @@ class SignInTest < ActionDispatch::IntegrationTest
78
80
  User.otp_authentication_timeout = old_timeout
79
81
  assert_equal new_user_session_path, current_path
80
82
  end
83
+
84
+ test "blank token flash message does not persist to successful authentication redirect." do
85
+ user = enable_otp_and_sign_in
86
+
87
+ fill_in "token", with: "123456"
88
+ click_button "Submit Token"
89
+
90
+ assert page.has_content?("The token you provided was invalid.")
91
+
92
+ fill_in "token", with: ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
93
+ click_button "Submit Token"
94
+
95
+ assert !page.has_content?("The token you provided was invalid.")
96
+ end
97
+
98
+ test "invalid token flash message does not persist to successful authentication redirect." do
99
+ user = enable_otp_and_sign_in
100
+
101
+ fill_in "token", with: ""
102
+ click_button "Submit Token"
103
+
104
+ assert page.has_content?("You need to type in the token you generated with your device.")
105
+
106
+ fill_in "token", with: ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
107
+ click_button "Submit Token"
108
+
109
+ assert !page.has_content?("You need to type in the token you generated with your device.")
110
+ end
81
111
  end
@@ -27,6 +27,17 @@ class ActionDispatch::IntegrationTest
27
27
  end
28
28
  end
29
29
 
30
+ def create_non_otp_user
31
+ @non_otp_user ||= begin
32
+ non_otp_user = NonOtpUser.create!(
33
+ email: "non-otp-user@email.invalid",
34
+ password: "12345678",
35
+ password_confirmation: "12345678"
36
+ )
37
+ non_otp_user
38
+ end
39
+ end
40
+
30
41
  def enable_otp_and_sign_in_with_otp
31
42
  enable_otp_and_sign_in.tap do |user|
32
43
  fill_in "token", with: ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
data/test/test_helper.rb CHANGED
@@ -1,17 +1,12 @@
1
1
  ENV["RAILS_ENV"] = "test"
2
- DEVISE_ORM = (ENV["DEVISE_ORM"] || :active_record).to_sym
3
2
 
4
- puts "\n==> Devise.orm = #{DEVISE_ORM.inspect}"
5
3
  require "dummy/config/environment"
6
- require "orm/#{DEVISE_ORM}"
7
4
  require "rails/test_help"
8
5
  require "capybara/rails"
9
6
  require "minitest/reporters"
10
7
 
11
8
  Minitest::Reporters.use!
12
9
 
13
- # I18n.load_path << File.expand_path("../support/locale/en.yml", __FILE__) if DEVISE_ORM == :mongoid
14
-
15
10
  # ActiveSupport::Deprecation.silenced = true
16
11
 
17
12
  class ActionDispatch::IntegrationTest