set('graph_contracts.types.deviceManagementScript', [ 'assignments_create_path' => '/deviceManagement/deviceManagementScripts/{id}/assign', 'assignments_create_method' => 'POST', 'assignments_payload_key' => 'deviceManagementScriptAssignments', ]); config()->set('graph_contracts.types.settingsCatalogPolicy', [ 'assignments_create_path' => '/deviceManagement/configurationPolicies/{id}/assign', 'assignments_create_method' => 'POST', ]); $this->graphClient = Mockery::mock(GraphClientInterface::class); $this->auditLogger = Mockery::mock(AuditLogger::class); $this->filterResolver = Mockery::mock(AssignmentFilterResolver::class); $this->filterResolver->shouldReceive('resolve')->andReturn([])->byDefault(); $this->service = new AssignmentRestoreService( $this->graphClient, app(GraphContractRegistry::class), app(GraphLogger::class), $this->auditLogger, $this->filterResolver, ); }); it('uses the contract assignment payload key for assign actions', function () { $tenant = Tenant::factory()->make([ 'tenant_id' => 'tenant-123', 'app_client_id' => null, 'app_client_secret' => null, ]); $policyId = 'policy-123'; $assignments = [ [ 'id' => 'assignment-1', 'target' => [ '@odata.type' => '#microsoft.graph.groupAssignmentTarget', 'groupId' => 'group-1', ], ], ]; $expectedAssignments = [ [ 'target' => [ '@odata.type' => '#microsoft.graph.groupAssignmentTarget', 'groupId' => 'group-1', ], ], ]; $this->graphClient ->shouldReceive('request') ->once() ->with('POST', "/deviceManagement/deviceManagementScripts/{$policyId}/assign", Mockery::on( fn (array $options) => ($options['json']['deviceManagementScriptAssignments'] ?? null) === $expectedAssignments )) ->andReturn(new GraphResponse(success: true, data: [])); $this->auditLogger ->shouldReceive('log') ->once() ->andReturn(new AuditLog); $result = $this->service->restore( $tenant, 'deviceManagementScript', $policyId, $assignments, [] ); expect($result['summary']['success'])->toBe(1); expect($result['summary']['failed'])->toBe(0); expect($result['summary']['skipped'])->toBe(0); }); it('maps assignment filter ids stored at the root of assignments', function () { $tenant = Tenant::factory()->make([ 'tenant_id' => 'tenant-123', 'app_client_id' => null, 'app_client_secret' => null, ]); $policyId = 'policy-789'; $assignments = [ [ 'id' => 'assignment-1', 'deviceAndAppManagementAssignmentFilterId' => 'filter-source', 'deviceAndAppManagementAssignmentFilterType' => 'include', 'target' => [ '@odata.type' => '#microsoft.graph.groupAssignmentTarget', 'groupId' => 'group-1', ], ], ]; $expectedAssignments = [ [ 'deviceAndAppManagementAssignmentFilterId' => 'filter-target', 'deviceAndAppManagementAssignmentFilterType' => 'include', 'target' => [ '@odata.type' => '#microsoft.graph.groupAssignmentTarget', 'groupId' => 'group-1', ], ], ]; $this->graphClient ->shouldReceive('request') ->once() ->with('POST', "/deviceManagement/configurationPolicies/{$policyId}/assign", Mockery::on( fn (array $options) => ($options['json']['assignments'] ?? null) === $expectedAssignments )) ->andReturn(new GraphResponse(success: true, data: [])); $this->auditLogger ->shouldReceive('log') ->once() ->andReturn(new AuditLog); $result = $this->service->restore( $tenant, 'settingsCatalogPolicy', $policyId, $assignments, [], [ 'assignmentFilter' => [ 'filter-source' => 'filter-target', ], ] ); expect($result['summary']['success'])->toBe(1); expect($result['summary']['failed'])->toBe(0); expect($result['summary']['skipped'])->toBe(0); }); it('keeps assignment filters when mapping is missing but filter exists in target', function () { $tenant = Tenant::factory()->make([ 'tenant_id' => 'tenant-123', 'app_client_id' => null, 'app_client_secret' => null, ]); $policyId = 'policy-999'; $assignments = [ [ 'id' => 'assignment-1', 'deviceAndAppManagementAssignmentFilterId' => 'filter-1', 'deviceAndAppManagementAssignmentFilterType' => 'include', 'target' => [ '@odata.type' => '#microsoft.graph.groupAssignmentTarget', 'groupId' => 'group-1', ], ], ]; $this->filterResolver ->shouldReceive('resolve') ->once() ->with(['filter-1'], $tenant) ->andReturn([['id' => 'filter-1', 'displayName' => 'Test']]); $this->graphClient ->shouldReceive('request') ->once() ->with('POST', "/deviceManagement/configurationPolicies/{$policyId}/assign", Mockery::on( fn (array $options) => ($options['json']['assignments'][0]['deviceAndAppManagementAssignmentFilterId'] ?? null) === 'filter-1' )) ->andReturn(new GraphResponse(success: true, data: [])); $this->auditLogger ->shouldReceive('log') ->once() ->andReturn(new AuditLog); $result = $this->service->restore( $tenant, 'settingsCatalogPolicy', $policyId, $assignments, [], [] ); expect($result['summary']['success'])->toBe(1); expect($result['summary']['failed'])->toBe(0); expect($result['summary']['skipped'])->toBe(0); });