Merge pull request #2255 from acelaya-forks/feature/expose-tracked-visits

Return `Visit` object created when tracking a visit successfully
This commit is contained in:
Alejandro Celaya 2024-11-11 09:19:20 +01:00 committed by GitHub
commit 59a4704658
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 120 additions and 116 deletions

View File

@ -107,7 +107,7 @@ class LocateVisitsCommandTest extends TestCase
#[Test, DataProvider('provideIgnoredAddresses')]
public function localhostAndEmptyAddressesAreIgnored(IpCannotBeLocatedException $e, string $message): void
{
$visit = Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance());
$visit = Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::empty());
$location = VisitLocation::fromGeolocation(Location::emptyInstance());
$this->lock->method('acquire')->with($this->isFalse())->willReturn(true);

View File

@ -51,7 +51,7 @@ final class Visitor
);
}
public static function emptyInstance(): self
public static function empty(): self
{
return new self('', '', null, '');
}

View File

@ -12,6 +12,7 @@ use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType;
use Shlinkio\Shlink\Core\Exception\InvalidIpFormatException;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Util\IpAddressUtils;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
use function Shlinkio\Shlink\Core\ipAddressFromRequest;
@ -22,24 +23,26 @@ readonly class RequestTracker implements RequestTrackerInterface, RequestMethodI
{
}
public function trackIfApplicable(ShortUrl $shortUrl, ServerRequestInterface $request): void
{
if ($this->shouldTrackRequest($request)) {
$this->visitsTracker->track($shortUrl, Visitor::fromRequest($request));
}
}
public function trackNotFoundIfApplicable(ServerRequestInterface $request): void
public function trackIfApplicable(ShortUrl $shortUrl, ServerRequestInterface $request): Visit|null
{
if (! $this->shouldTrackRequest($request)) {
return;
return null;
}
return $this->visitsTracker->track($shortUrl, Visitor::fromRequest($request));
}
public function trackNotFoundIfApplicable(ServerRequestInterface $request): Visit|null
{
if (! $this->shouldTrackRequest($request)) {
return null;
}
/** @var NotFoundType|null $notFoundType */
$notFoundType = $request->getAttribute(NotFoundType::class);
$visitor = Visitor::fromRequest($request);
match (true) {
return match (true) {
$notFoundType?->isBaseUrl() => $this->visitsTracker->trackBaseUrlVisit($visitor),
$notFoundType?->isRegularNotFound() => $this->visitsTracker->trackRegularNotFoundVisit($visitor),
$notFoundType?->isInvalidShortUrl() => $this->visitsTracker->trackInvalidShortUrlVisit($visitor),

View File

@ -6,10 +6,11 @@ namespace Shlinkio\Shlink\Core\Visit;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
interface RequestTrackerInterface
{
public function trackIfApplicable(ShortUrl $shortUrl, ServerRequestInterface $request): void;
public function trackIfApplicable(ShortUrl $shortUrl, ServerRequestInterface $request): Visit|null;
public function trackNotFoundIfApplicable(ServerRequestInterface $request): void;
public function trackNotFoundIfApplicable(ServerRequestInterface $request): Visit|null;
}

View File

@ -21,65 +21,63 @@ readonly class VisitsTracker implements VisitsTrackerInterface
) {
}
public function track(ShortUrl $shortUrl, Visitor $visitor): void
public function track(ShortUrl $shortUrl, Visitor $visitor): Visit|null
{
$this->trackVisit(
return $this->trackVisit(
fn (Visitor $v) => Visit::forValidShortUrl($shortUrl, $v, $this->options->anonymizeRemoteAddr),
$visitor,
);
}
public function trackInvalidShortUrlVisit(Visitor $visitor): void
public function trackInvalidShortUrlVisit(Visitor $visitor): Visit|null
{
$this->trackOrphanVisit(
return $this->trackOrphanVisit(
fn (Visitor $v) => Visit::forInvalidShortUrl($v, $this->options->anonymizeRemoteAddr),
$visitor,
);
}
public function trackBaseUrlVisit(Visitor $visitor): void
public function trackBaseUrlVisit(Visitor $visitor): Visit|null
{
$this->trackOrphanVisit(
return $this->trackOrphanVisit(
fn (Visitor $v) => Visit::forBasePath($v, $this->options->anonymizeRemoteAddr),
$visitor,
);
}
public function trackRegularNotFoundVisit(Visitor $visitor): void
public function trackRegularNotFoundVisit(Visitor $visitor): Visit|null
{
$this->trackOrphanVisit(
return $this->trackOrphanVisit(
fn (Visitor $v) => Visit::forRegularNotFound($v, $this->options->anonymizeRemoteAddr),
$visitor,
);
}
private function trackOrphanVisit(callable $createVisit, Visitor $visitor): void
private function trackOrphanVisit(callable $createVisit, Visitor $visitor): Visit|null
{
if (! $this->options->trackOrphanVisits) {
return;
return null;
}
$this->trackVisit($createVisit, $visitor);
return $this->trackVisit($createVisit, $visitor);
}
/**
* @param callable(Visitor $visitor): Visit $createVisit
*/
private function trackVisit(callable $createVisit, Visitor $visitor): void
private function trackVisit(callable $createVisit, Visitor $visitor): Visit|null
{
if ($this->options->disableTracking) {
return;
return null;
}
$visit = $createVisit($visitor->normalizeForTrackingOptions($this->options));
// Wrap persisting and flushing the visit in a transaction, so that the ShortUrlVisitsCountTracker performs
// changes inside that very same transaction atomically
$this->em->wrapInTransaction(function () use ($visit): void {
$this->em->persist($visit);
$this->em->flush();
});
// Wrap persisting the visit in a transaction, so that the ShortUrlVisitsCountTracker performs changes inside
// that very same transaction atomically
$this->em->wrapInTransaction(fn () => $this->em->persist($visit));
$this->eventDispatcher->dispatch(new UrlVisited($visit->getId(), $visitor->remoteAddress));
return $visit;
}
}

View File

@ -5,15 +5,16 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Visit;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
interface VisitsTrackerInterface
{
public function track(ShortUrl $shortUrl, Visitor $visitor): void;
public function track(ShortUrl $shortUrl, Visitor $visitor): Visit|null;
public function trackInvalidShortUrlVisit(Visitor $visitor): void;
public function trackInvalidShortUrlVisit(Visitor $visitor): Visit|null;
public function trackBaseUrlVisit(Visitor $visitor): void;
public function trackBaseUrlVisit(Visitor $visitor): Visit|null;
public function trackRegularNotFoundVisit(Visitor $visitor): void;
public function trackRegularNotFoundVisit(Visitor $visitor): Visit|null;
}

View File

@ -92,7 +92,7 @@ class DeleteExpiredShortUrlsRepositoryTest extends DatabaseTestCase
$this->getEntityManager()->persist($shortUrl);
for ($j = 0; $j < $visitsPerShortUrl; $j++) {
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl, Visitor::empty()));
}
}
}

View File

@ -74,7 +74,7 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
$foo2 = ShortUrl::withLongUrl('https://foo_2');
$visits2 = array_map(function () use ($foo2) {
$visit = Visit::forValidShortUrl($foo2, Visitor::emptyInstance());
$visit = Visit::forValidShortUrl($foo2, Visitor::empty());
$this->getEntityManager()->persist($visit);
return $visit;
@ -304,9 +304,9 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
'maxVisits' => 3,
]), $this->relationResolver);
$this->getEntityManager()->persist($shortUrl4);
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl4, Visitor::emptyInstance()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl4, Visitor::emptyInstance()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl4, Visitor::emptyInstance()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl4, Visitor::empty()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl4, Visitor::empty()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl4, Visitor::empty()));
$this->getEntityManager()->flush();

View File

@ -79,13 +79,13 @@ class TagRepositoryTest extends DatabaseTestCase
$shortUrl = ShortUrl::create($metaWithTags($firstUrlTags, $apiKey), $this->relationResolver);
$this->getEntityManager()->persist($shortUrl);
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl, Visitor::empty()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl, Visitor::empty()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl, Visitor::botInstance()));
$shortUrl2 = ShortUrl::create($metaWithTags($secondUrlTags, null), $this->relationResolver);
$this->getEntityManager()->persist($shortUrl2);
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl2, Visitor::emptyInstance()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl2, Visitor::empty()));
// One of the tags has two extra short URLs, but with no visits
$this->getEntityManager()->persist(

View File

@ -26,7 +26,7 @@ class OrphanVisitsCountTrackerTest extends DatabaseTestCase
#[Test]
public function createsNewEntriesWhenNoneExist(): void
{
$visit = Visit::forBasePath(Visitor::emptyInstance());
$visit = Visit::forBasePath(Visitor::empty());
$this->getEntityManager()->persist($visit);
$this->getEntityManager()->flush();
@ -47,7 +47,7 @@ class OrphanVisitsCountTrackerTest extends DatabaseTestCase
}
$this->getEntityManager()->flush();
$visit = Visit::forRegularNotFound(Visitor::emptyInstance());
$visit = Visit::forRegularNotFound(Visitor::empty());
$this->getEntityManager()->persist($visit);
$this->getEntityManager()->flush();

View File

@ -30,7 +30,7 @@ class ShortUrlVisitsCountTrackerTest extends DatabaseTestCase
$shortUrl = ShortUrl::createFake();
$this->getEntityManager()->persist($shortUrl);
$visit = Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance());
$visit = Visit::forValidShortUrl($shortUrl, Visitor::empty());
$this->getEntityManager()->persist($visit);
$this->getEntityManager()->flush();
@ -54,7 +54,7 @@ class ShortUrlVisitsCountTrackerTest extends DatabaseTestCase
}
$this->getEntityManager()->flush();
$visit = Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance());
$visit = Visit::forValidShortUrl($shortUrl, Visitor::empty());
$this->getEntityManager()->persist($visit);
$this->getEntityManager()->flush();

View File

@ -28,8 +28,8 @@ class VisitDeleterRepositoryTest extends DatabaseTestCase
{
$shortUrl1 = ShortUrl::withLongUrl('https://foo.com');
$this->getEntityManager()->persist($shortUrl1);
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl1, Visitor::emptyInstance()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl1, Visitor::emptyInstance()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl1, Visitor::empty()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl1, Visitor::empty()));
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([
ShortUrlInputFilter::LONG_URL => 'https://foo.com',
@ -37,17 +37,17 @@ class VisitDeleterRepositoryTest extends DatabaseTestCase
ShortUrlInputFilter::CUSTOM_SLUG => 'foo',
]), new PersistenceShortUrlRelationResolver($this->getEntityManager()));
$this->getEntityManager()->persist($shortUrl2);
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl2, Visitor::emptyInstance()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl2, Visitor::emptyInstance()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl2, Visitor::emptyInstance()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl2, Visitor::emptyInstance()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl2, Visitor::empty()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl2, Visitor::empty()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl2, Visitor::empty()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl2, Visitor::empty()));
$shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([
ShortUrlInputFilter::LONG_URL => 'https://foo.com',
ShortUrlInputFilter::CUSTOM_SLUG => 'foo',
]), new PersistenceShortUrlRelationResolver($this->getEntityManager()));
$this->getEntityManager()->persist($shortUrl3);
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl3, Visitor::emptyInstance()));
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl3, Visitor::empty()));
$this->getEntityManager()->flush();
@ -62,7 +62,7 @@ class VisitDeleterRepositoryTest extends DatabaseTestCase
#[Test]
public function deletesExpectedOrphanVisits(): void
{
$visitor = Visitor::emptyInstance();
$visitor = Visitor::empty();
$this->getEntityManager()->persist(Visit::forBasePath($visitor));
$this->getEntityManager()->persist(Visit::forInvalidShortUrl($visitor));
$this->getEntityManager()->persist(Visit::forRegularNotFound($visitor));

View File

@ -37,7 +37,7 @@ class VisitIterationRepositoryTest extends DatabaseTestCase
$unmodifiedDate = Chronos::now();
for ($i = 0; $i < 6; $i++) {
Chronos::setTestNow($unmodifiedDate->subDays($i)); // Enforce a different day for every visit
$visit = Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance());
$visit = Visit::forValidShortUrl($shortUrl, Visitor::empty());
if ($i >= 2) {
$location = VisitLocation::fromGeolocation(Location::emptyInstance());

View File

@ -311,9 +311,9 @@ class VisitRepositoryTest extends DatabaseTestCase
$this->getEntityManager()->persist($domainApiKey);
// Visits not linked to any short URL
$this->getEntityManager()->persist(Visit::forBasePath(Visitor::emptyInstance()));
$this->getEntityManager()->persist(Visit::forInvalidShortUrl(Visitor::emptyInstance()));
$this->getEntityManager()->persist(Visit::forRegularNotFound(Visitor::emptyInstance()));
$this->getEntityManager()->persist(Visit::forBasePath(Visitor::empty()));
$this->getEntityManager()->persist(Visit::forInvalidShortUrl(Visitor::empty()));
$this->getEntityManager()->persist(Visit::forRegularNotFound(Visitor::empty()));
$this->getEntityManager()->persist(Visit::forRegularNotFound(Visitor::botInstance()));
$this->getEntityManager()->flush();
@ -370,15 +370,15 @@ class VisitRepositoryTest extends DatabaseTestCase
$botsCount = 3;
for ($i = 0; $i < 6; $i++) {
$this->getEntityManager()->persist($this->setDateOnVisit(
fn () => Visit::forBasePath($botsCount < 1 ? Visitor::emptyInstance() : Visitor::botInstance()),
fn () => Visit::forBasePath($botsCount < 1 ? Visitor::empty() : Visitor::botInstance()),
Chronos::parse(sprintf('2020-01-0%s', $i + 1)),
));
$this->getEntityManager()->persist($this->setDateOnVisit(
fn () => Visit::forInvalidShortUrl(Visitor::emptyInstance()),
fn () => Visit::forInvalidShortUrl(Visitor::empty()),
Chronos::parse(sprintf('2020-01-0%s', $i + 1)),
));
$this->getEntityManager()->persist($this->setDateOnVisit(
fn () => Visit::forRegularNotFound(Visitor::emptyInstance()),
fn () => Visit::forRegularNotFound(Visitor::empty()),
Chronos::parse(sprintf('2020-01-0%s', $i + 1)),
));
@ -428,15 +428,15 @@ class VisitRepositoryTest extends DatabaseTestCase
for ($i = 0; $i < 6; $i++) {
$this->getEntityManager()->persist($this->setDateOnVisit(
fn () => Visit::forBasePath(Visitor::emptyInstance()),
fn () => Visit::forBasePath(Visitor::empty()),
Chronos::parse(sprintf('2020-01-0%s', $i + 1)),
));
$this->getEntityManager()->persist($this->setDateOnVisit(
fn () => Visit::forInvalidShortUrl(Visitor::emptyInstance()),
fn () => Visit::forInvalidShortUrl(Visitor::empty()),
Chronos::parse(sprintf('2020-01-0%s', $i + 1)),
));
$this->getEntityManager()->persist($this->setDateOnVisit(
fn () => Visit::forRegularNotFound(Visitor::emptyInstance()),
fn () => Visit::forRegularNotFound(Visitor::empty()),
Chronos::parse(sprintf('2020-01-0%s', $i + 1)),
));
}
@ -515,7 +515,7 @@ class VisitRepositoryTest extends DatabaseTestCase
{
$this->assertNull($this->repo->findMostRecentOrphanVisit());
$lastVisit = Visit::forBasePath(Visitor::emptyInstance());
$lastVisit = Visit::forBasePath(Visitor::empty());
$this->getEntityManager()->persist($lastVisit);
$this->getEntityManager()->flush();
@ -567,7 +567,7 @@ class VisitRepositoryTest extends DatabaseTestCase
$visit = $this->setDateOnVisit(
fn () => Visit::forValidShortUrl(
$shortUrl,
$botsAmount < 1 ? Visitor::emptyInstance() : Visitor::botInstance(),
$botsAmount < 1 ? Visitor::empty() : Visitor::botInstance(),
),
Chronos::parse(sprintf('2016-01-%s', str_pad((string) ($i + 1), 2, '0', STR_PAD_LEFT)))->startOfDay(),
);

View File

@ -39,7 +39,7 @@ class LocateUnlocatedVisitsTest extends TestCase
#[Test]
public function visitToLocationHelperIsCalledToGeolocateVisits(): void
{
$visit = Visit::forBasePath(Visitor::emptyInstance());
$visit = Visit::forBasePath(Visitor::empty());
$location = Location::emptyInstance();
$this->visitToLocation->expects($this->once())->method('resolveVisitLocation')->with($visit)->willReturn(

View File

@ -60,7 +60,7 @@ class SendVisitToMatomoTest extends TestCase
public function visitIsSentWhenItExists(string|null $originalIpAddress): void
{
$visitId = '123';
$visit = Visit::forBasePath(Visitor::emptyInstance());
$visit = Visit::forBasePath(Visitor::empty());
$this->em->expects($this->once())->method('find')->with(Visit::class, $visitId)->willReturn($visit);
$this->visitSender->expects($this->once())->method('sendVisit')->with($visit, $originalIpAddress);

View File

@ -61,7 +61,7 @@ class NotifyVisitToMercureTest extends TestCase
public function notificationsAreSentWhenVisitIsFound(): void
{
$visitId = '123';
$visit = Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance());
$visit = Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::empty());
$update = Update::forTopicAndPayload('', []);
$this->em->expects($this->once())->method('find')->with(Visit::class, $visitId)->willReturn($visit);
@ -81,7 +81,7 @@ class NotifyVisitToMercureTest extends TestCase
public function debugIsLoggedWhenExceptionIsThrown(): void
{
$visitId = '123';
$visit = Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance());
$visit = Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::empty());
$update = Update::forTopicAndPayload('', []);
$e = new RuntimeException('Error');
@ -122,7 +122,7 @@ class NotifyVisitToMercureTest extends TestCase
public static function provideOrphanVisits(): iterable
{
$visitor = Visitor::emptyInstance();
$visitor = Visitor::empty();
yield VisitType::REGULAR_404->value => [Visit::forRegularNotFound($visitor)];
yield VisitType::INVALID_SHORT_URL->value => [Visit::forInvalidShortUrl($visitor)];

View File

@ -48,7 +48,7 @@ class PublishingUpdatesGeneratorTest extends TestCase
'longUrl' => 'https://longUrl',
'title' => $title,
]));
$visit = Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance());
$visit = Visit::forValidShortUrl($shortUrl, Visitor::empty());
/** @var Update $update */
$update = $this->generator->{$method}($visit);
@ -111,7 +111,7 @@ class PublishingUpdatesGeneratorTest extends TestCase
public static function provideOrphanVisits(): iterable
{
$visitor = Visitor::emptyInstance();
$visitor = Visitor::empty();
yield VisitType::REGULAR_404->value => [Visit::forRegularNotFound($visitor)];
yield VisitType::INVALID_SHORT_URL->value => [Visit::forInvalidShortUrl($visitor)];

View File

@ -90,7 +90,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
public static function provideVisits(): iterable
{
$visitor = Visitor::emptyInstance();
$visitor = Visitor::empty();
yield 'orphan visit' => [Visit::forBasePath($visitor), ['newOrphanVisitUpdate']];
yield 'non-orphan visit' => [
@ -110,7 +110,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
{
$visitId = '123';
$this->em->expects($this->once())->method('find')->with(Visit::class, $visitId)->willReturn(
Visit::forBasePath(Visitor::emptyInstance()),
Visit::forBasePath(Visitor::empty()),
);
$this->updatesGenerator->expects($this->once())->method('newOrphanVisitUpdate')->with(
$this->isInstanceOf(Visit::class),
@ -152,7 +152,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
$never = static fn () => $exactly(0);
yield 'non-orphan visit' => [
Visit::forValidShortUrl(ShortUrl::withLongUrl('https://longUrl'), Visitor::emptyInstance()),
Visit::forValidShortUrl(ShortUrl::withLongUrl('https://longUrl'), Visitor::empty()),
function (MockObject & PublishingUpdatesGeneratorInterface $updatesGenerator) use ($once, $never): void {
$update = Update::forTopicAndPayload('', []);
$updatesGenerator->expects($never())->method('newOrphanVisitUpdate');
@ -166,7 +166,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
},
];
yield 'orphan visit' => [
Visit::forBasePath(Visitor::emptyInstance()),
Visit::forBasePath(Visitor::empty()),
function (MockObject & PublishingUpdatesGeneratorInterface $updatesGenerator) use ($once, $never): void {
$update = Update::forTopicAndPayload('', []);
$updatesGenerator->expects($once())->method('newOrphanVisitUpdate')->willReturn($update);

View File

@ -53,7 +53,7 @@ class NotifyVisitToRedisTest extends TestCase
{
$visitId = '123';
$this->em->expects($this->once())->method('find')->with(Visit::class, $visitId)->willReturn(
Visit::forBasePath(Visitor::emptyInstance()),
Visit::forBasePath(Visitor::empty()),
);
$this->updatesGenerator->expects($this->once())->method('newOrphanVisitUpdate')->with(
$this->isInstanceOf(Visit::class),

View File

@ -77,9 +77,9 @@ class MatomoVisitSenderTest extends TestCase
public static function provideTrackerMethods(): iterable
{
yield 'unlocated orphan visit' => [Visit::forBasePath(Visitor::emptyInstance()), null, []];
yield 'unlocated orphan visit' => [Visit::forBasePath(Visitor::empty()), null, []];
yield 'located regular visit' => [
Visit::forValidShortUrl(ShortUrl::withLongUrl('https://shlink.io'), Visitor::emptyInstance())
Visit::forValidShortUrl(ShortUrl::withLongUrl('https://shlink.io'), Visitor::empty())
->locate(VisitLocation::fromGeolocation(new Location(
countryCode: 'countryCode',
countryName: 'countryName',
@ -115,7 +115,7 @@ class MatomoVisitSenderTest extends TestCase
public static function provideUrlsToTrack(): iterable
{
yield 'orphan visit without visited URL' => [Visit::forBasePath(Visitor::emptyInstance()), ''];
yield 'orphan visit without visited URL' => [Visit::forBasePath(Visitor::empty()), ''];
yield 'orphan visit with visited URL' => [
Visit::forBasePath(new Visitor('', '', null, 'https://s.test/foo')),
'https://s.test/foo',
@ -126,7 +126,7 @@ class MatomoVisitSenderTest extends TestCase
ShortUrlInputFilter::LONG_URL => 'https://shlink.io',
ShortUrlInputFilter::CUSTOM_SLUG => 'bar',
]),
), Visitor::emptyInstance()),
), Visitor::empty()),
'http://s2.test/bar',
];
}
@ -135,7 +135,7 @@ class MatomoVisitSenderTest extends TestCase
public function multipleVisitsCanBeSent(): void
{
$dateRange = DateRange::allTime();
$visitor = Visitor::emptyInstance();
$visitor = Visitor::empty();
$bot = Visitor::botInstance();
$this->visitIterationRepository->expects($this->once())->method('findAllVisits')->with($dateRange)->willReturn([

View File

@ -34,7 +34,7 @@ class DeleteShortUrlServiceTest extends TestCase
protected function setUp(): void
{
$shortUrl = ShortUrl::createFake()->setVisits(new ArrayCollection(
array_map(fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()), range(0, 10)),
array_map(fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::empty()), range(0, 10)),
));
$this->shortCode = $shortUrl->getShortCode();

View File

@ -128,7 +128,7 @@ class ShortUrlResolverTest extends TestCase
ShortUrlCreation::fromRawData(['maxVisits' => 3, 'longUrl' => 'https://longUrl']),
);
$shortUrl->setVisits(new ArrayCollection(array_map(
fn () => Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance()),
fn () => Visit::forValidShortUrl($shortUrl, Visitor::empty()),
range(0, 4),
)));
@ -147,7 +147,7 @@ class ShortUrlResolverTest extends TestCase
'longUrl' => 'https://longUrl',
]));
$shortUrl->setVisits(new ArrayCollection(array_map(
fn () => Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance()),
fn () => Visit::forValidShortUrl($shortUrl, Visitor::empty()),
range(0, 4),
)));

View File

@ -55,7 +55,7 @@ class VisitTest extends TestCase
public static function provideOrphanVisits(): iterable
{
yield 'base path visit' => [
$visit = Visit::forBasePath(Visitor::emptyInstance()),
$visit = Visit::forBasePath(Visitor::empty()),
[
'referer' => '',
'date' => $visit->date->toAtomString(),

View File

@ -48,7 +48,7 @@ class VisitLocatorTest extends TestCase
$unlocatedVisits = array_map(
fn (int $i) => Visit::forValidShortUrl(
ShortUrl::withLongUrl(sprintf('https://short_code_%s', $i)),
Visitor::emptyInstance(),
Visitor::empty(),
),
range(1, 200),
);
@ -87,7 +87,7 @@ class VisitLocatorTest extends TestCase
bool $isNonLocatableAddress,
): void {
$unlocatedVisits = [
Visit::forValidShortUrl(ShortUrl::withLongUrl('https://foo'), Visitor::emptyInstance()),
Visit::forValidShortUrl(ShortUrl::withLongUrl('https://foo'), Visitor::empty()),
];
$this->repo->expects($this->once())->method($expectedRepoMethodName)->willReturn($unlocatedVisits);

View File

@ -40,7 +40,7 @@ class VisitToLocationHelperTest extends TestCase
public static function provideNonLocatableVisits(): iterable
{
yield [Visit::forBasePath(Visitor::emptyInstance()), IpCannotBeLocatedException::forEmptyAddress()];
yield [Visit::forBasePath(Visitor::empty()), IpCannotBeLocatedException::forEmptyAddress()];
yield [
Visit::forBasePath(new Visitor('foo', 'bar', IpAddress::LOCALHOST, '')),
IpCannotBeLocatedException::forLocalhost(),

View File

@ -53,7 +53,7 @@ class NonOrphanVisitsPaginatorAdapterTest extends TestCase
#[Test, DataProvider('provideLimitAndOffset')]
public function getSliceDelegatesToRepository(int $limit, int $offset): void
{
$visitor = Visitor::emptyInstance();
$visitor = Visitor::empty();
$list = [Visit::forRegularNotFound($visitor), Visit::forInvalidShortUrl($visitor)];
$this->repo->expects($this->once())->method('findNonOrphanVisits')->with(new VisitsListFiltering(
$this->params->dateRange,

View File

@ -53,7 +53,7 @@ class OrphanVisitsPaginatorAdapterTest extends TestCase
#[Test, DataProvider('provideLimitAndOffset')]
public function getSliceDelegatesToRepository(int $limit, int $offset): void
{
$visitor = Visitor::emptyInstance();
$visitor = Visitor::empty();
$list = [Visit::forRegularNotFound($visitor), Visit::forInvalidShortUrl($visitor)];
$this->repo->expects($this->once())->method('findOrphanVisits')->with(new OrphanVisitsListFiltering(
dateRange: $this->params->dateRange,

View File

@ -104,7 +104,7 @@ class VisitsStatsHelperTest extends TestCase
$repo->expects($this->once())->method('shortCodeIsInUse')->with($identifier, $spec)->willReturn(true);
$list = array_map(
static fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()),
static fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::empty()),
range(0, 1),
);
$repo2 = $this->createMock(VisitRepository::class);
@ -164,7 +164,7 @@ class VisitsStatsHelperTest extends TestCase
$repo->expects($this->once())->method('tagExists')->with($tag, $apiKey)->willReturn(true);
$list = array_map(
static fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()),
static fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::empty()),
range(0, 1),
);
$repo2 = $this->createMock(VisitRepository::class);
@ -205,7 +205,7 @@ class VisitsStatsHelperTest extends TestCase
$repo->expects($this->once())->method('domainExists')->with($domain, $apiKey)->willReturn(true);
$list = array_map(
static fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()),
static fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::empty()),
range(0, 1),
);
$repo2 = $this->createMock(VisitRepository::class);
@ -235,7 +235,7 @@ class VisitsStatsHelperTest extends TestCase
$repo->expects($this->never())->method('domainExists');
$list = array_map(
static fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()),
static fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::empty()),
range(0, 1),
);
$repo2 = $this->createMock(VisitRepository::class);
@ -261,7 +261,7 @@ class VisitsStatsHelperTest extends TestCase
#[Test]
public function orphanVisitsAreReturnedAsExpected(): void
{
$list = array_map(static fn () => Visit::forBasePath(Visitor::emptyInstance()), range(0, 3));
$list = array_map(static fn () => Visit::forBasePath(Visitor::empty()), range(0, 3));
$repo = $this->createMock(VisitRepository::class);
$repo->expects($this->once())->method('countOrphanVisits')->with(
$this->isInstanceOf(OrphanVisitsCountFiltering::class),
@ -280,7 +280,7 @@ class VisitsStatsHelperTest extends TestCase
public function nonOrphanVisitsAreReturnedAsExpected(): void
{
$list = array_map(
static fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()),
static fn () => Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::empty()),
range(0, 3),
);
$repo = $this->createMock(VisitRepository::class);

View File

@ -33,43 +33,44 @@ class VisitsTrackerTest extends TestCase
#[Test, DataProvider('provideTrackingMethodNames')]
public function trackPersistsVisitAndDispatchesEvent(string $method, array $args): void
{
$this->em->expects($this->once())->method('persist')->with(
$this->callback(fn (Visit $visit) => $visit->setId('1') !== null),
);
$this->em->expects($this->once())->method('flush');
$this->em->expects($this->once())->method('persist')->with($this->isInstanceOf(Visit::class));
$this->eventDispatcher->expects($this->once())->method('dispatch')->with(
$this->isInstanceOf(UrlVisited::class),
);
$this->visitsTracker()->{$method}(...$args);
$result = $this->visitsTracker()->{$method}(...$args);
self::assertInstanceOf(Visit::class, $result);
}
#[Test, DataProvider('provideTrackingMethodNames')]
public function trackingIsSkippedCompletelyWhenDisabledFromOptions(string $method, array $args): void
{
$this->em->expects($this->never())->method('persist');
$this->em->expects($this->never())->method('flush');
$this->eventDispatcher->expects($this->never())->method('dispatch');
$this->visitsTracker(new TrackingOptions(disableTracking: true))->{$method}(...$args);
$result = $this->visitsTracker(new TrackingOptions(disableTracking: true))->{$method}(...$args);
self::assertNull($result);
}
public static function provideTrackingMethodNames(): iterable
{
yield 'track' => ['track', [ShortUrl::createFake(), Visitor::emptyInstance()]];
yield 'trackInvalidShortUrlVisit' => ['trackInvalidShortUrlVisit', [Visitor::emptyInstance()]];
yield 'trackBaseUrlVisit' => ['trackBaseUrlVisit', [Visitor::emptyInstance()]];
yield 'trackRegularNotFoundVisit' => ['trackRegularNotFoundVisit', [Visitor::emptyInstance()]];
yield 'track' => ['track', [ShortUrl::createFake(), Visitor::empty()]];
yield 'trackInvalidShortUrlVisit' => ['trackInvalidShortUrlVisit', [Visitor::empty()]];
yield 'trackBaseUrlVisit' => ['trackBaseUrlVisit', [Visitor::empty()]];
yield 'trackRegularNotFoundVisit' => ['trackRegularNotFoundVisit', [Visitor::empty()]];
}
#[Test, DataProvider('provideOrphanTrackingMethodNames')]
public function orphanVisitsAreNotTrackedWhenDisabled(string $method): void
{
$this->em->expects($this->never())->method('persist');
$this->em->expects($this->never())->method('flush');
$this->eventDispatcher->expects($this->never())->method('dispatch');
$this->visitsTracker(new TrackingOptions(trackOrphanVisits: false))->{$method}(Visitor::emptyInstance());
$result = $this->visitsTracker(new TrackingOptions(trackOrphanVisits: false))->{$method}(Visitor::empty());
self::assertNull($result);
}
public static function provideOrphanTrackingMethodNames(): iterable

View File

@ -35,7 +35,7 @@ class OrphanVisitsActionTest extends TestCase
#[Test]
public function requestIsHandled(): void
{
$visitor = Visitor::emptyInstance();
$visitor = Visitor::empty();
$visits = [Visit::forInvalidShortUrl($visitor), Visit::forRegularNotFound($visitor)];
$this->visitsHelper->expects($this->once())->method('orphanVisits')->with(
$this->isInstanceOf(OrphanVisitsParams::class),