feat: add specs
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user