*/ public array $applyPolicyCalls = []; /** * @var array */ public array $requestCalls = []; /** * @param array $requestResponses */ public function __construct( private readonly GraphResponse $applyPolicyResponse, private array $requestResponses = [], ) {} 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[] = [ 'policy_type' => $policyType, 'policy_id' => $policyId, 'payload' => $payload, ]; return $this->applyPolicyResponse; } public function getServicePrincipalPermissions(array $options = []): GraphResponse { return new GraphResponse(true, []); } public function request(string $method, string $path, array $options = []): GraphResponse { $this->requestCalls[] = [ 'method' => strtoupper($method), 'path' => $path, 'payload' => $options['json'] ?? null, ]; $response = array_shift($this->requestResponses); return $response ?? new GraphResponse(true, []); } } test('restore marks settings catalog policy as partial when a setting PATCH fails', function () { $policyResponse = new GraphResponse( success: true, data: [], status: 200, errors: [], warnings: [], meta: ['request_id' => 'req-policy', 'client_request_id' => 'client-policy'], ); $graphResponse = new GraphResponse( success: false, data: ['error' => ['code' => 'BadRequest', 'message' => 'settings are read-only']], status: 400, errors: [['code' => 'BadRequest', 'message' => 'settings are read-only']], warnings: [], meta: [ 'error_code' => 'BadRequest', 'error_message' => 'settings are read-only', 'request_id' => 'req-123', 'client_request_id' => 'client-abc', ], ); $client = new SettingsCatalogRestoreGraphClient($policyResponse, [$graphResponse]); app()->instance(GraphClientInterface::class, $client); $tenant = Tenant::create([ 'tenant_id' => 'tenant-1', 'name' => 'Tenant One', 'metadata' => [], ]); $policy = Policy::create([ 'tenant_id' => $tenant->id, 'external_id' => 'scp-1', 'policy_type' => 'settingsCatalogPolicy', 'display_name' => 'Settings Catalog Alpha', 'platform' => 'windows', ]); $backupSet = BackupSet::create([ 'tenant_id' => $tenant->id, 'name' => 'Backup', 'status' => 'completed', 'item_count' => 1, ]); $payload = [ 'id' => 'scp-1', 'displayName' => 'Settings Catalog Alpha', 'version' => 3, '@odata.type' => '#microsoft.graph.deviceManagementConfigurationPolicy', 'createdDateTime' => '2024-01-01T00:00:00Z', 'Settings' => [ [ 'id' => 'setting-1', 'settingInstance' => [ '@odata.type' => '#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance', 'settingDefinitionId' => 'setting_definition', 'simpleSettingValue' => [ '@odata.type' => '#microsoft.graph.deviceManagementConfigurationStringSettingValue', 'value' => 'test-value', ], ], ], ], 'assignments' => [ ['id' => 'assignment-1'], ], 'Platforms' => ['windows'], ]; $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' => $payload, ]); $user = User::factory()->create(); $this->actingAs($user); $service = app(RestoreService::class); $run = $service->execute( tenant: $tenant, backupSet: $backupSet, selectedItemIds: [$backupItem->id], dryRun: false, actorEmail: $user->email, actorName: $user->name, )->refresh(); expect($run->status)->toBe('partial'); expect($run->results[0]['status'])->toBe('partial'); expect($run->results[0]['settings_apply']['failed'])->toBe(1); expect($run->results[0]['settings_apply']['issues'][0]['graph_error_message'])->toContain('settings are read-only'); expect($run->results[0]['settings_apply']['issues'][0]['graph_request_id'])->toBe('req-123'); expect($run->results[0]['settings_apply']['issues'][0]['graph_client_request_id'])->toBe('client-abc'); expect($client->applyPolicyCalls)->toHaveCount(1); expect($client->applyPolicyCalls[0]['payload'])->toHaveKey('name'); expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('id'); expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('@odata.type'); expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('version'); expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('assignments'); expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('platforms'); expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('settings'); expect($client->requestCalls)->toHaveCount(1); expect($client->requestCalls[0]['method'])->toBe('PATCH'); expect($client->requestCalls[0]['path'])->toBe('deviceManagement/configurationPolicies/scp-1/settings/setting-1'); expect($client->requestCalls[0]['payload'])->toBeArray(); expect($client->requestCalls[0]['payload'])->toHaveKey('@odata.type'); expect($client->requestCalls[0]['payload'])->not->toHaveKey('id'); expect($client->requestCalls[0]['payload']['settingInstance']['@odata.type'])->toBe('#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance'); $response = $this ->get(route('filament.admin.resources.restore-runs.view', ['record' => $run])); $response->assertOk(); $response->assertSee('settings are read-only'); $response->assertSee('req-123'); }); test('restore success for settings catalog uses strict payload', function () { $graphResponse = new GraphResponse( success: true, data: [], status: 200, errors: [], warnings: [], meta: ['request_id' => 'req-success', 'client_request_id' => 'client-success'], ); $client = new SettingsCatalogRestoreGraphClient($graphResponse); app()->instance(GraphClientInterface::class, $client); $tenant = Tenant::create([ 'tenant_id' => 'tenant-2', 'name' => 'Tenant Two', 'metadata' => [], ]); $policy = Policy::create([ 'tenant_id' => $tenant->id, 'external_id' => 'scp-2', 'policy_type' => 'settingsCatalogPolicy', 'display_name' => 'Settings Catalog Beta', 'platform' => 'windows', ]); $backupSet = BackupSet::create([ 'tenant_id' => $tenant->id, 'name' => 'Backup', 'status' => 'completed', 'item_count' => 1, ]); $payload = [ 'displayName' => 'Settings Catalog Beta', 'Description' => 'desc', 'Platforms' => ['windows'], 'Settings' => [ [ 'id' => 'setting-1', 'settingInstance' => [ '@odata.type' => '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance', 'settingDefinitionId' => 'test_setting', 'choiceSettingValue' => [ 'value' => 'test_value', ], ], ], ], ]; $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' => $payload, ]); $user = User::factory()->create(); $this->actingAs($user); $service = app(RestoreService::class); $run = $service->execute( tenant: $tenant, backupSet: $backupSet, selectedItemIds: [$backupItem->id], dryRun: false, actorEmail: $user->email, actorName: $user->name, )->refresh(); expect($run->status)->toBe('completed'); expect($client->applyPolicyCalls)->toHaveCount(1); expect($client->applyPolicyCalls[0]['payload'])->toHaveKeys(['name', 'description']); expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('platforms'); expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('Platforms'); expect($client->applyPolicyCalls[0]['payload'])->not->toHaveKey('settings'); expect($client->applyPolicyCalls[0]['payload']['name'])->toBe('Settings Catalog Beta'); expect($client->applyPolicyCalls[0]['payload']['description'])->toBe('desc'); expect($client->requestCalls)->toHaveCount(1); expect($client->requestCalls[0]['method'])->toBe('PATCH'); expect($client->requestCalls[0]['path'])->toBe('deviceManagement/configurationPolicies/scp-2/settings/setting-1'); // Ensure we preserved settingInstance @odata.type and stripped ids in the per-setting call expect($client->requestCalls[0]['payload'])->toHaveKey('@odata.type'); expect($client->requestCalls[0]['payload']['@odata.type'])->toBe('#microsoft.graph.deviceManagementConfigurationSetting'); expect($client->requestCalls[0]['payload'])->not->toHaveKey('id'); expect($client->requestCalls[0]['payload']['settingInstance'])->toHaveKey('@odata.type'); expect($client->requestCalls[0]['payload']['settingInstance']['@odata.type'])->toBe('#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'); });