265 lines
9.5 KiB
PHP
265 lines
9.5 KiB
PHP
<?php
|
|
|
|
use App\Models\BackupItem;
|
|
use App\Models\Policy;
|
|
use App\Models\PolicyVersion;
|
|
use App\Models\Tenant;
|
|
use App\Services\Graph\AssignmentFetcher;
|
|
use App\Services\Graph\GroupResolver;
|
|
use App\Services\Intune\BackupService;
|
|
use App\Services\Intune\PolicySnapshotService;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Mockery\MockInterface;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
$this->tenant = Tenant::factory()->create(['status' => 'active']);
|
|
|
|
$this->policy = Policy::factory()->create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'external_id' => 'test-policy-123',
|
|
'policy_type' => 'settingsCatalogPolicy',
|
|
'platform' => 'windows10',
|
|
'display_name' => 'Test Policy',
|
|
]);
|
|
|
|
$this->snapshotPayload = [
|
|
'@odata.type' => '#microsoft.graph.deviceManagementConfigurationPolicy',
|
|
'id' => 'test-policy-123',
|
|
'name' => 'Test Policy',
|
|
'description' => 'Test Description',
|
|
'platforms' => 'windows10',
|
|
'technologies' => 'mdm',
|
|
'settings' => [],
|
|
];
|
|
|
|
$this->assignmentsPayload = [
|
|
[
|
|
'id' => 'assignment-1',
|
|
'target' => [
|
|
'@odata.type' => '#microsoft.graph.groupAssignmentTarget',
|
|
'groupId' => 'group-123',
|
|
],
|
|
],
|
|
[
|
|
'id' => 'assignment-2',
|
|
'target' => [
|
|
'@odata.type' => '#microsoft.graph.allDevicesAssignmentTarget',
|
|
],
|
|
],
|
|
];
|
|
|
|
$this->resolvedAssignments = [
|
|
[
|
|
'id' => 'assignment-1',
|
|
'target' => [
|
|
'@odata.type' => '#microsoft.graph.groupAssignmentTarget',
|
|
'groupId' => 'group-123',
|
|
'group_name' => 'Test Group',
|
|
],
|
|
],
|
|
[
|
|
'id' => 'assignment-2',
|
|
'target' => [
|
|
'@odata.type' => '#microsoft.graph.allDevicesAssignmentTarget',
|
|
],
|
|
],
|
|
];
|
|
|
|
// Mock PolicySnapshotService
|
|
$this->mock(PolicySnapshotService::class, function (MockInterface $mock) {
|
|
$mock->shouldReceive('fetch')
|
|
->andReturn([
|
|
'payload' => $this->snapshotPayload,
|
|
'metadata' => ['fetched_at' => now()->toISOString()],
|
|
'warnings' => [],
|
|
]);
|
|
});
|
|
|
|
// Mock AssignmentFetcher
|
|
$this->mock(AssignmentFetcher::class, function (MockInterface $mock) {
|
|
$mock->shouldReceive('fetch')
|
|
->andReturn($this->assignmentsPayload);
|
|
});
|
|
|
|
// Mock GroupResolver
|
|
$this->mock(GroupResolver::class, function (MockInterface $mock) {
|
|
$mock->shouldReceive('resolveGroupIds')
|
|
->andReturn([
|
|
'group-123' => [
|
|
'id' => 'group-123',
|
|
'displayName' => 'Test Group',
|
|
'orphaned' => false,
|
|
],
|
|
]);
|
|
});
|
|
});
|
|
|
|
it('creates backup with includeAssignments=true and both BackupItem and PolicyVersion have assignments', function () {
|
|
$backupService = app(BackupService::class);
|
|
|
|
$backupSet = $backupService->createBackupSet(
|
|
tenant: $this->tenant,
|
|
policyIds: [$this->policy->id],
|
|
actorEmail: 'test@example.com',
|
|
actorName: 'Test User',
|
|
name: 'Test Backup With Assignments',
|
|
includeAssignments: true,
|
|
);
|
|
|
|
expect($backupSet)->not->toBeNull();
|
|
expect($backupSet->items)->toHaveCount(1);
|
|
|
|
$backupItem = $backupSet->items->first();
|
|
expect($backupItem->assignments)->not->toBeNull();
|
|
expect($backupItem->assignments)->toBeArray();
|
|
expect($backupItem->assignments)->toHaveCount(2);
|
|
expect($backupItem->assignments[0]['target']['groupId'])->toBe('group-123');
|
|
|
|
// CRITICAL: PolicyVersion must also have assignments (domain consistency)
|
|
expect($backupItem->policy_version_id)->not->toBeNull();
|
|
$version = PolicyVersion::find($backupItem->policy_version_id);
|
|
expect($version)->not->toBeNull();
|
|
expect($version->assignments)->not->toBeNull();
|
|
expect($version->assignments)->toBeArray();
|
|
expect($version->assignments)->toHaveCount(2);
|
|
expect($version->assignments[0]['target']['groupId'])->toBe('group-123');
|
|
|
|
// Verify assignments match between BackupItem and PolicyVersion
|
|
expect($backupItem->assignments)->toEqual($version->assignments);
|
|
});
|
|
|
|
it('creates backup with includeAssignments=false and both BackupItem and PolicyVersion have no assignments', function () {
|
|
$backupService = app(BackupService::class);
|
|
|
|
$backupSet = $backupService->createBackupSet(
|
|
tenant: $this->tenant,
|
|
policyIds: [$this->policy->id],
|
|
actorEmail: 'test@example.com',
|
|
actorName: 'Test User',
|
|
name: 'Test Backup Without Assignments',
|
|
includeAssignments: false,
|
|
);
|
|
|
|
expect($backupSet)->not->toBeNull();
|
|
expect($backupSet->items)->toHaveCount(1);
|
|
|
|
$backupItem = $backupSet->items->first();
|
|
expect($backupItem->assignments)->toBeNull();
|
|
|
|
// CRITICAL: PolicyVersion must also have no assignments (domain consistency)
|
|
expect($backupItem->policy_version_id)->not->toBeNull();
|
|
$version = PolicyVersion::find($backupItem->policy_version_id);
|
|
expect($version)->not->toBeNull();
|
|
expect($version->assignments)->toBeNull();
|
|
});
|
|
|
|
it('backfills existing PolicyVersion without assignments when creating backup with includeAssignments=true', function () {
|
|
// Create an existing PolicyVersion without assignments (simulate old backup)
|
|
$existingVersion = PolicyVersion::create([
|
|
'policy_id' => $this->policy->id,
|
|
'tenant_id' => $this->tenant->id,
|
|
'version_number' => 1,
|
|
'policy_type' => 'settingsCatalogPolicy',
|
|
'platform' => 'windows10',
|
|
'snapshot' => $this->snapshotPayload,
|
|
'assignments' => null, // NO ASSIGNMENTS
|
|
'scope_tags' => null,
|
|
'assignments_hash' => null,
|
|
'scope_tags_hash' => null,
|
|
'created_by' => 'legacy-system@example.com',
|
|
]);
|
|
|
|
expect($existingVersion->assignments)->toBeNull();
|
|
expect($existingVersion->assignments_hash)->toBeNull();
|
|
|
|
$backupService = app(BackupService::class);
|
|
|
|
// Create new backup with includeAssignments=true
|
|
// Orchestrator should detect existing version and backfill it
|
|
$backupSet = $backupService->createBackupSet(
|
|
tenant: $this->tenant,
|
|
policyIds: [$this->policy->id],
|
|
actorEmail: 'test@example.com',
|
|
actorName: 'Test User',
|
|
name: 'Test Backup Backfills Version',
|
|
includeAssignments: true,
|
|
);
|
|
|
|
expect($backupSet)->not->toBeNull();
|
|
expect($backupSet->items)->toHaveCount(1);
|
|
|
|
$backupItem = $backupSet->items->first();
|
|
|
|
// BackupItem should have assignments
|
|
expect($backupItem->assignments)->not->toBeNull();
|
|
expect($backupItem->assignments)->toHaveCount(2);
|
|
|
|
// CRITICAL: Existing PolicyVersion should now be backfilled (idempotent)
|
|
// The orchestrator should have detected same payload_hash and enriched it
|
|
$existingVersion->refresh();
|
|
expect($existingVersion->assignments)->not->toBeNull();
|
|
expect($existingVersion->assignments)->toHaveCount(2);
|
|
expect($existingVersion->assignments_hash)->not->toBeNull();
|
|
expect($existingVersion->assignments[0]['target']['groupId'])->toBe('group-123');
|
|
|
|
// BackupItem should reference the backfilled version
|
|
expect($backupItem->policy_version_id)->toBe($existingVersion->id);
|
|
});
|
|
|
|
it('does not overwrite existing PolicyVersion assignments when they already exist (idempotent)', function () {
|
|
// Create an existing PolicyVersion WITH assignments
|
|
$existingAssignments = [
|
|
[
|
|
'id' => 'old-assignment',
|
|
'target' => ['@odata.type' => '#microsoft.graph.allLicensedUsersAssignmentTarget'],
|
|
],
|
|
];
|
|
|
|
$existingVersion = PolicyVersion::create([
|
|
'policy_id' => $this->policy->id,
|
|
'tenant_id' => $this->tenant->id,
|
|
'version_number' => 1,
|
|
'policy_type' => 'settingsCatalogPolicy',
|
|
'platform' => 'windows10',
|
|
'snapshot' => $this->snapshotPayload,
|
|
'assignments' => $existingAssignments,
|
|
'scope_tags' => null,
|
|
'assignments_hash' => hash('sha256', json_encode($existingAssignments)),
|
|
'scope_tags_hash' => null,
|
|
'created_by' => 'previous-backup@example.com',
|
|
]);
|
|
|
|
$backupService = app(BackupService::class);
|
|
|
|
// Create new backup - orchestrator should NOT overwrite existing assignments
|
|
$backupSet = $backupService->createBackupSet(
|
|
tenant: $this->tenant,
|
|
policyIds: [$this->policy->id],
|
|
actorEmail: 'test@example.com',
|
|
actorName: 'Test User',
|
|
name: 'Test Backup Preserves Existing',
|
|
includeAssignments: true,
|
|
);
|
|
|
|
expect($backupSet)->not->toBeNull();
|
|
expect($backupSet->items)->toHaveCount(1);
|
|
|
|
$backupItem = $backupSet->items->first();
|
|
|
|
// BackupItem should have NEW assignments (from current fetch)
|
|
expect($backupItem->assignments)->not->toBeNull();
|
|
expect($backupItem->assignments)->toHaveCount(2);
|
|
expect($backupItem->assignments[0]['target']['groupId'])->toBe('group-123');
|
|
|
|
// CRITICAL: Existing PolicyVersion should NOT be modified (idempotent)
|
|
$existingVersion->refresh();
|
|
expect($existingVersion->assignments)->toEqual($existingAssignments);
|
|
expect($existingVersion->assignments)->toHaveCount(1);
|
|
expect($existingVersion->assignments[0]['id'])->toBe('old-assignment');
|
|
|
|
// BackupItem should reference the existing version (reused)
|
|
expect($backupItem->policy_version_id)->toBe($existingVersion->id);
|
|
});
|