## Summary - add a first-class finding exception domain with request, approval, rejection, renewal, and revocation lifecycle support - add tenant-scoped exception register, finding governance surfaces, and a canonical workspace approval queue in Filament - add audit, badge, evidence, and review-pack integrations plus focused Pest coverage for workflow, authorization, and governance validity ## Validation - vendor/bin/sail bin pint --dirty --format agent - CI=1 vendor/bin/sail artisan test --compact - manual integrated-browser smoke test for the request-exception happy path, tenant register visibility, and canonical queue visibility ## Notes - Filament implementation remains on v5 with Livewire v4-compatible surfaces - canonical queue lives in the admin panel; provider registration stays in bootstrap/providers.php - finding exceptions stay out of global search in this rollout Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #184
123 lines
5.5 KiB
PHP
123 lines
5.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Concerns\InteractsWithTenantOwnedRecords;
|
|
use App\Filament\Resources\FindingExceptionResource;
|
|
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();
|
|
}
|
|
});
|
|
|
|
it('keeps finding exception discovery tenant-scoped and global-search disabled', function (): void {
|
|
$source = tenantOwnedFamilySource(FindingExceptionResource::class);
|
|
|
|
expect(preg_match('/protected\s+static\s+bool\s+\$isGloballySearchable\s*=\s*false;/', $source) === 1)
|
|
->toBeTrue('FindingExceptionResource must keep global search disabled until a tenant-safe global search flow is introduced.');
|
|
|
|
expect(preg_match('/getTenantOwnedEloquentQuery\s*\(\)\s*->with\(static::relationshipsForView\(\)\)/s', $source) === 1)
|
|
->toBeTrue('FindingExceptionResource must derive list queries from the canonical tenant-owned query helper and scoped relationship loader.');
|
|
|
|
expect(preg_match('/resolveTenantOwnedRecordOrFail\s*\(\$record,\s*parent::getEloquentQuery\(\)->with\(static::relationshipsForView\(\)\)\)/s', $source) === 1)
|
|
->toBeTrue('FindingExceptionResource must resolve detail records through the shared tenant-owned resolver with the same scoped relationships.');
|
|
});
|