tenant = Tenant::factory()->create([ 'tenant_id' => 'tenant-123', 'status' => 'active', ]); $this->user = User::factory()->create(); $this->policy = Policy::factory()->create([ 'tenant_id' => $this->tenant->id, 'external_id' => 'policy-456', 'policy_type' => 'settingsCatalogPolicy', 'platform' => 'windows10', ]); $this->tenant->makeCurrent(); }); test('creates backup with assignments when checkbox enabled', function () { // Mock PolicySnapshotService to return fake payload $this->mock(PolicySnapshotService::class, function (MockInterface $mock) { $mock->shouldReceive('fetch') ->once() ->andReturn([ 'payload' => [ 'id' => 'policy-456', 'name' => 'Test Policy', 'roleScopeTagIds' => ['0', '123'], 'settings' => [], ], 'metadata' => [], 'warnings' => [], ]); }); // Mock AssignmentFetcher $this->mock(AssignmentFetcher::class, function (MockInterface $mock) { $mock->shouldReceive('fetch') ->once() ->with('tenant-123', 'policy-456') ->andReturn([ [ 'id' => 'assignment-1', 'target' => [ '@odata.type' => '#microsoft.graph.groupAssignmentTarget', 'groupId' => 'group-abc', ], 'intent' => 'apply', ], [ 'id' => 'assignment-2', 'target' => [ '@odata.type' => '#microsoft.graph.groupAssignmentTarget', 'groupId' => 'group-def', ], 'intent' => 'apply', ], ]); }); // Mock GroupResolver $this->mock(GroupResolver::class, function (MockInterface $mock) { $mock->shouldReceive('resolveGroupIds') ->once() ->with(['group-abc', 'group-def'], 'tenant-123') ->andReturn([ 'group-abc' => [ 'id' => 'group-abc', 'displayName' => 'All Users', 'orphaned' => false, ], 'group-def' => [ 'id' => 'group-def', 'displayName' => 'IT Department', 'orphaned' => false, ], ]); }); // Mock ScopeTagResolver $this->mock(ScopeTagResolver::class, function (MockInterface $mock) { $mock->shouldReceive('resolve') ->once() ->with(['0', '123']) ->andReturn([ '0' => 'Default', '123' => 'HR-Admins', ]); }); /** @var BackupService $backupService */ $backupService = app(BackupService::class); $backupSet = $backupService->createBackupSet( tenant: $this->tenant, policyIds: [$this->policy->id], actorEmail: $this->user->email, actorName: $this->user->name, name: 'Test Backup with Assignments', includeAssignments: true ); expect($backupSet)->toBeInstanceOf(BackupSet::class) ->and($backupSet->status)->toBe('completed') ->and($backupSet->item_count)->toBe(1); $backupItem = $backupSet->items()->first(); expect($backupItem)->toBeInstanceOf(BackupItem::class) ->and($backupItem->assignments)->toBeArray() ->and($backupItem->assignments)->toHaveCount(2) ->and($backupItem->metadata['assignment_count'])->toBe(2) ->and($backupItem->metadata['scope_tag_ids'])->toBe(['0', '123']) ->and($backupItem->metadata['scope_tag_names'])->toBe(['Default', 'HR-Admins']) ->and($backupItem->metadata['has_orphaned_assignments'])->toBeFalse() ->and($backupItem->metadata['assignments_fetch_failed'] ?? false)->toBeFalse(); // Verify audit log $this->assertDatabaseHas('audit_logs', [ 'tenant_id' => $this->tenant->id, 'action' => 'backup.created', 'resource_type' => 'backup_set', 'resource_id' => (string) $backupSet->id, 'status' => 'success', ]); }); test('creates backup without assignments when checkbox disabled', function () { // Mock PolicySnapshotService $this->mock(PolicySnapshotService::class, function (MockInterface $mock) { $mock->shouldReceive('fetch') ->once() ->andReturn([ 'payload' => [ 'id' => 'policy-456', 'name' => 'Test Policy', 'roleScopeTagIds' => ['0', '123'], 'settings' => [], ], 'metadata' => [], 'warnings' => [], ]); }); // AssignmentFetcher should NOT be called $this->mock(AssignmentFetcher::class, function (MockInterface $mock) { $mock->shouldReceive('fetch')->never(); }); // GroupResolver should NOT be called for assignments $this->mock(GroupResolver::class, function (MockInterface $mock) { $mock->shouldReceive('resolveGroupIds')->never(); }); // ScopeTagResolver should still be called for scope tags $this->mock(ScopeTagResolver::class, function (MockInterface $mock) { $mock->shouldReceive('resolve') ->once() ->with(['0', '123']) ->andReturn([ ['id' => '0', 'displayName' => 'Default'], ['id' => '123', 'displayName' => 'HR-Admins'], ]); }); /** @var BackupService $backupService */ $backupService = app(BackupService::class); $backupSet = $backupService->createBackupSet( tenant: $this->tenant, policyIds: [$this->policy->id], actorEmail: $this->user->email, actorName: $this->user->name, name: 'Test Backup without Assignments', includeAssignments: false ); expect($backupSet)->toBeInstanceOf(BackupSet::class) ->and($backupSet->status)->toBe('completed') ->and($backupSet->item_count)->toBe(1); $backupItem = $backupSet->items()->first(); expect($backupItem)->toBeInstanceOf(BackupItem::class) ->and($backupItem->assignments)->toBeNull() ->and($backupItem->metadata['assignment_count'] ?? 0)->toBe(0) ->and($backupItem->metadata['scope_tag_ids'])->toBe(['0', '123']) ->and($backupItem->metadata['scope_tag_names'])->toBe(['Default', 'HR-Admins']); }); test('handles fetch failure gracefully', function () { // Mock PolicySnapshotService $this->mock(PolicySnapshotService::class, function (MockInterface $mock) { $mock->shouldReceive('fetch') ->once() ->andReturn([ 'payload' => [ 'id' => 'policy-456', 'name' => 'Test Policy', 'roleScopeTagIds' => ['0', '123'], 'settings' => [], ], 'metadata' => [], 'warnings' => [], ]); }); // Mock AssignmentFetcher to throw exception $this->mock(AssignmentFetcher::class, function (MockInterface $mock) { $mock->shouldReceive('fetch') ->once() ->with('tenant-123', 'policy-456') ->andReturn([]); // Returns empty array on failure (fail-soft) }); // Mock GroupResolver (won't be called if assignments empty) $this->mock(GroupResolver::class, function (MockInterface $mock) { $mock->shouldReceive('resolveGroupIds')->never(); }); // Mock ScopeTagResolver $this->mock(ScopeTagResolver::class, function (MockInterface $mock) { $mock->shouldReceive('resolve') ->once() ->with(['0', '123']) ->andReturn([ ['id' => '0', 'displayName' => 'Default'], ['id' => '123', 'displayName' => 'HR-Admins'], ]); }); /** @var BackupService $backupService */ $backupService = app(BackupService::class); $backupSet = $backupService->createBackupSet( tenant: $this->tenant, policyIds: [$this->policy->id], actorEmail: $this->user->email, actorName: $this->user->name, name: 'Test Backup with Fetch Failure', includeAssignments: true ); // Backup should still complete (fail-soft) expect($backupSet)->toBeInstanceOf(BackupSet::class) ->and($backupSet->status)->toBe('completed') ->and($backupSet->item_count)->toBe(1); $backupItem = $backupSet->items()->first(); expect($backupItem)->toBeInstanceOf(BackupItem::class) ->and($backupItem->assignments)->toBeArray() ->and($backupItem->assignments)->toBeEmpty() ->and($backupItem->metadata['assignment_count'])->toBe(0); }); test('detects orphaned groups', function () { // Mock AssignmentFetcher $this->mock(AssignmentFetcher::class, function (MockInterface $mock) { $mock->shouldReceive('fetch') ->once() ->with('tenant-123', 'policy-456') ->andReturn([ [ 'id' => 'assignment-1', 'target' => [ '@odata.type' => '#microsoft.graph.groupAssignmentTarget', 'groupId' => 'group-abc', ], 'intent' => 'apply', ], [ 'id' => 'assignment-2', 'target' => [ '@odata.type' => '#microsoft.graph.groupAssignmentTarget', 'groupId' => 'group-orphaned', ], 'intent' => 'apply', ], ]); }); // Mock GroupResolver with orphaned group $this->mock(GroupResolver::class, function (MockInterface $mock) { $mock->shouldReceive('resolveGroupIds') ->once() ->with(['group-abc', 'group-orphaned'], 'tenant-123') ->andReturn([ 'group-abc' => [ 'id' => 'group-abc', 'displayName' => 'All Users', 'orphaned' => false, ], 'group-orphaned' => [ 'id' => 'group-orphaned', 'displayName' => null, 'orphaned' => true, ], ]); }); // Mock ScopeTagResolver $this->mock(ScopeTagResolver::class, function (MockInterface $mock) { $mock->shouldReceive('resolve') ->once() ->with(['0', '123']) ->andReturn([ ['id' => '0', 'displayName' => 'Default'], ['id' => '123', 'displayName' => 'HR-Admins'], ]); }); /** @var BackupService $backupService */ $backupService = app(BackupService::class); $backupSet = $backupService->createBackupSet( tenant: $this->tenant, policyIds: [$this->policy->id], actorEmail: $this->user->email, actorName: $this->user->name, name: 'Test Backup with Orphaned Groups', includeAssignments: true ); $backupItem = $backupSet->items()->first(); expect($backupItem->metadata['has_orphaned_assignments'])->toBeTrue() ->and($backupItem->metadata['assignment_count'])->toBe(2); });