zammad/spec/models/user_spec.rb

1566 lines
61 KiB
Ruby
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
require 'rails_helper'
require 'models/application_model_examples'
require 'models/concerns/has_groups_examples'
require 'models/concerns/has_history_examples'
require 'models/concerns/has_roles_examples'
require 'models/concerns/has_groups_permissions_examples'
require 'models/concerns/has_xss_sanitized_note_examples'
require 'models/concerns/has_image_sanitized_note_examples'
require 'models/concerns/can_be_imported_examples'
require 'models/concerns/can_csv_import_examples'
require 'models/concerns/can_csv_import_user_examples'
require 'models/concerns/has_object_manager_attributes_examples'
require 'models/user/can_lookup_search_index_attributes_examples'
require 'models/user/performs_geo_lookup_examples'
require 'models/concerns/has_taskbars_examples'
require 'models/concerns/has_recent_closes_examples'
require 'models/concerns/has_two_factor_examples'
RSpec.describe User, type: :model do
subject(:user) { create(:user) }
let(:customer) { create(:customer) }
let(:agent) { create(:agent) }
let(:admin) { create(:admin) }
it_behaves_like 'ApplicationModel',
can_assets: { associations: :organization },
can_param: { sample_data_attribute: :email }
it_behaves_like 'HasGroups', group_access_factory: :agent
it_behaves_like 'HasHistory'
it_behaves_like 'HasRoles', group_access_factory: :agent
it_behaves_like 'HasXssSanitizedNote', model_factory: :user
it_behaves_like 'HasImageSanitizedNote', model_factory: :user
it_behaves_like 'HasGroups and Permissions', group_access_no_permission_factory: :user
it_behaves_like 'CanBeImported'
# it_behaves_like 'CanCsvImport', unique_attributes: 'email'
include_examples 'CanCsvImport - User specific tests'
it_behaves_like 'HasObjectManagerAttributes'
it_behaves_like 'CanLookupSearchIndexAttributes'
it_behaves_like 'HasTaskbars'
it_behaves_like 'HasRecentCloses'
it_behaves_like 'UserPerformsGeoLookup'
it_behaves_like 'Association clears cache', association: :roles
it_behaves_like 'Association clears cache', association: :organizations
it_behaves_like 'User::HasTwoFactor'
describe 'Class methods:' do
describe '.identify' do
it 'returns users by given login' do
expect(described_class.identify(user.login)).to eq(user)
end
it 'returns users by given email' do
expect(described_class.identify(user.email)).to eq(user)
end
it 'returns nil for empty username' do
expect(described_class.identify('')).to be_nil
end
end
describe '.reset_notifications_preferences!' do
let(:sample_notifications) { { sample_notifications: true } }
def change_setting_ticket_agent_default_notifications
Setting.set('ticket_agent_default_notifications', sample_notifications)
end
context 'when user is agent' do
before do
# Create the agent, before the default notifications are set, so
agent
change_setting_ticket_agent_default_notifications
end
it 'changes existing matrix' do
expect { described_class.reset_notifications_preferences!(agent) }
.to change { agent.preferences.dig('notification_config', 'matrix') }
.to sample_notifications
end
it 'sets matrix if preferences are empty' do
agent.update_columns preferences: nil
expect { described_class.reset_notifications_preferences!(agent) }
.to change { agent.preferences&.dig('notification_config', 'matrix') }
.to(sample_notifications)
.from(nil)
end
it 'does not touch selected groups do' do
agent.preferences['notification_config']['group_ids'] = ['123']
agent.save!
expect { described_class.reset_notifications_preferences!(agent) }
.not_to change { agent.preferences&.dig('notification_config', 'group_ids') }
end
end
context 'when user is not agent' do
before do
# Create the customer, before the default notifications are set, so
customer
change_setting_ticket_agent_default_notifications
end
it 'does not change existing matrix' do
expect { described_class.reset_notifications_preferences!(customer) }
.not_to change { customer.preferences.dig('notification_config', 'matrix') }
end
it 'sets matrix if preferences are empty' do
customer.update_columns preferences: nil
expect { described_class.reset_notifications_preferences!(customer) }
.not_to change { customer.preferences&.dig('notification_config', 'matrix') }
.from(nil)
end
end
end
describe '.by_mobile' do
let!(:user) { create(:customer, mobile: saved_mobile) }
let(:saved_mobile) { '+4912341234' }
context 'with a number saved with prefixed +' do
context 'searching for the same mobile number' do
it 'finds the user (by direct lookup)' do
expect(described_class.by_mobile(number: saved_mobile)).to eq(user)
end
end
context 'searching for the E.164 number without prefixed +' do
it 'finds the user (through CTI lookup)' do
expect(described_class.by_mobile(number: '4912341234')).to eq(user)
end
end
end
context 'with a number saved without prefixed +' do
let(:saved_mobile) { '4912341234' }
context 'searching for the same mobile number' do
it 'finds the user (by direct lookup)' do
expect(described_class.by_mobile(number: saved_mobile)).to eq(user)
end
end
context 'searching for the number prefixed with +' do
it 'finds the user (through CTI lookup)' do
expect(described_class.by_mobile(number: '+4912341234')).to eq(user)
end
end
end
context 'with a non-matching number' do
it 'does not find the user' do
expect(described_class.by_mobile(number: '99999999999')).to be_nil
end
end
end
end
describe 'Instance methods:' do
describe '#by_reset_token' do
subject(:user) { token.user }
let(:token) { create(:token_password_reset) }
context 'with a valid token' do
it 'returns the matching user' do
expect(described_class.by_reset_token(token.token)).to eq(user)
end
end
context 'with an invalid token' do
it 'returns nil' do
expect(described_class.by_reset_token('not-existing')).to be_nil
end
end
end
describe '#password_reset_via_token' do
subject(:user) { token.user }
let!(:token) { create(:token_password_reset) }
it 'changes the password of the token user and destroys the token' do
expect { described_class.password_reset_via_token(token.token, 'VYxesRc6O2') }
.to change { user.reload.password }
.and change(Token, :count).by(-1)
end
end
describe '#admin_password_auth_new_token' do
context 'with user role agent' do
subject(:user) { create(:agent) }
it 'returns no token' do
expect(described_class.admin_password_auth_new_token(user.login)).to be_nil
end
end
context 'with user role admin' do
subject(:user) { create(:admin) }
it 'returns token' do
expect(described_class.admin_password_auth_new_token(user.login).keys).to include(:user, :token)
end
it 'delete existing tokens when creating multiple times' do
described_class.admin_password_auth_new_token(user.login)
described_class.admin_password_auth_new_token(user.login)
expect(Token.where(action: 'AdminAuth', user_id: user.id).count).to eq(1)
end
end
end
describe '#admin_password_auth_via_token' do
context 'with invalid token' do
it 'returns nil' do
expect(described_class.admin_password_auth_via_token('not-existing')).to be_nil
end
end
context 'with valid token' do
let(:user) { create(:admin) }
it 'returns the matching user' do
result = described_class.admin_password_auth_new_token(user.login)
token = result[:token].token
expect(described_class.admin_password_auth_via_token(token)).to match(user)
end
it 'destroys token' do
result = described_class.admin_password_auth_new_token(user.login)
token = result[:token].token
expect { described_class.admin_password_auth_via_token(token) }.to change(Token, :count).by(-1)
end
end
end
describe '#locale' do
subject(:user) { create(:user, preferences: preferences) }
context 'with no #preferences[:locale]' do
let(:preferences) { {} }
context 'with default locale' do
before { Setting.set('locale_default', 'foo') }
it 'returns the system-wide default locale' do
expect(user.locale).to eq('foo')
end
end
context 'without default locale' do
before { Setting.set('locale_default', nil) }
it 'returns en-us' do
expect(user.locale).to eq('en-us')
end
end
end
context 'with a #preferences[:locale]' do
let(:preferences) { { locale: 'bar' } }
it 'returns the users configured locale' do
expect(user.locale).to eq('bar')
end
end
end
describe '#check_login' do
let(:agent) { create(:agent, login: nil) }
it 'does use given login' do
new_agent = create(:agent)
expect(new_agent.login).not_to end_with('1')
end
it 'ensures login is downcase and without white spaces' do
new_agent = create(:agent, login: ' TestUser ')
expect(new_agent.login).to eq('testuser')
end
it 'returns validation error if the login is already taken' do
new_agent = build(:agent, login: agent.login)
new_agent.valid?
expect(new_agent.errors[:login]).to include('has already been taken')
end
context 'when email-as-login was used and is changed' do
it 'updates login' do
new_agent = create(:agent, login: nil)
new_agent.update! email: Faker::Internet.unique.email
expect(new_agent.login).to eq(new_agent.email)
end
end
context 'when email is empty too' do
it 'generates auto-login' do
new_agent = create(:agent, login: nil, email: nil)
expect(new_agent.login).to start_with('auto-')
end
end
context 'when user_email_multiple_use is enabled' do
before { Setting.set('user_email_multiple_use', true) }
context 'when login is given' do
it 'raises error if the login is already taken' do
new_agent = build(:agent, login: agent.login)
new_agent.valid?
expect(new_agent.errors[:login]).to include('has already been taken')
end
end
context 'when login is not given' do
it 'uses email as fallback' do
new_agent = create(:agent, login: nil)
expect(new_agent.login).to eq(new_agent.email)
end
it 'does number up agent logins (1)' do
new_agent = create(:agent, login: nil, email: agent.email)
expect(new_agent.login).to eq("#{new_agent.email}1")
end
it 'does number up agent logins (5)' do
new_agent = create(:agent, login: nil, email: agent.email)
4.times do
new_agent = create(:agent, login: nil, email: agent.email)
end
expect(new_agent.login).to eq("#{new_agent.email}5")
end
it 'does backup with uuid in cases of many duplicates' do
new_agent = create(:agent, login: nil, email: agent.email)
20.times do
new_agent = create(:agent, login: nil, email: agent.email)
end
expect(new_agent.login.sub!(new_agent.email, '')).to be_a_uuid
end
end
context 'when email-as-login was used and is changed' do
it 'updates login' do
new_agent = create(:agent, login: nil)
new_agent.update! email: Faker::Internet.unique.email
expect(new_agent.login).to eq(new_agent.email)
end
it 'number up agent logins (1)' do
new_agent = create(:agent, login: nil, email: agent.email)
new_email = Faker::Internet.unique.email
agent.update! email: new_email
new_agent.update! email: new_email
expect(new_agent.login).to eq("#{new_agent.email}1")
end
end
end
end
describe '#check_name' do
shared_examples 'preserving name' do |expected_firstname, expected_lastname|
it 'preserves the given name' do
expect(user).to have_attributes(firstname: expected_firstname, lastname: expected_lastname)
end
end
context 'without postmaster context' do
context 'when only lastname is present' do
let(:user) { create(:user, firstname: '', lastname: lastname, email: Faker::Internet.unique.email) }
context 'with all-uppercase single word' do
let(:lastname) { 'TESTUSER' }
it_behaves_like 'preserving name', '', 'TESTUSER'
end
context 'with all-lowercase single word' do
let(:lastname) { 'testuser' }
it_behaves_like 'preserving name', '', 'testuser'
end
context 'with mixed-case single word' do
let(:lastname) { 'McTester' }
it_behaves_like 'preserving name', '', 'McTester'
end
end
context 'when only firstname is present' do
let(:user) { create(:user, firstname: firstname, lastname: '', email: Faker::Internet.unique.email) }
context 'with two words (splits and capitalizes via name_guess)' do
let(:firstname) { 'perkūnas ąžuolas' }
it_behaves_like 'preserving name', 'Perkūnas', 'Ąžuolas'
end
end
context 'when both names are present' do
let(:user) { create(:user, firstname: 'John', lastname: 'TESTUSER', email: Faker::Internet.unique.email) }
it_behaves_like 'preserving name', 'John', 'TESTUSER'
end
end
context 'with postmaster context' do
context 'when only firstname is present' do
let(:user) do
ApplicationHandleInfo.use('scheduler.postmaster') do
create(:user, firstname: firstname, lastname: '', email: Faker::Internet.unique.email)
end
end
context 'with two lowercase words (splits into first/last)' do
let(:firstname) { 'yann degran' }
it_behaves_like 'preserving name', 'Yann', 'Degran'
end
context 'with two uppercase words (splits and capitalizes)' do
let(:firstname) { 'YANN DEGRAN' }
it_behaves_like 'preserving name', 'Yann', 'Degran'
end
context 'with non-ASCII characters' do
let(:firstname) { 'perkūnas ąžuolas' }
it_behaves_like 'preserving name', 'Perkūnas', 'Ąžuolas'
end
end
context 'when both names are blank' do
context 'with firstname.lastname email' do
let(:user) do
ApplicationHandleInfo.use('scheduler.postmaster') do
create(:user, firstname: '', lastname: '', email: 'john.doe@example.com')
end
end
it_behaves_like 'preserving name', 'John', 'Doe'
end
end
end
end
end
describe 'Attributes:' do
describe '#login_failed' do
before { user.update(login_failed: 1) }
it 'is reset to 0 when password is updated' do
expect { user.update(password: Faker::Internet.password) }
.to change(user, :login_failed).to(0)
end
end
describe '#password' do
let(:password) { Faker::Internet.password }
context 'when set to plaintext password' do
it 'hashes password before saving to DB' do
user.password = password
expect { user.save }
.to change { PasswordHash.crypted?(user.password) }
end
end
context 'for existing user records' do
before do
user.update(password: password)
allow(user).to receive(:ensured_password).and_call_original
end
context 'when changed to empty string' do
it 'keeps previous password' do
expect { user.update!(password: '') }
.not_to change(user, :password)
end
it 'calls #ensured_password' do
user.update!(password: '')
expect(user).to have_received(:ensured_password)
end
end
context 'when changed to nil' do
it 'keeps previous password' do
expect { user.update!(password: nil) }
.not_to change(user, :password)
end
it 'calls #ensured_password' do
user.update!(password: nil)
expect(user).to have_received(:ensured_password)
end
end
context 'when changed another attribute' do
it 'keeps previous password' do
expect { user.update!(email: "123#{user.email}") }
.not_to change(user, :password)
end
it 'does not call #ensured_password' do
user.update!(email: "123#{user.email}")
expect(user).not_to have_received(:ensured_password)
end
end
end
context 'for new user records' do
context 'when passed as an empty string' do
let(:another_user) { create(:user, password: '') }
it 'sets password to nil' do
expect(another_user.password).to be_nil
end
end
context 'when passed as nil' do
let(:another_user) { create(:user, password: nil) }
it 'sets password to nil' do
expect(another_user.password).to be_nil
end
end
end
context 'when set to SHA2 digest (to facilitate OTRS imports)' do
it 'does not re-hash before saving' do
user.password = "{sha2}#{Digest::SHA2.hexdigest(password)}"
expect { user.save }.not_to change(user, :password)
end
end
context 'when set to Argon2 digest' do
it 'does not re-hash before saving' do
user.password = PasswordHash.crypt(password)
expect { user.save }.not_to change(user, :password)
end
end
context 'when creating two users with the same password' do
before { user.update(password: password) }
let(:another_user) { create(:user, password: password) }
it 'does not generate the same password hash' do
expect(user.password).not_to eq(another_user.password)
end
end
context 'when saving a very long password' do
let(:long_string) { "asd1ASDasd!#{Faker::Lorem.characters(number: 1_000)}" }
it 'marks object as invalid by adding error' do
user.update(password: long_string)
expect(user.errors.first.full_message).to eq('Password is too long')
end
end
end
describe '#phone' do
subject(:user) { create(:user, phone: orig_number) }
context 'when included on create' do
let(:orig_number) { '1234567890' }
it 'adds corresponding CallerId record' do
expect { user }
.to change { Cti::CallerId.where(caller_id: orig_number).count }.by(1)
end
end
context 'when added on update' do
let(:orig_number) { nil }
let(:new_number) { '1234567890' }
before { user } # create user
it 'adds corresponding CallerId record' do
expect { user.update(phone: new_number) }
.to change { Cti::CallerId.where(caller_id: new_number).count }.by(1)
end
end
context 'when falsely added on update (change: [nil, ""])' do
let(:orig_number) { nil }
let(:new_number) { '' }
before { user } # create user
it 'does not attempt to update CallerId record' do
allow(Cti::CallerId).to receive(:add).with(any_args)
expect(Cti::CallerId.where(object: 'User', o_id: user.id).count)
.to eq(0)
expect { user.update(phone: new_number) }
.not_to change { Cti::CallerId.where(object: 'User', o_id: user.id).count }
expect(Cti::CallerId).not_to have_received(:add)
end
end
context 'when removed on update' do
let(:orig_number) { '1234567890' }
let(:new_number) { nil }
before { user } # create user
it 'removes corresponding CallerId record' do
expect { user.update(phone: nil) }
.to change { Cti::CallerId.where(caller_id: orig_number).count }.by(-1)
end
end
context 'when changed on update' do
let(:orig_number) { '1234567890' }
let(:new_number) { orig_number.next }
before { user } # create user
it 'replaces CallerId record' do
expect { user.update(phone: new_number) }
.to change { Cti::CallerId.where(caller_id: orig_number).count }.by(-1)
.and change { Cti::CallerId.where(caller_id: new_number).count }.by(1)
end
end
end
describe '#preferences' do
describe '"mail_delivery_failed{,_data}" keys' do
before do
user.update(
preferences: {
mail_delivery_failed: true,
mail_delivery_failed_data: Time.current
}
)
end
it 'deletes "mail_delivery_failed"' do
expect { user.update(email: Faker::Internet.email) }
.to change { user.preferences.key?(:mail_delivery_failed) }.to(false)
end
it 'leaves "mail_delivery_failed_data" untouched' do
expect { user.update(email: Faker::Internet.email) }
.to not_change { user.preferences[:mail_delivery_failed_data] }
end
end
end
describe '#image' do
describe 'when value is invalid' do
let(:value) { 'Th1515n0t4v4l1dh45h' }
it 'prevents create' do
expect { create(:user, image: value) }.to raise_error(Exceptions::UnprocessableContent, %r{#{value}})
end
it 'prevents update' do
expect { create(:user).update!(image: value) }.to raise_error(Exceptions::UnprocessableContent, %r{#{value}})
end
end
end
describe '#image_source' do
describe 'when value is invalid' do
let(:value) { 'Th1515n0t4v4l1dh45h' }
let(:escaped) { Regexp.escape(value) }
it 'valid create' do
expect(create(:user, image_source: 'https://zammad.org/avatar.png').image_source).not_to be_nil
end
it 'removes invalid image source of create' do
expect(create(:user, image_source: value).image_source).to be_nil
end
it 'removes invalid image source of update' do
user = create(:user)
user.update!(image_source: value)
expect(user.image_source).to be_nil
end
end
end
describe 'fetch_avatar_for_email', performs_jobs: true do
it 'enqueues avatar job when creating a user with email' do
expect { create(:user) }.to have_enqueued_job AvatarCreateJob
end
it 'does not enqueue avatar job when creating a user without email' do
expect { create(:user, :without_email) }.not_to have_enqueued_job AvatarCreateJob
end
context 'with an existing user' do
before do
agent
clear_jobs
end
it 'enqueues avatar job when updating a user with email' do
expect { agent.update! email: 'avatar@example.com' }.to have_enqueued_job AvatarCreateJob
end
it 'does not enqueue avatar job when updating a user without email' do
expect { agent.update! login: 'avatar_login', email: nil }.not_to have_enqueued_job AvatarCreateJob
end
it 'does not enqueue avatar job when updating a user having email' do
expect { agent.update! firstname: 'no avatar update' }.not_to have_enqueued_job AvatarCreateJob
end
end
end
end
describe 'Associations:' do
subject(:user) { create(:agent, groups: [group_subject]) }
let!(:group_subject) { create(:group) }
it 'does remove references before destroy' do
refs_known = {
'Group' => { 'created_by_id' => 1, 'updated_by_id' => 0 },
'Token' => { 'user_id' => 1 },
'Ticket::Article' => { 'created_by_id' => 1, 'updated_by_id' => 1, 'origin_by_id' => 1 },
'Ticket::StateType' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'Ticket::Article::Sender' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'Ticket::Article::Type' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'Ticket::Article::Flag' => { 'created_by_id' => 0 },
'Ticket::Priority' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'Ticket::SharedDraftStart' => { 'created_by_id' => 1, 'updated_by_id' => 0 },
'Ticket::SharedDraftZoom' => { 'created_by_id' => 1, 'updated_by_id' => 0 },
'Ticket::TimeAccounting' => { 'created_by_id' => 0 },
'Ticket::TimeAccounting::Type' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'Ticket::State' => { 'created_by_id' => 1, 'updated_by_id' => 1 },
'PostmasterFilter' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'PublicLink' => { 'created_by_id' => 1, 'updated_by_id' => 0 },
'User::TwoFactorPreference' => { 'created_by_id' => 1, 'updated_by_id' => 1, 'user_id' => 1 },
'OnlineNotification' => { 'user_id' => 1, 'created_by_id' => 0, 'updated_by_id' => 0 },
'Ticket' => { 'created_by_id' => 0, 'updated_by_id' => 0, 'owner_id' => 1, 'customer_id' => 3 },
'Template' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'Avatar' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'Scheduler' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'Chat' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'HttpLog' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'EmailAddress' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'Taskbar' => { 'user_id' => 1 },
'Sla' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'UserDevice' => { 'user_id' => 1 },
'Chat::Message' => { 'created_by_id' => 1 },
'Chat::Agent' => { 'created_by_id' => 1, 'updated_by_id' => 1 },
'Chat::Session' => { 'user_id' => 1, 'created_by_id' => 0, 'updated_by_id' => 0 },
'Tag' => { 'created_by_id' => 0 },
'RecentClose' => { 'user_id' => 1 },
'RecentView' => { 'created_by_id' => 1 },
'KnowledgeBase::Answer::Translation' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'LdapSource' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'KnowledgeBase::Answer' => { 'archived_by_id' => 1, 'published_by_id' => 1, 'internal_by_id' => 1 },
'Report::Profile' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'Package' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'Job' => { 'created_by_id' => 0, 'updated_by_id' => 1 },
'Store' => { 'created_by_id' => 0 },
'Cti::CallerId' => { 'user_id' => 1 },
'DataPrivacyTask' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'Trigger' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'Translation' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'ObjectManager::Attribute' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'User' => { 'created_by_id' => 2, 'out_of_office_replacement_id' => 1, 'updated_by_id' => 2 },
'User::OverviewSorting' => { 'created_by_id' => 0, 'updated_by_id' => 0, 'user_id' => 1 },
'Organization' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'Macro' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'CoreWorkflow' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'Mention' => { 'created_by_id' => 1, 'updated_by_id' => 0, 'user_id' => 1 },
'Channel' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'Role' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'History' => { 'created_by_id' => 6 },
'Webhook' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'Overview' => { 'created_by_id' => 1, 'updated_by_id' => 0 },
'PGPKey' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'AI::Agent' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'AI::Analytics::Usage' => { 'user_id' => 1 },
'AI::TextTool' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'ActivityStream' => { 'created_by_id' => 0 },
'StatsStore' => { 'created_by_id' => 0 },
'TextModule' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'Calendar' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'UserGroup' => { 'user_id' => 1 },
'Signature' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'Authorization' => { 'user_id' => 1 },
'SystemReport' => { 'created_by_id' => 0 },
'Checklist' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'Checklist::Item' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'ChecklistTemplate' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
'ChecklistTemplate::Item' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
}
# delete objects
token = create(:token, user: user)
online_notification = create(:online_notification, user: user)
taskbar = create(:taskbar, :with_ticket, user: user)
user_device = create(:user_device, user: user)
cti_caller_id = create(:cti_caller_id, user: user)
authorization = create(:twitter_authorization, user: user)
recent_view = create(:recent_view, created_by: user)
avatar = create(:avatar, o_id: user.id)
overview = create(:overview, created_by_id: user.id, user_ids: [user.id])
mention = build(:mention, mentionable: create(:ticket), user: user).tap { |elem| elem.save!(validate: false) }
mention_created_by = build(:mention, mentionable: create(:ticket), user: create(:agent), created_by: user).tap { |elem| elem.save!(validate: false) }
user_created_by = create(:customer, created_by_id: user.id, updated_by_id: user.id, out_of_office_replacement_id: user.id)
chat_session = create(:'chat/session', user: user)
chat_message = create(:'chat/message', chat_session: chat_session)
chat_message2 = create(:'chat/message', chat_session: chat_session, created_by: user)
draft_start = create(:ticket_shared_draft_start, created_by: user)
draft_zoom = create(:ticket_shared_draft_zoom, created_by: user)
public_link = create(:public_link, created_by: user)
user_two_factor_preference = create(:user_two_factor_preference, :authenticator_app, user: user)
user_overview_sorting = create(:'user/overview_sorting', user: user)
recent_close = create(:recent_close, user: user)
ai_usage = create(:ai_analytics_usage, user: user)
expect(overview.reload.user_ids).to eq([user.id])
# create a chat agent for admin user (id=1) before agent user
# to be sure that the data gets removed and not mapped which
# would result in a foreign key because of the unique key on the
# created_by_id and updated_by_id.
create(:'chat/agent')
chat_agent_user = create(:'chat/agent', created_by_id: user.id, updated_by_id: user.id)
# invalid user (by email) which has been updated by the user which
# will get deleted (#3935)
invalid_user = build(:user, email: 'abc', created_by_id: user.id, updated_by_id: user.id)
invalid_user.save!(validate: false)
# move ownership objects
group = create(:group, created_by_id: user.id)
job = create(:job, updated_by_id: user.id)
ticket = create(:ticket, group: group_subject, owner: user)
ticket_article = create(:ticket_article, ticket: ticket, created_by_id: user.id, updated_by_id: user.id, origin_by_id: user.id)
customer_ticket1 = create(:ticket, group: group_subject, customer: user)
customer_ticket2 = create(:ticket, group: group_subject, customer: user)
customer_ticket3 = create(:ticket, group: group_subject, customer: user)
knowledge_base_answer = create(:knowledge_base_answer, archived_by_id: user.id, published_by_id: user.id, internal_by_id: user.id)
ticket_state = create(:ticket_state, created_by_id: user.id)
ticket_merged_state = Ticket::State.find_by(name: 'merged').tap { it.update!(updated_by_id: user.id) }
refs_user = Models.references('User', user.id, true)
expect(refs_user).to eq(refs_known)
user.destroy
expect { token.reload }.to raise_exception(ActiveRecord::RecordNotFound)
expect { online_notification.reload }.to raise_exception(ActiveRecord::RecordNotFound)
expect { taskbar.reload }.to raise_exception(ActiveRecord::RecordNotFound)
expect { user_device.reload }.to raise_exception(ActiveRecord::RecordNotFound)
expect { cti_caller_id.reload }.to raise_exception(ActiveRecord::RecordNotFound)
expect { authorization.reload }.to raise_exception(ActiveRecord::RecordNotFound)
expect { recent_view.reload }.to raise_exception(ActiveRecord::RecordNotFound)
expect { avatar.reload }.to raise_exception(ActiveRecord::RecordNotFound)
expect { customer_ticket1.reload }.to raise_exception(ActiveRecord::RecordNotFound)
expect { customer_ticket2.reload }.to raise_exception(ActiveRecord::RecordNotFound)
expect { customer_ticket3.reload }.to raise_exception(ActiveRecord::RecordNotFound)
expect { chat_agent_user.reload }.to raise_exception(ActiveRecord::RecordNotFound)
expect { mention.reload }.to raise_exception(ActiveRecord::RecordNotFound)
expect(mention_created_by.reload.created_by_id).not_to eq(user.id)
expect(overview.reload.user_ids).to eq([])
expect { chat_session.reload }.to raise_exception(ActiveRecord::RecordNotFound)
expect { chat_message.reload }.to raise_exception(ActiveRecord::RecordNotFound)
expect { chat_message2.reload }.to raise_exception(ActiveRecord::RecordNotFound)
expect { user_two_factor_preference.reload }.to raise_exception(ActiveRecord::RecordNotFound)
expect { user_overview_sorting.reload }.to raise_exception(ActiveRecord::RecordNotFound)
expect { recent_close.reload }.to raise_exception(ActiveRecord::RecordNotFound)
expect { ai_usage.reload }.to raise_exception(ActiveRecord::RecordNotFound)
# move ownership objects
expect { group.reload }.to change(group, :created_by_id).to(1)
expect { job.reload }.to change(job, :updated_by_id).to(1)
expect { ticket.reload }.to change(ticket, :owner_id).to(1)
expect { ticket_article.reload }
.to change(ticket_article, :origin_by_id).to(1)
.and change(ticket_article, :updated_by_id).to(1)
.and change(ticket_article, :created_by_id).to(1)
expect { knowledge_base_answer.reload }
.to change(knowledge_base_answer, :archived_by_id).to(1)
.and change(knowledge_base_answer, :published_by_id).to(1)
.and change(knowledge_base_answer, :internal_by_id).to(1)
expect { user_created_by.reload }
.to change(user_created_by, :created_by_id).to(1)
.and change(user_created_by, :updated_by_id).to(1)
.and change(user_created_by, :out_of_office_replacement_id).to(1)
expect { draft_start.reload }.to change(draft_start, :created_by_id).to(1)
expect { draft_zoom.reload }.to change(draft_zoom, :created_by_id).to(1)
expect { invalid_user.reload }.to change(invalid_user, :created_by_id).to(1)
expect { public_link.reload }.to change(public_link, :created_by_id).to(1)
expect { ticket_state.reload }.to change(ticket_state, :created_by_id).to(1)
expect { ticket_merged_state.reload }.to change(ticket_merged_state, :updated_by_id).to(1)
end
it 'does delete cache after user deletion' do
online_notification = create(:online_notification, created_by_id: user.id)
online_notification.attributes_with_association_ids
user.destroy
expect(online_notification.reload.attributes_with_association_ids['created_by_id']).to eq(1)
end
it 'does return an exception on blocking dependencies' do
expect { user.send(:destroy_move_dependency_ownership) }.to raise_error(RuntimeError, 'Failed deleting references! Check logic for UserGroup->user_id.')
end
describe '#organization' do
describe 'email domain-based assignment' do
subject(:user) { build(:user) }
context 'when not set on creation' do
before { user.assign_attributes(organization: nil) }
context 'and #email domain matches an existing Organization#domain' do
before { user.assign_attributes(email: 'user@example.com') }
let(:organization) { create(:organization, domain: 'example.com') }
context 'and Organization#domain_assignment is false (default)' do
before { organization.update(domain_assignment: false) }
it 'remains nil' do
expect { user.save }.not_to change(user, :organization)
end
end
context 'and Organization#domain_assignment is true' do
before { organization.update(domain_assignment: true) }
it 'is automatically set to matching Organization' do
expect { user.save }
.to change(user, :organization).to(organization)
end
end
end
context 'and #email domain doesnt match any Organization#domain' do
before { user.assign_attributes(email: 'user@example.net') }
let(:organization) { create(:organization, domain: 'example.com') }
context 'and Organization#domain_assignment is true' do
before { organization.update(domain_assignment: true) }
it 'remains nil' do
expect { user.save }.not_to change(user, :organization)
end
end
end
end
context 'when set on creation' do
before { user.assign_attributes(organization: specified_organization) }
let(:specified_organization) { create(:organization, domain: 'example.net') }
context 'and #email domain matches a DIFFERENT Organization#domain' do
before { user.assign_attributes(email: 'user@example.com') }
let!(:matching_organization) { create(:organization, domain: 'example.com') }
context 'and Organization#domain_assignment is true' do
before { matching_organization.update(domain_assignment: true) }
it 'is NOT automatically set to matching Organization' do
expect { user.save }
.not_to change(user, :organization).from(specified_organization)
end
end
end
end
end
end
end
describe 'Callbacks, Observers, & Async Transactions -' do
describe 'System-wide agent limit checks:' do
let(:agent_role) { Role.lookup(name: 'Agent') }
let(:admin_role) { Role.lookup(name: 'Admin') }
let(:current_agents) { described_class.with_permissions('ticket.agent') }
describe '#validate_agent_limit_by_role' do
context 'for Integer value of system_agent_limit' do
context 'before exceeding the agent limit' do
before { Setting.set('system_agent_limit', current_agents.count + 1) }
it 'grants agent creation' do
expect { create(:agent) }
.to change(current_agents, :count).by(1)
end
it 'grants role change' do
future_agent = create(:customer)
expect { future_agent.roles = [agent_role] }
.to change(current_agents, :count).by(1)
end
describe 'role updates' do
let(:agent) { create(:agent) }
it 'grants update by instances' do
expect { agent.roles = [admin_role, agent_role] }
.not_to raise_error
end
it 'grants update by id (Integer)' do
expect { agent.role_ids = [admin_role.id, agent_role.id] }
.not_to raise_error
end
it 'grants update by id (String)' do
expect { agent.role_ids = [admin_role.id.to_s, agent_role.id.to_s] }
.not_to raise_error
end
end
end
context 'when exceeding the agent limit' do
it 'creation of new agents' do
Setting.set('system_agent_limit', current_agents.count + 2)
create_list(:agent, 2)
expect { create(:agent) }
.to raise_error(Exceptions::UnprocessableContent)
.and not_change(current_agents, :count)
end
it 'prevents role change' do
Setting.set('system_agent_limit', current_agents.count)
future_agent = create(:customer)
expect { future_agent.roles = [agent_role] }
.to raise_error(Exceptions::UnprocessableContent)
.and not_change(current_agents, :count)
end
end
end
context 'for String value of system_agent_limit' do
context 'before exceeding the agent limit' do
before { Setting.set('system_agent_limit', (current_agents.count + 1).to_s) }
it 'grants agent creation' do
expect { create(:agent) }
.to change(current_agents, :count).by(1)
end
it 'grants role change' do
future_agent = create(:customer)
expect { future_agent.roles = [agent_role] }
.to change(current_agents, :count).by(1)
end
describe 'role updates' do
let(:agent) { create(:agent) }
it 'grants update by instances' do
expect { agent.roles = [admin_role, agent_role] }
.not_to raise_error
end
it 'grants update by id (Integer)' do
expect { agent.role_ids = [admin_role.id, agent_role.id] }
.not_to raise_error
end
it 'grants update by id (String)' do
expect { agent.role_ids = [admin_role.id.to_s, agent_role.id.to_s] }
.not_to raise_error
end
end
end
context 'when exceeding the agent limit' do
it 'creation of new agents' do
Setting.set('system_agent_limit', (current_agents.count + 2).to_s)
create_list(:agent, 2)
expect { create(:agent) }
.to raise_error(Exceptions::UnprocessableContent)
.and not_change(current_agents, :count)
end
it 'prevents role change' do
Setting.set('system_agent_limit', current_agents.count.to_s)
future_agent = create(:customer)
expect { future_agent.roles = [agent_role] }
.to raise_error(Exceptions::UnprocessableContent)
.and not_change(current_agents, :count)
end
end
context 'when limit was exceeded but users where removed' do
let(:agent_1) { create(:agent) }
let(:agent_2) { create(:agent) }
before do
agent_1 && agent_2
Setting.set('system_agent_limit', current_agents.count)
end
it 'allows to create a new agent after destroying agents to be under the limit' do
agent_1.destroy!
agent_2.destroy!
expect { create(:agent) }
.not_to raise_error
end
end
end
end
describe '#validate_agent_limit_by_attributes' do
context 'for Integer value of system_agent_limit' do
before { Setting.set('system_agent_limit', current_agents.count) }
context 'when exceeding the agent limit' do
it 'prevents re-activation of agents' do
inactive_agent = create(:agent, active: false)
expect { inactive_agent.update!(active: true) }
.to raise_error(Exceptions::UnprocessableContent)
.and not_change(current_agents, :count)
end
end
end
context 'for String value of system_agent_limit' do
before { Setting.set('system_agent_limit', current_agents.count.to_s) }
context 'when exceeding the agent limit' do
it 'prevents re-activation of agents' do
inactive_agent = create(:agent, active: false)
expect { inactive_agent.update!(active: true) }
.to raise_error(Exceptions::UnprocessableContent)
.and not_change(current_agents, :count)
end
end
end
end
end
describe 'Touching associations on update:' do
subject!(:user) { create(:customer) }
let!(:organization) { create(:organization) }
context 'when a customer gets a organization' do
it 'touches its organization' do
expect { user.update(organization: organization) }
.to change { organization.reload.updated_at }
end
end
end
describe 'Cti::CallerId syncing:' do
context 'with a #phone attribute' do
subject(:user) { build(:user, phone: '1234567890') }
it 'adds CallerId record on creation (via Cti::CallerId.add)' do
expect(Cti::CallerId).to receive(:add).with(user)
user.save
end
it 'does not update CallerId record on touch/update (via Cti::CallerId.add)' do
expect(Cti::CallerId).to receive(:add).with(user)
user.save
expect(Cti::CallerId).not_to receive(:add).with(user)
user.touch
end
it 'destroys CallerId record on deletion' do
user.save
expect { user.destroy }
.to change(Cti::CallerId, :count).by(-1)
end
end
end
describe 'Cti::Log syncing:' do
context 'with existing Log records', performs_jobs: true do
context 'for incoming calls from an unknown number' do
let!(:log) { create(:'cti/log', :with_preferences, from: '1234567890', direction: 'in') }
context 'when creating a new user with that number' do
subject(:user) { build(:user, phone: log.from) }
it 'populates #preferences[:from] hash in all associated Log records (in a bg job)' do
expect do
user.save
perform_enqueued_jobs commit_transaction: true
end.to change { log.reload.preferences[:from]&.first }
.to(hash_including('caller_id' => user.phone))
end
end
context 'when updating a user with that number' do
subject(:user) { create(:user) }
it 'populates #preferences[:from] hash in all associated Log records (in a bg job)' do
expect do
user.update(phone: log.from)
perform_enqueued_jobs commit_transaction: true
end.to change { log.reload.preferences[:from]&.first }
.to(hash_including('object' => 'User', 'o_id' => user.id))
end
end
context 'when creating a new user with an empty number' do
subject(:user) { build(:user, phone: '') }
it 'does not modify any Log records' do
expect do
user.save
perform_enqueued_jobs commit_transaction: true
end.not_to change { log.reload.attributes }
end
end
context 'when creating a new user with no number' do
subject(:user) { build(:user, phone: nil) }
it 'does not modify any Log records' do
expect do
user.save
perform_enqueued_jobs commit_transaction: true
end.not_to change { log.reload.attributes }
end
end
end
context 'for incoming calls from the given user' do
subject(:user) { create(:user, phone: '1234567890') }
let!(:logs) { create_list(:'cti/log', 5, :with_preferences, from: user.phone, direction: 'in') }
context 'when updating #phone attribute' do
context 'to another number' do
it 'empties #preferences[:from] hash in all associated Log records (in a bg job)' do
expect do
user.update(phone: '0123456789')
perform_enqueued_jobs commit_transaction: true
end.to change { logs.map(&:reload).map { |log| log.preferences[:from] } }
.to(Array.new(5) { nil })
end
end
context 'to an empty string' do
it 'empties #preferences[:from] hash in all associated Log records (in a bg job)' do
expect do
user.update(phone: '')
perform_enqueued_jobs commit_transaction: true
end.to change { logs.map(&:reload).map { |log| log.preferences[:from] } }
.to(Array.new(5) { nil })
end
end
context 'to nil' do
it 'empties #preferences[:from] hash in all associated Log records (in a bg job)' do
expect do
user.update(phone: nil)
perform_enqueued_jobs commit_transaction: true
end.to change { logs.map(&:reload).map { |log| log.preferences[:from] } }
.to(Array.new(5) { nil })
end
end
end
context 'when updating attributes other than #phone' do
it 'does not modify any Log records' do
expect do
user.update(mobile: '2345678901')
perform_enqueued_jobs commit_transaction: true
end.not_to change { logs.map { |x| x.reload.attributes } }
end
end
end
end
end
end
describe 'Assign user to multiple organizations #1573' do
context 'when importing users via csv' do
let(:organization1) { create(:organization) }
let(:organization2) { create(:organization) }
let(:organization3) { create(:organization) }
let(:organization4) { create(:organization) }
let(:user) { create(:agent, organization: organization1, organizations: [organization2, organization3]) }
def csv_import(string)
User.csv_import(
string: string,
parse_params: {
col_sep: ',',
},
try: false,
delete: false,
)
end
before do
user
end
it 'does not change user on re-import' do
expect { csv_import(described_class.csv_example) }.not_to change { user.reload.updated_at }
end
it 'does not change user on different organization order' do
string = described_class.csv_example
string.sub!(organization3.name, organization2.name)
string.sub!(organization2.name, organization3.name)
expect { csv_import(string) }.not_to change { user.reload.updated_at }
end
it 'does change user on different organizations' do
string = described_class.csv_example
string.sub!(organization2.name, organization4.name)
expect { csv_import(string) }.to change { user.reload.updated_at }
end
end
context 'when creating users' do
it 'does not allow creation without primary organization but secondary organizations' do
expect { create(:agent, organization: nil, organizations: create_list(:organization, 1)) }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Secondary organizations are only allowed when the primary organization is given.')
end
it 'does not allow creation with more than 250 organizations' do
expect { create(:agent, organization: create(:organization), organizations: create_list(:organization, 251)) }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: More than 250 secondary organizations are not allowed.')
end
end
end
describe 'Check default agent notifications preferences' do
context 'when creating users' do
it 'does apply default agent notification to agent preferences' do
user = create(:agent)
expect(user.reload.preferences[:notification_config][:matrix]).to eq(Setting.get('ticket_agent_default_notifications'))
end
it 'does not apply default agent notification to customer preferences' do
user = create(:customer)
expect(user.reload.preferences[:notification_config]).to be_blank
end
end
context 'when adding role to existing user' do
it 'does apply default agent notification to agent preferences (without "ticket.agent" permission before)' do
future_agent = create(:customer)
expect { future_agent.roles = [Role.lookup(name: 'Agent')] }
.to change { future_agent.reload.preferences.dig('notification_config', 'matrix') }
.to Setting.get('ticket_agent_default_notifications')
end
it 'does not apply default agent notification to agent preferences (with "ticket.agent" permission before)' do
agent = create(:agent)
expect { agent.roles = [Role.lookup(name: 'Customer')] }
.not_to change { agent.reload.preferences.dig('notification_config', 'matrix') }
end
end
end
describe 'Sanitizes name attributes for offending URLs' do
shared_examples 'sanitizing user name attributes' do |firstname, lastname|
it 'sanitizes user name attributes' do
expect(user).to have_attributes(firstname: firstname, lastname: lastname)
end
end
context 'with firstname attribute only' do
let(:user) { create(:customer, firstname: value, lastname: nil, email: Faker::Internet.unique.email) }
context 'when equaling a URL with a scheme' do
let(:value) { 'https://zammad.org/participate' }
it_behaves_like 'sanitizing user name attributes', 'zammad.org/participate'
end
context 'when equaling a URL without a scheme' do
let(:value) { 'zammad.org' }
it_behaves_like 'sanitizing user name attributes', 'zammad.org'
end
context 'when containing a URL with a scheme' do
let(:value) { 'Click here to confirm https://zammad.org/participate then log in' }
it_behaves_like 'sanitizing user name attributes', 'Click', 'here to confirm zammad.org/participate then log in'
end
context 'when containing a URL with an invalid scheme' do
let(:value) { 'A: Testing' }
it_behaves_like 'sanitizing user name attributes', 'A:', 'Testing'
end
end
context 'with lastname attribute only' do
let(:user) { create(:customer, firstname: nil, lastname: value, email: Faker::Internet.unique.email) }
context 'when equaling a URL with a scheme' do
let(:value) { 'https://zammad.org/participate' }
it_behaves_like 'sanitizing user name attributes', nil, 'zammad.org/participate'
end
context 'when equaling a URL without a scheme' do
let(:value) { 'zammad.org' }
it_behaves_like 'sanitizing user name attributes', nil, 'zammad.org'
end
context 'when containing a URL with a scheme' do
let(:value) { 'Click here to confirm https://zammad.org/participate then log in' }
it_behaves_like 'sanitizing user name attributes', 'Click', 'here to confirm zammad.org/participate then log in'
end
end
context 'with both firstname and lastname attribute' do
let(:user) { create(:customer, firstname: firstname, lastname: lastname, email: Faker::Internet.unique.email) }
context 'when equaling a URL with a scheme' do
let(:firstname) { 'Click here to confirm' }
let(:lastname) { 'https://zammad.org/participate' }
it_behaves_like 'sanitizing user name attributes', 'Click here to confirm', 'zammad.org/participate'
end
context 'when equaling a URL without a scheme' do
let(:firstname) { 'zammad.org' }
let(:lastname) { 'Foundation' }
it_behaves_like 'sanitizing user name attributes', 'zammad.org', 'Foundation'
end
context 'when containing a URL with a scheme' do
let(:firstname) { 'Click here to confirm' }
let(:lastname) { 'https://zammad.org/participate then log in' }
it_behaves_like 'sanitizing user name attributes', 'Click here to confirm', 'zammad.org/participate then log in'
end
context 'when containing a URL with an invalid scheme' do
let(:firstname) { 'Dummy R: Berlin' }
let(:lastname) { 'Mail' }
it_behaves_like 'sanitizing user name attributes', 'Dummy R: Berlin', 'Mail'
end
end
end
describe 'Performance: Remove assets which are present in collection assets #5495' do
let!(:user) { create(:user, groups: create_list(:group, 5)) }
it 'does not deliver global assets' do
expect(user.groups).to be_present
expect(user.assets({}).deep_symbolize_keys.keys).not_to include(:TicketPriority, :Role, :TicketState, :Group)
end
end
describe 'Prevent an organization from being both primary and secondary #5254' do
let(:organizations) { create_list(:organization, 3) }
it 'is not allowed to assign the same organization as primary and secondary' do
expect { create(:user, organization_id: organizations.first.id, organization_ids: [organizations.first.id, organizations.second.id]) }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Secondary organizations cannot include the primary organization.')
end
it 'is not allowed to add one of the secondary orgaizations as primary' do
user = create(:user, organization: organizations.first, organizations: [organizations.second])
expect { user.organizations << organizations.first }
.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Secondary organizations cannot include the primary organization.')
end
it 'allows to move organization from secondary to primary' do
user = create(:user, organization: organizations.first, organizations: [organizations.second])
expect { user.update!(organization: organizations.second, organizations: [organizations.first]) }
.not_to raise_error
end
end
describe '#all_organization_ids' do
it 'returns empty array when user has no organizations' do
user = create(:user, organization: nil, organization_ids: [])
expect(user.all_organization_ids).to eq([])
end
it 'returns only primary organization id when user has only primary organization' do
organization = create(:organization)
user = create(:user, organization: organization, organization_ids: [])
expect(user.all_organization_ids).to eq([organization.id])
end
it 'returns both primary and secondary organization ids' do
organization1 = create(:organization)
organization2 = create(:organization)
organization3 = create(:organization)
user = create(:user, organization: organization1, organizations: [organization2, organization3])
expect(user.all_organization_ids).to contain_exactly(organization1.id, organization2.id, organization3.id)
end
end
end