From f0f76d0b003bc740801ad97a4df9767562cdd263 Mon Sep 17 00:00:00 2001 From: Thomas Flori <thflori@gmail.com> Date: Tue, 7 Aug 2018 07:57:04 +0200 Subject: [PATCH] test server response methods --- composer.json | 3 +- src/ServerRequest.php | 48 +++++++++++- src/ServerResponse.php | 44 +++++++++-- tests/ServerResponseTest.php | 140 ++++++++++++++++++++++++++++++++++- 4 files changed, 223 insertions(+), 12 deletions(-) diff --git a/composer.json b/composer.json index 4906abb..3f6a398 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,8 @@ }, "require-dev": { "phpunit/phpunit": "^7.2.7", - "squizlabs/php_codesniffer": "^3.3.1" + "squizlabs/php_codesniffer": "^3.3.1", + "mockery/mockery": "^1.1.0" }, "autoload": { "psr-4": { diff --git a/src/ServerRequest.php b/src/ServerRequest.php index dd11648..404655c 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -360,13 +360,59 @@ class ServerRequest extends Request implements ServerRequestInterface return $new; } + /** + * Get cookie by name + * + * @param string $name + * @param mixed $default + * @return mixed + */ public function getCookie(string $name, $default = null) { return $this->getCookieParams()[$name] ?? $default; } - public function hasCookie(string $name) + /** + * Check if cookie is set + * + * @param string $name + * @return bool + */ + public function hasCookie(string $name): bool { return isset($this->getCookieParams()[$name]); } + + /** + * Get the relative path to $base. + * + * If $base is not given it will be determined by $_SERVER['SCRIPT_NAME']. + * + * e. g. The path is `/shop/products/foo` and the SCRIPT_NAME is `/shop/index.php` the result would be + * `/products/foo`. + * + * @param string $base The base where you want to start without slash + * @return string The path including the first slash + */ + public function getRelativePath(string $base = null): string + { + if ($base === null) { + $base = $this->getBase(); + } + + return substr($this->getUri()->getPath(), strlen(rtrim($base))); + } + + /** + * Get the domain absolute path to your application. + * + * If your software is installed on a separate (sub-)domain this will return '/' otherwise the path to your + * php file. + * + * @return string + */ + public function getBase(): string + { + return dirname($this->getServerParams()['SCRIPT_NAME']); + } } diff --git a/src/ServerResponse.php b/src/ServerResponse.php index 94b03ca..ffd9f87 100644 --- a/src/ServerResponse.php +++ b/src/ServerResponse.php @@ -17,8 +17,8 @@ class ServerResponse extends Response implements ServerResponseInterface /** * Sends this response to the client. * - * @param int $bufferSize - * @param Server|null $server + * @param int $bufferSize Send maximum this amount of bytes. + * @param Server $server For testing proposes you can provide a Server object * @return static */ public function send(int $bufferSize = 8192, Server $server = null) @@ -34,13 +34,13 @@ class ServerResponse extends Response implements ServerResponseInterface } } - $http_line = sprintf( + $httpLine = sprintf( 'HTTP/%s %s %s', $this->getProtocolVersion(), $this->getStatusCode(), $this->getReasonPhrase() ); - $server->header($http_line, true, $this->getStatusCode()); + $server->header($httpLine, true, $this->getStatusCode()); $stream = $this->getBody(); if ($stream->isSeekable()) { @@ -52,6 +52,22 @@ class ServerResponse extends Response implements ServerResponseInterface return $this; } + /** + * Returns an instance with the Set-Cookie header. + * + * Instead of providing a timestamp it expects an max age in seconds. + * + * @link http://php.net/manual/en/function.setcookie.php + * @param $name + * @param string $value + * @param int $maxAge + * @param string $path + * @param string $domain + * @param bool $secure + * @param bool $httponly + * @param bool $sameSite + * @return ServerResponse + */ public function withSetCookie( $name, $value = "", @@ -66,6 +82,22 @@ class ServerResponse extends Response implements ServerResponseInterface return $new->setCookie($name, $value, $maxAge, $path, $domain, $secure, $httponly, $sameSite); } + /** + * Adds a Set-Cookie header. + * + * Instead of providing a timestamp it expects an max age in seconds. + * + * @link http://php.net/manual/en/function.setcookie.php + * @param $name + * @param string $value + * @param int $maxAge + * @param string $path + * @param string $domain + * @param bool $secure + * @param bool $httponly + * @param bool $sameSite + * @return $this + */ public function setCookie( $name, $value = "", @@ -85,8 +117,8 @@ class ServerResponse extends Response implements ServerResponseInterface $headerLine = sprintf('%s=%s', $name, urlencode($value)); if ($maxAge) { - $headerLine .= '; expires=' . gmdate('D, d M Y H:i:s T', time() + $maxAge); - $headerLine .= '; Max-Age=' . $maxAge; + $headerLine .= '; expires=' . gmdate('r', time() + $maxAge); + $headerLine .= '; Max-Age=' . max($maxAge, 0); } if ($path) { diff --git a/tests/ServerResponseTest.php b/tests/ServerResponseTest.php index e981583..2092873 100644 --- a/tests/ServerResponseTest.php +++ b/tests/ServerResponseTest.php @@ -2,19 +2,151 @@ namespace Tal\Test; -use PHPUnit\Framework\TestCase; +use GuzzleHttp\Psr7\Stream; +use Mockery\Adapter\Phpunit\MockeryTestCase; +use Tal\Server; use Tal\ServerResponse; +use Mockery as m; -class ServerResponseTest extends TestCase +class ServerResponseTest extends MockeryTestCase { + /** @var Server|m\Mock */ + protected $server; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->server = m::mock(Server::class); + $this->server->shouldReceive('header')->byDefault(); + $this->server->shouldReceive('echo')->byDefault(); + } + + public function testSetStatusIsPublic() + { + $response = new ServerResponse(); + + $response->setStatus(404); + + self::assertSame(404, $response->getStatusCode()); + } + public function testSetCookieAddsHeader() { $response = new ServerResponse(); - $response->setCookie('foo', 'bar'); + $response->setCookie('foo', 'bär', 3600, '/', 'localhost', true, true, true); self::assertEquals([ - 'Set-Cookie' => ['foo=bar'] + 'Set-Cookie' => [ + 'foo=b%C3%A4r' . + '; expires=' . gmdate('r', time()+3600) . '; Max-Age=3600' . + '; path=/' . + '; domain=localhost' . + '; secure; HttpOnly; SameSite=strict', + ] ], $response->getHeaders()); } + + public function testCookieNameHasToBeValid() + { + $response = new ServerResponse(); + + self::expectException(\InvalidArgumentException::class); + self::expectExceptionMessage('Cookie names cannot contain'); + + $response->setCookie('a=b', 'foo'); + } + + public function testWithCookieChangesClone() + { + $response = new ServerResponse(); + + $clone = $response->withSetCookie('foo', 'bar'); + + self::assertNotSame($response, $clone); + self::assertEmpty($response->getHeaders()); + self::assertArrayHasKey('Set-Cookie', $clone->getHeaders()); + } + + public function testDeleteCookieHeader() + { + $response = new ServerResponse(); + + $response->deleteCookie('foo'); + + self::assertEquals([ + 'Set-Cookie' => [ + 'foo=deleted' . + '; expires=' . gmdate('r', time()-1) . '; Max-Age=0', + ] + ], $response->getHeaders()); + } + + public function testWithDeleteCookie() + { + $response = new ServerResponse(); + + $clone = $response->withDeleteCookie('foo'); + + self::assertNotSame($response, $clone); + self::assertEmpty($response->getHeaders()); + } + + public function testSendsHeaderline() + { + $response = new ServerResponse(404); + $response->setProtocolVersion('1.0'); + + $this->server->shouldReceive('header')->with('HTTP/1.0 404 Not Found', true, 404) + ->once(); + + $response->send(8192, $this->server); + } + + public function testRewindsStream() + { + $stream = m::mock(new Stream(fopen('php://memory', 'w+'))); + $response = new ServerResponse(200, [], $stream); + + $stream->shouldReceive('rewind')->with() + ->once(); + + $response->send(8192, $this->server); + } + + public function testSendsHeadersBeforeStatusLine() + { + $response = new ServerResponse(); + $response->addHeader('Content-Type', 'text/html'); + + $this->server->shouldReceive('header')->with('Content-Type: text/html', false)->once()->ordered(); + $this->server->shouldReceive('header')->with('HTTP/1.1 200 OK', true, 200)->once()->ordered(); + + $response->send(8192, $this->server); + } + + public function testMultipleHeaders() + { + $response = new ServerResponse(); + $response->addHeader('Vary', 'User-Agent'); + $response->addHeader('Vary', 'Accept'); + + $this->server->shouldReceive('header')->with('Vary: User-Agent,Accept', false) + ->once(); + + $response->send(8192, $this->server); + } + + public function testMultipleSetCookieHeaders() + { + $response = new ServerResponse(); + $response->setCookie('foo', 'bar'); + $response->setCookie('sid', 'abc'); + + $this->server->shouldReceive('header')->with(m::pattern('/^Set-Cookie: /'), false)->twice(); + + $response->send(8192, $this->server); + } } -- GitLab