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'
|
2019-03-13 23:51:22 +00:00
|
|
|
|
require 'models/concerns/has_object_manager_attributes_validation_examples'
|
2020-02-20 13:34:03 +00:00
|
|
|
|
require 'models/user/has_ticket_create_screen_impact_examples'
|
2020-03-02 14:25:33 +00:00
|
|
|
|
require 'models/user/can_lookup_search_index_attributes_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'
|
2019-03-13 23:51:22 +00:00
|
|
|
|
it_behaves_like 'HasObjectManagerAttributesValidation'
|
2020-02-20 13:34:03 +00:00
|
|
|
|
it_behaves_like 'HasTicketCreateScreenImpact'
|
2020-03-02 14:25:33 +00:00
|
|
|
|
it_behaves_like 'CanLookupSearchIndexAttributes'
|
2017-02-24 17:27:27 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
describe 'Class methods:' do
|
|
|
|
|
describe '.authenticate' do
|
|
|
|
|
subject(:user) { create(:user, password: password) }
|
2019-04-15 01:41:17 +00:00
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
let(:password) { Faker::Internet.password }
|
|
|
|
|
|
|
|
|
|
context 'with valid credentials' do
|
2019-03-18 09:53:00 +00:00
|
|
|
|
context 'using #login' do
|
|
|
|
|
it 'returns the matching user' do
|
|
|
|
|
expect(described_class.authenticate(user.login, password))
|
|
|
|
|
.to eq(user)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'is not case-sensitive' do
|
|
|
|
|
expect(described_class.authenticate(user.login.upcase, password))
|
|
|
|
|
.to eq(user)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'using #email' do
|
|
|
|
|
it 'returns the matching user' do
|
|
|
|
|
expect(described_class.authenticate(user.email, password))
|
|
|
|
|
.to eq(user)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'is not case-sensitive' do
|
|
|
|
|
expect(described_class.authenticate(user.email.upcase, password))
|
|
|
|
|
.to eq(user)
|
|
|
|
|
end
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'but exceeding failed login limit' do
|
|
|
|
|
before { user.update(login_failed: 999) }
|
|
|
|
|
|
|
|
|
|
it 'returns nil' do
|
|
|
|
|
expect(described_class.authenticate(user.login, password))
|
|
|
|
|
.to be(nil)
|
|
|
|
|
end
|
|
|
|
|
end
|
2019-02-26 13:28:11 +00:00
|
|
|
|
|
2019-03-18 09:56:20 +00:00
|
|
|
|
context 'when previous login was' do
|
|
|
|
|
context 'never' do
|
|
|
|
|
it 'updates #last_login and #updated_at' do
|
|
|
|
|
expect { described_class.authenticate(user.login, password) }
|
|
|
|
|
.to change { user.reload.last_login }
|
|
|
|
|
.and change { user.reload.updated_at }
|
|
|
|
|
end
|
2019-02-26 13:28:11 +00:00
|
|
|
|
end
|
|
|
|
|
|
2019-03-18 09:56:20 +00:00
|
|
|
|
context 'less than 10 minutes ago' do
|
|
|
|
|
before do
|
|
|
|
|
described_class.authenticate(user.login, password)
|
|
|
|
|
travel 9.minutes
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'does not update #last_login and #updated_at' do
|
|
|
|
|
expect { described_class.authenticate(user.login, password) }
|
|
|
|
|
.to not_change { user.reload.last_login }
|
|
|
|
|
.and not_change { user.reload.updated_at }
|
|
|
|
|
end
|
|
|
|
|
end
|
2019-02-26 13:28:11 +00:00
|
|
|
|
|
2019-03-18 09:56:20 +00:00
|
|
|
|
context 'more than 10 minutes ago' do
|
|
|
|
|
before do
|
|
|
|
|
described_class.authenticate(user.login, password)
|
|
|
|
|
travel 11.minutes
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'updates #last_login and #updated_at' do
|
|
|
|
|
expect { described_class.authenticate(user.login, password) }
|
|
|
|
|
.to change { user.reload.last_login }
|
|
|
|
|
.and change { user.reload.updated_at }
|
|
|
|
|
end
|
2019-02-26 13:28:11 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'with valid user and invalid password' do
|
|
|
|
|
it 'increments failed login count' do
|
2019-06-27 11:51:29 +00:00
|
|
|
|
expect(described_class).to receive(:sleep).with(1)
|
2019-01-31 04:41:54 +00:00
|
|
|
|
expect { described_class.authenticate(user.login, password.next) }
|
|
|
|
|
.to change { user.reload.login_failed }.by(1)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
it 'returns nil' do
|
2019-06-27 11:51:29 +00:00
|
|
|
|
expect(described_class).to receive(:sleep).with(1)
|
2019-01-31 04:41:54 +00:00
|
|
|
|
expect(described_class.authenticate(user.login, password.next)).to be(nil)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'with inactive user’s login' do
|
|
|
|
|
before { user.update(active: false) }
|
|
|
|
|
|
|
|
|
|
it 'returns nil' do
|
|
|
|
|
expect(described_class.authenticate(user.login, password)).to be(nil)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'with non-existent user login' do
|
|
|
|
|
it 'returns nil' do
|
|
|
|
|
expect(described_class.authenticate('john.doe', password)).to be(nil)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'with empty login string' do
|
|
|
|
|
it 'returns nil' do
|
|
|
|
|
expect(described_class.authenticate('', password)).to be(nil)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'with empty password string' do
|
|
|
|
|
it 'returns nil' do
|
|
|
|
|
expect(described_class.authenticate(user.login, '')).to be(nil)
|
|
|
|
|
end
|
|
|
|
|
end
|
2019-06-27 11:51:29 +00:00
|
|
|
|
|
|
|
|
|
context 'with empty password string when the stored password is an empty string' do
|
|
|
|
|
before { user.update_column(:password, '') }
|
|
|
|
|
|
|
|
|
|
context 'when password is an empty string' do
|
|
|
|
|
it 'returns nil' do
|
|
|
|
|
expect(described_class.authenticate(user.login, '')).to be(nil)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'when password is nil' do
|
|
|
|
|
it 'returns nil' do
|
|
|
|
|
expect(described_class.authenticate(user.login, nil)).to be(nil)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'with empty password string when the stored hash represents an empty string' do
|
|
|
|
|
before { user.update(password: PasswordHash.crypt('')) }
|
|
|
|
|
|
|
|
|
|
context 'when password is an empty string' do
|
|
|
|
|
it 'returns nil' do
|
|
|
|
|
expect(described_class.authenticate(user.login, '')).to be(nil)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'when password is nil' do
|
|
|
|
|
it 'returns nil' do
|
|
|
|
|
expect(described_class.authenticate(user.login, nil)).to be(nil)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
describe 'Instance methods:' do
|
|
|
|
|
describe '#max_login_failed?' do
|
|
|
|
|
it { is_expected.to respond_to(:max_login_failed?) }
|
|
|
|
|
|
|
|
|
|
context 'with "password_max_login_failed" setting' do
|
2019-04-15 01:41:17 +00:00
|
|
|
|
before do
|
|
|
|
|
Setting.set('password_max_login_failed', 5)
|
|
|
|
|
user.update(login_failed: 5)
|
|
|
|
|
end
|
2019-01-31 04:41:54 +00:00
|
|
|
|
|
|
|
|
|
it 'returns true once user’s #login_failed count exceeds the setting' do
|
|
|
|
|
expect { user.update(login_failed: 6) }
|
2019-04-15 01:41:17 +00:00
|
|
|
|
.to change(user, :max_login_failed?).to(true)
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'without password_max_login_failed setting' do
|
2019-04-15 01:41:17 +00:00
|
|
|
|
before do
|
|
|
|
|
Setting.set('password_max_login_failed', nil)
|
|
|
|
|
user.update(login_failed: 0)
|
|
|
|
|
end
|
2019-01-31 04:41:54 +00:00
|
|
|
|
|
|
|
|
|
it 'defaults to 0' do
|
|
|
|
|
expect { user.update(login_failed: 1) }
|
2019-04-15 01:41:17 +00:00
|
|
|
|
.to change(user, :max_login_failed?).to(true)
|
2019-01-31 04:41:54 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
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
|
|
|
|
|
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
|
|
|
|
|
expect(user.out_of_office_agent).to be(nil)
|
|
|
|
|
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
|
|
|
|
|
expect(user.out_of_office_agent).to be(nil)
|
|
|
|
|
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
|
|
|
|
|
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
|
|
|
|
|
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
|
|
|
|
|
expect(described_class.by_reset_token('not-existing')).to be(nil)
|
|
|
|
|
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
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
context 'when given a sub-permission (i.e., child permission)' do
|
|
|
|
|
let(:subpermission) { create(:permission, name: 'foo.bar') }
|
|
|
|
|
|
|
|
|
|
context 'that exists' do
|
|
|
|
|
before { subpermission }
|
|
|
|
|
|
|
|
|
|
it 'returns true' do
|
|
|
|
|
expect(user.permissions?('foo.bar')).to be(true)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'that is inactive' do
|
|
|
|
|
before { subpermission.update(active: false) }
|
|
|
|
|
|
|
|
|
|
it 'returns false' do
|
|
|
|
|
expect(user.permissions?('foo.bar')).to be(false)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
context 'that does not exist' do
|
|
|
|
|
it 'returns true' do
|
|
|
|
|
expect(user.permissions?('foo.bar')).to be(true)
|
|
|
|
|
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
|
|
|
|
|
expect(user.permissions?('foo.bar')).to be(false)
|
|
|
|
|
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
|
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
|
|
|
|
|
expect(another_user.password).to eq(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 eq(nil)
|
|
|
|
|
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
|
2017-09-05 09:49:32 +00:00
|
|
|
|
end
|
|
|
|
|
|
2019-01-31 04:41:54 +00:00
|
|
|
|
describe 'Associations:' do
|
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
|
|
|
|
|
|
|
|
|
|
it 'updates CallerId record on touch/update (via Cti::CallerId.build)' do
|
|
|
|
|
user.save
|
|
|
|
|
|
|
|
|
|
expect(Cti::CallerId).to receive(:build).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' 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
|
|
|
|
|
Observer::Transaction.commit
|
|
|
|
|
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)
|
|
|
|
|
Observer::Transaction.commit
|
|
|
|
|
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
|
|
|
|
|
Observer::Transaction.commit
|
|
|
|
|
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
|
|
|
|
|
Observer::Transaction.commit
|
|
|
|
|
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')
|
|
|
|
|
Observer::Transaction.commit
|
|
|
|
|
Scheduler.worker(true)
|
|
|
|
|
end.to change { logs.map(&:reload).map(&:preferences) }
|
|
|
|
|
.to(Array.new(5) { {} })
|
|
|
|
|
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: '')
|
|
|
|
|
Observer::Transaction.commit
|
|
|
|
|
Scheduler.worker(true)
|
|
|
|
|
end.to change { logs.map(&:reload).map(&:preferences) }
|
|
|
|
|
.to(Array.new(5) { {} })
|
|
|
|
|
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)
|
|
|
|
|
Observer::Transaction.commit
|
|
|
|
|
Scheduler.worker(true)
|
|
|
|
|
end.to change { logs.map(&:reload).map(&:preferences) }
|
|
|
|
|
.to(Array.new(5) { {} })
|
|
|
|
|
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')
|
|
|
|
|
Observer::Transaction.commit
|
|
|
|
|
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
|