From 0032b193cb716ab7a37ec2c00321200f4e4ec9ab Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:51:41 -0500 Subject: [PATCH 01/14] Remove legacy favicon assets (#11582) changelog: Internal, Code Cleanup, Remove legacy favicon assets --- public/apple-touch-icon.png | Bin 3090 -> 0 bytes public/browserconfig.xml | 9 --------- public/favicon-16x16.png | Bin 562 -> 0 bytes public/favicon-32x32.png | Bin 927 -> 0 bytes public/mstile-150x150.png | Bin 3188 -> 0 bytes public/safari-pinned-tab.svg | 1 - 6 files changed, 10 deletions(-) delete mode 100644 public/apple-touch-icon.png delete mode 100644 public/browserconfig.xml delete mode 100644 public/favicon-16x16.png delete mode 100644 public/favicon-32x32.png delete mode 100644 public/mstile-150x150.png delete mode 100644 public/safari-pinned-tab.svg diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png deleted file mode 100644 index bc8d68b0279aecbeb05f1c725c2db77f4193693b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3090 zcmb_edpy$(7k8h?lvbh%xt9?tzfyV%Z4q*7B;@|Mlnu)znIgG0mlY|OaxFq`h1}-0 ztz2rzgc)5bUOb3`8t+ zQdLiR%o0o6;O5x&lyX8j^TibkF+xt4MEOAiU?Hd|@J&S!th^UUIZ6MkElP0D{{q;j z?ebG7l1Ac>ZF4Q_xkek?TLGy>sm=WwU>HO!`|irGW@)RG9 zXGy`C7SQilchd7MyejfM7ZG%~85}<+{=_W+?b+OaGV*u8#TQdRq}*BeE?y1yT{&Lu zs^z>W{B_C4X&kXE-iK?DDFOF@^u1(i!j#4eq@jcbmzQ2z30y=`CxNlOek9v-x*$3D zp^=aOoyy9$2)YmT>13{&e^PbH+mp(PlIaogBcU(5B&os2jFN#IhpTx}f&c=7;seDCfawZWt^81BF(4}1m>+~n zQK$2PrG%lam;UPz$6dMLCIj~468T5nR0DgFC6e52aX4L0|CB-G+!CSvA%j9rR#NPg z^{#x%b?;Alj7zF5$1FFTxm$U6D{rYuGxCPBZiq9D?I$B9Nv}IeMt3;Ks`jGGLpzbz z^G<#8A$$}5V^3n^;t+ z%^_2E*MxvxRW)1;WMuADs3WL0AHTC~oBLns%YTJ9V)j9))v zSB?D~U2&Ps6@V%$;@-?N10qUvEuyO(5MKN?OD9Tu(ybl?qEOVvb9OoFuf0{=q3M zUw@geQysoa$kJhKeGvqbl9gzbXOS%ws;SQLZ$ECFXWcNY=6kY=-fKEz_I<_NG>HP4 zCKA)Vh!Xr@27@k+o7K0zw!JltWRsyNL{xj5O&+A*!arQH_@D!d%X3fgA&%38pn-J6 zOm?Aq(V$0jTz`Txa8o|?1`DKq<=w--wp@;ifHhDZjr+BEsX z()YyT_rWiV1;l+l;P#Se)}%@OPB?OL4Ws^Vhd5j~+iyVQJqmZ={kX#=!_prr!GW{r z;EM&AGHh~q=7zk>MAP%2tBFcFWq#XQ3!ZmWo&;S&e3cTRCWM{aANI20Z`TB_zM zrI{52G|0CIANPA}8OHog(%rqMJ?I!DZ#19k$l>I@pWPL&typ0IaT$sx!&n(U&Mvus z9CvD~7*Wx?Ty)x2scV7Hz>tO4gSdWEU=O;ZxK1ByVl|M3|0?PAFnX$;aN=rHeJ8|3 zj_A9lX3;eRQ$Di0>NLZX_Cb&GNaH^_?Ma!`#>XD*gyo;(D^apzx|ar*wH?_C>3h=r zfyp#EERIlW-WVi~N{Im2i$f61hNB_zG>9Clq*xm?5(2@LnT)eyYg-lf<6>-SjL}Ua z856mHzi*|S5?(GN#VB7{7X?+U)~gs{FxLls-ds27lA@=G z5?`-;u7X&rA6hMjL>V1}>WqyNIUD+1u=qdsq0_@#)qgnzPmrBHt1>X`GhXSpOsZ^E z6vq+X{U(RhbU$chR$|H}9F~%I=C{uDA%m$dR&wv(iD%cuE-)4S+~2+#9=g40UzWq+ z^fsJ}OqiNS{XAlghxheHE;gNrVoX}?+>V`GO;gvS)6aiSWb^Fk0CuI<8YaB` z_Q|zJMl3coaY%r;#USCacD%?1MLeC(^|br#7XZSMsQN1PyyE>7d;-gr!U+`c3$&*j?)Gd>lC@_kjxByXIr);a*rQK|^PXZN8gdp(DUz4!_cgS1(e}t1BM#RnB0A#-B_2= zeSx3J3Nj*a5A!#R(>;y8qj5lBSCYZb4}%1vSr-`r06nuI-gjo3yq-Q*!n#Re7q3c) zkgv<^h1aD_ZdsP|8VsI3$|t4KYSiGL^qc}G=%;$HAF9HB7;%VJf!j{K38uPUT^--$ zTDs;)_U$%rIUR%puNy6>ezmEtdM9xt*tKmx`O`^F4gtWLs&Y1U-LM5?ex;WR97~=% zZ?Ji~aJ)huy6sK-nTEXGk+^plWi0k1XHv!EqpP0CAy5mZN4%lIBKw#|nh>bC;q$0` zWRD|kF9g@NXtV2&DE4|XUWEM?g96ioW#YLr@dG;@1e@GO4E(-bZFxFKkf>tqba&T6 zaO1!25D2*eC4ac$`7ZZg3c~?&aofR>mD8{DEFWBxU{nF2@Y$NnfU>xMxa^SQJTj&^@T6!g; zx7g)Q9Vh*7)RYSgL@O?38E3zr-ET8FXCW@c104_R8au-&qD|7ZzpM#~TvSQD?JEw1 zH|$%Tfv}ibE~R3qTXkfM=}98=BIN6XFn;JPrQtkn%%p7dLZ^X6JZj4MbgH*D`-p4; zGA9g1pOZ+<;^9EZwGqrLgHH5<=h)9sUHoqzYDF&@x97T{wZ>54lwUj>N(u7fvS!6~ zL4Mr_t>`nb!)wT)dxJ-ZE~_?73H@t<_ko~@!zcyU)`v0tASsdm3wX`Hgk@hY`dScm T-)GnB^IbKuFfPA*`_X>@GY|u3 diff --git a/public/browserconfig.xml b/public/browserconfig.xml deleted file mode 100644 index 4a718762cf5..00000000000 --- a/public/browserconfig.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - #2b5797 - - - diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png deleted file mode 100644 index 9cb30c9ad4b7aeec088f14b77f53a5f49e42085b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 562 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GXl47zG1-LR^8||6p{+*m#we)+!m9 zXHv$uRm``ksN7X@xvONnTS;lXg2Fv9=dGfmTO=i)OW1CfQFtO^{Ycz;o4D)_3E4;D zHan$ccZ$hA60_MMA@@+iW}k%8Jqfez;&Kngtsjfo02xojY#xhS-xs&sEv^XEwM{~H zyM*i$F`z8a5V^82S3yEKWOK4#N6SjlIvbWQ=rK} z2Rs(Dx+iY&Sj6(Nu*F?5^SdHuJC!wdODOIZSJ*A40JM6WxXe}{5|iF4B9&KLzXKR3 z1|>m$!3>N{tZbZIe0==;`~u<lUe!3a;NQu zO%E?8F!?v@)UaJD6-s6M(^TRR-|Tv)L^1MNLETm6EuLpew)*^uvZ-O{UwAHXr_K9# uHifV&*4DOpx(W;)`@Vg8duEYUpS{$6lY>V>T@`_DW$<+Mb6Mw<&;$UvmgxBa diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png deleted file mode 100644 index 9d58b77d905ee9e93763ae462fc7e3ddbd712cfb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 927 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabRA=0U_2e*6XFWw{s*B3AlU$7gsuXS z4*MA#&KMi-6xVwt>aa~k<*t&;ZY8Dr3J%+NRPHESy%claDk}Ow#P)%t?IQ{M?GmzE z#TB+m$vqOa-X@~(M8tZ(gfdX@wz&CYaqI13@{h!l!|Nnn~|Ni^)=l}0N|9<}Z`}61TPoMw3|M=(i z+aIr9|9SrE&$CzGpFjWh?D@B+&wf9A_UroHPuFk!ym;f|)ob7Pop^ol(3dsapKadq zW#!f&8=iG@%qirU%!9<`TOtRX3729 zz|>;l>EaktaVuFt%AlE1*jQS`z-R$OV5n`afq{WhBZFxnn~Fl(Z5CGDzmpdp@MH39 ztP%LHe03W&b^ckdpfWN?A?CY4jStv6*! z`LVvObi8-3k@a$+){8S=K7adma$aMwqx&jzfh7;k7AguZQOIQSV=XC+}Ao)9*Mhw5MbvIV1_0;7h()78&qol`;+0EQchTL1t6 diff --git a/public/mstile-150x150.png b/public/mstile-150x150.png deleted file mode 100644 index 3c12cb3812ade280f269d7610c8f2911e36a7038..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3188 zcmcJRX*|^J9>&L`Y{M{?ERF0LOU>B-D!Z(e>}Lq6P?=$(Y-7tdV=cR^Nkj^fN-}E3 z(qdObV+=#Wkcn(VIC{==-krDS{(P=C*YEy(ulxGFx|7eJvlQTy;sb#|0@haM4j>Q* z`D-b|D?6KiBlMLkvie;?gQ&y=Hzn}9n>-Cw=ODv@j~+gu40O!_^7}> zW*|A9w-qY7Y=LlsSBulD(eSCM$!ECt7tHKVOfz zvQsD`S}%I@?8dimJ8^<+LHf6C#Y6kq|F2VE`^3Pt*g4coYy2K@H%Ofop_fLDQXQr; z(W{0pFuAE~0qeL3T@+}yBtxl=a=kkQ^Qe@f%C2*vL!UZ$xo-4Yu+ql&eh`Llk?wTa z1#H4Xhg_Flu{w6ppQBVRhmJd^zrS@TwmmtQs97%@PqmgF_*hhb?)XNPQ@aycjyNsV zuvw3q$$+JFh5R~ppqj5QE^cMLP?%qfygV`mM$Hf-UbT+Bi1l?w%b-<8IRmfHIG^E7 z`pT%r^oCxN`6i!PeNek-(xBo#*Si0Vhqlro0pSdzk>##Y%F_`yV=Wcn1HO4BT1!bZ zT^tgs@7FEU6zgkGE6%ucE3{WP1;q%);WEkAIQSRO`^|V?~-uLg_M)(C-b*xyD#z(KTDe=9Q28 zR~h7TTI0m`Ds|bOoBr1yxzMvzZeQKR0!vw}j--8)Pqo>0jGm%$o5uvmi<+VPad=w* zJlw|r=%Osi1OWY4oKOgfL+Bq#5U@fJ+2;)4a$r96i;AR}mXc5cXYZ#`ta;W0_pQ@o zrK01~n!&>*E8!DAR_60OKfG|}6uy?7HXdN3&cj`pdV@hwyk2I68B?fK-uOUrm-)c5 zBxY4sh%_R@ZEP&Neby(HgMPEyRyrpQvM?;oW3maH3_mA8KG_X&stLMZqK?NJ5!41K60h-?mn0I_1PaW>Ldw`FW}LlhVwQ}GNd)4nR1tdEPuPhQTj!`VTz zb(8IX@XmnSR^=+q-vOcQ6`hC$m4hwEq#MKLv}*FpEG?8~Lqs65+Kh(mmFYY=l%cN5 zS`ee-VcI%fo|poK$cpZ-%4eN2-gu(2((@4bpyXH{f3%0({^LkD#&+IKd|Z%#oCRXf zCn^BW0Yq1qqvvefa^f8;?>HXZjBms3U>@^N0?}zw@%NJ#D8YO`MsJ!MD`lOYqXs}a zBm?5e9pIx~7mazPL_{it5l|CTg3v?(oXhOeC85r@6w*<+o=Il;J8bgT(TLaE6pvPS zgweLPqbHVkzOS)q_P61O?45(#-%r_FMN)+ zB_^D?3mxyg;Y~hzptxl}Zj~XCfd^WL+NMuFRC|sStcNH%h=LO@hqYBKG7e7aIO&x5UX#&)<8S%#rbYgihwMHs5la}E+ZHU-oP7NjJOzOSfE51jBy}WW=&t2G zE-Dp!Gdj7TZlC3VC%eQ}9Yjb5y)iTd2*Zdv;r1tg`wHar7tRZr5-B8}StHt8tdO+~ zr0B5pVI#afGbD#IC5P4LN$Gn>Ec6 z>LL4skJyKFSJdsR7=tv;&x6S#;QW+Z|M3a6ocYAKuuw!=7Qw>4+4t#aMBjdxWCp(; zbtLLQ$23>FQ+uu#%zP-x>;kKV0>?S-i$vq zkOdOvl~97FdRNziool`Rz`@-=ohmDDN|WkiiX&oQ&n~nPa(2>or~Mop3(8Y<2rk^v zrvl#a(OBaB4Em?$nzNqc;>YD9tN5N;W|VtRL|>pk6A{r$o!7riDR_D# zr`9nKZmtQY8YsPHE*x&!a6$*nNq+EFlZf`J%&(w27BgFgV32XeziE`_Kg!aoV!P*X0^j2fJEL4dui(`DrUv%$% zm1Kyt)lQDLh3sExK1VyI$1)!q0s4NgO%;`d@K<5&vHSYsD&{VEg^kt3-Vabc zTkVc&`*B{8B!#@RF7Dz7LP|R4RC|VW0$WfIyeZfN+9Ml2U%zg^%~vOKDzSNWTf4qM z-O0zV%yQ63!jk(ZQx+|a6?4{gT8+5TARoc-h?{X~v0a)GHZDl*6quhhl_EXH~q+_qQ z0&yGt@|Kd^*vRux=xN@Uzj`nsz6gD!1Q-lGT36EX~*)Ws|8x8&G<5xO+&ueWD!qd(6 zc8P>bA-R-W?F>dA_7f)(n2SA03Zi=y6ib?=eKGCaqSx#8`5b)F`O#>;2 zKFC)Hj;@8E#+xiRmt)P>T25#&3i|g5(*lN8SvcXzKGZ^}#i38n+pMD}JhCW?&*4l&u88x!HYs zS@?X*KaRAFD-o-&Q&&HA7nH5y*&7%t6A`q~IUf_M0h?t$+1&ia-ks(}O4oU2eR@1n zc)mp9sqL6VhHb@~qx4F?heN!I*h@@(Z=gy)ZF#@7)0_LgMPk0jwD4=QAILY$3jg5RbD7(AVcgDgP}39r5%3r9gC&8=K*n>@ow%uP7#u$LdMZ$!=u zwe-CnfjC~p`?lA@VOO#=e7>lGl~}*N6s5@V|N2+|5AMYw501_xuJ$I%sgOTo>(l4V J>r5}k{TuI+w@3g0 diff --git a/public/safari-pinned-tab.svg b/public/safari-pinned-tab.svg deleted file mode 100644 index 6d7cda7d61c..00000000000 --- a/public/safari-pinned-tab.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From 7ea7f12443dd650e9aa2093c98f7b057bf077185 Mon Sep 17 00:00:00 2001 From: John Maxwell Date: Tue, 3 Dec 2024 13:24:39 -0500 Subject: [PATCH 02/14] LG-14010 - More detailed error page for Socure errors (#11560) * LG-14010 - More detailed error page for Socure errors Added plumbing and UX display for categorized Socure errors. changelog: Upcoming Features, Socure, Added nice error display for Socure failures --- .../concerns/idv/document_capture_concern.rb | 20 +- .../concerns/idv/socure_errors_concern.rb | 40 ++ .../idv/document_capture_controller.rb | 3 +- .../document_capture_controller.rb | 3 +- .../socure/document_capture_controller.rb | 21 +- .../idv/socure/document_capture_controller.rb | 20 +- app/models/document_capture_session.rb | 2 + app/presenters/socure_error_presenter.rb | 166 ++++++++ .../socure/responses/docv_result_response.rb | 3 +- .../document_capture_session_result.rb | 6 +- .../socure/document_capture/errors.html.erb | 17 + .../session_errors/state_id_warning.html.erb | 2 +- app/views/idv/shared/_error.html.erb | 37 +- .../socure/document_capture/errors.html.erb | 17 + app/views/idv/welcome/show.html.erb | 22 +- config/locales/en.yml | 16 + config/locales/es.yml | 16 + config/locales/fr.yml | 16 + config/locales/zh.yml | 16 + config/routes.rb | 4 + .../idv/document_capture_controller_spec.rb | 2 + .../document_capture_controller_spec.rb | 19 +- .../idv/link_sent_controller_spec.rb | 4 +- .../document_capture_controller_spec.rb | 26 +- .../doc_auth/socure_document_capture_spec.rb | 219 ++++++---- .../hybrid_socure_mobile_spec.rb | 403 +++++++++++------- .../features/document_capture_step_helper.rb | 4 + spec/support/socure_docv_fixtures.rb | 10 + spec/views/idv/shared/_error.html.erb_spec.rb | 8 +- 29 files changed, 848 insertions(+), 294 deletions(-) create mode 100644 app/controllers/concerns/idv/socure_errors_concern.rb create mode 100644 app/presenters/socure_error_presenter.rb create mode 100644 app/views/idv/hybrid_mobile/socure/document_capture/errors.html.erb create mode 100644 app/views/idv/socure/document_capture/errors.html.erb diff --git a/app/controllers/concerns/idv/document_capture_concern.rb b/app/controllers/concerns/idv/document_capture_concern.rb index edcfaa0f4d8..790f0c64907 100644 --- a/app/controllers/concerns/idv/document_capture_concern.rb +++ b/app/controllers/concerns/idv/document_capture_concern.rb @@ -13,7 +13,7 @@ def handle_stored_result(user: current_user, store_in_session: true) successful_response else extra = { stored_result_present: stored_result.present? } - failure(I18n.t('doc_auth.errors.general.network_error'), extra) + failure(nil, extra) end end @@ -22,13 +22,25 @@ def successful_response end # copied from Flow::Failure module - def failure(message, extra = nil) - flash[:error] = message - form_response_params = { success: false, errors: { message: message } } + def failure(message = nil, extra = nil) + form_response_params = { success: false } + form_response_params[:errors] = make_error_hash(message) form_response_params[:extra] = extra unless extra.nil? FormResponse.new(**form_response_params) end + def make_error_hash(message) + Rails.logger.info("make_error_hash: stored_result: #{stored_result.inspect}") + + error_hash = { message: message || I18n.t('doc_auth.errors.general.network_error') } + + if stored_result&.errors&.has_key?(:socure) + error_hash[:socure] = stored_result.errors[:socure] + end + + error_hash + end + def extract_pii_from_doc(user, store_in_session: false) if defined?(idv_session) # hybrid mobile does not have idv_session idv_session.had_barcode_read_failure = stored_result.attention_with_barcode? diff --git a/app/controllers/concerns/idv/socure_errors_concern.rb b/app/controllers/concerns/idv/socure_errors_concern.rb new file mode 100644 index 00000000000..036d8b31a2b --- /dev/null +++ b/app/controllers/concerns/idv/socure_errors_concern.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Idv + module SocureErrorsConcern + def errors + @presenter = socure_errors_presenter(handle_stored_result) + end + + def goto_in_person + InPersonEnrollment.find_or_initialize_by( + user: document_capture_session.user, + status: :establishing, + sponsor_id: IdentityConfig.store.usps_ipp_sponsor_id, + ).save! + + redirect_to idv_in_person_url + end + + private + + def remaining_attempts + RateLimiter.new( + user: document_capture_session.user, + rate_limit_type: :idv_doc_auth, + ).remaining_count + end + + def error_code_for(result) + if result.errors[:socure] + result.errors.dig(:socure, :reason_codes).first + elsif result.errors[:network] + :network + else + # No error information available (shouldn't happen). Default + # to :network if it does. + :network + end + end + end +end diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb index 59249f6d9f2..1b8cf732cb4 100644 --- a/app/controllers/idv/document_capture_controller.rb +++ b/app/controllers/idv/document_capture_controller.rb @@ -12,7 +12,8 @@ class DocumentCaptureController < ApplicationController before_action :confirm_step_allowed, unless: -> { allow_direct_ipp? } before_action :override_csp_to_allow_acuant before_action :set_usps_form_presenter - before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::LEXIS_NEXIS, false) } + before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::LEXIS_NEXIS, false) }, + only: :show def show analytics.idv_doc_auth_document_capture_visited(**analytics_arguments) diff --git a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb index 023ce16fa46..5893501d39b 100644 --- a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb +++ b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb @@ -11,7 +11,8 @@ class DocumentCaptureController < ApplicationController before_action :override_csp_to_allow_acuant before_action :confirm_document_capture_needed, only: :show before_action :set_usps_form_presenter - before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::LEXIS_NEXIS, true) } + before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::LEXIS_NEXIS, true) }, + only: :show def show analytics.idv_doc_auth_document_capture_visited(**analytics_arguments) diff --git a/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb index 243c4e5d598..44fccfa0182 100644 --- a/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb +++ b/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb @@ -8,10 +8,12 @@ class DocumentCaptureController < ApplicationController include DocumentCaptureConcern include Idv::HybridMobile::HybridMobileConcern include RenderConditionConcern + include SocureErrorsConcern check_or_render_not_found -> { IdentityConfig.store.socure_docv_enabled } before_action :check_valid_document_capture_session, except: [:update] - before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::SOCURE, true) } + before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::SOCURE, true) }, + only: :show def show Funnel::DocAuth::RegisterStep.new(document_capture_user.id, sp_session[:issuer]). @@ -28,8 +30,10 @@ def show @document_response = document_response @url = document_response.dig(:data, :url) - # placeholder until we get an error page for url not being present - return redirect_to idv_unavailable_url if @url.nil? + if @url.nil? + redirect_to idv_hybrid_mobile_socure_document_capture_errors_url + return + end document_capture_session = DocumentCaptureSession.find_by( uuid: document_capture_session_uuid, @@ -63,12 +67,21 @@ def update if result.success? redirect_to idv_hybrid_mobile_capture_complete_url else - redirect_to idv_hybrid_mobile_socure_document_capture_url + redirect_to idv_hybrid_mobile_socure_document_capture_errors_url end end private + def socure_errors_presenter(result) + SocureErrorPresenter.new( + error_code: error_code_for(result), + remaining_attempts:, + sp_name: decorated_sp_session&.sp_name || APP_NAME, + hybrid_mobile: true, + ) + end + def wait_for_result? return false if stored_result.present? diff --git a/app/controllers/idv/socure/document_capture_controller.rb b/app/controllers/idv/socure/document_capture_controller.rb index 1f764f64856..3f29ad659e1 100644 --- a/app/controllers/idv/socure/document_capture_controller.rb +++ b/app/controllers/idv/socure/document_capture_controller.rb @@ -7,11 +7,13 @@ class DocumentCaptureController < ApplicationController include IdvStepConcern include DocumentCaptureConcern include RenderConditionConcern + include SocureErrorsConcern check_or_render_not_found -> { IdentityConfig.store.socure_docv_enabled } before_action :confirm_not_rate_limited before_action :confirm_step_allowed - before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::SOCURE, false) } + before_action -> { redirect_to_correct_vendor(Idp::Constants::Vendors::SOCURE, false) }, + only: :show # reconsider and maybe remove these when implementing the real # update handler @@ -40,7 +42,10 @@ def show @url = document_response.dig(:data, :url) # placeholder until we get an error page for url not being present - return redirect_to idv_unavailable_url if @url.nil? + if @url.nil? + redirect_to idv_socure_document_capture_errors_url + return + end document_capture_session = DocumentCaptureSession.find_by( uuid: document_capture_session_uuid, @@ -79,7 +84,7 @@ def update if result.success? redirect_to idv_ssn_url else - redirect_to idv_socure_document_capture_url + redirect_to idv_socure_document_capture_errors_url end end @@ -107,6 +112,15 @@ def self.step_info private + def socure_errors_presenter(result) + SocureErrorPresenter.new( + error_code: error_code_for(result), + remaining_attempts:, + sp_name: decorated_sp_session&.sp_name || APP_NAME, + hybrid_mobile: false, + ) + end + def wait_for_result? return false if stored_result.present? diff --git a/app/models/document_capture_session.rb b/app/models/document_capture_session.rb index 3b117e56ded..7caa58febf5 100644 --- a/app/models/document_capture_session.rb +++ b/app/models/document_capture_session.rb @@ -21,6 +21,8 @@ def store_result_from_response(doc_auth_response) session_result.attention_with_barcode = doc_auth_response.attention_with_barcode? session_result.doc_auth_success = doc_auth_response.doc_auth_success? session_result.selfie_status = doc_auth_response.selfie_status + session_result.errors = doc_auth_response.errors + EncryptedRedisStructStorage.store( session_result, expires_in: IdentityConfig.store.doc_capture_request_valid_for_minutes.minutes.in_seconds, diff --git a/app/presenters/socure_error_presenter.rb b/app/presenters/socure_error_presenter.rb new file mode 100644 index 00000000000..d57dbad1664 --- /dev/null +++ b/app/presenters/socure_error_presenter.rb @@ -0,0 +1,166 @@ +# frozen_string_literal: true + +class SocureErrorPresenter + include Rails.application.routes.url_helpers + include ActionView::Helpers::UrlHelper + include ActionView::Helpers::TranslationHelper + include LinkHelper + + attr_reader :url_options + + def initialize(error_code:, remaining_attempts:, sp_name:, hybrid_mobile:) + @error_code = error_code + @remaining_attempts = remaining_attempts + @sp_name = sp_name + @hybrid_mobile = hybrid_mobile + @url_options = {} + end + + def heading + heading_string_for(error_code) + end + + def body_text + error_string_for(error_code) + end + + def rate_limit_text + if remaining_attempts == 1 + t('doc_auth.rate_limit_warning.singular_html') + else + t('doc_auth.rate_limit_warning.plural_html', remaining_attempts: remaining_attempts) + end + end + + def action + url = hybrid_mobile ? idv_hybrid_mobile_socure_document_capture_path + : idv_socure_document_capture_path + { + text: I18n.t('idv.failure.button.warning'), + url: url, + } + end + + def secondary_action_heading + I18n.t('in_person_proofing.headings.cta') + end + + def secondary_action_text + I18n.t('in_person_proofing.body.cta.prompt_detail') + end + + def secondary_action + url = hybrid_mobile ? idv_hybrid_mobile_socure_document_capture_goto_in_person_path + : idv_socure_document_capture_goto_in_person_path + + { + text: I18n.t('in_person_proofing.body.cta.button'), + url: url, + } + end + + def troubleshooting_heading + I18n.t('components.troubleshooting_options.ipp_heading') + end + + def options + [ + { + url: help_center_redirect_path( + category: 'verify-your-identity', + article: 'how-to-add-images-of-your-state-issued-id', + ), + text: I18n.t('idv.troubleshooting.options.doc_capture_tips'), + isExternal: true, + }, + { + url: help_center_redirect_path( + category: 'verify-your-identity', + article: 'accepted-identification-documents', + ), + text: I18n.t('idv.troubleshooting.options.supported_documents'), + isExternal: true, + }, + { + url: return_to_sp_failure_to_proof_url(step: 'document_capture'), + text: t( + 'idv.failure.verify.fail_link_html', + sp_name: sp_name, + ), + isExternal: true, + }, + ] + end + + def step_indicator_steps + Idv::StepIndicatorConcern::STEP_INDICATOR_STEPS + end + + private + + attr_reader :error_code, :remaining_attempts, :sp_name, :hybrid_mobile + + SOCURE_ERROR_MAP = { + 'I848' => 'unreadable_id', + 'I854' => 'unreadable_id', + 'R810' => 'unreadable_id', + 'R820' => 'unreadable_id', + 'R822' => 'unreadable_id', + 'R823' => 'unreadable_id', + 'R824' => 'unreadable_id', + 'R825' => 'unreadable_id', + 'R826' => 'unreadable_id', + 'R831' => 'unreadable_id', + 'R833' => 'unreadable_id', + 'R838' => 'unreadable_id', + 'R859' => 'unreadable_id', + 'R861' => 'unreadable_id', + 'R863' => 'unreadable_id', + + 'I849' => 'unaccepted_id_type', + 'R853' => 'unaccepted_id_type', + 'R862' => 'unaccepted_id_type', + + 'R827' => 'expired_id', + + 'I808' => 'low_resolution', + + 'R845' => 'underage', + + 'I856' => 'id_not_found', + 'R819' => 'id_not_found', + }.freeze + + def remapped_error(error_code) + SOCURE_ERROR_MAP[error_code] || 'unreadable_id' + end + + def heading_string_for(error_code) + if error_code == :network + t('doc_auth.headers.general.network_error') + else + # i18n-tasks-use t('doc_auth.headers.unreadable_id') + # i18n-tasks-use t('doc_auth.headers.unaccepted_id_type') + # i18n-tasks-use t('doc_auth.headers.expired_id') + # i18n-tasks-use t('doc_auth.headers.low_resolution') + # i18n-tasks-use t('doc_auth.headers.underage') + # i18n-tasks-use t('doc_auth.headers.id_not_found') + I18n.t("doc_auth.headers.#{remapped_error(error_code)}") + end + end + + def error_string_for(error_code) + if error_code == :network + t('doc_auth.errors.general.new_network_error') + elsif remapped_error(error_code) == 'underage' # special handling because it says 'Login.gov' + I18n.t('doc_auth.errors.underage', app_name: APP_NAME) + else + # i18n-tasks-use t('doc_auth.errors.unreadable_id') + # i18n-tasks-use t('doc_auth.errors.unaccepted_id_type') + # i18n-tasks-use t('doc_auth.errors.expired_id') + # i18n-tasks-use t('doc_auth.errors.low_resolution') + # i18n-tasks-use t('doc_auth.errors.id_not_found') + I18n.t("doc_auth.errors.#{remapped_error(error_code)}") + end + end +end diff --git a/app/services/doc_auth/socure/responses/docv_result_response.rb b/app/services/doc_auth/socure/responses/docv_result_response.rb index d2beacb7966..4a93b9232ce 100644 --- a/app/services/doc_auth/socure/responses/docv_result_response.rb +++ b/app/services/doc_auth/socure/responses/docv_result_response.rb @@ -38,6 +38,7 @@ class DocvResultResponse < DocAuth::Response def initialize(http_response:, biometric_comparison_required: false) @http_response = http_response + @biometric_comparison_required = biometric_comparison_required @pii_from_doc = read_pii @@ -99,7 +100,7 @@ def error_messages return {} if successful_result? { - reason_codes: get_data(DATA_PATHS[:reason_codes]), + socure: { reason_codes: get_data(DATA_PATHS[:reason_codes]) }, } end diff --git a/app/services/document_capture_session_result.rb b/app/services/document_capture_session_result.rb index 31ddc9b5e0b..dc45a26a2c2 100644 --- a/app/services/document_capture_session_result.rb +++ b/app/services/document_capture_session_result.rb @@ -10,11 +10,13 @@ :failed_back_image_fingerprints, :failed_selfie_image_fingerprints, :captured_at, - :doc_auth_success, :selfie_status, + :doc_auth_success, + :selfie_status, + :errors, keyword_init: true, allowed_members: [:id, :success, :attention_with_barcode, :failed_front_image_fingerprints, :failed_back_image_fingerprints, :failed_selfie_image_fingerprints, - :captured_at, :doc_auth_success, :selfie_status] + :captured_at, :doc_auth_success, :selfie_status, :errors], ) do include DocAuth::SelfieConcern diff --git a/app/views/idv/hybrid_mobile/socure/document_capture/errors.html.erb b/app/views/idv/hybrid_mobile/socure/document_capture/errors.html.erb new file mode 100644 index 00000000000..a7210f8471f --- /dev/null +++ b/app/views/idv/hybrid_mobile/socure/document_capture/errors.html.erb @@ -0,0 +1,17 @@ +<%= render( + 'idv/shared/error', + type: :warning, + heading: @presenter.heading, + action: @presenter.action, + secondary_action: @presenter.secondary_action, + current_step: :verify_id, + troubleshooting_heading: @presenter.troubleshooting_heading, + secondary_action_heading: @presenter.secondary_action_heading, + secondary_action_text: @presenter.secondary_action_text, + options: @presenter.options, + step_indicator_steps: @presenter.step_indicator_steps, + ) do +%> +

<%= @presenter.body_text %>

+

<%= @presenter.rate_limit_text %>

+<% end %> diff --git a/app/views/idv/session_errors/state_id_warning.html.erb b/app/views/idv/session_errors/state_id_warning.html.erb index 8534c683d64..183b28a7f8e 100644 --- a/app/views/idv/session_errors/state_id_warning.html.erb +++ b/app/views/idv/session_errors/state_id_warning.html.erb @@ -8,7 +8,7 @@ text: t('idv.warning.state_id.try_again_button'), url: @try_again_path, }, - action_secondary: { + secondary_action: { text: t('idv.warning.state_id.cancel_button', app_name: APP_NAME), url: return_to_sp_failure_to_proof_url(step: 'verify_info', location: 'state_id_warning'), }, diff --git a/app/views/idv/shared/_error.html.erb b/app/views/idv/shared/_error.html.erb index f2cff69cb3d..f9668ab2344 100644 --- a/app/views/idv/shared/_error.html.erb +++ b/app/views/idv/shared/_error.html.erb @@ -5,17 +5,24 @@ locals: * heading: Primary page heading text. Also used as page title if title is not given. * title: Optional custom page title, defaulting to the heading. * action: Optional hash of `text`, `url`, optional `method` of a primary action link. -* action_secondary: Optional hash of `text`, `url`, optional `method` of a secondary action link. +* secondary_action: Optional hash of `text`, `url`, optional `method` of a secondary action link. * current_step: Optionally identify the current step of the IdV flow. If provided, the step indicator will be rendered. * options: Array of troubleshooting options. %> <% if local_assigns.fetch(:type, :error) == :error icon_name = :error - troubleshooting_heading = t('idv.troubleshooting.headings.need_assistance') -else - icon_name = :warning - troubleshooting_heading = t('components.troubleshooting_options.default_heading') -end %> + else + icon_name = :warning + end + + troubleshooting_heading = local_assigns.fetch(:troubleshooting_heading) do + if local_assigns.fetch(:type, :error) == :error + troubleshooting_heading = t('idv.troubleshooting.headings.need_assistance') + else + troubleshooting_heading = t('components.troubleshooting_options.default_heading') + end + end + %> <% self.title = local_assigns.fetch(:title, heading) %> @@ -46,13 +53,23 @@ end %> ) %> - <% if local_assigns[:action_secondary] %> + <% if local_assigns[:secondary_action] %> +
+ + <% if local_assigns[:secondary_action_heading] %> +

<%= local_assigns[:secondary_action_heading] %>

+ <% end %> + + <% if local_assigns[:secondary_action_text] %> +

<%= local_assigns[:secondary_action_text] %>

+ <% end %> +
<%= button_or_link_to( - action_secondary[:text], - action_secondary[:url], + secondary_action[:text], + secondary_action[:url], class: 'usa-button usa-button--big usa-button--wide usa-button--outline', - method: action_secondary[:method], + method: secondary_action[:method], ) %>
<% end %> diff --git a/app/views/idv/socure/document_capture/errors.html.erb b/app/views/idv/socure/document_capture/errors.html.erb new file mode 100644 index 00000000000..741e7852045 --- /dev/null +++ b/app/views/idv/socure/document_capture/errors.html.erb @@ -0,0 +1,17 @@ +<%= render( + 'idv/shared/error', + type: :warning, + heading: @presenter.heading, + action: @presenter.action, + secondary_action: @presenter.secondary_action, + current_step: :verify_id, + troubleshooting_heading: @presenter.troubleshooting_heading, + secondary_action_heading: @presenter.secondary_action_heading, + secondary_action_text: @presenter.secondary_action_text, + options: @presenter.options, + step_indicator_steps: @presenter.step_indicator_steps, + ) do +%> +

<%= @presenter.body_text %>

+

<%= @presenter.rate_limit_text %>

+<% end %> diff --git a/app/views/idv/welcome/show.html.erb b/app/views/idv/welcome/show.html.erb index 39653251a16..6663f75943b 100644 --- a/app/views/idv/welcome/show.html.erb +++ b/app/views/idv/welcome/show.html.erb @@ -36,18 +36,18 @@ method: 'put', html: { class: 'margin-top-2 margin-bottom-5 js-consent-continue-form' }, ) do |f| %> +
+ <%= render( + SpinnerButtonComponent.new( + type: :submit, + big: true, + wide: true, + spin_on_click: false, + ).with_content(t('doc_auth.buttons.continue')), + ) %> +
+ <% end %> -
- <%= render( - SpinnerButtonComponent.new( - type: :submit, - big: true, - wide: true, - spin_on_click: false, - ).with_content(t('doc_auth.buttons.continue')), - ) %> -
-<% end %> <%= render 'shared/cancel', link: idv_cancel_path(step: 'welcome') %> <% end %> <%= javascript_packs_tag_once('document-capture-welcome') %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 2ba1e98735e..7593fbd2ff2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -537,11 +537,13 @@ doc_auth.errors.document_capture_canceled: You have canceled uploading photos of doc_auth.errors.dpi.failed_short: Image is too small or blurry, please try again. doc_auth.errors.dpi.top_msg: We couldn’t read your ID. Your image size may be too small, or your ID is too small or blurry in the photo. Make sure your ID is large within the image frame and try taking a new picture. doc_auth.errors.dpi.top_msg_plural: We couldn’t read your ID. Your image sizes may be too small, or your ID is too small or blurry in the photos. Make sure your ID is large within the image frame and try taking new pictures. +doc_auth.errors.expired_id: You cannot use an expired ID card for identity verification. If your ID card is not expired, make sure everything on your ID is in focus and that the card fills the frame. doc_auth.errors.file_type.invalid: This file type is not accepted, please choose a JPG or PNG file. doc_auth.errors.general.fallback_field_level: Please add a new image doc_auth.errors.general.multiple_back_id_failures: We couldn’t verify the back of your ID. Try taking a new picture. doc_auth.errors.general.multiple_front_id_failures: We couldn’t verify the front of your ID. Try taking a new picture. doc_auth.errors.general.network_error: We are having technical difficulties on our end. Please try to submit your images again later. +doc_auth.errors.general.new_network_error: Try again later. doc_auth.errors.general.no_liveness: Try taking new pictures. doc_auth.errors.general.selfie_failure: We couldn’t verify the photo of yourself. Try taking a new picture. doc_auth.errors.general.selfie_failure_help_link_text: Review more tips for taking clear photos @@ -555,6 +557,8 @@ doc_auth.errors.http.image_size.failed_short: Image file is not supported, pleas doc_auth.errors.http.image_size.top_msg: Your image size is too large or too small. Please add images of your ID that are about 2025 x 1275 pixels. doc_auth.errors.http.pixel_depth.failed_short: Image file is not supported, please try again. doc_auth.errors.http.pixel_depth.top_msg: The pixel depth of your image file is not supported. Please take new photos of your ID and try again. Supported image pixel depth is 24-bit RGB. +doc_auth.errors.id_not_found: Make sure you are taking a picture of your physical ID and not of a photo or screenshot. +doc_auth.errors.low_resolution: We couldn’t verify your ID because your device could not take a clear enough photo. If this error keeps occurring, try using another device or verify your ID at a local Post Office. doc_auth.errors.not_a_file: The selection was not a valid file. doc_auth.errors.phone_step_incomplete: You must go to your phone and upload photos of your ID before continuing. We sent you a link with instructions. doc_auth.errors.pii.birth_date_min_age: Your birthday does not meet the minimum age requirement. @@ -567,11 +571,21 @@ doc_auth.errors.send_link_limited: You tried too many times, please try again in doc_auth.errors.sharpness.failed_short: Image is blurry, please try again. doc_auth.errors.sharpness.top_msg: We couldn’t read your ID. Your photo may be too blurry or dark. Try taking a new picture in a bright area. doc_auth.errors.sharpness.top_msg_plural: We couldn’t read your ID. Your photos may be too blurry or dark. Try taking new pictures in a bright area. +doc_auth.errors.unaccepted_id_type: We do not accept passports, military IDs, paper IDs or temporary IDs. You may only use a driver’s license or an ID issued by a U.S. state or territory. +doc_auth.errors.underage: You must be at least 13 years old to use %{app_name}. If you are 13 or older, try again and make sure your date of birth is in focus. +doc_auth.errors.unreadable_id: Take your photos in a well-lit area without shadows or glare. Make sure that everything on your ID is in focus and that the card fills the frame. doc_auth.errors.upload_error: Sorry, something went wrong on our end. doc_auth.forms.change_file: Change file doc_auth.forms.choose_file_html: Drag file here or choose from folder doc_auth.forms.doc_success: We verified your information doc_auth.forms.selected_file: Selected file +doc_auth.headers.expired_id: Your ID may have expired +doc_auth.headers.general.network_error: We are having technical difficulties +doc_auth.headers.id_not_found: We couldn’t find your ID. +doc_auth.headers.low_resolution: Your device’s camera may not be supported. +doc_auth.headers.unaccepted_id_type: Use a driver’s license or a state ID +doc_auth.headers.underage: Age requirement not met +doc_auth.headers.unreadable_id: We could not read your ID doc_auth.headings.address: Update your mailing address doc_auth.headings.back: Back of your driver’s license or state ID doc_auth.headings.capture_complete: We verified your ID @@ -685,6 +699,8 @@ doc_auth.instructions.text1: Other forms of ID are not accepted. We’ll check t doc_auth.instructions.text2: You will not need your physical SSN card. doc_auth.instructions.text3: We match your phone number with your personal information and send a one-time code to your phone. doc_auth.instructions.text4: Your password saves and encrypts your personal information. +doc_auth.rate_limit_warning.plural_html: For security reasons, you have %{remaining_attempts} attempts remaining. +doc_auth.rate_limit_warning.singular_html: For security reasons, you have 1 attempt remaining. doc_auth.tips.document_capture_hint: Must be a JPG or PNG doc_auth.tips.document_capture_id_text1: Use a flat and dark surface doc_auth.tips.document_capture_id_text2: Take photos in a well-lit place diff --git a/config/locales/es.yml b/config/locales/es.yml index d93a88dbcf3..baffc5ec289 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -548,11 +548,13 @@ doc_auth.errors.document_capture_canceled: Ha cancelado la carga de fotos de su doc_auth.errors.dpi.failed_short: La imagen es demasiado pequeña o está borrosa; inténtelo de nuevo. doc_auth.errors.dpi.top_msg: No pudimos leer su identificación. Es posible que el tamaño de su imagen o de su identificación sea demasiado pequeño o que la foto esté borrosa. Asegúrese de que su identificación se vea más grande dentro del marco de la imagen e intente tomar una nueva foto. doc_auth.errors.dpi.top_msg_plural: No pudimos leer su identificación. Es posible que el tamaño de sus imágenes o de su identificación sea demasiado pequeño o que las fotos estén borrosas. Asegúrese de que su identificación se vea grande dentro del marco de la imagen e intente tomar nuevas fotos. +doc_auth.errors.expired_id: No puede usar una tarjeta de identificación vencida para verificar su identidad. Si su tarjeta de identificación no está vencida, revise que la información en esta se vea nítida y que la tarjeta llene el marco. doc_auth.errors.file_type.invalid: No se acepta este tipo de archivo; elija un archivo JPG o PNG. doc_auth.errors.general.fallback_field_level: Agregue una imagen nueva doc_auth.errors.general.multiple_back_id_failures: No pudimos verificar el reverso de su identificación. Intente tomar una nueva foto. doc_auth.errors.general.multiple_front_id_failures: No pudimos verificar el frente de su identificación. Intente tomar una nueva foto. doc_auth.errors.general.network_error: Estamos teniendo problemas técnicos. Intente enviar sus imágenes de nuevo más tarde. +doc_auth.errors.general.new_network_error: Vuelva a intentarlo más tarde. doc_auth.errors.general.no_liveness: Intente tomar nuevas fotos. doc_auth.errors.general.selfie_failure: No pudimos verificar su foto. Intente tomar una nueva foto. doc_auth.errors.general.selfie_failure_help_link_text: Consulte más consejos para tomar fotos claras @@ -566,6 +568,8 @@ doc_auth.errors.http.image_size.failed_short: El archivo de la imagen no es comp doc_auth.errors.http.image_size.top_msg: El tamaño de la imagen es demasiado grande o demasiado pequeño. Añada imágenes de su identificación de unos 2025 x 1275 píxeles. doc_auth.errors.http.pixel_depth.failed_short: El archivo de la imagen no es compatible; inténtelo de nuevo. doc_auth.errors.http.pixel_depth.top_msg: La profundidad de píxel de su archivo de imagen no es compatible. Tome nuevas fotos de su identificación e inténtelo de nuevo. La profundidad de píxel de la imagen admitida es RGB de 24 bits. +doc_auth.errors.id_not_found: Verifique que está tomando una foto de su identificación física y no de una fotografía o captura de pantalla. +doc_auth.errors.low_resolution: No pudimos verificar su identificación porque su dispositivo no tomó una fotografía bastante clara. Si se sigue presentando este error, use otro dispositivo o verifique su identidad en una oficina de correos de su localidad. doc_auth.errors.not_a_file: La selección no era un archivo válido. doc_auth.errors.phone_step_incomplete: Debe ir a su teléfono y cargar fotos de su identificación antes de continuar. Le enviamos un vínculo con instrucciones. doc_auth.errors.pii.birth_date_min_age: Su fecha de nacimiento no cumple con el requisito de edad mínima. @@ -578,11 +582,21 @@ doc_auth.errors.send_link_limited: Lo intentó demasiadas veces; vuelva a intent doc_auth.errors.sharpness.failed_short: La imagen está borrosa; inténtelo de nuevo. doc_auth.errors.sharpness.top_msg: No pudimos leer su identificación. Es posible que su foto esté demasiado borrosa u oscura. Intente tomar una nueva foto en un lugar iluminado. doc_auth.errors.sharpness.top_msg_plural: No pudimos leer su identificación. Es posible que sus fotos estén demasiado borrosas u oscuras. Intente tomar nuevas fotos en un lugar iluminado. +doc_auth.errors.unaccepted_id_type: No aceptamos pasaportes, identificaciones militares, ni identificaciones impresas o temporales. Solo puede usar una licencia de conducir o una identificación emitida por un estado o territorio de los EE. UU. +doc_auth.errors.underage: Debe tener al menos 13 años para usar %{app_name}. Si tiene 13 años o más, vuelva a intentarlo y verifique que su fecha de nacimiento se vea nítida. +doc_auth.errors.unreadable_id: Tome sus fotografías en un lugar bien iluminado sin sombras ni reflejos. Revise que la información en su identificación se vea nítida y que la tarjeta llene el marco. doc_auth.errors.upload_error: Lo sentimos, algo no funcionó bien. doc_auth.forms.change_file: Cambiar archivo doc_auth.forms.choose_file_html: Arrastrar el archivo aquí o seleccionarlo de la carpeta doc_auth.forms.doc_success: Verificamos su información doc_auth.forms.selected_file: Archivo seleccionado +doc_auth.headers.expired_id: Su identificación puede estar vencida +doc_auth.headers.general.network_error: Estamos teniendo problemas técnicos +doc_auth.headers.id_not_found: No pudimos encontrar su identificación +doc_auth.headers.low_resolution: Es posible que la cámara de su dispositivo no sea compatible +doc_auth.headers.unaccepted_id_type: Use una licencia de conducir o una identificación estatal +doc_auth.headers.underage: No se cumplió con el requisito de edad +doc_auth.headers.unreadable_id: No pudimos leer su identificación doc_auth.headings.address: Actualice su dirección postal doc_auth.headings.back: Reverso de su licencia de conducir o identificación estatal doc_auth.headings.capture_complete: Verificamos su identificación @@ -696,6 +710,8 @@ doc_auth.instructions.text1: No se aceptan otras formas de identificación. Revi doc_auth.instructions.text2: No necesita la tarjeta física del Seguro Social. doc_auth.instructions.text3: Revisamos que su número de teléfono coincida con su información personal y enviamos un código de un solo uso a su teléfono. doc_auth.instructions.text4: Su contraseña guarda y cifra su información personal. +doc_auth.rate_limit_warning.plural_html: Por motivos de seguridad, le quedan %{remaining_attempts} intentos. +doc_auth.rate_limit_warning.singular_html: Por motivos de seguridad, le queda un intento. doc_auth.tips.document_capture_hint: Debe ser un archivo JPG o PNG doc_auth.tips.document_capture_id_text1: Use una superficie plana y de color oscuro. doc_auth.tips.document_capture_id_text2: Tome fotos en un lugar bien iluminado diff --git a/config/locales/fr.yml b/config/locales/fr.yml index e20fa43644b..dc2ae99a5ed 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -537,11 +537,13 @@ doc_auth.errors.document_capture_canceled: Vous avez annulé le téléchargement doc_auth.errors.dpi.failed_short: Image trop petite ou floue, veuillez réessayer. doc_auth.errors.dpi.top_msg: Nous n’avons pas pu lire votre pièce d’identité. Il se peut que votre image soit de trop petite taille ou que votre pièce d’identité soit trop petite ou floue sur la photo. Assurez-vous que votre pièce d’identité remplisse le cadre de l’image, puis essayez de prendre une nouvelle photo. doc_auth.errors.dpi.top_msg_plural: Nous n’avons pas pu lire votre pièce d’identité. Il se peut que votre image soit de trop petite taille ou que votre pièce d’identité soit trop petite ou floue sur les photos. Assurez-vous que votre pièce d’identité remplisse le cadre de l’image, puis essayez de prendre une nouvelle photo. +doc_auth.errors.expired_id: Il n’est pas possible d’utiliser une pièce d’identité périmée pour confirmer votre identité. Si votre pièce d’identité est toujours valable, veillez à ce que tous les renseignements y figurant soient nets et que l’ensemble de la carte remplisse le cadre. doc_auth.errors.file_type.invalid: Ce type de fichier n’est pas accepté ; veuillez choisir un fichier JPG ou PNG. doc_auth.errors.general.fallback_field_level: Veuillez ajouter une nouvelle image doc_auth.errors.general.multiple_back_id_failures: Nous n’avons pas pu vérifier le verso de votre pièce d’identité. Essayez de prendre une nouvelle photo. doc_auth.errors.general.multiple_front_id_failures: Nous n’avons pas pu vérifier le recto de votre pièce d’identité. Essayez de prendre une nouvelle photo. doc_auth.errors.general.network_error: Nous rencontrons actuellement des difficultés techniques de notre côté. Veuillez réessayer d’envoyer vos images ultérieurement. +doc_auth.errors.general.new_network_error: Veuillez réessayer ultérieurement. doc_auth.errors.general.no_liveness: Essayez de prendre de nouvelles photos. doc_auth.errors.general.selfie_failure: Nous n’avons pas pu vérifier votre photo. Essayez de prendre une nouvelle photo. doc_auth.errors.general.selfie_failure_help_link_text: Consultez plus de conseils pour prendre des photos claires @@ -555,6 +557,8 @@ doc_auth.errors.http.image_size.failed_short: Le fichier image n’est pas pris doc_auth.errors.http.image_size.top_msg: La taille de votre image est trop grande ou trop petite. Veuillez ajouter des images de votre pièce d’identité d’environ 2 025 x 1 275 pixels. doc_auth.errors.http.pixel_depth.failed_short: Le fichier image n’est pas pris en charge, veuillez réessayer. doc_auth.errors.http.pixel_depth.top_msg: La profondeur de pixel de votre fichier image n’est pas prise en charge. Veuillez prendre de nouvelles photos de votre pièce d’identité et réessayer. La profondeur de pixel de l’image prise en charge est de 24 bits RGB. +doc_auth.errors.id_not_found: Veillez à prendre une photo de votre pièce d’identité physique et non d’une photo ou d’une capture d’écran. +doc_auth.errors.low_resolution: Nous n’avons pas pu vérifier votre identité car votre appareil n’a pas pris une photo suffisamment claire. Si cette erreur persiste, utilisez un autre appareil ou confirmez votre identité dans un bureau de poste local. doc_auth.errors.not_a_file: La sélection n’était pas un fichier valide. doc_auth.errors.phone_step_incomplete: Vous devez aller sur votre téléphone et télécharger des photos de votre pièce d’identité avant de continuer. Nous vous avons envoyé un lien avec des instructions. doc_auth.errors.pii.birth_date_min_age: Votre anniversaire ne correspond pas à l’âge minimum requis. @@ -567,11 +571,21 @@ doc_auth.errors.send_link_limited: Vous avez essayé trop de fois, veuillez rée doc_auth.errors.sharpness.failed_short: L’image est floue, veuillez réessayer. doc_auth.errors.sharpness.top_msg: Nous n’avons pas pu lire votre pièce d’identité. Il se peut que votre photo soit trop floue ou trop sombre. Essayez de prendre une nouvelle photo dans un endroit bien éclairé. doc_auth.errors.sharpness.top_msg_plural: Nous n’avons pas pu lire votre pièce d’identité. Il se peut que vos photos soient trop floues ou trop sombres. Essayez de prendre de nouvelles photos dans un endroit bien éclairé. +doc_auth.errors.unaccepted_id_type: Nous n’acceptons pas de passeports, de cartes d’identité militaires, de pièces d’identité papier ou provisoires. Vous pouvez uniquement utiliser un permis de conduire ou une pièce d’identité délivré par un État ou un territoire des États-Unis. +doc_auth.errors.underage: Vous devez être âgé de 13 ans au moins pour utiliser %{app_name}. Si vous avez 13 ans ou plus, réessayez et assurez-vous que votre date de naissance est bien visible. +doc_auth.errors.unreadable_id: Prenez vos photos dans un endroit bien éclairé sans ombre ou reflet. Veillez à ce que tous les renseignements figurant sur votre pièce d’identité soient nets et que l’ensemble de la pièce soit visible à l’intérieur du cadre. doc_auth.errors.upload_error: Désolé, il y a eu un problème de notre côté. doc_auth.forms.change_file: Changer de fichier doc_auth.forms.choose_file_html: Faites glisser le fichier ici ou choisissez dans un dossier doc_auth.forms.doc_success: Nous avons vérifié vos informations doc_auth.forms.selected_file: Fichier sélectionné +doc_auth.headers.expired_id: Votre pièce d’identité est peut-être périmée +doc_auth.headers.general.network_error: Nous rencontrons des difficultés techniques +doc_auth.headers.id_not_found: Nous n’avons pas trouvé votre pièce d’identité +doc_auth.headers.low_resolution: Il se peut que la caméra de votre appareil ne soit pas prise en charge +doc_auth.headers.unaccepted_id_type: Utiliser un permis de conduire ou une carte d’identité d’un État +doc_auth.headers.underage: Condition d’âge non remplie +doc_auth.headers.unreadable_id: Nous n’avons pas pu lire votre pièce d’identité doc_auth.headings.address: Mettre à jour votre adresse postale doc_auth.headings.back: Verso de votre permis de conduire ou de votre carte d’identité d’un État doc_auth.headings.capture_complete: Nous avons vérifié votre pièce d’identité @@ -685,6 +699,8 @@ doc_auth.instructions.text1: Les autres pièces d’identité ne sont pas accept doc_auth.instructions.text2: Vous n’aurez pas besoin de votre carte de sécurité sociale papier. doc_auth.instructions.text3: Nous comparons votre numéro de téléphone à vos informations personnelles et vous envoyons un code à usage unique sur votre téléphone. doc_auth.instructions.text4: Votre mot de passe s’enregistre et chiffre vos informations personnelles. +doc_auth.rate_limit_warning.plural_html: Pour des raisons de sécurité, il vous reste %{remaining_attempts} tentatives. +doc_auth.rate_limit_warning.singular_html: Pour des raisons de sécurité, il vous reste une tentative. doc_auth.tips.document_capture_hint: Doit être au format JPG ou PNG doc_auth.tips.document_capture_id_text1: Utilisez une surface plane et foncée doc_auth.tips.document_capture_id_text2: Prenez vos photos dans un endroit bien éclairé diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 89061d000ad..ea75d8f839a 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -548,11 +548,13 @@ doc_auth.errors.document_capture_canceled: 你已取消了用手机上传身份 doc_auth.errors.dpi.failed_short: 图像太小或模糊,请再试一次。 doc_auth.errors.dpi.top_msg: 我们无法辨认你的身份证件。你的图像尺寸可能太小,或者照片中你的身份证件太小或太模糊。确保你的身份证件在图框中比较大,然后试着重拍一下。 doc_auth.errors.dpi.top_msg_plural: 我们无法读取你的身份证件。你的图像尺寸可能太小,或者照片中你的身份证件太小或模糊。确保你的身份证件在图片框中很大,然后试着拍一张新照片。 +doc_auth.errors.expired_id: 你不能使用过期的身份证件进行身份验证。如果你的身份证件未过期,请确保你的身份证件上的所有内容对焦,而且身份卡填满画面。 doc_auth.errors.file_type.invalid: 这一文件类型我们不接受。请选择一个 JPG 或 PNG 文件。 doc_auth.errors.general.fallback_field_level: 请添加一个新图像 doc_auth.errors.general.multiple_back_id_failures: 我们无法验证你身份证件的背面。尝试重拍一张。 doc_auth.errors.general.multiple_front_id_failures: 我们无法验证你身份证件的正面。尝试重拍一张。 doc_auth.errors.general.network_error: 我们这边有技术困难。请稍后再提交你的图像。 +doc_auth.errors.general.new_network_error: 请稍后再试。 doc_auth.errors.general.no_liveness: 尝试重拍。 doc_auth.errors.general.selfie_failure: 我们无法验证你自己的照片。请重拍一张。 doc_auth.errors.general.selfie_failure_help_link_text: 查看更多有关拍摄清晰照片的提示 @@ -566,6 +568,8 @@ doc_auth.errors.http.image_size.failed_short: 系统不支持图像文件,请 doc_auth.errors.http.image_size.top_msg: 你的图像尺寸太大或太小。请添加你身份证件的图像,其像素应约为 2025 x 1275。 doc_auth.errors.http.pixel_depth.failed_short: 系统不支持图像文件,请再试一次。 doc_auth.errors.http.pixel_depth.top_msg: 你图像文件的像素深度系统不支持。请重拍你的身份证件并再试一次。系统支持的图像像素深度为 24位 RGB。 +doc_auth.errors.id_not_found: 确保你拍摄的是你的实体身份证件,而不是照片或屏幕截图。 +doc_auth.errors.low_resolution: 我们无法验证你的身份,因为你设备拍摄的照片不够清晰。如果这个错误持续发生,请使用其他设备或在当地邮局验证你的身份。 doc_auth.errors.not_a_file: 你选择的不是一个正确的文件。 doc_auth.errors.phone_step_incomplete: 在继续之前你必须使用手机上传你身份证件的图片。我们已给你发了带有说明的链接。 doc_auth.errors.pii.birth_date_min_age: 你的生日不满足最低年龄要求。 @@ -578,11 +582,21 @@ doc_auth.errors.send_link_limited: 你尝试了太多次。请在 %{timeout}后 doc_auth.errors.sharpness.failed_short: 图像模糊,请再试一次。 doc_auth.errors.sharpness.top_msg: 我们无法读取你的身份证件。你的照片可能太模糊或太暗。尝试在明亮的地方重拍一张。 doc_auth.errors.sharpness.top_msg_plural: 我们无法读取你的身份证件。你的照片可能太模糊或太暗。尝试在明亮的地方重拍一张。 +doc_auth.errors.unaccepted_id_type: 我们不接受护照、军人身份证件、纸质身份证件或临时身份证件。你只能使用驾驶执照或美国州或领地颁发的身份证件。 +doc_auth.errors.underage: 你必须年满 13 岁才能使用 %{app_name}。如果你年满 13 岁,请重试并确保你的出生日期对焦。 +doc_auth.errors.unreadable_id: 请在光线充足而且没有阴影或眩光的地方拍照。确保你身份证件上的所有内容都清晰对焦,并且身份卡填满画面。 doc_auth.errors.upload_error: 抱歉,我们这边出错了。 doc_auth.forms.change_file: 更改文件 doc_auth.forms.choose_file_html: 将文件拖到此处或者从文件夹中选择。 doc_auth.forms.doc_success: 我们验证了你的信息 doc_auth.forms.selected_file: 被选文件 +doc_auth.headers.expired_id: 你的身份证件可能已过期 +doc_auth.headers.general.network_error: 我们目前遇到技术困难 +doc_auth.headers.id_not_found: 我们找不到你的身份证件 +doc_auth.headers.low_resolution: 你设备的相机可能不受支持 +doc_auth.headers.unaccepted_id_type: 使用驾驶执照或州颁发的身份证件 +doc_auth.headers.underage: 不符合年龄规定 +doc_auth.headers.unreadable_id: 我们无法读取你的身份证件 doc_auth.headings.address: 更新你的邮政地址 doc_auth.headings.back: 驾照或州政府颁发身份证件的背面。 doc_auth.headings.capture_complete: 我们验证了你的身份证件 @@ -696,6 +710,8 @@ doc_auth.instructions.text1: 其他形式的身份证件不被接受。我们要 doc_auth.instructions.text2: 不需要你社会保障卡实体。 doc_auth.instructions.text3: 我们把你的电话号码与个人信息匹配,并向你的电话发送一个一次性代码。 doc_auth.instructions.text4: 你的密码对你个人信息进行存储并加密。 +doc_auth.rate_limit_warning.plural_html: 出于安全原因,你还有 %{remaining_attempts} 次尝试机会。 +doc_auth.rate_limit_warning.singular_html: 出于安全原因,你还有 1 次尝试机会。 doc_auth.tips.document_capture_hint: 必须是 JPG 或 PNG doc_auth.tips.document_capture_id_text1: 使用水平和深色表面 doc_auth.tips.document_capture_id_text2: 在光线明亮的地方拍照 diff --git a/config/routes.rb b/config/routes.rb index a01b1257562..f30ff1f5936 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -361,6 +361,8 @@ put '/document_capture' => 'document_capture#update' get '/socure/document_capture' => 'socure/document_capture#show' get '/socure/document_capture_update' => 'socure/document_capture#update', as: :socure_document_capture_update + get '/socure/document_capture_errors' => 'socure/document_capture#errors', as: :socure_document_capture_errors + get '/socure/document_capture_goto_in_person' => 'socure/document_capture#goto_in_person', as: :socure_document_capture_goto_in_person # This route is included in SMS messages sent to users who start the IdV hybrid flow. It # should be kept short, and should not include underscores ("_"). get '/documents' => 'hybrid_mobile/entry#show', as: :hybrid_mobile_entry @@ -369,6 +371,8 @@ get '/hybrid_mobile/capture_complete' => 'hybrid_mobile/capture_complete#show' get '/hybrid_mobile/socure/document_capture' => 'hybrid_mobile/socure/document_capture#show' get '/hybrid_mobile/socure/document_capture_update' => 'hybrid_mobile/socure/document_capture#update', as: :hybrid_mobile_socure_document_capture_update + get '/hybrid_mobile/socure/document_capture_errors' => 'hybrid_mobile/socure/document_capture#errors', as: :hybrid_mobile_socure_document_capture_errors + get '/hybrid_mobile/socure/document_capture_goto_in_person' => 'hybrid_mobile/socure/document_capture#goto_in_person', as: :hybrid_mobile_socure_document_capture_goto_in_person get '/hybrid_handoff' => 'hybrid_handoff#show' put '/hybrid_handoff' => 'hybrid_handoff#update' get '/link_sent' => 'link_sent#show' diff --git a/spec/controllers/idv/document_capture_controller_spec.rb b/spec/controllers/idv/document_capture_controller_spec.rb index 88d1ca61523..f3792613efc 100644 --- a/spec/controllers/idv/document_capture_controller_spec.rb +++ b/spec/controllers/idv/document_capture_controller_spec.rb @@ -365,6 +365,7 @@ expect(controller).to receive(:selfie_requirement_met?). and_return(performed_if_needed) allow(result).to receive(:success?).and_return(true) + allow(result).to receive(:errors).and_return(result[:errors]) allow(subject).to receive(:stored_result).and_return(result) allow(subject).to receive(:extract_pii_from_doc) end @@ -374,6 +375,7 @@ it 'stays on document capture' do put :update + expect(response).to redirect_to idv_document_capture_url end end diff --git a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb index ba96dbcfbb4..6ad142fd008 100644 --- a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb +++ b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb @@ -175,7 +175,7 @@ it 'redirects to idv unavailable url' do get(:show) - expect(response).to redirect_to(idv_unavailable_path) + expect(response).to redirect_to(idv_hybrid_mobile_socure_document_capture_errors_url) expect(controller.send(:instance_variable_get, :@url)).not_to be end end @@ -220,7 +220,7 @@ it 'connection timeout still responds to user' do stub_request(:post, fake_socure_endpoint).to_raise(Faraday::ConnectionFailed) get(:show) - expect(response).to redirect_to(idv_unavailable_path) + expect(response).to redirect_to(idv_hybrid_mobile_socure_document_capture_errors_url) end it 'socure error response still gives a result to user' do @@ -229,31 +229,34 @@ body: JSON.generate(failed_response_body), ) get(:show) - expect(response).to redirect_to(idv_unavailable_path) + expect(response).to redirect_to(idv_hybrid_mobile_socure_document_capture_errors_url) end + it 'socure nil response still gives a result to user' do stub_request(:post, fake_socure_endpoint).to_return( status: 500, body: nil, ) get(:show) - expect(response).to redirect_to(idv_unavailable_path) + expect(response).to redirect_to(idv_hybrid_mobile_socure_document_capture_errors_url) end + it 'socure nil response still gives a result to user' do stub_request(:post, fake_socure_endpoint).to_return( status: 401, body: JSON.generate(response_body_401), ) get(:show) - expect(response).to redirect_to(idv_unavailable_path) + expect(response).to redirect_to(idv_hybrid_mobile_socure_document_capture_errors_url) end + it 'socure nil response still gives a result to user' do stub_request(:post, fake_socure_endpoint).to_return( status: 401, body: JSON.generate(no_doc_found_response_body), ) get(:show) - expect(response).to redirect_to(idv_unavailable_path) + expect(response).to redirect_to(idv_hybrid_mobile_socure_document_capture_errors_url) end end end @@ -297,10 +300,10 @@ ) end - it 'redirects back to the capture page' do + it 'redirects to the error page' do get(:update) - expect(response).to redirect_to(idv_hybrid_mobile_socure_document_capture_url) + expect(response).to redirect_to(idv_hybrid_mobile_socure_document_capture_errors_url) expect(@analytics).to have_logged_event('IdV: doc auth document_capture submitted') end end diff --git a/spec/controllers/idv/link_sent_controller_spec.rb b/spec/controllers/idv/link_sent_controller_spec.rb index 9a49ea0c1cd..fe7f3ede25d 100644 --- a/spec/controllers/idv/link_sent_controller_spec.rb +++ b/spec/controllers/idv/link_sent_controller_spec.rb @@ -153,6 +153,7 @@ allow(load_result).to receive(:success?).and_return(load_result_success) allow(load_result).to receive(:selfie_check_performed?).and_return(false) + allow(load_result).to receive(:errors).and_return({ message: 'an error message' }) document_capture_session = DocumentCaptureSession.create!( user: user, @@ -200,7 +201,6 @@ it 'flashes an error and does not redirect' do put :update - expect(flash[:error]).to eq t('doc_auth.errors.phone_step_incomplete') expect(response.status).to eq(204) end end @@ -232,7 +232,6 @@ put :update expect(response).to redirect_to(idv_hybrid_handoff_url) - expect(flash[:error]).to eq(error_message) end end @@ -243,7 +242,6 @@ put :update expect(response).to have_http_status(204) - expect(flash[:error]).to eq(t('doc_auth.errors.phone_step_incomplete')) end end end diff --git a/spec/controllers/idv/socure/document_capture_controller_spec.rb b/spec/controllers/idv/socure/document_capture_controller_spec.rb index e431b493a6a..76750c9ff44 100644 --- a/spec/controllers/idv/socure/document_capture_controller_spec.rb +++ b/spec/controllers/idv/socure/document_capture_controller_spec.rb @@ -185,10 +185,10 @@ context 'there is no url in the socure response' do let(:response_body) { {} } - it 'redirects to idv unavailable url' do + it 'redirects to the errors page' do get(:show) - expect(response).to redirect_to(idv_unavailable_path) + expect(response).to redirect_to(idv_socure_document_capture_errors_url) expect(controller.send(:instance_variable_get, :@url)).not_to be end end @@ -233,7 +233,7 @@ it 'connection timeout still responds to user' do stub_request(:post, fake_socure_endpoint).to_raise(Faraday::ConnectionFailed) get(:show) - expect(response).to redirect_to(idv_unavailable_path) + expect(response).to redirect_to(idv_socure_document_capture_errors_url) end it 'socure error response still gives a result to user' do @@ -242,7 +242,7 @@ body: JSON.generate(failed_response_body), ) get(:show) - expect(response).to redirect_to(idv_unavailable_path) + expect(response).to redirect_to(idv_socure_document_capture_errors_url) end it 'socure nil response still gives a result to user' do stub_request(:post, fake_socure_endpoint).to_return( @@ -250,7 +250,7 @@ body: nil, ) get(:show) - expect(response).to redirect_to(idv_unavailable_path) + expect(response).to redirect_to(idv_socure_document_capture_errors_url) end it 'socure nil response still gives a result to user' do stub_request(:post, fake_socure_endpoint).to_return( @@ -258,7 +258,7 @@ body: JSON.generate(response_body_401), ) get(:show) - expect(response).to redirect_to(idv_unavailable_path) + expect(response).to redirect_to(idv_socure_document_capture_errors_url) end it 'socure nil response still gives a result to user' do stub_request(:post, fake_socure_endpoint).to_return( @@ -266,15 +266,17 @@ body: JSON.generate(no_doc_found_response_body), ) get(:show) - expect(response).to redirect_to(idv_unavailable_path) + expect(response).to redirect_to(idv_socure_document_capture_errors_url) end end end describe '#update' do - it 'returns FOUND (302) and redirects to SSN' do - get(:update) + before do + get :update + end + it 'returns FOUND (302) and redirects to SSN' do expect(response).to redirect_to(idv_ssn_path) expect(@analytics).to have_logged_event('IdV: doc auth document_capture submitted') end @@ -282,10 +284,10 @@ context 'when doc auth fails' do let(:doc_auth_success) { false } - it 'redirects to document capture' do + it 'renders the errors' do get(:update) - expect(response).to redirect_to(idv_socure_document_capture_path) + expect(response).to redirect_to idv_socure_document_capture_errors_url expect(@analytics).to have_logged_event('IdV: doc auth document_capture submitted') end end @@ -316,8 +318,6 @@ let(:socure_docv_enabled) { false } it 'the webhook route does not exist' do - get(:update) - expect(response).to be_not_found end end diff --git a/spec/features/idv/doc_auth/socure_document_capture_spec.rb b/spec/features/idv/doc_auth/socure_document_capture_spec.rb index be8b28388fc..b645ea89562 100644 --- a/spec/features/idv/doc_auth/socure_document_capture_spec.rb +++ b/spec/features/idv/doc_auth/socure_document_capture_spec.rb @@ -24,7 +24,6 @@ allow(IdentityConfig.store).to receive(:ruby_workers_idv_enabled).and_return(false) allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics) @docv_transaction_token = stub_docv_document_request - stub_docv_verification_data_pass end before(:all) do @@ -33,127 +32,185 @@ after(:all) { @user.destroy } - context 'standard desktop flow' do + context 'happy path' do before do - visit_idp_from_oidc_sp_with_ial2 - sign_in_and_2fa_user(@user) - complete_doc_auth_steps_before_document_capture_step - click_idv_continue + stub_docv_verification_data_pass end - context 'rate limits calls to backend docauth vendor', allow_browser_log: true do + context 'standard desktop flow' do before do - allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts) - (max_attempts - 1).times do - socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) - end - end - - it 'redirects to the rate limited error page' do - expect(page).to have_current_path(fake_socure_document_capture_app_url) - visit idv_socure_document_capture_path - expect(page).to have_current_path(idv_socure_document_capture_path) - socure_docv_upload_documents( - docv_transaction_token: @docv_transaction_token, - ) - visit idv_socure_document_capture_path - expect(page).to have_current_path(idv_session_errors_rate_limited_path) - expect(fake_analytics).to have_logged_event( - 'Rate Limit Reached', - limiter_type: :idv_doc_auth, - ) + visit_idp_from_oidc_sp_with_ial2 + sign_in_and_2fa_user(@user) + complete_doc_auth_steps_before_document_capture_step + click_idv_continue end - context 'successfully processes image on last attempt' do + context 'rate limits calls to backend docauth vendor', allow_browser_log: true do before do - allow(IdentityConfig.store).to receive(:ruby_workers_idv_enabled).and_return(false) - DocAuth::Mock::DocAuthMockClient.reset! - allow(Analytics).to receive(:new).and_return(fake_analytics) + allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts) + (max_attempts - 1).times do + socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) + end end - it 'proceeds to the next page with valid info' do + it 'redirects to the rate limited error page' do expect(page).to have_current_path(fake_socure_document_capture_app_url) visit idv_socure_document_capture_path expect(page).to have_current_path(idv_socure_document_capture_path) socure_docv_upload_documents( docv_transaction_token: @docv_transaction_token, ) - visit idv_socure_document_capture_update_path - expect(page).to have_current_path(idv_ssn_url) - visit idv_socure_document_capture_path - expect(page).to have_current_path(idv_session_errors_rate_limited_path) expect(fake_analytics).to have_logged_event( - :idv_socure_verification_data_requested, + 'Rate Limit Reached', + limiter_type: :idv_doc_auth, ) end + + context 'successfully processes image on last attempt' do + before do + DocAuth::Mock::DocAuthMockClient.reset! + end + + it 'proceeds to the next page with valid info' do + expect(page).to have_current_path(fake_socure_document_capture_app_url) + visit idv_socure_document_capture_path + expect(page).to have_current_path(idv_socure_document_capture_path) + socure_docv_upload_documents( + docv_transaction_token: @docv_transaction_token, + ) + + visit idv_socure_document_capture_update_path + expect(page).to have_current_path(idv_ssn_url) + + visit idv_socure_document_capture_path + + expect(page).to have_current_path(idv_session_errors_rate_limited_path) + end + end end - end - # ToDo post LG-14010 - context 'network connection errors' do - xit 'catches network connection errors on document request', allow_browser_log: true do - # expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error')) + context 'network connection errors' do + context 'getting the capture path' do + before do + allow_any_instance_of(Faraday::Connection).to receive(:post). + and_raise(Faraday::ConnectionFailed) + end + + it 'shows the network error page', js: true do + visit_idp_from_oidc_sp_with_ial2 + sign_in_and_2fa_user(@user) + + complete_doc_auth_steps_before_document_capture_step + + expect(page).to have_content(t('doc_auth.headers.general.network_error')) + expect(page).to have_content(t('doc_auth.errors.general.new_network_error')) + end + end + + # ToDo post LG-14010. Does this belong here, or on the polling page tests? + xit 'catches network connection errors on verification data request', + allow_browser_log: true do + # expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error')) + end end - xit 'catches network connection errors on verification data request', - allow_browser_log: true do - # expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error')) + it 'does not track state if state tracking is disabled' do + allow(IdentityConfig.store).to receive(:state_tracking_enabled).and_return(false) + socure_docv_upload_documents( + docv_transaction_token: @docv_transaction_token, + ) + + expect(DocAuthLog.find_by(user_id: @user.id).state).to be_nil + end + + xit 'does track state if state tracking is disabled' do + allow(IdentityConfig.store).to receive(:state_tracking_enabled).and_return(true) + socure_docv_upload_documents( + docv_transaction_token: @docv_transaction_token, + ) + + expect(DocAuthLog.find_by(user_id: @user.id).state).not_to be_nil end end - it 'does not track state if state tracking is disabled' do - allow(IdentityConfig.store).to receive(:state_tracking_enabled).and_return(false) - socure_docv_upload_documents( - docv_transaction_token: @docv_transaction_token, - ) + context 'standard mobile flow' do + it 'proceeds to the next page with valid info' do + perform_in_browser(:mobile) do + visit_idp_from_oidc_sp_with_ial2 + sign_in_and_2fa_user(@user) + complete_doc_auth_steps_before_document_capture_step - expect(DocAuthLog.find_by(user_id: @user.id).state).to be_nil + expect(page).to have_current_path(idv_socure_document_capture_url) + expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) + click_idv_continue + socure_docv_upload_documents( + docv_transaction_token: @docv_transaction_token, + ) + visit idv_socure_document_capture_update_path + expect(page).to have_current_path(idv_ssn_url) + + expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('NY') + + fill_out_ssn_form_ok + click_idv_continue + complete_verify_step + expect(page).to have_current_path(idv_phone_url) + end + end end + end + + shared_examples 'a properly categorized Socure error' do |socure_error_code, expected_header_key| + before do + stub_docv_verification_data_fail_with([socure_error_code]) + + visit_idp_from_oidc_sp_with_ial2 + sign_in_and_2fa_user(@user) + + complete_doc_auth_steps_before_document_capture_step - xit 'does track state if state tracking is disabled' do - allow(IdentityConfig.store).to receive(:state_tracking_enabled).and_return(true) + click_idv_continue socure_docv_upload_documents( docv_transaction_token: @docv_transaction_token, ) + visit idv_socure_document_capture_update_path + end - expect(DocAuthLog.find_by(user_id: @user.id).state).not_to be_nil + it 'shows the correct error page' do + expect(page).to have_content(t(expected_header_key)) end end - context 'standard mobile flow' do - before do - allow(IdentityConfig.store).to receive(:ruby_workers_idv_enabled).and_return(false) - allow(Analytics).to receive(:new).and_return(fake_analytics) - end + context 'a type 1 error (because we do not recognize the code)' do + it_behaves_like 'a properly categorized Socure error', 'XXXX', 'doc_auth.headers.unreadable_id' + end - it 'proceeds to the next page with valid info' do - perform_in_browser(:mobile) do - visit_idp_from_oidc_sp_with_ial2 - sign_in_and_2fa_user(@user) - complete_doc_auth_steps_before_document_capture_step + context 'a type 1 error' do + it_behaves_like 'a properly categorized Socure error', 'I848', 'doc_auth.headers.unreadable_id' + end - expect(page).to have_current_path(idv_socure_document_capture_url) - expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) - click_idv_continue - socure_docv_upload_documents( - docv_transaction_token: @docv_transaction_token, - ) - visit idv_socure_document_capture_update_path - expect(page).to have_current_path(idv_ssn_url) + context 'a type 2 error' do + it_behaves_like 'a properly categorized Socure error', + 'I849', + 'doc_auth.headers.unaccepted_id_type' + end - expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('NY') - expect(fake_analytics).to have_logged_event( - :idv_socure_verification_data_requested, - ) + context 'a type 3 error' do + it_behaves_like 'a properly categorized Socure error', 'R827', 'doc_auth.headers.expired_id' + end - fill_out_ssn_form_ok - click_idv_continue - complete_verify_step - expect(page).to have_current_path(idv_phone_url) - end - end + context 'a type 4 error' do + it_behaves_like 'a properly categorized Socure error', 'I808', 'doc_auth.headers.low_resolution' + end + + context 'a type 5 error' do + it_behaves_like 'a properly categorized Socure error', 'R845', 'doc_auth.headers.underage' + end + + context 'a type 6 error' do + it_behaves_like 'a properly categorized Socure error', 'I856', 'doc_auth.headers.id_not_found' end def expect_rate_limited_header(expected_to_be_present) diff --git a/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb b/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb index d2da26e722f..0245ba60c5a 100644 --- a/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb +++ b/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb @@ -26,160 +26,253 @@ @docv_transaction_token = stub_docv_document_request end - it 'proofs and hands off to mobile', js: true do - user = nil + context 'happy path' do + it 'proofs and hands off to mobile', js: true do + user = nil - perform_in_browser(:desktop) do - visit_idp_from_sp_with_ial2(sp) - user = sign_up_and_2fa_ial1_user + perform_in_browser(:desktop) do + visit_idp_from_sp_with_ial2(sp) + user = sign_up_and_2fa_ial1_user - complete_doc_auth_steps_before_hybrid_handoff_step - clear_and_fill_in(:doc_auth_phone, phone_number) - click_send_link + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link - expect(page).to have_content(t('doc_auth.headings.text_message')) - expect(page).to have_content(t('doc_auth.info.you_entered')) - expect(page).to have_content('+1 415-555-0199') + expect(page).to have_content(t('doc_auth.headings.text_message')) + expect(page).to have_content(t('doc_auth.info.you_entered')) + expect(page).to have_content('+1 415-555-0199') - # Confirm that Continue button is not shown when polling is enabled - expect(page).not_to have_content(t('doc_auth.buttons.continue')) - end + # Confirm that Continue button is not shown when polling is enabled + expect(page).not_to have_content(t('doc_auth.buttons.continue')) + end - expect(@sms_link).to be_present + expect(@sms_link).to be_present - perform_in_browser(:mobile) do - visit @sms_link + perform_in_browser(:mobile) do + visit @sms_link - # Confirm that jumping to LinkSent page does not cause errors - visit idv_link_sent_url - expect(page).to have_current_path(root_url) + # Confirm that jumping to LinkSent page does not cause errors + visit idv_link_sent_url + expect(page).to have_current_path(root_url) - # Confirm that we end up on the LN / Mock page even if we try to - # go to the Socure one. - visit idv_hybrid_mobile_socure_document_capture_url - expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) + # Confirm that we end up on the LN / Mock page even if we try to + # go to the Socure one. + visit idv_hybrid_mobile_socure_document_capture_url + expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) - # Confirm that clicking cancel and then coming back doesn't cause errors - click_link 'Cancel' - visit idv_hybrid_mobile_socure_document_capture_url + # Confirm that clicking cancel and then coming back doesn't cause errors + click_link 'Cancel' + visit idv_hybrid_mobile_socure_document_capture_url - # Confirm that jumping to Phone page does not cause errors - visit idv_phone_url - expect(page).to have_current_path(root_url) - visit idv_hybrid_mobile_socure_document_capture_url + # Confirm that jumping to Phone page does not cause errors + visit idv_phone_url + expect(page).to have_current_path(root_url) + visit idv_hybrid_mobile_socure_document_capture_url - # Confirm that jumping to Welcome page does not cause errors - visit idv_welcome_url - expect(page).to have_current_path(root_url) - visit idv_hybrid_mobile_socure_document_capture_url + # Confirm that jumping to Welcome page does not cause errors + visit idv_welcome_url + expect(page).to have_current_path(root_url) + visit idv_hybrid_mobile_socure_document_capture_url - expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) + expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) - stub_docv_verification_data_pass - click_idv_continue - expect(page).to have_current_path(fake_socure_document_capture_app_url) - socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) - visit idv_hybrid_mobile_socure_document_capture_update_url + stub_docv_verification_data_pass + click_idv_continue + expect(page).to have_current_path(fake_socure_document_capture_app_url) + socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) + visit idv_hybrid_mobile_socure_document_capture_update_url - expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) - expect(page).to have_content(strip_nbsp(t('doc_auth.headings.capture_complete'))) - expect(page).to have_text(t('doc_auth.instructions.switch_back')) - expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + expect(page).to have_content(strip_nbsp(t('doc_auth.headings.capture_complete'))) + expect(page).to have_text(t('doc_auth.instructions.switch_back')) + expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) - # To be fixed in app: - # Confirm app disallows jumping back to DocumentCapture page - # visit idv_hybrid_mobile_socure_document_capture_url - # expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) - end + # To be fixed in app: + # Confirm app disallows jumping back to DocumentCapture page + # visit idv_hybrid_mobile_socure_document_capture_url + # expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + end - perform_in_browser(:desktop) do - expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10) - expect(page).to have_current_path(idv_ssn_path) + perform_in_browser(:desktop) do + expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10) + expect(page).to have_current_path(idv_ssn_path) - fill_out_ssn_form_ok - click_idv_continue + fill_out_ssn_form_ok + click_idv_continue - expect(page).to have_content(t('headings.verify')) - complete_verify_step + expect(page).to have_content(t('headings.verify')) + complete_verify_step - prefilled_phone = page.find(id: 'idv_phone_form_phone').value + prefilled_phone = page.find(id: 'idv_phone_form_phone').value - expect( - PhoneFormatter.format(prefilled_phone), - ).to eq( - PhoneFormatter.format(user.default_phone_configuration.phone), - ) + expect( + PhoneFormatter.format(prefilled_phone), + ).to eq( + PhoneFormatter.format(user.default_phone_configuration.phone), + ) - fill_out_phone_form_ok - verify_phone_otp + fill_out_phone_form_ok + verify_phone_otp - fill_in t('idv.form.password'), with: Features::SessionHelper::VALID_PASSWORD - click_idv_continue + fill_in t('idv.form.password'), with: Features::SessionHelper::VALID_PASSWORD + click_idv_continue - acknowledge_and_confirm_personal_key + acknowledge_and_confirm_personal_key - validate_idv_completed_page(user) - click_agree_and_continue + validate_idv_completed_page(user) + click_agree_and_continue - validate_return_to_sp + validate_return_to_sp + end end - end - it 'shows the waiting screen correctly after cancelling from mobile and restarting', js: true do - user = nil + it 'shows the waiting screen correctly after cancelling from mobile and restarting', js: true do + user = nil - perform_in_browser(:desktop) do - user = sign_in_and_2fa_user - complete_doc_auth_steps_before_hybrid_handoff_step - clear_and_fill_in(:doc_auth_phone, phone_number) - click_send_link + perform_in_browser(:desktop) do + user = sign_in_and_2fa_user + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link - expect(page).to have_content(t('doc_auth.headings.text_message')) - end + expect(page).to have_content(t('doc_auth.headings.text_message')) + end + + expect(@sms_link).to be_present - expect(@sms_link).to be_present + perform_in_browser(:mobile) do + visit @sms_link + expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) + expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) + click_on t('links.cancel') + click_on t('forms.buttons.cancel') # Yes, cancel + end - perform_in_browser(:mobile) do - visit @sms_link - expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) - expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) - click_on t('links.cancel') - click_on t('forms.buttons.cancel') # Yes, cancel + perform_in_browser(:desktop) do + expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10) + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + + expect(page).to have_content(t('doc_auth.headings.text_message')) + end end - perform_in_browser(:desktop) do - expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10) - clear_and_fill_in(:doc_auth_phone, phone_number) - click_send_link + context 'user is rate limited on mobile' do + let(:max_attempts) { IdentityConfig.store.doc_auth_max_attempts } + + before do + allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts) + DocAuth::Mock::DocAuthMockClient.mock_response!( + method: :post_front_image, + response: DocAuth::Response.new( + success: false, + errors: { network: I18n.t('doc_auth.errors.general.network_error') }, + ), + ) + end + + it 'shows capture complete on mobile and error page on desktop', js: true do + user = nil - expect(page).to have_content(t('doc_auth.headings.text_message')) + perform_in_browser(:desktop) do + user = sign_in_and_2fa_user + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + + expect(page).to have_content(t('doc_auth.headings.text_message')) + end + + expect(@sms_link).to be_present + + perform_in_browser(:mobile) do + visit @sms_link + + click_idv_continue + expect(page).to have_current_path(fake_socure_document_capture_app_url) + stub_docv_verification_data_pass + max_attempts.times do + socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) + end + + visit idv_hybrid_mobile_socure_document_capture_update_url + + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + expect(page).to have_text(t('doc_auth.instructions.switch_back')) + end + + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_session_errors_rate_limited_path, wait: 10) + end + end end - end - context 'user is rate limited on mobile' do - let(:max_attempts) { IdentityConfig.store.doc_auth_max_attempts } + it 'prefills the phone number used on the phone step if the user has no MFA phone', :js do + user = create(:user, :with_authentication_app) - before do - allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts) - DocAuth::Mock::DocAuthMockClient.mock_response!( - method: :post_front_image, - response: DocAuth::Response.new( - success: false, - errors: { network: I18n.t('doc_auth.errors.general.network_error') }, - ), - ) + perform_in_browser(:desktop) do + start_idv_from_sp(facial_match_required: false) + sign_in_and_2fa_user(user) + + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + end + + expect(@sms_link).to be_present + + perform_in_browser(:mobile) do + visit @sms_link + + expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) + stub_docv_verification_data_pass + click_idv_continue + expect(page).to have_current_path(fake_socure_document_capture_app_url) + socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) + visit idv_hybrid_mobile_socure_document_capture_update_url + + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + expect(page).to have_text(t('doc_auth.instructions.switch_back')) + end + + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_ssn_path, wait: 10) + + fill_out_ssn_form_ok + click_idv_continue + + expect(page).to have_content(t('headings.verify')) + complete_verify_step + + prefilled_phone = page.find(id: 'idv_phone_form_phone').value + + expect( + PhoneFormatter.format(prefilled_phone), + ).to eq( + PhoneFormatter.format(phone_number), + ) + end end + end - it 'shows capture complete on mobile and error page on desktop', js: true do + shared_examples 'a properly categorized Socure error' do |socure_error_code, expected_header_key| + it 'shows the correct error page', js: true do user = nil perform_in_browser(:desktop) do - user = sign_in_and_2fa_user + visit_idp_from_sp_with_ial2(sp) + user = sign_up_and_2fa_ial1_user + complete_doc_auth_steps_before_hybrid_handoff_step clear_and_fill_in(:doc_auth_phone, phone_number) click_send_link expect(page).to have_content(t('doc_auth.headings.text_message')) + expect(page).to have_content(t('doc_auth.info.you_entered')) + expect(page).to have_content('+1 415-555-0199') + + # Confirm that Continue button is not shown when polling is enabled + expect(page).not_to have_content(t('doc_auth.buttons.continue')) end expect(@sms_link).to be_present @@ -187,69 +280,85 @@ perform_in_browser(:mobile) do visit @sms_link + stub_docv_verification_data_fail_with([socure_error_code]) + click_idv_continue - expect(page).to have_current_path(fake_socure_document_capture_app_url) - stub_docv_verification_data_pass - max_attempts.times do - socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) - end + socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) visit idv_hybrid_mobile_socure_document_capture_update_url - expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) - expect(page).to have_text(t('doc_auth.instructions.switch_back')) + expect(page).to have_text(t(expected_header_key)) + + click_try_again + + expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_path) end perform_in_browser(:desktop) do - expect(page).to have_current_path(idv_session_errors_rate_limited_path, wait: 10) + expect(page).to have_current_path(idv_link_sent_path) end end end - it 'prefills the phone number used on the phone step if the user has no MFA phone', :js do - user = create(:user, :with_authentication_app) + context 'a type 1 error (because we do not recognize the code)' do + it_behaves_like 'a properly categorized Socure error', 'XXXX', 'doc_auth.headers.unreadable_id' + end - perform_in_browser(:desktop) do - start_idv_from_sp(facial_match_required: false) - sign_in_and_2fa_user(user) + context 'a type 1 error' do + it_behaves_like 'a properly categorized Socure error', 'I848', 'doc_auth.headers.unreadable_id' + end - complete_doc_auth_steps_before_hybrid_handoff_step - clear_and_fill_in(:doc_auth_phone, phone_number) - click_send_link - end + context 'a type 2 error' do + it_behaves_like 'a properly categorized Socure error', + 'I849', + 'doc_auth.headers.unaccepted_id_type' + end - expect(@sms_link).to be_present + context 'a type 3 error' do + it_behaves_like 'a properly categorized Socure error', 'R827', 'doc_auth.headers.expired_id' + end + + context 'a type 4 error' do + it_behaves_like 'a properly categorized Socure error', 'I808', 'doc_auth.headers.low_resolution' + end - perform_in_browser(:mobile) do - visit @sms_link + context 'a type 5 error' do + it_behaves_like 'a properly categorized Socure error', 'R845', 'doc_auth.headers.underage' + end - expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) - stub_docv_verification_data_pass - click_idv_continue - expect(page).to have_current_path(fake_socure_document_capture_app_url) - socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) - visit idv_hybrid_mobile_socure_document_capture_update_url + context 'a type 6 error' do + it_behaves_like 'a properly categorized Socure error', 'I856', 'doc_auth.headers.id_not_found' + end - expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) - expect(page).to have_text(t('doc_auth.instructions.switch_back')) + context 'with a network error requesting the capture app url' do + before do + allow_any_instance_of(Faraday::Connection).to receive(:post). + and_raise(Faraday::ConnectionFailed) end - perform_in_browser(:desktop) do - expect(page).to have_current_path(idv_ssn_path, wait: 10) + it 'shows the network error page on the phone and the link sent page on the desktop', + js: true do + user = nil - fill_out_ssn_form_ok - click_idv_continue + perform_in_browser(:desktop) do + visit_idp_from_sp_with_ial2(sp) + user = sign_up_and_2fa_ial1_user - expect(page).to have_content(t('headings.verify')) - complete_verify_step + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + end - prefilled_phone = page.find(id: 'idv_phone_form_phone').value + perform_in_browser(:mobile) do + visit @sms_link - expect( - PhoneFormatter.format(prefilled_phone), - ).to eq( - PhoneFormatter.format(phone_number), - ) + expect(page).to have_text(t('doc_auth.headers.general.network_error')) + expect(page).to have_text(t('doc_auth.errors.general.new_network_error')) + end + + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_link_sent_path) + end end end end diff --git a/spec/support/features/document_capture_step_helper.rb b/spec/support/features/document_capture_step_helper.rb index f5b82332515..9143136c3fe 100644 --- a/spec/support/features/document_capture_step_helper.rb +++ b/spec/support/features/document_capture_step_helper.rb @@ -107,6 +107,10 @@ def stub_docv_verification_data_pass stub_docv_verification_data(body: SocureDocvFixtures.pass_json) end + def stub_docv_verification_data_fail_with(errors) + stub_docv_verification_data(body: SocureDocvFixtures.fail_json(errors)) + end + def stub_docv_verification_data(body:) stub_request(:post, "#{IdentityConfig.store.socure_idplus_base_url}/api/3.0/EmailAuthScore"). to_return( diff --git a/spec/support/socure_docv_fixtures.rb b/spec/support/socure_docv_fixtures.rb index a0e6ab756c4..1a4bb4e256a 100644 --- a/spec/support/socure_docv_fixtures.rb +++ b/spec/support/socure_docv_fixtures.rb @@ -7,6 +7,16 @@ def pass_json JSON.parse(raw).to_json end + def fail_json(errors) + raw = read_fixture_file_at_path('pass.json') + body = JSON.parse(raw) + + body['documentVerification']['decision']['value'] = 'reject' + body['documentVerification']['reasonCodes'] = errors + + body.to_json + end + private def read_fixture_file_at_path(filepath) diff --git a/spec/views/idv/shared/_error.html.erb_spec.rb b/spec/views/idv/shared/_error.html.erb_spec.rb index 20a0fdf5a10..89cb5c1452f 100644 --- a/spec/views/idv/shared/_error.html.erb_spec.rb +++ b/spec/views/idv/shared/_error.html.erb_spec.rb @@ -5,7 +5,7 @@ let(:options) { [{ text: 'Example', url: '#example' }] } let(:heading) { 'Error' } let(:action) { nil } - let(:action_secondary) { nil } + let(:secondary_action) { nil } let(:type) { nil } let(:current_step) { nil } let(:step_indicator_steps) { nil } @@ -14,7 +14,7 @@ type: type, heading: heading, action: action, - action_secondary: action_secondary, + secondary_action: secondary_action, current_step: current_step, options: options, } @@ -73,7 +73,7 @@ end context 'with secondary action' do - let(:action_secondary) { { text: 'Secondary Action', url: '#secondary' } } + let(:secondary_action) { { text: 'Secondary Action', url: '#secondary' } } it 'renders secondary action button' do expect(rendered).to have_link('Secondary Action', href: '#secondary') @@ -81,7 +81,7 @@ end context 'with form action' do - let(:action_secondary) { { text: 'Delete', url: '#delete', method: :delete } } + let(:secondary_action) { { text: 'Delete', url: '#delete', method: :delete } } it 'renders action button' do expect(rendered).to have_button('Delete') From 165ef1803827567f8c4dc4a10d6c17b2c92ff02e Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Tue, 3 Dec 2024 14:56:36 -0500 Subject: [PATCH 03/14] LG-15183: Associate user_id for reCAPTCHA result analytics of failed sign-in (#11580) * LG-15183: Associate user_id for reCAPTCHA result analytics of failed sign-in changelog: Internal, Anti-Fraud, Associate user_id for reCAPTCHA result analytics of failed sign-in * Add FakeAnalytics#reset! to support resetting analytics stub * Assert SessionsController user through stub_analytics * Move SessionsController#analytics_user to public Match ApplicationController#analytics_user visibility --- app/controllers/users/sessions_controller.rb | 10 +++++- app/services/analytics_events.rb | 3 -- .../users/sessions_controller_spec.rb | 36 +++++++------------ spec/support/analytics_helper.rb | 2 +- spec/support/fake_analytics.rb | 4 +++ spec/support/shared_examples/sign_in.rb | 26 +++++++++++--- 6 files changed, 48 insertions(+), 33 deletions(-) diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index aed6d671f5c..2b7570b4501 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -19,6 +19,7 @@ class SessionsController < Devise::SessionsController before_action :check_user_needs_redirect, only: [:new] before_action :apply_secure_headers_override, only: [:new, :create] before_action :clear_session_bad_password_count_if_window_expired, only: [:create] + before_action :set_analytics_user_from_params, only: :create before_action :allow_csp_recaptcha_src, if: :recaptcha_enabled? after_action :add_recaptcha_resource_hints, if: :recaptcha_enabled? @@ -58,6 +59,10 @@ def destroy end end + def analytics_user + @analytics_user || AnonymousUser.new + end + private def clear_session_bad_password_count_if_window_expired @@ -168,6 +173,10 @@ def auth_params params.require(:user).permit(:email, :password) end + def set_analytics_user_from_params + @analytics_user = user_from_params + end + def process_locked_out_user presenter = TwoFactorAuthCode::MaxAttemptsReachedPresenter.new( 'generic_login_attempts', @@ -210,7 +219,6 @@ def track_authentication_attempt analytics.email_and_password_auth( **recaptcha_response, success: success, - user_id: user.uuid, user_locked_out: user_locked_out?(user), rate_limited: rate_limited?, captcha_validation_performed: captcha_validation_performed?, diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 980d83a391f..d917f01cd9c 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -478,7 +478,6 @@ def edit_password_visit(required_password_change: false, **extra) # @param [Boolean] success Whether form validation was successful # @param [Hash] error_details Details for errors that occurred in unsuccessful submission - # @param [String] user_id UUID for user associated with attempted email address # @param [Boolean] user_locked_out if the user is currently locked out of their second factor # @param [Boolean] rate_limited Whether the user has exceeded user IP rate limiting # @param [Boolean] valid_captcha_result Whether user passed the reCAPTCHA check or was exempt @@ -491,7 +490,6 @@ def edit_password_visit(required_password_change: false, **extra) # Tracks authentication attempts at the email/password screen def email_and_password_auth( success:, - user_id:, user_locked_out:, rate_limited:, valid_captcha_result:, @@ -507,7 +505,6 @@ def email_and_password_auth( 'Email and Password Authentication', success:, error_details:, - user_id:, user_locked_out:, rate_limited:, valid_captcha_result:, diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb index c0a3f57cf76..9c614032c48 100644 --- a/spec/controllers/users/sessions_controller_spec.rb +++ b/spec/controllers/users/sessions_controller_spec.rb @@ -87,14 +87,13 @@ end it 'tracks the successful authentication for existing user' do - stub_analytics + stub_analytics(user:) response expect(@analytics).to have_logged_event( 'Email and Password Authentication', success: true, - user_id: user.uuid, user_locked_out: false, rate_limited: false, valid_captcha_result: true, @@ -161,14 +160,13 @@ end it 'tracks as not being from a new device' do - stub_analytics + stub_analytics(user:) response expect(@analytics).to have_logged_event( 'Email and Password Authentication', success: true, - user_id: user.uuid, user_locked_out: false, rate_limited: false, valid_captcha_result: true, @@ -222,8 +220,8 @@ attempt_window_max: 12.hours.in_minutes, }, ) - stub_analytics user = create(:user, :fully_registered) + stub_analytics(user:) travel_to (3.hours + 1.minute).ago do 2.times do @@ -252,7 +250,6 @@ expect(@analytics).to have_logged_event( 'Email and Password Authentication', success: false, - user_id: user.uuid, user_locked_out: false, rate_limited: true, valid_captcha_result: true, @@ -267,7 +264,7 @@ it 'tracks the unsuccessful authentication for existing user' do user = create(:user, :fully_registered) - stub_analytics + stub_analytics(user:) expect(SCrypt::Engine).to receive(:hash_secret).once.and_call_original post :create, params: { user: { email: user.email.upcase, password: 'invalid_password' } } @@ -275,7 +272,6 @@ expect(@analytics).to have_logged_event( 'Email and Password Authentication', success: false, - user_id: user.uuid, user_locked_out: false, rate_limited: false, valid_captcha_result: true, @@ -288,7 +284,7 @@ end it 'tracks the authentication attempt for nonexistent user' do - stub_analytics + stub_analytics(user: kind_of(AnonymousUser)) expect(SCrypt::Engine).to receive(:hash_secret).once.and_call_original post :create, params: { user: { email: 'foo@example.com', password: 'password' } } @@ -296,7 +292,6 @@ expect(@analytics).to have_logged_event( 'Email and Password Authentication', success: false, - user_id: 'anonymous-uuid', user_locked_out: false, rate_limited: false, valid_captcha_result: true, @@ -314,14 +309,13 @@ second_factor_locked_at: Time.zone.now, ) - stub_analytics + stub_analytics(user:) post :create, params: { user: { email: user.email.upcase, password: user.password } } expect(@analytics).to have_logged_event( 'Email and Password Authentication', success: false, - user_id: user.uuid, user_locked_out: true, rate_limited: false, valid_captcha_result: true, @@ -365,7 +359,7 @@ it 'tracks unsuccessful authentication for failed reCAPTCHA' do user = create(:user, :fully_registered) - stub_analytics + stub_analytics(user:) post :create, params: { user: { email: user.email, password: user.password, score: 0.1 } } @@ -373,7 +367,6 @@ 'Email and Password Authentication', success: false, error_details: { recaptcha_token: { blank: true } }, - user_id: user.uuid, user_locked_out: false, rate_limited: false, valid_captcha_result: false, @@ -400,14 +393,13 @@ :fully_registered, ) - stub_analytics + stub_analytics(user:) post :create, params: { user: { email: user.email.upcase, password: 'invalid' } } post :create, params: { user: { email: user.email.upcase, password: 'invalid' } } expect(@analytics).to have_logged_event( 'Email and Password Authentication', success: false, - user_id: user.uuid, user_locked_out: false, rate_limited: false, valid_captcha_result: true, @@ -420,14 +412,13 @@ it 'tracks the presence of SP request_url in session' do subject.session[:sp] = { request_url: mock_valid_site } - stub_analytics + stub_analytics(user: kind_of(AnonymousUser)) post :create, params: { user: { email: 'foo@example.com', password: 'password' } } expect(@analytics).to have_logged_event( 'Email and Password Authentication', success: false, - user_id: 'anonymous-uuid', user_locked_out: false, rate_limited: false, valid_captcha_result: true, @@ -592,14 +583,13 @@ }.to_json, ) - stub_analytics + stub_analytics(user:) post :create, params: { user: { email: user.email, password: user.password } } expect(@analytics).to have_logged_event( 'Email and Password Authentication', success: true, - user_id: user.uuid, user_locked_out: false, rate_limited: false, valid_captcha_result: true, @@ -718,14 +708,13 @@ expires: 2.days.from_now, } - stub_analytics + stub_analytics(user:) post :create, params: { user: { email: user.email, password: user.password } } expect(@analytics).to have_logged_event( 'Email and Password Authentication', success: true, - user_id: user.uuid, user_locked_out: false, rate_limited: false, valid_captcha_result: true, @@ -746,14 +735,13 @@ value: RememberDeviceCookie.new(user_id: user.id, created_at: 2.days.ago).to_json, } - stub_analytics + stub_analytics(user:) post :create, params: { user: { email: user.email, password: user.password } } expect(@analytics).to have_logged_event( 'Email and Password Authentication', success: true, - user_id: user.uuid, user_locked_out: false, rate_limited: false, valid_captcha_result: true, diff --git a/spec/support/analytics_helper.rb b/spec/support/analytics_helper.rb index 90920f23576..b4da7cc1cdd 100644 --- a/spec/support/analytics_helper.rb +++ b/spec/support/analytics_helper.rb @@ -9,7 +9,7 @@ def stub_analytics(user: nil) end stub.to receive(:analytics).and_wrap_original do |original| - expect(original.call.user).to eq(user) if user + expect(original.call.user).to match(user) if user analytics end diff --git a/spec/support/fake_analytics.rb b/spec/support/fake_analytics.rb index b67674988e2..6f43aaa06e7 100644 --- a/spec/support/fake_analytics.rb +++ b/spec/support/fake_analytics.rb @@ -171,6 +171,10 @@ def track_event(event, attributes = {}) def browser_attributes {} end + + def reset! + @events = Hash.new + end end RSpec.configure do |c| diff --git a/spec/support/shared_examples/sign_in.rb b/spec/support/shared_examples/sign_in.rb index bbe09be0a3e..8136ec3cbc2 100644 --- a/spec/support/shared_examples/sign_in.rb +++ b/spec/support/shared_examples/sign_in.rb @@ -324,16 +324,34 @@ def user_with_broken_personal_key(scenario) fake_analytics = FakeAnalytics.new allow_any_instance_of(ApplicationController).to receive(:analytics). and_wrap_original do |original| - original_analytics = original.call - if original_analytics.request.params[:controller] == 'users/sessions' && - original_analytics.request.params[:action] == 'create' - expect(original_analytics.user).to eq(user) + if original.receiver.instance_of?(Users::SessionsController) && + original.receiver.action_name == 'create' + expect(original.call.user).to eq(user) asserted_expected_user = true end fake_analytics end + fill_in_credentials_and_submit(user.email, 'wrongpassword') + expect(asserted_expected_user).to eq(true) + expect(fake_analytics).to have_logged_event( + 'reCAPTCHA verify result received', + recaptcha_result: { + assessment_id: kind_of(String), + success: true, + score: 1.0, + errors: [], + reasons: [], + }, + evaluated_as_valid: true, + score_threshold: 0.2, + recaptcha_action: 'sign_in', + form_class: 'RecaptchaMockForm', + ) + asserted_expected_user = false + fake_analytics.reset! + fill_in :user_recaptcha_mock_score, with: '0.1' fill_in_credentials_and_submit(user.email, user.password) expect(asserted_expected_user).to eq(true) From 6673030eb232b0665841aee9e80748fd2f7b671b Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Wed, 4 Dec 2024 07:26:31 -0500 Subject: [PATCH 04/14] Add additional logging details for partner email selection (#11550) * Add additional logging details for partner email selection changelog: Internal, Analytics, Add additional logging details for partner email selection * Avoid logging from_select_email_flow in sign up email confirmation Not relevant for this flow. Unfortunate consequence of sharing event between initial account creation email confirmation, and subsequent email additions * Drop from_select_email_flow from signup email confirmed Never present * Use attr_reader shorthand for selected_email_id * Default token validator from_select_email_flow to nil Avoid in logging result except where used in account email controller * Omit nil values from extra token validator analytics UndocumentedParams checker still flags if passed as nil, avoid passing altogether Effect on user_id should be non-regressing, since user_id should only supersede default if associated with a user * Include nil user_id in extra token attributes Required keyword argument on add_email_confirmation, avoid regressing * Restore nil user_id assertion in token validator spec * Avoid handling email select flow in token validator Since it's shared, and only 1 place needs this in logging result, instead append to logging result from controller * Use Object#try to convert param to numeric Co-authored-by: Zach Margolis * Restore presence check Avoid logging 0 ID for empty value --------- Co-authored-by: Zach Margolis --- .../concerns/unconfirmed_user_concern.rb | 2 +- .../sign_up/cancellations_controller.rb | 2 +- .../users/email_confirmations_controller.rb | 16 ++-- app/controllers/users/emails_controller.rb | 12 ++- app/forms/add_user_email_form.rb | 6 +- app/forms/select_email_form.rb | 13 ++- app/services/analytics_events.rb | 41 ++++++-- .../email_confirmation_token_validator.rb | 2 +- .../selected_email_controller_spec.rb | 8 +- .../sign_up/passwords_controller_spec.rb | 2 +- .../sign_up/select_email_controller_spec.rb | 2 + .../email_confirmations_controller_spec.rb | 19 ++++ .../users/emails_controller_spec.rb | 94 ++++++++++++++++++- spec/forms/add_user_email_form_spec.rb | 41 +++++++- spec/forms/select_email_form_spec.rb | 21 ++++- ...email_confirmation_token_validator_spec.rb | 4 +- 16 files changed, 246 insertions(+), 39 deletions(-) diff --git a/app/controllers/concerns/unconfirmed_user_concern.rb b/app/controllers/concerns/unconfirmed_user_concern.rb index a027d47f8c4..f9ee2fe3f73 100644 --- a/app/controllers/concerns/unconfirmed_user_concern.rb +++ b/app/controllers/concerns/unconfirmed_user_concern.rb @@ -40,7 +40,7 @@ def email_confirmation_token_validator_result def email_confirmation_token_validator @email_confirmation_token_validator ||= begin - EmailConfirmationTokenValidator.new(@email_address, current_user) + EmailConfirmationTokenValidator.new(email_address: @email_address, current_user:) end end diff --git a/app/controllers/sign_up/cancellations_controller.rb b/app/controllers/sign_up/cancellations_controller.rb index 185f51056cd..b04993147e5 100644 --- a/app/controllers/sign_up/cancellations_controller.rb +++ b/app/controllers/sign_up/cancellations_controller.rb @@ -38,7 +38,7 @@ def find_user confirmation_token = session[:user_confirmation_token] email_address = EmailAddress.find_with_confirmation_token(confirmation_token) - @token_validator = EmailConfirmationTokenValidator.new(email_address, current_user) + @token_validator = EmailConfirmationTokenValidator.new(email_address:, current_user:) result = @token_validator.submit if result.success? diff --git a/app/controllers/users/email_confirmations_controller.rb b/app/controllers/users/email_confirmations_controller.rb index 0f6d468ffa3..e0f0c989fb4 100644 --- a/app/controllers/users/email_confirmations_controller.rb +++ b/app/controllers/users/email_confirmations_controller.rb @@ -3,8 +3,9 @@ module Users class EmailConfirmationsController < ApplicationController def create + store_from_select_email_flow_in_session result = email_confirmation_token_validator.submit - analytics.add_email_confirmation(**result) + analytics.add_email_confirmation(**result, from_select_email_flow: from_select_email_flow?) if result.success? process_successful_confirmation(email_address) else @@ -28,12 +29,8 @@ def email_address end def email_confirmation_token_validator - @email_confirmation_token_validator ||= begin - EmailConfirmationTokenValidator.new( - email_address, - current_user, - ) - end + @email_confirmation_token_validator ||= + EmailConfirmationTokenValidator.new(email_address:, current_user:) end def email_address_already_confirmed? @@ -42,7 +39,6 @@ def email_address_already_confirmed? def process_successful_confirmation(email_address) confirm_and_notify(email_address) - store_from_select_email_flow_in_session if current_user flash[:success] = t('devise.confirmations.confirmed') if params[:request_id] @@ -107,5 +103,9 @@ def confirmation_params def store_from_select_email_flow_in_session session[:from_select_email_flow] = params[:from_select_email_flow].to_s == 'true' end + + def from_select_email_flow? + session[:from_select_email_flow] == true + end end end diff --git a/app/controllers/users/emails_controller.rb b/app/controllers/users/emails_controller.rb index 09e24410d3e..0a229b41e13 100644 --- a/app/controllers/users/emails_controller.rb +++ b/app/controllers/users/emails_controller.rb @@ -11,16 +11,14 @@ class EmailsController < ApplicationController before_action :confirm_recently_authenticated_2fa def show - analytics.add_email_visit - session[:in_select_email_flow] = params[:in_select_email_flow] + session[:in_select_email_flow] = true if params[:in_select_email_flow] + analytics.add_email_visit(in_select_email_flow: in_select_email_flow?) @add_user_email_form = AddUserEmailForm.new @pending_completions_consent = pending_completions_consent? end def add - @add_user_email_form = AddUserEmailForm.new( - session[:in_select_email_flow], - ) + @add_user_email_form = AddUserEmailForm.new(in_select_email_flow: in_select_email_flow?) result = @add_user_email_form.submit( current_user, permitted_params.merge(request_id:) @@ -83,6 +81,10 @@ def verify private + def in_select_email_flow? + session[:in_select_email_flow] == true + end + def authorize_user_to_edit_email return render_not_found if email_address.user != current_user rescue ActiveRecord::RecordNotFound diff --git a/app/forms/add_user_email_form.rb b/app/forms/add_user_email_form.rb index f34ad455ba5..b7fe652b9d8 100644 --- a/app/forms/add_user_email_form.rb +++ b/app/forms/add_user_email_form.rb @@ -6,12 +6,13 @@ class AddUserEmailForm include ActionView::Helpers::TranslationHelper attr_reader :email, :in_select_email_flow + alias_method :in_select_email_flow?, :in_select_email_flow def self.model_name ActiveModel::Name.new(self, nil, 'User') end - def initialize(in_select_email_flow = nil) + def initialize(in_select_email_flow: false) @in_select_email_flow = in_select_email_flow end @@ -52,13 +53,14 @@ def process_successful_submission @success = true email_address.save! SendAddEmailConfirmation.new(user). - call(email_address:, in_select_email_flow:, request_id:) + call(email_address:, in_select_email_flow: in_select_email_flow?, request_id:) end def extra_analytics_attributes { user_id: existing_user.uuid, domain_name: email&.split('@')&.last, + in_select_email_flow: in_select_email_flow?, } end diff --git a/app/forms/select_email_form.rb b/app/forms/select_email_form.rb index c5b9ca3517d..b00bc3ce636 100644 --- a/app/forms/select_email_form.rb +++ b/app/forms/select_email_form.rb @@ -14,16 +14,25 @@ def initialize(user:, identity: nil) end def submit(params) - @selected_email_id = params[:selected_email_id] + @selected_email_id = params[:selected_email_id].try(:to_i) if !params[:selected_email_id].blank? success = valid? identity.update(email_address_id: selected_email_id) if success && identity - FormResponse.new(success:, errors:, serialize_error_details_only: true) + FormResponse.new( + success:, + errors:, + extra: extra_analytics_attributes, + serialize_error_details_only: true, + ) end private + def extra_analytics_attributes + { selected_email_id: } + end + def validate_owns_selected_email return if user.confirmed_email_addresses.exists?(id: selected_email_id) errors.add(:selected_email_id, :not_found, message: t('email_address.not_found')) diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index d917f01cd9c..c8d3d1f64b9 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -243,14 +243,24 @@ def account_visit # @param [Hash] errors Errors resulting from form validation # @param [Hash] error_details Details for errors that occurred in unsuccessful submission # @param [String] user_id User the email is linked to + # @param [Boolean] from_select_email_flow Whether email was added as part of partner email + # selection. # A user has clicked the confirmation link in an email - def add_email_confirmation(user_id:, success:, errors:, error_details: nil, **extra) + def add_email_confirmation( + user_id:, + success:, + errors:, + from_select_email_flow:, + error_details: nil, + **extra + ) track_event( 'Add Email: Email Confirmation', user_id:, success:, errors:, error_details:, + from_select_email_flow:, **extra, ) end @@ -259,21 +269,33 @@ def add_email_confirmation(user_id:, success:, errors:, error_details: nil, **ex # @param [Hash] errors Errors resulting from form validation # @param [Hash] error_details Details for errors that occurred in unsuccessful submission # @param [String] domain_name Domain name of email address submitted + # @param [Boolean] in_select_email_flow Whether email is being added as part of partner email + # selection. # Tracks request for adding new emails to an account - def add_email_request(success:, errors:, domain_name:, error_details: nil, **extra) + def add_email_request( + success:, + errors:, + domain_name:, + in_select_email_flow:, + error_details: nil, + **extra + ) track_event( 'Add Email Requested', success:, errors:, error_details:, domain_name:, + in_select_email_flow:, **extra, ) end # When a user views the add email address page - def add_email_visit - track_event('Add Email Address Page Visited') + # @param [Boolean] in_select_email_flow Whether email is being added as part of partner email + # selection. + def add_email_visit(in_select_email_flow:, **extra) + track_event('Add Email Address Page Visited', in_select_email_flow:, **extra) end # Tracks When users visit the add phone page @@ -6822,10 +6844,12 @@ def sp_revoke_consent_visited(issuer:, **extra) # User submitted form to change email shared with service provider # @param [Boolean] success Whether form validation was successful # @param [Hash] error_details Details for errors that occurred in unsuccessful submission + # @param [Integer] selected_email_id Selected email address record ID # @param [String, nil] needs_completion_screen_reason Reason for the consent screen being shown, # if user is changing email in consent flow def sp_select_email_submitted( success:, + selected_email_id:, error_details: nil, needs_completion_screen_reason: nil, **extra @@ -6835,6 +6859,7 @@ def sp_select_email_submitted( success:, error_details:, needs_completion_screen_reason:, + selected_email_id:, **extra, ) end @@ -7132,10 +7157,10 @@ def user_registration_email_confirmation( ) track_event( 'User Registration: Email Confirmation', - success: success, - errors: errors, - error_details: error_details, - user_id: user_id, + success:, + errors:, + error_details:, + user_id:, **extra, ) end diff --git a/app/services/email_confirmation_token_validator.rb b/app/services/email_confirmation_token_validator.rb index 49baae8350b..8898071bdf4 100644 --- a/app/services/email_confirmation_token_validator.rb +++ b/app/services/email_confirmation_token_validator.rb @@ -9,7 +9,7 @@ class EmailConfirmationTokenValidator validate :email_not_already_confirmed, if: :email_address_found_with_token? validate :token_not_expired, if: :email_address_found_with_token? - def initialize(email_address, current_user = nil) + def initialize(email_address:, current_user: nil) @current_user = current_user @email_address = email_address @user = email_address&.user diff --git a/spec/controllers/accounts/connected_accounts/selected_email_controller_spec.rb b/spec/controllers/accounts/connected_accounts/selected_email_controller_spec.rb index 0b66e62c194..5bf6e380b58 100644 --- a/spec/controllers/accounts/connected_accounts/selected_email_controller_spec.rb +++ b/spec/controllers/accounts/connected_accounts/selected_email_controller_spec.rb @@ -90,11 +90,15 @@ response - expect(@analytics).to have_logged_event(:sp_select_email_submitted, success: true) + expect(@analytics).to have_logged_event( + :sp_select_email_submitted, + success: true, + selected_email_id: selected_email.id, + ) end context 'with invalid submission' do - let(:params) { super().merge(select_email_form: { selected_email_id: nil }) } + let(:params) { super().merge(select_email_form: { selected_email_id: '' }) } it 'redirects to form with flash' do expect(response).to redirect_to(edit_connected_account_selected_email_path(identity.id)) diff --git a/spec/controllers/sign_up/passwords_controller_spec.rb b/spec/controllers/sign_up/passwords_controller_spec.rb index 9d910e9bf57..e0979324944 100644 --- a/spec/controllers/sign_up/passwords_controller_spec.rb +++ b/spec/controllers/sign_up/passwords_controller_spec.rb @@ -171,7 +171,7 @@ end it 'rejects when confirmation_token is invalid' do - validator = EmailConfirmationTokenValidator.new(user.email_addresses.first) + validator = EmailConfirmationTokenValidator.new(email_address: user.email_addresses.first) result = validator.submit expect(result.success?).to eq false diff --git a/spec/controllers/sign_up/select_email_controller_spec.rb b/spec/controllers/sign_up/select_email_controller_spec.rb index 4966281dccc..8c6f65575fb 100644 --- a/spec/controllers/sign_up/select_email_controller_spec.rb +++ b/spec/controllers/sign_up/select_email_controller_spec.rb @@ -107,6 +107,7 @@ :sp_select_email_submitted, success: true, needs_completion_screen_reason: :new_attributes, + selected_email_id: selected_email.id, ) end @@ -131,6 +132,7 @@ success: false, error_details: { selected_email_id: { not_found: true } }, needs_completion_screen_reason: :new_attributes, + selected_email_id: selected_email.id, ) end end diff --git a/spec/controllers/users/email_confirmations_controller_spec.rb b/spec/controllers/users/email_confirmations_controller_spec.rb index 8d3197ad185..826b9374f01 100644 --- a/spec/controllers/users/email_confirmations_controller_spec.rb +++ b/spec/controllers/users/email_confirmations_controller_spec.rb @@ -4,6 +4,8 @@ describe '#create' do describe 'Valid email confirmation tokens' do it 'tracks a valid email confirmation token event' do + stub_analytics + user = create(:user) new_email = Faker::Internet.email @@ -23,6 +25,14 @@ email_record = add_email_form.email_address_record(new_email) get :create, params: { confirmation_token: email_record.reload.confirmation_token } + + expect(@analytics).to have_logged_event( + 'Add Email: Email Confirmation', + success: true, + errors: {}, + from_select_email_flow: false, + user_id: user.uuid, + ) end context 'when select email feature is disabled' do @@ -126,6 +136,7 @@ end it 'adds an email from the service provider consent flow' do + stub_analytics new_email = Faker::Internet.email add_email_form = AddUserEmailForm.new add_email_form.submit(user, email: new_email, request_id: sp_request_uuid) @@ -134,8 +145,16 @@ get :create, params: { confirmation_token: email_record.reload.confirmation_token, request_id: sp_request_uuid, + from_select_email_flow: 'true', } + expect(@analytics).to have_logged_event( + 'Add Email: Email Confirmation', + success: true, + errors: {}, + from_select_email_flow: true, + user_id: user.uuid, + ) expect(response).to redirect_to(sign_up_select_email_url) end end diff --git a/spec/controllers/users/emails_controller_spec.rb b/spec/controllers/users/emails_controller_spec.rb index bfc142718af..ae29f8e71ea 100644 --- a/spec/controllers/users/emails_controller_spec.rb +++ b/spec/controllers/users/emails_controller_spec.rb @@ -1,6 +1,94 @@ require 'rails_helper' RSpec.describe Users::EmailsController do + describe '#show' do + subject(:response) { get :show, params: params } + let(:params) { {} } + + before do + stub_sign_in + end + + it 'does not session value for email selection flow' do + expect { response }.not_to change { controller.session[:in_select_email_flow] }.from(nil) + end + + it 'logs visit' do + stub_analytics + + response + + expect(@analytics).to have_logged_event( + 'Add Email Address Page Visited', + in_select_email_flow: false, + ) + end + + context 'when adding through partner email selection flow' do + let(:params) { { in_select_email_flow: true } } + + it 'assigns session value for email selection flow' do + expect { response }.to change { controller.session[:in_select_email_flow] }. + from(nil).to(true) + end + + it 'logs visit with selected email value' do + stub_analytics + + response + + expect(@analytics).to have_logged_event( + 'Add Email Address Page Visited', + in_select_email_flow: true, + ) + end + end + end + + describe '#add' do + subject(:response) { post :add, params: params } + let(:user) { create(:user) } + let(:params) { { user: { email:, request_id: } } } + let(:email) { 'new@example.com' } + let(:request_id) { 'request-id-1' } + + before do + stub_sign_in(user) + end + + it 'logs submission' do + stub_analytics + + response + + expect(@analytics).to have_logged_event( + 'Add Email Requested', + success: true, + errors: {}, + domain_name: 'example.com', + in_select_email_flow: false, + user_id: user.uuid, + ) + end + + context 'when adding through partner email selection flow' do + before do + controller.session[:in_select_email_flow] = true + end + + it 'logs submission with selected email value' do + stub_analytics + + response + + expect(@analytics).to have_logged_event( + 'Add Email Requested', + hash_including(in_select_email_flow: true), + ) + end + end + end + describe '#verify' do context 'with malformed payload' do it 'does not blow up' do @@ -21,7 +109,10 @@ it 'renders the show view' do get :show - expect(@analytics).to have_logged_event('Add Email Address Page Visited') + expect(@analytics).to have_logged_event( + 'Add Email Address Page Visited', + in_select_email_flow: false, + ) end end @@ -86,6 +177,7 @@ errors: {}, user_id: user.uuid, domain_name: email.split('@').last, + in_select_email_flow: false, ) post :resend diff --git a/spec/forms/add_user_email_form_spec.rb b/spec/forms/add_user_email_form_spec.rb index 18a1b72b0fe..a6d1d45c1b6 100644 --- a/spec/forms/add_user_email_form_spec.rb +++ b/spec/forms/add_user_email_form_spec.rb @@ -8,8 +8,19 @@ describe '#submit' do let(:new_email) { 'new@example.com' } - - subject(:submit) { form.submit(user, email: new_email) } + let(:request_id) { 'request-id-1' } + + subject(:submit) { form.submit(user, email: new_email, request_id:) } + + it 'returns a successful result' do + expect(submit.to_h).to eq( + success: true, + errors: {}, + domain_name: 'example.com', + in_select_email_flow: false, + user_id: user.uuid, + ) + end it 'creates a new EmailAddress record for a new email address' do expect(EmailAddress.find_with_email(new_email)).to be_nil @@ -79,5 +90,31 @@ end end end + + context 'in select email flow' do + subject(:form) { AddUserEmailForm.new(in_select_email_flow: true) } + + it 'sends email confirm with parameter value' do + send_add_email_confirmation = instance_double(SendAddEmailConfirmation) + expect(SendAddEmailConfirmation).to receive(:new).and_return(send_add_email_confirmation) + expect(send_add_email_confirmation).to receive(:call).with( + email_address: kind_of(EmailAddress), + in_select_email_flow: true, + request_id:, + ) + + submit + end + + it 'includes extra analytics in result for flow value' do + expect(submit.to_h).to eq( + success: true, + errors: {}, + domain_name: 'example.com', + in_select_email_flow: true, + user_id: user.uuid, + ) + end + end end end diff --git a/spec/forms/select_email_form_spec.rb b/spec/forms/select_email_form_spec.rb index 3fea0f38232..539fd7ef340 100644 --- a/spec/forms/select_email_form_spec.rb +++ b/spec/forms/select_email_form_spec.rb @@ -14,7 +14,7 @@ let(:selected_email_id) { user.confirmed_email_addresses.take.id } it 'is successful' do - expect(response.to_h).to eq(success: true) + expect(response.to_h).to eq(success: true, selected_email_id:) end context 'with associated identity' do @@ -29,15 +29,28 @@ end context 'with an invalid email id' do - let(:selected_email_id) { nil } + let(:selected_email_id) { '' } it 'is unsuccessful' do expect(response.to_h).to eq( success: false, error_details: { selected_email_id: { not_found: true } }, + selected_email_id: nil, ) end + context 'with present value that does not convert to numeric' do + let(:selected_email_id) { true } + + it 'is unsuccessful without raising exception' do + expect(response.to_h).to eq( + success: false, + error_details: { selected_email_id: { not_found: true } }, + selected_email_id: nil, + ) + end + end + context 'with associated identity' do let(:identity) do create( @@ -55,7 +68,7 @@ end context 'with an unconfirmed email address added' do - let(:selected_email_id) { user.email_addresses.find_by(confirmed_at: nil) } + let(:selected_email_id) { user.email_addresses.find_by(confirmed_at: nil).id } before do create(:email_address, :unconfirmed, user:) @@ -65,6 +78,7 @@ expect(response.to_h).to eq( success: false, error_details: { selected_email_id: { not_found: true } }, + selected_email_id:, ) end @@ -85,6 +99,7 @@ expect(response.to_h).to eq( success: false, error_details: { selected_email_id: { not_found: true } }, + selected_email_id:, ) end diff --git a/spec/services/email_confirmation_token_validator_spec.rb b/spec/services/email_confirmation_token_validator_spec.rb index 8be91518c75..d63e3a2d747 100644 --- a/spec/services/email_confirmation_token_validator_spec.rb +++ b/spec/services/email_confirmation_token_validator_spec.rb @@ -2,7 +2,7 @@ RSpec.describe EmailConfirmationTokenValidator do describe '#submit' do - subject { described_class.new(email_address, current_user) } + subject { described_class.new(email_address:, current_user:) } context 'the email of the user does not match the user confirming' do let(:current_user) { create(:user, :fully_registered) } @@ -88,7 +88,7 @@ end describe '#email_address_already_confirmed_by_user?' do - subject { described_class.new(email_address) } + subject { described_class.new(email_address:) } context 'the email address was confirmed by the user' do let(:email_address) do From 88d95f0ad7f164186c8ae64197186dae942fe1ae Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Wed, 4 Dec 2024 09:01:39 -0500 Subject: [PATCH 05/14] LG-15155: Logging when user visits connected accounts page (#11554) * add analytics event for when users visits connected accounts page changelog: Internal, logging bugfix, add logging event for connected accounts page visit * add supporting test * lintfix: event in alphabetical order * Update app/controllers/accounts/connected_accounts_controller.rb Co-authored-by: Andrew Duthie <1779930+aduth@users.noreply.github.com> * flatten test --------- Co-authored-by: Andrew Duthie <1779930+aduth@users.noreply.github.com> --- .../accounts/connected_accounts_controller.rb | 1 + app/services/analytics_events.rb | 5 +++++ .../connected_accounts_controller_spec.rb | 21 +++++++++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 spec/controllers/accounts/connected_accounts_controller_spec.rb diff --git a/app/controllers/accounts/connected_accounts_controller.rb b/app/controllers/accounts/connected_accounts_controller.rb index 401ef2eda2a..f9c4581986e 100644 --- a/app/controllers/accounts/connected_accounts_controller.rb +++ b/app/controllers/accounts/connected_accounts_controller.rb @@ -8,6 +8,7 @@ class ConnectedAccountsController < ApplicationController layout 'account_side_nav' def show + analytics.connected_accounts_page_visited @presenter = AccountShowPresenter.new( decrypted_pii: nil, sp_session_request_url: sp_session_request_url_with_updated_params, diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index c8d3d1f64b9..d5f8016451d 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -451,6 +451,11 @@ def concurrent_session_logout track_event(:concurrent_session_logout) end + # User visits the connected accounts page + def connected_accounts_page_visited + track_event(:connected_accounts_page_visited) + end + # @param [String] redirect_url URL user was directed to # @param [String, nil] step which step # @param [String, nil] location which part of a step, if applicable diff --git a/spec/controllers/accounts/connected_accounts_controller_spec.rb b/spec/controllers/accounts/connected_accounts_controller_spec.rb new file mode 100644 index 00000000000..2bf473bb5d0 --- /dev/null +++ b/spec/controllers/accounts/connected_accounts_controller_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Accounts::ConnectedAccountsController do + describe '#show' do + let(:user) { create(:user, :fully_registered) } + + before do + stub_sign_in(user) if user + end + + it 'shows and logs a visit' do + stub_analytics + + get :show + + expect(@analytics).to have_logged_event(:connected_accounts_page_visited) + end + end +end From c5c2191436911128abf09c2e4663229aabc3edda Mon Sep 17 00:00:00 2001 From: Jessica Dembe Date: Wed, 4 Dec 2024 09:37:43 -0500 Subject: [PATCH 06/14] LG-14189: A/B test for allowing F/T setup and authentication on desktop (#11347) * add desktop ab test information to form * add a/b test configuration * remove anything related to ab test bucket * add passkey support on desktop * remove device supported check * show_unsupported_passkey_platform_authentication_setup * fix associated test * changelog: Upcoming Features, desktop f/t unlock, A/B setup for desktop f/t unlock * fix js test * remove `deskton_ab_bucket?`; remove device does not support passkey tests * note to self * restore `isWebauthnPaskeySupported` * remove desktop qualifying fns, change logic for supported and available devices * restore `show_unsupported_passkey` functionality * lintfixes * fix tests * remove `@desktop_ab_test_bucket` * rename to `desktop_ft_unlock_setup_option_percent_tested` * work on specs for A/B test * add tag so that functionality to show/hide can be in place * toggle show based on english language * restore conditional to show based on A/B enablement * add javascript test * lintfix * changelog: Upcoming Features, A/B test, create A/B test for desktop F/T unlock setup * fix setup for desktop f/t unlock test * lintfixes * track event when user is in a/b test but would not show otherwise * WIP: show/hide based on bucket * Add component tests for desktop-ft-unlock-option * Add controller specs for presenter assigns ab test value * Fix syntax error on assignment * Add feature test for A/B test setup on desktop * fix js code and tests * fix tests and associated logic * add desktop ft unlock capability on login options * set logic for `desktop-ft-unlock-option` class on sign in for test * show/hide based on value * fix error on line * remove * rename bucket, remove a/b test setup from log in files, pass bucket percentage to sign up screen * lintfix * fix js test * js lintfixes * fix F/T unlock show logic * fix for f/t unlock logic? * fix javascript test * remove unneeded logic from input element, remove `trackEvent` * lintfix, change default config number * more lintfixes * clean up hidden and webauthn-input-element specs * Update app/components/webauthn_input_component.rb Co-authored-by: Andrew Duthie <1779930+aduth@users.noreply.github.com> * Update app/javascript/packages/webauthn/webauthn-input-element.ts Co-authored-by: Andrew Duthie <1779930+aduth@users.noreply.github.com> * update test * delete unused analytics event * change analytics event, remove duplicate a/b test * do check for bucket type * set up method if in bucket and test is running * check for A/B test flag * For bucket check, change value type and fix test * fix js test * lintfix * remove desktop only check * Update app/controllers/users/two_factor_authentication_setup_controller.rb Co-authored-by: Andrew Duthie <1779930+aduth@users.noreply.github.com> * add test that should not have been deleted * change definition name * lintfix by changing name of method * add `User Registration: 2FA Setup visited` to `DESKTOP_FT_UNLOCK_SETUP` A/B test * change placement of test * remove duplicate test * lintfix --------- Co-authored-by: Andrew Duthie Co-authored-by: Andrew Duthie <1779930+aduth@users.noreply.github.com> --- app/components/webauthn_input_component.rb | 11 ++++- ..._factor_authentication_setup_controller.rb | 6 +++ .../users/webauthn_setup_controller.rb | 2 +- .../webauthn/webauthn-input-element.spec.ts | 14 ++++++ .../webauthn/webauthn-input-element.ts | 11 ++++- .../set_up_selection_presenter.rb | 10 +++- ...p_webauthn_platform_selection_presenter.rb | 1 + .../two_factor_options_presenter.rb | 8 +++- config/application.yml.default | 2 + config/initializers/ab_tests.rb | 11 +++++ lib/identity_config.rb | 1 + .../webauthn_input_component_spec.rb | 22 ++++++++- spec/config/initializers/ab_tests_spec.rb | 47 ++++++++++++++++++- ...or_authentication_setup_controller_spec.rb | 22 +++++++++ spec/features/webauthn/hidden_spec.rb | 31 ++++++++++++ 15 files changed, 188 insertions(+), 11 deletions(-) diff --git a/app/components/webauthn_input_component.rb b/app/components/webauthn_input_component.rb index 8c2b952b5f6..f029d9f1c8f 100644 --- a/app/components/webauthn_input_component.rb +++ b/app/components/webauthn_input_component.rb @@ -1,21 +1,25 @@ # frozen_string_literal: true class WebauthnInputComponent < BaseComponent - attr_reader :platform, :passkey_supported_only, :show_unsupported_passkey, :tag_options + attr_reader :platform, :passkey_supported_only, :show_unsupported_passkey, + :desktop_ft_unlock_option, :tag_options alias_method :platform?, :platform alias_method :passkey_supported_only?, :passkey_supported_only alias_method :show_unsupported_passkey?, :show_unsupported_passkey + alias_method :desktop_ft_unlock_option?, :desktop_ft_unlock_option def initialize( platform: false, passkey_supported_only: false, show_unsupported_passkey: false, + desktop_ft_unlock_option: false, **tag_options ) @platform = platform @passkey_supported_only = passkey_supported_only @show_unsupported_passkey = show_unsupported_passkey + @desktop_ft_unlock_option = desktop_ft_unlock_option @tag_options = tag_options end @@ -26,6 +30,7 @@ def call **tag_options, **initial_hidden_tag_options, 'show-unsupported-passkey': show_unsupported_passkey?.presence, + 'desktop-ft-unlock-option': show_desktop_ft_unlock_option?.presence, ) end @@ -36,4 +41,8 @@ def initial_hidden_tag_options { class: 'js' } end end + + def show_desktop_ft_unlock_option? + desktop_ft_unlock_option? && I18n.locale == :en + end end diff --git a/app/controllers/users/two_factor_authentication_setup_controller.rb b/app/controllers/users/two_factor_authentication_setup_controller.rb index 34b5370f2ce..3bfe5e83455 100644 --- a/app/controllers/users/two_factor_authentication_setup_controller.rb +++ b/app/controllers/users/two_factor_authentication_setup_controller.rb @@ -5,6 +5,7 @@ class TwoFactorAuthenticationSetupController < ApplicationController include UserAuthenticator include MfaSetupConcern include AbTestingConcern + include ApplicationHelper before_action :authenticate_user before_action :confirm_user_authenticated_for_2fa_setup @@ -68,6 +69,7 @@ def two_factor_options_presenter show_skip_additional_mfa_link: show_skip_additional_mfa_link?, after_mfa_setup_path:, return_to_sp_cancel_path:, + desktop_ft_ab_test: in_ab_test_bucket?, ) end @@ -81,5 +83,9 @@ def two_factor_options_form_params rescue ActionController::ParameterMissing ActionController::Parameters.new(selection: []) end + + def in_ab_test_bucket? + ab_test_bucket(:DESKTOP_FT_UNLOCK_SETUP) == (:desktop_ft_unlock_option_shown) + end end end diff --git a/app/controllers/users/webauthn_setup_controller.rb b/app/controllers/users/webauthn_setup_controller.rb index b67f5b04275..b7ae7912f5a 100644 --- a/app/controllers/users/webauthn_setup_controller.rb +++ b/app/controllers/users/webauthn_setup_controller.rb @@ -88,7 +88,7 @@ def validate_existing_platform_authenticator if platform_authenticator? && in_account_creation_flow? && current_user.webauthn_configurations.platform_authenticators.present? redirect_to authentication_methods_setup_path - end + end end def webauthn_auth_method diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts index afd363cc143..641058c2ec3 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts @@ -40,6 +40,20 @@ describe('WebauthnInputElement', () => { }); }); + context('as a part of A/B test', () => { + beforeEach(() => { + isWebauthnPasskeySupported.returns(false); + isWebauthnPlatformAvailable.resolves(true); + document.body.innerHTML = ``; + }); + + it('becomes visible', async () => { + const element = document.querySelector('lg-webauthn-input')!; + + await waitFor(() => expect(element.hidden).to.be.false()); + }); + }); + context('unsupported passkey shown', () => { beforeEach(() => { isWebauthnPasskeySupported.returns(false); diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts index 954cac625c0..a7e9759b6c4 100644 --- a/app/javascript/packages/webauthn/webauthn-input-element.ts +++ b/app/javascript/packages/webauthn/webauthn-input-element.ts @@ -1,11 +1,15 @@ -import isWebauthnPasskeySupported from './is-webauthn-passkey-supported'; import isWebauthnPlatformAuthenticatorAvailable from './is-webauthn-platform-authenticator-available'; +import isWebauthnPasskeySupported from './is-webauthn-passkey-supported'; export class WebauthnInputElement extends HTMLElement { connectedCallback() { this.toggleVisibleIfPasskeySupported(); } + get isOptedInToAbTest(): boolean { + return this.hasAttribute('desktop-ft-unlock-option'); + } + get isPlatform(): boolean { return this.hasAttribute('platform'); } @@ -19,7 +23,10 @@ export class WebauthnInputElement extends HTMLElement { return; } - if (isWebauthnPasskeySupported() && (await isWebauthnPlatformAuthenticatorAvailable())) { + if ( + (isWebauthnPasskeySupported() || this.isOptedInToAbTest) && + (await isWebauthnPlatformAuthenticatorAvailable()) + ) { this.hidden = false; } else if (this.showUnsupportedPasskey) { this.hidden = false; diff --git a/app/presenters/two_factor_authentication/set_up_selection_presenter.rb b/app/presenters/two_factor_authentication/set_up_selection_presenter.rb index 798cd289c67..7942bcfc260 100644 --- a/app/presenters/two_factor_authentication/set_up_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/set_up_selection_presenter.rb @@ -4,7 +4,11 @@ module TwoFactorAuthentication class SetUpSelectionPresenter include ActionView::Helpers::TranslationHelper - attr_reader :user, :piv_cac_required, :phishing_resistant_required, :user_agent + attr_reader :user, + :piv_cac_required, + :phishing_resistant_required, + :user_agent, + :desktop_ft_ab_test alias_method :piv_cac_required?, :piv_cac_required alias_method :phishing_resistant_required?, :phishing_resistant_required @@ -12,12 +16,14 @@ def initialize( user:, piv_cac_required: false, phishing_resistant_required: false, - user_agent: nil + user_agent: nil, + desktop_ft_ab_test: nil ) @user = user @piv_cac_required = piv_cac_required @phishing_resistant_required = phishing_resistant_required @user_agent = user_agent + @desktop_ft_ab_test = desktop_ft_ab_test end def render_in(view_context, &block) diff --git a/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb b/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb index a4fef5d7285..1e7ff563da8 100644 --- a/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/set_up_webauthn_platform_selection_presenter.rb @@ -13,6 +13,7 @@ def render_in(view_context, &block) passkey_supported_only: true, show_unsupported_passkey: IdentityConfig.store.show_unsupported_passkey_platform_authentication_setup, + desktop_ft_unlock_option: desktop_ft_ab_test, ), &block ) diff --git a/app/presenters/two_factor_options_presenter.rb b/app/presenters/two_factor_options_presenter.rb index 3477afbd009..1dc080b59e4 100644 --- a/app/presenters/two_factor_options_presenter.rb +++ b/app/presenters/two_factor_options_presenter.rb @@ -8,7 +8,8 @@ class TwoFactorOptionsPresenter :return_to_sp_cancel_path, :phishing_resistant_required, :piv_cac_required, - :user_agent + :user_agent, + :desktop_ft_ab_test delegate :two_factor_enabled?, to: :mfa_policy def initialize( @@ -18,7 +19,8 @@ def initialize( piv_cac_required: false, show_skip_additional_mfa_link: true, after_mfa_setup_path: nil, - return_to_sp_cancel_path: nil + return_to_sp_cancel_path: nil, + desktop_ft_ab_test: false ) @user_agent = user_agent @user = user @@ -27,6 +29,7 @@ def initialize( @show_skip_additional_mfa_link = show_skip_additional_mfa_link @after_mfa_setup_path = after_mfa_setup_path @return_to_sp_cancel_path = return_to_sp_cancel_path + @desktop_ft_ab_test = desktop_ft_ab_test end def options @@ -47,6 +50,7 @@ def all_options_sorted piv_cac_required: piv_cac_required?, phishing_resistant_required: phishing_resistant_only?, user_agent:, + desktop_ft_ab_test:, ) end. partition(&:recommended?). diff --git a/config/application.yml.default b/config/application.yml.default index ea3d95f4d9e..e4017b0d886 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -90,6 +90,7 @@ database_worker_jobs_sslmode: 'verify-full' database_worker_jobs_username: '' deleted_user_accounts_report_configs: '[]' deliver_mail_async: false +desktop_ft_unlock_setup_option_percent_tested: 0 development_mailer_deliver_method: letter_opener disable_email_sending: true disable_logout_get_request: true @@ -458,6 +459,7 @@ development: compromised_password_randomizer_value: 1 dashboard_api_token: test_token dashboard_url: http://localhost:3001/api/service_providers + desktop_ft_unlock_setup_option_percent_tested: 100 doc_auth_selfie_desktop_test_mode: true domain_name: localhost:3000 enable_rate_limiting: false diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index fff91a2dd65..c4a4cce8924 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -104,4 +104,15 @@ def self.all shadow_mode_enabled: IdentityConfig.store.socure_idplus_shadow_mode_percent, }, ).freeze + + DESKTOP_FT_UNLOCK_SETUP = AbTest.new( + experiment_name: 'Desktop F/T unlock setup', + should_log: [ + 'User Registration: 2FA Setup visited', + :webauthn_setup_submitted, + 'Multi-Factor Authentication Setup', + ].to_set, + buckets: { desktop_ft_unlock_option_shown: + IdentityConfig.store.desktop_ft_unlock_setup_option_percent_tested }, + ).freeze end diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 2b721e2c7a7..9f9fa617b22 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -108,6 +108,7 @@ def self.store config.add(:database_worker_jobs_username, type: :string) config.add(:deleted_user_accounts_report_configs, type: :json) config.add(:deliver_mail_async, type: :boolean) + config.add(:desktop_ft_unlock_setup_option_percent_tested, type: :integer) config.add(:development_mailer_deliver_method, type: :symbol, enum: [:file, :letter_opener]) config.add(:disable_email_sending, type: :boolean) config.add(:disable_logout_get_request, type: :boolean) diff --git a/spec/components/webauthn_input_component_spec.rb b/spec/components/webauthn_input_component_spec.rb index 2e9f99d9931..5212698d7e9 100644 --- a/spec/components/webauthn_input_component_spec.rb +++ b/spec/components/webauthn_input_component_spec.rb @@ -17,8 +17,26 @@ expect(component.passkey_supported_only?).to eq(false) end - it 'exposes boolean alias for show_unsupported_passkey option' do - expect(component.show_unsupported_passkey?).to eq(false) + it 'does not render desktop-ft-unlock-option attribute' do + expect(rendered).to have_css('lg-webauthn-input:not([desktop-ft-unlock-option="false"])') + end + + context 'with desktop_ft_unlock_option' do + let(:options) { super().merge(desktop_ft_unlock_option: true) } + + it 'does render desktop-ft-unlock-option attribute' do + expect(rendered).to have_css('lg-webauthn-input[desktop-ft-unlock-option="true"]') + end + + context 'in a locale other than english' do + before do + I18n.locale = I18n.available_locales.sample + end + + it 'does not render desktop-ft-unlock-option attribute' do + expect(rendered).to have_css('lg-webauthn-input:not([desktop-ft-unlock-option="false"])') + end + end end context 'with platform option' do diff --git a/spec/config/initializers/ab_tests_spec.rb b/spec/config/initializers/ab_tests_spec.rb index 8c14e6f0924..724b71e32df 100644 --- a/spec/config/initializers/ab_tests_spec.rb +++ b/spec/config/initializers/ab_tests_spec.rb @@ -220,7 +220,7 @@ end end - describe '.RECOMMEND_WEBAUTHN_PLATFORM_FOR_SMS_USER' do + describe 'RECOMMEND_WEBAUTHN_PLATFORM_FOR_SMS_USER' do let(:user) { create(:user) } subject(:bucket) do @@ -301,4 +301,49 @@ end end end + + describe 'DESKTOP_FT_UNLOCK_SETUP' do + let(:user) { nil } + let(:user_session) { {} } + + subject(:bucket) do + AbTests::DESKTOP_FT_UNLOCK_SETUP.bucket( + request: nil, + service_provider: nil, + session: nil, + user:, + user_session:, + ) + end + + context 'when A/B test is disabled' do + before do + allow(IdentityConfig.store).to receive(:desktop_ft_unlock_setup_option_percent_tested). + and_return(0) + reload_ab_tests + end + + context 'when it would otherwise assign a bucket' do + let(:user) { build(:user) } + + it 'does not return a bucket' do + expect(bucket).to be_nil + end + end + end + + context 'when A/B test is enabled' do + before do + allow(IdentityConfig.store).to receive(:desktop_ft_unlock_setup_option_percent_tested). + and_return(100) + reload_ab_tests + end + + let(:user) { build(:user) } + + it 'returns a bucket' do + expect(bucket).not_to be_nil + end + end + end end diff --git a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb index 1cc8d62a217..574627a8860 100644 --- a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb +++ b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb @@ -4,6 +4,8 @@ describe 'GET index' do let(:user) { create(:user) } + subject(:response) { get :index } + before do stub_sign_in_before_2fa(user) if user stub_analytics @@ -19,6 +21,12 @@ ) end + it 'initializes presenter with false ab test bucket value' do + response + + expect(assigns(:presenter).desktop_ft_ab_test).to be false + end + context 'with user having gov or mil email' do let!(:federal_domain) { create(:federal_email_domain, name: 'gsa.gov') } let(:user) do @@ -101,6 +109,20 @@ expect(response).to redirect_to(user_two_factor_authentication_url) end end + + context 'with user opted in to desktop ft unlock setup ab test' do + before do + allow(controller).to receive(:ab_test_bucket).with( + :DESKTOP_FT_UNLOCK_SETUP, + ).and_return(:desktop_ft_unlock_option_shown) + end + + it 'initializes presenter with ab test bucket value' do + response + + expect(assigns(:presenter).desktop_ft_ab_test).to eq(true) + end + end end describe '#create' do diff --git a/spec/features/webauthn/hidden_spec.rb b/spec/features/webauthn/hidden_spec.rb index 57b52cbb92b..0c0ea800316 100644 --- a/spec/features/webauthn/hidden_spec.rb +++ b/spec/features/webauthn/hidden_spec.rb @@ -3,6 +3,7 @@ RSpec.describe 'webauthn hide' do include JavascriptDriverHelper include WebAuthnHelper + include AbTestsHelper describe 'security key' do let(:option_id) { 'two_factor_options_form_selection_webauthn' } @@ -59,6 +60,36 @@ expect(webauthn_option_hidden?).to eq(true) end + context 'when in ab test for desktop setup' do + before do + allow(IdentityConfig.store).to receive(:desktop_ft_unlock_setup_option_percent_tested). + and_return(100) + reload_ab_tests + end + + it 'displays the authenticator option' do + sign_up_and_set_password + simulate_platform_authenticator_available + + expect(webauthn_option_hidden?).to eq(false) + end + end + + context 'when A/B test is disabled' do + before do + allow(IdentityConfig.store).to receive(:desktop_ft_unlock_setup_option_percent_tested). + and_return(0) + reload_ab_tests + end + + it 'hides the authenticator option' do + sign_up_and_set_password + simulate_platform_authenticator_available + + expect(webauthn_option_hidden?).to eq(true) + end + end + context 'with supported browser and platform authenticator available', driver: :headless_chrome_mobile do it 'displays the authenticator option' do From 9c37e3e1167826ed1340a71a3cf6d53062c2af22 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Wed, 4 Dec 2024 10:15:23 -0500 Subject: [PATCH 07/14] Update rails-html-sanitizer to 1.6.1 (#11589) changelog: Internal, Dependencies, Update dependency to resolve security advisory --- Gemfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index e27bf114bc0..0f7b894333c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -429,7 +429,7 @@ GEM method_source (1.1.0) mini_histogram (0.3.1) mini_mime (1.1.5) - mini_portile2 (2.8.7) + mini_portile2 (2.8.8) minitest (5.25.1) msgpack (1.7.2) multiset (0.5.3) @@ -451,7 +451,7 @@ GEM net-ssh (6.1.0) newrelic_rpm (9.7.0) nio4r (2.7.3) - nokogiri (1.16.7) + nokogiri (1.16.8) mini_portile2 (~> 2.8.2) racc (~> 1.4) numbers_and_words (0.11.12) @@ -544,9 +544,9 @@ GEM activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) + rails-html-sanitizer (1.6.1) loofah (~> 2.21) - nokogiri (~> 1.14) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) rails-i18n (7.0.6) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) From 64fc60021683be0cbd003d4da6bacd02091351ee Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Wed, 4 Dec 2024 10:55:22 -0500 Subject: [PATCH 08/14] Update Rubocop to latest version (#11590) * Update Rubocop to 1.69 changelog: Internal, Dependencies, Update dependencies to latest version * Update custom linters to extend base class Avoid deprecation warnings: "Inheriting from `RuboCop::Cop::Cop` is deprecated. Use `RuboCop::Cop::Base` instead." * Update Rubocop plugin gems * Remove unnecessary use of unsupported location arg * Fix existing issues after upgrade * Enable newly-available cops from Rubocop plugins * Remove more usage of unsupported location argument --- .rubocop.yml | 9 ++++ Gemfile | 8 ++-- Gemfile.lock | 41 ++++++++----------- .../account_creation_threat_metrix_job.rb | 1 - config/initializers/strong_migrations.rb | 4 +- lib/linters/analytics_event_name_linter.rb | 3 +- lib/linters/errors_add_linter.rb | 4 +- lib/linters/image_size_linter.rb | 4 +- .../localized_validation_message_linter.rb | 4 +- lib/linters/mail_later_linter.rb | 4 +- lib/linters/redirect_back_linter.rb | 6 +-- lib/linters/url_options_linter.rb | 4 +- spec/models/backup_code_configuration_spec.rb | 10 +++-- 13 files changed, 52 insertions(+), 50 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 00d3129d03b..e4e614283cd 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -803,6 +803,9 @@ Performance/Squeeze: Performance/StartWith: Enabled: true +Performance/StringBytesize: + Enabled: true + Performance/StringIdentifierArgument: Enabled: true @@ -1050,6 +1053,12 @@ Rails/WhereRange: RSpec/LeakyConstantDeclaration: Enabled: true +RSpec/MissingExpectationTargetMethod: + Enabled: true + +RSpec/RedundantPredicateMatcher: + Enabled: true + Security/Eval: Enabled: true diff --git a/Gemfile b/Gemfile index 8c3c853be75..a78cabedce0 100644 --- a/Gemfile +++ b/Gemfile @@ -118,10 +118,10 @@ group :development, :test do gem 'psych' gem 'rspec', '~> 3.13.0' gem 'rspec-rails', '~> 7.0' - gem 'rubocop', '~> 1.62.0', require: false - gem 'rubocop-performance', '~> 1.20.2', require: false - gem 'rubocop-rails', '>= 2.26.2', require: false - gem 'rubocop-rspec', require: false + gem 'rubocop', '~> 1.69.1', require: false + gem 'rubocop-performance', '~> 1.23.0', require: false + gem 'rubocop-rails', '~> 2.27.0', require: false + gem 'rubocop-rspec', '~> 3.2.0', require: false gem 'sqlite3', require: false end diff --git a/Gemfile.lock b/Gemfile.lock index 0f7b894333c..ee3f5b1670c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -382,7 +382,7 @@ GEM jmespath (1.6.2) jsbundling-rails (1.1.2) railties (>= 6.0.0) - json (2.7.2) + json (2.9.0) jwe (0.4.0) jwt (2.7.1) knapsack (4.0.0) @@ -460,7 +460,7 @@ GEM openssl-signature_algorithm (1.2.1) openssl (> 2.0, < 3.1) orm_adapter (0.5.0) - parallel (1.25.1) + parallel (1.26.3) parser (3.3.4.2) ast (~> 2.4.1) racc @@ -571,7 +571,7 @@ GEM redis-client (>= 0.22.0) redis-client (0.22.2) connection_pool - regexp_parser (2.9.2) + regexp_parser (2.9.3) reline (0.5.9) io-console (~> 0.5) request_store (1.5.1) @@ -612,35 +612,28 @@ GEM rspec-support (3.13.1) rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (1.62.1) + rubocop (1.69.1) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.31.1, < 2.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.36.2, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.31.3) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.36.2) parser (>= 3.3.1.0) - rubocop-capybara (2.19.0) - rubocop (~> 1.41) - rubocop-factory_bot (2.24.0) - rubocop (~> 1.33) - rubocop-performance (1.20.2) + rubocop-performance (1.23.0) rubocop (>= 1.48.1, < 2.0) - rubocop-ast (>= 1.30.0, < 2.0) - rubocop-rails (2.26.2) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rails (2.27.0) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.52.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rspec (2.24.1) - rubocop (~> 1.33) - rubocop-capybara (~> 2.17) - rubocop-factory_bot (~> 2.22) + rubocop-rspec (3.2.0) + rubocop (~> 1.61) ruby-progressbar (1.13.0) ruby-saml (1.17.0) nokogiri (>= 1.13.10) @@ -851,10 +844,10 @@ DEPENDENCIES rspec-rails (~> 7.0) rspec-retry rspec_junit_formatter - rubocop (~> 1.62.0) - rubocop-performance (~> 1.20.2) - rubocop-rails (>= 2.26.2) - rubocop-rspec + rubocop (~> 1.69.1) + rubocop-performance (~> 1.23.0) + rubocop-rails (~> 2.27.0) + rubocop-rspec (~> 3.2.0) ruby-progressbar ruby-saml safe_target_blank (>= 1.0.2) diff --git a/app/jobs/account_creation_threat_metrix_job.rb b/app/jobs/account_creation_threat_metrix_job.rb index 4d1dd53fe32..aa5b439e01d 100644 --- a/app/jobs/account_creation_threat_metrix_job.rb +++ b/app/jobs/account_creation_threat_metrix_job.rb @@ -7,7 +7,6 @@ def perform( request_ip: nil, email: nil ) - device_profiling_result = AccountCreation::DeviceProfiling.new.proof( request_ip: request_ip, threatmetrix_session_id: threatmetrix_session_id, diff --git a/config/initializers/strong_migrations.rb b/config/initializers/strong_migrations.rb index b7e3d6c5a8a..8320ec2edb2 100644 --- a/config/initializers/strong_migrations.rb +++ b/config/initializers/strong_migrations.rb @@ -15,13 +15,13 @@ class IdpStrongMigrations StrongMigrations.add_check do |method, (table, column, type, _options)| is_excluded = IdpStrongMigrations::EXCLUDED_COLUMNS.include?([table, column]) if !is_excluded && method == :add_column && column.to_s.ends_with?('_id') && type == :integer - stop! """ + stop! " Columns referencing another table should use :bigint instead of integer. add_column #{table.inspect}, #{column.inspect}, :bigint OR t.bigint #{column.inspect} - """ + " end end diff --git a/lib/linters/analytics_event_name_linter.rb b/lib/linters/analytics_event_name_linter.rb index eed859cd3d4..bd4d8566d0b 100644 --- a/lib/linters/analytics_event_name_linter.rb +++ b/lib/linters/analytics_event_name_linter.rb @@ -3,7 +3,7 @@ module RuboCop module Cop module IdentityIdp - class AnalyticsEventNameLinter < RuboCop::Cop::Cop + class AnalyticsEventNameLinter < RuboCop::Cop::Base RESTRICT_ON_SEND = [:track_event].freeze # DO NOT ADD TO THIS LIST OR YOU WILL MAKE A KITTEN CRY! @@ -45,7 +45,6 @@ def on_send(node) return if LEGACY_EVENT_NAMES.include?(Digest::MD5.hexdigest(actual_name.to_s)[0...7]) add_offense( first_argument, - location: :expression, message: "Event name must match the method name, expected `:#{expected_name}`", ) end diff --git a/lib/linters/errors_add_linter.rb b/lib/linters/errors_add_linter.rb index 3c36112fe4d..72447e0af80 100644 --- a/lib/linters/errors_add_linter.rb +++ b/lib/linters/errors_add_linter.rb @@ -14,7 +14,7 @@ module IdentityIdp # #good # errors.add(:iss, 'invalid issuer', type: :invalid_issuer) # - class ErrorsAddLinter < RuboCop::Cop::Cop + class ErrorsAddLinter < RuboCop::Cop::Base MSG = 'Please set a unique key for this error' RESTRICT_ON_SEND = [:add].freeze @@ -29,7 +29,7 @@ def on_send(node) return if type && type.type == :sym options = type if type && type.type == :hash return if options && options.type == :hash && options.keys.map(&:value).include?(:type) - add_offense(node, location: :expression) + add_offense(node) end end end diff --git a/lib/linters/image_size_linter.rb b/lib/linters/image_size_linter.rb index b5f37076489..680e3357629 100644 --- a/lib/linters/image_size_linter.rb +++ b/lib/linters/image_size_linter.rb @@ -15,13 +15,13 @@ module IdentityIdp # # good # image_tag 'example.svg', width: 10, height: 20 # - class ImageSizeLinter < RuboCop::Cop::Cop + class ImageSizeLinter < RuboCop::Cop::Base MSG = 'Assign width and height to images' RESTRICT_ON_SEND = [:image_tag].freeze def on_send(node) - add_offense(node, location: :expression) if !valid?(node) + add_offense(node) if !valid?(node) end private diff --git a/lib/linters/localized_validation_message_linter.rb b/lib/linters/localized_validation_message_linter.rb index 04f791d07fd..54e56dd483f 100644 --- a/lib/linters/localized_validation_message_linter.rb +++ b/lib/linters/localized_validation_message_linter.rb @@ -3,7 +3,7 @@ module RuboCop module Cop module IdentityIdp - class LocalizedValidationMessageLinter < RuboCop::Cop::Cop + class LocalizedValidationMessageLinter < RuboCop::Cop::Base MSG = 'Use proc when translating validation message' RESTRICT_ON_SEND = [ @@ -33,7 +33,7 @@ class LocalizedValidationMessageLinter < RuboCop::Cop::Cop def on_send(node) if translated_validation_message?(node) || translated_validation_helper_message?(node) - add_offense(node, location: :expression) + add_offense(node) end end end diff --git a/lib/linters/mail_later_linter.rb b/lib/linters/mail_later_linter.rb index cb70b536f08..12c891d5df3 100644 --- a/lib/linters/mail_later_linter.rb +++ b/lib/linters/mail_later_linter.rb @@ -16,7 +16,7 @@ module IdentityIdp # UserMailer.with(params).signup_with_your_email(user, email).deliver_now_or_later # ReportMailer.report_mail(data).deliver_now # - class MailLaterLinter < RuboCop::Cop::Cop + class MailLaterLinter < RuboCop::Cop::Base MSG = 'Please send mail using deliver_now_or_later instead' RESTRICT_ON_SEND = [:deliver_now, :deliver_later].freeze @@ -33,7 +33,7 @@ def on_send(node) receiver.receiver.const_name end - add_offense(node, location: :expression) if mailer_name == 'UserMailer' + add_offense(node) if mailer_name == 'UserMailer' end end end diff --git a/lib/linters/redirect_back_linter.rb b/lib/linters/redirect_back_linter.rb index cc756ebd54d..ad2fba37d4c 100644 --- a/lib/linters/redirect_back_linter.rb +++ b/lib/linters/redirect_back_linter.rb @@ -16,7 +16,7 @@ module IdentityIdp # #good # redirect_back fallback_location: '/', allow_other_host: false # - class RedirectBackLinter < RuboCop::Cop::Cop + class RedirectBackLinter < RuboCop::Cop::Base MSG = 'Please set a fallback_location and the allow_other_host parameter to false' RESTRICT_ON_SEND = [:redirect_back].freeze @@ -26,7 +26,7 @@ class RedirectBackLinter < RuboCop::Cop::Cop PATTERN def on_send(node) - add_offense(node, location: :expression) && return if node.arguments.empty? + add_offense(node) && return if node.arguments.empty? sets_fallback_location, sets_allow_other_host_false = false redirect_back_matcher(node) do |arguments| @@ -45,7 +45,7 @@ def on_send(node) return if sets_fallback_location && sets_allow_other_host_false - add_offense(node, location: :expression) + add_offense(node) end end end diff --git a/lib/linters/url_options_linter.rb b/lib/linters/url_options_linter.rb index 0c2303bdab7..3a2a8ba038a 100644 --- a/lib/linters/url_options_linter.rb +++ b/lib/linters/url_options_linter.rb @@ -33,7 +33,7 @@ module IdentityIdp # end # end # - class UrlOptionsLinter < RuboCop::Cop::Cop + class UrlOptionsLinter < RuboCop::Cop::Base MSG = 'Please define url_options when including Rails.application.routes.url_helpers' RESTRICT_ON_SEND = [:include].freeze @@ -47,7 +47,7 @@ def on_send(node) return unless includes_url_helpers?(node) return if defines_url_options?(node) - add_offense(node, location: :expression) + add_offense(node) end private diff --git a/spec/models/backup_code_configuration_spec.rb b/spec/models/backup_code_configuration_spec.rb index 05114d8473d..0558f99deef 100644 --- a/spec/models/backup_code_configuration_spec.rb +++ b/spec/models/backup_code_configuration_spec.rb @@ -140,8 +140,9 @@ def save_and_find(find:, save: 'just-some-not-null-value') bc = BackupCodeConfiguration.new set = BackupCodeConfiguration.selection_presenters([bc]) - expect(set.first). - instance_of? TwoFactorAuthentication::SignInBackupCodeSelectionPresenter.class + expect(set.first).to be_instance_of( + TwoFactorAuthentication::SignInBackupCodeSelectionPresenter, + ) end it 'returns only one selection presenter if multiple backup code configurations' do @@ -149,8 +150,9 @@ def save_and_find(find:, save: 'just-some-not-null-value') bc2 = BackupCodeConfiguration.new set = BackupCodeConfiguration.selection_presenters([bc, bc2]) - expect(set.first). - instance_of? TwoFactorAuthentication::SignInBackupCodeSelectionPresenter.class + expect(set.first).to be_instance_of( + TwoFactorAuthentication::SignInBackupCodeSelectionPresenter, + ) expect(set.size).to eq(1) end end From cd1f017c529d6cbe9fa4af9d8ded5684ccce3520 Mon Sep 17 00:00:00 2001 From: Vraj Mohan Date: Wed, 4 Dec 2024 08:17:14 -0800 Subject: [PATCH 09/14] Configure timeouts for reCAPTCHA requests See https://cm-jira.usa.gov/browse/LG-14520 changelog: Internal, reCAPTCHA, Configure timeouts for reCAPTCHA requests * Remove redundant configuration of specific timeouts in other places in the code as the generic timeout parameter should cover it. --- app/forms/recaptcha_form.rb | 1 + app/jobs/risc_delivery_job.rb | 3 --- app/services/doc_auth/lexis_nexis/request.rb | 3 --- app/services/doc_auth/socure/request.rb | 3 --- app/services/outbound_health_checker.rb | 3 --- app/services/piv_cac_service.rb | 3 --- app/services/proofing/lexis_nexis/authenticated_request.rb | 3 --- app/services/proofing/lexis_nexis/request.rb | 3 --- app/services/proofing/socure/id_plus/request.rb | 3 --- app/services/proofing/socure/reason_codes/api_client.rb | 3 --- app/services/recaptcha_annotator.rb | 2 ++ app/services/usps_in_person_proofing/proofer.rb | 3 --- config/application.yml.default | 1 + lib/identity_config.rb | 1 + 14 files changed, 5 insertions(+), 30 deletions(-) diff --git a/app/forms/recaptcha_form.rb b/app/forms/recaptcha_form.rb index 83ff73ab01f..2ce7c2b1d16 100644 --- a/app/forms/recaptcha_form.rb +++ b/app/forms/recaptcha_form.rb @@ -89,6 +89,7 @@ def recaptcha_result def faraday Faraday.new do |conn| + conn.options.timeout = IdentityConfig.store.recaptcha_request_timeout_in_seconds conn.request :instrumentation, name: 'request_log.faraday' conn.response :json end diff --git a/app/jobs/risc_delivery_job.rb b/app/jobs/risc_delivery_job.rb index 0598f9ff0bf..abe511bd2cd 100644 --- a/app/jobs/risc_delivery_job.rb +++ b/app/jobs/risc_delivery_job.rb @@ -90,9 +90,6 @@ def faraday f.request :instrumentation, name: 'request_log.faraday' f.adapter :net_http f.options.timeout = IdentityConfig.store.risc_notifications_request_timeout - f.options.read_timeout = IdentityConfig.store.risc_notifications_request_timeout - f.options.open_timeout = IdentityConfig.store.risc_notifications_request_timeout - f.options.write_timeout = IdentityConfig.store.risc_notifications_request_timeout end end diff --git a/app/services/doc_auth/lexis_nexis/request.rb b/app/services/doc_auth/lexis_nexis/request.rb index 5de35610644..a4dc52da630 100644 --- a/app/services/doc_auth/lexis_nexis/request.rb +++ b/app/services/doc_auth/lexis_nexis/request.rb @@ -109,9 +109,6 @@ def faraday_connection conn.request :authorization, :basic, username, password unless hmac_auth_enabled? conn.adapter :net_http conn.options.timeout = timeout - conn.options.read_timeout = timeout - conn.options.open_timeout = timeout - conn.options.write_timeout = timeout end end diff --git a/app/services/doc_auth/socure/request.rb b/app/services/doc_auth/socure/request.rb index cb7644c14a3..e1754634417 100644 --- a/app/services/doc_auth/socure/request.rb +++ b/app/services/doc_auth/socure/request.rb @@ -89,9 +89,6 @@ def faraday_connection conn.request :instrumentation, name: 'request_metric.faraday' conn.adapter :net_http conn.options.timeout = timeout - conn.options.read_timeout = timeout - conn.options.open_timeout = timeout - conn.options.write_timeout = timeout end end diff --git a/app/services/outbound_health_checker.rb b/app/services/outbound_health_checker.rb index 32863df5c0a..ca6db70f9b4 100644 --- a/app/services/outbound_health_checker.rb +++ b/app/services/outbound_health_checker.rb @@ -42,9 +42,6 @@ def faraday conn.request :retry, retry_options conn.options.timeout = IdentityConfig.store.outbound_connection_check_timeout - conn.options.read_timeout = IdentityConfig.store.outbound_connection_check_timeout - conn.options.open_timeout = IdentityConfig.store.outbound_connection_check_timeout - conn.options.write_timeout = IdentityConfig.store.outbound_connection_check_timeout # raises errors on 4XX or 5XX responses conn.response :raise_error diff --git a/app/services/piv_cac_service.rb b/app/services/piv_cac_service.rb index f85279f4a22..192d2524447 100644 --- a/app/services/piv_cac_service.rb +++ b/app/services/piv_cac_service.rb @@ -63,9 +63,6 @@ def token_response(token) Faraday.new(ssl: ssl_config) do |f| f.request :instrumentation, name: 'request_metric.faraday' f.options.timeout = IdentityConfig.store.piv_cac_service_timeout - f.options.read_timeout = IdentityConfig.store.piv_cac_service_timeout - f.options.open_timeout = IdentityConfig.store.piv_cac_service_timeout - f.options.write_timeout = IdentityConfig.store.piv_cac_service_timeout end.post( verify_token_uri, URI.encode_www_form({ token: token }), diff --git a/app/services/proofing/lexis_nexis/authenticated_request.rb b/app/services/proofing/lexis_nexis/authenticated_request.rb index 84f42644e4c..25d1d4f3d76 100644 --- a/app/services/proofing/lexis_nexis/authenticated_request.rb +++ b/app/services/proofing/lexis_nexis/authenticated_request.rb @@ -10,9 +10,6 @@ def send_request f.request :authorization, :basic, config.username, config.password end f.options.timeout = timeout - f.options.read_timeout = timeout - f.options.open_timeout = timeout - f.options.write_timeout = timeout end Response.new( diff --git a/app/services/proofing/lexis_nexis/request.rb b/app/services/proofing/lexis_nexis/request.rb index e54e0cf8e6a..abe95f5a3dc 100644 --- a/app/services/proofing/lexis_nexis/request.rb +++ b/app/services/proofing/lexis_nexis/request.rb @@ -19,9 +19,6 @@ def send_request conn = Faraday.new do |f| f.request :instrumentation, name: 'request_metric.faraday' f.options.timeout = timeout - f.options.read_timeout = timeout - f.options.open_timeout = timeout - f.options.write_timeout = timeout end Response.new( diff --git a/app/services/proofing/socure/id_plus/request.rb b/app/services/proofing/socure/id_plus/request.rb index 74b380ffe0b..8ec6f8c2c61 100644 --- a/app/services/proofing/socure/id_plus/request.rb +++ b/app/services/proofing/socure/id_plus/request.rb @@ -56,9 +56,6 @@ def send_request f.response :raise_error f.response :json f.options.timeout = config.timeout - f.options.read_timeout = config.timeout - f.options.open_timeout = config.timeout - f.options.write_timeout = config.timeout end Response.new( diff --git a/app/services/proofing/socure/reason_codes/api_client.rb b/app/services/proofing/socure/reason_codes/api_client.rb index 11c3730f748..5b62c08544b 100644 --- a/app/services/proofing/socure/reason_codes/api_client.rb +++ b/app/services/proofing/socure/reason_codes/api_client.rb @@ -26,9 +26,6 @@ def make_reason_code_http_request f.response :raise_error f.response :json f.options.timeout = IdentityConfig.store.socure_reason_code_timeout_in_seconds - f.options.read_timeout = IdentityConfig.store.socure_reason_code_timeout_in_seconds - f.options.open_timeout = IdentityConfig.store.socure_reason_code_timeout_in_seconds - f.options.write_timeout = IdentityConfig.store.socure_reason_code_timeout_in_seconds end conn.get(url, { group: true }, headers) do |req| diff --git a/app/services/recaptcha_annotator.rb b/app/services/recaptcha_annotator.rb index d9fea16d6b5..3b5e86fcdcf 100644 --- a/app/services/recaptcha_annotator.rb +++ b/app/services/recaptcha_annotator.rb @@ -39,6 +39,8 @@ def submit_annotation(assessment_id:, reason:, annotation:) def faraday Faraday.new do |conn| + conn.options.timeout = IdentityConfig.store.recaptcha_request_timeout_in_seconds + conn.request :instrumentation, name: 'request_log.faraday' conn.request :json conn.response :json diff --git a/app/services/usps_in_person_proofing/proofer.rb b/app/services/usps_in_person_proofing/proofer.rb index 9d24bc856c5..9aac7a62f88 100644 --- a/app/services/usps_in_person_proofing/proofer.rb +++ b/app/services/usps_in_person_proofing/proofer.rb @@ -119,9 +119,6 @@ def token def faraday Faraday.new(headers: request_headers) do |conn| conn.options.timeout = IdentityConfig.store.usps_ipp_request_timeout - conn.options.read_timeout = IdentityConfig.store.usps_ipp_request_timeout - conn.options.open_timeout = IdentityConfig.store.usps_ipp_request_timeout - conn.options.write_timeout = IdentityConfig.store.usps_ipp_request_timeout # Log request metrics conn.request :instrumentation, name: 'request_metric.faraday' diff --git a/config/application.yml.default b/config/application.yml.default index e4017b0d886..86e01893ec2 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -315,6 +315,7 @@ reauthn_window: 1200 recaptcha_enterprise_api_key: '' recaptcha_enterprise_project_id: '' recaptcha_mock_validator: true +recaptcha_request_timeout_in_seconds: 5 recaptcha_secret_key: '' recaptcha_site_key: '' recommend_webauthn_platform_for_sms_ab_test_account_creation_percent: 0 diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 9f9fa617b22..b66644e7792 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -351,6 +351,7 @@ def self.store config.add(:recaptcha_enterprise_api_key, type: :string) config.add(:recaptcha_enterprise_project_id, type: :string) config.add(:recaptcha_mock_validator, type: :boolean) + config.add(:recaptcha_request_timeout_in_seconds, type: :integer) config.add(:recaptcha_secret_key, type: :string) config.add(:recaptcha_site_key, type: :string) config.add(:recovery_code_length, type: :integer) From 0dede8b0a9423a3d02496cfe172cd1c461cca1de Mon Sep 17 00:00:00 2001 From: Mitchell Henke Date: Wed, 4 Dec 2024 14:34:48 -0600 Subject: [PATCH 10/14] Update saml_idp gem to add support for AES-GCM encryption algorithms (#11593) changelog: Upcoming Features, SAML, Update saml_idp gem to add support for AES-GCM encryption algorithms --- Gemfile | 4 ++-- Gemfile.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index a78cabedce0..1030f0c677c 100644 --- a/Gemfile +++ b/Gemfile @@ -74,7 +74,7 @@ gem 'rqrcode' gem 'ruby-progressbar' gem 'ruby-saml' gem 'safe_target_blank', '>= 1.0.2' -gem 'saml_idp', github: '18F/saml_idp', tag: '0.23.3-18f' +gem 'saml_idp', github: '18F/saml_idp', tag: '0.23.4-18f' gem 'scrypt' gem 'simple_form', '>= 5.0.2' gem 'stringex', require: false @@ -85,7 +85,7 @@ gem 'valid_email', '>= 0.1.3', github: 'hallelujah/valid_email', ref: '486b860' gem 'view_component', '~> 3.0' gem 'webauthn', '~> 2.5.2' gem 'xmldsig', '~> 0.6' -gem 'xmlenc', '~> 0.7', '>= 0.7.1' +gem 'xmlenc', '0.8.0' gem 'yard', require: false gem 'zlib', require: false diff --git a/Gemfile.lock b/Gemfile.lock index ee3f5b1670c..dda8909ff93 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -36,10 +36,10 @@ GIT GIT remote: https://github.com/18F/saml_idp.git - revision: 752085a6f88cd3ce75ecc7a64afe064a0e4f9e35 - tag: 0.23.3-18f + revision: e5d876cf10ce9b39bba0cc523d06c4dda1af5124 + tag: 0.23.4-18f specs: - saml_idp (0.23.3.pre.18f) + saml_idp (0.23.4.pre.18f) activesupport builder faraday @@ -869,7 +869,7 @@ DEPENDENCIES webauthn (~> 2.5.2) webmock xmldsig (~> 0.6) - xmlenc (~> 0.7, >= 0.7.1) + xmlenc (= 0.8.0) yard zlib zonebie From 48da2e951eb6c6f4e1a809579309ef1ed083d49c Mon Sep 17 00:00:00 2001 From: Alex Bradley Date: Thu, 5 Dec 2024 09:28:54 -0500 Subject: [PATCH 11/14] LG-14813 default users requiring facial match to LN (#11531) * default users requiring facial match to LN changelog: Upcoming Features, IdV Socure, default users requiring facial match to LN * add check for mock vendor * also route to mock for doc auth vendor * add mock to doc auth buckets * changed some vot specs to acr * change doc_auth_vendor to nil if LN is disabled * Update app/controllers/concerns/idv/doc_auth_vendor_concern.rb Co-authored-by: Amir Reavis-Bey * change doc auth vendor bucketing Co-authored-by: Amir Reavis-Bey * add vendor_switching_enabled to tests * remove unused function in doc_auth_vendor_concern --------- Co-authored-by: Amir Reavis-Bey --- .../concerns/idv/doc_auth_vendor_concern.rb | 26 ++++++- app/services/doc_auth_router.rb | 2 + .../idv/document_capture_controller_spec.rb | 21 ++++++ .../hybrid_mobile/entry_controller_spec.rb | 68 +++++++++++++++++++ .../document_capture_controller_spec.rb | 37 ++++++++-- .../document_capture_controller_spec.rb | 37 ++++++++-- 6 files changed, 182 insertions(+), 9 deletions(-) diff --git a/app/controllers/concerns/idv/doc_auth_vendor_concern.rb b/app/controllers/concerns/idv/doc_auth_vendor_concern.rb index 25225c3d6a4..079a828753b 100644 --- a/app/controllers/concerns/idv/doc_auth_vendor_concern.rb +++ b/app/controllers/concerns/idv/doc_auth_vendor_concern.rb @@ -6,8 +6,32 @@ module DocAuthVendorConcern # @returns[String] String identifying the vendor to use for doc auth. def doc_auth_vendor - bucket = ab_test_bucket(:DOC_AUTH_VENDOR) + if resolved_authn_context_result.facial_match? + if doc_auth_vendor_enabled?(Idp::Constants::Vendors::LEXIS_NEXIS) + bucket = :lexis_nexis + elsif doc_auth_vendor_enabled?(Idp::Constants::Vendors::MOCK) + bucket = :mock + else + return nil + end + else + bucket = ab_test_bucket(:DOC_AUTH_VENDOR) + end DocAuthRouter.doc_auth_vendor_for_bucket(bucket) end + + def doc_auth_vendor_enabled?(vendor) + return true if IdentityConfig.store.doc_auth_vendor_default == vendor + return false unless IdentityConfig.store.doc_auth_vendor_switching_enabled + + case vendor + when Idp::Constants::Vendors::SOCURE + IdentityConfig.store.doc_auth_vendor_socure_percent > 0 + when Idp::Constants::Vendors::LEXIS_NEXIS + IdentityConfig.store.doc_auth_vendor_lexis_nexis_percent > 0 + else + false + end + end end end diff --git a/app/services/doc_auth_router.rb b/app/services/doc_auth_router.rb index a4c43f1d75f..c985bf876d9 100644 --- a/app/services/doc_auth_router.rb +++ b/app/services/doc_auth_router.rb @@ -201,6 +201,8 @@ def self.doc_auth_vendor_for_bucket(bucket) Idp::Constants::Vendors::SOCURE when :lexis_nexis Idp::Constants::Vendors::LEXIS_NEXIS + when :mock + Idp::Constants::Vendors::MOCK else # e.g., nil IdentityConfig.store.doc_auth_vendor_default end diff --git a/spec/controllers/idv/document_capture_controller_spec.rb b/spec/controllers/idv/document_capture_controller_spec.rb index f3792613efc..eb98f5c1f73 100644 --- a/spec/controllers/idv/document_capture_controller_spec.rb +++ b/spec/controllers/idv/document_capture_controller_spec.rb @@ -117,6 +117,7 @@ end let(:idv_vendor) { Idp::Constants::Vendors::LEXIS_NEXIS } + let(:vendor_switching_enabled) { true } before do allow(IdentityConfig.store).to receive(:doc_auth_vendor).and_return( @@ -125,6 +126,9 @@ allow(IdentityConfig.store).to receive(:doc_auth_vendor_default).and_return( idv_vendor, ) + allow(IdentityConfig.store).to receive(:doc_auth_vendor_switching_enabled).and_return( + vendor_switching_enabled, + ) end it 'has non-nil presenter' do @@ -142,6 +146,23 @@ end end + context 'socure is the default vendor but facial match is required' do + let(:idv_vendor) { Idp::Constants::Vendors::SOCURE } + let(:vot) { 'Pb' } + + before do + resolved_authn_context = Vot::Parser.new(vector_of_trust: vot).parse + allow(controller).to receive(:resolved_authn_context_result). + and_return(resolved_authn_context) + end + + it 'does not redirect to Socure controller' do + get :show + + expect(response).to_not redirect_to idv_socure_document_capture_url + end + end + it 'renders the show template' do expect(subject).to receive(:render).with( :show, diff --git a/spec/controllers/idv/hybrid_mobile/entry_controller_spec.rb b/spec/controllers/idv/hybrid_mobile/entry_controller_spec.rb index 8d43b38e37f..15d1bb50191 100644 --- a/spec/controllers/idv/hybrid_mobile/entry_controller_spec.rb +++ b/spec/controllers/idv/hybrid_mobile/entry_controller_spec.rb @@ -56,9 +56,29 @@ {} end let(:idv_vendor) { Idp::Constants::Vendors::MOCK } + let(:vendor_switching_enabled) { true } + let(:lexis_nexis_percent) { 100 } + let(:acr_values) do + [ + Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + ].join(' ') + end before do + resolved_authn_context = AuthnContextResolver.new( + user: user, + service_provider: nil, + vtr: nil, + acr_values: acr_values, + ).result allow(controller).to receive(:session).and_return(session) + allow(controller).to receive(:resolved_authn_context_result). + and_return(resolved_authn_context) + allow(IdentityConfig.store).to receive(:doc_auth_vendor_switching_enabled). + and_return(vendor_switching_enabled) + allow(IdentityConfig.store).to receive(:doc_auth_vendor_lexis_nexis_percent). + and_return(lexis_nexis_percent) get :show, params: { 'document-capture-session': session_uuid } end @@ -70,6 +90,46 @@ end end + context 'facial match is required' do + let(:acr_values) do + [ + Saml::Idp::Constants::IAL2_BIO_REQUIRED_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + ].join(' ') + end + + context 'doc auth vendor is socure with facial match required' do + let(:idv_vendor) { Idp::Constants::Vendors::SOCURE } + + it 'redirects to the lexis nexis first step' do + expect(response).to redirect_to idv_hybrid_mobile_document_capture_url + end + end + + context 'doc auth vendor is mock with facial match required' do + let(:idv_vendor) { Idp::Constants::Vendors::MOCK } + + it 'redirects to the lexis nexis first step' do + expect(response).to redirect_to idv_hybrid_mobile_document_capture_url + end + end + + context 'lexis nexis is disabled' do + let(:idv_vendor) { nil } + let(:vendor_switching_enabled) { false } + let(:lexis_nexis_percent) { 0 } + + before do + allow(IdentityConfig.store).to receive(:doc_auth_vendor_lexis_nexis_percent). + and_return(lexis_nexis_percent) + end + + it 'causes an 404 error' do + expect(response.status).to eq(404) + end + end + end + context 'doc auth vendor is lexis nexis' do let(:idv_vendor) { Idp::Constants::Vendors::LEXIS_NEXIS } @@ -78,6 +138,14 @@ end end + context 'doc auth vendor is mock' do + let(:idv_vendor) { Idp::Constants::Vendors::MOCK } + + it 'redirects to the first step' do + expect(response).to redirect_to idv_hybrid_mobile_document_capture_url + end + end + context 'but we already had a session' do let!(:different_document_capture_session) do DocumentCaptureSession.create!( diff --git a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb index 6ad142fd008..636e81c430b 100644 --- a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb +++ b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb @@ -4,6 +4,7 @@ include FlowPolicyHelper let(:idv_vendor) { Idp::Constants::Vendors::SOCURE } + let(:vendor_switching_enabled) { true } let(:fake_socure_endpoint) { 'https://fake-socure.test' } let(:user) { create(:user) } let(:stored_result) { nil } @@ -24,6 +25,8 @@ and_return(fake_socure_endpoint) allow(IdentityConfig.store).to receive(:doc_auth_vendor).and_return(idv_vendor) allow(IdentityConfig.store).to receive(:doc_auth_vendor_default).and_return(idv_vendor) + allow(IdentityConfig.store).to receive(:doc_auth_vendor_switching_enabled). + and_return(vendor_switching_enabled) allow(subject).to receive(:stored_result).and_return(stored_result) @@ -66,11 +69,37 @@ end context 'when we try to use this controller but we should be using the LN/mock version' do - let(:idv_vendor) { Idp::Constants::Vendors::LEXIS_NEXIS } + context 'when doc_auth_vendor is Lexis Nexis' do + let(:idv_vendor) { Idp::Constants::Vendors::LEXIS_NEXIS } - it 'redirects to the LN/mock controller' do - get :show - expect(response).to redirect_to idv_hybrid_mobile_document_capture_url + it 'redirects to the LN/mock controller' do + get :show + expect(response).to redirect_to idv_hybrid_mobile_document_capture_url + end + end + + context 'when facial match is required' do + let(:acr_values) do + [ + Saml::Idp::Constants::IAL2_BIO_REQUIRED_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + ].join(' ') + end + before do + resolved_authn_context = AuthnContextResolver.new( + user: user, + service_provider: nil, + vtr: nil, + acr_values: acr_values, + ).result + allow(controller).to receive(:resolved_authn_context_result). + and_return(resolved_authn_context) + end + + it 'redirects to the LN/mock controller' do + get :show + expect(response).to redirect_to idv_hybrid_mobile_document_capture_url + end end end diff --git a/spec/controllers/idv/socure/document_capture_controller_spec.rb b/spec/controllers/idv/socure/document_capture_controller_spec.rb index 76750c9ff44..09b00461415 100644 --- a/spec/controllers/idv/socure/document_capture_controller_spec.rb +++ b/spec/controllers/idv/socure/document_capture_controller_spec.rb @@ -4,6 +4,7 @@ include FlowPolicyHelper let(:idv_vendor) { Idp::Constants::Vendors::SOCURE } + let(:vendor_switching_enabled) { true } let(:fake_socure_endpoint) { 'https://fake-socure.test' } let(:user) { create(:user) } let(:doc_auth_success) { true } @@ -33,6 +34,8 @@ and_return(fake_socure_endpoint) allow(IdentityConfig.store).to receive(:doc_auth_vendor).and_return(idv_vendor) allow(IdentityConfig.store).to receive(:doc_auth_vendor_default).and_return(idv_vendor) + allow(IdentityConfig.store).to receive(:doc_auth_vendor_switching_enabled). + and_return(vendor_switching_enabled) allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user) allow(subject).to receive(:stored_result).and_return(stored_result) @@ -79,11 +82,37 @@ end context 'when we try to use this controller but we should be using the LN/mock version' do - let(:idv_vendor) { Idp::Constants::Vendors::LEXIS_NEXIS } + context 'when doc_auth_vendor is Lexis Nexis' do + let(:idv_vendor) { Idp::Constants::Vendors::LEXIS_NEXIS } - it 'redirects to the LN/mock controller' do - get :show - expect(response).to redirect_to idv_document_capture_url + it 'redirects to the LN/mock controller' do + get :show + expect(response).to redirect_to idv_document_capture_url + end + end + + context 'when facial match is required' do + let(:acr_values) do + [ + Saml::Idp::Constants::IAL2_BIO_REQUIRED_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + ].join(' ') + end + before do + resolved_authn_context = AuthnContextResolver.new( + user: user, + service_provider: nil, + vtr: nil, + acr_values: acr_values, + ).result + allow(controller).to receive(:resolved_authn_context_result). + and_return(resolved_authn_context) + end + + it 'redirects to the LN/mock controller' do + get :show + expect(response).to redirect_to idv_document_capture_url + end end end From 5a76ef8ce84f191c1521e64e3e8863c46801fbfe Mon Sep 17 00:00:00 2001 From: Malick Diarra Date: Thu, 5 Dec 2024 09:33:46 -0500 Subject: [PATCH 12/14] LG-15603: Add local Attribute for Account creation (#11575) * changelog: Upcoming Features, Authentication, Threatmetrix API add local_attribute_1 for user when available * change strong migration back * remove &. for service provider * add new line --- app/controllers/concerns/mfa_setup_concern.rb | 1 + .../account_creation_threat_metrix_job.rb | 4 +++- .../account_creation/device_profiling.rb | 8 ++++++-- ...account_creation_threat_metrix_job_spec.rb | 2 ++ .../account_creation/device_profiling_spec.rb | 2 ++ .../ddp/verification_request_spec.rb | 19 +++++++++++++++++++ 6 files changed, 33 insertions(+), 3 deletions(-) diff --git a/app/controllers/concerns/mfa_setup_concern.rb b/app/controllers/concerns/mfa_setup_concern.rb index 6f673afde94..e3c1e07ad51 100644 --- a/app/controllers/concerns/mfa_setup_concern.rb +++ b/app/controllers/concerns/mfa_setup_concern.rb @@ -97,6 +97,7 @@ def threatmetrix_attrs request_ip: request&.remote_ip, threatmetrix_session_id: session[:threatmetrix_session_id], email: EmailContext.new(current_user).last_sign_in_email_address.email, + uuid_prefix: current_sp&.app_id, } end diff --git a/app/jobs/account_creation_threat_metrix_job.rb b/app/jobs/account_creation_threat_metrix_job.rb index aa5b439e01d..1644cee124e 100644 --- a/app/jobs/account_creation_threat_metrix_job.rb +++ b/app/jobs/account_creation_threat_metrix_job.rb @@ -5,12 +5,14 @@ def perform( user_id: nil, threatmetrix_session_id: nil, request_ip: nil, - email: nil + email: nil, + uuid_prefix: nil ) device_profiling_result = AccountCreation::DeviceProfiling.new.proof( request_ip: request_ip, threatmetrix_session_id: threatmetrix_session_id, user_email: email, + uuid_prefix: uuid_prefix, ) ensure user = User.find_by(id: user_id) diff --git a/app/services/account_creation/device_profiling.rb b/app/services/account_creation/device_profiling.rb index 7cccde45b12..c74fae53642 100644 --- a/app/services/account_creation/device_profiling.rb +++ b/app/services/account_creation/device_profiling.rb @@ -5,15 +5,18 @@ class DeviceProfiling attr_reader :request_ip, :threatmetrix_session_id, :user_email, - :device_profile_result + :device_profile_result, + :uuid_prefix def proof( request_ip:, threatmetrix_session_id:, - user_email: + user_email:, + uuid_prefix: ) @request_ip = request_ip @threatmetrix_session_id = threatmetrix_session_id @user_email = user_email + @uuid_prefix = uuid_prefix @device_profile_result = device_profile end @@ -27,6 +30,7 @@ def device_profile threatmetrix_session_id: threatmetrix_session_id, email: user_email, request_ip: request_ip, + uuid_prefix: uuid_prefix, ) end diff --git a/spec/jobs/account_creation_threat_metrix_job_spec.rb b/spec/jobs/account_creation_threat_metrix_job_spec.rb index cffb09cfda7..091df7d8fda 100644 --- a/spec/jobs/account_creation_threat_metrix_job_spec.rb +++ b/spec/jobs/account_creation_threat_metrix_job_spec.rb @@ -3,6 +3,7 @@ RSpec.describe AccountCreationThreatMetrixJob, type: :job do let(:user) { create(:user, :fully_registered) } let(:request_ip) { Faker::Internet.ip_v4_address } + let(:service_provider) { create(:service_provider) } let(:threatmetrix_session_id) { SecureRandom.uuid } let(:authentication_device_profiling) { :collect_only } let(:lexisnexis_threatmetrix_mock_enabled) { false } @@ -28,6 +29,7 @@ user_id: user.id, threatmetrix_session_id: threatmetrix_session_id, request_ip: request_ip, + uuid_prefix: service_provider.app_id, ) end diff --git a/spec/services/account_creation/device_profiling_spec.rb b/spec/services/account_creation/device_profiling_spec.rb index 3178f51170e..03f3c69bfec 100644 --- a/spec/services/account_creation/device_profiling_spec.rb +++ b/spec/services/account_creation/device_profiling_spec.rb @@ -5,6 +5,7 @@ let(:threatmetrix_proofer_result) do instance_double(Proofing::DdpResult, success?: true, transaction_id: 'ddp-123') end + let(:service_provider) { create(:service_provider) } let(:threatmetrix_proofer) do instance_double( Proofing::LexisNexis::Ddp::Proofer, @@ -24,6 +25,7 @@ request_ip: Faker::Internet.ip_v4_address, threatmetrix_session_id: threatmetrix_session_id, user_email: Faker::Internet.email, + uuid_prefix: service_provider.app_id, ) end diff --git a/spec/services/proofing/lexis_nexis/ddp/verification_request_spec.rb b/spec/services/proofing/lexis_nexis/ddp/verification_request_spec.rb index 199cef5e19a..4e541595f69 100644 --- a/spec/services/proofing/lexis_nexis/ddp/verification_request_spec.rb +++ b/spec/services/proofing/lexis_nexis/ddp/verification_request_spec.rb @@ -81,6 +81,25 @@ expected_json = JSON.parse(LexisNexisFixtures.ddp_authentication_request_json) expect(response_json).to eq(expected_json) end + + context 'with service provider associated with user' do + let(:applicant) do + { + threatmetrix_session_id: 'UNIQUE_SESSION_ID', + email: 'test@example.com', + request_ip: '127.0.0.1', + uuid_prefix: 'SPNUM', + } + end + + it 'returns a properly formed request body' do + response_json = JSON.parse(subject.body) + + base_json = JSON.parse(LexisNexisFixtures.ddp_authentication_request_json) + expected_json = base_json.merge({ 'local_attrib_1' => 'SPNUM' }) + expect(response_json).to eq(expected_json) + end + end end end From 6301c626e4b2c61a0c049a635da0cf0d1356919d Mon Sep 17 00:00:00 2001 From: Mitchell Henke Date: Thu, 5 Dec 2024 10:27:13 -0600 Subject: [PATCH 13/14] Exclude old IAAs from Combined Invoice Supplement Report V2 (#11597) * Exclude old IAAs from Combined Invoice Supplement Report V2 changelog: Internal, Reporting, Exclude old IAAs from Combined Invoice Supplement Report V2 * Add spec --- .../combined_invoice_supplement_report_v2.rb | 4 ++- ...bined_invoice_supplement_report_v2_spec.rb | 28 +++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/app/jobs/reports/combined_invoice_supplement_report_v2.rb b/app/jobs/reports/combined_invoice_supplement_report_v2.rb index 454db21f0b4..6de8bb3fa4b 100644 --- a/app/jobs/reports/combined_invoice_supplement_report_v2.rb +++ b/app/jobs/reports/combined_invoice_supplement_report_v2.rb @@ -7,7 +7,9 @@ class CombinedInvoiceSupplementReportV2 < BaseReport REPORT_NAME = 'combined-invoice-supplement-report-v2' def perform(_date) - csv = build_csv(IaaReportingHelper.iaas, IaaReportingHelper.partner_accounts) + # Exclude IAAs that ended more than 90 days ago + iaas = IaaReportingHelper.iaas.filter { |x| x.end_date > 90.days.ago } + csv = build_csv(iaas, IaaReportingHelper.partner_accounts) save_report(REPORT_NAME, csv, extension: 'csv') end diff --git a/spec/jobs/reports/combined_invoice_supplement_report_v2_spec.rb b/spec/jobs/reports/combined_invoice_supplement_report_v2_spec.rb index e04ce038453..c933253c950 100644 --- a/spec/jobs/reports/combined_invoice_supplement_report_v2_spec.rb +++ b/spec/jobs/reports/combined_invoice_supplement_report_v2_spec.rb @@ -51,8 +51,13 @@ let(:user2) { create(:user, profiles: [profile2]) } let(:profile2) { build(:profile, verified_at: DateTime.new(2018, 6, 1).utc) } + let(:report_date) { inside_iaa1 } - let(:csv) { CSV.parse(report.perform(Time.zone.today), headers: true) } + let(:csv) do + travel_to report_date do + CSV.parse(report.perform(report_date), headers: true) + end + end before do iaa_order1.integrations << build_integration( @@ -140,6 +145,14 @@ expect(row['issuer_unique_users'].to_i).to eq(2) end end + + context 'when IAA ended more than 90 days ago' do + let(:report_date) { iaa1_range.end + 90.days } + + it 'is excluded from the report' do + expect(csv.length).to eq(0) + end + end end context 'with an IAA with two issuers in September 2020' do @@ -206,7 +219,11 @@ let(:user10) { create(:user, profiles: [profile10]) } let(:profile10) { build(:profile, verified_at: DateTime.new(2015, 1, 1).utc) } - let(:csv) { CSV.parse(report.perform(Time.zone.today), headers: true) } + let(:csv) do + travel_to inside_iaa2 do + CSV.parse(report.perform(Time.zone.today), headers: true) + end + end before do iaa_order2.integrations << build_integration( @@ -385,6 +402,7 @@ let(:partner_account3) { create(:partner_account) } let(:iaa3_range) { DateTime.new(2020, 9, 1).utc..DateTime.new(2021, 8, 30).utc } + let(:inside_iaa3) { iaa3_range.begin + 1.day } let(:gtc3) do create( @@ -417,7 +435,11 @@ let(:user12) { create(:user, profiles: [profile12]) } let(:profile12) { build(:profile, verified_at: DateTime.new(2017, 9, 10).utc) } - let(:csv) { CSV.parse(report.perform(Time.zone.today), headers: true) } + let(:csv) do + travel_to inside_iaa3 do + CSV.parse(report.perform(Time.zone.today), headers: true) + end + end before do iaa_order3.integrations << build_integration( From 240606a6e772ebc6e361726a30f4030b510f452d Mon Sep 17 00:00:00 2001 From: eileen-nava <80347702+eileen-nava@users.noreply.github.com> Date: Thu, 5 Dec 2024 11:41:20 -0500 Subject: [PATCH 14/14] LG-12270: Audit ipp mock data and test helpers (#11573) * delete spec scaffolding that appears unused * refactor idp/constants.rb and usage of constants * delete unnecessary code * refactor test setup for clarity * continue auditing and ipp refactoring specs * create usps ipp service helper * Changelog: Internal, In-person proofing, audit and update test mock data and helper functions for ipp * respond to feedback * fix feature tests that broke due to helper method refactoring * fix another broken feature test --- lib/idp/constants.rb | 11 +- .../idv/in_person/address_controller_spec.rb | 39 +++-- .../idv/in_person/state_id_controller_spec.rb | 24 +-- spec/factories/in_person_enrollments.rb | 4 - spec/factories/users.rb | 1 + spec/features/idv/analytics_spec.rb | 2 +- .../idv/doc_auth/address_step_spec.rb | 6 +- .../idv/doc_auth/verify_info_step_spec.rb | 19 ++- spec/features/idv/in_person_spec.rb | 8 +- .../idv/in_person_threatmetrix_spec.rb | 8 +- .../idv/steps/in_person/state_id_spec.rb | 4 +- .../idv/steps/in_person/verify_info_spec.rb | 8 +- .../idv/steps/in_person_opt_in_ipp_spec.rb | 24 ++- spec/fixtures/artifacts/test-1.txt | 1 - spec/fixtures/artifacts/test-2.txt | 1 - spec/fixtures/artifacts/test-3.txt | 1 - .../enrollment_helper_spec.rb | 33 +---- .../usps_in_person_proofing/proofer_spec.rb | 15 +- spec/support/features/doc_auth_helper.rb | 138 ------------------ spec/support/features/idv_step_helper.rb | 24 +-- spec/support/features/in_person_helper.rb | 47 ++---- spec/support/features/verify_step_helper.rb | 19 --- spec/support/geocoder_stubs.rb | 41 ------ spec/support/usps_ipp_service_helper.rb | 40 +++++ 24 files changed, 161 insertions(+), 357 deletions(-) delete mode 100644 spec/fixtures/artifacts/test-1.txt delete mode 100644 spec/fixtures/artifacts/test-2.txt delete mode 100644 spec/fixtures/artifacts/test-3.txt delete mode 100644 spec/support/features/verify_step_helper.rb delete mode 100644 spec/support/geocoder_stubs.rb create mode 100644 spec/support/usps_ipp_service_helper.rb diff --git a/lib/idp/constants.rb b/lib/idp/constants.rb index 0f2d44f86ca..e9b66cca1d7 100644 --- a/lib/idp/constants.rb +++ b/lib/idp/constants.rb @@ -94,7 +94,10 @@ module Vendors AAL2 = 2 AAL3 = 3 + MOCK_IDV_APPLICANT_FULL_STATE = 'Montana' + MOCK_IDV_APPLICANT_FULL_STATE_ID_JURISDICTION = 'North Dakota' MOCK_IDV_APPLICANT_STATE_ID_JURISDICTION = 'ND' + MOCK_IDV_APPLICANT_STATE = 'MT' MOCK_IDV_APPLICANT = { address1: '1 FAKE RD', address2: nil, @@ -107,7 +110,7 @@ module Vendors last_name: 'MCFAKERSON', middle_name: nil, name_suffix: 'JR', - state: 'MT', + state: MOCK_IDV_APPLICANT_STATE, state_id_expiration: '2099-12-31', state_id_issued: '2019-12-31', state_id_jurisdiction: MOCK_IDV_APPLICANT_STATE_ID_JURISDICTION, @@ -138,6 +141,7 @@ module Vendors MOCK_IDV_APPLICANT_WITH_SSN = MOCK_IDV_APPLICANT.merge(ssn: '900-66-1234').freeze + MOCK_IDV_APPLICANT_FULL_IDENTITY_DOC_ADDRESS_STATE = 'Virginia' MOCK_IDV_APPLICANT_STATE_ID_ADDRESS = MOCK_IDV_APPLICANT_WITH_SSN.merge( identity_doc_address1: '123 Way St', identity_doc_address2: '2nd Address Line', @@ -170,10 +174,5 @@ module Vendors MOCK_IDV_APPLICANT_SAME_ADDRESS_AS_ID_WITH_PHONE = MOCK_IDV_APPLICANT_SAME_ADDRESS_AS_ID.merge(phone: '12025551212').freeze - - MOCK_IDV_APPLICANT_FULL_STATE_ID_JURISDICTION = 'North Dakota' - MOCK_IDV_APPLICANT_FULL_STATE = 'Montana' - MOCK_IDV_APPLICANT_FULL_IDENTITY_DOC_ADDRESS_STATE = 'Virginia' - MOCK_IDV_APPLICANT_STATE = 'MT' end end diff --git a/spec/controllers/idv/in_person/address_controller_spec.rb b/spec/controllers/idv/in_person/address_controller_spec.rb index ed079de766a..638175330c0 100644 --- a/spec/controllers/idv/in_person/address_controller_spec.rb +++ b/spec/controllers/idv/in_person/address_controller_spec.rb @@ -5,6 +5,7 @@ include InPersonHelper let(:user) { build(:user) } + let(:pii_from_user) { Idp::Constants::MOCK_IPP_APPLICANT_SAME_ADDRESS_AS_ID_FALSE } before do allow(IdentityConfig.store).to receive(:usps_ipp_transliteration_enabled). @@ -12,7 +13,7 @@ stub_sign_in(user) stub_up_to(:hybrid_handoff, idv_session: subject.idv_session) subject.user_session['idv/in_person'] = { - pii_from_user: Idp::Constants::MOCK_IPP_APPLICANT_SAME_ADDRESS_AS_ID_FALSE.dup, + pii_from_user: pii_from_user, } subject.idv_session.ssn = nil stub_analytics @@ -30,11 +31,22 @@ :before, :set_usps_form_presenter, ) + expect(subject).to have_actions( + :before, + :confirm_in_person_state_id_step_complete, + ) + expect(subject).to have_actions( + :before, + :confirm_in_person_address_step_needed, + ) end context '#confirm_in_person_state_id_step_complete' do - it 'redirects to state id page if not complete' do + before do subject.user_session['idv/in_person'][:pii_from_user].delete(:identity_doc_address1) + end + + it 'redirects to state id page if not complete' do get :show expect(response).to redirect_to idv_in_person_state_id_url @@ -42,8 +54,10 @@ end context '#confirm_in_person_address_step_needed' do + before do + request.env['HTTP_REFERER'] = idv_in_person_verify_info_url + end it 'remains on page when referer is verify info' do - subject.request = idv_in_person_verify_info_url get :show expect(response).to render_template :show @@ -68,12 +82,15 @@ expect(response).to render_template :show end - it 'redirects to ssn page when address1 present' do - subject.user_session['idv/in_person'][:pii_from_user][:address1] = '123 Main St' - - get :show + context 'when address1 present' do + before do + subject.user_session['idv/in_person'][:pii_from_user][:address1] = '123 Main St' + end + it 'redirects to ssn page' do + get :show - expect(response).to redirect_to idv_in_person_ssn_url + expect(response).to redirect_to idv_in_person_ssn_url + end end it 'logs idv_in_person_proofing_address_visited' do @@ -101,10 +118,10 @@ describe '#update' do context 'valid address details' do - let(:address1) { '1 FAKE RD' } + let(:address1) { Idp::Constants::MOCK_IDV_APPLICANT[:address1] } let(:address2) { 'APT 1B' } - let(:city) { 'GREAT FALLS' } - let(:zipcode) { '59010-4444' } + let(:city) { Idp::Constants::MOCK_IDV_APPLICANT[:city] } + let(:zipcode) { Idp::Constants::MOCK_IDV_APPLICANT[:zipcode] } let(:state) { 'Montana' } let(:params) do { in_person_address: { diff --git a/spec/controllers/idv/in_person/state_id_controller_spec.rb b/spec/controllers/idv/in_person/state_id_controller_spec.rb index 731f64212f9..b24ab440209 100644 --- a/spec/controllers/idv/in_person/state_id_controller_spec.rb +++ b/spec/controllers/idv/in_person/state_id_controller_spec.rb @@ -225,15 +225,21 @@ expect(subject.user_session['idv/in_person'][:pii_from_user]).to_not have_key attr end - make_pii + build_pii_before_state_id_update - # pii includes address attrs on re-visiting state id pg + # since same_address_as_id was initially true, pii includes residential address attrs, + # which are the same as state id address attrs, on re-visiting state id pg expect(subject.user_session['idv/in_person'][:pii_from_user]).to include( - address1:, - address2:, - city:, - state:, - zipcode:, + identity_doc_address1:, + identity_doc_address2:, + identity_doc_city:, + identity_doc_address_state:, + identity_doc_zipcode:, + address1: identity_doc_address1, + address2: identity_doc_address2, + city: identity_doc_city, + state: identity_doc_address_state, + zipcode: identity_doc_zipcode, ) # On Verify, user changes response from "Yes,..." to @@ -285,7 +291,7 @@ expect(subject.user_session['idv/in_person'][:pii_from_user]).to_not have_key attr end - make_pii(same_address_as_id: 'false') + build_pii_before_state_id_update(same_address_as_id: 'false') # On Verify, user changes response from "No,..." to # "Yes, I live at the address on my state-issued ID @@ -322,7 +328,7 @@ end # User picks "No, I live at a different address" on state ID - make_pii(same_address_as_id: 'false') + build_pii_before_state_id_update(same_address_as_id: 'false') # On Verify, user does not changes response "No,..." put :update, params: params diff --git a/spec/factories/in_person_enrollments.rb b/spec/factories/in_person_enrollments.rb index bfb36296ea4..268164e51b2 100644 --- a/spec/factories/in_person_enrollments.rb +++ b/spec/factories/in_person_enrollments.rb @@ -49,9 +49,5 @@ trait :enhanced_ipp do sponsor_id { IdentityConfig.store.usps_eipp_sponsor_id } end - - trait :with_nil_sponsor_id do - sponsor_id { nil } - end end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 8199af755a0..15ed22e6303 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -279,6 +279,7 @@ user: user, ) create(:in_person_enrollment, :passed, user: user, profile: profile) + profile.in_person_verification_pending_at = nil end end diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index b224ab7321e..ce49724a1b1 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -160,7 +160,7 @@ }, biographical_info: { birth_year: 1938, - identity_doc_address_state: 'ND', + identity_doc_address_state: 'MT', state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############', diff --git a/spec/features/idv/doc_auth/address_step_spec.rb b/spec/features/idv/doc_auth/address_step_spec.rb index ed8969a8409..f21a26795db 100644 --- a/spec/features/idv/doc_auth/address_step_spec.rb +++ b/spec/features/idv/doc_auth/address_step_spec.rb @@ -2,7 +2,6 @@ RSpec.feature 'doc auth verify step', :js do include IdvStepHelper - include DocAuthHelper let(:puerto_rico_address1_hint) do "#{t('forms.example')} 150 Calle A Apt 3" @@ -56,7 +55,10 @@ it 'allows the user to enter in a new address' do expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_info')) expect(page).not_to have_content(t('forms.example')) - fill_out_address_form_ok + fill_in 'idv_form_address1', with: '123 Main St' + fill_in 'idv_form_city', with: 'Nowhere' + select 'Virginia', from: 'idv_form_state' + fill_in 'idv_form_zipcode', with: '66044' click_button t('forms.buttons.submit.update') expect(page).to have_current_path(idv_verify_info_path) 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 661c75dc0f9..86960660722 100644 --- a/spec/features/idv/doc_auth/verify_info_step_spec.rb +++ b/spec/features/idv/doc_auth/verify_info_step_spec.rb @@ -7,17 +7,16 @@ let(:fake_analytics) { FakeAnalytics.new } let(:user) { user_with_2fa } - # values from Idp::Constants::MOCK_IDV_APPLICANT let(:fake_pii_details) do { - document_state: 'MT', - document_number: '1111111111111', - document_issued: '2019-12-31', - document_expiration: '2099-12-31', - first_name: 'FAKEY', - last_name: 'MCFAKERSON', - date_of_birth: '1938-10-06', - address: '1 FAKE RD', + document_state: MOCK_IDV_APPLICANT[:state], + document_number: MOCK_IDV_APPLICANT[:state_id_number], + document_issued: MOCK_IDV_APPLICANT[:state_id_issued], + document_expiration: MOCK_IDV_APPLICANT[:state_id_expiration], + first_name: MOCK_IDV_APPLICANT[:first_name], + last_name: MOCK_IDV_APPLICANT[:last_name], + date_of_birth: MOCK_IDV_APPLICANT[:dob], + address: MOCK_IDV_APPLICANT[:address1], } end @@ -247,7 +246,7 @@ context 'AAMVA' do let(:mock_state_id_jurisdiction) do - [Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction]] + [Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_JURISDICTION] end context 'when the user lives in an AAMVA supported state' do diff --git a/spec/features/idv/in_person_spec.rb b/spec/features/idv/in_person_spec.rb index 68bd0646446..c5af6f91e50 100644 --- a/spec/features/idv/in_person_spec.rb +++ b/spec/features/idv/in_person_spec.rb @@ -56,7 +56,8 @@ expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1).twice expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2).twice expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_CITY).twice - expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction], count: 3) + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_JURISDICTION, count: 1) + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT_STATE, count: 2) expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE).twice expect(page).to have_text(DocAuthHelper::GOOD_SSN_MASKED) @@ -428,12 +429,13 @@ expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1) expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2) expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_CITY) - expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction]).twice + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_JURISDICTION).once + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT_STATE).twice expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE) expect(page).to have_text(InPersonHelper::GOOD_ADDRESS1) expect(page).to have_text(InPersonHelper::GOOD_CITY) expect(page).to have_text(InPersonHelper::GOOD_ZIPCODE) - expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:state]) + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT_STATE) expect(page).to have_text(DocAuthHelper::GOOD_SSN_MASKED) complete_verify_step(user) diff --git a/spec/features/idv/in_person_threatmetrix_spec.rb b/spec/features/idv/in_person_threatmetrix_spec.rb index 799acbc5a98..6747cd4cbbc 100644 --- a/spec/features/idv/in_person_threatmetrix_spec.rb +++ b/spec/features/idv/in_person_threatmetrix_spec.rb @@ -99,8 +99,12 @@ def deactivate_profile_update_enrollment(status:) expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2).twice expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_CITY).twice expect(page).to have_text( - Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction], - count: 3, + Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_JURISDICTION, + count: 1, + ) + expect(page).to have_text( + Idp::Constants::MOCK_IDV_APPLICANT_STATE, + count: 2, ) expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE).twice expect(page).to have_text(DocAuthHelper::GOOD_SSN_MASKED) diff --git a/spec/features/idv/steps/in_person/state_id_spec.rb b/spec/features/idv/steps/in_person/state_id_spec.rb index b9ae0d07f62..7572eba495b 100644 --- a/spec/features/idv/steps/in_person/state_id_spec.rb +++ b/spec/features/idv/steps/in_person/state_id_spec.rb @@ -86,7 +86,7 @@ expect(page).to have_field(t('components.memorable_date.year'), with: '1938') expect(page).to have_field( t('in_person_proofing.form.state_id.state_id_jurisdiction'), - with: Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction], + with: Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_JURISDICTION, ) expect(page).to have_field( t('in_person_proofing.form.state_id.state_id_number'), @@ -110,7 +110,7 @@ ) expect(page).to have_field( t('in_person_proofing.form.state_id.identity_doc_address_state'), - with: Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction], + with: Idp::Constants::MOCK_IDV_APPLICANT_STATE, ) expect(page).to have_checked_field( t('in_person_proofing.form.state_id.same_address_as_id_yes'), diff --git a/spec/features/idv/steps/in_person/verify_info_spec.rb b/spec/features/idv/steps/in_person/verify_info_spec.rb index 528e6607973..29cf6a5eb1b 100644 --- a/spec/features/idv/steps/in_person/verify_info_spec.rb +++ b/spec/features/idv/steps/in_person/verify_info_spec.rb @@ -33,8 +33,12 @@ expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2).twice expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_CITY).twice expect(page).to have_text( - Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction], - count: 3, + Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_JURISDICTION, + count: 1, + ) + expect(page).to have_text( + Idp::Constants::MOCK_IDV_APPLICANT_STATE, + count: 2, ) expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE).twice expect(page).to have_text(DocAuthHelper::GOOD_SSN_MASKED) diff --git a/spec/features/idv/steps/in_person_opt_in_ipp_spec.rb b/spec/features/idv/steps/in_person_opt_in_ipp_spec.rb index db8de8f064d..c9068552a2d 100644 --- a/spec/features/idv/steps/in_person_opt_in_ipp_spec.rb +++ b/spec/features/idv/steps/in_person_opt_in_ipp_spec.rb @@ -58,8 +58,12 @@ expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2).twice expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_CITY).twice expect(page).to have_text( - Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction], - count: 3, + Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_JURISDICTION, + count: 1, + ) + expect(page).to have_text( + Idp::Constants::MOCK_IDV_APPLICANT_STATE, + count: 2, ) expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE).twice expect(page).to have_text(DocAuthHelper::GOOD_SSN_MASKED) @@ -175,8 +179,12 @@ expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2).twice expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_CITY).twice expect(page).to have_text( - Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction], - count: 3, + Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_JURISDICTION, + count: 1, + ) + expect(page).to have_text( + Idp::Constants::MOCK_IDV_APPLICANT_STATE, + count: 2, ) expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE).twice expect(page).to have_text(DocAuthHelper::GOOD_SSN_MASKED) @@ -360,8 +368,12 @@ expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2).twice expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_CITY).twice expect(page).to have_text( - Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction], - count: 3, + Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_JURISDICTION, + count: 1, + ) + expect(page).to have_text( + Idp::Constants::MOCK_IDV_APPLICANT_STATE, + count: 2, ) expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE).twice expect(page).to have_text(DocAuthHelper::GOOD_SSN_MASKED) diff --git a/spec/fixtures/artifacts/test-1.txt b/spec/fixtures/artifacts/test-1.txt deleted file mode 100644 index ba62c8ecf82..00000000000 --- a/spec/fixtures/artifacts/test-1.txt +++ /dev/null @@ -1 +0,0 @@ -local-test-1 diff --git a/spec/fixtures/artifacts/test-2.txt b/spec/fixtures/artifacts/test-2.txt deleted file mode 100644 index aa355c3cef3..00000000000 --- a/spec/fixtures/artifacts/test-2.txt +++ /dev/null @@ -1 +0,0 @@ -local-test-2 diff --git a/spec/fixtures/artifacts/test-3.txt b/spec/fixtures/artifacts/test-3.txt deleted file mode 100644 index 6a1440f4109..00000000000 --- a/spec/fixtures/artifacts/test-3.txt +++ /dev/null @@ -1 +0,0 @@ -local-test-3 diff --git a/spec/services/usps_in_person_proofing/enrollment_helper_spec.rb b/spec/services/usps_in_person_proofing/enrollment_helper_spec.rb index e0381815dd5..cb549770d51 100644 --- a/spec/services/usps_in_person_proofing/enrollment_helper_spec.rb +++ b/spec/services/usps_in_person_proofing/enrollment_helper_spec.rb @@ -2,6 +2,7 @@ RSpec.describe UspsInPersonProofing::EnrollmentHelper do include UspsIppHelper + include UspsIppServiceHelper let(:usps_mock_fallback) { false } let(:user) { build(:user) } @@ -43,7 +44,7 @@ end describe '#schedule_in_person_enrollment' do - context 'when the user does not have a establishing in person enrollment' do + context 'when the user does not have an establishing in person enrollment' do let(:user) { double('user', establishing_in_person_enrollment: nil) } it 'returns without error' do @@ -332,9 +333,7 @@ describe '#create_usps_enrollment' do let(:usps_mock_fallback) { true } - let(:enrollment) { create(:in_person_enrollment, :with_service_provider) } let(:usps_eipp_sponsor_id) { '314159265359' } - let(:is_enhanced_ipp) { true } let(:pii) do Pii::Attributes.new_from_hash( Idp::Constants::MOCK_IDV_APPLICANT, @@ -369,6 +368,7 @@ profile: nil, ) end + let(:is_enhanced_ipp) { true } it 'creates an enhanced ipp enrollment' do expect(proofer).to receive(:request_enroll).with(applicant, is_enhanced_ipp) @@ -382,31 +382,4 @@ end end end - - def transliterated_without_change(value) - UspsInPersonProofing::Transliterator::TransliterationResult.new( - changed?: false, - original: value, - transliterated: value, - unsupported_chars: [], - ) - end - - def transliterated(value) - UspsInPersonProofing::Transliterator::TransliterationResult.new( - changed?: true, - original: value, - transliterated: "transliterated_#{value}", - unsupported_chars: [], - ) - end - - def transliterated_with_failure(value) - UspsInPersonProofing::Transliterator::TransliterationResult.new( - changed?: true, - original: value, - transliterated: "transliterated_failed_#{value}", - unsupported_chars: [':'], - ) - end end diff --git a/spec/services/usps_in_person_proofing/proofer_spec.rb b/spec/services/usps_in_person_proofing/proofer_spec.rb index a31e7fe7d88..eae18260f75 100644 --- a/spec/services/usps_in_person_proofing/proofer_spec.rb +++ b/spec/services/usps_in_person_proofing/proofer_spec.rb @@ -1,19 +1,8 @@ require 'rails_helper' -def expect_facility_fields_to_be_present(facility) - expect(facility.address).to be_present - expect(facility.city).to be_present - expect(facility.name).to be_present - expect(facility.saturday_hours).to be_present - expect(facility.state).to be_present - expect(facility.sunday_hours).to be_present - expect(facility.weekday_hours).to be_present - expect(facility.zip_code_4).to be_present - expect(facility.zip_code_5).to be_present -end - RSpec.describe UspsInPersonProofing::Proofer do include UspsIppHelper + include UspsIppServiceHelper let(:subject) { UspsInPersonProofing::Proofer.new } let(:root_url) { 'http://my.root.url' } @@ -297,8 +286,6 @@ def expect_facility_fields_to_be_present(facility) end let(:is_enhanced_ipp) { false } let(:request_url) { "#{root_url}/ivs-ippaas-api/IPPRest/resources/rest/optInIPPApplicant" } - let(:usps_ipp_sponsor_id) { '42' } - let(:ipp_assurance_level) { '1.5' } before do stub_request_token diff --git a/spec/support/features/doc_auth_helper.rb b/spec/support/features/doc_auth_helper.rb index 264cee35488..bf374379bbd 100644 --- a/spec/support/features/doc_auth_helper.rb +++ b/spec/support/features/doc_auth_helper.rb @@ -9,7 +9,6 @@ module DocAuthHelper GOOD_SSN = (Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN[:ssn]).freeze GOOD_SSN_MASKED = '9**-**-***4'.freeze - SAMPLE_TMX_SUMMARY_REASON_CODE = { tmx_summary_reason_code: ['Identity_Negative_History'] }.freeze SSN_THAT_FAILS_RESOLUTION = '123-45-6666'.freeze SSN_THAT_RAISES_EXCEPTION = '000-00-0000'.freeze @@ -42,10 +41,6 @@ def click_send_link click_on t('forms.buttons.send_link') end - def click_upload_from_computer - click_on t('forms.buttons.upload_photos') - end - def complete_doc_auth_steps_before_welcome_step(expect_accessible: false) visit idv_welcome_url unless current_path == idv_welcome_url click_idv_continue if current_path == idv_mail_only_warning_path @@ -130,16 +125,6 @@ def complete_document_capture_step_with_yml(proofing_yml, expected_path: idv_ssn expect(page).to have_current_path(expected_path, wait: 10) end - def complete_doc_auth_steps_before_phone_otp_step(expect_accessible: false, with_selfie: false) - complete_doc_auth_steps_before_verify_step( - expect_accessible: expect_accessible, - with_selfie: with_selfie, - ) - click_idv_continue - expect_page_to_have_no_accessibility_violations(page) if expect_accessible - click_idv_continue - end - def mobile_device Browser.new(mobile_user_agent) end @@ -221,16 +206,6 @@ def complete_proofing_steps(with_selfie: false) click_agree_and_continue end - def mock_general_doc_auth_client_error(method) - DocAuth::Mock::DocAuthMockClient.mock_response!( - method: method, - response: DocAuth::Response.new( - success: false, - errors: { error: I18n.t('doc_auth.errors.general.no_liveness') }, - ), - ) - end - def mock_doc_auth_attention_with_barcode attention_with_barcode_response = instance_double( Faraday::Response, @@ -246,121 +221,12 @@ def mock_doc_auth_attention_with_barcode ) end - def mock_doc_auth_success_face_match_fail - failure_response = instance_double( - Faraday::Response, - status: 200, - body: LexisNexisFixtures.true_id_response_with_face_match_fail, - ) - DocAuth::Mock::DocAuthMockClient.mock_response!( - method: :get_results, - response: DocAuth::LexisNexis::Responses::TrueIdResponse.new( - failure_response, - DocAuth::LexisNexis::Config.new, - true, # liveness_checking_enabled - ), - ) - end - - def mock_doc_auth_failure_face_match_pass - failure_response = instance_double( - Faraday::Response, - status: 200, - body: LexisNexisFixtures.true_id_response_failure_with_face_match_pass, - ) - DocAuth::Mock::DocAuthMockClient.mock_response!( - method: :get_results, - response: DocAuth::LexisNexis::Responses::TrueIdResponse.new( - failure_response, - DocAuth::LexisNexis::Config.new, - true, # liveness_checking_enabled - ), - ) - end - - def mock_doc_auth_fail_face_match_fail - failure_response = instance_double( - Faraday::Response, - status: 200, - body: LexisNexisFixtures.true_id_response_failure_with_face_match_fail, - ) - DocAuth::Mock::DocAuthMockClient.mock_response!( - method: :get_results, - response: DocAuth::LexisNexis::Responses::TrueIdResponse.new( - failure_response, - DocAuth::LexisNexis::Config.new, - true, # liveness_checking_enabled - ), - ) - end - - def mock_doc_auth_pass_and_portrait_match_not_live - failure_response = instance_double( - Faraday::Response, - status: 200, - body: LexisNexisFixtures.true_id_response_success_with_portrait_match_not_live, - ) - DocAuth::Mock::DocAuthMockClient.mock_response!( - method: :get_results, - response: DocAuth::LexisNexis::Responses::TrueIdResponse.new( - failure_response, - DocAuth::LexisNexis::Config.new, - true, # liveness_checking_enabled - ), - ) - end - - def mock_doc_auth_pass_face_match_pass_no_address1 - response = instance_double( - Faraday::Response, - status: 200, - body: LexisNexisFixtures.true_id_response_success_with_liveness, - ) - DocAuth::Mock::DocAuthMockClient.mock_response!( - method: :get_results, - response: DocAuth::LexisNexis::Responses::TrueIdResponse.new( - response, - DocAuth::LexisNexis::Config.new, - true, # liveness_checking_enabled - ), - ) - end - - def mock_doc_auth_trueid_http_non2xx_status(status) - network_error_response = instance_double( - Faraday::Response, - status: status, - body: '{}', - ) - DocAuth::Mock::DocAuthMockClient.mock_response!( - method: :get_results, - response: DocAuth::LexisNexis::Responses::TrueIdResponse.new( - network_error_response, - DocAuth::LexisNexis::Config.new, - ), - ) - end - def verify_phone_otp choose_idv_otp_delivery_method_sms fill_in_code_with_last_phone_otp click_submit_default end - def fill_out_address_form_ok - fill_in 'idv_form_address1', with: '123 Main St' - fill_in 'idv_form_city', with: 'Nowhere' - select 'Virginia', from: 'idv_form_state' - fill_in 'idv_form_zipcode', with: '66044' - end - - def fill_out_address_form_resolution_fail - fill_in 'idv_form_address1', with: '123 Main St' - fill_in 'idv_form_city', with: 'Nowhere' - select 'Virginia', from: 'idv_form_state' - fill_in 'idv_form_zipcode', with: '00000' - end - def fill_out_address_form_fail fill_in 'idv_form_address1', with: '123 Main St' fill_in 'idv_form_city', with: 'Nowhere' @@ -368,10 +234,6 @@ def fill_out_address_form_fail fill_in 'idv_form_zipcode', with: '1' end - def fill_out_doc_auth_phone_form_ok(phone = '415-555-0199') - fill_in :doc_auth_phone, with: phone - end - def complete_all_idv_steps_with(threatmetrix:) allow(IdentityConfig.store).to receive(:otp_delivery_blocklist_maxretry).and_return(300) user = create(:user, :fully_registered) diff --git a/spec/support/features/idv_step_helper.rb b/spec/support/features/idv_step_helper.rb index 5833ef4d25d..c8765ed6100 100644 --- a/spec/support/features/idv_step_helper.rb +++ b/spec/support/features/idv_step_helper.rb @@ -64,10 +64,6 @@ def click_what_to_bring_link click_link t('in_person_proofing.body.barcode.learn_more') end - def visit_sp_from_in_person_ready_to_verify - click_sp_link_in_person_ready_to_verify - end - def sp_friendly_name 'Test SP' end @@ -76,7 +72,7 @@ def link_text t('in_person_proofing.body.barcode.return_to_partner_link', sp_name: sp_friendly_name) end - def click_sp_link_in_person_ready_to_verify + def visit_sp_from_in_person_ready_to_verify expect(page).to have_content(link_text) click_link(link_text) end @@ -107,14 +103,6 @@ def complete_idv_steps_with_gpo_before_confirmation_step(user = user_with_2fa) click_continue end - def complete_idv_steps_before_confirmation_step(address_verification_mechanism = :phone) - if address_verification_mechanism == :phone - complete_idv_steps_with_phone_before_confirmation_step - else - complete_idv_steps_with_gpo_before_confirmation_step - end - end - def complete_idv_steps_before_step(step, user = user_with_2fa) send(:"complete_idv_steps_before_#{step}_step", user) end @@ -146,14 +134,4 @@ def complete_idv_steps_before_ssn(user = user_with_2fa) fill_out_state_id_form_ok(same_address_as_id: true) click_idv_continue end - - private - - def stub_idv_session(**session_attributes) - allow(Idv::Session).to receive(:new).and_wrap_original do |original, kwargs| - result = original.call(**kwargs) - kwargs[:user_session][:idv].merge!(session_attributes) - result - end - end end diff --git a/spec/support/features/in_person_helper.rb b/spec/support/features/in_person_helper.rb index 41fbaef6b9c..81de665e188 100644 --- a/spec/support/features/in_person_helper.rb +++ b/spec/support/features/in_person_helper.rb @@ -47,7 +47,7 @@ def fill_out_state_id_form_ok(same_address_as_id: false, first_name: GOOD_FIRST_ fill_in t('in_person_proofing.form.state_id.address2'), with: GOOD_IDENTITY_DOC_ADDRESS2 fill_in t('in_person_proofing.form.state_id.city'), with: GOOD_IDENTITY_DOC_CITY fill_in t('in_person_proofing.form.state_id.zipcode'), with: GOOD_IDENTITY_DOC_ZIPCODE - select GOOD_STATE_ID_JURISDICTION, + select GOOD_STATE, from: t('in_person_proofing.form.state_id.identity_doc_address_state') if same_address_as_id choose t('in_person_proofing.form.state_id.same_address_as_id_yes') @@ -120,17 +120,6 @@ def complete_prepare_step(_user = nil) click_on t('forms.buttons.continue') end - def complete_state_id_step(_user = nil, same_address_as_id: true, first_name: GOOD_FIRST_NAME) - # Wait for page to load before attempting to fill out form - expect(page).to have_current_path(idv_in_person_state_id_path, wait: 10) - fill_out_state_id_form_ok(same_address_as_id: same_address_as_id, first_name:) - click_idv_continue - unless same_address_as_id - expect(page).to have_current_path(idv_in_person_address_path, wait: 10) - expect_in_person_step_indicator_current_step(t('step_indicator.flows.idv.verify_info')) - end - end - def complete_state_id_controller(_user = nil, same_address_as_id: true, first_name: GOOD_FIRST_NAME) # Wait for page to load before attempting to fill out form @@ -158,14 +147,6 @@ def complete_verify_step(_user = nil) click_idv_submit_default end - def complete_steps_before_state_id_step - sign_in_and_2fa_user - begin_in_person_proofing - complete_prepare_step - complete_location_step - expect(page).to have_current_path(idv_in_person_state_id_path, wait: 10) - end - def complete_steps_before_state_id_controller sign_in_and_2fa_user begin_in_person_proofing @@ -205,11 +186,6 @@ def complete_entire_ipp_flow(user = user_with_2fa, tmx_status = nil, same_addres end def expect_in_person_step_indicator_current_step(text) - expect_in_person_step_indicator - expect_step_indicator_current_step(text) - end - - def expect_in_person_step_indicator # Normally we're only concerned with the "current" step, but since some steps are shared between # flows, we also want to make sure that at least one of the in-person-specific steps exists in # the step indicator. @@ -217,20 +193,29 @@ def expect_in_person_step_indicator '.step-indicator__step', text: t('step_indicator.flows.idv.find_a_post_office'), ) + expect_step_indicator_current_step(text) end - def make_pii(same_address_as_id: 'true') + def build_pii_before_state_id_update(same_address_as_id: 'true') pii_from_user[:same_address_as_id] = same_address_as_id pii_from_user[:identity_doc_address1] = identity_doc_address1 pii_from_user[:identity_doc_address2] = identity_doc_address2 pii_from_user[:identity_doc_city] = identity_doc_city pii_from_user[:identity_doc_address_state] = identity_doc_address_state pii_from_user[:identity_doc_zipcode] = identity_doc_zipcode - pii_from_user[:address1] = address1 - pii_from_user[:address2] = address2 - pii_from_user[:city] = city - pii_from_user[:state] = state - pii_from_user[:zipcode] = zipcode + if same_address_as_id == 'true' + pii_from_user[:address1] = identity_doc_address1 + pii_from_user[:address2] = identity_doc_address2 + pii_from_user[:city] = identity_doc_city + pii_from_user[:state] = identity_doc_address_state + pii_from_user[:zipcode] = identity_doc_zipcode + else + pii_from_user[:address1] = address1 + pii_from_user[:address2] = address2 + pii_from_user[:city] = city + pii_from_user[:state] = state + pii_from_user[:zipcode] = zipcode + end end def mark_in_person_enrollment_passed(user) diff --git a/spec/support/features/verify_step_helper.rb b/spec/support/features/verify_step_helper.rb deleted file mode 100644 index 2f4c7eaf831..00000000000 --- a/spec/support/features/verify_step_helper.rb +++ /dev/null @@ -1,19 +0,0 @@ -module VerifyStepHelper - include InPersonHelper - - def expect_good_state_id_address - expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS1) - expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ADDRESS2) - expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_CITY) - expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:state_id_jurisdiction]) - expect(page).to have_text(InPersonHelper::GOOD_IDENTITY_DOC_ZIPCODE) - end - - def expect_good_address - expect(page).to have_text(InPersonHelper::GOOD_ADDRESS1) - expect(page).to have_content(t('idv.form.address2')) - expect(page).to have_text(InPersonHelper::GOOD_CITY) - expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:state]) - expect(page).to have_text(InPersonHelper::GOOD_ZIPCODE) - end -end diff --git a/spec/support/geocoder_stubs.rb b/spec/support/geocoder_stubs.rb deleted file mode 100644 index 4a5a58728dc..00000000000 --- a/spec/support/geocoder_stubs.rb +++ /dev/null @@ -1,41 +0,0 @@ -Geocoder.configure(ip_lookup: :test) - -Geocoder::Lookup::Test.add_stub( - '1.2.3.4', [ - { - 'city' => 'foo', - 'country' => 'United States', - 'state_code' => '', - }, - ] -) - -Geocoder::Lookup::Test.add_stub( - '159.142.31.80', [ - { - 'city' => 'Arlington', - 'country' => 'United States', - 'state_code' => 'VA', - }, - ] -) - -Geocoder::Lookup::Test.add_stub( - '4.3.2.1', [ - { - 'city' => '', - 'country' => '', - 'state_code' => '', - }, - ] -) - -Geocoder::Lookup::Test.add_stub( - '127.0.0.1', [ - { - 'city' => '', - 'country' => 'United States', - 'state_code' => '', - }, - ] -) diff --git a/spec/support/usps_ipp_service_helper.rb b/spec/support/usps_ipp_service_helper.rb new file mode 100644 index 00000000000..0b5c9cdd7a8 --- /dev/null +++ b/spec/support/usps_ipp_service_helper.rb @@ -0,0 +1,40 @@ +module UspsIppServiceHelper + def expect_facility_fields_to_be_present(facility) + expect(facility.address).to be_present + expect(facility.city).to be_present + expect(facility.name).to be_present + expect(facility.saturday_hours).to be_present + expect(facility.state).to be_present + expect(facility.sunday_hours).to be_present + expect(facility.weekday_hours).to be_present + expect(facility.zip_code_4).to be_present + expect(facility.zip_code_5).to be_present + end + + def transliterated_without_change(value) + UspsInPersonProofing::Transliterator::TransliterationResult.new( + changed?: false, + original: value, + transliterated: value, + unsupported_chars: [], + ) + end + + def transliterated(value) + UspsInPersonProofing::Transliterator::TransliterationResult.new( + changed?: true, + original: value, + transliterated: "transliterated_#{value}", + unsupported_chars: [], + ) + end + + def transliterated_with_failure(value) + UspsInPersonProofing::Transliterator::TransliterationResult.new( + changed?: true, + original: value, + transliterated: "transliterated_failed_#{value}", + unsupported_chars: [':'], + ) + end +end