mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-12 01:54:41 +08:00
Extract logic to send visits to Matomo to its own service
This commit is contained in:
@@ -6,7 +6,6 @@ namespace ShlinkioTest\Shlink\Core\EventDispatcher\Matomo;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Exception;
|
||||
use MatomoTracker;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
@@ -15,34 +14,28 @@ use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Event\VisitLocated;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Matomo\SendVisitToMatomo;
|
||||
use Shlinkio\Shlink\Core\Matomo\MatomoOptions;
|
||||
use Shlinkio\Shlink\Core\Matomo\MatomoTrackerBuilderInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
|
||||
use Shlinkio\Shlink\Core\Matomo\MatomoVisitSenderInterface;
|
||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
|
||||
class SendVisitToMatomoTest extends TestCase
|
||||
{
|
||||
private MockObject & EntityManagerInterface $em;
|
||||
private MockObject & LoggerInterface $logger;
|
||||
private MockObject & MatomoTrackerBuilderInterface $trackerBuilder;
|
||||
private MockObject & MatomoVisitSenderInterface $visitSender;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->em = $this->createMock(EntityManagerInterface::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->trackerBuilder = $this->createMock(MatomoTrackerBuilderInterface::class);
|
||||
$this->visitSender = $this->createMock(MatomoVisitSenderInterface::class);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function visitIsNotSentWhenMatomoIsDisabled(): void
|
||||
{
|
||||
$this->em->expects($this->never())->method('find');
|
||||
$this->trackerBuilder->expects($this->never())->method('buildMatomoTracker');
|
||||
$this->visitSender->expects($this->never())->method('sendVisitToMatomo');
|
||||
$this->logger->expects($this->never())->method('error');
|
||||
$this->logger->expects($this->never())->method('warning');
|
||||
|
||||
@@ -53,7 +46,7 @@ class SendVisitToMatomoTest extends TestCase
|
||||
public function visitIsNotSentWhenItDoesNotExist(): void
|
||||
{
|
||||
$this->em->expects($this->once())->method('find')->willReturn(null);
|
||||
$this->trackerBuilder->expects($this->never())->method('buildMatomoTracker');
|
||||
$this->visitSender->expects($this->never())->method('sendVisitToMatomo');
|
||||
$this->logger->expects($this->never())->method('error');
|
||||
$this->logger->expects($this->once())->method('warning')->with(
|
||||
'Tried to send visit with id "{visitId}" to matomo, but it does not exist.',
|
||||
@@ -63,97 +56,24 @@ class SendVisitToMatomoTest extends TestCase
|
||||
($this->listener())(new VisitLocated('123'));
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideTrackerMethods')]
|
||||
public function visitIsSentWhenItExists(Visit $visit, ?string $originalIpAddress, array $invokedMethods): void
|
||||
#[Test, DataProvider('provideOriginalIpAddress')]
|
||||
public function visitIsSentWhenItExists(?string $originalIpAddress): void
|
||||
{
|
||||
$visitId = '123';
|
||||
|
||||
$tracker = $this->createMock(MatomoTracker::class);
|
||||
$tracker->expects($this->once())->method('setUrl')->willReturn($tracker);
|
||||
$tracker->expects($this->once())->method('setUserAgent')->willReturn($tracker);
|
||||
$tracker->expects($this->once())->method('setUrlReferrer')->willReturn($tracker);
|
||||
$tracker->expects($this->once())->method('doTrackPageView')->with($visit->shortUrl?->title() ?? '');
|
||||
|
||||
if ($visit->isOrphan()) {
|
||||
$tracker->expects($this->exactly(2))->method('setCustomTrackingParameter')->willReturnMap([
|
||||
['type', $visit->type->value, $tracker],
|
||||
['orphan', 'true', $tracker],
|
||||
]);
|
||||
} else {
|
||||
$tracker->expects($this->once())->method('setCustomTrackingParameter')->with(
|
||||
'type',
|
||||
$visit->type->value,
|
||||
)->willReturn($tracker);
|
||||
}
|
||||
|
||||
foreach ($invokedMethods as $invokedMethod) {
|
||||
$tracker->expects($this->once())->method($invokedMethod)->willReturn($tracker);
|
||||
}
|
||||
$visit = Visit::forBasePath(Visitor::emptyInstance());
|
||||
|
||||
$this->em->expects($this->once())->method('find')->with(Visit::class, $visitId)->willReturn($visit);
|
||||
$this->trackerBuilder->expects($this->once())->method('buildMatomoTracker')->willReturn($tracker);
|
||||
$this->visitSender->expects($this->once())->method('sendVisitToMatomo')->with($visit, $originalIpAddress);
|
||||
$this->logger->expects($this->never())->method('error');
|
||||
$this->logger->expects($this->never())->method('warning');
|
||||
|
||||
($this->listener())(new VisitLocated($visitId, $originalIpAddress));
|
||||
}
|
||||
|
||||
public static function provideTrackerMethods(): iterable
|
||||
public static function provideOriginalIpAddress(): iterable
|
||||
{
|
||||
yield 'unlocated orphan visit' => [Visit::forBasePath(Visitor::emptyInstance()), null, []];
|
||||
yield 'located regular visit' => [
|
||||
Visit::forValidShortUrl(ShortUrl::withLongUrl('https://shlink.io'), Visitor::emptyInstance())
|
||||
->locate(VisitLocation::fromGeolocation(new Location(
|
||||
countryCode: 'countryCode',
|
||||
countryName: 'countryName',
|
||||
regionName: 'regionName',
|
||||
city: 'city',
|
||||
latitude: 123,
|
||||
longitude: 123,
|
||||
timeZone: 'timeZone',
|
||||
))),
|
||||
'1.2.3.4',
|
||||
['setCity', 'setCountry', 'setLatitude', 'setLongitude', 'setIp'],
|
||||
];
|
||||
yield 'fallback IP' => [Visit::forBasePath(new Visitor('', '', '1.2.3.4', '')), null, ['setIp']];
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideUrlsToTrack')]
|
||||
public function properUrlIsTracked(Visit $visit, string $expectedTrackedUrl): void
|
||||
{
|
||||
$visitId = '123';
|
||||
|
||||
$tracker = $this->createMock(MatomoTracker::class);
|
||||
$tracker->expects($this->once())->method('setUrl')->with($expectedTrackedUrl)->willReturn($tracker);
|
||||
$tracker->expects($this->once())->method('setUserAgent')->willReturn($tracker);
|
||||
$tracker->expects($this->once())->method('setUrlReferrer')->willReturn($tracker);
|
||||
$tracker->expects($this->any())->method('setCustomTrackingParameter')->willReturn($tracker);
|
||||
$tracker->expects($this->once())->method('doTrackPageView');
|
||||
|
||||
$this->em->expects($this->once())->method('find')->with(Visit::class, $visitId)->willReturn($visit);
|
||||
$this->trackerBuilder->expects($this->once())->method('buildMatomoTracker')->willReturn($tracker);
|
||||
$this->logger->expects($this->never())->method('error');
|
||||
$this->logger->expects($this->never())->method('warning');
|
||||
|
||||
($this->listener())(new VisitLocated($visitId));
|
||||
}
|
||||
|
||||
public static function provideUrlsToTrack(): iterable
|
||||
{
|
||||
yield 'orphan visit without visited URL' => [Visit::forBasePath(Visitor::emptyInstance()), ''];
|
||||
yield 'orphan visit with visited URL' => [
|
||||
Visit::forBasePath(new Visitor('', '', null, 'https://s.test/foo')),
|
||||
'https://s.test/foo',
|
||||
];
|
||||
yield 'non-orphan visit' => [
|
||||
Visit::forValidShortUrl(ShortUrl::create(
|
||||
ShortUrlCreation::fromRawData([
|
||||
ShortUrlInputFilter::LONG_URL => 'https://shlink.io',
|
||||
ShortUrlInputFilter::CUSTOM_SLUG => 'bar',
|
||||
]),
|
||||
), Visitor::emptyInstance()),
|
||||
'http://s2.test/bar',
|
||||
];
|
||||
yield 'no original IP address' => [null];
|
||||
yield 'original IP address' => ['1.2.3.4'];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -165,7 +85,7 @@ class SendVisitToMatomoTest extends TestCase
|
||||
$this->em->expects($this->once())->method('find')->with(Visit::class, $visitId)->willReturn(
|
||||
$this->createMock(Visit::class),
|
||||
);
|
||||
$this->trackerBuilder->expects($this->once())->method('buildMatomoTracker')->willThrowException($e);
|
||||
$this->visitSender->expects($this->once())->method('sendVisitToMatomo')->willThrowException($e);
|
||||
$this->logger->expects($this->never())->method('warning');
|
||||
$this->logger->expects($this->once())->method('error')->with(
|
||||
'An error occurred while trying to send visit to Matomo. {e}',
|
||||
@@ -180,9 +100,8 @@ class SendVisitToMatomoTest extends TestCase
|
||||
return new SendVisitToMatomo(
|
||||
$this->em,
|
||||
$this->logger,
|
||||
new ShortUrlStringifier(['hostname' => 's2.test']),
|
||||
new MatomoOptions(enabled: $enabled),
|
||||
$this->trackerBuilder,
|
||||
$this->visitSender,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
119
module/Core/test/Matomo/MatomoVisitSenderTest.php
Normal file
119
module/Core/test/Matomo/MatomoVisitSenderTest.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Matomo;
|
||||
|
||||
use MatomoTracker;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Matomo\MatomoTrackerBuilderInterface;
|
||||
use Shlinkio\Shlink\Core\Matomo\MatomoVisitSender;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
|
||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
|
||||
class MatomoVisitSenderTest extends TestCase
|
||||
{
|
||||
private MockObject & MatomoTrackerBuilderInterface $trackerBuilder;
|
||||
private MatomoVisitSender $visitSender;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->trackerBuilder = $this->createMock(MatomoTrackerBuilderInterface::class);
|
||||
$this->visitSender = new MatomoVisitSender(
|
||||
$this->trackerBuilder,
|
||||
new ShortUrlStringifier(['hostname' => 's2.test']),
|
||||
);
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideTrackerMethods')]
|
||||
public function visitIsSentToMatomo(Visit $visit, ?string $originalIpAddress, array $invokedMethods): void
|
||||
{
|
||||
$tracker = $this->createMock(MatomoTracker::class);
|
||||
$tracker->expects($this->once())->method('setUrl')->willReturn($tracker);
|
||||
$tracker->expects($this->once())->method('setUserAgent')->willReturn($tracker);
|
||||
$tracker->expects($this->once())->method('setUrlReferrer')->willReturn($tracker);
|
||||
$tracker->expects($this->once())->method('doTrackPageView')->with($visit->shortUrl?->title() ?? '');
|
||||
|
||||
if ($visit->isOrphan()) {
|
||||
$tracker->expects($this->exactly(2))->method('setCustomTrackingParameter')->willReturnMap([
|
||||
['type', $visit->type->value, $tracker],
|
||||
['orphan', 'true', $tracker],
|
||||
]);
|
||||
} else {
|
||||
$tracker->expects($this->once())->method('setCustomTrackingParameter')->with(
|
||||
'type',
|
||||
$visit->type->value,
|
||||
)->willReturn($tracker);
|
||||
}
|
||||
|
||||
foreach ($invokedMethods as $invokedMethod) {
|
||||
$tracker->expects($this->once())->method($invokedMethod)->willReturn($tracker);
|
||||
}
|
||||
|
||||
$this->trackerBuilder->expects($this->once())->method('buildMatomoTracker')->willReturn($tracker);
|
||||
|
||||
$this->visitSender->sendVisitToMatomo($visit, $originalIpAddress);
|
||||
}
|
||||
|
||||
public static function provideTrackerMethods(): iterable
|
||||
{
|
||||
yield 'unlocated orphan visit' => [Visit::forBasePath(Visitor::emptyInstance()), null, []];
|
||||
yield 'located regular visit' => [
|
||||
Visit::forValidShortUrl(ShortUrl::withLongUrl('https://shlink.io'), Visitor::emptyInstance())
|
||||
->locate(VisitLocation::fromGeolocation(new Location(
|
||||
countryCode: 'countryCode',
|
||||
countryName: 'countryName',
|
||||
regionName: 'regionName',
|
||||
city: 'city',
|
||||
latitude: 123,
|
||||
longitude: 123,
|
||||
timeZone: 'timeZone',
|
||||
))),
|
||||
'1.2.3.4',
|
||||
['setCity', 'setCountry', 'setLatitude', 'setLongitude', 'setIp'],
|
||||
];
|
||||
yield 'fallback IP' => [Visit::forBasePath(new Visitor('', '', '1.2.3.4', '')), null, ['setIp']];
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideUrlsToTrack')]
|
||||
public function properUrlIsTracked(Visit $visit, string $expectedTrackedUrl): void
|
||||
{
|
||||
$tracker = $this->createMock(MatomoTracker::class);
|
||||
$tracker->expects($this->once())->method('setUrl')->with($expectedTrackedUrl)->willReturn($tracker);
|
||||
$tracker->expects($this->once())->method('setUserAgent')->willReturn($tracker);
|
||||
$tracker->expects($this->once())->method('setUrlReferrer')->willReturn($tracker);
|
||||
$tracker->expects($this->any())->method('setCustomTrackingParameter')->willReturn($tracker);
|
||||
$tracker->expects($this->once())->method('doTrackPageView');
|
||||
|
||||
$this->trackerBuilder->expects($this->once())->method('buildMatomoTracker')->willReturn($tracker);
|
||||
|
||||
$this->visitSender->sendVisitToMatomo($visit);
|
||||
}
|
||||
|
||||
public static function provideUrlsToTrack(): iterable
|
||||
{
|
||||
yield 'orphan visit without visited URL' => [Visit::forBasePath(Visitor::emptyInstance()), ''];
|
||||
yield 'orphan visit with visited URL' => [
|
||||
Visit::forBasePath(new Visitor('', '', null, 'https://s.test/foo')),
|
||||
'https://s.test/foo',
|
||||
];
|
||||
yield 'non-orphan visit' => [
|
||||
Visit::forValidShortUrl(ShortUrl::create(
|
||||
ShortUrlCreation::fromRawData([
|
||||
ShortUrlInputFilter::LONG_URL => 'https://shlink.io',
|
||||
ShortUrlInputFilter::CUSTOM_SLUG => 'bar',
|
||||
]),
|
||||
), Visitor::emptyInstance()),
|
||||
'http://s2.test/bar',
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user