141 lines
6.0 KiB
PHP
141 lines
6.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Pages\TenantDashboard;
|
|
use App\Filament\Widgets\Tenant\TenantTriageArrivalContinuity;
|
|
use App\Models\AuditLog;
|
|
use App\Models\Tenant;
|
|
use App\Models\TenantTriageReview;
|
|
use App\Models\User;
|
|
use App\Support\Audit\AuditActionId;
|
|
use App\Support\BackupHealth\TenantBackupHealthAssessment;
|
|
use App\Support\PortfolioTriage\PortfolioArrivalContextToken;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Filament\Facades\Filament;
|
|
use Filament\Actions\Action;
|
|
use Livewire\Livewire;
|
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
use Tests\Feature\Concerns\BuildsPortfolioTriageFixtures;
|
|
|
|
uses(BuildsPortfolioTriageFixtures::class);
|
|
|
|
function triageReviewArrivalState(Tenant $tenant): array
|
|
{
|
|
return [
|
|
'sourceSurface' => PortfolioArrivalContextToken::SOURCE_TENANT_REGISTRY,
|
|
'tenantRouteKey' => (string) $tenant->external_id,
|
|
'workspaceId' => (int) $tenant->workspace_id,
|
|
'concernFamily' => PortfolioArrivalContextToken::FAMILY_BACKUP_HEALTH,
|
|
'concernState' => TenantBackupHealthAssessment::POSTURE_STALE,
|
|
'concernReason' => TenantBackupHealthAssessment::REASON_LATEST_BACKUP_STALE,
|
|
'returnFilters' => [
|
|
'backup_posture' => [TenantBackupHealthAssessment::POSTURE_STALE],
|
|
],
|
|
];
|
|
}
|
|
|
|
function triageReviewDashboardWidget(User $user, Tenant $tenant, array $state): mixed
|
|
{
|
|
test()->actingAs($user);
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
|
setTenantPanelContext($tenant);
|
|
request()->attributes->remove('portfolio_triage.arrival_context');
|
|
|
|
return Livewire::withQueryParams([
|
|
PortfolioArrivalContextToken::QUERY_PARAMETER => PortfolioArrivalContextToken::encode($state),
|
|
])->actingAs($user)->test(TenantTriageArrivalContinuity::class);
|
|
}
|
|
|
|
it('returns 404 for non-members on the tenant dashboard triage route', function (): void {
|
|
$tenant = Tenant::factory()->create(['status' => 'active']);
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
|
$this->seedPortfolioBackupConcern($tenant, TenantBackupHealthAssessment::POSTURE_STALE);
|
|
|
|
$foreignTenant = Tenant::factory()->create([
|
|
'status' => 'active',
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
]);
|
|
$this->seedPortfolioBackupConcern($foreignTenant, TenantBackupHealthAssessment::POSTURE_STALE);
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
|
->get(TenantDashboard::getUrl([
|
|
PortfolioArrivalContextToken::QUERY_PARAMETER => PortfolioArrivalContextToken::encode(triageReviewArrivalState($foreignTenant)),
|
|
], panel: 'tenant', tenant: $foreignTenant))
|
|
->assertNotFound();
|
|
});
|
|
|
|
it('shows review actions as disabled for readonly members and still rejects a bypassed mutation with 403', function (): void {
|
|
$tenant = Tenant::factory()->create(['status' => 'active']);
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
|
$this->seedPortfolioBackupConcern($tenant, TenantBackupHealthAssessment::POSTURE_STALE);
|
|
|
|
$component = triageReviewDashboardWidget($user, $tenant, triageReviewArrivalState($tenant))
|
|
->assertActionVisible('markReviewed')
|
|
->assertActionDisabled('markReviewed');
|
|
|
|
$instance = $component->instance();
|
|
$componentReflection = new ReflectionObject($instance);
|
|
$cachedActionsProperty = $componentReflection->getProperty('cachedActions');
|
|
$cachedActionsProperty->setAccessible(true);
|
|
$cachedActions = $cachedActionsProperty->getValue($instance);
|
|
$action = $cachedActions['markReviewed'] ?? null;
|
|
|
|
expect($action)->not->toBeNull();
|
|
|
|
$actionReflection = new ReflectionObject($action);
|
|
$disabledProperty = $actionReflection->getProperty('isDisabled');
|
|
$disabledProperty->setAccessible(true);
|
|
$disabledProperty->setValue($action, false);
|
|
$cachedActionsProperty->setValue($instance, $cachedActions);
|
|
|
|
$instance->mountAction('markReviewed');
|
|
|
|
$mountedAction = $instance->getMountedAction();
|
|
expect($mountedAction)->not->toBeNull();
|
|
|
|
$mountedReflection = new ReflectionObject($mountedAction);
|
|
$mountedDisabledProperty = $mountedReflection->getProperty('isDisabled');
|
|
$mountedDisabledProperty->setAccessible(true);
|
|
$mountedDisabledProperty->setValue($mountedAction, false);
|
|
|
|
try {
|
|
$instance->callMountedAction();
|
|
$this->fail('Expected a 403 when bypassing the disabled action.');
|
|
} catch (HttpException $exception) {
|
|
expect($exception->getStatusCode())->toBe(403);
|
|
}
|
|
|
|
expect(TenantTriageReview::query()->count())->toBe(0);
|
|
});
|
|
|
|
it('writes review progress and audit state only after the preview-confirmed action executes', function (): void {
|
|
[$user, $tenant] = $this->makePortfolioTriageActor('Authorization Success Tenant');
|
|
$this->seedPortfolioBackupConcern($tenant, TenantBackupHealthAssessment::POSTURE_STALE);
|
|
|
|
$component = triageReviewDashboardWidget($user, $tenant, triageReviewArrivalState($tenant))
|
|
->assertActionExists('markFollowUpNeeded', fn (Action $action): bool => $action->isConfirmationRequired()
|
|
&& str_contains((string) $action->getModalDescription(), 'Target state: Follow-up needed')
|
|
&& str_contains((string) $action->getModalDescription(), 'TenantPilot only'))
|
|
->mountAction('markFollowUpNeeded');
|
|
|
|
expect(TenantTriageReview::query()->count())->toBe(0);
|
|
|
|
$component
|
|
->callMountedAction();
|
|
|
|
expect(TenantTriageReview::query()
|
|
->where('tenant_id', (int) $tenant->getKey())
|
|
->where('current_state', TenantTriageReview::STATE_FOLLOW_UP_NEEDED)
|
|
->whereNull('resolved_at')
|
|
->exists())->toBeTrue()
|
|
->and(AuditLog::query()
|
|
->where('workspace_id', (int) $tenant->workspace_id)
|
|
->where('tenant_id', (int) $tenant->getKey())
|
|
->where('action', AuditActionId::TenantTriageReviewMarkedFollowUpNeeded->value)
|
|
->exists())->toBeTrue();
|
|
|
|
Filament::setTenant(null, true);
|
|
});
|