actingAs($user); $backupSet = BackupSet::factory()->create([ 'tenant_id' => $tenant->id, 'name' => 'Test backup', 'status' => 'completed', 'metadata' => ['failures' => []], ]); $policyA = Policy::factory()->create([ 'tenant_id' => $tenant->id, 'ignored_at' => null, ]); $policyB = Policy::factory()->create([ 'tenant_id' => $tenant->id, 'ignored_at' => null, ]); $versionA = PolicyVersion::factory()->create([ 'tenant_id' => $tenant->id, 'policy_id' => $policyA->id, 'policy_type' => $policyA->policy_type, 'platform' => $policyA->platform, 'snapshot' => ['id' => $policyA->external_id], ]); $run = OperationRun::factory()->create([ 'tenant_id' => $tenant->id, 'user_id' => $user->id, 'initiator_name' => $user->name, 'type' => 'backup_set.add_policies', 'status' => 'queued', 'outcome' => 'pending', 'context' => [ 'backup_set_id' => (int) $backupSet->getKey(), 'policy_ids' => [(int) $policyA->getKey(), (int) $policyB->getKey()], ], 'summary_counts' => [], 'failure_summary' => [], ]); $this->mock(PolicyCaptureOrchestrator::class, function (MockInterface $mock) use ($policyA, $policyB, $tenant, $versionA) { $mock->shouldReceive('capture') ->twice() ->andReturnUsing(function ( Policy $policy, \App\Models\Tenant $tenantArg, bool $includeAssignments = false, bool $includeScopeTags = false, ?string $createdBy = null, array $metadata = [] ) use ($policyA, $policyB, $tenant, $versionA) { expect($tenantArg->id)->toBe($tenant->id); expect($includeAssignments)->toBeTrue(); expect($includeScopeTags)->toBeTrue(); expect($metadata['backup_set_id'] ?? null)->not->toBeNull(); if ($policy->is($policyA)) { return [ 'version' => $versionA, 'captured' => [ 'payload' => [ 'id' => $policyA->external_id, '@odata.type' => '#microsoft.graph.deviceManagementConfigurationPolicy', ], 'assignments' => [], 'scope_tags' => ['ids' => ['0'], 'names' => ['Default']], 'metadata' => [], ], ]; } expect($policy->is($policyB))->toBeTrue(); return [ 'failure' => [ 'policy_id' => $policyB->id, 'reason' => 'Forbidden', 'status' => 403, ], ]; }); }); $job = new AddPoliciesToBackupSetJob( tenantId: (int) $tenant->getKey(), userId: (int) $user->getKey(), backupSetId: (int) $backupSet->getKey(), policyIds: [(int) $policyA->getKey(), (int) $policyB->getKey()], options: [ 'include_assignments' => true, 'include_scope_tags' => true, 'include_foundations' => false, ], idempotencyKey: 'test-idempotency-key', operationRun: $run, ); $job->handle( operationRunService: app(OperationRunService::class), captureOrchestrator: app(PolicyCaptureOrchestrator::class), foundationSnapshots: $this->mock(FoundationSnapshotService::class), snapshotValidator: app(SnapshotValidator::class), versionService: app(VersionService::class), ); $run->refresh(); $backupSet->refresh(); expect($run->status)->toBe('completed'); expect($run->outcome)->toBe('partially_succeeded'); expect((int) ($run->summary_counts['total'] ?? 0))->toBe(2); expect((int) ($run->summary_counts['processed'] ?? 0))->toBe(2); expect((int) ($run->summary_counts['succeeded'] ?? 0))->toBe(1); expect((int) ($run->summary_counts['failed'] ?? 0))->toBe(1); expect((int) ($run->summary_counts['skipped'] ?? 0))->toBe(0); expect(BackupItem::query() ->where('backup_set_id', $backupSet->id) ->where('policy_id', $policyA->id) ->exists())->toBeTrue(); $failureEntry = collect($run->failure_summary ?? []) ->first(fn ($entry): bool => is_array($entry) && (($entry['code'] ?? null) === 'graph.graph_forbidden')); expect($failureEntry)->not->toBeNull(); expect($backupSet->status)->toBe('partial'); }); it('captures RBAC foundation items with linked policy versions when include_foundations is enabled', function () { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); config()->set('tenantpilot.foundation_types', [ [ 'type' => 'intuneRoleDefinition', 'label' => 'Intune Role Definition', 'category' => 'RBAC', 'platform' => 'all', 'endpoint' => 'deviceManagement/roleDefinitions', 'backup' => 'full', 'restore' => 'preview-only', 'risk' => 'high', ], [ 'type' => 'intuneRoleAssignment', 'label' => 'Intune Role Assignment', 'category' => 'RBAC', 'platform' => 'all', 'endpoint' => 'deviceManagement/roleAssignments', 'backup' => 'full', 'restore' => 'preview-only', 'risk' => 'high', ], ]); $backupSet = BackupSet::factory()->create([ 'tenant_id' => $tenant->id, 'name' => 'RBAC foundations', 'status' => 'completed', 'metadata' => ['failures' => []], ]); $policy = Policy::factory()->create([ 'tenant_id' => $tenant->id, 'policy_type' => 'deviceConfiguration', 'platform' => 'windows', 'ignored_at' => null, ]); $version = PolicyVersion::factory()->create([ 'tenant_id' => $tenant->id, 'policy_id' => $policy->id, 'policy_type' => $policy->policy_type, 'platform' => $policy->platform, 'snapshot' => ['id' => $policy->external_id], ]); $run = OperationRun::factory()->create([ 'tenant_id' => $tenant->id, 'user_id' => $user->id, 'initiator_name' => $user->name, 'type' => 'backup_set.add_policies', 'status' => 'queued', 'outcome' => 'pending', 'context' => [ 'backup_set_id' => (int) $backupSet->getKey(), 'policy_ids' => [(int) $policy->getKey()], ], 'summary_counts' => [], 'failure_summary' => [], ]); $this->mock(PolicyCaptureOrchestrator::class, function (MockInterface $mock) use ($policy, $tenant, $version) { $mock->shouldReceive('capture') ->once() ->andReturnUsing(function ( Policy $capturedPolicy, \App\Models\Tenant $tenantArg, bool $includeAssignments = false, bool $includeScopeTags = false, ?string $createdBy = null, array $metadata = [] ) use ($policy, $tenant, $version) { expect($capturedPolicy->is($policy))->toBeTrue(); expect($tenantArg->is($tenant))->toBeTrue(); expect($metadata['backup_set_id'] ?? null)->toBe((int) $metadata['backup_set_id']); return [ 'version' => $version, 'captured' => [ 'payload' => [ 'id' => $policy->external_id, '@odata.type' => '#microsoft.graph.deviceManagementConfigurationPolicy', ], 'assignments' => [], 'scope_tags' => null, 'metadata' => [], ], ]; }); }); $this->mock(FoundationSnapshotService::class, function (MockInterface $mock) { $mock->shouldReceive('fetchAll') ->twice() ->andReturnUsing(function (\App\Models\Tenant $tenant, string $foundationType): array { return match ($foundationType) { 'intuneRoleDefinition' => [ 'items' => [[ 'source_id' => 'role-def-1', 'display_name' => 'Policy and Profile Manager', 'payload' => [ 'id' => 'role-def-1', 'displayName' => 'Policy and Profile Manager', 'description' => 'Built-in RBAC role', 'isBuiltIn' => true, ], 'metadata' => [ 'displayName' => 'Policy and Profile Manager', 'kind' => 'intuneRoleDefinition', ], ]], 'failures' => [], ], 'intuneRoleAssignment' => [ 'items' => [[ 'source_id' => 'role-assign-1', 'display_name' => 'Helpdesk Assignment', 'payload' => [ 'id' => 'role-assign-1', 'displayName' => 'Helpdesk Assignment', 'members' => ['group-1'], 'resourceScopes' => ['/'], 'roleDefinition' => [ 'id' => 'role-def-1', 'displayName' => 'Policy and Profile Manager', ], ], 'metadata' => [ 'displayName' => 'Helpdesk Assignment', 'kind' => 'intuneRoleAssignment', ], ]], 'failures' => [], ], default => [ 'items' => [], 'failures' => [], ], }; }); }); $job = new AddPoliciesToBackupSetJob( tenantId: (int) $tenant->getKey(), userId: (int) $user->getKey(), backupSetId: (int) $backupSet->getKey(), policyIds: [(int) $policy->getKey()], options: [ 'include_assignments' => false, 'include_scope_tags' => false, 'include_foundations' => true, ], idempotencyKey: 'rbac-foundation-additions', operationRun: $run, ); $job->handle( operationRunService: app(OperationRunService::class), captureOrchestrator: app(PolicyCaptureOrchestrator::class), foundationSnapshots: app(FoundationSnapshotService::class), snapshotValidator: app(SnapshotValidator::class), versionService: app(VersionService::class), ); $run->refresh(); $backupSet->refresh(); $definitionItem = BackupItem::query() ->where('backup_set_id', $backupSet->id) ->where('policy_type', 'intuneRoleDefinition') ->first(); $assignmentItem = BackupItem::query() ->where('backup_set_id', $backupSet->id) ->where('policy_type', 'intuneRoleAssignment') ->first(); expect($run->outcome)->toBe('succeeded'); expect($run->summary_counts)->toMatchArray([ 'total' => 3, 'processed' => 3, 'succeeded' => 3, 'created' => 3, 'items' => 3, ]); expect($backupSet->status)->toBe('completed'); expect($backupSet->item_count)->toBe(3); expect($definitionItem)->not->toBeNull(); expect($definitionItem?->policy_id)->not->toBeNull(); expect($definitionItem?->policy_version_id)->not->toBeNull(); expect($definitionItem?->resolvedDisplayName())->toBe('Policy and Profile Manager'); expect($assignmentItem)->not->toBeNull(); expect($assignmentItem?->policy_id)->not->toBeNull(); expect($assignmentItem?->policy_version_id)->not->toBeNull(); expect($assignmentItem?->resolvedDisplayName())->toBe('Helpdesk Assignment'); });