create(); $tenant->makeCurrent(); $user = User::factory()->create(); $this->actingAs($user); $backupSet = BackupSet::factory()->create([ 'tenant_id' => $tenant->id, 'status' => 'completed', ]); $policy = Policy::factory()->create([ 'tenant_id' => $tenant->id, 'last_synced_at' => now(), 'ignored_at' => null, ]); $existingVersion = PolicyVersion::factory()->create([ 'tenant_id' => $tenant->id, 'policy_id' => $policy->id, 'captured_at' => now(), 'snapshot' => ['id' => $policy->external_id, 'name' => $policy->display_name], 'assignments' => null, 'scope_tags' => null, ]); $this->mock(PolicyCaptureOrchestrator::class, function (MockInterface $mock) use ($existingVersion) { $mock->shouldReceive('capture') ->once() ->andReturn([ 'version' => $existingVersion, 'captured' => [ 'payload' => $existingVersion->snapshot, 'assignments' => $existingVersion->assignments, 'scope_tags' => $existingVersion->scope_tags, 'metadata' => [], ], ]); }); $service = app(BackupService::class); $service->addPoliciesToSet( tenant: $tenant, backupSet: $backupSet, policyIds: [$policy->id], actorEmail: $user->email, actorName: $user->name, includeAssignments: false, includeScopeTags: false, includeFoundations: false, ); expect(PolicyVersion::query()->where('policy_id', $policy->id)->count())->toBe(1); $item = $backupSet->items()->first(); expect($item)->not->toBeNull(); expect($item->policy_version_id)->toBe($existingVersion->id); }); it('captures a new policy version for backup when no suitable existing version is available', function () { $tenant = Tenant::factory()->create(); $tenant->makeCurrent(); $user = User::factory()->create(); $this->actingAs($user); $backupSet = BackupSet::factory()->create([ 'tenant_id' => $tenant->id, 'status' => 'completed', ]); $policy = Policy::factory()->create([ 'tenant_id' => $tenant->id, 'last_synced_at' => now(), 'ignored_at' => null, ]); $staleVersion = PolicyVersion::factory()->create([ 'tenant_id' => $tenant->id, 'policy_id' => $policy->id, 'version_number' => 1, 'captured_at' => now()->subDays(2), 'snapshot' => ['id' => $policy->external_id, 'name' => $policy->display_name], ]); $policy->update(['last_synced_at' => now()]); $this->mock(PolicyCaptureOrchestrator::class, function (MockInterface $mock) use ($policy, $tenant) { $mock->shouldReceive('capture') ->once() ->andReturnUsing(function () use ($policy, $tenant) { $newVersion = PolicyVersion::factory()->create([ 'tenant_id' => $tenant->id, 'policy_id' => $policy->id, 'version_number' => 2, 'captured_at' => now(), 'snapshot' => ['id' => $policy->external_id, 'name' => $policy->display_name, 'changed' => true], ]); return [ 'version' => $newVersion, 'captured' => [ 'payload' => $newVersion->snapshot, 'assignments' => null, 'scope_tags' => null, 'metadata' => [], ], ]; }); }); $service = app(BackupService::class); $service->addPoliciesToSet( tenant: $tenant, backupSet: $backupSet, policyIds: [$policy->id], actorEmail: $user->email, actorName: $user->name, includeAssignments: false, includeScopeTags: false, includeFoundations: false, ); $item = $backupSet->items()->first(); expect($item)->not->toBeNull(); expect($item->policy_version_id)->not->toBe($staleVersion->id); }); it('reuses an existing RBAC foundation version across backup sets when the snapshot is unchanged', function () { $tenant = Tenant::factory()->create(['status' => 'active']); ensureDefaultProviderConnection($tenant); 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', ], ]); $payload = [ 'id' => 'role-def-1', 'displayName' => 'Policy and Profile Manager', 'description' => 'Built-in RBAC role', 'isBuiltIn' => true, 'rolePermissions' => [ [ 'resourceActions' => [ [ 'allowedResourceActions' => [ 'Microsoft.Intune/deviceConfigurations/read', ], ], ], ], ], ]; $this->mock(FoundationSnapshotService::class, function (MockInterface $mock) use ($payload) { $mock->shouldReceive('fetchAll') ->twice() ->withArgs(fn (Tenant $tenant, string $foundationType): bool => $foundationType === 'intuneRoleDefinition') ->andReturn([ 'items' => [[ 'source_id' => 'role-def-1', 'display_name' => 'Policy and Profile Manager', 'payload' => $payload, 'metadata' => [ 'displayName' => 'Policy and Profile Manager', 'kind' => 'intuneRoleDefinition', 'graph' => [ 'resource' => 'deviceManagement/roleDefinitions', 'apiVersion' => 'beta', ], ], ]], 'failures' => [], ]); }); $service = app(BackupService::class); $firstBackupSet = $service->createBackupSet( tenant: $tenant, policyIds: [], name: 'RBAC Backup 1', includeFoundations: true, ); $secondBackupSet = $service->createBackupSet( tenant: $tenant, policyIds: [], name: 'RBAC Backup 2', includeFoundations: true, ); $firstItem = $firstBackupSet->items()->first(); $secondItem = $secondBackupSet->items()->first(); expect($firstItem)->not->toBeNull(); expect($secondItem)->not->toBeNull(); expect($firstItem->policy_id)->toBe($secondItem->policy_id); expect($firstItem->policy_version_id)->toBe($secondItem->policy_version_id); expect(PolicyVersion::query()->where('tenant_id', $tenant->id)->where('policy_type', 'intuneRoleDefinition')->count())->toBe(1); });