TenantAtlas/tests/Feature/Guards/LivewireTrustedStateGuardTest.php
ahmido 5ec62cd117 feat: harden livewire trusted state boundaries (#182)
## Summary
- add the shared trusted-state model and resolver helpers for first-slice Livewire and Filament surfaces
- harden managed tenant onboarding, tenant required permissions, and system runbooks against forged or stale public state
- add focused Pest guard and regression coverage plus the complete spec 152 artifact set

## Validation
- `vendor/bin/sail artisan test --compact`
- manual smoke validated on `/admin/onboarding/{onboardingDraft}`
- manual smoke validated on `/admin/tenants/{tenant}/required-permissions`
- manual smoke validated on `/system/ops/runbooks`

## Notes
- Livewire v4.0+ / Filament v5 stack unchanged
- no new panels, routes, assets, or global-search changes
- provider registration remains in `bootstrap/providers.php`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #182
2026-03-18 23:01:14 +00:00

127 lines
5.8 KiB
PHP

<?php
declare(strict_types=1);
use App\Support\Livewire\TrustedState\TrustedStateClass;
use App\Support\Livewire\TrustedState\TrustedStatePolicy;
use App\Support\Livewire\TrustedState\TrustedStateResolver;
use App\Support\Workspaces\WorkspaceContext;
function livewireTrustedStateFirstSliceFixtures(): array
{
return [
TrustedStatePolicy::MANAGED_TENANT_ONBOARDING_WIZARD => 'app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php',
TrustedStatePolicy::TENANT_REQUIRED_PERMISSIONS => 'app/Filament/Pages/TenantRequiredPermissions.php',
TrustedStatePolicy::SYSTEM_RUNBOOKS => 'app/Filament/System/Pages/Ops/Runbooks.php',
];
}
function livewireTrustedStateFieldGroups(array $componentPolicy): array
{
return [
'locked_identity_fields' => $componentPolicy['locked_identity_fields'],
'mutable_selector_fields' => $componentPolicy['mutable_selector_fields'],
'server_derived_authority_fields' => $componentPolicy['server_derived_authority_fields'],
];
}
it('documents a first-slice trusted-state policy for every guarded surface', function (): void {
$policy = app(TrustedStatePolicy::class);
expect($policy->components())
->toBe(array_keys(livewireTrustedStateFirstSliceFixtures()));
foreach (livewireTrustedStateFirstSliceFixtures() as $component => $relativePath) {
expect(file_exists(base_path($relativePath)))->toBeTrue();
expect($policy->forComponent($component)['component_name'])->not->toBe('');
}
});
it('keeps first-slice policies explicit about state lanes and authority sources', function (): void {
$policy = app(TrustedStatePolicy::class);
$validClasses = array_map(
static fn (TrustedStateClass $class): string => $class->value,
TrustedStateClass::cases(),
);
expect($validClasses)->toContain('presentation', 'locked_identity', 'server_derived_authority');
foreach ($policy->firstSlice() as $componentPolicy) {
expect($componentPolicy['plane'])->not->toBe('')
->and($componentPolicy['authority_sources'])->not->toBeEmpty()
->and($componentPolicy['mutable_selectors'])->toBeArray()
->and($componentPolicy['locked_identities'])->toBeArray()
->and($componentPolicy['locked_identity_fields'])->toBeArray()
->and($componentPolicy['mutable_selector_fields'])->toBeArray()
->and($componentPolicy['server_derived_authority_fields'])->toBeArray()
->and($componentPolicy['forbidden_public_authority_fields'])->toBeArray();
}
});
it('keeps the first-slice trusted-state inventory implementation-backed', function (): void {
$policy = app(TrustedStatePolicy::class);
foreach (livewireTrustedStateFirstSliceFixtures() as $component => $relativePath) {
$componentPolicy = $policy->forComponent($component);
$contents = file_get_contents(base_path($relativePath));
expect($contents)->not->toBeFalse();
foreach (livewireTrustedStateFieldGroups($componentPolicy) as $groupName => $fields) {
foreach ($fields as $field) {
expect($field['notes'])->not->toBe('');
expect($field['implementation_markers'])->not->toBeEmpty();
$expectedStateClass = match ($groupName) {
'locked_identity_fields' => TrustedStateClass::LockedIdentity->value,
'mutable_selector_fields' => TrustedStateClass::Presentation->value,
'server_derived_authority_fields' => TrustedStateClass::ServerDerivedAuthority->value,
default => throw new InvalidArgumentException('Unknown trusted-state group.'),
};
expect($field['state_class'])->toBe($expectedStateClass);
foreach ($field['implementation_markers'] as $marker) {
if (str_contains($marker, PHP_EOL)) {
$pattern = '/'.str_replace(
['\\ '.preg_quote(PHP_EOL, '/').'\\ ', '\\ '.preg_quote(PHP_EOL, '/'), preg_quote(PHP_EOL, '/').'\\ '],
['\\s+', '\\s+', '\\s+'],
preg_quote($marker, '/')
).'/s';
expect(preg_match($pattern, (string) $contents))
->toBe(1, "Missing implementation marker [{$marker}] for {$component}.{$field['name']}");
continue;
}
expect(str_contains((string) $contents, $marker))
->toBeTrue("Missing implementation marker [{$marker}] for {$component}.{$field['name']}");
}
}
}
}
});
it('documents first-slice trusted-state helper semantics through reusable enum and resolver APIs', function (): void {
$policy = app(TrustedStatePolicy::class);
$resolver = app(TrustedStateResolver::class);
expect(TrustedStateClass::Presentation->allowsClientMutation())->toBeTrue()
->and(TrustedStateClass::Presentation->requiresServerRevalidation())->toBeFalse()
->and(TrustedStateClass::LockedIdentity->allowsClientMutation())->toBeFalse()
->and(TrustedStateClass::LockedIdentity->requiresServerRevalidation())->toBeTrue()
->and(TrustedStateClass::ServerDerivedAuthority->allowsClientMutation())->toBeFalse()
->and(TrustedStateClass::ServerDerivedAuthority->requiresServerRevalidation())->toBeTrue();
foreach ($policy->components() as $component) {
expect($resolver->requiredAuthoritySources($component, $policy))
->toBe($policy->forComponent($component)['authority_sources']);
}
});
it('binds the shared trusted-state resolver through the container', function (): void {
expect(app(TrustedStateResolver::class))->toBeInstanceOf(TrustedStateResolver::class)
->and(app(WorkspaceContext::class))->toBeInstanceOf(WorkspaceContext::class);
});