## 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
124 lines
4.2 KiB
PHP
124 lines
4.2 KiB
PHP
<?php
|
|
|
|
use App\Models\Tenant;
|
|
use App\Services\Graph\GraphClientInterface;
|
|
use App\Services\Graph\GraphResponse;
|
|
use App\Services\Intune\FoundationSnapshotService;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
class FoundationSnapshotGraphClient 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('returns a failure when the foundation contract is missing', function () {
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
ensureDefaultProviderConnection($tenant, 'microsoft');
|
|
|
|
$client = new FoundationSnapshotGraphClient([]);
|
|
app()->instance(GraphClientInterface::class, $client);
|
|
|
|
$service = app(FoundationSnapshotService::class);
|
|
$result = $service->fetchAll($tenant, 'unknownFoundation');
|
|
|
|
expect($result['items'])->toBeEmpty();
|
|
expect($result['failures'])->toHaveCount(1);
|
|
expect($result['failures'][0]['foundation_type'])->toBe('unknownFoundation');
|
|
});
|
|
|
|
it('hydrates foundation snapshots across pages with metadata', function () {
|
|
config()->set('graph_contracts.types.assignmentFilter', [
|
|
'resource' => 'deviceManagement/assignmentFilters',
|
|
'allowed_select' => ['id', 'displayName'],
|
|
]);
|
|
|
|
$tenant = Tenant::factory()->create([
|
|
'tenant_id' => 'tenant-123',
|
|
'app_client_id' => 'client-123',
|
|
'app_client_secret' => 'secret-123',
|
|
]);
|
|
|
|
ensureDefaultProviderConnection($tenant, 'microsoft');
|
|
|
|
$responses = [
|
|
new GraphResponse(
|
|
success: true,
|
|
data: [
|
|
'value' => [
|
|
['id' => 'filter-1', 'displayName' => 'Filter One'],
|
|
],
|
|
'@odata.nextLink' => 'https://graph.microsoft.com/beta/deviceManagement/assignmentFilters?$skiptoken=abc',
|
|
],
|
|
),
|
|
new GraphResponse(
|
|
success: true,
|
|
data: [
|
|
'value' => [
|
|
['id' => 'filter-2', 'displayName' => 'Filter Two'],
|
|
],
|
|
],
|
|
),
|
|
];
|
|
|
|
$client = new FoundationSnapshotGraphClient($responses);
|
|
app()->instance(GraphClientInterface::class, $client);
|
|
|
|
$service = app(FoundationSnapshotService::class);
|
|
$result = $service->fetchAll($tenant, 'assignmentFilter');
|
|
|
|
expect($result['items'])->toHaveCount(2);
|
|
expect($result['items'][0]['source_id'])->toBe('filter-1');
|
|
expect($result['items'][0]['metadata']['displayName'])->toBe('Filter One');
|
|
expect($result['items'][0]['metadata']['kind'])->toBe('assignmentFilter');
|
|
expect($result['items'][1]['source_id'])->toBe('filter-2');
|
|
|
|
expect($client->requests[0]['path'])->toBe('deviceManagement/assignmentFilters');
|
|
expect($client->requests[0]['options']['query']['$select'])->toBe('id,displayName');
|
|
expect($client->requests[1]['path'])->toBe('deviceManagement/assignmentFilters?$skiptoken=abc');
|
|
expect($client->requests[1]['options']['query'])->toBe([]);
|
|
});
|