-
-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
SapiEmitter now emits streaming (#602)
- Loading branch information
Showing
9 changed files
with
605 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Spiral\Tests\Http\SapiEmitter; | ||
|
||
include 'Support/httpFunctionMocks.php'; | ||
|
||
use Nyholm\Psr7\Response; | ||
use PHPUnit\Framework\TestCase; | ||
use Psr\Http\Message\ResponseInterface; | ||
use Psr\Http\Message\StreamInterface; | ||
use Spiral\Http\Exception\EmitterException; | ||
use Spiral\Http\Emitter\SapiEmitter; | ||
use Spiral\Tests\Http\SapiEmitter\Support\HTTPFunctions; | ||
use Spiral\Tests\Http\SapiEmitter\Support\NotReadableStream; | ||
|
||
/** | ||
* @runInSeparateProcess | ||
*/ | ||
final class SapiEmitterTest extends TestCase | ||
{ | ||
public function setUp(): void | ||
{ | ||
HTTPFunctions::reset(); | ||
} | ||
|
||
public static function tearDownAfterClass(): void | ||
{ | ||
HTTPFunctions::reset(); | ||
} | ||
|
||
public function testEmit(): void | ||
{ | ||
$body = 'Example body'; | ||
$response = $this->createResponse(200, ['X-Test' => 1], $body); | ||
|
||
$this->createEmitter()->emit($response); | ||
|
||
$this->assertEquals(200, $this->getResponseCode()); | ||
$this->assertContains('X-Test: 1', $this->getHeaders()); | ||
// $this->assertContains('Content-Length: ' . strlen($body), $this->getHeaders()); | ||
$this->expectOutputString($body); | ||
} | ||
|
||
public function testEmitterWithNotReadableStream(): void | ||
{ | ||
$body = new NotReadableStream(); | ||
$response = $this->createResponse(200, ['X-Test' => 42], $body); | ||
|
||
$this->createEmitter()->emit($response); | ||
|
||
$this->assertEquals(200, $this->getResponseCode()); | ||
$this->assertCount(1, $this->getHeaders()); | ||
$this->assertContains('X-Test: 42', $this->getHeaders()); | ||
} | ||
|
||
public function testContentLengthNotOverwrittenIfPresent(): void | ||
{ | ||
$length = 100; | ||
$response = $this->createResponse(200, ['Content-Length' => $length, 'X-Test' => 1], 'Example body'); | ||
|
||
$this->createEmitter()->emit($response); | ||
|
||
$this->assertEquals(200, $this->getResponseCode()); | ||
$this->assertCount(2, $this->getHeaders()); | ||
$this->assertContains('X-Test: 1', $this->getHeaders()); | ||
$this->assertContains('Content-Length: ' . $length, $this->getHeaders()); | ||
$this->expectOutputString('Example body'); | ||
} | ||
|
||
public function testContentFullyEmitted(): void | ||
{ | ||
$body = 'Example body'; | ||
$response = $this->createResponse(200, ['Content-length' => 1, 'X-Test' => 1], $body); | ||
|
||
$this->createEmitter()->emit($response); | ||
|
||
$this->expectOutputString($body); | ||
} | ||
|
||
public function testSentHeadersRemoved(): void | ||
{ | ||
HTTPFunctions::header('Cookie-Set: First Cookie'); | ||
HTTPFunctions::header('X-Test: 1'); | ||
$body = 'Example body'; | ||
$response = $this->createResponse(200, [], $body); | ||
|
||
$this->createEmitter()->emit($response); | ||
|
||
// $this->assertEquals(['Content-Length: ' . strlen($body)], $this->getHeaders()); | ||
$this->assertEquals([ | ||
'Cookie-Set: First Cookie', | ||
'X-Test: 1', | ||
// 'Content-Length: ' . strlen($body) | ||
], $this->getHeaders()); | ||
$this->expectOutputString($body); | ||
} | ||
|
||
public function testExceptionWhenHeadersHaveBeenSent(): void | ||
{ | ||
$body = 'Example body'; | ||
$response = $this->createResponse(200, [], $body); | ||
HTTPFunctions::set_headers_sent(true, 'test-file.php', 200); | ||
|
||
$this->expectException(EmitterException::class); | ||
$this->expectExceptionMessage('Unable to emit response, headers already send.'); | ||
|
||
$this->createEmitter()->emit($response); | ||
} | ||
|
||
public function testExceptionBufferWithData(): void | ||
{ | ||
ob_start(); | ||
$body = 'Example body'; | ||
$response = $this->createResponse(200, [], $body); | ||
|
||
$this->expectException(EmitterException::class); | ||
$this->expectExceptionMessage('Unable to emit response, found non closed buffered output.'); | ||
|
||
try { | ||
echo 'some data'; | ||
$this->createEmitter()->emit($response); | ||
} catch (\Throwable $e) { | ||
throw $e; | ||
} finally { | ||
\ob_end_clean(); | ||
} | ||
} | ||
|
||
public function testEmitDuplicateHeaders(): void | ||
{ | ||
$body = 'Example body'; | ||
$response = $this->createResponse(200, [], $body) | ||
->withHeader('X-Test', '1') | ||
->withAddedHeader('X-Test', '2') | ||
->withAddedHeader('X-Test', '3; 3.5') | ||
->withHeader('Cookie-Set', '1') | ||
->withAddedHeader('cookie-Set', '2') | ||
->withAddedHeader('Cookie-set', '3'); | ||
|
||
(new SapiEmitter())->emit($response); | ||
$this->assertEquals(200, $this->getResponseCode()); | ||
$this->assertContains('X-Test: 1', $this->getHeaders()); | ||
$this->assertContains('X-Test: 2', $this->getHeaders()); | ||
$this->assertContains('X-Test: 3; 3.5', $this->getHeaders()); | ||
$this->assertContains('Cookie-Set: 1', $this->getHeaders()); | ||
$this->assertContains('Cookie-Set: 2', $this->getHeaders()); | ||
$this->assertContains('Cookie-Set: 3', $this->getHeaders()); | ||
// $this->assertContains('Content-Length: ' . strlen($body), $this->getHeaders()); | ||
$this->expectOutputString($body); | ||
} | ||
|
||
private function createEmitter(?int $bufferSize = null): SapiEmitter | ||
{ | ||
$emitter = new SapiEmitter(); | ||
if ($bufferSize !== null) { | ||
$emitter->bufferSize = $bufferSize; | ||
} | ||
return $emitter; | ||
} | ||
|
||
private function createResponse( | ||
int $status = 200, | ||
array $headers = [], | ||
$body = null, | ||
string $version = '1.1' | ||
): ResponseInterface { | ||
$response = (new Response()) | ||
->withStatus($status) | ||
->withProtocolVersion($version); | ||
foreach ($headers as $header => $value) { | ||
$response = $response->withHeader($header, $value); | ||
} | ||
if ($body instanceof StreamInterface) { | ||
$response = $response->withBody($body); | ||
} elseif (is_string($body)) { | ||
$response->getBody()->write($body); | ||
} | ||
return $response; | ||
} | ||
|
||
private function getHeaders(): array | ||
{ | ||
return HTTPFunctions::headers_list(); | ||
} | ||
|
||
private function getResponseCode(): int | ||
{ | ||
return HTTPFunctions::http_response_code(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
<?php | ||
/** | ||
* | ||
* This class used to override some header*() functions and http_response_code() | ||
* | ||
* We put these into the SapiEmitter namespace, so that SapiEmitter will use these versions of header*() and | ||
* http_response_code() when we test its output. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Spiral\Tests\Http\SapiEmitter\Support; | ||
|
||
/** | ||
* @source https://github.com/yiisoft/yii-web/blob/master/tests/Emitter/Support/HTTPFunctions.php | ||
* @license MIT | ||
* @copyright Yii Software LLC (http://www.yiisoft.com) All rights reserved. | ||
*/ | ||
class HTTPFunctions | ||
{ | ||
/** @var string[][] */ | ||
private static $headers = []; | ||
/** @var int */ | ||
private static $responseCode = 200; | ||
private static $headersSent = false; | ||
private static string $headersSentFile = ''; | ||
private static int $headersSentLine = 0; | ||
|
||
/** | ||
* Reset state | ||
*/ | ||
public static function reset(): void | ||
{ | ||
self::$headers = []; | ||
self::$responseCode = 200; | ||
self::$headersSent = false; | ||
self::$headersSentFile = ''; | ||
self::$headersSentLine = 0; | ||
} | ||
|
||
/** | ||
* Set header_sent() state | ||
*/ | ||
public static function set_headers_sent(bool $value = false, string $file = '', int $line = 0): void | ||
{ | ||
static::$headersSent = $value; | ||
static::$headersSentFile = $file; | ||
static::$headersSentLine = $line; | ||
} | ||
|
||
/** | ||
* Check if headers have been sent | ||
*/ | ||
public static function headers_sent(&$file = null, &$line = null): bool | ||
{ | ||
$file = static::$headersSentFile; | ||
$line = static::$headersSentLine; | ||
return static::$headersSent; | ||
} | ||
|
||
/** | ||
* Send a raw HTTP header | ||
*/ | ||
public static function header(string $string, bool $replace = true, ?int $http_response_code = null): void | ||
{ | ||
if (strpos($string, 'HTTP/') !== 0) { | ||
$header = strtolower(explode(':', $string, 2)[0]); | ||
if ($replace || !array_key_exists($header, self::$headers)) { | ||
self::$headers[$header] = []; | ||
} | ||
self::$headers[$header][] = $string; | ||
} | ||
if ($http_response_code !== null) { | ||
self::$responseCode = $http_response_code; | ||
} | ||
} | ||
|
||
/** | ||
* Remove previously set headers | ||
*/ | ||
public static function header_remove(?string $header = null): void | ||
{ | ||
if ($header === null) { | ||
self::$headers = []; | ||
} else { | ||
unset(self::$headers[strtolower($header)]); | ||
} | ||
} | ||
|
||
/** | ||
* Returns a list of response headers sent | ||
* | ||
* @return string[] | ||
*/ | ||
public static function headers_list(): array | ||
{ | ||
$result = []; | ||
foreach (self::$headers as $values) { | ||
foreach ($values as $header) { | ||
$result[] = $header; | ||
} | ||
} | ||
return $result; | ||
} | ||
|
||
/** | ||
* Get or Set the HTTP response code | ||
*/ | ||
public static function http_response_code(?int $response_code = null): int | ||
{ | ||
if ($response_code !== null) { | ||
self::$responseCode = $response_code; | ||
} | ||
return self::$responseCode; | ||
} | ||
|
||
/** | ||
* Check header is exists | ||
*/ | ||
public static function hasHeader(string $header): bool | ||
{ | ||
return array_key_exists(strtolower($header), self::$headers); | ||
} | ||
} |
Oops, something went wrong.