mirror of
https://github.com/shlinkio/shlink.git
synced 2025-03-14 12:11:19 +03:00
Removed deprecated features
This commit is contained in:
parent
cc4afa7b62
commit
ad3805a560
64 changed files with 96 additions and 1083 deletions
17
Dockerfile
17
Dockerfile
|
@ -7,7 +7,6 @@ ENV SHLINK_RUNTIME ${SHLINK_RUNTIME}
|
|||
ARG SHLINK_USER_ID='root'
|
||||
ENV SHLINK_USER_ID ${SHLINK_USER_ID}
|
||||
|
||||
ENV OPENSWOOLE_VERSION 22.1.2
|
||||
ENV PDO_SQLSRV_VERSION 5.12.0
|
||||
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
||||
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
||||
|
@ -26,13 +25,8 @@ RUN \
|
|||
apk del .dev-deps && \
|
||||
apk add --no-cache postgresql icu libzip libpng
|
||||
|
||||
# Install openswoole and sqlsrv driver for x86_64 builds
|
||||
# Install sqlsrv driver for x86_64 builds
|
||||
RUN apk add --no-cache --virtual .phpize-deps ${PHPIZE_DEPS} unixodbc-dev && \
|
||||
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \
|
||||
# Openswoole is deprecated. Remove in v4.0.0
|
||||
pecl install openswoole-${OPENSWOOLE_VERSION} && \
|
||||
docker-php-ext-enable openswoole ; \
|
||||
fi; \
|
||||
if [ $(uname -m) == "x86_64" ]; then \
|
||||
wget https://download.microsoft.com/download/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
apk add --allow-untrusted msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
|
@ -47,14 +41,7 @@ FROM base as builder
|
|||
COPY . .
|
||||
COPY --from=composer:2 /usr/bin/composer ./composer.phar
|
||||
RUN apk add --no-cache git && \
|
||||
# FIXME Ignoring ext-openswoole platform req, as it makes install fail with roadrunner, even though it's a dev dependency and we are passing --no-dev
|
||||
php composer.phar install --no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction --ignore-platform-req=ext-openswoole && \
|
||||
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \
|
||||
# Openswoole is deprecated. Remove in v4.0.0
|
||||
php composer.phar remove spiral/roadrunner spiral/roadrunner-jobs spiral/roadrunner-cli spiral/roadrunner-http --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interaction ; \
|
||||
elif [ "$SHLINK_RUNTIME" == 'rr' ]; then \
|
||||
php composer.phar remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interaction --ignore-platform-req=ext-openswoole ; \
|
||||
fi; \
|
||||
php composer.phar install --no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction && \
|
||||
php composer.phar clear-cache && \
|
||||
rm -r docker composer.* && \
|
||||
sed -i "s/%SHLINK_VERSION%/${SHLINK_VERSION}/g" config/autoload/app_options.global.php
|
||||
|
|
21
build.sh
21
build.sh
|
@ -1,18 +1,15 @@
|
|||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
if [ "$#" -lt 1 ] || [ "$#" -gt 2 ] || ([ "$#" == 2 ] && [ "$2" != "--no-swoole" ]); then
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "Usage:" >&2
|
||||
echo " $0 {version} [--no-swoole]" >&2
|
||||
echo " $0 {version}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version=$1
|
||||
noSwoole=$2
|
||||
phpVersion=$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')
|
||||
# Openswoole is deprecated. Remove in v4.0.0
|
||||
[[ $noSwoole ]] && swooleSuffix="" || swooleSuffix="_openswoole"
|
||||
distId="shlink${version}_php${phpVersion}${swooleSuffix}_dist"
|
||||
distId="shlink${version}_php${phpVersion}_dist"
|
||||
builtContent="./build/${distId}"
|
||||
projectdir=$(pwd)
|
||||
[[ -f ./composer.phar ]] && composerBin='./composer.phar' || composerBin='composer'
|
||||
|
@ -31,18 +28,8 @@ cd "${builtContent}"
|
|||
|
||||
# Install dependencies
|
||||
echo "Installing dependencies with $composerBin..."
|
||||
composerFlags="--optimize-autoloader --no-progress --no-interaction"
|
||||
${composerBin} self-update
|
||||
${composerBin} install --no-dev --prefer-dist $composerFlags
|
||||
|
||||
if [[ $noSwoole ]]; then
|
||||
# If generating a dist not for openswoole, uninstall mezzio-swoole
|
||||
${composerBin} remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev $composerFlags
|
||||
else
|
||||
# Deprecated. Remove in Shlink v4.0.0
|
||||
# If generating a dist for openswoole, uninstall RoadRunner
|
||||
${composerBin} remove spiral/roadrunner spiral/roadrunner-jobs spiral/roadrunner-cli spiral/roadrunner-http --with-all-dependencies --update-no-dev $composerFlags
|
||||
fi
|
||||
${composerBin} install --no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction
|
||||
|
||||
# Delete development files
|
||||
echo 'Deleting dev files...'
|
||||
|
|
|
@ -21,8 +21,6 @@ return [
|
|||
Option\Database\DatabaseUnixSocketConfigOption::class,
|
||||
Option\UrlShortener\ShortDomainHostConfigOption::class,
|
||||
Option\UrlShortener\ShortDomainSchemaConfigOption::class,
|
||||
Option\Visit\VisitsWebhooksConfigOption::class,
|
||||
Option\Visit\OrphanVisitsWebhooksConfigOption::class,
|
||||
Option\Redirect\BaseUrlRedirectConfigOption::class,
|
||||
Option\Redirect\InvalidShortUrlRedirectConfigOption::class,
|
||||
Option\Redirect\Regular404RedirectConfigOption::class,
|
||||
|
|
|
@ -47,7 +47,6 @@ return [
|
|||
'rest' => [
|
||||
'path' => '/rest',
|
||||
'middleware' => [
|
||||
Rest\Middleware\ErrorHandler\BackwardsCompatibleProblemDetailsHandler::class,
|
||||
Router\Middleware\ImplicitOptionsMiddleware::class,
|
||||
Rest\Middleware\BodyParserMiddleware::class,
|
||||
Rest\Middleware\AuthenticationMiddleware::class,
|
||||
|
|
|
@ -14,9 +14,6 @@ return [
|
|||
'user' => EnvVars::RABBITMQ_USER->loadFromEnv(),
|
||||
'password' => EnvVars::RABBITMQ_PASSWORD->loadFromEnv(),
|
||||
'vhost' => EnvVars::RABBITMQ_VHOST->loadFromEnv('/'),
|
||||
|
||||
// Deprecated
|
||||
'legacy_visits_publishing' => (bool) EnvVars::RABBITMQ_LEGACY_VISITS_PUBLISHING->loadFromEnv(false),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
@ -14,7 +14,7 @@ return (static function (): array {
|
|||
MIN_SHORT_CODES_LENGTH,
|
||||
);
|
||||
$modeFromEnv = EnvVars::SHORT_URL_MODE->loadFromEnv(ShortUrlMode::STRICT->value);
|
||||
$mode = ShortUrlMode::tryDeprecated($modeFromEnv) ?? ShortUrlMode::STRICT;
|
||||
$mode = ShortUrlMode::tryFrom($modeFromEnv) ?? ShortUrlMode::STRICT;
|
||||
|
||||
return [
|
||||
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
// Deprecated. Webhooks are no longer supported. To be removed in Shlink 4.0.0
|
||||
return (static function (): array {
|
||||
$webhooks = EnvVars::VISITS_WEBHOOKS->loadFromEnv();
|
||||
|
||||
return [
|
||||
|
||||
'visits_webhooks' => [
|
||||
'webhooks' => $webhooks === null ? [] : explode(',', $webhooks),
|
||||
'notify_orphan_visits_to_webhooks' =>
|
||||
(bool) EnvVars::NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS->loadFromEnv(false),
|
||||
],
|
||||
|
||||
];
|
||||
})();
|
|
@ -9,7 +9,7 @@ use Shlinkio\Shlink\Core\Util\RedirectStatus;
|
|||
const DEFAULT_DELETE_SHORT_URL_THRESHOLD = 15;
|
||||
const DEFAULT_SHORT_CODES_LENGTH = 5;
|
||||
const MIN_SHORT_CODES_LENGTH = 4;
|
||||
const DEFAULT_REDIRECT_STATUS_CODE = RedirectStatus::STATUS_302; // Deprecated. Default to 307 for Shlink v4
|
||||
const DEFAULT_REDIRECT_STATUS_CODE = RedirectStatus::STATUS_302;
|
||||
const DEFAULT_REDIRECT_CACHE_LIFETIME = 30;
|
||||
const LOCAL_LOCK_FACTORY = 'Shlinkio\Shlink\LocalLockFactory';
|
||||
const TITLE_TAG_VALUE = '/<title[^>]*>(.*?)<\/title>/i'; // Matches the value inside a html title tag
|
||||
|
@ -19,6 +19,5 @@ const DEFAULT_QR_CODE_MARGIN = 0;
|
|||
const DEFAULT_QR_CODE_FORMAT = 'png';
|
||||
const DEFAULT_QR_CODE_ERROR_CORRECTION = 'l';
|
||||
const DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = true;
|
||||
// Deprecated. Shlink 4.0.0 should change default value to `true`
|
||||
const DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS = false;
|
||||
const DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS = true;
|
||||
const MIN_TASK_WORKERS = 4;
|
||||
|
|
|
@ -20,19 +20,6 @@ fi
|
|||
|
||||
php vendor/bin/shlink-installer init ${flags}
|
||||
|
||||
# Periodically run visit:locate every hour, if ENABLE_PERIODIC_VISIT_LOCATE=true was provided and running as root
|
||||
# FIXME: ENABLE_PERIODIC_VISIT_LOCATE is deprecated. Remove cron support in Shlink 4.0.0
|
||||
if [ "${ENABLE_PERIODIC_VISIT_LOCATE}" = "true" ] && [ "${SHLINK_USER_ID}" = "root" ]; then
|
||||
echo "Configuring periodic visit location..."
|
||||
echo "0 * * * * php /etc/shlink/bin/cli visit:locate -q" > /etc/crontabs/root
|
||||
/usr/sbin/crond &
|
||||
fi
|
||||
|
||||
if [ "$SHLINK_RUNTIME" = 'openswoole' ]; then
|
||||
# Openswoole is deprecated. Remove in Shlink 4.0.0
|
||||
# When restarting the container, openswoole might think it is already in execution
|
||||
# This forces the app to be started every second until the exit code is 0
|
||||
until php vendor/bin/laminas mezzio:swoole:start; do sleep 1 ; done
|
||||
elif [ "$SHLINK_RUNTIME" = 'rr' ]; then
|
||||
if [ "$SHLINK_RUNTIME" = 'rr' ]; then
|
||||
./bin/rr serve -c config/roadrunner/.rr.yml
|
||||
fi
|
||||
|
|
|
@ -122,11 +122,6 @@
|
|||
"visitsSummary": {
|
||||
"$ref": "#/components/schemas/VisitsSummary"
|
||||
},
|
||||
"visitsCount": {
|
||||
"deprecated": true,
|
||||
"type": "integer",
|
||||
"description": "The number of visits that this short URL has received."
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
"longUrl",
|
||||
"deviceLongUrls",
|
||||
"dateCreated",
|
||||
"visitsCount",
|
||||
"visitsSummary",
|
||||
"tags",
|
||||
"meta",
|
||||
|
@ -36,11 +35,6 @@
|
|||
"format": "date-time",
|
||||
"description": "The date in which the short URL was created in ISO format."
|
||||
},
|
||||
"visitsCount": {
|
||||
"deprecated": true,
|
||||
"type": "integer",
|
||||
"description": "**[DEPRECATED]** Use `visitsSummary.total` instead."
|
||||
},
|
||||
"visitsSummary": {
|
||||
"$ref": "./VisitsSummary.json"
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"type": "object",
|
||||
"required": ["tag", "shortUrlsCount", "visitsSummary", "visitsCount"],
|
||||
"required": ["tag", "shortUrlsCount", "visitsSummary"],
|
||||
"properties": {
|
||||
"tag": {
|
||||
"type": "string",
|
||||
|
@ -12,11 +12,6 @@
|
|||
},
|
||||
"visitsSummary": {
|
||||
"$ref": "./VisitsSummary.json"
|
||||
},
|
||||
"visitsCount": {
|
||||
"deprecated": true,
|
||||
"type": "number",
|
||||
"description": "**[DEPRECATED]** Use visitsSummary.total instead"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,12 @@
|
|||
{
|
||||
"type": "object",
|
||||
"required": ["nonOrphanVisits", "orphanVisits", "visitsCount", "orphanVisitsCount"],
|
||||
"required": ["nonOrphanVisits", "orphanVisits"],
|
||||
"properties": {
|
||||
"nonOrphanVisits": {
|
||||
"$ref": "./VisitsSummary.json"
|
||||
},
|
||||
"orphanVisits": {
|
||||
"$ref": "./VisitsSummary.json"
|
||||
},
|
||||
"visitsCount": {
|
||||
"deprecated": true,
|
||||
"type": "number",
|
||||
"description": "**[DEPRECATED]** Use nonOrphanVisits.total instead"
|
||||
},
|
||||
"orphanVisitsCount": {
|
||||
"deprecated": true,
|
||||
"type": "number",
|
||||
"description": "**[DEPRECATED]** Use orphanVisits.total instead"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -218,7 +218,7 @@ class ListShortUrlsCommand extends Command
|
|||
'Short URL' => $pickProp('shortUrl'),
|
||||
'Long URL' => $pickProp('longUrl'),
|
||||
'Date created' => $pickProp('dateCreated'),
|
||||
'Visits count' => $pickProp('visitsCount'),
|
||||
'Visits count' => static fn (array $shortUrl) => $shortUrl['visitsSummary']->total,
|
||||
];
|
||||
if ($input->getOption('show-tags')) {
|
||||
$columnsMap['Tags'] = static fn (array $shortUrl): string => implode(', ', $shortUrl['tags']);
|
||||
|
|
|
@ -31,7 +31,6 @@ return [
|
|||
Options\TrackingOptions::class => [ValinorConfigFactory::class, 'config.tracking'],
|
||||
Options\QrCodeOptions::class => [ValinorConfigFactory::class, 'config.qr_codes'],
|
||||
Options\RabbitMqOptions::class => [ValinorConfigFactory::class, 'config.rabbitmq'],
|
||||
Options\WebhookOptions::class => ConfigAbstractFactory::class,
|
||||
|
||||
ShortUrl\UrlShortener::class => ConfigAbstractFactory::class,
|
||||
ShortUrl\ShortUrlService::class => ConfigAbstractFactory::class,
|
||||
|
@ -113,8 +112,6 @@ return [
|
|||
Domain\DomainService::class,
|
||||
],
|
||||
|
||||
Options\WebhookOptions::class => ['config.visits_webhooks'],
|
||||
|
||||
ShortUrl\UrlShortener::class => [
|
||||
ShortUrl\Helper\ShortUrlTitleResolutionHelper::class,
|
||||
'em',
|
||||
|
|
|
@ -37,7 +37,6 @@ return (static function (): array {
|
|||
EventDispatcher\Mercure\NotifyVisitToMercure::class,
|
||||
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class,
|
||||
EventDispatcher\RedisPubSub\NotifyVisitToRedis::class,
|
||||
EventDispatcher\NotifyVisitToWebHooks::class,
|
||||
EventDispatcher\UpdateGeoLiteDb::class,
|
||||
],
|
||||
EventDispatcher\Event\ShortUrlCreated::class => [
|
||||
|
@ -66,7 +65,6 @@ return (static function (): array {
|
|||
EventDispatcher\LocateVisit::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\Matomo\SendVisitToMatomo::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\LocateUnlocatedVisits::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class => ConfigAbstractFactory::class,
|
||||
|
@ -104,9 +102,6 @@ return (static function (): array {
|
|||
EventDispatcher\LocateUnlocatedVisits::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
@ -119,14 +114,6 @@ return (static function (): array {
|
|||
EventDispatcherInterface::class,
|
||||
],
|
||||
EventDispatcher\LocateUnlocatedVisits::class => [VisitLocator::class, VisitToLocationHelper::class],
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => [
|
||||
'httpClient',
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
Options\WebhookOptions::class,
|
||||
ShortUrl\Transformer\ShortUrlDataTransformer::class,
|
||||
Options\AppOptions::class,
|
||||
],
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class => [
|
||||
MercureHubPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
|
@ -144,7 +131,6 @@ return (static function (): array {
|
|||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
Visit\Transformer\OrphanVisitDataTransformer::class,
|
||||
Options\RabbitMqOptions::class,
|
||||
],
|
||||
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class => [
|
||||
|
@ -187,7 +173,6 @@ return (static function (): array {
|
|||
Options\RabbitMqOptions::class,
|
||||
'config.redis.pub_sub_enabled',
|
||||
MercureOptions::class,
|
||||
Options\WebhookOptions::class,
|
||||
GeoLite2Options::class,
|
||||
MatomoOptions::class,
|
||||
],
|
||||
|
|
|
@ -36,8 +36,6 @@ enum EnvVars: string
|
|||
case RABBITMQ_PASSWORD = 'RABBITMQ_PASSWORD';
|
||||
case RABBITMQ_VHOST = 'RABBITMQ_VHOST';
|
||||
case RABBITMQ_USE_SSL = 'RABBITMQ_USE_SSL';
|
||||
/** @deprecated */
|
||||
case RABBITMQ_LEGACY_VISITS_PUBLISHING = 'RABBITMQ_LEGACY_VISITS_PUBLISHING';
|
||||
case MATOMO_ENABLED = 'MATOMO_ENABLED';
|
||||
case MATOMO_BASE_URL = 'MATOMO_BASE_URL';
|
||||
case MATOMO_SITE_ID = 'MATOMO_SITE_ID';
|
||||
|
@ -74,10 +72,6 @@ enum EnvVars: string
|
|||
case REDIRECT_APPEND_EXTRA_PATH = 'REDIRECT_APPEND_EXTRA_PATH';
|
||||
case TIMEZONE = 'TIMEZONE';
|
||||
case MULTI_SEGMENT_SLUGS_ENABLED = 'MULTI_SEGMENT_SLUGS_ENABLED';
|
||||
/** @deprecated */
|
||||
case VISITS_WEBHOOKS = 'VISITS_WEBHOOKS';
|
||||
/** @deprecated */
|
||||
case NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS = 'NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS';
|
||||
|
||||
public function loadFromEnv(mixed $default = null): mixed
|
||||
{
|
||||
|
|
|
@ -8,19 +8,17 @@ use Shlinkio\Shlink\Common\Mercure\MercureOptions;
|
|||
use Shlinkio\Shlink\Core\EventDispatcher;
|
||||
use Shlinkio\Shlink\Core\Matomo\MatomoOptions;
|
||||
use Shlinkio\Shlink\Core\Options\RabbitMqOptions;
|
||||
use Shlinkio\Shlink\Core\Options\WebhookOptions;
|
||||
use Shlinkio\Shlink\EventDispatcher\Listener\EnabledListenerCheckerInterface;
|
||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\GeoLite2Options;
|
||||
|
||||
class EnabledListenerChecker implements EnabledListenerCheckerInterface
|
||||
readonly class EnabledListenerChecker implements EnabledListenerCheckerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RabbitMqOptions $rabbitMqOptions,
|
||||
private readonly bool $redisPubSubEnabled,
|
||||
private readonly MercureOptions $mercureOptions,
|
||||
private readonly WebhookOptions $webhookOptions,
|
||||
private readonly GeoLite2Options $geoLiteOptions,
|
||||
private readonly MatomoOptions $matomoOptions,
|
||||
private RabbitMqOptions $rabbitMqOptions,
|
||||
private bool $redisPubSubEnabled,
|
||||
private MercureOptions $mercureOptions,
|
||||
private GeoLite2Options $geoLiteOptions,
|
||||
private MatomoOptions $matomoOptions,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -38,7 +36,6 @@ class EnabledListenerChecker implements EnabledListenerCheckerInterface
|
|||
EventDispatcher\Mercure\NotifyVisitToMercure::class,
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => $this->mercureOptions->isEnabled(),
|
||||
EventDispatcher\Matomo\SendVisitToMatomo::class => $this->matomoOptions->enabled,
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => $this->webhookOptions->hasWebhooks(),
|
||||
EventDispatcher\UpdateGeoLiteDb::class => $this->geoLiteOptions->hasLicenseKey(),
|
||||
default => false, // Any unknown async listener should not be enabled by default
|
||||
};
|
||||
|
|
|
@ -1,101 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\EventDispatcher;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Fig\Http\Message\RequestMethodInterface;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Promise\Promise;
|
||||
use GuzzleHttp\Promise\PromiseInterface;
|
||||
use GuzzleHttp\Promise\Utils;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Event\VisitLocated;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Shlinkio\Shlink\Core\Options\WebhookOptions;
|
||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||
use Throwable;
|
||||
|
||||
use function array_map;
|
||||
|
||||
/** @deprecated */
|
||||
class NotifyVisitToWebHooks
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ClientInterface $httpClient,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly WebhookOptions $webhookOptions,
|
||||
private readonly DataTransformerInterface $transformer,
|
||||
private readonly AppOptions $appOptions,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(VisitLocated $shortUrlLocated): void
|
||||
{
|
||||
if (! $this->webhookOptions->hasWebhooks()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$visitId = $shortUrlLocated->visitId;
|
||||
|
||||
/** @var Visit|null $visit */
|
||||
$visit = $this->em->find(Visit::class, $visitId);
|
||||
if ($visit === null) {
|
||||
$this->logger->warning('Tried to notify webhooks for visit with id "{visitId}", but it does not exist.', [
|
||||
'visitId' => $visitId,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($visit->isOrphan() && ! $this->webhookOptions->notifyOrphanVisits()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$requestOptions = $this->buildRequestOptions($visit);
|
||||
$requestPromises = $this->performRequests($requestOptions, $visitId);
|
||||
|
||||
// Wait for all the promises to finish, ignoring rejections, as in those cases we only want to log the error.
|
||||
Utils::settle($requestPromises)->wait();
|
||||
}
|
||||
|
||||
private function buildRequestOptions(Visit $visit): array
|
||||
{
|
||||
$payload = ['visit' => $visit->jsonSerialize()];
|
||||
$shortUrl = $visit->getShortUrl();
|
||||
if ($shortUrl !== null) {
|
||||
$payload['shortUrl'] = $this->transformer->transform($shortUrl);
|
||||
}
|
||||
|
||||
return [
|
||||
RequestOptions::TIMEOUT => 10,
|
||||
RequestOptions::JSON => $payload,
|
||||
RequestOptions::HEADERS => ['User-Agent' => $this->appOptions->__toString()],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Promise[] $requestOptions
|
||||
*/
|
||||
private function performRequests(array $requestOptions, string $visitId): array
|
||||
{
|
||||
return array_map(
|
||||
fn (string $webhook): PromiseInterface => $this->httpClient
|
||||
->requestAsync(RequestMethodInterface::METHOD_POST, $webhook, $requestOptions)
|
||||
->otherwise(fn (Throwable $e) => $this->logWebhookFailure($webhook, $visitId, $e)),
|
||||
$this->webhookOptions->webhooks(),
|
||||
);
|
||||
}
|
||||
|
||||
private function logWebhookFailure(string $webhook, string $visitId, Throwable $e): void
|
||||
{
|
||||
$this->logger->warning('Failed to notify visit with id "{visitId}" to webhook "{webhook}". {e}', [
|
||||
'visitId' => $visitId,
|
||||
'webhook' => $webhook,
|
||||
'e' => $e,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -6,15 +6,11 @@ namespace Shlinkio\Shlink\Core\EventDispatcher\RabbitMq;
|
|||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
|
||||
use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface;
|
||||
use Shlinkio\Shlink\Common\UpdatePublishing\Update;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Async\AbstractNotifyVisitListener;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Async\RemoteSystem;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Topic;
|
||||
use Shlinkio\Shlink\Core\Options\RabbitMqOptions;
|
||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||
|
||||
class NotifyVisitToRabbitMq extends AbstractNotifyVisitListener
|
||||
{
|
||||
|
@ -23,42 +19,11 @@ class NotifyVisitToRabbitMq extends AbstractNotifyVisitListener
|
|||
PublishingUpdatesGeneratorInterface $updatesGenerator,
|
||||
EntityManagerInterface $em,
|
||||
LoggerInterface $logger,
|
||||
private readonly DataTransformerInterface $orphanVisitTransformer,
|
||||
private readonly RabbitMqOptions $options,
|
||||
) {
|
||||
parent::__construct($rabbitMqHelper, $updatesGenerator, $em, $logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Update[]
|
||||
*/
|
||||
protected function determineUpdatesForVisit(Visit $visit): array
|
||||
{
|
||||
// Once the two deprecated cases below have been removed, make parent method private
|
||||
if (! $this->options->legacyVisitsPublishing) {
|
||||
return parent::determineUpdatesForVisit($visit);
|
||||
}
|
||||
|
||||
// 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()
|
||||
? [
|
||||
Update::forTopicAndPayload(
|
||||
Topic::NEW_ORPHAN_VISIT->value,
|
||||
$this->orphanVisitTransformer->transform($visit),
|
||||
),
|
||||
]
|
||||
: [
|
||||
Update::forTopicAndPayload(Topic::NEW_VISIT->value, $visit->jsonSerialize()),
|
||||
Update::forTopicAndPayload(
|
||||
Topic::newShortUrlVisit($visit->getShortUrl()?->getShortCode()),
|
||||
$visit->jsonSerialize(),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
protected function isEnabled(): bool
|
||||
{
|
||||
return $this->options->enabled;
|
||||
|
|
|
@ -4,12 +4,10 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\Core\Options;
|
||||
|
||||
final class RabbitMqOptions
|
||||
final readonly class RabbitMqOptions
|
||||
{
|
||||
public function __construct(
|
||||
public readonly bool $enabled = false,
|
||||
/** @deprecated */
|
||||
public readonly bool $legacyVisitsPublishing = false,
|
||||
public bool $enabled = false,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Options;
|
||||
|
||||
use Laminas\Stdlib\AbstractOptions;
|
||||
|
||||
/** @deprecated */
|
||||
class WebhookOptions extends AbstractOptions
|
||||
{
|
||||
protected $__strictMode__ = false; // phpcs:ignore
|
||||
|
||||
private array $webhooks = [];
|
||||
private bool $notifyOrphanVisitsToWebhooks = false;
|
||||
|
||||
public function webhooks(): array
|
||||
{
|
||||
return $this->webhooks;
|
||||
}
|
||||
|
||||
public function hasWebhooks(): bool
|
||||
{
|
||||
return ! empty($this->webhooks);
|
||||
}
|
||||
|
||||
protected function setWebhooks(array $webhooks): void
|
||||
{
|
||||
$this->webhooks = $webhooks;
|
||||
}
|
||||
|
||||
public function notifyOrphanVisits(): bool
|
||||
{
|
||||
return $this->notifyOrphanVisitsToWebhooks;
|
||||
}
|
||||
|
||||
protected function setNotifyOrphanVisitsToWebhooks(bool $notifyOrphanVisitsToWebhooks): void
|
||||
{
|
||||
$this->notifyOrphanVisitsToWebhooks = $notifyOrphanVisitsToWebhooks;
|
||||
}
|
||||
}
|
|
@ -6,10 +6,4 @@ enum ShortUrlMode: string
|
|||
{
|
||||
case STRICT = 'strict';
|
||||
case LOOSE = 'loose';
|
||||
|
||||
/** @deprecated */
|
||||
public static function tryDeprecated(string $mode): ?self
|
||||
{
|
||||
return $mode === 'loosely' ? self::LOOSE : self::tryFrom($mode);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,9 +39,6 @@ class ShortUrlDataTransformer implements DataTransformerInterface
|
|||
$shortUrl->getVisitsCount(),
|
||||
$shortUrl->nonBotVisitsCount(),
|
||||
),
|
||||
|
||||
// Deprecated
|
||||
'visitsCount' => $shortUrl->getVisitsCount(),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -10,15 +10,13 @@ enum OrderableField: string
|
|||
case SHORT_URLS_COUNT = 'shortUrlsCount';
|
||||
case VISITS = 'visits';
|
||||
case NON_BOT_VISITS = 'nonBotVisits';
|
||||
/** @deprecated Use VISITS instead */
|
||||
case VISITS_COUNT = 'visitsCount';
|
||||
|
||||
public static function toSnakeCaseValidField(?string $field): self
|
||||
public static function toValidField(?string $field): self
|
||||
{
|
||||
$parsed = $field !== null ? self::tryFrom($field) : self::TAG;
|
||||
return match ($parsed) {
|
||||
self::VISITS_COUNT, null => self::VISITS,
|
||||
default => $parsed,
|
||||
};
|
||||
if ($field === null) {
|
||||
return self::TAG;
|
||||
}
|
||||
|
||||
return self::tryFrom($field) ?? self::TAG;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,13 +7,13 @@ namespace Shlinkio\Shlink\Core\Tag\Model;
|
|||
use JsonSerializable;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\VisitsSummary;
|
||||
|
||||
final class TagInfo implements JsonSerializable
|
||||
final readonly class TagInfo implements JsonSerializable
|
||||
{
|
||||
public readonly VisitsSummary $visitsSummary;
|
||||
public VisitsSummary $visitsSummary;
|
||||
|
||||
public function __construct(
|
||||
public readonly string $tag,
|
||||
public readonly int $shortUrlsCount,
|
||||
public string $tag,
|
||||
public int $shortUrlsCount,
|
||||
int $visitsCount,
|
||||
?int $nonBotVisitsCount = null,
|
||||
) {
|
||||
|
@ -36,9 +36,6 @@ final class TagInfo implements JsonSerializable
|
|||
'tag' => $this->tag,
|
||||
'shortUrlsCount' => $this->shortUrlsCount,
|
||||
'visitsSummary' => $this->visitsSummary,
|
||||
|
||||
// Deprecated
|
||||
'visitsCount' => $this->visitsSummary->total,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,8 +14,6 @@ final class TagsParams extends AbstractInfinitePaginableListParams
|
|||
private function __construct(
|
||||
public readonly ?string $searchTerm,
|
||||
public readonly Ordering $orderBy,
|
||||
/** @deprecated */
|
||||
public readonly bool $withStats,
|
||||
?int $page,
|
||||
?int $itemsPerPage,
|
||||
) {
|
||||
|
@ -27,7 +25,6 @@ final class TagsParams extends AbstractInfinitePaginableListParams
|
|||
return new self(
|
||||
$query['searchTerm'] ?? null,
|
||||
Ordering::fromTuple(isset($query['orderBy']) ? parseOrderBy($query['orderBy']) : [null, null]),
|
||||
($query['withStats'] ?? null) === 'true',
|
||||
isset($query['page']) ? (int) $query['page'] : null,
|
||||
isset($query['itemsPerPage']) ? (int) $query['itemsPerPage'] : null,
|
||||
);
|
||||
|
|
|
@ -43,7 +43,7 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
|
|||
*/
|
||||
public function findTagsWithInfo(?TagsListFiltering $filtering = null): array
|
||||
{
|
||||
$orderField = OrderableField::toSnakeCaseValidField($filtering?->orderBy?->field);
|
||||
$orderField = OrderableField::toValidField($filtering?->orderBy?->field);
|
||||
$orderDir = $filtering?->orderBy?->direction ?? 'ASC';
|
||||
$apiKey = $filtering?->apiKey;
|
||||
$conn = $this->getEntityManager()->getConnection();
|
||||
|
|
|
@ -6,10 +6,10 @@ namespace Shlinkio\Shlink\Core\Visit\Model;
|
|||
|
||||
use JsonSerializable;
|
||||
|
||||
final class VisitsStats implements JsonSerializable
|
||||
final readonly class VisitsStats implements JsonSerializable
|
||||
{
|
||||
private readonly VisitsSummary $nonOrphanVisitsSummary;
|
||||
private readonly VisitsSummary $orphanVisitsSummary;
|
||||
private VisitsSummary $nonOrphanVisitsSummary;
|
||||
private VisitsSummary $orphanVisitsSummary;
|
||||
|
||||
public function __construct(
|
||||
int $nonOrphanVisitsTotal,
|
||||
|
@ -32,10 +32,6 @@ final class VisitsStats implements JsonSerializable
|
|||
return [
|
||||
'nonOrphanVisits' => $this->nonOrphanVisitsSummary,
|
||||
'orphanVisits' => $this->orphanVisitsSummary,
|
||||
|
||||
// Deprecated
|
||||
'visitsCount' => $this->nonOrphanVisitsSummary->total,
|
||||
'orphanVisitsCount' => $this->orphanVisitsSummary->total,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
|
|||
class QrCodeTest extends ApiTestCase
|
||||
{
|
||||
#[Test]
|
||||
public function returnsNotFoundWhenShortUrlIsNotEnabled(): void
|
||||
public function returnsQrCodeEvenIfShortUrlIsNotEnabled(): void
|
||||
{
|
||||
// The QR code successfully resolves at first
|
||||
$response = $this->callShortUrl('custom/qr-code');
|
||||
|
@ -20,8 +20,8 @@ class QrCodeTest extends ApiTestCase
|
|||
$this->callShortUrl('custom');
|
||||
$this->callShortUrl('custom');
|
||||
|
||||
// After 2 visits, the QR code should return a 404
|
||||
$response = $this->callShortUrl('custom/qr-code');
|
||||
self::assertEquals(404, $response->getStatusCode());
|
||||
// After 2 visits, the short URL returns a 404, but the QR code should still work
|
||||
self::assertEquals(404, $this->callShortUrl('custom')->getStatusCode());
|
||||
self::assertEquals(200, $this->callShortUrl('custom/qr-code')->getStatusCode());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -197,13 +197,6 @@ class TagRepositoryTest extends DatabaseTestCase
|
|||
['another', 0, 0, 0],
|
||||
],
|
||||
];
|
||||
yield 'visits count DESC ordering and limit' => [
|
||||
new TagsListFiltering(2, null, null, Ordering::fromTuple([OrderableField::VISITS_COUNT->value, 'DESC'])),
|
||||
[
|
||||
['foo', 2, 4, 3],
|
||||
['bar', 3, 3, 2],
|
||||
],
|
||||
];
|
||||
yield 'api key' => [new TagsListFiltering(null, null, null, null, ApiKey::fromMeta(
|
||||
ApiKeyMeta::withRoles(RoleDefinition::forAuthoredShortUrls()),
|
||||
)), [
|
||||
|
|
|
@ -265,7 +265,7 @@ class QrCodeActionTest extends TestCase
|
|||
$this->urlResolver,
|
||||
new ShortUrlStringifier(['domain' => 's.test']),
|
||||
new NullLogger(),
|
||||
$options ?? new QrCodeOptions(),
|
||||
$options ?? new QrCodeOptions(enabledForDisabledShortUrls: false),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ use Shlinkio\Shlink\Core\EventDispatcher\Helper\EnabledListenerChecker;
|
|||
use Shlinkio\Shlink\Core\EventDispatcher\Matomo\SendVisitToMatomo;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Mercure\NotifyNewShortUrlToMercure;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Mercure\NotifyVisitToMercure;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\NotifyVisitToWebHooks;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\RabbitMq\NotifyVisitToRabbitMq;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis;
|
||||
|
@ -20,7 +19,6 @@ use Shlinkio\Shlink\Core\EventDispatcher\RedisPubSub\NotifyVisitToRedis;
|
|||
use Shlinkio\Shlink\Core\EventDispatcher\UpdateGeoLiteDb;
|
||||
use Shlinkio\Shlink\Core\Matomo\MatomoOptions;
|
||||
use Shlinkio\Shlink\Core\Options\RabbitMqOptions;
|
||||
use Shlinkio\Shlink\Core\Options\WebhookOptions;
|
||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\GeoLite2Options;
|
||||
|
||||
class EnabledListenerCheckerTest extends TestCase
|
||||
|
@ -41,7 +39,6 @@ class EnabledListenerCheckerTest extends TestCase
|
|||
[NotifyVisitToMercure::class],
|
||||
[NotifyNewShortUrlToMercure::class],
|
||||
[SendVisitToMatomo::class],
|
||||
[NotifyVisitToWebHooks::class],
|
||||
[UpdateGeoLiteDb::class],
|
||||
];
|
||||
}
|
||||
|
@ -68,7 +65,6 @@ class EnabledListenerCheckerTest extends TestCase
|
|||
NotifyNewShortUrlToRedis::class => false,
|
||||
NotifyVisitToMercure::class => false,
|
||||
NotifyNewShortUrlToMercure::class => false,
|
||||
NotifyVisitToWebHooks::class => false,
|
||||
UpdateGeoLiteDb::class => false,
|
||||
'unknown' => false,
|
||||
]];
|
||||
|
@ -79,7 +75,6 @@ class EnabledListenerCheckerTest extends TestCase
|
|||
NotifyNewShortUrlToRedis::class => true,
|
||||
NotifyVisitToMercure::class => false,
|
||||
NotifyNewShortUrlToMercure::class => false,
|
||||
NotifyVisitToWebHooks::class => false,
|
||||
UpdateGeoLiteDb::class => false,
|
||||
'unknown' => false,
|
||||
]];
|
||||
|
@ -90,18 +85,6 @@ class EnabledListenerCheckerTest extends TestCase
|
|||
NotifyNewShortUrlToRedis::class => false,
|
||||
NotifyVisitToMercure::class => true,
|
||||
NotifyNewShortUrlToMercure::class => true,
|
||||
NotifyVisitToWebHooks::class => false,
|
||||
UpdateGeoLiteDb::class => false,
|
||||
'unknown' => false,
|
||||
]];
|
||||
yield 'Webhooks' => [self::checker(webhooksEnabled: true), [
|
||||
NotifyVisitToRabbitMq::class => false,
|
||||
NotifyNewShortUrlToRabbitMq::class => false,
|
||||
NotifyVisitToRedis::class => false,
|
||||
NotifyNewShortUrlToRedis::class => false,
|
||||
NotifyVisitToMercure::class => false,
|
||||
NotifyNewShortUrlToMercure::class => false,
|
||||
NotifyVisitToWebHooks::class => true,
|
||||
UpdateGeoLiteDb::class => false,
|
||||
'unknown' => false,
|
||||
]];
|
||||
|
@ -112,7 +95,6 @@ class EnabledListenerCheckerTest extends TestCase
|
|||
NotifyNewShortUrlToRedis::class => false,
|
||||
NotifyVisitToMercure::class => false,
|
||||
NotifyNewShortUrlToMercure::class => false,
|
||||
NotifyVisitToWebHooks::class => false,
|
||||
UpdateGeoLiteDb::class => true,
|
||||
'unknown' => false,
|
||||
]];
|
||||
|
@ -124,7 +106,6 @@ class EnabledListenerCheckerTest extends TestCase
|
|||
NotifyVisitToMercure::class => false,
|
||||
NotifyNewShortUrlToMercure::class => false,
|
||||
SendVisitToMatomo::class => true,
|
||||
NotifyVisitToWebHooks::class => false,
|
||||
UpdateGeoLiteDb::class => false,
|
||||
'unknown' => false,
|
||||
]];
|
||||
|
@ -135,7 +116,6 @@ class EnabledListenerCheckerTest extends TestCase
|
|||
NotifyNewShortUrlToRedis::class => false,
|
||||
NotifyVisitToMercure::class => false,
|
||||
NotifyNewShortUrlToMercure::class => false,
|
||||
NotifyVisitToWebHooks::class => false,
|
||||
UpdateGeoLiteDb::class => false,
|
||||
'unknown' => false,
|
||||
]];
|
||||
|
@ -143,7 +123,6 @@ class EnabledListenerCheckerTest extends TestCase
|
|||
rabbitMqEnabled: true,
|
||||
redisPubSubEnabled: true,
|
||||
mercureEnabled: true,
|
||||
webhooksEnabled: true,
|
||||
geoLiteEnabled: true,
|
||||
matomoEnabled: true,
|
||||
), [
|
||||
|
@ -154,7 +133,6 @@ class EnabledListenerCheckerTest extends TestCase
|
|||
NotifyVisitToMercure::class => true,
|
||||
NotifyNewShortUrlToMercure::class => true,
|
||||
SendVisitToMatomo::class => true,
|
||||
NotifyVisitToWebHooks::class => true,
|
||||
UpdateGeoLiteDb::class => true,
|
||||
'unknown' => false,
|
||||
]];
|
||||
|
@ -164,7 +142,6 @@ class EnabledListenerCheckerTest extends TestCase
|
|||
bool $rabbitMqEnabled = false,
|
||||
bool $redisPubSubEnabled = false,
|
||||
bool $mercureEnabled = false,
|
||||
bool $webhooksEnabled = false,
|
||||
bool $geoLiteEnabled = false,
|
||||
bool $matomoEnabled = false,
|
||||
): EnabledListenerChecker {
|
||||
|
@ -172,7 +149,6 @@ class EnabledListenerCheckerTest extends TestCase
|
|||
new RabbitMqOptions(enabled: $rabbitMqEnabled),
|
||||
$redisPubSubEnabled,
|
||||
new MercureOptions(publicHubUrl: $mercureEnabled ? 'the-url' : null),
|
||||
new WebhookOptions(['webhooks' => $webhooksEnabled ? ['foo', 'bar'] : []]),
|
||||
new GeoLite2Options(licenseKey: $geoLiteEnabled ? 'the-key' : null),
|
||||
new MatomoOptions(enabled: $matomoEnabled),
|
||||
);
|
||||
|
|
|
@ -1,144 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\EventDispatcher;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Exception;
|
||||
use Fig\Http\Message\RequestMethodInterface;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Promise\FulfilledPromise;
|
||||
use GuzzleHttp\Promise\RejectedPromise;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Event\VisitLocated;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\NotifyVisitToWebHooks;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Shlinkio\Shlink\Core\Options\WebhookOptions;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer;
|
||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||
|
||||
use function count;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
||||
|
||||
class NotifyVisitToWebHooksTest extends TestCase
|
||||
{
|
||||
private MockObject & ClientInterface $httpClient;
|
||||
private MockObject & EntityManagerInterface $em;
|
||||
private MockObject & LoggerInterface $logger;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->httpClient = $this->createMock(ClientInterface::class);
|
||||
$this->em = $this->createMock(EntityManagerInterface::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function emptyWebhooksMakeNoFurtherActions(): void
|
||||
{
|
||||
$this->em->expects($this->never())->method('find');
|
||||
|
||||
$this->createListener([])(new VisitLocated('1'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function invalidVisitDoesNotPerformAnyRequest(): void
|
||||
{
|
||||
$this->em->expects($this->once())->method('find')->with(Visit::class, '1')->willReturn(null);
|
||||
$this->httpClient->expects($this->never())->method('requestAsync');
|
||||
$this->logger->expects($this->once())->method('warning')->with(
|
||||
'Tried to notify webhooks for visit with id "{visitId}", but it does not exist.',
|
||||
['visitId' => '1'],
|
||||
);
|
||||
|
||||
$this->createListener(['foo', 'bar'])(new VisitLocated('1'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function orphanVisitDoesNotPerformAnyRequestWhenDisabled(): void
|
||||
{
|
||||
$this->em->expects($this->once())->method('find')->with(Visit::class, '1')->willReturn(
|
||||
Visit::forBasePath(Visitor::emptyInstance()),
|
||||
);
|
||||
$this->httpClient->expects($this->never())->method('requestAsync');
|
||||
$this->logger->expects($this->never())->method('warning');
|
||||
|
||||
$this->createListener(['foo', 'bar'], false)(new VisitLocated('1'));
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideVisits')]
|
||||
public function expectedRequestsArePerformedToWebhooks(Visit $visit, array $expectedResponseKeys): void
|
||||
{
|
||||
$webhooks = ['foo', 'invalid', 'bar', 'baz'];
|
||||
$invalidWebhooks = ['invalid', 'baz'];
|
||||
|
||||
$this->em->expects($this->once())->method('find')->with(Visit::class, '1')->willReturn($visit);
|
||||
$this->httpClient->expects($this->exactly(count($webhooks)))->method('requestAsync')->with(
|
||||
RequestMethodInterface::METHOD_POST,
|
||||
$this->istype('string'),
|
||||
$this->callback(function (array $requestOptions) use ($expectedResponseKeys) {
|
||||
Assert::assertArrayHasKey(RequestOptions::HEADERS, $requestOptions);
|
||||
Assert::assertArrayHasKey(RequestOptions::JSON, $requestOptions);
|
||||
Assert::assertArrayHasKey(RequestOptions::TIMEOUT, $requestOptions);
|
||||
Assert::assertEquals(10, $requestOptions[RequestOptions::TIMEOUT]);
|
||||
Assert::assertEquals(['User-Agent' => 'Shlink:v1.2.3'], $requestOptions[RequestOptions::HEADERS]);
|
||||
|
||||
$json = $requestOptions[RequestOptions::JSON];
|
||||
Assert::assertCount(count($expectedResponseKeys), $json);
|
||||
foreach ($expectedResponseKeys as $key) {
|
||||
Assert::assertArrayHasKey($key, $json);
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
)->willReturnCallback(function ($_, $webhook) use ($invalidWebhooks) {
|
||||
$shouldReject = contains($webhook, $invalidWebhooks);
|
||||
return $shouldReject ? new RejectedPromise(new Exception('')) : new FulfilledPromise('');
|
||||
});
|
||||
$this->logger->expects($this->exactly(count($invalidWebhooks)))->method('warning')->with(
|
||||
'Failed to notify visit with id "{visitId}" to webhook "{webhook}". {e}',
|
||||
$this->callback(function (array $extra): bool {
|
||||
Assert::assertArrayHasKey('webhook', $extra);
|
||||
Assert::assertArrayHasKey('visitId', $extra);
|
||||
Assert::assertArrayHasKey('e', $extra);
|
||||
|
||||
return true;
|
||||
}),
|
||||
);
|
||||
|
||||
$this->createListener($webhooks)(new VisitLocated('1'));
|
||||
}
|
||||
|
||||
public static function provideVisits(): iterable
|
||||
{
|
||||
yield 'regular visit' => [
|
||||
Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()),
|
||||
['shortUrl', 'visit'],
|
||||
];
|
||||
yield 'orphan visit' => [Visit::forBasePath(Visitor::emptyInstance()), ['visit']];
|
||||
}
|
||||
|
||||
private function createListener(array $webhooks, bool $notifyOrphanVisits = true): NotifyVisitToWebHooks
|
||||
{
|
||||
return new NotifyVisitToWebHooks(
|
||||
$this->httpClient,
|
||||
$this->em,
|
||||
$this->logger,
|
||||
new WebhookOptions(
|
||||
['webhooks' => $webhooks, 'notify_orphan_visits_to_webhooks' => $notifyOrphanVisits],
|
||||
),
|
||||
new ShortUrlDataTransformer(new ShortUrlStringifier([])),
|
||||
new AppOptions('Shlink', '1.2.3'),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -53,7 +53,6 @@ class PublishingUpdatesGeneratorTest extends TestCase
|
|||
'longUrl' => 'https://longUrl',
|
||||
'deviceLongUrls' => $shortUrl->deviceLongUrls(),
|
||||
'dateCreated' => $shortUrl->getDateCreated()->toAtomString(),
|
||||
'visitsCount' => 0,
|
||||
'tags' => [],
|
||||
'meta' => [
|
||||
'validSince' => null,
|
||||
|
@ -128,7 +127,6 @@ class PublishingUpdatesGeneratorTest extends TestCase
|
|||
'longUrl' => 'https://longUrl',
|
||||
'deviceLongUrls' => $shortUrl->deviceLongUrls(),
|
||||
'dateCreated' => $shortUrl->getDateCreated()->toAtomString(),
|
||||
'visitsCount' => 0,
|
||||
'tags' => [],
|
||||
'meta' => [
|
||||
'validSince' => null,
|
||||
|
|
|
@ -7,7 +7,6 @@ namespace ShlinkioTest\Shlink\Core\EventDispatcher\RabbitMq;
|
|||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use DomainException;
|
||||
use Exception;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
@ -24,7 +23,6 @@ use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
|||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Visit\Transformer\OrphanVisitDataTransformer;
|
||||
use Throwable;
|
||||
|
||||
use function array_walk;
|
||||
|
@ -132,9 +130,8 @@ class NotifyVisitToRabbitMqTest extends TestCase
|
|||
yield [new DomainException('DomainException Error')];
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideLegacyPayloads')]
|
||||
#[Test, DataProvider('providePayloads')]
|
||||
public function expectedPayloadIsPublishedDependingOnConfig(
|
||||
bool $legacy,
|
||||
Visit $visit,
|
||||
callable $setup,
|
||||
callable $expect,
|
||||
|
@ -144,44 +141,12 @@ class NotifyVisitToRabbitMqTest extends TestCase
|
|||
$setup($this->updatesGenerator);
|
||||
$expect($this->helper, $this->updatesGenerator);
|
||||
|
||||
($this->listener(new RabbitMqOptions(true, $legacy)))(new VisitLocated($visitId));
|
||||
($this->listener())(new VisitLocated($visitId));
|
||||
}
|
||||
|
||||
public static function provideLegacyPayloads(): iterable
|
||||
public static function providePayloads(): iterable
|
||||
{
|
||||
yield 'legacy non-orphan visit' => [
|
||||
true,
|
||||
$visit = Visit::forValidShortUrl(ShortUrl::withLongUrl('https://longUrl'), Visitor::emptyInstance()),
|
||||
static fn () => null,
|
||||
function (MockObject & PublishingHelperInterface $helper) use ($visit): void {
|
||||
$helper->method('publishUpdate')->with(self::callback(function (Update $update) use ($visit): bool {
|
||||
$payload = $update->payload;
|
||||
Assert::assertEquals($payload, $visit->jsonSerialize());
|
||||
Assert::assertArrayNotHasKey('visitedUrl', $payload);
|
||||
Assert::assertArrayNotHasKey('type', $payload);
|
||||
Assert::assertArrayNotHasKey('visit', $payload);
|
||||
Assert::assertArrayNotHasKey('shortUrl', $payload);
|
||||
|
||||
return true;
|
||||
}));
|
||||
},
|
||||
];
|
||||
yield 'legacy orphan visit' => [
|
||||
true,
|
||||
Visit::forBasePath(Visitor::emptyInstance()),
|
||||
static fn () => null,
|
||||
function (MockObject & PublishingHelperInterface $helper): void {
|
||||
$helper->method('publishUpdate')->with(self::callback(function (Update $update): bool {
|
||||
$payload = $update->payload;
|
||||
Assert::assertArrayHasKey('visitedUrl', $payload);
|
||||
Assert::assertArrayHasKey('type', $payload);
|
||||
|
||||
return true;
|
||||
}));
|
||||
},
|
||||
];
|
||||
yield 'non-legacy non-orphan visit' => [
|
||||
false,
|
||||
yield 'non-orphan visit' => [
|
||||
Visit::forValidShortUrl(ShortUrl::withLongUrl('https://longUrl'), Visitor::emptyInstance()),
|
||||
function (MockObject & PublishingUpdatesGeneratorInterface $updatesGenerator): void {
|
||||
$update = Update::forTopicAndPayload('', []);
|
||||
|
@ -195,8 +160,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
|
|||
$helper->expects(self::exactly(2))->method('publishUpdate')->with(self::isInstanceOf(Update::class));
|
||||
},
|
||||
];
|
||||
yield 'non-legacy orphan visit' => [
|
||||
false,
|
||||
yield 'orphan visit' => [
|
||||
Visit::forBasePath(Visitor::emptyInstance()),
|
||||
function (MockObject & PublishingUpdatesGeneratorInterface $updatesGenerator): void {
|
||||
$update = Update::forTopicAndPayload('', []);
|
||||
|
@ -217,8 +181,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
|
|||
$this->updatesGenerator,
|
||||
$this->em,
|
||||
$this->logger,
|
||||
new OrphanVisitDataTransformer(),
|
||||
$options ?? new RabbitMqOptions(enabled: true, legacyVisitsPublishing: false),
|
||||
$options ?? new RabbitMqOptions(enabled: true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\ShortUrl\Model;
|
||||
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode;
|
||||
|
||||
class ShortUrlModeTest extends TestCase
|
||||
{
|
||||
#[Test, DataProvider('provideModes')]
|
||||
public function deprecatedValuesAreProperlyParsed(string $mode, ?ShortUrlMode $expected): void
|
||||
{
|
||||
self::assertSame($expected, ShortUrlMode::tryDeprecated($mode));
|
||||
}
|
||||
|
||||
public static function provideModes(): iterable
|
||||
{
|
||||
yield 'invalid' => ['invalid', null];
|
||||
yield 'foo' => ['foo', null];
|
||||
yield 'loose' => ['loose', ShortUrlMode::LOOSE];
|
||||
yield 'loosely' => ['loosely', ShortUrlMode::LOOSE];
|
||||
yield 'strict' => ['strict', ShortUrlMode::STRICT];
|
||||
}
|
||||
}
|
|
@ -55,7 +55,6 @@ return [
|
|||
Middleware\ShortUrl\DefaultShortCodesLengthMiddleware::class => ConfigAbstractFactory::class,
|
||||
Middleware\ShortUrl\OverrideDomainMiddleware::class => ConfigAbstractFactory::class,
|
||||
Middleware\Mercure\NotConfiguredMercureErrorHandler::class => ConfigAbstractFactory::class,
|
||||
Middleware\ErrorHandler\BackwardsCompatibleProblemDetailsHandler::class => InvokableFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
|
|
|
@ -8,14 +8,11 @@ use Laminas\Diactoros\Response\JsonResponse;
|
|||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtilsTrait;
|
||||
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
||||
use Shlinkio\Shlink\Core\Tag\Model\TagsParams;
|
||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
|
||||
|
||||
use function array_map;
|
||||
|
||||
class ListTagsAction extends AbstractRestAction
|
||||
{
|
||||
use PagerfantaUtilsTrait;
|
||||
|
@ -32,17 +29,8 @@ class ListTagsAction extends AbstractRestAction
|
|||
$params = TagsParams::fromRawData($request->getQueryParams());
|
||||
$apiKey = AuthenticationMiddleware::apiKeyFromRequest($request);
|
||||
|
||||
if (! $params->withStats) {
|
||||
return new JsonResponse([
|
||||
'tags' => $this->serializePaginator($this->tagService->listTags($params, $apiKey)),
|
||||
]);
|
||||
}
|
||||
|
||||
// This part is deprecated. To get tags with stats, the /tags/stats endpoint should be used instead
|
||||
$tagsInfo = $this->tagService->tagsInfo($params, $apiKey);
|
||||
$rawTags = $this->serializePaginator($tagsInfo, dataProp: 'stats');
|
||||
$rawTags['data'] = array_map(static fn (TagInfo $info) => $info->tag, [...$tagsInfo]);
|
||||
|
||||
return new JsonResponse(['tags' => $rawTags]);
|
||||
return new JsonResponse([
|
||||
'tags' => $this->serializePaginator($this->tagService->listTags($params, $apiKey)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,8 +40,8 @@ enum Role: string
|
|||
|
||||
public static function toSpec(ApiKeyRole $role, ?string $context = null): Specification
|
||||
{
|
||||
return match ($role->role()) {
|
||||
self::AUTHORED_SHORT_URLS => new BelongsToApiKey($role->apiKey(), $context),
|
||||
return match ($role->role) {
|
||||
self::AUTHORED_SHORT_URLS => new BelongsToApiKey($role->apiKey, $context),
|
||||
self::DOMAIN_SPECIFIC => new BelongsToDomain(self::domainIdFromMeta($role->meta()), $context),
|
||||
default => Spec::andX(),
|
||||
};
|
||||
|
@ -49,8 +49,8 @@ enum Role: string
|
|||
|
||||
public static function toInlinedSpec(ApiKeyRole $role): Specification
|
||||
{
|
||||
return match ($role->role()) {
|
||||
self::AUTHORED_SHORT_URLS => Spec::andX(new BelongsToApiKeyInlined($role->apiKey())),
|
||||
return match ($role->role) {
|
||||
self::AUTHORED_SHORT_URLS => Spec::andX(new BelongsToApiKeyInlined($role->apiKey)),
|
||||
self::DOMAIN_SPECIFIC => Spec::andX(new BelongsToDomainInlined(self::domainIdFromMeta($role->meta()))),
|
||||
default => Spec::andX(),
|
||||
};
|
||||
|
|
|
@ -156,7 +156,7 @@ class ApiKey extends AbstractEntity
|
|||
*/
|
||||
public function mapRoles(callable $fun): array
|
||||
{
|
||||
return $this->roles->map(fn (ApiKeyRole $role) => $fun($role->role(), $role->meta()))->getValues();
|
||||
return $this->roles->map(fn (ApiKeyRole $role) => $fun($role->role, $role->meta()))->getValues();
|
||||
}
|
||||
|
||||
public function registerRole(RoleDefinition $roleDefinition): void
|
||||
|
|
|
@ -13,22 +13,6 @@ class ApiKeyRole extends AbstractEntity
|
|||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use property access directly
|
||||
*/
|
||||
public function role(): Role
|
||||
{
|
||||
return $this->role;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use property access directly
|
||||
*/
|
||||
public function apiKey(): ApiKey
|
||||
{
|
||||
return $this->apiKey;
|
||||
}
|
||||
|
||||
public function meta(): array
|
||||
{
|
||||
return $this->meta;
|
||||
|
|
|
@ -1,99 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest\Exception;
|
||||
|
||||
use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface;
|
||||
use Shlinkio\Shlink\Core\Exception\DeleteShortUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\DomainNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Exception\ForbiddenTagOperationException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Exception\TagConflictException;
|
||||
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
|
||||
use function end;
|
||||
use function explode;
|
||||
|
||||
/** @deprecated */
|
||||
class BackwardsCompatibleProblemDetailsException extends RuntimeException implements ProblemDetailsExceptionInterface
|
||||
{
|
||||
private function __construct(private readonly ProblemDetailsExceptionInterface $e)
|
||||
{
|
||||
parent::__construct($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
|
||||
public static function fromProblemDetails(ProblemDetailsExceptionInterface $e): self
|
||||
{
|
||||
return new self($e);
|
||||
}
|
||||
|
||||
public function getStatus(): int
|
||||
{
|
||||
return $this->e->getStatus();
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->remapType($this->e->getType());
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->e->getTitle();
|
||||
}
|
||||
|
||||
public function getDetail(): string
|
||||
{
|
||||
return $this->e->getDetail();
|
||||
}
|
||||
|
||||
public function getAdditionalData(): array
|
||||
{
|
||||
return $this->e->getAdditionalData();
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->remapTypeInArray($this->e->toArray());
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->remapTypeInArray($this->e->jsonSerialize());
|
||||
}
|
||||
|
||||
private function remapTypeInArray(array $wrappedArray): array
|
||||
{
|
||||
if (! isset($wrappedArray['type'])) {
|
||||
return $wrappedArray;
|
||||
}
|
||||
|
||||
return [...$wrappedArray, 'type' => $this->remapType($wrappedArray['type'])];
|
||||
}
|
||||
|
||||
private function remapType(string $wrappedType): string
|
||||
{
|
||||
$segments = explode('/', $wrappedType);
|
||||
$lastSegment = end($segments);
|
||||
|
||||
return match ($lastSegment) {
|
||||
ValidationException::ERROR_CODE => 'INVALID_ARGUMENT',
|
||||
DeleteShortUrlException::ERROR_CODE => 'INVALID_SHORT_URL_DELETION',
|
||||
DomainNotFoundException::ERROR_CODE => 'DOMAIN_NOT_FOUND',
|
||||
ForbiddenTagOperationException::ERROR_CODE => 'FORBIDDEN_OPERATION',
|
||||
InvalidUrlException::ERROR_CODE => 'INVALID_URL',
|
||||
NonUniqueSlugException::ERROR_CODE => 'INVALID_SLUG',
|
||||
ShortUrlNotFoundException::ERROR_CODE => 'INVALID_SHORTCODE',
|
||||
TagConflictException::ERROR_CODE => 'TAG_CONFLICT',
|
||||
TagNotFoundException::ERROR_CODE => 'TAG_NOT_FOUND',
|
||||
MercureException::ERROR_CODE => 'MERCURE_NOT_CONFIGURED',
|
||||
MissingAuthenticationException::ERROR_CODE => 'INVALID_AUTHORIZATION',
|
||||
VerifyAuthenticationException::ERROR_CODE => 'INVALID_API_KEY',
|
||||
default => $wrappedType,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest\Middleware\ErrorHandler;
|
||||
|
||||
use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Rest\Exception\BackwardsCompatibleProblemDetailsException;
|
||||
|
||||
use function version_compare;
|
||||
|
||||
/** @deprecated */
|
||||
class BackwardsCompatibleProblemDetailsHandler implements MiddlewareInterface
|
||||
{
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
try {
|
||||
return $handler->handle($request);
|
||||
} catch (ProblemDetailsExceptionInterface $e) {
|
||||
$version = $request->getAttribute('version') ?? '2';
|
||||
throw version_compare($version, '3', '>=')
|
||||
? $e
|
||||
: BackwardsCompatibleProblemDetailsException::fromProblemDetails($e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ class CreateShortUrlTest extends ApiTestCase
|
|||
#[Test]
|
||||
public function createsNewShortUrlWhenOnlyLongUrlIsProvided(): void
|
||||
{
|
||||
$expectedKeys = ['shortCode', 'shortUrl', 'longUrl', 'dateCreated', 'visitsCount', 'tags'];
|
||||
$expectedKeys = ['shortCode', 'shortUrl', 'longUrl', 'dateCreated', 'tags'];
|
||||
[$statusCode, $payload] = $this->createShortUrl();
|
||||
|
||||
self::assertEquals(self::STATUS_OK, $statusCode);
|
||||
|
@ -48,7 +48,7 @@ class CreateShortUrlTest extends ApiTestCase
|
|||
self::assertEquals(self::STATUS_BAD_REQUEST, $statusCode);
|
||||
self::assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
|
||||
self::assertEquals($detail, $payload['detail']);
|
||||
self::assertEquals('INVALID_SLUG', $payload['type']);
|
||||
self::assertEquals('https://shlink.io/api/error/non-unique-slug', $payload['type']);
|
||||
self::assertEquals('Invalid custom slug', $payload['title']);
|
||||
self::assertEquals($slug, $payload['customSlug']);
|
||||
|
||||
|
@ -70,8 +70,8 @@ class CreateShortUrlTest extends ApiTestCase
|
|||
|
||||
public static function provideDuplicatedSlugApiVersions(): iterable
|
||||
{
|
||||
yield ['1', 'INVALID_SLUG'];
|
||||
yield ['2', 'INVALID_SLUG'];
|
||||
yield ['1', 'https://shlink.io/api/error/non-unique-slug'];
|
||||
yield ['2', 'https://shlink.io/api/error/non-unique-slug'];
|
||||
yield ['3', 'https://shlink.io/api/error/non-unique-slug'];
|
||||
}
|
||||
|
||||
|
@ -241,7 +241,7 @@ class CreateShortUrlTest extends ApiTestCase
|
|||
|
||||
public static function provideInvalidUrls(): iterable
|
||||
{
|
||||
yield 'API version 2' => ['https://this-has-to-be-invalid.com', '2', 'INVALID_URL'];
|
||||
yield 'API version 2' => ['https://this-has-to-be-invalid.com', '2', 'https://shlink.io/api/error/invalid-url'];
|
||||
yield 'API version 3' => ['https://this-has-to-be-invalid.com', '3', 'https://shlink.io/api/error/invalid-url'];
|
||||
}
|
||||
|
||||
|
@ -264,18 +264,18 @@ class CreateShortUrlTest extends ApiTestCase
|
|||
|
||||
public static function provideInvalidArgumentApiVersions(): iterable
|
||||
{
|
||||
yield 'missing long url v2' => [[], '2', 'INVALID_ARGUMENT'];
|
||||
yield 'missing long url v2' => [[], '2', 'https://shlink.io/api/error/invalid-data'];
|
||||
yield 'missing long url v3' => [[], '3', 'https://shlink.io/api/error/invalid-data'];
|
||||
yield 'empty long url v2' => [['longUrl' => null], '2', 'INVALID_ARGUMENT'];
|
||||
yield 'empty long url v2' => [['longUrl' => null], '2', 'https://shlink.io/api/error/invalid-data'];
|
||||
yield 'empty long url v3' => [['longUrl' => ' '], '3', 'https://shlink.io/api/error/invalid-data'];
|
||||
yield 'missing url schema v2' => [['longUrl' => 'foo.com'], '2', 'INVALID_ARGUMENT'];
|
||||
yield 'missing url schema v2' => [['longUrl' => 'foo.com'], '2', 'https://shlink.io/api/error/invalid-data'];
|
||||
yield 'missing url schema v3' => [['longUrl' => 'foo.com'], '3', 'https://shlink.io/api/error/invalid-data'];
|
||||
yield 'empty device long url v2' => [[
|
||||
'longUrl' => 'foo',
|
||||
'deviceLongUrls' => [
|
||||
'android' => null,
|
||||
],
|
||||
], '2', 'INVALID_ARGUMENT'];
|
||||
], '2', 'https://shlink.io/api/error/invalid-data'];
|
||||
yield 'empty device long url v3' => [[
|
||||
'longUrl' => 'foo',
|
||||
'deviceLongUrls' => [
|
||||
|
|
|
@ -31,7 +31,7 @@ class DeleteShortUrlTest extends ApiTestCase
|
|||
|
||||
self::assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode());
|
||||
self::assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
|
||||
self::assertEquals('INVALID_SHORTCODE', $payload['type']);
|
||||
self::assertEquals('https://shlink.io/api/error/short-url-not-found', $payload['type']);
|
||||
self::assertEquals($expectedDetail, $payload['detail']);
|
||||
self::assertEquals('Short URL not found', $payload['title']);
|
||||
self::assertEquals($shortCode, $payload['shortCode']);
|
||||
|
@ -52,8 +52,8 @@ class DeleteShortUrlTest extends ApiTestCase
|
|||
|
||||
public static function provideApiVersions(): iterable
|
||||
{
|
||||
yield ['1', 'INVALID_SHORTCODE'];
|
||||
yield ['2', 'INVALID_SHORTCODE'];
|
||||
yield ['1', 'https://shlink.io/api/error/short-url-not-found'];
|
||||
yield ['2', 'https://shlink.io/api/error/short-url-not-found'];
|
||||
yield ['3', 'https://shlink.io/api/error/short-url-not-found'];
|
||||
}
|
||||
|
||||
|
|
|
@ -30,8 +30,8 @@ class DeleteTagsTest extends ApiTestCase
|
|||
|
||||
public static function provideNonAdminApiKeys(): iterable
|
||||
{
|
||||
yield 'author' => ['author_api_key', '2', 'FORBIDDEN_OPERATION'];
|
||||
yield 'domain' => ['domain_api_key', '2', 'FORBIDDEN_OPERATION'];
|
||||
yield 'author' => ['author_api_key', '2', 'https://shlink.io/api/error/forbidden-tag-operation'];
|
||||
yield 'domain' => ['domain_api_key', '2', 'https://shlink.io/api/error/forbidden-tag-operation'];
|
||||
yield 'version 3' => ['domain_api_key', '3', 'https://shlink.io/api/error/forbidden-tag-operation'];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ class DomainRedirectsTest extends ApiTestCase
|
|||
|
||||
self::assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode());
|
||||
self::assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
|
||||
self::assertEquals('INVALID_ARGUMENT', $payload['type']);
|
||||
self::assertEquals('https://shlink.io/api/error/invalid-data', $payload['type']);
|
||||
self::assertEquals('Provided data is not valid', $payload['detail']);
|
||||
self::assertEquals('Invalid data', $payload['title']);
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ class DomainVisitsTest extends ApiTestCase
|
|||
|
||||
self::assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode());
|
||||
self::assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
|
||||
self::assertEquals('DOMAIN_NOT_FOUND', $payload['type']);
|
||||
self::assertEquals('https://shlink.io/api/error/domain-not-found', $payload['type']);
|
||||
self::assertEquals(sprintf('Domain with authority "%s" could not be found', $domain), $payload['detail']);
|
||||
self::assertEquals('Domain not found', $payload['title']);
|
||||
self::assertEquals($domain, $payload['authority']);
|
||||
|
@ -73,8 +73,8 @@ class DomainVisitsTest extends ApiTestCase
|
|||
|
||||
public static function provideApiVersions(): iterable
|
||||
{
|
||||
yield ['1', 'DOMAIN_NOT_FOUND'];
|
||||
yield ['2', 'DOMAIN_NOT_FOUND'];
|
||||
yield ['1', 'https://shlink.io/api/error/domain-not-found'];
|
||||
yield ['2', 'https://shlink.io/api/error/domain-not-found'];
|
||||
yield ['3', 'https://shlink.io/api/error/domain-not-found'];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,7 +96,7 @@ class EditShortUrlTest extends ApiTestCase
|
|||
public static function provideLongUrls(): iterable
|
||||
{
|
||||
yield 'valid URL' => ['https://shlink.io', self::STATUS_OK, null];
|
||||
yield 'invalid URL' => ['http://foo', self::STATUS_BAD_REQUEST, 'INVALID_URL'];
|
||||
yield 'invalid URL' => ['http://foo', self::STATUS_BAD_REQUEST, 'https://shlink.io/api/error/invalid-url'];
|
||||
}
|
||||
|
||||
#[Test, DataProviderExternal(ApiTestDataProviders::class, 'invalidUrlsProvider')]
|
||||
|
@ -112,7 +112,7 @@ class EditShortUrlTest extends ApiTestCase
|
|||
|
||||
self::assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode());
|
||||
self::assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
|
||||
self::assertEquals('INVALID_SHORTCODE', $payload['type']);
|
||||
self::assertEquals('https://shlink.io/api/error/short-url-not-found', $payload['type']);
|
||||
self::assertEquals($expectedDetail, $payload['detail']);
|
||||
self::assertEquals('Short URL not found', $payload['title']);
|
||||
self::assertEquals($shortCode, $payload['shortCode']);
|
||||
|
@ -131,7 +131,7 @@ class EditShortUrlTest extends ApiTestCase
|
|||
|
||||
self::assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode());
|
||||
self::assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
|
||||
self::assertEquals('INVALID_ARGUMENT', $payload['type']);
|
||||
self::assertEquals('https://shlink.io/api/error/invalid-data', $payload['type']);
|
||||
self::assertEquals($expectedDetail, $payload['detail']);
|
||||
self::assertEquals('Invalid data', $payload['title']);
|
||||
}
|
||||
|
|
|
@ -17,10 +17,8 @@ class GlobalVisitsTest extends ApiTestCase
|
|||
$payload = $this->getJsonResponsePayload($resp);
|
||||
|
||||
self::assertArrayHasKey('visits', $payload);
|
||||
self::assertArrayHasKey('visitsCount', $payload['visits']);
|
||||
self::assertArrayHasKey('orphanVisitsCount', $payload['visits']);
|
||||
self::assertEquals($expectedVisits, $payload['visits']['visitsCount']);
|
||||
self::assertEquals($expectedOrphanVisits, $payload['visits']['orphanVisitsCount']);
|
||||
self::assertEquals($expectedVisits, $payload['visits']['nonOrphanVisits']['total']);
|
||||
self::assertEquals($expectedOrphanVisits, $payload['visits']['orphanVisits']['total']);
|
||||
}
|
||||
|
||||
public static function provideApiKeys(): iterable
|
||||
|
|
|
@ -20,7 +20,6 @@ class ListShortUrlsTest extends ApiTestCase
|
|||
'shortUrl' => 'http://s.test/abc123',
|
||||
'longUrl' => 'https://shlink.io',
|
||||
'dateCreated' => '2018-05-01T00:00:00+00:00',
|
||||
'visitsCount' => 3,
|
||||
'visitsSummary' => [
|
||||
'total' => 3,
|
||||
'nonBots' => 3,
|
||||
|
@ -42,7 +41,6 @@ class ListShortUrlsTest extends ApiTestCase
|
|||
'shortUrl' => 'http://s.test/ghi789',
|
||||
'longUrl' => 'https://shlink.io/documentation/',
|
||||
'dateCreated' => '2018-05-01T00:00:00+00:00',
|
||||
'visitsCount' => 2,
|
||||
'visitsSummary' => [
|
||||
'total' => 2,
|
||||
'nonBots' => 2,
|
||||
|
@ -64,7 +62,6 @@ class ListShortUrlsTest extends ApiTestCase
|
|||
'shortUrl' => 'http://some-domain.com/custom-with-domain',
|
||||
'longUrl' => 'https://google.com',
|
||||
'dateCreated' => '2018-10-20T00:00:00+00:00',
|
||||
'visitsCount' => 0,
|
||||
'visitsSummary' => [
|
||||
'total' => 0,
|
||||
'nonBots' => 0,
|
||||
|
@ -88,7 +85,6 @@ class ListShortUrlsTest extends ApiTestCase
|
|||
'https://blog.alejandrocelaya.com/2017/12/09'
|
||||
. '/acmailer-7-0-the-most-important-release-in-a-long-time/',
|
||||
'dateCreated' => '2019-01-01T00:00:10+00:00',
|
||||
'visitsCount' => 2,
|
||||
'visitsSummary' => [
|
||||
'total' => 2,
|
||||
'nonBots' => 1,
|
||||
|
@ -110,7 +106,6 @@ class ListShortUrlsTest extends ApiTestCase
|
|||
'shortUrl' => 'http://s.test/custom',
|
||||
'longUrl' => 'https://shlink.io',
|
||||
'dateCreated' => '2019-01-01T00:00:20+00:00',
|
||||
'visitsCount' => 0,
|
||||
'visitsSummary' => [
|
||||
'total' => 0,
|
||||
'nonBots' => 0,
|
||||
|
@ -134,7 +129,6 @@ class ListShortUrlsTest extends ApiTestCase
|
|||
'https://blog.alejandrocelaya.com/2019/04/27'
|
||||
. '/considerations-to-properly-use-open-source-software-projects/',
|
||||
'dateCreated' => '2019-01-01T00:00:30+00:00',
|
||||
'visitsCount' => 0,
|
||||
'visitsSummary' => [
|
||||
'total' => 0,
|
||||
'nonBots' => 0,
|
||||
|
@ -310,7 +304,7 @@ class ListShortUrlsTest extends ApiTestCase
|
|||
self::assertEquals([
|
||||
'invalidElements' => $expectedInvalidElements,
|
||||
'title' => 'Invalid data',
|
||||
'type' => 'INVALID_ARGUMENT',
|
||||
'type' => 'https://shlink.io/api/error/invalid-data',
|
||||
'status' => 400,
|
||||
'detail' => 'Provided data is not valid',
|
||||
], $respPayload);
|
||||
|
|
|
@ -24,7 +24,7 @@ class RenameTagTest extends ApiTestCase
|
|||
|
||||
self::assertEquals(self::STATUS_FORBIDDEN, $resp->getStatusCode());
|
||||
self::assertEquals(self::STATUS_FORBIDDEN, $payload['status']);
|
||||
self::assertEquals('FORBIDDEN_OPERATION', $payload['type']);
|
||||
self::assertEquals('https://shlink.io/api/error/forbidden-tag-operation', $payload['type']);
|
||||
self::assertEquals('You are not allowed to rename tags', $payload['detail']);
|
||||
self::assertEquals('Forbidden tag operation', $payload['title']);
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ class ResolveShortUrlTest extends ApiTestCase
|
|||
|
||||
self::assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode());
|
||||
self::assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
|
||||
self::assertEquals('INVALID_SHORTCODE', $payload['type']);
|
||||
self::assertEquals('https://shlink.io/api/error/short-url-not-found', $payload['type']);
|
||||
self::assertEquals($expectedDetail, $payload['detail']);
|
||||
self::assertEquals('Short URL not found', $payload['title']);
|
||||
self::assertEquals($shortCode, $payload['shortCode']);
|
||||
|
|
|
@ -34,7 +34,7 @@ class ShortUrlVisitsTest extends ApiTestCase
|
|||
|
||||
self::assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode());
|
||||
self::assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
|
||||
self::assertEquals('INVALID_SHORTCODE', $payload['type']);
|
||||
self::assertEquals('https://shlink.io/api/error/short-url-not-found', $payload['type']);
|
||||
self::assertEquals($expectedDetail, $payload['detail']);
|
||||
self::assertEquals('Short URL not found', $payload['title']);
|
||||
self::assertEquals($shortCode, $payload['shortCode']);
|
||||
|
|
|
@ -38,7 +38,7 @@ class SingleStepCreateShortUrlTest extends ApiTestCase
|
|||
|
||||
self::assertEquals(self::STATUS_UNAUTHORIZED, $resp->getStatusCode());
|
||||
self::assertEquals(self::STATUS_UNAUTHORIZED, $payload['status']);
|
||||
self::assertEquals('INVALID_AUTHORIZATION', $payload['type']);
|
||||
self::assertEquals('https://shlink.io/api/error/missing-authentication', $payload['type']);
|
||||
self::assertEquals($expectedDetail, $payload['detail']);
|
||||
self::assertEquals('Invalid authorization', $payload['title']);
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ class TagVisitsTest extends ApiTestCase
|
|||
|
||||
self::assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode());
|
||||
self::assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
|
||||
self::assertEquals('TAG_NOT_FOUND', $payload['type']);
|
||||
self::assertEquals('https://shlink.io/api/error/tag-not-found', $payload['type']);
|
||||
self::assertEquals(sprintf('Tag with name "%s" could not be found', $tag), $payload['detail']);
|
||||
self::assertEquals('Tag not found', $payload['title']);
|
||||
}
|
||||
|
|
|
@ -25,29 +25,12 @@ class TagsStatsTest extends ApiTestCase
|
|||
self::assertEquals($expectedPagination, $tags['pagination']);
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideQueries')]
|
||||
public function expectedListOfTagsIsReturnedForDeprecatedApproach(
|
||||
string $apiKey,
|
||||
array $query,
|
||||
array $expectedStats,
|
||||
array $expectedPagination,
|
||||
): void {
|
||||
$query['withStats'] = 'true';
|
||||
$resp = $this->callApiWithKey(self::METHOD_GET, '/tags', [RequestOptions::QUERY => $query], $apiKey);
|
||||
['tags' => $tags] = $this->getJsonResponsePayload($resp);
|
||||
|
||||
self::assertEquals($expectedStats, $tags['stats']);
|
||||
self::assertEquals($expectedPagination, $tags['pagination']);
|
||||
self::assertArrayHasKey('data', $tags);
|
||||
}
|
||||
|
||||
public static function provideQueries(): iterable
|
||||
{
|
||||
yield 'admin API key' => ['valid_api_key', [], [
|
||||
[
|
||||
'tag' => 'bar',
|
||||
'shortUrlsCount' => 1,
|
||||
'visitsCount' => 2,
|
||||
'visitsSummary' => [
|
||||
'total' => 2,
|
||||
'nonBots' => 1,
|
||||
|
@ -57,7 +40,6 @@ class TagsStatsTest extends ApiTestCase
|
|||
[
|
||||
'tag' => 'baz',
|
||||
'shortUrlsCount' => 0,
|
||||
'visitsCount' => 0,
|
||||
'visitsSummary' => [
|
||||
'total' => 0,
|
||||
'nonBots' => 0,
|
||||
|
@ -67,7 +49,6 @@ class TagsStatsTest extends ApiTestCase
|
|||
[
|
||||
'tag' => 'foo',
|
||||
'shortUrlsCount' => 3,
|
||||
'visitsCount' => 5,
|
||||
'visitsSummary' => [
|
||||
'total' => 5,
|
||||
'nonBots' => 4,
|
||||
|
@ -85,7 +66,6 @@ class TagsStatsTest extends ApiTestCase
|
|||
[
|
||||
'tag' => 'bar',
|
||||
'shortUrlsCount' => 1,
|
||||
'visitsCount' => 2,
|
||||
'visitsSummary' => [
|
||||
'total' => 2,
|
||||
'nonBots' => 1,
|
||||
|
@ -95,7 +75,6 @@ class TagsStatsTest extends ApiTestCase
|
|||
[
|
||||
'tag' => 'baz',
|
||||
'shortUrlsCount' => 0,
|
||||
'visitsCount' => 0,
|
||||
'visitsSummary' => [
|
||||
'total' => 0,
|
||||
'nonBots' => 0,
|
||||
|
@ -113,7 +92,6 @@ class TagsStatsTest extends ApiTestCase
|
|||
[
|
||||
'tag' => 'bar',
|
||||
'shortUrlsCount' => 1,
|
||||
'visitsCount' => 2,
|
||||
'visitsSummary' => [
|
||||
'total' => 2,
|
||||
'nonBots' => 1,
|
||||
|
@ -123,7 +101,6 @@ class TagsStatsTest extends ApiTestCase
|
|||
[
|
||||
'tag' => 'foo',
|
||||
'shortUrlsCount' => 2,
|
||||
'visitsCount' => 5,
|
||||
'visitsSummary' => [
|
||||
'total' => 5,
|
||||
'nonBots' => 4,
|
||||
|
@ -141,7 +118,6 @@ class TagsStatsTest extends ApiTestCase
|
|||
[
|
||||
'tag' => 'foo',
|
||||
'shortUrlsCount' => 2,
|
||||
'visitsCount' => 5,
|
||||
'visitsSummary' => [
|
||||
'total' => 5,
|
||||
'nonBots' => 4,
|
||||
|
@ -159,7 +135,6 @@ class TagsStatsTest extends ApiTestCase
|
|||
[
|
||||
'tag' => 'foo',
|
||||
'shortUrlsCount' => 1,
|
||||
'visitsCount' => 0,
|
||||
'visitsSummary' => [
|
||||
'total' => 0,
|
||||
'nonBots' => 0,
|
||||
|
|
|
@ -23,7 +23,7 @@ class UpdateTagTest extends ApiTestCase
|
|||
|
||||
self::assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode());
|
||||
self::assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
|
||||
self::assertEquals('INVALID_ARGUMENT', $payload['type']);
|
||||
self::assertEquals('https://shlink.io/api/error/invalid-data', $payload['type']);
|
||||
self::assertEquals($expectedDetail, $payload['detail']);
|
||||
self::assertEquals('Invalid data', $payload['title']);
|
||||
}
|
||||
|
@ -55,8 +55,8 @@ class UpdateTagTest extends ApiTestCase
|
|||
|
||||
public static function provideTagNotFoundApiVersions(): iterable
|
||||
{
|
||||
yield 'version 1' => ['1', 'TAG_NOT_FOUND'];
|
||||
yield 'version 2' => ['2', 'TAG_NOT_FOUND'];
|
||||
yield 'version 1' => ['1', 'https://shlink.io/api/error/tag-not-found'];
|
||||
yield 'version 2' => ['2', 'https://shlink.io/api/error/tag-not-found'];
|
||||
yield 'version 3' => ['3', 'https://shlink.io/api/error/tag-not-found'];
|
||||
}
|
||||
|
||||
|
@ -80,8 +80,8 @@ class UpdateTagTest extends ApiTestCase
|
|||
|
||||
public static function provideTagConflictsApiVersions(): iterable
|
||||
{
|
||||
yield 'version 1' => ['1', 'TAG_CONFLICT'];
|
||||
yield 'version 2' => ['2', 'TAG_CONFLICT'];
|
||||
yield 'version 1' => ['1', 'https://shlink.io/api/error/tag-conflict'];
|
||||
yield 'version 2' => ['2', 'https://shlink.io/api/error/tag-conflict'];
|
||||
yield 'version 3' => ['3', 'https://shlink.io/api/error/tag-conflict'];
|
||||
}
|
||||
|
||||
|
|
|
@ -32,8 +32,6 @@ class VisitStatsTest extends ApiTestCase
|
|||
'nonBots' => 2,
|
||||
'bots' => 1,
|
||||
],
|
||||
'visitsCount' => 7,
|
||||
'orphanVisitsCount' => 3,
|
||||
]];
|
||||
yield 'domain-only API key' => ['domain_api_key', [
|
||||
'nonOrphanVisits' => [
|
||||
|
@ -46,8 +44,6 @@ class VisitStatsTest extends ApiTestCase
|
|||
'nonBots' => 2,
|
||||
'bots' => 1,
|
||||
],
|
||||
'visitsCount' => 0,
|
||||
'orphanVisitsCount' => 3,
|
||||
]];
|
||||
yield 'author API key' => ['author_api_key', [
|
||||
'nonOrphanVisits' => [
|
||||
|
@ -60,8 +56,6 @@ class VisitStatsTest extends ApiTestCase
|
|||
'nonBots' => 2,
|
||||
'bots' => 1,
|
||||
],
|
||||
'visitsCount' => 5,
|
||||
'orphanVisitsCount' => 3,
|
||||
]];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,8 +29,8 @@ class AuthenticationTest extends ApiTestCase
|
|||
|
||||
public static function provideApiVersions(): iterable
|
||||
{
|
||||
yield 'version 1' => ['1', 'INVALID_AUTHORIZATION'];
|
||||
yield 'version 2' => ['2', 'INVALID_AUTHORIZATION'];
|
||||
yield 'version 1' => ['1', 'https://shlink.io/api/error/missing-authentication'];
|
||||
yield 'version 2' => ['2', 'https://shlink.io/api/error/missing-authentication'];
|
||||
yield 'version 3' => ['3', 'https://shlink.io/api/error/missing-authentication'];
|
||||
}
|
||||
|
||||
|
@ -58,9 +58,9 @@ class AuthenticationTest extends ApiTestCase
|
|||
|
||||
public static function provideInvalidApiKeys(): iterable
|
||||
{
|
||||
yield 'key which does not exist' => ['invalid', '2', 'INVALID_API_KEY'];
|
||||
yield 'key which is expired' => ['expired_api_key', '2', 'INVALID_API_KEY'];
|
||||
yield 'key which is disabled' => ['disabled_api_key', '2', 'INVALID_API_KEY'];
|
||||
yield 'key which does not exist' => ['invalid', '2', 'https://shlink.io/api/error/invalid-api-key'];
|
||||
yield 'key which is expired' => ['expired_api_key', '2', 'https://shlink.io/api/error/invalid-api-key'];
|
||||
yield 'key which is disabled' => ['disabled_api_key', '2', 'https://shlink.io/api/error/invalid-api-key'];
|
||||
yield 'version 3' => ['disabled_api_key', '3', 'https://shlink.io/api/error/invalid-api-key'];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,14 +7,12 @@ namespace ShlinkioTest\Shlink\Rest\Action\Tag;
|
|||
use Laminas\Diactoros\Response\JsonResponse;
|
||||
use Laminas\Diactoros\ServerRequestFactory;
|
||||
use Pagerfanta\Adapter\ArrayAdapter;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||
use Shlinkio\Shlink\Core\Tag\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\Tag\ListTagsAction;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
@ -32,8 +30,8 @@ class ListTagsActionTest extends TestCase
|
|||
$this->action = new ListTagsAction($this->tagService);
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideNoStatsQueries')]
|
||||
public function returnsBaseDataWhenStatsAreNotRequested(array $query): void
|
||||
#[Test]
|
||||
public function returnsBaseDataWhenStatsAreNotRequested(): void
|
||||
{
|
||||
$tags = [new Tag('foo'), new Tag('bar')];
|
||||
$tagsCount = count($tags);
|
||||
|
@ -43,7 +41,7 @@ class ListTagsActionTest extends TestCase
|
|||
)->willReturn(new Paginator(new ArrayAdapter($tags)));
|
||||
|
||||
/** @var JsonResponse $resp */
|
||||
$resp = $this->action->handle($this->requestWithApiKey()->withQueryParams($query));
|
||||
$resp = $this->action->handle($this->requestWithApiKey());
|
||||
$payload = $resp->getPayload();
|
||||
|
||||
self::assertEquals([
|
||||
|
@ -60,46 +58,6 @@ class ListTagsActionTest extends TestCase
|
|||
], $payload);
|
||||
}
|
||||
|
||||
public static function provideNoStatsQueries(): iterable
|
||||
{
|
||||
yield 'no query' => [[]];
|
||||
yield 'withStats is false' => [['withStats' => 'withStats']];
|
||||
yield 'withStats is something else' => [['withStats' => 'foo']];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function returnsStatsWhenRequested(): void
|
||||
{
|
||||
$stats = [
|
||||
new TagInfo('foo', 1, 1),
|
||||
new TagInfo('bar', 3, 10),
|
||||
];
|
||||
$itemsCount = count($stats);
|
||||
$this->tagService->expects($this->once())->method('tagsInfo')->with(
|
||||
$this->anything(),
|
||||
$this->isInstanceOf(ApiKey::class),
|
||||
)->willReturn(new Paginator(new ArrayAdapter($stats)));
|
||||
$req = $this->requestWithApiKey()->withQueryParams(['withStats' => 'true']);
|
||||
|
||||
/** @var JsonResponse $resp */
|
||||
$resp = $this->action->handle($req);
|
||||
$payload = $resp->getPayload();
|
||||
|
||||
self::assertEquals([
|
||||
'tags' => [
|
||||
'data' => ['foo', 'bar'],
|
||||
'stats' => $stats,
|
||||
'pagination' => [
|
||||
'currentPage' => 1,
|
||||
'pagesCount' => 1,
|
||||
'itemsPerPage' => 10,
|
||||
'itemsInCurrentPage' => $itemsCount,
|
||||
'totalItems' => $itemsCount,
|
||||
],
|
||||
],
|
||||
], $payload);
|
||||
}
|
||||
|
||||
private function requestWithApiKey(): ServerRequestInterface
|
||||
{
|
||||
return ServerRequestFactory::fromGlobals()->withAttribute(ApiKey::class, ApiKey::create());
|
||||
|
|
|
@ -1,113 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Rest\Exception;
|
||||
|
||||
use Exception;
|
||||
use Mezzio\ProblemDetails\Exception\ProblemDetailsExceptionInterface;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Exception\DeleteShortUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\DomainNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Exception\ForbiddenTagOperationException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Exception\TagConflictException;
|
||||
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
use Shlinkio\Shlink\Rest\Exception\BackwardsCompatibleProblemDetailsException;
|
||||
use Shlinkio\Shlink\Rest\Exception\MercureException;
|
||||
use Shlinkio\Shlink\Rest\Exception\MissingAuthenticationException;
|
||||
use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException;
|
||||
|
||||
class BackwardsCompatibleProblemDetailsExceptionTest extends TestCase
|
||||
{
|
||||
#[Test, DataProvider('provideTypes')]
|
||||
public function typeIsRemappedOnWrappedException(
|
||||
string $wrappedType,
|
||||
string $expectedType,
|
||||
bool $expectSameType = false,
|
||||
): void {
|
||||
$original = new class ($wrappedType) extends Exception implements ProblemDetailsExceptionInterface {
|
||||
public function __construct(private readonly string $type)
|
||||
{
|
||||
parent::__construct('');
|
||||
}
|
||||
|
||||
public function getStatus(): int
|
||||
{
|
||||
return 123;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'title';
|
||||
}
|
||||
|
||||
public function getDetail(): string
|
||||
{
|
||||
return 'detail';
|
||||
}
|
||||
|
||||
public function getAdditionalData(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return ['type' => $this->type];
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return ['type' => $this->type];
|
||||
}
|
||||
};
|
||||
$e = BackwardsCompatibleProblemDetailsException::fromProblemDetails($original);
|
||||
|
||||
self::assertEquals($e->getType(), $expectedType);
|
||||
self::assertEquals($e->toArray(), ['type' => $expectedType]);
|
||||
self::assertEquals($e->jsonSerialize(), ['type' => $expectedType]);
|
||||
|
||||
self::assertEquals($original->getTitle(), $e->getTitle());
|
||||
self::assertEquals($original->getDetail(), $e->getDetail());
|
||||
self::assertEquals($original->getAdditionalData(), $e->getAdditionalData());
|
||||
|
||||
if ($expectSameType) {
|
||||
self::assertEquals($original->getType(), $e->getType());
|
||||
self::assertEquals($original->toArray(), $e->toArray());
|
||||
self::assertEquals($original->jsonSerialize(), $e->jsonSerialize());
|
||||
} else {
|
||||
self::assertNotEquals($original->getType(), $e->getType());
|
||||
self::assertNotEquals($original->toArray(), $e->toArray());
|
||||
self::assertNotEquals($original->jsonSerialize(), $e->jsonSerialize());
|
||||
}
|
||||
}
|
||||
|
||||
public static function provideTypes(): iterable
|
||||
{
|
||||
yield ['foo', 'foo', true];
|
||||
yield ['bar', 'bar', true];
|
||||
yield [ValidationException::ERROR_CODE, 'INVALID_ARGUMENT'];
|
||||
yield [DeleteShortUrlException::ERROR_CODE, 'INVALID_SHORT_URL_DELETION'];
|
||||
yield [DomainNotFoundException::ERROR_CODE, 'DOMAIN_NOT_FOUND'];
|
||||
yield [ForbiddenTagOperationException::ERROR_CODE, 'FORBIDDEN_OPERATION'];
|
||||
yield [InvalidUrlException::ERROR_CODE, 'INVALID_URL'];
|
||||
yield [NonUniqueSlugException::ERROR_CODE, 'INVALID_SLUG'];
|
||||
yield [ShortUrlNotFoundException::ERROR_CODE, 'INVALID_SHORTCODE'];
|
||||
yield [TagConflictException::ERROR_CODE, 'TAG_CONFLICT'];
|
||||
yield [TagNotFoundException::ERROR_CODE, 'TAG_NOT_FOUND'];
|
||||
yield [MercureException::ERROR_CODE, 'MERCURE_NOT_CONFIGURED'];
|
||||
yield [MissingAuthenticationException::ERROR_CODE, 'INVALID_AUTHORIZATION'];
|
||||
yield [VerifyAuthenticationException::ERROR_CODE, 'INVALID_API_KEY'];
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Rest\Middleware\ErrorHandler;
|
||||
|
||||
use Laminas\Diactoros\ServerRequestFactory;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
use Shlinkio\Shlink\Rest\Exception\BackwardsCompatibleProblemDetailsException;
|
||||
use Shlinkio\Shlink\Rest\Middleware\ErrorHandler\BackwardsCompatibleProblemDetailsHandler;
|
||||
use Throwable;
|
||||
|
||||
class BackwardsCompatibleProblemDetailsHandlerTest extends TestCase
|
||||
{
|
||||
private BackwardsCompatibleProblemDetailsHandler $handler;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->handler = new BackwardsCompatibleProblemDetailsHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<Throwable> $expectedException
|
||||
*/
|
||||
#[Test, DataProvider('provideExceptions')]
|
||||
public function expectedExceptionIsThrownBasedOnTheRequestVersion(
|
||||
ServerRequestInterface $request,
|
||||
Throwable $thrownException,
|
||||
string $expectedException,
|
||||
): void {
|
||||
$handler = $this->createMock(RequestHandlerInterface::class);
|
||||
$handler->expects($this->once())->method('handle')->with($request)->willThrowException($thrownException);
|
||||
|
||||
$this->expectException($expectedException);
|
||||
|
||||
$this->handler->process($request, $handler);
|
||||
}
|
||||
|
||||
public static function provideExceptions(): iterable
|
||||
{
|
||||
$baseRequest = ServerRequestFactory::fromGlobals();
|
||||
|
||||
yield 'no version' => [
|
||||
$baseRequest,
|
||||
ValidationException::fromArray([]),
|
||||
BackwardsCompatibleProblemDetailsException::class,
|
||||
];
|
||||
yield 'version 1' => [
|
||||
$baseRequest->withAttribute('version', '1'),
|
||||
ValidationException::fromArray([]),
|
||||
BackwardsCompatibleProblemDetailsException::class,
|
||||
];
|
||||
yield 'version 2' => [
|
||||
$baseRequest->withAttribute('version', '2'),
|
||||
ValidationException::fromArray([]),
|
||||
BackwardsCompatibleProblemDetailsException::class,
|
||||
];
|
||||
yield 'version 3' => [
|
||||
$baseRequest->withAttribute('version', '3'),
|
||||
ValidationException::fromArray([]),
|
||||
ValidationException::class,
|
||||
];
|
||||
yield 'version 4' => [
|
||||
$baseRequest->withAttribute('version', '3'),
|
||||
ValidationException::fromArray([]),
|
||||
ValidationException::class,
|
||||
];
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue