bind(GraphClientInterface::class, fn () => new class implements GraphClientInterface { 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 { return new GraphResponse(true, []); } public function request(string $method, string $path, array $options = []): GraphResponse { return new GraphResponse(true, []); } public function getServicePrincipalPermissions(array $options = []): GraphResponse { return new GraphResponse(true, []); } }); config()->set('graph_contracts.types.deviceConfiguration.assignments_payload_key', 'assignments'); $tenant = Tenant::create([ 'tenant_id' => 'tenant-1', 'name' => 'Tenant One', 'metadata' => [], ]); $policy = Policy::create([ 'tenant_id' => $tenant->id, 'external_id' => 'policy-1', 'policy_type' => 'deviceConfiguration', 'display_name' => 'Policy A', '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' => [ 'foo' => 'bar', 'roleScopeTagIds' => ['0', 'scope-1'], ], 'metadata' => [ 'scope_tag_ids' => ['0', 'scope-1'], 'scope_tag_names' => ['Default', 'Verbund-1'], ], 'assignments' => [ [ 'target' => [ '@odata.type' => '#microsoft.graph.groupAssignmentTarget', 'groupId' => 'group-1', 'group_display_name' => 'Group One', ], 'intent' => 'apply', ], ], ]); $user = User::factory()->create(['email' => 'tester@example.com']); $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, ); expect($run->status)->toBe('completed'); $result = $run->results['items'][$backupItem->id] ?? null; expect($result)->not->toBeNull(); expect($result['status'] ?? null)->toBe('applied'); $this->assertDatabaseHas('audit_logs', [ 'action' => 'restore.executed', 'resource_id' => (string) $run->id, ]); expect(PolicyVersion::where('policy_id', $policy->id)->count())->toBe(1); $version = PolicyVersion::where('policy_id', $policy->id)->first(); expect($version)->not->toBeNull(); expect($version->scope_tags)->toBe([ 'ids' => ['0', 'scope-1'], 'names' => ['Default', 'Verbund-1'], ]); expect($version->assignments)->toBe($backupItem->assignments); }); test('restore execution records foundation mappings', function () { config()->set('tenantpilot.foundation_types', [ [ 'type' => 'assignmentFilter', 'label' => 'Assignment Filter', 'category' => 'Foundations', 'platform' => 'all', 'endpoint' => 'deviceManagement/assignmentFilters', 'backup' => 'full', 'restore' => 'enabled', 'risk' => 'low', ], ]); $tenant = Tenant::factory()->create(); $backupSet = BackupSet::factory()->for($tenant)->create(); $backupItem = BackupItem::factory() ->for($tenant) ->for($backupSet) ->state([ 'policy_id' => null, 'policy_identifier' => 'filter-1', 'policy_type' => 'assignmentFilter', 'platform' => 'all', 'payload' => [ 'id' => 'filter-1', 'displayName' => 'Filter One', ], 'metadata' => [ 'displayName' => 'Filter One', ], ]) ->create(); $entries = [ [ 'type' => 'assignmentFilter', 'sourceId' => 'filter-1', 'sourceName' => 'Filter One', 'decision' => 'created', 'targetId' => 'filter-2', 'targetName' => 'Filter One', ], ]; $this->mock(FoundationMappingService::class, function (MockInterface $mock) use ($entries) { $mock->shouldReceive('map') ->twice() ->andReturn([ 'entries' => $entries, 'mapping' => ['filter-1' => 'filter-2'], 'failed' => 0, 'skipped' => 0, ]); }); $user = User::factory()->create(['email' => 'tester@example.com']); $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, ); expect($run->status)->toBe('completed'); expect($run->results['foundations'] ?? [])->toHaveCount(1); expect(($run->results['foundations'][0]['decision'] ?? null))->toBe('created'); $this->assertDatabaseHas('audit_logs', [ 'action' => 'restore.foundation.created', 'resource_id' => (string) $run->id, ]); }); test('restore execution records compliance notification mapping outcomes', function () { app()->bind(GraphClientInterface::class, fn () => new class implements GraphClientInterface { 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 { return new GraphResponse(true, []); } public function request(string $method, string $path, array $options = []): GraphResponse { return new GraphResponse(true, []); } public function getServicePrincipalPermissions(array $options = []): GraphResponse { return new GraphResponse(true, []); } }); $tenant = Tenant::create([ 'tenant_id' => 'tenant-3', 'name' => 'Tenant Three', 'metadata' => [], ]); $policy = Policy::create([ 'tenant_id' => $tenant->id, 'external_id' => 'policy-3', 'policy_type' => 'deviceCompliancePolicy', 'display_name' => 'Compliance 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' => [ 'scheduledActionsForRule' => [ [ 'ruleName' => 'Password', 'scheduledActionConfigurations' => [ [ 'actionType' => 'notification', 'notificationTemplateId' => 'template-1', ], ], ], ], ], ]); $user = User::factory()->create(['email' => 'tester@example.com']); $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, ); expect($run->status)->toBe('partial'); $result = $run->results['items'][$backupItem->id] ?? null; expect($result)->not->toBeNull(); expect($result['status'] ?? null)->toBe('partial'); expect($result['compliance_action_summary']['skipped'] ?? null)->toBe(1); $this->assertDatabaseHas('audit_logs', [ 'action' => 'restore.compliance.actions.mapped', 'resource_id' => (string) $run->id, ]); }); test('restore execution applies scope tags even without foundation mapping', function () { $client = new class implements GraphClientInterface { public array $applied = []; 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->applied[] = [ 'policyType' => $policyType, 'policyId' => $policyId, 'payload' => $payload, 'options' => $options, ]; return new GraphResponse(true, []); } public function request(string $method, string $path, array $options = []): GraphResponse { return new GraphResponse(true, []); } public function getServicePrincipalPermissions(array $options = []): GraphResponse { return new GraphResponse(true, []); } }; app()->instance(GraphClientInterface::class, $client); $tenant = Tenant::factory()->create([ 'tenant_id' => 'tenant-scope-tags', 'name' => 'Tenant Scope Tags', 'status' => 'active', 'metadata' => [], ]); $policy = Policy::create([ 'tenant_id' => $tenant->id, 'external_id' => 'app-1', 'policy_type' => 'mobileApp', 'display_name' => 'Mozilla Firefox', 'platform' => 'all', ]); $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' => [ '@odata.type' => '#microsoft.graph.winGetApp', 'displayName' => 'Mozilla Firefox', 'roleScopeTagIds' => ['0', 'tag-1'], ], ]); $user = User::factory()->create(['email' => 'tester@example.com']); $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, ); expect($run->status)->toBe('completed'); expect($client->applied)->toHaveCount(1); expect($client->applied[0]['policyType'])->toBe('mobileApp'); expect($client->applied[0]['policyId'])->toBe('app-1'); expect($client->applied[0]['payload']['roleScopeTagIds'])->toBe(['0', 'tag-1']); }); test('restore execution creates an autopilot profile when missing', function () { $graphClient = new class implements GraphClientInterface { public int $applyCalls = 0; public int $getCalls = 0; public int $createCalls = 0; public array $createPayloads = []; public function listPolicies(string $policyType, array $options = []): GraphResponse { return new GraphResponse(true, []); } public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse { $this->getCalls++; return new GraphResponse(false, [], 404, [], [], [ 'error_code' => 'ResourceNotFound', 'error_message' => 'Resource not found.', ]); } public function getOrganization(array $options = []): GraphResponse { return new GraphResponse(true, []); } public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse { $this->applyCalls++; return new GraphResponse(false, [], 500, [], [], [ 'error_code' => 'InternalServerError', 'error_message' => 'An internal server error has occurred.', ]); } public function request(string $method, string $path, array $options = []): GraphResponse { if ($method === 'POST' && str_contains($path, 'windowsAutopilotDeploymentProfiles')) { $this->createCalls++; $this->createPayloads[] = $options['json'] ?? []; return new GraphResponse(true, ['id' => 'autopilot-created']); } return new GraphResponse(true, []); } public function getServicePrincipalPermissions(array $options = []): GraphResponse { return new GraphResponse(true, []); } }; app()->instance(GraphClientInterface::class, $graphClient); $tenant = Tenant::factory()->create([ 'tenant_id' => 'tenant-1', 'name' => 'Tenant One', 'metadata' => [], ]); $backupSet = BackupSet::factory()->for($tenant)->create([ 'status' => 'completed', 'item_count' => 1, ]); $backupItem = BackupItem::factory() ->for($tenant) ->for($backupSet) ->state([ 'policy_id' => null, 'policy_identifier' => 'autopilot-1', 'policy_type' => 'windowsAutopilotDeploymentProfile', 'platform' => 'windows', 'payload' => [ '@odata.type' => '#microsoft.graph.azureADWindowsAutopilotDeploymentProfile', 'displayName' => 'Autopilot Profile', 'language' => 'en-US', ], ]) ->create(); $user = User::factory()->create(['email' => 'tester@example.com']); $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, ); expect($graphClient->applyCalls)->toBe(1); expect($graphClient->getCalls)->toBe(1); expect($graphClient->createCalls)->toBe(1); expect($graphClient->createPayloads[0]['displayName'] ?? null)->toBe('Restored_Autopilot Profile'); expect($run->status)->toBe('completed'); $result = $run->results['items'][$backupItem->id] ?? null; expect($result)->not->toBeNull(); expect($result['status'] ?? null)->toBe('applied'); expect($result['created_policy_id'] ?? null)->toBe('autopilot-created'); }); test('restore execution creates missing policy using contracts', function () { $graphClient = new class implements GraphClientInterface { public int $applyCalls = 0; public int $createCalls = 0; public array $createPayloads = []; 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->applyCalls++; return new GraphResponse(false, [], 404, [], [], [ 'error_code' => 'ResourceNotFound', 'error_message' => 'Resource not found.', ]); } public function request(string $method, string $path, array $options = []): GraphResponse { if ($method === 'POST' && $path === 'deviceManagement/deviceCompliancePolicies') { $this->createCalls++; $this->createPayloads[] = $options['json'] ?? []; return new GraphResponse(true, ['id' => 'compliance-created']); } return new GraphResponse(true, []); } public function getServicePrincipalPermissions(array $options = []): GraphResponse { return new GraphResponse(true, []); } }; app()->instance(GraphClientInterface::class, $graphClient); $tenant = Tenant::factory()->create([ 'tenant_id' => 'tenant-4', 'name' => 'Tenant Four', 'metadata' => [], ]); $backupSet = BackupSet::factory()->for($tenant)->create([ 'status' => 'completed', 'item_count' => 1, ]); $backupItem = BackupItem::factory() ->for($tenant) ->for($backupSet) ->state([ 'policy_id' => null, 'policy_identifier' => 'compliance-1', 'policy_type' => 'deviceCompliancePolicy', 'platform' => 'windows', 'payload' => [ '@odata.type' => '#microsoft.graph.windows10CompliancePolicy', 'displayName' => 'Compliance Policy', 'description' => 'Test policy', ], ]) ->create(); $user = User::factory()->create(['email' => 'tester@example.com']); $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, ); expect($graphClient->applyCalls)->toBe(1); expect($graphClient->createCalls)->toBe(1); expect($graphClient->createPayloads[0]['displayName'] ?? null)->toBe('Restored_Compliance Policy'); expect($run->status)->toBe('completed'); $result = $run->results['items'][$backupItem->id] ?? null; expect($result)->not->toBeNull(); expect($result['status'] ?? null)->toBe('applied'); expect($result['created_policy_id'] ?? null)->toBe('compliance-created'); });