TenantAtlas/tests/Feature/BackupSets/AddPoliciesToBackupSetJobTest.php
2026-03-09 11:39:36 +01:00

350 lines
13 KiB
PHP

<?php
use App\Jobs\AddPoliciesToBackupSetJob;
use App\Models\BackupItem;
use App\Models\BackupSet;
use App\Models\OperationRun;
use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Services\Intune\FoundationSnapshotService;
use App\Services\Intune\PolicyCaptureOrchestrator;
use App\Services\Intune\SnapshotValidator;
use App\Services\Intune\VersionService;
use App\Services\OperationRunService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery\MockInterface;
uses(RefreshDatabase::class);
it('records stable failure reason codes and keeps run counts consistent', function () {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$this->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');
});