Browse Source

Fix sending reset email for admins (#313)

* Fix sending reset email for admins

* Reset does not let you bypass 2FA

* Trigger SMS on admin password reset

* Update flash message for 2FA

* Update Admin Login header

* Rubocop

* 422

* Revert unused view
pull/318/head
baarkerlounger 3 years ago committed by GitHub
parent
commit
bbae370bda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 45
      app/controllers/auth/passwords_controller.rb
  2. 20
      app/controllers/auth/sessions_controller.rb
  3. 5
      app/controllers/users_controller.rb
  4. 13
      app/mailers/devise_notify_mailer.rb
  5. 9
      app/models/admin_user.rb
  6. 7
      app/models/user.rb
  7. 2
      app/views/devise/passwords/edit.html.erb
  8. 33
      app/views/devise/passwords/reset_password.html.erb
  9. 8
      app/views/devise/sessions/new.html.erb
  10. 1
      config/locales/devise.en.yml
  11. 6
      config/routes.rb
  12. 13
      db/migrate/20220216163601_add_trackable_to_admin_user.rb
  13. 6
      db/schema.rb
  14. 72
      spec/features/admin_panel_spec.rb
  15. 5
      spec/features/organisation_spec.rb
  16. 0
      spec/features/reset_password.html.erb
  17. 6
      spec/features/user_spec.rb
  18. 2
      spec/request_helper.rb
  19. 159
      spec/requests/auth/passwords_controller_spec.rb

45
app/controllers/auth/passwords_controller.rb

@ -24,12 +24,53 @@ class Auth::PasswordsController < Devise::PasswordsController
def edit def edit
super super
render "users/reset_password" render "devise/passwords/reset_password"
end
def update
self.resource = resource_class.reset_password_by_token(resource_params)
yield resource if block_given?
if resource.errors.empty?
resource.unlock_access! if unlockable?(resource)
if Devise.sign_in_after_reset_password
set_flash_message!(:notice, password_update_flash_message)
resource.after_database_authentication
sign_in(resource_name, resource)
else
set_flash_message!(:notice, :updated_not_active)
end
respond_with resource, location: after_resetting_password_path_for(resource)
else
set_minimum_password_length
respond_with resource, status: :unprocessable_entity
end
end end
protected protected
def password_update_flash_message
resource_class == AdminUser ? :updated_2FA : :updated
end
def resource_class_name
resource_class.name.underscore
end
def after_sending_reset_password_instructions_path_for(_resource) def after_sending_reset_password_instructions_path_for(_resource)
confirmations_reset_path(email: params.dig("user", "email")) confirmations_reset_path(email: params.dig(resource_class_name, "email"))
end
def after_resetting_password_path_for(resource)
if Devise.sign_in_after_reset_password
if resource_class == AdminUser
resource.send_new_otp
admin_user_two_factor_authentication_path
else
after_sign_in_path_for(resource)
end
else
new_session_path(resource_name)
end
end end
end end

20
app/controllers/auth/sessions_controller.rb

@ -3,12 +3,12 @@ class Auth::SessionsController < Devise::SessionsController
def create def create
self.resource = resource_class.new self.resource = resource_class.new
if params.dig("user", "email").empty? if params.dig(resource_class_name, "email").empty?
resource.errors.add :email, "Enter an email address" resource.errors.add :email, "Enter an email address"
elsif !email_valid?(params.dig("user", "email")) elsif !email_valid?(params.dig(resource_class_name, "email"))
resource.errors.add :email, "Enter an email address in the correct format, like name@example.com" resource.errors.add :email, "Enter an email address in the correct format, like name@example.com"
end end
if params.dig("user", "password").empty? if params.dig(resource_class_name, "password").empty?
resource.errors.add :password, "Enter a password" resource.errors.add :password, "Enter a password"
end end
if resource.errors.present? if resource.errors.present?
@ -20,7 +20,19 @@ class Auth::SessionsController < Devise::SessionsController
private private
def resource_class
request.path.include?("admin") ? AdminUser : User
end
def resource_class_name
resource_class.name.underscore
end
def after_sign_in_path_for(resource) def after_sign_in_path_for(resource)
params.dig("user", "start").present? ? case_logs_path : super if resource_class == AdminUser
admin_user_two_factor_authentication_path
else
params.dig(resource_class_name, "start").present? ? case_logs_path : super
end
end end
end end

5
app/controllers/users_controller.rb

@ -12,7 +12,8 @@ class UsersController < ApplicationController
redirect_to user_path(@user) redirect_to user_path(@user)
elsif user_params.key?("password") elsif user_params.key?("password")
format_error_messages format_error_messages
render "devise/passwords/edit", status: :unprocessable_entity @minimum_password_length = User.password_length.min
render "devise/passwords/edit", locals: { resource: @user, resource_name: "user" }, status: :unprocessable_entity
else else
format_error_messages format_error_messages
render :edit, status: :unprocessable_entity render :edit, status: :unprocessable_entity
@ -41,7 +42,7 @@ class UsersController < ApplicationController
def edit_password def edit_password
@minimum_password_length = User.password_length.min @minimum_password_length = User.password_length.min
render "devise/passwords/edit" render "devise/passwords/edit", locals: { resource: @user, resource_name: "user" }
end end
private private

13
app/mailers/devise_notify_mailer.rb

@ -1,9 +1,6 @@
class DeviseNotifyMailer < Devise::Mailer class DeviseNotifyMailer < Devise::Mailer
require "notifications/client" require "notifications/client"
RESET_PASSWORD_TEMPLATE_ID = "2c410c19-80a7-481c-a531-2bcb3264f8e6".freeze
SET_PASSWORD_TEMPLATE_ID = "257460a6-6616-4640-a3f9-17c3d73d9e91".freeze
def notify_client def notify_client
@notify_client ||= ::Notifications::Client.new(ENV["GOVUK_NOTIFY_API_KEY"]) @notify_client ||= ::Notifications::Client.new(ENV["GOVUK_NOTIFY_API_KEY"])
end end
@ -21,14 +18,14 @@ class DeviseNotifyMailer < Devise::Mailer
end end
def reset_password_instructions(record, token, _opts = {}) def reset_password_instructions(record, token, _opts = {})
template_id = record.last_sign_in_at ? RESET_PASSWORD_TEMPLATE_ID : SET_PASSWORD_TEMPLATE_ID url = public_send("edit_#{record.class.name.underscore}_password_url")
personalisation = { personalisation = {
name: record.name, name: record.name || record.email,
email: record.email, email: record.email,
organisation: record.organisation.name, organisation: record.respond_to?(:organisation) ? record.organisation.name : "",
link: "https://#{host}/users/password/edit?reset_password_token=#{token}", link: "#{url}?reset_password_token=#{token}",
} }
send_email(record.email, template_id, personalisation) send_email(record.email, record.reset_password_notify_template, personalisation)
end end
# def confirmation_instructions(record, token, _opts = {}) # def confirmation_instructions(record, token, _opts = {})

9
app/models/admin_user.rb

@ -1,8 +1,8 @@
class AdminUser < ApplicationRecord class AdminUser < ApplicationRecord
# Include default devise modules. Others available are: # Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable # :confirmable, :lockable, :timeoutable, :omniauthable
devise :two_factor_authenticatable, :database_authenticatable, :recoverable, devise :two_factor_authenticatable, :database_authenticatable, :recoverable,
:rememberable, :validatable :rememberable, :validatable, :trackable
has_one_time_password(encrypted: true) has_one_time_password(encrypted: true)
@ -11,10 +11,15 @@ class AdminUser < ApplicationRecord
validates :phone, presence: true, numericality: true validates :phone, presence: true, numericality: true
MFA_SMS_TEMPLATE_ID = "bf309d93-804e-4f95-b1f4-bd513c48ecb0".freeze MFA_SMS_TEMPLATE_ID = "bf309d93-804e-4f95-b1f4-bd513c48ecb0".freeze
RESET_PASSWORD_TEMPLATE_ID = "fbb2d415-b9b1-4507-ba0a-6e542fa3504d".freeze
def send_two_factor_authentication_code(code) def send_two_factor_authentication_code(code)
template_id = MFA_SMS_TEMPLATE_ID template_id = MFA_SMS_TEMPLATE_ID
personalisation = { otp: code } personalisation = { otp: code }
Sms.send(phone, template_id, personalisation) Sms.send(phone, template_id, personalisation)
end end
def reset_password_notify_template
RESET_PASSWORD_TEMPLATE_ID
end
end end

7
app/models/user.rb

@ -25,4 +25,11 @@ class User < ApplicationRecord
def not_completed_case_logs def not_completed_case_logs
case_logs.not_completed case_logs.not_completed
end end
RESET_PASSWORD_TEMPLATE_ID = "2c410c19-80a7-481c-a531-2bcb3264f8e6".freeze
SET_PASSWORD_TEMPLATE_ID = "257460a6-6616-4640-a3f9-17c3d73d9e91".freeze
def reset_password_notify_template
last_sign_in_at ? RESET_PASSWORD_TEMPLATE_ID : SET_PASSWORD_TEMPLATE_ID
end
end end

2
app/views/devise/passwords/edit.html.erb

@ -7,7 +7,7 @@
) %> ) %>
<% end %> <% end %>
<%= form_for(@user, as: :user, html: { method: :patch }) do |f| %> <%= form_for(resource, as: resource_name, html: { method: :patch }) do |f| %>
<div class="govuk-grid-row"> <div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds"> <div class="govuk-grid-column-two-thirds">
<%= f.govuk_error_summary(presenter: ErrorSummaryFullMessagesPresenter) %> <%= f.govuk_error_summary(presenter: ErrorSummaryFullMessagesPresenter) %>

33
app/views/devise/passwords/reset_password.html.erb

@ -0,0 +1,33 @@
<% content_for :title, "Reset your password" %>
<% content_for :before_content do %>
<%= govuk_back_link(
text: 'Back',
href: :back,
) %>
<% end %>
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %>
<%= f.hidden_field :reset_password_token %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<%= f.govuk_error_summary %>
<h1 class="govuk-heading-l">
<%= content_for(:title) %>
</h1>
<%= f.govuk_password_field :password,
label: { text: "New password" },
hint: @minimum_password_length ? { text: "Your password must be at least #{@minimum_password_length} characters and hard to guess." } : nil,
autocomplete: "new-password"
%>
<%= f.govuk_password_field :password_confirmation,
label: { text: "Confirm new password" }
%>
<%= f.govuk_submit "Update" %>
</div>
</div>
<% end %>

8
app/views/devise/sessions/new.html.erb

@ -1,4 +1,10 @@
<% content_for :title, "Sign in to your account to submit CORE data" %> <% if resource_name == :admin_user %>
<% title = "Sign in to your CORE administration account" %>
<% else %>
<% title = "Sign in to your account to submit CORE data" %>
<% end %>
<% content_for :title, title %>
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
<div class="govuk-grid-row"> <div class="govuk-grid-row">

1
config/locales/devise.en.yml

@ -35,6 +35,7 @@ en:
send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."
send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
updated: "Your password has been changed successfully. You are now signed in." updated: "Your password has been changed successfully. You are now signed in."
updated_2FA: "Your password has been changed successfully. Your security code has been sent."
updated_not_active: "Your password has been changed successfully." updated_not_active: "Your password has been changed successfully."
registrations: registrations:
destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon."

6
config/routes.rb

@ -2,14 +2,14 @@ Rails.application.routes.draw do
devise_for :admin_users, { devise_for :admin_users, {
path: :admin, path: :admin,
controllers: { controllers: {
sessions: "active_admin/devise/sessions", sessions: "auth/sessions",
passwords: "active_admin/devise/passwords", passwords: "auth/passwords",
unlocks: "active_admin/devise/unlocks", unlocks: "active_admin/devise/unlocks",
registrations: "active_admin/devise/registrations", registrations: "active_admin/devise/registrations",
confirmations: "active_admin/devise/confirmations", confirmations: "active_admin/devise/confirmations",
two_factor_authentication: "auth/two_factor_authentication", two_factor_authentication: "auth/two_factor_authentication",
}, },
path_names: { sign_in: "login", sign_out: "logout", two_factor_authentication: "two-factor-authentication" }, path_names: { sign_in: "sign-in", sign_out: "sign-out", two_factor_authentication: "two-factor-authentication" },
sign_out_via: %i[delete get], sign_out_via: %i[delete get],
} }

13
db/migrate/20220216163601_add_trackable_to_admin_user.rb

@ -0,0 +1,13 @@
class AddTrackableToAdminUser < ActiveRecord::Migration[7.0]
def change
change_table :admin_users, bulk: true do |t|
t.string :name
## Trackable
t.integer :sign_in_count, default: 0, null: false
t.datetime :current_sign_in_at
t.datetime :last_sign_in_at
t.string :current_sign_in_ip
t.string :last_sign_in_ip
end
end
end

6
db/schema.rb

@ -30,6 +30,12 @@ ActiveRecord::Schema[7.0].define(version: 202202071123100) do
t.datetime "direct_otp_sent_at", precision: nil t.datetime "direct_otp_sent_at", precision: nil
t.datetime "totp_timestamp", precision: nil t.datetime "totp_timestamp", precision: nil
t.string "phone" t.string "phone"
t.string "name"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at", precision: nil
t.datetime "last_sign_in_at", precision: nil
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.index ["encrypted_otp_secret_key"], name: "index_admin_users_on_encrypted_otp_secret_key", unique: true t.index ["encrypted_otp_secret_key"], name: "index_admin_users_on_encrypted_otp_secret_key", unique: true
end end

72
spec/features/admin_panel_spec.rb

@ -11,6 +11,12 @@ RSpec.describe "Admin Panel" do
allow(notify_client).to receive(:send_sms).and_return(true) allow(notify_client).to receive(:send_sms).and_return(true)
end end
it "shows the admin sign in page" do
visit("/admin")
expect(page).to have_current_path("/admin/sign-in")
expect(page).to have_content("Sign in to your CORE administration account")
end
context "with a valid 2FA code" do context "with a valid 2FA code" do
before do before do
allow(SecureRandom).to receive(:random_number).and_return(otp) allow(SecureRandom).to receive(:random_number).and_return(otp)
@ -23,7 +29,7 @@ RSpec.describe "Admin Panel" do
expect(notify_client).to receive(:send_sms).with( expect(notify_client).to receive(:send_sms).with(
hash_including(phone_number: admin.phone, template_id: mfa_template_id), hash_including(phone_number: admin.phone, template_id: mfa_template_id),
) )
click_button("Login") click_button("Sign in")
fill_in("code", with: otp) fill_in("code", with: otp)
click_button("Submit") click_button("Submit")
expect(page).to have_content("Dashboard") expect(page).to have_content("Dashboard")
@ -32,7 +38,7 @@ RSpec.describe "Admin Panel" do
context "but it is more than 15 minutes old" do context "but it is more than 15 minutes old" do
it "does not authenticate successfully" do it "does not authenticate successfully" do
click_button("Login") click_button("Sign in")
admin.update!(direct_otp_sent_at: 16.minutes.ago) admin.update!(direct_otp_sent_at: 16.minutes.ago)
fill_in("code", with: otp) fill_in("code", with: otp)
click_button("Submit") click_button("Submit")
@ -49,7 +55,7 @@ RSpec.describe "Admin Panel" do
visit("/admin") visit("/admin")
fill_in("admin_user[email]", with: admin.email) fill_in("admin_user[email]", with: admin.email)
fill_in("admin_user[password]", with: admin.password) fill_in("admin_user[password]", with: admin.password)
click_button("Login") click_button("Sign in")
fill_in("code", with: otp) fill_in("code", with: otp)
click_button("Submit") click_button("Submit")
expect(page).to have_content("Check your phone") expect(page).to have_content("Check your phone")
@ -64,7 +70,7 @@ RSpec.describe "Admin Panel" do
visit("/admin") visit("/admin")
fill_in("admin_user[email]", with: admin.email) fill_in("admin_user[email]", with: admin.email)
fill_in("admin_user[password]", with: admin.password) fill_in("admin_user[password]", with: admin.password)
click_button("Login") click_button("Sign in")
end end
it "displays the resend view" do it "displays the resend view" do
@ -88,14 +94,68 @@ RSpec.describe "Admin Panel" do
visit("/admin") visit("/admin")
fill_in("admin_user[email]", with: admin.email) fill_in("admin_user[email]", with: admin.email)
fill_in("admin_user[password]", with: admin.password) fill_in("admin_user[password]", with: admin.password)
click_button("Login") click_button("Sign in")
fill_in("code", with: otp) fill_in("code", with: otp)
click_button("Submit") click_button("Submit")
click_link("Logout") click_link("Logout")
visit("/admin")
fill_in("admin_user[email]", with: admin.email) fill_in("admin_user[email]", with: admin.email)
fill_in("admin_user[password]", with: admin.password) fill_in("admin_user[password]", with: admin.password)
click_button("Login") click_button("Sign in")
expect(page).to have_content("Check your phone") expect(page).to have_content("Check your phone")
end end
end end
context "when the admin has forgotten their password" do
let!(:admin_user) { FactoryBot.create(:admin_user, last_sign_in_at: Time.zone.now) }
let(:notify_client) { instance_double(Notifications::Client) }
let(:reset_password_token) { "MCDH5y6Km-U7CFPgAMVS" }
let(:devise_notify_mailer) { DeviseNotifyMailer.new }
before do
allow(DeviseNotifyMailer).to receive(:new).and_return(devise_notify_mailer)
allow(devise_notify_mailer).to receive(:notify_client).and_return(notify_client)
allow(notify_client).to receive(:send_email).and_return(true)
allow(Devise.token_generator).to receive(:generate).and_return(reset_password_token)
end
it " is redirected to the reset password page when they click the reset password link" do
visit("/admin")
click_link("reset your password")
expect(page).to have_current_path("/admin/password/new")
end
it " is shown an error message if they submit without entering an email address" do
visit("/admin/password/new")
click_button("Send email")
expect(page).to have_selector("#error-summary-title")
expect(page).to have_selector("#user-email-field-error")
expect(page).to have_title("Error")
end
it " is redirected to admin login page after reset email is sent" do
visit("/admin/password/new")
fill_in("admin_user[email]", with: admin_user.email)
click_button("Send email")
expect(page).to have_content("Check your email")
end
it " is sent a reset password email via Notify" do
expect(notify_client).to receive(:send_email).with(
{
email_address: admin_user.email,
template_id: admin_user.reset_password_notify_template,
personalisation: {
name: admin_user.email,
email: admin_user.email,
organisation: "",
link: "http://localhost:3000/admin/password/edit?reset_password_token=#{reset_password_token}",
},
},
)
visit("/admin/password/new")
fill_in("admin_user[email]", with: admin_user.email)
click_button("Send email")
end
end
end end

5
spec/features/organisation_spec.rb

@ -5,7 +5,7 @@ RSpec.describe "User Features" do
include Helpers include Helpers
let(:organisation) { user.organisation } let(:organisation) { user.organisation }
let(:org_id) { organisation.id } let(:org_id) { organisation.id }
let(:set_password_template_id) { DeviseNotifyMailer::SET_PASSWORD_TEMPLATE_ID } let(:set_password_template_id) { User::SET_PASSWORD_TEMPLATE_ID }
let(:notify_client) { instance_double(Notifications::Client) } let(:notify_client) { instance_double(Notifications::Client) }
let(:reset_password_token) { "MCDH5y6Km-U7CFPgAMVS" } let(:reset_password_token) { "MCDH5y6Km-U7CFPgAMVS" }
let(:devise_notify_mailer) { DeviseNotifyMailer.new } let(:devise_notify_mailer) { DeviseNotifyMailer.new }
@ -13,7 +13,6 @@ RSpec.describe "User Features" do
before do before do
allow(DeviseNotifyMailer).to receive(:new).and_return(devise_notify_mailer) allow(DeviseNotifyMailer).to receive(:new).and_return(devise_notify_mailer)
allow(devise_notify_mailer).to receive(:notify_client).and_return(notify_client) allow(devise_notify_mailer).to receive(:notify_client).and_return(notify_client)
allow(devise_notify_mailer).to receive(:host).and_return("test.com")
allow(Devise.token_generator).to receive(:generate).and_return(reset_password_token) allow(Devise.token_generator).to receive(:generate).and_return(reset_password_token)
allow(notify_client).to receive(:send_email).and_return(true) allow(notify_client).to receive(:send_email).and_return(true)
sign_in user sign_in user
@ -56,7 +55,7 @@ RSpec.describe "User Features" do
name: "New User", name: "New User",
email: "new_user@example.com", email: "new_user@example.com",
organisation: organisation.name, organisation: organisation.name,
link: "https://test.com/users/password/edit?reset_password_token=#{reset_password_token}", link: "http://localhost:3000/users/password/edit?reset_password_token=#{reset_password_token}",
}, },
}, },
) )

0
app/views/users/reset_password.html.erb → spec/features/reset_password.html.erb

6
spec/features/user_spec.rb

@ -2,7 +2,7 @@ require "rails_helper"
RSpec.describe "User Features" do RSpec.describe "User Features" do
let!(:user) { FactoryBot.create(:user, last_sign_in_at: Time.zone.now) } let!(:user) { FactoryBot.create(:user, last_sign_in_at: Time.zone.now) }
let(:reset_password_template_id) { DeviseNotifyMailer::RESET_PASSWORD_TEMPLATE_ID } let(:reset_password_template_id) { User::RESET_PASSWORD_TEMPLATE_ID }
let(:notify_client) { instance_double(Notifications::Client) } let(:notify_client) { instance_double(Notifications::Client) }
let(:reset_password_token) { "MCDH5y6Km-U7CFPgAMVS" } let(:reset_password_token) { "MCDH5y6Km-U7CFPgAMVS" }
let(:devise_notify_mailer) { DeviseNotifyMailer.new } let(:devise_notify_mailer) { DeviseNotifyMailer.new }
@ -10,7 +10,6 @@ RSpec.describe "User Features" do
before do before do
allow(DeviseNotifyMailer).to receive(:new).and_return(devise_notify_mailer) allow(DeviseNotifyMailer).to receive(:new).and_return(devise_notify_mailer)
allow(devise_notify_mailer).to receive(:notify_client).and_return(notify_client) allow(devise_notify_mailer).to receive(:notify_client).and_return(notify_client)
allow(devise_notify_mailer).to receive(:host).and_return("test.com")
allow(notify_client).to receive(:send_email).and_return(true) allow(notify_client).to receive(:send_email).and_return(true)
allow(Devise.token_generator).to receive(:generate).and_return(reset_password_token) allow(Devise.token_generator).to receive(:generate).and_return(reset_password_token)
end end
@ -19,6 +18,7 @@ RSpec.describe "User Features" do
it " is required to log in" do it " is required to log in" do
visit("/logs") visit("/logs")
expect(page).to have_current_path("/users/sign-in") expect(page).to have_current_path("/users/sign-in")
expect(page).to have_content("Sign in to your account to submit CORE data")
end end
it "does not see the default devise error message" do it "does not see the default devise error message" do
@ -109,7 +109,7 @@ RSpec.describe "User Features" do
name: user.name, name: user.name,
email: user.email, email: user.email,
organisation: user.organisation.name, organisation: user.organisation.name,
link: "https://test.com/users/password/edit?reset_password_token=#{reset_password_token}", link: "http://localhost:3000/users/password/edit?reset_password_token=#{reset_password_token}",
}, },
}, },
) )

2
spec/request_helper.rb

@ -7,6 +7,8 @@ module RequestHelper
.to_return(status: 200, body: "{\"status\":404,\"error\":\"Postcode not found\"}", headers: {}) .to_return(status: 200, body: "{\"status\":404,\"error\":\"Postcode not found\"}", headers: {})
WebMock.stub_request(:post, /api.notifications.service.gov.uk\/v2\/notifications\/email/) WebMock.stub_request(:post, /api.notifications.service.gov.uk\/v2\/notifications\/email/)
.to_return(status: 200, body: "", headers: {}) .to_return(status: 200, body: "", headers: {})
WebMock.stub_request(:post, /api.notifications.service.gov.uk\/v2\/notifications\/sms/)
.to_return(status: 200, body: "", headers: {})
end end
def self.real_http_requests def self.real_http_requests

159
spec/requests/auth/passwords_controller_spec.rb

@ -2,7 +2,6 @@ require "rails_helper"
require_relative "../../support/devise" require_relative "../../support/devise"
RSpec.describe Auth::PasswordsController, type: :request do RSpec.describe Auth::PasswordsController, type: :request do
let(:params) { { user: { email: } } }
let(:page) { Capybara::Node::Simple.new(response.body) } let(:page) { Capybara::Node::Simple.new(response.body) }
let(:notify_client) { instance_double(Notifications::Client) } let(:notify_client) { instance_double(Notifications::Client) }
let(:devise_notify_mailer) { DeviseNotifyMailer.new } let(:devise_notify_mailer) { DeviseNotifyMailer.new }
@ -13,60 +12,134 @@ RSpec.describe Auth::PasswordsController, type: :request do
allow(notify_client).to receive(:send_email).and_return(true) allow(notify_client).to receive(:send_email).and_return(true)
end end
context "when a password reset is requested for a valid email" do context "when a regular user" do
let(:user) { FactoryBot.create(:user) } let(:params) { { user: { email: } } }
let(:email) { user.email }
it "redirects to the email sent page" do context "when a password reset is requested for a valid email" do
post "/users/password", params: params let(:user) { FactoryBot.create(:user) }
expect(response).to have_http_status(:redirect) let(:email) { user.email }
follow_redirect!
expect(response.body).to match(/Check your email/) it "redirects to the email sent page" do
post "/users/password", params: params
expect(response).to have_http_status(:redirect)
follow_redirect!
expect(response.body).to match(/Check your email/)
end
end end
end
context "when a password reset is requested with an email that doesn't exist in the system" do context "when a password reset is requested with an email that doesn't exist in the system" do
before do before do
allow(Devise.navigational_formats).to receive(:include?).and_return(false) allow(Devise.navigational_formats).to receive(:include?).and_return(false)
end
let(:email) { "madeup_email@test.com" }
it "redirects to the email sent page anyway" do
post "/users/password", params: params
expect(response).to have_http_status(:redirect)
follow_redirect!
expect(response.body).to match(/Check your email/)
end
end end
let(:email) { "madeup_email@test.com" } describe "#Update - reset password" do
let(:user) { FactoryBot.create(:user) }
let(:token) { user.send(:set_reset_password_token) }
let(:updated_password) { "updated_password_280" }
let(:update_password_params) do
{
user:
{
reset_password_token: token,
password: updated_password,
password_confirmation: updated_password,
},
}
end
let(:message) { "Your password has been changed successfully. You are now signed in" }
it "redirects to the email sent page anyway" do it "changes the password" do
post "/users/password", params: params expect { put "/users/password", params: update_password_params }
expect(response).to have_http_status(:redirect) .to(change { user.reload.encrypted_password })
follow_redirect! end
expect(response.body).to match(/Check your email/)
it "after password change, the user is signed in" do
put "/users/password", params: update_password_params
# Devise redirects once after re-sign in with new password and then root redirects as well.
follow_redirect!
follow_redirect!
expect(page).to have_css("div", class: "govuk-notification-banner__heading", text: message)
end
end end
end end
describe "#Update - reset password" do context "when an admin user" do
let(:user) { FactoryBot.create(:user) } let(:admin_user) { FactoryBot.create(:admin_user) }
let(:token) { user.send(:set_reset_password_token) }
let(:updated_password) { "updated_password_280" } describe "reset password" do
let(:update_password_params) do let(:new_value) { "new-password" }
{
user: before do
allow(Sms).to receive(:notify_client).and_return(notify_client)
allow(notify_client).to receive(:send_sms).and_return(true)
end
it "renders the user edit password view" do
_raw, enc = Devise.token_generator.generate(AdminUser, :reset_password_token)
get "/admin/password/edit?reset_password_token=#{enc}"
expect(page).to have_css("h1", text: "Reset your password")
end
context "when passwords entered don't match" do
let(:raw) { admin_user.send_reset_password_instructions }
let(:params) do
{ {
reset_password_token: token, id: admin_user.id,
password: updated_password, admin_user: {
password_confirmation: updated_password, password: new_value,
}, password_confirmation: "something_else",
} reset_password_token: raw,
end },
let(:message) { "Your password has been changed successfully. You are now signed in" } }
end
it "changes the password" do it "shows an error" do
expect { put "/users/password", params: update_password_params } put "/admin/password", headers: headers, params: params
.to(change { user.reload.encrypted_password }) expect(response).to have_http_status(:unprocessable_entity)
end expect(page).to have_content("doesn't match Password")
end
end
context "when passwords is reset" do
let(:raw) { admin_user.send_reset_password_instructions }
let(:params) do
{
id: admin_user.id,
admin_user: {
password: new_value,
password_confirmation: new_value,
reset_password_token: raw,
},
}
end
it "updates the password" do
expect {
put "/admin/password", headers: headers, params: params
admin_user.reload
}.to change(admin_user, :encrypted_password)
end
it "sends you to the 2FA page" do
put "/admin/password", headers: headers, params: params
expect(response).to redirect_to("/admin/two-factor-authentication")
end
it "after password change, the user is signed in" do it "triggers an SMS" do
put "/users/password", params: update_password_params expect(notify_client).to receive(:send_sms)
# Devise redirects once after re-sign in with new password and then root redirects as well. put "/admin/password", headers: headers, params: params
follow_redirect! end
follow_redirect! end
expect(page).to have_css("div", class: "govuk-notification-banner__heading", text: message)
end end
end end
end end

Loading…
Cancel
Save