Files
HRM-System/tests/Feature/BranchScopedAccessTest.php
admin ef1aa7cf24 feat(biometrics): enforce strict branch scoping on biometric attendance and sync
- Restrict biometric attendances query and grouped records strictly to employees of assigned branch
- Scope bulk sync (syncAll) to only sync biometric records for employees within the user's branch
- Guard individual sync and custom sync against processing employees from foreign branches
- Tighten attendance records query to strictly filter by employee assigned branch
- Add feature test verifying biometric attendance isolation and permission guards
2026-09-10 11:32:48 +08:00

568 lines
19 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 App\Models\BiometricAttendance;
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']);
Permission::firstOrCreate(['name' => 'manage-biometric-attendance']);
}
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');
}
public function test_branch_scoped_user_only_sees_assigned_branch_biometric_attendance()
{
$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,
]);
$empA = Employee::create([
'user_id' => $userA->id,
'employee_id' => 'EMP-A-' . rand(100, 999),
'biometric_emp_id' => 'BIO-101',
'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,
]);
$empB = Employee::create([
'user_id' => $userB->id,
'employee_id' => 'EMP-B-' . rand(100, 999),
'biometric_emp_id' => 'BIO-102',
'branch_id' => $branchB->id,
'created_by' => $company->id,
]);
// Biometric punches
BiometricAttendance::create([
'biometric_emp_id' => 'BIO-101',
'punch_time' => '2026-05-10 09:00:00',
'punch_state' => 0,
'branch_id' => $branchA->id,
'sync_status' => 'pending',
]);
BiometricAttendance::create([
'biometric_emp_id' => 'BIO-102',
'punch_time' => '2026-05-10 09:00:00',
'punch_state' => 0,
'branch_id' => $branchB->id,
'sync_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,
]);
$hrUser->givePermissionTo('manage-biometric-attendance');
$response = $this->actingAs($hrUser)->get(route('hr.biometric-attendance.index'));
$response->assertStatus(200);
$response->assertSee('BIO-101');
$response->assertDontSee('BIO-102');
// Test show route for foreign branch punch
$foreignShowResponse = $this->actingAs($hrUser)->get(route('hr.biometric-attendance.show', [
'employeeCode' => 'BIO-102',
'date' => '2026-05-10',
]));
$foreignShowResponse->assertStatus(403);
}
}