mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-26 02:40:41 -06:00
Merge pull request #1484 from acelaya-forks/feature/short-url-created-event
Feature/short url created event
This commit is contained in:
commit
71c8f99dab
@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
||||
|
||||
Now you can run `tag:visits`, `domain:visits`, `visit:orphan` or `visit:non-orphan` to get the corresponding list of visits from the command line.
|
||||
|
||||
* [#962](https://github.com/shlinkio/shlink/issues/962) Added new real-time update for new short URLs.
|
||||
|
||||
You can now subscribe to the `https://shlink.io/new-short-url` topic on any of the supported async updates technologies in order to get notified when a short URL is created.
|
||||
|
||||
### Changed
|
||||
* [#1452](https://github.com/shlinkio/shlink/issues/1452) Updated to monolog 3
|
||||
|
||||
|
@ -40,12 +40,11 @@
|
||||
"mlocati/ip-lib": "^1.17",
|
||||
"ocramius/proxy-manager": "^2.11",
|
||||
"pagerfanta/core": "^3.5",
|
||||
"php-amqplib/php-amqplib": "^3.1",
|
||||
"php-middleware/request-id": "^4.1",
|
||||
"predis/predis": "^1.1",
|
||||
"pugx/shortid-php": "^1.0",
|
||||
"ramsey/uuid": "^4.2",
|
||||
"shlinkio/shlink-common": "dev-main#3244088 as 4.5",
|
||||
"shlinkio/shlink-common": "dev-main#0396706 as 4.5",
|
||||
"shlinkio/shlink-config": "^1.6",
|
||||
"shlinkio/shlink-event-dispatcher": "^2.4",
|
||||
"shlinkio/shlink-importer": "^3.0",
|
||||
|
@ -2,9 +2,6 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Laminas\ServiceManager\Proxy\LazyServiceFactory;
|
||||
use PhpAmqpLib\Connection\AMQPStreamConnection;
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
return [
|
||||
@ -18,30 +15,4 @@ return [
|
||||
'vhost' => EnvVars::RABBITMQ_VHOST->loadFromEnv('/'),
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
AMQPStreamConnection::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
'delegators' => [
|
||||
AMQPStreamConnection::class => [
|
||||
LazyServiceFactory::class,
|
||||
],
|
||||
],
|
||||
'lazy_services' => [
|
||||
'class_map' => [
|
||||
AMQPStreamConnection::class => AMQPStreamConnection::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
AMQPStreamConnection::class => [
|
||||
'config.rabbitmq.host',
|
||||
'config.rabbitmq.port',
|
||||
'config.rabbitmq.user',
|
||||
'config.rabbitmq.password',
|
||||
'config.rabbitmq.vhost',
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"asyncapi": "2.0.0",
|
||||
"asyncapi": "2.4.0",
|
||||
"info": {
|
||||
"title": "Shlink",
|
||||
"version": "2.0.0",
|
||||
"version": "3.0.0",
|
||||
"description": "Shlink, the self-hosted URL shortener",
|
||||
"license": {
|
||||
"name": "MIT",
|
||||
@ -75,6 +75,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"https://shlink.io/new-short-url": {
|
||||
"subscribe": {
|
||||
"summary": "Receive information about any new short URL.",
|
||||
"operationId": "newshortUrl",
|
||||
"message": {
|
||||
"payload": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"shortUrl": {
|
||||
"$ref": "#/components/schemas/ShortUrl"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
|
@ -3,7 +3,7 @@
|
||||
"info": {
|
||||
"title": "Shlink",
|
||||
"description": "Shlink, the self-hosted URL shortener",
|
||||
"version": "1.0"
|
||||
"version": "2.0"
|
||||
},
|
||||
|
||||
"externalDocs": {
|
||||
|
@ -98,6 +98,7 @@ return [
|
||||
'em',
|
||||
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class,
|
||||
Service\ShortUrl\ShortCodeUniquenessHelper::class,
|
||||
EventDispatcherInterface::class,
|
||||
],
|
||||
Visit\VisitsTracker::class => [
|
||||
'em',
|
||||
|
@ -5,9 +5,9 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Core;
|
||||
|
||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use PhpAmqpLib\Connection\AMQPStreamConnection;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
||||
use Shlinkio\Shlink\Common\RabbitMq\RabbitMqPublishingHelper;
|
||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
|
||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||
use Symfony\Component\Mercure\Hub;
|
||||
@ -22,11 +22,15 @@ return [
|
||||
],
|
||||
'async' => [
|
||||
EventDispatcher\Event\VisitLocated::class => [
|
||||
EventDispatcher\NotifyVisitToMercure::class,
|
||||
EventDispatcher\NotifyVisitToRabbitMq::class,
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class,
|
||||
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class,
|
||||
EventDispatcher\NotifyVisitToWebHooks::class,
|
||||
EventDispatcher\UpdateGeoLiteDb::class,
|
||||
],
|
||||
EventDispatcher\Event\ShortUrlCreated::class => [
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class,
|
||||
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
@ -34,16 +38,24 @@ return [
|
||||
'factories' => [
|
||||
EventDispatcher\LocateVisit::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\NotifyVisitToMercure::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\NotifyVisitToRabbitMq::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\UpdateGeoLiteDb::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
|
||||
'delegators' => [
|
||||
EventDispatcher\NotifyVisitToMercure::class => [
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\NotifyVisitToRabbitMq::class => [
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => [
|
||||
@ -68,17 +80,31 @@ return [
|
||||
ShortUrl\Transformer\ShortUrlDataTransformer::class,
|
||||
Options\AppOptions::class,
|
||||
],
|
||||
EventDispatcher\NotifyVisitToMercure::class => [
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class => [
|
||||
Hub::class,
|
||||
Mercure\MercureUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
],
|
||||
EventDispatcher\NotifyVisitToRabbitMq::class => [
|
||||
AMQPStreamConnection::class,
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => [
|
||||
Hub::class,
|
||||
Mercure\MercureUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
],
|
||||
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class => [
|
||||
RabbitMqPublishingHelper::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
Visit\Transformer\OrphanVisitDataTransformer::class,
|
||||
ShortUrl\Transformer\ShortUrlDataTransformer::class,
|
||||
'config.rabbitmq.enabled',
|
||||
],
|
||||
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class => [
|
||||
RabbitMqPublishingHelper::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
ShortUrl\Transformer\ShortUrlDataTransformer::class,
|
||||
'config.rabbitmq.enabled',
|
||||
],
|
||||
EventDispatcher\UpdateGeoLiteDb::class => [GeolocationDbUpdater::class, 'Logger_Shlink'],
|
||||
|
21
module/Core/src/EventDispatcher/Event/ShortUrlCreated.php
Normal file
21
module/Core/src/EventDispatcher/Event/ShortUrlCreated.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\EventDispatcher\Event;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
final class ShortUrlCreated implements JsonSerializable
|
||||
{
|
||||
public function __construct(public readonly string $shortUrlId)
|
||||
{
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'shortUrlId' => $this->shortUrlId,
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\EventDispatcher\Mercure;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Event\ShortUrlCreated;
|
||||
use Shlinkio\Shlink\Core\Mercure\MercureUpdatesGeneratorInterface;
|
||||
use Symfony\Component\Mercure\HubInterface;
|
||||
use Throwable;
|
||||
|
||||
class NotifyNewShortUrlToMercure
|
||||
{
|
||||
public function __construct(
|
||||
private readonly HubInterface $hub,
|
||||
private readonly MercureUpdatesGeneratorInterface $updatesGenerator,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(ShortUrlCreated $shortUrlCreated): void
|
||||
{
|
||||
$shortUrlId = $shortUrlCreated->shortUrlId;
|
||||
$shortUrl = $this->em->find(ShortUrl::class, $shortUrlId);
|
||||
|
||||
if ($shortUrl === null) {
|
||||
$this->logger->warning(
|
||||
'Tried to notify Mercure for new short URL with id "{shortUrlId}", but it does not exist.',
|
||||
['shortUrlId' => $shortUrlId],
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->hub->publish($this->updatesGenerator->newShortUrlUpdate($shortUrl));
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->debug('Error while trying to notify mercure hub with new short URL. {e}', ['e' => $e]);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\EventDispatcher;
|
||||
namespace Shlinkio\Shlink\Core\EventDispatcher\Mercure;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
@ -18,10 +18,10 @@ use function Functional\each;
|
||||
class NotifyVisitToMercure
|
||||
{
|
||||
public function __construct(
|
||||
private HubInterface $hub,
|
||||
private MercureUpdatesGeneratorInterface $updatesGenerator,
|
||||
private EntityManagerInterface $em,
|
||||
private LoggerInterface $logger,
|
||||
private readonly HubInterface $hub,
|
||||
private readonly MercureUpdatesGeneratorInterface $updatesGenerator,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
@ -1,102 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\EventDispatcher;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PhpAmqpLib\Connection\AMQPStreamConnection;
|
||||
use PhpAmqpLib\Exchange\AMQPExchangeType;
|
||||
use PhpAmqpLib\Message\AMQPMessage;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Event\VisitLocated;
|
||||
use Throwable;
|
||||
|
||||
use function Shlinkio\Shlink\Common\json_encode;
|
||||
use function sprintf;
|
||||
|
||||
class NotifyVisitToRabbitMq
|
||||
{
|
||||
private const NEW_VISIT_QUEUE = 'https://shlink.io/new-visit';
|
||||
private const NEW_ORPHAN_VISIT_QUEUE = 'https://shlink.io/new-orphan-visit';
|
||||
|
||||
public function __construct(
|
||||
private AMQPStreamConnection $connection,
|
||||
private EntityManagerInterface $em,
|
||||
private LoggerInterface $logger,
|
||||
private DataTransformerInterface $orphanVisitTransformer,
|
||||
private bool $isEnabled,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(VisitLocated $shortUrlLocated): void
|
||||
{
|
||||
if (! $this->isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
$visitId = $shortUrlLocated->visitId;
|
||||
$visit = $this->em->find(Visit::class, $visitId);
|
||||
|
||||
if ($visit === null) {
|
||||
$this->logger->warning('Tried to notify RabbitMQ for visit with id "{visitId}", but it does not exist.', [
|
||||
'visitId' => $visitId,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->connection->isConnected()) {
|
||||
$this->connection->reconnect();
|
||||
}
|
||||
|
||||
$queues = $this->determineQueuesToPublishTo($visit);
|
||||
$message = $this->visitToMessage($visit);
|
||||
|
||||
try {
|
||||
$channel = $this->connection->channel();
|
||||
|
||||
foreach ($queues as $queue) {
|
||||
// Declare an exchange and a queue that will persist server restarts
|
||||
$exchange = $queue; // We use the same name for the exchange and the queue
|
||||
$channel->exchange_declare($exchange, AMQPExchangeType::DIRECT, false, true, false);
|
||||
$channel->queue_declare($queue, false, true, false, false);
|
||||
|
||||
// Bind the exchange and the queue together, and publish the message
|
||||
$channel->queue_bind($queue, $exchange);
|
||||
$channel->basic_publish($message, $exchange);
|
||||
}
|
||||
|
||||
$channel->close();
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->debug('Error while trying to notify RabbitMQ with new visit. {e}', ['e' => $e]);
|
||||
} finally {
|
||||
$this->connection->close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function determineQueuesToPublishTo(Visit $visit): array
|
||||
{
|
||||
if ($visit->isOrphan()) {
|
||||
return [self::NEW_ORPHAN_VISIT_QUEUE];
|
||||
}
|
||||
|
||||
return [
|
||||
self::NEW_VISIT_QUEUE,
|
||||
sprintf('%s/%s', self::NEW_VISIT_QUEUE, $visit->getShortUrl()?->getShortCode()),
|
||||
];
|
||||
}
|
||||
|
||||
private function visitToMessage(Visit $visit): AMQPMessage
|
||||
{
|
||||
$messageBody = json_encode(! $visit->isOrphan() ? $visit : $this->orphanVisitTransformer->transform($visit));
|
||||
return new AMQPMessage($messageBody, [
|
||||
'content_type' => 'application/json',
|
||||
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
|
||||
]);
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\EventDispatcher\RabbitMq;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Common\RabbitMq\RabbitMqPublishingHelperInterface;
|
||||
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Event\ShortUrlCreated;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Topic;
|
||||
use Throwable;
|
||||
|
||||
class NotifyNewShortUrlToRabbitMq
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RabbitMqPublishingHelperInterface $rabbitMqHelper,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly DataTransformerInterface $shortUrlTransformer,
|
||||
private readonly bool $isEnabled,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(ShortUrlCreated $shortUrlCreated): void
|
||||
{
|
||||
if (! $this->isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
$shortUrlId = $shortUrlCreated->shortUrlId;
|
||||
$shortUrl = $this->em->find(ShortUrl::class, $shortUrlId);
|
||||
|
||||
if ($shortUrl === null) {
|
||||
$this->logger->warning(
|
||||
'Tried to notify RabbitMQ for new short URL with id "{shortUrlId}", but it does not exist.',
|
||||
['shortUrlId' => $shortUrlId],
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->rabbitMqHelper->publishPayloadInQueue(
|
||||
['shortUrl' => $this->shortUrlTransformer->transform($shortUrl)],
|
||||
Topic::NEW_SHORT_URL->value,
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->debug('Error while trying to notify RabbitMQ with new short URL. {e}', ['e' => $e]);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\EventDispatcher\RabbitMq;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Common\RabbitMq\RabbitMqPublishingHelperInterface;
|
||||
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Event\VisitLocated;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Topic;
|
||||
use Throwable;
|
||||
|
||||
class NotifyVisitToRabbitMq
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RabbitMqPublishingHelperInterface $rabbitMqHelper,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly DataTransformerInterface $orphanVisitTransformer,
|
||||
private readonly DataTransformerInterface $shortUrlTransformer, // @phpstan-ignore-line
|
||||
private readonly bool $isEnabled,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(VisitLocated $shortUrlLocated): void
|
||||
{
|
||||
if (! $this->isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
$visitId = $shortUrlLocated->visitId;
|
||||
$visit = $this->em->find(Visit::class, $visitId);
|
||||
|
||||
if ($visit === null) {
|
||||
$this->logger->warning('Tried to notify RabbitMQ for visit with id "{visitId}", but it does not exist.', [
|
||||
'visitId' => $visitId,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$queues = $this->determineQueuesToPublishTo($visit);
|
||||
$payload = $this->visitToPayload($visit);
|
||||
|
||||
try {
|
||||
foreach ($queues as $queue) {
|
||||
$this->rabbitMqHelper->publishPayloadInQueue($payload, $queue);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->debug('Error while trying to notify RabbitMQ with new visit. {e}', ['e' => $e]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function determineQueuesToPublishTo(Visit $visit): array
|
||||
{
|
||||
if ($visit->isOrphan()) {
|
||||
return [Topic::NEW_ORPHAN_VISIT->value];
|
||||
}
|
||||
|
||||
return [
|
||||
Topic::NEW_VISIT->value,
|
||||
Topic::newShortUrlVisit($visit->getShortUrl()?->getShortCode()),
|
||||
];
|
||||
}
|
||||
|
||||
private function visitToPayload(Visit $visit): array
|
||||
{
|
||||
// FIXME This was defined incorrectly.
|
||||
// According to the spec, both the visit and the short URL it belongs to, should be published.
|
||||
// The shape should be ['visit' => [...], 'shortUrl' => ?[...]]
|
||||
// However, this would be a breaking change, so we need a flag that determines the shape of the payload.
|
||||
|
||||
return ! $visit->isOrphan() ? $visit->jsonSerialize() : $this->orphanVisitTransformer->transform($visit);
|
||||
|
||||
if ($visit->isOrphan()) { // @phpstan-ignore-line
|
||||
return ['visit' => $this->orphanVisitTransformer->transform($visit)];
|
||||
}
|
||||
|
||||
return [
|
||||
'visit' => $visit->jsonSerialize(),
|
||||
'shortUrl' => $this->shortUrlTransformer->transform($visit->getShortUrl()),
|
||||
];
|
||||
}
|
||||
}
|
19
module/Core/src/EventDispatcher/Topic.php
Normal file
19
module/Core/src/EventDispatcher/Topic.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\EventDispatcher;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
enum Topic: string
|
||||
{
|
||||
case NEW_VISIT = 'https://shlink.io/new-visit';
|
||||
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 $shortCode): string
|
||||
{
|
||||
return sprintf('%s/%s', self::NEW_VISIT->value, $shortCode ?? '');
|
||||
}
|
||||
}
|
@ -5,26 +5,24 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Core\Mercure;
|
||||
|
||||
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Topic;
|
||||
use Symfony\Component\Mercure\Update;
|
||||
|
||||
use function Shlinkio\Shlink\Common\json_encode;
|
||||
use function sprintf;
|
||||
|
||||
final class MercureUpdatesGenerator implements MercureUpdatesGeneratorInterface
|
||||
{
|
||||
private const NEW_VISIT_TOPIC = 'https://shlink.io/new-visit';
|
||||
private const NEW_ORPHAN_VISIT_TOPIC = 'https://shlink.io/new-orphan-visit';
|
||||
|
||||
public function __construct(
|
||||
private DataTransformerInterface $shortUrlTransformer,
|
||||
private DataTransformerInterface $orphanVisitTransformer,
|
||||
private readonly DataTransformerInterface $shortUrlTransformer,
|
||||
private readonly DataTransformerInterface $orphanVisitTransformer,
|
||||
) {
|
||||
}
|
||||
|
||||
public function newVisitUpdate(Visit $visit): Update
|
||||
{
|
||||
return new Update(self::NEW_VISIT_TOPIC, json_encode([
|
||||
return new Update(Topic::NEW_VISIT->value, json_encode([
|
||||
'shortUrl' => $this->shortUrlTransformer->transform($visit->getShortUrl()),
|
||||
'visit' => $visit,
|
||||
]));
|
||||
@ -32,7 +30,7 @@ final class MercureUpdatesGenerator implements MercureUpdatesGeneratorInterface
|
||||
|
||||
public function newOrphanVisitUpdate(Visit $visit): Update
|
||||
{
|
||||
return new Update(self::NEW_ORPHAN_VISIT_TOPIC, json_encode([
|
||||
return new Update(Topic::NEW_ORPHAN_VISIT->value, json_encode([
|
||||
'visit' => $this->orphanVisitTransformer->transform($visit),
|
||||
]));
|
||||
}
|
||||
@ -40,11 +38,18 @@ final class MercureUpdatesGenerator implements MercureUpdatesGeneratorInterface
|
||||
public function newShortUrlVisitUpdate(Visit $visit): Update
|
||||
{
|
||||
$shortUrl = $visit->getShortUrl();
|
||||
$topic = sprintf('%s/%s', self::NEW_VISIT_TOPIC, $shortUrl?->getShortCode());
|
||||
$topic = Topic::newShortUrlVisit($shortUrl?->getShortCode());
|
||||
|
||||
return new Update($topic, json_encode([
|
||||
'shortUrl' => $this->shortUrlTransformer->transform($shortUrl),
|
||||
'visit' => $visit,
|
||||
]));
|
||||
}
|
||||
|
||||
public function newShortUrlUpdate(ShortUrl $shortUrl): Update
|
||||
{
|
||||
return new Update(Topic::NEW_SHORT_URL->value, json_encode([
|
||||
'shortUrl' => $this->shortUrlTransformer->transform($shortUrl),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Mercure;
|
||||
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Symfony\Component\Mercure\Update;
|
||||
|
||||
@ -14,4 +15,6 @@ interface MercureUpdatesGeneratorInterface
|
||||
public function newOrphanVisitUpdate(Visit $visit): Update;
|
||||
|
||||
public function newShortUrlVisitUpdate(Visit $visit): Update;
|
||||
|
||||
public function newShortUrlUpdate(ShortUrl $shortUrl): Update;
|
||||
}
|
||||
|
@ -5,7 +5,9 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Core\Service;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Event\ShortUrlCreated;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
@ -17,10 +19,11 @@ use Shlinkio\Shlink\Core\ShortUrl\Resolver\ShortUrlRelationResolverInterface;
|
||||
class UrlShortener implements UrlShortenerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ShortUrlTitleResolutionHelperInterface $titleResolutionHelper,
|
||||
private EntityManagerInterface $em,
|
||||
private ShortUrlRelationResolverInterface $relationResolver,
|
||||
private ShortCodeUniquenessHelperInterface $shortCodeHelper,
|
||||
private readonly ShortUrlTitleResolutionHelperInterface $titleResolutionHelper,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly ShortUrlRelationResolverInterface $relationResolver,
|
||||
private readonly ShortCodeUniquenessHelperInterface $shortCodeHelper,
|
||||
private readonly EventDispatcherInterface $eventDispatcher,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -39,7 +42,8 @@ class UrlShortener implements UrlShortenerInterface
|
||||
/** @var ShortUrlMeta $meta */
|
||||
$meta = $this->titleResolutionHelper->processTitleAndValidateUrl($meta);
|
||||
|
||||
return $this->em->transactional(function () use ($meta) {
|
||||
/** @var ShortUrl $newShortUrl */
|
||||
$newShortUrl = $this->em->wrapInTransaction(function () use ($meta) {
|
||||
$shortUrl = ShortUrl::fromMeta($meta, $this->relationResolver);
|
||||
|
||||
$this->verifyShortCodeUniqueness($meta, $shortUrl);
|
||||
@ -47,6 +51,10 @@ class UrlShortener implements UrlShortenerInterface
|
||||
|
||||
return $shortUrl;
|
||||
});
|
||||
|
||||
$this->eventDispatcher->dispatch(new ShortUrlCreated($newShortUrl->getId()));
|
||||
|
||||
return $newShortUrl;
|
||||
}
|
||||
|
||||
private function findExistingShortUrlIfExists(ShortUrlMeta $meta): ?ShortUrl
|
||||
|
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\EventDispatcher\Mercure;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Exception;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Event\ShortUrlCreated;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Mercure\NotifyNewShortUrlToMercure;
|
||||
use Shlinkio\Shlink\Core\Mercure\MercureUpdatesGeneratorInterface;
|
||||
use Symfony\Component\Mercure\HubInterface;
|
||||
use Symfony\Component\Mercure\Update;
|
||||
|
||||
class NotifyNewShortUrlToMercureTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private NotifyNewShortUrlToMercure $listener;
|
||||
private ObjectProphecy $hub;
|
||||
private ObjectProphecy $updatesGenerator;
|
||||
private ObjectProphecy $em;
|
||||
private ObjectProphecy $logger;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->hub = $this->prophesize(HubInterface::class);
|
||||
$this->updatesGenerator = $this->prophesize(MercureUpdatesGeneratorInterface::class);
|
||||
$this->em = $this->prophesize(EntityManagerInterface::class);
|
||||
$this->logger = $this->prophesize(LoggerInterface::class);
|
||||
|
||||
$this->listener = new NotifyNewShortUrlToMercure(
|
||||
$this->hub->reveal(),
|
||||
$this->updatesGenerator->reveal(),
|
||||
$this->em->reveal(),
|
||||
$this->logger->reveal(),
|
||||
);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function messageIsLoggedWhenShortUrlIsNotFound(): void
|
||||
{
|
||||
$find = $this->em->find(ShortUrl::class, '123')->willReturn(null);
|
||||
|
||||
($this->listener)(new ShortUrlCreated('123'));
|
||||
|
||||
$find->shouldHaveBeenCalledOnce();
|
||||
$this->logger->warning(
|
||||
'Tried to notify Mercure for new short URL with id "{shortUrlId}", but it does not exist.',
|
||||
['shortUrlId' => '123'],
|
||||
)->shouldHaveBeenCalledOnce();
|
||||
$this->hub->publish(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$this->updatesGenerator->newShortUrlUpdate(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$this->logger->debug(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function expectedNotificationIsPublished(): void
|
||||
{
|
||||
$shortUrl = ShortUrl::withLongUrl('');
|
||||
$update = new Update([]);
|
||||
|
||||
$find = $this->em->find(ShortUrl::class, '123')->willReturn($shortUrl);
|
||||
$newUpdate = $this->updatesGenerator->newShortUrlUpdate($shortUrl)->willReturn($update);
|
||||
$publish = $this->hub->publish($update)->willReturn('');
|
||||
|
||||
($this->listener)(new ShortUrlCreated('123'));
|
||||
|
||||
$find->shouldHaveBeenCalledOnce();
|
||||
$newUpdate->shouldHaveBeenCalledOnce();
|
||||
$publish->shouldHaveBeenCalledOnce();
|
||||
$this->logger->warning(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$this->logger->debug(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function messageIsPrintedIfPublishingFails(): void
|
||||
{
|
||||
$shortUrl = ShortUrl::withLongUrl('');
|
||||
$update = new Update([]);
|
||||
$e = new Exception('Error');
|
||||
|
||||
$find = $this->em->find(ShortUrl::class, '123')->willReturn($shortUrl);
|
||||
$newUpdate = $this->updatesGenerator->newShortUrlUpdate($shortUrl)->willReturn($update);
|
||||
$publish = $this->hub->publish($update)->willThrow($e);
|
||||
|
||||
($this->listener)(new ShortUrlCreated('123'));
|
||||
|
||||
$find->shouldHaveBeenCalledOnce();
|
||||
$newUpdate->shouldHaveBeenCalledOnce();
|
||||
$publish->shouldHaveBeenCalledOnce();
|
||||
$this->logger->warning(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$this->logger->debug(
|
||||
'Error while trying to notify mercure hub with new short URL. {e}',
|
||||
['e' => $e],
|
||||
)->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\EventDispatcher;
|
||||
namespace ShlinkioTest\Shlink\Core\EventDispatcher\Mercure;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@ -14,7 +14,7 @@ use RuntimeException;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Event\VisitLocated;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\NotifyVisitToMercure;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Mercure\NotifyVisitToMercure;
|
||||
use Shlinkio\Shlink\Core\Mercure\MercureUpdatesGeneratorInterface;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
|
@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\EventDispatcher\RabbitMq;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use DomainException;
|
||||
use Exception;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
use Shlinkio\Shlink\Common\RabbitMq\RabbitMqPublishingHelperInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Event\ShortUrlCreated;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Topic;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer;
|
||||
use Throwable;
|
||||
|
||||
class NotifyNewShortUrlToRabbitMqTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private NotifyNewShortUrlToRabbitMq $listener;
|
||||
private ObjectProphecy $helper;
|
||||
private ObjectProphecy $em;
|
||||
private ObjectProphecy $logger;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->helper = $this->prophesize(RabbitMqPublishingHelperInterface::class);
|
||||
$this->em = $this->prophesize(EntityManagerInterface::class);
|
||||
$this->logger = $this->prophesize(LoggerInterface::class);
|
||||
|
||||
$this->listener = new NotifyNewShortUrlToRabbitMq(
|
||||
$this->helper->reveal(),
|
||||
$this->em->reveal(),
|
||||
$this->logger->reveal(),
|
||||
new ShortUrlDataTransformer(new ShortUrlStringifier([])),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function doesNothingWhenTheFeatureIsNotEnabled(): void
|
||||
{
|
||||
$listener = new NotifyNewShortUrlToRabbitMq(
|
||||
$this->helper->reveal(),
|
||||
$this->em->reveal(),
|
||||
$this->logger->reveal(),
|
||||
new ShortUrlDataTransformer(new ShortUrlStringifier([])),
|
||||
false,
|
||||
);
|
||||
|
||||
$listener(new ShortUrlCreated('123'));
|
||||
|
||||
$this->em->find(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$this->logger->warning(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$this->logger->debug(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$this->helper->publishPayloadInQueue(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function notificationsAreNotSentWhenShortUrlCannotBeFound(): void
|
||||
{
|
||||
$shortUrlId = '123';
|
||||
$find = $this->em->find(ShortUrl::class, $shortUrlId)->willReturn(null);
|
||||
$logWarning = $this->logger->warning(
|
||||
'Tried to notify RabbitMQ for new short URL with id "{shortUrlId}", but it does not exist.',
|
||||
['shortUrlId' => $shortUrlId],
|
||||
);
|
||||
|
||||
($this->listener)(new ShortUrlCreated($shortUrlId));
|
||||
|
||||
$find->shouldHaveBeenCalledOnce();
|
||||
$logWarning->shouldHaveBeenCalledOnce();
|
||||
$this->logger->debug(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$this->helper->publishPayloadInQueue(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function expectedChannelIsNotified(): void
|
||||
{
|
||||
$shortUrlId = '123';
|
||||
$find = $this->em->find(ShortUrl::class, $shortUrlId)->willReturn(ShortUrl::withLongUrl(''));
|
||||
|
||||
($this->listener)(new ShortUrlCreated($shortUrlId));
|
||||
|
||||
$find->shouldHaveBeenCalledOnce();
|
||||
$this->helper->publishPayloadInQueue(
|
||||
Argument::type('array'),
|
||||
Topic::NEW_SHORT_URL->value,
|
||||
)->shouldHaveBeenCalledOnce();
|
||||
$this->logger->debug(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideExceptions
|
||||
*/
|
||||
public function printsDebugMessageInCaseOfError(Throwable $e): void
|
||||
{
|
||||
$shortUrlId = '123';
|
||||
$find = $this->em->find(ShortUrl::class, $shortUrlId)->willReturn(ShortUrl::withLongUrl(''));
|
||||
$publish = $this->helper->publishPayloadInQueue(Argument::cetera())->willThrow($e);
|
||||
|
||||
($this->listener)(new ShortUrlCreated($shortUrlId));
|
||||
|
||||
$this->logger->debug(
|
||||
'Error while trying to notify RabbitMQ with new short URL. {e}',
|
||||
['e' => $e],
|
||||
)->shouldHaveBeenCalledOnce();
|
||||
$find->shouldHaveBeenCalledOnce();
|
||||
$publish->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideExceptions(): iterable
|
||||
{
|
||||
yield [new RuntimeException('RuntimeException Error')];
|
||||
yield [new Exception('Exception Error')];
|
||||
yield [new DomainException('DomainException Error')];
|
||||
}
|
||||
}
|
@ -2,25 +2,26 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\EventDispatcher;
|
||||
namespace ShlinkioTest\Shlink\Core\EventDispatcher\RabbitMq;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use DomainException;
|
||||
use Exception;
|
||||
use PhpAmqpLib\Channel\AMQPChannel;
|
||||
use PhpAmqpLib\Connection\AMQPStreamConnection;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
use Shlinkio\Shlink\Common\RabbitMq\RabbitMqPublishingHelperInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Event\VisitLocated;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\NotifyVisitToRabbitMq;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\RabbitMq\NotifyVisitToRabbitMq;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer;
|
||||
use Shlinkio\Shlink\Core\Visit\Transformer\OrphanVisitDataTransformer;
|
||||
use Throwable;
|
||||
|
||||
@ -32,28 +33,22 @@ class NotifyVisitToRabbitMqTest extends TestCase
|
||||
use ProphecyTrait;
|
||||
|
||||
private NotifyVisitToRabbitMq $listener;
|
||||
private ObjectProphecy $connection;
|
||||
private ObjectProphecy $helper;
|
||||
private ObjectProphecy $em;
|
||||
private ObjectProphecy $logger;
|
||||
private ObjectProphecy $orphanVisitTransformer;
|
||||
private ObjectProphecy $channel;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->channel = $this->prophesize(AMQPChannel::class);
|
||||
|
||||
$this->connection = $this->prophesize(AMQPStreamConnection::class);
|
||||
$this->connection->isConnected()->willReturn(false);
|
||||
$this->connection->channel()->willReturn($this->channel->reveal());
|
||||
|
||||
$this->helper = $this->prophesize(RabbitMqPublishingHelperInterface::class);
|
||||
$this->em = $this->prophesize(EntityManagerInterface::class);
|
||||
$this->logger = $this->prophesize(LoggerInterface::class);
|
||||
|
||||
$this->listener = new NotifyVisitToRabbitMq(
|
||||
$this->connection->reveal(),
|
||||
$this->helper->reveal(),
|
||||
$this->em->reveal(),
|
||||
$this->logger->reveal(),
|
||||
new OrphanVisitDataTransformer(),
|
||||
new ShortUrlDataTransformer(new ShortUrlStringifier([])),
|
||||
true,
|
||||
);
|
||||
}
|
||||
@ -62,10 +57,11 @@ class NotifyVisitToRabbitMqTest extends TestCase
|
||||
public function doesNothingWhenTheFeatureIsNotEnabled(): void
|
||||
{
|
||||
$listener = new NotifyVisitToRabbitMq(
|
||||
$this->connection->reveal(),
|
||||
$this->helper->reveal(),
|
||||
$this->em->reveal(),
|
||||
$this->logger->reveal(),
|
||||
new OrphanVisitDataTransformer(),
|
||||
new ShortUrlDataTransformer(new ShortUrlStringifier([])),
|
||||
false,
|
||||
);
|
||||
|
||||
@ -74,8 +70,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
|
||||
$this->em->find(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$this->logger->warning(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$this->logger->debug(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$this->connection->isConnected()->shouldNotHaveBeenCalled();
|
||||
$this->connection->close()->shouldNotHaveBeenCalled();
|
||||
$this->helper->publishPayloadInQueue(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
@ -93,8 +88,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
|
||||
$findVisit->shouldHaveBeenCalledOnce();
|
||||
$logWarning->shouldHaveBeenCalledOnce();
|
||||
$this->logger->debug(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$this->connection->isConnected()->shouldNotHaveBeenCalled();
|
||||
$this->connection->close()->shouldNotHaveBeenCalled();
|
||||
$this->helper->publishPayloadInQueue(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -105,27 +99,17 @@ class NotifyVisitToRabbitMqTest extends TestCase
|
||||
{
|
||||
$visitId = '123';
|
||||
$findVisit = $this->em->find(Visit::class, $visitId)->willReturn($visit);
|
||||
$argumentWithExpectedChannel = Argument::that(fn (string $channel) => contains($expectedChannels, $channel));
|
||||
$argumentWithExpectedChannels = Argument::that(
|
||||
static fn (string $channel) => contains($expectedChannels, $channel),
|
||||
);
|
||||
|
||||
($this->listener)(new VisitLocated($visitId));
|
||||
|
||||
$findVisit->shouldHaveBeenCalledOnce();
|
||||
$this->channel->exchange_declare($argumentWithExpectedChannel, Argument::cetera())->shouldHaveBeenCalledTimes(
|
||||
count($expectedChannels),
|
||||
);
|
||||
$this->channel->queue_declare($argumentWithExpectedChannel, Argument::cetera())->shouldHaveBeenCalledTimes(
|
||||
count($expectedChannels),
|
||||
);
|
||||
$this->channel->queue_bind(
|
||||
$argumentWithExpectedChannel,
|
||||
$argumentWithExpectedChannel,
|
||||
$this->helper->publishPayloadInQueue(
|
||||
Argument::type('array'),
|
||||
$argumentWithExpectedChannels,
|
||||
)->shouldHaveBeenCalledTimes(count($expectedChannels));
|
||||
$this->channel->basic_publish(Argument::any(), $argumentWithExpectedChannel)->shouldHaveBeenCalledTimes(
|
||||
count($expectedChannels),
|
||||
);
|
||||
$this->channel->close()->shouldHaveBeenCalledOnce();
|
||||
$this->connection->reconnect()->shouldHaveBeenCalledOnce();
|
||||
$this->connection->close()->shouldHaveBeenCalledOnce();
|
||||
$this->logger->debug(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
@ -154,7 +138,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
|
||||
{
|
||||
$visitId = '123';
|
||||
$findVisit = $this->em->find(Visit::class, $visitId)->willReturn(Visit::forBasePath(Visitor::emptyInstance()));
|
||||
$channel = $this->connection->channel()->willThrow($e);
|
||||
$publish = $this->helper->publishPayloadInQueue(Argument::cetera())->willThrow($e);
|
||||
|
||||
($this->listener)(new VisitLocated($visitId));
|
||||
|
||||
@ -162,11 +146,8 @@ class NotifyVisitToRabbitMqTest extends TestCase
|
||||
'Error while trying to notify RabbitMQ with new visit. {e}',
|
||||
['e' => $e],
|
||||
)->shouldHaveBeenCalledOnce();
|
||||
$this->connection->close()->shouldHaveBeenCalledOnce();
|
||||
$this->connection->reconnect()->shouldHaveBeenCalledOnce();
|
||||
$findVisit->shouldHaveBeenCalledOnce();
|
||||
$channel->shouldHaveBeenCalledOnce();
|
||||
$this->channel->close()->shouldNotHaveBeenCalled();
|
||||
$publish->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideExceptions(): iterable
|
@ -7,6 +7,7 @@ namespace ShlinkioTest\Shlink\Core\Mercure;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Topic;
|
||||
use Shlinkio\Shlink\Core\Mercure\MercureUpdatesGenerator;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
@ -109,4 +110,35 @@ class MercureUpdatesGeneratorTest extends TestCase
|
||||
yield VisitType::INVALID_SHORT_URL->value => [Visit::forInvalidShortUrl($visitor)];
|
||||
yield VisitType::BASE_URL->value => [Visit::forBasePath($visitor)];
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function shortUrlIsProperlySerializedIntoUpdate(): void
|
||||
{
|
||||
$shortUrl = ShortUrl::fromMeta(ShortUrlMeta::fromRawData([
|
||||
'customSlug' => 'foo',
|
||||
'longUrl' => '',
|
||||
'title' => 'The title',
|
||||
]));
|
||||
|
||||
$update = $this->generator->newShortUrlUpdate($shortUrl);
|
||||
|
||||
self::assertEquals([Topic::NEW_SHORT_URL->value], $update->getTopics());
|
||||
self::assertEquals(['shortUrl' => [
|
||||
'shortCode' => $shortUrl->getShortCode(),
|
||||
'shortUrl' => 'http:/' . $shortUrl->getShortCode(),
|
||||
'longUrl' => '',
|
||||
'dateCreated' => $shortUrl->getDateCreated()->toAtomString(),
|
||||
'visitsCount' => 0,
|
||||
'tags' => [],
|
||||
'meta' => [
|
||||
'validSince' => null,
|
||||
'validUntil' => null,
|
||||
'maxVisits' => null,
|
||||
],
|
||||
'domain' => null,
|
||||
'title' => $shortUrl->title(),
|
||||
'crawlable' => false,
|
||||
'forwardQuery' => true,
|
||||
],], json_decode($update->getData()));
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
@ -27,6 +28,7 @@ class UrlShortenerTest extends TestCase
|
||||
private ObjectProphecy $em;
|
||||
private ObjectProphecy $titleResolutionHelper;
|
||||
private ObjectProphecy $shortCodeHelper;
|
||||
private ObjectProphecy $eventDispatcher;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
@ -39,7 +41,7 @@ class UrlShortenerTest extends TestCase
|
||||
[$shortUrl] = $arguments;
|
||||
$shortUrl->setId('10');
|
||||
});
|
||||
$this->em->transactional(Argument::type('callable'))->will(function (array $args) {
|
||||
$this->em->wrapInTransaction(Argument::type('callable'))->will(function (array $args) {
|
||||
/** @var callable $callback */
|
||||
[$callback] = $args;
|
||||
|
||||
@ -51,11 +53,14 @@ class UrlShortenerTest extends TestCase
|
||||
$this->shortCodeHelper = $this->prophesize(ShortCodeUniquenessHelperInterface::class);
|
||||
$this->shortCodeHelper->ensureShortCodeUniqueness(Argument::cetera())->willReturn(true);
|
||||
|
||||
$this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
|
||||
|
||||
$this->urlShortener = new UrlShortener(
|
||||
$this->titleResolutionHelper->reveal(),
|
||||
$this->em->reveal(),
|
||||
new SimpleShortUrlRelationResolver(),
|
||||
$this->shortCodeHelper->reveal(),
|
||||
$this->eventDispatcher->reveal(),
|
||||
);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user