Completed
Pull Request — master (#506)
by Alejandro
13:13
created

transactionIsRolledBackAndExceptionRethrownWhenExceptionIsThrown()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 10
nc 1
nop 0
dl 0
loc 15
rs 9.9332
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ShlinkioTest\Shlink\Core\Service;
6
7
use Cake\Chronos\Chronos;
8
use Doctrine\Common\Collections\ArrayCollection;
9
use Doctrine\DBAL\Connection;
10
use Doctrine\ORM\EntityManagerInterface;
11
use Doctrine\ORM\ORMException;
12
use GuzzleHttp\ClientInterface;
13
use GuzzleHttp\Exception\ClientException;
14
use GuzzleHttp\Psr7\Request;
15
use PHPUnit\Framework\TestCase;
16
use Prophecy\Argument;
17
use Prophecy\Prophecy\ObjectProphecy;
18
use Shlinkio\Shlink\Core\Entity\ShortUrl;
19
use Shlinkio\Shlink\Core\Entity\Tag;
20
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
21
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
22
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
23
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
24
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
25
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
26
use Shlinkio\Shlink\Core\Service\UrlShortener;
27
use Zend\Diactoros\Uri;
28
29
use function array_map;
30
31
class UrlShortenerTest extends TestCase
32
{
33
    /** @var UrlShortener */
34
    private $urlShortener;
35
    /** @var ObjectProphecy */
36
    private $em;
37
    /** @var ObjectProphecy */
38
    private $httpClient;
39
40
    public function setUp(): void
41
    {
42
        $this->httpClient = $this->prophesize(ClientInterface::class);
43
44
        $this->em = $this->prophesize(EntityManagerInterface::class);
45
        $conn = $this->prophesize(Connection::class);
46
        $conn->isTransactionActive()->willReturn(false);
47
        $this->em->getConnection()->willReturn($conn->reveal());
48
        $this->em->flush()->willReturn(null);
49
        $this->em->commit()->willReturn(null);
50
        $this->em->beginTransaction()->willReturn(null);
51
        $this->em->persist(Argument::any())->will(function ($arguments) {
52
            /** @var ShortUrl $shortUrl */
53
            $shortUrl = $arguments[0];
54
            $shortUrl->setId('10');
55
        });
56
        $repo = $this->prophesize(ShortUrlRepository::class);
57
        $repo->shortCodeIsInUse(Argument::cetera())->willReturn(false);
58
        $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
59
60
        $this->setUrlShortener(false);
61
    }
62
63
    private function setUrlShortener(bool $urlValidationEnabled): void
64
    {
65
        $this->urlShortener = new UrlShortener(
66
            $this->httpClient->reveal(),
67
            $this->em->reveal(),
68
            new UrlShortenerOptions(['validate_url' => $urlValidationEnabled])
69
        );
70
    }
71
72
    /** @test */
73
    public function urlIsProperlyShortened(): void
74
    {
75
        $shortUrl = $this->urlShortener->urlToShortCode(
76
            new Uri('http://foobar.com/12345/hello?foo=bar'),
77
            [],
78
            ShortUrlMeta::createEmpty()
79
        );
80
81
        $this->assertEquals('http://foobar.com/12345/hello?foo=bar', $shortUrl->getLongUrl());
82
    }
83
84
    /** @test */
85
    public function shortCodeIsRegeneratedIfAlreadyInUse(): void
86
    {
87
        $callIndex = 0;
88
        $expectedCalls = 3;
89
        $repo = $this->prophesize(ShortUrlRepository::class);
90
        $shortCodeIsInUse = $repo->shortCodeIsInUse(Argument::cetera())->will(
91
            function () use (&$callIndex, $expectedCalls) {
92
                $callIndex++;
93
                return $callIndex < $expectedCalls;
94
            }
95
        );
96
        $repo->findBy(Argument::cetera())->willReturn([]);
97
        $getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
98
99
        $shortUrl = $this->urlShortener->urlToShortCode(
100
            new Uri('http://foobar.com/12345/hello?foo=bar'),
101
            [],
102
            ShortUrlMeta::createEmpty()
103
        );
104
105
        $this->assertEquals('http://foobar.com/12345/hello?foo=bar', $shortUrl->getLongUrl());
106
        $getRepo->shouldBeCalledTimes($expectedCalls);
107
        $shortCodeIsInUse->shouldBeCalledTimes($expectedCalls);
108
    }
109
110
    /** @test */
111
    public function transactionIsRolledBackAndExceptionRethrownWhenExceptionIsThrown(): void
112
    {
113
        $conn = $this->prophesize(Connection::class);
114
        $conn->isTransactionActive()->willReturn(true);
115
        $this->em->getConnection()->willReturn($conn->reveal());
116
        $this->em->rollback()->shouldBeCalledOnce();
117
        $this->em->close()->shouldBeCalledOnce();
118
119
        $this->em->flush()->willThrow(new ORMException());
120
121
        $this->expectException(ORMException::class);
122
        $this->urlShortener->urlToShortCode(
123
            new Uri('http://foobar.com/12345/hello?foo=bar'),
124
            [],
125
            ShortUrlMeta::createEmpty()
126
        );
127
    }
128
129
    /** @test */
130
    public function exceptionIsThrownWhenUrlDoesNotExist(): void
131
    {
132
        $this->setUrlShortener(true);
133
134
        $this->httpClient->request(Argument::cetera())->willThrow(
135
            new ClientException('', $this->prophesize(Request::class)->reveal())
136
        );
137
138
        $this->expectException(InvalidUrlException::class);
139
        $this->urlShortener->urlToShortCode(
140
            new Uri('http://foobar.com/12345/hello?foo=bar'),
141
            [],
142
            ShortUrlMeta::createEmpty()
143
        );
144
    }
145
146
    /** @test */
147
    public function exceptionIsThrownWhenNonUniqueSlugIsProvided(): void
148
    {
149
        $repo = $this->prophesize(ShortUrlRepository::class);
150
        $shortCodeIsInUse = $repo->shortCodeIsInUse('custom-slug', null)->willReturn(true);
151
        $repo->findBy(Argument::cetera())->willReturn([]);
152
        $getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
153
154
        $shortCodeIsInUse->shouldBeCalledOnce();
155
        $getRepo->shouldBeCalled();
156
        $this->expectException(NonUniqueSlugException::class);
157
158
        $this->urlShortener->urlToShortCode(
159
            new Uri('http://foobar.com/12345/hello?foo=bar'),
160
            [],
161
            ShortUrlMeta::createFromRawData(['customSlug' => 'custom-slug'])
162
        );
163
    }
164
165
    /**
166
     * @test
167
     * @dataProvider provideExistingShortUrls
168
     */
169
    public function existingShortUrlIsReturnedWhenRequested(
170
        string $url,
171
        array $tags,
172
        ShortUrlMeta $meta,
173
        ShortUrl $expected
174
    ): void {
175
        $repo = $this->prophesize(ShortUrlRepository::class);
176
        $findExisting = $repo->findBy(Argument::any())->willReturn([$expected]);
177
        $getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
178
179
        $result = $this->urlShortener->urlToShortCode(new Uri($url), $tags, $meta);
180
181
        $findExisting->shouldHaveBeenCalledOnce();
182
        $getRepo->shouldHaveBeenCalledOnce();
183
        $this->em->persist(Argument::cetera())->shouldNotHaveBeenCalled();
184
        $this->assertSame($expected, $result);
185
    }
186
187
    public function provideExistingShortUrls(): iterable
188
    {
189
        $url = 'http://foo.com';
190
191
        yield [$url, [], ShortUrlMeta::createFromRawData(['findIfExists' => true]), new ShortUrl($url)];
192
        yield [$url, [], ShortUrlMeta::createFromRawData(
193
            ['findIfExists' => true, 'customSlug' => 'foo']
194
        ), new ShortUrl($url)];
195
        yield [
196
            $url,
197
            ['foo', 'bar'],
198
            ShortUrlMeta::createFromRawData(['findIfExists' => true]),
199
            (new ShortUrl($url))->setTags(new ArrayCollection([new Tag('bar'), new Tag('foo')])),
200
        ];
201
        yield [
202
            $url,
203
            [],
204
            ShortUrlMeta::createFromRawData(['findIfExists' => true, 'maxVisits' => 3]),
205
            new ShortUrl($url, ShortUrlMeta::createFromRawData(['maxVisits' => 3])),
206
        ];
207
        yield [
208
            $url,
209
            [],
210
            ShortUrlMeta::createFromRawData(['findIfExists' => true, 'validSince' => Chronos::parse('2017-01-01')]),
211
            new ShortUrl($url, ShortUrlMeta::createFromRawData(['validSince' => Chronos::parse('2017-01-01')])),
212
        ];
213
        yield [
214
            $url,
215
            [],
216
            ShortUrlMeta::createFromRawData(['findIfExists' => true, 'validUntil' => Chronos::parse('2017-01-01')]),
217
            new ShortUrl($url, ShortUrlMeta::createFromRawData(['validUntil' => Chronos::parse('2017-01-01')])),
218
        ];
219
        yield [
220
            $url,
221
            [],
222
            ShortUrlMeta::createFromRawData(['findIfExists' => true, 'domain' => 'example.com']),
223
            new ShortUrl($url, ShortUrlMeta::createFromRawData(['domain' => 'example.com'])),
224
        ];
225
        yield [
226
            $url,
227
            ['baz', 'foo', 'bar'],
228
            ShortUrlMeta::createFromRawData([
229
                'findIfExists' => true,
230
                'validUntil' => Chronos::parse('2017-01-01'),
231
                'maxVisits' => 4,
232
            ]),
233
            (new ShortUrl($url, ShortUrlMeta::createFromRawData([
234
                'validUntil' => Chronos::parse('2017-01-01'),
235
                'maxVisits' => 4,
236
            ])))->setTags(new ArrayCollection([new Tag('foo'), new Tag('bar'), new Tag('baz')])),
237
        ];
238
    }
239
240
    /** @test */
241
    public function properExistingShortUrlIsReturnedWhenMultipleMatch(): void
242
    {
243
        $url = 'http://foo.com';
244
        $tags = ['baz', 'foo', 'bar'];
245
        $meta = ShortUrlMeta::createFromRawData([
246
            'findIfExists' => true,
247
            'validUntil' => Chronos::parse('2017-01-01'),
248
            'maxVisits' => 4,
249
        ]);
250
        $tagsCollection = new ArrayCollection(array_map(function (string $tag) {
251
            return new Tag($tag);
252
        }, $tags));
253
        $expected = (new ShortUrl($url, $meta))->setTags($tagsCollection);
254
255
        $repo = $this->prophesize(ShortUrlRepository::class);
256
        $findExisting = $repo->findBy(Argument::any())->willReturn([
257
            new ShortUrl($url),
258
            new ShortUrl($url, $meta),
259
            $expected,
260
            (new ShortUrl($url))->setTags($tagsCollection),
261
        ]);
262
        $getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
263
264
        $result = $this->urlShortener->urlToShortCode(new Uri($url), $tags, $meta);
265
266
        $this->assertSame($expected, $result);
267
        $findExisting->shouldHaveBeenCalledOnce();
268
        $getRepo->shouldHaveBeenCalledOnce();
269
    }
270
271
    /** @test */
272
    public function shortCodeIsProperlyParsed(): void
273
    {
274
        $shortUrl = new ShortUrl('expected_url');
275
        $shortCode = $shortUrl->getShortCode();
276
277
        $repo = $this->prophesize(ShortUrlRepositoryInterface::class);
278
        $repo->findOneByShortCode($shortCode, null)->willReturn($shortUrl);
279
        $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
280
281
        $url = $this->urlShortener->shortCodeToUrl($shortCode);
282
        $this->assertSame($shortUrl, $url);
283
    }
284
}
285