270 lines
9.2 KiB
PHP
270 lines
9.2 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Modules\ContractorManagement\Models\Contractor;
|
|
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']);
|
|
}
|
|
|
|
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_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_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 = \Modules\MasterData\Models\Material::create([
|
|
'name' => 'Custom Cement',
|
|
'unit' => 'bag',
|
|
'contractor_id' => $contractor->id,
|
|
]);
|
|
|
|
$globalMaterial = \Modules\MasterData\Models\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 = \Modules\MasterData\Models\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 = \Modules\MasterData\Models\Material::create([
|
|
'name' => 'Private Steel',
|
|
'unit' => 'kg',
|
|
'contractor_id' => $contractor->id,
|
|
]);
|
|
|
|
$globalMaterial = \Modules\MasterData\Models\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 = \Modules\MasterData\Models\Material::all();
|
|
|
|
$this->assertTrue($visibleMaterials->contains($customMaterial));
|
|
$this->assertFalse($visibleMaterials->contains($globalMaterial));
|
|
}
|
|
}
|