TenantAtlas/tests/Feature/BackupServiceVersionReuseTest.php
ahmido c6e7591d19 feat: add Intune RBAC inventory and backup support (#155)
## Summary
- add Intune RBAC role definitions and role assignments as foundation-backed inventory, backup, and versioned snapshot types
- add RBAC-specific normalization, coverage, permission-warning handling, and preview-only restore safety behavior across existing Filament and service surfaces
- add spec 127 artifacts, contracts, audits, and focused regression coverage for inventory, backup, versioning, verification, and authorization behavior

## Testing
- `vendor/bin/sail bin pint --dirty --format agent`
- `vendor/bin/sail artisan test --compact tests/Feature/Inventory/InventorySyncServiceTest.php tests/Feature/Filament/InventoryCoverageTableTest.php tests/Feature/FoundationBackupTest.php tests/Feature/Filament/RestoreExecutionTest.php tests/Feature/RestoreUnknownPolicyTypeSafetyTest.php tests/Unit/GraphContractRegistryTest.php tests/Unit/FoundationSnapshotServiceTest.php tests/Feature/Verification/IntuneRbacPermissionCoverageTest.php tests/Unit/IntuneRoleDefinitionNormalizerTest.php tests/Unit/IntuneRoleAssignmentNormalizerTest.php`

## Notes
- tasks in `specs/127-rbac-inventory-backup/tasks.md` are complete except `T041`, which is the documented manual QA validation step

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #155
2026-03-09 10:40:51 +00:00

229 lines
7.5 KiB
PHP

<?php
use App\Models\BackupSet;
use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\Tenant;
use App\Models\User;
use App\Services\Intune\BackupService;
use App\Services\Intune\FoundationSnapshotService;
use App\Services\Intune\PolicyCaptureOrchestrator;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery\MockInterface;
uses(RefreshDatabase::class);
it('reuses latest policy version for backup when it is up-to-date and satisfies capture options', 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,
]);
$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);
});