8000 [FrameworkBundle] Allow BrowserKit relative URL redirect assert · symfony/symfony@fb4e9ae · GitHub
[go: up one dir, main page]

Skip to content

Commit fb4e9ae

Browse files
julienfalquefabpot
authored andcommitted
[FrameworkBundle] Allow BrowserKit relative URL redirect assert
1 parent 5872615 commit fb4e9ae

File tree

5 files changed

+242
-4
lines changed

5 files changed

+242
-4
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ CHANGELOG
2525
* Deprecate `framework.validation.enable_annotations`, use `framework.validation.enable_attributes` instead
2626
* Deprecate `framework.serializer.enable_annotations`, use `framework.serializer.enable_attributes` instead
2727
* Add `array $tokenAttributes = []` optional parameter to `KernelBrowser::loginUser()`
28+
* Add support for relative URLs in BrowserKit's redirect assertion.
2829

2930
6.3
3031
---

src/Symfony/Bundle/FrameworkBundle/Test/BrowserKitAssertionsTrait.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,13 @@ public static function assertResponseRedirects(string $expectedLocation = null,
4747
{
4848
$constraint = new ResponseConstraint\ResponseIsRedirected();
4949
if ($expectedLocation) {
50-
$constraint = LogicalAnd::fromConstraints($constraint, new ResponseConstraint\ResponseHeaderSame('Location', $expectedLocation));
50+
if (class_exists(ResponseConstraint\ResponseHeaderLocationSame::class)) {
51+
$locationConstraint = new ResponseConstraint\ResponseHeaderLocationSame(self::getRequest(), $expectedLocation);
52+
} else {
53+
$locationConstraint = new ResponseConstraint\ResponseHeaderSame('Location', $expectedLocation);
54+
}
55+
56+
$constraint = LogicalAnd::fromConstraints($constraint, $locationConstraint);
5157
}
5258
if ($expectedCode) {
5359
$constraint = LogicalAnd::fromConstraints($constraint, new ResponseConstraint\ResponseStatusCodeSame($expectedCode));

src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Symfony\Component\HttpFoundation\Cookie as HttpFoundationCookie;
2424
use Symfony\Component\HttpFoundation\Request;
2525
use Symfony\Component\HttpFoundation\Response;
26+
use Symfony\Component\HttpFoundation\Test\Constraint\ResponseHeaderLocationSame;
2627

2728
class WebTestCaseTest extends TestCase
2829
{
@@ -55,10 +56,34 @@ public function testAssertResponseRedirectsWithLocation()
5556
{
5657
$this->getResponseTester(new Response('', 301, ['Location' => 'https://example.com/']))->assertResponseRedirects('https://example.com/');
5758
$this->expectException(AssertionFailedError::class);
58-
$this->expectExceptionMessage('is redirected and has header "Location" with value "https://example.com/".');
59+
$this->expectExceptionMessageMatches('#is redirected and has header "Location" (with value|matching) "https://example\.com/"\.#');
5960
$this->getResponseTester(new Response('', 301))->assertResponseRedirects('https://example.com/');
6061
}
6162

63+
public function testAssertResponseRedirectsWithLocationWithoutHost()
64+
{
65+
if (!class_exists(ResponseHeaderLocationSame::class)) {
66+
$this->markTestSkipped('Requires symfony/http-foundation 6.3 or higher.');
67+
}
68+
69+
$this->getResponseTester(new Response('', 301, ['Location' => 'https://example.com/']))->assertResponseRedirects('/');
70+
$this->expectException(AssertionFailedError::class);
71+
$this->expectExceptionMessage('is redirected and has header "Location" matching "/".');
72+
$this->getResponseTester(new Response('', 301))->assertResponseRedirects('/');
73+
}
74+
75+
public function testAssertResponseRedirectsWithLocationWithoutScheme()
76+
{
77+
if (!class_exists(ResponseHeaderLocationSame::class)) {
78+
$this->markTestSkipped('Requires symfony/http-foundation 6.3 or higher.');
79+
}
80+
81+
$this->getResponseTester(new Response('', 301, ['Location' => 'https://example.com/']))->assertResponseRedirects('//example.com/');
82+
$this->expectException(AssertionFailedError::class);
83+
$this->expectExceptionMessage('is redirected and has header "Location" matching "//example.com/".');
84+
$this->getResponseTester(new Response('', 301))->assertResponseRedirects('//example.com/');
85+
}
86+
6287
public function testAssertResponseRedirectsWithStatusCode()
6388
{
6489
$this->getResponseTester(new Response('', 302))->assertResponseRedirects(null, 302);
@@ -71,7 +96,7 @@ public function testAssertResponseRedirectsWithLocationAndStatusCode()
7196
{
7297
$this->getResponseTester(new Response('', 302, ['Location' => 'https://example.com/']))->assertResponseRedirects('https://example.com/', 302);
7398
$this->expectException(AssertionFailedError::class);
74-
$this->expectExceptionMessageMatches('#(:?\( )?is redirected and has header "Location" with value "https://example\.com/" (:?\) )?and status code is 301\.#');
99+
$this->expectExceptionMessageMatches('#(:?\( )?is redirected and has header "Location" (with value|matching) "https://example\.com/" (:?\) )?and status code is 301\.#');
75100
$this->getResponseTester(new Response('', 302))->assertResponseRedirects('https://example.com/', 301);
76101
}
77102

@@ -330,7 +355,11 @@ private function getResponseTester(Response $response): WebTestCase
330355
$client = $this->createMock(KernelBrowser::class);
331356
$client->expects($this->any())->method('getResponse')->willReturn($response);
332357

333-
$request = new Request();
358+
$request = new Request([], [], [], [], [], [
359+
'HTTPS' => 'on',
360+
'SERVER_PORT' => 443,
361+
'SERVER_NAME' => 'example.com',
362+
]);
334363
$request->setFormat('custom', ['application/vnd.myformat']);
335364
$client->expects($this->any())->method('getRequest')->willReturn($request);
336365

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Test\Constraint;
13+
14+
use PHPUnit\Framework\Constraint\Constraint;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\Response;
17+
18+
final class ResponseHeaderLocationSame extends Constraint
19+
{
20+
public function __construct(private Request $request, private string $expectedValue)
21+
{
22+
}
23+
24+
public function toString(): string
25+
{
26+
return sprintf('has header "Location" matching "%s"', $this->expectedValue);
27+
}
28+
29+
protected function matches($other): bool
30+
{
31+
if (!$other instanceof Response) {
32+
return false;
33+
}
34+
35+
$location = $other->headers->get('Location');
36+
37+
if (null === $location) {
38+
return false;
39+
}
40+
41+
return $this->toFullUrl($this->expectedValue) === $this->toFullUrl($location);
42+
}
43+
44+
protected function failureDescription($other): string
45+
{
46+
return 'the Response '.$this->toString();
47+
}
48+
49+
private function toFullUrl(string $url): string
50+
{
51+
if (null === parse_url($url, \PHP_URL_PATH)) {
52+
$url .= '/';
53+
}
54+
55+
if (str_starts_with($url, '//')) {
56+
return "{$this->request->getScheme()}:{$url}";
57+
}
58+
59+
if (str_starts_with($url, '/')) {
60+
return "{$this->request->getSchemeAndHttpHost()}{$url}";
61+
}
62+
63+
return $url;
64+
}
65+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Tests\Test\Constraint;
13+
14+
use PHPUnit\Framework\ExpectationFailedException;
15+
use PHPUnit\Framework\TestCase;
16+
use Symfony\Component\HttpFoundation\Request;
17+
use Symfony\Component\HttpFoundation\Response;
18+
use Symfony\Component\HttpFoundation\Test\Constraint\ResponseHeaderLocationSame;
19+
20+
class ResponseHeaderLocationSameTest extends TestCase
21+
{
22+
/**
23+
* @dataProvider provideSuccessCases
24+
*/
25+
public function testConstraintSuccess(string $requestUrl, ?string $location, string $expectedLocation)
26+
{
27+
$request = Request::create($requestUrl);
28+
29+
$response = new Response();
30+
if (null !== $location) {
31+
$response->headers->set('Location', $location);
32+
}
33+
34+
$constraint = new ResponseHeaderLocationSame($request, $expectedLocation);
35+
36+
self::assertTrue($constraint->evaluate($response, '', true));
37+
}
38+
< F438 code>39+
public function provideSuccessCases(): iterable
40+
{
41+
yield ['http://example.com', 'http://example.com', 'http://example.com'];
42+
yield ['http://example.com', 'http://example.com', '//example.com'];
43+
yield ['http://example.com', 'http://example.com', '/'];
44+
yield ['http://example.com', '//example.com', 'http://example.com'];
45+
yield ['http://example.com', '//example.com', '//example.com'];
46+
yield ['http://example.com', '//example.com', '/'];
47+
yield ['http://example.com', '/', 'http://example.com'];
48+
yield ['http://example.com', '/', '//example.com'];
49+
yield ['http://example.com', '/', '/'];
50+
51+
yield ['http://example.com/', 'http://example.com/', 'http://example.com/'];
52+
yield ['http://example.com/', 'http://example.com/', '//example.com/'];
53+
yield ['http://example.com/', 'http://example.com/', '/'];
54+
yield ['http://example.com/', '//example.com/', 'http://example.com/'];
55+
yield ['http://example.com/', '//example.com/', '//example.com/'];
56+
yield ['http://example.com/', '//example.com/', '/'];
57+
yield ['http://example.com/', '/', 'http://example.com/'];
58+
yield ['http://example.com/', '/', '//example.com/'];
59+
yield ['http://example.com/', '/', '/'];
60+
61+
yield ['http://example.com/foo', 'http://example.com/', 'http://example.com/'];
62+
yield ['http://example.com/foo', 'http://example.com/', '//example.com/'];
63+
yield ['http://example.com/foo', 'http://example.com/', '/'];
64+
yield ['http://example.com/foo', '//example.com/', 'http://example.com/'];
65+
yield ['http://example.com/foo', '//example.com/', '//example.com/'];
66+
yield ['http://example.com/foo', '//example.com/', '/'];
67+
yield ['http://example.com/foo', '/', 'http://example.com/'];
68+
yield ['http://example.com/foo', '/', '//example.com/'];
69+
yield ['http://example.com/foo', '/', '/'];
70+
71+
yield ['http://example.com/foo', 'http://example.com/bar', 'http://example.com/bar'];
72+
yield ['http://example.com/foo', 'http://example.com/bar', '//example.com/bar'];
73+
yield ['http://example.com/foo', 'http://example.com/bar', '/bar'];
74+
yield ['http://example.com/foo', '//example.com/bar', 'http://example.com/bar'];
75+
yield ['http://example.com/foo', '//example.com/bar', '//example.com/bar'];
76+
yield ['http://example.com/foo', '//example.com/bar', '/bar'];
77+
yield ['http://example.com/foo', '/bar', 'http://example.com/bar'];
78+
yield ['http://example.com/foo', '/bar', '//example.com/bar'];
79+
yield ['http://example.com/foo', '/bar', '/bar'];
80+
81+
yield ['http://example.com', 'http://example.com/bar', 'http://example.com/bar'];
82+
yield ['http://example.com', 'http://example.com/bar', '//example.com/bar'];
83+
yield ['http://example.com', 'http://example.com/bar', '/bar'];
84+
yield ['http://example.com', '//example.com/bar', 'http://example.com/bar'];
85+
yield ['http://example.com', '//example.com/bar', '//example.com/bar'];
86+
yield ['http://example.com', '//example.com/bar', '/bar'];
87+
yield ['http://example.com', '/bar', 'http://example.com/bar'];
88+
yield ['http://example.com', '/bar', '//example.com/bar'];
89+
yield ['http://example.com', '/bar', '/bar'];
90+
91+
yield ['http://example.com/', 'http://another-example.com', 'http://another-example.com'];
92+
}
93+
94+
/**
95+
* @dataProvider provideFailureCases
96+
*/
97+
public function testConstraintFailure(string $requestUrl, ?string $location, string $expectedLocation)
98+
{
99+
$request = Request::create($requestUrl);
100+
101+
$response = new Response();
102+
if (null !== $location) {
103+
$response->headers->set('Location', $location);
104+
}
105+
106+
$constraint = new ResponseHeaderLocationSame($request, $expectedLocation);
107+
108+
self::assertFalse($constraint->evaluate($response, '', true));
109+
110+
$this->expectException(ExpectationFailedException::class);
111+
112+
$constraint->evaluate($response);
113+
}
114+
115+
public function provideFailureCases(): iterable
116+
{
117+
yield ['http://example.com', null, 'http://example.com'];
118+
yield ['http://example.com', null, '//example.com'];
119+
yield ['http://example.com', null, '/'];
120+
121+
yield ['http://example.com', 'http://another-example.com', 'http://example.com'];
122+
yield ['http://example.com', 'http://another-example.com', '//example.com'];
123+
yield ['http://example.com', 'http://another-example.com', '/'];
124+
125+
yield ['http://example.com', 'http://example.com/bar', 'http://example.com'];
126+
yield ['http://example.com', 'http://example.com/bar', '//example.com'];
127+
yield ['http://example.com', 'http://example.com/bar', '/'];
128+
129+
yield ['http://example.com/foo', 'http://example.com/bar', 'http://example.com'];
130+
yield ['http://example.com/foo', 'http://example.com/bar', '//example.com'];
131+
yield ['http://example.com/foo', 'http://example.com/bar', '/'];
132+
133+
yield ['http://example.com/foo', 'http://example.com/bar', 'http://example.com/foo'];
134+
yield ['http://example.com/foo', 'http://example.com/bar', '//example.com/foo'];
135+
yield ['http://example.com/foo', 'http://example.com/bar', '/foo'];
136+
}
137+
}

0 commit comments

Comments
 (0)
0