diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5ee9aa3037a..0ba01a76764 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -105,7 +105,7 @@ install:
specs:
stage: test
- parallel: 7
+ parallel: 11
cache:
- <<: *ruby_cache
- <<: *yarn_cache
@@ -185,6 +185,17 @@ js_tests:
- *yarn_install
- yarn test
+
+pinpoint-check:
+ stage: test
+ cache:
+ - <<: *ruby_cache
+ - <<: *yarn_cache
+ script:
+ - *bundle_install
+ - *yarn_install
+ - make lint_country_dialing_codes
+
coverage:
stage: after_test
cache:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index bed401f13ce..2fb2fc233c9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -55,123 +55,6 @@ changelog categories.
[changelog check script]: https://github.com/18F/identity-idp/blob/main/scripts/changelog_check.rb
-### Style, Readability, and OO
-- Rubocop or Reek offenses are not disabled unless they are false positives.
-If you're not sure, please ask a teammate.
-
-- Related methods in the same class are in descending order of abstraction.
-This is best explained through this video: https://youtu.be/0rsilnpU1DU?t=554
-
-- Compound conditionals are replaced with more readable methods that describe
-the business rule. For example, a conditional like
-`user_session[:personal_key].nil? && current_user.personal_key.present?` could
-be extracted into a method called
-`current_user_has_already_confirmed_their_personal_key?`.
-Another example is explained in this video: https://youtu.be/0rsilnpU1DU?t=40s
-
-- Service Objects should usually only have one public method, usually named
-`call`. This mostly applies to classes that perform a specific task, unlike
-Presenters, View Objects, and Value Objects, for example. Read
-[7 Patterns to Refactor Fat ActiveRecord Models] for a good overview of the
-different types of classes used in Rails.
-
- References:
- - https://medium.com/selleo/essential-rubyonrails-patterns-part-1-service-objects-1af9f9573ca1
- - https://multithreaded.stitchfix.com/blog/2015/06/02/anatomy-of-service-objects-in-rails/
- - https://hackernoon.com/the-3-tenets-of-service-objects-c936b891b3c2
- - http://katafrakt.me//2018/07/04/writing-service-objects/
- - https://pawelurbanek.com/2018/02/12/ruby-on-rails-service-objects-and-testing-in-isolation/
-
-[7 Patterns to Refactor Fat ActiveRecord Models]: https://codeclimate.com/blog/7-ways-to-decompose-fat-activerecord-models/
-
-### RESTful controllers
-
-* Only use CRUD methods in controllers.
-
-* Prefer adding a new controller with one of the CRUD methods over creating a
- custom method in an existing controller. For example, if your app allows a
- user to update their email and their password on two different pages, instead of
- using a single controller with methods called `update_email` and
- `update_password`, create two controllers and name the methods `update`, i.e.
- `EmailsController#update` and `PasswordsController#update`. See
- http://jeromedalbert.com/how-dhh-organizes-his-rails-controllers/ for more about
- this design pattern.
-
-### Lean controllers
-* Keep as much business logic as possible out of controllers.
-
-* Use specialized classes to handle the operations
- * These will be Form Objects for the most part, since
- most of what the app does is process user input via a form submission, or
- clicking a link in email that contains a token.
-
-* Form Object rules:
- - Should have a single public method called `submit` that returns a [FormResponse] object.
- - Should use ActiveModel validations to validate the user input.
- - Should be placed in `app/forms`.
-
-* Examples of Form/Service Objects:
- - [EmailConfirmationTokenValidator]
- - [PasswordForm]
-
-* The basic outline of how a controller interacts with this class is:
-```ruby
-result = Class.new(user).submit(params) # this returns a FormResponse object
-# all the necessary analytics attributes are captured inside the Form Object
-analytics.track_event('Some Event Name', result.to_h)
-
-if result.success?
- handle_success
-else
- handle_failure
-end
-```
-
-* Only make one call to `analytics.track_event` after submitting the form, as
-opposed to one call when handling success and another when handling failure. The
-Form Object, when used properly, will return a FormResponse object that already
-tells us whether the action was successful or not.
-
-### Importance of the controller design for analytics
-
-This design pattern was the result of many iterations, and agreed upon by all
-team members in early 2017. It keeps controllers clean and predictable. Having a
-controller interact with a Form Object or some other specialized class is not a
-new concept. Rails developers have been using them since at least 2012. What
-might seem new is the `FormResponse` object. **The most important reason
-controllers expect an object that responds to `success?` and `to_h` is to define
-an analytics API, or a contract, if you will, between the analytics logs and the
-folks who query them.**
-
-For example, if someone wants to look up all events that have failed, they would
-run this query in Kibana: `properties.event_properties.success:false`. Now let's
-say a developer introduces a new controller that doesn't adhere to our established
-convention, and captures analytics in their own way, without adding `success`
-and `errors` keys, which are expected to be included in all analytics events.
-This means that any failures for this controller won't show up when running the
-query above, and the person running the query might not realize data is missing.
-
-Deviating from the convention also causes confusion. The next developer to join
-the team will not be sure which pattern to use, and might end up picking the
-wrong pattern. As Sandi Metz says:
-
-> For better or for worse, the patterns you establish today will be replicated
-forever. When the code lies you must be alert to programmers believing and then
-propagating that lie.
-
-### Secure controllers
-Rails by default is currently vulnerable to [cache poisoning attacks]
-through modification of the `X-Forwarded-For` and `Host` headers. In
-order to protect against the latter, there are two pieces that must be
-in place. The first one is already taken care of by defining
-`default_url_options` in `ApplicationController` with a `host` value
-that we control.
-
-The other one is up to you when adding or modifying redirects:
-
-- Always use `_url` helpers (as opposed to `_path`) when calling
-`redirect_to` in a controller.
-
### Additional notes on pull requests and code reviews
Please follow our [Code Review][review] guidelines.
@@ -181,27 +64,10 @@ reading.
[review]: https://engineering.18f.gov/code-review/
[thoughts]: http://glen.nu/ramblings/oncodereview.php
-- Prioritize code reviews for the current sprint above your other work
-- Review pull requests for the current sprint within 24 hours of being opened
- Keep pull requests as small as possible, and focused on a single topic
- Once a pull request is good to go, the person who opened it squashes related
commits together, merges it, then deletes the branch.
-### Recommended reading, viewing, and courses
-
-- [Practical Object-Oriented Design in Ruby](http://www.poodr.com/)
-- [99 Bottles of OOP](https://sandimetz.dpdcart.com/)
-- [Sandi Metz blog](https://www.sandimetz.com/blog/)
-- [Sandi Metz talks](https://www.youtube.com/playlist?list=PLFQBiiaZoyrcTBYAGAUjvEUI6TUrp110W)
-- [Learn Clean Code](https://thoughtbot.com/upcase/clean-code)
-- [Ruby Science](https://gumroad.com/l/ruby-science)
-- [Ruby Tapas](https://www.rubytapas.com/)
-- [Master the Object-Oriented Mindset in Ruby and Rails](https://avdi.codes/moom/)
-- [Refactoring Rails](https://www.refactoringrails.io/)
-- [Growing Rails Applications in Practice](https://pragprog.com/book/d-kegrap/growing-rails-applications-in-practice)
-- [The 30-Day Code Quality Challenge](https://www.codequalitychallenge.com/)
-- [SourceMaking](https://sourcemaking.com/)
-
## Public domain
This project is in the public domain within the United States, and
diff --git a/Gemfile b/Gemfile
index 51b974ddc06..1933f5df2fe 100644
--- a/Gemfile
+++ b/Gemfile
@@ -3,7 +3,7 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}.git" }
ruby "~> #{File.read('.ruby-version').strip}"
-gem 'rails', '~> 6.1.6.1'
+gem 'rails', '~> 7.0.0'
gem 'ahoy_matey', '~> 3.0'
gem 'aws-sdk-kms', '~> 1.4'
@@ -26,7 +26,7 @@ gem 'foundation_emails'
gem 'good_job', '~> 2.99.0'
gem 'hashie', '~> 4.1'
gem 'http_accept_language'
-gem 'identity-hostdata', github: '18F/identity-hostdata', tag: 'v3.4.0'
+gem 'identity-hostdata', github: '18F/identity-hostdata', tag: 'v3.4.1'
gem 'identity-logging', github: '18F/identity-logging', tag: 'v0.1.0'
gem 'identity_validations', github: '18F/identity-validations', tag: 'v0.7.2'
gem 'jsbundling-rails', '~> 1.0.0'
@@ -60,6 +60,7 @@ gem 'safe_target_blank', '>= 1.0.2'
gem 'saml_idp', github: '18F/saml_idp', tag: '0.17.0-18f'
gem 'scrypt'
gem 'simple_form', '>= 5.0.2'
+gem 'sprockets-rails'
gem 'stringex', require: false
gem 'strong_migrations', '>= 0.4.2'
gem 'subprocess', require: false
@@ -89,7 +90,7 @@ end
group :development, :test do
gem 'aws-sdk-cloudwatchlogs', require: false
gem 'brakeman', require: false
- gem 'bullet', '>= 6.0.2'
+ gem 'bullet', '~> 7.0'
gem 'capybara-webmock', git: 'https://github.com/hashrocket/capybara-webmock.git', ref: '63d790a0'
gem 'data_uri', require: false
gem 'erb_lint', '~> 0.1.0', require: false
@@ -103,7 +104,7 @@ group :development, :test do
gem 'pry-rails'
gem 'psych'
gem 'puma'
- gem 'rspec-rails', '~> 4.0'
+ gem 'rspec-rails', '6.0.0.rc1'
gem 'rubocop', '~> 1.29.1', require: false
gem 'rubocop-performance', '~> 1.12.0', require: false
gem 'rubocop-rails', '>= 2.5.2', require: false
@@ -117,7 +118,7 @@ group :test do
gem 'simplecov-cobertura'
gem 'simplecov_json_formatter'
gem 'email_spec'
- gem 'factory_bot_rails', '>= 5.2.0'
+ gem 'factory_bot_rails', '>= 6.2.0'
gem 'faker'
gem 'rack_session_access', '>= 0.2.0'
gem 'rack-test', '>= 1.1.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 42b8d6df000..bc44ebe8851 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,10 +1,10 @@
GIT
remote: https://github.com/18F/identity-hostdata.git
- revision: c69cca28c5e9dd35c66c1bfbdb5c2218b560e14b
- tag: v3.4.0
+ revision: 25a7e98919b1eb0d61dbcce314807a412aff62ad
+ tag: v3.4.1
specs:
- identity-hostdata (3.4.0)
- activesupport (~> 6.1)
+ identity-hostdata (3.4.1)
+ activesupport (>= 6.1, < 8)
aws-sdk-s3 (~> 1.8)
GIT
@@ -52,65 +52,71 @@ GIT
GEM
remote: https://rubygems.org/
specs:
- actioncable (6.1.6.1)
- actionpack (= 6.1.6.1)
- activesupport (= 6.1.6.1)
+ actioncable (7.0.3.1)
+ actionpack (= 7.0.3.1)
+ activesupport (= 7.0.3.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
- actionmailbox (6.1.6.1)
- actionpack (= 6.1.6.1)
- activejob (= 6.1.6.1)
- activerecord (= 6.1.6.1)
- activestorage (= 6.1.6.1)
- activesupport (= 6.1.6.1)
+ actionmailbox (7.0.3.1)
+ actionpack (= 7.0.3.1)
+ activejob (= 7.0.3.1)
+ activerecord (= 7.0.3.1)
+ activestorage (= 7.0.3.1)
+ activesupport (= 7.0.3.1)
mail (>= 2.7.1)
- actionmailer (6.1.6.1)
- actionpack (= 6.1.6.1)
- actionview (= 6.1.6.1)
- activejob (= 6.1.6.1)
- activesupport (= 6.1.6.1)
+ net-imap
+ net-pop
+ net-smtp
+ actionmailer (7.0.3.1)
+ actionpack (= 7.0.3.1)
+ actionview (= 7.0.3.1)
+ activejob (= 7.0.3.1)
+ activesupport (= 7.0.3.1)
mail (~> 2.5, >= 2.5.4)
+ net-imap
+ net-pop
+ net-smtp
rails-dom-testing (~> 2.0)
- actionpack (6.1.6.1)
- actionview (= 6.1.6.1)
- activesupport (= 6.1.6.1)
- rack (~> 2.0, >= 2.0.9)
+ actionpack (7.0.3.1)
+ actionview (= 7.0.3.1)
+ activesupport (= 7.0.3.1)
+ rack (~> 2.0, >= 2.2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
- actiontext (6.1.6.1)
- actionpack (= 6.1.6.1)
- activerecord (= 6.1.6.1)
- activestorage (= 6.1.6.1)
- activesupport (= 6.1.6.1)
+ actiontext (7.0.3.1)
+ actionpack (= 7.0.3.1)
+ activerecord (= 7.0.3.1)
+ activestorage (= 7.0.3.1)
+ activesupport (= 7.0.3.1)
+ globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
- actionview (6.1.6.1)
- activesupport (= 6.1.6.1)
+ actionview (7.0.3.1)
+ activesupport (= 7.0.3.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
- activejob (6.1.6.1)
- activesupport (= 6.1.6.1)
+ activejob (7.0.3.1)
+ activesupport (= 7.0.3.1)
globalid (>= 0.3.6)
- activemodel (6.1.6.1)
- activesupport (= 6.1.6.1)
- activerecord (6.1.6.1)
- activemodel (= 6.1.6.1)
- activesupport (= 6.1.6.1)
- activestorage (6.1.6.1)
- actionpack (= 6.1.6.1)
- activejob (= 6.1.6.1)
- activerecord (= 6.1.6.1)
- activesupport (= 6.1.6.1)
+ activemodel (7.0.3.1)
+ activesupport (= 7.0.3.1)
+ activerecord (7.0.3.1)
+ activemodel (= 7.0.3.1)
+ activesupport (= 7.0.3.1)
+ activestorage (7.0.3.1)
+ actionpack (= 7.0.3.1)
+ activejob (= 7.0.3.1)
+ activerecord (= 7.0.3.1)
+ activesupport (= 7.0.3.1)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
- activesupport (6.1.6.1)
+ activesupport (7.0.3.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
- zeitwerk (~> 2.3)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
ahoy_matey (3.3.0)
@@ -122,17 +128,17 @@ GEM
ast (2.4.2)
awrence (1.2.1)
aws-eventstream (1.2.0)
- aws-partitions (1.540.0)
+ aws-partitions (1.543.0)
aws-sdk-cloudwatchlogs (1.49.0)
aws-sdk-core (~> 3, >= 3.122.0)
aws-sigv4 (~> 1.1)
- aws-sdk-core (3.124.0)
+ aws-sdk-core (3.125.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
- aws-sdk-kms (1.52.0)
- aws-sdk-core (~> 3, >= 3.122.0)
+ aws-sdk-kms (1.53.0)
+ aws-sdk-core (~> 3, >= 3.125.0)
aws-sigv4 (~> 1.1)
aws-sdk-pinpoint (1.62.0)
aws-sdk-core (~> 3, >= 3.122.0)
@@ -140,10 +146,10 @@ GEM
aws-sdk-pinpointsmsvoice (1.29.0)
aws-sdk-core (~> 3, >= 3.122.0)
aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.87.0)
- aws-sdk-core (~> 3, >= 3.109.0)
+ aws-sdk-s3 (1.110.0)
+ aws-sdk-core (~> 3, >= 3.125.0)
aws-sdk-kms (~> 1)
- aws-sigv4 (~> 1.1)
+ aws-sigv4 (~> 1.4)
aws-sdk-ses (1.44.0)
aws-sdk-core (~> 3, >= 3.122.0)
aws-sigv4 (~> 1.1)
@@ -185,18 +191,19 @@ GEM
blueprinter (0.25.3)
bootsnap (1.9.3)
msgpack (~> 1.0)
- brakeman (5.2.0)
+ brakeman (5.2.1)
browser (5.3.1)
builder (3.2.4)
- bullet (6.1.5)
+ bullet (7.0.1)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
bundler-audit (0.9.0.1)
bundler (>= 1.2.0, < 3)
thor (~> 1.0)
byebug (11.1.3)
- capybara (3.35.3)
+ capybara (3.36.0)
addressable
+ matrix
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
rack (>= 1.6.0)
@@ -246,7 +253,8 @@ GEM
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
- diff-lcs (1.4.4)
+ diff-lcs (1.5.0)
+ digest (3.1.0)
docile (1.4.0)
dotiw (5.3.2)
activesupport
@@ -269,7 +277,7 @@ GEM
et-orbi (1.2.7)
tzinfo
execjs (2.8.1)
- factory_bot (6.2.0)
+ factory_bot (6.2.1)
activesupport (>= 5.0.0)
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
@@ -345,7 +353,7 @@ GEM
http_accept_language (2.1.1)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
- i18n-tasks (0.9.35)
+ i18n-tasks (0.9.37)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
erubi
@@ -387,6 +395,7 @@ GEM
mail (2.7.1)
mini_mime (>= 0.1.1)
marcel (1.0.2)
+ matrix (0.4.2)
maxminddb (0.1.22)
memory_profiler (0.9.14)
method_source (1.0.0)
@@ -398,8 +407,22 @@ GEM
multipart-post (2.1.1)
multiset (0.5.3)
nenv (0.3.0)
+ net-imap (0.2.3)
+ digest
+ net-protocol
+ strscan
+ net-pop (0.1.1)
+ digest
+ net-protocol
+ timeout
+ net-protocol (0.1.3)
+ timeout
net-sftp (3.0.0)
net-ssh (>= 5.0.0, < 7.0.0)
+ net-smtp (0.3.1)
+ digest
+ net-protocol
+ timeout
net-ssh (6.1.0)
newrelic_rpm (8.8.0)
nio4r (2.5.8)
@@ -468,21 +491,20 @@ GEM
rack_session_access (0.2.0)
builder (>= 2.0.0)
rack (>= 1.0.0)
- rails (6.1.6.1)
- actioncable (= 6.1.6.1)
- actionmailbox (= 6.1.6.1)
- actionmailer (= 6.1.6.1)
- actionpack (= 6.1.6.1)
- actiontext (= 6.1.6.1)
- actionview (= 6.1.6.1)
- activejob (= 6.1.6.1)
- activemodel (= 6.1.6.1)
- activerecord (= 6.1.6.1)
- activestorage (= 6.1.6.1)
- activesupport (= 6.1.6.1)
+ rails (7.0.3.1)
+ actioncable (= 7.0.3.1)
+ actionmailbox (= 7.0.3.1)
+ actionmailer (= 7.0.3.1)
+ actionpack (= 7.0.3.1)
+ actiontext (= 7.0.3.1)
+ actionview (= 7.0.3.1)
+ activejob (= 7.0.3.1)
+ activemodel (= 7.0.3.1)
+ activerecord (= 7.0.3.1)
+ activestorage (= 7.0.3.1)
+ activesupport (= 7.0.3.1)
bundler (>= 1.15.0)
- railties (= 6.1.6.1)
- sprockets-rails (>= 2.0.0)
+ railties (= 7.0.3.1)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@@ -497,15 +519,16 @@ GEM
ruby-graphviz (~> 1.2)
rails-html-sanitizer (1.4.3)
loofah (~> 2.3)
- rails-i18n (6.0.0)
+ rails-i18n (7.0.3)
i18n (>= 0.7, < 2)
- railties (>= 6.0.0, < 7)
- railties (6.1.6.1)
- actionpack (= 6.1.6.1)
- activesupport (= 6.1.6.1)
+ railties (>= 6.0.0, < 8)
+ railties (7.0.3.1)
+ actionpack (= 7.0.3.1)
+ activesupport (= 7.0.3.1)
method_source
rake (>= 12.2)
thor (~> 1.0)
+ zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.0.6)
rb-fsevent (0.10.4)
@@ -533,29 +556,29 @@ GEM
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
- rspec (3.10.0)
- rspec-core (~> 3.10.0)
- rspec-expectations (~> 3.10.0)
- rspec-mocks (~> 3.10.0)
- rspec-core (3.10.1)
- rspec-support (~> 3.10.0)
- rspec-expectations (3.10.1)
+ rspec (3.11.0)
+ rspec-core (~> 3.11.0)
+ rspec-expectations (~> 3.11.0)
+ rspec-mocks (~> 3.11.0)
+ rspec-core (3.11.0)
+ rspec-support (~> 3.11.0)
+ rspec-expectations (3.11.0)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.10.0)
- rspec-mocks (3.10.2)
+ rspec-support (~> 3.11.0)
+ rspec-mocks (3.11.1)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.10.0)
- rspec-rails (4.1.2)
- actionpack (>= 4.2)
- activesupport (>= 4.2)
- railties (>= 4.2)
- rspec-core (~> 3.10)
- rspec-expectations (~> 3.10)
- rspec-mocks (~> 3.10)
- rspec-support (~> 3.10)
+ rspec-support (~> 3.11.0)
+ rspec-rails (6.0.0.rc1)
+ actionpack (>= 6.1)
+ activesupport (>= 6.1)
+ railties (>= 6.1)
+ rspec-core (~> 3.11)
+ rspec-expectations (~> 3.11)
+ rspec-mocks (~> 3.11)
+ rspec-support (~> 3.11)
rspec-retry (0.6.2)
rspec-core (> 3.3)
- rspec-support (3.10.3)
+ rspec-support (3.11.0)
rubocop (1.29.1)
parallel (~> 1.10)
parser (>= 3.1.0.0)
@@ -616,7 +639,7 @@ GEM
simpleidn (0.2.1)
unf (~> 0.1.4)
smart_properties (1.17.0)
- sprockets (4.1.1)
+ sprockets (4.0.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.4.2)
@@ -624,14 +647,16 @@ GEM
activesupport (>= 5.2)
sprockets (>= 3.0.0)
stringex (2.8.5)
- strong_migrations (0.7.9)
- activerecord (>= 5)
+ strong_migrations (0.8.0)
+ activerecord (>= 5.2)
+ strscan (3.0.1)
subprocess (1.5.5)
systemu (2.6.5)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
thor (1.2.1)
thread_safe (0.3.6)
+ timeout (0.3.0)
tpm-key_attestation (0.10.0)
bindata (~> 2.4)
openssl-signature_algorithm (~> 1.0)
@@ -716,7 +741,7 @@ DEPENDENCIES
bootsnap (~> 1.9.0)
brakeman
browser
- bullet (>= 6.0.2)
+ bullet (~> 7.0)
bundler-audit
capybara-selenium (>= 0.0.6)
capybara-webmock!
@@ -728,7 +753,7 @@ DEPENDENCIES
dotiw (>= 4.0.1)
email_spec
erb_lint (~> 0.1.0)
- factory_bot_rails (>= 5.2.0)
+ factory_bot_rails (>= 6.2.0)
faker
faraday
faraday_middleware
@@ -773,7 +798,7 @@ DEPENDENCIES
rack-test (>= 1.1.0)
rack-timeout
rack_session_access (>= 0.2.0)
- rails (~> 6.1.6.1)
+ rails (~> 7.0.0)
rails-controller-testing (>= 1.0.4)
rails-erd (>= 1.6.0)
redacted_struct
@@ -783,7 +808,7 @@ DEPENDENCIES
retries
rotp (~> 6.1)
rqrcode
- rspec-rails (~> 4.0)
+ rspec-rails (= 6.0.0.rc1)
rspec-retry
rubocop (~> 1.29.1)
rubocop-performance (~> 1.12.0)
@@ -798,6 +823,7 @@ DEPENDENCIES
simplecov (~> 0.21.0)
simplecov-cobertura
simplecov_json_formatter
+ sprockets-rails
stringex
strong_migrations (>= 0.4.2)
subprocess
diff --git a/Makefile b/Makefile
index 27bb4dcb9e8..7c451433afc 100644
--- a/Makefile
+++ b/Makefile
@@ -62,8 +62,6 @@ lint: ## Runs all lint tests
make lint_analytics_events
@echo "--- brakeman ---"
bundle exec brakeman
- @echo "--- zeitwerk check ---"
- bin/rails zeitwerk:check
@echo "--- bundler-audit ---"
bundle exec bundler-audit check --update
# JavaScript
diff --git a/app/assets/images/email/README.md b/app/assets/images/email/README.md
new file mode 100644
index 00000000000..9796d3c69ac
--- /dev/null
+++ b/app/assets/images/email/README.md
@@ -0,0 +1,3 @@
+# Email Images
+
+This folder contains images for exclusive use by mailer templates. This includes email-specific imagery, and also variants of existing assets. For example, since [SVG images are not well-supported](https://www.caniemail.com/features/image-svg/) in all email clients, this folder may include rasterized versions of common SVG images.
diff --git a/app/assets/images/email/info.png b/app/assets/images/email/info.png
new file mode 100644
index 00000000000..67da418b193
Binary files /dev/null and b/app/assets/images/email/info.png differ
diff --git a/app/assets/images/email/user-signup-ial2.png b/app/assets/images/email/user-signup-ial2.png
new file mode 100644
index 00000000000..9d8521fc12e
Binary files /dev/null and b/app/assets/images/email/user-signup-ial2.png differ
diff --git a/app/assets/stylesheets/components/_barcode.scss b/app/assets/stylesheets/components/_barcode.scss
new file mode 100644
index 00000000000..e842cb94627
--- /dev/null
+++ b/app/assets/stylesheets/components/_barcode.scss
@@ -0,0 +1,14 @@
+.barcode.barby-barcode {
+ width: auto;
+ table-layout: fixed;
+ border-spacing: 0;
+}
+
+.barcode .barby-cell {
+ width: 2px;
+ height: 96px;
+
+ &.on {
+ background-color: #000;
+ }
+}
diff --git a/app/assets/stylesheets/components/_location-collection-item.scss b/app/assets/stylesheets/components/_location-collection-item.scss
new file mode 100644
index 00000000000..d47b3ed69e9
--- /dev/null
+++ b/app/assets/stylesheets/components/_location-collection-item.scss
@@ -0,0 +1,38 @@
+.location-collection-item {
+ max-width: 64ex;
+ list-style-type: none;
+ padding-left: 0;
+ align-items: flex-start;
+ border-bottom-width: 1px;
+ border-bottom-style: solid;
+ display: flex;
+ margin-bottom: 1rem;
+ margin-top: 1rem;
+ padding-bottom: 1rem;
+ border-color: $border-color;
+}
+
+@media screen {
+ .wrap-name {
+ overflow-wrap: break-word;
+ }
+}
+
+@media screen and (min-width: 320px) {
+ .usa-button-mobile {
+ width: -webkit-fill-available;
+ margin-top: 8px;
+ }
+}
+
+@media screen and (max-width: 480px) {
+ .usa-button-mobile-hidden {
+ display: none;
+ }
+}
+
+@media screen and (min-width: 481px) {
+ .usa-button-desktop-hidden {
+ display: none;
+ }
+}
diff --git a/app/assets/stylesheets/components/all.scss b/app/assets/stylesheets/components/all.scss
index ee295a189d1..09ecaeaebff 100644
--- a/app/assets/stylesheets/components/all.scss
+++ b/app/assets/stylesheets/components/all.scss
@@ -1,5 +1,6 @@
@import 'account-header';
@import 'banner';
+@import 'barcode';
@import 'block-link';
@import 'block-submit-button';
@import 'btn';
@@ -27,3 +28,4 @@
@import 'troubleshooting-options';
@import 'validated-checkbox';
@import 'i18n-dropdown';
+@import 'location-collection-item';
diff --git a/app/assets/stylesheets/email.css.scss b/app/assets/stylesheets/email.css.scss
index f1ae38d61e7..b9e1286126f 100644
--- a/app/assets/stylesheets/email.css.scss
+++ b/app/assets/stylesheets/email.css.scss
@@ -1,5 +1,9 @@
+@import 'required';
@import 'variables/email';
@import 'foundation-emails/scss/foundation-emails';
+@import 'identity-style-guide/dist/assets/scss/packages/required';
+@import 'identity-style-guide/dist/assets/scss/packages/utilities';
+@import './components/barcode';
.gray {
&:active,
@@ -28,6 +32,11 @@
line-height: 10px;
}
+.s16 {
+ font-size: 16px;
+ line-height: 24px;
+}
+
.s30 {
font-size: 30px;
line-height: 30px;
@@ -118,3 +127,37 @@ h4 {
padding-right: $global-gutter-small !important;
}
}
+
+.info-alert {
+ background-color: color('info-lighter');
+ padding: 0 units(0.5);
+
+ td {
+ padding: units(1.5);
+ padding-right: units(1);
+
+ & + td {
+ padding-left: 0;
+ padding-right: units(1.5);
+ }
+ }
+}
+
+.process-list td {
+ padding-bottom: units(4);
+}
+
+.process-list__circle {
+ border-radius: 50%;
+ width: units(3);
+ height: units(2.5);
+ background-color: color($theme-process-list-counter-background-color);
+ border: units($theme-process-list-counter-border-width) solid
+ color($theme-process-list-counter-border-color);
+ color: color('white');
+ font-size: units(2);
+ font-weight: 700;
+ text-align: center;
+ padding-top: units(0.5);
+ margin-right: units(1.5);
+}
diff --git a/app/components/barcode_component.html.erb b/app/components/barcode_component.html.erb
new file mode 100644
index 00000000000..c41cb0b82b7
--- /dev/null
+++ b/app/components/barcode_component.html.erb
@@ -0,0 +1,16 @@
+<%# Beware: This component is used in mailer content, so be mindful of email markup compatibility %>
+<%= content_tag(
+ :div,
+ role: 'figure',
+ 'aria-labelledby': barcode_caption_id,
+ class: css_class,
+ **tag_options,
+ ) do %>
+ <%= barcode_html.html_safe %>
+
+ <% if label.present? %>
+ <%= label %>:
+ <% end %>
+ <%= formatted_data %>
+
+<% end %>
diff --git a/app/components/barcode_component.rb b/app/components/barcode_component.rb
new file mode 100644
index 00000000000..6316d940031
--- /dev/null
+++ b/app/components/barcode_component.rb
@@ -0,0 +1,38 @@
+require 'barby'
+require 'barby/barcode/code_128'
+require 'barby/outputter/html_outputter'
+
+class BarcodeComponent < BaseComponent
+ attr_reader :barcode_data, :label, :label_formatter, :tag_options
+
+ def initialize(barcode_data:, label:, label_formatter: nil, **tag_options)
+ @barcode_data = barcode_data
+ @label = label
+ @label_formatter = label_formatter
+ @tag_options = tag_options
+ end
+
+ def formatted_data
+ formatted_data = barcode_data
+ formatted_data = label_formatter.call(formatted_data) if label_formatter
+ formatted_data
+ end
+
+ def barcode_html
+ html = Barby::Code128.new(barcode_data).to_html(class_name: 'barcode')
+ # The Barby gem doesn't provide much control over rendered output, so we need to manually slice
+ # in accessibility features (label as substitute to illegible inner content).
+ html.gsub(
+ '>',
+ %( aria-label="#{t('components.barcode.table_label')}"> ),
+ )
+ end
+
+ def barcode_caption_id
+ "barcode-caption-#{unique_id}"
+ end
+
+ def css_class
+ [*tag_options[:class], 'display-inline-block margin-0']
+ end
+end
diff --git a/app/controllers/api/verify/password_confirm_controller.rb b/app/controllers/api/verify/password_confirm_controller.rb
index 1c728e0c7d4..3f312595121 100644
--- a/app/controllers/api/verify/password_confirm_controller.rb
+++ b/app/controllers/api/verify/password_confirm_controller.rb
@@ -10,7 +10,7 @@ def create
user = User.find_by(uuid: result.extra[:user_uuid])
add_proofing_component(user)
store_session_last_gpo_code(form.gpo_code)
- save_in_person_enrollment(user, form.profile)
+
render json: {
personal_key: personal_key,
completion_url: completion_url(result, user),
@@ -57,77 +57,8 @@ def completion_url(result, user)
def in_person_enrollment?(user)
return false unless IdentityConfig.store.in_person_proofing_enabled
- # WILLFIX: After LG-6872 and we have enrollment saved, reference enrollment instead.
ProofingComponent.find_by(user: user)&.document_check == Idp::Constants::Vendors::USPS
end
-
- def usps_proofer
- if IdentityConfig.store.usps_mock_fallback
- UspsInPersonProofing::Mock::Proofer.new
- else
- UspsInPersonProofing::Proofer.new
- end
- end
-
- def create_usps_enrollment(enrollment)
- pii = user_session[:idv][:pii]
- applicant = UspsInPersonProofing::Applicant.new(
- {
- unique_id: enrollment.usps_unique_id,
- first_name: pii.first_name,
- last_name: pii.last_name,
- address: pii.address1,
- # do we need address2?
- city: pii.city,
- state: pii.state,
- zip_code: pii.zipcode,
- email: 'no-reply@login.gov',
- },
- )
- proofer = usps_proofer
-
- response = proofer.request_enroll(applicant)
- response['enrollmentCode']
- end
-
- def save_in_person_enrollment(user, profile)
- return unless in_person_enrollment?(user)
-
- enrollment = InPersonEnrollment.create!(
- profile: profile,
- user: user,
- current_address_matches_id: user_session.dig(:idv, :applicant, :same_address_as_id),
- unique_id: InPersonEnrollment.generate_unique_id,
- selected_location_details: {
- 'name' => 'BALTIMORE — Post Office™',
- 'streetAddress' => '900 E FAYETTE ST RM 118',
- 'city' => 'BALTIMORE',
- 'state' => 'MD',
- 'zip5' => '21233',
- 'zip4' => '9715',
- 'phone' => '555-123-6409',
- 'hours' => [
- {
- 'weekdayHours' => '8:30 AM - 4:30 PM',
- },
- {
- 'saturdayHours' => '9:00 AM - 12:00 PM',
- },
- {
- 'sundayHours' => 'Closed',
- },
- ],
- },
- )
-
- enrollment_code = create_usps_enrollment(enrollment)
- return unless enrollment_code
-
- # update the enrollment to status pending
- enrollment.enrollment_code = enrollment_code
- enrollment.status = :pending
- enrollment.save!
- end
end
end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 825f05a3f0c..ed114631686 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -11,6 +11,7 @@ class ApplicationController < ActionController::Base
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
+ rescue_from ActionController::Redirecting::UnsafeRedirectError, with: :unsafe_redirect_error
rescue_from ActionController::InvalidAuthenticityToken, with: :invalid_auth_token
rescue_from ActionController::UnknownFormat, with: :render_not_found
rescue_from ActionView::MissingTemplate, with: :render_not_acceptable
@@ -267,7 +268,24 @@ def invalid_auth_token(_exception)
user_signed_in: user_signed_in?,
)
flash[:error] = t('errors.general')
- redirect_back fallback_location: new_user_session_url, allow_other_host: false
+ begin
+ redirect_back fallback_location: new_user_session_url, allow_other_host: false
+ rescue ActionController::Redirecting::UnsafeRedirectError => err
+ # Exceptions raised inside exception handlers are not propagated up, so we manually rescue
+ unsafe_redirect_error(err)
+ end
+ end
+
+ def unsafe_redirect_error(_exception)
+ controller_info = "#{controller_path}##{action_name}"
+ analytics.unsafe_redirect_error(
+ controller: controller_info,
+ user_signed_in: user_signed_in?,
+ referer: request.referer,
+ )
+
+ flash[:error] = t('errors.general')
+ redirect_to new_user_session_url
end
def user_fully_authenticated?
diff --git a/app/controllers/concerns/two_factor_authenticatable_methods.rb b/app/controllers/concerns/two_factor_authenticatable_methods.rb
index a645776f312..9c70b576be4 100644
--- a/app/controllers/concerns/two_factor_authenticatable_methods.rb
+++ b/app/controllers/concerns/two_factor_authenticatable_methods.rb
@@ -161,7 +161,7 @@ def track_mfa_method_added
mfa_user = MfaContext.new(current_user)
mfa_count = mfa_user.enabled_mfa_methods_count
analytics.multi_factor_auth_added_phone(enabled_mfa_methods_count: mfa_count)
- Funnel::Registration::AddMfa.call(current_user.id, 'phone')
+ Funnel::Registration::AddMfa.call(current_user.id, 'phone', analytics)
end
def handle_valid_otp_for_authentication_context
diff --git a/app/controllers/idv/capture_doc_status_controller.rb b/app/controllers/idv/capture_doc_status_controller.rb
index f1a24ab502a..2503771a2a2 100644
--- a/app/controllers/idv/capture_doc_status_controller.rb
+++ b/app/controllers/idv/capture_doc_status_controller.rb
@@ -5,12 +5,7 @@ class CaptureDocStatusController < ApplicationController
respond_to :json
def show
- render(
- json: {
- redirect: status == :too_many_requests ? idv_session_errors_throttled_url : nil,
- }.compact,
- status: status,
- )
+ render(json: { redirect: redirect_url }.compact, status: status)
end
private
@@ -23,7 +18,7 @@ def status
:gone
elsif throttled?
:too_many_requests
- elsif confirmed_barcode_attention_result?
+ elsif confirmed_barcode_attention_result? || user_has_establishing_in_person_enrollment?
:ok
elsif session_result.blank? || pending_barcode_attention_confirmation?
:accepted
@@ -35,6 +30,16 @@ def status
end
end
+ def redirect_url
+ return unless flow_session && document_capture_session
+
+ if throttled?
+ idv_session_errors_throttled_url
+ elsif user_has_establishing_in_person_enrollment?
+ idv_in_person_url
+ end
+ end
+
def flow_session
user_session['idv/doc_auth']
end
@@ -55,10 +60,16 @@ def document_capture_session_uuid
end
def throttled?
- Throttle.new(
- user: document_capture_session.user,
- throttle_type: :idv_doc_auth,
- ).throttled?
+ throttle.throttled?
+ end
+
+ def throttle
+ @throttle ||= Throttle.new(user: document_capture_session.user, throttle_type: :idv_doc_auth)
+ end
+
+ def user_has_establishing_in_person_enrollment?
+ return false unless IdentityConfig.store.in_person_proofing_enabled
+ current_user.establishing_in_person_enrollment.present?
end
def confirmed_barcode_attention_result?
diff --git a/app/controllers/idv/doc_auth_controller.rb b/app/controllers/idv/doc_auth_controller.rb
index 45a937fc904..8dc73cea4f1 100644
--- a/app/controllers/idv/doc_auth_controller.rb
+++ b/app/controllers/idv/doc_auth_controller.rb
@@ -3,6 +3,7 @@ class DocAuthController < ApplicationController
before_action :confirm_two_factor_authenticated
before_action :redirect_if_mail_bounced
before_action :redirect_if_pending_profile
+ before_action :redirect_if_pending_in_person_enrollment
before_action :extend_timeout_using_meta_refresh_for_select_paths
include IdvSession # remove if we retire the non docauth LOA3 flow
@@ -36,6 +37,11 @@ def redirect_if_pending_profile
redirect_to idv_gpo_verify_url if current_user.decorate.pending_profile_requires_verification?
end
+ def redirect_if_pending_in_person_enrollment
+ return if !IdentityConfig.store.in_person_proofing_enabled
+ redirect_to idv_in_person_ready_to_verify_url if current_user.pending_in_person_enrollment
+ end
+
def update_if_skipping_upload
return if params[:step] != 'upload' || !flow_session || !flow_session[:skip_upload_step]
track_step_visited
diff --git a/app/controllers/idv/gpo_controller.rb b/app/controllers/idv/gpo_controller.rb
index 384d18e05a6..351833e53fc 100644
--- a/app/controllers/idv/gpo_controller.rb
+++ b/app/controllers/idv/gpo_controller.rb
@@ -78,7 +78,10 @@ def pii(address_pii)
def non_address_pii
pii_to_h.
slice('first_name', 'middle_name', 'last_name', 'dob', 'phone', 'ssn').
- merge(uuid_prefix: ServiceProvider.find_by(issuer: sp_session[:issuer])&.app_id)
+ merge(
+ uuid: current_user.uuid,
+ uuid_prefix: ServiceProvider.find_by(issuer: sp_session[:issuer])&.app_id,
+ )
end
def pii_to_h
diff --git a/app/controllers/idv/gpo_verify_controller.rb b/app/controllers/idv/gpo_verify_controller.rb
index 94fa42750c5..eed5efd069f 100644
--- a/app/controllers/idv/gpo_verify_controller.rb
+++ b/app/controllers/idv/gpo_verify_controller.rb
@@ -9,7 +9,7 @@ def index
analytics.idv_gpo_verification_visited
gpo_mail = Idv::GpoMail.new(current_user)
@mail_spammed = gpo_mail.mail_spammed?
- @gpo_verify_form = GpoVerifyForm.new(user: current_user)
+ @gpo_verify_form = GpoVerifyForm.new(user: current_user, pii: pii)
@code = session[:last_gpo_confirmation_code] if FeatureManagement.reveal_gpo_code?
if throttle.throttled?
@@ -19,6 +19,10 @@ def index
end
end
+ def pii
+ Pii::Cacher.new(current_user, user_session).fetch
+ end
+
def create
@gpo_verify_form = build_gpo_verify_form
@@ -29,15 +33,19 @@ def create
analytics.idv_gpo_verification_submitted(**result.to_h)
if result.success?
- event = create_user_event_with_disavowal(:account_verified)
- UserAlerts::AlertUserAboutAccountVerified.call(
- user: current_user,
- date_time: event.created_at,
- sp_name: decorated_session.sp_name,
- disavowal_token: event.disavowal_token,
- )
- flash[:success] = t('account.index.verification.success')
- redirect_to sign_up_completed_url
+ if result.extra[:pending_in_person_enrollment]
+ redirect_to idv_in_person_ready_to_verify_url
+ else
+ event = create_user_event_with_disavowal(:account_verified)
+ UserAlerts::AlertUserAboutAccountVerified.call(
+ user: current_user,
+ date_time: event.created_at,
+ sp_name: decorated_session.sp_name,
+ disavowal_token: event.disavowal_token,
+ )
+ flash[:success] = t('account.index.verification.success')
+ redirect_to sign_up_completed_url
+ end
else
flash[:error] = @gpo_verify_form.errors.first.message
redirect_to idv_gpo_verify_url
@@ -66,6 +74,7 @@ def render_throttled
def build_gpo_verify_form
GpoVerifyForm.new(
user: current_user,
+ pii: pii,
otp: params_otp,
)
end
diff --git a/app/controllers/idv/in_person/usps_locations_controller.rb b/app/controllers/idv/in_person/usps_locations_controller.rb
new file mode 100644
index 00000000000..c712c64dd9f
--- /dev/null
+++ b/app/controllers/idv/in_person/usps_locations_controller.rb
@@ -0,0 +1,48 @@
+require 'json'
+
+module Idv
+ module InPerson
+ class UspsLocationsController < ApplicationController
+ include UspsInPersonProofing
+ include EffectiveUser
+
+ # get the list of all pilot Post Office locations
+ def index
+ usps_response = []
+ begin
+ usps_response = Proofer.new.request_pilot_facilities
+ rescue Faraday::ConnectionFailed => _error
+ nil
+ end
+
+ render json: usps_response.to_json
+ end
+
+ # save the Post Office location the user selected to an enrollment
+ def update
+ enrollment.update!(selected_location_details: permitted_params.as_json)
+
+ render json: { success: true }, status: :ok
+ end
+
+ protected
+
+ def enrollment
+ UspsInPersonProofing::EnrollmentHelper.
+ establishing_in_person_enrollment_for_user(effective_user)
+ end
+
+ def permitted_params
+ params.require(:usps_location).permit(
+ :formatted_city_state_zip,
+ :name,
+ :phone,
+ :saturday_hours,
+ :street_address,
+ :sunday_hours,
+ :weekday_hours,
+ )
+ end
+ end
+ end
+end
diff --git a/app/controllers/idv/review_controller.rb b/app/controllers/idv/review_controller.rb
index 78fa290f07a..2f1c0e457cb 100644
--- a/app/controllers/idv/review_controller.rb
+++ b/app/controllers/idv/review_controller.rb
@@ -90,7 +90,7 @@ def init_profile
idv_session.cache_encrypted_pii(password)
idv_session.complete_session
- if idv_session.phone_confirmed?
+ if idv_session.profile.active?
event = create_user_event_with_disavowal(:account_verified)
UserAlerts::AlertUserAboutAccountVerified.call(
user: current_user,
diff --git a/app/controllers/idv/session_errors_controller.rb b/app/controllers/idv/session_errors_controller.rb
index b238169f511..4b4d9987347 100644
--- a/app/controllers/idv/session_errors_controller.rb
+++ b/app/controllers/idv/session_errors_controller.rb
@@ -5,6 +5,9 @@ class SessionErrorsController < ApplicationController
before_action :confirm_two_factor_authenticated_or_user_id_in_session
before_action :confirm_idv_session_step_needed
+ before_action :set_try_again_path, only: [:warning, :exception]
+
+ def exception; end
def warning
@remaining_attempts = Throttle.new(
@@ -51,5 +54,13 @@ def confirm_idv_session_step_needed
return unless user_fully_authenticated?
redirect_to idv_phone_url if idv_session.profile_confirmation == true
end
+
+ def set_try_again_path
+ if params[:from]&.starts_with? idv_in_person_path
+ @try_again_path = idv_in_person_path
+ else
+ @try_again_path = idv_doc_auth_path
+ end
+ end
end
end
diff --git a/app/controllers/idv/sessions_controller.rb b/app/controllers/idv/sessions_controller.rb
index 821d62d7566..51d17250806 100644
--- a/app/controllers/idv/sessions_controller.rb
+++ b/app/controllers/idv/sessions_controller.rb
@@ -6,11 +6,13 @@ class SessionsController < ApplicationController
def destroy
cancel_verification_attempt_if_pending_profile
+ cancel_in_person_enrollment_if_exists
analytics.idv_start_over(
step: location_params[:step],
location: location_params[:location],
)
user_session['idv/doc_auth'] = {}
+ user_session['idv/in_person'] = {}
idv_session.clear
Pii::Cacher.new(current_user, user_session).delete
redirect_to idv_url
@@ -19,10 +21,15 @@ def destroy
private
def cancel_verification_attempt_if_pending_profile
- return if current_user.profiles.verification_pending.blank?
+ return if current_user.profiles.gpo_verification_pending.blank?
Idv::CancelVerificationAttempt.new(user: current_user).call
end
+ def cancel_in_person_enrollment_if_exists
+ return if !IdentityConfig.store.in_person_proofing_enabled
+ current_user.pending_in_person_enrollment&.update(status: :cancelled)
+ end
+
def location_params
params.permit(:step, :location).to_h.symbolize_keys
end
diff --git a/app/controllers/openid_connect/authorization_controller.rb b/app/controllers/openid_connect/authorization_controller.rb
index ba2446c1dab..90202919ff2 100644
--- a/app/controllers/openid_connect/authorization_controller.rb
+++ b/app/controllers/openid_connect/authorization_controller.rb
@@ -66,7 +66,7 @@ def ial_context
def handle_successful_handoff
track_events
SpHandoffBounce::AddHandoffTimeToSession.call(sp_session)
- redirect_to @authorize_form.success_redirect_uri
+ redirect_to @authorize_form.success_redirect_uri, allow_other_host: true
delete_branded_experience
end
@@ -113,7 +113,7 @@ def validate_authorize_form
return if result.success?
if (redirect_uri = result.extra[:redirect_uri])
- redirect_to redirect_uri
+ redirect_to redirect_uri, allow_other_host: true
else
render :error
end
diff --git a/app/controllers/openid_connect/logout_controller.rb b/app/controllers/openid_connect/logout_controller.rb
index 0ec7dc359c7..b25132f0d9a 100644
--- a/app/controllers/openid_connect/logout_controller.rb
+++ b/app/controllers/openid_connect/logout_controller.rb
@@ -13,7 +13,12 @@ def index
if result.success? && (redirect_uri = result.extra[:redirect_uri])
sign_out
- redirect_to redirect_uri unless logout_params[:prevent_logout_redirect] == 'true'
+ unless logout_params[:prevent_logout_redirect] == 'true'
+ redirect_to(
+ redirect_uri,
+ allow_other_host: true,
+ )
+ end
else
render :error
end
diff --git a/app/controllers/redirect/redirect_controller.rb b/app/controllers/redirect/redirect_controller.rb
index d51f850a695..ad103661aef 100644
--- a/app/controllers/redirect/redirect_controller.rb
+++ b/app/controllers/redirect/redirect_controller.rb
@@ -15,7 +15,7 @@ def redirect_to_and_log(url, event: nil, tracker_method: analytics.method(:exter
else
tracker_method.call(redirect_url: url, **location_params)
end
- redirect_to(url)
+ redirect_to(url, allow_other_host: true)
end
end
end
diff --git a/app/controllers/redirect/return_to_sp_controller.rb b/app/controllers/redirect/return_to_sp_controller.rb
index 119ec31cbd8..a1a18f0ca9a 100644
--- a/app/controllers/redirect/return_to_sp_controller.rb
+++ b/app/controllers/redirect/return_to_sp_controller.rb
@@ -5,14 +5,14 @@ class ReturnToSpController < Redirect::RedirectController
def cancel
redirect_url = sp_return_url_resolver.return_to_sp_url
analytics.return_to_sp_cancelled(redirect_url: redirect_url, **location_params)
- redirect_to(redirect_url)
+ redirect_to(redirect_url, allow_other_host: true)
end
def failure_to_proof
redirect_url = sp_return_url_resolver.failure_to_proof_url
analytics.return_to_sp_failure_to_proof(redirect_url: redirect_url, **location_params)
- redirect_to(redirect_url)
+ redirect_to(redirect_url, allow_other_host: true)
end
private
diff --git a/app/controllers/sign_out_controller.rb b/app/controllers/sign_out_controller.rb
index 8eb656e5f08..95c2062b1a5 100644
--- a/app/controllers/sign_out_controller.rb
+++ b/app/controllers/sign_out_controller.rb
@@ -6,7 +6,7 @@ def destroy
url_after_cancellation = decorated_session.cancel_link_url
sign_out
flash[:success] = t('devise.sessions.signed_out')
- redirect_to url_after_cancellation
+ redirect_to(url_after_cancellation, allow_other_host: true)
delete_branded_experience
end
end
diff --git a/app/controllers/sign_up/completions_controller.rb b/app/controllers/sign_up/completions_controller.rb
index a48828f9e53..c6e77351c08 100644
--- a/app/controllers/sign_up/completions_controller.rb
+++ b/app/controllers/sign_up/completions_controller.rb
@@ -21,7 +21,10 @@ def update
if decider.go_back_to_mobile_app?
sign_user_out_and_instruct_to_go_back_to_mobile_app
else
- redirect_to(sp_session_request_url_with_updated_params || account_url)
+ redirect_to(
+ sp_session_request_url_with_updated_params || account_url,
+ allow_other_host: true,
+ )
end
end
diff --git a/app/controllers/two_factor_authentication/otp_verification_controller.rb b/app/controllers/two_factor_authentication/otp_verification_controller.rb
index e3996c60f58..f8747294520 100644
--- a/app/controllers/two_factor_authentication/otp_verification_controller.rb
+++ b/app/controllers/two_factor_authentication/otp_verification_controller.rb
@@ -98,6 +98,8 @@ def analytics_properties
phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164),
phone_configuration_id: user_session[:phone_id] ||
current_user.default_phone_configuration&.id,
+ in_multi_mfa_selection_flow: in_multi_mfa_selection_flow?,
+ enabled_mfa_methods_count: mfa_context.enabled_mfa_methods_count,
}
end
end
diff --git a/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb b/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb
index 4ed00032819..4a44fa98e5a 100644
--- a/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb
+++ b/app/controllers/two_factor_authentication/piv_cac_verification_controller.rb
@@ -20,7 +20,7 @@ def redirect_to_piv_cac_service
redirect_to PivCacService.piv_cac_service_link(
nonce: piv_cac_nonce,
redirect_uri: login_two_factor_piv_cac_url,
- )
+ ), allow_other_host: true
end
private
diff --git a/app/controllers/users/additional_mfa_required_controller.rb b/app/controllers/users/additional_mfa_required_controller.rb
index a451a659104..ab79d2998d5 100644
--- a/app/controllers/users/additional_mfa_required_controller.rb
+++ b/app/controllers/users/additional_mfa_required_controller.rb
@@ -19,6 +19,13 @@ def skip
).call
end
analytics.non_restricted_mfa_required_prompt_skipped
+ # should_count as complete as well
+ analytics.user_registration_mfa_setup_complete(
+ mfa_method_counts: mfa_context.enabled_two_factor_configuration_counts_hash,
+ enabled_mfa_methods_count: mfa_context.enabled_mfa_methods_count,
+ pii_like_keypaths: [[:mfa_method_counts, :phone]],
+ success: true,
+ )
redirect_to after_sign_in_path_for(current_user)
end
@@ -28,6 +35,10 @@ def enforcement_date
@enforcement_date ||= IdentityConfig.store.kantara_restriction_enforcement_date
end
+ def mfa_context
+ @mfa_context ||= MfaContext.new(current_user)
+ end
+
def confirm_user_fully_authenticated
unless user_fully_authenticated?
return confirm_two_factor_authenticated(sp_session[:request_id])
diff --git a/app/controllers/users/authorization_confirmation_controller.rb b/app/controllers/users/authorization_confirmation_controller.rb
index 9160b3e641a..7c674a6aa12 100644
--- a/app/controllers/users/authorization_confirmation_controller.rb
+++ b/app/controllers/users/authorization_confirmation_controller.rb
@@ -16,7 +16,7 @@ def new
def create
analytics.authentication_confirmation_continue
- redirect_to sp_session_request_url_with_updated_params
+ redirect_to sp_session_request_url_with_updated_params, allow_other_host: true
end
def destroy
diff --git a/app/controllers/users/backup_code_setup_controller.rb b/app/controllers/users/backup_code_setup_controller.rb
index 8cd48955eba..43e6ee64f4e 100644
--- a/app/controllers/users/backup_code_setup_controller.rb
+++ b/app/controllers/users/backup_code_setup_controller.rb
@@ -29,6 +29,7 @@ def edit; end
def continue
flash[:success] = t('notices.backup_codes_configured')
+ analytics.multi_factor_auth_setup(**analytics_properties)
redirect_to next_setup_path || after_mfa_setup_path
end
@@ -54,7 +55,7 @@ def track_backup_codes_created
analytics.backup_code_created(
enabled_mfa_methods_count: mfa_user.enabled_mfa_methods_count,
)
- Funnel::Registration::AddMfa.call(current_user.id, 'backup_codes')
+ Funnel::Registration::AddMfa.call(current_user.id, 'backup_codes', analytics)
end
def mfa_user
@@ -111,5 +112,14 @@ def authorize_backup_code_disable
return if MfaPolicy.new(current_user).multiple_non_restricted_factors_enabled?
redirect_to account_two_factor_authentication_path
end
+
+ def analytics_properties
+ {
+ success: true,
+ multi_factor_auth_method: 'backup_codes',
+ in_multi_mfa_selection_flow: in_multi_mfa_selection_flow?,
+ enabled_mfa_methods_count: mfa_context.enabled_mfa_methods_count,
+ }
+ end
end
end
diff --git a/app/controllers/users/mfa_selection_controller.rb b/app/controllers/users/mfa_selection_controller.rb
index 5dd11276c2b..f655a934b49 100644
--- a/app/controllers/users/mfa_selection_controller.rb
+++ b/app/controllers/users/mfa_selection_controller.rb
@@ -9,7 +9,7 @@ class MfaSelectionController < ApplicationController
before_action :multiple_factors_enabled?
def index
- @two_factor_options_form = TwoFactorOptionsForm.new(current_user)
+ two_factor_options_form
@after_setup_path = after_mfa_setup_path
@presenter = two_factor_options_presenter
analytics.user_registration_2fa_additional_setup_visit
@@ -37,8 +37,7 @@ def update
private
def submit_form
- @two_factor_options_form = TwoFactorOptionsForm.new(current_user)
- @two_factor_options_form.submit(two_factor_options_form_params)
+ two_factor_options_form.submit(two_factor_options_form_params)
end
def two_factor_options_presenter
@@ -50,6 +49,14 @@ def two_factor_options_presenter
)
end
+ def two_factor_options_form
+ @two_factor_options_form ||= TwoFactorOptionsForm.new(
+ user: current_user,
+ aal3_required: service_provider_mfa_policy.aal3_required?,
+ piv_cac_required: service_provider_mfa_policy.piv_cac_required?,
+ )
+ end
+
def process_valid_form
user_session[:mfa_selections] = @two_factor_options_form.selection
diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb
index df32ed2db1e..ae88634539e 100644
--- a/app/controllers/users/passwords_controller.rb
+++ b/app/controllers/users/passwords_controller.rb
@@ -38,6 +38,7 @@ def user_params
end
def handle_valid_password
+ send_password_reset_risc_event
create_event_and_notify_user_about_password_change
bypass_sign_in current_user
@@ -45,6 +46,11 @@ def handle_valid_password
redirect_to account_url, flash: { info: t('notices.password_changed') }
end
+ def send_password_reset_risc_event
+ event = PushNotification::PasswordResetEvent.new(user: current_user)
+ PushNotification::HttpPush.deliver(event)
+ end
+
def create_event_and_notify_user_about_password_change
event = create_user_event_with_disavowal(:password_changed)
UserAlerts::AlertUserAboutPasswordChange.call(current_user, event.disavowal_token)
diff --git a/app/controllers/users/piv_cac_authentication_setup_controller.rb b/app/controllers/users/piv_cac_authentication_setup_controller.rb
index 8741abefb08..e6b74b7aa17 100644
--- a/app/controllers/users/piv_cac_authentication_setup_controller.rb
+++ b/app/controllers/users/piv_cac_authentication_setup_controller.rb
@@ -44,7 +44,7 @@ def submit_new_piv_cac
if good_nickname
user_session[:piv_cac_nickname] = params[:name]
create_piv_cac_nonce
- redirect_to piv_cac_service_url_with_redirect
+ redirect_to piv_cac_service_url_with_redirect, allow_other_host: true
else
flash[:error] = I18n.t('errors.piv_cac_setup.unique_name')
render_prompt
@@ -84,7 +84,8 @@ def piv_cac_service_url_with_redirect
def process_piv_cac_setup
result = user_piv_cac_form.submit
- analytics.multi_factor_auth_setup(**result.to_h)
+ properties = result.to_h.merge(analytics_properties)
+ analytics.multi_factor_auth_setup(**properties)
if result.success?
process_valid_submission
else
@@ -121,7 +122,7 @@ def track_mfa_method_added
analytics.multi_factor_auth_added_piv_cac(
enabled_mfa_methods_count: mfa_user.enabled_mfa_methods_count,
)
- Funnel::Registration::AddMfa.call(current_user.id, 'piv_cac')
+ Funnel::Registration::AddMfa.call(current_user.id, 'piv_cac', analytics)
end
def piv_cac_enabled?
@@ -150,6 +151,13 @@ def good_nickname
name.present? && !PivCacConfiguration.exists?(user_id: current_user.id, name: name)
end
+ def analytics_properties
+ {
+ in_multi_mfa_selection_flow: in_multi_mfa_selection_flow?,
+ enabled_mfa_methods_count: mfa_context.enabled_mfa_methods_count,
+ }
+ end
+
def cap_piv_cac_count
return unless IdentityConfig.store.max_piv_cac_per_account <= current_cac_count
redirect_to account_two_factor_authentication_path
diff --git a/app/controllers/users/piv_cac_login_controller.rb b/app/controllers/users/piv_cac_login_controller.rb
index 6e8c945881c..2b3a8641d37 100644
--- a/app/controllers/users/piv_cac_login_controller.rb
+++ b/app/controllers/users/piv_cac_login_controller.rb
@@ -17,7 +17,7 @@ def redirect_to_piv_cac_service
redirect_to PivCacService.piv_cac_service_link(
nonce: piv_cac_nonce,
redirect_uri: login_piv_cac_url,
- )
+ ), allow_other_host: true
end
def account_not_found; end
diff --git a/app/controllers/users/piv_cac_setup_from_sign_in_controller.rb b/app/controllers/users/piv_cac_setup_from_sign_in_controller.rb
index 3b8594bfdc3..19d3cb5eebc 100644
--- a/app/controllers/users/piv_cac_setup_from_sign_in_controller.rb
+++ b/app/controllers/users/piv_cac_setup_from_sign_in_controller.rb
@@ -35,7 +35,8 @@ def render_prompt
def process_piv_cac_setup
result = user_piv_cac_form.submit
- analytics.multi_factor_auth_setup(**result.to_h)
+ properties = result.to_h.merge(analytics_properties)
+ analytics.multi_factor_auth_setup(**properties)
if result.success?
process_valid_submission
else
@@ -66,5 +67,12 @@ def process_valid_submission
create_user_event(:piv_cac_enabled)
redirect_to login_add_piv_cac_success_url
end
+
+ def analytics_properties
+ {
+ in_multi_mfa_selection_flow: false,
+ enabled_mfa_methods_count: MfaContext.new(current_user).enabled_mfa_methods_count,
+ }
+ end
end
end
diff --git a/app/controllers/users/reset_passwords_controller.rb b/app/controllers/users/reset_passwords_controller.rb
index 90eaf46b0b3..290c5b96b63 100644
--- a/app/controllers/users/reset_passwords_controller.rb
+++ b/app/controllers/users/reset_passwords_controller.rb
@@ -111,11 +111,17 @@ def build_user
end
def handle_successful_password_reset
+ send_password_reset_risc_event
create_reset_event_and_send_notification
flash[:info] = t('devise.passwords.updated_not_active') if is_flashing_format?
redirect_to new_user_session_url
end
+ def send_password_reset_risc_event
+ event = PushNotification::PasswordResetEvent.new(user: resource)
+ PushNotification::HttpPush.deliver(event)
+ end
+
def handle_unsuccessful_password_reset(result)
reset_password_token_errors = result.errors[:reset_password_token]
if reset_password_token_errors.present?
diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb
index 993d772412b..9806097fdc8 100644
--- a/app/controllers/users/sessions_controller.rb
+++ b/app/controllers/users/sessions_controller.rb
@@ -40,6 +40,11 @@ def create
def destroy
analytics.logout_initiated(sp_initiated: false, oidc: false)
+ irs_attempts_api_tracker.logout_initiated(
+ user_uuid: current_user.uuid,
+ unique_session_id: current_user.unique_session_id,
+ success: true,
+ )
super
end
diff --git a/app/controllers/users/totp_setup_controller.rb b/app/controllers/users/totp_setup_controller.rb
index 7abb664d231..0044f3d698c 100644
--- a/app/controllers/users/totp_setup_controller.rb
+++ b/app/controllers/users/totp_setup_controller.rb
@@ -24,7 +24,8 @@ def new
def confirm
result = totp_setup_form.submit
- analytics.multi_factor_auth_setup(**result.to_h)
+ properties = result.to_h.merge(analytics_properties)
+ analytics.multi_factor_auth_setup(**properties)
if result.success?
process_valid_code
@@ -97,7 +98,7 @@ def create_events
analytics.multi_factor_auth_added_totp(
enabled_mfa_methods_count: mfa_user.enabled_mfa_methods_count,
)
- Funnel::Registration::AddMfa.call(current_user.id, 'auth_app')
+ Funnel::Registration::AddMfa.call(current_user.id, 'auth_app', analytics)
end
def process_successful_disable
@@ -140,5 +141,12 @@ def cap_auth_app_count
def current_auth_app_count
current_user.auth_app_configurations.count
end
+
+ def analytics_properties
+ {
+ in_multi_mfa_selection_flow: in_multi_mfa_selection_flow?,
+ pii_like_keypaths: [[:mfa_method_counts, :phone]],
+ }
+ end
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 32f3587c56f..15f9273c1de 100644
--- a/app/controllers/users/two_factor_authentication_setup_controller.rb
+++ b/app/controllers/users/two_factor_authentication_setup_controller.rb
@@ -8,7 +8,7 @@ class TwoFactorAuthenticationSetupController < ApplicationController
before_action :confirm_user_needs_2fa_setup
def index
- @two_factor_options_form = TwoFactorOptionsForm.new(current_user)
+ two_factor_options_form
@presenter = two_factor_options_presenter
analytics.user_registration_2fa_setup_visit
end
@@ -24,6 +24,7 @@ def create
flash[:phone_error] = t('errors.two_factor_auth_setup.must_select_additional_option')
redirect_to authentication_methods_setup_path(anchor: 'select_phone')
else
+ flash[:error] = t('errors.two_factor_auth_setup.must_select_option')
@presenter = two_factor_options_presenter
render :index
end
@@ -35,8 +36,7 @@ def create
private
def submit_form
- @two_factor_options_form = TwoFactorOptionsForm.new(current_user)
- @two_factor_options_form.submit(two_factor_options_form_params)
+ two_factor_options_form.submit(two_factor_options_form_params)
end
def two_factor_options_presenter
@@ -48,6 +48,14 @@ def two_factor_options_presenter
)
end
+ def two_factor_options_form
+ @two_factor_options_form ||= TwoFactorOptionsForm.new(
+ user: current_user,
+ aal3_required: service_provider_mfa_policy.aal3_required?,
+ piv_cac_required: service_provider_mfa_policy.piv_cac_required?,
+ )
+ end
+
def process_valid_form
user_session[:mfa_selections] = @two_factor_options_form.selection
redirect_to confirmation_path(user_session[:mfa_selections].first)
diff --git a/app/controllers/users/webauthn_setup_controller.rb b/app/controllers/users/webauthn_setup_controller.rb
index 0ad642cda6a..b3771332ea7 100644
--- a/app/controllers/users/webauthn_setup_controller.rb
+++ b/app/controllers/users/webauthn_setup_controller.rb
@@ -39,7 +39,8 @@ def confirm
remember_device_default: remember_device_default,
platform_authenticator: @platform_authenticator,
)
- analytics.multi_factor_auth_setup(**result.to_h)
+ properties = result.to_h.merge(analytics_properties)
+ analytics.multi_factor_auth_setup(**properties)
if result.success?
process_valid_webauthn(form)
else
@@ -135,7 +136,7 @@ def process_valid_webauthn(form)
platform_authenticator: form.platform_authenticator?,
enabled_mfa_methods_count: mfa_user.enabled_mfa_methods_count,
)
- Funnel::Registration::AddMfa.call(current_user.id, 'webauthn')
+ Funnel::Registration::AddMfa.call(current_user.id, 'webauthn', analytics)
mark_user_as_fully_authenticated
handle_remember_device
if form.platform_authenticator?
@@ -147,6 +148,12 @@ def process_valid_webauthn(form)
redirect_to next_setup_path || after_mfa_setup_path
end
+ def analytics_properties
+ {
+ in_multi_mfa_selection_flow: in_multi_mfa_selection_flow?,
+ }
+ end
+
def handle_remember_device
save_user_opted_remember_device_pref
save_remember_device_preference
diff --git a/app/forms/api/profile_creation_form.rb b/app/forms/api/profile_creation_form.rb
index 9a84ceb3dd4..9e2e41b3527 100644
--- a/app/forms/api/profile_creation_form.rb
+++ b/app/forms/api/profile_creation_form.rb
@@ -52,8 +52,29 @@ def cache_encrypted_pii
end
def complete_session
- complete_profile if phone_confirmed?
- create_gpo_entry if user_bundle.gpo_address_verification?
+ associate_in_person_enrollment_with_profile
+
+ if user_bundle.gpo_address_verification?
+ profile.deactivate(:gpo_verification_pending)
+ create_gpo_entry
+ elsif phone_confirmed?
+ if pending_in_person_enrollment?
+ UspsInPersonProofing::EnrollmentHelper.schedule_in_person_enrollment(user, session[:pii])
+ profile.deactivate(:in_person_verification_pending)
+ else
+ complete_profile
+ end
+ end
+ end
+
+ def associate_in_person_enrollment_with_profile
+ return unless pending_in_person_enrollment? && user.establishing_in_person_enrollment
+ user.establishing_in_person_enrollment.update(profile: profile)
+ end
+
+ def pending_in_person_enrollment?
+ return false unless IdentityConfig.store.in_person_proofing_enabled
+ ProofingComponent.find_by(user: user)&.document_check == Idp::Constants::Vendors::USPS
end
def phone_confirmed?
@@ -61,7 +82,7 @@ def phone_confirmed?
end
def complete_profile
- user.pending_profile&.activate
+ profile.activate
move_pii_to_user_session
end
@@ -127,7 +148,7 @@ def form_valid?
def extra_attributes
if user.present?
@extra_attributes ||= {
- profile_pending: user.pending_profile?,
+ profile_pending: user_bundle.gpo_address_verification?,
user_uuid: user.uuid,
}
else
@@ -150,5 +171,10 @@ def public_key
key
end
+
+ def in_person_enrollment?
+ return false unless IdentityConfig.store.in_person_proofing_enabled
+ ProofingComponent.find_by(user: user)&.document_check == Idp::Constants::Vendors::USPS
+ end
end
end
diff --git a/app/forms/gpo_verify_form.rb b/app/forms/gpo_verify_form.rb
index e5ca6a4a894..62b42cdbb02 100644
--- a/app/forms/gpo_verify_form.rb
+++ b/app/forms/gpo_verify_form.rb
@@ -6,18 +6,24 @@ class GpoVerifyForm
validate :validate_otp_not_expired
validate :validate_pending_profile
- attr_accessor :otp, :pii_attributes
+ attr_accessor :otp, :pii, :pii_attributes
attr_reader :user
- def initialize(user:, otp: nil)
+ def initialize(user:, pii:, otp: nil)
@user = user
+ @pii = pii
@otp = otp
end
def submit
result = valid?
if result
- activate_profile
+ if pending_in_person_enrollment?
+ UspsInPersonProofing::EnrollmentHelper.schedule_in_person_enrollment(user, pii)
+ pending_profile&.deactivate(:in_person_verification_pending)
+ else
+ activate_profile
+ end
else
reset_sensitive_fields
end
@@ -26,6 +32,7 @@ def submit
errors: errors,
extra: {
pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
+ pending_in_person_enrollment: pending_in_person_enrollment?,
},
)
end
@@ -65,6 +72,11 @@ def reset_sensitive_fields
self.otp = nil
end
+ def pending_in_person_enrollment?
+ return false unless IdentityConfig.store.in_person_proofing_enabled
+ pending_profile.proofing_components&.[]('document_check') == Idp::Constants::Vendors::USPS
+ end
+
def activate_profile
user.pending_profile&.activate
end
diff --git a/app/forms/idv/api_document_verification_status_form.rb b/app/forms/idv/api_document_verification_status_form.rb
index 08f675ccce7..414fa7e673b 100644
--- a/app/forms/idv/api_document_verification_status_form.rb
+++ b/app/forms/idv/api_document_verification_status_form.rb
@@ -18,6 +18,7 @@ def submit
errors: errors,
extra: {
remaining_attempts: remaining_attempts,
+ doc_auth_result: @async_state&.result&.[](:doc_auth_result),
},
)
end
diff --git a/app/forms/idv/inherited_proofing/va/form.rb b/app/forms/idv/inherited_proofing/va/form.rb
new file mode 100644
index 00000000000..712d0b8c978
--- /dev/null
+++ b/app/forms/idv/inherited_proofing/va/form.rb
@@ -0,0 +1,165 @@
+module Idv
+ module InheritedProofing
+ module Va
+ class Form
+ include ActiveModel::Model
+
+ class << self
+ def model_name
+ ActiveModel::Name.new(self, nil, namespaced_model_name)
+ end
+
+ def namespaced_model_name
+ self.to_s.gsub('::', '')
+ end
+
+ # Returns the field names based on the validators we've set up.
+ def field_names
+ @field_names ||= fields.keys
+ end
+
+ def fields
+ @fields ||= {
+ first_name: { required: true },
+ last_name: { required: true },
+ phone: { required: false },
+ birth_date: { required: true },
+ ssn: { required: true },
+ address_street: { required: true },
+ address_street2: { required: false },
+ address_city: { required: false },
+ address_state: { required: false },
+ address_country: { required: false },
+ address_zip: { required: true },
+ }
+ end
+
+ def required_fields
+ @required_fields ||= fields.filter_map do |field_name, options|
+ field_name if options[:required]
+ end
+ end
+
+ def optional_fields
+ @optional_fields ||= fields.filter_map do |field_name, options|
+ field_name unless options[:required]
+ end
+ end
+ end
+
+ private_class_method :namespaced_model_name, :required_fields, :optional_fields
+
+ attr_reader :payload_hash
+
+ validate :validate_field_names
+
+ required_fields.each { |required_field| validates(required_field, presence: true) }
+
+ # This must be performed after our validators are defined.
+ attr_accessor(*self.field_names)
+
+ def initialize(payload_hash:)
+ raise ArgumentError, 'payload_hash is blank?' if payload_hash.blank?
+ raise ArgumentError, 'payload_hash is not a Hash' unless payload_hash.is_a? Hash
+
+ @payload_hash = payload_hash.dup
+
+ populate_field_data
+ end
+
+ def submit
+ validate
+
+ FormResponse.new(
+ success: valid?,
+ errors: errors,
+ extra: {
+ },
+ )
+ end
+
+ private
+
+ attr_writer :payload_hash
+
+ # Populates our field data from the payload hash.
+ def populate_field_data
+ payload_field_info.each do |field_name, field_info|
+ # Ignore fields we're not interested in.
+ next unless respond_to? field_name
+
+ value = payload_hash.dig(
+ *[field_info[:namespace],
+ field_info[:field_name]].flatten.compact,
+ )
+ public_send("#{field_name}=", value)
+ end
+ end
+
+ # Validator for field names. All fields (not the presence of data) are required.
+ def validate_field_names
+ self.class.field_names.each do |field_name|
+ next if payload_field_info.key? field_name
+ errors.add(field_name, 'field is missing', type: :missing_required_field)
+ end
+ end
+
+ def payload_field_info
+ @payload_field_info ||= field_name_info_from payload_hash: payload_hash
+ end
+
+ # This method simply navigates the payload hash received and creates qualified
+ # hash key names that can be used to verify/map to our field names in this model.
+ # This can be used to qualify nested hash fields and saves us some headaches
+ # if there are nested field names with the same name:
+ #
+ # given:
+ #
+ # payload_hash = {
+ # first_name: 'first_name',
+ # ...
+ # address: {
+ # street: '',
+ # ...
+ # }
+ # }
+ #
+ # field_name_info_from(payload_hash: payload_hash) #=>
+ #
+ # {
+ # :first_name=>{:field_name=>:first_name, :namespace=>[]},
+ # ...
+ # :address_street=>{:field_name=>:street, :namespace=>[:address]},
+ # ...
+ # }
+ #
+ # The generated, qualified field names expected to map to our model, because we named
+ # them as such.
+ #
+ # :field_name is the actual, unqualified field name found in the payload hash sent.
+ # :namespace is the hash key by which :field_name can be found in the payload hash
+ # if need be.
+ def field_name_info_from(payload_hash:, namespace: [], field_name_info: {})
+ payload_hash.each do |key, value|
+ if value.is_a? Hash
+ field_name_info_from payload_hash: value, namespace: namespace << key,
+ field_name_info: field_name_info
+ namespace.pop
+ next
+ end
+
+ namespace = namespace.dup
+ if namespace.blank?
+ field_name_info[key] = { field_name: key, namespace: namespace }
+ else
+ field_name_info["#{namespace.split.join('_')}_#{key}".to_sym] =
+ { field_name: key, namespace: namespace }
+ end
+ end
+
+ field_name_info
+ end
+ end
+ end
+ end
+end
diff --git a/app/forms/totp_setup_form.rb b/app/forms/totp_setup_form.rb
index fc024a9062e..5974b9fea06 100644
--- a/app/forms/totp_setup_form.rb
+++ b/app/forms/totp_setup_form.rb
@@ -42,14 +42,23 @@ def process_valid_submission
user.save!
end
+ def mfa_context
+ user
+ end
+
def extra_analytics_attributes
{
totp_secret_present: secret.present?,
multi_factor_auth_method: 'totp',
auth_app_configuration_id: @auth_app_config&.id,
+ enabled_mfa_methods_count: mfa_user.enabled_mfa_methods_count,
}
end
+ def mfa_user
+ MfaContext.new(user)
+ end
+
def create_auth_app(user, secret, new_timestamp, name)
@auth_app_config = Db::AuthAppConfiguration.create(user, secret, new_timestamp, name)
end
diff --git a/app/forms/two_factor_options_form.rb b/app/forms/two_factor_options_form.rb
index c47da559bf5..4bc3a3bdbe5 100644
--- a/app/forms/two_factor_options_form.rb
+++ b/app/forms/two_factor_options_form.rb
@@ -1,18 +1,19 @@
class TwoFactorOptionsForm
include ActiveModel::Model
- attr_reader :selection
- attr_reader :configuration_id
+ attr_accessor :selection, :user, :aal3_required, :piv_cac_required
validates :selection, inclusion: { in: %w[phone sms voice auth_app piv_cac
webauthn webauthn_platform
backup_code] }
- validates :selection, length: { minimum: 1 }, if: :has_no_configured_mfa?
+ validates :selection, length: { minimum: 1 }, if: :has_no_mfa_or_in_required_flow?
validates :selection, length: { minimum: 2, message: 'phone' }, if: :phone_validations?
- def initialize(user)
+ def initialize(user:, aal3_required:, piv_cac_required:)
self.user = user
+ self.aal3_required = aal3_required
+ self.piv_cac_required = piv_cac_required
end
def submit(params)
@@ -25,9 +26,6 @@ def submit(params)
private
- attr_accessor :user
- attr_writer :selection
-
def mfa_user
@mfa_user ||= MfaContext.new(user)
end
@@ -35,10 +33,15 @@ def mfa_user
def extra_analytics_attributes
{
selection: selection,
+ selected_mfa_count: selection.count,
enabled_mfa_methods_count: mfa_user.enabled_mfa_methods_count,
}
end
+ def in_aal3_or_piv_cac_required_flow?
+ aal3_required || piv_cac_required
+ end
+
def user_needs_updating?
(%w[voice sms] & selection).present? &&
!selection.include?(user.otp_delivery_preference)
@@ -58,6 +61,10 @@ def has_no_configured_mfa?
mfa_user.enabled_mfa_methods_count == 0
end
+ def has_no_mfa_or_in_required_flow?
+ has_no_configured_mfa? || in_aal3_or_piv_cac_required_flow?
+ end
+
def kantara_2fa_phone_restricted?
IdentityConfig.store.kantara_2fa_phone_restricted
end
diff --git a/app/javascript/packages/components/index.ts b/app/javascript/packages/components/index.ts
index 5c6b215a486..426b984c260 100644
--- a/app/javascript/packages/components/index.ts
+++ b/app/javascript/packages/components/index.ts
@@ -12,6 +12,8 @@ export { default as IconListItem } from './icon-list/icon-list-item';
export { default as IconListTitle } from './icon-list/icon-list-title';
export { default as Link } from './link';
export { default as PageFooter } from './page-footer';
+export { default as LocationCollection } from './location-collection';
+export { default as LocationCollectionItem } from './location-collection-item';
export { default as PageHeading } from './page-heading';
export { default as ProcessList } from './process-list/process-list';
export { default as ProcessListHeading } from './process-list/process-list-heading';
diff --git a/app/javascript/packages/components/location-collection-item.spec.tsx b/app/javascript/packages/components/location-collection-item.spec.tsx
new file mode 100644
index 00000000000..8aa1431745d
--- /dev/null
+++ b/app/javascript/packages/components/location-collection-item.spec.tsx
@@ -0,0 +1,62 @@
+import { render } from '@testing-library/react';
+import sinon from 'sinon';
+import LocationCollectionItem from './location-collection-item';
+
+describe('LocationCollectionItem', () => {
+ it('renders the component with expected class and children', () => {
+ const onClick = sinon.stub();
+ const { container } = render(
+ ,
+ );
+
+ const wrapper = container.firstElementChild!;
+ expect(wrapper.classList.contains('location-collection-item')).to.be.true();
+ const locationCollectionItem = wrapper.firstElementChild!;
+ expect(locationCollectionItem.classList.contains('usa-collection__body')).to.be.true();
+ const display = locationCollectionItem.firstElementChild!;
+ expect(display.classList.contains('display-flex')).to.be.true();
+ expect(display.classList.contains('flex-justify')).to.be.true();
+ const heading = display.firstElementChild!;
+ expect(heading.classList.contains('usa-collection__heading')).to.be.true();
+ });
+
+ it('renders the component with expected data', () => {
+ const onClick = sinon.stub();
+ const { getByText } = render(
+ ,
+ );
+
+ const addressParent = getByText('123 Test Address').parentElement!;
+ expect(addressParent.textContent).to.contain('test name');
+ expect(addressParent.textContent).to.contain('123 Test Address');
+ expect(addressParent.textContent).to.contain('City, State 12345-1234');
+ const wkDayHours = getByText(
+ 'in_person_proofing.body.location.retail_hours_weekday 9 AM - 5 PM',
+ ).parentElement!;
+ expect(wkDayHours.textContent).to.contain('9 AM - 5 PM');
+ const satHours = getByText('in_person_proofing.body.location.retail_hours_sat 9 AM - 6 PM')
+ .parentElement!;
+ expect(satHours.textContent).to.contain('9 AM - 6 PM');
+ const sunHours = getByText('in_person_proofing.body.location.retail_hours_sun Closed')
+ .parentElement!;
+ expect(sunHours.textContent).to.contain('Closed');
+ });
+});
diff --git a/app/javascript/packages/components/location-collection-item.tsx b/app/javascript/packages/components/location-collection-item.tsx
new file mode 100644
index 00000000000..0d1491b2877
--- /dev/null
+++ b/app/javascript/packages/components/location-collection-item.tsx
@@ -0,0 +1,62 @@
+import { Button } from '@18f/identity-components';
+import { useI18n } from '@18f/identity-react-i18n';
+
+interface LocationCollectionItemProps {
+ formattedCityStateZip: string;
+ handleSelect: (event: React.FormEvent, selection: number) => void;
+ name: string;
+ saturdayHours: string;
+ selectId: number;
+ streetAddress: string;
+ sundayHours: string;
+ weekdayHours: string;
+}
+
+function LocationCollectionItem({
+ formattedCityStateZip,
+ handleSelect,
+ name,
+ saturdayHours,
+ selectId,
+ streetAddress,
+ sundayHours,
+ weekdayHours,
+}: LocationCollectionItemProps) {
+ const { t } = useI18n();
+
+ return (
+
+
+
+
{name}
+ {
+ handleSelect(event, selectId);
+ }}
+ type="submit"
+ >
+ {t('in_person_proofing.body.location.location_button')}
+
+
+
{streetAddress}
+
{formattedCityStateZip}
+
{t('in_person_proofing.body.location.retail_hours_heading')}
+
{`${t('in_person_proofing.body.location.retail_hours_weekday')} ${weekdayHours}`}
+
{`${t('in_person_proofing.body.location.retail_hours_sat')} ${saturdayHours}`}
+
{`${t('in_person_proofing.body.location.retail_hours_sun')} ${sundayHours}`}
+
handleSelect(event, selectId)}
+ type="submit"
+ >
+ {t('in_person_proofing.body.location.location_button')}
+
+
+
+ );
+}
+
+export default LocationCollectionItem;
diff --git a/app/javascript/packages/components/location-collection.spec.tsx b/app/javascript/packages/components/location-collection.spec.tsx
new file mode 100644
index 00000000000..9f3a86d17a2
--- /dev/null
+++ b/app/javascript/packages/components/location-collection.spec.tsx
@@ -0,0 +1,31 @@
+import { render } from '@testing-library/react';
+import LocationCollection from './location-collection';
+
+describe('LocationCollection', () => {
+ it('renders the component with expected class and children', () => {
+ const { getByText } = render(
+
+ LCI
+ ,
+ );
+
+ const child = getByText('LCI');
+ const item = child.parentElement!;
+
+ expect(item.classList.contains('usa-collection')).to.be.true();
+ expect(item.textContent).to.equal('LCI');
+ });
+
+ it('renders the component with custom class', () => {
+ const { getByText } = render(
+
+ LCI
+ ,
+ );
+
+ const child = getByText('LCI');
+ const item = child.parentElement!;
+
+ expect(item.classList.contains('custom-class')).to.be.true();
+ });
+});
diff --git a/app/javascript/packages/components/location-collection.tsx b/app/javascript/packages/components/location-collection.tsx
new file mode 100644
index 00000000000..d5747c2963f
--- /dev/null
+++ b/app/javascript/packages/components/location-collection.tsx
@@ -0,0 +1,14 @@
+import type { ReactNode } from 'react';
+
+interface LocationCollectionProps {
+ className?: string;
+
+ children?: ReactNode;
+}
+
+function LocationCollection({ children, className }: LocationCollectionProps) {
+ const classes = ['usa-collection', className].filter(Boolean).join(' ');
+ return ;
+}
+
+export default LocationCollection;
diff --git a/app/javascript/packages/document-capture-polling/index.js b/app/javascript/packages/document-capture-polling/index.ts
similarity index 69%
rename from app/javascript/packages/document-capture-polling/index.js
rename to app/javascript/packages/document-capture-polling/index.ts
index c4f93e1f1f6..9c12b6a8c94 100644
--- a/app/javascript/packages/document-capture-polling/index.js
+++ b/app/javascript/packages/document-capture-polling/index.ts
@@ -6,49 +6,49 @@ export const MAX_DOC_CAPTURE_POLL_ATTEMPTS = Math.floor(
DOC_CAPTURE_TIMEOUT / DOC_CAPTURE_POLL_INTERVAL,
);
-/**
- * @typedef DocumentCapturePollingElements
- *
- * @prop {HTMLFormElement} form
- * @prop {HTMLAnchorElement} backLink
- */
+interface DocumentCapturePollingElements {
+ form: HTMLFormElement;
-/**
- * @typedef DocumentCapturePollingOptions
- *
- * @prop {string} statusEndpoint
- * @prop {DocumentCapturePollingElements} elements
- * @prop {typeof defaultTrackEvent=} trackEvent
- */
+ backLink: HTMLAnchorElement;
+}
-/**
- * @enum {number}
- */
-const StatusCodes = {
- SUCCESS: 200,
- GONE: 410,
- TOO_MANY_REQUESTS: 429,
-};
+interface DocumentCapturePollingOptions {
+ statusEndpoint: string;
-/**
- * @enum {string}
- */
-const ResultType = {
- SUCCESS: 'SUCCESS',
- CANCELLED: 'CANCELLED',
- THROTTLED: 'THROTTLED',
-};
+ elements: DocumentCapturePollingElements;
+
+ trackEvent?: typeof defaultTrackEvent;
+}
+
+enum StatusCodes {
+ SUCCESS = 200,
+ GONE = 410,
+ TOO_MANY_REQUESTS = 429,
+}
+
+enum ResultType {
+ SUCCESS = 'SUCCESS',
+ CANCELLED = 'CANCELLED',
+ THROTTLED = 'THROTTLED',
+}
/**
* Manages polling requests for document capture hybrid flow.
*/
export class DocumentCapturePolling {
+ elements: DocumentCapturePollingElements;
+
+ statusEndpoint: string;
+
+ trackEvent: typeof defaultTrackEvent;
+
pollAttempts = 0;
- /**
- * @param {DocumentCapturePollingOptions} options
- */
- constructor({ elements, statusEndpoint, trackEvent = defaultTrackEvent }) {
+ constructor({
+ elements,
+ statusEndpoint,
+ trackEvent = defaultTrackEvent,
+ }: DocumentCapturePollingOptions) {
this.elements = elements;
this.statusEndpoint = statusEndpoint;
this.trackEvent = trackEvent;
@@ -62,10 +62,7 @@ export class DocumentCapturePolling {
this.elements.backLink.addEventListener('click', () => this.bindPromptOnNavigate(false));
}
- /**
- * @param {boolean} isVisible
- */
- toggleFormVisible(isVisible) {
+ toggleFormVisible(isVisible: boolean) {
this.elements.form.classList.toggle('display-none', !isVisible);
}
@@ -85,10 +82,7 @@ export class DocumentCapturePolling {
this.toggleFormVisible(true);
}
- /**
- * @param {{ result?: ResultType, redirect?: string }} params
- */
- async onComplete({ result = ResultType.SUCCESS, redirect } = {}) {
+ async onComplete({ result, redirect }: { result: ResultType; redirect?: string }) {
await this.trackEvent('IdV: Link sent capture doc polling complete', {
isCancelled: result === ResultType.CANCELLED,
isThrottled: result === ResultType.THROTTLED,
@@ -112,10 +106,11 @@ export class DocumentCapturePolling {
async poll() {
const response = await window.fetch(this.statusEndpoint);
+ const { redirect } = (await response.json()) as { redirect?: string };
switch (response.status) {
case StatusCodes.SUCCESS:
- this.onComplete();
+ this.onComplete({ result: ResultType.SUCCESS, redirect });
break;
case StatusCodes.GONE:
@@ -123,7 +118,6 @@ export class DocumentCapturePolling {
break;
case StatusCodes.TOO_MANY_REQUESTS: {
- const { redirect } = await response.json();
this.onComplete({ result: ResultType.THROTTLED, redirect });
break;
}
diff --git a/app/javascript/packages/document-capture/components/document-capture-troubleshooting-options.tsx b/app/javascript/packages/document-capture/components/document-capture-troubleshooting-options.tsx
index 3c3d7d6198b..885321ae284 100644
--- a/app/javascript/packages/document-capture/components/document-capture-troubleshooting-options.tsx
+++ b/app/javascript/packages/document-capture/components/document-capture-troubleshooting-options.tsx
@@ -22,6 +22,11 @@ interface DocumentCaptureTroubleshootingOptionsProps {
*/
showDocumentTips?: boolean;
+ /**
+ * Whether to include option to verify in person.
+ */
+ showInPersonOption?: boolean;
+
/**
* If there are any errors (toggles whether or not to show in person proofing option)
*/
@@ -32,6 +37,7 @@ function DocumentCaptureTroubleshootingOptions({
heading,
location = 'document_capture_troubleshooting_options',
showDocumentTips = true,
+ showInPersonOption = true,
hasErrors,
}: DocumentCaptureTroubleshootingOptionsProps) {
const { t } = useI18n();
@@ -71,7 +77,7 @@ function DocumentCaptureTroubleshootingOptions({
].filter(Boolean) as TroubleshootingOption[]
}
/>
- {hasErrors && inPersonURL && (
+ {hasErrors && inPersonURL && showInPersonOption && (
{
+ const response = await fetch(locationUrl).then((res) =>
+ res.json().catch((error) => {
+ throw error;
+ }),
+ );
+ return response;
+};
+
+const formatLocation = (postOffices: PostOffice[]) => {
+ const formattedLocations = [] as FormattedLocation[];
+ postOffices.forEach((po: PostOffice, index) => {
+ const location = {
+ formattedCityStateZip: `${po.city}, ${po.state}, ${po.zip_code_5}-${po.zip_code_4}`,
+ id: index,
+ name: po.name,
+ phone: po.phone,
+ saturdayHours: po.saturday_hours,
+ streetAddress: po.address,
+ sundayHours: po.sunday_hours,
+ weekdayHours: po.weekday_hours,
+ } as FormattedLocation;
+ formattedLocations.push(location);
+ });
+ return formattedLocations;
+};
+
+const snakeCase = (value: string) =>
+ value
+ .split(/(?=[A-Z])/)
+ .join('_')
+ .toLowerCase();
+
+// snake case the keys of the location
+const prepToSend = (location: object) => {
+ const sendObject = {};
+ Object.keys(location).forEach((key) => {
+ sendObject[snakeCase(key)] = location[key];
+ });
+ return sendObject;
+};
+
+function InPersonLocationStep({ onChange }) {
const { t } = useI18n();
+ const [locationData, setLocationData] = useState([] as FormattedLocation[]);
+ const [inProgress, setInProgress] = useState(false);
+ const [autoSubmit, setAutoSubmit] = useState(false);
+ const [isLoadingComplete, setIsLoadingComplete] = useState(false);
+
+ // ref allows us to avoid a memory leak
+ const mountedRef = useRef(false);
+
+ useEffect(() => {
+ mountedRef.current = true;
+ return () => {
+ mountedRef.current = false;
+ };
+ }, []);
+
+ // useCallBack here prevents unnecessary rerenders due to changing function identity
+ const handleLocationSelect = useCallback(
+ async (e: any, id: number) => {
+ onChange({ selectedLocationName: locationData[id].name });
+ if (autoSubmit) {
+ return;
+ }
+ // prevent navigation from continuing
+ e.preventDefault();
+ if (inProgress) {
+ return;
+ }
+ const selected = prepToSend(locationData[id]);
+ const headers = { 'Content-Type': 'application/json' };
+ const meta: HTMLMetaElement | null = document.querySelector('meta[name="csrf-token"]');
+ const csrf = meta?.content;
+ if (csrf) {
+ headers['X-CSRF-Token'] = csrf;
+ }
+ setInProgress(true);
+ await fetch(locationUrl, {
+ method: 'PUT',
+ body: JSON.stringify(selected),
+ headers,
+ })
+ .then(() => {
+ if (!mountedRef.current) {
+ return;
+ }
+ setAutoSubmit(true);
+ setImmediate(() => {
+ // continue with navigation
+ e.target.click();
+ // allow process to be re-triggered in case submission did not work as expected
+ setAutoSubmit(false);
+ });
+ })
+ .finally(() => {
+ if (!mountedRef.current) {
+ return;
+ }
+ setInProgress(false);
+ });
+ },
+ [locationData, inProgress],
+ );
+
+ useEffect(() => {
+ let mounted = true;
+ (async () => {
+ try {
+ const fetchedLocations = await getResponse();
+ if (mounted) {
+ const formattedLocations = formatLocation(fetchedLocations);
+ setLocationData(formattedLocations);
+ }
+ } finally {
+ if (mounted) {
+ setIsLoadingComplete(true);
+ }
+ }
+ })();
+ return () => {
+ mounted = false;
+ };
+ }, []);
+
+ let locationItems: React.ReactNode;
+ if (!isLoadingComplete) {
+ locationItems = ;
+ } else if (locationData.length < 1) {
+ locationItems = {t('in_person_proofing.body.location.none_found')} ;
+ } else {
+ locationItems = locationData.map((item, index) => (
+
+ ));
+ }
return (
<>
{t('in_person_proofing.headings.location')}
-
+
+ {t('in_person_proofing.body.location.location_step_about')}
+ {locationItems}
>
);
}
diff --git a/app/javascript/packages/document-capture/components/in-person-prepare-step.tsx b/app/javascript/packages/document-capture/components/in-person-prepare-step.tsx
index 514e3c6383f..bc8c8a6593f 100644
--- a/app/javascript/packages/document-capture/components/in-person-prepare-step.tsx
+++ b/app/javascript/packages/document-capture/components/in-person-prepare-step.tsx
@@ -11,17 +11,26 @@ import { removeUnloadProtection } from '@18f/identity-url';
import { useContext } from 'react';
import { FlowContext } from '@18f/identity-verify-flow';
import { useI18n } from '@18f/identity-react-i18n';
-import InPersonTroubleshootingOptions from './in-person-troubleshooting-options';
+import { FormStepsButton } from '@18f/identity-form-steps';
+import UploadContext from '../context/upload';
+// WILLFIX: Hiding this component until help links are finalized; see LG-6875
+// import InPersonTroubleshootingOptions from './in-person-troubleshooting-options';
-function InPersonPrepareStep() {
+function InPersonPrepareStep({ value }) {
const { t } = useI18n();
const { inPersonURL } = useContext(FlowContext);
+ const { flowPath } = useContext(UploadContext);
+ const { selectedLocationName } = value;
return (
<>
-
- {t('in_person_proofing.body.prepare.alert_selected_post_office', { name: 'EASTCHESTER' })}
-
+ {selectedLocationName && (
+
+ {t('in_person_proofing.body.prepare.alert_selected_post_office', {
+ name: selectedLocationName,
+ })}
+
+ )}
{t('in_person_proofing.headings.prepare')}
{t('in_person_proofing.body.prepare.verify_step_about')}
@@ -74,14 +83,18 @@ function InPersonPrepareStep() {
- {inPersonURL && (
+ {flowPath === 'hybrid' && }
+ {inPersonURL && flowPath === 'standard' && (
{t('forms.buttons.continue')}
)}
+ {/*
+ WILLFIX: Hiding this component until help links are finalized; see LG-6875
+ */}
>
);
}
diff --git a/app/javascript/packages/document-capture/components/in-person-switch-back-step.tsx b/app/javascript/packages/document-capture/components/in-person-switch-back-step.tsx
new file mode 100644
index 00000000000..bcf11f094c0
--- /dev/null
+++ b/app/javascript/packages/document-capture/components/in-person-switch-back-step.tsx
@@ -0,0 +1,26 @@
+import { useLayoutEffect } from 'react';
+import { PageHeading } from '@18f/identity-components';
+import { getAssetPath } from '@18f/identity-assets';
+import { t } from '@18f/identity-i18n';
+import type { FormStepComponentProps } from '@18f/identity-form-steps';
+
+function InPersonSwitchBackStep({ onChange }: FormStepComponentProps) {
+ // Resetting the value prevents the user from being prompted about unsaved changes when closing
+ // the tab. `useLayoutEffect` is used to avoid race conditions where the callback could occur at
+ // the same time as the change handler's `ifStillMounted` wrapping `useEffect`, which would treat
+ // it as unmounted and not update the value.
+ useLayoutEffect(() => onChange({}, { patch: false }), []);
+
+ return (
+ <>
+ {t('in_person_proofing.headings.switch_back')}
+
+ >
+ );
+}
+
+export default InPersonSwitchBackStep;
diff --git a/app/javascript/packages/document-capture/components/review-issues-step.tsx b/app/javascript/packages/document-capture/components/review-issues-step.tsx
index ad71f384118..a981efc0d83 100644
--- a/app/javascript/packages/document-capture/components/review-issues-step.tsx
+++ b/app/javascript/packages/document-capture/components/review-issues-step.tsx
@@ -50,6 +50,8 @@ interface ReviewIssuesStepValue {
interface ReviewIssuesStepProps extends FormStepComponentProps {
remainingAttempts: number;
+ isFailedResult: boolean;
+
captureHints: boolean;
pii?: PII;
@@ -70,6 +72,7 @@ function ReviewIssuesStep({
onError = () => {},
registerField = () => undefined,
remainingAttempts = Infinity,
+ isFailedResult = false,
pii,
captureHints = false,
}: ReviewIssuesStepProps) {
@@ -110,6 +113,7 @@ function ReviewIssuesStep({
}
>
diff --git a/app/javascript/packages/document-capture/context/upload.tsx b/app/javascript/packages/document-capture/context/upload.tsx
index b02fac344a1..b24b844284b 100644
--- a/app/javascript/packages/document-capture/context/upload.tsx
+++ b/app/javascript/packages/document-capture/context/upload.tsx
@@ -94,6 +94,11 @@ export interface UploadErrorResponse {
* Personally-identifiable information from OCR analysis.
*/
ocr_pii?: PII;
+
+ /**
+ * Whether the unsuccessful result was the failure type.
+ */
+ result_failed: boolean;
}
export type UploadImplementation = (
diff --git a/app/javascript/packages/document-capture/services/upload.ts b/app/javascript/packages/document-capture/services/upload.ts
index e49d4a6519f..84b98fd1935 100644
--- a/app/javascript/packages/document-capture/services/upload.ts
+++ b/app/javascript/packages/document-capture/services/upload.ts
@@ -36,6 +36,8 @@ export class UploadFormEntriesError extends FormError {
remainingAttempts = Infinity;
+ isFailedResult = false;
+
pii?: PII;
hints = false;
@@ -111,6 +113,8 @@ const upload: UploadImplementation = async function (payload, { method = 'POST',
error.hints = result.hints;
}
+ error.isFailedResult = !!result.result_failed;
+
throw error;
}
diff --git a/app/javascript/packages/form-steps/form-steps.spec.tsx b/app/javascript/packages/form-steps/form-steps.spec.tsx
index e6f8ef3983a..a562f057d76 100644
--- a/app/javascript/packages/form-steps/form-steps.spec.tsx
+++ b/app/javascript/packages/form-steps/form-steps.spec.tsx
@@ -1,4 +1,4 @@
-import { useContext } from 'react';
+import { useContext, useCallback } from 'react';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { waitFor } from '@testing-library/dom';
@@ -250,6 +250,43 @@ describe('FormSteps', () => {
expect(onChange).to.have.been.calledWith({ changed: true, secondInputOne: 'one' });
});
+ it('provides set onChange option for non-patch value change', async () => {
+ const steps = [
+ {
+ name: 'first',
+ title: 'First Title',
+ form: ({ onChange, value }) => (
+ <>
+ onChange({ a: 1 }))
+ .onSecondCall()
+ .callsFake(() => onChange({ b: 2 }, { patch: false })),
+ [],
+ )}
+ >
+ Change Value
+
+ {JSON.stringify(value)}
+ >
+ ),
+ },
+ ];
+
+ const { getByRole, getByTestId } = render( );
+
+ const button = getByRole('button', { name: 'Change Value' });
+ await userEvent.click(button);
+ await userEvent.click(button);
+
+ const value = getByTestId('value');
+ expect(value.textContent).to.equal('{"b":2}');
+ });
+
it('submits with form values', async () => {
const onComplete = sinon.spy();
const { getByText, getByLabelText } = render(
diff --git a/app/javascript/packages/form-steps/form-steps.tsx b/app/javascript/packages/form-steps/form-steps.tsx
index 73057d23177..72486e91b8c 100644
--- a/app/javascript/packages/form-steps/form-steps.tsx
+++ b/app/javascript/packages/form-steps/form-steps.tsx
@@ -39,9 +39,9 @@ type FormValues = Record;
export interface FormStepComponentProps {
/**
- * Update values, merging with existing values.
+ * Update values, merging with existing values if configured as a patch.
*/
- onChange: (nextValues: Partial) => void;
+ onChange: (nextValues: Partial, options?: { patch: boolean }) => void;
/**
* Trigger a field error.
@@ -426,11 +426,15 @@ function FormSteps({
value={values}
errors={activeErrors}
unknownFieldErrors={unknownFieldErrors}
- onChange={ifStillMounted((nextValuesPatch) => {
+ onChange={ifStillMounted((nextValues, { patch } = { patch: true }) => {
setActiveErrors((prevActiveErrors) =>
- prevActiveErrors.filter(({ field }) => !field || !(field in nextValuesPatch)),
+ prevActiveErrors.filter(({ field }) => !field || !(field in nextValues)),
);
- setPatchValues(nextValuesPatch);
+ if (patch) {
+ setPatchValues(nextValues);
+ } else {
+ setValues(nextValues);
+ }
})}
onError={ifStillMounted((error, { field } = {}) => {
if (field) {
diff --git a/app/jobs/get_usps_proofing_results_job.rb b/app/jobs/get_usps_proofing_results_job.rb
index 85450fb8bfc..8b3b78bd7dd 100644
--- a/app/jobs/get_usps_proofing_results_job.rb
+++ b/app/jobs/get_usps_proofing_results_job.rb
@@ -70,6 +70,7 @@ def handle_bad_request_error(err, enrollment)
analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_exception(
reason: 'Request exception',
enrollment_id: enrollment.id,
+ enrollment_code: enrollment.enrollment_code,
exception_class: err.class.to_s,
exception_message: err.message,
)
@@ -80,6 +81,7 @@ def handle_standard_error(err, enrollment)
analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_exception(
reason: 'Request exception',
enrollment_id: enrollment.id,
+ enrollment_code: enrollment.enrollment_code,
exception_class: err.class.to_s,
exception_message: err.message,
)
@@ -89,6 +91,7 @@ def handle_response_is_not_a_hash(enrollment)
analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_exception(
reason: 'Bad response structure',
enrollment_id: enrollment.id,
+ enrollment_code: enrollment.enrollment_code,
)
end
@@ -96,6 +99,7 @@ def handle_unsupported_status(enrollment, status)
analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_enrollment_failure(
reason: 'Unsupported status',
enrollment_id: enrollment.id,
+ enrollment_code: enrollment.enrollment_code,
status: status,
)
end
@@ -104,6 +108,7 @@ def handle_unsupported_id_type(enrollment, primary_id_type)
analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_enrollment_failure(
reason: 'Unsupported ID type',
enrollment_id: enrollment.id,
+ enrollment_code: enrollment.enrollment_code,
primary_id_type: primary_id_type,
)
end
@@ -112,11 +117,10 @@ def handle_failed_status(enrollment, response)
analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_enrollment_failure(
reason: 'Failed status',
enrollment_id: enrollment.id,
+ enrollment_code: enrollment.enrollment_code,
failure_reason: response['failureReason'],
fraud_suspected: response['fraudSuspected'],
primary_id_type: response['primaryIdType'],
- proofing_city: response['proofingCity'],
- proofing_post_office: response['proofingPostOffice'],
proofing_state: response['proofingState'],
secondary_id_type: response['secondaryIdType'],
transaction_end_date_time: response['transactionEndDateTime'],
@@ -124,12 +128,22 @@ def handle_failed_status(enrollment, response)
)
end
+ def handle_successful_status_update(enrollment)
+ analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_enrollment_success(
+ reason: 'Successful status update',
+ enrollment_id: enrollment.id,
+ enrollment_code: enrollment.enrollment_code,
+ )
+ end
+
def update_enrollment_status(enrollment, response)
case response['status']
when IPP_STATUS_PASSED
if SUPPORTED_ID_TYPES.include?(response['primaryIdType'])
enrollment.profile.activate
enrollment.update(status: :passed)
+ handle_successful_status_update(enrollment)
+ send_verified_email(enrollment.user, enrollment)
else
# Unsupported ID type
enrollment.update(status: :failed)
@@ -138,8 +152,29 @@ def update_enrollment_status(enrollment, response)
when IPP_STATUS_FAILED
enrollment.update(status: :failed)
handle_failed_status(enrollment, response)
+ send_failed_email(enrollment.user, enrollment)
else
handle_unsupported_status(enrollment, response['status'])
end
end
+
+ def send_verified_email(user, enrollment)
+ user.confirmed_email_addresses.each do |email_address|
+ UserMailer.in_person_verified(
+ user,
+ email_address,
+ enrollment: enrollment,
+ ).deliver_now_or_later
+ end
+ end
+
+ def send_failed_email(user, enrollment)
+ user.confirmed_email_addresses.each do |email_address|
+ UserMailer.in_person_failed(
+ user,
+ email_address,
+ enrollment: enrollment,
+ ).deliver_now_or_later
+ end
+ end
end
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index 130bc7f05ad..ccb2bef93d6 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -224,6 +224,39 @@ def account_verified(user, email_address, date_time:, sp_name:, disavowal_token:
end
end
+ def in_person_ready_to_verify(user, email_address, first_name:, enrollment:)
+ with_user_locale(user) do
+ @header = t('in_person_proofing.headings.barcode')
+ @first_name = first_name
+ @presenter = Idv::InPerson::ReadyToVerifyPresenter.new(enrollment: enrollment)
+ mail(
+ to: email_address.email,
+ subject: t('user_mailer.in_person_ready_to_verify.subject', app_name: APP_NAME),
+ )
+ end
+ end
+
+ def in_person_verified(user, email_address, enrollment:)
+ with_user_locale(user) do
+ @hide_title = true
+ @presenter = Idv::InPerson::VerificationResultsEmailPresenter.new(enrollment: enrollment)
+ mail(
+ to: email_address.email,
+ subject: t('user_mailer.in_person_verified.subject', app_name: APP_NAME),
+ )
+ end
+ end
+
+ def in_person_failed(user, email_address, enrollment:)
+ with_user_locale(user) do
+ @presenter = Idv::InPerson::VerificationResultsEmailPresenter.new(enrollment: enrollment)
+ mail(
+ to: email_address.email,
+ subject: t('user_mailer.in_person_failed.subject', app_name: APP_NAME),
+ )
+ end
+ end
+
private
def email_should_receive_nonessential_notifications?(email)
diff --git a/app/models/backup_code_configuration.rb b/app/models/backup_code_configuration.rb
index 05138a035e5..5db2cc84f40 100644
--- a/app/models/backup_code_configuration.rb
+++ b/app/models/backup_code_configuration.rb
@@ -14,7 +14,7 @@ def self.unused
end
def mfa_enabled?
- user.backup_code_configurations.unused.any? if user
+ persisted? && used_at.nil?
end
def selection_presenters
diff --git a/app/models/in_person_enrollment.rb b/app/models/in_person_enrollment.rb
index df33c0c161f..f2f5fc192e6 100644
--- a/app/models/in_person_enrollment.rb
+++ b/app/models/in_person_enrollment.rb
@@ -9,7 +9,7 @@ class InPersonEnrollment < ApplicationRecord
passed: 2,
failed: 3,
expired: 4,
- canceled: 5,
+ cancelled: 5,
}
validate :profile_belongs_to_user
@@ -51,7 +51,9 @@ def on_status_updated
end
def profile_belongs_to_user
- unless profile&.user == user
+ return unless profile.present?
+
+ unless profile.user == user
errors.add :profile, I18n.t('idv.failure.exceptions.internal_error'),
type: :in_person_enrollment_user_profile_mismatch
end
diff --git a/app/models/profile.rb b/app/models/profile.rb
index 9ee54bc5cbe..8465f997a65 100644
--- a/app/models/profile.rb
+++ b/app/models/profile.rb
@@ -13,8 +13,9 @@ class Profile < ApplicationRecord
enum deactivation_reason: {
password_reset: 1,
encryption_error: 2,
- verification_pending: 3,
+ gpo_verification_pending: 3,
verification_cancelled: 4,
+ in_person_verification_pending: 5,
}
attr_reader :personal_key
diff --git a/app/models/user.rb b/app/models/user.rb
index 56277bd34d5..d9ebc343577 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -45,7 +45,13 @@ class User < ApplicationRecord
has_many :sign_in_restrictions, dependent: :destroy
has_many :in_person_enrollments, dependent: :destroy
- has_one :pending_in_person_enrollment, -> { where(status: :pending).order(created_at: :desc) },
+ has_one :pending_in_person_enrollment,
+ -> { where(status: :pending).order(created_at: :desc) },
+ class_name: 'InPersonEnrollment', foreign_key: :user_id, inverse_of: :user,
+ dependent: :destroy
+
+ has_one :establishing_in_person_enrollment,
+ -> { where(status: :establishing).order(created_at: :desc) },
class_name: 'InPersonEnrollment', foreign_key: :user_id, inverse_of: :user,
dependent: :destroy
@@ -92,7 +98,7 @@ def pending_profile?
end
def pending_profile
- profiles.verification_pending.order(created_at: :desc).first
+ profiles.gpo_verification_pending.order(created_at: :desc).first
end
def default_phone_configuration
diff --git a/app/presenters/idv/in_person/ready_to_verify_presenter.rb b/app/presenters/idv/in_person/ready_to_verify_presenter.rb
index e48fcdced0c..575ce7cbd0d 100644
--- a/app/presenters/idv/in_person/ready_to_verify_presenter.rb
+++ b/app/presenters/idv/in_person/ready_to_verify_presenter.rb
@@ -1,36 +1,23 @@
-require 'barby'
-require 'barby/barcode/code_128'
-require 'barby/outputter/png_outputter'
-
module Idv
module InPerson
class ReadyToVerifyPresenter
# WILLFIX: With LG-6881, confirm timezone or use deadline from enrollment response.
USPS_SERVER_TIMEZONE = ActiveSupport::TimeZone['America/New_York']
- delegate :selected_location_details, to: :enrollment
+ delegate :selected_location_details, :enrollment_code, to: :enrollment
def initialize(enrollment:)
@enrollment = enrollment
end
- def barcode_data_url
- "data:image/png;base64,#{Base64.strict_encode64(barcode_image_data)}"
- end
-
def formatted_due_date
due_date.in_time_zone(USPS_SERVER_TIMEZONE).strftime(I18n.t('time.formats.event_date'))
end
- def formatted_enrollment_code
- EnrollmentCodeFormatter.format(enrollment_code)
- end
-
def selected_location_hours(prefix)
- selected_location_details['hours'].each do |hours_candidate|
- hours = hours_candidate["#{prefix}Hours"]
- return localized_hours(hours) if hours
- end
+ return unless selected_location_details
+ hours = selected_location_details["#{prefix}_hours"]
+ return localized_hours(hours) if hours
end
def needs_proof_of_address?
@@ -40,7 +27,6 @@ def needs_proof_of_address?
private
attr_reader :enrollment
- delegate :enrollment_code, to: :enrollment
def barcode_image_data
Barby::Code128C.new(enrollment_code).to_png(margin: 0, xdim: 2)
diff --git a/app/presenters/idv/in_person/verification_results_email_presenter.rb b/app/presenters/idv/in_person/verification_results_email_presenter.rb
new file mode 100644
index 00000000000..d8970ec9388
--- /dev/null
+++ b/app/presenters/idv/in_person/verification_results_email_presenter.rb
@@ -0,0 +1,22 @@
+module Idv
+ module InPerson
+ class VerificationResultsEmailPresenter
+ # update to user's time zone when out of pilot
+ USPS_SERVER_TIMEZONE = ActiveSupport::TimeZone['America/New_York']
+
+ def initialize(enrollment:)
+ @enrollment = enrollment
+ end
+
+ def location_name
+ @enrollment.selected_location_details['name']
+ end
+
+ def formatted_verified_date
+ @enrollment.status_updated_at.in_time_zone(USPS_SERVER_TIMEZONE).strftime(
+ I18n.t('time.formats.event_date'),
+ )
+ end
+ end
+ end
+end
diff --git a/app/presenters/image_upload_response_presenter.rb b/app/presenters/image_upload_response_presenter.rb
index b79c5e8c821..9b319a45c08 100644
--- a/app/presenters/image_upload_response_presenter.rb
+++ b/app/presenters/image_upload_response_presenter.rb
@@ -38,6 +38,7 @@ def as_json(*)
json[:redirect] = idv_session_errors_throttled_url if remaining_attempts&.zero?
json[:hints] = true if show_hints?
json[:ocr_pii] = ocr_pii
+ json[:result_failed] = doc_auth_result_failed?
json
end
end
@@ -48,6 +49,10 @@ def url_options
private
+ def doc_auth_result_failed?
+ @form_response.to_h[:doc_auth_result] == DocAuth::Acuant::ResultCodes::FAILED.name
+ end
+
def show_hints?
@form_response.errors[:hints].present? || attention_with_barcode?
end
diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb
index 36d87e45af6..45af378d4ee 100644
--- a/app/services/analytics_events.rb
+++ b/app/services/analytics_events.rb
@@ -997,6 +997,25 @@ def invalid_authenticity_token(
)
end
+ # @param [String] controller
+ # @param [String] referer
+ # @param [Boolean] user_signed_in
+ # Redirect was almost sent to an invalid external host unexpectedly
+ def unsafe_redirect_error(
+ controller:,
+ referer:,
+ user_signed_in: nil,
+ **extra
+ )
+ track_event(
+ 'Unsafe Redirect',
+ controller: controller,
+ referer: referer,
+ user_signed_in: user_signed_in,
+ **extra,
+ )
+ end
+
# @param [Integer] acknowledged_event_count number of acknowledged events in the API call
# @param [Integer] rendered_event_count how many events were rendered in the API response
# @param [String] set_errors JSON encoded representation of SET errors from the client
@@ -1325,10 +1344,16 @@ def multi_factor_auth_max_sends
# Tracks when a user sets up a multi factor auth method
# @param [String] multi_factor_auth_method
- def multi_factor_auth_setup(multi_factor_auth_method:, **extra)
+ # @param [Boolean] in_multi_mfa_selection_flow
+ # @param [integer] enabled_mfa_methods_count
+ def multi_factor_auth_setup(multi_factor_auth_method:,
+ enabled_mfa_methods_count:, in_multi_mfa_selection_flow:,
+ **extra)
track_event(
'Multi-Factor Authentication Setup',
multi_factor_auth_method: multi_factor_auth_method,
+ in_multi_mfa_selection_flow: in_multi_mfa_selection_flow,
+ enabled_mfa_methods_count: enabled_mfa_methods_count,
**extra,
)
end
@@ -2094,11 +2119,13 @@ def user_registration_2fa_additional_setup_visit
# @param [Boolean] success
# @param [Hash] errors
# @param [Integer] enabled_mfa_methods_count
+ # @param [Integer] selected_mfa_count
# @param ['voice', 'auth_app'] selection
# Tracks when the the user has selected and submitted MFA auth methods on user registration
def user_registration_2fa_setup(
success:,
errors: nil,
+ selected_mfa_count: nil,
enabled_mfa_methods_count: nil,
selection: nil,
**extra
@@ -2108,6 +2135,7 @@ def user_registration_2fa_setup(
{
success: success,
errors: errors,
+ selected_mfa_count: selected_mfa_count,
enabled_mfa_methods_count: enabled_mfa_methods_count,
selection: selection,
**extra,
@@ -2115,6 +2143,21 @@ def user_registration_2fa_setup(
)
end
+ # @param [String] mfa_method
+ # Tracks when the the user fully registered by submitting their first MFA method into the system
+ def user_registration_user_fully_registered(
+ mfa_method:,
+ **extra
+ )
+ track_event(
+ 'User Registration: User Fully Registered',
+ {
+ mfa_method: mfa_method,
+ **extra,
+ }.compact,
+ )
+ end
+
# @param [Boolean] success
# @param [Hash] mfa_method_counts
# @param [Integer] enabled_mfa_methods_count
@@ -2350,5 +2393,17 @@ def idv_in_person_usps_proofing_results_job_enrollment_failure(reason:, enrollme
**extra,
)
end
+
+ # Tracks individual enrollments that succeed during GetUspsProofingResultsJob
+ # @param [String] reason why did this enrollment pass?
+ # @param [String] enrollment_id
+ def idv_in_person_usps_proofing_results_job_enrollment_success(reason:, enrollment_id:, **extra)
+ track_event(
+ 'GetUspsProofingResultsJob: Enrollment passed proofing',
+ reason: reason,
+ enrollment_id: enrollment_id,
+ **extra,
+ )
+ end
end
# rubocop:enable Metrics/ModuleLength
diff --git a/app/services/doc_auth/acuant/responses/get_results_response.rb b/app/services/doc_auth/acuant/responses/get_results_response.rb
index 9f77da3f06d..b301bd93feb 100644
--- a/app/services/doc_auth/acuant/responses/get_results_response.rb
+++ b/app/services/doc_auth/acuant/responses/get_results_response.rb
@@ -53,13 +53,14 @@ def response_info
def create_response_info
alerts = processed_alerts
+ log_alert_formatter = DocAuth::ProcessedAlertToLogAlertFormatter.new
{
vendor: 'Acuant',
billed: result_code.billed,
doc_auth_result: result_code.name,
processed_alerts: alerts,
alert_failure_count: alerts[:failed]&.count.to_i,
- log_alert_results: log_alerts(alerts),
+ log_alert_results: log_alert_formatter.log_alerts(alerts),
image_metrics: processed_image_metrics,
tamper_result: tamper_result_code&.name,
}
@@ -95,19 +96,6 @@ def processed_alerts
@processed_alerts ||= process_raw_alerts(raw_alerts)
end
- def log_alerts(alerts)
- log_alert_results = {}
- alerts.keys.each do |key|
- alerts[key.to_sym].each do |alert|
- side = alert[:side] || 'no_side'
- log_alert_results[alert[:name].
- downcase.
- parameterize(separator: '_').to_sym] = { "#{side}": alert[:result] }
- end
- end
- log_alert_results
- end
-
def processed_image_metrics
@processed_image_metrics ||= raw_images_data.index_by do |image|
image.delete('Uri')
diff --git a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb
index 7f544dcd5c7..20bff9ab194 100644
--- a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb
+++ b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb
@@ -122,21 +122,9 @@ def response_info
@response_info ||= create_response_info
end
- def log_alerts(alerts)
- log_alert_results = {}
- alerts.keys.each do |key|
- alerts[key.to_sym].each do |alert|
- side = alert[:side] || 'no_side'
- log_alert_results[alert[:name].
- downcase.
- parameterize(separator: '_').to_sym] = { "#{side}": alert[:result] }
- end
- end
- log_alert_results
- end
-
def create_response_info
alerts = parsed_alerts
+ log_alert_formatter = DocAuth::ProcessedAlertToLogAlertFormatter.new
{
liveness_enabled: @liveness_checking_enabled,
@@ -146,7 +134,7 @@ def create_response_info
doc_auth_result: doc_auth_result,
processed_alerts: alerts,
alert_failure_count: alerts[:failed]&.count.to_i,
- log_alert_results: log_alerts(alerts),
+ log_alert_results: log_alert_formatter.log_alerts(alerts),
portrait_match_results: true_id_product[:PORTRAIT_MATCH_RESULT],
image_metrics: parse_image_metrics,
}
@@ -260,9 +248,13 @@ def transform_metrics(img_metrics)
end
def parse_date(year:, month:, day:)
- if year.to_i.positive? && month.to_i.positive? && day.to_i.positive?
- Date.new(year.to_i, month.to_i, day.to_i).to_s
- end
+ Date.new(year.to_i, month.to_i, day.to_i).to_s if year.to_i.positive?
+ rescue ArgumentError
+ message = {
+ event: 'Failure to parse TrueID date',
+ }.to_json
+ Rails.logger.info(message)
+ nil
end
end
end
diff --git a/app/services/doc_auth/mock/result_response.rb b/app/services/doc_auth/mock/result_response.rb
index 23b3cdf102f..e7b3d6ada50 100644
--- a/app/services/doc_auth/mock/result_response.rb
+++ b/app/services/doc_auth/mock/result_response.rb
@@ -12,7 +12,7 @@ def initialize(uploaded_file, config, liveness_enabled)
errors: errors,
pii_from_doc: pii_from_doc,
extra: {
- doc_auth_result: success? ? 'Passed' : 'Caution',
+ doc_auth_result: doc_auth_result,
billed: true,
},
)
@@ -78,6 +78,22 @@ def parsed_data_from_uploaded_file
@parsed_data_from_uploaded_file = parse_uri || parse_yaml
end
+ def doc_auth_result
+ doc_auth_result_from_uploaded_file || doc_auth_result_from_success
+ end
+
+ def doc_auth_result_from_uploaded_file
+ parsed_data_from_uploaded_file&.[]('doc_auth_result')
+ end
+
+ def doc_auth_result_from_success
+ if success?
+ DocAuth::Acuant::ResultCodes::PASSED.name
+ else
+ DocAuth::Acuant::ResultCodes::CAUTION.name
+ end
+ end
+
def parse_uri
uri = URI.parse(uploaded_file.chomp)
if uri.scheme == 'data'
diff --git a/app/services/doc_auth/processed_alert_to_log_alert_formatter.rb b/app/services/doc_auth/processed_alert_to_log_alert_formatter.rb
new file mode 100644
index 00000000000..a21c09429a4
--- /dev/null
+++ b/app/services/doc_auth/processed_alert_to_log_alert_formatter.rb
@@ -0,0 +1,34 @@
+module DocAuth
+ class ProcessedAlertToLogAlertFormatter
+ def get_alert_result(log_alert_results, side, alert_name_key, result)
+ if log_alert_results.dig(alert_name_key, side.to_sym).present?
+ alert_value = log_alert_results[alert_name_key][side.to_sym]
+ Rails.logger.
+ info("ALERT ALREADY HAS A VALUE: #{alert_value}, #{result}")
+ end
+ result
+ end
+
+ def log_alerts(alerts)
+ log_alert_results = {}
+
+ alerts.keys.each do |key|
+ alerts[key.to_sym].each do |alert|
+ alert_name_key = alert[:name].
+ downcase.
+ parameterize(separator: '_').to_sym
+ side = alert[:side] || 'no_side'
+
+ log_alert_results[alert_name_key] =
+ { "#{side}": get_alert_result(
+ log_alert_results,
+ side,
+ alert_name_key,
+ alert[:result],
+ ) }
+ end
+ end
+ log_alert_results
+ end
+ end
+end
diff --git a/app/services/funnel/registration/add_mfa.rb b/app/services/funnel/registration/add_mfa.rb
index 5dbfdea8b8d..8246c52e636 100644
--- a/app/services/funnel/registration/add_mfa.rb
+++ b/app/services/funnel/registration/add_mfa.rb
@@ -1,22 +1,23 @@
module Funnel
module Registration
class AddMfa
- def self.call(user_id, mfa_method)
+ def self.call(user_id, mfa_method, analytics)
now = Time.zone.now
funnel = RegistrationLog.find_by(user_id: user_id)
return if funnel.blank? || funnel.second_mfa.present?
- params = if funnel.first_mfa.present?
- {
- second_mfa: mfa_method,
- }
- else
- {
- first_mfa: mfa_method,
- first_mfa_at: now,
- registered_at: now,
- }
- end
+ if funnel.first_mfa.present?
+ params = {
+ second_mfa: mfa_method,
+ }
+ else
+ params = {
+ first_mfa: mfa_method,
+ first_mfa_at: now,
+ registered_at: now,
+ }
+ analytics.user_registration_user_fully_registered(mfa_method: mfa_method)
+ end
funnel.update!(params)
end
diff --git a/app/services/idv/actions/ipp/cancel_update_ssn_action.rb b/app/services/idv/actions/ipp/cancel_update_ssn_action.rb
new file mode 100644
index 00000000000..6e37e6c9f04
--- /dev/null
+++ b/app/services/idv/actions/ipp/cancel_update_ssn_action.rb
@@ -0,0 +1,11 @@
+module Idv
+ module Actions
+ module Ipp
+ class CancelUpdateSsnAction < Idv::Steps::DocAuthBaseStep
+ def call
+ mark_step_complete(:ssn) if flow_session.dig(:pii_from_user, :ssn)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/idv/cancel_verification_attempt.rb b/app/services/idv/cancel_verification_attempt.rb
index e9d760bb2a5..8df5c4210fe 100644
--- a/app/services/idv/cancel_verification_attempt.rb
+++ b/app/services/idv/cancel_verification_attempt.rb
@@ -7,7 +7,7 @@ def initialize(user:)
end
def call
- user.profiles.verification_pending.each do |profile|
+ user.profiles.gpo_verification_pending.each do |profile|
profile.update!(
active: false,
deactivation_reason: :verification_cancelled,
diff --git a/app/services/idv/flows/in_person_flow.rb b/app/services/idv/flows/in_person_flow.rb
index dff64ba9a19..07fc1d6e115 100644
--- a/app/services/idv/flows/in_person_flow.rb
+++ b/app/services/idv/flows/in_person_flow.rb
@@ -16,6 +16,7 @@ class InPersonFlow < Flow::BaseFlow
}.freeze
ACTIONS = {
+ cancel_update_ssn: Idv::Actions::Ipp::CancelUpdateSsnAction,
redo_state_id: Idv::Actions::Ipp::RedoStateIdAction,
redo_address: Idv::Actions::Ipp::RedoAddressAction,
redo_ssn: Idv::Actions::RedoSsnAction,
@@ -41,7 +42,10 @@ def initialize(controller, session, name)
@idv_session = self.class.session_idv(session)
super(controller, STEPS, ACTIONS, session[name])
@flow_session ||= {}
- @flow_session[:pii_from_user] ||= {}
+ @flow_session[:pii_from_user] ||= { uuid: current_user.uuid }
+ # there may be data in @idv_session to copy to @flow_session
+ applicant = @idv_session['applicant'] || {}
+ @flow_session[:pii_from_user] = @flow_session[:pii_from_user].to_h.merge(applicant)
end
def self.session_idv(session)
diff --git a/app/services/idv/profile_maker.rb b/app/services/idv/profile_maker.rb
index 1c93d0a7170..70e780b58ce 100644
--- a/app/services/idv/profile_maker.rb
+++ b/app/services/idv/profile_maker.rb
@@ -9,10 +9,7 @@ def initialize(applicant:, user:, user_password:)
end
def save_profile
- profile = Profile.new(
- deactivation_reason: :verification_pending,
- user: user,
- )
+ profile = Profile.new(user: user, active: false)
profile.encrypt_pii(pii_attributes, user_password)
profile.proofing_components = current_proofing_components
profile.save!
diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb
index 31220460131..c561d46a110 100644
--- a/app/services/idv/session.rb
+++ b/app/services/idv/session.rb
@@ -72,17 +72,41 @@ def clear
user_session.delete(:idv)
end
+ def pending_in_person_enrollment?
+ return false unless IdentityConfig.store.in_person_proofing_enabled
+ ProofingComponent.find_by(user: current_user)&.document_check == Idp::Constants::Vendors::USPS
+ end
+
def phone_confirmed?
vendor_phone_confirmation == true && user_phone_confirmation == true
end
def complete_session
- complete_profile if phone_confirmed?
- create_gpo_entry if address_verification_mechanism == 'gpo'
+ associate_in_person_enrollment_with_profile
+
+ if address_verification_mechanism == 'gpo'
+ profile.deactivate(:gpo_verification_pending)
+ create_gpo_entry
+ elsif phone_confirmed?
+ if pending_in_person_enrollment?
+ UspsInPersonProofing::EnrollmentHelper.schedule_in_person_enrollment(
+ current_user,
+ applicant,
+ )
+ profile.deactivate(:in_person_verification_pending)
+ else
+ complete_profile
+ end
+ end
+ end
+
+ def associate_in_person_enrollment_with_profile
+ return unless pending_in_person_enrollment? && current_user.establishing_in_person_enrollment
+ current_user.establishing_in_person_enrollment.update(profile: profile)
end
def complete_profile
- current_user.pending_profile&.activate
+ profile.activate
move_pii_to_user_session
end
@@ -146,5 +170,10 @@ def build_profile_maker(user_password)
user_password: user_password,
)
end
+
+ def in_person_enrollment?
+ return false unless IdentityConfig.store.in_person_proofing_enabled
+ ProofingComponent.find_by(user: current_user)&.document_check == Idp::Constants::Vendors::USPS
+ end
end
end
diff --git a/app/services/idv/steps/doc_auth_base_step.rb b/app/services/idv/steps/doc_auth_base_step.rb
index cf34dcb0037..734c7bd0dce 100644
--- a/app/services/idv/steps/doc_auth_base_step.rb
+++ b/app/services/idv/steps/doc_auth_base_step.rb
@@ -27,13 +27,17 @@ def idv_failure(result)
step_name: self.class.name,
remaining_attempts: throttle.remaining_count,
)
- redirect_to idv_session_errors_exception_url
+ redirect_to idv_session_errors_exception_url(
+ from: request.path,
+ )
else
@flow.analytics.idv_doc_auth_warning_visited(
step_name: self.class.name,
remaining_attempts: throttle.remaining_count,
)
- redirect_to idv_session_errors_warning_url
+ redirect_to idv_session_errors_warning_url(
+ from: request.path,
+ )
end
result
end
diff --git a/app/services/idv/steps/welcome_step.rb b/app/services/idv/steps/welcome_step.rb
index 4830ff6a054..92ff02bf433 100644
--- a/app/services/idv/steps/welcome_step.rb
+++ b/app/services/idv/steps/welcome_step.rb
@@ -6,6 +6,7 @@ class WelcomeStep < DocAuthBaseStep
def call
return no_camera_redirect if params[:no_camera]
create_document_capture_session(document_capture_session_uuid_key)
+ cancel_previous_in_person_enrollments
end
private
@@ -15,6 +16,12 @@ def no_camera_redirect
msg = 'Doc Auth error: Javascript could not detect camera on mobile device.'
failure(msg)
end
+
+ def cancel_previous_in_person_enrollments
+ return unless IdentityConfig.store.in_person_proofing_enabled
+ UspsInPersonProofing::EnrollmentHelper.
+ cancel_stale_establishing_enrollments_for_user(current_user)
+ end
end
end
end
diff --git a/app/services/inherited_proofing/va/service.rb b/app/services/inherited_proofing/va/service.rb
new file mode 100644
index 00000000000..87a7c047190
--- /dev/null
+++ b/app/services/inherited_proofing/va/service.rb
@@ -0,0 +1,86 @@
+module InheritedProofing
+ module Va
+ # Encapsulates request, response, error handling, validation, etc. for calling
+ # the VA service to gain PII for a particular user that will be subsequently
+ # used to proof the user using inherited proofing.
+ class Service
+ BASE_URI = IdentityConfig.store.inherited_proofing_va_base_url
+
+ attr_reader :auth_code
+
+ def initialize(auth_code)
+ @auth_code = auth_code
+ end
+
+ # Calls the endpoint and returns the decrypted response.
+ def execute
+ raise 'The provided auth_code is blank?' if auth_code.blank?
+
+ response = request
+ payload_to_hash decrypt_payload(response)
+ end
+
+ private
+
+ def request
+ connection.get(request_uri) { |req| req.headers = request_headers }
+ end
+
+ def connection
+ Faraday.new do |conn|
+ conn.options.timeout = request_timeout
+ conn.options.read_timeout = request_timeout
+ conn.options.open_timeout = request_timeout
+ conn.options.write_timeout = request_timeout
+ conn.request :instrumentation, name: 'inherited_proofing.va'
+
+ # raises errors on 4XX or 5XX responses
+ conn.response :raise_error
+ end
+ end
+
+ def request_timeout
+ @request_timeout ||= IdentityConfig.store.doc_auth_s3_request_timeout
+ end
+
+ def request_uri
+ @request_uri ||= "#{ URI(BASE_URI) }/inherited_proofing/user_attributes"
+ end
+
+ def request_headers
+ { Authorization: "Bearer #{jwt_token}" }
+ end
+
+ def jwt_token
+ JWT.encode(jwt_payload, private_key, jwt_encryption)
+ end
+
+ def jwt_payload
+ { inherited_proofing_auth: auth_code, exp: jwt_expires }
+ end
+
+ def private_key
+ @private_key ||= AppArtifacts.store.oidc_private_key
+ end
+
+ def jwt_encryption
+ 'RS256'
+ end
+
+ def jwt_expires
+ 1.day.from_now.to_i
+ end
+
+ def decrypt_payload(response)
+ payload = JSON.parse(response.body)['data']
+ JWE.decrypt(payload, private_key) if payload
+ end
+
+ def payload_to_hash(decrypted_payload, default: nil)
+ return default unless decrypted_payload.present?
+
+ JSON.parse(decrypted_payload, symbolize_names: true)
+ end
+ end
+ end
+end
diff --git a/app/services/irs_attempts_api/tracker_events.rb b/app/services/irs_attempts_api/tracker_events.rb
index c24dc1174a4..54e1eb6616b 100644
--- a/app/services/irs_attempts_api/tracker_events.rb
+++ b/app/services/irs_attempts_api/tracker_events.rb
@@ -10,5 +10,18 @@ def email_and_password_auth(email:, success:)
success: success,
)
end
+
+ # @param [String] user_uuid The user's uuid
+ # @param [String] unique_session_id The unique session id
+ # @param [Boolean] success True if the email and password matched
+ # A user has initiated a logout event
+ def logout_initiated(user_uuid:, unique_session_id:, success:)
+ track_event(
+ :logout_initiated,
+ user_uuid: user_uuid,
+ unique_session_id: unique_session_id,
+ success: success,
+ )
+ end
end
end
diff --git a/app/services/pii/attributes.rb b/app/services/pii/attributes.rb
index a248976d365..d8605821ec4 100644
--- a/app/services/pii/attributes.rb
+++ b/app/services/pii/attributes.rb
@@ -5,7 +5,7 @@ module Pii
Attributes = RedactedStruct.new(
:first_name, :middle_name, :last_name,
- :address1, :address2, :city, :state, :zipcode,
+ :address1, :address2, :city, :state, :zipcode, :same_address_as_id,
:ssn, :dob, :phone,
:prev_address1, :prev_address2, :prev_city, :prev_state, :prev_zipcode,
*DEPRECATED_PII_ATTRIBUTES
diff --git a/app/services/proofing/mock/resolution_mock_client.rb b/app/services/proofing/mock/resolution_mock_client.rb
index af6afe5103e..3abf893d3db 100644
--- a/app/services/proofing/mock/resolution_mock_client.rb
+++ b/app/services/proofing/mock/resolution_mock_client.rb
@@ -3,9 +3,17 @@ module Mock
class ResolutionMockClient < Proofing::Base
vendor_name 'ResolutionMock'
- required_attributes :first_name, :ssn, :zipcode
-
- optional_attributes :uuid, :uuid_prefix
+ required_attributes :uuid,
+ :first_name,
+ :last_name,
+ :dob,
+ :ssn,
+ :address1,
+ :city,
+ :state,
+ :zipcode
+
+ optional_attributes :address2, :uuid_prefix, :dob_year_only
stage :resolution
diff --git a/app/services/push_notification/password_reset_event.rb b/app/services/push_notification/password_reset_event.rb
new file mode 100644
index 00000000000..b969b52f522
--- /dev/null
+++ b/app/services/push_notification/password_reset_event.rb
@@ -0,0 +1,17 @@
+module PushNotification
+ class PasswordResetEvent
+ include IssSubEvent
+
+ EVENT_TYPE = 'https://schemas.login.gov/secevent/risc/event-type/password-reset'.freeze
+
+ attr_reader :user
+
+ def initialize(user:)
+ @user = user
+ end
+
+ def event_type
+ EVENT_TYPE
+ end
+ end
+end
diff --git a/app/services/usps_in_person_proofing/enrollment_helper.rb b/app/services/usps_in_person_proofing/enrollment_helper.rb
new file mode 100644
index 00000000000..62d8d4baf7d
--- /dev/null
+++ b/app/services/usps_in_person_proofing/enrollment_helper.rb
@@ -0,0 +1,77 @@
+module UspsInPersonProofing
+ class EnrollmentHelper
+ class << self
+ def schedule_in_person_enrollment(user, pii)
+ enrollment = user.establishing_in_person_enrollment
+ return unless enrollment
+
+ enrollment.current_address_matches_id = pii['same_address_as_id']
+ enrollment.save!
+
+ enrollment_code = create_usps_enrollment(enrollment, pii)
+ return unless enrollment_code
+
+ # update the enrollment to status pending
+ enrollment.enrollment_code = enrollment_code
+ enrollment.status = :pending
+ enrollment.enrollment_established_at = Time.zone.now
+ enrollment.save!
+
+ send_ready_to_verify_email(user, pii, enrollment)
+ end
+
+ def send_ready_to_verify_email(user, pii, enrollment)
+ user.confirmed_email_addresses.each do |email_address|
+ UserMailer.in_person_ready_to_verify(
+ user,
+ email_address,
+ first_name: pii['first_name'],
+ enrollment: enrollment,
+ ).deliver_now_or_later
+ end
+ end
+
+ def establishing_in_person_enrollment_for_user(user)
+ enrollment = user.establishing_in_person_enrollment
+ return enrollment if enrollment.present?
+
+ InPersonEnrollment.create!(user: user, profile: nil)
+ end
+
+ def usps_proofer
+ if IdentityConfig.store.usps_mock_fallback
+ UspsInPersonProofing::Mock::Proofer.new
+ else
+ UspsInPersonProofing::Proofer.new
+ end
+ end
+
+ def create_usps_enrollment(enrollment, pii)
+ applicant = UspsInPersonProofing::Applicant.new(
+ {
+ unique_id: enrollment.usps_unique_id,
+ first_name: pii['first_name'],
+ last_name: pii['last_name'],
+ address: pii['address1'],
+ # do we need address2?
+ city: pii['city'],
+ state: pii['state'],
+ zip_code: pii['zipcode'],
+ email: 'no-reply@login.gov',
+ },
+ )
+ proofer = usps_proofer
+
+ response = proofer.request_enroll(applicant)
+ response['enrollmentCode']
+ end
+
+ def cancel_stale_establishing_enrollments_for_user(user)
+ user.
+ in_person_enrollments.
+ where(status: :establishing).
+ each(&:cancelled!)
+ end
+ end
+ end
+end
diff --git a/app/services/usps_in_person_proofing/mock.rb b/app/services/usps_in_person_proofing/mock.rb
index 6e20843562b..91a88adb54b 100644
--- a/app/services/usps_in_person_proofing/mock.rb
+++ b/app/services/usps_in_person_proofing/mock.rb
@@ -3,7 +3,32 @@ module Mock
class Proofer
def request_enroll(_applicant)
JSON.load_file(
- Rails.root.join('spec/fixtures/usps_ipp_responses/request_enroll_response.json'),
+ Rails.root.join(
+ 'spec',
+ 'fixtures',
+ 'usps_ipp_responses',
+ 'request_enroll_response.json',
+ ),
+ )
+ end
+
+ def request_facilities(_location)
+ JSON.load_file(
+ Rails.root.join(
+ 'spec',
+ 'fixtures',
+ 'usps_ipp_responses',
+ 'request_facilities_response.json',
+ ),
+ )
+ end
+
+ def request_pilot_facilities
+ JSON.load_file(
+ Rails.root.join(
+ 'config',
+ 'ipp_pilot_usps_facilities.json',
+ ),
)
end
end
diff --git a/app/services/usps_in_person_proofing/post_office.rb b/app/services/usps_in_person_proofing/post_office.rb
index 20ae83c5e4f..3febd3fccad 100644
--- a/app/services/usps_in_person_proofing/post_office.rb
+++ b/app/services/usps_in_person_proofing/post_office.rb
@@ -1,5 +1,16 @@
module UspsInPersonProofing
PostOffice = Struct.new(
- :distance, :address, :city, :phone, :name, :zip_code, :state, keyword_init: true
+ :address,
+ :city,
+ :distance,
+ :name,
+ :phone,
+ :saturday_hours,
+ :state,
+ :sunday_hours,
+ :weekday_hours,
+ :zip_code_4,
+ :zip_code_5,
+ keyword_init: true,
)
end
diff --git a/app/services/usps_in_person_proofing/proofer.rb b/app/services/usps_in_person_proofing/proofer.rb
index e4579cf6a03..2c43d7452b5 100644
--- a/app/services/usps_in_person_proofing/proofer.rb
+++ b/app/services/usps_in_person_proofing/proofer.rb
@@ -16,21 +16,25 @@ def request_facilities(location)
city: location.city,
state: location.state,
zipCode: location.zip_code,
- }
+ }.to_json
- resp = faraday.post(url, body, dynamic_headers)
+ headers = request_headers.merge(
+ 'Authorization' => @token,
+ 'RequestID' => request_id,
+ )
- resp.body['postOffices'].map do |post_office|
- PostOffice.new(
- distance: post_office['distance'],
- address: post_office['streetAddress'],
- city: post_office['city'],
- phone: post_office['phone'],
- name: post_office['name'],
- zip_code: post_office['zip5'],
- state: post_office['state'],
- )
- end
+ parse_facilities(
+ faraday.post(url, body, headers) do |req|
+ req.options.context = { service_name: 'usps_facilities' }
+ end.body,
+ )
+ end
+
+ # Temporary function to return a static set of facilities
+ # @return [Array] Facility locations
+ def request_pilot_facilities
+ resp = File.read(Rails.root.join('config', 'ipp_pilot_usps_facilities.json'))
+ parse_facilities(JSON.parse(resp))
end
# Makes HTTP request to enroll an applicant in in-person proofing.
@@ -56,7 +60,9 @@ def request_enroll(applicant)
IPPAssuranceLevel: '1.5',
}
- faraday.post(url, body, dynamic_headers).body
+ faraday.post(url, body, dynamic_headers) do |req|
+ req.options.context = { service_name: 'usps_enroll' }
+ end.body
end
# Makes HTTP request to retrieve proofing status
@@ -75,7 +81,9 @@ def request_proofing_results(unique_id, enrollment_code)
enrollmentCode: enrollment_code,
}
- faraday.post(url, body, dynamic_headers).body
+ faraday.post(url, body, dynamic_headers) do |req|
+ req.options.context = { service_name: 'usps_proofing_results' }
+ end.body
end
# Makes HTTP request to retrieve enrollment code
@@ -92,7 +100,9 @@ def request_enrollment_code(unique_id)
uniqueID: unique_id,
}
- faraday.post(url, body, dynamic_headers).body
+ faraday.post(url, body, dynamic_headers) do |req|
+ req.options.context = { service_name: 'usps_enrollment_code' }
+ end.body
end
# Makes a request to retrieve a new OAuth token
@@ -118,6 +128,9 @@ def faraday
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'
+
# Raise an error subclassing Faraday::Error on 4xx, 5xx, and malformed responses
# Note: The order of this matters for parsing the error response body.
conn.response :raise_error
@@ -160,7 +173,9 @@ def request_token
scope: 'ivs.ippaas.apis',
}
- faraday.post(url, body).body
+ faraday.post(url, body) do |req|
+ req.options.context = { service_name: 'usps_token' }
+ end.body
end
def root_url
@@ -178,5 +193,30 @@ def request_id
def request_headers
{ 'Content-Type' => 'application/json; charset=utf-8' }
end
+
+ def parse_facilities(facilities)
+ facilities['postOffices'].map do |post_office|
+ hours = {}
+ post_office['hours'].each do |hour_details|
+ hour_details.keys.each do |key|
+ hours[key] = hour_details[key]
+ end
+ end
+
+ PostOffice.new(
+ address: post_office['streetAddress'],
+ city: post_office['city'],
+ distance: post_office['distance'],
+ name: post_office['name'],
+ phone: post_office['phone'],
+ saturday_hours: hours['saturdayHours'],
+ state: post_office['state'],
+ sunday_hours: hours['sundayHours'],
+ weekday_hours: hours['weekdayHours'],
+ zip_code_4: post_office['zip4'],
+ zip_code_5: post_office['zip5'],
+ )
+ end
+ end
end
end
diff --git a/app/views/account_reset/pending/confirm.html.erb b/app/views/account_reset/pending/confirm.html.erb
index 4ef46167b20..872066ff582 100644
--- a/app/views/account_reset/pending/confirm.html.erb
+++ b/app/views/account_reset/pending/confirm.html.erb
@@ -3,6 +3,7 @@
<%= button_to(
account_reset_pending_cancel_path,
class: 'usa-button usa-button--wide usa-button--big margin-bottom-2',
+ method: :post,
) { t('forms.buttons.continue') } %>
<%= link_to(t('links.go_back'), account_reset_pending_path) %>
diff --git a/app/views/account_reset/request/show.html.erb b/app/views/account_reset/request/show.html.erb
index e324060b477..bfcceb2a086 100644
--- a/app/views/account_reset/request/show.html.erb
+++ b/app/views/account_reset/request/show.html.erb
@@ -23,6 +23,7 @@
<%= button_to(
account_reset_request_path,
class: 'usa-button usa-button--unstyled',
+ method: :post,
) { t('account_reset.request.yes_continue') } %>
<%= render PageFooterComponent.new do %>
diff --git a/app/views/idv/address/new.html.erb b/app/views/idv/address/new.html.erb
index ab18bf14353..7739d880393 100644
--- a/app/views/idv/address/new.html.erb
+++ b/app/views/idv/address/new.html.erb
@@ -69,5 +69,5 @@
<% end %>
<% end %>
-<%= render 'idv/doc_auth/back', step: 'verify' %>
+<%= render 'idv/shared/back', step: 'verify' %>
<%= javascript_packs_tag_once('formatted-fields') %>
diff --git a/app/views/idv/cancellations/destroy.html.erb b/app/views/idv/cancellations/destroy.html.erb
index fba5e2911c1..84a641f32f7 100644
--- a/app/views/idv/cancellations/destroy.html.erb
+++ b/app/views/idv/cancellations/destroy.html.erb
@@ -4,5 +4,5 @@
<% c.header { t('idv.cancel.headings.confirmation.hybrid') } %>
<%= t('doc_auth.instructions.switch_back') %>
- <%= image_tag(asset_url('idv/switch.png'), width: 193) %>
+ <%= image_tag(asset_url('idv/switch.png'), width: 193, alt: t('doc_auth.instructions.switch_back_image')) %>
<% end %>
diff --git a/app/views/idv/capture_doc/capture_complete.html.erb b/app/views/idv/capture_doc/capture_complete.html.erb
index 28c8ea90bcf..4199e3d5dcc 100644
--- a/app/views/idv/capture_doc/capture_complete.html.erb
+++ b/app/views/idv/capture_doc/capture_complete.html.erb
@@ -6,4 +6,4 @@
<%= render PageHeadingComponent.new.with_content(t('doc_auth.instructions.switch_back')) %>
-<%= image_tag(asset_url('idv/switch.png'), width: 193) %>
+<%= image_tag(asset_url('idv/switch.png'), width: 193, alt: t('doc_auth.instructions.switch_back_image')) %>
diff --git a/app/views/idv/doc_auth/link_sent.html.erb b/app/views/idv/doc_auth/link_sent.html.erb
index df657f97ec9..bb952544359 100644
--- a/app/views/idv/doc_auth/link_sent.html.erb
+++ b/app/views/idv/doc_auth/link_sent.html.erb
@@ -44,4 +44,4 @@
<%= javascript_packs_tag_once 'doc-capture-polling' %>
<% end %>
-<%= render 'idv/doc_auth/back', action: 'cancel_link_sent', class: 'link-sent-back-link' %>
+<%= render 'idv/shared/back', action: 'cancel_link_sent', class: 'link-sent-back-link' %>
diff --git a/app/views/idv/doc_auth/send_link.html.erb b/app/views/idv/doc_auth/send_link.html.erb
index edc17672ddb..be1365ce6ce 100644
--- a/app/views/idv/doc_auth/send_link.html.erb
+++ b/app/views/idv/doc_auth/send_link.html.erb
@@ -32,4 +32,4 @@
<%= t('forms.buttons.continue') %>
<% end %>
-<%= render 'idv/doc_auth/back', action: 'cancel_send_link' %>
+<%= render 'idv/shared/back', action: 'cancel_send_link' %>
diff --git a/app/views/idv/gpo/index.html.erb b/app/views/idv/gpo/index.html.erb
index 3c1b4b4adf0..f5a372195ea 100644
--- a/app/views/idv/gpo/index.html.erb
+++ b/app/views/idv/gpo/index.html.erb
@@ -41,4 +41,4 @@
],
) %>
-<%= render 'idv/doc_auth/back', fallback_path: @presenter.fallback_back_path %>
+<%= render 'idv/shared/back', fallback_path: @presenter.fallback_back_path %>
diff --git a/app/views/idv/in_person/address.html.erb b/app/views/idv/in_person/address.html.erb
index b18482c7587..869d2e68844 100644
--- a/app/views/idv/in_person/address.html.erb
+++ b/app/views/idv/in_person/address.html.erb
@@ -8,7 +8,9 @@
<%= t('in_person_proofing.body.address.info') %>
- <%= new_window_link_to(t('in_person_proofing.body.address.learn_more'), MarketingSite.security_and_privacy_practices_url) %>
+ <%# WILLFIX: Hiding the link below until help links are finalized; see LG-6875 %>
+ <%# i18n-tasks-use t('in_person_proofing.body.address.learn_more') %>
+ <%# new_window_link_to(t('in_person_proofing.body.address.learn_more'), MarketingSite.security_and_privacy_practices_url) %>
<%= simple_form_for(
@@ -91,4 +93,6 @@
<% end %>
<% end %>
+<%= render 'idv/doc_auth/cancel', step: 'address' %>
+
<%= javascript_packs_tag_once('formatted-fields') %>
diff --git a/app/views/idv/in_person/ready_to_verify/show.html.erb b/app/views/idv/in_person/ready_to_verify/show.html.erb
index dff4908268f..6e93663230c 100644
--- a/app/views/idv/in_person/ready_to_verify/show.html.erb
+++ b/app/views/idv/in_person/ready_to_verify/show.html.erb
@@ -22,7 +22,7 @@
<% end %>
<%= render AlertComponent.new(class: 'margin-y-4', text_tag: :div) do %>
- <%= t('in_person_proofing.body.barcode.deadline', deadline: @presenter.formatted_due_date) %>
+ <%= t('in_person_proofing.body.barcode.deadline', deadline: @presenter.formatted_due_date) %>
<%= t('in_person_proofing.body.barcode.deadline_restart') %>
<% end %>
@@ -32,18 +32,11 @@
<%= render ProcessListComponent.new(heading_level: :h3, class: 'margin-y-3') do |c| %>
<% c.item(heading: t('in_person_proofing.process.barcode.heading')) do %>
<%= t('in_person_proofing.process.barcode.info') %>
-
- <%= image_tag(
- @presenter.barcode_data_url,
- skip_pipeline: true,
- alt: t('in_person_proofing.process.barcode.image_alt'),
- class: 'display-block margin-bottom-1',
- ) %>
-
- <%= t('in_person_proofing.process.barcode.caption_label') %>:
- <%= @presenter.formatted_enrollment_code %>
-
-
+ <%= render BarcodeComponent.new(
+ barcode_data: @presenter.enrollment_code,
+ label: t('in_person_proofing.process.barcode.caption_label'),
+ label_formatter: Idv::InPerson::EnrollmentCodeFormatter.method(:format),
+ ) %>
<% end %>
<% c.item(heading: t('in_person_proofing.process.state_id.heading')) do %>
<%= t('in_person_proofing.process.state_id.info') %>
@@ -66,8 +59,9 @@
<% end %>
<% end %>
- <%= t('in_person_proofing.body.barcode.items_to_bring_questions') %>
- <%= new_window_link_to(
+ <%# WILLFIX: Hiding this text and link until help links are finalized; see LG-6875 %>
+ <%# t('in_person_proofing.body.barcode.items_to_bring_questions') %>
+ <%# new_window_link_to(
t('in_person_proofing.body.barcode.learn_more'),
MarketingSite.help_center_article_url(
category: 'verify-your-identity',
@@ -77,29 +71,29 @@
-
-
- <%= @presenter.selected_location_details['name'] %>
-
- <%= @presenter.selected_location_details['streetAddress'] %>
- <%= @presenter.selected_location_details['city'] %>,
- <%= @presenter.selected_location_details['state'] %>
- <%= @presenter.selected_location_details['zip5'] %>-<%= @presenter.selected_location_details['zip4'] %>
-
- <%= t('in_person_proofing.body.barcode.retail_hours') %>
-
- <%= t('date.range', from: t('date.day_names')[0], to: t('date.day_names')[4]) %>: <%= @presenter.selected_location_hours(:weekday) %>
- <%= t('date.day_names')[5] %>: <%= @presenter.selected_location_hours(:saturday) %>
- <%= t('date.day_names')[6] %>: <%= @presenter.selected_location_hours(:sunday) %>
-
-
- <%= t('in_person_proofing.body.barcode.retail_phone_label') %>:
- <%= @presenter.selected_location_details['phone'] %>
-
-
-
+<% if @presenter.selected_location_details.present? %>
+
+
+ <%= @presenter.selected_location_details['name'] %>
+
+ <%= @presenter.selected_location_details['street_address'] %>
+ <%= @presenter.selected_location_details['formatted_city_state_zip'] %>
+
+ <%= t('in_person_proofing.body.barcode.retail_hours') %>
+
+ <%= t('date.range', from: t('date.day_names')[0], to: t('date.day_names')[4]) %>: <%= @presenter.selected_location_hours(:weekday) %>
+ <%= t('date.day_names')[5] %>: <%= @presenter.selected_location_hours(:saturday) %>
+ <%= t('date.day_names')[6] %>: <%= @presenter.selected_location_hours(:sunday) %>
+
+
+ <%= t('in_person_proofing.body.barcode.retail_phone_label') %>:
+ <%= @presenter.selected_location_details['phone'] %>
+
+
+
-<%= t('in_person_proofing.body.barcode.speak_to_associate') %>
+ <%= t('in_person_proofing.body.barcode.speak_to_associate') %>
+<% end %>
<%= t('in_person_proofing.body.barcode.deadline', deadline: @presenter.formatted_due_date) %>
diff --git a/app/views/idv/in_person/state_id.html.erb b/app/views/idv/in_person/state_id.html.erb
index 41bbf53f68d..40da7642b1f 100644
--- a/app/views/idv/in_person/state_id.html.erb
+++ b/app/views/idv/in_person/state_id.html.erb
@@ -89,3 +89,5 @@
<% end %>
<% end %>
<% end %>
+
+<%= render 'idv/doc_auth/cancel', step: 'state_id' %>
diff --git a/app/views/idv/session_errors/exception.html.erb b/app/views/idv/session_errors/exception.html.erb
index 9a88749d3b4..69dece31929 100644
--- a/app/views/idv/session_errors/exception.html.erb
+++ b/app/views/idv/session_errors/exception.html.erb
@@ -4,7 +4,7 @@
heading: t('idv.failure.sessions.exception'),
action: {
text: t('idv.failure.button.warning'),
- url: idv_doc_auth_path,
+ url: @try_again_path,
},
options: [
{
diff --git a/app/views/idv/session_errors/warning.html.erb b/app/views/idv/session_errors/warning.html.erb
index 34c7372d58d..34a91cf6760 100644
--- a/app/views/idv/session_errors/warning.html.erb
+++ b/app/views/idv/session_errors/warning.html.erb
@@ -7,7 +7,7 @@
<%= t('idv.failure.attempts', count: @remaining_attempts) %>
<% c.action_button(
- action: ->(**tag_options, &block) { link_to(idv_doc_auth_path, **tag_options, &block) },
+ action: ->(**tag_options, &block) { link_to(@try_again_path, **tag_options, &block) },
) { t('idv.forgot_password.try_again') } %>
<% c.troubleshooting_options do |tc| %>
diff --git a/app/views/idv/doc_auth/_back.html.erb b/app/views/idv/shared/_back.html.erb
similarity index 84%
rename from app/views/idv/doc_auth/_back.html.erb
rename to app/views/idv/shared/_back.html.erb
index 3f5d0ef5b6f..88f45338b35 100644
--- a/app/views/idv/doc_auth/_back.html.erb
+++ b/app/views/idv/shared/_back.html.erb
@@ -5,13 +5,15 @@ path can be passed in case the HTTP header is not specified or is invalid. If no
yield a useable URL, nothing will be rendered.
locals:
+* step_url: (Optional) Base target for flow step URL calls, falls back to @step_url.
* action: (Optional) Flow action to call to return to the previous step.
* step: (Optional) Name of step to which user should be returned.
* fallback_path: (Optional) Path to redirect absent action, step, and HTTP referer.
%>
<% text = '‹ ' + t('forms.buttons.back')
+ step_url = local_assigns[:step_url] || @step_url
step = local_assigns[:action] || local_assigns[:step]
- path = step ? idv_doc_auth_step_path(step: step) : go_back_path
+ path = (step_url && step) ? send(step_url, step: step) : go_back_path
path ||= local_assigns[:fallback_path]
classes = []
classes << local_assigns[:class] if local_assigns[:class] %>
diff --git a/app/views/idv/shared/_ssn.html.erb b/app/views/idv/shared/_ssn.html.erb
index ffef9a165ce..a79f37fc4a4 100644
--- a/app/views/idv/shared/_ssn.html.erb
+++ b/app/views/idv/shared/_ssn.html.erb
@@ -60,7 +60,7 @@ locals:
<% end %>
<% if updating_ssn %>
- <%= render 'idv/doc_auth/back', action: 'cancel_update_ssn' %>
+ <%= render 'idv/shared/back', action: 'cancel_update_ssn' %>
<% else %>
<%= render 'idv/doc_auth/cancel', step: 'ssn' %>
<% end %>
diff --git a/app/views/layouts/user_mailer.html.erb b/app/views/layouts/user_mailer.html.erb
index f695ac2cfe6..243a7b28278 100644
--- a/app/views/layouts/user_mailer.html.erb
+++ b/app/views/layouts/user_mailer.html.erb
@@ -71,8 +71,11 @@
- <%= @header || message.subject %>
- <%= yield %>
+ <% unless @hide_title %>
+ <%= @header || message.subject %>
+
+ <% end %>
+ <%= yield %>
diff --git a/app/views/shared/_in-person-verification-results-email-lower.html.erb b/app/views/shared/_in-person-verification-results-email-lower.html.erb
new file mode 100644
index 00000000000..3ebe6dca242
--- /dev/null
+++ b/app/views/shared/_in-person-verification-results-email-lower.html.erb
@@ -0,0 +1,38 @@
+
+
+
+ <%= link_to idv_url, idv_url, target: '_blank', rel: 'noopener' %>
+
+
+
+
+ <%= t(
+ 'user_mailer.in_person_verified.warning_contact_us_html',
+ contact_us_url: MarketingSite.contact_url,
+ sign_in_url: idv_url,
+ )
+ %>
+
diff --git a/app/views/user_mailer/in_person_failed.html.erb b/app/views/user_mailer/in_person_failed.html.erb
new file mode 100644
index 00000000000..f34e8718abb
--- /dev/null
+++ b/app/views/user_mailer/in_person_failed.html.erb
@@ -0,0 +1,20 @@
+
+ <%= t('user_mailer.in_person_verified.greeting') %>
+
+ <%= t(
+ 'user_mailer.in_person_failed.intro',
+ location: @presenter.location_name,
+ date: @presenter.formatted_verified_date,
+ ) %>
+
+
+ <%= t('user_mailer.in_person_failed.body', app_name: APP_NAME) %>
+
+
<%= t('user_mailer.in_person_failed.verifying_identity') %>
+
+ <%= t('user_mailer.in_person_failed.verifying_step_not_expired') %>
+ <%= t('user_mailer.in_person_failed.verifying_step_proof_of_address') %>
+
+
+
+<%= render 'shared/in-person-verification-results-email-lower' %>
diff --git a/app/views/user_mailer/in_person_ready_to_verify.html.erb b/app/views/user_mailer/in_person_ready_to_verify.html.erb
new file mode 100644
index 00000000000..f12c97a82b0
--- /dev/null
+++ b/app/views/user_mailer/in_person_ready_to_verify.html.erb
@@ -0,0 +1,101 @@
+
+ <%= t('user_mailer.in_person_ready_to_verify.greeting', name: @first_name) %>
+ <%= t('user_mailer.in_person_ready_to_verify.intro') %>
+
+
+
+
+
+ <%= image_tag('email/info.png', width: 16, height: 16, alt: '') %>
+
+
+ <%= t('in_person_proofing.body.barcode.deadline', deadline: @presenter.formatted_due_date) %>
+ <%= t('in_person_proofing.body.barcode.deadline_restart') %>
+
+
+
+
+
+
+ <%= t('in_person_proofing.body.barcode.items_to_bring') %>
+
+
+
+ 1
+
+ <%= t('in_person_proofing.process.barcode.heading') %>
+ <%= t('in_person_proofing.process.barcode.info') %>
+ <%= render BarcodeComponent.new(
+ barcode_data: @presenter.enrollment_code,
+ label: nil,
+ label_formatter: Idv::InPerson::EnrollmentCodeFormatter.method(:format),
+ ) %>
+
+
+
+ 2
+
+ <%= t('in_person_proofing.process.state_id.heading') %>
+ <%= t('in_person_proofing.process.state_id.info') %>
+
+ <% t('in_person_proofing.process.state_id.acceptable_documents').each do |document| %>
+ <%= document %>
+ <% end %>
+
+ <%= t('in_person_proofing.process.state_id.no_other_documents') %>
+
+
+ <% if @presenter.needs_proof_of_address? %>
+
+ 3
+
+ <%= t('in_person_proofing.process.proof_of_address.heading') %>
+ <%= t('in_person_proofing.process.proof_of_address.info') %>
+
+ <% t('in_person_proofing.process.proof_of_address.acceptable_proof').each do |proof| %>
+ <%= proof %>
+ <% end %>
+
+
+
+ <% end %>
+
+
+ <%# WILLFIX: Hiding this text and link until help links are finalized; see LG-6875 %>
+ <%# i18n-tasks-use t('in_person_proofing.body.barcode.items_to_bring_questions') %>
+ <%# t('in_person_proofing.body.barcode.items_to_bring_questions') %>
+ <%# link_to(
+ t('in_person_proofing.body.barcode.learn_more'),
+ MarketingSite.help_center_article_url(
+ category: 'verify-your-identity',
+ article: 'how-to-verify-in-person',
+ ),
+ ) %>
+
+
+
+<% if @presenter.selected_location_details.present? %>
+
+
<%= @presenter.selected_location_details['name'] %>
+
+ <%= @presenter.selected_location_details['street_address'] %>
+ <%= @presenter.selected_location_details['formatted_city_state_zip'] %>
+
+
<%= t('in_person_proofing.body.barcode.retail_hours') %>
+
+ <%= t('date.range', from: t('date.day_names')[0], to: t('date.day_names')[4]) %>: <%= @presenter.selected_location_hours(:weekday) %>
+ <%= t('date.day_names')[5] %>: <%= @presenter.selected_location_hours(:saturday) %>
+ <%= t('date.day_names')[6] %>: <%= @presenter.selected_location_hours(:sunday) %>
+
+
+ <%= @presenter.selected_location_details[:phone] %>
+
+
+<% end %>
+
+<%= t('in_person_proofing.body.barcode.speak_to_associate') %>
+
+
+ <%= t('in_person_proofing.body.barcode.deadline', deadline: @presenter.formatted_due_date) %>
+ <%= t('in_person_proofing.body.barcode.no_appointment_required') %>
+
diff --git a/app/views/user_mailer/in_person_verified.html.erb b/app/views/user_mailer/in_person_verified.html.erb
new file mode 100644
index 00000000000..8ffd02a9cce
--- /dev/null
+++ b/app/views/user_mailer/in_person_verified.html.erb
@@ -0,0 +1,19 @@
+<%= image_tag(
+ 'email/user-signup-ial2.png',
+ width: 140,
+ height: 177,
+ alt: '',
+ class: 'float-center padding-bottom-4',
+ ) %>
+<%= message.subject %>
+
+ <%= t('user_mailer.in_person_verified.greeting') %>
+ <%= t(
+ 'user_mailer.in_person_verified.intro',
+ location: @presenter.location_name,
+ date: @presenter.formatted_verified_date,
+ ) %>
+ <%= t('user_mailer.in_person_verified.next_sign_in', app_name: APP_NAME) %>
+
+
+<%= render 'shared/in-person-verification-results-email-lower' %>
diff --git a/app/views/users/authorization_confirmation/new.html.erb b/app/views/users/authorization_confirmation/new.html.erb
index 8e4287bc166..d13ab57eb32 100644
--- a/app/views/users/authorization_confirmation/new.html.erb
+++ b/app/views/users/authorization_confirmation/new.html.erb
@@ -30,7 +30,7 @@
- <%= button_to(user_authorization_confirmation_path, class: 'usa-button usa-button--big usa-button--wide') { t('user_authorization_confirmation.continue') } %>
+ <%= button_to(user_authorization_confirmation_path, class: 'usa-button usa-button--big usa-button--wide', method: :post) { t('user_authorization_confirmation.continue') } %>
<%= t('user_authorization_confirmation.or') %>
diff --git a/app/views/vendor_outage/show.html.erb b/app/views/vendor_outage/show.html.erb
index 99317898f20..f204a6f3a56 100644
--- a/app/views/vendor_outage/show.html.erb
+++ b/app/views/vendor_outage/show.html.erb
@@ -21,4 +21,4 @@
<%= @specific_message %>
<% end %>
-<%= render('idv/doc_auth/back') %>
+<%= render('idv/shared/back') %>
diff --git a/config/application.rb b/config/application.rb
index 21855158294..703d4ac6ce7 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -48,7 +48,9 @@ class Application < Rails::Application
end
end
- config.load_defaults '6.1'
+ config.load_defaults '7.0'
+ # Delete after deploying once
+ config.active_support.cache_format_version = 6.1
config.active_record.belongs_to_required_by_default = false
config.active_record.legacy_connection_handling = false
config.assets.unknown_asset_fallback = true
@@ -78,12 +80,6 @@ class Application < Rails::Application
config.time_zone = 'UTC'
- # Generate CSRF tokens that are encoded in URL-safe Base64.
- #
- # This change is not backwards compatible with earlier Rails versions.
- # It's best enabled when your entire app is migrated and stable on 6.1.
- Rails.application.config.action_controller.urlsafe_csrf_tokens = false
-
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{yml}')]
config.i18n.available_locales = %w[en es fr]
config.i18n.default_locale = :en
diff --git a/config/application.yml.default b/config/application.yml.default
index d0705f770a4..c8f835fddda 100644
--- a/config/application.yml.default
+++ b/config/application.yml.default
@@ -141,6 +141,15 @@ lexisnexis_trueid_liveness_nocropping_workflow: customers.gsa.trueid.workflow
lexisnexis_trueid_noliveness_cropping_workflow: customers.gsa.trueid.workflow
lexisnexis_trueid_noliveness_nocropping_workflow: customers.gsa.trueid.workflow
###################################################################
+# LexisNexis ThreatMetrix ##########################################
+lexisnexis_threatmetrix_base_url: https://www.example.com
+lexisnexis_threatmetrix_request_mode: testing
+lexisnexis_threatmetrix_account_id: test_account
+lexisnexis_threatmetrix_username: test_username
+lexisnexis_threatmetrix_password: test_password
+lexisnexis_threatmetrix_instant_verify_timeout: 1.0
+lexisnexis_threatmetrix_instant_verify_workflow: customers.gsa.instant.verify.workflow
+###################################################################
lockout_period_in_minutes: 10
log_to_stdout: false
logins_per_email_and_ip_bantime: 60
@@ -187,6 +196,8 @@ piv_cac_verify_token_url: https://localhost:8443/
platform_authentication_enabled: true
poll_rate_for_verify_in_seconds: 3
proofer_mock_fallback: true
+proofing_device_profiling_collecting_enabled: false
+proofing_device_profiling_decisioning_enabled: false
proofing_send_partial_dob: false
proof_address_max_attempts: 5
proof_address_max_attempt_window_in_minutes: 360
@@ -272,6 +283,7 @@ voice_otp_speech_rate: 'slow'
voip_check: true
voip_block: true
voip_allowed_phones: '[]'
+inherited_proofing_va_base_url: 'https://staging-api.va.gov'
development:
aamva_private_key: 123abc
@@ -371,8 +383,6 @@ production:
doc_auth_vendor_randomize_percent: 0
doc_auth_vendor_randomize_alternate_vendor: ''
domain_name: login.gov
- enable_numeric_authentication_otp: false
- enable_numeric_authentication_otp_input: false
enable_test_routes: false
enable_usps_verification: false
hmac_fingerprinter_key:
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 19b5f8d9c59..711fe6db86a 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -55,6 +55,7 @@
:webauthn_configurations,
:email_addresses,
:proofing_component,
+ :account_reset_request,
].each do |association|
Bullet.add_safelist(type: :n_plus_one_query, class_name: 'User', association: association)
end
diff --git a/config/initializers/secure_headers.rb b/config/initializers/secure_headers.rb
index 8ce946d7589..8f9769790f4 100644
--- a/config/initializers/secure_headers.rb
+++ b/config/initializers/secure_headers.rb
@@ -5,7 +5,7 @@
}
config.action_dispatch.default_headers.merge!(
- 'X-Frame-Options' => 'DENY',
+ 'X-Frame-Options' => IdentityConfig.store.rails_mailer_previews_enabled ? 'SAMEORIGIN' : 'DENY',
'X-XSS-Protection' => '1; mode=block',
'X-Download-Options' => 'noopen',
)
diff --git a/config/ipp_pilot_usps_facilities.json b/config/ipp_pilot_usps_facilities.json
new file mode 100644
index 00000000000..249d09ca2bc
--- /dev/null
+++ b/config/ipp_pilot_usps_facilities.json
@@ -0,0 +1,158 @@
+{
+ "postOffices": [
+ {
+ "parking": "Lot",
+ "hours": [
+ {
+ "weekdayHours": "8:30 AM - 7:00 PM"
+ },
+ {
+ "saturdayHours": "8:30 AM - 5:00 PM"
+ },
+ {
+ "sundayHours": "Closed"
+ }
+ ],
+ "distance": "0.0 mi",
+ "streetAddress": "900 E FAYETTE ST RM 118",
+ "city": "BALTIMORE",
+ "phone": "410-347-4202",
+ "name": "BALTIMORE",
+ "zip4": "9715",
+ "state": "MD",
+ "zip5": "21233"
+ },
+ {
+ "parking": "Street",
+ "hours": [
+ {
+ "weekdayHours": "9:00 AM - 5:00 PM"
+ },
+ {
+ "saturdayHours": "9:00 AM - 4:00 PM"
+ },
+ {
+ "sundayHours": "Closed"
+ }
+ ],
+ "distance": "0.0 mi",
+ "streetAddress": "6900 WISCONSIN AVE STE 100",
+ "city": "CHEVY CHASE",
+ "phone": "301-941-2670",
+ "name": "BETHESDA",
+ "zip4": "9996",
+ "state": "MD",
+ "zip5": "20815"
+ },
+ {
+ "parking": "Lot",
+ "hours": [
+ {
+ "weekdayHours": "8:00 AM - 6:00 PM"
+ },
+ {
+ "saturdayHours": "8:00 AM - 4:00 PM"
+ },
+ {
+ "sundayHours": "10:00 AM - 4:00 PM"
+ }
+ ],
+ "distance": "0.0 mi",
+ "streetAddress": "4005 WISCONSIN AVE NW",
+ "city": "WASHINGTON",
+ "phone": "202-842-3332",
+ "name": "FRIENDSHIP",
+ "zip4": "9997",
+ "state": "DC",
+ "zip5": "20016"
+ },
+ {
+ "parking": "Lot",
+ "hours": [
+ {
+ "weekdayHours": "9:00 AM - 5:00 PM"
+ },
+ {
+ "saturdayHours": "9:00 AM - 4:00 PM"
+ },
+ {
+ "sundayHours": "Closed"
+ }
+ ],
+ "distance": "0.0 mi",
+ "streetAddress": "900 BRENTWOOD RD NE",
+ "city": "WASHINGTON",
+ "phone": "202-636-1259",
+ "name": "WASHINGTON",
+ "zip4": "9998",
+ "state": "DC",
+ "zip5": "20066"
+ },
+ {
+ "parking": "Street",
+ "hours": [
+ {
+ "weekdayHours": "9:00 AM - 5:00 PM"
+ },
+ {
+ "saturdayHours": "9:00 AM - 1:00 PM"
+ },
+ {
+ "sundayHours": "Closed"
+ }
+ ],
+ "distance": "0.0 mi",
+ "streetAddress": "3118 WASHINGTON BLVD",
+ "city": "ARLINGTON",
+ "phone": "703-993-0072",
+ "name": "ARLINGTON",
+ "zip4": "9998",
+ "state": "VA",
+ "zip5": "22201"
+ },
+ {
+ "parking": "Lot",
+ "hours": [
+ {
+ "weekdayHours": "9:00 AM - 6:00 PM"
+ },
+ {
+ "saturdayHours": "9:00 AM - 1:00 PM"
+ },
+ {
+ "sundayHours": "Closed"
+ }
+ ],
+ "distance": "0.0 mi",
+ "streetAddress": "44715 PRENTICE DR",
+ "city": "DULLES",
+ "phone": "703-406-6291",
+ "name": "ASHBURN",
+ "zip4": "9996",
+ "state": "VA",
+ "zip5": "20101"
+ },
+ {
+ "parking": "Lot",
+ "hours": [
+ {
+ "weekdayHours": "9:00 AM - 8:00 PM"
+ },
+ {
+ "saturdayHours": "9:00 AM - 5:00 PM"
+ },
+ {
+ "sundayHours": "9:00 AM - 5:00 PM"
+ }
+ ],
+ "distance": "0.0 mi",
+ "streetAddress": "8409 LEE HWY",
+ "city": "MERRIFIELD",
+ "phone": "703-698-6377",
+ "name": "MERRIFIELD",
+ "zip4": "9998",
+ "state": "VA",
+ "zip5": "22116"
+ }
+ ]
+}
diff --git a/config/locales/components/en.yml b/config/locales/components/en.yml
index d197121dd90..94915725810 100644
--- a/config/locales/components/en.yml
+++ b/config/locales/components/en.yml
@@ -1,6 +1,8 @@
---
en:
components:
+ barcode:
+ table_label: Barcode
clipboard_button:
label: Copy
javascript_required:
diff --git a/config/locales/components/es.yml b/config/locales/components/es.yml
index 11f564c2f78..cec7ec4d61c 100644
--- a/config/locales/components/es.yml
+++ b/config/locales/components/es.yml
@@ -1,6 +1,8 @@
---
es:
components:
+ barcode:
+ table_label: Código de barras
clipboard_button:
label: Copiar
javascript_required:
diff --git a/config/locales/components/fr.yml b/config/locales/components/fr.yml
index 5011d86f66f..62f05681bf4 100644
--- a/config/locales/components/fr.yml
+++ b/config/locales/components/fr.yml
@@ -1,6 +1,8 @@
---
fr:
components:
+ barcode:
+ table_label: Code-barres
clipboard_button:
label: Copier
javascript_required:
diff --git a/config/locales/doc_auth/en.yml b/config/locales/doc_auth/en.yml
index a53767be631..07f9ad760f7 100644
--- a/config/locales/doc_auth/en.yml
+++ b/config/locales/doc_auth/en.yml
@@ -216,6 +216,7 @@ en:
send_sms: We’ll send a text message to your device with a link. Follow that
link to your browser to take photos of the front and back of your ID.
switch_back: Switch back to your computer to finish verifying your identity.
+ switch_back_image: Arrow pointing from phone to computer
test_ssn: In the test environment only SSNs that begin with “900-” or “666-” are
considered valid. Do not enter real PII in this field.
text1a: such as a phone or computer.
diff --git a/config/locales/doc_auth/es.yml b/config/locales/doc_auth/es.yml
index 37571454714..d1801b37386 100644
--- a/config/locales/doc_auth/es.yml
+++ b/config/locales/doc_auth/es.yml
@@ -252,6 +252,7 @@ es:
trasera de su identificación.
switch_back: Regrese a su computadora para continuar con la verificación de su
identidad.
+ switch_back_image: Flecha que apunta del teléfono a la computadora
test_ssn: En el entorno de prueba solo los SSN que comienzan con “900-” o “666-”
se consideran válidos. No ingrese PII real en este campo.
text1a: como un teléfono o una computadora.
diff --git a/config/locales/doc_auth/fr.yml b/config/locales/doc_auth/fr.yml
index 49511596b52..63fda376067 100644
--- a/config/locales/doc_auth/fr.yml
+++ b/config/locales/doc_auth/fr.yml
@@ -267,6 +267,7 @@ fr:
ce lien vers votre navigateur pour prendre des photos du recto et du
verso de votre identifiant.
switch_back: Retournez sur votre ordinateur pour continuer à vérifier votre identité.
+ switch_back_image: Flèche pointant du téléphone vers l’ordinateur
test_ssn: Dans l’environnement de test seuls les SSN commençant par “900-” ou
“900-” sont considérés comme valides. N’entrez pas de vrais PII dans ce
champ.
diff --git a/config/locales/idv/es.yml b/config/locales/idv/es.yml
index 76750bd4c96..74460ccaee2 100644
--- a/config/locales/idv/es.yml
+++ b/config/locales/idv/es.yml
@@ -6,10 +6,10 @@ es:
%{last_number}
buttons:
cancel: Cancele y regrese a su perfil
- change_address_label: Cambiar dirección
- change_label: Cambio
- change_ssn_label: Cambiar número de Seguridad Social
- change_state_id_label: Cambiar ID de estado
+ change_address_label: Actualizar su dirección actual
+ change_label: Actualizar
+ change_ssn_label: Actualizar su número de la Seguridad Social
+ change_state_id_label: Actualizar la información de su cédula de identidad
continue_plain: Continuar
mail:
resend: Enviar otra carta
@@ -106,7 +106,7 @@ es:
city: Ciudad
dob: Fecha de nacimiento
first_name: Nombre de pila
- id_number: Número de identificación
+ id_number: Número de cédula
last_name: Apellido
password: Contraseña
ssn: Número de seguridad social
@@ -190,7 +190,7 @@ es:
review: Vuelve a ingresar tu contraseña de %{app_name} para encriptar tus datos
troubleshooting:
headings:
- are_you_near: '¿Se encuentra cerca de Washington D. C.?'
+ are_you_near: '¿Se encuentra cerca de la ciudad de Washington?'
missing_required_items: '¿Le falta alguno de estos puntos?'
need_assistance: '¿Necesita ayuda inmediata? Así es como puede obtener ayuda:'
still_having_trouble: '¿Sigue teniendo dificultades?'
@@ -208,8 +208,7 @@ es:
supported_documents: Vea la lista de documentos de identidad emitidos por el
estado que son aceptados
verify_by_mail: Verifique su dirección por correo
- verify_in_person: Verifique su identificación de manera presencial en una
- oficina de correos
+ verify_in_person: Verifique su cédula de identidad en persona en una oficina de correos
welcome:
no_js_header: Debe habilitar JavaScript para verificar su identidad.
no_js_intro: '%{sp_name} requiere que usted verifique su identidad. Debe
diff --git a/config/locales/idv/fr.yml b/config/locales/idv/fr.yml
index 2d9db2c6a10..1ab7443d94c 100644
--- a/config/locales/idv/fr.yml
+++ b/config/locales/idv/fr.yml
@@ -6,10 +6,10 @@ fr:
%{last_number}
buttons:
cancel: Annuler et retourner à votre profil
- change_address_label: Changement d’adresse
- change_label: Changement
- change_ssn_label: Changement de numéro de sécurité sociale
- change_state_id_label: Modifier l’ID d’état
+ change_address_label: Mettre à jour votre adresse actuelle
+ change_label: Mettre à jour
+ change_ssn_label: Mettre à jour votre numéro de Sécurité Sociale
+ change_state_id_label: Mettre à jour les informations figurant sur votre document d’identité
continue_plain: Continuer
mail:
resend: Envoyer une autre lettre
@@ -117,7 +117,7 @@ fr:
ssn: Numéro de sécurité sociale
ssn_label_html: Numéro de sécurité sociale
state: État
- zipcode: Code ZIP
+ zipcode: Code postal
index:
id:
need_html: Si vous créez un compte, vous aurez besoin d’un
identifiant
@@ -203,7 +203,7 @@ fr:
review: Entrez à nouveau votre mot de passe %{app_name} pour crypter vos données
troubleshooting:
headings:
- are_you_near: Êtes-vous près de Washington, D.C.?
+ are_you_near: Êtes-vous situé près de Washington, D.C.?
missing_required_items: Est-ce qu’il vous manque un de ces éléments?
need_assistance: 'Avez-vous besoin d’une assistance immédiate? Voici comment
obtenir de l’aide:'
@@ -221,7 +221,7 @@ fr:
learn_more_verify_in_person: En savoir plus sur la vérification en personne
supported_documents: Voir la liste des pièces d’identité acceptées et délivrées par l’État
verify_by_mail: Vérifiez plutôt votre adresse par courrier
- verify_in_person: Vérifiez votre identité en personne dans un bureau de poste
+ verify_in_person: Vérifier votre identité en personne dans un bureau de poste
welcome:
no_js_header: Vous devez activer JavaScript pour vérifier votre identité.
no_js_intro: '%{sp_name} a besoin de vous pour vérifier votre identité. Vous
diff --git a/config/locales/in_person_proofing/en.yml b/config/locales/in_person_proofing/en.yml
index faf2ce8e0e4..ae4b35e8de7 100644
--- a/config/locales/in_person_proofing/en.yml
+++ b/config/locales/in_person_proofing/en.yml
@@ -21,6 +21,17 @@ en:
retail_phone_label: Phone number
speak_to_associate: You can speak with any retail associate at this Post Office
to verify your identity.
+ location:
+ location_button: Select
+ location_step_about: If you are having trouble adding your ID, you may be able
+ to verify in person at a local United States Post Office in select
+ locations.
+ none_found: No locations found.
+ post_office: 'Post Office ™'
+ retail_hours_heading: Retail Hours
+ retail_hours_sat: 'Sat:'
+ retail_hours_sun: 'Sun:'
+ retail_hours_weekday: 'Monday to Friday:'
prepare:
alert_selected_post_office: 'You’ve selected the %{name} Post Office.'
bring_barcode_header: A copy of your barcode
@@ -77,13 +88,13 @@ en:
location: Select a location to verify your ID
prepare: Verify your identity in person
state_id: Enter the information on your ID
+ switch_back: Switch back to your computer to prepare to verify your identity in person
update_address: Update your current address
update_state_id: Update the information on your ID
process:
barcode:
caption_label: Enrollment code
heading: A copy of your barcode
- image_alt: Barcode
info: Print or scan from your mobile device.
proof_of_address:
acceptable_proof:
diff --git a/config/locales/in_person_proofing/es.yml b/config/locales/in_person_proofing/es.yml
index 2546488f7e5..e1df11062d8 100644
--- a/config/locales/in_person_proofing/es.yml
+++ b/config/locales/in_person_proofing/es.yml
@@ -18,11 +18,22 @@ es:
learn_more: Más información
location_details: Detalles de la ubicación
no_appointment_required: No es necesario pedir cita.
- retail_hours: Heures de vente au détail
+ retail_hours: Horario de atención al público
retail_hours_closed: Cerrado
retail_phone_label: Número de teléfono
speak_to_associate: Puedes hablar con cualquier socio de ventas en esta oficina
de correos para verificar tu identidad.
+ location:
+ location_button: Seleccionar
+ location_step_about: Si tiene problemas para añadir su cédula de identidad, es
+ posible que pueda realizar la verificación en persona en una oficina
+ de correos local de los Estados Unidos en determinadas localidades.
+ none_found: No se encontraron localidades.
+ post_office: Oficina de Correos
+ retail_hours_heading: Horario de atención al público
+ retail_hours_sat: 'Sáb:'
+ retail_hours_sun: 'Dom:'
+ retail_hours_weekday: 'De Lunes a Viernes:'
prepare:
alert_selected_post_office: 'Ha seleccionado la oficina de correos %{name}.'
bring_barcode_header: Una copia de su código de barras
@@ -81,13 +92,14 @@ es:
location: Seleccione un lugar para verificar su cédula
prepare: Verifique su identidad en persona
state_id: Ingrese la información de su cédula
+ switch_back: Vuelva a su computadora para prepararse para verificar su identidad
+ en persona
update_address: Actualizar su dirección actual
update_state_id: Actualizar la información de su cédula de identidad
process:
barcode:
caption_label: Código de registro
heading: Una copia de su código de barras
- image_alt: Código de barras
info: Imprima o escanee desde su dispositivo móvil.
proof_of_address:
acceptable_proof:
diff --git a/config/locales/in_person_proofing/fr.yml b/config/locales/in_person_proofing/fr.yml
index 0e333ff59f4..7db2b9364fa 100644
--- a/config/locales/in_person_proofing/fr.yml
+++ b/config/locales/in_person_proofing/fr.yml
@@ -24,6 +24,17 @@ fr:
retail_phone_label: Numéro de téléphone
speak_to_associate: Vous pouvez vous adresser à n’importe quel employé de ce
bureau de poste pour faire vérifier votre identité.
+ location:
+ location_button: Sélectionner
+ location_step_about: Si vous ne parvenez pas à ajouter votre pièce d’identité,
+ vous pourrez peut-être vérifier en personne dans un bureau de poste
+ américain local dans certains endroits.
+ none_found: Aucun endroit trouvé.
+ post_office: Bureau de Poste
+ retail_hours_heading: Heures de vente au détail
+ retail_hours_sat: 'Sam:'
+ retail_hours_sun: 'Dim:'
+ retail_hours_weekday: 'Lundi à Vendredi:'
prepare:
alert_selected_post_office: 'Vous avez sélectionné le bureau de poste de %{name}.'
bring_barcode_header: Une copie de votre code-barres
@@ -85,13 +96,14 @@ fr:
location: Sélectionnez un lieu pour vérifier votre identité
prepare: Vérifiez votre identité en personne
state_id: Saisissez les informations figurant sur votre document d’identité
+ switch_back: Retournez sur votre ordinateur pour vous préparer à vérifier votre
+ identité en personne
update_address: Mettre à jour votre adresse actuelle
update_state_id: Mettre à jour les informations figurant sur votre document d’identité
process:
barcode:
caption_label: Code d’inscription
heading: Une copie de votre code-barres
- image_alt: Code-barres
info: Imprimez ou numérisez depuis votre appareil mobile.
proof_of_address:
acceptable_proof:
diff --git a/config/locales/step_indicator/es.yml b/config/locales/step_indicator/es.yml
index 2cf963a9360..ba1363dce93 100644
--- a/config/locales/step_indicator/es.yml
+++ b/config/locales/step_indicator/es.yml
@@ -4,9 +4,9 @@ es:
accessible_label: Progreso por pasos
flows:
idv:
- find_a_post_office: Encontrar una Oficina de Correos
+ find_a_post_office: Encontrar una oficina de correos
getting_started: Inicio
- go_to_the_post_office: Ir a la Oficina de Correos
+ go_to_the_post_office: Ir a la oficina de correos
secure_account: Proteje tu cuenta
verify_id: Verifica tu identificación
verify_info: Verifica tus datos personales
diff --git a/config/locales/step_indicator/fr.yml b/config/locales/step_indicator/fr.yml
index 2b6af4f389f..fa93938adeb 100644
--- a/config/locales/step_indicator/fr.yml
+++ b/config/locales/step_indicator/fr.yml
@@ -4,9 +4,9 @@ fr:
accessible_label: Progression par étapes
flows:
idv:
- find_a_post_office: Trouver un Bureau de Poste
+ find_a_post_office: Trouver un bureau de poste
getting_started: Démarrer
- go_to_the_post_office: Aller au Bureau de Poste
+ go_to_the_post_office: Se rendre au bureau de poste
secure_account: Sécurisez votre compte
verify_id: Vérifier votre identité
verify_info: Vérifier vos données personnelles
diff --git a/config/locales/user_mailer/en.yml b/config/locales/user_mailer/en.yml
index 7b6f7f33011..634f1ad81aa 100644
--- a/config/locales/user_mailer/en.yml
+++ b/config/locales/user_mailer/en.yml
@@ -100,6 +100,35 @@ en:
%{app_name} %{help_link} or %{contact_link}.
subject: Email address deleted
help_link_text: Help Center
+ in_person_failed:
+ body: Click the button or copy the link below to try verifying your identity
+ online again through %{app_name}. If you are still experiencing issues,
+ please contact the agency you are trying to access.
+ intro: Your identity could not be verified at the %{location} Post Office on
+ %{date}.
+ subject: Your identity could not be verified in person
+ verifying_identity: 'When verifying your identity:'
+ verifying_step_not_expired: 'Your state-issued ID or driver’s license must not
+ be expired. We do not currently accept any other forms of
+ identification, such as passports and military IDs.'
+ verifying_step_proof_of_address: 'If you try to verify your identity in person
+ again, you need to bring a valid proof of address if your current
+ address is different than the address on your ID.'
+ in_person_ready_to_verify:
+ greeting: Hi %{name},
+ intro: Here are the details to verify your identity in person at a United States
+ Post Office near you.
+ subject: You’re ready to verify your identity with %{app_name} in person
+ in_person_verified:
+ greeting: Hello,
+ intro: You successfully verified your identity at the %{location} Post Office on
+ %{date}.
+ next_sign_in: Next, click the button or copy the link below to sign in to %{app_name}.
+ sign_in: Sign in
+ subject: You successfully verified your identity with %{app_name}
+ warning_contact_us_html: If you did not attempt to verify your identity in
+ person, please contact us and sign in to change your password.
letter_reminder:
info_html: The letter you are about to receive will contain a confirmation code
that helps us verify your address. You can complete the identity
diff --git a/config/locales/user_mailer/es.yml b/config/locales/user_mailer/es.yml
index 9bcc9d62f61..9f0634ed76a 100644
--- a/config/locales/user_mailer/es.yml
+++ b/config/locales/user_mailer/es.yml
@@ -106,6 +106,38 @@ es:
%{app_name} %{help_link} o el %{contact_link}.
subject: Dirección de correo electrónico eliminada
help_link_text: Centro de Ayuda
+ in_person_failed:
+ body: Haga clic en el botón o copie el enlace siguiente para volver a intentar
+ verificar su identidad en línea con %{app_name}. Si sigue teniendo
+ problemas, póngase en contacto con la agencia a la que intenta acceder.
+ intro: El %{date}, no se pudo verificar su identidad en la oficina de correos de
+ %{location}.
+ subject: No se pudo verificar su identidad en persona
+ verifying_identity: 'Al verificar su identidad:'
+ verifying_step_not_expired: Su documento de identidad o permiso de conducir
+ emitido por el estado debe estar vigente. Por el momento, no aceptamos
+ otras formas de identificación, como pasaportes o cartillas militares.
+ verifying_step_proof_of_address: Si vuelve a intentar verificar su identidad en
+ persona, deberá llevar un comprobante de domicilio vigente si su
+ dirección actual es distinta de la que aparece en su documento de
+ identidad.
+ in_person_ready_to_verify:
+ greeting: 'Hola, %{name}:'
+ intro: Estos son los detalles para verificar su identidad en persona en una
+ oficina de correos de los Estados Unidos cercana a usted.
+ subject: Está listo para verificar su identidad con %{app_name} en persona
+ in_person_verified:
+ greeting: Hola,
+ intro: El %{date}, verificó correctamente su identidad en la oficina de correos
+ de %{location}.
+ next_sign_in: Luego, haga clic en el botón o copie el enlace que aparece a
+ continuación para iniciar sesión en %{app_name}.
+ sign_in: Iniciar sesión
+ subject: Verificó correctamente su identidad con %{app_name}
+ warning_contact_us_html: Si usted no intentó verificar su identidad en persona,
+ por favor, póngase en contacto con
+ nosotros e inicie sesión para cambiar
+ su contraseña.
letter_reminder:
info_html: La carta que está a punto de recibir contendrá un código de
confirmación que nos ayudará a verificar su dirección. Puede completar
diff --git a/config/locales/user_mailer/fr.yml b/config/locales/user_mailer/fr.yml
index 616a7302c7f..1e72e5204ce 100644
--- a/config/locales/user_mailer/fr.yml
+++ b/config/locales/user_mailer/fr.yml
@@ -109,6 +109,40 @@ fr:
veuillez visiter le %{help_link} de %{app_name} ou %{contact_link}.
subject: Adresse email supprimée
help_link_text: Centre d’aide
+ in_person_failed:
+ body: Cliquez sur le bouton ou copiez le lien ci-dessous pour essayer de
+ vérifier à nouveau votre identité en ligne par le biais de %{app_name}.
+ Si vous rencontrez toujours des problèmes, veuillez contacter l’agence à
+ laquelle vous essayez d’accéder.
+ intro: Votre identité n’a pas pu être vérifiée au bureau de poste de %{location}
+ le %{date}.
+ subject: Votre identité n’a pas pu être vérifiée en personne
+ verifying_identity: 'Lors de la vérification de votre identité :'
+ verifying_step_not_expired: Votre carte d’identité ou votre permis de conduire
+ délivré par l’État ne doit pas être périmé. Nous n’acceptons
+ actuellement aucune autre forme d’identification, comme les passeports
+ et les cartes d’identité militaires.
+ verifying_step_proof_of_address: Si vous tentez à nouveau de vérifier votre
+ identité en personne, vous devez apporter un justificatif de domicile
+ valable si votre adresse actuelle est différente de celle figurant sur
+ votre pièce d’identité.
+ in_person_ready_to_verify:
+ greeting: Bonjour %{name},
+ intro: Voici les détails pour vérifier votre identité en personne dans un bureau
+ de poste des États-Unis près de chez vous.
+ subject: Vous êtes prêt à vérifier votre identité avec %{app_name} en personne
+ in_person_verified:
+ greeting: Bonjour,
+ intro: Vous avez vérifié avec succès votre identité au bureau de poste de
+ %{location} le %{date}.
+ next_sign_in: Ensuite, cliquez sur le bouton ou copiez le lien ci-dessous pour
+ vous connecter à %{app_name}.
+ sign_in: Se connecter
+ subject: Vous avez vérifié avec succès votre identité avec %{app_name}
+ warning_contact_us_html: Si vous n’avez pas essayé de vérifier votre identité en
+ personne, veuillez nous contacter et
+ vous connecter pour changer votre mot de
+ passe.
letter_reminder:
info_html: La lettre que vous êtes sur le point de recevoir contiendra un code
de confirmation nous permettant de vérifier votre adresse. Vous pouvez
diff --git a/config/routes.rb b/config/routes.rb
index aaf5302755d..c4ece5930fa 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -329,6 +329,8 @@
get '/in_person' => 'in_person#index'
get '/in_person/ready_to_verify' => 'in_person/ready_to_verify#show',
as: :in_person_ready_to_verify
+ get '/in_person/usps_locations' => 'in_person/usps_locations#index'
+ put '/in_person/usps_locations' => 'in_person/usps_locations#update'
get '/in_person/:step' => 'in_person#show', as: :in_person_step
put '/in_person/:step' => 'in_person#update'
diff --git a/config/service_providers.localdev.yml b/config/service_providers.localdev.yml
index 8a693224a19..5e22299273b 100644
--- a/config/service_providers.localdev.yml
+++ b/config/service_providers.localdev.yml
@@ -419,6 +419,7 @@ development:
redirect_uris:
- 'http://localhost:3001/auth/logindotgov/callback'
- 'http://localhost:3001'
+ push_notification_url: http://localhost:3001/api/security_events
'urn:gov:gsa:openidconnect:development':
redirect_uris:
diff --git a/db/primary_migrate/20220728223809_allow_null_profiles_on_in_person_enrollments.rb b/db/primary_migrate/20220728223809_allow_null_profiles_on_in_person_enrollments.rb
new file mode 100644
index 00000000000..1ae58ee4cee
--- /dev/null
+++ b/db/primary_migrate/20220728223809_allow_null_profiles_on_in_person_enrollments.rb
@@ -0,0 +1,5 @@
+class AllowNullProfilesOnInPersonEnrollments < ActiveRecord::Migration[7.0]
+ def change
+ change_column_null :in_person_enrollments, :profile_id, true
+ end
+end
diff --git a/db/primary_migrate/20220728223843_add_enrollment_established_at_to_in_person_enrollments.rb b/db/primary_migrate/20220728223843_add_enrollment_established_at_to_in_person_enrollments.rb
new file mode 100644
index 00000000000..3fc06fb5439
--- /dev/null
+++ b/db/primary_migrate/20220728223843_add_enrollment_established_at_to_in_person_enrollments.rb
@@ -0,0 +1,5 @@
+class AddEnrollmentEstablishedAtToInPersonEnrollments < ActiveRecord::Migration[7.0]
+ def change
+ add_column :in_person_enrollments, :enrollment_established_at, :datetime, null: true, comment: "When the enrollment was successfully established"
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index e5ea41ec1ef..bacb570dbb6 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,8 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2022_07_21_170157) do
-
+ActiveRecord::Schema[7.0].define(version: 2022_07_28_223843) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
enable_extension "pgcrypto"
@@ -19,14 +18,14 @@
create_table "account_reset_requests", force: :cascade do |t|
t.integer "user_id", null: false
- t.datetime "requested_at"
+ t.datetime "requested_at", precision: nil
t.string "request_token"
- t.datetime "cancelled_at"
- t.datetime "reported_fraud_at"
- t.datetime "granted_at"
+ t.datetime "cancelled_at", precision: nil
+ t.datetime "reported_fraud_at", precision: nil
+ t.datetime "granted_at", precision: nil
t.string "granted_token"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.index ["cancelled_at", "granted_at", "requested_at"], name: "index_account_reset_requests_on_timestamps"
t.index ["granted_token"], name: "index_account_reset_requests_on_granted_token", unique: true
t.index ["request_token"], name: "index_account_reset_requests_on_request_token", unique: true
@@ -54,17 +53,17 @@
t.string "encrypted_otp_secret_key", null: false
t.string "name", null: false
t.integer "totp_timestamp"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.index ["user_id", "created_at"], name: "index_auth_app_configurations_on_user_id_and_created_at", unique: true
t.index ["user_id", "name"], name: "index_auth_app_configurations_on_user_id_and_name", unique: true
end
create_table "backup_code_configurations", force: :cascade do |t|
t.integer "user_id", null: false
- t.datetime "used_at"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "used_at", precision: nil
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.string "salted_code_fingerprint"
t.string "code_salt"
t.string "code_cost"
@@ -75,8 +74,8 @@
create_table "deleted_users", force: :cascade do |t|
t.integer "user_id", null: false
t.string "uuid", null: false
- t.datetime "user_created_at", null: false
- t.datetime "deleted_at", null: false
+ t.datetime "user_created_at", precision: nil, null: false
+ t.datetime "deleted_at", precision: nil, null: false
t.index ["user_id"], name: "index_deleted_users_on_user_id", unique: true
t.index ["uuid"], name: "index_deleted_users_on_uuid", unique: true
end
@@ -85,98 +84,98 @@
t.integer "user_id", null: false
t.string "cookie_uuid", null: false
t.string "user_agent", null: false
- t.datetime "last_used_at", null: false
+ t.datetime "last_used_at", precision: nil, null: false
t.string "last_ip", limit: 255, null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.index ["cookie_uuid"], name: "index_devices_on_cookie_uuid"
t.index ["user_id", "last_used_at"], name: "index_device_user_id_last_used_at"
end
create_table "doc_auth_logs", force: :cascade do |t|
t.integer "user_id", null: false
- t.datetime "welcome_view_at"
+ t.datetime "welcome_view_at", precision: nil
t.integer "welcome_view_count", default: 0
- t.datetime "upload_view_at"
+ t.datetime "upload_view_at", precision: nil
t.integer "upload_view_count", default: 0
- t.datetime "send_link_view_at"
+ t.datetime "send_link_view_at", precision: nil
t.integer "send_link_view_count", default: 0
- t.datetime "link_sent_view_at"
+ t.datetime "link_sent_view_at", precision: nil
t.integer "link_sent_view_count", default: 0
- t.datetime "email_sent_view_at"
+ t.datetime "email_sent_view_at", precision: nil
t.integer "email_sent_view_count", default: 0
- t.datetime "front_image_view_at"
+ t.datetime "front_image_view_at", precision: nil
t.integer "front_image_view_count", default: 0
t.integer "front_image_submit_count", default: 0
t.integer "front_image_error_count", default: 0
- t.datetime "back_image_view_at"
+ t.datetime "back_image_view_at", precision: nil
t.integer "back_image_view_count", default: 0
t.integer "back_image_submit_count", default: 0
t.integer "back_image_error_count", default: 0
- t.datetime "mobile_front_image_view_at"
+ t.datetime "mobile_front_image_view_at", precision: nil
t.integer "mobile_front_image_view_count", default: 0
- t.datetime "mobile_back_image_view_at"
+ t.datetime "mobile_back_image_view_at", precision: nil
t.integer "mobile_back_image_view_count", default: 0
- t.datetime "ssn_view_at"
+ t.datetime "ssn_view_at", precision: nil
t.integer "ssn_view_count", default: 0
- t.datetime "verify_view_at"
+ t.datetime "verify_view_at", precision: nil
t.integer "verify_view_count", default: 0
t.integer "verify_submit_count", default: 0
t.integer "verify_error_count", default: 0
- t.datetime "verify_phone_view_at"
+ t.datetime "verify_phone_view_at", precision: nil
t.integer "verify_phone_view_count", default: 0
- t.datetime "usps_address_view_at"
+ t.datetime "usps_address_view_at", precision: nil
t.integer "usps_address_view_count", default: 0
- t.datetime "encrypt_view_at"
+ t.datetime "encrypt_view_at", precision: nil
t.integer "encrypt_view_count", default: 0
- t.datetime "verified_view_at"
+ t.datetime "verified_view_at", precision: nil
t.integer "verified_view_count", default: 0
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.integer "mobile_front_image_submit_count", default: 0
t.integer "mobile_front_image_error_count", default: 0
t.integer "mobile_back_image_submit_count", default: 0
t.integer "mobile_back_image_error_count", default: 0
t.integer "usps_letter_sent_submit_count", default: 0
t.integer "usps_letter_sent_error_count", default: 0
- t.datetime "capture_mobile_back_image_view_at"
+ t.datetime "capture_mobile_back_image_view_at", precision: nil
t.integer "capture_mobile_back_image_view_count", default: 0
- t.datetime "capture_complete_view_at"
+ t.datetime "capture_complete_view_at", precision: nil
t.integer "capture_complete_view_count", default: 0
t.integer "capture_mobile_back_image_submit_count", default: 0
t.integer "capture_mobile_back_image_error_count", default: 0
- t.datetime "no_sp_session_started_at"
- t.datetime "choose_method_view_at"
+ t.datetime "no_sp_session_started_at", precision: nil
+ t.datetime "choose_method_view_at", precision: nil
t.integer "choose_method_view_count", default: 0
- t.datetime "present_cac_view_at"
+ t.datetime "present_cac_view_at", precision: nil
t.integer "present_cac_view_count", default: 0
t.integer "present_cac_submit_count", default: 0
t.integer "present_cac_error_count", default: 0
- t.datetime "enter_info_view_at"
+ t.datetime "enter_info_view_at", precision: nil
t.integer "enter_info_view_count", default: 0
- t.datetime "success_view_at"
+ t.datetime "success_view_at", precision: nil
t.integer "success_view_count", default: 0
- t.datetime "selfie_view_at"
+ t.datetime "selfie_view_at", precision: nil
t.integer "selfie_view_count", default: 0
t.integer "selfie_submit_count", default: 0
t.integer "selfie_error_count", default: 0
t.string "issuer"
t.string "last_document_error"
- t.datetime "document_capture_view_at"
+ t.datetime "document_capture_view_at", precision: nil
t.integer "document_capture_view_count", default: 0
t.integer "document_capture_submit_count", default: 0
t.integer "document_capture_error_count", default: 0
- t.datetime "agreement_view_at"
+ t.datetime "agreement_view_at", precision: nil
t.integer "agreement_view_count", default: 0
t.string "state"
t.boolean "aamva"
- t.datetime "verify_submit_at"
+ t.datetime "verify_submit_at", precision: nil
t.integer "verify_phone_submit_count", default: 0
- t.datetime "verify_phone_submit_at"
- t.datetime "document_capture_submit_at"
- t.datetime "back_image_submit_at"
- t.datetime "capture_mobile_back_image_submit_at"
- t.datetime "mobile_back_image_submit_at"
+ t.datetime "verify_phone_submit_at", precision: nil
+ t.datetime "document_capture_submit_at", precision: nil
+ t.datetime "back_image_submit_at", precision: nil
+ t.datetime "capture_mobile_back_image_submit_at", precision: nil
+ t.datetime "mobile_back_image_submit_at", precision: nil
t.index ["issuer"], name: "index_doc_auth_logs_on_issuer"
t.index ["user_id"], name: "index_doc_auth_logs_on_user_id", unique: true
t.index ["verified_view_at"], name: "index_doc_auth_logs_on_verified_view_at"
@@ -186,12 +185,12 @@
t.string "uuid"
t.string "result_id"
t.bigint "user_id"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.datetime "requested_at"
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
+ t.datetime "requested_at", precision: nil
t.boolean "ial2_strict"
t.string "issuer"
- t.datetime "cancelled_at"
+ t.datetime "cancelled_at", precision: nil
t.boolean "ocr_confirmation_pending", default: false
t.index ["result_id"], name: "index_document_capture_sessions_on_result_id"
t.index ["user_id"], name: "index_document_capture_sessions_on_user_id"
@@ -201,13 +200,13 @@
create_table "email_addresses", force: :cascade do |t|
t.bigint "user_id"
t.string "confirmation_token", limit: 255
- t.datetime "confirmed_at"
- t.datetime "confirmation_sent_at"
+ t.datetime "confirmed_at", precision: nil
+ t.datetime "confirmation_sent_at", precision: nil
t.string "email_fingerprint", default: "", null: false
t.string "encrypted_email", default: "", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.datetime "last_sign_in_at"
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
+ t.datetime "last_sign_in_at", precision: nil
t.index ["confirmation_token"], name: "index_email_addresses_on_confirmation_token", unique: true
t.index ["email_fingerprint", "user_id"], name: "index_email_addresses_on_email_fingerprint_and_user_id", unique: true
t.index ["email_fingerprint"], name: "index_email_addresses_on_email_fingerprint", unique: true, where: "(confirmed_at IS NOT NULL)"
@@ -217,11 +216,11 @@
create_table "events", id: :serial, force: :cascade do |t|
t.integer "user_id", null: false
t.integer "event_type", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.integer "device_id"
t.string "ip"
- t.datetime "disavowed_at"
+ t.datetime "disavowed_at", precision: nil
t.string "disavowal_token_fingerprint"
t.index ["device_id", "created_at"], name: "index_events_on_device_id_and_created_at"
t.index ["disavowal_token_fingerprint"], name: "index_events_on_disavowal_token_fingerprint"
@@ -257,10 +256,10 @@
create_table "identities", id: :serial, force: :cascade do |t|
t.string "service_provider", limit: 255
- t.datetime "last_authenticated_at"
+ t.datetime "last_authenticated_at", precision: nil
t.integer "user_id"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", precision: nil
+ t.datetime "updated_at", precision: nil
t.string "session_uuid", limit: 255
t.string "uuid", null: false
t.string "nonce"
@@ -270,11 +269,11 @@
t.string "code_challenge"
t.string "rails_session_id"
t.json "verified_attributes"
- t.datetime "verified_at"
- t.datetime "last_consented_at"
- t.datetime "last_ial1_authenticated_at"
- t.datetime "last_ial2_authenticated_at"
- t.datetime "deleted_at"
+ t.datetime "verified_at", precision: nil
+ t.datetime "last_consented_at", precision: nil
+ t.datetime "last_ial1_authenticated_at", precision: nil
+ t.datetime "last_ial2_authenticated_at", precision: nil
+ t.datetime "deleted_at", precision: nil
t.index ["access_token"], name: "index_identities_on_access_token", unique: true
t.index ["session_uuid"], name: "index_identities_on_session_uuid", unique: true
t.index ["user_id", "service_provider"], name: "index_identities_on_user_id_and_service_provider", unique: true
@@ -283,16 +282,17 @@
create_table "in_person_enrollments", comment: "Details and status of an in-person proofing enrollment for one user and profile", force: :cascade do |t|
t.bigint "user_id", null: false, comment: "Foreign key to the user this enrollment belongs to"
- t.bigint "profile_id", null: false, comment: "Foreign key to the profile this enrollment belongs to"
+ t.bigint "profile_id", comment: "Foreign key to the profile this enrollment belongs to"
t.string "enrollment_code", comment: "The code returned by the USPS service"
- t.datetime "status_check_attempted_at", comment: "The last time a status check was attempted"
- t.datetime "status_updated_at", comment: "The last time the status was successfully updated with a value from the USPS API"
+ t.datetime "status_check_attempted_at", precision: nil, comment: "The last time a status check was attempted"
+ t.datetime "status_updated_at", precision: nil, comment: "The last time the status was successfully updated with a value from the USPS API"
t.integer "status", default: 0, comment: "The status of the enrollment"
- t.datetime "created_at", precision: 6, null: false
- t.datetime "updated_at", precision: 6, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
t.boolean "current_address_matches_id", comment: "True if the user indicates that their current address matches the address on the ID they're bringing to the Post Office."
t.jsonb "selected_location_details", comment: "The location details of the Post Office the user selected (including title, address, hours of operation)"
t.string "unique_id", comment: "Unique ID to use with the USPS service"
+ t.datetime "enrollment_established_at", comment: "When the enrollment was successfully established"
t.index ["profile_id"], name: "index_in_person_enrollments_on_profile_id"
t.index ["unique_id"], name: "index_in_person_enrollments_on_unique_id", unique: true
t.index ["user_id", "status"], name: "index_in_person_enrollments_on_user_id_and_status", unique: true, where: "(status = 1)"
@@ -330,7 +330,7 @@
end
create_table "letter_requests_to_usps_ftp_logs", force: :cascade do |t|
- t.datetime "ftp_at", null: false
+ t.datetime "ftp_at", precision: nil, null: false
t.integer "letter_requests_count", null: false
t.index ["ftp_at"], name: "index_letter_requests_to_usps_ftp_logs_on_ftp_at"
end
@@ -353,12 +353,12 @@
end
create_table "otp_requests_trackers", id: :serial, force: :cascade do |t|
- t.datetime "otp_last_sent_at"
+ t.datetime "otp_last_sent_at", precision: nil
t.integer "otp_send_count", default: 0
t.string "attribute_cost"
t.string "phone_fingerprint", default: "", null: false
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", precision: nil
+ t.datetime "updated_at", precision: nil
t.boolean "phone_confirmed", default: false
t.index ["phone_fingerprint", "phone_confirmed"], name: "index_on_phone_and_confirmed", unique: true
end
@@ -390,11 +390,11 @@
t.text "encrypted_phone", null: false
t.integer "delivery_preference", default: 0, null: false
t.boolean "mfa_enabled", default: true, null: false
- t.datetime "confirmation_sent_at"
- t.datetime "confirmed_at"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.datetime "made_default_at"
+ t.datetime "confirmation_sent_at", precision: nil
+ t.datetime "confirmed_at", precision: nil
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
+ t.datetime "made_default_at", precision: nil
t.index ["user_id", "made_default_at", "created_at"], name: "index_phone_configurations_on_made_default_at"
end
@@ -402,8 +402,8 @@
t.string "encrypted_phone"
t.string "phone_fingerprint", null: false
t.string "uuid"
- t.datetime "created_at", precision: 6, null: false
- t.datetime "updated_at", precision: 6, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
t.index ["phone_fingerprint"], name: "index_phone_number_opt_outs_on_phone_fingerprint", unique: true
t.index ["uuid"], name: "index_phone_number_opt_outs_on_uuid", unique: true
end
@@ -412,8 +412,8 @@
t.integer "user_id", null: false
t.string "x509_dn_uuid", null: false
t.string "name", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.string "x509_issuer"
t.index ["user_id", "created_at"], name: "index_piv_cac_configurations_on_user_id_and_created_at", unique: true
t.index ["user_id", "name"], name: "index_piv_cac_configurations_on_user_id_and_name", unique: true
@@ -423,10 +423,10 @@
create_table "profiles", id: :serial, force: :cascade do |t|
t.integer "user_id", null: false
t.boolean "active", default: false, null: false
- t.datetime "verified_at"
- t.datetime "activated_at"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "verified_at", precision: nil
+ t.datetime "activated_at", precision: nil
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.text "encrypted_pii"
t.string "ssn_signature", limit: 64
t.text "encrypted_pii_recovery"
@@ -449,9 +449,9 @@
t.string "source_check"
t.string "resolution_check"
t.string "address_check"
- t.datetime "verified_at"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "verified_at", precision: nil
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.string "liveness_check"
t.index ["user_id"], name: "index_proofing_components_on_user_id", unique: true
t.index ["verified_at"], name: "index_proofing_components_on_verified_at"
@@ -466,8 +466,8 @@
t.integer "lexis_nexis_address_count", default: 0
t.integer "gpo_letter_count", default: 0
t.integer "phone_otp_count", default: 0
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.integer "acuant_result_count", default: 0
t.integer "acuant_selfie_count", default: 0
t.index ["user_id"], name: "index_proofing_costs_on_user_id", unique: true
@@ -475,13 +475,13 @@
create_table "registration_logs", force: :cascade do |t|
t.integer "user_id", null: false
- t.datetime "submitted_at", null: false
- t.datetime "confirmed_at"
- t.datetime "password_at"
+ t.datetime "submitted_at", precision: nil, null: false
+ t.datetime "confirmed_at", precision: nil
+ t.datetime "password_at", precision: nil
t.string "first_mfa"
- t.datetime "first_mfa_at"
+ t.datetime "first_mfa_at", precision: nil
t.string "second_mfa"
- t.datetime "registered_at"
+ t.datetime "registered_at", precision: nil
t.index ["registered_at"], name: "index_registration_logs_on_registered_at"
t.index ["submitted_at"], name: "index_registration_logs_on_submitted_at"
t.index ["user_id"], name: "index_registration_logs_on_user_id", unique: true
@@ -492,9 +492,9 @@
t.string "event_type", null: false
t.string "jti"
t.string "issuer"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.datetime "occurred_at"
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
+ t.datetime "occurred_at", precision: nil
t.index ["jti", "user_id", "issuer"], name: "index_security_events_on_jti_and_user_id_and_issuer", unique: true
t.index ["user_id"], name: "index_security_events_on_user_id"
end
@@ -519,8 +519,8 @@
t.text "sp_initiated_login_url"
t.text "return_to_sp_url"
t.json "attribute_bundle"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", precision: nil
+ t.datetime "updated_at", precision: nil
t.boolean "active", default: false, null: false
t.boolean "approved", default: false, null: false
t.boolean "native", default: false, null: false
@@ -553,8 +553,8 @@
create_table "sign_in_restrictions", force: :cascade do |t|
t.integer "user_id", null: false
t.string "service_provider"
- t.datetime "created_at", precision: 6, null: false
- t.datetime "updated_at", precision: 6, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
t.index ["user_id", "service_provider"], name: "index_sign_in_restrictions_on_user_id_and_service_provider", unique: true
end
@@ -562,20 +562,20 @@
t.string "issuer", null: false
t.integer "agency_id", null: false
t.string "cost_type", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.integer "ial"
t.string "transaction_id"
t.index ["created_at"], name: "index_sp_costs_on_created_at"
end
create_table "sp_return_logs", force: :cascade do |t|
- t.datetime "requested_at", null: false
+ t.datetime "requested_at", precision: nil, null: false
t.string "request_id", null: false
t.integer "ial", null: false
t.string "issuer", null: false
t.integer "user_id"
- t.datetime "returned_at"
+ t.datetime "returned_at", precision: nil
t.boolean "billable"
t.index "((requested_at)::date), issuer", name: "index_sp_return_logs_on_requested_at_date_issuer", where: "(returned_at IS NOT NULL)"
t.index ["request_id"], name: "index_sp_return_logs_on_request_id", unique: true
@@ -583,25 +583,25 @@
create_table "users", id: :serial, force: :cascade do |t|
t.string "reset_password_token", limit: 255
- t.datetime "reset_password_sent_at"
- t.datetime "remember_created_at"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.datetime "confirmed_at"
+ t.datetime "reset_password_sent_at", precision: nil
+ t.datetime "remember_created_at", precision: nil
+ t.datetime "created_at", precision: nil
+ t.datetime "updated_at", precision: nil
+ t.datetime "confirmed_at", precision: nil
t.integer "second_factor_attempts_count", default: 0
t.string "uuid", limit: 255, null: false
- t.datetime "second_factor_locked_at"
- t.datetime "phone_confirmed_at"
+ t.datetime "second_factor_locked_at", precision: nil
+ t.datetime "phone_confirmed_at", precision: nil
t.string "direct_otp"
- t.datetime "direct_otp_sent_at"
+ t.datetime "direct_otp_sent_at", precision: nil
t.string "unique_session_id"
t.integer "otp_delivery_preference", default: 0, null: false
t.string "encrypted_password_digest", default: ""
t.string "encrypted_recovery_code_digest", default: ""
- t.datetime "remember_device_revoked_at"
+ t.datetime "remember_device_revoked_at", precision: nil
t.string "email_language", limit: 10
- t.datetime "accepted_terms_at"
- t.datetime "encrypted_recovery_code_digest_generated_at"
+ t.datetime "accepted_terms_at", precision: nil
+ t.datetime "encrypted_recovery_code_digest_generated_at", precision: nil
t.date "non_restricted_mfa_required_prompt_skip_date"
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
t.index ["uuid"], name: "index_users_on_uuid", unique: true
@@ -610,18 +610,18 @@
create_table "usps_confirmation_codes", force: :cascade do |t|
t.integer "profile_id", null: false
t.string "otp_fingerprint", null: false
- t.datetime "code_sent_at", default: -> { "CURRENT_TIMESTAMP" }, null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.datetime "bounced_at"
+ t.datetime "code_sent_at", precision: nil, default: -> { "CURRENT_TIMESTAMP" }, null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
+ t.datetime "bounced_at", precision: nil
t.index ["otp_fingerprint"], name: "index_usps_confirmation_codes_on_otp_fingerprint"
t.index ["profile_id"], name: "index_usps_confirmation_codes_on_profile_id"
end
create_table "usps_confirmations", id: :serial, force: :cascade do |t|
t.text "entry", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
end
create_table "webauthn_configurations", force: :cascade do |t|
@@ -629,8 +629,8 @@
t.string "name", null: false
t.text "credential_id", null: false
t.text "credential_public_key", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.boolean "platform_authenticator"
t.index ["user_id"], name: "index_webauthn_configurations_on_user_id"
end
diff --git a/db/worker_jobs_schema.rb b/db/worker_jobs_schema.rb
index 597bfbcc5a8..5677efb08d4 100644
--- a/db/worker_jobs_schema.rb
+++ b/db/worker_jobs_schema.rb
@@ -10,15 +10,14 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2022_07_20_185043) do
-
+ActiveRecord::Schema[7.0].define(version: 2022_07_20_185043) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
create_table "good_job_processes", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.datetime "created_at", precision: 6, null: false
- t.datetime "updated_at", precision: 6, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
t.jsonb "state"
end
@@ -26,17 +25,17 @@
t.text "queue_name"
t.integer "priority"
t.jsonb "serialized_params"
- t.datetime "scheduled_at"
- t.datetime "performed_at"
- t.datetime "finished_at"
+ t.datetime "scheduled_at", precision: nil
+ t.datetime "performed_at", precision: nil
+ t.datetime "finished_at", precision: nil
t.text "error"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.uuid "active_job_id"
t.text "concurrency_key"
t.text "cron_key"
t.uuid "retried_good_job_id"
- t.datetime "cron_at"
+ t.datetime "cron_at", precision: nil
t.index ["active_job_id", "created_at"], name: "index_good_jobs_on_active_job_id_and_created_at"
t.index ["active_job_id"], name: "index_good_jobs_on_active_job_id"
t.index ["concurrency_key"], name: "index_good_jobs_on_concurrency_key_when_unfinished", where: "(finished_at IS NULL)"
diff --git a/docs/backend.md b/docs/backend.md
index c177e9f03b7..e3329a88cc4 100644
--- a/docs/backend.md
+++ b/docs/backend.md
@@ -35,7 +35,7 @@ We use `ActiveModel::Model` validations to help build useful error structures.
Forms should have a `#submit` method that returns a `FormResponse`.
- `success:` is usually `#valid?` from ActiveModel
-- `errors:` is usually `errors` from ActiveModel
+- `errors:` is usually `#errors` from ActiveModel
- `extra:` is, by convention, a method called `extra_analytics_attributes` that
returns a Hash
@@ -66,19 +66,34 @@ with YARD so that we can auto-generate
### Controllers
-These tie everything together
+These tie everything together! We aim for lean, "RESTful" controllers
-```ruby
-def index
- form = MyForm.new(params)
+* Keep as much business logic as possible out of controllers moving that logic
+ into Forms or Services
+
+* Prefer adding a new controller with one of the CRUD methods over creating a
+ custom method in an existing controller. For example, if your app allows a
+ user to update their email and their password on two different pages, instead of
+ using a single controller with methods called `update_email` and
+ `update_password`, create two controllers and name the methods `update`, i.e.
+ `EmailsController#update` and `PasswordsController#update`. See
+ http://jeromedalbert.com/how-dhh-organizes-his-rails-controllers/ for more about
+ this design pattern.
- result = form.submit
- analytics.my_event(**result.to_h)
- if result.success?
- do_something(form.sensitive_value_here)
- else
- do_something_else
+```ruby
+class MyController < ApplicationController
+ def update
+ form = MyForm.new(params)
+
+ result = form.submit
+ analytics.my_event(**result.to_h)
+
+ if result.success?
+ do_something(form.sensitive_value_here)
+ else
+ do_something_else
+ end
end
end
```
diff --git a/lib/identity_config.rb b/lib/identity_config.rb
index 3487bf8eeb2..bbeeed761d2 100644
--- a/lib/identity_config.rb
+++ b/lib/identity_config.rb
@@ -210,6 +210,13 @@ def self.build_store(config_map)
config.add(:lexisnexis_trueid_noliveness_cropping_workflow, type: :string)
config.add(:lexisnexis_trueid_noliveness_nocropping_workflow, type: :string)
config.add(:lexisnexis_trueid_timeout, type: :float)
+ config.add(:lexisnexis_threatmetrix_base_url, type: :string)
+ config.add(:lexisnexis_threatmetrix_request_mode, type: :string)
+ config.add(:lexisnexis_threatmetrix_account_id, type: :string)
+ config.add(:lexisnexis_threatmetrix_username, type: :string)
+ config.add(:lexisnexis_threatmetrix_password, type: :string)
+ config.add(:lexisnexis_threatmetrix_instant_verify_timeout, type: :float)
+ config.add(:lexisnexis_threatmetrix_instant_verify_workflow, type: :string)
config.add(:liveness_checking_enabled, type: :boolean)
config.add(:lockout_period_in_minutes, type: :integer)
config.add(:log_to_stdout, type: :boolean)
@@ -271,6 +278,8 @@ def self.build_store(config_map)
config.add(:platform_authentication_enabled, type: :boolean)
config.add(:poll_rate_for_verify_in_seconds, type: :integer)
config.add(:proofer_mock_fallback, type: :boolean)
+ config.add(:proofing_device_profiling_collecting_enabled, type: :boolean)
+ config.add(:proofing_device_profiling_decisioning_enabled, type: :boolean)
config.add(:proofing_send_partial_dob, type: :boolean)
config.add(:proof_address_max_attempts, type: :integer)
config.add(:proof_address_max_attempt_window_in_minutes, type: :integer)
@@ -366,6 +375,7 @@ def self.build_store(config_map)
config.add(:voip_allowed_phones, type: :json)
config.add(:voip_block, type: :boolean)
config.add(:voip_check, type: :boolean)
+ config.add(:inherited_proofing_va_base_url, type: :string)
@store = RedactedStruct.new('IdentityConfig', *config.written_env.keys, keyword_init: true).
new(**config.written_env)
diff --git a/lib/linters/errors_add_linter.rb b/lib/linters/errors_add_linter.rb
index 73c81e9bbfe..d6c9c9043e1 100644
--- a/lib/linters/errors_add_linter.rb
+++ b/lib/linters/errors_add_linter.rb
@@ -1,16 +1,16 @@
module RuboCop
module Cop
module IdentityIdp
- # This lint ensures `redirect_back` is called with
- # the fallback_location option and allow_other_host set to false.
- # This is to prevent open redirects via the Referer header.
+ # This lint helps make sure that we have language-agnostic identifiers
+ # for errors that we log. The error strings are different in each locale
+ # so this helps us compare them more directly.
#
# @example
# #bad
# errors.add(:iss, 'invalid issuer')
#
# #good
- # rrors.add(:iss, 'invalid issuer', type: issue)
+ # errors.add(:iss, 'invalid issuer', type: :invalid_issuer)
#
class ErrorsAddLinter < RuboCop::Cop::Cop
MSG = 'Please set a unique key for this error'.freeze
diff --git a/lib/session_encryptor.rb b/lib/session_encryptor.rb
index b225a369588..397b7e46a09 100644
--- a/lib/session_encryptor.rb
+++ b/lib/session_encryptor.rb
@@ -5,9 +5,9 @@ class SensitiveValueError < StandardError; end
NEW_CIPHERTEXT_HEADER = 'v2'
SENSITIVE_KEYS = [
'first_name', 'middle_name', 'last_name', 'address1', 'address2', 'city', 'state', 'zipcode',
- 'zip_code', 'dob', 'phone_number', 'phone', 'ssn', 'prev_address1', 'prev_address2',
- 'prev_city', 'prev_state', 'prev_zipcode', 'pii', 'pii_from_doc', 'pii_from_user', 'password',
- 'personal_key', 'email', 'email_address', 'unconfirmed_phone'
+ 'zip_code', 'same_address_as_id', 'dob', 'phone_number', 'phone', 'ssn', 'prev_address1',
+ 'prev_address2', 'prev_city', 'prev_state', 'prev_zipcode', 'pii', 'pii_from_doc',
+ 'pii_from_user', 'password', 'personal_key', 'email', 'email_address', 'unconfirmed_phone'
].to_set.freeze
# 'idv/doc_auth' and 'idv' are used during the proofing process and can contain PII
diff --git a/spec/components/barcode_component_spec.rb b/spec/components/barcode_component_spec.rb
new file mode 100644
index 00000000000..44e171fc434
--- /dev/null
+++ b/spec/components/barcode_component_spec.rb
@@ -0,0 +1,51 @@
+require 'rails_helper'
+
+RSpec.describe BarcodeComponent, type: :component do
+ it 'renders expected content' do
+ rendered = render_inline BarcodeComponent.new(barcode_data: '1234', label: 'Code')
+
+ caption = page.find_css('table + div', text: 'Code: 1234').first
+
+ expect(rendered).to have_css("table.barcode[aria-label=#{t('components.barcode.table_label')}]")
+ expect(rendered).to have_css("[role=figure][aria-labelledby=#{caption.attr(:id)}]")
+ expect(rendered).to have_css('table tbody[aria-hidden=true]')
+ end
+
+ context 'with tag options' do
+ it 'renders with attributes' do
+ rendered = render_inline(
+ BarcodeComponent.new(
+ barcode_data: '1234',
+ label: '',
+ data: { foo: 'bar' },
+ aria: { hidden: 'false' },
+ class: 'example',
+ ),
+ )
+
+ expect(rendered).to have_css(
+ '.example[role=figure][aria-labelledby][data-foo=bar][aria-hidden=false]',
+ )
+ end
+ end
+
+ context 'with empty label' do
+ it 'renders label without prefix' do
+ rendered = render_inline BarcodeComponent.new(barcode_data: '1234', label: '')
+
+ expect(rendered).to have_css('table + div', text: '1234')
+ end
+ end
+
+ context 'with label formatter' do
+ it 'renders formatted label' do
+ rendered = render_inline BarcodeComponent.new(
+ barcode_data: '1234',
+ label: '',
+ label_formatter: ->(barcode_data) { barcode_data + '5678' },
+ )
+
+ expect(rendered).to have_css('table + div', text: '12345678')
+ end
+ end
+end
diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb
index 7bbc6ba4160..ff263a19a8c 100644
--- a/spec/controllers/accounts_controller_spec.rb
+++ b/spec/controllers/accounts_controller_spec.rb
@@ -63,7 +63,7 @@
user = create(
:user,
:signed_up,
- profiles: [build(:profile, deactivation_reason: :verification_pending)],
+ profiles: [build(:profile, deactivation_reason: :gpo_verification_pending)],
)
sign_in user
diff --git a/spec/controllers/api/verify/document_capture_controller_spec.rb b/spec/controllers/api/verify/document_capture_controller_spec.rb
index 48faa73128f..573fae40621 100644
--- a/spec/controllers/api/verify/document_capture_controller_spec.rb
+++ b/spec/controllers/api/verify/document_capture_controller_spec.rb
@@ -75,16 +75,18 @@
it 'returns inprogress status when create is called' do
agent = instance_double(Idv::Agent)
allow(Idv::Agent).to receive(:new).with(
- user_uuid: user.uuid,
- uuid_prefix: nil,
- document_arguments: {
- 'encryption_key' => encryption_key,
- 'front_image_iv' => front_image_iv,
- 'back_image_iv' => back_image_iv,
- 'selfie_image_iv' => selfie_image_iv,
- 'front_image_url' => front_image_url,
- 'back_image_url' => back_image_url,
- 'selfie_image_url' => selfie_image_url,
+ {
+ user_uuid: user.uuid,
+ uuid_prefix: nil,
+ document_arguments: {
+ 'encryption_key' => encryption_key,
+ 'front_image_iv' => front_image_iv,
+ 'back_image_iv' => back_image_iv,
+ 'selfie_image_iv' => selfie_image_iv,
+ 'front_image_url' => front_image_url,
+ 'back_image_url' => back_image_url,
+ 'selfie_image_url' => selfie_image_url,
+ },
},
).and_return(agent)
diff --git a/spec/controllers/api/verify/password_confirm_controller_spec.rb b/spec/controllers/api/verify/password_confirm_controller_spec.rb
index 8b235e1e3b4..8812e08e235 100644
--- a/spec/controllers/api/verify/password_confirm_controller_spec.rb
+++ b/spec/controllers/api/verify/password_confirm_controller_spec.rb
@@ -60,6 +60,14 @@ def stub_idv_session
end
context 'with in person profile' do
+ let(:applicant) {
+ Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE.merge(same_address_as_id: true)
+ }
+ let(:stub_idv_session) do
+ stub_user_with_applicant_data(user, applicant)
+ end
+ let!(:enrollment) { create(:in_person_enrollment, :establishing, user: user, profile: nil) }
+
before do
ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS)
allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
@@ -106,31 +114,48 @@ def stub_idv_session
post :create, params: { password: password, user_bundle_token: jwt }
end
- it 'creates an in-person enrollment record' do
- expect(InPersonEnrollment.count).to be(0)
+ it 'updates in-person enrollment record to associate profile' do
post :create, params: { password: password, user_bundle_token: jwt }
- expect(InPersonEnrollment.count).to be(1)
- enrollment = InPersonEnrollment.where(user_id: user.id).first
+ enrollment.reload
+
expect(enrollment.status).to eq('pending')
expect(enrollment.user_id).to eq(user.id)
expect(enrollment.enrollment_code).to be_a(String)
+ expect(enrollment.profile).to eq(user.profiles.last)
+ expect(enrollment.profile.deactivation_reason).to eq('in_person_verification_pending')
end
it 'leaves the enrollment in establishing when no enrollment code is returned' do
proofer = UspsInPersonProofing::Mock::Proofer.new
expect(UspsInPersonProofing::Mock::Proofer).to receive(:new).and_return(proofer)
expect(proofer).to receive(:request_enroll).and_return({})
- expect(InPersonEnrollment.count).to be(0)
post :create, params: { password: password, user_bundle_token: jwt }
+ enrollment.reload
+
expect(InPersonEnrollment.count).to be(1)
- enrollment = InPersonEnrollment.where(user_id: user.id).first
expect(enrollment.status).to eq('establishing')
- expect(enrollment.user_id).to eq(user.id)
+ expect(enrollment.user_id).to be(user.id)
expect(enrollment.enrollment_code).to be_nil
end
+
+ it 'sends ready to verify email' do
+ mailer = instance_double(ActionMailer::MessageDelivery, deliver_now_or_later: true)
+ user.email_addresses.each do |email_address|
+ expect(UserMailer).to receive(:in_person_ready_to_verify).
+ with(
+ user,
+ email_address,
+ enrollment: instance_of(InPersonEnrollment),
+ first_name: kind_of(String),
+ ).
+ and_return(mailer)
+ end
+
+ post :create, params: { password: password, user_bundle_token: jwt }
+ end
end
context 'with associated sp session' do
@@ -146,7 +171,13 @@ def stub_idv_session
end
context 'with pending profile' do
- let(:jwt_metadata) { { vendor_phone_confirmation: false, user_phone_confirmation: false } }
+ let(:jwt_metadata) do
+ {
+ vendor_phone_confirmation: false,
+ user_phone_confirmation: false,
+ address_verification_mechanism: 'gpo',
+ }
+ end
it 'creates a profile and returns completion url' do
post :create, params: { password: password, user_bundle_token: jwt }
@@ -155,6 +186,13 @@ def stub_idv_session
end
context 'with in person profile' do
+ let(:applicant) {
+ Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE.merge(same_address_as_id: true)
+ }
+ let(:stub_idv_session) do
+ stub_user_with_applicant_data(user, applicant)
+ end
+
before do
ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS)
allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 70d74a5c73a..c39d5da3144 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -134,6 +134,48 @@ def index
expect(response).to redirect_to(new_user_session_url)
end
+
+ it 'redirects back to home page if present and referer is invalid' do
+ referer = '@@ABC'
+
+ request.env['HTTP_REFERER'] = referer
+
+ get :index
+
+ expect(response).to redirect_to(new_user_session_url)
+ end
+ end
+
+ describe 'handling UnsafeRedirectError exceptions' do
+ controller do
+ def index
+ raise ActionController::Redirecting::UnsafeRedirectError
+ end
+ end
+
+ it 'tracks the Unsafe Redirect event and does not sign the user out' do
+ referer = '@@ABC'
+ request.env['HTTP_REFERER'] = referer
+ sign_in_as_user
+ expect(subject.current_user).to be_present
+
+ stub_analytics
+ event_properties = { controller: 'anonymous#index', user_signed_in: true, referer: referer }
+ expect(@analytics).to receive(:track_event).
+ with('Unsafe Redirect', event_properties)
+
+ get :index
+
+ expect(flash[:error]).to eq t('errors.general')
+ expect(response).to redirect_to(root_url)
+ expect(subject.current_user).to be_present
+ end
+
+ it 'redirects back to home page' do
+ get :index
+
+ expect(response).to redirect_to(new_user_session_url)
+ end
end
describe '#append_info_to_payload' do
diff --git a/spec/controllers/idv/capture_doc_status_controller_spec.rb b/spec/controllers/idv/capture_doc_status_controller_spec.rb
index 27721c9a4c7..035a8e29413 100644
--- a/spec/controllers/idv/capture_doc_status_controller_spec.rb
+++ b/spec/controllers/idv/capture_doc_status_controller_spec.rb
@@ -234,5 +234,19 @@
end
end
end
+
+ context 'when user opted for in-person proofing' do
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
+ create(:in_person_enrollment, :establishing, user: user, profile: nil)
+ end
+
+ it 'returns success with redirect' do
+ get :show
+
+ expect(response.status).to eq(200)
+ expect(JSON.parse(response.body)).to include('redirect' => idv_in_person_url)
+ end
+ end
end
end
diff --git a/spec/controllers/idv/doc_auth_controller_spec.rb b/spec/controllers/idv/doc_auth_controller_spec.rb
index d4b0ae65d6a..56ae560a832 100644
--- a/spec/controllers/idv/doc_auth_controller_spec.rb
+++ b/spec/controllers/idv/doc_auth_controller_spec.rb
@@ -235,6 +235,16 @@
attention_with_barcode: false,
}
end
+ let(:hard_fail_result) do
+ {
+ pii_from_doc: {},
+ success: false,
+ errors: { front: 'Wrong document' },
+ messages: ['message'],
+ attention_with_barcode: false,
+ doc_auth_result: 'Failed',
+ }
+ end
it 'returns status of success' do
set_up_document_capture_result(
@@ -284,14 +294,15 @@
errors: [{ field: 'front', message: 'Wrong document' }],
remaining_attempts: IdentityConfig.store.doc_auth_max_attempts,
ocr_pii: nil,
+ result_failed: false,
}.to_json,
)
end
- it 'returns status of fail with incomplete PII from doc auth' do
+ it 'returns status of hard fail' do
set_up_document_capture_result(
uuid: document_capture_session_uuid,
- idv_result: bad_pii_result,
+ idv_result: hard_fail_result,
)
put :update,
params: {
@@ -303,15 +314,40 @@
expect(response.body).to eq(
{
success: false,
- errors: [{ field: 'pii',
- message: I18n.t('doc_auth.errors.general.no_liveness') }],
+ errors: [{ field: 'front', message: 'Wrong document' }],
remaining_attempts: IdentityConfig.store.doc_auth_max_attempts,
ocr_pii: nil,
+ result_failed: true,
}.to_json,
)
- expect(@analytics).to have_received(:track_event).with(
- 'IdV: ' + "#{Analytics::DOC_AUTH} verify_document_status submitted".downcase, {
- errors: { pii: [I18n.t('doc_auth.errors.general.no_liveness')] },
+ end
+
+ it 'returns status of fail with incomplete PII from doc auth' do
+ set_up_document_capture_result(
+ uuid: document_capture_session_uuid,
+ idv_result: bad_pii_result,
+ )
+
+ expect(@analytics).to receive(:track_event).with(
+ "IdV: #{Analytics::DOC_AUTH.downcase} image upload vendor pii validation", include(
+ errors: include(
+ pii: [I18n.t('doc_auth.errors.general.no_liveness')],
+ ),
+ error_details: { pii: [I18n.t('doc_auth.errors.general.no_liveness')] },
+ attention_with_barcode: false,
+ success: false,
+ remaining_attempts: IdentityConfig.store.doc_auth_max_attempts,
+ flow_path: 'standard',
+ pii_like_keypaths: [[:pii]],
+ user_id: nil,
+ )
+ )
+
+ expect(@analytics).to receive(:track_event).with(
+ "IdV: #{Analytics::DOC_AUTH.downcase} verify_document_status submitted", include(
+ errors: include(
+ pii: [I18n.t('doc_auth.errors.general.no_liveness')],
+ ),
error_details: { pii: [I18n.t('doc_auth.errors.general.no_liveness')] },
attention_with_barcode: false,
success: false,
@@ -320,7 +356,26 @@
flow_path: 'standard',
step_count: 1,
pii_like_keypaths: [[:pii]],
- }
+ doc_auth_result: nil,
+ )
+ )
+
+ put :update,
+ params: {
+ step: 'verify_document_status',
+ document_capture_session_uuid: document_capture_session_uuid,
+ }
+
+ expect(response.status).to eq(400)
+ expect(response.body).to eq(
+ {
+ success: false,
+ errors: [{ field: 'pii',
+ message: I18n.t('doc_auth.errors.general.no_liveness') }],
+ remaining_attempts: IdentityConfig.store.doc_auth_max_attempts,
+ ocr_pii: nil,
+ result_failed: false,
+ }.to_json,
)
end
end
diff --git a/spec/controllers/idv/gpo_verify_controller_spec.rb b/spec/controllers/idv/gpo_verify_controller_spec.rb
index 7aefd6ae7c6..fa192bff695 100644
--- a/spec/controllers/idv/gpo_verify_controller_spec.rb
+++ b/spec/controllers/idv/gpo_verify_controller_spec.rb
@@ -5,7 +5,15 @@
let(:success) { true }
let(:otp) { 'ABC123' }
let(:submitted_otp) { otp }
- let(:pending_profile) { build(:profile) }
+ let(:pending_profile) {
+ create(
+ :profile,
+ :with_pii,
+ user: user,
+ proofing_components: proofing_components,
+ )
+ }
+ let(:proofing_components) { nil }
let(:user) { create(:user) }
before do
@@ -96,6 +104,7 @@
'IdV: GPO verification submitted',
success: true,
errors: {},
+ pending_in_person_enrollment: false,
pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
)
@@ -106,6 +115,44 @@
expect(disavowal_event_count).to eq 1
expect(response).to redirect_to(sign_up_completed_url)
end
+
+ it 'dispatches account verified alert' do
+ expect(UserAlerts::AlertUserAboutAccountVerified).to receive(:call)
+
+ action
+ end
+
+ context 'with establishing in person enrollment' do
+ let(:proofing_components) {
+ ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS)
+ }
+
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
+ allow(controller).to receive(:pii).
+ and_return(user.pending_profile.decrypt_pii(user.password).to_h)
+ end
+
+ it 'redirects to ready to verify screen' do
+ expect(@analytics).to receive(:track_event).with(
+ 'IdV: GPO verification submitted',
+ success: true,
+ errors: {},
+ pending_in_person_enrollment: true,
+ pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
+ )
+
+ action
+
+ expect(response).to redirect_to(idv_in_person_ready_to_verify_url)
+ end
+
+ it 'does not dispatch account verified alert' do
+ expect(UserAlerts::AlertUserAboutAccountVerified).not_to receive(:call)
+
+ action
+ end
+ end
end
context 'with an invalid form' do
@@ -116,6 +163,7 @@
'IdV: GPO verification submitted',
success: false,
errors: { otp: [t('errors.messages.confirmation_code_incorrect')] },
+ pending_in_person_enrollment: false,
error_details: { otp: [:confirmation_code_incorrect] },
pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
)
@@ -142,6 +190,7 @@
'IdV: GPO verification submitted',
success: false,
errors: { otp: [t('errors.messages.confirmation_code_incorrect')] },
+ pending_in_person_enrollment: false,
error_details: { otp: [:confirmation_code_incorrect] },
pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
).exactly(max_attempts).times
diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb
index 1691be5c6a0..cb60cad2084 100644
--- a/spec/controllers/idv/image_uploads_controller_spec.rb
+++ b/spec/controllers/idv/image_uploads_controller_spec.rb
@@ -159,6 +159,7 @@
success: false,
errors: [{ field: 'front', message: 'Please fill in this field.' }],
remaining_attempts: Throttle.max_attempts(:idv_doc_auth) - 2,
+ result_failed: false,
ocr_pii: nil,
},
)
@@ -176,6 +177,7 @@
errors: [{ field: 'limit', message: 'We could not verify your ID' }],
redirect: idv_session_errors_throttled_url,
remaining_attempts: 0,
+ result_failed: false,
ocr_pii: nil,
},
)
diff --git a/spec/controllers/idv/in_person/usps_locations_controller_spec.rb b/spec/controllers/idv/in_person/usps_locations_controller_spec.rb
new file mode 100644
index 00000000000..7c4cfaf4804
--- /dev/null
+++ b/spec/controllers/idv/in_person/usps_locations_controller_spec.rb
@@ -0,0 +1,99 @@
+require 'rails_helper'
+
+describe Idv::InPerson::UspsLocationsController do
+ include IdvHelper
+
+ let(:user) { create(:user) }
+ let(:in_person_proofing_enabled) { false }
+ let(:selected_location) do
+ {
+ usps_location: {
+ formatted_city_state_zip: 'BALTIMORE, MD, 21233-9715',
+ name: 'BALTIMORE',
+ phone: '410-555-1212',
+ saturday_hours: '8:30 AM - 5:00 PM',
+ street_address: '123 Fake St.',
+ sunday_hours: 'Closed',
+ weekday_hours: '8:30 AM - 7:00 PM',
+ },
+ }
+ end
+
+ before do
+ stub_analytics
+ stub_sign_in(user)
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).
+ and_return(in_person_proofing_enabled)
+ end
+
+ describe '#index' do
+ let(:proofer) { double('Proofer') }
+ let(:locations) {
+ [
+ { name: 'Location 1' },
+ { name: 'Location 2' },
+ { name: 'Location 3' },
+ { name: 'Location 4' },
+ ]
+ }
+ subject(:response) { get :index }
+
+ before do
+ allow(UspsInPersonProofing::Proofer).to receive(:new).and_return(proofer)
+ end
+
+ context 'with successful fetch' do
+ before do
+ allow(proofer).to receive(:request_pilot_facilities).and_return(locations)
+ end
+
+ it 'gets successful pilot response' do
+ response = get :index
+ json = response.body
+ facilities = JSON.parse(json)
+ expect(facilities.length).to eq 4
+ end
+ end
+
+ context 'with unsuccessful fetch' do
+ before do
+ exception = Faraday::ConnectionFailed.new('error')
+ allow(proofer).to receive(:request_pilot_facilities).and_raise(exception)
+ end
+
+ it 'gets an empty pilot response' do
+ response = get :index
+ json = response.body
+ facilities = JSON.parse(json)
+ expect(facilities.length).to eq 0
+ end
+ end
+ end
+
+ describe '#update' do
+ it 'writes the passed location to in-person enrollment' do
+ put :update, params: selected_location
+
+ expect(user.reload.establishing_in_person_enrollment.selected_location_details).to eq(
+ selected_location[:usps_location].as_json,
+ )
+ end
+
+ context 'with hybrid user' do
+ let(:user) { nil }
+ let(:effective_user) { create(:user) }
+
+ before do
+ session[:doc_capture_user_id] = effective_user.id
+ end
+
+ it 'writes the passed location to in-person enrollment associated with effective user' do
+ put :update, params: selected_location
+
+ expect(
+ effective_user.reload.establishing_in_person_enrollment.selected_location_details,
+ ).to eq(selected_location[:usps_location].as_json)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/idv/phone_controller_spec.rb b/spec/controllers/idv/phone_controller_spec.rb
index e80f5700f48..7bc08e7106c 100644
--- a/spec/controllers/idv/phone_controller_spec.rb
+++ b/spec/controllers/idv/phone_controller_spec.rb
@@ -389,17 +389,18 @@
end
it 'tracks throttled event' do
- put :create, params: { idv_phone_form: { phone: bad_phone } }
-
stub_analytics
allow(@analytics).to receive(:track_event)
expect(@analytics).to receive(:track_event).with(
'Throttler Rate Limit Triggered',
- throttle_type: :proof_address,
- step_name: a_kind_of(Symbol),
+ {
+ throttle_type: :proof_address,
+ step_name: :phone,
+ },
)
+ put :create, params: { idv_phone_form: { phone: bad_phone } }
get :new
end
end
diff --git a/spec/controllers/idv/review_controller_spec.rb b/spec/controllers/idv/review_controller_spec.rb
index efc94e05bd9..bc3a856c817 100644
--- a/spec/controllers/idv/review_controller_spec.rb
+++ b/spec/controllers/idv/review_controller_spec.rb
@@ -207,7 +207,10 @@ def show
expect(flash.now[:success]).to eq(
t(
'idv.messages.review.info_verified_html',
- phone_message: "#{t('idv.messages.phone.phone_of_record')} ",
+ phone_message: ActionController::Base.helpers.content_tag(
+ :strong,
+ t('idv.messages.phone.phone_of_record'),
+ ),
),
)
end
@@ -355,6 +358,12 @@ def show
expect(profile).to be_active
end
+ it 'dispatches account verified alert' do
+ expect(UserAlerts::AlertUserAboutAccountVerified).to receive(:call)
+
+ put :create, params: { user: { password: ControllerHelper::VALID_PASSWORD } }
+ end
+
it 'creates an `account_verified` event once per confirmation' do
put :create, params: { user: { password: ControllerHelper::VALID_PASSWORD } }
disavowal_event_count = user.events.where(event_type: :account_verified, ip: '0.0.0.0').
@@ -374,6 +383,22 @@ def show
expect(response).to redirect_to idv_app_url
end
end
+
+ context 'with in person enrollment' do
+ before do
+ ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS)
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
+ idv_session.applicant = Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE.merge(
+ same_address_as_id: true,
+ ).as_json
+ end
+
+ it 'does not dispatch account verified alert' do
+ expect(UserAlerts::AlertUserAboutAccountVerified).not_to receive(:call)
+
+ put :create, params: { user: { password: ControllerHelper::VALID_PASSWORD } }
+ end
+ end
end
context 'user picked GPO confirmation' do
diff --git a/spec/controllers/idv/session_errors_controller_spec.rb b/spec/controllers/idv/session_errors_controller_spec.rb
index beea0a52cea..9927ff69e3c 100644
--- a/spec/controllers/idv/session_errors_controller_spec.rb
+++ b/spec/controllers/idv/session_errors_controller_spec.rb
@@ -77,6 +77,20 @@
expect(assigns(:remaining_attempts)).to be_kind_of(Numeric)
end
+
+ it 'assigns URL to try again' do
+ get action
+
+ expect(assigns(:try_again_path)).to eq(idv_doc_auth_path)
+ end
+
+ context 'referrer is page from In-Person Proofing flow' do
+ it 'assigns URL to try again' do
+ get action, params: { from: idv_in_person_ready_to_verify_path }
+
+ expect(assigns(:try_again_path)).to eq(idv_in_person_path)
+ end
+ end
end
end
diff --git a/spec/controllers/idv/sessions_controller_spec.rb b/spec/controllers/idv/sessions_controller_spec.rb
index f4a5fc265fe..6d0f7d567b1 100644
--- a/spec/controllers/idv/sessions_controller_spec.rb
+++ b/spec/controllers/idv/sessions_controller_spec.rb
@@ -17,6 +17,7 @@
allow(idv_session).to receive(:clear)
allow(subject).to receive(:idv_session).and_return(idv_session)
controller.user_session['idv/doc_auth'] = flow_session
+ controller.user_session['idv/in_person'] = flow_session
controller.user_session[:decrypted_pii] = pii
end
@@ -26,6 +27,7 @@
delete :destroy
expect(controller.user_session['idv/doc_auth']).to be_blank
+ expect(controller.user_session['idv/in_person']).to be_blank
expect(controller.user_session[:decrypted_pii]).to be_blank
end
@@ -49,7 +51,7 @@
let(:user) do
create(
:user,
- profiles: [create(:profile, deactivation_reason: :verification_pending)],
+ profiles: [create(:profile, deactivation_reason: :gpo_verification_pending)],
)
end
diff --git a/spec/controllers/openid_connect/token_controller_spec.rb b/spec/controllers/openid_connect/token_controller_spec.rb
index 03be4f8b7a8..c1ca44c68f7 100644
--- a/spec/controllers/openid_connect/token_controller_spec.rb
+++ b/spec/controllers/openid_connect/token_controller_spec.rb
@@ -53,11 +53,12 @@
it 'tracks a successful event in analytics' do
stub_analytics
expect(@analytics).to receive(:track_event).
- with('OpenID Connect: token',
- success: true,
- client_id: client_id,
- user_id: user.uuid,
- errors: {})
+ with('OpenID Connect: token', {
+ success: true,
+ client_id: client_id,
+ user_id: user.uuid,
+ errors: {},
+ })
action
end
end
@@ -77,12 +78,13 @@
it 'tracks an unsuccessful event in analytics' do
stub_analytics
expect(@analytics).to receive(:track_event).
- with('OpenID Connect: token',
- success: false,
- client_id: client_id,
- user_id: user.uuid,
- errors: hash_including(:grant_type),
- error_details: hash_including(:grant_type))
+ with('OpenID Connect: token', {
+ success: false,
+ client_id: client_id,
+ user_id: user.uuid,
+ errors: hash_including(:grant_type),
+ error_details: hash_including(:grant_type),
+ })
action
end
diff --git a/spec/controllers/saml_idp_controller_spec.rb b/spec/controllers/saml_idp_controller_spec.rb
index 89b45b4876a..9eda5cf5a2f 100644
--- a/spec/controllers/saml_idp_controller_spec.rb
+++ b/spec/controllers/saml_idp_controller_spec.rb
@@ -537,27 +537,28 @@ def name_id_version(format_urn)
it 'tracks IAL2 authentication events' do
stub_analytics
expect(@analytics).to receive(:track_event).
- with('SAML Auth Request',
- requested_ial: authn_context,
- service_provider: sp1_issuer)
+ with('SAML Auth Request', {
+ requested_ial: authn_context,
+ service_provider: sp1_issuer,
+ })
expect(@analytics).to receive(:track_event).
- with('SAML Auth',
- success: true,
- errors: {},
- nameid_format: Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT,
- authn_context: [authn_context],
- authn_context_comparison: 'exact',
- requested_ial: authn_context,
- service_provider: sp1_issuer,
- endpoint: '/api/saml/auth2022',
- idv: false,
- finish_profile: false)
+ with('SAML Auth', {
+ success: true,
+ errors: {},
+ nameid_format: Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT,
+ authn_context: [authn_context],
+ authn_context_comparison: 'exact',
+ requested_ial: authn_context,
+ service_provider: sp1_issuer,
+ endpoint: '/api/saml/auth2022',
+ idv: false,
+ finish_profile: false,
+ })
expect(@analytics).to receive(:track_event).
- with(
- 'SP redirect initiated',
+ with('SP redirect initiated', {
ial: ial,
billed_ial: [ial, 2].min,
- )
+ })
allow(controller).to receive(:identity_needs_verification?).and_return(false)
saml_get_auth(ial2_settings)
@@ -694,27 +695,25 @@ def name_id_version(format_urn)
it 'tracks IAL2 authentication events' do
stub_analytics
expect(@analytics).to receive(:track_event).
- with('SAML Auth Request',
- requested_ial: 'ialmax',
- service_provider: sp1_issuer)
+ with('SAML Auth Request', {
+ requested_ial: 'ialmax',
+ service_provider: sp1_issuer,
+ })
expect(@analytics).to receive(:track_event).
- with('SAML Auth',
- success: true,
- errors: {},
- nameid_format: Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT,
- authn_context: ['http://idmanagement.gov/ns/assurance/ial/1'],
- authn_context_comparison: 'minimum',
- requested_ial: 'ialmax',
- service_provider: sp1_issuer,
- endpoint: '/api/saml/auth2022',
- idv: false,
- finish_profile: false)
+ with('SAML Auth', {
+ success: true,
+ errors: {},
+ nameid_format: Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT,
+ authn_context: ['http://idmanagement.gov/ns/assurance/ial/1'],
+ authn_context_comparison: 'minimum',
+ requested_ial: 'ialmax',
+ service_provider: sp1_issuer,
+ endpoint: '/api/saml/auth2022',
+ idv: false,
+ finish_profile: false,
+ })
expect(@analytics).to receive(:track_event).
- with(
- 'SP redirect initiated',
- ial: 0,
- billed_ial: 2,
- )
+ with('SP redirect initiated', { ial: 0, billed_ial: 2 })
allow(controller).to receive(:identity_needs_verification?).and_return(false)
saml_get_auth(ialmax_settings)
@@ -1380,9 +1379,10 @@ def name_id_version(format_urn)
it 'logs SAML Auth Request but does not log SAML Auth' do
stub_analytics
expect(@analytics).to receive(:track_event).
- with('SAML Auth Request',
- requested_ial: Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF,
- service_provider: 'http://localhost:3000')
+ with('SAML Auth Request', {
+ requested_ial: Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF,
+ service_provider: 'http://localhost:3000',
+ })
saml_get_auth(saml_settings)
end
@@ -1828,9 +1828,10 @@ def stub_auth
}
expect(@analytics).to receive(:track_event).
- with('SAML Auth Request',
- requested_ial: Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF,
- service_provider: 'http://localhost:3000')
+ with('SAML Auth Request', {
+ requested_ial: Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF,
+ service_provider: 'http://localhost:3000',
+ })
expect(@analytics).to receive(:track_event).
with('SAML Auth', analytics_hash)
@@ -1870,16 +1871,13 @@ def stub_requested_attributes
}
expect(@analytics).to receive(:track_event).
- with('SAML Auth Request',
- requested_ial: Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF,
- service_provider: 'http://localhost:3000')
+ with('SAML Auth Request', {
+ requested_ial: Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF,
+ service_provider: 'http://localhost:3000',
+ })
expect(@analytics).to receive(:track_event).with('SAML Auth', analytics_hash)
expect(@analytics).to receive(:track_event).
- with(
- 'SP redirect initiated',
- ial: 1,
- billed_ial: 1,
- )
+ with('SP redirect initiated', { ial: 1, billed_ial: 1 })
generate_saml_response(user)
end
@@ -1907,16 +1905,16 @@ def stub_requested_attributes
}
expect(@analytics).to receive(:track_event).
- with('SAML Auth Request',
- requested_ial: Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF,
- service_provider: 'http://localhost:3000')
+ with('SAML Auth Request', {
+ requested_ial: Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF,
+ service_provider: 'http://localhost:3000',
+ })
expect(@analytics).to receive(:track_event).with('SAML Auth', analytics_hash)
expect(@analytics).to receive(:track_event).
- with(
- 'SP redirect initiated',
+ with('SP redirect initiated', {
ial: 1,
billed_ial: 1,
- )
+ })
generate_saml_response(user)
end
diff --git a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb
index a1b8c2e1308..5e796cd5155 100644
--- a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb
+++ b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb
@@ -56,6 +56,8 @@
area_code: parsed_phone.area_code,
country_code: parsed_phone.country,
phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164),
+ enabled_mfa_methods_count: 1,
+ in_multi_mfa_selection_flow: false,
}
expect(@analytics).to receive(:track_event).
@@ -102,6 +104,8 @@
area_code: parsed_phone.area_code,
country_code: parsed_phone.country,
phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164),
+ enabled_mfa_methods_count: 1,
+ in_multi_mfa_selection_flow: false,
}
stub_analytics
expect(@analytics).to receive(:track_mfa_submit_event).
@@ -151,6 +155,8 @@
area_code: parsed_phone.area_code,
country_code: parsed_phone.country,
phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164),
+ enabled_mfa_methods_count: 1,
+ in_multi_mfa_selection_flow: false,
}
stub_analytics
@@ -206,6 +212,8 @@
area_code: parsed_phone.area_code,
country_code: parsed_phone.country,
phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164),
+ enabled_mfa_methods_count: 1,
+ in_multi_mfa_selection_flow: false,
}
stub_analytics
@@ -334,6 +342,8 @@
area_code: parsed_phone.area_code,
country_code: parsed_phone.country,
phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164),
+ enabled_mfa_methods_count: 1,
+ in_multi_mfa_selection_flow: false,
}
expect(@analytics).to receive(:track_event).
@@ -400,6 +410,8 @@
area_code: parsed_phone.area_code,
country_code: parsed_phone.country,
phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164),
+ enabled_mfa_methods_count: 1,
+ in_multi_mfa_selection_flow: false,
}
expect(@analytics).to have_received(:track_event).
@@ -448,6 +460,8 @@
area_code: parsed_phone.area_code,
country_code: parsed_phone.country,
phone_fingerprint: Pii::Fingerprinter.fingerprint(parsed_phone.e164),
+ enabled_mfa_methods_count: 0,
+ in_multi_mfa_selection_flow: false,
}
expect(@analytics).to have_received(:track_event).
diff --git a/spec/controllers/users/mfa_selection_controller_spec.rb b/spec/controllers/users/mfa_selection_controller_spec.rb
index 0535b0fb8b0..d1660277bef 100644
--- a/spec/controllers/users/mfa_selection_controller_spec.rb
+++ b/spec/controllers/users/mfa_selection_controller_spec.rb
@@ -36,8 +36,9 @@
params = ActionController::Parameters.new(voice_params)
response = FormResponse.new(success: true, errors: {}, extra: { selection: ['voice'] })
+ form_params = { user: user, aal3_required: false, piv_cac_required: nil }
form = instance_double(TwoFactorOptionsForm)
- allow(TwoFactorOptionsForm).to receive(:new).with(user).and_return(form)
+ allow(TwoFactorOptionsForm).to receive(:new).with(form_params).and_return(form)
expect(form).to receive(:submit).
with(params.require(:two_factor_options_form).permit(:selection)).
and_return(response)
diff --git a/spec/controllers/users/passwords_controller_spec.rb b/spec/controllers/users/passwords_controller_spec.rb
index bbf18c82d6a..c869f2048d7 100644
--- a/spec/controllers/users/passwords_controller_spec.rb
+++ b/spec/controllers/users/passwords_controller_spec.rb
@@ -47,6 +47,16 @@
patch :update, params: { update_user_password_form: params }
end
+ it 'sends a security event' do
+ user = create(:user)
+ stub_sign_in(user)
+ security_event = PushNotification::PasswordResetEvent.new(user: user)
+ expect(PushNotification::HttpPush).to receive(:deliver).with(security_event)
+
+ params = { password: 'salty new password' }
+ patch :update, params: { update_user_password_form: params }
+ end
+
it 'sends the user an email' do
user = create(:user)
mail = double
diff --git a/spec/controllers/users/reset_passwords_controller_spec.rb b/spec/controllers/users/reset_passwords_controller_spec.rb
index 4de4f5a4f23..8127559f321 100644
--- a/spec/controllers/users/reset_passwords_controller_spec.rb
+++ b/spec/controllers/users/reset_passwords_controller_spec.rb
@@ -194,6 +194,9 @@
old_confirmed_at = user.reload.confirmed_at
allow(user).to receive(:active_profile).and_return(nil)
+ security_event = PushNotification::PasswordResetEvent.new(user: user)
+ expect(PushNotification::HttpPush).to receive(:deliver).with(security_event)
+
stub_user_mailer(user)
password = 'a really long passw0rd'
@@ -235,6 +238,9 @@
)
_profile = create(:profile, :active, :verified, user: user)
+ security_event = PushNotification::PasswordResetEvent.new(user: user)
+ expect(PushNotification::HttpPush).to receive(:deliver).with(security_event)
+
stub_user_mailer(user)
get :edit, params: { reset_password_token: raw_reset_token }
@@ -274,6 +280,9 @@
reset_password_sent_at: Time.zone.now,
)
+ security_event = PushNotification::PasswordResetEvent.new(user: user)
+ expect(PushNotification::HttpPush).to receive(:deliver).with(security_event)
+
stub_user_mailer(user)
password = 'a really long passw0rd'
diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb
index c77f2240f8f..56080339b47 100644
--- a/spec/controllers/users/sessions_controller_spec.rb
+++ b/spec/controllers/users/sessions_controller_spec.rb
@@ -128,6 +128,12 @@
sign_in_as_user
+ expect_any_instance_of(IrsAttemptsApi::Tracker).to receive(:logout_initiated).with(
+ user_uuid: controller.current_user.uuid,
+ unique_session_id: controller.current_user.unique_session_id,
+ success: true,
+ )
+
get :destroy
expect(controller.current_user).to be nil
end
@@ -146,6 +152,12 @@
sign_in_as_user
+ expect_any_instance_of(IrsAttemptsApi::Tracker).to receive(:logout_initiated).with(
+ user_uuid: controller.current_user.uuid,
+ unique_session_id: controller.current_user.unique_session_id,
+ success: true,
+ )
+
delete :destroy
expect(controller.current_user).to be nil
end
@@ -316,7 +328,7 @@
user = create(:user, :signed_up)
create(
:profile,
- deactivation_reason: :verification_pending,
+ deactivation_reason: :gpo_verification_pending,
user: user, pii: { ssn: '1234' }
)
@@ -564,7 +576,7 @@
it 'redirects to the verify profile page' do
profile = create(
:profile,
- deactivation_reason: :verification_pending,
+ deactivation_reason: :gpo_verification_pending,
pii: { ssn: '6666', dob: '1920-01-01' },
)
user = profile.user
diff --git a/spec/controllers/users/totp_setup_controller_spec.rb b/spec/controllers/users/totp_setup_controller_spec.rb
index cc4095b742c..3c68d10f6a2 100644
--- a/spec/controllers/users/totp_setup_controller_spec.rb
+++ b/spec/controllers/users/totp_setup_controller_spec.rb
@@ -108,6 +108,9 @@
totp_secret_present: true,
multi_factor_auth_method: 'totp',
auth_app_configuration_id: nil,
+ enabled_mfa_methods_count: 0,
+ in_multi_mfa_selection_flow: false,
+ pii_like_keypaths: [[:mfa_method_counts, :phone]],
}
expect(@analytics).to have_received(:track_event).
@@ -137,6 +140,9 @@
totp_secret_present: true,
multi_factor_auth_method: 'totp',
auth_app_configuration_id: next_auth_app_id,
+ enabled_mfa_methods_count: 2,
+ in_multi_mfa_selection_flow: false,
+ pii_like_keypaths: [[:mfa_method_counts, :phone]],
}
expect(@analytics).to have_received(:track_event).
@@ -167,6 +173,9 @@
totp_secret_present: true,
multi_factor_auth_method: 'totp',
auth_app_configuration_id: nil,
+ enabled_mfa_methods_count: 1,
+ in_multi_mfa_selection_flow: false,
+ pii_like_keypaths: [[:mfa_method_counts, :phone]],
}
expect(@analytics).to have_received(:track_event).
@@ -198,6 +207,9 @@
totp_secret_present: true,
multi_factor_auth_method: 'totp',
auth_app_configuration_id: nil,
+ enabled_mfa_methods_count: 1,
+ in_multi_mfa_selection_flow: false,
+ pii_like_keypaths: [[:mfa_method_counts, :phone]],
}
expect(@analytics).to have_received(:track_event).
@@ -228,6 +240,9 @@
totp_secret_present: true,
multi_factor_auth_method: 'totp',
auth_app_configuration_id: nil,
+ enabled_mfa_methods_count: 0,
+ in_multi_mfa_selection_flow: false,
+ pii_like_keypaths: [[:mfa_method_counts, :phone]],
}
expect(@analytics).to have_received(:track_event).
with('Multi-Factor Authentication Setup', result)
@@ -259,6 +274,9 @@
totp_secret_present: true,
multi_factor_auth_method: 'totp',
auth_app_configuration_id: next_auth_app_id,
+ enabled_mfa_methods_count: 1,
+ in_multi_mfa_selection_flow: true,
+ pii_like_keypaths: [[:mfa_method_counts, :phone]],
}
expect(@analytics).to have_received(:track_event).
@@ -269,7 +287,7 @@
context 'when user has multiple MFA methods left in user session' do
let(:mfa_selections) { ['auth_app', 'voice'] }
- it 'redirects to mfa confirmation path with a success message and still logs analytics' do
+ it 'redirects to next mfa path with a success message and still logs analytics' do
expect(response).to redirect_to(phone_setup_url)
result = {
@@ -278,6 +296,9 @@
totp_secret_present: true,
multi_factor_auth_method: 'totp',
auth_app_configuration_id: next_auth_app_id,
+ enabled_mfa_methods_count: 1,
+ in_multi_mfa_selection_flow: true,
+ pii_like_keypaths: [[:mfa_method_counts, :phone]],
}
expect(@analytics).to have_received(:track_event).
@@ -306,6 +327,9 @@
totp_secret_present: false,
multi_factor_auth_method: 'totp',
auth_app_configuration_id: nil,
+ enabled_mfa_methods_count: 0,
+ in_multi_mfa_selection_flow: false,
+ pii_like_keypaths: [[:mfa_method_counts, :phone]],
}
expect(@analytics).to have_received(:track_event).
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 f66ae7a4436..366a5b1d62c 100644
--- a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb
+++ b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb
@@ -67,8 +67,9 @@
params = ActionController::Parameters.new(voice_params)
response = FormResponse.new(success: true, errors: {}, extra: { selection: ['voice'] })
+ form_params = { user: user, aal3_required: false, piv_cac_required: nil }
form = instance_double(TwoFactorOptionsForm)
- allow(TwoFactorOptionsForm).to receive(:new).with(user).and_return(form)
+ allow(TwoFactorOptionsForm).to receive(:new).with(form_params).and_return(form)
expect(form).to receive(:submit).
with(params.require(:two_factor_options_form).permit(:selection)).
and_return(response)
@@ -87,6 +88,7 @@
enabled_mfa_methods_count: 0,
selection: ['voice', 'auth_app'],
success: true,
+ selected_mfa_count: 2,
errors: {},
}
diff --git a/spec/controllers/users/webauthn_setup_controller_spec.rb b/spec/controllers/users/webauthn_setup_controller_spec.rb
index 29ed30cf21a..a33a5545666 100644
--- a/spec/controllers/users/webauthn_setup_controller_spec.rb
+++ b/spec/controllers/users/webauthn_setup_controller_spec.rb
@@ -68,6 +68,7 @@
before do
allow(IdentityConfig.store).to receive(:domain_name).and_return('localhost:3000')
+ request.host = 'localhost:3000'
controller.user_session[:webauthn_challenge] = webauthn_challenge
end
@@ -80,27 +81,26 @@
multi_factor_auth_method: 'webauthn',
success: true,
errors: {},
+ in_multi_mfa_selection_flow: false,
pii_like_keypaths: [[:mfa_method_counts, :phone]],
}
expect(@analytics).to receive(:track_event).
with('Multi-Factor Authentication Setup', result)
expect(@analytics).to receive(:track_event).
- with(
- 'Multi-Factor Authentication: Added webauthn',
+ with('Multi-Factor Authentication: Added webauthn', {
enabled_mfa_methods_count: 3,
method_name: :webauthn,
platform_authenticator: false,
- )
+ })
expect(@analytics).to receive(:track_event).
- with(
- 'User Registration: MFA Setup Complete',
+ with('User Registration: MFA Setup Complete', {
enabled_mfa_methods_count: 3,
mfa_method_counts: { auth_app: 1, phone: 1, webauthn: 1 },
pii_like_keypaths: [[:mfa_method_counts, :phone]],
success: true,
- )
+ })
patch :confirm, params: params
end
@@ -172,6 +172,7 @@
stub_analytics
stub_sign_in(user)
allow(IdentityConfig.store).to receive(:domain_name).and_return('localhost:3000')
+ request.host = 'localhost:3000'
controller.user_session[:webauthn_challenge] = webauthn_challenge
end
context ' Multiple MFA options turned on' do
diff --git a/spec/features/idv/doc_auth/verify_step_spec.rb b/spec/features/idv/doc_auth/verify_step_spec.rb
index d4f7fb6043c..faa38652603 100644
--- a/spec/features/idv/doc_auth/verify_step_spec.rb
+++ b/spec/features/idv/doc_auth/verify_step_spec.rb
@@ -99,7 +99,9 @@
step_name: 'Idv::Steps::VerifyWaitStepShow',
remaining_attempts: 4,
)
- expect(page).to have_current_path(idv_session_errors_warning_path)
+ expect(page).to have_current_path(
+ idv_session_errors_warning_path(from: idv_doc_auth_step_path(step: :verify_wait)),
+ )
click_on t('idv.failure.button.warning')
@@ -118,7 +120,9 @@
step_name: 'Idv::Steps::VerifyWaitStepShow',
remaining_attempts: 5,
)
- expect(page).to have_current_path(idv_session_errors_exception_path)
+ expect(page).to have_current_path(
+ idv_session_errors_exception_path(from: idv_doc_auth_step_path(step: :verify_wait)),
+ )
click_on t('idv.failure.button.warning')
@@ -132,7 +136,9 @@
click_idv_continue
(max_attempts - 1).times do
click_idv_continue
- expect(page).to have_current_path(idv_session_errors_warning_path)
+ expect(page).to have_current_path(
+ idv_session_errors_warning_path(from: idv_doc_auth_step_path(step: :verify_wait)),
+ )
visit idv_doc_auth_verify_step
end
click_idv_continue
diff --git a/spec/features/idv/in_person_spec.rb b/spec/features/idv/in_person_spec.rb
index bab09da2b80..2210daa2d13 100644
--- a/spec/features/idv/in_person_spec.rb
+++ b/spec/features/idv/in_person_spec.rb
@@ -17,11 +17,13 @@
it 'works for a happy path', allow_browser_log: true do
user = user_with_2fa
+ sign_in_and_2fa_user(user)
begin_in_person_proofing(user)
# location page
expect(page).to have_content(t('in_person_proofing.headings.location'))
- complete_location_step(user)
+ bethesda_location = page.find_all('.location-collection-item')[1]
+ bethesda_location.click_button(t('in_person_proofing.body.location.location_button'))
# prepare page
expect(page).to have_content(t('in_person_proofing.headings.prepare'))
@@ -95,10 +97,87 @@
expect(page).to have_content(t('in_person_proofing.headings.barcode'))
expect(page).to have_content(Idv::InPerson::EnrollmentCodeFormatter.format(enrollment_code))
expect(page).to have_content(t('in_person_proofing.body.barcode.deadline', deadline: deadline))
+ expect(page).to have_content('BETHESDA')
+ expect(page).to have_content(
+ "#{t('date.day_names')[6]}: #{t('in_person_proofing.body.barcode.retail_hours_closed')}",
+ )
+
+ # signing in again before completing in-person proofing at a post office
+ sign_in_and_2fa_user(user)
+ complete_doc_auth_steps_before_welcome_step
+ expect(page).to have_current_path(idv_in_person_ready_to_verify_path)
+ end
+
+ it 'allows the user to cancel and start over from the beginning', allow_browser_log: true do
+ sign_in_and_2fa_user
+ begin_in_person_proofing
+ complete_all_in_person_proofing_steps
+
+ click_link t('links.cancel')
+ click_on t('idv.cancel.actions.start_over')
+
+ expect(page).to have_current_path(idv_doc_auth_welcome_step)
+ begin_in_person_proofing
+ complete_all_in_person_proofing_steps
+ end
+
+ context 'with hybrid document capture' do
+ before do
+ allow(FeatureManagement).to receive(:doc_capture_polling_enabled?).and_return(true)
+ allow(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config|
+ @sms_link = config[:link]
+ impl.call(**config)
+ end
+ end
+
+ it 'resumes desktop session with in-person proofing', allow_browser_log: true do
+ user = nil
+
+ perform_in_browser(:desktop) do
+ user = sign_in_and_2fa_user
+ complete_doc_auth_steps_before_send_link_step
+ fill_in :doc_auth_phone, with: '415-555-0199'
+ click_idv_continue
+ end
+
+ perform_in_browser(:mobile) do
+ visit @sms_link
+ mock_doc_auth_attention_with_barcode
+ attach_and_submit_images
+
+ click_button t('idv.troubleshooting.options.verify_in_person')
+
+ bethesda_location = page.find_all('.location-collection-item')[1]
+ bethesda_location.click_button(t('in_person_proofing.body.location.location_button'))
+
+ click_idv_continue
+
+ expect(page).to have_content(t('in_person_proofing.headings.switch_back'))
+ end
+
+ perform_in_browser(:desktop) do
+ expect(page).to have_current_path(idv_in_person_step_path(step: :state_id), wait: 10)
+
+ complete_state_id_step(user)
+ complete_address_step(user)
+ complete_ssn_step(user)
+ complete_verify_step(user)
+ complete_phone_step(user)
+ complete_review_step(user)
+ acknowledge_and_confirm_personal_key
+
+ expect(page).to have_content('BETHESDA')
+ end
+ end
end
context 'verify address by mail (GPO letter)' do
+ before do
+ allow(FeatureManagement).to receive(:reveal_gpo_code?).and_return(true)
+ end
+
it 'requires address verification before showing instructions', allow_browser_log: true do
+ sign_in_and_2fa_user
begin_in_person_proofing
complete_all_in_person_proofing_steps
click_on t('idv.troubleshooting.options.verify_by_mail')
@@ -109,7 +188,29 @@
expect(page).to have_content(t('idv.titles.come_back_later'))
expect(page).to have_current_path(idv_come_back_later_path)
- # WILLFIX: After LG-6897, assert that "Ready to Verify" content is shown after code entry.
+ click_idv_continue
+ expect(page).to have_current_path(account_path)
+ expect(page).not_to have_content(t('headings.account.verified_account'))
+ click_on t('account.index.verification.reactivate_button')
+ click_button t('forms.verify_profile.submit')
+
+ expect(page).to have_current_path(idv_in_person_ready_to_verify_path)
+ expect(page).not_to have_content(t('account.index.verification.success'))
+ end
+
+ it 'lets the user clear and start over from gpo confirmation', allow_browser_log: true do
+ sign_in_and_2fa_user
+ begin_in_person_proofing
+ complete_all_in_person_proofing_steps
+ click_on t('idv.troubleshooting.options.verify_by_mail')
+ click_on t('idv.buttons.mail.send')
+ complete_review_step
+ acknowledge_and_confirm_personal_key
+ click_idv_continue
+ click_on t('account.index.verification.reactivate_button')
+ click_on t('idv.messages.clear_and_start_over')
+
+ expect(page).to have_current_path(idv_doc_auth_welcome_step)
end
end
end
diff --git a/spec/features/idv/steps/gpo_step_spec.rb b/spec/features/idv/steps/gpo_step_spec.rb
index a57e024c93c..6e061cc639c 100644
--- a/spec/features/idv/steps/gpo_step_spec.rb
+++ b/spec/features/idv/steps/gpo_step_spec.rb
@@ -64,7 +64,7 @@ def expect_user_to_be_unverified(user)
profile = user.profiles.first
expect(profile.active?).to eq false
- expect(profile.deactivation_reason).to eq 'verification_pending'
+ expect(profile.deactivation_reason).to eq 'gpo_verification_pending'
end
end
end
diff --git a/spec/features/idv/strict_ial2/usps_upload_disallowed_spec.rb b/spec/features/idv/strict_ial2/usps_upload_disallowed_spec.rb
index 60f8137e511..a8adef0377d 100644
--- a/spec/features/idv/strict_ial2/usps_upload_disallowed_spec.rb
+++ b/spec/features/idv/strict_ial2/usps_upload_disallowed_spec.rb
@@ -51,7 +51,7 @@
it 'does not prompt a pending user for a mailed code' do
user = create(
:profile,
- deactivation_reason: :verification_pending,
+ deactivation_reason: :gpo_verification_pending,
pii: { first_name: 'John', ssn: '111223333' },
).user
diff --git a/spec/features/openid_connect/aal3_required_spec.rb b/spec/features/openid_connect/aal3_required_spec.rb
index 1d7e77734c5..5bf1a2930d2 100644
--- a/spec/features/openid_connect/aal3_required_spec.rb
+++ b/spec/features/openid_connect/aal3_required_spec.rb
@@ -47,6 +47,21 @@
expect(page).to have_content(t('two_factor_authentication.two_factor_aal3_choice'))
expect(page).to have_xpath("//img[@alt='important alert icon']")
end
+
+ it 'throws an error if user doesnt select AAL3 auth' do
+ user = user_with_2fa
+
+ visit_idp_from_ial1_oidc_sp_defaulting_to_aal3(prompt: 'select_account')
+ sign_in_live_with_2fa(user)
+
+ expect(current_url).to eq(authentication_methods_setup_url)
+ expect(page).to have_content(t('two_factor_authentication.two_factor_aal3_choice'))
+ expect(page).to have_xpath("//img[@alt='important alert icon']")
+
+ click_continue
+
+ expect(page).to have_content(t('errors.two_factor_auth_setup.must_select_option'))
+ end
end
context 'user has aal3 auth configured' do
diff --git a/spec/features/saml/ial2_sso_spec.rb b/spec/features/saml/ial2_sso_spec.rb
index bc925fb60bf..830bfbbe9b2 100644
--- a/spec/features/saml/ial2_sso_spec.rb
+++ b/spec/features/saml/ial2_sso_spec.rb
@@ -85,7 +85,7 @@ def sign_out_user
let(:profile) do
create(
:profile,
- deactivation_reason: :verification_pending,
+ deactivation_reason: :gpo_verification_pending,
pii: { ssn: '6666', dob: '1920-01-01' },
)
end
diff --git a/spec/features/users/verify_profile_spec.rb b/spec/features/users/verify_profile_spec.rb
index 0b246e23abb..a979c0f966b 100644
--- a/spec/features/users/verify_profile_spec.rb
+++ b/spec/features/users/verify_profile_spec.rb
@@ -7,7 +7,7 @@
before do
profile = create(
:profile,
- deactivation_reason: :verification_pending,
+ deactivation_reason: :gpo_verification_pending,
pii: { ssn: '666-66-1234', dob: '1920-01-01', phone: '+1 703-555-9999' },
user: user,
)
diff --git a/spec/fixtures/usps_ipp_responses/enrollment_selected_location_details.json b/spec/fixtures/usps_ipp_responses/enrollment_selected_location_details.json
new file mode 100644
index 00000000000..76ef1d31cd7
--- /dev/null
+++ b/spec/fixtures/usps_ipp_responses/enrollment_selected_location_details.json
@@ -0,0 +1,10 @@
+{
+ "formatted_city_state_zip": "ARLINGTON, VA, 22201-9998",
+ "id": 4,
+ "name": "ARLINGTON",
+ "phone": "703-993-0072",
+ "saturday_hours": "9:00 AM - 1:00 PM",
+ "street_address": "3118 WASHINGTON BLVD",
+ "sunday_hours": "Closed",
+ "weekday_hours": "9:00 AM - 5:00 PM"
+}
diff --git a/spec/fixtures/usps_ipp_responses/request_show_usps_location_response.json b/spec/fixtures/usps_ipp_responses/request_show_usps_location_response.json
new file mode 100644
index 00000000000..9356efbf6ca
--- /dev/null
+++ b/spec/fixtures/usps_ipp_responses/request_show_usps_location_response.json
@@ -0,0 +1,10 @@
+{
+ "formattedCityStateZip": "ARLINGTON, VA, 22201-9998",
+ "id": 4,
+ "name": "ARLINGTON",
+ "phone": "703-993-0072",
+ "saturdayHours": "9:00 AM - 1:00 PM",
+ "streetAddress": "3118 WASHINGTON BLVD",
+ "sundayHours": "Closed",
+ "weekdayHours": "9:00 AM - 5:00 PM"
+}
diff --git a/spec/forms/api/profile_creation_form_spec.rb b/spec/forms/api/profile_creation_form_spec.rb
index bc351f67790..7c1330ca326 100644
--- a/spec/forms/api/profile_creation_form_spec.rb
+++ b/spec/forms/api/profile_creation_form_spec.rb
@@ -78,6 +78,35 @@
expect(stored_pii['first_name']).to eq 'Ada'
end
+
+ context 'with establishing in person enrollment' do
+ let!(:enrollment) do
+ create(:in_person_enrollment, :establishing, user: user, profile: nil)
+ end
+
+ before do
+ ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS)
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
+ end
+
+ it 'sets profile to pending in person verification' do
+ subject.submit
+ profile = user.profiles.first
+
+ expect(profile.active?).to be false
+ expect(profile.deactivation_reason).to eq('in_person_verification_pending')
+ end
+
+ it 'saves in person enrollment' do
+ expect(UspsInPersonProofing::EnrollmentHelper).
+ to receive(:schedule_in_person_enrollment).
+ with(user, Pii::Attributes.new_from_hash(pii))
+
+ subject.submit
+
+ expect(enrollment.reload.profile).to eq(user.profiles.last)
+ end
+ end
end
context 'with the user having verified their address via GPO letter' do
@@ -92,6 +121,7 @@
profile = user.profiles.first
expect(profile.active?).to be false
+ expect(profile.deactivation_reason).to eq('gpo_verification_pending')
end
it 'moves the pii to the user_session' do
@@ -121,6 +151,21 @@
expect(subject.gpo_code).to eq(gpo_code)
end
end
+
+ context 'with establishing in person enrollment' do
+ before do
+ ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS)
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
+ end
+
+ it 'does not activate the user profile' do
+ subject.submit
+ profile = user.profiles.first
+
+ expect(profile.active?).to be false
+ expect(profile.deactivation_reason).to eq('gpo_verification_pending')
+ end
+ end
end
end
diff --git a/spec/forms/gpo_verify_form_spec.rb b/spec/forms/gpo_verify_form_spec.rb
index 2032ba1cd8a..4881c8d1627 100644
--- a/spec/forms/gpo_verify_form_spec.rb
+++ b/spec/forms/gpo_verify_form_spec.rb
@@ -2,14 +2,23 @@
describe GpoVerifyForm do
subject(:form) do
- GpoVerifyForm.new(user: user, otp: entered_otp)
+ GpoVerifyForm.new(user: user, pii: applicant, otp: entered_otp)
end
- let(:user) { pending_profile.user }
+ let(:user) { create(:user, :signed_up) }
+ let(:applicant) { Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE.merge(same_address_as_id: true) }
let(:entered_otp) { otp }
let(:otp) { 'ABC123' }
let(:code_sent_at) { Time.zone.now }
- let(:pending_profile) { create(:profile, deactivation_reason: :verification_pending) }
+ let(:pending_profile) {
+ create(
+ :profile,
+ user: user,
+ deactivation_reason: :gpo_verification_pending,
+ proofing_components: proofing_components,
+ )
+ }
+ let(:proofing_components) { nil }
before do
next if pending_profile.blank?
@@ -101,6 +110,36 @@
expect(pending_profile.reload).to be_active
end
+
+ context 'pending in person enrollment' do
+ let!(:enrollment) do
+ create(:in_person_enrollment, :establishing, profile: pending_profile, user: user)
+ end
+ let(:proofing_components) {
+ ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS)
+ }
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
+ end
+
+ it 'sets profile to pending in person verification' do
+ subject.submit
+ pending_profile.reload
+
+ expect(pending_profile).not_to be_active
+ expect(pending_profile.deactivation_reason).to eq('in_person_verification_pending')
+ end
+
+ it 'updates establishing in-person enrollment to pending' do
+ subject.submit
+
+ enrollment.reload
+
+ expect(enrollment.status).to eq('pending')
+ expect(enrollment.user_id).to eq(user.id)
+ expect(enrollment.enrollment_code).to be_a(String)
+ end
+ end
end
context 'incorrect OTP' do
diff --git a/spec/forms/idv/api_document_verification_status_form_spec.rb b/spec/forms/idv/api_document_verification_status_form_spec.rb
index 431f481d1ec..402d02817d9 100644
--- a/spec/forms/idv/api_document_verification_status_form_spec.rb
+++ b/spec/forms/idv/api_document_verification_status_form_spec.rb
@@ -86,5 +86,18 @@
response = form.submit
expect(response.extra[:remaining_attempts]).to be_a_kind_of(Numeric)
end
+
+ it 'includes doc_auth_result' do
+ response = form.submit
+ expect(response.extra[:doc_auth_result]).to be_nil
+
+ expect(async_state).to receive(:result).and_return(doc_auth_result: nil)
+ response = form.submit
+ expect(response.extra[:doc_auth_result]).to be_nil
+
+ expect(async_state).to receive(:result).and_return(doc_auth_result: 'Failed')
+ response = form.submit
+ expect(response.extra[:doc_auth_result]).to eq('Failed')
+ end
end
end
diff --git a/spec/forms/idv/inherited_proofing/va/form_spec.rb b/spec/forms/idv/inherited_proofing/va/form_spec.rb
new file mode 100644
index 00000000000..ba74559849d
--- /dev/null
+++ b/spec/forms/idv/inherited_proofing/va/form_spec.rb
@@ -0,0 +1,216 @@
+require 'rails_helper'
+
+RSpec.shared_examples 'the hash is blank?' do
+ it 'raises an error' do
+ expect { subject }.to raise_error 'payload_hash is blank?'
+ end
+end
+
+RSpec.describe Idv::InheritedProofing::Va::Form do
+ subject(:form) { described_class.new payload_hash: payload_hash }
+
+ let(:payload_hash) do
+ {
+ first_name: 'Henry',
+ last_name: 'Ford',
+ phone: '12222222222',
+ birth_date: '2000-01-01',
+ ssn: '111223333',
+ address: {
+ street: '1234 Model Street',
+ street2: 'Suite A',
+ city: 'Detroit',
+ state: 'MI',
+ country: 'United States',
+ zip: '12345',
+ },
+ }
+ end
+
+ describe 'class methods' do
+ describe '.model_name' do
+ it 'returns the right model name' do
+ expect(described_class.model_name).to eq 'IdvInheritedProofingVaForm'
+ end
+ end
+
+ describe '.field_names' do
+ let(:expected_field_names) do
+ [
+ :address_city,
+ :address_country,
+ :address_state,
+ :address_street,
+ :address_street2,
+ :address_zip,
+ :birth_date,
+ :first_name,
+ :last_name,
+ :phone,
+ :ssn,
+ ].sort
+ end
+
+ it 'returns the right model name' do
+ expect(described_class.field_names).to match_array expected_field_names
+ end
+ end
+ end
+
+ describe '#initialize' do
+ context 'when passing an invalid payload hash' do
+ context 'when not a Hash' do
+ let(:payload_hash) { :x }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error 'payload_hash is not a Hash'
+ end
+ end
+
+ context 'when nil?' do
+ let(:payload_hash) { nil }
+
+ it_behaves_like 'the hash is blank?'
+ end
+
+ context 'when empty?' do
+ let(:payload_hash) { {} }
+
+ it_behaves_like 'the hash is blank?'
+ end
+ end
+
+ context 'when passing a valid payload hash' do
+ it 'raises no errors' do
+ expect { subject }.to_not raise_error
+ end
+ end
+ end
+
+ describe '#validate' do
+ context 'with valid payload data' do
+ it 'returns true' do
+ expect(subject.validate).to eq true
+ end
+ end
+
+ context 'with invalid payload data' do
+ context 'when the payload has missing fields' do
+ let(:payload_hash) do
+ {
+ xfirst_name: 'Henry',
+ xlast_name: 'Ford',
+ xphone: '12222222222',
+ xbirth_date: '2000-01-01',
+ xssn: '111223333',
+ xaddress: {
+ xstreet: '1234 Model Street',
+ xstreet2: 'Suite A',
+ xcity: 'Detroit',
+ xstate: 'MI',
+ xcountry: 'United States',
+ xzip: '12345',
+ },
+ }
+ end
+
+ let(:expected_error_messages) do
+ [
+ # Required field presence
+ 'First name field is missing',
+ 'Last name field is missing',
+ 'Phone field is missing',
+ 'Birth date field is missing',
+ 'Ssn field is missing',
+ 'Address street field is missing',
+ 'Address street2 field is missing',
+ 'Address city field is missing',
+ 'Address state field is missing',
+ 'Address country field is missing',
+ 'Address zip field is missing',
+ ]
+ end
+
+ it 'returns false' do
+ expect(subject.validate).to eq false
+ end
+
+ it 'adds the correct error messages for missing fields' do
+ subject.validate
+ expect(
+ expected_error_messages.all? do |error_message|
+ subject.errors.full_messages.include? error_message
+ end,
+ ).to eq true
+ end
+ end
+
+ context 'when the payload has missing required field data' do
+ let(:payload_hash) do
+ {
+ first_name: nil,
+ last_name: '',
+ phone: nil,
+ birth_date: '',
+ ssn: nil,
+ address: {
+ street: '',
+ street2: nil,
+ city: '',
+ state: nil,
+ country: '',
+ zip: nil,
+ },
+ }
+ end
+
+ let(:expected_error_messages) do
+ [
+ # Required field data presence
+ 'First name Please fill in this field.',
+ 'Last name Please fill in this field.',
+ 'Birth date Please fill in this field.',
+ 'Ssn Please fill in this field.',
+ 'Address street Please fill in this field.',
+ 'Address zip Please fill in this field.',
+ ]
+ end
+
+ it 'returns false' do
+ expect(subject.validate).to eq false
+ end
+
+ it 'adds the correct error messages for required fields that are missing data' do
+ subject.validate
+ expect(subject.errors.full_messages).to match_array expected_error_messages
+ end
+ end
+ end
+ end
+
+ describe '#submit' do
+ it 'returns a FormResponse object' do
+ expect(subject.submit).to be_kind_of FormResponse
+ end
+
+ describe 'before returning' do
+ after do
+ subject.submit
+ end
+
+ it 'calls #validate' do
+ expect(subject).to receive(:validate).once
+ end
+ end
+
+ context 'with an invalid payload' do
+ context 'when the payload has missing fields' do
+ it 'returns a FormResponse indicating errors'
+ end
+
+ context 'when the payload has invalid field data' do
+ it 'returns a FormResponse indicating errors'
+ end
+ end
+ end
+end
diff --git a/spec/forms/totp_setup_form_spec.rb b/spec/forms/totp_setup_form_spec.rb
index ec01fd29ee4..a7d0bdc78fe 100644
--- a/spec/forms/totp_setup_form_spec.rb
+++ b/spec/forms/totp_setup_form_spec.rb
@@ -14,6 +14,7 @@
totp_secret_present: true,
multi_factor_auth_method: 'totp',
auth_app_configuration_id: next_auth_app_id,
+ enabled_mfa_methods_count: 1,
}
expect(form.submit.to_h).to eq(
diff --git a/spec/forms/two_factor_options_form_spec.rb b/spec/forms/two_factor_options_form_spec.rb
index 928a2edced2..5e345d10627 100644
--- a/spec/forms/two_factor_options_form_spec.rb
+++ b/spec/forms/two_factor_options_form_spec.rb
@@ -2,7 +2,15 @@
describe TwoFactorOptionsForm do
let(:user) { build(:user) }
- subject { described_class.new(user) }
+ let(:aal3_required) { false }
+ let(:piv_cac_required) { false }
+ subject {
+ described_class.new(
+ user: user,
+ aal3_required: aal3_required,
+ piv_cac_required: piv_cac_required,
+ )
+ }
describe '#submit' do
let(:submit_phone) { subject.submit(selection: 'phone') }
@@ -120,6 +128,37 @@
end
end
+ context 'when a user wants to is required to add piv_cac on sign in' do
+ let(:user) { build(:user, :with_authentication_app) }
+ let(:enabled_mfa_methods_count) { 1 }
+ let(:mfa_selection) { ['phone'] }
+ let(:aal3_required) { true }
+ let(:piv_cac_required) { false }
+
+ before do
+ allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return(true)
+ end
+
+ context 'when user is didnt select an mfa' do
+ let(:mfa_selection) { nil }
+
+ it 'does not submits the form' do
+ submission = subject.submit(selection: mfa_selection)
+ expect(submission.success?).to be_falsey
+ end
+ end
+
+ context 'when user selects an mfa' do
+ it 'submits the form' do
+ submission = subject.submit(selection: mfa_selection)
+ expect(submission.success?).to be_truthy
+ end
+ end
+ end
+
+ context 'when user doesnt select mfa selection with existing account' do
+ end
+
context 'when the feature flag toggle for 2FA phone restriction is off' do
before do
allow(IdentityConfig.store).to receive(:select_multiple_mfa_options).and_return(true)
diff --git a/spec/helpers/link_helper_spec.rb b/spec/helpers/link_helper_spec.rb
index 52fddb42a47..b19b8d8dccf 100644
--- a/spec/helpers/link_helper_spec.rb
+++ b/spec/helpers/link_helper_spec.rb
@@ -39,7 +39,8 @@
it 'renders a form' do
expect(subject).to have_selector("form[action='#{url}']")
expect(subject).to have_selector("input[name='_method'][value='#{method}']", visible: :all)
- expect(subject).to have_selector("input.#{css_class}[type='submit'][value='#{text}']")
+ expect(subject).to have_selector("button.#{css_class}[type='submit']")
+ expect(subject).to have_text(text)
end
end
end
diff --git a/spec/helpers/script_helper_spec.rb b/spec/helpers/script_helper_spec.rb
index 57173e5a241..1483415be93 100644
--- a/spec/helpers/script_helper_spec.rb
+++ b/spec/helpers/script_helper_spec.rb
@@ -43,9 +43,9 @@
'script[type="application/json"][data-asset-map]',
visible: :all,
text: {
- 'clock.svg' => '/clock.svg',
+ 'clock.svg' => 'http://test.host/clock.svg',
'identity-style-guide/dist/assets/img/sprite.svg' =>
- '/identity-style-guide/dist/assets/img/sprite.svg',
+ 'http://test.host/identity-style-guide/dist/assets/img/sprite.svg',
}.to_json,
)
end
diff --git a/spec/javascripts/packages/document-capture-polling/index-spec.js b/spec/javascripts/packages/document-capture-polling/index-spec.js
index 293e7b0f135..598480cf85b 100644
--- a/spec/javascripts/packages/document-capture-polling/index-spec.js
+++ b/spec/javascripts/packages/document-capture-polling/index-spec.js
@@ -49,12 +49,16 @@ describe('DocumentCapturePolling', () => {
});
it('polls', async () => {
- sandbox.stub(window, 'fetch').withArgs('/status').resolves({ status: 202 });
+ sandbox
+ .stub(window, 'fetch')
+ .withArgs('/status')
+ .resolves({ status: 202, json: () => Promise.resolve({}) });
sandbox.clock.tick(DOC_CAPTURE_POLL_INTERVAL);
expect(window.fetch).to.have.been.calledOnce();
- await flushPromises();
+ await flushPromises(); // Flush `fetch`
+ await flushPromises(); // Flush `json`
sandbox.clock.tick(DOC_CAPTURE_POLL_INTERVAL);
expect(window.fetch).to.have.been.calledTwice();
@@ -64,11 +68,15 @@ describe('DocumentCapturePolling', () => {
it('submits when done', async () => {
sandbox.stub(subject.elements.form, 'submit');
- sandbox.stub(window, 'fetch').withArgs('/status').resolves({ status: 200 });
+ sandbox
+ .stub(window, 'fetch')
+ .withArgs('/status')
+ .resolves({ status: 200, json: () => Promise.resolve({}) });
subject.bind();
sandbox.clock.tick(DOC_CAPTURE_POLL_INTERVAL);
- await flushPromises();
+ await flushPromises(); // Flush `fetch`
+ await flushPromises(); // Flush `json`
expect(subject.elements.form.submit).to.have.been.called();
expect(trackEvent).to.have.been.calledWith('IdV: Link sent capture doc polling started');
@@ -78,12 +86,31 @@ describe('DocumentCapturePolling', () => {
});
});
+ it('redirects if given redirect URL on success', async () => {
+ sandbox.stub(subject.elements.form, 'submit');
+ sandbox
+ .stub(window, 'fetch')
+ .withArgs('/status')
+ .resolves({ status: 200, json: () => Promise.resolve({ redirect: '#redirect' }) });
+
+ sandbox.clock.tick(DOC_CAPTURE_POLL_INTERVAL);
+ await flushPromises(); // Flush `fetch`
+ await flushPromises(); // Flush `json`
+
+ expect(window.location.hash).to.equal('#redirect');
+ expect(subject.elements.form.submit).not.to.have.been.called();
+ });
+
it('submits when cancelled', async () => {
sandbox.stub(subject.elements.form, 'submit');
- sandbox.stub(window, 'fetch').withArgs('/status').resolves({ status: 410 });
+ sandbox
+ .stub(window, 'fetch')
+ .withArgs('/status')
+ .resolves({ status: 410, json: () => Promise.resolve({}) });
sandbox.clock.tick(DOC_CAPTURE_POLL_INTERVAL);
- await flushPromises();
+ await flushPromises(); // Flush `fetch`
+ await flushPromises(); // Flush `json`
expect(trackEvent).to.have.been.calledWith('IdV: Link sent capture doc polling started');
expect(trackEvent).to.have.been.calledWith('IdV: Link sent capture doc polling complete', {
@@ -100,7 +127,8 @@ describe('DocumentCapturePolling', () => {
.resolves({ status: 429, json: () => Promise.resolve({ redirect: '#throttled' }) });
sandbox.clock.tick(DOC_CAPTURE_POLL_INTERVAL);
- await flushPromises();
+ await flushPromises(); // Flush `fetch`
+ await flushPromises(); // Flush `json`
expect(trackEvent).to.have.been.calledWith('IdV: Link sent capture doc polling started');
expect(trackEvent).to.have.been.calledWith('IdV: Link sent capture doc polling complete', {
@@ -111,12 +139,17 @@ describe('DocumentCapturePolling', () => {
});
it('polls until max, then showing form to submit', async () => {
- sandbox.stub(window, 'fetch').withArgs('/status').resolves({ status: 202 });
+ sandbox
+ .stub(window, 'fetch')
+ .withArgs('/status')
+ .resolves({ status: 202, json: () => Promise.resolve({}) });
for (let i = MAX_DOC_CAPTURE_POLL_ATTEMPTS; i; i--) {
sandbox.clock.tick(DOC_CAPTURE_POLL_INTERVAL);
// eslint-disable-next-line no-await-in-loop
- await flushPromises();
+ await flushPromises(); // Flush `fetch`
+ // eslint-disable-next-line no-await-in-loop
+ await flushPromises(); // Flush `json`
}
expect(screen.getByText('Submit').closest('.display-none')).to.not.be.ok();
@@ -143,10 +176,14 @@ describe('DocumentCapturePolling', () => {
it('does not prompt by navigating away via form submission', async () => {
sandbox.stub(subject.elements.form, 'submit');
- sandbox.stub(window, 'fetch').withArgs('/status').resolves({ status: 200 });
+ sandbox
+ .stub(window, 'fetch')
+ .withArgs('/status')
+ .resolves({ status: 200, json: () => Promise.resolve({}) });
subject.bind();
sandbox.clock.tick(DOC_CAPTURE_POLL_INTERVAL);
- await flushPromises();
+ await flushPromises(); // Flush `fetch`
+ await flushPromises(); // Flush `json`
window.dispatchEvent(event);
expect(event.defaultPrevented).to.be.false();
diff --git a/spec/javascripts/packages/document-capture/components/document-capture-troubleshooting-options-spec.jsx b/spec/javascripts/packages/document-capture/components/document-capture-troubleshooting-options-spec.jsx
index 508402580de..2e5c889ebf3 100644
--- a/spec/javascripts/packages/document-capture/components/document-capture-troubleshooting-options-spec.jsx
+++ b/spec/javascripts/packages/document-capture/components/document-capture-troubleshooting-options-spec.jsx
@@ -155,6 +155,22 @@ describe('DocumentCaptureTroubleshootingOptions', () => {
expect(ippButton).to.exist();
});
});
+
+ context('hasErrors and inPersonURL but showInPersonOption is false', () => {
+ const wrapper = ({ children }) => (
+ {children}
+ );
+
+ it('does not have link to IPP flow', () => {
+ const { queryAllByText, queryAllByRole } = render(
+ ,
+ { wrapper },
+ );
+
+ expect(queryAllByText('components.troubleshooting_options.new_feature').length).to.equal(0);
+ expect(queryAllByRole('button').length).to.equal(0);
+ });
+ });
});
context('with document tips hidden', () => {
diff --git a/spec/javascripts/packages/document-capture/services/upload-spec.js b/spec/javascripts/packages/document-capture/services/upload-spec.js
index 379b9b4a94a..38eb6ab24a5 100644
--- a/spec/javascripts/packages/document-capture/services/upload-spec.js
+++ b/spec/javascripts/packages/document-capture/services/upload-spec.js
@@ -169,6 +169,7 @@ describe('document-capture/services/upload', () => {
],
remaining_attempts: 3,
hints: true,
+ result_failed: true,
ocr_pii: { first_name: 'Fakey', last_name: 'McFakerson', dob: '1938-10-06' },
}),
}),
@@ -187,6 +188,7 @@ describe('document-capture/services/upload', () => {
last_name: 'McFakerson',
dob: '1938-10-06',
});
+ expect(error.isFailedResult).to.be.true();
expect(error.formEntryErrors[0]).to.be.instanceOf(UploadFormEntryError);
expect(error.formEntryErrors[0].field).to.equal('front');
expect(error.formEntryErrors[0].message).to.equal('Please fill in this field');
diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb
index 017f7b0824d..5500a41e826 100644
--- a/spec/jobs/get_usps_proofing_results_job_spec.rb
+++ b/spec/jobs/get_usps_proofing_results_job_spec.rb
@@ -17,16 +17,36 @@
let!(:passed_enrollment) { create(:in_person_enrollment, :passed) }
let!(:pending_enrollment) do
- create(:in_person_enrollment, status: :pending, enrollment_code: SecureRandom.hex(16))
+ create(
+ :in_person_enrollment,
+ status: :pending,
+ enrollment_code: SecureRandom.hex(16),
+ selected_location_details: { name: 'FRIENDSHIP' },
+ )
end
let!(:pending_enrollment_2) do
- create(:in_person_enrollment, status: :pending, enrollment_code: SecureRandom.hex(16))
+ create(
+ :in_person_enrollment,
+ status: :pending,
+ enrollment_code: SecureRandom.hex(16),
+ selected_location_details: { name: 'BALTIMORE' },
+ )
end
let!(:pending_enrollment_3) do
- create(:in_person_enrollment, status: :pending, enrollment_code: SecureRandom.hex(16))
+ create(
+ :in_person_enrollment,
+ status: :pending,
+ enrollment_code: SecureRandom.hex(16),
+ selected_location_details: { name: 'WASHINGTON' },
+ )
end
let!(:pending_enrollment_4) do
- create(:in_person_enrollment, status: :pending, enrollment_code: SecureRandom.hex(16))
+ create(
+ :in_person_enrollment,
+ status: :pending,
+ enrollment_code: SecureRandom.hex(16),
+ selected_location_details: { name: 'ARLINGTON' },
+ )
end
let(:pending_enrollments) do
[
@@ -93,7 +113,7 @@
)
end
- it 'logs details about failed requests' do
+ it 'logs details about a failed proofing' do
stub_request_token
stub_request_failed_proofing_results
@@ -110,11 +130,10 @@
'GetUspsProofingResultsJob: Enrollment failed proofing',
reason: 'Failed status',
enrollment_id: pending_enrollment.id,
+ enrollment_code: pending_enrollment.enrollment_code,
failure_reason: 'Clerk indicates that ID name or address does not match source data.',
fraud_suspected: false,
primary_id_type: 'Uniformed Services identification card',
- proofing_city: 'WILKES BARRE',
- proofing_post_office: 'WILKES BARRE',
proofing_state: 'PA',
secondary_id_type: 'Deed of Trust',
transaction_end_date_time: '12/17/2020 034055',
@@ -122,7 +141,29 @@
)
end
- it 'updates enrollment records and activates profiles on 2xx responses with valid JSON' do
+ it 'sends proofing failed email on response with failed status' do
+ stub_request_token
+ stub_request_failed_proofing_results
+
+ allow(InPersonEnrollment).to receive(:needs_usps_status_check).
+ and_return([pending_enrollment])
+
+ mailer = instance_double(ActionMailer::MessageDelivery, deliver_now_or_later: true)
+ user = pending_enrollment.user
+ user.email_addresses.each do |email_address|
+ expect(UserMailer).to receive(:in_person_failed).
+ with(
+ user,
+ email_address,
+ enrollment: instance_of(InPersonEnrollment),
+ ).
+ and_return(mailer)
+ end
+
+ job.perform(Time.zone.now)
+ end
+
+ it 'updates enrollment records and activates profiles on response with passed status' do
stub_request_token
stub_request_passed_proofing_results
@@ -139,7 +180,36 @@
expected_range.cover?(timestamp)
end
expect(enrollment.profile.active).to be(true)
+
+ expect(job_analytics).to have_logged_event(
+ 'GetUspsProofingResultsJob: Enrollment passed proofing',
+ reason: 'Successful status update',
+ enrollment_id: enrollment.id,
+ enrollment_code: enrollment.enrollment_code,
+ )
+ end
+ end
+
+ it 'sends verifed email on 2xx responses with valid JSON' do
+ stub_request_token
+ stub_request_passed_proofing_results
+
+ allow(InPersonEnrollment).to receive(:needs_usps_status_check).
+ and_return([pending_enrollment])
+
+ mailer = instance_double(ActionMailer::MessageDelivery, deliver_now_or_later: true)
+ user = pending_enrollment.user
+ user.email_addresses.each do |email_address|
+ expect(UserMailer).to receive(:in_person_verified).
+ with(
+ user,
+ email_address,
+ enrollment: instance_of(InPersonEnrollment),
+ ).
+ and_return(mailer)
end
+
+ job.perform(Time.zone.now)
end
it 'receives a non-hash value' do
@@ -152,6 +222,7 @@
'GetUspsProofingResultsJob: Exception raised',
reason: 'Bad response structure',
enrollment_id: pending_enrollment.id,
+ enrollment_code: pending_enrollment.enrollment_code,
)
end
@@ -172,6 +243,7 @@
'GetUspsProofingResultsJob: Enrollment failed proofing',
reason: 'Unsupported status',
enrollment_id: pending_enrollment.id,
+ enrollment_code: pending_enrollment.enrollment_code,
status: 'Not supported',
)
end
@@ -193,6 +265,7 @@
'GetUspsProofingResultsJob: Exception raised',
reason: 'Request exception',
enrollment_id: pending_enrollment.id,
+ enrollment_code: pending_enrollment.enrollment_code,
)
end
@@ -213,6 +286,7 @@
'GetUspsProofingResultsJob: Exception raised',
reason: 'Request exception',
enrollment_id: pending_enrollment.id,
+ enrollment_code: pending_enrollment.enrollment_code,
)
end
@@ -257,6 +331,7 @@
'GetUspsProofingResultsJob: Exception raised',
reason: 'Request exception',
enrollment_id: pending_enrollment.id,
+ enrollment_code: pending_enrollment.enrollment_code,
)
end
@@ -277,6 +352,7 @@
'GetUspsProofingResultsJob: Enrollment failed proofing',
reason: 'Unsupported ID type',
enrollment_id: pending_enrollment.id,
+ enrollment_code: pending_enrollment.enrollment_code,
primary_id_type: 'Not supported',
)
end
diff --git a/spec/jobs/reports/sp_user_counts_report_spec.rb b/spec/jobs/reports/sp_user_counts_report_spec.rb
index 55721544347..4d818298ad1 100644
--- a/spec/jobs/reports/sp_user_counts_report_spec.rb
+++ b/spec/jobs/reports/sp_user_counts_report_spec.rb
@@ -18,28 +18,36 @@
freeze_time do
timestamp = Time.zone.now.iso8601
expect(subject).to receive(:write_hash_to_reports_log).with(
- app_id: app_id,
- ial1_user_total: 3,
- ial2_user_total: 0,
- issuer: issuer,
- name: 'Report SP User Counts',
- time: timestamp,
- user_total: 3,
+ {
+ app_id: app_id,
+ ial1_user_total: 3,
+ ial2_user_total: 0,
+ issuer: issuer,
+ name: 'Report SP User Counts',
+ time: timestamp,
+ user_total: 3,
+ },
)
expect(subject).to receive(:write_hash_to_reports_log).with(
- name: 'Report Registered Users Count',
- time: timestamp,
- count: 0,
+ {
+ name: 'Report Registered Users Count',
+ time: timestamp,
+ count: 0,
+ },
)
expect(subject).to receive(:write_hash_to_reports_log).with(
- name: 'Report IAL1 Users Linked to SPs Count',
- time: timestamp,
- count: 2,
+ {
+ name: 'Report IAL1 Users Linked to SPs Count',
+ time: timestamp,
+ count: 2,
+ },
)
expect(subject).to receive(:write_hash_to_reports_log).with(
- name: 'Report IAL2 Users Linked to SPs Count',
- time: timestamp,
- count: 1,
+ {
+ name: 'Report IAL2 Users Linked to SPs Count',
+ time: timestamp,
+ count: 1,
+ },
)
subject.perform(Time.zone.today)
end
diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb
index 461a28822b7..d4535666b41 100644
--- a/spec/mailers/previews/user_mailer_preview.rb
+++ b/spec/mailers/previews/user_mailer_preview.rb
@@ -134,6 +134,31 @@ def account_verified
)
end
+ def in_person_ready_to_verify
+ UserMailer.in_person_ready_to_verify(
+ user,
+ email_address_record,
+ first_name: 'Michael',
+ enrollment: in_person_enrollment,
+ )
+ end
+
+ def in_person_verified
+ UserMailer.in_person_verified(
+ user,
+ email_address_record,
+ enrollment: in_person_enrollment,
+ )
+ end
+
+ def in_person_failed
+ UserMailer.in_person_failed(
+ user,
+ email_address_record,
+ enrollment: in_person_enrollment,
+ )
+ end
+
private
def user
@@ -148,6 +173,28 @@ def email_address_record
unsaveable(EmailAddress.new(email: email_address))
end
+ def in_person_enrollment
+ unsaveable(
+ InPersonEnrollment.new(
+ user: user,
+ profile: unsaveable(Profile.new(user: user)),
+ enrollment_code: '2048702198804358',
+ created_at: Time.zone.now - 2.hours,
+ status_updated_at: Time.zone.now - 1.hour,
+ current_address_matches_id: true,
+ selected_location_details: {
+ 'name' => 'BALTIMORE',
+ 'street_address' => '900 E FAYETTE ST RM 118',
+ 'formatted_city_state_zip' => 'BALTIMORE, MD 21233-9715',
+ 'phone' => '555-123-6409',
+ 'weekday_hours' => '8:30 AM - 4:30 PM',
+ 'saturday_hours' => '9:00 AM - 12:00 PM',
+ 'sunday_hours' => 'Closed',
+ },
+ ),
+ )
+ end
+
# Remove #save and #save! to make sure we can't write these made-up records
def unsaveable(record)
class << record
diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb
index 878879d46c9..feb9d2188fe 100644
--- a/spec/mailers/user_mailer_spec.rb
+++ b/spec/mailers/user_mailer_spec.rb
@@ -520,6 +520,72 @@ def expect_email_body_to_have_help_and_contact_links
end
end
+ describe '#in_person_ready_to_verify' do
+ let(:first_name) { 'Michael' }
+ let!(:enrollment) {
+ create(
+ :in_person_enrollment,
+ :pending,
+ selected_location_details: { name: 'FRIENDSHIP' },
+ status_updated_at: Time.zone.now - 2.hours,
+ )
+ }
+
+ let(:mail) do
+ UserMailer.in_person_ready_to_verify(
+ user,
+ user.email_addresses.first,
+ first_name: first_name,
+ enrollment: enrollment,
+ )
+ end
+
+ it_behaves_like 'a system email'
+ it_behaves_like 'an email that respects user email locale preference'
+ end
+
+ describe '#in_person_verified' do
+ let(:enrollment) {
+ create(
+ :in_person_enrollment,
+ selected_location_details: { name: 'FRIENDSHIP' },
+ status_updated_at: Time.zone.now - 2.hours,
+ )
+ }
+
+ let(:mail) do
+ UserMailer.in_person_verified(
+ user,
+ user.email_addresses.first,
+ enrollment: enrollment,
+ )
+ end
+
+ it_behaves_like 'a system email'
+ it_behaves_like 'an email that respects user email locale preference'
+ end
+
+ describe '#in_person_failed' do
+ let(:enrollment) {
+ create(
+ :in_person_enrollment,
+ selected_location_details: { name: 'FRIENDSHIP' },
+ status_updated_at: Time.zone.now - 2.hours,
+ )
+ }
+
+ let(:mail) do
+ UserMailer.in_person_failed(
+ user,
+ user.email_addresses.first,
+ enrollment: enrollment,
+ )
+ end
+
+ it_behaves_like 'a system email'
+ it_behaves_like 'an email that respects user email locale preference'
+ end
+
def strip_tags(str)
ActionController::Base.helpers.strip_tags(str)
end
diff --git a/spec/models/backup_code_configuration_spec.rb b/spec/models/backup_code_configuration_spec.rb
index d6a14ad1773..9a346dd7278 100644
--- a/spec/models/backup_code_configuration_spec.rb
+++ b/spec/models/backup_code_configuration_spec.rb
@@ -25,9 +25,10 @@
user = User.new
user.save
BackupCodeGenerator.new(user).create
- backup_code_config = BackupCodeConfiguration.new(user_id: user.id)
- expect(backup_code_config.mfa_enabled?).to be_truthy
+ user.backup_code_configurations.each do |backup_code_config|
+ expect(backup_code_config.mfa_enabled?).to be_truthy
+ end
end
end
diff --git a/spec/models/in_person_enrollment_spec.rb b/spec/models/in_person_enrollment_spec.rb
index 25e78b231c7..df0298f09f9 100644
--- a/spec/models/in_person_enrollment_spec.rb
+++ b/spec/models/in_person_enrollment_spec.rb
@@ -9,7 +9,7 @@
describe 'Status' do
it 'defines enum correctly' do
should define_enum_for(:status).
- with_values([:establishing, :pending, :passed, :failed, :expired, :canceled])
+ with_values([:establishing, :pending, :passed, :failed, :expired, :cancelled])
end
end
@@ -17,7 +17,7 @@
it 'requires the profile to be associated with the user' do
user1 = create(:user)
user2 = create(:user)
- profile2 = create(:profile, :verification_pending, user: user2)
+ profile2 = create(:profile, :gpo_verification_pending, user: user2)
expect { create(:in_person_enrollment, user: user1, profile: profile2) }.
to raise_error ActiveRecord::RecordInvalid
expect(InPersonEnrollment.count).to eq 0
@@ -25,8 +25,8 @@
it 'does not allow more than one pending enrollment per user' do
user = create(:user)
- profile = create(:profile, :verification_pending, user: user)
- profile2 = create(:profile, :verification_pending, user: user)
+ profile = create(:profile, :gpo_verification_pending, user: user)
+ profile2 = create(:profile, :gpo_verification_pending, user: user)
create(:in_person_enrollment, user: user, profile: profile, status: :pending)
expect(InPersonEnrollment.pending.count).to eq 1
expect { create(:in_person_enrollment, user: user, profile: profile2, status: :pending) }.
@@ -36,7 +36,7 @@
it 'does not allow duplicate unique ids' do
user = create(:user)
- profile = create(:profile, :verification_pending, user: user)
+ profile = create(:profile, :gpo_verification_pending, user: user)
unique_id = InPersonEnrollment.generate_unique_id
create(:in_person_enrollment, user: user, profile: profile, unique_id: unique_id)
expect { create(:in_person_enrollment, user: user, profile: profile, unique_id: unique_id) }.
@@ -49,13 +49,13 @@
expect {
InPersonEnrollment.statuses.each do |key,|
status = InPersonEnrollment.statuses[key]
- profile = create(:profile, :verification_pending, user: user)
+ profile = create(:profile, :gpo_verification_pending, user: user)
create(:in_person_enrollment, user: user, profile: profile, status: status)
end
InPersonEnrollment.statuses.each do |key,|
status = InPersonEnrollment.statuses[key]
if status != InPersonEnrollment.statuses[:pending]
- profile = create(:profile, :verification_pending, user: user)
+ profile = create(:profile, :gpo_verification_pending, user: user)
create(:in_person_enrollment, user: user, profile: profile, status: status)
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index aa0dc753537..98c37c38449 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -197,7 +197,7 @@
create(:profile, :verification_cancelled, user: user, pii: { first_name: 'Jane' })
}
let(:profile2) {
- create(:profile, :verification_pending, user: user, pii: { first_name: 'Susan' })
+ create(:profile, :gpo_verification_pending, user: user, pii: { first_name: 'Susan' })
}
let!(:enrollment1) { create(:in_person_enrollment, :failed, user: user, profile: profile1) }
@@ -425,18 +425,18 @@
end
describe '#pending_profile' do
- context 'when a profile with a verification_pending deactivation_reason exists' do
+ context 'when a profile with a gpo_verification_pending deactivation_reason exists' do
it 'returns the most recent profile' do
user = User.new
_old_profile = create(
:profile,
- :verification_pending,
+ :gpo_verification_pending,
created_at: 1.day.ago,
user: user,
)
new_profile = create(
:profile,
- :verification_pending,
+ :gpo_verification_pending,
user: user,
)
@@ -444,7 +444,7 @@
end
end
- context 'when a verification_pending profile does not exist' do
+ context 'when a gpo_verification_pending profile does not exist' do
it 'returns nil' do
user = User.new
create(
diff --git a/spec/presenters/idv/gpo_presenter_spec.rb b/spec/presenters/idv/gpo_presenter_spec.rb
index e9015aa25e4..fcafc1ea2d4 100644
--- a/spec/presenters/idv/gpo_presenter_spec.rb
+++ b/spec/presenters/idv/gpo_presenter_spec.rb
@@ -49,7 +49,7 @@
describe '#fallback_back_path' do
context 'when the user has a pending profile' do
it 'returns the verify account path' do
- create(:profile, user: user, deactivation_reason: :verification_pending)
+ create(:profile, user: user, deactivation_reason: :gpo_verification_pending)
expect(subject.fallback_back_path).to eq('/account/verify')
end
end
diff --git a/spec/presenters/idv/in_person/ready_to_verify_presenter_spec.rb b/spec/presenters/idv/in_person/ready_to_verify_presenter_spec.rb
index 1155d09a966..5b604ac051a 100644
--- a/spec/presenters/idv/in_person/ready_to_verify_presenter_spec.rb
+++ b/spec/presenters/idv/in_person/ready_to_verify_presenter_spec.rb
@@ -6,6 +6,9 @@
let(:enrollment_code) { '2048702198804358' }
let(:current_address_matches_id) { true }
let(:created_at) { described_class::USPS_SERVER_TIMEZONE.parse('2022-07-14T00:00:00Z') }
+ let(:enrollment_selected_location_details) do
+ JSON.parse(UspsIppFixtures.enrollment_selected_location_details)
+ end
let(:enrollment) do
InPersonEnrollment.new(
user: user,
@@ -14,21 +17,12 @@
unique_id: InPersonEnrollment.generate_unique_id,
created_at: created_at,
current_address_matches_id: current_address_matches_id,
- selected_location_details:
- JSON.parse(UspsIppFixtures.request_facilities_response)['postOffices'].first,
+ selected_location_details: enrollment_selected_location_details,
)
end
subject(:presenter) { described_class.new(enrollment: enrollment) }
- describe '#barcode_data_url' do
- subject(:barcode_data_url) { presenter.barcode_data_url }
-
- it 'returns a valid data URL' do
- expect(barcode_data_url).to match URI::DEFAULT_PARSER.make_regexp('data')
- end
- end
-
describe '#formatted_due_date' do
subject(:formatted_due_date) { presenter.formatted_due_date }
@@ -41,35 +35,28 @@
end
end
- describe '#formatted_enrollment_code' do
- subject(:formatted_enrollment_code) { presenter.formatted_enrollment_code }
-
- it 'returns a formatted enrollment code' do
- expect(formatted_enrollment_code).to eq(
- Idv::InPerson::EnrollmentCodeFormatter.format(enrollment_code),
- )
- end
- end
-
describe '#selected_location_details' do
subject(:selected_location_details) { presenter.selected_location_details }
it 'returns a hash of location details associated with the enrollment' do
expect(selected_location_details).to include(
+ 'formatted_city_state_zip' => kind_of(String),
'name' => kind_of(String),
- 'streetAddress' => kind_of(String),
- 'city' => kind_of(String),
- 'state' => kind_of(String),
- 'zip5' => kind_of(String),
- 'zip4' => kind_of(String),
'phone' => kind_of(String),
- 'hours' => array_including(
- hash_including('weekdayHours' => kind_of(String)),
- hash_including('saturdayHours' => kind_of(String)),
- hash_including('sundayHours' => kind_of(String)),
- ),
+ 'saturday_hours' => kind_of(String),
+ 'street_address' => kind_of(String),
+ 'sunday_hours' => kind_of(String),
+ 'weekday_hours' => kind_of(String),
)
end
+
+ context 'with blank selected_location_details' do
+ let(:enrollment_selected_location_details) { nil }
+
+ it 'returns nil' do
+ expect(selected_location_details).to be_nil
+ end
+ end
end
describe '#selected_location_hours' do
@@ -78,11 +65,11 @@
before do
allow(presenter).to receive(:selected_location_details).and_return(
- 'hours' => [
- { 'weekdayHours' => hours_open },
- { 'saturdayHours' => hours_open },
- { 'sundayHours' => hours_closed },
- ],
+ {
+ 'weekday_hours' => hours_open,
+ 'saturday_hours' => hours_open,
+ 'sunday_hours' => hours_closed,
+ },
)
end
diff --git a/spec/presenters/idv/in_person/verification_results_email_presenter_spec.rb b/spec/presenters/idv/in_person/verification_results_email_presenter_spec.rb
new file mode 100644
index 00000000000..8d05000ea47
--- /dev/null
+++ b/spec/presenters/idv/in_person/verification_results_email_presenter_spec.rb
@@ -0,0 +1,32 @@
+require 'rails_helper'
+
+RSpec.describe Idv::InPerson::VerificationResultsEmailPresenter do
+ let(:location_name) { 'FRIENDSHIP' }
+ let(:status_updated_at) { described_class::USPS_SERVER_TIMEZONE.parse('2022-07-14T00:00:00Z') }
+ let!(:enrollment) do
+ create(
+ :in_person_enrollment,
+ :pending,
+ selected_location_details: { name: location_name },
+ )
+ end
+
+ subject(:presenter) { described_class.new(enrollment: enrollment) }
+
+ describe '#location_name' do
+ it 'returns the enrollment location name' do
+ expect(presenter.location_name).to eq(location_name)
+ end
+ end
+
+ describe '#formatted_verified_date' do
+ around do |example|
+ Time.use_zone('UTC') { example.run }
+ end
+
+ it 'returns a formatted verified date' do
+ enrollment.update(status_updated_at: status_updated_at)
+ expect(presenter.formatted_verified_date).to eq 'July 13, 2022'
+ end
+ end
+end
diff --git a/spec/presenters/image_upload_response_presenter_spec.rb b/spec/presenters/image_upload_response_presenter_spec.rb
index 439e0f990f5..760f052d253 100644
--- a/spec/presenters/image_upload_response_presenter_spec.rb
+++ b/spec/presenters/image_upload_response_presenter_spec.rb
@@ -115,6 +115,7 @@
it 'returns hash of properties' do
expected = {
success: false,
+ result_failed: false,
errors: [{ field: :limit, message: t('errors.doc_auth.throttled_heading') }],
redirect: idv_session_errors_throttled_url,
remaining_attempts: 0,
@@ -140,6 +141,7 @@
it 'returns hash of properties' do
expected = {
success: false,
+ result_failed: false,
errors: [{ field: :front, message: t('doc_auth.errors.not_a_file') }],
hints: true,
remaining_attempts: 3,
@@ -149,6 +151,32 @@
expect(presenter.as_json).to eq expected
end
+ context 'hard fail' do
+ let(:form_response) do
+ FormResponse.new(
+ success: false,
+ errors: {
+ front: t('doc_auth.errors.not_a_file'),
+ hints: true,
+ },
+ extra: { doc_auth_result: 'Failed', remaining_attempts: 3 },
+ )
+ end
+
+ it 'returns hash of properties' do
+ expected = {
+ success: false,
+ result_failed: true,
+ errors: [{ field: :front, message: t('doc_auth.errors.not_a_file') }],
+ hints: true,
+ remaining_attempts: 3,
+ ocr_pii: nil,
+ }
+
+ expect(presenter.as_json).to eq expected
+ end
+ end
+
context 'no remaining attempts' do
let(:form_response) do
FormResponse.new(
@@ -164,6 +192,7 @@
it 'returns hash of properties' do
expected = {
success: false,
+ result_failed: false,
errors: [{ field: :front, message: t('doc_auth.errors.not_a_file') }],
hints: true,
redirect: idv_session_errors_throttled_url,
@@ -190,6 +219,7 @@
it 'returns hash of properties' do
expected = {
success: false,
+ result_failed: false,
errors: [],
hints: true,
remaining_attempts: 3,
diff --git a/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb b/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb
index 25afbc384b9..8e96f5b4510 100644
--- a/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb
+++ b/spec/presenters/two_factor_auth_code/phone_delivery_presenter_spec.rb
@@ -50,7 +50,7 @@
it 'specifies when the code will expire' do
text = t(
'instructions.mfa.sms.number_message_html',
- number: "#{data[:phone_number]} ",
+ number: ActionController::Base.helpers.content_tag(:strong, data[:phone_number]),
expiration: TwoFactorAuthenticatable::DIRECT_OTP_VALID_FOR_MINUTES,
)
expect(presenter.phone_number_message).to eq text
diff --git a/spec/services/doc_auth/acuant/responses/get_results_response_spec.rb b/spec/services/doc_auth/acuant/responses/get_results_response_spec.rb
index 275e187537f..092feac1edd 100644
--- a/spec/services/doc_auth/acuant/responses/get_results_response_spec.rb
+++ b/spec/services/doc_auth/acuant/responses/get_results_response_spec.rb
@@ -162,6 +162,40 @@
expect(response.result_code.billed?).to eq(false)
end
+ context 'when visiual_pattern passes and fails' do
+ let(:http_response) do
+ [1, 2].each do |index|
+ parsed_response_body['Alerts'] << {
+ Key: 'Visible Pattern',
+ Name: 'Visible Pattern',
+ RegionReferences: [],
+ Result: index,
+ }
+ end
+
+ instance_double(
+ Faraday::Response,
+ body: parsed_response_body.to_json,
+ )
+ end
+
+ it 'returns log_alert_results for visible_pattern with multiple results comma separated' do
+ expect(response.to_h[:processed_alerts]).to eq(
+ passed: [{ name: 'Visible Pattern', result: 'Passed' }],
+ failed:
+ [{ name: 'Document Classification',
+ result: 'Failed' },
+ { name: 'Visible Pattern',
+ result: 'Failed' }],
+ )
+
+ expect(response.to_h[:log_alert_results]).to eq(
+ { visible_pattern: { no_side: 'Failed' },
+ document_classification: { no_side: 'Failed' } },
+ )
+ end
+ end
+
context 'when with an acuant error message' do
let(:http_response) do
parsed_response_body['Alerts'].first['Disposition'] = 'This message does not have a key'
diff --git a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb
index f5bc8c60b9e..ca903dec728 100644
--- a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb
+++ b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb
@@ -102,7 +102,7 @@
transaction_reason_code: 'trueid_pass',
product_status: 'pass',
doc_auth_result: 'Passed',
- processed_alerts: a_hash_including(:passed, :failed),
+ processed_alerts: a_hash_including(:failed),
alert_failure_count: a_kind_of(Numeric),
portrait_match_results: nil,
image_metrics: a_hash_including(:front, :back),
@@ -185,6 +185,12 @@
expect(errors[:hints]).to eq(true)
end
+ it 'returns Failed for visible_pattern when it gets passed and failed value ' do
+ output = described_class.new(failure_response_no_liveness, false, config).to_h
+ expect(output.to_h[:log_alert_results]).
+ to match(a_hash_including(visible_pattern: { no_side: 'Failed' }))
+ end
+
it 'produces appropriate errors with liveness' do
output = described_class.new(failure_response_with_liveness, true, config).to_h
errors = output[:errors]
@@ -367,6 +373,34 @@
end
end
+ describe '#parse_date' do
+ let(:response) { described_class.new(success_response, false, config) }
+
+ it 'handles an invalid month' do
+ allow(Rails.logger).to receive(:info)
+ expect(Rails.logger).to receive(:info).with(
+ { event: 'Failure to parse TrueID date' }.to_json,
+ ).once
+ expect(response.send(:parse_date, year: 2022, month: 13, day: 1)).to eq(nil)
+ end
+
+ it 'handles an invalid leap day' do
+ allow(Rails.logger).to receive(:info)
+ expect(Rails.logger).to receive(:info).with(
+ { event: 'Failure to parse TrueID date' }.to_json,
+ ).once
+ expect(response.send(:parse_date, year: 2022, month: 2, day: 29)).to eq(nil)
+ end
+
+ it 'handles a day past the end of the month' do
+ allow(Rails.logger).to receive(:info)
+ expect(Rails.logger).to receive(:info).with(
+ { event: 'Failure to parse TrueID date' }.to_json,
+ ).once
+ expect(response.send(:parse_date, year: 2022, month: 4, day: 31)).to eq(nil)
+ end
+ end
+
describe '#attention_with_barcode?' do
let(:response) { described_class.new(success_response, false, config) }
subject(:attention_with_barcode) { response.attention_with_barcode? }
diff --git a/spec/services/doc_auth/mock/result_response_spec.rb b/spec/services/doc_auth/mock/result_response_spec.rb
index 81138cbd64b..721ab88dbe9 100644
--- a/spec/services/doc_auth/mock/result_response_spec.rb
+++ b/spec/services/doc_auth/mock/result_response_spec.rb
@@ -274,6 +274,34 @@
expect(response.exception).to eq(nil)
expect(response.pii_from_doc).to eq({})
expect(response.attention_with_barcode?).to eq(false)
+ expect(response.extra).to eq(
+ doc_auth_result: DocAuth::Acuant::ResultCodes::PASSED.name,
+ billed: true,
+ )
+ end
+ end
+
+ context 'with a yaml file containing a failed result' do
+ let(:input) do
+ <<~YAML
+ doc_auth_result: Failed
+ YAML
+ end
+
+ it 'returns a failed result' do
+ expect(response.success?).to eq(false)
+ expect(response.errors).to eq(
+ general: [DocAuth::Errors::BARCODE_READ_CHECK],
+ back: [DocAuth::Errors::FALLBACK_FIELD_LEVEL],
+ hints: true,
+ )
+ expect(response.exception).to eq(nil)
+ expect(response.pii_from_doc).to eq({})
+ expect(response.attention_with_barcode?).to eq(false)
+ expect(response.extra).to eq(
+ doc_auth_result: DocAuth::Acuant::ResultCodes::FAILED.name,
+ billed: true,
+ )
end
end
@@ -315,6 +343,10 @@
state_id_type: 'drivers_license',
)
expect(response.attention_with_barcode?).to eq(false)
+ expect(response.extra).to eq(
+ doc_auth_result: DocAuth::Acuant::ResultCodes::PASSED.name,
+ billed: true,
+ )
end
end
end
diff --git a/spec/services/doc_auth/processed_alert_to_log_alert_formatter_spec.rb b/spec/services/doc_auth/processed_alert_to_log_alert_formatter_spec.rb
new file mode 100644
index 00000000000..ce00c1fe573
--- /dev/null
+++ b/spec/services/doc_auth/processed_alert_to_log_alert_formatter_spec.rb
@@ -0,0 +1,37 @@
+# Take the proccessed alerts and reformat them in a hash so that its easier to search and collect
+# stats through cloudwatch.
+
+require 'rails_helper'
+
+RSpec.describe DocAuth::ProcessedAlertToLogAlertFormatter do
+ let(:alerts) do
+ { passed: [{ alert: 'Alert_1', name: 'Visible Pattern', result: 'Passed' }],
+ failed:
+ [
+ { alert: 'Alert_1', name: '2D Barcode Read', result: 'Failed' },
+ { alert: 'Alert_2', name: 'Layout Valid', result: 'Attention' },
+ { alert: 'Alert_3', name: '2D Barcode Read', result: 'Failed' },
+ { alert: 'Alert_4', name: 'Visible Pattern', result: 'Failed' },
+ { alert: 'Alert_5', name: 'Visible Photo Characteristics', result: 'Failed' },
+ ] }
+ end
+
+ context('when ProcessedAlertToLogAlertFormatter is called') do
+ subject {
+ DocAuth::ProcessedAlertToLogAlertFormatter.new.log_alerts(alerts)
+ }
+
+ it('returns failed if both passed and failed are returned by the alert') do
+ expect(subject).to match(a_hash_including(visible_pattern: { no_side: 'Failed' }))
+ end
+
+ it('returns the formatted log hash') do
+ expect(subject).to eq(
+ { '2d_barcode_read': { no_side: 'Failed' },
+ layout_valid: { no_side: 'Attention' },
+ visible_pattern: { no_side: 'Failed' },
+ visible_photo_characteristics: { no_side: 'Failed' } },
+ )
+ end
+ end
+end
diff --git a/spec/services/encryption/encryptors/pii_encryptor_spec.rb b/spec/services/encryption/encryptors/pii_encryptor_spec.rb
index fba039cb9d8..1c68f366901 100644
--- a/spec/services/encryption/encryptors/pii_encryptor_spec.rb
+++ b/spec/services/encryption/encryptors/pii_encryptor_spec.rb
@@ -44,7 +44,7 @@
and_return('aes_ciphertext')
expect(subject.send(:kms_client)).to receive(:encrypt).
- with('aes_ciphertext', 'context' => 'pii-encryption', 'user_uuid' => 'uuid-123-abc').
+ with('aes_ciphertext', { 'context' => 'pii-encryption', 'user_uuid' => 'uuid-123-abc' }).
and_return('kms_ciphertext')
expected_ciphertext = Base64.strict_encode64('kms_ciphertext')
diff --git a/spec/services/encryption/password_verifier_spec.rb b/spec/services/encryption/password_verifier_spec.rb
index a5f25e230d9..36a1f2d34d2 100644
--- a/spec/services/encryption/password_verifier_spec.rb
+++ b/spec/services/encryption/password_verifier_spec.rb
@@ -43,7 +43,7 @@
kms_client = Encryption::KmsClient.new
expect(kms_client).to receive(:encrypt).with(
encoded_scrypt_password,
- 'user_uuid' => user_uuid, 'context' => 'password-digest',
+ { 'user_uuid' => user_uuid, 'context' => 'password-digest' },
).and_return('kms_ciphertext')
expect(Encryption::KmsClient).to receive(:new).and_return(kms_client)
diff --git a/spec/services/funnel/registration/add_mfa_spec.rb b/spec/services/funnel/registration/add_mfa_spec.rb
index a31f79a13d7..375a3a596c0 100644
--- a/spec/services/funnel/registration/add_mfa_spec.rb
+++ b/spec/services/funnel/registration/add_mfa_spec.rb
@@ -1,6 +1,7 @@
require 'rails_helper'
describe Funnel::Registration::AddMfa do
+ let(:analytics) { FakeAnalytics.new }
subject { described_class }
let(:user_id) do
@@ -12,15 +13,15 @@
let(:funnel) { RegistrationLog.all.first }
it 'adds an 1st mfa' do
- subject.call(user_id, 'phone')
+ subject.call(user_id, 'phone', analytics)
expect(funnel.first_mfa).to eq('phone')
expect(funnel.first_mfa_at).to be_present
end
it 'adds a 2nd mfa' do
- subject.call(user_id, 'phone')
- subject.call(user_id, 'backup_codes')
+ subject.call(user_id, 'phone', analytics)
+ subject.call(user_id, 'backup_codes', analytics)
expect(funnel.first_mfa).to eq('phone')
expect(funnel.first_mfa_at).to be_present
@@ -28,9 +29,9 @@
end
it 'does not add a 3rd mfa' do
- subject.call(user_id, 'phone')
- subject.call(user_id, 'backup_codes')
- subject.call(user_id, 'auth_app')
+ subject.call(user_id, 'phone', analytics)
+ subject.call(user_id, 'backup_codes', analytics)
+ subject.call(user_id, 'auth_app', analytics)
expect(funnel.first_mfa).to eq('phone')
expect(funnel.second_mfa).to eq('backup_codes')
diff --git a/spec/services/funnel/registration/range_registered_count_spec.rb b/spec/services/funnel/registration/range_registered_count_spec.rb
index eaf520bf821..259f473a8ed 100644
--- a/spec/services/funnel/registration/range_registered_count_spec.rb
+++ b/spec/services/funnel/registration/range_registered_count_spec.rb
@@ -1,6 +1,7 @@
require 'rails_helper'
describe Funnel::Registration::RangeRegisteredCount do
+ let(:analytics) { FakeAnalytics.new }
subject { described_class }
let(:start) { '2019-01-01 00:00:00' }
@@ -53,7 +54,7 @@ def register_user(year, month, day)
user_id = user.id
Funnel::Registration::Create.call(user_id)
Funnel::Registration::AddPassword.call(user_id)
- Funnel::Registration::AddMfa.call(user_id, 'backup_codes')
+ Funnel::Registration::AddMfa.call(user_id, 'backup_codes', analytics)
end
end
end
diff --git a/spec/services/funnel/registration/total_registered_count_spec.rb b/spec/services/funnel/registration/total_registered_count_spec.rb
index 0af532d6791..03641f68a88 100644
--- a/spec/services/funnel/registration/total_registered_count_spec.rb
+++ b/spec/services/funnel/registration/total_registered_count_spec.rb
@@ -1,6 +1,7 @@
require 'rails_helper'
describe Funnel::Registration::TotalRegisteredCount do
+ let(:analytics) { FakeAnalytics.new }
subject { described_class }
it 'returns 0' do
@@ -18,7 +19,7 @@
expect(Funnel::Registration::TotalRegisteredCount.call).to eq(0)
- Funnel::Registration::AddMfa.call(user_id, 'phone')
+ Funnel::Registration::AddMfa.call(user_id, 'phone', analytics)
expect(Funnel::Registration::TotalRegisteredCount.call).to eq(1)
end
@@ -41,6 +42,6 @@ def register_user
user_id = user.id
Funnel::Registration::Create.call(user_id)
Funnel::Registration::AddPassword.call(user_id)
- Funnel::Registration::AddMfa.call(user_id, 'backup_codes')
+ Funnel::Registration::AddMfa.call(user_id, 'backup_codes', analytics)
end
end
diff --git a/spec/services/idv/agent_spec.rb b/spec/services/idv/agent_spec.rb
index 0debecd563f..2b941f7767e 100644
--- a/spec/services/idv/agent_spec.rb
+++ b/spec/services/idv/agent_spec.rb
@@ -4,6 +4,8 @@
describe Idv::Agent do
include IdvHelper
+ let(:user) { build(:user) }
+
let(:bad_phone) do
Proofing::Mock::AddressMockClient::UNVERIFIABLE_PHONE_NUMBER
end
@@ -20,8 +22,7 @@
context 'proofing state_id enabled' do
it 'does not proof state_id if resolution fails' do
agent = Idv::Agent.new(
- { ssn: '444-55-6666', first_name: Faker::Name.first_name,
- zipcode: '11111' },
+ Idp::Constants::MOCK_IDV_APPLICANT.merge(uuid: user.uuid, ssn: '444-55-6666'),
)
agent.proof_resolution(
document_capture_session, should_proof_state_id: true, trace_id: trace_id
@@ -33,14 +34,7 @@
end
it 'does proof state_id if resolution succeeds' do
- agent = Idv::Agent.new(
- ssn: '900-55-8888',
- first_name: Faker::Name.first_name,
- zipcode: '11111',
- state_id_number: '123456789',
- state_id_type: 'drivers_license',
- state_id_jurisdiction: 'MD',
- )
+ agent = Idv::Agent.new(Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN.merge(uuid: user.uuid))
agent.proof_resolution(
document_capture_session, should_proof_state_id: true, trace_id: trace_id
)
@@ -55,8 +49,7 @@
context 'proofing state_id disabled' do
it 'does not proof state_id if resolution fails' do
agent = Idv::Agent.new(
- { ssn: '444-55-6666', first_name: Faker::Name.first_name,
- zipcode: '11111' },
+ Idp::Constants::MOCK_IDV_APPLICANT.merge(uuid: user.uuid, ssn: '444-55-6666'),
)
agent.proof_resolution(
document_capture_session, should_proof_state_id: true, trace_id: trace_id
@@ -67,10 +60,7 @@
end
it 'does not proof state_id if resolution succeeds' do
- agent = Idv::Agent.new(
- { ssn: '900-55-8888', first_name: Faker::Name.first_name,
- zipcode: '11111' },
- )
+ agent = Idv::Agent.new(Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN.merge(uuid: user.uuid))
agent.proof_resolution(
document_capture_session, should_proof_state_id: false, trace_id: trace_id
)
@@ -84,8 +74,7 @@
it 'returns a successful result if SSN does not start with 900 but is in SSN allowlist' do
agent = Idv::Agent.new(
- ssn: '999-99-9999', first_name: Faker::Name.first_name,
- zipcode: '11111'
+ Idp::Constants::MOCK_IDV_APPLICANT.merge(uuid: user.uuid, ssn: '999-99-9999'),
)
agent.proof_resolution(
@@ -101,8 +90,10 @@
it 'returns an unsuccessful result and notifies exception trackers if an exception occurs' do
agent = Idv::Agent.new(
- ssn: '900-55-8888', first_name: 'Time Exception',
- zipcode: '11111'
+ Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN.merge(
+ uuid: user.uuid,
+ first_name: 'Time Exception',
+ ),
)
agent.proof_resolution(
diff --git a/spec/services/idv/cancel_verification_attempt_spec.rb b/spec/services/idv/cancel_verification_attempt_spec.rb
index 3cfa7262a00..848c820f4ae 100644
--- a/spec/services/idv/cancel_verification_attempt_spec.rb
+++ b/spec/services/idv/cancel_verification_attempt_spec.rb
@@ -2,7 +2,7 @@
describe Idv::CancelVerificationAttempt do
let(:user) { create(:user, profiles: profiles) }
- let(:profiles) { [create(:profile, deactivation_reason: :verification_pending)] }
+ let(:profiles) { [create(:profile, deactivation_reason: :gpo_verification_pending)] }
subject { described_class.new(user: user) }
@@ -12,13 +12,13 @@
expect(profiles[0].active).to eq(false)
expect(profiles[0].reload.deactivation_reason).to eq('verification_cancelled')
- expect(user.reload.profiles.verification_pending).to be_empty
+ expect(user.reload.profiles.gpo_verification_pending).to be_empty
end
end
context 'the user has multiple pending profiles' do
let(:profiles) do
- super().push(create(:profile, deactivation_reason: :verification_pending))
+ super().push(create(:profile, deactivation_reason: :gpo_verification_pending))
end
it 'deactivates both profiles' do
@@ -28,7 +28,7 @@
expect(profiles[0].reload.deactivation_reason).to eq('verification_cancelled')
expect(profiles[1].active).to eq(false)
expect(profiles[1].reload.deactivation_reason).to eq('verification_cancelled')
- expect(user.reload.profiles.verification_pending).to be_empty
+ expect(user.reload.profiles.gpo_verification_pending).to be_empty
end
end
@@ -44,20 +44,20 @@
expect(profiles[0].reload.deactivation_reason).to eq('verification_cancelled')
expect(profiles[1].active).to eq(true)
expect(profiles[1].reload.deactivation_reason).to be_nil
- expect(user.reload.profiles.verification_pending).to be_empty
+ expect(user.reload.profiles.gpo_verification_pending).to be_empty
end
end
context 'when there are pending profiles for other users' do
it 'only updates profiles for the specificed user' do
- other_profile = create(:profile, deactivation_reason: :verification_pending)
+ other_profile = create(:profile, deactivation_reason: :gpo_verification_pending)
subject.call
expect(profiles[0].active).to eq(false)
expect(profiles[0].reload.deactivation_reason).to eq('verification_cancelled')
expect(other_profile.active).to eq(false)
- expect(other_profile.reload.deactivation_reason).to eq('verification_pending')
+ expect(other_profile.reload.deactivation_reason).to eq('gpo_verification_pending')
end
end
end
diff --git a/spec/services/idv/profile_maker_spec.rb b/spec/services/idv/profile_maker_spec.rb
index 5fb57840654..f996ad8c2bc 100644
--- a/spec/services/idv/profile_maker_spec.rb
+++ b/spec/services/idv/profile_maker_spec.rb
@@ -13,7 +13,8 @@
user_password: user_password,
)
end
- it 'creates a Profile with encrypted PII' do
+
+ it 'creates an inactive Profile with encrypted PII' do
proofing_component = ProofingComponent.create(user_id: user.id, document_check: 'acuant')
profile = subject.save_profile
pii = subject.pii_attributes
@@ -23,6 +24,8 @@
expect(profile.encrypted_pii).to_not be_nil
expect(profile.encrypted_pii).to_not match 'Some'
expect(profile.proofing_components).to match proofing_component.as_json
+ expect(profile.active).to eq false
+ expect(profile.deactivation_reason).to be_nil
expect(pii).to be_a Pii::Attributes
expect(pii.first_name).to eq 'Some'
diff --git a/spec/services/idv/session_spec.rb b/spec/services/idv/session_spec.rb
index f39d7cd3c20..db0887dc64e 100644
--- a/spec/services/idv/session_spec.rb
+++ b/spec/services/idv/session_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
describe Idv::Session do
- let(:user) { build(:user) }
+ let(:user) { create(:user) }
let(:user_session) { {} }
subject {
@@ -39,7 +39,7 @@
describe '#complete_session' do
context 'with phone verifed by vendor' do
before do
- subject.address_verification_mechanism = :phone
+ subject.address_verification_mechanism = 'phone'
subject.vendor_phone_confirmation = true
allow(subject).to receive(:complete_profile)
end
@@ -57,11 +57,61 @@
expect(subject).not_to have_received(:complete_profile)
end
+
+ context 'with establishing in person enrollment' do
+ let!(:enrollment) do
+ create(:in_person_enrollment, :establishing, user: user, profile: nil)
+ end
+
+ before do
+ ProofingComponent.create(user: user, document_check: Idp::Constants::Vendors::USPS)
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
+ subject.user_phone_confirmation = true
+ subject.applicant = Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE.merge(
+ same_address_as_id: true,
+ ).with_indifferent_access
+ subject.create_profile_from_applicant_with_password(user.password)
+ end
+
+ it 'sets profile to pending in person verification' do
+ subject.complete_session
+
+ expect(subject).not_to have_received(:complete_profile)
+ expect(subject.profile.deactivation_reason).to eq('in_person_verification_pending')
+ end
+
+ it 'creates a USPS enrollment' do
+ expect(UspsInPersonProofing::EnrollmentHelper).
+ to receive(:schedule_in_person_enrollment).
+ with(user, subject.applicant.transform_keys(&:to_s))
+
+ subject.complete_session
+
+ expect(enrollment.reload.profile).to eq(user.profiles.last)
+ end
+ end
+ end
+
+ context 'with gpo address verification' do
+ before do
+ subject.address_verification_mechanism = 'gpo'
+ subject.vendor_phone_confirmation = false
+ allow(subject).to receive(:complete_profile)
+ end
+
+ it 'sets profile to pending gpo verification' do
+ subject.applicant = {}
+ subject.create_profile_from_applicant_with_password(user.password)
+ subject.complete_session
+
+ expect(subject).not_to have_received(:complete_profile)
+ expect(subject.profile.deactivation_reason).to eq('gpo_verification_pending')
+ end
end
context 'without a confirmed phone number' do
before do
- subject.address_verification_mechanism = :phone
+ subject.address_verification_mechanism = 'phone'
subject.vendor_phone_confirmation = false
end
diff --git a/spec/services/idv/steps/ipp/verify_wait_step_show_spec.rb b/spec/services/idv/steps/ipp/verify_wait_step_show_spec.rb
index 7dcc87490af..87feefcb16e 100644
--- a/spec/services/idv/steps/ipp/verify_wait_step_show_spec.rb
+++ b/spec/services/idv/steps/ipp/verify_wait_step_show_spec.rb
@@ -7,6 +7,8 @@
let(:issuer) { 'test_issuer' }
let(:service_provider) { build(:service_provider, issuer: issuer) }
+ let(:request) { FakeRequest.new }
+
let(:controller) do
instance_double(
'controller',
@@ -16,6 +18,7 @@
flash: {},
poll_with_meta_refresh: nil,
url_options: {},
+ request: request,
)
end
@@ -126,7 +129,35 @@
}
end
- it 'marks the verify step incomplete' do
+ it 'marks the verify step incomplete and redirects to the warning page' do
+ expect(step).to receive(:redirect_to).with(
+ idv_session_errors_warning_url(
+ from: request.path,
+ ),
+ )
+ expect(flow.flow_session['Idv::Steps::Ipp::VerifyStep']).to be true
+ step.call
+
+ expect(flow.flow_session['Idv::Steps::Ipp::VerifyStep']).to be_nil
+ end
+ end
+
+ context 'when verification encounters an exception' do
+ let(:idv_result) do
+ {
+ context: { stages: { resolution: {} } },
+ errors: {},
+ exception: StandardError.new('testing'),
+ success: false,
+ }
+ end
+
+ it 'marks the verify step incomplete and redirects to the exception page' do
+ expect(step).to receive(:redirect_to).with(
+ idv_session_errors_exception_url(
+ from: request.path,
+ ),
+ )
expect(flow.flow_session['Idv::Steps::Ipp::VerifyStep']).to be true
step.call
diff --git a/spec/services/idv/steps/welcome_step_spec.rb b/spec/services/idv/steps/welcome_step_spec.rb
new file mode 100644
index 00000000000..9603863b623
--- /dev/null
+++ b/spec/services/idv/steps/welcome_step_spec.rb
@@ -0,0 +1,53 @@
+require 'rails_helper'
+
+describe Idv::Steps::WelcomeStep do
+ include Rails.application.routes.url_helpers
+
+ let(:user) { build(:user) }
+ let(:params) { {} }
+ let(:controller) do
+ instance_double('controller', current_user: user, params: params, session: {}, url_options: {})
+ end
+ let(:flow) do
+ Idv::Flows::DocAuthFlow.new(controller, {}, 'idv/doc_auth').tap do |flow|
+ flow.flow_session = {}
+ end
+ end
+
+ subject(:step) { Idv::Steps::WelcomeStep.new(flow) }
+
+ describe '#call' do
+ context 'without camera' do
+ let(:params) { { no_camera: true } }
+
+ it 'redirects to no camera error page' do
+ result = step.call
+
+ expect(redirect).to eq(idv_doc_auth_errors_no_camera_url)
+ expect(result.success?).to eq(false)
+ expect(result.errors).to eq(
+ message: 'Doc Auth error: Javascript could not detect camera on mobile device.',
+ )
+ end
+ end
+
+ context 'with previous establishing in-person enrollments' do
+ let!(:enrollment) { create(:in_person_enrollment, :establishing, user: user, profile: nil) }
+
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true)
+ end
+
+ it 'cancels all previous establishing enrollments' do
+ step.call
+
+ expect(enrollment.reload.status).to eq('cancelled')
+ expect(user.establishing_in_person_enrollment).to be_blank
+ end
+ end
+ end
+
+ def redirect
+ step.instance_variable_get(:@flow).instance_variable_get(:@redirect)
+ end
+end
diff --git a/spec/services/inherited_proofing/va/service_spec.rb b/spec/services/inherited_proofing/va/service_spec.rb
new file mode 100644
index 00000000000..11bf20689aa
--- /dev/null
+++ b/spec/services/inherited_proofing/va/service_spec.rb
@@ -0,0 +1,62 @@
+require 'rails_helper'
+
+RSpec.shared_examples 'an invalid auth code error is raised' do
+ it 'raises an error' do
+ expect { subject.execute }.to raise_error 'The provided auth_code is blank?'
+ end
+end
+
+RSpec.describe InheritedProofing::Va::Service do
+ include_context 'va_api_context'
+ include_context 'va_user_context'
+
+ subject(:service) { described_class.new auth_code }
+
+ before do
+ allow(service).to receive(:private_key).and_return(private_key)
+ end
+
+ it { respond_to :execute }
+
+ it do
+ expect(service.send(:private_key)).to eq private_key
+ end
+
+ describe '#execute' do
+ context 'when the auth code is valid' do
+ let(:auth_code) { 'mocked-auth-code-for-testing' }
+
+ it 'makes an authenticated request' do
+ stub = stub_request(:get, request_uri).
+ with(headers: request_headers).
+ to_return(status: 200, body: '{}', headers: {})
+
+ service.execute
+
+ expect(stub).to have_been_requested.once
+ end
+
+ it 'decrypts the response' do
+ stub_request(:get, request_uri).
+ with(headers: request_headers).
+ to_return(status: 200, body: encrypted_user_attributes, headers: {})
+
+ expect(service.execute).to eq user_attributes
+ end
+ end
+
+ context 'when the auth code is invalid' do
+ context 'when an empty? string' do
+ let(:auth_code) { '' }
+
+ it_behaves_like 'an invalid auth code error is raised'
+ end
+
+ context 'when an nil?' do
+ let(:auth_code) { nil }
+
+ it_behaves_like 'an invalid auth code error is raised'
+ end
+ end
+ end
+end
diff --git a/spec/services/push_notification/password_reset_event_spec.rb b/spec/services/push_notification/password_reset_event_spec.rb
new file mode 100644
index 00000000000..fde556d5d9f
--- /dev/null
+++ b/spec/services/push_notification/password_reset_event_spec.rb
@@ -0,0 +1,33 @@
+require 'rails_helper'
+
+RSpec.describe PushNotification::PasswordResetEvent do
+ include Rails.application.routes.url_helpers
+
+ subject(:event) do
+ described_class.new(user: user)
+ end
+
+ let(:user) { build(:user) }
+
+ describe '#event_type' do
+ it 'is the RISC event type' do
+ expect(event.event_type).to eq(described_class::EVENT_TYPE)
+ end
+ end
+
+ describe '#payload' do
+ let(:iss_sub) { SecureRandom.uuid }
+
+ subject(:payload) { event.payload(iss_sub: iss_sub) }
+
+ it 'is a subject with the provided iss_sub ' do
+ expect(payload).to eq(
+ subject: {
+ subject_type: 'iss-sub',
+ sub: iss_sub,
+ iss: root_url,
+ },
+ )
+ end
+ end
+end
diff --git a/spec/services/usps_in_person_proofing/enrollment_helper_spec.rb b/spec/services/usps_in_person_proofing/enrollment_helper_spec.rb
new file mode 100644
index 00000000000..293d1186fbc
--- /dev/null
+++ b/spec/services/usps_in_person_proofing/enrollment_helper_spec.rb
@@ -0,0 +1,73 @@
+require 'rails_helper'
+
+RSpec.describe UspsInPersonProofing::EnrollmentHelper do
+ let(:user) { build(:user) }
+ let(:current_address_matches_id) { false }
+ let(:pii) do
+ Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE.
+ merge(same_address_as_id: current_address_matches_id).
+ transform_keys(&:to_s)
+ end
+ let(:subject) { described_class }
+
+ describe '#schedule_in_person_enrollment' do
+ context 'an establishing enrollment record exists for the user' do
+ let!(:enrollment) do
+ create(:in_person_enrollment, user: user, status: :establishing, profile: nil)
+ end
+
+ it 'updates the existing enrollment record' do
+ expect(user.in_person_enrollments.length).to eq(1)
+
+ subject.schedule_in_person_enrollment(user, pii)
+ enrollment.reload
+
+ expect(enrollment.current_address_matches_id).to eq(current_address_matches_id)
+ end
+
+ it 'creates usps enrollment' do
+ proofer = UspsInPersonProofing::Mock::Proofer.new
+ mock = double
+
+ expect(UspsInPersonProofing::Mock::Proofer).to receive(:new).and_return(mock)
+ expect(mock).to receive(:request_enroll) do |applicant|
+ expect(applicant.first_name).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:first_name])
+ expect(applicant.last_name).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:last_name])
+ expect(applicant.address).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:address1])
+ expect(applicant.city).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:city])
+ expect(applicant.state).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:state])
+ expect(applicant.zip_code).to eq(Idp::Constants::MOCK_IDV_APPLICANT[:zipcode])
+ expect(applicant.email).to eq('no-reply@login.gov')
+ expect(applicant.unique_id).to be_a(String)
+
+ proofer.request_enroll(applicant)
+ end
+
+ subject.schedule_in_person_enrollment(user, pii)
+ end
+
+ it 'sets enrollment status to pending and sets enrollment established at date' do
+ subject.schedule_in_person_enrollment(user, pii)
+
+ expect(user.in_person_enrollments.first.status).to eq('pending')
+ expect(user.in_person_enrollments.first.enrollment_established_at).to_not be_nil
+ end
+
+ it 'sends verification emails' do
+ mailer = instance_double(ActionMailer::MessageDelivery, deliver_now_or_later: true)
+ user.email_addresses.each do |email_address|
+ expect(UserMailer).to receive(:in_person_ready_to_verify).
+ with(
+ user,
+ email_address,
+ enrollment: instance_of(InPersonEnrollment),
+ first_name: Idp::Constants::MOCK_IDV_APPLICANT_WITH_PHONE[:first_name],
+ ).
+ and_return(mailer)
+ end
+
+ subject.schedule_in_person_enrollment(user, pii)
+ end
+ end
+ end
+end
diff --git a/spec/services/usps_in_person_proofer_spec.rb b/spec/services/usps_in_person_proofing/proofer_spec.rb
similarity index 79%
rename from spec/services/usps_in_person_proofer_spec.rb
rename to spec/services/usps_in_person_proofing/proofer_spec.rb
index cb242644f77..f73cd103af0 100644
--- a/spec/services/usps_in_person_proofer_spec.rb
+++ b/spec/services/usps_in_person_proofing/proofer_spec.rb
@@ -15,6 +15,20 @@
end
end
+ def check_facility(facility)
+ expect(facility.address).to be_present
+ expect(facility.city).to be_present
+ expect(facility.distance).to be_present
+ expect(facility.name).to be_present
+ expect(facility.phone).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
+
describe '#request_facilities' do
it 'returns facilities' do
stub_request_token
@@ -29,14 +43,16 @@
facilities = subject.request_facilities(location)
- facility = facilities[0]
- expect(facility.distance).to be_present
- expect(facility.address).to be_present
- expect(facility.city).to be_present
- expect(facility.phone).to be_present
- expect(facility.name).to be_present
- expect(facility.zip_code).to be_present
- expect(facility.state).to be_present
+ check_facility(facilities[0])
+ end
+ end
+
+ describe '#request_pilot_facilities' do
+ it 'returns facilities' do
+ facilities = subject.request_pilot_facilities
+ expect(facilities.length).to eq(7)
+
+ check_facility(facilities[0])
end
end
@@ -109,14 +125,12 @@
enrollment_code: '123456789',
)
- expect(
- -> do
- subject.request_proofing_results(
- applicant.unique_id,
- applicant.enrollment_code,
- )
- end,
- ).to raise_error(
+ expect do
+ subject.request_proofing_results(
+ applicant.unique_id,
+ applicant.enrollment_code,
+ )
+ end.to raise_error(
an_instance_of(Faraday::BadRequestError).
and(having_attributes(
response: include(
diff --git a/spec/support/controller_helper.rb b/spec/support/controller_helper.rb
index f3863129499..6ee1590361f 100644
--- a/spec/support/controller_helper.rb
+++ b/spec/support/controller_helper.rb
@@ -47,6 +47,20 @@ def stub_verify_steps_one_and_two(user)
allow(subject).to receive(:user_session).and_return(user_session)
end
+ def stub_user_with_applicant_data(user, applicant)
+ user_session = {}
+ stub_sign_in(user)
+ idv_session = Idv::Session.new(
+ user_session: user_session, current_user: user,
+ service_provider: nil
+ )
+ idv_session.applicant = applicant.with_indifferent_access
+ idv_session.profile_confirmation = true
+ allow(subject).to receive(:confirm_idv_session_started).and_return(true)
+ allow(subject).to receive(:idv_session).and_return(idv_session)
+ allow(subject).to receive(:user_session).and_return(user_session)
+ end
+
def stub_decorated_user_with_pending_profile(user)
decorated_user = instance_double(UserDecorator)
allow(user).to receive(:decorate).and_return(decorated_user)
diff --git a/spec/support/features/idv_helper.rb b/spec/support/features/idv_helper.rb
index 08550106ffa..b31fbc39c10 100644
--- a/spec/support/features/idv_helper.rb
+++ b/spec/support/features/idv_helper.rb
@@ -30,6 +30,10 @@ def click_idv_continue
click_spinner_button_and_wait t('forms.buttons.continue')
end
+ def click_idv_select
+ click_select_button_and_wait t('in_person_proofing.body.location.location_button')
+ end
+
def choose_idv_otp_delivery_method_sms
page.find(
'label',
diff --git a/spec/support/features/in_person_helper.rb b/spec/support/features/in_person_helper.rb
index e2490ae8c92..4d028801b0a 100644
--- a/spec/support/features/in_person_helper.rb
+++ b/spec/support/features/in_person_helper.rb
@@ -34,8 +34,7 @@ def fill_out_address_form_ok
choose t('in_person_proofing.form.address.same_address_choice_yes')
end
- def begin_in_person_proofing(user = user_with_2fa)
- sign_in_and_2fa_user(user)
+ def begin_in_person_proofing(_user = user_with_2fa)
complete_doc_auth_steps_before_document_capture_step
mock_doc_auth_attention_with_barcode
attach_and_submit_images
@@ -44,7 +43,8 @@ def begin_in_person_proofing(user = user_with_2fa)
end
def complete_location_step(_user = user_with_2fa)
- click_idv_continue
+ first('.location-collection-item').
+ click_button(t('in_person_proofing.body.location.location_button'))
end
def complete_prepare_step(_user = user_with_2fa)
diff --git a/spec/support/features/interaction_helper.rb b/spec/support/features/interaction_helper.rb
index c6d57ade971..a2d7da1f9fc 100644
--- a/spec/support/features/interaction_helper.rb
+++ b/spec/support/features/interaction_helper.rb
@@ -1,6 +1,11 @@
module InteractionHelper
def click_spinner_button_and_wait(...)
- click_button(...)
+ click_on(...)
expect(page).to have_no_css('lg-spinner-button.spinner-button--spinner-active', wait: 10)
end
+
+ def click_select_button_and_wait(...)
+ click_button(...)
+ expect(page).to have_no_css('button.usa-button', wait: 10)
+ end
end
diff --git a/spec/support/idv_examples/gpo_otp_verification_step.rb b/spec/support/idv_examples/gpo_otp_verification_step.rb
index 12bdfb4e431..f2a6a8f1485 100644
--- a/spec/support/idv_examples/gpo_otp_verification_step.rb
+++ b/spec/support/idv_examples/gpo_otp_verification_step.rb
@@ -3,7 +3,7 @@
let(:profile) do
create(
:profile,
- deactivation_reason: :verification_pending,
+ deactivation_reason: :gpo_verification_pending,
pii: { ssn: '123-45-6789', dob: '1970-01-01' },
)
end
diff --git a/spec/support/matchers/have_actions.rb b/spec/support/matchers/have_actions.rb
index e349af88807..70583f08a51 100644
--- a/spec/support/matchers/have_actions.rb
+++ b/spec/support/matchers/have_actions.rb
@@ -38,7 +38,7 @@
callbacks = controller._process_action_callbacks.select { |callback| callback.kind == kind }
actions = callbacks.each_with_object([]) do |f, result|
- result << f.filter unless action_has_only_option?(f) || action_has_except_option?(f)
+ result << f.filter
result << [f.filter, only: parsed_only_action(f)] if action_has_only_option?(f)
result << [f.filter, if: parsed_only_action(f)] if action_has_only_option?(f)
result << [f.filter, except: parsed_except_action(f)] if action_has_except_option?(f)
@@ -67,9 +67,7 @@ def unless_option_for(action)
end
def parsed_only_action(action)
- only_option = if_option_for(action)[0]
-
- "#{only_option.class}OptionParser".constantize.new(only_option).parse
+ if_option_for(action)[0]
end
def parsed_except_action(action)
diff --git a/spec/support/private_key_file_helper.rb b/spec/support/private_key_file_helper.rb
new file mode 100644
index 00000000000..94a4aaaaf5e
--- /dev/null
+++ b/spec/support/private_key_file_helper.rb
@@ -0,0 +1,27 @@
+module PrivateKeyFileHelper
+ # Returns the private key in AppArtifacts.store.oidc_private_key if
+ # Identity::Hostdata.in_datacenter? or if the private key file does
+ # not exist; otherwise, the private key from the file is returned.
+ def private_key_from_store_or(file_name:)
+ file_name = force_tmp_private_key_file_name file_name: file_name
+
+ if Rails.env.test? && !File.exist?(file_name)
+ puts "WARNING: Private key file '#{file_name}' not found!"
+ end
+
+ if File.exist?(file_name)
+ OpenSSL::PKey::RSA.new(File.read(file_name))
+ else
+ return AppArtifacts.store.oidc_private_key
+ end
+ end
+
+ # Always ensure we're referencing files in the /tmp/ folder!
+ def force_tmp_private_key_file_name(file_name:)
+ "#{Rails.root}/tmp/#{File.basename(file_name)}"
+ end
+end
+
+RSpec.configure do |config|
+ config.include PrivateKeyFileHelper
+end
diff --git a/spec/support/shared_contexts/inherited_proofing/encrypted_user_attributes.json b/spec/support/shared_contexts/inherited_proofing/encrypted_user_attributes.json
new file mode 100644
index 00000000000..af29a512873
--- /dev/null
+++ b/spec/support/shared_contexts/inherited_proofing/encrypted_user_attributes.json
@@ -0,0 +1 @@
+{"data":"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.IL_uTLpwR3ZoDQKuRY_clxK1AmrEnf3rCREIj8XGQ-iA7NxCiYfZ2CxuXFOTIzFbKXjcNYT1F56bCUuPwSmHNt88AGumB3RcskR6POfBu8EcjK2CI6myycGuQwm_1Dp9Vi55TQpSFRy5Bld7IR0gbk4ju0qTVSeH59-AyBGr0w07vojdHcPe-SDWEC1pG0_4iyVg0x2wOFAh6kIjMJ04sJYB4e7uW8hEI7lSwDLpiW-8KsjGhwCVIkUGPw7XKtLiWo1U_nXSragpG-E6XRx0Hn3YckSwEAMTATeZZPJr0TAAMO_jtukL0e7_ApwsCI-sEdI035_4befLlDnuz1QFJg.oLmsRlZKFlL_3Th4.YumiTPq6y8jyCpVwuSpqsd8iWQ_AqEN81v8pV9lB2dPb6po03aj05K361IWmWfB3gXir--L3nPpUdlFFkxF1X12QVkpfmH03kj01Zoaq9hZcQvY7d4QoOkMNkdONNFZ3_sp-4-11m5ki2TpD1AidkLe7AIaSvBvhYOq0TC-0veLwRvp5234-XyDq9o5hLogzUa3G1BxcZO_TxpS5IhV4CzJ2a-o_ymSgUULDjrAty23XMiqXxTMFbVCpMDrvgGTX2TYOYx0PngjySlir6Zf4WjKhvFBOd34hvx2MUYTEGPw.UcPA0owzraT7ckc1cRDzeg"}
diff --git a/spec/support/shared_contexts/inherited_proofing/va_api_context.rb b/spec/support/shared_contexts/inherited_proofing/va_api_context.rb
new file mode 100644
index 00000000000..230a5b8307b
--- /dev/null
+++ b/spec/support/shared_contexts/inherited_proofing/va_api_context.rb
@@ -0,0 +1,15 @@
+RSpec.shared_context 'va_api_context' do
+ # Sample mocked API call:
+ # stub_request(:get, request_uri).
+ # with(headers: request_headers).
+ # to_return(status: 200, body: '{}', headers: {})
+
+ let(:auth_code) { 'mocked-auth-code-for-testing' }
+ let(:private_key) { private_key_from_store_or(file_name: 'empty.key') }
+ let(:payload) { { inherited_proofing_auth: auth_code, exp: 1.day.from_now.to_i } }
+ let(:jwt_token) { JWT.encode(payload, private_key, 'RS256') }
+ let(:request_uri) {
+ "#{InheritedProofing::Va::Service::BASE_URI}/inherited_proofing/user_attributes"
+ }
+ let(:request_headers) { { Authorization: "Bearer #{jwt_token}" } }
+end
diff --git a/spec/support/shared_contexts/inherited_proofing/va_user_context.rb b/spec/support/shared_contexts/inherited_proofing/va_user_context.rb
new file mode 100644
index 00000000000..4510ccb1fa5
--- /dev/null
+++ b/spec/support/shared_contexts/inherited_proofing/va_user_context.rb
@@ -0,0 +1,18 @@
+RSpec.shared_context 'va_user_context' do
+ # As given to us from VA
+ let(:user_attributes) {
+ { first_name: 'Fakey',
+ last_name: 'Fakerson',
+ address: { street: '123 Fake St',
+ street2: 'Apt 235',
+ city: 'Faketown',
+ state: 'WA',
+ country: nil,
+ zip: '98037' },
+ phone: '2063119187',
+ birth_date: '2022-1-31',
+ ssn: '123456789' }
+ }
+ # Encrypted with AppArtifacts.store.oidc_private_key for testing
+ let(:encrypted_user_attributes) { File.read("#{__dir__}/encrypted_user_attributes.json") }
+end
diff --git a/spec/support/usps_ipp_fixtures.rb b/spec/support/usps_ipp_fixtures.rb
index d4121f6fb49..78603c9f292 100644
--- a/spec/support/usps_ipp_fixtures.rb
+++ b/spec/support/usps_ipp_fixtures.rb
@@ -7,6 +7,14 @@ def self.request_facilities_response
load_response_fixture('request_facilities_response.json')
end
+ def self.request_show_usps_location_response
+ load_response_fixture('request_show_usps_location_response.json')
+ end
+
+ def self.enrollment_selected_location_details
+ load_response_fixture('enrollment_selected_location_details.json')
+ end
+
def self.request_enroll_response
load_response_fixture('request_enroll_response.json')
end
diff --git a/spec/views/idv/doc_auth/_back.html.erb_spec.rb b/spec/views/idv/doc_auth/_back.html.erb_spec.rb
deleted file mode 100644
index c6dd601f161..00000000000
--- a/spec/views/idv/doc_auth/_back.html.erb_spec.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-require 'rails_helper'
-
-describe 'idv/doc_auth/_back.html.erb' do
- let(:action) { nil }
- let(:step) { nil }
- let(:classes) { nil }
- let(:fallback_path) { nil }
-
- subject do
- render 'idv/doc_auth/back', {
- action: action,
- step: step,
- class: classes,
- fallback_path: fallback_path,
- }
- end
-
- shared_examples 'back link with class' do
- let(:classes) { 'example-class' }
-
- it 'renders with class' do
- expect(subject).to have_css('.example-class')
- end
- end
-
- context 'with action' do
- let(:action) { 'redo_ssn' }
-
- it 'renders' do
- expect(subject).to have_selector("form[action='#{idv_doc_auth_step_path(step: 'redo_ssn')}']")
- expect(subject).to have_selector('input[name="_method"][value="put"]', visible: false)
- expect(subject).to have_selector("[type='submit']")
- expect(subject).to have_selector('button', text: '‹ ' + t('forms.buttons.back'))
- end
-
- it_behaves_like 'back link with class'
- end
-
- context 'with step' do
- let(:step) { 'verify' }
-
- it 'renders' do
- expect(subject).to have_selector("a[href='#{idv_doc_auth_step_path(step: 'verify')}']")
- expect(subject).to have_content('‹ ' + t('forms.buttons.back'))
- end
-
- it_behaves_like 'back link with class'
- end
-
- context 'with back path' do
- before do
- allow(view).to receive(:go_back_path).and_return('/example')
- end
-
- it 'renders with back path' do
- expect(subject).to have_selector('a[href="/example"]')
- expect(subject).to have_content('‹ ' + t('forms.buttons.back'))
- end
- end
-
- context 'with fallback link' do
- let(:fallback_path) { '/example' }
-
- it 'renders' do
- expect(subject).to have_selector('a[href="/example"]')
- expect(subject).to have_content('‹ ' + t('forms.buttons.back'))
- end
-
- it_behaves_like 'back link with class'
- end
-
- context 'no back path' do
- it 'renders nothing' do
- render 'idv/doc_auth/back'
-
- expect(subject).to be_empty
- end
- end
-end
diff --git a/spec/views/idv/in_person/ready_to_verify/show.html.erb_spec.rb b/spec/views/idv/in_person/ready_to_verify/show.html.erb_spec.rb
index 7661da3eadd..c46f8cb537b 100644
--- a/spec/views/idv/in_person/ready_to_verify/show.html.erb_spec.rb
+++ b/spec/views/idv/in_person/ready_to_verify/show.html.erb_spec.rb
@@ -7,6 +7,9 @@
let(:profile) { build(:profile, user: user) }
let(:enrollment_code) { '2048702198804358' }
let(:current_address_matches_id) { true }
+ let(:selected_location_details) {
+ JSON.parse(UspsIppFixtures.enrollment_selected_location_details)
+ }
let(:created_at) { Time.zone.parse('2022-07-13') }
let(:enrollment) do
InPersonEnrollment.new(
@@ -16,8 +19,7 @@
unique_id: InPersonEnrollment.generate_unique_id,
created_at: created_at,
current_address_matches_id: current_address_matches_id,
- selected_location_details:
- JSON.parse(UspsIppFixtures.request_facilities_response)['postOffices'].first,
+ selected_location_details: selected_location_details,
)
end
let(:presenter) { Idv::InPerson::ReadyToVerifyPresenter.new(enrollment: enrollment) }
@@ -45,4 +47,22 @@
expect(rendered).to have_content(t('in_person_proofing.process.proof_of_address.heading'))
end
end
+
+ context 'with enrollment where selected_location_details is present' do
+ it 'renders a location' do
+ render
+
+ expect(rendered).to have_content(t('in_person_proofing.body.barcode.speak_to_associate'))
+ end
+ end
+
+ context 'with enrollment where selected_location_details is not present' do
+ let(:selected_location_details) { nil }
+
+ it 'does not render a location' do
+ render
+
+ expect(rendered).not_to have_content(t('in_person_proofing.body.barcode.speak_to_associate'))
+ end
+ end
end
diff --git a/spec/views/idv/session_errors/exception.html.erb_spec.rb b/spec/views/idv/session_errors/exception.html.erb_spec.rb
index a575741e774..0a78c990215 100644
--- a/spec/views/idv/session_errors/exception.html.erb_spec.rb
+++ b/spec/views/idv/session_errors/exception.html.erb_spec.rb
@@ -5,6 +5,7 @@
let(:sp_issuer) { nil }
let(:in_person_proofing_enabled) { false }
let(:in_person_proofing_enabled_issuers) { [] }
+ let(:try_again_path) { '/example/path' }
before do
decorated_session = instance_double(
@@ -18,11 +19,13 @@
allow(IdentityConfig.store).to receive(:in_person_proofing_enabled_issuers).
and_return(in_person_proofing_enabled_issuers)
+ assign(:try_again_path, try_again_path)
+
render
end
it 'shows a primary action' do
- expect(rendered).to have_link(t('idv.failure.button.warning'), href: idv_doc_auth_path)
+ expect(rendered).to have_link(t('idv.failure.button.warning'), href: try_again_path)
end
it 'renders a list of troubleshooting options' do
diff --git a/spec/views/idv/session_errors/warning.html.erb_spec.rb b/spec/views/idv/session_errors/warning.html.erb_spec.rb
index f1a3dd05516..c567b408b2f 100644
--- a/spec/views/idv/session_errors/warning.html.erb_spec.rb
+++ b/spec/views/idv/session_errors/warning.html.erb_spec.rb
@@ -2,6 +2,7 @@
describe 'idv/session_errors/warning.html.erb' do
let(:sp_name) { nil }
+ let(:try_again_path) { '/example/path' }
let(:remaining_attempts) { 5 }
let(:user_session) { {} }
@@ -11,12 +12,13 @@
allow(view).to receive(:user_session).and_return(user_session)
assign(:remaining_attempts, remaining_attempts)
+ assign(:try_again_path, try_again_path)
render
end
it 'shows a primary action' do
- expect(rendered).to have_link(t('idv.failure.button.warning'), href: idv_doc_auth_path)
+ expect(rendered).to have_link(t('idv.failure.button.warning'), href: try_again_path)
end
it 'shows remaining attempts' do
diff --git a/spec/views/idv/shared/_back.html.erb_spec.rb b/spec/views/idv/shared/_back.html.erb_spec.rb
new file mode 100644
index 00000000000..83d004fef13
--- /dev/null
+++ b/spec/views/idv/shared/_back.html.erb_spec.rb
@@ -0,0 +1,129 @@
+require 'rails_helper'
+
+describe 'idv/doc_auth/_back.html.erb' do
+ let(:step_url) { nil }
+ let(:action) { nil }
+ let(:step) { nil }
+ let(:classes) { nil }
+ let(:fallback_path) { nil }
+
+ subject do
+ render 'idv/shared/back', {
+ step_url: step_url,
+ action: action,
+ step: step,
+ class: classes,
+ fallback_path: fallback_path,
+ }
+ end
+
+ shared_examples 'back link with class' do
+ let(:classes) { 'example-class' }
+
+ it 'renders with class' do
+ expect(subject).to have_css('.example-class')
+ end
+ end
+
+ context 'with step URL in locals' do
+ let(:step_url) { :idv_doc_auth_step_url }
+
+ context 'with action' do
+ let(:action) { 'redo_ssn' }
+
+ it 'renders' do
+ expect(subject).to have_selector(
+ "form[action='#{send(:idv_doc_auth_step_url, step: 'redo_ssn')}']",
+ )
+ expect(subject).to have_selector('input[name="_method"][value="put"]', visible: false)
+ expect(subject).to have_selector("[type='submit']")
+ expect(subject).to have_selector('button', text: '‹ ' + t('forms.buttons.back'))
+ end
+
+ it_behaves_like 'back link with class'
+ end
+
+ context 'with step' do
+ let(:step) { 'verify' }
+
+ it 'renders' do
+ expect(subject).to have_selector(
+ "a[href='#{send(
+ :idv_doc_auth_step_url,
+ step: 'verify',
+ )}']",
+ )
+ expect(subject).to have_content('‹ ' + t('forms.buttons.back'))
+ end
+
+ it_behaves_like 'back link with class'
+ end
+ end
+
+ context 'with step URL in instance variable' do
+ before do
+ assign(:step_url, :idv_doc_auth_step_url)
+ end
+
+ context 'with action' do
+ let(:action) { 'redo_ssn' }
+
+ it 'renders' do
+ expect(subject).to have_selector(
+ "form[action='#{send(:idv_doc_auth_step_url, step: 'redo_ssn')}']",
+ )
+ expect(subject).to have_selector('input[name="_method"][value="put"]', visible: false)
+ expect(subject).to have_selector("[type='submit']")
+ expect(subject).to have_selector('button', text: '‹ ' + t('forms.buttons.back'))
+ end
+
+ it_behaves_like 'back link with class'
+ end
+
+ context 'with step' do
+ let(:step) { 'verify' }
+
+ it 'renders' do
+ expect(subject).to have_selector(
+ "a[href='#{send(
+ :idv_doc_auth_step_url,
+ step: 'verify',
+ )}']",
+ )
+ expect(subject).to have_content('‹ ' + t('forms.buttons.back'))
+ end
+
+ it_behaves_like 'back link with class'
+ end
+ end
+
+ context 'with back path' do
+ before do
+ allow(view).to receive(:go_back_path).and_return('/example')
+ end
+
+ it 'renders with back path' do
+ expect(subject).to have_selector('a[href="/example"]')
+ expect(subject).to have_content('‹ ' + t('forms.buttons.back'))
+ end
+ end
+
+ context 'with fallback link' do
+ let(:fallback_path) { '/example' }
+
+ it 'renders' do
+ expect(subject).to have_selector('a[href="/example"]')
+ expect(subject).to have_content('‹ ' + t('forms.buttons.back'))
+ end
+
+ it_behaves_like 'back link with class'
+ end
+
+ context 'no back path' do
+ it 'renders nothing' do
+ render 'idv/shared/back'
+
+ expect(subject).to be_empty
+ end
+ end
+end
diff --git a/spec/views/shared/_one_time_code_input.html.erb_spec.rb b/spec/views/shared/_one_time_code_input.html.erb_spec.rb
index 39e5286e464..92b97abda0d 100644
--- a/spec/views/shared/_one_time_code_input.html.erb_spec.rb
+++ b/spec/views/shared/_one_time_code_input.html.erb_spec.rb
@@ -102,7 +102,6 @@
describe 'maxlength' do
context 'no maxlength given' do
it 'renders input maxlength DIRECT_OTP_LENGTH' do
- puts rendered
expect(rendered).to have_selector(
"[maxlength=\"#{TwoFactorAuthenticatable::DIRECT_OTP_LENGTH}\"]",
)