Files
GSB-Construction/tests/Feature/TenantScopeTest.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));
}
}