Http::response(['value' => []], 200), ]); $logger = mock(GraphLogger::class); $logger->shouldReceive('logRequest')->zeroOrMoreTimes()->andReturnNull(); $logger->shouldReceive('logResponse')->zeroOrMoreTimes()->andReturnNull(); $client = new MicrosoftGraphClient( logger: $logger, contracts: app(GraphContractRegistry::class), ); $client->listPolicies('endpointSecurityPolicy', [ 'access_token' => 'test-token', ]); $client->listPolicies('securityBaselinePolicy', [ 'access_token' => 'test-token', ]); $client->listPolicies('settingsCatalogPolicy', [ 'access_token' => 'test-token', ]); Http::assertSent(function (Request $request) { $url = $request->url(); if (! str_contains($url, '/deviceManagement/configurationPolicies')) { return false; } parse_str((string) parse_url($url, PHP_URL_QUERY), $query); expect($query)->toHaveKey('$select'); $select = (string) $query['$select']; expect($select)->toContain('technologies') ->and($select)->toContain('templateReference') ->and($select)->toContain('name') ->and($select)->not->toContain('@odata.type'); expect($select)->not->toContain('displayName'); expect($select)->not->toContain('version'); return true; }); }); it('retries list policies without $select on select/expand parsing errors', function () { Http::fake([ 'graph.microsoft.com/*' => Http::sequence() ->push([ 'error' => [ 'code' => 'BadRequest', 'message' => "Parsing OData Select and Expand failed: Could not find a property named 'version' on type 'microsoft.graph.deviceManagementConfigurationPolicy'.", ], ], 400) ->push([ 'error' => [ 'code' => 'BadRequest', 'message' => "Parsing OData Select and Expand failed: Could not find a property named 'version' on type 'microsoft.graph.deviceManagementConfigurationPolicy'.", ], ], 400) ->push(['value' => [['id' => 'policy-1', 'name' => 'Policy One']]], 200), ]); $logger = mock(GraphLogger::class); $logger->shouldReceive('logRequest')->zeroOrMoreTimes()->andReturnNull(); $logger->shouldReceive('logResponse')->zeroOrMoreTimes()->andReturnNull(); $client = new MicrosoftGraphClient( logger: $logger, contracts: app(GraphContractRegistry::class), ); $response = $client->listPolicies('settingsCatalogPolicy', [ 'access_token' => 'test-token', ]); expect($response->successful())->toBeTrue(); expect($response->data)->toHaveCount(1); expect($response->warnings)->toContain('Capability fallback applied: removed $select for compatibility.'); $recorded = Http::recorded(); expect($recorded)->toHaveCount(3); [$firstRequest] = $recorded[0]; [$secondRequest] = $recorded[1]; [$thirdRequest] = $recorded[2]; parse_str((string) parse_url($firstRequest->url(), PHP_URL_QUERY), $firstQuery); parse_str((string) parse_url($secondRequest->url(), PHP_URL_QUERY), $secondQuery); parse_str((string) parse_url($thirdRequest->url(), PHP_URL_QUERY), $thirdQuery); expect($firstQuery)->toHaveKey('$select'); expect($secondQuery)->toHaveKey('$select'); expect($thirdQuery)->not->toHaveKey('$select'); }); it('paginates list policies when nextLink is present', function () { $nextLink = 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies?$skiptoken=page2'; Http::fake([ 'graph.microsoft.com/*' => Http::sequence() ->push([ 'value' => [ ['id' => 'policy-1', 'name' => 'Policy One'], ], '@odata.nextLink' => $nextLink, ], 200) ->push([ 'value' => [ ['id' => 'policy-2', 'name' => 'Policy Two'], ], ], 200), ]); $logger = mock(GraphLogger::class); $logger->shouldReceive('logRequest')->zeroOrMoreTimes()->andReturnNull(); $logger->shouldReceive('logResponse')->zeroOrMoreTimes()->andReturnNull(); $client = new MicrosoftGraphClient( logger: $logger, contracts: app(GraphContractRegistry::class), ); $response = $client->listPolicies('settingsCatalogPolicy', [ 'access_token' => 'test-token', ]); expect($response->successful())->toBeTrue(); expect($response->data)->toHaveCount(2); expect(collect($response->data)->pluck('id')->all())->toMatchArray(['policy-1', 'policy-2']); $recorded = Http::recorded(); expect($recorded)->toHaveCount(2); [$firstRequest] = $recorded[0]; [$secondRequest] = $recorded[1]; expect($firstRequest->url())->toContain('/deviceManagement/configurationPolicies'); expect($secondRequest->url())->toBe($nextLink); });