From 6c5d13c66002916568aec1619805fb6bf1d8d5af Mon Sep 17 00:00:00 2001 From: Alex Bradley Date: Fri, 16 Dec 2022 15:04:03 -0500 Subject: [PATCH 01/20] LG-8084 Hybrid Handoff SMS (#7499) * change the doc_auth sms to new text * add app_name to beginning of sms message * add user's service provider to the sms * add service_provider to send_doc_auth_link test * put doc auth link back to the front of the sms * add changelog changelog: Improvements, Hybrid Flow, Change SMS of the Hybrid Flow * fix test helper for mobile to get correct link * clean up obtaining service provider in send link step * change variable to sp_or_app_name for more clarity * fix test --- app/services/idv/steps/send_link_step.rb | 5 +++ config/locales/telephony/en.yml | 4 +-- config/locales/telephony/es.yml | 5 ++- config/locales/telephony/fr.yml | 5 ++- lib/telephony/alert_sender.rb | 9 ++++-- spec/lib/telephony/alert_sender_spec.rb | 32 ++++++++++++++++--- .../services/idv/steps/send_link_step_spec.rb | 1 + spec/support/features/doc_capture_helper.rb | 2 +- 8 files changed, 48 insertions(+), 15 deletions(-) diff --git a/app/services/idv/steps/send_link_step.rb b/app/services/idv/steps/send_link_step.rb index 2b64cf087b4..1f412e5c58a 100644 --- a/app/services/idv/steps/send_link_step.rb +++ b/app/services/idv/steps/send_link_step.rb @@ -65,9 +65,14 @@ def send_link to: formatted_destination_phone, link: link(session_uuid), country_code: Phonelib.parse(formatted_destination_phone).country, + sp_or_app_name: sp_or_app_name, ) end + def sp_or_app_name + current_sp&.friendly_name.presence || APP_NAME + end + def form_submit params = permit(:phone) params[:otp_delivery_preference] = 'sms' diff --git a/config/locales/telephony/en.yml b/config/locales/telephony/en.yml index 15981300e10..dbdd9ab9d67 100644 --- a/config/locales/telephony/en.yml +++ b/config/locales/telephony/en.yml @@ -21,8 +21,8 @@ en: voice: Hello! Your %{app_name} one-time code is, %{code}. Your one-time code is, %{code}. Again, your one-time code is %{code}. This code expires in %{expiration} minutes. - doc_auth_link: "%{link} You've requested to verify your identity on a mobile - phone. Please take a photo of your state issued ID." + doc_auth_link: |- + %{app_name}: %{link} You're verifying your identity to access %{sp_or_app_name}. Take a photo of your ID to continue. error: friendly_message: daily_voice_limit_reached: Your one-time code failed to send because you diff --git a/config/locales/telephony/es.yml b/config/locales/telephony/es.yml index 9e02ac42573..36fa8087bd5 100644 --- a/config/locales/telephony/es.yml +++ b/config/locales/telephony/es.yml @@ -21,9 +21,8 @@ es: voice: ¡Hola! Su código único de %{app_name} es %{code}, Su código único es %{code}, De nuevo, su código único es %{code}, Este código expira en %{expiration} minutos. - doc_auth_link: '%{link} Has solicitado verificar tu identidad en un teléfono - móvil. Por favor, tome una foto de la identificación emitida por su - estado' + doc_auth_link: |- + %{app_name}: %{link} Está verificando su identidad para acceder a %{sp_or_app_name}. Tome una foto de su identificación para continuar. error: friendly_message: daily_voice_limit_reached: Su código de un solo uso no se ha podido enviar, ya diff --git a/config/locales/telephony/fr.yml b/config/locales/telephony/fr.yml index 7e52d5f7938..76b55235db3 100644 --- a/config/locales/telephony/fr.yml +++ b/config/locales/telephony/fr.yml @@ -21,9 +21,8 @@ fr: voice: Bonjour! Votre code %{app_name} à usage unique est %{code}. Votre code à usage unique est %{code}. Une fois de plus, votre code à usage unique est %{code}. Ce code expire dans %{expiration} minutes. - doc_auth_link: "%{link} Vous avez demandé à vérifier votre identité sur un - téléphone mobile. S'il vous plaît prendre une photo de votre identité - émise par l'état" + doc_auth_link: |- + %{app_name}: %{link} Vous vérifiez votre identité pour accéder à %{sp_or_app_name}. Prenez une photo de votre pièce d'identité pour continuer. error: friendly_message: daily_voice_limit_reached: L’envoi de votre code à usage unique a échoué car diff --git a/lib/telephony/alert_sender.rb b/lib/telephony/alert_sender.rb index c2a02e6819b..7686fc248f3 100644 --- a/lib/telephony/alert_sender.rb +++ b/lib/telephony/alert_sender.rb @@ -16,8 +16,13 @@ def send_account_reset_cancellation_notice(to:, country_code:) response end - def send_doc_auth_link(to:, link:, country_code:) - message = I18n.t('telephony.doc_auth_link', link: link) + def send_doc_auth_link(to:, link:, country_code:, sp_or_app_name:) + message = I18n.t( + 'telephony.doc_auth_link', + app_name: APP_NAME, + sp_or_app_name: sp_or_app_name, + link: link, + ) response = adapter.send(message: message, to: to, country_code: country_code) context = __method__.to_s.gsub(/^send_/, '') if link.length > SMS_MAX_LENGTH diff --git a/spec/lib/telephony/alert_sender_spec.rb b/spec/lib/telephony/alert_sender_spec.rb index d35aa0b40b8..1484886511b 100644 --- a/spec/lib/telephony/alert_sender_spec.rb +++ b/spec/lib/telephony/alert_sender_spec.rb @@ -37,13 +37,27 @@ let(:link) do 'https://idp.int.identitysandbox.com/verify/capture-doc/mobile-front-image?token=aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' end + let(:app_name) { APP_NAME } + let(:sp_or_app_name) { 'Batman.gov' } it 'sends the correct message' do - subject.send_doc_auth_link(to: recipient, link: link, country_code: 'US') + subject.send_doc_auth_link( + to: recipient, + link: link, + country_code: 'US', + sp_or_app_name: sp_or_app_name, + ) last_message = Telephony::Test::Message.messages.last expect(last_message.to).to eq(recipient) - expect(last_message.body).to eq(I18n.t('telephony.doc_auth_link', link: link)) + expect(last_message.body).to eq( + I18n.t( + 'telephony.doc_auth_link', + app_name: app_name, + link: link, + sp_or_app_name: sp_or_app_name, + ), + ) end I18n.available_locales.each do |locale| @@ -57,7 +71,12 @@ end it 'puts the URL in the first 160 characters, so it stays within a single SMS message' do - subject.send_doc_auth_link(to: recipient, link: link, country_code: 'US') + subject.send_doc_auth_link( + to: recipient, + link: link, + country_code: 'US', + sp_or_app_name: sp_or_app_name, + ) last_message = Telephony::Test::Message.messages.last first160 = last_message.body[0...160] @@ -71,7 +90,12 @@ expect(Telephony.config.logger).to receive(:warn) - subject.send_doc_auth_link(to: recipient, link: long_link, country_code: 'US') + subject.send_doc_auth_link( + to: recipient, + link: long_link, + country_code: 'US', + sp_or_app_name: sp_or_app_name, + ) end end diff --git a/spec/services/idv/steps/send_link_step_spec.rb b/spec/services/idv/steps/send_link_step_spec.rb index 1e40106d605..1ac97d58339 100644 --- a/spec/services/idv/steps/send_link_step_spec.rb +++ b/spec/services/idv/steps/send_link_step_spec.rb @@ -33,6 +33,7 @@ session: { sp: { issuer: service_provider.issuer } }, params: params, current_user: user, + current_sp: service_provider, analytics: FakeAnalytics.new, url_options: {}, request: request, diff --git a/spec/support/features/doc_capture_helper.rb b/spec/support/features/doc_capture_helper.rb index 5f48b445d62..a4724f972e6 100644 --- a/spec/support/features/doc_capture_helper.rb +++ b/spec/support/features/doc_capture_helper.rb @@ -3,7 +3,7 @@ def doc_capture_request_uri(user = user_with_2fa) allow_any_instance_of(Browser).to receive(:mobile?).and_return(false) sign_in_and_2fa_user(user) complete_doc_auth_steps_before_link_sent_step - url = Telephony::Test::Message.messages.last.body.split(' ').first + url = Telephony::Test::Message.messages.last.body.split(' ')[1] allow_any_instance_of(Browser).to receive(:mobile?).and_call_original URI.parse(url).request_uri end From 209f0badd04cb5e77ad1ea401e6ceb8409f18cc0 Mon Sep 17 00:00:00 2001 From: Rwolfe-Nava <87499456+Rwolfe-Nava@users.noreply.github.com> Date: Fri, 16 Dec 2022 17:01:44 -0500 Subject: [PATCH 02/20] added elapsed_time to analytics irs_attempts_api_events (#7504) * added elapsed_time to analytics irs_attempts_api_events changelog: Internal, Attempts API, logging and monitoring * updated relevant spec files to reflect changes * implemented changes reflecting Matt's feedback. --- .../api/irs_attempts_api_controller.rb | 22 ++++++++++++++++--- app/services/analytics_events.rb | 3 +++ .../api/irs_attempts_api_controller_spec.rb | 4 ++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/irs_attempts_api_controller.rb b/app/controllers/api/irs_attempts_api_controller.rb index aa1f698bfcb..138d7768efc 100644 --- a/app/controllers/api/irs_attempts_api_controller.rb +++ b/app/controllers/api/irs_attempts_api_controller.rb @@ -18,6 +18,7 @@ class IrsAttemptsApiController < ApplicationController respond_to :json def create + start_time = Time.zone.now.to_f if timestamp if IdentityConfig.store.irs_attempt_api_aws_s3_enabled if IrsAttemptApiLogFile.find_by(requested_time: timestamp_key(key: timestamp)) @@ -53,7 +54,12 @@ def create render json: { status: :unprocessable_entity, description: 'Invalid timestamp parameter' }, status: :unprocessable_entity end - analytics.irs_attempts_api_events(**analytics_properties(authenticated: true)) + analytics.irs_attempts_api_events( + **analytics_properties( + authenticated: true, + elapsed_time: elapsed_time(start_time), + ), + ) end private @@ -62,7 +68,12 @@ def authenticate_client bearer, csp_id, token = request.authorization&.split(' ', 3) if bearer != 'Bearer' || !valid_auth_tokens.include?(token) || csp_id != IdentityConfig.store.irs_attempt_api_csp_id - analytics.irs_attempts_api_events(**analytics_properties(authenticated: false)) + analytics.irs_attempts_api_events( + **analytics_properties( + authenticated: false, + elapsed_time: 0, + ), + ) render json: { status: 401, description: 'Unauthorized' }, status: :unauthorized end end @@ -99,10 +110,11 @@ def valid_auth_tokens IdentityConfig.store.irs_attempt_api_auth_tokens end - def analytics_properties(authenticated:) + def analytics_properties(authenticated:, elapsed_time:) { rendered_event_count: security_event_tokens.count, timestamp: timestamp&.iso8601, + elapsed_time: elapsed_time, authenticated: authenticated, success: authenticated && timestamp.present?, } @@ -118,5 +130,9 @@ def timestamp rescue ArgumentError nil end + + def elapsed_time(start_time) + Time.zone.now.to_f - start_time + end end end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index b7e2a367342..cd4b517b227 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -1463,11 +1463,13 @@ def unsafe_redirect_error( # @param [Integer] rendered_event_count how many events were rendered in the API response # @param [Boolean] authenticated whether the request was successfully authenticated + # @param [Float] elapsed_time the amount of time the function took to run # @param [Boolean] success # An IRS Attempt API client has requested events def irs_attempts_api_events( rendered_event_count:, authenticated:, + elapsed_time:, success:, **extra ) @@ -1475,6 +1477,7 @@ def irs_attempts_api_events( 'IRS Attempt API: Events submitted', rendered_event_count: rendered_event_count, authenticated: authenticated, + elapsed_time: elapsed_time, success: success, **extra, ) diff --git a/spec/controllers/api/irs_attempts_api_controller_spec.rb b/spec/controllers/api/irs_attempts_api_controller_spec.rb index ab748d5a7f3..a3bcfc8db2c 100644 --- a/spec/controllers/api/irs_attempts_api_controller_spec.rb +++ b/spec/controllers/api/irs_attempts_api_controller_spec.rb @@ -170,6 +170,7 @@ 'IRS Attempt API: Events submitted', rendered_event_count: 3, authenticated: false, + elapsed_time: 0, success: false, timestamp: time.iso8601, ) @@ -190,6 +191,8 @@ end it 'renders encrypted events' do + allow_any_instance_of(described_class).to receive(:elapsed_time).and_return(0.1234) + post :create, params: { timestamp: time.iso8601 } expect(response).to be_ok @@ -201,6 +204,7 @@ 'IRS Attempt API: Events submitted', rendered_event_count: existing_events.count, authenticated: true, + elapsed_time: 0.1234, success: true, timestamp: time.iso8601, ) From b995562d3918a0296bb179542ba36cf696abba64 Mon Sep 17 00:00:00 2001 From: Matt Gardner Date: Sun, 18 Dec 2022 14:40:53 -0500 Subject: [PATCH 03/20] LG-8407: PO Search: search button shows loading state (#7502) --- .../components/address-search.spec.tsx | 67 +++ .../components/address-search.tsx | 56 +- package.json | 1 + yarn.lock | 481 +++++++++++++++++- 4 files changed, 580 insertions(+), 25 deletions(-) create mode 100644 app/javascript/packages/document-capture/components/address-search.spec.tsx diff --git a/app/javascript/packages/document-capture/components/address-search.spec.tsx b/app/javascript/packages/document-capture/components/address-search.spec.tsx new file mode 100644 index 00000000000..c9118ba6a61 --- /dev/null +++ b/app/javascript/packages/document-capture/components/address-search.spec.tsx @@ -0,0 +1,67 @@ +import { render } from '@testing-library/react'; +import { setupServer } from 'msw/node'; +import { rest } from 'msw'; +import type { SetupServerApi } from 'msw/node'; +import { fetch } from 'whatwg-fetch'; +import { useSandbox } from '@18f/identity-test-helpers'; +import userEvent from '@testing-library/user-event'; +import AddressSearch, { ADDRESS_SEARCH_URL } from './address-search'; + +const DEFAULT_RESPONSE = [ + { + address: '100 Main St E, Bronwood, Georgia, 39826', + location: { + latitude: 31.831686000000005, + longitude: -84.363768, + }, + street_address: '100 Main St E', + city: 'Bronwood', + state: 'GA', + zip_code: '39826', + }, +]; + +describe('AddressSearch', () => { + const sandbox = useSandbox(); + + let server: SetupServerApi; + before(() => { + global.window.fetch = fetch; + server = setupServer( + rest.post(ADDRESS_SEARCH_URL, (_req, res, ctx) => res(ctx.json(DEFAULT_RESPONSE))), + ); + server.listen(); + }); + + after(() => { + server.close(); + global.window.fetch = () => Promise.reject(new Error('Fetch must be stubbed')); + }); + + it('fires the callback with correct input', async () => { + const handleAddressFound = sandbox.stub(); + const { findByText, findByLabelText } = render( + , + ); + + await userEvent.type( + await findByLabelText('in_person_proofing.body.location.po_search.address_search_label'), + '200 main', + ); + await userEvent.click( + await findByText('in_person_proofing.body.location.po_search.search_button'), + ); + + await expect(handleAddressFound).to.eventually.be.calledWith(DEFAULT_RESPONSE[0]); + }); + + it('validates input and shows inline error', async () => { + const { findByText } = render(); + + await userEvent.click( + await findByText('in_person_proofing.body.location.po_search.search_button'), + ); + + await findByText('in_person_proofing.body.location.inline_error'); + }); +}); diff --git a/app/javascript/packages/document-capture/components/address-search.tsx b/app/javascript/packages/document-capture/components/address-search.tsx index 92c80e4904b..6d317a596de 100644 --- a/app/javascript/packages/document-capture/components/address-search.tsx +++ b/app/javascript/packages/document-capture/components/address-search.tsx @@ -1,8 +1,11 @@ -import { TextInput, Button } from '@18f/identity-components'; +import { TextInput } from '@18f/identity-components'; import { request } from '@18f/identity-request'; -import { useState, useCallback, ChangeEvent, useRef, Ref } from 'react'; +import { useState, useCallback, ChangeEvent, useRef, useEffect } from 'react'; import { useI18n } from '@18f/identity-react-i18n'; import ValidatedField from '@18f/identity-validated-field/validated-field'; +import SpinnerButton, { SpinnerButtonRefHandle } from '@18f/identity-spinner-button/spinner-button'; +import type { RegisterFieldCallback } from '@18f/identity-form-steps'; +import useSWR from 'swr'; interface Location { street_address: string; @@ -14,31 +17,49 @@ interface Location { interface AddressSearchProps { onAddressFound?: (location: Location) => void; - registerField: (field: string) => Ref; + registerField?: RegisterFieldCallback; } export const ADDRESS_SEARCH_URL = '/api/addresses'; -function AddressSearch({ onAddressFound = () => {}, registerField }: AddressSearchProps) { +function requestAddressCandidates(unvalidatedAddressInput: string): Promise { + return request(ADDRESS_SEARCH_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + json: { address: unvalidatedAddressInput }, + }); +} + +function AddressSearch({ + onAddressFound = () => {}, + registerField = () => undefined, +}: AddressSearchProps) { const validatedFieldRef = useRef(null); const [unvalidatedAddressInput, setUnvalidatedAddressInput] = useState(''); + const [addressQuery, setAddressQuery] = useState(''); const { t } = useI18n(); + const { data: addressCandidates } = useSWR([ADDRESS_SEARCH_URL, addressQuery], () => + addressQuery ? requestAddressCandidates(unvalidatedAddressInput) : null, + ); + const ref = useRef(null); + + useEffect(() => { + if (addressCandidates) { + const bestMatchedAddress = addressCandidates[0]; + onAddressFound(bestMatchedAddress); + ref.current?.toggleSpinner(false); + } + }, [addressCandidates]); const handleAddressSearch = useCallback( - async (event) => { + (event) => { event.preventDefault(); validatedFieldRef.current?.reportValidity(); if (unvalidatedAddressInput === '') { return; } - const addressCandidates = await request(ADDRESS_SEARCH_URL, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - json: { address: unvalidatedAddressInput }, - }); - const bestMatchedAddress = addressCandidates[0]; - onAddressFound(bestMatchedAddress); + setAddressQuery(unvalidatedAddressInput); }, [unvalidatedAddressInput], ); @@ -63,9 +84,16 @@ function AddressSearch({ onAddressFound = () => {}, registerField }: AddressSear hint={t('in_person_proofing.body.location.po_search.address_search_hint')} /> - + ); } diff --git a/package.json b/package.json index f41fe1f7977..d7f9b5ce313 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "jsdom": "^20.0.0", "mocha": "^10.0.0", "mq-polyfill": "^1.1.8", + "msw": "^0.49.2", "postcss": "^8.4.17", "prettier": "^2.7.1", "react-test-renderer": "^17.0.2", diff --git a/yarn.lock b/yarn.lock index 5f07fe15ba0..879612b5492 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1111,6 +1111,28 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== +"@mswjs/cookies@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@mswjs/cookies/-/cookies-0.2.2.tgz#b4e207bf6989e5d5427539c2443380a33ebb922b" + integrity sha512-mlN83YSrcFgk7Dm1Mys40DLssI1KdJji2CMKN8eOlBqsTADYzj2+jWzsANsUTFbxDMWPD5e9bfA1RGqBpS3O1g== + dependencies: + "@types/set-cookie-parser" "^2.4.0" + set-cookie-parser "^2.4.6" + +"@mswjs/interceptors@^0.17.5": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.17.6.tgz#7f7900f4cd26f70d9f698685e4485b2f4101d26a" + integrity sha512-201pBIWehTURb6q8Gheu4Zhvd3Ox1U4BJq5KiOQsYzkWyfiOG4pwcz5hPZIEryztgrf8/sdwABpvY757xMmfrQ== + dependencies: + "@open-draft/until" "^1.0.3" + "@types/debug" "^4.1.7" + "@xmldom/xmldom" "^0.8.3" + debug "^4.3.3" + headers-polyfill "^3.1.0" + outvariant "^1.2.1" + strict-event-emitter "^0.2.4" + web-encoding "^1.1.5" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1132,6 +1154,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@open-draft/until@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca" + integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q== + "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.3": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -1273,6 +1300,18 @@ dependencies: "@types/node" "*" +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== + +"@types/debug@^4.1.7": + version "4.1.7" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" + integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg== + dependencies: + "@types/ms" "*" + "@types/dirty-chai@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@types/dirty-chai/-/dirty-chai-2.0.2.tgz#eeac4802329a41ed7815ac0c1a6360335bf77d0c" @@ -1352,6 +1391,11 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/js-levenshtein@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/js-levenshtein/-/js-levenshtein-1.1.1.tgz#ba05426a43f9e4e30b631941e0aa17bf0c890ed5" + integrity sha512-qC4bCqYGy1y/NP7dDVr7KJarn+PbX1nSpwA7JXdu0HxT3QYjO8MJ+cntENtHFVy2dRAyBV23OZ6MxsW1AM1L8g== + "@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" @@ -1377,6 +1421,11 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.0.tgz#3d9018c575f0e3f7386c1de80ee66cc21fbb7a52" integrity sha512-rADY+HtTOA52l9VZWtgQfn4p+UDVM2eDVkMZT1I6syp0YKxW2F9v+0pbRZLsvskhQv/vMb6ZfCay81GHbz5SHg== +"@types/ms@*": + version "0.7.31" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" + integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== + "@types/newrelic@^7.0.3": version "7.0.3" resolved "https://registry.yarnpkg.com/@types/newrelic/-/newrelic-7.0.3.tgz#07ae3d0175f712e0fcaef69564dbe06b6039febb" @@ -1460,6 +1509,13 @@ "@types/mime" "*" "@types/node" "*" +"@types/set-cookie-parser@^2.4.0": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@types/set-cookie-parser/-/set-cookie-parser-2.4.2.tgz#b6a955219b54151bfebd4521170723df5e13caad" + integrity sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w== + dependencies: + "@types/node" "*" + "@types/sinon-chai@^3.2.8": version "3.2.8" resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.8.tgz#5871d09ab50d671d8e6dd72e9073f8e738ac61dc" @@ -1741,6 +1797,11 @@ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.7.0.tgz#e1993689ac42d2b16e9194376cfb6753f6254db1" integrity sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q== +"@xmldom/xmldom@^0.8.3": + version "0.8.6" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.6.tgz#8a1524eb5bd5e965c1e3735476f0262469f71440" + integrity sha512-uRjjusqpoqfmRkTaNuLJ2VohVr67Q5YwDATW3VU7PfzTj6IRaihGrYI7zckGZjxQPBIp63nfvJbM+Yu5ICh0Bg== + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -1751,6 +1812,11 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== +"@zxing/text-encoding@0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@zxing/text-encoding/-/text-encoding-0.9.0.tgz#fb50ffabc6c7c66a0c96b4c03e3d9be74864b70b" + integrity sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA== + abab@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" @@ -1861,6 +1927,13 @@ ansi-colors@4.1.1: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + ansi-html-community@^0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" @@ -1986,6 +2059,11 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= +available-typed-arrays@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" + integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== + axe-core@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.3.tgz#11c74d23d5013c0fa5d183796729bc3482bd2f6f" @@ -2063,6 +2141,11 @@ base32-encode@^1.1.1: resolved "https://registry.yarnpkg.com/base32-encode/-/base32-encode-1.1.1.tgz#d022d86aca0002a751bbe1bf20eb4a9b1cef4e95" integrity sha512-eqa0BeGghj3guezlasdHJhr3+J5ZbbQvxeprkcDMbRQrjlqOT832IUDT4Al4ofAwekFYMqkkM9KMUHs9Cu0HKA== +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + batch@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" @@ -2073,6 +2156,15 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +bl@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + body-parser@1.20.0: version "1.20.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" @@ -2158,6 +2250,14 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -2225,6 +2325,14 @@ chai@^4.3.6: pathval "^1.1.1" type-detect "^4.0.5" +chalk@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" + integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@^2.0.0: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -2234,7 +2342,7 @@ chalk@^2.0.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0, chalk@^4.0.0, chalk@^4.1.0: +chalk@^4.0, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -2247,12 +2355,17 @@ charcodes@^0.2.0: resolved "https://registry.yarnpkg.com/charcodes/-/charcodes-0.2.0.tgz#5208d327e6cc05f99eb80ffc814707572d1f14e4" integrity sha512-Y4kiDb+AM4Ecy58YkuZrrSRJBDQdQ2L+NyS1vHHFtNtUjgutcZfx3yp1dAONI/oPaPmyGfCLx5CxL+zauIMyKQ== +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + check-error@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= -chokidar@3.5.3, chokidar@^3.5.3: +chokidar@3.5.3, chokidar@^3.4.2, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -2292,6 +2405,23 @@ cleave.js@^1.6.0: resolved "https://registry.yarnpkg.com/cleave.js/-/cleave.js-1.6.0.tgz#0e4e011943bdd70c67c9dcf4ff800ce710529171" integrity sha512-ivqesy3j5hQVG3gywPfwKPbi/7ZSftY/UNp5uphnqjr25yI2CP8FS2ODQPzuLXXnNLi29e2+PgPkkiKUXLs/Nw== +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-spinners@^2.5.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.7.0.tgz#f815fd30b5f9eaac02db604c7a231ed7cb2f797a" + integrity sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw== + +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== + clipboard-polyfill@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/clipboard-polyfill/-/clipboard-polyfill-3.0.3.tgz#159ea0768e20edc7ffda404bd13c54c73de4ff40" @@ -2306,6 +2436,15 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + clone-deep@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" @@ -2315,6 +2454,11 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -2435,6 +2579,11 @@ cookie@0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + core-js-compat@^3.16.0, core-js-compat@^3.16.2, core-js-compat@^3.21.0: version "3.21.1" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.21.1.tgz#cac369f67c8d134ff8f9bd1623e3bc2c42068c82" @@ -2567,7 +2716,7 @@ debug@2.6.9, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: +debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -2628,6 +2777,13 @@ default-gateway@^6.0.3: dependencies: execa "^5.0.0" +defaults@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" + integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== + dependencies: + clone "^1.0.2" + define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" @@ -3152,7 +3308,7 @@ eventemitter3@^4.0.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -events@^3.2.0: +events@^3.2.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -3209,6 +3365,15 @@ express@^4.17.3: utils-merge "1.0.1" vary "~1.1.2" +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -3259,6 +3424,13 @@ faye-websocket@^0.11.3: dependencies: websocket-driver ">=0.5.1" +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -3357,6 +3529,13 @@ follow-redirects@^1.0.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -3546,6 +3725,13 @@ google-protobuf@^3.11.4: resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.20.1.tgz#1b255c2b59bcda7c399df46c65206aa3c7a0ce8b" integrity sha512-XMf1+O32FjYIV3CYu6Tuh5PNbfNEU5Xu22X+Xkdb/DUexFlCzhvv7d5Iirm4AOwn8lv4al1YvIhzGrg2j9Zfzw== +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" @@ -3556,6 +3742,11 @@ grapheme-splitter@^1.0.4: resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +"graphql@^15.0.0 || ^16.0.0": + version "16.6.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.6.0.tgz#c2dcffa4649db149f6282af726c8c83f1c7c5fdb" + integrity sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw== + handle-thing@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" @@ -3612,6 +3803,11 @@ he@1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +headers-polyfill@^3.1.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-3.1.2.tgz#9a4dcb545c5b95d9569592ef7ec0708aab763fbe" + integrity sha512-tWCK4biJ6hcLqTviLXVR9DTRfYGQMXEIUj3gwJ2rZ5wO/at3XtkI4g8mCvFdUF9l1KMBNCfmNAdnahm1cgavQA== + hosted-git-info@^2.1.4: version "2.8.9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" @@ -3724,7 +3920,7 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -iconv-lite@0.4.24: +iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -3746,6 +3942,11 @@ identity-style-guide@^6.5.0: domready "1.0.8" uswds "^2.13.3" +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" @@ -3795,7 +3996,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -3810,6 +4011,27 @@ ini@^1.3.5: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +inquirer@^8.2.0: + version "8.2.5" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.5.tgz#d8654a7542c35a9b9e069d27e2df4858784d54f8" + integrity sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.1" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.21" + mute-stream "0.0.8" + ora "^5.4.1" + run-async "^2.4.0" + rxjs "^7.5.5" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + wrap-ansi "^7.0.0" + internal-slot@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" @@ -3839,6 +4061,14 @@ ipaddr.js@^2.0.1: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0" integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== +is-arguments@^1.0.4: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -3866,7 +4096,7 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-callable@^1.1.4, is-callable@^1.2.6: +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.6: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== @@ -3900,6 +4130,13 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-generator-function@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -3907,11 +4144,21 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + is-negative-zero@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== +is-node-process@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-node-process/-/is-node-process-1.0.1.tgz#4fc7ac3a91e8aac58175fe0578abbc56f2831b23" + integrity sha512-5IcdXuf++TTNt3oGl9EBdkvndXA8gmc4bz/Y+mdEpWh3Mcn/+kOw6hI7LD5CocqJWMzeb0I0ClndRVNdEPuJXQ== + is-number-object@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" @@ -3990,6 +4237,17 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" +is-typed-array@^1.1.10, is-typed-array@^1.1.3: + version "1.1.10" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f" + integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" @@ -4038,6 +4296,11 @@ jest-worker@^27.4.5: merge-stream "^2.0.0" supports-color "^8.0.0" +js-levenshtein@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" + integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g== + js-sdsl@^4.1.4: version "4.1.4" resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.1.4.tgz#78793c90f80e8430b7d8dc94515b6c77d98a26a6" @@ -4313,12 +4576,12 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= -lodash@^4.17.15: +lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-symbols@4.1.0: +log-symbols@4.1.0, log-symbols@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== @@ -4558,6 +4821,31 @@ ms@2.1.3, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +msw@^0.49.2: + version "0.49.2" + resolved "https://registry.yarnpkg.com/msw/-/msw-0.49.2.tgz#c815fa514a1b3532e3d3af01d308bb63329ad1e2" + integrity sha512-70/E10f+POE2UmMw16v8PnKatpZplpkUwVRLBqiIdimpgaC3le7y2yOq9JmOrL15jpwWM5wAcPTOj0f550LI3g== + dependencies: + "@mswjs/cookies" "^0.2.2" + "@mswjs/interceptors" "^0.17.5" + "@open-draft/until" "^1.0.3" + "@types/cookie" "^0.4.1" + "@types/js-levenshtein" "^1.1.1" + chalk "4.1.1" + chokidar "^3.4.2" + cookie "^0.4.2" + graphql "^15.0.0 || ^16.0.0" + headers-polyfill "^3.1.0" + inquirer "^8.2.0" + is-node-process "^1.0.1" + js-levenshtein "^1.1.6" + node-fetch "^2.6.7" + outvariant "^1.3.0" + path-to-regexp "^6.2.0" + strict-event-emitter "^0.2.6" + type-fest "^2.19.0" + yargs "^17.3.1" + multicast-dns@^7.2.5: version "7.2.5" resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" @@ -4566,6 +4854,11 @@ multicast-dns@^7.2.5: dns-packet "^5.2.2" thunky "^1.0.2" +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + nanoid@3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" @@ -4602,6 +4895,13 @@ nise@^5.1.1: just-extend "^4.0.2" path-to-regexp "^1.7.0" +node-fetch@^2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + node-forge@^1: version "1.3.1" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" @@ -4745,7 +5045,7 @@ once@^1.3.0: dependencies: wrappy "1" -onetime@^5.1.2: +onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== @@ -4785,6 +5085,31 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" +ora@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + dependencies: + bl "^4.1.0" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== + +outvariant@^1.2.1, outvariant@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.3.0.tgz#c39723b1d2cba729c930b74bf962317a81b9b1c9" + integrity sha512-yeWM9k6UPfG/nzxdaPlJkB2p08hCg4xP6Lx99F+vP8YF7xyZVfTmJjrrNalkmzudD4WFvNLVudQikqUmF8zhVQ== + p-all@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-all/-/p-all-3.0.0.tgz#077c023c37e75e760193badab2bad3ccd5782bfb" @@ -4913,6 +5238,11 @@ path-to-regexp@^1.7.0: dependencies: isarray "0.0.1" +path-to-regexp@^6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5" + integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw== + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -5195,7 +5525,7 @@ readable-stream@^2.0.1: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6: +readable-stream@^3.0.6, readable-stream@^3.4.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -5352,6 +5682,14 @@ resolve@^2.0.0-next.3: is-core-module "^2.2.0" path-parse "^1.0.6" +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + retry@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" @@ -5369,6 +5707,11 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -5383,6 +5726,13 @@ rxjs@^7.4.0: dependencies: tslib "^2.1.0" +rxjs@^7.5.5: + version "7.8.0" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" + integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== + dependencies: + tslib "^2.1.0" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -5584,6 +5934,11 @@ serve-static@1.15.0: parseurl "~1.3.3" send "0.18.0" +set-cookie-parser@^2.4.6: + version "2.5.1" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.5.1.tgz#ddd3e9a566b0e8e0862aca974a6ac0e01349430b" + integrity sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ== + setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" @@ -5773,6 +6128,13 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +strict-event-emitter@^0.2.4, strict-event-emitter@^0.2.6: + version "0.2.8" + resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.2.8.tgz#b4e768927c67273c14c13d20e19d5e6c934b47ca" + integrity sha512-KDf/ujU8Zud3YaLtMCcTI4xkZlZVIYxTLr+XIULexP+77EEVWixeXroLUXQXiVtH4XH2W7jr/3PT1v3zBuvc3A== + dependencies: + events "^3.3.0" + string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -6054,11 +6416,23 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + thunky@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -6092,6 +6466,11 @@ tr46@^3.0.0: dependencies: punycode "^2.1.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + trim-newlines@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" @@ -6153,6 +6532,11 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + type-fest@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" @@ -6163,6 +6547,11 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -6255,6 +6644,17 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +util@^0.12.3: + version "0.12.5" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" + integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + is-typed-array "^1.1.3" + which-typed-array "^1.1.2" + utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" @@ -6312,11 +6712,32 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" +wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== + dependencies: + defaults "^1.0.3" + +web-encoding@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/web-encoding/-/web-encoding-1.1.5.tgz#fc810cf7667364a6335c939913f5051d3e0c4864" + integrity sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA== + dependencies: + util "^0.12.3" + optionalDependencies: + "@zxing/text-encoding" "0.9.0" + webcrypto-shim@^0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/webcrypto-shim/-/webcrypto-shim-0.1.7.tgz#da8be23061a0451cf23b424d4a9b61c10f091c12" integrity sha512-JAvAQR5mRNRxZW2jKigWMjCMkjSdmP5cColRP1U/pTg69VgHXEi1orv5vVpJ55Zc5MIaPc1aaurzd9pjv2bveg== +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + webidl-conversions@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" @@ -6489,6 +6910,14 @@ whatwg-url@^11.0.0: tr46 "^3.0.0" webidl-conversions "^7.0.0" +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" @@ -6500,6 +6929,18 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" +which-typed-array@^1.1.2: + version "1.1.9" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" + integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + is-typed-array "^1.1.10" + which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -6596,6 +7037,11 @@ yargs-parser@^20.2.2, yargs-parser@^20.2.3: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + yargs-unparser@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" @@ -6619,6 +7065,19 @@ yargs@16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" +yargs@^17.3.1: + version "17.6.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541" + integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" From 10cf687071b2bb1d6f8ba62b683f4a86620ad059 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 19 Dec 2022 09:04:21 -0500 Subject: [PATCH 04/20] Fix "No such file" error on "make normalize_yaml" (#7507) [skip changelog] --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 19d0ebf671b..b47e39d78cc 100644 --- a/Makefile +++ b/Makefile @@ -159,7 +159,7 @@ run-https: tmp/$(HOST)-$(PORT).key tmp/$(HOST)-$(PORT).crt ## Runs the developme normalize_yaml: ## Normalizes YAML files (alphabetizes keys, fixes line length, smart quotes) yarn normalize-yaml .rubocop.yml --disable-sort-keys --disable-smart-punctuation - find ./config/locales/telephony "./config/locales/telephony*" -type f | xargs yarn normalize-yaml --disable-smart-punctuation + find ./config/locales/telephony -type f | xargs yarn normalize-yaml --disable-smart-punctuation find ./config/locales -not -path "./config/locales/telephony*" -type f | xargs yarn normalize-yaml \ config/pinpoint_supported_countries.yml \ config/pinpoint_overrides.yml \ From b326a97511428b10b2306a1f1e1269e2704f7f2f Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 19 Dec 2022 09:27:46 -0500 Subject: [PATCH 05/20] Render ValidatedFieldComponent as aria-invalid when errors exist (#7503) * Render ValidatedFieldComponent as aria-invalid when errors exist changelog: Improvements, Accessibility, Mark fields as invalid when shown with initial errors * Avoid updating DOM on non-changing validity state --- .../validated_field_component.html.erb | 2 +- app/components/validated_field_component.rb | 4 +- .../validated-field-element.spec.ts | 51 ++++++++++++++++++- .../validated-field-element.ts | 10 +++- .../validated_field_component_spec.rb | 12 +++++ 5 files changed, 74 insertions(+), 5 deletions(-) diff --git a/app/components/validated_field_component.html.erb b/app/components/validated_field_component.html.erb index 88a77b187e1..773b559072d 100644 --- a/app/components/validated_field_component.html.erb +++ b/app/components/validated_field_component.html.erb @@ -17,7 +17,7 @@ input_html: { class: [*tag_options.dig(:input_html, :class), 'validated-field__input'], aria: { - invalid: false, + invalid: has_errors?, describedby: aria_describedby_idrefs, }, }, diff --git a/app/components/validated_field_component.rb b/app/components/validated_field_component.rb index 53eef78a625..d7e07afb34c 100644 --- a/app/components/validated_field_component.rb +++ b/app/components/validated_field_component.rb @@ -26,8 +26,6 @@ def aria_describedby_idrefs idrefs end - private - def has_errors? form.object.respond_to?(:errors) && form.object.errors.key?(name) end @@ -44,6 +42,8 @@ def hint_id "validated-field-hint-#{unique_id}" end + private + def value_missing_error_message case input_type when :boolean diff --git a/app/javascript/packages/validated-field/validated-field-element.spec.ts b/app/javascript/packages/validated-field/validated-field-element.spec.ts index d22b9341cb1..a4d93e4ffed 100644 --- a/app/javascript/packages/validated-field/validated-field-element.spec.ts +++ b/app/javascript/packages/validated-field/validated-field-element.spec.ts @@ -27,7 +27,7 @@ describe('ValidatedFieldElement', () => { Required Field { expect(element.querySelector('.usa-error-message')).to.not.exist(); }); + it('does not needlessly update DOM state when validity does not change', async () => { + const element = createAndConnectElement(); + const input = getByRole(element, 'textbox'); + sinon.spy(input, 'setAttribute'); + + await userEvent.type(input, '5'); + + expect(input.setAttribute).not.to.have.been.called(); + }); + it('shows error state and focuses on form validation', () => { const element = createAndConnectElement(); @@ -223,4 +233,43 @@ describe('ValidatedFieldElement', () => { expect(message.style.maxWidth).to.equal(''); }); }); + + describe('#isValid', () => { + context('without initial error', () => { + it('is true', () => { + const element = createAndConnectElement({ hasInitialError: false }); + + expect(element.isValid).to.be.true(); + }); + }); + + context('with initial error', () => { + it('is false', () => { + const element = createAndConnectElement({ hasInitialError: true }); + + expect(element.isValid).to.be.false(); + }); + }); + + context('after becoming invalid', () => { + it('is false', () => { + const element = createAndConnectElement(); + element.closest('form')!.checkValidity(); + + expect(element.isValid).to.be.false(); + }); + }); + + context('after becoming valid', () => { + it('is true', async () => { + const element = createAndConnectElement(); + const input = getByRole(element, 'textbox'); + element.closest('form')!.checkValidity(); + await userEvent.type(input, '5'); + element.closest('form')!.checkValidity(); + + expect(element.isValid).to.be.true(); + }); + }); + }); }); diff --git a/app/javascript/packages/validated-field/validated-field-element.ts b/app/javascript/packages/validated-field/validated-field-element.ts index 39b2713fa35..212f0fbc2c1 100644 --- a/app/javascript/packages/validated-field/validated-field-element.ts +++ b/app/javascript/packages/validated-field/validated-field-element.ts @@ -46,7 +46,11 @@ class ValidatedFieldElement extends HTMLElement { } get descriptorIdRefs(): string[] { - return this?.input?.getAttribute('aria-describedby')?.split(' ').filter(Boolean) || []; + return this.input?.getAttribute('aria-describedby')?.split(' ').filter(Boolean) || []; + } + + get isValid(): boolean { + return this.input?.getAttribute('aria-invalid') !== 'true'; } /** @@ -88,6 +92,10 @@ class ValidatedFieldElement extends HTMLElement { * @param isValid Whether input is valid. */ setInputIsValid(isValid: boolean) { + if (isValid === this.isValid) { + return; + } + this.input?.classList.toggle('usa-input--error', !isValid); this.input?.setAttribute('aria-invalid', String(!isValid)); diff --git a/spec/components/validated_field_component_spec.rb b/spec/components/validated_field_component_spec.rb index 62f48697b2f..0c5ef7b956a 100644 --- a/spec/components/validated_field_component_spec.rb +++ b/spec/components/validated_field_component_spec.rb @@ -31,6 +31,12 @@ expect(field.attr('aria-describedby')).to be_blank end + it 'is not rendered as invalid' do + field = rendered.at_css('input') + + expect(field.attr('aria-invalid')).to eq('false') + end + context 'when form has errors for field' do let(:error_message) { 'Field is required' } @@ -44,6 +50,12 @@ expect(field).to have_description error_message end + it 'is rendered as invalid' do + field = rendered.at_css('input') + + expect(field.attr('aria-invalid')).to eq('true') + end + context 'with aria tag option' do let(:tag_options) { { input_html: { aria: { describedby: 'foo' } } } } From 33ea85e563f20ff64bb0bfcd00dae4a31daaa02d Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 19 Dec 2022 10:53:45 -0500 Subject: [PATCH 06/20] Rename "Improvements" changelog category (#7511) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Rearrange changelog contributing guidance to optimize deeplinking changelog: Open Source, Update Contributing guidance for commit changelogs * Rename "Improvements" changelog category * Add changelog (correctly, this time) changelog: Internal, Open Source, Update Contributing guidance for commit changelogs ... the irony of failing to add a valid changelog in a pull request about improving the clarity of our changelog guidance is not lost on me 🤦‍♂️ --- CONTRIBUTING.md | 15 +++++++++------ scripts/changelog_check.rb | 11 ++++++++--- spec/fixtures/git_log_changelog.yml | 6 +++--- spec/scripts/changelog_check_spec.rb | 2 +- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad70afb5828..27c5745c7aa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,10 +26,8 @@ pull request is about. - If the pull request is in response to a Jira ticket, include the ticket ID in the commit title (e.g. "LG-1234 Add the stuff to the thing") -- Include a changelog message which describes the changes in human-readable -terms. These messages are included in release notes, so they should be easy to -understand for our partners and users. In the rare case that a change should -not be included in release notes, add `[skip changelog]` to the commit. +- Include a changelog message which describes the changes in human-readable terms. Refer to the +[_Changelog Messages_ section](#changelog-messages) below for specific changelog requirements. Example: @@ -52,7 +50,9 @@ changelog: Internal, Automated Testing, Improve performance of test suite #### Changelog Messages -You must include a changelog message in one commit of your pull request. +You must include a changelog message in one commit of your pull request. The changelog message +describes the changes in human-readable terms. These messages are included in release notes, so they +should be easy to understand for our partners and users. A changelog message should be written in the following format: @@ -63,7 +63,7 @@ changelog: [Category], [Subcategory], [Description] Replace `[Category]`, `[Subcategory]`, and `[Description]` with text relevant for your changes: - **Category** must be one of the following: - - **Improvements** are user-facing improvements to the application experience, such as a new UI component or updated text. + - **User-Facing Improvements** are improvements to the application experience benefitting the end-user, such as a new UI component or updated text. - **Bug Fixes** are corrections to a broken behavior, such as preventing a raised exception. - **Internal** are changes which benefit the Login.gov team, such as analytics or code quality. - **Upcoming Features** are iterations contributing to a feature which has not yet been enabled for users in production. @@ -72,6 +72,9 @@ Replace `[Category]`, `[Subcategory]`, and `[Description]` with text relevant fo If multiple pull requests iterate on the same feature, it's a good idea to use the same commit message, since identical messages will be combined into a single entry when the release notes are compiled. +In the rare case that a change should not be included in release notes, add `[skip changelog]` to +the commit. + ### Additional notes on pull requests and code reviews Please follow our [Code Review][review] guidelines. diff --git a/scripts/changelog_check.rb b/scripts/changelog_check.rb index 58c7f1e1a3c..8d2802059e4 100755 --- a/scripts/changelog_check.rb +++ b/scripts/changelog_check.rb @@ -5,7 +5,8 @@ CHANGELOG_REGEX = %r{^(?:\* )?[cC]hangelog: ?(?[\w -/]{2,}), ?(?[\w -]{2,}), ?(?.+)$} CATEGORIES = [ - 'Improvements', + 'User-Facing Improvements', + 'Improvements', # Temporary for transitional period 'Bug Fixes', 'Internal', 'Upcoming Features', @@ -93,7 +94,7 @@ def generate_invalid_changes(git_log) end def closest_change_category(change) - CATEGORIES. + category = CATEGORIES. map do |category| CategoryDistance.new( category, @@ -103,6 +104,10 @@ def closest_change_category(change) filter { |category_distance| category_distance.distance <= MAX_CATEGORY_DISTANCE }. max { |category_distance| category_distance.distance }&. category + + # Temporarily normalize legacy category in transitional period + category = 'User-Facing Improvements' if category == 'Improvements' + category end # Get the last valid changelog line for every Pull Request and tie it to the commit subject. @@ -249,7 +254,7 @@ def main(args) changelog: CATEGORY, SUBCATEGORY, CHANGE_DESCRIPTION example: - changelog: Improvements, Authentication, Updating Authentication (LG-9998) + changelog: User-Facing Improvements, WebAuthn, Improve error flow for WebAuthn (LG-5515) categories: #{CATEGORIES.map { |category| "- #{category}" }.join("\n")} diff --git a/spec/fixtures/git_log_changelog.yml b/spec/fixtures/git_log_changelog.yml index 7921ca0c420..dfef2690b5a 100644 --- a/spec/fixtures/git_log_changelog.yml +++ b/spec/fixtures/git_log_changelog.yml @@ -41,7 +41,7 @@ squashed_commit_with_multiple_commits: * add alert - * changelog: Improvements, Webauthn, Provide better error flow for users who may not be able to leverage webauthn (LG-5515) + * changelog: User-Facing Improvements, Webauthn, Provide better error flow for users who may not be able to leverage webauthn (LG-5515) * fix linting, debug Co-authored-by: Jessica Dembe @@ -51,11 +51,11 @@ squashed_commit_with_multiple_commits: commit_messages: - '* add check to see if platform is available' - '* add alert' - - '* changelog: Improvements, Webauthn, Provide better error flow for users who may not be able to leverage webauthn (LG-5515)' + - '* changelog: User-Facing Improvements, Webauthn, Provide better error flow for users who may not be able to leverage webauthn (LG-5515)' - '* fix linting, debug' - 'Co-authored-by: Jessica Dembe ' - 'Co-authored-by: Zach Margolis ' - category: 'Improvements' + category: 'User-Facing Improvements' subcategory: 'Webauthn' change: 'Provide better error flow for users who may not be able to leverage webauthn (LG-5515)' pr_number: '5976' diff --git a/spec/scripts/changelog_check_spec.rb b/spec/scripts/changelog_check_spec.rb index 29824a1e3fa..6b7c2dd0a04 100644 --- a/spec/scripts/changelog_check_spec.rb +++ b/spec/scripts/changelog_check_spec.rb @@ -104,7 +104,7 @@ formatted_changelog = format_changelog(changelogs) expect(formatted_changelog).to eq <<~CHANGELOG.chomp - ## Improvements + ## User-Facing Improvements - Webauthn: Provide better error flow for users who may not be able to leverage webauthn (LG-5515) ([#5976](https://github.com/18F/identity-idp/pull/5976)) ## Internal From 875cdc128dc9cbb801670c8de9d2cff503c2ef89 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 19 Dec 2022 10:54:04 -0500 Subject: [PATCH 07/20] Colocate GitHub issue templates (#7510) changelog: Internal, Open Source, Colocate GitHub issue templates --- .github/{ISSUE_TEMPLATE.md => issue_template.md} | 0 pull_request_template.md => .github/pull_request_template.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename .github/{ISSUE_TEMPLATE.md => issue_template.md} (100%) rename pull_request_template.md => .github/pull_request_template.md (100%) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/issue_template.md similarity index 100% rename from .github/ISSUE_TEMPLATE.md rename to .github/issue_template.md diff --git a/pull_request_template.md b/.github/pull_request_template.md similarity index 100% rename from pull_request_template.md rename to .github/pull_request_template.md From 28ac3ed9c33ca9bbed67ed587b499f43efc5b57c Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 19 Dec 2022 11:21:33 -0500 Subject: [PATCH 08/20] Improve reliability of flakey countdown spec (#7512) * Improve reliability of flakey countdown spec changelog: Internal, Automated Testing, Improve reliability of automated tests * Remove seconds normalization https://github.com/18F/identity-idp/pull/7512#discussion_r1052374756 --- spec/features/users/sign_in_spec.rb | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/spec/features/users/sign_in_spec.rb b/spec/features/users/sign_in_spec.rb index c2cae129948..e20d1d82523 100644 --- a/spec/features/users/sign_in_spec.rb +++ b/spec/features/users/sign_in_spec.rb @@ -266,12 +266,22 @@ end scenario 'user sees warning before session times out' do - expect(page).to have_content(/14 minutes and 5[0-9] seconds/, wait: 5) - - time1 = page.text[/14 minutes and 5[0-9] seconds/] - sleep(1.5) - time2 = page.text[/14 minutes and 5[0-9] seconds/] - expect(time2).to be < time1 + minutes_and = [ + t('datetime.dotiw.minutes', count: IdentityConfig.store.session_timeout_in_minutes - 1), + t('datetime.dotiw.two_words_connector'), + ].join('') + + pattern1 = Regexp.new(minutes_and + t('datetime.dotiw.seconds.other', count: '\d+')) + expect(page).to have_content(pattern1, wait: 5) + time = page.text[pattern1] + seconds = time.split(t('datetime.dotiw.two_words_connector')).last[/\d+/].to_i + pattern2 = Regexp.new( + minutes_and + t( + 'datetime.dotiw.seconds.other', + count: (seconds - 10...seconds).to_a.join('|'), + ), + ) + expect(page).to have_content(pattern2, wait: 5) end scenario 'user can continue browsing' do From d05fbccdee1b5cdfa9ba07d90c4b9d3396c78af0 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 20 Dec 2022 13:49:33 -0500 Subject: [PATCH 09/20] LG-7483: Remove feature flag select_multiple_mfa_options (#7505) * LG-7483: Remove feature flag select_multiple_mfa_options changelog: Bug Fixes, Account Management, Fix missing links to delete MFA methods when phone method is present * Fix specs which assumed second MFA disabled --- app/controllers/concerns/mfa_setup_concern.rb | 7 +- .../users/mfa_selection_controller.rb | 6 - app/forms/two_factor_options_form.rb | 11 +- app/policies/mfa_policy.rb | 6 +- .../selection_presenter.rb | 8 - .../two_factor_options_presenter.rb | 4 +- app/views/sign_up/completions/show.html.erb | 2 +- .../index.html.erb | 29 +-- config/application.yml.default | 3 - .../locales/two_factor_authentication/en.yml | 2 - .../locales/two_factor_authentication/es.yml | 2 - .../locales/two_factor_authentication/fr.yml | 2 - lib/identity_config.rb | 1 - .../otp_verification_controller_spec.rb | 3 +- .../backup_code_setup_controller_spec.rb | 3 +- .../users/mfa_selection_controller_spec.rb | 6 +- ...ac_authentication_setup_controller_spec.rb | 203 +++++++----------- .../users/totp_setup_controller_spec.rb | 1 - ...or_authentication_setup_controller_spec.rb | 3 +- .../users/webauthn_setup_controller_spec.rb | 3 +- .../mfa_cta_spec.rb | 19 +- spec/features/phone/confirmation_spec.rb | 2 +- spec/features/remember_device/phone_spec.rb | 1 + .../remember_device/sp_expiration_spec.rb | 1 + spec/features/remember_device/totp_spec.rb | 1 + .../user_opted_preference_spec.rb | 3 + .../features/remember_device/webauthn_spec.rb | 2 + spec/features/saml/ial1_sso_spec.rb | 1 - .../backup_code_sign_up_spec.rb | 4 +- .../multiple_mfa_sign_up_spec.rb | 1 - .../non_restricted_required_sign_in_spec.rb | 1 - spec/features/users/sign_in_spec.rb | 1 + spec/features/users/sign_up_spec.rb | 3 + spec/features/webauthn/hidden_spec.rb | 7 +- spec/features/webauthn/sign_up_spec.rb | 3 +- spec/forms/two_factor_options_form_spec.rb | 13 +- spec/policies/user_mfa_policy_spec.rb | 52 ++++- .../additional_mfa_required_presenter_spec.rb | 1 - .../mfa_confirmation_presenter_spec.rb | 4 - spec/support/features/session_helper.rb | 10 +- .../shared_examples/account_creation.rb | 5 +- .../show.html.erb_spec.rb | 4 +- .../mfa_confirmation/show.html.erb_spec.rb | 1 - .../sign_up/completions/show.html.erb_spec.rb | 41 +--- .../index.html.erb_spec.rb | 6 +- 45 files changed, 186 insertions(+), 306 deletions(-) diff --git a/app/controllers/concerns/mfa_setup_concern.rb b/app/controllers/concerns/mfa_setup_concern.rb index 75998220886..159c157dc94 100644 --- a/app/controllers/concerns/mfa_setup_concern.rb +++ b/app/controllers/concerns/mfa_setup_concern.rb @@ -2,7 +2,7 @@ module MfaSetupConcern extend ActiveSupport::Concern def next_setup_path - if user_needs_confirmation_screen? + if suggest_second_mfa? auth_method_confirmation_url elsif next_setup_choice confirmation_path @@ -45,11 +45,6 @@ def confirm_user_authenticated_for_2fa_setup redirect_to user_two_factor_authentication_url end - def user_needs_confirmation_screen? - suggest_second_mfa? && - IdentityConfig.store.select_multiple_mfa_options - end - def in_multi_mfa_selection_flow? return false unless user_session[:mfa_selections].present? mfa_selection_index < mfa_selection_count diff --git a/app/controllers/users/mfa_selection_controller.rb b/app/controllers/users/mfa_selection_controller.rb index ca168dcfbc6..eeb79ab0a85 100644 --- a/app/controllers/users/mfa_selection_controller.rb +++ b/app/controllers/users/mfa_selection_controller.rb @@ -6,7 +6,6 @@ class MfaSelectionController < ApplicationController before_action :authenticate_user before_action :confirm_user_authenticated_for_2fa_setup - before_action :multiple_factors_enabled? def index two_factor_options_form @@ -70,10 +69,5 @@ def process_valid_form def two_factor_options_form_params params.require(:two_factor_options_form).permit(:selection, selection: []) end - - def multiple_factors_enabled? - return if IdentityConfig.store.select_multiple_mfa_options - redirect_to after_mfa_setup_path - end end end diff --git a/app/forms/two_factor_options_form.rb b/app/forms/two_factor_options_form.rb index 3417acba94d..03f7346bce7 100644 --- a/app/forms/two_factor_options_form.rb +++ b/app/forms/two_factor_options_form.rb @@ -8,7 +8,7 @@ class TwoFactorOptionsForm backup_code] } validates :selection, length: { minimum: 1 }, if: :has_no_mfa_or_in_required_flow? - validates :selection, length: { minimum: 2, message: 'phone' }, if: :phone_validations? + validates :selection, length: { minimum: 2, message: 'phone' }, if: :phone_valid? def initialize(user:, phishing_resistant_required:, piv_cac_required:) self.user = user @@ -74,9 +74,10 @@ def phone_alternative_enabled? count >= 2 || (count == 1 && MfaContext.new(user).phone_configurations.none?) end - def phone_validations? - IdentityConfig.store.select_multiple_mfa_options && - phone_selected? && has_no_configured_mfa? && - !phone_alternative_enabled? && kantara_2fa_phone_restricted? + def phone_valid? + phone_selected? && + has_no_configured_mfa? && + !phone_alternative_enabled? && + kantara_2fa_phone_restricted? end end diff --git a/app/policies/mfa_policy.rb b/app/policies/mfa_policy.rb index ac443adee95..a59f177e233 100644 --- a/app/policies/mfa_policy.rb +++ b/app/policies/mfa_policy.rb @@ -18,9 +18,11 @@ def multiple_factors_enabled? end def multiple_non_restricted_factors_enabled? - IdentityConfig.store.select_multiple_mfa_options ? - mfa_user.enabled_non_restricted_mfa_methods_count > 1 : + if IdentityConfig.store.kantara_2fa_phone_restricted + mfa_user.enabled_non_restricted_mfa_methods_count > 1 + else multiple_factors_enabled? + end end def unphishable? diff --git a/app/presenters/two_factor_authentication/selection_presenter.rb b/app/presenters/two_factor_authentication/selection_presenter.rb index fadc22da592..bec75a4d272 100644 --- a/app/presenters/two_factor_authentication/selection_presenter.rb +++ b/app/presenters/two_factor_authentication/selection_presenter.rb @@ -120,16 +120,8 @@ def setup_info(type) t('two_factor_authentication.two_factor_choice_options.auth_app_info') when 'backup_code' t('two_factor_authentication.two_factor_choice_options.backup_code_info') - when 'phone' - IdentityConfig.store.select_multiple_mfa_options ? - t('two_factor_authentication.two_factor_choice_options.phone_info_html') : - t('two_factor_authentication.two_factor_choice_options.phone_info') when 'piv_cac' t('two_factor_authentication.two_factor_choice_options.piv_cac_info') - when 'sms' - t('two_factor_authentication.two_factor_choice_options.sms_info') - when 'voice' - t('two_factor_authentication.two_factor_choice_options.voice_info') when 'webauthn' t('two_factor_authentication.two_factor_choice_options.webauthn_info') when 'webauthn_platform' diff --git a/app/presenters/two_factor_options_presenter.rb b/app/presenters/two_factor_options_presenter.rb index ee7ab372800..c2ca917724a 100644 --- a/app/presenters/two_factor_options_presenter.rb +++ b/app/presenters/two_factor_options_presenter.rb @@ -41,10 +41,8 @@ def intro t('two_factor_authentication.two_factor_hspd12_choice_intro') elsif phishing_resistant_only? t('two_factor_authentication.two_factor_aal3_choice_intro') - elsif IdentityConfig.store.select_multiple_mfa_options - t('mfa.info') else - t('two_factor_authentication.two_factor_choice_intro') + t('mfa.info') end end diff --git a/app/views/sign_up/completions/show.html.erb b/app/views/sign_up/completions/show.html.erb index c7adf057820..f8182ba1baf 100644 --- a/app/views/sign_up/completions/show.html.erb +++ b/app/views/sign_up/completions/show.html.erb @@ -14,7 +14,7 @@ <%= render 'sign_up/completions/requested_attributes', pii: @presenter.pii %> -<% if !@multiple_factors_enabled && IdentityConfig.store.select_multiple_mfa_options %> +<% if !@multiple_factors_enabled %> <%= render(AlertComponent.new(type: :warning, class: 'margin-bottom-4')) do %> <%= link_to( t('mfa.second_method_warning.link'), diff --git a/app/views/users/two_factor_authentication_setup/index.html.erb b/app/views/users/two_factor_authentication_setup/index.html.erb index 53c51676462..d9e173e090c 100644 --- a/app/views/users/two_factor_authentication_setup/index.html.erb +++ b/app/views/users/two_factor_authentication_setup/index.html.erb @@ -21,34 +21,11 @@
<%= @presenter.intro %> - <% if IdentityConfig.store.select_multiple_mfa_options %> - <%= hidden_field_tag 'two_factor_options_form[selection][]', nil %> - <% end %> + <%= hidden_field_tag 'two_factor_options_form[selection][]', nil %> <% @presenter.options.each do |option| %>
" class="<%= option.html_class %>"> - <% if IdentityConfig.store.select_multiple_mfa_options %> - <%= render partial: 'partials/multi_factor_authentication/mfa_selection', - locals: { form: f, option: option } %> - <% else %> - <%= radio_button_tag( - 'two_factor_options_form[selection]', - option.type, - false, - disabled: option.disabled?, - class: 'usa-radio__input usa-radio__input--tile', - ) %> - <%= label_tag( - "two_factor_options_form_selection_#{option.type}", - class: 'usa-radio__label usa-radio__label--illustrated', - ) do %> - <%= image_tag(asset_url("mfa-options/#{option.type}.svg"), alt: "#{option.label} icon", class: 'usa-radio__image') %> -
<%= option.label %> - - <%= option.info %> - -
- <% end %> - <% end %> + <%= render partial: 'partials/multi_factor_authentication/mfa_selection', + locals: { form: f, option: option } %>
<% end %>
diff --git a/config/application.yml.default b/config/application.yml.default index 3ec13f201ca..e1380fc6295 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -278,7 +278,6 @@ s3_reports_enabled: false saml_internal_post: false saml_secret_rotation_enabled: false seed_agreements_data: true -select_multiple_mfa_options: false service_provider_request_ttl_hours: 24 session_check_delay: 30 session_check_frequency: 30 @@ -389,7 +388,6 @@ development: saml_endpoint_configs: '[{"suffix":"2021","secret_key_passphrase":"trust-but-verify"},{"suffix":"2022","secret_key_passphrase":"trust-but-verify"}]' scrypt_cost: 10000$8$1$ secret_key_base: development_secret_key_base - select_multiple_mfa_options: true session_encryption_key: 27bad3c25711099429c1afdfd1890910f3b59f5a4faec1c85e945cb8b02b02f261ba501d99cfbb4fab394e0102de6fecf8ffe260f322f610db3e96b2a775c120 show_account_recovery_recovery_options: true skip_encryption_allowed_list: '["urn:gov:gsa:SAML:2.0.profiles:sp:sso:localhost"]' @@ -564,7 +562,6 @@ test: saml_endpoint_configs: '[{"suffix":"2022","secret_key_passphrase":"trust-but-verify"}]' saml_internal_post: true scrypt_cost: 800$8$1$ - select_multiple_mfa_options: false secret_key_base: test_secret_key_base session_encryption_key: 27bad3c25711099429c1afdfd1890910f3b59f5a4faec1c85e945cb8b02b02f261ba501d99cfbb4fab394e0102de6fecf8ffe260f322f610db3e96b2a775c120 skip_encryption_allowed_list: '[]' diff --git a/config/locales/two_factor_authentication/en.yml b/config/locales/two_factor_authentication/en.yml index d740c3eac57..b91bfcd162c 100644 --- a/config/locales/two_factor_authentication/en.yml +++ b/config/locales/two_factor_authentication/en.yml @@ -125,8 +125,6 @@ en: need to verify your identity using a physical device such as a security key or government employee ID (PIV or CAC) to access your information. two_factor_choice: Authentication method setup - two_factor_choice_intro: Add another layer of security by using one of the - multi-factor authentication options below. two_factor_choice_options: auth_app: Authentication application auth_app_info: Download or use an authentication app of your choice to generate diff --git a/config/locales/two_factor_authentication/es.yml b/config/locales/two_factor_authentication/es.yml index d8acfb0792d..b8c8a9554bc 100644 --- a/config/locales/two_factor_authentication/es.yml +++ b/config/locales/two_factor_authentication/es.yml @@ -134,8 +134,6 @@ es: llave de seguridad o una identificación de empleado del Gobierno (PIV o CAC) para acceder a su información. two_factor_choice: Configuración del método de autenticación - two_factor_choice_intro: Agregue un nivel adicional de seguridad usando una de - las siguientes opciones de autenticación de múltiples factores. two_factor_choice_options: auth_app: Aplicación de autenticación auth_app_info: Descargue o use la aplicación de autenticación de su preferencia diff --git a/config/locales/two_factor_authentication/fr.yml b/config/locales/two_factor_authentication/fr.yml index c0a1031d407..79b8ab3f19a 100644 --- a/config/locales/two_factor_authentication/fr.yml +++ b/config/locales/two_factor_authentication/fr.yml @@ -139,8 +139,6 @@ fr: physique tel qu’une clé de sécurité ou un badge d’employé du gouvernement (PIV ou CAC) pour accéder à vos informations. two_factor_choice: Configuration de la méthode d’authentification - two_factor_choice_intro: Ajoutez une autre étape de sécurité en utilisant l’une - des options d’authentification multifactorielle ci-dessous. two_factor_choice_options: auth_app: Demande d’authentification auth_app_info: Téléchargez ou utilisez une application d’authentification de diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 1a62c38af3c..e93e6e7cb24 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -372,7 +372,6 @@ def self.build_store(config_map) config.add(:scrypt_cost, type: :string) config.add(:secret_key_base, type: :string) config.add(:seed_agreements_data, type: :boolean) - config.add(:select_multiple_mfa_options, type: :boolean) config.add(:service_provider_request_ttl_hours, type: :integer) config.add(:saml_internal_post, type: :boolean) config.add(:session_check_delay, type: :integer) diff --git a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb index 2b729f27333..7faaeb9efde 100644 --- a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb @@ -572,11 +572,10 @@ end end - context 'Feature flag #select_multiple_mfa_options is true' do + describe 'multiple MFA handling' do let(:mfa_selections) { ['sms', 'backup_code'] } before do subject.user_session[:mfa_selections] = mfa_selections - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return true post( :create, diff --git a/spec/controllers/users/backup_code_setup_controller_spec.rb b/spec/controllers/users/backup_code_setup_controller_spec.rb index 9c1b6be6b8a..3c5a8a4a16e 100644 --- a/spec/controllers/users/backup_code_setup_controller_spec.rb +++ b/spec/controllers/users/backup_code_setup_controller_spec.rb @@ -53,13 +53,12 @@ expect(user.backup_code_configurations.length).to eq 10 end - context 'with multiple MFA selection on' do + describe 'multiple MFA handling' do let(:mfa_selections) { ['backup_code', 'voice'] } before do @user = build(:user) stub_sign_in(@user) controller.user_session[:mfa_selections] = mfa_selections - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return true end context 'when user selects multiple mfas on account creation' do diff --git a/spec/controllers/users/mfa_selection_controller_spec.rb b/spec/controllers/users/mfa_selection_controller_spec.rb index e52e6677e9a..588165ee2e2 100644 --- a/spec/controllers/users/mfa_selection_controller_spec.rb +++ b/spec/controllers/users/mfa_selection_controller_spec.rb @@ -2,9 +2,6 @@ describe Users::MfaSelectionController do let(:current_sp) { create(:service_provider) } - before do - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return(true) - end describe '#index' do before do @@ -47,9 +44,8 @@ patch :update, params: voice_params end - context 'when the selection is only phone and multi mfa is enabled' do + context 'when the selection is only phone and kantara phone restriction is enabled' do before do - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return(true) allow(IdentityConfig.store).to receive(:kantara_2fa_phone_restricted).and_return(true) stub_sign_in_before_2fa diff --git a/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb b/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb index 9b3e0f2ad23..ff067b336a0 100644 --- a/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb +++ b/spec/controllers/users/piv_cac_authentication_setup_controller_spec.rb @@ -94,137 +94,92 @@ end context 'when redirected with a good token' do - context 'with multiple MFA options feature toggle on' do - let(:user) do - create(:user) - end - let(:mfa_selections) { ['piv_cac', 'voice'] } - before do - subject.user_session[:mfa_selections] = mfa_selections - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return true + let(:user) do + create(:user) + end + let(:mfa_selections) { ['piv_cac', 'voice'] } + before do + subject.user_session[:mfa_selections] = mfa_selections + end + + context 'with no additional MFAs chosen on setup' do + let(:mfa_selections) { ['piv_cac'] } + it 'redirects to suggest 2nd MFA page' do + stub_attempts_tracker + expect(@irs_attempts_api_tracker).to receive(:track_event).with( + :mfa_enroll_piv_cac, + success: true, + subject_dn: 'some dn', + failure_reason: nil, + ) + + get :new, params: { token: good_token } + expect(response).to redirect_to(auth_method_confirmation_url) end - context 'with no additional MFAs chosen on setup' do - let(:mfa_selections) { ['piv_cac'] } - it 'redirects to suggest 2nd MFA page' do - stub_attempts_tracker - expect(@irs_attempts_api_tracker).to receive(:track_event).with( - :mfa_enroll_piv_cac, - success: true, - subject_dn: 'some dn', - failure_reason: nil, - ) - - get :new, params: { token: good_token } - expect(response).to redirect_to(auth_method_confirmation_url) - end - - it 'sets the piv/cac session information' do - get :new, params: { token: good_token } - json = { - 'subject' => 'some dn', - 'issuer' => nil, - 'presented' => true, - }.to_json - - expect(subject.user_session[:decrypted_x509]).to eq json - end - - it 'sets the session to not require piv setup upon sign-in' do - stub_attempts_tracker - expect(@irs_attempts_api_tracker).to receive(:track_event).with( - :mfa_enroll_piv_cac, - success: true, - subject_dn: 'some dn', - failure_reason: nil, - ) - - get :new, params: { token: good_token } - - expect(subject.session[:needs_to_setup_piv_cac_after_sign_in]).to eq false - end + it 'sets the piv/cac session information' do + get :new, params: { token: good_token } + json = { + 'subject' => 'some dn', + 'issuer' => nil, + 'presented' => true, + }.to_json + + expect(subject.user_session[:decrypted_x509]).to eq json end - context 'with additional MFAs leftover' do - it 'redirects to Mfa Confirmation page' do - stub_attempts_tracker - expect(@irs_attempts_api_tracker).to receive(:track_event).with( - :mfa_enroll_piv_cac, - success: true, - subject_dn: 'some dn', - failure_reason: nil, - ) - - get :new, params: { token: good_token } - expect(response).to redirect_to(phone_setup_url) - end - - it 'sets the piv/cac session information' do - stub_attempts_tracker - expect(@irs_attempts_api_tracker).to receive(:track_event).with( - :mfa_enroll_piv_cac, - success: true, - subject_dn: 'some dn', - failure_reason: nil, - ) - - get :new, params: { token: good_token } - json = { - 'subject' => 'some dn', - 'issuer' => nil, - 'presented' => true, - }.to_json - - expect(subject.user_session[:decrypted_x509]).to eq json - end - - it 'sets the session to not require piv setup upon sign-in' do - get :new, params: { token: good_token } - - expect(subject.session[:needs_to_setup_piv_cac_after_sign_in]).to eq false - end + it 'sets the session to not require piv setup upon sign-in' do + stub_attempts_tracker + expect(@irs_attempts_api_tracker).to receive(:track_event).with( + :mfa_enroll_piv_cac, + success: true, + subject_dn: 'some dn', + failure_reason: nil, + ) + + get :new, params: { token: good_token } + + expect(subject.session[:needs_to_setup_piv_cac_after_sign_in]).to eq false end end - context 'with multiple MFA options feature toggle off' do - context 'with no additional MFAs chosen on setup' do - it 'redirects to suggest account page' do - stub_attempts_tracker - expect(@irs_attempts_api_tracker).to receive(:track_event).with( - :mfa_enroll_piv_cac, - success: true, - subject_dn: 'some dn', - failure_reason: nil, - ) - - get :new, params: { token: good_token } - expect(response).to redirect_to(account_url) - end - - it 'sets the piv/cac session information' do - get :new, params: { token: good_token } - json = { - 'subject' => 'some dn', - 'issuer' => nil, - 'presented' => true, - }.to_json - - expect(subject.user_session[:decrypted_x509]).to eq json - end - - it 'sets the session to not require piv setup upon sign-in' do - stub_attempts_tracker - expect(@irs_attempts_api_tracker).to receive(:track_event).with( - :mfa_enroll_piv_cac, - success: true, - subject_dn: 'some dn', - failure_reason: nil, - ) - - get :new, params: { token: good_token } - - expect(subject.session[:needs_to_setup_piv_cac_after_sign_in]).to eq false - end + context 'with additional MFAs leftover' do + it 'redirects to Mfa Confirmation page' do + stub_attempts_tracker + expect(@irs_attempts_api_tracker).to receive(:track_event).with( + :mfa_enroll_piv_cac, + success: true, + subject_dn: 'some dn', + failure_reason: nil, + ) + + get :new, params: { token: good_token } + expect(response).to redirect_to(phone_setup_url) + end + + it 'sets the piv/cac session information' do + stub_attempts_tracker + expect(@irs_attempts_api_tracker).to receive(:track_event).with( + :mfa_enroll_piv_cac, + success: true, + subject_dn: 'some dn', + failure_reason: nil, + ) + + get :new, params: { token: good_token } + json = { + 'subject' => 'some dn', + 'issuer' => nil, + 'presented' => true, + }.to_json + + expect(subject.user_session[:decrypted_x509]).to eq json + end + + it 'sets the session to not require piv setup upon sign-in' do + get :new, params: { token: good_token } + + expect(subject.session[:needs_to_setup_piv_cac_after_sign_in]).to eq false end end end diff --git a/spec/controllers/users/totp_setup_controller_spec.rb b/spec/controllers/users/totp_setup_controller_spec.rb index e2ee9c3140f..cfcf61a74c6 100644 --- a/spec/controllers/users/totp_setup_controller_spec.rb +++ b/spec/controllers/users/totp_setup_controller_spec.rb @@ -285,7 +285,6 @@ allow(@irs_attempts_api_tracker).to receive(:track_event) subject.user_session[:new_totp_secret] = secret subject.user_session[:mfa_selections] = mfa_selections - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return true patch :confirm, params: { name: name, code: generate_totp_code(secret) } end diff --git a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb index ee52dd52395..670e602aeb3 100644 --- a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb +++ b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb @@ -117,9 +117,8 @@ } end - context 'when the selection is only phone and multi mfa is enabled' do + context 'when the selection is only phone and kantara phone restriction is enabled' do before do - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return(true) allow(IdentityConfig.store).to receive(:kantara_2fa_phone_restricted).and_return(true) stub_sign_in_before_2fa diff --git a/spec/controllers/users/webauthn_setup_controller_spec.rb b/spec/controllers/users/webauthn_setup_controller_spec.rb index 0b3472035c3..aa570edd5e8 100644 --- a/spec/controllers/users/webauthn_setup_controller_spec.rb +++ b/spec/controllers/users/webauthn_setup_controller_spec.rb @@ -172,12 +172,11 @@ request.host = 'localhost:3000' controller.user_session[:webauthn_challenge] = webauthn_challenge end - context ' Multiple MFA options turned on' do + describe 'multiple MFA handling' do let(:mfa_selections) { ['webauthn', 'voice'] } before do controller.user_session[:mfa_selections] = mfa_selections - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return true end context 'with multiple MFA methods chosen on account creation' do diff --git a/spec/features/multi_factor_authentication/mfa_cta_spec.rb b/spec/features/multi_factor_authentication/mfa_cta_spec.rb index 08de9231746..48104794d65 100644 --- a/spec/features/multi_factor_authentication/mfa_cta_spec.rb +++ b/spec/features/multi_factor_authentication/mfa_cta_spec.rb @@ -4,24 +4,7 @@ include DocAuthHelper include SamlAuthHelper - context 'When multiple factor authentication feature is disabled' do - it 'does not display a banner as the feature is disabled' do - visit_idp_from_sp_with_ial1(:oidc) - user = sign_up_and_set_password - select_2fa_option('backup_code') - click_continue - - expect(MfaPolicy.new(user).multiple_factors_enabled?).to eq false - expect(page).to have_current_path(sign_up_completed_path) - expect(page).not_to have_content(t('mfa.second_method_warning.text')) - end - end - - context 'When the multiple factor authentication feature is enabled' do - before do - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return(true) - end - + describe 'multiple MFA handling' do it 'displays a banner after configuring a single MFA method' do visit_idp_from_sp_with_ial1(:oidc) user = sign_up_and_set_password diff --git a/spec/features/phone/confirmation_spec.rb b/spec/features/phone/confirmation_spec.rb index aea73dec685..e04170f0715 100644 --- a/spec/features/phone/confirmation_spec.rb +++ b/spec/features/phone/confirmation_spec.rb @@ -20,7 +20,7 @@ def visit_otp_confirmation(delivery_method) def expect_successful_otp_confirmation(delivery_method) expect(page).to have_content(t('notices.phone_confirmed')) - expect(page).to have_current_path(account_path) + expect(page).to have_current_path(auth_method_confirmation_path) expect(phone_configuration.confirmed_at).to_not be_nil expect(phone_configuration.delivery_preference).to eq(delivery_method.to_s) end diff --git a/spec/features/remember_device/phone_spec.rb b/spec/features/remember_device/phone_spec.rb index c5c9d4c445d..dddbb978ef9 100644 --- a/spec/features/remember_device/phone_spec.rb +++ b/spec/features/remember_device/phone_spec.rb @@ -34,6 +34,7 @@ def remember_device_and_sign_out_user check t('forms.messages.remember_device') fill_in_code_with_last_phone_otp click_submit_default + skip_second_mfa_prompt first(:link, t('links.sign_out')).click user diff --git a/spec/features/remember_device/sp_expiration_spec.rb b/spec/features/remember_device/sp_expiration_spec.rb index 900f0def364..d229081ea1c 100644 --- a/spec/features/remember_device/sp_expiration_spec.rb +++ b/spec/features/remember_device/sp_expiration_spec.rb @@ -106,6 +106,7 @@ check t('forms.messages.remember_device') fill_in_code_with_last_phone_otp click_submit_default + skip_second_mfa_prompt first(:link, t('links.sign_out')).click user_record diff --git a/spec/features/remember_device/totp_spec.rb b/spec/features/remember_device/totp_spec.rb index 2e9fd05f062..7064cb579b2 100644 --- a/spec/features/remember_device/totp_spec.rb +++ b/spec/features/remember_device/totp_spec.rb @@ -30,6 +30,7 @@ def remember_device_and_sign_out_user fill_in :code, with: totp_secret_from_page check t('forms.messages.remember_device') click_submit_default + skip_second_mfa_prompt first(:link, t('links.sign_out')).click user diff --git a/spec/features/remember_device/user_opted_preference_spec.rb b/spec/features/remember_device/user_opted_preference_spec.rb index 2ac2059cee9..ab626f24f00 100644 --- a/spec/features/remember_device/user_opted_preference_spec.rb +++ b/spec/features/remember_device/user_opted_preference_spec.rb @@ -15,6 +15,7 @@ uncheck 'remember_device' click_button 'Submit' + skip_second_mfa_prompt first(:link, t('links.sign_out')).click sign_in_user(user) @@ -41,6 +42,7 @@ mock_press_button_on_hardware_key_on_setup click_continue + skip_second_mfa_prompt first(:link, t('links.sign_out')).click sign_in_user(user) @@ -68,6 +70,7 @@ uncheck 'remember_device' click_submit_default + skip_second_mfa_prompt first(:link, t('links.sign_out')).click sign_in_user(user) diff --git a/spec/features/remember_device/webauthn_spec.rb b/spec/features/remember_device/webauthn_spec.rb index 9eeca977213..1a36f905e49 100644 --- a/spec/features/remember_device/webauthn_spec.rb +++ b/spec/features/remember_device/webauthn_spec.rb @@ -45,6 +45,7 @@ def remember_device_and_sign_out_user fill_in_nickname_and_click_continue check t('forms.messages.remember_device') mock_press_button_on_hardware_key_on_setup + skip_second_mfa_prompt first(:link, t('links.sign_out')).click user @@ -108,6 +109,7 @@ def remember_device_and_sign_out_user fill_in_nickname_and_click_continue check t('forms.messages.remember_device') mock_press_button_on_hardware_key_on_setup + skip_second_mfa_prompt first(:link, t('links.sign_out')).click user diff --git a/spec/features/saml/ial1_sso_spec.rb b/spec/features/saml/ial1_sso_spec.rb index c5ab25a9b91..6d279f2ad92 100644 --- a/spec/features/saml/ial1_sso_spec.rb +++ b/spec/features/saml/ial1_sso_spec.rb @@ -15,7 +15,6 @@ perform_in_browser(:two) do confirm_email_in_a_different_browser(email) - click_submit_default expect(current_path).to eq sign_up_completed_path within('.requested-attributes') do expect(page).to have_content t('help_text.requested_attributes.email') diff --git a/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb b/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb index 01afa8f84ce..d7598ee06ee 100644 --- a/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb +++ b/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb @@ -17,7 +17,8 @@ click_continue expect(page).to have_content(t('notices.backup_codes_configured')) - expect(current_path).to eq account_path + + expect(current_path).to eq auth_method_confirmation_path expect(user.backup_code_configurations.count).to eq(10) end @@ -66,6 +67,7 @@ sign_up_and_set_password select_2fa_option('backup_code') click_continue + skip_second_mfa_prompt expect(page).to have_current_path(sign_up_completed_path) diff --git a/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb b/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb index 9335d9951af..9be50575386 100644 --- a/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb +++ b/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb @@ -2,7 +2,6 @@ feature 'Multi Two Factor Authentication' do before do - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return(true) allow(IdentityConfig.store).to receive(:kantara_2fa_phone_restricted).and_return(true) end diff --git a/spec/features/users/non_restricted_required_sign_in_spec.rb b/spec/features/users/non_restricted_required_sign_in_spec.rb index 6127561b00c..e24dc16a489 100644 --- a/spec/features/users/non_restricted_required_sign_in_spec.rb +++ b/spec/features/users/non_restricted_required_sign_in_spec.rb @@ -10,7 +10,6 @@ let(:enforcement_date) { Time.zone.today - 6.days } before do - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return(true) allow(IdentityConfig.store).to receive(:kantara_2fa_phone_existing_user_restriction). and_return(true) allow(IdentityConfig.store).to receive(:kantara_restriction_enforcement_date). diff --git a/spec/features/users/sign_in_spec.rb b/spec/features/users/sign_in_spec.rb index e20d1d82523..31614b5eda7 100644 --- a/spec/features/users/sign_in_spec.rb +++ b/spec/features/users/sign_in_spec.rb @@ -150,6 +150,7 @@ click_send_one_time_code fill_in_code_with_last_phone_otp click_submit_default + skip_second_mfa_prompt click_agree_and_continue expect(current_url).to start_with('http://localhost:7654/auth/result') end diff --git a/spec/features/users/sign_up_spec.rb b/spec/features/users/sign_up_spec.rb index 3e29ac14723..504343b419c 100644 --- a/spec/features/users/sign_up_spec.rb +++ b/spec/features/users/sign_up_spec.rb @@ -179,6 +179,8 @@ fill_in 'name', with: 'Authentication app' click_button 'Submit' + skip_second_mfa_prompt + expect(page).to have_current_path account_path end end @@ -231,6 +233,7 @@ it 'allows a user to choose TOTP as 2FA method during sign up' do sign_in_user set_up_2fa_with_authenticator_app + skip_second_mfa_prompt expect(page).to have_current_path account_path end diff --git a/spec/features/webauthn/hidden_spec.rb b/spec/features/webauthn/hidden_spec.rb index 29e5013fe49..fbe908a5c64 100644 --- a/spec/features/webauthn/hidden_spec.rb +++ b/spec/features/webauthn/hidden_spec.rb @@ -42,8 +42,9 @@ end def webauthn_option_hidden? - page.find( - 'label[for=two_factor_options_form_selection_webauthn]', - ).find(:xpath, '..')[:class].include?('display-none') + page.find('label[for=two_factor_options_form_selection_webauthn]').ancestor('.display-none') + true + rescue Capybara::ElementNotFound + false end end diff --git a/spec/features/webauthn/sign_up_spec.rb b/spec/features/webauthn/sign_up_spec.rb index 9c9a4b7ab77..74f09e95ea0 100644 --- a/spec/features/webauthn/sign_up_spec.rb +++ b/spec/features/webauthn/sign_up_spec.rb @@ -13,7 +13,7 @@ def visit_webauthn_setup def expect_webauthn_setup_success expect(page).to have_content(t('notices.webauthn_configured')) - expect(page).to have_current_path(account_path) + expect(page).to have_current_path(auth_method_confirmation_path) end def expect_webauthn_setup_error @@ -34,6 +34,7 @@ def expect_webauthn_setup_error fill_in_nickname_and_click_continue mock_press_button_on_hardware_key_on_setup + skip_second_mfa_prompt expect(current_path).to eq(sign_up_completed_path) end diff --git a/spec/forms/two_factor_options_form_spec.rb b/spec/forms/two_factor_options_form_spec.rb index 88919626717..ee7daaef43f 100644 --- a/spec/forms/two_factor_options_form_spec.rb +++ b/spec/forms/two_factor_options_form_spec.rb @@ -25,8 +25,7 @@ end end - it 'is unsuccessful if the selection is invalid for multi mfa' do - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return(true) + it 'is unsuccessful if the selection is invalid for kantara phone restriction' do allow(IdentityConfig.store).to receive(:kantara_2fa_phone_restricted).and_return(true) %w[phone sms voice !!!!].each do |selection| result = subject.submit(selection: selection) @@ -99,7 +98,6 @@ context 'when phone is selected as their first authentication method' do before do - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return(true) allow(IdentityConfig.store).to receive(:kantara_2fa_phone_restricted).and_return(true) end @@ -113,10 +111,6 @@ let(:enabled_mfa_methods_count) { 1 } let(:mfa_selection) { ['phone'] } - before do - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return(true) - end - it 'submits the form' do expect(submit_phone.success?).to eq true end @@ -135,10 +129,6 @@ let(:phishing_resistant_required) { true } let(:piv_cac_required) { false } - before do - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return(true) - end - context 'when user is didnt select an mfa' do let(:mfa_selection) { nil } @@ -161,7 +151,6 @@ context 'when the feature flag toggle for 2FA phone restriction is off' do before do - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return(true) allow(IdentityConfig.store).to receive(:kantara_2fa_phone_restricted).and_return(false) end diff --git a/spec/policies/user_mfa_policy_spec.rb b/spec/policies/user_mfa_policy_spec.rb index 8ef05ad8526..5fba3df1742 100644 --- a/spec/policies/user_mfa_policy_spec.rb +++ b/spec/policies/user_mfa_policy_spec.rb @@ -39,26 +39,56 @@ end describe '#multiple_non_restricted_factors_enabled?' do - context 'with multi mfa disabled returns true ' do - before do - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return false + context 'with kantara phone restriction disabled' do + context 'with single mfa method' do + let(:user) { create(:user, :with_phone) } + + it { expect(subject.multiple_non_restricted_factors_enabled?).to eq false } end - let(:user) { create(:user, :with_phone, :with_piv_or_cac) } + context 'with multiple mfa methods' do + let(:user) { create(:user, :with_phone) } + + before do + user.phone_configurations << build(:phone_configuration, delivery_preference: :sms) + end - it { expect(subject.multiple_non_restricted_factors_enabled?).to eq true } + it { expect(subject.multiple_non_restricted_factors_enabled?).to eq true } + end end - end - describe '#multiple_non_restricted_factors_enabled?' do - context 'with multi mfa enabled returns false ' do + context 'with kantara phone restriction enabled' do before do - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return true + allow(IdentityConfig.store).to receive(:kantara_2fa_phone_restricted).and_return(true) + end + + context 'with single restricted mfa method' do + let(:user) { create(:user, :with_phone) } + + it { expect(subject.multiple_non_restricted_factors_enabled?).to eq false } end - let(:user) { create(:user, :with_phone, :with_piv_or_cac) } + context 'with multiple restricted mfa methods' do + let(:user) { create(:user, :with_phone) } - it { expect(subject.multiple_non_restricted_factors_enabled?).to eq false } + before do + user.phone_configurations << build(:phone_configuration, delivery_preference: :sms) + end + + it { expect(subject.multiple_non_restricted_factors_enabled?).to eq false } + end + + context 'with single restricted and single unrestricted mfa methods' do + let(:user) { create(:user, :with_phone, :with_piv_or_cac) } + + it { expect(subject.multiple_non_restricted_factors_enabled?).to eq false } + end + + context 'with multiple unrestricted mfa methods' do + let(:user) { create(:user, :with_webauthn, :with_piv_or_cac) } + + it { expect(subject.multiple_non_restricted_factors_enabled?).to eq true } + end end end end diff --git a/spec/presenters/additional_mfa_required_presenter_spec.rb b/spec/presenters/additional_mfa_required_presenter_spec.rb index 2cda8a0ac0e..bb31eb87897 100644 --- a/spec/presenters/additional_mfa_required_presenter_spec.rb +++ b/spec/presenters/additional_mfa_required_presenter_spec.rb @@ -7,7 +7,6 @@ let(:current_date) { Time.zone.today } before do - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return(true) allow(IdentityConfig.store).to receive(:kantara_restriction_enforcement_date). and_return(enforcement_date.to_datetime) end diff --git a/spec/presenters/mfa_confirmation_presenter_spec.rb b/spec/presenters/mfa_confirmation_presenter_spec.rb index 74e264b852d..77d94ee2036 100644 --- a/spec/presenters/mfa_confirmation_presenter_spec.rb +++ b/spec/presenters/mfa_confirmation_presenter_spec.rb @@ -4,10 +4,6 @@ let(:user) { create(:user, :with_phone) } let(:presenter) { described_class.new(user) } - before do - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return(true) - end - describe '#heading?' do it 'supplies a message' do expect(presenter.heading). diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb index 1487b5fb7ad..e7e2df844a3 100644 --- a/spec/support/features/session_helper.rb +++ b/spec/support/features/session_helper.rb @@ -41,6 +41,7 @@ def sign_up_and_2fa_ial1_user uncheck(t('forms.messages.remember_device')) fill_in_code_with_last_phone_otp click_submit_default + skip_second_mfa_prompt user end @@ -442,7 +443,7 @@ def confirm_email_in_a_different_browser(email) submit_form_with_valid_password set_up_2fa_with_valid_phone - # expect(page).to have_css('img[src*=sp-logos]') + skip_second_mfa_prompt end def click_sign_in_from_landing_page_then_click_create_account @@ -529,6 +530,7 @@ def set_up_mfa_with_backup_codes def register_user(email = 'test@test.com') confirm_email_and_password(email) set_up_2fa_with_valid_phone + skip_second_mfa_prompt User.find_with_email(email) end @@ -548,6 +550,7 @@ def confirm_email_and_password(email) def register_user_with_authenticator_app(email = 'test@test.com') confirm_email_and_password(email) set_up_2fa_with_authenticator_app + skip_second_mfa_prompt end def set_up_2fa_with_authenticator_app @@ -570,6 +573,7 @@ def register_user_with_piv_cac(email = 'test@test.com') ) set_up_2fa_with_piv_cac + skip_second_mfa_prompt end def set_up_2fa_with_piv_cac @@ -587,6 +591,10 @@ def set_up_2fa_with_piv_cac ) end + def skip_second_mfa_prompt + click_on t('mfa.skip') + end + def sign_in_via_branded_page(user) fill_in_credentials_and_submit(user.confirmed_email_addresses.first.email, user.password) fill_in_code_with_last_phone_otp diff --git a/spec/support/shared_examples/account_creation.rb b/spec/support/shared_examples/account_creation.rb index 572bd1f96da..b1eadb39f88 100644 --- a/spec/support/shared_examples/account_creation.rb +++ b/spec/support/shared_examples/account_creation.rb @@ -9,7 +9,6 @@ to(include('form-action \'self\' http://localhost:7654')) end - click_submit_default if sp == :saml click_agree_and_continue if :sp == :saml expect(current_url).to eq UriService.add_params(@saml_authn_request, locale: :es) @@ -76,7 +75,6 @@ expect(page.response_headers['Content-Security-Policy']). to(include('form-action \'self\' http://localhost:7654')) end - click_submit_default if sp == :saml click_agree_and_continue expect(current_url).to eq complete_saml_url if sp == :saml @@ -97,6 +95,7 @@ select_2fa_option('webauthn', visible: :all) fill_in_nickname_and_click_continue mock_press_button_on_hardware_key_on_setup + skip_second_mfa_prompt expect(page).to have_current_path(idv_doc_auth_step_path(step: :welcome)) complete_all_doc_auth_steps fill_out_phone_form_ok @@ -130,7 +129,6 @@ perform_in_browser(:two) do confirm_email_in_a_different_browser(first_email) - click_submit_default if sp == :saml click_agree_and_continue continue_as(first_email) @@ -152,7 +150,6 @@ perform_in_browser(:two) do Capybara.reset_session! confirm_email_in_a_different_browser(second_email) - click_submit_default if sp == :saml click_agree_and_continue continue_as(second_email) diff --git a/spec/views/accounts/two_factor_authentication/show.html.erb_spec.rb b/spec/views/accounts/two_factor_authentication/show.html.erb_spec.rb index 7024b083bc3..2509b4be2d8 100644 --- a/spec/views/accounts/two_factor_authentication/show.html.erb_spec.rb +++ b/spec/views/accounts/two_factor_authentication/show.html.erb_spec.rb @@ -81,10 +81,10 @@ end end - context 'when multiple mfa is enabled' do + context 'when kantara phone restriction is enabled' do let(:user) { create(:user, :with_phone, :with_authentication_app) } before do - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return true + allow(IdentityConfig.store).to receive(:kantara_2fa_phone_restricted).and_return(true) assign( :presenter, AccountShowPresenter.new( diff --git a/spec/views/mfa_confirmation/show.html.erb_spec.rb b/spec/views/mfa_confirmation/show.html.erb_spec.rb index bc022db4463..25db24e1c15 100644 --- a/spec/views/mfa_confirmation/show.html.erb_spec.rb +++ b/spec/views/mfa_confirmation/show.html.erb_spec.rb @@ -5,7 +5,6 @@ let(:decorated_user) { user.decorate } before do - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return(true) allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:enforce_second_mfa?).and_return(true) @content = MfaConfirmationPresenter.new(user) diff --git a/spec/views/sign_up/completions/show.html.erb_spec.rb b/spec/views/sign_up/completions/show.html.erb_spec.rb index f37eb8ed9f4..ad88cb9412f 100644 --- a/spec/views/sign_up/completions/show.html.erb_spec.rb +++ b/spec/views/sign_up/completions/show.html.erb_spec.rb @@ -95,55 +95,26 @@ describe 'MFA CTA banner' do let(:multiple_factors_enabled) { nil } - let(:select_multiple_mfa_options) { nil } before do - allow(IdentityConfig.store).to receive(:select_multiple_mfa_options). - and_return(select_multiple_mfa_options) @multiple_factors_enabled = multiple_factors_enabled end context 'with multiple factors disabled' do let(:multiple_factors_enabled) { false } - context 'with multiple MFA feature flag disabled' do - let(:select_multiple_mfa_options) { false } - - it 'does not show a banner' do - render - expect(rendered).not_to have_content(t('mfa.second_method_warning.text')) - end - end - - context 'with multiple MFA feature flag enabled' do - let(:select_multiple_mfa_options) { true } - - it 'shows a banner if the user selects one MFA option' do - render - expect(rendered).to have_content(t('mfa.second_method_warning.text')) - end + it 'shows a banner if the user selects one MFA option' do + render + expect(rendered).to have_content(t('mfa.second_method_warning.text')) end end context 'with multiple factors enabled' do let(:multiple_factors_enabled) { true } - context 'with multiple MFA feature flag disabled' do - let(:select_multiple_mfa_options) { false } - - it 'does not show a banner' do - render - expect(rendered).not_to have_content(t('mfa.second_method_warning.text')) - end - end - - context 'with multiple MFA feature flag enabled' do - let(:select_multiple_mfa_options) { true } - - it 'does not show a banner' do - render - expect(rendered).not_to have_content(t('mfa.second_method_warning.text')) - end + it 'does not show a banner' do + render + expect(rendered).not_to have_content(t('mfa.second_method_warning.text')) end end end diff --git a/spec/views/users/two_factor_authentication_setup/index.html.erb_spec.rb b/spec/views/users/two_factor_authentication_setup/index.html.erb_spec.rb index 74c3d19477d..524f13846d6 100644 --- a/spec/views/users/two_factor_authentication_setup/index.html.erb_spec.rb +++ b/spec/views/users/two_factor_authentication_setup/index.html.erb_spec.rb @@ -7,7 +7,7 @@ before do user = build_stubbed(:user) - @presenter = TwoFactorOptionsPresenter.new(user_agent: '') + @presenter = TwoFactorOptionsPresenter.new(user_agent: '', user: user) @two_factor_options_form = TwoFactorLoginOptionsForm.new(user) end @@ -23,7 +23,7 @@ it 'disables phone option' do expect(rendered).to have_field( - 'two_factor_options_form[selection]', + 'two_factor_options_form[selection][]', with: :phone, disabled: true, ) @@ -42,7 +42,7 @@ it 'does not disable phone option' do expect(rendered).to have_field( - 'two_factor_options_form[selection]', + 'two_factor_options_form[selection][]', with: :phone, disabled: false, ) From 0075355aa292c2a20f375812b6a968e962d2a554 Mon Sep 17 00:00:00 2001 From: Jess Date: Tue, 20 Dec 2022 11:06:50 -0800 Subject: [PATCH 10/20] LG-8373: Update PO Search "About" text to pilot copy (#7501) changelog: User-Facing Improvements, In-person proofing, Update PO Search About text to reflect additional POs coming available soon and CTA Prompt Detail to include comma before D.C. (all languages) --- config/locales/in_person_proofing/en.yml | 11 +++++++---- config/locales/in_person_proofing/es.yml | 9 +++++---- config/locales/in_person_proofing/fr.yml | 10 ++++++---- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/config/locales/in_person_proofing/en.yml b/config/locales/in_person_proofing/en.yml index 84669dfcaa8..bf75a5be6ba 100644 --- a/config/locales/in_person_proofing/en.yml +++ b/config/locales/in_person_proofing/en.yml @@ -23,8 +23,9 @@ en: cta: button: Verify your ID in person new_feature: New Feature - prompt_detail: If you are located near Washington D.C. or Baltimore, MD, you may - be able to verify your ID in person at limited post office locations. + prompt_detail: If you are located near Washington, D.C. or Baltimore, MD, you + may be able to verify your ID in person at limited post office + locations. location: inline_error: Include a city, state, and ZIP code location_button: Select @@ -39,8 +40,10 @@ en: %{address}. none_found_tip: You can search using a different address, or add photos of your ID to try and verify your identity online again. - po_search_about: If you are having trouble adding your ID, you may be able to - verify your identity in person at a local United States Post Office. + po_search_about: If you are located near Washington, D.C. or Baltimore, MD, you + may be able to verify your ID in person at limited Post Office + locations. More locations across all 50 states and some US + territories will be coming soon. results_description: There are %{count} participating Post Offices within 50 miles of %{address}. results_instructions: Select a Post Office location below, or search again using diff --git a/config/locales/in_person_proofing/es.yml b/config/locales/in_person_proofing/es.yml index 77c0eab40a0..557314541d1 100644 --- a/config/locales/in_person_proofing/es.yml +++ b/config/locales/in_person_proofing/es.yml @@ -26,7 +26,7 @@ es: cta: button: Verifique su identificación en persona new_feature: Nuevo artículo - prompt_detail: Si usted está ubicado cerca de Washington D.C. o Baltimore, MD, + prompt_detail: Si usted está ubicado cerca de Washington, D.C. o Baltimore, MD, puede verificar su identificación en persona en las oficinas de correos seleccionadas. location: @@ -45,9 +45,10 @@ es: none_found_tip: Puede buscar utilizando una dirección diferente, o añadir fotos de su documento de identidad para intentar verificar su identidad en línea de nuevo. - po_search_about: Si tiene problemas para añadir su documento de identidad, es - posible que pueda verificar su identidad en persona en una oficina - de correos local de los Estados Unidos. + po_search_about: Si vives cerca de Washington, D.C. o Baltimore, MD, puedes + verificar tu ID directamente en algunas oficinas de correos. Pronto + habrá más oficinas en los 50 estados y en algunos otros territorios + de los Estados Unidos. results_description: Hay %{count} de oficinas de correos participantes en un radio de 50 millas de la %{address}. results_instructions: Seleccione una ubicación de la Oficina de Correos a diff --git a/config/locales/in_person_proofing/fr.yml b/config/locales/in_person_proofing/fr.yml index 9bf296f3d88..3a2aaed8aaa 100644 --- a/config/locales/in_person_proofing/fr.yml +++ b/config/locales/in_person_proofing/fr.yml @@ -27,7 +27,7 @@ fr: cta: button: Vérifiez votre identité en personne new_feature: Nouvelle fonctionnalité - prompt_detail: Si vous êtes situé près de Washington D.C. ou de Baltimore + prompt_detail: Si vous êtes situé près de Washington, D.C. ou de Baltimore (Maryland), vous pourrez peut-être vérifier votre identité en personne dans certains bureaux de poste. location: @@ -44,9 +44,11 @@ fr: none_found_tip: Vous pouvez effectuer une recherche en utilisant une autre adresse, ou ajouter des photos de votre pièce d’identité pour essayer de vérifier à nouveau votre identité en ligne. - po_search_about: Si vous avez des difficultés à ajouter votre pièce d’identité, - vous pouvez vérifier votre identité en personne dans un bureau de - poste américain proche. + po_search_about: Si vous êtes situé près de Washington, D.C. ou de Baltimore, + MD, vous pourrez peut-être vérifier votre pièce d’identité en + personne dans certains bureaux de poste. D’autres bureaux dans les + 50 États et certains territoires américains seront bientôt + disponibles. results_description: Il y a %{count} de bureaux de poste participants dans un rayon de 80 km autour de %{address}. results_instructions: Sélectionnez un emplacement de bureau de poste ci-dessous, From de3d7d57ab103e35f925a99489f026c8cfa12b41 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 20 Dec 2022 14:32:09 -0500 Subject: [PATCH 11/20] Initialize JavaScript tests with polyfilled fetch (#7518) changelog: Internal, Automated Tests, Improve JavaScript tests simulating network requests --- .../document-capture/components/address-search.spec.tsx | 3 --- app/javascript/packs/form-steps-wait.tsx | 4 +++- spec/javascripts/packs/form-steps-wait-spec.js | 4 ++++ spec/javascripts/spec_helper.js | 4 ++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/javascript/packages/document-capture/components/address-search.spec.tsx b/app/javascript/packages/document-capture/components/address-search.spec.tsx index c9118ba6a61..7905b4f7f4b 100644 --- a/app/javascript/packages/document-capture/components/address-search.spec.tsx +++ b/app/javascript/packages/document-capture/components/address-search.spec.tsx @@ -2,7 +2,6 @@ import { render } from '@testing-library/react'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; import type { SetupServerApi } from 'msw/node'; -import { fetch } from 'whatwg-fetch'; import { useSandbox } from '@18f/identity-test-helpers'; import userEvent from '@testing-library/user-event'; import AddressSearch, { ADDRESS_SEARCH_URL } from './address-search'; @@ -26,7 +25,6 @@ describe('AddressSearch', () => { let server: SetupServerApi; before(() => { - global.window.fetch = fetch; server = setupServer( rest.post(ADDRESS_SEARCH_URL, (_req, res, ctx) => res(ctx.json(DEFAULT_RESPONSE))), ); @@ -35,7 +33,6 @@ describe('AddressSearch', () => { after(() => { server.close(); - global.window.fetch = () => Promise.reject(new Error('Fetch must be stubbed')); }); it('fires the callback with correct input', async () => { diff --git a/app/javascript/packs/form-steps-wait.tsx b/app/javascript/packs/form-steps-wait.tsx index 4fac77fbca6..2d06e7b5ebe 100644 --- a/app/javascript/packs/form-steps-wait.tsx +++ b/app/javascript/packs/form-steps-wait.tsx @@ -5,6 +5,8 @@ interface FormStepsWaitElements { form: HTMLFormElement; } +type FetchOrFetchPolyfill = typeof window.fetch & { polyfill?: boolean }; + interface FormStepsWaitOptions { /** * Poll interval. @@ -118,7 +120,7 @@ export class FormStepsWait { body: new window.FormData(form), }); - if ('polyfill' in window.fetch) { + if ((window.fetch as FetchOrFetchPolyfill).polyfill) { // The fetch polyfill is implemented using XMLHttpRequest, which suffers from an issue where a // Content-Type header from a POST is carried into a redirected GET, which is exactly the flow // we are handling here. The current version of Rack neither handles nor provides easy insight diff --git a/spec/javascripts/packs/form-steps-wait-spec.js b/spec/javascripts/packs/form-steps-wait-spec.js index 206491a47e8..abf9a28c0a1 100644 --- a/spec/javascripts/packs/form-steps-wait-spec.js +++ b/spec/javascripts/packs/form-steps-wait-spec.js @@ -95,6 +95,10 @@ describe('FormStepsWait', () => { return form; } + beforeEach(() => { + sandbox.stub(window.fetch, 'polyfill').value(undefined); + }); + it('submits form via fetch', () => { const action = new URL('/', window.location).toString(); const method = 'post'; diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js index 3ae9c6d2946..204032e0b4f 100644 --- a/spec/javascripts/spec_helper.js +++ b/spec/javascripts/spec_helper.js @@ -3,7 +3,7 @@ import chai from 'chai'; import dirtyChai from 'dirty-chai'; import sinonChai from 'sinon-chai'; import chaiAsPromised from 'chai-as-promised'; -import { Response } from 'whatwg-fetch'; +import { fetch, Response } from 'whatwg-fetch'; // Remove in favor of native fetch in Node v20+ (https://nodejs.org/docs/latest/api/globals.html#fetch) import { createDOM, useCleanDOM } from './support/dom'; import { chaiConsoleSpy, useConsoleLogSpy } from './support/console'; import { sinonChaiAsPromised } from './support/sinon'; @@ -26,7 +26,7 @@ const windowGlobals = Object.fromEntries( .map((key) => [key, window[key]]), ); Object.assign(global, windowGlobals); -global.window.fetch = () => Promise.reject(new Error('Fetch must be stubbed')); +global.window.fetch = fetch; Object.defineProperty(global.window, 'crypto', { value: webcrypto }); global.window.URL.createObjectURL = createObjectURLAsDataURL; global.window.URL.revokeObjectURL = () => {}; From a7577b6de1355ae6f00f8e71ba2fc14e1db42c62 Mon Sep 17 00:00:00 2001 From: Julia Allen <51330839+julialeague@users.noreply.github.com> Date: Tue, 20 Dec 2022 11:58:09 -0800 Subject: [PATCH 12/20] LG-8432: SAML - Update ForceAuthn check (#7514) * only ForceAuthn if not at final post endpoint * changelog: Bug Fixes, SAML Auth, Update ForceAuthn enforcement * Fixing before_action syntax per rubocop, commenting out spec temporarily --- .../concerns/saml_idp_auth_concern.rb | 6 +- app/controllers/saml_idp_controller.rb | 5 +- spec/controllers/saml_idp_controller_spec.rb | 56 ++++++++++++++----- spec/support/saml_auth_helper.rb | 6 ++ 4 files changed, 57 insertions(+), 16 deletions(-) diff --git a/app/controllers/concerns/saml_idp_auth_concern.rb b/app/controllers/concerns/saml_idp_auth_concern.rb index 5acf7eb9b33..7059bb19896 100644 --- a/app/controllers/concerns/saml_idp_auth_concern.rb +++ b/app/controllers/concerns/saml_idp_auth_concern.rb @@ -20,7 +20,11 @@ module SamlIdpAuthConcern def sign_out_if_forceauthn_is_true_and_user_is_signed_in return unless user_signed_in? && saml_request.force_authn? - sign_out unless sp_session[:request_url] == request.original_url + if IdentityConfig.store.saml_internal_post + sign_out unless request.path.start_with?('/api/saml/finalauthpost') + else + sign_out unless sp_session[:request_url] == request.original_url + end end def check_sp_active diff --git a/app/controllers/saml_idp_controller.rb b/app/controllers/saml_idp_controller.rb index c7650735abb..440ac7c9cd8 100644 --- a/app/controllers/saml_idp_controller.rb +++ b/app/controllers/saml_idp_controller.rb @@ -3,6 +3,10 @@ require 'uuid' class SamlIdpController < ApplicationController + # This needs to precede sign_out_if_forceauthn_is_true_and_user_is_signed_in + # which is added when SamlIdpAuthConcern is included + skip_before_action :verify_authenticity_token, only: [:logout, :remotelogout] + include SamlIdp::Controller include SamlIdpAuthConcern include SamlIdpLogoutConcern @@ -17,7 +21,6 @@ class SamlIdpController < ApplicationController prepend_before_action :skip_session_load, only: [:metadata, :remotelogout] prepend_before_action :skip_session_expiration, only: [:metadata, :remotelogout] - skip_before_action :verify_authenticity_token before_action :log_external_saml_auth_request, only: [:auth] before_action :handle_banned_user before_action :confirm_user_is_authenticated_with_fresh_mfa, only: :auth diff --git a/spec/controllers/saml_idp_controller_spec.rb b/spec/controllers/saml_idp_controller_spec.rb index 57dd0fbcf3c..da36cd4c403 100644 --- a/spec/controllers/saml_idp_controller_spec.rb +++ b/spec/controllers/saml_idp_controller_spec.rb @@ -873,17 +873,43 @@ def name_id_version(format_urn) end end - context 'ForceAuthn set to true' do - it 'signs the user out if a session is active' do + context 'saml_internal_post feature configuration is set to true with ForceAuthn' do + let(:user) { create(:user, :signed_up) } + + it 'signs the user out if a session is active and request path is not "finalauthpost"' do user = create(:user, :signed_up) sign_in(user) generate_saml_response(user, saml_settings(overrides: { force_authn: true })) - # would be 200 if the user's session persists expect(response.status).to eq(302) # implicit test of request storage since request_id would be missing otherwise expect(response.location).to match(%r{#{root_url}\?request_id=.+}) end + + it 'skips signing out the user when request is from "finalauthpost"' do + link_user_to_identity(user, true, saml_settings(overrides: { force_authn: true })) + sign_in(user) + saml_final_post_auth(saml_request(saml_settings(overrides: { force_authn: true }))) + expect(response).to_not be_redirect + expect(response.status).to eq(200) + end + end + + context 'saml_internal_post feature configuration is set to false' do + let(:user) { create(:user, :signed_up) } + + before { allow(IdentityConfig.store).to receive(:saml_internal_post).and_return(false) } + + context 'ForceAuthn set to true' do + it 'signs the user out if a session is active' do + sign_in(user) + generate_saml_response(user, saml_settings(overrides: { force_authn: true })) + # would be 200 if the user's session persists + expect(response.status).to eq(302) + # implicit test of request storage since request_id would be missing otherwise + expect(response.location).to match(%r{#{root_url}\?request_id=.+}) + end + end end context 'service provider is inactive' do @@ -2052,17 +2078,19 @@ def stub_requested_attributes end end - describe 'before_actions' do - it 'includes the appropriate before_actions' do - expect(subject).to have_actions( - :before, - :disable_caching, - :validate_saml_request, - :validate_service_provider_and_authn_context, - :store_saml_request, - ) - end - end + # temporarily commenting out this spec because it needs to be updated to work + # describe 'before_actions' do + # it 'includes the appropriate before_actions' do + # expect(subject).to have_actions( + # :before, + # :disable_caching, + # :validate_saml_request, + # :validate_service_provider_and_authn_context, + # :store_saml_request, + # [:verify_authenticity_token, only: [:logout, :remotelogout]] + # ) + # end + # end describe '#external_saml_request' do it 'returns false for malformed referer' do diff --git a/spec/support/saml_auth_helper.rb b/spec/support/saml_auth_helper.rb index 61e56c2dcbd..0973f2a1993 100644 --- a/spec/support/saml_auth_helper.rb +++ b/spec/support/saml_auth_helper.rb @@ -108,6 +108,12 @@ def saml_get_auth(settings) def saml_post_auth(saml_request) # POST redirect binding Authn Request + request.path = '/api/saml/authpost2022' + post :auth, params: { SAMLRequest: CGI.unescape(saml_request) } + end + + def saml_final_post_auth(saml_request) + request.path = '/api/saml/finalauthpost2022' post :auth, params: { SAMLRequest: CGI.unescape(saml_request) } end From 76eae0b1c4298cfa002dfc5d96b836647614e66a Mon Sep 17 00:00:00 2001 From: Malick Diarra Date: Tue, 20 Dec 2022 16:12:15 -0500 Subject: [PATCH 13/20] LG-8089: Update to support notifying user of OTP format in message (#7486) * Improvements, Voice messaging, Notify voice OTP users how long and what type of code to expect (LG-8089) * update voice * translation update * add check to otp_sender * changelog: Improvements, Voice messaging, Notify voice OTP users how long and what type of code to expect * fix spec --- .../two_factor_authentication_controller.rb | 1 + .../idv/send_phone_confirmation_otp.rb | 1 + config/locales/telephony/en.yml | 15 ++- config/locales/telephony/es.yml | 15 ++- config/locales/telephony/fr.yml | 16 ++- lib/telephony.rb | 10 +- lib/telephony/otp_sender.rb | 8 +- .../test/telephony_controller_spec.rb | 4 + ...o_factor_authentication_controller_spec.rb | 3 + .../change_factor_spec.rb | 1 + spec/lib/telephony/otp_sender_spec.rb | 107 ++++++++++++------ .../idv/send_phone_confirmation_otp_spec.rb | 2 + 12 files changed, 122 insertions(+), 61 deletions(-) diff --git a/app/controllers/users/two_factor_authentication_controller.rb b/app/controllers/users/two_factor_authentication_controller.rb index 8ea1c9d69ae..cc9a876b11f 100644 --- a/app/controllers/users/two_factor_authentication_controller.rb +++ b/app/controllers/users/two_factor_authentication_controller.rb @@ -289,6 +289,7 @@ def send_user_otp(method) to: phone_to_deliver_to, otp: current_user.direct_otp, expiration: TwoFactorAuthenticatable::DIRECT_OTP_VALID_FOR_MINUTES, + otp_format: t('telephony.format_type.digit'), channel: method.to_sym, domain: IdentityConfig.store.domain_name, country_code: parsed_phone.country, diff --git a/app/services/idv/send_phone_confirmation_otp.rb b/app/services/idv/send_phone_confirmation_otp.rb index af338052bb2..94226916c7f 100644 --- a/app/services/idv/send_phone_confirmation_otp.rb +++ b/app/services/idv/send_phone_confirmation_otp.rb @@ -57,6 +57,7 @@ def send_otp otp: code, to: phone, expiration: TwoFactorAuthenticatable::DIRECT_OTP_VALID_FOR_MINUTES, + otp_format: I18n.t('telephony.format_type.character'), channel: delivery_method, domain: IdentityConfig.store.domain_name, country_code: parsed_phone.country, diff --git a/config/locales/telephony/en.yml b/config/locales/telephony/en.yml index dbdd9ab9d67..0e1158a9529 100644 --- a/config/locales/telephony/en.yml +++ b/config/locales/telephony/en.yml @@ -10,17 +10,17 @@ en: %{app_name}: Your one-time code is %{code}. It expires in %{expiration} minutes. Don't share this code with anyone. @%{domain} #%{code} - voice: Hello! Your %{app_name} one-time code is, %{code}. Your one-time code is, - %{code}. Again, your one-time code is %{code}. This code expires in - %{expiration} minutes. + voice: Hello! Your 6-%{format_type} %{app_name} one-time code is, %{code}. Your + one-time code is, %{code}. Again, your one-time code is %{code}. This + code expires in %{expiration} minutes. confirmation_otp: sms: |- %{app_name}: Your one-time code is %{code}. It expires in %{expiration} minutes. Don't share this code with anyone. @%{domain} #%{code} - voice: Hello! Your %{app_name} one-time code is, %{code}. Your one-time code is, - %{code}. Again, your one-time code is %{code}. This code expires in - %{expiration} minutes. + voice: Hello! Your 6-%{format_type} %{app_name} one-time code is, %{code}. Your + one-time code is, %{code}. Again, your one-time code is %{code}. This + code expires in %{expiration} minutes. doc_auth_link: |- %{app_name}: %{link} You're verifying your identity to access %{sp_or_app_name}. Take a photo of your ID to continue. error: @@ -45,6 +45,9 @@ en: unknown_failure: We are experiencing technical difficulties. Please try again later. voice_unsupported: Invalid phone number. Check that you’ve entered the correct country code or area code. + format_type: + character: character + digit: digit personal_key_regeneration_notice: A new personal key has been issued for your %{app_name} account. If this wasn't you, reset your password. personal_key_sign_in_notice: Your personal key was just used to sign into your diff --git a/config/locales/telephony/es.yml b/config/locales/telephony/es.yml index 36fa8087bd5..45ebac66770 100644 --- a/config/locales/telephony/es.yml +++ b/config/locales/telephony/es.yml @@ -10,17 +10,17 @@ es: %{app_name}: El código de un solo uso es %{code}. Este código se vence en %{expiration} minutos. No lo comparta con ninguna persona. @%{domain} #%{code} - voice: ¡Hola! Su código único de %{app_name} es %{code}, Su código único es - %{code}, De nuevo, su código único es %{code}, Este código expira en - %{expiration} minutos. + voice: ¡Hola! Su código único de seis %{format_type} de %{app_name} es %{code}, + Su código único es %{code}, De nuevo, Su código único de es %{code}, + Este código expira en %{expiration} minutos. confirmation_otp: sms: |- %{app_name}: El código de un solo uso es %{code}. Este código se vence en %{expiration} minutos. No lo comparta con ninguna persona. @%{domain} #%{code} - voice: ¡Hola! Su código único de %{app_name} es %{code}, Su código único es - %{code}, De nuevo, su código único es %{code}, Este código expira en - %{expiration} minutos. + voice: ¡Hola! Su código único de seis %{format_type} de %{app_name} es %{code}, + Su código único es %{code}, De nuevo, Su código único de es %{code}, + Este código expira en %{expiration} minutos. doc_auth_link: |- %{app_name}: %{link} Está verificando su identidad para acceder a %{sp_or_app_name}. Tome una foto de su identificación para continuar. error: @@ -48,6 +48,9 @@ es: inténtelo de nuevo más tarde. voice_unsupported: Numero de telefono invalido. Verifique que haya ingresado el código de país o de área correcto. + format_type: + character: carácter + digit: dígitos personal_key_regeneration_notice: Se ha emitido una nueva clave personal para tu cuenta %{app_name}. Si no eres tú, restablece tu contraseña. personal_key_sign_in_notice: Su clave personal solo se utilizó para iniciar diff --git a/config/locales/telephony/fr.yml b/config/locales/telephony/fr.yml index 76b55235db3..ba28f66b95c 100644 --- a/config/locales/telephony/fr.yml +++ b/config/locales/telephony/fr.yml @@ -10,17 +10,18 @@ fr: %{app_name}: Votre code à usage unique est %{code}. Il est valable pendant %{expiration} minutes. Vous ne devez pas partager ce code avec personne. @%{domain} #%{code} - voice: Bonjour! Votre code %{app_name} à usage unique est %{code}, Votre code à - usage unique est %{code}, Une fois de plus, votre code à usage unique - est %{code}, Ce code expire dans %{expiration} minutes. + voice: Bonjour! Votre code à usage unique de six %{format_type} pour %{app_name} + est %{code}. Votre code à usage unique est %{code}. Une fois de plus, + votre code à usage unique est %{code}. Ce code expire dans %{expiration} + minutes. confirmation_otp: sms: |- %{app_name}: Votre code à usage unique est %{code}. Il est valable pendant %{expiration} minutes. Vous ne devez pas partager ce code avec personne. @%{domain} #%{code} - voice: Bonjour! Votre code %{app_name} à usage unique est %{code}. Votre code à - usage unique est %{code}. Une fois de plus, votre code à usage unique - est %{code}. Ce code expire dans %{expiration} minutes. + voice: Bonjour! Votre code à usage unique de six %{format_type} pour %{app_name} + est %{code}. Votre code à usage unique est %{code}. Une fois de plus, + votre code à usage unique est %{code}. Ce code expire dans %{expiration} doc_auth_link: |- %{app_name}: %{link} Vous vérifiez votre identité pour accéder à %{sp_or_app_name}. Prenez une photo de votre pièce d'identité pour continuer. error: @@ -48,6 +49,9 @@ fr: plus tard. voice_unsupported: Numéro de téléphone invalide. Vérifiez que vous avez entré le bon indicatif international ou régional. + format_type: + character: caractère + digit: chiffres personal_key_regeneration_notice: Une nouvelle clé personnelle a été émise pour votre compte %{app_name}. Si vous ne l'avez pas demandée, réinitialisez votre mot de passe. diff --git a/lib/telephony.rb b/lib/telephony.rb index 44a8c4fdbfc..14bdc4fc1bc 100644 --- a/lib/telephony.rb +++ b/lib/telephony.rb @@ -43,12 +43,13 @@ def self.config @config end - def self.send_authentication_otp(to:, otp:, expiration:, channel:, domain:, country_code:, - extra_metadata:) + def self.send_authentication_otp(to:, otp:, expiration:, otp_format:, + channel:, domain:, country_code:, extra_metadata:) OtpSender.new( to: to, otp: otp, expiration: expiration, + otp_format: otp_format, channel: channel, domain: domain, country_code: country_code, @@ -56,12 +57,13 @@ def self.send_authentication_otp(to:, otp:, expiration:, channel:, domain:, coun ).send_authentication_otp end - def self.send_confirmation_otp(to:, otp:, expiration:, channel:, domain:, country_code:, - extra_metadata:) + def self.send_confirmation_otp(to:, otp:, expiration:, otp_format:, + channel:, domain:, country_code:, extra_metadata:) OtpSender.new( to: to, otp: otp, expiration: expiration, + otp_format: otp_format, channel: channel, domain: domain, country_code: country_code, diff --git a/lib/telephony/otp_sender.rb b/lib/telephony/otp_sender.rb index 9a8e0525da8..23625be7368 100644 --- a/lib/telephony/otp_sender.rb +++ b/lib/telephony/otp_sender.rb @@ -1,11 +1,13 @@ module Telephony class OtpSender - attr_reader :recipient_phone, :otp, :expiration, :channel, :domain, :country_code, + attr_reader :recipient_phone, :otp, :expiration, :otp_format, :channel, :domain, :country_code, :extra_metadata - def initialize(to:, otp:, expiration:, channel:, domain:, country_code:, extra_metadata:) + def initialize(to:, otp:, expiration:, otp_format:, + channel:, domain:, country_code:, extra_metadata:) @recipient_phone = to @otp = otp + @otp_format = otp_format @expiration = expiration @channel = channel.to_sym @domain = domain @@ -41,6 +43,7 @@ def authentication_message "telephony.authentication_otp.#{channel}", app_name: APP_NAME, code: otp_transformed_for_channel, + format_type: otp_format, expiration: expiration, domain: domain, ), @@ -53,6 +56,7 @@ def confirmation_message "telephony.confirmation_otp.#{channel}", app_name: APP_NAME, code: otp_transformed_for_channel, + format_type: otp_format, expiration: expiration, domain: domain, ), diff --git a/spec/controllers/test/telephony_controller_spec.rb b/spec/controllers/test/telephony_controller_spec.rb index 548599b3e29..f917f0e26d7 100644 --- a/spec/controllers/test/telephony_controller_spec.rb +++ b/spec/controllers/test/telephony_controller_spec.rb @@ -8,6 +8,7 @@ otp: '123456', expiration: 10, channel: :sms, + otp_format: 'digit', domain: IdentityConfig.store.domain_name, country_code: 'US', extra_metadata: {}, @@ -17,6 +18,7 @@ otp: '654321', expiration: 10, channel: :voice, + otp_format: 'digit', domain: IdentityConfig.store.domain_name, country_code: 'US', extra_metadata: {}, @@ -47,6 +49,7 @@ otp: '123456', expiration: 10, channel: :sms, + otp_format: 'digit', domain: IdentityConfig.store.domain_name, country_code: 'US', extra_metadata: {}, @@ -56,6 +59,7 @@ otp: '654321', expiration: 10, channel: :voice, + otp_format: 'digit', domain: IdentityConfig.store.domain_name, country_code: 'US', extra_metadata: {}, diff --git a/spec/controllers/users/two_factor_authentication_controller_spec.rb b/spec/controllers/users/two_factor_authentication_controller_spec.rb index da374826bae..2ccb824cab4 100644 --- a/spec/controllers/users/two_factor_authentication_controller_spec.rb +++ b/spec/controllers/users/two_factor_authentication_controller_spec.rb @@ -298,6 +298,7 @@ def index to: phone, expiration: 10, channel: :sms, + otp_format: 'digit', domain: IdentityConfig.store.domain_name, country_code: 'US', extra_metadata: { @@ -463,6 +464,7 @@ def index to: phone, expiration: 10, channel: :voice, + otp_format: 'digit', domain: IdentityConfig.store.domain_name, country_code: 'US', extra_metadata: { @@ -552,6 +554,7 @@ def index to: @unconfirmed_phone, expiration: 10, channel: :sms, + otp_format: 'digit', domain: IdentityConfig.store.domain_name, country_code: 'US', extra_metadata: { diff --git a/spec/features/two_factor_authentication/change_factor_spec.rb b/spec/features/two_factor_authentication/change_factor_spec.rb index 279d5ecf0aa..ae431785e26 100644 --- a/spec/features/two_factor_authentication/change_factor_spec.rb +++ b/spec/features/two_factor_authentication/change_factor_spec.rb @@ -39,6 +39,7 @@ to: old_phone, expiration: 10, channel: :sms, + otp_format: 'digit', domain: IdentityConfig.store.domain_name, country_code: 'US', extra_metadata: { diff --git a/spec/lib/telephony/otp_sender_spec.rb b/spec/lib/telephony/otp_sender_spec.rb index abae33e5c20..466a988c4fc 100644 --- a/spec/lib/telephony/otp_sender_spec.rb +++ b/spec/lib/telephony/otp_sender_spec.rb @@ -20,6 +20,7 @@ to: to, otp: otp, expiration: expiration, + otp_format: otp_format, channel: channel, domain: domain, country_code: country_code, @@ -30,6 +31,7 @@ let(:to) { '+1 (202) 262-1234' } let(:otp) { '123456' } let(:expiration) { 5 } + let(:otp_format) { I18n.t('telephony.format_type.digit') } let(:domain) { 'login.gov' } let(:country_code) { 'US' } @@ -94,6 +96,7 @@ to: to, otp: otp, expiration: expiration, + otp_format: otp_format, channel: channel, domain: domain, country_code: country_code, @@ -104,6 +107,7 @@ let(:to) { '+1 (202) 262-1234' } let(:otp) { '123456' } let(:expiration) { 5 } + let(:otp_format) { I18n.t('telephony.format_type.digit') } let(:domain) { 'login.gov' } let(:country_code) { 'US' } @@ -159,46 +163,71 @@ context 'for voice' do let(:channel) { :voice } + context 'for signin/signup' do + it 'sends an authentication OTP with Pinpoint Voice' do + message = <<~XML.squish + + + Hello! Your 6-digit #{APP_NAME} one-time code is, + 1 2 3 + 4 5 6. + XML + + adapter = instance_double(Telephony::Pinpoint::VoiceSender) + expect(adapter).to receive(:send).with( + message: start_with(message), + to: to, + otp: otp, + country_code: country_code, + ) + expect(Telephony::Pinpoint::VoiceSender).to receive(:new).and_return(adapter) + + subject.send_confirmation_otp + end - it 'sends an authentication OTP with Pinpoint Voice' do - message = <<~XML.squish - - - Hello! Your #{APP_NAME} one-time code is, - 1 2 3 - 4 5 6. - XML - - adapter = instance_double(Telephony::Pinpoint::VoiceSender) - expect(adapter).to receive(:send).with( - message: start_with(message), - to: to, - otp: otp, - country_code: country_code, - ) - expect(Telephony::Pinpoint::VoiceSender).to receive(:new).and_return(adapter) - - subject.send_confirmation_otp + it 'sends a confirmation OTP with Pinpoint Voice' do + message = <<~XML.squish + + + Hello! Your 6-digit #{APP_NAME} one-time code is, + 1 2 3 + 4 5 6. + XML + + adapter = instance_double(Telephony::Pinpoint::VoiceSender) + expect(adapter).to receive(:send).with( + message: start_with(message), + to: to, + otp: otp, + country_code: country_code, + ) + expect(Telephony::Pinpoint::VoiceSender).to receive(:new).and_return(adapter) + subject.send_confirmation_otp + end end - it 'sends a confirmation OTP with Pinpoint Voice' do - message = <<~XML.squish - - - Hello! Your #{APP_NAME} one-time code is, - 1 2 3 - 4 5 6. - XML - - adapter = instance_double(Telephony::Pinpoint::VoiceSender) - expect(adapter).to receive(:send).with( - message: start_with(message), - to: to, - otp: otp, - country_code: country_code, - ) - expect(Telephony::Pinpoint::VoiceSender).to receive(:new).and_return(adapter) - subject.send_confirmation_otp + context 'for Idv phone confirmation' do + let(:otp_format) { I18n.t('telephony.format_type.character') } + + it 'sends a confirmation OTP with Pinpoint Voice for Idv' do + message = <<~XML.squish + + + Hello! Your 6-character #{APP_NAME} one-time code is, + 1 2 3 + 4 5 6. + XML + + adapter = instance_double(Telephony::Pinpoint::VoiceSender) + expect(adapter).to receive(:send).with( + message: start_with(message), + to: to, + otp: otp, + country_code: country_code, + ) + expect(Telephony::Pinpoint::VoiceSender).to receive(:new).and_return(adapter) + subject.send_confirmation_otp + end end it 'sends valid XML' do @@ -224,6 +253,7 @@ otp: otp, channel: channel, expiration: Time.zone.now, + otp_format: I18n.t('telephony.format_type.digit'), domain: 'login.gov', country_code: country_code, extra_metadata: {}, @@ -279,6 +309,7 @@ otp: 'ABC123', channel: channel, expiration: TwoFactorAuthenticatable::DIRECT_OTP_VALID_FOR_MINUTES, + otp_format: I18n.t('telephony.format_type.digit'), domain: 'secure.login.gov', country_code: 'US', extra_metadata: {}, @@ -349,12 +380,14 @@ describe '#confirmation_message' do let(:channel) { nil } + let(:otp_format) { I18n.t('telephony.format_type.digit') } let(:sender) do Telephony::OtpSender.new( to: '+18888675309', otp: 'ABC123', channel: channel, expiration: TwoFactorAuthenticatable::DIRECT_OTP_VALID_FOR_MINUTES, + otp_format: otp_format, domain: 'secure.login.gov', country_code: 'US', extra_metadata: {}, diff --git a/spec/services/idv/send_phone_confirmation_otp_spec.rb b/spec/services/idv/send_phone_confirmation_otp_spec.rb index 01d8d363a5b..72aaa24e9ca 100644 --- a/spec/services/idv/send_phone_confirmation_otp_spec.rb +++ b/spec/services/idv/send_phone_confirmation_otp_spec.rb @@ -59,6 +59,7 @@ to: phone, expiration: 10, channel: :sms, + otp_format: 'character', domain: IdentityConfig.store.domain_name, country_code: 'US', extra_metadata: { @@ -91,6 +92,7 @@ to: phone, expiration: 10, channel: :voice, + otp_format: 'character', domain: IdentityConfig.store.domain_name, country_code: 'US', extra_metadata: { From ad58354ea4668ffca58f23c53de9789ec60a5e62 Mon Sep 17 00:00:00 2001 From: Oren Kanner Date: Tue, 20 Dec 2022 17:12:15 -0500 Subject: [PATCH 14/20] Revert "LG-8432: SAML - Update ForceAuthn check (#7514)" (#7520) This reverts commit a7577b6de1355ae6f00f8e71ba2fc14e1db42c62. [skip changelog] --- .../concerns/saml_idp_auth_concern.rb | 6 +- app/controllers/saml_idp_controller.rb | 5 +- spec/controllers/saml_idp_controller_spec.rb | 56 +++++-------------- spec/support/saml_auth_helper.rb | 6 -- 4 files changed, 16 insertions(+), 57 deletions(-) diff --git a/app/controllers/concerns/saml_idp_auth_concern.rb b/app/controllers/concerns/saml_idp_auth_concern.rb index 7059bb19896..5acf7eb9b33 100644 --- a/app/controllers/concerns/saml_idp_auth_concern.rb +++ b/app/controllers/concerns/saml_idp_auth_concern.rb @@ -20,11 +20,7 @@ module SamlIdpAuthConcern def sign_out_if_forceauthn_is_true_and_user_is_signed_in return unless user_signed_in? && saml_request.force_authn? - if IdentityConfig.store.saml_internal_post - sign_out unless request.path.start_with?('/api/saml/finalauthpost') - else - sign_out unless sp_session[:request_url] == request.original_url - end + sign_out unless sp_session[:request_url] == request.original_url end def check_sp_active diff --git a/app/controllers/saml_idp_controller.rb b/app/controllers/saml_idp_controller.rb index 440ac7c9cd8..c7650735abb 100644 --- a/app/controllers/saml_idp_controller.rb +++ b/app/controllers/saml_idp_controller.rb @@ -3,10 +3,6 @@ require 'uuid' class SamlIdpController < ApplicationController - # This needs to precede sign_out_if_forceauthn_is_true_and_user_is_signed_in - # which is added when SamlIdpAuthConcern is included - skip_before_action :verify_authenticity_token, only: [:logout, :remotelogout] - include SamlIdp::Controller include SamlIdpAuthConcern include SamlIdpLogoutConcern @@ -21,6 +17,7 @@ class SamlIdpController < ApplicationController prepend_before_action :skip_session_load, only: [:metadata, :remotelogout] prepend_before_action :skip_session_expiration, only: [:metadata, :remotelogout] + skip_before_action :verify_authenticity_token before_action :log_external_saml_auth_request, only: [:auth] before_action :handle_banned_user before_action :confirm_user_is_authenticated_with_fresh_mfa, only: :auth diff --git a/spec/controllers/saml_idp_controller_spec.rb b/spec/controllers/saml_idp_controller_spec.rb index da36cd4c403..57dd0fbcf3c 100644 --- a/spec/controllers/saml_idp_controller_spec.rb +++ b/spec/controllers/saml_idp_controller_spec.rb @@ -873,43 +873,17 @@ def name_id_version(format_urn) end end - context 'saml_internal_post feature configuration is set to true with ForceAuthn' do - let(:user) { create(:user, :signed_up) } - - it 'signs the user out if a session is active and request path is not "finalauthpost"' do + context 'ForceAuthn set to true' do + it 'signs the user out if a session is active' do user = create(:user, :signed_up) sign_in(user) generate_saml_response(user, saml_settings(overrides: { force_authn: true })) + # would be 200 if the user's session persists expect(response.status).to eq(302) # implicit test of request storage since request_id would be missing otherwise expect(response.location).to match(%r{#{root_url}\?request_id=.+}) end - - it 'skips signing out the user when request is from "finalauthpost"' do - link_user_to_identity(user, true, saml_settings(overrides: { force_authn: true })) - sign_in(user) - saml_final_post_auth(saml_request(saml_settings(overrides: { force_authn: true }))) - expect(response).to_not be_redirect - expect(response.status).to eq(200) - end - end - - context 'saml_internal_post feature configuration is set to false' do - let(:user) { create(:user, :signed_up) } - - before { allow(IdentityConfig.store).to receive(:saml_internal_post).and_return(false) } - - context 'ForceAuthn set to true' do - it 'signs the user out if a session is active' do - sign_in(user) - generate_saml_response(user, saml_settings(overrides: { force_authn: true })) - # would be 200 if the user's session persists - expect(response.status).to eq(302) - # implicit test of request storage since request_id would be missing otherwise - expect(response.location).to match(%r{#{root_url}\?request_id=.+}) - end - end end context 'service provider is inactive' do @@ -2078,19 +2052,17 @@ def stub_requested_attributes end end - # temporarily commenting out this spec because it needs to be updated to work - # describe 'before_actions' do - # it 'includes the appropriate before_actions' do - # expect(subject).to have_actions( - # :before, - # :disable_caching, - # :validate_saml_request, - # :validate_service_provider_and_authn_context, - # :store_saml_request, - # [:verify_authenticity_token, only: [:logout, :remotelogout]] - # ) - # end - # end + describe 'before_actions' do + it 'includes the appropriate before_actions' do + expect(subject).to have_actions( + :before, + :disable_caching, + :validate_saml_request, + :validate_service_provider_and_authn_context, + :store_saml_request, + ) + end + end describe '#external_saml_request' do it 'returns false for malformed referer' do diff --git a/spec/support/saml_auth_helper.rb b/spec/support/saml_auth_helper.rb index 0973f2a1993..61e56c2dcbd 100644 --- a/spec/support/saml_auth_helper.rb +++ b/spec/support/saml_auth_helper.rb @@ -108,12 +108,6 @@ def saml_get_auth(settings) def saml_post_auth(saml_request) # POST redirect binding Authn Request - request.path = '/api/saml/authpost2022' - post :auth, params: { SAMLRequest: CGI.unescape(saml_request) } - end - - def saml_final_post_auth(saml_request) - request.path = '/api/saml/finalauthpost2022' post :auth, params: { SAMLRequest: CGI.unescape(saml_request) } end From 5e9a119d0f3f113fbcac2b09aa8dff3dbf109f78 Mon Sep 17 00:00:00 2001 From: Doug Price Date: Wed, 21 Dec 2022 11:26:39 -0500 Subject: [PATCH 15/20] LG-8388: Confirm ThreatMetrix doesn't block IPP (#7521) * LG-8388: Confirm ThreatMetrix doesn't block IPP Spec to confirm that IPP is not blocked by a ThreatMetrix result of 'Reject' [skip changelog] * ensure the threatmetrix profiling is enabled * lints * Update spec/features/idv/in_person_spec.rb Co-authored-by: Andrew Duthie Co-authored-by: Eric Gade <105373963+eric-gade@users.noreply.github.com> Co-authored-by: Andrew Duthie --- spec/features/idv/in_person_spec.rb | 105 ++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/spec/features/idv/in_person_spec.rb b/spec/features/idv/in_person_spec.rb index 283bc6f1126..ea304697cbe 100644 --- a/spec/features/idv/in_person_spec.rb +++ b/spec/features/idv/in_person_spec.rb @@ -10,6 +10,111 @@ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) end + context 'ThreatMetrix review pending' do + let(:user) { user_with_2fa } + + before do + allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_enabled).and_return(true) + allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_required_to_verify). + and_return(true) + allow(IdentityConfig.store).to receive(:proofing_device_profiling_decisioning_enabled). + and_return(true) + end + + it 'allows the user to continue down the happy path', allow_browser_log: true do + sign_in_and_2fa_user(user) + begin_in_person_proofing(user) + + # location page + bethesda_location = page.find_all('.location-collection-item')[1] + bethesda_location.click_button(t('in_person_proofing.body.location.location_button')) + + # prepare page + complete_prepare_step(user) + + # state ID page + complete_state_id_step(user) + + # address page + complete_address_step(user) + + # ssn page + select 'Reject', from: :mock_profiling_result + complete_ssn_step(user) + + # verify page + expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.verify_info')) + expect(page).to have_content(t('headings.verify')) + expect(page).to have_text(InPersonHelper::GOOD_FIRST_NAME) + expect(page).to have_text(InPersonHelper::GOOD_LAST_NAME) + expect(page).to have_text(InPersonHelper::GOOD_DOB) + expect(page).to have_text(InPersonHelper::GOOD_STATE_ID_NUMBER) + expect(page).to have_text(InPersonHelper::GOOD_ADDRESS1) + expect(page).to have_text(InPersonHelper::GOOD_CITY) + expect(page).to have_text(InPersonHelper::GOOD_ZIPCODE) + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:state]) + expect(page).to have_text('9**-**-***4') + complete_verify_step(user) + + # phone page + expect_in_person_step_indicator_current_step( + t('step_indicator.flows.idv.verify_phone_or_address'), + ) + expect(page).to have_content(t('idv.titles.session.phone')) + fill_out_phone_form_ok(MfaContext.new(user).phone_configurations.first.phone) + click_idv_send_security_code + expect_in_person_step_indicator_current_step( + t('step_indicator.flows.idv.verify_phone_or_address'), + ) + + expect_in_person_step_indicator_current_step( + t('step_indicator.flows.idv.verify_phone_or_address'), + ) + fill_in_code_with_last_phone_otp + click_submit_default + + # password confirm page + expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.secure_account')) + expect(page).to have_content(t('idv.titles.session.review', app_name: APP_NAME)) + complete_review_step(user) + + # personal key page + expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.secure_account')) + expect(page).to have_content(t('titles.idv.personal_key')) + deadline = nil + freeze_time do + acknowledge_and_confirm_personal_key + deadline = (Time.zone.now + + IdentityConfig.store.in_person_enrollment_validity_in_days.days). + in_time_zone(Idv::InPerson::ReadyToVerifyPresenter::USPS_SERVER_TIMEZONE). + strftime(t('time.formats.event_date')) + end + + # ready to verify page + expect_in_person_step_indicator_current_step( + t('step_indicator.flows.idv.go_to_the_post_office'), + ) + expect(page).to be_axe_clean.according_to :section508, :"best-practice", :wcag21aa + enrollment_code = JSON.parse( + UspsInPersonProofing::Mock::Fixtures.request_enroll_response, + )['enrollmentCode'] + expect(page).to have_content(t('in_person_proofing.headings.barcode')) + expect(page).to have_content(Idv::InPerson::EnrollmentCodeFormatter.format(enrollment_code)) + expect(page).to have_content( + t('in_person_proofing.body.barcode.deadline', deadline: deadline), + ) + expect(page).to have_content('BETHESDA') + expect(page).to have_content( + "#{t('date.day_names')[6]}: #{t('in_person_proofing.body.barcode.retail_hours_closed')}", + ) + + # signing in again before completing in-person proofing at a post office + sign_in_and_2fa_user(user) + complete_doc_auth_steps_before_welcome_step + expect(page).to have_current_path(idv_in_person_ready_to_verify_path) + end + end + it 'works for a happy path', allow_browser_log: true do user = user_with_2fa From b5d7f6b1dec8f7541707f65f0e0c6428c1826189 Mon Sep 17 00:00:00 2001 From: Eric Gade <105373963+eric-gade@users.noreply.github.com> Date: Wed, 21 Dec 2022 12:47:20 -0500 Subject: [PATCH 16/20] Switching newer Acuant SDK to be the default version (#7513) * Switching newer Acuant SDK to be the default version -- What The introduction of the AB Testing framework for upgrading the Acuant SDK assumes that the "newer" version is the one being tested, and that the older is the "default". This change updates the step doc capture step to assume that version 11.7.1 should be used in all cases when the ab test is switched off. changelog: Internal, Upgrades, Acuant SDK Upgrade * Specifying acuant versions in configs and updating names Specifically, we are switching from use_newer_sdk to use_alternate_sdk changelog: Internal, Upgrades, Acuant SDK Upgrade * Acuant context should use config's acuant version by default changelog: Internal, Upgrades, Acuant SDK Upgrade * Fixing lints [skip changelog] * Revert "Acuant context should use config's acuant version by default" This reverts commit b4209981a0c98e642310064f133a1616d9c69736. [skip changelog] * Removing redundant condition (via @aduth) [skip changelog] --- app/javascript/packs/document-capture.tsx | 6 +-- .../idv/steps/document_capture_step.rb | 12 ++++-- .../idv/capture_doc/document_capture.html.erb | 2 +- .../idv/doc_auth/document_capture.html.erb | 2 +- .../idv/shared/_document_capture.html.erb | 2 +- config/application.yml.default | 2 + config/initializers/ab_tests.rb | 2 +- lib/identity_config.rb | 2 + spec/features/idv/analytics_spec.rb | 12 +++--- .../idv/steps/document_capture_step_spec.rb | 40 ++++++++++++++++--- .../shared/_document_capture.html.erb_spec.rb | 4 +- 11 files changed, 63 insertions(+), 23 deletions(-) diff --git a/app/javascript/packs/document-capture.tsx b/app/javascript/packs/document-capture.tsx index c8f36c47f18..5cfa0d640c3 100644 --- a/app/javascript/packs/document-capture.tsx +++ b/app/javascript/packs/document-capture.tsx @@ -26,7 +26,7 @@ interface AppRootData { maxCaptureAttemptsBeforeTips: string; maxAttemptsBeforeNativeCamera: string; acuantSdkUpgradeABTestingEnabled: string; - useNewerSdk: string; + useAlternateSdk: string; acuantVersion: string; flowPath: FlowPath; cancelUrl: string; @@ -65,13 +65,13 @@ function getMetaContent(name): string | null { const device: DeviceContextValue = { isMobile: isCameraCapableMobile() }; const trackEvent: typeof baseTrackEvent = (event, payload) => { - const { flowPath, acuantSdkUpgradeABTestingEnabled, useNewerSdk, acuantVersion } = + const { flowPath, acuantSdkUpgradeABTestingEnabled, useAlternateSdk, acuantVersion } = appRoot.dataset; return baseTrackEvent(event, { ...payload, flow_path: flowPath, acuant_sdk_upgrade_a_b_testing_enabled: acuantSdkUpgradeABTestingEnabled, - use_newer_sdk: useNewerSdk, + use_alternate_sdk: useAlternateSdk, acuant_version: acuantVersion, }); }; diff --git a/app/services/idv/steps/document_capture_step.rb b/app/services/idv/steps/document_capture_step.rb index fb18b941ff4..7cd0cb343a2 100644 --- a/app/services/idv/steps/document_capture_step.rb +++ b/app/services/idv/steps/document_capture_step.rb @@ -43,11 +43,17 @@ def native_camera_ab_testing_variables def acuant_sdk_upgrade_a_b_testing_variables bucket = AbTests::ACUANT_SDK.bucket(flow_session[:document_capture_session_uuid]) - acuant_version = (bucket == :use_newer_sdk) ? '11.7.1' : '11.7.0' + testing_enabled = IdentityConfig.store.idv_acuant_sdk_upgrade_a_b_testing_enabled + use_alternate_sdk = (bucket == :use_alternate_sdk) + if use_alternate_sdk + acuant_version = IdentityConfig.store.idv_acuant_sdk_version_alternate + else + acuant_version = IdentityConfig.store.idv_acuant_sdk_version_default + end { acuant_sdk_upgrade_a_b_testing_enabled: - IdentityConfig.store.idv_acuant_sdk_upgrade_a_b_testing_enabled, - use_newer_sdk: (bucket == :use_newer_sdk), + testing_enabled, + use_alternate_sdk: use_alternate_sdk, acuant_version: acuant_version, } end diff --git a/app/views/idv/capture_doc/document_capture.html.erb b/app/views/idv/capture_doc/document_capture.html.erb index 96e13ed4fe0..558e540c1c3 100644 --- a/app/views/idv/capture_doc/document_capture.html.erb +++ b/app/views/idv/capture_doc/document_capture.html.erb @@ -7,6 +7,6 @@ front_image_upload_url: front_image_upload_url, back_image_upload_url: back_image_upload_url, acuant_sdk_upgrade_a_b_testing_enabled: acuant_sdk_upgrade_a_b_testing_enabled, - use_newer_sdk: use_newer_sdk, + use_alternate_sdk: use_alternate_sdk, acuant_version: acuant_version, ) %> diff --git a/app/views/idv/doc_auth/document_capture.html.erb b/app/views/idv/doc_auth/document_capture.html.erb index b025a507948..83de02e8367 100644 --- a/app/views/idv/doc_auth/document_capture.html.erb +++ b/app/views/idv/doc_auth/document_capture.html.erb @@ -7,6 +7,6 @@ front_image_upload_url: front_image_upload_url, back_image_upload_url: back_image_upload_url, acuant_sdk_upgrade_a_b_testing_enabled: acuant_sdk_upgrade_a_b_testing_enabled, - use_newer_sdk: use_newer_sdk, + use_alternate_sdk: use_alternate_sdk, acuant_version: acuant_version, ) %> diff --git a/app/views/idv/shared/_document_capture.html.erb b/app/views/idv/shared/_document_capture.html.erb index 7aa34d2c11b..deca885ec3f 100644 --- a/app/views/idv/shared/_document_capture.html.erb +++ b/app/views/idv/shared/_document_capture.html.erb @@ -32,7 +32,7 @@ max_capture_attempts_before_native_camera: IdentityConfig.store.doc_auth_max_capture_attempts_before_native_camera, max_submission_attempts_before_native_camera: IdentityConfig.store.doc_auth_max_submission_attempts_before_native_camera, acuant_sdk_upgrade_a_b_testing_enabled: acuant_sdk_upgrade_a_b_testing_enabled, - use_newer_sdk: use_newer_sdk, + use_alternate_sdk: use_alternate_sdk, acuant_version: acuant_version, sp_name: sp_name, flow_path: flow_path, diff --git a/config/application.yml.default b/config/application.yml.default index e1380fc6295..bf3a6768f5d 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -114,6 +114,8 @@ idv_attempt_window_in_hours: 6 idv_contact_url: https://www.example.com idv_max_attempts: 5 idv_min_age_years: 13 +idv_acuant_sdk_version_default: '11.7.1' +idv_acuant_sdk_version_alternate: '11.7.0' idv_acuant_sdk_upgrade_a_b_testing_enabled: false idv_acuant_sdk_upgrade_a_b_testing_percent: 50 idv_send_link_attempt_window_in_minutes: 10 diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index d8388e9a3b6..4df55b07756 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -13,7 +13,7 @@ module AbTests ACUANT_SDK = AbTestBucket.new( experiment_name: 'Acuant SDK Upgrade', buckets: { - use_newer_sdk: IdentityConfig.store.idv_acuant_sdk_upgrade_a_b_testing_enabled ? + use_alternate_sdk: IdentityConfig.store.idv_acuant_sdk_upgrade_a_b_testing_enabled ? IdentityConfig.store.idv_acuant_sdk_upgrade_a_b_testing_percent : 0, }, diff --git a/lib/identity_config.rb b/lib/identity_config.rb index e93e6e7cb24..a04dfe7fa62 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -193,6 +193,8 @@ def self.build_store(config_map) config.add(:idv_contact_url, type: :string) config.add(:idv_max_attempts, type: :integer) config.add(:idv_min_age_years, type: :integer) + config.add(:idv_acuant_sdk_version_default, type: :string) + config.add(:idv_acuant_sdk_version_alternate, type: :string) config.add(:idv_acuant_sdk_upgrade_a_b_testing_enabled, type: :boolean) config.add(:idv_acuant_sdk_upgrade_a_b_testing_percent, type: :integer) config.add(:idv_send_link_attempt_window_in_minutes, type: :integer) diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index 061aa65c1a8..54ecaa525ce 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -17,8 +17,8 @@ 'IdV: doc auth upload visited' => { flow_path: 'standard', step: 'upload', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth upload submitted' => { success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'upload', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth document_capture visited' => { flow_path: 'standard', step: 'document_capture', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'Frontend: IdV: front image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_newer_sdk' => anything, 'acuant_version' => anything }, - 'Frontend: IdV: back image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_newer_sdk' => anything, 'acuant_version' => anything }, + 'Frontend: IdV: front image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything }, + 'Frontend: IdV: back image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything }, 'IdV: doc auth image upload form submitted' => { success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard' }, 'IdV: doc auth image upload vendor pii validation' => { success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'standard', attention_with_barcode: false }, 'IdV: doc auth document_capture submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'document_capture', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, @@ -53,8 +53,8 @@ 'IdV: doc auth upload visited' => { flow_path: 'standard', step: 'upload', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth upload submitted' => { success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'upload', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth document_capture visited' => { flow_path: 'standard', step: 'document_capture', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'Frontend: IdV: front image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_newer_sdk' => anything, 'acuant_version' => anything }, - 'Frontend: IdV: back image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_newer_sdk' => anything, 'acuant_version' => anything }, + 'Frontend: IdV: front image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything }, + 'Frontend: IdV: back image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything }, 'IdV: doc auth image upload form submitted' => { success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard' }, 'IdV: doc auth image upload vendor pii validation' => { success: true, errors: {}, user_id: user.uuid, attempts: 1, remaining_attempts: 3, flow_path: 'standard', attention_with_barcode: false }, 'IdV: doc auth document_capture submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'document_capture', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, @@ -85,8 +85,8 @@ 'IdV: doc auth upload visited' => { flow_path: 'standard', step: 'upload', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth upload submitted' => { success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'upload', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth document_capture visited' => { flow_path: 'standard', step: 'document_capture', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'Frontend: IdV: front image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_newer_sdk' => anything, 'acuant_version' => anything }, - 'Frontend: IdV: back image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_newer_sdk' => anything, 'acuant_version' => anything }, + 'Frontend: IdV: front image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything }, + 'Frontend: IdV: back image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything }, 'IdV: doc auth image upload form submitted' => { success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard' }, 'IdV: doc auth image upload vendor submitted' => hash_including(success: true, flow_path: 'standard', attention_with_barcode: true, doc_auth_result: 'Attention'), 'IdV: verify in person troubleshooting option clicked' => { flow_path: 'standard' }, diff --git a/spec/services/idv/steps/document_capture_step_spec.rb b/spec/services/idv/steps/document_capture_step_spec.rb index 34fd1816c1c..574d372998a 100644 --- a/spec/services/idv/steps/document_capture_step_spec.rb +++ b/spec/services/idv/steps/document_capture_step_spec.rb @@ -40,6 +40,9 @@ end end + let(:default_sdk_version) { IdentityConfig.store.idv_acuant_sdk_version_default } + let(:alternate_sdk_version) { IdentityConfig.store.idv_acuant_sdk_version_alternate } + subject(:step) do Idv::Steps::DocumentCaptureStep.new(flow) end @@ -54,6 +57,33 @@ end describe '#extra_view_variables' do + context 'with acuant sdk upgrade A/B testing disabled' do + let(:session_uuid) { SecureRandom.uuid } + + before do + allow(IdentityConfig.store). + to receive(:idv_acuant_sdk_upgrade_a_b_testing_enabled). + and_return(false) + + flow.flow_session[:document_capture_session_uuid] = session_uuid + end + + context 'and A/B test specifies the older acuant version' do + before do + stub_const( + 'AbTests::ACUANT_SDK', + FakeAbTestBucket.new.tap { |ab| ab.assign(session_uuid => 0) }, + ) + end + + it 'passes correct variables and acuant version when older is specified' do + expect(subject.extra_view_variables[:acuant_sdk_upgrade_a_b_testing_enabled]).to eq(false) + expect(subject.extra_view_variables[:use_alternate_sdk]).to eq(false) + expect(subject.extra_view_variables[:acuant_version]).to eq(default_sdk_version) + end + end + end + context 'with acuant sdk upgrade A/B testing enabled' do let(:session_uuid) { SecureRandom.uuid } @@ -69,14 +99,14 @@ before do stub_const( 'AbTests::ACUANT_SDK', - FakeAbTestBucket.new.tap { |ab| ab.assign(session_uuid => :use_newer_sdk) }, + FakeAbTestBucket.new.tap { |ab| ab.assign(session_uuid => :use_alternate_sdk) }, ) end it 'passes correct variables and acuant version when newer is specified' do expect(subject.extra_view_variables[:acuant_sdk_upgrade_a_b_testing_enabled]).to eq(true) - expect(subject.extra_view_variables[:use_newer_sdk]).to eq(true) - expect(subject.extra_view_variables[:acuant_version]).to eq('11.7.1') + expect(subject.extra_view_variables[:use_alternate_sdk]).to eq(true) + expect(subject.extra_view_variables[:acuant_version]).to eq(alternate_sdk_version) end end @@ -90,8 +120,8 @@ it 'passes correct variables and acuant version when older is specified' do expect(subject.extra_view_variables[:acuant_sdk_upgrade_a_b_testing_enabled]).to eq(true) - expect(subject.extra_view_variables[:use_newer_sdk]).to eq(false) - expect(subject.extra_view_variables[:acuant_version]).to eq('11.7.0') + expect(subject.extra_view_variables[:use_alternate_sdk]).to eq(false) + expect(subject.extra_view_variables[:acuant_version]).to eq(default_sdk_version) end end end diff --git a/spec/views/idv/shared/_document_capture.html.erb_spec.rb b/spec/views/idv/shared/_document_capture.html.erb_spec.rb index d7653696839..a4563c01318 100644 --- a/spec/views/idv/shared/_document_capture.html.erb_spec.rb +++ b/spec/views/idv/shared/_document_capture.html.erb_spec.rb @@ -14,7 +14,7 @@ let(:front_image_upload_url) { nil } let(:back_image_upload_url) { nil } let(:acuant_sdk_upgrade_a_b_testing_enabled) { false } - let(:use_newer_sdk) { false } + let(:use_alternate_sdk) { false } let(:acuant_version) { '11.7.1' } before do @@ -48,7 +48,7 @@ front_image_upload_url: front_image_upload_url, back_image_upload_url: back_image_upload_url, acuant_sdk_upgrade_a_b_testing_enabled: acuant_sdk_upgrade_a_b_testing_enabled, - use_newer_sdk: use_newer_sdk, + use_alternate_sdk: use_alternate_sdk, acuant_version: acuant_version, } end From 510225d98f3e3fb9e19c72efec1cb73c2ad28235 Mon Sep 17 00:00:00 2001 From: John Skiles Skinner Date: Wed, 21 Dec 2022 10:02:59 -0800 Subject: [PATCH 17/20] Add logging criteria, abstract logging decision into method (#7516) * Abstract logging decision into method * [skip changelog] * lintfix * Test logic in should_log? method * lintfix --- .../lexis_nexis/verification_error_parser.rb | 9 +++++++- .../verification_error_parser_spec.rb | 23 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/app/services/proofing/lexis_nexis/verification_error_parser.rb b/app/services/proofing/lexis_nexis/verification_error_parser.rb index 25e40c09827..89d198bbdd1 100644 --- a/app/services/proofing/lexis_nexis/verification_error_parser.rb +++ b/app/services/proofing/lexis_nexis/verification_error_parser.rb @@ -47,7 +47,7 @@ def parse_product_error_messages return {} if products.nil? products.each_with_object({}) do |product, error_messages| - next if product['ProductStatus'] == 'pass' + next unless should_log?(product) # don't log PhoneFinder reflected PII product.delete('ParameterDetails') if product['ProductType'] == 'PhoneFinder' @@ -56,6 +56,13 @@ def parse_product_error_messages error_messages[key] = product end end + + def should_log?(product) + return true if product['ProductStatus'] != 'pass' + return true if product['ProductType'] == 'InstantVerify' + return true if product['Items']&.flat_map(&:keys)&.include?('ItemReason') + return false + end end end end diff --git a/spec/services/proofing/lexis_nexis/verification_error_parser_spec.rb b/spec/services/proofing/lexis_nexis/verification_error_parser_spec.rb index 05b2b29b413..063e40b0941 100644 --- a/spec/services/proofing/lexis_nexis/verification_error_parser_spec.rb +++ b/spec/services/proofing/lexis_nexis/verification_error_parser_spec.rb @@ -20,6 +20,29 @@ expect(errors[:Discovery]).to eq(nil) # This should be absent since it passed expect(errors[:SomeOtherProduct]).to eq(response_body['Products'][1]) expect(errors[:InstantVerify]).to eq(response_body['Products'][2]) + expect(errors[:'Execute Instant Verify']).to be_a Hash # log product error + end + + it 'should not log a passing response containing no important information' do + response_body['Products'].first['ProductType'] = 'Fake Product' + response_body['Products'].first['ProductStatus'] = 'pass' + response_body['Products'].first['Items'].map { |i| i.delete('ItemReason') } + + expect(errors[:'Execute Instant Verify']).to eq(nil) + end + + it 'should log any Instant Verify response, including a pass' do + response_body['Products'].first['ProductStatus'] = 'pass' + response_body['Products'].first['Items'].map { |i| i.delete('ItemReason') } + + expect(errors[:'Execute Instant Verify']).to be_a Hash + end + + it 'should log any response with an ItemReason, including a pass' do + response_body['Products'].first['ProductType'] = 'Fake Product' + response_body['Products'].first['ProductStatus'] = 'pass' + + expect(errors[:'Execute Instant Verify']).to be_a Hash end end end From 307f4dc2430e7cdb8a9a6d014256e1f5dcd0802c Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 21 Dec 2022 14:43:24 -0500 Subject: [PATCH 18/20] Add filename fingerprint to JavaScript locale data script (#7527) * Add filename fingerprint to JavaScript locale data script changelog: Internal, Build Tooling, Invalidate cache for content-only revisions to JavaScript bundles * Update specs --- .../extract-keys-webpack-plugin.js | 22 +++++-- .../extract-keys-webpack-plugin.spec.js | 24 ++++++- .../rails-i18n-webpack-plugin.spec.js | 63 +++++++++++++++++++ .../spec/fixtures/production/in.js | 1 + 4 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 app/javascript/packages/rails-i18n-webpack-plugin/spec/fixtures/production/in.js diff --git a/app/javascript/packages/rails-i18n-webpack-plugin/extract-keys-webpack-plugin.js b/app/javascript/packages/rails-i18n-webpack-plugin/extract-keys-webpack-plugin.js index 49214620f88..ca97a2c5bfc 100644 --- a/app/javascript/packages/rails-i18n-webpack-plugin/extract-keys-webpack-plugin.js +++ b/app/javascript/packages/rails-i18n-webpack-plugin/extract-keys-webpack-plugin.js @@ -1,4 +1,5 @@ const sources = require('webpack-sources'); +const createHash = require('webpack/lib/util/createHash'); /** @typedef {import('webpack/lib/ChunkGroup')} ChunkGroup */ /** @typedef {import('webpack/lib/Entrypoint')} Entrypoint */ @@ -23,13 +24,19 @@ const TRANSLATE_CALL = /(?:^|[^\w'-])t\)?\(\[?(['"][a-z\d\s_.,'"]+['"])]?[,\s)]/ * Given an original file name and locale, returns a modified file name with the locale injected * prior to the original file extension. * - * @param {string} filename - * @param {string} locale + * @param {object} options + * @param {string} options.filename + * @param {string} options.locale + * @param {string} options.content + * @param {boolean} options.includeHash * * @return {string} */ -function getAdditionalAssetFilename(filename, locale) { +function getAdditionalAssetFilename({ filename, locale, content, includeHash }) { const parts = filename.split('.'); + if (includeHash) { + parts[parts.length - 2] += `-${createHash('md4').update(content).digest('hex').slice(0, 8)}`; + } parts.splice(parts.length - 1, 0, locale); return parts.join('.'); } @@ -91,6 +98,8 @@ class ExtractKeysWebpackPlugin { } apply(compiler) { + const includeHash = compiler.options.mode === 'production'; + compiler.hooks.compilation.tap('compile', (compilation) => { compilation.hooks.additionalAssets.tapPromise(PLUGIN, () => Promise.all( @@ -101,7 +110,12 @@ class ExtractKeysWebpackPlugin { const keys = getTranslationKeys(source); const additionalAssets = await this.getAdditionalAssets(keys); for (const [locale, content] of Object.entries(additionalAssets)) { - const assetFilename = getAdditionalAssetFilename(filename, locale); + const assetFilename = getAdditionalAssetFilename({ + filename, + locale, + content, + includeHash, + }); compilation.emitAsset(assetFilename, new sources.RawSource(content)); chunk.groupsIterable.forEach((group) => addFileToEntrypoint(assetFilename, group), diff --git a/app/javascript/packages/rails-i18n-webpack-plugin/extract-keys-webpack-plugin.spec.js b/app/javascript/packages/rails-i18n-webpack-plugin/extract-keys-webpack-plugin.spec.js index 304a837c7cb..3fb6e286daf 100644 --- a/app/javascript/packages/rails-i18n-webpack-plugin/extract-keys-webpack-plugin.spec.js +++ b/app/javascript/packages/rails-i18n-webpack-plugin/extract-keys-webpack-plugin.spec.js @@ -6,11 +6,29 @@ const { describe('getAdditionalAssetFilename', () => { it('adds suffix to an existing file name', () => { - const original = 'original.js'; - const suffix = 'en'; + const filename = 'original.js'; + const locale = 'en'; + const content = 'content'; + const includeHash = false; const expected = 'original.en.js'; - expect(getAdditionalAssetFilename(original, suffix)).to.equal(expected); + expect(getAdditionalAssetFilename({ filename, locale, content, includeHash })).to.equal( + expected, + ); + }); + + context('with hash included', () => { + it('adds suffix to an file name', () => { + const filename = 'original.js'; + const locale = 'en'; + const content = 'content'; + const includeHash = true; + const expected = 'original-ae771fd2.en.js'; + + expect(getAdditionalAssetFilename({ filename, locale, content, includeHash })).to.equal( + expected, + ); + }); }); }); diff --git a/app/javascript/packages/rails-i18n-webpack-plugin/rails-i18n-webpack-plugin.spec.js b/app/javascript/packages/rails-i18n-webpack-plugin/rails-i18n-webpack-plugin.spec.js index 98334dddd8f..d46c64c362d 100644 --- a/app/javascript/packages/rails-i18n-webpack-plugin/rails-i18n-webpack-plugin.spec.js +++ b/app/javascript/packages/rails-i18n-webpack-plugin/rails-i18n-webpack-plugin.spec.js @@ -123,6 +123,69 @@ describe('RailsI18nWebpackPlugin', () => { }, ); }); + + context('in production mode', () => { + it('adds hash suffix to javascript locale assets', (done) => { + webpack( + { + mode: 'production', + devtool: false, + entry: path.resolve(__dirname, 'spec/fixtures/production/in.js'), + plugins: [ + new RailsI18nWebpackPlugin({ + configPath: path.resolve(__dirname, 'spec/fixtures/locales'), + }), + new WebpackAssetsManifest({ + entrypoints: true, + publicPath: true, + writeToDisk: true, + output: 'actualmanifest.json', + }), + ], + externals: { + '@18f/identity-i18n': '_i18n_', + }, + output: { + path: path.resolve(__dirname, 'spec/fixtures/production'), + filename: 'actual[name].js', + }, + }, + async (webpackError) => { + try { + expect(webpackError).to.be.null(); + const manifest = JSON.parse( + await fs.readFile( + path.resolve(__dirname, 'spec/fixtures/production/actualmanifest.json'), + 'utf-8', + ), + ); + + expect(manifest).to.deep.equal({ + 'actualmain-3b0c232b.en.js': 'actualmain-3b0c232b.en.js', + 'actualmain-a43216c8.es.js': 'actualmain-a43216c8.es.js', + 'actualmain.js': 'actualmain.js', + entrypoints: { + main: { + assets: { + js: [ + 'actualmain.js', + 'actualmain-3b0c232b.en.js', + 'actualmain-a43216c8.es.js', + 'actualmain-3b0c232b.fr.js', + ], + }, + }, + }, + 'main.js': 'actualmain-3b0c232b.fr.js', + }); + done(); + } catch (error) { + done(error); + } + }, + ); + }); + }); }); describe('dig', () => { diff --git a/app/javascript/packages/rails-i18n-webpack-plugin/spec/fixtures/production/in.js b/app/javascript/packages/rails-i18n-webpack-plugin/spec/fixtures/production/in.js new file mode 100644 index 00000000000..69dc5ee9578 --- /dev/null +++ b/app/javascript/packages/rails-i18n-webpack-plugin/spec/fixtures/production/in.js @@ -0,0 +1 @@ +t('forms.button.submit'); From 521fdd1dd18a15169ac955acf432bbc961f4079c Mon Sep 17 00:00:00 2001 From: jc-gsa <104452882+jc-gsa@users.noreply.github.com> Date: Wed, 21 Dec 2022 14:53:58 -0600 Subject: [PATCH 19/20] LG-8438: Update language on pages related to OTP (#7515) Update OTP language changelog: Improvements, Multi-Factor Authentication, Update OTP language --- config/locales/instructions/en.yml | 7 ++++--- config/locales/instructions/es.yml | 7 ++++--- config/locales/instructions/fr.yml | 7 ++++--- spec/features/phone/default_phone_selection_spec.rb | 2 ++ 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/config/locales/instructions/en.yml b/config/locales/instructions/en.yml index ab7f8bfaa85..9f8940b510f 100644 --- a/config/locales/instructions/en.yml +++ b/config/locales/instructions/en.yml @@ -67,11 +67,12 @@ en: try_again: try again sms: confirm_code_html: Need another code? %{resend_code_link}. Message rates may apply. - number_message_html: We sent a one-time code to %{number}. This code will expire - in %{expiration} minutes. + number_message_html: We sent a text (SMS) with a one-time code to %{number}. + This code will expire in %{expiration} minutes. voice: confirm_code_html: Want us to call you again? %{resend_code_link} - number_message_html: We just called you at %{number}. + number_message_html: We made a call with a one-time code to %{number}. This code + will expire in %{expiration} minutes. webauthn: confirm_webauthn_html: Present the security key that you associated with your account. confirm_webauthn_only_html: This app requires a higher level of security. You diff --git a/config/locales/instructions/es.yml b/config/locales/instructions/es.yml index 24d83436f64..34f3e07255a 100644 --- a/config/locales/instructions/es.yml +++ b/config/locales/instructions/es.yml @@ -71,11 +71,12 @@ es: sms: confirm_code_html: '¿Necesita otro código? %{resend_code_link}. Puede estar sujeto a cargos de mensajería y datos.' - number_message_html: Hemos enviado un código único al %{number}. Este código - expirará en %{expiration} minutos. + number_message_html: Enviamos un mensaje de texto (SMS) con un código único al + %{number}. Este código caducará en %{expiration} minutos. voice: confirm_code_html: '¿Desea que le llamemos de nuevo? %{resend_code_link}' - number_message_html: Acabamos de llamarte en %{number}. + number_message_html: Realizamos una llamada con un código de un solo uso al + %{number}. Este código expirará en %{expiration} minutos. webauthn: confirm_webauthn_html: Presente la clave de seguridad que asoció con su cuenta. confirm_webauthn_only_html: Esta aplicación requiere un mayor nivel de diff --git a/config/locales/instructions/fr.yml b/config/locales/instructions/fr.yml index 02051732390..0a0932956f1 100644 --- a/config/locales/instructions/fr.yml +++ b/config/locales/instructions/fr.yml @@ -79,11 +79,12 @@ fr: sms: confirm_code_html: Vous avez besoin d’un autre code? %{resend_code_link}. Les frais liés aux messages texte peuvent s’appliquer. - number_message_html: Nous avons envoyé un code à usage unique au %{number}. Ce - code expirera dans %{expiration} minutes. + number_message_html: Nous avons envoyé un texte (SMS) avec un code à usage + unique au %{number}. Ce code expirera dans %{expiration} minutes. voice: confirm_code_html: Vous voulez que nous vous appelions de nouveau? %{resend_code_link} - number_message_html: Nous venons de vous appeler à %{number}. + number_message_html: Nous avons envoyé un code à usage unique par appel au + %{number}. Ce code expirera dans %{expiration} minutes. webauthn: confirm_webauthn_html: Présentez la clé de sécurité associée à votre compte. confirm_webauthn_only_html: Cette application nécessite un niveau de sécurité diff --git a/spec/features/phone/default_phone_selection_spec.rb b/spec/features/phone/default_phone_selection_spec.rb index 0061da6534a..e08dd9620c8 100644 --- a/spec/features/phone/default_phone_selection_spec.rb +++ b/spec/features/phone/default_phone_selection_spec.rb @@ -109,6 +109,7 @@ expect(page).to have_content t( 'instructions.mfa.voice.number_message_html', number: '+1 202-555-3434', + expiration: TwoFactorAuthenticatable::DIRECT_OTP_VALID_FOR_MINUTES, ) submit_prefilled_otp_code(user, 'voice') @@ -121,6 +122,7 @@ expect(page).to have_content t( 'instructions.mfa.voice.number_message_html', number: '(***) ***-3434', + expiration: TwoFactorAuthenticatable::DIRECT_OTP_VALID_FOR_MINUTES, ) end end From 9c009b4dd07c0f976d8db7cbb52f20c15a6cf1db Mon Sep 17 00:00:00 2001 From: Kimball Bighorse Date: Wed, 21 Dec 2022 15:12:48 -0700 Subject: [PATCH 20/20] Doc auth retry timer should decrease (#7498) * Move timeout calculation to helper * Fix calculation of attempted_at and expires_at * changelog: Bug Fixes, DocAuth, Modify retry timer to decrease * Remove instance variable from helper * Revert helper * Indent Co-authored-by: Mitchell Henke --- app/services/throttle.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/throttle.rb b/app/services/throttle.rb index c0fc4bf7e2b..e4f868061b6 100644 --- a/app/services/throttle.rb +++ b/app/services/throttle.rb @@ -167,8 +167,8 @@ def fetch_state! @redis_attempted_at = nil else @redis_attempted_at = - Time.zone.now + - Throttle.attempt_window_in_minutes(throttle_type).minutes - ttl.seconds + Time.zone.now - + Throttle.attempt_window_in_minutes(throttle_type).minutes + ttl.seconds end self