Skip to content

Commit 223867b

Browse files
authored
feat: fix inconsistencies across assertion traits and tests (#228)
* Fix inconsistencies across assertion traits * Remove dead code and over-engineering from test app * Optimize HttpClient trace filtering * Cache routes by action to avoid redundant O(N) iterations * Use Stringable interface in HttpClientAssertionsTrait
1 parent c37d23e commit 223867b

18 files changed

Lines changed: 126 additions & 129 deletions

src/Codeception/Module/Symfony/BrowserAssertionsTrait.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
use Symfony\Component\HttpFoundation\Test\Constraint\ResponseIsUnprocessable;
2222
use Symfony\Component\HttpFoundation\Test\Constraint\ResponseStatusCodeSame;
2323

24+
use function class_exists;
25+
use function count;
2426
use function sprintf;
2527

2628
trait BrowserAssertionsTrait

src/Codeception/Module/Symfony/CacheTrait.php

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,8 @@ public function _getContainer(): ContainerInterface
3232
protected function getInternalDomains(): array
3333
{
3434
if (isset($this->state['internalDomains'])) {
35-
/** @var list<non-empty-string> $domains */
36-
$domains = $this->state['internalDomains'];
37-
38-
return $domains;
35+
/** @var list<non-empty-string> */
36+
return $this->state['internalDomains'];
3937
}
4038

4139
$domains = [];
@@ -48,12 +46,14 @@ protected function getInternalDomains(): array
4846
}
4947
}
5048

51-
return $this->state['internalDomains'] = array_values(array_unique($domains));
49+
/** @var list<non-empty-string> $domains */
50+
$domains = array_values(array_unique($domains));
51+
return $this->state['internalDomains'] = $domains;
5252
}
5353

54-
protected function clearInternalDomainsCache(): void
54+
protected function clearRouterCache(): void
5555
{
56-
unset($this->state['internalDomains']);
56+
unset($this->state['internalDomains'], $this->state['cachedRoutes']);
5757
}
5858

5959
/**
@@ -64,7 +64,6 @@ protected function clearInternalDomainsCache(): void
6464
*/
6565
protected function grabCachedService(string $expectedClass, array $serviceIds): ?object
6666
{
67-
/** @var ?string $serviceId */
6867
$serviceId = $this->state[$expectedClass] ??= (function () use ($serviceIds, $expectedClass): ?string {
6968
foreach ($serviceIds as $id) {
7069
if ($this->getService($id) instanceof $expectedClass) {
@@ -75,7 +74,7 @@ protected function grabCachedService(string $expectedClass, array $serviceIds):
7574
return null;
7675
})();
7776

78-
if ($serviceId === null) {
77+
if (!is_string($serviceId)) {
7978
return null;
8079
}
8180

src/Codeception/Module/Symfony/ConsoleAssertionsTrait.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Symfony\Component\Console\Tester\CommandTester;
1010
use Symfony\Component\HttpKernel\KernelInterface;
1111

12+
use function is_int;
1213
use function sprintf;
1314

1415
trait ConsoleAssertionsTrait

src/Codeception/Module/Symfony/FormAssertionsTrait.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44

55
namespace Codeception\Module\Symfony;
66

7+
use PHPUnit\Framework\Assert;
78
use Symfony\Component\Form\Extension\DataCollector\FormDataCollector;
89
use Symfony\Component\VarDumper\Cloner\Data;
910

11+
use function count;
12+
use function implode;
1013
use function is_array;
1114
use function is_int;
1215
use function is_numeric;
@@ -86,7 +89,7 @@ public function seeFormErrorMessage(string $field, ?string $message = null): voi
8689
$errors = $this->getErrorsForField($field);
8790

8891
if ($errors === []) {
89-
$this->fail("No form error message for field '{$field}'.");
92+
Assert::fail("No form error message for field '{$field}'.");
9093
}
9194

9295
if ($message !== null) {
@@ -203,7 +206,7 @@ private function getErrorsForField(string $field): array
203206
}
204207

205208
if (!$fieldFound) {
206-
$this->fail("The field '{$field}' does not exist in the form.");
209+
Assert::fail("The field '{$field}' does not exist in the form.");
207210
}
208211

209212
return $errorsForField;

src/Codeception/Module/Symfony/HttpClientAssertionsTrait.php

Lines changed: 57 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,15 @@
44

55
namespace Codeception\Module\Symfony;
66

7+
use PHPUnit\Framework\Assert;
8+
use Stringable;
79
use Symfony\Component\HttpClient\DataCollector\HttpClientDataCollector;
810
use Symfony\Component\VarDumper\Cloner\Data;
911

1012
use function array_change_key_case;
11-
use function array_filter;
1213
use function array_intersect_key;
13-
use function in_array;
1414
use function is_array;
1515
use function is_object;
16-
use function is_string;
1716
use function method_exists;
1817
use function sprintf;
1918

@@ -45,35 +44,8 @@ public function assertHttpClientRequest(
4544
array $expectedHeaders = [],
4645
string $httpClientId = 'http_client',
4746
): void {
48-
$matchingTraces = array_filter(
49-
$this->getHttpClientTraces($httpClientId, __FUNCTION__),
50-
function ($trace) use ($expectedUrl, $expectedMethod, $expectedBody, $expectedHeaders): bool {
51-
if (!is_array($trace) || !$this->matchesUrlAndMethod($trace, $expectedUrl, $expectedMethod)) {
52-
return false;
53-
}
54-
55-
$options = $this->extractValue($trace['options'] ?? []);
56-
$options = is_array($options) ? $options : [];
57-
58-
$expectedTraceBody = $this->extractValue($options['body'] ?? $options['json'] ?? null);
59-
if ($expectedBody !== null && $expectedBody !== $expectedTraceBody) {
60-
return false;
61-
}
62-
63-
if ($expectedHeaders === []) {
64-
return true;
65-
}
66-
67-
$actualHeaders = $this->extractValue($options['headers'] ?? []);
68-
$expected = array_change_key_case($expectedHeaders);
69-
70-
return is_array($actualHeaders)
71-
&& $expected === array_intersect_key(array_change_key_case($actualHeaders), $expected);
72-
},
73-
);
74-
75-
$this->assertNotEmpty(
76-
$matchingTraces,
47+
$this->assertTrue(
48+
$this->hasHttpClientRequest($httpClientId, __FUNCTION__, $expectedUrl, $expectedMethod, $expectedBody, $expectedHeaders),
7749
sprintf('The expected request has not been called: "%s" - "%s"', $expectedMethod, $expectedUrl)
7850
);
7951
}
@@ -106,53 +78,79 @@ public function assertNotHttpClientRequest(
10678
string $unexpectedMethod = 'GET',
10779
string $httpClientId = 'http_client',
10880
): void {
109-
$matchingTraces = array_filter(
110-
$this->getHttpClientTraces($httpClientId, __FUNCTION__),
111-
fn($trace): bool => is_array($trace) && $this->matchesUrlAndMethod($trace, $unexpectedUrl, $unexpectedMethod),
112-
);
113-
114-
$this->assertEmpty(
115-
$matchingTraces,
81+
$this->assertFalse(
82+
$this->hasHttpClientRequest($httpClientId, __FUNCTION__, $unexpectedUrl, $unexpectedMethod),
11683
sprintf('Unexpected URL was called: "%s" - "%s"', $unexpectedMethod, $unexpectedUrl)
11784
);
11885
}
11986

87+
/**
88+
* @param string|array<mixed>|null $expectedBody
89+
* @param array<string,string|string[]> $expectedHeaders
90+
*/
91+
private function hasHttpClientRequest(
92+
string $httpClientId,
93+
string $function,
94+
string $expectedUrl,
95+
string $expectedMethod,
96+
string|array|null $expectedBody = null,
97+
array $expectedHeaders = []
98+
): bool {
99+
$expectedHeadersLower = $expectedHeaders === [] ? [] : array_change_key_case($expectedHeaders);
100+
101+
foreach ($this->getHttpClientTraces($httpClientId, $function) as $trace) {
102+
if (!is_array($trace) || ($trace['method'] ?? null) !== $expectedMethod) {
103+
continue;
104+
}
105+
106+
$info = $this->extractValue($trace['info'] ?? []);
107+
$infoUrl = is_array($info) ? ($info['url'] ?? $info['original_url'] ?? null) : null;
108+
if ($expectedUrl !== $infoUrl && $expectedUrl !== ($trace['url'] ?? null)) {
109+
continue;
110+
}
111+
112+
if ($expectedBody === null && $expectedHeadersLower === []) {
113+
return true;
114+
}
115+
116+
$options = $this->extractValue($trace['options'] ?? []);
117+
$options = is_array($options) ? $options : [];
118+
if ($expectedBody !== null && $expectedBody !== $this->extractValue($options['body'] ?? $options['json'] ?? null)) {
119+
continue;
120+
}
121+
122+
if ($expectedHeadersLower === []) {
123+
return true;
124+
}
125+
126+
$actualHeaders = $this->extractValue($options['headers'] ?? []);
127+
if (is_array($actualHeaders) && $expectedHeadersLower === array_intersect_key(array_change_key_case($actualHeaders), $expectedHeadersLower)) {
128+
return true;
129+
}
130+
}
131+
132+
return false;
133+
}
134+
120135
/** @return array<mixed> */
121136
private function getHttpClientTraces(string $httpClientId, string $function): array
122137
{
123138
$clients = $this->grabHttpClientCollector($function)->getClients();
124-
125-
if (!isset($clients[$httpClientId])) {
126-
$this->fail(sprintf('HttpClient "%s" is not registered.', $httpClientId));
139+
if (!isset($clients[$httpClientId]) || !is_array($clients[$httpClientId])) {
140+
Assert::fail(sprintf('HttpClient "%s" is not registered.', $httpClientId));
127141
}
128142

129143
/** @var array{traces: array<mixed>} $clientData */
130144
$clientData = $clients[$httpClientId];
131145
return $clientData['traces'];
132146
}
133147

134-
/** @param array<mixed> $trace */
135-
private function matchesUrlAndMethod(array $trace, string $expectedUrl, string $expectedMethod): bool
136-
{
137-
$method = $trace['method'] ?? null;
138-
$url = $trace['url'] ?? null;
139-
140-
if (!is_string($method) || !is_string($url) || $expectedMethod !== $method) {
141-
return false;
142-
}
143-
144-
$info = $this->extractValue($trace['info'] ?? []);
145-
$infoUrl = is_array($info) ? ($info['url'] ?? $info['original_url'] ?? null) : null;
146-
147-
return in_array($expectedUrl, [$infoUrl, $url], true);
148-
}
149-
150148
private function extractValue(mixed $value): mixed
151149
{
152150
return match (true) {
153151
$value instanceof Data => $value->getValue(true),
154152
is_object($value) && method_exists($value, 'getValue') => $value->getValue(true),
155-
is_object($value) && method_exists($value, '__toString') => (string) $value,
153+
$value instanceof Stringable => (string) $value,
156154
default => $value,
157155
};
158156
}

src/Codeception/Module/Symfony/LoggerAssertionsTrait.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
use Symfony\Component\HttpKernel\DataCollector\LoggerDataCollector;
88
use Symfony\Component\VarDumper\Cloner\Data;
99

10+
use function array_map;
11+
use function count;
12+
use function implode;
13+
use function is_scalar;
14+
use function is_string;
15+
use function json_encode;
1016
use function sprintf;
1117

1218
trait LoggerAssertionsTrait

src/Codeception/Module/Symfony/MimeAssertionsTrait.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use Symfony\Component\Mime\Email;
1010
use Symfony\Component\Mime\Test\Constraint as MimeConstraint;
1111

12+
use function sprintf;
13+
1214
trait MimeAssertionsTrait
1315
{
1416
/**

src/Codeception/Module/Symfony/NotifierAssertionsTrait.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
use Symfony\Component\Notifier\Message\MessageInterface;
1414
use Symfony\Component\Notifier\Test\Constraint as NotifierConstraint;
1515

16-
use function end;
16+
use function array_key_last;
1717
use function version_compare;
1818

1919
trait NotifierAssertionsTrait
@@ -163,10 +163,9 @@ public function dontSeeNotificationIsSent(): void
163163
*/
164164
public function grabLastSentNotification(): ?MessageInterface
165165
{
166-
$notification = $this->getNotifierMessages();
167-
$lastNotification = end($notification);
166+
$notifications = $this->getNotifierMessages();
168167

169-
return $lastNotification ?: null;
168+
return $notifications ? $notifications[array_key_last($notifications)] : null;
170169
}
171170

172171
/**
@@ -205,7 +204,7 @@ public function seeNotificationIsSent(int $expectedCount = 1): void
205204
}
206205

207206
/** @return MessageEvent[] */
208-
public function getNotifierEvents(?string $transportName = null): array
207+
protected function getNotifierEvents(?string $transportName = null): array
209208
{
210209
return $this->getNotificationEvents()->getEvents($transportName);
211210
}
@@ -224,7 +223,7 @@ public function getNotifierEvent(int $index = 0, ?string $transportName = null):
224223
}
225224

226225
/** @return MessageInterface[] */
227-
public function getNotifierMessages(?string $transportName = null): array
226+
protected function getNotifierMessages(?string $transportName = null): array
228227
{
229228
return $this->getNotificationEvents()->getMessages($transportName);
230229
}

src/Codeception/Module/Symfony/RouterAssertionsTrait.php

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public function amOnRoute(string $routeName, array $params = []): void
5555
public function invalidateCachedRouter(): void
5656
{
5757
$this->unpersistService('router');
58-
$this->clearInternalDomainsCache();
58+
$this->clearRouterCache();
5959
}
6060

6161
/**
@@ -118,15 +118,34 @@ private function getCurrentRouteMatch(string $routeName): array
118118

119119
private function findRouteByActionOrFail(string $action): string
120120
{
121-
foreach ($this->grabRouterService()->getRouteCollection()->all() as $name => $route) {
122-
$ctrl = $route->getDefault('_controller');
123-
if (is_string($ctrl) && str_ends_with($ctrl, $action)) {
121+
foreach ($this->getCachedRoutes() as $ctrl => $name) {
122+
if (str_ends_with($ctrl, $action)) {
124123
return $name;
125124
}
126125
}
126+
127127
Assert::fail(sprintf("Action '%s' does not exist.", $action));
128128
}
129129

130+
/** @return array<string, string> */
131+
private function getCachedRoutes(): array
132+
{
133+
if (isset($this->state['cachedRoutes'])) {
134+
/** @var array<string, string> */
135+
return $this->state['cachedRoutes'];
136+
}
137+
138+
$routes = [];
139+
foreach ($this->grabRouterService()->getRouteCollection()->all() as $name => $route) {
140+
$ctrl = $route->getDefault('_controller');
141+
if (is_string($ctrl) && !isset($routes[$ctrl])) {
142+
$routes[$ctrl] = (string) $name;
143+
}
144+
}
145+
146+
return $this->state['cachedRoutes'] = $routes;
147+
}
148+
130149
private function assertRouteExists(string $routeName): void
131150
{
132151
$this->assertNotNull(

src/Codeception/Module/Symfony/SessionAssertionsTrait.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ public function amLoggedInAs(UserInterface $user, string $firewallName = 'main',
4646
$this->amLoggedInWithToken($this->createAuthenticationToken($user, $firewallName), $firewallName, $firewallContext);
4747
}
4848

49+
/**
50+
* Login with the given authentication token.
51+
* If you have more than one firewall or firewall context, you can specify the desired one as a parameter.
52+
*
53+
* ```php
54+
* <?php
55+
* $I->amLoggedInWithToken($token);
56+
* ```
57+
*/
4958
public function amLoggedInWithToken(TokenInterface $token, string $firewallName = 'main', ?string $firewallContext = null): void
5059
{
5160
$this->getTokenStorage()->setToken($token);

0 commit comments

Comments
 (0)