Skip to content

Commit 0cf2530

Browse files
committed
HttpClient PHPStan fix
1 parent e16aeaf commit 0cf2530

File tree

1 file changed

+107
-82
lines changed

1 file changed

+107
-82
lines changed

src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php

Lines changed: 107 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,24 @@
55
namespace Codeception\Module\Symfony;
66

77
use Symfony\Component\HttpClient\DataCollector\HttpClientDataCollector;
8+
use Symfony\Component\VarDumper\Cloner\Data;
9+
use function array_change_key_case;
10+
use function array_filter;
11+
use function array_intersect_key;
812
use function array_key_exists;
9-
use function is_string;
13+
use function in_array;
14+
use function is_array;
15+
use function is_object;
16+
use function method_exists;
17+
use function sprintf;
1018

1119
trait HttpClientAssertionsTrait
1220
{
1321
/**
14-
* Asserts that the given URL has been called using, if specified, the given method body and headers.
15-
* By default, it will check on the HttpClient, but you can also pass a specific HttpClient ID.
16-
* (It will succeed if the request has been called multiple times.)
22+
* Asserts that the given URL has been called using, if specified, the given method, body and/or headers.
23+
* By default, it will inspect the default Symfony HttpClient; you may check a different one by passing its
24+
* service-id in $httpClientId.
25+
* It succeeds even if the request was executed multiple times.
1726
*
1827
* ```php
1928
* <?php
@@ -24,115 +33,131 @@ trait HttpClientAssertionsTrait
2433
* ['Authorization' => 'Bearer token']
2534
* );
2635
* ```
36+
*
37+
* @param string|array<mixed>|null $expectedBody
38+
* @param array<string,string|string[]> $expectedHeaders
2739
*/
28-
public function assertHttpClientRequest(string $expectedUrl, string $expectedMethod = 'GET', string|array|null $expectedBody = null, array $expectedHeaders = [], string $httpClientId = 'http_client'): void
29-
{
30-
$httpClientCollector = $this->grabHttpClientCollector(__FUNCTION__);
31-
$expectedRequestHasBeenFound = false;
32-
33-
if (!array_key_exists($httpClientId, $httpClientCollector->getClients())) {
34-
$this->fail(sprintf('HttpClient "%s" is not registered.', $httpClientId));
35-
}
36-
37-
foreach ($httpClientCollector->getClients()[$httpClientId]['traces'] as $trace) {
38-
if (($expectedUrl !== $trace['info']['url'] && $expectedUrl !== $trace['url'])
39-
|| $expectedMethod !== $trace['method']
40-
) {
41-
continue;
42-
}
43-
44-
if (null !== $expectedBody) {
45-
$actualBody = null;
46-
47-
if (null !== $trace['options']['body'] && null === $trace['options']['json']) {
48-
$actualBody = is_string($trace['options']['body']) ? $trace['options']['body'] : $trace['options']['body']->getValue(true);
40+
public function assertHttpClientRequest(
41+
string $expectedUrl,
42+
string $expectedMethod = 'GET',
43+
string|array|null $expectedBody = null,
44+
array $expectedHeaders = [],
45+
string $httpClientId = 'http_client',
46+
): void {
47+
$matchingRequests = array_filter(
48+
$this->getHttpClientTraces($httpClientId, __FUNCTION__),
49+
function (array $trace) use ($expectedUrl, $expectedMethod, $expectedBody, $expectedHeaders): bool {
50+
if (!$this->matchesUrlAndMethod($trace, $expectedUrl, $expectedMethod)) {
51+
return false;
4952
}
5053

51-
if (null === $trace['options']['body'] && null !== $trace['options']['json']) {
52-
$actualBody = $trace['options']['json']->getValue(true);
53-
}
54-
55-
if (!$actualBody) {
56-
continue;
57-
}
54+
$options = $trace['options'] ?? [];
55+
$actualBody = $this->extractValue($options['body'] ?? $options['json'] ?? null);
56+
$bodyMatches = $expectedBody === null || $expectedBody === $actualBody;
5857

59-
if ($expectedBody === $actualBody) {
60-
$expectedRequestHasBeenFound = true;
58+
$headersMatch = $expectedHeaders === [] || (
59+
is_array($headerValues = $this->extractValue($options['headers'] ?? []))
60+
&& ($normalizedExpected = array_change_key_case($expectedHeaders))
61+
=== array_intersect_key(array_change_key_case($headerValues), $normalizedExpected)
62+
);
6163

62-
if (!$expectedHeaders) {
63-
break;
64-
}
65-
}
66-
}
67-
68-
if ($expectedHeaders) {
69-
$actualHeaders = $trace['options']['headers'] ?? [];
70-
71-
foreach ($actualHeaders as $headerKey => $actualHeader) {
72-
if (array_key_exists($headerKey, $expectedHeaders)
73-
&& $expectedHeaders[$headerKey] === $actualHeader->getValue(true)
74-
) {
75-
$expectedRequestHasBeenFound = true;
76-
break 2;
77-
}
78-
}
79-
}
80-
81-
$expectedRequestHasBeenFound = true;
82-
break;
83-
}
64+
return $bodyMatches && $headersMatch;
65+
},
66+
);
8467

85-
$this->assertTrue($expectedRequestHasBeenFound, 'The expected request has not been called: "' . $expectedMethod . '" - "' . $expectedUrl . '"');
68+
$this->assertNotEmpty(
69+
$matchingRequests,
70+
sprintf('The expected request has not been called: "%s" - "%s"', $expectedMethod, $expectedUrl)
71+
);
8672
}
8773

8874
/**
89-
* Asserts that the given number of requests has been made on the HttpClient.
90-
* By default, it will check on the HttpClient, but you can also pass a specific HttpClient ID.
75+
* Asserts that exactly $count requests have been executed by the given HttpClient.
76+
* By default, it will inspect the default Symfony HttpClient; you may check a different one by passing its
77+
* service-id in $httpClientId.
9178
*
9279
* ```php
93-
* <?php
9480
* $I->assertHttpClientRequestCount(3);
9581
* ```
9682
*/
9783
public function assertHttpClientRequestCount(int $count, string $httpClientId = 'http_client'): void
9884
{
99-
$httpClientCollector = $this->grabHttpClientCollector(__FUNCTION__);
100-
101-
$this->assertCount($count, $httpClientCollector->getClients()[$httpClientId]['traces']);
85+
$this->assertCount($count, $this->getHttpClientTraces($httpClientId, __FUNCTION__));
10286
}
10387

10488
/**
105-
* Asserts that the given URL has not been called using GET or the specified method.
106-
* By default, it will check on the HttpClient, but a HttpClient id can be specified.
107-
*
89+
* Asserts that the given URL *has not* been requested with the supplied HTTP method.
90+
* By default, it will inspect the default Symfony HttpClient; you may check a different one by passing its
91+
* service-id in $httpClientId.
10892
* ```php
109-
* <?php
11093
* $I->assertNotHttpClientRequest('https://example.com/unexpected', 'GET');
11194
* ```
11295
*/
113-
public function assertNotHttpClientRequest(string $unexpectedUrl, string $expectedMethod = 'GET', string $httpClientId = 'http_client'): void
114-
{
115-
$httpClientCollector = $this->grabHttpClientCollector(__FUNCTION__);
116-
$unexpectedUrlHasBeenFound = false;
96+
public function assertNotHttpClientRequest(
97+
string $unexpectedUrl,
98+
string $unexpectedMethod = 'GET',
99+
string $httpClientId = 'http_client',
100+
): void {
101+
$matchingRequests = array_filter(
102+
$this->getHttpClientTraces($httpClientId, __FUNCTION__),
103+
fn(array $trace): bool => $this->matchesUrlAndMethod($trace, $unexpectedUrl, $unexpectedMethod)
104+
);
105+
106+
$this->assertEmpty(
107+
$matchingRequests,
108+
sprintf('Unexpected URL was called: "%s" - "%s"', $unexpectedMethod, $unexpectedUrl)
109+
);
110+
}
117111

118-
if (!array_key_exists($httpClientId, $httpClientCollector->getClients())) {
112+
/**
113+
* @return list<array{
114+
* info: array{url: string},
115+
* url: string,
116+
* method: string,
117+
* options?: array{body?: mixed, json?: mixed, headers?: mixed}
118+
* }>
119+
*/
120+
private function getHttpClientTraces(string $httpClientId, string $function): array
121+
{
122+
$httpClientCollector = $this->grabHttpClientCollector($function);
123+
124+
/** @var array<string, array{traces: list<array{
125+
* info: array{url: string},
126+
* url: string,
127+
* method: string,
128+
* options?: array{body?: mixed, json?: mixed, headers?: mixed}
129+
* }>}> $clients
130+
*/
131+
$clients = $httpClientCollector->getClients();
132+
133+
if (!array_key_exists($httpClientId, $clients)) {
119134
$this->fail(sprintf('HttpClient "%s" is not registered.', $httpClientId));
120135
}
121136

122-
foreach ($httpClientCollector->getClients()[$httpClientId]['traces'] as $trace) {
123-
if (($unexpectedUrl === $trace['info']['url'] || $unexpectedUrl === $trace['url'])
124-
&& $expectedMethod === $trace['method']
125-
) {
126-
$unexpectedUrlHasBeenFound = true;
127-
break;
128-
}
129-
}
137+
return $clients[$httpClientId]['traces'];
138+
}
130139

131-
$this->assertFalse($unexpectedUrlHasBeenFound, sprintf('Unexpected URL called: "%s" - "%s"', $expectedMethod, $unexpectedUrl));
140+
/** @param array{info: array{url: string}, url: string, method: string} $trace */
141+
private function matchesUrlAndMethod(array $trace, string $expectedUrl, string $expectedMethod): bool
142+
{
143+
return in_array($expectedUrl, [$trace['info']['url'], $trace['url']], true)
144+
&& $expectedMethod === $trace['method'];
145+
}
146+
147+
private function extractValue(mixed $value): mixed
148+
{
149+
return match (true) {
150+
$value instanceof Data => $value->getValue(true),
151+
is_object($value) && method_exists($value, 'getValue') => $value->getValue(true),
152+
is_object($value) && method_exists($value, '__toString') => (string) $value,
153+
default => $value,
154+
};
132155
}
133156

134157
protected function grabHttpClientCollector(string $function): HttpClientDataCollector
135158
{
136-
return $this->grabCollector('http_client', $function);
159+
/** @var HttpClientDataCollector $collector */
160+
$collector = $this->grabCollector('http_client', $function);
161+
return $collector;
137162
}
138163
}

0 commit comments

Comments
 (0)