From 35e6a680a9b1293a12d769d289c21d971af3b2b8 Mon Sep 17 00:00:00 2001
From: Thomas Flori <thflori@gmail.com>
Date: Sun, 5 Aug 2018 13:59:28 +0200
Subject: [PATCH] add tests and .travis.yml and .gitattributes

---
 .gitattributes               |   5 +
 .travis.yml                  |  23 ++
 phpunit.xml                  |  20 ++
 tests/ClientRequestTest.php  | 194 +++++++++++++
 tests/ClientResponseTest.php | 250 +++++++++++++++++
 tests/ServerRequestTest.php  | 522 +++++++++++++++++++++++++++++++++++
 tests/ServerResponseTest.php |  10 +
 7 files changed, 1024 insertions(+)
 create mode 100644 .gitattributes
 create mode 100644 .travis.yml
 create mode 100644 phpunit.xml
 create mode 100644 tests/ClientRequestTest.php
 create mode 100644 tests/ClientResponseTest.php
 create mode 100644 tests/ServerRequestTest.php
 create mode 100644 tests/ServerResponseTest.php

diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..09dfee1
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,5 @@
+/tests export-ignore
+.gitattributes export-ignore
+.gitignore export-ignore
+.travis.yml export-ignore
+phpunit.xml export-ignore
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..cae9ee3
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,23 @@
+language: php
+php:
+- 7.1
+- 7.2
+
+sudo: false
+
+cache:
+    directories:
+    - $HOME/.composer/cache
+
+matrix:
+    fast_finish: true
+
+before_script:
+- composer install --no-interaction
+- sh -c 'if [ "$TRAVIS_PHP_VERSION" = "7.1" ]; then composer require satooshi/php-coveralls:~0.6@stable; fi;'
+- mkdir -p build/logs
+
+script:
+- composer code-style
+- vendor/bin/phpunit -c phpunit.xml --coverage-clover=build/logs/clover.xml --coverage-text
+- sh -c 'if [ "$TRAVIS_PHP_VERSION" = "7.1" ]; then php vendor/bin/coveralls -v; fi;'
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..8cd9d23
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit bootstrap="vendor/autoload.php"
+         backupGlobals="true"
+         backupStaticAttributes="false"
+         verbose="true"
+         colors="true"
+         convertErrorsToExceptions="true"
+         convertNoticesToExceptions="true"
+         convertWarningsToExceptions="true"
+         processIsolation="false"
+         stopOnFailure="false">
+    <testsuite name="tests">
+        <directory>./tests/</directory>
+    </testsuite>
+    <filter>
+        <whitelist processUncoveredFilesFromWhitelist="true">
+            <directory suffix=".php">./src/</directory>
+        </whitelist>
+    </filter>
+</phpunit>
diff --git a/tests/ClientRequestTest.php b/tests/ClientRequestTest.php
new file mode 100644
index 0000000..c35d1ba
--- /dev/null
+++ b/tests/ClientRequestTest.php
@@ -0,0 +1,194 @@
+<?php
+
+namespace Tal\Test;
+
+use GuzzleHttp\Psr7;
+use PHPUnit\Framework\TestCase;
+use Tal\ClientRequest as Request;
+use GuzzleHttp\Psr7\Uri;
+
+class RequestTest extends TestCase
+{
+    public function testRequestUriMayBeString()
+    {
+        $r = new Request('GET', '/');
+        $this->assertEquals('/', (string) $r->getUri());
+    }
+
+    public function testRequestUriMayBeUri()
+    {
+        $uri = new Uri('/');
+        $r = new Request('GET', $uri);
+        $this->assertSame($uri, $r->getUri());
+    }
+
+    /**
+     * @expectedException \InvalidArgumentException
+     */
+    public function testValidateRequestUri()
+    {
+        new Request('GET', '///');
+    }
+
+    public function testCanConstructWithBody()
+    {
+        $r = new Request('GET', '/', [], 'baz');
+        $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $r->getBody());
+        $this->assertEquals('baz', (string) $r->getBody());
+    }
+
+    public function testNullBody()
+    {
+        $r = new Request('GET', '/', [], null);
+        $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $r->getBody());
+        $this->assertSame('', (string) $r->getBody());
+    }
+
+    public function testFalseyBody()
+    {
+        $r = new Request('GET', '/', [], '0');
+        $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $r->getBody());
+        $this->assertSame('0', (string) $r->getBody());
+    }
+
+    public function testConstructorDoesNotReadStreamBody()
+    {
+        $streamIsRead = false;
+        $body = Psr7\FnStream::decorate(Psr7\stream_for(''), [
+            '__toString' => function () use (&$streamIsRead) {
+                $streamIsRead = true;
+                return '';
+            }
+        ]);
+
+        $r = new Request('GET', '/', [], $body);
+        $this->assertFalse($streamIsRead);
+        $this->assertSame($body, $r->getBody());
+    }
+
+    public function testCapitalizesMethod()
+    {
+        $r = new Request('get', '/');
+        $this->assertEquals('GET', $r->getMethod());
+    }
+
+    public function testCapitalizesWithMethod()
+    {
+        $r = new Request('GET', '/');
+        $this->assertEquals('PUT', $r->withMethod('put')->getMethod());
+    }
+
+    public function testWithUri()
+    {
+        $r1 = new Request('GET', '/');
+        $u1 = $r1->getUri();
+        $u2 = new Uri('http://www.example.com');
+        $r2 = $r1->withUri($u2);
+        $this->assertNotSame($r1, $r2);
+        $this->assertSame($u2, $r2->getUri());
+        $this->assertSame($u1, $r1->getUri());
+    }
+
+    public function testSameInstanceWhenSameUri()
+    {
+        $r1 = new Request('GET', 'http://foo.com');
+        $r2 = $r1->withUri($r1->getUri());
+        $this->assertSame($r1, $r2);
+    }
+
+    public function testWithRequestTarget()
+    {
+        $r1 = new Request('GET', '/');
+        $r2 = $r1->withRequestTarget('*');
+        $this->assertEquals('*', $r2->getRequestTarget());
+        $this->assertEquals('/', $r1->getRequestTarget());
+    }
+
+    /**
+     * @expectedException \InvalidArgumentException
+     */
+    public function testRequestTargetDoesNotAllowSpaces()
+    {
+        $r1 = new Request('GET', '/');
+        $r1->withRequestTarget('/foo bar');
+    }
+
+    public function testRequestTargetDefaultsToSlash()
+    {
+        $r1 = new Request('GET', '');
+        $this->assertEquals('/', $r1->getRequestTarget());
+        $r2 = new Request('GET', '*');
+        $this->assertEquals('*', $r2->getRequestTarget());
+        $r3 = new Request('GET', 'http://foo.com/bar baz/');
+        $this->assertEquals('/bar%20baz/', $r3->getRequestTarget());
+    }
+
+    public function testBuildsRequestTarget()
+    {
+        $r1 = new Request('GET', 'http://foo.com/baz?bar=bam');
+        $this->assertEquals('/baz?bar=bam', $r1->getRequestTarget());
+    }
+
+    public function testBuildsRequestTargetWithFalseyQuery()
+    {
+        $r1 = new Request('GET', 'http://foo.com/baz?0');
+        $this->assertEquals('/baz?0', $r1->getRequestTarget());
+    }
+
+    public function testHostIsAddedFirst()
+    {
+        $r = new Request('GET', 'http://foo.com/baz?bar=bam', ['Foo' => 'Bar']);
+        $this->assertEquals([
+            'Host' => ['foo.com'],
+            'Foo'  => ['Bar']
+        ], $r->getHeaders());
+    }
+
+    public function testCanGetHeaderAsCsv()
+    {
+        $r = new Request('GET', 'http://foo.com/baz?bar=bam', [
+            'Foo' => ['a', 'b', 'c']
+        ]);
+        $this->assertEquals('a, b, c', $r->getHeaderLine('Foo'));
+        $this->assertEquals('', $r->getHeaderLine('Bar'));
+    }
+
+    public function testHostIsNotOverwrittenWhenPreservingHost()
+    {
+        $r = new Request('GET', 'http://foo.com/baz?bar=bam', ['Host' => 'a.com']);
+        $this->assertEquals(['Host' => ['a.com']], $r->getHeaders());
+        $r2 = $r->withUri(new Uri('http://www.foo.com/bar'), true);
+        $this->assertEquals('a.com', $r2->getHeaderLine('Host'));
+    }
+
+    public function testOverridesHostWithUri()
+    {
+        $r = new Request('GET', 'http://foo.com/baz?bar=bam');
+        $this->assertEquals(['Host' => ['foo.com']], $r->getHeaders());
+        $r2 = $r->withUri(new Uri('http://www.baz.com/bar'));
+        $this->assertEquals('www.baz.com', $r2->getHeaderLine('Host'));
+    }
+
+    public function testAggregatesHeaders()
+    {
+        $r = new Request('GET', '', [
+            'ZOO' => 'zoobar',
+            'zoo' => ['foobar', 'zoobar']
+        ]);
+        $this->assertEquals(['ZOO' => ['zoobar', 'foobar', 'zoobar']], $r->getHeaders());
+        $this->assertEquals('zoobar, foobar, zoobar', $r->getHeaderLine('zoo'));
+    }
+
+    public function testAddsPortToHeader()
+    {
+        $r = new Request('GET', 'http://foo.com:8124/bar');
+        $this->assertEquals('foo.com:8124', $r->getHeaderLine('host'));
+    }
+
+    public function testAddsPortToHeaderAndReplacePreviousPort()
+    {
+        $r = new Request('GET', 'http://foo.com:8124/bar');
+        $r = $r->withUri(new Uri('http://foo.com:8125/bar'));
+        $this->assertEquals('foo.com:8125', $r->getHeaderLine('host'));
+    }
+}
diff --git a/tests/ClientResponseTest.php b/tests/ClientResponseTest.php
new file mode 100644
index 0000000..0c30ecc
--- /dev/null
+++ b/tests/ClientResponseTest.php
@@ -0,0 +1,250 @@
+<?php
+
+namespace Tal\Test;
+
+use GuzzleHttp\Psr7;
+use PHPUnit\Framework\TestCase;
+use Tal\ClientResponse as Response;
+
+class ClientResponseTest extends TestCase
+{
+    public function testDefaultConstructor()
+    {
+        $r = new Response();
+        $this->assertSame(200, $r->getStatusCode());
+        $this->assertSame('1.1', $r->getProtocolVersion());
+        $this->assertSame('OK', $r->getReasonPhrase());
+        $this->assertSame([], $r->getHeaders());
+        $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $r->getBody());
+        $this->assertSame('', (string) $r->getBody());
+    }
+
+    public function testCanConstructWithStatusCode()
+    {
+        $r = new Response(404);
+        $this->assertSame(404, $r->getStatusCode());
+        $this->assertSame('Not Found', $r->getReasonPhrase());
+    }
+
+    public function testConstructorDoesNotReadStreamBody()
+    {
+        $streamIsRead = false;
+        $body = Psr7\FnStream::decorate(Psr7\stream_for(''), [
+            '__toString' => function () use (&$streamIsRead) {
+                $streamIsRead = true;
+                return '';
+            }
+        ]);
+
+        $r = new Response(200, [], $body);
+        $this->assertFalse($streamIsRead);
+        $this->assertSame($body, $r->getBody());
+    }
+
+    public function testStatusCanBeNumericString()
+    {
+        $r = new Response('404');
+        $r2 = $r->withStatus('201');
+        $this->assertSame(404, $r->getStatusCode());
+        $this->assertSame('Not Found', $r->getReasonPhrase());
+        $this->assertSame(201, $r2->getStatusCode());
+        $this->assertSame('Created', $r2->getReasonPhrase());
+    }
+
+    public function testCanConstructWithHeaders()
+    {
+        $r = new Response(200, ['Foo' => 'Bar']);
+        $this->assertSame(['Foo' => ['Bar']], $r->getHeaders());
+        $this->assertSame('Bar', $r->getHeaderLine('Foo'));
+        $this->assertSame(['Bar'], $r->getHeader('Foo'));
+    }
+
+    public function testCanConstructWithHeadersAsArray()
+    {
+        $r = new Response(200, [
+            'Foo' => ['baz', 'bar']
+        ]);
+        $this->assertSame(['Foo' => ['baz', 'bar']], $r->getHeaders());
+        $this->assertSame('baz, bar', $r->getHeaderLine('Foo'));
+        $this->assertSame(['baz', 'bar'], $r->getHeader('Foo'));
+    }
+
+    public function testCanConstructWithBody()
+    {
+        $r = new Response(200, [], 'baz');
+        $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $r->getBody());
+        $this->assertSame('baz', (string) $r->getBody());
+    }
+
+    public function testNullBody()
+    {
+        $r = new Response(200, [], null);
+        $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $r->getBody());
+        $this->assertSame('', (string) $r->getBody());
+    }
+
+    public function testFalseyBody()
+    {
+        $r = new Response(200, [], '0');
+        $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $r->getBody());
+        $this->assertSame('0', (string) $r->getBody());
+    }
+
+    public function testCanConstructWithReason()
+    {
+        $r = new Response(200, [], null, '1.1', 'bar');
+        $this->assertSame('bar', $r->getReasonPhrase());
+
+        $r = new Response(200, [], null, '1.1', '0');
+        $this->assertSame('0', $r->getReasonPhrase(), 'Falsey reason works');
+    }
+
+    public function testCanConstructWithProtocolVersion()
+    {
+        $r = new Response(200, [], null, '1000');
+        $this->assertSame('1000', $r->getProtocolVersion());
+    }
+
+    public function testWithStatusCodeAndNoReason()
+    {
+        $r = (new Response())->withStatus(201);
+        $this->assertSame(201, $r->getStatusCode());
+        $this->assertSame('Created', $r->getReasonPhrase());
+    }
+
+    public function testWithStatusCodeAndReason()
+    {
+        $r = (new Response())->withStatus(201, 'Foo');
+        $this->assertSame(201, $r->getStatusCode());
+        $this->assertSame('Foo', $r->getReasonPhrase());
+
+        $r = (new Response())->withStatus(201, '0');
+        $this->assertSame(201, $r->getStatusCode());
+        $this->assertSame('0', $r->getReasonPhrase(), 'Falsey reason works');
+    }
+
+    public function testWithProtocolVersion()
+    {
+        $r = (new Response())->withProtocolVersion('1000');
+        $this->assertSame('1000', $r->getProtocolVersion());
+    }
+
+    public function testSameInstanceWhenSameProtocol()
+    {
+        $r = new Response();
+        $this->assertSame($r, $r->withProtocolVersion('1.1'));
+    }
+
+    public function testWithBody()
+    {
+        $b = Psr7\stream_for('0');
+        $r = (new Response())->withBody($b);
+        $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $r->getBody());
+        $this->assertSame('0', (string) $r->getBody());
+    }
+
+    public function testSameInstanceWhenSameBody()
+    {
+        $r = new Response();
+        $b = $r->getBody();
+        $this->assertSame($r, $r->withBody($b));
+    }
+
+    public function testWithHeader()
+    {
+        $r = new Response(200, ['Foo' => 'Bar']);
+        $r2 = $r->withHeader('baZ', 'Bam');
+        $this->assertSame(['Foo' => ['Bar']], $r->getHeaders());
+        $this->assertSame(['Foo' => ['Bar'], 'baZ' => ['Bam']], $r2->getHeaders());
+        $this->assertSame('Bam', $r2->getHeaderLine('baz'));
+        $this->assertSame(['Bam'], $r2->getHeader('baz'));
+    }
+
+    public function testWithHeaderAsArray()
+    {
+        $r = new Response(200, ['Foo' => 'Bar']);
+        $r2 = $r->withHeader('baZ', ['Bam', 'Bar']);
+        $this->assertSame(['Foo' => ['Bar']], $r->getHeaders());
+        $this->assertSame(['Foo' => ['Bar'], 'baZ' => ['Bam', 'Bar']], $r2->getHeaders());
+        $this->assertSame('Bam, Bar', $r2->getHeaderLine('baz'));
+        $this->assertSame(['Bam', 'Bar'], $r2->getHeader('baz'));
+    }
+
+    public function testWithHeaderReplacesDifferentCase()
+    {
+        $r = new Response(200, ['Foo' => 'Bar']);
+        $r2 = $r->withHeader('foO', 'Bam');
+        $this->assertSame(['Foo' => ['Bar']], $r->getHeaders());
+        $this->assertSame(['foO' => ['Bam']], $r2->getHeaders());
+        $this->assertSame('Bam', $r2->getHeaderLine('foo'));
+        $this->assertSame(['Bam'], $r2->getHeader('foo'));
+    }
+
+    public function testWithAddedHeader()
+    {
+        $r = new Response(200, ['Foo' => 'Bar']);
+        $r2 = $r->withAddedHeader('foO', 'Baz');
+        $this->assertSame(['Foo' => ['Bar']], $r->getHeaders());
+        $this->assertSame(['Foo' => ['Bar', 'Baz']], $r2->getHeaders());
+        $this->assertSame('Bar, Baz', $r2->getHeaderLine('foo'));
+        $this->assertSame(['Bar', 'Baz'], $r2->getHeader('foo'));
+    }
+
+    public function testWithAddedHeaderAsArray()
+    {
+        $r = new Response(200, ['Foo' => 'Bar']);
+        $r2 = $r->withAddedHeader('foO', ['Baz', 'Bam']);
+        $this->assertSame(['Foo' => ['Bar']], $r->getHeaders());
+        $this->assertSame(['Foo' => ['Bar', 'Baz', 'Bam']], $r2->getHeaders());
+        $this->assertSame('Bar, Baz, Bam', $r2->getHeaderLine('foo'));
+        $this->assertSame(['Bar', 'Baz', 'Bam'], $r2->getHeader('foo'));
+    }
+
+    public function testWithAddedHeaderThatDoesNotExist()
+    {
+        $r = new Response(200, ['Foo' => 'Bar']);
+        $r2 = $r->withAddedHeader('nEw', 'Baz');
+        $this->assertSame(['Foo' => ['Bar']], $r->getHeaders());
+        $this->assertSame(['Foo' => ['Bar'], 'nEw' => ['Baz']], $r2->getHeaders());
+        $this->assertSame('Baz', $r2->getHeaderLine('new'));
+        $this->assertSame(['Baz'], $r2->getHeader('new'));
+    }
+
+    public function testWithoutHeaderThatExists()
+    {
+        $r = new Response(200, ['Foo' => 'Bar', 'Baz' => 'Bam']);
+        $r2 = $r->withoutHeader('foO');
+        $this->assertTrue($r->hasHeader('foo'));
+        $this->assertSame(['Foo' => ['Bar'], 'Baz' => ['Bam']], $r->getHeaders());
+        $this->assertFalse($r2->hasHeader('foo'));
+        $this->assertSame(['Baz' => ['Bam']], $r2->getHeaders());
+    }
+
+    public function testWithoutHeaderThatDoesNotExist()
+    {
+        $r = new Response(200, ['Baz' => 'Bam']);
+        $r2 = $r->withoutHeader('foO');
+        $this->assertSame($r, $r2);
+        $this->assertFalse($r2->hasHeader('foo'));
+        $this->assertSame(['Baz' => ['Bam']], $r2->getHeaders());
+    }
+
+    public function testSameInstanceWhenRemovingMissingHeader()
+    {
+        $r = new Response();
+        $this->assertSame($r, $r->withoutHeader('foo'));
+    }
+
+    public function testHeaderValuesAreTrimmed()
+    {
+        $r1 = new Response(200, ['OWS' => " \t \tFoo\t \t "]);
+        $r2 = (new Response())->withHeader('OWS', " \t \tFoo\t \t ");
+        $r3 = (new Response())->withAddedHeader('OWS', " \t \tFoo\t \t ");;
+
+        foreach ([$r1, $r2, $r3] as $r) {
+            $this->assertSame(['OWS' => ['Foo']], $r->getHeaders());
+            $this->assertSame('Foo', $r->getHeaderLine('OWS'));
+            $this->assertSame(['Foo'], $r->getHeader('OWS'));
+        }
+    }
+}
diff --git a/tests/ServerRequestTest.php b/tests/ServerRequestTest.php
new file mode 100644
index 0000000..9e30186
--- /dev/null
+++ b/tests/ServerRequestTest.php
@@ -0,0 +1,522 @@
+<?php
+
+namespace Tal\Test;
+
+use PHPUnit\Framework\TestCase;
+use Tal\ServerRequest;
+use GuzzleHttp\Psr7\UploadedFile;
+use GuzzleHttp\Psr7\Uri;
+
+class ServerRequestTest extends TestCase
+{
+    public function dataNormalizeFiles()
+    {
+        return [
+            'Single file' => [
+                [
+                    'file' => [
+                        'name' => 'MyFile.txt',
+                        'type' => 'text/plain',
+                        'tmp_name' => '/tmp/php/php1h4j1o',
+                        'error' => '0',
+                        'size' => '123'
+                    ]
+                ],
+                [
+                    'file' => new UploadedFile(
+                        '/tmp/php/php1h4j1o',
+                        123,
+                        UPLOAD_ERR_OK,
+                        'MyFile.txt',
+                        'text/plain'
+                    )
+                ]
+            ],
+            'Empty file' => [
+                [
+                    'image_file' => [
+                        'name' => '',
+                        'type' => '',
+                        'tmp_name' => '',
+                        'error' => '4',
+                        'size' => '0'
+                    ]
+                ],
+                [
+                    'image_file' => new UploadedFile(
+                        '',
+                        0,
+                        UPLOAD_ERR_NO_FILE,
+                        '',
+                        ''
+                    )
+                ]
+            ],
+            'Already Converted' => [
+                [
+                    'file' => new UploadedFile(
+                        '/tmp/php/php1h4j1o',
+                        123,
+                        UPLOAD_ERR_OK,
+                        'MyFile.txt',
+                        'text/plain'
+                    )
+                ],
+                [
+                    'file' => new UploadedFile(
+                        '/tmp/php/php1h4j1o',
+                        123,
+                        UPLOAD_ERR_OK,
+                        'MyFile.txt',
+                        'text/plain'
+                    )
+                ]
+            ],
+            'Already Converted array' => [
+                [
+                    'file' => [
+                        new UploadedFile(
+                            '/tmp/php/php1h4j1o',
+                            123,
+                            UPLOAD_ERR_OK,
+                            'MyFile.txt',
+                            'text/plain'
+                        ),
+                        new UploadedFile(
+                            '',
+                            0,
+                            UPLOAD_ERR_NO_FILE,
+                            '',
+                            ''
+                        )
+                    ],
+                ],
+                [
+                    'file' => [
+                        new UploadedFile(
+                            '/tmp/php/php1h4j1o',
+                            123,
+                            UPLOAD_ERR_OK,
+                            'MyFile.txt',
+                            'text/plain'
+                        ),
+                        new UploadedFile(
+                            '',
+                            0,
+                            UPLOAD_ERR_NO_FILE,
+                            '',
+                            ''
+                        )
+                    ],
+                ]
+            ],
+            'Multiple files' => [
+                [
+                    'text_file' => [
+                        'name' => 'MyFile.txt',
+                        'type' => 'text/plain',
+                        'tmp_name' => '/tmp/php/php1h4j1o',
+                        'error' => '0',
+                        'size' => '123'
+                    ],
+                    'image_file' => [
+                        'name' => '',
+                        'type' => '',
+                        'tmp_name' => '',
+                        'error' => '4',
+                        'size' => '0'
+                    ]
+                ],
+                [
+                    'text_file' => new UploadedFile(
+                        '/tmp/php/php1h4j1o',
+                        123,
+                        UPLOAD_ERR_OK,
+                        'MyFile.txt',
+                        'text/plain'
+                    ),
+                    'image_file' => new UploadedFile(
+                        '',
+                        0,
+                        UPLOAD_ERR_NO_FILE,
+                        '',
+                        ''
+                    )
+                ]
+            ],
+            'Nested files' => [
+                [
+                    'file' => [
+                        'name' => [
+                            0 => 'MyFile.txt',
+                            1 => 'Image.png',
+                        ],
+                        'type' => [
+                            0 => 'text/plain',
+                            1 => 'image/png',
+                        ],
+                        'tmp_name' => [
+                            0 => '/tmp/php/hp9hskjhf',
+                            1 => '/tmp/php/php1h4j1o',
+                        ],
+                        'error' => [
+                            0 => '0',
+                            1 => '0',
+                        ],
+                        'size' => [
+                            0 => '123',
+                            1 => '7349',
+                        ],
+                    ],
+                    'nested' => [
+                        'name' => [
+                            'other' => 'Flag.txt',
+                            'test' => [
+                                0 => 'Stuff.txt',
+                                1 => '',
+                            ],
+                        ],
+                        'type' => [
+                            'other' => 'text/plain',
+                            'test' => [
+                                0 => 'text/plain',
+                                1 => '',
+                            ],
+                        ],
+                        'tmp_name' => [
+                            'other' => '/tmp/php/hp9hskjhf',
+                            'test' => [
+                                0 => '/tmp/php/asifu2gp3',
+                                1 => '',
+                            ],
+                        ],
+                        'error' => [
+                            'other' => '0',
+                            'test' => [
+                                0 => '0',
+                                1 => '4',
+                            ],
+                        ],
+                        'size' => [
+                            'other' => '421',
+                            'test' => [
+                                0 => '32',
+                                1 => '0',
+                            ]
+                        ]
+                    ],
+                ],
+                [
+                    'file' => [
+                        0 => new UploadedFile(
+                            '/tmp/php/hp9hskjhf',
+                            123,
+                            UPLOAD_ERR_OK,
+                            'MyFile.txt',
+                            'text/plain'
+                        ),
+                        1 => new UploadedFile(
+                            '/tmp/php/php1h4j1o',
+                            7349,
+                            UPLOAD_ERR_OK,
+                            'Image.png',
+                            'image/png'
+                        ),
+                    ],
+                    'nested' => [
+                        'other' => new UploadedFile(
+                            '/tmp/php/hp9hskjhf',
+                            421,
+                            UPLOAD_ERR_OK,
+                            'Flag.txt',
+                            'text/plain'
+                        ),
+                        'test' => [
+                            0 => new UploadedFile(
+                                '/tmp/php/asifu2gp3',
+                                32,
+                                UPLOAD_ERR_OK,
+                                'Stuff.txt',
+                                'text/plain'
+                            ),
+                            1 => new UploadedFile(
+                                '',
+                                0,
+                                UPLOAD_ERR_NO_FILE,
+                                '',
+                                ''
+                            ),
+                        ]
+                    ]
+                ]
+            ]
+        ];
+    }
+
+    /**
+     * @dataProvider dataNormalizeFiles
+     */
+    public function testNormalizeFiles($files, $expected)
+    {
+        $result = ServerRequest::normalizeFiles($files);
+
+        $this->assertEquals($expected, $result);
+    }
+
+    public function testNormalizeFilesRaisesException()
+    {
+        self::expectException('InvalidArgumentException');
+        self::expectExceptionMessage('Invalid value in files specification');
+
+        ServerRequest::normalizeFiles(['test' => 'something']);
+    }
+
+    public function dataGetUriFromGlobals()
+    {
+        $server = [
+            'REQUEST_URI' => '/blog/article.php?id=10&user=foo',
+            'SERVER_PORT' => '443',
+            'SERVER_ADDR' => '217.112.82.20',
+            'SERVER_NAME' => 'www.example.org',
+            'SERVER_PROTOCOL' => 'HTTP/1.1',
+            'REQUEST_METHOD' => 'POST',
+            'QUERY_STRING' => 'id=10&user=foo',
+            'DOCUMENT_ROOT' => '/path/to/your/server/root/',
+            'HTTP_HOST' => 'www.example.org',
+            'HTTPS' => 'on',
+            'REMOTE_ADDR' => '193.60.168.69',
+            'REMOTE_PORT' => '5390',
+            'SCRIPT_NAME' => '/blog/article.php',
+            'SCRIPT_FILENAME' => '/path/to/your/server/root/blog/article.php',
+            'PHP_SELF' => '/blog/article.php',
+        ];
+
+        return [
+            'HTTPS request' => [
+                'https://www.example.org/blog/article.php?id=10&user=foo',
+                $server,
+            ],
+            'HTTPS request with different on value' => [
+                'https://www.example.org/blog/article.php?id=10&user=foo',
+                array_merge($server, ['HTTPS' => '1']),
+            ],
+            'HTTP request' => [
+                'http://www.example.org/blog/article.php?id=10&user=foo',
+                array_merge($server, ['HTTPS' => 'off', 'SERVER_PORT' => '80']),
+            ],
+            'HTTP_HOST missing -> fallback to SERVER_NAME' => [
+                'https://www.example.org/blog/article.php?id=10&user=foo',
+                array_merge($server, ['HTTP_HOST' => null]),
+            ],
+            'HTTP_HOST and SERVER_NAME missing -> fallback to SERVER_ADDR' => [
+                'https://217.112.82.20/blog/article.php?id=10&user=foo',
+                array_merge($server, ['HTTP_HOST' => null, 'SERVER_NAME' => null]),
+            ],
+            'No query String' => [
+                'https://www.example.org/blog/article.php',
+                array_merge($server, ['REQUEST_URI' => '/blog/article.php', 'QUERY_STRING' => '']),
+            ],
+            'Host header with port' => [
+                'https://www.example.org:8324/blog/article.php?id=10&user=foo',
+                array_merge($server, ['HTTP_HOST' => 'www.example.org:8324']),
+            ],
+            'Different port with SERVER_PORT' => [
+                'https://www.example.org:8324/blog/article.php?id=10&user=foo',
+                array_merge($server, ['SERVER_PORT' => '8324']),
+            ],
+            'REQUEST_URI missing query string' => [
+                'https://www.example.org/blog/article.php?id=10&user=foo',
+                array_merge($server, ['REQUEST_URI' => '/blog/article.php']),
+            ],
+            'Empty server variable' => [
+                'http://localhost',
+                [],
+            ],
+        ];
+    }
+
+    /**
+     * @dataProvider dataGetUriFromGlobals
+     */
+    public function testGetUriFromGlobals($expected, $serverParams)
+    {
+        $_SERVER = $serverParams;
+
+        $this->assertEquals(new Uri($expected), ServerRequest::getUriFromGlobals());
+    }
+
+    public function testFromGlobals()
+    {
+        $_SERVER = [
+            'REQUEST_URI' => '/blog/article.php?id=10&user=foo',
+            'SERVER_PORT' => '443',
+            'SERVER_ADDR' => '217.112.82.20',
+            'SERVER_NAME' => 'www.example.org',
+            'SERVER_PROTOCOL' => 'HTTP/1.1',
+            'REQUEST_METHOD' => 'POST',
+            'QUERY_STRING' => 'id=10&user=foo',
+            'DOCUMENT_ROOT' => '/path/to/your/server/root/',
+            'HTTP_HOST' => 'www.example.org',
+            'HTTPS' => 'on',
+            'REMOTE_ADDR' => '193.60.168.69',
+            'REMOTE_PORT' => '5390',
+            'SCRIPT_NAME' => '/blog/article.php',
+            'SCRIPT_FILENAME' => '/path/to/your/server/root/blog/article.php',
+            'PHP_SELF' => '/blog/article.php',
+        ];
+
+        $_COOKIE = [
+            'logged-in' => 'yes!'
+        ];
+
+        $_POST = [
+            'name' => 'Pesho',
+            'email' => 'pesho@example.com',
+        ];
+
+        $_GET = [
+            'id' => 10,
+            'user' => 'foo',
+        ];
+
+        $_FILES = [
+            'file' => [
+                'name' => 'MyFile.txt',
+                'type' => 'text/plain',
+                'tmp_name' => '/tmp/php/php1h4j1o',
+                'error' => UPLOAD_ERR_OK,
+                'size' => 123,
+            ]
+        ];
+
+        $server = ServerRequest::fromGlobals();
+
+        $this->assertSame('POST', $server->getMethod());
+        $this->assertEquals(['Host' => ['www.example.org']], $server->getHeaders());
+        $this->assertSame('', (string) $server->getBody());
+        $this->assertSame('1.1', $server->getProtocolVersion());
+        $this->assertEquals($_COOKIE, $server->getCookieParams());
+        $this->assertEquals($_POST, $server->getParsedBody());
+        $this->assertEquals($_GET, $server->getQueryParams());
+
+        $this->assertEquals(
+            new Uri('https://www.example.org/blog/article.php?id=10&user=foo'),
+            $server->getUri()
+        );
+
+        $expectedFiles = [
+            'file' => new UploadedFile(
+                '/tmp/php/php1h4j1o',
+                123,
+                UPLOAD_ERR_OK,
+                'MyFile.txt',
+                'text/plain'
+            ),
+        ];
+
+        $this->assertEquals($expectedFiles, $server->getUploadedFiles());
+    }
+
+    public function testUploadedFiles()
+    {
+        $request1 = new ServerRequest('GET', '/');
+
+        $files = [
+            'file' => new UploadedFile('test', 123, UPLOAD_ERR_OK)
+        ];
+
+        $request2 = $request1->withUploadedFiles($files);
+
+        $this->assertNotSame($request2, $request1);
+        $this->assertSame([], $request1->getUploadedFiles());
+        $this->assertSame($files, $request2->getUploadedFiles());
+    }
+
+    public function testServerParams()
+    {
+        $params = ['name' => 'value'];
+
+        $request = new ServerRequest('GET', '/', [], null, '1.1', $params);
+        $this->assertSame($params, $request->getServerParams());
+    }
+
+    public function testCookieParams()
+    {
+        $request1 = new ServerRequest('GET', '/');
+
+        $params = ['name' => 'value'];
+
+        $request2 = $request1->withCookieParams($params);
+
+        $this->assertNotSame($request2, $request1);
+        $this->assertEmpty($request1->getCookieParams());
+        $this->assertSame($params, $request2->getCookieParams());
+    }
+
+    public function testQueryParams()
+    {
+        $request1 = new ServerRequest('GET', '/');
+
+        $params = ['name' => 'value'];
+
+        $request2 = $request1->withQueryParams($params);
+
+        $this->assertNotSame($request2, $request1);
+        $this->assertEmpty($request1->getQueryParams());
+        $this->assertSame($params, $request2->getQueryParams());
+    }
+
+    public function testParsedBody()
+    {
+        $request1 = new ServerRequest('GET', '/');
+
+        $params = ['name' => 'value'];
+
+        $request2 = $request1->withParsedBody($params);
+
+        $this->assertNotSame($request2, $request1);
+        $this->assertEmpty($request1->getParsedBody());
+        $this->assertSame($params, $request2->getParsedBody());
+    }
+
+    public function testAttributes()
+    {
+        $request1 = new ServerRequest('GET', '/');
+
+        $request2 = $request1->withAttribute('name', 'value');
+        $request3 = $request2->withAttribute('other', 'otherValue');
+        $request4 = $request3->withoutAttribute('other');
+        $request5 = $request3->withoutAttribute('unknown');
+
+        $this->assertNotSame($request2, $request1);
+        $this->assertNotSame($request3, $request2);
+        $this->assertNotSame($request4, $request3);
+        $this->assertSame($request5, $request3);
+
+        $this->assertSame([], $request1->getAttributes());
+        $this->assertNull($request1->getAttribute('name'));
+        $this->assertSame(
+            'something',
+            $request1->getAttribute('name', 'something'),
+            'Should return the default value'
+        );
+
+        $this->assertSame('value', $request2->getAttribute('name'));
+        $this->assertSame(['name' => 'value'], $request2->getAttributes());
+        $this->assertEquals(['name' => 'value', 'other' => 'otherValue'], $request3->getAttributes());
+        $this->assertSame(['name' => 'value'], $request4->getAttributes());
+    }
+
+    public function testNullAttribute()
+    {
+        $request = (new ServerRequest('GET', '/'))->withAttribute('name', null);
+
+        $this->assertSame(['name' => null], $request->getAttributes());
+        $this->assertNull($request->getAttribute('name', 'different-default'));
+
+        $requestWithoutAttribute = $request->withoutAttribute('name');
+
+        $this->assertSame([], $requestWithoutAttribute->getAttributes());
+        $this->assertSame('different-default', $requestWithoutAttribute->getAttribute('name', 'different-default'));
+    }
+}
diff --git a/tests/ServerResponseTest.php b/tests/ServerResponseTest.php
new file mode 100644
index 0000000..3057e3c
--- /dev/null
+++ b/tests/ServerResponseTest.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Tal\Test;
+
+use PHPUnit\Framework\TestCase;
+
+class ServerResponseTest extends TestCase
+{
+
+}
-- 
GitLab