1
0

fix(routing): prevent login redirect loops on alias targets

This commit is contained in:
2026-04-26 10:20:28 +02:00
parent d0544d3428
commit dffc4db9a2
7 changed files with 149 additions and 4 deletions

View File

@@ -138,6 +138,21 @@ final class ModuleRegistryTest extends TestCase
ModuleRegistry::boot($this->fixturesDir, ['mod-a', 'mod-b']);
}
public function testPublicRouteAddsPathAndTargetToPublicPaths(): void
{
$this->createModuleManifest('mod-public', [
'id' => 'mod-public',
'routes' => [
['path' => 'public/entry', 'target' => 'public/internal', 'public' => true],
],
]);
$registry = ModuleRegistry::boot($this->fixturesDir, ['mod-public']);
self::assertContains('public/entry', $registry->getPublicPaths());
self::assertContains('public/internal', $registry->getPublicPaths());
}
public function testUiSlotConflictThrows(): void
{
$panelTemplateA = $this->createFixtureFile('mod-a-panel.phtml', '<ul></ul>');

View File

@@ -0,0 +1,126 @@
<?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;
}
}

View File

@@ -40,7 +40,7 @@ final class RouteCatalogTest extends TestCase
],
$catalog->coreRoutes()
);
self::assertSame(['login'], $catalog->corePublicPaths());
self::assertSame(['auth/login', 'login'], $catalog->corePublicPaths());
}
public function testThrowsWhenRouteConfigDoesNotReturnArray(): void