Passed
Push — master ( 4437d5...b3ea29 )
by Alejandro
04:00 queued 13s
created

exceptionIsThrownWhenNonUniqueSlugIsProvided()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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