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+
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy