mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-06 23:33:13 +08:00
Compare commits
147 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
228bd83b75 | ||
|
|
a21dcb852a | ||
|
|
058391cf06 | ||
|
|
24e6acc6e8 | ||
|
|
56d299a7dc | ||
|
|
575e6bf707 | ||
|
|
e50c21440f | ||
|
|
7cff11080d | ||
|
|
72381f9844 | ||
|
|
b6792d3fb8 | ||
|
|
2f0d658432 | ||
|
|
8c1865c3ec | ||
|
|
096d2098d6 | ||
|
|
882d64ae11 | ||
|
|
3352bcd186 | ||
|
|
9743c1624d | ||
|
|
e85d59c5a4 | ||
|
|
ac0ff8fb94 | ||
|
|
90f93ee4ec | ||
|
|
794d926e3a | ||
|
|
bd41ebef9f | ||
|
|
725370704f | ||
|
|
f03b7689ce | ||
|
|
fb31e2a5e4 | ||
|
|
d688c6da7e | ||
|
|
618784dc3b | ||
|
|
9d64d4ed1d | ||
|
|
7f02243c6c | ||
|
|
3916c68126 | ||
|
|
a6f0c66331 | ||
|
|
bdfb220126 | ||
|
|
abcf2f86be | ||
|
|
a4d8ebdfc9 | ||
|
|
b51c149c30 | ||
|
|
39095a3098 | ||
|
|
765199727e | ||
|
|
c7043af853 | ||
|
|
02a8ef7dd9 | ||
|
|
6bb8c1b2f5 | ||
|
|
3cf253fd0f | ||
|
|
0365728337 | ||
|
|
b8143a5bb4 | ||
|
|
531a19dde9 | ||
|
|
69ff7de481 | ||
|
|
ffc0555c7c | ||
|
|
84a7981dfa | ||
|
|
2573c2bf98 | ||
|
|
3b4c1501f3 | ||
|
|
e836bedecc | ||
|
|
a797b74a70 | ||
|
|
ab497403ca | ||
|
|
d4dea9a1d2 | ||
|
|
28d93ea5e0 | ||
|
|
e6a31b16ed | ||
|
|
9553192281 | ||
|
|
74069f2d24 | ||
|
|
b4b00a57c1 | ||
|
|
a516ef691d | ||
|
|
e80b7448f5 | ||
|
|
f129544f83 | ||
|
|
9fa291a32f | ||
|
|
d06e92ffc2 | ||
|
|
1b83344995 | ||
|
|
cf49393ef2 | ||
|
|
f2ecbceae9 | ||
|
|
c582eba753 | ||
|
|
de86b62cdd | ||
|
|
73150471e9 | ||
|
|
ec751f4ac2 | ||
|
|
e652166289 | ||
|
|
a671d555cb | ||
|
|
6240554f4c | ||
|
|
4ee9c9bbe3 | ||
|
|
c830439085 | ||
|
|
f2196583c8 | ||
|
|
3dbca2115c | ||
|
|
b45d8de27d | ||
|
|
3ba46bbbfa | ||
|
|
06f3f0c86c | ||
|
|
06f07e3e40 | ||
|
|
740740b8c6 | ||
|
|
b6ed39b18b | ||
|
|
958c4704f8 | ||
|
|
ef075fb0ce | ||
|
|
556520583a | ||
|
|
399c56a097 | ||
|
|
f078d95588 | ||
|
|
33911afcd6 | ||
|
|
ae8d31e83f | ||
|
|
72c4052012 | ||
|
|
f713a1fa7e | ||
|
|
62488ac4e5 | ||
|
|
ab4c6e5fca | ||
|
|
26f4a969c9 | ||
|
|
703965915d | ||
|
|
24e38a3cf9 | ||
|
|
b12cfaedf3 | ||
|
|
71807e698c | ||
|
|
1d155298c1 | ||
|
|
4dfc5ae681 | ||
|
|
26f237069c | ||
|
|
b6e1c65c4c | ||
|
|
11f94b8306 | ||
|
|
01bcedef7a | ||
|
|
e51384fcc0 | ||
|
|
83c53c8b2e | ||
|
|
1afe08caed | ||
|
|
7289833928 | ||
|
|
f4d10df0f3 | ||
|
|
652b0df054 | ||
|
|
0e9ea5027c | ||
|
|
658303d375 | ||
|
|
ccc3a4b584 | ||
|
|
ef5ac86e0a | ||
|
|
91b90b276a | ||
|
|
85c32c3c9a | ||
|
|
40838255a7 | ||
|
|
a67ccb384f | ||
|
|
cb31e5a581 | ||
|
|
3c12a55872 | ||
|
|
6da8b11674 | ||
|
|
552489611f | ||
|
|
e48d0f4f0c | ||
|
|
49b6063501 | ||
|
|
dd049feb40 | ||
|
|
76a86c452e | ||
|
|
41aec15fab | ||
|
|
245cb0e35d | ||
|
|
7a0b1e8494 | ||
|
|
70c1c9f018 | ||
|
|
97e965157b | ||
|
|
04bbd471ff | ||
|
|
650a286982 | ||
|
|
ad44a8441a | ||
|
|
b339cf2429 | ||
|
|
9cd97c2f1e | ||
|
|
a7f6b60cba | ||
|
|
0d7dc50670 | ||
|
|
4bc5b9261f | ||
|
|
fb572d5abb | ||
|
|
8fa4219b30 | ||
|
|
a52d0cd419 | ||
|
|
0080ab5132 | ||
|
|
8afa582aa5 | ||
|
|
d847c7648e | ||
|
|
c140db16d1 | ||
|
|
adbf7c6f5e |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,7 +1,7 @@
|
|||||||
<!--
|
<!--
|
||||||
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
||||||
I'm always happy to help and provide support, but some understanding will be expected.
|
I'm always happy to help and provide support, but some understanding will be expected.
|
||||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
|
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personally if an issue gets eventually closed.
|
||||||
You may also be asked to provide tests or ways to reproduce reported bugs.
|
You may also be asked to provide tests or ways to reproduce reported bugs.
|
||||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||||
-->
|
-->
|
||||||
|
|||||||
8
.github/ISSUE_TEMPLATE/Bug.md
vendored
8
.github/ISSUE_TEMPLATE/Bug.md
vendored
@@ -7,18 +7,18 @@ labels: bug
|
|||||||
<!--
|
<!--
|
||||||
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
||||||
I'm always happy to help and provide support, but some understanding will be expected.
|
I'm always happy to help and provide support, but some understanding will be expected.
|
||||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
|
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personally if an issue gets eventually closed.
|
||||||
You may also be asked to provide tests or ways to reproduce reported bugs.
|
You may also be asked to provide tests or ways to reproduce reported bugs.
|
||||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||||
|
|
||||||
With that said, please fill in the information requested next. More information might be requested next (like logs or system configs).
|
With that said, please fill in the information requested next. More information might be requested next (like logs or system configs).
|
||||||
-->
|
-->
|
||||||
|
|
||||||
#### How Shlink is set-up
|
#### How Shlink is set up
|
||||||
|
|
||||||
* Shlink Version: x.y.z
|
* Shlink Version: x.y.z
|
||||||
* PHP Version: x.y.z
|
* PHP Version: x.y.z
|
||||||
* How do you serve Shlink: Self-hosted Apache|Self-hosted nginx|Self-hosted openswoole|Docker image
|
* How do you serve Shlink: Self-hosted Apache|Self-hosted nginx|Self-hosted openswoole|Self-hosted RoadRunner|Openswoole Docker image|RoadRunner Docker image
|
||||||
* Database engine used: MySQL|MariaDB|PostgreSQL|MicrosoftSQL|SQLite (x.y.z)
|
* Database engine used: MySQL|MariaDB|PostgreSQL|MicrosoftSQL|SQLite (x.y.z)
|
||||||
|
|
||||||
#### Summary
|
#### Summary
|
||||||
@@ -31,7 +31,7 @@ With that said, please fill in the information requested next. More information
|
|||||||
|
|
||||||
#### Expected behavior
|
#### Expected behavior
|
||||||
|
|
||||||
<!-- How did you expected to behave? -->
|
<!-- How did you expect it to behave? -->
|
||||||
|
|
||||||
#### How to reproduce
|
#### How to reproduce
|
||||||
|
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/Feature_Request.md
vendored
2
.github/ISSUE_TEMPLATE/Feature_Request.md
vendored
@@ -7,7 +7,7 @@ labels: feature
|
|||||||
<!--
|
<!--
|
||||||
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
||||||
I'm always happy to help and provide support, but some understanding will be expected.
|
I'm always happy to help and provide support, but some understanding will be expected.
|
||||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
|
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personally if an issue gets eventually closed.
|
||||||
You may also be asked to provide tests or ways to reproduce reported bugs.
|
You may also be asked to provide tests or ways to reproduce reported bugs.
|
||||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||||
|
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/Question_Support.md
vendored
6
.github/ISSUE_TEMPLATE/Question_Support.md
vendored
@@ -7,18 +7,18 @@ labels: question
|
|||||||
<!--
|
<!--
|
||||||
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
||||||
I'm always happy to help and provide support, but some understanding will be expected.
|
I'm always happy to help and provide support, but some understanding will be expected.
|
||||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
|
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personally if an issue gets eventually closed.
|
||||||
You may also be asked to provide tests or ways to reproduce reported bugs.
|
You may also be asked to provide tests or ways to reproduce reported bugs.
|
||||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||||
|
|
||||||
With that said, please fill in the information requested next. More information might be requested next (like logs or system configs).
|
With that said, please fill in the information requested next. More information might be requested next (like logs or system configs).
|
||||||
-->
|
-->
|
||||||
|
|
||||||
#### How Shlink is set-up
|
#### How Shlink is set up
|
||||||
|
|
||||||
* Shlink Version: x.y.z
|
* Shlink Version: x.y.z
|
||||||
* PHP Version: x.y.z
|
* PHP Version: x.y.z
|
||||||
* How do you serve Shlink: Self-hosted Apache|Self-hosted nginx|Self-hosted openswoole|Docker image
|
* How do you serve Shlink: Self-hosted Apache|Self-hosted nginx|Self-hosted openswoole|Self-hosted RoadRunner|Openswoole Docker image|RoadRunner Docker image
|
||||||
* Database engine used: MySQL|MariaDB|PostgreSQL|MicrosoftSQL|SQLite (x.y.z)
|
* Database engine used: MySQL|MariaDB|PostgreSQL|MicrosoftSQL|SQLite (x.y.z)
|
||||||
|
|
||||||
#### Summary
|
#### Summary
|
||||||
|
|||||||
2
.github/actions/ci-setup/action.yml
vendored
2
.github/actions/ci-setup/action.yml
vendored
@@ -28,7 +28,7 @@ runs:
|
|||||||
extensions: ${{ inputs.php-extensions }}
|
extensions: ${{ inputs.php-extensions }}
|
||||||
key: ${{ inputs.extensions-cache-key }}
|
key: ${{ inputs.extensions-cache-key }}
|
||||||
- name: Cache extensions
|
- name: Cache extensions
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.extcache.outputs.dir }}
|
path: ${{ steps.extcache.outputs.dir }}
|
||||||
key: ${{ steps.extcache.outputs.key }}
|
key: ${{ steps.extcache.outputs.key }}
|
||||||
|
|||||||
2
.github/workflows/ci-db-tests.yml
vendored
2
.github/workflows/ci-db-tests.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
- uses: './.github/actions/ci-setup'
|
- uses: './.github/actions/ci-setup'
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php-version }}
|
php-version: ${{ matrix.php-version }}
|
||||||
php-extensions: openswoole-4.12.1, pdo_sqlsrv-5.10.1
|
php-extensions: openswoole-22.0.0, pdo_sqlsrv-5.10.1
|
||||||
extensions-cache-key: db-tests-extensions-${{ matrix.php-version }}-${{ inputs.platform }}
|
extensions-cache-key: db-tests-extensions-${{ matrix.php-version }}-${{ inputs.platform }}
|
||||||
- name: Create test database
|
- name: Create test database
|
||||||
if: ${{ inputs.platform == 'ms' }}
|
if: ${{ inputs.platform == 'ms' }}
|
||||||
|
|||||||
8
.github/workflows/ci-mutation-tests.yml
vendored
8
.github/workflows/ci-mutation-tests.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
- uses: './.github/actions/ci-setup'
|
- uses: './.github/actions/ci-setup'
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php-version }}
|
php-version: ${{ matrix.php-version }}
|
||||||
php-extensions: openswoole-4.12.1
|
php-extensions: openswoole-22.0.0
|
||||||
extensions-cache-key: mutation-tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
|
extensions-cache-key: mutation-tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
@@ -27,14 +27,14 @@ jobs:
|
|||||||
path: build
|
path: build
|
||||||
- name: Resolve infection args
|
- name: Resolve infection args
|
||||||
id: infection_args
|
id: infection_args
|
||||||
run: echo "::set-output name=args::--logger-github=false"
|
run: echo "args=--logger-github=false" >> $GITHUB_OUTPUT
|
||||||
# TODO Try to filter mutation tests to improve execution times. Investigate why --git-diff-lines --git-diff-base=develop does not work
|
# TODO Try to filter mutation tests to improve execution times. Investigate why --git-diff-lines --git-diff-base=develop does not work
|
||||||
# run: |
|
# run: |
|
||||||
# BRANCH="${GITHUB_REF#refs/heads/}" |
|
# BRANCH="${GITHUB_REF#refs/heads/}" |
|
||||||
# if [[ $BRANCH == 'main' || $BRANCH == 'develop' ]]; then
|
# if [[ $BRANCH == 'main' || $BRANCH == 'develop' ]]; then
|
||||||
# echo "::set-output name=args::--logger-github=false"
|
# echo "args=--logger-github=false" >> $GITHUB_OUTPUT
|
||||||
# else
|
# else
|
||||||
# echo "::set-output name=args::--logger-github=false --git-diff-lines --git-diff-base=develop"
|
# echo "args=--logger-github=false --git-diff-lines --git-diff-base=develop" >> $GITHUB_OUTPUT
|
||||||
# fi;
|
# fi;
|
||||||
shell: bash
|
shell: bash
|
||||||
- if: ${{ inputs.test-group == 'unit' }}
|
- if: ${{ inputs.test-group == 'unit' }}
|
||||||
|
|||||||
2
.github/workflows/ci-tests.yml
vendored
2
.github/workflows/ci-tests.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
- uses: './.github/actions/ci-setup'
|
- uses: './.github/actions/ci-setup'
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php-version }}
|
php-version: ${{ matrix.php-version }}
|
||||||
php-extensions: openswoole-4.12.1
|
php-extensions: openswoole-22.0.0
|
||||||
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
|
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
|
||||||
- run: composer test:${{ inputs.test-group }}:ci
|
- run: composer test:${{ inputs.test-group }}:ci
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
|
|||||||
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
- uses: './.github/actions/ci-setup'
|
- uses: './.github/actions/ci-setup'
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php-version }}
|
php-version: ${{ matrix.php-version }}
|
||||||
php-extensions: openswoole-4.12.1
|
php-extensions: openswoole-22.0.0
|
||||||
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ matrix.command }}
|
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ matrix.command }}
|
||||||
- run: composer ${{ matrix.command }}
|
- run: composer ${{ matrix.command }}
|
||||||
|
|
||||||
@@ -69,8 +69,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php-version }}
|
php-version: ${{ matrix.php-version }}
|
||||||
tools: composer
|
tools: composer
|
||||||
- run: composer install --no-interaction --prefer-dist
|
- run: composer install --no-interaction --prefer-dist --ignore-platform-req=ext-openswoole
|
||||||
- run: ./vendor/bin/rr get --no-interaction --location bin/ && chmod +x bin/rr
|
- run: ./vendor/bin/rr get --no-interaction --no-config --location bin/ && chmod +x bin/rr
|
||||||
- run: composer test:api:rr
|
- run: composer test:api:rr
|
||||||
|
|
||||||
sqlite-db-tests:
|
sqlite-db-tests:
|
||||||
@@ -152,8 +152,8 @@ jobs:
|
|||||||
- run: mv build/coverage-db/coverage-db.cov build/coverage-db.cov
|
- run: mv build/coverage-db/coverage-db.cov build/coverage-db.cov
|
||||||
- run: mv build/coverage-api/coverage-api.cov build/coverage-api.cov
|
- run: mv build/coverage-api/coverage-api.cov build/coverage-api.cov
|
||||||
- run: mv build/coverage-cli/coverage-cli.cov build/coverage-cli.cov
|
- run: mv build/coverage-cli/coverage-cli.cov build/coverage-cli.cov
|
||||||
- run: wget https://phar.phpunit.de/phpcov-8.2.1.phar
|
- run: wget https://phar.phpunit.de/phpcov-9.0.0.phar
|
||||||
- run: php phpcov-8.2.1.phar merge build --clover build/clover.xml
|
- run: php phpcov-9.0.0.phar merge build --clover build/clover.xml
|
||||||
- name: Publish coverage
|
- name: Publish coverage
|
||||||
uses: codecov/codecov-action@v1
|
uses: codecov/codecov-action@v1
|
||||||
with:
|
with:
|
||||||
@@ -168,10 +168,7 @@ jobs:
|
|||||||
- upload-coverage
|
- upload-coverage
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: geekyeggo/delete-artifact@v1
|
- uses: geekyeggo/delete-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: |
|
name: |
|
||||||
coverage-unit
|
coverage-*
|
||||||
coverage-db
|
|
||||||
coverage-api
|
|
||||||
coverage-cli
|
|
||||||
|
|||||||
35
.github/workflows/publish-docker-image.yml
vendored
35
.github/workflows/publish-docker-image.yml
vendored
@@ -2,8 +2,6 @@ name: Build and publish docker image
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- 'LICENSE'
|
- 'LICENSE'
|
||||||
- '.*'
|
- '.*'
|
||||||
@@ -12,24 +10,35 @@ on:
|
|||||||
- '*.yml*'
|
- '*.yml*'
|
||||||
- '*.json5'
|
- '*.json5'
|
||||||
- '*.neon'
|
- '*.neon'
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-openswoole:
|
build-image:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- runtime: 'rr'
|
||||||
|
platforms: 'linux/arm64/v8,linux/amd64'
|
||||||
|
- runtime: 'rr'
|
||||||
|
tag-suffix: 'roadrunner'
|
||||||
|
platforms: 'linux/arm64/v8,linux/amd64'
|
||||||
|
- runtime: 'openswoole'
|
||||||
|
tag-suffix: 'openswoole'
|
||||||
|
platforms: 'linux/arm/v7,linux/arm64/v8,linux/amd64'
|
||||||
|
- runtime: 'rr'
|
||||||
|
tag-suffix: 'non-root'
|
||||||
|
platforms: 'linux/arm64/v8,linux/amd64'
|
||||||
|
user-id: '1001'
|
||||||
uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main
|
uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
image-name: shlinkio/shlink
|
image-name: shlinkio/shlink
|
||||||
version-arg-name: SHLINK_VERSION
|
version-arg-name: SHLINK_VERSION
|
||||||
|
platforms: ${{ matrix.platforms }}
|
||||||
build-roadrunner:
|
tags-suffix: ${{ matrix.tag-suffix }}
|
||||||
uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main
|
|
||||||
secrets: inherit
|
|
||||||
with:
|
|
||||||
image-name: shlinkio/shlink
|
|
||||||
version-arg-name: SHLINK_VERSION
|
|
||||||
platforms: 'linux/arm64/v8,linux/amd64'
|
|
||||||
tags-suffix: roadrunner
|
|
||||||
extra-build-args: |
|
extra-build-args: |
|
||||||
SHLINK_RUNTIME=rr
|
SHLINK_RUNTIME=${{ matrix.runtime }}
|
||||||
|
SHLINK_USER_ID=${{ matrix.user-id && matrix.user-id || 'root' }}
|
||||||
|
|||||||
10
.github/workflows/publish-release.yml
vendored
10
.github/workflows/publish-release.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- uses: './.github/actions/ci-setup'
|
- uses: './.github/actions/ci-setup'
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php-version }}
|
php-version: ${{ matrix.php-version }}
|
||||||
php-extensions: openswoole-4.12.1
|
php-extensions: openswoole-22.0.0
|
||||||
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
|
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
|
||||||
install-deps: 'no'
|
install-deps: 'no'
|
||||||
- if: ${{ matrix.swoole == 'yes' }}
|
- if: ${{ matrix.swoole == 'yes' }}
|
||||||
@@ -49,11 +49,7 @@ jobs:
|
|||||||
delete-artifacts:
|
delete-artifacts:
|
||||||
needs: ['publish']
|
needs: ['publish']
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
php-version: ['8.1', '8.2']
|
|
||||||
swoole: ['yes', 'no']
|
|
||||||
steps:
|
steps:
|
||||||
- uses: geekyeggo/delete-artifact@v1
|
- uses: geekyeggo/delete-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: dist-files-${{ matrix.php-version }}-${{ matrix.swoole }}
|
name: dist-files-*
|
||||||
|
|||||||
6
.github/workflows/publish-swagger-spec.yml
vendored
6
.github/workflows/publish-swagger-spec.yml
vendored
@@ -15,18 +15,18 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Determine version
|
- name: Determine version
|
||||||
id: determine_version
|
id: determine_version
|
||||||
run: echo "::set-output name=version::${GITHUB_REF#refs/tags/}"
|
run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
shell: bash
|
shell: bash
|
||||||
- uses: './.github/actions/ci-setup'
|
- uses: './.github/actions/ci-setup'
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php-version }}
|
php-version: ${{ matrix.php-version }}
|
||||||
php-extensions: openswoole-4.12.1
|
php-extensions: openswoole-22.0.0
|
||||||
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
|
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
|
||||||
- run: composer swagger:inline
|
- run: composer swagger:inline
|
||||||
- run: mkdir ${{ steps.determine_version.outputs.version }}
|
- run: mkdir ${{ steps.determine_version.outputs.version }}
|
||||||
- run: mv docs/swagger/swagger-inlined.json ${{ steps.determine_version.outputs.version }}/open-api-spec.json
|
- run: mv docs/swagger/swagger-inlined.json ${{ steps.determine_version.outputs.version }}/open-api-spec.json
|
||||||
- name: Publish spec
|
- name: Publish spec
|
||||||
uses: JamesIves/github-pages-deploy-action@4.1.7
|
uses: JamesIves/github-pages-deploy-action@4.4.1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.OAS_PUBLISH_TOKEN }}
|
token: ${{ secrets.OAS_PUBLISH_TOKEN }}
|
||||||
repository-name: 'shlinkio/shlink-open-api-specs'
|
repository-name: 'shlinkio/shlink-open-api-specs'
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,4 @@
|
|||||||
.idea
|
.idea
|
||||||
bin/.rr.*
|
|
||||||
bin/rr
|
bin/rr
|
||||||
config/roadrunner/.pid
|
config/roadrunner/.pid
|
||||||
build
|
build
|
||||||
|
|||||||
179
CHANGELOG.md
179
CHANGELOG.md
@@ -4,6 +4,181 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
|
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||||
|
|
||||||
|
## [3.6.3] - 2023-06-14
|
||||||
|
### Added
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* [#1817](https://github.com/shlinkio/shlink/issues/1817) Fix Shlink trying to create SQLite database tables even if they already exist.
|
||||||
|
|
||||||
|
|
||||||
|
## [3.6.2] - 2023-06-08
|
||||||
|
### Added
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* [#1808](https://github.com/shlinkio/shlink/issues/1808) Fix `rr` binary downloading during Shlink update.
|
||||||
|
|
||||||
|
|
||||||
|
## [3.6.1] - 2023-06-04
|
||||||
|
### Added
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* [#1413](https://github.com/shlinkio/shlink/issues/1413) Fix error when creating initial DB in Postgres in a cluster where a default `postgres` db does not exist or the credentials do not grant permissions to connect.
|
||||||
|
* [#1803](https://github.com/shlinkio/shlink/issues/1803) Fix default RoadRunner port when not using docker image.
|
||||||
|
|
||||||
|
|
||||||
|
## [3.6.0] - 2023-05-24
|
||||||
|
### Added
|
||||||
|
* [#1148](https://github.com/shlinkio/shlink/issues/1148) Add support to delete short URL visits.
|
||||||
|
|
||||||
|
This can be done via `DELETE /short-urls/{shortCode}/visits` REST endpoint or via `short-url:visits-delete` console command.
|
||||||
|
|
||||||
|
The CLI command includes a warning and requires the user to confirm before proceeding.
|
||||||
|
|
||||||
|
* [#1681](https://github.com/shlinkio/shlink/issues/1681) Add support to delete orphan visits.
|
||||||
|
|
||||||
|
This can be done via `DELETE /visits/orphan` REST endpoint or via `visit:orphan-delete` console command.
|
||||||
|
|
||||||
|
The CLI command includes a warning and requires the user to confirm before proceeding.
|
||||||
|
|
||||||
|
* [#1753](https://github.com/shlinkio/shlink/issues/1753) Add a new `vendor/bin/shlink-installer init` command that can be used to automate Shlink installations.
|
||||||
|
|
||||||
|
This command can create the initial database, update it, create proxies, clean cache, download initial GeoLite db files, etc
|
||||||
|
|
||||||
|
The official docker image also uses it on its entry point script.
|
||||||
|
|
||||||
|
* [#1656](https://github.com/shlinkio/shlink/issues/1656) Add support for openswoole 22
|
||||||
|
* [#1784](https://github.com/shlinkio/shlink/issues/1784) Add new docker tag where the container runs as a non-root user.
|
||||||
|
* [#953](https://github.com/shlinkio/shlink/issues/953) Add locks that prevent errors on duplicated keys when creating short URLs in parallel that depend on the same new tag or domain.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* [#1755](https://github.com/shlinkio/shlink/issues/1755) Update to roadrunner 2023
|
||||||
|
* [#1745](https://github.com/shlinkio/shlink/issues/1745) Roadrunner is now the default docker runtime.
|
||||||
|
|
||||||
|
There are now three different docker images published:
|
||||||
|
|
||||||
|
* Versions without suffix (like `3.6.0`) will contain the default runtime, whichever it is.
|
||||||
|
* Versions with `-roadrunner` suffix (like `3.6.0-roadrunner`) will always use roadrunner as the runtime, even if default one changes in the future.
|
||||||
|
* Versions with `-openswoole` suffix (like `3.6.0-openswoole`) will always use openswoole as the runtime, even if default one changes in the future.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* [#1760](https://github.com/shlinkio/shlink/issues/1760) Fix domain not being set to null when importing short URLs with default domain.
|
||||||
|
* [#953](https://github.com/shlinkio/shlink/issues/953) Fix duplicated key errors and short URL creation failing when creating short URLs in parallel that depend on the same new tag or domain.
|
||||||
|
* [#1741](https://github.com/shlinkio/shlink/issues/1741) Fix randomly using 100% CPU in task workers when trying to download GeoLite DB files.
|
||||||
|
* Fix Shlink trying to connect to RabbitMQ even if configuration set to not connect.
|
||||||
|
|
||||||
|
|
||||||
|
## [3.5.4] - 2023-04-12
|
||||||
|
### Added
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* [#1742](https://github.com/shlinkio/shlink/issues/1742) Fix URLs using schemas which do not contain `//`, like `mailto:`, to no longer be considered valid.
|
||||||
|
* [#1743](https://github.com/shlinkio/shlink/issues/1743) Fix Error when trying to create short URLs from CLI on an openswoole context.
|
||||||
|
|
||||||
|
Unfortunately the reason are real-time updates do not work with openswoole when outside an openswoole request, so the feature has been disabled for that context.
|
||||||
|
|
||||||
|
|
||||||
|
## [3.5.3] - 2023-03-31
|
||||||
|
### Added
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* [#1715](https://github.com/shlinkio/shlink/issues/1715) Fix short URL creation/edition allowing long URLs without schema. Now a validation error is thrown.
|
||||||
|
* [#1537](https://github.com/shlinkio/shlink/issues/1537) Fix incorrect list of tags being returned for some author-only API keys.
|
||||||
|
* [#1738](https://github.com/shlinkio/shlink/issues/1738) Fix memory leak when importing short URLs with many visits.
|
||||||
|
|
||||||
|
|
||||||
|
## [3.5.2] - 2023-02-16
|
||||||
|
### Added
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* [#1696](https://github.com/shlinkio/shlink/issues/1696) Migrated to PHPUnit 10.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* [#1698](https://github.com/shlinkio/shlink/issues/1698) Fixed error 500 in `robots.txt`.
|
||||||
|
* [#1688](https://github.com/shlinkio/shlink/issues/1688) Fixed huge performance degradation on `/tags/stats` endpoint.
|
||||||
|
* [#1693](https://github.com/shlinkio/shlink/issues/1693) Fixed Shlink thinking database already exists if it finds foreign tables.
|
||||||
|
|
||||||
|
|
||||||
|
## [3.5.1] - 2023-02-04
|
||||||
|
### Added
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* [#1685](https://github.com/shlinkio/shlink/issues/1685) Changed `loosely` mode to `loose`, as it was a typo. The old one keeps working and maps to the new one, but it's considered deprecated.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* [#1682](https://github.com/shlinkio/shlink/issues/1682) Fixed incorrect case-insensitive checks in short URLs when using Microsoft SQL server.
|
||||||
|
* [#1684](https://github.com/shlinkio/shlink/issues/1684) Fixed entities metadata cache not being cleared at docker container start-up when using redis with replication.
|
||||||
|
|
||||||
|
|
||||||
## [3.5.0] - 2023-01-28
|
## [3.5.0] - 2023-01-28
|
||||||
### Added
|
### Added
|
||||||
* [#1557](https://github.com/shlinkio/shlink/issues/1557) Added support to dynamically redirect to different long URLs based on the visitor's device type.
|
* [#1557](https://github.com/shlinkio/shlink/issues/1557) Added support to dynamically redirect to different long URLs based on the visitor's device type.
|
||||||
@@ -25,9 +200,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
|||||||
* [#1662](https://github.com/shlinkio/shlink/issues/1662) Added support to provide openswoole-specific config options via env vars prefixed with `OPENSWOOLE_`.
|
* [#1662](https://github.com/shlinkio/shlink/issues/1662) Added support to provide openswoole-specific config options via env vars prefixed with `OPENSWOOLE_`.
|
||||||
* [#1389](https://github.com/shlinkio/shlink/issues/1389) and [#706](https://github.com/shlinkio/shlink/issues/706) Added support for case-insensitive short URLs.
|
* [#1389](https://github.com/shlinkio/shlink/issues/1389) and [#706](https://github.com/shlinkio/shlink/issues/706) Added support for case-insensitive short URLs.
|
||||||
|
|
||||||
In order to achieve this, a new env var/config option has been implemented (`SHORT_URL_MODE`), which allows either `strict` or `loosely`.
|
In order to achieve this, a new env var/config option has been implemented (`SHORT_URL_MODE`), which allows either `strict` or ~~`loosely`~~ `loose`.
|
||||||
|
|
||||||
Default value is `strict`, but if `loosely` is provided, then short URLs will be matched in a case-insensitive way, and new short URLs will be generated with short-codes in lowercase only.
|
Default value is `strict`, but if `loose` is provided, then short URLs will be matched in a case-insensitive way, and new short URLs will be generated with short-codes in lowercase only.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
|
|||||||
32
Dockerfile
32
Dockerfile
@@ -2,13 +2,16 @@ FROM php:8.2-alpine3.17 as base
|
|||||||
|
|
||||||
ARG SHLINK_VERSION=latest
|
ARG SHLINK_VERSION=latest
|
||||||
ENV SHLINK_VERSION ${SHLINK_VERSION}
|
ENV SHLINK_VERSION ${SHLINK_VERSION}
|
||||||
ARG SHLINK_RUNTIME=openswoole
|
ARG SHLINK_RUNTIME=rr
|
||||||
ENV SHLINK_RUNTIME ${SHLINK_RUNTIME}
|
ENV SHLINK_RUNTIME ${SHLINK_RUNTIME}
|
||||||
ENV OPENSWOOLE_VERSION 4.12.1
|
ARG SHLINK_USER_ID='root'
|
||||||
|
ENV SHLINK_USER_ID ${SHLINK_USER_ID}
|
||||||
|
|
||||||
|
ENV OPENSWOOLE_VERSION 22.0.0
|
||||||
ENV PDO_SQLSRV_VERSION 5.10.1
|
ENV PDO_SQLSRV_VERSION 5.10.1
|
||||||
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
||||||
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
||||||
ENV LC_ALL "C"
|
ENV LC_ALL 'C'
|
||||||
|
|
||||||
WORKDIR /etc/shlink
|
WORKDIR /etc/shlink
|
||||||
|
|
||||||
@@ -43,11 +46,12 @@ FROM base as builder
|
|||||||
COPY . .
|
COPY . .
|
||||||
COPY --from=composer:2 /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 --prefer-dist --optimize-autoloader --no-progress --no-interaction && \
|
# FIXME Ignoring ext-openswoole platform req, as it makes install fail with roadrunner, even though it's a dev dependency and we are passing --no-dev
|
||||||
|
php composer.phar install --no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction --ignore-platform-req=ext-openswoole && \
|
||||||
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \
|
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \
|
||||||
php composer.phar remove spiral/roadrunner spiral/roadrunner-jobs --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interactionc ; \
|
php composer.phar remove spiral/roadrunner spiral/roadrunner-jobs spiral/roadrunner-cli spiral/roadrunner-http --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interaction ; \
|
||||||
elif [ $SHLINK_RUNTIME == 'rr' ]; then \
|
elif [ "$SHLINK_RUNTIME" == 'rr' ]; then \
|
||||||
php composer.phar remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interaction ; \
|
php composer.phar remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interaction --ignore-platform-req=ext-openswoole ; \
|
||||||
fi; \
|
fi; \
|
||||||
php composer.phar clear-cache && \
|
php composer.phar clear-cache && \
|
||||||
rm -r docker composer.* && \
|
rm -r docker composer.* && \
|
||||||
@@ -58,10 +62,10 @@ RUN apk add --no-cache git && \
|
|||||||
FROM base
|
FROM base
|
||||||
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
||||||
|
|
||||||
COPY --from=builder /etc/shlink .
|
COPY --from=builder --chown=${SHLINK_USER_ID} /etc/shlink .
|
||||||
RUN ln -s /etc/shlink/bin/cli /usr/local/bin/shlink && \
|
RUN ln -s /etc/shlink/bin/cli /usr/local/bin/shlink && \
|
||||||
if [ "$SHLINK_RUNTIME" == 'rr' ]; then \
|
if [ "$SHLINK_RUNTIME" == 'rr' ]; then \
|
||||||
php ./vendor/bin/rr get --no-interaction --location bin/ && chmod +x bin/rr ; \
|
php ./vendor/bin/rr get --no-interaction --no-config --location bin/ && chmod +x bin/rr ; \
|
||||||
fi;
|
fi;
|
||||||
|
|
||||||
# Expose default port
|
# Expose default port
|
||||||
@@ -72,14 +76,6 @@ COPY docker/docker-entrypoint.sh docker-entrypoint.sh
|
|||||||
COPY docker/config/shlink_in_docker.local.php config/autoload/shlink_in_docker.local.php
|
COPY docker/config/shlink_in_docker.local.php config/autoload/shlink_in_docker.local.php
|
||||||
COPY docker/config/php.ini ${PHP_INI_DIR}/conf.d/
|
COPY docker/config/php.ini ${PHP_INI_DIR}/conf.d/
|
||||||
|
|
||||||
# Change the ownership of /etc/shlink/data to be writable, then change the user to non-root
|
USER ${SHLINK_USER_ID}
|
||||||
# FIXME Disabled for now, as it conflicts with ENABLE_PERIODIC_VISIT_LOCATE, which is used to configure a cron as root.
|
|
||||||
# Ref: https://github.com/shlinkio/shlink/issues/1132
|
|
||||||
#RUN chown 1001 /etc/shlink/data
|
|
||||||
#RUN chown 1001 /etc/shlink/data/locks
|
|
||||||
#RUN chown 1001 /etc/shlink/data/proxies
|
|
||||||
#RUN chown 1001 /etc/shlink/data/cache
|
|
||||||
#RUN chown 1001 /etc/shlink/data/log
|
|
||||||
#USER 1001
|
|
||||||
|
|
||||||
ENTRYPOINT ["/bin/sh", "./docker-entrypoint.sh"]
|
ENTRYPOINT ["/bin/sh", "./docker-entrypoint.sh"]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
[](https://packagist.org/packages/shlinkio/shlink)
|
[](https://packagist.org/packages/shlinkio/shlink)
|
||||||
[](https://hub.docker.com/r/shlinkio/shlink/)
|
[](https://hub.docker.com/r/shlinkio/shlink/)
|
||||||
[](https://github.com/shlinkio/shlink/blob/main/LICENSE)
|
[](https://github.com/shlinkio/shlink/blob/main/LICENSE)
|
||||||
[](https://twitter.com/shlinkio)
|
[](https://twitter.com/shlinkio)
|
||||||
[](https://fosstodon.org/@shlinkio)
|
[](https://fosstodon.org/@shlinkio)
|
||||||
[](https://slnk.to/donate)
|
[](https://slnk.to/donate)
|
||||||
|
|
||||||
|
|||||||
2
build.sh
2
build.sh
@@ -39,7 +39,7 @@ if [[ $noSwoole ]]; then
|
|||||||
${composerBin} remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev $composerFlags
|
${composerBin} remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev $composerFlags
|
||||||
else
|
else
|
||||||
# If generating a dist for openswoole, uninstall RoadRunner
|
# If generating a dist for openswoole, uninstall RoadRunner
|
||||||
${composerBin} remove spiral/roadrunner spiral/roadrunner-jobs --with-all-dependencies --update-no-dev $composerFlags
|
${composerBin} remove spiral/roadrunner spiral/roadrunner-jobs spiral/roadrunner-cli spiral/roadrunner-http --with-all-dependencies --update-no-dev $composerFlags
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Delete development files
|
# Delete development files
|
||||||
|
|||||||
@@ -20,63 +20,64 @@
|
|||||||
"akrabat/ip-address-middleware": "^2.1",
|
"akrabat/ip-address-middleware": "^2.1",
|
||||||
"cakephp/chronos": "^2.3",
|
"cakephp/chronos": "^2.3",
|
||||||
"doctrine/migrations": "^3.5",
|
"doctrine/migrations": "^3.5",
|
||||||
"doctrine/orm": "^2.13.3",
|
"doctrine/orm": "^2.14",
|
||||||
"endroid/qr-code": "^4.6",
|
"endroid/qr-code": "^4.7",
|
||||||
"geoip2/geoip2": "^2.13",
|
"geoip2/geoip2": "^2.13",
|
||||||
"guzzlehttp/guzzle": "^7.5",
|
"guzzlehttp/guzzle": "^7.5",
|
||||||
"happyr/doctrine-specification": "^2.0",
|
"happyr/doctrine-specification": "^2.0",
|
||||||
"jaybizzle/crawler-detect": "^1.2.112",
|
"jaybizzle/crawler-detect": "^1.2.112",
|
||||||
"laminas/laminas-config": "^3.7",
|
"laminas/laminas-config": "^3.8",
|
||||||
"laminas/laminas-config-aggregator": "^1.11",
|
"laminas/laminas-config-aggregator": "^1.13",
|
||||||
"laminas/laminas-diactoros": "^2.19",
|
"laminas/laminas-diactoros": "^2.24",
|
||||||
"laminas/laminas-inputfilter": "^2.22",
|
"laminas/laminas-inputfilter": "^2.24",
|
||||||
"laminas/laminas-servicemanager": "^3.19",
|
"laminas/laminas-servicemanager": "^3.20",
|
||||||
"laminas/laminas-stdlib": "^3.15",
|
"laminas/laminas-stdlib": "^3.16",
|
||||||
"lcobucci/jwt": "^4.2",
|
|
||||||
"league/uri": "^6.8",
|
"league/uri": "^6.8",
|
||||||
"lstrojny/functional-php": "^1.17",
|
"lstrojny/functional-php": "^1.17",
|
||||||
"mezzio/mezzio": "^3.13",
|
"mezzio/mezzio": "^3.15",
|
||||||
"mezzio/mezzio-fastroute": "^3.7",
|
"mezzio/mezzio-fastroute": "^3.8",
|
||||||
"mezzio/mezzio-problem-details": "^1.7",
|
"mezzio/mezzio-problem-details": "^1.11",
|
||||||
"mezzio/mezzio-swoole": "^4.5",
|
"mezzio/mezzio-swoole": "^4.6",
|
||||||
"mlocati/ip-lib": "^1.18",
|
"mlocati/ip-lib": "^1.18",
|
||||||
"mobiledetect/mobiledetectlib": "^3.74",
|
"mobiledetect/mobiledetectlib": "^3.74",
|
||||||
"ocramius/proxy-manager": "^2.14",
|
"ocramius/proxy-manager": "^2.14",
|
||||||
"pagerfanta/core": "^3.6",
|
"pagerfanta/core": "^3.7",
|
||||||
"php-middleware/request-id": "^4.1",
|
"php-middleware/request-id": "^4.1",
|
||||||
"pugx/shortid-php": "^1.1",
|
"pugx/shortid-php": "^1.1",
|
||||||
"ramsey/uuid": "^4.5",
|
"ramsey/uuid": "^4.7",
|
||||||
"shlinkio/shlink-common": "^5.3",
|
"shlinkio/shlink-common": "^5.5",
|
||||||
"shlinkio/shlink-config": "^2.4",
|
"shlinkio/shlink-config": "^2.4",
|
||||||
"shlinkio/shlink-event-dispatcher": "^2.6",
|
"shlinkio/shlink-event-dispatcher": "^3.0",
|
||||||
"shlinkio/shlink-importer": "^5.0",
|
"shlinkio/shlink-importer": "^5.1",
|
||||||
"shlinkio/shlink-installer": "^8.3",
|
"shlinkio/shlink-installer": "^8.4.1",
|
||||||
"shlinkio/shlink-ip-geolocation": "^3.2",
|
"shlinkio/shlink-ip-geolocation": "^3.2",
|
||||||
"spiral/roadrunner": "^2.11",
|
"shlinkio/shlink-json": "^1.0",
|
||||||
"spiral/roadrunner-jobs": "^2.5",
|
"spiral/roadrunner": "^2023.1",
|
||||||
"symfony/console": "^6.1",
|
"spiral/roadrunner-cli": "^2.5",
|
||||||
"symfony/filesystem": "^6.1",
|
"spiral/roadrunner-http": "^3.0",
|
||||||
"symfony/lock": "^6.1",
|
"spiral/roadrunner-jobs": "^4.0",
|
||||||
"symfony/process": "^6.1",
|
"symfony/console": "^6.2",
|
||||||
"symfony/string": "^6.1"
|
"symfony/filesystem": "^6.2",
|
||||||
|
"symfony/lock": "^6.2",
|
||||||
|
"symfony/process": "^6.2",
|
||||||
|
"symfony/string": "^6.2"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"cebe/php-openapi": "^1.7",
|
"cebe/php-openapi": "^1.7",
|
||||||
"devster/ubench": "^2.1",
|
"devster/ubench": "^2.1",
|
||||||
"dms/phpunit-arraysubset-asserts": "^0.4.0",
|
"infection/infection": "^0.27",
|
||||||
"infection/infection": "^0.26.15",
|
"openswoole/ide-helper": "~22.0.0",
|
||||||
"openswoole/ide-helper": "~4.11.5",
|
"phpstan/phpstan": "^1.9",
|
||||||
"phpstan/phpstan": "^1.8",
|
|
||||||
"phpstan/phpstan-doctrine": "^1.3",
|
"phpstan/phpstan-doctrine": "^1.3",
|
||||||
"phpstan/phpstan-phpunit": "^1.1",
|
"phpstan/phpstan-phpunit": "^1.3",
|
||||||
"phpstan/phpstan-symfony": "^1.2",
|
"phpstan/phpstan-symfony": "^1.2",
|
||||||
"phpunit/php-code-coverage": "^9.2",
|
"phpunit/php-code-coverage": "^10.0",
|
||||||
"phpunit/phpunit": "^9.5",
|
"phpunit/phpunit": "~10.1.0",
|
||||||
"roave/security-advisories": "dev-master",
|
"roave/security-advisories": "dev-master",
|
||||||
"shlinkio/php-coding-standard": "~2.3.0",
|
"shlinkio/php-coding-standard": "~2.3.0",
|
||||||
"shlinkio/shlink-test-utils": "^3.4",
|
"shlinkio/shlink-test-utils": "~3.6.0",
|
||||||
"symfony/var-dumper": "^6.1",
|
"symfony/var-dumper": "^6.2",
|
||||||
"veewee/composer-run-parallel": "^1.1"
|
"veewee/composer-run-parallel": "^1.2"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
@@ -109,7 +110,7 @@
|
|||||||
"@parallel cs stan swagger:validate test:unit:ci test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms",
|
"@parallel cs stan swagger:validate test:unit:ci test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms",
|
||||||
"@parallel infect:test:api infect:test:cli infect:ci:unit infect:ci:db"
|
"@parallel infect:test:api infect:test:cli infect:ci:unit infect:ci:db"
|
||||||
],
|
],
|
||||||
"cs": "phpcs",
|
"cs": "phpcs -s",
|
||||||
"cs:fix": "phpcbf",
|
"cs:fix": "phpcbf",
|
||||||
"stan": "APP_ENV=test php vendor/bin/phpstan analyse module/*/src module/*/test* module/*/config config docker/config data/migrations --level=8",
|
"stan": "APP_ENV=test php vendor/bin/phpstan analyse module/*/src module/*/test* module/*/config config docker/config data/migrations --level=8",
|
||||||
"test": [
|
"test": [
|
||||||
@@ -133,7 +134,7 @@
|
|||||||
"test:cli": "APP_ENV=test DB_DRIVER=maria TEST_ENV=cli php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-cli.xml --log-junit=build/coverage-cli/junit.xml",
|
"test:cli": "APP_ENV=test DB_DRIVER=maria TEST_ENV=cli php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-cli.xml --log-junit=build/coverage-cli/junit.xml",
|
||||||
"test:cli:ci": "GENERATE_COVERAGE=yes composer test:cli",
|
"test:cli:ci": "GENERATE_COVERAGE=yes composer test:cli",
|
||||||
"test:cli:pretty": "GENERATE_COVERAGE=pretty composer test:cli",
|
"test:cli:pretty": "GENERATE_COVERAGE=pretty composer test:cli",
|
||||||
"infect:ci:base": "infection --threads=max --only-covered --only-covering-test-cases --skip-initial-tests",
|
"infect:ci:base": "infection --threads=max --only-covered --skip-initial-tests",
|
||||||
"infect:ci:unit": "@infect:ci:base --coverage=build/coverage-unit --min-msi=80",
|
"infect:ci:unit": "@infect:ci:base --coverage=build/coverage-unit --min-msi=80",
|
||||||
"infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json5",
|
"infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json5",
|
||||||
"infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=80 --configuration=infection-api.json5",
|
"infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=80 --configuration=infection-api.json5",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||||
|
use Mezzio\Application;
|
||||||
use Mezzio\Container;
|
use Mezzio\Container;
|
||||||
use Psr\Http\Client\ClientInterface;
|
use Psr\Http\Client\ClientInterface;
|
||||||
use Psr\Http\Message\ServerRequestFactoryInterface;
|
use Psr\Http\Message\ServerRequestFactoryInterface;
|
||||||
@@ -20,7 +21,7 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
'delegators' => [
|
'delegators' => [
|
||||||
Mezzio\Application::class => [
|
Application::class => [
|
||||||
Container\ApplicationConfigInjectionDelegator::class,
|
Container\ApplicationConfigInjectionDelegator::class,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -6,12 +6,40 @@ return [
|
|||||||
|
|
||||||
'entity_manager' => [
|
'entity_manager' => [
|
||||||
'connection' => [
|
'connection' => [
|
||||||
|
// MySQL
|
||||||
'user' => 'root',
|
'user' => 'root',
|
||||||
'password' => 'root',
|
'password' => 'root',
|
||||||
'driver' => 'pdo_mysql',
|
'driver' => 'pdo_mysql',
|
||||||
'host' => 'shlink_db_mysql',
|
'host' => 'shlink_db_mysql',
|
||||||
'dbname' => 'shlink',
|
'dbname' => 'shlink',
|
||||||
|
// 'dbname' => 'shlink_foo',
|
||||||
'charset' => 'utf8mb4',
|
'charset' => 'utf8mb4',
|
||||||
|
|
||||||
|
// MariaDB
|
||||||
|
// 'user' => 'root',
|
||||||
|
// 'password' => 'root',
|
||||||
|
// 'driver' => 'pdo_mysql',
|
||||||
|
// 'host' => 'shlink_db_maria',
|
||||||
|
// 'dbname' => 'shlink_foo',
|
||||||
|
// 'charset' => 'utf8mb4',
|
||||||
|
|
||||||
|
// Postgres
|
||||||
|
// 'user' => 'postgres',
|
||||||
|
// 'password' => 'root',
|
||||||
|
// 'driver' => 'pdo_pgsql',
|
||||||
|
// 'host' => 'shlink_db_postgres',
|
||||||
|
// 'dbname' => 'shlink_foo',
|
||||||
|
// 'charset' => 'utf8',
|
||||||
|
|
||||||
|
// MSSQL
|
||||||
|
// 'user' => 'sa',
|
||||||
|
// 'password' => 'Passw0rd!',
|
||||||
|
// 'driver' => 'pdo_sqlsrv',
|
||||||
|
// 'host' => 'shlink_db_ms',
|
||||||
|
// 'dbname' => 'shlink_foo',
|
||||||
|
// 'driverOptions' => [
|
||||||
|
// 'TrustServerCertificate' => 'true',
|
||||||
|
// ],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -4,51 +4,63 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink;
|
namespace Shlinkio\Shlink;
|
||||||
|
|
||||||
|
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||||
use Monolog\Level;
|
use Monolog\Level;
|
||||||
use Monolog\Logger;
|
use Monolog\Logger;
|
||||||
use PhpMiddleware\RequestId;
|
use PhpMiddleware\RequestId;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
use Shlinkio\Shlink\Common\Logger\LoggerFactory;
|
use Shlinkio\Shlink\Common\Logger\LoggerFactory;
|
||||||
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
||||||
|
use Shlinkio\Shlink\Common\Middleware\AccessLogMiddleware;
|
||||||
|
|
||||||
$common = [
|
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
||||||
'level' => Level::Info->value,
|
|
||||||
'processors' => [RequestId\MonologProcessor::class],
|
|
||||||
'line_format' => '[%datetime%] [%extra.request_id%] %channel%.%level_name% - %message%',
|
|
||||||
];
|
|
||||||
|
|
||||||
return [
|
return (static function (): array {
|
||||||
|
$common = [
|
||||||
|
'level' => Level::Info->value,
|
||||||
|
'processors' => [RequestId\MonologProcessor::class],
|
||||||
|
'line_format' => '[%datetime%] [%extra.request_id%] %channel%.%level_name% - %message%',
|
||||||
|
];
|
||||||
|
|
||||||
'logger' => [
|
return [
|
||||||
'Shlink' => [
|
|
||||||
'type' => LoggerType::FILE->value,
|
|
||||||
...$common,
|
|
||||||
],
|
|
||||||
'Access' => [
|
|
||||||
'type' => LoggerType::STREAM->value,
|
|
||||||
...$common,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
'dependencies' => [
|
'logger' => [
|
||||||
'factories' => [
|
'Shlink' => [
|
||||||
'Logger_Shlink' => [LoggerFactory::class, 'Shlink'],
|
'type' => LoggerType::FILE->value,
|
||||||
'Logger_Access' => [LoggerFactory::class, 'Access'],
|
...$common,
|
||||||
],
|
],
|
||||||
'aliases' => [
|
'Access' => [
|
||||||
'logger' => 'Logger_Shlink',
|
'type' => LoggerType::STREAM->value,
|
||||||
Logger::class => 'Logger_Shlink',
|
'destination' => 'php://stderr',
|
||||||
LoggerInterface::class => 'Logger_Shlink',
|
'add_new_line' => ! runningInRoadRunner(),
|
||||||
],
|
...$common,
|
||||||
],
|
|
||||||
|
|
||||||
'mezzio-swoole' => [
|
|
||||||
'swoole-http-server' => [
|
|
||||||
'logger' => [
|
|
||||||
'logger-name' => 'Logger_Access',
|
|
||||||
'format' => '%u "%r" %>s %B',
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
|
||||||
|
|
||||||
];
|
'dependencies' => [
|
||||||
|
'factories' => [
|
||||||
|
'Logger_Shlink' => [LoggerFactory::class, 'Shlink'],
|
||||||
|
'Logger_Access' => [LoggerFactory::class, 'Access'],
|
||||||
|
NullLogger::class => InvokableFactory::class,
|
||||||
|
],
|
||||||
|
'aliases' => [
|
||||||
|
'logger' => 'Logger_Shlink',
|
||||||
|
Logger::class => 'Logger_Shlink',
|
||||||
|
LoggerInterface::class => 'Logger_Shlink',
|
||||||
|
AccessLogMiddleware::LOGGER_SERVICE_NAME => 'Logger_Access',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'mezzio-swoole' => [
|
||||||
|
'swoole-http-server' => [
|
||||||
|
'logger' => [
|
||||||
|
// Let's disable mezio-swoole access logging, so that we can provide our own implementation,
|
||||||
|
// consistent for roadrunner and openswoole
|
||||||
|
'logger-name' => NullLogger::class,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
})();
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ declare(strict_types=1);
|
|||||||
use Monolog\Level;
|
use Monolog\Level;
|
||||||
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
||||||
|
|
||||||
$isSwoole = extension_loaded('openswoole');
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'logger' => [
|
'logger' => [
|
||||||
'Shlink' => [
|
'Shlink' => [
|
||||||
// For swoole, send logs as stream
|
'type' => LoggerType::STREAM->value,
|
||||||
'type' => $isSwoole ? LoggerType::STREAM->value : LoggerType::FILE->value,
|
'destination' => 'php://stderr',
|
||||||
'level' => Level::Debug->value,
|
'level' => Level::Debug->value,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use Mezzio\ProblemDetails;
|
|||||||
use Mezzio\Router;
|
use Mezzio\Router;
|
||||||
use PhpMiddleware\RequestId\RequestIdMiddleware;
|
use PhpMiddleware\RequestId\RequestIdMiddleware;
|
||||||
use RKA\Middleware\IpAddress;
|
use RKA\Middleware\IpAddress;
|
||||||
|
use Shlinkio\Shlink\Common\Middleware\AccessLogMiddleware;
|
||||||
use Shlinkio\Shlink\Common\Middleware\ContentLengthMiddleware;
|
use Shlinkio\Shlink\Common\Middleware\ContentLengthMiddleware;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -16,6 +17,7 @@ return [
|
|||||||
'middleware_pipeline' => [
|
'middleware_pipeline' => [
|
||||||
'error-handler' => [
|
'error-handler' => [
|
||||||
'middleware' => [
|
'middleware' => [
|
||||||
|
AccessLogMiddleware::class,
|
||||||
ContentLengthMiddleware::class,
|
ContentLengthMiddleware::class,
|
||||||
RequestIdMiddleware::class,
|
RequestIdMiddleware::class,
|
||||||
ErrorHandler::class,
|
ErrorHandler::class,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ return (static function (): array {
|
|||||||
Action\Visit\DomainVisitsAction::getRouteDef(),
|
Action\Visit\DomainVisitsAction::getRouteDef(),
|
||||||
Action\Visit\GlobalVisitsAction::getRouteDef(),
|
Action\Visit\GlobalVisitsAction::getRouteDef(),
|
||||||
Action\Visit\OrphanVisitsAction::getRouteDef(),
|
Action\Visit\OrphanVisitsAction::getRouteDef(),
|
||||||
|
Action\Visit\DeleteOrphanVisitsAction::getRouteDef(),
|
||||||
Action\Visit\NonOrphanVisitsAction::getRouteDef(),
|
Action\Visit\NonOrphanVisitsAction::getRouteDef(),
|
||||||
|
|
||||||
// Short URLs
|
// Short URLs
|
||||||
@@ -53,6 +54,7 @@ return (static function (): array {
|
|||||||
]),
|
]),
|
||||||
Action\ShortUrl\EditShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
Action\ShortUrl\EditShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||||
Action\ShortUrl\DeleteShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
Action\ShortUrl\DeleteShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||||
|
Action\ShortUrl\DeleteShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]),
|
||||||
Action\ShortUrl\ResolveShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
Action\ShortUrl\ResolveShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||||
Action\ShortUrl\ListShortUrlsAction::getRouteDef(),
|
Action\ShortUrl\ListShortUrlsAction::getRouteDef(),
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ return (static function (): array {
|
|||||||
MIN_SHORT_CODES_LENGTH,
|
MIN_SHORT_CODES_LENGTH,
|
||||||
);
|
);
|
||||||
$modeFromEnv = EnvVars::SHORT_URL_MODE->loadFromEnv(ShortUrlMode::STRICT->value);
|
$modeFromEnv = EnvVars::SHORT_URL_MODE->loadFromEnv(ShortUrlMode::STRICT->value);
|
||||||
$mode = ShortUrlMode::tryFrom($modeFromEnv) ?? ShortUrlMode::STRICT;
|
$mode = ShortUrlMode::tryDeprecated($modeFromEnv) ?? ShortUrlMode::STRICT;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ const DEFAULT_REDIRECT_STATUS_CODE = RedirectStatus::STATUS_302; // Deprecated.
|
|||||||
const DEFAULT_REDIRECT_CACHE_LIFETIME = 30;
|
const DEFAULT_REDIRECT_CACHE_LIFETIME = 30;
|
||||||
const LOCAL_LOCK_FACTORY = 'Shlinkio\Shlink\LocalLockFactory';
|
const LOCAL_LOCK_FACTORY = 'Shlinkio\Shlink\LocalLockFactory';
|
||||||
const TITLE_TAG_VALUE = '/<title[^>]*>(.*?)<\/title>/i'; // Matches the value inside a html title tag
|
const TITLE_TAG_VALUE = '/<title[^>]*>(.*?)<\/title>/i'; // Matches the value inside a html title tag
|
||||||
|
const LOOSE_URI_MATCHER = '/(.+)\:(.+)/i'; // Matches anything starting with a schema.
|
||||||
const DEFAULT_QR_CODE_SIZE = 300;
|
const DEFAULT_QR_CODE_SIZE = 300;
|
||||||
const DEFAULT_QR_CODE_MARGIN = 0;
|
const DEFAULT_QR_CODE_MARGIN = 0;
|
||||||
const DEFAULT_QR_CODE_FORMAT = 'png';
|
const DEFAULT_QR_CODE_FORMAT = 'png';
|
||||||
const DEFAULT_QR_CODE_ERROR_CORRECTION = 'l';
|
const DEFAULT_QR_CODE_ERROR_CORRECTION = 'l';
|
||||||
const DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = true;
|
const DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = true;
|
||||||
const MIN_TASK_WORKERS = 4;
|
const MIN_TASK_WORKERS = 4;
|
||||||
const MIGRATIONS_TABLE = 'migrations';
|
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ chdir(dirname(__DIR__));
|
|||||||
|
|
||||||
require 'vendor/autoload.php';
|
require 'vendor/autoload.php';
|
||||||
|
|
||||||
|
// Workaround to make this compatible with both openswoole 22 and earlier versions.
|
||||||
|
if (! function_exists('swoole_set_process_name')) {
|
||||||
|
// phpcs:disable
|
||||||
|
function swoole_set_process_name(string $name): void
|
||||||
|
{
|
||||||
|
OpenSwoole\Util::setProcessName($name);
|
||||||
|
}
|
||||||
|
// phpcs:enable
|
||||||
|
}
|
||||||
|
|
||||||
// This is one of the first files loaded. Configure the timezone here
|
// This is one of the first files loaded. Configure the timezone here
|
||||||
date_default_timezone_set(EnvVars::TIMEZONE->loadFromEnv(date_default_timezone_get()));
|
date_default_timezone_set(EnvVars::TIMEZONE->loadFromEnv(date_default_timezone_get()));
|
||||||
|
|
||||||
@@ -21,7 +31,6 @@ if (! class_exists(LOCAL_LOCK_FACTORY)) {
|
|||||||
class_alias(Lock\LockFactory::class, LOCAL_LOCK_FACTORY);
|
class_alias(Lock\LockFactory::class, LOCAL_LOCK_FACTORY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build container
|
|
||||||
return (static function (): ServiceManager {
|
return (static function (): ServiceManager {
|
||||||
$config = require __DIR__ . '/config.php';
|
$config = require __DIR__ . '/config.php';
|
||||||
$container = new ServiceManager($config['dependencies']);
|
$container = new ServiceManager($config['dependencies']);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
version: '2.7'
|
version: '3.0'
|
||||||
|
|
||||||
rpc:
|
rpc:
|
||||||
listen: tcp://127.0.0.1:6001
|
listen: tcp://127.0.0.1:6001
|
||||||
@@ -14,10 +14,12 @@ http:
|
|||||||
forbid: ['.php', '.htaccess']
|
forbid: ['.php', '.htaccess']
|
||||||
pool:
|
pool:
|
||||||
num_workers: 1
|
num_workers: 1
|
||||||
|
debug: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pool:
|
pool:
|
||||||
num_workers: 1
|
num_workers: 1
|
||||||
|
debug: true
|
||||||
timeout: 300
|
timeout: 300
|
||||||
consume: ['shlink']
|
consume: ['shlink']
|
||||||
pipelines:
|
pipelines:
|
||||||
@@ -31,19 +33,8 @@ logs:
|
|||||||
mode: development
|
mode: development
|
||||||
channels:
|
channels:
|
||||||
http:
|
http:
|
||||||
level: debug
|
mode: 'off' # Disable logging as Shlink handles it internally
|
||||||
server:
|
server:
|
||||||
level: debug
|
level: debug
|
||||||
metrics:
|
metrics:
|
||||||
level: debug
|
level: debug
|
||||||
|
|
||||||
reload:
|
|
||||||
interval: 1s
|
|
||||||
patterns: ['.php']
|
|
||||||
services:
|
|
||||||
http:
|
|
||||||
dirs: ['../../bin', '../../config', '../../data/migrations', '../../module', '../../vendor']
|
|
||||||
recursive: true
|
|
||||||
jobs:
|
|
||||||
dirs: ['../../bin', '../../config', '../../data/migrations', '../../module', '../../vendor']
|
|
||||||
recursive: true
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
version: '2.7'
|
version: '3.0'
|
||||||
|
|
||||||
rpc:
|
rpc:
|
||||||
listen: tcp://127.0.0.1:6001
|
listen: tcp://127.0.0.1:6001
|
||||||
@@ -7,18 +7,18 @@ server:
|
|||||||
command: 'php -dopcache.enable_cli=1 -dopcache.validate_timestamps=0 ../../bin/roadrunner-worker.php'
|
command: 'php -dopcache.enable_cli=1 -dopcache.validate_timestamps=0 ../../bin/roadrunner-worker.php'
|
||||||
|
|
||||||
http:
|
http:
|
||||||
address: '0.0.0.0:${PORT}'
|
address: '0.0.0.0:${PORT:-8080}'
|
||||||
middleware: ['static']
|
middleware: ['static']
|
||||||
static:
|
static:
|
||||||
dir: '../../public'
|
dir: '../../public'
|
||||||
forbid: ['.php', '.htaccess']
|
forbid: ['.php', '.htaccess']
|
||||||
pool:
|
pool:
|
||||||
num_workers: ${WEB_WORKER_NUM}
|
num_workers: ${WEB_WORKER_NUM:-0}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
timeout: 300 # 5 minutes
|
timeout: 300 # 5 minutes
|
||||||
pool:
|
pool:
|
||||||
num_workers: ${TASK_WORKER_NUM}
|
num_workers: ${TASK_WORKER_NUM:-0}
|
||||||
consume: ['shlink']
|
consume: ['shlink']
|
||||||
pipelines:
|
pipelines:
|
||||||
shlink:
|
shlink:
|
||||||
@@ -31,6 +31,6 @@ logs:
|
|||||||
mode: production
|
mode: production
|
||||||
channels:
|
channels:
|
||||||
http:
|
http:
|
||||||
level: info # Log all http requests, set to info to disable
|
mode: 'off' # Disable logging as Shlink handles it internally
|
||||||
server:
|
server:
|
||||||
level: debug # Everything written to worker stderr is logged
|
level: debug # Everything written to worker stderr is logged
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ if (file_exists($covFile)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$testHelper->createTestDb(
|
$testHelper->createTestDb(
|
||||||
['bin/cli', 'db:create'],
|
createDbCommand: ['bin/cli', 'db:create'],
|
||||||
['bin/cli', 'db:migrate'],
|
migrateDbCommand: ['bin/cli', 'db:migrate'],
|
||||||
['bin/doctrine', 'orm:schema-tool:drop'],
|
dropSchemaCommand: ['bin/doctrine', 'orm:schema-tool:drop'],
|
||||||
['bin/doctrine', 'dbal:run-sql'],
|
runSqlCommand: ['bin/doctrine', 'dbal:run-sql'],
|
||||||
);
|
);
|
||||||
CliTest\CliTestCase::setSeedFixturesCallback(
|
CliTest\CliTestCase::setSeedFixturesCallback(
|
||||||
static fn () => $testHelper->seedFixtures($em, $config['data_fixtures'] ?? []),
|
static fn () => $testHelper->seedFixtures($em, $config['data_fixtures'] ?? []),
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ $buildTestLoggerConfig = static fn (string $filename) => [
|
|||||||
'level' => Level::Debug->value,
|
'level' => Level::Debug->value,
|
||||||
'type' => LoggerType::STREAM->value,
|
'type' => LoggerType::STREAM->value,
|
||||||
'destination' => sprintf('data/log/api-tests/%s', $filename),
|
'destination' => sprintf('data/log/api-tests/%s', $filename),
|
||||||
|
'add_new_line' => true,
|
||||||
];
|
];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
|
curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
|
||||||
curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list > /etc/apt/sources.list.d/mssql-release.list
|
curl https://packages.microsoft.com/config/ubuntu/22.04/prod.list > /etc/apt/sources.list.d/mssql-release.list
|
||||||
apt-get update
|
apt-get update
|
||||||
ACCEPT_EULA=Y apt-get install msodbcsql17
|
ACCEPT_EULA=Y apt-get install msodbcsql18
|
||||||
apt-get install unixodbc-dev
|
# apt-get install unixodbc-dev
|
||||||
|
|||||||
@@ -71,6 +71,6 @@ CMD \
|
|||||||
# Install dependencies if the vendor dir does not exist
|
# Install dependencies if the vendor dir does not exist
|
||||||
if [[ ! -d "./vendor" ]]; then /usr/local/bin/composer install ; fi && \
|
if [[ ! -d "./vendor" ]]; then /usr/local/bin/composer install ; fi && \
|
||||||
# Download roadrunner binary
|
# Download roadrunner binary
|
||||||
if [[ ! -f "./bin/rr" ]]; then ./vendor/bin/rr get --no-interaction --location bin/ && chmod +x bin/rr ; fi && \
|
if [[ ! -f "./bin/rr" ]]; then ./vendor/bin/rr get --no-interaction --no-config --location bin/ && chmod +x bin/rr ; fi && \
|
||||||
# This forces the app to be started every second until the exit code is 0
|
# This forces the app to be started every second until the exit code is 0
|
||||||
until ./bin/rr serve -c config/roadrunner/.rr.dev.yml; do sleep 1 ; done
|
until ./bin/rr serve -c config/roadrunner/.rr.dev.yml; do sleep 1 ; done
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
|||||||
|
|
||||||
ENV APCU_VERSION 5.1.21
|
ENV APCU_VERSION 5.1.21
|
||||||
ENV INOTIFY_VERSION 3.0.0
|
ENV INOTIFY_VERSION 3.0.0
|
||||||
ENV OPENSWOOLE_VERSION 4.12.1
|
ENV OPENSWOOLE_VERSION 22.0.0
|
||||||
ENV PDO_SQLSRV_VERSION 5.10.1
|
ENV PDO_SQLSRV_VERSION 5.10.1
|
||||||
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
||||||
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
||||||
|
|||||||
50
data/migrations/Version20230130090946.php
Normal file
50
data/migrations/Version20230130090946.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||||
|
use Doctrine\DBAL\Platforms\SQLServerPlatform;
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20230130090946 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->skipIf(! $this->isMsSql(), 'This only sets MsSQL-specific database options');
|
||||||
|
|
||||||
|
$shortUrls = $schema->getTable('short_urls');
|
||||||
|
$shortCode = $shortUrls->getColumn('short_code');
|
||||||
|
// Drop the unique index before changing the collation, as the field is part of this index
|
||||||
|
$shortUrls->dropIndex('unique_short_code_plus_domain');
|
||||||
|
$shortCode->setPlatformOption('collation', 'Latin1_General_CS_AS');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function postUp(Schema $schema): void
|
||||||
|
{
|
||||||
|
if ($this->isMsSql()) {
|
||||||
|
// The index needs to be re-created in postUp, but here, we can only use statements run against the
|
||||||
|
// connection directly
|
||||||
|
$this->connection->executeStatement(
|
||||||
|
'CREATE INDEX unique_short_code_plus_domain ON short_urls (domain_id, short_code);',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// No down
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isTransactional(): bool
|
||||||
|
{
|
||||||
|
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isMsSql(): bool
|
||||||
|
{
|
||||||
|
return $this->connection->getDatabasePlatform() instanceof SQLServerPlatform;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
data/migrations/Version20230211171904.php
Normal file
27
data/migrations/Version20230211171904.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20230211171904 extends AbstractMigration
|
||||||
|
{
|
||||||
|
private const INDEX_NAME = 'IDX_visits_potential_bot';
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$visits = $schema->getTable('visits');
|
||||||
|
$this->skipIf($visits->hasIndex(self::INDEX_NAME));
|
||||||
|
|
||||||
|
$visits->addIndex(['short_url_id', 'potential_bot'], self::INDEX_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isTransactional(): bool
|
||||||
|
{
|
||||||
|
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
data/migrations/Version20230303164233.php
Normal file
28
data/migrations/Version20230303164233.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20230303164233 extends AbstractMigration
|
||||||
|
{
|
||||||
|
private const INDEX_NAME = 'visits_potential_bot_IDX';
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$visits = $schema->getTable('visits');
|
||||||
|
$this->skipIf($visits->hasIndex(self::INDEX_NAME));
|
||||||
|
|
||||||
|
$visits->dropIndex('IDX_visits_potential_bot'); // Old index
|
||||||
|
$visits->addIndex(['potential_bot'], self::INDEX_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isTransactional(): bool
|
||||||
|
{
|
||||||
|
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
log_errors_max_len=0
|
log_errors_max_len=0
|
||||||
zend.assertions=1
|
zend.assertions=1
|
||||||
assert.exception=1
|
assert.exception=1
|
||||||
memory_limit=256M
|
memory_limit=512M
|
||||||
|
|||||||
@@ -6,14 +6,12 @@ namespace Shlinkio\Shlink;
|
|||||||
|
|
||||||
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
||||||
|
|
||||||
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'logger' => [
|
'logger' => [
|
||||||
'Shlink' => [
|
'Shlink' => [
|
||||||
'type' => LoggerType::STREAM->value,
|
'type' => LoggerType::STREAM->value,
|
||||||
'destination' => runningInRoadRunner() ? 'php://stderr' : 'php://stdout',
|
'destination' => 'php://stderr',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,25 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# If SHELL_VERBOSITY was not explicitly provided, run commands in quite mode (-q)
|
|
||||||
[ $SHELL_VERBOSITY ] && flags="" || flags="-q"
|
|
||||||
|
|
||||||
cd /etc/shlink
|
cd /etc/shlink
|
||||||
|
|
||||||
echo "Creating fresh database if needed..."
|
flags="--clear-db-cache"
|
||||||
php bin/cli db:create -n ${flags}
|
|
||||||
|
|
||||||
echo "Updating database..."
|
# Skip downloading GeoLite2 db file if the license key env var was not defined or skipping was explicitly set
|
||||||
php bin/cli db:migrate -n ${flags}
|
if [ -z "${GEOLITE_LICENSE_KEY}" ] || [ "${SKIP_INITIAL_GEOLITE_DOWNLOAD}" == "true" ]; then
|
||||||
|
flags="${flags} --skip-download-geolite"
|
||||||
echo "Generating proxies..."
|
|
||||||
php bin/doctrine orm:generate-proxies -n ${flags}
|
|
||||||
|
|
||||||
echo "Clearing entities cache..."
|
|
||||||
php bin/doctrine orm:clear-cache:metadata -n ${flags}
|
|
||||||
|
|
||||||
# Try to download GeoLite2 db file only if the license key env var was defined and skipping was not explicitly set
|
|
||||||
if [ ! -z "${GEOLITE_LICENSE_KEY}" ] && [ "${SKIP_INITIAL_GEOLITE_DOWNLOAD}" != "true" ]; then
|
|
||||||
echo "Downloading GeoLite2 db file..."
|
|
||||||
php bin/cli visit:download-db -n ${flags}
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Periodically run visit:locate every hour, if ENABLE_PERIODIC_VISIT_LOCATE=true was provided
|
php vendor/bin/shlink-installer init ${flags}
|
||||||
if [ "${ENABLE_PERIODIC_VISIT_LOCATE}" = "true" ]; then
|
|
||||||
|
# Periodically run visit:locate every hour, if ENABLE_PERIODIC_VISIT_LOCATE=true was provided and running as root
|
||||||
|
# ENABLE_PERIODIC_VISIT_LOCATE is deprecated. Remove cron support in Shlink 4.0.0
|
||||||
|
if [ "${ENABLE_PERIODIC_VISIT_LOCATE}" = "true" ] && [ "${SHLINK_USER_ID}" = "root" ]; then
|
||||||
echo "Configuring periodic visit location..."
|
echo "Configuring periodic visit location..."
|
||||||
echo "0 * * * * php /etc/shlink/bin/cli visit:locate -q" > /etc/crontabs/root
|
echo "0 * * * * php /etc/shlink/bin/cli visit:locate -q" > /etc/crontabs/root
|
||||||
/usr/sbin/crond &
|
/usr/sbin/crond &
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# RoadRunner config needs these to have been set, so falling back to default values if not set yet
|
|
||||||
if [ "$SHLINK_RUNTIME" == 'rr' ]; then
|
|
||||||
export PORT="${PORT:-"8080"}"
|
|
||||||
# Default to 0 so that RoadRunner decides the number of workers based on the amount of logical CPUs
|
|
||||||
export WEB_WORKER_NUM="${WEB_WORKER_NUM:-"0"}"
|
|
||||||
export TASK_WORKER_NUM="${TASK_WORKER_NUM:-"0"}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then
|
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then
|
||||||
# When restarting the container, openswoole might think it is already in execution
|
# When restarting the container, openswoole might think it is already in execution
|
||||||
# This forces the app to be started every second until the exit code is 0
|
# This forces the app to be started every second until the exit code is 0
|
||||||
|
|||||||
9
docs/swagger/parameters/shortCode.json
Normal file
9
docs/swagger/parameters/shortCode.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "shortCode",
|
||||||
|
"in": "path",
|
||||||
|
"description": "The short code for the short URL.",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,13 +11,7 @@
|
|||||||
"$ref": "../parameters/version.json"
|
"$ref": "../parameters/version.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "shortCode",
|
"$ref": "../parameters/shortCode.json"
|
||||||
"in": "path",
|
|
||||||
"description": "The short code to resolve.",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"$ref": "../parameters/domain.json"
|
"$ref": "../parameters/domain.json"
|
||||||
@@ -127,13 +121,7 @@
|
|||||||
"$ref": "../parameters/version.json"
|
"$ref": "../parameters/version.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "shortCode",
|
"$ref": "../parameters/shortCode.json"
|
||||||
"in": "path",
|
|
||||||
"description": "The short code to edit.",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"$ref": "../parameters/domain.json"
|
"$ref": "../parameters/domain.json"
|
||||||
@@ -295,13 +283,7 @@
|
|||||||
"$ref": "../parameters/version.json"
|
"$ref": "../parameters/version.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "shortCode",
|
"$ref": "../parameters/shortCode.json"
|
||||||
"in": "path",
|
|
||||||
"description": "The short code to edit.",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"$ref": "../parameters/domain.json"
|
"$ref": "../parameters/domain.json"
|
||||||
|
|||||||
@@ -11,13 +11,7 @@
|
|||||||
"$ref": "../parameters/version.json"
|
"$ref": "../parameters/version.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "shortCode",
|
"$ref": "../parameters/shortCode.json"
|
||||||
"in": "path",
|
|
||||||
"description": "The short code for the short URL from which we want to get the visits.",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"$ref": "../parameters/domain.json"
|
"$ref": "../parameters/domain.json"
|
||||||
@@ -172,5 +166,79 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"delete": {
|
||||||
|
"operationId": "deleteShortUrlVisits",
|
||||||
|
"tags": [
|
||||||
|
"Visits"
|
||||||
|
],
|
||||||
|
"summary": "Delete visits for short URL",
|
||||||
|
"description": "Delete all existing visits on the short URL behind provided short code.",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"$ref": "../parameters/version.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "../parameters/shortCode.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "../parameters/domain.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKey": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Deleted visits",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"deletedVisits": {
|
||||||
|
"description": "Amount of affected visits",
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"deletedVisits": 536
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "The short code does not belong to any short URL.",
|
||||||
|
"content": {
|
||||||
|
"application/problem+json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "../definitions/Error.json"
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"Short URL not found with API v3 and newer": {
|
||||||
|
"$ref": "../examples/short-url-not-found-v3.json"
|
||||||
|
},
|
||||||
|
"Short URL not found previous to API v3": {
|
||||||
|
"$ref": "../examples/short-url-not-found-v2.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"description": "Unexpected error.",
|
||||||
|
"content": {
|
||||||
|
"application/problem+json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "../definitions/Error.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,5 +148,55 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"delete": {
|
||||||
|
"operationId": "deleteOrphanVisits",
|
||||||
|
"tags": [
|
||||||
|
"Visits"
|
||||||
|
],
|
||||||
|
"summary": "Delete orphan visits",
|
||||||
|
"description": "Delete all visits to invalid short URLs, the base URL or any other 404.",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"$ref": "../parameters/version.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKey": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Deleted visits",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"deletedVisits": {
|
||||||
|
"description": "Amount of affected visits",
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"deletedVisits": 536
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"description": "Unexpected error.",
|
||||||
|
"content": {
|
||||||
|
"application/problem+json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "../definitions/Error.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,7 @@
|
|||||||
"description": "Represents a short URL. Tracks the visit and redirects tio the corresponding long URL",
|
"description": "Represents a short URL. Tracks the visit and redirects tio the corresponding long URL",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "shortCode",
|
"$ref": "../parameters/shortCode.json"
|
||||||
"in": "path",
|
|
||||||
"description": "The short code to resolve.",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
|||||||
@@ -8,13 +8,7 @@
|
|||||||
"description": "Generates a QR code image pointing to a short URL.<br />Since this is not an API endpoint but an image one, when an invalid value is provided for any of the query params, they will fall to their default values instead of throwing an error.",
|
"description": "Generates a QR code image pointing to a short URL.<br />Since this is not an API endpoint but an image one, when an invalid value is provided for any of the query params, they will fall to their default values instead of throwing an error.",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "shortCode",
|
"$ref": "../parameters/shortCode.json"
|
||||||
"in": "path",
|
|
||||||
"description": "The short code to resolve.",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "size",
|
"name": "size",
|
||||||
|
|||||||
@@ -8,13 +8,7 @@
|
|||||||
"description": "Generates a 1px transparent image which can be used to track emails with a short URL",
|
"description": "Generates a 1px transparent image which can be used to track emails with a short URL",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "shortCode",
|
"$ref": "../parameters/shortCode.json"
|
||||||
"in": "path",
|
|
||||||
"description": "The short code to resolve.",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
|||||||
@@ -2,15 +2,13 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use const Shlinkio\Shlink\MIGRATIONS_TABLE;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'migrations_paths' => [
|
'migrations_paths' => [
|
||||||
'ShlinkMigrations' => 'data/migrations',
|
'ShlinkMigrations' => 'data/migrations',
|
||||||
],
|
],
|
||||||
'table_storage' => [
|
'table_storage' => [
|
||||||
'table_name' => MIGRATIONS_TABLE,
|
'table_name' => 'migrations',
|
||||||
],
|
],
|
||||||
'custom_template' => 'data/migrations_template.txt',
|
'custom_template' => 'data/migrations_template.txt',
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ return [
|
|||||||
Command\ShortUrl\ListShortUrlsCommand::NAME => Command\ShortUrl\ListShortUrlsCommand::class,
|
Command\ShortUrl\ListShortUrlsCommand::NAME => Command\ShortUrl\ListShortUrlsCommand::class,
|
||||||
Command\ShortUrl\GetShortUrlVisitsCommand::NAME => Command\ShortUrl\GetShortUrlVisitsCommand::class,
|
Command\ShortUrl\GetShortUrlVisitsCommand::NAME => Command\ShortUrl\GetShortUrlVisitsCommand::class,
|
||||||
Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class,
|
Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class,
|
||||||
|
Command\ShortUrl\DeleteShortUrlVisitsCommand::NAME => Command\ShortUrl\DeleteShortUrlVisitsCommand::class,
|
||||||
|
|
||||||
Command\Visit\LocateVisitsCommand::NAME => Command\Visit\LocateVisitsCommand::class,
|
Command\Visit\LocateVisitsCommand::NAME => Command\Visit\LocateVisitsCommand::class,
|
||||||
Command\Visit\DownloadGeoLiteDbCommand::NAME => Command\Visit\DownloadGeoLiteDbCommand::class,
|
Command\Visit\DownloadGeoLiteDbCommand::NAME => Command\Visit\DownloadGeoLiteDbCommand::class,
|
||||||
Command\Visit\GetOrphanVisitsCommand::NAME => Command\Visit\GetOrphanVisitsCommand::class,
|
Command\Visit\GetOrphanVisitsCommand::NAME => Command\Visit\GetOrphanVisitsCommand::class,
|
||||||
|
Command\Visit\DeleteOrphanVisitsCommand::NAME => Command\Visit\DeleteOrphanVisitsCommand::class,
|
||||||
Command\Visit\GetNonOrphanVisitsCommand::NAME => Command\Visit\GetNonOrphanVisitsCommand::class,
|
Command\Visit\GetNonOrphanVisitsCommand::NAME => Command\Visit\GetNonOrphanVisitsCommand::class,
|
||||||
|
|
||||||
Command\Api\GenerateKeyCommand::NAME => Command\Api\GenerateKeyCommand::class,
|
Command\Api\GenerateKeyCommand::NAME => Command\Api\GenerateKeyCommand::class,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI;
|
namespace Shlinkio\Shlink\CLI;
|
||||||
|
|
||||||
use Doctrine\DBAL\Connection;
|
|
||||||
use GeoIp2\Database\Reader;
|
use GeoIp2\Database\Reader;
|
||||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||||
@@ -43,10 +42,12 @@ return [
|
|||||||
Command\ShortUrl\ListShortUrlsCommand::class => ConfigAbstractFactory::class,
|
Command\ShortUrl\ListShortUrlsCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\ShortUrl\GetShortUrlVisitsCommand::class => ConfigAbstractFactory::class,
|
Command\ShortUrl\GetShortUrlVisitsCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
|
Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
|
||||||
|
Command\ShortUrl\DeleteShortUrlVisitsCommand::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Command\Visit\DownloadGeoLiteDbCommand::class => ConfigAbstractFactory::class,
|
Command\Visit\DownloadGeoLiteDbCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\Visit\LocateVisitsCommand::class => ConfigAbstractFactory::class,
|
Command\Visit\LocateVisitsCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\Visit\GetOrphanVisitsCommand::class => ConfigAbstractFactory::class,
|
Command\Visit\GetOrphanVisitsCommand::class => ConfigAbstractFactory::class,
|
||||||
|
Command\Visit\DeleteOrphanVisitsCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\Visit\GetNonOrphanVisitsCommand::class => ConfigAbstractFactory::class,
|
Command\Visit\GetNonOrphanVisitsCommand::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Command\Api\GenerateKeyCommand::class => ConfigAbstractFactory::class,
|
Command\Api\GenerateKeyCommand::class => ConfigAbstractFactory::class,
|
||||||
@@ -89,6 +90,7 @@ return [
|
|||||||
],
|
],
|
||||||
Command\ShortUrl\GetShortUrlVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
Command\ShortUrl\GetShortUrlVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
||||||
Command\ShortUrl\DeleteShortUrlCommand::class => [ShortUrl\DeleteShortUrlService::class],
|
Command\ShortUrl\DeleteShortUrlCommand::class => [ShortUrl\DeleteShortUrlService::class],
|
||||||
|
Command\ShortUrl\DeleteShortUrlVisitsCommand::class => [ShortUrl\ShortUrlVisitsDeleter::class],
|
||||||
|
|
||||||
Command\Visit\DownloadGeoLiteDbCommand::class => [GeoLite\GeolocationDbUpdater::class],
|
Command\Visit\DownloadGeoLiteDbCommand::class => [GeoLite\GeolocationDbUpdater::class],
|
||||||
Command\Visit\LocateVisitsCommand::class => [
|
Command\Visit\LocateVisitsCommand::class => [
|
||||||
@@ -97,6 +99,7 @@ return [
|
|||||||
LockFactory::class,
|
LockFactory::class,
|
||||||
],
|
],
|
||||||
Command\Visit\GetOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
Command\Visit\GetOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
||||||
|
Command\Visit\DeleteOrphanVisitsCommand::class => [Visit\VisitsDeleter::class],
|
||||||
Command\Visit\GetNonOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class, ShortUrlStringifier::class],
|
Command\Visit\GetNonOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class, ShortUrlStringifier::class],
|
||||||
|
|
||||||
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class, ApiKey\RoleResolver::class],
|
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class, ApiKey\RoleResolver::class],
|
||||||
@@ -116,7 +119,7 @@ return [
|
|||||||
LockFactory::class,
|
LockFactory::class,
|
||||||
Util\ProcessRunner::class,
|
Util\ProcessRunner::class,
|
||||||
PhpExecutableFinder::class,
|
PhpExecutableFinder::class,
|
||||||
Connection::class,
|
'em',
|
||||||
NoDbNameConnectionFactory::SERVICE_NAME,
|
NoDbNameConnectionFactory::SERVICE_NAME,
|
||||||
],
|
],
|
||||||
Command\Db\MigrateDatabaseCommand::class => [
|
Command\Db\MigrateDatabaseCommand::class => [
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
@@ -39,10 +39,10 @@ class DisableKeyCommand extends Command
|
|||||||
try {
|
try {
|
||||||
$this->apiKeyService->disable($apiKey);
|
$this->apiKeyService->disable($apiKey);
|
||||||
$io->success(sprintf('API key "%s" properly disabled', $apiKey));
|
$io->success(sprintf('API key "%s" properly disabled', $apiKey));
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
return ExitCode::EXIT_SUCCESS;
|
||||||
} catch (InvalidArgumentException $e) {
|
} catch (InvalidArgumentException $e) {
|
||||||
$io->error($e->getMessage());
|
$io->error($e->getMessage());
|
||||||
return ExitCodes::EXIT_FAILURE;
|
return ExitCode::EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ namespace Shlinkio\Shlink\CLI\Command\Api;
|
|||||||
|
|
||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
use Shlinkio\Shlink\CLI\ApiKey\RoleResolverInterface;
|
use Shlinkio\Shlink\CLI\ApiKey\RoleResolverInterface;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||||
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
||||||
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
@@ -99,7 +100,7 @@ class GenerateKeyCommand extends Command
|
|||||||
$io = new SymfonyStyle($input, $output);
|
$io = new SymfonyStyle($input, $output);
|
||||||
$io->success(sprintf('Generated API key: "%s"', $apiKey->toString()));
|
$io->success(sprintf('Generated API key: "%s"', $apiKey->toString()));
|
||||||
|
|
||||||
if (! $apiKey->isAdmin()) {
|
if (! ApiKey::isAdmin($apiKey)) {
|
||||||
ShlinkTable::default($io)->render(
|
ShlinkTable::default($io)->render(
|
||||||
['Role name', 'Role metadata'],
|
['Role name', 'Role metadata'],
|
||||||
$apiKey->mapRoles(fn (Role $role, array $meta) => [$role->value, arrayToString($meta, 0)]),
|
$apiKey->mapRoles(fn (Role $role, array $meta) => [$role->value, arrayToString($meta, 0)]),
|
||||||
@@ -108,6 +109,6 @@ class GenerateKeyCommand extends Command
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
return ExitCode::EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||||
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
||||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
@@ -59,7 +59,7 @@ class ListKeysCommand extends Command
|
|||||||
$rowData[] = sprintf($messagePattern, $this->getEnabledSymbol($apiKey));
|
$rowData[] = sprintf($messagePattern, $this->getEnabledSymbol($apiKey));
|
||||||
}
|
}
|
||||||
$rowData[] = $expiration?->toAtomString() ?? '-';
|
$rowData[] = $expiration?->toAtomString() ?? '-';
|
||||||
$rowData[] = $apiKey->isAdmin() ? 'Admin' : implode("\n", $apiKey->mapRoles(
|
$rowData[] = ApiKey::isAdmin($apiKey) ? 'Admin' : implode("\n", $apiKey->mapRoles(
|
||||||
fn (Role $role, array $meta) =>
|
fn (Role $role, array $meta) =>
|
||||||
empty($meta)
|
empty($meta)
|
||||||
? $role->toFriendlyName()
|
? $role->toFriendlyName()
|
||||||
@@ -77,7 +77,7 @@ class ListKeysCommand extends Command
|
|||||||
'Roles',
|
'Roles',
|
||||||
]), $rows);
|
]), $rows);
|
||||||
|
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
return ExitCode::EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function determineMessagePattern(ApiKey $apiKey): string
|
private function determineMessagePattern(ApiKey $apiKey): string
|
||||||
|
|||||||
@@ -5,22 +5,25 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink\CLI\Command\Db;
|
namespace Shlinkio\Shlink\CLI\Command\Db;
|
||||||
|
|
||||||
use Doctrine\DBAL\Connection;
|
use Doctrine\DBAL\Connection;
|
||||||
use Doctrine\DBAL\Platforms\SqlitePlatform;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||||
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Shlinkio\Shlink\CLI\Util\ProcessRunnerInterface;
|
use Shlinkio\Shlink\CLI\Util\ProcessRunnerInterface;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
use Symfony\Component\Lock\LockFactory;
|
use Symfony\Component\Lock\LockFactory;
|
||||||
use Symfony\Component\Process\PhpExecutableFinder;
|
use Symfony\Component\Process\PhpExecutableFinder;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
use function Functional\contains;
|
use function Functional\contains;
|
||||||
use function Functional\filter;
|
use function Functional\map;
|
||||||
|
use function Functional\some;
|
||||||
use const Shlinkio\Shlink\MIGRATIONS_TABLE;
|
|
||||||
|
|
||||||
class CreateDatabaseCommand extends AbstractDatabaseCommand
|
class CreateDatabaseCommand extends AbstractDatabaseCommand
|
||||||
{
|
{
|
||||||
|
private readonly Connection $regularConn;
|
||||||
|
|
||||||
public const NAME = 'db:create';
|
public const NAME = 'db:create';
|
||||||
public const DOCTRINE_SCRIPT = 'bin/doctrine';
|
public const DOCTRINE_SCRIPT = 'bin/doctrine';
|
||||||
public const DOCTRINE_CREATE_SCHEMA_COMMAND = 'orm:schema-tool:create';
|
public const DOCTRINE_CREATE_SCHEMA_COMMAND = 'orm:schema-tool:create';
|
||||||
@@ -29,9 +32,10 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
|
|||||||
LockFactory $locker,
|
LockFactory $locker,
|
||||||
ProcessRunnerInterface $processRunner,
|
ProcessRunnerInterface $processRunner,
|
||||||
PhpExecutableFinder $phpFinder,
|
PhpExecutableFinder $phpFinder,
|
||||||
private Connection $regularConn,
|
private readonly EntityManagerInterface $em,
|
||||||
private Connection $noDbNameConn,
|
private readonly Connection $noDbNameConn,
|
||||||
) {
|
) {
|
||||||
|
$this->regularConn = $this->em->getConnection();
|
||||||
parent::__construct($locker, $processRunner, $phpFinder);
|
parent::__construct($locker, $processRunner, $phpFinder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,11 +53,9 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
|
|||||||
{
|
{
|
||||||
$io = new SymfonyStyle($input, $output);
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
$this->checkDbExists();
|
if ($this->databaseTablesExist()) {
|
||||||
|
|
||||||
if ($this->schemaExists()) {
|
|
||||||
$io->success('Database already exists. Run "db:migrate" command to make sure it is up to date.');
|
$io->success('Database already exists. Run "db:migrate" command to make sure it is up to date.');
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
return ExitCode::EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create database
|
// Create database
|
||||||
@@ -61,32 +63,34 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
|
|||||||
$this->runPhpCommand($output, [self::DOCTRINE_SCRIPT, self::DOCTRINE_CREATE_SCHEMA_COMMAND]);
|
$this->runPhpCommand($output, [self::DOCTRINE_SCRIPT, self::DOCTRINE_CREATE_SCHEMA_COMMAND]);
|
||||||
$io->success('Database properly created!');
|
$io->success('Database properly created!');
|
||||||
|
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
return ExitCode::EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function checkDbExists(): void
|
private function databaseTablesExist(): bool
|
||||||
{
|
{
|
||||||
if ($this->regularConn->getDriver()->getDatabasePlatform() instanceof SqlitePlatform) {
|
$existingTables = $this->ensureDatabaseExistsAndGetTables();
|
||||||
return;
|
$allMetadata = $this->em->getMetadataFactory()->getAllMetadata();
|
||||||
}
|
$shlinkTables = map($allMetadata, static fn (ClassMetadata $metadata) => $metadata->getTableName());
|
||||||
|
|
||||||
// In order to create the new database, we have to use a connection where the dbname was not set.
|
|
||||||
// Otherwise, it will fail to connect and will not be able to create the new database
|
|
||||||
$schemaManager = $this->noDbNameConn->createSchemaManager();
|
|
||||||
$databases = $schemaManager->listDatabases();
|
|
||||||
$shlinkDatabase = $this->regularConn->getParams()['dbname'] ?? null;
|
|
||||||
|
|
||||||
if ($shlinkDatabase !== null && ! contains($databases, $shlinkDatabase)) {
|
|
||||||
$schemaManager->createDatabase($shlinkDatabase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function schemaExists(): bool
|
|
||||||
{
|
|
||||||
// If at least one of the shlink tables exist, we will consider the database exists somehow.
|
// If at least one of the shlink tables exist, we will consider the database exists somehow.
|
||||||
// We exclude the migrations table, in case db:migrate was run first by mistake.
|
// Any other inconsistency will be taken care of by the migrations.
|
||||||
// Any other inconsistency will be taken care by the migrations.
|
return some($shlinkTables, static fn (string $shlinkTable) => contains($existingTables, $shlinkTable));
|
||||||
$schemaManager = $this->regularConn->createSchemaManager();
|
}
|
||||||
return ! empty(filter($schemaManager->listTableNames(), fn (string $table) => $table !== MIGRATIONS_TABLE));
|
|
||||||
|
private function ensureDatabaseExistsAndGetTables(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Trying to list tables requires opening a connection to configured database.
|
||||||
|
// If it fails, it means it does not exist yet.
|
||||||
|
return $this->regularConn->createSchemaManager()->listTableNames();
|
||||||
|
} catch (Throwable) {
|
||||||
|
// We cannot use getDatabase() to get the database name here, because then the driver will try to connect.
|
||||||
|
// Instead, we read from the raw params.
|
||||||
|
$shlinkDatabase = $this->regularConn->getParams()['dbname'] ?? '';
|
||||||
|
// Create the database using a connection where the dbname was not set.
|
||||||
|
$this->noDbNameConn->createSchemaManager()->createDatabase($shlinkDatabase);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Db;
|
namespace Shlinkio\Shlink\CLI\Command\Db;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
@@ -31,6 +31,6 @@ class MigrateDatabaseCommand extends AbstractDatabaseCommand
|
|||||||
$this->runPhpCommand($output, [self::DOCTRINE_MIGRATIONS_SCRIPT, self::DOCTRINE_MIGRATE_COMMAND]);
|
$this->runPhpCommand($output, [self::DOCTRINE_MIGRATIONS_SCRIPT, self::DOCTRINE_MIGRATE_COMMAND]);
|
||||||
$io->success('Database properly migrated!');
|
$io->success('Database properly migrated!');
|
||||||
|
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
return ExitCode::EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Domain;
|
namespace Shlinkio\Shlink\CLI\Command\Domain;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Shlinkio\Shlink\Core\Config\NotFoundRedirects;
|
use Shlinkio\Shlink\Core\Config\NotFoundRedirects;
|
||||||
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
||||||
use Shlinkio\Shlink\Core\Domain\Model\DomainItem;
|
use Shlinkio\Shlink\Core\Domain\Model\DomainItem;
|
||||||
@@ -109,6 +109,6 @@ class DomainRedirectsCommand extends Command
|
|||||||
|
|
||||||
$io->success(sprintf('"Not found" redirects properly set for "%s"', $domainAuthority));
|
$io->success(sprintf('"Not found" redirects properly set for "%s"', $domainAuthority));
|
||||||
|
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
return ExitCode::EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Domain;
|
namespace Shlinkio\Shlink\CLI\Command\Domain;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||||
use Shlinkio\Shlink\Core\Config\NotFoundRedirectConfigInterface;
|
use Shlinkio\Shlink\Core\Config\NotFoundRedirectConfigInterface;
|
||||||
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
||||||
@@ -59,7 +59,7 @@ class ListDomainsCommand extends Command
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
return ExitCode::EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function notFoundRedirectsToString(NotFoundRedirectConfigInterface $config): string
|
private function notFoundRedirectsToString(NotFoundRedirectConfigInterface $config): string
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
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\Options\UrlShortenerOptions;
|
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||||
@@ -31,7 +31,6 @@ class CreateShortUrlCommand extends Command
|
|||||||
public const NAME = 'short-url:create';
|
public const NAME = 'short-url:create';
|
||||||
|
|
||||||
private ?SymfonyStyle $io;
|
private ?SymfonyStyle $io;
|
||||||
private string $defaultDomain;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly UrlShortenerInterface $urlShortener,
|
private readonly UrlShortenerInterface $urlShortener,
|
||||||
@@ -39,7 +38,6 @@ class CreateShortUrlCommand extends Command
|
|||||||
private readonly UrlShortenerOptions $options,
|
private readonly UrlShortenerOptions $options,
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->defaultDomain = $this->options->domain['hostname'] ?? '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
@@ -121,7 +119,6 @@ class CreateShortUrlCommand extends Command
|
|||||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||||
{
|
{
|
||||||
$this->verifyLongUrlArgument($input, $output);
|
$this->verifyLongUrlArgument($input, $output);
|
||||||
$this->verifyDomainArgument($input);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function verifyLongUrlArgument(InputInterface $input, OutputInterface $output): void
|
private function verifyLongUrlArgument(InputInterface $input, OutputInterface $output): void
|
||||||
@@ -138,19 +135,13 @@ class CreateShortUrlCommand extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function verifyDomainArgument(InputInterface $input): void
|
|
||||||
{
|
|
||||||
$domain = $input->getOption('domain');
|
|
||||||
$input->setOption('domain', $domain === $this->defaultDomain ? null : $domain);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||||
{
|
{
|
||||||
$io = $this->getIO($input, $output);
|
$io = $this->getIO($input, $output);
|
||||||
$longUrl = $input->getArgument('longUrl');
|
$longUrl = $input->getArgument('longUrl');
|
||||||
if (empty($longUrl)) {
|
if (empty($longUrl)) {
|
||||||
$io->error('A URL was not provided!');
|
$io->error('A URL was not provided!');
|
||||||
return ExitCodes::EXIT_FAILURE;
|
return ExitCode::EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
$explodeWithComma = curry(explode(...))(',');
|
$explodeWithComma = curry(explode(...))(',');
|
||||||
@@ -161,7 +152,7 @@ class CreateShortUrlCommand extends Command
|
|||||||
$doValidateUrl = $input->getOption('validate-url');
|
$doValidateUrl = $input->getOption('validate-url');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$shortUrl = $this->urlShortener->shorten(ShortUrlCreation::fromRawData([
|
$result = $this->urlShortener->shorten(ShortUrlCreation::fromRawData([
|
||||||
ShortUrlInputFilter::LONG_URL => $longUrl,
|
ShortUrlInputFilter::LONG_URL => $longUrl,
|
||||||
ShortUrlInputFilter::VALID_SINCE => $input->getOption('valid-since'),
|
ShortUrlInputFilter::VALID_SINCE => $input->getOption('valid-since'),
|
||||||
ShortUrlInputFilter::VALID_UNTIL => $input->getOption('valid-until'),
|
ShortUrlInputFilter::VALID_UNTIL => $input->getOption('valid-until'),
|
||||||
@@ -176,14 +167,19 @@ class CreateShortUrlCommand extends Command
|
|||||||
ShortUrlInputFilter::FORWARD_QUERY => !$input->getOption('no-forward-query'),
|
ShortUrlInputFilter::FORWARD_QUERY => !$input->getOption('no-forward-query'),
|
||||||
], $this->options));
|
], $this->options));
|
||||||
|
|
||||||
|
$result->onEventDispatchingError(static fn () => $io->isVerbose() && $io->warning(
|
||||||
|
'Short URL properly created, but the real-time updates cannot be notified when generating the '
|
||||||
|
. 'short URL from the command line. Migrate to roadrunner in order to bypass this limitation.',
|
||||||
|
));
|
||||||
|
|
||||||
$io->writeln([
|
$io->writeln([
|
||||||
sprintf('Processed long URL: <info>%s</info>', $longUrl),
|
sprintf('Processed long URL: <info>%s</info>', $longUrl),
|
||||||
sprintf('Generated short URL: <info>%s</info>', $this->stringifier->stringify($shortUrl)),
|
sprintf('Generated short URL: <info>%s</info>', $this->stringifier->stringify($result->shortUrl)),
|
||||||
]);
|
]);
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
return ExitCode::EXIT_SUCCESS;
|
||||||
} catch (InvalidUrlException | NonUniqueSlugException $e) {
|
} catch (InvalidUrlException | NonUniqueSlugException $e) {
|
||||||
$io->error($e->getMessage());
|
$io->error($e->getMessage());
|
||||||
return ExitCodes::EXIT_FAILURE;
|
return ExitCode::EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Shlinkio\Shlink\Core\Exception;
|
use Shlinkio\Shlink\Core\Exception;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\DeleteShortUrlServiceInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\DeleteShortUrlServiceInterface;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||||
@@ -55,10 +55,10 @@ class DeleteShortUrlCommand extends Command
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$this->runDelete($io, $identifier, $ignoreThreshold);
|
$this->runDelete($io, $identifier, $ignoreThreshold);
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
return ExitCode::EXIT_SUCCESS;
|
||||||
} catch (Exception\ShortUrlNotFoundException $e) {
|
} catch (Exception\ShortUrlNotFoundException $e) {
|
||||||
$io->error($e->getMessage());
|
$io->error($e->getMessage());
|
||||||
return ExitCodes::EXIT_FAILURE;
|
return ExitCode::EXIT_FAILURE;
|
||||||
} catch (Exception\DeleteShortUrlException $e) {
|
} catch (Exception\DeleteShortUrlException $e) {
|
||||||
return $this->retry($io, $identifier, $e->getMessage());
|
return $this->retry($io, $identifier, $e->getMessage());
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ class DeleteShortUrlCommand extends Command
|
|||||||
$io->warning('Short URL was not deleted.');
|
$io->warning('Short URL was not deleted.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $forceDelete ? ExitCodes::EXIT_SUCCESS : ExitCodes::EXIT_WARNING;
|
return $forceDelete ? ExitCode::EXIT_SUCCESS : ExitCode::EXIT_WARNING;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function runDelete(SymfonyStyle $io, ShortUrlIdentifier $identifier, bool $ignoreThreshold): void
|
private function runDelete(SymfonyStyle $io, ShortUrlIdentifier $identifier, bool $ignoreThreshold): void
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\CLI\Command\Visit\AbstractDeleteVisitsCommand;
|
||||||
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
|
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlVisitsDeleterInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
|
class DeleteShortUrlVisitsCommand extends AbstractDeleteVisitsCommand
|
||||||
|
{
|
||||||
|
public const NAME = 'short-url:visits-delete';
|
||||||
|
|
||||||
|
public function __construct(private readonly ShortUrlVisitsDeleterInterface $deleter)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setName(self::NAME)
|
||||||
|
->setDescription('Deletes visits from a short URL')
|
||||||
|
->addArgument(
|
||||||
|
'shortCode',
|
||||||
|
InputArgument::REQUIRED,
|
||||||
|
'The short code for the short URL which visits will be deleted',
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'domain',
|
||||||
|
'd',
|
||||||
|
InputOption::VALUE_REQUIRED,
|
||||||
|
'The domain if the short code does not belong to the default one',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doExecute(InputInterface $input, SymfonyStyle $io): ?int
|
||||||
|
{
|
||||||
|
$identifier = ShortUrlIdentifier::fromCli($input);
|
||||||
|
try {
|
||||||
|
$result = $this->deleter->deleteShortUrlVisits($identifier);
|
||||||
|
$io->success(sprintf('Successfully deleted %s visits', $result->affectedItems));
|
||||||
|
|
||||||
|
return ExitCode::EXIT_SUCCESS;
|
||||||
|
} catch (ShortUrlNotFoundException) {
|
||||||
|
$io->warning(sprintf('Short URL not found for "%s"', $identifier->__toString()));
|
||||||
|
return ExitCode::EXIT_WARNING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getWarningMessage(): string
|
||||||
|
{
|
||||||
|
return 'You are about to delete all visits for a short URL. This operation cannot be undone.';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,9 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Option\EndDateOption;
|
use Shlinkio\Shlink\CLI\Input\EndDateOption;
|
||||||
use Shlinkio\Shlink\CLI\Option\StartDateOption;
|
use Shlinkio\Shlink\CLI\Input\StartDateOption;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtilsTrait;
|
use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtilsTrait;
|
||||||
@@ -102,6 +102,12 @@ class ListShortUrlsCommand extends Command
|
|||||||
InputOption::VALUE_NONE,
|
InputOption::VALUE_NONE,
|
||||||
'Whether to display the tags or not.',
|
'Whether to display the tags or not.',
|
||||||
)
|
)
|
||||||
|
->addOption(
|
||||||
|
'show-domain',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_NONE,
|
||||||
|
'Whether to display the domain or not. Those belonging to default domain will have value "DEFAULT".',
|
||||||
|
)
|
||||||
->addOption(
|
->addOption(
|
||||||
'show-api-key',
|
'show-api-key',
|
||||||
'k',
|
'k',
|
||||||
@@ -167,7 +173,7 @@ class ListShortUrlsCommand extends Command
|
|||||||
$io->newLine();
|
$io->newLine();
|
||||||
$io->success('Short URLs properly listed');
|
$io->success('Short URLs properly listed');
|
||||||
|
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
return ExitCode::EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function renderPage(
|
private function renderPage(
|
||||||
@@ -217,6 +223,10 @@ class ListShortUrlsCommand extends Command
|
|||||||
if ($input->getOption('show-tags')) {
|
if ($input->getOption('show-tags')) {
|
||||||
$columnsMap['Tags'] = static fn (array $shortUrl): string => implode(', ', $shortUrl['tags']);
|
$columnsMap['Tags'] = static fn (array $shortUrl): string => implode(', ', $shortUrl['tags']);
|
||||||
}
|
}
|
||||||
|
if ($input->getOption('show-domain')) {
|
||||||
|
$columnsMap['Domain'] = static fn (array $_, ShortUrl $shortUrl): string =>
|
||||||
|
$shortUrl->getDomain()?->authority ?? 'DEFAULT';
|
||||||
|
}
|
||||||
if ($input->getOption('show-api-key')) {
|
if ($input->getOption('show-api-key')) {
|
||||||
$columnsMap['API Key'] = static fn (array $_, ShortUrl $shortUrl): string =>
|
$columnsMap['API Key'] = static fn (array $_, ShortUrl $shortUrl): string =>
|
||||||
$shortUrl->authorApiKey()?->__toString() ?? '';
|
$shortUrl->authorApiKey()?->__toString() ?? '';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
|
||||||
@@ -56,10 +56,10 @@ class ResolveUrlCommand extends Command
|
|||||||
try {
|
try {
|
||||||
$url = $this->urlResolver->resolveShortUrl(ShortUrlIdentifier::fromCli($input));
|
$url = $this->urlResolver->resolveShortUrl(ShortUrlIdentifier::fromCli($input));
|
||||||
$output->writeln(sprintf('Long URL: <info>%s</info>', $url->getLongUrl()));
|
$output->writeln(sprintf('Long URL: <info>%s</info>', $url->getLongUrl()));
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
return ExitCode::EXIT_SUCCESS;
|
||||||
} catch (ShortUrlNotFoundException $e) {
|
} catch (ShortUrlNotFoundException $e) {
|
||||||
$io->error($e->getMessage());
|
$io->error($e->getMessage());
|
||||||
return ExitCodes::EXIT_FAILURE;
|
return ExitCode::EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
@@ -41,11 +41,11 @@ class DeleteTagsCommand extends Command
|
|||||||
|
|
||||||
if (empty($tagNames)) {
|
if (empty($tagNames)) {
|
||||||
$io->warning('You have to provide at least one tag name');
|
$io->warning('You have to provide at least one tag name');
|
||||||
return ExitCodes::EXIT_WARNING;
|
return ExitCode::EXIT_WARNING;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->tagService->deleteTags($tagNames);
|
$this->tagService->deleteTags($tagNames);
|
||||||
$io->success('Tags properly deleted');
|
$io->success('Tags properly deleted');
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
return ExitCode::EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||||
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
||||||
use Shlinkio\Shlink\Core\Tag\Model\TagsParams;
|
use Shlinkio\Shlink\Core\Tag\Model\TagsParams;
|
||||||
@@ -34,7 +34,7 @@ class ListTagsCommand extends Command
|
|||||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||||
{
|
{
|
||||||
ShlinkTable::default($output)->render(['Name', 'URLs amount', 'Visits amount'], $this->getTagsRows());
|
ShlinkTable::default($output)->render(['Name', 'URLs amount', 'Visits amount'], $this->getTagsRows());
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
return ExitCode::EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getTagsRows(): array
|
private function getTagsRows(): array
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Shlinkio\Shlink\Core\Exception\TagConflictException;
|
use Shlinkio\Shlink\Core\Exception\TagConflictException;
|
||||||
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
|
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
|
||||||
use Shlinkio\Shlink\Core\Tag\Model\TagRenaming;
|
use Shlinkio\Shlink\Core\Tag\Model\TagRenaming;
|
||||||
@@ -42,10 +42,10 @@ class RenameTagCommand extends Command
|
|||||||
try {
|
try {
|
||||||
$this->tagService->renameTag(TagRenaming::fromNames($oldName, $newName));
|
$this->tagService->renameTag(TagRenaming::fromNames($oldName, $newName));
|
||||||
$io->success('Tag properly renamed.');
|
$io->success('Tag properly renamed.');
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
return ExitCode::EXIT_SUCCESS;
|
||||||
} catch (TagNotFoundException | TagConflictException $e) {
|
} catch (TagNotFoundException | TagConflictException $e) {
|
||||||
$io->error($e->getMessage());
|
$io->error($e->getMessage());
|
||||||
return ExitCodes::EXIT_FAILURE;
|
return ExitCode::EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Util;
|
namespace Shlinkio\Shlink\CLI\Command\Util;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
@@ -28,7 +28,7 @@ abstract class AbstractLockedCommand extends Command
|
|||||||
$output->writeln(
|
$output->writeln(
|
||||||
sprintf('<comment>Command "%s" is already in progress. Skipping.</comment>', $lockConfig->lockName),
|
sprintf('<comment>Command "%s" is already in progress. Skipping.</comment>', $lockConfig->lockName),
|
||||||
);
|
);
|
||||||
return ExitCodes::EXIT_WARNING;
|
return ExitCode::EXIT_WARNING;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
35
module/CLI/src/Command/Visit/AbstractDeleteVisitsCommand.php
Normal file
35
module/CLI/src/Command/Visit/AbstractDeleteVisitsCommand.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
abstract class AbstractDeleteVisitsCommand extends Command
|
||||||
|
{
|
||||||
|
final protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
if (! $this->confirm($io)) {
|
||||||
|
$io->info('Operation aborted');
|
||||||
|
return ExitCode::EXIT_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->doExecute($input, $io);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function confirm(SymfonyStyle $io): bool
|
||||||
|
{
|
||||||
|
$io->warning($this->getWarningMessage());
|
||||||
|
return $io->confirm('<comment>Continue deleting visits?</comment>', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract protected function doExecute(InputInterface $input, SymfonyStyle $io): ?int;
|
||||||
|
|
||||||
|
abstract protected function getWarningMessage(): string;
|
||||||
|
}
|
||||||
@@ -4,9 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Option\EndDateOption;
|
use Shlinkio\Shlink\CLI\Input\EndDateOption;
|
||||||
use Shlinkio\Shlink\CLI\Option\StartDateOption;
|
use Shlinkio\Shlink\CLI\Input\StartDateOption;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
@@ -43,7 +43,7 @@ abstract class AbstractVisitsListCommand extends Command
|
|||||||
|
|
||||||
ShlinkTable::default($output)->render($headers, $rows);
|
ShlinkTable::default($output)->render($headers, $rows);
|
||||||
|
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
return ExitCode::EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveRowsAndHeaders(Paginator $paginator): array
|
private function resolveRowsAndHeaders(Paginator $paginator): array
|
||||||
|
|||||||
42
module/CLI/src/Command/Visit/DeleteOrphanVisitsCommand.php
Normal file
42
module/CLI/src/Command/Visit/DeleteOrphanVisitsCommand.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\VisitsDeleterInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
|
class DeleteOrphanVisitsCommand extends AbstractDeleteVisitsCommand
|
||||||
|
{
|
||||||
|
public const NAME = 'visit:orphan-delete';
|
||||||
|
|
||||||
|
public function __construct(private readonly VisitsDeleterInterface $deleter)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setName(self::NAME)
|
||||||
|
->setDescription('Deletes all orphan visits');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doExecute(InputInterface $input, SymfonyStyle $io): ?int
|
||||||
|
{
|
||||||
|
$result = $this->deleter->deleteOrphanVisits();
|
||||||
|
$io->success(sprintf('Successfully deleted %s visits', $result->affectedItems));
|
||||||
|
|
||||||
|
return ExitCode::EXIT_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getWarningMessage(): string
|
||||||
|
{
|
||||||
|
return 'You are about to delete all orphan visits. This operation cannot be undone.';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Visit;
|
|||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||||
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface;
|
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Helper\ProgressBar;
|
use Symfony\Component\Console\Helper\ProgressBar;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
@@ -56,7 +56,7 @@ class DownloadGeoLiteDbCommand extends Command
|
|||||||
$io->success('GeoLite2 db file properly downloaded.');
|
$io->success('GeoLite2 db file properly downloaded.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
return ExitCode::EXIT_SUCCESS;
|
||||||
} catch (GeolocationDbUpdateFailedException $e) {
|
} catch (GeolocationDbUpdateFailedException $e) {
|
||||||
$olderDbExists = $e->olderDbExists();
|
$olderDbExists = $e->olderDbExists();
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ class DownloadGeoLiteDbCommand extends Command
|
|||||||
$this->getApplication()?->renderThrowable($e, $io);
|
$this->getApplication()?->renderThrowable($e, $io);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $olderDbExists ? ExitCodes::EXIT_WARNING : ExitCodes::EXIT_FAILURE;
|
return $olderDbExists ? ExitCode::EXIT_WARNING : ExitCode::EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Visit;
|
|||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand;
|
use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand;
|
||||||
use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
|
use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||||
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||||
@@ -116,14 +116,14 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->io->success('Finished locating visits');
|
$this->io->success('Finished locating visits');
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
return ExitCode::EXIT_SUCCESS;
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$this->io->error($e->getMessage());
|
$this->io->error($e->getMessage());
|
||||||
if ($this->io->isVerbose()) {
|
if ($this->io->isVerbose()) {
|
||||||
$this->getApplication()?->renderThrowable($e, $this->io);
|
$this->getApplication()?->renderThrowable($e, $this->io);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ExitCodes::EXIT_FAILURE;
|
return ExitCode::EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
|||||||
$downloadDbCommand = $cliApp->find(DownloadGeoLiteDbCommand::NAME);
|
$downloadDbCommand = $cliApp->find(DownloadGeoLiteDbCommand::NAME);
|
||||||
$exitCode = $downloadDbCommand->run(new ArrayInput([]), $this->io);
|
$exitCode = $downloadDbCommand->run(new ArrayInput([]), $this->io);
|
||||||
|
|
||||||
if ($exitCode === ExitCodes::EXIT_FAILURE) {
|
if ($exitCode === ExitCode::EXIT_FAILURE) {
|
||||||
throw new RuntimeException('It is not possible to locate visits without a GeoLite2 db file.');
|
throw new RuntimeException('It is not possible to locate visits without a GeoLite2 db file.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Option;
|
namespace Shlinkio\Shlink\CLI\Input;
|
||||||
|
|
||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Option;
|
namespace Shlinkio\Shlink\CLI\Input;
|
||||||
|
|
||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Option;
|
namespace Shlinkio\Shlink\CLI\Input;
|
||||||
|
|
||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Util;
|
namespace Shlinkio\Shlink\CLI\Util;
|
||||||
|
|
||||||
final class ExitCodes
|
final class ExitCode
|
||||||
{
|
{
|
||||||
public const EXIT_SUCCESS = 0;
|
public const EXIT_SUCCESS = 0;
|
||||||
public const EXIT_FAILURE = -1;
|
public const EXIT_FAILURE = -1;
|
||||||
31
module/CLI/test-cli/Command/CreateShortUrlTest.php
Normal file
31
module/CLI/test-cli/Command/CreateShortUrlTest.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioCliTest\Shlink\CLI\Command;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\CreateShortUrlCommand;
|
||||||
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
|
||||||
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
|
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
|
||||||
|
|
||||||
|
class CreateShortUrlTest extends CliTestCase
|
||||||
|
{
|
||||||
|
#[Test]
|
||||||
|
public function defaultDomainIsIgnoredWhenExplicitlyProvided(): void
|
||||||
|
{
|
||||||
|
$slug = 'testing-default-domain';
|
||||||
|
$defaultDomain = 's.test';
|
||||||
|
|
||||||
|
[$output, $exitCode] = $this->exec(
|
||||||
|
[CreateShortUrlCommand::NAME, 'https://example.com', '--domain', $defaultDomain, '--custom-slug', $slug],
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertEquals(ExitCode::EXIT_SUCCESS, $exitCode);
|
||||||
|
self::assertStringContainsString('Generated short URL: http://' . $defaultDomain . '/' . $slug, $output);
|
||||||
|
|
||||||
|
[$listOutput] = $this->exec([ListShortUrlsCommand::NAME, '--show-domain', '--search-term', $slug]);
|
||||||
|
self::assertStringContainsString('DEFAULT', $listOutput);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,18 +4,19 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioCliTest\Shlink\CLI\Command;
|
namespace ShlinkioCliTest\Shlink\CLI\Command;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
|
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
|
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
|
||||||
|
|
||||||
class GenerateApiKeyTest extends CliTestCase
|
class GenerateApiKeyTest extends CliTestCase
|
||||||
{
|
{
|
||||||
/** @test */
|
#[Test]
|
||||||
public function outputIsCorrect(): void
|
public function outputIsCorrect(): void
|
||||||
{
|
{
|
||||||
[$output, $exitCode] = $this->exec([GenerateKeyCommand::NAME]);
|
[$output, $exitCode] = $this->exec([GenerateKeyCommand::NAME]);
|
||||||
|
|
||||||
self::assertStringContainsString('[OK] Generated API key', $output);
|
self::assertStringContainsString('[OK] Generated API key', $output);
|
||||||
self::assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode);
|
self::assertEquals(ExitCode::EXIT_SUCCESS, $exitCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
79
module/CLI/test-cli/Command/ImportShortUrlsTest.php
Normal file
79
module/CLI/test-cli/Command/ImportShortUrlsTest.php
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioCliTest\Shlink\CLI\Command;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
|
||||||
|
use Shlinkio\Shlink\Importer\Command\ImportCommand;
|
||||||
|
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
|
||||||
|
|
||||||
|
use function fclose;
|
||||||
|
use function fopen;
|
||||||
|
use function fwrite;
|
||||||
|
use function is_string;
|
||||||
|
use function sys_get_temp_dir;
|
||||||
|
use function tempnam;
|
||||||
|
use function unlink;
|
||||||
|
|
||||||
|
class ImportShortUrlsTest extends CliTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var false|string|null
|
||||||
|
* @todo Use native type once PHP 8.1 support is dropped
|
||||||
|
*/
|
||||||
|
private mixed $tempCsvFile = null;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->tempCsvFile = tempnam(sys_get_temp_dir(), 'shlink_csv');
|
||||||
|
if (! $this->tempCsvFile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$handle = fopen($this->tempCsvFile, 'w+');
|
||||||
|
if (! $handle) {
|
||||||
|
$this->fail('It was not possible to open the temporary file to write CSV on it');
|
||||||
|
}
|
||||||
|
|
||||||
|
fwrite(
|
||||||
|
$handle,
|
||||||
|
<<<CSV
|
||||||
|
longURL;tags;domain;short code;Title
|
||||||
|
https://shlink.io;foo,baz;s.test;testing-default-domain-import-1;
|
||||||
|
https://example.com;foo;s.test;testing-default-domain-import-2;
|
||||||
|
CSV,
|
||||||
|
);
|
||||||
|
fclose($handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
if (is_string($this->tempCsvFile)) {
|
||||||
|
unlink($this->tempCsvFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function defaultDomainIsIgnoredWhenExplicitlyProvided(): void
|
||||||
|
{
|
||||||
|
if (! $this->tempCsvFile) {
|
||||||
|
$this->fail('It was not possible to create a temporary CSV file');
|
||||||
|
}
|
||||||
|
|
||||||
|
[$output] = $this->exec([ImportCommand::NAME, 'csv'], [$this->tempCsvFile, ';']);
|
||||||
|
|
||||||
|
self::assertStringContainsString('https://shlink.io: Imported', $output);
|
||||||
|
self::assertStringContainsString('https://example.com: Imported', $output);
|
||||||
|
|
||||||
|
[$listOutput1] = $this->exec(
|
||||||
|
[ListShortUrlsCommand::NAME, '--show-domain', '--search-term', 'testing-default-domain-import-1'],
|
||||||
|
);
|
||||||
|
self::assertStringContainsString('DEFAULT', $listOutput1);
|
||||||
|
[$listOutput1] = $this->exec(
|
||||||
|
[ListShortUrlsCommand::NAME, '--show-domain', '--search-term', 'testing-default-domain-import-2'],
|
||||||
|
);
|
||||||
|
self::assertStringContainsString('DEFAULT', $listOutput1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,25 +5,24 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioCliTest\Shlink\CLI\Command;
|
namespace ShlinkioCliTest\Shlink\CLI\Command;
|
||||||
|
|
||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
|
use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
|
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
|
||||||
|
|
||||||
class ListApiKeysTest extends CliTestCase
|
class ListApiKeysTest extends CliTestCase
|
||||||
{
|
{
|
||||||
/**
|
#[Test, DataProvider('provideFlags')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideFlags
|
|
||||||
*/
|
|
||||||
public function generatesExpectedOutput(array $flags, string $expectedOutput): void
|
public function generatesExpectedOutput(array $flags, string $expectedOutput): void
|
||||||
{
|
{
|
||||||
[$output, $exitCode] = $this->exec([ListKeysCommand::NAME, ...$flags]);
|
[$output, $exitCode] = $this->exec([ListKeysCommand::NAME, ...$flags]);
|
||||||
|
|
||||||
self::assertEquals($expectedOutput, $output);
|
self::assertEquals($expectedOutput, $output);
|
||||||
self::assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode);
|
self::assertEquals(ExitCode::EXIT_SUCCESS, $exitCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideFlags(): iterable
|
public static function provideFlags(): iterable
|
||||||
{
|
{
|
||||||
$expiredApiKeyDate = Chronos::now()->subDay()->startOfDay()->toAtomString();
|
$expiredApiKeyDate = Chronos::now()->subDay()->startOfDay()->toAtomString();
|
||||||
$enabledOnlyOutput = <<<OUT
|
$enabledOnlyOutput = <<<OUT
|
||||||
|
|||||||
@@ -4,22 +4,21 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioCliTest\Shlink\CLI\Command;
|
namespace ShlinkioCliTest\Shlink\CLI\Command;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
|
||||||
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
|
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
|
||||||
|
|
||||||
class ListShortUrlsTest extends CliTestCase
|
class ListShortUrlsTest extends CliTestCase
|
||||||
{
|
{
|
||||||
/**
|
#[Test, DataProvider('provideFlagsAndOutput')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideFlagsAndOutput
|
|
||||||
*/
|
|
||||||
public function generatesExpectedOutput(array $flags, string $expectedOutput): void
|
public function generatesExpectedOutput(array $flags, string $expectedOutput): void
|
||||||
{
|
{
|
||||||
[$output] = $this->exec([ListShortUrlsCommand::NAME, ...$flags], ['no']);
|
[$output] = $this->exec([ListShortUrlsCommand::NAME, ...$flags], ['no']);
|
||||||
self::assertStringContainsString($expectedOutput, $output);
|
self::assertStringContainsString($expectedOutput, $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideFlagsAndOutput(): iterable
|
public static function provideFlagsAndOutput(): iterable
|
||||||
{
|
{
|
||||||
// phpcs:disable Generic.Files.LineLength
|
// phpcs:disable Generic.Files.LineLength
|
||||||
yield 'no flags' => [[], <<<OUTPUT
|
yield 'no flags' => [[], <<<OUTPUT
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\ApiKey;
|
namespace ShlinkioTest\Shlink\CLI\ApiKey;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\CLI\ApiKey\RoleResolver;
|
use Shlinkio\Shlink\CLI\ApiKey\RoleResolver;
|
||||||
@@ -27,29 +29,27 @@ class RoleResolverTest extends TestCase
|
|||||||
$this->resolver = new RoleResolver($this->domainService, 'default.com');
|
$this->resolver = new RoleResolver($this->domainService, 'default.com');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideRoles')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideRoles
|
|
||||||
*/
|
|
||||||
public function properRolesAreResolvedBasedOnInput(
|
public function properRolesAreResolvedBasedOnInput(
|
||||||
InputInterface $input,
|
callable $createInput,
|
||||||
array $expectedRoles,
|
array $expectedRoles,
|
||||||
int $expectedDomainCalls,
|
int $expectedDomainCalls,
|
||||||
): void {
|
): void {
|
||||||
|
$input = $createInput($this);
|
||||||
$this->domainService->expects($this->exactly($expectedDomainCalls))->method('getOrCreate')->with(
|
$this->domainService->expects($this->exactly($expectedDomainCalls))->method('getOrCreate')->with(
|
||||||
'example.com',
|
'example.com',
|
||||||
)->willReturn($this->domainWithId(Domain::withAuthority('example.com')));
|
)->willReturn(self::domainWithId(Domain::withAuthority('example.com')));
|
||||||
|
|
||||||
$result = $this->resolver->determineRoles($input);
|
$result = $this->resolver->determineRoles($input);
|
||||||
|
|
||||||
self::assertEquals($expectedRoles, $result);
|
self::assertEquals($expectedRoles, $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideRoles(): iterable
|
public static function provideRoles(): iterable
|
||||||
{
|
{
|
||||||
$domain = $this->domainWithId(Domain::withAuthority('example.com'));
|
$domain = self::domainWithId(Domain::withAuthority('example.com'));
|
||||||
$buildInput = function (array $definition): InputInterface {
|
$buildInput = static fn (array $definition) => function (TestCase $test) use ($definition): InputInterface {
|
||||||
$input = $this->createStub(InputInterface::class);
|
$input = $test->createStub(InputInterface::class);
|
||||||
$input->method('getOption')->willReturnMap(
|
$input->method('getOption')->willReturnMap(
|
||||||
map($definition, static fn (mixed $returnValue, string $param) => [$param, $returnValue]),
|
map($definition, static fn (mixed $returnValue, string $param) => [$param, $returnValue]),
|
||||||
);
|
);
|
||||||
@@ -98,7 +98,7 @@ class RoleResolverTest extends TestCase
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function exceptionIsThrownWhenTryingToAddDomainOnlyLinkedToDefaultDomain(): void
|
public function exceptionIsThrownWhenTryingToAddDomainOnlyLinkedToDefaultDomain(): void
|
||||||
{
|
{
|
||||||
$input = $this->createStub(InputInterface::class);
|
$input = $this->createStub(InputInterface::class);
|
||||||
@@ -114,7 +114,7 @@ class RoleResolverTest extends TestCase
|
|||||||
$this->resolver->determineRoles($input);
|
$this->resolver->determineRoles($input);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function domainWithId(Domain $domain): Domain
|
private static function domainWithId(Domain $domain): Domain
|
||||||
{
|
{
|
||||||
$domain->setId('1');
|
$domain->setId('1');
|
||||||
return $domain;
|
return $domain;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\CLI\Command\Api\DisableKeyCommand;
|
use Shlinkio\Shlink\CLI\Command\Api\DisableKeyCommand;
|
||||||
@@ -25,7 +26,7 @@ class DisableKeyCommandTest extends TestCase
|
|||||||
$this->commandTester = $this->testerForCommand(new DisableKeyCommand($this->apiKeyService));
|
$this->commandTester = $this->testerForCommand(new DisableKeyCommand($this->apiKeyService));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function providedApiKeyIsDisabled(): void
|
public function providedApiKeyIsDisabled(): void
|
||||||
{
|
{
|
||||||
$apiKey = 'abcd1234';
|
$apiKey = 'abcd1234';
|
||||||
@@ -39,7 +40,7 @@ class DisableKeyCommandTest extends TestCase
|
|||||||
self::assertStringContainsString('API key "abcd1234" properly disabled', $output);
|
self::assertStringContainsString('API key "abcd1234" properly disabled', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function errorIsReturnedIfServiceThrowsException(): void
|
public function errorIsReturnedIfServiceThrowsException(): void
|
||||||
{
|
{
|
||||||
$apiKey = 'abcd1234';
|
$apiKey = 'abcd1234';
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||||
|
|
||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\CLI\ApiKey\RoleResolverInterface;
|
use Shlinkio\Shlink\CLI\ApiKey\RoleResolverInterface;
|
||||||
@@ -32,7 +33,7 @@ class GenerateKeyCommandTest extends TestCase
|
|||||||
$this->commandTester = $this->testerForCommand($command);
|
$this->commandTester = $this->testerForCommand($command);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function noExpirationDateIsDefinedIfNotProvided(): void
|
public function noExpirationDateIsDefinedIfNotProvided(): void
|
||||||
{
|
{
|
||||||
$this->apiKeyService->expects($this->once())->method('create')->with(
|
$this->apiKeyService->expects($this->once())->method('create')->with(
|
||||||
@@ -46,7 +47,7 @@ class GenerateKeyCommandTest extends TestCase
|
|||||||
self::assertStringContainsString('Generated API key: ', $output);
|
self::assertStringContainsString('Generated API key: ', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function expirationDateIsDefinedIfProvided(): void
|
public function expirationDateIsDefinedIfProvided(): void
|
||||||
{
|
{
|
||||||
$this->apiKeyService->expects($this->once())->method('create')->with(
|
$this->apiKeyService->expects($this->once())->method('create')->with(
|
||||||
@@ -59,7 +60,7 @@ class GenerateKeyCommandTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function nameIsDefinedIfProvided(): void
|
public function nameIsDefinedIfProvided(): void
|
||||||
{
|
{
|
||||||
$this->apiKeyService->expects($this->once())->method('create')->with(
|
$this->apiKeyService->expects($this->once())->method('create')->with(
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||||
|
|
||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
|
use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
|
||||||
@@ -29,10 +31,7 @@ class ListKeysCommandTest extends TestCase
|
|||||||
$this->commandTester = $this->testerForCommand(new ListKeysCommand($this->apiKeyService));
|
$this->commandTester = $this->testerForCommand(new ListKeysCommand($this->apiKeyService));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideKeysAndOutputs')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideKeysAndOutputs
|
|
||||||
*/
|
|
||||||
public function returnsExpectedOutput(array $keys, bool $enabledOnly, string $expected): void
|
public function returnsExpectedOutput(array $keys, bool $enabledOnly, string $expected): void
|
||||||
{
|
{
|
||||||
$this->apiKeyService->expects($this->once())->method('listKeys')->with($enabledOnly)->willReturn($keys);
|
$this->apiKeyService->expects($this->once())->method('listKeys')->with($enabledOnly)->willReturn($keys);
|
||||||
@@ -43,7 +42,7 @@ class ListKeysCommandTest extends TestCase
|
|||||||
self::assertEquals($expected, $output);
|
self::assertEquals($expected, $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideKeysAndOutputs(): iterable
|
public static function provideKeysAndOutputs(): iterable
|
||||||
{
|
{
|
||||||
$dateInThePast = Chronos::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:00');
|
$dateInThePast = Chronos::createFromFormat('Y-m-d H:i:s', '2020-01-01 00:00:00');
|
||||||
|
|
||||||
@@ -84,14 +83,14 @@ class ListKeysCommandTest extends TestCase
|
|||||||
yield 'with roles' => [
|
yield 'with roles' => [
|
||||||
[
|
[
|
||||||
$apiKey1 = ApiKey::create(),
|
$apiKey1 = ApiKey::create(),
|
||||||
$apiKey2 = $this->apiKeyWithRoles([RoleDefinition::forAuthoredShortUrls()]),
|
$apiKey2 = self::apiKeyWithRoles([RoleDefinition::forAuthoredShortUrls()]),
|
||||||
$apiKey3 = $this->apiKeyWithRoles(
|
$apiKey3 = self::apiKeyWithRoles(
|
||||||
[RoleDefinition::forDomain($this->domainWithId(Domain::withAuthority('example.com')))],
|
[RoleDefinition::forDomain(self::domainWithId(Domain::withAuthority('example.com')))],
|
||||||
),
|
),
|
||||||
$apiKey4 = ApiKey::create(),
|
$apiKey4 = ApiKey::create(),
|
||||||
$apiKey5 = $this->apiKeyWithRoles([
|
$apiKey5 = self::apiKeyWithRoles([
|
||||||
RoleDefinition::forAuthoredShortUrls(),
|
RoleDefinition::forAuthoredShortUrls(),
|
||||||
RoleDefinition::forDomain($this->domainWithId(Domain::withAuthority('example.com'))),
|
RoleDefinition::forDomain(self::domainWithId(Domain::withAuthority('example.com'))),
|
||||||
]),
|
]),
|
||||||
$apiKey6 = ApiKey::create(),
|
$apiKey6 = ApiKey::create(),
|
||||||
],
|
],
|
||||||
@@ -141,7 +140,7 @@ class ListKeysCommandTest extends TestCase
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function apiKeyWithRoles(array $roles): ApiKey
|
private static function apiKeyWithRoles(array $roles): ApiKey
|
||||||
{
|
{
|
||||||
$apiKey = ApiKey::create();
|
$apiKey = ApiKey::create();
|
||||||
foreach ($roles as $role) {
|
foreach ($roles as $role) {
|
||||||
@@ -151,7 +150,7 @@ class ListKeysCommandTest extends TestCase
|
|||||||
return $apiKey;
|
return $apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function domainWithId(Domain $domain): Domain
|
private static function domainWithId(Domain $domain): Domain
|
||||||
{
|
{
|
||||||
$domain->setId('1');
|
$domain->setId('1');
|
||||||
return $domain;
|
return $domain;
|
||||||
|
|||||||
@@ -7,8 +7,13 @@ namespace ShlinkioTest\Shlink\CLI\Command\Db;
|
|||||||
use Doctrine\DBAL\Connection;
|
use Doctrine\DBAL\Connection;
|
||||||
use Doctrine\DBAL\Driver;
|
use Doctrine\DBAL\Driver;
|
||||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||||
use Doctrine\DBAL\Platforms\SqlitePlatform;
|
|
||||||
use Doctrine\DBAL\Schema\AbstractSchemaManager;
|
use Doctrine\DBAL\Schema\AbstractSchemaManager;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||||
|
use Doctrine\Persistence\Mapping\ClassMetadataFactory;
|
||||||
|
use Exception;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\CLI\Command\Db\CreateDatabaseCommand;
|
use Shlinkio\Shlink\CLI\Command\Db\CreateDatabaseCommand;
|
||||||
@@ -20,8 +25,6 @@ use Symfony\Component\Lock\LockFactory;
|
|||||||
use Symfony\Component\Lock\LockInterface;
|
use Symfony\Component\Lock\LockInterface;
|
||||||
use Symfony\Component\Process\PhpExecutableFinder;
|
use Symfony\Component\Process\PhpExecutableFinder;
|
||||||
|
|
||||||
use const Shlinkio\Shlink\MIGRATIONS_TABLE;
|
|
||||||
|
|
||||||
class CreateDatabaseCommandTest extends TestCase
|
class CreateDatabaseCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
use CliTestUtilsTrait;
|
use CliTestUtilsTrait;
|
||||||
@@ -29,6 +32,7 @@ class CreateDatabaseCommandTest extends TestCase
|
|||||||
private CommandTester $commandTester;
|
private CommandTester $commandTester;
|
||||||
private MockObject & ProcessRunnerInterface $processHelper;
|
private MockObject & ProcessRunnerInterface $processHelper;
|
||||||
private MockObject & Connection $regularConn;
|
private MockObject & Connection $regularConn;
|
||||||
|
private MockObject & ClassMetadataFactory $metadataFactory;
|
||||||
private MockObject & AbstractSchemaManager $schemaManager;
|
private MockObject & AbstractSchemaManager $schemaManager;
|
||||||
private MockObject & Driver $driver;
|
private MockObject & Driver $driver;
|
||||||
|
|
||||||
@@ -49,31 +53,30 @@ class CreateDatabaseCommandTest extends TestCase
|
|||||||
$this->regularConn->method('createSchemaManager')->willReturn($this->schemaManager);
|
$this->regularConn->method('createSchemaManager')->willReturn($this->schemaManager);
|
||||||
$this->driver = $this->createMock(Driver::class);
|
$this->driver = $this->createMock(Driver::class);
|
||||||
$this->regularConn->method('getDriver')->willReturn($this->driver);
|
$this->regularConn->method('getDriver')->willReturn($this->driver);
|
||||||
|
|
||||||
|
$this->metadataFactory = $this->createMock(ClassMetadataFactory::class);
|
||||||
|
$em = $this->createMock(EntityManagerInterface::class);
|
||||||
|
$em->method('getConnection')->willReturn($this->regularConn);
|
||||||
|
$em->method('getMetadataFactory')->willReturn($this->metadataFactory);
|
||||||
|
|
||||||
$noDbNameConn = $this->createMock(Connection::class);
|
$noDbNameConn = $this->createMock(Connection::class);
|
||||||
$noDbNameConn->method('createSchemaManager')->withAnyParameters()->willReturn($this->schemaManager);
|
$noDbNameConn->method('createSchemaManager')->withAnyParameters()->willReturn($this->schemaManager);
|
||||||
|
|
||||||
$command = new CreateDatabaseCommand(
|
$command = new CreateDatabaseCommand($locker, $this->processHelper, $phpExecutableFinder, $em, $noDbNameConn);
|
||||||
$locker,
|
|
||||||
$this->processHelper,
|
|
||||||
$phpExecutableFinder,
|
|
||||||
$this->regularConn,
|
|
||||||
$noDbNameConn,
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->commandTester = $this->testerForCommand($command);
|
$this->commandTester = $this->testerForCommand($command);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function successMessageIsPrintedIfDatabaseAlreadyExists(): void
|
public function successMessageIsPrintedIfDatabaseAlreadyExists(): void
|
||||||
{
|
{
|
||||||
$shlinkDatabase = 'shlink_database';
|
$this->regularConn->expects($this->never())->method('getParams');
|
||||||
$this->regularConn->expects($this->once())->method('getParams')->willReturn(['dbname' => $shlinkDatabase]);
|
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(AbstractPlatform::class));
|
||||||
$this->schemaManager->expects($this->once())->method('listDatabases')->willReturn(
|
|
||||||
['foo', $shlinkDatabase, 'bar'],
|
$metadataMock = $this->createMock(ClassMetadata::class);
|
||||||
);
|
$metadataMock->expects($this->once())->method('getTableName')->willReturn('foo_table');
|
||||||
|
$this->metadataFactory->method('getAllMetadata')->willReturn([$metadataMock]);
|
||||||
$this->schemaManager->expects($this->never())->method('createDatabase');
|
$this->schemaManager->expects($this->never())->method('createDatabase');
|
||||||
$this->schemaManager->expects($this->once())->method('listTableNames')->willReturn(['foo_table', 'bar_table']);
|
$this->schemaManager->expects($this->once())->method('listTableNames')->willReturn(['foo_table', 'bar_table']);
|
||||||
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(AbstractPlatform::class));
|
|
||||||
|
|
||||||
$this->commandTester->execute([]);
|
$this->commandTester->execute([]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
@@ -81,32 +84,29 @@ class CreateDatabaseCommandTest extends TestCase
|
|||||||
self::assertStringContainsString('Database already exists. Run "db:migrate" command', $output);
|
self::assertStringContainsString('Database already exists. Run "db:migrate" command', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function databaseIsCreatedIfItDoesNotExist(): void
|
public function databaseIsCreatedIfItDoesNotExist(): void
|
||||||
{
|
{
|
||||||
|
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(AbstractPlatform::class));
|
||||||
|
|
||||||
$shlinkDatabase = 'shlink_database';
|
$shlinkDatabase = 'shlink_database';
|
||||||
$this->regularConn->expects($this->once())->method('getParams')->willReturn(['dbname' => $shlinkDatabase]);
|
$this->regularConn->expects($this->once())->method('getParams')->willReturn(['dbname' => $shlinkDatabase]);
|
||||||
$this->schemaManager->expects($this->once())->method('listDatabases')->willReturn(['foo', 'bar']);
|
$this->metadataFactory->method('getAllMetadata')->willReturn([]);
|
||||||
$this->schemaManager->expects($this->once())->method('createDatabase')->with($shlinkDatabase);
|
$this->schemaManager->expects($this->once())->method('createDatabase')->with($shlinkDatabase);
|
||||||
$this->schemaManager->expects($this->once())->method('listTableNames')->willReturn(
|
$this->schemaManager->expects($this->once())->method('listTableNames')->willThrowException(new Exception(''));
|
||||||
['foo_table', 'bar_table', MIGRATIONS_TABLE],
|
|
||||||
);
|
|
||||||
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(AbstractPlatform::class));
|
|
||||||
|
|
||||||
$this->commandTester->execute([]);
|
$this->commandTester->execute([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideEmptyDatabase')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideEmptyDatabase
|
|
||||||
*/
|
|
||||||
public function tablesAreCreatedIfDatabaseIsEmpty(array $tables): void
|
public function tablesAreCreatedIfDatabaseIsEmpty(array $tables): void
|
||||||
{
|
{
|
||||||
$shlinkDatabase = 'shlink_database';
|
$this->regularConn->expects($this->never())->method('getParams');
|
||||||
$this->regularConn->expects($this->once())->method('getParams')->willReturn(['dbname' => $shlinkDatabase]);
|
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(AbstractPlatform::class));
|
||||||
$this->schemaManager->expects($this->once())->method('listDatabases')->willReturn(
|
|
||||||
['foo', $shlinkDatabase, 'bar'],
|
$metadata = $this->createMock(ClassMetadata::class);
|
||||||
);
|
$metadata->method('getTableName')->willReturn('shlink_table');
|
||||||
|
$this->metadataFactory->method('getAllMetadata')->willReturn([$metadata]);
|
||||||
$this->schemaManager->expects($this->never())->method('createDatabase');
|
$this->schemaManager->expects($this->never())->method('createDatabase');
|
||||||
$this->schemaManager->expects($this->once())->method('listTableNames')->willReturn($tables);
|
$this->schemaManager->expects($this->once())->method('listTableNames')->willReturn($tables);
|
||||||
$this->processHelper->expects($this->once())->method('run')->with($this->isInstanceOf(OutputInterface::class), [
|
$this->processHelper->expects($this->once())->method('run')->with($this->isInstanceOf(OutputInterface::class), [
|
||||||
@@ -115,7 +115,6 @@ class CreateDatabaseCommandTest extends TestCase
|
|||||||
CreateDatabaseCommand::DOCTRINE_CREATE_SCHEMA_COMMAND,
|
CreateDatabaseCommand::DOCTRINE_CREATE_SCHEMA_COMMAND,
|
||||||
'--no-interaction',
|
'--no-interaction',
|
||||||
]);
|
]);
|
||||||
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(AbstractPlatform::class));
|
|
||||||
|
|
||||||
$this->commandTester->execute([]);
|
$this->commandTester->execute([]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
@@ -124,22 +123,9 @@ class CreateDatabaseCommandTest extends TestCase
|
|||||||
self::assertStringContainsString('Database properly created!', $output);
|
self::assertStringContainsString('Database properly created!', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideEmptyDatabase(): iterable
|
public static function provideEmptyDatabase(): iterable
|
||||||
{
|
{
|
||||||
yield 'no tables' => [[]];
|
yield 'no tables' => [[]];
|
||||||
yield 'migrations table' => [[MIGRATIONS_TABLE]];
|
yield 'migrations table' => [['non_shlink_table']];
|
||||||
}
|
|
||||||
|
|
||||||
/** @test */
|
|
||||||
public function databaseCheckIsSkippedForSqlite(): void
|
|
||||||
{
|
|
||||||
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(SqlitePlatform::class));
|
|
||||||
|
|
||||||
$this->regularConn->expects($this->never())->method('getParams');
|
|
||||||
$this->schemaManager->expects($this->never())->method('listDatabases');
|
|
||||||
$this->schemaManager->expects($this->never())->method('createDatabase');
|
|
||||||
$this->schemaManager->expects($this->once())->method('listTableNames')->willReturn(['foo_table', 'bar_table']);
|
|
||||||
|
|
||||||
$this->commandTester->execute([]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command\Db;
|
namespace ShlinkioTest\Shlink\CLI\Command\Db;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\CLI\Command\Db\MigrateDatabaseCommand;
|
use Shlinkio\Shlink\CLI\Command\Db\MigrateDatabaseCommand;
|
||||||
@@ -38,7 +39,7 @@ class MigrateDatabaseCommandTest extends TestCase
|
|||||||
$this->commandTester = $this->testerForCommand($command);
|
$this->commandTester = $this->testerForCommand($command);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function migrationsCommandIsRunWithProperVerbosity(): void
|
public function migrationsCommandIsRunWithProperVerbosity(): void
|
||||||
{
|
{
|
||||||
$this->processHelper->expects($this->once())->method('run')->with($this->isInstanceOf(OutputInterface::class), [
|
$this->processHelper->expects($this->once())->method('run')->with($this->isInstanceOf(OutputInterface::class), [
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command\Domain;
|
namespace ShlinkioTest\Shlink\CLI\Command\Domain;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\CLI\Command\Domain\DomainRedirectsCommand;
|
use Shlinkio\Shlink\CLI\Command\Domain\DomainRedirectsCommand;
|
||||||
@@ -30,10 +32,7 @@ class DomainRedirectsCommandTest extends TestCase
|
|||||||
$this->commandTester = $this->testerForCommand(new DomainRedirectsCommand($this->domainService));
|
$this->commandTester = $this->testerForCommand(new DomainRedirectsCommand($this->domainService));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideDomains')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideDomains
|
|
||||||
*/
|
|
||||||
public function onlyPlainQuestionsAreAskedForNewDomainsAndDomainsWithNoRedirects(?Domain $domain): void
|
public function onlyPlainQuestionsAreAskedForNewDomainsAndDomainsWithNoRedirects(?Domain $domain): void
|
||||||
{
|
{
|
||||||
$domainAuthority = 'my-domain.com';
|
$domainAuthority = 'my-domain.com';
|
||||||
@@ -60,13 +59,13 @@ class DomainRedirectsCommandTest extends TestCase
|
|||||||
self::assertEquals(3, substr_count($output, '(Leave empty for no redirect)'));
|
self::assertEquals(3, substr_count($output, '(Leave empty for no redirect)'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideDomains(): iterable
|
public static function provideDomains(): iterable
|
||||||
{
|
{
|
||||||
yield 'no domain' => [null];
|
yield 'no domain' => [null];
|
||||||
yield 'domain without redirects' => [Domain::withAuthority('')];
|
yield 'domain without redirects' => [Domain::withAuthority('')];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function offersNewOptionsForDomainsWithExistingRedirects(): void
|
public function offersNewOptionsForDomainsWithExistingRedirects(): void
|
||||||
{
|
{
|
||||||
$domainAuthority = 'example.com';
|
$domainAuthority = 'example.com';
|
||||||
@@ -95,7 +94,7 @@ class DomainRedirectsCommandTest extends TestCase
|
|||||||
self::assertEquals(3, substr_count($output, 'Remove redirect'));
|
self::assertEquals(3, substr_count($output, 'Remove redirect'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function authorityIsRequestedWhenNotProvidedAndNoOtherDomainsExist(): void
|
public function authorityIsRequestedWhenNotProvidedAndNoOtherDomainsExist(): void
|
||||||
{
|
{
|
||||||
$domainAuthority = 'example.com';
|
$domainAuthority = 'example.com';
|
||||||
@@ -117,7 +116,7 @@ class DomainRedirectsCommandTest extends TestCase
|
|||||||
self::assertStringContainsString('Domain authority for which you want to set specific redirects', $output);
|
self::assertStringContainsString('Domain authority for which you want to set specific redirects', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function oneOfTheExistingDomainsCanBeSelected(): void
|
public function oneOfTheExistingDomainsCanBeSelected(): void
|
||||||
{
|
{
|
||||||
$domainAuthority = 'existing-two.com';
|
$domainAuthority = 'existing-two.com';
|
||||||
@@ -146,7 +145,7 @@ class DomainRedirectsCommandTest extends TestCase
|
|||||||
self::assertStringContainsString($domainAuthority, $output);
|
self::assertStringContainsString($domainAuthority, $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function aNewDomainCanBeCreatedEvenIfOthersAlreadyExist(): void
|
public function aNewDomainCanBeCreatedEvenIfOthersAlreadyExist(): void
|
||||||
{
|
{
|
||||||
$domainAuthority = 'new-domain.com';
|
$domainAuthority = 'new-domain.com';
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioTest\Shlink\CLI\Command\Domain;
|
namespace ShlinkioTest\Shlink\CLI\Command\Domain;
|
||||||
|
|
||||||
use Pagerfanta\Adapter\ArrayAdapter;
|
use Pagerfanta\Adapter\ArrayAdapter;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\CLI\Command\Domain\GetDomainVisitsCommand;
|
use Shlinkio\Shlink\CLI\Command\Domain\GetDomainVisitsCommand;
|
||||||
@@ -37,7 +38,7 @@ class GetDomainVisitsCommandTest extends TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function outputIsProperlyGenerated(): void
|
public function outputIsProperlyGenerated(): void
|
||||||
{
|
{
|
||||||
$shortUrl = ShortUrl::createFake();
|
$shortUrl = ShortUrl::createFake();
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command\Domain;
|
namespace ShlinkioTest\Shlink\CLI\Command\Domain;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\CLI\Command\Domain\ListDomainsCommand;
|
use Shlinkio\Shlink\CLI\Command\Domain\ListDomainsCommand;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Shlinkio\Shlink\Core\Config\NotFoundRedirects;
|
use Shlinkio\Shlink\Core\Config\NotFoundRedirects;
|
||||||
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
||||||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||||
@@ -29,10 +31,7 @@ class ListDomainsCommandTest extends TestCase
|
|||||||
$this->commandTester = $this->testerForCommand(new ListDomainsCommand($this->domainService));
|
$this->commandTester = $this->testerForCommand(new ListDomainsCommand($this->domainService));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideInputsAndOutputs')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideInputsAndOutputs
|
|
||||||
*/
|
|
||||||
public function allDomainsAreProperlyPrinted(array $input, string $expectedOutput): void
|
public function allDomainsAreProperlyPrinted(array $input, string $expectedOutput): void
|
||||||
{
|
{
|
||||||
$bazDomain = Domain::withAuthority('baz.com');
|
$bazDomain = Domain::withAuthority('baz.com');
|
||||||
@@ -54,10 +53,10 @@ class ListDomainsCommandTest extends TestCase
|
|||||||
$this->commandTester->execute($input);
|
$this->commandTester->execute($input);
|
||||||
|
|
||||||
self::assertEquals($expectedOutput, $this->commandTester->getDisplay());
|
self::assertEquals($expectedOutput, $this->commandTester->getDisplay());
|
||||||
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
self::assertEquals(ExitCode::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideInputsAndOutputs(): iterable
|
public static function provideInputsAndOutputs(): iterable
|
||||||
{
|
{
|
||||||
$withoutRedirectsOutput = <<<OUTPUT
|
$withoutRedirectsOutput = <<<OUTPUT
|
||||||
+---------+------------+
|
+---------+------------+
|
||||||
|
|||||||
@@ -4,27 +4,30 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
|
use Laminas\ServiceManager\Exception\ServiceNotFoundException;
|
||||||
use PHPUnit\Framework\Assert;
|
use PHPUnit\Framework\Assert;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\CreateShortUrlCommand;
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\CreateShortUrlCommand;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
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\Options\UrlShortenerOptions;
|
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Model\UrlShorteningResult;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\UrlShortenerInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\UrlShortenerInterface;
|
||||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Tester\CommandTester;
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
|
||||||
class CreateShortUrlCommandTest extends TestCase
|
class CreateShortUrlCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
use CliTestUtilsTrait;
|
use CliTestUtilsTrait;
|
||||||
|
|
||||||
private const DEFAULT_DOMAIN = 'default.com';
|
|
||||||
|
|
||||||
private CommandTester $commandTester;
|
private CommandTester $commandTester;
|
||||||
private MockObject & UrlShortenerInterface $urlShortener;
|
private MockObject & UrlShortenerInterface $urlShortener;
|
||||||
private MockObject & ShortUrlStringifierInterface $stringifier;
|
private MockObject & ShortUrlStringifierInterface $stringifier;
|
||||||
@@ -38,18 +41,20 @@ class CreateShortUrlCommandTest extends TestCase
|
|||||||
$this->urlShortener,
|
$this->urlShortener,
|
||||||
$this->stringifier,
|
$this->stringifier,
|
||||||
new UrlShortenerOptions(
|
new UrlShortenerOptions(
|
||||||
domain: ['hostname' => self::DEFAULT_DOMAIN, 'schema' => ''],
|
domain: ['hostname' => 'example.com', 'schema' => ''],
|
||||||
defaultShortCodesLength: 5,
|
defaultShortCodesLength: 5,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
$this->commandTester = $this->testerForCommand($command);
|
$this->commandTester = $this->testerForCommand($command);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function properShortCodeIsCreatedIfLongUrlIsCorrect(): void
|
public function properShortCodeIsCreatedIfLongUrlIsCorrect(): void
|
||||||
{
|
{
|
||||||
$shortUrl = ShortUrl::createFake();
|
$shortUrl = ShortUrl::createFake();
|
||||||
$this->urlShortener->expects($this->once())->method('shorten')->withAnyParameters()->willReturn($shortUrl);
|
$this->urlShortener->expects($this->once())->method('shorten')->withAnyParameters()->willReturn(
|
||||||
|
UrlShorteningResult::withoutErrorOnEventDispatching($shortUrl),
|
||||||
|
);
|
||||||
$this->stringifier->expects($this->once())->method('stringify')->with($shortUrl)->willReturn(
|
$this->stringifier->expects($this->once())->method('stringify')->with($shortUrl)->willReturn(
|
||||||
'stringified_short_url',
|
'stringified_short_url',
|
||||||
);
|
);
|
||||||
@@ -57,14 +62,15 @@ class CreateShortUrlCommandTest extends TestCase
|
|||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'longUrl' => 'http://domain.com/foo/bar',
|
'longUrl' => 'http://domain.com/foo/bar',
|
||||||
'--max-visits' => '3',
|
'--max-visits' => '3',
|
||||||
]);
|
], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
self::assertEquals(ExitCode::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||||
self::assertStringContainsString('stringified_short_url', $output);
|
self::assertStringContainsString('stringified_short_url', $output);
|
||||||
|
self::assertStringNotContainsString('but the real-time updates cannot', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function exceptionWhileParsingLongUrlOutputsError(): void
|
public function exceptionWhileParsingLongUrlOutputsError(): void
|
||||||
{
|
{
|
||||||
$url = 'http://domain.com/invalid';
|
$url = 'http://domain.com/invalid';
|
||||||
@@ -76,11 +82,11 @@ class CreateShortUrlCommandTest extends TestCase
|
|||||||
$this->commandTester->execute(['longUrl' => $url]);
|
$this->commandTester->execute(['longUrl' => $url]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
self::assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
|
self::assertEquals(ExitCode::EXIT_FAILURE, $this->commandTester->getStatusCode());
|
||||||
self::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
|
||||||
{
|
{
|
||||||
$this->urlShortener->expects($this->once())->method('shorten')->withAnyParameters()->willThrowException(
|
$this->urlShortener->expects($this->once())->method('shorten')->withAnyParameters()->willThrowException(
|
||||||
@@ -91,11 +97,11 @@ class CreateShortUrlCommandTest extends TestCase
|
|||||||
$this->commandTester->execute(['longUrl' => 'http://domain.com/invalid', '--custom-slug' => 'my-slug']);
|
$this->commandTester->execute(['longUrl' => 'http://domain.com/invalid', '--custom-slug' => 'my-slug']);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
self::assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
|
self::assertEquals(ExitCode::EXIT_FAILURE, $this->commandTester->getStatusCode());
|
||||||
self::assertStringContainsString('Provided slug "my-slug" is already in use', $output);
|
self::assertStringContainsString('Provided slug "my-slug" is already in use', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function properlyProcessesProvidedTags(): void
|
public function properlyProcessesProvidedTags(): void
|
||||||
{
|
{
|
||||||
$shortUrl = ShortUrl::createFake();
|
$shortUrl = ShortUrl::createFake();
|
||||||
@@ -104,7 +110,7 @@ class CreateShortUrlCommandTest extends TestCase
|
|||||||
Assert::assertEquals(['foo', 'bar', 'baz', 'boo', 'zar'], $creation->tags);
|
Assert::assertEquals(['foo', 'bar', 'baz', 'boo', 'zar'], $creation->tags);
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
)->willReturn($shortUrl);
|
)->willReturn(UrlShorteningResult::withoutErrorOnEventDispatching($shortUrl));
|
||||||
$this->stringifier->expects($this->once())->method('stringify')->with($shortUrl)->willReturn(
|
$this->stringifier->expects($this->once())->method('stringify')->with($shortUrl)->willReturn(
|
||||||
'stringified_short_url',
|
'stringified_short_url',
|
||||||
);
|
);
|
||||||
@@ -115,14 +121,11 @@ class CreateShortUrlCommandTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
self::assertEquals(ExitCode::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||||
self::assertStringContainsString('stringified_short_url', $output);
|
self::assertStringContainsString('stringified_short_url', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideDomains')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideDomains
|
|
||||||
*/
|
|
||||||
public function properlyProcessesProvidedDomain(array $input, ?string $expectedDomain): void
|
public function properlyProcessesProvidedDomain(array $input, ?string $expectedDomain): void
|
||||||
{
|
{
|
||||||
$this->urlShortener->expects($this->once())->method('shorten')->with(
|
$this->urlShortener->expects($this->once())->method('shorten')->with(
|
||||||
@@ -130,27 +133,23 @@ class CreateShortUrlCommandTest extends TestCase
|
|||||||
Assert::assertEquals($expectedDomain, $meta->domain);
|
Assert::assertEquals($expectedDomain, $meta->domain);
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
)->willReturn(ShortUrl::createFake());
|
)->willReturn(UrlShorteningResult::withoutErrorOnEventDispatching(ShortUrl::createFake()));
|
||||||
$this->stringifier->method('stringify')->with($this->isInstanceOf(ShortUrl::class))->willReturn('');
|
$this->stringifier->method('stringify')->with($this->isInstanceOf(ShortUrl::class))->willReturn('');
|
||||||
|
|
||||||
$input['longUrl'] = 'http://domain.com/foo/bar';
|
$input['longUrl'] = 'http://domain.com/foo/bar';
|
||||||
$this->commandTester->execute($input);
|
$this->commandTester->execute($input);
|
||||||
|
|
||||||
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
self::assertEquals(ExitCode::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideDomains(): iterable
|
public static function provideDomains(): iterable
|
||||||
{
|
{
|
||||||
yield 'no domain' => [[], null];
|
yield 'no domain' => [[], null];
|
||||||
yield 'non-default domain foo' => [['--domain' => 'foo.com'], 'foo.com'];
|
yield 'domain foo' => [['--domain' => 'foo.com'], 'foo.com'];
|
||||||
yield 'non-default domain bar' => [['-d' => 'bar.com'], 'bar.com'];
|
yield 'domain bar' => [['-d' => 'bar.com'], 'bar.com'];
|
||||||
yield 'default domain' => [['--domain' => self::DEFAULT_DOMAIN], null];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideFlags')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideFlags
|
|
||||||
*/
|
|
||||||
public function urlValidationHasExpectedValueBasedOnProvidedFlags(array $options, ?bool $expectedValidateUrl): void
|
public function urlValidationHasExpectedValueBasedOnProvidedFlags(array $options, ?bool $expectedValidateUrl): void
|
||||||
{
|
{
|
||||||
$shortUrl = ShortUrl::createFake();
|
$shortUrl = ShortUrl::createFake();
|
||||||
@@ -159,16 +158,52 @@ class CreateShortUrlCommandTest extends TestCase
|
|||||||
Assert::assertEquals($expectedValidateUrl, $meta->doValidateUrl());
|
Assert::assertEquals($expectedValidateUrl, $meta->doValidateUrl());
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
)->willReturn($shortUrl);
|
)->willReturn(UrlShorteningResult::withoutErrorOnEventDispatching($shortUrl));
|
||||||
$this->stringifier->method('stringify')->with($this->isInstanceOf(ShortUrl::class))->willReturn('');
|
$this->stringifier->method('stringify')->with($this->isInstanceOf(ShortUrl::class))->willReturn('');
|
||||||
|
|
||||||
$options['longUrl'] = 'http://domain.com/foo/bar';
|
$options['longUrl'] = 'http://domain.com/foo/bar';
|
||||||
$this->commandTester->execute($options);
|
$this->commandTester->execute($options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideFlags(): iterable
|
public static function provideFlags(): iterable
|
||||||
{
|
{
|
||||||
yield 'no flags' => [[], null];
|
yield 'no flags' => [[], null];
|
||||||
yield 'validate-url' => [['--validate-url' => true], true];
|
yield 'validate-url' => [['--validate-url' => true], true];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param callable(string $output): void $assert
|
||||||
|
*/
|
||||||
|
#[Test, DataProvider('provideDispatchBehavior')]
|
||||||
|
public function warningIsPrintedInVerboseModeWhenDispatchErrors(int $verbosity, callable $assert): void
|
||||||
|
{
|
||||||
|
$shortUrl = ShortUrl::createFake();
|
||||||
|
$this->urlShortener->expects($this->once())->method('shorten')->withAnyParameters()->willReturn(
|
||||||
|
UrlShorteningResult::withErrorOnEventDispatching($shortUrl, new ServiceNotFoundException()),
|
||||||
|
);
|
||||||
|
$this->stringifier->method('stringify')->willReturn('stringified_short_url');
|
||||||
|
|
||||||
|
$this->commandTester->execute(['longUrl' => 'http://domain.com/foo/bar'], ['verbosity' => $verbosity]);
|
||||||
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
|
$assert($output);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function provideDispatchBehavior(): iterable
|
||||||
|
{
|
||||||
|
$containsAssertion = static fn (string $output) => self::assertStringContainsString(
|
||||||
|
'but the real-time updates cannot',
|
||||||
|
$output,
|
||||||
|
);
|
||||||
|
$doesNotContainAssertion = static fn (string $output) => self::assertStringNotContainsString(
|
||||||
|
'but the real-time updates cannot',
|
||||||
|
$output,
|
||||||
|
);
|
||||||
|
|
||||||
|
yield 'quiet' => [OutputInterface::VERBOSITY_QUIET, $doesNotContainAssertion];
|
||||||
|
yield 'normal' => [OutputInterface::VERBOSITY_NORMAL, $doesNotContainAssertion];
|
||||||
|
yield 'verbose' => [OutputInterface::VERBOSITY_VERBOSE, $containsAssertion];
|
||||||
|
yield 'very verbose' => [OutputInterface::VERBOSITY_VERY_VERBOSE, $containsAssertion];
|
||||||
|
yield 'debug' => [OutputInterface::VERBOSITY_DEBUG, $containsAssertion];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\DeleteShortUrlCommand;
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\DeleteShortUrlCommand;
|
||||||
@@ -30,7 +32,7 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||||||
$this->commandTester = $this->testerForCommand(new DeleteShortUrlCommand($this->service));
|
$this->commandTester = $this->testerForCommand(new DeleteShortUrlCommand($this->service));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function successMessageIsPrintedIfUrlIsProperlyDeleted(): void
|
public function successMessageIsPrintedIfUrlIsProperlyDeleted(): void
|
||||||
{
|
{
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
@@ -48,7 +50,7 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function invalidShortCodePrintsMessage(): void
|
public function invalidShortCodePrintsMessage(): void
|
||||||
{
|
{
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
@@ -64,10 +66,7 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||||||
self::assertStringContainsString(sprintf('No URL found with short code "%s"', $shortCode), $output);
|
self::assertStringContainsString(sprintf('No URL found with short code "%s"', $shortCode), $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideRetryDeleteAnswers')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideRetryDeleteAnswers
|
|
||||||
*/
|
|
||||||
public function deleteIsRetriedWhenThresholdIsReachedAndQuestionIsAccepted(
|
public function deleteIsRetriedWhenThresholdIsReachedAndQuestionIsAccepted(
|
||||||
array $retryAnswer,
|
array $retryAnswer,
|
||||||
int $expectedDeleteCalls,
|
int $expectedDeleteCalls,
|
||||||
@@ -98,14 +97,14 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||||||
self::assertStringContainsString($expectedMessage, $output);
|
self::assertStringContainsString($expectedMessage, $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideRetryDeleteAnswers(): iterable
|
public static function provideRetryDeleteAnswers(): iterable
|
||||||
{
|
{
|
||||||
yield 'answering yes to retry' => [['yes'], 2, 'Short URL with short code "abc123" successfully deleted.'];
|
yield 'answering yes to retry' => [['yes'], 2, 'Short URL with short code "abc123" successfully deleted.'];
|
||||||
yield 'answering no to retry' => [['no'], 1, 'Short URL was not deleted.'];
|
yield 'answering no to retry' => [['no'], 1, 'Short URL was not deleted.'];
|
||||||
yield 'answering default to retry' => [[PHP_EOL], 1, 'Short URL was not deleted.'];
|
yield 'answering default to retry' => [[PHP_EOL], 1, 'Short URL was not deleted.'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function deleteIsNotRetriedWhenThresholdIsReachedAndQuestionIsDeclined(): void
|
public function deleteIsNotRetriedWhenThresholdIsReachedAndQuestionIsDeclined(): void
|
||||||
{
|
{
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\DeleteShortUrlVisitsCommand;
|
||||||
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
|
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||||
|
use Shlinkio\Shlink\Core\Model\BulkDeleteResult;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlVisitsDeleterInterface;
|
||||||
|
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||||
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
|
||||||
|
class DeleteShortUrlVisitsCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
use CliTestUtilsTrait;
|
||||||
|
|
||||||
|
private CommandTester $commandTester;
|
||||||
|
private MockObject & ShortUrlVisitsDeleterInterface $deleter;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->deleter = $this->createMock(ShortUrlVisitsDeleterInterface::class);
|
||||||
|
$this->commandTester = $this->testerForCommand(new DeleteShortUrlVisitsCommand($this->deleter));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test, DataProvider('provideCancellingInputs')]
|
||||||
|
public function executionIsAbortedIfManuallyCancelled(array $input): void
|
||||||
|
{
|
||||||
|
$this->deleter->expects($this->never())->method('deleteShortUrlVisits');
|
||||||
|
$this->commandTester->setInputs($input);
|
||||||
|
|
||||||
|
$exitCode = $this->commandTester->execute(['shortCode' => 'foo']);
|
||||||
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
|
self::assertEquals(ExitCode::EXIT_SUCCESS, $exitCode);
|
||||||
|
self::assertStringContainsString('Operation aborted', $output);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function provideCancellingInputs(): iterable
|
||||||
|
{
|
||||||
|
yield 'default input' => [[]];
|
||||||
|
yield 'no' => [['no']];
|
||||||
|
yield 'n' => [['n']];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test, DataProvider('provideErrorArgs')]
|
||||||
|
public function warningIsPrintedInCaseOfNotFoundShortUrl(array $args, string $expectedError): void
|
||||||
|
{
|
||||||
|
$this->deleter->expects($this->once())->method('deleteShortUrlVisits')->willThrowException(
|
||||||
|
new ShortUrlNotFoundException(),
|
||||||
|
);
|
||||||
|
$this->commandTester->setInputs(['yes']);
|
||||||
|
|
||||||
|
$exitCode = $this->commandTester->execute($args);
|
||||||
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
|
self::assertEquals(ExitCode::EXIT_WARNING, $exitCode);
|
||||||
|
self::assertStringContainsString($expectedError, $output);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function provideErrorArgs(): iterable
|
||||||
|
{
|
||||||
|
yield 'domain' => [['shortCode' => 'foo'], 'Short URL not found for "foo"'];
|
||||||
|
yield 'no domain' => [['shortCode' => 'foo', '--domain' => 's.test'], 'Short URL not found for "s.test/foo"'];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function successMessageIsPrintedForValidShortUrls(): void
|
||||||
|
{
|
||||||
|
$this->deleter->expects($this->once())->method('deleteShortUrlVisits')->willReturn(new BulkDeleteResult(5));
|
||||||
|
$this->commandTester->setInputs(['yes']);
|
||||||
|
|
||||||
|
$exitCode = $this->commandTester->execute(['shortCode' => 'foo']);
|
||||||
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
|
self::assertEquals(ExitCode::EXIT_SUCCESS, $exitCode);
|
||||||
|
self::assertStringContainsString('Successfully deleted 5 visits', $output);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
|||||||
|
|
||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
use Pagerfanta\Adapter\ArrayAdapter;
|
use Pagerfanta\Adapter\ArrayAdapter;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\GetShortUrlVisitsCommand;
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\GetShortUrlVisitsCommand;
|
||||||
@@ -39,7 +40,7 @@ class GetShortUrlVisitsCommandTest extends TestCase
|
|||||||
$this->commandTester = $this->testerForCommand($command);
|
$this->commandTester = $this->testerForCommand($command);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function noDateFlagsTriesToListWithoutDateRange(): void
|
public function noDateFlagsTriesToListWithoutDateRange(): void
|
||||||
{
|
{
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
@@ -51,7 +52,7 @@ class GetShortUrlVisitsCommandTest extends TestCase
|
|||||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function providingDateFlagsTheListGetsFiltered(): void
|
public function providingDateFlagsTheListGetsFiltered(): void
|
||||||
{
|
{
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
@@ -69,7 +70,7 @@ class GetShortUrlVisitsCommandTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function providingInvalidDatesPrintsWarning(): void
|
public function providingInvalidDatesPrintsWarning(): void
|
||||||
{
|
{
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
@@ -91,7 +92,7 @@ class GetShortUrlVisitsCommandTest extends TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function outputIsProperlyGenerated(): void
|
public function outputIsProperlyGenerated(): void
|
||||||
{
|
{
|
||||||
$visit = Visit::forValidShortUrl(ShortUrl::createFake(), new Visitor('bar', 'foo', '', ''))->locate(
|
$visit = Visit::forValidShortUrl(ShortUrl::createFake(), new Visitor('bar', 'foo', '', ''))->locate(
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
|||||||
|
|
||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
use Pagerfanta\Adapter\ArrayAdapter;
|
use Pagerfanta\Adapter\ArrayAdapter;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
|
||||||
@@ -41,13 +43,13 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
$this->commandTester = $this->testerForCommand($command);
|
$this->commandTester = $this->testerForCommand($command);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function loadingMorePagesCallsListMoreTimes(): void
|
public function loadingMorePagesCallsListMoreTimes(): void
|
||||||
{
|
{
|
||||||
// The paginator will return more than one page
|
// The paginator will return more than one page
|
||||||
$data = [];
|
$data = [];
|
||||||
for ($i = 0; $i < 50; $i++) {
|
for ($i = 0; $i < 50; $i++) {
|
||||||
$data[] = ShortUrl::withLongUrl('url_' . $i);
|
$data[] = ShortUrl::withLongUrl('https://url_' . $i);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->shortUrlService->expects($this->exactly(3))->method('listShortUrls')->withAnyParameters()
|
$this->shortUrlService->expects($this->exactly(3))->method('listShortUrls')->withAnyParameters()
|
||||||
@@ -63,13 +65,13 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
self::assertStringNotContainsString('Continue with page 5?', $output);
|
self::assertStringNotContainsString('Continue with page 5?', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function havingMorePagesButAnsweringNoCallsListJustOnce(): void
|
public function havingMorePagesButAnsweringNoCallsListJustOnce(): void
|
||||||
{
|
{
|
||||||
// The paginator will return more than one page
|
// The paginator will return more than one page
|
||||||
$data = [];
|
$data = [];
|
||||||
for ($i = 0; $i < 30; $i++) {
|
for ($i = 0; $i < 30; $i++) {
|
||||||
$data[] = ShortUrl::withLongUrl('url_' . $i);
|
$data[] = ShortUrl::withLongUrl('https://url_' . $i);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(
|
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(
|
||||||
@@ -89,7 +91,7 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
self::assertStringNotContainsString('Continue with page 3?', $output);
|
self::assertStringNotContainsString('Continue with page 3?', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function passingPageWillMakeListStartOnThatPage(): void
|
public function passingPageWillMakeListStartOnThatPage(): void
|
||||||
{
|
{
|
||||||
$page = 5;
|
$page = 5;
|
||||||
@@ -101,10 +103,7 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
$this->commandTester->execute(['--page' => $page]);
|
$this->commandTester->execute(['--page' => $page]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideOptionalFlags')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideOptionalFlags
|
|
||||||
*/
|
|
||||||
public function provideOptionalFlagsMakesNewColumnsToBeIncluded(
|
public function provideOptionalFlagsMakesNewColumnsToBeIncluded(
|
||||||
array $input,
|
array $input,
|
||||||
array $expectedContents,
|
array $expectedContents,
|
||||||
@@ -115,7 +114,7 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
ShortUrlsParams::emptyInstance(),
|
ShortUrlsParams::emptyInstance(),
|
||||||
)->willReturn(new Paginator(new ArrayAdapter([
|
)->willReturn(new Paginator(new ArrayAdapter([
|
||||||
ShortUrl::create(ShortUrlCreation::fromRawData([
|
ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
'longUrl' => 'foo.com',
|
'longUrl' => 'https://foo.com',
|
||||||
'tags' => ['foo', 'bar', 'baz'],
|
'tags' => ['foo', 'bar', 'baz'],
|
||||||
'apiKey' => $apiKey,
|
'apiKey' => $apiKey,
|
||||||
])),
|
])),
|
||||||
@@ -137,7 +136,7 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideOptionalFlags(): iterable
|
public static function provideOptionalFlags(): iterable
|
||||||
{
|
{
|
||||||
$apiKey = ApiKey::fromMeta(ApiKeyMeta::withName('my api key'));
|
$apiKey = ApiKey::fromMeta(ApiKeyMeta::withName('my api key'));
|
||||||
$key = $apiKey->toString();
|
$key = $apiKey->toString();
|
||||||
@@ -145,13 +144,19 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
yield 'tags only' => [
|
yield 'tags only' => [
|
||||||
['--show-tags' => true],
|
['--show-tags' => true],
|
||||||
['| Tags ', '| foo, bar, baz'],
|
['| Tags ', '| foo, bar, baz'],
|
||||||
['| API Key ', '| API Key Name |', $key, '| my api key'],
|
['| API Key ', '| API Key Name |', $key, '| my api key', '| Domain', '| DEFAULT'],
|
||||||
|
$apiKey,
|
||||||
|
];
|
||||||
|
yield 'domain only' => [
|
||||||
|
['--show-domain' => true],
|
||||||
|
['| Domain', '| DEFAULT'],
|
||||||
|
['| Tags ', '| foo, bar, baz', '| API Key ', '| API Key Name |', $key, '| my api key'],
|
||||||
$apiKey,
|
$apiKey,
|
||||||
];
|
];
|
||||||
yield 'api key only' => [
|
yield 'api key only' => [
|
||||||
['--show-api-key' => true],
|
['--show-api-key' => true],
|
||||||
['| API Key ', $key],
|
['| API Key ', $key],
|
||||||
['| Tags ', '| foo, bar, baz', '| API Key Name |', '| my api key'],
|
['| Tags ', '| foo, bar, baz', '| API Key Name |', '| my api key', '| Domain', '| DEFAULT'],
|
||||||
$apiKey,
|
$apiKey,
|
||||||
];
|
];
|
||||||
yield 'api key name only' => [
|
yield 'api key name only' => [
|
||||||
@@ -166,18 +171,30 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
['| API Key Name |', '| my api key'],
|
['| API Key Name |', '| my api key'],
|
||||||
$apiKey,
|
$apiKey,
|
||||||
];
|
];
|
||||||
|
yield 'tags and domain' => [
|
||||||
|
['--show-tags' => true, '--show-domain' => true],
|
||||||
|
['| Tags ', '| foo, bar, baz', '| Domain', '| DEFAULT'],
|
||||||
|
['| API Key Name |', '| my api key'],
|
||||||
|
$apiKey,
|
||||||
|
];
|
||||||
yield 'all' => [
|
yield 'all' => [
|
||||||
['--show-tags' => true, '--show-api-key' => true, '--show-api-key-name' => true],
|
['--show-tags' => true, '--show-domain' => true, '--show-api-key' => true, '--show-api-key-name' => true],
|
||||||
['| API Key ', '| Tags ', '| API Key Name |', '| foo, bar, baz', $key, '| my api key'],
|
[
|
||||||
|
'| API Key ',
|
||||||
|
'| Tags ',
|
||||||
|
'| API Key Name |',
|
||||||
|
'| foo, bar, baz',
|
||||||
|
$key,
|
||||||
|
'| my api key',
|
||||||
|
'| Domain',
|
||||||
|
'| DEFAULT',
|
||||||
|
],
|
||||||
[],
|
[],
|
||||||
$apiKey,
|
$apiKey,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideArgs')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideArgs
|
|
||||||
*/
|
|
||||||
public function serviceIsInvokedWithProvidedArgs(
|
public function serviceIsInvokedWithProvidedArgs(
|
||||||
array $commandArgs,
|
array $commandArgs,
|
||||||
?int $page,
|
?int $page,
|
||||||
@@ -200,7 +217,7 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
$this->commandTester->execute($commandArgs);
|
$this->commandTester->execute($commandArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideArgs(): iterable
|
public static function provideArgs(): iterable
|
||||||
{
|
{
|
||||||
yield [[], 1, null, [], TagsMode::ANY->value];
|
yield [[], 1, null, [], TagsMode::ANY->value];
|
||||||
yield [['--page' => $page = 3], $page, null, [], TagsMode::ANY->value];
|
yield [['--page' => $page = 3], $page, null, [], TagsMode::ANY->value];
|
||||||
@@ -241,10 +258,7 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideOrderBy')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideOrderBy
|
|
||||||
*/
|
|
||||||
public function orderByIsProperlyComputed(array $commandArgs, ?string $expectedOrderBy): void
|
public function orderByIsProperlyComputed(array $commandArgs, ?string $expectedOrderBy): void
|
||||||
{
|
{
|
||||||
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(ShortUrlsParams::fromRawData([
|
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(ShortUrlsParams::fromRawData([
|
||||||
@@ -255,7 +269,7 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
$this->commandTester->execute($commandArgs);
|
$this->commandTester->execute($commandArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideOrderBy(): iterable
|
public static function provideOrderBy(): iterable
|
||||||
{
|
{
|
||||||
yield [[], null];
|
yield [[], null];
|
||||||
yield [['--order-by' => 'visits'], 'visits'];
|
yield [['--order-by' => 'visits'], 'visits'];
|
||||||
@@ -264,7 +278,7 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
yield [['--order-by' => 'title-DESC'], 'title-DESC'];
|
yield [['--order-by' => 'title-DESC'], 'title-DESC'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function requestingAllElementsWillSetItemsPerPage(): void
|
public function requestingAllElementsWillSetItemsPerPage(): void
|
||||||
{
|
{
|
||||||
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(ShortUrlsParams::fromRawData([
|
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(ShortUrlsParams::fromRawData([
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\ResolveUrlCommand;
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\ResolveUrlCommand;
|
||||||
@@ -31,7 +32,7 @@ class ResolveUrlCommandTest extends TestCase
|
|||||||
$this->commandTester = $this->testerForCommand(new ResolveUrlCommand($this->urlResolver));
|
$this->commandTester = $this->testerForCommand(new ResolveUrlCommand($this->urlResolver));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function correctShortCodeResolvesUrl(): void
|
public function correctShortCodeResolvesUrl(): void
|
||||||
{
|
{
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
@@ -46,7 +47,7 @@ class ResolveUrlCommandTest extends TestCase
|
|||||||
self::assertEquals('Long URL: ' . $expectedUrl . PHP_EOL, $output);
|
self::assertEquals('Long URL: ' . $expectedUrl . PHP_EOL, $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function incorrectShortCodeOutputsErrorMessage(): void
|
public function incorrectShortCodeOutputsErrorMessage(): void
|
||||||
{
|
{
|
||||||
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain('abc123');
|
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain('abc123');
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\CLI\Command\Tag\DeleteTagsCommand;
|
use Shlinkio\Shlink\CLI\Command\Tag\DeleteTagsCommand;
|
||||||
@@ -24,7 +25,7 @@ class DeleteTagsCommandTest extends TestCase
|
|||||||
$this->commandTester = $this->testerForCommand(new DeleteTagsCommand($this->tagService));
|
$this->commandTester = $this->testerForCommand(new DeleteTagsCommand($this->tagService));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function errorIsReturnedWhenNoTagsAreProvided(): void
|
public function errorIsReturnedWhenNoTagsAreProvided(): void
|
||||||
{
|
{
|
||||||
$this->commandTester->execute([]);
|
$this->commandTester->execute([]);
|
||||||
@@ -33,7 +34,7 @@ class DeleteTagsCommandTest extends TestCase
|
|||||||
self::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]
|
||||||
public function serviceIsInvokedOnSuccess(): void
|
public function serviceIsInvokedOnSuccess(): void
|
||||||
{
|
{
|
||||||
$tagNames = ['foo', 'bar'];
|
$tagNames = ['foo', 'bar'];
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
||||||
|
|
||||||
use Pagerfanta\Adapter\ArrayAdapter;
|
use Pagerfanta\Adapter\ArrayAdapter;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\CLI\Command\Tag\GetTagVisitsCommand;
|
use Shlinkio\Shlink\CLI\Command\Tag\GetTagVisitsCommand;
|
||||||
@@ -37,7 +38,7 @@ class GetTagVisitsCommandTest extends TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function outputIsProperlyGenerated(): void
|
public function outputIsProperlyGenerated(): void
|
||||||
{
|
{
|
||||||
$shortUrl = ShortUrl::createFake();
|
$shortUrl = ShortUrl::createFake();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
||||||
|
|
||||||
use Pagerfanta\Adapter\ArrayAdapter;
|
use Pagerfanta\Adapter\ArrayAdapter;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\CLI\Command\Tag\ListTagsCommand;
|
use Shlinkio\Shlink\CLI\Command\Tag\ListTagsCommand;
|
||||||
@@ -27,7 +28,7 @@ class ListTagsCommandTest extends TestCase
|
|||||||
$this->commandTester = $this->testerForCommand(new ListTagsCommand($this->tagService));
|
$this->commandTester = $this->testerForCommand(new ListTagsCommand($this->tagService));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function noTagsPrintsEmptyMessage(): void
|
public function noTagsPrintsEmptyMessage(): void
|
||||||
{
|
{
|
||||||
$this->tagService->expects($this->once())->method('tagsInfo')->withAnyParameters()->willReturn(
|
$this->tagService->expects($this->once())->method('tagsInfo')->withAnyParameters()->willReturn(
|
||||||
@@ -40,7 +41,7 @@ class ListTagsCommandTest extends TestCase
|
|||||||
self::assertStringContainsString('No tags found', $output);
|
self::assertStringContainsString('No tags found', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function listOfTagsIsPrinted(): void
|
public function listOfTagsIsPrinted(): void
|
||||||
{
|
{
|
||||||
$this->tagService->expects($this->once())->method('tagsInfo')->withAnyParameters()->willReturn(
|
$this->tagService->expects($this->once())->method('tagsInfo')->withAnyParameters()->willReturn(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\CLI\Command\Tag\RenameTagCommand;
|
use Shlinkio\Shlink\CLI\Command\Tag\RenameTagCommand;
|
||||||
@@ -27,7 +28,7 @@ class RenameTagCommandTest extends TestCase
|
|||||||
$this->commandTester = $this->testerForCommand(new RenameTagCommand($this->tagService));
|
$this->commandTester = $this->testerForCommand(new RenameTagCommand($this->tagService));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function errorIsPrintedIfExceptionIsThrown(): void
|
public function errorIsPrintedIfExceptionIsThrown(): void
|
||||||
{
|
{
|
||||||
$oldName = 'foo';
|
$oldName = 'foo';
|
||||||
@@ -45,7 +46,7 @@ class RenameTagCommandTest extends TestCase
|
|||||||
self::assertStringContainsString('Tag with name "foo" could not be found', $output);
|
self::assertStringContainsString('Tag with name "foo" could not be found', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function successIsPrintedIfNoErrorOccurs(): void
|
public function successIsPrintedIfNoErrorOccurs(): void
|
||||||
{
|
{
|
||||||
$oldName = 'foo';
|
$oldName = 'foo';
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Shlinkio\Shlink\CLI\Command\Visit\DeleteOrphanVisitsCommand;
|
||||||
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
|
use Shlinkio\Shlink\Core\Model\BulkDeleteResult;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\VisitsDeleterInterface;
|
||||||
|
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||||
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
|
||||||
|
class DeleteOrphanVisitsCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
use CliTestUtilsTrait;
|
||||||
|
|
||||||
|
private CommandTester $commandTester;
|
||||||
|
private MockObject & VisitsDeleterInterface $deleter;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->deleter = $this->createMock(VisitsDeleterInterface::class);
|
||||||
|
$this->commandTester = $this->testerForCommand(new DeleteOrphanVisitsCommand($this->deleter));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function successMessageIsPrintedAfterDeletion(): void
|
||||||
|
{
|
||||||
|
$this->deleter->expects($this->once())->method('deleteOrphanVisits')->willReturn(new BulkDeleteResult(5));
|
||||||
|
$this->commandTester->setInputs(['yes']);
|
||||||
|
|
||||||
|
$exitCode = $this->commandTester->execute([]);
|
||||||
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
|
self::assertEquals(ExitCode::EXIT_SUCCESS, $exitCode);
|
||||||
|
self::assertStringContainsString('You are about to delete all orphan visits.', $output);
|
||||||
|
self::assertStringContainsString('Successfully deleted 5 visits', $output);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user