nonLocatableVisitsResolveToEmptyLocations()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 12
nc 1
nop 1
dl 0
loc 18
rs 9.8666
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ShlinkioTest\Shlink\Core\EventDispatcher;
6
7
use Doctrine\ORM\EntityManagerInterface;
8
use PHPUnit\Framework\TestCase;
9
use Prophecy\Argument;
10
use Prophecy\PhpUnit\ProphecyTrait;
11
use Prophecy\Prophecy\ObjectProphecy;
12
use Psr\EventDispatcher\EventDispatcherInterface;
13
use Psr\Log\LoggerInterface;
14
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
15
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
16
use Shlinkio\Shlink\Common\Util\IpAddress;
17
use Shlinkio\Shlink\Core\Entity\ShortUrl;
18
use Shlinkio\Shlink\Core\Entity\Visit;
19
use Shlinkio\Shlink\Core\Entity\VisitLocation;
20
use Shlinkio\Shlink\Core\EventDispatcher\LocateShortUrlVisit;
21
use Shlinkio\Shlink\Core\EventDispatcher\ShortUrlVisited;
22
use Shlinkio\Shlink\Core\EventDispatcher\VisitLocated;
23
use Shlinkio\Shlink\Core\Model\Visitor;
24
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
25
use Shlinkio\Shlink\IpGeolocation\Model\Location;
26
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
27
28
class LocateShortUrlVisitTest extends TestCase
29
{
30
    use ProphecyTrait;
31
32
    private LocateShortUrlVisit $locateVisit;
33
    private ObjectProphecy $ipLocationResolver;
34
    private ObjectProphecy $em;
35
    private ObjectProphecy $logger;
36
    private ObjectProphecy $dbUpdater;
37
    private ObjectProphecy $eventDispatcher;
38
39
    public function setUp(): void
40
    {
41
        $this->ipLocationResolver = $this->prophesize(IpLocationResolverInterface::class);
42
        $this->em = $this->prophesize(EntityManagerInterface::class);
43
        $this->logger = $this->prophesize(LoggerInterface::class);
44
        $this->dbUpdater = $this->prophesize(GeolocationDbUpdaterInterface::class);
45
        $this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
46
47
        $this->locateVisit = new LocateShortUrlVisit(
48
            $this->ipLocationResolver->reveal(),
49
            $this->em->reveal(),
50
            $this->logger->reveal(),
51
            $this->dbUpdater->reveal(),
52
            $this->eventDispatcher->reveal(),
53
        );
54
    }
55
56
    /** @test */
57
    public function invalidVisitLogsWarning(): void
58
    {
59
        $event = new ShortUrlVisited('123');
60
        $findVisit = $this->em->find(Visit::class, '123')->willReturn(null);
61
        $logWarning = $this->logger->warning('Tried to locate visit with id "{visitId}", but it does not exist.', [
62
            'visitId' => 123,
63
        ]);
64
        $dispatch = $this->eventDispatcher->dispatch(new VisitLocated('123'))->will(function (): void {
65
        });
66
67
        ($this->locateVisit)($event);
68
69
        $findVisit->shouldHaveBeenCalledOnce();
70
        $this->em->flush()->shouldNotHaveBeenCalled();
71
        $this->ipLocationResolver->resolveIpLocation(Argument::cetera())->shouldNotHaveBeenCalled();
72
        $logWarning->shouldHaveBeenCalled();
73
        $dispatch->shouldNotHaveBeenCalled();
74
    }
75
76
    /** @test */
77
    public function invalidAddressLogsWarning(): void
78
    {
79
        $event = new ShortUrlVisited('123');
80
        $findVisit = $this->em->find(Visit::class, '123')->willReturn(
81
            new Visit(new ShortUrl(''), new Visitor('', '', '1.2.3.4')),
82
        );
83
        $resolveLocation = $this->ipLocationResolver->resolveIpLocation(Argument::cetera())->willThrow(
84
            WrongIpException::class,
85
        );
86
        $logWarning = $this->logger->warning(
87
            Argument::containingString('Tried to locate visit with id "{visitId}", but its address seems to be wrong.'),
88
            Argument::type('array'),
89
        );
90
        $dispatch = $this->eventDispatcher->dispatch(new VisitLocated('123'))->will(function (): void {
91
        });
92
93
        ($this->locateVisit)($event);
94
95
        $findVisit->shouldHaveBeenCalledOnce();
96
        $resolveLocation->shouldHaveBeenCalledOnce();
97
        $logWarning->shouldHaveBeenCalled();
98
        $this->em->flush()->shouldNotHaveBeenCalled();
99
        $dispatch->shouldHaveBeenCalledOnce();
100
    }
101
102
    /**
103
     * @test
104
     * @dataProvider provideNonLocatableVisits
105
     */
106
    public function nonLocatableVisitsResolveToEmptyLocations(Visit $visit): void
107
    {
108
        $event = new ShortUrlVisited('123');
109
        $findVisit = $this->em->find(Visit::class, '123')->willReturn($visit);
110
        $flush = $this->em->flush()->will(function (): void {
111
        });
112
        $resolveIp = $this->ipLocationResolver->resolveIpLocation(Argument::any());
113
        $dispatch = $this->eventDispatcher->dispatch(new VisitLocated('123'))->will(function (): void {
114
        });
115
116
        ($this->locateVisit)($event);
117
118
        self::assertEquals($visit->getVisitLocation(), new VisitLocation(Location::emptyInstance()));
119
        $findVisit->shouldHaveBeenCalledOnce();
120
        $flush->shouldHaveBeenCalledOnce();
121
        $resolveIp->shouldNotHaveBeenCalled();
122
        $this->logger->warning(Argument::cetera())->shouldNotHaveBeenCalled();
123
        $dispatch->shouldHaveBeenCalledOnce();
124
    }
125
126
    public function provideNonLocatableVisits(): iterable
127
    {
128
        $shortUrl = new ShortUrl('');
129
130
        yield 'null IP' => [new Visit($shortUrl, new Visitor('', '', null))];
131
        yield 'empty IP' => [new Visit($shortUrl, new Visitor('', '', ''))];
132
        yield 'localhost' => [new Visit($shortUrl, new Visitor('', '', IpAddress::LOCALHOST))];
133
    }
134
135
    /**
136
     * @test
137
     * @dataProvider provideIpAddresses
138
     */
139
    public function locatableVisitsResolveToLocation(string $anonymizedIpAddress, ?string $originalIpAddress): void
140
    {
141
        $ipAddr = $originalIpAddress ?? $anonymizedIpAddress;
142
        $visit = new Visit(new ShortUrl(''), new Visitor('', '', $ipAddr));
143
        $location = new Location('', '', '', '', 0.0, 0.0, '');
144
        $event = new ShortUrlVisited('123', $originalIpAddress);
145
146
        $findVisit = $this->em->find(Visit::class, '123')->willReturn($visit);
147
        $flush = $this->em->flush()->will(function (): void {
148
        });
149
        $resolveIp = $this->ipLocationResolver->resolveIpLocation($ipAddr)->willReturn($location);
150
        $dispatch = $this->eventDispatcher->dispatch(new VisitLocated('123'))->will(function (): void {
151
        });
152
153
        ($this->locateVisit)($event);
154
155
        self::assertEquals($visit->getVisitLocation(), new VisitLocation($location));
156
        $findVisit->shouldHaveBeenCalledOnce();
157
        $flush->shouldHaveBeenCalledOnce();
158
        $resolveIp->shouldHaveBeenCalledOnce();
159
        $this->logger->warning(Argument::cetera())->shouldNotHaveBeenCalled();
160
        $dispatch->shouldHaveBeenCalledOnce();
161
    }
162
163
    public function provideIpAddresses(): iterable
164
    {
165
        yield 'no original IP address' => ['1.2.3.0', null];
166
        yield 'original IP address' => ['1.2.3.0', '1.2.3.4'];
167
    }
168
169
    /** @test */
170
    public function errorWhenUpdatingGeoLiteWithExistingCopyLogsWarning(): void
171
    {
172
        $e = GeolocationDbUpdateFailedException::create(true);
173
        $ipAddr = '1.2.3.0';
174
        $visit = new Visit(new ShortUrl(''), new Visitor('', '', $ipAddr));
175
        $location = new Location('', '', '', '', 0.0, 0.0, '');
176
        $event = new ShortUrlVisited('123');
177
178
        $findVisit = $this->em->find(Visit::class, '123')->willReturn($visit);
179
        $flush = $this->em->flush()->will(function (): void {
180
        });
181
        $resolveIp = $this->ipLocationResolver->resolveIpLocation($ipAddr)->willReturn($location);
182
        $checkUpdateDb = $this->dbUpdater->checkDbUpdate(Argument::cetera())->willThrow($e);
183
        $dispatch = $this->eventDispatcher->dispatch(new VisitLocated('123'))->will(function (): void {
184
        });
185
186
        ($this->locateVisit)($event);
187
188
        self::assertEquals($visit->getVisitLocation(), new VisitLocation($location));
189
        $findVisit->shouldHaveBeenCalledOnce();
190
        $flush->shouldHaveBeenCalledOnce();
191
        $resolveIp->shouldHaveBeenCalledOnce();
192
        $checkUpdateDb->shouldHaveBeenCalledOnce();
193
        $this->logger->warning(
194
            'GeoLite2 database update failed. Proceeding with old version. {e}',
195
            ['e' => $e],
196
        )->shouldHaveBeenCalledOnce();
197
        $dispatch->shouldHaveBeenCalledOnce();
198
    }
199
200
    /** @test */
201
    public function errorWhenDownloadingGeoLiteCancelsLocation(): void
202
    {
203
        $e = GeolocationDbUpdateFailedException::create(false);
204
        $ipAddr = '1.2.3.0';
205
        $visit = new Visit(new ShortUrl(''), new Visitor('', '', $ipAddr));
206
        $location = new Location('', '', '', '', 0.0, 0.0, '');
207
        $event = new ShortUrlVisited('123');
208
209
        $findVisit = $this->em->find(Visit::class, '123')->willReturn($visit);
210
        $flush = $this->em->flush()->will(function (): void {
211
        });
212
        $resolveIp = $this->ipLocationResolver->resolveIpLocation($ipAddr)->willReturn($location);
213
        $checkUpdateDb = $this->dbUpdater->checkDbUpdate(Argument::cetera())->willThrow($e);
214
        $logError = $this->logger->error(
215
            'GeoLite2 database download failed. It is not possible to locate visit with id {visitId}. {e}',
216
            ['e' => $e, 'visitId' => 123],
217
        );
218
        $dispatch = $this->eventDispatcher->dispatch(new VisitLocated('123'))->will(function (): void {
219
        });
220
221
        ($this->locateVisit)($event);
222
223
        self::assertNull($visit->getVisitLocation());
224
        $findVisit->shouldHaveBeenCalledOnce();
225
        $flush->shouldNotHaveBeenCalled();
226
        $resolveIp->shouldNotHaveBeenCalled();
227
        $checkUpdateDb->shouldHaveBeenCalledOnce();
228
        $logError->shouldHaveBeenCalledOnce();
229
        $dispatch->shouldHaveBeenCalledOnce();
230
    }
231
}
232