TenantAtlas/apps/platform/tests/Feature/Concerns/BuildsPortfolioTriageFixtures.php
2026-04-10 23:34:02 +02:00

217 lines
7.4 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Feature\Concerns;
use App\Filament\Resources\TenantResource\Pages\ListTenants;
use App\Models\BackupSet;
use App\Models\RestoreRun;
use App\Models\Tenant;
use App\Models\TenantTriageReview;
use App\Models\User;
use App\Services\PortfolioTriage\TenantTriageReviewService;
use App\Support\BackupHealth\TenantBackupHealthAssessment;
use App\Support\BackupHealth\TenantBackupHealthResolver;
use App\Support\RestoreSafety\RestoreResultAttention;
use App\Support\RestoreSafety\RestoreSafetyResolver;
use App\Support\Tenants\TenantRecoveryTriagePresentation;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament;
use InvalidArgumentException;
use Livewire\Livewire;
trait BuildsPortfolioTriageFixtures
{
/**
* @return array{0: User, 1: Tenant}
*/
protected function makePortfolioTriageActor(
string $tenantName = 'Anchor Tenant',
string $role = 'owner',
string $workspaceRole = 'readonly',
): array {
$tenant = Tenant::factory()->create([
'status' => 'active',
'name' => $tenantName,
]);
[$user, $tenant] = createUserWithTenant(
tenant: $tenant,
role: $role,
workspaceRole: $workspaceRole,
);
workspaceOverviewSeedQuietTenantTruth($tenant);
return [$user, $tenant];
}
protected function makePortfolioTriagePeer(User $user, Tenant $workspaceTenant, string $name): Tenant
{
$tenant = Tenant::factory()->create([
'status' => 'active',
'workspace_id' => (int) $workspaceTenant->workspace_id,
'name' => $name,
]);
createUserWithTenant(
tenant: $tenant,
user: $user,
role: 'owner',
workspaceRole: 'readonly',
);
workspaceOverviewSeedQuietTenantTruth($tenant);
return $tenant;
}
protected function seedPortfolioBackupConcern(Tenant $tenant, string $state): ?BackupSet
{
return match ($state) {
TenantBackupHealthAssessment::POSTURE_ABSENT => null,
TenantBackupHealthAssessment::POSTURE_STALE => BackupSet::factory()
->for($tenant)
->staleCompleted()
->create([
'name' => 'Portfolio stale backup',
]),
TenantBackupHealthAssessment::POSTURE_DEGRADED => BackupSet::factory()
->for($tenant)
->degradedCompleted()
->create([
'name' => 'Portfolio degraded backup',
]),
default => workspaceOverviewSeedHealthyBackup($tenant, [
'name' => 'Portfolio healthy backup',
]),
};
}
protected function seedPortfolioRecoveryConcern(
Tenant $tenant,
string $reason = 'no_history',
?BackupSet $backupSet = null,
): ?RestoreRun {
$backupSet ??= workspaceOverviewSeedHealthyBackup($tenant, [
'name' => 'Portfolio recovery backup',
]);
return match ($reason) {
'no_history' => null,
RestoreResultAttention::STATE_FAILED => RestoreRun::factory()
->for($tenant)
->for($backupSet)
->failedOutcome()
->create([
'completed_at' => now()->subMinutes(10),
]),
RestoreResultAttention::STATE_PARTIAL => RestoreRun::factory()
->for($tenant)
->for($backupSet)
->partialOutcome()
->create([
'completed_at' => now()->subMinutes(10),
]),
RestoreResultAttention::STATE_COMPLETED_WITH_FOLLOW_UP => RestoreRun::factory()
->for($tenant)
->for($backupSet)
->completedWithFollowUp()
->create([
'completed_at' => now()->subMinutes(10),
]),
default => RestoreRun::factory()
->for($tenant)
->for($backupSet)
->completedOutcome()
->create([
'completed_at' => now()->subMinutes(10),
]),
};
}
protected function usePortfolioTriageWorkspace(User $user, Tenant $tenant): void
{
test()->actingAs($user);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
Filament::setTenant(null, true);
request()->attributes->remove('tenant_resource.posture_snapshot');
session()->forget('tables.'.md5(ListTenants::class).'_filters');
session()->forget('tables.'.md5(ListTenants::class).'_search');
session()->forget('tables.'.md5(ListTenants::class).'_sort');
}
protected function portfolioTriageRegistryList(User $user, Tenant $workspaceTenant, array $query = []): mixed
{
$this->usePortfolioTriageWorkspace($user, $workspaceTenant);
$factory = $query !== []
? Livewire::withQueryParams($query)->actingAs($user)
: Livewire::actingAs($user);
return $factory->test(ListTenants::class);
}
/**
* @return array{backup_posture: list<string>, recovery_evidence: list<string>, triage_sort: string|null}
*/
protected function portfolioReturnFilters(
array $backupPosture = [],
array $recoveryEvidence = [],
array $reviewState = [],
?string $triageSort = TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST,
): array {
return [
'backup_posture' => $backupPosture,
'recovery_evidence' => $recoveryEvidence,
'review_state' => $reviewState,
'triage_sort' => $triageSort,
];
}
protected function seedPortfolioTriageReview(
Tenant $tenant,
string $concernFamily,
string $manualState = TenantTriageReview::STATE_REVIEWED,
?User $actor = null,
bool $changedFingerprint = false,
): TenantTriageReview {
$backupHealth = app(TenantBackupHealthResolver::class)->assess($tenant);
$recoveryEvidence = app(RestoreSafetyResolver::class)->dashboardRecoveryEvidence($tenant);
$review = match ($manualState) {
TenantTriageReview::STATE_REVIEWED => app(TenantTriageReviewService::class)->markReviewed(
tenant: $tenant,
concernFamily: $concernFamily,
backupHealth: $backupHealth,
recoveryEvidence: $recoveryEvidence,
actor: $actor,
),
TenantTriageReview::STATE_FOLLOW_UP_NEEDED => app(TenantTriageReviewService::class)->markFollowUpNeeded(
tenant: $tenant,
concernFamily: $concernFamily,
backupHealth: $backupHealth,
recoveryEvidence: $recoveryEvidence,
actor: $actor,
),
default => throw new InvalidArgumentException('Unsupported triage review state.'),
};
if ($changedFingerprint) {
$review->forceFill([
'review_fingerprint' => hash('sha256', sprintf(
'%s:%s:%d',
$concernFamily,
$manualState,
(int) $review->getKey(),
)),
])->save();
}
request()->attributes->remove('tenant_resource.triage_review_snapshot');
return $review->fresh(['reviewer']);
}
}