MultipartRequestParserTest.php 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-License-Identifier: AGPL-3.0-only
  5. */
  6. namespace OCA\DAV\Tests\unit\DAV;
  7. use OCA\DAV\BulkUpload\MultipartRequestParser;
  8. use Psr\Log\LoggerInterface;
  9. use Test\TestCase;
  10. class MultipartRequestParserTest extends TestCase {
  11. protected LoggerInterface $logger;
  12. protected function setUp(): void {
  13. $this->logger = $this->createMock(LoggerInterface::class);
  14. }
  15. private function getValidBodyObject() {
  16. return [
  17. [
  18. 'headers' => [
  19. 'Content-Length' => 7,
  20. 'X-File-MD5' => '4f2377b4d911f7ec46325fe603c3af03',
  21. 'X-File-Path' => '/coucou.txt'
  22. ],
  23. 'content' => "Coucou\n"
  24. ]
  25. ];
  26. }
  27. private function getMultipartParser(array $parts, array $headers = [], string $boundary = 'boundary_azertyuiop'): MultipartRequestParser {
  28. $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
  29. ->disableOriginalConstructor()
  30. ->getMock();
  31. $headers = array_merge(['Content-Type' => 'multipart/related; boundary=' . $boundary], $headers);
  32. $request->expects($this->any())
  33. ->method('getHeader')
  34. ->willReturnCallback(function (string $key) use (&$headers) {
  35. return $headers[$key];
  36. });
  37. $body = '';
  38. foreach ($parts as $part) {
  39. $body .= '--' . $boundary . "\r\n";
  40. foreach ($part['headers'] as $headerKey => $headerPart) {
  41. $body .= $headerKey . ': ' . $headerPart . "\r\n";
  42. }
  43. $body .= "\r\n";
  44. $body .= $part['content'] . "\r\n";
  45. }
  46. $body .= '--' . $boundary . '--';
  47. $stream = fopen('php://temp', 'r+');
  48. fwrite($stream, $body);
  49. rewind($stream);
  50. $request->expects($this->any())
  51. ->method('getBody')
  52. ->willReturn($stream);
  53. return new MultipartRequestParser($request, $this->logger);
  54. }
  55. /**
  56. * Test validation of the request's body type
  57. */
  58. public function testBodyTypeValidation(): void {
  59. $bodyStream = 'I am not a stream, but pretend to be';
  60. $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')
  61. ->disableOriginalConstructor()
  62. ->getMock();
  63. $request->expects($this->any())
  64. ->method('getBody')
  65. ->willReturn($bodyStream);
  66. $this->expectExceptionMessage('Body should be of type resource');
  67. new MultipartRequestParser($request, $this->logger);
  68. }
  69. /**
  70. * Test with valid request.
  71. * - valid boundary
  72. * - valid md5 hash
  73. * - valid content-length
  74. * - valid file content
  75. * - valid file path
  76. */
  77. public function testValidRequest(): void {
  78. $multipartParser = $this->getMultipartParser(
  79. $this->getValidBodyObject()
  80. );
  81. [$headers, $content] = $multipartParser->parseNextPart();
  82. $this->assertSame((int)$headers['content-length'], 7, 'Content-Length header should be the same as provided.');
  83. $this->assertSame($headers['x-file-md5'], '4f2377b4d911f7ec46325fe603c3af03', 'X-File-MD5 header should be the same as provided.');
  84. $this->assertSame($headers['x-file-path'], '/coucou.txt', 'X-File-Path header should be the same as provided.');
  85. $this->assertSame($content, "Coucou\n", 'Content should be the same');
  86. }
  87. /**
  88. * Test with invalid md5 hash.
  89. */
  90. public function testInvalidMd5Hash(): void {
  91. $bodyObject = $this->getValidBodyObject();
  92. $bodyObject['0']['headers']['X-File-MD5'] = 'f2377b4d911f7ec46325fe603c3af03';
  93. $multipartParser = $this->getMultipartParser(
  94. $bodyObject
  95. );
  96. $this->expectExceptionMessage('Computed md5 hash is incorrect.');
  97. $multipartParser->parseNextPart();
  98. }
  99. /**
  100. * Test with a null md5 hash.
  101. */
  102. public function testNullMd5Hash(): void {
  103. $bodyObject = $this->getValidBodyObject();
  104. unset($bodyObject['0']['headers']['X-File-MD5']);
  105. $multipartParser = $this->getMultipartParser(
  106. $bodyObject
  107. );
  108. $this->expectExceptionMessage('The X-File-MD5 header must not be null.');
  109. $multipartParser->parseNextPart();
  110. }
  111. /**
  112. * Test with a null Content-Length.
  113. */
  114. public function testNullContentLength(): void {
  115. $bodyObject = $this->getValidBodyObject();
  116. unset($bodyObject['0']['headers']['Content-Length']);
  117. $multipartParser = $this->getMultipartParser(
  118. $bodyObject
  119. );
  120. $this->expectExceptionMessage('The Content-Length header must not be null.');
  121. $multipartParser->parseNextPart();
  122. }
  123. /**
  124. * Test with a lower Content-Length.
  125. */
  126. public function testLowerContentLength(): void {
  127. $bodyObject = $this->getValidBodyObject();
  128. $bodyObject['0']['headers']['Content-Length'] = 6;
  129. $multipartParser = $this->getMultipartParser(
  130. $bodyObject
  131. );
  132. $this->expectExceptionMessage('Computed md5 hash is incorrect.');
  133. $multipartParser->parseNextPart();
  134. }
  135. /**
  136. * Test with a higher Content-Length.
  137. */
  138. public function testHigherContentLength(): void {
  139. $bodyObject = $this->getValidBodyObject();
  140. $bodyObject['0']['headers']['Content-Length'] = 8;
  141. $multipartParser = $this->getMultipartParser(
  142. $bodyObject
  143. );
  144. $this->expectExceptionMessage('Computed md5 hash is incorrect.');
  145. $multipartParser->parseNextPart();
  146. }
  147. /**
  148. * Test with wrong boundary in body.
  149. */
  150. public function testWrongBoundary(): void {
  151. $bodyObject = $this->getValidBodyObject();
  152. $multipartParser = $this->getMultipartParser(
  153. $bodyObject,
  154. ['Content-Type' => 'multipart/related; boundary=boundary_poiuytreza']
  155. );
  156. $this->expectExceptionMessage('Boundary not found where it should be.');
  157. $multipartParser->parseNextPart();
  158. }
  159. /**
  160. * Test with no boundary in request headers.
  161. */
  162. public function testNoBoundaryInHeader(): void {
  163. $bodyObject = $this->getValidBodyObject();
  164. $this->expectExceptionMessage('Error while parsing boundary in Content-Type header.');
  165. $this->getMultipartParser(
  166. $bodyObject,
  167. ['Content-Type' => 'multipart/related']
  168. );
  169. }
  170. /**
  171. * Test with no boundary in the request's headers.
  172. */
  173. public function testNoBoundaryInBody(): void {
  174. $bodyObject = $this->getValidBodyObject();
  175. $multipartParser = $this->getMultipartParser(
  176. $bodyObject,
  177. ['Content-Type' => 'multipart/related; boundary=boundary_azertyuiop'],
  178. ''
  179. );
  180. $this->expectExceptionMessage('Boundary not found where it should be.');
  181. $multipartParser->parseNextPart();
  182. }
  183. /**
  184. * Test with a boundary with quotes in the request's headers.
  185. */
  186. public function testBoundaryWithQuotes(): void {
  187. $bodyObject = $this->getValidBodyObject();
  188. $multipartParser = $this->getMultipartParser(
  189. $bodyObject,
  190. ['Content-Type' => 'multipart/related; boundary="boundary_azertyuiop"'],
  191. );
  192. $multipartParser->parseNextPart();
  193. // Dummy assertion, we just want to test that the parsing works.
  194. $this->assertTrue(true);
  195. }
  196. /**
  197. * Test with a wrong Content-Type in the request's headers.
  198. */
  199. public function testWrongContentType(): void {
  200. $bodyObject = $this->getValidBodyObject();
  201. $this->expectExceptionMessage('Content-Type must be multipart/related');
  202. $this->getMultipartParser(
  203. $bodyObject,
  204. ['Content-Type' => 'multipart/form-data; boundary="boundary_azertyuiop"'],
  205. );
  206. }
  207. /**
  208. * Test with a wrong key after the content type in the request's headers.
  209. */
  210. public function testWrongKeyInContentType(): void {
  211. $bodyObject = $this->getValidBodyObject();
  212. $this->expectExceptionMessage('Boundary is invalid');
  213. $this->getMultipartParser(
  214. $bodyObject,
  215. ['Content-Type' => 'multipart/related; wrongkey="boundary_azertyuiop"'],
  216. );
  217. }
  218. /**
  219. * Test with a null Content-Type in the request's headers.
  220. */
  221. public function testNullContentType(): void {
  222. $bodyObject = $this->getValidBodyObject();
  223. $this->expectExceptionMessage('Content-Type can not be null');
  224. $this->getMultipartParser(
  225. $bodyObject,
  226. ['Content-Type' => null],
  227. );
  228. }
  229. }