diff --git a/composer.json b/composer.json index 26669e2e..f391842b 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,7 @@ "shlinkio/shlink-config": "^4.0", "shlinkio/shlink-event-dispatcher": "^4.2", "shlinkio/shlink-importer": "^5.6", - "shlinkio/shlink-installer": "^9.5", + "shlinkio/shlink-installer": "dev-develop#c5a8523 as 9.6", "shlinkio/shlink-ip-geolocation": "^4.3", "shlinkio/shlink-json": "^1.2", "spiral/roadrunner": "^2025.1", diff --git a/config/autoload/installer.global.php b/config/autoload/installer.global.php index 24b9263e..063ff7d4 100644 --- a/config/autoload/installer.global.php +++ b/config/autoload/installer.global.php @@ -76,6 +76,7 @@ return [ Option\Matomo\MatomoBaseUrlConfigOption::class, Option\Matomo\MatomoSiteIdConfigOption::class, Option\Matomo\MatomoApiTokenConfigOption::class, + Option\RealTimeUpdates\RealTimeUpdatesTopicsConfigOption::class, ], 'installation_commands' => [ diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index 1cd1e961..75c70594 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -36,6 +36,7 @@ return [ Config\Options\QrCodeOptions::class => [Config\Options\QrCodeOptions::class, 'fromEnv'], Config\Options\RabbitMqOptions::class => [Config\Options\RabbitMqOptions::class, 'fromEnv'], Config\Options\RobotsOptions::class => [Config\Options\RobotsOptions::class, 'fromEnv'], + Config\Options\RealTimeUpdatesOptions::class => [Config\Options\RealTimeUpdatesOptions::class, 'fromEnv'], RedirectRule\ShortUrlRedirectRuleService::class => ConfigAbstractFactory::class, RedirectRule\ShortUrlRedirectionResolver::class => ConfigAbstractFactory::class, diff --git a/module/Core/config/event_dispatcher.config.php b/module/Core/config/event_dispatcher.config.php index d146a516..b5db4b85 100644 --- a/module/Core/config/event_dispatcher.config.php +++ b/module/Core/config/event_dispatcher.config.php @@ -110,18 +110,21 @@ return (static function (): array { EventDispatcher\PublishingUpdatesGenerator::class, 'em', 'Logger_Shlink', + Config\Options\RealTimeUpdatesOptions::class, ], EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => [ MercureHubPublishingHelper::class, EventDispatcher\PublishingUpdatesGenerator::class, 'em', 'Logger_Shlink', + Config\Options\RealTimeUpdatesOptions::class, ], EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class => [ RabbitMqPublishingHelper::class, EventDispatcher\PublishingUpdatesGenerator::class, 'em', 'Logger_Shlink', + Config\Options\RealTimeUpdatesOptions::class, Config\Options\RabbitMqOptions::class, ], EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class => [ @@ -129,6 +132,7 @@ return (static function (): array { EventDispatcher\PublishingUpdatesGenerator::class, 'em', 'Logger_Shlink', + Config\Options\RealTimeUpdatesOptions::class, Config\Options\RabbitMqOptions::class, ], EventDispatcher\RedisPubSub\NotifyVisitToRedis::class => [ @@ -136,6 +140,7 @@ return (static function (): array { EventDispatcher\PublishingUpdatesGenerator::class, 'em', 'Logger_Shlink', + Config\Options\RealTimeUpdatesOptions::class, 'config.redis.pub_sub_enabled', ], EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => [ @@ -143,6 +148,7 @@ return (static function (): array { EventDispatcher\PublishingUpdatesGenerator::class, 'em', 'Logger_Shlink', + Config\Options\RealTimeUpdatesOptions::class, 'config.redis.pub_sub_enabled', ], diff --git a/module/Core/functions/functions.php b/module/Core/functions/functions.php index 513e885d..72406e6a 100644 --- a/module/Core/functions/functions.php +++ b/module/Core/functions/functions.php @@ -255,14 +255,36 @@ function toProblemDetailsType(string $errorCode): string * @return string[] */ function enumValues(string $enum): array +{ + return enumSide($enum, 'value'); +} + +/** + * @param class-string $enum + * @return string[] + */ +function enumNames(string $enum): array +{ + return enumSide($enum, 'name'); +} + +/** + * @param class-string $enum + * @param 'name'|'value' $type + * @return string[] + */ +function enumSide(string $enum, string $type): array { static $cache; if ($cache === null) { $cache = []; } - return $cache[$enum] ?? ( - $cache[$enum] = array_map(static fn (BackedEnum $type) => (string) $type->value, $enum::cases()) + return $cache[$type][$enum] ?? ( + $cache[$type][$enum] = array_map( + static fn (BackedEnum $entry) => (string) ($type === 'name' ? $entry->name : $entry->value), + $enum::cases(), + ) ); } diff --git a/module/Core/src/Config/EnvVars.php b/module/Core/src/Config/EnvVars.php index 1f6f2c03..7fa1a95b 100644 --- a/module/Core/src/Config/EnvVars.php +++ b/module/Core/src/Config/EnvVars.php @@ -85,6 +85,7 @@ enum EnvVars: string case MEMORY_LIMIT = 'MEMORY_LIMIT'; case INITIAL_API_KEY = 'INITIAL_API_KEY'; case SKIP_INITIAL_GEOLITE_DOWNLOAD = 'SKIP_INITIAL_GEOLITE_DOWNLOAD'; + case REAL_TIME_UPDATES_TOPICS = 'REAL_TIME_UPDATES_TOPICS'; /** @deprecated Use REDIRECT_EXTRA_PATH */ case REDIRECT_APPEND_EXTRA_PATH = 'REDIRECT_APPEND_EXTRA_PATH'; diff --git a/module/Core/src/Config/Options/RealTimeUpdatesOptions.php b/module/Core/src/Config/Options/RealTimeUpdatesOptions.php new file mode 100644 index 00000000..1800b92f --- /dev/null +++ b/module/Core/src/Config/Options/RealTimeUpdatesOptions.php @@ -0,0 +1,63 @@ +enabledTopics = $enabledTopics === null ? $validTopics : self::validateTopics( + $enabledTopics, + $validTopics, + ); + } + + public static function fromEnv(): self + { + $enabledTopics = splitByComma(EnvVars::REAL_TIME_UPDATES_TOPICS->loadFromEnv()); + return new self(enabledTopics: count($enabledTopics) === 0 ? null : $enabledTopics); + } + + /** + * @param string[] $validTopics + * @return string[] + */ + private static function validateTopics(array $providedTopics, array $validTopics): array + { + return map($providedTopics, function (string $topic) use ($validTopics): string { + if (contains($topic, $validTopics)) { + return $topic; + } + + throw ValidationException::fromArray([ + 'topic' => sprintf( + 'Real-time updates topic "%s" is not valid. Expected one of ["%s"].', + $topic, + implode('", "', $validTopics), + ), + ]); + }); + } + + public function isTopicEnabled(Topic $topic): bool + { + return contains($topic->name, $this->enabledTopics); + } +} diff --git a/module/Core/src/EventDispatcher/Async/AbstractNotifyNewShortUrlListener.php b/module/Core/src/EventDispatcher/Async/AbstractNotifyNewShortUrlListener.php index 1aa2235d..aced04f0 100644 --- a/module/Core/src/EventDispatcher/Async/AbstractNotifyNewShortUrlListener.php +++ b/module/Core/src/EventDispatcher/Async/AbstractNotifyNewShortUrlListener.php @@ -7,8 +7,10 @@ namespace Shlinkio\Shlink\Core\EventDispatcher\Async; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface; +use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions; use Shlinkio\Shlink\Core\EventDispatcher\Event\ShortUrlCreated; use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface; +use Shlinkio\Shlink\Core\EventDispatcher\Topic; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; use Throwable; @@ -19,6 +21,7 @@ abstract class AbstractNotifyNewShortUrlListener extends AbstractAsyncListener private readonly PublishingUpdatesGeneratorInterface $updatesGenerator, private readonly EntityManagerInterface $em, private readonly LoggerInterface $logger, + private readonly RealTimeUpdatesOptions $realTimeUpdatesOptions, ) { } @@ -40,6 +43,10 @@ abstract class AbstractNotifyNewShortUrlListener extends AbstractAsyncListener return; } + if (! $this->realTimeUpdatesOptions->isTopicEnabled(Topic::NEW_SHORT_URL)) { + return; + } + try { $this->publishingHelper->publishUpdate($this->updatesGenerator->newShortUrlUpdate($shortUrl)); } catch (Throwable $e) { diff --git a/module/Core/src/EventDispatcher/Async/AbstractNotifyVisitListener.php b/module/Core/src/EventDispatcher/Async/AbstractNotifyVisitListener.php index e871588f..affe4f32 100644 --- a/module/Core/src/EventDispatcher/Async/AbstractNotifyVisitListener.php +++ b/module/Core/src/EventDispatcher/Async/AbstractNotifyVisitListener.php @@ -8,8 +8,10 @@ use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface; use Shlinkio\Shlink\Common\UpdatePublishing\Update; +use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions; use Shlinkio\Shlink\Core\EventDispatcher\Event\UrlVisited; use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface; +use Shlinkio\Shlink\Core\EventDispatcher\Topic; use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Throwable; @@ -22,6 +24,7 @@ abstract class AbstractNotifyVisitListener extends AbstractAsyncListener private readonly PublishingUpdatesGeneratorInterface $updatesGenerator, private readonly EntityManagerInterface $em, private readonly LoggerInterface $logger, + private readonly RealTimeUpdatesOptions $realTimeUpdatesOptions, ) { } @@ -61,12 +64,19 @@ abstract class AbstractNotifyVisitListener extends AbstractAsyncListener protected function determineUpdatesForVisit(Visit $visit): array { if ($visit->isOrphan()) { - return [$this->updatesGenerator->newOrphanVisitUpdate($visit)]; + return $this->realTimeUpdatesOptions->isTopicEnabled(Topic::NEW_ORPHAN_VISIT) + ? [$this->updatesGenerator->newOrphanVisitUpdate($visit)] + : []; } - return [ - $this->updatesGenerator->newShortUrlVisitUpdate($visit), - $this->updatesGenerator->newVisitUpdate($visit), - ]; + $topics = []; + if ($this->realTimeUpdatesOptions->isTopicEnabled(Topic::NEW_SHORT_URL_VISIT)) { + $topics[] = $this->updatesGenerator->newShortUrlVisitUpdate($visit); + } + if ($this->realTimeUpdatesOptions->isTopicEnabled(Topic::NEW_VISIT)) { + $topics[] = $this->updatesGenerator->newVisitUpdate($visit); + } + + return $topics; } } diff --git a/module/Core/src/EventDispatcher/RabbitMq/NotifyNewShortUrlToRabbitMq.php b/module/Core/src/EventDispatcher/RabbitMq/NotifyNewShortUrlToRabbitMq.php index b8c18fb6..ef63b1e4 100644 --- a/module/Core/src/EventDispatcher/RabbitMq/NotifyNewShortUrlToRabbitMq.php +++ b/module/Core/src/EventDispatcher/RabbitMq/NotifyNewShortUrlToRabbitMq.php @@ -8,6 +8,7 @@ use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface; use Shlinkio\Shlink\Core\Config\Options\RabbitMqOptions; +use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions; use Shlinkio\Shlink\Core\EventDispatcher\Async\AbstractNotifyNewShortUrlListener; use Shlinkio\Shlink\Core\EventDispatcher\Async\RemoteSystem; use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface; @@ -19,9 +20,10 @@ class NotifyNewShortUrlToRabbitMq extends AbstractNotifyNewShortUrlListener PublishingUpdatesGeneratorInterface $updatesGenerator, EntityManagerInterface $em, LoggerInterface $logger, + RealTimeUpdatesOptions $realTimeUpdatesOptions, private readonly RabbitMqOptions $options, ) { - parent::__construct($rabbitMqHelper, $updatesGenerator, $em, $logger); + parent::__construct($rabbitMqHelper, $updatesGenerator, $em, $logger, $realTimeUpdatesOptions); } protected function isEnabled(): bool diff --git a/module/Core/src/EventDispatcher/RabbitMq/NotifyVisitToRabbitMq.php b/module/Core/src/EventDispatcher/RabbitMq/NotifyVisitToRabbitMq.php index bb55f169..0bab0739 100644 --- a/module/Core/src/EventDispatcher/RabbitMq/NotifyVisitToRabbitMq.php +++ b/module/Core/src/EventDispatcher/RabbitMq/NotifyVisitToRabbitMq.php @@ -8,6 +8,7 @@ use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface; use Shlinkio\Shlink\Core\Config\Options\RabbitMqOptions; +use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions; use Shlinkio\Shlink\Core\EventDispatcher\Async\AbstractNotifyVisitListener; use Shlinkio\Shlink\Core\EventDispatcher\Async\RemoteSystem; use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface; @@ -19,9 +20,10 @@ class NotifyVisitToRabbitMq extends AbstractNotifyVisitListener PublishingUpdatesGeneratorInterface $updatesGenerator, EntityManagerInterface $em, LoggerInterface $logger, + RealTimeUpdatesOptions $realTimeUpdatesOptions, private readonly RabbitMqOptions $options, ) { - parent::__construct($rabbitMqHelper, $updatesGenerator, $em, $logger); + parent::__construct($rabbitMqHelper, $updatesGenerator, $em, $logger, $realTimeUpdatesOptions); } protected function isEnabled(): bool diff --git a/module/Core/src/EventDispatcher/RedisPubSub/NotifyNewShortUrlToRedis.php b/module/Core/src/EventDispatcher/RedisPubSub/NotifyNewShortUrlToRedis.php index 5cee9d5e..62b4e291 100644 --- a/module/Core/src/EventDispatcher/RedisPubSub/NotifyNewShortUrlToRedis.php +++ b/module/Core/src/EventDispatcher/RedisPubSub/NotifyNewShortUrlToRedis.php @@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core\EventDispatcher\RedisPubSub; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface; +use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions; use Shlinkio\Shlink\Core\EventDispatcher\Async\AbstractNotifyNewShortUrlListener; use Shlinkio\Shlink\Core\EventDispatcher\Async\RemoteSystem; use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface; @@ -18,9 +19,10 @@ class NotifyNewShortUrlToRedis extends AbstractNotifyNewShortUrlListener PublishingUpdatesGeneratorInterface $updatesGenerator, EntityManagerInterface $em, LoggerInterface $logger, + RealTimeUpdatesOptions $realTimeUpdatesOptions, private readonly bool $enabled, ) { - parent::__construct($redisHelper, $updatesGenerator, $em, $logger); + parent::__construct($redisHelper, $updatesGenerator, $em, $logger, $realTimeUpdatesOptions); } protected function isEnabled(): bool diff --git a/module/Core/src/EventDispatcher/RedisPubSub/NotifyVisitToRedis.php b/module/Core/src/EventDispatcher/RedisPubSub/NotifyVisitToRedis.php index ae349495..1d4dc5be 100644 --- a/module/Core/src/EventDispatcher/RedisPubSub/NotifyVisitToRedis.php +++ b/module/Core/src/EventDispatcher/RedisPubSub/NotifyVisitToRedis.php @@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core\EventDispatcher\RedisPubSub; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface; +use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions; use Shlinkio\Shlink\Core\EventDispatcher\Async\AbstractNotifyVisitListener; use Shlinkio\Shlink\Core\EventDispatcher\Async\RemoteSystem; use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface; @@ -18,9 +19,10 @@ class NotifyVisitToRedis extends AbstractNotifyVisitListener PublishingUpdatesGeneratorInterface $updatesGenerator, EntityManagerInterface $em, LoggerInterface $logger, + RealTimeUpdatesOptions $realTimeUpdatesOptions, private readonly bool $enabled, ) { - parent::__construct($redisHelper, $updatesGenerator, $em, $logger); + parent::__construct($redisHelper, $updatesGenerator, $em, $logger, $realTimeUpdatesOptions); } protected function isEnabled(): bool diff --git a/module/Core/src/EventDispatcher/Topic.php b/module/Core/src/EventDispatcher/Topic.php index 8c7a7d45..9f018bb3 100644 --- a/module/Core/src/EventDispatcher/Topic.php +++ b/module/Core/src/EventDispatcher/Topic.php @@ -4,16 +4,23 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\EventDispatcher; +use function Shlinkio\Shlink\Core\enumNames; use function sprintf; enum Topic: string { case NEW_VISIT = 'https://shlink.io/new-visit'; + case NEW_SHORT_URL_VISIT = 'https://shlink.io/new-visit/%s'; case NEW_ORPHAN_VISIT = 'https://shlink.io/new-orphan-visit'; case NEW_SHORT_URL = 'https://shlink.io/new-short-url'; public static function newShortUrlVisit(string|null $shortCode): string { - return sprintf('%s/%s', self::NEW_VISIT->value, $shortCode ?? ''); + return sprintf(self::NEW_SHORT_URL_VISIT->value, $shortCode ?? ''); + } + + public static function allTopicNames(): array + { + return enumNames(self::class); } } diff --git a/module/Core/test/Config/Options/RealTimeUpdatesOptionsTest.php b/module/Core/test/Config/Options/RealTimeUpdatesOptionsTest.php new file mode 100644 index 00000000..0e1634f1 --- /dev/null +++ b/module/Core/test/Config/Options/RealTimeUpdatesOptionsTest.php @@ -0,0 +1,41 @@ +enabledTopics); + } + + #[Test] + public function exceptionIsThrownIfAnyProvidedTopicIsInvalid(): void + { + $this->expectException(ValidationException::class); + new RealTimeUpdatesOptions(['NEW_SHORT_URL_VISIT', 'invalid']); + } + + #[Test] + public function checkingIfTopicIsEnabledWorks(): void + { + $options = new RealTimeUpdatesOptions(['NEW_ORPHAN_VISIT', 'NEW_SHORT_URL']); + + self::assertTrue($options->isTopicEnabled(Topic::NEW_ORPHAN_VISIT)); + self::assertTrue($options->isTopicEnabled(Topic::NEW_SHORT_URL)); + self::assertFalse($options->isTopicEnabled(Topic::NEW_VISIT)); + self::assertFalse($options->isTopicEnabled(Topic::NEW_SHORT_URL_VISIT)); + } +} diff --git a/module/Core/test/EventDispatcher/Mercure/NotifyNewShortUrlToMercureTest.php b/module/Core/test/EventDispatcher/Mercure/NotifyNewShortUrlToMercureTest.php index 20d6830d..3294e929 100644 --- a/module/Core/test/EventDispatcher/Mercure/NotifyNewShortUrlToMercureTest.php +++ b/module/Core/test/EventDispatcher/Mercure/NotifyNewShortUrlToMercureTest.php @@ -12,6 +12,7 @@ use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface; use Shlinkio\Shlink\Common\UpdatePublishing\Update; +use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions; use Shlinkio\Shlink\Core\EventDispatcher\Event\ShortUrlCreated; use Shlinkio\Shlink\Core\EventDispatcher\Mercure\NotifyNewShortUrlToMercure; use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface; @@ -19,7 +20,6 @@ use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; class NotifyNewShortUrlToMercureTest extends TestCase { - private NotifyNewShortUrlToMercure $listener; private MockObject & PublishingHelperInterface $helper; private MockObject & PublishingUpdatesGeneratorInterface $updatesGenerator; private MockObject & EntityManagerInterface $em; @@ -31,13 +31,6 @@ class NotifyNewShortUrlToMercureTest extends TestCase $this->updatesGenerator = $this->createMock(PublishingUpdatesGeneratorInterface::class); $this->em = $this->createMock(EntityManagerInterface::class); $this->logger = $this->createMock(LoggerInterface::class); - - $this->listener = new NotifyNewShortUrlToMercure( - $this->helper, - $this->updatesGenerator, - $this->em, - $this->logger, - ); } #[Test] @@ -52,7 +45,7 @@ class NotifyNewShortUrlToMercureTest extends TestCase ); $this->logger->expects($this->never())->method('debug'); - ($this->listener)(new ShortUrlCreated('123')); + $this->listener()(new ShortUrlCreated('123')); } #[Test] @@ -69,7 +62,7 @@ class NotifyNewShortUrlToMercureTest extends TestCase $this->logger->expects($this->never())->method('warning'); $this->logger->expects($this->never())->method('debug'); - ($this->listener)(new ShortUrlCreated('123')); + $this->listener()(new ShortUrlCreated('123')); } #[Test] @@ -93,6 +86,30 @@ class NotifyNewShortUrlToMercureTest extends TestCase ['e' => $e, 'name' => 'Mercure'], ); - ($this->listener)(new ShortUrlCreated('123')); + $this->listener()(new ShortUrlCreated('123')); + } + + #[Test] + public function publishingIsSkippedIfNewShortUrlTopicIsNotEnabled(): void + { + $shortUrl = ShortUrl::withLongUrl('https://longUrl'); + $update = Update::forTopicAndPayload('', []); + + $this->em->expects($this->once())->method('find')->with(ShortUrl::class, '123')->willReturn($shortUrl); + $this->updatesGenerator->expects($this->never())->method('newShortUrlUpdate'); + $this->helper->expects($this->never())->method('publishUpdate'); + + $this->listener(enableShortUrlTopic: false)(new ShortUrlCreated('123')); + } + + private function listener(bool $enableShortUrlTopic = true): NotifyNewShortUrlToMercure + { + return new NotifyNewShortUrlToMercure( + $this->helper, + $this->updatesGenerator, + $this->em, + $this->logger, + new RealTimeUpdatesOptions(enabledTopics: $enableShortUrlTopic ? null : []), + ); } } diff --git a/module/Core/test/EventDispatcher/Mercure/NotifyVisitToMercureTest.php b/module/Core/test/EventDispatcher/Mercure/NotifyVisitToMercureTest.php index 91569c9b..b86a2896 100644 --- a/module/Core/test/EventDispatcher/Mercure/NotifyVisitToMercureTest.php +++ b/module/Core/test/EventDispatcher/Mercure/NotifyVisitToMercureTest.php @@ -7,12 +7,14 @@ namespace ShlinkioTest\Shlink\Core\EventDispatcher\Mercure; use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use RuntimeException; use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface; use Shlinkio\Shlink\Common\UpdatePublishing\Update; +use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions; use Shlinkio\Shlink\Core\EventDispatcher\Event\UrlVisited; use Shlinkio\Shlink\Core\EventDispatcher\Mercure\NotifyVisitToMercure; use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface; @@ -23,7 +25,6 @@ use Shlinkio\Shlink\Core\Visit\Model\VisitType; class NotifyVisitToMercureTest extends TestCase { - private NotifyVisitToMercure $listener; private MockObject & PublishingHelperInterface $helper; private MockObject & PublishingUpdatesGeneratorInterface $updatesGenerator; private MockObject & EntityManagerInterface $em; @@ -35,8 +36,6 @@ class NotifyVisitToMercureTest extends TestCase $this->updatesGenerator = $this->createMock(PublishingUpdatesGeneratorInterface::class); $this->em = $this->createMock(EntityManagerInterface::class); $this->logger = $this->createMock(LoggerInterface::class); - - $this->listener = new NotifyVisitToMercure($this->helper, $this->updatesGenerator, $this->em, $this->logger); } #[Test] @@ -54,11 +53,15 @@ class NotifyVisitToMercureTest extends TestCase $this->updatesGenerator->expects($this->never())->method('newVisitUpdate'); $this->helper->expects($this->never())->method('publishUpdate'); - ($this->listener)(new UrlVisited($visitId)); + $this->listener()(new UrlVisited($visitId)); } #[Test] - public function notificationsAreSentWhenVisitIsFound(): void + #[TestWith([2, ['NEW_SHORT_URL_VISIT', 'NEW_VISIT']])] + #[TestWith([1, ['NEW_VISIT']])] + #[TestWith([1, ['NEW_SHORT_URL_VISIT']])] + #[TestWith([0, []])] + public function notificationsAreSentWhenVisitIsFound(int $publishUpdateCalls, array $enabledTopics): void { $visitId = '123'; $visit = Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::empty()); @@ -67,14 +70,12 @@ class NotifyVisitToMercureTest extends TestCase $this->em->expects($this->once())->method('find')->with(Visit::class, $visitId)->willReturn($visit); $this->logger->expects($this->never())->method('warning'); $this->logger->expects($this->never())->method('debug'); - $this->updatesGenerator->expects($this->once())->method('newShortUrlVisitUpdate')->with($visit)->willReturn( - $update, - ); + $this->updatesGenerator->method('newShortUrlVisitUpdate')->willReturn($update); $this->updatesGenerator->expects($this->never())->method('newOrphanVisitUpdate'); - $this->updatesGenerator->expects($this->once())->method('newVisitUpdate')->with($visit)->willReturn($update); - $this->helper->expects($this->exactly(2))->method('publishUpdate')->with($update); + $this->updatesGenerator->method('newVisitUpdate')->willReturn($update); + $this->helper->expects($this->exactly($publishUpdateCalls))->method('publishUpdate')->with($update); - ($this->listener)(new UrlVisited($visitId)); + $this->listener(enabledTopics: $enabledTopics)(new UrlVisited($visitId)); } #[Test] @@ -98,12 +99,15 @@ class NotifyVisitToMercureTest extends TestCase $this->updatesGenerator->expects($this->once())->method('newVisitUpdate')->with($visit)->willReturn($update); $this->helper->expects($this->once())->method('publishUpdate')->with($update)->willThrowException($e); - ($this->listener)(new UrlVisited($visitId)); + $this->listener()(new UrlVisited($visitId)); } #[Test, DataProvider('provideOrphanVisits')] - public function notificationsAreSentForOrphanVisits(Visit $visit): void - { + public function notificationsAreSentForOrphanVisits( + Visit $visit, + array $enabledTopics, + int $publishUpdateCalls, + ): void { $visitId = '123'; $update = Update::forTopicAndPayload('', []); @@ -111,21 +115,31 @@ class NotifyVisitToMercureTest extends TestCase $this->logger->expects($this->never())->method('warning'); $this->logger->expects($this->never())->method('debug'); $this->updatesGenerator->expects($this->never())->method('newShortUrlVisitUpdate'); - $this->updatesGenerator->expects($this->once())->method('newOrphanVisitUpdate')->with($visit)->willReturn( - $update, - ); + $this->updatesGenerator->method('newOrphanVisitUpdate')->willReturn($update); $this->updatesGenerator->expects($this->never())->method('newVisitUpdate'); - $this->helper->expects($this->once())->method('publishUpdate')->with($update); + $this->helper->expects($this->exactly($publishUpdateCalls))->method('publishUpdate')->with($update); - ($this->listener)(new UrlVisited($visitId)); + $this->listener(enabledTopics: $enabledTopics)(new UrlVisited($visitId)); } public static function provideOrphanVisits(): iterable { $visitor = Visitor::empty(); - yield VisitType::REGULAR_404->value => [Visit::forRegularNotFound($visitor)]; - yield VisitType::INVALID_SHORT_URL->value => [Visit::forInvalidShortUrl($visitor)]; - yield VisitType::BASE_URL->value => [Visit::forBasePath($visitor)]; + yield VisitType::REGULAR_404->value => [Visit::forRegularNotFound($visitor), ['NEW_ORPHAN_VISIT'], 1]; + yield VisitType::INVALID_SHORT_URL->value => [Visit::forInvalidShortUrl($visitor), ['NEW_ORPHAN_VISIT'], 1]; + yield VisitType::BASE_URL->value => [Visit::forBasePath($visitor), ['NEW_ORPHAN_VISIT'], 1]; + yield VisitType::BASE_URL->value . ' disabled' => [Visit::forBasePath($visitor), [], 0]; + } + + private function listener(array|null $enabledTopics = null): NotifyVisitToMercure + { + return new NotifyVisitToMercure( + $this->helper, + $this->updatesGenerator, + $this->em, + $this->logger, + new RealTimeUpdatesOptions(enabledTopics: $enabledTopics), + ); } } diff --git a/module/Core/test/EventDispatcher/RabbitMq/NotifyNewShortUrlToRabbitMqTest.php b/module/Core/test/EventDispatcher/RabbitMq/NotifyNewShortUrlToRabbitMqTest.php index b0c5a0e0..e4351c36 100644 --- a/module/Core/test/EventDispatcher/RabbitMq/NotifyNewShortUrlToRabbitMqTest.php +++ b/module/Core/test/EventDispatcher/RabbitMq/NotifyNewShortUrlToRabbitMqTest.php @@ -16,6 +16,7 @@ use RuntimeException; use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface; use Shlinkio\Shlink\Common\UpdatePublishing\Update; use Shlinkio\Shlink\Core\Config\Options\RabbitMqOptions; +use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions; use Shlinkio\Shlink\Core\EventDispatcher\Event\ShortUrlCreated; use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface; use Shlinkio\Shlink\Core\EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq; @@ -115,6 +116,7 @@ class NotifyNewShortUrlToRabbitMqTest extends TestCase $this->updatesGenerator, $this->em, $this->logger, + new RealTimeUpdatesOptions(), new RabbitMqOptions($enabled), ); } diff --git a/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php b/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php index 8af719b3..78abb5c7 100644 --- a/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php +++ b/module/Core/test/EventDispatcher/RabbitMq/NotifyVisitToRabbitMqTest.php @@ -17,6 +17,7 @@ use RuntimeException; use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface; use Shlinkio\Shlink\Common\UpdatePublishing\Update; use Shlinkio\Shlink\Core\Config\Options\RabbitMqOptions; +use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions; use Shlinkio\Shlink\Core\EventDispatcher\Event\UrlVisited; use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface; use Shlinkio\Shlink\Core\EventDispatcher\RabbitMq\NotifyVisitToRabbitMq; @@ -189,6 +190,7 @@ class NotifyVisitToRabbitMqTest extends TestCase $this->updatesGenerator, $this->em, $this->logger, + new RealTimeUpdatesOptions(), $options ?? new RabbitMqOptions(enabled: true), ); } diff --git a/module/Core/test/EventDispatcher/RedisPubSub/NotifyNewShortUrlToRedisTest.php b/module/Core/test/EventDispatcher/RedisPubSub/NotifyNewShortUrlToRedisTest.php index abbe23b9..412f603e 100644 --- a/module/Core/test/EventDispatcher/RedisPubSub/NotifyNewShortUrlToRedisTest.php +++ b/module/Core/test/EventDispatcher/RedisPubSub/NotifyNewShortUrlToRedisTest.php @@ -15,6 +15,7 @@ use Psr\Log\LoggerInterface; use RuntimeException; use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface; use Shlinkio\Shlink\Common\UpdatePublishing\Update; +use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions; use Shlinkio\Shlink\Core\EventDispatcher\Event\ShortUrlCreated; use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface; use Shlinkio\Shlink\Core\EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis; @@ -77,6 +78,13 @@ class NotifyNewShortUrlToRedisTest extends TestCase private function createListener(bool $enabled = true): NotifyNewShortUrlToRedis { - return new NotifyNewShortUrlToRedis($this->helper, $this->updatesGenerator, $this->em, $this->logger, $enabled); + return new NotifyNewShortUrlToRedis( + $this->helper, + $this->updatesGenerator, + $this->em, + $this->logger, + new RealTimeUpdatesOptions(), + $enabled, + ); } } diff --git a/module/Core/test/EventDispatcher/RedisPubSub/NotifyVisitToRedisTest.php b/module/Core/test/EventDispatcher/RedisPubSub/NotifyVisitToRedisTest.php index 7cbf68b6..d14ab631 100644 --- a/module/Core/test/EventDispatcher/RedisPubSub/NotifyVisitToRedisTest.php +++ b/module/Core/test/EventDispatcher/RedisPubSub/NotifyVisitToRedisTest.php @@ -15,6 +15,7 @@ use Psr\Log\LoggerInterface; use RuntimeException; use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface; use Shlinkio\Shlink\Common\UpdatePublishing\Update; +use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions; use Shlinkio\Shlink\Core\EventDispatcher\Event\UrlVisited; use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface; use Shlinkio\Shlink\Core\EventDispatcher\RedisPubSub\NotifyVisitToRedis; @@ -76,6 +77,13 @@ class NotifyVisitToRedisTest extends TestCase private function createListener(bool $enabled = true): NotifyVisitToRedis { - return new NotifyVisitToRedis($this->helper, $this->updatesGenerator, $this->em, $this->logger, $enabled); + return new NotifyVisitToRedis( + $this->helper, + $this->updatesGenerator, + $this->em, + $this->logger, + new RealTimeUpdatesOptions(), + $enabled, + ); } }