Skip to content

Commit 47c49a1

Browse files
test: add two-factor test infrastructure
Add test_otp 2FA method that simulates a real extension gem with a simple OTP check. Add UserWithTwoFactor model (ActiveRecord + Mongoid), shared behavior, migration, routes, and challenge view.
1 parent 10cfd01 commit 47c49a1

9 files changed

Lines changed: 94 additions & 2 deletions

File tree

test/models/serializable_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class SerializableTest < ActiveSupport::TestCase
99

1010
test 'should not include unsafe keys on JSON' do
1111
keys = from_json().keys.select{ |key| !key.include?("id") }
12-
assert_equal %w(created_at email facebook_token updated_at username), keys.sort
12+
assert_equal %w(created_at email facebook_token otp_secret updated_at username), keys.sort
1313
end
1414

1515
test 'should not include unsafe keys on JSON even if a new except is provided' do
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# frozen_string_literal: true
2+
3+
require 'shared_user_with_two_factor'
4+
5+
class UserWithTwoFactor < ActiveRecord::Base
6+
self.table_name = 'users'
7+
include Shim
8+
include SharedUserWithTwoFactor
9+
end
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# frozen_string_literal: true
2+
3+
require 'shared_user_with_two_factor'
4+
5+
class UserWithTwoFactor
6+
include Mongoid::Document
7+
include Shim
8+
include SharedUserWithTwoFactor
9+
10+
field :username, type: String
11+
field :email, type: String, default: ""
12+
field :encrypted_password, type: String, default: ""
13+
field :otp_secret, type: String
14+
end
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<h2>Two-Factor Authentication</h2>
2+
3+
<%= form_tag(two_factor_path(resource_name), method: :post) do %>
4+
<%= text_field_tag :otp_attempt %>
5+
<%= submit_tag "Verify" %>
6+
<% end %>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# frozen_string_literal: true
2+
3+
# Test-only two-factor method for integration testing.
4+
# Simulates a real 2FA extension with a simple OTP check.
5+
6+
require 'devise/models/two_factor_authenticatable'
7+
8+
module Devise
9+
module Models
10+
module TestOtp
11+
extend ActiveSupport::Concern
12+
13+
def test_otp_two_factor_enabled?
14+
respond_to?(:otp_secret) && otp_secret.present?
15+
end
16+
end
17+
end
18+
end
19+
20+
module Devise
21+
module Strategies
22+
class TestOtp < Devise::Strategies::TwoFactor
23+
def valid?
24+
super && params[:otp_attempt].present?
25+
end
26+
27+
def verify_two_factor!(resource)
28+
unless resource.respond_to?(:otp_secret) && params[:otp_attempt] == resource.otp_secret
29+
fail!(:invalid_otp)
30+
return
31+
end
32+
end
33+
end
34+
end
35+
end
36+
37+
Warden::Strategies.add(:test_otp, Devise::Strategies::TestOtp)
38+
39+
Devise.register_two_factor_method :test_otp,
40+
model: 'devise/models/test_otp',
41+
strategy: :test_otp

test/rails_app/config/routes.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
# Users scope
2323
devise_for :users, controllers: { omniauth_callbacks: "users/omniauth_callbacks" }
2424

25+
devise_for :user_with_two_factors
26+
2527
devise_for :user_on_main_apps,
2628
class_name: 'UserOnMainApp',
2729
router_name: :main_app,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
class AddOtpSecretToUsers < ActiveRecord::Migration[6.0]
4+
def change
5+
add_column :users, :otp_secret, :string
6+
end
7+
end

test/rails_app/db/schema.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
#
1414
# It's strongly recommended that you check this file into your version control system.
1515

16-
ActiveRecord::Schema.define(version: 20100401102949) do
16+
ActiveRecord::Schema.define(version: 20260303000000) do
1717

1818
create_table "admins", force: true do |t|
1919
t.string "email"
@@ -50,6 +50,7 @@
5050
t.integer "failed_attempts", default: 0
5151
t.string "unlock_token"
5252
t.datetime "locked_at"
53+
t.string "otp_secret"
5354
t.datetime "created_at"
5455
t.datetime "updated_at"
5556
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
module SharedUserWithTwoFactor
4+
extend ActiveSupport::Concern
5+
6+
included do
7+
devise :database_authenticatable, :registerable, :recoverable,
8+
:two_factor_authenticatable, two_factor_methods: [:test_otp]
9+
10+
validates_uniqueness_of :email, allow_blank: true, if: :devise_will_save_change_to_email?
11+
end
12+
end

0 commit comments

Comments
 (0)