PageRenderTime 39ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php

http://github.com/symfony/symfony
PHP | 480 lines | 334 code | 89 blank | 57 comment | 3 complexity | 304cebacc22fc4b42c21f02cd8a1b789 MD5 | raw file
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Validator\Tests\Constraints;
  11. use Symfony\Component\HttpFoundation\File\UploadedFile;
  12. use Symfony\Component\Validator\Constraints\File;
  13. use Symfony\Component\Validator\Constraints\FileValidator;
  14. use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
  15. abstract class FileValidatorTest extends ConstraintValidatorTestCase
  16. {
  17. protected $path;
  18. protected $file;
  19. protected function createValidator()
  20. {
  21. return new FileValidator();
  22. }
  23. protected function setUp(): void
  24. {
  25. parent::setUp();
  26. $this->path = sys_get_temp_dir().\DIRECTORY_SEPARATOR.'FileValidatorTest';
  27. $this->file = fopen($this->path, 'w');
  28. fwrite($this->file, ' ', 1);
  29. }
  30. protected function tearDown(): void
  31. {
  32. parent::tearDown();
  33. if (\is_resource($this->file)) {
  34. fclose($this->file);
  35. }
  36. if (file_exists($this->path)) {
  37. unlink($this->path);
  38. }
  39. $this->path = null;
  40. $this->file = null;
  41. }
  42. public function testNullIsValid()
  43. {
  44. $this->validator->validate(null, new File());
  45. $this->assertNoViolation();
  46. }
  47. public function testEmptyStringIsValid()
  48. {
  49. $this->validator->validate('', new File());
  50. $this->assertNoViolation();
  51. }
  52. public function testExpectsStringCompatibleTypeOrFile()
  53. {
  54. $this->expectException('Symfony\Component\Validator\Exception\UnexpectedValueException');
  55. $this->validator->validate(new \stdClass(), new File());
  56. }
  57. public function testValidFile()
  58. {
  59. $this->validator->validate($this->path, new File());
  60. $this->assertNoViolation();
  61. }
  62. public function testValidUploadedfile()
  63. {
  64. file_put_contents($this->path, '1');
  65. $file = new UploadedFile($this->path, 'originalName', null, null, true);
  66. $this->validator->validate($file, new File());
  67. $this->assertNoViolation();
  68. }
  69. public function provideMaxSizeExceededTests()
  70. {
  71. // We have various interesting limit - size combinations to test.
  72. // Assume a limit of 1000 bytes (1 kB). Then the following table
  73. // lists the violation messages for different file sizes:
  74. // -----------+--------------------------------------------------------
  75. // Size | Violation Message
  76. // -----------+--------------------------------------------------------
  77. // 1000 bytes | No violation
  78. // 1001 bytes | "Size of 1001 bytes exceeded limit of 1000 bytes"
  79. // 1004 bytes | "Size of 1004 bytes exceeded limit of 1000 bytes"
  80. // | NOT: "Size of 1 kB exceeded limit of 1 kB"
  81. // 1005 bytes | "Size of 1.01 kB exceeded limit of 1 kB"
  82. // -----------+--------------------------------------------------------
  83. // As you see, we have two interesting borders:
  84. // 1000/1001 - The border as of which a violation occurs
  85. // 1004/1005 - The border as of which the message can be rounded to kB
  86. // Analogous for kB/MB.
  87. // Prior to Symfony 2.5, violation messages are always displayed in the
  88. // same unit used to specify the limit.
  89. // As of Symfony 2.5, the above logic is implemented.
  90. return [
  91. // limit in bytes
  92. [1001, 1000, '1001', '1000', 'bytes'],
  93. [1004, 1000, '1004', '1000', 'bytes'],
  94. [1005, 1000, '1.01', '1', 'kB'],
  95. [1000001, 1000000, '1000001', '1000000', 'bytes'],
  96. [1004999, 1000000, '1005', '1000', 'kB'],
  97. [1005000, 1000000, '1.01', '1', 'MB'],
  98. // limit in kB
  99. [1001, '1k', '1001', '1000', 'bytes'],
  100. [1004, '1k', '1004', '1000', 'bytes'],
  101. [1005, '1k', '1.01', '1', 'kB'],
  102. [1000001, '1000k', '1000001', '1000000', 'bytes'],
  103. [1004999, '1000k', '1005', '1000', 'kB'],
  104. [1005000, '1000k', '1.01', '1', 'MB'],
  105. // limit in MB
  106. [1000001, '1M', '1000001', '1000000', 'bytes'],
  107. [1004999, '1M', '1005', '1000', 'kB'],
  108. [1005000, '1M', '1.01', '1', 'MB'],
  109. // limit in KiB
  110. [1025, '1Ki', '1025', '1024', 'bytes'],
  111. [1029, '1Ki', '1029', '1024', 'bytes'],
  112. [1030, '1Ki', '1.01', '1', 'KiB'],
  113. [1048577, '1024Ki', '1048577', '1048576', 'bytes'],
  114. [1053818, '1024Ki', '1029.12', '1024', 'KiB'],
  115. [1053819, '1024Ki', '1.01', '1', 'MiB'],
  116. // limit in MiB
  117. [1048577, '1Mi', '1048577', '1048576', 'bytes'],
  118. [1053818, '1Mi', '1029.12', '1024', 'KiB'],
  119. [1053819, '1Mi', '1.01', '1', 'MiB'],
  120. ];
  121. }
  122. /**
  123. * @dataProvider provideMaxSizeExceededTests
  124. */
  125. public function testMaxSizeExceeded($bytesWritten, $limit, $sizeAsString, $limitAsString, $suffix)
  126. {
  127. fseek($this->file, $bytesWritten - 1, SEEK_SET);
  128. fwrite($this->file, '0');
  129. fclose($this->file);
  130. $constraint = new File([
  131. 'maxSize' => $limit,
  132. 'maxSizeMessage' => 'myMessage',
  133. ]);
  134. $this->validator->validate($this->getFile($this->path), $constraint);
  135. $this->buildViolation('myMessage')
  136. ->setParameter('{{ limit }}', $limitAsString)
  137. ->setParameter('{{ size }}', $sizeAsString)
  138. ->setParameter('{{ suffix }}', $suffix)
  139. ->setParameter('{{ file }}', '"'.$this->path.'"')
  140. ->setParameter('{{ name }}', '"'.basename($this->path).'"')
  141. ->setCode(File::TOO_LARGE_ERROR)
  142. ->assertRaised();
  143. }
  144. public function provideMaxSizeNotExceededTests()
  145. {
  146. return [
  147. // limit in bytes
  148. [1000, 1000],
  149. [1000000, 1000000],
  150. // limit in kB
  151. [1000, '1k'],
  152. [1000000, '1000k'],
  153. // limit in MB
  154. [1000000, '1M'],
  155. // limit in KiB
  156. [1024, '1Ki'],
  157. [1048576, '1024Ki'],
  158. // limit in MiB
  159. [1048576, '1Mi'],
  160. ];
  161. }
  162. /**
  163. * @dataProvider provideMaxSizeNotExceededTests
  164. */
  165. public function testMaxSizeNotExceeded($bytesWritten, $limit)
  166. {
  167. fseek($this->file, $bytesWritten - 1, SEEK_SET);
  168. fwrite($this->file, '0');
  169. fclose($this->file);
  170. $constraint = new File([
  171. 'maxSize' => $limit,
  172. 'maxSizeMessage' => 'myMessage',
  173. ]);
  174. $this->validator->validate($this->getFile($this->path), $constraint);
  175. $this->assertNoViolation();
  176. }
  177. public function testInvalidMaxSize()
  178. {
  179. $this->expectException('Symfony\Component\Validator\Exception\ConstraintDefinitionException');
  180. $constraint = new File([
  181. 'maxSize' => '1abc',
  182. ]);
  183. $this->validator->validate($this->path, $constraint);
  184. }
  185. public function provideBinaryFormatTests()
  186. {
  187. return [
  188. [11, 10, null, '11', '10', 'bytes'],
  189. [11, 10, true, '11', '10', 'bytes'],
  190. [11, 10, false, '11', '10', 'bytes'],
  191. // round(size) == 1.01kB, limit == 1kB
  192. [ceil(1000 * 1.01), 1000, null, '1.01', '1', 'kB'],
  193. [ceil(1000 * 1.01), '1k', null, '1.01', '1', 'kB'],
  194. [ceil(1024 * 1.01), '1Ki', null, '1.01', '1', 'KiB'],
  195. [ceil(1024 * 1.01), 1024, true, '1.01', '1', 'KiB'],
  196. [ceil(1024 * 1.01 * 1000), '1024k', true, '1010', '1000', 'KiB'],
  197. [ceil(1024 * 1.01), '1Ki', true, '1.01', '1', 'KiB'],
  198. [ceil(1000 * 1.01), 1000, false, '1.01', '1', 'kB'],
  199. [ceil(1000 * 1.01), '1k', false, '1.01', '1', 'kB'],
  200. [ceil(1024 * 1.01 * 10), '10Ki', false, '10.34', '10.24', 'kB'],
  201. ];
  202. }
  203. /**
  204. * @dataProvider provideBinaryFormatTests
  205. */
  206. public function testBinaryFormat($bytesWritten, $limit, $binaryFormat, $sizeAsString, $limitAsString, $suffix)
  207. {
  208. fseek($this->file, $bytesWritten - 1, SEEK_SET);
  209. fwrite($this->file, '0');
  210. fclose($this->file);
  211. $constraint = new File([
  212. 'maxSize' => $limit,
  213. 'binaryFormat' => $binaryFormat,
  214. 'maxSizeMessage' => 'myMessage',
  215. ]);
  216. $this->validator->validate($this->getFile($this->path), $constraint);
  217. $this->buildViolation('myMessage')
  218. ->setParameter('{{ limit }}', $limitAsString)
  219. ->setParameter('{{ size }}', $sizeAsString)
  220. ->setParameter('{{ suffix }}', $suffix)
  221. ->setParameter('{{ file }}', '"'.$this->path.'"')
  222. ->setParameter('{{ name }}', '"'.basename($this->path).'"')
  223. ->setCode(File::TOO_LARGE_ERROR)
  224. ->assertRaised();
  225. }
  226. public function testValidMimeType()
  227. {
  228. $file = $this
  229. ->getMockBuilder('Symfony\Component\HttpFoundation\File\File')
  230. ->setConstructorArgs([__DIR__.'/Fixtures/foo'])
  231. ->getMock();
  232. $file
  233. ->expects($this->once())
  234. ->method('getPathname')
  235. ->willReturn($this->path);
  236. $file
  237. ->expects($this->once())
  238. ->method('getMimeType')
  239. ->willReturn('image/jpg');
  240. $constraint = new File([
  241. 'mimeTypes' => ['image/png', 'image/jpg'],
  242. ]);
  243. $this->validator->validate($file, $constraint);
  244. $this->assertNoViolation();
  245. }
  246. public function testValidWildcardMimeType()
  247. {
  248. $file = $this
  249. ->getMockBuilder('Symfony\Component\HttpFoundation\File\File')
  250. ->setConstructorArgs([__DIR__.'/Fixtures/foo'])
  251. ->getMock();
  252. $file
  253. ->expects($this->once())
  254. ->method('getPathname')
  255. ->willReturn($this->path);
  256. $file
  257. ->expects($this->once())
  258. ->method('getMimeType')
  259. ->willReturn('image/jpg');
  260. $constraint = new File([
  261. 'mimeTypes' => ['image/*'],
  262. ]);
  263. $this->validator->validate($file, $constraint);
  264. $this->assertNoViolation();
  265. }
  266. public function testInvalidMimeType()
  267. {
  268. $file = $this
  269. ->getMockBuilder('Symfony\Component\HttpFoundation\File\File')
  270. ->setConstructorArgs([__DIR__.'/Fixtures/foo'])
  271. ->getMock();
  272. $file
  273. ->expects($this->once())
  274. ->method('getPathname')
  275. ->willReturn($this->path);
  276. $file
  277. ->expects($this->once())
  278. ->method('getMimeType')
  279. ->willReturn('application/pdf');
  280. $constraint = new File([
  281. 'mimeTypes' => ['image/png', 'image/jpg'],
  282. 'mimeTypesMessage' => 'myMessage',
  283. ]);
  284. $this->validator->validate($file, $constraint);
  285. $this->buildViolation('myMessage')
  286. ->setParameter('{{ type }}', '"application/pdf"')
  287. ->setParameter('{{ types }}', '"image/png", "image/jpg"')
  288. ->setParameter('{{ file }}', '"'.$this->path.'"')
  289. ->setParameter('{{ name }}', '"'.basename($this->path).'"')
  290. ->setCode(File::INVALID_MIME_TYPE_ERROR)
  291. ->assertRaised();
  292. }
  293. public function testInvalidWildcardMimeType()
  294. {
  295. $file = $this
  296. ->getMockBuilder('Symfony\Component\HttpFoundation\File\File')
  297. ->setConstructorArgs([__DIR__.'/Fixtures/foo'])
  298. ->getMock();
  299. $file
  300. ->expects($this->once())
  301. ->method('getPathname')
  302. ->willReturn($this->path);
  303. $file
  304. ->expects($this->once())
  305. ->method('getMimeType')
  306. ->willReturn('application/pdf');
  307. $constraint = new File([
  308. 'mimeTypes' => ['image/*', 'image/jpg'],
  309. 'mimeTypesMessage' => 'myMessage',
  310. ]);
  311. $this->validator->validate($file, $constraint);
  312. $this->buildViolation('myMessage')
  313. ->setParameter('{{ type }}', '"application/pdf"')
  314. ->setParameter('{{ types }}', '"image/*", "image/jpg"')
  315. ->setParameter('{{ file }}', '"'.$this->path.'"')
  316. ->setParameter('{{ name }}', '"'.basename($this->path).'"')
  317. ->setCode(File::INVALID_MIME_TYPE_ERROR)
  318. ->assertRaised();
  319. }
  320. public function testDisallowEmpty()
  321. {
  322. ftruncate($this->file, 0);
  323. $constraint = new File([
  324. 'disallowEmptyMessage' => 'myMessage',
  325. ]);
  326. $this->validator->validate($this->getFile($this->path), $constraint);
  327. $this->buildViolation('myMessage')
  328. ->setParameter('{{ file }}', '"'.$this->path.'"')
  329. ->setParameter('{{ name }}', '"'.basename($this->path).'"')
  330. ->setCode(File::EMPTY_ERROR)
  331. ->assertRaised();
  332. }
  333. /**
  334. * @dataProvider uploadedFileErrorProvider
  335. */
  336. public function testUploadedFileError($error, $message, array $params = [], $maxSize = null)
  337. {
  338. $file = new UploadedFile(tempnam(sys_get_temp_dir(), 'file-validator-test-'), 'originalName', 'mime', $error);
  339. $constraint = new File([
  340. $message => 'myMessage',
  341. 'maxSize' => $maxSize,
  342. ]);
  343. $this->validator->validate($file, $constraint);
  344. $this->buildViolation('myMessage')
  345. ->setParameters($params)
  346. ->setCode($error)
  347. ->assertRaised();
  348. }
  349. public function uploadedFileErrorProvider()
  350. {
  351. $tests = [
  352. [(string) UPLOAD_ERR_FORM_SIZE, 'uploadFormSizeErrorMessage'],
  353. [(string) UPLOAD_ERR_PARTIAL, 'uploadPartialErrorMessage'],
  354. [(string) UPLOAD_ERR_NO_FILE, 'uploadNoFileErrorMessage'],
  355. [(string) UPLOAD_ERR_NO_TMP_DIR, 'uploadNoTmpDirErrorMessage'],
  356. [(string) UPLOAD_ERR_CANT_WRITE, 'uploadCantWriteErrorMessage'],
  357. [(string) UPLOAD_ERR_EXTENSION, 'uploadExtensionErrorMessage'],
  358. ];
  359. if (class_exists('Symfony\Component\HttpFoundation\File\UploadedFile')) {
  360. // when no maxSize is specified on constraint, it should use the ini value
  361. $tests[] = [(string) UPLOAD_ERR_INI_SIZE, 'uploadIniSizeErrorMessage', [
  362. '{{ limit }}' => UploadedFile::getMaxFilesize() / 1048576,
  363. '{{ suffix }}' => 'MiB',
  364. ]];
  365. // it should use the smaller limitation (maxSize option in this case)
  366. $tests[] = [(string) UPLOAD_ERR_INI_SIZE, 'uploadIniSizeErrorMessage', [
  367. '{{ limit }}' => 1,
  368. '{{ suffix }}' => 'bytes',
  369. ], '1'];
  370. // access FileValidator::factorizeSizes() private method to format max file size
  371. $reflection = new \ReflectionClass(\get_class(new FileValidator()));
  372. $method = $reflection->getMethod('factorizeSizes');
  373. $method->setAccessible(true);
  374. list(, $limit, $suffix) = $method->invokeArgs(new FileValidator(), [0, UploadedFile::getMaxFilesize(), false]);
  375. // it correctly parses the maxSize option and not only uses simple string comparison
  376. // 1000M should be bigger than the ini value
  377. $tests[] = [(string) UPLOAD_ERR_INI_SIZE, 'uploadIniSizeErrorMessage', [
  378. '{{ limit }}' => $limit,
  379. '{{ suffix }}' => $suffix,
  380. ], '1000M'];
  381. // it correctly parses the maxSize option and not only uses simple string comparison
  382. // 1000M should be bigger than the ini value
  383. $tests[] = [(string) UPLOAD_ERR_INI_SIZE, 'uploadIniSizeErrorMessage', [
  384. '{{ limit }}' => '0.1',
  385. '{{ suffix }}' => 'MB',
  386. ], '100K'];
  387. }
  388. return $tests;
  389. }
  390. abstract protected function getFile($filename);
  391. }