Passed
Push — master ( 86b6f9...3f7190 )
by Adrien
02:48
created

testWillServeThumbnailJpgByDefault()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
c 0
b 0
f 0
dl 0
loc 21
rs 9.7998
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace EcodevTests\Felix\Handler;
6
7
use Doctrine\Persistence\ObjectRepository;
8
use Ecodev\Felix\Handler\ImageHandler;
9
use Ecodev\Felix\Model\Image;
10
use Ecodev\Felix\Service\ImageResizer;
11
use Exception;
12
use Laminas\Diactoros\ServerRequest;
13
use org\bovigo\vfs\vfsStream;
14
use PHPUnit\Framework\TestCase;
15
use Psr\Http\Message\ResponseInterface;
16
use Psr\Http\Message\ServerRequestInterface;
17
18
class ImageHandlerTest extends TestCase
19
{
20
    protected function setUp(): void
21
    {
22
        // Minimal binary headers to cheat mime detection
23
        $virtualFileSystem = [
24
            'image.png' => '',
25
            'image-100.jpg' => "\xff\xd8\xff\xe0\x00\x10\x4a\x46\x49\x46\x00\x01\x01\x01\x00\x60",
26
            'image-100.webp' => 'RIFF4<..WEBPVP8',
27
        ];
28
29
        vfsStream::setup('felix', null, $virtualFileSystem);
30
    }
31
32
    public function testWillServeThumbnailJpgByDefault(): void
33
    {
34
        $image = $this->createImageMock();
35
        $repository = $this->createRepositoryMock($image);
36
37
        $maxHeight = 100;
38
        $imageResizer = $this->createMock(ImageResizer::class);
39
        $imageResizer->expects(self::once())
40
            ->method('resize')
41
            ->with($image, $maxHeight, false)
42
            ->willReturn('vfs://felix/image-100.jpg');
43
44
        // A request without accept header
45
        $request = new ServerRequest();
46
        $request = $request->withAttribute('maxHeight', $maxHeight);
47
48
        $response = $this->handle($repository, $imageResizer, $request);
49
50
        self::assertSame('image/jpeg', $response->getHeaderLine('content-type'));
51
        self::assertSame('16', $response->getHeaderLine('content-length'));
52
        self::assertSame('max-age=21600', $response->getHeaderLine('cache-control'));
53
    }
54
55
    public function testWillServeThumbnailWebpIfAccepted(): void
56
    {
57
        $image = $this->createImageMock();
58
        $repository = $this->createRepositoryMock($image);
59
60
        $maxHeight = 100;
61
        $imageResizer = $this->createMock(ImageResizer::class);
62
        $imageResizer->expects(self::once())
63
            ->method('resize')
64
            ->with($image, $maxHeight, true)
65
            ->willReturn('vfs://felix/image-100.webp');
66
67
        // A request specifically accepting webp images
68
        $request = new ServerRequest();
69
        $request = $request->withAttribute('maxHeight', $maxHeight)
70
            ->withHeader('accept', 'text/html, image/webp, */*;q=0.8');
71
72
        $response = $this->handle($repository, $imageResizer, $request);
73
74
        self::assertSame('image/webp', $response->getHeaderLine('content-type'));
75
        self::assertSame('15', $response->getHeaderLine('content-length'));
76
        self::assertSame('max-age=21600', $response->getHeaderLine('cache-control'));
77
    }
78
79
    public function testWillServeOriginalWebpIfAccepted(): void
80
    {
81
        $image = $this->createImageMock('vfs://felix/image-100.webp');
82
        $repository = $this->createRepositoryMock($image);
83
84
        $imageResizer = $this->createMock(ImageResizer::class);
85
86
        // A request specifically accepting webp images
87
        $request = new ServerRequest();
88
        $request = $request
89
            ->withHeader('accept', 'text/html, image/webp, */*;q=0.8');
90
91
        $response = $this->handle($repository, $imageResizer, $request);
92
93
        self::assertSame('image/webp', $response->getHeaderLine('content-type'));
94
        self::assertSame('15', $response->getHeaderLine('content-length'));
95
        self::assertSame('max-age=21600', $response->getHeaderLine('cache-control'));
96
    }
97
98
    public function testWillServeOriginalJpgIfWebpNotAccepted(): void
99
    {
100
        $image = $this->createImageMock('vfs://felix/image-100.webp');
101
        $repository = $this->createRepositoryMock($image);
102
103
        $imageResizer = $this->createMock(ImageResizer::class);
104
        $imageResizer->expects(self::once())
105
            ->method('webpToJpg')
106
            ->with($image)
107
            ->willReturn('vfs://felix/image-100.jpg');
108
109
        // A request specifically accepting webp images
110
        $request = new ServerRequest();
111
112
        $response = $this->handle($repository, $imageResizer, $request);
113
114
        self::assertSame('image/jpeg', $response->getHeaderLine('content-type'));
115
        self::assertSame('16', $response->getHeaderLine('content-length'));
116
        self::assertSame('max-age=21600', $response->getHeaderLine('cache-control'));
117
    }
118
119
    public function testWillErrorIfImageNotFoundInDatabase(): void
120
    {
121
        $repository = $this->createRepositoryMock(null);
122
        $imageResizer = $this->createMock(ImageResizer::class);
123
        $request = new ServerRequest();
124
125
        $response = $this->handle($repository, $imageResizer, $request);
126
        $this->assertError(['error' => 'Image 0 not found in database'], $response);
127
    }
128
129
    public function testWillErrorIfImageNotFoundOnDisk(): void
130
    {
131
        $image = $this->createImageMock('vfs://felix/totally-non-existing-path');
132
        $repository = $this->createRepositoryMock($image);
133
        $imageResizer = $this->createMock(ImageResizer::class);
134
        $request = new ServerRequest();
135
136
        $response = $this->handle($repository, $imageResizer, $request);
137
        $this->assertError(['error' => 'Image for image 0 not found on disk, or not readable'], $response);
138
    }
139
140
    private function assertError(array $expected, ResponseInterface $response): void
141
    {
142
        self::assertSame('application/json', $response->getHeaderLine('content-type'));
143
        self::assertSame('', $response->getHeaderLine('content-length'));
144
        self::assertSame('', $response->getHeaderLine('cache-control'));
145
        self::assertSame($expected, json_decode($response->getBody()->getContents(), true));
146
    }
147
148
    private function handle(ObjectRepository $repository, ImageResizer $imageResizer, ServerRequestInterface $request): ResponseInterface
149
    {
150
        $handler = new ImageHandler($repository, $imageResizer);
151
152
        return $handler->handle($request);
153
    }
154
155
    private function createImageMock(string $path = 'vfs://felix/image.png'): Image
156
    {
157
        $image = $this->createMock(Image::class);
158
        $image->expects(self::once())->method('getPath')->willReturn($path);
159
        $image->expects(self::atMost(1))->method('getMime')->willReturn(match ($path) {
160
            'vfs://felix/image.png' => 'image/png',
161
            'vfs://felix/image-100.jpg' => 'image/jpeg',
162
            'vfs://felix/image-100.webp' => 'image/webp',
163
            'vfs://felix/totally-non-existing-path' => '',
164
            default => throw new Exception('Unsupported :' . $path),
165
        });
166
167
        return $image;
168
    }
169
170
    private function createRepositoryMock(?Image $image): ObjectRepository
171
    {
172
        $repository = $this->createMock(ObjectRepository::class);
173
        $repository->expects(self::once())->method('find')->willReturn($image);
174
175
        return $repository;
176
    }
177
}
178