TenantAtlas/tests/Unit/FoundationMappingServiceTest.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

287 lines
9.4 KiB
PHP

<?php
use App\Models\BackupItem;
use App\Models\BackupSet;
use App\Models\Tenant;
use App\Services\Graph\GraphClientInterface;
use App\Services\Graph\GraphResponse;
use App\Services\Intune\FoundationMappingService;
use App\Services\Intune\FoundationSnapshotService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery\MockInterface;
uses(RefreshDatabase::class);
class FoundationMappingGraphClient implements GraphClientInterface
{
public array $requests = [];
/**
* @param array<int, GraphResponse> $responses
*/
public function __construct(private array $responses = []) {}
public function listPolicies(string $policyType, array $options = []): GraphResponse
{
return new GraphResponse(success: true, data: []);
}
public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse
{
return new GraphResponse(success: true, data: []);
}
public function getOrganization(array $options = []): GraphResponse
{
return new GraphResponse(success: true, data: []);
}
public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse
{
return new GraphResponse(success: true, data: []);
}
public function getServicePrincipalPermissions(array $options = []): GraphResponse
{
return new GraphResponse(success: true, data: []);
}
public function request(string $method, string $path, array $options = []): GraphResponse
{
$this->requests[] = [
'method' => $method,
'path' => $path,
'options' => $options,
];
return array_shift($this->responses) ?? new GraphResponse(success: true, data: []);
}
}
it('maps existing foundations by display name', function () {
$tenant = Tenant::factory()->create();
ensureDefaultProviderConnection($tenant, 'microsoft');
$backupSet = BackupSet::factory()->for($tenant)->create();
$item = BackupItem::factory()
->for($tenant)
->for($backupSet)
->state([
'policy_id' => null,
'policy_identifier' => 'filter-1',
'policy_type' => 'assignmentFilter',
'platform' => 'all',
'payload' => [
'id' => 'filter-1',
'displayName' => 'Filter One',
],
'metadata' => [
'displayName' => 'Filter One',
],
])
->create();
$this->mock(FoundationSnapshotService::class, function (MockInterface $mock) {
$mock->shouldReceive('fetchAll')
->once()
->andReturn([
'items' => [
[
'source_id' => 'filter-2',
'display_name' => 'Filter One',
'payload' => [],
'metadata' => [],
],
],
'failures' => [],
]);
});
$client = new FoundationMappingGraphClient;
app()->instance(GraphClientInterface::class, $client);
$service = app(FoundationMappingService::class);
$result = $service->map($tenant, collect([$item]), false);
expect($result['failed'])->toBe(0);
expect($result['skipped'])->toBe(0);
expect($result['mapping'])->toBe(['filter-1' => 'filter-2']);
expect($result['entries'])->toHaveCount(1);
expect($result['entries'][0]['decision'])->toBe('mapped_existing');
expect($result['entries'][0]['targetId'])->toBe('filter-2');
expect($result['entries'][0]['sourceName'])->toBe('Filter One');
});
it('creates missing foundations when executing', function () {
config()->set('graph_contracts.types.assignmentFilter', [
'resource' => 'deviceManagement/assignmentFilters',
'create_method' => 'POST',
'update_strip_keys' => ['isBuiltIn'],
]);
$tenant = Tenant::factory()->create([
'tenant_id' => 'tenant-1',
'app_client_id' => 'client-1',
'app_client_secret' => 'secret-1',
]);
ensureDefaultProviderConnection($tenant, 'microsoft');
$backupSet = BackupSet::factory()->for($tenant)->create();
$item = BackupItem::factory()
->for($tenant)
->for($backupSet)
->state([
'policy_id' => null,
'policy_identifier' => 'filter-1',
'policy_type' => 'assignmentFilter',
'platform' => 'all',
'payload' => [
'id' => 'filter-1',
'@odata.type' => '#microsoft.graph.deviceAndAppManagementAssignmentFilter',
'displayName' => 'Filter One',
'isBuiltIn' => false,
],
'metadata' => [
'displayName' => 'Filter One',
],
])
->create();
$this->mock(FoundationSnapshotService::class, function (MockInterface $mock) {
$mock->shouldReceive('fetchAll')
->once()
->andReturn([
'items' => [
[
'source_id' => 'filter-2',
'display_name' => 'Filter One',
'payload' => [],
'metadata' => [],
],
[
'source_id' => 'filter-3',
'display_name' => 'Filter One',
'payload' => [],
'metadata' => [],
],
],
'failures' => [],
]);
});
$client = new FoundationMappingGraphClient([
new GraphResponse(true, [
'id' => 'filter-99',
'displayName' => 'Filter One (Copy)',
]),
]);
app()->instance(GraphClientInterface::class, $client);
$service = app(FoundationMappingService::class);
$result = $service->map($tenant, collect([$item]), true);
expect($result['mapping'])->toBe(['filter-1' => 'filter-99']);
expect($result['entries'][0]['decision'])->toBe('created_copy');
expect($result['entries'][0]['targetName'])->toBe('Filter One (Copy)');
expect($client->requests)->toHaveCount(1);
expect($client->requests[0]['method'])->toBe('POST');
expect($client->requests[0]['path'])->toBe('deviceManagement/assignmentFilters');
$payload = $client->requests[0]['options']['json'];
expect($payload['displayName'])->toBe('Filter One (Copy)');
expect($payload)->not->toHaveKey('id');
expect($payload)->not->toHaveKey('@odata.type');
expect($payload)->not->toHaveKey('isBuiltIn');
});
it('skips built-in scope tags', function () {
$tenant = Tenant::factory()->create();
ensureDefaultProviderConnection($tenant, 'microsoft');
$backupSet = BackupSet::factory()->for($tenant)->create();
$item = BackupItem::factory()
->for($tenant)
->for($backupSet)
->state([
'policy_id' => null,
'policy_identifier' => '0',
'policy_type' => 'roleScopeTag',
'platform' => 'all',
'payload' => [
'id' => '0',
'displayName' => 'Default',
'isBuiltIn' => true,
],
'metadata' => [
'displayName' => 'Default',
],
])
->create();
$this->mock(FoundationSnapshotService::class, function (MockInterface $mock) {
$mock->shouldReceive('fetchAll')
->once()
->andReturn([
'items' => [],
'failures' => [],
]);
});
$client = new FoundationMappingGraphClient;
app()->instance(GraphClientInterface::class, $client);
$service = app(FoundationMappingService::class);
$result = $service->map($tenant, collect([$item]), false);
expect($result['skipped'])->toBe(1);
expect($result['entries'][0]['decision'])->toBe('skipped');
expect($result['entries'][0]['reason'])->toBe('Built-in scope tag cannot be created.');
});
it('marks failures when foundation listing fails', function () {
$tenant = Tenant::factory()->create();
$backupSet = BackupSet::factory()->for($tenant)->create();
$item = BackupItem::factory()
->for($tenant)
->for($backupSet)
->state([
'policy_id' => null,
'policy_identifier' => 'filter-1',
'policy_type' => 'assignmentFilter',
'platform' => 'all',
'payload' => [
'id' => 'filter-1',
'displayName' => 'Filter One',
],
'metadata' => [
'displayName' => 'Filter One',
],
])
->create();
$this->mock(FoundationSnapshotService::class, function (MockInterface $mock) {
$mock->shouldReceive('fetchAll')
->once()
->andReturn([
'items' => [],
'failures' => [
[
'foundation_type' => 'assignmentFilter',
'reason' => 'Graph failure',
'status' => 500,
],
],
]);
});
$client = new FoundationMappingGraphClient;
app()->instance(GraphClientInterface::class, $client);
$service = app(FoundationMappingService::class);
$result = $service->map($tenant, collect([$item]), false);
expect($result['failed'])->toBe(1);
expect($result['entries'][0]['decision'])->toBe('failed');
expect($result['entries'][0]['reason'])->toBe('Graph failure');
});