workDir = sys_get_temp_dir() . '/book-cover-test-' . bin2hex(random_bytes(4)); $filesystem = new Filesystem(); $filesystem->mkdir($this->workDir); $filesystem->remove(static::getContainer()->getParameter('kernel.project_dir') . '/var/test/covers'); $this->createBook(1, 'Covered'); } protected function tearDown(): void { new Filesystem()->remove($this->workDir); parent::tearDown(); } public function testOriginalIsKeptAndThumbnailsAreCreated(): void { $file = $this->createImage(1000, 2000, 'cover.png'); $name = $this->uploadCover($file); self::assertMatchesRegularExpression('/^[0-9a-f]{32}\.png$/', $name); self::assertFileEquals($file, $this->storage()->originalPath($name)); self::assertSame([113, 225], $this->imageSize($this->storage()->thumbnailPath($name, 'list'))); self::assertSame([450, 900], $this->imageSize($this->storage()->thumbnailPath($name, 'details'))); self::assertSame(\IMAGETYPE_JPEG, exif_imagetype($this->storage()->thumbnailPath($name, 'list'))); } public function testSmallImagesAreNotEnlarged(): void { $name = $this->uploadCover($this->createImage(80, 100, 'small.png')); self::assertSame([80, 100], $this->imageSize($this->storage()->thumbnailPath($name, 'details'))); } public function testThumbnailsAreShownOnListAndDetails(): void { $name = $this->uploadCover($this->createImage(300, 400, 'cover.png')); $crawler = $this->client->request('GET', '/list'); self::assertSame('/covers/' . $name . '.list.jpg', $crawler->filter('.tile-cover')->attr('src')); $crawler = $this->client->request('GET', '/book/1'); self::assertSame('/covers/' . $name . '.details.jpg', $crawler->filter('.book-cover')->attr('src')); self::assertSame('/cover/' . $name, $crawler->filter('.book-cover')->ancestors()->first()->attr('href')); } public function testOriginalCanBeDownloaded(): void { $file = $this->createImage(300, 400, 'cover.png'); $name = $this->uploadCover($file); $this->client->request('GET', '/cover/' . $name); self::assertResponseIsSuccessful(); self::assertResponseHeaderSame('Content-Type', 'image/png'); self::assertResponseHeaderSame('X-Content-Type-Options', 'nosniff'); } public function testNewCoverReplacesTheOldOne(): void { $first = $this->uploadCover($this->createImage(300, 400, 'first.png')); $second = $this->uploadCover($this->createImage(300, 400, 'second.png')); self::assertNotSame($first, $second); self::assertFileDoesNotExist($this->storage()->originalPath($first)); self::assertFileDoesNotExist($this->storage()->thumbnailPath($first, 'list')); self::assertFileExists($this->storage()->originalPath($second)); } public function testCoverCanBeRemoved(): void { $name = $this->uploadCover($this->createImage(300, 400, 'cover.png')); $crawler = $this->client->request('GET', '/book/1'); $this->client->submit($crawler->selectButton('Remove cover')->form()); self::assertNull($this->findBook(1)?->getCover()); self::assertFileDoesNotExist($this->storage()->originalPath($name)); self::assertFileDoesNotExist($this->storage()->thumbnailPath($name, 'details')); } public function testExifRotationIsApplied(): void { $fixture = \dirname(__DIR__) . '/Fixtures/rotated-orientation-6.jpg'; $file = $this->workDir . '/photo.jpg'; copy($fixture, $file); $name = $this->uploadCover($file); self::assertSame([20, 40], $this->imageSize($this->storage()->thumbnailPath($name, 'details'))); } public function testFilesThatAreNotImagesAreRejected(): void { $file = $this->workDir . '/cover.png'; file_put_contents($file, 'client->request('GET', '/book/1'); $form = $crawler->selectButton('Upload cover')->form(); $form['cover[cover]']->upload($file); $this->client->submit($form); $this->client->followRedirect(); self::assertSelectorExists('.toast-error'); self::assertNull($this->findBook(1)?->getCover()); } public function testCoverFollowsTheBookWhenOusted(): void { $name = $this->uploadCover($this->createImage(300, 400, 'cover.png')); $crawler = $this->client->request('GET', '/book/1'); $this->client->submit($crawler->selectButton('Take out')->form(['oust[reason]' => 'Lost'])); $this->entityManager()->clear(); self::assertSame($name, $this->entityManager()->find(OustedBook::class, 1)?->getCover()); self::assertFileExists($this->storage()->originalPath($name)); $this->client->request('GET', '/ousted/1'); self::assertSelectorExists('.book-cover'); } public function testOriginalsCannotBeReachedOutsideTheCoverPattern(): void { $this->client->request('GET', '/cover/..%2F..%2F.env'); self::assertResponseStatusCodeSame(404); } private function uploadCover(string $file): string { $crawler = $this->client->request('GET', '/book/1'); $form = $crawler->selectButton('Upload cover')->form(); $form['cover[cover]']->upload($file); $this->client->submit($form); self::assertResponseRedirects('/book/1'); $name = $this->findBook(1)?->getCover(); self::assertNotNull($name); return $name; } private function createImage(int $width, int $height, string $fileName): string { $path = $this->workDir . '/' . $fileName; $image = imagecreatetruecolor($width, $height); // Random pixels make each generated file unique. imagesetpixel($image, 0, 0, random_int(1, 0xFFFFFF)); imagepng($image, $path); return $path; } /** * @return array{int, int} */ private function imageSize(string $path): array { [$width, $height] = getimagesize($path); return [$width, $height]; } private function storage(): CoverStorage { return static::getContainer()->get(CoverStorage::class); } }