fix: properly forward return urls where missing

This commit is contained in:
Björn Fromme
2026-08-11 14:34:46 +02:00
parent 102e1c1c3c
commit 48cd2d6b67
27 changed files with 194 additions and 24 deletions
@@ -39,6 +39,7 @@ class DeleteController extends AbstractController
return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_detail', [ return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_detail', [
'uuid' => $application->getAssignment()->getUuid(), 'uuid' => $application->getAssignment()->getUuid(),
'r' => $request->query->get('r'),
])); ]));
} }
@@ -46,6 +46,7 @@ class DisposeController extends AbstractController
$redirectUrl = $this->generateUrl('app_administrative_assignment_detail', [ $redirectUrl = $this->generateUrl('app_administrative_assignment_detail', [
'uuid' => $application->getAssignment()->getUuid(), 'uuid' => $application->getAssignment()->getUuid(),
'r' => $request->query->get('r'),
]); ]);
return new HxRedirectResponse($redirectUrl); return new HxRedirectResponse($redirectUrl);
@@ -57,6 +57,7 @@ class StatusController extends AbstractController
return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_detail', [ return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_detail', [
'uuid' => $application->getAssignment()->getUuid(), 'uuid' => $application->getAssignment()->getUuid(),
'r' => $request->query->get('r'),
])); ]));
} }
@@ -53,6 +53,7 @@ class DeleteController extends AbstractController
return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_detail', [ return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_detail', [
'uuid' => $disposition->getAssignment()->getUuid(), 'uuid' => $disposition->getAssignment()->getUuid(),
'r' => $request->query->get('r'),
])); ]));
} }
@@ -44,6 +44,7 @@ class SkillsController extends AbstractController
return $this->redirectToRoute('app_admin_teamer_skills', [ return $this->redirectToRoute('app_admin_teamer_skills', [
'uuid' => $teamer->getUuid(), 'uuid' => $teamer->getUuid(),
'r' => $request->query->get('r'),
]); ]);
} }
@@ -55,6 +55,7 @@ class DuplicateController extends AbstractController
return $this->redirectToRoute('app_administrative_assignment_edit', [ return $this->redirectToRoute('app_administrative_assignment_edit', [
'uuid' => $copy->getUuid(), 'uuid' => $copy->getUuid(),
'r' => $request->query->get('r'),
]); ]);
} }
@@ -38,6 +38,7 @@ class BusProNetSyncToggleController extends AbstractController
return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_detail', [ return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_detail', [
'uuid' => $disposition->getAssignment()->getUuid(), 'uuid' => $disposition->getAssignment()->getUuid(),
'r' => $request->query->get('r'),
])); ]));
} }
} }
@@ -55,6 +55,7 @@ class CallOffController extends AbstractController
return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_detail', [ return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_detail', [
'uuid' => $assignment->getUuid(), 'uuid' => $assignment->getUuid(),
'r' => $request->query->get('r'),
])); ]));
} }
@@ -61,6 +61,7 @@ class DocumentUploadController extends AbstractController
return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_detail', [ return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_detail', [
'uuid' => $disposition->getAssignment()->getUuid(), 'uuid' => $disposition->getAssignment()->getUuid(),
'r' => $request->query->get('r'),
])); ]));
} }
@@ -38,6 +38,7 @@ class SpecialAgreementsController extends AbstractController
return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_detail', [ return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_detail', [
'uuid' => $disposition->getAssignment()->getUuid(), 'uuid' => $disposition->getAssignment()->getUuid(),
'r' => $request->query->get('r'),
])); ]));
} }
@@ -24,7 +24,13 @@ class InfoController extends AbstractController
#[IsGranted(new Expression('is_granted("ROLE_ADMINISTRATIVE") or is_granted("ROLE_HOUSE_MANAGER")'))] #[IsGranted(new Expression('is_granted("ROLE_ADMINISTRATIVE") or is_granted("ROLE_HOUSE_MANAGER")'))]
public function index(Teamer $teamer, Request $request): Response public function index(Teamer $teamer, Request $request): Response
{ {
$returnUrl = $this->getReturnUrl($request, 'app_administrative_assignment_index'); // House managers reach this modal but not the administrative overviews,
// so their fallback has to stay inside their own area.
$defaultReturnUrlRoute = $this->isGranted('ROLE_ADMINISTRATIVE')
? 'app_administrative_assignment_index'
: 'app_house_manager_index';
$returnUrl = $this->getReturnUrl($request, $defaultReturnUrlRoute);
$recentDispositions = $this->dispositionRepository->findRecentDispositionsByTeamer($teamer); $recentDispositions = $this->dispositionRepository->findRecentDispositionsByTeamer($teamer);
@@ -66,6 +66,7 @@ class DetailController extends AbstractController
return $this->redirectToRoute('app_teamer_disposition_detail', [ return $this->redirectToRoute('app_teamer_disposition_detail', [
'uuid' => $disposition->getUuid(), 'uuid' => $disposition->getUuid(),
'r' => $request->query->get('r'),
]); ]);
} }
+8 -2
View File
@@ -8,9 +8,15 @@ trait ReturnUrlTrait
{ {
public function getReturnUrl(Request $request, string $defaultRoute, array $parameters = []): string public function getReturnUrl(Request $request, string $defaultRoute, array $parameters = []): string
{ {
$defaultUrl = $this->generateUrl($defaultRoute, $parameters); $returnUrl = $request->query->get('r');
return rawurldecode($request->query->get('r', $defaultUrl)); // An empty "r" reaches us whenever a link forwards a return url it never got
// itself, it must not be mistaken for a return url pointing at the root.
if (false === is_string($returnUrl) || '' === $returnUrl) {
return $this->generateUrl($defaultRoute, $parameters);
}
return rawurldecode($returnUrl);
} }
/** /**
+1
View File
@@ -32,6 +32,7 @@ class AppExtension extends AbstractExtension
new TwigFunction('icon', [AppRuntime::class, 'renderIcon'], ['needs_environment' => true, 'is_safe' => ['html']]), new TwigFunction('icon', [AppRuntime::class, 'renderIcon'], ['needs_environment' => true, 'is_safe' => ['html']]),
new TwigFunction('is_current_route', [AppRuntime::class, 'isCurrentRoute']), new TwigFunction('is_current_route', [AppRuntime::class, 'isCurrentRoute']),
new TwigFunction('return_url', [AppRuntime::class, 'getEncodedReturnUrl']), new TwigFunction('return_url', [AppRuntime::class, 'getEncodedReturnUrl']),
new TwigFunction('forwarded_return_url', [AppRuntime::class, 'getForwardedReturnUrl']),
new TwigFunction('qa_attribute', [AppRuntime::class, 'renderQaAttribute'], ['is_safe' => ['html']]), new TwigFunction('qa_attribute', [AppRuntime::class, 'renderQaAttribute'], ['is_safe' => ['html']]),
new TwigFunction('has_pickup', [AppRuntime::class, 'hasPickup']), new TwigFunction('has_pickup', [AppRuntime::class, 'hasPickup']),
]; ];
+15
View File
@@ -192,6 +192,21 @@ class AppRuntime implements RuntimeExtensionInterface
return rawurlencode($masterRequest->getRequestUri()); return rawurlencode($masterRequest->getRequestUri());
} }
/**
* Passes the return url of the current page on to the next one.
*
* Used by links that lead deeper into a detail view or trigger an action on it:
* they have to keep pointing back at where the user originally came from instead
* of at the page they sit on. Null when there is nothing to pass on, so that the
* url generator drops the parameter rather than emitting an empty one.
*/
public function getForwardedReturnUrl(): ?string
{
$returnUrl = $this->requestStack->getMainRequest()?->query->get('r');
return is_string($returnUrl) && '' !== $returnUrl ? $returnUrl : null;
}
public function renderQaAttribute(string $label, ?string $value = null): string public function renderQaAttribute(string $label, ?string $value = null): string
{ {
if ('test' !== $this->environment) { if ('test' !== $this->environment) {
+1 -1
View File
@@ -107,7 +107,7 @@
role="button" role="button"
title="Bewerber:innen-Info anzeigen" title="Bewerber:innen-Info anzeigen"
aria-label="Bewerber:innen-Info anzeigen" aria-label="Bewerber:innen-Info anzeigen"
hx-get="{{ path('app_administrative_teamer_info', { 'uuid': application.teamer.uuid }) }}" hx-get="{{ path('app_administrative_teamer_info', { 'uuid': application.teamer.uuid, 'r': return_url() }) }}"
hx-target="body" hx-target="body"
hx-swap="beforeend"> hx-swap="beforeend">
{% set requirementsMet = true %} {% set requirementsMet = true %}
+1 -1
View File
@@ -41,7 +41,7 @@
{% for disposition in newDispositions %} {% for disposition in newDispositions %}
{% set assignment = disposition.assignment %} {% set assignment = disposition.assignment %}
<li class="py-2 first:pt-0 last:pb-0 overflow-x-hidden"> <li class="py-2 first:pt-0 last:pb-0 overflow-x-hidden">
<a href="{{ path('app_administrative_assignment_detail', { 'uuid': assignment.uuid }) }}" class="flex items-center space-x-1"> <a href="{{ path('app_administrative_assignment_detail', { 'uuid': assignment.uuid, 'r': return_url() }) }}" class="flex items-center space-x-1">
{{ icon('hand', 'w-4 h-4 shrink-0') }} {{ icon('hand', 'w-4 h-4 shrink-0') }}
{% include '_partials/_disposition_data.html.twig' with { 'disposition': disposition } %} {% include '_partials/_disposition_data.html.twig' with { 'disposition': disposition } %}
</a> </a>
@@ -73,7 +73,8 @@
class="text-green-500" class="text-green-500"
title="einteilen" title="einteilen"
hx-get="{{ path('app_admin_application_dispose', { hx-get="{{ path('app_admin_application_dispose', {
'uuid': application.uuid 'uuid': application.uuid,
'r': forwarded_return_url()
}) }}" }) }}"
hx-target="body" hx-target="body"
hx-swap="beforeend"> hx-swap="beforeend">
@@ -85,7 +86,8 @@
class="text-primary" class="text-primary"
title="Bewerbungsstatus bearbeiten" title="Bewerbungsstatus bearbeiten"
hx-get="{{ path('app_admin_application_status', { hx-get="{{ path('app_admin_application_status', {
'uuid': application.uuid 'uuid': application.uuid,
'r': forwarded_return_url()
}) }}" }) }}"
hx-target="body" hx-target="body"
hx-swap="beforeend"> hx-swap="beforeend">
@@ -97,7 +99,8 @@
class="text-red-500" class="text-red-500"
title="Bewerbung löschen" title="Bewerbung löschen"
hx-get="{{ path('app_admin_application_delete', { hx-get="{{ path('app_admin_application_delete', {
'uuid': application.uuid 'uuid': application.uuid,
'r': forwarded_return_url()
}) }}" }) }}"
hx-target="body" hx-target="body"
hx-swap="beforeend"> hx-swap="beforeend">
@@ -152,7 +155,7 @@
<button type="button" <button type="button"
class="flex items-center space-x-1" class="flex items-center space-x-1"
title="Teamer:inneninfo {{ disposition.teamer }}" title="Teamer:inneninfo {{ disposition.teamer }}"
hx-get="{{ path('app_administrative_teamer_info', { 'uuid': disposition.teamer.uuid }) }}" hx-get="{{ path('app_administrative_teamer_info', { 'uuid': disposition.teamer.uuid, 'r': return_url() }) }}"
hx-target="body" hx-target="body"
hx-swap="beforeend"> hx-swap="beforeend">
<span>{{ disposition.teamer }}</span> <span>{{ disposition.teamer }}</span>
@@ -183,7 +186,7 @@
</td> </td>
<td> <td>
<div class="flex items-center justify-end space-x-2"> <div class="flex items-center justify-end space-x-2">
<button type="button" hx-post="{{ path('app_administrative_disposition_bpn_sync_toggle', { 'uuid': disposition.uuid }) }}" hx-swap="none"> <button type="button" hx-post="{{ path('app_administrative_disposition_bpn_sync_toggle', { 'uuid': disposition.uuid, 'r': forwarded_return_url() }) }}" hx-swap="none">
{{ icon('chair', html_classes('w-5 h-5', {'text-green-500': disposition.syncedWithBusProNet })) }} {{ icon('chair', html_classes('w-5 h-5', {'text-green-500': disposition.syncedWithBusProNet })) }}
</button> </button>
<twig:DropDown> <twig:DropDown>
@@ -191,7 +194,7 @@
class="text-gray-700 hover:text-primary block px-4 py-2 text-sm w-full text-left" class="text-gray-700 hover:text-primary block px-4 py-2 text-sm w-full text-left"
role="menuitem" role="menuitem"
tabindex="-1" tabindex="-1"
hx-get="{{ path('app_administrative_disposition_special_agreements', { 'uuid': disposition.uuid }) }}" hx-get="{{ path('app_administrative_disposition_special_agreements', { 'uuid': disposition.uuid, 'r': forwarded_return_url() }) }}"
hx-target="body" hx-target="body"
hx-swap="beforeend"> hx-swap="beforeend">
zusätzliche Absprachen zusätzliche Absprachen
@@ -201,7 +204,7 @@
class="text-gray-700 hover:text-primary block px-4 py-2 text-sm w-full text-left" class="text-gray-700 hover:text-primary block px-4 py-2 text-sm w-full text-left"
role="menuitem" role="menuitem"
tabindex="-1" tabindex="-1"
hx-get="{{ path('app_administrative_disposition_document_upload', { 'uuid': disposition.uuid, 'type': 'contract' }) }}" hx-get="{{ path('app_administrative_disposition_document_upload', { 'uuid': disposition.uuid, 'type': 'contract', 'r': forwarded_return_url() }) }}"
hx-target="body" hx-target="body"
hx-swap="beforeend"> hx-swap="beforeend">
Honorarvertrag hochladen Honorarvertrag hochladen
@@ -210,7 +213,7 @@
class="text-gray-700 hover:text-primary block px-4 py-2 text-sm w-full text-left" class="text-gray-700 hover:text-primary block px-4 py-2 text-sm w-full text-left"
role="menuitem" role="menuitem"
tabindex="-1" tabindex="-1"
hx-get="{{ path('app_administrative_disposition_document_upload', { 'uuid': disposition.uuid, 'type': 'invoice' }) }}" hx-get="{{ path('app_administrative_disposition_document_upload', { 'uuid': disposition.uuid, 'type': 'invoice', 'r': forwarded_return_url() }) }}"
hx-target="body" hx-target="body"
hx-swap="beforeend"> hx-swap="beforeend">
Honorarnote hochladen Honorarnote hochladen
@@ -221,7 +224,7 @@
class="text-red-500 hover:text-primary block px-4 py-2 text-sm w-full text-left" class="text-red-500 hover:text-primary block px-4 py-2 text-sm w-full text-left"
role="menuitem" role="menuitem"
tabindex="-1" tabindex="-1"
hx-get="{{ path('app_administrative_disposition_call_off', { 'uuid': disposition.uuid }) }}" hx-get="{{ path('app_administrative_disposition_call_off', { 'uuid': disposition.uuid, 'r': forwarded_return_url() }) }}"
hx-target="body" hx-target="body"
hx-swap="beforeend"> hx-swap="beforeend">
absagen absagen
@@ -232,7 +235,7 @@
class="text-red-500 hover:text-primary block px-4 py-2 text-sm w-full text-left" class="text-red-500 hover:text-primary block px-4 py-2 text-sm w-full text-left"
role="menuitem" role="menuitem"
tabindex="-1" tabindex="-1"
hx-get="{{ path('app_admin_disposition_delete', { 'uuid': disposition.uuid }) }}" hx-get="{{ path('app_admin_disposition_delete', { 'uuid': disposition.uuid, 'r': forwarded_return_url() }) }}"
hx-target="body" hx-target="body"
hx-swap="beforeend"> hx-swap="beforeend">
löschen löschen
@@ -146,7 +146,7 @@
<div role="button" <div role="button"
title="Teamer:innen-Info anzeigen" title="Teamer:innen-Info anzeigen"
aria-label="Teamer:innen-Info anzeigen" aria-label="Teamer:innen-Info anzeigen"
hx-get="{{ path('app_administrative_teamer_info', { 'uuid': disposition.teamer.uuid }) }}" hx-get="{{ path('app_administrative_teamer_info', { 'uuid': disposition.teamer.uuid, 'r': return_url() }) }}"
hx-target="body" hx-target="body"
hx-swap="beforeend"> hx-swap="beforeend">
<span class="whitespace-nowrap">{{ disposition.teamer }}</span> <span class="whitespace-nowrap">{{ disposition.teamer }}</span>
@@ -197,7 +197,7 @@
class="block px-4 py-2 text-sm w-full text-left" class="block px-4 py-2 text-sm w-full text-left"
role="menuitem" role="menuitem"
tabindex="-1" tabindex="-1"
hx-post="{{ path('app_administrative_assignment_publish', { 'uuid': assignment.uuid }) }}"> hx-post="{{ path('app_administrative_assignment_publish', { 'uuid': assignment.uuid, 'r': return_url() }) }}">
veröffentlichen veröffentlichen
</button> </button>
{% endif %} {% endif %}
@@ -248,7 +248,7 @@
</div> </div>
</div> </div>
<a href="{{ path('app_administrative_assignment_create') }}" class="btn"> <a href="{{ path('app_administrative_assignment_create', { 'r': return_url() }) }}" class="btn">
Neu Neu
</a> </a>
{% endblock %} {% endblock %}
@@ -23,7 +23,7 @@
<button type="button" <button type="button"
class="flex items-center space-x-2" class="flex items-center space-x-2"
title="Teamer:inneninfo {{ item.teamerName }}" title="Teamer:inneninfo {{ item.teamerName }}"
hx-get="{{ path('app_administrative_teamer_info', { 'uuid': item.teamerUuid }) }}" hx-get="{{ path('app_administrative_teamer_info', { 'uuid': item.teamerUuid, 'r': return_url() }) }}"
hx-target="body" hx-target="body"
hx-swap="beforeend"> hx-swap="beforeend">
<span class="text-xs font-semibold">{{ item.teamerName | raw }}</span> <span class="text-xs font-semibold">{{ item.teamerName | raw }}</span>
@@ -101,7 +101,7 @@
{% endif %} {% endif %}
{% if is_granted('CHECK', upload) %} {% if is_granted('CHECK', upload) %}
<button type="button" <button type="button"
hx-get="{{ path('app_administrative_document_check', { 'uuid': upload.uuid }) }}" hx-get="{{ path('app_administrative_document_check', { 'uuid': upload.uuid, 'r': return_url() }) }}"
hx-target="body" hx-target="body"
hx-swap="beforeend"> hx-swap="beforeend">
{{ icon('edit') }} {{ icon('edit') }}
@@ -1,7 +1,13 @@
{% extends 'htmx_modal.html.twig' %} {% extends 'htmx_modal.html.twig' %}
{% block title %} {% block title %}
Teamer:inneninfo <a href="{{ path('app_administrative_teamer_profile', { 'uuid': teamer.uuid, 'r': returnUrl }) }}">{{ teamer }}</a> {# The profile is administrative only, house managers reach this modal but not it. #}
Teamer:inneninfo
{% if is_granted('ROLE_ADMINISTRATIVE') %}
<a href="{{ path('app_administrative_teamer_profile', { 'uuid': teamer.uuid, 'r': returnUrl }) }}">{{ teamer }}</a>
{% else %}
{{ teamer }}
{% endif %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
+1 -1
View File
@@ -6,7 +6,7 @@
<button type="button" <button type="button"
class="flex items-center space-x-1" class="flex items-center space-x-1"
title="Teamer:inneninfo {{ disposition.teamer }}" title="Teamer:inneninfo {{ disposition.teamer }}"
hx-get="{{ path('app_administrative_teamer_info', { 'uuid': disposition.teamer.uuid }) }}" hx-get="{{ path('app_administrative_teamer_info', { 'uuid': disposition.teamer.uuid, 'r': return_url() }) }}"
hx-target="body" hx-target="body"
hx-swap="beforeend"> hx-swap="beforeend">
{{ icon('info', 'w-4 h-4') }} {{ icon('info', 'w-4 h-4') }}
+1 -1
View File
@@ -75,7 +75,7 @@
{% for disposition in dispositions %} {% for disposition in dispositions %}
{% set assignment = disposition.assignment %} {% set assignment = disposition.assignment %}
<li class="py-2 first:pt-0 last:pb-0 overflow-x-hidden"> <li class="py-2 first:pt-0 last:pb-0 overflow-x-hidden">
<a href="{{ path('app_administrative_assignment_detail', { 'uuid': assignment.uuid }) }}" <a href="{{ path('app_administrative_assignment_detail', { 'uuid': assignment.uuid, 'r': return_url() }) }}"
class="flex items-center space-x-1"> class="flex items-center space-x-1">
{{ icon('hand', 'w-4 h-4 shrink-0') }} {{ icon('hand', 'w-4 h-4 shrink-0') }}
{% include '_partials/_disposition_data.html.twig' with { 'disposition': disposition } %} {% include '_partials/_disposition_data.html.twig' with { 'disposition': disposition } %}
+1 -1
View File
@@ -34,7 +34,7 @@
<button type="button" <button type="button"
class="btn btn--secondary" class="btn btn--secondary"
title="Bewerbung zurückziehen" title="Bewerbung zurückziehen"
hx-get="{{ path('app_teamer_application_withdraw', { 'uuid': application.uuid }) }}" hx-get="{{ path('app_teamer_application_withdraw', { 'uuid': application.uuid, 'r': forwarded_return_url() }) }}"
hx-target="body" hx-target="body"
hx-swap="beforeend"> hx-swap="beforeend">
Bewerbung zurückziehen Bewerbung zurückziehen
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Tests\Controller\Traits;
use App\Controller\Traits\ReturnUrlTrait;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
class ReturnUrlTraitTest extends TestCase
{
public const DEFAULT_ROUTE = 'app_administrative_teamer_index';
public const DEFAULT_URL = '/administrative/teamer';
private object $controller;
protected function setUp(): void
{
$this->controller = new class {
use ReturnUrlTrait;
/**
* @param array<string, mixed> $parameters
*/
public function generateUrl(string $route, array $parameters = []): string
{
return ReturnUrlTraitTest::DEFAULT_URL.([] === $parameters ? '' : '?'.http_build_query($parameters));
}
};
}
public function testTheReturnUrlIsDecoded(): void
{
$request = Request::create('/somewhere?r='.rawurlencode('/administrative/teamer?page=3&sort=name'));
self::assertSame(
'/administrative/teamer?page=3&sort=name',
$this->controller->getReturnUrl($request, self::DEFAULT_ROUTE)
);
}
public function testAMissingParameterFallsBackToTheDefaultRoute(): void
{
$request = Request::create('/somewhere');
self::assertSame(self::DEFAULT_URL, $this->controller->getReturnUrl($request, self::DEFAULT_ROUTE));
}
/**
* A link forwarding a return url it never received itself emits "r=", which must
* not be read as a return url pointing at the root.
*/
public function testAnEmptyParameterFallsBackToTheDefaultRoute(): void
{
$request = Request::create('/somewhere?r=');
self::assertSame(self::DEFAULT_URL, $this->controller->getReturnUrl($request, self::DEFAULT_ROUTE));
}
public function testTheDefaultRouteKeepsItsParameters(): void
{
$request = Request::create('/somewhere');
self::assertSame(
self::DEFAULT_URL.'?uuid=abc',
$this->controller->getReturnUrl($request, self::DEFAULT_ROUTE, ['uuid' => 'abc'])
);
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Tests\Twig;
use App\Twig\AppRuntime;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
class ForwardedReturnUrlTest extends KernelTestCase
{
private RequestStack $requestStack;
private AppRuntime $appRuntime;
protected function setUp(): void
{
self::bootKernel();
$this->requestStack = self::getContainer()->get(RequestStack::class);
$this->appRuntime = self::getContainer()->get(AppRuntime::class);
}
public function testTheReturnUrlIsPassedOnUntouched(): void
{
$encoded = rawurlencode('/administrative/assignment?page=3');
$this->requestStack->push(Request::create('/administrative/assignment/detail/abc?r='.$encoded));
// The raw query value, handed straight back to the url generator, which
// re-encodes it. No decoding of its own, that stays the consumer's job.
self::assertSame('/administrative/assignment?page=3', $this->appRuntime->getForwardedReturnUrl());
}
/**
* Null rather than an empty string, so the url generator drops the parameter
* instead of emitting "r=".
*/
public function testNothingIsPassedOnWithoutAReturnUrl(): void
{
$this->requestStack->push(Request::create('/administrative/assignment/detail/abc'));
self::assertNull($this->appRuntime->getForwardedReturnUrl());
}
public function testNothingIsPassedOnForAnEmptyReturnUrl(): void
{
$this->requestStack->push(Request::create('/administrative/assignment/detail/abc?r='));
self::assertNull($this->appRuntime->getForwardedReturnUrl());
}
}