<%= t('users.delete.subheading') %>
diff --git a/app/views/users/webauthn_setup/new.html.erb b/app/views/users/webauthn_setup/new.html.erb
index 52d99e72020..3d6a7c5604c 100644
--- a/app/views/users/webauthn_setup/new.html.erb
+++ b/app/views/users/webauthn_setup/new.html.erb
@@ -21,10 +21,9 @@
<%= hidden_field_tag :webauthn_public_key, '', id: 'webauthn_public_key' %>
<%= hidden_field_tag :attestation_object, '', id: 'attestation_object' %>
<%= hidden_field_tag :client_data_json, '', id: 'client_data_json' %>
- <%= label_tag 'code', t('forms.webauthn_setup.nickname'), class: 'block bold' %>
+ <%= label_tag 'code', t('forms.webauthn_setup.nickname'), class: 'block bold', for: 'nickname' %>
<%= text_field_tag :name, '', required: true, id: 'nickname',
- class: 'block col-12 field monospace', size: 16, maxlength: 20,
- 'aria-labelledby': 'totp-label' %>
+ class: 'block col-12 field monospace', size: 16, maxlength: 20 %>
<%= hidden_field_tag 'remember_device', false, id: 'remember_device_preference' %>
<%= check_box_tag 'remember_device', true, @presenter.remember_device_box_checked?, class: 'my2 ml2 mr1' %>
diff --git a/config/application.yml.default b/config/application.yml.default
index cb5c5222670..e973e7c0fea 100644
--- a/config/application.yml.default
+++ b/config/application.yml.default
@@ -48,6 +48,7 @@ disallow_ial2_recovery:
doc_capture_request_valid_for_minutes: '15'
doc_auth_extend_timeout_by_minutes: '40'
document_capture_step_enabled: 'false'
+document_capture_react_enabled: 'true'
email_from: no-reply@login.gov
enable_load_testing_mode: 'false'
event_disavowal_expiration_hours: '240'
diff --git a/config/locales/doc_auth/en.yml b/config/locales/doc_auth/en.yml
index 092e6f48a1e..d840943fd57 100644
--- a/config/locales/doc_auth/en.yml
+++ b/config/locales/doc_auth/en.yml
@@ -12,7 +12,7 @@ en:
forms:
address1: Address
change_file: Change file
- choose_file: Drag file here or choose from folder
+ choose_file_html: Drag file here or
choose from folder
city: City
dob: Date of Birth
doc_success: We've verified your social security number and state-issued ID.
@@ -25,9 +25,13 @@ en:
headings:
address: Mailing Address
capture_complete: We have verified your state issued ID
- document_capture_html: Upload your state‑issued ID
- document_capture_with_selfie_html: Upload your state‑issued ID and
- a photo of you
+ document_capture: Add your state-issued ID
+ document_capture_back: Back of your ID
+ document_capture_front: Front of your ID
+ document_capture_heading_html: Add your state‑issued ID
+ document_capture_heading_with_selfie_html: Add your state‑issued ID
+ and selfie
+ document_capture_selfie: Your photo
selfie: Take a selfie.
ssn: Please enter your social security number.
take_pic_back: Take a photo of the back of your ID
@@ -36,14 +40,14 @@ en:
text_message: We sent a message to your phone
upload: How would you like to upload your state issued ID?
upload_back: Upload an image of the back of your state issued ID
- upload_back_html: Upload an image of the back of your state‑issued ID
upload_from_phone: Take a photo with a mobile phone to upload your ID
upload_front: Upload an image of the front of your state issued ID
- upload_front_html: Upload an image of the front of your state‑issued ID
verify: Please verify your information
welcome: We need to verify your identity
info:
camera_required: Your mobile phone must have a camera and a web browser
+ document_capture_upload_image: We only use your ID to verify your identity,
+ and we will not save any images.
link_sent:
- Please check your phone and follow instructions to take a photo of your state
issued ID.
@@ -72,6 +76,8 @@ en:
and share your personal information. We will only use it to verify your identity.
document_capture_fallback_html: Having trouble? %{link}
document_capture_fallback_link: Click here to upload an image
+ document_capture_selfie_instructions: Now take a picture of yourself. We'll
+ compare it to the image on the front of your ID.
email_sent: Link sent to %{email}. Please check your desktop email and follow
instructions to verify your identity.
learn_more: Learn more.
@@ -89,6 +95,16 @@ en:
text4: make sure you always have access
welcome: 'What you''ll need to do:'
tips:
+ document_capture_header_text: 'For best results:'
+ document_capture_hint: Must be a JPG, BMP, PNG, or TIFF
+ document_capture_id_text1: Use a dark background
+ document_capture_id_text2: Take the photo on a flat surface
+ document_capture_id_text3: Do not use the flash on your camera
+ document_capture_id_text4: File size should be at least 2 MB
+ document_capture_selfie_text1: Face the camera and ensure your entire head is
+ in the photo
+ document_capture_selfie_text2: Take a photo against a plain background
+ document_capture_selfie_text3: Do not wear a hat or sunglasses
header_text: Guidelines for taking a photo of your ID
text1: Take it in a room with lots of light. Indirect sunlight is best.
text2: Make sure your ID doesn't have dirt or damaged barcodes.
@@ -100,9 +116,7 @@ en:
information.
text7: Use a high-resolution camera. A good mobile phone or tablet camera will
work.
- title: Don't take the photo on a white surface!
title_html: "
Don't take the photo on a white surface! See
more tips..."
- title_more: See more tips…
titles:
doc_auth: Document Authentication
diff --git a/config/locales/doc_auth/es.yml b/config/locales/doc_auth/es.yml
index 53afdd42885..60e139b9ddf 100644
--- a/config/locales/doc_auth/es.yml
+++ b/config/locales/doc_auth/es.yml
@@ -12,7 +12,7 @@ es:
forms:
address1: Dirección
change_file: Cambiar archivo
- choose_file: Arrastre el archivo aquí o elija de la carpeta
+ choose_file_html: Arrastre el archivo aquí o
elija de la carpeta
city: Ciudad
dob: Fecha de nacimiento
doc_success: Verificamos su número de seguro social y su identificación emitida
@@ -26,9 +26,13 @@ es:
headings:
address: Dirección de envio
capture_complete: Hemos verificado la identificación emitida por su estado
- document_capture_html: Cargue su identificación emitida por el estado
- document_capture_with_selfie_html: Cargue su identificación emitida por el estado
- y una foto suya
+ document_capture: Agregue su identificación emitida por el estado
+ document_capture_back: Detrás de su identificación
+ document_capture_front: Frente de su identificación
+ document_capture_heading_html: Cargue su identificación emitida por el estado
+ document_capture_heading_with_selfie_html: Cargue su identificación emitida
+ por el estado y una foto suya
+ document_capture_selfie: Tu foto
selfie: Toma una selfie.
ssn: Por favor ingrese su número de seguro social.
take_pic_back: Toma una foto de la parte posterior de tu identificación
@@ -37,17 +41,15 @@ es:
text_message: Enviamos un mensaje a su teléfono
upload: "¿Cómo te gustaría subir tu identificación emitida por el estado?"
upload_back: Cargue una foto del dorso de su identificación emitida por el estado.
- upload_back_html: Cargue una foto del dorso de su identificación emitida por
- el estado.
upload_from_phone: Tome una foto con un teléfono móvil para cargar su identificación
upload_front: Cargue una foto del frente de su identificación emitida por el
estado.
- upload_front_html: Cargue una foto del frente de su identificación emitida por
- el
verify: Por favor verifica tu información
welcome: Nosotros necesitamos verificar tu identidad
info:
camera_required: Su teléfono móvil debe tener una cámara y un navegador web
+ document_capture_upload_image: Solo utilizamos su ID para verificar su identidad
+ y no guardaremos ninguna imagen.
link_sent:
- Verifique su teléfono y siga las instrucciones para tomar una fotografía de
la identificación emitida por su estado.
@@ -78,6 +80,8 @@ es:
su identidad.
document_capture_fallback_html: "¿Teniendo problemas? %{link}"
document_capture_fallback_link: Haga clic aquí para cargar una imagen.
+ document_capture_selfie_instructions: Ahora toma una foto de ti mismo. Lo compararemos
+ con la imagen en el frente de su identificación.
email_sent: Enlace enviado a %{email}. Compruebe el correo electrónico de su
escritorio y siga las instrucciones para verificar su identidad.
learn_more: Aprende más.
@@ -96,6 +100,16 @@ es:
text4: asegúrate de tener siempre acceso
welcome: 'Lo que necesitarás hacer:'
tips:
+ document_capture_header_text: 'Para mejores resultados:'
+ document_capture_hint: Aceptamos los formatos BMP, PNG, TIFF y JPG.
+ document_capture_id_text1: Usa un fondo oscuro
+ document_capture_id_text2: Toma la foto sobre una superficie plana
+ document_capture_id_text3: No uses el flash en tu cámara
+ document_capture_id_text4: El tamaño del archivo debe ser de al menos 2 MB
+ document_capture_selfie_text1: Mira a la cámara y asegúrate de que toda tu cabeza
+ esté en la foto
+ document_capture_selfie_text2: Toma una foto sobre un fondo liso
+ document_capture_selfie_text3: No use sombrero o lentes de sol
header_text: Pautas para tomar una foto de su identificación
text1: Tómalo en una habitación con mucha luz. La luz solar indirecta es la
mejor.
@@ -109,8 +123,6 @@ es:
la información.
text7: Utilice una cámara de alta resolución. La cámara de un buen teléfono
móvil o tableta funcionará.
- title: "¡No tome la foto en una superficie blanca!"
title_html: "
¡No tome la foto en una superficie blanca! Ver más..."
- title_more: Ver más…
titles:
doc_auth: Autenticación de documentos
diff --git a/config/locales/doc_auth/fr.yml b/config/locales/doc_auth/fr.yml
index 72d4c2a1c02..2906b0ea405 100644
--- a/config/locales/doc_auth/fr.yml
+++ b/config/locales/doc_auth/fr.yml
@@ -12,7 +12,8 @@ fr:
forms:
address1: Adresse
change_file: Changer de fichier
- choose_file: Faites glisser le fichier ici ou choisissez dans un dossier
+ choose_file_html: Faites glisser le fichier ici ou
choisissez
+ dans un dossier
city: Ville
dob: Date de naissance
doc_success: Nous avons vérifié votre numéro de sécurité sociale et votre identifiant
@@ -26,9 +27,14 @@ fr:
headings:
address: Adresse mail
capture_complete: Nous avons vérifié votre ID émis par l'état
- document_capture_html: Téléchargez votre pièce d'identité délivrée par l'État
- document_capture_with_selfie_html: Téléchargez votre pièce d'identité officielle
- et une photo de vous
+ document_capture: Ajoutez votre pièce d'identité émise par l'État
+ document_capture_back: Dos de votre pièce d'identité
+ document_capture_front: Recto de votre pièce d'identité
+ document_capture_heading_html: Téléchargez votre pièce d'identité délivrée par
+ l'État
+ document_capture_heading_with_selfie_html: Téléchargez votre pièce d'identité
+ officielle et une photo de vous
+ document_capture_selfie: Ta photo
selfie: Prendre un selfie.
ssn: S'il vous plaît entrez votre numéro de sécurité sociale.
take_pic_back: Prenez une photo au verso de votre identifiant
@@ -38,19 +44,17 @@ fr:
upload: Comment voudriez-vous télécharger votre ID émis par l'état?
upload_back: S'il vous plaît télécharger une photo du dos de votre ID émis par
l'état.
- upload_back_html: S'il vous plaît télécharger une photo du dos de votre ID émis
- par l'état.
upload_from_phone: Prenez une photo avec un téléphone portable pour télécharger
votre pièce d'identité
upload_front: Veuillez télécharger une photo du recto de votre identifiant émis
par l'État.
- upload_front_html: Veuillez télécharger une photo du recto de votre identifiant
- émis par l'État.
verify: S'il vous plaît vérifier vos informations
welcome: Nous devons vérifier votre identité
info:
camera_required: Votre téléphone portable doit avoir une caméra et un navigateur
Web
+ document_capture_upload_image: Nous n'utilisons votre identifiant que pour vérifier
+ votre identité, et nous n'enregistrerons aucune image.
link_sent:
- Veuillez vérifier votre téléphone et suivre les instructions pour prendre
une photo de votre identité émise par l'État.
@@ -84,6 +88,8 @@ fr:
que pour vérifier votre identité.
document_capture_fallback_html: Avoir des problèmes? %{link}
document_capture_fallback_link: Cliquez ici pour télécharger une image
+ document_capture_selfie_instructions: Maintenant, prenez une photo de vous.
+ Nous le comparerons à l'image au recto de votre pièce d'identité.
email_sent: Lien envoyé à %{email}. Veuillez vérifier votre email de bureau
et suivez les instructions pour vérifier votre identité.
learn_more: Apprendre encore plus.
@@ -103,6 +109,16 @@ fr:
text4: assurez-vous d'avoir toujours accès
welcome: 'Ce que vous devez faire:'
tips:
+ document_capture_header_text: 'Pour les meilleurs résultats:'
+ document_capture_hint: Nous acceptons les formats BMP, PNG, TIFF et JPG.
+ document_capture_id_text1: Utilisez un fond sombre
+ document_capture_id_text2: Prenez la photo sur une surface plane
+ document_capture_id_text3: Ne pas utiliser le flash sur votre appareil photo
+ document_capture_id_text4: La taille du fichier doit être d'au moins 2 Mo
+ document_capture_selfie_text1: Faites face à la caméra et assurez-vous que toute
+ votre tête est sur la photo
+ document_capture_selfie_text2: Prenez une photo sur un fond uni
+ document_capture_selfie_text3: Ne portez pas de chapeau ni de lunettes de soleil
header_text: Directives pour prendre une photo de votre identité
text1: Prenez-le dans une pièce très éclairée. La lumière solaire indirecte
est la meilleure.
@@ -117,8 +133,6 @@ fr:
de lire toutes les informations.
text7: Utilisez une caméra haute résolution. La caméra d'un bon téléphone mobile
ou d'une tablette fonctionnera.
- title: Ne prenez pas la photo sur une surface blanche!
title_html: "
Ne prenez pas la photo sur une surface blanche! Voir plus..."
- title_more: Voir plus…
titles:
doc_auth: Authentification de document
diff --git a/config/locales/image_description/en.yml b/config/locales/image_description/en.yml
index d3356067d0e..d2769473198 100644
--- a/config/locales/image_description/en.yml
+++ b/config/locales/image_description/en.yml
@@ -1,8 +1,6 @@
---
en:
image_description:
- accordian_minus_buttom: Minus button
- accordian_plus_buttom: Plus button
camera_mobile_phone: Camera flashing on a mobile phone
close: Close button
spinner: Loading spinner
diff --git a/config/locales/image_description/es.yml b/config/locales/image_description/es.yml
index 82b94c798ef..584f2930615 100644
--- a/config/locales/image_description/es.yml
+++ b/config/locales/image_description/es.yml
@@ -1,8 +1,6 @@
---
es:
image_description:
- accordian_minus_buttom: Botón menos
- accordian_plus_buttom: Botón más
camera_mobile_phone: Cámara parpadeando en un teléfono móvil
close: Botón de cierre
spinner: Indicador de carga
diff --git a/config/locales/image_description/fr.yml b/config/locales/image_description/fr.yml
index ec598581fc8..ed6875d62f3 100644
--- a/config/locales/image_description/fr.yml
+++ b/config/locales/image_description/fr.yml
@@ -1,8 +1,6 @@
---
fr:
image_description:
- accordian_minus_buttom: Bouton moins
- accordian_plus_buttom: Bouton Plus
camera_mobile_phone: Appareil photo clignotant sur un téléphone mobile
close: Bouton de fermeture
spinner: Indicateur de chargement
diff --git a/config/locales/instructions/en.yml b/config/locales/instructions/en.yml
index b376ceea3d3..98575f722fb 100644
--- a/config/locales/instructions/en.yml
+++ b/config/locales/instructions/en.yml
@@ -61,11 +61,11 @@ en:
confirm_code_html: Want us to call you again? %{resend_code_link}
number_message_html: We just called you at %{number}.
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 need to verify your identity using a security key that you previously
set up to access your information.
- confirm_webauthn_html: Present the security key that you associated with your
- account.
wrong_number_html: Entered the wrong phone number? %{link}
password:
forgot: Don’t know your password? Reset it after confirming your email address.
diff --git a/config/service_providers.localdev.yml b/config/service_providers.localdev.yml
index 3a92a18a79a..2575f765dd0 100644
--- a/config/service_providers.localdev.yml
+++ b/config/service_providers.localdev.yml
@@ -314,6 +314,7 @@ development:
return_to_sp_url: 'http://localhost:3001'
redirect_uris:
- 'http://localhost:3001/auth/logindotgov/callback'
+ - 'http://localhost:3001'
'urn:gov:gsa:openidconnect:development':
redirect_uris:
diff --git a/db/migrate/20200803211123_add_acuant_result_to_proofing_costs.rb b/db/migrate/20200803211123_add_acuant_result_to_proofing_costs.rb
new file mode 100644
index 00000000000..6b44d80627b
--- /dev/null
+++ b/db/migrate/20200803211123_add_acuant_result_to_proofing_costs.rb
@@ -0,0 +1,10 @@
+class AddAcuantResultToProofingCosts < ActiveRecord::Migration[5.2]
+ def up
+ add_column :proofing_costs, :acuant_result_count, :integer
+ change_column_default :proofing_costs, :acuant_result_count, 0
+ end
+
+ def down
+ remove_column :proofing_costs, :acuant_result_count
+ end
+end
diff --git a/db/migrate/20200803211145_backfill_add_acuant_result_to_proofing_costs.rb b/db/migrate/20200803211145_backfill_add_acuant_result_to_proofing_costs.rb
new file mode 100644
index 00000000000..3b7abae0e69
--- /dev/null
+++ b/db/migrate/20200803211145_backfill_add_acuant_result_to_proofing_costs.rb
@@ -0,0 +1,10 @@
+class BackfillAddAcuantResultToProofingCosts < ActiveRecord::Migration[5.2]
+ disable_ddl_transaction!
+
+ def up
+ ProofingCost.unscoped.in_batches do |relation|
+ relation.update_all acuant_result_count: 0
+ sleep(0.01)
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index b423803ba02..831c5d1595e 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2020_07_23_214611) do
+ActiveRecord::Schema.define(version: 2020_08_03_211145) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -380,6 +380,7 @@
t.integer "phone_otp_count", default: 0
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.integer "acuant_result_count", default: 0
t.index ["user_id"], name: "index_proofing_costs_on_user_id", unique: true
end
diff --git a/lib/asset_checker.rb b/lib/asset_checker.rb
index 74e7cb78b48..3893a4151cb 100644
--- a/lib/asset_checker.rb
+++ b/lib/asset_checker.rb
@@ -9,8 +9,8 @@ def self.check_files(argv)
def self.file_has_missing?(file)
data = File.open(file).read
- missing_translations = find_missing(data, /\Wt\s?\(['"]([^'^"]*)['"]\)/, @translation_strings)
- missing_assets = find_missing(data, /\WassetPath=["'](.*)['"]/, @asset_strings)
+ missing_translations = find_missing(data, /\Wt\s?\(['"]([^'"]*?)['"]\)/, @translation_strings)
+ missing_assets = find_missing(data, /\WassetPath=["'](.*?)['"]/, @asset_strings)
has_missing = (missing_translations.any? || missing_assets.any?)
if has_missing
warn file
diff --git a/package.json b/package.json
index ba5a69e246a..ba440eee6f2 100644
--- a/package.json
+++ b/package.json
@@ -38,9 +38,7 @@
"@babel/preset-react": "^7.10.4",
"@babel/register": "^7.4.4",
"@testing-library/user-event": "^12.0.11",
- "atob": "^2.1.2",
"babel-eslint": "^10.1.0",
- "btoa": "^1.2.1",
"chai": "^3.5.0",
"dirty-chai": "^1.2.2",
"eslint": "^7.4.0",
diff --git a/spec/controllers/risc/security_events_controller_spec.rb b/spec/controllers/risc/security_events_controller_spec.rb
index f4c63bf8067..696a5827158 100644
--- a/spec/controllers/risc/security_events_controller_spec.rb
+++ b/spec/controllers/risc/security_events_controller_spec.rb
@@ -27,7 +27,7 @@
subject: {
subject_type: 'iss_sub',
iss: root_url,
- sub: identity.uuid,
+ sub: AgencyIdentityLinker.new(identity).link_identity.uuid,
},
},
},
diff --git a/spec/features/accessibility/idv_pages_spec.rb b/spec/features/accessibility/idv_pages_spec.rb
index 1f1fd2c475a..dab67d0261a 100644
--- a/spec/features/accessibility/idv_pages_spec.rb
+++ b/spec/features/accessibility/idv_pages_spec.rb
@@ -46,6 +46,29 @@
visit idv_path
complete_all_doc_auth_steps
click_idv_continue
+ fill_in :user_password, with: Features::SessionHelper::VALID_PASSWORD
+ click_continue
+
+ expect(current_path).to eq idv_confirmations_path
+ expect(page).to be_accessible.according_to :section508, :"best-practice"
+ end
+
+ scenario 'doc auth steps accessibility' do
+ sign_in_and_2fa_user
+ visit idv_path
+ complete_all_doc_auth_steps(expect_accessible: true)
+ click_idv_continue
+ fill_in :user_password, with: Features::SessionHelper::VALID_PASSWORD
+ click_continue
+
+ expect(current_path).to eq idv_confirmations_path
+ expect(page).to be_accessible.according_to :section508, :"best-practice"
+ end
+
+ scenario 'doc auth steps accessibility on mobile', driver: :headless_chrome_mobile do
+ sign_in_and_2fa_user
+ visit idv_path
+ complete_all_doc_auth_steps(expect_accessible: true)
click_idv_continue
fill_in :user_password, with: Features::SessionHelper::VALID_PASSWORD
click_continue
diff --git a/spec/features/accessibility/user_pages_spec.rb b/spec/features/accessibility/user_pages_spec.rb
index 6b838b3d3bc..d77edafe6d8 100644
--- a/spec/features/accessibility/user_pages_spec.rb
+++ b/spec/features/accessibility/user_pages_spec.rb
@@ -138,4 +138,22 @@
expect(page).to be_accessible.according_to :section508, :"best-practice"
end
+
+ scenario 'device events page' do
+ user = sign_in_and_2fa_user
+ device = create(:device, user: user)
+ create(:event, user: user)
+
+ visit account_events_path(id: device.id)
+
+ expect(page).to be_accessible.according_to :section508, :"best-practice"
+ end
+
+ scenario 'delete user page' do
+ sign_in_and_2fa_user
+
+ visit account_delete_path
+
+ expect(page).to be_accessible.according_to :section508, :"best-practice"
+ end
end
diff --git a/spec/features/accessibility/visitor_pages_spec.rb b/spec/features/accessibility/visitor_pages_spec.rb
index f08cd2a6333..e99be638cf4 100644
--- a/spec/features/accessibility/visitor_pages_spec.rb
+++ b/spec/features/accessibility/visitor_pages_spec.rb
@@ -25,4 +25,10 @@
expect(page).to be_accessible.according_to :section508, :"best-practice"
end
+
+ scenario 'new user cancel registration page' do
+ visit sign_up_cancel_path
+
+ expect(page).to be_accessible.according_to :section508, :"best-practice"
+ end
end
diff --git a/spec/features/idv/doc_auth/document_capture_step_spec.rb b/spec/features/idv/doc_auth/document_capture_step_spec.rb
index d2994280e28..2fde83adce5 100644
--- a/spec/features/idv/doc_auth/document_capture_step_spec.rb
+++ b/spec/features/idv/doc_auth/document_capture_step_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'document capture step' do
+feature 'doc auth document capture step' do
include IdvStepHelper
include DocAuthHelper
include InPersonHelper
@@ -9,10 +9,12 @@
let(:user) { user_with_2fa }
let(:liveness_enabled) { 'false' }
before do
+ allow(Figaro.env).to receive(:document_capture_react_enabled).and_return('false')
allow(Figaro.env).to receive(:document_capture_step_enabled).
and_return(document_capture_step_enabled)
allow(Figaro.env).to receive(:liveness_checking_enabled).
and_return(liveness_enabled)
+ allow(Figaro.env).to receive(:acuant_sdk_document_capture_enabled).and_return('true')
sign_in_and_2fa_user(user)
complete_doc_auth_steps_before_document_capture_step
end
@@ -33,14 +35,21 @@
it 'is on the correct_page' do
expect(current_path).to eq(idv_doc_auth_document_capture_step)
- expect(page).to have_content(render_html_string(t('doc_auth.headings.upload_front_html')))
- expect(page).to have_content(render_html_string(t('doc_auth.headings.upload_back_html')))
- expect(page).to have_content(t('doc_auth.headings.selfie'))
+ expect(page).to have_content(t('doc_auth.headings.document_capture_front'))
+ expect(page).to have_content(t('doc_auth.headings.document_capture_back'))
+ expect(page).to have_content(t('doc_auth.headings.document_capture_selfie'))
end
- it 'displays tips and sample images' do
- expect(page).to have_content(I18n.t('doc_auth.tips.text1'))
- expect(page).to have_css('img[src*=state-id-sample-front]')
+ it 'displays tips' do
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_header_text'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text1'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text2'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text3'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text4'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_hint'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text1'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text2'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text3'))
end
it 'proceeds to the next page with valid info' do
@@ -138,9 +147,21 @@
it 'is on the correct_page, but does not show the selfie upload option' do
expect(current_path).to eq(idv_doc_auth_document_capture_step)
- expect(page).to have_content(render_html_string(t('doc_auth.headings.upload_front_html')))
- expect(page).to have_content(render_html_string(t('doc_auth.headings.upload_back_html')))
- expect(page).not_to have_content(t('doc_auth.headings.selfie'))
+ expect(page).to have_content(t('doc_auth.headings.document_capture_front'))
+ expect(page).to have_content(t('doc_auth.headings.document_capture_back'))
+ expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
+ end
+
+ it 'displays tips' do
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_header_text'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text1'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text2'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text3'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text4'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_hint'))
+ expect(page).not_to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text1'))
+ expect(page).not_to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text2'))
+ expect(page).not_to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text3'))
end
it 'proceeds to the next page with valid info' do
@@ -212,9 +233,4 @@
def next_step
idv_doc_auth_ssn_step
end
-
- def render_html_string(html_string)
- rendered = Nokogiri::HTML.parse(html_string).text
- strip_nbsp(rendered)
- end
end
diff --git a/spec/features/idv/doc_auth/mobile_document_capture_step_spec.rb b/spec/features/idv/doc_auth/mobile_document_capture_step_spec.rb
new file mode 100644
index 00000000000..7c046928c82
--- /dev/null
+++ b/spec/features/idv/doc_auth/mobile_document_capture_step_spec.rb
@@ -0,0 +1,236 @@
+require 'rails_helper'
+
+feature 'doc auth mobile document capture step' do
+ include IdvStepHelper
+ include DocAuthHelper
+ include InPersonHelper
+
+ let(:max_attempts) { Figaro.env.acuant_max_attempts.to_i }
+ let(:user) { user_with_2fa }
+ let(:liveness_enabled) { 'false' }
+ before do
+ allow(Figaro.env).to receive(:document_capture_react_enabled).and_return('false')
+ allow(Figaro.env).to receive(:document_capture_step_enabled).
+ and_return(document_capture_step_enabled)
+ allow(Figaro.env).to receive(:liveness_checking_enabled).
+ and_return(liveness_enabled)
+ allow(Figaro.env).to receive(:acuant_sdk_document_capture_enabled).and_return('true')
+ sign_in_and_2fa_user(user)
+ complete_doc_auth_steps_before_mobile_document_capture_step
+ end
+
+ context 'when the step is disabled' do
+ let(:document_capture_step_enabled) { 'false' }
+
+ it 'takes the user to the mobile front image step' do
+ expect(current_path).to eq(idv_doc_auth_mobile_front_image_step)
+ end
+ end
+
+ context 'when the step is enabled' do
+ let(:document_capture_step_enabled) { 'true' }
+
+ context 'when liveness checking is enabled' do
+ let(:liveness_enabled) { 'true' }
+
+ it 'is on the correct_page' do
+ expect(current_path).to eq(idv_doc_auth_mobile_document_capture_step)
+ expect(page).to have_content(t('doc_auth.headings.document_capture_front'))
+ expect(page).to have_content(t('doc_auth.headings.document_capture_back'))
+ expect(page).to have_content(t('doc_auth.headings.document_capture_selfie'))
+ end
+
+ it 'displays tips' do
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_header_text'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text1'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text2'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text3'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text4'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_hint'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text1'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text2'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text3'))
+ end
+
+ it 'proceeds to the next page with valid info' do
+ attach_images
+ click_idv_continue
+
+ expect(page).to have_current_path(next_step)
+ end
+
+ it 'allows the use of a base64 encoded data url representation of the image' do
+ attach_front_image_data_url
+ attach_back_image_data_url
+ attach_selfie_image_data_url
+ click_idv_continue
+
+ expect(page).to have_current_path(next_step)
+ expect(DocAuthMock::DocAuthMockClient.last_uploaded_front_image).to eq(
+ doc_auth_front_image_data_url_data,
+ )
+ expect(DocAuthMock::DocAuthMockClient.last_uploaded_back_image).to eq(
+ doc_auth_back_image_data_url_data,
+ )
+ expect(DocAuthMock::DocAuthMockClient.last_uploaded_selfie_image).to eq(
+ doc_auth_selfie_image_data_url_data,
+ )
+ end
+
+ it 'does not proceed to the next page with invalid info' do
+ mock_general_doc_auth_client_error(:create_document)
+ attach_images
+ click_idv_continue
+
+ expect(page).to have_current_path(idv_doc_auth_mobile_document_capture_step)
+ end
+
+ it 'offers in person option on failure' do
+ enable_in_person_proofing
+
+ expect(page).to_not have_link(t('in_person_proofing.opt_in_link'),
+ href: idv_in_person_welcome_step)
+
+ mock_general_doc_auth_client_error(:create_document)
+ attach_images
+ click_idv_continue
+
+ expect(page).to have_link(t('in_person_proofing.opt_in_link'),
+ href: idv_in_person_welcome_step)
+ end
+
+ it 'throttles calls to acuant and allows retry after the attempt window' do
+ allow(Figaro.env).to receive(:acuant_max_attempts).and_return(max_attempts)
+ max_attempts.times do
+ attach_images
+ click_idv_continue
+
+ expect(page).to have_current_path(next_step)
+ click_on t('doc_auth.buttons.start_over')
+ complete_doc_auth_steps_before_mobile_document_capture_step
+ end
+
+ attach_images
+ click_idv_continue
+
+ expect(page).to have_current_path(idv_session_errors_throttled_path)
+
+ Timecop.travel(Figaro.env.acuant_attempt_window_in_minutes.to_i.minutes.from_now) do
+ sign_in_and_2fa_user(user)
+ complete_doc_auth_steps_before_mobile_document_capture_step
+ attach_images
+ click_idv_continue
+
+ expect(page).to have_current_path(next_step)
+ end
+ end
+
+ it 'catches network connection errors on post_front_image' do
+ DocAuthMock::DocAuthMockClient.mock_response!(
+ method: :post_front_image,
+ response: Acuant::Response.new(
+ success: false,
+ errors: [I18n.t('errors.doc_auth.acuant_network_error')],
+ ),
+ )
+
+ attach_images
+ click_idv_continue
+
+ expect(page).to have_current_path(idv_doc_auth_mobile_document_capture_step)
+ expect(page).to have_content(I18n.t('errors.doc_auth.acuant_network_error'))
+ end
+ end
+
+ context 'when liveness checking is not enabled' do
+ let(:liveness_enabled) { 'false' }
+
+ it 'is on the correct_page, but does not show the selfie upload option' do
+ expect(current_path).to eq(idv_doc_auth_mobile_document_capture_step)
+ expect(page).to have_content(t('doc_auth.headings.document_capture_front'))
+ expect(page).to have_content(t('doc_auth.headings.document_capture_back'))
+ expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie'))
+ end
+
+ it 'displays tips' do
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_header_text'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text1'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text2'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text3'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_id_text4'))
+ expect(page).to have_content(I18n.t('doc_auth.tips.document_capture_hint'))
+ expect(page).not_to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text1'))
+ expect(page).not_to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text2'))
+ expect(page).not_to have_content(I18n.t('doc_auth.tips.document_capture_selfie_text3'))
+ end
+
+ it 'proceeds to the next page with valid info' do
+ attach_images(liveness_enabled: false)
+ click_idv_continue
+
+ expect(page).to have_current_path(next_step)
+ end
+
+ it 'allows the use of a base64 encoded data url representation of the image' do
+ attach_front_image_data_url
+ attach_back_image_data_url
+ click_idv_continue
+
+ expect(page).to have_current_path(next_step)
+ expect(DocAuthMock::DocAuthMockClient.last_uploaded_front_image).to eq(
+ doc_auth_front_image_data_url_data,
+ )
+ expect(DocAuthMock::DocAuthMockClient.last_uploaded_back_image).to eq(
+ doc_auth_back_image_data_url_data,
+ )
+ expect(DocAuthMock::DocAuthMockClient.last_uploaded_selfie_image).to be_nil
+ end
+
+ it 'throttles calls to acuant and allows retry after the attempt window' do
+ allow(Figaro.env).to receive(:acuant_max_attempts).and_return(max_attempts)
+ max_attempts.times do
+ attach_images(liveness_enabled: false)
+ click_idv_continue
+
+ expect(page).to have_current_path(next_step)
+ click_on t('doc_auth.buttons.start_over')
+ complete_doc_auth_steps_before_mobile_document_capture_step
+ end
+
+ attach_images(liveness_enabled: false)
+ click_idv_continue
+
+ expect(page).to have_current_path(idv_session_errors_throttled_path)
+
+ Timecop.travel(Figaro.env.acuant_attempt_window_in_minutes.to_i.minutes.from_now) do
+ sign_in_and_2fa_user(user)
+ complete_doc_auth_steps_before_mobile_document_capture_step
+ attach_images(liveness_enabled: false)
+ click_idv_continue
+
+ expect(page).to have_current_path(next_step)
+ end
+ end
+
+ it 'catches network connection errors on post_front_image' do
+ DocAuthMock::DocAuthMockClient.mock_response!(
+ method: :post_front_image,
+ response: Acuant::Response.new(
+ success: false,
+ errors: [I18n.t('errors.doc_auth.acuant_network_error')],
+ ),
+ )
+
+ attach_images(liveness_enabled: false)
+ click_idv_continue
+
+ expect(page).to have_current_path(idv_doc_auth_mobile_document_capture_step)
+ expect(page).to have_content(I18n.t('errors.doc_auth.acuant_network_error'))
+ end
+ end
+ end
+
+ def next_step
+ idv_doc_auth_ssn_step
+ end
+end
diff --git a/spec/features/reports/proofing_costs_report_spec.rb b/spec/features/reports/proofing_costs_report_spec.rb
index a971fa403c7..1fdb2b913fb 100644
--- a/spec/features/reports/proofing_costs_report_spec.rb
+++ b/spec/features/reports/proofing_costs_report_spec.rb
@@ -4,7 +4,7 @@
include IdvStepHelper
include DocAuthHelper
- let(:subject) { Reports::ProofingCostsReport }
+ let(:report) { JSON.parse(Reports::ProofingCostsReport.new.call) }
let(:user) { create(:user, :signed_up) }
let(:user2) { create(:user, :signed_up) }
let(:summary1) do
@@ -21,6 +21,7 @@
{
'acuant_front_image_count_average' => 1.0,
'acuant_back_image_count_average' => 1.0,
+ 'acuant_result_count_average' => 1.0,
'aamva_count_average' => 1.0,
'lexis_nexis_resolution_count_average' => 1.0,
'gpo_letter_count_average' => 0.0,
@@ -30,14 +31,14 @@
end
it 'works for no records' do
- expect(JSON.parse(subject.new.call)).to eq({})
+ expect(report).to eq({})
end
it 'works for one flow' do
sign_in_and_2fa_user(user)
complete_doc_auth_steps_before_doc_success_step
- expect(JSON.parse(subject.new.call)).to eq(doc_success_funnel.merge(summary1))
+ expect(report).to eq(doc_success_funnel.merge(summary1))
end
it 'works for two flows' do
@@ -46,6 +47,6 @@
sign_in_and_2fa_user(user2)
complete_doc_auth_steps_before_doc_success_step
- expect(JSON.parse(subject.new.call)).to eq(doc_success_funnel.merge(summary2))
+ expect(report).to eq(doc_success_funnel.merge(summary2))
end
end
diff --git a/spec/features/saml/ial1_sso_spec.rb b/spec/features/saml/ial1_sso_spec.rb
index 55d4dac2a16..0b5d6ead4f4 100644
--- a/spec/features/saml/ial1_sso_spec.rb
+++ b/spec/features/saml/ial1_sso_spec.rb
@@ -64,7 +64,7 @@
expect(current_url).to match new_user_session_path
expect(page).to have_content(sp_content)
- expect(page).to_not have_css('.accordion-header')
+ expect(page).to_not have_css('.usa-accordion__heading')
end
it 'shows user the start page with a link back to the SP' do
diff --git a/spec/forms/security_event_form_spec.rb b/spec/forms/security_event_form_spec.rb
index 75d8cd75081..eb6f1e2657f 100644
--- a/spec/forms/security_event_form_spec.rb
+++ b/spec/forms/security_event_form_spec.rb
@@ -6,7 +6,8 @@
subject(:form) { SecurityEventForm.new(body: jwt) }
let(:user) { create(:user) }
- let(:service_provider) { create(:service_provider) }
+ let(:agency) { Agency.last || Agency.create(name: 'Test Agency') }
+ let(:service_provider) { create(:service_provider, agency_id: agency.id) }
let(:rp_private_key) do
OpenSSL::PKey::RSA.new(
File.read(Rails.root.join('keys', 'saml_test_sp.key')),
@@ -33,7 +34,7 @@
}
end
- let(:subject_sub) { identity.uuid }
+ let(:subject_sub) { AgencyIdentityLinker.new(identity).link_identity.uuid }
let(:jwt_headers) { { typ: 'secevent+jwt' } }
let(:jwt) { JWT.encode(jwt_payload, rp_private_key, 'RS256', jwt_headers) }
@@ -272,6 +273,15 @@
expect(form.errors[:sub]).to include('invalid event.subject.sub claim')
end
end
+
+ context 'when the service provider has no agency' do
+ let(:service_provider) { create(:service_provider, agency: nil, agency_id: nil) }
+
+ it 'is still valid' do
+ expect(valid?).to eq(true)
+ expect(form.error_code).to eq(nil)
+ end
+ end
end
context 'with a top-level sub claim' do
diff --git a/spec/javascripts/app/document-capture/components/accordion-spec.jsx b/spec/javascripts/app/document-capture/components/accordion-spec.jsx
deleted file mode 100644
index 04d1a361076..00000000000
--- a/spec/javascripts/app/document-capture/components/accordion-spec.jsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-import render from '../../../support/render';
-import Accordion from '../../../../../app/javascript/app/document-capture/components/accordion';
-
-describe('document-capture/components/accordion', () => {
- it('renders with a unique ID', () => {
- const { container } = render(
- <>
-
Content
-
Content
- >,
- );
-
- const contents = container.querySelectorAll('[id^="accordion-content-"]');
-
- expect(contents).to.have.lengthOf(2);
- expect(contents[0].id).to.be.ok();
- expect(contents[1].id).to.be.ok();
- expect(contents[0].id).not.to.equal(contents[1].id);
- });
-});
diff --git a/spec/javascripts/app/document-capture/components/acuant-capture-spec.jsx b/spec/javascripts/app/document-capture/components/acuant-capture-spec.jsx
index 6f97c5934a2..49afedf44ae 100644
--- a/spec/javascripts/app/document-capture/components/acuant-capture-spec.jsx
+++ b/spec/javascripts/app/document-capture/components/acuant-capture-spec.jsx
@@ -25,7 +25,18 @@ describe('document-capture/components/acuant-capture', () => {
expect(container.textContent).to.equal('Loading…');
});
- it('renders an error indicator if acuant fails to load', () => {
+ it('renders an error indicator if acuant script fails to load', async () => {
+ const { findByText } = render(
+
+
+ ,
+ );
+
+ expect(await findByText('Error!')).to.be.ok();
+ expect(console).to.have.loggedError(/^Error: Could not load script:/);
+ });
+
+ it('renders an error indicator if acuant fails to initialize', () => {
const { container } = render(
diff --git a/spec/javascripts/app/document-capture/components/documents-step-spec.jsx b/spec/javascripts/app/document-capture/components/documents-step-spec.jsx
index c74c5810969..7a54900dacf 100644
--- a/spec/javascripts/app/document-capture/components/documents-step-spec.jsx
+++ b/spec/javascripts/app/document-capture/components/documents-step-spec.jsx
@@ -2,6 +2,7 @@ import React from 'react';
import userEvent from '@testing-library/user-event';
import sinon from 'sinon';
import render from '../../../support/render';
+import DeviceContext from '../../../../../app/javascript/app/document-capture/context/device';
import DocumentsStep from '../../../../../app/javascript/app/document-capture/components/documents-step';
describe('document-capture/components/documents-step', () => {
@@ -39,4 +40,18 @@ describe('document-capture/components/documents-step', () => {
// See: https://github.com/testing-library/user-event/issues/421
expect(input.getAttribute('accept')).to.equal('image/*');
});
+
+ it('renders device-specific instructions', () => {
+ let { getByText } = render(
+
+
+ ,
+ );
+
+ expect(() => getByText('doc_auth.tips.document_capture_id_text4')).to.throw();
+
+ getByText = render().getByText;
+
+ expect(() => getByText('doc_auth.tips.document_capture_id_text4')).not.to.throw();
+ });
});
diff --git a/spec/javascripts/app/document-capture/components/suspense-error-boundary-spec.jsx b/spec/javascripts/app/document-capture/components/suspense-error-boundary-spec.jsx
index 2fd8d68f003..9e49f4fe18e 100644
--- a/spec/javascripts/app/document-capture/components/suspense-error-boundary-spec.jsx
+++ b/spec/javascripts/app/document-capture/components/suspense-error-boundary-spec.jsx
@@ -1,5 +1,4 @@
import React, { lazy } from 'react';
-import sinon from 'sinon';
import render from '../../../support/render';
import SuspenseErrorBoundary from '../../../../../app/javascript/app/document-capture/components/suspense-error-boundary';
@@ -32,8 +31,6 @@ describe('document-capture/components/suspense-error-boundary', () => {
throw new Error();
};
- sinon.stub(console, 'error').callsFake(() => {});
-
const { findByText } = render(
@@ -41,8 +38,6 @@ describe('document-capture/components/suspense-error-boundary', () => {
);
expect(await findByText('Error')).to.be.ok();
-
- // eslint-disable-next-line no-console
- console.error.restore();
+ expect(console).to.have.loggedError();
});
});
diff --git a/spec/javascripts/app/document-capture/context/device-spec.jsx b/spec/javascripts/app/document-capture/context/device-spec.jsx
new file mode 100644
index 00000000000..1e392a36866
--- /dev/null
+++ b/spec/javascripts/app/document-capture/context/device-spec.jsx
@@ -0,0 +1,13 @@
+import React, { useContext } from 'react';
+import render from '../../../support/render';
+import DeviceContext from '../../../../../app/javascript/app/document-capture/context/device';
+
+describe('document-capture/context/device', () => {
+ const ContextValue = () => JSON.stringify(useContext(DeviceContext));
+
+ it('defaults to an object shape of device supports', () => {
+ const { container } = render();
+
+ expect(container.textContent).to.equal('{"isMobile":false}');
+ });
+});
diff --git a/spec/javascripts/app/document-capture/hooks/use-async-spec.jsx b/spec/javascripts/app/document-capture/hooks/use-async-spec.jsx
index b4540e417ed..93a661792aa 100644
--- a/spec/javascripts/app/document-capture/hooks/use-async-spec.jsx
+++ b/spec/javascripts/app/document-capture/hooks/use-async-spec.jsx
@@ -1,5 +1,4 @@
import React from 'react';
-import sinon from 'sinon';
import render from '../../../support/render';
import useAsync from '../../../../../app/javascript/app/document-capture/hooks/use-async';
import SuspenseErrorBoundary from '../../../../../app/javascript/app/document-capture/components/suspense-error-boundary';
@@ -52,11 +51,9 @@ describe('document-capture/hooks/use-async', () => {
expect(container.textContent).to.equal('Loading');
- sinon.stub(console, 'error').callsFake(() => {});
reject();
expect(await findByText('Error')).to.be.ok();
- // eslint-disable-next-line no-console
- console.error.restore();
+ expect(console).to.have.loggedError();
});
});
diff --git a/spec/javascripts/app/document-capture/hooks/use-i18n-spec.jsx b/spec/javascripts/app/document-capture/hooks/use-i18n-spec.jsx
index 1186ec060a3..f04b66081ec 100644
--- a/spec/javascripts/app/document-capture/hooks/use-i18n-spec.jsx
+++ b/spec/javascripts/app/document-capture/hooks/use-i18n-spec.jsx
@@ -1,24 +1,72 @@
import React from 'react';
import render from '../../../support/render';
import I18nContext from '../../../../../app/javascript/app/document-capture/context/i18n';
-import useI18n from '../../../../../app/javascript/app/document-capture/hooks/use-i18n';
+import useI18n, {
+ formatHTML,
+} from '../../../../../app/javascript/app/document-capture/hooks/use-i18n';
describe('document-capture/hooks/use-i18n', () => {
- const LocalizedString = ({ stringKey }) => useI18n()(stringKey);
+ describe('formatHTML', () => {
+ it('returns html string treated as escaped text without handler', () => {
+ const formatted = formatHTML('Hello world!', {});
- it('returns localized key value', () => {
- const { container } = render(
-
-
- ,
- );
+ const { container } = render(formatted);
- expect(container.textContent).to.equal('translation');
+ expect(container.innerHTML).to.equal('Hello <strong>world</strong>!');
+ });
+
+ it('returns html string chunked by handlers', () => {
+ const formatted = formatHTML('Hello world!', {
+ strong: ({ children }) => {children},
+ });
+
+ const { container } = render(formatted);
+
+ expect(container.innerHTML).to.equal('Hello world!');
+ });
+
+ it('returns html string chunked by multiple handlers', () => {
+ const formatted = formatHTML(
+ 'Message: Hello world!',
+ {
+ 'lg-custom': () => 'Greetings',
+ strong: ({ children }) => {children},
+ },
+ );
+
+ const { container } = render(formatted);
+
+ expect(container.innerHTML).to.equal('Message: Greetings world!');
+ });
+
+ it('removes dangling empty text fragment', () => {
+ const formatted = formatHTML('Hello world', {
+ strong: ({ children }) => {children},
+ });
+
+ const { container } = render(formatted);
+
+ expect(container.childNodes).to.have.lengthOf(2);
+ });
});
- it('falls back to key value', () => {
- const { container } = render();
+ describe('t', () => {
+ const LocalizedString = ({ stringKey }) => useI18n().t(stringKey);
+
+ it('returns localized key value', () => {
+ const { container } = render(
+
+
+ ,
+ );
+
+ expect(container.textContent).to.equal('translation');
+ });
+
+ it('falls back to key value', () => {
+ const { container } = render();
- expect(container.textContent).to.equal('sample');
+ expect(container.textContent).to.equal('sample');
+ });
});
});
diff --git a/spec/javascripts/app/webauthn_spec.js b/spec/javascripts/app/webauthn_spec.js
index a60096d94be..1393d075fe6 100644
--- a/spec/javascripts/app/webauthn_spec.js
+++ b/spec/javascripts/app/webauthn_spec.js
@@ -1,23 +1,22 @@
-import atob from 'atob';
-import btoa from 'btoa';
import * as WebAuthn from '../../../app/javascript/app/webauthn';
describe('WebAuthn', () => {
+ let originalNavigator;
+ let originalCredentials;
beforeEach(() => {
- global.window = {
- atob,
- btoa,
- location: { hostname: 'testing.webauthn.js' },
- };
- global.Uint8Array = Buffer;
- global.navigator = {
- credentials: {
- create: () => {},
- get: () => {},
- },
+ originalNavigator = global.navigator;
+ originalCredentials = global.navigator.credentials;
+ global.navigator.credentials = {
+ create: () => {},
+ get: () => {},
};
});
+ afterEach(() => {
+ global.navigator = originalNavigator;
+ global.navigator.credentials = originalCredentials;
+ });
+
describe('isWebAuthnEnabled', () => {
it('returns true if webauthn is enabled', () => {
expect(WebAuthn.isWebAuthnEnabled()).to.equal(true);
@@ -41,15 +40,15 @@ describe('WebAuthn', () => {
const userId = '123';
const userEmail = 'test@test.com';
const userChallenge = '[1, 2, 3, 4, 5, 6, 7, 8]';
- const excludeCredentials = 'credential123,credential456';
+ const excludeCredentials = 'Y3JlZGVudGlhbDEyMw==,Y3JlZGVudGlhbDQ1Ng=='; // Base64-encoded 'credential123,credential456'
it('enrolls a device using the proper create options', (done) => {
const expectedCreateOptions = {
publicKey: {
- challenge: Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]),
- rp: { name: 'testing.webauthn.js' },
+ challenge: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]),
+ rp: { name: 'example.test' },
user: {
- id: Buffer.from([123, 0, 0, 0, 0, 0, 0, 0]),
+ id: new Uint8Array([123, 0, 0, 0, 0, 0, 0, 0]),
name: 'test@test.com',
displayName: 'test@test.com',
},
@@ -143,13 +142,13 @@ describe('WebAuthn', () => {
describe('verifyWebauthnDevice', () => {
const userChallenge = '[1, 2, 3, 4, 5, 6, 7, 8]';
- const credentialIds = 'credential123,credential456';
+ const credentialIds = 'Y3JlZGVudGlhbDEyMw==,Y3JlZGVudGlhbDQ1Ng=='; // Base64-encoded 'credential123,credential456'
it('enrolls a device using the proper get options', (done) => {
const expectedGetOptions = {
publicKey: {
- challenge: Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]),
- rpId: 'testing.webauthn.js',
+ challenge: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]),
+ rpId: 'example.test',
allowCredentials: [
{
// encodes to 'credential123'
diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js
index 723272a82eb..e2d5fd0cf10 100644
--- a/spec/javascripts/spec_helper.js
+++ b/spec/javascripts/spec_helper.js
@@ -1,21 +1,20 @@
-const chai = require('chai');
-const dirtyChai = require('dirty-chai');
-const { JSDOM } = require('jsdom');
+import chai from 'chai';
+import dirtyChai from 'dirty-chai';
+import { createDOM, useCleanDOM } from './support/dom';
+import { chaiConsoleSpy, useConsoleLogSpy } from './support/console';
chai.use(dirtyChai);
+chai.use(chaiConsoleSpy);
global.expect = chai.expect;
// Emulate a DOM, since many modules will assume the presence of these globals exist as a side
// effect of their import (focus-trap, classList.js, etc). A URL is provided as a prerequisite to
// managing history API (pushState, etc).
-const dom = new JSDOM('', { url: 'http://example.test' });
+const dom = createDOM();
global.window = dom.window;
global.navigator = window.navigator;
global.document = window.document;
global.self = window;
-beforeEach(() => {
- while (document.body.firstChild) {
- document.body.removeChild(document.body.firstChild);
- }
-});
+useCleanDOM();
+useConsoleLogSpy();
diff --git a/spec/javascripts/support/console.js b/spec/javascripts/support/console.js
new file mode 100644
index 00000000000..cfe8603ab88
--- /dev/null
+++ b/spec/javascripts/support/console.js
@@ -0,0 +1,61 @@
+/* eslint-disable no-console */
+
+import sinon from 'sinon';
+
+/**
+ * Chai plugin which adds chainable `logged` method, to be used in combination with
+ * `useConsoleLogSpy` test helper to validate expected console logging.
+ *
+ * @see https://www.chaijs.com/guide/plugins/
+ * @see https://www.chaijs.com/api/plugins/
+ *
+ * @param {import('chai')} chai Chai object.
+ * @param {import('chai/lib/chai/utils')} utils Chai plugin utilities.
+ */
+export function chaiConsoleSpy(chai, utils) {
+ utils.addChainableMethod(
+ chai.Assertion.prototype,
+ 'loggedError',
+ (message) => {
+ if (message) {
+ const index = console.unverifiedCalls.findIndex((calledMessage) =>
+ message instanceof RegExp ? message.test(calledMessage) : message === calledMessage,
+ );
+ let error = `Expected console to have logged: ${message}. `;
+ error += console.unverifiedCalls
+ ? `Console logged with: ${console.unverifiedCalls.join(', ')}`
+ : 'Console did not log.';
+
+ expect(index).to.not.equal(-1, error);
+
+ console.unverifiedCalls.splice(index, 1);
+ if (console.unverifiedCalls.length === 0) {
+ delete console.unverifiedCalls;
+ }
+ } else {
+ delete console.unverifiedCalls;
+ }
+ },
+ undefined,
+ );
+}
+
+/**
+ * Test lifecycle helper which stubs `console.error` and verifies that any logging which occurs to
+ * this method is validated using the `logged` chainable assertion implemented by the
+ * `chaiConsoleSpy` Chai plugin.
+ */
+export function useConsoleLogSpy() {
+ beforeEach(() => {
+ sinon.stub(console, 'error').callsFake((message) => {
+ console.unverifiedCalls = (console.unverifiedCalls || []).concat(message);
+ });
+ });
+
+ afterEach(() => {
+ console.error.restore();
+ expect(console.unverifiedCalls).to.be.undefined(
+ `Unexpected console logging: ${(console.unverifiedCalls || []).join(', ')}`,
+ );
+ });
+}
diff --git a/spec/javascripts/support/dom.js b/spec/javascripts/support/dom.js
new file mode 100644
index 00000000000..aae99158d4b
--- /dev/null
+++ b/spec/javascripts/support/dom.js
@@ -0,0 +1,32 @@
+import { JSDOM, ResourceLoader } from 'jsdom';
+
+/**
+ * Returns an instance of a JSDOM DOM instance configured for the test environment.
+ *
+ * @return {import('jsdom').JSDOM} DOM instance.
+ */
+export function createDOM() {
+ return new JSDOM('', {
+ url: 'http://example.test',
+ resources: new (class extends ResourceLoader {
+ // eslint-disable-next-line class-methods-use-this
+ fetch(url) {
+ return url === 'about:blank'
+ ? Promise.resolve(Buffer.from(''))
+ : Promise.reject(new Error('Failed to load'));
+ }
+ })(),
+ runScripts: 'dangerously',
+ });
+}
+
+/**
+ * Test lifecycle helper which ensures a clean DOM document for each test case.
+ */
+export function useCleanDOM() {
+ beforeEach(() => {
+ while (document.body.firstChild) {
+ document.body.removeChild(document.body.firstChild);
+ }
+ });
+}
diff --git a/spec/lib/asset_checker_spec.rb b/spec/lib/asset_checker_spec.rb
index 8c9b2f5647f..e82dc0c82ac 100644
--- a/spec/lib/asset_checker_spec.rb
+++ b/spec/lib/asset_checker_spec.rb
@@ -9,7 +9,7 @@ def get_js_with_strings(asset, translation)
import useI18n from '../hooks/use-i18n';
function DocumentCapture() {
- const t = useI18n();
+ const { t } = useI18n();
const sample = (
{t('#{translation}')}
+
>
);
}
@@ -73,7 +74,7 @@ def get_js_with_strings(asset, translation)
translation_strings)
expect(AssetChecker).to receive(:warn).with(tempfile.path)
expect(AssetChecker).to receive(:warn).with('Missing translation, not-found')
- expect(AssetChecker).to receive(:warn).with('Missing asset, wont_find.svg')
+ expect(AssetChecker).to receive(:warn).twice.with('Missing asset, wont_find.svg')
expect(AssetChecker.check_files([tempfile.path])).to eq(true)
end
end
diff --git a/spec/services/acuant/responses/get_results_response_spec.rb b/spec/services/acuant/responses/get_results_response_spec.rb
index 89e1df14e03..bd444395d57 100644
--- a/spec/services/acuant/responses/get_results_response_spec.rb
+++ b/spec/services/acuant/responses/get_results_response_spec.rb
@@ -15,6 +15,14 @@
expect(response.success?).to eq(true)
expect(response.errors).to eq([])
expect(response.exception).to be_nil
+ expect(response.to_h).to eq(
+ success: true,
+ errors: [],
+ exception: nil,
+ result: 'Passed',
+ )
+ expect(response.result_code).to eq(Acuant::ResultCodes::PASSED)
+ expect(response.result_code.billed?).to eq(true)
end
it 'parsed PII from the doc' do
@@ -52,6 +60,8 @@
[I18n.t('friendly_errors.doc_auth.document_type_could_not_be_determined')],
)
expect(response.exception).to be_nil
+ expect(response.result_code).to eq(Acuant::ResultCodes::UNKNOWN)
+ expect(response.result_code.billed?).to eq(false)
end
context 'when a friendly error does not exist for the acuant error message' do
diff --git a/spec/services/acuant/responses/liveness_response_spec.rb b/spec/services/acuant/responses/liveness_response_spec.rb
index 9a82ced4200..2bea2331814 100644
--- a/spec/services/acuant/responses/liveness_response_spec.rb
+++ b/spec/services/acuant/responses/liveness_response_spec.rb
@@ -17,6 +17,7 @@
success: true,
errors: [],
exception: nil,
+ liveness_assessment: 'Live',
liveness_score: 99,
acuant_error: { message: nil, code: nil },
)
@@ -39,6 +40,7 @@
success: false,
errors: [I18n.t('errors.doc_auth.selfie')],
exception: nil,
+ liveness_assessment: nil,
liveness_score: nil,
acuant_error: {
message: 'Face is too small. Move the camera closer to the face and retake the picture.',
diff --git a/spec/services/acuant/result_codes_spec.rb b/spec/services/acuant/result_codes_spec.rb
new file mode 100644
index 00000000000..9af0fcc207f
--- /dev/null
+++ b/spec/services/acuant/result_codes_spec.rb
@@ -0,0 +1,16 @@
+require 'rails_helper'
+
+RSpec.describe Acuant::ResultCodes do
+ describe '.from_int' do
+ it 'is a result code for the int' do
+ result_code = Acuant::ResultCodes.from_int(1)
+ expect(result_code).to be_a(Acuant::ResultCodes::ResultCode)
+ expect(result_code.billed?).to eq(true)
+ end
+
+ it 'is nil when there is no matching code' do
+ result_code = Acuant::ResultCodes.from_int(999)
+ expect(result_code).to be_nil
+ end
+ end
+end
diff --git a/spec/support/features/doc_auth_helper.rb b/spec/support/features/doc_auth_helper.rb
index 7d0e40c8d6d..e84f25c19cd 100644
--- a/spec/support/features/doc_auth_helper.rb
+++ b/spec/support/features/doc_auth_helper.rb
@@ -42,6 +42,10 @@ def idv_doc_auth_document_capture_step
idv_doc_auth_step_path(step: :document_capture)
end
+ def idv_doc_auth_mobile_document_capture_step
+ idv_doc_auth_step_path(step: :mobile_document_capture)
+ end
+
def idv_doc_auth_front_image_step
idv_doc_auth_step_path(step: :front_image)
end
@@ -82,23 +86,33 @@ def idv_doc_auth_email_sent_step
idv_doc_auth_step_path(step: :email_sent)
end
- def complete_doc_auth_steps_before_welcome_step
+ def complete_doc_auth_steps_before_welcome_step(expect_accessible: false)
visit idv_doc_auth_welcome_step unless current_path == idv_doc_auth_welcome_step
+ expect(page).to be_accessible.according_to :section508, :"best-practice" if expect_accessible
end
- def complete_doc_auth_steps_before_upload_step
+ def complete_doc_auth_steps_before_upload_step(expect_accessible: false)
visit idv_doc_auth_welcome_step unless current_path == idv_doc_auth_welcome_step
+ expect(page).to be_accessible.according_to :section508, :"best-practice" if expect_accessible
find('label', text: t('doc_auth.instructions.consent')).click
click_on t('doc_auth.buttons.continue')
end
- def complete_doc_auth_steps_before_document_capture_step
- complete_doc_auth_steps_before_upload_step
+ def complete_doc_auth_steps_before_document_capture_step(expect_accessible: false)
+ complete_doc_auth_steps_before_upload_step(expect_accessible: expect_accessible)
+ expect(page).to be_accessible.according_to :section508, :"best-practice" if expect_accessible
click_on t('doc_auth.info.upload_computer_link')
end
- def complete_doc_auth_steps_before_front_image_step
+ def complete_doc_auth_steps_before_mobile_document_capture_step
+ allow(DeviceDetector).to receive(:new).and_return(mobile_device)
complete_doc_auth_steps_before_upload_step
+ click_on t('doc_auth.buttons.use_phone')
+ end
+
+ def complete_doc_auth_steps_before_front_image_step(expect_accessible: false)
+ complete_doc_auth_steps_before_upload_step(expect_accessible: expect_accessible)
+ expect(page).to be_accessible.according_to :section508, :"best-practice" if expect_accessible
click_on t('doc_auth.info.upload_computer_link')
end
@@ -113,14 +127,16 @@ def mobile_device
AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1')
end
- def complete_doc_auth_steps_before_ssn_step
- complete_doc_auth_steps_before_back_image_step
+ def complete_doc_auth_steps_before_ssn_step(expect_accessible: false)
+ complete_doc_auth_steps_before_back_image_step(expect_accessible: expect_accessible)
+ expect(page).to be_accessible.according_to :section508, :"best-practice" if expect_accessible
attach_image
click_idv_continue
end
- def complete_doc_auth_steps_before_back_image_step
- complete_doc_auth_steps_before_front_image_step
+ def complete_doc_auth_steps_before_back_image_step(expect_accessible: false)
+ complete_doc_auth_steps_before_front_image_step(expect_accessible: expect_accessible)
+ expect(page).to be_accessible.according_to :section508, :"best-practice" if expect_accessible
attach_image
click_idv_continue
end
@@ -131,26 +147,31 @@ def complete_doc_auth_steps_before_mobile_back_image_step
click_idv_continue
end
- def complete_doc_auth_steps_before_doc_success_step
- complete_doc_auth_steps_before_verify_step
+ def complete_doc_auth_steps_before_doc_success_step(expect_accessible: false)
+ complete_doc_auth_steps_before_verify_step(expect_accessible: expect_accessible)
+ expect(page).to be_accessible.according_to :section508, :"best-practice" if expect_accessible
click_idv_continue
end
- def complete_all_doc_auth_steps
- complete_doc_auth_steps_before_doc_success_step
+ def complete_all_doc_auth_steps(expect_accessible: false)
+ complete_doc_auth_steps_before_doc_success_step(expect_accessible: expect_accessible)
+ expect(page).to be_accessible.according_to :section508, :"best-practice" if expect_accessible
click_idv_continue
end
- def complete_doc_auth_steps_before_address_step
+ def complete_doc_auth_steps_before_address_step(expect_accessible: false)
complete_doc_auth_steps_before_verify_step
+ expect(page).to be_accessible.according_to :section508, :"best-practice" if expect_accessible
click_link t('doc_auth.buttons.change_address')
end
- def complete_doc_auth_steps_before_verify_step
- complete_doc_auth_steps_before_ssn_step
+ def complete_doc_auth_steps_before_verify_step(expect_accessible: false)
+ complete_doc_auth_steps_before_ssn_step(expect_accessible: expect_accessible)
+ expect(page).to be_accessible.according_to :section508, :"best-practice" if expect_accessible
if page.current_path == idv_doc_auth_selfie_step
attach_image
click_idv_continue
+ expect(page).to be_accessible.according_to :section508, :"best-practice" if expect_accessible
end
fill_out_ssn_form_ok
click_idv_continue
diff --git a/spec/svg_spec.rb b/spec/svg_spec.rb
index 556f2695b40..e6aa245ad7d 100644
--- a/spec/svg_spec.rb
+++ b/spec/svg_spec.rb
@@ -10,7 +10,9 @@
it 'does not contain inline style tags (that render poorly in IE due to CSP)' do
doc = Nokogiri::XML(File.read(svg_path))
- expect(doc.css('style')).to be_empty
+ expect(doc.css('style')).to be_empty.or(
+ have_attributes(text: match(%r{^\s*/\*\s*lint-ignore\s*\*/})),
+ )
end
end
end
diff --git a/spec/views/idv/review/new.html.slim_spec.rb b/spec/views/idv/review/new.html.slim_spec.rb
index 25b84e5b230..4a5c79bcb3d 100644
--- a/spec/views/idv/review/new.html.slim_spec.rb
+++ b/spec/views/idv/review/new.html.slim_spec.rb
@@ -38,7 +38,7 @@
end
it 'contains an accordion with verified user information' do
- accordion_selector = generate_class_selector('accordion')
+ accordion_selector = generate_class_selector('usa-accordion')
expect(rendered).to have_xpath("//#{accordion_selector}")
end
diff --git a/spec/views/shared/_footer_lite.html.slim_spec.rb b/spec/views/shared/_footer_lite.html.erb_spec.rb
similarity index 96%
rename from spec/views/shared/_footer_lite.html.slim_spec.rb
rename to spec/views/shared/_footer_lite.html.erb_spec.rb
index 78196788915..000b5328d69 100644
--- a/spec/views/shared/_footer_lite.html.slim_spec.rb
+++ b/spec/views/shared/_footer_lite.html.erb_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'shared/_footer_lite.html.slim' do
+describe 'shared/_footer_lite.html.erb' do
context 'user is signed out' do
before do
controller.request.path_parameters[:controller] = 'users/sessions'
diff --git a/yarn.lock b/yarn.lock
index c06d05807ed..92717799769 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2232,11 +2232,6 @@ browserslist@^4.0.0, browserslist@^4.6.4, browserslist@^4.8.3, browserslist@^4.9
node-releases "^1.1.52"
pkg-up "^3.1.0"
-btoa@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73"
- integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==
-
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"