Completed
Push — develop ( 0686ac...7ecc3a )
by Alejandro
18s queued 11s
created

importedShortUrlsAreSearchedAsExpected()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 13
c 1
b 0
f 0
dl 0
loc 19
rs 9.8333
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ShlinkioTest\Shlink\Core\Repository;
6
7
use Cake\Chronos\Chronos;
8
use Doctrine\Common\Collections\ArrayCollection;
9
use ReflectionObject;
10
use Shlinkio\Shlink\Common\Util\DateRange;
11
use Shlinkio\Shlink\Core\Entity\Domain;
12
use Shlinkio\Shlink\Core\Entity\ShortUrl;
13
use Shlinkio\Shlink\Core\Entity\Tag;
14
use Shlinkio\Shlink\Core\Entity\Visit;
15
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
16
use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering;
17
use Shlinkio\Shlink\Core\Model\Visitor;
18
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
19
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
20
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
21
use Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase;
22
23
use function count;
24
25
class ShortUrlRepositoryTest extends DatabaseTestCase
26
{
27
    use TagManagerTrait;
28
29
    protected const ENTITIES_TO_EMPTY = [
30
        Tag::class,
31
        Visit::class,
32
        ShortUrl::class,
33
        Domain::class,
34
    ];
35
36
    private ShortUrlRepository $repo;
37
38
    public function setUp(): void
39
    {
40
        $this->repo = $this->getEntityManager()->getRepository(ShortUrl::class);
41
    }
42
43
    /** @test */
44
    public function findOneWithDomainFallbackReturnsProperData(): void
45
    {
46
        $regularOne = new ShortUrl('foo', ShortUrlMeta::fromRawData(['customSlug' => 'foo']));
47
        $this->getEntityManager()->persist($regularOne);
48
49
        $withDomain = new ShortUrl('foo', ShortUrlMeta::fromRawData(
50
            ['domain' => 'example.com', 'customSlug' => 'domain-short-code'],
51
        ));
52
        $this->getEntityManager()->persist($withDomain);
53
54
        $withDomainDuplicatingRegular = new ShortUrl('foo_with_domain', ShortUrlMeta::fromRawData(
55
            ['domain' => 'doma.in', 'customSlug' => 'foo'],
56
        ));
57
        $this->getEntityManager()->persist($withDomainDuplicatingRegular);
58
59
        $this->getEntityManager()->flush();
60
61
        self::assertSame($regularOne, $this->repo->findOneWithDomainFallback($regularOne->getShortCode()));
62
        self::assertSame($regularOne, $this->repo->findOneWithDomainFallback(
63
            $withDomainDuplicatingRegular->getShortCode(),
64
        ));
65
        self::assertSame($withDomain, $this->repo->findOneWithDomainFallback(
66
            $withDomain->getShortCode(),
67
            'example.com',
68
        ));
69
        self::assertSame(
70
            $withDomainDuplicatingRegular,
71
            $this->repo->findOneWithDomainFallback($withDomainDuplicatingRegular->getShortCode(), 'doma.in'),
72
        );
73
        self::assertSame(
74
            $regularOne,
75
            $this->repo->findOneWithDomainFallback($withDomainDuplicatingRegular->getShortCode(), 'other-domain.com'),
76
        );
77
        self::assertNull($this->repo->findOneWithDomainFallback('invalid'));
78
        self::assertNull($this->repo->findOneWithDomainFallback($withDomain->getShortCode()));
79
        self::assertNull($this->repo->findOneWithDomainFallback($withDomain->getShortCode(), 'other-domain.com'));
80
    }
81
82
    /** @test */
83
    public function countListReturnsProperNumberOfResults(): void
84
    {
85
        $count = 5;
86
        for ($i = 0; $i < $count; $i++) {
87
            $this->getEntityManager()->persist(new ShortUrl((string) $i));
88
        }
89
        $this->getEntityManager()->flush();
90
91
        self::assertEquals($count, $this->repo->countList());
92
    }
93
94
    /** @test */
95
    public function findListProperlyFiltersResult(): void
96
    {
97
        $tag = new Tag('bar');
98
        $this->getEntityManager()->persist($tag);
99
100
        $foo = new ShortUrl('foo');
101
        $foo->setTags(new ArrayCollection([$tag]));
102
        $this->getEntityManager()->persist($foo);
103
104
        $bar = new ShortUrl('bar');
105
        $visit = new Visit($bar, Visitor::emptyInstance());
106
        $this->getEntityManager()->persist($visit);
107
        $bar->setVisits(new ArrayCollection([$visit]));
108
        $this->getEntityManager()->persist($bar);
109
110
        $foo2 = new ShortUrl('foo_2');
111
        $ref = new ReflectionObject($foo2);
112
        $dateProp = $ref->getProperty('dateCreated');
113
        $dateProp->setAccessible(true);
114
        $dateProp->setValue($foo2, Chronos::now()->subDays(5));
115
        $this->getEntityManager()->persist($foo2);
116
117
        $this->getEntityManager()->flush();
118
119
        $result = $this->repo->findList(null, null, 'foo', ['bar']);
120
        self::assertCount(1, $result);
121
        self::assertEquals(1, $this->repo->countList('foo', ['bar']));
122
        self::assertSame($foo, $result[0]);
123
124
        $result = $this->repo->findList();
125
        self::assertCount(3, $result);
126
127
        $result = $this->repo->findList(2);
128
        self::assertCount(2, $result);
129
130
        $result = $this->repo->findList(2, 1);
131
        self::assertCount(2, $result);
132
133
        self::assertCount(1, $this->repo->findList(2, 2));
134
135
        $result = $this->repo->findList(null, null, null, [], ShortUrlsOrdering::fromRawData([
136
            'orderBy' => ['visits' => 'DESC'],
137
        ]));
138
        self::assertCount(3, $result);
139
        self::assertSame($bar, $result[0]);
140
141
        $result = $this->repo->findList(null, null, null, [], null, new DateRange(null, Chronos::now()->subDays(2)));
142
        self::assertCount(1, $result);
143
        self::assertEquals(1, $this->repo->countList(null, [], new DateRange(null, Chronos::now()->subDays(2))));
144
        self::assertSame($foo2, $result[0]);
145
146
        self::assertCount(
147
            2,
148
            $this->repo->findList(null, null, null, [], null, new DateRange(Chronos::now()->subDays(2))),
149
        );
150
        self::assertEquals(2, $this->repo->countList(null, [], new DateRange(Chronos::now()->subDays(2))));
151
    }
152
153
    /** @test */
154
    public function findListProperlyMapsFieldNamesToColumnNamesWhenOrdering(): void
155
    {
156
        $urls = ['a', 'z', 'c', 'b'];
157
        foreach ($urls as $url) {
158
            $this->getEntityManager()->persist(new ShortUrl($url));
159
        }
160
161
        $this->getEntityManager()->flush();
162
163
        $result = $this->repo->findList(null, null, null, [], ShortUrlsOrdering::fromRawData([
164
            'orderBy' => ['longUrl' => 'ASC'],
165
        ]));
166
167
        self::assertCount(count($urls), $result);
168
        self::assertEquals('a', $result[0]->getLongUrl());
169
        self::assertEquals('b', $result[1]->getLongUrl());
170
        self::assertEquals('c', $result[2]->getLongUrl());
171
        self::assertEquals('z', $result[3]->getLongUrl());
172
    }
173
174
    /** @test */
175
    public function shortCodeIsInUseLooksForShortUrlInProperSetOfTables(): void
176
    {
177
        $shortUrlWithoutDomain = new ShortUrl('foo', ShortUrlMeta::fromRawData(['customSlug' => 'my-cool-slug']));
178
        $this->getEntityManager()->persist($shortUrlWithoutDomain);
179
180
        $shortUrlWithDomain = new ShortUrl(
181
            'foo',
182
            ShortUrlMeta::fromRawData(['domain' => 'doma.in', 'customSlug' => 'another-slug']),
183
        );
184
        $this->getEntityManager()->persist($shortUrlWithDomain);
185
186
        $this->getEntityManager()->flush();
187
188
        self::assertTrue($this->repo->shortCodeIsInUse('my-cool-slug'));
189
        self::assertFalse($this->repo->shortCodeIsInUse('my-cool-slug', 'doma.in'));
190
        self::assertFalse($this->repo->shortCodeIsInUse('slug-not-in-use'));
191
        self::assertFalse($this->repo->shortCodeIsInUse('another-slug'));
192
        self::assertFalse($this->repo->shortCodeIsInUse('another-slug', 'example.com'));
193
        self::assertTrue($this->repo->shortCodeIsInUse('another-slug', 'doma.in'));
194
    }
195
196
    /** @test */
197
    public function findOneLooksForShortUrlInProperSetOfTables(): void
198
    {
199
        $shortUrlWithoutDomain = new ShortUrl('foo', ShortUrlMeta::fromRawData(['customSlug' => 'my-cool-slug']));
200
        $this->getEntityManager()->persist($shortUrlWithoutDomain);
201
202
        $shortUrlWithDomain = new ShortUrl(
203
            'foo',
204
            ShortUrlMeta::fromRawData(['domain' => 'doma.in', 'customSlug' => 'another-slug']),
205
        );
206
        $this->getEntityManager()->persist($shortUrlWithDomain);
207
208
        $this->getEntityManager()->flush();
209
210
        self::assertNotNull($this->repo->findOne('my-cool-slug'));
211
        self::assertNull($this->repo->findOne('my-cool-slug', 'doma.in'));
212
        self::assertNull($this->repo->findOne('slug-not-in-use'));
213
        self::assertNull($this->repo->findOne('another-slug'));
214
        self::assertNull($this->repo->findOne('another-slug', 'example.com'));
215
        self::assertNotNull($this->repo->findOne('another-slug', 'doma.in'));
216
    }
217
218
    /** @test */
219
    public function findOneMatchingReturnsNullForNonExistingShortUrls(): void
220
    {
221
        self::assertNull($this->repo->findOneMatching('', [], ShortUrlMeta::createEmpty()));
222
        self::assertNull($this->repo->findOneMatching('foobar', [], ShortUrlMeta::createEmpty()));
223
        self::assertNull($this->repo->findOneMatching('foobar', ['foo', 'bar'], ShortUrlMeta::createEmpty()));
224
        self::assertNull($this->repo->findOneMatching('foobar', ['foo', 'bar'], ShortUrlMeta::fromRawData([
225
            'validSince' => Chronos::parse('2020-03-05 20:18:30'),
226
            'customSlug' => 'this_slug_does_not_exist',
227
        ])));
228
    }
229
230
    /** @test */
231
    public function findOneMatchingAppliesProperConditions(): void
232
    {
233
        $start = Chronos::parse('2020-03-05 20:18:30');
234
        $end = Chronos::parse('2021-03-05 20:18:30');
235
236
        $shortUrl = new ShortUrl('foo', ShortUrlMeta::fromRawData(['validSince' => $start]));
237
        $shortUrl->setTags($this->tagNamesToEntities($this->getEntityManager(), ['foo', 'bar']));
238
        $this->getEntityManager()->persist($shortUrl);
239
240
        $shortUrl2 = new ShortUrl('bar', ShortUrlMeta::fromRawData(['validUntil' => $end]));
241
        $this->getEntityManager()->persist($shortUrl2);
242
243
        $shortUrl3 = new ShortUrl('baz', ShortUrlMeta::fromRawData(['validSince' => $start, 'validUntil' => $end]));
244
        $this->getEntityManager()->persist($shortUrl3);
245
246
        $shortUrl4 = new ShortUrl('foo', ShortUrlMeta::fromRawData(['customSlug' => 'custom', 'validUntil' => $end]));
247
        $this->getEntityManager()->persist($shortUrl4);
248
249
        $shortUrl5 = new ShortUrl('foo', ShortUrlMeta::fromRawData(['maxVisits' => 3]));
250
        $this->getEntityManager()->persist($shortUrl5);
251
252
        $shortUrl6 = new ShortUrl('foo', ShortUrlMeta::fromRawData(['domain' => 'doma.in']));
253
        $this->getEntityManager()->persist($shortUrl6);
254
255
        $this->getEntityManager()->flush();
256
257
        self::assertSame(
258
            $shortUrl,
259
            $this->repo->findOneMatching('foo', ['foo', 'bar'], ShortUrlMeta::fromRawData(['validSince' => $start])),
260
        );
261
        self::assertSame(
262
            $shortUrl2,
263
            $this->repo->findOneMatching('bar', [], ShortUrlMeta::fromRawData(['validUntil' => $end])),
264
        );
265
        self::assertSame(
266
            $shortUrl3,
267
            $this->repo->findOneMatching('baz', [], ShortUrlMeta::fromRawData([
268
                'validSince' => $start,
269
                'validUntil' => $end,
270
            ])),
271
        );
272
        self::assertSame(
273
            $shortUrl4,
274
            $this->repo->findOneMatching('foo', [], ShortUrlMeta::fromRawData([
275
                'customSlug' => 'custom',
276
                'validUntil' => $end,
277
            ])),
278
        );
279
        self::assertSame(
280
            $shortUrl5,
281
            $this->repo->findOneMatching('foo', [], ShortUrlMeta::fromRawData(['maxVisits' => 3])),
282
        );
283
        self::assertSame(
284
            $shortUrl6,
285
            $this->repo->findOneMatching('foo', [], ShortUrlMeta::fromRawData(['domain' => 'doma.in'])),
286
        );
287
    }
288
289
    /** @test */
290
    public function findOneMatchingReturnsOldestOneWhenThereAreMultipleMatches(): void
291
    {
292
        $start = Chronos::parse('2020-03-05 20:18:30');
293
        $meta = ['validSince' => $start, 'maxVisits' => 50];
294
        $tags = ['foo', 'bar'];
295
        $tagEntities = $this->tagNamesToEntities($this->getEntityManager(), $tags);
296
297
        $shortUrl1 = new ShortUrl('foo', ShortUrlMeta::fromRawData($meta));
298
        $shortUrl1->setTags($tagEntities);
299
        $this->getEntityManager()->persist($shortUrl1);
300
301
        $shortUrl2 = new ShortUrl('foo', ShortUrlMeta::fromRawData($meta));
302
        $shortUrl2->setTags($tagEntities);
303
        $this->getEntityManager()->persist($shortUrl2);
304
305
        $shortUrl3 = new ShortUrl('foo', ShortUrlMeta::fromRawData($meta));
306
        $shortUrl3->setTags($tagEntities);
307
        $this->getEntityManager()->persist($shortUrl3);
308
309
        $this->getEntityManager()->flush();
310
311
        self::assertSame(
312
            $shortUrl1,
313
            $this->repo->findOneMatching('foo', $tags, ShortUrlMeta::fromRawData($meta)),
314
        );
315
        self::assertNotSame(
316
            $shortUrl2,
317
            $this->repo->findOneMatching('foo', $tags, ShortUrlMeta::fromRawData($meta)),
318
        );
319
        self::assertNotSame(
320
            $shortUrl3,
321
            $this->repo->findOneMatching('foo', $tags, ShortUrlMeta::fromRawData($meta)),
322
        );
323
    }
324
325
    /** @test */
326
    public function importedShortUrlsAreSearchedAsExpected(): void
327
    {
328
        $buildImported = static fn (string $shortCode, ?String $domain = null) =>
329
            new ImportedShlinkUrl('', 'foo', [], Chronos::now(), $domain, $shortCode);
330
331
        $shortUrlWithoutDomain = ShortUrl::fromImport($buildImported('my-cool-slug'), true);
332
        $this->getEntityManager()->persist($shortUrlWithoutDomain);
333
334
        $shortUrlWithDomain = ShortUrl::fromImport($buildImported('another-slug', 'doma.in'), true);
335
        $this->getEntityManager()->persist($shortUrlWithDomain);
336
337
        $this->getEntityManager()->flush();
338
339
        self::assertTrue($this->repo->importedUrlExists($buildImported('my-cool-slug')));
340
        self::assertTrue($this->repo->importedUrlExists($buildImported('another-slug', 'doma.in')));
341
        self::assertFalse($this->repo->importedUrlExists($buildImported('non-existing-slug')));
342
        self::assertFalse($this->repo->importedUrlExists($buildImported('non-existing-slug', 'doma.in')));
343
        self::assertFalse($this->repo->importedUrlExists($buildImported('my-cool-slug', 'doma.in')));
344
        self::assertFalse($this->repo->importedUrlExists($buildImported('another-slug')));
345
    }
346
}
347