From 1d158ca9bfe8c93c6322db7e67f768ef68a47a69 Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Sat, 3 Jan 2026 05:13:13 +0100 Subject: [PATCH] fix: treat unknown restore types as preview-only --- app/Services/Intune/RestoreRiskChecker.php | 12 +- app/Services/Intune/RestoreService.php | 5 + .../RestoreUnknownPolicyTypeSafetyTest.php | 117 ++++++++++++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/RestoreUnknownPolicyTypeSafetyTest.php diff --git a/app/Services/Intune/RestoreRiskChecker.php b/app/Services/Intune/RestoreRiskChecker.php index b2e2136..84be502 100644 --- a/app/Services/Intune/RestoreRiskChecker.php +++ b/app/Services/Intune/RestoreRiskChecker.php @@ -756,7 +756,17 @@ private function resolveRestoreMode(?string $policyType): string { $meta = $this->resolveTypeMeta($policyType); - return (string) ($meta['restore'] ?? 'enabled'); + if ($meta === []) { + return 'preview-only'; + } + + $restore = $meta['restore'] ?? 'enabled'; + + if (! is_string($restore) || $restore === '') { + return 'enabled'; + } + + return $restore; } private function resolveTypeLabel(?string $policyType): string diff --git a/app/Services/Intune/RestoreService.php b/app/Services/Intune/RestoreService.php index 3b20271..0fb6cc8 100644 --- a/app/Services/Intune/RestoreService.php +++ b/app/Services/Intune/RestoreService.php @@ -931,6 +931,11 @@ private function resolveTypeMeta(string $policyType): array private function resolveRestoreMode(string $policyType): string { $meta = $this->resolveTypeMeta($policyType); + + if ($meta === []) { + return 'preview-only'; + } + $restore = $meta['restore'] ?? 'enabled'; if (! is_string($restore) || $restore === '') { diff --git a/tests/Feature/RestoreUnknownPolicyTypeSafetyTest.php b/tests/Feature/RestoreUnknownPolicyTypeSafetyTest.php new file mode 100644 index 0000000..f66f7c6 --- /dev/null +++ b/tests/Feature/RestoreUnknownPolicyTypeSafetyTest.php @@ -0,0 +1,117 @@ +}> */ + public array $applyPolicyCalls = []; + + public function listPolicies(string $policyType, array $options = []): GraphResponse + { + return new GraphResponse(true, []); + } + + public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse + { + return new GraphResponse(true, ['payload' => []]); + } + + public function getOrganization(array $options = []): GraphResponse + { + return new GraphResponse(true, []); + } + + public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse + { + $this->applyPolicyCalls[] = [ + 'policyType' => $policyType, + 'policyId' => $policyId, + 'payload' => $payload, + 'options' => $options, + ]; + + return new GraphResponse(false, ['error' => ['message' => 'Bad request']], 400, [], [], [ + 'error_code' => 'BadRequest', + 'error_message' => 'Bad request', + ]); + } + + public function getServicePrincipalPermissions(array $options = []): GraphResponse + { + return new GraphResponse(true, []); + } + + public function request(string $method, string $path, array $options = []): GraphResponse + { + return new GraphResponse(true, []); + } +} + +beforeEach(function () { + $this->originalSupportedTypes = config('tenantpilot.supported_policy_types'); + $this->originalSecurityBaselineContract = config('graph_contracts.types.securityBaselinePolicy'); +}); + +afterEach(function () { + config()->set('tenantpilot.supported_policy_types', $this->originalSupportedTypes); + + if (is_array($this->originalSecurityBaselineContract)) { + config()->set('graph_contracts.types.securityBaselinePolicy', $this->originalSecurityBaselineContract); + } +}); + +test('restore skips security baseline policies when type metadata is missing', function () { + $client = new RestoreUnknownTypeGraphClient; + app()->instance(GraphClientInterface::class, $client); + + $supported = array_values(array_filter( + config('tenantpilot.supported_policy_types', []), + static fn (array $type): bool => ($type['type'] ?? null) !== 'securityBaselinePolicy' + )); + + config()->set('tenantpilot.supported_policy_types', $supported); + config()->set('graph_contracts.types.securityBaselinePolicy', []); + + $tenant = Tenant::factory()->create(); + $backupSet = BackupSet::factory()->for($tenant)->create([ + 'status' => 'completed', + 'item_count' => 1, + ]); + + $backupItem = BackupItem::factory()->for($tenant)->for($backupSet)->create([ + 'policy_id' => null, + 'policy_identifier' => 'baseline-1', + 'policy_type' => 'securityBaselinePolicy', + 'platform' => 'windows', + 'payload' => [ + 'id' => 'baseline-1', + '@odata.type' => '#microsoft.graph.deviceManagementConfigurationPolicy', + 'name' => 'Security Baseline Policy', + ], + 'assignments' => null, + ]); + + $service = app(RestoreService::class); + $run = $service->execute( + tenant: $tenant, + backupSet: $backupSet, + selectedItemIds: [$backupItem->id], + dryRun: false, + ); + + expect($client->applyPolicyCalls)->toHaveCount(0); + + $result = $run->results[0] ?? null; + expect($result)->toBeArray(); + expect($result['status'] ?? null)->toBe('skipped'); + expect($result['restore_mode'] ?? null)->toBe('preview-only'); +});