add leave controllers and rspec
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user