diff --git a/Gemfile b/Gemfile index 5868b4d4c5..45fbe98b7c 100644 --- a/Gemfile +++ b/Gemfile @@ -64,6 +64,8 @@ gem "devise-authy", ">= 1.10.0" gem "pundit", "~> 0.3" gem "devise_zxcvbn", ">= 4.4.1" gem "devise-security", "~> 0.18.0" +gem "omniauth-rails_csrf_protection" +gem "omniauth-oauth2" # GOV.UK Notify support (for mailers) gem "mail-notify", "~> 2.0" diff --git a/Gemfile.lock b/Gemfile.lock index 0eefc5ca02..a4de23a04f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -352,6 +352,8 @@ GEM money (6.16.0) i18n (>= 0.6.4, <= 2) multi_json (1.15.0) + multi_xml (0.7.1) + bigdecimal (~> 3.1) multipart-post (2.3.0) mustermann (3.0.0) ruby2_keywords (~> 0.0.1) @@ -379,6 +381,21 @@ GEM racc (~> 1.4) notifications-ruby-client (6.0.0) jwt (>= 1.5, < 3) + oauth2 (1.4.11) + faraday (>= 0.17.3, < 3.0) + jwt (>= 1.0, < 3.0) + multi_json (~> 1.3) + multi_xml (~> 0.5) + rack (>= 1.2, < 4) + omniauth (1.9.2) + hashie (>= 3.4.6) + rack (>= 1.6.2, < 3) + omniauth-oauth2 (1.3.1) + oauth2 (~> 1.0) + omniauth (~> 1.2) + omniauth-rails_csrf_protection (0.1.2) + actionpack (>= 4.2) + omniauth (>= 1.3.1) orm_adapter (0.5.0) pagy (7.0.11) paper_trail (12.2.0) @@ -775,6 +792,8 @@ DEPENDENCIES matrix nilify_blanks nokogiri + omniauth-oauth2 + omniauth-rails_csrf_protection paper_trail (~> 12.2.0) paper_trail-association_tracking pdf-inspector diff --git a/app/controllers/admin/admins_controller.rb b/app/controllers/admin/admins_controller.rb index 10cadf06f0..d1785e8ddd 100644 --- a/app/controllers/admin/admins_controller.rb +++ b/app/controllers/admin/admins_controller.rb @@ -1,5 +1,6 @@ -class Admin::AdminsController < Admin::UsersController - before_action :find_resource, except: [:index, :new, :create, :login_as_assessor, :login_as_user] +class Admin::AdminsController < Admin::BaseController + before_action :find_resource, except: [:index, :login_as_assessor, :login_as_user] + def index params[:search] ||= AdminSearch::DEFAULT_SEARCH params[:search].permit! @@ -7,48 +8,7 @@ def index @search = AdminSearch.new(Admin.all) .search(params[:search]) @resources = @search.results.page(params[:page]) - end - - def new - @resource = Admin.new - authorize @resource, :create? - end - - def create - @resource = Admin.new(resource_params) - @resource.skip_password_validation = true - authorize @resource, :create? - - @resource.save - - render_flash_message_for(@resource, message: @resource.errors.none? ? nil : @resource.errors.messages.values.flatten.uniq.join("
")) - - location = @resource.persisted? ? admin_admins_path : nil - respond_with :admin, @resource, location: location - end - - def update - authorize @resource, :update? - - if resource_params[:password].present? - @resource.update(resource_params) - else - @resource.skip_password_validation = true - @resource.update_without_password(resource_params) - end - - render_flash_message_for(@resource, message: @resource.errors.none? ? nil : @resource.errors.messages.values.flatten.uniq.join("
")) - - respond_with :admin, @resource, location: admin_admins_path - end - - def destroy - authorize @resource, :destroy? - @resource.soft_delete! - - render_flash_message_for(@resource, message: @resource.errors.none? ? nil : @resource.errors.messages.values.flatten.uniq.join("
")) - - respond_with :admin, @resource, location: admin_admins_path + render template: "admin/users/index" end # NOTE: debug abilities for Admin - BEGIN @@ -74,13 +34,4 @@ def login_as_user def find_resource @resource = Admin.find(params[:id]) end - - def resource_params - params.require(:admin) - .permit(:email, - :password, - :password_confirmation, - :first_name, - :last_name) - end end diff --git a/app/controllers/admins/confirmations_controller.rb b/app/controllers/admins/confirmations_controller.rb index c54d0ad7db..f60c74d866 100644 --- a/app/controllers/admins/confirmations_controller.rb +++ b/app/controllers/admins/confirmations_controller.rb @@ -1,9 +1,2 @@ class Admins::ConfirmationsController < Devise::ConfirmationsController - include PasswordSettable - - private - - def password_reset_path(token) - edit_admin_password_path(reset_password_token: token, locals: { password_not_set: true }) - end end diff --git a/app/controllers/admins/omniauth_callbacks_controller.rb b/app/controllers/admins/omniauth_callbacks_controller.rb new file mode 100644 index 0000000000..26eaa26e2b --- /dev/null +++ b/app/controllers/admins/omniauth_callbacks_controller.rb @@ -0,0 +1,33 @@ +class Admins::OmniauthCallbacksController < Devise::OmniauthCallbacksController + skip_before_action :verify_authenticity_token, only: :developer + + def dbt_staff_sso + FindAndUpdateWithAuth.new(auth: request.env["omniauth.auth"], model: Admin).run.tap do |result| + if result.success + sign_in_and_redirect result.user, event: :authentication + set_flash_message(:notice, :success, kind: "DBT Staff SSO") + else + set_flash_message(:notice, :error, kind: result.errors) + redirect_to new_admin_session_url + end + end + end + + def developer + raise "developer strategy should only be used in development!" unless Rails.env.development? + + FindAndUpdateWithAuth.new(auth: request.env["omniauth.auth"], model: Admin).run.tap do |result| + if result.success + sign_in_and_redirect result.user, event: :authentication + set_flash_message(:notice, :success, kind: "Developer SSO") + else + set_flash_message(:notice, :error, kind: result.errors) + redirect_to new_admin_session_url + end + end + end + + def failure + redirect_to admin_root_path + end +end diff --git a/app/controllers/admins/sessions_controller.rb b/app/controllers/admins/sessions_controller.rb new file mode 100644 index 0000000000..78ca0749de --- /dev/null +++ b/app/controllers/admins/sessions_controller.rb @@ -0,0 +1,2 @@ +class Admins::SessionsController < Devise::SessionsController +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 84a1737eb1..17f5ac0eba 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -31,6 +31,14 @@ def after_sign_in_path_for(resource) end end + def after_sign_out_path_for(scope) + if scope == :admin + new_admin_session_path + else + super + end + end + def admin_in_read_only_mode? @admin_in_read_only_mode ||= (admin_signed_in? || assessor_signed_in?) && session["warden.user.user.key"] && diff --git a/app/interactors/find_and_update_with_auth.rb b/app/interactors/find_and_update_with_auth.rb new file mode 100644 index 0000000000..137257c678 --- /dev/null +++ b/app/interactors/find_and_update_with_auth.rb @@ -0,0 +1,52 @@ +class FindAndUpdateWithAuth + attr_reader :success, :errors, :user + + def initialize(auth:, model:) + @model = model + @sso_uid = auth.uid + @sso_info = auth.info + @sso_provider = auth.provider + end + + def run + @user = find_or_create_user + @success = @user.persisted? + @errors = @user.errors unless @success + self + end + + private + + # 1 - check if they have already set up sso; when they have update them to ensure we capture the current email/first_name/last_name + # 2 - check if the user already existed prior to sso; when they do update them with the auth details + # 3 - finally if they dont exist; create them from the auth details + def find_or_create_user + find_and_update_with_auth(sso_provider: @sso_provider, sso_uid: @sso_uid) || find_and_update_with_auth(email: @sso_info.email) || create_from_auth + end + + def find_and_update_with_auth(conditions) + raise "conditions should not be nil: #{conditions}" if conditions.compact.empty? + + existing = @model.find_by(**conditions) + return unless existing + + @model.update( + existing.id, + sso_provider: @sso_provider, + sso_uid: @sso_uid, + email: @sso_info.email, + first_name: @sso_info.first_name, + last_name: @sso_info.last_name, + ) + end + + def create_from_auth + @model.create( + email: @sso_info.email, + first_name: @sso_info.first_name, + last_name: @sso_info.last_name, + sso_uid: @sso_uid, + sso_provider: @sso_provider, + ) + end +end diff --git a/app/models/admin.rb b/app/models/admin.rb index a5fd57acbf..f8471cb248 100644 --- a/app/models/admin.rb +++ b/app/models/admin.rb @@ -2,15 +2,11 @@ class Admin < ApplicationRecord include PgSearch::Model include AutosaveTokenGeneration - # Include default devise modules. Others available are: - # :confirmable, :lockable, :timeoutable and :omniauthable - - devise :authy_authenticatable, :database_authenticatable, - :recoverable, :trackable, :validatable, :confirmable, - :zxcvbnable, :lockable, :timeoutable, :session_limitable - include PasswordValidator + devise :trackable, :timeoutable, :session_limitable + devise :omniauthable, omniauth_providers: %i[dbt_staff_sso developer] validates :first_name, :last_name, presence: true + validates :email, format: { with: Devise.email_regexp } has_many :form_answer_attachments, as: :attachable diff --git a/app/views/admin/admins/_list.html.slim b/app/views/admin/admins/_list.html.slim index e192d772d9..f822f1c466 100644 --- a/app/views/admin/admins/_list.html.slim +++ b/app/views/admin/admins/_list.html.slim @@ -10,7 +10,7 @@ th.sortable = sort_link f, 'Signed in on', @search, :last_sign_in_at th.sortable - = sort_link f, "Confirmed on", @search, :confirmed_at + = sort_link f, 'Single sign on ID', @search, :sso_uid tbody - if AdminDecorator.decorate_collection(resources).none? tr @@ -30,10 +30,4 @@ = admin.formatted_last_sign_in_at_long span.hidden-lg = admin.formatted_last_sign_in_at_short - td - - if admin.confirmed_at.present? - small.text-muted - = l admin.confirmed_at, format: :date_at_time - - else - small.text-danger - ' Not confirmed + td = admin.sso_uid diff --git a/app/views/admin/users/index.html.slim b/app/views/admin/users/index.html.slim index 8bcb1c0213..18ee035cfa 100644 --- a/app/views/admin/users/index.html.slim +++ b/app/views/admin/users/index.html.slim @@ -25,8 +25,9 @@ = link_to "Edit assessors’ access to the system", suspension_status_admin_assessors_path, class: "btn btn-default btn-md" '   - = link_to public_send("new_admin_#{controller_name.singularize}_path"), class: 'new-user btn btn-secondary btn-md' do - = "+ Add #{controller_name == "users" ? "applicant" : controller_name.singularize}" + - unless controller_name == "admins" + = link_to public_send("new_admin_#{controller_name.singularize}_path"), class: 'new-user btn btn-secondary btn-md' do + = "+ Add #{controller_name == "users" ? "applicant" : controller_name.singularize}" .clear - if @search.query? diff --git a/app/views/admins/sessions/new.html.slim b/app/views/admins/sessions/new.html.slim index 5e8eafbaa2..076ea57935 100644 --- a/app/views/admins/sessions/new.html.slim +++ b/app/views/admins/sessions/new.html.slim @@ -9,16 +9,15 @@ = resource.model_name ' - Sign in - = simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| + - unless devise_mapping.omniauthable? + = simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| + .govuk-form-group + = f.input :email, required: false, autofocus: true, label: "Email", input_html: {class: "govuk-input--width-20"} - .govuk-form-group - = f.input :email, required: false, autofocus: true, label: "Email", input_html: {class: "govuk-input--width-20"} + .govuk-form-group + = f.input :password, required: false, label: "Password", input_html: {class: "govuk-input--width-10"} - .govuk-form-group - = f.input :password, required: false, label: "Password", input_html: {class: "govuk-input--width-10"} - - = f.input :remember_me, as: :boolean if devise_mapping.rememberable? - - = f.button :submit, "Sign in", class: "button large" + = f.input :remember_me, as: :boolean if devise_mapping.rememberable? + = f.button :submit, "Sign in", class: "button large" = render "admins/shared/links" diff --git a/app/views/admins/shared/_links.html.slim b/app/views/admins/shared/_links.html.slim index afc7b25cf4..1ddafb8020 100644 --- a/app/views/admins/shared/_links.html.slim +++ b/app/views/admins/shared/_links.html.slim @@ -15,6 +15,9 @@ p = link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name), class: 'govuk-link' - if devise_mapping.omniauthable? - - resource_class.omniauth_providers.each do |provider| - p - = link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider), class: 'govuk-link' + - if Rails.env.development? + = link_to "Sign in with Developer", omniauth_authorize_path(resource_name, :developer), class: 'govuk-link', method: :post + - else + - resource_class.omniauth_providers.excluding(:developer).each do |provider| + p + = link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider), class: 'govuk-link', method: :post diff --git a/app/views/layouts/application-admin.html.slim b/app/views/layouts/application-admin.html.slim index 59a8f768ba..92ca4a84ff 100644 --- a/app/views/layouts/application-admin.html.slim +++ b/app/views/layouts/application-admin.html.slim @@ -91,10 +91,6 @@ html.no-js strong = current_subject.decorate.full_name br small = current_subject.email - li.divider - - unless current_admin.authy_enabled - li - = link_to "Enable 2FA", admin_enable_authy_path li = button_to "Sign out", destroy_admin_session_path, @@ -110,10 +106,6 @@ html.no-js strong = current_subject.decorate.full_name br small = current_subject.email - li.divider - - unless current_admin.authy_enabled - li - = link_to "Enable 2FA", admin_enable_authy_path li = link_to "Sign out", destroy_admin_session_path, diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 5b426f8fab..811714828e 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -206,6 +206,13 @@ # manager.default_strategies(scope: :user).unshift :some_external_strategy # end + config.omniauth :dbt_staff_sso, + ENV.fetch("DBT_STAFF_SSO_APP_ID", nil), # remove the nils once we have these set in ci etc + ENV.fetch("DBT_STAFF_SSO_APP_SECRET", nil), + info_fields: OmniAuth::Strategies::DbtStaffSso::INFO_FIELD_NAMES + + config.omniauth :developer, fields: [:first_name, :last_name, :email] if Rails.env.development? + config.warden do |manager| manager.failure_app = QaeFailureApp end diff --git a/config/routes.rb b/config/routes.rb index dee13d7daf..5bc2de311a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,14 +15,12 @@ unlocks: "users/unlocks", } - devise_for :admins, controllers: { - confirmations: "admins/confirmations", - devise_authy: "admin/devise_authy", - }, path_names: { - verify_authy: "/verify-token", - enable_authy: "/enable-two-factor", - verify_authy_installation: "/verify-installation", - } + devise_for :admins, controllers: { omniauth_callbacks: "admins/omniauth_callbacks" } + + devise_scope :admin do + get "admins/sign_in", to: "devise/sessions#new", as: :new_admin_session + delete "admins/sign_out", to: "devise/sessions#destroy", as: :destroy_admin_session + end authenticate :admin do mount Sidekiq::Web => "/sidekiq" diff --git a/db/migrate/20241028142655_add_omniauth_to_admins.rb b/db/migrate/20241028142655_add_omniauth_to_admins.rb new file mode 100644 index 0000000000..9883f32263 --- /dev/null +++ b/db/migrate/20241028142655_add_omniauth_to_admins.rb @@ -0,0 +1,6 @@ +class AddOmniauthToAdmins < ActiveRecord::Migration[7.0] + def change + add_column :admins, :sso_provider, :string + add_column :admins, :sso_uid, :string + end +end diff --git a/db/seeds.rb b/db/seeds.rb index d8abf9c43a..409f09de2f 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,7 +1,6 @@ unless Admin.exists? admin_args = { email: "admin@example.com", - password: "^#ur9EkLm@1W+OaDvg", first_name: "First name", last_name: "Last name", confirmed_at: DateTime.now diff --git a/db/structure.sql b/db/structure.sql index 99590be2a8..069fe90c73 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1,6 +1,7 @@ SET statement_timeout = 0; SET lock_timeout = 0; SET idle_in_transaction_session_timeout = 0; +SET transaction_timeout = 0; SET client_encoding = 'UTF8'; SET standard_conforming_strings = on; SELECT pg_catalog.set_config('search_path', '', false); @@ -9,6 +10,13 @@ SET xmloption = content; SET client_min_messages = warning; SET row_security = off; +-- +-- Name: public; Type: SCHEMA; Schema: -; Owner: - +-- + +-- *not* creating schema, since initdb creates it + + -- -- Name: citext; Type: EXTENSION; Schema: -; Owner: - -- @@ -253,7 +261,9 @@ CREATE TABLE public.admins ( superadmin boolean DEFAULT false, deleted boolean DEFAULT false, autosave_token character varying, - unique_session_id character varying + unique_session_id character varying, + sso_provider character varying, + sso_uid character varying ); @@ -4389,5 +4399,4 @@ INSERT INTO "schema_migrations" (version) VALUES ('20240314105208'), ('20240501155226'), ('20241101151806'); - - +('20241102142655'); diff --git a/lib/omni_auth/strategies/dbt_staff_sso.rb b/lib/omni_auth/strategies/dbt_staff_sso.rb new file mode 100644 index 0000000000..943f6d07d2 --- /dev/null +++ b/lib/omni_auth/strategies/dbt_staff_sso.rb @@ -0,0 +1,31 @@ +module OmniAuth + module Strategies + class DbtStaffSso < OmniAuth::Strategies::OAuth2 + URL = ENV.fetch("DBT_STAFF_SSO_URL", nil) # remove the nils once we have these set in ci etc + INFO_PATH = ENV.fetch("DBT_STAFF_SSO_INFO_PATH", nil) + INFO_FIELD_NAMES = [ + "first_name", + "last_name", + "email", + ] + + option :name, :dbt_staff_sso + option :client_options, site: URL + option :pkce, true + + uid do + raw_info["id"] + end + + info do + INFO_FIELD_NAMES.index_with do |field_name| + raw_info[field_name] + end + end + + def raw_info + @raw_info ||= access_token.get(INFO_PATH).parsed + end + end + end +end diff --git a/spec/acceptance/admin_sign_in.feature b/spec/acceptance/admin_sign_in.feature deleted file mode 100644 index 03f9ed37bc..0000000000 --- a/spec/acceptance/admin_sign_in.feature +++ /dev/null @@ -1,7 +0,0 @@ -Feature: Admin Sign in - Background: - Given Admin user exists - - Scenario: I'm able to sign in - When I sign in as admin - Then I should see sign out link diff --git a/spec/acceptance/steps/admin_sign_in_steps.rb b/spec/acceptance/steps/admin_sign_in_steps.rb index 4ba54adcce..d6dec0718f 100644 --- a/spec/acceptance/steps/admin_sign_in_steps.rb +++ b/spec/acceptance/steps/admin_sign_in_steps.rb @@ -1,12 +1,9 @@ step "Admin user exists" do - @admin = FactoryBot.create(:admin, password: "my98ssdkjv9823kds=2") + @admin = FactoryBot.create(:admin) end step "I sign in as admin" do - visit "/admins/sign_in" - fill_in "admin_email", with: @admin.email - fill_in "admin_password", with: "my98ssdkjv9823kds=2" - click_button "Sign in" + login_admin(@admin) end step "I should see sign out link" do diff --git a/spec/controllers/admin/admins_controller_spec.rb b/spec/controllers/admin/admins_controller_spec.rb index b1a2d1dbe9..4ec5d345b3 100644 --- a/spec/controllers/admin/admins_controller_spec.rb +++ b/spec/controllers/admin/admins_controller_spec.rb @@ -1,6 +1,6 @@ require "rails_helper" -RSpec.describe Admin::AdminsController do +describe Admin::AdminsController do let!(:admin) { create(:admin, superadmin: true) } before do sign_in admin @@ -14,44 +14,6 @@ end end - describe "GET new" do - it "renders the new template" do - get :new - expect(response).to render_template("new") - end - end - - describe "POST create" do - it "should create a resource" do - post :create, params: { admin: FactoryBot.attributes_for(:admin) } - expect(response).to redirect_to admin_admins_url - expect(Admin.count).to eq 2 - end - end - describe "GET edit" do - it "renders the template" do - get :edit, params: { id: admin.id } - expect(response).to render_template("edit") - end - end - - describe "PUT update" do - it "should update a resource" do - put :update, params: { id: admin.id, admin: { first_name: "changed first name" } } - expect(response).to redirect_to admin_admins_url - expect(Admin.first.first_name).to eq "changed first name" - end - end - - describe "Delete destroy" do - it "should destroy a resource" do - to_be_deleted = create(:admin) - delete :destroy, params: { id: to_be_deleted.id } - expect(response).to redirect_to admin_admins_url - expect(Admin.count).to eq 1 - end - end - describe "GET login_as_assessor" do it "should login_as_assessor" do assessor = create(:assessor) diff --git a/spec/factories/admin_factory.rb b/spec/factories/admin_factory.rb index be180cb490..bee5db3052 100644 --- a/spec/factories/admin_factory.rb +++ b/spec/factories/admin_factory.rb @@ -10,7 +10,6 @@ factory :admin do first_name { "John" } last_name { "Doe" } - password { "my98ssdkjv9823kds=2" } email confirmed_at { Time.zone.now } end diff --git a/spec/features/admins/signing_in_with_single_sign_on_spec.rb b/spec/features/admins/signing_in_with_single_sign_on_spec.rb new file mode 100644 index 0000000000..4f57ff181d --- /dev/null +++ b/spec/features/admins/signing_in_with_single_sign_on_spec.rb @@ -0,0 +1,132 @@ +require "rails_helper" + +describe "Signing in with Single Sign on" do + let(:provider) { :dbt_staff_sso } + let(:uid) { "B0d$-Bu770N$" } + + context "when the admin existed prior to SSO" do + let(:admin) { create(:admin) } + + context "with the same details" do + before { stub_sso_request(email: admin.email, first_name: admin.first_name, last_name: admin.last_name, uid:, provider:) } + + it "signs the admin in and updates the SSO fields" do + expect { login_admin(admin, stub_sso: false) }.not_to change { Admin.count } + expect_admin_to_be_signed_in + admin.reload + expect(admin.sso_uid).to eq(uid) + expect(admin.sso_provider).to eq(provider.to_s) + end + end + + context "when the first_name has changed" do + let(:new_first_name) { "Bob" } + + before { stub_sso_request(email: admin.email, first_name: new_first_name, last_name: admin.last_name, uid:, provider:) } + + it "signs the admin in and updates the SSO fields" do + expect { login_admin(admin, stub_sso: false) }.not_to change { Admin.count } + expect_admin_to_be_signed_in + admin.reload + expect(admin.sso_uid).to eq(uid) + expect(admin.sso_provider).to eq(provider.to_s) + expect(admin.first_name).to eq(new_first_name) + end + end + + context "when the last_name has changed" do + let(:new_last_name) { "Buttons" } + + before { stub_sso_request(email: admin.email, first_name: admin.first_name, last_name: new_last_name, uid:, provider:) } + + it "signs the admin in, updates the SSO fields and the last_name" do + expect { login_admin(admin, stub_sso: false) }.not_to change { Admin.count } + expect_admin_to_be_signed_in + admin.reload + expect(admin.sso_uid).to eq(uid) + expect(admin.sso_provider).to eq(provider.to_s) + expect(admin.last_name).to eq(new_last_name) + end + end + end + + context "when the admin has previously signed in with SSO" do + let(:admin) { create(:admin, sso_uid: uid, sso_provider: provider) } + + context "with the same details" do + before { stub_sso_request(email: admin.email, first_name: admin.first_name, last_name: admin.last_name, uid: admin.sso_uid, provider: admin.sso_provider) } + + it "signs the admin in" do + expect { login_admin(admin, stub_sso: false) }.not_to change { Admin.count } + expect_admin_to_be_signed_in + end + end + + context "when the email has changed" do + let(:new_email) { "bob@buttons.com" } + + before { stub_sso_request(email: new_email, first_name: admin.first_name, last_name: admin.last_name, uid: admin.sso_uid, provider: provider) } + + it "signs the admin in and updates the email" do + expect { login_admin(admin, stub_sso: false) }.not_to change { Admin.count } + expect_admin_to_be_signed_in + admin.reload + expect(admin.email).to eq(new_email) + end + end + + context "when the first_name has changed" do + let(:new_first_name) { "Bob" } + + before { stub_sso_request(email: admin.email, first_name: new_first_name, last_name: admin.last_name, uid: admin.sso_uid, provider: provider) } + + it "signs the admin in and updates the first_name" do + expect { login_admin(admin, stub_sso: false) }.not_to change { Admin.count } + expect_admin_to_be_signed_in + admin.reload + expect(admin.first_name).to eq(new_first_name) + end + end + + context "when the last_name has changed" do + let(:new_last_name) { "Buttons" } + + before { stub_sso_request(email: admin.email, first_name: admin.first_name, last_name: new_last_name, uid: admin.sso_uid, provider: provider) } + + it "signs the admin in and updates the last_name" do + expect { login_admin(admin, stub_sso: false) }.not_to change { Admin.count } + expect_admin_to_be_signed_in + admin.reload + expect(admin.last_name).to eq(new_last_name) + end + end + end + + context "when the admin is using the platform for the first time" do + let(:email) { "bob@buttons.com" } + let(:first_name) { "Bob" } + let(:last_name) { "Buttons" } + + before do + stub_sso_request(email:, first_name:, last_name:, uid:, provider:) + visit "/admins/sign_in" + end + + it "signs the admin in and creates a new admin record" do + expect { click_link "Sign in with Dbt Staff Sso" }.to change { Admin.count }.from(0).to(1) + expect_admin_to_be_signed_in + + admin = Admin.last + expect(admin.email).to eq(email) + expect(admin.first_name).to eq(first_name) + expect(admin.last_name).to eq(last_name) + expect(admin.sso_uid).to eq(uid) + expect(admin.sso_provider).to eq(provider.to_s) + end + end + + def expect_admin_to_be_signed_in + expect(page).to have_content("Successfully authenticated from DBT Staff SSO account.") + expect(page).to have_selector(:link_or_button, "Sign out") + end +end diff --git a/spec/models/admin_spec.rb b/spec/models/admin_spec.rb index b0c7dac295..a568161c45 100644 --- a/spec/models/admin_spec.rb +++ b/spec/models/admin_spec.rb @@ -2,17 +2,11 @@ require "rails_helper" -RSpec.describe Admin, type: :model do +describe Admin, type: :model do describe "#create" do - it "creates an autosave token for an admin" do - admin = Admin.create!( - email: "john@example.com", - first_name: "John", - last_name: "Smith", - password: "^#ur9EkLm@1W+OaDvg", - password_confirmation: "^#ur9EkLm@1W+OaDvg", - ) + let(:admin) { Admin.create!(email: "john@example.com", first_name: "John", last_name: "Smith") } + it "creates an autosave token for an admin" do expect(admin.autosave_token).not_to be nil end end @@ -30,16 +24,10 @@ end describe "soft_delete!" do + let(:admin) { create(:admin) } + it "should set deleted" do - admin = create(:admin) - admin.soft_delete! - expect(admin.deleted.present?).to be_truthy + expect { admin.soft_delete! }.to change { admin.deleted.present? }.from(false).to(true) end end - - context "devise mailers" do - let(:user) { create(:admin) } - - include_context "devise mailers instructions" - end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index c780c9636a..673b493298 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -115,6 +115,8 @@ def matches?(source_file) config.include(Shoulda::Matchers::ActiveModel, type: :model) config.include(Shoulda::Matchers::ActiveRecord, type: :model) end + + OmniAuth.config.test_mode = true end RSpec::Matchers.define_negated_matcher :not_change, :change diff --git a/spec/support/user_step_definitions.rb b/spec/support/user_step_definitions.rb index b6272e282d..aa8de6a2f7 100644 --- a/spec/support/user_step_definitions.rb +++ b/spec/support/user_step_definitions.rb @@ -1,8 +1,28 @@ module UserStepDefinitions - def login_admin(admin) + def login_admin(admin, stub_sso: true) + if stub_sso + stub_sso_request( + email: admin.email, + first_name: admin.first_name, + last_name: admin.last_name, + provider: :dbt_staff_sso, + ) + end + visit "/admins/sign_in" - fill_in "admin_email", with: admin.email - fill_in "admin_password", with: "my98ssdkjv9823kds=2" - click_button "Sign in" + click_link "Sign in with Dbt Staff Sso" end + + def stub_sso_request(email:, first_name:, last_name:, provider:, uid: SecureRandom.uuid, auth: OmniAuth) + auth.config.mock_auth[provider] = AuthStruct.new( + uid, + provider, + InfoStruct.new(email, first_name, last_name), + ) + end + + private + + AuthStruct = Struct.new(:uid, :provider, :info) + InfoStruct = Struct.new(:email, :first_name, :last_name) end