responseManipulator = new ResponseManipulator(); // Create real request ID generator with test secret $this->requestIdGenerator = new RequestIdGenerator('test-secret-for-testing'); $this->middleware = new RequestIdMiddleware( $this->requestIdGenerator, $this->responseManipulator ); // Create test request $this->request = new HttpRequest( method: Method::GET, path: '/test' ); $this->stateManager = new RequestStateManager(new WeakMap(), $this->request); $this->context = new MiddlewareContext($this->request); }); it('adds request ID header to response', function () { // Create response $headers = new Headers([ 'Content-Type' => 'application/json', ]); $response = new HttpResponse(Status::OK, $headers, '{"test": true}'); // Create next handler that returns context with response $next = new class ($response) implements Next { public function __construct(private HttpResponse $response) { } public function __invoke(MiddlewareContext $context): MiddlewareContext { return $context->withResponse($this->response); } }; $result = $this->middleware->__invoke( $this->context, $next, $this->stateManager ); expect($result->hasResponse())->toBeTrue(); expect($result->response->headers->has('X-Request-ID'))->toBeTrue(); expect($result->response->headers->getFirst('X-Request-ID'))->not->toBeEmpty(); expect($result->response->headers->has('Content-Type'))->toBeTrue(); expect($result->response->headers->getFirst('Content-Type'))->toBe('application/json'); }); it('preserves existing headers when adding request ID', function () { // Create response with multiple headers $headers = new Headers([ 'Content-Type' => 'text/html', 'Cache-Control' => 'no-cache', 'Server' => 'test-server', ]); $response = new HttpResponse(Status::OK, $headers, 'test'); $next = new class ($response) implements Next { public function __construct(private HttpResponse $response) { } public function __invoke(MiddlewareContext $context): MiddlewareContext { return $context->withResponse($this->response); } }; $result = $this->middleware->__invoke( $this->context, $next, $this->stateManager ); expect($result->hasResponse())->toBeTrue(); expect($result->response->headers->has('X-Request-ID'))->toBeTrue(); expect($result->response->headers->getFirst('X-Request-ID'))->not->toBeEmpty(); expect($result->response->headers->has('Content-Type'))->toBeTrue(); expect($result->response->headers->getFirst('Content-Type'))->toBe('text/html'); expect($result->response->headers->has('Cache-Control'))->toBeTrue(); expect($result->response->headers->getFirst('Cache-Control'))->toBe('no-cache'); expect($result->response->headers->has('Server'))->toBeTrue(); expect($result->response->headers->getFirst('Server'))->toBe('test-server'); }); it('passes through context when no response present', function () { // Next handler that doesn't set a response $next = new class () implements Next { public function __invoke(MiddlewareContext $context): MiddlewareContext { return $context; } }; $result = $this->middleware->__invoke( $this->context, $next, $this->stateManager ); expect($result->hasResponse())->toBeFalse(); }); it('preserves response body and status', function () { $headers = new Headers([ 'Content-Type' => 'application/json', ]); $response = new HttpResponse(Status::CREATED, $headers, '{"created": true}'); $next = new class ($response) implements Next { public function __construct(private HttpResponse $response) { } public function __invoke(MiddlewareContext $context): MiddlewareContext { return $context->withResponse($this->response); } }; $result = $this->middleware->__invoke( $this->context, $next, $this->stateManager ); expect($result->hasResponse())->toBeTrue(); expect($result->response->status)->toBe(Status::CREATED); expect($result->response->body)->toBe('{"created": true}'); expect($result->response->headers->has('X-Request-ID'))->toBeTrue(); expect($result->response->headers->getFirst('X-Request-ID'))->not->toBeEmpty(); }); it('uses correct header name', function () { $headers = new Headers(['Content-Type' => 'text/plain']); $response = new HttpResponse(Status::OK, $headers, 'test'); $next = new class ($response) implements Next { public function __construct(private HttpResponse $response) { } public function __invoke(MiddlewareContext $context): MiddlewareContext { return $context->withResponse($this->response); } }; $result = $this->middleware->__invoke($this->context, $next, $this->stateManager); expect($result->response->headers->has('X-Request-ID'))->toBeTrue(); expect(RequestIdGenerator::getHeaderName())->toBe('X-Request-ID'); });