pathProvider = new PathProvider(__DIR__ . '/../../../../'); $this->container = new DefaultContainer(); $this->templateRenderer = Mockery::mock(TemplateRenderer::class); // RequestId needs a secret for tests $this->requestId = new RequestId('test-secret-for-unit-tests'); }); it('detects admin routes correctly', function () { $request = new HttpRequest( method: Method::GET, path: '/admin/dashboard', id: $this->requestId ); $responder = new RouteResponder( $this->pathProvider, $this->container, $this->templateRenderer, $request ); // Use reflection to test private method $reflection = new ReflectionClass($responder); $method = $reflection->getMethod('isAdminRoute'); $method->setAccessible(true); expect($method->invoke($responder, '/admin/dashboard'))->toBeTrue(); expect($method->invoke($responder, '/admin'))->toBeTrue(); expect($method->invoke($responder, '/admin/users'))->toBeTrue(); expect($method->invoke($responder, '/'))->toBeFalse(); expect($method->invoke($responder, '/api/users'))->toBeFalse(); expect($method->invoke($responder, '/admin-api/users'))->toBeFalse(); }); it('enriches admin routes with layout data', function () { $request = new HttpRequest( method: Method::GET, path: '/admin/dashboard', id: $this->requestId ); $layoutProcessor = Mockery::mock(AdminLayoutProcessor::class); $layoutProcessor->shouldReceive('processLayoutFromArray') ->once() ->with(['title' => 'Dashboard']) ->andReturn([ 'title' => 'Dashboard', 'navigation_menu' => ['System' => ['items' => []]], 'breadcrumbs' => [], 'current_path' => '/admin/dashboard', ]); // Register layout processor in real container $this->container->instance(AdminLayoutProcessor::class, $layoutProcessor); $this->templateRenderer->shouldReceive('render') ->once() ->andReturn('rendered'); $responder = new RouteResponder( $this->pathProvider, $this->container, $this->templateRenderer, $request ); $viewResult = new ViewResult( template: 'dashboard', metaData: new MetaData('Dashboard'), data: ['title' => 'Dashboard'] ); $context = $responder->getContext($viewResult); expect($context)->toBeInstanceOf(RenderContext::class); expect($context->data)->toHaveKey('navigation_menu'); expect($context->data)->toHaveKey('breadcrumbs'); expect($context->data)->toHaveKey('current_path'); }); it('does not enrich non-admin routes', function () { $request = new HttpRequest( method: Method::GET, path: '/home', id: $this->requestId ); $this->templateRenderer->shouldReceive('render') ->once() ->andReturn('rendered'); $responder = new RouteResponder( $this->pathProvider, $this->container, $this->templateRenderer, $request ); $viewResult = new ViewResult( template: 'home', metaData: new MetaData('Home'), data: ['title' => 'Home'] ); $context = $responder->getContext($viewResult); expect($context)->toBeInstanceOf(RenderContext::class); expect($context->data)->not->toHaveKey('navigation_menu'); expect($context->data)->not->toHaveKey('breadcrumbs'); expect($context->data['title'])->toBe('Home'); }); it('handles missing AdminLayoutProcessor gracefully', function () { $request = new HttpRequest( method: Method::GET, path: '/admin/dashboard', id: $this->requestId ); // Container doesn't have AdminLayoutProcessor registered // (it's a fresh container without it) $this->templateRenderer->shouldReceive('render') ->once() ->andReturn('rendered'); $responder = new RouteResponder( $this->pathProvider, $this->container, $this->templateRenderer, $request ); $viewResult = new ViewResult( template: 'dashboard', metaData: new MetaData('Dashboard'), data: ['title' => 'Dashboard'] ); // Should not throw, but return original data $context = $responder->getContext($viewResult); expect($context)->toBeInstanceOf(RenderContext::class); expect($context->data['title'])->toBe('Dashboard'); }); it('handles AdminLayoutProcessor exceptions gracefully', function () { $request = new HttpRequest( method: Method::GET, path: '/admin/dashboard', id: $this->requestId ); $layoutProcessor = Mockery::mock(AdminLayoutProcessor::class); $layoutProcessor->shouldReceive('processLayoutFromArray') ->once() ->andThrow(new RuntimeException('Layout processing failed')); // Register layout processor that throws exception $this->container->instance(AdminLayoutProcessor::class, $layoutProcessor); $this->templateRenderer->shouldReceive('render') ->once() ->andReturn('rendered'); $responder = new RouteResponder( $this->pathProvider, $this->container, $this->templateRenderer, $request ); $viewResult = new ViewResult( template: 'dashboard', metaData: new MetaData('Dashboard'), data: ['title' => 'Dashboard'] ); // Should not throw, but return original data $context = $responder->getContext($viewResult); expect($context)->toBeInstanceOf(RenderContext::class); expect($context->data['title'])->toBe('Dashboard'); }); it('preserves original data when enriching', function () { $request = new HttpRequest( method: Method::GET, path: '/admin/users', id: $this->requestId ); $originalData = [ 'title' => 'Users', 'users' => [['id' => 1, 'name' => 'John']], 'pagination' => ['page' => 1], ]; $layoutProcessor = Mockery::mock(AdminLayoutProcessor::class); $layoutProcessor->shouldReceive('processLayoutFromArray') ->once() ->with($originalData) ->andReturn(array_merge($originalData, [ 'navigation_menu' => ['Content' => ['items' => []]], 'breadcrumbs' => [], 'current_path' => '/admin/users', ])); // Register layout processor in real container $this->container->instance(AdminLayoutProcessor::class, $layoutProcessor); $this->templateRenderer->shouldReceive('render') ->once() ->andReturn('rendered'); $responder = new RouteResponder( $this->pathProvider, $this->container, $this->templateRenderer, $request ); $viewResult = new ViewResult( template: 'users', metaData: new MetaData('Users'), data: $originalData ); $context = $responder->getContext($viewResult); expect($context->data)->toHaveKey('users'); expect($context->data)->toHaveKey('pagination'); expect($context->data)->toHaveKey('navigation_menu'); expect($context->data['users'])->toBe($originalData['users']); }); }); describe('RouteResponder - Admin Route Detection Edge Cases', function () { beforeEach(function () { // PathProvider and DefaultContainer are final, so we need real instances $this->pathProvider = new PathProvider(__DIR__ . '/../../../../'); $this->container = new DefaultContainer(); $this->templateRenderer = Mockery::mock(TemplateRenderer::class); // RequestId needs a secret for tests $this->requestId = new RequestId('test-secret-for-unit-tests'); }); it('handles exact /admin path', function () { $request = new HttpRequest( method: Method::GET, path: '/admin', id: $this->requestId ); $layoutProcessor = Mockery::mock(AdminLayoutProcessor::class); $layoutProcessor->shouldReceive('processLayoutFromArray') ->once() ->andReturn(['title' => 'Admin', 'navigation_menu' => [], 'breadcrumbs' => []]); // Register layout processor in real container $this->container->instance(AdminLayoutProcessor::class, $layoutProcessor); $this->templateRenderer->shouldReceive('render') ->once() ->andReturn('rendered'); $responder = new RouteResponder( $this->pathProvider, $this->container, $this->templateRenderer, $request ); $viewResult = new ViewResult( template: 'admin', metaData: new MetaData('Admin'), data: ['title' => 'Admin'] ); $context = $responder->getContext($viewResult); expect($context->data)->toHaveKey('navigation_menu'); }); it('does not treat /admin-api as admin route', function () { $request = new HttpRequest( method: Method::GET, path: '/admin-api/users', id: $this->requestId ); $this->templateRenderer->shouldReceive('render') ->once() ->andReturn('rendered'); $responder = new RouteResponder( $this->pathProvider, $this->container, $this->templateRenderer, $request ); $viewResult = new ViewResult( template: 'api', metaData: new MetaData('API'), data: ['title' => 'API'] ); $context = $responder->getContext($viewResult); expect($context->data)->not->toHaveKey('navigation_menu'); }); });