Files
SGF/tests/Feature/CompanyIsolationTest.php
T

94 lines
4.9 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\User;
use App\Modules\Identity\Models\Company;
use App\Modules\Identity\Models\Tenant;
use App\Modules\Identity\Models\Unit;
use App\Shared\Context\CompanyContext;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Tests\TestCase;
final class CompanyIsolationTest extends TestCase
{
use RefreshDatabase;
private function scenario(): array
{
$tenant = Tenant::create(['name' => 'Tenant A']);
$otherTenant = Tenant::create(['name' => 'Tenant B']);
$company = Company::create(['tenant_id' => $tenant->id, 'name' => 'Company A']);
$sibling = Company::create(['tenant_id' => $tenant->id, 'name' => 'Company B']);
$foreign = Company::create(['tenant_id' => $otherTenant->id, 'name' => 'Company C']);
$user = User::create(['name' => 'Synthetic User', 'email' => 'synthetic@example.test', 'password' => 'test-only-password']);
DB::table('company_memberships')->insert(['user_id' => $user->id, 'tenant_id' => $tenant->id, 'company_id' => $company->id]);
\Tests\Support\IdentityFixture::grant($user->id, $tenant->id, $company->id, 'company.view');
return [$user, $tenant, $company, $sibling, $foreign];
}
public function test_membership_is_required_even_for_another_company_in_the_same_tenant(): void
{
[$user, $tenant, $company, $sibling, $foreign] = $this->scenario();
$this->actingAs($user)->withSession(['auth_version' => 1, 'company_context' => ['tenant_id' => $tenant->id, 'company_id' => $company->id, 'unit_id' => null, 'token' => 'synthetic']])
->getJson('/api/context')->assertOk()->assertExactJson(['tenant_id' => $tenant->id, 'company_id' => $company->id, 'unit_id' => null]);
foreach ([$sibling, $foreign] as $denied) {
$this->withSession(['company_context' => ['tenant_id' => $denied->tenant_id, 'company_id' => $denied->id, 'unit_id' => null, 'token' => 'synthetic']])->getJson('/api/context')->assertForbidden();
}
$this->withSession(['company_context' => []])->getJson('/api/context')->assertForbidden();
}
public function test_company_owned_reads_and_bulk_writes_do_not_cross_company_boundaries(): void
{
[$user, $tenant, $company, $sibling] = $this->scenario();
DB::table('units')->insert([
['tenant_id' => $tenant->id, 'company_id' => $company->id, 'name' => 'Visible'],
['tenant_id' => $tenant->id, 'company_id' => $sibling->id, 'name' => 'Private'],
]);
$visibleId = DB::table('units')->where('company_id', $company->id)->value('id');
\Tests\Support\IdentityFixture::grant($user->id, $tenant->id, $company->id, 'unit.view', $visibleId);
$context = app(CompanyContext::class);
$context->selectFor($user->id, $tenant->id, $company->id);
$this->assertSame(['Visible'], Unit::query()->pluck('name')->all());
$this->assertSame(1, Unit::query()->update(['name' => 'Updated']));
$this->assertSame('Private', DB::table('units')->where('company_id', $sibling->id)->value('name'));
$unit = Unit::create(['name' => 'New']);
$this->assertSame($company->id, $unit->company_id);
$context->clear();
$this->expectException(AccessDeniedHttpException::class);
Unit::query()->count();
}
public function test_stale_model_cannot_be_saved_under_another_company_context(): void
{
[$user, $tenant, $company, $sibling] = $this->scenario();
DB::table('company_memberships')->insert(['user_id' => $user->id, 'tenant_id' => $tenant->id, 'company_id' => $sibling->id]);
\Tests\Support\IdentityFixture::grant($user->id, $tenant->id, $sibling->id, 'company.view');
$context = app(CompanyContext::class);
$context->selectFor($user->id, $tenant->id, $company->id);
$unit = Unit::create(['name' => 'Original']);
$context->selectFor($user->id, $tenant->id, $sibling->id);
$unit->name = 'Forbidden';
$this->expectException(AccessDeniedHttpException::class);
$unit->save();
}
public function test_database_rejects_mismatched_tenant_company_relationship(): void
{
[, $tenant, , , $foreign] = $this->scenario();
$this->expectException(\Illuminate\Database\QueryException::class);
DB::table('units')->insert(['tenant_id' => $tenant->id, 'company_id' => $foreign->id, 'name' => 'Invalid']);
}
public function test_context_is_cleared_after_a_request(): void
{
[$user, $tenant, $company] = $this->scenario();
$this->actingAs($user)->withSession(['auth_version' => 1, 'company_context' => ['tenant_id' => $tenant->id, 'company_id' => $company->id, 'unit_id' => null, 'token' => 'synthetic']])->getJson('/api/context')->assertOk();
$this->expectException(AccessDeniedHttpException::class);
app(CompanyContext::class)->identifiers();
}
}