From f554a751f372faad9e80b5124d85eb3fbe7c22bb Mon Sep 17 00:00:00 2001 From: Timon Heuser Date: Wed, 22 Jan 2025 15:51:24 +0100 Subject: [PATCH] TASK: render links to nodes with trailing slash --- Classes/Helper/BlocklistHelper.php | 33 ++++ Classes/Helper/ConfigurationHelper.php | 42 ++++ Classes/Helper/LowerCaseHelper.php | 28 +++ Classes/Helper/TrailingSlashHelper.php | 32 ++++ Classes/LinkingServiceAspect.php | 53 ++++++ Classes/RoutingMiddleware.php | 81 ++------ README.md | 11 +- Tests/Unit/Helper/BlocklistHelperTest.php | 48 +++++ Tests/Unit/Helper/ConfigurationHelperTest.php | 86 +++++++++ Tests/Unit/Helper/LowerCaseHelperTest.php | 50 +++++ Tests/Unit/Helper/TrailingSlashHelperTest.php | 50 +++++ Tests/Unit/LinkingServiceAspectTest.php | 112 +++++++++++ Tests/Unit/RoutingMiddlewareTest.php | 180 ++++++------------ composer.json | 4 +- 14 files changed, 623 insertions(+), 187 deletions(-) create mode 100644 Classes/Helper/BlocklistHelper.php create mode 100644 Classes/Helper/ConfigurationHelper.php create mode 100644 Classes/Helper/LowerCaseHelper.php create mode 100644 Classes/Helper/TrailingSlashHelper.php create mode 100644 Classes/LinkingServiceAspect.php create mode 100644 Tests/Unit/Helper/BlocklistHelperTest.php create mode 100644 Tests/Unit/Helper/ConfigurationHelperTest.php create mode 100644 Tests/Unit/Helper/LowerCaseHelperTest.php create mode 100644 Tests/Unit/Helper/TrailingSlashHelperTest.php create mode 100644 Tests/Unit/LinkingServiceAspectTest.php diff --git a/Classes/Helper/BlocklistHelper.php b/Classes/Helper/BlocklistHelper.php new file mode 100644 index 0000000..a339841 --- /dev/null +++ b/Classes/Helper/BlocklistHelper.php @@ -0,0 +1,33 @@ +getPath(); + foreach ($this->configurationHelper->getBlocklist() as $rawPattern => $active) { + $pattern = '/' . str_replace('/', '\/', $rawPattern) . '/'; + + if (! $active) { + continue; + } + + if (preg_match($pattern, $path) === 1) { + return true; + } + } + + return false; + } +} diff --git a/Classes/Helper/ConfigurationHelper.php b/Classes/Helper/ConfigurationHelper.php new file mode 100644 index 0000000..6759f55 --- /dev/null +++ b/Classes/Helper/ConfigurationHelper.php @@ -0,0 +1,42 @@ +configuration['enable']['trailingSlash'] ?? false; + } + + public function isToLowerCaseEnabled(): bool + { + return $this->configuration['enable']['toLowerCase'] ?? false; + } + + public function getStatusCode(): int + { + return $this->configuration['statusCode'] ?? 301; + } + + /** + * @return array{string: bool} + */ + public function getBlocklist(): array + { + return $this->blocklist; + } +} diff --git a/Classes/Helper/LowerCaseHelper.php b/Classes/Helper/LowerCaseHelper.php new file mode 100644 index 0000000..1ba0e1f --- /dev/null +++ b/Classes/Helper/LowerCaseHelper.php @@ -0,0 +1,28 @@ +getPath()); + + if ($uri->getPath() === $loweredPath) { + return $uri; + } + + // bypass links to files + if (array_key_exists('extension', pathinfo($uri->getPath()))) { + return $uri; + } + + return $uri->withPath($loweredPath); + } +} diff --git a/Classes/Helper/TrailingSlashHelper.php b/Classes/Helper/TrailingSlashHelper.php new file mode 100644 index 0000000..d7b5ac1 --- /dev/null +++ b/Classes/Helper/TrailingSlashHelper.php @@ -0,0 +1,32 @@ +getPath()) === 0) { + return $uri; + } + + // bypass links to files + if (array_key_exists('extension', pathinfo($uri->getPath()))) { + return $uri; + } + + // bypass mailto and tel links + if (in_array($uri->getScheme(), ['mailto', 'tel'], true)) { + return $uri; + } + + return $uri->withPath(rtrim($uri->getPath(), '/') . '/'); + } +} diff --git a/Classes/LinkingServiceAspect.php b/Classes/LinkingServiceAspect.php new file mode 100644 index 0000000..198168e --- /dev/null +++ b/Classes/LinkingServiceAspect.php @@ -0,0 +1,53 @@ +createNodeUri())')] + public function appendTrailingSlashToNodeUri(JoinPointInterface $joinPoint): string + { + /** @var string $result */ + $result = $joinPoint->getAdviceChain()->proceed($joinPoint); + + if (! $this->configurationHelper->isTrailingSlashEnabled()) { + return $result; + } + + try { + $uri = new Uri($result); + } catch (MalformedUriException) { + return $result; + } + + if ($this->blocklistHelper->isUriInBlocklist($uri)) { + return $result; + } + + return (string)$this->trailingSlashHelper->appendTrailingSlash($uri); + } +} diff --git a/Classes/RoutingMiddleware.php b/Classes/RoutingMiddleware.php index b82baa3..6714ce4 100644 --- a/Classes/RoutingMiddleware.php +++ b/Classes/RoutingMiddleware.php @@ -4,35 +4,38 @@ namespace Flowpack\SeoRouting; +use Flowpack\SeoRouting\Helper\BlocklistHelper; +use Flowpack\SeoRouting\Helper\ConfigurationHelper; +use Flowpack\SeoRouting\Helper\LowerCaseHelper; +use Flowpack\SeoRouting\Helper\TrailingSlashHelper; use Neos\Flow\Annotations as Flow; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Message\UriFactoryInterface; -use Psr\Http\Message\UriInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -final class RoutingMiddleware implements MiddlewareInterface +class RoutingMiddleware implements MiddlewareInterface { #[Flow\Inject] protected ResponseFactoryInterface $responseFactory; #[Flow\Inject] - protected UriFactoryInterface $uriFactory; + protected ConfigurationHelper $configurationHelper; - /** @var array{enable: array{trailingSlash: bool, toLowerCase: bool}, statusCode?: int} */ - #[Flow\InjectConfiguration(path: 'redirect')] - protected array $configuration; + #[Flow\Inject] + protected BlocklistHelper $blocklistHelper; + + #[Flow\Inject] + protected TrailingSlashHelper $trailingSlashHelper; - /** @var array{string: bool} */ - #[Flow\InjectConfiguration(path: 'blocklist')] - protected array $blocklist; + #[Flow\Inject] + protected LowerCaseHelper $lowerCaseHelper; public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $isTrailingSlashEnabled = $this->configuration['enable']['trailingSlash'] ?? false; - $isToLowerCaseEnabled = $this->configuration['enable']['toLowerCase'] ?? false; + $isTrailingSlashEnabled = $this->configurationHelper->isTrailingSlashEnabled(); + $isToLowerCaseEnabled = $this->configurationHelper->isToLowerCaseEnabled(); $uri = $request->getUri(); @@ -40,72 +43,26 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface return $handler->handle($request); } - if ($this->matchesBlocklist($uri)) { + if ($this->blocklistHelper->isUriInBlocklist($uri)) { return $handler->handle($request); } $oldPath = $uri->getPath(); if ($isTrailingSlashEnabled) { - $uri = $this->handleTrailingSlash($uri); + $uri = $this->trailingSlashHelper->appendTrailingSlash($uri); } if ($isToLowerCaseEnabled) { - $uri = $this->handleToLowerCase($uri); + $uri = $this->lowerCaseHelper->convertPathToLowerCase($uri); } if ($uri->getPath() === $oldPath) { return $handler->handle($request); } - $response = $this->responseFactory->createResponse($this->configuration['statusCode'] ?? 301); + $response = $this->responseFactory->createResponse($this->configurationHelper->getStatusCode()); return $response->withAddedHeader('Location', (string)$uri); } - - private function handleTrailingSlash(UriInterface $uri): UriInterface - { - if (strlen($uri->getPath()) === 0) { - return $uri; - } - - if (array_key_exists('extension', pathinfo($uri->getPath()))) { - return $uri; - } - - return $uri->withPath(rtrim($uri->getPath(), '/') . '/') - ->withQuery($uri->getQuery()) - ->withFragment($uri->getFragment()); - } - - private function handleToLowerCase(UriInterface $uri): UriInterface - { - $loweredPath = strtolower($uri->getPath()); - - if ($uri->getPath() === $loweredPath) { - return $uri; - } - - $newUri = str_replace($uri->getPath(), $loweredPath, (string)$uri); - - return $this->uriFactory->createUri($newUri); - } - - private function matchesBlocklist(UriInterface $uri): bool - { - $path = $uri->getPath(); - foreach ($this->blocklist as $rawPattern => $active) { - $pattern = '/' . str_replace('/', '\/', $rawPattern) . '/'; - - if (! $active) { - continue; - } - - if (preg_match($pattern, $path) === 1) { - return true; - } - } - - return false; - } } diff --git a/README.md b/README.md index 6ac8829..cdf023d 100644 --- a/README.md +++ b/README.md @@ -26,14 +26,15 @@ This package allows you to enforce a trailing slash and/or lower case urls in Fl This package has 2 main features: -- **trailingSlash**: ensure that all links ends with a trailing slash (e.g. `example.com/test/` instead of - `example.com/test`) +- **trailingSlash**: ensure that all rendered internal links in the frontend end with a trailing slash (e.g. `example. +com/test/` instead of `example.com/test`) and all called URLs without trailing slash will be redirected to the same + page with a trailing slash - **toLowerCase**: ensure that camelCase links gets redirected to lowercase (e.g. `example.com/lowercase` instead of `example.com/lowerCase`) You can de- and activate both of them. -Another small feature is to restrict all _new_ neos pages to have a lowercased `uriPathSegment`. This is done by +Another small feature is to restrict all _new_ Neos pages to have a lowercased `uriPathSegment`. This is done by extending the `NodeTypes.Document.yaml`. ## Installation @@ -50,8 +51,8 @@ In the standard configuration we have activated the trailingSlash (to redirect a with / at the end) and do all redirects with a 301 http status. *Note: The lowercase redirect is deactivated by default, because you have to make sure, that there is -no `uriPathSegment` -with camelCase or upperspace letters - this would lead to redirects in the neverland.* +no Neos page with an `uriPathSegment` with camelCase or upperspace letters - this would lead to redirects in the +neverland.* ``` Flowpack: diff --git a/Tests/Unit/Helper/BlocklistHelperTest.php b/Tests/Unit/Helper/BlocklistHelperTest.php new file mode 100644 index 0000000..cff0fe9 --- /dev/null +++ b/Tests/Unit/Helper/BlocklistHelperTest.php @@ -0,0 +1,48 @@ +createMock(ConfigurationHelper::class); + + $configurationHelperMock->expects($this->once())->method('getBlocklist')->willReturn( + ['/neos.*' => false, '.*test.*' => true] + ); + + $reflection = new ReflectionClass($blocklistHelper); + $property = $reflection->getProperty('configurationHelper'); + $property->setValue($blocklistHelper, $configurationHelperMock); + + $uri = new Uri($input); + + self::assertSame($expected, $blocklistHelper->isUriInBlocklist($uri)); + } + + /** + * @return array{array{string, bool}} + */ + public static function urlDataProvider(): array + { + return [ + ['https://test.de/neos', false], + ['https://test.de/neos/test', true], + ['https://neos.de/foo', false], + ]; + } +} diff --git a/Tests/Unit/Helper/ConfigurationHelperTest.php b/Tests/Unit/Helper/ConfigurationHelperTest.php new file mode 100644 index 0000000..1d741b1 --- /dev/null +++ b/Tests/Unit/Helper/ConfigurationHelperTest.php @@ -0,0 +1,86 @@ + */ + private readonly ReflectionClass $configurationHelperReflection; + + protected function setUp(): void + { + parent::setUp(); + + $this->configurationHelper = new ConfigurationHelper(); + $this->configurationHelperReflection = new ReflectionClass($this->configurationHelper); + } + + public function testIsTrailingSlashEnabledShouldReturnTrue(): void + { + $this->injectConfiguration(['enable' => ['trailingSlash' => true, 'toLowerCase' => false]]); + + self::assertTrue($this->configurationHelper->isTrailingSlashEnabled()); + } + + public function testIsTrailingSlashEnabledShouldReturnFalse(): void + { + $this->injectConfiguration(['enable' => ['trailingSlash' => false, 'toLowerCase' => true]]); + + self::assertFalse($this->configurationHelper->isTrailingSlashEnabled()); + } + + public function testIsToLowerCaseEnabledShouldReturnTrue(): void + { + $this->injectConfiguration(['enable' => ['trailingSlash' => false, 'toLowerCase' => true]]); + + self::assertTrue($this->configurationHelper->isToLowerCaseEnabled()); + } + + public function testIsToLowerCaseEnabledShouldReturnFalse(): void + { + $this->injectConfiguration(['enable' => ['trailingSlash' => true, 'toLowerCase' => false]]); + + self::assertFalse($this->configurationHelper->isToLowerCaseEnabled()); + } + + public function testGetBlocklist(): void + { + $property = $this->configurationHelperReflection->getProperty('blocklist'); + $property->setValue($this->configurationHelper, ['/neos.*' => false,]); + + self::assertEquals(['/neos.*' => false,], $this->configurationHelper->getBlocklist()); + } + + public function testGetStatusCodeShouldReturnDefaultValue(): void + { + $this->injectConfiguration([]); + + self::assertSame(301, $this->configurationHelper->getStatusCode()); + } + + public function testGetStatusCodeShouldReturnConfiguredValue(): void + { + $this->injectConfiguration(['enable' => ['trailingSlash' => true, 'toLowerCase' => false], 'statusCode' => 302] + ); + + self::assertSame(302, $this->configurationHelper->getStatusCode()); + } + + /** + * @param array{enable: array{trailingSlash: bool, toLowerCase: bool}, statusCode?: int}|array{} $configuration + */ + private function injectConfiguration(array $configuration): void + { + $property = $this->configurationHelperReflection->getProperty('configuration'); + $property->setValue($this->configurationHelper, $configuration); + } +} diff --git a/Tests/Unit/Helper/LowerCaseHelperTest.php b/Tests/Unit/Helper/LowerCaseHelperTest.php new file mode 100644 index 0000000..fa69440 --- /dev/null +++ b/Tests/Unit/Helper/LowerCaseHelperTest.php @@ -0,0 +1,50 @@ +convertPathToLowerCase($uri)); + } + + /** + * @return array{string[]} + */ + public static function urlDataProvider(): array + { + return [ + ['', ''], + ['/', '/'], + ['/foo', '/foo'], + ['/foo/bar', '/foo/bar'], + ['https://test.de', 'https://test.de'], + ['https://test.de/', 'https://test.de/'], + ['https://test.de/FOO/bar', 'https://test.de/foo/bar'], + ['https://test.de/foo/bar/', 'https://test.de/foo/bar/'], + ['/foo/bar?some-QUERY=foo%20bar', '/foo/bar?some-QUERY=foo%20bar'], + ['/foo/bar#some-fragment', '/foo/bar#some-fragment'], + ['/foo/bar?some-query=foo%20bar#some-FRAGMENT', '/foo/bar?some-query=foo%20bar#some-FRAGMENT'], + [ + 'https://test.de/FOO/bar?some-query=foo%20bar#SOME-fragment', + 'https://test.de/foo/bar?some-query=foo%20bar#SOME-fragment', + ], + ['mailto:some.email@FOO.bar', 'mailto:some.email@FOO.bar'], + ['tel:+4906516564', 'tel:+4906516564'], + ['https://test.de/foo/BAR.css', 'https://test.de/foo/BAR.css'], + ]; + } +} diff --git a/Tests/Unit/Helper/TrailingSlashHelperTest.php b/Tests/Unit/Helper/TrailingSlashHelperTest.php new file mode 100644 index 0000000..1c7b019 --- /dev/null +++ b/Tests/Unit/Helper/TrailingSlashHelperTest.php @@ -0,0 +1,50 @@ +appendTrailingSlash($uri)); + } + + /** + * @return array{string[]} + */ + public static function urlDataProvider(): array + { + return [ + ['', ''], + ['/', '/'], + ['/foo', '/foo/'], + ['/foo/bar', '/foo/bar/'], + ['https://test.de', 'https://test.de'], + ['https://test.de/', 'https://test.de/'], + ['https://test.de/foo/bar', 'https://test.de/foo/bar/'], + ['https://test.de/foo/bar/', 'https://test.de/foo/bar/'], + ['/foo/bar?some-query=foo%20bar', '/foo/bar/?some-query=foo%20bar'], + ['/foo/bar#some-fragment', '/foo/bar/#some-fragment'], + ['/foo/bar?some-query=foo%20bar#some-fragment', '/foo/bar/?some-query=foo%20bar#some-fragment'], + [ + 'https://test.de/foo/bar?some-query=foo%20bar#some-fragment', + 'https://test.de/foo/bar/?some-query=foo%20bar#some-fragment', + ], + ['mailto:some.email@foo.bar', 'mailto:some.email@foo.bar'], + ['tel:+4906516564', 'tel:+4906516564'], + ['https://test.de/foo/bar.css', 'https://test.de/foo/bar.css'], + ]; + } +} diff --git a/Tests/Unit/LinkingServiceAspectTest.php b/Tests/Unit/LinkingServiceAspectTest.php new file mode 100644 index 0000000..4f1dee3 --- /dev/null +++ b/Tests/Unit/LinkingServiceAspectTest.php @@ -0,0 +1,112 @@ +linkingServiceAspect = new LinkingServiceAspect(); + + $this->joinPointMock = $this->createMock(JoinPointInterface::class); + $this->adviceChainMock = $this->createMock(AdviceChain::class); + $this->trailingSlashHelperMock = $this->createMock(TrailingSlashHelper::class); + $this->configurationHelperMock = $this->createMock(ConfigurationHelper::class); + $this->blocklistHelperMock = $this->createMock(BlocklistHelper::class); + + $reflection = new ReflectionClass($this->linkingServiceAspect); + $reflectionProperty = $reflection->getProperty('trailingSlashHelper'); + $reflectionProperty->setValue($this->linkingServiceAspect, $this->trailingSlashHelperMock); + $reflectionProperty = $reflection->getProperty('configurationHelper'); + $reflectionProperty->setValue($this->linkingServiceAspect, $this->configurationHelperMock); + $reflectionProperty = $reflection->getProperty('blocklistHelper'); + $reflectionProperty->setValue($this->linkingServiceAspect, $this->blocklistHelperMock); + + $this->joinPointMock->expects($this->once())->method('getAdviceChain')->willReturn($this->adviceChainMock); + } + + public function testAppendTrailingSlashToNodeUriShouldNotChangeResultIfTrailingSlashIsDisabled(): void + { + $result = 'foo'; + + $this->configurationHelperMock->expects($this->once())->method('isTrailingSlashEnabled')->willReturn(false); + + $this->adviceChainMock->expects($this->once())->method('proceed')->willReturn($result); + + assertSame( + $result, + $this->linkingServiceAspect->appendTrailingSlashToNodeUri($this->joinPointMock) + ); + } + + public function testAppendTrailingSlashToNodeUriShouldNotChangeResultIfUriIsMalformed(): void + { + $result = 'https://'; + $this->adviceChainMock->expects($this->once())->method('proceed')->willReturn($result); + + $this->configurationHelperMock->expects($this->once())->method('isTrailingSlashEnabled')->willReturn(true); + + assertSame( + $result, + $this->linkingServiceAspect->appendTrailingSlashToNodeUri($this->joinPointMock) + ); + } + + public function testAppendTrailingSlashToNodeUriShouldNotChangeResultIfUriIsInBlocklist(): void + { + $result = 'foo'; + $this->adviceChainMock->expects($this->once())->method('proceed')->willReturn($result); + + $this->configurationHelperMock->expects($this->once())->method('isTrailingSlashEnabled')->willReturn(true); + $this->blocklistHelperMock->expects($this->once())->method('isUriInBlocklist')->willReturn(true); + + + assertSame( + $result, + $this->linkingServiceAspect->appendTrailingSlashToNodeUri($this->joinPointMock) + ); + } + + public function testAppendTrailingSlashToNodeUriShouldChangeResult(): void + { + $result = 'foo/'; + $this->adviceChainMock->expects($this->once())->method('proceed')->willReturn('foo'); + + $this->configurationHelperMock->expects($this->once())->method('isTrailingSlashEnabled')->willReturn(true); + $this->blocklistHelperMock->expects($this->once())->method('isUriInBlocklist')->willReturn(false); + $this->trailingSlashHelperMock->expects($this->once())->method('appendTrailingSlash')->willReturn( + new Uri($result) + ); + + assertSame( + $result, + $this->linkingServiceAspect->appendTrailingSlashToNodeUri($this->joinPointMock) + ); + } +} diff --git a/Tests/Unit/RoutingMiddlewareTest.php b/Tests/Unit/RoutingMiddlewareTest.php index 88847cb..8281926 100644 --- a/Tests/Unit/RoutingMiddlewareTest.php +++ b/Tests/Unit/RoutingMiddlewareTest.php @@ -4,6 +4,10 @@ namespace Flowpack\SeoRouting\Tests\Unit; +use Flowpack\SeoRouting\Helper\BlocklistHelper; +use Flowpack\SeoRouting\Helper\ConfigurationHelper; +use Flowpack\SeoRouting\Helper\LowerCaseHelper; +use Flowpack\SeoRouting\Helper\TrailingSlashHelper; use Flowpack\SeoRouting\RoutingMiddleware; use GuzzleHttp\Psr7\Uri; use PHPUnit\Framework\Attributes\CoversClass; @@ -14,26 +18,24 @@ use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Message\UriFactoryInterface; use Psr\Http\Server\RequestHandlerInterface; use ReflectionClass; -use ReflectionException; #[CoversClass(RoutingMiddleware::class)] class RoutingMiddlewareTest extends TestCase { private readonly RoutingMiddleware $routingMiddleware; - /** @var ReflectionClass */ - private readonly ReflectionClass $routingMiddlewareReflection; private readonly ResponseFactoryInterface&MockObject $responseFactoryMock; private readonly ResponseInterface&MockObject $responseMock; - private readonly UriFactoryInterface&MockObject $uriFactoryMock; private readonly ServerRequestInterface&MockObject $requestMock; private readonly RequestHandlerInterface&MockObject $requestHandlerMock; + private readonly ConfigurationHelper&MockObject $configurationHelperMock; + private readonly BlocklistHelper&MockObject $blocklistHelperMock; + private readonly TrailingSlashHelper&MockObject $trailingSlashHelperMock; + private readonly LowerCaseHelper&MockObject $lowerCaseHelperMock; /** * @throws Exception - * @throws ReflectionException */ protected function setUp(): void { @@ -43,45 +45,52 @@ protected function setUp(): void $this->responseFactoryMock = $this->createMock(ResponseFactoryInterface::class); $this->responseMock = $this->createMock(ResponseInterface::class); - $this->uriFactoryMock = $this->createMock(UriFactoryInterface::class); $this->requestMock = $this->createMock(ServerRequestInterface::class); $this->requestHandlerMock = $this->createMock(RequestHandlerInterface::class); + $this->configurationHelperMock = $this->createMock(ConfigurationHelper::class); + $this->blocklistHelperMock = $this->createMock(BlocklistHelper::class); + $this->trailingSlashHelperMock = $this->createMock(TrailingSlashHelper::class); + $this->lowerCaseHelperMock = $this->createMock(LowerCaseHelper::class); - $this->routingMiddlewareReflection = new ReflectionClass($this->routingMiddleware); + $routingMiddlewareReflection = new ReflectionClass($this->routingMiddleware); - $property = $this->routingMiddlewareReflection->getProperty('responseFactory'); + $property = $routingMiddlewareReflection->getProperty('responseFactory'); $property->setValue($this->routingMiddleware, $this->responseFactoryMock); - $property = $this->routingMiddlewareReflection->getProperty('uriFactory'); - $property->setValue($this->routingMiddleware, $this->uriFactoryMock); + $property = $routingMiddlewareReflection->getProperty('configurationHelper'); + $property->setValue($this->routingMiddleware, $this->configurationHelperMock); - $this->injectBlocklist([]); + $property = $routingMiddlewareReflection->getProperty('blocklistHelper'); + $property->setValue($this->routingMiddleware, $this->blocklistHelperMock); + + $property = $routingMiddlewareReflection->getProperty('trailingSlashHelper'); + $property->setValue($this->routingMiddleware, $this->trailingSlashHelperMock); + + $property = $routingMiddlewareReflection->getProperty('lowerCaseHelper'); + $property->setValue($this->routingMiddleware, $this->lowerCaseHelperMock); } - /** - * @param array{enable: array{trailingSlash: bool, toLowerCase: bool}, statusCode?: int} $configuration - * @param array{string: bool} $blocklist - */ - #[DataProvider('urlsWithConfigAndBlocklist')] + + #[DataProvider('urlsDataProvider')] public function testProcessShouldHandleUrlsCorrectly( string $originalUrl, string $expectedUrl, - array $configuration, - array $blocklist, + bool $isTrailingSlashEnabledResult, + bool $isToLowerCaseEnabledResult, + bool $isUriInBlocklistResult, + int $statusCode ): void { - $this->injectConfiguration($configuration); - $this->injectBlocklist($blocklist); - - /* - * We're not using a Mock here because it wouldn't make sense and we couldn't really test whether the code - * works, the test would just consist of a lot of expect assertions. - */ $originalUri = new Uri($originalUrl); $expectedUri = new Uri($expectedUrl); + $this->configurationHelperMock->method('isTrailingSlashEnabled')->willReturn($isTrailingSlashEnabledResult); + $this->configurationHelperMock->method('isToLowerCaseEnabled')->willReturn($isToLowerCaseEnabledResult); + $this->blocklistHelperMock->method('isUriInBlocklist')->willReturn($isUriInBlocklistResult); + $this->trailingSlashHelperMock->method('appendTrailingSlash')->willReturn($expectedUri); + $this->lowerCaseHelperMock->method('convertPathToLowerCase')->willReturn($expectedUri); + $this->configurationHelperMock->method('getStatusCode')->willReturn($statusCode); $this->requestMock->expects($this->once())->method('getUri')->willReturn($originalUri); - $this->uriFactoryMock->method('createUri')->willReturn($expectedUri); if ($originalUrl === $expectedUrl) { $this->requestHandlerMock->method('handle')->willReturn($this->responseMock); @@ -89,7 +98,7 @@ public function testProcessShouldHandleUrlsCorrectly( $this->responseFactoryMock ->expects($this->once()) ->method('createResponse') - ->with($configuration['statusCode'] ?? 301) + ->with($statusCode) ->willReturn($this->responseMock); $this->responseMock @@ -105,108 +114,43 @@ public function testProcessShouldHandleUrlsCorrectly( ); } - /** - * @param array{enable: array{trailingSlash: bool, toLowerCase: bool}, statusCode?: int} $configuration - */ - private function injectConfiguration(array $configuration): void - { - $property = $this->routingMiddlewareReflection->getProperty('configuration'); - $property->setValue($this->routingMiddleware, $configuration); - } - - /** - * @param array{string: bool}|array{} $blocklist - */ - private function injectBlocklist(array $blocklist): void - { - $property = $this->routingMiddlewareReflection->getProperty('blocklist'); - $property->setValue($this->routingMiddleware, $blocklist); - } - /** * @return mixed[] */ - public static function urlsWithConfigAndBlocklist(): array + public static function urlsDataProvider(): array { - /* - * originalUrl, expectedUrl, configuration, blocklist - */ return [ [ - 'https://local.dev', - 'https://local.dev', - ['enable' => ['trailingSlash' => false, 'toLowerCase' => false]], - [], - ], - [ - 'https://local.dev', - 'https://local.dev', - ['enable' => ['trailingSlash' => true, 'toLowerCase' => false], 'statusCode' => 302], - [], - ], - [ - 'https://local.dev/test/test2', - 'https://local.dev/test/test2/', - ['enable' => ['trailingSlash' => true, 'toLowerCase' => false,],], - [], - ], - [ - 'https://local.dev/public/css/main.css', - 'https://local.dev/public/css/main.css', - ['enable' => ['trailingSlash' => true, 'toLowerCase' => false,],], - [], - ], - [ - 'https://local.dev/test?foo=bar&bar=baz', - 'https://local.dev/test/?foo=bar&bar=baz', - ['enable' => ['trailingSlash' => true, 'toLowerCase' => false,],], - [], - ], - [ - 'https://local.dev/test/', - 'https://local.dev/test/', - ['enable' => ['trailingSlash' => true, 'toLowerCase' => false,],], - [], - ], - [ - 'https://local.dev/test/test2?foo=bar&bar=baz#hash', - 'https://local.dev/test/test2/?foo=bar&bar=baz#hash', - ['enable' => ['trailingSlash' => true, 'toLowerCase' => false,],], - [], - ], - [ - 'https://local.dev/camelCase/?foo=bar&bar=baz#hash', - 'https://local.dev/camelcase/?foo=bar&bar=baz#hash', - ['enable' => ['trailingSlash' => false, 'toLowerCase' => true]], - [], - ], - [ - 'https://local.dev/nocaps', - 'https://local.dev/nocaps', - ['enable' => ['trailingSlash' => false, 'toLowerCase' => true]], - [], + 'originalUrl' => 'https://local.dev', + 'expectedUrl' => 'https://local.dev', + 'isTrailingSlashEnabledResult' => false, + 'isToLowerCaseEnabledResult' => false, + 'isUriInBlocklistResult' => false, + 'statusCode' => 301, ], [ - 'https://local.dev/CAPSLOCK?foo=bar&bar=baz#hash', - 'https://local.dev/capslock/?foo=bar&bar=baz#hash', - ['enable' => ['trailingSlash' => true, 'toLowerCase' => true]], - [], + 'originalUrl' => 'https://local.dev', + 'expectedUrl' => 'https://local.dev', + 'isTrailingSlashEnabledResult' => true, + 'isToLowerCaseEnabledResult' => false, + 'isUriInBlocklistResult' => false, + 'statusCode' => 302, ], [ - 'https://local.dev/neos', - 'https://local.dev/neos', - ['enable' => ['trailingSlash' => true, 'toLowerCase' => true]], - [ - '/neos.*' => true, - ], + 'originalUrl' => 'https://local.dev/test/test2', + 'expectedUrl' => 'https://local.dev/test/test2', + 'isTrailingSlashEnabledResult' => false, + 'isToLowerCaseEnabledResult' => true, + 'isUriInBlocklistResult' => true, + 'statusCode' => 301, ], [ - 'https://local.dev/NEOS', - 'https://local.dev/neos/', - ['enable' => ['trailingSlash' => true, 'toLowerCase' => true]], - [ - '/neos.*' => false, - ], + 'originalUrl' => 'https://local.dev/test/test2', + 'expectedUrl' => 'https://local.dev/test/test2/', + 'isTrailingSlashEnabledResult' => false, + 'isToLowerCaseEnabledResult' => true, + 'isUriInBlocklistResult' => false, + 'statusCode' => 301, ], ]; } diff --git a/composer.json b/composer.json index f07749e..45eca2c 100644 --- a/composer.json +++ b/composer.json @@ -11,9 +11,9 @@ } }, "require": { - "neos/flow": "^8.3", "guzzlehttp/psr7": "^2.0", - "php": "^8.1" + "php": "^8.1", + "neos/neos": "^8.3" }, "require-dev": { "phpstan/phpstan": "^2.1",