From b8f33c414a16f6ab9080c163161edb37fe1ca6e8 Mon Sep 17 00:00:00 2001 From: Adam Biagianti Date: Tue, 27 Jun 2017 16:13:07 -0400 Subject: [PATCH 01/22] Renames account recovery to account reactivation **Why**: The feature in question is account reactivation, this improves naming consistency --- ...ern.rb => account_reactivation_concern.rb} | 2 +- .../reactivate_account_controller.rb | 2 +- .../users/verify_password_controller.rb | 2 +- .../users/verify_personal_key_controller.rb | 8 +- app/controllers/verify_controller.rb | 2 +- config/locales/notices/en.yml | 2 +- config/locales/notices/es.yml | 2 +- .../users/verify_password_controller_spec.rb | 89 ++++++++++--------- .../verify_personal_key_controller_spec.rb | 2 +- 9 files changed, 56 insertions(+), 55 deletions(-) rename app/controllers/concerns/{account_recovery_concern.rb => account_reactivation_concern.rb} (90%) diff --git a/app/controllers/concerns/account_recovery_concern.rb b/app/controllers/concerns/account_reactivation_concern.rb similarity index 90% rename from app/controllers/concerns/account_recovery_concern.rb rename to app/controllers/concerns/account_reactivation_concern.rb index 46a36b11026..4c0140fcbcf 100644 --- a/app/controllers/concerns/account_recovery_concern.rb +++ b/app/controllers/concerns/account_reactivation_concern.rb @@ -1,4 +1,4 @@ -module AccountRecoveryConcern +module AccountReactivationConcern extend ActiveSupport::Concern def confirm_password_reset_profile diff --git a/app/controllers/reactivate_account_controller.rb b/app/controllers/reactivate_account_controller.rb index a9eff6cc5e4..dbb0eccba49 100644 --- a/app/controllers/reactivate_account_controller.rb +++ b/app/controllers/reactivate_account_controller.rb @@ -1,5 +1,5 @@ class ReactivateAccountController < ApplicationController - include AccountRecoveryConcern + include AccountReactivationConcern before_action :confirm_two_factor_authenticated before_action :confirm_password_reset_profile diff --git a/app/controllers/users/verify_password_controller.rb b/app/controllers/users/verify_password_controller.rb index 67bea6d230f..2dc268cb5e4 100644 --- a/app/controllers/users/verify_password_controller.rb +++ b/app/controllers/users/verify_password_controller.rb @@ -1,6 +1,6 @@ module Users class VerifyPasswordController < ApplicationController - include AccountRecoveryConcern + include AccountReactivationConcern before_action :confirm_two_factor_authenticated before_action :confirm_password_reset_profile diff --git a/app/controllers/users/verify_personal_key_controller.rb b/app/controllers/users/verify_personal_key_controller.rb index 6316fc9441b..0e7fbe39643 100644 --- a/app/controllers/users/verify_personal_key_controller.rb +++ b/app/controllers/users/verify_personal_key_controller.rb @@ -1,10 +1,10 @@ module Users class VerifyPersonalKeyController < ApplicationController - include AccountRecoveryConcern + include AccountReactivationConcern before_action :confirm_two_factor_authenticated before_action :confirm_password_reset_profile - before_action :init_account_recovery, only: [:new] + before_action :init_account_reactivation, only: [:new] def new @personal_key_form = VerifyPersonalKeyForm.new( @@ -25,10 +25,10 @@ def create private - def init_account_recovery + def init_account_reactivation return if reactivate_account_session.started? - flash.now[:notice] = t('notices.account_recovery') + flash.now[:notice] = t('notices.account_reactivation') reactivate_account_session.start end diff --git a/app/controllers/verify_controller.rb b/app/controllers/verify_controller.rb index 1f62949184a..b4ac0e88992 100644 --- a/app/controllers/verify_controller.rb +++ b/app/controllers/verify_controller.rb @@ -1,6 +1,6 @@ class VerifyController < ApplicationController include IdvSession - include AccountRecoveryConcern + include AccountReactivationConcern before_action :confirm_two_factor_authenticated before_action :confirm_idv_needed, only: %i[cancel fail] diff --git a/config/locales/notices/en.yml b/config/locales/notices/en.yml index 98188b14bbd..89edc2ba0a8 100644 --- a/config/locales/notices/en.yml +++ b/config/locales/notices/en.yml @@ -1,7 +1,7 @@ --- en: notices: - account_recovery: Great! You have your personal key. + account_reactivation: Great! You have your personal key. dap_html: > diff --git a/config/locales/notices/es.yml b/config/locales/notices/es.yml index 46c9667b8f6..d9e1d0713e6 100644 --- a/config/locales/notices/es.yml +++ b/config/locales/notices/es.yml @@ -1,7 +1,7 @@ --- es: notices: - account_recovery: NOT TRANSLATED YET + account_reactivation: NOT TRANSLATED YET dap_html: NOT TRANSLATED YET forgot_password: use_diff_email: diff --git a/spec/controllers/users/verify_password_controller_spec.rb b/spec/controllers/users/verify_password_controller_spec.rb index 540ce2967a7..fba5173e971 100644 --- a/spec/controllers/users/verify_password_controller_spec.rb +++ b/spec/controllers/users/verify_password_controller_spec.rb @@ -20,71 +20,72 @@ end end - context 'without personal key flag set' do - let(:profiles) { [create(:profile, deactivation_reason: :password_reset)] } - - describe '#new' do - it 'redirects to the root url' do - get :new - expect(response).to redirect_to(root_url) - end - end - - describe '#update' do - it 'redirects to the root url' do - get :new - expect(response).to redirect_to(root_url) - end - end - end - context 'with password reset profile' do let(:profiles) { [create(:profile, deactivation_reason: :password_reset)] } let(:response_ok) { FormResponse.new(success: true, errors: {}, extra: { personal_key: key }) } let(:response_bad) { FormResponse.new(success: false, errors: {}) } let(:key) { 'key' } - before do - allow(subject.reactivate_account_session).to receive(:personal_key?).and_return(personal_key) - end - - describe '#new' do - it 'renders the `new` template' do - get :new + context 'without personal key flag set' do + describe '#new' do + it 'redirects to the root url' do + get :new + expect(response).to redirect_to(root_url) + end + end - expect(response).to render_template(:new) + describe '#update' do + it 'redirects to the root url' do + get :new + expect(response).to redirect_to(root_url) + end end end - describe '#update' do - let(:form) { instance_double(VerifyPasswordForm) } - + context 'with personal key flag set' do before do - expect(controller).to receive(:verify_password_form).and_return(form) + allow(subject.reactivate_account_session).to receive(:personal_key?). + and_return(personal_key) end - context 'with valid password' do - before do - allow(form).to receive(:submit).and_return(response_ok) - put :update, user: { password: user.password } + describe '#new' do + it 'renders the `new` template' do + get :new + + expect(response).to render_template(:new) end + end - it 'redirects to the account page' do - expect(response).to redirect_to(account_url) + describe '#update' do + let(:form) { instance_double(VerifyPasswordForm) } + + before do + expect(controller).to receive(:verify_password_form).and_return(form) end - it 'sets a new personal key as a flash message' do - expect(flash[:personal_key]).to eq(key) + context 'with valid password' do + before do + allow(form).to receive(:submit).and_return(response_ok) + put :update, user: { password: user.password } + end + + it 'redirects to the account page' do + expect(response).to redirect_to(account_url) + end + + it 'sets a new personal key as a flash message' do + expect(flash[:personal_key]).to eq(key) + end end - end - context 'without valid password' do - it 'renders the new template' do - allow(form).to receive(:submit).and_return(response_bad) + context 'without valid password' do + it 'renders the new template' do + allow(form).to receive(:submit).and_return(response_bad) - put :update, user: { password: user.password } + put :update, user: { password: user.password } - expect(response).to render_template(:new) + expect(response).to render_template(:new) + end end end end diff --git a/spec/controllers/users/verify_personal_key_controller_spec.rb b/spec/controllers/users/verify_personal_key_controller_spec.rb index 0e16e56d7ca..d8d7698bfa3 100644 --- a/spec/controllers/users/verify_personal_key_controller_spec.rb +++ b/spec/controllers/users/verify_personal_key_controller_spec.rb @@ -33,7 +33,7 @@ it 'displays a flash message to the user' do get :new - expect(subject.flash[:notice]).to eq(t('notices.account_recovery')) + expect(subject.flash[:notice]).to eq(t('notices.account_reactivation')) end end end From 2e3b85227e47124e0f0b8c72b9a79ff078573e54 Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Wed, 28 Jun 2017 13:03:00 -0400 Subject: [PATCH 02/22] Return to branded page consistently when canceling **Why**: Consistency is key. In this case, the cancel link on the forgot password page was always linking to the home page. **How**: Don't use the `shared/cancel` partial because the user will never be signing up or going through IdV if they are accessing the Forgot Password page, so we can directly link to the decorated session's `cancel_link_path` which contains the logic to determine whether it should point to the home page or the branded page. --- app/views/devise/passwords/new.html.slim | 4 +++- .../devise/passwords/new.html.slim_spec.rb | 20 +++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/app/views/devise/passwords/new.html.slim b/app/views/devise/passwords/new.html.slim index 345c3eab860..52b74d9aaee 100644 --- a/app/views/devise/passwords/new.html.slim +++ b/app/views/devise/passwords/new.html.slim @@ -10,4 +10,6 @@ p.mt-tiny.mb0#email-description = f.input :email, required: true, input_html: { 'aria-describedby': 'email-description' } = f.button :submit, t('forms.buttons.continue'), class: 'mt2' -= render 'shared/cancel', link: new_user_session_path +.mt2.pt1.border-top + = link_to t('links.cancel'), decorated_session.cancel_link_path, class: 'h5' + diff --git a/spec/views/devise/passwords/new.html.slim_spec.rb b/spec/views/devise/passwords/new.html.slim_spec.rb index 2cf85198c02..ad5d316f3f5 100644 --- a/spec/views/devise/passwords/new.html.slim_spec.rb +++ b/spec/views/devise/passwords/new.html.slim_spec.rb @@ -1,12 +1,18 @@ require 'rails_helper' describe 'devise/passwords/new.html.slim' do - let(:user) { build_stubbed(:user) } - before do @password_reset_email_form = PasswordResetEmailForm.new('') - - allow(view).to receive(:current_user).and_return(user) + sp = build_stubbed( + :service_provider, + friendly_name: 'Awesome Application!', + return_to_sp_url: 'www.awesomeness.com' + ) + view_context = ActionController::Base.new.view_context + @decorated_session = DecoratedSession.new( + sp: sp, view_context: view_context, sp_session: {} + ).call + allow(view).to receive(:decorated_session).and_return(@decorated_session) end it 'has a localized title' do @@ -26,4 +32,10 @@ expect(rendered).to have_xpath("//form[@autocomplete='off']") end + + it 'has a cancel link that points to the decorated_session cancel_link_path' do + render + + expect(rendered).to have_link(t('links.cancel'), href: @decorated_session.cancel_link_path) + end end From 7b46cb0bd7cf853f38964941aa4357d04383853b Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Wed, 28 Jun 2017 13:26:58 -0400 Subject: [PATCH 03/22] Check href when testing links **Why**: Testing for the link text alone is not enough. We also want to make sure the link is pointing to the right place. --- spec/views/verify/review/new.html.slim_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/views/verify/review/new.html.slim_spec.rb b/spec/views/verify/review/new.html.slim_spec.rb index 258c9d201da..3ffe21b10a1 100644 --- a/spec/views/verify/review/new.html.slim_spec.rb +++ b/spec/views/verify/review/new.html.slim_spec.rb @@ -48,7 +48,8 @@ end it 'renders a link telling user why financial info is not visible' do - expect(rendered).to have_link t('idv.messages.review.financial_info') + expect(rendered). + to have_link(t('idv.messages.review.financial_info'), href: MarketingSite.help_url) end end end From aef8a02fa03c0afbb8fcda8020e7d8ccb5d6e085 Mon Sep 17 00:00:00 2001 From: Zach Margolis Date: Wed, 28 Jun 2017 16:25:41 -0400 Subject: [PATCH 04/22] Remove idv_session from VendorValidator **Why**: Removing references to the session will make it easier to extract VendorValidator work into a background job --- app/services/idv/financials_validator.rb | 6 +---- app/services/idv/phone_validator.rb | 6 +---- app/services/idv/step.rb | 10 ++++++-- app/services/idv/vendor_validator.rb | 16 ++++++------- .../services/idv/financials_validator_spec.rb | 23 +++++++++++-------- spec/services/idv/phone_validator_spec.rb | 23 +++++++++++-------- 6 files changed, 43 insertions(+), 41 deletions(-) diff --git a/app/services/idv/financials_validator.rb b/app/services/idv/financials_validator.rb index d4c4746b0c2..187dcabd348 100644 --- a/app/services/idv/financials_validator.rb +++ b/app/services/idv/financials_validator.rb @@ -2,13 +2,9 @@ module Idv class FinancialsValidator < VendorValidator private - def session_id - idv_session.vendor_session_id - end - def try_submit try_agent_action do - idv_agent.submit_financials(vendor_params, session_id) + idv_agent.submit_financials(vendor_params, vendor_session_id) end end end diff --git a/app/services/idv/phone_validator.rb b/app/services/idv/phone_validator.rb index e6bb1d3188a..7e4b26cc009 100644 --- a/app/services/idv/phone_validator.rb +++ b/app/services/idv/phone_validator.rb @@ -2,13 +2,9 @@ module Idv class PhoneValidator < VendorValidator private - def session_id - idv_session.vendor_session_id - end - def try_submit try_agent_action do - idv_agent.submit_phone(vendor_params, session_id) + idv_agent.submit_phone(vendor_params, vendor_session_id) end end end diff --git a/app/services/idv/step.rb b/app/services/idv/step.rb index 72e93cd0924..e77d5a1c268 100644 --- a/app/services/idv/step.rb +++ b/app/services/idv/step.rb @@ -37,14 +37,20 @@ def merge_vendor_errors(errors) end end + def idv_vendor + @_idv_vendor ||= Idv::Vendor.new + end + def vendor_errors @_vendor_errors ||= vendor_validator.errors end def vendor_validator @_vendor_validator ||= vendor_validator_class.new( - idv_session: idv_session, - vendor_params: vendor_params + applicant: idv_session.applicant, + vendor: (idv_session.vendor || idv_vendor.pick), + vendor_params: vendor_params, + vendor_session_id: idv_session.vendor_session_id ) end end diff --git a/app/services/idv/vendor_validator.rb b/app/services/idv/vendor_validator.rb index bb0ca462b9e..e7888057195 100644 --- a/app/services/idv/vendor_validator.rb +++ b/app/services/idv/vendor_validator.rb @@ -2,11 +2,13 @@ module Idv class VendorValidator delegate :success?, :errors, to: :result - attr_reader :idv_session, :vendor_params + attr_reader :applicant, :vendor, :vendor_params, :vendor_session_id - def initialize(idv_session:, vendor_params:) - @idv_session = idv_session + def initialize(applicant:, vendor:, vendor_params:, vendor_session_id:) + @applicant = applicant + @vendor = vendor @vendor_params = vendor_params + @vendor_session_id = vendor_session_id end def reasons @@ -15,14 +17,10 @@ def reasons private - def idv_vendor - @_idv_vendor ||= Idv::Vendor.new - end - def idv_agent @_agent ||= Idv::Agent.new( - applicant: idv_session.applicant, - vendor: (idv_session.vendor || idv_vendor.pick) + applicant: applicant, + vendor: vendor ) end diff --git a/spec/services/idv/financials_validator_spec.rb b/spec/services/idv/financials_validator_spec.rb index 0292c6b204d..04a6e910dac 100644 --- a/spec/services/idv/financials_validator_spec.rb +++ b/spec/services/idv/financials_validator_spec.rb @@ -3,13 +3,9 @@ describe Idv::FinancialsValidator do let(:user) { build(:user) } - let(:idv_session) do - idvs = Idv::Session.new(user_session: {}, current_user: user, issuer: nil) - idvs.vendor = :mock - idvs - end - - let(:session_id) { idv_session.vendor_session_id } + let(:applicant) { Proofer::Applicant.new({}) } + let(:vendor) { :mock } + let(:vendor_session_id) { SecureRandom.uuid } let(:params) do { ccn: '123-45-6789' } @@ -17,15 +13,22 @@ let(:confirmation) { instance_double(Proofer::Confirmation) } - subject { Idv::FinancialsValidator.new(idv_session: idv_session, vendor_params: params) } + subject do + Idv::FinancialsValidator.new( + applicant: applicant, + vendor: vendor, + vendor_params: params, + vendor_session_id: vendor_session_id + ) + end def stub_agent_calls agent = instance_double(Idv::Agent) allow(Idv::Agent).to receive(:new). - with(applicant: idv_session.applicant, vendor: :mock). + with(applicant: applicant, vendor: vendor). and_return(agent) expect(agent).to receive(:submit_financials). - with(params, idv_session.vendor_session_id).and_return(confirmation) + with(params, vendor_session_id).and_return(confirmation) end describe '#success?' do diff --git a/spec/services/idv/phone_validator_spec.rb b/spec/services/idv/phone_validator_spec.rb index 83b849ca116..74dcf9debce 100644 --- a/spec/services/idv/phone_validator_spec.rb +++ b/spec/services/idv/phone_validator_spec.rb @@ -3,13 +3,9 @@ describe Idv::PhoneValidator do let(:user) { build(:user) } - let(:idv_session) do - idvs = Idv::Session.new(user_session: {}, current_user: user, issuer: nil) - idvs.vendor = :mock - idvs - end - - let(:session_id) { idv_session.vendor_session_id } + let(:applicant) { Proofer::Applicant.new({}) } + let(:vendor) { :mock } + let(:vendor_session_id) { SecureRandom.uuid } let(:params) do { phone: '202-555-1212' } @@ -17,15 +13,22 @@ let(:confirmation) { instance_double(Proofer::Confirmation) } - subject { Idv::PhoneValidator.new(idv_session: idv_session, vendor_params: params) } + subject do + Idv::PhoneValidator.new( + applicant: applicant, + vendor: vendor, + vendor_params: params, + vendor_session_id: vendor_session_id + ) + end def stub_agent_calls agent = instance_double(Idv::Agent) allow(Idv::Agent).to receive(:new). - with(applicant: idv_session.applicant, vendor: :mock). + with(applicant: applicant, vendor: vendor). and_return(agent) expect(agent).to receive(:submit_phone). - with(params, idv_session.vendor_session_id).and_return(confirmation) + with(params, vendor_session_id).and_return(confirmation) end describe '#success?' do From 6a4f1986736a4b7188f8745761cbc36e650cb603 Mon Sep 17 00:00:00 2001 From: Brian Hurst Date: Tue, 27 Jun 2017 15:42:25 -0400 Subject: [PATCH 05/22] Use param for i18n --- .reek | 1 + app/controllers/application_controller.rb | 9 +- app/services/locale_chooser.rb | 21 + app/services/locale_validator.rb | 13 + app/services/marketing_site.rb | 15 +- config/application.rb | 3 +- config/routes.rb | 252 +-- package-lock.json | 2114 +++++++++++++++++++++ spec/features/visitors/i18n_spec.rb | 37 +- spec/services/marketing_site_spec.rb | 44 +- 10 files changed, 2374 insertions(+), 135 deletions(-) create mode 100644 app/services/locale_chooser.rb create mode 100644 app/services/locale_validator.rb create mode 100644 package-lock.json diff --git a/.reek b/.reek index 37bd18db79d..51c9fc3ced3 100644 --- a/.reek +++ b/.reek @@ -89,6 +89,7 @@ UtilityFunction: public_methods_only: true exclude: - AnalyticsEventJob#perform + - ApplicationController#default_url_options - ApplicationHelper#step_class - PersonalKeyFormatter#regexp - SessionTimeoutWarningHelper#frequency diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ec5518ca0a3..9b6e0ee328c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -50,6 +50,11 @@ def decorated_session ).call end + def default_url_options + active_locale = I18n.locale + { locale: active_locale == I18n.default_locale ? nil : active_locale } + end + private def disable_caching @@ -130,9 +135,7 @@ def skip_session_expiration end def set_locale - I18n.locale = - http_accept_language.compatible_language_from(I18n.available_locales) || - I18n.default_locale + I18n.locale = LocaleChooser.new(params[:locale], request).locale end def sp_session diff --git a/app/services/locale_chooser.rb b/app/services/locale_chooser.rb new file mode 100644 index 00000000000..8b8676e6a29 --- /dev/null +++ b/app/services/locale_chooser.rb @@ -0,0 +1,21 @@ +class LocaleChooser + include HttpAcceptLanguage::EasyAccess + + def initialize(locale_param, request) + @locale_param = locale_param + @request = request + end + + def locale + return locale_param if locale_valid? + http_accept_language.compatible_language_from(I18n.available_locales) || I18n.default_locale + end + + private + + attr_reader :locale_param, :request + + def locale_valid? + LocaleValidator.new(locale_param).success? + end +end diff --git a/app/services/locale_validator.rb b/app/services/locale_validator.rb new file mode 100644 index 00000000000..b1e6f1bb59b --- /dev/null +++ b/app/services/locale_validator.rb @@ -0,0 +1,13 @@ +class LocaleValidator + def initialize(locale) + @locale = locale + end + + def success? + locale.present? && I18n.available_locales.include?(locale.to_sym) + end + + private + + attr_reader :locale +end diff --git a/app/services/marketing_site.rb b/app/services/marketing_site.rb index ccbfa81b344..776fdb339cd 100644 --- a/app/services/marketing_site.rb +++ b/app/services/marketing_site.rb @@ -1,23 +1,28 @@ class MarketingSite BASE_URL = URI('https://www.login.gov').freeze + def self.locale_segment + active_locale = I18n.locale + active_locale == I18n.default_locale ? '/' : "/#{active_locale}/" + end + def self.base_url - BASE_URL.to_s + URI.join(BASE_URL, locale_segment).to_s end def self.privacy_url - URI.join(BASE_URL, '/policy').to_s + URI.join(BASE_URL, locale_segment, 'policy').to_s end def self.contact_url - URI.join(BASE_URL, '/contact').to_s + URI.join(BASE_URL, locale_segment, 'contact').to_s end def self.help_url - URI.join(BASE_URL, '/help').to_s + URI.join(BASE_URL, locale_segment, 'help').to_s end def self.help_authenticator_app_url - URI.join(BASE_URL, '/help/signing-in/what-is-an-authenticator-app/').to_s + URI.join(BASE_URL, locale_segment, 'help/signing-in/what-is-an-authenticator-app/').to_s end end diff --git a/config/application.rb b/config/application.rb index b12f84bf8a2..91fbd99d570 100644 --- a/config/application.rb +++ b/config/application.rb @@ -16,8 +16,9 @@ class Application < Rails::Application config.browserify_rails.force = true config.browserify_rails.commandline_options = '-t [ babelify --presets [ es2015 ] ]' - config.i18n.available_locales = Figaro.env.available_locales.split(' ') config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{yml}')] + config.i18n.available_locales = Figaro.env.available_locales.split(' ') + config.i18n.default_locale = :en routes.default_url_options[:host] = Figaro.env.domain_name diff --git a/config/routes.rb b/config/routes.rb index 9e81f39e9fa..64c727915d0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,60 +2,7 @@ require 'sidekiq/web' mount Sidekiq::Web => '/sidekiq', constraints: AdminConstraint.new - # Devise handles login itself. It's first in the chain to avoid a redirect loop during - # authentication failure. - devise_for( - :users, - skip: %i[confirmations sessions registrations two_factor_authentication], - controllers: { passwords: 'users/reset_passwords' } - ) - - # Additional device controller routes. - devise_scope :user do - get '/' => 'users/sessions#new', as: :new_user_session - post '/' => 'users/sessions#create', as: :user_session - get '/active' => 'users/sessions#active' - - get '/login/two_factor/authenticator' => 'two_factor_authentication/totp_verification#show' - post '/login/two_factor/authenticator' => 'two_factor_authentication/totp_verification#create' - get '/login/two_factor/personal_key' => 'two_factor_authentication/personal_key_verification#show' - post '/login/two_factor/personal_key' => 'two_factor_authentication/personal_key_verification#create' - get '/login/two_factor/:otp_delivery_preference' => 'two_factor_authentication/otp_verification#show', - as: :login_two_factor - post '/login/two_factor/:otp_delivery_preference' => 'two_factor_authentication/otp_verification#create', - as: :login_otp - - get '/reauthn' => 'mfa_confirmation#new', as: :user_password_confirm - post '/reauthn' => 'mfa_confirmation#create', as: :reauthn_user_password - get '/timeout' => 'users/sessions#timeout' - end - - if Figaro.env.enable_test_routes == 'true' - namespace :test do - # Assertion granting test start + return. - get '/saml' => 'saml_test#start' - get '/saml/decode_assertion' => 'saml_test#start' - post '/saml/decode_assertion' => 'saml_test#decode_response' - post '/saml/decode_slo_request' => 'saml_test#decode_slo_request' - end - end - - # Non-devise-controller routes. Alphabetically sorted. - get '/.well-known/openid-configuration' => 'openid_connect/configuration#index', - as: :openid_connect_configuration - - get '/account' => 'accounts#show' - get '/account/reactivate/start' => 'reactivate_account#index', as: :reactivate_account - put '/account/reactivate/start' => 'reactivate_account#update' - get '/account/reactivate/verify_password' => 'users/verify_password#new', as: :verify_password - put '/account/reactivate/verify_password' => 'users/verify_password#update', as: :update_verify_password - get '/account/reactivate/verify_personal_key' => 'users/verify_personal_key#new', - as: :verify_personal_key - post '/account/reactivate/verify_personal_key' => 'users/verify_personal_key#create', - as: :create_verify_personal_key - get '/account/verify_phone' => 'users/verify_profile_phone#index', as: :verify_profile_phone - post '/account/verify_phone' => 'users/verify_profile_phone#create' - + # Non i18n routes. Alphabetically sorted. get '/api/health/workers' => 'health/workers#index' get '/api/openid_connect/certs' => 'openid_connect/certs#index' post '/api/openid_connect/token' => 'openid_connect/token#create' @@ -69,84 +16,141 @@ post '/api/service_provider' => 'service_provider#update' - delete '/authenticator_setup' => 'users/totp_setup#disable', as: :disable_totp - get '/authenticator_setup' => 'users/totp_setup#new' - patch '/authenticator_setup' => 'users/totp_setup#confirm' - get '/authenticator_start' => 'users/totp_setup#start' + get '/openid_connect/authorize' => 'openid_connect/authorization#index' + get '/openid_connect/logout' => 'openid_connect/logout#index' - get '/forgot_password' => 'forgot_password#show' + # i18n routes. Alphabetically sorted. + scope '(:locale)', locale: /#{I18n.available_locales.join('|')}/ do + # Devise handles login itself. It's first in the chain to avoid a redirect loop during + # authentication failure. + devise_for( + :users, + skip: %i[confirmations sessions registrations two_factor_authentication], + controllers: { passwords: 'users/reset_passwords' } + ) + + # Additional device controller routes. + devise_scope :user do + get '/' => 'users/sessions#new', as: :new_user_session + post '/' => 'users/sessions#create', as: :user_session + get '/active' => 'users/sessions#active' + + get '/login/two_factor/authenticator' => 'two_factor_authentication/totp_verification#show' + post '/login/two_factor/authenticator' => 'two_factor_authentication/totp_verification#create' + get '/login/two_factor/personal_key' => 'two_factor_authentication/personal_key_verification#show' + post '/login/two_factor/personal_key' => 'two_factor_authentication/personal_key_verification#create' + get '/login/two_factor/:otp_delivery_preference' => 'two_factor_authentication/otp_verification#show', + as: :login_two_factor + post '/login/two_factor/:otp_delivery_preference' => 'two_factor_authentication/otp_verification#create', + as: :login_otp + + get '/reauthn' => 'mfa_confirmation#new', as: :user_password_confirm + post '/reauthn' => 'mfa_confirmation#create', as: :reauthn_user_password + get '/timeout' => 'users/sessions#timeout' + end - get '/manage/email' => 'users/emails#edit' - match '/manage/email' => 'users/emails#update', via: %i[patch put] - get '/manage/password' => 'users/passwords#edit' - patch '/manage/password' => 'users/passwords#update' - get '/manage/phone' => 'users/phones#edit' - match '/manage/phone' => 'users/phones#update', via: %i[patch put] - get '/manage/personal_key' => 'users/personal_keys#show', as: :manage_personal_key - post '/manage/personal_key' => 'users/personal_keys#update' + if Figaro.env.enable_test_routes == 'true' + namespace :test do + # Assertion granting test start + return. + get '/saml' => 'saml_test#start' + get '/saml/decode_assertion' => 'saml_test#start' + post '/saml/decode_assertion' => 'saml_test#decode_response' + post '/saml/decode_slo_request' => 'saml_test#decode_slo_request' + end + end - get '/openid_connect/authorize' => 'openid_connect/authorization#index' - get '/openid_connect/logout' => 'openid_connect/logout#index' + # Non-devise-controller routes. Alphabetically sorted. + get '/.well-known/openid-configuration' => 'openid_connect/configuration#index', + as: :openid_connect_configuration + + get '/account' => 'accounts#show' + get '/account/reactivate/start' => 'reactivate_account#index', as: :reactivate_account + put '/account/reactivate/start' => 'reactivate_account#update' + get '/account/reactivate/verify_password' => 'users/verify_password#new', as: :verify_password + put '/account/reactivate/verify_password' => 'users/verify_password#update', as: :update_verify_password + get '/account/reactivate/verify_personal_key' => 'users/verify_personal_key#new', + as: :verify_personal_key + post '/account/reactivate/verify_personal_key' => 'users/verify_personal_key#create', + as: :create_verify_personal_key + get '/account/verify_phone' => 'users/verify_profile_phone#index', as: :verify_profile_phone + post '/account/verify_phone' => 'users/verify_profile_phone#create' + + delete '/authenticator_setup' => 'users/totp_setup#disable', as: :disable_totp + get '/authenticator_setup' => 'users/totp_setup#new' + patch '/authenticator_setup' => 'users/totp_setup#confirm' + get '/authenticator_start' => 'users/totp_setup#start' + + get '/forgot_password' => 'forgot_password#show' + + get '/manage/email' => 'users/emails#edit' + match '/manage/email' => 'users/emails#update', via: %i[patch put] + get '/manage/password' => 'users/passwords#edit' + patch '/manage/password' => 'users/passwords#update' + get '/manage/phone' => 'users/phones#edit' + match '/manage/phone' => 'users/phones#update', via: %i[patch put] + get '/manage/personal_key' => 'users/personal_keys#show', as: :manage_personal_key + post '/manage/personal_key' => 'users/personal_keys#update' + + get '/otp/send' => 'users/two_factor_authentication#send_code' + get '/phone_setup' => 'users/two_factor_authentication_setup#index' + patch '/phone_setup' => 'users/two_factor_authentication_setup#set' + get '/users/two_factor_authentication' => 'users/two_factor_authentication#show', + as: :user_two_factor_authentication # route name is used by two_factor_authentication gem + + get '/profile', to: redirect('/account') + get '/profile/reactivate', to: redirect('/account/reactivate') + get '/profile/verify', to: redirect('/account/verify') + + post '/sign_up/create_password' => 'sign_up/passwords#create', as: :sign_up_create_password + get '/sign_up/email/confirm' => 'sign_up/email_confirmations#create', + as: :sign_up_create_email_confirmation + get '/sign_up/enter_email' => 'sign_up/registrations#new', as: :sign_up_email + post '/sign_up/enter_email' => 'sign_up/registrations#create', as: :sign_up_register + get '/sign_up/enter_email/resend' => 'sign_up/email_resend#new', as: :sign_up_email_resend + post '/sign_up/enter_email/resend' => 'sign_up/email_resend#create', + as: :sign_up_create_email_resend + get '/sign_up/enter_password' => 'sign_up/passwords#new' + get '/sign_up/personal_key' => 'sign_up/personal_keys#show' + post '/sign_up/personal_key' => 'sign_up/personal_keys#update' + get '/sign_up/start' => 'sign_up/registrations#show', as: :sign_up_start + get '/sign_up/verify_email' => 'sign_up/emails#show', as: :sign_up_verify_email + get '/sign_up/completed' => 'sign_up/completions#show', as: :sign_up_completed + post '/sign_up/completed' => 'sign_up/completions#update' + + delete '/users' => 'users#destroy', as: :destroy_user + + if FeatureManagement.enable_identity_verification? + get '/verify' => 'verify#index' + get '/verify/activated' => 'verify#activated' + get '/verify/address' => 'verify/address#index' + get '/verify/cancel' => 'verify#cancel' + get '/verify/confirmations' => 'verify/confirmations#show' + post '/verify/confirmations' => 'verify/confirmations#update' + get '/verify/fail' => 'verify#fail' + get '/verify/finance' => 'verify/finance#new' + put '/verify/finance' => 'verify/finance#create' + get '/verify/finance/other' => 'verify/finance_other#new' + get '/verify/phone' => 'verify/phone#new' + put '/verify/phone' => 'verify/phone#create' + get '/verify/review' => 'verify/review#new' + put '/verify/review' => 'verify/review#create' + get '/verify/session' => 'verify/sessions#new' + put '/verify/session' => 'verify/sessions#create' + delete '/verify/session' => 'verify/sessions#destroy' + get '/verify/session/dupe' => 'verify/sessions#dupe' - get '/otp/send' => 'users/two_factor_authentication#send_code' - get '/phone_setup' => 'users/two_factor_authentication_setup#index' - patch '/phone_setup' => 'users/two_factor_authentication_setup#set' - get '/users/two_factor_authentication' => 'users/two_factor_authentication#show', - as: :user_two_factor_authentication # route name is used by two_factor_authentication gem - - get '/profile', to: redirect('/account') - get '/profile/reactivate', to: redirect('/account/reactivate') - get '/profile/verify', to: redirect('/account/verify') - - post '/sign_up/create_password' => 'sign_up/passwords#create', as: :sign_up_create_password - get '/sign_up/email/confirm' => 'sign_up/email_confirmations#create', - as: :sign_up_create_email_confirmation - get '/sign_up/enter_email' => 'sign_up/registrations#new', as: :sign_up_email - post '/sign_up/enter_email' => 'sign_up/registrations#create', as: :sign_up_register - get '/sign_up/enter_email/resend' => 'sign_up/email_resend#new', as: :sign_up_email_resend - post '/sign_up/enter_email/resend' => 'sign_up/email_resend#create', - as: :sign_up_create_email_resend - get '/sign_up/enter_password' => 'sign_up/passwords#new' - get '/sign_up/personal_key' => 'sign_up/personal_keys#show' - post '/sign_up/personal_key' => 'sign_up/personal_keys#update' - get '/sign_up/start' => 'sign_up/registrations#show', as: :sign_up_start - get '/sign_up/verify_email' => 'sign_up/emails#show', as: :sign_up_verify_email - get '/sign_up/completed' => 'sign_up/completions#show', as: :sign_up_completed - post '/sign_up/completed' => 'sign_up/completions#update' - - delete '/users' => 'users#destroy', as: :destroy_user - - if FeatureManagement.enable_identity_verification? - get '/verify' => 'verify#index' - get '/verify/activated' => 'verify#activated' - get '/verify/address' => 'verify/address#index' - get '/verify/cancel' => 'verify#cancel' - get '/verify/confirmations' => 'verify/confirmations#show' - post '/verify/confirmations' => 'verify/confirmations#update' - get '/verify/fail' => 'verify#fail' - get '/verify/finance' => 'verify/finance#new' - put '/verify/finance' => 'verify/finance#create' - get '/verify/finance/other' => 'verify/finance_other#new' - get '/verify/phone' => 'verify/phone#new' - put '/verify/phone' => 'verify/phone#create' - get '/verify/review' => 'verify/review#new' - put '/verify/review' => 'verify/review#create' - get '/verify/session' => 'verify/sessions#new' - put '/verify/session' => 'verify/sessions#create' - delete '/verify/session' => 'verify/sessions#destroy' - get '/verify/session/dupe' => 'verify/sessions#dupe' + end - end + if FeatureManagement.enable_usps_verification? + get '/account/verify' => 'users/verify_account#index', as: :verify_account + post '/account/verify' => 'users/verify_account#create' + get '/verify/usps' => 'verify/usps#index' + put '/verify/usps' => 'verify/usps#create' + end - if FeatureManagement.enable_usps_verification? - get '/account/verify' => 'users/verify_account#index', as: :verify_account - post '/account/verify' => 'users/verify_account#create' - get '/verify/usps' => 'verify/usps#index' - put '/verify/usps' => 'verify/usps#create' + root to: 'users/sessions#new' end - root to: 'users/sessions#new' - # Make sure any new routes are added above this line! # The line below will route all requests that aren't # defined route to the 404 page. Therefore, anything you put after this rule diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000000..41ba3b3cfa3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2114 @@ +{ + "name": "upaya", + "version": "0.0.1", + "lockfileVersion": 1, + "dependencies": { + "acorn": { + "version": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", + "dev": true + }, + "acorn-jsx": { + "version": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "dependencies": { + "acorn": { + "version": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "ajv": { + "version": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "dependencies": { + "json-stable-stringify": { + "version": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true + } + } + }, + "ajv-keywords": { + "version": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", + "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", + "dev": true + }, + "amdefine": { + "version": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true, + "optional": true + }, + "ansi-escapes": { + "version": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "dev": true + }, + "ansi-regex": { + "version": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "argparse": { + "version": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "dev": true + }, + "array-filter": { + "version": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", + "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", + "dev": true + }, + "array-map": { + "version": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", + "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", + "dev": true + }, + "array-reduce": { + "version": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", + "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", + "dev": true + }, + "array-union": { + "version": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true + }, + "array-uniq": { + "version": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "arrify": { + "version": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asn1.js": { + "version": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.1.tgz", + "integrity": "sha1-SLokC0WpKA6UdImQull9IWYX/UA=", + "dev": true + }, + "assert": { + "version": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "dev": true + }, + "assertion-error": { + "version": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", + "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", + "dev": true + }, + "astw": { + "version": "https://registry.npmjs.org/astw/-/astw-2.2.0.tgz", + "integrity": "sha1-e9QXhNMkk5h66yOba04cV6hzuRc=", + "dev": true + }, + "async": { + "version": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "babel-code-frame": { + "version": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz", + "integrity": "sha1-AnYgvuVnqIwyVhV05/0IAdMxGOQ=", + "dev": true + }, + "babel-core": { + "version": "https://registry.npmjs.org/babel-core/-/babel-core-6.24.1.tgz", + "integrity": "sha1-jEKFZNzh4fQfszfsNPTDsCK1rYM=", + "dev": true + }, + "babel-eslint": { + "version": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-7.2.3.tgz", + "integrity": "sha1-sv4tgBJkcPXBlELcdXJTqJdxCCc=", + "dev": true + }, + "babel-generator": { + "version": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.24.1.tgz", + "integrity": "sha1-5xX0hsWN7SVknYiJRNUqoHxdlJc=", + "dev": true, + "dependencies": { + "jsesc": { + "version": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + } + } + }, + "babel-helper-call-delegate": { + "version": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", + "dev": true + }, + "babel-helper-define-map": { + "version": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.24.1.tgz", + "integrity": "sha1-epdH8ljYlH0y1RX2qhx70CIEoIA=", + "dev": true + }, + "babel-helper-function-name": { + "version": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "dev": true + }, + "babel-helper-get-function-arity": { + "version": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "dev": true + }, + "babel-helper-hoist-variables": { + "version": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", + "dev": true + }, + "babel-helper-optimise-call-expression": { + "version": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", + "dev": true + }, + "babel-helper-regex": { + "version": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.24.1.tgz", + "integrity": "sha1-024i+rEAjXnYhkjjIRaGgShFbOg=", + "dev": true + }, + "babel-helper-replace-supers": { + "version": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", + "dev": true + }, + "babel-helpers": { + "version": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "dev": true + }, + "babel-messages": { + "version": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true + }, + "babel-plugin-check-es2015-constants": { + "version": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", + "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", + "dev": true + }, + "babel-plugin-transform-es2015-arrow-functions": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", + "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", + "dev": true + }, + "babel-plugin-transform-es2015-block-scoped-functions": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", + "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", + "dev": true + }, + "babel-plugin-transform-es2015-block-scoping": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.24.1.tgz", + "integrity": "sha1-dsKV3DpHQbFmWt/TFnIV3P8ypXY=", + "dev": true + }, + "babel-plugin-transform-es2015-classes": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", + "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", + "dev": true + }, + "babel-plugin-transform-es2015-computed-properties": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", + "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", + "dev": true + }, + "babel-plugin-transform-es2015-destructuring": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", + "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", + "dev": true + }, + "babel-plugin-transform-es2015-duplicate-keys": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", + "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", + "dev": true + }, + "babel-plugin-transform-es2015-for-of": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", + "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", + "dev": true + }, + "babel-plugin-transform-es2015-function-name": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", + "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", + "dev": true + }, + "babel-plugin-transform-es2015-literals": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", + "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", + "dev": true + }, + "babel-plugin-transform-es2015-modules-amd": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", + "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", + "dev": true + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.1.tgz", + "integrity": "sha1-0+MQtA72ZKNmIiAAl8bUQCmPK/4=", + "dev": true + }, + "babel-plugin-transform-es2015-modules-systemjs": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", + "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", + "dev": true + }, + "babel-plugin-transform-es2015-modules-umd": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", + "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", + "dev": true + }, + "babel-plugin-transform-es2015-object-super": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", + "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", + "dev": true + }, + "babel-plugin-transform-es2015-parameters": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", + "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", + "dev": true + }, + "babel-plugin-transform-es2015-shorthand-properties": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", + "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", + "dev": true + }, + "babel-plugin-transform-es2015-spread": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", + "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", + "dev": true + }, + "babel-plugin-transform-es2015-sticky-regex": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", + "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", + "dev": true + }, + "babel-plugin-transform-es2015-template-literals": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", + "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", + "dev": true + }, + "babel-plugin-transform-es2015-typeof-symbol": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", + "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", + "dev": true + }, + "babel-plugin-transform-es2015-unicode-regex": { + "version": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", + "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", + "dev": true + }, + "babel-plugin-transform-regenerator": { + "version": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.24.1.tgz", + "integrity": "sha1-uNowWtQ8PJm0hI5P5AN7dw0jxBg=", + "dev": true + }, + "babel-plugin-transform-strict-mode": { + "version": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", + "dev": true + }, + "babel-preset-es2015": { + "version": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", + "integrity": "sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk=", + "dev": true + }, + "babel-register": { + "version": "https://registry.npmjs.org/babel-register/-/babel-register-6.24.1.tgz", + "integrity": "sha1-fhDhOi9xBlvfrVoXh7pFvKbe118=", + "dev": true + }, + "babel-runtime": { + "version": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.23.0.tgz", + "integrity": "sha1-CpSJ8UTecO+zzkMArM2zKeL8VDs=", + "dev": true + }, + "babel-template": { + "version": "https://registry.npmjs.org/babel-template/-/babel-template-6.24.1.tgz", + "integrity": "sha1-BK5RTx+Ts6JTfyoPYKWkX7gwgzM=", + "dev": true + }, + "babel-traverse": { + "version": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.24.1.tgz", + "integrity": "sha1-qzZnP9NW+aCUhlnnszjV/q2zFpU=", + "dev": true + }, + "babel-types": { + "version": "https://registry.npmjs.org/babel-types/-/babel-types-6.24.1.tgz", + "integrity": "sha1-oTaHncFbNga9oNkMH8dDBML/CXU=", + "dev": true + }, + "babelify": { + "version": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz", + "integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=", + "dev": true + }, + "babylon": { + "version": "https://registry.npmjs.org/babylon/-/babylon-6.17.2.tgz", + "integrity": "sha1-IB0l71+JLEG65JSIsI2w3Udun1w=", + "dev": true + }, + "balanced-match": { + "version": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + }, + "base64-js": { + "version": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.0.tgz", + "integrity": "sha1-o5mS1yNYSBGYK+XikLtqU9hnAPE=", + "dev": true + }, + "basscss": { + "version": "https://registry.npmjs.org/basscss/-/basscss-7.1.1.tgz", + "integrity": "sha1-e/MSAxl6Kd8O7lUfQlYXqL58d3s=" + }, + "basscss-align": { + "version": "https://registry.npmjs.org/basscss-align/-/basscss-align-1.0.2.tgz", + "integrity": "sha1-KUqmidb5nahuSvTFwokocIVcHDc=" + }, + "basscss-background-colors": { + "version": "https://registry.npmjs.org/basscss-background-colors/-/basscss-background-colors-1.1.3.tgz", + "integrity": "sha1-VKKDZRxAklZTJKTvW8JdcL52IdY=" + }, + "basscss-base-forms": { + "version": "https://registry.npmjs.org/basscss-base-forms/-/basscss-base-forms-2.0.2.tgz", + "integrity": "sha1-Fgi5n/SG3WuJEZLKjSXi7VdYWao=" + }, + "basscss-base-reset": { + "version": "https://registry.npmjs.org/basscss-base-reset/-/basscss-base-reset-2.0.3.tgz", + "integrity": "sha1-WScWF55JfgtOfJrdHxxMNAF8Vy0=" + }, + "basscss-base-tables": { + "version": "https://registry.npmjs.org/basscss-base-tables/-/basscss-base-tables-1.0.2.tgz", + "integrity": "sha1-uFDqHWSwb5GSK/z0AUuqoINquzA=" + }, + "basscss-base-typography": { + "version": "https://registry.npmjs.org/basscss-base-typography/-/basscss-base-typography-2.0.3.tgz", + "integrity": "sha1-H0vzRXEkgoII9oa8OFzTMK5+enI=" + }, + "basscss-border": { + "version": "https://registry.npmjs.org/basscss-border/-/basscss-border-3.0.4.tgz", + "integrity": "sha1-ZZk1aNoIZ+t12Wtwn5fjOyuQJIY=" + }, + "basscss-border-colors": { + "version": "https://registry.npmjs.org/basscss-border-colors/-/basscss-border-colors-1.1.3.tgz", + "integrity": "sha1-nrIya0eeqpe/m9bE7rwf2CTYCf4=" + }, + "basscss-borders": { + "version": "https://registry.npmjs.org/basscss-borders/-/basscss-borders-2.0.5.tgz", + "integrity": "sha1-PYRw+6kOzoknBeGlskwna3Oe644=" + }, + "basscss-btn": { + "version": "https://registry.npmjs.org/basscss-btn/-/basscss-btn-1.1.1.tgz", + "integrity": "sha1-xCFX8gG9lduaJRVoxULnQLKj178=" + }, + "basscss-btn-outline": { + "version": "https://registry.npmjs.org/basscss-btn-outline/-/basscss-btn-outline-1.1.0.tgz", + "integrity": "sha1-uEROqdPVCM0Adgqdb9/0uvXUJ1g=" + }, + "basscss-btn-primary": { + "version": "https://registry.npmjs.org/basscss-btn-primary/-/basscss-btn-primary-1.1.0.tgz", + "integrity": "sha1-DBJJKXHiFuQr3xNEEa/DgCw9i/c=" + }, + "basscss-color-base": { + "version": "https://registry.npmjs.org/basscss-color-base/-/basscss-color-base-2.0.2.tgz", + "integrity": "sha1-7YSL/OORq1NabRSnAqFPpMTzJC0=" + }, + "basscss-color-forms": { + "version": "https://registry.npmjs.org/basscss-color-forms/-/basscss-color-forms-3.0.2.tgz", + "integrity": "sha1-jy0dB9X8tmRVbNNUvvmM67LV4Ms=" + }, + "basscss-color-tables": { + "version": "https://registry.npmjs.org/basscss-color-tables/-/basscss-color-tables-1.0.4.tgz", + "integrity": "sha1-1DXsfF8hD6F959G4gT3rvKalQ+E=" + }, + "basscss-colors": { + "version": "https://registry.npmjs.org/basscss-colors/-/basscss-colors-2.2.0.tgz", + "integrity": "sha1-3Mt3Picu/kXfSkgJYsi9iLams+E=", + "dependencies": { + "colors.css": { + "version": "https://registry.npmjs.org/colors.css/-/colors.css-3.0.0.tgz", + "integrity": "sha1-URz0L7inGZqMvvSciKTqTx2Pnvw=" + } + } + }, + "basscss-defaults": { + "version": "https://registry.npmjs.org/basscss-defaults/-/basscss-defaults-2.1.3.tgz", + "integrity": "sha1-tOpjToFcaSPwx2ZbFIpMyLO0OSc=" + }, + "basscss-grid": { + "version": "https://registry.npmjs.org/basscss-grid/-/basscss-grid-1.0.6.tgz", + "integrity": "sha1-GlEsc7h0MwXkejanQyqtXCbMKGc=" + }, + "basscss-layout": { + "version": "https://registry.npmjs.org/basscss-layout/-/basscss-layout-3.1.0.tgz", + "integrity": "sha1-+fOS5IDaZmV9n+XenKTAfFecOk4=" + }, + "basscss-margin": { + "version": "https://registry.npmjs.org/basscss-margin/-/basscss-margin-1.0.7.tgz", + "integrity": "sha1-WpLYzamO85HHOhXt6Xs0tIiGQXw=" + }, + "basscss-padding": { + "version": "https://registry.npmjs.org/basscss-padding/-/basscss-padding-1.1.3.tgz", + "integrity": "sha1-adt5lBTm3Vi+2Dd2lSzCmeLmh04=" + }, + "basscss-position": { + "version": "https://registry.npmjs.org/basscss-position/-/basscss-position-2.0.3.tgz", + "integrity": "sha1-RnGAofjzhukHLtjQgpTSpuC6QwU=" + }, + "basscss-positions": { + "version": "https://registry.npmjs.org/basscss-positions/-/basscss-positions-1.0.5.tgz", + "integrity": "sha1-5P37bQMc8ljGERf5M3Hzq7uTiBo=" + }, + "basscss-responsive-states": { + "version": "https://registry.npmjs.org/basscss-responsive-states/-/basscss-responsive-states-1.0.6.tgz", + "integrity": "sha1-2JI0PheZiFwD5PHHAs18GrUo8AI=" + }, + "basscss-sass": { + "version": "https://registry.npmjs.org/basscss-sass/-/basscss-sass-3.0.0.tgz", + "integrity": "sha1-nxvoX6jqafmUQVN2ImjEavMLxM0=" + }, + "basscss-type-scale": { + "version": "https://registry.npmjs.org/basscss-type-scale/-/basscss-type-scale-1.0.5.tgz", + "integrity": "sha1-I79eQcnRQsgGHPmCnM8j6bMljsc=" + }, + "basscss-typography": { + "version": "https://registry.npmjs.org/basscss-typography/-/basscss-typography-3.0.3.tgz", + "integrity": "sha1-GCz0PffE6+0CdQ3HSAQcut/2DUM=" + }, + "bn.js": { + "version": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU=", + "dev": true + }, + "brace-expansion": { + "version": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", + "integrity": "sha1-Pv/DxQ4ABTH7cg6v+A8K6O8jz1k=", + "dev": true + }, + "brorand": { + "version": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browser-pack": { + "version": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.0.2.tgz", + "integrity": "sha1-+GzWzvT1MAyOY+B6TVEvZfv/RTE=", + "dev": true + }, + "browser-resolve": { + "version": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.2.tgz", + "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=", + "dev": true, + "dependencies": { + "resolve": { + "version": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } + } + }, + "browserify": { + "version": "https://registry.npmjs.org/browserify/-/browserify-13.3.0.tgz", + "integrity": "sha1-tanJAgJD8McORnW+yCI7xifkFc4=", + "dev": true + }, + "browserify-aes": { + "version": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.0.6.tgz", + "integrity": "sha1-Xncl297x/Vkw1OurSFZ85FHEigo=", + "dev": true + }, + "browserify-cache-api": { + "version": "https://registry.npmjs.org/browserify-cache-api/-/browserify-cache-api-3.0.1.tgz", + "integrity": "sha1-liR+hT8Gj9bg1FzHPwuyzZd47wI=", + "dev": true + }, + "browserify-cipher": { + "version": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz", + "integrity": "sha1-mYgkSHS/XtTijalWZtzWasj8Njo=", + "dev": true + }, + "browserify-des": { + "version": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.0.tgz", + "integrity": "sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=", + "dev": true + }, + "browserify-incremental": { + "version": "https://registry.npmjs.org/browserify-incremental/-/browserify-incremental-3.1.1.tgz", + "integrity": "sha1-BxPLdYckemMqnwjPG9FpuHi2Koo=", + "dev": true, + "dependencies": { + "jsonparse": { + "version": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz", + "integrity": "sha1-MwVCrT8KZUZlt3jz6y2an6UHrGQ=", + "dev": true + }, + "JSONStream": { + "version": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.10.0.tgz", + "integrity": "sha1-dDSdDYlSK3HzDwoD/5vSDKbxKsA=", + "dev": true + } + } + }, + "browserify-rsa": { + "version": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true + }, + "browserify-sign": { + "version": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true + }, + "browserify-zlib": { + "version": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", + "dev": true + }, + "buffer": { + "version": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "dev": true + }, + "buffer-xor": { + "version": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-modules": { + "version": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "builtin-status-codes": { + "version": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "cached-path-relative": { + "version": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.1.tgz", + "integrity": "sha1-0JxLUoAKpMB44t2BqGmqyQ0uVOc=", + "dev": true + }, + "caller-path": { + "version": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true + }, + "callsites": { + "version": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "chai": { + "version": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", + "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", + "dev": true + }, + "chalk": { + "version": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true + }, + "cipher-base": { + "version": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.3.tgz", + "integrity": "sha1-7qvxlEGc6QDaMBjCB9IS8qbfCgc=", + "dev": true + }, + "circular-json": { + "version": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.1.tgz", + "integrity": "sha1-vos2rvzN6LPKeqLWr8B6NyQsDS0=", + "dev": true + }, + "classlist.js": { + "version": "https://registry.npmjs.org/classlist.js/-/classlist.js-1.1.20150312.tgz", + "integrity": "sha1-HXCEL3Ai8I2awIbOaeWyUPLFd4k=" + }, + "cli-cursor": { + "version": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true + }, + "cli-width": { + "version": "https://registry.npmjs.org/cli-width/-/cli-width-2.1.0.tgz", + "integrity": "sha1-sjTKIJsp72b8UY2bmNWEewDt8Ao=", + "dev": true + }, + "clipboard": { + "version": "https://registry.npmjs.org/clipboard/-/clipboard-1.7.1.tgz", + "integrity": "sha1-Ng1taUbpmnof7zleQrqStem1oWs=" + }, + "co": { + "version": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "code-point-at": { + "version": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "colors.css": { + "version": "https://registry.npmjs.org/colors.css/-/colors.css-2.3.0.tgz", + "integrity": "sha1-6JU4N1Q+GdmOKRf/C5mPbbKGITs=" + }, + "combine-source-map": { + "version": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.7.2.tgz", + "integrity": "sha1-CHAxKFazB6h8xKxIbzqaYq7MwJ4=", + "dev": true, + "dependencies": { + "convert-source-map": { + "version": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", + "dev": true + } + } + }, + "concat-map": { + "version": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", + "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", + "dev": true, + "dependencies": { + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true + } + } + }, + "console-browserify": { + "version": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true + }, + "constants-browserify": { + "version": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "contains-path": { + "version": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, + "convert-source-map": { + "version": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.0.tgz", + "integrity": "sha1-ms1whRxtXf3ZPZKC5e35SgP/RrU=", + "dev": true + }, + "core-js": { + "version": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", + "integrity": "sha1-TekR5mew6ukSTjQlS1OupvxhjT4=", + "dev": true + }, + "core-util-is": { + "version": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "create-ecdh": { + "version": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz", + "integrity": "sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=", + "dev": true + }, + "create-hash": { + "version": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", + "integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=", + "dev": true + }, + "create-hmac": { + "version": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz", + "integrity": "sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=", + "dev": true + }, + "crypto-browserify": { + "version": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.11.0.tgz", + "integrity": "sha1-NlKgkGq5sqfgw85mpAjpV6JIVSI=", + "dev": true + }, + "d": { + "version": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "dev": true + }, + "date-now": { + "version": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true + }, + "deep-eql": { + "version": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", + "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", + "dev": true, + "dependencies": { + "type-detect": { + "version": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", + "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", + "dev": true + } + } + }, + "deep-is": { + "version": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "defined": { + "version": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, + "del": { + "version": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true + }, + "delegate": { + "version": "https://registry.npmjs.org/delegate/-/delegate-3.1.3.tgz", + "integrity": "sha1-moJRp3fXAl+qVXN7w7BxdCEnqf0=" + }, + "deps-sort": { + "version": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.0.tgz", + "integrity": "sha1-CRckkC6EZYJg65EHSMzNGvbiH7U=", + "dev": true + }, + "des.js": { + "version": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true + }, + "detect-indent": { + "version": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true + }, + "detective": { + "version": "https://registry.npmjs.org/detective/-/detective-4.5.0.tgz", + "integrity": "sha1-blqMaybmx6JUsca210kNmOyR7dE=", + "dev": true + }, + "diffie-hellman": { + "version": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", + "integrity": "sha1-tYNXOScM/ias9jIJn97SoH8gnl4=", + "dev": true + }, + "dirty-chai": { + "version": "https://registry.npmjs.org/dirty-chai/-/dirty-chai-1.2.2.tgz", + "integrity": "sha1-eEleYZY19/5EIZqkyDeEm/GDFC4=", + "dev": true + }, + "doctrine": { + "version": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz", + "integrity": "sha1-xz2NKQnSIpHhoAejlYBNqLZl/mM=", + "dev": true + }, + "domain-browser": { + "version": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", + "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=", + "dev": true + }, + "duplexer2": { + "version": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "dev": true + }, + "elliptic": { + "version": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", + "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "dev": true + }, + "error-ex": { + "version": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true + }, + "es5-ext": { + "version": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.22.tgz", + "integrity": "sha1-GHbFH5kHacESx4HqPr6J+E/TkHE=", + "dev": true + }, + "es6-iterator": { + "version": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.1.tgz", + "integrity": "sha1-jjGcnwRTv1ddN0lAplWSDlnKVRI=", + "dev": true + }, + "es6-map": { + "version": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "dev": true + }, + "es6-set": { + "version": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "dev": true + }, + "es6-symbol": { + "version": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "dev": true + }, + "es6-weak-map": { + "version": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", + "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", + "dev": true + }, + "escape-string-regexp": { + "version": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "dependencies": { + "esprima": { + "version": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "estraverse": { + "version": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "source-map": { + "version": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true + } + } + }, + "escope": { + "version": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", + "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", + "dev": true + }, + "eslint": { + "version": "https://registry.npmjs.org/eslint/-/eslint-3.19.0.tgz", + "integrity": "sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=", + "dev": true, + "dependencies": { + "json-stable-stringify": { + "version": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true + } + } + }, + "eslint-config-airbnb-base": { + "version": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-11.2.0.tgz", + "integrity": "sha1-GancRIGib3CQRUXsBAEWh2AY+FM=", + "dev": true + }, + "eslint-import-resolver-node": { + "version": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.2.3.tgz", + "integrity": "sha1-Wt2BBujJKNssuiMrzZ76hG49oWw=", + "dev": true + }, + "eslint-module-utils": { + "version": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.0.0.tgz", + "integrity": "sha1-pvjCHZATWHWc3DXbrBmCrh7li84=", + "dev": true, + "dependencies": { + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true + }, + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.3.0.tgz", + "integrity": "sha1-N8gB4K2g4pbL3yDD85OstbUq82s=", + "dev": true, + "dependencies": { + "doctrine": { + "version": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true + } + } + }, + "espree": { + "version": "https://registry.npmjs.org/espree/-/espree-3.4.3.tgz", + "integrity": "sha1-KRC1zNSc6JPC//+qtP2LOjG4I3Q=", + "dev": true, + "dependencies": { + "acorn": { + "version": "https://registry.npmjs.org/acorn/-/acorn-5.0.3.tgz", + "integrity": "sha1-xGDfCEkUY/AozLguqzcwvwEIez0=", + "dev": true + } + } + }, + "esprima": { + "version": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "dev": true + }, + "esquery": { + "version": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz", + "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", + "dev": true + }, + "esrecurse": { + "version": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.1.0.tgz", + "integrity": "sha1-RxO2U2rffyrE8yfVWed1a/9kgiA=", + "dev": true, + "dependencies": { + "estraverse": { + "version": "https://registry.npmjs.org/estraverse/-/estraverse-4.1.1.tgz", + "integrity": "sha1-9srKcokzqFDvkGYdDheYK6RxEaI=", + "dev": true + } + } + }, + "estraverse": { + "version": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "esutils": { + "version": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "event-emitter": { + "version": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "dev": true + }, + "events": { + "version": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "dev": true + }, + "evp_bytestokey": { + "version": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.0.tgz", + "integrity": "sha1-SXtmrZ/vZc18CKYYCCS6FHa2blM=", + "dev": true + }, + "exit-hook": { + "version": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", + "dev": true + }, + "fast-levenshtein": { + "version": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "field-kit": { + "version": "https://registry.npmjs.org/field-kit/-/field-kit-2.1.0.tgz", + "integrity": "sha1-5o7eX04wUbLcQlgQWklcVBo7LX8=" + }, + "figures": { + "version": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true + }, + "file-entry-cache": { + "version": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true + }, + "fill-keys": { + "version": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", + "integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=", + "dev": true + }, + "find-up": { + "version": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true + }, + "flat-cache": { + "version": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.2.2.tgz", + "integrity": "sha1-+oZxTnLCHbiGAXYezy9VXRq8a5Y=", + "dev": true + }, + "flex-object": { + "version": "https://registry.npmjs.org/flex-object/-/flex-object-2.0.5.tgz", + "integrity": "sha1-Ebm7wPT4ZOncYH6YzixnBEK9yFE=" + }, + "focus-trap": { + "version": "https://registry.npmjs.org/focus-trap/-/focus-trap-2.3.0.tgz", + "integrity": "sha1-B8kZZIZ9NGMV9PX434i/lkVTFuI=" + }, + "formatio": { + "version": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz", + "integrity": "sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek=" + }, + "fs.realpath": { + "version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "function-bind": { + "version": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz", + "integrity": "sha1-FhdnFMgBeY5Ojyz391KUZ7tKV3E=", + "dev": true + }, + "generate-function": { + "version": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", + "dev": true + }, + "generate-object-property": { + "version": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true + }, + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "dev": true + }, + "globals": { + "version": "https://registry.npmjs.org/globals/-/globals-9.17.0.tgz", + "integrity": "sha1-DAymltm5u2lNLlRwvTd3fKrVAoY=", + "dev": true + }, + "globby": { + "version": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true + }, + "good-listener": { + "version": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=" + }, + "graceful-fs": { + "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "has": { + "version": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", + "dev": true + }, + "has-ansi": { + "version": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true + }, + "has-require": { + "version": "https://registry.npmjs.org/has-require/-/has-require-1.2.2.tgz", + "integrity": "sha1-khZ1qxMNvZdo/I2o8ajiQt+kF3Q=", + "dev": true + }, + "hash-base": { + "version": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", + "integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=", + "dev": true + }, + "hash.js": { + "version": "https://registry.npmjs.org/hash.js/-/hash.js-1.0.3.tgz", + "integrity": "sha1-EzL/ABVsCg/92CNgE9B7d6BFFXM=", + "dev": true + }, + "hint.css": { + "version": "https://registry.npmjs.org/hint.css/-/hint.css-2.5.0.tgz", + "integrity": "sha1-OMrjZn5C2R392+UDEAqzSTL2/WU=" + }, + "hmac-drbg": { + "version": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true + }, + "home-or-tmp": { + "version": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "dev": true + }, + "hosted-git-info": { + "version": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.4.2.tgz", + "integrity": "sha1-AHa59GonBQbduq6lZJaJdGBhKmc=", + "dev": true + }, + "htmlescape": { + "version": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", + "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=", + "dev": true + }, + "https-browserify": { + "version": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz", + "integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=", + "dev": true + }, + "ieee754": { + "version": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=", + "dev": true + }, + "ignore": { + "version": "https://registry.npmjs.org/ignore/-/ignore-3.3.3.tgz", + "integrity": "sha1-QyNS5XrM2HqzEQ6C0/6g5HgSFW0=", + "dev": true + }, + "imurmurhash": { + "version": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indexof": { + "version": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "inflight": { + "version": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true + }, + "inherits": { + "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + }, + "inline-source-map": { + "version": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", + "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", + "dev": true + }, + "input-sim": { + "version": "https://registry.npmjs.org/input-sim/-/input-sim-3.1.0.tgz", + "integrity": "sha1-g/nCFPTW2MjpCU00V5eVkeqIzCw=" + }, + "inquirer": { + "version": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", + "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", + "dev": true + }, + "insert-module-globals": { + "version": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.0.1.tgz", + "integrity": "sha1-wDv04BywhtW15azorQr+eInWOMM=", + "dev": true + }, + "interpret": { + "version": "https://registry.npmjs.org/interpret/-/interpret-1.0.3.tgz", + "integrity": "sha1-y8NcYu7uc/Gat7EKgBURQBr8D5A=", + "dev": true + }, + "invariant": { + "version": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", + "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=", + "dev": true + }, + "is-arrayish": { + "version": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-buffer": { + "version": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", + "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", + "dev": true + }, + "is-builtin-module": { + "version": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true + }, + "is-finite": { + "version": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true + }, + "is-my-json-valid": { + "version": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", + "integrity": "sha1-8Hndm/2uZe4gOKrorLyGqxCeNpM=", + "dev": true + }, + "is-object": { + "version": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", + "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", + "dev": true + }, + "is-path-cwd": { + "version": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", + "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", + "dev": true + }, + "is-path-inside": { + "version": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz", + "integrity": "sha1-/AbloWg/vaE95mev9xe7wQpI838=", + "dev": true + }, + "is-property": { + "version": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-resolvable": { + "version": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz", + "integrity": "sha1-jfV8YeouPFAUCNEA+wE8+NbgzGI=", + "dev": true + }, + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "js-tokens": { + "version": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.1.tgz", + "integrity": "sha1-COnxMkhKLEWjCQfp3E1VZ7fxFNc=", + "dev": true + }, + "js-yaml": { + "version": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.8.4.tgz", + "integrity": "sha1-UgtFZPhlc7qWZir4Woyvp7S1pvY=", + "dev": true + }, + "jsesc": { + "version": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + }, + "json-stable-stringify": { + "version": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", + "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=", + "dev": true + }, + "json5": { + "version": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "jsonify": { + "version": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsonparse": { + "version": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, + "jsonpointer": { + "version": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "JSONStream": { + "version": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz", + "integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=", + "dev": true + }, + "labeled-stream-splicer": { + "version": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.0.tgz", + "integrity": "sha1-pS4dE4AkwAuGscDJH2d5GLiuClk=", + "dev": true, + "dependencies": { + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "levn": { + "version": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true + }, + "lexical-scope": { + "version": "https://registry.npmjs.org/lexical-scope/-/lexical-scope-1.2.0.tgz", + "integrity": "sha1-/Ope3HBKSzqHls3KQZw6CvryLfQ=", + "dev": true + }, + "load-json-file": { + "version": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true + }, + "locate-path": { + "version": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "dependencies": { + "path-exists": { + "version": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "lodash": { + "version": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + }, + "lodash.cond": { + "version": "https://registry.npmjs.org/lodash.cond/-/lodash.cond-4.5.2.tgz", + "integrity": "sha1-9HGh2khr5g9quVXRcRVSPdHSVdU=", + "dev": true + }, + "lodash.memoize": { + "version": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", + "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=", + "dev": true + }, + "lolex": { + "version": "https://registry.npmjs.org/lolex/-/lolex-1.3.2.tgz", + "integrity": "sha1-fD2mL/yzDw9agKJWbKJORdigHzE=" + }, + "loose-envify": { + "version": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "dev": true + }, + "merge-descriptors": { + "version": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "miller-rabin": { + "version": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.0.tgz", + "integrity": "sha1-SmL7HUKTPAVYOYL0xxb2+55sbT0=", + "dev": true + }, + "minimalistic-assert": { + "version": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", + "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true + }, + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true + }, + "module-deps": { + "version": "https://registry.npmjs.org/module-deps/-/module-deps-4.1.1.tgz", + "integrity": "sha1-IyFYM/HaE/1gbMuAh7RIUty4If0=", + "dev": true + }, + "module-not-found-error": { + "version": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", + "integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=", + "dev": true + }, + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "mute-stream": { + "version": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", + "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", + "dev": true + }, + "natural-compare": { + "version": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "normalize-package-data": { + "version": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.3.8.tgz", + "integrity": "sha1-2Bntoqne29H/pWPqQHHZNngilbs=", + "dev": true + }, + "normalize.css": { + "version": "https://registry.npmjs.org/normalize.css/-/normalize.css-4.2.0.tgz", + "integrity": "sha1-IdZsxVcVTUN5/R4HnsfeWKN5sJk=" + }, + "number-is-nan": { + "version": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-assign": { + "version": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "once": { + "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true + }, + "onetime": { + "version": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "optionator": { + "version": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true + }, + "os-browserify": { + "version": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.1.2.tgz", + "integrity": "sha1-ScoCk+CxlZCl9d4Qx/JlphfY/lQ=", + "dev": true + }, + "os-homedir": { + "version": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-tmpdir": { + "version": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-limit": { + "version": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz", + "integrity": "sha1-sH/y2aXYi+yAYDWJWiurZqJ5iLw=", + "dev": true + }, + "p-locate": { + "version": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true + }, + "pako": { + "version": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=", + "dev": true + }, + "parents": { + "version": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", + "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=", + "dev": true + }, + "parse-asn1": { + "version": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz", + "integrity": "sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=", + "dev": true + }, + "parse-json": { + "version": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true + }, + "path-browserify": { + "version": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", + "dev": true + }, + "path-exists": { + "version": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true + }, + "path-is-absolute": { + "version": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-parse": { + "version": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "path-platform": { + "version": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", + "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=", + "dev": true + }, + "path-type": { + "version": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true + }, + "pbkdf2": { + "version": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.12.tgz", + "integrity": "sha1-vjZ4XFBn6kjYBv+SMojF91C2uKI=", + "dev": true + }, + "pff": { + "version": "https://registry.npmjs.org/pff/-/pff-1.0.0.tgz", + "integrity": "sha1-6l8J7mVxyuKSp4/CgJBaOGVmjng=", + "dev": true + }, + "pify": { + "version": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true + }, + "pkg-dir": { + "version": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "dev": true + }, + "pluralize": { + "version": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", + "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", + "dev": true + }, + "prelude-ls": { + "version": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "private": { + "version": "https://registry.npmjs.org/private/-/private-0.1.7.tgz", + "integrity": "sha1-aM5eih7woju1cMwoU3tTMqumPvE=", + "dev": true + }, + "process": { + "version": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "progress": { + "version": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + }, + "proxyquireify": { + "version": "https://registry.npmjs.org/proxyquireify/-/proxyquireify-3.2.1.tgz", + "integrity": "sha1-Fb7hATYKzJHc2G7k2aRF+Klx7qA=", + "dev": true, + "dependencies": { + "acorn": { + "version": "https://registry.npmjs.org/acorn/-/acorn-1.2.2.tgz", + "integrity": "sha1-yM4n3grMdtiW0rH6099YjZ6C8BQ=", + "dev": true + }, + "detective": { + "version": "https://registry.npmjs.org/detective/-/detective-4.1.1.tgz", + "integrity": "sha1-nEusHp+4uzT38YyuCA6h0Dr/LNo=", + "dev": true + }, + "through": { + "version": "https://registry.npmjs.org/through/-/through-2.2.7.tgz", + "integrity": "sha1-bo4hIAGR1OtqmfbwEN9Gqhxusr0=", + "dev": true + }, + "xtend": { + "version": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", + "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=", + "dev": true + } + } + }, + "public-encrypt": { + "version": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz", + "integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=", + "dev": true + }, + "punycode": { + "version": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "querystring": { + "version": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "randombytes": { + "version": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.4.tgz", + "integrity": "sha1-lVHfIIQiyPgOtY4jJt0LhA/yLv0=", + "dev": true + }, + "read-only-stream": { + "version": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", + "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=", + "dev": true + }, + "read-pkg": { + "version": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true + }, + "read-pkg-up": { + "version": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "dependencies": { + "find-up": { + "version": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true + } + } + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.10.tgz", + "integrity": "sha1-7/5yu3yITA3TNeI3nVJhltnQEe4=", + "dev": true, + "dependencies": { + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz", + "integrity": "sha1-YuIA8DmVWmgQ2N8KM//A8BNmLZg=", + "dev": true + } + } + }, + "readline2": { + "version": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", + "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", + "dev": true + }, + "rechoir": { + "version": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true + }, + "regenerate": { + "version": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.2.tgz", + "integrity": "sha1-0ZQcZ7rUN+G+dkM63Vs4X5WxkmA=", + "dev": true + }, + "regenerator-runtime": { + "version": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", + "dev": true + }, + "regenerator-transform": { + "version": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.9.11.tgz", + "integrity": "sha1-On0GdSDLe3F2dp61/4aGkb7+EoM=", + "dev": true + }, + "regexpu-core": { + "version": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", + "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", + "dev": true + }, + "regjsgen": { + "version": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true + }, + "repeating": { + "version": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true + }, + "require-deps": { + "version": "https://registry.npmjs.org/require-deps/-/require-deps-1.0.1.tgz", + "integrity": "sha1-JBXPScNb02pdMXc5UQjT8jcgUmM=", + "dev": true + }, + "require-uncached": { + "version": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true + }, + "resolve": { + "version": "https://registry.npmjs.org/resolve/-/resolve-1.3.3.tgz", + "integrity": "sha1-ZVkHw0aahoDcLeOidaj91paR8OU=", + "dev": true + }, + "resolve-from": { + "version": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + }, + "restore-cursor": { + "version": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true + }, + "rimraf": { + "version": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", + "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", + "dev": true + }, + "ripemd160": { + "version": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", + "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=", + "dev": true + }, + "run-async": { + "version": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", + "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", + "dev": true + }, + "rx-lite": { + "version": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", + "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", + "dev": true + }, + "safe-buffer": { + "version": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", + "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=", + "dev": true + }, + "samsam": { + "version": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz", + "integrity": "sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc=" + }, + "select": { + "version": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=" + }, + "semver": { + "version": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + }, + "sha.js": { + "version": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.8.tgz", + "integrity": "sha1-NwaMLEdra69ALRSknGf1l5IfY08=", + "dev": true + }, + "shasum": { + "version": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", + "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=", + "dev": true + }, + "shell-quote": { + "version": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", + "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "dev": true + }, + "shelljs": { + "version": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.7.tgz", + "integrity": "sha1-svXHfvlxSPS09uImguELuoZnz/E=", + "dev": true + }, + "sinon": { + "version": "https://registry.npmjs.org/sinon/-/sinon-1.17.7.tgz", + "integrity": "sha1-RUKk9JugxFwF6y6d2dID4rjv4L8=" + }, + "slash": { + "version": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "slice-ansi": { + "version": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true + }, + "source-map": { + "version": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", + "dev": true + }, + "source-map-support": { + "version": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.15.tgz", + "integrity": "sha1-AyAt9lwG0r2MfsI2KhkwVv7407E=", + "dev": true + }, + "spdx-correct": { + "version": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "dev": true + }, + "spdx-expression-parse": { + "version": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=", + "dev": true + }, + "spdx-license-ids": { + "version": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", + "dev": true + }, + "sprintf-js": { + "version": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "stream-browserify": { + "version": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "dev": true + }, + "stream-combiner2": { + "version": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", + "dev": true + }, + "stream-http": { + "version": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.1.tgz", + "integrity": "sha1-VGpRdBrVprB+njGwsQRBqRffUoo=", + "dev": true + }, + "stream-splicer": { + "version": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.0.tgz", + "integrity": "sha1-G2O+Q4oTPktnHMGTUZdgAXWRDYM=", + "dev": true + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "string-width": { + "version": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true + }, + "strip-ansi": { + "version": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true + }, + "strip-bom": { + "version": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-json-comments": { + "version": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "stround": { + "version": "https://registry.npmjs.org/stround/-/stround-0.3.1.tgz", + "integrity": "sha1-vl8uHdf1tqFGhoJUish0YbjWZFQ=" + }, + "subarg": { + "version": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", + "dev": true, + "dependencies": { + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "supports-color": { + "version": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "syntax-error": { + "version": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.3.0.tgz", + "integrity": "sha1-HtkmbE1AvnXcVb+bsct3Biu5bKE=", + "dev": true + }, + "tabbable": { + "version": "https://registry.npmjs.org/tabbable/-/tabbable-1.0.6.tgz", + "integrity": "sha1-fCaofqb0ol7fXtthl0WgrnQHJPw=" + }, + "table": { + "version": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", + "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", + "dev": true, + "dependencies": { + "is-fullwidth-code-point": { + "version": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "https://registry.npmjs.org/string-width/-/string-width-2.0.0.tgz", + "integrity": "sha1-Y1xUNsxypuDDh87KJ41OLuxSaH4=", + "dev": true + } + } + }, + "text-table": { + "version": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "dev": true + }, + "timers-browserify": { + "version": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", + "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", + "dev": true + }, + "tiny-emitter": { + "version": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.0.tgz", + "integrity": "sha1-utMnrbGAS0KiMa+nQVMr2ITNCa0=" + }, + "to-arraybuffer": { + "version": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "trim-right": { + "version": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "tryit": { + "version": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz", + "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=", + "dev": true + }, + "tty-browserify": { + "version": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "type-check": { + "version": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true + }, + "type-detect": { + "version": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", + "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", + "dev": true + }, + "typedarray": { + "version": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "umd": { + "version": "https://registry.npmjs.org/umd/-/umd-3.0.1.tgz", + "integrity": "sha1-iuVW4RAR9jwllnCKiDclnwGz1g4=", + "dev": true + }, + "url": { + "version": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "dependencies": { + "punycode": { + "version": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "user-home": { + "version": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", + "dev": true + }, + "util": { + "version": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=" + }, + "util-deprecate": { + "version": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "validate-npm-package-license": { + "version": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "dev": true + }, + "vm-browserify": { + "version": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "dev": true + }, + "wordwrap": { + "version": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrappy": { + "version": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true + }, + "xtend": { + "version": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "zxcvbn": { + "version": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz", + "integrity": "sha1-KOwXzwl0PtyrBW3dixsGJizHPDA=" + } + } +} diff --git a/spec/features/visitors/i18n_spec.rb b/spec/features/visitors/i18n_spec.rb index e86cf30231c..12f3a72f814 100644 --- a/spec/features/visitors/i18n_spec.rb +++ b/spec/features/visitors/i18n_spec.rb @@ -1,7 +1,14 @@ require 'rails_helper' feature 'Internationalization' do - context 'visit homepage' do + context 'visit homepage with no locale set' do + it 'displays a header in the default locale' do + visit root_path + expect(page).to have_content t('headings.sign_in_without_sp', locale: 'en') + end + end + + context 'visit homepage with locale set in header' do before do page.driver.header 'Accept-Language', locale visit root_path @@ -22,5 +29,33 @@ expect(page).to have_content t('headings.sign_in_without_sp', locale: 'es') end end + + context 'when the user selects an unsupported locale' do + let(:locale) { :es } + + it 'it does not raise an exception' do + expect { visit root_path + '?locale=foo' }.to_not raise_exception + end + + it 'it falls back to the locale set in header' do + expect(page).to have_content t('headings.sign_in_without_sp', locale: 'es') + end + end + end + + context 'visit homepage without a locale param set' do + it 'displays header in the default locale' do + visit '/' + + expect(page).to have_content t('headings.sign_in_without_sp', locale: 'en') + end + end + + context 'visit homepage with locale param set to :es' do + it 'displays a translated header to the user' do + visit '/es/' + + expect(page).to have_content t('headings.sign_in_without_sp', locale: 'es') + end end end diff --git a/spec/services/marketing_site_spec.rb b/spec/services/marketing_site_spec.rb index bd72aca8384..80127eac095 100644 --- a/spec/services/marketing_site_spec.rb +++ b/spec/services/marketing_site_spec.rb @@ -3,7 +3,15 @@ RSpec.describe MarketingSite do describe '.base_url' do it 'points to the base URL' do - expect(MarketingSite.base_url).to eq('https://www.login.gov') + expect(MarketingSite.base_url).to eq('https://www.login.gov/') + end + + context 'when the user has set their locale to :es' do + before { I18n.locale = :es } + + it 'points to the base URL with the locale appended' do + expect(MarketingSite.base_url).to eq('https://www.login.gov/es/') + end end end @@ -11,18 +19,42 @@ it 'points to the privacy page' do expect(MarketingSite.privacy_url).to eq('https://www.login.gov/policy') end + + context 'when the user has set their locale to :es' do + before { I18n.locale = :es } + + it 'points to the privacy page with the locale appended' do + expect(MarketingSite.privacy_url).to eq('https://www.login.gov/es/policy') + end + end end describe '.contact_url' do it 'points to the contact page' do expect(MarketingSite.contact_url).to eq('https://www.login.gov/contact') end + + context 'when the user has set their locale to :es' do + before { I18n.locale = :es } + + it 'points to the contact page with the locale appended' do + expect(MarketingSite.contact_url).to eq('https://www.login.gov/es/contact') + end + end end describe '.help_url' do it 'points to the help page' do expect(MarketingSite.help_url).to eq('https://www.login.gov/help') end + + context 'when the user has set their locale to :es' do + before { I18n.locale = :es } + + it 'points to the help page with the locale appended' do + expect(MarketingSite.help_url).to eq('https://www.login.gov/es/help') + end + end end describe '.help_authenticator_app_url' do @@ -31,5 +63,15 @@ 'https://www.login.gov/help/signing-in/what-is-an-authenticator-app/' ) end + + context 'when the user has set their locale to :es' do + before { I18n.locale = :es } + + it 'points to the authenticator app section of the help page with the locale appended' do + expect(MarketingSite.help_authenticator_app_url).to eq( + 'https://www.login.gov/es/help/signing-in/what-is-an-authenticator-app/' + ) + end + end end end From 0ff17d2eeabc24bfc9d089299b5cf78cd494c882 Mon Sep 17 00:00:00 2001 From: Zach Margolis Date: Wed, 28 Jun 2017 17:29:51 -0400 Subject: [PATCH 06/22] Extract Idv::ProoferValidator#result **Why**: Extracting this into a serializable object will make it easier to refactor these classes into background jobs. --- .reek | 2 + app/services/idv/financials_step.rb | 2 +- app/services/idv/phone_step.rb | 2 +- app/services/idv/profile_step.rb | 6 +- app/services/idv/profile_validator.rb | 6 -- app/services/idv/step.rb | 20 +++++- app/services/idv/vendor_result.rb | 26 ++++++++ app/services/idv/vendor_validator.rb | 9 +-- .../services/idv/financials_validator_spec.rb | 13 ++-- spec/services/idv/phone_validator_spec.rb | 12 ++-- spec/services/idv/vendor_result_spec.rb | 62 +++++++++++++++++++ 11 files changed, 125 insertions(+), 35 deletions(-) create mode 100644 app/services/idv/vendor_result.rb create mode 100644 spec/services/idv/vendor_result_spec.rb diff --git a/.reek b/.reek index 37bd18db79d..e7777a0c7b0 100644 --- a/.reek +++ b/.reek @@ -24,6 +24,7 @@ FeatureEnvy: - Pii::Attributes#[]= - OpenidConnectLogoutForm#load_identity - Idv::ProfileMaker#pii_from_applicant + - Idv::Step#vendor_validator_result InstanceVariableAssumption: exclude: - User @@ -53,6 +54,7 @@ TooManyInstanceVariables: exclude: - OpenidConnectAuthorizeForm - OpenidConnectRedirector + - Idv::VendorResult TooManyStatements: max_statements: 6 exclude: diff --git a/app/services/idv/financials_step.rb b/app/services/idv/financials_step.rb index cffa4264dd0..5dc601c25ef 100644 --- a/app/services/idv/financials_step.rb +++ b/app/services/idv/financials_step.rb @@ -30,7 +30,7 @@ def vendor_validator_class end def vendor_reasons - vendor_validator.reasons if form_valid? + vendor_validator_result.reasons if form_valid? end def vendor_params diff --git a/app/services/idv/phone_step.rb b/app/services/idv/phone_step.rb index 252b6f86f5d..c0973a7cec6 100644 --- a/app/services/idv/phone_step.rb +++ b/app/services/idv/phone_step.rb @@ -29,7 +29,7 @@ def vendor_params end def vendor_reasons - vendor_validator.reasons if form_valid? + vendor_validator_result.reasons if form_valid? end def update_idv_session diff --git a/app/services/idv/profile_step.rb b/app/services/idv/profile_step.rb index 3e92d2314e6..4f7ff1c51cf 100644 --- a/app/services/idv/profile_step.rb +++ b/app/services/idv/profile_step.rb @@ -63,8 +63,8 @@ def vendor_validator_class def update_idv_session idv_session.profile_confirmation = true - idv_session.vendor_session_id = vendor_validator.session_id - idv_session.normalized_applicant_params = vendor_validator.normalized_applicant.to_hash + idv_session.vendor_session_id = vendor_validator_result.session_id + idv_session.normalized_applicant_params = vendor_validator_result.normalized_applicant.to_hash idv_session.resolution_successful = true end @@ -76,7 +76,7 @@ def extra_analytics_attributes end def vendor_reasons - vendor_validator.reasons if form_valid? + vendor_validator_result.reasons if form_valid? end end end diff --git a/app/services/idv/profile_validator.rb b/app/services/idv/profile_validator.rb index eb1cd5ce051..f3c72a064e3 100644 --- a/app/services/idv/profile_validator.rb +++ b/app/services/idv/profile_validator.rb @@ -1,15 +1,9 @@ module Idv class ProfileValidator < VendorValidator - delegate :session_id, to: :result - def result @_result ||= try_start end - def normalized_applicant - result.vendor_resp.normalized_applicant - end - private def try_start diff --git a/app/services/idv/step.rb b/app/services/idv/step.rb index e77d5a1c268..b3c9cfbd7ae 100644 --- a/app/services/idv/step.rb +++ b/app/services/idv/step.rb @@ -21,7 +21,23 @@ def form_validate(params) end def vendor_validation_passed? - vendor_validator.success? + vendor_validator_result.success? + end + + def vendor_validator_result + @_vendor_validator_result ||= extract_vendor_result(vendor_validator.result) + end + + def extract_vendor_result(result) + vendor_resp = result.vendor_resp + + Idv::VendorResult.new( + success: result.success?, + errors: result.errors, + reasons: vendor_resp.reasons, + normalized_applicant: vendor_resp.try(:normalized_applicant), + session_id: result.try(:session_id) + ) end def errors @@ -42,7 +58,7 @@ def idv_vendor end def vendor_errors - @_vendor_errors ||= vendor_validator.errors + @_vendor_errors ||= vendor_validator_result.errors end def vendor_validator diff --git a/app/services/idv/vendor_result.rb b/app/services/idv/vendor_result.rb new file mode 100644 index 00000000000..87db473181d --- /dev/null +++ b/app/services/idv/vendor_result.rb @@ -0,0 +1,26 @@ +module Idv + class VendorResult + attr_reader :success, :errors, :reasons, :session_id, :normalized_applicant + + def self.new_from_json(json) + parsed = JSON.parse(json, symbolize_names: true) + + applicant = parsed[:normalized_applicant] + parsed[:normalized_applicant] = Proofer::Applicant.new(applicant) if applicant + + new(**parsed) + end + + def initialize(success:, errors:, reasons:, session_id:, normalized_applicant:) + @success = success + @errors = errors + @reasons = reasons + @session_id = session_id + @normalized_applicant = normalized_applicant + end + + def success? + success + end + end +end diff --git a/app/services/idv/vendor_validator.rb b/app/services/idv/vendor_validator.rb index e7888057195..8b0c6222101 100644 --- a/app/services/idv/vendor_validator.rb +++ b/app/services/idv/vendor_validator.rb @@ -1,7 +1,6 @@ # abstract base class for proofing vendor validation module Idv class VendorValidator - delegate :success?, :errors, to: :result attr_reader :applicant, :vendor, :vendor_params, :vendor_session_id def initialize(applicant:, vendor:, vendor_params:, vendor_session_id:) @@ -11,8 +10,8 @@ def initialize(applicant:, vendor:, vendor_params:, vendor_session_id:) @vendor_session_id = vendor_session_id end - def reasons - result.vendor_resp.reasons + def result + @_result ||= try_submit end private @@ -24,10 +23,6 @@ def idv_agent ) end - def result - @_result ||= try_submit - end - def try_agent_action yield rescue => err diff --git a/spec/services/idv/financials_validator_spec.rb b/spec/services/idv/financials_validator_spec.rb index 04a6e910dac..2010f5bd8bb 100644 --- a/spec/services/idv/financials_validator_spec.rb +++ b/spec/services/idv/financials_validator_spec.rb @@ -31,27 +31,24 @@ def stub_agent_calls with(params, vendor_session_id).and_return(confirmation) end - describe '#success?' do - it 'returns Proofer::Confirmation#success?' do + describe '#result' do + it 'has success' do stub_agent_calls success_string = 'true' - expect(confirmation).to receive(:success?).and_return(success_string) - expect(subject.success?).to eq success_string + expect(subject.result.success?).to eq success_string end - end - describe '#error' do - it 'returns Proofer::Confirmation#errors' do + it 'has errors' do stub_agent_calls error_string = 'mucho errors' expect(confirmation).to receive(:errors).and_return(error_string) - expect(subject.errors).to eq error_string + expect(subject.result.errors).to eq error_string end end end diff --git a/spec/services/idv/phone_validator_spec.rb b/spec/services/idv/phone_validator_spec.rb index 74dcf9debce..faa6755c778 100644 --- a/spec/services/idv/phone_validator_spec.rb +++ b/spec/services/idv/phone_validator_spec.rb @@ -31,27 +31,25 @@ def stub_agent_calls with(params, vendor_session_id).and_return(confirmation) end - describe '#success?' do - it 'returns Proofer::Confirmation#success?' do + describe '#result' do + it 'has success' do stub_agent_calls success_string = 'true' expect(confirmation).to receive(:success?).and_return(success_string) - expect(subject.success?).to eq success_string + expect(subject.result.success?).to eq success_string end - end - describe '#error' do - it 'returns Proofer::Confirmation#errors' do + it 'has errors' do stub_agent_calls error_string = 'mucho errors' expect(confirmation).to receive(:errors).and_return(error_string) - expect(subject.errors).to eq error_string + expect(subject.result.errors).to eq error_string end end end diff --git a/spec/services/idv/vendor_result_spec.rb b/spec/services/idv/vendor_result_spec.rb new file mode 100644 index 00000000000..9dce4b8f52d --- /dev/null +++ b/spec/services/idv/vendor_result_spec.rb @@ -0,0 +1,62 @@ +require 'rails_helper' + +RSpec.describe Idv::VendorResult do + let(:success) { true } + let(:errors) { { foo: ['is not valid'] } } + let(:reasons) { %w[foo bar baz] } + let(:session_id) { SecureRandom.uuid } + let(:normalized_applicant) do + Proofer::Applicant.new( + last_name: 'Ever', + first_name: 'Greatest' + ) + end + + subject(:vendor_result) do + Idv::VendorResult.new( + success: success, + errors: errors, + reasons: reasons, + session_id: session_id, + normalized_applicant: normalized_applicant + ) + end + + describe '#success?' do + it 'is the success value' do + expect(vendor_result.success?).to eq(success) + end + end + + describe '#to_json' do + it 'serializes normalized_applicant correctly' do + json = vendor_result.to_json + + parsed = JSON.parse(json, symbolize_names: true) + expect(parsed[:normalized_applicant][:last_name]).to eq(normalized_applicant.last_name) + end + end + + describe '.new_from_json' do + subject(:new_from_json) { Idv::VendorResult.new_from_json(vendor_result.to_json) } + + it 'has simple attributes' do + expect(new_from_json.success?).to eq(vendor_result.success?) + expect(new_from_json.errors).to eq(vendor_result.errors) + expect(new_from_json.reasons).to eq(vendor_result.reasons) + expect(new_from_json.session_id).to eq(vendor_result.session_id) + end + + it 'turns applicant into a full object' do + expect(new_from_json.normalized_applicant.last_name).to eq(normalized_applicant.last_name) + end + + context 'without an applicant' do + let(:normalized_applicant) { nil } + + it 'does not have an applicant' do + expect(new_from_json.normalized_applicant).to eq(nil) + end + end + end +end From 19fcc9d487e2e748299f87b7de8e27bb7901a92e Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Wed, 28 Jun 2017 20:22:16 -0400 Subject: [PATCH 07/22] Make otp_rate_limiter_spec more robust **Why**: We were comparing datetimes without freezing Time.zone.now, leading to flickering specs. **How**: Instead of comparing `otp_last_sent_at` with the current time, check that the current `otp_last_sent_at` is greater than the previous one. --- app/services/otp_rate_limiter.rb | 4 +--- spec/services/otp_rate_limiter_spec.rb | 8 +++++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/services/otp_rate_limiter.rb b/app/services/otp_rate_limiter.rb index 1fb7bc29dcf..b0287467a20 100644 --- a/app/services/otp_rate_limiter.rb +++ b/app/services/otp_rate_limiter.rb @@ -32,10 +32,8 @@ def lock_out_user end def increment - now = Time.zone.now - entry_for_current_phone.otp_send_count += 1 - entry_for_current_phone.otp_last_sent_at = now + entry_for_current_phone.otp_last_sent_at = Time.zone.now entry_for_current_phone.save! end diff --git a/spec/services/otp_rate_limiter_spec.rb b/spec/services/otp_rate_limiter_spec.rb index bdf838dcac8..f2e6b2eb349 100644 --- a/spec/services/otp_rate_limiter_spec.rb +++ b/spec/services/otp_rate_limiter_spec.rb @@ -23,11 +23,13 @@ end describe '#increment' do - it 'sets the otp_last_sent_at' do - now = Time.zone.now + it 'updates otp_last_sent_at' do + tracker = OtpRequestsTracker.find_or_create_with_phone(current_user.phone) + old_otp_last_sent_at = tracker.reload.otp_last_sent_at otp_rate_limiter.increment + new_otp_last_sent_at = tracker.reload.otp_last_sent_at - expect(rate_limited_phone.otp_last_sent_at.to_i).to eq(now.to_i) + expect(new_otp_last_sent_at).to be > old_otp_last_sent_at end it 'increments the otp_send_count' do From 1ca179dac0561398e17b674d1858909d8a83ac35 Mon Sep 17 00:00:00 2001 From: Zach Margolis Date: Thu, 29 Jun 2017 17:01:19 -0400 Subject: [PATCH 08/22] Add CBP GOES production config **Why**: To enable rollout --- certs/sp/cbp_goes_prod.crt | 45 ++++++++++++++++++++++++++++++++++++ config/service_providers.yml | 10 ++++++++ 2 files changed, 55 insertions(+) create mode 100644 certs/sp/cbp_goes_prod.crt diff --git a/certs/sp/cbp_goes_prod.crt b/certs/sp/cbp_goes_prod.crt new file mode 100644 index 00000000000..5bf2246006b --- /dev/null +++ b/certs/sp/cbp_goes_prod.crt @@ -0,0 +1,45 @@ +-----BEGIN CERTIFICATE----- +MIIH8DCCBtigAwIBAgIEWRoIeDANBgkqhkiG9w0BAQsFADCBhzELMAkGA1UEBhMC +VVMxGDAWBgNVBAoTD1UuUy4gR292ZXJubWVudDEoMCYGA1UECxMfRGVwYXJ0bWVu +dCBvZiBIb21lbGFuZCBTZWN1cml0eTEiMCAGA1UECxMZQ2VydGlmaWNhdGlvbiBB +dXRob3JpdGllczEQMA4GA1UECxMHREhTIENBNDAeFw0xNzA2MjQxNDQ2MTlaFw0y +MDA2MjQxNTE2MTlaMIGPMQswCQYDVQQGEwJVUzEYMBYGA1UEChMPVS5TLiBHb3Zl +cm5tZW50MSgwJgYDVQQLEx9EZXBhcnRtZW50IG9mIEhvbWVsYW5kIFNlY3VyaXR5 +MQwwCgYDVQQLEwNDQlAxEDAOBgNVBAsTB0RldmljZXMxHDAaBgNVBAMTE3R0cC1h +cHAuY2JwLmRocy5nb3YwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDP +ubSOea85FVVr9Adn+mez7LjuPBs3aeMCEK9gM9xnV2tGFJra1Mh9ef/jnnO2ANur +jtRovws/ea/k+J54ngIv7ZXCZGUvZZoOtyFqX7Mjnj8gFvIrFCVAD4a/FySSiGNo +Z7+X/ypX1rFb8CNFSi/SxU+zH61ZS0i+OAZ2xAk3Gv+OkZ4DHRHXsGJn8rCJ2O1N +c/OyKpBOpkS5EjTAPw3OqD/U8CU9Hl6QbK52ovxFLgtkHGWv37dLc0Qojwa8Lqaa +FAxjWPyND2oo4aDfGtu7YtbGk0zRf97QNvfoqjkGYAaUK0ozCTMKZEVuXknhDtvy +a6C26kDVYA8RUn9RtFjNAgMBAAGjggRYMIIEVDAOBgNVHQ8BAf8EBAMCBaAwFwYD +VR0gBBAwDjAMBgpghkgBZQMCAQMIMIIBGwYIKwYBBQUHAQEEggENMIIBCTA0Bggr +BgEFBQcwAoYoaHR0cDovL3BraS5kaW1jLmRocy5nb3YvZGhzY2FfZWVfYWlhLnA3 +YzCBqgYIKwYBBQUHMAKGgZ1sZGFwOi8vbGRhcDAxLmRpbWMuZGhzLmdvdi9vdT1E +SFMlMjBDQTQsb3U9Q2VydGlmaWNhdGlvbiUyMEF1dGhvcml0aWVzLG91PURlcGFy +dG1lbnQlMjBvZiUyMEhvbWVsYW5kJTIwU2VjdXJpdHksbz1VLlMuJTIwR292ZXJu +bWVudCxjPVVTP2NBQ2VydGlmaWNhdGU7YmluYXJ5MCQGCCsGAQUFBzABhhhodHRw +Oi8vb2NzcC5kaW1jLmRocy5nb3YwEwYDVR0lBAwwCgYIKwYBBQUHAwEwggEBBgNV +HREEgfkwgfaCE3R0cC1hcHAuY2JwLmRocy5nb3aCGHR0cC1pbnRlcm5hbC5jYnAu +ZGhzLmdvdoIcdHRwLWludGVybmFsLWFwcC5jYnAuZGhzLmdvdoIZdHRwLXNjaGVk +dWxlci5jYnAuZGhzLmdvdoIddHRwLXNjaGVkdWxlci1hcHAuY2JwLmRocy5nb3aC +F3R0cC1jcmVkc3ZjLmNicC5kaHMuZ292ght0dHAtY3JlZHN2Yy1hcHAuY2JwLmRo +cy5nb3aCF3R0cC1hZHMtYXBwLmNicC5kaHMuZ292gh50dHAtc2FwYWRhcHRlci1h +cHAuY2JwLmRocy5nb3YwggGTBgNVHR8EggGKMIIBhjCCAVegggFToIIBT6SBnDCB +mTELMAkGA1UEBhMCVVMxGDAWBgNVBAoTD1UuUy4gR292ZXJubWVudDEoMCYGA1UE +CxMfRGVwYXJ0bWVudCBvZiBIb21lbGFuZCBTZWN1cml0eTEiMCAGA1UECxMZQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdGllczEQMA4GA1UECxMHREhTIENBNDEQMA4GA1UE +AxMHQ1JMNTM5MYaBrWxkYXA6Ly9sZGFwMDEuZGltYy5kaHMuZ292L2NuPUNSTDUz +OTEsb3U9REhTJTIwQ0E0LG91PUNlcnRpZmljYXRpb24lMjBBdXRob3JpdGllcyxv +dT1EZXBhcnRtZW50JTIwb2YlMjBIb21lbGFuZCUyMFNlY3VyaXR5LG89VS5TLiUy +MEdvdmVybm1lbnQsYz1VUz9jZXJ0aWZpY2F0ZVJldm9jYXRpb25MaXN0MCmgJ6Al +hiNodHRwOi8vcGtpLmRpbWMuZGhzLmdvdi9ESFNfQ0EyLmNybDAfBgNVHSMEGDAW +gBR8w0pcuh82q4NRffTg5Q6QfxwTQTAdBgNVHQ4EFgQUIWVxsBQWw5oIGgPYwrvG +o7c35EowGQYJKoZIhvZ9B0EABAwwChsEVjguMQMCA6gwDQYJKoZIhvcNAQELBQAD +ggEBAG3uUeSo89FY4HcDGGhO9mBjXuk7HRHGLnY8fAdqJoT1gA/oTHoV/fgw8QZu +D3X9FLgpEjQz43iHFrv/ps2tYwgSKiiovoeYyb68s5X9NuG5kNnn0dGp1TjnNlGX +9lQSBr4CReaJpCtSAPbOtuZ+8b7MA7ODnMg/1u1qNKovIqMV6eIq5vrD/+MeSG61 ++c+MI6Gita5j6J6lzZqqwtAltfMFetc1Qkl2tjqdpce9u60Ch7yuVR3Xh1SEhaAa +jRJsbOuqNAfC038A6ASv3nxGao8AYUHt2aR9chXApKfBzfx6VYW0462UPqzeRi5l +RCa8sud1bb1pFRJv8LWiqOC/9yw= +-----END CERTIFICATE----- diff --git a/config/service_providers.yml b/config/service_providers.yml index e61ac94bdde..6a19af7b1ba 100644 --- a/config/service_providers.yml +++ b/config/service_providers.yml @@ -387,3 +387,13 @@ production: cert: 'cbp_goes_pre_prod' redirect_uris: - 'http://10.156.152.27/login' + + 'urn:gov:dhs.cbp.jobs:openidconnect:aws-cbp-ttp': + agency: 'DHS' + allow_on_prod_chef_env: 'true' + block_encryption: 'aes256-cbc' + cert: 'cbp_goes_prod' + friendly_name: 'CBP Trusted Traveler Programs' + logo: 'cbp.png' + redirect_uris: + - 'https://ttp.cbp.dhs.gov' From fd64256841004c0745e214d82aab2215da5e8092 Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Thu, 29 Jun 2017 15:57:51 -0400 Subject: [PATCH 09/22] Improve test coverage for OtpRequestsTracker **Why**: The retry and raise portion of `find_or_create_with_phone` was not tested. --- spec/models/otp_requests_tracker_spec.rb | 51 ++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 spec/models/otp_requests_tracker_spec.rb diff --git a/spec/models/otp_requests_tracker_spec.rb b/spec/models/otp_requests_tracker_spec.rb new file mode 100644 index 00000000000..21c0f8fdde1 --- /dev/null +++ b/spec/models/otp_requests_tracker_spec.rb @@ -0,0 +1,51 @@ +require 'rails_helper' + +describe OtpRequestsTracker do + describe '.find_or_create_with_phone' do + let(:phone) { '+1 703 555 1212' } + let(:phone_fingerprint) { Pii::Fingerprinter.fingerprint(phone) } + + context 'match found' do + it 'returns the existing record and does not change it' do + OtpRequestsTracker.create( + phone_fingerprint: phone_fingerprint, + otp_send_count: 3, + otp_last_sent_at: Time.zone.now - 1.hour + ) + + existing = OtpRequestsTracker.where(phone_fingerprint: phone_fingerprint).first + + expect { OtpRequestsTracker.find_or_create_with_phone(phone) }. + to_not change(OtpRequestsTracker, :count) + expect { OtpRequestsTracker.find_or_create_with_phone(phone) }. + to_not change { existing.otp_send_count } + expect { OtpRequestsTracker.find_or_create_with_phone(phone) }. + to_not change { existing.otp_last_sent_at } + end + end + + context 'match not found' do + it 'creates new record with otp_send_count = 0 and otp_last_sent_at = current time' do + expect { OtpRequestsTracker.find_or_create_with_phone(phone) }. + to change(OtpRequestsTracker, :count).by(1) + + existing = OtpRequestsTracker.where(phone_fingerprint: phone_fingerprint).first + + expect(existing.otp_send_count).to eq 0 + expect(existing.otp_last_sent_at).to be_within(2.seconds).of(Time.zone.now) + end + end + + context 'race condition' do + it 'retries once, then raises ActiveRecord::RecordNotUnique' do + tracker = OtpRequestsTracker.new + allow(OtpRequestsTracker).to receive(:where). + and_raise(ActiveRecord::RecordNotUnique.new(tracker)) + + expect(OtpRequestsTracker).to receive(:where).exactly(:once) + expect { OtpRequestsTracker.find_or_create_with_phone(phone) }. + to raise_error ActiveRecord::RecordNotUnique + end + end + end +end From 2c636fb8d8bc2a06d8f5e77f5a6037b01e0c8fef Mon Sep 17 00:00:00 2001 From: Nick Bristow Date: Thu, 29 Jun 2017 17:32:58 -0400 Subject: [PATCH 10/22] Add aria-hidden to accordian content div **Why**: This tells the screen reader the content in this div is hidden. When the accordian is expanded, aria- hidden is changed to false, letting the screen reader know there is new content on screen. --- app/assets/javascripts/app/components/accordion.js | 2 ++ app/views/shared/_accordion.html.slim | 1 + 2 files changed, 3 insertions(+) diff --git a/app/assets/javascripts/app/components/accordion.js b/app/assets/javascripts/app/components/accordion.js index 2724585c6e5..14f5da6239d 100644 --- a/app/assets/javascripts/app/components/accordion.js +++ b/app/assets/javascripts/app/components/accordion.js @@ -72,6 +72,7 @@ class Accordion extends Events { this.content.classList.add('shown'); this.content.classList.remove('animate-out'); this.content.classList.add('animate-in'); + this.content.setAttribute('aria-hidden', 'false'); this.emit('accordion.show'); } @@ -81,6 +82,7 @@ class Accordion extends Events { this.shownIcon.classList.add('display-none'); this.content.classList.remove('animate-in'); this.content.classList.add('animate-out'); + this.content.setAttribute('aria-hidden', 'true'); this.emit('accordion.hide'); this.header.focus(); } diff --git a/app/views/shared/_accordion.html.slim b/app/views/shared/_accordion.html.slim index b36a57c2bab..e42dfed41b3 100644 --- a/app/views/shared/_accordion.html.slim +++ b/app/views/shared/_accordion.html.slim @@ -18,6 +18,7 @@ .accordion-content.clearfix.pt1( id="#{target_id}" role="region" + aria-hidden="true" ) .px2 = yield From 5ab507b7d4eea60672ae98f1da86e1461b5b78ae Mon Sep 17 00:00:00 2001 From: Brian Hedberg Date: Fri, 30 Jun 2017 00:53:28 -0500 Subject: [PATCH 11/22] Prevent Verify by mail flow redirect bug **Why** The situation As @andrewhughey had stated in 18F/identity-private#1890 (comment), here is the flow that this PR is fixing: create account (email, password, 2FA) verify identity (person info, financial info) choose to confirm address by mail send letter see profile / flash message that letter was sent sign out sign in asked for code from letter but oh no! I never got a letter choose send new letter expected: send new letter screen actual: restart identity verification **How** This PR adds logic to prevent the user from being redirected back to identity verification, sending them to the "Send another letter" confirmation screen instead, as expected. The faulty redirect was happening because the a request to send more mail was triggering the IdvSession service, which redirects to verification if verification hasn't been completed. To fix this, we added an exception that prevents that redirect if a user has mail already sent mail. For issue: 18F/identity-private#1890 --- app/controllers/concerns/idv_session.rb | 1 + app/controllers/verify/usps_controller.rb | 5 +-- app/decorators/usps_decorator.rb | 8 ++--- app/services/idv/usps_mail.rb | 4 +++ app/views/verify/usps/index.html.slim | 4 +-- spec/decorators/usps_decorator_spec.rb | 21 ++++-------- spec/features/saml/loa3_sso_spec.rb | 34 +++++++++++++++++++ .../views/verify/usps/index.html.slim_spec.rb | 18 ++++++++++ 8 files changed, 71 insertions(+), 24 deletions(-) create mode 100644 spec/views/verify/usps/index.html.slim_spec.rb diff --git a/app/controllers/concerns/idv_session.rb b/app/controllers/concerns/idv_session.rb index 271de8555f2..819eff862ec 100644 --- a/app/controllers/concerns/idv_session.rb +++ b/app/controllers/concerns/idv_session.rb @@ -2,6 +2,7 @@ module IdvSession extend ActiveSupport::Concern def confirm_idv_session_started + return if current_user.decorate.needs_profile_usps_verification? redirect_to verify_session_url if idv_session.params.blank? end diff --git a/app/controllers/verify/usps_controller.rb b/app/controllers/verify/usps_controller.rb index 5a72c9b68b5..bc850e6c921 100644 --- a/app/controllers/verify/usps_controller.rb +++ b/app/controllers/verify/usps_controller.rb @@ -5,10 +5,7 @@ class UspsController < ApplicationController before_action :confirm_mail_not_spammed def index - @applicant = idv_session.normalized_applicant_params - decorated_usps = UspsDecorator.new(idv_session) - @title = decorated_usps.title - @button = decorated_usps.button + @decorated_usps = UspsDecorator.new(usps_mail_service) end def create diff --git a/app/decorators/usps_decorator.rb b/app/decorators/usps_decorator.rb index 6777bc95cc5..31e3845a556 100644 --- a/app/decorators/usps_decorator.rb +++ b/app/decorators/usps_decorator.rb @@ -1,8 +1,8 @@ class UspsDecorator - attr_reader :idv_session + attr_reader :usps_mail_service - def initialize(idv_session) - @idv_session = idv_session + def initialize(usps_mail_service) + @usps_mail_service = usps_mail_service end def title @@ -16,6 +16,6 @@ def button private def letter_already_sent? - @idv_session.address_verification_mechanism == 'usps' + @usps_mail_service.any_mail_sent? end end diff --git a/app/services/idv/usps_mail.rb b/app/services/idv/usps_mail.rb index dadb08f4b73..26b9193049f 100644 --- a/app/services/idv/usps_mail.rb +++ b/app/services/idv/usps_mail.rb @@ -12,6 +12,10 @@ def mail_spammed? max_events? && updated_within_last_month? end + def any_mail_sent? + user_mail_events.any? + end + private attr_reader :current_user diff --git a/app/views/verify/usps/index.html.slim b/app/views/verify/usps/index.html.slim index 75c25ae8732..302048a8ad2 100644 --- a/app/views/verify/usps/index.html.slim +++ b/app/views/verify/usps/index.html.slim @@ -2,7 +2,7 @@ = image_tag(asset_url('check-email.svg'), size: '48x48', alt: 'check email',\ class: 'absolute top-n24 left-0 right-0 mx-auto') h1.h2 - = @title + = @decorated_usps.title p = t('idv.messages.usps.byline') @@ -10,7 +10,7 @@ strong = t('idv.messages.usps.success') -= button_to @button, verify_usps_path, method: 'put', += button_to @decorated_usps.button, verify_usps_path, method: 'put', class: 'btn btn-primary btn-wide', form_class: 'inline-block mr2' = link_to t('idv.messages.usps.bad_address'), verify_phone_path diff --git a/spec/decorators/usps_decorator_spec.rb b/spec/decorators/usps_decorator_spec.rb index 890182ba499..d26a29f2aaf 100644 --- a/spec/decorators/usps_decorator_spec.rb +++ b/spec/decorators/usps_decorator_spec.rb @@ -1,23 +1,16 @@ require 'rails_helper' RSpec.describe UspsDecorator do + let(:user) { create(:user) } subject(:decorator) do - user = create( - :user, - :signed_up, - profiles: [build(:profile, :active, :verified, pii: { first_name: 'Jane' })] - ) - - idv_session = Idv::Session.new(user_session: {}, current_user: user, issuer: nil) - UspsDecorator.new(idv_session) + usps_mail_service = Idv::UspsMail.new(user) + UspsDecorator.new(usps_mail_service) end describe '#title' do context 'a letter has not been sent' do - let(:idv_session) { subject.idv_session } - it 'provides text to send' do - subject.idv_session.address_verification_mechanism = nil + allow(subject.usps_mail_service).to receive(:any_mail_sent?).and_return(false) expect(subject.title).to eq( I18n.t('idv.titles.mail.verify') ) @@ -26,7 +19,7 @@ context 'a letter has been sent' do it 'provides text to resend' do - subject.idv_session.address_verification_mechanism = 'usps' + allow(subject.usps_mail_service).to receive(:any_mail_sent?).and_return(true) expect(subject.title).to eq( I18n.t('idv.titles.mail.resend') ) @@ -37,7 +30,7 @@ describe '#button' do context 'a letter has not been sent' do it 'provides text to send' do - subject.idv_session.address_verification_mechanism = nil + allow(subject.usps_mail_service).to receive(:any_mail_sent?).and_return(false) expect(subject.button).to eq( I18n.t('idv.buttons.mail.send') ) @@ -46,7 +39,7 @@ context 'a letter has been sent' do it 'provides text to resend' do - subject.idv_session.address_verification_mechanism = 'usps' + allow(subject.usps_mail_service).to receive(:any_mail_sent?).and_return(true) expect(subject.button).to eq( I18n.t('idv.buttons.mail.resend') ) diff --git a/spec/features/saml/loa3_sso_spec.rb b/spec/features/saml/loa3_sso_spec.rb index 85785816185..a1a58671f14 100644 --- a/spec/features/saml/loa3_sso_spec.rb +++ b/spec/features/saml/loa3_sso_spec.rb @@ -4,6 +4,24 @@ include SamlAuthHelper include IdvHelper + def perform_id_verification_with_usps_without_confirming_code_then_sign_out(user) + saml_authn_request = auth_request.create(loa3_with_bundle_saml_settings) + visit saml_authn_request + sign_in_live_with_2fa(user) + click_idv_begin + fill_out_idv_form_ok + click_idv_continue + fill_out_financial_form_ok + click_idv_continue + click_idv_address_choose_usps + click_on t('idv.buttons.mail.send') + fill_in :user_password, with: user.password + click_submit_default + click_acknowledge_personal_key + first(:link, t('links.sign_out')).click + click_submit_default + end + context 'First time registration' do let(:email) { 'test@test.com' } before do @@ -186,6 +204,7 @@ sign_in_live_with_2fa(user) expect(current_path).to eq verify_account_path + expect(page).to have_content t('idv.messages.usps.resend') click_button t('forms.verify_profile.submit') @@ -196,6 +215,21 @@ expect(current_url).to eq saml_authn_request end + + it 'provides an option to send another letter' do + user = create(:user, :signed_up) + + perform_id_verification_with_usps_without_confirming_code_then_sign_out(user) + + sign_in_live_with_2fa(user) + + expect(current_path).to eq verify_account_path + + click_link(t('idv.messages.usps.resend')) + + expect(user.events.account_verified.size).to be(0) + expect(current_path).to eq(verify_usps_path) + end end context 'having previously cancelled phone verification' do diff --git a/spec/views/verify/usps/index.html.slim_spec.rb b/spec/views/verify/usps/index.html.slim_spec.rb new file mode 100644 index 00000000000..c72e0b65701 --- /dev/null +++ b/spec/views/verify/usps/index.html.slim_spec.rb @@ -0,0 +1,18 @@ +require 'rails_helper' + +describe 'verify/usps/index.html.slim' do + it 'calls UspsDecorator#title and #button' do + user = build_stubbed(:user, :signed_up) + usps_mail_service = Idv::UspsMail.new(user) + + usps_decorator = instance_double(UspsDecorator) + allow(UspsDecorator).to receive(:new).with(usps_mail_service). + and_return(usps_decorator) + @decorated_usps = usps_decorator + + expect(usps_decorator).to receive(:title) + expect(usps_decorator).to receive(:button) + + render + end +end From ba9cd3c7d789a0c9ce6e0acd79a8660b7bac135c Mon Sep 17 00:00:00 2001 From: Jonathan Hooper Date: Thu, 29 Jun 2017 15:02:10 -0500 Subject: [PATCH 12/22] Return FormResponse from IdV form submission **Why**: As part of backgrounding vendor proofing, it will be helpful for the IdV form objects to behave like the rest of the form objects in the app so we can extract them from the `Idv::Step` subclasses and invoke them in the controller. --- app/controllers/users/phones_controller.rb | 2 +- app/forms/idv/finance_form.rb | 11 +-- app/forms/idv/phone_form.rb | 7 +- app/forms/idv/profile_form.rb | 2 + app/forms/update_user_phone_form.rb | 2 +- app/services/idv/step.rb | 2 +- spec/forms/idv/finance_form_spec.rb | 69 ++++++++++++------- spec/forms/idv/phone_form_spec.rb | 32 +++++++-- spec/forms/idv/profile_form_spec.rb | 22 ++++++ spec/services/idv/financials_step_spec.rb | 31 ++++----- spec/services/idv/phone_step_spec.rb | 24 +++---- spec/services/idv/profile_step_spec.rb | 61 ++++++++-------- .../shared_examples_for_phone_validation.rb | 12 +++- 13 files changed, 176 insertions(+), 101 deletions(-) diff --git a/app/controllers/users/phones_controller.rb b/app/controllers/users/phones_controller.rb index 3fbeaadf54f..564ff586ce6 100644 --- a/app/controllers/users/phones_controller.rb +++ b/app/controllers/users/phones_controller.rb @@ -11,7 +11,7 @@ def edit def update @update_user_phone_form = UpdateUserPhoneForm.new(current_user) - if @update_user_phone_form.submit(user_params) + if @update_user_phone_form.submit(user_params).success? process_updates bypass_sign_in current_user else diff --git a/app/forms/idv/finance_form.rb b/app/forms/idv/finance_form.rb index 8746f69e46d..51f1a72af65 100644 --- a/app/forms/idv/finance_form.rb +++ b/app/forms/idv/finance_form.rb @@ -44,11 +44,14 @@ def initialize(idv_params) def submit(params) @params = params finance_value = update_finance_values(params) - return false unless valid? - clear_idv_params_finance - idv_params[finance_type] = finance_value - true + success = valid? + if success + clear_idv_params_finance + idv_params[finance_type] = finance_value + end + + FormResponse.new(success: success, errors: errors.messages) end def self.finance_other_type_choices diff --git a/app/forms/idv/phone_form.rb b/app/forms/idv/phone_form.rb index c6161e7947f..61266f96da9 100644 --- a/app/forms/idv/phone_form.rb +++ b/app/forms/idv/phone_form.rb @@ -20,11 +20,10 @@ def submit(params) self.phone = formatted_phone - return false unless valid? + success = valid? + update_idv_params(formatted_phone) if success - update_idv_params(formatted_phone) - - true + FormResponse.new(success: success, errors: errors.messages) end private diff --git a/app/forms/idv/profile_form.rb b/app/forms/idv/profile_form.rb index 720b26f6b1b..3824376e6a0 100644 --- a/app/forms/idv/profile_form.rb +++ b/app/forms/idv/profile_form.rb @@ -40,6 +40,8 @@ def pii_attributes def submit(params) initialize_params(params) profile.ssn_signature = ssn_signature + + FormResponse.new(success: valid?, errors: errors.messages) end private diff --git a/app/forms/update_user_phone_form.rb b/app/forms/update_user_phone_form.rb index 538cf17b310..b6256895a60 100644 --- a/app/forms/update_user_phone_form.rb +++ b/app/forms/update_user_phone_form.rb @@ -17,7 +17,7 @@ def initialize(user) def submit(params) check_phone_change(params) - valid? + FormResponse.new(success: valid?, errors: errors.messages) end def phone_changed? diff --git a/app/services/idv/step.rb b/app/services/idv/step.rb index b3c9cfbd7ae..96aef281a05 100644 --- a/app/services/idv/step.rb +++ b/app/services/idv/step.rb @@ -8,7 +8,7 @@ def initialize(idv_form:, idv_session:, params:) end def form_valid? - form_validate(params) + form_validate(params).success? end private diff --git a/spec/forms/idv/finance_form_spec.rb b/spec/forms/idv/finance_form_spec.rb index a2b0411a10a..2c2a027da57 100644 --- a/spec/forms/idv/finance_form_spec.rb +++ b/spec/forms/idv/finance_form_spec.rb @@ -25,39 +25,58 @@ end describe '#submit' do - it 'adds ccn key to idv_params when valid' do - expect(subject.submit(ccn: '12345678', finance_type: :ccn)).to eq true + context 'when the form is valid' do + let(:result) { subject.submit(ccn: '12345678', finance_type: :ccn) } - expected_params = { - ccn: '12345678', - } + it 'returns a successful form response' do + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(true) + expect(result.errors).to be_empty + end - expect(subject.idv_params).to eq expected_params + it 'adds ccn key to idv_params' do + expected_params = { + ccn: '12345678', + } + subject.submit(ccn: '12345678', finance_type: :ccn) + expect(subject.idv_params).to eq expected_params + end end - it 'fails when missing all finance fields' do - expect(subject.submit(foo: 'bar')).to eq false + context 'when the form is invalid' do + it 'returns an unsuccessful form response' do + result = subject.submit(foo: 'bar') + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to be_present + end end context 'when CCN is invalid' do - it 'fails when alpha' do - expect(subject.submit(ccn: '1234567a', finance_type: :ccn)).to eq false - expect(subject.errors[:ccn]).to eq([t('idv.errors.invalid_ccn')]) + it 'returns an unsuccessful form response when alpha' do + result = subject.submit(ccn: '1234567a', finance_type: :ccn) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors[:ccn]).to eq([t('idv.errors.invalid_ccn')]) end - it 'fails when long' do - expect(subject.submit(ccn: '123456789', finance_type: :ccn)).to eq false - expect(subject.errors[:ccn]).to eq([t('idv.errors.invalid_ccn')]) + it 'returns an unsuccessful form response when long' do + result = subject.submit(ccn: '123456789', finance_type: :ccn) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors[:ccn]).to eq([t('idv.errors.invalid_ccn')]) end - it 'fails when short' do - expect(subject.submit(ccn: '1234567', finance_type: :ccn)).to eq false - expect(subject.errors[:ccn]).to eq([t('idv.errors.invalid_ccn')]) + it 'returns an unsuccessful form response when short' do + result = subject.submit(ccn: '1234567', finance_type: :ccn) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors[:ccn]).to eq([t('idv.errors.invalid_ccn')]) end end context 'any non-ccn financial value is less than the minimum allowed digits' do - it 'fails' do + it 'returns an unsuccessful form response' do finance_types = Idv::FinanceForm::FINANCE_TYPES short_value = '1' * (FormFinanceValidator::VALID_MINIMUM_LENGTH - 1) @@ -65,8 +84,10 @@ next if type == :ccn params = { type => short_value, finance_type: type } - expect(subject.submit(params)).to eq false - expect(subject.errors[type]).to eq([t( + result = subject.submit(params) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors[type]).to eq([t( 'idv.errors.finance_number_length', minimum: FormFinanceValidator::VALID_MINIMUM_LENGTH, maximum: FormFinanceValidator::VALID_MAXIMUM_LENGTH @@ -76,7 +97,7 @@ end context 'any non-ccn financial value is over the max allowed digits' do - it 'fails' do + it 'returns an unsuccessful form response' do finance_types = Idv::FinanceForm::FINANCE_TYPES long_value = '1' * (FormFinanceValidator::VALID_MAXIMUM_LENGTH + 1) @@ -88,8 +109,10 @@ finance_type: symbolized_type, } - expect(subject.submit(params)).to eq false - expect(subject.errors[symbolized_type]).to eq([t( + result = subject.submit(params) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors[symbolized_type]).to eq([t( 'idv.errors.finance_number_length', minimum: FormFinanceValidator::VALID_MINIMUM_LENGTH, maximum: FormFinanceValidator::VALID_MAXIMUM_LENGTH diff --git a/spec/forms/idv/phone_form_spec.rb b/spec/forms/idv/phone_form_spec.rb index 7eb9673c2a1..9aa04797889 100644 --- a/spec/forms/idv/phone_form_spec.rb +++ b/spec/forms/idv/phone_form_spec.rb @@ -7,14 +7,34 @@ it_behaves_like 'a phone form' describe '#submit' do - it 'adds phone key to idv_params when valid' do - subject.submit(phone: '703-555-1212') + context 'when the form is valid' do + it 'returns a successful form response' do + result = subject.submit(phone: '703-555-1212') - expected_params = { - phone: '+1 (703) 555-1212', - } + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(true) + expect(result.errors).to be_empty + end - expect(subject.idv_params).to eq expected_params + it 'adds phone key to idv_params' do + subject.submit(phone: '703-555-1212') + + expected_params = { + phone: '+1 (703) 555-1212', + } + + expect(subject.idv_params).to eq expected_params + end + end + + context 'when the form is invalid' do + it 'returns an unsuccessful form response' do + result = subject.submit(phone: 'Im not a phone number 🙃') + + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to include(:phone) + end end it 'adds phone_confirmed_at key to idv_params when submitted phone equals user phone' do diff --git a/spec/forms/idv/profile_form_spec.rb b/spec/forms/idv/profile_form_spec.rb index 60a8b935e7e..3c0987ef2ad 100644 --- a/spec/forms/idv/profile_form_spec.rb +++ b/spec/forms/idv/profile_form_spec.rb @@ -19,6 +19,28 @@ } end + describe '#submit' do + context 'when the form is valid' do + it 'returns a successful form response' do + result = subject.submit(profile_attrs) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(true) + expect(result.errors).to be_empty + end + end + + context 'when the form is invalid' do + before { profile_attrs[:dob] = nil } + + it 'returns an unsuccessful form response' do + result = subject.submit(profile_attrs) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to include(:dob) + end + end + end + describe 'presence validations' do it 'is invalid when required attribute is not present' do %i[first_name last_name ssn dob address1 city state zipcode].each do |attr| diff --git a/spec/services/idv/financials_step_spec.rb b/spec/services/idv/financials_step_spec.rb index 4e2407e82cb..9f675b75fe4 100644 --- a/spec/services/idv/financials_step_spec.rb +++ b/spec/services/idv/financials_step_spec.rb @@ -22,27 +22,22 @@ def build_step(params) step = build_step(finance_type: :ccn, ccn: '1234') errors = { ccn: [t('idv.errors.invalid_ccn')] } - response = instance_double(FormResponse) - allow(FormResponse).to receive(:new).and_return(response) - submission = step.submit + result = step.submit + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to eq(errors) - expect(submission).to eq response - expect(FormResponse).to have_received(:new). - with(success: false, errors: errors) expect(idv_session.financials_confirmation).to eq false end it 'returns FormResponse with success: true for mock-happy CCN' do step = build_step(finance_type: :ccn, ccn: '12345678') - response = instance_double(FormResponse) - allow(FormResponse).to receive(:new).and_return(response) + result = step.submit + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(true) + expect(result.errors).to be_empty - submission = step.submit - - expect(submission).to eq response - expect(FormResponse).to have_received(:new). - with(success: true, errors: {}) expect(idv_session.financials_confirmation).to eq true expect(idv_session.params).to eq idv_finance_form.idv_params end @@ -52,13 +47,11 @@ def build_step(params) errors = { ccn: ['The ccn could not be verified.'] } - response = instance_double(FormResponse) - allow(FormResponse).to receive(:new).and_return(response) - submission = step.submit + result = step.submit + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to eq(errors) - expect(submission).to eq response - expect(FormResponse).to have_received(:new). - with(success: false, errors: errors) expect(idv_session.financials_confirmation).to eq false end end diff --git a/spec/services/idv/phone_step_spec.rb b/spec/services/idv/phone_step_spec.rb index 3a609f56829..3e5ccd84af3 100644 --- a/spec/services/idv/phone_step_spec.rb +++ b/spec/services/idv/phone_step_spec.rb @@ -26,22 +26,22 @@ def build_step(params) errors = { phone: [invalid_phone_message] } - result = instance_double(FormResponse) + result = step.submit - expect(FormResponse).to receive(:new). - with(success: false, errors: errors).and_return(result) - expect(step.submit).to eq result + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to eq(errors) expect(idv_session.phone_confirmation).to eq false end it 'returns true for mock-happy phone' do step = build_step(phone: '555-555-0000') - result = instance_double(FormResponse) + result = step.submit - expect(FormResponse).to receive(:new).with(success: true, errors: {}). - and_return(result) - expect(step.submit).to eq result + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(true) + expect(result.errors).to be_empty expect(idv_session.phone_confirmation).to eq true expect(idv_session.params).to eq idv_phone_form.idv_params end @@ -51,11 +51,11 @@ def build_step(params) errors = { phone: ['The phone number could not be verified.'] } - result = instance_double(FormResponse) + result = step.submit - expect(FormResponse).to receive(:new). - with(success: false, errors: errors).and_return(result) - expect(step.submit).to eq result + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to eq(errors) expect(idv_session.phone_confirmation).to eq false end end diff --git a/spec/services/idv/profile_step_spec.rb b/spec/services/idv/profile_step_spec.rb index e51f6f48c25..03ec4ca0ee1 100644 --- a/spec/services/idv/profile_step_spec.rb +++ b/spec/services/idv/profile_step_spec.rb @@ -30,15 +30,17 @@ def build_step(params) it 'succeeds with good params' do step = build_step(user_attrs) - result = instance_double(FormResponse) extra = { idv_attempts_exceeded: false, vendor: { reasons: ['Everything looks good'] }, } - expect(FormResponse).to receive(:new). - with(success: true, errors: {}, extra: extra).and_return(result) - expect(step.submit).to eq result + result = step.submit + + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(true) + expect(result.errors).to be_empty + expect(result.extra).to eq(extra) expect(idv_session.profile_confirmation).to eq true end @@ -51,11 +53,12 @@ def build_step(params) vendor: { reasons: ['The SSN was suspicious'] }, } - result = instance_double(FormResponse) + result = step.submit - expect(FormResponse).to receive(:new). - with(success: false, errors: errors, extra: extra).and_return(result) - expect(step.submit).to eq result + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to eq(errors) + expect(result.extra).to eq(extra) expect(idv_session.profile_confirmation).to be_nil end @@ -68,11 +71,12 @@ def build_step(params) vendor: { reasons: nil }, } - result = instance_double(FormResponse) + result = step.submit - expect(FormResponse).to receive(:new). - with(success: false, errors: errors, extra: extra).and_return(result) - expect(step.submit).to eq result + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to eq(errors) + expect(result.extra).to eq(extra) expect(idv_session.profile_confirmation).to be_nil end @@ -80,16 +84,17 @@ def build_step(params) step = build_step(user_attrs.merge(first_name: 'Bad')) errors = { first_name: ['Unverified first name.'] } - - result = instance_double(FormResponse) extra = { idv_attempts_exceeded: false, vendor: { reasons: ['The name was suspicious'] }, } - expect(FormResponse).to receive(:new). - with(success: false, errors: errors, extra: extra).and_return(result) - expect(step.submit).to eq result + result = step.submit + + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to eq(errors) + expect(result.extra).to eq(extra) expect(idv_session.profile_confirmation).to be_nil end @@ -97,16 +102,17 @@ def build_step(params) step = build_step(user_attrs.merge(zipcode: '00000')) errors = { zipcode: ['Unverified ZIP code.'] } - - result = instance_double(FormResponse) extra = { idv_attempts_exceeded: false, vendor: { reasons: ['The ZIP code was suspicious'] }, } - expect(FormResponse).to receive(:new). - with(success: false, errors: errors, extra: extra).and_return(result) - expect(step.submit).to eq result + result = step.submit + + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to eq(errors) + expect(result.extra).to eq(extra) expect(idv_session.profile_confirmation).to be_nil end @@ -114,16 +120,17 @@ def build_step(params) step = build_step(user_attrs.merge(prev_zipcode: '00000')) errors = { zipcode: ['Unverified ZIP code.'] } - - result = instance_double(FormResponse) extra = { idv_attempts_exceeded: false, vendor: { reasons: ['The ZIP code was suspicious'] }, } - expect(FormResponse).to receive(:new). - with(success: false, errors: errors, extra: extra).and_return(result) - expect(step.submit).to eq result + result = step.submit + + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(result.errors).to eq(errors) + expect(result.extra).to eq(extra) expect(idv_session.profile_confirmation).to be_nil end diff --git a/spec/support/shared_examples_for_phone_validation.rb b/spec/support/shared_examples_for_phone_validation.rb index a6527b2831b..ac2686f021a 100644 --- a/spec/support/shared_examples_for_phone_validation.rb +++ b/spec/support/shared_examples_for_phone_validation.rb @@ -24,19 +24,25 @@ allow(User).to receive(:exists?).with(email: 'new@gmail.com').and_return(false) allow(User).to receive(:exists?).with(phone: second_user.phone).and_return(true) - expect(subject.submit(phone: second_user.phone)).to be true + result = subject.submit(phone: second_user.phone) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(true) end end context 'when phone is not already taken' do it 'is valid' do - expect(subject.submit(phone: '+1 (703) 555-1212')).to be true + result = subject.submit(phone: '+1 (703) 555-1212') + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to be true end end context 'when phone is same as current user' do it 'is valid' do - expect(subject.submit(phone: user.phone)).to be true + result = subject.submit(phone: user.phone) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to be true end end end From c0b76f19fb6f3c2eeb4e6ecc74123c7d089d7f6e Mon Sep 17 00:00:00 2001 From: Zach Margolis Date: Fri, 30 Jun 2017 14:01:56 -0400 Subject: [PATCH 13/22] Send plain ASCII attributes in verify-by-mail **Why**: Our test print had issues with unicode --- app/services/usps_confirmation_maker.rb | 16 +++++------ spec/services/usps_exporter_spec.rb | 35 ++++++++++++------------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/app/services/usps_confirmation_maker.rb b/app/services/usps_confirmation_maker.rb index 1fb164a1597..e9a3ddf8af5 100644 --- a/app/services/usps_confirmation_maker.rb +++ b/app/services/usps_confirmation_maker.rb @@ -17,14 +17,14 @@ def perform # This method is single statement spread across many lines for readability def attributes { - address1: pii[:address1], - address2: pii[:address2], - city: pii[:city], - otp: pii[:otp], - first_name: pii[:first_name], - last_name: pii[:last_name], - state: pii[:state], - zipcode: pii[:zipcode], + address1: pii[:address1].norm, + address2: pii[:address2].norm, + city: pii[:city].norm, + otp: pii[:otp].norm, + first_name: pii[:first_name].norm, + last_name: pii[:last_name].norm, + state: pii[:state].norm, + zipcode: pii[:zipcode].norm, issuer: issuer, } end diff --git a/spec/services/usps_exporter_spec.rb b/spec/services/usps_exporter_spec.rb index 2e4eba2144c..a2da4deee36 100644 --- a/spec/services/usps_exporter_spec.rb +++ b/spec/services/usps_exporter_spec.rb @@ -2,18 +2,17 @@ describe UspsExporter do let(:export_file) { Tempfile.new('usps_export.psv') } - let(:usps_entry) { UspsConfirmationEntry.new_from_hash(pii_attributes) } let(:pii_attributes) do - { - first_name: 'Some', - last_name: 'One', - address1: '123 Any St', - address2: 'Ste 123', - city: 'Somewhere', - state: 'KS', - zipcode: '66666-1234', - otp: 123, - } + Pii::Attributes.new_from_hash( + first_name: { raw: 'Söme', norm: 'Some' }, + last_name: { raw: 'Öne', norm: 'One' }, + address1: { raw: '123 Añy St', norm: '123 Any St' }, + address2: { raw: 'Sté 123', norm: 'Ste 123' }, + city: { raw: 'Sömewhere', norm: 'Somewhere' }, + state: { raw: 'KS', norm: 'KS' }, + zipcode: { raw: '66666-1234', norm: '66666-1234' }, + otp: { raw: 123, norm: 123 } + ) end let(:service_provider) { ServiceProvider.from_issuer('http://localhost:3000') } let(:psv_row_contents) do @@ -23,13 +22,13 @@ due_date = due.strftime('%-B %-e') values = [ UspsExporter::CONTENT_ROW_ID, - usps_entry.first_name + ' ' + usps_entry.last_name, - usps_entry.address1, - usps_entry.address2, - usps_entry.city, - usps_entry.state, - usps_entry.zipcode, - usps_entry.otp, + pii_attributes.first_name.norm + ' ' + pii_attributes.last_name.norm, + pii_attributes.address1.norm, + pii_attributes.address2.norm, + pii_attributes.city.norm, + pii_attributes.state.norm, + pii_attributes.zipcode.norm, + pii_attributes.otp.norm, "#{current_date}, #{now.year}", "#{due_date}, #{due.year}", service_provider.friendly_name, From ecdad0e772b7c427cd5bf789d655e0ee86e7cb67 Mon Sep 17 00:00:00 2001 From: Zach Margolis Date: Fri, 30 Jun 2017 15:14:06 -0400 Subject: [PATCH 14/22] Make sure i18n keys are consistent **Why**: Helps with our translation pipeline **How**: Make sure keys use word characters and underscores only --- app/assets/javascripts/app/form-validation.js | 2 +- .../javascripts/misc/i18n-strings.js.erb | 59 +++++++------- app/assets/javascripts/misc/pw-strength.js | 3 +- app/validators/form_password_validator.rb | 6 +- config/locales/idv/en.yml | 2 +- config/locales/idv/es.yml | 2 +- config/locales/zxcvbn/en.yml | 79 ++++++++++--------- config/locales/zxcvbn/es.yml | 54 ++++++------- spec/features/visitors/set_password_spec.rb | 4 +- spec/i18n_spec.rb | 30 +++++++ 10 files changed, 141 insertions(+), 100 deletions(-) diff --git a/app/assets/javascripts/app/form-validation.js b/app/assets/javascripts/app/form-validation.js index 7d82dec7729..7d534d6e6c0 100644 --- a/app/assets/javascripts/app/form-validation.js +++ b/app/assets/javascripts/app/form-validation.js @@ -10,7 +10,7 @@ document.addEventListener('DOMContentLoaded', () => { if (input) { input.addEventListener('input', () => { if (input.validity.patternMismatch) { - input.setCustomValidity(I18n.t(`idv.errors.pattern_mismatch.${f}`)); + input.setCustomValidity(I18n.t(`idv.errors.pattern_mismatch.${I18n.key(f)}`)); } else { input.setCustomValidity(''); } diff --git a/app/assets/javascripts/misc/i18n-strings.js.erb b/app/assets/javascripts/misc/i18n-strings.js.erb index 4c5fa4c198d..32deb44529d 100644 --- a/app/assets/javascripts/misc/i18n-strings.js.erb +++ b/app/assets/javascripts/misc/i18n-strings.js.erb @@ -5,7 +5,7 @@ window.LoginGov = window.LoginGov || {}; 'errors.messages.missing_field', 'forms.passwords.show', 'idv.errors.pattern_mismatch.dob', - 'idv.errors.pattern_mismatch.personal-key', + 'idv.errors.pattern_mismatch.personal_key', 'idv.errors.pattern_mismatch.ssn', 'idv.errors.pattern_mismatch.zipcode', 'idv.modal.button.warning', @@ -16,38 +16,39 @@ window.LoginGov = window.LoginGov || {}; 'instructions.password.strength.v', 'links.remove', 'valid_email.validations.email.invalid', - 'zxcvbn.feedback.Use a few words, avoid common phrases', - 'zxcvbn.feedback.No need for symbols, digits, or uppercase letters', - 'zxcvbn.feedback.Add another word or two_ Uncommon words are better_', - 'zxcvbn.feedback.Straight rows of keys are easy to guess', - 'zxcvbn.feedback.Short keyboard patterns are easy to guess', - 'zxcvbn.feedback.Use a longer keyboard pattern with more turns', - 'zxcvbn.feedback.Repeats like "aaa" are easy to guess', - 'zxcvbn.feedback.Repeats like "abcabcabc" are only slightly harder to guess than "abc"', - 'zxcvbn.feedback.Avoid repeated words and characters', - 'zxcvbn.feedback.Sequences like abc or 6543 are easy to guess', - 'zxcvbn.feedback.Avoid sequences', - 'zxcvbn.feedback.Recent years are easy to guess', - 'zxcvbn.feedback.Avoid recent years', - 'zxcvbn.feedback.Avoid years that are associated with you', - 'zxcvbn.feedback.Dates are often easy to guess', - 'zxcvbn.feedback.Avoid dates and years that are associated with you', - 'zxcvbn.feedback.This is a top-10 common password', - 'zxcvbn.feedback.This is a top-100 common password', - 'zxcvbn.feedback.This is a very common password', - 'zxcvbn.feedback.This is similar to a commonly used password', - 'zxcvbn.feedback.A word by itself is easy to guess', - 'zxcvbn.feedback.Names and surnames by themselves are easy to guess', - 'zxcvbn.feedback.Common names and surnames are easy to guess', - 'zxcvbn.feedback.Capitalization doesn\'t help very much', - 'zxcvbn.feedback.All-uppercase is almost as easy to guess as all-lowercase', - 'zxcvbn.feedback.Reversed words aren\'t much harder to guess', - 'zxcvbn.feedback.Predictable substitutions like \'@\' instead of \'a\' don\'t help very much' + 'zxcvbn.feedback.a_word_by_itself_is_easy_to_guess', + 'zxcvbn.feedback.add_another_word_or_two_uncommon_words_are_better', + 'zxcvbn.feedback.all_uppercase_is_almost_as_easy_to_guess_as_all_lowercase', + 'zxcvbn.feedback.avoid_dates_and_years_that_are_associated_with_you', + 'zxcvbn.feedback.avoid_recent_years', + 'zxcvbn.feedback.avoid_repeated_words_and_characters', + 'zxcvbn.feedback.avoid_sequences', + 'zxcvbn.feedback.avoid_years_that_are_associated_with_you', + 'zxcvbn.feedback.capitalization_doesnt_help_very_much', + 'zxcvbn.feedback.common_names_and_surnames_are_easy_to_guess', + 'zxcvbn.feedback.dates_are_often_easy_to_guess', + 'zxcvbn.feedback.names_and_surnames_by_themselves_are_easy_to_guess', + 'zxcvbn.feedback.there_is_no_need_for_symbols_digits_or_uppercase_letters', + 'zxcvbn.feedback.predictable_substitutions_like__instead_of_a_dont_help_very_much', + 'zxcvbn.feedback.recent_years_are_easy_to_guess', + 'zxcvbn.feedback.repeats_like_aaa_are_easy_to_guess', + 'zxcvbn.feedback.repeats_like_abcabcabc_are_only_slightly_harder_to_guess_than_abc', + 'zxcvbn.feedback.reversed_words_arent_much_harder_to_guess', + 'zxcvbn.feedback.sequences_like_abc_or_6543_are_easy_to_guess', + 'zxcvbn.feedback.short_keyboard_patterns_are_easy_to_guess', + 'zxcvbn.feedback.straight_rows_of_keys_are_easy_to_guess', + 'zxcvbn.feedback.this_is_a_top_10_common_password', + 'zxcvbn.feedback.this_is_a_top_100_common_password', + 'zxcvbn.feedback.this_is_a_very_common_password', + 'zxcvbn.feedback.this_is_similar_to_a_commonly_used_password', + 'zxcvbn.feedback.for_a_stronger_password_use_a_few_words_separated_by_spaces_but_avoid_common_phrases', + 'zxcvbn.feedback.use_a_longer_keyboard_pattern_with_more_turns' ] %> window.LoginGov.I18n = { strings: {}, - t: function(key) { return this.strings[key]; } + t: function(key) { return this.strings[key]; }, + key: function(key) { return key.replace(/[ -]/g, '_').replace(/\W/g, '').toLowerCase(); } }; <% keys.each do |key| %> diff --git a/app/assets/javascripts/misc/pw-strength.js b/app/assets/javascripts/misc/pw-strength.js index ae0b444be1b..82c0a79c92a 100644 --- a/app/assets/javascripts/misc/pw-strength.js +++ b/app/assets/javascripts/misc/pw-strength.js @@ -27,8 +27,7 @@ function getFeedback(z) { const { warning, suggestions } = z.feedback; function lookup(str) { - const strFormatted = str.replace(/\./g, '_'); - return I18n.t(`zxcvbn.feedback.${strFormatted}`); + return I18n.t(`zxcvbn.feedback.${I18n.key(str)}`); } if (!warning && !suggestions.length) return ''; diff --git a/app/validators/form_password_validator.rb b/app/validators/form_password_validator.rb index 23c6b9a9a4f..6997842b5a5 100644 --- a/app/validators/form_password_validator.rb +++ b/app/validators/form_password_validator.rb @@ -40,11 +40,15 @@ def zxcvbn_feedback feedback = @pass_score.feedback.values.flatten.reject(&:empty?) feedback.map do |error| - I18n.t("zxcvbn.feedback.#{error.tr('.', '_')}") + I18n.t("zxcvbn.feedback.#{i18n_key(error)}") end.join('. ').gsub(/\.\s*\./, '.') end def password_strength_enabled? @enabled ||= FeatureManagement.password_strength_enabled? end + + def i18n_key(key) + key.tr(' -', '_').gsub(/\W/, '').downcase + end end diff --git a/config/locales/idv/en.yml b/config/locales/idv/en.yml index fd3104718a8..50dac2ba93e 100644 --- a/config/locales/idv/en.yml +++ b/config/locales/idv/en.yml @@ -30,7 +30,7 @@ en: mail_limit_reached: You have have requested too much mail in the last month. pattern_mismatch: dob: Your date of birth must be entered in as mm/dd/yyyy - "personal-key": > + personal_key: > Please enter your personal key for this account. Example: ABC1-DEF2-G3HI-J456 ssn: 'Your Social Security Number must be entered in as ###-##-####' zipcode: 'Your zipcode must be entered in as #####-####' diff --git a/config/locales/idv/es.yml b/config/locales/idv/es.yml index efb28f903ff..b54e1fbce07 100644 --- a/config/locales/idv/es.yml +++ b/config/locales/idv/es.yml @@ -26,7 +26,7 @@ es: mail_limit_reached: NOT TRANSLATED YET pattern_mismatch: dob: NOT TRANSLATED YET - "personal-key": NOT TRANSLATED YET + personal_key: NOT TRANSLATED YET ssn: NOT TRANSLATED YET zipcode: NOT TRANSLATED YET form: diff --git a/config/locales/zxcvbn/en.yml b/config/locales/zxcvbn/en.yml index 05e228179f1..7c530bb8064 100644 --- a/config/locales/zxcvbn/en.yml +++ b/config/locales/zxcvbn/en.yml @@ -2,39 +2,46 @@ en: zxcvbn: feedback: - "A word by itself is easy to guess": A word by itself is easy to guess - "Add another word or two_ Uncommon words are better_": >- - Add another word or two. Uncommon words are better - "All-uppercase is almost as easy to guess as all-lowercase": >- - All-uppercase is almost as easy to guess as all-lowercase - "Avoid dates and years that are associated with you": >- - Avoid dates and years that are associated with you - "Avoid recent years": Avoid recent years - "Avoid repeated words and characters": Avoid repeated words and characters - "Avoid sequences": Avoid sequences - "Avoid years that are associated with you": Avoid years that are associated with you - "Capitalization doesn't help very much": Capitalization doesn’t help very much - "Common names and surnames are easy to guess": Common names and surnames are easy to guess - "Dates are often easy to guess": Dates are often easy to guess - "Names and surnames by themselves are easy to guess": >- - Names and surnames by themselves are easy to guess - "No need for symbols, digits, or uppercase letters": >- - There is no need for symbols, digits, or uppercase letters - "Predictable substitutions like '@' instead of 'a' don't help very much": - Predictable substitutions like '@' instead of 'a' don’t help very much - "Recent years are easy to guess": Recent years are easy to guess - "Repeats like \"aaa\" are easy to guess": Repeats like "aaa" are easy to guess - "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": >- - Repeats like "abcabcabc" are only slightly harder to guess than "abc" - "Reversed words aren't much harder to guess": Reversed words aren’t much harder to guess - "Sequences like abc or 6543 are easy to guess": Sequences like abc or 6543 are easy to guess - "Short keyboard patterns are easy to guess": Short keyboard patterns are easy to guess - "Straight rows of keys are easy to guess": Straight rows of keys are easy to guess - "This is a top-10 common password": This is a top-10 common password - "This is a top-100 common password": This is a top-100 common password - "This is a very common password": This is a very common password - "This is similar to a commonly used password": This is similar to a commonly used password - "Use a few words, avoid common phrases": - For a stronger password, use a few words separated by spaces, but avoid common phrases - "Use a longer keyboard pattern with more turns": >- - Use a longer keyboard pattern with more turns + a_word_by_itself_is_easy_to_guess: A word by itself is easy to guess + add_another_word_or_two_uncommon_words_are_better: Add another word or two. + Uncommon words are better + all_uppercase_is_almost_as_easy_to_guess_as_all_lowercase: All-uppercase is + almost as easy to guess as all-lowercase + avoid_dates_and_years_that_are_associated_with_you: Avoid dates and years that + are associated with you + avoid_recent_years: Avoid recent years + avoid_repeated_words_and_characters: Avoid repeated words and characters + avoid_sequences: Avoid sequences + avoid_years_that_are_associated_with_you: Avoid years that are associated with + you + capitalization_doesnt_help_very_much: Capitalization doesn’t help very much + common_names_and_surnames_are_easy_to_guess: Common names and surnames are easy + to guess + dates_are_often_easy_to_guess: Dates are often easy to guess + names_and_surnames_by_themselves_are_easy_to_guess: Names and surnames by themselves + are easy to guess + there_is_no_need_for_symbols_digits_or_uppercase_letters: There is no need for + symbols, digits, or uppercase letters + predictable_substitutions_like__instead_of_a_dont_help_very_much: Predictable + substitutions like '@' instead of 'a' don’t help very much + recent_years_are_easy_to_guess: Recent years are easy to guess + repeats_like_aaa_are_easy_to_guess: Repeats like "aaa" are easy to guess + repeats_like_abcabcabc_are_only_slightly_harder_to_guess_than_abc: Repeats like + "abcabcabc" are only slightly harder to guess than "abc" + reversed_words_arent_much_harder_to_guess: Reversed words aren’t much harder + to guess + sequences_like_abc_or_6543_are_easy_to_guess: Sequences like abc or 6543 are + easy to guess + short_keyboard_patterns_are_easy_to_guess: Short keyboard patterns are easy + to guess + straight_rows_of_keys_are_easy_to_guess: Straight rows of keys are easy to guess + this_is_a_top_10_common_password: This is a top-10 common password + this_is_a_top_100_common_password: This is a top-100 common password + this_is_a_very_common_password: This is a very common password + this_is_similar_to_a_commonly_used_password: This is similar to a commonly used + password + for_a_stronger_password_use_a_few_words_separated_by_spaces_but_avoid_common_phrases: For + a stronger password, use a few words separated by spaces, but avoid common + phrases + use_a_longer_keyboard_pattern_with_more_turns: Use a longer keyboard pattern + with more turns diff --git a/config/locales/zxcvbn/es.yml b/config/locales/zxcvbn/es.yml index 086ffa24261..38be5d1fb42 100644 --- a/config/locales/zxcvbn/es.yml +++ b/config/locales/zxcvbn/es.yml @@ -2,30 +2,30 @@ es: zxcvbn: feedback: - "A word by itself is easy to guess": NOT TRANSLATED YET - "Add another word or two_ Uncommon words are better_": NOT TRANSLATED YET - "All-uppercase is almost as easy to guess as all-lowercase": NOT TRANSLATED YET - "Avoid dates and years that are associated with you": NOT TRANSLATED YET - "Avoid recent years": NOT TRANSLATED YET - "Avoid repeated words and characters": NOT TRANSLATED YET - "Avoid sequences": NOT TRANSLATED YET - "Avoid years that are associated with you": NOT TRANSLATED YET - "Capitalization doesn't help very much": NOT TRANSLATED YET - "Common names and surnames are easy to guess": NOT TRANSLATED YET - "Dates are often easy to guess": NOT TRANSLATED YET - "Names and surnames by themselves are easy to guess": NOT TRANSLATED YET - "No need for symbols, digits, or uppercase letters": NOT TRANSLATED YET - "Predictable substitutions like '@' instead of 'a' don't help very much": NOT TRANSLATED YET - "Recent years are easy to guess": NOT TRANSLATED YET - "Repeats like \"aaa\" are easy to guess": NOT TRANSLATED YET - "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": NOT TRANSLATED YET - "Reversed words aren't much harder to guess": NOT TRANSLATED YET - "Sequences like abc or 6543 are easy to guess": NOT TRANSLATED YET - "Short keyboard patterns are easy to guess": NOT TRANSLATED YET - "Straight rows of keys are easy to guess": NOT TRANSLATED YET - "This is a top-10 common password": NOT TRANSLATED YET - "This is a top-100 common password": NOT TRANSLATED YET - "This is a very common password": NOT TRANSLATED YET - "This is similar to a commonly used password": NOT TRANSLATED YET - "Use a few words, avoid common phrases": NOT TRANSLATED YET - "Use a longer keyboard pattern with more turns": NOT TRANSLATED YET + a_word_by_itself_is_easy_to_guess: NOT TRANSLATED YET + add_another_word_or_two_uncommon_words_are_better: NOT TRANSLATED YET + all_uppercase_is_almost_as_easy_to_guess_as_all_lowercase: NOT TRANSLATED YET + avoid_dates_and_years_that_are_associated_with_you: NOT TRANSLATED YET + avoid_recent_years: NOT TRANSLATED YET + avoid_repeated_words_and_characters: NOT TRANSLATED YET + avoid_sequences: NOT TRANSLATED YET + avoid_years_that_are_associated_with_you: NOT TRANSLATED YET + capitalization_doesnt_help_very_much: NOT TRANSLATED YET + common_names_and_surnames_are_easy_to_guess: NOT TRANSLATED YET + dates_are_often_easy_to_guess: NOT TRANSLATED YET + names_and_surnames_by_themselves_are_easy_to_guess: NOT TRANSLATED YET + there_is_no_need_for_symbols_digits_or_uppercase_letters: NOT TRANSLATED YET + predictable_substitutions_like__instead_of_a_dont_help_very_much: NOT TRANSLATED YET + recent_years_are_easy_to_guess: NOT TRANSLATED YET + repeats_like_aaa_are_easy_to_guess: NOT TRANSLATED YET + repeats_like_abcabcabc_are_only_slightly_harder_to_guess_than_abc: NOT TRANSLATED YET + reversed_words_arent_much_harder_to_guess: NOT TRANSLATED YET + sequences_like_abc_or_6543_are_easy_to_guess: NOT TRANSLATED YET + short_keyboard_patterns_are_easy_to_guess: NOT TRANSLATED YET + straight_rows_of_keys_are_easy_to_guess: NOT TRANSLATED YET + this_is_a_top_10_common_password: NOT TRANSLATED YET + this_is_a_top_100_common_password: NOT TRANSLATED YET + this_is_a_very_common_password: NOT TRANSLATED YET + this_is_similar_to_a_commonly_used_password: NOT TRANSLATED YET + for_a_stronger_password_use_a_few_words_separated_by_spaces_but_avoid_common_phrases: NOT TRANSLATED YET + use_a_longer_keyboard_pattern_with_more_turns: NOT TRANSLATED YET diff --git a/spec/features/visitors/set_password_spec.rb b/spec/features/visitors/set_password_spec.rb index 8a518521deb..ad3efba140c 100644 --- a/spec/features/visitors/set_password_spec.rb +++ b/spec/features/visitors/set_password_spec.rb @@ -55,7 +55,7 @@ expect(page).to have_content '...' fill_in 'password_form_password', with: 'password' - expect(page).to have_content 'This is a top-10 common password' + expect(page).to have_content t('zxcvbn.feedback.this_is_a_top_10_common_password') end end @@ -92,7 +92,7 @@ click_button t('forms.buttons.continue') - expect(page).to have_content t('zxcvbn.feedback.This is a top-10 common password') + expect(page).to have_content t('zxcvbn.feedback.this_is_a_top_10_common_password') end end end diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb index 8dd0319a723..5ab0b18e9d7 100644 --- a/spec/i18n_spec.rb +++ b/spec/i18n_spec.rb @@ -20,4 +20,34 @@ "#{unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them" ) end + + root_dir = File.expand_path(File.join(File.dirname(__FILE__), '../')) + + Dir[File.join(root_dir, '/config/locales/**/*.yml')].each do |full_path| + i18n_file = full_path.sub("#{root_dir}/", '') + + describe i18n_file do + it 'has only lower_snake_case keys' do + keys = hash_keys(YAML.load_file(full_path)) + + bad_keys = keys.reject { |key| key =~ /^[a-z0-9_.]+$/ } + + expect(bad_keys).to be_empty + end + end + end + + def hash_keys(hash, parent_keys: []) + keys = [] + + hash.each do |key, value| + if value.is_a?(Hash) + keys += hash_keys(value, parent_keys: parent_keys + [key]) + else + keys << [*parent_keys, key].join('.') + end + end + + keys + end end From d459ca4a18a1b31ff2801a8b69efcae9fcbd43cc Mon Sep 17 00:00:00 2001 From: Zach Margolis Date: Fri, 30 Jun 2017 10:21:22 -0400 Subject: [PATCH 15/22] Separate idv_form from Idv::Step classes **Why**: This will let us break submission in to two steps and into two controller actions which will further help us when we move the call to our vendors to background jobs. --- .reek | 1 + .../concerns/idv_failure_concern.rb | 6 ++- app/controllers/verify/finance_controller.rb | 28 +++++++++++--- app/controllers/verify/phone_controller.rb | 23 +++++++++--- app/controllers/verify/sessions_controller.rb | 28 ++++++++++---- app/forms/idv/profile_form.rb | 12 +++--- app/services/analytics.rb | 9 +++-- app/services/idv/financials_step.rb | 17 +-------- app/services/idv/phone_step.rb | 16 +------- app/services/idv/profile_step.rb | 36 ++---------------- app/services/idv/session.rb | 4 +- app/services/idv/step.rb | 37 +++++-------------- .../verify/finance_controller_spec.rb | 13 +++++-- .../verify/phone_controller_spec.rb | 20 ++++++---- .../verify/sessions_controller_spec.rb | 26 ++++++++----- spec/services/idv/financials_step_spec.rb | 26 ++++--------- spec/services/idv/phone_step_spec.rb | 23 +++--------- spec/services/idv/profile_step_spec.rb | 34 ++++------------- 18 files changed, 157 insertions(+), 202 deletions(-) diff --git a/.reek b/.reek index fdb463ebd0c..5dd7ff3513f 100644 --- a/.reek +++ b/.reek @@ -74,6 +74,7 @@ TooManyMethods: - OpenidConnect::AuthorizationController - Idv::Session - User + - Verify::SessionsController UncommunicativeMethodName: exclude: - PhoneConfirmationFlow diff --git a/app/controllers/concerns/idv_failure_concern.rb b/app/controllers/concerns/idv_failure_concern.rb index 689a3e61205..be6348ad420 100644 --- a/app/controllers/concerns/idv_failure_concern.rb +++ b/app/controllers/concerns/idv_failure_concern.rb @@ -5,7 +5,7 @@ def render_failure if step_attempts_exceeded? @view_model = view_model(error: 'fail') flash_message(type: :error) - elsif step.form_valid_but_vendor_validation_failed? + elsif form_valid_but_vendor_validation_failed? @view_model = view_model(error: 'warning') flash_message(type: :warning) else @@ -13,6 +13,10 @@ def render_failure end end + def form_valid_but_vendor_validation_failed? + idv_form.valid? && !step.vendor_validation_passed? + end + def flash_message(type:) flash.now[type.to_sym] = @view_model.flash_message end diff --git a/app/controllers/verify/finance_controller.rb b/app/controllers/verify/finance_controller.rb index e41a5af7c4f..7150a79cf1d 100644 --- a/app/controllers/verify/finance_controller.rb +++ b/app/controllers/verify/finance_controller.rb @@ -5,6 +5,7 @@ class FinanceController < ApplicationController before_action :confirm_step_needed before_action :confirm_step_allowed + before_action :submit_idv_form, only: [:create] def new @view_model = view_model @@ -13,7 +14,7 @@ def new def create result = step.submit - analytics.track_event(Analytics::IDV_FINANCE_CONFIRMATION, result.to_h) + analytics.track_event(Analytics::IDV_FINANCE_CONFIRMATION_VENDOR, result.to_h) increment_step_attempts if result.success? @@ -26,6 +27,16 @@ def create private + def submit_idv_form + result = idv_form.submit(step_params) + analytics.track_event(Analytics::IDV_FINANCE_CONFIRMATION_FORM, result.to_h) + + return if result.success? + + @view_model = view_model + render_form + end + def step_name :financials end @@ -38,12 +49,12 @@ def view_model(error: nil) Verify::FinancialsNew.new( error: error, remaining_attempts: remaining_step_attempts, - idv_form: idv_finance_form + idv_form: idv_form ) end - def idv_finance_form - @_idv_finance_form ||= Idv::FinanceForm.new(idv_session.params) + def idv_form + @_idv_form ||= Idv::FinanceForm.new(idv_session.params) end def handle_success @@ -53,9 +64,9 @@ def handle_success def step @_step ||= Idv::FinancialsStep.new( - idv_form: idv_finance_form, + idv_form_params: idv_form.idv_params, idv_session: idv_session, - params: step_params + vendor_params: vendor_params ) end @@ -70,5 +81,10 @@ def render_form render 'verify/finance_other/new' end end + + def vendor_params + finance_type = idv_form.finance_type + { finance_type => idv_form.idv_params[finance_type] } + end end end diff --git a/app/controllers/verify/phone_controller.rb b/app/controllers/verify/phone_controller.rb index 0a941349b2f..2a0a0999cbd 100644 --- a/app/controllers/verify/phone_controller.rb +++ b/app/controllers/verify/phone_controller.rb @@ -5,6 +5,7 @@ class PhoneController < ApplicationController before_action :confirm_step_needed before_action :confirm_step_allowed + before_action :submit_idv_form, only: [:create] def new @view_model = view_model @@ -13,7 +14,7 @@ def new def create result = step.submit - analytics.track_event(Analytics::IDV_PHONE_CONFIRMATION, result.to_h) + analytics.track_event(Analytics::IDV_PHONE_CONFIRMATION_VENDOR, result.to_h) increment_step_attempts if result.success? @@ -26,15 +27,25 @@ def create private + def submit_idv_form + result = idv_form.submit(step_params) + analytics.track_event(Analytics::IDV_PHONE_CONFIRMATION_FORM, result.to_h) + + return if result.success? + + @view_model = view_model + render :new + end + def step_name :phone end def step @_step ||= Idv::PhoneStep.new( - idv_form: idv_phone_form, idv_session: idv_session, - params: step_params + idv_form_params: idv_form.idv_params, + vendor_params: idv_form.phone ) end @@ -42,7 +53,7 @@ def view_model(error: nil) Verify::PhoneNew.new( error: error, remaining_attempts: remaining_step_attempts, - idv_form: idv_phone_form + idv_form: idv_form ) end @@ -54,8 +65,8 @@ def confirm_step_needed redirect_to verify_review_path if idv_session.phone_confirmation == true end - def idv_phone_form - @_idv_phone_form ||= Idv::PhoneForm.new(idv_session.params, current_user) + def idv_form + @_idv_form ||= Idv::PhoneForm.new(idv_session.params, current_user) end end end diff --git a/app/controllers/verify/sessions_controller.rb b/app/controllers/verify/sessions_controller.rb index 6d944c73fea..3be7548c016 100644 --- a/app/controllers/verify/sessions_controller.rb +++ b/app/controllers/verify/sessions_controller.rb @@ -7,6 +7,8 @@ class SessionsController < ApplicationController before_action :confirm_idv_attempts_allowed before_action :confirm_idv_needed before_action :confirm_step_needed, except: [:destroy] + before_action :initialize_idv_session, only: [:create] + before_action :submit_idv_form, only: [:create] delegate :attempts_exceeded?, to: :step, prefix: true @@ -18,7 +20,7 @@ def new def create result = step.submit - analytics.track_event(Analytics::IDV_BASIC_INFO_SUBMITTED, result.to_h) + analytics.track_event(Analytics::IDV_BASIC_INFO_SUBMITTED_VENDOR, result.to_h) if result.success? process_success @@ -35,6 +37,13 @@ def destroy private + def submit_idv_form + result = idv_form.submit(profile_params) + analytics.track_event(Analytics::IDV_BASIC_INFO_SUBMITTED_FORM, result.to_h) + + process_failure unless result.success? + end + def step_name :sessions end @@ -45,9 +54,9 @@ def confirm_step_needed def step @_step ||= Idv::ProfileStep.new( - idv_form: idv_profile_form, + idv_form_params: profile_params, idv_session: idv_session, - params: profile_params + vendor_params: idv_session.vendor_params ) end @@ -68,7 +77,7 @@ def process_success end def process_failure - if step.duplicate_ssn? + if idv_form.duplicate_ssn? flash[:error] = t('idv.errors.duplicate_ssn') redirect_to verify_session_dupe_path else @@ -81,7 +90,7 @@ def view_model(error: nil) Verify::SessionsNew.new( error: error, remaining_attempts: remaining_idv_attempts, - idv_form: idv_profile_form + idv_form: idv_form ) end @@ -89,8 +98,13 @@ def remaining_idv_attempts Idv::Attempter.idv_max_attempts - current_user.idv_attempts end - def idv_profile_form - @_idv_profile_form ||= Idv::ProfileForm.new((idv_session.params || {}), current_user) + def idv_form + @_idv_form ||= Idv::ProfileForm.new((idv_session.params || {}), current_user) + end + + def initialize_idv_session + idv_session.params.merge!(profile_params) + idv_session.applicant = idv_session.vendor_params end def profile_params diff --git a/app/forms/idv/profile_form.rb b/app/forms/idv/profile_form.rb index 3824376e6a0..7d2912f44b5 100644 --- a/app/forms/idv/profile_form.rb +++ b/app/forms/idv/profile_form.rb @@ -44,6 +44,11 @@ def submit(params) FormResponse.new(success: valid?, errors: errors.messages) end + def duplicate_ssn? + return true if any_matching_ssn_signatures?(ssn_signature) + return true if ssn_is_duplicate_with_old_key? + end + private attr_writer(*Pii::Attributes.members) @@ -61,12 +66,7 @@ def ssn_signature(key = Pii::Fingerprinter.current_key) end def ssn_is_unique - errors.add :ssn, I18n.t('idv.errors.duplicate_ssn') if ssn_is_duplicate? - end - - def ssn_is_duplicate? - return true if any_matching_ssn_signatures?(ssn_signature) - return true if ssn_is_duplicate_with_old_key? + errors.add :ssn, I18n.t('idv.errors.duplicate_ssn') if duplicate_ssn? end def ssn_is_duplicate_with_old_key? diff --git a/app/services/analytics.rb b/app/services/analytics.rb index 96811367afd..6d59735fcbe 100644 --- a/app/services/analytics.rb +++ b/app/services/analytics.rb @@ -38,14 +38,17 @@ def uuid EMAIL_CONFIRMATION = 'Email Confirmation'.freeze EMAIL_CONFIRMATION_RESEND = 'Email Confirmation requested due to invalid token'.freeze IDV_BASIC_INFO_VISIT = 'IdV: basic info visited'.freeze - IDV_BASIC_INFO_SUBMITTED = 'IdV: basic info submitted'.freeze + IDV_BASIC_INFO_SUBMITTED_FORM = 'IdV: basic info form submitted'.freeze + IDV_BASIC_INFO_SUBMITTED_VENDOR = 'IdV: basic info vendor submitted'.freeze IDV_MAX_ATTEMPTS_EXCEEDED = 'IdV: max attempts exceeded'.freeze IDV_FINAL = 'IdV: final resolution'.freeze IDV_FINANCE_CCN_VISIT = 'IdV: finance ccn visited'.freeze - IDV_FINANCE_CONFIRMATION = 'IdV: finance confirmation'.freeze + IDV_FINANCE_CONFIRMATION_FORM = 'IdV: finance confirmation form'.freeze + IDV_FINANCE_CONFIRMATION_VENDOR = 'IdV: finance confirmation vendor'.freeze IDV_FINANCE_OTHER_VISIT = 'IdV: finance other visited'.freeze IDV_INTRO_VISIT = 'IdV: intro visited'.freeze - IDV_PHONE_CONFIRMATION = 'IdV: phone confirmation'.freeze + IDV_PHONE_CONFIRMATION_FORM = 'IdV: phone confirmation form'.freeze + IDV_PHONE_CONFIRMATION_VENDOR = 'IdV: phone confirmation vendor'.freeze IDV_PHONE_RECORD_VISIT = 'IdV: phone of record visited'.freeze IDV_REVIEW_COMPLETE = 'IdV: review complete'.freeze IDV_REVIEW_VISIT = 'IdV: review info visited'.freeze diff --git a/app/services/idv/financials_step.rb b/app/services/idv/financials_step.rb index 5dc601c25ef..e3deec1b95b 100644 --- a/app/services/idv/financials_step.rb +++ b/app/services/idv/financials_step.rb @@ -4,7 +4,7 @@ def submit if complete? @success = true idv_session.financials_confirmation = true - idv_session.params = idv_form.idv_params + idv_session.params = idv_form_params else @success = false idv_session.financials_confirmation = false @@ -13,29 +13,16 @@ def submit FormResponse.new(success: success, errors: errors) end - def form_valid_but_vendor_validation_failed? - form_valid? && !vendor_validation_passed? - end - private attr_reader :success def complete? - form_valid? && vendor_validation_passed? + vendor_validation_passed? end def vendor_validator_class Idv::FinancialsValidator end - - def vendor_reasons - vendor_validator_result.reasons if form_valid? - end - - def vendor_params - finance_type = idv_form.finance_type - { finance_type => idv_form.idv_params[finance_type] } - end end end diff --git a/app/services/idv/phone_step.rb b/app/services/idv/phone_step.rb index c0973a7cec6..4345e56b8ed 100644 --- a/app/services/idv/phone_step.rb +++ b/app/services/idv/phone_step.rb @@ -10,32 +10,20 @@ def submit FormResponse.new(success: complete?, errors: errors) end - def form_valid_but_vendor_validation_failed? - form_valid? && !vendor_validation_passed? - end - private def complete? - form_valid? && vendor_validation_passed? + vendor_validation_passed? end def vendor_validator_class Idv::PhoneValidator end - def vendor_params - idv_form.phone - end - - def vendor_reasons - vendor_validator_result.reasons if form_valid? - end - def update_idv_session idv_session.phone_confirmation = true idv_session.address_verification_mechanism = :phone - idv_session.params = idv_form.idv_params + idv_session.params = idv_form_params end end end diff --git a/app/services/idv/profile_step.rb b/app/services/idv/profile_step.rb index 4f7ff1c51cf..cccbc0be87c 100644 --- a/app/services/idv/profile_step.rb +++ b/app/services/idv/profile_step.rb @@ -1,12 +1,9 @@ module Idv class ProfileStep < Step def submit - initialize_idv_session - submit_idv_form - @success = complete? - increment_attempts_count if form_valid? + increment_attempts_count update_idv_session if success FormResponse.new(success: success, errors: errors, extra: extra_analytics_attributes) @@ -16,47 +13,22 @@ def attempts_exceeded? attempter.exceeded? end - def duplicate_ssn? - errors.key?(:ssn) && errors[:ssn].include?(I18n.t('idv.errors.duplicate_ssn')) - end - - def form_valid_but_vendor_validation_failed? - form_valid? && !vendor_validation_passed? - end - private attr_reader :success - def initialize_idv_session - idv_session.params.merge!(params) - idv_session.applicant = vendor_params - end - - def vendor_params - idv_session.vendor_params - end - - def submit_idv_form - idv_form.submit(params) - end - def complete? - !attempts_exceeded? && form_valid? && vendor_validation_passed? + !attempts_exceeded? && vendor_validation_passed? end def attempter - @_idv_attempter ||= Idv::Attempter.new(idv_form.user) + @_idv_attempter ||= Idv::Attempter.new(idv_session.current_user) end def increment_attempts_count attempter.increment end - def form_valid? - @_form_valid ||= idv_form.valid? - end - def vendor_validator_class Idv::ProfileValidator end @@ -76,7 +48,7 @@ def extra_analytics_attributes end def vendor_reasons - vendor_validator_result.reasons if form_valid? + vendor_validator_result.reasons end end end diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb index 188ef878467..a0baa6894a9 100644 --- a/app/services/idv/session.rb +++ b/app/services/idv/session.rb @@ -17,6 +17,8 @@ class Session vendor_session_id ].freeze + attr_reader :current_user + def initialize(user_session:, current_user:, issuer:) @user_session = user_session @current_user = current_user @@ -96,7 +98,7 @@ def address_mechanism_chosen? private - attr_accessor :user_session, :current_user, :issuer + attr_accessor :user_session, :issuer def new_idv_session { params: {}, step_attempts: { financials: 0, phone: 0 } } diff --git a/app/services/idv/step.rb b/app/services/idv/step.rb index 96aef281a05..ad5c3d11b42 100644 --- a/app/services/idv/step.rb +++ b/app/services/idv/step.rb @@ -1,28 +1,20 @@ # abstract base class for Idv Steps module Idv class Step - def initialize(idv_form:, idv_session:, params:) - @idv_form = idv_form + def initialize(idv_session:, idv_form_params:, vendor_params:) + @idv_form_params = idv_form_params @idv_session = idv_session - @params = params + @vendor_params = vendor_params end - def form_valid? - form_validate(params).success? + def vendor_validation_passed? + vendor_validator_result.success? end private attr_accessor :idv_session - attr_reader :idv_form, :params - - def form_validate(params) - @form_result ||= idv_form.submit(params) - end - - def vendor_validation_passed? - vendor_validator_result.success? - end + attr_reader :idv_form_params, :vendor_params def vendor_validator_result @_vendor_validator_result ||= extract_vendor_result(vendor_validator.result) @@ -41,15 +33,10 @@ def extract_vendor_result(result) end def errors - errors = idv_form.errors.messages.dup - return errors unless form_valid? && vendor_errors - merge_vendor_errors(errors) - end - - def merge_vendor_errors(errors) - vendor_errors.each_with_object(errors) do |(key, value), errs| - value = [value] unless value.is_a?(Array) - errs[key] = value + @_errors ||= begin + vendor_validator_result.errors.each_with_object({}) do |(key, value), errs| + errs[key] = Array(value) + end end end @@ -57,10 +44,6 @@ def idv_vendor @_idv_vendor ||= Idv::Vendor.new end - def vendor_errors - @_vendor_errors ||= vendor_validator_result.errors - end - def vendor_validator @_vendor_validator ||= vendor_validator_class.new( applicant: idv_session.applicant, diff --git a/spec/controllers/verify/finance_controller_spec.rb b/spec/controllers/verify/finance_controller_spec.rb index 4ae70d9091c..335e4f2cc93 100644 --- a/spec/controllers/verify/finance_controller_spec.rb +++ b/spec/controllers/verify/finance_controller_spec.rb @@ -141,7 +141,10 @@ } expect(@analytics).to have_received(:track_event).with( - Analytics::IDV_FINANCE_CONFIRMATION, result + Analytics::IDV_FINANCE_CONFIRMATION_FORM, result + ) + expect(@analytics).to have_received(:track_event).with( + Analytics::IDV_FINANCE_CONFIRMATION_VENDOR, result ) end end @@ -156,7 +159,9 @@ } expect(@analytics).to have_received(:track_event). - with(Analytics::IDV_FINANCE_CONFIRMATION, result) + with(Analytics::IDV_FINANCE_CONFIRMATION_FORM, success: true, errors: {}) + expect(@analytics).to have_received(:track_event). + with(Analytics::IDV_FINANCE_CONFIRMATION_VENDOR, result) end end @@ -172,8 +177,8 @@ } expect(@analytics).to have_received(:track_event). - with(Analytics::IDV_FINANCE_CONFIRMATION, result) - expect(subject.idv_session.financials_confirmation).to eq false + with(Analytics::IDV_FINANCE_CONFIRMATION_FORM, result) + expect(subject.idv_session.financials_confirmation).to be_falsy expect(Idv::FinancialsValidator).to_not have_received(:new) end end diff --git a/spec/controllers/verify/phone_controller_spec.rb b/spec/controllers/verify/phone_controller_spec.rb index 5f20d7df012..8631d32ae97 100644 --- a/spec/controllers/verify/phone_controller_spec.rb +++ b/spec/controllers/verify/phone_controller_spec.rb @@ -1,7 +1,8 @@ require 'rails_helper' -include Features::LocalizationHelper describe Verify::PhoneController do + include Features::LocalizationHelper + let(:max_attempts) { Idv::Attempter.idv_max_attempts } let(:good_phone) { '+1 (555) 555-0000' } let(:bad_phone) { '+1 (555) 555-5555' } @@ -57,7 +58,7 @@ end it 'tracks form error and does not make a vendor API call' do - allow(Idv::PhoneValidator).to receive(:new) + expect(Idv::PhoneValidator).to_not receive(:new) put :create, idv_phone_form: { phone: '703' } @@ -69,10 +70,9 @@ } expect(@analytics).to have_received(:track_event).with( - Analytics::IDV_PHONE_CONFIRMATION, result + Analytics::IDV_PHONE_CONFIRMATION_FORM, result ) - expect(subject.idv_session.phone_confirmation).to eq false - expect(Idv::PhoneValidator).to_not have_received(:new) + expect(subject.idv_session.phone_confirmation).to be_falsy end end @@ -91,7 +91,10 @@ result = { success: true, errors: {} } expect(@analytics).to have_received(:track_event).with( - Analytics::IDV_PHONE_CONFIRMATION, result + Analytics::IDV_PHONE_CONFIRMATION_FORM, result + ) + expect(@analytics).to have_received(:track_event).with( + Analytics::IDV_PHONE_CONFIRMATION_VENDOR, result ) end @@ -111,7 +114,10 @@ expect(flash[:warning]).to match t('idv.modal.phone.heading') expect(flash[:warning]).to match t('idv.modal.attempts', count: max_attempts - 1) expect(@analytics).to have_received(:track_event).with( - Analytics::IDV_PHONE_CONFIRMATION, result + Analytics::IDV_PHONE_CONFIRMATION_FORM, success: true, errors: {} + ) + expect(@analytics).to have_received(:track_event).with( + Analytics::IDV_PHONE_CONFIRMATION_VENDOR, result ) end diff --git a/spec/controllers/verify/sessions_controller_spec.rb b/spec/controllers/verify/sessions_controller_spec.rb index b0e90722b48..67ff7dc8888 100644 --- a/spec/controllers/verify/sessions_controller_spec.rb +++ b/spec/controllers/verify/sessions_controller_spec.rb @@ -104,13 +104,11 @@ result = { success: false, - idv_attempts_exceeded: false, errors: { ssn: [t('idv.errors.duplicate_ssn')] }, - vendor: { reasons: nil }, } expect(@analytics).to receive(:track_event). - with(Analytics::IDV_BASIC_INFO_SUBMITTED, result) + with(Analytics::IDV_BASIC_INFO_SUBMITTED_FORM, result) post :create, profile: user_attrs.merge(ssn: '666-66-1234') @@ -129,15 +127,21 @@ end context 'missing fields' do - it 'checks for required fields' do - partial_attrs = user_attrs.dup - partial_attrs.delete :first_name + let(:partial_attrs) do + user_attrs.tap { |attrs| attrs.delete :first_name } + end + it 'checks for required fields' do post :create, profile: partial_attrs expect(response).to render_template(:new) expect(flash[:warning]).to be_nil end + + it 'does not increment attempts count' do + expect { post :create, profile: partial_attrs }. + to_not change(user, :idv_attempts) + end end context 'un-resolvable attributes' do @@ -164,7 +168,7 @@ } expect(@analytics).to have_received(:track_event). - with(Analytics::IDV_BASIC_INFO_SUBMITTED, result) + with(Analytics::IDV_BASIC_INFO_SUBMITTED_VENDOR, result) end end @@ -189,7 +193,7 @@ } expect(@analytics).to have_received(:track_event). - with(Analytics::IDV_BASIC_INFO_SUBMITTED, result) + with(Analytics::IDV_BASIC_INFO_SUBMITTED_VENDOR, result) expect(response).to render_template(:new) end end @@ -206,7 +210,11 @@ } expect(@analytics).to have_received(:track_event). - with(Analytics::IDV_BASIC_INFO_SUBMITTED, result) + with(Analytics::IDV_BASIC_INFO_SUBMITTED_VENDOR, result) + end + + it 'increments attempts count' do + expect { post :create, profile: user_attrs }.to change(user, :idv_attempts).by(1) end end diff --git a/spec/services/idv/financials_step_spec.rb b/spec/services/idv/financials_step_spec.rb index 9f675b75fe4..b20fbd952fc 100644 --- a/spec/services/idv/financials_step_spec.rb +++ b/spec/services/idv/financials_step_spec.rb @@ -7,31 +7,19 @@ idvs.vendor = :mock idvs end - let(:idv_finance_form) { Idv::FinanceForm.new(idv_session.params) } + let(:idv_form_params) { idv_session.params } - def build_step(params) + def build_step(vendor_params) described_class.new( - idv_form: idv_finance_form, + idv_form_params: idv_form_params, idv_session: idv_session, - params: params + vendor_params: vendor_params ) end describe '#submit' do - it 'returns FormResponse with success: false for invalid params' do - step = build_step(finance_type: :ccn, ccn: '1234') - errors = { ccn: [t('idv.errors.invalid_ccn')] } - - result = step.submit - expect(result).to be_kind_of(FormResponse) - expect(result.success?).to eq(false) - expect(result.errors).to eq(errors) - - expect(idv_session.financials_confirmation).to eq false - end - it 'returns FormResponse with success: true for mock-happy CCN' do - step = build_step(finance_type: :ccn, ccn: '12345678') + step = build_step(ccn: '12345678') result = step.submit expect(result).to be_kind_of(FormResponse) @@ -39,11 +27,11 @@ def build_step(params) expect(result.errors).to be_empty expect(idv_session.financials_confirmation).to eq true - expect(idv_session.params).to eq idv_finance_form.idv_params + expect(idv_session.params).to eq idv_form_params end it 'returns FormResponse with success: false for mock-sad CCN' do - step = build_step(finance_type: :ccn, ccn: '00000000') + step = build_step(ccn: '00000000') errors = { ccn: ['The ccn could not be verified.'] } diff --git a/spec/services/idv/phone_step_spec.rb b/spec/services/idv/phone_step_spec.rb index 3e5ccd84af3..7dacb2ace6c 100644 --- a/spec/services/idv/phone_step_spec.rb +++ b/spec/services/idv/phone_step_spec.rb @@ -12,30 +12,17 @@ end let(:idv_phone_form) { Idv::PhoneForm.new(idv_session.params, user) } - def build_step(params) + def build_step(phone) described_class.new( - idv_form: idv_phone_form, idv_session: idv_session, - params: params + idv_form_params: { phone: phone }, + vendor_params: phone ) end describe '#submit' do - it 'returns false for invalid-looking phone' do - step = build_step(phone: '555') - - errors = { phone: [invalid_phone_message] } - - result = step.submit - - expect(result).to be_kind_of(FormResponse) - expect(result.success?).to eq(false) - expect(result.errors).to eq(errors) - expect(idv_session.phone_confirmation).to eq false - end - it 'returns true for mock-happy phone' do - step = build_step(phone: '555-555-0000') + step = build_step('555-555-0000') result = step.submit @@ -47,7 +34,7 @@ def build_step(params) end it 'returns false for mock-sad phone' do - step = build_step(phone: '555-555-5555') + step = build_step('555-555-5555') errors = { phone: ['The phone number could not be verified.'] } diff --git a/spec/services/idv/profile_step_spec.rb b/spec/services/idv/profile_step_spec.rb index 03ec4ca0ee1..3906d020b1d 100644 --- a/spec/services/idv/profile_step_spec.rb +++ b/spec/services/idv/profile_step_spec.rb @@ -19,10 +19,13 @@ end def build_step(params) + idv_session.params.merge!(params) + idv_session.applicant = idv_session.vendor_params + described_class.new( - idv_form: idv_profile_form, - idv_session: idv_session, - params: params + idv_form_params: params, + vendor_params: idv_session.vendor_params, + idv_session: idv_session ) end @@ -62,24 +65,6 @@ def build_step(params) expect(idv_session.profile_confirmation).to be_nil end - it 'fails when form validation fails' do - step = build_step(user_attrs.merge(ssn: '6666')) - - errors = { ssn: [t('idv.errors.pattern_mismatch.ssn')] } - extra = { - idv_attempts_exceeded: false, - vendor: { reasons: nil }, - } - - result = step.submit - - expect(result).to be_kind_of(FormResponse) - expect(result.success?).to eq(false) - expect(result.errors).to eq(errors) - expect(result.extra).to eq(extra) - expect(idv_session.profile_confirmation).to be_nil - end - it 'fails with invalid first name' do step = build_step(user_attrs.merge(first_name: 'Bad')) @@ -134,16 +119,11 @@ def build_step(params) expect(idv_session.profile_confirmation).to be_nil end - it 'increments attempts count if the form is valid' do + it 'increments attempts count' do step = build_step(user_attrs) expect { step.submit }.to change(user, :idv_attempts).by(1) end - it 'does not increment the attempts count if the form is not valid' do - step = build_step(user_attrs.merge(ssn: '666')) - expect { step.submit }.to change(user, :idv_attempts).by(0) - end - it 'initializes the idv_session' do step = build_step(user_attrs) step.submit From d0ed55dde2ac1261d75e2370917fcef08b1e6f56 Mon Sep 17 00:00:00 2001 From: Zach Margolis Date: Mon, 3 Jul 2017 10:41:09 -0400 Subject: [PATCH 16/22] Check in French translations **Why**: Results back from vendor --- config/application.yml.example | 4 +- config/locales/account/fr.yml | 32 +++++ config/locales/activerecord/fr.yml | 11 ++ config/locales/devise/fr.yml | 153 +++++++++++++++++++++ config/locales/errors/fr.yml | 35 +++++ config/locales/event_types/fr.yml | 13 ++ config/locales/forms/fr.yml | 48 +++++++ config/locales/headings/fr.yml | 32 +++++ config/locales/help_text/fr.yml | 14 ++ config/locales/idv/fr.yml | 191 +++++++++++++++++++++++++++ config/locales/instructions/fr.yml | 51 +++++++ config/locales/jobs/fr.yml | 13 ++ config/locales/links/fr.yml | 36 +++++ config/locales/mailer/fr.yml | 32 +++++ config/locales/notices/fr.yml | 52 ++++++++ config/locales/openid_connect/fr.yml | 24 ++++ config/locales/pages/fr.yml | 6 + config/locales/saml_idp/fr.yml | 12 ++ config/locales/shared/fr.yml | 7 + config/locales/sign_up/fr.yml | 16 +++ config/locales/simple_form/fr.yml | 11 ++ config/locales/time/fr.yml | 5 + config/locales/titles/fr.yml | 33 +++++ config/locales/tooltips/fr.yml | 7 + config/locales/user_mailer/fr.yml | 30 +++++ config/locales/users/fr.yml | 22 +++ config/locales/valid_email/fr.yml | 7 + config/locales/zxcvbn/fr.yml | 51 +++++++ 28 files changed, 946 insertions(+), 2 deletions(-) create mode 100644 config/locales/account/fr.yml create mode 100644 config/locales/activerecord/fr.yml create mode 100644 config/locales/devise/fr.yml create mode 100644 config/locales/errors/fr.yml create mode 100644 config/locales/event_types/fr.yml create mode 100644 config/locales/forms/fr.yml create mode 100644 config/locales/headings/fr.yml create mode 100644 config/locales/help_text/fr.yml create mode 100644 config/locales/idv/fr.yml create mode 100644 config/locales/instructions/fr.yml create mode 100644 config/locales/jobs/fr.yml create mode 100644 config/locales/links/fr.yml create mode 100644 config/locales/mailer/fr.yml create mode 100644 config/locales/notices/fr.yml create mode 100644 config/locales/openid_connect/fr.yml create mode 100644 config/locales/pages/fr.yml create mode 100644 config/locales/saml_idp/fr.yml create mode 100644 config/locales/shared/fr.yml create mode 100644 config/locales/sign_up/fr.yml create mode 100644 config/locales/simple_form/fr.yml create mode 100644 config/locales/time/fr.yml create mode 100644 config/locales/titles/fr.yml create mode 100644 config/locales/tooltips/fr.yml create mode 100644 config/locales/user_mailer/fr.yml create mode 100644 config/locales/users/fr.yml create mode 100644 config/locales/valid_email/fr.yml create mode 100644 config/locales/zxcvbn/fr.yml diff --git a/config/application.yml.example b/config/application.yml.example index 3d9d481d90c..6a8e465e094 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -53,7 +53,7 @@ development: attribute_cost: '4000$8$4$' # SCrypt::Engine.calibrate(max_time: 0.5) attribute_encryption_key: '2086dfbd15f5b0c584f3664422a1d3409a0d2aa6084f65b6ba57d64d4257431c124158670c7655e45cabe64194f7f7b6c7970153c285bdb8287ec0c4f7553e25' attribute_encryption_key_queue: '["old-key-one", "old-key-two"]' - available_locales: 'en es' + available_locales: 'en es fr' aws_kms_key_id: 'alias/login-dot-gov-development-keymaker' aws_region: 'us-east-1' dashboard_api_token: 'test_token' @@ -164,7 +164,7 @@ test: attribute_cost: '800$8$1$' # SCrypt::Engine.calibrate(max_time: 0.01) attribute_encryption_key: '2086dfbd15f5b0c584f3664422a1d3409a0d2aa6084f65b6ba57d64d4257431c124158670c7655e45cabe64194f7f7b6c7970153c285bdb8287ec0c4f7553e25' attribute_encryption_key_queue: '["old-key-one", "old-key-two"]' - available_locales: 'en es' + available_locales: 'en es fr' aws_kms_key_id: 'alias/login-dot-gov-test-keymaker' aws_region: 'us-east-1' domain_name: 'www.example.com' diff --git a/config/locales/account/fr.yml b/config/locales/account/fr.yml new file mode 100644 index 00000000000..643b1ae1eba --- /dev/null +++ b/config/locales/account/fr.yml @@ -0,0 +1,32 @@ +--- +fr: + account: + index: + address: Adresse actuelle + auth_app_disabled: non activée + auth_app_enabled: activée + authentication_app: Application d'authentification + dob: Date de naissance + email: Adresse courriel + full_name: Nom complet + login: Information de connexion + password: Mot de passe + phone: Numéro de téléphone + previous_address: Adresse précédente + reactivation: + instructions: Votre profil a été récemment désactivé en raison d'une réinitialisation de mot passe. Vous pouvez utiliser votre clé personnelle pour réactiver votre profil. + link: NOT TRANSLATED YET + ssn: Numéro d'assurance sociale + verification: + instructions: Votre compte requiert la vérification d'un code secret. + reactivate_button: Entrez le code que vous avez reçu par US mail + success: Votre compte a été vérifié. + with_phone_button: Verifiez avec votre téléphone + items: + personal_key: Clé personnelle + links: + regenerate_personal_key: Obtenez une nouvelle clé + security: + link: En apprendre davantage dans le Centre d'aide + text: L'information de votre profil est verrouillée pour votre sécurité. + welcome: Bienvenue diff --git a/config/locales/activerecord/fr.yml b/config/locales/activerecord/fr.yml new file mode 100644 index 00000000000..86bdb29d16f --- /dev/null +++ b/config/locales/activerecord/fr.yml @@ -0,0 +1,11 @@ +--- +fr: + activerecord: + errors: + models: + app_setting: + attributes: + value: + cannot_disable_2fa_in_prod: L'authentification à deux facteurs ne peut + être désactivée en production + invalid: La valeur doit être de '1' ou de '0' diff --git a/config/locales/devise/fr.yml b/config/locales/devise/fr.yml new file mode 100644 index 00000000000..d0c4eda7908 --- /dev/null +++ b/config/locales/devise/fr.yml @@ -0,0 +1,153 @@ +--- +fr: + devise: + confirmations: + already_confirmed: Votre adresse courriel a déjà été confirmée. %{action} + confirmed: Vous avez confirmé votre adresse courriel + confirmed_but_must_set_password: Vous avez confirmé votre adresse courriel + send_instructions: 'Vous recevrez dans quelques instants un courriel avec des instructions pour confirmer votre adresse courriel. + +' + send_paranoid_instructions: 'Vous recevrez dans quelques instants un courriel avec des instructions pour la confirmation de votre adresse courriel. + +' + failure: + already_authenticated: '' + inactive: Votre compte n'est pas encore activé. + invalid: Adresse courriel ou mot de passe non valide. + last_attempt: Il vous reste un essai avant que votre compte ne soit verrouillé. + locked: Votre compte est maintenant verrouillé. + not_found_in_database: Adresse courriel ou mot de passe non valide. + session_limited: 'Vos authentifiants ont été utilisés dans un autre navigateur. Veuillez vous connecter de nouveau pour continuer avec ce navigateur. + +' + timeout: Votre session est expirée. Veuillez vous connecter de nouveau pour continuer. + unauthenticated: '' + unconfirmed: Vous devez confirmer votre adresse courriel avant de continuer. + mailer: + account_locked: + subject: Votre compte login.gov a été verrouillé + confirmation_instructions: + subject: Confirmez votre adresse courriel + password_updated: + subject: Votre mot de passe a été modifié + reset_password_instructions: + subject: Réinitialisez votre mot de passe + passwords: + choose_new_password: Choisissez un nouveau mot de passe. + invalid_token: Le jeton de réinitialisation de mot de passe n'est pas valide. Veuillez essayer de nouveau. + no_token: 'Vous ne pouvez accéder à cette page que depuis un courriel de réinitialisation de mot de passe. Si vous avez été redirigé à partir d''un courriel de réinitialisation de mot de passe, veuillez vous assurer que vous avez utilisé le lien fourni complet. + +' + send_instructions: 'Vous recevrez dans quelques instants un courriel avec des instructions pour réinitialiser votre mot de passe. + +' + send_paranoid_instructions: 'Vous recevrez dans quelques instants un courriel avec des instructions pour réinitialiser votre mot de passe. + +' + token_expired: Vous avez pris trop de temps pour réinitialiser votre mot de passe. Veuillez essayer de nouveau. + updated: Votre mot de passe a été modifié. Vous êtes maintenant connectée(e). + updated_not_active: 'Votre mot de passe a été modifié. Veuillez vous connecter avec votre nouveau mot de passe. + +' + registrations: + close_window: Vous pouvez fermer cette fenêtre si vous avez terminé. + destroy_confirm: 'La suppression de votre compte est irréversible. Toutes les données associées à votre compte seront effacées. Souhaitez-vous vraiment supprimer votre compte? + +' + destroyed: Votre compte a bien été supprimé. + email_and_phone_need_confirmation: 'Avant de terminer la mise-à-jour de votre compte, nous devons confirmer votre nouveau numéro et votre nouvelle adresse courriel. Veuillez suivre les instructions ci-dessous pour confirmer votre nouveau numéro, puis surveillez la réception d''un courriel envoyé par login.gov. Suivez le lien inclus dans le courriel pour confirmer votre nouvelle adresse courriel. + +' + email_update_needs_confirmation: 'Vous avez mis à jour votre compte, mais nous devons confirmer votre nouvelle adresse courriel. Surveillez la réception d''un courriel envoyé par nous et suivez le lien inscrit pour confirmer votre nouvelle adresse courriel. + +' + enabled_twofactor: Vous avez activé l'authentication à deux facteurs. + phone_update_needs_confirmation: 'Votre demande de mise à jour de votre numéro de téléphone a été traitée, mais nous devons d''abord confirmer votre nouveau numéro. Veuillez suivre les instructions ci-dessous. Si vous ne confirmez pas votre nouveau numéro, nous continuerons d''utiliser votre ancien numéro de téléphone. + +' + signed_up: Bienvenue! Vous avez créé un compte. + signed_up_but_inactive: 'Vous avez créé un compte. Cependant, nous n''avons pu vous connecter, car votre compte n''est pas encore activé. + +' + signed_up_but_locked: 'Vous avez créé un compte. Cependant, nous n''avons pu vous connecter, car votre compte est verrouillé. + +' + start: + accordion: Vous devrez + bullet_1_html: fournir votre adresse courriel et créer un mot de passe fort. + bullet_2_html: 'Activez l''authentication à deux facteurs. Cela permet d''ajouter un degré de sécurité à votre compte, car vous devez entrer un nouveau code envoyé à votre téléphone chaque fois que vous vous connectez. + +' + bullet_3_html: 'Fournissez de l''information de base, comme votre nom, adresse et numéro de sécurité sociale. + +' + bullet_4_html: 'Fournissez un numéro de compte bancaire, comme votre numéro de carte de crédit. + +' + bullet_5_html: 'Lorsque vous aurez terminé ce processus, nous vous donnerons une clé personnelle. Notez-la et placez-la en lieu sûr : c''est important. On vous demandera la clé personnelle chaque fois que vous apporterez des changements à votre compte. + +' + learn_more: En savoir plus sur la vérification de votre identité + updated: Votre compte a été mis à jour! + sessions: + already_signed_out: Vous êtes maintenant connecté(e). + signed_in: '' + signed_out: Vous êtes maintenant déconnecté(e). + two_factor_authentication: + buttons: + confirm_with_sms: Confirmer avec des messages texte + confirm_with_voice: Confirmer avec un appel vocal + choose_delivery_confirmation: 'Comment souhaitez-vous recevoir votre code de sécurité de confirmation pour %{phone}? + +' + choose_otp_delivery_html: 'Nous l''envoyons à %{phone} immédiatement. Les tarifs liés aux messages texte et aux données peuvent s''appliquer. + +' + contact_administrator: Veuillez communiquer avec votre administrateur système. + header_text: Entrez votre code de sécurité + invalid_otp: 'Ce code de sécurité est non valide. Vous pouvez essayer de l''entrer de nouveau ou demander un nouveau code de sécurité à utilisation unique. + +' + invalid_personal_key: Cette clé personnelle est non valide. + max_generic_login_attempts_reached: 'Votre compte est temporairement verrouillé. + +' + max_otp_login_attempts_reached: 'Votre compte est temporairement verrouillé, car vous avez entré le code de sécurité à utilisation unique de façon erronée à de trop nombreuses reprises. + +' + max_otp_requests_reached: NOT TRANSLATED YET + max_personal_key_login_attempts_reached: 'Votre compte est temporairement verrouillé, car vous avez entré le code de sécurité à utilisation unique de façon erronée à de trop nombreuses reprises. + +' + otp_delivery_preference: + instruction: Vous pourrez changer votre choix la prochaine fois que vous vous connecterez + sms: Message texte (SMS) + title: Comment souhaitez-vous recevoir votre code de sécurité? + voice: Appel téléphonique + otp_phone_label: Numéro de téléphone + otp_phone_label_info: Cellulaire ou ligne fixe est acceptable + otp_setup_html: "Chaque fois que vous vous connecterez, nous vous enverrons un code de sécurité à utilisation unique par message texte ou par appel téléphonique. Cela aide à protéger votre compte.\n" + otp_sms_disclaimer: Les tarifs liés aux messages texte et aux données peuvent s'appliquer. + personal_key_fallback: + link: Utlisez plutôt une clé personnelle + text_html: Vous n'avez pas accès à votre téléphone? %{link}. + personal_key_header_text: Entrez votre clé personnelle + personal_key_prompt: 'Vous pouvez utiliser cette clé personnelle une fois seulement. Si vous avez toujours besoin d''un code après votre connexion, allez à la page des réglages de votre compte pour en obtenir un nouveau. + +' + please_confirm: 'Votre numéro de téléphone a été entré. Confirmez-le en entrant le code de sécurité ci-dessous. + +' + please_try_again_html: Veuillez essayer de nouveau dans %{time_remaining}. + totp_fallback: + sms_link_text: Obtenir un code via message texte + text_html: 'Si vous ne pouvez utiliser votre application d''authentification maintenant, vous pouvez %{sms_link} ou %{voice_link}. + +' + voice_link_text: obtenir un code par appel téléphonique + totp_header_text: Entrez votre code d'application d'authentification + totp_info: Utilisez n'importe quelle application d'authentification pour balayer le code QR ci-dessous. + two_factor_setup: Ajoutez un numéro de téléphone + user: + new_otp_sent: Nous vous avons envoyé un code de sécurité à utilisation unique. diff --git a/config/locales/errors/fr.yml b/config/locales/errors/fr.yml new file mode 100644 index 00000000000..b96aad84756 --- /dev/null +++ b/config/locales/errors/fr.yml @@ -0,0 +1,35 @@ +--- +fr: + errors: + confirm_password_incorrect: Mot de passe incorrect. + invalid_authenticity_token: Oups, une erreur s'est produite. Veuillez vous connecter + de nouveau. + invalid_totp: Code non valide. Veuillez essayer de nouveau. + max_password_attempts_reached: Vous avez inscrit des mots de passe incorrects + un trop grand nombre de fois. Vous pouvez réinitialiser votre mot de passe en + utilisant le lien « Vous avez oublié votre mot de passe? ». + messages: + already_confirmed: a déjà été confirmé, veuillez essayer de vous connecter + blank: Veuillez remplir ce champ. + confirmation_code_incorrect: Code non valide. L'avez-vous inscrit correctement? + confirmation_invalid_token: Lien de confirmation non valide. Le lien est expiré ou vous avez déjà confirmé votre compte. + confirmation_period_expired: Lien de confirmation expiré. Vous pouvez cliquer sur « Envoyer les instructions de confirmation de nouveau » pour en obtenir un autre. + expired: est expiré, veuillez en demander un nouveau + format_mismatch: Veuillez vous assurer de respecter le format requis. + improbable_phone: Numéro de téléphone non valide. Veuillez vous assurer d'entrer un numéro de téléphone à 10 chiffres. + missing_field: Veuillez remplir ce champ. + no_password_reset_profile: Aucun profil récemment désactivé en raison d'une réinitialisation de mot de passe + no_pending_profile: Aucun profil en attente de vérification + not_found: introuvable + not_locked: n'a pas été verrouillé + not_saved: + one: "1 erreur a interdit la sauvegarde de cette %{resource} :" + other: "%{count} des erreurs ont empêché la sauvegarde de cette %{resource} :" + otp_incorrect: Code non valide. L'avez-vous inscrit correctement? + password_incorrect: Mot de passe incorrect + personal_key_incorrect: Clé personnelle incorrecte + requires_phone: vous demande d'entrer votre numéro de téléphone. + unauthorized_authn_context: Contexte d'authentification non autorisé + unauthorized_service_provider: Fournisseur de service non autorisé + weak_password: Votre mot de passe n'est pas assez fort. %{feedback} + not_authorized: Vous n'êtes pas autorisé(e) à effectuer cette action. diff --git a/config/locales/event_types/fr.yml b/config/locales/event_types/fr.yml new file mode 100644 index 00000000000..62131dacbc6 --- /dev/null +++ b/config/locales/event_types/fr.yml @@ -0,0 +1,13 @@ +--- +fr: + event_types: + account_created: Compte créé + account_verified: Compte vérifié + authenticated_at: Connecté à %{service_provider} + authenticator_disabled: Application d'authentification désactivée + authenticator_enabled: Application d'authentification activée + eastern_timestamp: "%{timestamp} (Eastern)" + email_changed: Adresse courriel modifiée + phone_changed: Numéro de téléphone modifié + phone_confirmed: Numéro de téléphone confirmé + usps_mail_sent: Lettre envoyée diff --git a/config/locales/forms/fr.yml b/config/locales/forms/fr.yml new file mode 100644 index 00000000000..22885c22e37 --- /dev/null +++ b/config/locales/forms/fr.yml @@ -0,0 +1,48 @@ +--- +fr: + forms: + buttons: + back: Retour + continue: Continuer + disable: Désactiver + edit: Modifier + enable: Activer + resend_confirmation: 'Envoyer les instructions de confirmation de nouveau ' + send_security_code: Envoyer le code de sécurité + submit: + confirm_change: Confirmer le changement + default: Soumettre + update: Mettre à jour + confirmation: + show_hdr: Créez un mot de passe fort + passwords: + edit: + buttons: + submit: Changer le mot de passe + labels: + password: Nouveau mot de passe + show: Afficher le mot de passe + personal_key: + alternative: NOT TRANSLATED YET + confirmation_label: Clé personnelle + instructions: Veuillez confirmer que vous avez une copie de votre clé personnelle en l'entrant ci-dessous. + title: Entrez votre clé personnelle + registration: + labels: + email: Adresse courriel + totp_setup: + totp_intro_html: 'Lorsque vous vous connectez, vous pouvez obtenir votre code de sécurité d''une application d''authentification. %{link} + +' + totp_step_1: Démarrez votre application d'authentification + totp_step_2: Entrez cette clé dans l'application + totp_step_3: Entrez le code à partir de l'application + two_factor: + code: Code de sécurité + personal_key: Clé personnelle + try_again: Utilisez un autre numéro de téléphone + verify_profile: + instructions: Entrez le code à dix caractères qui se trouve dans la lettre que nous vous avons envoyée. + name: Code de confirmation + submit: Confirmer le compte + title: Confirmez votre compte diff --git a/config/locales/headings/fr.yml b/config/locales/headings/fr.yml new file mode 100644 index 00000000000..4f72ab3c2c9 --- /dev/null +++ b/config/locales/headings/fr.yml @@ -0,0 +1,32 @@ +--- +fr: + headings: + account: + account_history: Historique du compte + login_info: Votre compte + profile_info: Information du profil + reactivate: NOT TRANSLATED YET + two_factor: Authentication à deux facteurs + verified_account: Compte vérifié + confirmations: + new: Envoyer un autre courriel de confirmation + create_account_with_sp: + sp_text: utilise login.gov pour rendre la connexion plus facile et plus sécurisée. + create_account_without_sp: Créer un compte login.gov + edit_info: + email: Changez votre courriel + password: Changez votre mot de passe + phone: Entrez votre nouveau numéro de téléphone + passwords: + change: Changez votre mot de passe + confirm: Confirmez votre mot de passe actuel pour continuer + forgot: Vous avez oublié votre mot de passe? + personal_key: Voici votre clé personnelle + registrations: + enter_email: Commencer à créer le compte + session_timeout_warning: Vous avez besoin de plus de temps? + sign_in_with_sp: Connectez-vous pour continuer à %{sp} + sign_in_without_sp: Connexion + totp_setup: + new: Activer une application d'authentification + verify_email: Consultez vos courriels diff --git a/config/locales/help_text/fr.yml b/config/locales/help_text/fr.yml new file mode 100644 index 00000000000..846349f8f36 --- /dev/null +++ b/config/locales/help_text/fr.yml @@ -0,0 +1,14 @@ +--- +fr: + help_text: + change_factor: 'Avant de pouvoir modifier votre %{factor}, vous devrez confirmer votre mot de passe et recevoir un code de sécurité sur votre téléphone. + +' + requested_attributes: + address: Adresse postale + birthdate: Date de naissance + email: Adresse courriel + full_name: Nom complet + intro_html: Il s'agit de la seule information que %{app_name} partagera avec %{sp} + phone: Numéro de téléphone + social_security_number: Numéro de sécurité sociale diff --git a/config/locales/idv/fr.yml b/config/locales/idv/fr.yml new file mode 100644 index 00000000000..30e470e53b8 --- /dev/null +++ b/config/locales/idv/fr.yml @@ -0,0 +1,191 @@ +--- +fr: + idv: + buttons: + activate_by_mail: Activer par la poste + activate_by_phone: Activer par téléphone + cancel: Annuler et retourner à votre profil + continue: Continuer la vérification d'identité + help: Continuer jusqu'au Centre d'aide + mail: + resend: Envoyer une autre lettre + send: Envoyer une lettre + cancel: + modal_header: Souhaitez-vous vraiment annuler? + warning_header: Si vous annulez maintenant + warning_points: + - Nous ne serons pas en mesure de vérifier votre identité + - Nous ne conserverons pas de dossier contenant votre nom, adresse, date de naissance ou numéro de sécurité sociale + - Vous ne pourrez pas accéder à votre information de manière sécuritaire en utilisant login.gov + - Vous conserverez un compte login.gov pour votre adresse courriel + - Vous pouvez gérer ce compte sur la page de votre profil + errors: + bad_dob: Votre date de naissance doit être une date valide. + duplicate_ssn: Un compte existe déjà avec l'information que vous avez fournie. + finance_number_length: Le nombre doit comprendre entre %{minimum} et %{maximum} chiffres. + hardfail: Nous sommes incapables de vérifier votre identité pour le moment. + incorrect_password: Le mot de passe que vous avez inscrit est incorrect. + invalid_ccn: Vous devez inscrire seulement les 8 derniers chiffres du numéro de carte de crédit. + mail_limit_reached: Vous avez demandé trop de courriels au cours du dernier mois. + missing_finance: Vous devez fournir un numéro de compte bancaire. + pattern_mismatch: + dob: 'Votre date de naissance doit être inscrite de cette façon: mm/jj/aaaa' + personal_key: 'Veuillez inscrire votre clé personnelle pour ce compte, par exemple : ABC1-DEF2-G3HI-J456 + +' + ssn: 'Votre numéro de sécurité sociale doit être inscrit de cette façon : ###-##-####' + zipcode: 'Votre code zip doit être inscrit de cette façon : #####-####' + form: + activate_by_mail: Activer mon compte par la poste. + address1: Adresse 1 + address2: Adresse 2 (optional) + auto_loan: Numéro du prêt automobile + ccn: Les 8 derniers chiffres d'une carte de crédit + city: Ville + dob: Date de naissance + dob_hint: 'exemple : 01/17/1964' + first_name: Prénom + home_equity_line: Numéro du compte de crédit hypothécaire + last_name: Nom de famille + mortgage: Numéro du compte de prêt hypothécaire + no_alternate_phone_html: Je n'ai pas de numéro de téléphone. %{link} + password: Mot de passe + personal_details: Information personnelle + phone: Numéro de téléphone + phone_label_aside: Ligne mobile ou fixe + previous_address_add: Inscrire votre adresse précédente + previous_address_html: A déménagé au cours des 3 derniers mois? + select_financial_account: Sélectionnez un compte bancaire… + ssn_label_html: Numéro de sécurité sociale %{tooltip} + state: État + use_ccn: Fournissez un numéro de carte de crédit > + use_financial_account: Vous n'avez pas de carte de crédit? + zipcode: Code ZIP + index: + continue_link: Oui, continuer + paragraph_1: "Nous avons un partenariat avec une entreprise de confiance afin de vérifier cette information ainsi que votre identité. \n" + prompt: Est-ce que vous avez accès à toute cette information en ce moment? + section_1: + bullet_1: Nom complet + bullet_2: Adresse postale + bullet_3: Date de naissance + bullet_4: Numéro de sécurité sociale + header: Information personnelle + subheader: 'Nous allons commencer avec de l''information personnelle, comme:' + section_2: + bullet_1: Les 8 derniers chiffres d'une carte de crédit + bullet_2: Numéro du compte de prêt automobile + bullet_3: Numéro du compte de crédit hypothécaire + bullet_4: Numéro du compte de prêt hypothécaire + footnote: "Vous n'aurez jamais à payer quoi que ce soit avec nous, ni à partager le solde de vos comptes. Nous vérifions uniquement le numéro du compte pour vérifier votre identité. \n" + header: Information bancaire + subheader: 'Nous avons besoin de l''un des numéros de compte suivants:' + section_3: + bullet_1: Votre nom ou celui d'un membre de votre famille + bullet_2_html: "Not un téléphone virtuel, comme Google Voice ou Skype\n" + bullet_3_html: "Not un téléphone prépayé\n" + header: Une ligne téléphonique à votre nom + subheader: "Nous utilisons les informations des compagnies de téléphone pour vérifier votre identité. Le numéro de téléphone que vous nous donnez devrait: \n" + subheader: Pour vérifier votre identité, nous vous demandons de répondre à quelques questions + messages: + activated_html: 'Votre identité a été vérifiée. Si vous souhaitez modifier votre information vérifiée, veuillez %{link}. + +' + activated_link: communiquer avec nous + cancel: "Pour continuer, %{app} doit vérifier votre identité. Nous devons recueillir quelques informations personnelles de base ainsi que certaines informations financières pour compléter ce processus. Si vous n'avez pas cette information sous la main, vous pouvez continuer plus tard. \n" + confirm: Vous avez crypté vos données vérifiées + dupe_ssn1: "Si vous recevez ce message d'erreur, il est possible que vous ayez déjà créé et vérifié un compte, mais avec une adresse courriel différente. \n" + dupe_ssn2_html: Veuillez %{link} avec l'adresse courriel que vous avez utilisée originalement. + dupe_ssn2_link: déconnectez-vous puis connectez-vous à nouveau + finance: + disclaimer: En continuant, vous acceptez que %{app_name} compare l'information du compte que vous avez fournie avec des sources tiers afin de vérifier votre identité. + hint: Vous pouvez utiliser Mastercard, Visa, Diner's Club, ou JCB. + intro_account: 'Aidez-nous à vérifier votre identité en fournissant le numéro de votre prêt automobile, de votre prêt hypothécaire, ou de votre crédit hypothécaire. + +' + intro_ccn_help: comment votre information sera utilisée + intro_ccn_html: 'Aidez-nous à vérifier votre identité en fournissant les 8 derniers chiffres de votre carte de crédit. Pour en apprendre davantage sur %{intro_help_link}. + +' + no_account: Vous préférez utiliser une carte de crédit? + no_account_info: Aidez-nous à vérifier votre identité en fournissant les 8 derniers chiffres de votre carte de crédit. + hardfail: Nous ne pouvons pas vérifier votre identité pour le moment. + hardfail4: Vous pouvez également aller sur %{sp} où vous trouverez davantage d'aide pour accéder à nos services. + help_center: Visitez notre Centre d'aide pour en apprendre davantage sur la façon dont nous vérifions votre compte. + mail_sent: Votre lettre est en route + personal_details_verified: Information personnelle vérifiée! + personal_key: 'Il s''agit de votre nouvelle clé personnelle. Notez-la et conservez-la dans un endroit sécuritaire. Vous en aurez besoin si vous perdez votre mot de passe. + +' + phone: + alert: Cette ligne téléphonique doit être + in_your_name: en votre nom ou celui d'un membre de votre famille + intro: Nous pouvez envoyez un code de confirmation à un numéro de téléphone lié à votre identité légale seulement. + phone_of_record: appels téléphoniques + prepaid: avec un contrat, et non prépayé + same_as_2fa: 'Ce numéro de téléphone peut être le même que celui que vous utilisez pour configurer votre mot de passe à usage unique, tant et aussi longtemps qu''il respecte les critères mentionnés plus haut. + +' + review: + financial_info: Où se trouve l'information sur mon compte bancaire? + info_verified_html: Nous avons trouvé des enregistrements liés à votre téléphone%{phone_message} + intro: Vos informations vérifiées + select_verification_with_sp: Afin de vous protéger des fraudes d'identité, vous ne pouvez pas utiliser votre compte au %{sp_name} tant que vous ne l'aurez pas activé en entrant votre code de confirmation. + select_verification_without_sp: Afin de protéger votre compte des fraudes liées à l'identité, votre profil ne sera pas activé tant que vous n'aurez pas entré votre code de confirmation. + sessions: + no_pii: N'utilisez pas de véritables données personnelles (il s'agit d'une démonstration seulement) + pii: Information personnelle + success: Nous avons trouvé des données qui correspondent à %{pii_message} + usps: + bad_address: Je ne peux pas recevoir de courrier à cette adresse + byline: Nous posterons une lettre à l'adresse vérifiée dans nos dossiers. Celle-ci contient un code de confirmation. + resend: Envoyez-moi une autre lettre. + success: Elle devrait arriver dans 5 à 10 jours ouvrables. + modal: + attempts: + one: Il ne vous reste qu' une tentative. + other: Il ne vous reste que %{count} tentatives. + button: + fail: OK + warning: Essayez à nouveau + financials: + fail: 'Votre compte est verrouillé pour 24 heures durant lesquelles vous ne pourrez pas y accéder. Consultez notre Centre d''aide pour plus d''information sur la vérification de votre identité. + +' + heading: Nous ne trouvons pas de données qui correspondent à vos données financières. + warning: Veuillez vérifier l'information que vous avez fournie. + phone: + fail: 'Votre compte est verrouillé pour 24 heures au cours desquelles vous ne pourrez pas y accéder. Consultez notre Centre d''aide pour plus d''information sur la vérification de votre identité. + +' + heading: Nous ne trouvons pas de données qui correspondent à vos informations téléphoniques. + warning: Veuillez vérifier l'information que vous avez fournie. + sessions: + fail: "Votre compte est verrouillé pour 24 heures au cours desquelles vous ne pourrez pas y accéder. \n" + heading: Nous ne pouvons pas trouver de données concordant avec vos informations personnelles. + warning: "Veuillez vérifier l'information que vous avez fournie. Un numéro de sécurité sociale, un code ZIP ou une date de naissance mal écrits sont des erreurs communes. \n" + review: + dob: Date de naissance + full_name: Nom complet + mailing_address: Adresse postale + ssn: Numéro de sécurité sociale (SSN) + titles: + activated: Votre identité a déjà été vérifiée + cancel: Nous ne pouvons pas vérifier votre identité + complete: Vérification de votre identité complétée + dupe: 'Erreur: un compte existe déjà' + expectations: Ensuite, aidez-vous à vous identifier + fail: Nous sommes dans l'incapacité de vérifier votre identité + financials: Veuillez fournir un numéro de compte bancaire + hardfail: Nous sommes dans l'incapacité de vérifier votre identité + intro: Aidez-nous à vous identifier + mail: + resend: Vous voulez une autre lettre? + verify: Vous voulez une lettre? + phone: Numéro de téléphone enregistré + review: Réviser et soumettre + select_verification: Activer votre compte + session: + phone: Obtenez un code par téléphone + review: Encryptez vos données personnelles en entrant votre mot de passe + sessions: D'abord, dites-nous qui vous êtes diff --git a/config/locales/instructions/fr.yml b/config/locales/instructions/fr.yml new file mode 100644 index 00000000000..92df49f1ec8 --- /dev/null +++ b/config/locales/instructions/fr.yml @@ -0,0 +1,51 @@ +--- +fr: + instructions: + 2fa: + authenticator: + accordion_header: Balayez le code avec votre appareil mobile + confirm_code_html: Entrez le code à partir de votre application d'authentification. Si vous avez plusieurs comptes configurés dans votre application, entrez le code correspondant à %{email} à %{app}.%{tooltip} + or: or + sms: + confirm_code_html: Nous l'avons envoyé par message texte à %{number}. Vous avez besoin d'un autre code? %{resend_code_link}. Les frais liés aux messages texte peuvent s'appliquer. + fallback_html: Si vous ne pouvez recevoir de messages texte pour le moment, vous pouvez %{link} + voice: + confirm_code_html: Nous venons de vous appeler au %{number}. Vous voulez que nous vous appelions de nouveau? %{resend_code_link} + fallback_html: Si vous ne pouvez recevoir d'appels pour le moment, vous pouvez %{link} + wrong_number_html: Vous avez entré un mauvais numéro de téléphone? %{link} + account: + reactivate: + begin: NOT TRANSLATED YET + explanation: NOT TRANSLATED YET + intro: NOT TRANSLATED YET + modal: + copy: NOT TRANSLATED YET + heading: NOT TRANSLATED YET + with_key: NOT TRANSLATED YET + forgot_password: + close_window: Vous pourrez fermer cette fenêtre de navigateur lorsque vous aurez réinitialisé votre mot de passe. + password: + forgot: 'Vous ne connaissez pas votre mot de passe? Réinitialisez-le après avoir confirmé votre adresse courriel. + +' + help_text: 'Plus long et inhabituel est le mot de passe, plus difficile il sera à trouver. Évitez donc d''utiliser des phrases communes. Évitez aussi de répéter des mots de passe d''autres comptes en ligne comme les comptes bancaires, les comptes courriel et les comptes de médias sociaux. + +' + help_text_header: Conseils sur la sécurité du mot de passe + info: + lead: 'Il doit avoir une longueur minimale de %{min_length} caractères et ne pas être un mot de passe couramment utilisé. C''est tout! + +' + strength: + i: Très faible + ii: Faible + iii: Correct + intro: 'Force du mot de passe : ' + iv: Bonne + v: Excellente! + personal_key_accent: Notez-la ou imprimez-la. + personal_key_html: 'Il s''agit de la seule façon de récupérer l''accès à votre compte si vous perdez votre mot de passe ou votre téléphone. %{accent} + +' + registration: + email: Choisissez une adresse que vous souhaitez utiliser pour les communications avec le gouvernement. diff --git a/config/locales/jobs/fr.yml b/config/locales/jobs/fr.yml new file mode 100644 index 00000000000..7d59cf58db8 --- /dev/null +++ b/config/locales/jobs/fr.yml @@ -0,0 +1,13 @@ +--- +fr: + jobs: + 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. + +' diff --git a/config/locales/links/fr.yml b/config/locales/links/fr.yml new file mode 100644 index 00000000000..60487a68a76 --- /dev/null +++ b/config/locales/links/fr.yml @@ -0,0 +1,36 @@ +--- +fr: + links: + account: + reactivate: + with_key: NOT TRANSLATED YET + without_key: NOT TRANSLATED YET + back_to_sp: Retour à %{sp} + cancel: Annuler + cancel_account_creation: "‹ Annuler la création du compte" + cancel_idv: "‹ Annuler la vérification du compte" + contact: Contact + copy: Copie + create_account: Créer un compte + help: Aide + next: Suivant + passwords: + forgot: Vous avez oublié votre mot de passe? + phone_confirmation: + auth_app_fallback_html: " ou %{link}." + fallback_to_sms_html: Envoyez-moi plutôt un message texte contenant le code + fallback_to_voice_html: Si vous ne pouvez recevoir de message texte pour le moment, vous pouvez obtenir un code de sécurité par %{link} + privacy_policy: Confidentialité et sécurité + remove: Retirer + resend: Envoyer le courriel de nouveau + reverify: NOT TRANSLATED YET + sign_in: Connexion + sign_out: Déconnexion + two_factor_authentication: + app: obtenir un code de sécurité par l'application d'authentification + resend_code: + sms: Recevoir un autre message texte + voice: Recevoir un autre appel téléphonique + sms: obtenir un code de sécurité par message texte + voice: obtenir un code par un appel téléphonique + what_is_totp: Qu'est-ce qu'une application d'authentification? diff --git a/config/locales/mailer/fr.yml b/config/locales/mailer/fr.yml new file mode 100644 index 00000000000..57717bd8818 --- /dev/null +++ b/config/locales/mailer/fr.yml @@ -0,0 +1,32 @@ +--- +fr: + mailer: + about: À propos %{app} + confirmation_instructions: + first_sentence: + confirmed: Vous tentez de changer votre adresse courriel? + reset_requested: 'Votre compte %{app} a été réinitialisé par un représentant + du soutien technique. Pour continuer, vous devez confirmer votre adresse + courriel. + +' + unconfirmed: Merci d'avoir créé un compte. + footer: Ce lien expirera dans %{confirmation_period}. + header: "%{intro} Veuillez cliquer sur le lien ci-dessous ou copier et coller + le lien complet dans votre navigateur.\n" + link_text: Confirmez votre adresse courriel + email_change_notice: + subject: Changez votre adresse courriel + email_reuse_notice: + subject: Cette adresse courriel est déjà associée à un compte. + help: Pour plus d'aide, visitez %{link}. + no_reply: Veuillez ne pas répondre à ce message. + privacy_policy: Politique de confidentialité + reset_password: + footer: Ce lien expire dans %{expires} heures. + header: 'Pour terminer la réinitialisation de votre mot de passe, veuillez cliquer + sur le lien ci-dessous ou copier et coller le lien complet dans votre navigateur. + +' + link_text: Réinitialisez votre mot de passe + sent_from: Envoyé à partir de %{app} diff --git a/config/locales/notices/fr.yml b/config/locales/notices/fr.yml new file mode 100644 index 00000000000..09a60022aa3 --- /dev/null +++ b/config/locales/notices/fr.yml @@ -0,0 +1,52 @@ +--- +fr: + notices: + account_reactivation: NOT TRANSLATED YET + dap_html: " \n" + forgot_password: + first_paragraph_end: 'avec un lien pour réinitialiser votre mot de passe. Suivez le lien pour continuer à réinitialiser votre mot de passe. + +' + first_paragraph_start: Nous avons envoyé un courriel à + no_email_sent_explanation_start: Vous n'avez pas reçu de courriel? + resend_email_success: Nous avons envoyé un autre courriel de réinitialisation de mot de passe. + use_diff_email: + link: Créer un nouveau compte + text_html: Ou, %{link} en utilisant une adresse courriel différente. + password_changed: Vous avez changé votre mot de passe. + resend_confirmation_email: + success: Nous avons envoyé un autre courriel de confirmation. + send_code: + personal_key: Vous avez une nouvelle clé personnelle. + session_cleared: 'Pour votre sécurité, nous effacerons l''information que vous avez entrée si vous ne vous déplacez pas vers une nouvelle page dans les %{minutes} prochaines minutes. + +' + signed_up_but_unconfirmed: + first_paragraph_end: 'avec un lien pour confirmer votre adresse courriel. Suivez le lien pour continuer à créer votre compte. + +' + first_paragraph_start: Nous avons envoyé un courriel à + no_email_sent_explanation_start: Vous n'avez pas reçu de courriel? + terms_of_service: + link: Pratiques en matière de sécurité et énoncé concernant la Loi sur la protection des renseignements personnels + timeout_warning: + partially_signed_in: + continue: Continuer la connexion + message_html: 'Pour votre sécurité, nous annulerons votre connexion dans %{time_left_in_session}. + +' + sign_out: Annuler la connexion + signed_in: + continue: Gardez ma connexion active + message_html: 'Pour votre sécurité, nous vous déconnecterons dans %{time_left_in_session}, sauf en cas d''avis contraire de votre part. + +' + sign_out: Déconnectez-moi + totp_configured: Vous avez activé l'application d'authentification. + totp_disabled: Vous avez désactivé l'application d'authentification. + use_diff_email: + link: utilisez une adresse courriel différente + text_html: Or, %{link} + session_timedout: 'Nous vous avons déconnecté. Pour votre sécurité, %{app} désactive votre session lorsque vous demeurez sur une page sans vous déplacer pendant %{minutes} minutes. + +' diff --git a/config/locales/openid_connect/fr.yml b/config/locales/openid_connect/fr.yml new file mode 100644 index 00000000000..c131dccca62 --- /dev/null +++ b/config/locales/openid_connect/fr.yml @@ -0,0 +1,24 @@ +--- +fr: + openid_connect: + authorization: + errors: + bad_client_id: Mauvaise client_id + no_valid_acr_values: Valeurs acr_values inacceptables trouvées + no_valid_scope: Aucune étendue de données valide trouvée + redirect_uri_invalid: redirect_uri est non valide + redirect_uri_no_match: redirect_uri ne correspond pas au redirect_uri enregistré + logout: + errors: + id_token_hint: id_token_hint n'a pas été reconnu + token: + errors: + invalid_aud: Affirmation liée à l'auditoire non valide, attendu %{url} + invalid_authentication: Le client doit s'authentifier par PKCE ou private_key_jwt, code_challenge ou client_assertion manquant + invalid_code: code non valide + invalid_code_verifier: code_verifier ne correspondait pas à code_challenge + user_info: + errors: + malformed_authorization: Forme de l'en-tête d'autorisation non valide + no_authorization: Aucune en-tête d'autorisation fournie + not_found: L'autorisation pour le contenu du access_token fourni introuvable ou il peut être expiré diff --git a/config/locales/pages/fr.yml b/config/locales/pages/fr.yml new file mode 100644 index 00000000000..15529918a62 --- /dev/null +++ b/config/locales/pages/fr.yml @@ -0,0 +1,6 @@ +--- +fr: + pages: + page_not_found: + body: Vous pouvez revérifier votre lien et essayer de nouveau. (404) + header: La page que vous recherchez n'existe pas. diff --git a/config/locales/saml_idp/fr.yml b/config/locales/saml_idp/fr.yml new file mode 100644 index 00000000000..a0fa4f5b4cd --- /dev/null +++ b/config/locales/saml_idp/fr.yml @@ -0,0 +1,12 @@ +--- +fr: + saml_idp: + shared: + saml_post_binding: + heading: Soumettre pour continuer + no_js: 'JavaScript semble être désactivé dans votre navigateur. Habituellement, + cette étape se déroule automatiquement, mais parce que vous avez désactivé + le JavaScript, veuillez cliquer sur le lien « soumettre » pour continuer + ou pour vous déconnecter. + +' diff --git a/config/locales/shared/fr.yml b/config/locales/shared/fr.yml new file mode 100644 index 00000000000..d54177fd046 --- /dev/null +++ b/config/locales/shared/fr.yml @@ -0,0 +1,7 @@ +--- +fr: + shared: + footer_lite: + gsa: Administration des services généraux des États-Unis + usa_banner: + official_site: Un site web officiel du gouvernement des États-Unis diff --git a/config/locales/sign_up/fr.yml b/config/locales/sign_up/fr.yml new file mode 100644 index 00000000000..44e45170925 --- /dev/null +++ b/config/locales/sign_up/fr.yml @@ -0,0 +1,16 @@ +--- +fr: + sign_up: + buttons: + cancel: Annuler et supprimer votre information + continue: Continuer la création du compte + cancel: + modal_header: Souhaitez-vous vraiment annuler? + success: Le dossier contenant votre information a été effacé + warning_header: Si vous annulez maintenant + warning_points: + - Vous n'aurez pas de compte login.gov + - Nous ne conserverons pas de dossier contenant votre adresse courriel, votre mot de passe et votre numéro de téléphone + - Vous ne serez pas en mesure d'accéder à votre information de façon sécuritaire en utilisant login.gov + registrations: + create_account: Créer un compte diff --git a/config/locales/simple_form/fr.yml b/config/locales/simple_form/fr.yml new file mode 100644 index 00000000000..9ba1c383810 --- /dev/null +++ b/config/locales/simple_form/fr.yml @@ -0,0 +1,11 @@ +--- +fr: + simple_form: + error_notification: + default_message: 'Veuillez examiner les problèmes ci-dessous :' + 'no': Non + required: + html: ' ' + mark: "*" + text: Ce champ est requis + 'yes': Oui diff --git a/config/locales/time/fr.yml b/config/locales/time/fr.yml new file mode 100644 index 00000000000..32b0b06bebf --- /dev/null +++ b/config/locales/time/fr.yml @@ -0,0 +1,5 @@ +--- +fr: + time: + formats: + event_timestamp: "%B %e, %Y à %-l:%M %p" diff --git a/config/locales/titles/fr.yml b/config/locales/titles/fr.yml new file mode 100644 index 00000000000..1b16899bd5c --- /dev/null +++ b/config/locales/titles/fr.yml @@ -0,0 +1,33 @@ +--- +fr: + titles: + account: Compte + account_locked: Compte verrouillé + confirmations: + new: Envoyer les instructions de confirmation pour votre compte + show: Choisissez un mot de passe + edit_info: + email: Modifier votre adresse courriel + password: Modifier votre mot de passe + phone: Modifier votre numéro de téléphone + enter_2fa_code: Entrez le code de sécurité à utilisation unique + passwords: + change: Changez le mot de passe de votre compte + confirm: Confirmez le mot de passe de votre compte + forgot: Réinitialisez le mot de passe de votre compte + personal_key: Juste au cas + reactivate_account: Réactiver le profil + registrations: + new: S'inscrire et créer un compte + start: Démarrer + sign_up: + completion_html: Vous avez %{accent} avec %{app} + loa1: créé votre compte + loa3: verifié votre identité + totp_setup: + new: Configurer l'authentification à deux facteurs + two_factor_setup: Configuration de l'authentification à deux facteurs + verify_email: Consultez vos courriels + verify_profile: Activez votre compte + visitors: + index: Bienvenue diff --git a/config/locales/tooltips/fr.yml b/config/locales/tooltips/fr.yml new file mode 100644 index 00000000000..6f8d4f5b6d1 --- /dev/null +++ b/config/locales/tooltips/fr.yml @@ -0,0 +1,7 @@ +--- +fr: + tooltips: + authentication_app: Une application d'authentification est une application de sécurité mobile qui génère des codes de sécurité même si vous n'avez pas de connexion internet ou de service cellulaire. + ssn: 'Nous vous demandons votre numéro de sécurité sociale pour nous aider à prouver votre identité. Certaines des agences avec lesquelles nous collaborons pourraient devoir accéder à votre dossier. + +' diff --git a/config/locales/user_mailer/fr.yml b/config/locales/user_mailer/fr.yml new file mode 100644 index 00000000000..a343bcaa0b3 --- /dev/null +++ b/config/locales/user_mailer/fr.yml @@ -0,0 +1,30 @@ +--- +fr: + user_mailer: + contact_link_text: communiquez avec nous + email_changed: + help: 'Si vous préférez ne pas changher votre adresse courriel, veuillez visiter le %{help_link} de %{app} ou %{contact_link}. + +' + intro: L'adresse courriel de votre compte %{app} a été changée. + help_link_text: Centre d'aide + password_changed: + help: 'Si vous n''avez pas changé votre mot de passe, veuillez visiter le %{help_link} de %{app} ou %{contact_link}. + +' + intro: Le mot de passe de votre compte %{app} a été changé. + phone_changed: + help: 'Si vous ne souhaitiez pas changer votre numéro de téléphone, veuillez visiter le %{help_link} de %{app} ou %{contact_link}. + +' + intro: Le numéro de téléphone associé à votre compte %{app} a été changé. + subject: Nouveau numéro de téléphone + signup_with_your_email: + help: 'Si vous n''avez pas demandé un nouveau compte ou que vous soupçonnez qu''une erreur s''est produite, veuillez visiter le %{help_link} de %{app} ou %{contact_link}. + +' + intro: 'Cette adresse courriel est déjà associée à un compte %{app}, nous ne pouvons donc pas l''utiliser pour créer un nouveau compte. Pour vous connecter à votre compte existant, suivez le lien ci-dessous. Si vous ne tentez pas de vous connecter avec cette adresse courriel, vous pouvez ignorer ce message. + +' + link_text: Allez à %{app} + reset_password: Si vous ne vous souvenez plus de votre mot de passe, allez à %{app} pour le réinitialiser. diff --git a/config/locales/users/fr.yml b/config/locales/users/fr.yml new file mode 100644 index 00000000000..ba3cfef2c73 --- /dev/null +++ b/config/locales/users/fr.yml @@ -0,0 +1,22 @@ +--- +fr: + users: + personal_key: + close: Fermer + confirmation_error: Vous avez entré un clé personnelle erronée. + generated_on_html: Générée le %{date} + get_another: Obtenir une autre clé + header: Votre clé personnelle + help_text: | + Pour protéger votre compte, vous devez avoir un mot de passe et l'accès à votre téléphone ou application d'authentification au moment de la connexion. Si vous ne pouvez utiliser votre téléphone ou application, vous pouvez vous connecter avec votre clé personnelle. + + Pour votre confidentialité et votre sécurité, login.gov ne conserve pas votre mot de passe ni votre clé personnelle. Seul(e) vous les connaissez. Seul(e) vous pouvez accéder à votre information personnelle et la partager . + + Nous vous demandons de conserver votre clé personnelle à l'extérieur de votre ordinateur ou appareil mobile afin qu'elle soit en sûreté même si vos appareils sont volés ou si vos comptes en ligne sont piratés. + + Si vous n'avez pas votre clé personnelle et que vous oubliez votre mot de passe, la seule façon de garder votre compte en sécurité est de vérifier que vous en êtes le(la) propriétaire légal(e). + help_text_header: Pourquoi dois-je conserver ma nouvelle clé sur papier? + print: Imprimer cette page + totp_setup: + new: + qr_img_alt: Code QR pour l'application d'authentification diff --git a/config/locales/valid_email/fr.yml b/config/locales/valid_email/fr.yml new file mode 100644 index 00000000000..2036952ded5 --- /dev/null +++ b/config/locales/valid_email/fr.yml @@ -0,0 +1,7 @@ +--- +fr: + valid_email: + validations: + email: + invalid: Format d'adresse courriel ou domaine entré non valide. Corrigez l'adresse + et entrez-la de nouveau. diff --git a/config/locales/zxcvbn/fr.yml b/config/locales/zxcvbn/fr.yml new file mode 100644 index 00000000000..da4adf5885c --- /dev/null +++ b/config/locales/zxcvbn/fr.yml @@ -0,0 +1,51 @@ +--- +fr: + zxcvbn: + feedback: + a_word_by_itself_is_easy_to_guess: Un mot seul est facile à deviner + add_another_word_or_two_uncommon_words_are_better: Ajoutez un ou deux autres + mots. Les mots non communs sont plus efficaces + all_uppercase_is_almost_as_easy_to_guess_as_all_lowercase: Tout en majuscules + est presque aussi facile à deviner que tout en minuscules + avoid_dates_and_years_that_are_associated_with_you: Évitez les dates et années + qui vous sont associées + avoid_recent_years: Évitez les années récentes + avoid_repeated_words_and_characters: Évitez les mots et caractères répétés + avoid_sequences: Évitez les séquences + avoid_years_that_are_associated_with_you: Évitez les années qui vous sont associées + capitalization_doesnt_help_very_much: La capitalisation n'aide pas beaucoup + common_names_and_surnames_are_easy_to_guess: Les prénoms et noms de famille + communs sont faciles à deviner + dates_are_often_easy_to_guess: Les dates sont souvent faciles à deviner + names_and_surnames_by_themselves_are_easy_to_guess: Les prénoms et noms de famille + seuls sont faciles à deviner + there_is_no_need_for_symbols_digits_or_uppercase_letters: Les symboles, les + chiffres ou les lettres majuscules ne sont pas nécessaires + predictable_substitutions_like__instead_of_a_dont_help_very_much: Les remplacements + prévisibles comme es « @ » au lieu de « à » n'aident pas beaucoup + recent_years_are_easy_to_guess: Les années récentes sont faciles à deviner + repeats_like_aaa_are_easy_to_guess: Les répétitions comme « aaa » sont faciles + à deviner + repeats_like_abcabcabc_are_only_slightly_harder_to_guess_than_abc: |- + Les répétitions comme « abcabcabc » sont à peine + plus difficiles à deviner que « abc » + reversed_words_arent_much_harder_to_guess: Les mots inversés ne sont pas très + difficiles à deviner + sequences_like_abc_or_6543_are_easy_to_guess: Les séquences comme abc ou 6543 + sont faciles à deviner + short_keyboard_patterns_are_easy_to_guess: Les motifs de clavier courts sont + faciles à deviner + straight_rows_of_keys_are_easy_to_guess: Les rangées de lettres consécutives + sont faciles à deviner + this_is_a_top_10_common_password: Il s'agit d'un des 10 mots de passe les plus + communs + this_is_a_top_100_common_password: Il s'agit d'un des 100 mots de passe les + plus communs + this_is_a_very_common_password: Il s'agit d'un mot de passe très commun + this_is_similar_to_a_commonly_used_password: Ceci est similaire à un mot de + passe souvent utilisé + for_a_stronger_password_use_a_few_words_separated_by_spaces_but_avoid_common_phrases: Pour + créer un mot de passe plus fort, utilisez quelques mots séparés par des espaces, + mais évitez les phrases communes + use_a_longer_keyboard_pattern_with_more_turns: Utilisez un motif de clavier + plus long avec plus de tours From 846bbb45a1d78af39599188345bbe191c73ce8d8 Mon Sep 17 00:00:00 2001 From: Zach Margolis Date: Wed, 5 Jul 2017 11:08:33 -0400 Subject: [PATCH 17/22] Set up SMS OTP for translation **Why**: To support a more complete localization --- app/jobs/sms_otp_sender_job.rb | 2 +- config/locales/jobs/en.yml | 2 ++ config/locales/jobs/es.yml | 2 ++ config/locales/jobs/fr.yml | 12 ++++++------ spec/jobs/sms_otp_sender_job_spec.rb | 3 +-- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/jobs/sms_otp_sender_job.rb b/app/jobs/sms_otp_sender_job.rb index af203aafec0..1bfd2a8e2f8 100644 --- a/app/jobs/sms_otp_sender_job.rb +++ b/app/jobs/sms_otp_sender_job.rb @@ -15,7 +15,7 @@ def otp_valid?(otp_created_at) def send_otp(twilio_service, code, phone) twilio_service.send_sms( to: phone, - body: "#{code} is your #{APP_NAME} one-time security code." + body: I18n.t('jobs.sms_otp_sender_job.message', code: code, app: APP_NAME) ) end end diff --git a/config/locales/jobs/en.yml b/config/locales/jobs/en.yml index 5405f8f767d..b4ca13ad3bc 100644 --- a/config/locales/jobs/en.yml +++ b/config/locales/jobs/en.yml @@ -8,3 +8,5 @@ en: message_repeat: > Hello! Your login.gov one time security code is, %{code}, again, your security code is, %{code}. Press 1 to repeat your code. + sms_otp_sender_job: + message: "%{code} is your %{app} one-time security code." diff --git a/config/locales/jobs/es.yml b/config/locales/jobs/es.yml index 6904a69ef59..c3fe2ebce3d 100644 --- a/config/locales/jobs/es.yml +++ b/config/locales/jobs/es.yml @@ -4,3 +4,5 @@ es: voice_otp_sender_job: message_final: NOT TRANSLATED YET message_repeat: NOT TRANSLATED YET + sms_otp_sender_job: + message: NOT TRANSLATED YET diff --git a/config/locales/jobs/fr.yml b/config/locales/jobs/fr.yml index 7d59cf58db8..f9d11487577 100644 --- a/config/locales/jobs/fr.yml +++ b/config/locales/jobs/fr.yml @@ -2,12 +2,12 @@ fr: jobs: voice_otp_sender_job: - message_final: 'Bonjour! Votre code de sécurité à utilisation unique de login.gov + 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 + 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. - -' + sms_otp_sender_job: + message: NOT TRANSLATED YET diff --git a/spec/jobs/sms_otp_sender_job_spec.rb b/spec/jobs/sms_otp_sender_job_spec.rb index 1afc5369d23..414e5b4b7ed 100644 --- a/spec/jobs/sms_otp_sender_job_spec.rb +++ b/spec/jobs/sms_otp_sender_job_spec.rb @@ -20,8 +20,7 @@ expect(msg.from).to match(/(\+19999999999|\+12222222222)/) expect(msg.to).to eq('555-5555') - expect(msg.body).to include('one-time security code') - expect(msg.body).to include('1234') + expect(msg.body).to eq(I18n.t('jobs.sms_otp_sender_job.message', code: '1234', app: APP_NAME)) end it 'does not send if the OTP code is expired' do From f221bcae0f8877a465dd782037627961d8bb0986 Mon Sep 17 00:00:00 2001 From: Zach Margolis Date: Wed, 5 Jul 2017 14:11:23 -0400 Subject: [PATCH 18/22] Update translated JS to use locale dynamically from HTML **Why**: ERBs are compiled once, so they're stuck with whatever locale they are compiled in. This makes one .js with all translated keys for all locales that can be cached, and then looks up the locale from the attribute on every page. --- app/assets/javascripts/misc/i18n-strings.js.erb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/misc/i18n-strings.js.erb b/app/assets/javascripts/misc/i18n-strings.js.erb index 32deb44529d..99bab39214d 100644 --- a/app/assets/javascripts/misc/i18n-strings.js.erb +++ b/app/assets/javascripts/misc/i18n-strings.js.erb @@ -46,11 +46,15 @@ window.LoginGov = window.LoginGov || {}; ] %> window.LoginGov.I18n = { + currentLocale: function() { return this.__currentLocale || (this.__currentLocale = document.querySelector('html').lang); }, strings: {}, - t: function(key) { return this.strings[key]; }, + t: function(key) { return this.strings[this.currentLocale()][key]; }, key: function(key) { return key.replace(/[ -]/g, '_').replace(/\W/g, '').toLowerCase(); } }; -<% keys.each do |key| %> -window.LoginGov.I18n.strings['<%= ActionController::Base.helpers.j key %>'] = '<%= ActionController::Base.helpers.j I18n.t(key) %>'; +<% I18n.available_locales.each do |locale| %> + window.LoginGov.I18n.strings['<%= ActionController::Base.helpers.j locale.to_s %>'] = {}; + <% keys.each do |key| %> + window.LoginGov.I18n.strings['<%= ActionController::Base.helpers.j locale.to_s %>']['<%= ActionController::Base.helpers.j key %>'] = '<%= ActionController::Base.helpers.j I18n.t(key, locale: locale) %>'; + <% end %> <% end %> From ae683556ba2cb95fe462d2e970115986758a06ea Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Wed, 5 Jul 2017 12:52:13 -0400 Subject: [PATCH 19/22] Return to branded page when canceling sign in **Why**: For consistency with the behavior during account creation cancellation. **How**: - Add a new controller and route for signing the user out without having to go through `SamlIdpController#logout` since there is no single logout to be performed in this scenario - Replace the URL of the Cancel link to point to this new route --- app/controllers/application_controller.rb | 12 ++++++--- app/controllers/sign_out_controller.rb | 13 +++++++++ .../service_provider_session_decorator.rb | 7 ++--- .../authenticator_delivery_presenter.rb | 2 +- .../phone_delivery_presenter.rb | 2 +- app/services/decorated_session.rb | 10 ++++--- .../personal_key_verification/show.html.slim | 2 +- config/routes.rb | 2 ++ spec/controllers/sign_out_controller_spec.rb | 22 +++++++++++++++ ...service_provider_session_decorator_spec.rb | 27 +++++++++++++++---- .../openid_connect/openid_connect_spec.rb | 5 +++- spec/features/saml/loa1_sso_spec.rb | 17 ++++++++++++ spec/support/shared_examples_for_otp_forms.rb | 2 +- .../devise/passwords/new.html.slim_spec.rb | 5 +++- .../devise/sessions/new.html.slim_spec.rb | 5 +++- .../layouts/application.html.slim_spec.rb | 14 ++++++++-- .../shared/_nav_branded.html.slim_spec.rb | 10 +++++-- .../registrations/new.html.slim_spec.rb | 2 +- .../registrations/show.html.slim_spec.rb | 2 +- spec/views/verify/fail.html.slim_spec.rb | 2 +- 20 files changed, 135 insertions(+), 28 deletions(-) create mode 100644 app/controllers/sign_out_controller.rb create mode 100644 spec/controllers/sign_out_controller_spec.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9b6e0ee328c..a19803fbbd4 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -46,7 +46,10 @@ def create_user_event(event_type, user = current_user) def decorated_session @_decorated_session ||= DecoratedSession.new( - sp: current_sp, view_context: view_context, sp_session: sp_session + sp: current_sp, + view_context: view_context, + sp_session: sp_session, + service_provider_request: service_provider_request ).call end @@ -79,11 +82,14 @@ def sp_from_sp_session end def sp_from_request_id - issuer = ServiceProviderRequest.from_uuid(params[:request_id]).issuer - sp = ServiceProvider.from_issuer(issuer) + sp = ServiceProvider.from_issuer(service_provider_request.issuer) sp if sp.is_a? ServiceProvider end + def service_provider_request + @service_provider_request ||= ServiceProviderRequest.from_uuid(params[:request_id]) + end + def after_sign_in_path_for(user) stored_location_for(user) || sp_session[:request_url] || signed_in_path end diff --git a/app/controllers/sign_out_controller.rb b/app/controllers/sign_out_controller.rb new file mode 100644 index 00000000000..84c24226cfe --- /dev/null +++ b/app/controllers/sign_out_controller.rb @@ -0,0 +1,13 @@ +class SignOutController < ApplicationController + include FullyAuthenticatable + + skip_before_action :handle_two_factor_authentication + + def destroy + path_after_cancellation = decorated_session.cancel_link_path + sign_out + flash[:success] = t('devise.sessions.signed_out') + redirect_to path_after_cancellation + delete_branded_experience + end +end diff --git a/app/decorators/service_provider_session_decorator.rb b/app/decorators/service_provider_session_decorator.rb index d2848e92495..d24b946301e 100644 --- a/app/decorators/service_provider_session_decorator.rb +++ b/app/decorators/service_provider_session_decorator.rb @@ -3,10 +3,11 @@ class ServiceProviderSessionDecorator DEFAULT_LOGO = 'generic.svg'.freeze - def initialize(sp:, view_context:, sp_session:) + def initialize(sp:, view_context:, sp_session:, service_provider_request:) @sp = sp @view_context = view_context @sp_session = sp_session + @service_provider_request = service_provider_request end def sp_logo @@ -71,10 +72,10 @@ def cancel_link_path private - attr_reader :sp, :view_context, :sp_session + attr_reader :sp, :view_context, :sp_session, :service_provider_request def request_url - sp_session[:request_url] + sp_session[:request_url] || service_provider_request.url end def openid_connect_redirector diff --git a/app/presenters/two_factor_auth_code/authenticator_delivery_presenter.rb b/app/presenters/two_factor_auth_code/authenticator_delivery_presenter.rb index 0f27b0959bf..fbf6a2736e3 100644 --- a/app/presenters/two_factor_auth_code/authenticator_delivery_presenter.rb +++ b/app/presenters/two_factor_auth_code/authenticator_delivery_presenter.rb @@ -22,7 +22,7 @@ def cancel_link if reauthn account_path else - destroy_user_session_path + sign_out_path end end diff --git a/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb b/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb index 85e805bdc34..411e52f9019 100644 --- a/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb +++ b/app/presenters/two_factor_auth_code/phone_delivery_presenter.rb @@ -22,7 +22,7 @@ def cancel_link if confirmation_for_phone_change || reauthn account_path else - destroy_user_session_path + sign_out_path end end diff --git a/app/services/decorated_session.rb b/app/services/decorated_session.rb index ebe7829e338..5a51d68fee7 100644 --- a/app/services/decorated_session.rb +++ b/app/services/decorated_session.rb @@ -1,14 +1,18 @@ class DecoratedSession - def initialize(sp:, view_context:, sp_session:) + def initialize(sp:, view_context:, sp_session:, service_provider_request:) @sp = sp @view_context = view_context @sp_session = sp_session + @service_provider_request = service_provider_request end def call if sp.is_a? ServiceProvider ServiceProviderSessionDecorator.new( - sp: sp, view_context: view_context, sp_session: sp_session + sp: sp, + view_context: view_context, + sp_session: sp_session, + service_provider_request: service_provider_request ) else SessionDecorator.new @@ -17,5 +21,5 @@ def call private - attr_reader :sp, :view_context, :sp_session + attr_reader :sp, :view_context, :sp_session, :service_provider_request end diff --git a/app/views/two_factor_authentication/personal_key_verification/show.html.slim b/app/views/two_factor_authentication/personal_key_verification/show.html.slim index c3f6d778ce5..00dacd73161 100644 --- a/app/views/two_factor_authentication/personal_key_verification/show.html.slim +++ b/app/views/two_factor_authentication/personal_key_verification/show.html.slim @@ -8,4 +8,4 @@ p.mt-tiny.mb0 = t('devise.two_factor_authentication.personal_key_prompt') = render 'partials/personal_key/entry_fields', f: f, attribute_name: :personal_key = f.button :submit, t('forms.buttons.submit.default'), class: 'btn btn-primary' -= render 'shared/cancel', link: destroy_user_session_path += render 'shared/cancel', link: sign_out_path diff --git a/config/routes.rb b/config/routes.rb index 64c727915d0..fdfa9f1835e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -117,6 +117,8 @@ get '/sign_up/completed' => 'sign_up/completions#show', as: :sign_up_completed post '/sign_up/completed' => 'sign_up/completions#update' + match '/sign_out' => 'sign_out#destroy', via: %i[get post delete] + delete '/users' => 'users#destroy', as: :destroy_user if FeatureManagement.enable_identity_verification? diff --git a/spec/controllers/sign_out_controller_spec.rb b/spec/controllers/sign_out_controller_spec.rb new file mode 100644 index 00000000000..a080e73cf6c --- /dev/null +++ b/spec/controllers/sign_out_controller_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +describe SignOutController do + describe '#destroy' do + it 'redirects to decorated_session.cancel_link_path with flash message' do + stub_sign_in_before_2fa + allow(controller.decorated_session).to receive(:cancel_link_path).and_return('foo') + + get :destroy + + expect(response).to redirect_to 'foo' + expect(flash[:success]).to eq t('devise.sessions.signed_out') + end + + it 'calls #sign_out and #delete_branded_experience' do + expect(controller).to receive(:sign_out).and_call_original + expect(controller).to receive(:delete_branded_experience) + + get :destroy + end + end +end diff --git a/spec/decorators/service_provider_session_decorator_spec.rb b/spec/decorators/service_provider_session_decorator_spec.rb index 3f1ed39ff0c..56bd0ab121a 100644 --- a/spec/decorators/service_provider_session_decorator_spec.rb +++ b/spec/decorators/service_provider_session_decorator_spec.rb @@ -3,7 +3,12 @@ RSpec.describe ServiceProviderSessionDecorator do let(:view_context) { ActionController::Base.new.view_context } subject do - ServiceProviderSessionDecorator.new(sp: sp, view_context: view_context, sp_session: {}) + ServiceProviderSessionDecorator.new( + sp: sp, + view_context: view_context, + sp_session: {}, + service_provider_request: ServiceProviderRequest.new + ) end let(:sp) { build_stubbed(:service_provider) } let(:sp_name) { subject.sp_name } @@ -59,7 +64,10 @@ it 'returns the agency name if friendly name is not present' do sp = build_stubbed(:service_provider, friendly_name: nil) subject = ServiceProviderSessionDecorator.new( - sp: sp, view_context: view_context, sp_session: {} + sp: sp, + view_context: view_context, + sp_session: {}, + service_provider_request: ServiceProviderRequest.new ) expect(subject.sp_name).to eq sp.agency expect(subject.sp_name).to_not be_nil @@ -73,7 +81,10 @@ sp = build_stubbed(:service_provider, logo: sp_logo) subject = ServiceProviderSessionDecorator.new( - sp: sp, view_context: view_context, sp_session: {} + sp: sp, + view_context: view_context, + sp_session: {}, + service_provider_request: ServiceProviderRequest.new ) expect(subject.sp_logo).to eq sp_logo @@ -85,7 +96,10 @@ sp = build_stubbed(:service_provider, logo: nil) subject = ServiceProviderSessionDecorator.new( - sp: sp, view_context: view_context, sp_session: {} + sp: sp, + view_context: view_context, + sp_session: {}, + service_provider_request: ServiceProviderRequest.new ) expect(subject.sp_logo).to eq 'generic.svg' @@ -96,7 +110,10 @@ describe '#cancel_link_path' do it 'returns sign_up_start_url with the request_id as a param' do subject = ServiceProviderSessionDecorator.new( - sp: sp, view_context: view_context, sp_session: { request_id: 'foo' } + sp: sp, + view_context: view_context, + sp_session: { request_id: 'foo' }, + service_provider_request: ServiceProviderRequest.new ) expect(subject.cancel_link_path). diff --git a/spec/features/openid_connect/openid_connect_spec.rb b/spec/features/openid_connect/openid_connect_spec.rb index 01de6a86d96..6dad66fde81 100644 --- a/spec/features/openid_connect/openid_connect_spec.rb +++ b/spec/features/openid_connect/openid_connect_spec.rb @@ -487,9 +487,12 @@ visit_idp_from_sp_with_loa1 click_link t('links.sign_in') fill_in_credentials_and_submit(user.email, user.password) + sp_request_id = ServiceProviderRequest.last.uuid + sp = ServiceProvider.from_issuer('urn:gov:gsa:openidconnect:sp:server') click_link t('links.cancel') - expect(current_url).to eq root_url + expect(current_url).to eq sign_up_start_url(request_id: sp_request_id) + expect(page).to have_content t('links.back_to_sp', sp: sp.friendly_name) end end diff --git a/spec/features/saml/loa1_sso_spec.rb b/spec/features/saml/loa1_sso_spec.rb index b74e07152fa..b8e1e9bae7a 100644 --- a/spec/features/saml/loa1_sso_spec.rb +++ b/spec/features/saml/loa1_sso_spec.rb @@ -152,6 +152,23 @@ end end + context 'canceling sign in after email and password' do + it 'returns to the branded landing page' do + user = create(:user, :signed_up) + authn_request = auth_request.create(saml_settings) + + visit authn_request + click_link t('links.sign_in') + fill_in_credentials_and_submit(user.email, user.password) + sp_request_id = ServiceProviderRequest.last.uuid + sp = ServiceProvider.from_issuer('http://localhost:3000') + click_link t('links.cancel') + + expect(current_url).to eq sign_up_start_url(request_id: sp_request_id) + expect(page).to have_content t('links.back_to_sp', sp: sp.friendly_name) + end + end + def sign_in_and_require_viewing_personal_key(user) login_as(user, scope: :user, run_callbacks: false) Warden.on_next_request do |proxy| diff --git a/spec/support/shared_examples_for_otp_forms.rb b/spec/support/shared_examples_for_otp_forms.rb index 7e8723de173..38a798dbe38 100644 --- a/spec/support/shared_examples_for_otp_forms.rb +++ b/spec/support/shared_examples_for_otp_forms.rb @@ -8,7 +8,7 @@ describe 'tertiary form actions' do it 'allows the user to cancel out of the sign in process' do render - expect(rendered).to have_link(t('links.cancel'), href: destroy_user_session_path) + expect(rendered).to have_link(t('links.cancel'), href: sign_out_path) end end end diff --git a/spec/views/devise/passwords/new.html.slim_spec.rb b/spec/views/devise/passwords/new.html.slim_spec.rb index ad5d316f3f5..d6669688339 100644 --- a/spec/views/devise/passwords/new.html.slim_spec.rb +++ b/spec/views/devise/passwords/new.html.slim_spec.rb @@ -10,7 +10,10 @@ ) view_context = ActionController::Base.new.view_context @decorated_session = DecoratedSession.new( - sp: sp, view_context: view_context, sp_session: {} + sp: sp, + view_context: view_context, + sp_session: {}, + service_provider_request: ServiceProviderRequest.new ).call allow(view).to receive(:decorated_session).and_return(@decorated_session) end diff --git a/spec/views/devise/sessions/new.html.slim_spec.rb b/spec/views/devise/sessions/new.html.slim_spec.rb index a5f5855b860..4fce885fbcd 100644 --- a/spec/views/devise/sessions/new.html.slim_spec.rb +++ b/spec/views/devise/sessions/new.html.slim_spec.rb @@ -54,7 +54,10 @@ ) view_context = ActionController::Base.new.view_context @decorated_session = DecoratedSession.new( - sp: sp, view_context: view_context, sp_session: {} + sp: sp, + view_context: view_context, + sp_session: {}, + service_provider_request: ServiceProviderRequest.new ).call allow(view).to receive(:decorated_session).and_return(@decorated_session) end diff --git a/spec/views/layouts/application.html.slim_spec.rb b/spec/views/layouts/application.html.slim_spec.rb index d52f33d2766..ed2129717fa 100644 --- a/spec/views/layouts/application.html.slim_spec.rb +++ b/spec/views/layouts/application.html.slim_spec.rb @@ -6,7 +6,12 @@ before do allow(view).to receive(:user_fully_authenticated?).and_return(true) allow(view).to receive(:decorated_session).and_return( - DecoratedSession.new(sp: nil, view_context: nil, sp_session: {}).call + DecoratedSession.new( + sp: nil, + view_context: nil, + sp_session: {}, + service_provider_request: ServiceProviderRequest.new + ).call ) allow(view.request).to receive(:original_url).and_return('http://test.host/foobar') allow(view).to receive(:current_user).and_return(User.new) @@ -76,7 +81,12 @@ allow(view).to receive(:current_user).and_return(nil) allow(view).to receive(:user_fully_authenticated?).and_return(false) allow(view).to receive(:decorated_session).and_return( - DecoratedSession.new(sp: nil, view_context: nil, sp_session: {}).call + DecoratedSession.new( + sp: nil, + view_context: nil, + sp_session: {}, + service_provider_request: nil + ).call ) allow(Figaro.env).to receive(:participate_in_dap).and_return('true') diff --git a/spec/views/shared/_nav_branded.html.slim_spec.rb b/spec/views/shared/_nav_branded.html.slim_spec.rb index 99a28197491..4a296cb6605 100644 --- a/spec/views/shared/_nav_branded.html.slim_spec.rb +++ b/spec/views/shared/_nav_branded.html.slim_spec.rb @@ -9,7 +9,10 @@ :service_provider, logo: 'generic.svg', friendly_name: 'Best SP ever' ) decorated_session = ServiceProviderSessionDecorator.new( - sp: sp_with_logo, view_context: view_context, sp_session: {} + sp: sp_with_logo, + view_context: view_context, + sp_session: {}, + service_provider_request: nil ) allow(view).to receive(:decorated_session).and_return(decorated_session) render @@ -24,7 +27,10 @@ before do sp_without_logo = build_stubbed(:service_provider, friendly_name: 'No logo no problem') decorated_session = ServiceProviderSessionDecorator.new( - sp: sp_without_logo, view_context: view_context, sp_session: {} + sp: sp_without_logo, + view_context: view_context, + sp_session: {}, + service_provider_request: nil ) allow(view).to receive(:decorated_session).and_return(decorated_session) render diff --git a/spec/views/sign_up/registrations/new.html.slim_spec.rb b/spec/views/sign_up/registrations/new.html.slim_spec.rb index 5d13ea5fbe1..54828b53f32 100644 --- a/spec/views/sign_up/registrations/new.html.slim_spec.rb +++ b/spec/views/sign_up/registrations/new.html.slim_spec.rb @@ -9,7 +9,7 @@ view_context = ActionController::Base.new.view_context @decorated_session = DecoratedSession.new( - sp: nil, view_context: view_context, sp_session: {} + sp: nil, view_context: view_context, sp_session: {}, service_provider_request: nil ).call allow(view).to receive(:decorated_session).and_return(@decorated_session) end diff --git a/spec/views/sign_up/registrations/show.html.slim_spec.rb b/spec/views/sign_up/registrations/show.html.slim_spec.rb index a64fdea7183..ec5c3bd8eaf 100644 --- a/spec/views/sign_up/registrations/show.html.slim_spec.rb +++ b/spec/views/sign_up/registrations/show.html.slim_spec.rb @@ -41,7 +41,7 @@ ) view_context = ActionController::Base.new.view_context @decorated_session = DecoratedSession.new( - sp: @sp, view_context: view_context, sp_session: {} + sp: @sp, view_context: view_context, sp_session: {}, service_provider_request: nil ).call allow(view).to receive(:decorated_session).and_return(@decorated_session) end diff --git a/spec/views/verify/fail.html.slim_spec.rb b/spec/views/verify/fail.html.slim_spec.rb index a2b80771bc3..404a7ea0d0d 100644 --- a/spec/views/verify/fail.html.slim_spec.rb +++ b/spec/views/verify/fail.html.slim_spec.rb @@ -7,7 +7,7 @@ before do sp = build_stubbed(:service_provider, friendly_name: 'Awesome Application!') @decorated_session = ServiceProviderSessionDecorator.new( - sp: sp, view_context: view_context, sp_session: {} + sp: sp, view_context: view_context, sp_session: {}, service_provider_request: nil ) allow(view).to receive(:decorated_session).and_return(@decorated_session) end From f2a99ab4f8d472ddf12e0a05cb693e582b82b9fd Mon Sep 17 00:00:00 2001 From: Zach Margolis Date: Wed, 5 Jul 2017 13:58:54 -0400 Subject: [PATCH 20/22] Add locale to URLs in mailers **Why**: For a consistently localized experience --- .reek | 1 + app/controllers/application_controller.rb | 4 +-- app/helpers/locale_helper.rb | 6 ++++ app/mailers/custom_devise_mailer.rb | 7 +++++ app/mailers/user_mailer.rb | 4 +-- .../confirmation_instructions.html.slim | 6 ++-- .../reset_password_instructions.html.slim | 6 ++-- spec/helpers/locale_helper_spec.rb | 31 +++++++++++++++++++ spec/mailers/user_mailer_spec.rb | 8 +++++ ...onfirmation_instructions.html.slim_spec.rb | 15 +++++++++ 10 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 app/helpers/locale_helper.rb create mode 100644 spec/helpers/locale_helper_spec.rb diff --git a/.reek b/.reek index 5dd7ff3513f..3b4a3620b61 100644 --- a/.reek +++ b/.reek @@ -101,6 +101,7 @@ UtilityFunction: - SessionDecorator - WorkerHealthChecker::Middleware#call - UserEncryptedAttributeOverrides#create_fingerprint + - LocaleHelper#locale_url_param 'app/controllers': InstanceVariableAssumption: enabled: false diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9b6e0ee328c..c0d202e465a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,6 +1,7 @@ class ApplicationController < ActionController::Base include UserSessionContext include VerifyProfileConcern + include LocaleHelper FLASH_KEYS = %w[alert error notice success warning].freeze @@ -51,8 +52,7 @@ def decorated_session end def default_url_options - active_locale = I18n.locale - { locale: active_locale == I18n.default_locale ? nil : active_locale } + { locale: locale_url_param } end private diff --git a/app/helpers/locale_helper.rb b/app/helpers/locale_helper.rb new file mode 100644 index 00000000000..ff18d64724e --- /dev/null +++ b/app/helpers/locale_helper.rb @@ -0,0 +1,6 @@ +module LocaleHelper + def locale_url_param + active_locale = I18n.locale + active_locale == I18n.default_locale ? nil : active_locale + end +end diff --git a/app/mailers/custom_devise_mailer.rb b/app/mailers/custom_devise_mailer.rb index efed6f6b003..ca9cfb44757 100644 --- a/app/mailers/custom_devise_mailer.rb +++ b/app/mailers/custom_devise_mailer.rb @@ -1,14 +1,21 @@ class CustomDeviseMailer < Devise::Mailer include Mailable + include LocaleHelper before_action :attach_images layout 'layouts/user_mailer' default from: email_with_name(Figaro.env.email_from, Figaro.env.email_from) + def reset_password_instructions(*) + @locale = locale_url_param + super + end + def confirmation_instructions(record, token, options = {}) presenter = ConfirmationEmailPresenter.new(record, view_context) @first_sentence = presenter.first_sentence @confirmation_period = presenter.confirmation_period @request_id = options[:request_id] + @locale = locale_url_param super end end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 6be2598e599..aff54835ba5 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -1,5 +1,6 @@ class UserMailer < ActionMailer::Base include Mailable + include LocaleHelper before_action :attach_images default from: email_with_name(Figaro.env.email_from, Figaro.env.email_from) @@ -8,8 +9,7 @@ def email_changed(old_email) end def signup_with_your_email(email) - @root_url = root_url - @new_user_password_url = new_user_password_url + @root_url = root_url(locale: locale_url_param) mail(to: email, subject: t('mailer.email_reuse_notice.subject')) end diff --git a/app/views/devise/mailer/confirmation_instructions.html.slim b/app/views/devise/mailer/confirmation_instructions.html.slim index f35fa928af9..e95edad6caf 100644 --- a/app/views/devise/mailer/confirmation_instructions.html.slim +++ b/app/views/devise/mailer/confirmation_instructions.html.slim @@ -12,15 +12,15 @@ table.button.expanded.large.radius center = link_to t('mailer.confirmation_instructions.link_text'), \ sign_up_create_email_confirmation_url(_request_id: \ - @request_id, confirmation_token: @token), \ + @request_id, confirmation_token: @token, locale: @locale), \ target: '_blank', \ class: 'float-center', align: 'center' td.expander p = link_to sign_up_create_email_confirmation_url(_request_id: @request_id, \ - confirmation_token: @token), \ + confirmation_token: @token, locale: @locale), \ sign_up_create_email_confirmation_url(_request_id: @request_id, \ - confirmation_token: @token), target: '_blank' + confirmation_token: @token, locale: @locale), target: '_blank' table.spacer tbody diff --git a/app/views/devise/mailer/reset_password_instructions.html.slim b/app/views/devise/mailer/reset_password_instructions.html.slim index a20f9ed0ede..71674e9e400 100644 --- a/app/views/devise/mailer/reset_password_instructions.html.slim +++ b/app/views/devise/mailer/reset_password_instructions.html.slim @@ -11,13 +11,13 @@ table.button.expanded.large.radius td center = link_to t('mailer.reset_password.link_text'), - edit_password_url(@resource, reset_password_token: @token), + edit_password_url(@resource, reset_password_token: @token, locale: @locale), target: '_blank', class: 'float-center', align: 'center' td.expander p - = link_to edit_password_url(@resource, reset_password_token: @token), \ - edit_password_url(@resource, reset_password_token: @token), \ + = link_to edit_password_url(@resource, reset_password_token: @token, locale: @locale), \ + edit_password_url(@resource, reset_password_token: @token, locale: @locale), \ target: '_blank' table.spacer diff --git a/spec/helpers/locale_helper_spec.rb b/spec/helpers/locale_helper_spec.rb new file mode 100644 index 00000000000..de3dc7818d1 --- /dev/null +++ b/spec/helpers/locale_helper_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' + +RSpec.describe LocaleHelper do + include LocaleHelper + + describe '#locale_url_param' do + context 'in the default locale' do + before { I18n.locale = :en } + + it 'is nil' do + expect(locale_url_param).to be_nil + end + end + + context 'in French (a non-default locale)' do + before { I18n.locale = :fr } + + it 'is that locale' do + expect(locale_url_param).to eq(:fr) + end + end + + context 'in Spanish (a non-default locale)' do + before { I18n.locale = :es } + + it 'is that locale' do + expect(locale_url_param).to eq(:es) + end + end + end +end diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index cfce7ebf52d..c3b16d2cd8a 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -67,6 +67,14 @@ ) expect_email_body_to_have_help_and_contact_links end + + context 'in a non-default locale' do + before { I18n.locale = :fr } + + it 'links to the correct locale' do + expect(mail.html_part.body).to include(root_url(locale: :fr)) + end + end end describe 'phone_changed' do diff --git a/spec/views/devise/mailer/confirmation_instructions.html.slim_spec.rb b/spec/views/devise/mailer/confirmation_instructions.html.slim_spec.rb index 5d27b671432..b5900800675 100644 --- a/spec/views/devise/mailer/confirmation_instructions.html.slim_spec.rb +++ b/spec/views/devise/mailer/confirmation_instructions.html.slim_spec.rb @@ -26,6 +26,21 @@ ) end + context 'in a non-default locale' do + before { assign(:locale, 'fr') } + + it 'puts the locale in the URL' do + assign(:resource, build_stubbed(:user, confirmed_at: Time.zone.now)) + assign(:token, 'foo') + render + + expect(rendered).to have_link( + 'http://test.host/fr/sign_up/email/confirm?confirmation_token=foo', + href: 'http://test.host/fr/sign_up/email/confirm?confirmation_token=foo' + ) + end + end + it 'mentions updating an account when user has already been confirmed' do user = build_stubbed(:user, confirmed_at: Time.zone.now) presenter = ConfirmationEmailPresenter.new(user, self) From f758ecb77d91792a2b9a6a57802fdad2163125f9 Mon Sep 17 00:00:00 2001 From: Zach Margolis Date: Mon, 3 Jul 2017 13:08:59 -0400 Subject: [PATCH 21/22] Extract a synchronous proofing job **Why**: One last small refactor step before making the job async --- .reek | 1 + .rubocop.yml | 3 + app/controllers/concerns/idv_session.rb | 4 ++ app/controllers/verify/finance_controller.rb | 11 +++- app/controllers/verify/phone_controller.rb | 11 +++- app/controllers/verify/sessions_controller.rb | 11 +++- app/jobs/vendor_validator_job.rb | 37 +++++++++++ app/services/idv/financials_step.rb | 4 -- app/services/idv/phone_step.rb | 4 -- app/services/idv/profile_step.rb | 4 -- app/services/idv/session.rb | 1 + app/services/idv/step.rb | 35 +--------- app/services/idv/vendor_result.rb | 3 +- app/services/submit_idv_job.rb | 32 +++++++++ .../vendor_validator_result_storage.rb | 24 +++++++ config/sidekiq.yml | 1 + spec/jobs/vendor_validator_job_spec.rb | 44 +++++++++++++ spec/services/idv/financials_step_spec.rb | 20 ++++-- spec/services/idv/phone_step_spec.rb | 22 +++++-- spec/services/idv/profile_step_spec.rb | 65 +++++++++++++------ spec/services/submit_idv_job_spec.rb | 53 +++++++++++++++ .../vendor_validator_result_storage_spec.rb | 51 +++++++++++++++ 22 files changed, 363 insertions(+), 78 deletions(-) create mode 100644 app/jobs/vendor_validator_job.rb create mode 100644 app/services/submit_idv_job.rb create mode 100644 app/services/vendor_validator_result_storage.rb create mode 100644 spec/jobs/vendor_validator_job_spec.rb create mode 100644 spec/services/submit_idv_job_spec.rb create mode 100644 spec/services/vendor_validator_result_storage_spec.rb diff --git a/.reek b/.reek index 3b4a3620b61..4eee4bbb042 100644 --- a/.reek +++ b/.reek @@ -42,6 +42,7 @@ NilCheck: LongParameterList: exclude: - IdentityLinker#optional_attributes + - VendorValidatorJob#perform RepeatedConditional: exclude: - Users::ResetPasswordsController diff --git a/.rubocop.yml b/.rubocop.yml index b2afe411f82..a8a5847908d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -93,6 +93,9 @@ Metrics/ModuleLength: - spec/**/* - 'app/controllers/concerns/two_factor_authenticatable.rb' +Metrics/ParameterLists: + CountKeywordArgs: false + # This is a Rails 5 feature, so it should be disabled until we upgrade Rails/HttpPositionalArguments: Description: 'Use keyword arguments instead of positional arguments in http method calls.' diff --git a/app/controllers/concerns/idv_session.rb b/app/controllers/concerns/idv_session.rb index 819eff862ec..eee58296772 100644 --- a/app/controllers/concerns/idv_session.rb +++ b/app/controllers/concerns/idv_session.rb @@ -40,4 +40,8 @@ def idv_vendor def idv_attempter @_idv_attempter ||= Idv::Attempter.new(current_user) end + + def vendor_validator_result + VendorValidatorResultStorage.new.load(idv_session.async_result_id) + end end diff --git a/app/controllers/verify/finance_controller.rb b/app/controllers/verify/finance_controller.rb index 7150a79cf1d..258a96be46a 100644 --- a/app/controllers/verify/finance_controller.rb +++ b/app/controllers/verify/finance_controller.rb @@ -6,6 +6,7 @@ class FinanceController < ApplicationController before_action :confirm_step_needed before_action :confirm_step_allowed before_action :submit_idv_form, only: [:create] + before_action :submit_idv_job, only: [:create] def new @view_model = view_model @@ -37,6 +38,14 @@ def submit_idv_form render_form end + def submit_idv_job + SubmitIdvJob.new( + vendor_validator_class: Idv::FinancialsValidator, + idv_session: idv_session, + vendor_params: vendor_params + ).call + end + def step_name :financials end @@ -66,7 +75,7 @@ def step @_step ||= Idv::FinancialsStep.new( idv_form_params: idv_form.idv_params, idv_session: idv_session, - vendor_params: vendor_params + vendor_validator_result: vendor_validator_result ) end diff --git a/app/controllers/verify/phone_controller.rb b/app/controllers/verify/phone_controller.rb index 2a0a0999cbd..1f498df83e5 100644 --- a/app/controllers/verify/phone_controller.rb +++ b/app/controllers/verify/phone_controller.rb @@ -6,6 +6,7 @@ class PhoneController < ApplicationController before_action :confirm_step_needed before_action :confirm_step_allowed before_action :submit_idv_form, only: [:create] + before_action :submit_idv_job, only: [:create] def new @view_model = view_model @@ -37,6 +38,14 @@ def submit_idv_form render :new end + def submit_idv_job + SubmitIdvJob.new( + vendor_validator_class: Idv::PhoneValidator, + idv_session: idv_session, + vendor_params: idv_form.phone + ).call + end + def step_name :phone end @@ -45,7 +54,7 @@ def step @_step ||= Idv::PhoneStep.new( idv_session: idv_session, idv_form_params: idv_form.idv_params, - vendor_params: idv_form.phone + vendor_validator_result: vendor_validator_result ) end diff --git a/app/controllers/verify/sessions_controller.rb b/app/controllers/verify/sessions_controller.rb index 3be7548c016..191507de2dc 100644 --- a/app/controllers/verify/sessions_controller.rb +++ b/app/controllers/verify/sessions_controller.rb @@ -9,6 +9,7 @@ class SessionsController < ApplicationController before_action :confirm_step_needed, except: [:destroy] before_action :initialize_idv_session, only: [:create] before_action :submit_idv_form, only: [:create] + before_action :submit_idv_job, only: [:create] delegate :attempts_exceeded?, to: :step, prefix: true @@ -44,6 +45,14 @@ def submit_idv_form process_failure unless result.success? end + def submit_idv_job + SubmitIdvJob.new( + vendor_validator_class: Idv::ProfileValidator, + idv_session: idv_session, + vendor_params: idv_session.vendor_params + ).call + end + def step_name :sessions end @@ -56,7 +65,7 @@ def step @_step ||= Idv::ProfileStep.new( idv_form_params: profile_params, idv_session: idv_session, - vendor_params: idv_session.vendor_params + vendor_validator_result: vendor_validator_result ) end diff --git a/app/jobs/vendor_validator_job.rb b/app/jobs/vendor_validator_job.rb new file mode 100644 index 00000000000..b78784082f8 --- /dev/null +++ b/app/jobs/vendor_validator_job.rb @@ -0,0 +1,37 @@ +class VendorValidatorJob < ActiveJob::Base + queue_as :idv + + def perform(result_id:, vendor_validator_class:, vendor:, vendor_params:, applicant_json:, + vendor_session_id:) + vendor_validator = vendor_validator_class.constantize.new( + applicant: Proofer::Applicant.new(JSON.parse(applicant_json, symbolize_names: true)), + vendor: vendor, + vendor_params: indifferent_access(vendor_params), + vendor_session_id: vendor_session_id + ) + + VendorValidatorResultStorage.new.store( + result_id: result_id, + result: extract_result(vendor_validator.result) + ) + end + + private + + def extract_result(result) + vendor_resp = result.vendor_resp + + Idv::VendorResult.new( + success: result.success?, + errors: result.errors, + reasons: vendor_resp.reasons, + normalized_applicant: vendor_resp.try(:normalized_applicant), + session_id: result.try(:session_id) + ) + end + + def indifferent_access(params) + return params if params.is_a?(String) + params.with_indifferent_access + end +end diff --git a/app/services/idv/financials_step.rb b/app/services/idv/financials_step.rb index e3deec1b95b..0979372b0b4 100644 --- a/app/services/idv/financials_step.rb +++ b/app/services/idv/financials_step.rb @@ -20,9 +20,5 @@ def submit def complete? vendor_validation_passed? end - - def vendor_validator_class - Idv::FinancialsValidator - end end end diff --git a/app/services/idv/phone_step.rb b/app/services/idv/phone_step.rb index 4345e56b8ed..49be5cfdd04 100644 --- a/app/services/idv/phone_step.rb +++ b/app/services/idv/phone_step.rb @@ -16,10 +16,6 @@ def complete? vendor_validation_passed? end - def vendor_validator_class - Idv::PhoneValidator - end - def update_idv_session idv_session.phone_confirmation = true idv_session.address_verification_mechanism = :phone diff --git a/app/services/idv/profile_step.rb b/app/services/idv/profile_step.rb index cccbc0be87c..371e6d2a71a 100644 --- a/app/services/idv/profile_step.rb +++ b/app/services/idv/profile_step.rb @@ -29,10 +29,6 @@ def increment_attempts_count attempter.increment end - def vendor_validator_class - Idv::ProfileValidator - end - def update_idv_session idv_session.profile_confirmation = true idv_session.vendor_session_id = vendor_validator_result.session_id diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb index a0baa6894a9..2467d7a7d03 100644 --- a/app/services/idv/session.rb +++ b/app/services/idv/session.rb @@ -1,6 +1,7 @@ module Idv class Session VALID_SESSION_ATTRIBUTES = %i[ + async_result_id address_verification_mechanism applicant financials_confirmation diff --git a/app/services/idv/step.rb b/app/services/idv/step.rb index ad5c3d11b42..355fde1cf3c 100644 --- a/app/services/idv/step.rb +++ b/app/services/idv/step.rb @@ -1,10 +1,10 @@ # abstract base class for Idv Steps module Idv class Step - def initialize(idv_session:, idv_form_params:, vendor_params:) + def initialize(idv_session:, idv_form_params:, vendor_validator_result:) @idv_form_params = idv_form_params @idv_session = idv_session - @vendor_params = vendor_params + @vendor_validator_result = vendor_validator_result end def vendor_validation_passed? @@ -14,23 +14,7 @@ def vendor_validation_passed? private attr_accessor :idv_session - attr_reader :idv_form_params, :vendor_params - - def vendor_validator_result - @_vendor_validator_result ||= extract_vendor_result(vendor_validator.result) - end - - def extract_vendor_result(result) - vendor_resp = result.vendor_resp - - Idv::VendorResult.new( - success: result.success?, - errors: result.errors, - reasons: vendor_resp.reasons, - normalized_applicant: vendor_resp.try(:normalized_applicant), - session_id: result.try(:session_id) - ) - end + attr_reader :idv_form_params, :vendor_validator_result def errors @_errors ||= begin @@ -39,18 +23,5 @@ def errors end end end - - def idv_vendor - @_idv_vendor ||= Idv::Vendor.new - end - - def vendor_validator - @_vendor_validator ||= vendor_validator_class.new( - applicant: idv_session.applicant, - vendor: (idv_session.vendor || idv_vendor.pick), - vendor_params: vendor_params, - vendor_session_id: idv_session.vendor_session_id - ) - end end end diff --git a/app/services/idv/vendor_result.rb b/app/services/idv/vendor_result.rb index 87db473181d..7f01ee62b90 100644 --- a/app/services/idv/vendor_result.rb +++ b/app/services/idv/vendor_result.rb @@ -11,7 +11,8 @@ def self.new_from_json(json) new(**parsed) end - def initialize(success:, errors:, reasons:, session_id:, normalized_applicant:) + def initialize(success: nil, errors: nil, reasons: nil, session_id: nil, + normalized_applicant: nil) @success = success @errors = errors @reasons = reasons diff --git a/app/services/submit_idv_job.rb b/app/services/submit_idv_job.rb new file mode 100644 index 00000000000..a74dfba5df0 --- /dev/null +++ b/app/services/submit_idv_job.rb @@ -0,0 +1,32 @@ +class SubmitIdvJob + def initialize(vendor_validator_class:, idv_session:, vendor_params:) + @vendor_validator_class = vendor_validator_class + @idv_session = idv_session + @vendor_params = vendor_params + end + + def call + idv_session.async_result_id = result_id + + VendorValidatorJob.perform_now( + result_id: result_id, + vendor_validator_class: vendor_validator_class.to_s, + vendor: vendor, + vendor_params: vendor_params, + vendor_session_id: idv_session.vendor_session_id, + applicant_json: idv_session.applicant.to_json + ) + end + + private + + attr_reader :vendor_validator_class, :idv_session, :vendor_params + + def result_id + @_result_id ||= SecureRandom.uuid + end + + def vendor + idv_session.vendor || Idv::Vendor.new.pick + end +end diff --git a/app/services/vendor_validator_result_storage.rb b/app/services/vendor_validator_result_storage.rb new file mode 100644 index 00000000000..098ba2422ff --- /dev/null +++ b/app/services/vendor_validator_result_storage.rb @@ -0,0 +1,24 @@ +class VendorValidatorResultStorage + TTL = Figaro.env.session_timeout_in_minutes.to_i.minutes.seconds.to_i + + def store(result_id:, result:) + Sidekiq.redis do |redis| + redis.setex(redis_key(result_id), TTL, result.to_json) + end + end + + def load(result_id) + result_json = Sidekiq.redis do |redis| + redis.get(redis_key(result_id)) + end + + return unless result_json + + Idv::VendorResult.new_from_json(result_json) + end + + # @api private + def redis_key(result_id) + "vendor-validator-result-#{result_id}" + end +end diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 8a0466b9b10..f72f4153bcd 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -3,4 +3,5 @@ - voice - mailers - analytics + - idv :logfile: 'log/sidekiq.log' diff --git a/spec/jobs/vendor_validator_job_spec.rb b/spec/jobs/vendor_validator_job_spec.rb new file mode 100644 index 00000000000..d10bd5f4533 --- /dev/null +++ b/spec/jobs/vendor_validator_job_spec.rb @@ -0,0 +1,44 @@ +require 'rails_helper' + +RSpec.describe VendorValidatorJob do + let(:result_id) { SecureRandom.uuid } + let(:vendor_validator_class) { 'Idv::PhoneValidator' } + let(:vendor) { :mock } + let(:vendor_params) { '+1 (888) 123-4567' } + let(:applicant) { Proofer::Applicant.new(first_name: 'Test') } + let(:applicant_json) { applicant.to_json } + let(:vendor_session_id) { SecureRandom.uuid } + + subject(:job) { VendorValidatorJob.new } + + describe '#perform' do + subject(:perform) do + job.perform( + result_id: result_id, + vendor_validator_class: vendor_validator_class, + vendor: vendor, + vendor_params: vendor_params, + applicant_json: applicant_json, + vendor_session_id: vendor_session_id + ) + end + + it 'calls out to a vendor and serializes the result' do + expect(Idv::PhoneValidator).to receive(:new). + with( + applicant: kind_of(Proofer::Applicant), + vendor: vendor, + vendor_params: vendor_params, + vendor_session_id: vendor_session_id + ).and_call_original + + before_result = VendorValidatorResultStorage.new.load(result_id) + expect(before_result).to be_nil + + perform + + after_result = VendorValidatorResultStorage.new.load(result_id) + expect(after_result).to be_a(Idv::VendorResult) + end + end +end diff --git a/spec/services/idv/financials_step_spec.rb b/spec/services/idv/financials_step_spec.rb index b20fbd952fc..1445ab394f7 100644 --- a/spec/services/idv/financials_step_spec.rb +++ b/spec/services/idv/financials_step_spec.rb @@ -9,17 +9,22 @@ end let(:idv_form_params) { idv_session.params } - def build_step(vendor_params) + def build_step(vendor_validator_result) described_class.new( idv_form_params: idv_form_params, idv_session: idv_session, - vendor_params: vendor_params + vendor_validator_result: vendor_validator_result ) end describe '#submit' do it 'returns FormResponse with success: true for mock-happy CCN' do - step = build_step(ccn: '12345678') + step = build_step( + Idv::VendorResult.new( + success: true, + errors: {} + ) + ) result = step.submit expect(result).to be_kind_of(FormResponse) @@ -31,10 +36,15 @@ def build_step(vendor_params) end it 'returns FormResponse with success: false for mock-sad CCN' do - step = build_step(ccn: '00000000') - errors = { ccn: ['The ccn could not be verified.'] } + step = build_step( + Idv::VendorResult.new( + success: false, + errors: errors + ) + ) + result = step.submit expect(result).to be_kind_of(FormResponse) expect(result.success?).to eq(false) diff --git a/spec/services/idv/phone_step_spec.rb b/spec/services/idv/phone_step_spec.rb index 7dacb2ace6c..4de8223d203 100644 --- a/spec/services/idv/phone_step_spec.rb +++ b/spec/services/idv/phone_step_spec.rb @@ -12,17 +12,23 @@ end let(:idv_phone_form) { Idv::PhoneForm.new(idv_session.params, user) } - def build_step(phone) + def build_step(phone, vendor_validator_result) described_class.new( idv_session: idv_session, idv_form_params: { phone: phone }, - vendor_params: phone + vendor_validator_result: vendor_validator_result ) end describe '#submit' do it 'returns true for mock-happy phone' do - step = build_step('555-555-0000') + step = build_step( + '555-555-0000', + Idv::VendorResult.new( + success: true, + errors: {} + ) + ) result = step.submit @@ -34,10 +40,16 @@ def build_step(phone) end it 'returns false for mock-sad phone' do - step = build_step('555-555-5555') - errors = { phone: ['The phone number could not be verified.'] } + step = build_step( + '555-555-5555', + Idv::VendorResult.new( + success: false, + errors: errors + ) + ) + result = step.submit expect(result).to be_kind_of(FormResponse) diff --git a/spec/services/idv/profile_step_spec.rb b/spec/services/idv/profile_step_spec.rb index 3906d020b1d..cc7655d0599 100644 --- a/spec/services/idv/profile_step_spec.rb +++ b/spec/services/idv/profile_step_spec.rb @@ -18,26 +18,35 @@ } end - def build_step(params) + def build_step(params, vendor_validator_result) idv_session.params.merge!(params) idv_session.applicant = idv_session.vendor_params described_class.new( idv_form_params: params, - vendor_params: idv_session.vendor_params, + vendor_validator_result: vendor_validator_result, idv_session: idv_session ) end describe '#submit' do it 'succeeds with good params' do - step = build_step(user_attrs) - + reasons = ['Everything looks good'] extra = { idv_attempts_exceeded: false, - vendor: { reasons: ['Everything looks good'] }, + vendor: { reasons: reasons }, } + step = build_step( + user_attrs, + Idv::VendorResult.new( + success: true, + errors: {}, + reasons: reasons, + normalized_applicant: Proofer::Applicant.new(first_name: 'Some') + ) + ) + result = step.submit expect(result).to be_kind_of(FormResponse) @@ -48,14 +57,18 @@ def build_step(params) end it 'fails with invalid SSN' do - step = build_step(user_attrs.merge(ssn: '666-66-6666')) - + reasons = ['The SSN was suspicious'] errors = { ssn: ['Unverified SSN.'] } extra = { idv_attempts_exceeded: false, - vendor: { reasons: ['The SSN was suspicious'] }, + vendor: { reasons: reasons }, } + step = build_step( + user_attrs.merge(ssn: '666-66-6666'), + Idv::VendorResult.new(success: false, errors: errors, reasons: reasons) + ) + result = step.submit expect(result).to be_kind_of(FormResponse) @@ -66,14 +79,18 @@ def build_step(params) end it 'fails with invalid first name' do - step = build_step(user_attrs.merge(first_name: 'Bad')) - errors = { first_name: ['Unverified first name.'] } + reasons = ['The name was suspicious'] extra = { idv_attempts_exceeded: false, - vendor: { reasons: ['The name was suspicious'] }, + vendor: { reasons: reasons }, } + step = build_step( + user_attrs.merge(first_name: 'Bad'), + Idv::VendorResult.new(success: false, errors: errors, reasons: reasons) + ) + result = step.submit expect(result).to be_kind_of(FormResponse) @@ -84,14 +101,18 @@ def build_step(params) end it 'fails with invalid ZIP code on current address' do - step = build_step(user_attrs.merge(zipcode: '00000')) - + reasons = ['The ZIP code was suspicious'] errors = { zipcode: ['Unverified ZIP code.'] } extra = { idv_attempts_exceeded: false, - vendor: { reasons: ['The ZIP code was suspicious'] }, + vendor: { reasons: reasons }, } + step = build_step( + user_attrs.merge(zipcode: '00000'), + Idv::VendorResult.new(success: false, errors: errors, reasons: reasons) + ) + result = step.submit expect(result).to be_kind_of(FormResponse) @@ -102,14 +123,18 @@ def build_step(params) end it 'fails with invalid ZIP code on previous address' do - step = build_step(user_attrs.merge(prev_zipcode: '00000')) - + reasons = ['The ZIP code was suspicious'] errors = { zipcode: ['Unverified ZIP code.'] } extra = { idv_attempts_exceeded: false, - vendor: { reasons: ['The ZIP code was suspicious'] }, + vendor: { reasons: reasons }, } + step = build_step( + user_attrs.merge(prev_zipcode: '00000'), + Idv::VendorResult.new(success: false, errors: errors, reasons: reasons) + ) + result = step.submit expect(result).to be_kind_of(FormResponse) @@ -120,12 +145,12 @@ def build_step(params) end it 'increments attempts count' do - step = build_step(user_attrs) + step = build_step(user_attrs, Idv::VendorResult.new(errors: {})) expect { step.submit }.to change(user, :idv_attempts).by(1) end it 'initializes the idv_session' do - step = build_step(user_attrs) + step = build_step(user_attrs, Idv::VendorResult.new(errors: {})) step.submit expect(idv_session.params).to eq user_attrs @@ -139,7 +164,7 @@ def build_step(params) allow(Idv::Attempter).to receive(:new).with(user).and_return(attempter) allow(attempter).to receive(:exceeded?) - step = build_step(user_attrs) + step = build_step(user_attrs, Idv::VendorResult.new(errors: {})) expect(step.attempts_exceeded?).to eq attempter.exceeded? end end diff --git a/spec/services/submit_idv_job_spec.rb b/spec/services/submit_idv_job_spec.rb new file mode 100644 index 00000000000..ba802f21269 --- /dev/null +++ b/spec/services/submit_idv_job_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +RSpec.describe SubmitIdvJob do + subject(:service) do + SubmitIdvJob.new( + vendor_validator_class: vendor_validator_class, + idv_session: idv_session, + vendor_params: vendor_params + ) + end + + let(:idv_session) do + Idv::Session.new( + current_user: user, + issuer: nil, + user_session: { + idv: { + applicant: applicant, + vendor_session_id: vendor_session_id, + vendor: :mock, + }, + } + ) + end + + let(:user) { build(:user) } + let(:applicant) { Proofer::Applicant.new(first_name: 'Greatest') } + let(:vendor_session_id) { '12345' } + let(:result_id) { 'abcdef' } + let(:vendor_params) { '+1 (888) 123-4567' } + let(:vendor_validator_class) { 'Idv::PhoneValidator' } + + describe '#call' do + subject(:call) { service.call } + + it 'generates a UUID and enqueues a job, and saves the UUID in the session' do + expect(SecureRandom).to receive(:uuid).and_return(result_id).once + + expect(VendorValidatorJob).to receive(:perform_now). + with( + result_id: result_id, + vendor_validator_class: vendor_validator_class, + vendor: :mock, + vendor_params: vendor_params, + vendor_session_id: vendor_session_id, + applicant_json: applicant.to_json + ) + + expect { call }. + to change { idv_session.async_result_id }.from(nil).to(result_id) + end + end +end diff --git a/spec/services/vendor_validator_result_storage_spec.rb b/spec/services/vendor_validator_result_storage_spec.rb new file mode 100644 index 00000000000..6cd36a9270f --- /dev/null +++ b/spec/services/vendor_validator_result_storage_spec.rb @@ -0,0 +1,51 @@ +require 'rails_helper' + +RSpec.describe VendorValidatorResultStorage do + subject(:service) { VendorValidatorResultStorage.new } + + let(:session_id) { SecureRandom.uuid } + let(:result_id) { SecureRandom.uuid } + let(:original_result) do + Idv::VendorResult.new( + success: false, + session_id: session_id, + normalized_applicant: Proofer::Applicant.new(first_name: 'First') + ) + end + + describe '#store_result' do + it 'stores the result in redis with a TTL' do + key = service.redis_key(result_id) + + before_redis = Sidekiq.redis { |redis| redis.get(key) } + expect(before_redis).to be_nil + + service.store(result_id: result_id, result: original_result) + + Sidekiq.redis do |redis| + expect(redis.get(key)).to be_present + expect(redis.ttl(key)).to be_within(1).of(VendorValidatorResultStorage::TTL) + end + end + end + + describe '#vendor_validator_result' do + before { service.store(result_id: result_id, result: original_result) } + + it 'retrieves a stored result' do + result = service.load(result_id) + + expect(result.success?).to eq(original_result.success?) + expect(result.errors).to eq(original_result.errors) + expect(result.reasons).to eq(original_result.reasons) + expect(result.normalized_applicant.as_json). + to eq(original_result.normalized_applicant.as_json) + end + + it 'is nil with a bad result id' do + result = service.load(SecureRandom.uuid) + + expect(result).to be_nil + end + end +end From 91b40feb6daf9225d046c099930f2bec5fa725ae Mon Sep 17 00:00:00 2001 From: Tom Black Date: Thu, 6 Jul 2017 14:26:46 -0400 Subject: [PATCH 22/22] Use CBP TTP logo for GOES SPs --- app/assets/images/sp-logos/cbp-ttp.png | Bin 0 -> 20215 bytes config/service_providers.yml | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 app/assets/images/sp-logos/cbp-ttp.png diff --git a/app/assets/images/sp-logos/cbp-ttp.png b/app/assets/images/sp-logos/cbp-ttp.png new file mode 100644 index 0000000000000000000000000000000000000000..2beaf6fc93af93992ab7fa330e51fde377cd9a3d GIT binary patch literal 20215 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

`tc9I0E$ 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 Y0M-2|*}Ao2CjbBd07*qoM6N<$f;5F7)c^nh literal 0 HcmV?d00001 diff --git a/config/service_providers.yml b/config/service_providers.yml index 6a19af7b1ba..199a8dc5e22 100644 --- a/config/service_providers.yml +++ b/config/service_providers.yml @@ -383,7 +383,7 @@ production: 'urn:gov:dhs.cbp.jobs:openidconnect:jenkins-pspd-credential-service': friendly_name: 'CBP PSPD Trusted Traveler Programs' agency: 'DHS' - logo: 'cbp.png' + logo: 'cbp-ttp.png' cert: 'cbp_goes_pre_prod' redirect_uris: - 'http://10.156.152.27/login' @@ -394,6 +394,6 @@ production: block_encryption: 'aes256-cbc' cert: 'cbp_goes_prod' friendly_name: 'CBP Trusted Traveler Programs' - logo: 'cbp.png' + logo: 'cbp-ttp.png' redirect_uris: - 'https://ttp.cbp.dhs.gov'