Cancelled
Pull Request — develop (#687)
by Alejandro
05:53
created

LocateShortUrlVisitTest::provideIpAddresses()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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