Skip to content

Commit 59deba5

Browse files
committed
[FrameworkBundle] Allow BrowserKit relative URL redirect assert
1 parent eed0ae4 commit 59deba5

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
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Add `DomCrawlerAssertionsTrait::assertSelectorCount(int $count, string $selector)`
88
* Allow to avoid `limit` definition in a RateLimiter configuration when using the `no_limit` policy
9+
* Add support for relative URLs in BrowserKit's redirect assertion.
910

1011
6.2
1112
---

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

@@ -304,7 +329,11 @@ private function getResponseTester(Response $response): WebTestCase
304329
$client = $this->createMock(KernelBrowser::class);
305330
$client->expects($this->any())->method('getResponse')->willReturn($response);
306331

307-
$request = new Request();
332+
$request = new Request([], [], [], [], [], [
333+
'HTTPS' => 'on',
334+
'SERVER_PORT' => 443,
335+
'SERVER_NAME' => 'example.com',
336+
]);
308337
$request->setFormat('custom', ['application/vnd.myformat']);
309338
$client->expects($this->any())->method('getRequest')->willReturn($request);
310339

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