5
5
namespace Codeception \Module \Symfony ;
6
6
7
7
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 ;
8
12
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 ;
10
18
11
19
trait HttpClientAssertionsTrait
12
20
{
13
21
/**
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.
17
26
*
18
27
* ```php
19
28
* <?php
@@ -24,115 +33,131 @@ trait HttpClientAssertionsTrait
24
33
* ['Authorization' => 'Bearer token']
25
34
* );
26
35
* ```
36
+ *
37
+ * @param string|array<mixed>|null $expectedBody
38
+ * @param array<string,string|string[]> $expectedHeaders
27
39
*/
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 ;
49
52
}
50
53
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 ;
58
57
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
+ );
61
63
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
+ );
84
67
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
+ );
86
72
}
87
73
88
74
/**
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.
91
78
*
92
79
* ```php
93
- * <?php
94
80
* $I->assertHttpClientRequestCount(3);
95
81
* ```
96
82
*/
97
83
public function assertHttpClientRequestCount (int $ count , string $ httpClientId = 'http_client ' ): void
98
84
{
99
- $ httpClientCollector = $ this ->grabHttpClientCollector (__FUNCTION__ );
100
-
101
- $ this ->assertCount ($ count , $ httpClientCollector ->getClients ()[$ httpClientId ]['traces ' ]);
85
+ $ this ->assertCount ($ count , $ this ->getHttpClientTraces ($ httpClientId , __FUNCTION__ ));
102
86
}
103
87
104
88
/**
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.
108
92
* ```php
109
- * <?php
110
93
* $I->assertNotHttpClientRequest('https://example.com/unexpected', 'GET');
111
94
* ```
112
95
*/
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
+ }
117
111
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 )) {
119
134
$ this ->fail (sprintf ('HttpClient "%s" is not registered. ' , $ httpClientId ));
120
135
}
121
136
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
+ }
130
139
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
+ };
132
155
}
133
156
134
157
protected function grabHttpClientCollector (string $ function ): HttpClientDataCollector
135
158
{
136
- return $ this ->grabCollector ('http_client ' , $ function );
159
+ /** @var HttpClientDataCollector $collector */
160
+ $ collector = $ this ->grabCollector ('http_client ' , $ function );
161
+ return $ collector ;
137
162
}
138
163
}
0 commit comments