From bd5d3cb6fa3b41d9b28fbaac7949c3071a716216 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 20 Nov 2023 10:11:15 +0100 Subject: [PATCH] Create SendVisitToMatomoTest --- .../ShortUrl/Helper/ShortUrlStringifier.php | 3 + .../Matomo/SendVisitToMatomoTest.php | 189 ++++++++++++++++++ .../test/Matomo/MatomoTrackerBuilderTest.php | 4 +- 3 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 module/Core/test/EventDispatcher/Matomo/SendVisitToMatomoTest.php diff --git a/module/Core/src/ShortUrl/Helper/ShortUrlStringifier.php b/module/Core/src/ShortUrl/Helper/ShortUrlStringifier.php index 9d21cb58..886a4d25 100644 --- a/module/Core/src/ShortUrl/Helper/ShortUrlStringifier.php +++ b/module/Core/src/ShortUrl/Helper/ShortUrlStringifier.php @@ -11,6 +11,9 @@ use function sprintf; class ShortUrlStringifier implements ShortUrlStringifierInterface { + /** + * @param array{schema?: string, hostname?: string} $domainConfig + */ public function __construct(private readonly array $domainConfig, private readonly string $basePath = '') { } diff --git a/module/Core/test/EventDispatcher/Matomo/SendVisitToMatomoTest.php b/module/Core/test/EventDispatcher/Matomo/SendVisitToMatomoTest.php new file mode 100644 index 00000000..154c7943 --- /dev/null +++ b/module/Core/test/EventDispatcher/Matomo/SendVisitToMatomoTest.php @@ -0,0 +1,189 @@ +em = $this->createMock(EntityManagerInterface::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->trackerBuilder = $this->createMock(MatomoTrackerBuilderInterface::class); + } + + #[Test] + public function visitIsNotSentWhenMatomoIsDisabled(): void + { + $this->em->expects($this->never())->method('find'); + $this->trackerBuilder->expects($this->never())->method('buildMatomoTracker'); + $this->logger->expects($this->never())->method('error'); + $this->logger->expects($this->never())->method('warning'); + + ($this->listener(enabled: false))(new VisitLocated('123')); + } + + #[Test] + public function visitIsNotSentWhenItDoesNotExist(): void + { + $this->em->expects($this->once())->method('find')->willReturn(null); + $this->trackerBuilder->expects($this->never())->method('buildMatomoTracker'); + $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.', + ['visitId' => '123'], + ); + + ($this->listener())(new VisitLocated('123')); + } + + #[Test, DataProvider('provideTrackerMethods')] + public function visitIsSentWhenItExists(Visit $visit, ?string $originalIpAddress, array $invokedMethods): 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('doTrackPageView')->with(''); + + 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->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, $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 + { + $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->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', + ]; + } + + #[Test] + public function logsErrorWhenTrackingFails(): void + { + $visitId = '123'; + $e = new Exception('Error!'); + + $tracker = $this->createMock(MatomoTracker::class); + $tracker->expects($this->once())->method('setUrl')->willThrowException($e); + + $this->em->expects($this->once())->method('find')->with(Visit::class, $visitId)->willReturn( + $this->createMock(Visit::class), + ); + $this->trackerBuilder->expects($this->once())->method('buildMatomoTracker')->willReturn($tracker); + $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}', + ['e' => $e], + ); + + ($this->listener())(new VisitLocated($visitId)); + } + + private function listener(bool $enabled = true): SendVisitToMatomo + { + return new SendVisitToMatomo( + $this->em, + $this->logger, + new ShortUrlStringifier(['hostname' => 's2.test']), + new MatomoOptions(enabled: $enabled), + $this->trackerBuilder, + ); + } +} diff --git a/module/Core/test/Matomo/MatomoTrackerBuilderTest.php b/module/Core/test/Matomo/MatomoTrackerBuilderTest.php index b7550bad..5a38412a 100644 --- a/module/Core/test/Matomo/MatomoTrackerBuilderTest.php +++ b/module/Core/test/Matomo/MatomoTrackerBuilderTest.php @@ -37,8 +37,8 @@ class MatomoTrackerBuilderTest extends TestCase { $tracker = $this->builder()->buildMatomoTracker(); - self::assertEquals('api_token', $tracker->token_auth); - self::assertEquals(5, $tracker->idSite); + self::assertEquals('api_token', $tracker->token_auth); // @phpstan-ignore-line + self::assertEquals(5, $tracker->idSite); // @phpstan-ignore-line } private function builder(?MatomoOptions $options = null): MatomoTrackerBuilder