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

VisitLocatorTest::provideIsNonLocatableAddress()

Size

Total Lines 16
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 12
c 1
b 0
f 0
dl 0
loc 16
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ShlinkioTest\Shlink\Core\Visit;
6
7
use Doctrine\ORM\EntityManager;
8
use Exception;
9
use PHPUnit\Framework\Assert;
10
use PHPUnit\Framework\TestCase;
11
use Prophecy\Argument;
12
use Prophecy\Prophecy\MethodProphecy;
13
use Prophecy\Prophecy\ObjectProphecy;
14
use Shlinkio\Shlink\Core\Entity\ShortUrl;
15
use Shlinkio\Shlink\Core\Entity\Visit;
16
use Shlinkio\Shlink\Core\Entity\VisitLocation;
17
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
18
use Shlinkio\Shlink\Core\Model\Visitor;
19
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
20
use Shlinkio\Shlink\Core\Visit\VisitGeolocationHelperInterface;
21
use Shlinkio\Shlink\Core\Visit\VisitLocator;
22
use Shlinkio\Shlink\IpGeolocation\Model\Location;
23
24
use function array_shift;
25
use function count;
26
use function floor;
27
use function func_get_args;
28
use function Functional\map;
29
use function range;
30
use function sprintf;
31
32
class VisitLocatorTest extends TestCase
33
{
34
    private VisitLocator $visitService;
35
    private ObjectProphecy $em;
36
    private ObjectProphecy $repo;
37
38
    public function setUp(): void
39
    {
40
        $this->em = $this->prophesize(EntityManager::class);
41
        $this->repo = $this->prophesize(VisitRepositoryInterface::class);
42
        $this->em->getRepository(Visit::class)->willReturn($this->repo->reveal());
43
44
        $this->visitService = new VisitLocator($this->em->reveal());
45
    }
46
47
    /**
48
     * @test
49
     * @dataProvider provideMethodNames
50
     */
51
    public function locateVisitsIteratesAndLocatesExpectedVisits(
52
        string $serviceMethodName,
53
        string $expectedRepoMethodName
54
    ): void {
55
        $unlocatedVisits = map(
56
            range(1, 200),
57
            fn (int $i) => new Visit(new ShortUrl(sprintf('short_code_%s', $i)), Visitor::emptyInstance()),
58
        );
59
60
        $findVisits = $this->mockRepoMethod($expectedRepoMethodName)->willReturn($unlocatedVisits);
61
62
        $persist = $this->em->persist(Argument::type(Visit::class))->will(function (): void {
63
        });
64
        $flush = $this->em->flush()->will(function (): void {
65
        });
66
        $clear = $this->em->clear()->will(function (): void {
67
        });
68
69
        $this->visitService->{$serviceMethodName}(new class implements VisitGeolocationHelperInterface {
70
            public function geolocateVisit(Visit $visit): Location
71
            {
72
                return Location::emptyInstance();
73
            }
74
75
            public function onVisitLocated(VisitLocation $visitLocation, Visit $visit): void
76
            {
77
                $args = func_get_args();
78
79
                Assert::assertInstanceOf(VisitLocation::class, array_shift($args));
80
                Assert::assertInstanceOf(Visit::class, array_shift($args));
81
            }
82
        });
83
84
        $findVisits->shouldHaveBeenCalledOnce();
85
        $persist->shouldHaveBeenCalledTimes(count($unlocatedVisits));
86
        $flush->shouldHaveBeenCalledTimes(floor(count($unlocatedVisits) / 200) + 1);
87
        $clear->shouldHaveBeenCalledTimes(floor(count($unlocatedVisits) / 200) + 1);
88
    }
89
90
    public function provideMethodNames(): iterable
91
    {
92
        yield 'locateUnlocatedVisits' => ['locateUnlocatedVisits', 'findUnlocatedVisits'];
93
        yield 'locateVisitsWithEmptyLocation' => ['locateVisitsWithEmptyLocation', 'findVisitsWithEmptyLocation'];
94
        yield 'locateAllVisits' => ['locateAllVisits', 'findAllVisits'];
95
    }
96
97
    /**
98
     * @test
99
     * @dataProvider provideIsNonLocatableAddress
100
     */
101
    public function visitsWhichCannotBeLocatedAreIgnoredOrLocatedAsEmpty(
102
        string $serviceMethodName,
103
        string $expectedRepoMethodName,
104
        bool $isNonLocatableAddress
105
    ): void {
106
        $unlocatedVisits = [
107
            new Visit(new ShortUrl('foo'), Visitor::emptyInstance()),
108
        ];
109
110
        $findVisits = $this->mockRepoMethod($expectedRepoMethodName)->willReturn($unlocatedVisits);
111
112
        $persist = $this->em->persist(Argument::type(Visit::class))->will(function (): void {
113
        });
114
        $flush = $this->em->flush()->will(function (): void {
115
        });
116
        $clear = $this->em->clear()->will(function (): void {
117
        });
118
119
        $this->visitService->{$serviceMethodName}(
120
            new class ($isNonLocatableAddress) implements VisitGeolocationHelperInterface {
121
                private bool $isNonLocatableAddress;
122
123
                public function __construct(bool $isNonLocatableAddress)
124
                {
125
                    $this->isNonLocatableAddress = $isNonLocatableAddress;
126
                }
127
128
                public function geolocateVisit(Visit $visit): Location
129
                {
130
                    throw $this->isNonLocatableAddress
131
                        ? new IpCannotBeLocatedException('Cannot be located')
132
                        : IpCannotBeLocatedException::forError(new Exception(''));
133
                }
134
135
                public function onVisitLocated(VisitLocation $visitLocation, Visit $visit): void
136
                {
137
                }
138
            },
139
        );
140
141
        $findVisits->shouldHaveBeenCalledOnce();
142
        $persist->shouldHaveBeenCalledTimes($isNonLocatableAddress ? 1 : 0);
143
        $flush->shouldHaveBeenCalledOnce();
144
        $clear->shouldHaveBeenCalledOnce();
145
    }
146
147
    public function provideIsNonLocatableAddress(): iterable
148
    {
149
        yield 'locateUnlocatedVisits - locatable address' => ['locateUnlocatedVisits', 'findUnlocatedVisits', false];
150
        yield 'locateUnlocatedVisits - non-locatable address' => ['locateUnlocatedVisits', 'findUnlocatedVisits', true];
151
        yield 'locateVisitsWithEmptyLocation - locatable address' => [
152
            'locateVisitsWithEmptyLocation',
153
            'findVisitsWithEmptyLocation',
154
            false,
155
        ];
156
        yield 'locateVisitsWithEmptyLocation - non-locatable address' => [
157
            'locateVisitsWithEmptyLocation',
158
            'findVisitsWithEmptyLocation',
159
            true,
160
        ];
161
        yield 'locateAllVisits - locatable address' => ['locateAllVisits', 'findAllVisits', false];
162
        yield 'locateAllVisits - non-locatable address' => ['locateAllVisits', 'findAllVisits', true];
163
    }
164
165
    private function mockRepoMethod(string $methodName): MethodProphecy
166
    {
167
        return (new MethodProphecy($this->repo, $methodName, new Argument\ArgumentsWildcard([])));
168
    }
169
}
170