Files
breadcrumb-the-shire/tests/Architecture/RouteAliasQuerySafetyContractTest.php

127 lines
4.0 KiB
PHP
Raw Normal View History

<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
/**
* Guard against fragile alias rewrites when query parameters are present.
*
* Minty's Router::applyRoutes() compares the full REQUEST_URI (including query)
* against alias source paths. Therefore, route aliases with query strings are
* brittle when path != target and should be avoided in runtime code.
*/
final class RouteAliasQuerySafetyContractTest extends TestCase
{
use ProjectFileAssertionSupport;
public function testRuntimeCodeDoesNotUseCoreAliasPathsWithQueryStringsWhenPathDiffersFromTarget(): void
{
$aliases = $this->coreAliasPathsWithDifferentTargets();
$this->assertNotSame([], $aliases, 'Expected at least one core route alias with path != target.');
$scanTargets = ['core', 'pages', 'templates', 'modules'];
$extensions = ['php', 'phtml'];
$violations = [];
foreach ($this->runtimeSourceFiles($scanTargets, $extensions) as $relativePath) {
$content = $this->readProjectFile($relativePath);
foreach ($aliases as $aliasPath) {
$quotedAlias = preg_quote($aliasPath, '#');
if (preg_match("#(?:lurl|Request::withLocale|Router::redirect)\\(\\s*['\"]{$quotedAlias}\\?#", $content) === 1) {
$violations[] = $relativePath . " uses alias '{$aliasPath}' with query string.";
}
}
}
sort($violations);
$this->assertSame([], $violations, "Alias+query routing anti-pattern found:\n" . implode("\n", $violations));
}
public function testAccessControlUsesConcreteLoginTargetPathForReturnToRedirect(): void
{
$content = $this->readProjectFile('core/Http/AccessControl.php');
$this->assertStringContainsString(
"Request::withLocale('auth/login', \$locale)",
$content
);
$this->assertStringNotContainsString(
"Request::withLocale('login', \$locale)",
$content
);
}
/**
* @return list<string>
*/
private function coreAliasPathsWithDifferentTargets(): array
{
$routesFile = $this->projectRootPath() . '/config/routes.php';
$this->assertFileExists($routesFile, 'Missing config/routes.php');
$routes = require $routesFile;
$this->assertIsArray($routes, 'config/routes.php must return an array');
$aliases = [];
foreach ($routes as $entry) {
if (!is_array($entry)) {
continue;
}
$path = trim((string) ($entry['path'] ?? ''));
$target = trim((string) ($entry['target'] ?? ''));
if ($path === '' || $target === '' || $path === $target) {
continue;
}
$aliases[] = $path;
}
$aliases = array_values(array_unique($aliases));
sort($aliases);
return $aliases;
}
/**
* @param list<string> $scanTargets
* @param list<string> $extensions
* @return list<string>
*/
private function runtimeSourceFiles(array $scanTargets, array $extensions): array
{
$root = $this->projectRootPath();
$files = [];
foreach ($scanTargets as $target) {
$fullPath = $root . '/' . $target;
if (!is_dir($fullPath)) {
continue;
}
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($fullPath, \FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if (!$file->isFile()) {
continue;
}
$extension = strtolower((string) $file->getExtension());
if (!in_array($extension, $extensions, true)) {
continue;
}
$files[] = str_replace($root . '/', '', $file->getPathname());
}
}
$files = array_values(array_unique($files));
sort($files);
return $files;
}
}