feat/011-restore-run-wizard #37

Merged
ahmido merged 26 commits from feat/011-restore-run-wizard into dev 2026-01-07 01:40:04 +00:00
2 changed files with 163 additions and 6 deletions
Showing only changes of commit 9e3c2b3011 - Show all commits

View File

@ -397,11 +397,7 @@ public static function getWizardSteps(): array
$selectedItemIds = is_array($selectedItemIds) ? $selectedItemIds : null; $selectedItemIds = is_array($selectedItemIds) ? $selectedItemIds : null;
$groupMapping = $get('group_mapping') ?? []; $groupMapping = static::normalizeGroupMapping($get('group_mapping'));
$groupMapping = is_array($groupMapping) ? $groupMapping : [];
$groupMapping = collect($groupMapping)
->map(fn ($value) => is_string($value) ? $value : null)
->all();
$checker = app(RestoreRiskChecker::class); $checker = app(RestoreRiskChecker::class);
$outcome = $checker->check( $outcome = $checker->check(
@ -1143,7 +1139,7 @@ public static function createRestoreRun(array $data): RestoreRun
$actorEmail = auth()->user()?->email; $actorEmail = auth()->user()?->email;
$actorName = auth()->user()?->name; $actorName = auth()->user()?->name;
$isDryRun = (bool) ($data['is_dry_run'] ?? true); $isDryRun = (bool) ($data['is_dry_run'] ?? true);
$groupMapping = $data['group_mapping'] ?? []; $groupMapping = static::normalizeGroupMapping($data['group_mapping'] ?? null);
$checkSummary = $data['check_summary'] ?? null; $checkSummary = $data['check_summary'] ?? null;
$checkResults = $data['check_results'] ?? null; $checkResults = $data['check_results'] ?? null;
@ -1386,6 +1382,76 @@ private static function unresolvedGroups(?int $backupSetId, ?array $selectedItem
return $unresolved; return $unresolved;
} }
/**
* @return array<string, string|null>
*/
private static function normalizeGroupMapping(mixed $mapping): array
{
if ($mapping instanceof \Illuminate\Contracts\Support\Arrayable) {
$mapping = $mapping->toArray();
}
if ($mapping instanceof \stdClass) {
$mapping = (array) $mapping;
}
if (! is_array($mapping)) {
return [];
}
$result = [];
if (array_key_exists('group_mapping', $mapping)) {
$nested = $mapping['group_mapping'];
if ($nested instanceof \Illuminate\Contracts\Support\Arrayable) {
$nested = $nested->toArray();
}
if ($nested instanceof \stdClass) {
$nested = (array) $nested;
}
if (is_array($nested)) {
$mapping = $nested;
}
}
foreach ($mapping as $key => $value) {
if (! is_string($key) || $key === '') {
continue;
}
$sourceGroupId = str_starts_with($key, 'group_mapping.')
? substr($key, strlen('group_mapping.'))
: $key;
if ($sourceGroupId === '') {
continue;
}
if ($value instanceof BackedEnum) {
$value = $value->value;
}
if (is_array($value) || $value instanceof \stdClass) {
$value = (array) $value;
$value = $value['value'] ?? $value['id'] ?? null;
}
if (is_string($value)) {
$value = trim($value);
$result[$sourceGroupId] = $value !== '' ? $value : null;
continue;
}
$result[$sourceGroupId] = null;
}
return $result;
}
/** /**
* @return array<string, string> * @return array<string, string>
*/ */

View File

@ -129,3 +129,94 @@
]); ]);
expect($run->metadata['check_summary']['blocking'] ?? null)->toBe(1); expect($run->metadata['check_summary']['blocking'] ?? null)->toBe(1);
}); });
test('restore wizard treats skipped orphaned groups as a warning instead of a blocker', 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' => 'settingsCatalogPolicy',
'display_name' => 'Settings Catalog',
'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,
'captured_at' => now(),
'payload' => ['id' => $policy->external_id],
'assignments' => [[
'target' => [
'@odata.type' => '#microsoft.graph.groupAssignmentTarget',
'groupId' => 'source-group-1',
'group_display_name' => 'Source Group',
],
'intent' => 'apply',
]],
]);
$this->mock(GroupResolver::class, function (MockInterface $mock) {
$mock->shouldReceive('resolveGroupIds')
->andReturnUsing(function (array $groupIds): array {
return collect($groupIds)
->mapWithKeys(fn (string $id) => [$id => [
'id' => $id,
'displayName' => null,
'orphaned' => true,
]])
->all();
});
});
$user = User::factory()->create();
$this->actingAs($user);
$component = Livewire::test(CreateRestoreRun::class)
->fillForm([
'backup_set_id' => $backupSet->id,
])
->goToNextWizardStep()
->fillForm([
'scope_mode' => 'selected',
'backup_item_ids' => [$backupItem->id],
])
->goToNextWizardStep()
->set('data.group_mapping', (object) [
'source-group-1' => 'SKIP',
])
->callFormComponentAction('check_results', 'run_restore_checks');
$summary = $component->get('data.check_summary');
$results = $component->get('data.check_results');
expect($summary)->toBeArray();
expect($summary['blocking'] ?? null)->toBe(0);
expect($summary['has_blockers'] ?? null)->toBeFalse();
expect($summary['warning'] ?? null)->toBe(1);
$assignmentCheck = collect($results)->firstWhere('code', 'assignment_groups');
expect($assignmentCheck)->toBeArray();
expect($assignmentCheck['severity'] ?? null)->toBe('warning');
$skippedGroups = $assignmentCheck['meta']['skipped'] ?? [];
expect($skippedGroups)->toBeArray();
expect($skippedGroups[0]['id'] ?? null)->toBe('source-group-1');
});