Skip to content

Commit

Permalink
Merge pull request #1745 from 18F/margolis-translate-voice-otp
Browse files Browse the repository at this point in the history
Set language in Voice OTP TwiML
  • Loading branch information
jmhooper authored Nov 13, 2017
2 parents ef18654 + bc8ef2b commit 64ebbcc
Show file tree
Hide file tree
Showing 17 changed files with 278 additions and 67 deletions.
1 change: 1 addition & 0 deletions .reek
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ DuplicateMethodCall:
- WorkerHealthChecker#status
- FileEncryptor#encrypt
- UserFlowExporter#self.massage_assets
- BasicAuthUrl#build
FeatureEnvy:
exclude:
- ActiveJob::Logging::LogSubscriber#json_for
Expand Down
3 changes: 3 additions & 0 deletions .slim-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@ linters:
exclude:
- 'app/views/pages/help.html.slim'
- 'app/views/pages/privacy_policy.html.slim'
TagCase:
exclude:
- 'app/views/voice/otp/show.xml.slim'
RuboCop:
enabled: true
56 changes: 56 additions & 0 deletions app/controllers/voice/otp_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
module Voice
class OtpController < ApplicationController
NUMBER_OF_TIMES_USER_CAN_REPEAT_CODE = 5

skip_before_action :verify_authenticity_token

def show
if code.blank?
render nothing: true, status: :bad_request
return
end

@message = message
@action_url = action_url
end

protected

def encrypted_code
params[:encrypted_code].to_s
end

def code
return unless encrypted_code.present?

cipher.decrypt(encrypted_code)
end

def message
t('voice.otp.message', code: code_with_pauses)
end

def code_with_pauses
code.scan(/\d/).join(', ')
end

def repeat_count
(params[:repeat_count].presence || NUMBER_OF_TIMES_USER_CAN_REPEAT_CODE).to_i
end

def action_url
return if repeat_count <= 1

BasicAuthUrl.build(
voice_otp_url(
encrypted_code: encrypted_code,
repeat_count: repeat_count - 1
)
)
end

def cipher
Gibberish::AES.new(Figaro.env.attribute_encryption_key)
end
end
end
45 changes: 11 additions & 34 deletions app/jobs/voice_otp_sender_job.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
class VoiceOtpSenderJob < ApplicationJob
include Rails.application.routes.url_helpers
include LocaleHelper

queue_as :voice

def perform(code:, phone:, otp_created_at:)
Expand All @@ -13,45 +16,19 @@ def otp_valid?(otp_created_at)
end

def send_otp(twilio_service, code, phone)
code_with_pauses = code.scan(/\d/).join(', ')
twilio_service.place_call(
to: phone,
url: twimlet_url(code_with_pauses),
record: Figaro.env.twilio_record_voice == 'true'
)
end

def twimlet_url(code) # rubocop:disable Metrics/MethodLength
repeat = message_repeat(code)

twimlet_menu(
repeat,
1 => twimlet_menu(
repeat,
1 => twimlet_menu(
repeat,
1 => twimlet_menu(repeat, 1 => twimlet_message(message_final(code)))
url: BasicAuthUrl.build(
voice_otp_url(
encrypted_code: cipher.encrypt(code),
locale: locale_url_param
)
)
),
record: Figaro.env.twilio_record_voice == 'true'
)
end

def message_repeat(code)
I18n.t('jobs.voice_otp_sender_job.message_repeat', code: code)
end

def message_final(code)
I18n.t('jobs.voice_otp_sender_job.message_final', code: code)
end

def twimlet_message(message)
'https://twimlets.com/message?' + { Message: { 0 => message } }.to_query
end

def twimlet_menu(message, options)
'https://twimlets.com/menu?' + {
Message: message,
Options: options.to_h,
}.to_query
def cipher
Gibberish::AES.new(Figaro.env.attribute_encryption_key)
end
end
10 changes: 10 additions & 0 deletions app/services/basic_auth_url.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module BasicAuthUrl
module_function

def build(url, user: Figaro.env.basic_auth_user_name, password: Figaro.env.basic_auth_password)
URI.parse(url).tap do |uri|
uri.user = user
uri.password = password
end.to_s
end
end
8 changes: 8 additions & 0 deletions app/views/voice/otp/show.xml.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
doctype xml
Response
Say language="#{I18n.locale}"
= @message
- if @action_url
Gather numDigits="1" action=@action_url
Say = t('voice.otp.repeat_instructions')
Hangup /
6 changes: 6 additions & 0 deletions config/application.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ development:
available_locales: 'en es fr'
aws_kms_key_id: 'alias/login-dot-gov-development-keymaker'
aws_region: 'us-east-1'
basic_auth_user_name: 'user'
basic_auth_password: 'secret'
dashboard_api_token: 'test_token'
dashboard_url: 'http://localhost:3001/api/service_providers'
database_host: ''
Expand Down Expand Up @@ -125,6 +127,8 @@ production:
available_locales: 'en es fr'
aws_kms_key_id:
aws_region:
basic_auth_user_name:
basic_auth_password:
disable_email_sending: 'false'
dashboard_api_token:
domain_name: 'login.gov'
Expand Down Expand Up @@ -186,6 +190,8 @@ test:
available_locales: 'en es fr'
aws_kms_key_id: 'alias/login-dot-gov-test-keymaker'
aws_region: 'us-east-1'
basic_auth_user_name: 'user'
basic_auth_password: 'secret'
domain_name: 'www.example.com'
database_host: ''
database_name: ''
Expand Down
5 changes: 0 additions & 5 deletions config/locales/jobs/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,3 @@ en:
jobs:
sms_otp_sender_job:
message: "%{code} is your %{app} one-time security code."
voice_otp_sender_job:
message_final: Hello! Your login.gov one time security code is, %{code}, again,
your security code is, %{code}, goodbye!
message_repeat: Hello! Your login.gov one time security code is, %{code}, again,
your security code is, %{code}. Press 1 to repeat your code.
6 changes: 0 additions & 6 deletions config/locales/jobs/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,3 @@ es:
jobs:
sms_otp_sender_job:
message: "%{code} es su %{app} código de seguridad de sólo un uso."
voice_otp_sender_job:
message_final: "¡Hola! Su código de seguridad de login.gov para uso único es,
%{code}, nuevamente, su código de seguridad es, % {code}, ¡adiós!"
message_repeat: "¡Hola! Su código de seguridad de login.gov para uso único es
%{code}, nuevamente, su código de seguridad es % {code}. Presione 1 para repetir
su código."
6 changes: 0 additions & 6 deletions config/locales/jobs/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,3 @@ fr:
jobs:
sms_otp_sender_job:
message: "%{code} est votre %{app} code de sécurité à utilisation unique."
voice_otp_sender_job:
message_final: Bonjour! Votre code de sécurité à utilisation unique de login.gov
est, %{code}, de nouveau, votre code de sécurité est, %{code}, au revoir!
message_repeat: Bonjour! Votre code de sécurité à utilisation unique de login.gov
est, %{code}, de nouveau, votre code de sécurité est, %{code}. Appuyez sur 1
pour répéter votre code.
7 changes: 7 additions & 0 deletions config/locales/voice/en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
en:
voice:
otp:
message: Hello! Your login.gov one time passcode is, %{code}, again, your passcode
is, %{code}.
repeat_instructions: Press 1 to repeat this message.
7 changes: 7 additions & 0 deletions config/locales/voice/es.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
es:
voice:
otp:
message: "¡Hola! Su código de acceso de login.gov es, %{code}, nuevamente, su
código de acceso es %{code}."
repeat_instructions: Presione 1 para repetir este mensaje.
7 changes: 7 additions & 0 deletions config/locales/voice/fr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
fr:
voice:
otp:
message: Bonjour! Votre code de sécurité à utilisation unique de login.gov est,
%{code}, de nouveau, votre code de sécurité est, %{code}, au revoir!
repeat_instructions: Appuyez sur 1 pour répéter votre code.
4 changes: 4 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
match '/api/saml/auth' => 'saml_idp#auth', via: %i[get post]

post '/api/service_provider' => 'service_provider#update'
match '/api/voice/otp' => 'voice/otp#show',
via: [:get, :post],
as: :voice_otp,
defaults: { format: :xml }

get '/openid_connect/authorize' => 'openid_connect/authorization#index'
get '/openid_connect/logout' => 'openid_connect/logout#index'
Expand Down
125 changes: 125 additions & 0 deletions spec/controllers/voice/otp_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
require 'rails_helper'

RSpec.describe Voice::OtpController do
describe '#show' do
subject(:action) do
get :show,
params: { encrypted_code: encrypted_code, repeat_count: repeat_count, locale: locale },
format: :xml
end
let(:locale) { nil }
let(:repeat_count) { nil }
let(:cipher) { Gibberish::AES.new(Figaro.env.attribute_encryption_key) }

context 'with a blank encrypted_code in the URL' do
let(:encrypted_code) { '' }

it 'renders a blank 400' do
action

expect(response).to be_bad_request
expect(response.body).to be_empty
end
end

context 'with an encrypted_code in the URL' do
render_views

let(:code) { '1234' }
let(:encrypted_code) { cipher.encrypt(code) }

it 'tells Twilio to <Say> the code with pauses in between' do
action

doc = Nokogiri::XML(response.body)
say = doc.css('Say').first
expect(say.text).to include('1, 2, 3, 4,')
end

it 'sets the lang attribute to english' do
action

doc = Nokogiri::XML(response.body)
say = doc.css('Say').first

expect(say[:language]).to eq('en')
end

context 'when the locale is in spanish' do
let(:locale) { :es }

it 'sets the lang attribute to english' do
action

doc = Nokogiri::XML(response.body)
say = doc.css('Say').first

expect(say[:language]).to eq('es')
end

it 'passes locale into the <Gather> action URL' do
action

doc = Nokogiri::XML(response.body)
gather = doc.css('Gather').first

params = URIService.params(gather[:action])
expect(params[:locale]).to eq('es')
end
end

context 'when the locale is in french' do
let(:locale) { :fr }

it 'sets the lang attribute to english' do
action

doc = Nokogiri::XML(response.body)
say = doc.css('Say').first

expect(say[:language]).to eq('fr')
end

it 'passes locale into the <Gather> action URL' do
action

doc = Nokogiri::XML(response.body)
gather = doc.css('Gather').first

params = URIService.params(gather[:action])
expect(params[:locale]).to eq('fr')
end
end

it 'has a <Gather> with instructions to repeat with a repeat_count' do
action

doc = Nokogiri::XML(response.body)
gather = doc.css('Gather').first

expect(gather[:action]).to include('repeat_count=4')
end

it 'puts the encrypted code in the <Gather> action' do
action

doc = Nokogiri::XML(response.body)
gather = doc.css('Gather').first
params = URIService.params(gather[:action])

expect(cipher.decrypt(params[:encrypted_code])).to eq(code)
end

context 'when repeat_count counts down to 1' do
let(:repeat_count) { 1 }

it 'does not have a <Gather> in the response' do
action

doc = Nokogiri::XML(response.body)
expect(doc.css('Gather')).to be_empty
end
end
end
end
end
Loading

0 comments on commit 64ebbcc

Please sign in to comment.