<?php
|
declare(strict_types=1);
|
|
namespace App\Tests\Functional;
|
|
use App\Entity\OustedBook;
|
use App\Service\CoverStorage;
|
use App\Tests\DatabaseTestCase;
|
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
|
use Symfony\Component\Filesystem\Filesystem;
|
|
#[RequiresPhpExtension('gd')]
|
final class CoverTest extends DatabaseTestCase {
|
private string $workDir;
|
|
protected function setUp(): void {
|
parent::setUp();
|
|
$this->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, '<?php echo "not an image";');
|
|
$crawler = $this->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);
|
}
|
}
|