VisitLocatorTest::mockRepoMethod()
last analyzed

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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