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