Applied the decision-first global surface IA contract to BackupSet views. Includes decision summary header, usability status, and separation of technical metadata. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #442
228 lines
7.9 KiB
PHP
228 lines
7.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Resources\BackupSetResource;
|
|
use App\Filament\Resources\BackupSetResource\Pages\ViewBackupSet;
|
|
use App\Models\BackupItem;
|
|
use App\Models\BackupSet;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\User;
|
|
use Filament\Actions\Action;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function (): void {
|
|
bindFailHardGraphClient();
|
|
});
|
|
|
|
it('renders a healthy backup set as a decision-first restore point without Graph calls', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
setAdminPanelContext($tenant);
|
|
|
|
$backupSet = spec371HealthyBackupSet($tenant, [
|
|
'name' => 'Spec 371 Healthy Backup',
|
|
'created_by' => 'operator@example.test',
|
|
'completed_at' => now()->subHour(),
|
|
'metadata' => ['source' => 'spec371'],
|
|
]);
|
|
|
|
$response = $this->actingAs($user)
|
|
->get(BackupSetResource::getUrl('view', ['record' => $backupSet], tenant: $tenant))
|
|
->assertOk()
|
|
->assertSee('Restore-point decision')
|
|
->assertSee('Usable for restore review')
|
|
->assertSee('Captured item inventory is available for operator review.')
|
|
->assertSee('Review included items below before starting any separate restore workflow.')
|
|
->assertSee('Technical and lifecycle detail')
|
|
->assertDontSee('Item review state')
|
|
->assertDontSee('This backup set has captured items ready for operator review before any separate restore workflow starts.')
|
|
->assertDontSee('Usable for review')
|
|
->assertDontSee('No degradations detected across');
|
|
|
|
spec371AssertOrdered($response->getContent(), [
|
|
'Backup set #'.$backupSet->getKey(),
|
|
'Restore-point decision',
|
|
'Usable for restore review',
|
|
'Technical and lifecycle detail',
|
|
]);
|
|
});
|
|
|
|
it('shows degraded backup input as action-needed before restore review', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
setAdminPanelContext($tenant);
|
|
|
|
$backupSet = BackupSet::factory()
|
|
->for($tenant)
|
|
->degradedCompleted()
|
|
->create([
|
|
'name' => 'Spec 371 Degraded Backup',
|
|
]);
|
|
|
|
$response = $this->actingAs($user)
|
|
->get(BackupSetResource::getUrl('view', ['record' => $backupSet], tenant: $tenant))
|
|
->assertOk()
|
|
->assertSee('Action needed before restore review')
|
|
->assertSee('Current degradation')
|
|
->assertSee('1 degraded item')
|
|
->assertSee('Treat this backup as investigation input until degraded items are reviewed.')
|
|
->assertSee('Review degraded included items and source operation before continuing into restore.');
|
|
|
|
spec371AssertOrdered($response->getContent(), [
|
|
'Restore-point decision',
|
|
'Current degradation',
|
|
'Technical and lifecycle detail',
|
|
]);
|
|
});
|
|
|
|
it('explains an empty backup set blocker before technical context', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
setAdminPanelContext($tenant);
|
|
|
|
$backupSet = BackupSet::factory()
|
|
->for($tenant)
|
|
->create([
|
|
'name' => 'Spec 371 Empty Backup',
|
|
'item_count' => 0,
|
|
'metadata' => [],
|
|
]);
|
|
|
|
$response = $this->actingAs($user)
|
|
->get(BackupSetResource::getUrl('view', ['record' => $backupSet], tenant: $tenant))
|
|
->assertOk()
|
|
->assertSee('Blocked until items are captured')
|
|
->assertSee('Current blocker')
|
|
->assertSee('No backup items were captured.')
|
|
->assertSee('Create or refresh a backup set before starting restore review.')
|
|
->assertDontSee('Backup quality, lifecycle status, and related operations stay ahead of raw backup metadata.');
|
|
|
|
spec371AssertOrdered($response->getContent(), [
|
|
'Restore-point decision',
|
|
'Current blocker',
|
|
'Technical and lifecycle detail',
|
|
]);
|
|
});
|
|
|
|
it('keeps the backup sets list scan-first without repeated healthy zero-noise', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
setAdminPanelContext($tenant);
|
|
|
|
spec371HealthyBackupSet($tenant, ['name' => 'Spec 371 Healthy Row']);
|
|
|
|
BackupSet::factory()
|
|
->for($tenant)
|
|
->degradedCompleted()
|
|
->create(['name' => 'Spec 371 Degraded Row']);
|
|
|
|
$this->actingAs($user)
|
|
->get(BackupSetResource::getUrl('index', tenant: $tenant))
|
|
->assertOk()
|
|
->assertSee('Restore-point decision')
|
|
->assertSee('Items captured')
|
|
->assertSee('Usable for restore review')
|
|
->assertSee('Action needed before restore review')
|
|
->assertSee('1 degraded item')
|
|
->assertDontSee('No degradations detected across');
|
|
});
|
|
|
|
it('shows a capability-aware empty state with one create backup CTA for authorized users', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
setAdminPanelContext($tenant);
|
|
|
|
$this->actingAs($user)
|
|
->get(BackupSetResource::getUrl('index', tenant: $tenant))
|
|
->assertOk()
|
|
->assertSee('No backup sets')
|
|
->assertSee('Create a backup set to capture policy versions and assignments for later restore review.')
|
|
->assertSee('Create backup set');
|
|
});
|
|
|
|
it('preserves destructive detail action confirmation and readonly blocking', function (): void {
|
|
[$owner, $tenant] = createUserWithTenant(role: 'owner');
|
|
$readonly = User::factory()->create();
|
|
createUserWithTenant(tenant: $tenant, user: $readonly, role: 'readonly');
|
|
|
|
$backupSet = spec371HealthyBackupSet($tenant, ['name' => 'Spec 371 Action Safety']);
|
|
|
|
$this->actingAs($owner);
|
|
setAdminPanelContext($tenant);
|
|
|
|
Livewire::actingAs($owner)
|
|
->test(ViewBackupSet::class, ['record' => (string) $backupSet->getKey()])
|
|
->assertActionExists('archive', fn (Action $action): bool => $action->isConfirmationRequired());
|
|
|
|
$this->actingAs($readonly);
|
|
setAdminPanelContext($tenant);
|
|
|
|
Livewire::actingAs($readonly)
|
|
->test(ViewBackupSet::class, ['record' => (string) $backupSet->getKey()])
|
|
->assertActionDisabled('archive')
|
|
->callAction('archive');
|
|
|
|
expect($backupSet->fresh()?->trashed())->toBeFalse();
|
|
});
|
|
|
|
it('denies wrong-environment backup set access as not found before content leaks', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$foreignTenant = ManagedEnvironment::factory()->active()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'name' => 'Spec 371 Foreign Environment',
|
|
]);
|
|
|
|
$foreignBackupSet = spec371HealthyBackupSet($foreignTenant, [
|
|
'name' => 'Spec 371 Foreign Backup',
|
|
]);
|
|
|
|
setAdminPanelContext($tenant);
|
|
|
|
$this->actingAs($user)
|
|
->get(BackupSetResource::getUrl('view', ['record' => (string) $foreignBackupSet->getKey()], tenant: $tenant))
|
|
->assertNotFound();
|
|
});
|
|
|
|
/**
|
|
* @param array<string, mixed> $attributes
|
|
*/
|
|
function spec371HealthyBackupSet(ManagedEnvironment $tenant, array $attributes = []): BackupSet
|
|
{
|
|
$backupSet = BackupSet::factory()
|
|
->for($tenant)
|
|
->create(array_merge([
|
|
'name' => 'Spec 371 Healthy Backup',
|
|
'item_count' => 1,
|
|
'completed_at' => now()->subMinutes(30),
|
|
'metadata' => [],
|
|
], $attributes));
|
|
|
|
BackupItem::factory()
|
|
->for($tenant)
|
|
->for($backupSet)
|
|
->create([
|
|
'payload' => ['id' => 'spec-371-policy'],
|
|
'metadata' => [],
|
|
'assignments' => [],
|
|
]);
|
|
|
|
return $backupSet;
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $needles
|
|
*/
|
|
function spec371AssertOrdered(string $content, array $needles): void
|
|
{
|
|
$lastPosition = -1;
|
|
|
|
foreach ($needles as $needle) {
|
|
$position = strpos($content, $needle);
|
|
|
|
expect($position)->not->toBeFalse();
|
|
expect($position)->toBeGreaterThan($lastPosition);
|
|
|
|
$lastPosition = (int) $position;
|
|
}
|
|
}
|