Files
breadcrumb-the-shire/tests/Support/Helpers/EndpointUrlTest.php

81 lines
2.8 KiB
PHP
Raw Normal View History

feat(core): endpointUrl() helper — query-safe URLs for module routes Introduce a helper that returns the registered route TARGET for a given source path, so URLs built for endpoints that carry query parameters do not rely on MintyPHP's applyRoutes() rewrite layer — that layer compares the full request URI (including `?query`) against source paths and silently misses modules where path ≠ target. - core/Support/helpers/app.php: endpointUrl($path) consults ModuleRegistry::getRoutes(), returns lurl(target) when the path is a registered module source path, otherwise falls through to lurl($path). Safe fallback when the container or registry is unavailable. - modules/audit/pages/audit/system-audit/index(default).phtml: replace the ad-hoc target-path workaround with endpointUrl('admin/system-audit/ export'). Callers can now write the natural source path without knowing about the rewrite trap. - Apply the same helper to modules/helpdesk/.../domains/index and pages/admin/users/index for consistency — a no-op where path already equals target, but establishes the convention: every export/data endpoint URL in page configs goes through endpointUrl(). - tests/Support/Helpers/EndpointUrlTest: 5 cases covering source→target resolution, idempotence when path == target, fall-through for unregistered paths, leading-slash normalization, and graceful degradation when the registry is missing. Uses AppContainerIsolationTrait per the contract test in tests/Architecture/AppContainerIsolationContractTest. Gates: PHPUnit 1894 OK, PHPStan 0 errors, module:sync ok. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:39:36 +02:00
<?php
namespace MintyPHP\Tests\Support\Helpers;
use MintyPHP\App\AppContainer;
use MintyPHP\App\Module\ModuleRegistry;
use MintyPHP\Tests\Support\AppContainerIsolationTrait;
use PHPUnit\Framework\TestCase;
class EndpointUrlTest extends TestCase
{
use AppContainerIsolationTrait;
protected function tearDown(): void
{
$this->restoreAppContainer();
}
public function testResolvesSourcePathToTarget(): void
{
$this->installRegistryWithRoutes([
['path' => 'admin/system-audit/export', 'target' => 'audit/system-audit/export', 'module_id' => 'audit'],
]);
$this->assertStringEndsWith('audit/system-audit/export', endpointUrl('admin/system-audit/export'));
}
public function testIdempotentWhenPathEqualsTarget(): void
{
$this->installRegistryWithRoutes([
['path' => 'helpdesk/domains/export', 'target' => 'helpdesk/domains/export', 'module_id' => 'helpdesk'],
]);
$this->assertStringEndsWith('helpdesk/domains/export', endpointUrl('helpdesk/domains/export'));
}
public function testFallsThroughToLurlForUnregisteredPath(): void
{
$this->installRegistryWithRoutes([]);
$this->assertSame(lurl('admin/users/export'), endpointUrl('admin/users/export'));
}
public function testIgnoresLeadingSlash(): void
{
$this->installRegistryWithRoutes([
['path' => 'admin/system-audit/export', 'target' => 'audit/system-audit/export', 'module_id' => 'audit'],
]);
$this->assertSame(
endpointUrl('admin/system-audit/export'),
endpointUrl('/admin/system-audit/export')
);
}
public function testDegradesGracefullyWhenRegistryMissing(): void
{
$this->pushAppContainer(new AppContainer());
// No ModuleRegistry registered — endpointUrl() must fall back, not throw.
$this->assertSame(lurl('some/path'), endpointUrl('some/path'));
}
/**
* @param list<array{path: string, target: string, module_id?: string}> $routes
*/
private function installRegistryWithRoutes(array $routes): void
{
// ModuleRegistry is final and has no public constructor — build via
// reflection and write the merged-routes field directly. Only used
// by endpointUrl() via getRoutes(), so that is all we need to stage.
$registry = (new \ReflectionClass(ModuleRegistry::class))->newInstanceWithoutConstructor();
$routesProp = (new \ReflectionClass(ModuleRegistry::class))->getProperty('mergedRoutes');
$routesProp->setValue($registry, $routes);
$container = new AppContainer();
$container->set(ModuleRegistry::class, static fn (): ModuleRegistry => $registry);
$this->pushAppContainer($container);
}
}