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 $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); } }