mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-07 07:43:12 +08:00
Compare commits
106 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bb40c74c1 | ||
|
|
4515a83e9b | ||
|
|
907a282b73 | ||
|
|
5154638ddf | ||
|
|
52c9994eb4 | ||
|
|
912f287a27 | ||
|
|
e99ab66afd | ||
|
|
fb022eae68 | ||
|
|
259c52a698 | ||
|
|
deeca582db | ||
|
|
4dbcf6857e | ||
|
|
5190a03113 | ||
|
|
d60c3a4aa9 | ||
|
|
ce1c70fd7c | ||
|
|
29bb201581 | ||
|
|
006ec7c1d0 | ||
|
|
1bc9e0643d | ||
|
|
d6395a3de8 | ||
|
|
098751d256 | ||
|
|
8577d6bd99 | ||
|
|
fe4e171ecb | ||
|
|
d99ea82761 | ||
|
|
27bc8d4823 | ||
|
|
7c9f572eb1 | ||
|
|
2732b05834 | ||
|
|
97f89bcede | ||
|
|
00255b04eb | ||
|
|
f90ea4bd98 | ||
|
|
0d7fb1163a | ||
|
|
cb340b5867 | ||
|
|
1621f3a943 | ||
|
|
ae636aef5a | ||
|
|
1346d7902e | ||
|
|
544836b986 | ||
|
|
397f7d09e3 | ||
|
|
efa707c676 | ||
|
|
51c8b80489 | ||
|
|
e71fb0ac7f | ||
|
|
681b7c836d | ||
|
|
7c2c90fc49 | ||
|
|
ebe6a5f4aa | ||
|
|
65651e4bbd | ||
|
|
33190c07c7 | ||
|
|
f651b0e5a1 | ||
|
|
c85eb84b4c | ||
|
|
86d428184e | ||
|
|
c1529b7d6c | ||
|
|
7ecc3aacc4 | ||
|
|
b091bd4e2a | ||
|
|
90b4bc9b1a | ||
|
|
de7096010e | ||
|
|
03a9697298 | ||
|
|
fdcf88de67 | ||
|
|
7c343f42c1 | ||
|
|
786e4f642b | ||
|
|
b1a073b1ab | ||
|
|
2256f6a9e7 | ||
|
|
ec3e7212b2 | ||
|
|
554d9b092f | ||
|
|
33d3837795 | ||
|
|
0686ac2fb1 | ||
|
|
ce3d267572 | ||
|
|
4ec90e02c9 | ||
|
|
e7bccb088d | ||
|
|
cbc9f1257d | ||
|
|
c7f15b77fd | ||
|
|
a8b0c46142 | ||
|
|
065d314608 | ||
|
|
d426dbc684 | ||
|
|
c6c78f383f | ||
|
|
450eea64aa | ||
|
|
c8d7413dd4 | ||
|
|
00a96e6215 | ||
|
|
b15e90408f | ||
|
|
34c10c0bc9 | ||
|
|
63a24342e3 | ||
|
|
073e4eeac8 | ||
|
|
06eda073bf | ||
|
|
614e1c37f8 | ||
|
|
24aab5cc0e | ||
|
|
76d6d9a7a9 | ||
|
|
8109ceb7eb | ||
|
|
6163e34327 | ||
|
|
84b291e310 | ||
|
|
20cd5cd752 | ||
|
|
d9d57743e6 | ||
|
|
cc57dcd01a | ||
|
|
10fbf8f8ff | ||
|
|
cfc9a1b772 | ||
|
|
2555424124 | ||
|
|
405369824b | ||
|
|
cdd87f5962 | ||
|
|
d5eac3b1c3 | ||
|
|
1f78f5266a | ||
|
|
aa0124f4e9 | ||
|
|
641f35ae05 | ||
|
|
4e94f07050 | ||
|
|
460ca032d2 | ||
|
|
8d438aa6aa | ||
|
|
504d08101a | ||
|
|
4b7184ac85 | ||
|
|
55d9f2a4a1 | ||
|
|
319b790628 | ||
|
|
ee563978ac | ||
|
|
be71a6eeb4 | ||
|
|
25fbbee883 |
14
.github/workflows/docker-image-build.yml
vendored
14
.github/workflows/docker-image-build.yml
vendored
@@ -13,12 +13,16 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Install buildx
|
- name: Set up QEMU
|
||||||
id: buildx
|
uses: docker/setup-qemu-action@v1
|
||||||
uses: crazy-max/ghaction-docker-buildx@v1
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
with:
|
with:
|
||||||
buildx-version: latest
|
version: latest
|
||||||
- name: Login to docker hub
|
- name: Login to docker hub
|
||||||
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Build the image
|
- name: Build the image
|
||||||
run: bash ./docker/build
|
run: bash ./docker/build
|
||||||
|
|||||||
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'
|
php: '7.4'
|
||||||
env:
|
env:
|
||||||
- COMPOSER_FLAGS=''
|
- 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:
|
before_install:
|
||||||
- echo 'extension = apcu.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
|
- echo 'extension = apcu.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
|
||||||
- phpenv config-rm xdebug.ini || return 0
|
- phpenv config-rm xdebug.ini || return 0
|
||||||
- sudo ./data/infra/ci/install-ms-odbc.sh
|
- 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
|
- 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:
|
install:
|
||||||
- composer self-update
|
- composer self-update
|
||||||
@@ -56,12 +44,13 @@ before_script:
|
|||||||
- export DOCKERFILE_CHANGED=$(git diff ${TRAVIS_COMMIT_RANGE:-origin/main} --name-only | grep Dockerfile)
|
- export DOCKERFILE_CHANGED=$(git diff ${TRAVIS_COMMIT_RANGE:-origin/main} --name-only | grep Dockerfile)
|
||||||
|
|
||||||
script:
|
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
|
- if [[ ! -z "${DOCKERFILE_CHANGED}" && "${TRAVIS_PHP_VERSION}" == "7.4" ]]; then docker build -t shlink-docker-image:temp . ; fi
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- rm -f build/clover.xml
|
- rm -f build/clover.xml
|
||||||
- wget https://phar.phpunit.de/phpcov-7.0.2.phar
|
- 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
|
- wget https://scrutinizer-ci.com/ocular.phar
|
||||||
- php ocular.phar code-coverage:upload --format=php-clover build/clover.xml
|
- php ocular.phar code-coverage:upload --format=php-clover build/clover.xml
|
||||||
|
|||||||
1157
CHANGELOG.md
1157
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.4.0
|
||||||
ENV SHLINK_VERSION ${SHLINK_VERSION}
|
ENV SHLINK_VERSION ${SHLINK_VERSION}
|
||||||
ENV SWOOLE_VERSION 4.5.2
|
ENV SWOOLE_VERSION 4.5.5
|
||||||
ENV LC_ALL "C"
|
ENV LC_ALL "C"
|
||||||
|
|
||||||
WORKDIR /etc/shlink
|
WORKDIR /etc/shlink
|
||||||
|
|
||||||
|
# Install required PHP extensions
|
||||||
RUN \
|
RUN \
|
||||||
# Install mysql and calendar
|
# Install mysql and calendar
|
||||||
docker-php-ext-install -j"$(nproc)" pdo_mysql calendar && \
|
docker-php-ext-install -j"$(nproc)" pdo_mysql calendar && \
|
||||||
@@ -21,13 +22,16 @@ RUN \
|
|||||||
docker-php-ext-install -j"$(nproc)" intl && \
|
docker-php-ext-install -j"$(nproc)" intl && \
|
||||||
# Install zip and gd
|
# Install zip and gd
|
||||||
apk add --no-cache libzip-dev zlib-dev libpng-dev && \
|
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
|
# Install sqlsrv driver
|
||||||
RUN if [ $(uname -m) == "x86_64" ]; then \
|
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 && \
|
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 --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 && \
|
pecl install pdo_sqlsrv && \
|
||||||
docker-php-ext-enable pdo_sqlsrv && \
|
docker-php-ext-enable pdo_sqlsrv && \
|
||||||
apk del .phpize-deps && \
|
apk del .phpize-deps && \
|
||||||
@@ -35,7 +39,7 @@ RUN if [ $(uname -m) == "x86_64" ]; then \
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Install swoole
|
# 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} && \
|
pecl install swoole-${SWOOLE_VERSION} && \
|
||||||
docker-php-ext-enable swoole && \
|
docker-php-ext-enable swoole && \
|
||||||
apk del .phpize-deps
|
apk del .phpize-deps
|
||||||
@@ -44,7 +48,7 @@ RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS && \
|
|||||||
# Install shlink
|
# Install shlink
|
||||||
FROM base as builder
|
FROM base as builder
|
||||||
COPY . .
|
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 && \
|
RUN apk add --no-cache git && \
|
||||||
php composer.phar install --no-dev --optimize-autoloader --prefer-dist --no-progress --no-interaction && \
|
php composer.phar install --no-dev --optimize-autoloader --prefer-dist --no-progress --no-interaction && \
|
||||||
php composer.phar clear-cache && \
|
php composer.phar clear-cache && \
|
||||||
@@ -59,7 +63,7 @@ LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
|||||||
COPY --from=builder /etc/shlink .
|
COPY --from=builder /etc/shlink .
|
||||||
RUN ln -s /etc/shlink/bin/cli /usr/local/bin/shlink
|
RUN ln -s /etc/shlink/bin/cli /usr/local/bin/shlink
|
||||||
|
|
||||||
# Expose swoole port
|
# Expose default swoole port
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
# Expose params config dir, since the user is expected to provide custom config from there
|
# Expose params config dir, since the user is expected to provide custom config from there
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||

|

|
||||||
|
|
||||||
[](https://travis-ci.org/shlinkio/shlink)
|
[](https://travis-ci.com/shlinkio/shlink)
|
||||||
[](https://scrutinizer-ci.com/g/shlinkio/shlink/)
|
[](https://scrutinizer-ci.com/g/shlinkio/shlink/)
|
||||||
[](https://scrutinizer-ci.com/g/shlinkio/shlink/)
|
[](https://scrutinizer-ci.com/g/shlinkio/shlink/)
|
||||||
[](https://packagist.org/packages/shlinkio/shlink)
|
[](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.
|
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
|
### Configure
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
export APP_ENV=test
|
export APP_ENV=test
|
||||||
export DB_DRIVER=mysql
|
export DB_DRIVER=mysql
|
||||||
|
export TEST_ENV=api
|
||||||
|
|
||||||
# Try to stop server just in case it hanged in last execution
|
# Try to stop server just in case it hanged in last execution
|
||||||
vendor/bin/mezzio-swoole stop
|
vendor/bin/mezzio-swoole stop
|
||||||
@@ -9,7 +10,7 @@ echo 'Starting server...'
|
|||||||
vendor/bin/mezzio-swoole start -d
|
vendor/bin/mezzio-swoole start -d
|
||||||
sleep 2
|
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=$?
|
testsExitCode=$?
|
||||||
|
|
||||||
vendor/bin/mezzio-swoole stop
|
vendor/bin/mezzio-swoole stop
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
"akrabat/ip-address-middleware": "^1.0",
|
"akrabat/ip-address-middleware": "^1.0",
|
||||||
"cakephp/chronos": "^1.2",
|
"cakephp/chronos": "^2.0",
|
||||||
"cocur/slugify": "^4.0",
|
"cocur/slugify": "^4.0",
|
||||||
"doctrine/cache": "^1.9",
|
"doctrine/cache": "^1.9",
|
||||||
"doctrine/dbal": "^2.10",
|
"doctrine/dbal": "^2.10",
|
||||||
@@ -27,7 +27,6 @@
|
|||||||
"guzzlehttp/guzzle": "^7.0",
|
"guzzlehttp/guzzle": "^7.0",
|
||||||
"laminas/laminas-config": "^3.3",
|
"laminas/laminas-config": "^3.3",
|
||||||
"laminas/laminas-config-aggregator": "^1.1",
|
"laminas/laminas-config-aggregator": "^1.1",
|
||||||
"laminas/laminas-dependency-plugin": "^1.0",
|
|
||||||
"laminas/laminas-diactoros": "^2.1.3",
|
"laminas/laminas-diactoros": "^2.1.3",
|
||||||
"laminas/laminas-inputfilter": "^2.10",
|
"laminas/laminas-inputfilter": "^2.10",
|
||||||
"laminas/laminas-paginator": "^2.8",
|
"laminas/laminas-paginator": "^2.8",
|
||||||
@@ -50,9 +49,10 @@
|
|||||||
"predis/predis": "^1.1",
|
"predis/predis": "^1.1",
|
||||||
"pugx/shortid-php": "^0.5",
|
"pugx/shortid-php": "^0.5",
|
||||||
"ramsey/uuid": "^3.9",
|
"ramsey/uuid": "^3.9",
|
||||||
"shlinkio/shlink-common": "^3.2.0",
|
"shlinkio/shlink-common": "^3.3.0",
|
||||||
"shlinkio/shlink-config": "^1.0",
|
"shlinkio/shlink-config": "^1.0",
|
||||||
"shlinkio/shlink-event-dispatcher": "^1.4",
|
"shlinkio/shlink-event-dispatcher": "^1.4",
|
||||||
|
"shlinkio/shlink-importer": "^2.0.1",
|
||||||
"shlinkio/shlink-installer": "^5.1.0",
|
"shlinkio/shlink-installer": "^5.1.0",
|
||||||
"shlinkio/shlink-ip-geolocation": "^1.5",
|
"shlinkio/shlink-ip-geolocation": "^1.5",
|
||||||
"symfony/console": "^5.1",
|
"symfony/console": "^5.1",
|
||||||
@@ -66,13 +66,15 @@
|
|||||||
"devster/ubench": "^2.0",
|
"devster/ubench": "^2.0",
|
||||||
"dms/phpunit-arraysubset-asserts": "^0.2.0",
|
"dms/phpunit-arraysubset-asserts": "^0.2.0",
|
||||||
"eaglewu/swoole-ide-helper": "dev-master",
|
"eaglewu/swoole-ide-helper": "dev-master",
|
||||||
"infection/infection": "^0.16.1",
|
"infection/infection": "^0.20.0",
|
||||||
"phpstan/phpstan": "^0.12.18",
|
"phpspec/prophecy-phpunit": "^2.0",
|
||||||
"phpunit/phpunit": "~9.0.1",
|
"phpstan/phpstan": "^0.12.52",
|
||||||
|
"phpunit/php-code-coverage": "^9.2",
|
||||||
|
"phpunit/phpunit": "^9.4",
|
||||||
"roave/security-advisories": "dev-master",
|
"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",
|
"shlinkio/shlink-test-utils": "^1.5",
|
||||||
"symfony/var-dumper": "^5.0"
|
"symfony/var-dumper": "^5.1"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
@@ -93,7 +95,10 @@
|
|||||||
"module/Core/test",
|
"module/Core/test",
|
||||||
"module/Core/test-db"
|
"module/Core/test-db"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"files": [
|
||||||
|
"config/test/constants.php"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ci": [
|
"ci": [
|
||||||
@@ -104,7 +109,7 @@
|
|||||||
],
|
],
|
||||||
"cs": "phpcs",
|
"cs": "phpcs",
|
||||||
"cs:fix": "phpcbf",
|
"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": [
|
||||||
"@test:unit",
|
"@test:unit",
|
||||||
"@test:db",
|
"@test:db",
|
||||||
@@ -114,8 +119,8 @@
|
|||||||
"@test:unit:ci",
|
"@test:unit:ci",
|
||||||
"@test:db"
|
"@test:db"
|
||||||
],
|
],
|
||||||
"test:unit": "phpdbg -qrr vendor/bin/phpunit --order-by=random --colors=always --coverage-php build/coverage-unit.cov --testdox",
|
"test:unit": "@php 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:ci": "@test:unit --coverage-xml=build/coverage-unit/coverage-xml --log-junit=build/coverage-unit/junit.xml",
|
||||||
"test:db": [
|
"test:db": [
|
||||||
"@test:db:sqlite:ci",
|
"@test:db:sqlite:ci",
|
||||||
"@test:db:mysql",
|
"@test:db:mysql",
|
||||||
@@ -123,14 +128,14 @@
|
|||||||
"@test:db:postgres",
|
"@test:db:postgres",
|
||||||
"@test:db:ms"
|
"@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: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:mysql": "DB_DRIVER=mysql composer test:db:sqlite",
|
||||||
"test:db:maria": "DB_DRIVER=maria 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:postgres": "DB_DRIVER=postgres composer test:db:sqlite",
|
||||||
"test:db:ms": "DB_DRIVER=mssql composer test:db:sqlite",
|
"test:db:ms": "DB_DRIVER=mssql composer test:db:sqlite",
|
||||||
"test:api": "bin/test/run-api-tests.sh",
|
"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": "infection --threads=4 --min-msi=80 --log-verbosity=default --only-covered",
|
||||||
"infect:ci:base": "@infect --skip-initial-tests",
|
"infect:ci:base": "@infect --skip-initial-tests",
|
||||||
"infect:ci": [
|
"infect:ci": [
|
||||||
@@ -166,6 +171,7 @@
|
|||||||
"clean:dev": "<fg=blue;options=bold>Deletes artifacts which are gitignored and could affect dev env</>"
|
"clean:dev": "<fg=blue;options=bold>Deletes artifacts which are gitignored and could affect dev env</>"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"sort-packages": true
|
"sort-packages": true,
|
||||||
|
"platform-check": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
use Mezzio\Container;
|
use Mezzio\Container;
|
||||||
|
use Psr\Http\Client\ClientInterface;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
@@ -13,6 +15,10 @@ return [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'aliases' => [
|
||||||
|
ClientInterface::class => Client::class,
|
||||||
|
],
|
||||||
|
|
||||||
'lazy_services' => [
|
'lazy_services' => [
|
||||||
'proxies_target_dir' => 'data/proxies',
|
'proxies_target_dir' => 'data/proxies',
|
||||||
'proxies_namespace' => 'ShlinkProxy',
|
'proxies_namespace' => 'ShlinkProxy',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
return [
|
return [
|
||||||
|
|
||||||
'not_found_redirects' => [
|
'not_found_redirects' => [
|
||||||
'invalid_short_url' => null, // Formerly url_shortener.not_found_short_url.redirect_to
|
'invalid_short_url' => null,
|
||||||
'regular_404' => null,
|
'regular_404' => null,
|
||||||
'base_url' => null,
|
'base_url' => null,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink;
|
namespace Shlinkio\Shlink;
|
||||||
|
|
||||||
use Laminas\ConfigAggregator;
|
use Laminas\ConfigAggregator;
|
||||||
|
use Laminas\Diactoros;
|
||||||
use Mezzio;
|
use Mezzio;
|
||||||
use Mezzio\ProblemDetails;
|
use Mezzio\ProblemDetails;
|
||||||
|
|
||||||
@@ -17,8 +18,10 @@ return (new ConfigAggregator\ConfigAggregator([
|
|||||||
Mezzio\Plates\ConfigProvider::class,
|
Mezzio\Plates\ConfigProvider::class,
|
||||||
Mezzio\Swoole\ConfigProvider::class,
|
Mezzio\Swoole\ConfigProvider::class,
|
||||||
ProblemDetails\ConfigProvider::class,
|
ProblemDetails\ConfigProvider::class,
|
||||||
|
Diactoros\ConfigProvider::class,
|
||||||
Common\ConfigProvider::class,
|
Common\ConfigProvider::class,
|
||||||
Config\ConfigProvider::class,
|
Config\ConfigProvider::class,
|
||||||
|
Importer\ConfigProvider::class,
|
||||||
IpGeolocation\ConfigProvider::class,
|
IpGeolocation\ConfigProvider::class,
|
||||||
EventDispatcher\ConfigProvider::class,
|
EventDispatcher\ConfigProvider::class,
|
||||||
Core\ConfigProvider::class,
|
Core\ConfigProvider::class,
|
||||||
|
|||||||
@@ -7,12 +7,28 @@ namespace Shlinkio\Shlink\TestUtils;
|
|||||||
use Doctrine\ORM\EntityManager;
|
use Doctrine\ORM\EntityManager;
|
||||||
use Psr\Container\ContainerInterface;
|
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 */
|
/** @var ContainerInterface $container */
|
||||||
$container = require __DIR__ . '/../container.php';
|
$container = require __DIR__ . '/../container.php';
|
||||||
$testHelper = $container->get(Helper\TestHelper::class);
|
$testHelper = $container->get(Helper\TestHelper::class);
|
||||||
$config = $container->get('config');
|
$config = $container->get('config');
|
||||||
$em = $container->get(EntityManager::class);
|
$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();
|
$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'] ?? []));
|
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 GuzzleHttp\Client;
|
||||||
use Laminas\ConfigAggregator\ConfigAggregator;
|
use Laminas\ConfigAggregator\ConfigAggregator;
|
||||||
|
use Laminas\Diactoros\Response\EmptyResponse;
|
||||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||||
|
use Laminas\Stdlib\Glob;
|
||||||
use PDO;
|
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 Shlinkio\Shlink\Common\env;
|
||||||
use function sprintf;
|
use function sprintf;
|
||||||
use function sys_get_temp_dir;
|
use function sys_get_temp_dir;
|
||||||
|
|
||||||
$swooleTestingHost = '127.0.0.1';
|
use const ShlinkioTest\Shlink\SWOOLE_TESTING_HOST;
|
||||||
$swooleTestingPort = 9999;
|
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 {
|
$buildDbConnection = function (): array {
|
||||||
$driver = env('DB_DRIVER', 'sqlite');
|
$driver = env('DB_DRIVER', 'sqlite');
|
||||||
@@ -78,8 +96,8 @@ return [
|
|||||||
'mezzio-swoole' => [
|
'mezzio-swoole' => [
|
||||||
'enable_coroutine' => false,
|
'enable_coroutine' => false,
|
||||||
'swoole-http-server' => [
|
'swoole-http-server' => [
|
||||||
'host' => $swooleTestingHost,
|
'host' => SWOOLE_TESTING_HOST,
|
||||||
'port' => $swooleTestingPort,
|
'port' => SWOOLE_TESTING_PORT,
|
||||||
'process-name' => 'shlink_test',
|
'process-name' => 'shlink_test',
|
||||||
'options' => [
|
'options' => [
|
||||||
'pid_file' => sys_get_temp_dir() . '/shlink-test-swoole.pid',
|
'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' => [
|
'mercure' => [
|
||||||
'public_hub_url' => null,
|
'public_hub_url' => null,
|
||||||
'internal_hub_url' => null,
|
'internal_hub_url' => null,
|
||||||
@@ -97,7 +144,7 @@ return [
|
|||||||
'dependencies' => [
|
'dependencies' => [
|
||||||
'services' => [
|
'services' => [
|
||||||
'shlink_test_api_client' => new Client([
|
'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,
|
'http_errors' => false,
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
FROM php:7.4.9-alpine3.12
|
FROM php:7.4.11-fpm-alpine3.12
|
||||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||||
|
|
||||||
ENV APCU_VERSION 5.1.18
|
ENV APCU_VERSION 5.1.18
|
||||||
ENV APCU_BC_VERSION 1.0.5
|
ENV APCU_BC_VERSION 1.0.5
|
||||||
ENV XDEBUG_VERSION 2.9.0
|
|
||||||
|
|
||||||
RUN apk update
|
RUN apk update
|
||||||
|
|
||||||
@@ -31,6 +30,9 @@ RUN docker-php-ext-install gd
|
|||||||
RUN apk add --no-cache postgresql-dev
|
RUN apk add --no-cache postgresql-dev
|
||||||
RUN docker-php-ext-install pdo_pgsql
|
RUN docker-php-ext-install pdo_pgsql
|
||||||
|
|
||||||
|
RUN apk add --no-cache gmp-dev
|
||||||
|
RUN docker-php-ext-install gmp
|
||||||
|
|
||||||
# Install APCu extension
|
# Install APCu extension
|
||||||
ADD https://pecl.php.net/get/apcu-$APCU_VERSION.tgz /tmp/apcu.tar.gz
|
ADD https://pecl.php.net/get/apcu-$APCU_VERSION.tgz /tmp/apcu.tar.gz
|
||||||
RUN mkdir -p /usr/src/php/ext/apcu\
|
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 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
|
RUN echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini
|
||||||
|
|
||||||
# Install xdebug
|
# Install pcov and sqlsrv driver
|
||||||
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
|
|
||||||
RUN wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_17.5.1.1-1_amd64.apk && \
|
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 --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 && \
|
pecl install pdo_sqlsrv pcov && \
|
||||||
docker-php-ext-enable pdo_sqlsrv && \
|
docker-php-ext-enable pdo_sqlsrv pcov && \
|
||||||
apk del .phpize-deps && \
|
apk del .phpize-deps && \
|
||||||
rm msodbcsql17_17.5.1.1-1_amd64.apk
|
rm msodbcsql17_17.5.1.1-1_amd64.apk
|
||||||
|
|
||||||
# Install composer
|
# Install composer
|
||||||
RUN php -r "readfile('https://getcomposer.org/installer');" | php
|
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
|
||||||
RUN chmod +x composer.phar
|
|
||||||
RUN mv composer.phar /usr/local/bin/composer
|
|
||||||
|
|
||||||
# Make home directory writable by anyone
|
# Make home directory writable by anyone
|
||||||
RUN chmod 777 /home
|
RUN chmod 777 /home
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ memory_limit=-1
|
|||||||
log_errors_max_len=0
|
log_errors_max_len=0
|
||||||
zend.assertions=1
|
zend.assertions=1
|
||||||
assert.exception=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>
|
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||||
|
|
||||||
ENV APCU_VERSION 5.1.18
|
ENV APCU_VERSION 5.1.18
|
||||||
ENV APCU_BC_VERSION 1.0.5
|
ENV APCU_BC_VERSION 1.0.5
|
||||||
ENV INOTIFY_VERSION 2.0.0
|
ENV INOTIFY_VERSION 2.0.0
|
||||||
ENV SWOOLE_VERSION 4.5.2
|
ENV SWOOLE_VERSION 4.5.5
|
||||||
|
|
||||||
RUN apk update
|
RUN apk update
|
||||||
|
|
||||||
@@ -32,6 +32,9 @@ RUN docker-php-ext-install gd
|
|||||||
RUN apk add --no-cache postgresql-dev
|
RUN apk add --no-cache postgresql-dev
|
||||||
RUN docker-php-ext-install pdo_pgsql
|
RUN docker-php-ext-install pdo_pgsql
|
||||||
|
|
||||||
|
RUN apk add --no-cache gmp-dev
|
||||||
|
RUN docker-php-ext-install gmp
|
||||||
|
|
||||||
# Install APCu extension
|
# Install APCu extension
|
||||||
ADD https://pecl.php.net/get/apcu-$APCU_VERSION.tgz /tmp/apcu.tar.gz
|
ADD https://pecl.php.net/get/apcu-$APCU_VERSION.tgz /tmp/apcu.tar.gz
|
||||||
RUN mkdir -p /usr/src/php/ext/apcu\
|
RUN mkdir -p /usr/src/php/ext/apcu\
|
||||||
@@ -66,19 +69,17 @@ RUN docker-php-ext-configure inotify\
|
|||||||
# cleanup
|
# cleanup
|
||||||
RUN rm /tmp/inotify.tar.gz
|
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 && \
|
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 --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 swoole-${SWOOLE_VERSION} pdo_sqlsrv && \
|
pecl install swoole-${SWOOLE_VERSION} pdo_sqlsrv pcov && \
|
||||||
docker-php-ext-enable swoole pdo_sqlsrv && \
|
docker-php-ext-enable swoole pdo_sqlsrv pcov && \
|
||||||
apk del .phpize-deps && \
|
apk del .phpize-deps && \
|
||||||
rm msodbcsql17_17.5.1.1-1_amd64.apk
|
rm msodbcsql17_17.5.1.1-1_amd64.apk
|
||||||
|
|
||||||
# Install composer
|
# Install composer
|
||||||
RUN php -r "readfile('https://getcomposer.org/installer');" | php
|
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
|
||||||
RUN chmod +x composer.phar
|
|
||||||
RUN mv composer.phar /usr/local/bin/composer
|
|
||||||
|
|
||||||
# Make home directory writable by anyone
|
# Make home directory writable by anyone
|
||||||
RUN chmod 777 /home
|
RUN chmod 777 /home
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ class Version20171021093246 extends AbstractMigration
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$shortUrls->addColumn('valid_since', Types::DATETIME, [
|
$shortUrls->addColumn('valid_since', Types::DATETIME_MUTABLE, [
|
||||||
'notnull' => false,
|
'notnull' => false,
|
||||||
]);
|
]);
|
||||||
$shortUrls->addColumn('valid_until', Types::DATETIME, [
|
$shortUrls->addColumn('valid_until', Types::DATETIME_MUTABLE, [
|
||||||
'notnull' => false,
|
'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');
|
||||||
|
}
|
||||||
|
}
|
||||||
89
data/migrations/Version20201102113208.php
Normal file
89
data/migrations/Version20201102113208.php
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<?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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string|int|null
|
||||||
|
*/
|
||||||
|
private function resolveOneApiKeyId(Result $result)
|
||||||
|
{
|
||||||
|
$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.
|
* `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_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.
|
* `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:
|
An example using all env vars could look like this:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run \
|
docker run \
|
||||||
--name shlink \
|
--name shlink \
|
||||||
-p 8080:8080 \
|
-p 8080:8888 \
|
||||||
-e SHORT_DOMAIN_HOST=doma.in \
|
-e SHORT_DOMAIN_HOST=doma.in \
|
||||||
-e SHORT_DOMAIN_SCHEMA=https \
|
-e SHORT_DOMAIN_SCHEMA=https \
|
||||||
|
-e PORT=8888 \
|
||||||
-e DB_DRIVER=mysql \
|
-e DB_DRIVER=mysql \
|
||||||
-e DB_NAME=shlink \
|
-e DB_NAME=shlink \
|
||||||
-e DB_USER=root \
|
-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",
|
"mercure_jwt_secret": "super_secret_key",
|
||||||
"anonymize_remote_addr": false,
|
"anonymize_remote_addr": false,
|
||||||
"redirect_status_code": 301,
|
"redirect_status_code": 301,
|
||||||
"redirect_cache_lifetime": 90
|
"redirect_cache_lifetime": 90,
|
||||||
|
"port": 8888
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ return [
|
|||||||
|
|
||||||
'mezzio-swoole' => [
|
'mezzio-swoole' => [
|
||||||
'swoole-http-server' => [
|
'swoole-http-server' => [
|
||||||
|
'port' => (int) env('PORT', 8080),
|
||||||
'options' => [
|
'options' => [
|
||||||
'worker_num' => (int) env('WEB_WORKER_NUM', 16),
|
'worker_num' => (int) env('WEB_WORKER_NUM', 16),
|
||||||
'task_worker_num' => (int) env('TASK_WORKER_NUM', 16),
|
'task_worker_num' => (int) env('TASK_WORKER_NUM', 16),
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
{
|
{
|
||||||
"name": "tags[]",
|
"name": "tags[]",
|
||||||
"in": "query",
|
"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,
|
"required": false,
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@@ -48,10 +48,14 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"longUrl",
|
"longUrl-ASC",
|
||||||
"shortCode",
|
"longUrl-DESC",
|
||||||
"dateCreated",
|
"shortCode-ASC",
|
||||||
"visits"
|
"shortCode-DESC",
|
||||||
|
"dateCreated-ASC",
|
||||||
|
"dateCreated-DESC",
|
||||||
|
"visits-ASC",
|
||||||
|
"visits-DESC"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -247,6 +251,10 @@
|
|||||||
"shortCodeLength": {
|
"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",
|
"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"
|
"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": {
|
"maxVisits": {
|
||||||
"description": "The maximum number of allowed visits for this short code",
|
"description": "The maximum number of allowed visits for this short code",
|
||||||
"type": "number"
|
"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": {
|
"post": {
|
||||||
|
"deprecated": true,
|
||||||
"operationId": "createTags",
|
"operationId": "createTags",
|
||||||
"tags": [
|
"tags": [
|
||||||
"Tags"
|
"Tags"
|
||||||
],
|
],
|
||||||
"summary": "Create 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": [
|
"security": [
|
||||||
{
|
{
|
||||||
"ApiKey": []
|
"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,
|
"maximum": 1000,
|
||||||
"default": 300
|
"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": {
|
"responses": {
|
||||||
@@ -38,6 +51,12 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "binary"
|
"format": "binary"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"image/svg+xml": {
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "binary"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,10 @@
|
|||||||
"$ref": "paths/v2_tags_{tag}_visits.json"
|
"$ref": "paths/v2_tags_{tag}_visits.json"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"/rest/v{version}/domains": {
|
||||||
|
"$ref": "paths/v2_domains.json"
|
||||||
|
},
|
||||||
|
|
||||||
"/rest/v{version}/mercure-info": {
|
"/rest/v{version}/mercure-info": {
|
||||||
"$ref": "paths/v2_mercure-info.json"
|
"$ref": "paths/v2_mercure-info.json"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ return [
|
|||||||
Command\Tag\RenameTagCommand::NAME => Command\Tag\RenameTagCommand::class,
|
Command\Tag\RenameTagCommand::NAME => Command\Tag\RenameTagCommand::class,
|
||||||
Command\Tag\DeleteTagsCommand::NAME => Command\Tag\DeleteTagsCommand::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\CreateDatabaseCommand::NAME => Command\Db\CreateDatabaseCommand::class,
|
||||||
Command\Db\MigrateDatabaseCommand::NAME => Command\Db\MigrateDatabaseCommand::class,
|
Command\Db\MigrateDatabaseCommand::NAME => Command\Db\MigrateDatabaseCommand::class,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
|||||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
||||||
use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory;
|
use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory;
|
||||||
|
use Shlinkio\Shlink\Core\Domain\DomainService;
|
||||||
use Shlinkio\Shlink\Core\Service;
|
use Shlinkio\Shlink\Core\Service;
|
||||||
use Shlinkio\Shlink\Core\Tag\TagService;
|
use Shlinkio\Shlink\Core\Tag\TagService;
|
||||||
use Shlinkio\Shlink\Core\Visit;
|
use Shlinkio\Shlink\Core\Visit;
|
||||||
@@ -52,6 +53,8 @@ return [
|
|||||||
|
|
||||||
Command\Db\CreateDatabaseCommand::class => ConfigAbstractFactory::class,
|
Command\Db\CreateDatabaseCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\Db\MigrateDatabaseCommand::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\RenameTagCommand::class => [TagService::class],
|
||||||
Command\Tag\DeleteTagsCommand::class => [TagService::class],
|
Command\Tag\DeleteTagsCommand::class => [TagService::class],
|
||||||
|
|
||||||
|
Command\Domain\ListDomainsCommand::class => [DomainService::class, 'config.url_shortener.domain.hostname'],
|
||||||
|
|
||||||
Command\Db\CreateDatabaseCommand::class => [
|
Command\Db\CreateDatabaseCommand::class => [
|
||||||
LockFactory::class,
|
LockFactory::class,
|
||||||
SymfonyCli\Helper\ProcessHelper::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\curry;
|
||||||
use function Functional\flatten;
|
use function Functional\flatten;
|
||||||
use function Functional\unique;
|
use function Functional\unique;
|
||||||
|
use function method_exists;
|
||||||
use function sprintf;
|
use function sprintf;
|
||||||
|
use function strpos;
|
||||||
|
|
||||||
class GenerateShortUrlCommand extends Command
|
class GenerateShortUrlCommand extends Command
|
||||||
{
|
{
|
||||||
@@ -94,6 +96,18 @@ class GenerateShortUrlCommand extends Command
|
|||||||
'l',
|
'l',
|
||||||
InputOption::VALUE_REQUIRED,
|
InputOption::VALUE_REQUIRED,
|
||||||
'The length for generated short code (it will be ignored if --customSlug was provided).',
|
'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');
|
$customSlug = $input->getOption('customSlug');
|
||||||
$maxVisits = $input->getOption('maxVisits');
|
$maxVisits = $input->getOption('maxVisits');
|
||||||
$shortCodeLength = $input->getOption('shortCodeLength') ?? $this->defaultShortCodeLength;
|
$shortCodeLength = $input->getOption('shortCodeLength') ?? $this->defaultShortCodeLength;
|
||||||
|
$doValidateUrl = $this->doValidateUrl($input);
|
||||||
|
|
||||||
try {
|
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_SINCE => $input->getOption('validSince'),
|
||||||
ShortUrlMetaInputFilter::VALID_UNTIL => $input->getOption('validUntil'),
|
ShortUrlMetaInputFilter::VALID_UNTIL => $input->getOption('validUntil'),
|
||||||
ShortUrlMetaInputFilter::CUSTOM_SLUG => $customSlug,
|
ShortUrlMetaInputFilter::CUSTOM_SLUG => $customSlug,
|
||||||
@@ -135,6 +150,7 @@ class GenerateShortUrlCommand extends Command
|
|||||||
ShortUrlMetaInputFilter::FIND_IF_EXISTS => $input->getOption('findIfExists'),
|
ShortUrlMetaInputFilter::FIND_IF_EXISTS => $input->getOption('findIfExists'),
|
||||||
ShortUrlMetaInputFilter::DOMAIN => $input->getOption('domain'),
|
ShortUrlMetaInputFilter::DOMAIN => $input->getOption('domain'),
|
||||||
ShortUrlMetaInputFilter::SHORT_CODE_LENGTH => $shortCodeLength,
|
ShortUrlMetaInputFilter::SHORT_CODE_LENGTH => $shortCodeLength,
|
||||||
|
ShortUrlMetaInputFilter::VALIDATE_URL => $doValidateUrl,
|
||||||
]));
|
]));
|
||||||
|
|
||||||
$io->writeln([
|
$io->writeln([
|
||||||
@@ -147,4 +163,18 @@ class GenerateShortUrlCommand extends Command
|
|||||||
return ExitCodes::EXIT_FAILURE;
|
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\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
/** @deprecated */
|
||||||
class CreateTagCommand extends Command
|
class CreateTagCommand extends Command
|
||||||
{
|
{
|
||||||
public const NAME = 'tag:create';
|
public const NAME = 'tag:create';
|
||||||
@@ -28,7 +29,7 @@ class CreateTagCommand extends Command
|
|||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setName(self::NAME)
|
->setName(self::NAME)
|
||||||
->setDescription('Creates one or more tags.')
|
->setDescription('[Deprecated] Creates one or more tags.')
|
||||||
->addOption(
|
->addOption(
|
||||||
'name',
|
'name',
|
||||||
't',
|
't',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Api\DisableKeyCommand;
|
use Shlinkio\Shlink\CLI\Command\Api\DisableKeyCommand;
|
||||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||||
@@ -14,6 +15,8 @@ use Symfony\Component\Console\Tester\CommandTester;
|
|||||||
|
|
||||||
class DisableKeyCommandTest extends TestCase
|
class DisableKeyCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private CommandTester $commandTester;
|
private CommandTester $commandTester;
|
||||||
private ObjectProphecy $apiKeyService;
|
private ObjectProphecy $apiKeyService;
|
||||||
|
|
||||||
@@ -37,7 +40,7 @@ class DisableKeyCommandTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertStringContainsString('API key "abcd1234" properly disabled', $output);
|
self::assertStringContainsString('API key "abcd1234" properly disabled', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
@@ -52,7 +55,7 @@ class DisableKeyCommandTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertStringContainsString($expectedMessage, $output);
|
self::assertStringContainsString($expectedMessage, $output);
|
||||||
$disable->shouldHaveBeenCalledOnce();
|
$disable->shouldHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
|||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
|
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
|
||||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
@@ -16,6 +17,8 @@ use Symfony\Component\Console\Tester\CommandTester;
|
|||||||
|
|
||||||
class GenerateKeyCommandTest extends TestCase
|
class GenerateKeyCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private CommandTester $commandTester;
|
private CommandTester $commandTester;
|
||||||
private ObjectProphecy $apiKeyService;
|
private ObjectProphecy $apiKeyService;
|
||||||
|
|
||||||
@@ -36,7 +39,7 @@ class GenerateKeyCommandTest extends TestCase
|
|||||||
$this->commandTester->execute([]);
|
$this->commandTester->execute([]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertStringContainsString('Generated API key: ', $output);
|
self::assertStringContainsString('Generated API key: ', $output);
|
||||||
$create->shouldHaveBeenCalledOnce();
|
$create->shouldHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
|
use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
|
||||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
@@ -14,6 +15,8 @@ use Symfony\Component\Console\Tester\CommandTester;
|
|||||||
|
|
||||||
class ListKeysCommandTest extends TestCase
|
class ListKeysCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private CommandTester $commandTester;
|
private CommandTester $commandTester;
|
||||||
private ObjectProphecy $apiKeyService;
|
private ObjectProphecy $apiKeyService;
|
||||||
|
|
||||||
@@ -38,11 +41,11 @@ class ListKeysCommandTest extends TestCase
|
|||||||
$this->commandTester->execute([]);
|
$this->commandTester->execute([]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertStringContainsString('Key', $output);
|
self::assertStringContainsString('Key', $output);
|
||||||
$this->assertStringContainsString('Is enabled', $output);
|
self::assertStringContainsString('Is enabled', $output);
|
||||||
$this->assertStringContainsString(' +++ ', $output);
|
self::assertStringContainsString(' +++ ', $output);
|
||||||
$this->assertStringNotContainsString(' --- ', $output);
|
self::assertStringNotContainsString(' --- ', $output);
|
||||||
$this->assertStringContainsString('Expiration date', $output);
|
self::assertStringContainsString('Expiration date', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
@@ -58,10 +61,10 @@ class ListKeysCommandTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertStringContainsString('Key', $output);
|
self::assertStringContainsString('Key', $output);
|
||||||
$this->assertStringNotContainsString('Is enabled', $output);
|
self::assertStringNotContainsString('Is enabled', $output);
|
||||||
$this->assertStringNotContainsString(' +++ ', $output);
|
self::assertStringNotContainsString(' +++ ', $output);
|
||||||
$this->assertStringNotContainsString(' --- ', $output);
|
self::assertStringNotContainsString(' --- ', $output);
|
||||||
$this->assertStringContainsString('Expiration date', $output);
|
self::assertStringContainsString('Expiration date', $output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use Doctrine\DBAL\Platforms\AbstractPlatform;
|
|||||||
use Doctrine\DBAL\Schema\AbstractSchemaManager;
|
use Doctrine\DBAL\Schema\AbstractSchemaManager;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Db\CreateDatabaseCommand;
|
use Shlinkio\Shlink\CLI\Command\Db\CreateDatabaseCommand;
|
||||||
use Symfony\Component\Console\Application;
|
use Symfony\Component\Console\Application;
|
||||||
@@ -22,10 +23,11 @@ use Symfony\Component\Process\Process;
|
|||||||
|
|
||||||
class CreateDatabaseCommandTest extends TestCase
|
class CreateDatabaseCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private CommandTester $commandTester;
|
private CommandTester $commandTester;
|
||||||
private ObjectProphecy $processHelper;
|
private ObjectProphecy $processHelper;
|
||||||
private ObjectProphecy $regularConn;
|
private ObjectProphecy $regularConn;
|
||||||
private ObjectProphecy $noDbNameConn;
|
|
||||||
private ObjectProphecy $schemaManager;
|
private ObjectProphecy $schemaManager;
|
||||||
private ObjectProphecy $databasePlatform;
|
private ObjectProphecy $databasePlatform;
|
||||||
|
|
||||||
@@ -48,15 +50,15 @@ class CreateDatabaseCommandTest extends TestCase
|
|||||||
$this->regularConn = $this->prophesize(Connection::class);
|
$this->regularConn = $this->prophesize(Connection::class);
|
||||||
$this->regularConn->getSchemaManager()->willReturn($this->schemaManager->reveal());
|
$this->regularConn->getSchemaManager()->willReturn($this->schemaManager->reveal());
|
||||||
$this->regularConn->getDatabasePlatform()->willReturn($this->databasePlatform->reveal());
|
$this->regularConn->getDatabasePlatform()->willReturn($this->databasePlatform->reveal());
|
||||||
$this->noDbNameConn = $this->prophesize(Connection::class);
|
$noDbNameConn = $this->prophesize(Connection::class);
|
||||||
$this->noDbNameConn->getSchemaManager()->willReturn($this->schemaManager->reveal());
|
$noDbNameConn->getSchemaManager()->willReturn($this->schemaManager->reveal());
|
||||||
|
|
||||||
$command = new CreateDatabaseCommand(
|
$command = new CreateDatabaseCommand(
|
||||||
$locker->reveal(),
|
$locker->reveal(),
|
||||||
$this->processHelper->reveal(),
|
$this->processHelper->reveal(),
|
||||||
$phpExecutableFinder->reveal(),
|
$phpExecutableFinder->reveal(),
|
||||||
$this->regularConn->reveal(),
|
$this->regularConn->reveal(),
|
||||||
$this->noDbNameConn->reveal(),
|
$noDbNameConn->reveal(),
|
||||||
);
|
);
|
||||||
$app = new Application();
|
$app = new Application();
|
||||||
$app->add($command);
|
$app->add($command);
|
||||||
@@ -77,7 +79,7 @@ class CreateDatabaseCommandTest extends TestCase
|
|||||||
$this->commandTester->execute([]);
|
$this->commandTester->execute([]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$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();
|
$getDatabase->shouldHaveBeenCalledOnce();
|
||||||
$listDatabases->shouldHaveBeenCalledOnce();
|
$listDatabases->shouldHaveBeenCalledOnce();
|
||||||
$createDatabase->shouldNotHaveBeenCalled();
|
$createDatabase->shouldNotHaveBeenCalled();
|
||||||
@@ -121,8 +123,8 @@ class CreateDatabaseCommandTest extends TestCase
|
|||||||
$this->commandTester->execute([]);
|
$this->commandTester->execute([]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertStringContainsString('Creating database tables...', $output);
|
self::assertStringContainsString('Creating database tables...', $output);
|
||||||
$this->assertStringContainsString('Database properly created!', $output);
|
self::assertStringContainsString('Database properly created!', $output);
|
||||||
$getDatabase->shouldHaveBeenCalledOnce();
|
$getDatabase->shouldHaveBeenCalledOnce();
|
||||||
$listDatabases->shouldHaveBeenCalledOnce();
|
$listDatabases->shouldHaveBeenCalledOnce();
|
||||||
$createDatabase->shouldNotHaveBeenCalled();
|
$createDatabase->shouldNotHaveBeenCalled();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\CLI\Command\Db;
|
|||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Db\MigrateDatabaseCommand;
|
use Shlinkio\Shlink\CLI\Command\Db\MigrateDatabaseCommand;
|
||||||
use Symfony\Component\Console\Application;
|
use Symfony\Component\Console\Application;
|
||||||
@@ -19,6 +20,8 @@ use Symfony\Component\Process\Process;
|
|||||||
|
|
||||||
class MigrateDatabaseCommandTest extends TestCase
|
class MigrateDatabaseCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private CommandTester $commandTester;
|
private CommandTester $commandTester;
|
||||||
private ObjectProphecy $processHelper;
|
private ObjectProphecy $processHelper;
|
||||||
|
|
||||||
@@ -60,8 +63,8 @@ class MigrateDatabaseCommandTest extends TestCase
|
|||||||
$this->commandTester->execute([]);
|
$this->commandTester->execute([]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertStringContainsString('Migrating database...', $output);
|
self::assertStringContainsString('Migrating database...', $output);
|
||||||
$this->assertStringContainsString('Database properly migrated!', $output);
|
self::assertStringContainsString('Database properly migrated!', $output);
|
||||||
$runCommand->shouldHaveBeenCalledOnce();
|
$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 PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\DeleteShortUrlCommand;
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\DeleteShortUrlCommand;
|
||||||
use Shlinkio\Shlink\Core\Exception;
|
use Shlinkio\Shlink\Core\Exception;
|
||||||
@@ -21,6 +22,8 @@ use const PHP_EOL;
|
|||||||
|
|
||||||
class DeleteShortUrlCommandTest extends TestCase
|
class DeleteShortUrlCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private CommandTester $commandTester;
|
private CommandTester $commandTester;
|
||||||
private ObjectProphecy $service;
|
private ObjectProphecy $service;
|
||||||
|
|
||||||
@@ -47,7 +50,7 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertStringContainsString(
|
self::assertStringContainsString(
|
||||||
sprintf('Short URL with short code "%s" successfully deleted.', $shortCode),
|
sprintf('Short URL with short code "%s" successfully deleted.', $shortCode),
|
||||||
$output,
|
$output,
|
||||||
);
|
);
|
||||||
@@ -66,7 +69,7 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$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();
|
$deleteByShortCode->shouldHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,11 +98,11 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$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.',
|
'Impossible to delete short URL with short code "%s" since it has more than "10" visits.',
|
||||||
$shortCode,
|
$shortCode,
|
||||||
), $output);
|
), $output);
|
||||||
$this->assertStringContainsString($expectedMessage, $output);
|
self::assertStringContainsString($expectedMessage, $output);
|
||||||
$deleteByShortCode->shouldHaveBeenCalledTimes($expectedDeleteCalls);
|
$deleteByShortCode->shouldHaveBeenCalledTimes($expectedDeleteCalls);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,11 +125,11 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$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.',
|
'Impossible to delete short URL with short code "%s" since it has more than "10" visits.',
|
||||||
$shortCode,
|
$shortCode,
|
||||||
), $output);
|
), $output);
|
||||||
$this->assertStringContainsString('Short URL was not deleted.', $output);
|
self::assertStringContainsString('Short URL was not deleted.', $output);
|
||||||
$deleteByShortCode->shouldHaveBeenCalledOnce();
|
$deleteByShortCode->shouldHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,18 +7,22 @@ namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
|||||||
use PHPUnit\Framework\Assert;
|
use PHPUnit\Framework\Assert;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\GenerateShortUrlCommand;
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\GenerateShortUrlCommand;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||||
|
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||||
use Symfony\Component\Console\Application;
|
use Symfony\Component\Console\Application;
|
||||||
use Symfony\Component\Console\Tester\CommandTester;
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
|
||||||
class GenerateShortUrlCommandTest extends TestCase
|
class GenerateShortUrlCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private const DOMAIN_CONFIG = [
|
private const DOMAIN_CONFIG = [
|
||||||
'schema' => 'http',
|
'schema' => 'http',
|
||||||
'hostname' => 'foo.com',
|
'hostname' => 'foo.com',
|
||||||
@@ -40,7 +44,7 @@ class GenerateShortUrlCommandTest extends TestCase
|
|||||||
public function properShortCodeIsCreatedIfLongUrlIsCorrect(): void
|
public function properShortCodeIsCreatedIfLongUrlIsCorrect(): void
|
||||||
{
|
{
|
||||||
$shortUrl = new ShortUrl('');
|
$shortUrl = new ShortUrl('');
|
||||||
$urlToShortCode = $this->urlShortener->urlToShortCode(Argument::cetera())->willReturn($shortUrl);
|
$urlToShortCode = $this->urlShortener->shorten(Argument::cetera())->willReturn($shortUrl);
|
||||||
|
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'longUrl' => 'http://domain.com/foo/bar',
|
'longUrl' => 'http://domain.com/foo/bar',
|
||||||
@@ -48,8 +52,8 @@ class GenerateShortUrlCommandTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||||
$this->assertStringContainsString($shortUrl->toString(self::DOMAIN_CONFIG), $output);
|
self::assertStringContainsString($shortUrl->toString(self::DOMAIN_CONFIG), $output);
|
||||||
$urlToShortCode->shouldHaveBeenCalledOnce();
|
$urlToShortCode->shouldHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,28 +61,28 @@ class GenerateShortUrlCommandTest extends TestCase
|
|||||||
public function exceptionWhileParsingLongUrlOutputsError(): void
|
public function exceptionWhileParsingLongUrlOutputsError(): void
|
||||||
{
|
{
|
||||||
$url = 'http://domain.com/invalid';
|
$url = 'http://domain.com/invalid';
|
||||||
$this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(InvalidUrlException::fromUrl($url))
|
$this->urlShortener->shorten(Argument::cetera())->willThrow(InvalidUrlException::fromUrl($url))
|
||||||
->shouldBeCalledOnce();
|
->shouldBeCalledOnce();
|
||||||
|
|
||||||
$this->commandTester->execute(['longUrl' => $url]);
|
$this->commandTester->execute(['longUrl' => $url]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
|
self::assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
|
||||||
$this->assertStringContainsString('Provided URL http://domain.com/invalid is invalid.', $output);
|
self::assertStringContainsString('Provided URL http://domain.com/invalid is invalid.', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
public function providingNonUniqueSlugOutputsError(): void
|
public function providingNonUniqueSlugOutputsError(): void
|
||||||
{
|
{
|
||||||
$urlToShortCode = $this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(
|
$urlToShortCode = $this->urlShortener->shorten(Argument::cetera())->willThrow(
|
||||||
NonUniqueSlugException::fromSlug('my-slug'),
|
NonUniqueSlugException::fromSlug('my-slug'),
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->commandTester->execute(['longUrl' => 'http://domain.com/invalid', '--customSlug' => 'my-slug']);
|
$this->commandTester->execute(['longUrl' => 'http://domain.com/invalid', '--customSlug' => 'my-slug']);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
|
self::assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
|
||||||
$this->assertStringContainsString('Provided slug "my-slug" is already in use', $output);
|
self::assertStringContainsString('Provided slug "my-slug" is already in use', $output);
|
||||||
$urlToShortCode->shouldHaveBeenCalledOnce();
|
$urlToShortCode->shouldHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +90,7 @@ class GenerateShortUrlCommandTest extends TestCase
|
|||||||
public function properlyProcessesProvidedTags(): void
|
public function properlyProcessesProvidedTags(): void
|
||||||
{
|
{
|
||||||
$shortUrl = new ShortUrl('');
|
$shortUrl = new ShortUrl('');
|
||||||
$urlToShortCode = $this->urlShortener->urlToShortCode(
|
$urlToShortCode = $this->urlShortener->shorten(
|
||||||
Argument::type('string'),
|
Argument::type('string'),
|
||||||
Argument::that(function (array $tags) {
|
Argument::that(function (array $tags) {
|
||||||
Assert::assertEquals(['foo', 'bar', 'baz', 'boo', 'zar'], $tags);
|
Assert::assertEquals(['foo', 'bar', 'baz', 'boo', 'zar'], $tags);
|
||||||
@@ -101,8 +105,38 @@ class GenerateShortUrlCommandTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||||
$this->assertStringContainsString($shortUrl->toString(self::DOMAIN_CONFIG), $output);
|
self::assertStringContainsString($shortUrl->toString(self::DOMAIN_CONFIG), $output);
|
||||||
$urlToShortCode->shouldHaveBeenCalledOnce();
|
$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 Laminas\Paginator\Paginator;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\GetVisitsCommand;
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\GetVisitsCommand;
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
@@ -27,6 +28,8 @@ use function sprintf;
|
|||||||
|
|
||||||
class GetVisitsCommandTest extends TestCase
|
class GetVisitsCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private CommandTester $commandTester;
|
private CommandTester $commandTester;
|
||||||
private ObjectProphecy $visitsTracker;
|
private ObjectProphecy $visitsTracker;
|
||||||
|
|
||||||
@@ -88,7 +91,7 @@ class GetVisitsCommandTest extends TestCase
|
|||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$info->shouldHaveBeenCalledOnce();
|
$info->shouldHaveBeenCalledOnce();
|
||||||
$this->assertStringContainsString(
|
self::assertStringContainsString(
|
||||||
sprintf('Ignored provided "startDate" since its value "%s" is not a valid date', $startDate),
|
sprintf('Ignored provided "startDate" since its value "%s" is not a valid date', $startDate),
|
||||||
$output,
|
$output,
|
||||||
);
|
);
|
||||||
@@ -108,8 +111,8 @@ class GetVisitsCommandTest extends TestCase
|
|||||||
|
|
||||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
$this->assertStringContainsString('foo', $output);
|
self::assertStringContainsString('foo', $output);
|
||||||
$this->assertStringContainsString('Spain', $output);
|
self::assertStringContainsString('Spain', $output);
|
||||||
$this->assertStringContainsString('bar', $output);
|
self::assertStringContainsString('bar', $output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use Laminas\Paginator\Adapter\ArrayAdapter;
|
|||||||
use Laminas\Paginator\Paginator;
|
use Laminas\Paginator\Paginator;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
@@ -21,6 +22,8 @@ use function explode;
|
|||||||
|
|
||||||
class ListShortUrlsCommandTest extends TestCase
|
class ListShortUrlsCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private CommandTester $commandTester;
|
private CommandTester $commandTester;
|
||||||
private ObjectProphecy $shortUrlService;
|
private ObjectProphecy $shortUrlService;
|
||||||
|
|
||||||
@@ -50,9 +53,9 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
$this->commandTester->execute([]);
|
$this->commandTester->execute([]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertStringContainsString('Continue with page 2?', $output);
|
self::assertStringContainsString('Continue with page 2?', $output);
|
||||||
$this->assertStringContainsString('Continue with page 3?', $output);
|
self::assertStringContainsString('Continue with page 3?', $output);
|
||||||
$this->assertStringContainsString('Continue with page 4?', $output);
|
self::assertStringContainsString('Continue with page 4?', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
@@ -72,13 +75,13 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
$this->commandTester->execute([]);
|
$this->commandTester->execute([]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertStringContainsString('url_1', $output);
|
self::assertStringContainsString('url_1', $output);
|
||||||
$this->assertStringContainsString('url_9', $output);
|
self::assertStringContainsString('url_9', $output);
|
||||||
$this->assertStringNotContainsString('url_10', $output);
|
self::assertStringNotContainsString('url_10', $output);
|
||||||
$this->assertStringNotContainsString('url_20', $output);
|
self::assertStringNotContainsString('url_20', $output);
|
||||||
$this->assertStringNotContainsString('url_30', $output);
|
self::assertStringNotContainsString('url_30', $output);
|
||||||
$this->assertStringContainsString('Continue with page 2?', $output);
|
self::assertStringContainsString('Continue with page 2?', $output);
|
||||||
$this->assertStringNotContainsString('Continue with page 3?', $output);
|
self::assertStringNotContainsString('Continue with page 3?', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
@@ -103,7 +106,7 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
$this->commandTester->setInputs(['y']);
|
$this->commandTester->setInputs(['y']);
|
||||||
$this->commandTester->execute(['--showTags' => true]);
|
$this->commandTester->execute(['--showTags' => true]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$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;
|
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\ResolveUrlCommand;
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\ResolveUrlCommand;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
@@ -20,6 +21,8 @@ use const PHP_EOL;
|
|||||||
|
|
||||||
class ResolveUrlCommandTest extends TestCase
|
class ResolveUrlCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private CommandTester $commandTester;
|
private CommandTester $commandTester;
|
||||||
private ObjectProphecy $urlResolver;
|
private ObjectProphecy $urlResolver;
|
||||||
|
|
||||||
@@ -44,7 +47,7 @@ class ResolveUrlCommandTest extends TestCase
|
|||||||
|
|
||||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
$this->assertEquals('Long URL: ' . $expectedUrl . PHP_EOL, $output);
|
self::assertEquals('Long URL: ' . $expectedUrl . PHP_EOL, $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
@@ -59,6 +62,6 @@ class ResolveUrlCommandTest extends TestCase
|
|||||||
|
|
||||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$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 Doctrine\Common\Collections\ArrayCollection;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Tag\CreateTagCommand;
|
use Shlinkio\Shlink\CLI\Command\Tag\CreateTagCommand;
|
||||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||||
@@ -14,6 +15,8 @@ use Symfony\Component\Console\Tester\CommandTester;
|
|||||||
|
|
||||||
class CreateTagCommandTest extends TestCase
|
class CreateTagCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private CommandTester $commandTester;
|
private CommandTester $commandTester;
|
||||||
private ObjectProphecy $tagService;
|
private ObjectProphecy $tagService;
|
||||||
|
|
||||||
@@ -34,7 +37,7 @@ class CreateTagCommandTest extends TestCase
|
|||||||
$this->commandTester->execute([]);
|
$this->commandTester->execute([]);
|
||||||
|
|
||||||
$output = $this->commandTester->getDisplay();
|
$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 */
|
/** @test */
|
||||||
@@ -48,7 +51,7 @@ class CreateTagCommandTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertStringContainsString('Tags properly created', $output);
|
self::assertStringContainsString('Tags properly created', $output);
|
||||||
$createTags->shouldHaveBeenCalled();
|
$createTags->shouldHaveBeenCalled();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Tag\DeleteTagsCommand;
|
use Shlinkio\Shlink\CLI\Command\Tag\DeleteTagsCommand;
|
||||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||||
@@ -13,6 +14,8 @@ use Symfony\Component\Console\Tester\CommandTester;
|
|||||||
|
|
||||||
class DeleteTagsCommandTest extends TestCase
|
class DeleteTagsCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private CommandTester $commandTester;
|
private CommandTester $commandTester;
|
||||||
private ObjectProphecy $tagService;
|
private ObjectProphecy $tagService;
|
||||||
|
|
||||||
@@ -33,7 +36,7 @@ class DeleteTagsCommandTest extends TestCase
|
|||||||
$this->commandTester->execute([]);
|
$this->commandTester->execute([]);
|
||||||
|
|
||||||
$output = $this->commandTester->getDisplay();
|
$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 */
|
/** @test */
|
||||||
@@ -48,7 +51,7 @@ class DeleteTagsCommandTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertStringContainsString('Tags properly deleted', $output);
|
self::assertStringContainsString('Tags properly deleted', $output);
|
||||||
$deleteTags->shouldHaveBeenCalled();
|
$deleteTags->shouldHaveBeenCalled();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Tag\ListTagsCommand;
|
use Shlinkio\Shlink\CLI\Command\Tag\ListTagsCommand;
|
||||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||||
@@ -15,6 +16,8 @@ use Symfony\Component\Console\Tester\CommandTester;
|
|||||||
|
|
||||||
class ListTagsCommandTest extends TestCase
|
class ListTagsCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private CommandTester $commandTester;
|
private CommandTester $commandTester;
|
||||||
private ObjectProphecy $tagService;
|
private ObjectProphecy $tagService;
|
||||||
|
|
||||||
@@ -37,7 +40,7 @@ class ListTagsCommandTest extends TestCase
|
|||||||
$this->commandTester->execute([]);
|
$this->commandTester->execute([]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertStringContainsString('No tags found', $output);
|
self::assertStringContainsString('No tags found', $output);
|
||||||
$tagsInfo->shouldHaveBeenCalled();
|
$tagsInfo->shouldHaveBeenCalled();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,12 +55,12 @@ class ListTagsCommandTest extends TestCase
|
|||||||
$this->commandTester->execute([]);
|
$this->commandTester->execute([]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertStringContainsString('| foo', $output);
|
self::assertStringContainsString('| foo', $output);
|
||||||
$this->assertStringContainsString('| bar', $output);
|
self::assertStringContainsString('| bar', $output);
|
||||||
$this->assertStringContainsString('| 10 ', $output);
|
self::assertStringContainsString('| 10 ', $output);
|
||||||
$this->assertStringContainsString('| 2 ', $output);
|
self::assertStringContainsString('| 2 ', $output);
|
||||||
$this->assertStringContainsString('| 7 ', $output);
|
self::assertStringContainsString('| 7 ', $output);
|
||||||
$this->assertStringContainsString('| 32 ', $output);
|
self::assertStringContainsString('| 32 ', $output);
|
||||||
$tagsInfo->shouldHaveBeenCalled();
|
$tagsInfo->shouldHaveBeenCalled();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Tag\RenameTagCommand;
|
use Shlinkio\Shlink\CLI\Command\Tag\RenameTagCommand;
|
||||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||||
@@ -15,6 +16,8 @@ use Symfony\Component\Console\Tester\CommandTester;
|
|||||||
|
|
||||||
class RenameTagCommandTest extends TestCase
|
class RenameTagCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private CommandTester $commandTester;
|
private CommandTester $commandTester;
|
||||||
private ObjectProphecy $tagService;
|
private ObjectProphecy $tagService;
|
||||||
|
|
||||||
@@ -42,7 +45,7 @@ class RenameTagCommandTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$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();
|
$renameTag->shouldHaveBeenCalled();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +62,7 @@ class RenameTagCommandTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertStringContainsString('Tag properly renamed', $output);
|
self::assertStringContainsString('Tag properly renamed', $output);
|
||||||
$renameTag->shouldHaveBeenCalled();
|
$renameTag->shouldHaveBeenCalled();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\CLI\Command\Visit;
|
|||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Visit\LocateVisitsCommand;
|
use Shlinkio\Shlink\CLI\Command\Visit\LocateVisitsCommand;
|
||||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||||
@@ -32,10 +33,11 @@ use const PHP_EOL;
|
|||||||
|
|
||||||
class LocateVisitsCommandTest extends TestCase
|
class LocateVisitsCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private CommandTester $commandTester;
|
private CommandTester $commandTester;
|
||||||
private ObjectProphecy $visitService;
|
private ObjectProphecy $visitService;
|
||||||
private ObjectProphecy $ipResolver;
|
private ObjectProphecy $ipResolver;
|
||||||
private ObjectProphecy $locker;
|
|
||||||
private ObjectProphecy $lock;
|
private ObjectProphecy $lock;
|
||||||
private ObjectProphecy $dbUpdater;
|
private ObjectProphecy $dbUpdater;
|
||||||
|
|
||||||
@@ -45,17 +47,17 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
$this->ipResolver = $this->prophesize(IpLocationResolverInterface::class);
|
$this->ipResolver = $this->prophesize(IpLocationResolverInterface::class);
|
||||||
$this->dbUpdater = $this->prophesize(GeolocationDbUpdaterInterface::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 = $this->prophesize(Lock\LockInterface::class);
|
||||||
$this->lock->acquire(false)->willReturn(true);
|
$this->lock->acquire(false)->willReturn(true);
|
||||||
$this->lock->release()->will(function (): void {
|
$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(
|
$command = new LocateVisitsCommand(
|
||||||
$this->visitService->reveal(),
|
$this->visitService->reveal(),
|
||||||
$this->ipResolver->reveal(),
|
$this->ipResolver->reveal(),
|
||||||
$this->locker->reveal(),
|
$locker->reveal(),
|
||||||
$this->dbUpdater->reveal(),
|
$this->dbUpdater->reveal(),
|
||||||
);
|
);
|
||||||
$app = new Application();
|
$app = new Application();
|
||||||
@@ -92,11 +94,11 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
$this->commandTester->execute($args);
|
$this->commandTester->execute($args);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertStringContainsString('Processing IP 1.2.3.0', $output);
|
self::assertStringContainsString('Processing IP 1.2.3.0', $output);
|
||||||
if ($expectWarningPrint) {
|
if ($expectWarningPrint) {
|
||||||
$this->assertStringContainsString('Continue at your own risk', $output);
|
self::assertStringContainsString('Continue at your own', $output);
|
||||||
} else {
|
} else {
|
||||||
$this->assertStringNotContainsString('Continue at your own risk', $output);
|
self::assertStringNotContainsString('Continue at your own', $output);
|
||||||
}
|
}
|
||||||
$locateVisits->shouldHaveBeenCalledTimes($expectedUnlocatedCalls);
|
$locateVisits->shouldHaveBeenCalledTimes($expectedUnlocatedCalls);
|
||||||
$locateEmptyVisits->shouldHaveBeenCalledTimes($expectedEmptyCalls);
|
$locateEmptyVisits->shouldHaveBeenCalledTimes($expectedEmptyCalls);
|
||||||
@@ -132,11 +134,11 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||||
|
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
$this->assertStringContainsString($message, $output);
|
self::assertStringContainsString($message, $output);
|
||||||
if (empty($address)) {
|
if (empty($address)) {
|
||||||
$this->assertStringNotContainsString('Processing IP', $output);
|
self::assertStringNotContainsString('Processing IP', $output);
|
||||||
} else {
|
} else {
|
||||||
$this->assertStringContainsString('Processing IP', $output);
|
self::assertStringContainsString('Processing IP', $output);
|
||||||
}
|
}
|
||||||
$locateVisits->shouldHaveBeenCalledOnce();
|
$locateVisits->shouldHaveBeenCalledOnce();
|
||||||
$resolveIpLocation->shouldNotHaveBeenCalled();
|
$resolveIpLocation->shouldNotHaveBeenCalled();
|
||||||
@@ -164,7 +166,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
|
|
||||||
$output = $this->commandTester->getDisplay();
|
$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();
|
$locateVisits->shouldHaveBeenCalledOnce();
|
||||||
$resolveIpLocation->shouldHaveBeenCalledOnce();
|
$resolveIpLocation->shouldHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
@@ -192,7 +194,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertStringContainsString(
|
self::assertStringContainsString(
|
||||||
sprintf('Command "%s" is already in progress. Skipping.', LocateVisitsCommand::NAME),
|
sprintf('Command "%s" is already in progress. Skipping.', LocateVisitsCommand::NAME),
|
||||||
$output,
|
$output,
|
||||||
);
|
);
|
||||||
@@ -222,11 +224,11 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
$this->commandTester->execute([]);
|
$this->commandTester->execute([]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertStringContainsString(
|
self::assertStringContainsString(
|
||||||
sprintf('%s GeoLite2 database...', $olderDbExists ? 'Updating' : 'Downloading'),
|
sprintf('%s GeoLite2 database...', $olderDbExists ? 'Updating' : 'Downloading'),
|
||||||
$output,
|
$output,
|
||||||
);
|
);
|
||||||
$this->assertStringContainsString($expectedMessage, $output);
|
self::assertStringContainsString($expectedMessage, $output);
|
||||||
$locateVisits->shouldHaveBeenCalledTimes((int) $olderDbExists);
|
$locateVisits->shouldHaveBeenCalledTimes((int) $olderDbExists);
|
||||||
$checkDbUpdate->shouldHaveBeenCalledOnce();
|
$checkDbUpdate->shouldHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
@@ -243,7 +245,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
$this->commandTester->execute(['--all' => true]);
|
$this->commandTester->execute(['--all' => true]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$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 */
|
/** @test */
|
||||||
public function confiIsProperlyReturned(): void
|
public function configIsProperlyReturned(): void
|
||||||
{
|
{
|
||||||
$config = ($this->configProvider)();
|
$config = ($this->configProvider)();
|
||||||
|
|
||||||
$this->assertArrayHasKey('cli', $config);
|
self::assertArrayHasKey('cli', $config);
|
||||||
$this->assertArrayHasKey('dependencies', $config);
|
self::assertArrayHasKey('dependencies', $config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ class GeolocationDbUpdateFailedExceptionTest extends TestCase
|
|||||||
{
|
{
|
||||||
$e = GeolocationDbUpdateFailedException::create($olderDbExists, $prev);
|
$e = GeolocationDbUpdateFailedException::create($olderDbExists, $prev);
|
||||||
|
|
||||||
$this->assertEquals($olderDbExists, $e->olderDbExists());
|
self::assertEquals($olderDbExists, $e->olderDbExists());
|
||||||
$this->assertEquals(
|
self::assertEquals(
|
||||||
'An error occurred while updating geolocation database, and an older version could not be found',
|
'An error occurred while updating geolocation database, and an older version could not be found',
|
||||||
$e->getMessage(),
|
$e->getMessage(),
|
||||||
);
|
);
|
||||||
$this->assertEquals(0, $e->getCode());
|
self::assertEquals(0, $e->getCode());
|
||||||
$this->assertEquals($prev, $e->getPrevious());
|
self::assertEquals($prev, $e->getPrevious());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideCreateArgs(): iterable
|
public function provideCreateArgs(): iterable
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace ShlinkioTest\Shlink\CLI\Factory;
|
|||||||
use Laminas\ServiceManager\ServiceManager;
|
use Laminas\ServiceManager\ServiceManager;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
|
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
|
||||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||||
@@ -15,6 +16,8 @@ use Symfony\Component\Console\Command\Command;
|
|||||||
|
|
||||||
class ApplicationFactoryTest extends TestCase
|
class ApplicationFactoryTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private ApplicationFactory $factory;
|
private ApplicationFactory $factory;
|
||||||
|
|
||||||
public function setUp(): void
|
public function setUp(): void
|
||||||
@@ -37,9 +40,9 @@ class ApplicationFactoryTest extends TestCase
|
|||||||
|
|
||||||
$instance = ($this->factory)($sm);
|
$instance = ($this->factory)($sm);
|
||||||
|
|
||||||
$this->assertTrue($instance->has('foo'));
|
self::assertTrue($instance->has('foo'));
|
||||||
$this->assertTrue($instance->has('bar'));
|
self::assertTrue($instance->has('bar'));
|
||||||
$this->assertFalse($instance->has('baz'));
|
self::assertFalse($instance->has('baz'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createServiceManager(array $config = []): ServiceManager
|
private function createServiceManager(array $config = []): ServiceManager
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use GeoIp2\Database\Reader;
|
|||||||
use MaxMind\Db\Reader\Metadata;
|
use MaxMind\Db\Reader\Metadata;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
||||||
@@ -22,35 +23,35 @@ use function range;
|
|||||||
|
|
||||||
class GeolocationDbUpdaterTest extends TestCase
|
class GeolocationDbUpdaterTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private GeolocationDbUpdater $geolocationDbUpdater;
|
private GeolocationDbUpdater $geolocationDbUpdater;
|
||||||
private ObjectProphecy $dbUpdater;
|
private ObjectProphecy $dbUpdater;
|
||||||
private ObjectProphecy $geoLiteDbReader;
|
private ObjectProphecy $geoLiteDbReader;
|
||||||
private ObjectProphecy $locker;
|
|
||||||
private ObjectProphecy $lock;
|
|
||||||
|
|
||||||
public function setUp(): void
|
public function setUp(): void
|
||||||
{
|
{
|
||||||
$this->dbUpdater = $this->prophesize(DbUpdaterInterface::class);
|
$this->dbUpdater = $this->prophesize(DbUpdaterInterface::class);
|
||||||
$this->geoLiteDbReader = $this->prophesize(Reader::class);
|
$this->geoLiteDbReader = $this->prophesize(Reader::class);
|
||||||
|
|
||||||
$this->locker = $this->prophesize(Lock\LockFactory::class);
|
$locker = $this->prophesize(Lock\LockFactory::class);
|
||||||
$this->lock = $this->prophesize(Lock\LockInterface::class);
|
$lock = $this->prophesize(Lock\LockInterface::class);
|
||||||
$this->lock->acquire(true)->willReturn(true);
|
$lock->acquire(true)->willReturn(true);
|
||||||
$this->lock->release()->will(function (): void {
|
$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->geolocationDbUpdater = new GeolocationDbUpdater(
|
||||||
$this->dbUpdater->reveal(),
|
$this->dbUpdater->reveal(),
|
||||||
$this->geoLiteDbReader->reveal(),
|
$this->geoLiteDbReader->reveal(),
|
||||||
$this->locker->reveal(),
|
$locker->reveal(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
public function exceptionIsThrownWhenOlderDbDoesNotExistAndDownloadFails(): void
|
public function exceptionIsThrownWhenOlderDbDoesNotExistAndDownloadFails(): void
|
||||||
{
|
{
|
||||||
$mustBeUpdated = fn () => $this->assertTrue(true);
|
$mustBeUpdated = fn () => self::assertTrue(true);
|
||||||
$prev = new RuntimeException('');
|
$prev = new RuntimeException('');
|
||||||
|
|
||||||
$fileExists = $this->dbUpdater->databaseFileExists()->willReturn(false);
|
$fileExists = $this->dbUpdater->databaseFileExists()->willReturn(false);
|
||||||
@@ -59,12 +60,12 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$this->geolocationDbUpdater->checkDbUpdate($mustBeUpdated);
|
$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) {
|
} catch (Throwable $e) {
|
||||||
/** @var GeolocationDbUpdateFailedException $e */
|
/** @var GeolocationDbUpdateFailedException $e */
|
||||||
$this->assertInstanceOf(GeolocationDbUpdateFailedException::class, $e);
|
self::assertInstanceOf(GeolocationDbUpdateFailedException::class, $e);
|
||||||
$this->assertSame($prev, $e->getPrevious());
|
self::assertSame($prev, $e->getPrevious());
|
||||||
$this->assertFalse($e->olderDbExists());
|
self::assertFalse($e->olderDbExists());
|
||||||
}
|
}
|
||||||
|
|
||||||
$fileExists->shouldHaveBeenCalledOnce();
|
$fileExists->shouldHaveBeenCalledOnce();
|
||||||
@@ -95,12 +96,12 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$this->geolocationDbUpdater->checkDbUpdate();
|
$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) {
|
} catch (Throwable $e) {
|
||||||
/** @var GeolocationDbUpdateFailedException $e */
|
/** @var GeolocationDbUpdateFailedException $e */
|
||||||
$this->assertInstanceOf(GeolocationDbUpdateFailedException::class, $e);
|
self::assertInstanceOf(GeolocationDbUpdateFailedException::class, $e);
|
||||||
$this->assertSame($prev, $e->getPrevious());
|
self::assertSame($prev, $e->getPrevious());
|
||||||
$this->assertTrue($e->olderDbExists());
|
self::assertTrue($e->olderDbExists());
|
||||||
}
|
}
|
||||||
|
|
||||||
$fileExists->shouldHaveBeenCalledOnce();
|
$fileExists->shouldHaveBeenCalledOnce();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\CLI\Util;
|
|||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use ReflectionObject;
|
use ReflectionObject;
|
||||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||||
@@ -15,6 +16,8 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||||||
|
|
||||||
class ShlinkTableTest extends TestCase
|
class ShlinkTableTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private ShlinkTable $shlinkTable;
|
private ShlinkTable $shlinkTable;
|
||||||
private ObjectProphecy $baseTable;
|
private ObjectProphecy $baseTable;
|
||||||
|
|
||||||
@@ -60,6 +63,6 @@ class ShlinkTableTest extends TestCase
|
|||||||
$baseTable = $ref->getProperty('baseTable');
|
$baseTable = $ref->getProperty('baseTable');
|
||||||
$baseTable->setAccessible(true);
|
$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 Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||||
use Mezzio\Template\TemplateRendererInterface;
|
use Mezzio\Template\TemplateRendererInterface;
|
||||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Shlinkio\Shlink\Core\Domain\Resolver;
|
|
||||||
use Shlinkio\Shlink\Core\ErrorHandler;
|
use Shlinkio\Shlink\Core\ErrorHandler;
|
||||||
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||||
|
use Shlinkio\Shlink\Importer\ImportedLinksProcessorInterface;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
@@ -31,21 +31,35 @@ return [
|
|||||||
Tag\TagService::class => ConfigAbstractFactory::class,
|
Tag\TagService::class => ConfigAbstractFactory::class,
|
||||||
Service\ShortUrl\DeleteShortUrlService::class => ConfigAbstractFactory::class,
|
Service\ShortUrl\DeleteShortUrlService::class => ConfigAbstractFactory::class,
|
||||||
Service\ShortUrl\ShortUrlResolver::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\UrlValidator::class => ConfigAbstractFactory::class,
|
||||||
|
Util\DoctrineBatchHelper::class => ConfigAbstractFactory::class,
|
||||||
|
Util\RedirectResponseHelper::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Action\RedirectAction::class => ConfigAbstractFactory::class,
|
Action\RedirectAction::class => ConfigAbstractFactory::class,
|
||||||
Action\PixelAction::class => ConfigAbstractFactory::class,
|
Action\PixelAction::class => ConfigAbstractFactory::class,
|
||||||
Action\QrCodeAction::class => ConfigAbstractFactory::class,
|
Action\QrCodeAction::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Resolver\PersistenceDomainResolver::class => ConfigAbstractFactory::class,
|
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Mercure\MercureUpdatesGenerator::class => ConfigAbstractFactory::class,
|
Mercure\MercureUpdatesGenerator::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
|
Importer\ImportedLinksProcessor::class => ConfigAbstractFactory::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'aliases' => [
|
||||||
|
ImportedLinksProcessorInterface::class => Importer\ImportedLinksProcessor::class,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
ConfigAbstractFactory::class => [
|
ConfigAbstractFactory::class => [
|
||||||
ErrorHandler\NotFoundRedirectHandler::class => [NotFoundRedirectOptions::class, 'config.router.base_path'],
|
ErrorHandler\NotFoundRedirectHandler::class => [
|
||||||
|
NotFoundRedirectOptions::class,
|
||||||
|
Util\RedirectResponseHelper::class,
|
||||||
|
'config.router.base_path',
|
||||||
|
],
|
||||||
ErrorHandler\NotFoundTemplateHandler::class => [TemplateRendererInterface::class],
|
ErrorHandler\NotFoundTemplateHandler::class => [TemplateRendererInterface::class],
|
||||||
|
|
||||||
Options\AppOptions::class => ['config.app_options'],
|
Options\AppOptions::class => ['config.app_options'],
|
||||||
@@ -53,7 +67,12 @@ return [
|
|||||||
Options\NotFoundRedirectOptions::class => ['config.not_found_redirects'],
|
Options\NotFoundRedirectOptions::class => ['config.not_found_redirects'],
|
||||||
Options\UrlShortenerOptions::class => ['config.url_shortener'],
|
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 => [
|
Service\VisitsTracker::class => [
|
||||||
'em',
|
'em',
|
||||||
EventDispatcherInterface::class,
|
EventDispatcherInterface::class,
|
||||||
@@ -69,14 +88,18 @@ return [
|
|||||||
Service\ShortUrl\ShortUrlResolver::class,
|
Service\ShortUrl\ShortUrlResolver::class,
|
||||||
],
|
],
|
||||||
Service\ShortUrl\ShortUrlResolver::class => ['em'],
|
Service\ShortUrl\ShortUrlResolver::class => ['em'],
|
||||||
|
Service\ShortUrl\ShortCodeHelper::class => ['em'],
|
||||||
|
Domain\DomainService::class => ['em'],
|
||||||
|
|
||||||
Util\UrlValidator::class => ['httpClient', Options\UrlShortenerOptions::class],
|
Util\UrlValidator::class => ['httpClient', Options\UrlShortenerOptions::class],
|
||||||
|
Util\DoctrineBatchHelper::class => ['em'],
|
||||||
|
Util\RedirectResponseHelper::class => [Options\UrlShortenerOptions::class],
|
||||||
|
|
||||||
Action\RedirectAction::class => [
|
Action\RedirectAction::class => [
|
||||||
Service\ShortUrl\ShortUrlResolver::class,
|
Service\ShortUrl\ShortUrlResolver::class,
|
||||||
Service\VisitsTracker::class,
|
Service\VisitsTracker::class,
|
||||||
Options\AppOptions::class,
|
Options\AppOptions::class,
|
||||||
Options\UrlShortenerOptions::class,
|
Util\RedirectResponseHelper::class,
|
||||||
'Logger_Shlink',
|
'Logger_Shlink',
|
||||||
],
|
],
|
||||||
Action\PixelAction::class => [
|
Action\PixelAction::class => [
|
||||||
@@ -91,9 +114,16 @@ return [
|
|||||||
'Logger_Shlink',
|
'Logger_Shlink',
|
||||||
],
|
],
|
||||||
|
|
||||||
Resolver\PersistenceDomainResolver::class => ['em'],
|
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class => ['em'],
|
||||||
|
|
||||||
Mercure\MercureUpdatesGenerator::class => ['config.url_shortener.domain'],
|
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 {
|
return static function (ClassMetadata $metadata, array $emConfig): void {
|
||||||
$builder = new ClassMetadataBuilder($metadata);
|
$builder = new ClassMetadataBuilder($metadata);
|
||||||
|
|
||||||
$builder->setTable(determineTableName('domains', $emConfig));
|
$builder->setTable(determineTableName('domains', $emConfig))
|
||||||
|
->setCustomRepositoryClass(Domain\Repository\DomainRepository::class);
|
||||||
|
|
||||||
$builder->createField('id', Types::BIGINT)
|
$builder->createField('id', Types::BIGINT)
|
||||||
->columnName('id')
|
->columnName('id')
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use Doctrine\DBAL\Types\Types;
|
|||||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||||
use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
|
use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
|
||||||
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
|
||||||
return static function (ClassMetadata $metadata, array $emConfig): void {
|
return static function (ClassMetadata $metadata, array $emConfig): void {
|
||||||
$builder = new ClassMetadataBuilder($metadata);
|
$builder = new ClassMetadataBuilder($metadata);
|
||||||
@@ -51,6 +52,16 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
|
|||||||
->nullable()
|
->nullable()
|
||||||
->build();
|
->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)
|
$builder->createOneToMany('visits', Entity\Visit::class)
|
||||||
->mappedBy('shortUrl')
|
->mappedBy('shortUrl')
|
||||||
->fetchExtraLazy()
|
->fetchExtraLazy()
|
||||||
@@ -68,5 +79,9 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
|
|||||||
->cascadePersist()
|
->cascadePersist()
|
||||||
->build();
|
->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');
|
$builder->addUniqueConstraint(['short_code', 'domain_id'], 'unique_short_code_plus_domain');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core;
|
|||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
use Fig\Http\Message\StatusCodeInterface;
|
use Fig\Http\Message\StatusCodeInterface;
|
||||||
|
use Laminas\InputFilter\InputFilter;
|
||||||
use PUGX\Shortid\Factory as ShortIdFactory;
|
use PUGX\Shortid\Factory as ShortIdFactory;
|
||||||
|
|
||||||
use function sprintf;
|
use function sprintf;
|
||||||
@@ -62,3 +63,15 @@ function determineTableName(string $tableName, array $emConfig = []): string
|
|||||||
|
|
||||||
return sprintf('%s.%s', $schema, $tableName);
|
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;
|
namespace Shlinkio\Shlink\Core\Action;
|
||||||
|
|
||||||
use Fig\Http\Message\RequestMethodInterface;
|
use Fig\Http\Message\RequestMethodInterface;
|
||||||
|
use GuzzleHttp\Psr7\Query;
|
||||||
use League\Uri\Uri;
|
use League\Uri\Uri;
|
||||||
use Mezzio\Router\Middleware\ImplicitHeadMiddleware;
|
use Mezzio\Router\Middleware\ImplicitHeadMiddleware;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
@@ -23,8 +24,6 @@ use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
|||||||
|
|
||||||
use function array_key_exists;
|
use function array_key_exists;
|
||||||
use function array_merge;
|
use function array_merge;
|
||||||
use function GuzzleHttp\Psr7\build_query;
|
|
||||||
use function GuzzleHttp\Psr7\parse_query;
|
|
||||||
|
|
||||||
abstract class AbstractTrackingAction implements MiddlewareInterface, RequestMethodInterface
|
abstract class AbstractTrackingAction implements MiddlewareInterface, RequestMethodInterface
|
||||||
{
|
{
|
||||||
@@ -68,13 +67,13 @@ abstract class AbstractTrackingAction implements MiddlewareInterface, RequestMet
|
|||||||
private function buildUrlToRedirectTo(ShortUrl $shortUrl, array $currentQuery, ?string $disableTrackParam): string
|
private function buildUrlToRedirectTo(ShortUrl $shortUrl, array $currentQuery, ?string $disableTrackParam): string
|
||||||
{
|
{
|
||||||
$uri = Uri::createFromString($shortUrl->getLongUrl());
|
$uri = Uri::createFromString($shortUrl->getLongUrl());
|
||||||
$hardcodedQuery = parse_query($uri->getQuery() ?? '');
|
$hardcodedQuery = Query::parse($uri->getQuery() ?? '');
|
||||||
if ($disableTrackParam !== null) {
|
if ($disableTrackParam !== null) {
|
||||||
unset($currentQuery[$disableTrackParam]);
|
unset($currentQuery[$disableTrackParam]);
|
||||||
}
|
}
|
||||||
$mergedQuery = array_merge($hardcodedQuery, $currentQuery);
|
$mergedQuery = array_merge($hardcodedQuery, $currentQuery);
|
||||||
|
|
||||||
return (string) (empty($mergedQuery) ? $uri : $uri->withQuery(build_query($mergedQuery)));
|
return (string) (empty($mergedQuery) ? $uri : $uri->withQuery(Query::build($mergedQuery)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function shouldTrackRequest(ServerRequestInterface $request, array $query, ?string $disableTrackParam): bool
|
private function shouldTrackRequest(ServerRequestInterface $request, array $query, ?string $disableTrackParam): bool
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink\Core\Action;
|
namespace Shlinkio\Shlink\Core\Action;
|
||||||
|
|
||||||
use Endroid\QrCode\QrCode;
|
use Endroid\QrCode\QrCode;
|
||||||
|
use Endroid\QrCode\Writer\SvgWriter;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Psr\Http\Server\MiddlewareInterface;
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
@@ -51,6 +52,11 @@ class QrCodeAction implements MiddlewareInterface
|
|||||||
$qrCode->setSize($this->getSizeParam($request));
|
$qrCode->setSize($this->getSizeParam($request));
|
||||||
$qrCode->setMargin(0);
|
$qrCode->setMargin(0);
|
||||||
|
|
||||||
|
$format = $request->getQueryParams()['format'] ?? 'png';
|
||||||
|
if ($format === 'svg') {
|
||||||
|
$qrCode->setWriter(new SvgWriter());
|
||||||
|
}
|
||||||
|
|
||||||
return new QrCodeResponse($qrCode);
|
return new QrCodeResponse($qrCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink\Core\Action;
|
namespace Shlinkio\Shlink\Core\Action;
|
||||||
|
|
||||||
use Fig\Http\Message\StatusCodeInterface;
|
use Fig\Http\Message\StatusCodeInterface;
|
||||||
use Laminas\Diactoros\Response\RedirectResponse;
|
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
@@ -13,32 +12,26 @@ use Psr\Log\LoggerInterface;
|
|||||||
use Shlinkio\Shlink\Core\Options;
|
use Shlinkio\Shlink\Core\Options;
|
||||||
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
||||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface;
|
||||||
use function sprintf;
|
|
||||||
|
|
||||||
class RedirectAction extends AbstractTrackingAction implements StatusCodeInterface
|
class RedirectAction extends AbstractTrackingAction implements StatusCodeInterface
|
||||||
{
|
{
|
||||||
private Options\UrlShortenerOptions $urlShortenerOptions;
|
private RedirectResponseHelperInterface $redirectResponseHelper;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ShortUrlResolverInterface $urlResolver,
|
ShortUrlResolverInterface $urlResolver,
|
||||||
VisitsTrackerInterface $visitTracker,
|
VisitsTrackerInterface $visitTracker,
|
||||||
Options\AppOptions $appOptions,
|
Options\AppOptions $appOptions,
|
||||||
Options\UrlShortenerOptions $urlShortenerOptions,
|
RedirectResponseHelperInterface $redirectResponseHelper,
|
||||||
?LoggerInterface $logger = null
|
?LoggerInterface $logger = null
|
||||||
) {
|
) {
|
||||||
parent::__construct($urlResolver, $visitTracker, $appOptions, $logger);
|
parent::__construct($urlResolver, $visitTracker, $appOptions, $logger);
|
||||||
$this->urlShortenerOptions = $urlShortenerOptions;
|
$this->redirectResponseHelper = $redirectResponseHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function createSuccessResp(string $longUrl): Response
|
protected function createSuccessResp(string $longUrl): Response
|
||||||
{
|
{
|
||||||
$statusCode = $this->urlShortenerOptions->redirectStatusCode();
|
return $this->redirectResponseHelper->buildRedirectResponse($longUrl);
|
||||||
$headers = $statusCode === self::STATUS_FOUND ? [] : [
|
|
||||||
'Cache-Control' => sprintf('private,max-age=%s', $this->urlShortenerOptions->redirectCacheLifetime()),
|
|
||||||
];
|
|
||||||
|
|
||||||
return new RedirectResponse($longUrl, $statusCode, $headers);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function createErrorResp(ServerRequestInterface $request, RequestHandlerInterface $handler): Response
|
protected function createErrorResp(ServerRequestInterface $request, RequestHandlerInterface $handler): Response
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class SimplifiedConfigParser
|
|||||||
'anonymize_remote_addr' => ['url_shortener', 'anonymize_remote_addr'],
|
'anonymize_remote_addr' => ['url_shortener', 'anonymize_remote_addr'],
|
||||||
'redirect_status_code' => ['url_shortener', 'redirect_status_code'],
|
'redirect_status_code' => ['url_shortener', 'redirect_status_code'],
|
||||||
'redirect_cache_lifetime' => ['url_shortener', 'redirect_cache_lifetime'],
|
'redirect_cache_lifetime' => ['url_shortener', 'redirect_cache_lifetime'],
|
||||||
|
'port' => ['mezzio-swoole', 'swoole-http-server', 'port'],
|
||||||
];
|
];
|
||||||
private const SIMPLIFIED_CONFIG_SIDE_EFFECTS = [
|
private const SIMPLIFIED_CONFIG_SIDE_EFFECTS = [
|
||||||
'delete_short_url_threshold' => [
|
'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 Doctrine\Common\Collections\Collection;
|
||||||
use Laminas\Diactoros\Uri;
|
use Laminas\Diactoros\Uri;
|
||||||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
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\Exception\ShortCodeCannotBeRegeneratedException;
|
||||||
use Shlinkio\Shlink\Core\Model\ShortUrlEdit;
|
use Shlinkio\Shlink\Core\Model\ShortUrlEdit;
|
||||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
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 count;
|
||||||
use function Functional\contains;
|
|
||||||
use function Functional\invoke;
|
|
||||||
use function Shlinkio\Shlink\Core\generateRandomShortCode;
|
use function Shlinkio\Shlink\Core\generateRandomShortCode;
|
||||||
|
|
||||||
class ShortUrl extends AbstractEntity
|
class ShortUrl extends AbstractEntity
|
||||||
@@ -36,13 +36,17 @@ class ShortUrl extends AbstractEntity
|
|||||||
private ?Domain $domain = null;
|
private ?Domain $domain = null;
|
||||||
private bool $customSlugWasProvided;
|
private bool $customSlugWasProvided;
|
||||||
private int $shortCodeLength;
|
private int $shortCodeLength;
|
||||||
|
private ?string $importSource = null;
|
||||||
|
private ?string $importOriginalShortCode = null;
|
||||||
|
private ?ApiKey $authorApiKey = null;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
string $longUrl,
|
string $longUrl,
|
||||||
?ShortUrlMeta $meta = null,
|
?ShortUrlMeta $meta = null,
|
||||||
?DomainResolverInterface $domainResolver = null
|
?ShortUrlRelationResolverInterface $relationResolver = null
|
||||||
) {
|
) {
|
||||||
$meta = $meta ?? ShortUrlMeta::createEmpty();
|
$meta = $meta ?? ShortUrlMeta::createEmpty();
|
||||||
|
$relationResolver = $relationResolver ?? new SimpleShortUrlRelationResolver();
|
||||||
|
|
||||||
$this->longUrl = $longUrl;
|
$this->longUrl = $longUrl;
|
||||||
$this->dateCreated = Chronos::now();
|
$this->dateCreated = Chronos::now();
|
||||||
@@ -54,7 +58,29 @@ class ShortUrl extends AbstractEntity
|
|||||||
$this->customSlugWasProvided = $meta->hasCustomSlug();
|
$this->customSlugWasProvided = $meta->hasCustomSlug();
|
||||||
$this->shortCodeLength = $meta->getShortCodeLength();
|
$this->shortCodeLength = $meta->getShortCodeLength();
|
||||||
$this->shortCode = $meta->getCustomSlug() ?? generateRandomShortCode($this->shortCodeLength);
|
$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
|
public function getLongUrl(): string
|
||||||
@@ -113,10 +139,10 @@ class ShortUrl extends AbstractEntity
|
|||||||
/**
|
/**
|
||||||
* @throws ShortCodeCannotBeRegeneratedException
|
* @throws ShortCodeCannotBeRegeneratedException
|
||||||
*/
|
*/
|
||||||
public function regenerateShortCode(): self
|
public function regenerateShortCode(): void
|
||||||
{
|
{
|
||||||
// In ShortUrls where a custom slug was provided, do nothing
|
// In ShortUrls where a custom slug was provided, throw error, unless it is an imported one
|
||||||
if ($this->customSlugWasProvided) {
|
if ($this->customSlugWasProvided && $this->importSource === null) {
|
||||||
throw ShortCodeCannotBeRegeneratedException::forShortUrlWithCustomSlug();
|
throw ShortCodeCannotBeRegeneratedException::forShortUrlWithCustomSlug();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +152,6 @@ class ShortUrl extends AbstractEntity
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->shortCode = generateRandomShortCode($this->shortCodeLength);
|
$this->shortCode = generateRandomShortCode($this->shortCodeLength);
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getValidSince(): ?Chronos
|
public function getValidSince(): ?Chronos
|
||||||
@@ -195,27 +220,4 @@ class ShortUrl extends AbstractEntity
|
|||||||
|
|
||||||
return $this->domain->getAuthority();
|
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\Core\ErrorHandler;
|
namespace Shlinkio\Shlink\Core\ErrorHandler;
|
||||||
|
|
||||||
use Laminas\Diactoros\Response;
|
|
||||||
use Mezzio\Router\RouteResult;
|
use Mezzio\Router\RouteResult;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
@@ -12,19 +11,25 @@ use Psr\Http\Message\UriInterface;
|
|||||||
use Psr\Http\Server\MiddlewareInterface;
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
||||||
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
use Shlinkio\Shlink\Core\Options;
|
||||||
|
use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface;
|
||||||
|
|
||||||
use function rtrim;
|
use function rtrim;
|
||||||
|
|
||||||
class NotFoundRedirectHandler implements MiddlewareInterface
|
class NotFoundRedirectHandler implements MiddlewareInterface
|
||||||
{
|
{
|
||||||
private NotFoundRedirectOptions $redirectOptions;
|
private Options\NotFoundRedirectOptions $redirectOptions;
|
||||||
|
private RedirectResponseHelperInterface $redirectResponseHelper;
|
||||||
private string $shlinkBasePath;
|
private string $shlinkBasePath;
|
||||||
|
|
||||||
public function __construct(NotFoundRedirectOptions $redirectOptions, string $shlinkBasePath)
|
public function __construct(
|
||||||
{
|
Options\NotFoundRedirectOptions $redirectOptions,
|
||||||
|
RedirectResponseHelperInterface $redirectResponseHelper,
|
||||||
|
string $shlinkBasePath
|
||||||
|
) {
|
||||||
$this->redirectOptions = $redirectOptions;
|
$this->redirectOptions = $redirectOptions;
|
||||||
$this->shlinkBasePath = $shlinkBasePath;
|
$this->shlinkBasePath = $shlinkBasePath;
|
||||||
|
$this->redirectResponseHelper = $redirectResponseHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
@@ -41,11 +46,13 @@ class NotFoundRedirectHandler implements MiddlewareInterface
|
|||||||
$isBaseUrl = rtrim($uri->getPath(), '/') === $this->shlinkBasePath;
|
$isBaseUrl = rtrim($uri->getPath(), '/') === $this->shlinkBasePath;
|
||||||
|
|
||||||
if ($isBaseUrl && $this->redirectOptions->hasBaseUrlRedirect()) {
|
if ($isBaseUrl && $this->redirectOptions->hasBaseUrlRedirect()) {
|
||||||
return new Response\RedirectResponse($this->redirectOptions->getBaseUrlRedirect());
|
return $this->redirectResponseHelper->buildRedirectResponse($this->redirectOptions->getBaseUrlRedirect());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$isBaseUrl && $routeResult->isFailure() && $this->redirectOptions->hasRegular404Redirect()) {
|
if (!$isBaseUrl && $routeResult->isFailure() && $this->redirectOptions->hasRegular404Redirect()) {
|
||||||
return new Response\RedirectResponse($this->redirectOptions->getRegular404Redirect());
|
return $this->redirectResponseHelper->buildRedirectResponse(
|
||||||
|
$this->redirectOptions->getRegular404Redirect(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -53,7 +60,9 @@ class NotFoundRedirectHandler implements MiddlewareInterface
|
|||||||
$routeResult->getMatchedRouteName() === RedirectAction::class &&
|
$routeResult->getMatchedRouteName() === RedirectAction::class &&
|
||||||
$this->redirectOptions->hasInvalidShortUrlRedirect()
|
$this->redirectOptions->hasInvalidShortUrlRedirect()
|
||||||
) {
|
) {
|
||||||
return new Response\RedirectResponse($this->redirectOptions->getInvalidShortUrlRedirect());
|
return $this->redirectResponseHelper->buildRedirectResponse(
|
||||||
|
$this->redirectOptions->getInvalidShortUrlRedirect(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
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 Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter;
|
||||||
|
|
||||||
use function array_key_exists;
|
use function array_key_exists;
|
||||||
|
use function Shlinkio\Shlink\Core\getOptionalBoolFromInputFilter;
|
||||||
|
use function Shlinkio\Shlink\Core\getOptionalIntFromInputFilter;
|
||||||
use function Shlinkio\Shlink\Core\parseDateField;
|
use function Shlinkio\Shlink\Core\parseDateField;
|
||||||
|
|
||||||
final class ShortUrlEdit
|
final class ShortUrlEdit
|
||||||
@@ -21,6 +23,7 @@ final class ShortUrlEdit
|
|||||||
private ?Chronos $validUntil = null;
|
private ?Chronos $validUntil = null;
|
||||||
private bool $maxVisitsPropWasProvided = false;
|
private bool $maxVisitsPropWasProvided = false;
|
||||||
private ?int $maxVisits = null;
|
private ?int $maxVisits = null;
|
||||||
|
private ?bool $validateUrl = null;
|
||||||
|
|
||||||
// Enforce named constructors
|
// Enforce named constructors
|
||||||
private function __construct()
|
private function __construct()
|
||||||
@@ -55,13 +58,8 @@ final class ShortUrlEdit
|
|||||||
$this->longUrl = $inputFilter->getValue(ShortUrlMetaInputFilter::LONG_URL);
|
$this->longUrl = $inputFilter->getValue(ShortUrlMetaInputFilter::LONG_URL);
|
||||||
$this->validSince = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_SINCE));
|
$this->validSince = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_SINCE));
|
||||||
$this->validUntil = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_UNTIL));
|
$this->validUntil = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_UNTIL));
|
||||||
$this->maxVisits = $this->getOptionalIntFromInputFilter($inputFilter, ShortUrlMetaInputFilter::MAX_VISITS);
|
$this->maxVisits = getOptionalIntFromInputFilter($inputFilter, ShortUrlMetaInputFilter::MAX_VISITS);
|
||||||
}
|
$this->validateUrl = getOptionalBoolFromInputFilter($inputFilter, ShortUrlMetaInputFilter::VALIDATE_URL);
|
||||||
|
|
||||||
private function getOptionalIntFromInputFilter(ShortUrlMetaInputFilter $inputFilter, string $fieldName): ?int
|
|
||||||
{
|
|
||||||
$value = $inputFilter->getValue($fieldName);
|
|
||||||
return $value !== null ? (int) $value : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function longUrl(): ?string
|
public function longUrl(): ?string
|
||||||
@@ -103,4 +101,9 @@ final class ShortUrlEdit
|
|||||||
{
|
{
|
||||||
return $this->maxVisitsPropWasProvided;
|
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\Exception\ValidationException;
|
||||||
use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter;
|
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 function Shlinkio\Shlink\Core\parseDateField;
|
||||||
|
|
||||||
use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH;
|
use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH;
|
||||||
@@ -21,6 +23,8 @@ final class ShortUrlMeta
|
|||||||
private ?bool $findIfExists = null;
|
private ?bool $findIfExists = null;
|
||||||
private ?string $domain = null;
|
private ?string $domain = null;
|
||||||
private int $shortCodeLength = 5;
|
private int $shortCodeLength = 5;
|
||||||
|
private ?bool $validateUrl = null;
|
||||||
|
private ?string $apiKey = null;
|
||||||
|
|
||||||
// Enforce named constructors
|
// Enforce named constructors
|
||||||
private function __construct()
|
private function __construct()
|
||||||
@@ -55,19 +59,15 @@ final class ShortUrlMeta
|
|||||||
$this->validSince = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_SINCE));
|
$this->validSince = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_SINCE));
|
||||||
$this->validUntil = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_UNTIL));
|
$this->validUntil = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_UNTIL));
|
||||||
$this->customSlug = $inputFilter->getValue(ShortUrlMetaInputFilter::CUSTOM_SLUG);
|
$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->findIfExists = $inputFilter->getValue(ShortUrlMetaInputFilter::FIND_IF_EXISTS);
|
||||||
|
$this->validateUrl = getOptionalBoolFromInputFilter($inputFilter, ShortUrlMetaInputFilter::VALIDATE_URL);
|
||||||
$this->domain = $inputFilter->getValue(ShortUrlMetaInputFilter::DOMAIN);
|
$this->domain = $inputFilter->getValue(ShortUrlMetaInputFilter::DOMAIN);
|
||||||
$this->shortCodeLength = $this->getOptionalIntFromInputFilter(
|
$this->shortCodeLength = getOptionalIntFromInputFilter(
|
||||||
$inputFilter,
|
$inputFilter,
|
||||||
ShortUrlMetaInputFilter::SHORT_CODE_LENGTH,
|
ShortUrlMetaInputFilter::SHORT_CODE_LENGTH,
|
||||||
) ?? DEFAULT_SHORT_CODES_LENGTH;
|
) ?? DEFAULT_SHORT_CODES_LENGTH;
|
||||||
}
|
$this->apiKey = $inputFilter->getValue(ShortUrlMetaInputFilter::API_KEY);
|
||||||
|
|
||||||
private function getOptionalIntFromInputFilter(ShortUrlMetaInputFilter $inputFilter, string $fieldName): ?int
|
|
||||||
{
|
|
||||||
$value = $inputFilter->getValue($fieldName);
|
|
||||||
return $value !== null ? (int) $value : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getValidSince(): ?Chronos
|
public function getValidSince(): ?Chronos
|
||||||
@@ -129,4 +129,14 @@ final class ShortUrlMeta
|
|||||||
{
|
{
|
||||||
return $this->shortCodeLength;
|
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 Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||||
|
|
||||||
|
use function explode;
|
||||||
use function is_array;
|
use function is_array;
|
||||||
use function is_string;
|
use function is_string;
|
||||||
use function key;
|
use function key;
|
||||||
@@ -40,15 +41,22 @@ final class ShortUrlsOrdering
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME Providing the ordering as array is considered deprecated. To be removed in v3.0.0
|
||||||
$isArray = is_array($orderBy);
|
$isArray = is_array($orderBy);
|
||||||
if (! $isArray && $orderBy !== null && ! is_string($orderBy)) {
|
if (! $isArray && ! is_string($orderBy)) {
|
||||||
throw ValidationException::fromArray([
|
throw ValidationException::fromArray([
|
||||||
'orderBy' => '"Order by" must be an array, string or null',
|
'orderBy' => '"Order by" must be an array, string or null',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->orderField = $isArray ? key($orderBy) : $orderBy;
|
if (! $isArray) {
|
||||||
$this->orderDirection = $isArray ? $orderBy[$this->orderField] : self::DEFAULT_ORDER_DIRECTION;
|
$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
|
public function orderField(): ?string
|
||||||
|
|||||||
@@ -5,13 +5,17 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink\Core\Repository;
|
namespace Shlinkio\Shlink\Core\Repository;
|
||||||
|
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use Doctrine\ORM\Query\Expr\Join;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
|
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||||
use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering;
|
use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering;
|
||||||
|
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
|
||||||
|
|
||||||
use function array_column;
|
use function array_column;
|
||||||
use function array_key_exists;
|
use function array_key_exists;
|
||||||
|
use function count;
|
||||||
use function Functional\contains;
|
use function Functional\contains;
|
||||||
|
|
||||||
class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryInterface
|
class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryInterface
|
||||||
@@ -186,6 +190,85 @@ DQL;
|
|||||||
->setParameter('slug', $slug)
|
->setParameter('slug', $slug)
|
||||||
->setMaxResults(1);
|
->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) {
|
if ($domain !== null) {
|
||||||
$qb->join('s.domain', 'd')
|
$qb->join('s.domain', 'd')
|
||||||
->andWhere($qb->expr()->eq('d.authority', ':authority'))
|
->andWhere($qb->expr()->eq('d.authority', ':authority'))
|
||||||
@@ -193,7 +276,5 @@ DQL;
|
|||||||
} else {
|
} else {
|
||||||
$qb->andWhere($qb->expr()->isNull('s.domain'));
|
$qb->andWhere($qb->expr()->isNull('s.domain'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $qb;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ namespace Shlinkio\Shlink\Core\Repository;
|
|||||||
use Doctrine\Persistence\ObjectRepository;
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
|
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||||
use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering;
|
use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering;
|
||||||
|
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
|
||||||
|
|
||||||
interface ShortUrlRepositoryInterface extends ObjectRepository
|
interface ShortUrlRepositoryInterface extends ObjectRepository
|
||||||
{
|
{
|
||||||
@@ -27,4 +29,8 @@ interface ShortUrlRepositoryInterface extends ObjectRepository
|
|||||||
public function findOne(string $shortCode, ?string $domain = null): ?ShortUrl;
|
public function findOne(string $shortCode, ?string $domain = null): ?ShortUrl;
|
||||||
|
|
||||||
public function shortCodeIsInUse(string $slug, ?string $domain): bool;
|
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
|
public function updateMetadataByShortCode(ShortUrlIdentifier $identifier, ShortUrlEdit $shortUrlEdit): ShortUrl
|
||||||
{
|
{
|
||||||
if ($shortUrlEdit->hasLongUrl()) {
|
if ($shortUrlEdit->hasLongUrl()) {
|
||||||
$this->urlValidator->validateUrl($shortUrlEdit->longUrl());
|
$this->urlValidator->validateUrl($shortUrlEdit->longUrl(), $shortUrlEdit->doValidateUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
$shortUrl = $this->urlResolver->resolveShortUrl($identifier);
|
$shortUrl = $this->urlResolver->resolveShortUrl($identifier);
|
||||||
|
|||||||
@@ -5,34 +5,36 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink\Core\Service;
|
namespace Shlinkio\Shlink\Core\Service;
|
||||||
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Shlinkio\Shlink\Core\Domain\Resolver\DomainResolverInterface;
|
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
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\TagManagerTrait;
|
||||||
use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
|
use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
use function array_reduce;
|
|
||||||
|
|
||||||
class UrlShortener implements UrlShortenerInterface
|
class UrlShortener implements UrlShortenerInterface
|
||||||
{
|
{
|
||||||
use TagManagerTrait;
|
use TagManagerTrait;
|
||||||
|
|
||||||
private EntityManagerInterface $em;
|
private EntityManagerInterface $em;
|
||||||
private UrlValidatorInterface $urlValidator;
|
private UrlValidatorInterface $urlValidator;
|
||||||
private DomainResolverInterface $domainResolver;
|
private ShortUrlRelationResolverInterface $relationResolver;
|
||||||
|
private ShortCodeHelperInterface $shortCodeHelper;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
UrlValidatorInterface $urlValidator,
|
UrlValidatorInterface $urlValidator,
|
||||||
EntityManagerInterface $em,
|
EntityManagerInterface $em,
|
||||||
DomainResolverInterface $domainResolver
|
ShortUrlRelationResolverInterface $relationResolver,
|
||||||
|
ShortCodeHelperInterface $shortCodeHelper
|
||||||
) {
|
) {
|
||||||
$this->urlValidator = $urlValidator;
|
$this->urlValidator = $urlValidator;
|
||||||
$this->em = $em;
|
$this->em = $em;
|
||||||
$this->domainResolver = $domainResolver;
|
$this->relationResolver = $relationResolver;
|
||||||
|
$this->shortCodeHelper = $shortCodeHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,7 +43,7 @@ class UrlShortener implements UrlShortenerInterface
|
|||||||
* @throws InvalidUrlException
|
* @throws InvalidUrlException
|
||||||
* @throws Throwable
|
* @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
|
// First, check if a short URL exists for all provided params
|
||||||
$existingShortUrl = $this->findExistingShortUrlIfExists($url, $tags, $meta);
|
$existingShortUrl = $this->findExistingShortUrlIfExists($url, $tags, $meta);
|
||||||
@@ -49,26 +51,17 @@ class UrlShortener implements UrlShortenerInterface
|
|||||||
return $existingShortUrl;
|
return $existingShortUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->urlValidator->validateUrl($url);
|
$this->urlValidator->validateUrl($url, $meta->doValidateUrl());
|
||||||
$this->em->beginTransaction();
|
|
||||||
$shortUrl = new ShortUrl($url, $meta, $this->domainResolver);
|
return $this->em->transactional(function () use ($url, $tags, $meta) {
|
||||||
$shortUrl->setTags($this->tagNamesToEntities($this->em, $tags));
|
$shortUrl = new ShortUrl($url, $meta, $this->relationResolver);
|
||||||
|
$shortUrl->setTags($this->tagNamesToEntities($this->em, $tags));
|
||||||
|
|
||||||
try {
|
|
||||||
$this->verifyShortCodeUniqueness($meta, $shortUrl);
|
$this->verifyShortCodeUniqueness($meta, $shortUrl);
|
||||||
$this->em->persist($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
|
private function findExistingShortUrlIfExists(string $url, array $tags, ShortUrlMeta $meta): ?ShortUrl
|
||||||
@@ -77,42 +70,23 @@ class UrlShortener implements UrlShortenerInterface
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$criteria = ['longUrl' => $url];
|
/** @var ShortUrlRepositoryInterface $repo */
|
||||||
if ($meta->hasCustomSlug()) {
|
$repo = $this->em->getRepository(ShortUrl::class);
|
||||||
$criteria['shortCode'] = $meta->getCustomSlug();
|
return $repo->findOneMatching($url, $tags, $meta);
|
||||||
}
|
|
||||||
/** @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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function verifyShortCodeUniqueness(ShortUrlMeta $meta, ShortUrl $shortUrlToBeCreated): void
|
private function verifyShortCodeUniqueness(ShortUrlMeta $meta, ShortUrl $shortUrlToBeCreated): void
|
||||||
{
|
{
|
||||||
$shortCode = $shortUrlToBeCreated->getShortCode();
|
$couldBeMadeUnique = $this->shortCodeHelper->ensureShortCodeUniqueness(
|
||||||
$domain = $meta->getDomain();
|
$shortUrlToBeCreated,
|
||||||
|
$meta->hasCustomSlug(),
|
||||||
|
);
|
||||||
|
|
||||||
/** @var ShortUrlRepository $repo */
|
if (! $couldBeMadeUnique) {
|
||||||
$repo = $this->em->getRepository(ShortUrl::class);
|
$domain = $shortUrlToBeCreated->getDomain();
|
||||||
$otherShortUrlsExist = $repo->shortCodeIsInUse($shortCode, $domain);
|
$domainAuthority = $domain !== null ? $domain->getAuthority() : null;
|
||||||
|
|
||||||
if ($otherShortUrlsExist && $meta->hasCustomSlug()) {
|
throw NonUniqueSlugException::fromSlug($shortUrlToBeCreated->getShortCode(), $domainAuthority);
|
||||||
throw NonUniqueSlugException::fromSlug($shortCode, $domain);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($otherShortUrlsExist) {
|
|
||||||
$shortUrlToBeCreated->regenerateShortCode();
|
|
||||||
$this->verifyShortCodeUniqueness($meta, $shortUrlToBeCreated);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,5 +16,5 @@ interface UrlShortenerInterface
|
|||||||
* @throws NonUniqueSlugException
|
* @throws NonUniqueSlugException
|
||||||
* @throws InvalidUrlException
|
* @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);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\Core\Domain\Resolver;
|
namespace Shlinkio\Shlink\Core\ShortUrl\Resolver;
|
||||||
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||||
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
|
||||||
class PersistenceDomainResolver implements DomainResolverInterface
|
class PersistenceShortUrlRelationResolver implements ShortUrlRelationResolverInterface
|
||||||
{
|
{
|
||||||
private EntityManagerInterface $em;
|
private EntityManagerInterface $em;
|
||||||
|
|
||||||
@@ -26,4 +27,15 @@ class PersistenceDomainResolver implements DomainResolverInterface
|
|||||||
$existingDomain = $this->em->getRepository(Domain::class)->findOneBy(['authority' => $domain]);
|
$existingDomain = $this->em->getRepository(Domain::class)->findOneBy(['authority' => $domain]);
|
||||||
return $existingDomain ?? new Domain($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
|
* Provided a list of tag names, creates all that do not exist yet
|
||||||
*
|
*
|
||||||
|
* @deprecated
|
||||||
* @param string[] $tagNames
|
* @param string[] $tagNames
|
||||||
* @return Collection|Tag[]
|
* @return Collection|Tag[]
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ interface TagServiceInterface
|
|||||||
public function deleteTags(array $tagNames): void;
|
public function deleteTags(array $tagNames): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @deprecated
|
||||||
* @param string[] $tagNames
|
* @param string[] $tagNames
|
||||||
* @return Collection|Tag[]
|
* @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;
|
||||||
|
}
|
||||||
32
module/Core/src/Util/RedirectResponseHelper.php
Normal file
32
module/Core/src/Util/RedirectResponseHelper.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\Util;
|
||||||
|
|
||||||
|
use Fig\Http\Message\StatusCodeInterface;
|
||||||
|
use Laminas\Diactoros\Response\RedirectResponse;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||||
|
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
|
class RedirectResponseHelper implements RedirectResponseHelperInterface
|
||||||
|
{
|
||||||
|
private UrlShortenerOptions $options;
|
||||||
|
|
||||||
|
public function __construct(UrlShortenerOptions $options)
|
||||||
|
{
|
||||||
|
$this->options = $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildRedirectResponse(string $location): ResponseInterface
|
||||||
|
{
|
||||||
|
$statusCode = $this->options->redirectStatusCode();
|
||||||
|
$headers = $statusCode === StatusCodeInterface::STATUS_FOUND ? [] : [
|
||||||
|
'Cache-Control' => sprintf('private,max-age=%s', $this->options->redirectCacheLifetime()),
|
||||||
|
];
|
||||||
|
|
||||||
|
return new RedirectResponse($location, $statusCode, $headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
module/Core/src/Util/RedirectResponseHelperInterface.php
Normal file
12
module/Core/src/Util/RedirectResponseHelperInterface.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\Util;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
|
||||||
|
interface RedirectResponseHelperInterface
|
||||||
|
{
|
||||||
|
public function buildRedirectResponse(string $location): ResponseInterface;
|
||||||
|
}
|
||||||
@@ -27,10 +27,11 @@ class UrlValidator implements UrlValidatorInterface, RequestMethodInterface
|
|||||||
/**
|
/**
|
||||||
* @throws InvalidUrlException
|
* @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 the URL validation is not enabled or it was explicitly set to not validate, skip check
|
||||||
if (! $this->options->isUrlValidationEnabled()) {
|
$doValidate = $doValidate ?? $this->options->isUrlValidationEnabled();
|
||||||
|
if (! $doValidate) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ interface UrlValidatorInterface
|
|||||||
/**
|
/**
|
||||||
* @throws InvalidUrlException
|
* @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 DOMAIN = 'domain';
|
||||||
public const SHORT_CODE_LENGTH = 'shortCodeLength';
|
public const SHORT_CODE_LENGTH = 'shortCodeLength';
|
||||||
public const LONG_URL = 'longUrl';
|
public const LONG_URL = 'longUrl';
|
||||||
|
public const VALIDATE_URL = 'validateUrl';
|
||||||
|
public const API_KEY = 'apiKey';
|
||||||
|
|
||||||
public function __construct(array $data)
|
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->createBooleanInput(self::FIND_IF_EXISTS, false));
|
||||||
|
|
||||||
|
$this->add($this->createInput(self::VALIDATE_URL, false));
|
||||||
|
|
||||||
$domain = $this->createInput(self::DOMAIN, false);
|
$domain = $this->createInput(self::DOMAIN, false);
|
||||||
$domain->getValidatorChain()->attach(new Validation\HostAndPortValidator());
|
$domain->getValidatorChain()->attach(new Validation\HostAndPortValidator());
|
||||||
$this->add($domain);
|
$this->add($domain);
|
||||||
|
|
||||||
|
$this->add($this->createInput(self::API_KEY, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createPositiveNumberInput(string $name, int $min = 1): Input
|
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\ShortUrlsOrdering;
|
||||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
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 Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase;
|
||||||
|
|
||||||
use function count;
|
use function count;
|
||||||
|
|
||||||
class ShortUrlRepositoryTest extends DatabaseTestCase
|
class ShortUrlRepositoryTest extends DatabaseTestCase
|
||||||
{
|
{
|
||||||
|
use TagManagerTrait;
|
||||||
|
|
||||||
protected const ENTITIES_TO_EMPTY = [
|
protected const ENTITIES_TO_EMPTY = [
|
||||||
Tag::class,
|
Tag::class,
|
||||||
Visit::class,
|
Visit::class,
|
||||||
@@ -54,25 +58,25 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||||||
|
|
||||||
$this->getEntityManager()->flush();
|
$this->getEntityManager()->flush();
|
||||||
|
|
||||||
$this->assertSame($regularOne, $this->repo->findOneWithDomainFallback($regularOne->getShortCode()));
|
self::assertSame($regularOne, $this->repo->findOneWithDomainFallback($regularOne->getShortCode()));
|
||||||
$this->assertSame($regularOne, $this->repo->findOneWithDomainFallback(
|
self::assertSame($regularOne, $this->repo->findOneWithDomainFallback(
|
||||||
$withDomainDuplicatingRegular->getShortCode(),
|
$withDomainDuplicatingRegular->getShortCode(),
|
||||||
));
|
));
|
||||||
$this->assertSame($withDomain, $this->repo->findOneWithDomainFallback(
|
self::assertSame($withDomain, $this->repo->findOneWithDomainFallback(
|
||||||
$withDomain->getShortCode(),
|
$withDomain->getShortCode(),
|
||||||
'example.com',
|
'example.com',
|
||||||
));
|
));
|
||||||
$this->assertSame(
|
self::assertSame(
|
||||||
$withDomainDuplicatingRegular,
|
$withDomainDuplicatingRegular,
|
||||||
$this->repo->findOneWithDomainFallback($withDomainDuplicatingRegular->getShortCode(), 'doma.in'),
|
$this->repo->findOneWithDomainFallback($withDomainDuplicatingRegular->getShortCode(), 'doma.in'),
|
||||||
);
|
);
|
||||||
$this->assertSame(
|
self::assertSame(
|
||||||
$regularOne,
|
$regularOne,
|
||||||
$this->repo->findOneWithDomainFallback($withDomainDuplicatingRegular->getShortCode(), 'other-domain.com'),
|
$this->repo->findOneWithDomainFallback($withDomainDuplicatingRegular->getShortCode(), 'other-domain.com'),
|
||||||
);
|
);
|
||||||
$this->assertNull($this->repo->findOneWithDomainFallback('invalid'));
|
self::assertNull($this->repo->findOneWithDomainFallback('invalid'));
|
||||||
$this->assertNull($this->repo->findOneWithDomainFallback($withDomain->getShortCode()));
|
self::assertNull($this->repo->findOneWithDomainFallback($withDomain->getShortCode()));
|
||||||
$this->assertNull($this->repo->findOneWithDomainFallback($withDomain->getShortCode(), 'other-domain.com'));
|
self::assertNull($this->repo->findOneWithDomainFallback($withDomain->getShortCode(), 'other-domain.com'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
@@ -84,7 +88,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||||||
}
|
}
|
||||||
$this->getEntityManager()->flush();
|
$this->getEntityManager()->flush();
|
||||||
|
|
||||||
$this->assertEquals($count, $this->repo->countList());
|
self::assertEquals($count, $this->repo->countList());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
@@ -113,37 +117,37 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||||||
$this->getEntityManager()->flush();
|
$this->getEntityManager()->flush();
|
||||||
|
|
||||||
$result = $this->repo->findList(null, null, 'foo', ['bar']);
|
$result = $this->repo->findList(null, null, 'foo', ['bar']);
|
||||||
$this->assertCount(1, $result);
|
self::assertCount(1, $result);
|
||||||
$this->assertEquals(1, $this->repo->countList('foo', ['bar']));
|
self::assertEquals(1, $this->repo->countList('foo', ['bar']));
|
||||||
$this->assertSame($foo, $result[0]);
|
self::assertSame($foo, $result[0]);
|
||||||
|
|
||||||
$result = $this->repo->findList();
|
$result = $this->repo->findList();
|
||||||
$this->assertCount(3, $result);
|
self::assertCount(3, $result);
|
||||||
|
|
||||||
$result = $this->repo->findList(2);
|
$result = $this->repo->findList(2);
|
||||||
$this->assertCount(2, $result);
|
self::assertCount(2, $result);
|
||||||
|
|
||||||
$result = $this->repo->findList(2, 1);
|
$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([
|
$result = $this->repo->findList(null, null, null, [], ShortUrlsOrdering::fromRawData([
|
||||||
'orderBy' => ['visits' => 'DESC'],
|
'orderBy' => ['visits' => 'DESC'],
|
||||||
]));
|
]));
|
||||||
$this->assertCount(3, $result);
|
self::assertCount(3, $result);
|
||||||
$this->assertSame($bar, $result[0]);
|
self::assertSame($bar, $result[0]);
|
||||||
|
|
||||||
$result = $this->repo->findList(null, null, null, [], null, new DateRange(null, Chronos::now()->subDays(2)));
|
$result = $this->repo->findList(null, null, null, [], null, new DateRange(null, Chronos::now()->subDays(2)));
|
||||||
$this->assertCount(1, $result);
|
self::assertCount(1, $result);
|
||||||
$this->assertEquals(1, $this->repo->countList(null, [], new DateRange(null, Chronos::now()->subDays(2))));
|
self::assertEquals(1, $this->repo->countList(null, [], new DateRange(null, Chronos::now()->subDays(2))));
|
||||||
$this->assertSame($foo2, $result[0]);
|
self::assertSame($foo2, $result[0]);
|
||||||
|
|
||||||
$this->assertCount(
|
self::assertCount(
|
||||||
2,
|
2,
|
||||||
$this->repo->findList(null, null, null, [], null, new DateRange(Chronos::now()->subDays(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 */
|
/** @test */
|
||||||
@@ -160,11 +164,11 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||||||
'orderBy' => ['longUrl' => 'ASC'],
|
'orderBy' => ['longUrl' => 'ASC'],
|
||||||
]));
|
]));
|
||||||
|
|
||||||
$this->assertCount(count($urls), $result);
|
self::assertCount(count($urls), $result);
|
||||||
$this->assertEquals('a', $result[0]->getLongUrl());
|
self::assertEquals('a', $result[0]->getLongUrl());
|
||||||
$this->assertEquals('b', $result[1]->getLongUrl());
|
self::assertEquals('b', $result[1]->getLongUrl());
|
||||||
$this->assertEquals('c', $result[2]->getLongUrl());
|
self::assertEquals('c', $result[2]->getLongUrl());
|
||||||
$this->assertEquals('z', $result[3]->getLongUrl());
|
self::assertEquals('z', $result[3]->getLongUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
@@ -181,12 +185,12 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||||||
|
|
||||||
$this->getEntityManager()->flush();
|
$this->getEntityManager()->flush();
|
||||||
|
|
||||||
$this->assertTrue($this->repo->shortCodeIsInUse('my-cool-slug'));
|
self::assertTrue($this->repo->shortCodeIsInUse('my-cool-slug'));
|
||||||
$this->assertFalse($this->repo->shortCodeIsInUse('my-cool-slug', 'doma.in'));
|
self::assertFalse($this->repo->shortCodeIsInUse('my-cool-slug', 'doma.in'));
|
||||||
$this->assertFalse($this->repo->shortCodeIsInUse('slug-not-in-use'));
|
self::assertFalse($this->repo->shortCodeIsInUse('slug-not-in-use'));
|
||||||
$this->assertFalse($this->repo->shortCodeIsInUse('another-slug'));
|
self::assertFalse($this->repo->shortCodeIsInUse('another-slug'));
|
||||||
$this->assertFalse($this->repo->shortCodeIsInUse('another-slug', 'example.com'));
|
self::assertFalse($this->repo->shortCodeIsInUse('another-slug', 'example.com'));
|
||||||
$this->assertTrue($this->repo->shortCodeIsInUse('another-slug', 'doma.in'));
|
self::assertTrue($this->repo->shortCodeIsInUse('another-slug', 'doma.in'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
@@ -203,11 +207,140 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||||||
|
|
||||||
$this->getEntityManager()->flush();
|
$this->getEntityManager()->flush();
|
||||||
|
|
||||||
$this->assertNotNull($this->repo->findOne('my-cool-slug'));
|
self::assertNotNull($this->repo->findOne('my-cool-slug'));
|
||||||
$this->assertNull($this->repo->findOne('my-cool-slug', 'doma.in'));
|
self::assertNull($this->repo->findOne('my-cool-slug', 'doma.in'));
|
||||||
$this->assertNull($this->repo->findOne('slug-not-in-use'));
|
self::assertNull($this->repo->findOne('slug-not-in-use'));
|
||||||
$this->assertNull($this->repo->findOne('another-slug'));
|
self::assertNull($this->repo->findOne('another-slug'));
|
||||||
$this->assertNull($this->repo->findOne('another-slug', 'example.com'));
|
self::assertNull($this->repo->findOne('another-slug', 'example.com'));
|
||||||
$this->assertNotNull($this->repo->findOne('another-slug', 'doma.in'));
|
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 */
|
/** @test */
|
||||||
public function deleteByNameDoesNothingWhenEmptyListIsProvided(): void
|
public function deleteByNameDoesNothingWhenEmptyListIsProvided(): void
|
||||||
{
|
{
|
||||||
$this->assertEquals(0, $this->repo->deleteByName([]));
|
self::assertEquals(0, $this->repo->deleteByName([]));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
@@ -46,7 +46,7 @@ class TagRepositoryTest extends DatabaseTestCase
|
|||||||
}
|
}
|
||||||
$this->getEntityManager()->flush();
|
$this->getEntityManager()->flush();
|
||||||
|
|
||||||
$this->assertEquals(2, $this->repo->deleteByName($toDelete));
|
self::assertEquals(2, $this->repo->deleteByName($toDelete));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
@@ -79,20 +79,20 @@ class TagRepositoryTest extends DatabaseTestCase
|
|||||||
|
|
||||||
$result = $this->repo->findTagsWithInfo();
|
$result = $this->repo->findTagsWithInfo();
|
||||||
|
|
||||||
$this->assertCount(4, $result);
|
self::assertCount(4, $result);
|
||||||
$this->assertEquals(
|
self::assertEquals(
|
||||||
['tag' => $tags[3], 'shortUrlsCount' => 0, 'visitsCount' => 0],
|
['tag' => $tags[3], 'shortUrlsCount' => 0, 'visitsCount' => 0],
|
||||||
$result[0]->jsonSerialize(),
|
$result[0]->jsonSerialize(),
|
||||||
);
|
);
|
||||||
$this->assertEquals(
|
self::assertEquals(
|
||||||
['tag' => $tags[1], 'shortUrlsCount' => 1, 'visitsCount' => 3],
|
['tag' => $tags[1], 'shortUrlsCount' => 1, 'visitsCount' => 3],
|
||||||
$result[1]->jsonSerialize(),
|
$result[1]->jsonSerialize(),
|
||||||
);
|
);
|
||||||
$this->assertEquals(
|
self::assertEquals(
|
||||||
['tag' => $tags[2], 'shortUrlsCount' => 1, 'visitsCount' => 3],
|
['tag' => $tags[2], 'shortUrlsCount' => 1, 'visitsCount' => 3],
|
||||||
$result[2]->jsonSerialize(),
|
$result[2]->jsonSerialize(),
|
||||||
);
|
);
|
||||||
$this->assertEquals(
|
self::assertEquals(
|
||||||
['tag' => $tags[0], 'shortUrlsCount' => 2, 'visitsCount' => 4],
|
['tag' => $tags[0], 'shortUrlsCount' => 2, 'visitsCount' => 4],
|
||||||
$result[3]->jsonSerialize(),
|
$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
|
// Important! assertCount will not work here, as this iterable object loads data dynamically and the count
|
||||||
// is 0 if not iterated
|
// is 0 if not iterated
|
||||||
$this->assertEquals(2, $countIterable($unlocated));
|
self::assertEquals(2, $countIterable($unlocated));
|
||||||
$this->assertEquals(4, $countIterable($withEmptyLocation));
|
self::assertEquals(4, $countIterable($withEmptyLocation));
|
||||||
$this->assertEquals(6, $countIterable($all));
|
self::assertEquals(6, $countIterable($all));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideBlockSize(): iterable
|
public function provideBlockSize(): iterable
|
||||||
@@ -90,22 +90,22 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||||||
{
|
{
|
||||||
[$shortCode, $domain] = $this->createShortUrlsAndVisits();
|
[$shortCode, $domain] = $this->createShortUrlsAndVisits();
|
||||||
|
|
||||||
$this->assertCount(0, $this->repo->findVisitsByShortCode('invalid'));
|
self::assertCount(0, $this->repo->findVisitsByShortCode('invalid'));
|
||||||
$this->assertCount(6, $this->repo->findVisitsByShortCode($shortCode));
|
self::assertCount(6, $this->repo->findVisitsByShortCode($shortCode));
|
||||||
$this->assertCount(3, $this->repo->findVisitsByShortCode($shortCode, $domain));
|
self::assertCount(3, $this->repo->findVisitsByShortCode($shortCode, $domain));
|
||||||
$this->assertCount(2, $this->repo->findVisitsByShortCode($shortCode, null, new DateRange(
|
self::assertCount(2, $this->repo->findVisitsByShortCode($shortCode, null, new DateRange(
|
||||||
Chronos::parse('2016-01-02'),
|
Chronos::parse('2016-01-02'),
|
||||||
Chronos::parse('2016-01-03'),
|
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'),
|
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'),
|
Chronos::parse('2016-01-03'),
|
||||||
)));
|
)));
|
||||||
$this->assertCount(3, $this->repo->findVisitsByShortCode($shortCode, null, null, 3, 2));
|
self::assertCount(3, $this->repo->findVisitsByShortCode($shortCode, null, null, 3, 2));
|
||||||
$this->assertCount(2, $this->repo->findVisitsByShortCode($shortCode, null, null, 5, 4));
|
self::assertCount(2, $this->repo->findVisitsByShortCode($shortCode, null, null, 5, 4));
|
||||||
$this->assertCount(1, $this->repo->findVisitsByShortCode($shortCode, $domain, null, 3, 2));
|
self::assertCount(1, $this->repo->findVisitsByShortCode($shortCode, $domain, null, 3, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
@@ -113,17 +113,17 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||||||
{
|
{
|
||||||
[$shortCode, $domain] = $this->createShortUrlsAndVisits();
|
[$shortCode, $domain] = $this->createShortUrlsAndVisits();
|
||||||
|
|
||||||
$this->assertEquals(0, $this->repo->countVisitsByShortCode('invalid'));
|
self::assertEquals(0, $this->repo->countVisitsByShortCode('invalid'));
|
||||||
$this->assertEquals(6, $this->repo->countVisitsByShortCode($shortCode));
|
self::assertEquals(6, $this->repo->countVisitsByShortCode($shortCode));
|
||||||
$this->assertEquals(3, $this->repo->countVisitsByShortCode($shortCode, $domain));
|
self::assertEquals(3, $this->repo->countVisitsByShortCode($shortCode, $domain));
|
||||||
$this->assertEquals(2, $this->repo->countVisitsByShortCode($shortCode, null, new DateRange(
|
self::assertEquals(2, $this->repo->countVisitsByShortCode($shortCode, null, new DateRange(
|
||||||
Chronos::parse('2016-01-02'),
|
Chronos::parse('2016-01-02'),
|
||||||
Chronos::parse('2016-01-03'),
|
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'),
|
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'),
|
Chronos::parse('2016-01-03'),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
@@ -147,13 +147,13 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||||||
|
|
||||||
$this->getEntityManager()->flush();
|
$this->getEntityManager()->flush();
|
||||||
|
|
||||||
$this->assertCount(0, $this->repo->findVisitsByTag('invalid'));
|
self::assertCount(0, $this->repo->findVisitsByTag('invalid'));
|
||||||
$this->assertCount(18, $this->repo->findVisitsByTag((string) $foo));
|
self::assertCount(18, $this->repo->findVisitsByTag((string) $foo));
|
||||||
$this->assertCount(6, $this->repo->findVisitsByTag((string) $foo, new DateRange(
|
self::assertCount(6, $this->repo->findVisitsByTag((string) $foo, new DateRange(
|
||||||
Chronos::parse('2016-01-02'),
|
Chronos::parse('2016-01-02'),
|
||||||
Chronos::parse('2016-01-03'),
|
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'),
|
Chronos::parse('2016-01-03'),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
@@ -174,13 +174,13 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||||||
|
|
||||||
$this->getEntityManager()->flush();
|
$this->getEntityManager()->flush();
|
||||||
|
|
||||||
$this->assertEquals(0, $this->repo->countVisitsByTag('invalid'));
|
self::assertEquals(0, $this->repo->countVisitsByTag('invalid'));
|
||||||
$this->assertEquals(12, $this->repo->countVisitsByTag((string) $foo));
|
self::assertEquals(12, $this->repo->countVisitsByTag((string) $foo));
|
||||||
$this->assertEquals(4, $this->repo->countVisitsByTag((string) $foo, new DateRange(
|
self::assertEquals(4, $this->repo->countVisitsByTag((string) $foo, new DateRange(
|
||||||
Chronos::parse('2016-01-02'),
|
Chronos::parse('2016-01-02'),
|
||||||
Chronos::parse('2016-01-03'),
|
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'),
|
Chronos::parse('2016-01-03'),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace ShlinkioTest\Shlink\Core\Action;
|
|||||||
use Laminas\Diactoros\ServerRequest;
|
use Laminas\Diactoros\ServerRequest;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
use Shlinkio\Shlink\Common\Response\PixelResponse;
|
use Shlinkio\Shlink\Common\Response\PixelResponse;
|
||||||
@@ -19,6 +20,8 @@ use Shlinkio\Shlink\Core\Service\VisitsTracker;
|
|||||||
|
|
||||||
class PixelActionTest extends TestCase
|
class PixelActionTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private PixelAction $action;
|
private PixelAction $action;
|
||||||
private ObjectProphecy $urlResolver;
|
private ObjectProphecy $urlResolver;
|
||||||
private ObjectProphecy $visitTracker;
|
private ObjectProphecy $visitTracker;
|
||||||
@@ -47,8 +50,8 @@ class PixelActionTest extends TestCase
|
|||||||
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode);
|
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode);
|
||||||
$response = $this->action->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal());
|
$response = $this->action->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal());
|
||||||
|
|
||||||
$this->assertInstanceOf(PixelResponse::class, $response);
|
self::assertInstanceOf(PixelResponse::class, $response);
|
||||||
$this->assertEquals(200, $response->getStatusCode());
|
self::assertEquals(200, $response->getStatusCode());
|
||||||
$this->assertEquals('image/gif', $response->getHeaderLine('content-type'));
|
self::assertEquals('image/gif', $response->getHeaderLine('content-type'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use Laminas\Diactoros\ServerRequest;
|
|||||||
use Mezzio\Router\RouterInterface;
|
use Mezzio\Router\RouterInterface;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
use Shlinkio\Shlink\Common\Response\QrCodeResponse;
|
use Shlinkio\Shlink\Common\Response\QrCodeResponse;
|
||||||
@@ -20,6 +21,8 @@ use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
|||||||
|
|
||||||
class QrCodeActionTest extends TestCase
|
class QrCodeActionTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private QrCodeAction $action;
|
private QrCodeAction $action;
|
||||||
private ObjectProphecy $urlResolver;
|
private ObjectProphecy $urlResolver;
|
||||||
|
|
||||||
@@ -77,8 +80,34 @@ class QrCodeActionTest extends TestCase
|
|||||||
$delegate->reveal(),
|
$delegate->reveal(),
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->assertInstanceOf(QrCodeResponse::class, $resp);
|
self::assertInstanceOf(QrCodeResponse::class, $resp);
|
||||||
$this->assertEquals(200, $resp->getStatusCode());
|
self::assertEquals(200, $resp->getStatusCode());
|
||||||
$delegate->handle(Argument::any())->shouldHaveBeenCalledTimes(0);
|
$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 Mezzio\Router\Middleware\ImplicitHeadMiddleware;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
||||||
@@ -19,27 +20,30 @@ use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
|||||||
use Shlinkio\Shlink\Core\Options;
|
use Shlinkio\Shlink\Core\Options;
|
||||||
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
||||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface;
|
||||||
|
|
||||||
use function array_key_exists;
|
use function array_key_exists;
|
||||||
|
|
||||||
class RedirectActionTest extends TestCase
|
class RedirectActionTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
private RedirectAction $action;
|
private RedirectAction $action;
|
||||||
private ObjectProphecy $urlResolver;
|
private ObjectProphecy $urlResolver;
|
||||||
private ObjectProphecy $visitTracker;
|
private ObjectProphecy $visitTracker;
|
||||||
private Options\UrlShortenerOptions $shortenerOpts;
|
private ObjectProphecy $redirectRespHelper;
|
||||||
|
|
||||||
public function setUp(): void
|
public function setUp(): void
|
||||||
{
|
{
|
||||||
$this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class);
|
$this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class);
|
||||||
$this->visitTracker = $this->prophesize(VisitsTrackerInterface::class);
|
$this->visitTracker = $this->prophesize(VisitsTrackerInterface::class);
|
||||||
$this->shortenerOpts = new Options\UrlShortenerOptions();
|
$this->redirectRespHelper = $this->prophesize(RedirectResponseHelperInterface::class);
|
||||||
|
|
||||||
$this->action = new RedirectAction(
|
$this->action = new RedirectAction(
|
||||||
$this->urlResolver->reveal(),
|
$this->urlResolver->reveal(),
|
||||||
$this->visitTracker->reveal(),
|
$this->visitTracker->reveal(),
|
||||||
new Options\AppOptions(['disableTrackParam' => 'foobar']),
|
new Options\AppOptions(['disableTrackParam' => 'foobar']),
|
||||||
$this->shortenerOpts,
|
$this->redirectRespHelper->reveal(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,14 +60,14 @@ class RedirectActionTest extends TestCase
|
|||||||
)->willReturn($shortUrl);
|
)->willReturn($shortUrl);
|
||||||
$track = $this->visitTracker->track(Argument::cetera())->will(function (): void {
|
$track = $this->visitTracker->track(Argument::cetera())->will(function (): void {
|
||||||
});
|
});
|
||||||
|
$expectedResp = new Response\RedirectResponse($expectedUrl);
|
||||||
|
$buildResp = $this->redirectRespHelper->buildRedirectResponse($expectedUrl)->willReturn($expectedResp);
|
||||||
|
|
||||||
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode)->withQueryParams($query);
|
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode)->withQueryParams($query);
|
||||||
$response = $this->action->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal());
|
$response = $this->action->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal());
|
||||||
|
|
||||||
$this->assertInstanceOf(Response\RedirectResponse::class, $response);
|
self::assertSame($expectedResp, $response);
|
||||||
$this->assertEquals(302, $response->getStatusCode());
|
$buildResp->shouldHaveBeenCalledOnce();
|
||||||
$this->assertTrue($response->hasHeader('Location'));
|
|
||||||
$this->assertEquals($expectedUrl, $response->getHeaderLine('Location'));
|
|
||||||
$shortCodeToUrl->shouldHaveBeenCalledOnce();
|
$shortCodeToUrl->shouldHaveBeenCalledOnce();
|
||||||
$track->shouldHaveBeenCalledTimes(array_key_exists('foobar', $query) ? 0 : 1);
|
$track->shouldHaveBeenCalledTimes(array_key_exists('foobar', $query) ? 0 : 1);
|
||||||
}
|
}
|
||||||
@@ -104,6 +108,9 @@ class RedirectActionTest extends TestCase
|
|||||||
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))->willReturn($shortUrl);
|
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))->willReturn($shortUrl);
|
||||||
$track = $this->visitTracker->track(Argument::cetera())->will(function (): void {
|
$track = $this->visitTracker->track(Argument::cetera())->will(function (): void {
|
||||||
});
|
});
|
||||||
|
$buildResp = $this->redirectRespHelper->buildRedirectResponse(
|
||||||
|
'http://domain.com/foo/bar?some=thing',
|
||||||
|
)->willReturn(new Response\RedirectResponse(''));
|
||||||
|
|
||||||
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode)
|
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode)
|
||||||
->withAttribute(
|
->withAttribute(
|
||||||
@@ -112,42 +119,7 @@ class RedirectActionTest extends TestCase
|
|||||||
);
|
);
|
||||||
$this->action->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal());
|
$this->action->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal());
|
||||||
|
|
||||||
|
$buildResp->shouldHaveBeenCalled();
|
||||||
$track->shouldNotHaveBeenCalled();
|
$track->shouldNotHaveBeenCalled();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
* @dataProvider provideRedirectConfigs
|
|
||||||
*/
|
|
||||||
public function expectedStatusCodeAndCacheIsReturnedBasedOnConfig(
|
|
||||||
int $configuredStatus,
|
|
||||||
int $configuredLifetime,
|
|
||||||
int $expectedStatus,
|
|
||||||
?string $expectedCacheControl
|
|
||||||
): void {
|
|
||||||
$this->shortenerOpts->redirectStatusCode = $configuredStatus;
|
|
||||||
$this->shortenerOpts->redirectCacheLifetime = $configuredLifetime;
|
|
||||||
|
|
||||||
$shortUrl = new ShortUrl('http://domain.com/foo/bar');
|
|
||||||
$shortCode = $shortUrl->getShortCode();
|
|
||||||
$this->urlResolver->resolveEnabledShortUrl(Argument::cetera())->willReturn($shortUrl);
|
|
||||||
|
|
||||||
$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 ?? '');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function provideRedirectConfigs(): iterable
|
|
||||||
{
|
|
||||||
yield 'status 302' => [302, 20, 302, null];
|
|
||||||
yield 'status over 302' => [400, 20, 302, null];
|
|
||||||
yield 'status below 301' => [201, 20, 302, null];
|
|
||||||
yield 'status 301 with valid expiration' => [301, 20, 301, 'private,max-age=20'];
|
|
||||||
yield 'status 301 with zero expiration' => [301, 0, 301, 'private,max-age=30'];
|
|
||||||
yield 'status 301 with negative expiration' => [301, -20, 301, 'private,max-age=30'];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user