- <%= image_tag(asset_url('state-id-sample-back.jpg'), height: 338, width: 450) %>
+ <%# ---- Selfie ----- %>
+ <% if liveness_checking_enabled? %>
+
+
+ <%= t('doc_auth.headings.selfie') %>
+
+
+ <%= f.input :selfie_image_data_url, as: :hidden %>
+ <%= f.input :selfie_image, label: false, as: :file, required: true, wrapper_class: 'mt3 sm-col-8' %>
+
<% end %>
- <%= f.input :back_image_data_url, as: :hidden %>
- <%= render 'idv/doc_auth/notices', flow_session: flow_session %>
- <%= f.input :back_image, label: false, as: :file, required: true, wrapper_class: 'mt3 sm-col-8' %>
-
-
- <%# ---- Selfie ----- %>
-
-
- <%# ---- Selfie ----- %>
+ <%# ---- Submit ----- %>
<%= render 'idv/doc_auth/submit_with_spinner' %>
@@ -79,3 +101,4 @@
<%= render 'idv/doc_auth/start_over_or_cancel' %>
<%= javascript_pack_tag 'image-preview' %>
+<%= javascript_pack_tag 'document-capture' %>
diff --git a/config/locales/doc_auth/en.yml b/config/locales/doc_auth/en.yml
index 29f25e08c0a..9c41b17eafc 100644
--- a/config/locales/doc_auth/en.yml
+++ b/config/locales/doc_auth/en.yml
@@ -22,6 +22,9 @@ 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
selfie: Take a selfie.
ssn: Please enter your social security number.
take_pic_back: Take a photo of the back of your ID
@@ -30,8 +33,10 @@ 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:
diff --git a/config/locales/doc_auth/es.yml b/config/locales/doc_auth/es.yml
index fa31b60c9bf..b9842a945b3 100644
--- a/config/locales/doc_auth/es.yml
+++ b/config/locales/doc_auth/es.yml
@@ -23,6 +23,9 @@ 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
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
@@ -31,9 +34,13 @@ 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:
diff --git a/config/locales/doc_auth/fr.yml b/config/locales/doc_auth/fr.yml
index f4c68e77c47..fe5460999c7 100644
--- a/config/locales/doc_auth/fr.yml
+++ b/config/locales/doc_auth/fr.yml
@@ -23,6 +23,9 @@ 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
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
@@ -32,10 +35,14 @@ 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:
diff --git a/config/locales/errors/en.yml b/config/locales/errors/en.yml
index 7a8dbea23fc..43cd9a9fcec 100644
--- a/config/locales/errors/en.yml
+++ b/config/locales/errors/en.yml
@@ -24,6 +24,26 @@ en:
to validate a document. Please try again later.
consent_form: Before you can continue, you must give us permission. Please check
the box below and then click continue.
+ document_capture_info_html: |-
+
Please check that:
+
+ - Images are clear (not blurry) and in focus
+ - No glare, shadow or reflection is present
+ - Your ID does not have any damaged barcodes
+ - Your ID represents 80% of the overall image
+ - Your ID is against a solid, dark background
+
+ document_capture_info_with_selfie_html: |-
+
Please check that:
+
+ - Images are clear (not blurry) and in focus
+ - No glare, shadow or reflection is present
+ - Your ID does not have any damaged barcodes
+ - Your ID represents 80% of the overall image
+ - Your ID is against a solid, dark background
+ - Your selfie is taken in a well-lit area with a plain background
+ - You are not wearing a hat or glasses, and your head is fully visible
+
general_error: We could not read or verify your ID. Try to take and upload new
images of both the front and back of your ID. Make sure everything can be
easily read, including the barcode.
diff --git a/config/locales/errors/es.yml b/config/locales/errors/es.yml
index 9d972e3de6b..9f648048df4 100644
--- a/config/locales/errors/es.yml
+++ b/config/locales/errors/es.yml
@@ -24,6 +24,26 @@ es:
intentar validar un documento. Por favor, inténtelo de nuevo más tarde.
consent_form: Antes de continuar, debe darnos permiso. Marque la casilla a continuación
y luego haga clic en continuar.
+ document_capture_info_html: |-
+
Por favor verifique que:
+
+ - Las imágenes son claras (no borrosas) y enfocadas
+ - No hay reflejos, sombras o reflejos
+ - Su identificación no tiene ningún código de barras dañado
+ - Su identificación representa el 80% de la imagen general
+ - Su identificación está contra un fondo sólido y oscuro
+
+ document_capture_info_with_selfie_html: |-
+
Por favor verifique que:
+
+ - Las imágenes son claras (no borrosas) y enfocadas
+ - No hay reflejos, sombras o reflejos
+ - Su identificación no tiene ningún código de barras dañado
+ - Su identificación representa el 80% de la imagen general
+ - Su identificación está contra un fondo sólido y oscuro
+ - Su selfie se toma en un área bien iluminada con un fondo liso
+ - No lleva puesto un sombrero o anteojos, y su cabeza es completamente visible
+
general_error: No pudimos leer ni verificar su identificación. Intenta tomar
y subir nuevas imágenes de la parte delantera y trasera de tu ID. Asegúrese
de que todo se pueda leer fácilmente, incluido el código de barras.
diff --git a/config/locales/errors/fr.yml b/config/locales/errors/fr.yml
index 6d84b17fa8d..b1113b43214 100644
--- a/config/locales/errors/fr.yml
+++ b/config/locales/errors/fr.yml
@@ -24,6 +24,26 @@ fr:
pouvez essayer de valider un document. Veuillez réessayer plus tard.
consent_form: Avant de pouvoir continuer, vous devez nous donner la permission.
Veuillez cocher la case ci-dessous puis cliquez sur continuer.
+ document_capture_info_html: |-
+
Veuillez vérifier que:
+
+ - Les images sont claires (non floues) et nettes
+ - Aucun éblouissement, ombre ou reflet n'est présent
+ - Votre identifiant n'a pas de code à barres endommagé
+ - Votre pièce d'identité représente 80% de l'image globale
+ - Votre pièce d'identité est sur un fond sombre et uni
+
+ document_capture_info_with_selfie_html: |-
+
Veuillez vérifier que:
+
+ - Images are clear (not blurry) and in focus
+ - Aucun éblouissement, ombre ou reflet n'est présent
+ - Votre identifiant n'a pas de code à barres endommagé
+ - Votre pièce d'identité représente 80% de l'image globale
+ - Votre pièce d'identité est sur un fond sombre et uni
+ - Votre selfie est pris dans une zone bien éclairée avec un arrière-plan uni
+ - Vous ne portez pas de chapeau ni de lunettes et votre tête est entièrement visible
+
general_error: Nous n'avons pas pu lire ni vérifier votre identité. Essayez
de prendre et de télécharger de nouvelles images des deux côtés de votre ID.
Assurez-vous que tout peut être facilement lu, y compris le code à barres.
diff --git a/lib/feature_management.rb b/lib/feature_management.rb
index dc4c89a53da..137f26a0cfe 100644
--- a/lib/feature_management.rb
+++ b/lib/feature_management.rb
@@ -112,6 +112,10 @@ def self.doc_capture_polling_enabled?
Figaro.env.doc_capture_polling_enabled == 'true'
end
+ def self.document_capture_step_enabled?
+ Figaro.env.document_capture_step_enabled == 'true'
+ end
+
def self.hide_phone_mfa_signup?
Figaro.env.hide_phone_mfa_signup == 'true'
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 177707dcf22..d2994280e28 100644
--- a/spec/features/idv/doc_auth/document_capture_step_spec.rb
+++ b/spec/features/idv/doc_auth/document_capture_step_spec.rb
@@ -7,9 +7,12 @@
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_step_enabled).
and_return(document_capture_step_enabled)
+ allow(Figaro.env).to receive(:liveness_checking_enabled).
+ and_return(liveness_enabled)
sign_in_and_2fa_user(user)
complete_doc_auth_steps_before_document_capture_step
end
@@ -25,104 +28,193 @@
context 'when the step is enabled' do
let(:document_capture_step_enabled) { 'true' }
- it 'is on the correct_page' do
- expect(current_path).to eq(idv_doc_auth_document_capture_step)
- expect(page).to have_content(t('doc_auth.headings.upload_front'))
- expect(page).to have_content(t('doc_auth.headings.upload_back'))
- end
+ context 'when liveness checking is enabled' do
+ let(:liveness_enabled) { 'true' }
- 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]')
- end
+ 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'))
+ end
- it 'proceeds to the next page with valid info' do
- attach_images
- click_idv_continue
+ 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]')
+ end
- expect(page).to have_current_path(next_step)
- end
+ it 'proceeds to the next page with valid info' do
+ attach_images
+ click_idv_continue
- 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,
- )
- end
+ expect(page).to have_current_path(next_step)
+ 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
+ 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(idv_doc_auth_document_capture_step)
- end
+ 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 'offers in person option on failure' do
- enable_in_person_proofing
+ 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_not have_link(t('in_person_proofing.opt_in_link'),
- href: idv_in_person_welcome_step)
+ expect(page).to have_current_path(idv_doc_auth_document_capture_step)
+ end
- mock_general_doc_auth_client_error(:create_document)
- attach_images
- click_idv_continue
+ it 'offers in person option on failure' do
+ enable_in_person_proofing
- expect(page).to have_link(t('in_person_proofing.opt_in_link'),
- href: idv_in_person_welcome_step)
- end
+ expect(page).to_not have_link(t('in_person_proofing.opt_in_link'),
+ href: idv_in_person_welcome_step)
- 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
+ mock_general_doc_auth_client_error(:create_document)
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_document_capture_step
+ expect(page).to have_link(t('in_person_proofing.opt_in_link'),
+ href: idv_in_person_welcome_step)
end
- attach_images
- click_idv_continue
+ 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(idv_session_errors_throttled_path)
+ expect(page).to have_current_path(next_step)
+ click_on t('doc_auth.buttons.start_over')
+ complete_doc_auth_steps_before_document_capture_step
+ end
- 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_document_capture_step
attach_images
click_idv_continue
- expect(page).to have_current_path(next_step)
+ 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_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_document_capture_step)
+ expect(page).to have_content(I18n.t('errors.doc_auth.acuant_network_error'))
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')],
- ),
- )
+ 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_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'))
+ 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_document_capture_step
+ end
+
+ attach_images(liveness_enabled: false)
+ click_idv_continue
- 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_document_capture_step
+ attach_images(liveness_enabled: false)
+ click_idv_continue
+
+ expect(page).to have_current_path(next_step)
+ end
+ end
- expect(page).to have_current_path(idv_doc_auth_document_capture_step)
- expect(page).to have_content(I18n.t('errors.doc_auth.acuant_network_error'))
+ 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_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_selfie_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/forms/idv/document_capture_form_spec.rb b/spec/forms/idv/document_capture_form_spec.rb
new file mode 100644
index 00000000000..00b61f9d8a2
--- /dev/null
+++ b/spec/forms/idv/document_capture_form_spec.rb
@@ -0,0 +1,128 @@
+require 'rails_helper'
+
+describe Idv::DocumentCaptureForm do
+ let(:liveness_enabled) { false }
+ let(:subject) { Idv::DocumentCaptureForm.new(liveness_checking_enabled: liveness_enabled) }
+ let(:front_image_data) { 'abc' }
+ let(:front_image_data_url) { 'data:image/jpeg;base64,abc' }
+ let(:back_image_data) { 'def' }
+ let(:back_image_data_url) { 'data:image/jpeg;base64,def' }
+ let(:selfie_image_data) { 'ghi' }
+ let(:selfie_image_data_url) { 'data:image/jpeg;base64,ghi' }
+
+ describe '#submit' do
+ context 'when liveness checking is not enabled' do
+ context 'when the form has front and back images' do
+ it 'returns a successful form response' do
+ result = subject.submit(front_image: front_image_data,
+ back_image: back_image_data)
+
+ expect(result).to be_kind_of(FormResponse)
+ expect(result.success?).to eq(true)
+ expect(result.errors).to be_empty
+ end
+ end
+
+ context 'when the form has a front and back data_urls' do
+ it 'returns a successful form response' do
+ result = subject.submit(front_image: front_image_data_url,
+ back_image: back_image_data_url)
+
+ expect(result).to be_kind_of(FormResponse)
+ expect(result.success?).to eq(true)
+ expect(result.errors).to be_empty
+ end
+ end
+ end
+
+ context 'when liveness checking is enabled' do
+ let(:liveness_enabled) { true }
+
+ context 'when the form has front, back, and selfie images' do
+ it 'returns a successful form response' do
+ result = subject.submit(front_image: front_image_data,
+ back_image: back_image_data,
+ selfie_image: selfie_image_data)
+
+ expect(result).to be_kind_of(FormResponse)
+ expect(result.success?).to eq(true)
+ expect(result.errors).to be_empty
+ end
+ end
+
+ context 'when the form only has front and back images' do
+ it 'returns a successful form response' do
+ result = subject.submit(front_image: front_image_data,
+ back_image: back_image_data)
+
+ expect(result).to be_kind_of(FormResponse)
+ expect(result.success?).to eq(false)
+ expect(subject.errors).to include(:selfie_image)
+ end
+ end
+
+ context 'when the form has a front, back, and selfie data_urls' do
+ it 'returns a successful form response' do
+ result = subject.submit(front_image: front_image_data_url,
+ back_image: back_image_data_url,
+ selfie_image: front_image_data_url)
+
+ expect(result).to be_kind_of(FormResponse)
+ expect(result.success?).to eq(true)
+ expect(result.errors).to be_empty
+ end
+ end
+
+ context 'when the form only has a front and back data_urls' do
+ it 'returns a successful form response' do
+ result = subject.submit(front_image: front_image_data_url,
+ back_image: back_image_data_url)
+
+ expect(result).to be_kind_of(FormResponse)
+ expect(result.success?).to eq(false)
+ expect(subject.errors).to include(:selfie_image)
+ end
+ end
+ end
+
+ context 'when the form has invalid attributes' do
+ it 'raises an error' do
+ expect do
+ subject.submit(front_image: front_image_data,
+ back_image: back_image_data_url,
+ foo: 1)
+ end.to raise_error(ArgumentError, 'foo is an invalid image attribute')
+ end
+ end
+ end
+
+ describe 'presence validations' do
+ context 'when liveness checking is not enabled' do
+ it 'is invalid when image and data_url attributes are not present' do
+ result = subject.submit({})
+
+ expect(subject).to_not be_valid
+ expect(subject.errors).to include(:front_image)
+ expect(subject.errors).to include(:back_image)
+ expect(subject.errors).not_to include(:selfie_image)
+ expect(result).to be_kind_of(FormResponse)
+ expect(result.success?).to eq(false)
+ end
+ end
+
+ context 'when liveness checking is enabled' do
+ let(:liveness_enabled) { true }
+
+ it 'is invalid when image and data_url attributes are not present' do
+ result = subject.submit({})
+
+ expect(subject).to_not be_valid
+ expect(subject.errors).to include(:front_image)
+ expect(subject.errors).to include(:back_image)
+ expect(subject.errors).to include(:selfie_image)
+ expect(result).to be_kind_of(FormResponse)
+ expect(result.success?).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/services/acuant/acuant_client_spec.rb b/spec/services/acuant/acuant_client_spec.rb
index e5631c7fcb3..a7973cd26f9 100644
--- a/spec/services/acuant/acuant_client_spec.rb
+++ b/spec/services/acuant/acuant_client_spec.rb
@@ -48,29 +48,96 @@
end
describe '#post_images' do
- it 'sends an upload image request for the front image' do
- instance_id = 'this-is-a-test-instance-id'
- url = URI.join(
+ let(:liveness_enabled) { false }
+ let(:instance_id) { 'this-is-a-test-instance-id' }
+ let(:image_upload_url) do
+ URI.join(
Figaro.env.acuant_assure_id_url, "/AssureIDService/Document/#{instance_id}/Image"
)
- stub_request(:post, url).with(query: { side: 0, light: 0 }).to_return(body: '', status: 201)
- stub_request(:post, url).with(query: { side: 1, light: 0 }).to_return(body: '', status: 201)
- results_url = URI.join(
+ end
+ let(:front_image_query) { { query: { side: 0, light: 0 } } }
+ let(:back_image_query) { { query: { side: 1, light: 0 } } }
+ let(:results_url) do
+ URI.join(
Figaro.env.acuant_assure_id_url, "/AssureIDService/Document/#{instance_id}"
)
+ end
+
+ before do
+ # DL image upload stubs
+ stub_request(:post, image_upload_url).with(front_image_query).to_return(body: '', status: 201)
+ stub_request(:post, image_upload_url).with(back_image_query).to_return(body: '', status: 201)
stub_request(:get, results_url).to_return(body: AcuantFixtures.get_results_response_success)
allow(subject).to receive(:create_document).and_return(
OpenStruct.new('success?' => true, instance_id: instance_id),
)
+ end
- result = subject.post_images(
- front_image: DocAuthImageFixtures.document_front_image,
- back_image: DocAuthImageFixtures.document_back_image,
- instance_id: instance_id,
- )
+ context 'with liveness checking enabled' do
+ let(:get_face_image_url) do
+ URI.join(
+ Figaro.env.acuant_assure_id_url,
+ "/AssureIDService/Document/#{instance_id}/Field/Image?key=Photo",
+ )
+ end
+ let(:facial_match_url) { URI.join(Figaro.env.acuant_facial_match_url, '/api/v1/facematch') }
+ let(:liveness_url) { URI.join(Figaro.env.acuant_passlive_url, '/api/v1/liveness') }
+ let(:liveness_enabled) { true }
- expect(result.success?).to eq(true)
+ it 'sends an upload image request for the front, back, and selfie images' do
+ # Selfie stubs
+ stub_request(:get, get_face_image_url).
+ to_return(body: AcuantFixtures.get_face_image_response)
+ stub_request(:post, facial_match_url).
+ to_return(body: AcuantFixtures.facial_match_response_success)
+ stub_request(:post, liveness_url).
+ to_return(body: AcuantFixtures.liveness_response_success)
+
+ result = subject.post_images(
+ front_image: DocAuthImageFixtures.document_front_image,
+ back_image: DocAuthImageFixtures.document_back_image,
+ selfie_image: DocAuthImageFixtures.selfie_image,
+ liveness_checking_enabled: liveness_enabled,
+ instance_id: instance_id,
+ )
+
+ expect(result.success?).to eq(true)
+ expect(result.class).to eq(Acuant::Responses::ResponseWithPii)
+ end
+ end
+
+ context 'with liveness checking disabled' do
+ it 'sends an upload image request for the front and back DL images' do
+ result = subject.post_images(
+ front_image: DocAuthImageFixtures.document_front_image,
+ back_image: DocAuthImageFixtures.document_back_image,
+ selfie_image: DocAuthImageFixtures.selfie_image,
+ liveness_checking_enabled: liveness_enabled,
+ instance_id: instance_id,
+ )
+
+ expect(result.success?).to eq(true)
+ expect(result.class).to eq(Acuant::Responses::GetResultsResponse)
+ end
+ end
+
+ context 'when the results return failure' do
+ it 'returns a FormResponse with failure' do
+ url = URI.join(Figaro.env.acuant_assure_id_url, "/AssureIDService/Document/#{instance_id}")
+ stub_request(:get, url).to_return(body: AcuantFixtures.get_results_response_failure)
+
+ result = subject.post_images(
+ front_image: DocAuthImageFixtures.document_front_image,
+ back_image: DocAuthImageFixtures.document_back_image,
+ selfie_image: DocAuthImageFixtures.selfie_image,
+ liveness_checking_enabled: liveness_enabled,
+ instance_id: instance_id,
+ )
+
+ expect(result.success?).to eq(false)
+ expect(result).to be_kind_of(FormResponse)
+ end
end
end
@@ -157,6 +224,7 @@
expect(result.success?).to eq(true)
expect(result.errors).to eq([])
+ expect(result.class).to eq(Acuant::Response)
expect(get_face_stub).to have_been_requested
expect(facial_match_stub).to have_been_requested
expect(liveness_stub).to have_been_requested
diff --git a/spec/services/acuant_mock/acuant_mock_client_spec.rb b/spec/services/acuant_mock/acuant_mock_client_spec.rb
index b1f7ce1120f..b5daaca5cdf 100644
--- a/spec/services/acuant_mock/acuant_mock_client_spec.rb
+++ b/spec/services/acuant_mock/acuant_mock_client_spec.rb
@@ -109,4 +109,32 @@
expect(described_class.new.create_document).to_not eq('Create doc test')
end
+
+ context 'when checking results gives a failure' do
+ let(:instance_id) { '1234567890' }
+ before do
+ DocAuthMock::DocAuthMockClient.mock_response!(
+ method: :get_results,
+ response: OpenStruct.new(
+ 'success?' => false,
+ instance_id: instance_id,
+ errors: [
+ { back_image: 'blurry' },
+ ],
+ ),
+ )
+ end
+
+ it 'returns a failure response if the results failed' do
+ post_images_response = client.post_images(
+ instance_id: instance_id,
+ front_image: DocAuthImageFixtures.document_front_image,
+ back_image: DocAuthImageFixtures.document_back_image,
+ selfie_image: nil,
+ )
+
+ expect(post_images_response.success?).to eq(false)
+ expect(post_images_response).to be_kind_of(FormResponse)
+ end
+ end
end
diff --git a/spec/support/features/doc_auth_helper.rb b/spec/support/features/doc_auth_helper.rb
index e1fafa6731b..7d0e40c8d6d 100644
--- a/spec/support/features/doc_auth_helper.rb
+++ b/spec/support/features/doc_auth_helper.rb
@@ -188,9 +188,10 @@ def mock_general_doc_auth_client_error(method)
)
end
- def attach_images
+ def attach_images(liveness_enabled: true)
attach_file 'doc_auth_front_image', 'app/assets/images/logo.png'
attach_file 'doc_auth_back_image', 'app/assets/images/logo.png'
+ attach_file 'doc_auth_selfie_image', 'app/assets/images/logo.png' if liveness_enabled
end
def attach_front_image_data_url
@@ -214,7 +215,19 @@ def doc_auth_back_image_data_url
end
def doc_auth_back_image_data_url_data
- Base64.decode64(doc_auth_front_image_data_url.split(',').last)
+ Base64.decode64(doc_auth_back_image_data_url.split(',').last)
+ end
+
+ def attach_selfie_image_data_url
+ page.find('#doc_auth_selfie_image_data_url', visible: false).set(doc_auth_selfie_image_data_url)
+ end
+
+ def doc_auth_selfie_image_data_url
+ File.read('spec/support/fixtures/doc_auth_selfie_image_data_url.data')
+ end
+
+ def doc_auth_selfie_image_data_url_data
+ Base64.decode64(doc_auth_selfie_image_data_url.split(',').last)
end
def attach_image
diff --git a/spec/support/features/step_tags_helper.rb b/spec/support/features/step_tags_helper.rb
index 8131446fd35..48c4e2f1123 100644
--- a/spec/support/features/step_tags_helper.rb
+++ b/spec/support/features/step_tags_helper.rb
@@ -3,5 +3,10 @@ module StripTagsHelper
def strip_tags(*args)
ActionController::Base.helpers.strip_tags(*args)
end
+
+ def strip_nbsp(text)
+ nbsp = Nokogiri::HTML.parse(' ').text
+ text.gsub(/ /, ' ').gsub(/#{nbsp}/, ' ')
+ end
end
end
diff --git a/spec/support/fixtures/doc_auth_selfie_image_data_url.data b/spec/support/fixtures/doc_auth_selfie_image_data_url.data
new file mode 100644
index 00000000000..ca0ca47272b
--- /dev/null
+++ b/spec/support/fixtures/doc_auth_selfie_image_data_url.data
@@ -0,0 +1 @@
+data:image/jpeg;base64,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