From 42ca3d97e6cbc0ae783938e9a45c5b8ad3fdeb98 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 28 Aug 2025 13:26:42 +0530 Subject: [PATCH] feat: add specs --- spec/builders/saml_user_builder_spec.rb | 209 ++++++++++++++++ .../omniauth_callbacks_controller_spec.rb | 229 ++++++++++++++++++ 2 files changed, 438 insertions(+) create mode 100644 spec/builders/saml_user_builder_spec.rb create mode 100644 spec/controllers/devise_overrides/omniauth_callbacks_controller_spec.rb diff --git a/spec/builders/saml_user_builder_spec.rb b/spec/builders/saml_user_builder_spec.rb new file mode 100644 index 000000000..4e9c68b42 --- /dev/null +++ b/spec/builders/saml_user_builder_spec.rb @@ -0,0 +1,209 @@ +require 'rails_helper' + +RSpec.describe SamlUserBuilder do + let(:email) { 'saml.user@example.com' } + let(:auth_hash) do + { + 'provider' => 'saml', + 'uid' => 'saml-uid-123', + 'info' => { + 'email' => email, + 'name' => 'SAML User', + 'first_name' => 'SAML', + 'last_name' => 'User' + }, + 'extra' => { + 'raw_info' => { + 'groups' => %w[Administrators Users] + } + } + } + end + let(:account) { create(:account) } + let(:builder) { described_class.new(auth_hash, account_id: account.id) } + + describe '#perform' do + context 'when user does not exist' do + it 'creates a new user' do + expect { builder.perform }.to change(User, :count).by(1) + end + + it 'creates user with correct attributes' do + user = builder.perform + + expect(user.email).to eq(email) + expect(user.name).to eq('SAML User') + expect(user.display_name).to eq('SAML User') + expect(user.provider).to eq('saml') + expect(user.uid).to eq(email) # User model sets uid to email in before_validation callback + expect(user.confirmed_at).to be_present + end + + it 'creates user with a random password' do + user = builder.perform + expect(user.encrypted_password).to be_present + end + + it 'adds user to the account' do + user = builder.perform + expect(user.accounts).to include(account) + end + + it 'sets default role as agent' do + user = builder.perform + account_user = AccountUser.find_by(user: user, account: account) + expect(account_user.role).to eq('agent') + end + + context 'when name is not provided' do + let(:auth_hash) do + { + 'provider' => 'saml', + 'uid' => 'saml-uid-123', + 'info' => { + 'email' => email + } + } + end + + it 'derives name from email' do + user = builder.perform + expect(user.name).to eq('saml.user') + end + end + end + + context 'when user already exists' do + let!(:existing_user) { create(:user, email: email) } + + it 'does not create a new user' do + expect { builder.perform }.not_to change(User, :count) + end + + it 'returns the existing user' do + user = builder.perform + expect(user).to eq(existing_user) + end + + it 'adds existing user to the account if not already added' do + user = builder.perform + expect(user.accounts).to include(account) + end + + it 'does not duplicate account association' do + existing_user.account_users.create!(account: account, role: 'agent') + + expect { builder.perform }.not_to change(AccountUser, :count) + end + end + + context 'without account_id' do + let(:builder) { described_class.new(auth_hash) } + + it 'creates user without account association' do + user = builder.perform + expect(user.persisted?).to be true + expect(user.accounts).to be_empty + end + end + + context 'with role mappings' do + let!(:saml_settings) do + create(:account_saml_settings, + account: account, + enabled: true, + role_mappings: { + 'Administrators' => { 'role' => 'administrator' }, + 'Agents' => { 'role' => 'agent' } + }) + end + + it 'applies administrator role based on SAML groups' do + user = builder.perform + account_user = AccountUser.find_by(user: user, account: account) + expect(account_user.role).to eq('administrator') + end + + context 'with custom role mapping' do + let!(:custom_role) { create(:custom_role, account: account) } + let!(:saml_settings) do + create(:account_saml_settings, + account: account, + enabled: true, + role_mappings: { + 'Administrators' => { 'custom_role_id' => custom_role.id } + }) + end + + it 'applies custom role based on SAML groups' do + user = builder.perform + account_user = AccountUser.find_by(user: user, account: account) + expect(account_user.custom_role_id).to eq(custom_role.id) + end + end + + context 'when user is not in any mapped groups' do + let(:auth_hash) do + { + 'provider' => 'saml', + 'uid' => 'saml-uid-123', + 'info' => { + 'email' => email, + 'name' => 'SAML User' + }, + 'extra' => { + 'raw_info' => { + 'groups' => ['UnmappedGroup'] + } + } + } + end + + it 'keeps default agent role' do + user = builder.perform + account_user = AccountUser.find_by(user: user, account: account) + expect(account_user.role).to eq('agent') + end + end + end + + context 'with different group attribute names' do + let(:auth_hash) do + { + 'provider' => 'saml', + 'uid' => 'saml-uid-123', + 'info' => { + 'email' => email, + 'name' => 'SAML User' + }, + 'extra' => { + 'raw_info' => { + 'memberOf' => ['CN=Administrators,OU=Groups,DC=example,DC=com'] + } + } + } + end + + it 'reads groups from memberOf attribute' do + allow_any_instance_of(described_class).to receive(:saml_groups).and_return(['CN=Administrators,OU=Groups,DC=example,DC=com']) + user = builder.perform + expect(user).to be_persisted + end + end + + context 'error handling' do + before do + allow_any_instance_of(User).to receive(:save).and_return(false) + end + + it 'returns unsaved user object' do + user = builder.perform + expect(user.persisted?).to be false + end + + it 'does not create account association for failed user' do + expect { builder.perform }.not_to change(AccountUser, :count) + end + end + end +end diff --git a/spec/controllers/devise_overrides/omniauth_callbacks_controller_spec.rb b/spec/controllers/devise_overrides/omniauth_callbacks_controller_spec.rb new file mode 100644 index 000000000..bf039e541 --- /dev/null +++ b/spec/controllers/devise_overrides/omniauth_callbacks_controller_spec.rb @@ -0,0 +1,229 @@ +require 'rails_helper' + +RSpec.describe DeviseOverrides::OmniauthCallbacksController, type: :controller do + let(:account) { create(:account) } + let(:email) { 'saml.user@example.com' } + + before do + # Set up OmniAuth test mode + OmniAuth.config.test_mode = true + request.env['devise.mapping'] = Devise.mappings[:user] + end + + after do + OmniAuth.config.test_mode = false + end + + describe 'POST #saml' do + let(:auth_hash) do + OmniAuth::AuthHash.new( + provider: 'saml', + uid: 'saml-uid-123', + info: { + email: email, + name: 'SAML User', + first_name: 'SAML', + last_name: 'User' + }, + extra: { + raw_info: { + groups: ['Administrators'] + } + } + ) + end + + context 'successful SAML authentication' do + before do + # Set up session with auth data as DeviseTokenAuth would + session['dta.omniauth.auth'] = auth_hash.to_hash + session[:saml_account_id] = account.id + + # Enable SAML for the account + create(:account_saml_settings, account: account, enabled: true) + end + + context 'when user does not exist' do + it 'creates a new user' do + expect do + post :saml + end.to change(User, :count).by(1) + end + + it 'redirects to login with SSO token' do + post :saml + + User.from_email(email) + expect(response).to redirect_to(%r{/app/login\?email=.*&sso_auth_token=}) + end + + it 'associates user with account' do + post :saml + + user = User.from_email(email) + expect(user.accounts).to include(account) + end + end + + context 'when user already exists' do + let!(:existing_user) { create(:user, email: email) } + + it 'does not create a new user' do + expect do + post :saml + end.not_to change(User, :count) + end + + it 'redirects to login with SSO token' do + post :saml + + expect(response).to redirect_to(%r{/app/login\?email=.*&sso_auth_token=}) + end + + it 'adds user to account if not already added' do + expect(existing_user.accounts).not_to include(account) + + post :saml + + expect(existing_user.reload.accounts).to include(account) + end + end + + context 'without account_id' do + before do + session.delete(:saml_account_id) + end + + it 'still processes the authentication' do + post :saml + + expect(response).to redirect_to(%r{/app/login}) + end + end + end + + context 'SAML not enabled for account' do + before do + session['dta.omniauth.auth'] = auth_hash.to_hash + session[:saml_account_id] = account.id + + # SAML is disabled for the account + create(:account_saml_settings, account: account, enabled: false) + end + + it 'redirects with error' do + post :saml + + expect(response).to redirect_to(%r{/app/login\?error=saml-not-enabled}) + end + + it 'does not create a user' do + expect do + post :saml + end.not_to change(User, :count) + end + end + + context 'without auth hash' do + before do + session.delete('dta.omniauth.auth') + end + + it 'redirects to login' do + post :saml + + expect(response).to redirect_to(%r{/app/login}) + end + end + end + + describe 'GET #redirect_callbacks' do + let(:auth_hash) do + OmniAuth::AuthHash.new( + provider: 'saml', + uid: 'saml-uid-123', + info: { + email: email, + name: 'SAML User' + } + ) + end + + before do + request.env['omniauth.auth'] = auth_hash + request.env['omniauth.params'] = { + 'resource_class' => 'User', + 'account_id' => account.id.to_s + } + end + + context 'for SAML provider' do + it 'stores auth data in session' do + get :redirect_callbacks, params: { provider: 'saml' } + + expect(session['dta.omniauth.auth']).to eq(auth_hash.except('extra')) + expect(session['dta.omniauth.params']).to include('account_id' => account.id.to_s) + end + + it 'redirects with 303 status for SAML' do + get :redirect_callbacks, params: { provider: 'saml' } + + expect(response).to have_http_status(303) + expect(response).to redirect_to(%r{/auth/saml/callback}) + end + end + + context 'for non-SAML provider' do + it 'redirects with 307 status' do + get :redirect_callbacks, params: { provider: 'google_oauth2' } + + expect(response).to have_http_status(307) + expect(response).to redirect_to(%r{/auth/google_oauth2/callback}) + end + end + end + + describe '#get_resource_from_auth_hash' do + let(:auth_hash) do + { + 'provider' => 'saml', + 'info' => { + 'email' => email + } + } + end + + before do + allow(controller).to receive(:auth_hash).and_return(auth_hash) + end + + context 'for SAML provider' do + it 'uses from_email to find user' do + existing_user = create(:user, email: email.upcase) # Test case insensitivity + + controller.send(:get_resource_from_auth_hash) + + expect(controller.instance_variable_get(:@resource)).to eq(existing_user) + end + end + + context 'for non-SAML provider' do + let(:auth_hash) do + { + 'provider' => 'google_oauth2', + 'info' => { + 'email' => email + } + } + end + + it 'uses where query to find user' do + existing_user = create(:user, email: email) + + controller.send(:get_resource_from_auth_hash) + + expect(controller.instance_variable_get(:@resource)).to eq(existing_user) + end + end + end +end