494 lines
17 KiB
PHP
494 lines
17 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Modules\ContractorManagement\Models\Contractor;
|
|
use Modules\MasterData\Models\Material;
|
|
use Modules\ProjectManagement\Models\Project;
|
|
use Spatie\Permission\Models\Role;
|
|
use Tests\TestCase;
|
|
|
|
class TenantScopeTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
// Create required roles for tests
|
|
Role::firstOrCreate(['name' => 'admin']);
|
|
Role::firstOrCreate(['name' => 'contractor-admin']);
|
|
Role::firstOrCreate(['name' => 'Super Admin']);
|
|
Role::firstOrCreate(['name' => 'Site Technical']);
|
|
}
|
|
|
|
public function test_tenant_isolation_is_enforced_between_independent_contractors(): void
|
|
{
|
|
// 1. Create two independent contractors
|
|
$contractorA = Contractor::create([
|
|
'company_name' => 'Contractor A',
|
|
'contact_person' => 'Person A',
|
|
'email' => 'a@contractor.com',
|
|
'status' => 'active',
|
|
'payment_terms' => 'net_30',
|
|
]);
|
|
$contractorB = Contractor::create([
|
|
'company_name' => 'Contractor B',
|
|
'contact_person' => 'Person B',
|
|
'email' => 'b@contractor.com',
|
|
'status' => 'active',
|
|
'payment_terms' => 'net_30',
|
|
]);
|
|
|
|
// 2. Create projects for each
|
|
$projectA = Project::create([
|
|
'name' => 'Project A',
|
|
'contractor_id' => $contractorA->id,
|
|
'code' => 'PRJ-2026-001',
|
|
]);
|
|
$projectB = Project::create([
|
|
'name' => 'Project B',
|
|
'contractor_id' => $contractorB->id,
|
|
'code' => 'PRJ-2026-002',
|
|
]);
|
|
|
|
// 3. Create admin user for Contractor B
|
|
$userB = User::factory()->create([
|
|
'contractor_id' => $contractorB->id,
|
|
'user_type' => 'admin',
|
|
'status' => 'active',
|
|
]);
|
|
$userB->assignRole('contractor-admin');
|
|
|
|
// 4. Authenticate as Contractor B user
|
|
$this->actingAs($userB);
|
|
|
|
// 5. Query projects and verify Contractor B only sees Project B
|
|
$visibleProjects = Project::all();
|
|
|
|
$this->assertTrue($visibleProjects->contains($projectB));
|
|
$this->assertFalse($visibleProjects->contains($projectA));
|
|
}
|
|
|
|
public function test_users_page_data_is_visible_to_platform_admin_but_isolated_for_contractor_admins(): void
|
|
{
|
|
$contractorA = Contractor::create([
|
|
'company_name' => 'Users Contractor A',
|
|
'contact_person' => 'Person A',
|
|
'email' => 'users-a@contractor.com',
|
|
'status' => 'active',
|
|
'payment_terms' => 'net_30',
|
|
]);
|
|
$contractorB = Contractor::create([
|
|
'company_name' => 'Users Contractor B',
|
|
'contact_person' => 'Person B',
|
|
'email' => 'users-b@contractor.com',
|
|
'status' => 'active',
|
|
'payment_terms' => 'net_30',
|
|
]);
|
|
|
|
$userA = User::factory()->create(['contractor_id' => $contractorA->id]);
|
|
$userB = User::factory()->create(['contractor_id' => $contractorB->id]);
|
|
$platformAdmin = User::factory()->create(['contractor_id' => null, 'user_type' => 'admin']);
|
|
|
|
$this->actingAs($platformAdmin);
|
|
$this->assertTrue(User::all()->contains($userA));
|
|
$this->assertTrue(User::all()->contains($userB));
|
|
|
|
$this->actingAs($userA);
|
|
$visibleUsers = User::all();
|
|
$this->assertTrue($visibleUsers->contains($userA));
|
|
$this->assertFalse($visibleUsers->contains($userB));
|
|
}
|
|
|
|
public function test_platform_owners_bypass_tenant_filtering(): void
|
|
{
|
|
// 1. Create independent contractors and projects
|
|
$contractorA = Contractor::create([
|
|
'company_name' => 'Contractor A',
|
|
'contact_person' => 'Person A',
|
|
'email' => 'a@contractor.com',
|
|
'status' => 'active',
|
|
'payment_terms' => 'net_30',
|
|
]);
|
|
$contractorB = Contractor::create([
|
|
'company_name' => 'Contractor B',
|
|
'contact_person' => 'Person B',
|
|
'email' => 'b@contractor.com',
|
|
'status' => 'active',
|
|
'payment_terms' => 'net_30',
|
|
]);
|
|
|
|
$projectA = Project::create([
|
|
'name' => 'Project A',
|
|
'contractor_id' => $contractorA->id,
|
|
'code' => 'PRJ-2026-001',
|
|
]);
|
|
$projectB = Project::create([
|
|
'name' => 'Project B',
|
|
'contractor_id' => $contractorB->id,
|
|
'code' => 'PRJ-2026-002',
|
|
]);
|
|
|
|
// 2. Create Platform Owner (contractor_id === null)
|
|
$platformAdmin = User::factory()->create([
|
|
'contractor_id' => null,
|
|
'user_type' => 'admin',
|
|
'status' => 'active',
|
|
]);
|
|
$platformAdmin->assignRole('admin');
|
|
|
|
// 3. Authenticate as Platform Owner
|
|
$this->actingAs($platformAdmin);
|
|
|
|
// 4. Query projects and verify both projects are visible
|
|
$visibleProjects = Project::all();
|
|
|
|
$this->assertTrue($visibleProjects->contains($projectA));
|
|
$this->assertTrue($visibleProjects->contains($projectB));
|
|
}
|
|
|
|
public function test_platform_super_admin_with_legacy_contractor_link_sees_all_users(): void
|
|
{
|
|
$contractorA = Contractor::create([
|
|
'company_name' => 'Legacy Link Contractor A',
|
|
'contact_person' => 'Person A',
|
|
'email' => 'legacy-a@contractor.com',
|
|
'status' => 'active',
|
|
'payment_terms' => 'net_30',
|
|
]);
|
|
$contractorB = Contractor::create([
|
|
'company_name' => 'Legacy Link Contractor B',
|
|
'contact_person' => 'Person B',
|
|
'email' => 'legacy-b@contractor.com',
|
|
'status' => 'active',
|
|
'payment_terms' => 'net_30',
|
|
]);
|
|
|
|
$userA = User::factory()->create(['contractor_id' => $contractorA->id]);
|
|
$userB = User::factory()->create(['contractor_id' => $contractorB->id]);
|
|
$platformAdmin = User::factory()->create([
|
|
'contractor_id' => $contractorA->id,
|
|
'user_type' => 'admin',
|
|
]);
|
|
$platformAdmin->assignRole('Super Admin');
|
|
|
|
$this->actingAs($platformAdmin);
|
|
$visibleUsers = User::all();
|
|
|
|
$this->assertTrue($visibleUsers->contains($userA));
|
|
$this->assertTrue($visibleUsers->contains($userB));
|
|
}
|
|
|
|
public function test_parent_contractor_can_traverse_subcontractor_hierarchy(): void
|
|
{
|
|
// 1. Create Parent Contractor A
|
|
$parentContractor = Contractor::create([
|
|
'company_name' => 'Parent Contractor',
|
|
'contact_person' => 'Parent Person',
|
|
'email' => 'parent@contractor.com',
|
|
'status' => 'active',
|
|
'type' => 'main',
|
|
'payment_terms' => 'net_30',
|
|
]);
|
|
|
|
// 2. Create Subcontractor B (parent_id = Parent Contractor A)
|
|
$subContractor = Contractor::create([
|
|
'company_name' => 'Sub Contractor',
|
|
'contact_person' => 'Sub Person',
|
|
'email' => 'sub@contractor.com',
|
|
'status' => 'active',
|
|
'type' => 'sub',
|
|
'parent_id' => $parentContractor->id,
|
|
'payment_terms' => 'net_30',
|
|
]);
|
|
|
|
// 3. Create projects
|
|
$parentProject = Project::create([
|
|
'name' => 'Parent Project',
|
|
'contractor_id' => $parentContractor->id,
|
|
'code' => 'PRJ-2026-001',
|
|
]);
|
|
$subProject = Project::create([
|
|
'name' => 'Sub Project',
|
|
'contractor_id' => $subContractor->id,
|
|
'code' => 'PRJ-2026-002',
|
|
]);
|
|
|
|
// 4. Create Parent Admin User
|
|
$parentAdmin = User::factory()->create([
|
|
'contractor_id' => $parentContractor->id,
|
|
'user_type' => 'admin',
|
|
'status' => 'active',
|
|
]);
|
|
$parentAdmin->assignRole('contractor-admin');
|
|
|
|
// 5. Authenticate as Parent Admin
|
|
$this->actingAs($parentAdmin);
|
|
|
|
// 6. Query projects and verify parent admin sees BOTH parent and sub projects
|
|
$visibleProjects = Project::all();
|
|
|
|
$this->assertTrue($visibleProjects->contains($parentProject));
|
|
$this->assertTrue($visibleProjects->contains($subProject));
|
|
|
|
// 7. Create Sub Admin User
|
|
$subAdmin = User::factory()->create([
|
|
'contractor_id' => $subContractor->id,
|
|
'user_type' => 'admin',
|
|
'status' => 'active',
|
|
]);
|
|
$subAdmin->assignRole('contractor-admin');
|
|
|
|
// 8. Authenticate as Sub Admin
|
|
$this->actingAs($subAdmin);
|
|
|
|
// 9. Query projects and verify sub admin ONLY sees sub project
|
|
$visibleProjectsForSub = Project::all();
|
|
|
|
$this->assertTrue($visibleProjectsForSub->contains($subProject));
|
|
$this->assertFalse($visibleProjectsForSub->contains($parentProject));
|
|
}
|
|
|
|
public function test_contractor_sees_global_materials_if_shares_materials_catalog_is_true(): void
|
|
{
|
|
// 1. Create a contractor with shares_materials_catalog = true
|
|
$contractor = Contractor::create([
|
|
'company_name' => 'Sharing Contractor',
|
|
'contact_person' => 'Person S',
|
|
'email' => 's@contractor.com',
|
|
'status' => 'active',
|
|
'shares_materials_catalog' => true,
|
|
'payment_terms' => 'net_30',
|
|
]);
|
|
|
|
// 2. Create custom material and global material
|
|
$customMaterial = Material::create([
|
|
'name' => 'Custom Cement',
|
|
'unit' => 'bag',
|
|
'contractor_id' => $contractor->id,
|
|
]);
|
|
|
|
$globalMaterial = Material::create([
|
|
'name' => 'Global Sand',
|
|
'unit' => 'cubic_meter',
|
|
'contractor_id' => null,
|
|
]);
|
|
|
|
// 3. Authenticate as contractor user
|
|
$user = User::factory()->create([
|
|
'contractor_id' => $contractor->id,
|
|
'user_type' => 'admin',
|
|
'status' => 'active',
|
|
]);
|
|
$this->actingAs($user);
|
|
|
|
// 4. Query materials and verify both are visible
|
|
$visibleMaterials = Material::all();
|
|
|
|
$this->assertTrue($visibleMaterials->contains($customMaterial));
|
|
$this->assertTrue($visibleMaterials->contains($globalMaterial));
|
|
}
|
|
|
|
public function test_contractor_does_not_see_global_materials_if_shares_materials_catalog_is_false(): void
|
|
{
|
|
// 1. Create a contractor with shares_materials_catalog = false
|
|
$contractor = Contractor::create([
|
|
'company_name' => 'Isolated Contractor',
|
|
'contact_person' => 'Person I',
|
|
'email' => 'i@contractor.com',
|
|
'status' => 'active',
|
|
'shares_materials_catalog' => false,
|
|
'payment_terms' => 'net_30',
|
|
]);
|
|
|
|
// 2. Create custom material and global material
|
|
$customMaterial = Material::create([
|
|
'name' => 'Private Steel',
|
|
'unit' => 'kg',
|
|
'contractor_id' => $contractor->id,
|
|
]);
|
|
|
|
$globalMaterial = Material::create([
|
|
'name' => 'Global Sand',
|
|
'unit' => 'cubic_meter',
|
|
'contractor_id' => null,
|
|
]);
|
|
|
|
// 3. Authenticate as contractor user
|
|
$user = User::factory()->create([
|
|
'contractor_id' => $contractor->id,
|
|
'user_type' => 'admin',
|
|
'status' => 'active',
|
|
]);
|
|
$this->actingAs($user);
|
|
|
|
// 4. Query materials and verify ONLY custom material is visible
|
|
$visibleMaterials = Material::all();
|
|
|
|
$this->assertTrue($visibleMaterials->contains($customMaterial));
|
|
$this->assertFalse($visibleMaterials->contains($globalMaterial));
|
|
}
|
|
|
|
public function test_project_code_generation_is_globally_unique_across_tenants(): void
|
|
{
|
|
// 1. Create two independent contractors
|
|
$contractorA = Contractor::create([
|
|
'company_name' => 'Contractor A',
|
|
'contact_person' => 'Person A',
|
|
'email' => 'a@contractor.com',
|
|
'status' => 'active',
|
|
'payment_terms' => 'net_30',
|
|
]);
|
|
$contractorB = Contractor::create([
|
|
'company_name' => 'Contractor B',
|
|
'contact_person' => 'Person B',
|
|
'email' => 'b@contractor.com',
|
|
'status' => 'active',
|
|
'payment_terms' => 'net_30',
|
|
]);
|
|
|
|
// 2. Create admin user for Contractor A
|
|
$userA = User::factory()->create([
|
|
'contractor_id' => $contractorA->id,
|
|
'user_type' => 'admin',
|
|
'status' => 'active',
|
|
]);
|
|
$userA->assignRole('contractor-admin');
|
|
|
|
// 3. Create admin user for Contractor B
|
|
$userB = User::factory()->create([
|
|
'contractor_id' => $contractorB->id,
|
|
'user_type' => 'admin',
|
|
'status' => 'active',
|
|
]);
|
|
$userB->assignRole('contractor-admin');
|
|
|
|
// 4. Act as Contractor A admin and create a project without code
|
|
$this->actingAs($userA);
|
|
$projectA = Project::create([
|
|
'name' => 'Project A for Contractor A',
|
|
]);
|
|
|
|
// Verify Project A got code PRJ-{year}-001
|
|
$year = now()->year;
|
|
$this->assertEquals(sprintf('PRJ-%d-001', $year), $projectA->code);
|
|
|
|
// 5. Act as Contractor B admin and create a project without code
|
|
$this->actingAs($userB);
|
|
$projectB = Project::create([
|
|
'name' => 'Project B for Contractor B',
|
|
]);
|
|
|
|
// Verify Project B got code PRJ-{year}-002, bypassing the TenantScope of Contractor B
|
|
// and avoiding duplicate key violation PRJ-{year}-001
|
|
$this->assertEquals(sprintf('PRJ-%d-002', $year), $projectB->code);
|
|
}
|
|
|
|
public function test_contractor_admin_sees_projects_assigned_through_project_contractor(): void
|
|
{
|
|
$contractor = Contractor::create([
|
|
'company_name' => 'Assigned Contractor',
|
|
'contact_person' => 'Person A',
|
|
'email' => 'assigned@contractor.com',
|
|
'status' => 'active',
|
|
'payment_terms' => 'net_30',
|
|
]);
|
|
|
|
$assignedProject = Project::create([
|
|
'name' => 'Assigned Project',
|
|
'code' => 'PRJ-2026-101',
|
|
'contractor_id' => null,
|
|
]);
|
|
$unrelatedProject = Project::create([
|
|
'name' => 'Unrelated Project',
|
|
'code' => 'PRJ-2026-102',
|
|
'contractor_id' => null,
|
|
]);
|
|
$contractor->projects()->attach($assignedProject->id, ['role' => 'subcontractor']);
|
|
|
|
$user = User::factory()->create([
|
|
'contractor_id' => $contractor->id,
|
|
'user_type' => 'admin',
|
|
'status' => 'active',
|
|
]);
|
|
$user->assignRole('contractor-admin');
|
|
|
|
$this->actingAs($user);
|
|
$visibleProjects = Project::all();
|
|
|
|
$this->assertTrue($visibleProjects->contains($assignedProject));
|
|
$this->assertFalse($visibleProjects->contains($unrelatedProject));
|
|
}
|
|
|
|
public function test_site_operations_user_sees_only_projects_assigned_to_their_personnel_record(): void
|
|
{
|
|
$siteUser = User::factory()->create([
|
|
'user_type' => 'employee',
|
|
'status' => 'active',
|
|
]);
|
|
$siteUser->assignRole('Site Technical');
|
|
|
|
$assignedProject = Project::create([
|
|
'name' => 'Site Assigned Project',
|
|
'code' => 'PRJ-2026-111',
|
|
]);
|
|
$unrelatedProject = Project::create([
|
|
'name' => 'Site Unrelated Project',
|
|
'code' => 'PRJ-2026-112',
|
|
]);
|
|
$assignedProject->personnel()->attach($siteUser->id, ['role' => 'site_technical']);
|
|
|
|
$this->actingAs($siteUser);
|
|
$visibleProjects = Project::all();
|
|
|
|
$this->assertTrue($visibleProjects->contains($assignedProject));
|
|
$this->assertFalse($visibleProjects->contains($unrelatedProject));
|
|
}
|
|
|
|
public function test_site_operations_user_with_contractor_link_sees_contractor_projects(): void
|
|
{
|
|
$contractor = Contractor::create([
|
|
'company_name' => 'Site Operations Contractor',
|
|
'contact_person' => 'Person S',
|
|
'email' => 'site-ops@contractor.com',
|
|
'status' => 'active',
|
|
'payment_terms' => 'net_30',
|
|
]);
|
|
$otherContractor = Contractor::create([
|
|
'company_name' => 'Other Contractor',
|
|
'contact_person' => 'Person O',
|
|
'email' => 'other@contractor.com',
|
|
'status' => 'active',
|
|
'payment_terms' => 'net_30',
|
|
]);
|
|
|
|
$contractorProject = Project::create([
|
|
'name' => 'Contractor Site Project',
|
|
'code' => 'PRJ-2026-121',
|
|
'contractor_id' => $contractor->id,
|
|
]);
|
|
$otherProject = Project::create([
|
|
'name' => 'Other Site Project',
|
|
'code' => 'PRJ-2026-122',
|
|
'contractor_id' => $otherContractor->id,
|
|
]);
|
|
|
|
$siteUser = User::factory()->create([
|
|
'contractor_id' => $contractor->id,
|
|
'user_type' => 'employee',
|
|
'status' => 'active',
|
|
]);
|
|
$siteUser->assignRole('Site Technical');
|
|
|
|
$this->actingAs($siteUser);
|
|
$visibleProjects = Project::all();
|
|
|
|
$this->assertTrue($visibleProjects->contains($contractorProject));
|
|
$this->assertFalse($visibleProjects->contains($otherProject));
|
|
}
|
|
}
|