Files
HRM-System/tests/Feature/BranchScopedAccessTest.php
admin 33f776c6b2 feat(auth): enforce strict branch scoping for overtime requests, attendance regularizations, and 13th month pay
- Restrict overtime applications query, submission, approval, and attendance details to assigned branch
- Restrict attendance regularization listing, statistics, and dropdowns to assigned branch
- Restrict 13th month pay batch generation, historical runs, entry updates, approvals, and CSV exports to assigned branch
- Add feature tests in BranchScopedAccessTest for overtime and 13th month isolation
2026-09-10 11:22:15 +08:00

474 lines
16 KiB
PHP

<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\User;
use App\Models\Branch;
use App\Models\Department;
use App\Models\Employee;
use App\Models\PayrollRun;
use App\Models\PayrollEntry;
use App\Models\Payslip;
use App\Models\OvertimeApplication;
use App\Models\ThirteenthMonthRun;
use App\Models\ThirteenthMonthEntry;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
class BranchScopedAccessTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
// Ensure permissions exist
Permission::firstOrCreate(['name' => 'manage-branches']);
Permission::firstOrCreate(['name' => 'manage-any-branches']);
Permission::firstOrCreate(['name' => 'manage-departments']);
Permission::firstOrCreate(['name' => 'manage-any-departments']);
Permission::firstOrCreate(['name' => 'manage-employees']);
Permission::firstOrCreate(['name' => 'manage-any-employees']);
Permission::firstOrCreate(['name' => 'manage-payslips']);
Permission::firstOrCreate(['name' => 'manage-any-payslips']);
Permission::firstOrCreate(['name' => 'manage-payroll-runs']);
}
public function test_auth_branch_id_helper_resolution()
{
$company = User::create([
'name' => 'Company Admin',
'email' => 'company_' . uniqid() . '@test.com',
'password' => bcrypt('password'),
'type' => 'company',
]);
$superadmin = User::create([
'name' => 'Super Admin',
'email' => 'admin_' . uniqid() . '@test.com',
'password' => bcrypt('password'),
'type' => 'superadmin',
]);
$branch = Branch::create([
'name' => 'Branch Alpha',
'created_by' => $company->id,
]);
$userDirectBranch = User::create([
'name' => 'Branch User',
'email' => 'hr_' . uniqid() . '@test.com',
'password' => bcrypt('password'),
'type' => 'hr',
'branch_id' => $branch->id,
'created_by' => $company->id,
]);
$this->actingAs($company);
$this->assertNull(authBranchId());
$this->actingAs($superadmin);
$this->assertNull(authBranchId());
$this->actingAs($userDirectBranch);
$this->assertEquals($branch->id, authBranchId());
}
public function test_branch_scoped_user_only_sees_assigned_branch()
{
$company = User::create([
'name' => 'Company Admin',
'email' => 'company_' . uniqid() . '@test.com',
'password' => bcrypt('password'),
'type' => 'company',
]);
$company->givePermissionTo('manage-branches');
$company->givePermissionTo('manage-any-branches');
$branchA = Branch::create([
'name' => 'Branch Alpha',
'created_by' => $company->id,
]);
$branchB = Branch::create([
'name' => 'Branch Beta',
'created_by' => $company->id,
]);
$hrUser = User::create([
'name' => 'HR User Branch A',
'email' => 'hr_' . uniqid() . '@test.com',
'password' => bcrypt('password'),
'type' => 'hr',
'branch_id' => $branchA->id,
'created_by' => $company->id,
]);
$hrUser->givePermissionTo('manage-branches');
$hrUser->givePermissionTo('manage-any-branches');
// Test HR accessing branches index
$response = $this->actingAs($hrUser)->get(route('hr.branches.index'));
$response->assertStatus(200);
$response->assertSee('Branch Alpha');
$response->assertDontSee('Branch Beta');
// Test Company Admin accessing branches index
$companyResponse = $this->actingAs($company)->get(route('hr.branches.index'));
$companyResponse->assertStatus(200);
$companyResponse->assertSee('Branch Alpha');
$companyResponse->assertSee('Branch Beta');
}
public function test_branch_scoped_user_only_sees_assigned_branch_departments_and_employees()
{
$company = User::create([
'name' => 'Company Admin',
'email' => 'company_' . uniqid() . '@test.com',
'password' => bcrypt('password'),
'type' => 'company',
]);
$branchA = Branch::create([
'name' => 'Branch Alpha',
'created_by' => $company->id,
]);
$branchB = Branch::create([
'name' => 'Branch Beta',
'created_by' => $company->id,
]);
$deptA = Department::create([
'name' => 'Dept Alpha Finance',
'branch_id' => $branchA->id,
'created_by' => $company->id,
]);
$deptB = Department::create([
'name' => 'Dept Beta Logistics',
'branch_id' => $branchB->id,
'created_by' => $company->id,
]);
// Employee in Branch A
$userA = User::create([
'name' => 'Employee Alpha User',
'email' => 'alpha_' . uniqid() . '@test.com',
'password' => bcrypt('password'),
'type' => 'employee',
'created_by' => $company->id,
]);
Employee::create([
'user_id' => $userA->id,
'employee_id' => 'EMP-A-' . rand(100, 999),
'branch_id' => $branchA->id,
'department_id' => $deptA->id,
'created_by' => $company->id,
]);
// Employee in Branch B
$userB = User::create([
'name' => 'Employee Beta User',
'email' => 'beta_' . uniqid() . '@test.com',
'password' => bcrypt('password'),
'type' => 'employee',
'created_by' => $company->id,
]);
Employee::create([
'user_id' => $userB->id,
'employee_id' => 'EMP-B-' . rand(100, 999),
'branch_id' => $branchB->id,
'department_id' => $deptB->id,
'created_by' => $company->id,
]);
$hrUser = User::create([
'name' => 'HR Branch A User',
'email' => 'hr_a_' . uniqid() . '@test.com',
'password' => bcrypt('password'),
'type' => 'hr',
'branch_id' => $branchA->id,
'created_by' => $company->id,
]);
$hrUser->givePermissionTo('manage-departments');
$hrUser->givePermissionTo('manage-any-departments');
$hrUser->givePermissionTo('manage-employees');
$hrUser->givePermissionTo('manage-any-employees');
// Department index check
$deptResponse = $this->actingAs($hrUser)->get(route('hr.departments.index'));
$deptResponse->assertStatus(200);
$deptResponse->assertSee('Dept Alpha Finance');
$deptResponse->assertDontSee('Dept Beta Logistics');
// Employee index check
$empResponse = $this->actingAs($hrUser)->get(route('hr.employees.index'));
$empResponse->assertStatus(200);
$empResponse->assertSee('Employee Alpha User');
$empResponse->assertDontSee('Employee Beta User');
}
public function test_branch_scoped_user_only_sees_assigned_branch_payslips()
{
$company = User::create([
'name' => 'Company Admin',
'email' => 'company_' . uniqid() . '@test.com',
'password' => bcrypt('password'),
'type' => 'company',
]);
$branchA = Branch::create([
'name' => 'Branch Alpha',
'created_by' => $company->id,
]);
$branchB = Branch::create([
'name' => 'Branch Beta',
'created_by' => $company->id,
]);
// Employee A
$userA = User::create([
'name' => 'Employee Alpha User',
'email' => 'alpha_' . uniqid() . '@test.com',
'password' => bcrypt('password'),
'type' => 'employee',
'created_by' => $company->id,
]);
Employee::create([
'user_id' => $userA->id,
'employee_id' => 'EMP-A-' . rand(100, 999),
'branch_id' => $branchA->id,
'created_by' => $company->id,
]);
// Employee B
$userB = User::create([
'name' => 'Employee Beta User',
'email' => 'beta_' . uniqid() . '@test.com',
'password' => bcrypt('password'),
'type' => 'employee',
'created_by' => $company->id,
]);
Employee::create([
'user_id' => $userB->id,
'employee_id' => 'EMP-B-' . rand(100, 999),
'branch_id' => $branchB->id,
'created_by' => $company->id,
]);
$payrollRun = PayrollRun::create([
'title' => 'May 1-15 2026',
'payroll_frequency' => 'monthly',
'pay_period_start' => '2026-05-01',
'pay_period_end' => '2026-05-15',
'pay_date' => '2026-05-15',
'status' => 'completed',
'created_by' => $company->id,
]);
$entryA = PayrollEntry::create([
'payroll_run_id' => $payrollRun->id,
'employee_id' => $userA->id,
'basic_salary' => 10000,
'created_by' => $company->id,
]);
$entryB = PayrollEntry::create([
'payroll_run_id' => $payrollRun->id,
'employee_id' => $userB->id,
'basic_salary' => 10000,
'created_by' => $company->id,
]);
// Payslips
$payslipA = Payslip::create([
'payroll_entry_id' => $entryA->id,
'employee_id' => $userA->id,
'payslip_number' => 'PAY-ALPHA-001',
'pay_period_start' => '2026-05-01',
'pay_period_end' => '2026-05-15',
'pay_date' => '2026-05-15',
'status' => 'generated',
'created_by' => $company->id,
]);
$payslipB = Payslip::create([
'payroll_entry_id' => $entryB->id,
'employee_id' => $userB->id,
'payslip_number' => 'PAY-BETA-002',
'pay_period_start' => '2026-05-01',
'pay_period_end' => '2026-05-15',
'pay_date' => '2026-05-15',
'status' => 'generated',
'created_by' => $company->id,
]);
$hrUser = User::create([
'name' => 'HR Branch A User',
'email' => 'hr_a_' . uniqid() . '@test.com',
'password' => bcrypt('password'),
'type' => 'hr',
'branch_id' => $branchA->id,
'created_by' => $company->id,
]);
$hrUser->givePermissionTo('manage-payslips');
$hrUser->givePermissionTo('manage-any-payslips');
$response = $this->actingAs($hrUser)->get(route('hr.payslips.index'));
$response->assertStatus(200);
$response->assertSee('PAY-ALPHA-001');
$response->assertDontSee('PAY-BETA-002');
}
public function test_branch_scoped_user_only_sees_assigned_branch_overtime_requests()
{
$company = User::create([
'name' => 'Company Admin',
'email' => 'company_' . uniqid() . '@test.com',
'password' => bcrypt('password'),
'type' => 'company',
]);
$branchA = Branch::create([
'name' => 'Branch Alpha',
'created_by' => $company->id,
]);
$branchB = Branch::create([
'name' => 'Branch Beta',
'created_by' => $company->id,
]);
// Employee in Branch A
$userA = User::create([
'name' => 'Employee Alpha User',
'email' => 'alpha_' . uniqid() . '@test.com',
'password' => bcrypt('password'),
'type' => 'employee',
'created_by' => $company->id,
]);
Employee::create([
'user_id' => $userA->id,
'employee_id' => 'EMP-A-' . rand(100, 999),
'branch_id' => $branchA->id,
'created_by' => $company->id,
]);
// Employee in Branch B
$userB = User::create([
'name' => 'Employee Beta User',
'email' => 'beta_' . uniqid() . '@test.com',
'password' => bcrypt('password'),
'type' => 'employee',
'created_by' => $company->id,
]);
Employee::create([
'user_id' => $userB->id,
'employee_id' => 'EMP-B-' . rand(100, 999),
'branch_id' => $branchB->id,
'created_by' => $company->id,
]);
// Overtime requests
OvertimeApplication::create([
'user_id' => $userA->id,
'date' => '2026-05-10',
'requested_hours' => 2.5,
'reason' => 'Alpha Overtime Work',
'status' => 'pending',
]);
OvertimeApplication::create([
'user_id' => $userB->id,
'date' => '2026-05-10',
'requested_hours' => 3.0,
'reason' => 'Beta Overtime Work',
'status' => 'pending',
]);
$hrUser = User::create([
'name' => 'HR Branch A User',
'email' => 'hr_a_' . uniqid() . '@test.com',
'password' => bcrypt('password'),
'type' => 'hr',
'branch_id' => $branchA->id,
'created_by' => $company->id,
]);
$response = $this->actingAs($hrUser)->get(route('hr.overtime.index'));
$response->assertStatus(200);
$response->assertSee('Alpha Overtime Work');
$response->assertDontSee('Beta Overtime Work');
}
public function test_branch_scoped_user_only_sees_assigned_branch_thirteenth_month()
{
$company = User::create([
'name' => 'Company Admin',
'email' => 'company_' . uniqid() . '@test.com',
'password' => bcrypt('password'),
'type' => 'company',
]);
$branchA = Branch::create([
'name' => 'Branch Alpha',
'created_by' => $company->id,
]);
$branchB = Branch::create([
'name' => 'Branch Beta',
'created_by' => $company->id,
]);
// Employee in Branch A
$userA = User::create([
'name' => 'Employee Alpha User',
'email' => 'alpha_' . uniqid() . '@test.com',
'password' => bcrypt('password'),
'type' => 'employee',
'created_by' => $company->id,
]);
Employee::create([
'user_id' => $userA->id,
'employee_id' => 'EMP-A-' . rand(100, 999),
'branch_id' => $branchA->id,
'created_by' => $company->id,
]);
// Employee in Branch B
$userB = User::create([
'name' => 'Employee Beta User',
'email' => 'beta_' . uniqid() . '@test.com',
'password' => bcrypt('password'),
'type' => 'employee',
'created_by' => $company->id,
]);
Employee::create([
'user_id' => $userB->id,
'employee_id' => 'EMP-B-' . rand(100, 999),
'branch_id' => $branchB->id,
'created_by' => $company->id,
]);
$hrUser = User::create([
'name' => 'HR Branch A User',
'email' => 'hr_a_' . uniqid() . '@test.com',
'password' => bcrypt('password'),
'type' => 'hr',
'branch_id' => $branchA->id,
'created_by' => $company->id,
]);
$hrUser->givePermissionTo('manage-payroll-runs');
$response = $this->actingAs($hrUser)->get(route('hr.thirteenth-month.index'));
$response->assertStatus(200);
$response->assertSee('Employee Alpha User');
$response->assertDontSee('Employee Beta User');
}
}