feat: add confirm & queued execute to restore wizard
This commit is contained in:
parent
4c9544e9b7
commit
26755df017
@ -6,6 +6,7 @@
|
|||||||
use App\Jobs\BulkRestoreRunDeleteJob;
|
use App\Jobs\BulkRestoreRunDeleteJob;
|
||||||
use App\Jobs\BulkRestoreRunForceDeleteJob;
|
use App\Jobs\BulkRestoreRunForceDeleteJob;
|
||||||
use App\Jobs\BulkRestoreRunRestoreJob;
|
use App\Jobs\BulkRestoreRunRestoreJob;
|
||||||
|
use App\Jobs\ExecuteRestoreRunJob;
|
||||||
use App\Models\BackupItem;
|
use App\Models\BackupItem;
|
||||||
use App\Models\BackupSet;
|
use App\Models\BackupSet;
|
||||||
use App\Models\RestoreRun;
|
use App\Models\RestoreRun;
|
||||||
@ -13,9 +14,11 @@
|
|||||||
use App\Services\BulkOperationService;
|
use App\Services\BulkOperationService;
|
||||||
use App\Services\Graph\GraphClientInterface;
|
use App\Services\Graph\GraphClientInterface;
|
||||||
use App\Services\Graph\GroupResolver;
|
use App\Services\Graph\GroupResolver;
|
||||||
|
use App\Services\Intune\AuditLogger;
|
||||||
use App\Services\Intune\RestoreDiffGenerator;
|
use App\Services\Intune\RestoreDiffGenerator;
|
||||||
use App\Services\Intune\RestoreRiskChecker;
|
use App\Services\Intune\RestoreRiskChecker;
|
||||||
use App\Services\Intune\RestoreService;
|
use App\Services\Intune\RestoreService;
|
||||||
|
use App\Support\RestoreRunStatus;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Actions\ActionGroup;
|
use Filament\Actions\ActionGroup;
|
||||||
@ -37,6 +40,7 @@
|
|||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
use UnitEnum;
|
use UnitEnum;
|
||||||
|
|
||||||
class RestoreRunResource extends Resource
|
class RestoreRunResource extends Resource
|
||||||
@ -178,6 +182,8 @@ public static function getWizardSteps(): array
|
|||||||
$set('backup_item_ids', null);
|
$set('backup_item_ids', null);
|
||||||
$set('group_mapping', []);
|
$set('group_mapping', []);
|
||||||
$set('is_dry_run', true);
|
$set('is_dry_run', true);
|
||||||
|
$set('acknowledged_impact', false);
|
||||||
|
$set('tenant_confirm', null);
|
||||||
$set('check_summary', null);
|
$set('check_summary', null);
|
||||||
$set('check_results', []);
|
$set('check_results', []);
|
||||||
$set('checks_ran_at', null);
|
$set('checks_ran_at', null);
|
||||||
@ -200,6 +206,9 @@ public static function getWizardSteps(): array
|
|||||||
->reactive()
|
->reactive()
|
||||||
->afterStateUpdated(function (Set $set, $state): void {
|
->afterStateUpdated(function (Set $set, $state): void {
|
||||||
$set('group_mapping', []);
|
$set('group_mapping', []);
|
||||||
|
$set('is_dry_run', true);
|
||||||
|
$set('acknowledged_impact', false);
|
||||||
|
$set('tenant_confirm', null);
|
||||||
$set('check_summary', null);
|
$set('check_summary', null);
|
||||||
$set('check_results', []);
|
$set('check_results', []);
|
||||||
$set('checks_ran_at', null);
|
$set('checks_ran_at', null);
|
||||||
@ -227,6 +236,9 @@ public static function getWizardSteps(): array
|
|||||||
->reactive()
|
->reactive()
|
||||||
->afterStateUpdated(function (Set $set): void {
|
->afterStateUpdated(function (Set $set): void {
|
||||||
$set('group_mapping', []);
|
$set('group_mapping', []);
|
||||||
|
$set('is_dry_run', true);
|
||||||
|
$set('acknowledged_impact', false);
|
||||||
|
$set('tenant_confirm', null);
|
||||||
$set('check_summary', null);
|
$set('check_summary', null);
|
||||||
$set('check_results', []);
|
$set('check_results', []);
|
||||||
$set('checks_ran_at', null);
|
$set('checks_ran_at', null);
|
||||||
@ -407,6 +419,10 @@ public static function getWizardSteps(): array
|
|||||||
$blockers = (int) ($summary['blocking'] ?? 0);
|
$blockers = (int) ($summary['blocking'] ?? 0);
|
||||||
$warnings = (int) ($summary['warning'] ?? 0);
|
$warnings = (int) ($summary['warning'] ?? 0);
|
||||||
|
|
||||||
|
if ($blockers > 0) {
|
||||||
|
$set('is_dry_run', true, shouldCallUpdatedHooks: true);
|
||||||
|
}
|
||||||
|
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->title('Safety checks completed')
|
->title('Safety checks completed')
|
||||||
->body("Blocking: {$blockers} • Warnings: {$warnings}")
|
->body("Blocking: {$blockers} • Warnings: {$warnings}")
|
||||||
@ -419,6 +435,9 @@ public static function getWizardSteps(): array
|
|||||||
->color('gray')
|
->color('gray')
|
||||||
->visible(fn (Get $get): bool => filled($get('check_results')) || filled($get('check_summary')))
|
->visible(fn (Get $get): bool => filled($get('check_results')) || filled($get('check_summary')))
|
||||||
->action(function (Set $set): void {
|
->action(function (Set $set): void {
|
||||||
|
$set('is_dry_run', true, shouldCallUpdatedHooks: true);
|
||||||
|
$set('acknowledged_impact', false, shouldCallUpdatedHooks: true);
|
||||||
|
$set('tenant_confirm', null, shouldCallUpdatedHooks: true);
|
||||||
$set('check_summary', null, shouldCallUpdatedHooks: true);
|
$set('check_summary', null, shouldCallUpdatedHooks: true);
|
||||||
$set('check_results', [], shouldCallUpdatedHooks: true);
|
$set('check_results', [], shouldCallUpdatedHooks: true);
|
||||||
$set('checks_ran_at', null, shouldCallUpdatedHooks: true);
|
$set('checks_ran_at', null, shouldCallUpdatedHooks: true);
|
||||||
@ -429,11 +448,6 @@ public static function getWizardSteps(): array
|
|||||||
Step::make('Preview')
|
Step::make('Preview')
|
||||||
->description('Dry-run preview')
|
->description('Dry-run preview')
|
||||||
->schema([
|
->schema([
|
||||||
Forms\Components\Toggle::make('is_dry_run')
|
|
||||||
->label('Preview only (dry-run)')
|
|
||||||
->default(true)
|
|
||||||
->disabled()
|
|
||||||
->helperText('Execution will be enabled once checks, preview, and confirmations are implemented (Phase 6).'),
|
|
||||||
Forms\Components\Hidden::make('preview_summary')
|
Forms\Components\Hidden::make('preview_summary')
|
||||||
->default(null),
|
->default(null),
|
||||||
Forms\Components\Hidden::make('preview_ran_at')
|
Forms\Components\Hidden::make('preview_ran_at')
|
||||||
@ -514,6 +528,9 @@ public static function getWizardSteps(): array
|
|||||||
->color('gray')
|
->color('gray')
|
||||||
->visible(fn (Get $get): bool => filled($get('preview_diffs')) || filled($get('preview_summary')))
|
->visible(fn (Get $get): bool => filled($get('preview_diffs')) || filled($get('preview_summary')))
|
||||||
->action(function (Set $set): void {
|
->action(function (Set $set): void {
|
||||||
|
$set('is_dry_run', true, shouldCallUpdatedHooks: true);
|
||||||
|
$set('acknowledged_impact', false, shouldCallUpdatedHooks: true);
|
||||||
|
$set('tenant_confirm', null, shouldCallUpdatedHooks: true);
|
||||||
$set('preview_summary', null, shouldCallUpdatedHooks: true);
|
$set('preview_summary', null, shouldCallUpdatedHooks: true);
|
||||||
$set('preview_diffs', [], shouldCallUpdatedHooks: true);
|
$set('preview_diffs', [], shouldCallUpdatedHooks: true);
|
||||||
$set('preview_ran_at', null, shouldCallUpdatedHooks: true);
|
$set('preview_ran_at', null, shouldCallUpdatedHooks: true);
|
||||||
@ -522,11 +539,76 @@ public static function getWizardSteps(): array
|
|||||||
->helperText('Generate a normalized diff preview before creating the dry-run restore.'),
|
->helperText('Generate a normalized diff preview before creating the dry-run restore.'),
|
||||||
]),
|
]),
|
||||||
Step::make('Confirm & Execute')
|
Step::make('Confirm & Execute')
|
||||||
->description('Explicit confirmations (Phase 6)')
|
->description('Point of no return')
|
||||||
->schema([
|
->schema([
|
||||||
Forms\Components\Placeholder::make('confirm_placeholder')
|
Forms\Components\Placeholder::make('confirm_environment')
|
||||||
->label('Execution')
|
->label('Environment')
|
||||||
->content('Execution confirmations and gating will be added in Phase 6.'),
|
->content(fn (): string => app()->environment('production') ? 'prod' : 'test'),
|
||||||
|
Forms\Components\Placeholder::make('confirm_tenant_label')
|
||||||
|
->label('Tenant hard-confirm label')
|
||||||
|
->content(function (): string {
|
||||||
|
$tenant = Tenant::current();
|
||||||
|
|
||||||
|
if (! $tenant) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenantIdentifier = $tenant->tenant_id ?? $tenant->external_id;
|
||||||
|
|
||||||
|
return (string) ($tenant->name ?? $tenantIdentifier ?? $tenant->getKey());
|
||||||
|
}),
|
||||||
|
Forms\Components\Toggle::make('is_dry_run')
|
||||||
|
->label('Preview only (dry-run)')
|
||||||
|
->default(true)
|
||||||
|
->reactive()
|
||||||
|
->disabled(function (Get $get): bool {
|
||||||
|
if (! filled($get('checks_ran_at'))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$summary = $get('check_summary');
|
||||||
|
|
||||||
|
if (! is_array($summary)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) ($summary['blocking'] ?? 0) > 0;
|
||||||
|
})
|
||||||
|
->helperText('Turn OFF to queue a real execution. Execution requires checks + preview + confirmations.'),
|
||||||
|
Forms\Components\Checkbox::make('acknowledged_impact')
|
||||||
|
->label('I reviewed the impact (checks + preview)')
|
||||||
|
->accepted()
|
||||||
|
->visible(fn (Get $get): bool => $get('is_dry_run') === false),
|
||||||
|
Forms\Components\TextInput::make('tenant_confirm')
|
||||||
|
->label('Type the tenant label to confirm execution')
|
||||||
|
->required(fn (Get $get): bool => $get('is_dry_run') === false)
|
||||||
|
->visible(fn (Get $get): bool => $get('is_dry_run') === false)
|
||||||
|
->in(function (): array {
|
||||||
|
$tenant = Tenant::current();
|
||||||
|
|
||||||
|
if (! $tenant) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenantIdentifier = $tenant->tenant_id ?? $tenant->external_id;
|
||||||
|
|
||||||
|
return [(string) ($tenant->name ?? $tenantIdentifier ?? $tenant->getKey())];
|
||||||
|
})
|
||||||
|
->validationMessages([
|
||||||
|
'in' => 'Tenant hard-confirm does not match.',
|
||||||
|
])
|
||||||
|
->helperText(function (): string {
|
||||||
|
$tenant = Tenant::current();
|
||||||
|
|
||||||
|
if (! $tenant) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenantIdentifier = $tenant->tenant_id ?? $tenant->external_id;
|
||||||
|
$expected = (string) ($tenant->name ?? $tenantIdentifier ?? $tenant->getKey());
|
||||||
|
|
||||||
|
return "Type: {$expected}";
|
||||||
|
}),
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -1055,19 +1137,13 @@ public static function createRestoreRun(array $data): RestoreRun
|
|||||||
$service = app(RestoreService::class);
|
$service = app(RestoreService::class);
|
||||||
|
|
||||||
$scopeMode = $data['scope_mode'] ?? 'all';
|
$scopeMode = $data['scope_mode'] ?? 'all';
|
||||||
$selectedItemIds = ($scopeMode === 'selected')
|
$selectedItemIds = ($scopeMode === 'selected') ? ($data['backup_item_ids'] ?? null) : null;
|
||||||
? ($data['backup_item_ids'] ?? null)
|
$selectedItemIds = is_array($selectedItemIds) ? $selectedItemIds : null;
|
||||||
: null;
|
|
||||||
|
|
||||||
$restoreRun = $service->execute(
|
$actorEmail = auth()->user()?->email;
|
||||||
tenant: $tenant,
|
$actorName = auth()->user()?->name;
|
||||||
backupSet: $backupSet,
|
$isDryRun = (bool) ($data['is_dry_run'] ?? true);
|
||||||
selectedItemIds: is_array($selectedItemIds) ? $selectedItemIds : null,
|
$groupMapping = $data['group_mapping'] ?? [];
|
||||||
dryRun: true,
|
|
||||||
actorEmail: auth()->user()?->email,
|
|
||||||
actorName: auth()->user()?->name,
|
|
||||||
groupMapping: $data['group_mapping'] ?? [],
|
|
||||||
);
|
|
||||||
|
|
||||||
$checkSummary = $data['check_summary'] ?? null;
|
$checkSummary = $data['check_summary'] ?? null;
|
||||||
$checkResults = $data['check_results'] ?? null;
|
$checkResults = $data['check_results'] ?? null;
|
||||||
@ -1076,14 +1152,57 @@ public static function createRestoreRun(array $data): RestoreRun
|
|||||||
$previewDiffs = $data['preview_diffs'] ?? null;
|
$previewDiffs = $data['preview_diffs'] ?? null;
|
||||||
$previewRanAt = $data['preview_ran_at'] ?? null;
|
$previewRanAt = $data['preview_ran_at'] ?? null;
|
||||||
|
|
||||||
if (
|
$tenantIdentifier = $tenant->tenant_id ?? $tenant->external_id;
|
||||||
is_array($checkSummary)
|
$highlanderLabel = (string) ($tenant->name ?? $tenantIdentifier ?? $tenant->getKey());
|
||||||
|| is_array($checkResults)
|
|
||||||
|| (is_string($checksRanAt) && $checksRanAt !== '')
|
if (! $isDryRun) {
|
||||||
|| is_array($previewSummary)
|
if (! is_array($checkSummary) || ! filled($checksRanAt)) {
|
||||||
|| is_array($previewDiffs)
|
throw ValidationException::withMessages([
|
||||||
|| (is_string($previewRanAt) && $previewRanAt !== '')
|
'check_summary' => 'Run safety checks before executing.',
|
||||||
) {
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$blocking = (int) ($checkSummary['blocking'] ?? 0);
|
||||||
|
$hasBlockers = (bool) ($checkSummary['has_blockers'] ?? ($blocking > 0));
|
||||||
|
|
||||||
|
if ($blocking > 0 || $hasBlockers) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'check_summary' => 'Blocking checks must be resolved before executing.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! filled($previewRanAt)) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'preview_ran_at' => 'Generate preview before executing.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! (bool) ($data['acknowledged_impact'] ?? false)) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'acknowledged_impact' => 'Please acknowledge that you reviewed the impact.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenantConfirm = $data['tenant_confirm'] ?? null;
|
||||||
|
|
||||||
|
if (! is_string($tenantConfirm) || $tenantConfirm !== $highlanderLabel) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'tenant_confirm' => 'Tenant hard-confirm does not match.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isDryRun) {
|
||||||
|
$restoreRun = $service->execute(
|
||||||
|
tenant: $tenant,
|
||||||
|
backupSet: $backupSet,
|
||||||
|
selectedItemIds: $selectedItemIds,
|
||||||
|
dryRun: true,
|
||||||
|
actorEmail: $actorEmail,
|
||||||
|
actorName: $actorName,
|
||||||
|
groupMapping: $groupMapping,
|
||||||
|
);
|
||||||
|
|
||||||
$metadata = $restoreRun->metadata ?? [];
|
$metadata = $restoreRun->metadata ?? [];
|
||||||
|
|
||||||
if (is_array($checkSummary)) {
|
if (is_array($checkSummary)) {
|
||||||
@ -1110,11 +1229,76 @@ public static function createRestoreRun(array $data): RestoreRun
|
|||||||
$metadata['preview_ran_at'] = $previewRanAt;
|
$metadata['preview_ran_at'] = $previewRanAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
$restoreRun->update([
|
$restoreRun->update(['metadata' => $metadata]);
|
||||||
'metadata' => $metadata,
|
|
||||||
]);
|
return $restoreRun->refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$preview = $service->preview($tenant, $backupSet, $selectedItemIds);
|
||||||
|
|
||||||
|
$metadata = [
|
||||||
|
'scope_mode' => $selectedItemIds === null ? 'all' : 'selected',
|
||||||
|
'environment' => app()->environment('production') ? 'prod' : 'test',
|
||||||
|
'highlander_label' => $highlanderLabel,
|
||||||
|
'confirmed_at' => now()->toIso8601String(),
|
||||||
|
'confirmed_by' => $actorEmail,
|
||||||
|
'confirmed_by_name' => $actorName,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (is_array($checkSummary)) {
|
||||||
|
$metadata['check_summary'] = $checkSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($checkResults)) {
|
||||||
|
$metadata['check_results'] = $checkResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($checksRanAt) && $checksRanAt !== '') {
|
||||||
|
$metadata['checks_ran_at'] = $checksRanAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($previewSummary)) {
|
||||||
|
$metadata['preview_summary'] = $previewSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($previewDiffs)) {
|
||||||
|
$metadata['preview_diffs'] = $previewDiffs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($previewRanAt) && $previewRanAt !== '') {
|
||||||
|
$metadata['preview_ran_at'] = $previewRanAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
$restoreRun = RestoreRun::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'backup_set_id' => $backupSet->id,
|
||||||
|
'requested_by' => $actorEmail,
|
||||||
|
'is_dry_run' => false,
|
||||||
|
'status' => RestoreRunStatus::Queued->value,
|
||||||
|
'requested_items' => $selectedItemIds,
|
||||||
|
'preview' => $preview,
|
||||||
|
'metadata' => $metadata,
|
||||||
|
'group_mapping' => $groupMapping !== [] ? $groupMapping : null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
app(AuditLogger::class)->log(
|
||||||
|
tenant: $tenant,
|
||||||
|
action: 'restore.queued',
|
||||||
|
context: [
|
||||||
|
'metadata' => [
|
||||||
|
'restore_run_id' => $restoreRun->id,
|
||||||
|
'backup_set_id' => $backupSet->id,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
actorEmail: $actorEmail,
|
||||||
|
actorName: $actorName,
|
||||||
|
resourceType: 'restore_run',
|
||||||
|
resourceId: (string) $restoreRun->id,
|
||||||
|
status: 'success',
|
||||||
|
);
|
||||||
|
|
||||||
|
ExecuteRestoreRunJob::dispatch($restoreRun->id, $actorEmail, $actorName);
|
||||||
|
|
||||||
return $restoreRun->refresh();
|
return $restoreRun->refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -22,8 +22,8 @@ public function getSteps(): array
|
|||||||
protected function getSubmitFormAction(): Action
|
protected function getSubmitFormAction(): Action
|
||||||
{
|
{
|
||||||
return parent::getSubmitFormAction()
|
return parent::getSubmitFormAction()
|
||||||
->label('Create preview (dry-run)')
|
->label('Create restore run')
|
||||||
->icon('heroicon-o-eye');
|
->icon('heroicon-o-check-circle');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function handleRecordCreation(array $data): Model
|
protected function handleRecordCreation(array $data): Model
|
||||||
|
|||||||
134
app/Jobs/ExecuteRestoreRunJob.php
Normal file
134
app/Jobs/ExecuteRestoreRunJob.php
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\RestoreRun;
|
||||||
|
use App\Services\Intune\AuditLogger;
|
||||||
|
use App\Services\Intune\RestoreService;
|
||||||
|
use App\Support\RestoreRunStatus;
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class ExecuteRestoreRunJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public int $restoreRunId,
|
||||||
|
public ?string $actorEmail = null,
|
||||||
|
public ?string $actorName = null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function handle(RestoreService $restoreService, AuditLogger $auditLogger): void
|
||||||
|
{
|
||||||
|
$restoreRun = RestoreRun::with(['tenant', 'backupSet'])->find($this->restoreRunId);
|
||||||
|
|
||||||
|
if (! $restoreRun) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($restoreRun->status !== RestoreRunStatus::Queued->value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant = $restoreRun->tenant;
|
||||||
|
$backupSet = $restoreRun->backupSet;
|
||||||
|
|
||||||
|
if (! $tenant || ! $backupSet || $backupSet->trashed()) {
|
||||||
|
$restoreRun->update([
|
||||||
|
'status' => RestoreRunStatus::Failed->value,
|
||||||
|
'failure_reason' => 'Backup set is archived or unavailable.',
|
||||||
|
'completed_at' => CarbonImmutable::now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($tenant) {
|
||||||
|
$auditLogger->log(
|
||||||
|
tenant: $tenant,
|
||||||
|
action: 'restore.failed',
|
||||||
|
context: [
|
||||||
|
'metadata' => [
|
||||||
|
'restore_run_id' => $restoreRun->id,
|
||||||
|
'backup_set_id' => $restoreRun->backup_set_id,
|
||||||
|
'reason' => 'Backup set is archived or unavailable.',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
actorEmail: $this->actorEmail,
|
||||||
|
actorName: $this->actorName,
|
||||||
|
resourceType: 'restore_run',
|
||||||
|
resourceId: (string) $restoreRun->id,
|
||||||
|
status: 'failed',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$restoreRun->update([
|
||||||
|
'status' => RestoreRunStatus::Running->value,
|
||||||
|
'started_at' => CarbonImmutable::now(),
|
||||||
|
'failure_reason' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$auditLogger->log(
|
||||||
|
tenant: $tenant,
|
||||||
|
action: 'restore.started',
|
||||||
|
context: [
|
||||||
|
'metadata' => [
|
||||||
|
'restore_run_id' => $restoreRun->id,
|
||||||
|
'backup_set_id' => $backupSet->id,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
actorEmail: $this->actorEmail,
|
||||||
|
actorName: $this->actorName,
|
||||||
|
resourceType: 'restore_run',
|
||||||
|
resourceId: (string) $restoreRun->id,
|
||||||
|
status: 'success',
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$restoreService->executeForRun(
|
||||||
|
restoreRun: $restoreRun,
|
||||||
|
tenant: $tenant,
|
||||||
|
backupSet: $backupSet,
|
||||||
|
actorEmail: $this->actorEmail,
|
||||||
|
actorName: $this->actorName,
|
||||||
|
);
|
||||||
|
} catch (Throwable $throwable) {
|
||||||
|
$restoreRun->refresh();
|
||||||
|
|
||||||
|
if ($restoreRun->status === RestoreRunStatus::Running->value) {
|
||||||
|
$restoreRun->update([
|
||||||
|
'status' => RestoreRunStatus::Failed->value,
|
||||||
|
'failure_reason' => $throwable->getMessage(),
|
||||||
|
'completed_at' => CarbonImmutable::now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tenant) {
|
||||||
|
$auditLogger->log(
|
||||||
|
tenant: $tenant,
|
||||||
|
action: 'restore.failed',
|
||||||
|
context: [
|
||||||
|
'metadata' => [
|
||||||
|
'restore_run_id' => $restoreRun->id,
|
||||||
|
'backup_set_id' => $backupSet->id,
|
||||||
|
'reason' => $throwable->getMessage(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
actorEmail: $this->actorEmail,
|
||||||
|
actorName: $this->actorName,
|
||||||
|
resourceType: 'restore_run',
|
||||||
|
resourceId: (string) $restoreRun->id,
|
||||||
|
status: 'failed',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $throwable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -184,6 +184,45 @@ public function executeFromPolicyVersion(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function executeForRun(
|
||||||
|
RestoreRun $restoreRun,
|
||||||
|
Tenant $tenant,
|
||||||
|
BackupSet $backupSet,
|
||||||
|
?string $actorEmail = null,
|
||||||
|
?string $actorName = null,
|
||||||
|
): RestoreRun {
|
||||||
|
$this->assertActiveContext($tenant, $backupSet);
|
||||||
|
|
||||||
|
if ($restoreRun->tenant_id !== $tenant->id) {
|
||||||
|
throw new \InvalidArgumentException('Restore run does not belong to the provided tenant.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($restoreRun->backup_set_id !== $backupSet->id) {
|
||||||
|
throw new \InvalidArgumentException('Restore run does not belong to the provided backup set.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($restoreRun->status, ['completed', 'partial', 'failed', 'cancelled'], true)) {
|
||||||
|
throw new \RuntimeException('Restore run is already finished.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedItemIds = is_array($restoreRun->requested_items) ? $restoreRun->requested_items : null;
|
||||||
|
|
||||||
|
if ($selectedItemIds === []) {
|
||||||
|
$selectedItemIds = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->execute(
|
||||||
|
tenant: $tenant,
|
||||||
|
backupSet: $backupSet,
|
||||||
|
selectedItemIds: $selectedItemIds,
|
||||||
|
dryRun: (bool) $restoreRun->is_dry_run,
|
||||||
|
actorEmail: $actorEmail,
|
||||||
|
actorName: $actorName,
|
||||||
|
groupMapping: $restoreRun->group_mapping ?? [],
|
||||||
|
existingRun: $restoreRun,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function execute(
|
public function execute(
|
||||||
Tenant $tenant,
|
Tenant $tenant,
|
||||||
BackupSet $backupSet,
|
BackupSet $backupSet,
|
||||||
@ -192,6 +231,7 @@ public function execute(
|
|||||||
?string $actorEmail = null,
|
?string $actorEmail = null,
|
||||||
?string $actorName = null,
|
?string $actorName = null,
|
||||||
array $groupMapping = [],
|
array $groupMapping = [],
|
||||||
|
?RestoreRun $existingRun = null,
|
||||||
): RestoreRun {
|
): RestoreRun {
|
||||||
$this->assertActiveContext($tenant, $backupSet);
|
$this->assertActiveContext($tenant, $backupSet);
|
||||||
|
|
||||||
@ -210,18 +250,46 @@ public function execute(
|
|||||||
'highlander_label' => (string) ($tenant->name ?? $tenantIdentifier ?? $tenant->getKey()),
|
'highlander_label' => (string) ($tenant->name ?? $tenantIdentifier ?? $tenant->getKey()),
|
||||||
];
|
];
|
||||||
|
|
||||||
$restoreRun = RestoreRun::create([
|
if ($existingRun !== null) {
|
||||||
'tenant_id' => $tenant->id,
|
if ($existingRun->tenant_id !== $tenant->id) {
|
||||||
'backup_set_id' => $backupSet->id,
|
throw new \InvalidArgumentException('Restore run does not belong to the provided tenant.');
|
||||||
'requested_by' => $actorEmail,
|
}
|
||||||
'is_dry_run' => $dryRun,
|
|
||||||
'status' => 'running',
|
if ($existingRun->backup_set_id !== $backupSet->id) {
|
||||||
'requested_items' => $selectedItemIds,
|
throw new \InvalidArgumentException('Restore run does not belong to the provided backup set.');
|
||||||
'preview' => $preview,
|
}
|
||||||
'started_at' => CarbonImmutable::now(),
|
|
||||||
'metadata' => $wizardMetadata,
|
$metadata = array_merge($wizardMetadata, $existingRun->metadata ?? []);
|
||||||
'group_mapping' => $groupMapping !== [] ? $groupMapping : null,
|
|
||||||
]);
|
$existingRun->update([
|
||||||
|
'requested_by' => $existingRun->requested_by ?? $actorEmail,
|
||||||
|
'is_dry_run' => $dryRun,
|
||||||
|
'status' => 'running',
|
||||||
|
'requested_items' => $selectedItemIds,
|
||||||
|
'preview' => $preview,
|
||||||
|
'results' => null,
|
||||||
|
'failure_reason' => null,
|
||||||
|
'started_at' => $existingRun->started_at ?? CarbonImmutable::now(),
|
||||||
|
'completed_at' => null,
|
||||||
|
'metadata' => $metadata,
|
||||||
|
'group_mapping' => $groupMapping !== [] ? $groupMapping : ($existingRun->group_mapping ?? null),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$restoreRun = $existingRun->refresh();
|
||||||
|
} else {
|
||||||
|
$restoreRun = RestoreRun::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'backup_set_id' => $backupSet->id,
|
||||||
|
'requested_by' => $actorEmail,
|
||||||
|
'is_dry_run' => $dryRun,
|
||||||
|
'status' => 'running',
|
||||||
|
'requested_items' => $selectedItemIds,
|
||||||
|
'preview' => $preview,
|
||||||
|
'started_at' => CarbonImmutable::now(),
|
||||||
|
'metadata' => $wizardMetadata,
|
||||||
|
'group_mapping' => $groupMapping !== [] ? $groupMapping : null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
if ($groupMapping !== []) {
|
if ($groupMapping !== []) {
|
||||||
$this->auditLogger->log(
|
$this->auditLogger->log(
|
||||||
|
|||||||
@ -30,12 +30,12 @@ ## Phase 5 — Preview (Diff)
|
|||||||
- [x] T014 Persist preview summary (and per-item diffs with safe limits) and require preview completion before execute.
|
- [x] T014 Persist preview summary (and per-item diffs with safe limits) and require preview completion before execute.
|
||||||
|
|
||||||
## Phase 6 — Confirm & Execute
|
## Phase 6 — Confirm & Execute
|
||||||
- [ ] T015 Implement Step 5 confirmations (ack checkbox + tenant hard-confirm).
|
- [x] T015 Implement Step 5 confirmations (ack checkbox + tenant hard-confirm).
|
||||||
- [ ] T016 Execute restore via a queued Job (preferred) and update statuses + timestamps.
|
- [x] T016 Execute restore via a queued Job (preferred) and update statuses + timestamps.
|
||||||
- [ ] T017 Persist execution outcomes and ensure audit logging entries exist for execution start/finish.
|
- [x] T017 Persist execution outcomes and ensure audit logging entries exist for execution start/finish.
|
||||||
|
|
||||||
## Phase 7 — Tests + Formatting
|
## Phase 7 — Tests + Formatting
|
||||||
- [ ] T018 Add Pest tests for wizard gating rules and status transitions.
|
- [x] T018 Add Pest tests for wizard gating rules and status transitions.
|
||||||
- [x] T019 Add Pest tests for safety checks persistence and blocking behavior.
|
- [x] T019 Add Pest tests for safety checks persistence and blocking behavior.
|
||||||
- [x] T020 Add Pest tests for preview summary generation.
|
- [x] T020 Add Pest tests for preview summary generation.
|
||||||
- [x] T021 Run `./vendor/bin/pint --dirty`.
|
- [x] T021 Run `./vendor/bin/pint --dirty`.
|
||||||
|
|||||||
68
tests/Feature/ExecuteRestoreRunJobTest.php
Normal file
68
tests/Feature/ExecuteRestoreRunJobTest.php
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Jobs\ExecuteRestoreRunJob;
|
||||||
|
use App\Models\BackupSet;
|
||||||
|
use App\Models\RestoreRun;
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use App\Services\Intune\AuditLogger;
|
||||||
|
use App\Services\Intune\RestoreService;
|
||||||
|
use App\Support\RestoreRunStatus;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Mockery\MockInterface;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
test('execute restore run job moves queued to running and calls the executor', function () {
|
||||||
|
$tenant = Tenant::create([
|
||||||
|
'tenant_id' => 'tenant-1',
|
||||||
|
'name' => 'Tenant One',
|
||||||
|
'metadata' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$backupSet = BackupSet::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'name' => 'Backup',
|
||||||
|
'status' => 'completed',
|
||||||
|
'item_count' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$restoreRun = RestoreRun::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'backup_set_id' => $backupSet->id,
|
||||||
|
'requested_by' => 'actor@example.com',
|
||||||
|
'is_dry_run' => false,
|
||||||
|
'status' => RestoreRunStatus::Queued->value,
|
||||||
|
'requested_items' => null,
|
||||||
|
'preview' => [],
|
||||||
|
'results' => null,
|
||||||
|
'metadata' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$restoreService = $this->mock(RestoreService::class, function (MockInterface $mock) use ($tenant, $backupSet) {
|
||||||
|
$mock->shouldReceive('executeForRun')
|
||||||
|
->once()
|
||||||
|
->withArgs(function (RestoreRun $run, Tenant $runTenant, BackupSet $runBackupSet, ?string $email, ?string $name) use ($tenant, $backupSet): bool {
|
||||||
|
return $run->status === RestoreRunStatus::Running->value
|
||||||
|
&& $runTenant->is($tenant)
|
||||||
|
&& $runBackupSet->is($backupSet)
|
||||||
|
&& $email === 'actor@example.com'
|
||||||
|
&& $name === 'Actor';
|
||||||
|
})
|
||||||
|
->andReturnUsing(function (RestoreRun $run): RestoreRun {
|
||||||
|
$run->update([
|
||||||
|
'status' => RestoreRunStatus::Completed->value,
|
||||||
|
'completed_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $run->refresh();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$job = new ExecuteRestoreRunJob($restoreRun->id, 'actor@example.com', 'Actor');
|
||||||
|
$job->handle($restoreService, app(AuditLogger::class));
|
||||||
|
|
||||||
|
$restoreRun->refresh();
|
||||||
|
|
||||||
|
expect($restoreRun->started_at)->not->toBeNull();
|
||||||
|
expect($restoreRun->status)->toBe(RestoreRunStatus::Completed->value);
|
||||||
|
});
|
||||||
@ -165,9 +165,6 @@
|
|||||||
])
|
])
|
||||||
->goToNextWizardStep()
|
->goToNextWizardStep()
|
||||||
->goToNextWizardStep()
|
->goToNextWizardStep()
|
||||||
->fillForm([
|
|
||||||
'is_dry_run' => true,
|
|
||||||
])
|
|
||||||
->callFormComponentAction('preview_diffs', 'run_restore_preview')
|
->callFormComponentAction('preview_diffs', 'run_restore_preview')
|
||||||
->goToNextWizardStep()
|
->goToNextWizardStep()
|
||||||
->call('create')
|
->call('create')
|
||||||
|
|||||||
165
tests/Feature/RestoreRunWizardExecuteTest.php
Normal file
165
tests/Feature/RestoreRunWizardExecuteTest.php
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Filament\Resources\RestoreRunResource\Pages\CreateRestoreRun;
|
||||||
|
use App\Jobs\ExecuteRestoreRunJob;
|
||||||
|
use App\Models\BackupItem;
|
||||||
|
use App\Models\BackupSet;
|
||||||
|
use App\Models\Policy;
|
||||||
|
use App\Models\RestoreRun;
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Support\RestoreRunStatus;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Bus;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
putenv('INTUNE_TENANT_ID');
|
||||||
|
unset($_ENV['INTUNE_TENANT_ID'], $_SERVER['INTUNE_TENANT_ID']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restore run wizard blocks execution when confirmations are missing', function () {
|
||||||
|
$tenant = Tenant::create([
|
||||||
|
'tenant_id' => 'tenant-1',
|
||||||
|
'name' => 'Tenant One',
|
||||||
|
'metadata' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
|
$policy = Policy::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'external_id' => 'policy-1',
|
||||||
|
'policy_type' => 'deviceConfiguration',
|
||||||
|
'display_name' => 'Device Config Policy',
|
||||||
|
'platform' => 'windows',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$backupSet = BackupSet::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'name' => 'Backup',
|
||||||
|
'status' => 'completed',
|
||||||
|
'item_count' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$backupItem = BackupItem::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'backup_set_id' => $backupSet->id,
|
||||||
|
'policy_id' => $policy->id,
|
||||||
|
'policy_identifier' => $policy->external_id,
|
||||||
|
'policy_type' => $policy->policy_type,
|
||||||
|
'platform' => $policy->platform,
|
||||||
|
'payload' => ['id' => $policy->external_id],
|
||||||
|
'metadata' => [
|
||||||
|
'displayName' => 'Backup Policy',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'email' => 'tester@example.com',
|
||||||
|
'name' => 'Tester',
|
||||||
|
]);
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
Livewire::test(CreateRestoreRun::class)
|
||||||
|
->fillForm([
|
||||||
|
'backup_set_id' => $backupSet->id,
|
||||||
|
])
|
||||||
|
->goToNextWizardStep()
|
||||||
|
->fillForm([
|
||||||
|
'scope_mode' => 'selected',
|
||||||
|
'backup_item_ids' => [$backupItem->id],
|
||||||
|
])
|
||||||
|
->goToNextWizardStep()
|
||||||
|
->callFormComponentAction('check_results', 'run_restore_checks')
|
||||||
|
->goToNextWizardStep()
|
||||||
|
->callFormComponentAction('preview_diffs', 'run_restore_preview')
|
||||||
|
->goToNextWizardStep()
|
||||||
|
->fillForm([
|
||||||
|
'is_dry_run' => false,
|
||||||
|
])
|
||||||
|
->call('create')
|
||||||
|
->assertHasFormErrors(['acknowledged_impact', 'tenant_confirm']);
|
||||||
|
|
||||||
|
expect(RestoreRun::count())->toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restore run wizard queues execution when gates are satisfied', function () {
|
||||||
|
Bus::fake();
|
||||||
|
|
||||||
|
$tenant = Tenant::create([
|
||||||
|
'tenant_id' => 'tenant-2',
|
||||||
|
'name' => 'Tenant Two',
|
||||||
|
'metadata' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
|
$policy = Policy::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'external_id' => 'policy-2',
|
||||||
|
'policy_type' => 'deviceConfiguration',
|
||||||
|
'display_name' => 'Device Config Policy',
|
||||||
|
'platform' => 'windows',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$backupSet = BackupSet::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'name' => 'Backup',
|
||||||
|
'status' => 'completed',
|
||||||
|
'item_count' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$backupItem = BackupItem::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'backup_set_id' => $backupSet->id,
|
||||||
|
'policy_id' => $policy->id,
|
||||||
|
'policy_identifier' => $policy->external_id,
|
||||||
|
'policy_type' => $policy->policy_type,
|
||||||
|
'platform' => $policy->platform,
|
||||||
|
'payload' => ['id' => $policy->external_id],
|
||||||
|
'metadata' => [
|
||||||
|
'displayName' => 'Backup Policy',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'email' => 'executor@example.com',
|
||||||
|
'name' => 'Executor',
|
||||||
|
]);
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
Livewire::test(CreateRestoreRun::class)
|
||||||
|
->fillForm([
|
||||||
|
'backup_set_id' => $backupSet->id,
|
||||||
|
])
|
||||||
|
->goToNextWizardStep()
|
||||||
|
->fillForm([
|
||||||
|
'scope_mode' => 'selected',
|
||||||
|
'backup_item_ids' => [$backupItem->id],
|
||||||
|
])
|
||||||
|
->goToNextWizardStep()
|
||||||
|
->callFormComponentAction('check_results', 'run_restore_checks')
|
||||||
|
->goToNextWizardStep()
|
||||||
|
->callFormComponentAction('preview_diffs', 'run_restore_preview')
|
||||||
|
->goToNextWizardStep()
|
||||||
|
->fillForm([
|
||||||
|
'is_dry_run' => false,
|
||||||
|
'acknowledged_impact' => true,
|
||||||
|
'tenant_confirm' => 'Tenant Two',
|
||||||
|
])
|
||||||
|
->call('create')
|
||||||
|
->assertHasNoFormErrors();
|
||||||
|
|
||||||
|
$run = RestoreRun::query()->latest('id')->first();
|
||||||
|
|
||||||
|
expect($run)->not->toBeNull();
|
||||||
|
expect($run->status)->toBe(RestoreRunStatus::Queued->value);
|
||||||
|
expect($run->is_dry_run)->toBeFalse();
|
||||||
|
expect($run->metadata['confirmed_by'] ?? null)->toBe('executor@example.com');
|
||||||
|
expect($run->metadata['confirmed_at'] ?? null)->toBeString();
|
||||||
|
|
||||||
|
Bus::assertDispatched(ExecuteRestoreRunJob::class);
|
||||||
|
});
|
||||||
@ -62,9 +62,6 @@
|
|||||||
])
|
])
|
||||||
->goToNextWizardStep()
|
->goToNextWizardStep()
|
||||||
->goToNextWizardStep()
|
->goToNextWizardStep()
|
||||||
->fillForm([
|
|
||||||
'is_dry_run' => true,
|
|
||||||
])
|
|
||||||
->callFormComponentAction('preview_diffs', 'run_restore_preview')
|
->callFormComponentAction('preview_diffs', 'run_restore_preview')
|
||||||
->goToNextWizardStep()
|
->goToNextWizardStep()
|
||||||
->call('create')
|
->call('create')
|
||||||
@ -87,55 +84,3 @@
|
|||||||
expect($run->metadata['environment'])->toBe('test');
|
expect($run->metadata['environment'])->toBe('test');
|
||||||
expect($run->metadata['highlander_label'])->toBe('Tenant One');
|
expect($run->metadata['highlander_label'])->toBe('Tenant One');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('restore run wizard always creates dry-run previews in phase 2', function () {
|
|
||||||
$tenant = Tenant::create([
|
|
||||||
'tenant_id' => 'tenant-2',
|
|
||||||
'name' => 'Tenant Two',
|
|
||||||
'metadata' => [],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$tenant->makeCurrent();
|
|
||||||
|
|
||||||
$backupSet = BackupSet::create([
|
|
||||||
'tenant_id' => $tenant->id,
|
|
||||||
'name' => 'Backup',
|
|
||||||
'status' => 'completed',
|
|
||||||
'item_count' => 1,
|
|
||||||
]);
|
|
||||||
|
|
||||||
BackupItem::create([
|
|
||||||
'tenant_id' => $tenant->id,
|
|
||||||
'backup_set_id' => $backupSet->id,
|
|
||||||
'policy_id' => null,
|
|
||||||
'policy_identifier' => 'policy-2',
|
|
||||||
'policy_type' => 'deviceConfiguration',
|
|
||||||
'platform' => 'windows',
|
|
||||||
'payload' => ['id' => 'policy-2'],
|
|
||||||
'metadata' => [
|
|
||||||
'displayName' => 'Backup Policy Two',
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$user = User::factory()->create();
|
|
||||||
$this->actingAs($user);
|
|
||||||
|
|
||||||
Livewire::test(CreateRestoreRun::class)
|
|
||||||
->fillForm([
|
|
||||||
'backup_set_id' => $backupSet->id,
|
|
||||||
])
|
|
||||||
->goToNextWizardStep()
|
|
||||||
->goToNextWizardStep()
|
|
||||||
->goToNextWizardStep()
|
|
||||||
->set('data.is_dry_run', false)
|
|
||||||
->callFormComponentAction('preview_diffs', 'run_restore_preview')
|
|
||||||
->goToNextWizardStep()
|
|
||||||
->call('create')
|
|
||||||
->assertHasNoFormErrors();
|
|
||||||
|
|
||||||
$run = RestoreRun::query()->latest('id')->first();
|
|
||||||
|
|
||||||
expect($run)->not->toBeNull();
|
|
||||||
expect($run->is_dry_run)->toBeTrue();
|
|
||||||
expect($run->status)->toBe('previewed');
|
|
||||||
});
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user