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), ); $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'); });