Files
prism/test/test_orm_token_for.rb

111 lines
4.0 KiB
Ruby

require 'minitest/autorun'
require 'prism'
require 'tmpdir'
require 'fileutils'
class OrmTokenForTest < Minitest::Test
def setup
@dir = Dir.mktmpdir('prism-token-for')
@store = Prism::Orm::Store.new(File.join(@dir, 'test.pstore'))
@store.create_table(:token_for_users, %w[id email password_digest])
Prism::Orm::TokenFor.primary_secret = 'primary-secret'
Prism::Orm::TokenFor.previous_secrets = []
user_class = Class.new(Prism::Orm::Model) do
table_name :token_for_users
has_secure_password
generates_token_for(:password_reset, expires_in: 900) { password_digest }
generates_token_for(:email_confirmation)
# negative expires_in => already in the past the instant it is minted,
# letting the expiry path be tested without stubbing the clock.
generates_token_for(:already_expiring, expires_in: -1)
end
user_class.store = @store
Object.const_set(:TokenForUser, user_class)
@user = TokenForUser.create!(email: 'foksiu@example.com', password: 'hunter2')
end
def teardown
FileUtils.remove_entry(@dir)
Object.send(:remove_const, :TokenForUser) if Object.const_defined?(:TokenForUser)
Prism::Orm::TokenFor.primary_secret = nil
Prism::Orm::TokenFor.previous_secrets = []
end
def test_token_round_trips_to_the_record
token = @user.generate_token_for(:password_reset)
found = TokenForUser.find_by_token_for(:password_reset, token)
assert_equal @user.id, found&.id
end
def test_token_is_scoped_to_its_own_purpose
token = @user.generate_token_for(:password_reset)
assert_nil TokenForUser.find_by_token_for(:email_confirmation, token)
end
def test_expired_token_is_rejected
token = @user.generate_token_for(:already_expiring)
assert_nil TokenForUser.find_by_token_for(:already_expiring, token)
end
def test_token_invalidated_by_changing_fingerprint_after_generation
token = @user.generate_token_for(:password_reset)
@user.password = 'a completely different passphrase'
@user.save!
assert_nil TokenForUser.find_by_token_for(:password_reset, token)
end
def test_token_without_a_fingerprint_block_is_unaffected_by_record_changes
token = @user.generate_token_for(:email_confirmation)
@user.update!(email: 'someone-else@example.com')
found = TokenForUser.find_by_token_for(:email_confirmation, token)
assert_equal @user.id, found&.id
end
def test_garbage_token_is_rejected
assert_nil TokenForUser.find_by_token_for(:password_reset, 'not-a-real-token')
assert_nil TokenForUser.find_by_token_for(:password_reset, nil)
end
def test_token_for_a_deleted_record_no_longer_resolves
token = @user.generate_token_for(:password_reset)
@store.delete(:token_for_users, @user.id)
assert_nil TokenForUser.find_by_token_for(:password_reset, token)
end
def test_unknown_purpose_raises
assert_raises(Prism::Orm::TokenFor::Error) { @user.generate_token_for(:not_declared) }
assert_raises(Prism::Orm::TokenFor::Error) { TokenForUser.find_by_token_for(:not_declared, 'x') }
end
def test_token_minted_under_a_retired_secret_still_verifies_after_rotation
token = @user.generate_token_for(:email_confirmation)
Prism::Orm::TokenFor.previous_secrets = ['primary-secret']
Prism::Orm::TokenFor.primary_secret = 'rotated-secret'
found = TokenForUser.find_by_token_for(:email_confirmation, token)
assert_equal @user.id, found&.id
end
def test_token_cannot_be_replayed_across_two_unrelated_models
@store.create_table(:token_for_widgets, %w[id name])
widget_class = Class.new(Prism::Orm::Model) do
table_name :token_for_widgets
generates_token_for(:password_reset, expires_in: 900)
end
widget_class.store = @store
Object.const_set(:TokenForWidget, widget_class)
token = @user.generate_token_for(:password_reset)
assert_nil TokenForWidget.find_by_token_for(:password_reset, token)
ensure
Object.send(:remove_const, :TokenForWidget) if Object.const_defined?(:TokenForWidget)
end
end