ResponseEmitterTest.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 3.3.5
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\Http;
  17. use Cake\Http\CallbackStream;
  18. use Cake\Http\Cookie\Cookie;
  19. use Cake\Http\Response;
  20. use Cake\Http\ResponseEmitter;
  21. use Cake\TestSuite\TestCase;
  22. require_once __DIR__ . '/server_mocks.php';
  23. /**
  24. * Response emitter test.
  25. */
  26. class ResponseEmitterTest extends TestCase
  27. {
  28. /**
  29. * @var \Cake\Http\ResponseEmitter
  30. */
  31. protected $emitter;
  32. /**
  33. * setup
  34. *
  35. * @return void
  36. */
  37. public function setUp(): void
  38. {
  39. parent::setUp();
  40. $GLOBALS['mockedHeadersSent'] = false;
  41. $GLOBALS['mockedHeaders'] = [];
  42. $this->emitter = $this->getMockBuilder(ResponseEmitter::class)
  43. ->setMethods(['setCookie'])
  44. ->getMock();
  45. $this->emitter->expects($this->any())
  46. ->method('setCookie')
  47. ->will($this->returnCallback(function ($cookie) {
  48. if (is_string($cookie)) {
  49. $cookie = Cookie::createFromHeaderString($cookie, ['path' => '']);
  50. }
  51. $GLOBALS['mockedCookies'][] = ['name' => $cookie->getName(), 'value' => $cookie->getValue()]
  52. + $cookie->getOptions();
  53. return true;
  54. }));
  55. }
  56. /**
  57. * teardown
  58. *
  59. * @return void
  60. */
  61. public function tearDown(): void
  62. {
  63. parent::tearDown();
  64. unset($GLOBALS['mockedHeadersSent']);
  65. }
  66. /**
  67. * Test emitting simple responses.
  68. *
  69. * @return void
  70. */
  71. public function testEmitResponseSimple()
  72. {
  73. $response = (new Response())
  74. ->withStatus(201)
  75. ->withHeader('Content-Type', 'text/html')
  76. ->withHeader('Location', 'http://example.com/cake/1');
  77. $response->getBody()->write('It worked');
  78. ob_start();
  79. $this->emitter->emit($response);
  80. $out = ob_get_clean();
  81. $this->assertSame('It worked', $out);
  82. $expected = [
  83. 'HTTP/1.1 201 Created',
  84. 'Content-Type: text/html',
  85. 'Location: http://example.com/cake/1',
  86. ];
  87. $this->assertEquals($expected, $GLOBALS['mockedHeaders']);
  88. }
  89. /**
  90. * Test emitting a no-content response
  91. *
  92. * @return void
  93. */
  94. public function testEmitNoContentResponse()
  95. {
  96. $response = (new Response())
  97. ->withHeader('X-testing', 'value')
  98. ->withStatus(204);
  99. $response->getBody()->write('It worked');
  100. ob_start();
  101. $this->emitter->emit($response);
  102. $out = ob_get_clean();
  103. $this->assertSame('', $out);
  104. $expected = [
  105. 'HTTP/1.1 204 No Content',
  106. 'X-testing: value',
  107. ];
  108. $this->assertEquals($expected, $GLOBALS['mockedHeaders']);
  109. }
  110. /**
  111. * Test emitting responses with array cookes
  112. *
  113. * @return void
  114. */
  115. public function testEmitResponseArrayCookies()
  116. {
  117. $response = (new Response())
  118. ->withCookie(new Cookie('simple', 'val', null, '/', '', true))
  119. ->withAddedHeader('Set-Cookie', 'google=not=nice;Path=/accounts; HttpOnly')
  120. ->withHeader('Content-Type', 'text/plain');
  121. $response->getBody()->write('ok');
  122. ob_start();
  123. $this->emitter->emit($response);
  124. $out = ob_get_clean();
  125. $this->assertSame('ok', $out);
  126. $expected = [
  127. 'HTTP/1.1 200 OK',
  128. 'Content-Type: text/plain',
  129. ];
  130. $this->assertEquals($expected, $GLOBALS['mockedHeaders']);
  131. $expected = [
  132. [
  133. 'name' => 'simple',
  134. 'value' => 'val',
  135. 'path' => '/',
  136. 'expires' => 0,
  137. 'domain' => '',
  138. 'secure' => true,
  139. 'httponly' => false,
  140. ],
  141. [
  142. 'name' => 'google',
  143. 'value' => 'not=nice',
  144. 'path' => '/accounts',
  145. 'expires' => 0,
  146. 'domain' => '',
  147. 'secure' => false,
  148. 'httponly' => true,
  149. ],
  150. ];
  151. $this->assertEquals($expected, $GLOBALS['mockedCookies']);
  152. }
  153. /**
  154. * Test emitting responses with cookies
  155. *
  156. * @return void
  157. */
  158. public function testEmitResponseCookies()
  159. {
  160. $response = (new Response())
  161. ->withAddedHeader('Set-Cookie', "simple=val;\tSecure")
  162. ->withAddedHeader('Set-Cookie', 'people=jim,jack,jonny";";Path=/accounts')
  163. ->withAddedHeader('Set-Cookie', 'google=not=nice;Path=/accounts; HttpOnly; samesite=Strict')
  164. ->withAddedHeader('Set-Cookie', 'a=b; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Domain=www.example.com;')
  165. ->withAddedHeader('Set-Cookie', 'list%5B%5D=a%20b%20c')
  166. ->withHeader('Content-Type', 'text/plain');
  167. $response->getBody()->write('ok');
  168. ob_start();
  169. $this->emitter->emit($response);
  170. $out = ob_get_clean();
  171. $this->assertSame('ok', $out);
  172. $expected = [
  173. 'HTTP/1.1 200 OK',
  174. 'Content-Type: text/plain',
  175. ];
  176. $this->assertEquals($expected, $GLOBALS['mockedHeaders']);
  177. $expected = [
  178. [
  179. 'name' => 'simple',
  180. 'value' => 'val',
  181. 'path' => '',
  182. 'expires' => 0,
  183. 'domain' => '',
  184. 'secure' => true,
  185. 'httponly' => false,
  186. ],
  187. [
  188. 'name' => 'people',
  189. 'value' => 'jim,jack,jonny";"',
  190. 'path' => '/accounts',
  191. 'expires' => 0,
  192. 'domain' => '',
  193. 'secure' => false,
  194. 'httponly' => false,
  195. ],
  196. [
  197. 'name' => 'google',
  198. 'value' => 'not=nice',
  199. 'path' => '/accounts',
  200. 'expires' => 0,
  201. 'domain' => '',
  202. 'secure' => false,
  203. 'httponly' => true,
  204. 'samesite' => 'Strict',
  205. ],
  206. [
  207. 'name' => 'a',
  208. 'value' => 'b',
  209. 'path' => '',
  210. 'expires' => 1610576581,
  211. 'domain' => 'www.example.com',
  212. 'secure' => false,
  213. 'httponly' => false,
  214. ],
  215. [
  216. 'name' => 'list[]',
  217. 'value' => 'a b c',
  218. 'path' => '',
  219. 'expires' => 0,
  220. 'domain' => '',
  221. 'secure' => false,
  222. 'httponly' => false,
  223. ],
  224. ];
  225. $this->assertEquals($expected, $GLOBALS['mockedCookies']);
  226. }
  227. /**
  228. * Test emitting responses using callback streams.
  229. *
  230. * We use callback streams for closure based responses.
  231. *
  232. * @return void
  233. */
  234. public function testEmitResponseCallbackStream()
  235. {
  236. $stream = new CallbackStream(function () {
  237. echo 'It worked';
  238. });
  239. $response = (new Response())
  240. ->withStatus(201)
  241. ->withBody($stream)
  242. ->withHeader('Content-Type', 'text/plain');
  243. ob_start();
  244. $this->emitter->emit($response);
  245. $out = ob_get_clean();
  246. $this->assertSame('It worked', $out);
  247. $expected = [
  248. 'HTTP/1.1 201 Created',
  249. 'Content-Type: text/plain',
  250. ];
  251. $this->assertEquals($expected, $GLOBALS['mockedHeaders']);
  252. }
  253. /**
  254. * Test valid body ranges.
  255. *
  256. * @return void
  257. */
  258. public function testEmitResponseBodyRange()
  259. {
  260. $response = (new Response())
  261. ->withHeader('Content-Type', 'text/plain')
  262. ->withHeader('Content-Range', 'bytes 1-4/9');
  263. $response->getBody()->write('It worked');
  264. ob_start();
  265. $this->emitter->emit($response);
  266. $out = ob_get_clean();
  267. $this->assertSame('t wo', $out);
  268. $expected = [
  269. 'HTTP/1.1 200 OK',
  270. 'Content-Type: text/plain',
  271. 'Content-Range: bytes 1-4/9',
  272. ];
  273. $this->assertEquals($expected, $GLOBALS['mockedHeaders']);
  274. }
  275. /**
  276. * Test valid body ranges.
  277. *
  278. * @return void
  279. */
  280. public function testEmitResponseBodyRangeComplete()
  281. {
  282. $response = (new Response())
  283. ->withHeader('Content-Type', 'text/plain')
  284. ->withHeader('Content-Range', 'bytes 0-20/9');
  285. $response->getBody()->write('It worked');
  286. ob_start();
  287. $this->emitter->emit($response, 2);
  288. $out = ob_get_clean();
  289. $this->assertSame('It worked', $out);
  290. }
  291. /**
  292. * Test out of bounds body ranges.
  293. *
  294. * @return void
  295. */
  296. public function testEmitResponseBodyRangeOverflow()
  297. {
  298. $response = (new Response())
  299. ->withHeader('Content-Type', 'text/plain')
  300. ->withHeader('Content-Range', 'bytes 5-20/9');
  301. $response->getBody()->write('It worked');
  302. ob_start();
  303. $this->emitter->emit($response);
  304. $out = ob_get_clean();
  305. $this->assertSame('rked', $out);
  306. }
  307. /**
  308. * Test malformed content-range header
  309. *
  310. * @return void
  311. */
  312. public function testEmitResponseBodyRangeMalformed()
  313. {
  314. $response = (new Response())
  315. ->withHeader('Content-Type', 'text/plain')
  316. ->withHeader('Content-Range', 'bytes 9-ba/a');
  317. $response->getBody()->write('It worked');
  318. ob_start();
  319. $this->emitter->emit($response);
  320. $out = ob_get_clean();
  321. $this->assertSame('It worked', $out);
  322. }
  323. /**
  324. * Test callback streams returning content and ranges
  325. *
  326. * @return void
  327. */
  328. public function testEmitResponseBodyRangeCallbackStream()
  329. {
  330. $stream = new CallbackStream(function () {
  331. return 'It worked';
  332. });
  333. $response = (new Response())
  334. ->withStatus(201)
  335. ->withBody($stream)
  336. ->withHeader('Content-Range', 'bytes 1-4/9')
  337. ->withHeader('Content-Type', 'text/plain');
  338. ob_start();
  339. $this->emitter->emit($response);
  340. $out = ob_get_clean();
  341. $this->assertSame('t wo', $out);
  342. $expected = [
  343. 'HTTP/1.1 201 Created',
  344. 'Content-Range: bytes 1-4/9',
  345. 'Content-Type: text/plain',
  346. ];
  347. $this->assertEquals($expected, $GLOBALS['mockedHeaders']);
  348. }
  349. }