# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ require 'rails_helper' RSpec.describe User::OutOfOffice, type: :model do subject(:user) { create(:user) } let(:agent) { create(:agent) } describe 'Instance methods:' do 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 'when #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 'when #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 'when 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).to have_attributes( out_of_office: true, out_of_office?: false ) end end end end context 'when 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 describe '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 end end describe '#someones_out_of_office_replacement?' do it 'returns true when is replacing someone' do create(:agent).update!( out_of_office: true, out_of_office_start_at: 1.day.ago, out_of_office_end_at: 1.day.from_now, out_of_office_replacement_id: user.id, ) expect(user).to be_someones_out_of_office_replacement end it 'returns false when is not replacing anyone' do expect(user).not_to be_someones_out_of_office_replacement end end 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 context 'when user has designated substitute' do subject(:user) 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: substitute.id,) end let(:substitute) { create(:user) } context 'when 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 'when 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 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 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(User).to receive(:out_of_office_agent).and_wrap_original do |method, **kwargs| ids_executed << method.receiver.id method.call(**kwargs) 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 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( :agent, 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 'with 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 'with 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 contain_exactly(agent_on_holiday) end end 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 contain_exactly(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 contain_exactly(agent_on_holiday_sub, agent_on_holiday_sub2) end end end end end end