Skip to content

Instantly share code, notes, and snippets.

@cintrzyk
Created July 12, 2023 12:13
Show Gist options
  • Save cintrzyk/98988598d99f463f33c7555d9ca0177f to your computer and use it in GitHub Desktop.
Save cintrzyk/98988598d99f463f33c7555d9ca0177f to your computer and use it in GitHub Desktop.
require 'dry/monads'
require 'dry/matcher/result_matcher'
module BaseTransaction
include Dry::Monads[:result, :try, :do]
include Dry::Matcher.for(:call, with: Dry::Matcher::ResultMatcher)
def call
run
end
end
# SAMPLE TRANSACTION
module Admins
module OTP
class EnableTransaction
extend Dry::Initializer
include BaseTransaction
option :admin
option :password
option :otp_attempt
option :headers, default: proc { {} }
def run
yield check_if_otp_enabled
yield check_otp_attempt_presence
yield check_otp_secret_presence
yield verify_password
enable_otp!
Success()
end
private
def check_if_otp_enabled
if admin.otp_enabled?
Failure(:otp_already_enabled)
else
Success()
end
end
def check_otp_attempt_presence
if otp_attempt.present?
Success()
else
Failure(:otp_attempt_empty)
end
end
def check_otp_secret_presence
if admin.otp_secret.present?
Success()
else
Failure(:otp_secret_empty)
end
end
def verify_password
# Fallback to devise for legacy auth
if admin.try(:authenticate, password) || admin.try(:valid_password?, password)
Success()
else
Failure(:invalid_password)
end
end
def enable_otp!
admin.update!(otp_enabled: true)
end
end
end
end
# TRANSACTION SPEC; "fail_with" is a custom matcher
RSpec.describe Admins::OTP::EnableTransaction do
subject(:transaction_call) { transaction.call }
let(:transaction) { described_class.new(**input) }
let(:input) do
{
admin: admin,
password: password,
otp_attempt: otp_attempt,
headers: headers
}
end
let(:admin) { create(:admin, otp_secret: ROTP::Base32.random, password: 'password') }
let(:password) { 'password' }
let(:otp_attempt) { ROTP::TOTP.new(admin.otp_secret).now }
let(:headers) { { 'key' => 'value' } }
around do |example|
freeze_time { example.run }
end
it 'succeeds' do
expect(transaction_call).to be_success
end
it 'enables mfa' do
expect { transaction_call }.to change { admin.reload.otp_enabled }.to(true)
end
context 'when otp is enabled' do
let(:admin) { create(:admin, :with_mfa, password: 'password') }
it 'fails with otp_already_enabled' do
expect(transaction).to fail_with(:otp_already_enabled)
end
end
context 'when otp_attempt is not present' do
let(:otp_attempt) { nil }
it 'fails with otp_attempt_empty' do
expect(transaction).to fail_with(:otp_attempt_empty)
end
end
context 'when otp_secret is not present' do
let(:otp_attempt) { 'asd' }
let(:admin) { create(:admin, password: 'password') }
it 'fails with otp_secret_empty' do
expect(transaction).to fail_with(:otp_secret_empty)
end
end
context 'when password is incorrect' do
let(:password) { 'invalid' }
it 'fails with invalid_password' do
expect(transaction).to fail_with(:invalid_password)
end
end
context 'when otp_attempt is incorrect' do
let(:otp_attempt) { 'invalid' }
it 'fails with invalid_otp_attempt' do
expect(transaction).to fail_with(:invalid_otp_attempt)
end
end
end
# SAMPLE TRANSACTION USAGE IN API
post do
transaction.call do |m|
m.success do
status :no_content
end
m.failure :otp_already_enabled do
handle_error!(error: :otp_already_enabled, code: :bad_request)
end
m.failure :otp_secret_empty do
handle_error!(error: :otp_secret_empty, code: :bad_request)
end
m.failure :invalid_password do
handle_error!(error: :invalid_password, code: :bad_request)
end
m.failure(&method(:error_500!))
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment