|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +RSpec.describe TwilioVerifyService, type: :service do |
| 4 | + let(:user) { create :twilio_verify_user } |
| 5 | + let(:phone_number) { user.mobile_phone } |
| 6 | + let(:formatted_phone_number) { "+1#{phone_number}" } |
| 7 | + let(:twilio_account_sid) { '123456789' } |
| 8 | + let(:twilio_auth_token) { '123456789' } |
| 9 | + let(:twilio_verify_service_sid) { '123456789' } |
| 10 | + |
| 11 | + before do |
| 12 | + allow(Rails.application.credentials).to receive(:twilio_account_sid).and_return twilio_account_sid |
| 13 | + allow(Rails.application.credentials).to receive(:twilio_auth_token).and_return twilio_auth_token |
| 14 | + allow(Rails.application.credentials).to receive(:twilio_verify_service_sid).and_return twilio_verify_service_sid |
| 15 | + end |
| 16 | + |
| 17 | + describe 'Missing Twilio credentials' do |
| 18 | + let(:twilio_auth_token) { '' } |
| 19 | + |
| 20 | + it "raises 'Missing Twilio credentials' exception if any credentials are missing" do |
| 21 | + expect { described_class.new }.to raise_error 'Missing Twilio credentials' |
| 22 | + end |
| 23 | + end |
| 24 | + |
| 25 | + # https://www.twilio.com/docs/verify/sms |
| 26 | + describe 'SMS 2FA' do |
| 27 | + let(:twilio_client) { Twilio::REST::Client.new(twilio_account_sid, twilio_auth_token) } |
| 28 | + let(:twilio_client_verify_service) { double('Twilio::REST::Verify::V2::ServiceContext') } |
| 29 | + |
| 30 | + before do |
| 31 | + allow(Twilio::REST::Client).to receive(:new).with(twilio_account_sid, twilio_auth_token).and_return twilio_client |
| 32 | + allow(twilio_client).to receive_message_chain(:verify, :services).with(twilio_verify_service_sid).and_return twilio_client_verify_service |
| 33 | + end |
| 34 | + |
| 35 | + describe '.send_sms_token' do |
| 36 | + it 'calls on the Twilio Verify API to send a one-time code to the given phone number via SMS' do |
| 37 | + expect(twilio_client_verify_service).to receive_message_chain(:verifications, :create).with(to: formatted_phone_number, channel: 'sms') |
| 38 | + described_class.send_sms_token(phone_number) |
| 39 | + end |
| 40 | + end |
| 41 | + |
| 42 | + describe '.verify_sms_token' do |
| 43 | + let(:token) { '123456' } |
| 44 | + |
| 45 | + it 'calls on the Twilio Verify API to verify a one-time code that was previously sent to the given phone number via SMS' do |
| 46 | + expect(twilio_client_verify_service).to receive_message_chain(:verification_checks, :create).with(to: formatted_phone_number, code: token) |
| 47 | + described_class.verify_sms_token(phone_number, token) |
| 48 | + end |
| 49 | + end |
| 50 | + end |
| 51 | + |
| 52 | + # https://www.twilio.com/docs/verify/quickstarts/totp |
| 53 | + describe 'TOTP 2FA' do |
| 54 | + let(:twilio_client) { Twilio::REST::Client.new(twilio_account_sid, twilio_auth_token) } |
| 55 | + let(:twilio_client_verify_service) { double('Twilio::REST::Verify::V2::ServiceContext') } |
| 56 | + let(:twilio_client_entity) { double('Twilio::REST::Verify::V2::ServiceContext::EntityContext') } |
| 57 | + |
| 58 | + before do |
| 59 | + allow(Twilio::REST::Client).to receive(:new).with(twilio_account_sid, twilio_auth_token).and_return twilio_client |
| 60 | + allow(twilio_client).to receive_message_chain(:verify, :v2, :services).with(twilio_verify_service_sid).and_return twilio_client_verify_service |
| 61 | + allow(twilio_client_verify_service).to receive(:entities).with("test-#{user.id}").and_return twilio_client_entity |
| 62 | + end |
| 63 | + |
| 64 | + describe '.setup_totp_service' do |
| 65 | + it 'calls on the Twilio Verify API to setup TOTP for a given user' do |
| 66 | + new_factor = double(:twilio_verify_totp_new_factor, sid: '123ABC') |
| 67 | + expect(twilio_client_entity).to receive_message_chain(:new_factors, :create).with(friendly_name: user.to_s, factor_type: 'totp').and_return new_factor |
| 68 | + |
| 69 | + expect(user.twilio_totp_factor_sid).to be_nil |
| 70 | + result = described_class.setup_totp_service(user) |
| 71 | + |
| 72 | + expect(user.reload.twilio_totp_factor_sid).to eq result.sid |
| 73 | + end |
| 74 | + end |
| 75 | + |
| 76 | + describe '.register_totp_service' do |
| 77 | + let(:token) { '123456' } |
| 78 | + let(:new_factor) { double(:twilio_verify_totp_new_factor, status: 'verified') } |
| 79 | + |
| 80 | + before do |
| 81 | + user.update!(twilio_totp_factor_sid: '123ABC') |
| 82 | + allow(twilio_client_entity).to receive(:factors).with(user.twilio_totp_factor_sid).and_return new_factor |
| 83 | + end |
| 84 | + |
| 85 | + it 'calls on the Twilio Verify API to register TOTP for a given user' do |
| 86 | + expect(new_factor).to receive(:update).with(auth_payload: token).and_return new_factor |
| 87 | + |
| 88 | + result = described_class.register_totp_service(user, token) |
| 89 | + expect(result.status).to eq 'verified' |
| 90 | + end |
| 91 | + |
| 92 | + context 'when an invalid token is provided' do |
| 93 | + let(:new_factor) { double(:twilio_verify_totp_new_factor, status: 'unverified') } |
| 94 | + |
| 95 | + it 'returns an unverified status' do |
| 96 | + expect(new_factor).to receive(:update).with(auth_payload: token).and_return new_factor |
| 97 | + |
| 98 | + result = described_class.register_totp_service(user, token) |
| 99 | + expect(result.status).to eq 'unverified' |
| 100 | + end |
| 101 | + end |
| 102 | + end |
| 103 | + |
| 104 | + describe '.verify_totp_token' do |
| 105 | + let(:token) { '123456' } |
| 106 | + |
| 107 | + before { user.update!(twilio_totp_factor_sid: '123ABC') } |
| 108 | + |
| 109 | + it 'calls on the Twilio Verify API to verify a TOTP based one time code for a given user' do |
| 110 | + expect(twilio_client_entity).to receive_message_chain(:challenges, :create).with(auth_payload: token, factor_sid: user.twilio_totp_factor_sid) |
| 111 | + described_class.verify_totp_token(user, token) |
| 112 | + end |
| 113 | + end |
| 114 | + end |
| 115 | + |
| 116 | + describe '.e164_format' do |
| 117 | + # https://en.wikipedia.org/wiki/E.164 |
| 118 | + it 'formats supplied phone number to the e164 format' do |
| 119 | + expect(described_class.e164_format(phone_number)).to eq formatted_phone_number |
| 120 | + expect(described_class.e164_format('(123) 456-7890')).to eq '+11234567890' |
| 121 | + expect(described_class.e164_format('123-456-7890')).to eq '+11234567890' |
| 122 | + expect(described_class.e164_format('1234567890')).to eq '+11234567890' |
| 123 | + end |
| 124 | + end |
| 125 | +end |
0 commit comments