Completed
Push — develop ( f71bd8...4fb2c6 )
by Alejandro
17s queued 14s
created

UrlShortenerTest::shortCodeIsProperlyParsed()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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