84 lines
3.3 KiB
PHP
84 lines
3.3 KiB
PHP
|
|
<?php
|
|||
|
|
|
|||
|
|
declare(strict_types=1);
|
|||
|
|
|
|||
|
|
use App\Http\Middleware\SetTenantContext;
|
|||
|
|
use App\Models\Project;
|
|||
|
|
use App\Models\Tenant;
|
|||
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|||
|
|
use Illuminate\Support\Facades\Route;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Тесты middleware SetTenantContext: резолюция tenant_id и установка
|
|||
|
|
* `app.current_tenant_id` для RLS-фильтрации.
|
|||
|
|
*
|
|||
|
|
* Не проверяет RLS-фильтрацию в самом запросе — это RlsSmokeTest.
|
|||
|
|
* Здесь только что middleware корректно устанавливает PG-переменную.
|
|||
|
|
*/
|
|||
|
|
uses(DatabaseTransactions::class);
|
|||
|
|
|
|||
|
|
beforeEach(function () {
|
|||
|
|
Route::middleware([SetTenantContext::class])->get('/_test/tenant-context', function () {
|
|||
|
|
// current_setting вернёт NULL если не установлено (с missing_ok=true).
|
|||
|
|
$value = DB::selectOne("SELECT current_setting('app.current_tenant_id', true) AS v")->v;
|
|||
|
|
|
|||
|
|
return response()->json(['tenant_id' => $value]);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('middleware возвращает 403 без tenant context', function () {
|
|||
|
|
$response = $this->get('/_test/tenant-context');
|
|||
|
|
$response->assertStatus(403);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('middleware устанавливает tenant_id из X-Tenant-Id header', function () {
|
|||
|
|
$tenant = Tenant::factory()->create();
|
|||
|
|
|
|||
|
|
$response = $this->withHeaders(['X-Tenant-Id' => (string) $tenant->id])
|
|||
|
|
->get('/_test/tenant-context');
|
|||
|
|
|
|||
|
|
$response->assertStatus(200);
|
|||
|
|
expect((int) $response->json('tenant_id'))->toBe($tenant->id);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('middleware игнорирует не-числовой X-Tenant-Id header', function () {
|
|||
|
|
$response = $this->withHeaders(['X-Tenant-Id' => 'not-a-number'])
|
|||
|
|
->get('/_test/tenant-context');
|
|||
|
|
|
|||
|
|
$response->assertStatus(403);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('middleware резолвит tenant_id по subdomain', function () {
|
|||
|
|
$tenant = Tenant::factory()->create(['subdomain' => 'acme-corp']);
|
|||
|
|
|
|||
|
|
$response = $this->get('http://acme-corp.liderra.ru/_test/tenant-context');
|
|||
|
|
|
|||
|
|
$response->assertStatus(200);
|
|||
|
|
expect((int) $response->json('tenant_id'))->toBe($tenant->id);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('middleware с tenant_id корректно фильтрует данные через RLS', function () {
|
|||
|
|
// Этот тест НЕ работает на postgres-superuser (BYPASSRLS),
|
|||
|
|
// но показывает что middleware устанавливает контекст для будущих
|
|||
|
|
// запросов под crm_app_user. Проверка эквивалентности значения.
|
|||
|
|
$tenant1 = Tenant::factory()->create();
|
|||
|
|
$tenant2 = Tenant::factory()->create();
|
|||
|
|
Project::factory()->count(2)->create(['tenant_id' => $tenant1->id]);
|
|||
|
|
Project::factory()->count(3)->create(['tenant_id' => $tenant2->id]);
|
|||
|
|
|
|||
|
|
Route::middleware([SetTenantContext::class])->get('/_test/projects-count', function () {
|
|||
|
|
// Без BYPASSRLS было бы COUNT только tenant1's projects
|
|||
|
|
$tenant1Count = DB::table('projects')->where('tenant_id', request()->header('X-Expected-Tenant'))->count();
|
|||
|
|
|
|||
|
|
return response()->json(['count' => $tenant1Count]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
$response = $this->withHeaders([
|
|||
|
|
'X-Tenant-Id' => (string) $tenant1->id,
|
|||
|
|
'X-Expected-Tenant' => (string) $tenant1->id,
|
|||
|
|
])->get('/_test/projects-count');
|
|||
|
|
|
|||
|
|
$response->assertStatus(200);
|
|||
|
|
expect($response->json('count'))->toBe(2);
|
|||
|
|
});
|