diff --git a/.codeclimate.yml b/.codeclimate.yml index 92e3e98c6a4..a4b07479f97 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,6 +1,11 @@ engines: brakeman: enabled: true + exclude_paths: + # Excluding User Flows tools since these are not loaded + # except when explicitly called from the User Flow rake tasks + - 'lib/user_flow_exporter.rb' + - 'lib/rspec/formatters/user_flow_formatter.rb' bundler-audit: enabled: true coffeelint: @@ -19,6 +24,8 @@ engines: - 'node_modules/**/*' - 'db/schema.rb' - 'app/forms/password_form.rb' + - 'lib/user_flow_exporter.rb' + - 'lib/rspec/formatters/user_flow_formatter.rb' eslint: enabled: true fixme: @@ -38,6 +45,8 @@ engines: exclude_paths: - 'spec/**/*' - 'db/migrate/*' + - 'lib/user_flow_exporter.rb' + - 'lib/rspec/formatters/user_flow_formatter.rb' rubocop: enabled: true scss-lint: @@ -50,4 +59,5 @@ ratings: - '**.rb' - '**.go' exclude_paths: - - 'lib/rspec/formatters/*' + - 'lib/user_flow_exporter.rb' + - 'lib/rspec/formatters/user_flow_formatter.rb' diff --git a/.rubocop.yml b/.rubocop.yml index f52cbfadd9d..cd7b91340b4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -14,6 +14,8 @@ AllCops: - 'config/initializers/devise.rb' - 'db/migrate/*' - 'spec/services/pii/nist_encryption_spec.rb' + - 'lib/rspec/user_flow_formatter.rb' + - 'lib/user_flow_exporter.rb' TargetRubyVersion: 2.3 UseCache: true diff --git a/README.md b/README.md index f595a88634a..c4aa2aba38e 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,16 @@ $ RAILS_ASSET_HOST=localhost:3000 rake spec:user_flows Then, visit http://localhost:3000/user_flows in your browser! +##### Exporting + +The user flows tool also has an export feature which allows you to export everything for the web. You may host these assets with someting like [`simplehttpserver`](https://www.npmjs.com/package/simplehttpserver) or publish to [Federalist](https://federalist.18f.gov/). To publish user flows for Federalist, first make sure the application is running locally (eg. localhost:3000) and run: + +``` +$ RAILS_ASSET_HOST=localhost:3000 FEDERALIST_PATH=/site/user/repository rake spec:user_flows:web +``` + +This will output your site to `public/site/user/repository` for quick publishing to [Federalist](https://federalist-docs.18f.gov/pages/using-federalist/). To test compatibility, run `simplehttpserver` from the app's `public` folder and visit `http://localhost:8000//user_flows` in your browser. + ### Load testing We provide some [Locust.io] Python scripts you can run to test how the diff --git a/config/environments/test.rb b/config/environments/test.rb index 522784f0c36..e23d43c3c47 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -17,14 +17,16 @@ config.action_mailer.asset_host = Figaro.env.mailer_domain_name config.action_mailer.default_options = { from: Figaro.env.email_from } + config.assets.debug = true + if ENV.key?('RAILS_ASSET_HOST') config.action_controller.asset_host = ENV['RAILS_ASSET_HOST'] else config.action_controller.asset_host = '//' end - config.assets.debug = true - config.assets.digest = true + config.assets.digest = ENV.key?('RAILS_DISABLE_ASSET_DIGEST') ? false : true + config.middleware.use RackSessionAccess::Middleware config.lograge.enabled = true config.lograge.custom_options = ->(event) { event.payload } diff --git a/lib/rspec/formatters/user_flow_formatter.rb b/lib/rspec/formatters/user_flow_formatter.rb index aea9bd71889..3176080cdfc 100644 --- a/lib/rspec/formatters/user_flow_formatter.rb +++ b/lib/rspec/formatters/user_flow_formatter.rb @@ -23,10 +23,10 @@ class UserFlowFormatter < RSpec::Core::Formatters::DocumentationFormatter :stop def initialize(output) - @html = '' \ + @html = '' \ '' \ '

' \ - '' \ + '' \ '/ user flows

' \ '
'
     @user_flows_html_file = Capybara.save_path.join('index.html').to_s
diff --git a/lib/tasks/user_flows.rake b/lib/tasks/user_flows.rake
index d71a0119412..fc5a38477ca 100644
--- a/lib/tasks/user_flows.rake
+++ b/lib/tasks/user_flows.rake
@@ -7,5 +7,13 @@ unless Rails.env.production?
                         --require ./lib/rspec/formatters/user_flow_formatter.rb
                         --format UserFlowFormatter]
     end
+
+    desc 'Exports user flows for the web'
+    task 'user_flows:web' do |t|
+      ENV['RAILS_DISABLE_ASSET_DIGEST'] = 'true'
+      require './lib/user_flow_exporter'
+      Rake::Task['spec:user_flows'].invoke
+      UserFlowExporter.run
+    end
   end
 end
diff --git a/lib/user_flow_exporter.rb b/lib/user_flow_exporter.rb
new file mode 100644
index 00000000000..b1b089b1e65
--- /dev/null
+++ b/lib/user_flow_exporter.rb
@@ -0,0 +1,91 @@
+# This module is part of the User Flows toolchest
+# 
+# UserFlowExporter.run - scrapes user flows for use on the web
+# 
+# Dependencies:
+#   - Must be running the application locally eg (localhost:3000)
+#   - Must have wget installed and available on your PATH
+# 
+# Executing:
+#   Start the application with:
+#     $ make run
+#   Export flows with:
+#     $ RAILS_ASSET_HOST=localhost:3000 FEDERALIST_PATH=/site/user/repo rake spec:user_flows:web
+#   Use the files output to public/ in a Github repo connected to Federalist
+#     $ cp -r ./public/site/user/repo ~/code/login-user-flows
+#   And commit the changes in the Federalist repo!
+
+module UserFlowExporter
+  ASSET_HOST = ENV['RAILS_ASSET_HOST'] || 'localhost:3000'
+  # Coming soon: signal testing for different devices
+  # USER_AGENT = "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.3) Gecko/2008092416 Firefox/3.0.3"
+  FEDERALIST_PATH = ENV['FEDERALIST_PATH'] || '/flows_export/'
+
+  def self.run
+    Kernel.puts "Preparing to scrape user flows...\n"
+    url = "http://#{ASSET_HOST}/user_flows/"
+    # The web-friendly flows are still output to the public directory
+    # in order to quickly test the content by visiting your locally
+    # hosted application (eg. localhost:3000/site/18f/identity-ux/user_flows)
+
+    if FEDERALIST_PATH[0] != '/'
+      raise 'Federalist path must start with a slash (eg. /site/18f/identity-ux)'
+    end
+
+    output_dir = "public#{FEDERALIST_PATH}"
+
+    # -r = recursively mirrors site
+    # -H = span hosts (e.g. include assets from other domains) 
+    # -p = download all assets associated with the page
+    # --no-host-directories = removes domain prefix from output path
+    # -P = output prefix (a.k.a the directory to dump the assets)
+    # --domains = whitelist of domains to include when following links
+    scrape_cmd = "wget -r -H -p --no-host-directories " \
+                "-P '#{output_dir}' " \
+                "--domains 'localhost' " \
+                "'#{url}'"
+    system(scrape_cmd)
+
+    massage_html(output_dir)
+    massage_assets(output_dir)
+
+    Kernel.puts 'Done! The user flows are now prepared for use on the interwebs!'
+  end
+
+  private
+
+  def self.massage_html(dir)
+    Dir.glob("#{dir}/**/*.html") do |html|
+      File.open(html) do |f|
+        contents = File.read(f.path)
+        contents.gsub!("http://#{ASSET_HOST}/", "#{FEDERALIST_PATH}/")
+        contents.gsub!('.css?body=1', '.css')
+        contents.gsub!('.js?body=1', '.js')
+        contents.gsub!('href="/assets/', "href=\"#{FEDERALIST_PATH}/assets/")
+        contents.gsub!('src="/assets/', "src=\"#{FEDERALIST_PATH}/assets/")
+        contents.gsub!("href='/user_flows/", "href='#{FEDERALIST_PATH}/user_flows/")
+
+        contents.gsub!("", "")
+
+        File.open(f.path, "w") {|file| file.puts contents }
+        Kernel.puts "Updated #{f.path} references"
+      end
+    end
+  end
+
+  def self.massage_assets(dir)
+    Dir.glob("#{dir}/assets/**/**") do |file|
+      if file[-11..-1] == '.css?body=1'
+        new_filename = file.gsub('.css?body=1', '.css')
+        `mv #{file} #{new_filename}`
+        Kernel.puts "Moved #{file} to #{new_filename}"
+      end
+
+      if file[-10..-1] == '.js?body=1'
+        new_filename = file.gsub('.js?body=1', '.js')
+        `mv #{file} #{new_filename}`
+        Kernel.puts "Moved #{file} to #{new_filename}"
+      end
+    end
+  end
+end
\ No newline at end of file
diff --git a/spec/features/flows/sp_authentication_flows_spec.rb b/spec/features/flows/sp_authentication_flows_spec.rb
index c8146a4cc59..49fdc1dd6f8 100644
--- a/spec/features/flows/sp_authentication_flows_spec.rb
+++ b/spec/features/flows/sp_authentication_flows_spec.rb
@@ -1,145 +1,375 @@
 require 'rails_helper'
-
+include IdvHelper
 include SamlAuthHelper
 
-feature 'SP-initiated authentication with login.gov', devise: true, user_flow: true do
+feature 'SP-initiated authentication with login.gov', user_flow: true do
   context 'with a valid SP' do
-    before do
-      visit authnrequest_get
-    end
-
-    it 'prompts the user to create an account or sign in' do
-      screenshot_and_save_page
-    end
-
-    context 'when choosing Create Account' do
+    context 'when LOA3' do
       before do
-        click_link t('sign_up.registrations.create_account')
+        visit loa3_authnrequest
       end
 
-      it 'displays an interstitial page with information' do
+      it 'prompts the user to create an account or sign in' do
         screenshot_and_save_page
       end
 
-      it 'prompts for email address' do
-        screenshot_and_save_page
-      end
-
-      context 'with a valid email address submitted' do
+      context 'when choosing Create Account' do
         before do
-          @email = Faker::Internet.safe_email
-          fill_in 'Email', with: @email
-          click_button t('forms.buttons.submit.default')
-          @user = User.find_with_email(@email)
+          click_link t('sign_up.registrations.create_account')
         end
 
-        it 'informs the user to check email' do
+        it 'prompts for email address' do
           screenshot_and_save_page
         end
 
-        context 'with a confirmed email address' do
+        context 'with a valid email address submitted' do
           before do
-            confirm_last_user
+            @email = Faker::Internet.safe_email
+            fill_in 'Email', with: @email
+            click_button t('forms.buttons.submit.default')
+            @user = User.find_with_email(@email)
           end
 
-          it 'prompts the user for a password' do
+          it 'informs the user to check email' do
             screenshot_and_save_page
           end
 
-          context 'with a valid password' do
+          context 'with a confirmed email address' do
             before do
-              fill_in 'password_form_password', with: Features::SessionHelper::VALID_PASSWORD
-              click_button t('forms.buttons.continue')
+              confirm_last_user
             end
 
-            it 'prompts the user to configure 2FA' do
+            it 'prompts the user for a password' do
               screenshot_and_save_page
             end
 
-            context 'with a valid phone number' do
+            context 'with a valid password' do
               before do
-                fill_in 'Phone', with: Faker::PhoneNumber.cell_phone
+                fill_in 'password_form_password', with: Features::SessionHelper::VALID_PASSWORD
+                click_button t('forms.buttons.continue')
               end
 
-              context 'with SMS delivery' do
-                before do
-                  choose t('devise.two_factor_authentication.otp_delivery_preference.sms')
-                  click_send_security_code
-                end
-
-                it 'prompts for OTP' do
-                  screenshot_and_save_page
-                end
+              it 'prompts the user to configure 2FA' do
+                screenshot_and_save_page
               end
 
-              context 'with Voice delivery' do
+              context 'with a valid phone number' do
                 before do
-                  choose t('devise.two_factor_authentication.otp_delivery_preference.voice')
-                  click_send_security_code
+                  fill_in 'Phone', with: Faker::PhoneNumber.cell_phone
                 end
 
-                it 'prompts for OTP' do
-                  screenshot_and_save_page
+                context 'with SMS delivery' do
+                  before do
+                    choose t('devise.two_factor_authentication.otp_delivery_preference.sms')
+                    click_send_security_code
+                  end
+
+                  it 'prompts for OTP' do
+                    screenshot_and_save_page
+                  end
+
+                  context 'with valid OTP confirmation' do
+                    before do
+                      fill_in 'code', with: @user.reload.direct_otp
+                      click_button t('forms.buttons.submit.default')
+                    end
+
+                    it 'prompts the user to verify oneself' do
+                      screenshot_and_save_page
+                    end
+
+                    context 'when choosing Yes, continue' do
+                      before do
+                        click_link t('idv.index.continue_link')
+                      end
+
+                      it 'prompts for personal information' do
+                        screenshot_and_save_page
+                      end
+
+                      context 'with valid personal information entered' do
+                        before do
+                          fill_in t('idv.form.first_name'), with: Faker::Name.first_name
+                          fill_in t('idv.form.last_name'), with: Faker::Name.last_name
+                          fill_in 'profile_address1', with: '123 Main St'
+                          fill_in 'profile_city', with: Faker::Address.city
+                          find('#profile_state').find(:xpath,
+                                                      "option[#{(1..50).to_a.sample}]").
+                            select_option
+                          fill_in 'profile_zipcode', with: Faker::Address.zip_code
+                          fill_in t('idv.form.dob'), with: "09/09/#{(1900..2000).to_a.sample}"
+                          fill_in 'profile_ssn', with: "999-99-#{(1000..9999).to_a.sample}"
+                          click_button t('forms.buttons.continue')
+                        end
+
+                        it 'prompts for the last 8 digits of a credit card' do
+                          screenshot_and_save_page
+                        end
+
+                        context 'with last 8 digits of credit card' do
+                          before do
+                            fill_out_financial_form_ok
+                          end
+
+                          it 'prompts to activate account by phone or mail' do
+                            screenshot_and_save_page
+                          end
+                        end
+
+                        context 'without a credit card' do
+                          before do
+                            click_link t('idv.form.use_financial_account')
+                          end
+
+                          it 'prompts user to provide a financial account number' do
+                            screenshot_and_save_page
+                          end
+
+                          context 'with a valid financial account' do
+                            before do
+                              select t('idv.form.mortgage'), from: 'idv_finance_form_finance_type'
+                              fill_in 'idv_finance_form_mortgage', with: '12345678'
+                              # click_idv_continue doesn't work with the JavaScript on this page
+                              # and enabling js: true causes unexpected behavior
+                              form = page.find('#new_idv_finance_form')
+                              class << form
+                                def submit!
+                                  Capybara::RackTest::Form.new(driver, native).submit({})
+                                end
+                              end
+                              form.submit!
+                            end
+
+                            it 'prompts to activate account by phone or mail' do
+                              screenshot_and_save_page
+                            end
+
+                            context 'when activating by phone' do
+                              before do
+                                click_idv_address_choose_phone
+                              end
+
+                              it 'prompts the user to confirm or enter phone number' do
+                                screenshot_and_save_page
+                              end
+                            end
+
+                            context 'when activating by mail' do
+                              before do
+                                click_idv_address_choose_usps
+                              end
+
+                              it 'prompts the user to confirm' do
+                                screenshot_and_save_page
+                              end
+
+                              context 'when confirming to mail' do
+                                before do
+                                  click_on t('idv.buttons.mail.send')
+                                end
+
+                                it 'prompts user for password to encrypt profile' do
+                                  screenshot_and_save_page
+                                end
+
+                                context 'when confirming password' do
+                                  before do
+                                    fill_in 'user_password',
+                                            with: Features::SessionHelper::VALID_PASSWORD
+                                    click_button t('forms.buttons.submit.default')
+                                  end
+
+                                  it 'provides a new personal key and prompts for verification' do
+                                    screenshot_and_save_page
+                                  end
+
+                                  context 'when clicking Continue' do
+                                    before do
+                                      click_acknowledge_personal_key
+                                    end
+
+                                    it 'displays the user profile' do
+                                      screenshot_and_save_page
+                                    end
+                                  end
+                                end
+                              end
+                            end
+
+                            # Disabling this spec because of js: true issue
+                            # Will re-enable this once resolved
+                            # context 'when choosing to cancel' do
+                            #   before do
+                            #     click_button t('links.cancel_idv')
+                            #   end
+
+                            #   it 'prompts to continue verification or visit profile' do
+                            #     screenshot_and_save_page
+                            #   end
+                            # end
+                          end
+                        end
+                      end
+
+                      context 'with invalid personal information entered' do
+                        before do
+                          fill_out_idv_form_fail
+                          click_button t('forms.buttons.continue')
+                        end
+
+                        it 'presents a modal with current retries remaining' do
+                          screenshot_and_save_page
+                        end
+                      end
+                    end
+                  end
                 end
               end
             end
           end
         end
       end
+
+      # context 'when choosing to sign in' do
+      #   TODO: duplicate scenarios from Create Account here
+      # end
     end
 
-    context 'when choosing to sign in' do
+    context 'when LOA1' do
       before do
-        @user = create(:user, :signed_up)
-        click_link t('links.sign_in')
+        visit authnrequest_get
+      end
+
+      it 'prompts the user to create an account or sign in' do
+        screenshot_and_save_page
       end
 
-      context 'with valid credentials entered' do
+      context 'when choosing Create Account' do
         before do
-          fill_in_credentials_and_submit(@user.email, @user.password)
+          click_link t('sign_up.registrations.create_account')
         end
 
-        it 'prompts for 2FA delivery method' do
+        it 'prompts for email address' do
           screenshot_and_save_page
         end
 
-        context 'with SMS OTP selected (default)' do
-          it 'prompts for OTP verification' do
+        context 'with a valid email address submitted' do
+          before do
+            @email = Faker::Internet.safe_email
+            fill_in 'Email', with: @email
+            click_button t('forms.buttons.submit.default')
+            @user = User.find_with_email(@email)
+          end
+
+          it 'informs the user to check email' do
             screenshot_and_save_page
           end
 
-          context 'with valid OTP confirmation' do
+          context 'with a confirmed email address' do
             before do
-              fill_in 'code', with: @user.reload.direct_otp
-              click_button t('forms.buttons.submit.default')
+              confirm_last_user
             end
 
-            xit 'redirects back to SP' do
+            it 'prompts the user for a password' do
               screenshot_and_save_page
             end
+
+            context 'with a valid password' do
+              before do
+                fill_in 'password_form_password', with: Features::SessionHelper::VALID_PASSWORD
+                click_button t('forms.buttons.continue')
+              end
+
+              it 'prompts the user to configure 2FA' do
+                screenshot_and_save_page
+              end
+
+              context 'with a valid phone number' do
+                before do
+                  fill_in 'Phone', with: Faker::PhoneNumber.cell_phone
+                end
+
+                context 'with SMS delivery' do
+                  before do
+                    choose t('devise.two_factor_authentication.otp_delivery_preference.sms')
+                    click_send_security_code
+                  end
+
+                  it 'prompts for OTP' do
+                    screenshot_and_save_page
+                  end
+                end
+
+                context 'with Voice delivery' do
+                  before do
+                    choose t('devise.two_factor_authentication.otp_delivery_preference.voice')
+                    click_send_security_code
+                  end
+
+                  it 'prompts for OTP' do
+                    screenshot_and_save_page
+                  end
+                end
+              end
+            end
           end
         end
       end
 
-      context 'without a valid username and password' do
-        context 'when choosing "Forgot your password?"' do
+      context 'when choosing to sign in' do
+        before do
+          @user = create(:user, :signed_up)
+          click_link t('links.sign_in')
+        end
+
+        context 'with valid credentials entered' do
           before do
-            click_link t('links.passwords.forgot')
+            fill_in_credentials_and_submit(@user.email, @user.password)
           end
 
-          it 'prompts for my email address' do
+          it 'prompts for 2FA delivery method' do
             screenshot_and_save_page
           end
 
-          context 'with not_a_real_email_dot.com submitted' do
+          context 'with SMS OTP selected (default)' do
+            it 'prompts for OTP verification' do
+              screenshot_and_save_page
+            end
+
+            context 'with valid OTP confirmation' do
+              before do
+                fill_in 'code', with: @user.reload.direct_otp
+                click_button t('forms.buttons.submit.default')
+              end
+
+              # Skipping since we have nothing to show: this occurs on the SP
+              xit 'redirects back to SP' do
+                screenshot_and_save_page
+              end
+            end
+          end
+        end
+
+        context 'without a valid username and password' do
+          context 'when choosing "Forgot your password?"' do
             before do
-              fill_in 'password_reset_email_form_email', with: 'not_a_real_email_dot.com'
-              click_button t('forms.buttons.continue')
+              click_link t('links.passwords.forgot')
             end
 
-            it 'displays a useful error' do
+            it 'prompts for my email address' do
               screenshot_and_save_page
             end
+
+            context 'with not_a_real_email_dot.com submitted' do
+              before do
+                fill_in 'password_reset_email_form_email', with: 'not_a_real_email_dot.com'
+                click_button t('forms.buttons.continue')
+              end
+
+              it 'displays a useful error' do
+                screenshot_and_save_page
+              end
+            end
           end
         end
       end
diff --git a/spec/features/flows/visitor_flows_spec.rb b/spec/features/flows/visitor_flows_spec.rb
index 913e836d63b..089cd9c0f61 100644
--- a/spec/features/flows/visitor_flows_spec.rb
+++ b/spec/features/flows/visitor_flows_spec.rb
@@ -129,4 +129,59 @@
       end
     end
   end
+
+  context 'when choosing \'Forgot your password?' do
+    before do
+      visit new_user_password_path
+    end
+
+    it 'prompts for email address' do
+      screenshot_and_save_page
+    end
+
+    context 'when submitting email for an existing account' do
+      before do
+        @user = create(:user, :signed_up)
+        fill_in 'Email', with: @user.email
+        click_button t('forms.buttons.continue')
+      end
+
+      it 'informs the user to check their email' do
+        screenshot_and_save_page
+      end
+
+      context 'when following link in email', email: true do
+        before do
+          open_last_email
+          click_email_link_matching(/reset_password_token/)
+        end
+
+        it 'prompts the user to enter a new password' do
+          screenshot_and_save_page
+        end
+
+        context 'when submitting a valid password' do
+          before do
+            fill_in t('forms.passwords.edit.labels.password'), with: 'NewVal!dPassw0rd'
+            click_button t('forms.passwords.edit.buttons.submit')
+          end
+
+          it 'redirects to the homepage with a helpful message' do
+            screenshot_and_save_page
+          end
+        end
+      end
+    end
+
+    context 'when submitting email not associated with an account' do
+      before do
+        fill_in 'Email', with: 'non-existent-email@example.com'
+        click_button t('forms.buttons.continue')
+      end
+
+      it 'informs the user to check their email' do
+        screenshot_and_save_page
+      end
+    end
+  end
 end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 72a6ca81653..50dce1ca0bd 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -3,6 +3,7 @@
   SimpleCov.start 'rails' do
     add_filter '/config/'
     add_filter '/lib/rspec/formatters/user_flow_formatter.rb'
+    add_filter '/lib/user_flow_exporter.rb'
   end
 end