Passed
Pull Request — master (#695)
by Alejandro
05:41
created

UrlShortenerTest::setUrlShortener()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 1
dl 0
loc 6
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\Domain\Resolver\SimpleDomainResolver;
17
use Shlinkio\Shlink\Core\Entity\ShortUrl;
18
use Shlinkio\Shlink\Core\Entity\Tag;
19
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
20
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
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
        $this->urlValidator->validateUrl('http://foobar.com/12345/hello?foo=bar')->will(
37
            function (): void {
38
            },
39
        );
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): void {
49
            /** @var ShortUrl $shortUrl */
50
            [$shortUrl] = $arguments;
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->urlShortener = new UrlShortener(
58
            $this->urlValidator->reveal(),
59
            $this->em->reveal(),
60
            new SimpleDomainResolver(),
61
        );
62
    }
63
64
    /** @test */
65
    public function urlIsProperlyShortened(): void
66
    {
67
        $shortUrl = $this->urlShortener->urlToShortCode(
68
            new Uri('http://foobar.com/12345/hello?foo=bar'),
69
            [],
70
            ShortUrlMeta::createEmpty(),
71
        );
72
73
        $this->assertEquals('http://foobar.com/12345/hello?foo=bar', $shortUrl->getLongUrl());
74
    }
75
76
    /** @test */
77
    public function shortCodeIsRegeneratedIfAlreadyInUse(): void
78
    {
79
        $callIndex = 0;
80
        $expectedCalls = 3;
81
        $repo = $this->prophesize(ShortUrlRepository::class);
82
        $shortCodeIsInUse = $repo->shortCodeIsInUse(Argument::cetera())->will(
83
            function () use (&$callIndex, $expectedCalls) {
84
                $callIndex++;
85
                return $callIndex < $expectedCalls;
86
            },
87
        );
88
        $repo->findBy(Argument::cetera())->willReturn([]);
89
        $getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
90
91
        $shortUrl = $this->urlShortener->urlToShortCode(
92
            new Uri('http://foobar.com/12345/hello?foo=bar'),
93
            [],
94
            ShortUrlMeta::createEmpty(),
95
        );
96
97
        $this->assertEquals('http://foobar.com/12345/hello?foo=bar', $shortUrl->getLongUrl());
98
        $getRepo->shouldBeCalledTimes($expectedCalls);
99
        $shortCodeIsInUse->shouldBeCalledTimes($expectedCalls);
100
    }
101
102
    /** @test */
103
    public function transactionIsRolledBackAndExceptionRethrownWhenExceptionIsThrown(): void
104
    {
105
        $conn = $this->prophesize(Connection::class);
106
        $conn->isTransactionActive()->willReturn(true);
107
        $this->em->getConnection()->willReturn($conn->reveal());
108
        $this->em->rollback()->shouldBeCalledOnce();
109
        $this->em->close()->shouldBeCalledOnce();
110
111
        $this->em->flush()->willThrow(new ORMException());
112
113
        $this->expectException(ORMException::class);
114
        $this->urlShortener->urlToShortCode(
115
            new Uri('http://foobar.com/12345/hello?foo=bar'),
116
            [],
117
            ShortUrlMeta::createEmpty(),
118
        );
119
    }
120
121
    /** @test */
122
    public function exceptionIsThrownWhenNonUniqueSlugIsProvided(): void
123
    {
124
        $repo = $this->prophesize(ShortUrlRepository::class);
125
        $shortCodeIsInUse = $repo->shortCodeIsInUse('custom-slug', null)->willReturn(true);
126
        $repo->findBy(Argument::cetera())->willReturn([]);
127
        $getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
128
129
        $shortCodeIsInUse->shouldBeCalledOnce();
130
        $getRepo->shouldBeCalled();
131
        $this->expectException(NonUniqueSlugException::class);
132
133
        $this->urlShortener->urlToShortCode(
134
            new Uri('http://foobar.com/12345/hello?foo=bar'),
135
            [],
136
            ShortUrlMeta::fromRawData(['customSlug' => 'custom-slug']),
137
        );
138
    }
139
140
    /**
141
     * @test
142
     * @dataProvider provideExistingShortUrls
143
     */
144
    public function existingShortUrlIsReturnedWhenRequested(
145
        string $url,
146
        array $tags,
147
        ShortUrlMeta $meta,
148
        ShortUrl $expected
149
    ): void {
150
        $repo = $this->prophesize(ShortUrlRepository::class);
151
        $findExisting = $repo->findBy(Argument::any())->willReturn([$expected]);
152
        $getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
153
154
        $result = $this->urlShortener->urlToShortCode(new Uri($url), $tags, $meta);
155
156
        $findExisting->shouldHaveBeenCalledOnce();
157
        $getRepo->shouldHaveBeenCalledOnce();
158
        $this->em->persist(Argument::cetera())->shouldNotHaveBeenCalled();
159
        $this->urlValidator->validateUrl(Argument::cetera())->shouldNotHaveBeenCalled();
160
        $this->assertSame($expected, $result);
161
    }
162
163
    public function provideExistingShortUrls(): iterable
164
    {
165
        $url = 'http://foo.com';
166
167
        yield [$url, [], ShortUrlMeta::fromRawData(['findIfExists' => true]), new ShortUrl($url)];
168
        yield [$url, [], ShortUrlMeta::fromRawData(
169
            ['findIfExists' => true, 'customSlug' => 'foo'],
170
        ), new ShortUrl($url)];
171
        yield [
172
            $url,
173
            ['foo', 'bar'],
174
            ShortUrlMeta::fromRawData(['findIfExists' => true]),
175
            (new ShortUrl($url))->setTags(new ArrayCollection([new Tag('bar'), new Tag('foo')])),
176
        ];
177
        yield [
178
            $url,
179
            [],
180
            ShortUrlMeta::fromRawData(['findIfExists' => true, 'maxVisits' => 3]),
181
            new ShortUrl($url, ShortUrlMeta::fromRawData(['maxVisits' => 3])),
182
        ];
183
        yield [
184
            $url,
185
            [],
186
            ShortUrlMeta::fromRawData(['findIfExists' => true, 'validSince' => Chronos::parse('2017-01-01')]),
187
            new ShortUrl($url, ShortUrlMeta::fromRawData(['validSince' => Chronos::parse('2017-01-01')])),
188
        ];
189
        yield [
190
            $url,
191
            [],
192
            ShortUrlMeta::fromRawData(['findIfExists' => true, 'validUntil' => Chronos::parse('2017-01-01')]),
193
            new ShortUrl($url, ShortUrlMeta::fromRawData(['validUntil' => Chronos::parse('2017-01-01')])),
194
        ];
195
        yield [
196
            $url,
197
            [],
198
            ShortUrlMeta::fromRawData(['findIfExists' => true, 'domain' => 'example.com']),
199
            new ShortUrl($url, ShortUrlMeta::fromRawData(['domain' => 'example.com'])),
200
        ];
201
        yield [
202
            $url,
203
            ['baz', 'foo', 'bar'],
204
            ShortUrlMeta::fromRawData([
205
                'findIfExists' => true,
206
                'validUntil' => Chronos::parse('2017-01-01'),
207
                'maxVisits' => 4,
208
            ]),
209
            (new ShortUrl($url, ShortUrlMeta::fromRawData([
210
                'validUntil' => Chronos::parse('2017-01-01'),
211
                'maxVisits' => 4,
212
            ])))->setTags(new ArrayCollection([new Tag('foo'), new Tag('bar'), new Tag('baz')])),
213
        ];
214
    }
215
216
    /** @test */
217
    public function properExistingShortUrlIsReturnedWhenMultipleMatch(): void
218
    {
219
        $url = 'http://foo.com';
220
        $tags = ['baz', 'foo', 'bar'];
221
        $meta = ShortUrlMeta::fromRawData([
222
            'findIfExists' => true,
223
            'validUntil' => Chronos::parse('2017-01-01'),
224
            'maxVisits' => 4,
225
        ]);
226
        $tagsCollection = new ArrayCollection(array_map(fn (string $tag) => new Tag($tag), $tags));
227
        $expected = (new ShortUrl($url, $meta))->setTags($tagsCollection);
228
229
        $repo = $this->prophesize(ShortUrlRepository::class);
230
        $findExisting = $repo->findBy(Argument::any())->willReturn([
231
            new ShortUrl($url),
232
            new ShortUrl($url, $meta),
233
            $expected,
234
            (new ShortUrl($url))->setTags($tagsCollection),
235
        ]);
236
        $getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
237
238
        $result = $this->urlShortener->urlToShortCode(new Uri($url), $tags, $meta);
239
240
        $this->assertSame($expected, $result);
241
        $findExisting->shouldHaveBeenCalledOnce();
242
        $getRepo->shouldHaveBeenCalledOnce();
243
    }
244
}
245