Deploy 1: i18n + Notifications + Global Search + Tests

- SetLocale middleware (ro/ru/en, session-first, user-persisted)
- Lang switcher in topbar (Filament render hook USER_MENU_BEFORE)
- POST /locale/{lang} route persists to user.locale + session
- Database notifications enabled on tenant panel (30s polling)
- GlobalSearch (Cmd+K / Ctrl+K) on Client, Vehicle, WorkOrder, Lead, Part
- Tests: TenantIsolation (4), AuthFlow (2), WorkOrderCalc (3), MarkupRule (3)
This commit is contained in:
2026-05-07 18:22:48 +00:00
parent 6c72fc7db1
commit d1e0695930
17 changed files with 770 additions and 0 deletions
+114
View File
@@ -0,0 +1,114 @@
<?php
namespace Tests\Feature;
use App\Models\Central\Company;
use App\Models\Central\Plan;
use App\Models\Tenant\Client;
use App\Models\Tenant\User;
use App\Tenancy\TenantManager;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* Verifică tenant-ul A NU poate vedea/modifica datele tenant-ului B.
* Această test e pilonul de securitate al întregii arhitecturi multi-tenant.
*/
class TenantIsolationTest extends TestCase
{
use RefreshDatabase;
public function test_tenant_scope_blocks_cross_tenant_reads(): void
{
[$companyA, $companyB] = $this->makeTwoCompanies();
// Create client in company A
app(TenantManager::class)->setCurrent($companyA);
$clientA = Client::create([
'name' => 'Alice', 'phone' => '+37300001',
'type' => 'individual', 'status' => 'active',
]);
// Switch to company B and try to read
app(TenantManager::class)->setCurrent($companyB);
$found = Client::find($clientA->id);
$this->assertNull($found, 'Client from company A leaked into company B query');
}
public function test_tenant_scope_returns_empty_when_no_tenant(): void
{
[$companyA] = $this->makeTwoCompanies();
app(TenantManager::class)->setCurrent($companyA);
Client::create([
'name' => 'X', 'phone' => '+37300002',
'type' => 'individual', 'status' => 'active',
]);
// Reset tenant — fail-safe must engage
app(TenantManager::class)->setCurrent(null);
$count = Client::query()->count();
$this->assertSame(0, $count, 'TenantScope did not engage WHERE 1=0 fail-safe');
}
public function test_user_email_can_repeat_across_tenants(): void
{
[$companyA, $companyB] = $this->makeTwoCompanies();
app(TenantManager::class)->setCurrent($companyA);
$userA = User::create([
'company_id' => $companyA->id,
'name' => 'A', 'email' => 'shared@example.com',
'password' => 'pwd', 'status' => 'active',
]);
app(TenantManager::class)->setCurrent($companyB);
$userB = User::create([
'company_id' => $companyB->id,
'name' => 'B', 'email' => 'shared@example.com',
'password' => 'pwd', 'status' => 'active',
]);
$this->assertNotEquals($userA->id, $userB->id);
$this->assertSame('shared@example.com', $userA->email);
$this->assertSame('shared@example.com', $userB->email);
}
public function test_creating_model_auto_fills_company_id(): void
{
[$companyA] = $this->makeTwoCompanies();
app(TenantManager::class)->setCurrent($companyA);
$client = Client::create([
'name' => 'Auto', 'phone' => '+37300003',
'type' => 'individual', 'status' => 'active',
]);
$this->assertSame($companyA->id, $client->company_id, 'BelongsToTenant trait did not auto-fill company_id');
}
private function makeTwoCompanies(): array
{
$plan = Plan::create([
'name' => 'Test', 'slug' => 'test', 'price' => 0, 'features' => [],
]);
$a = Company::create([
'plan_id' => $plan->id,
'slug' => 'aaa-' . uniqid(),
'name' => 'AAA Service',
'status' => 'active',
]);
$b = Company::create([
'plan_id' => $plan->id,
'slug' => 'bbb-' . uniqid(),
'name' => 'BBB Service',
'status' => 'active',
]);
return [$a, $b];
}
}