217 lines
7.4 KiB
PHP
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']);
|
|
}
|
|
}
|