Passed
Pull Request — master (#276)
by Alejandro
02:48
created

localhostAndEmptyAddressesAreIgnored()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 28
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 19
nc 2
nop 2
dl 0
loc 28
rs 9.6333
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
5
6
use PHPUnit\Framework\TestCase;
7
use Prophecy\Argument;
8
use Prophecy\Prophecy\ObjectProphecy;
9
use Shlinkio\Shlink\CLI\Command\Visit\ProcessVisitsCommand;
10
use Shlinkio\Shlink\Common\Exception\WrongIpException;
11
use Shlinkio\Shlink\Common\IpGeolocation\IpApiLocationResolver;
12
use Shlinkio\Shlink\Common\Util\IpAddress;
13
use Shlinkio\Shlink\Core\Entity\ShortUrl;
14
use Shlinkio\Shlink\Core\Entity\Visit;
15
use Shlinkio\Shlink\Core\Entity\VisitLocation;
16
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
17
use Shlinkio\Shlink\Core\Model\Visitor;
18
use Shlinkio\Shlink\Core\Service\VisitService;
19
use Symfony\Component\Console\Application;
20
use Symfony\Component\Console\Output\OutputInterface;
21
use Symfony\Component\Console\Tester\CommandTester;
22
use Symfony\Component\Lock;
23
use Throwable;
24
use Zend\I18n\Translator\Translator;
25
use function array_shift;
26
use function sprintf;
27
28
class ProcessVisitsCommandTest extends TestCase
29
{
30
    /**
31
     * @var CommandTester
32
     */
33
    private $commandTester;
34
    /**
35
     * @var ObjectProphecy
36
     */
37
    private $visitService;
38
    /**
39
     * @var ObjectProphecy
40
     */
41
    private $ipResolver;
42
    /**
43
     * @var ObjectProphecy
44
     */
45
    private $locker;
46
    /**
47
     * @var ObjectProphecy
48
     */
49
    private $lock;
50
51
    public function setUp()
52
    {
53
        $this->visitService = $this->prophesize(VisitService::class);
54
        $this->ipResolver = $this->prophesize(IpApiLocationResolver::class);
55
56
        $this->locker = $this->prophesize(Lock\Factory::class);
57
        $this->lock = $this->prophesize(Lock\LockInterface::class);
58
        $this->lock->acquire()->willReturn(true);
59
        $this->lock->release()->will(function () {
60
        });
61
        $this->locker->createLock(Argument::type('string'))->willReturn($this->lock->reveal());
62
63
        $command = new ProcessVisitsCommand(
64
            $this->visitService->reveal(),
65
            $this->ipResolver->reveal(),
66
            $this->locker->reveal(),
67
            Translator::factory([])
68
        );
69
        $app = new Application();
70
        $app->add($command);
71
72
        $this->commandTester = new CommandTester($command);
73
    }
74
75
    /**
76
     * @test
77
     */
78
    public function allPendingVisitsAreProcessed()
79
    {
80
        $visit = new Visit(new ShortUrl(''), new Visitor('', '', '1.2.3.4'));
81
        $location = new VisitLocation([]);
82
83
        $locateVisits = $this->visitService->locateVisits(Argument::cetera())->will(
84
            function (array $args) use ($visit, $location) {
85
                $firstCallback = array_shift($args);
86
                $firstCallback($visit);
87
88
                $secondCallback = array_shift($args);
89
                $secondCallback($location, $visit);
90
            }
91
        );
92
        $resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([]);
93
94
        $this->commandTester->execute([
95
            'command' => 'visit:process',
96
        ]);
97
        $output = $this->commandTester->getDisplay();
98
99
        $this->assertContains('Processing IP 1.2.3.0', $output);
100
        $locateVisits->shouldHaveBeenCalledOnce();
101
        $resolveIpLocation->shouldHaveBeenCalledOnce();
102
    }
103
104
    /**
105
     * @test
106
     * @dataProvider provideIgnoredAddresses
107
     */
108
    public function localhostAndEmptyAddressesAreIgnored(?string $address, string $message)
109
    {
110
        $visit = new Visit(new ShortUrl(''), new Visitor('', '', $address));
111
        $location = new VisitLocation([]);
112
113
        $locateVisits = $this->visitService->locateVisits(Argument::cetera())->will(
114
            function (array $args) use ($visit, $location) {
115
                $firstCallback = array_shift($args);
116
                $firstCallback($visit);
117
118
                $secondCallback = array_shift($args);
119
                $secondCallback($location, $visit);
120
            }
121
        );
122
        $resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([]);
123
124
        try {
125
            $this->commandTester->execute([
126
                'command' => 'visit:process',
127
            ], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
128
        } catch (Throwable $e) {
129
            $output = $this->commandTester->getDisplay();
130
131
            $this->assertInstanceOf(IpCannotBeLocatedException::class, $e);
132
133
            $this->assertContains($message, $output);
134
            $locateVisits->shouldHaveBeenCalledOnce();
135
            $resolveIpLocation->shouldNotHaveBeenCalled();
136
        }
137
    }
138
139
    public function provideIgnoredAddresses(): array
140
    {
141
        return [
142
            ['', 'Ignored visit with no IP address'],
143
            [null, 'Ignored visit with no IP address'],
144
            [IpAddress::LOCALHOST, 'Ignored localhost address'],
145
        ];
146
    }
147
148
    /**
149
     * @test
150
     */
151
    public function errorWhileLocatingIpIsDisplayed()
152
    {
153
        $visit = new Visit(new ShortUrl(''), new Visitor('', '', '1.2.3.4'));
154
        $location = new VisitLocation([]);
155
156
        $locateVisits = $this->visitService->locateVisits(Argument::cetera())->will(
157
            function (array $args) use ($visit, $location) {
158
                $firstCallback = array_shift($args);
159
                $firstCallback($visit);
160
161
                $secondCallback = array_shift($args);
162
                $secondCallback($location, $visit);
163
            }
164
        );
165
        $resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willThrow(WrongIpException::class);
166
167
        try {
168
            $this->commandTester->execute([
169
                'command' => 'visit:process',
170
            ], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
171
        } catch (Throwable $e) {
172
            $output = $this->commandTester->getDisplay();
173
174
            $this->assertInstanceOf(IpCannotBeLocatedException::class, $e);
175
176
            $this->assertContains('An error occurred while locating IP. Skipped', $output);
177
            $locateVisits->shouldHaveBeenCalledOnce();
178
            $resolveIpLocation->shouldHaveBeenCalledOnce();
179
        }
180
    }
181
182
    /**
183
     * @test
184
     */
185
    public function noActionIsPerformedIfLockIsAcquired()
186
    {
187
        $this->lock->acquire()->willReturn(false);
188
189
        $locateVisits = $this->visitService->locateVisits(Argument::cetera())->will(function () {
190
        });
191
        $resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([]);
192
193
        $this->commandTester->execute([
194
            'command' => 'visit:process',
195
        ], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
196
        $output = $this->commandTester->getDisplay();
197
198
        $this->assertContains(
199
            sprintf('There is already an instance of the "%s" command', ProcessVisitsCommand::NAME),
200
            $output
201
        );
202
        $locateVisits->shouldNotHaveBeenCalled();
203
        $resolveIpLocation->shouldNotHaveBeenCalled();
204
    }
205
}
206