mirror of
https://github.com/shlinkio/shlink.git
synced 2025-02-16 07:19:52 +03:00
commit
29bb201581
207 changed files with 3501 additions and 2262 deletions
30
.github/workflows/publish-release.yml
vendored
Normal file
30
.github/workflows/publish-release.yml
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
name: Publish release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Use PHP 7.4
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '7.4' # Publish release with lowest supported PHP version
|
||||
tools: composer
|
||||
extensions: swoole-4.5.5
|
||||
- name: Generate release assets
|
||||
run: ./build.sh ${GITHUB_REF#refs/tags/v}
|
||||
- name: Publish release with assets
|
||||
uses: docker://antonyurchenko/git-release:latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ALLOW_TAG_PREFIX: "true"
|
||||
ALLOW_EMPTY_CHANGELOG: "true"
|
||||
with:
|
||||
args: |
|
||||
build/shlink_*_dist.zip
|
19
.travis.yml
19
.travis.yml
|
@ -26,25 +26,13 @@ jobs:
|
|||
php: '7.4'
|
||||
env:
|
||||
- COMPOSER_FLAGS=''
|
||||
# Deploy release only on smallest supported PHP version
|
||||
before_deploy:
|
||||
- rm -f ocular.phar
|
||||
- ./build.sh ${TRAVIS_TAG#?}
|
||||
deploy:
|
||||
- provider: releases
|
||||
api_key:
|
||||
secure: a9dbZchocqeuOViwUeNH54bQR5Sz7rEYXx5b9WPFtnFn9LGKKUaLbA2U91UQ9QKPrcTpsALubUYbw2CnNmvCwzaY+R8lCD3gkU4ohsEnbpnw3deOeixI74sqBHJAuCH9FSaRDGILoBMtUKx2xlzIymFxkIsgIukkGbdkWHDlRWY3oTUUuw1SQ2Xk9KDsbJQtjIc1+G/O6gHaV4qv/R9W8NPmJExKTNDrAZbC1vIUnxqp4UpVo1hst8qPd1at94CndDYM5rG+7imGbdtxTxzamt819qdTO1OfvtctKawNAm7YXZrrWft6c7gI6j6SI4hxd+ZrrPBqbaRFHkZHjnNssO/yn4SaOHFFzccmu0MzvpPCf0qWZwd3sGHVYer1MnR2mHYqU84QPlW3nrHwJjkrpq3+q0JcBY6GsJs+RskHNtkMTKV05Iz6QUI5YZGwTpuXaRm036SmavjGc4IDlMaYCk/NmbB9BKpthJxLdUpczOHpnjXXHziotWD6cfEnbjU3byfD8HY5WrxSjsNT7SKmXN3hRof7bk985ewQVjGT42O3NbnfnqjQQWr/B7/zFTpLR4f526Bkq12CdCyf5lvrbq+POkLVdJ+uFfR7ds248Ue/jBQy6kM1tWmKF9QiwisFlA84eQ4CW3I93Rp97URv+AQa9zmbD0Ve3Udp+g6nF5I=
|
||||
file: "./build/shlink_${TRAVIS_TAG#?}_dist.zip"
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
|
||||
before_install:
|
||||
- echo 'extension = apcu.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
|
||||
- phpenv config-rm xdebug.ini || return 0
|
||||
- sudo ./data/infra/ci/install-ms-odbc.sh
|
||||
- docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_ms shlink_db shlink_db_postgres shlink_db_maria
|
||||
- yes | pecl install pdo_sqlsrv swoole-4.5.2
|
||||
- yes | pecl install pdo_sqlsrv-5.9.0preview1 swoole-4.5.5 pcov
|
||||
|
||||
install:
|
||||
- composer self-update
|
||||
|
@ -56,12 +44,13 @@ before_script:
|
|||
- export DOCKERFILE_CHANGED=$(git diff ${TRAVIS_COMMIT_RANGE:-origin/main} --name-only | grep Dockerfile)
|
||||
|
||||
script:
|
||||
- bin/test/run-api-tests.sh --coverage-php build/coverage-api.cov && composer ci
|
||||
- composer ci
|
||||
- bin/test/run-api-tests.sh
|
||||
- if [[ ! -z "${DOCKERFILE_CHANGED}" && "${TRAVIS_PHP_VERSION}" == "7.4" ]]; then docker build -t shlink-docker-image:temp . ; fi
|
||||
|
||||
after_success:
|
||||
- rm -f build/clover.xml
|
||||
- wget https://phar.phpunit.de/phpcov-7.0.2.phar
|
||||
- phpdbg -qrr phpcov-7.0.2.phar merge build --clover build/clover.xml
|
||||
- php phpcov-7.0.2.phar merge build --clover build/clover.xml
|
||||
- wget https://scrutinizer-ci.com/ocular.phar
|
||||
- php ocular.phar code-coverage:upload --format=php-clover build/clover.xml
|
||||
|
|
1138
CHANGELOG.md
1138
CHANGELOG.md
File diff suppressed because it is too large
Load diff
20
Dockerfile
20
Dockerfile
|
@ -1,12 +1,13 @@
|
|||
FROM php:7.4.9-alpine3.12 as base
|
||||
FROM php:7.4.11-alpine3.12 as base
|
||||
|
||||
ARG SHLINK_VERSION=2.2.2
|
||||
ARG SHLINK_VERSION=2.3.0
|
||||
ENV SHLINK_VERSION ${SHLINK_VERSION}
|
||||
ENV SWOOLE_VERSION 4.5.2
|
||||
ENV SWOOLE_VERSION 4.5.5
|
||||
ENV LC_ALL "C"
|
||||
|
||||
WORKDIR /etc/shlink
|
||||
|
||||
# Install required PHP extensions
|
||||
RUN \
|
||||
# Install mysql and calendar
|
||||
docker-php-ext-install -j"$(nproc)" pdo_mysql calendar && \
|
||||
|
@ -21,13 +22,16 @@ RUN \
|
|||
docker-php-ext-install -j"$(nproc)" intl && \
|
||||
# Install zip and gd
|
||||
apk add --no-cache libzip-dev zlib-dev libpng-dev && \
|
||||
docker-php-ext-install -j"$(nproc)" zip gd
|
||||
docker-php-ext-install -j"$(nproc)" zip gd && \
|
||||
# Install gmp
|
||||
apk add --no-cache gmp-dev && \
|
||||
docker-php-ext-install -j"$(nproc)" gmp
|
||||
|
||||
# Install sqlsrv driver
|
||||
RUN if [ $(uname -m) == "x86_64" ]; then \
|
||||
wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_17.5.1.1-1_amd64.apk && \
|
||||
apk add --allow-untrusted msodbcsql17_17.5.1.1-1_amd64.apk && \
|
||||
apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \
|
||||
apk add --no-cache --virtual .phpize-deps ${PHPIZE_DEPS} unixodbc-dev && \
|
||||
pecl install pdo_sqlsrv && \
|
||||
docker-php-ext-enable pdo_sqlsrv && \
|
||||
apk del .phpize-deps && \
|
||||
|
@ -35,7 +39,7 @@ RUN if [ $(uname -m) == "x86_64" ]; then \
|
|||
fi
|
||||
|
||||
# Install swoole
|
||||
RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS && \
|
||||
RUN apk add --no-cache --virtual .phpize-deps ${PHPIZE_DEPS} && \
|
||||
pecl install swoole-${SWOOLE_VERSION} && \
|
||||
docker-php-ext-enable swoole && \
|
||||
apk del .phpize-deps
|
||||
|
@ -44,7 +48,7 @@ RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS && \
|
|||
# Install shlink
|
||||
FROM base as builder
|
||||
COPY . .
|
||||
COPY --from=composer:1.10.1 /usr/bin/composer ./composer.phar
|
||||
COPY --from=composer:2 /usr/bin/composer ./composer.phar
|
||||
RUN apk add --no-cache git && \
|
||||
php composer.phar install --no-dev --optimize-autoloader --prefer-dist --no-progress --no-interaction && \
|
||||
php composer.phar clear-cache && \
|
||||
|
@ -59,7 +63,7 @@ LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
|||
COPY --from=builder /etc/shlink .
|
||||
RUN ln -s /etc/shlink/bin/cli /usr/local/bin/shlink
|
||||
|
||||
# Expose swoole port
|
||||
# Expose default swoole port
|
||||
EXPOSE 8080
|
||||
|
||||
# Expose params config dir, since the user is expected to provide custom config from there
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
![Shlink](https://raw.githubusercontent.com/shlinkio/shlink.io/main/public/images/shlink-hero.png)
|
||||
|
||||
[![Build Status](https://img.shields.io/travis/shlinkio/shlink.svg?style=flat-square)](https://travis-ci.org/shlinkio/shlink)
|
||||
[![Build Status](https://img.shields.io/travis/com/shlinkio/shlink.svg?style=flat-square)](https://travis-ci.com/shlinkio/shlink)
|
||||
[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/shlinkio/shlink.svg?style=flat-square)](https://scrutinizer-ci.com/g/shlinkio/shlink/)
|
||||
[![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/shlinkio/shlink.svg?style=flat-square)](https://scrutinizer-ci.com/g/shlinkio/shlink/)
|
||||
[![Latest Stable Version](https://img.shields.io/github/release/shlinkio/shlink.svg?style=flat-square)](https://packagist.org/packages/shlinkio/shlink)
|
||||
|
@ -64,7 +64,7 @@ In order to run Shlink, you will need a built version of the project. There are
|
|||
|
||||
After that, you will have a `shlink_x.x.x_dist.zip` dist file inside the `build` directory, that you need to decompress in the location fo your choice.
|
||||
|
||||
> This is the process used when releasing new shlink versions. After tagging the new version with git, the Github release is automatically created by [travis](https://travis-ci.org/shlinkio/shlink), attaching the generated dist file to it.
|
||||
> This is the process used when releasing new shlink versions. After tagging the new version with git, the Github release is automatically created by [travis](https://travis-ci.com/shlinkio/shlink), attaching the generated dist file to it.
|
||||
|
||||
### Configure
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#!/usr/bin/env sh
|
||||
export APP_ENV=test
|
||||
export DB_DRIVER=mysql
|
||||
export TEST_ENV=api
|
||||
|
||||
# Try to stop server just in case it hanged in last execution
|
||||
vendor/bin/mezzio-swoole stop
|
||||
|
@ -9,7 +10,7 @@ echo 'Starting server...'
|
|||
vendor/bin/mezzio-swoole start -d
|
||||
sleep 2
|
||||
|
||||
phpdbg -qrr vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always $*
|
||||
vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always --log-junit=build/coverage-api/junit.xml $*
|
||||
testsExitCode=$?
|
||||
|
||||
vendor/bin/mezzio-swoole stop
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"akrabat/ip-address-middleware": "^1.0",
|
||||
"cakephp/chronos": "^1.2",
|
||||
"cakephp/chronos": "^2.0",
|
||||
"cocur/slugify": "^4.0",
|
||||
"doctrine/cache": "^1.9",
|
||||
"doctrine/dbal": "^2.10",
|
||||
|
@ -27,7 +27,6 @@
|
|||
"guzzlehttp/guzzle": "^7.0",
|
||||
"laminas/laminas-config": "^3.3",
|
||||
"laminas/laminas-config-aggregator": "^1.1",
|
||||
"laminas/laminas-dependency-plugin": "^1.0",
|
||||
"laminas/laminas-diactoros": "^2.1.3",
|
||||
"laminas/laminas-inputfilter": "^2.10",
|
||||
"laminas/laminas-paginator": "^2.8",
|
||||
|
@ -50,9 +49,10 @@
|
|||
"predis/predis": "^1.1",
|
||||
"pugx/shortid-php": "^0.5",
|
||||
"ramsey/uuid": "^3.9",
|
||||
"shlinkio/shlink-common": "^3.2.0",
|
||||
"shlinkio/shlink-common": "^3.3.0",
|
||||
"shlinkio/shlink-config": "^1.0",
|
||||
"shlinkio/shlink-event-dispatcher": "^1.4",
|
||||
"shlinkio/shlink-importer": "^2.0.1",
|
||||
"shlinkio/shlink-installer": "^5.1.0",
|
||||
"shlinkio/shlink-ip-geolocation": "^1.5",
|
||||
"symfony/console": "^5.1",
|
||||
|
@ -66,13 +66,15 @@
|
|||
"devster/ubench": "^2.0",
|
||||
"dms/phpunit-arraysubset-asserts": "^0.2.0",
|
||||
"eaglewu/swoole-ide-helper": "dev-master",
|
||||
"infection/infection": "^0.16.1",
|
||||
"phpstan/phpstan": "^0.12.18",
|
||||
"phpunit/phpunit": "~9.0.1",
|
||||
"infection/infection": "^0.20.0",
|
||||
"phpspec/prophecy-phpunit": "^2.0",
|
||||
"phpstan/phpstan": "^0.12.52",
|
||||
"phpunit/php-code-coverage": "^9.2",
|
||||
"phpunit/phpunit": "^9.4",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"shlinkio/php-coding-standard": "~2.1.0",
|
||||
"shlinkio/php-coding-standard": "~2.1.1",
|
||||
"shlinkio/shlink-test-utils": "^1.5",
|
||||
"symfony/var-dumper": "^5.0"
|
||||
"symfony/var-dumper": "^5.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
@ -93,7 +95,10 @@
|
|||
"module/Core/test",
|
||||
"module/Core/test-db"
|
||||
]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"config/test/constants.php"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"ci": [
|
||||
|
@ -104,7 +109,7 @@
|
|||
],
|
||||
"cs": "phpcs",
|
||||
"cs:fix": "phpcbf",
|
||||
"stan": "phpstan analyse module/*/src/ module/*/config config docker/config --level=6",
|
||||
"stan": "phpstan analyse module/*/src/ module/*/config config docker/config data/migrations --level=6",
|
||||
"test": [
|
||||
"@test:unit",
|
||||
"@test:db",
|
||||
|
@ -114,8 +119,8 @@
|
|||
"@test:unit:ci",
|
||||
"@test:db"
|
||||
],
|
||||
"test:unit": "phpdbg -qrr vendor/bin/phpunit --order-by=random --colors=always --coverage-php build/coverage-unit.cov --testdox",
|
||||
"test:unit:ci": "@test:unit --coverage-clover=build/clover.xml --coverage-xml=build/coverage-unit/coverage-xml --log-junit=build/coverage-unit/junit.xml",
|
||||
"test:unit": "@php vendor/bin/phpunit --order-by=random --colors=always --coverage-php build/coverage-unit.cov --testdox",
|
||||
"test:unit:ci": "@test:unit --coverage-xml=build/coverage-unit/coverage-xml --log-junit=build/coverage-unit/junit.xml",
|
||||
"test:db": [
|
||||
"@test:db:sqlite:ci",
|
||||
"@test:db:mysql",
|
||||
|
@ -123,14 +128,14 @@
|
|||
"@test:db:postgres",
|
||||
"@test:db:ms"
|
||||
],
|
||||
"test:db:sqlite": "APP_ENV=test phpdbg -qrr vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-db.xml",
|
||||
"test:db:sqlite": "APP_ENV=test php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-db.xml",
|
||||
"test:db:sqlite:ci": "@test:db:sqlite --coverage-php build/coverage-db.cov --coverage-xml=build/coverage-db/coverage-xml --log-junit=build/coverage-db/junit.xml",
|
||||
"test:db:mysql": "DB_DRIVER=mysql composer test:db:sqlite",
|
||||
"test:db:maria": "DB_DRIVER=maria composer test:db:sqlite",
|
||||
"test:db:postgres": "DB_DRIVER=postgres composer test:db:sqlite",
|
||||
"test:db:ms": "DB_DRIVER=mssql composer test:db:sqlite",
|
||||
"test:api": "bin/test/run-api-tests.sh",
|
||||
"test:unit:pretty": "phpdbg -qrr vendor/bin/phpunit --order-by=random --colors=always --coverage-html build/coverage",
|
||||
"test:unit:pretty": "@php vendor/bin/phpunit --order-by=random --colors=always --coverage-html build/coverage-unit-html",
|
||||
"infect": "infection --threads=4 --min-msi=80 --log-verbosity=default --only-covered",
|
||||
"infect:ci:base": "@infect --skip-initial-tests",
|
||||
"infect:ci": [
|
||||
|
@ -166,6 +171,7 @@
|
|||
"clean:dev": "<fg=blue;options=bold>Deletes artifacts which are gitignored and could affect dev env</>"
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
"sort-packages": true,
|
||||
"platform-check": false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use Mezzio\Container;
|
||||
use Psr\Http\Client\ClientInterface;
|
||||
|
||||
return [
|
||||
|
||||
|
@ -13,6 +15,10 @@ return [
|
|||
],
|
||||
],
|
||||
|
||||
'aliases' => [
|
||||
ClientInterface::class => Client::class,
|
||||
],
|
||||
|
||||
'lazy_services' => [
|
||||
'proxies_target_dir' => 'data/proxies',
|
||||
'proxies_namespace' => 'ShlinkProxy',
|
||||
|
|
|
@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||
return [
|
||||
|
||||
'not_found_redirects' => [
|
||||
'invalid_short_url' => null, // Formerly url_shortener.not_found_short_url.redirect_to
|
||||
'invalid_short_url' => null,
|
||||
'regular_404' => null,
|
||||
'base_url' => null,
|
||||
],
|
||||
|
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Shlinkio\Shlink;
|
||||
|
||||
use Laminas\ConfigAggregator;
|
||||
use Laminas\Diactoros;
|
||||
use Mezzio;
|
||||
use Mezzio\ProblemDetails;
|
||||
|
||||
|
@ -17,8 +18,10 @@ return (new ConfigAggregator\ConfigAggregator([
|
|||
Mezzio\Plates\ConfigProvider::class,
|
||||
Mezzio\Swoole\ConfigProvider::class,
|
||||
ProblemDetails\ConfigProvider::class,
|
||||
Diactoros\ConfigProvider::class,
|
||||
Common\ConfigProvider::class,
|
||||
Config\ConfigProvider::class,
|
||||
Importer\ConfigProvider::class,
|
||||
IpGeolocation\ConfigProvider::class,
|
||||
EventDispatcher\ConfigProvider::class,
|
||||
Core\ConfigProvider::class,
|
||||
|
|
|
@ -7,12 +7,28 @@ namespace Shlinkio\Shlink\TestUtils;
|
|||
use Doctrine\ORM\EntityManager;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
use function register_shutdown_function;
|
||||
use function sprintf;
|
||||
|
||||
use const ShlinkioTest\Shlink\SWOOLE_TESTING_HOST;
|
||||
use const ShlinkioTest\Shlink\SWOOLE_TESTING_PORT;
|
||||
|
||||
/** @var ContainerInterface $container */
|
||||
$container = require __DIR__ . '/../container.php';
|
||||
$testHelper = $container->get(Helper\TestHelper::class);
|
||||
$config = $container->get('config');
|
||||
$em = $container->get(EntityManager::class);
|
||||
$httpClient = $container->get('shlink_test_api_client');
|
||||
|
||||
// Start code coverage collecting on swoole process, and stop it when process shuts down
|
||||
$httpClient->request('GET', sprintf('http://%s:%s/api-tests/start-coverage', SWOOLE_TESTING_HOST, SWOOLE_TESTING_PORT));
|
||||
register_shutdown_function(function () use ($httpClient): void {
|
||||
$httpClient->request(
|
||||
'GET',
|
||||
sprintf('http://%s:%s/api-tests/stop-coverage', SWOOLE_TESTING_HOST, SWOOLE_TESTING_PORT),
|
||||
);
|
||||
});
|
||||
|
||||
$testHelper->createTestDb();
|
||||
ApiTest\ApiTestCase::setApiClient($container->get('shlink_test_api_client'));
|
||||
ApiTest\ApiTestCase::setApiClient($httpClient);
|
||||
ApiTest\ApiTestCase::setSeedFixturesCallback(fn () => $testHelper->seedFixtures($em, $config['data_fixtures'] ?? []));
|
||||
|
|
8
config/test/constants.php
Normal file
8
config/test/constants.php
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink;
|
||||
|
||||
const SWOOLE_TESTING_HOST = '127.0.0.1';
|
||||
const SWOOLE_TESTING_PORT = 9999;
|
|
@ -6,15 +6,33 @@ namespace Shlinkio\Shlink;
|
|||
|
||||
use GuzzleHttp\Client;
|
||||
use Laminas\ConfigAggregator\ConfigAggregator;
|
||||
use Laminas\Diactoros\Response\EmptyResponse;
|
||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||
use Laminas\Stdlib\Glob;
|
||||
use PDO;
|
||||
use PHPUnit\Runner\Version;
|
||||
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
||||
use SebastianBergmann\CodeCoverage\Driver\Selector;
|
||||
use SebastianBergmann\CodeCoverage\Filter;
|
||||
use SebastianBergmann\CodeCoverage\Report\PHP;
|
||||
use SebastianBergmann\CodeCoverage\Report\Xml\Facade as Xml;
|
||||
|
||||
use function Laminas\Stratigility\middleware;
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
use function sprintf;
|
||||
use function sys_get_temp_dir;
|
||||
|
||||
$swooleTestingHost = '127.0.0.1';
|
||||
$swooleTestingPort = 9999;
|
||||
use const ShlinkioTest\Shlink\SWOOLE_TESTING_HOST;
|
||||
use const ShlinkioTest\Shlink\SWOOLE_TESTING_PORT;
|
||||
|
||||
$isApiTest = env('TEST_ENV') === 'api';
|
||||
if ($isApiTest) {
|
||||
$filter = new Filter();
|
||||
foreach (Glob::glob(__DIR__ . '/../../module/*/src') as $item) {
|
||||
$filter->includeDirectory($item);
|
||||
}
|
||||
$coverage = new CodeCoverage((new Selector())->forLineCoverage($filter), $filter);
|
||||
}
|
||||
|
||||
$buildDbConnection = function (): array {
|
||||
$driver = env('DB_DRIVER', 'sqlite');
|
||||
|
@ -78,8 +96,8 @@ return [
|
|||
'mezzio-swoole' => [
|
||||
'enable_coroutine' => false,
|
||||
'swoole-http-server' => [
|
||||
'host' => $swooleTestingHost,
|
||||
'port' => $swooleTestingPort,
|
||||
'host' => SWOOLE_TESTING_HOST,
|
||||
'port' => SWOOLE_TESTING_PORT,
|
||||
'process-name' => 'shlink_test',
|
||||
'options' => [
|
||||
'pid_file' => sys_get_temp_dir() . '/shlink-test-swoole.pid',
|
||||
|
@ -88,6 +106,35 @@ return [
|
|||
],
|
||||
],
|
||||
|
||||
'routes' => !$isApiTest ? [] : [
|
||||
[
|
||||
'name' => 'start_collecting_coverage',
|
||||
'path' => '/api-tests/start-coverage',
|
||||
'middleware' => middleware(static function () use (&$coverage) {
|
||||
if ($coverage) {
|
||||
$coverage->start('API tests');
|
||||
}
|
||||
return new EmptyResponse();
|
||||
}),
|
||||
'allowed_methods' => ['GET'],
|
||||
],
|
||||
[
|
||||
'name' => 'dump_coverage',
|
||||
'path' => '/api-tests/stop-coverage',
|
||||
'middleware' => middleware(static function () use (&$coverage) {
|
||||
if ($coverage) {
|
||||
$basePath = __DIR__ . '/../../build/coverage-api';
|
||||
$coverage->stop();
|
||||
(new PHP())->process($coverage, $basePath . '.cov');
|
||||
(new Xml(Version::getVersionString()))->process($coverage, $basePath . '/coverage-xml');
|
||||
}
|
||||
|
||||
return new EmptyResponse();
|
||||
}),
|
||||
'allowed_methods' => ['GET'],
|
||||
],
|
||||
],
|
||||
|
||||
'mercure' => [
|
||||
'public_hub_url' => null,
|
||||
'internal_hub_url' => null,
|
||||
|
@ -97,7 +144,7 @@ return [
|
|||
'dependencies' => [
|
||||
'services' => [
|
||||
'shlink_test_api_client' => new Client([
|
||||
'base_uri' => sprintf('http://%s:%s/', $swooleTestingHost, $swooleTestingPort),
|
||||
'base_uri' => sprintf('http://%s:%s/', SWOOLE_TESTING_HOST, SWOOLE_TESTING_PORT),
|
||||
'http_errors' => false,
|
||||
]),
|
||||
],
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
FROM php:7.4.9-alpine3.12
|
||||
FROM php:7.4.11-alpine3.12
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
ENV APCU_VERSION 5.1.18
|
||||
ENV APCU_BC_VERSION 1.0.5
|
||||
ENV XDEBUG_VERSION 2.9.0
|
||||
|
||||
RUN apk update
|
||||
|
||||
|
@ -31,6 +30,9 @@ RUN docker-php-ext-install gd
|
|||
RUN apk add --no-cache postgresql-dev
|
||||
RUN docker-php-ext-install pdo_pgsql
|
||||
|
||||
RUN apk add --no-cache gmp-dev
|
||||
RUN docker-php-ext-install gmp
|
||||
|
||||
# Install APCu extension
|
||||
ADD https://pecl.php.net/get/apcu-$APCU_VERSION.tgz /tmp/apcu.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/apcu\
|
||||
|
@ -55,29 +57,17 @@ RUN rm /tmp/apcu_bc.tar.gz
|
|||
RUN rm /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini
|
||||
RUN echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini
|
||||
|
||||
# Install xdebug
|
||||
ADD https://pecl.php.net/get/xdebug-$XDEBUG_VERSION /tmp/xdebug.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/xdebug\
|
||||
&& tar xf /tmp/xdebug.tar.gz -C /usr/src/php/ext/xdebug --strip-components=1
|
||||
# configure and install
|
||||
RUN docker-php-ext-configure xdebug\
|
||||
&& docker-php-ext-install xdebug
|
||||
# cleanup
|
||||
RUN rm /tmp/xdebug.tar.gz
|
||||
|
||||
# Install sqlsrv driver
|
||||
# Install pcov and sqlsrv driver
|
||||
RUN wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_17.5.1.1-1_amd64.apk && \
|
||||
apk add --allow-untrusted msodbcsql17_17.5.1.1-1_amd64.apk && \
|
||||
apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \
|
||||
pecl install pdo_sqlsrv && \
|
||||
docker-php-ext-enable pdo_sqlsrv && \
|
||||
pecl install pdo_sqlsrv pcov && \
|
||||
docker-php-ext-enable pdo_sqlsrv pcov && \
|
||||
apk del .phpize-deps && \
|
||||
rm msodbcsql17_17.5.1.1-1_amd64.apk
|
||||
|
||||
# Install composer
|
||||
RUN php -r "readfile('https://getcomposer.org/installer');" | php
|
||||
RUN chmod +x composer.phar
|
||||
RUN mv composer.phar /usr/local/bin/composer
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
|
||||
|
||||
# Make home directory writable by anyone
|
||||
RUN chmod 777 /home
|
||||
|
|
|
@ -4,3 +4,5 @@ memory_limit=-1
|
|||
log_errors_max_len=0
|
||||
zend.assertions=1
|
||||
assert.exception=1
|
||||
pcov.enabled=1
|
||||
pcov.directory=module
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
FROM php:7.4.9-alpine3.12
|
||||
FROM php:7.4.11-alpine3.12
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
ENV APCU_VERSION 5.1.18
|
||||
ENV APCU_BC_VERSION 1.0.5
|
||||
ENV INOTIFY_VERSION 2.0.0
|
||||
ENV SWOOLE_VERSION 4.5.2
|
||||
ENV SWOOLE_VERSION 4.5.5
|
||||
|
||||
RUN apk update
|
||||
|
||||
|
@ -32,6 +32,9 @@ RUN docker-php-ext-install gd
|
|||
RUN apk add --no-cache postgresql-dev
|
||||
RUN docker-php-ext-install pdo_pgsql
|
||||
|
||||
RUN apk add --no-cache gmp-dev
|
||||
RUN docker-php-ext-install gmp
|
||||
|
||||
# Install APCu extension
|
||||
ADD https://pecl.php.net/get/apcu-$APCU_VERSION.tgz /tmp/apcu.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/apcu\
|
||||
|
@ -66,19 +69,17 @@ RUN docker-php-ext-configure inotify\
|
|||
# cleanup
|
||||
RUN rm /tmp/inotify.tar.gz
|
||||
|
||||
# Install swoole and mssql driver
|
||||
# Install swoole, pcov and mssql driver
|
||||
RUN wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_17.5.1.1-1_amd64.apk && \
|
||||
apk add --allow-untrusted msodbcsql17_17.5.1.1-1_amd64.apk && \
|
||||
apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \
|
||||
pecl install swoole-${SWOOLE_VERSION} pdo_sqlsrv && \
|
||||
docker-php-ext-enable swoole pdo_sqlsrv && \
|
||||
pecl install swoole-${SWOOLE_VERSION} pdo_sqlsrv pcov && \
|
||||
docker-php-ext-enable swoole pdo_sqlsrv pcov && \
|
||||
apk del .phpize-deps && \
|
||||
rm msodbcsql17_17.5.1.1-1_amd64.apk
|
||||
|
||||
# Install composer
|
||||
RUN php -r "readfile('https://getcomposer.org/installer');" | php
|
||||
RUN chmod +x composer.phar
|
||||
RUN mv composer.phar /usr/local/bin/composer
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
|
||||
|
||||
# Make home directory writable by anyone
|
||||
RUN chmod 777 /home
|
||||
|
|
|
@ -24,10 +24,10 @@ class Version20171021093246 extends AbstractMigration
|
|||
return;
|
||||
}
|
||||
|
||||
$shortUrls->addColumn('valid_since', Types::DATETIME, [
|
||||
$shortUrls->addColumn('valid_since', Types::DATETIME_MUTABLE, [
|
||||
'notnull' => false,
|
||||
]);
|
||||
$shortUrls->addColumn('valid_until', Types::DATETIME, [
|
||||
$shortUrls->addColumn('valid_until', Types::DATETIME_MUTABLE, [
|
||||
'notnull' => false,
|
||||
]);
|
||||
}
|
||||
|
|
44
data/migrations/Version20201023090929.php
Normal file
44
data/migrations/Version20201023090929.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20201023090929 extends AbstractMigration
|
||||
{
|
||||
private const IMPORT_SOURCE_COLUMN = 'import_source';
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
$this->skipIf($shortUrls->hasColumn(self::IMPORT_SOURCE_COLUMN));
|
||||
|
||||
$shortUrls->addColumn(self::IMPORT_SOURCE_COLUMN, Types::STRING, [
|
||||
'length' => 255,
|
||||
'notnull' => false,
|
||||
]);
|
||||
$shortUrls->addColumn('import_original_short_code', Types::STRING, [
|
||||
'length' => 255,
|
||||
'notnull' => false,
|
||||
]);
|
||||
|
||||
$shortUrls->addUniqueIndex(
|
||||
[self::IMPORT_SOURCE_COLUMN, 'import_original_short_code', 'domain_id'],
|
||||
'unique_imports',
|
||||
);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
$this->skipIf(! $shortUrls->hasColumn(self::IMPORT_SOURCE_COLUMN));
|
||||
|
||||
$shortUrls->dropColumn(self::IMPORT_SOURCE_COLUMN);
|
||||
$shortUrls->dropColumn('import_original_short_code');
|
||||
$shortUrls->dropIndex('unique_imports');
|
||||
}
|
||||
}
|
86
data/migrations/Version20201102113208.php
Normal file
86
data/migrations/Version20201102113208.php
Normal file
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Doctrine\DBAL\Driver\Result;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20201102113208 extends AbstractMigration
|
||||
{
|
||||
private const API_KEY_COLUMN = 'author_api_key_id';
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
$this->skipIf($shortUrls->hasColumn(self::API_KEY_COLUMN));
|
||||
|
||||
$shortUrls->addColumn(self::API_KEY_COLUMN, Types::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'notnull' => false,
|
||||
]);
|
||||
|
||||
$shortUrls->addForeignKeyConstraint('api_keys', [self::API_KEY_COLUMN], ['id'], [
|
||||
'onDelete' => 'SET NULL',
|
||||
'onUpdate' => 'RESTRICT',
|
||||
], 'FK_' . self::API_KEY_COLUMN);
|
||||
}
|
||||
|
||||
public function postUp(Schema $schema): void
|
||||
{
|
||||
// If there's only one API key and it's active, link all existing URLs with it
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->select('id')
|
||||
->from('api_keys')
|
||||
->where($qb->expr()->eq('enabled', ':enabled'))
|
||||
->andWhere($qb->expr()->or(
|
||||
$qb->expr()->isNull('expiration_date'),
|
||||
$qb->expr()->gt('expiration_date', ':expiration'),
|
||||
))
|
||||
->setParameters([
|
||||
'enabled' => true,
|
||||
'expiration' => Chronos::now()->toDateTimeString(),
|
||||
]);
|
||||
|
||||
/** @var Result $result */
|
||||
$result = $qb->execute();
|
||||
$id = $this->resolveOneApiKeyId($result);
|
||||
if ($id === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->update('short_urls')
|
||||
->set(self::API_KEY_COLUMN, ':apiKeyId')
|
||||
->setParameter('apiKeyId', $id)
|
||||
->execute();
|
||||
}
|
||||
|
||||
private function resolveOneApiKeyId(Result $result): ?string
|
||||
{
|
||||
$results = [];
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
// As soon as we have to iterate more than once, then we cannot resolve a single API key
|
||||
if (! empty($results)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$results[] = $row['id'] ?? null;
|
||||
}
|
||||
|
||||
return $results[0] ?? null;
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
$this->skipIf(! $shortUrls->hasColumn(self::API_KEY_COLUMN));
|
||||
|
||||
$shortUrls->removeForeignKey('FK_' . self::API_KEY_COLUMN);
|
||||
$shortUrls->dropColumn(self::API_KEY_COLUMN);
|
||||
}
|
||||
}
|
|
@ -176,15 +176,17 @@ This is the complete list of supported env vars:
|
|||
* `ANONYMIZE_REMOTE_ADDR`: Tells if IP addresses from visitors should be obfuscated before storing them in the database. Default value is `true`. **Careful!** Setting this to `false` will make your Shlink instance no longer be in compliance with the GDPR and other similar data protection regulations.
|
||||
* `REDIRECT_STATUS_CODE`: Either **301** or **302**. Used to determine if redirects from short to long URLs should be done with a 301 or 302 status. Defaults to 302.
|
||||
* `REDIRECT_CACHE_LIFETIME`: Allows to set the amount of seconds that redirects should be cached when redirect status is 301. Default values is 30.
|
||||
* `PORT`: Can be used to set the port in which shlink listens. Defaults to 8080 (Some cloud providers, like Google cloud or Heroku, expect to be able to customize exposed port by providing this env var).
|
||||
|
||||
An example using all env vars could look like this:
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
--name shlink \
|
||||
-p 8080:8080 \
|
||||
-p 8080:8888 \
|
||||
-e SHORT_DOMAIN_HOST=doma.in \
|
||||
-e SHORT_DOMAIN_SCHEMA=https \
|
||||
-e PORT=8888 \
|
||||
-e DB_DRIVER=mysql \
|
||||
-e DB_NAME=shlink \
|
||||
-e DB_USER=root \
|
||||
|
@ -257,7 +259,8 @@ The whole configuration should have this format, but it can be split into multip
|
|||
"mercure_jwt_secret": "super_secret_key",
|
||||
"anonymize_remote_addr": false,
|
||||
"redirect_status_code": 301,
|
||||
"redirect_cache_lifetime": 90
|
||||
"redirect_cache_lifetime": 90,
|
||||
"port": 8888
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -159,6 +159,7 @@ return [
|
|||
|
||||
'mezzio-swoole' => [
|
||||
'swoole-http-server' => [
|
||||
'port' => (int) env('PORT', 8080),
|
||||
'options' => [
|
||||
'worker_num' => (int) env('WEB_WORKER_NUM', 16),
|
||||
'task_worker_num' => (int) env('TASK_WORKER_NUM', 16),
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
{
|
||||
"name": "tags[]",
|
||||
"in": "query",
|
||||
"description": "A list of tags used to filter the resultset. Only short URLs tagged with at least one of the provided tags will be returned. (Since v1.3.0)",
|
||||
"description": "A list of tags used to filter the result set. Only short URLs tagged with at least one of the provided tags will be returned. (Since v1.3.0)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "array",
|
||||
|
@ -48,10 +48,14 @@
|
|||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"longUrl",
|
||||
"shortCode",
|
||||
"dateCreated",
|
||||
"visits"
|
||||
"longUrl-ASC",
|
||||
"longUrl-DESC",
|
||||
"shortCode-ASC",
|
||||
"shortCode-DESC",
|
||||
"dateCreated-ASC",
|
||||
"dateCreated-DESC",
|
||||
"visits-ASC",
|
||||
"visits-DESC"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -247,6 +251,10 @@
|
|||
"shortCodeLength": {
|
||||
"description": "The length for generated short code. It has to be at least 4 and defaults to 5. It will be ignored when customSlug is provided",
|
||||
"type": "number"
|
||||
},
|
||||
"validateUrl": {
|
||||
"description": "Tells if the long URL should or should not be validated as a reachable URL. If not provided, it will fall back to app-level config",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -127,6 +127,10 @@
|
|||
"maxVisits": {
|
||||
"description": "The maximum number of allowed visits for this short code",
|
||||
"type": "number"
|
||||
},
|
||||
"validateUrl": {
|
||||
"description": "Tells if the long URL (if provided) should or should not be validated as a reachable URL. If not provided, it will fall back to app-level config",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,12 +87,13 @@
|
|||
},
|
||||
|
||||
"post": {
|
||||
"deprecated": true,
|
||||
"operationId": "createTags",
|
||||
"tags": [
|
||||
"Tags"
|
||||
],
|
||||
"summary": "Create tags",
|
||||
"description": "Provided a list of tags, creates all that do not yet exist",
|
||||
"description": "Provided a list of tags, creates all that do not yet exist<br />This endpoint is deprecated, as tags are automatically created while creating a short URL",
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
|
|
86
docs/swagger/paths/v2_domains.json
Normal file
86
docs/swagger/paths/v2_domains.json
Normal file
|
@ -0,0 +1,86 @@
|
|||
{
|
||||
"get": {
|
||||
"operationId": "listDomains",
|
||||
"tags": [
|
||||
"Domains"
|
||||
],
|
||||
"summary": "List existing domains",
|
||||
"description": "Returns the list of all domains ever used, with a flag that tells if they are the default domain",
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The list of tags",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["domains"],
|
||||
"properties": {
|
||||
"domains": {
|
||||
"type": "object",
|
||||
"required": ["data"],
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["domain", "isDefault"],
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string"
|
||||
},
|
||||
"isDefault": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"domains": {
|
||||
"data": [
|
||||
{
|
||||
"domain": "example.com",
|
||||
"isDefault": true
|
||||
},
|
||||
{
|
||||
"domain": "aaa.com",
|
||||
"isDefault": false
|
||||
},
|
||||
{
|
||||
"domain": "bbb.com",
|
||||
"isDefault": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -27,6 +27,19 @@
|
|||
"maximum": 1000,
|
||||
"default": 300
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "format",
|
||||
"in": "query",
|
||||
"description": "The format for the QR code image, being valid values png and svg. Not providing the param or providing any other value will fall back to png.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"png",
|
||||
"svg"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
@ -38,6 +51,12 @@
|
|||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
},
|
||||
"image/svg+xml": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -88,6 +88,10 @@
|
|||
"$ref": "paths/v2_tags_{tag}_visits.json"
|
||||
},
|
||||
|
||||
"/rest/v{version}/domains": {
|
||||
"$ref": "paths/v2_domains.json"
|
||||
},
|
||||
|
||||
"/rest/v{version}/mercure-info": {
|
||||
"$ref": "paths/v2_mercure-info.json"
|
||||
},
|
||||
|
|
|
@ -25,6 +25,8 @@ return [
|
|||
Command\Tag\RenameTagCommand::NAME => Command\Tag\RenameTagCommand::class,
|
||||
Command\Tag\DeleteTagsCommand::NAME => Command\Tag\DeleteTagsCommand::class,
|
||||
|
||||
Command\Domain\ListDomainsCommand::NAME => Command\Domain\ListDomainsCommand::class,
|
||||
|
||||
Command\Db\CreateDatabaseCommand::NAME => Command\Db\CreateDatabaseCommand::class,
|
||||
Command\Db\MigrateDatabaseCommand::NAME => Command\Db\MigrateDatabaseCommand::class,
|
||||
],
|
||||
|
|
|
@ -10,6 +10,7 @@ use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
|||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
||||
use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory;
|
||||
use Shlinkio\Shlink\Core\Domain\DomainService;
|
||||
use Shlinkio\Shlink\Core\Service;
|
||||
use Shlinkio\Shlink\Core\Tag\TagService;
|
||||
use Shlinkio\Shlink\Core\Visit;
|
||||
|
@ -52,6 +53,8 @@ return [
|
|||
|
||||
Command\Db\CreateDatabaseCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Db\MigrateDatabaseCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Domain\ListDomainsCommand::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
|
@ -84,6 +87,8 @@ return [
|
|||
Command\Tag\RenameTagCommand::class => [TagService::class],
|
||||
Command\Tag\DeleteTagsCommand::class => [TagService::class],
|
||||
|
||||
Command\Domain\ListDomainsCommand::class => [DomainService::class, 'config.url_shortener.domain.hostname'],
|
||||
|
||||
Command\Db\CreateDatabaseCommand::class => [
|
||||
LockFactory::class,
|
||||
SymfonyCli\Helper\ProcessHelper::class,
|
||||
|
|
49
module/CLI/src/Command/Domain/ListDomainsCommand.php
Normal file
49
module/CLI/src/Command/Domain/ListDomainsCommand.php
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Domain;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function Functional\map;
|
||||
|
||||
class ListDomainsCommand extends Command
|
||||
{
|
||||
public const NAME = 'domain:list';
|
||||
|
||||
private DomainServiceInterface $domainService;
|
||||
private string $defaultDomain;
|
||||
|
||||
public function __construct(DomainServiceInterface $domainService, string $defaultDomain)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->domainService = $domainService;
|
||||
$this->defaultDomain = $defaultDomain;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('List all domains that have been ever used for some short URL');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$regularDomains = $this->domainService->listDomainsWithout($this->defaultDomain);
|
||||
|
||||
ShlinkTable::fromOutput($output)->render(['Domain', 'Is default'], [
|
||||
[$this->defaultDomain, 'Yes'],
|
||||
...map($regularDomains, fn (Domain $domain) => [$domain->getAuthority(), 'No']),
|
||||
]);
|
||||
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
}
|
|
@ -21,7 +21,9 @@ use function array_map;
|
|||
use function Functional\curry;
|
||||
use function Functional\flatten;
|
||||
use function Functional\unique;
|
||||
use function method_exists;
|
||||
use function sprintf;
|
||||
use function strpos;
|
||||
|
||||
class GenerateShortUrlCommand extends Command
|
||||
{
|
||||
|
@ -94,6 +96,18 @@ class GenerateShortUrlCommand extends Command
|
|||
'l',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The length for generated short code (it will be ignored if --customSlug was provided).',
|
||||
)
|
||||
->addOption(
|
||||
'validate-url',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Forces the long URL to be validated, regardless what is globally configured.',
|
||||
)
|
||||
->addOption(
|
||||
'no-validate-url',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Forces the long URL to not be validated, regardless what is globally configured.',
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -125,9 +139,10 @@ class GenerateShortUrlCommand extends Command
|
|||
$customSlug = $input->getOption('customSlug');
|
||||
$maxVisits = $input->getOption('maxVisits');
|
||||
$shortCodeLength = $input->getOption('shortCodeLength') ?? $this->defaultShortCodeLength;
|
||||
$doValidateUrl = $this->doValidateUrl($input);
|
||||
|
||||
try {
|
||||
$shortUrl = $this->urlShortener->urlToShortCode($longUrl, $tags, ShortUrlMeta::fromRawData([
|
||||
$shortUrl = $this->urlShortener->shorten($longUrl, $tags, ShortUrlMeta::fromRawData([
|
||||
ShortUrlMetaInputFilter::VALID_SINCE => $input->getOption('validSince'),
|
||||
ShortUrlMetaInputFilter::VALID_UNTIL => $input->getOption('validUntil'),
|
||||
ShortUrlMetaInputFilter::CUSTOM_SLUG => $customSlug,
|
||||
|
@ -135,6 +150,7 @@ class GenerateShortUrlCommand extends Command
|
|||
ShortUrlMetaInputFilter::FIND_IF_EXISTS => $input->getOption('findIfExists'),
|
||||
ShortUrlMetaInputFilter::DOMAIN => $input->getOption('domain'),
|
||||
ShortUrlMetaInputFilter::SHORT_CODE_LENGTH => $shortCodeLength,
|
||||
ShortUrlMetaInputFilter::VALIDATE_URL => $doValidateUrl,
|
||||
]));
|
||||
|
||||
$io->writeln([
|
||||
|
@ -147,4 +163,18 @@ class GenerateShortUrlCommand extends Command
|
|||
return ExitCodes::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
private function doValidateUrl(InputInterface $input): ?bool
|
||||
{
|
||||
$rawInput = method_exists($input, '__toString') ? $input->__toString() : '';
|
||||
|
||||
if (strpos($rawInput, '--no-validate-url') !== false) {
|
||||
return false;
|
||||
}
|
||||
if (strpos($rawInput, '--validate-url') !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ use Symfony\Component\Console\Input\InputOption;
|
|||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/** @deprecated */
|
||||
class CreateTagCommand extends Command
|
||||
{
|
||||
public const NAME = 'tag:create';
|
||||
|
@ -28,7 +29,7 @@ class CreateTagCommand extends Command
|
|||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('Creates one or more tags.')
|
||||
->setDescription('[Deprecated] Creates one or more tags.')
|
||||
->addOption(
|
||||
'name',
|
||||
't',
|
||||
|
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Api\DisableKeyCommand;
|
||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||
|
@ -14,6 +15,8 @@ use Symfony\Component\Console\Tester\CommandTester;
|
|||
|
||||
class DisableKeyCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $apiKeyService;
|
||||
|
||||
|
@ -37,7 +40,7 @@ class DisableKeyCommandTest extends TestCase
|
|||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('API key "abcd1234" properly disabled', $output);
|
||||
self::assertStringContainsString('API key "abcd1234" properly disabled', $output);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -52,7 +55,7 @@ class DisableKeyCommandTest extends TestCase
|
|||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString($expectedMessage, $output);
|
||||
self::assertStringContainsString($expectedMessage, $output);
|
||||
$disable->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
|||
use Cake\Chronos\Chronos;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
@ -16,6 +17,8 @@ use Symfony\Component\Console\Tester\CommandTester;
|
|||
|
||||
class GenerateKeyCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $apiKeyService;
|
||||
|
||||
|
@ -36,7 +39,7 @@ class GenerateKeyCommandTest extends TestCase
|
|||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('Generated API key: ', $output);
|
||||
self::assertStringContainsString('Generated API key: ', $output);
|
||||
$create->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
@ -14,6 +15,8 @@ use Symfony\Component\Console\Tester\CommandTester;
|
|||
|
||||
class ListKeysCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $apiKeyService;
|
||||
|
||||
|
@ -38,11 +41,11 @@ class ListKeysCommandTest extends TestCase
|
|||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('Key', $output);
|
||||
$this->assertStringContainsString('Is enabled', $output);
|
||||
$this->assertStringContainsString(' +++ ', $output);
|
||||
$this->assertStringNotContainsString(' --- ', $output);
|
||||
$this->assertStringContainsString('Expiration date', $output);
|
||||
self::assertStringContainsString('Key', $output);
|
||||
self::assertStringContainsString('Is enabled', $output);
|
||||
self::assertStringContainsString(' +++ ', $output);
|
||||
self::assertStringNotContainsString(' --- ', $output);
|
||||
self::assertStringContainsString('Expiration date', $output);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -58,10 +61,10 @@ class ListKeysCommandTest extends TestCase
|
|||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('Key', $output);
|
||||
$this->assertStringNotContainsString('Is enabled', $output);
|
||||
$this->assertStringNotContainsString(' +++ ', $output);
|
||||
$this->assertStringNotContainsString(' --- ', $output);
|
||||
$this->assertStringContainsString('Expiration date', $output);
|
||||
self::assertStringContainsString('Key', $output);
|
||||
self::assertStringNotContainsString('Is enabled', $output);
|
||||
self::assertStringNotContainsString(' +++ ', $output);
|
||||
self::assertStringNotContainsString(' --- ', $output);
|
||||
self::assertStringContainsString('Expiration date', $output);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ use Doctrine\DBAL\Platforms\AbstractPlatform;
|
|||
use Doctrine\DBAL\Schema\AbstractSchemaManager;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Db\CreateDatabaseCommand;
|
||||
use Symfony\Component\Console\Application;
|
||||
|
@ -22,10 +23,11 @@ use Symfony\Component\Process\Process;
|
|||
|
||||
class CreateDatabaseCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $processHelper;
|
||||
private ObjectProphecy $regularConn;
|
||||
private ObjectProphecy $noDbNameConn;
|
||||
private ObjectProphecy $schemaManager;
|
||||
private ObjectProphecy $databasePlatform;
|
||||
|
||||
|
@ -48,15 +50,15 @@ class CreateDatabaseCommandTest extends TestCase
|
|||
$this->regularConn = $this->prophesize(Connection::class);
|
||||
$this->regularConn->getSchemaManager()->willReturn($this->schemaManager->reveal());
|
||||
$this->regularConn->getDatabasePlatform()->willReturn($this->databasePlatform->reveal());
|
||||
$this->noDbNameConn = $this->prophesize(Connection::class);
|
||||
$this->noDbNameConn->getSchemaManager()->willReturn($this->schemaManager->reveal());
|
||||
$noDbNameConn = $this->prophesize(Connection::class);
|
||||
$noDbNameConn->getSchemaManager()->willReturn($this->schemaManager->reveal());
|
||||
|
||||
$command = new CreateDatabaseCommand(
|
||||
$locker->reveal(),
|
||||
$this->processHelper->reveal(),
|
||||
$phpExecutableFinder->reveal(),
|
||||
$this->regularConn->reveal(),
|
||||
$this->noDbNameConn->reveal(),
|
||||
$noDbNameConn->reveal(),
|
||||
);
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
@ -77,7 +79,7 @@ class CreateDatabaseCommandTest extends TestCase
|
|||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('Database already exists. Run "db:migrate" command', $output);
|
||||
self::assertStringContainsString('Database already exists. Run "db:migrate" command', $output);
|
||||
$getDatabase->shouldHaveBeenCalledOnce();
|
||||
$listDatabases->shouldHaveBeenCalledOnce();
|
||||
$createDatabase->shouldNotHaveBeenCalled();
|
||||
|
@ -121,8 +123,8 @@ class CreateDatabaseCommandTest extends TestCase
|
|||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('Creating database tables...', $output);
|
||||
$this->assertStringContainsString('Database properly created!', $output);
|
||||
self::assertStringContainsString('Creating database tables...', $output);
|
||||
self::assertStringContainsString('Database properly created!', $output);
|
||||
$getDatabase->shouldHaveBeenCalledOnce();
|
||||
$listDatabases->shouldHaveBeenCalledOnce();
|
||||
$createDatabase->shouldNotHaveBeenCalled();
|
||||
|
|
|
@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\CLI\Command\Db;
|
|||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Db\MigrateDatabaseCommand;
|
||||
use Symfony\Component\Console\Application;
|
||||
|
@ -19,6 +20,8 @@ use Symfony\Component\Process\Process;
|
|||
|
||||
class MigrateDatabaseCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $processHelper;
|
||||
|
||||
|
@ -60,8 +63,8 @@ class MigrateDatabaseCommandTest extends TestCase
|
|||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('Migrating database...', $output);
|
||||
$this->assertStringContainsString('Database properly migrated!', $output);
|
||||
self::assertStringContainsString('Migrating database...', $output);
|
||||
self::assertStringContainsString('Database properly migrated!', $output);
|
||||
$runCommand->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
|
|
59
module/CLI/test/Command/Domain/ListDomainsCommandTest.php
Normal file
59
module/CLI/test/Command/Domain/ListDomainsCommandTest.php
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Domain;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Domain\ListDomainsCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class ListDomainsCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $domainService;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->domainService = $this->prophesize(DomainServiceInterface::class);
|
||||
|
||||
$command = new ListDomainsCommand($this->domainService->reveal(), 'foo.com');
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function allDomainsAreProperlyPrinted(): void
|
||||
{
|
||||
$expectedOutput = <<<OUTPUT
|
||||
+---------+------------+
|
||||
| Domain | Is default |
|
||||
+---------+------------+
|
||||
| foo.com | Yes |
|
||||
| bar.com | No |
|
||||
| baz.com | No |
|
||||
+---------+------------+
|
||||
|
||||
OUTPUT;
|
||||
$listDomains = $this->domainService->listDomainsWithout('foo.com')->willReturn([
|
||||
new Domain('bar.com'),
|
||||
new Domain('baz.com'),
|
||||
]);
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
|
||||
self::assertEquals($expectedOutput, $this->commandTester->getDisplay());
|
||||
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||
$listDomains->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
|||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\DeleteShortUrlCommand;
|
||||
use Shlinkio\Shlink\Core\Exception;
|
||||
|
@ -21,6 +22,8 @@ use const PHP_EOL;
|
|||
|
||||
class DeleteShortUrlCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $service;
|
||||
|
||||
|
@ -47,7 +50,7 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString(
|
||||
self::assertStringContainsString(
|
||||
sprintf('Short URL with short code "%s" successfully deleted.', $shortCode),
|
||||
$output,
|
||||
);
|
||||
|
@ -66,7 +69,7 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString(sprintf('No URL found with short code "%s"', $shortCode), $output);
|
||||
self::assertStringContainsString(sprintf('No URL found with short code "%s"', $shortCode), $output);
|
||||
$deleteByShortCode->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
|
@ -95,11 +98,11 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString(sprintf(
|
||||
self::assertStringContainsString(sprintf(
|
||||
'Impossible to delete short URL with short code "%s" since it has more than "10" visits.',
|
||||
$shortCode,
|
||||
), $output);
|
||||
$this->assertStringContainsString($expectedMessage, $output);
|
||||
self::assertStringContainsString($expectedMessage, $output);
|
||||
$deleteByShortCode->shouldHaveBeenCalledTimes($expectedDeleteCalls);
|
||||
}
|
||||
|
||||
|
@ -122,11 +125,11 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString(sprintf(
|
||||
self::assertStringContainsString(sprintf(
|
||||
'Impossible to delete short URL with short code "%s" since it has more than "10" visits.',
|
||||
$shortCode,
|
||||
), $output);
|
||||
$this->assertStringContainsString('Short URL was not deleted.', $output);
|
||||
self::assertStringContainsString('Short URL was not deleted.', $output);
|
||||
$deleteByShortCode->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,18 +7,22 @@ namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
|||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\GenerateShortUrlCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class GenerateShortUrlCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private const DOMAIN_CONFIG = [
|
||||
'schema' => 'http',
|
||||
'hostname' => 'foo.com',
|
||||
|
@ -40,7 +44,7 @@ class GenerateShortUrlCommandTest extends TestCase
|
|||
public function properShortCodeIsCreatedIfLongUrlIsCorrect(): void
|
||||
{
|
||||
$shortUrl = new ShortUrl('');
|
||||
$urlToShortCode = $this->urlShortener->urlToShortCode(Argument::cetera())->willReturn($shortUrl);
|
||||
$urlToShortCode = $this->urlShortener->shorten(Argument::cetera())->willReturn($shortUrl);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'longUrl' => 'http://domain.com/foo/bar',
|
||||
|
@ -48,8 +52,8 @@ class GenerateShortUrlCommandTest extends TestCase
|
|||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||
$this->assertStringContainsString($shortUrl->toString(self::DOMAIN_CONFIG), $output);
|
||||
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||
self::assertStringContainsString($shortUrl->toString(self::DOMAIN_CONFIG), $output);
|
||||
$urlToShortCode->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
|
@ -57,28 +61,28 @@ class GenerateShortUrlCommandTest extends TestCase
|
|||
public function exceptionWhileParsingLongUrlOutputsError(): void
|
||||
{
|
||||
$url = 'http://domain.com/invalid';
|
||||
$this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(InvalidUrlException::fromUrl($url))
|
||||
$this->urlShortener->shorten(Argument::cetera())->willThrow(InvalidUrlException::fromUrl($url))
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute(['longUrl' => $url]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
|
||||
$this->assertStringContainsString('Provided URL http://domain.com/invalid is invalid.', $output);
|
||||
self::assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
|
||||
self::assertStringContainsString('Provided URL http://domain.com/invalid is invalid.', $output);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function providingNonUniqueSlugOutputsError(): void
|
||||
{
|
||||
$urlToShortCode = $this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(
|
||||
$urlToShortCode = $this->urlShortener->shorten(Argument::cetera())->willThrow(
|
||||
NonUniqueSlugException::fromSlug('my-slug'),
|
||||
);
|
||||
|
||||
$this->commandTester->execute(['longUrl' => 'http://domain.com/invalid', '--customSlug' => 'my-slug']);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
|
||||
$this->assertStringContainsString('Provided slug "my-slug" is already in use', $output);
|
||||
self::assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
|
||||
self::assertStringContainsString('Provided slug "my-slug" is already in use', $output);
|
||||
$urlToShortCode->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
|
@ -86,7 +90,7 @@ class GenerateShortUrlCommandTest extends TestCase
|
|||
public function properlyProcessesProvidedTags(): void
|
||||
{
|
||||
$shortUrl = new ShortUrl('');
|
||||
$urlToShortCode = $this->urlShortener->urlToShortCode(
|
||||
$urlToShortCode = $this->urlShortener->shorten(
|
||||
Argument::type('string'),
|
||||
Argument::that(function (array $tags) {
|
||||
Assert::assertEquals(['foo', 'bar', 'baz', 'boo', 'zar'], $tags);
|
||||
|
@ -101,8 +105,38 @@ class GenerateShortUrlCommandTest extends TestCase
|
|||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||
$this->assertStringContainsString($shortUrl->toString(self::DOMAIN_CONFIG), $output);
|
||||
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||
self::assertStringContainsString($shortUrl->toString(self::DOMAIN_CONFIG), $output);
|
||||
$urlToShortCode->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideFlags
|
||||
*/
|
||||
public function urlValidationHasExpectedValueBasedOnProvidedTags(array $options, ?bool $expectedValidateUrl): void
|
||||
{
|
||||
$shortUrl = new ShortUrl('');
|
||||
$urlToShortCode = $this->urlShortener->shorten(
|
||||
Argument::type('string'),
|
||||
Argument::type('array'),
|
||||
Argument::that(function (ShortUrlMeta $meta) use ($expectedValidateUrl) {
|
||||
Assert::assertEquals($expectedValidateUrl, $meta->doValidateUrl());
|
||||
return $meta;
|
||||
}),
|
||||
)->willReturn($shortUrl);
|
||||
|
||||
$options['longUrl'] = 'http://domain.com/foo/bar';
|
||||
$this->commandTester->execute($options);
|
||||
|
||||
$urlToShortCode->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideFlags(): iterable
|
||||
{
|
||||
yield 'no flags' => [[], null];
|
||||
yield 'no-validate-url only' => [['--no-validate-url' => true], false];
|
||||
yield 'validate-url' => [['--validate-url' => true], true];
|
||||
yield 'both flags' => [['--validate-url' => true, '--no-validate-url' => true], false];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ use Laminas\Paginator\Adapter\ArrayAdapter;
|
|||
use Laminas\Paginator\Paginator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\GetVisitsCommand;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
|
@ -27,6 +28,8 @@ use function sprintf;
|
|||
|
||||
class GetVisitsCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $visitsTracker;
|
||||
|
||||
|
@ -88,7 +91,7 @@ class GetVisitsCommandTest extends TestCase
|
|||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$info->shouldHaveBeenCalledOnce();
|
||||
$this->assertStringContainsString(
|
||||
self::assertStringContainsString(
|
||||
sprintf('Ignored provided "startDate" since its value "%s" is not a valid date', $startDate),
|
||||
$output,
|
||||
);
|
||||
|
@ -108,8 +111,8 @@ class GetVisitsCommandTest extends TestCase
|
|||
|
||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertStringContainsString('foo', $output);
|
||||
$this->assertStringContainsString('Spain', $output);
|
||||
$this->assertStringContainsString('bar', $output);
|
||||
self::assertStringContainsString('foo', $output);
|
||||
self::assertStringContainsString('Spain', $output);
|
||||
self::assertStringContainsString('bar', $output);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ use Laminas\Paginator\Adapter\ArrayAdapter;
|
|||
use Laminas\Paginator\Paginator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
|
@ -21,6 +22,8 @@ use function explode;
|
|||
|
||||
class ListShortUrlsCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $shortUrlService;
|
||||
|
||||
|
@ -50,9 +53,9 @@ class ListShortUrlsCommandTest extends TestCase
|
|||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('Continue with page 2?', $output);
|
||||
$this->assertStringContainsString('Continue with page 3?', $output);
|
||||
$this->assertStringContainsString('Continue with page 4?', $output);
|
||||
self::assertStringContainsString('Continue with page 2?', $output);
|
||||
self::assertStringContainsString('Continue with page 3?', $output);
|
||||
self::assertStringContainsString('Continue with page 4?', $output);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -72,13 +75,13 @@ class ListShortUrlsCommandTest extends TestCase
|
|||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('url_1', $output);
|
||||
$this->assertStringContainsString('url_9', $output);
|
||||
$this->assertStringNotContainsString('url_10', $output);
|
||||
$this->assertStringNotContainsString('url_20', $output);
|
||||
$this->assertStringNotContainsString('url_30', $output);
|
||||
$this->assertStringContainsString('Continue with page 2?', $output);
|
||||
$this->assertStringNotContainsString('Continue with page 3?', $output);
|
||||
self::assertStringContainsString('url_1', $output);
|
||||
self::assertStringContainsString('url_9', $output);
|
||||
self::assertStringNotContainsString('url_10', $output);
|
||||
self::assertStringNotContainsString('url_20', $output);
|
||||
self::assertStringNotContainsString('url_30', $output);
|
||||
self::assertStringContainsString('Continue with page 2?', $output);
|
||||
self::assertStringNotContainsString('Continue with page 3?', $output);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -103,7 +106,7 @@ class ListShortUrlsCommandTest extends TestCase
|
|||
$this->commandTester->setInputs(['y']);
|
||||
$this->commandTester->execute(['--showTags' => true]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertStringContainsString('Tags', $output);
|
||||
self::assertStringContainsString('Tags', $output);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\ResolveUrlCommand;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
|
@ -20,6 +21,8 @@ use const PHP_EOL;
|
|||
|
||||
class ResolveUrlCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $urlResolver;
|
||||
|
||||
|
@ -44,7 +47,7 @@ class ResolveUrlCommandTest extends TestCase
|
|||
|
||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertEquals('Long URL: ' . $expectedUrl . PHP_EOL, $output);
|
||||
self::assertEquals('Long URL: ' . $expectedUrl . PHP_EOL, $output);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -59,6 +62,6 @@ class ResolveUrlCommandTest extends TestCase
|
|||
|
||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertStringContainsString(sprintf('No URL found with short code "%s"', $shortCode), $output);
|
||||
self::assertStringContainsString(sprintf('No URL found with short code "%s"', $shortCode), $output);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
|||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Tag\CreateTagCommand;
|
||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||
|
@ -14,6 +15,8 @@ use Symfony\Component\Console\Tester\CommandTester;
|
|||
|
||||
class CreateTagCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $tagService;
|
||||
|
||||
|
@ -34,7 +37,7 @@ class CreateTagCommandTest extends TestCase
|
|||
$this->commandTester->execute([]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertStringContainsString('You have to provide at least one tag name', $output);
|
||||
self::assertStringContainsString('You have to provide at least one tag name', $output);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -48,7 +51,7 @@ class CreateTagCommandTest extends TestCase
|
|||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('Tags properly created', $output);
|
||||
self::assertStringContainsString('Tags properly created', $output);
|
||||
$createTags->shouldHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Tag\DeleteTagsCommand;
|
||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||
|
@ -13,6 +14,8 @@ use Symfony\Component\Console\Tester\CommandTester;
|
|||
|
||||
class DeleteTagsCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $tagService;
|
||||
|
||||
|
@ -33,7 +36,7 @@ class DeleteTagsCommandTest extends TestCase
|
|||
$this->commandTester->execute([]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertStringContainsString('You have to provide at least one tag name', $output);
|
||||
self::assertStringContainsString('You have to provide at least one tag name', $output);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -48,7 +51,7 @@ class DeleteTagsCommandTest extends TestCase
|
|||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('Tags properly deleted', $output);
|
||||
self::assertStringContainsString('Tags properly deleted', $output);
|
||||
$deleteTags->shouldHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Tag\ListTagsCommand;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
|
@ -15,6 +16,8 @@ use Symfony\Component\Console\Tester\CommandTester;
|
|||
|
||||
class ListTagsCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $tagService;
|
||||
|
||||
|
@ -37,7 +40,7 @@ class ListTagsCommandTest extends TestCase
|
|||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('No tags found', $output);
|
||||
self::assertStringContainsString('No tags found', $output);
|
||||
$tagsInfo->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
|
@ -52,12 +55,12 @@ class ListTagsCommandTest extends TestCase
|
|||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('| foo', $output);
|
||||
$this->assertStringContainsString('| bar', $output);
|
||||
$this->assertStringContainsString('| 10 ', $output);
|
||||
$this->assertStringContainsString('| 2 ', $output);
|
||||
$this->assertStringContainsString('| 7 ', $output);
|
||||
$this->assertStringContainsString('| 32 ', $output);
|
||||
self::assertStringContainsString('| foo', $output);
|
||||
self::assertStringContainsString('| bar', $output);
|
||||
self::assertStringContainsString('| 10 ', $output);
|
||||
self::assertStringContainsString('| 2 ', $output);
|
||||
self::assertStringContainsString('| 7 ', $output);
|
||||
self::assertStringContainsString('| 32 ', $output);
|
||||
$tagsInfo->shouldHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Tag\RenameTagCommand;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
|
@ -15,6 +16,8 @@ use Symfony\Component\Console\Tester\CommandTester;
|
|||
|
||||
class RenameTagCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $tagService;
|
||||
|
||||
|
@ -42,7 +45,7 @@ class RenameTagCommandTest extends TestCase
|
|||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('Tag with name "foo" could not be found', $output);
|
||||
self::assertStringContainsString('Tag with name "foo" could not be found', $output);
|
||||
$renameTag->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
|
@ -59,7 +62,7 @@ class RenameTagCommandTest extends TestCase
|
|||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('Tag properly renamed', $output);
|
||||
self::assertStringContainsString('Tag properly renamed', $output);
|
||||
$renameTag->shouldHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\CLI\Command\Visit;
|
|||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Visit\LocateVisitsCommand;
|
||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
|
@ -32,10 +33,11 @@ use const PHP_EOL;
|
|||
|
||||
class LocateVisitsCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $visitService;
|
||||
private ObjectProphecy $ipResolver;
|
||||
private ObjectProphecy $locker;
|
||||
private ObjectProphecy $lock;
|
||||
private ObjectProphecy $dbUpdater;
|
||||
|
||||
|
@ -45,17 +47,17 @@ class LocateVisitsCommandTest extends TestCase
|
|||
$this->ipResolver = $this->prophesize(IpLocationResolverInterface::class);
|
||||
$this->dbUpdater = $this->prophesize(GeolocationDbUpdaterInterface::class);
|
||||
|
||||
$this->locker = $this->prophesize(Lock\LockFactory::class);
|
||||
$locker = $this->prophesize(Lock\LockFactory::class);
|
||||
$this->lock = $this->prophesize(Lock\LockInterface::class);
|
||||
$this->lock->acquire(false)->willReturn(true);
|
||||
$this->lock->release()->will(function (): void {
|
||||
});
|
||||
$this->locker->createLock(Argument::type('string'), 90.0, false)->willReturn($this->lock->reveal());
|
||||
$locker->createLock(Argument::type('string'), 90.0, false)->willReturn($this->lock->reveal());
|
||||
|
||||
$command = new LocateVisitsCommand(
|
||||
$this->visitService->reveal(),
|
||||
$this->ipResolver->reveal(),
|
||||
$this->locker->reveal(),
|
||||
$locker->reveal(),
|
||||
$this->dbUpdater->reveal(),
|
||||
);
|
||||
$app = new Application();
|
||||
|
@ -92,11 +94,11 @@ class LocateVisitsCommandTest extends TestCase
|
|||
$this->commandTester->execute($args);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('Processing IP 1.2.3.0', $output);
|
||||
self::assertStringContainsString('Processing IP 1.2.3.0', $output);
|
||||
if ($expectWarningPrint) {
|
||||
$this->assertStringContainsString('Continue at your own risk', $output);
|
||||
self::assertStringContainsString('Continue at your own', $output);
|
||||
} else {
|
||||
$this->assertStringNotContainsString('Continue at your own risk', $output);
|
||||
self::assertStringNotContainsString('Continue at your own', $output);
|
||||
}
|
||||
$locateVisits->shouldHaveBeenCalledTimes($expectedUnlocatedCalls);
|
||||
$locateEmptyVisits->shouldHaveBeenCalledTimes($expectedEmptyCalls);
|
||||
|
@ -132,11 +134,11 @@ class LocateVisitsCommandTest extends TestCase
|
|||
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertStringContainsString($message, $output);
|
||||
self::assertStringContainsString($message, $output);
|
||||
if (empty($address)) {
|
||||
$this->assertStringNotContainsString('Processing IP', $output);
|
||||
self::assertStringNotContainsString('Processing IP', $output);
|
||||
} else {
|
||||
$this->assertStringContainsString('Processing IP', $output);
|
||||
self::assertStringContainsString('Processing IP', $output);
|
||||
}
|
||||
$locateVisits->shouldHaveBeenCalledOnce();
|
||||
$resolveIpLocation->shouldNotHaveBeenCalled();
|
||||
|
@ -164,7 +166,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('An error occurred while locating IP. Skipped', $output);
|
||||
self::assertStringContainsString('An error occurred while locating IP. Skipped', $output);
|
||||
$locateVisits->shouldHaveBeenCalledOnce();
|
||||
$resolveIpLocation->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
@ -192,7 +194,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString(
|
||||
self::assertStringContainsString(
|
||||
sprintf('Command "%s" is already in progress. Skipping.', LocateVisitsCommand::NAME),
|
||||
$output,
|
||||
);
|
||||
|
@ -222,11 +224,11 @@ class LocateVisitsCommandTest extends TestCase
|
|||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString(
|
||||
self::assertStringContainsString(
|
||||
sprintf('%s GeoLite2 database...', $olderDbExists ? 'Updating' : 'Downloading'),
|
||||
$output,
|
||||
);
|
||||
$this->assertStringContainsString($expectedMessage, $output);
|
||||
self::assertStringContainsString($expectedMessage, $output);
|
||||
$locateVisits->shouldHaveBeenCalledTimes((int) $olderDbExists);
|
||||
$checkDbUpdate->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
@ -243,7 +245,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||
$this->commandTester->execute(['--all' => true]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('The --all flag has no effect on its own', $output);
|
||||
self::assertStringContainsString('The --all flag has no effect on its own', $output);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,11 +17,11 @@ class ConfigProviderTest extends TestCase
|
|||
}
|
||||
|
||||
/** @test */
|
||||
public function confiIsProperlyReturned(): void
|
||||
public function configIsProperlyReturned(): void
|
||||
{
|
||||
$config = ($this->configProvider)();
|
||||
|
||||
$this->assertArrayHasKey('cli', $config);
|
||||
$this->assertArrayHasKey('dependencies', $config);
|
||||
self::assertArrayHasKey('cli', $config);
|
||||
self::assertArrayHasKey('dependencies', $config);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,13 +20,13 @@ class GeolocationDbUpdateFailedExceptionTest extends TestCase
|
|||
{
|
||||
$e = GeolocationDbUpdateFailedException::create($olderDbExists, $prev);
|
||||
|
||||
$this->assertEquals($olderDbExists, $e->olderDbExists());
|
||||
$this->assertEquals(
|
||||
self::assertEquals($olderDbExists, $e->olderDbExists());
|
||||
self::assertEquals(
|
||||
'An error occurred while updating geolocation database, and an older version could not be found',
|
||||
$e->getMessage(),
|
||||
);
|
||||
$this->assertEquals(0, $e->getCode());
|
||||
$this->assertEquals($prev, $e->getPrevious());
|
||||
self::assertEquals(0, $e->getCode());
|
||||
self::assertEquals($prev, $e->getPrevious());
|
||||
}
|
||||
|
||||
public function provideCreateArgs(): iterable
|
||||
|
|
|
@ -7,6 +7,7 @@ namespace ShlinkioTest\Shlink\CLI\Factory;
|
|||
use Laminas\ServiceManager\ServiceManager;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
|
@ -15,6 +16,8 @@ use Symfony\Component\Console\Command\Command;
|
|||
|
||||
class ApplicationFactoryTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private ApplicationFactory $factory;
|
||||
|
||||
public function setUp(): void
|
||||
|
@ -37,9 +40,9 @@ class ApplicationFactoryTest extends TestCase
|
|||
|
||||
$instance = ($this->factory)($sm);
|
||||
|
||||
$this->assertTrue($instance->has('foo'));
|
||||
$this->assertTrue($instance->has('bar'));
|
||||
$this->assertFalse($instance->has('baz'));
|
||||
self::assertTrue($instance->has('foo'));
|
||||
self::assertTrue($instance->has('bar'));
|
||||
self::assertFalse($instance->has('baz'));
|
||||
}
|
||||
|
||||
private function createServiceManager(array $config = []): ServiceManager
|
||||
|
|
|
@ -9,6 +9,7 @@ use GeoIp2\Database\Reader;
|
|||
use MaxMind\Db\Reader\Metadata;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
||||
|
@ -22,35 +23,35 @@ use function range;
|
|||
|
||||
class GeolocationDbUpdaterTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private GeolocationDbUpdater $geolocationDbUpdater;
|
||||
private ObjectProphecy $dbUpdater;
|
||||
private ObjectProphecy $geoLiteDbReader;
|
||||
private ObjectProphecy $locker;
|
||||
private ObjectProphecy $lock;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->dbUpdater = $this->prophesize(DbUpdaterInterface::class);
|
||||
$this->geoLiteDbReader = $this->prophesize(Reader::class);
|
||||
|
||||
$this->locker = $this->prophesize(Lock\LockFactory::class);
|
||||
$this->lock = $this->prophesize(Lock\LockInterface::class);
|
||||
$this->lock->acquire(true)->willReturn(true);
|
||||
$this->lock->release()->will(function (): void {
|
||||
$locker = $this->prophesize(Lock\LockFactory::class);
|
||||
$lock = $this->prophesize(Lock\LockInterface::class);
|
||||
$lock->acquire(true)->willReturn(true);
|
||||
$lock->release()->will(function (): void {
|
||||
});
|
||||
$this->locker->createLock(Argument::type('string'))->willReturn($this->lock->reveal());
|
||||
$locker->createLock(Argument::type('string'))->willReturn($lock->reveal());
|
||||
|
||||
$this->geolocationDbUpdater = new GeolocationDbUpdater(
|
||||
$this->dbUpdater->reveal(),
|
||||
$this->geoLiteDbReader->reveal(),
|
||||
$this->locker->reveal(),
|
||||
$locker->reveal(),
|
||||
);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function exceptionIsThrownWhenOlderDbDoesNotExistAndDownloadFails(): void
|
||||
{
|
||||
$mustBeUpdated = fn () => $this->assertTrue(true);
|
||||
$mustBeUpdated = fn () => self::assertTrue(true);
|
||||
$prev = new RuntimeException('');
|
||||
|
||||
$fileExists = $this->dbUpdater->databaseFileExists()->willReturn(false);
|
||||
|
@ -59,12 +60,12 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||
|
||||
try {
|
||||
$this->geolocationDbUpdater->checkDbUpdate($mustBeUpdated);
|
||||
$this->assertTrue(false); // If this is reached, the test will fail
|
||||
self::assertTrue(false); // If this is reached, the test will fail
|
||||
} catch (Throwable $e) {
|
||||
/** @var GeolocationDbUpdateFailedException $e */
|
||||
$this->assertInstanceOf(GeolocationDbUpdateFailedException::class, $e);
|
||||
$this->assertSame($prev, $e->getPrevious());
|
||||
$this->assertFalse($e->olderDbExists());
|
||||
self::assertInstanceOf(GeolocationDbUpdateFailedException::class, $e);
|
||||
self::assertSame($prev, $e->getPrevious());
|
||||
self::assertFalse($e->olderDbExists());
|
||||
}
|
||||
|
||||
$fileExists->shouldHaveBeenCalledOnce();
|
||||
|
@ -95,12 +96,12 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||
|
||||
try {
|
||||
$this->geolocationDbUpdater->checkDbUpdate();
|
||||
$this->assertTrue(false); // If this is reached, the test will fail
|
||||
self::assertTrue(false); // If this is reached, the test will fail
|
||||
} catch (Throwable $e) {
|
||||
/** @var GeolocationDbUpdateFailedException $e */
|
||||
$this->assertInstanceOf(GeolocationDbUpdateFailedException::class, $e);
|
||||
$this->assertSame($prev, $e->getPrevious());
|
||||
$this->assertTrue($e->olderDbExists());
|
||||
self::assertInstanceOf(GeolocationDbUpdateFailedException::class, $e);
|
||||
self::assertSame($prev, $e->getPrevious());
|
||||
self::assertTrue($e->olderDbExists());
|
||||
}
|
||||
|
||||
$fileExists->shouldHaveBeenCalledOnce();
|
||||
|
|
|
@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\CLI\Util;
|
|||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use ReflectionObject;
|
||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||
|
@ -15,6 +16,8 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||
|
||||
class ShlinkTableTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private ShlinkTable $shlinkTable;
|
||||
private ObjectProphecy $baseTable;
|
||||
|
||||
|
@ -60,6 +63,6 @@ class ShlinkTableTest extends TestCase
|
|||
$baseTable = $ref->getProperty('baseTable');
|
||||
$baseTable->setAccessible(true);
|
||||
|
||||
$this->assertInstanceOf(Table::class, $baseTable->getValue($instance));
|
||||
self::assertInstanceOf(Table::class, $baseTable->getValue($instance));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,9 +7,9 @@ namespace Shlinkio\Shlink\Core;
|
|||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Mezzio\Template\TemplateRendererInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Shlinkio\Shlink\Core\Domain\Resolver;
|
||||
use Shlinkio\Shlink\Core\ErrorHandler;
|
||||
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||
use Shlinkio\Shlink\Importer\ImportedLinksProcessorInterface;
|
||||
|
||||
return [
|
||||
|
||||
|
@ -31,16 +31,25 @@ return [
|
|||
Tag\TagService::class => ConfigAbstractFactory::class,
|
||||
Service\ShortUrl\DeleteShortUrlService::class => ConfigAbstractFactory::class,
|
||||
Service\ShortUrl\ShortUrlResolver::class => ConfigAbstractFactory::class,
|
||||
Service\ShortUrl\ShortCodeHelper::class => ConfigAbstractFactory::class,
|
||||
Domain\DomainService::class => ConfigAbstractFactory::class,
|
||||
|
||||
Util\UrlValidator::class => ConfigAbstractFactory::class,
|
||||
Util\DoctrineBatchHelper::class => ConfigAbstractFactory::class,
|
||||
|
||||
Action\RedirectAction::class => ConfigAbstractFactory::class,
|
||||
Action\PixelAction::class => ConfigAbstractFactory::class,
|
||||
Action\QrCodeAction::class => ConfigAbstractFactory::class,
|
||||
|
||||
Resolver\PersistenceDomainResolver::class => ConfigAbstractFactory::class,
|
||||
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class => ConfigAbstractFactory::class,
|
||||
|
||||
Mercure\MercureUpdatesGenerator::class => ConfigAbstractFactory::class,
|
||||
|
||||
Importer\ImportedLinksProcessor::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
|
||||
'aliases' => [
|
||||
ImportedLinksProcessorInterface::class => Importer\ImportedLinksProcessor::class,
|
||||
],
|
||||
],
|
||||
|
||||
|
@ -53,7 +62,12 @@ return [
|
|||
Options\NotFoundRedirectOptions::class => ['config.not_found_redirects'],
|
||||
Options\UrlShortenerOptions::class => ['config.url_shortener'],
|
||||
|
||||
Service\UrlShortener::class => [Util\UrlValidator::class, 'em', Resolver\PersistenceDomainResolver::class],
|
||||
Service\UrlShortener::class => [
|
||||
Util\UrlValidator::class,
|
||||
'em',
|
||||
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class,
|
||||
Service\ShortUrl\ShortCodeHelper::class,
|
||||
],
|
||||
Service\VisitsTracker::class => [
|
||||
'em',
|
||||
EventDispatcherInterface::class,
|
||||
|
@ -69,8 +83,11 @@ return [
|
|||
Service\ShortUrl\ShortUrlResolver::class,
|
||||
],
|
||||
Service\ShortUrl\ShortUrlResolver::class => ['em'],
|
||||
Service\ShortUrl\ShortCodeHelper::class => ['em'],
|
||||
Domain\DomainService::class => ['em'],
|
||||
|
||||
Util\UrlValidator::class => ['httpClient', Options\UrlShortenerOptions::class],
|
||||
Util\DoctrineBatchHelper::class => ['em'],
|
||||
|
||||
Action\RedirectAction::class => [
|
||||
Service\ShortUrl\ShortUrlResolver::class,
|
||||
|
@ -91,9 +108,16 @@ return [
|
|||
'Logger_Shlink',
|
||||
],
|
||||
|
||||
Resolver\PersistenceDomainResolver::class => ['em'],
|
||||
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class => ['em'],
|
||||
|
||||
Mercure\MercureUpdatesGenerator::class => ['config.url_shortener.domain'],
|
||||
|
||||
Importer\ImportedLinksProcessor::class => [
|
||||
'em',
|
||||
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class,
|
||||
Service\ShortUrl\ShortCodeHelper::class,
|
||||
Util\DoctrineBatchHelper::class,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
@ -11,7 +11,8 @@ use Doctrine\ORM\Mapping\ClassMetadata;
|
|||
return static function (ClassMetadata $metadata, array $emConfig): void {
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(determineTableName('domains', $emConfig));
|
||||
$builder->setTable(determineTableName('domains', $emConfig))
|
||||
->setCustomRepositoryClass(Domain\Repository\DomainRepository::class);
|
||||
|
||||
$builder->createField('id', Types::BIGINT)
|
||||
->columnName('id')
|
||||
|
|
|
@ -8,6 +8,7 @@ use Doctrine\DBAL\Types\Types;
|
|||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
return static function (ClassMetadata $metadata, array $emConfig): void {
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
@ -51,6 +52,16 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
|
|||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('importSource', Types::STRING)
|
||||
->columnName('import_source')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('importOriginalShortCode', Types::STRING)
|
||||
->columnName('import_original_short_code')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createOneToMany('visits', Entity\Visit::class)
|
||||
->mappedBy('shortUrl')
|
||||
->fetchExtraLazy()
|
||||
|
@ -68,5 +79,9 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
|
|||
->cascadePersist()
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('authorApiKey', ApiKey::class)
|
||||
->addJoinColumn('author_api_key_id', 'id', true, false, 'SET NULL')
|
||||
->build();
|
||||
|
||||
$builder->addUniqueConstraint(['short_code', 'domain_id'], 'unique_short_code_plus_domain');
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core;
|
|||
use Cake\Chronos\Chronos;
|
||||
use DateTimeInterface;
|
||||
use Fig\Http\Message\StatusCodeInterface;
|
||||
use Laminas\InputFilter\InputFilter;
|
||||
use PUGX\Shortid\Factory as ShortIdFactory;
|
||||
|
||||
use function sprintf;
|
||||
|
@ -62,3 +63,15 @@ function determineTableName(string $tableName, array $emConfig = []): string
|
|||
|
||||
return sprintf('%s.%s', $schema, $tableName);
|
||||
}
|
||||
|
||||
function getOptionalIntFromInputFilter(InputFilter $inputFilter, string $fieldName): ?int
|
||||
{
|
||||
$value = $inputFilter->getValue($fieldName);
|
||||
return $value !== null ? (int) $value : null;
|
||||
}
|
||||
|
||||
function getOptionalBoolFromInputFilter(InputFilter $inputFilter, string $fieldName): ?bool
|
||||
{
|
||||
$value = $inputFilter->getValue($fieldName);
|
||||
return $value !== null ? (bool) $value : null;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Shlinkio\Shlink\Core\Action;
|
||||
|
||||
use Endroid\QrCode\QrCode;
|
||||
use Endroid\QrCode\Writer\SvgWriter;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
|
@ -51,6 +52,11 @@ class QrCodeAction implements MiddlewareInterface
|
|||
$qrCode->setSize($this->getSizeParam($request));
|
||||
$qrCode->setMargin(0);
|
||||
|
||||
$format = $request->getQueryParams()['format'] ?? 'png';
|
||||
if ($format === 'svg') {
|
||||
$qrCode->setWriter(new SvgWriter());
|
||||
}
|
||||
|
||||
return new QrCodeResponse($qrCode);
|
||||
}
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ class SimplifiedConfigParser
|
|||
'anonymize_remote_addr' => ['url_shortener', 'anonymize_remote_addr'],
|
||||
'redirect_status_code' => ['url_shortener', 'redirect_status_code'],
|
||||
'redirect_cache_lifetime' => ['url_shortener', 'redirect_cache_lifetime'],
|
||||
'port' => ['mezzio-swoole', 'swoole-http-server', 'port'],
|
||||
];
|
||||
private const SIMPLIFIED_CONFIG_SIDE_EFFECTS = [
|
||||
'delete_short_url_threshold' => [
|
||||
|
|
29
module/Core/src/Domain/DomainService.php
Normal file
29
module/Core/src/Domain/DomainService.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Domain;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Shlinkio\Shlink\Core\Domain\Repository\DomainRepositoryInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
|
||||
class DomainService implements DomainServiceInterface
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Domain[]
|
||||
*/
|
||||
public function listDomainsWithout(?string $excludeDomain = null): array
|
||||
{
|
||||
/** @var DomainRepositoryInterface $repo */
|
||||
$repo = $this->em->getRepository(Domain::class);
|
||||
return $repo->findDomainsWithout($excludeDomain);
|
||||
}
|
||||
}
|
15
module/Core/src/Domain/DomainServiceInterface.php
Normal file
15
module/Core/src/Domain/DomainServiceInterface.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Domain;
|
||||
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
|
||||
interface DomainServiceInterface
|
||||
{
|
||||
/**
|
||||
* @return Domain[]
|
||||
*/
|
||||
public function listDomainsWithout(?string $excludeDomain = null): array;
|
||||
}
|
26
module/Core/src/Domain/Repository/DomainRepository.php
Normal file
26
module/Core/src/Domain/Repository/DomainRepository.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Domain\Repository;
|
||||
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
|
||||
class DomainRepository extends EntityRepository implements DomainRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* @return Domain[]
|
||||
*/
|
||||
public function findDomainsWithout(?string $excludedAuthority = null): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('d')->orderBy('d.authority', 'ASC');
|
||||
|
||||
if ($excludedAuthority !== null) {
|
||||
$qb->where($qb->expr()->neq('d.authority', ':excludedAuthority'))
|
||||
->setParameter('excludedAuthority', $excludedAuthority);
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Domain\Repository;
|
||||
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
|
||||
interface DomainRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
/**
|
||||
* @return Domain[]
|
||||
*/
|
||||
public function findDomainsWithout(?string $excludedAuthority = null): array;
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Domain\Resolver;
|
||||
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
|
||||
interface DomainResolverInterface
|
||||
{
|
||||
public function resolveDomain(?string $domain): ?Domain;
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Domain\Resolver;
|
||||
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
|
||||
class SimpleDomainResolver implements DomainResolverInterface
|
||||
{
|
||||
public function resolveDomain(?string $domain): ?Domain
|
||||
{
|
||||
return $domain !== null ? new Domain($domain) : null;
|
||||
}
|
||||
}
|
|
@ -9,16 +9,16 @@ use Doctrine\Common\Collections\ArrayCollection;
|
|||
use Doctrine\Common\Collections\Collection;
|
||||
use Laminas\Diactoros\Uri;
|
||||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||
use Shlinkio\Shlink\Core\Domain\Resolver\DomainResolverInterface;
|
||||
use Shlinkio\Shlink\Core\Domain\Resolver\SimpleDomainResolver;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortCodeCannotBeRegeneratedException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlEdit;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Resolver\ShortUrlRelationResolverInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Resolver\SimpleShortUrlRelationResolver;
|
||||
use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter;
|
||||
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
use function array_reduce;
|
||||
use function count;
|
||||
use function Functional\contains;
|
||||
use function Functional\invoke;
|
||||
use function Shlinkio\Shlink\Core\generateRandomShortCode;
|
||||
|
||||
class ShortUrl extends AbstractEntity
|
||||
|
@ -36,13 +36,17 @@ class ShortUrl extends AbstractEntity
|
|||
private ?Domain $domain = null;
|
||||
private bool $customSlugWasProvided;
|
||||
private int $shortCodeLength;
|
||||
private ?string $importSource = null;
|
||||
private ?string $importOriginalShortCode = null;
|
||||
private ?ApiKey $authorApiKey = null;
|
||||
|
||||
public function __construct(
|
||||
string $longUrl,
|
||||
?ShortUrlMeta $meta = null,
|
||||
?DomainResolverInterface $domainResolver = null
|
||||
?ShortUrlRelationResolverInterface $relationResolver = null
|
||||
) {
|
||||
$meta = $meta ?? ShortUrlMeta::createEmpty();
|
||||
$relationResolver = $relationResolver ?? new SimpleShortUrlRelationResolver();
|
||||
|
||||
$this->longUrl = $longUrl;
|
||||
$this->dateCreated = Chronos::now();
|
||||
|
@ -54,7 +58,29 @@ class ShortUrl extends AbstractEntity
|
|||
$this->customSlugWasProvided = $meta->hasCustomSlug();
|
||||
$this->shortCodeLength = $meta->getShortCodeLength();
|
||||
$this->shortCode = $meta->getCustomSlug() ?? generateRandomShortCode($this->shortCodeLength);
|
||||
$this->domain = ($domainResolver ?? new SimpleDomainResolver())->resolveDomain($meta->getDomain());
|
||||
$this->domain = $relationResolver->resolveDomain($meta->getDomain());
|
||||
$this->authorApiKey = $relationResolver->resolveApiKey($meta->getApiKey());
|
||||
}
|
||||
|
||||
public static function fromImport(
|
||||
ImportedShlinkUrl $url,
|
||||
bool $importShortCode,
|
||||
?ShortUrlRelationResolverInterface $relationResolver = null
|
||||
): self {
|
||||
$meta = [
|
||||
ShortUrlMetaInputFilter::DOMAIN => $url->domain(),
|
||||
ShortUrlMetaInputFilter::VALIDATE_URL => false,
|
||||
];
|
||||
if ($importShortCode) {
|
||||
$meta[ShortUrlMetaInputFilter::CUSTOM_SLUG] = $url->shortCode();
|
||||
}
|
||||
|
||||
$instance = new self($url->longUrl(), ShortUrlMeta::fromRawData($meta), $relationResolver);
|
||||
$instance->importSource = $url->source();
|
||||
$instance->importOriginalShortCode = $url->shortCode();
|
||||
$instance->dateCreated = Chronos::instance($url->createdAt());
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
public function getLongUrl(): string
|
||||
|
@ -113,10 +139,10 @@ class ShortUrl extends AbstractEntity
|
|||
/**
|
||||
* @throws ShortCodeCannotBeRegeneratedException
|
||||
*/
|
||||
public function regenerateShortCode(): self
|
||||
public function regenerateShortCode(): void
|
||||
{
|
||||
// In ShortUrls where a custom slug was provided, do nothing
|
||||
if ($this->customSlugWasProvided) {
|
||||
// In ShortUrls where a custom slug was provided, throw error, unless it is an imported one
|
||||
if ($this->customSlugWasProvided && $this->importSource === null) {
|
||||
throw ShortCodeCannotBeRegeneratedException::forShortUrlWithCustomSlug();
|
||||
}
|
||||
|
||||
|
@ -126,7 +152,6 @@ class ShortUrl extends AbstractEntity
|
|||
}
|
||||
|
||||
$this->shortCode = generateRandomShortCode($this->shortCodeLength);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValidSince(): ?Chronos
|
||||
|
@ -195,27 +220,4 @@ class ShortUrl extends AbstractEntity
|
|||
|
||||
return $this->domain->getAuthority();
|
||||
}
|
||||
|
||||
public function matchesCriteria(ShortUrlMeta $meta, array $tags): bool
|
||||
{
|
||||
if ($meta->hasMaxVisits() && $meta->getMaxVisits() !== $this->maxVisits) {
|
||||
return false;
|
||||
}
|
||||
if ($meta->hasDomain() && $meta->getDomain() !== $this->resolveDomain()) {
|
||||
return false;
|
||||
}
|
||||
if ($meta->hasValidSince() && ($this->validSince === null || ! $meta->getValidSince()->eq($this->validSince))) {
|
||||
return false;
|
||||
}
|
||||
if ($meta->hasValidUntil() && ($this->validUntil === null || ! $meta->getValidUntil()->eq($this->validUntil))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$shortUrlTags = invoke($this->getTags(), '__toString');
|
||||
return count($shortUrlTags) === count($tags) && array_reduce(
|
||||
$tags,
|
||||
fn (bool $hasAllTags, string $tag) => $hasAllTags && contains($shortUrlTags, $tag),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
98
module/Core/src/Importer/ImportedLinksProcessor.php
Normal file
98
module/Core/src/Importer/ImportedLinksProcessor.php
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Importer;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortCodeHelperInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Resolver\ShortUrlRelationResolverInterface;
|
||||
use Shlinkio\Shlink\Core\Util\DoctrineBatchHelperInterface;
|
||||
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
|
||||
use Shlinkio\Shlink\Importer\ImportedLinksProcessorInterface;
|
||||
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
|
||||
use Symfony\Component\Console\Style\StyleInterface;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class ImportedLinksProcessor implements ImportedLinksProcessorInterface
|
||||
{
|
||||
use TagManagerTrait;
|
||||
|
||||
private EntityManagerInterface $em;
|
||||
private ShortUrlRelationResolverInterface $relationResolver;
|
||||
private ShortCodeHelperInterface $shortCodeHelper;
|
||||
private DoctrineBatchHelperInterface $batchHelper;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
ShortUrlRelationResolverInterface $relationResolver,
|
||||
ShortCodeHelperInterface $shortCodeHelper,
|
||||
DoctrineBatchHelperInterface $batchHelper
|
||||
) {
|
||||
$this->em = $em;
|
||||
$this->relationResolver = $relationResolver;
|
||||
$this->shortCodeHelper = $shortCodeHelper;
|
||||
$this->batchHelper = $batchHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param iterable|ImportedShlinkUrl[] $shlinkUrls
|
||||
*/
|
||||
public function process(StyleInterface $io, iterable $shlinkUrls, array $params): void
|
||||
{
|
||||
/** @var ShortUrlRepositoryInterface $shortUrlRepo */
|
||||
$shortUrlRepo = $this->em->getRepository(ShortUrl::class);
|
||||
$importShortCodes = $params['import_short_codes'];
|
||||
$iterable = $this->batchHelper->wrapIterable($shlinkUrls, 100);
|
||||
|
||||
/** @var ImportedShlinkUrl $url */
|
||||
foreach ($iterable as $url) {
|
||||
$longUrl = $url->longUrl();
|
||||
|
||||
// Skip already imported URLs
|
||||
if ($shortUrlRepo->importedUrlExists($url)) {
|
||||
$io->text(sprintf('%s: <comment>Skipped</comment>', $longUrl));
|
||||
continue;
|
||||
}
|
||||
|
||||
$shortUrl = ShortUrl::fromImport($url, $importShortCodes, $this->relationResolver);
|
||||
$shortUrl->setTags($this->tagNamesToEntities($this->em, $url->tags()));
|
||||
|
||||
if (! $this->handleShortCodeUniqueness($url, $shortUrl, $io, $importShortCodes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->em->persist($shortUrl);
|
||||
$io->text(sprintf('%s: <info>Imported</info>', $longUrl));
|
||||
}
|
||||
}
|
||||
|
||||
private function handleShortCodeUniqueness(
|
||||
ImportedShlinkUrl $url,
|
||||
ShortUrl $shortUrl,
|
||||
StyleInterface $io,
|
||||
bool $importShortCodes
|
||||
): bool {
|
||||
if ($this->shortCodeHelper->ensureShortCodeUniqueness($shortUrl, $importShortCodes)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$longUrl = $url->longUrl();
|
||||
$action = $io->choice(sprintf(
|
||||
'Failed to import URL "%s" because its short-code "%s" is already in use. Do you want to generate a new '
|
||||
. 'one or skip it?',
|
||||
$longUrl,
|
||||
$url->shortCode(),
|
||||
), ['Generate new short-code', 'Skip'], 1);
|
||||
|
||||
if ($action === 'Skip') {
|
||||
$io->text(sprintf('%s: <comment>Skipped</comment>', $longUrl));
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->shortCodeHelper->ensureShortCodeUniqueness($shortUrl, false);
|
||||
}
|
||||
}
|
|
@ -9,6 +9,8 @@ use Shlinkio\Shlink\Core\Exception\ValidationException;
|
|||
use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter;
|
||||
|
||||
use function array_key_exists;
|
||||
use function Shlinkio\Shlink\Core\getOptionalBoolFromInputFilter;
|
||||
use function Shlinkio\Shlink\Core\getOptionalIntFromInputFilter;
|
||||
use function Shlinkio\Shlink\Core\parseDateField;
|
||||
|
||||
final class ShortUrlEdit
|
||||
|
@ -21,6 +23,7 @@ final class ShortUrlEdit
|
|||
private ?Chronos $validUntil = null;
|
||||
private bool $maxVisitsPropWasProvided = false;
|
||||
private ?int $maxVisits = null;
|
||||
private ?bool $validateUrl = null;
|
||||
|
||||
// Enforce named constructors
|
||||
private function __construct()
|
||||
|
@ -55,13 +58,8 @@ final class ShortUrlEdit
|
|||
$this->longUrl = $inputFilter->getValue(ShortUrlMetaInputFilter::LONG_URL);
|
||||
$this->validSince = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_SINCE));
|
||||
$this->validUntil = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_UNTIL));
|
||||
$this->maxVisits = $this->getOptionalIntFromInputFilter($inputFilter, ShortUrlMetaInputFilter::MAX_VISITS);
|
||||
}
|
||||
|
||||
private function getOptionalIntFromInputFilter(ShortUrlMetaInputFilter $inputFilter, string $fieldName): ?int
|
||||
{
|
||||
$value = $inputFilter->getValue($fieldName);
|
||||
return $value !== null ? (int) $value : null;
|
||||
$this->maxVisits = getOptionalIntFromInputFilter($inputFilter, ShortUrlMetaInputFilter::MAX_VISITS);
|
||||
$this->validateUrl = getOptionalBoolFromInputFilter($inputFilter, ShortUrlMetaInputFilter::VALIDATE_URL);
|
||||
}
|
||||
|
||||
public function longUrl(): ?string
|
||||
|
@ -103,4 +101,9 @@ final class ShortUrlEdit
|
|||
{
|
||||
return $this->maxVisitsPropWasProvided;
|
||||
}
|
||||
|
||||
public function doValidateUrl(): ?bool
|
||||
{
|
||||
return $this->validateUrl;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ use Cake\Chronos\Chronos;
|
|||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter;
|
||||
|
||||
use function Shlinkio\Shlink\Core\getOptionalBoolFromInputFilter;
|
||||
use function Shlinkio\Shlink\Core\getOptionalIntFromInputFilter;
|
||||
use function Shlinkio\Shlink\Core\parseDateField;
|
||||
|
||||
use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH;
|
||||
|
@ -21,6 +23,8 @@ final class ShortUrlMeta
|
|||
private ?bool $findIfExists = null;
|
||||
private ?string $domain = null;
|
||||
private int $shortCodeLength = 5;
|
||||
private ?bool $validateUrl = null;
|
||||
private ?string $apiKey = null;
|
||||
|
||||
// Enforce named constructors
|
||||
private function __construct()
|
||||
|
@ -55,19 +59,15 @@ final class ShortUrlMeta
|
|||
$this->validSince = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_SINCE));
|
||||
$this->validUntil = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_UNTIL));
|
||||
$this->customSlug = $inputFilter->getValue(ShortUrlMetaInputFilter::CUSTOM_SLUG);
|
||||
$this->maxVisits = $this->getOptionalIntFromInputFilter($inputFilter, ShortUrlMetaInputFilter::MAX_VISITS);
|
||||
$this->maxVisits = getOptionalIntFromInputFilter($inputFilter, ShortUrlMetaInputFilter::MAX_VISITS);
|
||||
$this->findIfExists = $inputFilter->getValue(ShortUrlMetaInputFilter::FIND_IF_EXISTS);
|
||||
$this->validateUrl = getOptionalBoolFromInputFilter($inputFilter, ShortUrlMetaInputFilter::VALIDATE_URL);
|
||||
$this->domain = $inputFilter->getValue(ShortUrlMetaInputFilter::DOMAIN);
|
||||
$this->shortCodeLength = $this->getOptionalIntFromInputFilter(
|
||||
$this->shortCodeLength = getOptionalIntFromInputFilter(
|
||||
$inputFilter,
|
||||
ShortUrlMetaInputFilter::SHORT_CODE_LENGTH,
|
||||
) ?? DEFAULT_SHORT_CODES_LENGTH;
|
||||
}
|
||||
|
||||
private function getOptionalIntFromInputFilter(ShortUrlMetaInputFilter $inputFilter, string $fieldName): ?int
|
||||
{
|
||||
$value = $inputFilter->getValue($fieldName);
|
||||
return $value !== null ? (int) $value : null;
|
||||
$this->apiKey = $inputFilter->getValue(ShortUrlMetaInputFilter::API_KEY);
|
||||
}
|
||||
|
||||
public function getValidSince(): ?Chronos
|
||||
|
@ -129,4 +129,14 @@ final class ShortUrlMeta
|
|||
{
|
||||
return $this->shortCodeLength;
|
||||
}
|
||||
|
||||
public function doValidateUrl(): ?bool
|
||||
{
|
||||
return $this->validateUrl;
|
||||
}
|
||||
|
||||
public function getApiKey(): ?string
|
||||
{
|
||||
return $this->apiKey;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core\Model;
|
|||
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
|
||||
use function explode;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use function key;
|
||||
|
@ -40,15 +41,22 @@ final class ShortUrlsOrdering
|
|||
return;
|
||||
}
|
||||
|
||||
// FIXME Providing the ordering as array is considered deprecated. To be removed in v3.0.0
|
||||
$isArray = is_array($orderBy);
|
||||
if (! $isArray && $orderBy !== null && ! is_string($orderBy)) {
|
||||
if (! $isArray && ! is_string($orderBy)) {
|
||||
throw ValidationException::fromArray([
|
||||
'orderBy' => '"Order by" must be an array, string or null',
|
||||
]);
|
||||
}
|
||||
|
||||
$this->orderField = $isArray ? key($orderBy) : $orderBy;
|
||||
$this->orderDirection = $isArray ? $orderBy[$this->orderField] : self::DEFAULT_ORDER_DIRECTION;
|
||||
if (! $isArray) {
|
||||
$parts = explode('-', $orderBy);
|
||||
$this->orderField = $parts[0];
|
||||
$this->orderDirection = $parts[1] ?? self::DEFAULT_ORDER_DIRECTION;
|
||||
} else {
|
||||
$this->orderField = key($orderBy);
|
||||
$this->orderDirection = $orderBy[$this->orderField];
|
||||
}
|
||||
}
|
||||
|
||||
public function orderField(): ?string
|
||||
|
|
|
@ -5,13 +5,17 @@ declare(strict_types=1);
|
|||
namespace Shlinkio\Shlink\Core\Repository;
|
||||
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering;
|
||||
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
|
||||
|
||||
use function array_column;
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
use function Functional\contains;
|
||||
|
||||
class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryInterface
|
||||
|
@ -186,6 +190,85 @@ DQL;
|
|||
->setParameter('slug', $slug)
|
||||
->setMaxResults(1);
|
||||
|
||||
$this->whereDomainIs($qb, $domain);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function findOneMatching(string $url, array $tags, ShortUrlMeta $meta): ?ShortUrl
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->select('s')
|
||||
->from(ShortUrl::class, 's')
|
||||
->where($qb->expr()->eq('s.longUrl', ':longUrl'))
|
||||
->setParameter('longUrl', $url)
|
||||
->setMaxResults(1)
|
||||
->orderBy('s.id');
|
||||
|
||||
if ($meta->hasCustomSlug()) {
|
||||
$qb->andWhere($qb->expr()->eq('s.shortCode', ':slug'))
|
||||
->setParameter('slug', $meta->getCustomSlug());
|
||||
}
|
||||
if ($meta->hasMaxVisits()) {
|
||||
$qb->andWhere($qb->expr()->eq('s.maxVisits', ':maxVisits'))
|
||||
->setParameter('maxVisits', $meta->getMaxVisits());
|
||||
}
|
||||
if ($meta->hasValidSince()) {
|
||||
$qb->andWhere($qb->expr()->eq('s.validSince', ':validSince'))
|
||||
->setParameter('validSince', $meta->getValidSince());
|
||||
}
|
||||
if ($meta->hasValidUntil()) {
|
||||
$qb->andWhere($qb->expr()->eq('s.validUntil', ':validUntil'))
|
||||
->setParameter('validUntil', $meta->getValidUntil());
|
||||
}
|
||||
|
||||
if ($meta->hasDomain()) {
|
||||
$qb->join('s.domain', 'd')
|
||||
->andWhere($qb->expr()->eq('d.authority', ':domain'))
|
||||
->setParameter('domain', $meta->getDomain());
|
||||
}
|
||||
|
||||
$tagsAmount = count($tags);
|
||||
if ($tagsAmount === 0) {
|
||||
return $qb->getQuery()->getOneOrNullResult();
|
||||
}
|
||||
|
||||
foreach ($tags as $index => $tag) {
|
||||
$alias = 't_' . $index;
|
||||
$qb->join('s.tags', $alias, Join::WITH, $alias . '.name = :tag' . $index)
|
||||
->setParameter('tag' . $index, $tag);
|
||||
}
|
||||
|
||||
// If tags where provided, we need an extra join to see the amount of tags that every short URL has, so that we
|
||||
// can discard those that also have more tags, making sure only those fully matching are included.
|
||||
$qb->join('s.tags', 't')
|
||||
->groupBy('s')
|
||||
->having($qb->expr()->eq('COUNT(t.id)', ':tagsAmount'))
|
||||
->setParameter('tagsAmount', $tagsAmount);
|
||||
|
||||
return $qb->getQuery()->getOneOrNullResult();
|
||||
}
|
||||
|
||||
public function importedUrlExists(ImportedShlinkUrl $url): bool
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->select('COUNT(DISTINCT s.id)')
|
||||
->from(ShortUrl::class, 's')
|
||||
->andWhere($qb->expr()->eq('s.importOriginalShortCode', ':shortCode'))
|
||||
->setParameter('shortCode', $url->shortCode())
|
||||
->andWhere($qb->expr()->eq('s.importSource', ':importSource'))
|
||||
->setParameter('importSource', $url->source())
|
||||
->setMaxResults(1);
|
||||
|
||||
$this->whereDomainIs($qb, $url->domain());
|
||||
|
||||
$result = (int) $qb->getQuery()->getSingleScalarResult();
|
||||
return $result > 0;
|
||||
}
|
||||
|
||||
private function whereDomainIs(QueryBuilder $qb, ?string $domain): void
|
||||
{
|
||||
if ($domain !== null) {
|
||||
$qb->join('s.domain', 'd')
|
||||
->andWhere($qb->expr()->eq('d.authority', ':authority'))
|
||||
|
@ -193,7 +276,5 @@ DQL;
|
|||
} else {
|
||||
$qb->andWhere($qb->expr()->isNull('s.domain'));
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,9 @@ namespace Shlinkio\Shlink\Core\Repository;
|
|||
use Doctrine\Persistence\ObjectRepository;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering;
|
||||
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
|
||||
|
||||
interface ShortUrlRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
|
@ -27,4 +29,8 @@ interface ShortUrlRepositoryInterface extends ObjectRepository
|
|||
public function findOne(string $shortCode, ?string $domain = null): ?ShortUrl;
|
||||
|
||||
public function shortCodeIsInUse(string $slug, ?string $domain): bool;
|
||||
|
||||
public function findOneMatching(string $url, array $tags, ShortUrlMeta $meta): ?ShortUrl;
|
||||
|
||||
public function importedUrlExists(ImportedShlinkUrl $url): bool;
|
||||
}
|
||||
|
|
41
module/Core/src/Service/ShortUrl/ShortCodeHelper.php
Normal file
41
module/Core/src/Service/ShortUrl/ShortCodeHelper.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Service\ShortUrl;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
|
||||
class ShortCodeHelper implements ShortCodeHelperInterface
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
public function ensureShortCodeUniqueness(ShortUrl $shortUrlToBeCreated, bool $hasCustomSlug): bool
|
||||
{
|
||||
$shortCode = $shortUrlToBeCreated->getShortCode();
|
||||
$domain = $shortUrlToBeCreated->getDomain();
|
||||
$domainAuthority = $domain !== null ? $domain->getAuthority() : null;
|
||||
|
||||
/** @var ShortUrlRepository $repo */
|
||||
$repo = $this->em->getRepository(ShortUrl::class);
|
||||
$otherShortUrlsExist = $repo->shortCodeIsInUse($shortCode, $domainAuthority);
|
||||
|
||||
if (! $otherShortUrlsExist) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($hasCustomSlug) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$shortUrlToBeCreated->regenerateShortCode();
|
||||
return $this->ensureShortCodeUniqueness($shortUrlToBeCreated, $hasCustomSlug);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Service\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
|
||||
interface ShortCodeHelperInterface
|
||||
{
|
||||
public function ensureShortCodeUniqueness(ShortUrl $shortUrlToBeCreated, bool $hasCustomSlug): bool;
|
||||
}
|
|
@ -71,7 +71,7 @@ class ShortUrlService implements ShortUrlServiceInterface
|
|||
public function updateMetadataByShortCode(ShortUrlIdentifier $identifier, ShortUrlEdit $shortUrlEdit): ShortUrl
|
||||
{
|
||||
if ($shortUrlEdit->hasLongUrl()) {
|
||||
$this->urlValidator->validateUrl($shortUrlEdit->longUrl());
|
||||
$this->urlValidator->validateUrl($shortUrlEdit->longUrl(), $shortUrlEdit->doValidateUrl());
|
||||
}
|
||||
|
||||
$shortUrl = $this->urlResolver->resolveShortUrl($identifier);
|
||||
|
|
|
@ -5,34 +5,36 @@ declare(strict_types=1);
|
|||
namespace Shlinkio\Shlink\Core\Service;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Shlinkio\Shlink\Core\Domain\Resolver\DomainResolverInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortCodeHelperInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Resolver\ShortUrlRelationResolverInterface;
|
||||
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
|
||||
use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
|
||||
use Throwable;
|
||||
|
||||
use function array_reduce;
|
||||
|
||||
class UrlShortener implements UrlShortenerInterface
|
||||
{
|
||||
use TagManagerTrait;
|
||||
|
||||
private EntityManagerInterface $em;
|
||||
private UrlValidatorInterface $urlValidator;
|
||||
private DomainResolverInterface $domainResolver;
|
||||
private ShortUrlRelationResolverInterface $relationResolver;
|
||||
private ShortCodeHelperInterface $shortCodeHelper;
|
||||
|
||||
public function __construct(
|
||||
UrlValidatorInterface $urlValidator,
|
||||
EntityManagerInterface $em,
|
||||
DomainResolverInterface $domainResolver
|
||||
ShortUrlRelationResolverInterface $relationResolver,
|
||||
ShortCodeHelperInterface $shortCodeHelper
|
||||
) {
|
||||
$this->urlValidator = $urlValidator;
|
||||
$this->em = $em;
|
||||
$this->domainResolver = $domainResolver;
|
||||
$this->relationResolver = $relationResolver;
|
||||
$this->shortCodeHelper = $shortCodeHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -41,7 +43,7 @@ class UrlShortener implements UrlShortenerInterface
|
|||
* @throws InvalidUrlException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function urlToShortCode(string $url, array $tags, ShortUrlMeta $meta): ShortUrl
|
||||
public function shorten(string $url, array $tags, ShortUrlMeta $meta): ShortUrl
|
||||
{
|
||||
// First, check if a short URL exists for all provided params
|
||||
$existingShortUrl = $this->findExistingShortUrlIfExists($url, $tags, $meta);
|
||||
|
@ -49,26 +51,17 @@ class UrlShortener implements UrlShortenerInterface
|
|||
return $existingShortUrl;
|
||||
}
|
||||
|
||||
$this->urlValidator->validateUrl($url);
|
||||
$this->em->beginTransaction();
|
||||
$shortUrl = new ShortUrl($url, $meta, $this->domainResolver);
|
||||
$shortUrl->setTags($this->tagNamesToEntities($this->em, $tags));
|
||||
$this->urlValidator->validateUrl($url, $meta->doValidateUrl());
|
||||
|
||||
return $this->em->transactional(function () use ($url, $tags, $meta) {
|
||||
$shortUrl = new ShortUrl($url, $meta, $this->relationResolver);
|
||||
$shortUrl->setTags($this->tagNamesToEntities($this->em, $tags));
|
||||
|
||||
try {
|
||||
$this->verifyShortCodeUniqueness($meta, $shortUrl);
|
||||
$this->em->persist($shortUrl);
|
||||
$this->em->flush();
|
||||
$this->em->commit();
|
||||
} catch (Throwable $e) {
|
||||
if ($this->em->getConnection()->isTransactionActive()) {
|
||||
$this->em->rollback();
|
||||
$this->em->close();
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $shortUrl;
|
||||
return $shortUrl;
|
||||
});
|
||||
}
|
||||
|
||||
private function findExistingShortUrlIfExists(string $url, array $tags, ShortUrlMeta $meta): ?ShortUrl
|
||||
|
@ -77,42 +70,23 @@ class UrlShortener implements UrlShortenerInterface
|
|||
return null;
|
||||
}
|
||||
|
||||
$criteria = ['longUrl' => $url];
|
||||
if ($meta->hasCustomSlug()) {
|
||||
$criteria['shortCode'] = $meta->getCustomSlug();
|
||||
}
|
||||
/** @var ShortUrl[] $shortUrls */
|
||||
$shortUrls = $this->em->getRepository(ShortUrl::class)->findBy($criteria);
|
||||
if (empty($shortUrls)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Iterate short URLs until one that matches is found, or return null otherwise
|
||||
return array_reduce($shortUrls, function (?ShortUrl $found, ShortUrl $shortUrl) use ($tags, $meta) {
|
||||
if ($found !== null) {
|
||||
return $found;
|
||||
}
|
||||
|
||||
return $shortUrl->matchesCriteria($meta, $tags) ? $shortUrl : null;
|
||||
});
|
||||
/** @var ShortUrlRepositoryInterface $repo */
|
||||
$repo = $this->em->getRepository(ShortUrl::class);
|
||||
return $repo->findOneMatching($url, $tags, $meta);
|
||||
}
|
||||
|
||||
private function verifyShortCodeUniqueness(ShortUrlMeta $meta, ShortUrl $shortUrlToBeCreated): void
|
||||
{
|
||||
$shortCode = $shortUrlToBeCreated->getShortCode();
|
||||
$domain = $meta->getDomain();
|
||||
$couldBeMadeUnique = $this->shortCodeHelper->ensureShortCodeUniqueness(
|
||||
$shortUrlToBeCreated,
|
||||
$meta->hasCustomSlug(),
|
||||
);
|
||||
|
||||
/** @var ShortUrlRepository $repo */
|
||||
$repo = $this->em->getRepository(ShortUrl::class);
|
||||
$otherShortUrlsExist = $repo->shortCodeIsInUse($shortCode, $domain);
|
||||
if (! $couldBeMadeUnique) {
|
||||
$domain = $shortUrlToBeCreated->getDomain();
|
||||
$domainAuthority = $domain !== null ? $domain->getAuthority() : null;
|
||||
|
||||
if ($otherShortUrlsExist && $meta->hasCustomSlug()) {
|
||||
throw NonUniqueSlugException::fromSlug($shortCode, $domain);
|
||||
}
|
||||
|
||||
if ($otherShortUrlsExist) {
|
||||
$shortUrlToBeCreated->regenerateShortCode();
|
||||
$this->verifyShortCodeUniqueness($meta, $shortUrlToBeCreated);
|
||||
throw NonUniqueSlugException::fromSlug($shortUrlToBeCreated->getShortCode(), $domainAuthority);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,5 +16,5 @@ interface UrlShortenerInterface
|
|||
* @throws NonUniqueSlugException
|
||||
* @throws InvalidUrlException
|
||||
*/
|
||||
public function urlToShortCode(string $url, array $tags, ShortUrlMeta $meta): ShortUrl;
|
||||
public function shorten(string $url, array $tags, ShortUrlMeta $meta): ShortUrl;
|
||||
}
|
||||
|
|
|
@ -2,12 +2,13 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Domain\Resolver;
|
||||
namespace Shlinkio\Shlink\Core\ShortUrl\Resolver;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
class PersistenceDomainResolver implements DomainResolverInterface
|
||||
class PersistenceShortUrlRelationResolver implements ShortUrlRelationResolverInterface
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
|
@ -26,4 +27,15 @@ class PersistenceDomainResolver implements DomainResolverInterface
|
|||
$existingDomain = $this->em->getRepository(Domain::class)->findOneBy(['authority' => $domain]);
|
||||
return $existingDomain ?? new Domain($domain);
|
||||
}
|
||||
|
||||
public function resolveApiKey(?string $key): ?ApiKey
|
||||
{
|
||||
if ($key === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var ApiKey|null $existingApiKey */
|
||||
$existingApiKey = $this->em->getRepository(ApiKey::class)->findOneBy(['key' => $key]);
|
||||
return $existingApiKey;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\ShortUrl\Resolver;
|
||||
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
interface ShortUrlRelationResolverInterface
|
||||
{
|
||||
public function resolveDomain(?string $domain): ?Domain;
|
||||
|
||||
public function resolveApiKey(?string $key): ?ApiKey;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\ShortUrl\Resolver;
|
||||
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
class SimpleShortUrlRelationResolver implements ShortUrlRelationResolverInterface
|
||||
{
|
||||
public function resolveDomain(?string $domain): ?Domain
|
||||
{
|
||||
return $domain !== null ? new Domain($domain) : null;
|
||||
}
|
||||
|
||||
public function resolveApiKey(?string $key): ?ApiKey
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -58,6 +58,7 @@ class TagService implements TagServiceInterface
|
|||
/**
|
||||
* Provided a list of tag names, creates all that do not exist yet
|
||||
*
|
||||
* @deprecated
|
||||
* @param string[] $tagNames
|
||||
* @return Collection|Tag[]
|
||||
*/
|
||||
|
|
|
@ -28,6 +28,7 @@ interface TagServiceInterface
|
|||
public function deleteTags(array $tagNames): void;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @param string[] $tagNames
|
||||
* @return Collection|Tag[]
|
||||
*/
|
||||
|
|
61
module/Core/src/Util/DoctrineBatchHelper.php
Normal file
61
module/Core/src/Util/DoctrineBatchHelper.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Util;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Inspired by ocramius/doctrine-batch-utils https://github.com/Ocramius/DoctrineBatchUtils
|
||||
*/
|
||||
class DoctrineBatchHelper implements DoctrineBatchHelperInterface
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function wrapIterable(iterable $resultSet, int $batchSize): iterable
|
||||
{
|
||||
$iteration = 0;
|
||||
|
||||
$this->em->beginTransaction();
|
||||
|
||||
try {
|
||||
foreach ($resultSet as $key => $value) {
|
||||
$iteration++;
|
||||
yield $key => $value;
|
||||
$this->flushAndClearBatch($iteration, $batchSize);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->em->rollback();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->flushAndClearEntityManager();
|
||||
$this->em->commit();
|
||||
}
|
||||
|
||||
private function flushAndClearBatch(int $iteration, int $batchSize): void
|
||||
{
|
||||
if ($iteration % $batchSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->flushAndClearEntityManager();
|
||||
}
|
||||
|
||||
private function flushAndClearEntityManager(): void
|
||||
{
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
}
|
||||
}
|
10
module/Core/src/Util/DoctrineBatchHelperInterface.php
Normal file
10
module/Core/src/Util/DoctrineBatchHelperInterface.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Util;
|
||||
|
||||
interface DoctrineBatchHelperInterface
|
||||
{
|
||||
public function wrapIterable(iterable $resultSet, int $batchSize): iterable;
|
||||
}
|
|
@ -27,10 +27,11 @@ class UrlValidator implements UrlValidatorInterface, RequestMethodInterface
|
|||
/**
|
||||
* @throws InvalidUrlException
|
||||
*/
|
||||
public function validateUrl(string $url): void
|
||||
public function validateUrl(string $url, ?bool $doValidate): void
|
||||
{
|
||||
// If the URL validation is not enabled, skip check
|
||||
if (! $this->options->isUrlValidationEnabled()) {
|
||||
// If the URL validation is not enabled or it was explicitly set to not validate, skip check
|
||||
$doValidate = $doValidate ?? $this->options->isUrlValidationEnabled();
|
||||
if (! $doValidate) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,5 +11,5 @@ interface UrlValidatorInterface
|
|||
/**
|
||||
* @throws InvalidUrlException
|
||||
*/
|
||||
public function validateUrl(string $url): void;
|
||||
public function validateUrl(string $url, ?bool $doValidate): void;
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ class ShortUrlMetaInputFilter extends InputFilter
|
|||
public const DOMAIN = 'domain';
|
||||
public const SHORT_CODE_LENGTH = 'shortCodeLength';
|
||||
public const LONG_URL = 'longUrl';
|
||||
public const VALIDATE_URL = 'validateUrl';
|
||||
public const API_KEY = 'apiKey';
|
||||
|
||||
public function __construct(array $data)
|
||||
{
|
||||
|
@ -64,9 +66,13 @@ class ShortUrlMetaInputFilter extends InputFilter
|
|||
|
||||
$this->add($this->createBooleanInput(self::FIND_IF_EXISTS, false));
|
||||
|
||||
$this->add($this->createInput(self::VALIDATE_URL, false));
|
||||
|
||||
$domain = $this->createInput(self::DOMAIN, false);
|
||||
$domain->getValidatorChain()->attach(new Validation\HostAndPortValidator());
|
||||
$this->add($domain);
|
||||
|
||||
$this->add($this->createInput(self::API_KEY, false));
|
||||
}
|
||||
|
||||
private function createPositiveNumberInput(string $name, int $min = 1): Input
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Domain\Repository;
|
||||
|
||||
use Shlinkio\Shlink\Core\Domain\Repository\DomainRepository;
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
use Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase;
|
||||
|
||||
class DomainRepositoryTest extends DatabaseTestCase
|
||||
{
|
||||
protected const ENTITIES_TO_EMPTY = [Domain::class];
|
||||
|
||||
private DomainRepository $repo;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->repo = $this->getEntityManager()->getRepository(Domain::class);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function findDomainsReturnsExpectedResult(): void
|
||||
{
|
||||
$fooDomain = new Domain('foo.com');
|
||||
$barDomain = new Domain('bar.com');
|
||||
$bazDomain = new Domain('baz.com');
|
||||
|
||||
$this->getEntityManager()->persist($fooDomain);
|
||||
$this->getEntityManager()->persist($barDomain);
|
||||
$this->getEntityManager()->persist($bazDomain);
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
self::assertEquals([$barDomain, $bazDomain, $fooDomain], $this->repo->findDomainsWithout());
|
||||
self::assertEquals([$barDomain, $bazDomain], $this->repo->findDomainsWithout('foo.com'));
|
||||
self::assertEquals([$bazDomain, $fooDomain], $this->repo->findDomainsWithout('bar.com'));
|
||||
self::assertEquals([$barDomain, $fooDomain], $this->repo->findDomainsWithout('baz.com'));
|
||||
}
|
||||
}
|
|
@ -16,12 +16,16 @@ use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
|||
use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
|
||||
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
|
||||
use Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase;
|
||||
|
||||
use function count;
|
||||
|
||||
class ShortUrlRepositoryTest extends DatabaseTestCase
|
||||
{
|
||||
use TagManagerTrait;
|
||||
|
||||
protected const ENTITIES_TO_EMPTY = [
|
||||
Tag::class,
|
||||
Visit::class,
|
||||
|
@ -54,25 +58,25 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
$this->assertSame($regularOne, $this->repo->findOneWithDomainFallback($regularOne->getShortCode()));
|
||||
$this->assertSame($regularOne, $this->repo->findOneWithDomainFallback(
|
||||
self::assertSame($regularOne, $this->repo->findOneWithDomainFallback($regularOne->getShortCode()));
|
||||
self::assertSame($regularOne, $this->repo->findOneWithDomainFallback(
|
||||
$withDomainDuplicatingRegular->getShortCode(),
|
||||
));
|
||||
$this->assertSame($withDomain, $this->repo->findOneWithDomainFallback(
|
||||
self::assertSame($withDomain, $this->repo->findOneWithDomainFallback(
|
||||
$withDomain->getShortCode(),
|
||||
'example.com',
|
||||
));
|
||||
$this->assertSame(
|
||||
self::assertSame(
|
||||
$withDomainDuplicatingRegular,
|
||||
$this->repo->findOneWithDomainFallback($withDomainDuplicatingRegular->getShortCode(), 'doma.in'),
|
||||
);
|
||||
$this->assertSame(
|
||||
self::assertSame(
|
||||
$regularOne,
|
||||
$this->repo->findOneWithDomainFallback($withDomainDuplicatingRegular->getShortCode(), 'other-domain.com'),
|
||||
);
|
||||
$this->assertNull($this->repo->findOneWithDomainFallback('invalid'));
|
||||
$this->assertNull($this->repo->findOneWithDomainFallback($withDomain->getShortCode()));
|
||||
$this->assertNull($this->repo->findOneWithDomainFallback($withDomain->getShortCode(), 'other-domain.com'));
|
||||
self::assertNull($this->repo->findOneWithDomainFallback('invalid'));
|
||||
self::assertNull($this->repo->findOneWithDomainFallback($withDomain->getShortCode()));
|
||||
self::assertNull($this->repo->findOneWithDomainFallback($withDomain->getShortCode(), 'other-domain.com'));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -84,7 +88,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||
}
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
$this->assertEquals($count, $this->repo->countList());
|
||||
self::assertEquals($count, $this->repo->countList());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -113,37 +117,37 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||
$this->getEntityManager()->flush();
|
||||
|
||||
$result = $this->repo->findList(null, null, 'foo', ['bar']);
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertEquals(1, $this->repo->countList('foo', ['bar']));
|
||||
$this->assertSame($foo, $result[0]);
|
||||
self::assertCount(1, $result);
|
||||
self::assertEquals(1, $this->repo->countList('foo', ['bar']));
|
||||
self::assertSame($foo, $result[0]);
|
||||
|
||||
$result = $this->repo->findList();
|
||||
$this->assertCount(3, $result);
|
||||
self::assertCount(3, $result);
|
||||
|
||||
$result = $this->repo->findList(2);
|
||||
$this->assertCount(2, $result);
|
||||
self::assertCount(2, $result);
|
||||
|
||||
$result = $this->repo->findList(2, 1);
|
||||
$this->assertCount(2, $result);
|
||||
self::assertCount(2, $result);
|
||||
|
||||
$this->assertCount(1, $this->repo->findList(2, 2));
|
||||
self::assertCount(1, $this->repo->findList(2, 2));
|
||||
|
||||
$result = $this->repo->findList(null, null, null, [], ShortUrlsOrdering::fromRawData([
|
||||
'orderBy' => ['visits' => 'DESC'],
|
||||
]));
|
||||
$this->assertCount(3, $result);
|
||||
$this->assertSame($bar, $result[0]);
|
||||
self::assertCount(3, $result);
|
||||
self::assertSame($bar, $result[0]);
|
||||
|
||||
$result = $this->repo->findList(null, null, null, [], null, new DateRange(null, Chronos::now()->subDays(2)));
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertEquals(1, $this->repo->countList(null, [], new DateRange(null, Chronos::now()->subDays(2))));
|
||||
$this->assertSame($foo2, $result[0]);
|
||||
self::assertCount(1, $result);
|
||||
self::assertEquals(1, $this->repo->countList(null, [], new DateRange(null, Chronos::now()->subDays(2))));
|
||||
self::assertSame($foo2, $result[0]);
|
||||
|
||||
$this->assertCount(
|
||||
self::assertCount(
|
||||
2,
|
||||
$this->repo->findList(null, null, null, [], null, new DateRange(Chronos::now()->subDays(2))),
|
||||
);
|
||||
$this->assertEquals(2, $this->repo->countList(null, [], new DateRange(Chronos::now()->subDays(2))));
|
||||
self::assertEquals(2, $this->repo->countList(null, [], new DateRange(Chronos::now()->subDays(2))));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -160,11 +164,11 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||
'orderBy' => ['longUrl' => 'ASC'],
|
||||
]));
|
||||
|
||||
$this->assertCount(count($urls), $result);
|
||||
$this->assertEquals('a', $result[0]->getLongUrl());
|
||||
$this->assertEquals('b', $result[1]->getLongUrl());
|
||||
$this->assertEquals('c', $result[2]->getLongUrl());
|
||||
$this->assertEquals('z', $result[3]->getLongUrl());
|
||||
self::assertCount(count($urls), $result);
|
||||
self::assertEquals('a', $result[0]->getLongUrl());
|
||||
self::assertEquals('b', $result[1]->getLongUrl());
|
||||
self::assertEquals('c', $result[2]->getLongUrl());
|
||||
self::assertEquals('z', $result[3]->getLongUrl());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -181,12 +185,12 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
$this->assertTrue($this->repo->shortCodeIsInUse('my-cool-slug'));
|
||||
$this->assertFalse($this->repo->shortCodeIsInUse('my-cool-slug', 'doma.in'));
|
||||
$this->assertFalse($this->repo->shortCodeIsInUse('slug-not-in-use'));
|
||||
$this->assertFalse($this->repo->shortCodeIsInUse('another-slug'));
|
||||
$this->assertFalse($this->repo->shortCodeIsInUse('another-slug', 'example.com'));
|
||||
$this->assertTrue($this->repo->shortCodeIsInUse('another-slug', 'doma.in'));
|
||||
self::assertTrue($this->repo->shortCodeIsInUse('my-cool-slug'));
|
||||
self::assertFalse($this->repo->shortCodeIsInUse('my-cool-slug', 'doma.in'));
|
||||
self::assertFalse($this->repo->shortCodeIsInUse('slug-not-in-use'));
|
||||
self::assertFalse($this->repo->shortCodeIsInUse('another-slug'));
|
||||
self::assertFalse($this->repo->shortCodeIsInUse('another-slug', 'example.com'));
|
||||
self::assertTrue($this->repo->shortCodeIsInUse('another-slug', 'doma.in'));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -203,11 +207,140 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
$this->assertNotNull($this->repo->findOne('my-cool-slug'));
|
||||
$this->assertNull($this->repo->findOne('my-cool-slug', 'doma.in'));
|
||||
$this->assertNull($this->repo->findOne('slug-not-in-use'));
|
||||
$this->assertNull($this->repo->findOne('another-slug'));
|
||||
$this->assertNull($this->repo->findOne('another-slug', 'example.com'));
|
||||
$this->assertNotNull($this->repo->findOne('another-slug', 'doma.in'));
|
||||
self::assertNotNull($this->repo->findOne('my-cool-slug'));
|
||||
self::assertNull($this->repo->findOne('my-cool-slug', 'doma.in'));
|
||||
self::assertNull($this->repo->findOne('slug-not-in-use'));
|
||||
self::assertNull($this->repo->findOne('another-slug'));
|
||||
self::assertNull($this->repo->findOne('another-slug', 'example.com'));
|
||||
self::assertNotNull($this->repo->findOne('another-slug', 'doma.in'));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function findOneMatchingReturnsNullForNonExistingShortUrls(): void
|
||||
{
|
||||
self::assertNull($this->repo->findOneMatching('', [], ShortUrlMeta::createEmpty()));
|
||||
self::assertNull($this->repo->findOneMatching('foobar', [], ShortUrlMeta::createEmpty()));
|
||||
self::assertNull($this->repo->findOneMatching('foobar', ['foo', 'bar'], ShortUrlMeta::createEmpty()));
|
||||
self::assertNull($this->repo->findOneMatching('foobar', ['foo', 'bar'], ShortUrlMeta::fromRawData([
|
||||
'validSince' => Chronos::parse('2020-03-05 20:18:30'),
|
||||
'customSlug' => 'this_slug_does_not_exist',
|
||||
])));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function findOneMatchingAppliesProperConditions(): void
|
||||
{
|
||||
$start = Chronos::parse('2020-03-05 20:18:30');
|
||||
$end = Chronos::parse('2021-03-05 20:18:30');
|
||||
|
||||
$shortUrl = new ShortUrl('foo', ShortUrlMeta::fromRawData(['validSince' => $start]));
|
||||
$shortUrl->setTags($this->tagNamesToEntities($this->getEntityManager(), ['foo', 'bar']));
|
||||
$this->getEntityManager()->persist($shortUrl);
|
||||
|
||||
$shortUrl2 = new ShortUrl('bar', ShortUrlMeta::fromRawData(['validUntil' => $end]));
|
||||
$this->getEntityManager()->persist($shortUrl2);
|
||||
|
||||
$shortUrl3 = new ShortUrl('baz', ShortUrlMeta::fromRawData(['validSince' => $start, 'validUntil' => $end]));
|
||||
$this->getEntityManager()->persist($shortUrl3);
|
||||
|
||||
$shortUrl4 = new ShortUrl('foo', ShortUrlMeta::fromRawData(['customSlug' => 'custom', 'validUntil' => $end]));
|
||||
$this->getEntityManager()->persist($shortUrl4);
|
||||
|
||||
$shortUrl5 = new ShortUrl('foo', ShortUrlMeta::fromRawData(['maxVisits' => 3]));
|
||||
$this->getEntityManager()->persist($shortUrl5);
|
||||
|
||||
$shortUrl6 = new ShortUrl('foo', ShortUrlMeta::fromRawData(['domain' => 'doma.in']));
|
||||
$this->getEntityManager()->persist($shortUrl6);
|
||||
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
self::assertSame(
|
||||
$shortUrl,
|
||||
$this->repo->findOneMatching('foo', ['foo', 'bar'], ShortUrlMeta::fromRawData(['validSince' => $start])),
|
||||
);
|
||||
self::assertSame(
|
||||
$shortUrl2,
|
||||
$this->repo->findOneMatching('bar', [], ShortUrlMeta::fromRawData(['validUntil' => $end])),
|
||||
);
|
||||
self::assertSame(
|
||||
$shortUrl3,
|
||||
$this->repo->findOneMatching('baz', [], ShortUrlMeta::fromRawData([
|
||||
'validSince' => $start,
|
||||
'validUntil' => $end,
|
||||
])),
|
||||
);
|
||||
self::assertSame(
|
||||
$shortUrl4,
|
||||
$this->repo->findOneMatching('foo', [], ShortUrlMeta::fromRawData([
|
||||
'customSlug' => 'custom',
|
||||
'validUntil' => $end,
|
||||
])),
|
||||
);
|
||||
self::assertSame(
|
||||
$shortUrl5,
|
||||
$this->repo->findOneMatching('foo', [], ShortUrlMeta::fromRawData(['maxVisits' => 3])),
|
||||
);
|
||||
self::assertSame(
|
||||
$shortUrl6,
|
||||
$this->repo->findOneMatching('foo', [], ShortUrlMeta::fromRawData(['domain' => 'doma.in'])),
|
||||
);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function findOneMatchingReturnsOldestOneWhenThereAreMultipleMatches(): void
|
||||
{
|
||||
$start = Chronos::parse('2020-03-05 20:18:30');
|
||||
$meta = ['validSince' => $start, 'maxVisits' => 50];
|
||||
$tags = ['foo', 'bar'];
|
||||
$tagEntities = $this->tagNamesToEntities($this->getEntityManager(), $tags);
|
||||
|
||||
$shortUrl1 = new ShortUrl('foo', ShortUrlMeta::fromRawData($meta));
|
||||
$shortUrl1->setTags($tagEntities);
|
||||
$this->getEntityManager()->persist($shortUrl1);
|
||||
|
||||
$shortUrl2 = new ShortUrl('foo', ShortUrlMeta::fromRawData($meta));
|
||||
$shortUrl2->setTags($tagEntities);
|
||||
$this->getEntityManager()->persist($shortUrl2);
|
||||
|
||||
$shortUrl3 = new ShortUrl('foo', ShortUrlMeta::fromRawData($meta));
|
||||
$shortUrl3->setTags($tagEntities);
|
||||
$this->getEntityManager()->persist($shortUrl3);
|
||||
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
self::assertSame(
|
||||
$shortUrl1,
|
||||
$this->repo->findOneMatching('foo', $tags, ShortUrlMeta::fromRawData($meta)),
|
||||
);
|
||||
self::assertNotSame(
|
||||
$shortUrl2,
|
||||
$this->repo->findOneMatching('foo', $tags, ShortUrlMeta::fromRawData($meta)),
|
||||
);
|
||||
self::assertNotSame(
|
||||
$shortUrl3,
|
||||
$this->repo->findOneMatching('foo', $tags, ShortUrlMeta::fromRawData($meta)),
|
||||
);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function importedShortUrlsAreSearchedAsExpected(): void
|
||||
{
|
||||
$buildImported = static fn (string $shortCode, ?String $domain = null) =>
|
||||
new ImportedShlinkUrl('', 'foo', [], Chronos::now(), $domain, $shortCode);
|
||||
|
||||
$shortUrlWithoutDomain = ShortUrl::fromImport($buildImported('my-cool-slug'), true);
|
||||
$this->getEntityManager()->persist($shortUrlWithoutDomain);
|
||||
|
||||
$shortUrlWithDomain = ShortUrl::fromImport($buildImported('another-slug', 'doma.in'), true);
|
||||
$this->getEntityManager()->persist($shortUrlWithDomain);
|
||||
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
self::assertTrue($this->repo->importedUrlExists($buildImported('my-cool-slug')));
|
||||
self::assertTrue($this->repo->importedUrlExists($buildImported('another-slug', 'doma.in')));
|
||||
self::assertFalse($this->repo->importedUrlExists($buildImported('non-existing-slug')));
|
||||
self::assertFalse($this->repo->importedUrlExists($buildImported('non-existing-slug', 'doma.in')));
|
||||
self::assertFalse($this->repo->importedUrlExists($buildImported('my-cool-slug', 'doma.in')));
|
||||
self::assertFalse($this->repo->importedUrlExists($buildImported('another-slug')));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ class TagRepositoryTest extends DatabaseTestCase
|
|||
/** @test */
|
||||
public function deleteByNameDoesNothingWhenEmptyListIsProvided(): void
|
||||
{
|
||||
$this->assertEquals(0, $this->repo->deleteByName([]));
|
||||
self::assertEquals(0, $this->repo->deleteByName([]));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -46,7 +46,7 @@ class TagRepositoryTest extends DatabaseTestCase
|
|||
}
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
$this->assertEquals(2, $this->repo->deleteByName($toDelete));
|
||||
self::assertEquals(2, $this->repo->deleteByName($toDelete));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -79,20 +79,20 @@ class TagRepositoryTest extends DatabaseTestCase
|
|||
|
||||
$result = $this->repo->findTagsWithInfo();
|
||||
|
||||
$this->assertCount(4, $result);
|
||||
$this->assertEquals(
|
||||
self::assertCount(4, $result);
|
||||
self::assertEquals(
|
||||
['tag' => $tags[3], 'shortUrlsCount' => 0, 'visitsCount' => 0],
|
||||
$result[0]->jsonSerialize(),
|
||||
);
|
||||
$this->assertEquals(
|
||||
self::assertEquals(
|
||||
['tag' => $tags[1], 'shortUrlsCount' => 1, 'visitsCount' => 3],
|
||||
$result[1]->jsonSerialize(),
|
||||
);
|
||||
$this->assertEquals(
|
||||
self::assertEquals(
|
||||
['tag' => $tags[2], 'shortUrlsCount' => 1, 'visitsCount' => 3],
|
||||
$result[2]->jsonSerialize(),
|
||||
);
|
||||
$this->assertEquals(
|
||||
self::assertEquals(
|
||||
['tag' => $tags[0], 'shortUrlsCount' => 2, 'visitsCount' => 4],
|
||||
$result[3]->jsonSerialize(),
|
||||
);
|
||||
|
|
|
@ -75,9 +75,9 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||
|
||||
// Important! assertCount will not work here, as this iterable object loads data dynamically and the count
|
||||
// is 0 if not iterated
|
||||
$this->assertEquals(2, $countIterable($unlocated));
|
||||
$this->assertEquals(4, $countIterable($withEmptyLocation));
|
||||
$this->assertEquals(6, $countIterable($all));
|
||||
self::assertEquals(2, $countIterable($unlocated));
|
||||
self::assertEquals(4, $countIterable($withEmptyLocation));
|
||||
self::assertEquals(6, $countIterable($all));
|
||||
}
|
||||
|
||||
public function provideBlockSize(): iterable
|
||||
|
@ -90,22 +90,22 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||
{
|
||||
[$shortCode, $domain] = $this->createShortUrlsAndVisits();
|
||||
|
||||
$this->assertCount(0, $this->repo->findVisitsByShortCode('invalid'));
|
||||
$this->assertCount(6, $this->repo->findVisitsByShortCode($shortCode));
|
||||
$this->assertCount(3, $this->repo->findVisitsByShortCode($shortCode, $domain));
|
||||
$this->assertCount(2, $this->repo->findVisitsByShortCode($shortCode, null, new DateRange(
|
||||
self::assertCount(0, $this->repo->findVisitsByShortCode('invalid'));
|
||||
self::assertCount(6, $this->repo->findVisitsByShortCode($shortCode));
|
||||
self::assertCount(3, $this->repo->findVisitsByShortCode($shortCode, $domain));
|
||||
self::assertCount(2, $this->repo->findVisitsByShortCode($shortCode, null, new DateRange(
|
||||
Chronos::parse('2016-01-02'),
|
||||
Chronos::parse('2016-01-03'),
|
||||
)));
|
||||
$this->assertCount(4, $this->repo->findVisitsByShortCode($shortCode, null, new DateRange(
|
||||
self::assertCount(4, $this->repo->findVisitsByShortCode($shortCode, null, new DateRange(
|
||||
Chronos::parse('2016-01-03'),
|
||||
)));
|
||||
$this->assertCount(1, $this->repo->findVisitsByShortCode($shortCode, $domain, new DateRange(
|
||||
self::assertCount(1, $this->repo->findVisitsByShortCode($shortCode, $domain, new DateRange(
|
||||
Chronos::parse('2016-01-03'),
|
||||
)));
|
||||
$this->assertCount(3, $this->repo->findVisitsByShortCode($shortCode, null, null, 3, 2));
|
||||
$this->assertCount(2, $this->repo->findVisitsByShortCode($shortCode, null, null, 5, 4));
|
||||
$this->assertCount(1, $this->repo->findVisitsByShortCode($shortCode, $domain, null, 3, 2));
|
||||
self::assertCount(3, $this->repo->findVisitsByShortCode($shortCode, null, null, 3, 2));
|
||||
self::assertCount(2, $this->repo->findVisitsByShortCode($shortCode, null, null, 5, 4));
|
||||
self::assertCount(1, $this->repo->findVisitsByShortCode($shortCode, $domain, null, 3, 2));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -113,17 +113,17 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||
{
|
||||
[$shortCode, $domain] = $this->createShortUrlsAndVisits();
|
||||
|
||||
$this->assertEquals(0, $this->repo->countVisitsByShortCode('invalid'));
|
||||
$this->assertEquals(6, $this->repo->countVisitsByShortCode($shortCode));
|
||||
$this->assertEquals(3, $this->repo->countVisitsByShortCode($shortCode, $domain));
|
||||
$this->assertEquals(2, $this->repo->countVisitsByShortCode($shortCode, null, new DateRange(
|
||||
self::assertEquals(0, $this->repo->countVisitsByShortCode('invalid'));
|
||||
self::assertEquals(6, $this->repo->countVisitsByShortCode($shortCode));
|
||||
self::assertEquals(3, $this->repo->countVisitsByShortCode($shortCode, $domain));
|
||||
self::assertEquals(2, $this->repo->countVisitsByShortCode($shortCode, null, new DateRange(
|
||||
Chronos::parse('2016-01-02'),
|
||||
Chronos::parse('2016-01-03'),
|
||||
)));
|
||||
$this->assertEquals(4, $this->repo->countVisitsByShortCode($shortCode, null, new DateRange(
|
||||
self::assertEquals(4, $this->repo->countVisitsByShortCode($shortCode, null, new DateRange(
|
||||
Chronos::parse('2016-01-03'),
|
||||
)));
|
||||
$this->assertEquals(1, $this->repo->countVisitsByShortCode($shortCode, $domain, new DateRange(
|
||||
self::assertEquals(1, $this->repo->countVisitsByShortCode($shortCode, $domain, new DateRange(
|
||||
Chronos::parse('2016-01-03'),
|
||||
)));
|
||||
}
|
||||
|
@ -147,13 +147,13 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
$this->assertCount(0, $this->repo->findVisitsByTag('invalid'));
|
||||
$this->assertCount(18, $this->repo->findVisitsByTag((string) $foo));
|
||||
$this->assertCount(6, $this->repo->findVisitsByTag((string) $foo, new DateRange(
|
||||
self::assertCount(0, $this->repo->findVisitsByTag('invalid'));
|
||||
self::assertCount(18, $this->repo->findVisitsByTag((string) $foo));
|
||||
self::assertCount(6, $this->repo->findVisitsByTag((string) $foo, new DateRange(
|
||||
Chronos::parse('2016-01-02'),
|
||||
Chronos::parse('2016-01-03'),
|
||||
)));
|
||||
$this->assertCount(12, $this->repo->findVisitsByTag((string) $foo, new DateRange(
|
||||
self::assertCount(12, $this->repo->findVisitsByTag((string) $foo, new DateRange(
|
||||
Chronos::parse('2016-01-03'),
|
||||
)));
|
||||
}
|
||||
|
@ -174,13 +174,13 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
$this->assertEquals(0, $this->repo->countVisitsByTag('invalid'));
|
||||
$this->assertEquals(12, $this->repo->countVisitsByTag((string) $foo));
|
||||
$this->assertEquals(4, $this->repo->countVisitsByTag((string) $foo, new DateRange(
|
||||
self::assertEquals(0, $this->repo->countVisitsByTag('invalid'));
|
||||
self::assertEquals(12, $this->repo->countVisitsByTag((string) $foo));
|
||||
self::assertEquals(4, $this->repo->countVisitsByTag((string) $foo, new DateRange(
|
||||
Chronos::parse('2016-01-02'),
|
||||
Chronos::parse('2016-01-03'),
|
||||
)));
|
||||
$this->assertEquals(8, $this->repo->countVisitsByTag((string) $foo, new DateRange(
|
||||
self::assertEquals(8, $this->repo->countVisitsByTag((string) $foo, new DateRange(
|
||||
Chronos::parse('2016-01-03'),
|
||||
)));
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ namespace ShlinkioTest\Shlink\Core\Action;
|
|||
use Laminas\Diactoros\ServerRequest;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Common\Response\PixelResponse;
|
||||
|
@ -19,6 +20,8 @@ use Shlinkio\Shlink\Core\Service\VisitsTracker;
|
|||
|
||||
class PixelActionTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private PixelAction $action;
|
||||
private ObjectProphecy $urlResolver;
|
||||
private ObjectProphecy $visitTracker;
|
||||
|
@ -47,8 +50,8 @@ class PixelActionTest extends TestCase
|
|||
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode);
|
||||
$response = $this->action->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal());
|
||||
|
||||
$this->assertInstanceOf(PixelResponse::class, $response);
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals('image/gif', $response->getHeaderLine('content-type'));
|
||||
self::assertInstanceOf(PixelResponse::class, $response);
|
||||
self::assertEquals(200, $response->getStatusCode());
|
||||
self::assertEquals('image/gif', $response->getHeaderLine('content-type'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ use Laminas\Diactoros\ServerRequest;
|
|||
use Mezzio\Router\RouterInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Common\Response\QrCodeResponse;
|
||||
|
@ -20,6 +21,8 @@ use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
|||
|
||||
class QrCodeActionTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private QrCodeAction $action;
|
||||
private ObjectProphecy $urlResolver;
|
||||
|
||||
|
@ -77,8 +80,34 @@ class QrCodeActionTest extends TestCase
|
|||
$delegate->reveal(),
|
||||
);
|
||||
|
||||
$this->assertInstanceOf(QrCodeResponse::class, $resp);
|
||||
$this->assertEquals(200, $resp->getStatusCode());
|
||||
self::assertInstanceOf(QrCodeResponse::class, $resp);
|
||||
self::assertEquals(200, $resp->getStatusCode());
|
||||
$delegate->handle(Argument::any())->shouldHaveBeenCalledTimes(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideQueries
|
||||
*/
|
||||
public function imageIsReturnedWithExpectedContentTypeBasedOnProvidedFormat(
|
||||
array $query,
|
||||
string $expectedContentType
|
||||
): void {
|
||||
$code = 'abc123';
|
||||
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($code, ''))->willReturn(new ShortUrl(''));
|
||||
$delegate = $this->prophesize(RequestHandlerInterface::class);
|
||||
$req = (new ServerRequest())->withAttribute('shortCode', $code)->withQueryParams($query);
|
||||
|
||||
$resp = $this->action->process($req, $delegate->reveal());
|
||||
|
||||
self::assertEquals($expectedContentType, $resp->getHeaderLine('Content-Type'));
|
||||
}
|
||||
|
||||
public function provideQueries(): iterable
|
||||
{
|
||||
yield 'no format' => [[], 'image/png'];
|
||||
yield 'png format' => [['format' => 'png'], 'image/png'];
|
||||
yield 'svg format' => [['format' => 'svg'], 'image/svg+xml'];
|
||||
yield 'unsupported format' => [['format' => 'jpg'], 'image/png'];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ use Laminas\Diactoros\ServerRequest;
|
|||
use Mezzio\Router\Middleware\ImplicitHeadMiddleware;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
||||
|
@ -24,6 +25,8 @@ use function array_key_exists;
|
|||
|
||||
class RedirectActionTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private RedirectAction $action;
|
||||
private ObjectProphecy $urlResolver;
|
||||
private ObjectProphecy $visitTracker;
|
||||
|
@ -60,10 +63,10 @@ class RedirectActionTest extends TestCase
|
|||
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode)->withQueryParams($query);
|
||||
$response = $this->action->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal());
|
||||
|
||||
$this->assertInstanceOf(Response\RedirectResponse::class, $response);
|
||||
$this->assertEquals(302, $response->getStatusCode());
|
||||
$this->assertTrue($response->hasHeader('Location'));
|
||||
$this->assertEquals($expectedUrl, $response->getHeaderLine('Location'));
|
||||
self::assertInstanceOf(Response\RedirectResponse::class, $response);
|
||||
self::assertEquals(302, $response->getStatusCode());
|
||||
self::assertTrue($response->hasHeader('Location'));
|
||||
self::assertEquals($expectedUrl, $response->getHeaderLine('Location'));
|
||||
$shortCodeToUrl->shouldHaveBeenCalledOnce();
|
||||
$track->shouldHaveBeenCalledTimes(array_key_exists('foobar', $query) ? 0 : 1);
|
||||
}
|
||||
|
@ -135,10 +138,10 @@ class RedirectActionTest extends TestCase
|
|||
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode);
|
||||
$response = $this->action->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal());
|
||||
|
||||
$this->assertInstanceOf(Response\RedirectResponse::class, $response);
|
||||
$this->assertEquals($expectedStatus, $response->getStatusCode());
|
||||
$this->assertEquals($response->hasHeader('Cache-Control'), $expectedCacheControl !== null);
|
||||
$this->assertEquals($response->getHeaderLine('Cache-Control'), $expectedCacheControl ?? '');
|
||||
self::assertInstanceOf(Response\RedirectResponse::class, $response);
|
||||
self::assertEquals($expectedStatus, $response->getStatusCode());
|
||||
self::assertEquals($response->hasHeader('Cache-Control'), $expectedCacheControl !== null);
|
||||
self::assertEquals($response->getHeaderLine('Cache-Control'), $expectedCacheControl ?? '');
|
||||
}
|
||||
|
||||
public function provideRedirectConfigs(): iterable
|
||||
|
|
|
@ -32,9 +32,9 @@ class BasePathPrefixerTest extends TestCase
|
|||
'url_shortener' => $urlShortener,
|
||||
] = ($this->prefixer)($originalConfig);
|
||||
|
||||
$this->assertEquals($expectedRoutes, $routes);
|
||||
$this->assertEquals($expectedMiddlewares, $middlewares);
|
||||
$this->assertEquals([
|
||||
self::assertEquals($expectedRoutes, $routes);
|
||||
self::assertEquals($expectedMiddlewares, $middlewares);
|
||||
self::assertEquals([
|
||||
'domain' => [
|
||||
'hostname' => $expectedHostname,
|
||||
],
|
||||
|
|
|
@ -29,7 +29,7 @@ class DeprecatedConfigParserTest extends TestCase
|
|||
|
||||
$result = ($this->postProcessor)($config);
|
||||
|
||||
$this->assertEquals($config, $result);
|
||||
self::assertEquals($config, $result);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -46,7 +46,7 @@ class DeprecatedConfigParserTest extends TestCase
|
|||
|
||||
$result = ($this->postProcessor)($config);
|
||||
|
||||
$this->assertEquals($config, $result);
|
||||
self::assertEquals($config, $result);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -68,7 +68,7 @@ class DeprecatedConfigParserTest extends TestCase
|
|||
|
||||
$result = ($this->postProcessor)($config);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
self::assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -89,7 +89,7 @@ class DeprecatedConfigParserTest extends TestCase
|
|||
|
||||
$result = ($this->postProcessor)($config);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
self::assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -106,6 +106,6 @@ class DeprecatedConfigParserTest extends TestCase
|
|||
|
||||
$result = ($this->postProcessor)($config);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
self::assertEquals($expected, $result);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,6 +67,7 @@ class SimplifiedConfigParserTest extends TestCase
|
|||
'anonymize_remote_addr' => false,
|
||||
'redirect_status_code' => 301,
|
||||
'redirect_cache_lifetime' => 90,
|
||||
'port' => 8888,
|
||||
];
|
||||
$expected = [
|
||||
'app_options' => [
|
||||
|
@ -132,6 +133,7 @@ class SimplifiedConfigParserTest extends TestCase
|
|||
|
||||
'mezzio-swoole' => [
|
||||
'swoole-http-server' => [
|
||||
'port' => 8888,
|
||||
'options' => [
|
||||
'task_worker_num' => 50,
|
||||
],
|
||||
|
@ -151,6 +153,6 @@ class SimplifiedConfigParserTest extends TestCase
|
|||
|
||||
$result = ($this->postProcessor)(array_merge($config, $simplified));
|
||||
|
||||
$this->assertEquals(array_merge($expected, $simplified), $result);
|
||||
self::assertEquals(array_merge($expected, $simplified), $result);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,9 +21,9 @@ class ConfigProviderTest extends TestCase
|
|||
{
|
||||
$config = $this->configProvider->__invoke();
|
||||
|
||||
$this->assertArrayHasKey('routes', $config);
|
||||
$this->assertArrayHasKey('dependencies', $config);
|
||||
$this->assertArrayHasKey('templates', $config);
|
||||
$this->assertArrayHasKey('mezzio', $config);
|
||||
self::assertArrayHasKey('routes', $config);
|
||||
self::assertArrayHasKey('dependencies', $config);
|
||||
self::assertArrayHasKey('templates', $config);
|
||||
self::assertArrayHasKey('mezzio', $config);
|
||||
}
|
||||
}
|
||||
|
|
52
module/Core/test/Domain/DomainServiceTest.php
Normal file
52
module/Core/test/Domain/DomainServiceTest.php
Normal file
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Domain;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Core\Domain\DomainService;
|
||||
use Shlinkio\Shlink\Core\Domain\Repository\DomainRepositoryInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
|
||||
class DomainServiceTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private DomainService $domainService;
|
||||
private ObjectProphecy $em;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->em = $this->prophesize(EntityManagerInterface::class);
|
||||
$this->domainService = new DomainService($this->em->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideExcludedDomains
|
||||
*/
|
||||
public function listDomainsWithoutDelegatesIntoRepository(?string $excludedDomain, array $expectedResult): void
|
||||
{
|
||||
$repo = $this->prophesize(DomainRepositoryInterface::class);
|
||||
$getRepo = $this->em->getRepository(Domain::class)->willReturn($repo->reveal());
|
||||
$findDomains = $repo->findDomainsWithout($excludedDomain)->willReturn($expectedResult);
|
||||
|
||||
$result = $this->domainService->listDomainsWithout($excludedDomain);
|
||||
|
||||
self::assertEquals($expectedResult, $result);
|
||||
$getRepo->shouldHaveBeenCalledOnce();
|
||||
$findDomains->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideExcludedDomains(): iterable
|
||||
{
|
||||
yield 'no excluded domain' => [null, []];
|
||||
yield 'foo.com excluded domain' => ['foo.com', []];
|
||||
yield 'bar.com excluded domain' => ['bar.com', [new Domain('bar.com')]];
|
||||
yield 'baz.com excluded domain' => ['baz.com', [new Domain('foo.com'), new Domain('bar.com')]];
|
||||
}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Domain\Resolver;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Core\Domain\Resolver\PersistenceDomainResolver;
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
|
||||
class PersistenceDomainResolverTest extends TestCase
|
||||
{
|
||||
private PersistenceDomainResolver $domainResolver;
|
||||
private ObjectProphecy $em;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->em = $this->prophesize(EntityManagerInterface::class);
|
||||
$this->domainResolver = new PersistenceDomainResolver($this->em->reveal());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function returnsEmptyWhenNoDomainIsProvided(): void
|
||||
{
|
||||
$getRepository = $this->em->getRepository(Domain::class);
|
||||
|
||||
$this->assertNull($this->domainResolver->resolveDomain(null));
|
||||
$getRepository->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideFoundDomains
|
||||
*/
|
||||
public function findsOrCreatesDomainWhenValueIsProvided(?Domain $foundDomain, string $authority): void
|
||||
{
|
||||
$repo = $this->prophesize(ObjectRepository::class);
|
||||
$findDomain = $repo->findOneBy(['authority' => $authority])->willReturn($foundDomain);
|
||||
$getRepository = $this->em->getRepository(Domain::class)->willReturn($repo->reveal());
|
||||
|
||||
$result = $this->domainResolver->resolveDomain($authority);
|
||||
|
||||
if ($foundDomain !== null) {
|
||||
$this->assertSame($result, $foundDomain);
|
||||
}
|
||||
$this->assertInstanceOf(Domain::class, $result);
|
||||
$this->assertEquals($authority, $result->getAuthority());
|
||||
$findDomain->shouldHaveBeenCalledOnce();
|
||||
$getRepository->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideFoundDomains(): iterable
|
||||
{
|
||||
$authority = 'doma.in';
|
||||
|
||||
yield 'without found domain' => [null, $authority];
|
||||
yield 'with found domain' => [new Domain($authority), $authority];
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue