add leave controllers and rspec

This commit is contained in:
Tanmay Deep Sharma
2025-08-18 14:22:27 +05:30
parent 02fa76f3df
commit d5d13792a8
15 changed files with 1387 additions and 0 deletions
+6
View File
@@ -96,6 +96,12 @@ Rails.application.routes.draw do
post :execute, on: :member
end
resources :sla_policies, only: [:index, :create, :show, :update, :destroy]
resources :leaves, only: [:index, :create, :show, :update, :destroy] do
member do
patch :approve
patch :reject
end
end
resources :custom_roles, only: [:index, :create, :show, :update, :destroy]
resources :campaigns, only: [:index, :create, :show, :update, :destroy]
resources :dashboard_apps, only: [:index, :show, :create, :update, :destroy]
@@ -0,0 +1,43 @@
class Api::V1::Accounts::LeavesController < Api::V1::Accounts::EnterpriseAccountsController
before_action :fetch_leave, only: [:show, :update, :destroy, :approve, :reject]
before_action :check_authorization
def index
@leaves = policy_scope(Current.account.leaves)
end
def show; end
def create
@leave = Current.account.leaves.create!(permitted_params.merge(user: current_user))
end
def update
@leave.update!(permitted_params)
end
def destroy
@leave.destroy!
head :ok
end
def approve
@leave.approve!(current_user)
render :show
end
def reject
@leave.reject!(current_user)
render :show
end
private
def permitted_params
params.require(:leave).permit(:start_date, :end_date, :leave_type, :reason)
end
def fetch_leave
@leave = Current.account.leaves.find_by(id: params[:id])
end
end
@@ -5,6 +5,7 @@ module Enterprise::Concerns::Account
has_many :sla_policies, dependent: :destroy_async
has_many :applied_slas, dependent: :destroy_async
has_many :custom_roles, dependent: :destroy_async
has_many :leaves, dependent: :destroy_async, class_name: 'Leave'
has_many :captain_assistants, dependent: :destroy_async, class_name: 'Captain::Assistant'
has_many :captain_assistant_responses, dependent: :destroy_async, class_name: 'Captain::AssistantResponse'
@@ -6,6 +6,8 @@ module Enterprise::Concerns::User
has_many :captain_responses, class_name: 'Captain::AssistantResponse', dependent: :nullify, as: :documentable
has_many :copilot_threads, dependent: :destroy_async
has_many :leaves, dependent: :destroy_async, class_name: 'Leave'
has_many :approved_leaves, class_name: 'Leave', foreign_key: 'approved_by_id', dependent: :nullify, inverse_of: :approver
end
def ensure_installation_pricing_plan_quantity
+105
View File
@@ -0,0 +1,105 @@
# == Schema Information
#
# Table name: leaves
#
# id :bigint not null, primary key
# approved_at :datetime
# end_date :date not null
# leave_type :integer default("annual"), not null
# reason :text
# start_date :date not null
# status :integer default("pending"), not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# approved_by_id :bigint
# user_id :bigint not null
#
# Indexes
#
# index_leaves_on_account_id (account_id)
# index_leaves_on_account_id_and_status (account_id,status)
# index_leaves_on_approved_by_id (approved_by_id)
# index_leaves_on_user_id (user_id)
#
class Leave < ApplicationRecord
belongs_to :account
belongs_to :user
belongs_to :approver, class_name: 'User', foreign_key: 'approved_by_id', optional: true, inverse_of: :approved_leaves
enum leave_type: {
annual: 0,
sick: 1,
personal: 2,
maternity: 3,
paternity: 4,
emergency: 5,
bereavement: 6,
study: 7,
other: 8
}
enum status: {
pending: 0,
approved: 1,
rejected: 2,
cancelled: 3
}
validates :start_date, :end_date, presence: true
validates :leave_type, :status, presence: true
validate :end_date_after_start_date
validate :future_dates_for_pending_leaves
validate :approver_is_admin
scope :for_account, ->(account_id) { where(account_id: account_id) }
scope :for_user, ->(user_id) { where(user_id: user_id) }
scope :by_status, ->(status) { where(status: status) }
scope :by_leave_type, ->(leave_type) { where(leave_type: leave_type) }
scope :in_date_range, ->(start_date, end_date) { where('start_date <= ? AND end_date >= ?', end_date, start_date) }
def approve!(approver)
update!(status: :approved, approved_by_id: approver.id, approved_at: Time.current)
end
def reject!(approver)
update!(status: :rejected, approved_by_id: approver.id, approved_at: Time.current)
end
def duration_in_days
return 0 unless start_date && end_date
(end_date - start_date).to_i + 1
end
def can_be_cancelled?
pending? || (approved? && start_date > Date.current)
end
def overlaps_with?(other_leave)
return false unless other_leave.is_a?(Leave)
start_date <= other_leave.end_date && end_date >= other_leave.start_date
end
private
def end_date_after_start_date
return unless start_date && end_date
errors.add(:end_date, 'must be after start date') if end_date < start_date
end
def future_dates_for_pending_leaves
return unless pending?
errors.add(:start_date, 'must be in the future') if start_date && start_date <= Date.current
end
def approver_is_admin
return unless approved_by_id && approver
account_user = account.account_users.find_by(user: approver)
errors.add(:approved_by, 'must be an administrator') unless account_user&.administrator?
end
end
+59
View File
@@ -0,0 +1,59 @@
class LeavePolicy < ApplicationPolicy
def index?
@account_user.administrator? || @account_user.agent?
end
def show?
@account_user.administrator? || owned_by_user?
end
def create?
@account_user.administrator? || @account_user.agent?
end
def update?
return false unless @record.pending?
@account_user.administrator? || owned_by_user?
end
def destroy?
return false unless @record.can_be_cancelled?
@account_user.administrator? || owned_by_user?
end
def approve?
@account_user.administrator?
end
def reject?
@account_user.administrator?
end
class Scope
attr_reader :user_context, :user, :scope, :account, :account_user
def initialize(user_context, scope)
@user_context = user_context
@user = user_context[:user]
@account = user_context[:account]
@account_user = user_context[:account_user]
@scope = scope
end
def resolve
if @account_user.administrator?
scope.includes(:user, :approver)
else
scope.where(user: @user).includes(:user, :approver)
end
end
end
private
def owned_by_user?
@record&.user_id == @account_user.user_id
end
end
@@ -0,0 +1,3 @@
json.payload do
json.partial! 'api/v1/models/leave', formats: [:json], leave: @leave
end
@@ -0,0 +1,5 @@
json.payload do
json.array! @leaves do |leave|
json.partial! 'api/v1/models/leave', formats: [:json], leave: leave
end
end
@@ -0,0 +1,3 @@
json.payload do
json.partial! 'api/v1/models/leave', formats: [:json], leave: @leave
end
@@ -0,0 +1,3 @@
json.payload do
json.partial! 'api/v1/models/leave', formats: [:json], leave: @leave
end
@@ -0,0 +1,26 @@
json.id leave.id
json.start_date leave.start_date
json.end_date leave.end_date
json.leave_type leave.leave_type
json.status leave.status
json.reason leave.reason
json.duration_in_days leave.duration_in_days
json.approved_at leave.approved_at&.to_i
json.created_at leave.created_at.to_i
json.updated_at leave.updated_at.to_i
json.user do
json.id leave.user.id
json.name leave.user.name
json.email leave.user.email
end
if leave.approver.present?
json.approver do
json.id leave.approver.id
json.name leave.approver.name
json.email leave.approver.email
end
else
json.approver nil
end
@@ -0,0 +1,550 @@
require 'rails_helper'
RSpec.describe 'Leaves API', type: :request do
let(:account) { create(:account) }
let(:administrator) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:other_user) { create(:user, account: account, role: :agent) }
describe 'GET #index' do
before do
create(:leave, account: account, user: agent)
create(:leave, account: account, user: other_user)
end
context 'when authenticated as administrator' do
it 'returns all leaves in the account' do
get "/api/v1/accounts/#{account.id}/leaves",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['leaves'].length).to eq(2)
end
end
context 'when authenticated as agent' do
it 'returns only own leaves' do
get "/api/v1/accounts/#{account.id}/leaves",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['leaves'].length).to eq(1)
expect(body['leaves'][0]['user']['id']).to eq(agent.id)
end
end
context 'when unauthenticated' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/leaves"
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'GET #show' do
let(:leave) { create(:leave, account: account, user: agent) }
let(:other_leave) { create(:leave, account: account, user: other_user) }
context 'when authenticated as administrator' do
it 'shows any leave in the account' do
get "/api/v1/accounts/#{account.id}/leaves/#{leave.id}",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['id']).to eq(leave.id)
end
end
context 'when authenticated as agent viewing own leave' do
it 'shows the leave' do
get "/api/v1/accounts/#{account.id}/leaves/#{leave.id}",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['id']).to eq(leave.id)
expect(body['user']['id']).to eq(agent.id)
end
end
context 'when authenticated as agent viewing other user leave' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/leaves/#{other_leave.id}",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when leave does not exist' do
it 'returns not found' do
get "/api/v1/accounts/#{account.id}/leaves/99999",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:not_found)
end
end
context 'when unauthenticated' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/leaves/#{leave.id}"
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'POST #create' do
let(:valid_params) do
{
leave: {
start_date: 1.week.from_now.to_date,
end_date: 2.weeks.from_now.to_date,
leave_type: 'annual',
reason: 'Annual vacation'
}
}
end
context 'when authenticated as administrator' do
it 'creates a leave for current user' do
expect do
post "/api/v1/accounts/#{account.id}/leaves",
params: valid_params,
headers: administrator.create_new_auth_token
end.to change(Leave, :count).by(1)
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['leave_type']).to eq('annual')
expect(body['user']['id']).to eq(administrator.id)
end
it 'creates leave with other type' do
params = valid_params.merge(leave: valid_params[:leave].merge(leave_type: 'other'))
post "/api/v1/accounts/#{account.id}/leaves",
params: params,
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['leave_type']).to eq('other')
end
end
context 'when authenticated as agent' do
it 'creates a leave for current user' do
expect do
post "/api/v1/accounts/#{account.id}/leaves",
params: valid_params,
headers: agent.create_new_auth_token
end.to change(Leave, :count).by(1)
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['user']['id']).to eq(agent.id)
end
end
context 'with invalid parameters' do
it 'returns validation errors for missing start date' do
invalid_params = valid_params.merge(leave: valid_params[:leave].except(:start_date))
post "/api/v1/accounts/#{account.id}/leaves",
params: invalid_params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unprocessable_entity)
end
it 'returns validation errors for end date before start date' do
invalid_params = valid_params.merge(
leave: valid_params[:leave].merge(
start_date: 2.weeks.from_now.to_date,
end_date: 1.week.from_now.to_date
)
)
post "/api/v1/accounts/#{account.id}/leaves",
params: invalid_params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unprocessable_entity)
end
it 'returns validation errors for past start date on pending leave' do
invalid_params = valid_params.merge(
leave: valid_params[:leave].merge(
start_date: 1.week.ago.to_date,
end_date: Date.current
)
)
post "/api/v1/accounts/#{account.id}/leaves",
params: invalid_params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unprocessable_entity)
end
end
context 'when unauthenticated' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/leaves",
params: valid_params
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'PUT #update' do
let(:leave) { create(:leave, account: account, user: agent, status: :pending) }
let(:other_leave) { create(:leave, account: account, user: other_user, status: :pending) }
let(:approved_leave) { create(:leave, account: account, user: agent, status: :approved) }
let(:update_params) do
{
leave: {
reason: 'Updated reason'
}
}
end
context 'when authenticated as administrator' do
it 'updates any pending leave' do
put "/api/v1/accounts/#{account.id}/leaves/#{leave.id}",
params: update_params,
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['reason']).to eq('Updated reason')
end
it 'cannot update non-pending leaves' do
put "/api/v1/accounts/#{account.id}/leaves/#{approved_leave.id}",
params: update_params,
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated as agent' do
it 'updates own pending leave' do
put "/api/v1/accounts/#{account.id}/leaves/#{leave.id}",
params: update_params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['reason']).to eq('Updated reason')
end
it 'cannot update other user leave' do
put "/api/v1/accounts/#{account.id}/leaves/#{other_leave.id}",
params: update_params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
it 'cannot update non-pending leaves' do
put "/api/v1/accounts/#{account.id}/leaves/#{approved_leave.id}",
params: update_params,
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when leave does not exist' do
it 'returns not found' do
put "/api/v1/accounts/#{account.id}/leaves/99999",
params: update_params,
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:not_found)
end
end
context 'when unauthenticated' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/leaves/#{leave.id}",
params: update_params
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'DELETE #destroy' do
let(:pending_leave) { create(:leave, account: account, user: agent, status: :pending) }
let(:approved_future_leave) do
create(:leave, account: account, user: agent, status: :approved,
start_date: 1.month.from_now.to_date, end_date: 1.month.from_now.to_date + 5.days)
end
let(:approved_past_leave) do
create(:leave, account: account, user: agent, status: :approved,
start_date: 1.week.ago.to_date, end_date: 3.days.ago.to_date)
end
let(:other_leave) { create(:leave, account: account, user: other_user, status: :pending) }
context 'when authenticated as administrator' do
it 'deletes cancellable leaves' do
expect do
delete "/api/v1/accounts/#{account.id}/leaves/#{pending_leave.id}",
headers: administrator.create_new_auth_token
end.to change(Leave, :count).by(-1)
expect(response).to have_http_status(:success)
end
it 'deletes approved future leaves' do
expect do
delete "/api/v1/accounts/#{account.id}/leaves/#{approved_future_leave.id}",
headers: administrator.create_new_auth_token
end.to change(Leave, :count).by(-1)
expect(response).to have_http_status(:success)
end
it 'cannot delete non-cancellable leaves' do
delete "/api/v1/accounts/#{account.id}/leaves/#{approved_past_leave.id}",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated as agent' do
it 'deletes own cancellable leaves' do
expect do
delete "/api/v1/accounts/#{account.id}/leaves/#{pending_leave.id}",
headers: agent.create_new_auth_token
end.to change(Leave, :count).by(-1)
expect(response).to have_http_status(:success)
end
it 'cannot delete other user leaves' do
delete "/api/v1/accounts/#{account.id}/leaves/#{other_leave.id}",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when leave does not exist' do
it 'returns not found' do
delete "/api/v1/accounts/#{account.id}/leaves/99999",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:not_found)
end
end
context 'when unauthenticated' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/leaves/#{pending_leave.id}"
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'PATCH #approve' do
let(:pending_leave) { create(:leave, account: account, user: agent, status: :pending) }
let(:approved_leave) { create(:leave, account: account, user: agent, status: :approved) }
context 'when authenticated as administrator' do
it 'approves pending leave' do
patch "/api/v1/accounts/#{account.id}/leaves/#{pending_leave.id}/approve",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['status']).to eq('approved')
expect(body['approver']['id']).to eq(administrator.id)
expect(body['approved_at']).not_to be_nil
pending_leave.reload
expect(pending_leave.status).to eq('approved')
expect(pending_leave.approver).to eq(administrator)
end
it 'updates approved_at timestamp' do
freeze_time = Time.current
allow(Time).to receive(:current).and_return(freeze_time)
patch "/api/v1/accounts/#{account.id}/leaves/#{pending_leave.id}/approve",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
pending_leave.reload
expect(pending_leave.approved_at.to_i).to eq(freeze_time.to_i)
end
end
context 'when authenticated as agent' do
it 'returns unauthorized' do
patch "/api/v1/accounts/#{account.id}/leaves/#{pending_leave.id}/approve",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when leave does not exist' do
it 'returns not found' do
patch "/api/v1/accounts/#{account.id}/leaves/99999/approve",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:not_found)
end
end
context 'when unauthenticated' do
it 'returns unauthorized' do
patch "/api/v1/accounts/#{account.id}/leaves/#{pending_leave.id}/approve"
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'PATCH #reject' do
let(:pending_leave) { create(:leave, account: account, user: agent, status: :pending) }
let(:rejected_leave) { create(:leave, account: account, user: agent, status: :rejected) }
context 'when authenticated as administrator' do
it 'rejects pending leave' do
patch "/api/v1/accounts/#{account.id}/leaves/#{pending_leave.id}/reject",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['status']).to eq('rejected')
expect(body['approver']['id']).to eq(administrator.id)
expect(body['approved_at']).not_to be_nil
pending_leave.reload
expect(pending_leave.status).to eq('rejected')
expect(pending_leave.approver).to eq(administrator)
end
it 'updates approved_at timestamp on rejection' do
freeze_time = Time.current
allow(Time).to receive(:current).and_return(freeze_time)
patch "/api/v1/accounts/#{account.id}/leaves/#{pending_leave.id}/reject",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
pending_leave.reload
expect(pending_leave.approved_at.to_i).to eq(freeze_time.to_i)
end
end
context 'when authenticated as agent' do
it 'returns unauthorized' do
patch "/api/v1/accounts/#{account.id}/leaves/#{pending_leave.id}/reject",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when leave does not exist' do
it 'returns not found' do
patch "/api/v1/accounts/#{account.id}/leaves/99999/reject",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:not_found)
end
end
context 'when unauthenticated' do
it 'returns unauthorized' do
patch "/api/v1/accounts/#{account.id}/leaves/#{pending_leave.id}/reject"
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'Business logic scenarios' do
describe 'leave overlap detection' do
let(:existing_leave) do
create(:leave, account: account, user: agent, status: :approved,
start_date: 1.month.from_now.to_date,
end_date: 1.month.from_now.to_date + 5.days)
end
it 'allows non-overlapping leaves' do
existing_leave
params = {
leave: {
start_date: 2.months.from_now.to_date,
end_date: 2.months.from_now.to_date + 3.days,
leave_type: 'annual',
reason: 'Second vacation'
}
}
post "/api/v1/accounts/#{account.id}/leaves",
params: params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:success)
end
end
describe 'leave duration calculation' do
it 'includes duration in response' do
params = {
leave: {
start_date: 1.week.from_now.to_date,
end_date: 1.week.from_now.to_date + 4.days,
leave_type: 'annual',
reason: 'Five day vacation'
}
}
post "/api/v1/accounts/#{account.id}/leaves",
params: params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body['duration_in_days']).to eq(5)
end
end
describe 'approver validation' do
let(:non_admin_approver) { create(:user, account: account, role: :agent) }
before do
# Ensure approver is linked to account
create(:account_user, account: account, user: non_admin_approver, role: :agent)
create(:account_user, account: account, user: administrator, role: :administrator)
end
it 'rejects leave approval by non-administrator' do
leave = create(:leave, account: account, user: agent, status: :pending)
# Simulate manual update with non-admin approver (would fail validation)
expect do
leave.update!(approved_by_id: non_admin_approver.id, status: :approved)
end.to raise_error(ActiveRecord::RecordInvalid)
end
end
end
end
+288
View File
@@ -0,0 +1,288 @@
require 'rails_helper'
RSpec.describe Leave, type: :model do
let(:account) { create(:account) }
let(:user) { create(:user, account: account) }
let(:approver) { create(:user, account: account, role: :administrator) }
describe '#duration_in_days' do
it 'calculates duration correctly for single day' do
leave = create(:leave, start_date: Date.current, end_date: Date.current)
expect(leave.duration_in_days).to eq(1)
end
it 'calculates duration correctly for multiple days' do
leave = create(:leave, start_date: Date.current, end_date: Date.current + 4.days)
expect(leave.duration_in_days).to eq(5)
end
it 'returns 0 when start_date is nil' do
leave = build(:leave, start_date: nil, end_date: Date.current)
expect(leave.duration_in_days).to eq(0)
end
it 'returns 0 when end_date is nil' do
leave = build(:leave, start_date: Date.current, end_date: nil)
expect(leave.duration_in_days).to eq(0)
end
end
describe '#can_be_cancelled?' do
context 'when leave is pending' do
it 'returns true' do
leave = create(:leave, status: :pending)
expect(leave.can_be_cancelled?).to be true
end
end
context 'when leave is approved' do
it 'returns true for future leaves' do
leave = create(:leave, status: :approved,
start_date: 1.week.from_now.to_date,
end_date: 2.weeks.from_now.to_date)
expect(leave.can_be_cancelled?).to be true
end
it 'returns false for current leaves' do
leave = create(:leave, status: :approved,
start_date: Date.current,
end_date: Date.current + 1.day)
expect(leave.can_be_cancelled?).to be false
end
it 'returns false for past leaves' do
leave = create(:leave, status: :approved,
start_date: 1.week.ago.to_date,
end_date: 3.days.ago.to_date)
expect(leave.can_be_cancelled?).to be false
end
end
context 'when leave is rejected' do
it 'returns false' do
leave = create(:leave, status: :rejected)
expect(leave.can_be_cancelled?).to be false
end
end
context 'when leave is cancelled' do
it 'returns false' do
leave = create(:leave, status: :cancelled)
expect(leave.can_be_cancelled?).to be false
end
end
end
describe '#overlaps_with?' do
let(:base_leave) do
create(:leave, start_date: Date.current + 10.days,
end_date: Date.current + 15.days)
end
it 'returns false for non-Leave objects' do
expect(base_leave.overlaps_with?('not a leave')).to be false
end
it 'detects overlapping leaves - same dates' do
overlapping_leave = build(:leave, start_date: Date.current + 10.days,
end_date: Date.current + 15.days)
expect(base_leave.overlaps_with?(overlapping_leave)).to be true
end
it 'detects overlapping leaves - partial overlap start' do
overlapping_leave = build(:leave, start_date: Date.current + 8.days,
end_date: Date.current + 12.days)
expect(base_leave.overlaps_with?(overlapping_leave)).to be true
end
it 'detects overlapping leaves - partial overlap end' do
overlapping_leave = build(:leave, start_date: Date.current + 13.days,
end_date: Date.current + 18.days)
expect(base_leave.overlaps_with?(overlapping_leave)).to be true
end
it 'detects overlapping leaves - contained within' do
overlapping_leave = build(:leave, start_date: Date.current + 12.days,
end_date: Date.current + 13.days)
expect(base_leave.overlaps_with?(overlapping_leave)).to be true
end
it 'detects overlapping leaves - contains other' do
overlapping_leave = build(:leave, start_date: Date.current + 8.days,
end_date: Date.current + 18.days)
expect(base_leave.overlaps_with?(overlapping_leave)).to be true
end
it 'returns false for non-overlapping leaves - before' do
non_overlapping_leave = build(:leave, start_date: Date.current + 5.days,
end_date: Date.current + 9.days)
expect(base_leave.overlaps_with?(non_overlapping_leave)).to be false
end
it 'returns false for non-overlapping leaves - after' do
non_overlapping_leave = build(:leave, start_date: Date.current + 16.days,
end_date: Date.current + 20.days)
expect(base_leave.overlaps_with?(non_overlapping_leave)).to be false
end
it 'returns false for adjacent leaves - ending where other starts' do
adjacent_leave = build(:leave, start_date: Date.current + 16.days,
end_date: Date.current + 20.days)
expect(base_leave.overlaps_with?(adjacent_leave)).to be false
end
it 'returns false for adjacent leaves - starting where other ends' do
adjacent_leave = build(:leave, start_date: Date.current + 5.days,
end_date: Date.current + 9.days)
expect(base_leave.overlaps_with?(adjacent_leave)).to be false
end
end
describe '#approve!' do
let(:leave) { create(:leave, status: :pending) }
before do
create(:account_user, account: account, user: approver, role: :administrator)
end
it 'updates status to approved' do
leave.approve!(approver)
expect(leave.status).to eq('approved')
end
it 'sets approver' do
leave.approve!(approver)
expect(leave.approver).to eq(approver)
end
it 'sets approved_at timestamp' do
freeze_time = Time.current
allow(Time).to receive(:current).and_return(freeze_time)
leave.approve!(approver)
expect(leave.approved_at.to_i).to eq(freeze_time.to_i)
end
it 'raises error if approver is not admin' do
non_admin = create(:user, account: account, role: :agent)
create(:account_user, account: account, user: non_admin, role: :agent)
expect do
leave.approve!(non_admin)
end.to raise_error(ActiveRecord::RecordInvalid, /must be an administrator/)
end
end
describe '#reject!' do
let(:leave) { create(:leave, status: :pending) }
before do
create(:account_user, account: account, user: approver, role: :administrator)
end
it 'updates status to rejected' do
leave.reject!(approver)
expect(leave.status).to eq('rejected')
end
it 'sets approver' do
leave.reject!(approver)
expect(leave.approver).to eq(approver)
end
it 'sets approved_at timestamp' do
freeze_time = Time.current
allow(Time).to receive(:current).and_return(freeze_time)
leave.reject!(approver)
expect(leave.approved_at.to_i).to eq(freeze_time.to_i)
end
it 'raises error if approver is not admin' do
non_admin = create(:user, account: account, role: :agent)
create(:account_user, account: account, user: non_admin, role: :agent)
expect do
leave.reject!(non_admin)
end.to raise_error(ActiveRecord::RecordInvalid, /must be an administrator/)
end
end
describe 'custom validations' do
describe '#end_date_after_start_date' do
it 'is valid when end_date is after start_date' do
leave = build(:leave, start_date: Date.current, end_date: Date.current + 1.day)
expect(leave).to be_valid
end
it 'is valid when end_date equals start_date' do
leave = build(:leave, start_date: Date.current, end_date: Date.current)
expect(leave).to be_valid
end
it 'is invalid when end_date is before start_date' do
leave = build(:leave, start_date: Date.current + 1.day, end_date: Date.current)
expect(leave).not_to be_valid
expect(leave.errors[:end_date]).to include('must be after start date')
end
it 'skips validation when dates are nil' do
leave = build(:leave, start_date: nil, end_date: nil)
leave.valid?
expect(leave.errors[:end_date]).not_to include('must be after start date')
end
end
describe '#future_dates_for_pending_leaves' do
it 'is valid for future start_date on pending leave' do
leave = build(:leave, status: :pending, start_date: Date.current + 1.day)
expect(leave).to be_valid
end
it 'is invalid for current start_date on pending leave' do
leave = build(:leave, status: :pending, start_date: Date.current)
expect(leave).not_to be_valid
expect(leave.errors[:start_date]).to include('must be in the future')
end
it 'is invalid for past start_date on pending leave' do
leave = build(:leave, status: :pending, start_date: Date.current - 1.day)
expect(leave).not_to be_valid
expect(leave.errors[:start_date]).to include('must be in the future')
end
it 'skips validation for non-pending leaves' do
leave = build(:leave, status: :approved, start_date: Date.current - 1.day)
leave.valid?
expect(leave.errors[:start_date]).not_to include('must be in the future')
end
end
describe '#approver_is_admin' do
let(:admin_user) { create(:user, account: account, role: :administrator) }
let(:agent_user) { create(:user, account: account, role: :agent) }
before do
create(:account_user, account: account, user: admin_user, role: :administrator)
create(:account_user, account: account, user: agent_user, role: :agent)
end
it 'is valid with admin approver' do
leave = build(:leave, account: account, approver: admin_user, status: :approved)
expect(leave).to be_valid
end
it 'is invalid with non-admin approver' do
leave = build(:leave, account: account, approver: agent_user, status: :approved)
expect(leave).not_to be_valid
expect(leave.errors[:approved_by]).to include('must be an administrator')
end
it 'skips validation when approver is nil' do
leave = build(:leave, account: account, approver: nil)
leave.valid?
expect(leave.errors[:approved_by]).not_to include('must be an administrator')
end
end
end
end
@@ -0,0 +1,250 @@
require 'rails_helper'
RSpec.describe LeavePolicy, type: :policy do
let(:account) { create(:account) }
let(:administrator) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:other_agent) { create(:user, account: account, role: :agent) }
let(:admin_account_user) { create(:account_user, account: account, user: administrator, role: :administrator) }
let(:agent_account_user) { create(:account_user, account: account, user: agent, role: :agent) }
let(:other_agent_account_user) { create(:account_user, account: account, user: other_agent, role: :agent) }
let(:admin_context) { { user: administrator, account: account, account_user: admin_account_user } }
let(:agent_context) { { user: agent, account: account, account_user: agent_account_user } }
let(:other_agent_context) { { user: other_agent, account: account, account_user: other_agent_account_user } }
let(:agent_leave) { create(:leave, account: account, user: agent, status: :pending) }
let(:other_agent_leave) { create(:leave, account: account, user: other_agent, status: :pending) }
describe 'update?' do
context 'when leave is pending' do
context 'when user is administrator' do
it 'allows update of any leave' do
policy = LeavePolicy.new(admin_context, agent_leave)
expect(policy.update?).to be true
end
end
context 'when user owns the leave' do
it 'allows update' do
policy = LeavePolicy.new(agent_context, agent_leave)
expect(policy.update?).to be true
end
end
context 'when user does not own the leave' do
it 'denies update' do
policy = LeavePolicy.new(other_agent_context, agent_leave)
expect(policy.update?).to be false
end
end
end
context 'when leave is not pending' do
let(:approved_leave) { create(:leave, account: account, user: agent, status: :approved) }
let(:rejected_leave) { create(:leave, account: account, user: agent, status: :rejected) }
let(:cancelled_leave) { create(:leave, account: account, user: agent, status: :cancelled) }
context 'when user is administrator' do
it 'denies update of approved leave' do
policy = LeavePolicy.new(admin_context, approved_leave)
expect(policy.update?).to be false
end
it 'denies update of rejected leave' do
policy = LeavePolicy.new(admin_context, rejected_leave)
expect(policy.update?).to be false
end
it 'denies update of cancelled leave' do
policy = LeavePolicy.new(admin_context, cancelled_leave)
expect(policy.update?).to be false
end
end
context 'when user owns the leave' do
it 'denies update of approved leave' do
policy = LeavePolicy.new(agent_context, approved_leave)
expect(policy.update?).to be false
end
it 'denies update of rejected leave' do
policy = LeavePolicy.new(agent_context, rejected_leave)
expect(policy.update?).to be false
end
it 'denies update of cancelled leave' do
policy = LeavePolicy.new(agent_context, cancelled_leave)
expect(policy.update?).to be false
end
end
end
end
describe 'destroy?' do
context 'when leave can be cancelled' do
let(:pending_leave) { create(:leave, account: account, user: agent, status: :pending) }
let(:approved_future_leave) do
create(:leave, account: account, user: agent, status: :approved,
start_date: 1.week.from_now.to_date, end_date: 2.weeks.from_now.to_date)
end
context 'when user is administrator' do
it 'allows destroy of pending leave' do
policy = LeavePolicy.new(admin_context, pending_leave)
expect(policy.destroy?).to be true
end
it 'allows destroy of approved future leave' do
policy = LeavePolicy.new(admin_context, approved_future_leave)
expect(policy.destroy?).to be true
end
it 'allows destroy of other user leaves' do
other_leave = create(:leave, account: account, user: other_agent, status: :pending)
policy = LeavePolicy.new(admin_context, other_leave)
expect(policy.destroy?).to be true
end
end
context 'when user owns the leave' do
it 'allows destroy of own pending leave' do
policy = LeavePolicy.new(agent_context, pending_leave)
expect(policy.destroy?).to be true
end
it 'allows destroy of own approved future leave' do
policy = LeavePolicy.new(agent_context, approved_future_leave)
expect(policy.destroy?).to be true
end
end
context 'when user does not own the leave' do
it 'denies destroy of other user leave' do
policy = LeavePolicy.new(other_agent_context, pending_leave)
expect(policy.destroy?).to be false
end
end
end
context 'when leave cannot be cancelled' do
let(:approved_current_leave) do
create(:leave, account: account, user: agent, status: :approved,
start_date: Date.current, end_date: Date.current + 2.days)
end
let(:approved_past_leave) do
create(:leave, account: account, user: agent, status: :approved,
start_date: 1.week.ago.to_date, end_date: 3.days.ago.to_date)
end
let(:rejected_leave) { create(:leave, account: account, user: agent, status: :rejected) }
context 'when user is administrator' do
it 'denies destroy of current approved leave' do
policy = LeavePolicy.new(admin_context, approved_current_leave)
expect(policy.destroy?).to be false
end
it 'denies destroy of past approved leave' do
policy = LeavePolicy.new(admin_context, approved_past_leave)
expect(policy.destroy?).to be false
end
it 'denies destroy of rejected leave' do
policy = LeavePolicy.new(admin_context, rejected_leave)
expect(policy.destroy?).to be false
end
end
context 'when user owns the leave' do
it 'denies destroy of current approved leave' do
policy = LeavePolicy.new(agent_context, approved_current_leave)
expect(policy.destroy?).to be false
end
it 'denies destroy of past approved leave' do
policy = LeavePolicy.new(agent_context, approved_past_leave)
expect(policy.destroy?).to be false
end
it 'denies destroy of rejected leave' do
policy = LeavePolicy.new(agent_context, rejected_leave)
expect(policy.destroy?).to be false
end
end
end
end
describe 'Scope' do
let(:admin_leave) { create(:leave, account: account, user: administrator) }
let(:agent_leave) { create(:leave, account: account, user: agent) }
let(:other_agent_leave) { create(:leave, account: account, user: other_agent) }
before do
admin_leave
agent_leave
other_agent_leave
end
context 'when user is administrator' do
it 'returns all leaves in account with includes' do
scope = LeavePolicy::Scope.new(admin_context, Leave.all)
result = scope.resolve
expect(result).to include(admin_leave, agent_leave, other_agent_leave)
expect(result.includes_values).to include(:user, :approver)
end
end
context 'when user is not administrator' do
it 'returns only own leaves with includes' do
scope = LeavePolicy::Scope.new(agent_context, Leave.all)
result = scope.resolve
expect(result).to include(agent_leave)
expect(result).not_to include(admin_leave, other_agent_leave)
expect(result.includes_values).to include(:user, :approver)
end
it 'filters correctly for other agent' do
scope = LeavePolicy::Scope.new(other_agent_context, Leave.all)
result = scope.resolve
expect(result).to include(other_agent_leave)
expect(result).not_to include(admin_leave, agent_leave)
end
end
context 'scope initialization' do
it 'properly initializes all context variables' do
scope = LeavePolicy::Scope.new(admin_context, Leave.all)
expect(scope.user_context).to eq(admin_context)
expect(scope.user).to eq(administrator)
expect(scope.account).to eq(account)
expect(scope.account_user).to eq(admin_account_user)
expect(scope.scope).to eq(Leave.all)
end
end
end
describe 'ownership checks' do
let(:policy) { LeavePolicy.new(agent_context, agent_leave) }
describe '#owned_by_user?' do
it 'returns true when user owns the leave' do
expect(policy.send(:owned_by_user?)).to be true
end
it 'returns false when user does not own the leave' do
policy = LeavePolicy.new(other_agent_context, agent_leave)
expect(policy.send(:owned_by_user?)).to be false
end
it 'returns false when record is nil' do
policy = LeavePolicy.new(agent_context, nil)
expect(policy.send(:owned_by_user?)).to be false
end
end
end
end
+43
View File
@@ -0,0 +1,43 @@
FactoryBot.define do
factory :leave do
account
user
start_date { 1.week.from_now.to_date }
end_date { 2.weeks.from_now.to_date }
leave_type { :annual }
status { :pending }
reason { 'Annual vacation leave' }
trait :sick do
leave_type { :sick }
reason { 'Sick leave for medical treatment' }
end
trait :emergency do
leave_type { :emergency }
reason { 'Emergency family matter' }
end
trait :other do
leave_type { :other }
reason { 'Other type of leave' }
end
trait :approved do
status { :approved }
approved_by { association(:user) }
approved_at { 1.day.ago }
end
trait :rejected do
status { :rejected }
approved_by { association(:user) }
approved_at { 1.day.ago }
end
trait :past_dates do
start_date { 2.weeks.ago.to_date }
end_date { 1.week.ago.to_date }
end
end
end