2022-01-01 13:38:12 +00:00
|
|
|
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
2021-06-01 12:20:20 +00:00
|
|
|
|
|
2017-02-24 17:27:27 +00:00
|
|
|
|
require 'rails_helper'
|
2019-01-22 16:35:01 +00:00
|
|
|
|
require 'models/application_model_examples'
|
2017-06-16 20:43:09 +00:00
|
|
|
|
require 'models/concerns/has_groups_examples'
|
2019-06-28 13:07:14 +00:00
|
|
|
|
require 'models/concerns/has_history_examples'
|
2017-06-16 20:43:09 +00:00
|
|
|
|
require 'models/concerns/has_roles_examples'
|
2017-06-20 15:13:42 +00:00
|
|
|
|
require 'models/concerns/has_groups_permissions_examples'
|
2019-02-12 07:38:59 +00:00
|
|
|
|
require 'models/concerns/has_xss_sanitized_note_examples'
|
2019-01-28 06:04:05 +00:00
|
|
|
|
require 'models/concerns/can_be_imported_examples'
|
2021-08-17 16:37:16 +00:00
|
|
|
|
require 'models/concerns/has_object_manager_attributes_examples'
|
2020-03-02 14:25:33 +00:00
|
|
|
|
require 'models/user/can_lookup_search_index_attributes_examples'
|
2021-03-25 08:30:18 +00:00
|
|
|
|
require 'models/user/performs_geo_lookup_examples'
|
2020-09-08 15:06:23 +00:00
|
|
|
|
require 'models/concerns/has_taskbars_examples'
|
2017-02-24 17:27:27 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
RSpec.describe User, type: :model do
|
2019-04-15 01:41:17 +00:00
|
|
|
|
subject(:user) { create(:user) }
|
|
|
|
|
|
2020-06-19 09:17:18 +00:00
|
|
|
|
let(:customer) { create(:customer) }
|
|
|
|
|
let(:agent) { create(:agent) }
|
|
|
|
|
let(:admin) { create(:admin) }
|
2019-04-15 01:41:17 +00:00
|
|
|
|
|
2019-02-26 11:00:46 +00:00
|
|
|
|
it_behaves_like 'ApplicationModel', can_assets: { associations: :organization }
|
2020-06-19 09:17:18 +00:00
|
|
|
|
it_behaves_like 'HasGroups', group_access_factory: :agent
|
2019-06-28 13:07:14 +00:00
|
|
|
|
it_behaves_like 'HasHistory'
|
2020-06-19 09:17:18 +00:00
|
|
|
|
it_behaves_like 'HasRoles', group_access_factory: :agent
|
2019-02-12 07:38:59 +00:00
|
|
|
|
it_behaves_like 'HasXssSanitizedNote', model_factory: :user
|
2019-01-24 10:13:04 +00:00
|
|
|
|
it_behaves_like 'HasGroups and Permissions', group_access_no_permission_factory: :user
|
2019-01-28 06:04:05 +00:00
|
|
|
|
it_behaves_like 'CanBeImported'
|
2021-08-17 16:37:16 +00:00
|
|
|
|
it_behaves_like 'HasObjectManagerAttributes'
|
2020-03-02 14:25:33 +00:00
|
|
|
|
it_behaves_like 'CanLookupSearchIndexAttributes'
|
2020-09-08 15:06:23 +00:00
|
|
|
|
it_behaves_like 'HasTaskbars'
|
2021-03-01 08:18:40 +00:00
|
|
|
|
it_behaves_like 'UserPerformsGeoLookup'
|
2017-02-24 17:27:27 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
describe 'Class methods:' do
|
|
|
|
|
describe '.identify' do
|
|
|
|
|
it 'returns users by given login' do
|
2019-09-16 15:04:17 +00:00
|
|
|
|
expect(described_class.identify(user.login)).to eq(user)
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'returns users by given email' do
|
2019-09-16 15:04:17 +00:00
|
|
|
|
expect(described_class.identify(user.email)).to eq(user)
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
2021-08-16 06:49:32 +00:00
|
|
|
|
|
|
|
|
|
it 'returns nil for empty username' do
|
2022-03-01 08:00:40 +00:00
|
|
|
|
expect(described_class.identify('')).to be_nil
|
2021-08-16 06:49:32 +00:00
|
|
|
|
end
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
describe 'Instance methods:' do
|
|
|
|
|
|
2019-03-21 15:18:17 +00:00
|
|
|
|
describe '#out_of_office?' do
|
|
|
|
|
context 'without any out_of_office_* attributes set' do
|
|
|
|
|
it 'returns false' do
|
|
|
|
|
expect(agent.out_of_office?).to be(false)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'with valid #out_of_office_* attributes' do
|
|
|
|
|
before do
|
|
|
|
|
agent.update(
|
|
|
|
|
out_of_office_start_at: Time.current.yesterday,
|
|
|
|
|
out_of_office_end_at: Time.current.tomorrow,
|
|
|
|
|
out_of_office_replacement_id: 1
|
|
|
|
|
)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'but #out_of_office: false' do
|
|
|
|
|
before { agent.update(out_of_office: false) }
|
|
|
|
|
|
|
|
|
|
it 'returns false' do
|
|
|
|
|
expect(agent.out_of_office?).to be(false)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'and #out_of_office: true' do
|
|
|
|
|
before { agent.update(out_of_office: true) }
|
|
|
|
|
|
|
|
|
|
it 'returns true' do
|
|
|
|
|
expect(agent.out_of_office?).to be(true)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'after the #out_of_office_end_at time has passed' do
|
|
|
|
|
before { travel 2.days }
|
|
|
|
|
|
|
|
|
|
it 'returns false (even though #out_of_office has not changed)' do
|
|
|
|
|
expect(agent.out_of_office).to be(true)
|
|
|
|
|
expect(agent.out_of_office?).to be(false)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
2021-08-02 13:38:11 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'date range is inclusive' do
|
|
|
|
|
before do
|
|
|
|
|
freeze_time
|
|
|
|
|
|
|
|
|
|
agent.update(
|
|
|
|
|
out_of_office: true,
|
|
|
|
|
out_of_office_start_at: 1.day.from_now.to_date,
|
|
|
|
|
out_of_office_end_at: 1.week.from_now.to_date,
|
|
|
|
|
out_of_office_replacement_id: 1
|
|
|
|
|
)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'today in office' do
|
|
|
|
|
expect(agent).not_to be_out_of_office
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'tomorrow not in office' do
|
|
|
|
|
travel 1.day
|
|
|
|
|
expect(agent).to be_out_of_office
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'after 7 days not in office' do
|
|
|
|
|
travel 7.days
|
|
|
|
|
expect(agent).to be_out_of_office
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'after 8 days in office' do
|
|
|
|
|
travel 8.days
|
|
|
|
|
expect(agent).not_to be_out_of_office
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# https://github.com/zammad/zammad/issues/3590
|
|
|
|
|
context 'when setting the same date' do
|
|
|
|
|
before do
|
|
|
|
|
freeze_time
|
|
|
|
|
|
|
|
|
|
target_date = 1.day.from_now.to_date
|
|
|
|
|
agent.update(
|
|
|
|
|
out_of_office: true,
|
|
|
|
|
out_of_office_start_at: target_date,
|
|
|
|
|
out_of_office_end_at: target_date,
|
|
|
|
|
out_of_office_replacement_id: 1
|
|
|
|
|
)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'agent is out of office tomorrow' do
|
|
|
|
|
travel 1.day
|
|
|
|
|
expect(agent).to be_out_of_office
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'agent is not out of office the day after tomorrow' do
|
|
|
|
|
travel 2.days
|
|
|
|
|
expect(agent).not_to be_out_of_office
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'agent is not out of office today' do
|
|
|
|
|
expect(agent).not_to be_out_of_office
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'given it respects system time zone' do
|
|
|
|
|
before do
|
|
|
|
|
travel_to Time.current.end_of_day
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'agent is in office if in UTC' do
|
|
|
|
|
expect(agent).not_to be_out_of_office
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'agent is out of office if ahead of UTC' do
|
|
|
|
|
travel_to Time.current.end_of_day
|
|
|
|
|
Setting.set('timezone_default', 'Europe/Vilnius')
|
|
|
|
|
|
|
|
|
|
expect(agent).to be_out_of_office
|
|
|
|
|
end
|
|
|
|
|
end
|
2019-03-21 15:18:17 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
describe '#out_of_office_agent' do
|
|
|
|
|
it { is_expected.to respond_to(:out_of_office_agent) }
|
|
|
|
|
|
|
|
|
|
context 'when user has no designated substitute' do
|
|
|
|
|
it 'returns nil' do
|
2022-03-01 08:00:40 +00:00
|
|
|
|
expect(user.out_of_office_agent).to be_nil
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2019-03-21 15:18:17 +00:00
|
|
|
|
context 'when user has designated substitute' do
|
2019-01-31 04:41:54 +00:00
|
|
|
|
subject(:user) do
|
|
|
|
|
create(:user,
|
2019-03-21 15:18:17 +00:00
|
|
|
|
out_of_office: out_of_office,
|
2019-01-31 04:41:54 +00:00
|
|
|
|
out_of_office_start_at: Time.zone.yesterday,
|
|
|
|
|
out_of_office_end_at: Time.zone.tomorrow,
|
|
|
|
|
out_of_office_replacement_id: substitute.id,)
|
|
|
|
|
end
|
|
|
|
|
|
2019-04-15 01:41:17 +00:00
|
|
|
|
let(:substitute) { create(:user) }
|
|
|
|
|
|
2019-03-21 15:18:17 +00:00
|
|
|
|
context 'but is not out of office' do
|
|
|
|
|
let(:out_of_office) { false }
|
|
|
|
|
|
|
|
|
|
it 'returns nil' do
|
2022-03-01 08:00:40 +00:00
|
|
|
|
expect(user.out_of_office_agent).to be_nil
|
2019-03-21 15:18:17 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'and is out of office' do
|
|
|
|
|
let(:out_of_office) { true }
|
|
|
|
|
|
|
|
|
|
it 'returns the designated substitute' do
|
|
|
|
|
expect(user.out_of_office_agent).to eq(substitute)
|
|
|
|
|
end
|
|
|
|
|
end
|
2021-08-27 15:09:11 +00:00
|
|
|
|
|
|
|
|
|
context 'with recursive out of office structure' do
|
|
|
|
|
let(:out_of_office) { true }
|
|
|
|
|
let(:substitute) do
|
|
|
|
|
create(:user,
|
|
|
|
|
out_of_office: out_of_office,
|
|
|
|
|
out_of_office_start_at: Time.zone.yesterday,
|
|
|
|
|
out_of_office_end_at: Time.zone.tomorrow,
|
|
|
|
|
out_of_office_replacement_id: user_active.id,)
|
|
|
|
|
end
|
|
|
|
|
let!(:user_active) { create(:user) }
|
|
|
|
|
|
|
|
|
|
it 'returns the designated substitute recursive' do
|
|
|
|
|
expect(user.out_of_office_agent).to eq(user_active)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'with recursive out of office structure with a endless loop' do
|
|
|
|
|
let(:out_of_office) { true }
|
|
|
|
|
let(:substitute) do
|
|
|
|
|
create(:user,
|
|
|
|
|
out_of_office: out_of_office,
|
|
|
|
|
out_of_office_start_at: Time.zone.yesterday,
|
|
|
|
|
out_of_office_end_at: Time.zone.tomorrow,
|
|
|
|
|
out_of_office_replacement_id: user_active.id,)
|
|
|
|
|
end
|
|
|
|
|
let!(:user_active) do
|
|
|
|
|
create(:user,
|
|
|
|
|
out_of_office: out_of_office,
|
|
|
|
|
out_of_office_start_at: Time.zone.yesterday,
|
|
|
|
|
out_of_office_end_at: Time.zone.tomorrow,
|
|
|
|
|
out_of_office_replacement_id: agent.id,)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
before do
|
|
|
|
|
user_active.update(out_of_office_replacement_id: substitute.id)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'returns the designated substitute recursive with a endless loop' do
|
|
|
|
|
expect(user.out_of_office_agent).to eq(substitute)
|
|
|
|
|
end
|
|
|
|
|
end
|
2021-10-29 08:49:17 +00:00
|
|
|
|
|
|
|
|
|
context 'with stack depth exceeding limit' do
|
|
|
|
|
let(:replacement_chain) do
|
|
|
|
|
user = create(:agent)
|
|
|
|
|
|
|
|
|
|
14
|
|
|
|
|
.times
|
|
|
|
|
.each_with_object([user]) do |_, memo|
|
|
|
|
|
memo << create(:agent, :ooo, ooo_agent: memo.last)
|
|
|
|
|
end
|
|
|
|
|
.reverse
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
let(:ids_executed) { [] }
|
|
|
|
|
|
|
|
|
|
before do
|
|
|
|
|
allow_any_instance_of(described_class).to receive(:out_of_office_agent).and_wrap_original do |method, *args| # rubocop:disable RSpec/AnyInstance
|
|
|
|
|
ids_executed << method.receiver.id
|
|
|
|
|
method.call(*args)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
allow(Rails.logger).to receive(:warn)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'returns the last agent at the limit' do
|
|
|
|
|
expect(replacement_chain.first.out_of_office_agent).to eq replacement_chain[10]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'does not evaluate element beyond the limit' do
|
|
|
|
|
user_beyond_limit = replacement_chain[11]
|
|
|
|
|
|
|
|
|
|
replacement_chain.first.out_of_office_agent
|
|
|
|
|
|
|
|
|
|
expect(ids_executed).not_to include(user_beyond_limit.id)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'does evaluate element within the limit' do
|
|
|
|
|
user_within_limit = replacement_chain[5]
|
|
|
|
|
|
|
|
|
|
replacement_chain.first.out_of_office_agent
|
|
|
|
|
|
|
|
|
|
expect(ids_executed).to include(user_within_limit.id)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'logs error below the limit' do
|
|
|
|
|
replacement_chain.first.out_of_office_agent
|
|
|
|
|
|
|
|
|
|
expect(Rails.logger).to have_received(:warn).with(%r{#{Regexp.escape('Found more than 10 replacement levels for agent')}})
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'does not logs warn within the limit' do
|
|
|
|
|
replacement_chain[10].out_of_office_agent
|
|
|
|
|
|
|
|
|
|
expect(Rails.logger).not_to have_received(:warn)
|
|
|
|
|
end
|
|
|
|
|
end
|
2019-03-21 15:18:17 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
describe '#out_of_office_agent_of' do
|
|
|
|
|
context 'when no other agents are out-of-office' do
|
|
|
|
|
it 'returns an empty ActiveRecord::Relation' do
|
|
|
|
|
expect(agent.out_of_office_agent_of)
|
|
|
|
|
.to be_an(ActiveRecord::Relation)
|
|
|
|
|
.and be_empty
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'when designated as the substitute' do
|
|
|
|
|
let!(:agent_on_holiday) do
|
|
|
|
|
create(
|
2020-06-19 09:17:18 +00:00
|
|
|
|
:agent,
|
2019-03-21 15:18:17 +00:00
|
|
|
|
out_of_office_start_at: Time.current.yesterday,
|
|
|
|
|
out_of_office_end_at: Time.current.tomorrow,
|
|
|
|
|
out_of_office_replacement_id: agent.id,
|
|
|
|
|
out_of_office: out_of_office
|
|
|
|
|
)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'of an in-office agent' do
|
|
|
|
|
let(:out_of_office) { false }
|
|
|
|
|
|
|
|
|
|
it 'returns an empty ActiveRecord::Relation' do
|
|
|
|
|
expect(agent.out_of_office_agent_of)
|
|
|
|
|
.to be_an(ActiveRecord::Relation)
|
|
|
|
|
.and be_empty
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'of an out-of-office agent' do
|
|
|
|
|
let(:out_of_office) { true }
|
|
|
|
|
|
|
|
|
|
it 'returns an ActiveRecord::Relation including that agent' do
|
|
|
|
|
expect(agent.out_of_office_agent_of)
|
|
|
|
|
.to match_array([agent_on_holiday])
|
|
|
|
|
end
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
2021-08-27 15:09:11 +00:00
|
|
|
|
|
|
|
|
|
context 'when inherited' do
|
|
|
|
|
let(:out_of_office) { true }
|
|
|
|
|
let!(:agent_on_holiday_sub) do
|
|
|
|
|
create(
|
|
|
|
|
:agent,
|
|
|
|
|
out_of_office_start_at: Time.current.yesterday,
|
|
|
|
|
out_of_office_end_at: Time.current.tomorrow,
|
|
|
|
|
out_of_office_replacement_id: agent_on_holiday.id,
|
|
|
|
|
out_of_office: out_of_office
|
|
|
|
|
)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'returns an ActiveRecord::Relation including both agents' do
|
|
|
|
|
expect(agent.out_of_office_agent_of)
|
|
|
|
|
.to match_array([agent_on_holiday, agent_on_holiday_sub])
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'when inherited endless loop' do
|
|
|
|
|
let(:out_of_office) { true }
|
|
|
|
|
let!(:agent_on_holiday_sub) do
|
|
|
|
|
create(
|
|
|
|
|
:agent,
|
|
|
|
|
out_of_office_start_at: Time.current.yesterday,
|
|
|
|
|
out_of_office_end_at: Time.current.tomorrow,
|
|
|
|
|
out_of_office_replacement_id: agent_on_holiday.id,
|
|
|
|
|
out_of_office: out_of_office
|
|
|
|
|
)
|
|
|
|
|
end
|
|
|
|
|
let!(:agent_on_holiday_sub2) do
|
|
|
|
|
create(
|
|
|
|
|
:agent,
|
|
|
|
|
out_of_office_start_at: Time.current.yesterday,
|
|
|
|
|
out_of_office_end_at: Time.current.tomorrow,
|
|
|
|
|
out_of_office_replacement_id: agent_on_holiday_sub.id,
|
|
|
|
|
out_of_office: out_of_office
|
|
|
|
|
)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
before do
|
|
|
|
|
agent_on_holiday_sub.update(out_of_office_replacement_id: agent_on_holiday_sub2.id)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'returns an ActiveRecord::Relation including both agents referencing each other' do
|
|
|
|
|
expect(agent_on_holiday_sub.out_of_office_agent_of)
|
|
|
|
|
.to match_array([agent_on_holiday_sub, agent_on_holiday_sub2])
|
|
|
|
|
end
|
|
|
|
|
end
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
describe '#by_reset_token' do
|
|
|
|
|
subject(:user) { token.user }
|
|
|
|
|
|
2019-04-15 01:41:17 +00:00
|
|
|
|
let(:token) { create(:token_password_reset) }
|
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
context 'with a valid token' do
|
|
|
|
|
it 'returns the matching user' do
|
|
|
|
|
expect(described_class.by_reset_token(token.name)).to eq(user)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'with an invalid token' do
|
|
|
|
|
it 'returns nil' do
|
2022-03-01 08:00:40 +00:00
|
|
|
|
expect(described_class.by_reset_token('not-existing')).to be_nil
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
describe '#password_reset_via_token' do
|
|
|
|
|
subject(:user) { token.user }
|
|
|
|
|
|
2019-04-15 01:41:17 +00:00
|
|
|
|
let!(:token) { create(:token_password_reset) }
|
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
it 'changes the password of the token user and destroys the token' do
|
|
|
|
|
expect { described_class.password_reset_via_token(token.name, Faker::Internet.password) }
|
|
|
|
|
.to change { user.reload.password }
|
2019-04-15 01:41:17 +00:00
|
|
|
|
.and change(Token, :count).by(-1)
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
Refactoring: Clarify complex Permission queries
This commit was prepared to facilitate a larger refactoring assignment
as part of the Pundit migration.
As a bonus, it includes some SQL query optimizations!
=== Why was this refactoring necessary?
It's not, strictly speaking.
But the Pundit migration involves taking complex querying logic
and moving it into Scope classes as appropriate,
and deciding where things belong is really difficult
when you can't see what they're doing.
=== So how does this refactoring fix the problem?
* It replaces raw SQL queries with Ruby-esque ActiveRecord queries.
* It replaces complex, procedural code
that's full of loops and obscure local variable assignment
with compact, cleanly-formatted code
that follows Ruby idioms and uses meaningful variable names.
In my opinion, it's much faster and easier
to understand what the code does this way.
=== What kinds of SQL query optimizations are included?
* n+1 query: user_access_token#index instantiated all active permissions
and then called current_user.permissions? on _every single one._
A fresh installation of Zammad contains 57 permissions,
so this was a lot of unnecessary queries.
The method has been rewritten to make only one query instead.
* User#permissions? used to query the DB once
for each argument it was given.
Now, it only queries the DB once, even when given many arguments.
* We had a couple SQL queries that used both #select and #pluck.
(When using #pluck, #select is redundant.)
Removing #select from these calls did not improve performance,
but it did clean up unnecessary code.
2020-07-07 11:14:58 +00:00
|
|
|
|
describe '#permissions' do
|
|
|
|
|
let(:user) { create(:agent).tap { |u| u.roles = [u.roles.first] } }
|
|
|
|
|
let(:role) { user.roles.first }
|
|
|
|
|
let(:permissions) { role.permissions }
|
|
|
|
|
|
2020-08-14 11:12:41 +00:00
|
|
|
|
it 'is a simple association getter' do
|
|
|
|
|
expect(user.permissions).to match_array(permissions)
|
Refactoring: Clarify complex Permission queries
This commit was prepared to facilitate a larger refactoring assignment
as part of the Pundit migration.
As a bonus, it includes some SQL query optimizations!
=== Why was this refactoring necessary?
It's not, strictly speaking.
But the Pundit migration involves taking complex querying logic
and moving it into Scope classes as appropriate,
and deciding where things belong is really difficult
when you can't see what they're doing.
=== So how does this refactoring fix the problem?
* It replaces raw SQL queries with Ruby-esque ActiveRecord queries.
* It replaces complex, procedural code
that's full of loops and obscure local variable assignment
with compact, cleanly-formatted code
that follows Ruby idioms and uses meaningful variable names.
In my opinion, it's much faster and easier
to understand what the code does this way.
=== What kinds of SQL query optimizations are included?
* n+1 query: user_access_token#index instantiated all active permissions
and then called current_user.permissions? on _every single one._
A fresh installation of Zammad contains 57 permissions,
so this was a lot of unnecessary queries.
The method has been rewritten to make only one query instead.
* User#permissions? used to query the DB once
for each argument it was given.
Now, it only queries the DB once, even when given many arguments.
* We had a couple SQL queries that used both #select and #pluck.
(When using #pluck, #select is redundant.)
Removing #select from these calls did not improve performance,
but it did clean up unnecessary code.
2020-07-07 11:14:58 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'for inactive permissions' do
|
2020-08-14 11:12:41 +00:00
|
|
|
|
before { permissions.first.update(active: false) }
|
Refactoring: Clarify complex Permission queries
This commit was prepared to facilitate a larger refactoring assignment
as part of the Pundit migration.
As a bonus, it includes some SQL query optimizations!
=== Why was this refactoring necessary?
It's not, strictly speaking.
But the Pundit migration involves taking complex querying logic
and moving it into Scope classes as appropriate,
and deciding where things belong is really difficult
when you can't see what they're doing.
=== So how does this refactoring fix the problem?
* It replaces raw SQL queries with Ruby-esque ActiveRecord queries.
* It replaces complex, procedural code
that's full of loops and obscure local variable assignment
with compact, cleanly-formatted code
that follows Ruby idioms and uses meaningful variable names.
In my opinion, it's much faster and easier
to understand what the code does this way.
=== What kinds of SQL query optimizations are included?
* n+1 query: user_access_token#index instantiated all active permissions
and then called current_user.permissions? on _every single one._
A fresh installation of Zammad contains 57 permissions,
so this was a lot of unnecessary queries.
The method has been rewritten to make only one query instead.
* User#permissions? used to query the DB once
for each argument it was given.
Now, it only queries the DB once, even when given many arguments.
* We had a couple SQL queries that used both #select and #pluck.
(When using #pluck, #select is redundant.)
Removing #select from these calls did not improve performance,
but it did clean up unnecessary code.
2020-07-07 11:14:58 +00:00
|
|
|
|
|
2020-08-14 11:12:41 +00:00
|
|
|
|
it 'omits them from the returned hash' do
|
|
|
|
|
expect(user.permissions).not_to include(permissions.first)
|
|
|
|
|
end
|
Refactoring: Clarify complex Permission queries
This commit was prepared to facilitate a larger refactoring assignment
as part of the Pundit migration.
As a bonus, it includes some SQL query optimizations!
=== Why was this refactoring necessary?
It's not, strictly speaking.
But the Pundit migration involves taking complex querying logic
and moving it into Scope classes as appropriate,
and deciding where things belong is really difficult
when you can't see what they're doing.
=== So how does this refactoring fix the problem?
* It replaces raw SQL queries with Ruby-esque ActiveRecord queries.
* It replaces complex, procedural code
that's full of loops and obscure local variable assignment
with compact, cleanly-formatted code
that follows Ruby idioms and uses meaningful variable names.
In my opinion, it's much faster and easier
to understand what the code does this way.
=== What kinds of SQL query optimizations are included?
* n+1 query: user_access_token#index instantiated all active permissions
and then called current_user.permissions? on _every single one._
A fresh installation of Zammad contains 57 permissions,
so this was a lot of unnecessary queries.
The method has been rewritten to make only one query instead.
* User#permissions? used to query the DB once
for each argument it was given.
Now, it only queries the DB once, even when given many arguments.
* We had a couple SQL queries that used both #select and #pluck.
(When using #pluck, #select is redundant.)
Removing #select from these calls did not improve performance,
but it did clean up unnecessary code.
2020-07-07 11:14:58 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'for permissions on inactive roles' do
|
|
|
|
|
before { role.update(active: false) }
|
|
|
|
|
|
2020-08-14 11:12:41 +00:00
|
|
|
|
it 'omits them from the returned hash' do
|
|
|
|
|
expect(user.permissions).not_to include(*role.permissions)
|
|
|
|
|
end
|
Refactoring: Clarify complex Permission queries
This commit was prepared to facilitate a larger refactoring assignment
as part of the Pundit migration.
As a bonus, it includes some SQL query optimizations!
=== Why was this refactoring necessary?
It's not, strictly speaking.
But the Pundit migration involves taking complex querying logic
and moving it into Scope classes as appropriate,
and deciding where things belong is really difficult
when you can't see what they're doing.
=== So how does this refactoring fix the problem?
* It replaces raw SQL queries with Ruby-esque ActiveRecord queries.
* It replaces complex, procedural code
that's full of loops and obscure local variable assignment
with compact, cleanly-formatted code
that follows Ruby idioms and uses meaningful variable names.
In my opinion, it's much faster and easier
to understand what the code does this way.
=== What kinds of SQL query optimizations are included?
* n+1 query: user_access_token#index instantiated all active permissions
and then called current_user.permissions? on _every single one._
A fresh installation of Zammad contains 57 permissions,
so this was a lot of unnecessary queries.
The method has been rewritten to make only one query instead.
* User#permissions? used to query the DB once
for each argument it was given.
Now, it only queries the DB once, even when given many arguments.
* We had a couple SQL queries that used both #select and #pluck.
(When using #pluck, #select is redundant.)
Removing #select from these calls did not improve performance,
but it did clean up unnecessary code.
2020-07-07 11:14:58 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2019-03-22 04:36:10 +00:00
|
|
|
|
describe '#permissions?' do
|
|
|
|
|
subject(:user) { create(:user, roles: [role]) }
|
2019-04-15 01:41:17 +00:00
|
|
|
|
|
2019-03-22 04:36:10 +00:00
|
|
|
|
let(:role) { create(:role, permissions: [permission]) }
|
|
|
|
|
let(:permission) { create(:permission, name: permission_name) }
|
|
|
|
|
|
|
|
|
|
context 'with privileges for a root permission (e.g., "foo", not "foo.bar")' do
|
|
|
|
|
let(:permission_name) { 'foo' }
|
|
|
|
|
|
|
|
|
|
context 'when given that exact permission' do
|
|
|
|
|
it 'returns true' do
|
|
|
|
|
expect(user.permissions?('foo')).to be(true)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2020-08-14 11:12:41 +00:00
|
|
|
|
context 'when given an active sub-permission' do
|
|
|
|
|
before { create(:permission, name: 'foo.bar') }
|
2019-03-22 04:36:10 +00:00
|
|
|
|
|
2020-08-14 11:12:41 +00:00
|
|
|
|
it 'returns true' do
|
|
|
|
|
expect(user.permissions?('foo.bar')).to be(true)
|
2019-03-22 04:36:10 +00:00
|
|
|
|
end
|
2020-08-14 11:12:41 +00:00
|
|
|
|
end
|
2019-03-22 04:36:10 +00:00
|
|
|
|
|
2020-08-14 11:12:41 +00:00
|
|
|
|
describe 'chain-of-ancestry quirk' do
|
|
|
|
|
context 'when given an inactive sub-permission' do
|
|
|
|
|
before { create(:permission, name: 'foo.bar.baz', active: false) }
|
2019-03-22 04:36:10 +00:00
|
|
|
|
|
2020-08-14 11:12:41 +00:00
|
|
|
|
it 'returns false, even with active ancestors' do
|
|
|
|
|
expect(user.permissions?('foo.bar.baz')).to be(false)
|
2019-03-22 04:36:10 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2020-08-14 11:12:41 +00:00
|
|
|
|
context 'when given a sub-permission that does not exist' do
|
|
|
|
|
before { create(:permission, name: 'foo.bar', active: false) }
|
|
|
|
|
|
|
|
|
|
it 'can return true, even with inactive ancestors' do
|
|
|
|
|
expect(user.permissions?('foo.bar.baz')).to be(true)
|
2019-03-22 04:36:10 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'when given a glob' do
|
|
|
|
|
context 'matching that permission' do
|
|
|
|
|
it 'returns true' do
|
|
|
|
|
expect(user.permissions?('foo.*')).to be(true)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'NOT matching that permission' do
|
|
|
|
|
it 'returns false' do
|
|
|
|
|
expect(user.permissions?('bar.*')).to be(false)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'with privileges for a sub-permission (e.g., "foo.bar", not "foo")' do
|
|
|
|
|
let(:permission_name) { 'foo.bar' }
|
|
|
|
|
|
|
|
|
|
context 'when given that exact sub-permission' do
|
|
|
|
|
it 'returns true' do
|
|
|
|
|
expect(user.permissions?('foo.bar')).to be(true)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'but the permission is inactive' do
|
|
|
|
|
before { permission.update(active: false) }
|
|
|
|
|
|
|
|
|
|
it 'returns false' do
|
|
|
|
|
expect(user.permissions?('foo.bar')).to be(false)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'when given a sibling sub-permission' do
|
|
|
|
|
let(:sibling_permission) { create(:permission, name: 'foo.baz') }
|
|
|
|
|
|
|
|
|
|
context 'that exists' do
|
|
|
|
|
before { sibling_permission }
|
|
|
|
|
|
|
|
|
|
it 'returns false' do
|
|
|
|
|
expect(user.permissions?('foo.baz')).to be(false)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'that does not exist' do
|
|
|
|
|
it 'returns false' do
|
|
|
|
|
expect(user.permissions?('foo.baz')).to be(false)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'when given the parent permission' do
|
|
|
|
|
it 'returns false' do
|
|
|
|
|
expect(user.permissions?('foo')).to be(false)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'when given a glob' do
|
|
|
|
|
context 'matching that sub-permission' do
|
|
|
|
|
it 'returns true' do
|
|
|
|
|
expect(user.permissions?('foo.*')).to be(true)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'but the permission is inactive' do
|
|
|
|
|
before { permission.update(active: false) }
|
|
|
|
|
|
|
|
|
|
it 'returns false' do
|
2020-08-14 11:12:41 +00:00
|
|
|
|
expect(user.permissions?('foo.*')).to be(false)
|
2019-03-22 04:36:10 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'NOT matching that sub-permission' do
|
|
|
|
|
it 'returns false' do
|
|
|
|
|
expect(user.permissions?('bar.*')).to be(false)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
describe '#permissions_with_child_ids' do
|
|
|
|
|
context 'with privileges for a root permission (e.g., "foo", not "foo.bar")' do
|
|
|
|
|
subject(:user) { create(:user, roles: [role]) }
|
2019-04-15 01:41:17 +00:00
|
|
|
|
|
2019-03-22 04:36:10 +00:00
|
|
|
|
let(:role) { create(:role, permissions: [permission]) }
|
|
|
|
|
let!(:permission) { create(:permission, name: 'foo') }
|
|
|
|
|
let!(:child_permission) { create(:permission, name: 'foo.bar') }
|
|
|
|
|
let!(:inactive_child_permission) { create(:permission, name: 'foo.baz', active: false) }
|
|
|
|
|
|
|
|
|
|
it 'includes the IDs of user’s explicit permissions' do
|
|
|
|
|
expect(user.permissions_with_child_ids)
|
|
|
|
|
.to include(permission.id)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'includes the IDs of user’s active sub-permissions' do
|
|
|
|
|
expect(user.permissions_with_child_ids)
|
|
|
|
|
.to include(child_permission.id)
|
|
|
|
|
.and not_include(inactive_child_permission.id)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
2019-04-01 07:22:29 +00:00
|
|
|
|
|
|
|
|
|
describe '#locale' do
|
|
|
|
|
subject(:user) { create(:user, preferences: preferences) }
|
|
|
|
|
|
|
|
|
|
context 'with no #preferences[:locale]' do
|
|
|
|
|
let(:preferences) { {} }
|
2019-04-15 01:41:17 +00:00
|
|
|
|
|
2020-01-27 09:28:17 +00:00
|
|
|
|
context 'with default locale' do
|
|
|
|
|
before { Setting.set('locale_default', 'foo') }
|
2019-04-01 07:22:29 +00:00
|
|
|
|
|
2020-01-27 09:28:17 +00:00
|
|
|
|
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
|
2019-04-01 07:22:29 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'with a #preferences[:locale]' do
|
|
|
|
|
let(:preferences) { { locale: 'bar' } }
|
|
|
|
|
|
|
|
|
|
it 'returns the user’s configured locale' do
|
|
|
|
|
expect(user.locale).to eq('bar')
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
2021-09-22 12:39:10 +00:00
|
|
|
|
|
|
|
|
|
describe '#check_login' do
|
|
|
|
|
let(:agent) { create(:agent) }
|
|
|
|
|
|
|
|
|
|
it 'does use the origin login' do
|
|
|
|
|
new_agent = create(:agent)
|
|
|
|
|
expect(new_agent.login).not_to end_with('1')
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'does number up agent logins (1)' do
|
|
|
|
|
new_agent = create(:agent, login: agent.login)
|
|
|
|
|
expect(new_agent.login).to eq("#{agent.login}1")
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'does number up agent logins (5)' do
|
|
|
|
|
new_agent = create(:agent, login: agent.login)
|
|
|
|
|
4.times do
|
|
|
|
|
new_agent = create(:agent, login: agent.login)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
expect(new_agent.login).to eq("#{agent.login}5")
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'does backup with uuid in cases of many duplicates' do
|
|
|
|
|
new_agent = create(:agent, login: agent.login)
|
|
|
|
|
20.times do
|
|
|
|
|
new_agent = create(:agent, login: agent.login)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
expect(new_agent.login.sub!(agent.login, '')).to be_a_uuid
|
|
|
|
|
end
|
|
|
|
|
end
|
2021-12-03 13:09:11 +00:00
|
|
|
|
|
|
|
|
|
describe '#check_name' do
|
|
|
|
|
it 'guesses user first/last name with non-ASCII characters' do
|
|
|
|
|
user = create(:user, firstname: 'perkūnas ąžuolas', lastname: '')
|
|
|
|
|
|
|
|
|
|
expect(user).to have_attributes(firstname: 'Perkūnas', lastname: 'Ąžuolas')
|
|
|
|
|
end
|
|
|
|
|
end
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
describe 'Attributes:' do
|
2019-03-21 15:18:17 +00:00
|
|
|
|
describe '#out_of_office' do
|
|
|
|
|
context 'with #out_of_office_start_at: nil' do
|
|
|
|
|
before { agent.update(out_of_office_start_at: nil, out_of_office_end_at: Time.current) }
|
|
|
|
|
|
|
|
|
|
it 'cannot be set to true' do
|
|
|
|
|
expect { agent.update(out_of_office: true) }
|
|
|
|
|
.to raise_error(Exceptions::UnprocessableEntity)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'with #out_of_office_end_at: nil' do
|
|
|
|
|
before { agent.update(out_of_office_start_at: Time.current, out_of_office_end_at: nil) }
|
|
|
|
|
|
|
|
|
|
it 'cannot be set to true' do
|
|
|
|
|
expect { agent.update(out_of_office: true) }
|
|
|
|
|
.to raise_error(Exceptions::UnprocessableEntity)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'when #out_of_office_start_at is AFTER #out_of_office_end_at' do
|
|
|
|
|
before { agent.update(out_of_office_start_at: Time.current.tomorrow, out_of_office_end_at: Time.current.next_month) }
|
|
|
|
|
|
|
|
|
|
it 'cannot be set to true' do
|
|
|
|
|
expect { agent.update(out_of_office: true) }
|
|
|
|
|
.to raise_error(Exceptions::UnprocessableEntity)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'when #out_of_office_start_at is AFTER Time.current' do
|
|
|
|
|
before { agent.update(out_of_office_start_at: Time.current.tomorrow, out_of_office_end_at: Time.current.yesterday) }
|
|
|
|
|
|
|
|
|
|
it 'cannot be set to true' do
|
|
|
|
|
expect { agent.update(out_of_office: true) }
|
|
|
|
|
.to raise_error(Exceptions::UnprocessableEntity)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'when #out_of_office_end_at is BEFORE Time.current' do
|
|
|
|
|
before { agent.update(out_of_office_start_at: Time.current.last_month, out_of_office_end_at: Time.current.yesterday) }
|
|
|
|
|
|
|
|
|
|
it 'cannot be set to true' do
|
|
|
|
|
expect { agent.update(out_of_office: true) }
|
|
|
|
|
.to raise_error(Exceptions::UnprocessableEntity)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
describe '#out_of_office_replacement_id' do
|
|
|
|
|
it 'cannot be set to invalid user ID' do
|
2019-09-16 15:04:17 +00:00
|
|
|
|
expect { agent.update(out_of_office_replacement_id: described_class.pluck(:id).max.next) }
|
2019-03-21 15:18:17 +00:00
|
|
|
|
.to raise_error(ActiveRecord::InvalidForeignKey)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'can be set to a valid user ID' do
|
|
|
|
|
expect { agent.update(out_of_office_replacement_id: 1) }
|
|
|
|
|
.not_to raise_error
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2018-12-13 09:10:32 +00:00
|
|
|
|
describe '#login_failed' do
|
|
|
|
|
before { user.update(login_failed: 1) }
|
2017-02-24 17:27:27 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
it 'is reset to 0 when password is updated' do
|
2018-12-13 09:10:32 +00:00
|
|
|
|
expect { user.update(password: Faker::Internet.password) }
|
2019-04-15 01:41:17 +00:00
|
|
|
|
.to change(user, :login_failed).to(0)
|
2018-12-13 09:10:32 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
2018-03-08 12:23:37 +00:00
|
|
|
|
|
2018-12-13 09:10:32 +00:00
|
|
|
|
describe '#password' do
|
2019-06-27 11:51:29 +00:00
|
|
|
|
let(:password) { Faker::Internet.password }
|
|
|
|
|
|
2018-12-13 09:10:32 +00:00
|
|
|
|
context 'when set to plaintext password' do
|
|
|
|
|
it 'hashes password before saving to DB' do
|
2019-06-27 11:51:29 +00:00
|
|
|
|
user.password = password
|
2018-03-08 12:23:37 +00:00
|
|
|
|
|
2018-12-13 09:10:32 +00:00
|
|
|
|
expect { user.save }
|
2019-06-27 11:51:29 +00:00
|
|
|
|
.to change { PasswordHash.crypted?(user.password) }
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'for existing user records' do
|
|
|
|
|
context 'when changed to empty string' do
|
|
|
|
|
before { user.update(password: password) }
|
|
|
|
|
|
|
|
|
|
it 'keeps previous password' do
|
|
|
|
|
|
|
|
|
|
expect { user.update!(password: '') }
|
|
|
|
|
.not_to change(user, :password)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'when changed to nil' do
|
|
|
|
|
before { user.update(password: password) }
|
|
|
|
|
|
|
|
|
|
it 'keeps previous password' do
|
|
|
|
|
expect { user.update!(password: nil) }
|
|
|
|
|
.not_to change(user, :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
|
2022-03-01 08:00:40 +00:00
|
|
|
|
expect(another_user.password).to be_nil
|
2019-06-27 11:51:29 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'when passed as nil' do
|
|
|
|
|
let(:another_user) { create(:user, password: nil) }
|
|
|
|
|
|
|
|
|
|
it 'sets password to nil' do
|
2022-03-01 08:00:40 +00:00
|
|
|
|
expect(another_user.password).to be_nil
|
2019-06-27 11:51:29 +00:00
|
|
|
|
end
|
2018-12-13 09:10:32 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'when set to SHA2 digest (to facilitate OTRS imports)' do
|
|
|
|
|
it 'does not re-hash before saving' do
|
2019-06-27 11:51:29 +00:00
|
|
|
|
user.password = "{sha2}#{Digest::SHA2.hexdigest(password)}"
|
2018-12-13 09:10:32 +00:00
|
|
|
|
|
2019-04-15 01:41:17 +00:00
|
|
|
|
expect { user.save }.not_to change(user, :password)
|
2018-12-13 09:10:32 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
2017-02-24 17:27:27 +00:00
|
|
|
|
|
2018-12-13 09:10:32 +00:00
|
|
|
|
context 'when set to Argon2 digest' do
|
|
|
|
|
it 'does not re-hash before saving' do
|
2019-06-27 11:51:29 +00:00
|
|
|
|
user.password = PasswordHash.crypt(password)
|
2017-09-05 09:49:32 +00:00
|
|
|
|
|
2019-04-15 01:41:17 +00:00
|
|
|
|
expect { user.save }.not_to change(user, :password)
|
2018-12-13 09:10:32 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
2019-06-27 11:51:29 +00:00
|
|
|
|
|
|
|
|
|
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
|
2017-09-05 09:49:32 +00:00
|
|
|
|
end
|
|
|
|
|
|
2018-12-13 09:10:32 +00:00
|
|
|
|
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
|
2017-09-05 09:49:32 +00:00
|
|
|
|
|
2018-12-13 09:10:32 +00:00
|
|
|
|
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(:build).with(any_args)
|
|
|
|
|
|
|
|
|
|
expect(Cti::CallerId.where(object: 'User', o_id: user.id).count)
|
|
|
|
|
.to eq(0)
|
|
|
|
|
|
|
|
|
|
expect { user.update(phone: new_number) }
|
|
|
|
|
.to change { Cti::CallerId.where(object: 'User', o_id: user.id).count }.by(0)
|
|
|
|
|
|
|
|
|
|
expect(Cti::CallerId).not_to have_received(:build)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'when removed on update' do
|
|
|
|
|
let(:orig_number) { '1234567890' }
|
|
|
|
|
let(:new_number) { nil }
|
2017-09-05 09:49:32 +00:00
|
|
|
|
|
2018-12-13 09:10:32 +00:00
|
|
|
|
before { user } # create user
|
2017-09-05 09:49:32 +00:00
|
|
|
|
|
2018-12-13 09:10:32 +00:00
|
|
|
|
it 'removes corresponding CallerId record' do
|
|
|
|
|
expect { user.update(phone: nil) }
|
|
|
|
|
.to change { Cti::CallerId.where(caller_id: orig_number).count }.by(-1)
|
|
|
|
|
end
|
2017-09-05 09:49:32 +00:00
|
|
|
|
end
|
|
|
|
|
|
2018-12-13 09:10:32 +00:00
|
|
|
|
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
|
2017-09-05 09:49:32 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
2019-03-14 13:01:29 +00:00
|
|
|
|
|
|
|
|
|
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
|
2021-04-12 14:03:51 +00:00
|
|
|
|
|
|
|
|
|
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::UnprocessableEntity, %r{#{value}})
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'prevents update' do
|
|
|
|
|
expect { create(:user).update!(image: value) }.to raise_error(Exceptions::UnprocessableEntity, %r{#{value}})
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
describe '#image_source' do
|
|
|
|
|
|
|
|
|
|
describe 'when value is invalid' do
|
|
|
|
|
let(:value) { 'Th1515n0t4v4l1dh45h' }
|
|
|
|
|
let(:escaped) { Regexp.escape(value) }
|
|
|
|
|
|
2021-07-01 08:58:09 +00:00
|
|
|
|
it 'valid create' do
|
2022-03-01 08:00:40 +00:00
|
|
|
|
expect(create(:user, image_source: 'https://zammad.org/avatar.png').image_source).not_to be_nil
|
2021-04-12 14:03:51 +00:00
|
|
|
|
end
|
|
|
|
|
|
2021-07-01 08:58:09 +00:00
|
|
|
|
it 'removes invalid image source of create' do
|
2022-03-01 08:00:40 +00:00
|
|
|
|
expect(create(:user, image_source: value).image_source).to be_nil
|
2021-07-01 08:58:09 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'removes invalid image source of update' do
|
|
|
|
|
user = create(:user)
|
|
|
|
|
user.update!(image_source: value)
|
2022-03-01 08:00:40 +00:00
|
|
|
|
expect(user.image_source).to be_nil
|
2021-04-12 14:03:51 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
2017-09-05 09:49:32 +00:00
|
|
|
|
end
|
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
describe 'Associations:' do
|
2020-09-08 15:06:23 +00:00
|
|
|
|
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' =>
|
2021-03-02 11:03:21 +00:00
|
|
|
|
{ 'created_by_id' => 1, 'updated_by_id' => 1, 'origin_by_id' => 1 },
|
2020-09-08 15:06:23 +00:00
|
|
|
|
'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 },
|
2022-02-24 11:33:52 +00:00
|
|
|
|
'Ticket::SharedDraftStart' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
|
|
|
|
'Ticket::SharedDraftZoom' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
2020-09-08 15:06:23 +00:00
|
|
|
|
'Ticket::TimeAccounting' => { 'created_by_id' => 0 },
|
|
|
|
|
'Ticket::State' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
|
|
|
|
'Ticket::Flag' => { 'created_by_id' => 0 },
|
|
|
|
|
'PostmasterFilter' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
|
|
|
|
'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 },
|
2020-11-11 13:53:54 +00:00
|
|
|
|
'Template' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
2020-09-08 15:06:23 +00:00
|
|
|
|
'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 },
|
2021-06-15 13:06:44 +00:00
|
|
|
|
'Chat::Message' => { 'created_by_id' => 1 },
|
2020-09-08 15:06:23 +00:00
|
|
|
|
'Chat::Agent' => { 'created_by_id' => 1, 'updated_by_id' => 1 },
|
2021-06-15 13:06:44 +00:00
|
|
|
|
'Chat::Session' => { 'user_id' => 1, 'created_by_id' => 0, 'updated_by_id' => 0 },
|
2020-09-08 15:06:23 +00:00
|
|
|
|
'Tag' => { 'created_by_id' => 0 },
|
|
|
|
|
'Karma::User' => { 'user_id' => 0 },
|
|
|
|
|
'Karma::ActivityLog' => { 'user_id' => 1 },
|
|
|
|
|
'RecentView' => { 'created_by_id' => 1 },
|
|
|
|
|
'KnowledgeBase::Answer::Translation' =>
|
|
|
|
|
{ '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 },
|
2021-06-02 10:25:12 +00:00
|
|
|
|
'User' => { 'created_by_id' => 1, 'out_of_office_replacement_id' => 1, 'updated_by_id' => 1 },
|
2020-09-08 15:06:23 +00:00
|
|
|
|
'Organization' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
|
|
|
|
'Macro' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
2021-08-25 12:24:42 +00:00
|
|
|
|
'CoreWorkflow' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
2021-03-16 08:59:32 +00:00
|
|
|
|
'Mention' => { 'created_by_id' => 1, 'updated_by_id' => 0, 'user_id' => 1 },
|
2020-09-08 15:06:23 +00:00
|
|
|
|
'Channel' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
|
|
|
|
'Role' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
2021-06-02 10:25:12 +00:00
|
|
|
|
'History' => { 'created_by_id' => 4 },
|
2021-02-23 14:52:16 +00:00
|
|
|
|
'Webhook' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
2020-10-15 06:37:21 +00:00
|
|
|
|
'Overview' => { 'created_by_id' => 1, 'updated_by_id' => 0 },
|
2020-09-08 15:06:23 +00:00
|
|
|
|
'ActivityStream' => { 'created_by_id' => 0 },
|
|
|
|
|
'StatsStore' => { 'created_by_id' => 0 },
|
2020-11-11 13:53:54 +00:00
|
|
|
|
'TextModule' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
2020-09-08 15:06:23 +00:00
|
|
|
|
'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 } }
|
|
|
|
|
|
|
|
|
|
# delete objects
|
|
|
|
|
token = create(:token, user: user)
|
|
|
|
|
online_notification = create(:online_notification, user: user)
|
|
|
|
|
taskbar = create(:taskbar, user: user)
|
|
|
|
|
user_device = create(:user_device, user: user)
|
|
|
|
|
karma_activity_log = create(:karma_activity_log, 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)
|
2020-10-15 06:37:21 +00:00
|
|
|
|
overview = create(:overview, created_by_id: user.id, user_ids: [user.id])
|
2021-03-16 08:59:32 +00:00
|
|
|
|
mention = create(:mention, mentionable: create(:ticket), user: user)
|
|
|
|
|
mention_created_by = create(:mention, mentionable: create(:ticket), user: create(:agent), created_by: user)
|
2021-06-02 10:25:12 +00:00
|
|
|
|
user_created_by = create(:customer, created_by_id: user.id, updated_by_id: user.id, out_of_office_replacement_id: user.id)
|
2021-06-15 13:06:44 +00:00
|
|
|
|
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)
|
2020-10-15 06:37:21 +00:00
|
|
|
|
expect(overview.reload.user_ids).to eq([user.id])
|
2020-09-08 15:06:23 +00:00
|
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
|
|
# 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)
|
2021-03-02 11:03:21 +00:00
|
|
|
|
ticket_article = create(:ticket_article, ticket: ticket, created_by_id: user.id, updated_by_id: user.id, origin_by_id: user.id)
|
2020-09-08 15:06:23 +00:00
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
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 { karma_activity_log.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)
|
2021-03-16 08:59:32 +00:00
|
|
|
|
expect { mention.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
|
|
|
|
expect(mention_created_by.reload.created_by_id).not_to eq(user.id)
|
2020-10-15 06:37:21 +00:00
|
|
|
|
expect(overview.reload.user_ids).to eq([])
|
2021-06-15 13:06:44 +00:00
|
|
|
|
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)
|
2020-09-08 15:06:23 +00:00
|
|
|
|
|
|
|
|
|
# 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)
|
2021-03-02 11:03:21 +00:00
|
|
|
|
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)
|
2020-09-08 15:06:23 +00:00
|
|
|
|
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)
|
2021-06-02 10:25:12 +00:00
|
|
|
|
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)
|
2020-09-08 15:06:23 +00:00
|
|
|
|
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
|
|
|
|
|
|
2019-01-22 09:20:14 +00:00
|
|
|
|
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') }
|
2019-04-15 01:41:17 +00:00
|
|
|
|
|
2019-01-22 09:20:14 +00:00
|
|
|
|
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
|
2019-04-15 01:41:17 +00:00
|
|
|
|
expect { user.save }.not_to change(user, :organization)
|
2019-01-22 09:20:14 +00:00
|
|
|
|
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 }
|
2019-04-15 01:41:17 +00:00
|
|
|
|
.to change(user, :organization).to(organization)
|
2019-01-22 09:20:14 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'and #email domain doesn’t match any Organization#domain' do
|
|
|
|
|
before { user.assign_attributes(email: 'user@example.net') }
|
2019-04-15 01:41:17 +00:00
|
|
|
|
|
2019-01-22 09:20:14 +00:00
|
|
|
|
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
|
2019-04-15 01:41:17 +00:00
|
|
|
|
expect { user.save }.not_to change(user, :organization)
|
2019-01-22 09:20:14 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'when set on creation' do
|
|
|
|
|
before { user.assign_attributes(organization: specified_organization) }
|
2019-04-15 01:41:17 +00:00
|
|
|
|
|
2019-01-22 09:20:14 +00:00
|
|
|
|
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') }
|
2019-04-15 01:41:17 +00:00
|
|
|
|
|
2019-01-22 09:20:14 +00:00
|
|
|
|
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 }
|
2019-04-15 01:41:17 +00:00
|
|
|
|
.not_to change(user, :organization).from(specified_organization)
|
2019-01-22 09:20:14 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2019-02-05 07:33:40 +00:00
|
|
|
|
describe 'Callbacks, Observers, & Async Transactions -' do
|
2019-01-31 04:41:54 +00:00
|
|
|
|
describe 'System-wide agent limit checks:' do
|
|
|
|
|
let(:agent_role) { Role.lookup(name: 'Agent') }
|
|
|
|
|
let(:admin_role) { Role.lookup(name: 'Admin') }
|
2019-09-16 15:04:17 +00:00
|
|
|
|
let(:current_agents) { described_class.with_permissions('ticket.agent') }
|
2018-06-22 17:42:28 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
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) }
|
2018-06-22 17:42:28 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
it 'grants agent creation' do
|
2020-06-19 09:17:18 +00:00
|
|
|
|
expect { create(:agent) }
|
2019-04-15 01:41:17 +00:00
|
|
|
|
.to change(current_agents, :count).by(1)
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
2018-06-22 17:42:28 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
it 'grants role change' do
|
2020-06-19 09:17:18 +00:00
|
|
|
|
future_agent = create(:customer)
|
2018-06-22 17:42:28 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
expect { future_agent.roles = [agent_role] }
|
2019-04-15 01:41:17 +00:00
|
|
|
|
.to change(current_agents, :count).by(1)
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
2018-06-22 17:42:28 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
describe 'role updates' do
|
2020-06-19 09:17:18 +00:00
|
|
|
|
let(:agent) { create(:agent) }
|
2018-06-22 17:42:28 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
it 'grants update by instances' do
|
|
|
|
|
expect { agent.roles = [admin_role, agent_role] }
|
|
|
|
|
.not_to raise_error
|
|
|
|
|
end
|
2018-06-22 17:42:28 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
it 'grants update by id (Integer)' do
|
|
|
|
|
expect { agent.role_ids = [admin_role.id, agent_role.id] }
|
|
|
|
|
.not_to raise_error
|
|
|
|
|
end
|
2018-06-22 17:42:28 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
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
|
2018-12-13 09:10:32 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
2018-06-22 17:42:28 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
context 'when exceeding the agent limit' do
|
|
|
|
|
it 'creation of new agents' do
|
|
|
|
|
Setting.set('system_agent_limit', current_agents.count + 2)
|
2018-06-22 17:42:28 +00:00
|
|
|
|
|
2020-06-19 09:17:18 +00:00
|
|
|
|
create_list(:agent, 2)
|
2018-06-22 17:42:28 +00:00
|
|
|
|
|
2020-06-19 09:17:18 +00:00
|
|
|
|
expect { create(:agent) }
|
2019-01-31 04:41:54 +00:00
|
|
|
|
.to raise_error(Exceptions::UnprocessableEntity)
|
2019-04-15 01:41:17 +00:00
|
|
|
|
.and change(current_agents, :count).by(0)
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
2018-06-22 17:42:28 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
it 'prevents role change' do
|
|
|
|
|
Setting.set('system_agent_limit', current_agents.count)
|
2018-06-22 17:42:28 +00:00
|
|
|
|
|
2020-06-19 09:17:18 +00:00
|
|
|
|
future_agent = create(:customer)
|
2018-06-22 17:42:28 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
expect { future_agent.roles = [agent_role] }
|
|
|
|
|
.to raise_error(Exceptions::UnprocessableEntity)
|
2019-04-15 01:41:17 +00:00
|
|
|
|
.and change(current_agents, :count).by(0)
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
2018-12-13 09:10:32 +00:00
|
|
|
|
end
|
2018-06-22 17:42:28 +00:00
|
|
|
|
end
|
2018-09-12 04:23:48 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
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) }
|
2018-09-12 04:23:48 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
it 'grants agent creation' do
|
2020-06-19 09:17:18 +00:00
|
|
|
|
expect { create(:agent) }
|
2019-04-15 01:41:17 +00:00
|
|
|
|
.to change(current_agents, :count).by(1)
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
2018-09-12 04:23:48 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
it 'grants role change' do
|
2020-06-19 09:17:18 +00:00
|
|
|
|
future_agent = create(:customer)
|
2018-09-12 04:23:48 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
expect { future_agent.roles = [agent_role] }
|
2019-04-15 01:41:17 +00:00
|
|
|
|
.to change(current_agents, :count).by(1)
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
2018-09-12 04:23:48 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
describe 'role updates' do
|
2020-06-19 09:17:18 +00:00
|
|
|
|
let(:agent) { create(:agent) }
|
2018-12-13 09:10:32 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
it 'grants update by instances' do
|
|
|
|
|
expect { agent.roles = [admin_role, agent_role] }
|
|
|
|
|
.not_to raise_error
|
|
|
|
|
end
|
2018-09-12 04:23:48 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
it 'grants update by id (Integer)' do
|
|
|
|
|
expect { agent.role_ids = [admin_role.id, agent_role.id] }
|
|
|
|
|
.not_to raise_error
|
|
|
|
|
end
|
2018-09-12 04:23:48 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
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
|
2018-12-13 09:10:32 +00:00
|
|
|
|
end
|
2018-09-12 04:23:48 +00:00
|
|
|
|
end
|
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
context 'when exceeding the agent limit' do
|
|
|
|
|
it 'creation of new agents' do
|
|
|
|
|
Setting.set('system_agent_limit', (current_agents.count + 2).to_s)
|
2018-09-12 04:23:48 +00:00
|
|
|
|
|
2020-06-19 09:17:18 +00:00
|
|
|
|
create_list(:agent, 2)
|
2018-09-12 04:23:48 +00:00
|
|
|
|
|
2020-06-19 09:17:18 +00:00
|
|
|
|
expect { create(:agent) }
|
2019-01-31 04:41:54 +00:00
|
|
|
|
.to raise_error(Exceptions::UnprocessableEntity)
|
2019-04-15 01:41:17 +00:00
|
|
|
|
.and change(current_agents, :count).by(0)
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
2018-09-12 04:23:48 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
it 'prevents role change' do
|
|
|
|
|
Setting.set('system_agent_limit', current_agents.count.to_s)
|
2018-09-12 04:23:48 +00:00
|
|
|
|
|
2020-06-19 09:17:18 +00:00
|
|
|
|
future_agent = create(:customer)
|
2018-09-12 04:23:48 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
expect { future_agent.roles = [agent_role] }
|
|
|
|
|
.to raise_error(Exceptions::UnprocessableEntity)
|
2019-04-15 01:41:17 +00:00
|
|
|
|
.and change(current_agents, :count).by(0)
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
2018-12-13 09:10:32 +00:00
|
|
|
|
end
|
2018-09-12 04:23:48 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
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) }
|
2018-09-12 04:23:48 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
context 'when exceeding the agent limit' do
|
|
|
|
|
it 'prevents re-activation of agents' do
|
2020-06-19 09:17:18 +00:00
|
|
|
|
inactive_agent = create(:agent, active: false)
|
2018-09-12 04:23:48 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
expect { inactive_agent.update!(active: true) }
|
|
|
|
|
.to raise_error(Exceptions::UnprocessableEntity)
|
2019-04-15 01:41:17 +00:00
|
|
|
|
.and change(current_agents, :count).by(0)
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
2018-12-13 09:10:32 +00:00
|
|
|
|
end
|
2018-09-12 04:23:48 +00:00
|
|
|
|
end
|
2018-07-23 08:28:21 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
context 'for String value of system_agent_limit' do
|
|
|
|
|
before { Setting.set('system_agent_limit', current_agents.count.to_s) }
|
2018-07-23 08:28:21 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
context 'when exceeding the agent limit' do
|
|
|
|
|
it 'prevents re-activation of agents' do
|
2020-06-19 09:17:18 +00:00
|
|
|
|
inactive_agent = create(:agent, active: false)
|
2018-07-23 08:28:21 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
expect { inactive_agent.update!(active: true) }
|
|
|
|
|
.to raise_error(Exceptions::UnprocessableEntity)
|
2019-04-15 01:41:17 +00:00
|
|
|
|
.and change(current_agents, :count).by(0)
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
2018-12-13 09:10:32 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
2018-07-23 08:28:21 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
2019-02-05 05:45:04 +00:00
|
|
|
|
|
2019-04-08 05:48:19 +00:00
|
|
|
|
describe 'Touching associations on update:' do
|
2020-06-19 09:17:18 +00:00
|
|
|
|
subject!(:user) { create(:customer) }
|
2019-04-15 01:41:17 +00:00
|
|
|
|
|
2020-02-20 16:46:24 +00:00
|
|
|
|
let!(:organization) { create(:organization) }
|
2019-04-08 05:48:19 +00:00
|
|
|
|
|
2020-02-20 16:46:24 +00:00
|
|
|
|
context 'when a customer gets a organization' do
|
2019-04-08 05:48:19 +00:00
|
|
|
|
it 'touches its organization' do
|
2020-02-20 16:46:24 +00:00
|
|
|
|
expect { user.update(organization: organization) }
|
2019-04-08 05:48:19 +00:00
|
|
|
|
.to change { organization.reload.updated_at }
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2019-02-05 07:33:40 +00:00
|
|
|
|
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.build)' do
|
|
|
|
|
expect(Cti::CallerId).to receive(:build).with(user)
|
|
|
|
|
|
|
|
|
|
user.save
|
|
|
|
|
end
|
|
|
|
|
|
2021-06-23 11:35:27 +00:00
|
|
|
|
it 'does not update CallerId record on touch/update (via Cti::CallerId.build)' do
|
2019-02-05 07:33:40 +00:00
|
|
|
|
expect(Cti::CallerId).to receive(:build).with(user)
|
2021-06-23 11:35:27 +00:00
|
|
|
|
user.save
|
2019-02-05 07:33:40 +00:00
|
|
|
|
|
2021-06-23 11:35:27 +00:00
|
|
|
|
expect(Cti::CallerId).not_to receive(:build).with(user)
|
2019-02-05 07:33:40 +00:00
|
|
|
|
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' 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
|
2021-05-20 06:59:02 +00:00
|
|
|
|
TransactionDispatcher.commit
|
2019-02-05 07:33:40 +00:00
|
|
|
|
Scheduler.worker(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)
|
2021-05-20 06:59:02 +00:00
|
|
|
|
TransactionDispatcher.commit
|
2019-02-05 07:33:40 +00:00
|
|
|
|
Scheduler.worker(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
|
2021-05-20 06:59:02 +00:00
|
|
|
|
TransactionDispatcher.commit
|
2019-02-05 07:33:40 +00:00
|
|
|
|
Scheduler.worker(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
|
2021-05-20 06:59:02 +00:00
|
|
|
|
TransactionDispatcher.commit
|
2019-02-05 07:33:40 +00:00
|
|
|
|
Scheduler.worker(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') }
|
2019-04-15 01:41:17 +00:00
|
|
|
|
|
2019-02-05 07:33:40 +00:00
|
|
|
|
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')
|
2021-05-20 06:59:02 +00:00
|
|
|
|
TransactionDispatcher.commit
|
2019-02-05 07:33:40 +00:00
|
|
|
|
Scheduler.worker(true)
|
2021-05-12 10:31:00 +00:00
|
|
|
|
end.to change { logs.map(&:reload).map { |log| log.preferences[:from] } }
|
|
|
|
|
.to(Array.new(5) { nil })
|
2019-02-05 07:33:40 +00:00
|
|
|
|
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: '')
|
2021-05-20 06:59:02 +00:00
|
|
|
|
TransactionDispatcher.commit
|
2019-02-05 07:33:40 +00:00
|
|
|
|
Scheduler.worker(true)
|
2021-05-12 10:31:00 +00:00
|
|
|
|
end.to change { logs.map(&:reload).map { |log| log.preferences[:from] } }
|
|
|
|
|
.to(Array.new(5) { nil })
|
2019-02-05 07:33:40 +00:00
|
|
|
|
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)
|
2021-05-20 06:59:02 +00:00
|
|
|
|
TransactionDispatcher.commit
|
2019-02-05 07:33:40 +00:00
|
|
|
|
Scheduler.worker(true)
|
2021-05-12 10:31:00 +00:00
|
|
|
|
end.to change { logs.map(&:reload).map { |log| log.preferences[:from] } }
|
|
|
|
|
.to(Array.new(5) { nil })
|
2019-02-05 07:33:40 +00:00
|
|
|
|
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')
|
2021-05-20 06:59:02 +00:00
|
|
|
|
TransactionDispatcher.commit
|
2019-02-05 07:33:40 +00:00
|
|
|
|
Scheduler.worker(true)
|
|
|
|
|
end.not_to change { logs.map(&:reload).map(&:attributes) }
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
2018-07-23 08:28:21 +00:00
|
|
|
|
end
|
2019-02-12 07:38:59 +00:00
|
|
|
|
|
2017-02-24 17:27:27 +00:00
|
|
|
|
end
|