94 lines
4.9 KiB
PHP
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();
|
|
}
|
|
}
|