TenantAtlas/tests/Feature/Guards/TenantOwnedQueryGuardTest.php
2026-03-18 09:30:13 +01:00

109 lines
4.4 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Concerns\InteractsWithTenantOwnedRecords;
use App\Support\WorkspaceIsolation\TenantOwnedModelFamilies;
use App\Support\WorkspaceIsolation\TenantOwnedTables;
function tenantOwnedFamilySource(string $className): string
{
$reflection = new ReflectionClass($className);
$fileName = $reflection->getFileName();
expect($fileName)->toBeString();
$contents = file_get_contents((string) $fileName);
expect($contents)->not->toBeFalse();
return (string) $contents;
}
it('keeps the first-slice tenant-owned family inventory aligned to tenant-owned tables', function (): void {
$allowedSearchPostures = ['scoped', 'disabled', 'not_applicable'];
foreach (TenantOwnedModelFamilies::firstSlice() as $familyName => $family) {
expect(TenantOwnedTables::contains($family['table']))
->toBeTrue("{$familyName} must point at a known tenant-owned table.");
expect(TenantOwnedTables::firstSlice())
->toContain($family['table']);
expect($allowedSearchPostures)
->toContain($family['search_posture']);
expect(class_exists($family['model']))->toBeTrue();
expect(class_exists($family['resource']))->toBeTrue();
}
});
it('keeps first-slice family names unique and explicit', function (): void {
$names = TenantOwnedModelFamilies::names();
expect($names)->not->toBeEmpty();
expect(array_unique($names))->toBe($names);
});
it('keeps the first-slice family registry exhaustive for declared first-slice tenant-owned tables', function (): void {
$familyTables = array_map(
static fn (array $family): string => $family['table'],
TenantOwnedModelFamilies::firstSlice(),
);
expect($familyTables)->toEqualCanonicalizing(TenantOwnedTables::firstSlice());
});
it('keeps the residual tenant-owned rollout inventory aligned to non-first-slice tenant-owned tables', function (): void {
$residualTables = array_map(
static fn (array $entry): string => $entry['table'],
TenantOwnedModelFamilies::residualRolloutInventory(),
);
expect($residualTables)->toEqualCanonicalizing(TenantOwnedTables::residual());
foreach (TenantOwnedModelFamilies::residualRolloutInventory() as $familyName => $entry) {
expect(trim($familyName))->not->toBe('');
expect(trim($entry['likely_surface']))->not->toBe('');
expect(trim($entry['why_not_in_first_slice']))->not->toBe('');
}
});
it('requires first-slice resources to use the shared tenant-owned query and record resolver entry points', function (): void {
foreach (TenantOwnedModelFamilies::firstSlice() as $familyName => $family) {
$resourceClass = $family['resource'];
$traits = class_uses_recursive($resourceClass);
$source = tenantOwnedFamilySource($resourceClass);
expect(in_array(InteractsWithTenantOwnedRecords::class, $traits, true))
->toBeTrue("{$familyName} must use the shared tenant-owned resource trait.");
expect(preg_match('/public\s+static\s+function\s+getEloquentQuery\s*\(/', $source) === 1)
->toBeTrue("{$familyName} must expose an explicit scoped query entry point.");
expect(preg_match('/static::(?:getTenantOwnedEloquentQuery|scopeTenantOwnedQuery)\s*\(/', $source) === 1)
->toBeTrue("{$familyName} must derive records from the canonical tenant-owned query helper.");
expect(preg_match('/public\s+static\s+function\s+resolveScopedRecordOrFail\s*\(/', $source) === 1)
->toBeTrue("{$familyName} must expose an explicit scoped-record resolver.");
expect(str_contains($source, 'resolveTenantOwnedRecordOrFail'))
->toBeTrue("{$familyName} must resolve detail records through the shared tenant-owned resolver.");
}
});
it('documents explicit scope exceptions with non-empty reasons', function (): void {
$exceptions = TenantOwnedModelFamilies::explicitScopeExceptions();
$exceptionMetadata = TenantOwnedModelFamilies::scopeExceptions();
expect($exceptions)->not->toBeEmpty();
expect(array_keys($exceptionMetadata))->toEqual(array_keys($exceptions));
foreach ($exceptions as $surfaceName => $reason) {
expect($surfaceName)->not->toBe('');
expect(trim($reason))->not->toBe('');
expect($exceptionMetadata[$surfaceName]['exception_kind'])->not->toBe('');
expect($exceptionMetadata[$surfaceName]['still_required_checks'])->not->toBeEmpty();
}
});