From 1f09f9be6ac13593ecec300828b0ed7dc6cde0a1 Mon Sep 17 00:00:00 2001 From: Benoit Jouhaud Date: Mon, 20 Feb 2023 11:06:41 +0100 Subject: [PATCH] Add unit tests and the related github action --- .github/workflows/tests.yml | 60 +++++++++ README.md | 4 +- .../Base64ToImageTransformer.php | 8 ++ tests/.gitignore | 0 tests/App/Model/Book.php | 30 +++++ tests/App/Resources/images/A.jpg | Bin 0 -> 3227 bytes .../Base64ToImageTransformerTest.php | 82 ++++++++++++ .../AddDeleteCheckboxListenerTest.php | 84 ++++++++++++ .../ClearBase64OnDeleteListenerTest.php | 76 +++++++++++ .../ImageType/DeleteFileListenerTest.php | 106 +++++++++++++++ tests/Unit/Form/ImageTypeTestCase.php | 92 +++++++++++++ tests/Unit/Form/Type/ImageTypeTest.php | 123 ++++++++++++++++++ 12 files changed, 664 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/tests.yml delete mode 100644 tests/.gitignore create mode 100644 tests/App/Model/Book.php create mode 100644 tests/App/Resources/images/A.jpg create mode 100644 tests/Unit/Form/DataTransformer/Base64ToImageTransformerTest.php create mode 100644 tests/Unit/Form/EventListener/ImageType/AddDeleteCheckboxListenerTest.php create mode 100644 tests/Unit/Form/EventListener/ImageType/ClearBase64OnDeleteListenerTest.php create mode 100644 tests/Unit/Form/EventListener/ImageType/DeleteFileListenerTest.php create mode 100644 tests/Unit/Form/ImageTypeTestCase.php create mode 100644 tests/Unit/Form/Type/ImageTypeTest.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..878cb58 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,60 @@ +name: 'Tests' + +on: + push: + branches: ['*'] + +jobs: + unit: + name: 'Unit tests' + runs-on: 'ubuntu-latest' + env: + SYMFONY_REQUIRE: '${{matrix.symfony-require}}' + + strategy: + matrix: + php-version: ['7.4', '8.0', '8.1', '8.2'] + symfony-require: ['5.4.*', '6.0.*', '6.1.*', '6.2.*'] + + steps: + - name: 'Setup PHP' + uses: 'shivammathur/setup-php@v2' + with: + php-version: '${{ matrix.php-version }}' + coverage: 'none' + + - name: 'Checkout sources' + uses: 'actions/checkout@v3' + + - name: 'Install dependencies' + run: 'composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist' + + - name: 'Execute Unit tests' + run: 'vendor/bin/phpunit' + + coverage: + name: 'Code coverage' + runs-on: 'ubuntu-latest' + env: + SYMFONY_REQUIRE: '6.2.*' + + steps: + - name: 'Setup PHP' + uses: 'shivammathur/setup-php@v2' + with: + php-version: '8.2' + coverage: 'pcov' + + - name: 'Checkout sources' + uses: 'actions/checkout@v3' + + - name: 'Install dependencies' + run: 'composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist' + + - name: 'Execute Unit tests' + run: 'vendor/bin/phpunit --coverage-clover=coverage.xml' + + - name: 'Upload coverage file' + uses: 'codecov/codecov-action@v3' + with: + files: 'coverage.xml' diff --git a/README.md b/README.md index c6e19b9..bba5b63 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ PrestaImageBundle ================= -![quality](https://github.com/prestaconcept/PrestaImageBundle/actions/workflows/quality.yml/badge.svg) +![tests](https://github.com/prestaconcept/PrestaImagebundle/actions/workflows/tests.yml/badge.svg) +![quality](https://github.com/prestaconcept/PrestaImagebundle/actions/workflows/quality.yml/badge.svg) +[![codecov](https://codecov.io/gh/prestaconcept/PrestaImagebundle/branch/4.x/graph/badge.svg?token=ls4VjT51Pi)](https://codecov.io/gh/prestaconcept/PrestaImagebundle) [![Latest Stable Version](https://poser.pugx.org/presta/image-bundle/v/stable.png)](https://packagist.org/packages/presta/image-bundle) [![Total Downloads](https://poser.pugx.org/presta/image-bundle/downloads.png)](https://packagist.org/packages/presta/image-bundle) diff --git a/src/Form/DataTransformer/Base64ToImageTransformer.php b/src/Form/DataTransformer/Base64ToImageTransformer.php index d637f23..4bb7433 100644 --- a/src/Form/DataTransformer/Base64ToImageTransformer.php +++ b/src/Form/DataTransformer/Base64ToImageTransformer.php @@ -31,12 +31,16 @@ public function reverseTransform($value): ?UploadedFile $filepath = tempnam(sys_get_temp_dir(), 'UploadedFile'); if (!\is_string($filepath)) { + // @codeCoverageIgnoreStart throw new \RuntimeException('Could not generate a valid temporary file path.'); + // @codeCoverageIgnoreEnd } $file = fopen($filepath, 'w'); if (!\is_resource($file)) { + // @codeCoverageIgnoreStart throw new \RuntimeException("Could not open the \"$filepath\" file in \"w\" mode."); + // @codeCoverageIgnoreEnd } stream_filter_append($file, 'convert.base64-decode'); @@ -48,12 +52,16 @@ public function reverseTransform($value): ?UploadedFile fclose($file); if (!\is_string($filename)) { + // @codeCoverageIgnoreStart throw new \RuntimeException('Could not get the generated file uri from metadata.'); + // @codeCoverageIgnoreEnd } $mimeType = mime_content_type($filename); if (!\is_string($mimeType)) { + // @codeCoverageIgnoreStart throw new \RuntimeException('Could not guess the image mime type.'); + // @codeCoverageIgnoreEnd } $extension = str_replace('image/', '', $mimeType); diff --git a/tests/.gitignore b/tests/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/tests/App/Model/Book.php b/tests/App/Model/Book.php new file mode 100644 index 0000000..a80deed --- /dev/null +++ b/tests/App/Model/Book.php @@ -0,0 +1,30 @@ +imageName = $imageName; + + return $book; + } +} diff --git a/tests/App/Resources/images/A.jpg b/tests/App/Resources/images/A.jpg new file mode 100644 index 0000000000000000000000000000000000000000..960fd282a193db04c8ef9db2cc1e63886502ba30 GIT binary patch literal 3227 zcmeH}X;f258iwoUW`zI(32T%csqlDSOJY0q)~h|bThd)^;)s&3tSzWVC@ zu5?uT0&H@1c5wy}1Ob2K0a6LDbz(9cyxf@9)ZoBvVUfp!sd1*pCRF{{n9vi!R2SDh zR9AP#9%`Hg-N1+%cswW|HiAWsiVcVeWu20dJ_AkwkHzA!7(5P#BM|U1M3Nj)R+gx) zq^v;Ntf94Kvj&Ai)iu_m>KM@|6n#qrBNJ0|b8{`d9lNaPHpXV=^z}s`0)ar3C928E zsnNA5+Vl-K={=x`hm0U27}5bKMF>`er1yaq01yVDT?hVegHSLUgT>(qGP1}7{w9Ee zU>JplF&H!&nVp1;1GFMWN!!!`tLzbg(}^O{lXJ@OG{-vvGIL-~*X(F?3PDCiRZU&v zi>-S428QO_EiA2eSUc@zIPcl(;_B&j;Gp**AKzn0u!2KE!(v#maq$VK61k~o&!wfG z&&bWoFZlja;pHOU^&90kD=Mq1>l+&RKi+L>Zt3W}|KMTQqo2A5hn@_NJRKby|K)El zU%eJiPQ95H%`g0OacOx){BE5K0`RX`zma{wrHF8$&}bNqTjzpM@rYnWG)CJLtK{H; z3y4zIp(o=>jyYv_1O%EHbB=s8dO$`+*Zh|+MC)k(A^SM6lusi24eUMFI3U6ha(J*J zumg)S`aI6>`aj0N;0-A^m*yO8J*!@^mOCj)dMyQQa=?l4&C=H;;^j+{mF>y~2MzC~ zSaZiYN*Vg4sLztY+{2*Dv_I5F*=rW<-g3AqJFCd4q>~@Q)JbJ9)DEbTlt_VBV={nm z$B6$}WCPThd4_j4jK9`CzHKuQr}egb*i-sQ9`=~eQCP_B6PJawR5dM6pV*;84oj!a zJ(ts;Pk7p^gxh%+K8Q%(uYOf=B}PInJ7nG4`gX*0Ky}gXl{@x1olxe2a|S~*md;m_ z8g{#x7Tcz+77dolhr4tCzMNSa;3qs%R?Tm%brAZU=LpUEACHg$$%FC9^102ORZUNO z%WZB;X$k8Y@P8)TyT>@vHifSbsi#yOjRZf{F+=wa*`(Ddg`YlH(%oRF(5L-uCB80^ zV;q_%wyepUZ|`Q8vtl2?+?+sv7avn!J?)NtWttwa_bKmB zo>P^R`3`V5J2i-7vKYGhtb?<9=VE(SVwvmx1{FQst9dT=n23-DDX8U?EM0mh1+HCH zw}Sq)t2cTHS5lD(ro&_(*$#w>UJ4(2&vi89=<%aszZ$~Qtk@C1_jXuGi^Gk#>bTD#yMGmT%w zEdImO9c4l+cXKoet5`Z*vMQ2-uB?^eZhchA`|iNnrC=$rVT)c$wnt}xiuYY}_VF<( z=+|0Jkb;Zf@;HWdS3)w?k9NY@@miu9FXS#Bn{kiMOP2JoT}LbH0#sG{6(!nTPx&Ca zk(uq4?Tmjh=ngSRA1`lKlg-Sq?jNfbwBC5tu=jF%_;kgEy)i3srE)UP+Fp08Kwy#Y z_3FOYKJ?7GIAmMkn9J$N`BuUWcNZ#_Mw;M1M|OO_eR-@hq?FM1G)4;Sl%!xHC~Lz5 zHyhiEc2@T0?c5saWJ)9N&j2%1)g#Jlyq_&4REv_z8grddh4Zr(^7WK6 z{#w4}|7-Cq>wS{%3Ylj@exB&$#|+2sYw`0tw;)doNQrv9R4Siw zy;$2uL75eh@xxjblFPf=MMDx2JIn`3g_El+DL4ya>dH6goKxSAzfqHTL9lHg7x!E( YaL!rI>ocp`=T^nv-~aHhLP0wAZ|+>+F#rGn literal 0 HcmV?d00001 diff --git a/tests/Unit/Form/DataTransformer/Base64ToImageTransformerTest.php b/tests/Unit/Form/DataTransformer/Base64ToImageTransformerTest.php new file mode 100644 index 0000000..b2f9485 --- /dev/null +++ b/tests/Unit/Form/DataTransformer/Base64ToImageTransformerTest.php @@ -0,0 +1,82 @@ +transform($value); + + self::assertIsArray($transformedValue); + self::assertArrayHasKey('base64', $transformedValue); + self::assertStringStartsWith("data:$mimeType;base64,", $transformedValue['base64']); + } + + /** + * @dataProvider invalidOriginalValues + * + * @param mixed $expected + * @param mixed $value + */ + public function testTransformingAnInvalidValue($expected, $value): void + { + $transformer = new Base64ToImageTransformer(); + + self::assertSame($expected, $transformer->transform($value)); + } + + /** + * @dataProvider invalidTransformedValues + * + * @param mixed $expected + * @param mixed $value + */ + public function testReverseTransform($expected, $value): void + { + $transformer = new Base64ToImageTransformer(); + + self::assertSame($expected, $transformer->reverseTransform($value)); + } + + public function validOriginalValues(): iterable + { + yield 'a ' . File::class . ' object related to a file on the filesystem should return a base64' => [ + 'image/jpeg', + new File(dirname(__DIR__) . '/../../App/Resources/images/A.jpg'), + ]; + } + + public function invalidOriginalValues(): iterable + { + yield 'an empty value (null) should return an empty (null) base64' => [['base64' => null], null]; + yield 'a value different from ' + . File::class + . ' should return an empty (null) base64' => [['base64' => null], new \stdClass()]; + yield 'a ' + . File::class + . ' object not related to a file on the filesystem should return an empty (null) base64' => [ + ['base64' => null], + new File('/tmp/foo.png', false), + ]; + } + + public function invalidTransformedValues(): iterable + { + yield 'an empty value (null) should return null' => [null, null]; + yield 'a value different from an array should return null' => [null, null]; + yield 'an array without a "base64" key should return null' => [null, null]; + } +} diff --git a/tests/Unit/Form/EventListener/ImageType/AddDeleteCheckboxListenerTest.php b/tests/Unit/Form/EventListener/ImageType/AddDeleteCheckboxListenerTest.php new file mode 100644 index 0000000..3bfc23a --- /dev/null +++ b/tests/Unit/Form/EventListener/ImageType/AddDeleteCheckboxListenerTest.php @@ -0,0 +1,84 @@ +storage + ->expects($this->once()) + ->method('resolvePath') + ->with($data, 'image') + ->willReturn('/tmp/foo.png') + ; + + $form = $this->factory->create(FormType::class, $data)->add('image', ImageType::class); + + $listener = new AddDeleteCheckboxListener($this->storage, 'foo', 'messages'); + $listener(new FormEvent($form->get('image'), $form->get('image')->getData())); + + $this->assertTrue($form->get('image')->has('delete')); + } + + /** + * @dataProvider notDeletableData + * + * @param mixed $data + */ + public function testAnImageTypeChildShouldNotHaveADeleteCheckboxIfCreated($data): void + { + $this->storage->method('resolvePath')->willReturn(null); + + $form = $this->factory->create(FormType::class, $data)->add('image', ImageType::class); + + $listener = new AddDeleteCheckboxListener($this->storage, 'foo', 'messages'); + $listener(new FormEvent($form->get('image'), $form->get('image')->getData())); + + $this->assertFalse($form->get('image')->has('delete')); + } + + public function testShouldCauseAnExceptionIfCreatedAsRootForm(): void + { + $this->expectException(\RuntimeException::class); + + $form = $this->factory->create(ImageType::class); + + $listener = new AddDeleteCheckboxListener($this->storage, 'foo', 'messages'); + $listener(new FormEvent($form, $form->getData())); + } + + public function testShouldCauseAnExceptionIfCreatedWithAnArrayAsData(): void + { + $this->expectException(UnexpectedTypeException::class); + + $form = $this->factory->create(FormType::class, [])->add('image', ImageType::class); + + $listener = new AddDeleteCheckboxListener($this->storage, 'foo', 'messages'); + $listener(new FormEvent($form->get('image'), $form->get('image')->getData())); + } + + public function deletableData(): iterable + { + yield 'an object related to a file stored on the filesystem' => [Book::withoutFile()]; + } + + public function notDeletableData(): iterable + { + yield 'no data (null)' => [null]; + yield 'an object not related to a file stored on the filesystem' => [Book::withoutFile()]; + } +} diff --git a/tests/Unit/Form/EventListener/ImageType/ClearBase64OnDeleteListenerTest.php b/tests/Unit/Form/EventListener/ImageType/ClearBase64OnDeleteListenerTest.php new file mode 100644 index 0000000..f121f3a --- /dev/null +++ b/tests/Unit/Form/EventListener/ImageType/ClearBase64OnDeleteListenerTest.php @@ -0,0 +1,76 @@ +factory->create(); + $event = new FormEvent($form, array_merge($submittedData, ['base64' => 'foo'])); + + $listener = new ClearBase64OnDeleteListener(); + $listener($event); + + $data = $event->getData(); + \assert(\is_array($data)); + + $this->assertArrayHasKey('base64', $data); + $this->assertNull($data['base64']); + } + + /** + * @dataProvider notDeletableSubmittedData + */ + public function testShouldNotClearTheSubmittedBase64DataIfSubmitted(array $submittedData): void + { + $form = $this->factory->create(); + $event = new FormEvent($form, array_merge($submittedData, ['base64' => 'foo'])); + + $listener = new ClearBase64OnDeleteListener(); + $listener($event); + + $data = $event->getData(); + \assert(\is_array($data)); + + $this->assertSame('foo', $data['base64']); + } + + public function testShouldEndUpWithNullBase64DataIfSubmittedWithNullData(): void + { + $form = $this->factory->create()->add('image', ImageType::class, self::ALLOW_DELETE_OPTIONS); + $event = new FormEvent($form, null); + + $listener = new ClearBase64OnDeleteListener(); + $listener($event); + + $this->assertNull($event->getData()); + } + + public function deletableSubmittedData(): iterable + { + yield 'the "delete" checkbox checked' => [ + ['delete' => true], + ]; + } + + public function notDeletableSubmittedData(): iterable + { + yield 'no "delete" checkbox data' => [ + [], + ]; + yield 'the "delete" checkbox not checked' => [ + ['delete' => false], + ]; + } +} diff --git a/tests/Unit/Form/EventListener/ImageType/DeleteFileListenerTest.php b/tests/Unit/Form/EventListener/ImageType/DeleteFileListenerTest.php new file mode 100644 index 0000000..c26ea26 --- /dev/null +++ b/tests/Unit/Form/EventListener/ImageType/DeleteFileListenerTest.php @@ -0,0 +1,106 @@ +storage + ->expects($this->once()) + ->method('resolvePath') + ->with($data, 'image') + ->willReturn($data->imageName) + ; + + $this->storage + ->expects($this->once()) + ->method('remove') + ->with($data, $this->isInstanceOf(PropertyMapping::class)) + ; + + $form = $this->factory + ->create(FormType::class, $data) + ->add('image', ImageType::class, self::ALLOW_DELETE_OPTIONS) + ; + + $form->get('image')->get('delete')->setData($delete); + + $listener = new DeleteFileListener($this->createUploadHandler()); + $listener(new FormEvent($form->get('image'), $form->get('image')->getViewData())); + } + + /** + * @dataProvider notDeletableConfig + * + * @param mixed $data + */ + public function testShouldNotTriggerRemovingTheFileFromTheFilesystemIfSubmitted( + $data, + array $options, + bool $delete = null + ): void { + $this->storage->expects($this->never())->method('remove'); + + $form = $this->factory + ->create(FormType::class, $data) + ->add('image', ImageType::class, $options) + ; + + if ($form->get('image')->has('delete')) { + $form->get('image')->get('delete')->setData($delete); + } + + $listener = new DeleteFileListener($this->createUploadHandler()); + $listener(new FormEvent($form->get('image'), $form->get('image')->getViewData())); + } + + public function testShouldCauseAnExceptionIfCreatedAsRootForm(): void + { + $this->expectException(\RuntimeException::class); + + $form = $this->factory->create(); + + $listener = new DeleteFileListener($this->createUploadHandler()); + $listener(new FormEvent($form, $form->getData())); + } + + public function testShouldCauseAnExceptionIfCreatedWithAnArrayAsData(): void + { + $this->expectException(UnexpectedTypeException::class); + + $form = $this->factory->create(FormType::class, [])->add('image'); + + $listener = new DeleteFileListener($this->createUploadHandler()); + $listener(new FormEvent($form->get('image'), $form->get('image')->getViewData())); + } + + public function deletableConfig(): iterable + { + yield 'the "delete" checkbox checked when created with an object related to an existing file' => [ + Book::withFile('/tmp/foo.png'), + true, + ]; + } + + public function notDeletableConfig(): iterable + { + yield 'no data (null)' => [null, self::ALLOW_DELETE_OPTIONS]; + yield 'no "delete" checkbox' => [Book::withoutFile(), ['allow_delete' => false]]; + yield 'the "delete" checkbox not checked' => [Book::withoutFile(), self::ALLOW_DELETE_OPTIONS, false]; + } +} diff --git a/tests/Unit/Form/ImageTypeTestCase.php b/tests/Unit/Form/ImageTypeTestCase.php new file mode 100644 index 0000000..a606375 --- /dev/null +++ b/tests/Unit/Form/ImageTypeTestCase.php @@ -0,0 +1,92 @@ + true, 'required' => false]; + + /** + * @var MockObject&StorageInterface + */ + protected MockObject $storage; + + /** + * @var MockObject&FileInjectorInterface + */ + private MockObject $fileInjector; + + /** + * @var MockObject&EventDispatcherInterface + */ + private MockObject $eventDispatcher; + + /** + * @var MockObject&ContainerInterface + */ + private MockObject $container; + + /** + * @var MockObject&AdvancedMetadataFactoryInterface + */ + private MockObject $advancedMetadataFactory; + + protected function setUp(): void + { + $this->storage = $this->createMock(StorageInterface::class); + $this->fileInjector = $this->createMock(FileInjectorInterface::class); + $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $this->container = $this->createMock(ContainerInterface::class); + $this->advancedMetadataFactory = $this->createMock(AdvancedMetadataFactoryInterface::class); + + $classMetadata = new ClassMetadata('anonymous'); + $classMetadata->fields['image'] = ['mapping' => 'default', 'name' => 'imageName']; + + $metadata = new ClassHierarchyMetadata(); + $metadata->addClassMetadata($classMetadata); + + $this->advancedMetadataFactory->method('getMetadataForClass')->willReturn($metadata); + + parent::setUp(); + } + + protected function getExtensions(): array + { + $type = new ImageType($this->storage, $this->createUploadHandler()); + + return [ + new PreloadedExtension([$type], []), + ]; + } + + protected function createUploadHandler(): UploadHandler + { + return new UploadHandler( + new PropertyMappingFactory( + $this->container, + new MetadataReader($this->advancedMetadataFactory), + ['default' => []] + ), + $this->storage, + $this->fileInjector, + $this->eventDispatcher + ); + } +} diff --git a/tests/Unit/Form/Type/ImageTypeTest.php b/tests/Unit/Form/Type/ImageTypeTest.php new file mode 100644 index 0000000..7aeb3c9 --- /dev/null +++ b/tests/Unit/Form/Type/ImageTypeTest.php @@ -0,0 +1,123 @@ +storage->method('resolvePath')->willReturn('/tmp/foo.png'); + + $options = ['allow_delete' => $allowDelete, 'required' => $required]; + $data = Book::withoutFile(); + + $form = $this->factory->create(FormType::class, $data)->add('image', ImageType::class, $options); + + $this->assertTrue($form->get('image')->has('delete')); + } + + /** + * @dataProvider notDeletableOptions + */ + public function testShouldNotHaveADeleteCheckboxIfCreated(bool $allowDelete, bool $required): void + { + $options = ['allow_delete' => $allowDelete, 'required' => $required]; + + $form = $this->factory->create()->add('image', ImageType::class, $options); + + $this->assertFalse($form->get('image')->has('delete')); + } + + /** + * @dataProvider deletableOptions + */ + public function testShouldClearTheBase64SubmittedDataIfCreated(bool $allowDelete, bool $required): void + { + $options = ['allow_delete' => $allowDelete, 'required' => $required]; + + $form = $this->factory->create()->add('image', ImageType::class, $options); + $form->submit(['image' => ['delete' => true, 'base64' => 'foo']]); + + $this->assertTrue($form->isSynchronized()); + $this->assertNull($form->get('image')->get('base64')->getData()); + } + + /** + * @dataProvider notDeletableOptions + */ + public function testShouldNotClearTheBase64SubmittedDataIfCreated(bool $allowDelete, bool $required): void + { + $options = ['allow_delete' => $allowDelete, 'required' => $required]; + + $form = $this->factory->create()->add('image', ImageType::class, $options); + $form->submit(['image' => ['delete' => true, 'base64' => 'foo']]); + + $this->assertTrue($form->isSynchronized()); + $this->assertSame('foo', $form->get('image')->get('base64')->getData()); + } + + /** + * @dataProvider deletableOptions + */ + public function testShouldRemoveTheFileFromTheFilesystemIfCreated(bool $allowDelete, bool $required): void + { + $options = ['allow_delete' => $allowDelete, 'required' => $required]; + $data = Book::withFile('/tmp/foo.png'); + + $this->storage + ->expects($this->once()) + ->method('resolvePath') + ->with($data, 'image') + ->willReturn($data->imageName) + ; + + $this->storage + ->expects($this->once()) + ->method('remove') + ->with($data, $this->isInstanceOf(PropertyMapping::class)) + ; + + $form = $this->factory->create(FormType::class, $data)->add('image', ImageType::class, $options); + $form->submit(['image' => ['delete' => true]]); + + $this->assertTrue($form->isSynchronized()); + } + + /** + * @dataProvider notDeletableOptions + */ + public function testShouldNotRemoveTheFileFromTheFilesystemIfCreated(bool $allowDelete, bool $required): void + { + $options = ['allow_delete' => $allowDelete, 'required' => $required]; + + $this->storage->expects($this->never())->method('remove'); + + $form = $this->factory->create()->add('image', ImageType::class, $options); + $form->submit([]); + + $this->assertTrue($form->isSynchronized()); + } + + public function deletableOptions(): iterable + { + yield 'option "allow_delete" set to true and option "required" set to false' => [true, false]; + } + + public function notDeletableOptions(): iterable + { + yield 'option "allow_delete" set to false and option "required" set to false' => [false, false]; + yield 'option "allow_delete" set to false and option "required" set to true' => [false, true]; + yield 'option "allow_delete" set to true and option "required" set to true' => [true, true]; + } +}