TenantAtlas/tests/Unit/AssignmentRestoreServiceTest.php
ahmido 1acbf8cc54 feat(spec-088): remove tenant graphOptions legacy path (#105)
## Summary
- remove tenant-based Graph options access from runtime service paths and enforce provider-only resolution
- add `MicrosoftGraphOptionsResolver` and `ProviderConfigurationRequiredException` for centralized, actionable provider-config errors
- turn `Tenant::graphOptions()` into a fail-fast kill switch to prevent legacy runtime usage
- add and update tests (including guardrail) to enforce no reintroduction in `app/`
- update Spec 088 artifacts (`spec`, `plan`, `research`, `tasks`, checklist)

## Validation
- `vendor/bin/sail bin pint --dirty`
- `vendor/bin/sail artisan test --compact --filter=NoLegacyTenantGraphOptions`
- `vendor/bin/sail artisan test --compact tests/Feature/Filament`
- `CI=1 vendor/bin/sail artisan test --compact`

## Notes
- Branch includes the guardrail test for legacy callsite detection in `app/`.
- Full suite currently green: 1227 passed, 5 skipped.

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box>
Reviewed-on: #105
2026-02-12 10:14:44 +00:00

266 lines
8.3 KiB
PHP

<?php
use App\Models\AuditLog;
use App\Models\Tenant;
use App\Services\AssignmentRestoreService;
use App\Services\Graph\AssignmentFilterResolver;
use App\Services\Graph\GraphClientInterface;
use App\Services\Graph\GraphContractRegistry;
use App\Services\Graph\GraphLogger;
use App\Services\Graph\GraphResponse;
use App\Services\Intune\AuditLogger;
use App\Services\Providers\MicrosoftGraphOptionsResolver;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
config()->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',
]);
config()->set('graph_contracts.types.appProtectionPolicy', [
'assignments_create_path' => '/deviceAppManagement/managedAppPolicies/{id}/assign',
'assignments_create_method' => 'POST',
'assignments_payload_key' => 'assignments',
]);
$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,
app(MicrosoftGraphOptionsResolver::class),
);
});
it('uses the contract assignment payload key for assign actions', function () {
$tenant = Tenant::factory()->create([
'tenant_id' => 'tenant-123',
'app_client_id' => null,
'app_client_secret' => null,
]);
ensureDefaultProviderConnection($tenant, 'microsoft');
$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('uses derived assign endpoints for app protection policies', function () {
$tenant = Tenant::factory()->create([
'tenant_id' => 'tenant-123',
'app_client_id' => null,
'app_client_secret' => null,
]);
ensureDefaultProviderConnection($tenant, 'microsoft');
$policyId = 'policy-123';
$assignments = [
[
'id' => 'assignment-1',
'target' => [
'@odata.type' => '#microsoft.graph.groupAssignmentTarget',
'groupId' => 'group-1',
],
],
];
$this->graphClient
->shouldReceive('request')
->once()
->with('POST', "/deviceAppManagement/androidManagedAppProtections/{$policyId}/assign", Mockery::on(
fn (array $options) => isset($options['json']['assignments'])
))
->andReturn(new GraphResponse(success: true, data: []));
$this->auditLogger
->shouldReceive('log')
->once()
->andReturn(new AuditLog);
$result = $this->service->restore(
$tenant,
'appProtectionPolicy',
$policyId,
$assignments,
[],
[],
null,
null,
null,
'#microsoft.graph.androidManagedAppProtection',
);
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()->create([
'tenant_id' => 'tenant-123',
'app_client_id' => null,
'app_client_secret' => null,
]);
ensureDefaultProviderConnection($tenant, 'microsoft');
$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()->create([
'tenant_id' => 'tenant-123',
'app_client_id' => null,
'app_client_secret' => null,
]);
ensureDefaultProviderConnection($tenant, 'microsoft');
$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);
});