diff --git a/.github/workflows/create-deploy-pr.yml b/.github/workflows/create-deploy-pr.yml new file mode 100644 index 00000000000..7027e598bb5 --- /dev/null +++ b/.github/workflows/create-deploy-pr.yml @@ -0,0 +1,32 @@ +name: Create deploy PR +on: + workflow_dispatch: + inputs: + deploy_type: + description: 'Type of deploy' + required: true + type: choice + options: + - Normal + - Patch + source: + description: 'Source branch/SHA (If blank, the current SHA running on staging will be used)' + required: false + type: string +permissions: + pull-requests: write + contents: write +jobs: + create-pr: + name: Create PR + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} + PATCH: ${{ inputs.deploy_type == 'Patch' && 1 || 0 }} + SOURCE: ${{ inputs.source }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Get all commits + - uses: ruby/setup-ruby@v1 + - run: scripts/create-deploy-pr diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml new file mode 100644 index 00000000000..9f9bd2fd323 --- /dev/null +++ b/.github/workflows/create-release.yml @@ -0,0 +1,18 @@ +name: Create release +run-name: "Create release based on ${{ github.event.pull_request.title }}" +on: + pull_request: + types: + - closed + branches: + - 'stages/prod' +jobs: + create-release: + name: Create release after PR merge + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v4 + - run: scripts/create-release ${{ github.event.pull_request.number }} \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index be562533971..7d2664d1461 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,7 +8,7 @@ variables: FF_SCRIPT_SECTIONS: 'true' JUNIT_OUTPUT: 'true' ECR_REGISTRY: '${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com' - IDP_CI_SHA: 'sha256:908cb207c214016f3e366b2ebbe89c2077cfe1d40f3b82ad8d79e58e0cec720b' + IDP_CI_SHA: 'sha256:756a1d450b422720dee36cb9a6217687bcad1e40b780219d360989861ce94212' PKI_IMAGE_TAG: 'main' DASHBOARD_IMAGE_TAG: 'main' @@ -163,17 +163,6 @@ check_changelog: exit 0 fi -check_content_freeze: - stage: test - script: |- - echo "Content change is not allowed during content freeze" - exit 1 - rules: - - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event" || $CI_PIPELINE_SOURCE == "external_pull_request_event" || $CI_PIPELINE_SOURCE == "web"' - changes: - compare_to: 'refs/heads/main' - paths: - - config/locales/**/en.yml specs: stage: test needs: diff --git a/.ruby-version b/.ruby-version index be94e6f53db..15a27998172 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2.2 +3.3.0 diff --git a/Gemfile b/Gemfile index 4b37b6aafd6..42c6e235359 100644 --- a/Gemfile +++ b/Gemfile @@ -16,11 +16,14 @@ gem 'aws-sdk-sns' gem 'aws-sdk-sqs' gem 'barby', '~> 0.6.8' gem 'base32-crockford' +gem 'base64' +gem 'bigdecimal' gem 'bootsnap', '~> 1.0', require: false gem 'browser' gem 'caxlsx', require: false gem 'concurrent-ruby' gem 'connection_pool' +gem 'csv' gem 'cssbundling-rails' gem 'devise', '~> 4.8' gem 'dotiw', '>= 4.0.1' @@ -101,7 +104,7 @@ group :development, :test do gem 'i18n-tasks', '~> 1.0' gem 'knapsack' gem 'listen' - gem 'nokogiri', '~> 1.14.0' + gem 'nokogiri', '~> 1.16.0' gem 'pg_query', require: false gem 'pry-byebug' gem 'pry-doc' diff --git a/Gemfile.lock b/Gemfile.lock index 3e41c25e321..d13d559d42c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -253,6 +253,7 @@ GEM addressable cssbundling-rails (1.0.0) railties (>= 6.0.0) + csv (3.2.8) date (3.3.4) dead_end (4.0.0) derailed_benchmarks (2.1.2) @@ -355,14 +356,14 @@ GEM rainbow (>= 2.2.2, < 4.0) terminal-table (>= 1.5.1) ice_nine (0.11.2) - io-console (0.6.0) - irb (1.9.1) + io-console (0.7.1) + irb (1.11.0) rdoc reline (>= 0.3.8) jmespath (1.6.2) jsbundling-rails (1.1.2) railties (>= 6.0.0) - json (2.7.0) + json (2.7.1) jwe (0.4.0) jwt (2.7.1) knapsack (4.0.0) @@ -427,15 +428,15 @@ GEM net-ssh (6.1.0) newrelic_rpm (9.7.0) nio4r (2.7.0) - nokogiri (1.14.5) - mini_portile2 (~> 2.8.0) + nokogiri (1.16.0) + mini_portile2 (~> 2.8.2) racc (~> 1.4) openssl (3.0.2) openssl-signature_algorithm (1.2.1) openssl (> 2.0, < 3.1) orm_adapter (0.5.0) parallel (1.23.0) - parser (3.2.2.4) + parser (3.3.0.0) ast (~> 2.4.1) racc pg (1.5.4) @@ -463,12 +464,12 @@ GEM pry-byebug (3.10.1) byebug (~> 11.0) pry (>= 0.13, < 0.15) - pry-doc (1.4.0) + pry-doc (1.5.0) pry (~> 0.11) yard (~> 0.9.11) pry-rails (0.3.9) pry (>= 0.10.4) - psych (5.1.1.1) + psych (5.1.2) stringio public_suffix (5.0.3) puma (6.4.2) @@ -481,7 +482,7 @@ GEM rack-cors (2.0.1) rack (>= 2.0.0) rack-headers_filter (0.0.1) - rack-mini-profiler (3.1.1) + rack-mini-profiler (3.3.0) rack (>= 1.2.0) rack-proxy (0.7.7) rack @@ -537,7 +538,7 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) - rdoc (6.6.0) + rdoc (6.6.2) psych (>= 4.0.0) redacted_struct (1.1.0) redcarpet (3.6.0) @@ -546,7 +547,7 @@ GEM redis-client (0.14.1) connection_pool regexp_parser (2.8.2) - reline (0.4.0) + reline (0.4.1) io-console (~> 0.5) request_store (1.5.1) rack (>= 1.4) @@ -658,7 +659,7 @@ GEM unf (~> 0.1.4) smart_properties (1.17.0) stringex (2.8.5) - stringio (3.0.9) + stringio (3.1.0) strong_migrations (1.6.4) activerecord (>= 5.2) subprocess (1.5.5) @@ -744,7 +745,9 @@ DEPENDENCIES axe-core-rspec (~> 4.2) barby (~> 0.6.8) base32-crockford + base64 better_errors (>= 2.5.1) + bigdecimal bootsnap (~> 1.0) brakeman browser @@ -755,6 +758,7 @@ DEPENDENCIES concurrent-ruby connection_pool cssbundling-rails + csv derailed_benchmarks devise (~> 4.8) dotiw (>= 4.0.1) @@ -788,7 +792,7 @@ DEPENDENCIES multiset net-sftp newrelic_rpm (~> 9.0) - nokogiri (~> 1.14.0) + nokogiri (~> 1.16.0) pg pg_query phonelib @@ -852,7 +856,7 @@ DEPENDENCIES zxcvbn (= 0.1.9) RUBY VERSION - ruby 3.2.2p53 + ruby 3.3.0p0 BUNDLED WITH 2.4.20 diff --git a/app/controllers/concerns/idv/document_capture_concern.rb b/app/controllers/concerns/idv/document_capture_concern.rb index aa0fda3665f..302bc509d58 100644 --- a/app/controllers/concerns/idv/document_capture_concern.rb +++ b/app/controllers/concerns/idv/document_capture_concern.rb @@ -32,22 +32,15 @@ def failure(message, extra = nil) # @param [DocAuth::Response, # DocumentCaptureSessionResult] response def extract_pii_from_doc(user, response, store_in_session: false) - pii_from_doc = response.pii_from_doc.merge( - uuid: user.uuid, - phone: user.phone_configurations.take&.phone, - uuid_prefix: ServiceProvider.find_by(issuer: sp_session[:issuer])&.app_id, - ) - if defined?(idv_session) # hybrid mobile does not have idv_session idv_session.had_barcode_read_failure = response.attention_with_barcode? if store_in_session - idv_session.pii_from_doc ||= {} - idv_session.pii_from_doc.merge!(pii_from_doc) + idv_session.pii_from_doc = response.pii_from_doc idv_session.selfie_check_performed = response.selfie_check_performed end end - track_document_issuing_state(user, pii_from_doc[:state]) + track_document_issuing_state(user, response.pii_from_doc[:state]) end def stored_result diff --git a/app/controllers/concerns/idv/phone_otp_sendable.rb b/app/controllers/concerns/idv/phone_otp_sendable.rb index 0d6c5c77b4f..31c5804f667 100644 --- a/app/controllers/concerns/idv/phone_otp_sendable.rb +++ b/app/controllers/concerns/idv/phone_otp_sendable.rb @@ -2,10 +2,6 @@ module Idv module PhoneOtpSendable extend ActiveSupport::Concern - included do - before_action :handle_locked_out_user - end - def send_phone_confirmation_otp send_phone_confirmation_otp_service.call end diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index be0e05b49eb..43dbe2b92c7 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -23,11 +23,14 @@ def shared_update idv_session.verify_info_step_document_capture_session_uuid = document_capture_session.uuid # proof_resolution job expects these values - pii[:uuid_prefix] = ServiceProvider.find_by(issuer: sp_session[:issuer])&.app_id - pii[:ssn] = idv_session.ssn - Idv::Agent.new(pii).proof_resolution( + agent_pii = pii.merge( + uuid: current_user.uuid, + uuid_prefix: ServiceProvider.find_by(issuer: sp_session[:issuer])&.app_id, + ssn: idv_session.ssn, + ) + Idv::Agent.new(agent_pii).proof_resolution( document_capture_session, - should_proof_state_id: aamva_state?(pii), + should_proof_state_id: aamva_state?, trace_id: amzn_trace_id, user_id: current_user.id, threatmetrix_session_id: idv_session.threatmetrix_session_id, @@ -44,10 +47,8 @@ def ipp_enrollment_in_progress? current_user.has_in_person_enrollment? end - def aamva_state?(pii) - IdentityConfig.store.aamva_supported_jurisdictions.include?( - pii['state_id_jurisdiction'], - ) + def aamva_state? + IdentityConfig.store.aamva_supported_jurisdictions.include?(pii['state_id_jurisdiction']) end def resolution_rate_limiter diff --git a/app/forms/idv/api_image_upload_form.rb b/app/forms/idv/api_image_upload_form.rb index 438794746d4..2eb3c2536d2 100644 --- a/app/forms/idv/api_image_upload_form.rb +++ b/app/forms/idv/api_image_upload_form.rb @@ -454,11 +454,18 @@ def store_failed_images(client_response, doc_pii_response) failed_back_fingerprint = nil unless errors_hash[:back]&.present? end document_capture_session. - store_failed_auth_image_fingerprint(failed_front_fingerprint, failed_back_fingerprint) + store_failed_auth_data( + front_image_fingerprint: failed_front_fingerprint, + back_image_fingerprint: failed_back_fingerprint, + doc_auth_success: client_response.doc_auth_success?, + selfie_success: client_response.selfie_success, + ) elsif doc_pii_response && !doc_pii_response.success? - document_capture_session.store_failed_auth_image_fingerprint( - extra_attributes[:front_image_fingerprint], - extra_attributes[:back_image_fingerprint], + document_capture_session.store_failed_auth_data( + front_image_fingerprint: extra_attributes[:front_image_fingerprint], + back_image_fingerprint: extra_attributes[:back_image_fingerprint], + doc_auth_success: client_response.doc_auth_success?, + selfie_success: client_response.selfie_success, ) end # retrieve updated data from session diff --git a/app/javascript/packages/document-capture/components/acuant-capture.scss b/app/javascript/packages/document-capture/components/acuant-capture.scss index 2ed7922a445..451142b77d3 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture.scss +++ b/app/javascript/packages/document-capture/components/acuant-capture.scss @@ -37,4 +37,12 @@ .document-capture-file-image--loading { @extend %pad-common-id-card; } + // Styles for the text that appears over the selfie capture screen to help users position their face for a good photo + .document-capture-selfie-feedback { + left: 50%; + top: 10%; + position: fixed; + transform: translateX(-50%); + z-index: 11; + } } diff --git a/app/javascript/packages/document-capture/components/acuant-capture.tsx b/app/javascript/packages/document-capture/components/acuant-capture.tsx index 8e61ba6e2b8..a9f22662c2b 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture.tsx +++ b/app/javascript/packages/document-capture/components/acuant-capture.tsx @@ -337,6 +337,7 @@ function AcuantCapture( const [attempt, incrementAttempt] = useCounter(1); const [acuantFailureCookie, setAcuantFailureCookie, refreshAcuantFailureCookie] = useCookie('AcuantCameraHasFailed'); + const [imageCaptureText, setImageCaptureText] = useState(''); // There's some pretty significant changes to this component when it's used for // selfie capture vs document image capture. This controls those changes. const selfieCapture = name === 'selfie'; @@ -653,6 +654,10 @@ function AcuantCapture( }); } + function onImageCaptureFeedback(text: string) { + setImageCaptureText(text); + } + return (
{isCapturingEnvironment && !selfieCapture && ( @@ -678,11 +683,13 @@ function AcuantCapture( onImageCaptureFailure={onSelfieCaptureFailure} onImageCaptureOpen={onSelfieCaptureOpen} onImageCaptureClose={onSelfieCaptureClosed} + onImageCaptureFeedback={onImageCaptureFeedback} > setIsCapturingEnvironment(false)} + imageCaptureText={imageCaptureText} /> )} diff --git a/app/javascript/packages/document-capture/components/acuant-selfie-camera.tsx b/app/javascript/packages/document-capture/components/acuant-selfie-camera.tsx index e031aef7008..29eb473aa2e 100644 --- a/app/javascript/packages/document-capture/components/acuant-selfie-camera.tsx +++ b/app/javascript/packages/document-capture/components/acuant-selfie-camera.tsx @@ -1,5 +1,6 @@ import { useContext, useEffect } from 'react'; import type { ReactNode } from 'react'; +import { t } from '@18f/identity-i18n'; import AcuantContext from '../context/acuant'; declare global { @@ -43,6 +44,11 @@ interface AcuantSelfieCameraContextProps { * when the fullscreen selfie capture page has been closed */ onImageCaptureClose: () => void; + /** + * Capture hint text from onDetection callback, tells the user + * why the acuant sdk cannot capture a selfie. + */ + onImageCaptureFeedback: (text: string) => void; /** * React children node */ @@ -63,8 +69,6 @@ interface FaceCaptureCallback { interface FaceDetectionStates { FACE_NOT_FOUND: string; TOO_MANY_FACES: string; - FACE_ANGLE_TOO_LARGE: string; - PROBABILITY_TOO_SMALL: string; FACE_TOO_SMALL: string; FACE_CLOSE_TO_BORDER: string; } @@ -74,6 +78,7 @@ function AcuantSelfieCamera({ onImageCaptureFailure = () => {}, onImageCaptureOpen = () => {}, onImageCaptureClose = () => {}, + onImageCaptureFeedback = () => {}, children, }: AcuantSelfieCameraContextProps) { const { isReady, setIsActive } = useContext(AcuantContext); @@ -85,7 +90,8 @@ function AcuantSelfieCamera({ // Until then, no actions are executed and the user sees only the camera stream. // You can opt to display an alert before the callback is triggered. }, - onDetection: () => { + onDetection: (text) => { + onImageCaptureFeedback(text); // Triggered when the face does not pass the scan. The UI element // should be updated here to provide guidence to the user }, @@ -104,6 +110,7 @@ function AcuantSelfieCamera({ }, onPhotoTaken: () => { // The photo has been taken and it's showing a preview with a button to accept or retake the image. + onImageCaptureFeedback(''); }, onPhotoRetake: () => { // Triggered when retake button is tapped @@ -115,12 +122,10 @@ function AcuantSelfieCamera({ }; const faceDetectionStates = { - FACE_NOT_FOUND: 'FACE NOT FOUND', - TOO_MANY_FACES: 'TOO MANY FACES', - FACE_ANGLE_TOO_LARGE: 'FACE ANGLE TOO LARGE', - PROBABILITY_TOO_SMALL: 'PROBABILITY TOO SMALL', - FACE_TOO_SMALL: 'FACE TOO SMALL', - FACE_CLOSE_TO_BORDER: 'TOO CLOSE TO THE FRAME', + FACE_NOT_FOUND: t('doc_auth.info.selfie_capture_status.face_not_found'), + TOO_MANY_FACES: t('doc_auth.info.selfie_capture_status.too_many_faces'), + FACE_TOO_SMALL: t('doc_auth.info.selfie_capture_status.face_too_small'), + FACE_CLOSE_TO_BORDER: t('doc_auth.info.selfie_capture_status.face_close_to_border'), }; const cleanupSelfieCamera = () => { window.AcuantPassiveLiveness.end(); diff --git a/app/javascript/packages/document-capture/components/acuant-selfie-capture-canvas.jsx b/app/javascript/packages/document-capture/components/acuant-selfie-capture-canvas.jsx index 6091567aabf..d89bed1d406 100644 --- a/app/javascript/packages/document-capture/components/acuant-selfie-capture-canvas.jsx +++ b/app/javascript/packages/document-capture/components/acuant-selfie-capture-canvas.jsx @@ -17,13 +17,20 @@ function FullScreenLoadingSpinner({ fullScreenRef, onRequestClose, fullScreenLab ); } -function AcuantSelfieCaptureCanvas({ fullScreenRef, onRequestClose, fullScreenLabel }) { +function AcuantSelfieCaptureCanvas({ + fullScreenRef, + onRequestClose, + fullScreenLabel, + imageCaptureText, +}) { const { isReady } = useContext(AcuantContext); // The Acuant SDK script AcuantPassiveLiveness attaches to whatever element has // this id. It then uses that element as the root for the full screen selfie capture const acuantCaptureContainerId = 'acuant-face-capture-container'; return isReady ? ( -
+
+

{imageCaptureText}

+
) : ( { - trackEvent('IdV: docauth not ready link clicked'); - forceRedirect( - addSearchParams(spName ? failureToProofURL : accountURL, { - step: currentStep, - location: 'not_ready', - }), - navigate, - ); - }; - - return ( - <> -

{t('doc_auth.not_ready.header')}

-

- {spName - ? t('doc_auth.not_ready.content_sp', { - sp_name: spName, - app_name: appName, - }) - : t('doc_auth.not_ready.content_nosp', { - app_name: appName, - })} -

- - - ); -} - -export default DocumentCaptureNotReady; diff --git a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx index ab1ba1f82fb..84c74fbcba0 100644 --- a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx +++ b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx @@ -6,7 +6,6 @@ import { useI18n } from '@18f/identity-react-i18n'; import type { FormStepComponentProps } from '@18f/identity-form-steps'; import UnknownError from './unknown-error'; import TipList from './tip-list'; -import DocumentCaptureNotReady from './document-capture-not-ready'; import { FeatureFlagContext } from '../context'; import DocumentCaptureAbandon from './document-capture-abandon'; import { @@ -36,8 +35,7 @@ function DocumentCaptureReviewIssues({ hasDismissed, }: DocumentCaptureReviewIssuesProps) { const { t } = useI18n(); - const { notReadySectionEnabled, exitQuestionSectionEnabled, selfieCaptureEnabled } = - useContext(FeatureFlagContext); + const { exitQuestionSectionEnabled, selfieCaptureEnabled } = useContext(FeatureFlagContext); const defaultSideProps = { registerField, @@ -74,7 +72,6 @@ function DocumentCaptureReviewIssues({ )} - {notReadySectionEnabled && } {exitQuestionSectionEnabled && } diff --git a/app/javascript/packages/document-capture/components/documents-step.tsx b/app/javascript/packages/document-capture/components/documents-step.tsx index 8bb2d7fd0e5..2b78b104426 100644 --- a/app/javascript/packages/document-capture/components/documents-step.tsx +++ b/app/javascript/packages/document-capture/components/documents-step.tsx @@ -12,7 +12,6 @@ import DocumentSideAcuantCapture from './document-side-acuant-capture'; import DeviceContext from '../context/device'; import UploadContext from '../context/upload'; import TipList from './tip-list'; -import DocumentCaptureNotReady from './document-capture-not-ready'; import { FeatureFlagContext } from '../context'; import DocumentCaptureAbandon from './document-capture-abandon'; @@ -110,8 +109,7 @@ function DocumentsStep({ const { isMobile } = useContext(DeviceContext); const { isLastStep } = useContext(FormStepsContext); const { flowPath } = useContext(UploadContext); - const { notReadySectionEnabled, exitQuestionSectionEnabled, selfieCaptureEnabled } = - useContext(FeatureFlagContext); + const { exitQuestionSectionEnabled, selfieCaptureEnabled } = useContext(FeatureFlagContext); const pageHeaderText = selfieCaptureEnabled ? t('doc_auth.headings.document_capture_with_selfie') @@ -142,7 +140,6 @@ function DocumentsStep({ )} {isLastStep ? : } - {notReadySectionEnabled && } {exitQuestionSectionEnabled && } diff --git a/app/javascript/packages/document-capture/context/feature-flag.tsx b/app/javascript/packages/document-capture/context/feature-flag.tsx index 8e9ad79ef83..ecd39bed17d 100644 --- a/app/javascript/packages/document-capture/context/feature-flag.tsx +++ b/app/javascript/packages/document-capture/context/feature-flag.tsx @@ -1,11 +1,6 @@ import { createContext } from 'react'; export interface FeatureFlagContextProps { - /** - * Specify whether to show the not-ready section on doc capture screen. - * Populated from backend configuration - */ - notReadySectionEnabled: boolean; /** * Specify whether to show exit optional questions on doc capture screen. */ @@ -17,7 +12,6 @@ export interface FeatureFlagContextProps { } const FeatureFlagContext = createContext({ - notReadySectionEnabled: false, exitQuestionSectionEnabled: false, selfieCaptureEnabled: false, }); diff --git a/app/javascript/packs/document-capture.tsx b/app/javascript/packs/document-capture.tsx index 93b4230e66b..01e37996959 100644 --- a/app/javascript/packs/document-capture.tsx +++ b/app/javascript/packs/document-capture.tsx @@ -37,7 +37,6 @@ interface AppRootData { securityAndPrivacyHowItWorksUrl: string; skipDocAuth: string; howToVerifyURL: string; - uiNotReadySectionEnabled: string; uiExitQuestionSectionEnabled: string; } @@ -105,7 +104,6 @@ const { usStatesTerritories = '', skipDocAuth, howToVerifyUrl, - uiNotReadySectionEnabled = '', uiExitQuestionSectionEnabled = '', } = appRoot.dataset as DOMStringMap & AppRootData; @@ -189,7 +187,6 @@ const App = composeComponents( FeatureFlagContext.Provider, { value: { - notReadySectionEnabled: String(uiNotReadySectionEnabled) === 'true', exitQuestionSectionEnabled: String(uiExitQuestionSectionEnabled) === 'true', selfieCaptureEnabled: getSelfieCaptureEnabled(), }, diff --git a/app/jobs/reports/irs_weekly_summary_report.rb b/app/jobs/reports/irs_weekly_summary_report.rb deleted file mode 100644 index cae28a2c1cd..00000000000 --- a/app/jobs/reports/irs_weekly_summary_report.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'csv' - -module Reports - class IrsWeeklySummaryReport < BaseReport - attr_reader :report_date - REPORT_NAME = 'irs-weekly-summary-report' - - def perform(report_date) - @name = REPORT_NAME - @report_date = report_date - - email = IdentityConfig.store.system_demand_report_email - ReportMailer.system_demand_report( - email: email, - data: generate_csv, - name: REPORT_NAME, - ).deliver_now - - # save report has a predefined bucket where things get saved - # upload_file_to_s3_bucket can be used to define specific buckets (ie. public/private) - save_report( - REPORT_NAME, - generate_csv, - extension: 'csv', - ) - end - - private - - # The total number of users registered with Login.gov - def query_system_demand - User.where('created_at <= ?', report_date.beginning_of_day).count - end - - def generate_csv - CSV.generate do |csv| - csv << [ - 'Data Requested', - 'Total Count', - ] - csv << [ - 'System Demand', - query_system_demand, - ] - end - end - end -end diff --git a/app/mailers/report_mailer.rb b/app/mailers/report_mailer.rb index 47a32155ce6..bfe7f2d475c 100644 --- a/app/mailers/report_mailer.rb +++ b/app/mailers/report_mailer.rb @@ -16,12 +16,6 @@ def deleted_user_accounts_report(email:, name:, issuers:, data:) mail(to: email, subject: t('report_mailer.deleted_accounts_report.subject')) end - def system_demand_report(email:, data:, name:) - @name = name - attachments['system_demand.csv'] = data - mail(to: email, subject: t('report_mailer.system_demand_report.subject')) - end - def warn_error(email:, error:, env: Rails.env) @error = error mail(to: email, subject: "[#{env}] identity-idp error: #{error.class.name}") diff --git a/app/models/document_capture_session.rb b/app/models/document_capture_session.rb index 867ec34fe68..d318fffe386 100644 --- a/app/models/document_capture_session.rb +++ b/app/models/document_capture_session.rb @@ -17,6 +17,9 @@ def store_result_from_response(doc_auth_response) session_result.captured_at = Time.zone.now session_result.attention_with_barcode = doc_auth_response.attention_with_barcode? session_result.selfie_check_performed = doc_auth_response.selfie_check_performed? + session_result.doc_auth_success = doc_auth_response.doc_auth_success? + # nil(selfie not required) or true/false + session_result.selfie_success = doc_auth_response.selfie_success EncryptedRedisStructStorage.store( session_result, expires_in: IdentityConfig.store.doc_capture_request_valid_for_minutes.minutes.seconds.to_i, @@ -25,12 +28,15 @@ def store_result_from_response(doc_auth_response) save! end - def store_failed_auth_image_fingerprint(front_image_fingerprint, back_image_fingerprint) + def store_failed_auth_data(front_image_fingerprint:, back_image_fingerprint:, doc_auth_success:, + selfie_success:) session_result = load_result || DocumentCaptureSessionResult.new( id: generate_result_id, ) session_result.success = false session_result.captured_at = Time.zone.now + session_result.doc_auth_success = doc_auth_success + session_result.selfie_success = selfie_success session_result.add_failed_front_image!(front_image_fingerprint) if front_image_fingerprint session_result.add_failed_back_image!(back_image_fingerprint) if back_image_fingerprint EncryptedRedisStructStorage.store( diff --git a/app/services/doc_auth/acuant/responses/get_results_response.rb b/app/services/doc_auth/acuant/responses/get_results_response.rb index 1b32808bdcc..eb6ca35d34b 100644 --- a/app/services/doc_auth/acuant/responses/get_results_response.rb +++ b/app/services/doc_auth/acuant/responses/get_results_response.rb @@ -43,6 +43,10 @@ def attention_with_barcode? end end + def doc_auth_success? + passed_result? + end + private attr_reader :http_response @@ -67,6 +71,8 @@ def create_response_info tamper_result: tamper_result_code&.name, classification_info: classification_info, address_line2_present: !pii_from_doc[:address2].blank?, + doc_auth_success: doc_auth_success?, + selfie_success: nil, } end diff --git a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb index c9c3c2244c6..0bdf08488c2 100644 --- a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb +++ b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb @@ -142,6 +142,23 @@ def billed? !!doc_auth_result end + def doc_auth_success? + transaction_status_passed? && + true_id_product.present? && + product_status_passed? && + doc_auth_result_passed? + end + + # @return [Boolean, nil] + # When selfie result is missing, return nil + # Otherwise: + # return true if selfie check result == 'Pass' + # return false + def selfie_success + return selfie_result if selfie_result.nil? + selfie_result == 'Pass' + end + private def conversation_id @@ -209,7 +226,7 @@ def create_response_info processed_alerts: alerts, alert_failure_count: alerts[:failed]&.count.to_i, log_alert_results: log_alert_formatter.log_alerts(alerts), - portrait_match_results: true_id_product[:PORTRAIT_MATCH_RESULT], + portrait_match_results: true_id_product&.dig(:PORTRAIT_MATCH_RESULT), image_metrics: parse_image_metrics, address_line2_present: !pii_from_doc[:address2].blank?, classification_info: classification_info, @@ -232,6 +249,10 @@ def all_passed? doc_auth_result_passed? end + def selfie_result + response_info&.dig(:portrait_match_results, :FaceMatchResult) + end + def product_status_passed? product_status == 'pass' end @@ -294,6 +315,7 @@ def parsed_alerts return @new_alerts if defined?(@new_alerts) @new_alerts = { passed: [], failed: [] } + return @new_alerts unless true_id_product&.dig(:AUTHENTICATION_RESULT).present? all_alerts = true_id_product[:AUTHENTICATION_RESULT].select do |key| key.start_with?('Alert_') end @@ -334,7 +356,7 @@ def combine_alert_data(all_alerts, alert_name, region_details) def parse_image_metrics image_metrics = {} - + return image_metrics unless true_id_product&.dig(:ParameterDetails).present? true_id_product[:ParameterDetails].each do |detail| next unless detail[:Group] == 'IMAGE_METRICS_RESULT' diff --git a/app/services/doc_auth/mock/result_response.rb b/app/services/doc_auth/mock/result_response.rb index cbe64c22810..fd3d6524655 100644 --- a/app/services/doc_auth/mock/result_response.rb +++ b/app/services/doc_auth/mock/result_response.rb @@ -130,6 +130,15 @@ def self.create_network_error_response ) end + def doc_auth_success? + doc_auth_result_from_uploaded_file == 'Passed' || errors.blank? + end + + def selfie_success + return nil if portrait_match_results&.dig(:FaceMatchResult).nil? + portrait_match_results[:FaceMatchResult] == 'Pass' + end + private def parsed_alerts diff --git a/app/services/doc_auth/response.rb b/app/services/doc_auth/response.rb index 3b6ea9d0532..f002df6c97f 100644 --- a/app/services/doc_auth/response.rb +++ b/app/services/doc_auth/response.rb @@ -56,6 +56,8 @@ def to_h exception: exception, attention_with_barcode: attention_with_barcode?, doc_type_supported: doc_type_supported?, + doc_auth_success: doc_auth_success?, + selfie_success: selfie_success, }.merge(extra) end @@ -77,5 +79,14 @@ def network_error? def selfie_check_performed? @selfie_check_performed end + + def selfie_success + # to be implemented by concrete subclass + end + + def doc_auth_success? + # to be implemented by concrete subclass + false + end end end diff --git a/app/services/document_capture_session_result.rb b/app/services/document_capture_session_result.rb index 1acb7f10458..9b9f0f560bd 100644 --- a/app/services/document_capture_session_result.rb +++ b/app/services/document_capture_session_result.rb @@ -10,9 +10,11 @@ :failed_back_image_fingerprints, :captured_at, :selfie_check_performed, + :doc_auth_success, :selfie_success, keyword_init: true, allowed_members: [:id, :success, :attention_with_barcode, :failed_front_image_fingerprints, - :failed_back_image_fingerprints, :captured_at, :selfie_check_performed], + :failed_back_image_fingerprints, :captured_at, :selfie_check_performed, + :doc_auth_success, :selfie_success] ) do def self.redis_key_prefix 'dcs:result' diff --git a/app/views/idv/in_person/ssn/show.html.erb b/app/views/idv/in_person/ssn/show.html.erb deleted file mode 100644 index 519a01a623b..00000000000 --- a/app/views/idv/in_person/ssn/show.html.erb +++ /dev/null @@ -1,88 +0,0 @@ -<%# -Renders a page asking the user to enter their SSN or update their SSN if they had previously entered it. - -locals: -* updating_ssn: true if the user is updating their SSN instead of providing it for the first time. This - will render a different page heading and different navigation buttons in the page footer -* threatmetrix_session_id: A session identifier needed by the ThreatMetrix tool -* threatmetrix_javascript_urls:: URLs to add to script tags to load the ThreatMetrix javascript -* threatmetrix_iframe_url: A URL to add to the page for Threatmetrix -%> - -<% content_for(:pre_flash_content) do %> - <%= render StepIndicatorComponent.new( - steps: Idv::Flows::InPersonFlow::STEP_INDICATOR_STEPS, - current_step: :verify_info, - locale_scope: 'idv', - class: 'margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4', - ) %> -<% end %> - -<% self.title = t('titles.doc_auth.ssn') %> - -<% if updating_ssn %> - <%= render PageHeadingComponent.new.with_content(t('doc_auth.headings.ssn_update')) %> -<% else %> - <%= render PageHeadingComponent.new.with_content(t('doc_auth.headings.ssn')) %> -<% end %> - -

- <%= t('doc_auth.info.ssn') %> - <%= new_tab_link_to(MarketingSite.security_and_privacy_practices_url, class: 'display-inline') do %> - <%= t('doc_auth.info.learn_more') %> - <% end %> -

- -<% if FeatureManagement.proofing_device_profiling_collecting_enabled? %> - <% if threatmetrix_session_id.present? %> - <% threatmetrix_javascript_urls.each do |threatmetrix_javascript_url| %> - <%= javascript_include_tag threatmetrix_javascript_url, nonce: true %> - <% end %> - - <% end %> -<% end %> - -<% if IdentityConfig.store.proofer_mock_fallback %> -
-
-

- <%= t('doc_auth.instructions.test_ssn') %> -

-
-
-<% end %> - -<%= simple_form_for( - Idv::SsnFormatForm.new(current_user, nil), - url: url_for, - method: :put, - html: { autocomplete: 'off' }, - ) do |f| %> -
- <%= render 'shared/ssn_field', f: f %> -
- -

<%= @error_message %>

- -<%= f.submit class: 'display-block margin-y-5' do %> - <% if updating_ssn %> - <%= t('forms.buttons.submit.update') %> - <% else %> - <%= t('forms.buttons.continue') %> - <% end %> - <% end %> -<% end %> - -<% if updating_ssn %> - <%= render 'idv/shared/back', fallback_path: idv_in_person_verify_info_path %> -<% else %> - <%= render 'idv/doc_auth/cancel', step: 'ssn' %> -<% end %> - diff --git a/app/views/idv/shared/_document_capture.html.erb b/app/views/idv/shared/_document_capture.html.erb index abf399a02bd..ee6d416e0eb 100644 --- a/app/views/idv/shared/_document_capture.html.erb +++ b/app/views/idv/shared/_document_capture.html.erb @@ -39,7 +39,6 @@ doc_auth_selfie_capture: FeatureManagement.idv_allow_selfie_check? && doc_auth_selfie_capture, skip_doc_auth: skip_doc_auth, how_to_verify_url: idv_how_to_verify_url, - ui_not_ready_section_enabled: IdentityConfig.store.doc_auth_not_ready_section_enabled, ui_exit_question_section_enabled: IdentityConfig.store.doc_auth_exit_question_section_enabled, } %> <%= simple_form_for( diff --git a/app/views/report_mailer/system_demand_report.html.erb b/app/views/report_mailer/system_demand_report.html.erb deleted file mode 100644 index 1a58e539b16..00000000000 --- a/app/views/report_mailer/system_demand_report.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= t('report_mailer.system_demand_report.name') %>: <%= @name %>
diff --git a/config/application.yml.default b/config/application.yml.default index 45bd6da339a..85d074759e5 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -87,7 +87,6 @@ doc_auth_exit_question_section_enabled: false doc_auth_max_attempts: 5 doc_auth_max_capture_attempts_before_native_camera: 3 doc_auth_max_submission_attempts_before_native_camera: 3 -doc_auth_not_ready_section_enabled: false doc_auth_selfie_capture_enabled: false doc_auth_sdk_capture_orientation: '{"horizontal": 100, "vertical": 0}' doc_auth_supported_country_codes: '["US", "GU", "VI", "AS", "MP", "PR", "USA" ,"GUM", "VIR", "ASM", "MNP", "PRI"]' @@ -314,7 +313,6 @@ sp_handoff_bounce_max_seconds: 2 show_unsupported_passkey_platform_authentication_setup: false show_user_attribute_deprecation_warnings: false otp_min_attempts_remaining_warning_count: 3 -system_demand_report_email: 'foo@bar.com' sp_issuer_user_counts_report_configs: '[]' team_ada_email: '' team_all_login_emails: '[]' @@ -369,6 +367,7 @@ development: attribute_encryption_key_queue: '[{ "key": "11111111111111111111111111111111" }, { "key": "22222222222222222222222222222222" }]' aws_logo_bucket: '' component_previews_enabled: true + component_previews_embed_frame_ancestors: '["http://localhost:4000"]' dashboard_api_token: test_token dashboard_url: http://localhost:3001/api/service_providers database_host: '' @@ -383,7 +382,6 @@ development: database_worker_jobs_host: '' database_worker_jobs_password: '' doc_auth_exit_question_section_enabled: false - doc_auth_not_ready_section_enabled: false doc_auth_selfie_capture_enabled: false doc_auth_vendor: 'mock' doc_auth_vendor_randomize: false diff --git a/config/initializers/job_configurations.rb b/config/initializers/job_configurations.rb index 29d3b053ffd..f6d21134c13 100644 --- a/config/initializers/job_configurations.rb +++ b/config/initializers/job_configurations.rb @@ -146,12 +146,6 @@ class: 'ThreatMetrixJsVerificationJob', cron: cron_1h, }, - # Weekly IRS report returning system demand - irs_weekly_summary_report: { - class: 'Reports::IrsWeeklySummaryReport', - cron: cron_1w, - args: -> { [Time.zone.now] }, - }, # Reject profiles that have been in fraud_review_pending for 30 days fraud_rejection: { class: 'FraudRejectionDailyJob', diff --git a/config/locales/doc_auth/en.yml b/config/locales/doc_auth/en.yml index 6334869c8bc..3bc08d364c2 100644 --- a/config/locales/doc_auth/en.yml +++ b/config/locales/doc_auth/en.yml @@ -220,6 +220,11 @@ en: secure_account: We’ll encrypt your account with your password. Encryption means your data is protected and only you will be able to access or change your information. + selfie_capture_status: + face_close_to_border: TOO CLOSE TO THE FRAME + face_not_found: FACE NOT FOUND + face_too_small: FACE TOO SMALL + too_many_faces: TOO MANY FACES ssn: We need your Social Security number to verify your name, date of birth and address. tag: Recommended @@ -263,15 +268,6 @@ en: - 'Verify by mail: We’ll mail a letter to your home address. This takes 5 to 10 days.' welcome: 'You will need your:' - not_ready: - button_nosp: Cancel and return to your profile - button_sp: Exit %{app_name} and return to %{sp_name} - content_nosp: If you exit %{app_name} now, you will not have verified your - identity. You can return later to finish this process. - content_sp: If you exit %{app_name} now and return to %{sp_name}, you will not - have verified your identity. You can return later to finish this - process. - header: Not ready to add photos? tips: document_capture_hint: Must be a JPG or PNG document_capture_id_text1: Use a dark background diff --git a/config/locales/doc_auth/es.yml b/config/locales/doc_auth/es.yml index ac12ab0e65f..9aa65026e33 100644 --- a/config/locales/doc_auth/es.yml +++ b/config/locales/doc_auth/es.yml @@ -258,6 +258,11 @@ es: secure_account: Vamos a encriptar su cuenta con su contraseña. La encriptación significa que sus datos están protegidos y solo usted podrá acceder o modificar su información. + selfie_capture_status: + face_close_to_border: DEMASIADO CERCA DEL MARCO + face_not_found: NO SE ENCONTRÓ LA CARA + face_too_small: LA CARA ES DEMASIADO CHICA + too_many_faces: HAY DEMASIADAS CARAS ssn: Necesitamos su número de Seguro Social para validar su nombre, fecha de nacimiento y dirección. tag: Recomendado @@ -306,15 +311,6 @@ es: - 'Verificar por correo: Le enviaremos una carta a su domicilio. Esto tarda entre 5 y 10 días.' welcome: 'Necesitará su:' - not_ready: - button_nosp: Cancelar y volver a su perfil - button_sp: Salir de %{app_name} y volver a %{sp_name} - content_nosp: Si sale ahora de %{app_name}, no habrá verificado su identidad. - Puede volver más tarde para completar este proceso. - content_sp: Si sale ahora de %{app_name} y regresa a %{sp_name}, no habrá - verificado su identidad. Puede volver más tarde para completar este - proceso. - header: ¿No está listo para enviar las fotos? tips: document_capture_hint: Debe ser un JPG o PNG document_capture_id_text1: Use un fondo oscuro diff --git a/config/locales/doc_auth/fr.yml b/config/locales/doc_auth/fr.yml index 846a3de8e9e..f4111c0fe0b 100644 --- a/config/locales/doc_auth/fr.yml +++ b/config/locales/doc_auth/fr.yml @@ -267,6 +267,11 @@ fr: secure_account: Nous chiffrerons votre compte avec votre mot de passe. Le chiffrage signifie que vos données sont protégées et que vous êtes le/la seul(e) à pouvoir accéder à vos informations ou les modifier. + selfie_capture_status: + face_close_to_border: TROP PRÈS DU CADRE + face_not_found: VISAGE NON TROUVÉ + face_too_small: VISAGE TROP PETIT + too_many_faces: TROP DE VISAGES ssn: Nous avons besoin de votre numéro de sécurité sociale pour vérifier votre nom, date de naissance et adresse. tag: Recommandation @@ -318,15 +323,6 @@ fr: lettre à votre adresse personnelle. Cela prend 5 à 10 jours.' welcome: 'Vous aurez besoin de votre:' - not_ready: - button_nosp: Annuler et revenir à votre profil - button_sp: Quittez %{app_name} et retournez à %{sp_name} - content_nosp: Si vous quittez %{app_name}, votre identité n’aura pas été - vérifiée. Vous pourrez revenir plus tard pour terminer ce processus. - content_sp: Si vous quittez %{app_name} maintenant et revenez sur %{sp_name}, - votre identité n’aura pas été vérifiée. Vous pourrez revenir plus tard - pour terminer ce processus. - header: Vous n’êtes pas prêt à ajouter des photos? tips: document_capture_hint: Doit être un JPG ou PNG document_capture_id_text1: Utilisez un fond sombre diff --git a/config/locales/report_mailer/en.yml b/config/locales/report_mailer/en.yml index 25737426436..f71e93c2ebb 100644 --- a/config/locales/report_mailer/en.yml +++ b/config/locales/report_mailer/en.yml @@ -5,6 +5,3 @@ en: issuers: Issuers name: Name subject: Deleted accounts report - system_demand_report: - name: Name - subject: System demand report diff --git a/config/locales/report_mailer/es.yml b/config/locales/report_mailer/es.yml index 05922400d5e..328c1e881b4 100644 --- a/config/locales/report_mailer/es.yml +++ b/config/locales/report_mailer/es.yml @@ -5,6 +5,3 @@ es: issuers: Emisores name: Nombre subject: Informe de cuentas eliminadas - system_demand_report: - name: Nombre - subject: Informe de demanda del sistema diff --git a/config/locales/report_mailer/fr.yml b/config/locales/report_mailer/fr.yml index 098761d4ace..4764291a220 100644 --- a/config/locales/report_mailer/fr.yml +++ b/config/locales/report_mailer/fr.yml @@ -5,6 +5,3 @@ fr: issuers: Émetteurs name: Nom subject: Rapport sur les comptes supprimés - system_demand_report: - name: Nom - subject: Rapport de demande du système diff --git a/dockerfiles/idp_ci.Dockerfile b/dockerfiles/idp_ci.Dockerfile index 2b020e22469..dca69ddb1fe 100644 --- a/dockerfiles/idp_ci.Dockerfile +++ b/dockerfiles/idp_ci.Dockerfile @@ -1,4 +1,4 @@ -FROM public.ecr.aws/docker/library/ruby:3.2.2-bullseye +FROM public.ecr.aws/docker/library/ruby:3.3.0-bullseye ENV NODE_MAJOR 20 diff --git a/dockerfiles/idp_review_app.Dockerfile b/dockerfiles/idp_review_app.Dockerfile index a163885d665..839115cc1d1 100644 --- a/dockerfiles/idp_review_app.Dockerfile +++ b/dockerfiles/idp_review_app.Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:3.2.2-slim +FROM ruby:3.3.0-slim # Set environment variables ARG ARG_CI_ENVIRONMENT_SLUG="placeholder" diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 161caa0b550..41ce1602b58 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -182,7 +182,6 @@ def self.build_store(config_map) config.add(:doc_auth_error_glare_threshold, type: :integer) config.add(:doc_auth_error_sharpness_threshold, type: :integer) config.add(:doc_auth_exit_question_section_enabled, type: :boolean) - config.add(:doc_auth_not_ready_section_enabled, type: :boolean) config.add(:doc_auth_max_attempts, type: :integer) config.add(:doc_auth_max_capture_attempts_before_native_camera, type: :integer) config.add(:doc_auth_max_submission_attempts_before_native_camera, type: :integer) @@ -452,7 +451,6 @@ def self.build_store(config_map) config.add(:sp_handoff_bounce_max_seconds, type: :integer) config.add(:sp_issuer_user_counts_report_configs, type: :json) config.add(:state_tracking_enabled, type: :boolean) - config.add(:system_demand_report_email, type: :string) config.add(:team_ada_email, type: :string) config.add(:team_all_login_emails, type: :json) config.add(:team_daily_reports_emails, type: :json) diff --git a/scripts/create-deploy-pr b/scripts/create-deploy-pr new file mode 100755 index 00000000000..a8623193067 --- /dev/null +++ b/scripts/create-deploy-pr @@ -0,0 +1,112 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ORIGIN=${ORIGIN:-origin} +SOURCE=${SOURCE:-} +DEPLOY_BRANCH=stages/prod +PATCH=${PATCH:-} +DRY_RUN=${DRY_RUN:-0} +CHANGELOG_FILE=${CHANGELOG_FILE:-.rc-changelog.md} + +function get_last_rc { + GH_OUTPUT=$(gh release list --exclude-drafts --exclude-pre-releases --limit 1 || true) + if [ -z "$GH_OUTPUT" ]; then + echo "Failed to get latest released" >&2 + exit 1 + fi + + LAST_RC=$(echo "$GH_OUTPUT" | grep -E --only-matching 'RC [0-9]+(\.[0-9]+)?' | sed 's/RC //') + if [ -z "$LAST_RC" ]; then + echo 0 + else + echo "$LAST_RC" + fi +} + +function get_next_rc { + LAST_RC="$1"; shift + MAJOR=$(echo "$LAST_RC" | sed -E 's/\.[0-9]+//') + MINOR=$(echo "$LAST_RC" | sed -E 's/[0-9]+(\.|$)//') + + if [ "$PATCH" == "1" ]; then + # Doing a patch, so increment minor version by 1 + if [ -z "$MINOR" ]; then + MINOR=0 + fi + + MINOR=$((MINOR + 1)) + else + # Not doing a patch, clear minor and increment major + MAJOR=$((MAJOR + 1)) + MINOR=0 + fi + + if [ "$MINOR" == "0" ]; then + echo "$MAJOR" + else + echo "$MAJOR.$MINOR" + fi +} + +function get_staging_sha { + curl --silent https://idp.staging.login.gov/api/deploy.json | jq -r .git_sha +} + +if [ -z "${CI:-}" ]; then + echo "This script is meant to be run in a continuous integration environment." + exit 1 +fi + +if [ -z "${GH_TOKEN:-}" ] && [ "$DRY_RUN" == "0" ]; then + echo "You must set the GH_TOKEN environment variable." + exit 1 +fi + +RC_BRANCH=stages/rc-$(date +'%Y-%m-%d') +if git rev-parse "$ORIGIN/$RC_BRANCH" > /dev/null 2>&1; then + echo "RC branch $RC_BRANCH already exists. Delete that branch and re-run this workflow to create a PR." >&2 + exit 1 +fi + +LAST_RC=$(get_last_rc) +NEXT_RC=$(get_next_rc "$LAST_RC") +echo "Last RC was ${LAST_RC}. The next RC will be ${NEXT_RC}." + +if [ -z "$SOURCE" ]; then + SHA=$(get_staging_sha) + echo "Staging currently running ${SHA}" +else + SHA=$(git rev-parse "$SOURCE" || true) + if [ -z $SHA ]; then + echo "Invalid source: '$SOURCE'" + exit 17 + elif [ "$SOURCE" == "$SHA" ]; then + echo "Using $SHA as the source" + else + echo "Using '$SOURCE' ($SHA) as the source" + fi +fi + +echo "Building changelog..." +scripts/changelog_check.rb -s "$SHA" -b "${ORIGIN}/${DEPLOY_BRANCH}" > "$CHANGELOG_FILE" + +if [[ $DRY_RUN -eq 0 ]]; then + echo "Pushing $RC_BRANCH to origin..." + git push $ORIGIN "$SHA:refs/heads/$RC_BRANCH" + + # Create PR + echo "Creating PR..." + gh pr create \ + --title "Deploy RC ${NEXT_RC} to Production" \ + --label 'status - promotion' \ + --base "$DEPLOY_BRANCH" \ + --head "$RC_BRANCH" \ + --body-file "$CHANGELOG_FILE" +else + echo "Dry run. Not creating PR." +fi + +echo "# Changelog" +cat "$CHANGELOG_FILE" && rm "$CHANGELOG_FILE" + diff --git a/scripts/create-release b/scripts/create-release new file mode 100755 index 00000000000..92ef2a73e42 --- /dev/null +++ b/scripts/create-release @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DEPLOY_BRANCH=stages/prod +PR_JSON_FILE=${PR_JSON_FILE:-.pr.json} +CHANGELOG_FILE=${CHANGELOG_FILE:-.changelog.md} + +USAGE=" +${0} [PULL_REQUEST_NUMBER] + +Creates a new release based on the given PR having been merged. +" + +if [ $# -eq 0 ]; then + echo $USAGE + exit 1 +fi + +PR="$1"; shift + +if [ -z "${CI:-}" ]; then + echo "This script is meant to be run in a continuous integration environment." + exit 1 +fi + +if [ -z "${GH_TOKEN:-}" ]; then + echo "You must set the GH_TOKEN environment variable." + exit 1 +fi + + +echo "Getting PR ${PR} data..." +gh pr list \ + --json number,title,body \ + --base "$DEPLOY_BRANCH" \ + --state merged \ + jq ".[] | select(.number == ${PR})" > "$PR_JSON_FILE" + +if [ ! -s "$PR_JSON_FILE" ]; then + echo "PR $PR not found." + exit 9 +fi + +RC=$(jq --raw-output '.title' < "$PR_JSON_FILE" | sed -E 's/Deploy RC (.+) to .*/\1/') +jq --raw-output '.body' < "$PR_JSON_FILE" > "$CHANGELOG_FILE" +TITLE="RC $RC" + +echo "Checking for existing release '$TITLE'..." +EXISTING_RELEASE=$(gh release list --exclude-drafts | (grep "$TITLE" || true)) + +if [ ! -z "$EXISTING_RELEASE" ]; then + echo "❌ Release already exists: $TITLE" >&2 + exit 10 +else + echo "No existing release found." +fi + +TAG=$(date -u +'%Y-%m-%dT%H%M%S') + +echo "Creating release $TITLE with tag $TAG..." +gh release create \ + "$TAG" \ + --latest \ + --target "$GITHUB_SHA" \ + --title "$TITLE" \ + --notes-file "$CHANGELOG_FILE" diff --git a/spec/components/previews/login_button_component_preview.rb b/spec/components/previews/login_button_component_preview.rb index bef99f6f771..ff36acdc8cd 100644 --- a/spec/components/previews/login_button_component_preview.rb +++ b/spec/components/previews/login_button_component_preview.rb @@ -5,9 +5,8 @@ def default end # @!endgroup - - # @param big toggle - # @param color select [~,primary,primary-darker,primary-lighter] + # @param big toggle "Change button size" + # @param color select [primary,primary-darker,primary-lighter] "Select button color" def workbench( big: false, color: 'primary' diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb index e1b61ae7e99..d762b5fc16e 100644 --- a/spec/controllers/idv/image_uploads_controller_spec.rb +++ b/spec/controllers/idv/image_uploads_controller_spec.rb @@ -434,6 +434,8 @@ front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), doc_type_supported: boolean, + doc_auth_success: boolean, + selfie_success: nil, ) expect(@analytics).to receive(:track_event).with( @@ -610,6 +612,8 @@ front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), doc_type_supported: boolean, + doc_auth_success: boolean, + selfie_success: nil, ) expect(@analytics).to receive(:track_event).with( @@ -699,6 +703,8 @@ front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), doc_type_supported: boolean, + doc_auth_success: boolean, + selfie_success: nil, ) expect(@analytics).to receive(:track_event).with( @@ -788,6 +794,8 @@ front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), doc_type_supported: boolean, + doc_auth_success: boolean, + selfie_success: nil, ) expect(@analytics).to receive(:track_event).with( @@ -874,6 +882,8 @@ front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), doc_type_supported: boolean, + doc_auth_success: boolean, + selfie_success: nil, ) expect(@analytics).to receive(:track_event).with( @@ -985,6 +995,8 @@ front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), doc_type_supported: boolean, + doc_auth_success: boolean, + selfie_success: nil, ) action @@ -1055,6 +1067,8 @@ front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), doc_type_supported: boolean, + doc_auth_success: boolean, + selfie_success: nil, ) action diff --git a/spec/controllers/idv/in_person/verify_info_controller_spec.rb b/spec/controllers/idv/in_person/verify_info_controller_spec.rb index f09de384860..55f9ead4f99 100644 --- a/spec/controllers/idv/in_person/verify_info_controller_spec.rb +++ b/spec/controllers/idv/in_person/verify_info_controller_spec.rb @@ -293,9 +293,12 @@ end it 'captures state id address fields in the pii' do - expect(Idv::Agent).to receive(:new). - with(Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_ADDRESS.merge(uuid_prefix: nil)). - and_call_original + expect(Idv::Agent).to receive(:new).with( + Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_ADDRESS.merge( + uuid_prefix: nil, + uuid: user.uuid, + ), + ).and_call_original put :update end end diff --git a/spec/controllers/idv/verify_info_controller_spec.rb b/spec/controllers/idv/verify_info_controller_spec.rb index 88b138fbc1b..21f325de2bf 100644 --- a/spec/controllers/idv/verify_info_controller_spec.rb +++ b/spec/controllers/idv/verify_info_controller_spec.rb @@ -391,9 +391,14 @@ sp_session = { issuer: sp.issuer } allow(controller).to receive(:sp_session).and_return(sp_session) - put :update + expect(Idv::Agent).to receive(:new).with( + hash_including( + uuid_prefix: app_id, + uuid: user.uuid, + ), + ).and_call_original - expect(subject.idv_session.pii_from_doc[:uuid_prefix]).to eq app_id + put :update end it 'updates DocAuthLog verify_submit_count' do diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 5a625a1ef05..8c9c2d09ec2 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -10,6 +10,7 @@ confirmed_at { Time.zone.now } confirmation_token { nil } confirmation_sent_at { 5.minutes.ago } + registered_at { Time.zone.now } end created_at { Time.zone.now } @@ -179,8 +180,8 @@ trait :fully_registered do with_phone - after :create do |user| - user.create_registration_log(registered_at: Time.zone.now) + after :create do |user, evaluator| + user.create_registration_log(registered_at: evaluator.registered_at) end end diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb index 8d4786ce09e..6c08f36450b 100644 --- a/spec/features/idv/doc_auth/document_capture_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_spec.rb @@ -10,13 +10,10 @@ let(:user) { user_with_2fa } let(:fake_analytics) { FakeAnalytics.new } let(:sp_name) { 'Test SP' } - let(:enable_not_ready) { true } let(:enable_exit_question) { true } before do allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics) allow_any_instance_of(ServiceProviderSession).to receive(:sp_name).and_return(sp_name) - allow(IdentityConfig.store).to receive(:doc_auth_not_ready_section_enabled). - and_return(enable_not_ready) allow(IdentityConfig.store).to receive(:doc_auth_exit_question_section_enabled). and_return(enable_exit_question) visit_idp_from_oidc_sp_with_ial2 @@ -156,17 +153,6 @@ ) expect(current_url).to start_with('http://localhost:7654/auth/result?error=access_denied') end - - context 'not ready section' do - it 'renders not ready section when enabled' do - expect(page).to have_content( - I18n.t( - 'doc_auth.not_ready.content_sp', sp_name: sp_name, - app_name: APP_NAME - ), - ) - end - end end context 'standard mobile flow' do diff --git a/spec/features/idv/doc_auth/verify_info_step_spec.rb b/spec/features/idv/doc_auth/verify_info_step_spec.rb index 9eafb35778f..533a9c96fd0 100644 --- a/spec/features/idv/doc_auth/verify_info_step_spec.rb +++ b/spec/features/idv/doc_auth/verify_info_step_spec.rb @@ -181,13 +181,15 @@ visit idv_verify_info_url expect(page).to have_current_path(idv_session_errors_failure_path) + # Manual expiration is needed because Redis timestamp doesn't always match ruby timestamp + RateLimiter.new(user: user, rate_limit_type: :idv_resolution).reset! travel_to(IdentityConfig.store.idv_attempt_window_in_hours.hours.from_now + 1) do sign_in_and_2fa_user(user) complete_doc_auth_steps_before_verify_step complete_verify_step expect(page).to have_current_path(idv_phone_path) - expect(RateLimiter.new(user: user, rate_limit_type: :idv_resolution)).to be_limited + expect(RateLimiter.new(user: user, rate_limit_type: :idv_resolution)).to_not be_limited end end @@ -232,12 +234,15 @@ visit idv_verify_info_url expect(page).to have_current_path(idv_session_errors_ssn_failure_path) + # Manual expiration is needed because Redis timestamp doesn't always match ruby timestamp + RateLimiter.new(user: user, rate_limit_type: :idv_resolution).reset! travel_to(IdentityConfig.store.idv_attempt_window_in_hours.hours.from_now + 1) do sign_in_and_2fa_user(user) complete_doc_auth_steps_before_verify_step complete_verify_step expect(page).to have_current_path(idv_phone_path) + expect(RateLimiter.new(user: user, rate_limit_type: :idv_resolution)).to_not be_limited end end diff --git a/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_success_with_liveness.json b/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_success_with_liveness.json new file mode 100644 index 00000000000..d158c84de08 --- /dev/null +++ b/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_success_with_liveness.json @@ -0,0 +1,1100 @@ +{ + "Status": { + "ConversationId": "70000300394121", + "RequestId": "614507871", + "TransactionStatus": "passed", + "TransactionReasonCode": { + "Code": "trueid_pass", + "Description": "TRUEID PASS" + }, + "Reference": "ca6e36c4-8a55-4831-aa8a-38d78b7c80e3" + }, + "Products": [ + { + "ProductType": "TrueID", + "ExecutedStepName": "True_ID_Step", + "ProductConfigurationName": "GSA2.V3.TrueID.CROP.PT.test", + "ProductStatus": "pass", + "ParameterDetails": [ + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocumentName", + "Values": [{ "Value": "Maryland (MD) Driver's License - STAR" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocAuthResult", + "Values": [{ "Value": "Passed" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocAuthTamperResult", + "Values": [{ "Value": "Passed" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocAuthTamperSensitivity", + "Values": [{ "Value": "Normal" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssuerCode", + "Values": [{ "Value": "MD" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssuerName", + "Values": [{ "Value": "Maryland" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssuerType", + "Values": [{ "Value": "StateProvince" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocClassCode", + "Values": [{ "Value": "DriversLicense" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocClass", + "Values": [{ "Value": "DriversLicense" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocClassName", + "Values": [{ "Value": "Drivers License" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIsGeneric", + "Values": [{ "Value": "false" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssue", + "Values": [{ "Value": "2016" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssueType", + "Values": [{ "Value": "Driver's License - STAR" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocSize", + "Values": [{ "Value": "ID1" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "ClassificationMode", + "Values": [{ "Value": "Automatic" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "OrientationChanged", + "Values": [{ "Value": "true" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "PresentationChanged", + "Values": [{ "Value": "false" }] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "Side", + "Values": [{ "Value": "Front" }, { "Value": "Back" }] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "GlareMetric", + "Values": [{ "Value": "100" }, { "Value": "100" }] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "SharpnessMetric", + "Values": [{ "Value": "65" }, { "Value": "65" }] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "IsTampered", + "Values": [{ "Value": "0" }, { "Value": "0" }] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "IsCropped", + "Values": [{ "Value": "1" }, { "Value": "1" }] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "HorizontalResolution", + "Values": [{ "Value": "600" }, { "Value": "600" }] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "VerticalResolution", + "Values": [{ "Value": "600" }, { "Value": "600" }] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "Light", + "Values": [{ "Value": "White" }, { "Value": "White" }] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "MimeType", + "Values": [ + { "Value": "image/vnd.ms-photo" }, + { "Value": "image/vnd.ms-photo" } + ] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "ImageMetrics_Id", + "Values": [ + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "637aa4c6-eeb3-453f-899e-a56effcf3747" } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "FullName", + "Values": [{ "Value": "DAVID LICENSE SAMPLE" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Sex", + "Values": [{ "Value": "Male" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Age", + "Values": [{ "Value": "33" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DOB_Year", + "Values": [{ "Value": "1985" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DOB_Month", + "Values": [{ "Value": "7" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DOB_Day", + "Values": [{ "Value": "1" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "ExpirationDate_Year", + "Values": [{ "Value": "2099" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "ExpirationDate_Month", + "Values": [{ "Value": "10" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "ExpirationDate_Day", + "Values": [{ "Value": "15" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_1_AlertName", + "Values": [{ "Value": "Document Expired" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_2_AlertName", + "Values": [{ "Value": "Visible Pattern" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_3_AlertName", + "Values": [{ "Value": "Document Tampering Detection" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_4_AlertName", + "Values": [{ "Value": "2D Barcode Content" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_5_AlertName", + "Values": [{ "Value": "2D Barcode Read" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_6_AlertName", + "Values": [{ "Value": "Barcode Encoding" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_7_AlertName", + "Values": [{ "Value": "Birth Date Crosscheck" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_8_AlertName", + "Values": [{ "Value": "Birth Date Valid" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_9_AlertName", + "Values": [{ "Value": "Document Classification" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_10_AlertName", + "Values": [{ "Value": "Document Crosscheck Aggregation" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_11_AlertName", + "Values": [{ "Value": "Document Number Crosscheck" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_12_AlertName", + "Values": [{ "Value": "Document Tampering Detection" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_13_AlertName", + "Values": [{ "Value": "Document Tampering Detection" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_14_AlertName", + "Values": [{ "Value": "Document Tampering Detection" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_15_AlertName", + "Values": [{ "Value": "Expiration Date Crosscheck" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_16_AlertName", + "Values": [{ "Value": "Expiration Date Valid" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_17_AlertName", + "Values": [{ "Value": "Full Name Crosscheck" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_18_AlertName", + "Values": [{ "Value": "Issue Date Crosscheck" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_19_AlertName", + "Values": [{ "Value": "Issue Date Valid" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_20_AlertName", + "Values": [{ "Value": "Series Expired" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_21_AlertName", + "Values": [{ "Value": "Sex Crosscheck" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_22_AlertName", + "Values": [{ "Value": "Visible Pattern" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_23_AlertName", + "Values": [{ "Value": "Visible Pattern" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_24_AlertName", + "Values": [{ "Value": "Visible Pattern" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_3_Model", + "Values": [{ "Value": "Text Tampering Detection V1.3.1 (Beta)" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_12_Model", + "Values": [{ "Value": "Photo Tampering Detection V2.4" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_13_Model", + "Values": [{ "Value": "Text Tampering Detection V1.2.1" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_14_Model", + "Values": [{ "Value": "Physical Document Presence V2.5" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_1_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Checked if the document is expired." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_2_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified the presence of a pattern on the visible image." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_3_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Examines a document for evidence of tampering" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_4_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Checked the contents of the two-dimensional barcode on the document." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_5_AuthenticationResult", + "Values": [ + { + "Value": "Attention", + "Detail": "Verified that the two-dimensional barcode on the document was read successfully." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_6_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified the format of the barcode." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_7_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Compare the machine-readable birth date field to the human-readable birth date field." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_8_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified that the birth date is valid." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_9_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified that the type of document is supported and is able to be fully authenticated." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_10_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Compared the machine-readable fields to the human-readable fields." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_11_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Compare the machine-readable document number field to the human-readable document number field." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_12_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Examines a document for evidence of tampering" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_13_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Examines a document for evidence of tampering" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_14_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Examines a document for evidence of tampering" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_15_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Compare the machine-readable expiration date field to the human-readable expiration date field." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_16_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified that the expiration date is valid." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_17_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Compare the machine-readable full name field to the human-readable full name field." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_18_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Compare the machine-readable issue date field to the human-readable issue date field." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_19_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified that the issue date is valid." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_20_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified whether the document type is still in circulation." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_21_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Compare the machine-readable sex field to the human-readable sex field." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_22_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified the presence of a pattern on the visible image." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_23_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified the presence of a pattern on the visible image." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_24_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified the presence of a pattern on the visible image." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_1_Disposition", + "Values": [{ "Value": "The document has expired" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_2_Disposition", + "Values": [{ "Value": "A visible pattern was not found" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_3_Disposition", + "Values": [ + { + "Value": "Evidence suggests that the document may have been tampered with." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_4_Disposition", + "Values": [{ "Value": "The 2D barcode is formatted correctly" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_5_Disposition", + "Values": [{ "Value": "The 2D barcode was read successfully" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_6_Disposition", + "Values": [ + { + "Value": "The barcode encoding is consistent with the expected encoding for the type" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_7_Disposition", + "Values": [{ "Value": "The birth dates match" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_8_Disposition", + "Values": [{ "Value": "The birth date is valid" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_9_Disposition", + "Values": [{ "Value": "The document type is supported" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_10_Disposition", + "Values": [ + { + "Value": "There are not a large number of differences between electronic and human-readable data sources" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_11_Disposition", + "Values": [{ "Value": "The document numbers match" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_12_Disposition", + "Values": [ + { "Value": "No evidence of document tampering was detected." } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_13_Disposition", + "Values": [ + { "Value": "No evidence of document tampering was detected." } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_14_Disposition", + "Values": [ + { "Value": "No evidence of document tampering was detected." } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_15_Disposition", + "Values": [{ "Value": "The expiration dates match" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_16_Disposition", + "Values": [{ "Value": "The expiration date is valid" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_17_Disposition", + "Values": [{ "Value": "The full names match" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_18_Disposition", + "Values": [{ "Value": "The issue dates match" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_19_Disposition", + "Values": [{ "Value": "The issue date is valid" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_20_Disposition", + "Values": [{ "Value": "The series has not expired" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_21_Disposition", + "Values": [{ "Value": "The sexes match" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_22_Disposition", + "Values": [{ "Value": "A visible pattern was found" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_23_Disposition", + "Values": [{ "Value": "A visible pattern was found" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_24_Disposition", + "Values": [{ "Value": "A visible pattern was found" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_2_Regions", + "Values": [{ "Value": "Background" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_3_Regions", + "Values": [ + { "Value": "Address" }, + { "Value": "Birth Date" }, + { "Value": "Document Number" }, + { "Value": "Expiration Date" }, + { "Value": "Full Name" }, + { "Value": "Issue Date" } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_12_Regions", + "Values": [{ "Value": "Photo" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_13_Regions", + "Values": [ + { "Value": "Address" }, + { "Value": "Birth Date" }, + { "Value": "Document Number" }, + { "Value": "Expiration Date" }, + { "Value": "Full Name" }, + { "Value": "Issue Date" } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_22_Regions", + "Values": [{ "Value": "Expires Label" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_23_Regions", + "Values": [{ "Value": "USA" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_24_Regions", + "Values": [{ "Value": "Background Upper" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_2_Regions_Reference", + "Values": [{ "Value": "faacfb79-d0a1-4a8e-b868-20c604988e84" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_3_Regions_Reference", + "Values": [ + { "Value": "c8be94b6-78ac-4e85-88cb-e17880371e4a" }, + { "Value": "a8226d92-e62c-42a3-a206-ab7c3e3d9796" }, + { "Value": "c2e18c41-e3de-46a8-abc7-3412015a6cef" }, + { "Value": "80f8f290-daa0-47e2-828e-52106bb26f31" }, + { "Value": "2c74b850-dd89-41bb-a21c-70ae0563ef77" }, + { "Value": "63bf5053-f81f-493f-aff0-33c07d07a894" } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_12_Regions_Reference", + "Values": [{ "Value": "f29b1fe5-6482-4b39-8b4a-d91caf4ecb57" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_13_Regions_Reference", + "Values": [ + { "Value": "c8be94b6-78ac-4e85-88cb-e17880371e4a" }, + { "Value": "a8226d92-e62c-42a3-a206-ab7c3e3d9796" }, + { "Value": "c2e18c41-e3de-46a8-abc7-3412015a6cef" }, + { "Value": "80f8f290-daa0-47e2-828e-52106bb26f31" }, + { "Value": "2c74b850-dd89-41bb-a21c-70ae0563ef77" }, + { "Value": "63bf5053-f81f-493f-aff0-33c07d07a894" } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_22_Regions_Reference", + "Values": [{ "Value": "d55f2c66-f84f-4213-a660-4ff9e5d0fde5" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_23_Regions_Reference", + "Values": [{ "Value": "bbf6ba02-ee3f-4b5c-a5d0-2fdb39ac79f7" }] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_24_Regions_Reference", + "Values": [{ "Value": "20203cb8-f8a4-4a5b-999f-3700f73fe4fe" }] + }, + { + "Group": "PORTRAIT_MATCH_RESULT", + "Name": "FaceMatchResult", + "Values": [{"Value": "Pass"}] + }, + { + "Group": "PORTRAIT_MATCH_RESULT", + "Name": "FaceMatchScore", + "Values": [{"Value": "96"}] + }, + { + "Group": "PORTRAIT_MATCH_RESULT", + "Name": "FaceStatusCode", + "Values": [{"Value": "1"}] + }, + { + "Group": "PORTRAIT_MATCH_RESULT", + "Name": "FaceErrorMessage", + "Values": [{"Value": "Successful. Liveness: Live"}] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_FullName", + "Values": [{ "Value": "DAVID LICENSE SAMPLE" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_Surname", + "Values": [{ "Value": "SAMPLE" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_GivenName", + "Values": [{ "Value": "DAVID LICENSE" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_FirstName", + "Values": [{ "Value": "DAVID" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_MiddleName", + "Values": [{ "Value": "LICENSE" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_DOB_Year", + "Values": [{ "Value": "1986" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_DOB_Month", + "Values": [{ "Value": "7" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_DOB_Day", + "Values": [{ "Value": "1" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_DocumentClassName", + "Values": [{ "Value": "Drivers License" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_DocumentNumber", + "Values": [{ "Value": "M555555555555" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_ExpirationDate_Year", + "Values": [{ "Value": "2099" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_ExpirationDate_Month", + "Values": [{ "Value": "10" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_xpirationDate_Day", + "Values": [{ "Value": "15" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_IssuingStateCode", + "Values": [{ "Value": "MD" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_IssuingStateName", + "Values": [{ "Value": "Maryland" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_CountryCode", + "Values": [{ "Value": "USA" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_Address", + "Values": [ + { + "Value": "123 ABC AVExE2x80xA8ANYTOWN, MD 12345" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_AddressLine1", + "Values": [{ "Value": "123 ABC AVE" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_AddressLine2", + "Values": [{ "Value": "APT 3E" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_City", + "Values": [{ "Value": "ANYTOWN" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_State", + "Values": [{ "Value": "MD" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_PostalCode", + "Values": [{ "Value": "12345" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_Height", + "Values": [{ "Value": "5' 9\"" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_IssueDate_Year", + "Values": [{ "Value": "2016" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_IssueDate_Month", + "Values": [{ "Value": "10" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_IssueDate_Day", + "Values": [{ "Value": "15" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_LicenseClass", + "Values": [{ "Value": "C" }] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_LicenseRestrictions", + "Values": [{ "Value": "B" }] + }, + { + "Group": "DOCUMENT_REGION", + "Name": "DocumentRegion_Id", + "Values": [ + { "Value": "ce2cf0e2-5373-4ec2-84e8-7fe44a01642b" }, + { "Value": "0b4f4f2b-cbd6-43e9-ac67-55bf2bdd9df5" }, + { "Value": "c8be94b6-78ac-4e85-88cb-e17880371e4a" }, + { "Value": "a0fdf00c-071c-4d8e-81af-8af0fc7688b8" }, + { "Value": "faacfb79-d0a1-4a8e-b868-20c604988e84" }, + { "Value": "3362ad4b-a36b-487e-826c-c748c7b04e8d" }, + { "Value": "20203cb8-f8a4-4a5b-999f-3700f73fe4fe" }, + { "Value": "a8226d92-e62c-42a3-a206-ab7c3e3d9796" }, + { "Value": "a3e3a625-8b0e-4deb-a91e-86afc55d036b" }, + { "Value": "cda4092d-9208-4871-bbc1-1b732c299d26" }, + { "Value": "42770baf-3a4a-4477-9e5f-4400237273fe" }, + { "Value": "c2e18c41-e3de-46a8-abc7-3412015a6cef" }, + { "Value": "b36594f2-d19c-48aa-98c2-2b4f8429744f" }, + { "Value": "80f8f290-daa0-47e2-828e-52106bb26f31" }, + { "Value": "d55f2c66-f84f-4213-a660-4ff9e5d0fde5" }, + { "Value": "26ca0c85-01ab-4311-bfd5-d27d2ee975eb" }, + { "Value": "0af79f76-1542-4391-ad17-a5d6169be57f" }, + { "Value": "2c74b850-dd89-41bb-a21c-70ae0563ef77" }, + { "Value": "64e8ee97-2b24-452d-a5af-1627951aa737" }, + { "Value": "63bf5053-f81f-493f-aff0-33c07d07a894" }, + { "Value": "f29b1fe5-6482-4b39-8b4a-d91caf4ecb57" }, + { "Value": "35442fb1-c3bb-4f2f-ad9e-537a8f0e7e0f" }, + { "Value": "29964031-e072-4204-a9ae-b2b7122bfdc1" }, + { "Value": "a3bbdf8b-62f5-438f-bf72-091bb2f6f0ff" }, + { "Value": "42d0c49e-fc7a-45da-8f6d-8896c4b5267f" }, + { "Value": "5430efcb-523b-4c62-a190-e9aa7eea4ebd" }, + { "Value": "bbf6ba02-ee3f-4b5c-a5d0-2fdb39ac79f7" }, + { "Value": "0686341c-0b3f-4544-840e-58822120ef06" } + ] + }, + { + "Group": "DOCUMENT_REGION", + "Name": "DocumentRegion_Key", + "Values": [ + { "Value": "1D Barcode" }, + { "Value": "2D Barcode" }, + { "Value": "Address" }, + { "Value": "Alaska Validator" }, + { "Value": "Background" }, + { "Value": "Background Lower" }, + { "Value": "Background Upper" }, + { "Value": "Birth Date" }, + { "Value": "Birth Date" }, + { "Value": "DOB Label" }, + { "Value": "DOB Label Text" }, + { "Value": "Document Number" }, + { "Value": "Document Type" }, + { "Value": "Expiration Date" }, + { "Value": "Expires Label" }, + { "Value": "Expires Label Position" }, + { "Value": "Eye Color" }, + { "Value": "Full Name" }, + { "Value": "Height" }, + { "Value": "Issue Date" }, + { "Value": "Photo" }, + { "Value": "Photo Printing" }, + { "Value": "Secondary Photo" }, + { "Value": "Sex" }, + { "Value": "Sex Height Labels" }, + { "Value": "Signature" }, + { "Value": "USA" }, + { "Value": "Weight" } + ] + }, + { + "Group": "DOCUMENT_REGION", + "Name": "DocumentRegion_ImageReference", + "Values": [ + { "Value": "637aa4c6-eeb3-453f-899e-a56effcf3747" }, + { "Value": "637aa4c6-eeb3-453f-899e-a56effcf3747" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "637aa4c6-eeb3-453f-899e-a56effcf3747" }, + { "Value": "637aa4c6-eeb3-453f-899e-a56effcf3747" }, + { "Value": "637aa4c6-eeb3-453f-899e-a56effcf3747" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" }, + { "Value": "8a19313b-5dc6-4113-85f5-42c9829d903e" } + ] + } + ] + }, + { + "ProductType": "TrueID_Decision", + "ExecutedStepName": "Decision", + "ProductConfigurationName": "TRUEID_PASS", + "ProductStatus": "pass", + "ProductReason": { + "Code": "trueid_pass", + "Description": "TRUEID PASS" + } + } + ] +} diff --git a/spec/forms/idv/api_image_upload_form_spec.rb b/spec/forms/idv/api_image_upload_form_spec.rb index 98f4099164e..3316727a368 100644 --- a/spec/forms/idv/api_image_upload_form_spec.rb +++ b/spec/forms/idv/api_image_upload_form_spec.rb @@ -159,6 +159,8 @@ front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), doc_type_supported: boolean, + doc_auth_success: boolean, + selfie_success: anything, ) end @@ -482,6 +484,8 @@ allow(client_response).to receive(:success?).and_return(false) allow(client_response).to receive(:network_error?).and_return(false) allow(client_response).to receive(:errors).and_return(errors) + allow(client_response).to receive(:doc_auth_success?).and_return(false) + allow(client_response).to receive(:selfie_success).and_return(nil) form.send(:validate_form) capture_result = form.send(:store_failed_images, client_response, doc_pii_response) expect(capture_result[:front]).not_to be_empty @@ -494,6 +498,8 @@ allow(client_response).to receive(:success?).and_return(false) allow(client_response).to receive(:network_error?).and_return(false) allow(client_response).to receive(:errors).and_return(errors) + allow(client_response).to receive(:doc_auth_success?).and_return(false) + allow(client_response).to receive(:selfie_success).and_return(nil) form.send(:validate_form) capture_result = form.send(:store_failed_images, client_response, doc_pii_response) expect(capture_result[:front]).not_to be_empty @@ -506,6 +512,8 @@ allow(client_response).to receive(:success?).and_return(false) allow(client_response).to receive(:network_error?).and_return(false) allow(client_response).to receive(:errors).and_return(errors) + allow(client_response).to receive(:doc_auth_success?).and_return(false) + allow(client_response).to receive(:selfie_success).and_return(nil) form.send(:validate_form) capture_result = form.send(:store_failed_images, client_response, doc_pii_response) expect(capture_result[:front]).not_to be_empty @@ -533,6 +541,8 @@ allow(client_response).to receive(:success?).and_return(false) allow(client_response).to receive(:network_error?).and_return(true) allow(client_response).to receive(:errors).and_return(errors) + allow(client_response).to receive(:doc_auth_success?).and_return(false) + allow(client_response).to receive(:selfie_success).and_return(nil) allow(doc_pii_response).to receive(:success?).and_return(false) form.send(:validate_form) capture_result = form.send(:store_failed_images, client_response, doc_pii_response) diff --git a/spec/javascript/packages/document-capture/components/acuant-selfie-camera-spec.jsx b/spec/javascript/packages/document-capture/components/acuant-selfie-camera-spec.jsx index d392b21026c..b4bd0858b40 100644 --- a/spec/javascript/packages/document-capture/components/acuant-selfie-camera-spec.jsx +++ b/spec/javascript/packages/document-capture/components/acuant-selfie-camera-spec.jsx @@ -1,6 +1,7 @@ import { AcuantContextProvider, DeviceContext } from '@18f/identity-document-capture'; import AcuantSelfieCamera from '@18f/identity-document-capture/components/acuant-selfie-camera'; import AcuantSelfieCaptureCanvas from '@18f/identity-document-capture/components/acuant-selfie-capture-canvas'; +import { t } from '@18f/identity-i18n'; import { render, useAcuant } from '../../../support/document-capture'; describe('document-capture/components/acuant-selfie-camera', () => { @@ -40,12 +41,10 @@ describe('document-capture/components/acuant-selfie-camera', () => { expect(callbackNames).to.equal(expectedCallbackNames); expect(window.AcuantPassiveLiveness.start.getCall(0).args[1]).to.deep.equal({ - FACE_NOT_FOUND: 'FACE NOT FOUND', - TOO_MANY_FACES: 'TOO MANY FACES', - FACE_ANGLE_TOO_LARGE: 'FACE ANGLE TOO LARGE', - PROBABILITY_TOO_SMALL: 'PROBABILITY TOO SMALL', - FACE_TOO_SMALL: 'FACE TOO SMALL', - FACE_CLOSE_TO_BORDER: 'TOO CLOSE TO THE FRAME', + FACE_NOT_FOUND: t('doc_auth.info.selfie_capture_status.face_not_found'), + TOO_MANY_FACES: t('doc_auth.info.selfie_capture_status.too_many_faces'), + FACE_TOO_SMALL: t('doc_auth.info.selfie_capture_status.face_too_small'), + FACE_CLOSE_TO_BORDER: t('doc_auth.info.selfie_capture_status.face_close_to_border'), }); }); diff --git a/spec/javascript/packages/document-capture/components/document-capture-not-ready-spec.tsx b/spec/javascript/packages/document-capture/components/document-capture-not-ready-spec.tsx deleted file mode 100644 index d5590131249..00000000000 --- a/spec/javascript/packages/document-capture/components/document-capture-not-ready-spec.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import sinon from 'sinon'; - -import { FlowContext } from '@18f/identity-verify-flow'; -import { I18nContext } from '@18f/identity-react-i18n'; -import { I18n } from '@18f/identity-i18n'; -import userEvent from '@testing-library/user-event'; -import type { Navigate } from '@18f/identity-url'; -import { - AnalyticsContextProvider, - ServiceProviderContextProvider, -} from '@18f/identity-document-capture/context'; -import DocumentCaptureNotReady from '@18f/identity-document-capture/components/document-capture-not-ready'; -import { expect } from 'chai'; -import { render } from '../../../support/document-capture'; - -describe('DocumentCaptureNotReady', () => { - beforeEach(() => { - const config = document.createElement('script'); - config.id = 'test-config'; - config.type = 'application/json'; - config.setAttribute('data-config', ''); - config.textContent = JSON.stringify({ appName: 'Login.gov' }); - document.body.append(config); - }); - const trackEvent = sinon.spy(); - const navigateSpy: Navigate = sinon.spy(); - context('with service provider', () => { - const spName = 'testSP'; - it('renders, track event and redirect', async () => { - const { getByRole } = render( - - '', - }} - > - - - - - - - , - ); - // header - expect(getByRole('heading', { name: 'header text', level: 2 })).to.be.ok(); - - // content and exit link - const exitLink = getByRole('button', { name: 'Exit Login.gov and return to testSP' }); - expect(exitLink).to.be.ok(); - await userEvent.click(exitLink); - expect(navigateSpy).to.be.called.calledWithMatch( - /failure-to-proof\?step=document_capture&location=not_ready/, - ); - expect(trackEvent).to.be.calledWithMatch(/IdV: docauth not ready link clicked/); - }); - }); - - context('without service provider', () => { - it('renders, track event and redirect', async () => { - const { getByRole } = render( - - '', - }} - > - - - - - - - , - ); - // header - expect(getByRole('heading', { name: 'header text', level: 2 })).to.be.ok(); - - // content and exit link - const exitLink = getByRole('button', { name: 'Cancel and return to your profile' }); - expect(exitLink).to.be.ok(); - await userEvent.click(exitLink); - expect(navigateSpy).to.be.called.calledWithMatch( - /account\?step=document_capture&location=not_ready/, - ); - expect(trackEvent).to.be.calledWithMatch(/IdV: docauth not ready link clicked/); - }); - }); -}); diff --git a/spec/javascript/packages/document-capture/components/documents-step-spec.jsx b/spec/javascript/packages/document-capture/components/documents-step-spec.jsx index 5675099308c..5eb17473141 100644 --- a/spec/javascript/packages/document-capture/components/documents-step-spec.jsx +++ b/spec/javascript/packages/document-capture/components/documents-step-spec.jsx @@ -8,7 +8,6 @@ import { UploadContextProvider, FailedCaptureAttemptsContextProvider, FeatureFlagContext, - InPersonContext, } from '@18f/identity-document-capture'; import DocumentsStep from '@18f/identity-document-capture/components/documents-step'; import { composeComponents } from '@18f/identity-compose-components'; @@ -90,68 +89,6 @@ describe('document-capture/components/documents-step', () => { expect(queryByText(notExpectedText)).to.not.exist(); }); - it('renders optional question part and not ready section', () => { - const App = composeComponents( - [ - FeatureFlagContext.Provider, - { - value: { - notReadySectionEnabled: true, - exitQuestionSectionEnabled: true, - }, - }, - ], - [ - InPersonContext.Provider, - { - value: { - inPersonURL: '/verify/doc_capture', - }, - }, - ], - [DocumentsStep], - ); - const { getByRole, getByText } = render(); - expect(getByRole('heading', { name: 'doc_auth.not_ready.header', level: 2 })).to.be.ok(); - expect(getByRole('heading', { name: 'doc_auth.exit_survey.header', level: 2 })).to.be.ok(); - expect(getByText('doc_auth.exit_survey.optional.button')).to.be.ok(); - }); - - context('not ready section', () => { - it('is rendered when enabled', () => { - const App = composeComponents( - [ - FeatureFlagContext.Provider, - { - value: { - notReadySectionEnabled: true, - }, - }, - ], - [DocumentsStep], - ); - const { getByRole } = render(); - expect(getByRole('heading', { name: 'doc_auth.not_ready.header', level: 2 })).to.be.ok(); - const button = getByRole('button', { name: 'doc_auth.not_ready.button_nosp' }); - expect(button).to.be.ok(); - }); - it('is not rendered when disabled', () => { - const App = composeComponents( - [ - FeatureFlagContext.Provider, - { - value: { - notReadySectionEnabled: false, - }, - }, - ], - [DocumentsStep], - ); - const { queryByRole } = render(); - expect(queryByRole('heading', { name: 'doc_auth.not_ready.header', level: 2 })).to.be.null(); - }); - }); - context('selfie capture', () => { it('renders with front, back, and selfie inputs when featureflag is on', () => { const App = composeComponents( diff --git a/spec/javascript/packages/document-capture/context/feature-flag-spec.jsx b/spec/javascript/packages/document-capture/context/feature-flag-spec.jsx index c1f43c67e50..3516e8b6f0b 100644 --- a/spec/javascript/packages/document-capture/context/feature-flag-spec.jsx +++ b/spec/javascript/packages/document-capture/context/feature-flag-spec.jsx @@ -6,12 +6,7 @@ describe('document-capture/context/feature-flag', () => { it('has expected default properties', () => { const { result } = renderHook(() => useContext(FeatureFlagContext)); - expect(result.current).to.have.keys([ - 'notReadySectionEnabled', - 'exitQuestionSectionEnabled', - 'selfieCaptureEnabled', - ]); - expect(result.current.notReadySectionEnabled).to.be.a('boolean'); + expect(result.current).to.have.keys(['exitQuestionSectionEnabled', 'selfieCaptureEnabled']); expect(result.current.exitQuestionSectionEnabled).to.be.a('boolean'); expect(result.current.selfieCaptureEnabled).to.be.a('boolean'); }); diff --git a/spec/jobs/reports/irs_weekly_summary_report_spec.rb b/spec/jobs/reports/irs_weekly_summary_report_spec.rb deleted file mode 100644 index ae3e7c1d47f..00000000000 --- a/spec/jobs/reports/irs_weekly_summary_report_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'rails_helper' - -RSpec.describe Reports::IrsWeeklySummaryReport do - subject(:report) { Reports::IrsWeeklySummaryReport.new } - let(:report_name) { 'irs-weekly-summary-report' } - let(:email) { 'foo@bar.com' } - - before do - create_list(:user, 10, { created_at: Date.yesterday }) - end - - describe '#perform' do - it 'sends out a report to the email listed with system demand' do - allow(IdentityConfig.store).to receive(:system_demand_report_email).and_return(email) - allow(ReportMailer).to receive(:system_demand_report).and_call_original - - report = "Data Requested,Total Count\nSystem Demand,10\n" - expect(ReportMailer).to receive(:system_demand_report).with( - email: email, data: report, name: report_name, - ) - - subject.perform(Time.zone.now) - end - - it 'uploads a file to S3 based on the report date' do - csv_data = CSV.parse(subject.perform(Time.zone.now), headers: true) - expect(csv_data[0]['Total Count']).to eq('10') - end - end -end diff --git a/spec/models/document_capture_session_spec.rb b/spec/models/document_capture_session_spec.rb index f140962c30b..f2de75d0462 100644 --- a/spec/models/document_capture_session_spec.rb +++ b/spec/models/document_capture_session_spec.rb @@ -89,14 +89,15 @@ end end - describe('#store_failed_auth_image_fingerprint') do + describe('#store_failed_auth_data') do it 'stores image finger print' do record = DocumentCaptureSession.new(result_id: SecureRandom.uuid) - - record.store_failed_auth_image_fingerprint( - 'fingerprint1', nil + record.store_failed_auth_data( + front_image_fingerprint: 'fingerprint1', + back_image_fingerprint: nil, + doc_auth_success: false, + selfie_success: nil, ) - result_id = record.result_id key = EncryptedRedisStructStorage.key(result_id, type: DocumentCaptureSessionResult) data = REDIS_POOL.with { |client| client.get(key) } @@ -105,28 +106,40 @@ expect(result.failed_front_image?('fingerprint1')).to eq(true) expect(result.failed_front_image?(nil)).to eq(false) expect(result.failed_back_image?(nil)).to eq(false) + expect(result.doc_auth_success).to eq(false) + expect(result.selfie_success).to be_nil end it 'saves failed image finterprints' do record = DocumentCaptureSession.new(result_id: SecureRandom.uuid) - record.store_failed_auth_image_fingerprint( - 'fingerprint1', nil + record.store_failed_auth_data( + front_image_fingerprint: 'fingerprint1', + back_image_fingerprint: nil, + doc_auth_success: false, + selfie_success: nil, ) old_result = record.load_result - record.store_failed_auth_image_fingerprint( - 'fingerprint2', 'fingerprint3' + record.store_failed_auth_data( + front_image_fingerprint: 'fingerprint2', + back_image_fingerprint: 'fingerprint3', + doc_auth_success: false, + selfie_success: nil, ) new_result = record.load_result expect(old_result.failed_front_image?('fingerprint1')).to eq(true) expect(old_result.failed_front_image?('fingerprint2')).to eq(false) expect(old_result.failed_back_image?('fingerprint3')).to eq(false) + expect(old_result.doc_auth_success).to eq(false) + expect(old_result.selfie_success).to be_nil expect(new_result.failed_front_image?('fingerprint1')).to eq(true) expect(new_result.failed_front_image?('fingerprint2')).to eq(true) expect(new_result.failed_back_image?('fingerprint3')).to eq(true) + expect(new_result.doc_auth_success).to eq(false) + expect(new_result.selfie_success).to be_nil end end end diff --git a/spec/services/doc_auth/acuant/responses/get_results_response_spec.rb b/spec/services/doc_auth/acuant/responses/get_results_response_spec.rb index 178b6027ffe..29e071d13a3 100644 --- a/spec/services/doc_auth/acuant/responses/get_results_response_spec.rb +++ b/spec/services/doc_auth/acuant/responses/get_results_response_spec.rb @@ -62,6 +62,8 @@ }, address_line2_present: true, doc_type_supported: true, + doc_auth_success: true, + selfie_success: nil, } processed_alerts = response_hash[:processed_alerts] @@ -418,6 +420,8 @@ }, address_line2_present: true, doc_type_supported: false, + doc_auth_success: true, + selfie_success: nil, } processed_alerts = response_hash[:processed_alerts] @@ -499,6 +503,8 @@ }, address_line2_present: true, doc_type_supported: false, + doc_auth_success: true, + selfie_success: nil, } expect(response_hash).to match(expected_hash) diff --git a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb index 3c39172fcb8..838d010d97b 100644 --- a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb @@ -2,9 +2,15 @@ RSpec.describe DocAuth::LexisNexis::Responses::TrueIdResponse do let(:success_response_body) { LexisNexisFixtures.true_id_response_success_3 } + let(:success_with_liveness_response_body) do + LexisNexisFixtures.true_id_response_success_with_liveness + end let(:success_response) do instance_double(Faraday::Response, status: 200, body: success_response_body) end + let(:success_with_liveness_response) do + instance_double(Faraday::Response, status: 200, body: success_with_liveness_response_body) + end let(:failure_body_no_liveness) { LexisNexisFixtures.true_id_response_failure_no_liveness } let(:failure_body_with_liveness) { LexisNexisFixtures.true_id_response_failure_with_liveness } let(:failure_body_with_all_failures) do @@ -140,6 +146,8 @@ Front: a_hash_including(:ClassName, :CountryCode, :IssuerType), Back: a_hash_including(:ClassName, :CountryCode, :IssuerType), }, + doc_auth_success: true, + selfie_success: nil, ) passed_alerts = response_hash.dig(:processed_alerts, :passed) passed_alerts.each do |alert| @@ -320,8 +328,11 @@ def get_decision_product(resp) end it 'returns Failed for liveness failure' do - output = described_class.new(failure_response_with_liveness, config).to_h + response = described_class.new(failure_response_with_liveness, config) + output = response.to_h expect(output[:success]).to eq(false) + expect(response.doc_auth_success?).to eq(false) + expect(response.selfie_success).to eq(false) end it 'produces expected hash output' do @@ -376,6 +387,8 @@ def get_decision_product(resp) Front: a_hash_including(:ClassName, :CountryCode, :IssuerType), Back: a_hash_including(:ClassName, :CountryCode, :IssuerType), }, + doc_auth_success: false, + selfie_success: false, ) end it 'produces appropriate errors with document tampering' do @@ -635,4 +648,48 @@ def get_decision_product(resp) end end end + describe '#doc_auth_success?' do + context 'when document validation is successful' do + let(:response) { described_class.new(success_response, config) } + it 'returns true' do + expect(response.doc_auth_success?).to eq(true) + end + end + context 'when document validation failed' do + let(:response) { described_class.new(failure_response_tampering, config) } + it 'returns false' do + expect(response.doc_auth_success?).to eq(false) + end + end + end + + describe '#selfie_success' do + context 'when selfie check is disabled' do + let(:response) { described_class.new(success_response, config, false) } + it 'returns nil' do + expect(response.selfie_success).to eq(nil) + end + end + + context 'when selfie check is enabled' do + context 'whe missing selfie result in response' do + let(:response) { described_class.new(success_response, config, true) } + it 'returns nil when missing selfie in response' do + expect(response.selfie_success).to eq(nil) + end + end + context 'when selfie passed' do + let(:response) { described_class.new(success_with_liveness_response, config, true) } + it 'returns true' do + expect(response.selfie_success).to eq(true) + end + end + context 'when selfie failed' do + let(:response) { described_class.new(failure_response_with_liveness, config, true) } + it 'returns false' do + expect(response.selfie_success).to eq(false) + end + end + end + end end diff --git a/spec/services/doc_auth/mock/result_response_spec.rb b/spec/services/doc_auth/mock/result_response_spec.rb index 586a87ef64d..e3296ca83c0 100644 --- a/spec/services/doc_auth/mock/result_response_spec.rb +++ b/spec/services/doc_auth/mock/result_response_spec.rb @@ -304,6 +304,8 @@ billed: true, classification_info: {}, ) + expect(response.doc_auth_success?).to eq(true) + expect(response.selfie_success).to be_nil end end @@ -669,6 +671,8 @@ expect(response.selfie_check_performed?).to eq(true) expect(response.success?).to eq(true) expect(response.extra[:portrait_match_results]).to eq(selfie_results) + expect(response.doc_auth_success?).to eq(true) + expect(response.selfie_success).to eq(true) end end @@ -693,6 +697,8 @@ expect(response.selfie_check_performed?).to eq(true) expect(response.success?).to eq(false) expect(response.extra[:portrait_match_results]).to eq(selfie_results) + expect(response.doc_auth_success?).to eq(true) + expect(response.selfie_success).to eq(false) end end end @@ -704,6 +710,8 @@ it 'returns the expected values' do expect(response.selfie_check_performed?).to eq(false) expect(response.extra).not_to have_key(:portrait_match_results) + expect(response.doc_auth_success?).to eq(true) + expect(response.selfie_success).to be_nil end end end diff --git a/spec/services/reporting/account_reuse_report_spec.rb b/spec/services/reporting/account_reuse_report_spec.rb index 1aec438fdb0..7c399430e44 100644 --- a/spec/services/reporting/account_reuse_report_spec.rb +++ b/spec/services/reporting/account_reuse_report_spec.rb @@ -21,14 +21,6 @@ let(:sp_c) { 'c' } let(:sp_d) { 'd' } - def create_identity(user_id:, created_at:, provider:, verified_at:) - ServiceProviderIdentity.create( - user_id: user_id, service_provider: provider, - created_at: created_at, - last_ial2_authenticated_at: in_query, verified_at: verified_at - ) - end - before do create( :service_provider, @@ -136,10 +128,11 @@ def create_identity(user_id:, created_at:, provider:, verified_at:) users_to_query.each do |user| user[:sp].each_with_index do |sp, i| - create_identity( + ServiceProviderIdentity.create( user_id: user[:id], + service_provider: sp, created_at: user[:created_timestamp], - provider: sp, + last_ial2_authenticated_at: in_query, verified_at: user[:sp_timestamp][i], ) end @@ -148,10 +141,20 @@ def create_identity(user_id:, created_at:, provider:, verified_at:) # Create active profiles for total_proofed_identities # These 13 profiles will yield 10 active profiles in the results (1..10).each do |_| - create(:profile, :active, activated_at: in_query) + create( + :profile, + :active, + activated_at: in_query, + user: create(:user, :fully_registered, registered_at: in_query), + ) end (1..3).each do |_| - create(:profile, :active, activated_at: out_of_query) + create( + :profile, + :active, + activated_at: out_of_query, + user: create(:user, :fully_registered, registered_at: in_query), + ) end end diff --git a/spec/support/lexis_nexis_fixtures.rb b/spec/support/lexis_nexis_fixtures.rb index 6d3e80f9cfa..750c9a51baf 100644 --- a/spec/support/lexis_nexis_fixtures.rb +++ b/spec/support/lexis_nexis_fixtures.rb @@ -160,6 +160,10 @@ def true_id_response_success_3 read_fixture_file_at_path('true_id/true_id_response_success_3.json') end + def true_id_response_success_with_liveness + read_fixture_file_at_path('true_id/true_id_response_success_with_liveness.json') + end + def true_id_response_failure_no_liveness read_fixture_file_at_path('true_id/true_id_response_failure_no_liveness.json') end