diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fabfe90a..0e5745a9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: pass_filenames: false - id: phpunit name: PHPUnit - entry: vendor/bin/phpunit + entry: env XDEBUG_MODE=coverage vendor/bin/phpunit language: system types: [php] pass_filenames: false diff --git a/src/Adapter/Psr7/ServerRequest.php b/src/Adapter/Psr7/ServerRequest.php new file mode 100644 index 00000000..f33975d4 --- /dev/null +++ b/src/Adapter/Psr7/ServerRequest.php @@ -0,0 +1,224 @@ +getHarRequest(); + $cookieParams = []; + foreach ($request->getCookies() as $cookie) { + $cookieParams[$cookie->getName()] = $cookie->getValue(); + } + + return $cookieParams; + } + + public function withCookieParams(array $cookies): ServerRequestInterface + { + $request = clone $this->getHarRequest(); + $harCookies = []; + foreach ($cookies as $name => $value) { + $harCookies[] = (new Cookie()) + ->setName($name) + ->setValue($value); + } + $request->setCookies($harCookies); + + return new static($request); + } + + public function getQueryParams(): array + { + $request = $this->getHarRequest(); + $queryParams = []; + foreach ($request->getQueryString() as $param) { + $queryParams[$param->getName()] = $param->getValue(); + } + + return $queryParams; + } + + public function withQueryParams(array $query): ServerRequestInterface + { + $request = clone $this->getHarRequest(); + $harParams = []; + foreach ($query as $name => $value) { + $harParams[] = (new Params()) + ->setName($name) + ->setValue((string) $value); + } + $request->setQueryString($harParams); + + return new static($request); + } + + public function getUploadedFiles(): array + { + return []; + } + + public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface + { + throw new \LogicException('Uploaded files are not supported.'); + } + + public function getParsedBody() + { + $request = $this->getHarRequest(); + + if (!$request->hasPostData()) { + return null; + } + + $postData = $request->getPostData(); + if ($postData->hasParams()) { + $parsedBody = []; + foreach ($postData->getParams() as $param) { + $parsedBody[$param->getName()] = $param->getValue(); + } + + return $parsedBody; + } + + if ($postData->hasText()) { + // Try to parse as form data if content type suggests it + $contentType = $postData->getMimeType(); + if ('application/x-www-form-urlencoded' === $contentType) { + $parsedBody = []; + parse_str($postData->getText(), $parsedBody); + + return $parsedBody; + } + } + + return null; + } + + public function withParsedBody($data): ServerRequestInterface + { + if (!\is_array($data) && !\is_object($data) && null !== $data) { + throw new \InvalidArgumentException('Parsed body must be an array, object, or null.'); + } + + $request = clone $this->getHarRequest(); + + if (\is_array($data) || \is_object($data)) { + $postData = new PostData(); + $harParams = []; + foreach ($data as $name => $value) { + $harParams[] = (new Params()) + ->setName($name) + ->setValue((string) $value); + } + $postData->setParams($harParams); + $request->setPostData($postData); + } elseif (null === $data) { + // Clear post data + $request->setPostData(new PostData()); + } + + return new static($request); + } + + public function getAttributes(): array + { + return []; + } + + public function getAttribute($name, $default = null) + { + return $default; + } + + public function withAttribute($name, $value): ServerRequestInterface + { + throw new \LogicException('Attributes are not supported.'); + } + + public function withoutAttribute($name): ServerRequestInterface + { + // Attributes are not part of HAR spec, return unchanged clone + return new static($this->getHarRequest()); + } + + /** + * Override parent methods to return ServerRequestInterface. + */ + public function withMethod($method): ServerRequestInterface + { + $parent = parent::withMethod($method); + + return new static($parent->getHarRequest()); + } + + public function withUri(UriInterface $uri, $preserveHost = false): ServerRequestInterface + { + $parent = parent::withUri($uri, $preserveHost); + + return new static($parent->getHarRequest()); + } + + public function withRequestTarget($requestTarget): ServerRequestInterface + { + $parent = parent::withRequestTarget($requestTarget); + + return new static($parent->getHarRequest()); + } + + public function withBody(StreamInterface $body): ServerRequestInterface + { + $parent = parent::withBody($body); + + return new static($parent->getHarRequest()); + } + + public function withHeader($name, $value): ServerRequestInterface + { + $parent = parent::withHeader($name, $value); + + return new static($parent->getHarRequest()); + } + + public function withAddedHeader($name, $value): ServerRequestInterface + { + $parent = parent::withAddedHeader($name, $value); + + return new static($parent->getHarRequest()); + } + + public function withoutHeader($name): ServerRequestInterface + { + $parent = parent::withoutHeader($name); + + return new static($parent->getHarRequest()); + } + + public function withProtocolVersion($version): ServerRequestInterface + { + $parent = parent::withProtocolVersion($version); + + return new static($parent->getHarRequest()); + } +} diff --git a/src/PostData.php b/src/PostData.php index 825a0ebb..24d6a23b 100644 --- a/src/PostData.php +++ b/src/PostData.php @@ -7,10 +7,9 @@ use Deviantintegral\Har\SharedFields\CommentTrait; use Deviantintegral\Har\SharedFields\MimeTypeTrait; use Deviantintegral\Har\SharedFields\TextTrait; +use GuzzleHttp\Psr7\Query; use JMS\Serializer\Annotation as Serializer; -use function GuzzleHttp\Psr7\build_query; - /** * @see http://www.softwareishard.com/blog/har-12-spec/#postData */ @@ -77,7 +76,7 @@ public function getBodySize(): int foreach ($this->params as $param) { $query[$param->getName()] = $param->getValue(); } - $string = build_query($query); + $string = Query::build($query); return \strlen($string); } diff --git a/src/Request.php b/src/Request.php index f0577fa5..ae388588 100644 --- a/src/Request.php +++ b/src/Request.php @@ -11,6 +11,7 @@ use Deviantintegral\Har\SharedFields\HttpVersionTrait; use JMS\Serializer\Annotation as Serializer; use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; /** * @see http://www.softwareishard.com/blog/har-12-spec/#request @@ -75,6 +76,41 @@ public static function fromPsr7Request(RequestInterface $source): self return $request->getHarRequest(); } + /** + * Construct a new Request from a PSR-7 ServerRequest. + */ + public static function fromPsr7ServerRequest(ServerRequestInterface $source): self + { + // Start with the basic request conversion (ServerRequest extends Request) + $harRequest = static::fromPsr7Request($source); + + // Extract and set cookies from ServerRequest + $cookies = []; + foreach ($source->getCookieParams() as $name => $value) { + $cookie = (new Cookie()) + ->setName($name) + ->setValue($value); + $cookies[] = $cookie; + } + if (!empty($cookies)) { + $harRequest->setCookies($cookies); + } + + // Extract and set query parameters from ServerRequest + $queryParams = []; + foreach ($source->getQueryParams() as $name => $value) { + $param = (new Params()) + ->setName($name) + ->setValue((string) $value); + $queryParams[] = $param; + } + if (!empty($queryParams)) { + $harRequest->setQueryString($queryParams); + } + + return $harRequest; + } + public function getMethod(): string { return $this->method; @@ -104,7 +140,7 @@ public function setUrl(\Psr\Http\Message\UriInterface $url): self */ public function getQueryString(): array { - return $this->queryString; + return $this->queryString ?? []; } /** diff --git a/src/SharedFields/CookiesTrait.php b/src/SharedFields/CookiesTrait.php index a0214d98..6e2c729a 100644 --- a/src/SharedFields/CookiesTrait.php +++ b/src/SharedFields/CookiesTrait.php @@ -20,7 +20,7 @@ trait CookiesTrait */ public function getCookies(): array { - return $this->cookies; + return $this->cookies ?? []; } /** diff --git a/src/SharedFields/MimeTypeTrait.php b/src/SharedFields/MimeTypeTrait.php index aa92bd29..14c49d6c 100644 --- a/src/SharedFields/MimeTypeTrait.php +++ b/src/SharedFields/MimeTypeTrait.php @@ -19,7 +19,7 @@ trait MimeTypeTrait public function getMimeType(): string { - return $this->mimeType; + return $this->mimeType ?? ''; } public function setMimeType(string $mimeType): self diff --git a/tests/src/Unit/Adapter/Psr7/ServerRequestTest.php b/tests/src/Unit/Adapter/Psr7/ServerRequestTest.php new file mode 100644 index 00000000..89f88a41 --- /dev/null +++ b/tests/src/Unit/Adapter/Psr7/ServerRequestTest.php @@ -0,0 +1,326 @@ +harRequest = (new Request()) + ->setMethod('POST') + ->setUrl(new Uri('https://www.example.com/path?foo=bar')) + ->setHttpVersion('HTTP/1.1') + ->setHeaders([ + (new Header())->setName('Host')->setValue('www.example.com'), + (new Header())->setName('Content-Type')->setValue('application/x-www-form-urlencoded'), + ]) + ->setQueryString([ + (new Params())->setName('foo')->setValue('bar'), + ]) + ->setCookies([ + (new Cookie())->setName('session')->setValue('abc123'), + ]) + ->setPostData( + (new PostData())->setParams([ + (new Params())->setName('username')->setValue('john'), + (new Params())->setName('password')->setValue('secret'), + ]) + ); + + $this->serverRequest = (new ServerRequest($this->harRequest)) + ->withCookieParams(['session' => 'abc123']) + ->withQueryParams(['foo' => 'bar']) + ->withParsedBody(['username' => 'john', 'password' => 'secret']); + } + + public function testGetServerParams() + { + // Server params are not part of HAR spec, always returns empty array + $this->assertEquals([], $this->serverRequest->getServerParams()); + } + + public function testGetCookieParams() + { + $this->assertEquals( + ['session' => 'abc123'], + $this->serverRequest->getCookieParams() + ); + } + + public function testWithCookieParams() + { + $new = $this->serverRequest->withCookieParams(['new_cookie' => 'xyz789']); + $this->assertEquals(['new_cookie' => 'xyz789'], $new->getCookieParams()); + // Verify immutability + $this->assertEquals(['session' => 'abc123'], $this->serverRequest->getCookieParams()); + } + + public function testGetQueryParams() + { + $this->assertEquals( + ['foo' => 'bar'], + $this->serverRequest->getQueryParams() + ); + } + + public function testWithQueryParams() + { + $new = $this->serverRequest->withQueryParams(['baz' => 'qux']); + $this->assertEquals(['baz' => 'qux'], $new->getQueryParams()); + // Verify immutability + $this->assertEquals(['foo' => 'bar'], $this->serverRequest->getQueryParams()); + } + + public function testGetUploadedFiles() + { + // Uploaded files are not part of HAR spec, always returns empty array + $this->assertEquals([], $this->serverRequest->getUploadedFiles()); + } + + public function testWithUploadedFiles() + { + // Uploaded files are not part of HAR spec, this is a no-op + $files = ['file' => 'mock_uploaded_file']; + $this->expectException(\LogicException::class); + $new = $this->serverRequest->withUploadedFiles($files); + } + + public function testGetParsedBody() + { + $this->assertEquals( + ['username' => 'john', 'password' => 'secret'], + $this->serverRequest->getParsedBody() + ); + } + + public function testWithParsedBody() + { + $new = $this->serverRequest->withParsedBody(['key' => 'value']); + $this->assertEquals(['key' => 'value'], $new->getParsedBody()); + // Verify immutability + $this->assertEquals( + ['username' => 'john', 'password' => 'secret'], + $this->serverRequest->getParsedBody() + ); + } + + public function testWithParsedBodyNull() + { + $new = $this->serverRequest->withParsedBody(null); + $this->assertNull($new->getParsedBody()); + } + + public function testWithParsedBodyInvalidType() + { + $this->expectException(\InvalidArgumentException::class); + $this->serverRequest->withParsedBody('invalid'); + } + + public function testGetAttributes() + { + // Attributes are not part of HAR spec, always returns empty array + $this->assertEquals([], $this->serverRequest->getAttributes()); + } + + public function testGetAttribute() + { + // Attributes are not part of HAR spec, always returns default + $this->assertNull($this->serverRequest->getAttribute('custom_attr')); + $this->assertNull($this->serverRequest->getAttribute('nonexistent')); + $this->assertEquals('default', $this->serverRequest->getAttribute('nonexistent', 'default')); + } + + public function testWithAttribute() + { + $this->expectException(\LogicException::class); + $this->serverRequest->withAttribute('new_attr', 'new_value'); + } + + public function testWithoutAttribute() + { + // Attributes are not part of HAR spec, this is a no-op + $new = $this->serverRequest->withoutAttribute('custom_attr'); + $this->assertNull($new->getAttribute('custom_attr')); + $this->assertNull($this->serverRequest->getAttribute('custom_attr')); + } + + public function testInheritedMethodsPreserveServerRequestState() + { + // Test that methods inherited from Request preserve ServerRequest-specific properties + $new = $this->serverRequest->withMethod('GET'); + $this->assertEquals('GET', $new->getMethod()); + $this->assertEquals(['session' => 'abc123'], $new->getCookieParams()); + $this->assertEquals(['foo' => 'bar'], $new->getQueryParams()); + } + + public function testWithBody() + { + $new = $this->serverRequest->withBody(Utils::streamFor('new body')); + $this->assertEquals('new body', $new->getBody()->getContents()); + // Verify ServerRequest state is preserved + $this->assertEquals(['session' => 'abc123'], $new->getCookieParams()); + $this->assertEquals(['foo' => 'bar'], $new->getQueryParams()); + } + + public function testWithUri() + { + $newUri = new Uri('https://www.newexample.com/newpath'); + $new = $this->serverRequest->withUri($newUri); + $this->assertEquals($newUri, $new->getUri()); + // Verify ServerRequest state is preserved + $this->assertEquals(['session' => 'abc123'], $new->getCookieParams()); + $this->assertEquals(['foo' => 'bar'], $new->getQueryParams()); + } + + public function testWithHeader() + { + $new = $this->serverRequest->withHeader('X-Custom', 'value'); + $this->assertEquals(['value'], $new->getHeader('X-Custom')); + // Verify ServerRequest state is preserved + $this->assertEquals(['session' => 'abc123'], $new->getCookieParams()); + $this->assertEquals(['foo' => 'bar'], $new->getQueryParams()); + } + + public function testInitializeFromHarRequest() + { + // Test that a ServerRequest can be created from a HAR request + // and properly extract query params, cookies, and parsed body + $serverRequest = new ServerRequest($this->harRequest); + + // Should extract query params from HAR request + $this->assertEquals(['foo' => 'bar'], $serverRequest->getQueryParams()); + + // Should extract cookies from HAR request + $this->assertEquals(['session' => 'abc123'], $serverRequest->getCookieParams()); + + // Should extract parsed body from HAR POST params + $this->assertEquals( + ['username' => 'john', 'password' => 'secret'], + $serverRequest->getParsedBody() + ); + + // Server params should be empty by default + $this->assertEquals([], $serverRequest->getServerParams()); + } + + public function testGetParsedBodyWithNoPostData() + { + // Test that getParsedBody returns null when there's no post data + $harRequest = (new Request()) + ->setMethod('GET') + ->setUrl(new Uri('https://www.example.com/path')); + + $serverRequest = new ServerRequest($harRequest); + $this->assertNull($serverRequest->getParsedBody()); + } + + public function testGetParsedBodyWithFormUrlEncodedText() + { + // Test parsing application/x-www-form-urlencoded text + $harRequest = (new Request()) + ->setMethod('POST') + ->setUrl(new Uri('https://www.example.com/path')) + ->setPostData( + (new PostData()) + ->setMimeType('application/x-www-form-urlencoded') + ->setText('username=john&password=secret') + ); + + $serverRequest = new ServerRequest($harRequest); + $this->assertEquals( + ['username' => 'john', 'password' => 'secret'], + $serverRequest->getParsedBody() + ); + } + + public function testGetParsedBodyWithNonFormEncodedText() + { + // Test that getParsedBody returns null for non-form-encoded text + $harRequest = (new Request()) + ->setMethod('POST') + ->setUrl(new Uri('https://www.example.com/path')) + ->setPostData( + (new PostData()) + ->setMimeType('application/json') + ->setText('{"username":"john","password":"secret"}') + ); + + $serverRequest = new ServerRequest($harRequest); + $this->assertNull($serverRequest->getParsedBody()); + } + + public function testWithParsedBodyObject() + { + // Test that withParsedBody works with objects + $data = new \stdClass(); + $data->username = 'john'; + $data->password = 'secret'; + + $new = $this->serverRequest->withParsedBody($data); + $parsedBody = $new->getParsedBody(); + + $this->assertIsArray($parsedBody); + $this->assertEquals('john', $parsedBody['username']); + $this->assertEquals('secret', $parsedBody['password']); + } + + public function testWithRequestTarget() + { + $new = $this->serverRequest->withRequestTarget('https://www.example.com/newpath'); + $this->assertEquals('https://www.example.com/newpath', $new->getRequestTarget()); + // Verify ServerRequest state is preserved + $this->assertEquals(['session' => 'abc123'], $new->getCookieParams()); + $this->assertEquals(['foo' => 'bar'], $new->getQueryParams()); + } + + public function testWithAddedHeader() + { + $new = $this->serverRequest->withAddedHeader('X-Custom', 'value1'); + $new = $new->withAddedHeader('X-Custom', 'value2'); + $this->assertEquals(['value1', 'value2'], $new->getHeader('X-Custom')); + // Verify ServerRequest state is preserved + $this->assertEquals(['session' => 'abc123'], $new->getCookieParams()); + $this->assertEquals(['foo' => 'bar'], $new->getQueryParams()); + } + + public function testWithoutHeader() + { + $new = $this->serverRequest->withoutHeader('Host'); + $this->assertFalse($new->hasHeader('Host')); + // Verify ServerRequest state is preserved + $this->assertEquals(['session' => 'abc123'], $new->getCookieParams()); + $this->assertEquals(['foo' => 'bar'], $new->getQueryParams()); + } + + public function testWithProtocolVersion() + { + $new = $this->serverRequest->withProtocolVersion('2.0'); + $this->assertEquals('2.0', $new->getProtocolVersion()); + // Verify ServerRequest state is preserved + $this->assertEquals(['session' => 'abc123'], $new->getCookieParams()); + $this->assertEquals(['foo' => 'bar'], $new->getQueryParams()); + } +} diff --git a/tests/src/Unit/RequestTest.php b/tests/src/Unit/RequestTest.php index 9cb935ca..aff10b43 100644 --- a/tests/src/Unit/RequestTest.php +++ b/tests/src/Unit/RequestTest.php @@ -78,4 +78,40 @@ public function testFromPsr7() $this->assertEquals(4, $har_request->getBodySize()); $this->assertEquals('HTTP/2.0', $har_request->getHttpVersion()); } + + public function testFromPsr7ServerRequest() + { + $uri = new Uri('https://www.example.com/path?foo=bar'); + $psr7 = new \GuzzleHttp\Psr7\ServerRequest( + 'POST', + $uri, + ['Accept' => '*/*', 'Cookie' => 'session=abc123'], + 'name=value', + '1.1', + [ + 'REQUEST_METHOD' => 'POST', + 'REQUEST_URI' => '/path?foo=bar', + 'SERVER_NAME' => 'www.example.com', + ] + ); + $psr7 = $psr7->withCookieParams(['session' => 'abc123']) + ->withQueryParams(['foo' => 'bar']) + ->withParsedBody(['name' => 'value']) + ->withAttribute('custom_attr', 'custom_value'); + + $har_request = Request::fromPsr7ServerRequest($psr7); + + $this->assertEquals('POST', $har_request->getMethod()); + $this->assertEquals($uri, $har_request->getUrl()); + $this->assertEquals('HTTP/1.1', $har_request->getHttpVersion()); + + // Verify headers + $headers = $har_request->getHeaders(); + $headerNames = array_map(fn ($h) => $h->getName(), $headers); + $this->assertContains('Host', $headerNames); + $this->assertContains('Accept', $headerNames); + + // Verify body + $this->assertEquals('name=value', $har_request->getPostData()->getText()); + } }