mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-06 23:33:13 +08:00
Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 }}
|
||||||
|
|||||||
6
.github/workflows/ci-mutation-tests.yml
vendored
6
.github/workflows/ci-mutation-tests.yml
vendored
@@ -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' }}
|
||||||
|
|||||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -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:
|
||||||
|
|||||||
2
.github/workflows/publish-swagger-spec.yml
vendored
2
.github/workflows/publish-swagger-spec.yml
vendored
@@ -15,7 +15,7 @@ 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:
|
||||||
|
|||||||
80
CHANGELOG.md
80
CHANGELOG.md
@@ -4,6 +4,82 @@ 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.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 +101,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*
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -20,63 +20,61 @@
|
|||||||
"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.4",
|
||||||
"shlinkio/shlink-config": "^2.4",
|
"shlinkio/shlink-config": "^2.4",
|
||||||
"shlinkio/shlink-event-dispatcher": "^2.6",
|
"shlinkio/shlink-event-dispatcher": "^2.6",
|
||||||
"shlinkio/shlink-importer": "^5.0",
|
"shlinkio/shlink-importer": "^5.0",
|
||||||
"shlinkio/shlink-installer": "^8.3",
|
"shlinkio/shlink-installer": "^8.3",
|
||||||
"shlinkio/shlink-ip-geolocation": "^3.2",
|
"shlinkio/shlink-ip-geolocation": "^3.2",
|
||||||
"spiral/roadrunner": "^2.11",
|
"spiral/roadrunner": "^2.12",
|
||||||
"spiral/roadrunner-jobs": "^2.5",
|
"spiral/roadrunner-jobs": "^2.7",
|
||||||
"symfony/console": "^6.1",
|
"symfony/console": "^6.2",
|
||||||
"symfony/filesystem": "^6.1",
|
"symfony/filesystem": "^6.2",
|
||||||
"symfony/lock": "^6.1",
|
"symfony/lock": "^6.2",
|
||||||
"symfony/process": "^6.1",
|
"symfony/process": "^6.2",
|
||||||
"symfony/string": "^6.1"
|
"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.26.19",
|
||||||
"infection/infection": "^0.26.15",
|
|
||||||
"openswoole/ide-helper": "~4.11.5",
|
"openswoole/ide-helper": "~4.11.5",
|
||||||
"phpstan/phpstan": "^1.8",
|
"phpstan/phpstan": "^1.9",
|
||||||
"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.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.5",
|
||||||
"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": {
|
||||||
@@ -133,7 +131,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",
|
||||||
|
|||||||
@@ -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',
|
||||||
|
// ],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,16 @@ 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');
|
use function Shlinkio\Shlink\Config\runningInOpenswoole;
|
||||||
|
|
||||||
|
$logToStream = runningInOpenswoole();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'logger' => [
|
'logger' => [
|
||||||
'Shlink' => [
|
'Shlink' => [
|
||||||
// For swoole, send logs as stream
|
// For openswoole, send logs as stream
|
||||||
'type' => $isSwoole ? LoggerType::STREAM->value : LoggerType::FILE->value,
|
'type' => $logToStream ? LoggerType::STREAM->value : LoggerType::FILE->value,
|
||||||
'level' => Level::Debug->value,
|
'level' => Level::Debug->value,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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';
|
|
||||||
|
|||||||
@@ -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'] ?? []),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -116,7 +115,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 => [
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use Shlinkio\Shlink\CLI\ApiKey\RoleResolverInterface;
|
|||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||||
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)]),
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ namespace Shlinkio\Shlink\CLI\Command\Db;
|
|||||||
|
|
||||||
use Doctrine\DBAL\Connection;
|
use Doctrine\DBAL\Connection;
|
||||||
use Doctrine\DBAL\Platforms\SqlitePlatform;
|
use Doctrine\DBAL\Platforms\SqlitePlatform;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||||
use Shlinkio\Shlink\CLI\Util\ProcessRunnerInterface;
|
use Shlinkio\Shlink\CLI\Util\ProcessRunnerInterface;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
@@ -15,12 +17,13 @@ use Symfony\Component\Lock\LockFactory;
|
|||||||
use Symfony\Component\Process\PhpExecutableFinder;
|
use Symfony\Component\Process\PhpExecutableFinder;
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +78,8 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
|
|||||||
// Otherwise, it will fail to connect and will not be able to create the new database
|
// Otherwise, it will fail to connect and will not be able to create the new database
|
||||||
$schemaManager = $this->noDbNameConn->createSchemaManager();
|
$schemaManager = $this->noDbNameConn->createSchemaManager();
|
||||||
$databases = $schemaManager->listDatabases();
|
$databases = $schemaManager->listDatabases();
|
||||||
|
// We cannot use getDatabase() to get the database name here, because then the driver will try to connect, and
|
||||||
|
// it does not exist yet. We need to read from the raw params instead.
|
||||||
$shlinkDatabase = $this->regularConn->getParams()['dbname'] ?? null;
|
$shlinkDatabase = $this->regularConn->getParams()['dbname'] ?? null;
|
||||||
|
|
||||||
if ($shlinkDatabase !== null && ! contains($databases, $shlinkDatabase)) {
|
if ($shlinkDatabase !== null && ! contains($databases, $shlinkDatabase)) {
|
||||||
@@ -83,10 +89,14 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
|
|||||||
|
|
||||||
private function schemaExists(): bool
|
private function schemaExists(): bool
|
||||||
{
|
{
|
||||||
// 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 by the migrations.
|
|
||||||
$schemaManager = $this->regularConn->createSchemaManager();
|
$schemaManager = $this->regularConn->createSchemaManager();
|
||||||
return ! empty(filter($schemaManager->listTableNames(), fn (string $table) => $table !== MIGRATIONS_TABLE));
|
$existingTables = $schemaManager->listTableNames();
|
||||||
|
|
||||||
|
$allMetadata = $this->em->getMetadataFactory()->getAllMetadata();
|
||||||
|
$shlinkTables = map($allMetadata, static fn (ClassMetadata $metadata) => $metadata->getTableName());
|
||||||
|
|
||||||
|
// If at least one of the shlink tables exist, we will consider the database exists somehow.
|
||||||
|
// Any other inconsistency will be taken care of by the migrations.
|
||||||
|
return some($shlinkTables, static fn (string $shlinkTable) => contains($existingTables, $shlinkTable));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,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,9 +176,14 @@ 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 ExitCodes::EXIT_SUCCESS;
|
||||||
} catch (InvalidUrlException | NonUniqueSlugException $e) {
|
} catch (InvalidUrlException | NonUniqueSlugException $e) {
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ 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\ExitCodes;
|
||||||
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]);
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ 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\ExitCodes;
|
||||||
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]);
|
||||||
@@ -23,7 +22,7 @@ class ListApiKeysTest extends CliTestCase
|
|||||||
self::assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode);
|
self::assertEquals(ExitCodes::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;
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ use Doctrine\DBAL\Driver;
|
|||||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||||
use Doctrine\DBAL\Platforms\SqlitePlatform;
|
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 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,25 +53,27 @@ 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';
|
$shlinkDatabase = 'shlink_database';
|
||||||
$this->regularConn->expects($this->once())->method('getParams')->willReturn(['dbname' => $shlinkDatabase]);
|
$this->regularConn->expects($this->once())->method('getParams')->willReturn(['dbname' => $shlinkDatabase]);
|
||||||
|
$metadataMock = $this->createMock(ClassMetadata::class);
|
||||||
|
$metadataMock->expects($this->once())->method('getTableName')->willReturn('foo_table');
|
||||||
|
$this->metadataFactory->method('getAllMetadata')->willReturn([$metadataMock]);
|
||||||
$this->schemaManager->expects($this->once())->method('listDatabases')->willReturn(
|
$this->schemaManager->expects($this->once())->method('listDatabases')->willReturn(
|
||||||
['foo', $shlinkDatabase, 'bar'],
|
['foo', $shlinkDatabase, 'bar'],
|
||||||
);
|
);
|
||||||
@@ -81,29 +87,30 @@ 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
|
||||||
{
|
{
|
||||||
$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->metadataFactory->method('getAllMetadata')->willReturn([]);
|
||||||
$this->schemaManager->expects($this->once())->method('listDatabases')->willReturn(['foo', 'bar']);
|
$this->schemaManager->expects($this->once())->method('listDatabases')->willReturn(['foo', 'bar']);
|
||||||
$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')->willReturn(
|
||||||
['foo_table', 'bar_table', MIGRATIONS_TABLE],
|
['foo_table', 'bar_table'],
|
||||||
);
|
);
|
||||||
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(AbstractPlatform::class));
|
$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';
|
$shlinkDatabase = 'shlink_database';
|
||||||
$this->regularConn->expects($this->once())->method('getParams')->willReturn(['dbname' => $shlinkDatabase]);
|
$this->regularConn->expects($this->once())->method('getParams')->willReturn(['dbname' => $shlinkDatabase]);
|
||||||
|
$metadata = $this->createMock(ClassMetadata::class);
|
||||||
|
$metadata->method('getTableName')->willReturn('shlink_table');
|
||||||
|
$this->metadataFactory->method('getAllMetadata')->willReturn([$metadata]);
|
||||||
$this->schemaManager->expects($this->once())->method('listDatabases')->willReturn(
|
$this->schemaManager->expects($this->once())->method('listDatabases')->willReturn(
|
||||||
['foo', $shlinkDatabase, 'bar'],
|
['foo', $shlinkDatabase, 'bar'],
|
||||||
);
|
);
|
||||||
@@ -124,18 +131,19 @@ 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 */
|
#[Test]
|
||||||
public function databaseCheckIsSkippedForSqlite(): void
|
public function databaseCheckIsSkippedForSqlite(): void
|
||||||
{
|
{
|
||||||
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(SqlitePlatform::class));
|
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(SqlitePlatform::class));
|
||||||
|
|
||||||
$this->regularConn->expects($this->never())->method('getParams');
|
$this->regularConn->expects($this->never())->method('getParams');
|
||||||
|
$this->metadataFactory->expects($this->once())->method('getAllMetadata')->willReturn([]);
|
||||||
$this->schemaManager->expects($this->never())->method('listDatabases');
|
$this->schemaManager->expects($this->never())->method('listDatabases');
|
||||||
$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']);
|
||||||
|
|||||||
@@ -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,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\ListDomainsCommand;
|
use Shlinkio\Shlink\CLI\Command\Domain\ListDomainsCommand;
|
||||||
@@ -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');
|
||||||
@@ -57,7 +56,7 @@ class ListDomainsCommandTest extends TestCase
|
|||||||
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideInputsAndOutputs(): iterable
|
public static function provideInputsAndOutputs(): iterable
|
||||||
{
|
{
|
||||||
$withoutRedirectsOutput = <<<OUTPUT
|
$withoutRedirectsOutput = <<<OUTPUT
|
||||||
+---------+------------+
|
+---------+------------+
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ 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;
|
||||||
@@ -15,8 +18,10 @@ 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
|
||||||
@@ -45,11 +50,13 @@ class CreateShortUrlCommandTest extends TestCase
|
|||||||
$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 +64,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(ExitCodes::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';
|
||||||
@@ -80,7 +88,7 @@ class CreateShortUrlCommandTest extends TestCase
|
|||||||
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(
|
||||||
@@ -95,7 +103,7 @@ class CreateShortUrlCommandTest extends TestCase
|
|||||||
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 +112,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',
|
||||||
);
|
);
|
||||||
@@ -119,10 +127,7 @@ class CreateShortUrlCommandTest extends TestCase
|
|||||||
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,7 +135,7 @@ 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';
|
||||||
@@ -139,7 +144,7 @@ class CreateShortUrlCommandTest extends TestCase
|
|||||||
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
self::assertEquals(ExitCodes::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 'non-default domain foo' => [['--domain' => 'foo.com'], 'foo.com'];
|
||||||
@@ -147,10 +152,7 @@ class CreateShortUrlCommandTest extends TestCase
|
|||||||
yield 'default domain' => [['--domain' => self::DEFAULT_DOMAIN], null];
|
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 +161,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';
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -174,10 +173,7 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideArgs')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideArgs
|
|
||||||
*/
|
|
||||||
public function serviceIsInvokedWithProvidedArgs(
|
public function serviceIsInvokedWithProvidedArgs(
|
||||||
array $commandArgs,
|
array $commandArgs,
|
||||||
?int $page,
|
?int $page,
|
||||||
@@ -200,7 +196,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 +237,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 +248,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 +257,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';
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
|
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
|
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\Visit\DownloadGeoLiteDbCommand;
|
use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand;
|
||||||
@@ -29,10 +31,7 @@ class DownloadGeoLiteDbCommandTest extends TestCase
|
|||||||
$this->commandTester = $this->testerForCommand(new DownloadGeoLiteDbCommand($this->dbUpdater));
|
$this->commandTester = $this->testerForCommand(new DownloadGeoLiteDbCommand($this->dbUpdater));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideFailureParams')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideFailureParams
|
|
||||||
*/
|
|
||||||
public function showsProperMessageWhenGeoLiteUpdateFails(
|
public function showsProperMessageWhenGeoLiteUpdateFails(
|
||||||
bool $olderDbExists,
|
bool $olderDbExists,
|
||||||
string $expectedMessage,
|
string $expectedMessage,
|
||||||
@@ -61,7 +60,7 @@ class DownloadGeoLiteDbCommandTest extends TestCase
|
|||||||
self::assertSame($expectedExitCode, $exitCode);
|
self::assertSame($expectedExitCode, $exitCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideFailureParams(): iterable
|
public static function provideFailureParams(): iterable
|
||||||
{
|
{
|
||||||
yield 'existing db' => [
|
yield 'existing db' => [
|
||||||
true,
|
true,
|
||||||
@@ -75,10 +74,7 @@ class DownloadGeoLiteDbCommandTest extends TestCase
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideSuccessParams')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideSuccessParams
|
|
||||||
*/
|
|
||||||
public function printsExpectedMessageWhenNoErrorOccurs(callable $checkUpdateBehavior, string $expectedMessage): void
|
public function printsExpectedMessageWhenNoErrorOccurs(callable $checkUpdateBehavior, string $expectedMessage): void
|
||||||
{
|
{
|
||||||
$this->dbUpdater->expects($this->once())->method('checkDbUpdate')->withAnyParameters()->willReturnCallback(
|
$this->dbUpdater->expects($this->once())->method('checkDbUpdate')->withAnyParameters()->willReturnCallback(
|
||||||
@@ -93,7 +89,7 @@ class DownloadGeoLiteDbCommandTest extends TestCase
|
|||||||
self::assertSame(ExitCodes::EXIT_SUCCESS, $exitCode);
|
self::assertSame(ExitCodes::EXIT_SUCCESS, $exitCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideSuccessParams(): iterable
|
public static function provideSuccessParams(): iterable
|
||||||
{
|
{
|
||||||
yield 'up to date db' => [fn () => GeolocationResult::CHECK_SKIPPED, '[INFO] GeoLite2 db file is up to date.'];
|
yield 'up to date db' => [fn () => GeolocationResult::CHECK_SKIPPED, '[INFO] GeoLite2 db file is up to date.'];
|
||||||
yield 'outdated db' => [function (callable $beforeDownload): GeolocationResult {
|
yield 'outdated db' => [function (callable $beforeDownload): GeolocationResult {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
|
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
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\Visit\GetNonOrphanVisitsCommand;
|
use Shlinkio\Shlink\CLI\Command\Visit\GetNonOrphanVisitsCommand;
|
||||||
@@ -37,7 +38,7 @@ class GetNonOrphanVisitsCommandTest 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\Visit;
|
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
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\Visit\GetOrphanVisitsCommand;
|
use Shlinkio\Shlink\CLI\Command\Visit\GetOrphanVisitsCommand;
|
||||||
@@ -30,7 +31,7 @@ class GetOrphanVisitsCommandTest extends TestCase
|
|||||||
$this->commandTester = $this->testerForCommand(new GetOrphanVisitsCommand($this->visitsHelper));
|
$this->commandTester = $this->testerForCommand(new GetOrphanVisitsCommand($this->visitsHelper));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function outputIsProperlyGenerated(): void
|
public function outputIsProperlyGenerated(): void
|
||||||
{
|
{
|
||||||
$visit = Visit::forBasePath(new Visitor('bar', 'foo', '', ''))->locate(
|
$visit = Visit::forBasePath(new Visitor('bar', 'foo', '', ''))->locate(
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
|
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
|
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\Visit\DownloadGeoLiteDbCommand;
|
use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand;
|
||||||
@@ -55,10 +57,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
$this->commandTester = $this->testerForCommand($command, $this->downloadDbCommand);
|
$this->commandTester = $this->testerForCommand($command, $this->downloadDbCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideArgs')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideArgs
|
|
||||||
*/
|
|
||||||
public function expectedSetOfVisitsIsProcessedBasedOnArgs(
|
public function expectedSetOfVisitsIsProcessedBasedOnArgs(
|
||||||
int $expectedUnlocatedCalls,
|
int $expectedUnlocatedCalls,
|
||||||
int $expectedEmptyCalls,
|
int $expectedEmptyCalls,
|
||||||
@@ -100,17 +99,14 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideArgs(): iterable
|
public static function provideArgs(): iterable
|
||||||
{
|
{
|
||||||
yield 'no args' => [1, 0, 0, false, []];
|
yield 'no args' => [1, 0, 0, false, []];
|
||||||
yield 'retry' => [1, 1, 0, false, ['--retry' => true]];
|
yield 'retry' => [1, 1, 0, false, ['--retry' => true]];
|
||||||
yield 'all' => [0, 0, 1, true, ['--retry' => true, '--all' => true]];
|
yield 'all' => [0, 0, 1, true, ['--retry' => true, '--all' => true]];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideIgnoredAddresses')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideIgnoredAddresses
|
|
||||||
*/
|
|
||||||
public function localhostAndEmptyAddressesAreIgnored(IpCannotBeLocatedException $e, string $message): void
|
public function localhostAndEmptyAddressesAreIgnored(IpCannotBeLocatedException $e, string $message): void
|
||||||
{
|
{
|
||||||
$visit = Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance());
|
$visit = Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance());
|
||||||
@@ -131,13 +127,13 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
self::assertStringContainsString($message, $output);
|
self::assertStringContainsString($message, $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideIgnoredAddresses(): iterable
|
public static function provideIgnoredAddresses(): iterable
|
||||||
{
|
{
|
||||||
yield 'empty address' => [IpCannotBeLocatedException::forEmptyAddress(), 'Ignored visit with no IP address'];
|
yield 'empty address' => [IpCannotBeLocatedException::forEmptyAddress(), 'Ignored visit with no IP address'];
|
||||||
yield 'localhost address' => [IpCannotBeLocatedException::forLocalhost(), 'Ignored localhost address'];
|
yield 'localhost address' => [IpCannotBeLocatedException::forLocalhost(), 'Ignored localhost address'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function errorWhileLocatingIpIsDisplayed(): void
|
public function errorWhileLocatingIpIsDisplayed(): void
|
||||||
{
|
{
|
||||||
$visit = Visit::forValidShortUrl(ShortUrl::createFake(), new Visitor('', '', '1.2.3.4', ''));
|
$visit = Visit::forValidShortUrl(ShortUrl::createFake(), new Visitor('', '', '1.2.3.4', ''));
|
||||||
@@ -168,7 +164,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function noActionIsPerformedIfLockIsAcquired(): void
|
public function noActionIsPerformedIfLockIsAcquired(): void
|
||||||
{
|
{
|
||||||
$this->lock->method('acquire')->with($this->isFalse())->willReturn(false);
|
$this->lock->method('acquire')->with($this->isFalse())->willReturn(false);
|
||||||
@@ -186,7 +182,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function showsProperMessageWhenGeoLiteUpdateFails(): void
|
public function showsProperMessageWhenGeoLiteUpdateFails(): void
|
||||||
{
|
{
|
||||||
$this->lock->method('acquire')->with($this->isFalse())->willReturn(true);
|
$this->lock->method('acquire')->with($this->isFalse())->willReturn(true);
|
||||||
@@ -199,7 +195,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
self::assertStringContainsString('It is not possible to locate visits without a GeoLite2 db file.', $output);
|
self::assertStringContainsString('It is not possible to locate visits without a GeoLite2 db file.', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function providingAllFlagOnItsOwnDisplaysNotice(): void
|
public function providingAllFlagOnItsOwnDisplaysNotice(): void
|
||||||
{
|
{
|
||||||
$this->lock->method('acquire')->with($this->isFalse())->willReturn(true);
|
$this->lock->method('acquire')->with($this->isFalse())->willReturn(true);
|
||||||
@@ -211,10 +207,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
self::assertStringContainsString('The --all flag has no effect on its own', $output);
|
self::assertStringContainsString('The --all flag has no effect on its own', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideAbortInputs')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideAbortInputs
|
|
||||||
*/
|
|
||||||
public function processingAllCancelsCommandIfUserDoesNotActivelyAgreeToConfirmation(array $inputs): void
|
public function processingAllCancelsCommandIfUserDoesNotActivelyAgreeToConfirmation(array $inputs): void
|
||||||
{
|
{
|
||||||
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS);
|
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS);
|
||||||
@@ -226,7 +219,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
$this->commandTester->execute(['--all' => true, '--retry' => true]);
|
$this->commandTester->execute(['--all' => true, '--retry' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideAbortInputs(): iterable
|
public static function provideAbortInputs(): iterable
|
||||||
{
|
{
|
||||||
yield 'n' => [['n']];
|
yield 'n' => [['n']];
|
||||||
yield 'no' => [['no']];
|
yield 'no' => [['no']];
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioTest\Shlink\CLI;
|
namespace ShlinkioTest\Shlink\CLI;
|
||||||
|
|
||||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\CLI\ConfigProvider;
|
use Shlinkio\Shlink\CLI\ConfigProvider;
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ class ConfigProviderTest extends TestCase
|
|||||||
$this->configProvider = new ConfigProvider();
|
$this->configProvider = new ConfigProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function configIsProperlyReturned(): void
|
public function configIsProperlyReturned(): void
|
||||||
{
|
{
|
||||||
$config = ($this->configProvider)();
|
$config = ($this->configProvider)();
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioTest\Shlink\CLI\Exception;
|
namespace ShlinkioTest\Shlink\CLI\Exception;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||||
@@ -12,10 +14,7 @@ use Throwable;
|
|||||||
|
|
||||||
class GeolocationDbUpdateFailedExceptionTest extends TestCase
|
class GeolocationDbUpdateFailedExceptionTest extends TestCase
|
||||||
{
|
{
|
||||||
/**
|
#[Test, DataProvider('providePrev')]
|
||||||
* @test
|
|
||||||
* @dataProvider providePrev
|
|
||||||
*/
|
|
||||||
public function withOlderDbBuildsException(?Throwable $prev): void
|
public function withOlderDbBuildsException(?Throwable $prev): void
|
||||||
{
|
{
|
||||||
$e = GeolocationDbUpdateFailedException::withOlderDb($prev);
|
$e = GeolocationDbUpdateFailedException::withOlderDb($prev);
|
||||||
@@ -29,10 +28,7 @@ class GeolocationDbUpdateFailedExceptionTest extends TestCase
|
|||||||
self::assertEquals($prev, $e->getPrevious());
|
self::assertEquals($prev, $e->getPrevious());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('providePrev')]
|
||||||
* @test
|
|
||||||
* @dataProvider providePrev
|
|
||||||
*/
|
|
||||||
public function withoutOlderDbBuildsException(?Throwable $prev): void
|
public function withoutOlderDbBuildsException(?Throwable $prev): void
|
||||||
{
|
{
|
||||||
$e = GeolocationDbUpdateFailedException::withoutOlderDb($prev);
|
$e = GeolocationDbUpdateFailedException::withoutOlderDb($prev);
|
||||||
@@ -46,14 +42,14 @@ class GeolocationDbUpdateFailedExceptionTest extends TestCase
|
|||||||
self::assertEquals($prev, $e->getPrevious());
|
self::assertEquals($prev, $e->getPrevious());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function providePrev(): iterable
|
public static function providePrev(): iterable
|
||||||
{
|
{
|
||||||
yield 'no prev' => [null];
|
yield 'no prev' => [null];
|
||||||
yield 'RuntimeException' => [new RuntimeException('prev')];
|
yield 'RuntimeException' => [new RuntimeException('prev')];
|
||||||
yield 'Exception' => [new Exception('prev')];
|
yield 'Exception' => [new Exception('prev')];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function withInvalidEpochInOldDbBuildsException(): void
|
public function withInvalidEpochInOldDbBuildsException(): void
|
||||||
{
|
{
|
||||||
$e = GeolocationDbUpdateFailedException::withInvalidEpochInOldDb('foobar');
|
$e = GeolocationDbUpdateFailedException::withInvalidEpochInOldDb('foobar');
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Exception;
|
namespace ShlinkioTest\Shlink\CLI\Exception;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\CLI\Exception\InvalidRoleConfigException;
|
use Shlinkio\Shlink\CLI\Exception\InvalidRoleConfigException;
|
||||||
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
||||||
@@ -12,7 +13,7 @@ use function sprintf;
|
|||||||
|
|
||||||
class InvalidRoleConfigExceptionTest extends TestCase
|
class InvalidRoleConfigExceptionTest extends TestCase
|
||||||
{
|
{
|
||||||
/** @test */
|
#[Test]
|
||||||
public function forDomainOnlyWithDefaultDomainGeneratesExpectedException(): void
|
public function forDomainOnlyWithDefaultDomainGeneratesExpectedException(): void
|
||||||
{
|
{
|
||||||
$e = InvalidRoleConfigException::forDomainOnlyWithDefaultDomain();
|
$e = InvalidRoleConfigException::forDomainOnlyWithDefaultDomain();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioTest\Shlink\CLI\Factory;
|
namespace ShlinkioTest\Shlink\CLI\Factory;
|
||||||
|
|
||||||
use Laminas\ServiceManager\ServiceManager;
|
use Laminas\ServiceManager\ServiceManager;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
|
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
|
||||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||||
@@ -21,7 +22,7 @@ class ApplicationFactoryTest extends TestCase
|
|||||||
$this->factory = new ApplicationFactory();
|
$this->factory = new ApplicationFactory();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function allCommandsWhichAreServicesAreAdded(): void
|
public function allCommandsWhichAreServicesAreAdded(): void
|
||||||
{
|
{
|
||||||
$sm = $this->createServiceManager([
|
$sm = $this->createServiceManager([
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ namespace ShlinkioTest\Shlink\CLI\GeoLite;
|
|||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
use GeoIp2\Database\Reader;
|
use GeoIp2\Database\Reader;
|
||||||
use MaxMind\Db\Reader\Metadata;
|
use MaxMind\Db\Reader\Metadata;
|
||||||
|
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\Exception\GeolocationDbUpdateFailedException;
|
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||||
@@ -35,7 +37,7 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||||||
$this->lock->method('acquire')->with($this->isTrue())->willReturn(true);
|
$this->lock->method('acquire')->with($this->isTrue())->willReturn(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function exceptionIsThrownWhenOlderDbDoesNotExistAndDownloadFails(): void
|
public function exceptionIsThrownWhenOlderDbDoesNotExistAndDownloadFails(): void
|
||||||
{
|
{
|
||||||
$mustBeUpdated = fn () => self::assertTrue(true);
|
$mustBeUpdated = fn () => self::assertTrue(true);
|
||||||
@@ -58,10 +60,7 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideBigDays')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideBigDays
|
|
||||||
*/
|
|
||||||
public function exceptionIsThrownWhenOlderDbIsTooOldAndDownloadFails(int $days): void
|
public function exceptionIsThrownWhenOlderDbIsTooOldAndDownloadFails(int $days): void
|
||||||
{
|
{
|
||||||
$prev = new DbUpdateException('');
|
$prev = new DbUpdateException('');
|
||||||
@@ -84,7 +83,7 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideBigDays(): iterable
|
public static function provideBigDays(): iterable
|
||||||
{
|
{
|
||||||
yield [36];
|
yield [36];
|
||||||
yield [50];
|
yield [50];
|
||||||
@@ -92,10 +91,7 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||||||
yield [100];
|
yield [100];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideSmallDays')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideSmallDays
|
|
||||||
*/
|
|
||||||
public function databaseIsNotUpdatedIfItIsNewEnough(string|int $buildEpoch): void
|
public function databaseIsNotUpdatedIfItIsNewEnough(string|int $buildEpoch): void
|
||||||
{
|
{
|
||||||
$this->dbUpdater->expects($this->once())->method('databaseFileExists')->willReturn(true);
|
$this->dbUpdater->expects($this->once())->method('databaseFileExists')->willReturn(true);
|
||||||
@@ -109,7 +105,7 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||||||
self::assertEquals(GeolocationResult::DB_IS_UP_TO_DATE, $result);
|
self::assertEquals(GeolocationResult::DB_IS_UP_TO_DATE, $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideSmallDays(): iterable
|
public static function provideSmallDays(): iterable
|
||||||
{
|
{
|
||||||
$generateParamsWithTimestamp = static function (int $days) {
|
$generateParamsWithTimestamp = static function (int $days) {
|
||||||
$timestamp = Chronos::now()->subDays($days)->getTimestamp();
|
$timestamp = Chronos::now()->subDays($days)->getTimestamp();
|
||||||
@@ -119,7 +115,7 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||||||
return map(range(0, 34), $generateParamsWithTimestamp);
|
return map(range(0, 34), $generateParamsWithTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function exceptionIsThrownWhenCheckingExistingDatabaseWithInvalidBuildEpoch(): void
|
public function exceptionIsThrownWhenCheckingExistingDatabaseWithInvalidBuildEpoch(): void
|
||||||
{
|
{
|
||||||
$this->dbUpdater->expects($this->once())->method('databaseFileExists')->willReturn(true);
|
$this->dbUpdater->expects($this->once())->method('databaseFileExists')->willReturn(true);
|
||||||
@@ -151,10 +147,7 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideTrackingOptions')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideTrackingOptions
|
|
||||||
*/
|
|
||||||
public function downloadDbIsSkippedIfTrackingIsDisabled(TrackingOptions $options): void
|
public function downloadDbIsSkippedIfTrackingIsDisabled(TrackingOptions $options): void
|
||||||
{
|
{
|
||||||
$result = $this->geolocationDbUpdater($options)->checkDbUpdate();
|
$result = $this->geolocationDbUpdater($options)->checkDbUpdate();
|
||||||
@@ -164,7 +157,7 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||||||
self::assertEquals(GeolocationResult::CHECK_SKIPPED, $result);
|
self::assertEquals(GeolocationResult::CHECK_SKIPPED, $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideTrackingOptions(): iterable
|
public static function provideTrackingOptions(): iterable
|
||||||
{
|
{
|
||||||
yield 'disableTracking' => [new TrackingOptions(disableTracking: true)];
|
yield 'disableTracking' => [new TrackingOptions(disableTracking: true)];
|
||||||
yield 'disableIpTracking' => [new TrackingOptions(disableIpTracking: true)];
|
yield 'disableIpTracking' => [new TrackingOptions(disableIpTracking: true)];
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Util;
|
namespace ShlinkioTest\Shlink\CLI\Util;
|
||||||
|
|
||||||
|
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\Util\ProcessRunner;
|
use Shlinkio\Shlink\CLI\Util\ProcessRunner;
|
||||||
@@ -34,7 +35,7 @@ class ProcessRunnerTest extends TestCase
|
|||||||
$this->runner = new ProcessRunner($this->helper, fn () => $this->process);
|
$this->runner = new ProcessRunner($this->helper, fn () => $this->process);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function noMessagesAreWrittenWhenOutputIsNotVerbose(): void
|
public function noMessagesAreWrittenWhenOutputIsNotVerbose(): void
|
||||||
{
|
{
|
||||||
$this->output->expects($this->exactly(2))->method('isVeryVerbose')->with()->willReturn(false);
|
$this->output->expects($this->exactly(2))->method('isVeryVerbose')->with()->willReturn(false);
|
||||||
@@ -50,7 +51,7 @@ class ProcessRunnerTest extends TestCase
|
|||||||
$this->runner->run($this->output, []);
|
$this->runner->run($this->output, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function someMessagesAreWrittenWhenOutputIsVerbose(): void
|
public function someMessagesAreWrittenWhenOutputIsVerbose(): void
|
||||||
{
|
{
|
||||||
$this->output->expects($this->exactly(2))->method('isVeryVerbose')->with()->willReturn(true);
|
$this->output->expects($this->exactly(2))->method('isVeryVerbose')->with()->willReturn(true);
|
||||||
@@ -66,7 +67,7 @@ class ProcessRunnerTest extends TestCase
|
|||||||
$this->runner->run($this->output, []);
|
$this->runner->run($this->output, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function wrapsCallbackWhenOutputIsDebug(): void
|
public function wrapsCallbackWhenOutputIsDebug(): void
|
||||||
{
|
{
|
||||||
$this->output->expects($this->exactly(2))->method('isVeryVerbose')->with()->willReturn(false);
|
$this->output->expects($this->exactly(2))->method('isVeryVerbose')->with()->willReturn(false);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Util;
|
namespace ShlinkioTest\Shlink\CLI\Util;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use ReflectionObject;
|
use ReflectionObject;
|
||||||
@@ -23,7 +24,7 @@ class ShlinkTableTest extends TestCase
|
|||||||
$this->shlinkTable = ShlinkTable::fromBaseTable($this->baseTable);
|
$this->shlinkTable = ShlinkTable::fromBaseTable($this->baseTable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function renderMakesTableToBeRenderedWithProvidedInfo(): void
|
public function renderMakesTableToBeRenderedWithProvidedInfo(): void
|
||||||
{
|
{
|
||||||
$headers = [];
|
$headers = [];
|
||||||
@@ -43,7 +44,7 @@ class ShlinkTableTest extends TestCase
|
|||||||
$this->shlinkTable->render($headers, $rows, $footerTitle, $headerTitle);
|
$this->shlinkTable->render($headers, $rows, $footerTitle, $headerTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function newTableIsCreatedForFactoryMethod(): void
|
public function newTableIsCreatedForFactoryMethod(): void
|
||||||
{
|
{
|
||||||
$instance = ShlinkTable::default($this->createMock(OutputInterface::class));
|
$instance = ShlinkTable::default($this->createMock(OutputInterface::class));
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ return [
|
|||||||
Util\DoctrineBatchHelper::class,
|
Util\DoctrineBatchHelper::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
Crawling\CrawlingHelper::class => ['em'],
|
Crawling\CrawlingHelper::class => [ShortUrl\Repository\CrawlableShortCodesQuery::class],
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ use Shlinkio\Shlink\Importer\Model\ImportedShlinkOrphanVisit;
|
|||||||
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
|
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
|
||||||
use Shlinkio\Shlink\Importer\Model\ImportResult;
|
use Shlinkio\Shlink\Importer\Model\ImportResult;
|
||||||
use Shlinkio\Shlink\Importer\Params\ImportParams;
|
use Shlinkio\Shlink\Importer\Params\ImportParams;
|
||||||
use Shlinkio\Shlink\Importer\Sources\ImportSource;
|
|
||||||
use Symfony\Component\Console\Style\OutputStyle;
|
use Symfony\Component\Console\Style\OutputStyle;
|
||||||
use Symfony\Component\Console\Style\StyleInterface;
|
use Symfony\Component\Console\Style\StyleInterface;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
@@ -55,8 +54,7 @@ class ImportedLinksProcessor implements ImportedLinksProcessorInterface
|
|||||||
private function importShortUrls(StyleInterface $io, iterable $shlinkUrls, ImportParams $params): void
|
private function importShortUrls(StyleInterface $io, iterable $shlinkUrls, ImportParams $params): void
|
||||||
{
|
{
|
||||||
$importShortCodes = $params->importShortCodes;
|
$importShortCodes = $params->importShortCodes;
|
||||||
$source = $params->source;
|
$iterable = $this->batchHelper->wrapIterable($shlinkUrls, $params->importVisits ? 10 : 100);
|
||||||
$iterable = $this->batchHelper->wrapIterable($shlinkUrls, $source === ImportSource::SHLINK ? 10 : 100);
|
|
||||||
|
|
||||||
foreach ($iterable as $importedUrl) {
|
foreach ($iterable as $importedUrl) {
|
||||||
$skipOnShortCodeConflict = static fn (): bool => $io->choice(sprintf(
|
$skipOnShortCodeConflict = static fn (): bool => $io->choice(sprintf(
|
||||||
@@ -82,7 +80,10 @@ class ImportedLinksProcessor implements ImportedLinksProcessorInterface
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$resultMessage = $shortUrlImporting->importVisits($importedUrl->visits, $this->em);
|
$resultMessage = $shortUrlImporting->importVisits(
|
||||||
|
$this->batchHelper->wrapIterable($importedUrl->visits, 100),
|
||||||
|
$this->em,
|
||||||
|
);
|
||||||
$io->text(sprintf('%s: %s', $longUrl, $resultMessage));
|
$io->text(sprintf('%s: %s', $longUrl, $resultMessage));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ final class ShortUrlImporting
|
|||||||
*/
|
*/
|
||||||
public function importVisits(iterable $visits, EntityManagerInterface $em): string
|
public function importVisits(iterable $visits, EntityManagerInterface $em): string
|
||||||
{
|
{
|
||||||
$mostRecentImportedDate = $this->shortUrl->mostRecentImportedVisitDate();
|
$mostRecentImportedDate = $this->resolveShortUrl($em)->mostRecentImportedVisitDate();
|
||||||
|
|
||||||
$importedVisits = 0;
|
$importedVisits = 0;
|
||||||
foreach ($visits as $importedVisit) {
|
foreach ($visits as $importedVisit) {
|
||||||
@@ -42,7 +42,7 @@ final class ShortUrlImporting
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$em->persist(Visit::fromImport($this->shortUrl, $importedVisit));
|
$em->persist(Visit::fromImport($this->resolveShortUrl($em), $importedVisit));
|
||||||
$importedVisits++;
|
$importedVisits++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,4 +54,14 @@ final class ShortUrlImporting
|
|||||||
? sprintf('<info>Imported</info> with <info>%s</info> visits', $importedVisits)
|
? sprintf('<info>Imported</info> with <info>%s</info> visits', $importedVisits)
|
||||||
: sprintf('<comment>Skipped</comment>. Imported <info>%s</info> visits', $importedVisits);
|
: sprintf('<comment>Skipped</comment>. Imported <info>%s</info> visits', $importedVisits);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function resolveShortUrl(EntityManagerInterface $em): ShortUrl
|
||||||
|
{
|
||||||
|
// Instead of directly accessing wrapped ShortUrl entity, try to get it from the EM.
|
||||||
|
// With this, we will get the same entity from memory if it is known by the EM, but if it was cleared, the EM
|
||||||
|
// will fetch it again from the database, preventing errors at runtime.
|
||||||
|
// However, if the EM was not flushed yet, the entity will not be found by ID, but it is known by the EM.
|
||||||
|
// In that case, we fall back to wrapped ShortUrl entity directly.
|
||||||
|
return $em->find(ShortUrl::class, $this->shortUrl->getId()) ?? $this->shortUrl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ final class UrlShortenerOptions
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isLooselyMode(): bool
|
public function isLooseMode(): bool
|
||||||
{
|
{
|
||||||
return $this->mode === ShortUrlMode::LOOSELY;
|
return $this->mode === ShortUrlMode::LOOSE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ class ShortUrl extends AbstractEntity
|
|||||||
private string $longUrl;
|
private string $longUrl;
|
||||||
private string $shortCode;
|
private string $shortCode;
|
||||||
private Chronos $dateCreated;
|
private Chronos $dateCreated;
|
||||||
/** @var Collection<int, Visit> */
|
/** @var Collection<int, Visit> & Selectable */
|
||||||
private Collection $visits;
|
private Collection & Selectable $visits;
|
||||||
/** @var Collection<string, DeviceLongUrl> */
|
/** @var Collection<string, DeviceLongUrl> */
|
||||||
private Collection $deviceLongUrls;
|
private Collection $deviceLongUrls;
|
||||||
/** @var Collection<int, Tag> */
|
/** @var Collection<int, Tag> */
|
||||||
@@ -68,7 +68,7 @@ class ShortUrl extends AbstractEntity
|
|||||||
*/
|
*/
|
||||||
public static function createFake(): self
|
public static function createFake(): self
|
||||||
{
|
{
|
||||||
return self::withLongUrl('foo');
|
return self::withLongUrl('https://foo');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -255,23 +255,19 @@ class ShortUrl extends AbstractEntity
|
|||||||
|
|
||||||
public function mostRecentImportedVisitDate(): ?Chronos
|
public function mostRecentImportedVisitDate(): ?Chronos
|
||||||
{
|
{
|
||||||
/** @var Selectable $visits */
|
|
||||||
$visits = $this->visits;
|
|
||||||
$criteria = Criteria::create()->where(Criteria::expr()->eq('type', VisitType::IMPORTED))
|
$criteria = Criteria::create()->where(Criteria::expr()->eq('type', VisitType::IMPORTED))
|
||||||
->orderBy(['id' => 'DESC'])
|
->orderBy(['id' => 'DESC'])
|
||||||
->setMaxResults(1);
|
->setMaxResults(1);
|
||||||
|
$visit = $this->visits->matching($criteria)->last();
|
||||||
|
|
||||||
/** @var Visit|false $visit */
|
return $visit instanceof Visit ? $visit->getDate() : null;
|
||||||
$visit = $visits->matching($criteria)->last();
|
|
||||||
|
|
||||||
return $visit === false ? null : $visit->getDate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Collection<int, Visit> $visits
|
* @param Collection<int, Visit> & Selectable $visits
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
public function setVisits(Collection $visits): self
|
public function setVisits(Collection & Selectable $visits): self
|
||||||
{
|
{
|
||||||
$this->visits = $visits;
|
$this->visits = $visits;
|
||||||
return $this;
|
return $this;
|
||||||
|
|||||||
@@ -5,5 +5,11 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Model;
|
|||||||
enum ShortUrlMode: string
|
enum ShortUrlMode: string
|
||||||
{
|
{
|
||||||
case STRICT = 'strict';
|
case STRICT = 'strict';
|
||||||
case LOOSELY = 'loosely';
|
case LOOSE = 'loose';
|
||||||
|
|
||||||
|
/** @deprecated */
|
||||||
|
public static function tryDeprecated(string $mode): ?self
|
||||||
|
{
|
||||||
|
return $mode === 'loosely' ? self::LOOSE : self::tryFrom($mode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
37
module/Core/src/ShortUrl/Model/UrlShorteningResult.php
Normal file
37
module/Core/src/ShortUrl/Model/UrlShorteningResult.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\ShortUrl\Model;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class UrlShorteningResult
|
||||||
|
{
|
||||||
|
private function __construct(
|
||||||
|
public readonly ShortUrl $shortUrl,
|
||||||
|
private readonly ?Throwable $errorOnEventDispatching,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param callable(Throwable $errorOnEventDispatching): mixed $handler
|
||||||
|
*/
|
||||||
|
public function onEventDispatchingError(callable $handler): void
|
||||||
|
{
|
||||||
|
if ($this->errorOnEventDispatching !== null) {
|
||||||
|
$handler($this->errorOnEventDispatching);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function withoutErrorOnEventDispatching(ShortUrl $shortUrl): self
|
||||||
|
{
|
||||||
|
return new self($shortUrl, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function withErrorOnEventDispatching(ShortUrl $shortUrl, Throwable $errorOnEventDispatching): self
|
||||||
|
{
|
||||||
|
return new self($shortUrl, $errorOnEventDispatching);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ class CustomSlugFilter implements FilterInterface
|
|||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
$value = $this->options->isLooselyMode() ? strtolower($value) : $value;
|
$value = $this->options->isLooseMode() ? strtolower($value) : $value;
|
||||||
return (match ($this->options->multiSegmentSlugsEnabled) {
|
return (match ($this->options->multiSegmentSlugsEnabled) {
|
||||||
true => trim(str_replace(' ', '-', $value), '/'),
|
true => trim(str_replace(' ', '-', $value), '/'),
|
||||||
false => str_replace([' ', '/'], '-', $value),
|
false => str_replace([' ', '/'], '-', $value),
|
||||||
|
|||||||
@@ -12,8 +12,11 @@ use Shlinkio\Shlink\Common\Validation;
|
|||||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
|
||||||
|
use function is_string;
|
||||||
|
use function preg_match;
|
||||||
use function substr;
|
use function substr;
|
||||||
|
|
||||||
|
use const Shlinkio\Shlink\LOOSE_URI_MATCHER;
|
||||||
use const Shlinkio\Shlink\MIN_SHORT_CODES_LENGTH;
|
use const Shlinkio\Shlink\MIN_SHORT_CODES_LENGTH;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,27 +62,13 @@ class ShortUrlInputFilter extends InputFilter
|
|||||||
|
|
||||||
private function initialize(bool $requireLongUrl, UrlShortenerOptions $options): void
|
private function initialize(bool $requireLongUrl, UrlShortenerOptions $options): void
|
||||||
{
|
{
|
||||||
$longUrlNotEmptyCommonOptions = [
|
|
||||||
Validator\NotEmpty::OBJECT,
|
|
||||||
Validator\NotEmpty::SPACE,
|
|
||||||
Validator\NotEmpty::EMPTY_ARRAY,
|
|
||||||
Validator\NotEmpty::BOOLEAN,
|
|
||||||
Validator\NotEmpty::STRING,
|
|
||||||
];
|
|
||||||
|
|
||||||
$longUrlInput = $this->createInput(self::LONG_URL, $requireLongUrl);
|
$longUrlInput = $this->createInput(self::LONG_URL, $requireLongUrl);
|
||||||
$longUrlInput->getValidatorChain()->attach(new Validator\NotEmpty([
|
$longUrlInput->getValidatorChain()->merge($this->longUrlValidators());
|
||||||
...$longUrlNotEmptyCommonOptions,
|
|
||||||
Validator\NotEmpty::NULL,
|
|
||||||
]));
|
|
||||||
$this->add($longUrlInput);
|
$this->add($longUrlInput);
|
||||||
|
|
||||||
$deviceLongUrlsInput = $this->createInput(self::DEVICE_LONG_URLS, false);
|
$deviceLongUrlsInput = $this->createInput(self::DEVICE_LONG_URLS, false);
|
||||||
$deviceLongUrlsInput->getValidatorChain()->attach(
|
$deviceLongUrlsInput->getValidatorChain()->attach(
|
||||||
new DeviceLongUrlsValidator(new Validator\NotEmpty([
|
new DeviceLongUrlsValidator($this->longUrlValidators(allowNull: ! $requireLongUrl)),
|
||||||
...$longUrlNotEmptyCommonOptions,
|
|
||||||
...($requireLongUrl ? [Validator\NotEmpty::NULL] : []),
|
|
||||||
])),
|
|
||||||
);
|
);
|
||||||
$this->add($deviceLongUrlsInput);
|
$this->add($deviceLongUrlsInput);
|
||||||
|
|
||||||
@@ -129,4 +118,25 @@ class ShortUrlInputFilter extends InputFilter
|
|||||||
|
|
||||||
$this->add($this->createBooleanInput(self::CRAWLABLE, false));
|
$this->add($this->createBooleanInput(self::CRAWLABLE, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function longUrlValidators(bool $allowNull = false): Validator\ValidatorChain
|
||||||
|
{
|
||||||
|
$emptyModifiers = [
|
||||||
|
Validator\NotEmpty::OBJECT,
|
||||||
|
Validator\NotEmpty::SPACE,
|
||||||
|
Validator\NotEmpty::EMPTY_ARRAY,
|
||||||
|
Validator\NotEmpty::BOOLEAN,
|
||||||
|
Validator\NotEmpty::STRING,
|
||||||
|
];
|
||||||
|
if (! $allowNull) {
|
||||||
|
$emptyModifiers[] = Validator\NotEmpty::NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (new Validator\ValidatorChain())
|
||||||
|
->attach(new Validator\NotEmpty($emptyModifiers))
|
||||||
|
->attach(new Validator\Callback(
|
||||||
|
// Non-strings is always allowed. Other validators will take care of those
|
||||||
|
static fn (mixed $value) => ! is_string($value) || preg_match(LOOSE_URI_MATCHER, $value) === 1,
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ class CrawlableShortCodesQuery extends EntitySpecificationRepository implements
|
|||||||
->from(ShortUrl::class, 's')
|
->from(ShortUrl::class, 's')
|
||||||
->where($qb->expr()->eq('s.crawlable', ':crawlable'))
|
->where($qb->expr()->eq('s.crawlable', ':crawlable'))
|
||||||
->setParameter('crawlable', true)
|
->setParameter('crawlable', true)
|
||||||
->setMaxResults($blockSize);
|
->setMaxResults($blockSize)
|
||||||
|
->orderBy('s.shortCode');
|
||||||
|
|
||||||
$page = 0;
|
$page = 0;
|
||||||
do {
|
do {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink\Core\ShortUrl;
|
namespace Shlinkio\Shlink\Core\ShortUrl;
|
||||||
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Container\ContainerExceptionInterface;
|
||||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Shlinkio\Shlink\Core\EventDispatcher\Event\ShortUrlCreated;
|
use Shlinkio\Shlink\Core\EventDispatcher\Event\ShortUrlCreated;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||||
@@ -13,6 +14,7 @@ use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
|||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortCodeUniquenessHelperInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortCodeUniquenessHelperInterface;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlTitleResolutionHelperInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlTitleResolutionHelperInterface;
|
||||||
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\Repository\ShortUrlRepositoryInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlRepositoryInterface;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Resolver\ShortUrlRelationResolverInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\Resolver\ShortUrlRelationResolverInterface;
|
||||||
|
|
||||||
@@ -31,12 +33,12 @@ class UrlShortener implements UrlShortenerInterface
|
|||||||
* @throws NonUniqueSlugException
|
* @throws NonUniqueSlugException
|
||||||
* @throws InvalidUrlException
|
* @throws InvalidUrlException
|
||||||
*/
|
*/
|
||||||
public function shorten(ShortUrlCreation $creation): ShortUrl
|
public function shorten(ShortUrlCreation $creation): UrlShorteningResult
|
||||||
{
|
{
|
||||||
// First, check if a short URL exists for all provided params
|
// First, check if a short URL exists for all provided params
|
||||||
$existingShortUrl = $this->findExistingShortUrlIfExists($creation);
|
$existingShortUrl = $this->findExistingShortUrlIfExists($creation);
|
||||||
if ($existingShortUrl !== null) {
|
if ($existingShortUrl !== null) {
|
||||||
return $existingShortUrl;
|
return UrlShorteningResult::withoutErrorOnEventDispatching($existingShortUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
$creation = $this->titleResolutionHelper->processTitleAndValidateUrl($creation);
|
$creation = $this->titleResolutionHelper->processTitleAndValidateUrl($creation);
|
||||||
@@ -51,9 +53,17 @@ class UrlShortener implements UrlShortenerInterface
|
|||||||
return $shortUrl;
|
return $shortUrl;
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->eventDispatcher->dispatch(new ShortUrlCreated($newShortUrl->getId()));
|
try {
|
||||||
|
$this->eventDispatcher->dispatch(new ShortUrlCreated($newShortUrl->getId()));
|
||||||
|
} catch (ContainerExceptionInterface $e) {
|
||||||
|
// Ignore container errors when dispatching the event.
|
||||||
|
// When using openswoole, this event will try to enqueue a task, which cannot be done outside an HTTP
|
||||||
|
// request.
|
||||||
|
// If the short URL is created from CLI, the event dispatching will fail.
|
||||||
|
return UrlShorteningResult::withErrorOnEventDispatching($newShortUrl, $e);
|
||||||
|
}
|
||||||
|
|
||||||
return $newShortUrl;
|
return UrlShorteningResult::withoutErrorOnEventDispatching($newShortUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function findExistingShortUrlIfExists(ShortUrlCreation $creation): ?ShortUrl
|
private function findExistingShortUrlIfExists(ShortUrlCreation $creation): ?ShortUrl
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ namespace Shlinkio\Shlink\Core\ShortUrl;
|
|||||||
|
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Model\UrlShorteningResult;
|
||||||
|
|
||||||
interface UrlShortenerInterface
|
interface UrlShortenerInterface
|
||||||
{
|
{
|
||||||
@@ -15,5 +15,5 @@ interface UrlShortenerInterface
|
|||||||
* @throws NonUniqueSlugException
|
* @throws NonUniqueSlugException
|
||||||
* @throws InvalidUrlException
|
* @throws InvalidUrlException
|
||||||
*/
|
*/
|
||||||
public function shorten(ShortUrlCreation $creation): ShortUrl;
|
public function shorten(ShortUrlCreation $creation): UrlShorteningResult;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\Core\Tag\Model;
|
namespace Shlinkio\Shlink\Core\Tag\Model;
|
||||||
|
|
||||||
use function Shlinkio\Shlink\Core\camelCaseToSnakeCase;
|
|
||||||
|
|
||||||
enum OrderableField: string
|
enum OrderableField: string
|
||||||
{
|
{
|
||||||
case TAG = 'tag';
|
case TAG = 'tag';
|
||||||
@@ -15,20 +13,12 @@ enum OrderableField: string
|
|||||||
/** @deprecated Use VISITS instead */
|
/** @deprecated Use VISITS instead */
|
||||||
case VISITS_COUNT = 'visitsCount';
|
case VISITS_COUNT = 'visitsCount';
|
||||||
|
|
||||||
public static function isAggregateField(string $field): bool
|
public static function toSnakeCaseValidField(?string $field): self
|
||||||
{
|
{
|
||||||
$parsed = self::tryFrom($field);
|
$parsed = $field !== null ? self::tryFrom($field) : self::TAG;
|
||||||
return $parsed !== null && $parsed !== self::TAG;
|
return match ($parsed) {
|
||||||
}
|
|
||||||
|
|
||||||
public static function toSnakeCaseValidField(?string $field): string
|
|
||||||
{
|
|
||||||
$parsed = $field !== null ? self::tryFrom($field) : self::VISITS;
|
|
||||||
$normalized = match ($parsed) {
|
|
||||||
self::VISITS_COUNT, null => self::VISITS,
|
self::VISITS_COUNT, null => self::VISITS,
|
||||||
default => $parsed,
|
default => $parsed,
|
||||||
};
|
};
|
||||||
|
|
||||||
return camelCaseToSnakeCase($normalized->value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\Core\Tag\Repository;
|
namespace Shlinkio\Shlink\Core\Tag\Repository;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Query\QueryBuilder as NativeQueryBuilder;
|
||||||
use Doctrine\ORM\Query\ResultSetMappingBuilder;
|
use Doctrine\ORM\Query\ResultSetMappingBuilder;
|
||||||
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
|
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
|
||||||
use Happyr\DoctrineSpecification\Spec;
|
use Happyr\DoctrineSpecification\Spec;
|
||||||
@@ -14,10 +15,11 @@ use Shlinkio\Shlink\Core\Tag\Model\TagsListFiltering;
|
|||||||
use Shlinkio\Shlink\Core\Tag\Spec\CountTagsWithName;
|
use Shlinkio\Shlink\Core\Tag\Spec\CountTagsWithName;
|
||||||
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
||||||
use Shlinkio\Shlink\Rest\ApiKey\Spec\WithApiKeySpecsEnsuringJoin;
|
use Shlinkio\Shlink\Rest\ApiKey\Spec\WithApiKeySpecsEnsuringJoin;
|
||||||
use Shlinkio\Shlink\Rest\ApiKey\Spec\WithInlinedApiKeySpecsEnsuringJoin;
|
|
||||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
|
||||||
|
use function Functional\each;
|
||||||
use function Functional\map;
|
use function Functional\map;
|
||||||
|
use function Shlinkio\Shlink\Core\camelCaseToSnakeCase;
|
||||||
|
|
||||||
use const PHP_INT_MAX;
|
use const PHP_INT_MAX;
|
||||||
|
|
||||||
@@ -41,78 +43,90 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
|
|||||||
*/
|
*/
|
||||||
public function findTagsWithInfo(?TagsListFiltering $filtering = null): array
|
public function findTagsWithInfo(?TagsListFiltering $filtering = null): array
|
||||||
{
|
{
|
||||||
$orderField = $filtering?->orderBy?->field;
|
$orderField = OrderableField::toSnakeCaseValidField($filtering?->orderBy?->field);
|
||||||
$orderDir = $filtering?->orderBy?->direction;
|
$orderDir = $filtering?->orderBy?->direction ?? 'ASC';
|
||||||
$orderMainQuery = $orderField !== null && OrderableField::isAggregateField($orderField);
|
$apiKey = $filtering?->apiKey;
|
||||||
|
|
||||||
$conn = $this->getEntityManager()->getConnection();
|
$conn = $this->getEntityManager()->getConnection();
|
||||||
$subQb = $this->createQueryBuilder('t');
|
|
||||||
$subQb->select('t.id', 't.name');
|
|
||||||
|
|
||||||
if (! $orderMainQuery) {
|
$applyApiKeyToNativeQb = static fn (NativeQueryBuilder $qb) =>
|
||||||
$subQb->orderBy('t.name', $orderDir ?? 'ASC')
|
$apiKey?->mapRoles(static fn (Role $role, array $meta) => match ($role) {
|
||||||
->setMaxResults($filtering?->limit ?? PHP_INT_MAX)
|
Role::DOMAIN_SPECIFIC => $qb->andWhere(
|
||||||
->setFirstResult($filtering?->offset ?? 0);
|
$qb->expr()->eq('s.domain_id', $conn->quote(Role::domainIdFromMeta($meta))),
|
||||||
}
|
),
|
||||||
|
Role::AUTHORED_SHORT_URLS => $qb->andWhere(
|
||||||
|
$qb->expr()->eq('s.author_api_key_id', $conn->quote($apiKey->getId())),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
// For admins and when no API key is present, we'll return tags which are not linked to any short URL
|
||||||
|
$joiningMethod = ApiKey::isAdmin($apiKey) ? 'leftJoin' : 'join';
|
||||||
|
$tagsSubQb = $conn->createQueryBuilder();
|
||||||
|
$tagsSubQb
|
||||||
|
->select('t.id AS tag_id', 't.name AS tag', 'COUNT(DISTINCT s.id) AS short_urls_count')
|
||||||
|
->from('tags', 't')
|
||||||
|
->groupBy('t.id', 't.name')
|
||||||
|
->{$joiningMethod}('t', 'short_urls_in_tags', 'st', $tagsSubQb->expr()->eq('st.tag_id', 't.id'))
|
||||||
|
->{$joiningMethod}('st', 'short_urls', 's', $tagsSubQb->expr()->eq('st.short_url_id', 's.id'));
|
||||||
|
|
||||||
$searchTerm = $filtering?->searchTerm;
|
$searchTerm = $filtering?->searchTerm;
|
||||||
if ($searchTerm !== null) {
|
if ($searchTerm !== null) {
|
||||||
$subQb->andWhere($subQb->expr()->like('t.name', $conn->quote('%' . $searchTerm . '%')));
|
$tagsSubQb->andWhere($tagsSubQb->expr()->like('t.name', $conn->quote('%' . $searchTerm . '%')));
|
||||||
}
|
}
|
||||||
|
|
||||||
$apiKey = $filtering?->apiKey;
|
$buildVisitsSubQb = static function (bool $excludeBots, string $aggregateAlias) use ($conn) {
|
||||||
$this->applySpecification($subQb, new WithInlinedApiKeySpecsEnsuringJoin($apiKey), 't');
|
$visitsSubQb = $conn->createQueryBuilder();
|
||||||
|
$commonJoinCondition = $visitsSubQb->expr()->eq('v.short_url_id', 's.id');
|
||||||
|
$visitsJoin = ! $excludeBots
|
||||||
|
? $commonJoinCondition
|
||||||
|
: $visitsSubQb->expr()->and(
|
||||||
|
$commonJoinCondition,
|
||||||
|
$visitsSubQb->expr()->eq('v.potential_bot', $conn->quote('0')),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $visitsSubQb
|
||||||
|
->select('st.tag_id AS tag_id', 'COUNT(DISTINCT v.id) AS ' . $aggregateAlias)
|
||||||
|
->from('visits', 'v')
|
||||||
|
->join('v', 'short_urls', 's', $visitsJoin) // @phpstan-ignore-line
|
||||||
|
->join('s', 'short_urls_in_tags', 'st', $visitsSubQb->expr()->eq('st.short_url_id', 's.id'))
|
||||||
|
->groupBy('st.tag_id');
|
||||||
|
};
|
||||||
|
$allVisitsSubQb = $buildVisitsSubQb(false, 'visits');
|
||||||
|
$nonBotVisitsSubQb = $buildVisitsSubQb(true, 'non_bot_visits');
|
||||||
|
|
||||||
|
// Apply API key specification to all sub-queries
|
||||||
|
each([$tagsSubQb, $allVisitsSubQb, $nonBotVisitsSubQb], $applyApiKeyToNativeQb);
|
||||||
|
|
||||||
// A native query builder needs to be used here, because DQL and ORM query builders do not support
|
// A native query builder needs to be used here, because DQL and ORM query builders do not support
|
||||||
// sub-queries at "from" and "join" level.
|
// sub-queries at "from" and "join" level.
|
||||||
// If no sub-query is used, the whole list is loaded even with pagination, making it very inefficient.
|
// If no sub-query is used, the whole list is loaded even with pagination, making it very inefficient.
|
||||||
$nativeQb = $conn->createQueryBuilder();
|
$mainQb = $conn->createQueryBuilder();
|
||||||
$nativeQb
|
$mainQb
|
||||||
->select(
|
->select(
|
||||||
't.id_0 AS id',
|
't.tag AS tag',
|
||||||
't.name_1 AS name',
|
'COALESCE(v.visits, 0) AS visits', // COALESCE required for postgres to properly order
|
||||||
'COUNT(DISTINCT s.id) AS short_urls_count',
|
'COALESCE(b.non_bot_visits, 0) AS non_bot_visits',
|
||||||
'COUNT(DISTINCT v.id) AS visits', // Native queries require snake_case for cross-db compatibility
|
'COALESCE(t.short_urls_count, 0) AS short_urls_count',
|
||||||
'COUNT(DISTINCT v2.id) AS non_bot_visits',
|
|
||||||
)
|
)
|
||||||
->from('(' . $subQb->getQuery()->getSQL() . ')', 't') // @phpstan-ignore-line
|
->from('(' . $tagsSubQb->getSQL() . ')', 't')
|
||||||
->leftJoin('t', 'short_urls_in_tags', 'st', $nativeQb->expr()->eq('t.id_0', 'st.tag_id'))
|
->leftJoin('t', '(' . $allVisitsSubQb->getSQL() . ')', 'v', $mainQb->expr()->eq('t.tag_id', 'v.tag_id'))
|
||||||
->leftJoin('st', 'short_urls', 's', $nativeQb->expr()->eq('s.id', 'st.short_url_id'))
|
->leftJoin('t', '(' . $nonBotVisitsSubQb->getSQL() . ')', 'b', $mainQb->expr()->eq('t.tag_id', 'b.tag_id'))
|
||||||
->leftJoin('st', 'visits', 'v', $nativeQb->expr()->eq('st.short_url_id', 'v.short_url_id'))
|
->setMaxResults($filtering?->limit ?? PHP_INT_MAX)
|
||||||
->leftJoin('st', 'visits', 'v2', $nativeQb->expr()->and( // @phpstan-ignore-line
|
->setFirstResult($filtering?->offset ?? 0);
|
||||||
$nativeQb->expr()->eq('st.short_url_id', 'v2.short_url_id'),
|
|
||||||
$nativeQb->expr()->eq('v2.potential_bot', $conn->quote('0')),
|
|
||||||
))
|
|
||||||
->groupBy('t.id_0', 't.name_1');
|
|
||||||
|
|
||||||
// Apply API key role conditions to the native query too, as they will affect the amounts on the aggregates
|
$mainQb->orderBy(camelCaseToSnakeCase($orderField->value), $orderDir);
|
||||||
$apiKey?->mapRoles(static fn (Role $role, array $meta) => match ($role) {
|
if ($orderField !== OrderableField::TAG) {
|
||||||
Role::DOMAIN_SPECIFIC => $nativeQb->andWhere(
|
// Add ordering by tag name, as a fallback in case of same amounts
|
||||||
$nativeQb->expr()->eq('s.domain_id', $conn->quote(Role::domainIdFromMeta($meta))),
|
$mainQb->addOrderBy('tag', 'ASC');
|
||||||
),
|
|
||||||
Role::AUTHORED_SHORT_URLS => $nativeQb->andWhere(
|
|
||||||
$nativeQb->expr()->eq('s.author_api_key_id', $conn->quote($apiKey->getId())),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
if ($orderMainQuery) {
|
|
||||||
$nativeQb
|
|
||||||
->orderBy(OrderableField::toSnakeCaseValidField($orderField), $orderDir ?? 'ASC')
|
|
||||||
->setMaxResults($filtering?->limit ?? PHP_INT_MAX)
|
|
||||||
->setFirstResult($filtering?->offset ?? 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add ordering by tag name, as a fallback in case of same amount, or as default ordering
|
|
||||||
$nativeQb->addOrderBy('t.name_1', $orderMainQuery || $orderDir === null ? 'ASC' : $orderDir);
|
|
||||||
|
|
||||||
$rsm = new ResultSetMappingBuilder($this->getEntityManager());
|
$rsm = new ResultSetMappingBuilder($this->getEntityManager());
|
||||||
$rsm->addScalarResult('name', 'tag');
|
$rsm->addScalarResult('tag', 'tag');
|
||||||
$rsm->addScalarResult('short_urls_count', 'shortUrlsCount');
|
|
||||||
$rsm->addScalarResult('visits', 'visits');
|
$rsm->addScalarResult('visits', 'visits');
|
||||||
$rsm->addScalarResult('non_bot_visits', 'nonBotVisits');
|
$rsm->addScalarResult('non_bot_visits', 'nonBotVisits');
|
||||||
|
$rsm->addScalarResult('short_urls_count', 'shortUrlsCount');
|
||||||
|
|
||||||
return map(
|
return map(
|
||||||
$this->getEntityManager()->createNativeQuery($nativeQb->getSQL(), $rsm)->getResult(),
|
$this->getEntityManager()->createNativeQuery($mainQb->getSQL(), $rsm)->getResult(),
|
||||||
TagInfo::fromRawData(...),
|
TagInfo::fromRawData(...),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class TagService implements TagServiceInterface
|
|||||||
*/
|
*/
|
||||||
public function deleteTags(array $tagNames, ?ApiKey $apiKey = null): void
|
public function deleteTags(array $tagNames, ?ApiKey $apiKey = null): void
|
||||||
{
|
{
|
||||||
if ($apiKey !== null && ! $apiKey->isAdmin()) {
|
if (! ApiKey::isAdmin($apiKey)) {
|
||||||
throw ForbiddenTagOperationException::forDeletion();
|
throw ForbiddenTagOperationException::forDeletion();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ class TagService implements TagServiceInterface
|
|||||||
*/
|
*/
|
||||||
public function renameTag(TagRenaming $renaming, ?ApiKey $apiKey = null): Tag
|
public function renameTag(TagRenaming $renaming, ?ApiKey $apiKey = null): Tag
|
||||||
{
|
{
|
||||||
if ($apiKey !== null && ! $apiKey->isAdmin()) {
|
if (! ApiKey::isAdmin($apiKey)) {
|
||||||
throw ForbiddenTagOperationException::forRenaming();
|
throw ForbiddenTagOperationException::forRenaming();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,14 @@ class DoctrineBatchHelper implements DoctrineBatchHelperInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param iterable<T> $resultSet
|
||||||
|
* @return iterable<T>
|
||||||
* @throws Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
public function wrapIterable(iterable $resultSet, int $batchSize): iterable
|
public function wrapIterable(iterable $resultSet, int $batchSize): iterable
|
||||||
{
|
{
|
||||||
$iteration = 0;
|
$iteration = 0;
|
||||||
|
|
||||||
$this->em->beginTransaction();
|
$this->em->beginTransaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -33,7 +35,6 @@ class DoctrineBatchHelper implements DoctrineBatchHelperInterface
|
|||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$this->em->rollback();
|
$this->em->rollback();
|
||||||
|
|
||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioApiTest\Shlink\Core\Action;
|
namespace ShlinkioApiTest\Shlink\Core\Action;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
|
use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
|
||||||
|
|
||||||
use const ShlinkioTest\Shlink\ANDROID_USER_AGENT;
|
use const ShlinkioTest\Shlink\ANDROID_USER_AGENT;
|
||||||
@@ -12,17 +14,14 @@ use const ShlinkioTest\Shlink\IOS_USER_AGENT;
|
|||||||
|
|
||||||
class RedirectTest extends ApiTestCase
|
class RedirectTest extends ApiTestCase
|
||||||
{
|
{
|
||||||
/**
|
#[Test, DataProvider('provideUserAgents')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideUserAgents
|
|
||||||
*/
|
|
||||||
public function properRedirectHappensBasedOnUserAgent(?string $userAgent, string $expectedRedirect): void
|
public function properRedirectHappensBasedOnUserAgent(?string $userAgent, string $expectedRedirect): void
|
||||||
{
|
{
|
||||||
$response = $this->callShortUrl('def456', $userAgent);
|
$response = $this->callShortUrl('def456', $userAgent);
|
||||||
self::assertEquals($expectedRedirect, $response->getHeaderLine('Location'));
|
self::assertEquals($expectedRedirect, $response->getHeaderLine('Location'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideUserAgents(): iterable
|
public static function provideUserAgents(): iterable
|
||||||
{
|
{
|
||||||
yield 'android' => [ANDROID_USER_AGENT, 'https://blog.alejandrocelaya.com/android'];
|
yield 'android' => [ANDROID_USER_AGENT, 'https://blog.alejandrocelaya.com/android'];
|
||||||
yield 'ios' => [IOS_USER_AGENT, 'https://blog.alejandrocelaya.com/ios'];
|
yield 'ios' => [IOS_USER_AGENT, 'https://blog.alejandrocelaya.com/ios'];
|
||||||
|
|||||||
32
module/Core/test-api/Action/RobotsTest.php
Normal file
32
module/Core/test-api/Action/RobotsTest.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioApiTest\Shlink\Core\Action;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
|
||||||
|
|
||||||
|
class RobotsTest extends ApiTestCase
|
||||||
|
{
|
||||||
|
#[Test]
|
||||||
|
public function expectedListOfCrawlableShortCodesIsReturned(): void
|
||||||
|
{
|
||||||
|
$resp = $this->callShortUrl('robots.txt');
|
||||||
|
$body = $resp->getBody()->__toString();
|
||||||
|
|
||||||
|
self::assertEquals(200, $resp->getStatusCode());
|
||||||
|
self::assertEquals(
|
||||||
|
<<<ROBOTS
|
||||||
|
# For more information about the robots.txt standard, see:
|
||||||
|
# https://www.robotstxt.org/orig.html
|
||||||
|
|
||||||
|
User-agent: *
|
||||||
|
Allow: /abc123
|
||||||
|
Allow: /custom
|
||||||
|
Disallow: /
|
||||||
|
ROBOTS,
|
||||||
|
$body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ namespace ShlinkioDbTest\Shlink\Core\Domain\Repository;
|
|||||||
|
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Shlinkio\Shlink\Core\Config\NotFoundRedirects;
|
use Shlinkio\Shlink\Core\Config\NotFoundRedirects;
|
||||||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||||
use Shlinkio\Shlink\Core\Domain\Repository\DomainRepository;
|
use Shlinkio\Shlink\Core\Domain\Repository\DomainRepository;
|
||||||
@@ -26,7 +27,7 @@ class DomainRepositoryTest extends DatabaseTestCase
|
|||||||
$this->repo = $this->getEntityManager()->getRepository(Domain::class);
|
$this->repo = $this->getEntityManager()->getRepository(Domain::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function expectedDomainsAreFoundWhenNoApiKeyIsInvolved(): void
|
public function expectedDomainsAreFoundWhenNoApiKeyIsInvolved(): void
|
||||||
{
|
{
|
||||||
$fooDomain = Domain::withAuthority('foo.com');
|
$fooDomain = Domain::withAuthority('foo.com');
|
||||||
@@ -61,7 +62,7 @@ class DomainRepositoryTest extends DatabaseTestCase
|
|||||||
self::assertTrue($this->repo->domainExists('detached.com'));
|
self::assertTrue($this->repo->domainExists('detached.com'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function expectedDomainsAreFoundWhenApiKeyIsProvided(): void
|
public function expectedDomainsAreFoundWhenApiKeyIsProvided(): void
|
||||||
{
|
{
|
||||||
$authorApiKey = ApiKey::fromMeta(ApiKeyMeta::withRoles(RoleDefinition::forAuthoredShortUrls()));
|
$authorApiKey = ApiKey::fromMeta(ApiKeyMeta::withRoles(RoleDefinition::forAuthoredShortUrls()));
|
||||||
@@ -131,7 +132,7 @@ class DomainRepositoryTest extends DatabaseTestCase
|
|||||||
{
|
{
|
||||||
return ShortUrl::create(
|
return ShortUrl::create(
|
||||||
ShortUrlCreation::fromRawData(
|
ShortUrlCreation::fromRawData(
|
||||||
['domain' => $domain->authority, 'apiKey' => $apiKey, 'longUrl' => 'foo'],
|
['domain' => $domain->authority, 'apiKey' => $apiKey, 'longUrl' => 'https://foo'],
|
||||||
),
|
),
|
||||||
new class ($domain) implements ShortUrlRelationResolverInterface {
|
new class ($domain) implements ShortUrlRelationResolverInterface {
|
||||||
public function __construct(private Domain $domain)
|
public function __construct(private Domain $domain)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioDbTest\Shlink\Core\ShortUrl\Repository;
|
namespace ShlinkioDbTest\Shlink\Core\ShortUrl\Repository;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Repository\CrawlableShortCodesQuery;
|
use Shlinkio\Shlink\Core\ShortUrl\Repository\CrawlableShortCodesQuery;
|
||||||
@@ -19,11 +20,11 @@ class CrawlableShortCodesQueryTest extends DatabaseTestCase
|
|||||||
$this->query = new CrawlableShortCodesQuery($em, $em->getClassMetadata(ShortUrl::class));
|
$this->query = new CrawlableShortCodesQuery($em, $em->getClassMetadata(ShortUrl::class));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function invokingQueryReturnsExpectedResult(): void
|
public function invokingQueryReturnsExpectedResult(): void
|
||||||
{
|
{
|
||||||
$createShortUrl = fn (bool $crawlable) => ShortUrl::create(
|
$createShortUrl = fn (bool $crawlable) => ShortUrl::create(
|
||||||
ShortUrlCreation::fromRawData(['crawlable' => $crawlable, 'longUrl' => 'foo.com']),
|
ShortUrlCreation::fromRawData(['crawlable' => $crawlable, 'longUrl' => 'https://foo.com']),
|
||||||
);
|
);
|
||||||
|
|
||||||
$shortUrl1 = $createShortUrl(true);
|
$shortUrl1 = $createShortUrl(true);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace ShlinkioDbTest\Shlink\Core\ShortUrl\Repository;
|
|||||||
|
|
||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use ReflectionObject;
|
use ReflectionObject;
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
use Shlinkio\Shlink\Core\Model\Ordering;
|
use Shlinkio\Shlink\Core\Model\Ordering;
|
||||||
@@ -37,28 +38,28 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
|
|||||||
$this->relationResolver = new PersistenceShortUrlRelationResolver($em);
|
$this->relationResolver = new PersistenceShortUrlRelationResolver($em);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function countListReturnsProperNumberOfResults(): void
|
public function countListReturnsProperNumberOfResults(): void
|
||||||
{
|
{
|
||||||
$count = 5;
|
$count = 5;
|
||||||
for ($i = 0; $i < $count; $i++) {
|
for ($i = 0; $i < $count; $i++) {
|
||||||
$this->getEntityManager()->persist(ShortUrl::withLongUrl((string) $i));
|
$this->getEntityManager()->persist(ShortUrl::withLongUrl('https://' . $i));
|
||||||
}
|
}
|
||||||
$this->getEntityManager()->flush();
|
$this->getEntityManager()->flush();
|
||||||
|
|
||||||
self::assertEquals($count, $this->repo->countList(new ShortUrlsCountFiltering()));
|
self::assertEquals($count, $this->repo->countList(new ShortUrlsCountFiltering()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function findListProperlyFiltersResult(): void
|
public function findListProperlyFiltersResult(): void
|
||||||
{
|
{
|
||||||
$foo = ShortUrl::create(
|
$foo = ShortUrl::create(
|
||||||
ShortUrlCreation::fromRawData(['longUrl' => 'foo', 'tags' => ['bar']]),
|
ShortUrlCreation::fromRawData(['longUrl' => 'https://foo', 'tags' => ['bar']]),
|
||||||
$this->relationResolver,
|
$this->relationResolver,
|
||||||
);
|
);
|
||||||
$this->getEntityManager()->persist($foo);
|
$this->getEntityManager()->persist($foo);
|
||||||
|
|
||||||
$bar = ShortUrl::withLongUrl('bar');
|
$bar = ShortUrl::withLongUrl('https://bar');
|
||||||
$visits = map(range(0, 5), function () use ($bar) {
|
$visits = map(range(0, 5), function () use ($bar) {
|
||||||
$visit = Visit::forValidShortUrl($bar, Visitor::botInstance());
|
$visit = Visit::forValidShortUrl($bar, Visitor::botInstance());
|
||||||
$this->getEntityManager()->persist($visit);
|
$this->getEntityManager()->persist($visit);
|
||||||
@@ -68,7 +69,7 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
|
|||||||
$bar->setVisits(new ArrayCollection($visits));
|
$bar->setVisits(new ArrayCollection($visits));
|
||||||
$this->getEntityManager()->persist($bar);
|
$this->getEntityManager()->persist($bar);
|
||||||
|
|
||||||
$foo2 = ShortUrl::withLongUrl('foo_2');
|
$foo2 = ShortUrl::withLongUrl('https://foo_2');
|
||||||
$visits2 = map(range(0, 3), function () use ($foo2) {
|
$visits2 = map(range(0, 3), function () use ($foo2) {
|
||||||
$visit = Visit::forValidShortUrl($foo2, Visitor::emptyInstance());
|
$visit = Visit::forValidShortUrl($foo2, Visitor::emptyInstance());
|
||||||
$this->getEntityManager()->persist($visit);
|
$this->getEntityManager()->persist($visit);
|
||||||
@@ -143,10 +144,10 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function findListProperlyMapsFieldNamesToColumnNamesWhenOrdering(): void
|
public function findListProperlyMapsFieldNamesToColumnNamesWhenOrdering(): void
|
||||||
{
|
{
|
||||||
$urls = ['a', 'z', 'c', 'b'];
|
$urls = ['https://a', 'https://z', 'https://c', 'https://b'];
|
||||||
foreach ($urls as $url) {
|
foreach ($urls as $url) {
|
||||||
$this->getEntityManager()->persist(ShortUrl::withLongUrl($url));
|
$this->getEntityManager()->persist(ShortUrl::withLongUrl($url));
|
||||||
}
|
}
|
||||||
@@ -158,37 +159,37 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
|
|||||||
);
|
);
|
||||||
|
|
||||||
self::assertCount(count($urls), $result);
|
self::assertCount(count($urls), $result);
|
||||||
self::assertEquals('a', $result[0]->getLongUrl());
|
self::assertEquals('https://a', $result[0]->getLongUrl());
|
||||||
self::assertEquals('b', $result[1]->getLongUrl());
|
self::assertEquals('https://b', $result[1]->getLongUrl());
|
||||||
self::assertEquals('c', $result[2]->getLongUrl());
|
self::assertEquals('https://c', $result[2]->getLongUrl());
|
||||||
self::assertEquals('z', $result[3]->getLongUrl());
|
self::assertEquals('https://z', $result[3]->getLongUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function findListReturnsOnlyThoseWithMatchingTags(): void
|
public function findListReturnsOnlyThoseWithMatchingTags(): void
|
||||||
{
|
{
|
||||||
$shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
$shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
'longUrl' => 'foo1',
|
'longUrl' => 'https://foo1',
|
||||||
'tags' => ['foo', 'bar'],
|
'tags' => ['foo', 'bar'],
|
||||||
]), $this->relationResolver);
|
]), $this->relationResolver);
|
||||||
$this->getEntityManager()->persist($shortUrl1);
|
$this->getEntityManager()->persist($shortUrl1);
|
||||||
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
'longUrl' => 'foo2',
|
'longUrl' => 'https://foo2',
|
||||||
'tags' => ['foo', 'baz'],
|
'tags' => ['foo', 'baz'],
|
||||||
]), $this->relationResolver);
|
]), $this->relationResolver);
|
||||||
$this->getEntityManager()->persist($shortUrl2);
|
$this->getEntityManager()->persist($shortUrl2);
|
||||||
$shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
$shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
'longUrl' => 'foo3',
|
'longUrl' => 'https://foo3',
|
||||||
'tags' => ['foo'],
|
'tags' => ['foo'],
|
||||||
]), $this->relationResolver);
|
]), $this->relationResolver);
|
||||||
$this->getEntityManager()->persist($shortUrl3);
|
$this->getEntityManager()->persist($shortUrl3);
|
||||||
$shortUrl4 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
$shortUrl4 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
'longUrl' => 'foo4',
|
'longUrl' => 'https://foo4',
|
||||||
'tags' => ['bar', 'baz'],
|
'tags' => ['bar', 'baz'],
|
||||||
]), $this->relationResolver);
|
]), $this->relationResolver);
|
||||||
$this->getEntityManager()->persist($shortUrl4);
|
$this->getEntityManager()->persist($shortUrl4);
|
||||||
$shortUrl5 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
$shortUrl5 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
'longUrl' => 'foo5',
|
'longUrl' => 'https://foo5',
|
||||||
'tags' => ['bar', 'baz'],
|
'tags' => ['bar', 'baz'],
|
||||||
]), $this->relationResolver);
|
]), $this->relationResolver);
|
||||||
$this->getEntityManager()->persist($shortUrl5);
|
$this->getEntityManager()->persist($shortUrl5);
|
||||||
@@ -273,21 +274,21 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function findListReturnsOnlyThoseWithMatchingDomains(): void
|
public function findListReturnsOnlyThoseWithMatchingDomains(): void
|
||||||
{
|
{
|
||||||
$shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
$shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
'longUrl' => 'foo1',
|
'longUrl' => 'https://foo1',
|
||||||
'domain' => null,
|
'domain' => null,
|
||||||
]), $this->relationResolver);
|
]), $this->relationResolver);
|
||||||
$this->getEntityManager()->persist($shortUrl1);
|
$this->getEntityManager()->persist($shortUrl1);
|
||||||
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
'longUrl' => 'foo2',
|
'longUrl' => 'https://foo2',
|
||||||
'domain' => null,
|
'domain' => null,
|
||||||
]), $this->relationResolver);
|
]), $this->relationResolver);
|
||||||
$this->getEntityManager()->persist($shortUrl2);
|
$this->getEntityManager()->persist($shortUrl2);
|
||||||
$shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
$shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
'longUrl' => 'foo3',
|
'longUrl' => 'https://foo3',
|
||||||
'domain' => 'another.com',
|
'domain' => 'another.com',
|
||||||
]), $this->relationResolver);
|
]), $this->relationResolver);
|
||||||
$this->getEntityManager()->persist($shortUrl3);
|
$this->getEntityManager()->persist($shortUrl3);
|
||||||
@@ -309,26 +310,26 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
|
|||||||
self::assertCount(0, $this->repo->findList($buildFiltering('no results')));
|
self::assertCount(0, $this->repo->findList($buildFiltering('no results')));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function findListReturnsOnlyThoseWithoutExcludedUrls(): void
|
public function findListReturnsOnlyThoseWithoutExcludedUrls(): void
|
||||||
{
|
{
|
||||||
$shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
$shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
'longUrl' => 'foo1',
|
'longUrl' => 'https://foo1',
|
||||||
'validUntil' => Chronos::now()->addDays(1)->toAtomString(),
|
'validUntil' => Chronos::now()->addDays(1)->toAtomString(),
|
||||||
'maxVisits' => 100,
|
'maxVisits' => 100,
|
||||||
]), $this->relationResolver);
|
]), $this->relationResolver);
|
||||||
$this->getEntityManager()->persist($shortUrl1);
|
$this->getEntityManager()->persist($shortUrl1);
|
||||||
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
'longUrl' => 'foo2',
|
'longUrl' => 'https://foo2',
|
||||||
'validUntil' => Chronos::now()->subDays(1)->toAtomString(),
|
'validUntil' => Chronos::now()->subDays(1)->toAtomString(),
|
||||||
]), $this->relationResolver);
|
]), $this->relationResolver);
|
||||||
$this->getEntityManager()->persist($shortUrl2);
|
$this->getEntityManager()->persist($shortUrl2);
|
||||||
$shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
$shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
'longUrl' => 'foo3',
|
'longUrl' => 'https://foo3',
|
||||||
]), $this->relationResolver);
|
]), $this->relationResolver);
|
||||||
$this->getEntityManager()->persist($shortUrl3);
|
$this->getEntityManager()->persist($shortUrl3);
|
||||||
$shortUrl4 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
$shortUrl4 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
'longUrl' => 'foo4',
|
'longUrl' => 'https://foo4',
|
||||||
'maxVisits' => 3,
|
'maxVisits' => 3,
|
||||||
]), $this->relationResolver);
|
]), $this->relationResolver);
|
||||||
$this->getEntityManager()->persist($shortUrl4);
|
$this->getEntityManager()->persist($shortUrl4);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioDbTest\Shlink\Core\ShortUrl\Repository;
|
namespace ShlinkioDbTest\Shlink\Core\ShortUrl\Repository;
|
||||||
|
|
||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
use Doctrine\DBAL\Platforms\SQLServerPlatform;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||||
@@ -31,19 +31,21 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||||||
$this->relationResolver = new PersistenceShortUrlRelationResolver($this->getEntityManager());
|
$this->relationResolver = new PersistenceShortUrlRelationResolver($this->getEntityManager());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function findOneWithDomainFallbackReturnsProperData(): void
|
public function findOneWithDomainFallbackReturnsProperData(): void
|
||||||
{
|
{
|
||||||
$regularOne = ShortUrl::create(ShortUrlCreation::fromRawData(['customSlug' => 'Foo', 'longUrl' => 'foo']));
|
$regularOne = ShortUrl::create(
|
||||||
|
ShortUrlCreation::fromRawData(['customSlug' => 'Foo', 'longUrl' => 'https://foo']),
|
||||||
|
);
|
||||||
$this->getEntityManager()->persist($regularOne);
|
$this->getEntityManager()->persist($regularOne);
|
||||||
|
|
||||||
$withDomain = ShortUrl::create(ShortUrlCreation::fromRawData(
|
$withDomain = ShortUrl::create(ShortUrlCreation::fromRawData(
|
||||||
['domain' => 'example.com', 'customSlug' => 'domain-short-code', 'longUrl' => 'foo'],
|
['domain' => 'example.com', 'customSlug' => 'domain-short-code', 'longUrl' => 'https://foo'],
|
||||||
));
|
));
|
||||||
$this->getEntityManager()->persist($withDomain);
|
$this->getEntityManager()->persist($withDomain);
|
||||||
|
|
||||||
$withDomainDuplicatingRegular = ShortUrl::create(ShortUrlCreation::fromRawData(
|
$withDomainDuplicatingRegular = ShortUrl::create(ShortUrlCreation::fromRawData(
|
||||||
['domain' => 's.test', 'customSlug' => 'Foo', 'longUrl' => 'foo_with_domain'],
|
['domain' => 's.test', 'customSlug' => 'Foo', 'longUrl' => 'https://foo_with_domain'],
|
||||||
));
|
));
|
||||||
$this->getEntityManager()->persist($withDomainDuplicatingRegular);
|
$this->getEntityManager()->persist($withDomainDuplicatingRegular);
|
||||||
|
|
||||||
@@ -55,19 +57,16 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||||||
));
|
));
|
||||||
self::assertSame($regularOne, $this->repo->findOneWithDomainFallback(
|
self::assertSame($regularOne, $this->repo->findOneWithDomainFallback(
|
||||||
ShortUrlIdentifier::fromShortCodeAndDomain('foo'),
|
ShortUrlIdentifier::fromShortCodeAndDomain('foo'),
|
||||||
ShortUrlMode::LOOSELY,
|
ShortUrlMode::LOOSE,
|
||||||
));
|
));
|
||||||
self::assertSame($regularOne, $this->repo->findOneWithDomainFallback(
|
self::assertSame($regularOne, $this->repo->findOneWithDomainFallback(
|
||||||
ShortUrlIdentifier::fromShortCodeAndDomain('fOo'),
|
ShortUrlIdentifier::fromShortCodeAndDomain('fOo'),
|
||||||
ShortUrlMode::LOOSELY,
|
ShortUrlMode::LOOSE,
|
||||||
|
));
|
||||||
|
self::assertNull($this->repo->findOneWithDomainFallback(
|
||||||
|
ShortUrlIdentifier::fromShortCodeAndDomain('foo'),
|
||||||
|
ShortUrlMode::STRICT,
|
||||||
));
|
));
|
||||||
// TODO MS is doing loosely checks always, making this fail.
|
|
||||||
if (! $this->getEntityManager()->getConnection()->getDatabasePlatform() instanceof SQLServerPlatform) {
|
|
||||||
self::assertNull($this->repo->findOneWithDomainFallback(
|
|
||||||
ShortUrlIdentifier::fromShortCodeAndDomain('foo'),
|
|
||||||
ShortUrlMode::STRICT,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
self::assertSame($regularOne, $this->repo->findOneWithDomainFallback(
|
self::assertSame($regularOne, $this->repo->findOneWithDomainFallback(
|
||||||
ShortUrlIdentifier::fromShortCodeAndDomain($withDomainDuplicatingRegular->getShortCode()),
|
ShortUrlIdentifier::fromShortCodeAndDomain($withDomainDuplicatingRegular->getShortCode()),
|
||||||
ShortUrlMode::STRICT,
|
ShortUrlMode::STRICT,
|
||||||
@@ -101,17 +100,17 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function shortCodeIsInUseLooksForShortUrlInProperSetOfTables(): void
|
public function shortCodeIsInUseLooksForShortUrlInProperSetOfTables(): void
|
||||||
{
|
{
|
||||||
$shortUrlWithoutDomain = ShortUrl::create(
|
$shortUrlWithoutDomain = ShortUrl::create(
|
||||||
ShortUrlCreation::fromRawData(['customSlug' => 'my-cool-slug', 'longUrl' => 'foo']),
|
ShortUrlCreation::fromRawData(['customSlug' => 'my-cool-slug', 'longUrl' => 'https://foo']),
|
||||||
);
|
);
|
||||||
$this->getEntityManager()->persist($shortUrlWithoutDomain);
|
$this->getEntityManager()->persist($shortUrlWithoutDomain);
|
||||||
|
|
||||||
$shortUrlWithDomain = ShortUrl::create(
|
$shortUrlWithDomain = ShortUrl::create(ShortUrlCreation::fromRawData(
|
||||||
ShortUrlCreation::fromRawData(['domain' => 's.test', 'customSlug' => 'another-slug', 'longUrl' => 'foo']),
|
['domain' => 's.test', 'customSlug' => 'another-slug', 'longUrl' => 'https://foo'],
|
||||||
);
|
));
|
||||||
$this->getEntityManager()->persist($shortUrlWithDomain);
|
$this->getEntityManager()->persist($shortUrlWithDomain);
|
||||||
|
|
||||||
$this->getEntityManager()->flush();
|
$this->getEntityManager()->flush();
|
||||||
@@ -130,17 +129,17 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function findOneLooksForShortUrlInProperSetOfTables(): void
|
public function findOneLooksForShortUrlInProperSetOfTables(): void
|
||||||
{
|
{
|
||||||
$shortUrlWithoutDomain = ShortUrl::create(
|
$shortUrlWithoutDomain = ShortUrl::create(
|
||||||
ShortUrlCreation::fromRawData(['customSlug' => 'my-cool-slug', 'longUrl' => 'foo']),
|
ShortUrlCreation::fromRawData(['customSlug' => 'my-cool-slug', 'longUrl' => 'https://foo']),
|
||||||
);
|
);
|
||||||
$this->getEntityManager()->persist($shortUrlWithoutDomain);
|
$this->getEntityManager()->persist($shortUrlWithoutDomain);
|
||||||
|
|
||||||
$shortUrlWithDomain = ShortUrl::create(
|
$shortUrlWithDomain = ShortUrl::create(ShortUrlCreation::fromRawData(
|
||||||
ShortUrlCreation::fromRawData(['domain' => 's.test', 'customSlug' => 'another-slug', 'longUrl' => 'foo']),
|
['domain' => 's.test', 'customSlug' => 'another-slug', 'longUrl' => 'https://foo'],
|
||||||
);
|
));
|
||||||
$this->getEntityManager()->persist($shortUrlWithDomain);
|
$this->getEntityManager()->persist($shortUrlWithDomain);
|
||||||
|
|
||||||
$this->getEntityManager()->flush();
|
$this->getEntityManager()->flush();
|
||||||
@@ -157,70 +156,75 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function findOneMatchingReturnsNullForNonExistingShortUrls(): void
|
public function findOneMatchingReturnsNullForNonExistingShortUrls(): void
|
||||||
{
|
{
|
||||||
self::assertNull($this->repo->findOneMatching(ShortUrlCreation::fromRawData(['longUrl' => 'foobar'])));
|
self::assertNull($this->repo->findOneMatching(ShortUrlCreation::fromRawData(['longUrl' => 'https://foobar'])));
|
||||||
self::assertNull($this->repo->findOneMatching(
|
self::assertNull($this->repo->findOneMatching(
|
||||||
ShortUrlCreation::fromRawData(['longUrl' => 'foobar', 'tags' => ['foo', 'bar']]),
|
ShortUrlCreation::fromRawData(['longUrl' => 'https://foobar', 'tags' => ['foo', 'bar']]),
|
||||||
));
|
));
|
||||||
self::assertNull($this->repo->findOneMatching(ShortUrlCreation::fromRawData([
|
self::assertNull($this->repo->findOneMatching(ShortUrlCreation::fromRawData([
|
||||||
'validSince' => Chronos::parse('2020-03-05 20:18:30'),
|
'validSince' => Chronos::parse('2020-03-05 20:18:30'),
|
||||||
'customSlug' => 'this_slug_does_not_exist',
|
'customSlug' => 'this_slug_does_not_exist',
|
||||||
'longUrl' => 'foobar',
|
'longUrl' => 'https://foobar',
|
||||||
'tags' => ['foo', 'bar'],
|
'tags' => ['foo', 'bar'],
|
||||||
])));
|
])));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function findOneMatchingAppliesProperConditions(): void
|
public function findOneMatchingAppliesProperConditions(): void
|
||||||
{
|
{
|
||||||
$start = Chronos::parse('2020-03-05 20:18:30');
|
$start = Chronos::parse('2020-03-05 20:18:30');
|
||||||
$end = Chronos::parse('2021-03-05 20:18:30');
|
$end = Chronos::parse('2021-03-05 20:18:30');
|
||||||
|
|
||||||
$shortUrl = ShortUrl::create(
|
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(
|
||||||
ShortUrlCreation::fromRawData(['validSince' => $start, 'longUrl' => 'foo', 'tags' => ['foo', 'bar']]),
|
['validSince' => $start, 'longUrl' => 'https://foo', 'tags' => ['foo', 'bar']],
|
||||||
$this->relationResolver,
|
), $this->relationResolver);
|
||||||
);
|
|
||||||
$this->getEntityManager()->persist($shortUrl);
|
$this->getEntityManager()->persist($shortUrl);
|
||||||
|
|
||||||
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData(['validUntil' => $end, 'longUrl' => 'bar']));
|
$shortUrl2 = ShortUrl::create(
|
||||||
|
ShortUrlCreation::fromRawData(['validUntil' => $end, 'longUrl' => 'https://bar']),
|
||||||
|
);
|
||||||
$this->getEntityManager()->persist($shortUrl2);
|
$this->getEntityManager()->persist($shortUrl2);
|
||||||
|
|
||||||
$shortUrl3 = ShortUrl::create(
|
$shortUrl3 = ShortUrl::create(
|
||||||
ShortUrlCreation::fromRawData(['validSince' => $start, 'validUntil' => $end, 'longUrl' => 'baz']),
|
ShortUrlCreation::fromRawData(['validSince' => $start, 'validUntil' => $end, 'longUrl' => 'https://baz']),
|
||||||
);
|
);
|
||||||
$this->getEntityManager()->persist($shortUrl3);
|
$this->getEntityManager()->persist($shortUrl3);
|
||||||
|
|
||||||
$shortUrl4 = ShortUrl::create(
|
$shortUrl4 = ShortUrl::create(
|
||||||
ShortUrlCreation::fromRawData(['customSlug' => 'custom', 'validUntil' => $end, 'longUrl' => 'foo']),
|
ShortUrlCreation::fromRawData(['customSlug' => 'custom', 'validUntil' => $end, 'longUrl' => 'https://foo']),
|
||||||
);
|
);
|
||||||
$this->getEntityManager()->persist($shortUrl4);
|
$this->getEntityManager()->persist($shortUrl4);
|
||||||
|
|
||||||
$shortUrl5 = ShortUrl::create(ShortUrlCreation::fromRawData(['maxVisits' => 3, 'longUrl' => 'foo']));
|
$shortUrl5 = ShortUrl::create(ShortUrlCreation::fromRawData(['maxVisits' => 3, 'longUrl' => 'https://foo']));
|
||||||
$this->getEntityManager()->persist($shortUrl5);
|
$this->getEntityManager()->persist($shortUrl5);
|
||||||
|
|
||||||
$shortUrl6 = ShortUrl::create(ShortUrlCreation::fromRawData(['domain' => 's.test', 'longUrl' => 'foo']));
|
$shortUrl6 = ShortUrl::create(
|
||||||
|
ShortUrlCreation::fromRawData(['domain' => 's.test', 'longUrl' => 'https://foo']),
|
||||||
|
);
|
||||||
$this->getEntityManager()->persist($shortUrl6);
|
$this->getEntityManager()->persist($shortUrl6);
|
||||||
|
|
||||||
$this->getEntityManager()->flush();
|
$this->getEntityManager()->flush();
|
||||||
|
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
$shortUrl,
|
$shortUrl,
|
||||||
$this->repo->findOneMatching(
|
$this->repo->findOneMatching(ShortUrlCreation::fromRawData(
|
||||||
ShortUrlCreation::fromRawData(['validSince' => $start, 'longUrl' => 'foo', 'tags' => ['foo', 'bar']]),
|
['validSince' => $start, 'longUrl' => 'https://foo', 'tags' => ['foo', 'bar']],
|
||||||
),
|
)),
|
||||||
);
|
);
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
$shortUrl2,
|
$shortUrl2,
|
||||||
$this->repo->findOneMatching(ShortUrlCreation::fromRawData(['validUntil' => $end, 'longUrl' => 'bar'])),
|
$this->repo->findOneMatching(
|
||||||
|
ShortUrlCreation::fromRawData(['validUntil' => $end, 'longUrl' => 'https://bar']),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
$shortUrl3,
|
$shortUrl3,
|
||||||
$this->repo->findOneMatching(ShortUrlCreation::fromRawData([
|
$this->repo->findOneMatching(ShortUrlCreation::fromRawData([
|
||||||
'validSince' => $start,
|
'validSince' => $start,
|
||||||
'validUntil' => $end,
|
'validUntil' => $end,
|
||||||
'longUrl' => 'baz',
|
'longUrl' => 'https://baz',
|
||||||
])),
|
])),
|
||||||
);
|
);
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
@@ -228,26 +232,28 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||||||
$this->repo->findOneMatching(ShortUrlCreation::fromRawData([
|
$this->repo->findOneMatching(ShortUrlCreation::fromRawData([
|
||||||
'customSlug' => 'custom',
|
'customSlug' => 'custom',
|
||||||
'validUntil' => $end,
|
'validUntil' => $end,
|
||||||
'longUrl' => 'foo',
|
'longUrl' => 'https://foo',
|
||||||
])),
|
])),
|
||||||
);
|
);
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
$shortUrl5,
|
$shortUrl5,
|
||||||
$this->repo->findOneMatching(ShortUrlCreation::fromRawData(['maxVisits' => 3, 'longUrl' => 'foo'])),
|
$this->repo->findOneMatching(ShortUrlCreation::fromRawData(['maxVisits' => 3, 'longUrl' => 'https://foo'])),
|
||||||
);
|
);
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
$shortUrl6,
|
$shortUrl6,
|
||||||
$this->repo->findOneMatching(ShortUrlCreation::fromRawData(['domain' => 's.test', 'longUrl' => 'foo'])),
|
$this->repo->findOneMatching(
|
||||||
|
ShortUrlCreation::fromRawData(['domain' => 's.test', 'longUrl' => 'https://foo']),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function findOneMatchingReturnsOldestOneWhenThereAreMultipleMatches(): void
|
public function findOneMatchingReturnsOldestOneWhenThereAreMultipleMatches(): void
|
||||||
{
|
{
|
||||||
$start = Chronos::parse('2020-03-05 20:18:30');
|
$start = Chronos::parse('2020-03-05 20:18:30');
|
||||||
$tags = ['foo', 'bar'];
|
$tags = ['foo', 'bar'];
|
||||||
$meta = ShortUrlCreation::fromRawData(
|
$meta = ShortUrlCreation::fromRawData(
|
||||||
['validSince' => $start, 'maxVisits' => 50, 'longUrl' => 'foo', 'tags' => $tags],
|
['validSince' => $start, 'maxVisits' => 50, 'longUrl' => 'https://foo', 'tags' => $tags],
|
||||||
);
|
);
|
||||||
|
|
||||||
$shortUrl1 = ShortUrl::create($meta, $this->relationResolver);
|
$shortUrl1 = ShortUrl::create($meta, $this->relationResolver);
|
||||||
@@ -269,7 +275,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||||||
self::assertNotSame($shortUrl3, $result);
|
self::assertNotSame($shortUrl3, $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function findOneMatchingAppliesProvidedApiKeyConditions(): void
|
public function findOneMatchingAppliesProvidedApiKeyConditions(): void
|
||||||
{
|
{
|
||||||
$start = Chronos::parse('2020-03-05 20:18:30');
|
$start = Chronos::parse('2020-03-05 20:18:30');
|
||||||
@@ -296,14 +302,14 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||||||
'validSince' => $start,
|
'validSince' => $start,
|
||||||
'apiKey' => $apiKey,
|
'apiKey' => $apiKey,
|
||||||
'domain' => $rightDomain->authority,
|
'domain' => $rightDomain->authority,
|
||||||
'longUrl' => 'foo',
|
'longUrl' => 'https://foo',
|
||||||
'tags' => ['foo', 'bar'],
|
'tags' => ['foo', 'bar'],
|
||||||
]), $this->relationResolver);
|
]), $this->relationResolver);
|
||||||
$this->getEntityManager()->persist($shortUrl);
|
$this->getEntityManager()->persist($shortUrl);
|
||||||
|
|
||||||
$nonDomainShortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([
|
$nonDomainShortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
'apiKey' => $apiKey,
|
'apiKey' => $apiKey,
|
||||||
'longUrl' => 'non-domain',
|
'longUrl' => 'https://non-domain',
|
||||||
]), $this->relationResolver);
|
]), $this->relationResolver);
|
||||||
$this->getEntityManager()->persist($nonDomainShortUrl);
|
$this->getEntityManager()->persist($nonDomainShortUrl);
|
||||||
|
|
||||||
@@ -311,26 +317,26 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||||||
|
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
$shortUrl,
|
$shortUrl,
|
||||||
$this->repo->findOneMatching(
|
$this->repo->findOneMatching(ShortUrlCreation::fromRawData(
|
||||||
ShortUrlCreation::fromRawData(['validSince' => $start, 'longUrl' => 'foo', 'tags' => ['foo', 'bar']]),
|
['validSince' => $start, 'longUrl' => 'https://foo', 'tags' => ['foo', 'bar']],
|
||||||
),
|
)),
|
||||||
);
|
);
|
||||||
self::assertSame($shortUrl, $this->repo->findOneMatching(ShortUrlCreation::fromRawData([
|
self::assertSame($shortUrl, $this->repo->findOneMatching(ShortUrlCreation::fromRawData([
|
||||||
'validSince' => $start,
|
'validSince' => $start,
|
||||||
'apiKey' => $apiKey,
|
'apiKey' => $apiKey,
|
||||||
'longUrl' => 'foo',
|
'longUrl' => 'https://foo',
|
||||||
'tags' => ['foo', 'bar'],
|
'tags' => ['foo', 'bar'],
|
||||||
])));
|
])));
|
||||||
self::assertSame($shortUrl, $this->repo->findOneMatching(ShortUrlCreation::fromRawData([
|
self::assertSame($shortUrl, $this->repo->findOneMatching(ShortUrlCreation::fromRawData([
|
||||||
'validSince' => $start,
|
'validSince' => $start,
|
||||||
'apiKey' => $adminApiKey,
|
'apiKey' => $adminApiKey,
|
||||||
'longUrl' => 'foo',
|
'longUrl' => 'https://foo',
|
||||||
'tags' => ['foo', 'bar'],
|
'tags' => ['foo', 'bar'],
|
||||||
])));
|
])));
|
||||||
self::assertNull($this->repo->findOneMatching(ShortUrlCreation::fromRawData([
|
self::assertNull($this->repo->findOneMatching(ShortUrlCreation::fromRawData([
|
||||||
'validSince' => $start,
|
'validSince' => $start,
|
||||||
'apiKey' => $otherApiKey,
|
'apiKey' => $otherApiKey,
|
||||||
'longUrl' => 'foo',
|
'longUrl' => 'https://foo',
|
||||||
'tags' => ['foo', 'bar'],
|
'tags' => ['foo', 'bar'],
|
||||||
])));
|
])));
|
||||||
|
|
||||||
@@ -339,7 +345,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||||||
$this->repo->findOneMatching(ShortUrlCreation::fromRawData([
|
$this->repo->findOneMatching(ShortUrlCreation::fromRawData([
|
||||||
'validSince' => $start,
|
'validSince' => $start,
|
||||||
'domain' => $rightDomain->authority,
|
'domain' => $rightDomain->authority,
|
||||||
'longUrl' => 'foo',
|
'longUrl' => 'https://foo',
|
||||||
'tags' => ['foo', 'bar'],
|
'tags' => ['foo', 'bar'],
|
||||||
])),
|
])),
|
||||||
);
|
);
|
||||||
@@ -349,7 +355,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||||||
'validSince' => $start,
|
'validSince' => $start,
|
||||||
'domain' => $rightDomain->authority,
|
'domain' => $rightDomain->authority,
|
||||||
'apiKey' => $rightDomainApiKey,
|
'apiKey' => $rightDomainApiKey,
|
||||||
'longUrl' => 'foo',
|
'longUrl' => 'https://foo',
|
||||||
'tags' => ['foo', 'bar'],
|
'tags' => ['foo', 'bar'],
|
||||||
])),
|
])),
|
||||||
);
|
);
|
||||||
@@ -359,7 +365,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||||||
'validSince' => $start,
|
'validSince' => $start,
|
||||||
'domain' => $rightDomain->authority,
|
'domain' => $rightDomain->authority,
|
||||||
'apiKey' => $apiKey,
|
'apiKey' => $apiKey,
|
||||||
'longUrl' => 'foo',
|
'longUrl' => 'https://foo',
|
||||||
'tags' => ['foo', 'bar'],
|
'tags' => ['foo', 'bar'],
|
||||||
])),
|
])),
|
||||||
);
|
);
|
||||||
@@ -368,7 +374,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||||||
'validSince' => $start,
|
'validSince' => $start,
|
||||||
'domain' => $rightDomain->authority,
|
'domain' => $rightDomain->authority,
|
||||||
'apiKey' => $wrongDomainApiKey,
|
'apiKey' => $wrongDomainApiKey,
|
||||||
'longUrl' => 'foo',
|
'longUrl' => 'https://foo',
|
||||||
'tags' => ['foo', 'bar'],
|
'tags' => ['foo', 'bar'],
|
||||||
])),
|
])),
|
||||||
);
|
);
|
||||||
@@ -377,29 +383,29 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||||||
$nonDomainShortUrl,
|
$nonDomainShortUrl,
|
||||||
$this->repo->findOneMatching(ShortUrlCreation::fromRawData([
|
$this->repo->findOneMatching(ShortUrlCreation::fromRawData([
|
||||||
'apiKey' => $apiKey,
|
'apiKey' => $apiKey,
|
||||||
'longUrl' => 'non-domain',
|
'longUrl' => 'https://non-domain',
|
||||||
])),
|
])),
|
||||||
);
|
);
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
$nonDomainShortUrl,
|
$nonDomainShortUrl,
|
||||||
$this->repo->findOneMatching(ShortUrlCreation::fromRawData([
|
$this->repo->findOneMatching(ShortUrlCreation::fromRawData([
|
||||||
'apiKey' => $adminApiKey,
|
'apiKey' => $adminApiKey,
|
||||||
'longUrl' => 'non-domain',
|
'longUrl' => 'https://non-domain',
|
||||||
])),
|
])),
|
||||||
);
|
);
|
||||||
self::assertNull(
|
self::assertNull(
|
||||||
$this->repo->findOneMatching(ShortUrlCreation::fromRawData([
|
$this->repo->findOneMatching(ShortUrlCreation::fromRawData([
|
||||||
'apiKey' => $otherApiKey,
|
'apiKey' => $otherApiKey,
|
||||||
'longUrl' => 'non-domain',
|
'longUrl' => 'https://non-domain',
|
||||||
])),
|
])),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function importedShortUrlsAreFoundWhenExpected(): void
|
public function importedShortUrlsAreFoundWhenExpected(): void
|
||||||
{
|
{
|
||||||
$buildImported = static fn (string $shortCode, ?String $domain = null) =>
|
$buildImported = static fn (string $shortCode, ?string $domain = null) =>
|
||||||
new ImportedShlinkUrl(ImportSource::BITLY, 'foo', [], Chronos::now(), $domain, $shortCode, null);
|
new ImportedShlinkUrl(ImportSource::BITLY, 'https://foo', [], Chronos::now(), $domain, $shortCode, null);
|
||||||
|
|
||||||
$shortUrlWithoutDomain = ShortUrl::fromImport($buildImported('my-cool-slug'), true);
|
$shortUrlWithoutDomain = ShortUrl::fromImport($buildImported('my-cool-slug'), true);
|
||||||
$this->getEntityManager()->persist($shortUrlWithoutDomain);
|
$this->getEntityManager()->persist($shortUrlWithoutDomain);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioDbTest\Shlink\Core\Tag\Paginator\Adapter;
|
namespace ShlinkioDbTest\Shlink\Core\Tag\Paginator\Adapter;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Shlinkio\Shlink\Core\Tag\Entity\Tag;
|
use Shlinkio\Shlink\Core\Tag\Entity\Tag;
|
||||||
use Shlinkio\Shlink\Core\Tag\Model\TagsParams;
|
use Shlinkio\Shlink\Core\Tag\Model\TagsParams;
|
||||||
use Shlinkio\Shlink\Core\Tag\Paginator\Adapter\TagsPaginatorAdapter;
|
use Shlinkio\Shlink\Core\Tag\Paginator\Adapter\TagsPaginatorAdapter;
|
||||||
@@ -24,9 +26,8 @@ class TagsPaginatorAdapterTest extends DatabaseTestCase
|
|||||||
/**
|
/**
|
||||||
* @param int<0, max> $offset
|
* @param int<0, max> $offset
|
||||||
* @param int<0, max> $length
|
* @param int<0, max> $length
|
||||||
* @test
|
|
||||||
* @dataProvider provideFilters
|
|
||||||
*/
|
*/
|
||||||
|
#[Test, DataProvider('provideFilters')]
|
||||||
public function expectedListOfTagsIsReturned(
|
public function expectedListOfTagsIsReturned(
|
||||||
?string $searchTerm,
|
?string $searchTerm,
|
||||||
?string $orderBy,
|
?string $orderBy,
|
||||||
@@ -52,7 +53,7 @@ class TagsPaginatorAdapterTest extends DatabaseTestCase
|
|||||||
self::assertEquals($expectedTotalCount, $adapter->getNbResults());
|
self::assertEquals($expectedTotalCount, $adapter->getNbResults());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideFilters(): iterable
|
public static function provideFilters(): iterable
|
||||||
{
|
{
|
||||||
yield [null, null, 0, 10, ['another', 'bar', 'baz', 'foo'], 4];
|
yield [null, null, 0, 10, ['another', 'bar', 'baz', 'foo'], 4];
|
||||||
yield [null, null, 2, 10, ['baz', 'foo'], 4];
|
yield [null, null, 2, 10, ['baz', 'foo'], 4];
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioDbTest\Shlink\Core\Tag\Repository;
|
namespace ShlinkioDbTest\Shlink\Core\Tag\Repository;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||||
use Shlinkio\Shlink\Core\Model\Ordering;
|
use Shlinkio\Shlink\Core\Model\Ordering;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
@@ -34,13 +36,13 @@ class TagRepositoryTest extends DatabaseTestCase
|
|||||||
$this->relationResolver = new PersistenceShortUrlRelationResolver($this->getEntityManager());
|
$this->relationResolver = new PersistenceShortUrlRelationResolver($this->getEntityManager());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function deleteByNameDoesNothingWhenEmptyListIsProvided(): void
|
public function deleteByNameDoesNothingWhenEmptyListIsProvided(): void
|
||||||
{
|
{
|
||||||
self::assertEquals(0, $this->repo->deleteByName([]));
|
self::assertEquals(0, $this->repo->deleteByName([]));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function allTagsWhichMatchNameAreDeleted(): void
|
public function allTagsWhichMatchNameAreDeleted(): void
|
||||||
{
|
{
|
||||||
$names = ['foo', 'bar', 'baz'];
|
$names = ['foo', 'bar', 'baz'];
|
||||||
@@ -54,10 +56,7 @@ class TagRepositoryTest extends DatabaseTestCase
|
|||||||
self::assertEquals(2, $this->repo->deleteByName($toDelete));
|
self::assertEquals(2, $this->repo->deleteByName($toDelete));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideFilters')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideFilters
|
|
||||||
*/
|
|
||||||
public function properTagsInfoIsReturned(?TagsListFiltering $filtering, array $expectedList): void
|
public function properTagsInfoIsReturned(?TagsListFiltering $filtering, array $expectedList): void
|
||||||
{
|
{
|
||||||
$names = ['foo', 'bar', 'baz', 'another'];
|
$names = ['foo', 'bar', 'baz', 'another'];
|
||||||
@@ -75,7 +74,7 @@ class TagRepositoryTest extends DatabaseTestCase
|
|||||||
[$firstUrlTags] = array_chunk($names, 3);
|
[$firstUrlTags] = array_chunk($names, 3);
|
||||||
$secondUrlTags = [$names[0]];
|
$secondUrlTags = [$names[0]];
|
||||||
$metaWithTags = static fn (array $tags, ?ApiKey $apiKey) => ShortUrlCreation::fromRawData(
|
$metaWithTags = static fn (array $tags, ?ApiKey $apiKey) => ShortUrlCreation::fromRawData(
|
||||||
['longUrl' => 'longUrl', 'tags' => $tags, 'apiKey' => $apiKey],
|
['longUrl' => 'https://longUrl', 'tags' => $tags, 'apiKey' => $apiKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
$shortUrl = ShortUrl::create($metaWithTags($firstUrlTags, $apiKey), $this->relationResolver);
|
$shortUrl = ShortUrl::create($metaWithTags($firstUrlTags, $apiKey), $this->relationResolver);
|
||||||
@@ -109,7 +108,7 @@ class TagRepositoryTest extends DatabaseTestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideFilters(): iterable
|
public static function provideFilters(): iterable
|
||||||
{
|
{
|
||||||
$defaultList = [
|
$defaultList = [
|
||||||
['another', 0, 0, 0],
|
['another', 0, 0, 0],
|
||||||
@@ -221,7 +220,7 @@ class TagRepositoryTest extends DatabaseTestCase
|
|||||||
]];
|
]];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function tagExistsReturnsExpectedResultBasedOnApiKey(): void
|
public function tagExistsReturnsExpectedResultBasedOnApiKey(): void
|
||||||
{
|
{
|
||||||
$domain = Domain::withAuthority('foo.com');
|
$domain = Domain::withAuthority('foo.com');
|
||||||
@@ -241,15 +240,14 @@ class TagRepositoryTest extends DatabaseTestCase
|
|||||||
|
|
||||||
[$firstUrlTags, $secondUrlTags] = array_chunk($names, 3);
|
[$firstUrlTags, $secondUrlTags] = array_chunk($names, 3);
|
||||||
|
|
||||||
$shortUrl = ShortUrl::create(
|
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(
|
||||||
ShortUrlCreation::fromRawData(['apiKey' => $authorApiKey, 'longUrl' => 'longUrl', 'tags' => $firstUrlTags]),
|
['apiKey' => $authorApiKey, 'longUrl' => 'https://longUrl', 'tags' => $firstUrlTags],
|
||||||
$this->relationResolver,
|
), $this->relationResolver);
|
||||||
);
|
|
||||||
$this->getEntityManager()->persist($shortUrl);
|
$this->getEntityManager()->persist($shortUrl);
|
||||||
|
|
||||||
$shortUrl2 = ShortUrl::create(
|
$shortUrl2 = ShortUrl::create(
|
||||||
ShortUrlCreation::fromRawData(
|
ShortUrlCreation::fromRawData(
|
||||||
['domain' => $domain->authority, 'longUrl' => 'longUrl', 'tags' => $secondUrlTags],
|
['domain' => $domain->authority, 'longUrl' => 'https://longUrl', 'tags' => $secondUrlTags],
|
||||||
),
|
),
|
||||||
$this->relationResolver,
|
$this->relationResolver,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioDbTest\Shlink\Core\Visit\Repository;
|
namespace ShlinkioDbTest\Shlink\Core\Visit\Repository;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||||
use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
|
use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
|
||||||
@@ -25,10 +27,7 @@ class VisitLocationRepositoryTest extends DatabaseTestCase
|
|||||||
$this->repo = new VisitLocationRepository($em, $em->getClassMetadata(Visit::class));
|
$this->repo = new VisitLocationRepository($em, $em->getClassMetadata(Visit::class));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideBlockSize')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideBlockSize
|
|
||||||
*/
|
|
||||||
public function findVisitsReturnsProperVisits(int $blockSize): void
|
public function findVisitsReturnsProperVisits(int $blockSize): void
|
||||||
{
|
{
|
||||||
$shortUrl = ShortUrl::createFake();
|
$shortUrl = ShortUrl::createFake();
|
||||||
@@ -56,7 +55,7 @@ class VisitLocationRepositoryTest extends DatabaseTestCase
|
|||||||
self::assertCount(6, [...$all]);
|
self::assertCount(6, [...$all]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideBlockSize(): iterable
|
public static function provideBlockSize(): iterable
|
||||||
{
|
{
|
||||||
return map(range(1, 10), fn (int $value) => [$value]);
|
return map(range(1, 10), fn (int $value) => [$value]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioDbTest\Shlink\Core\Visit\Repository;
|
namespace ShlinkioDbTest\Shlink\Core\Visit\Repository;
|
||||||
|
|
||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use ReflectionObject;
|
use ReflectionObject;
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||||
@@ -40,7 +41,7 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||||||
$this->relationResolver = new PersistenceShortUrlRelationResolver($this->getEntityManager());
|
$this->relationResolver = new PersistenceShortUrlRelationResolver($this->getEntityManager());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function findVisitsByShortCodeReturnsProperData(): void
|
public function findVisitsByShortCodeReturnsProperData(): void
|
||||||
{
|
{
|
||||||
[$shortCode, $domain] = $this->createShortUrlsAndVisits();
|
[$shortCode, $domain] = $this->createShortUrlsAndVisits();
|
||||||
@@ -89,7 +90,7 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function countVisitsByShortCodeReturnsProperData(): void
|
public function countVisitsByShortCodeReturnsProperData(): void
|
||||||
{
|
{
|
||||||
[$shortCode, $domain] = $this->createShortUrlsAndVisits();
|
[$shortCode, $domain] = $this->createShortUrlsAndVisits();
|
||||||
@@ -126,7 +127,7 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function findVisitsByShortCodeReturnsProperDataWhenUsingAPiKeys(): void
|
public function findVisitsByShortCodeReturnsProperDataWhenUsingAPiKeys(): void
|
||||||
{
|
{
|
||||||
$adminApiKey = ApiKey::create();
|
$adminApiKey = ApiKey::create();
|
||||||
@@ -158,7 +159,7 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function findVisitsByTagReturnsProperData(): void
|
public function findVisitsByTagReturnsProperData(): void
|
||||||
{
|
{
|
||||||
$foo = 'foo';
|
$foo = 'foo';
|
||||||
@@ -183,7 +184,7 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function countVisitsByTagReturnsProperData(): void
|
public function countVisitsByTagReturnsProperData(): void
|
||||||
{
|
{
|
||||||
$foo = 'foo';
|
$foo = 'foo';
|
||||||
@@ -205,7 +206,7 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function findVisitsByDomainReturnsProperData(): void
|
public function findVisitsByDomainReturnsProperData(): void
|
||||||
{
|
{
|
||||||
$this->createShortUrlsAndVisits('s.test');
|
$this->createShortUrlsAndVisits('s.test');
|
||||||
@@ -229,7 +230,7 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function countVisitsByDomainReturnsProperData(): void
|
public function countVisitsByDomainReturnsProperData(): void
|
||||||
{
|
{
|
||||||
$this->createShortUrlsAndVisits('s.test');
|
$this->createShortUrlsAndVisits('s.test');
|
||||||
@@ -253,7 +254,7 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function countVisitsReturnsExpectedResultBasedOnApiKey(): void
|
public function countVisitsReturnsExpectedResultBasedOnApiKey(): void
|
||||||
{
|
{
|
||||||
$domain = Domain::withAuthority('foo.com');
|
$domain = Domain::withAuthority('foo.com');
|
||||||
@@ -265,7 +266,7 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||||||
$this->getEntityManager()->persist($apiKey1);
|
$this->getEntityManager()->persist($apiKey1);
|
||||||
$shortUrl = ShortUrl::create(
|
$shortUrl = ShortUrl::create(
|
||||||
ShortUrlCreation::fromRawData(
|
ShortUrlCreation::fromRawData(
|
||||||
['apiKey' => $apiKey1, 'domain' => $domain->authority, 'longUrl' => 'longUrl'],
|
['apiKey' => $apiKey1, 'domain' => $domain->authority, 'longUrl' => 'https://longUrl'],
|
||||||
),
|
),
|
||||||
$this->relationResolver,
|
$this->relationResolver,
|
||||||
);
|
);
|
||||||
@@ -274,13 +275,15 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||||||
|
|
||||||
$apiKey2 = ApiKey::fromMeta(ApiKeyMeta::withRoles(RoleDefinition::forAuthoredShortUrls()));
|
$apiKey2 = ApiKey::fromMeta(ApiKeyMeta::withRoles(RoleDefinition::forAuthoredShortUrls()));
|
||||||
$this->getEntityManager()->persist($apiKey2);
|
$this->getEntityManager()->persist($apiKey2);
|
||||||
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData(['apiKey' => $apiKey2, 'longUrl' => 'longUrl']));
|
$shortUrl2 = ShortUrl::create(
|
||||||
|
ShortUrlCreation::fromRawData(['apiKey' => $apiKey2, 'longUrl' => 'https://longUrl']),
|
||||||
|
);
|
||||||
$this->getEntityManager()->persist($shortUrl2);
|
$this->getEntityManager()->persist($shortUrl2);
|
||||||
$this->createVisitsForShortUrl($shortUrl2, 5);
|
$this->createVisitsForShortUrl($shortUrl2, 5);
|
||||||
|
|
||||||
$shortUrl3 = ShortUrl::create(
|
$shortUrl3 = ShortUrl::create(
|
||||||
ShortUrlCreation::fromRawData(
|
ShortUrlCreation::fromRawData(
|
||||||
['apiKey' => $apiKey2, 'domain' => $domain->authority, 'longUrl' => 'longUrl'],
|
['apiKey' => $apiKey2, 'domain' => $domain->authority, 'longUrl' => 'https://longUrl'],
|
||||||
),
|
),
|
||||||
$this->relationResolver,
|
$this->relationResolver,
|
||||||
);
|
);
|
||||||
@@ -316,10 +319,10 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||||||
self::assertEquals(3, $this->repo->countOrphanVisits(new VisitsCountFiltering(null, true)));
|
self::assertEquals(3, $this->repo->countOrphanVisits(new VisitsCountFiltering(null, true)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function findOrphanVisitsReturnsExpectedResult(): void
|
public function findOrphanVisitsReturnsExpectedResult(): void
|
||||||
{
|
{
|
||||||
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => 'longUrl']));
|
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => 'https://longUrl']));
|
||||||
$this->getEntityManager()->persist($shortUrl);
|
$this->getEntityManager()->persist($shortUrl);
|
||||||
$this->createVisitsForShortUrl($shortUrl, 7);
|
$this->createVisitsForShortUrl($shortUrl, 7);
|
||||||
|
|
||||||
@@ -365,10 +368,10 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function countOrphanVisitsReturnsExpectedResult(): void
|
public function countOrphanVisitsReturnsExpectedResult(): void
|
||||||
{
|
{
|
||||||
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => 'longUrl']));
|
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => 'https://longUrl']));
|
||||||
$this->getEntityManager()->persist($shortUrl);
|
$this->getEntityManager()->persist($shortUrl);
|
||||||
$this->createVisitsForShortUrl($shortUrl, 7);
|
$this->createVisitsForShortUrl($shortUrl, 7);
|
||||||
|
|
||||||
@@ -402,18 +405,18 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function findNonOrphanVisitsReturnsExpectedResult(): void
|
public function findNonOrphanVisitsReturnsExpectedResult(): void
|
||||||
{
|
{
|
||||||
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => '1']));
|
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => 'https://1']));
|
||||||
$this->getEntityManager()->persist($shortUrl);
|
$this->getEntityManager()->persist($shortUrl);
|
||||||
$this->createVisitsForShortUrl($shortUrl, 7);
|
$this->createVisitsForShortUrl($shortUrl, 7);
|
||||||
|
|
||||||
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => '2']));
|
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => 'https://2']));
|
||||||
$this->getEntityManager()->persist($shortUrl2);
|
$this->getEntityManager()->persist($shortUrl2);
|
||||||
$this->createVisitsForShortUrl($shortUrl2, 4);
|
$this->createVisitsForShortUrl($shortUrl2, 4);
|
||||||
|
|
||||||
$shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => '3']));
|
$shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => 'https://3']));
|
||||||
$this->getEntityManager()->persist($shortUrl3);
|
$this->getEntityManager()->persist($shortUrl3);
|
||||||
$this->createVisitsForShortUrl($shortUrl3, 10);
|
$this->createVisitsForShortUrl($shortUrl3, 10);
|
||||||
|
|
||||||
@@ -445,7 +448,7 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||||||
self::assertCount(5, $this->repo->findNonOrphanVisits(new VisitsListFiltering(null, false, null, 5, 5)));
|
self::assertCount(5, $this->repo->findNonOrphanVisits(new VisitsListFiltering(null, false, null, 5, 5)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function findMostRecentOrphanVisitReturnsExpectedVisit(): void
|
public function findMostRecentOrphanVisitReturnsExpectedVisit(): void
|
||||||
{
|
{
|
||||||
$this->assertNull($this->repo->findMostRecentOrphanVisit());
|
$this->assertNull($this->repo->findMostRecentOrphanVisit());
|
||||||
@@ -472,7 +475,7 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||||||
?ApiKey $apiKey = null,
|
?ApiKey $apiKey = null,
|
||||||
): array {
|
): array {
|
||||||
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([
|
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
ShortUrlInputFilter::LONG_URL => 'longUrl',
|
ShortUrlInputFilter::LONG_URL => 'https://longUrl',
|
||||||
ShortUrlInputFilter::TAGS => $tags,
|
ShortUrlInputFilter::TAGS => $tags,
|
||||||
ShortUrlInputFilter::API_KEY => $apiKey,
|
ShortUrlInputFilter::API_KEY => $apiKey,
|
||||||
]), $this->relationResolver);
|
]), $this->relationResolver);
|
||||||
@@ -486,7 +489,7 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||||||
$shortUrlWithDomain = ShortUrl::create(ShortUrlCreation::fromRawData([
|
$shortUrlWithDomain = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||||
'customSlug' => $shortCode,
|
'customSlug' => $shortCode,
|
||||||
'domain' => $domain,
|
'domain' => $domain,
|
||||||
'longUrl' => 'longUrl',
|
'longUrl' => 'https://longUrl',
|
||||||
]));
|
]));
|
||||||
$this->getEntityManager()->persist($shortUrlWithDomain);
|
$this->getEntityManager()->persist($shortUrlWithDomain);
|
||||||
$this->createVisitsForShortUrl($shortUrlWithDomain, 3);
|
$this->createVisitsForShortUrl($shortUrlWithDomain, 3);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioTest\Shlink\Core\Action;
|
namespace ShlinkioTest\Shlink\Core\Action;
|
||||||
|
|
||||||
use Laminas\Diactoros\ServerRequest;
|
use Laminas\Diactoros\ServerRequest;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
@@ -29,7 +30,7 @@ class PixelActionTest extends TestCase
|
|||||||
$this->action = new PixelAction($this->urlResolver, $this->requestTracker);
|
$this->action = new PixelAction($this->urlResolver, $this->requestTracker);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function imageIsReturned(): void
|
public function imageIsReturned(): void
|
||||||
{
|
{
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ namespace ShlinkioTest\Shlink\Core\Action;
|
|||||||
use Laminas\Diactoros\Response;
|
use Laminas\Diactoros\Response;
|
||||||
use Laminas\Diactoros\ServerRequest;
|
use Laminas\Diactoros\ServerRequest;
|
||||||
use Laminas\Diactoros\ServerRequestFactory;
|
use Laminas\Diactoros\ServerRequestFactory;
|
||||||
|
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 Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
@@ -37,7 +39,7 @@ class QrCodeActionTest extends TestCase
|
|||||||
$this->urlResolver = $this->createMock(ShortUrlResolverInterface::class);
|
$this->urlResolver = $this->createMock(ShortUrlResolverInterface::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function aNotFoundShortCodeWillDelegateIntoNextMiddleware(): void
|
public function aNotFoundShortCodeWillDelegateIntoNextMiddleware(): void
|
||||||
{
|
{
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
@@ -50,7 +52,7 @@ class QrCodeActionTest extends TestCase
|
|||||||
$this->action()->process((new ServerRequest())->withAttribute('shortCode', $shortCode), $delegate);
|
$this->action()->process((new ServerRequest())->withAttribute('shortCode', $shortCode), $delegate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function aCorrectRequestReturnsTheQrCodeResponse(): void
|
public function aCorrectRequestReturnsTheQrCodeResponse(): void
|
||||||
{
|
{
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
@@ -66,10 +68,7 @@ class QrCodeActionTest extends TestCase
|
|||||||
self::assertEquals(200, $resp->getStatusCode());
|
self::assertEquals(200, $resp->getStatusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideQueries')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideQueries
|
|
||||||
*/
|
|
||||||
public function imageIsReturnedWithExpectedContentTypeBasedOnProvidedFormat(
|
public function imageIsReturnedWithExpectedContentTypeBasedOnProvidedFormat(
|
||||||
string $defaultFormat,
|
string $defaultFormat,
|
||||||
array $query,
|
array $query,
|
||||||
@@ -87,7 +86,7 @@ class QrCodeActionTest extends TestCase
|
|||||||
self::assertEquals($expectedContentType, $resp->getHeaderLine('Content-Type'));
|
self::assertEquals($expectedContentType, $resp->getHeaderLine('Content-Type'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideQueries(): iterable
|
public static function provideQueries(): iterable
|
||||||
{
|
{
|
||||||
yield 'no format, png default' => ['png', [], 'image/png'];
|
yield 'no format, png default' => ['png', [], 'image/png'];
|
||||||
yield 'no format, svg default' => ['svg', [], 'image/svg+xml'];
|
yield 'no format, svg default' => ['svg', [], 'image/svg+xml'];
|
||||||
@@ -99,10 +98,7 @@ class QrCodeActionTest extends TestCase
|
|||||||
yield 'unsupported format, svg default' => ['svg', ['format' => 'jpg'], 'image/svg+xml'];
|
yield 'unsupported format, svg default' => ['svg', ['format' => 'jpg'], 'image/svg+xml'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideRequestsWithSize')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideRequestsWithSize
|
|
||||||
*/
|
|
||||||
public function imageIsReturnedWithExpectedSize(
|
public function imageIsReturnedWithExpectedSize(
|
||||||
QrCodeOptions $defaultOptions,
|
QrCodeOptions $defaultOptions,
|
||||||
ServerRequestInterface $req,
|
ServerRequestInterface $req,
|
||||||
@@ -122,7 +118,7 @@ class QrCodeActionTest extends TestCase
|
|||||||
self::assertEquals($expectedSize, $size);
|
self::assertEquals($expectedSize, $size);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideRequestsWithSize(): iterable
|
public static function provideRequestsWithSize(): iterable
|
||||||
{
|
{
|
||||||
yield 'different margin and size defaults' => [
|
yield 'different margin and size defaults' => [
|
||||||
new QrCodeOptions(size: 660, margin: 40),
|
new QrCodeOptions(size: 660, margin: 40),
|
||||||
@@ -188,10 +184,7 @@ class QrCodeActionTest extends TestCase
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideRoundBlockSize')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideRoundBlockSize
|
|
||||||
*/
|
|
||||||
public function imageCanRemoveExtraMarginWhenBlockRoundIsDisabled(
|
public function imageCanRemoveExtraMarginWhenBlockRoundIsDisabled(
|
||||||
QrCodeOptions $defaultOptions,
|
QrCodeOptions $defaultOptions,
|
||||||
?string $roundBlockSize,
|
?string $roundBlockSize,
|
||||||
@@ -215,7 +208,7 @@ class QrCodeActionTest extends TestCase
|
|||||||
self::assertEquals($color, $expectedColor);
|
self::assertEquals($color, $expectedColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideRoundBlockSize(): iterable
|
public static function provideRoundBlockSize(): iterable
|
||||||
{
|
{
|
||||||
yield 'no round block param' => [new QrCodeOptions(), null, self::WHITE];
|
yield 'no round block param' => [new QrCodeOptions(), null, self::WHITE];
|
||||||
yield 'no round block param, but disabled by default' => [
|
yield 'no round block param, but disabled by default' => [
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\Core\Action;
|
|||||||
|
|
||||||
use Laminas\Diactoros\Response;
|
use Laminas\Diactoros\Response;
|
||||||
use Laminas\Diactoros\ServerRequest;
|
use Laminas\Diactoros\ServerRequest;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
@@ -44,7 +45,7 @@ class RedirectActionTest extends TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function redirectionIsPerformedToLongUrl(): void
|
public function redirectionIsPerformedToLongUrl(): void
|
||||||
{
|
{
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
@@ -64,7 +65,7 @@ class RedirectActionTest extends TestCase
|
|||||||
self::assertSame($expectedResp, $response);
|
self::assertSame($expectedResp, $response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function nextMiddlewareIsInvokedIfLongUrlIsNotFound(): void
|
public function nextMiddlewareIsInvokedIfLongUrlIsNotFound(): void
|
||||||
{
|
{
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioTest\Shlink\Core\Action;
|
namespace ShlinkioTest\Shlink\Core\Action;
|
||||||
|
|
||||||
use Laminas\Diactoros\ServerRequestFactory;
|
use Laminas\Diactoros\ServerRequestFactory;
|
||||||
|
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\Core\Action\RobotsAction;
|
use Shlinkio\Shlink\Core\Action\RobotsAction;
|
||||||
@@ -21,10 +23,7 @@ class RobotsActionTest extends TestCase
|
|||||||
$this->action = new RobotsAction($this->helper);
|
$this->action = new RobotsAction($this->helper);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideShortCodes')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideShortCodes
|
|
||||||
*/
|
|
||||||
public function buildsRobotsLinesFromCrawlableShortCodes(array $shortCodes, string $expected): void
|
public function buildsRobotsLinesFromCrawlableShortCodes(array $shortCodes, string $expected): void
|
||||||
{
|
{
|
||||||
$this->helper
|
$this->helper
|
||||||
@@ -39,7 +38,7 @@ class RobotsActionTest extends TestCase
|
|||||||
self::assertEquals('text/plain', $response->getHeaderLine('Content-Type'));
|
self::assertEquals('text/plain', $response->getHeaderLine('Content-Type'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideShortCodes(): iterable
|
public static function provideShortCodes(): iterable
|
||||||
{
|
{
|
||||||
yield 'three short codes' => [['foo', 'bar', 'baz'], <<<ROBOTS
|
yield 'three short codes' => [['foo', 'bar', 'baz'], <<<ROBOTS
|
||||||
# For more information about the robots.txt standard, see:
|
# For more information about the robots.txt standard, see:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink\Core\Config;
|
namespace ShlinkioTest\Shlink\Core\Config;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\Core\Config\EmptyNotFoundRedirectConfig;
|
use Shlinkio\Shlink\Core\Config\EmptyNotFoundRedirectConfig;
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ class EmptyNotFoundRedirectConfigTest extends TestCase
|
|||||||
$this->redirectsConfig = new EmptyNotFoundRedirectConfig();
|
$this->redirectsConfig = new EmptyNotFoundRedirectConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function allMethodsReturnHardcodedValues(): void
|
public function allMethodsReturnHardcodedValues(): void
|
||||||
{
|
{
|
||||||
self::assertNull($this->redirectsConfig->invalidShortUrlRedirect());
|
self::assertNull($this->redirectsConfig->invalidShortUrlRedirect());
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink\Core\Config;
|
namespace ShlinkioTest\Shlink\Core\Config;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||||
|
|
||||||
@@ -23,16 +25,13 @@ class EnvVarsTest extends TestCase
|
|||||||
putenv(EnvVars::DB_NAME->value . '=');
|
putenv(EnvVars::DB_NAME->value . '=');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideExistingEnvVars')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideExistingEnvVars
|
|
||||||
*/
|
|
||||||
public function existsInEnvReturnsExpectedValue(EnvVars $envVar, bool $exists): void
|
public function existsInEnvReturnsExpectedValue(EnvVars $envVar, bool $exists): void
|
||||||
{
|
{
|
||||||
self::assertEquals($exists, $envVar->existsInEnv());
|
self::assertEquals($exists, $envVar->existsInEnv());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideExistingEnvVars(): iterable
|
public static function provideExistingEnvVars(): iterable
|
||||||
{
|
{
|
||||||
yield 'DB_NAME' => [EnvVars::DB_NAME, true];
|
yield 'DB_NAME' => [EnvVars::DB_NAME, true];
|
||||||
yield 'BASE_PATH' => [EnvVars::BASE_PATH, true];
|
yield 'BASE_PATH' => [EnvVars::BASE_PATH, true];
|
||||||
@@ -40,16 +39,13 @@ class EnvVarsTest extends TestCase
|
|||||||
yield 'DEFAULT_REGULAR_404_REDIRECT' => [EnvVars::DEFAULT_REGULAR_404_REDIRECT, false];
|
yield 'DEFAULT_REGULAR_404_REDIRECT' => [EnvVars::DEFAULT_REGULAR_404_REDIRECT, false];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideEnvVarsValues')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideEnvVarsValues
|
|
||||||
*/
|
|
||||||
public function expectedValueIsLoadedFromEnv(EnvVars $envVar, mixed $expected, mixed $default): void
|
public function expectedValueIsLoadedFromEnv(EnvVars $envVar, mixed $expected, mixed $default): void
|
||||||
{
|
{
|
||||||
self::assertEquals($expected, $envVar->loadFromEnv($default));
|
self::assertEquals($expected, $envVar->loadFromEnv($default));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideEnvVarsValues(): iterable
|
public static function provideEnvVarsValues(): iterable
|
||||||
{
|
{
|
||||||
yield 'DB_NAME without default' => [EnvVars::DB_NAME, 'shlink', null];
|
yield 'DB_NAME without default' => [EnvVars::DB_NAME, 'shlink', null];
|
||||||
yield 'DB_NAME with default' => [EnvVars::DB_NAME, 'shlink', 'foobar'];
|
yield 'DB_NAME with default' => [EnvVars::DB_NAME, 'shlink', 'foobar'];
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ use Laminas\Diactoros\ServerRequestFactory;
|
|||||||
use Laminas\Diactoros\Uri;
|
use Laminas\Diactoros\Uri;
|
||||||
use Mezzio\Router\Route;
|
use Mezzio\Router\Route;
|
||||||
use Mezzio\Router\RouteResult;
|
use Mezzio\Router\RouteResult;
|
||||||
|
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 Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Message\UriInterface;
|
use Psr\Http\Message\UriInterface;
|
||||||
use Psr\Http\Server\MiddlewareInterface;
|
|
||||||
use Psr\Log\NullLogger;
|
use Psr\Log\NullLogger;
|
||||||
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
||||||
use Shlinkio\Shlink\Core\Config\NotFoundRedirectResolver;
|
use Shlinkio\Shlink\Core\Config\NotFoundRedirectResolver;
|
||||||
@@ -21,6 +22,8 @@ use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType;
|
|||||||
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||||
use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface;
|
use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface;
|
||||||
|
|
||||||
|
use function Laminas\Stratigility\middleware;
|
||||||
|
|
||||||
class NotFoundRedirectResolverTest extends TestCase
|
class NotFoundRedirectResolverTest extends TestCase
|
||||||
{
|
{
|
||||||
private NotFoundRedirectResolver $resolver;
|
private NotFoundRedirectResolver $resolver;
|
||||||
@@ -32,10 +35,7 @@ class NotFoundRedirectResolverTest extends TestCase
|
|||||||
$this->resolver = new NotFoundRedirectResolver($this->helper, new NullLogger());
|
$this->resolver = new NotFoundRedirectResolver($this->helper, new NullLogger());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideRedirects')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideRedirects
|
|
||||||
*/
|
|
||||||
public function expectedRedirectionIsReturnedDependingOnTheCase(
|
public function expectedRedirectionIsReturnedDependingOnTheCase(
|
||||||
UriInterface $uri,
|
UriInterface $uri,
|
||||||
NotFoundType $notFoundType,
|
NotFoundType $notFoundType,
|
||||||
@@ -52,47 +52,47 @@ class NotFoundRedirectResolverTest extends TestCase
|
|||||||
self::assertSame($expectedResp, $resp);
|
self::assertSame($expectedResp, $resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideRedirects(): iterable
|
public static function provideRedirects(): iterable
|
||||||
{
|
{
|
||||||
yield 'base URL with trailing slash' => [
|
yield 'base URL with trailing slash' => [
|
||||||
$uri = new Uri('/'),
|
$uri = new Uri('/'),
|
||||||
$this->notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
|
self::notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
|
||||||
new NotFoundRedirectOptions(baseUrl: 'baseUrl'),
|
new NotFoundRedirectOptions(baseUrl: 'baseUrl'),
|
||||||
'baseUrl',
|
'baseUrl',
|
||||||
];
|
];
|
||||||
yield 'base URL with domain placeholder' => [
|
yield 'base URL with domain placeholder' => [
|
||||||
$uri = new Uri('https://s.test'),
|
$uri = new Uri('https://s.test'),
|
||||||
$this->notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
|
self::notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
|
||||||
new NotFoundRedirectOptions(baseUrl: 'https://redirect-here.com/{DOMAIN}'),
|
new NotFoundRedirectOptions(baseUrl: 'https://redirect-here.com/{DOMAIN}'),
|
||||||
'https://redirect-here.com/s.test',
|
'https://redirect-here.com/s.test',
|
||||||
];
|
];
|
||||||
yield 'base URL with domain placeholder in query' => [
|
yield 'base URL with domain placeholder in query' => [
|
||||||
$uri = new Uri('https://s.test'),
|
$uri = new Uri('https://s.test'),
|
||||||
$this->notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
|
self::notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
|
||||||
new NotFoundRedirectOptions(baseUrl: 'https://redirect-here.com/?domain={DOMAIN}'),
|
new NotFoundRedirectOptions(baseUrl: 'https://redirect-here.com/?domain={DOMAIN}'),
|
||||||
'https://redirect-here.com/?domain=s.test',
|
'https://redirect-here.com/?domain=s.test',
|
||||||
];
|
];
|
||||||
yield 'base URL without trailing slash' => [
|
yield 'base URL without trailing slash' => [
|
||||||
$uri = new Uri(''),
|
$uri = new Uri(''),
|
||||||
$this->notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
|
self::notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
|
||||||
new NotFoundRedirectOptions(baseUrl: 'baseUrl'),
|
new NotFoundRedirectOptions(baseUrl: 'baseUrl'),
|
||||||
'baseUrl',
|
'baseUrl',
|
||||||
];
|
];
|
||||||
yield 'regular 404' => [
|
yield 'regular 404' => [
|
||||||
$uri = new Uri('/foo/bar'),
|
$uri = new Uri('/foo/bar'),
|
||||||
$this->notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
|
self::notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
|
||||||
new NotFoundRedirectOptions(regular404: 'regular404'),
|
new NotFoundRedirectOptions(regular404: 'regular404'),
|
||||||
'regular404',
|
'regular404',
|
||||||
];
|
];
|
||||||
yield 'regular 404 with path placeholder in query' => [
|
yield 'regular 404 with path placeholder in query' => [
|
||||||
$uri = new Uri('/foo/bar'),
|
$uri = new Uri('/foo/bar'),
|
||||||
$this->notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
|
self::notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
|
||||||
new NotFoundRedirectOptions(regular404: 'https://redirect-here.com/?path={ORIGINAL_PATH}'),
|
new NotFoundRedirectOptions(regular404: 'https://redirect-here.com/?path={ORIGINAL_PATH}'),
|
||||||
'https://redirect-here.com/?path=%2Ffoo%2Fbar',
|
'https://redirect-here.com/?path=%2Ffoo%2Fbar',
|
||||||
];
|
];
|
||||||
yield 'regular 404 with multiple placeholders' => [
|
yield 'regular 404 with multiple placeholders' => [
|
||||||
$uri = new Uri('https://s.test/foo/bar'),
|
$uri = new Uri('https://s.test/foo/bar'),
|
||||||
$this->notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
|
self::notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
|
||||||
new NotFoundRedirectOptions(
|
new NotFoundRedirectOptions(
|
||||||
regular404: 'https://redirect-here.com/{ORIGINAL_PATH}/{DOMAIN}/?d={DOMAIN}&p={ORIGINAL_PATH}',
|
regular404: 'https://redirect-here.com/{ORIGINAL_PATH}/{DOMAIN}/?d={DOMAIN}&p={ORIGINAL_PATH}',
|
||||||
),
|
),
|
||||||
@@ -100,22 +100,22 @@ class NotFoundRedirectResolverTest extends TestCase
|
|||||||
];
|
];
|
||||||
yield 'invalid short URL' => [
|
yield 'invalid short URL' => [
|
||||||
new Uri('/foo'),
|
new Uri('/foo'),
|
||||||
$this->notFoundType($this->requestForRoute(RedirectAction::class)),
|
self::notFoundType(self::requestForRoute(RedirectAction::class)),
|
||||||
new NotFoundRedirectOptions(invalidShortUrl: 'invalidShortUrl'),
|
new NotFoundRedirectOptions(invalidShortUrl: 'invalidShortUrl'),
|
||||||
'invalidShortUrl',
|
'invalidShortUrl',
|
||||||
];
|
];
|
||||||
yield 'invalid short URL with path placeholder' => [
|
yield 'invalid short URL with path placeholder' => [
|
||||||
new Uri('/foo'),
|
new Uri('/foo'),
|
||||||
$this->notFoundType($this->requestForRoute(RedirectAction::class)),
|
self::notFoundType(self::requestForRoute(RedirectAction::class)),
|
||||||
new NotFoundRedirectOptions(invalidShortUrl: 'https://redirect-here.com/{ORIGINAL_PATH}'),
|
new NotFoundRedirectOptions(invalidShortUrl: 'https://redirect-here.com/{ORIGINAL_PATH}'),
|
||||||
'https://redirect-here.com/foo',
|
'https://redirect-here.com/foo',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function noResponseIsReturnedIfNoConditionsMatch(): void
|
public function noResponseIsReturnedIfNoConditionsMatch(): void
|
||||||
{
|
{
|
||||||
$notFoundType = $this->notFoundType($this->requestForRoute('foo'));
|
$notFoundType = self::notFoundType(self::requestForRoute('foo'));
|
||||||
$this->helper->expects($this->never())->method('buildRedirectResponse');
|
$this->helper->expects($this->never())->method('buildRedirectResponse');
|
||||||
|
|
||||||
$result = $this->resolver->resolveRedirectResponse($notFoundType, new NotFoundRedirectOptions(), new Uri());
|
$result = $this->resolver->resolveRedirectResponse($notFoundType, new NotFoundRedirectOptions(), new Uri());
|
||||||
@@ -123,12 +123,12 @@ class NotFoundRedirectResolverTest extends TestCase
|
|||||||
self::assertNull($result);
|
self::assertNull($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function notFoundType(ServerRequestInterface $req): NotFoundType
|
private static function notFoundType(ServerRequestInterface $req): NotFoundType
|
||||||
{
|
{
|
||||||
return NotFoundType::fromRequest($req, '');
|
return NotFoundType::fromRequest($req, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function requestForRoute(string $routeName): ServerRequestInterface
|
private static function requestForRoute(string $routeName): ServerRequestInterface
|
||||||
{
|
{
|
||||||
return ServerRequestFactory::fromGlobals()
|
return ServerRequestFactory::fromGlobals()
|
||||||
->withAttribute(
|
->withAttribute(
|
||||||
@@ -136,7 +136,8 @@ class NotFoundRedirectResolverTest extends TestCase
|
|||||||
RouteResult::fromRoute(
|
RouteResult::fromRoute(
|
||||||
new Route(
|
new Route(
|
||||||
'foo',
|
'foo',
|
||||||
$this->createMock(MiddlewareInterface::class),
|
middleware(function (): void {
|
||||||
|
}),
|
||||||
['GET'],
|
['GET'],
|
||||||
$routeName,
|
$routeName,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink\Core\Config\PostProcessor;
|
namespace ShlinkioTest\Shlink\Core\Config\PostProcessor;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\Core\Config\PostProcessor\BasePathPrefixer;
|
use Shlinkio\Shlink\Core\Config\PostProcessor\BasePathPrefixer;
|
||||||
|
|
||||||
@@ -16,10 +18,7 @@ class BasePathPrefixerTest extends TestCase
|
|||||||
$this->prefixer = new BasePathPrefixer();
|
$this->prefixer = new BasePathPrefixer();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideConfig')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideConfig
|
|
||||||
*/
|
|
||||||
public function parsesConfigAsExpected(
|
public function parsesConfigAsExpected(
|
||||||
array $originalConfig,
|
array $originalConfig,
|
||||||
array $expectedRoutes,
|
array $expectedRoutes,
|
||||||
@@ -31,7 +30,7 @@ class BasePathPrefixerTest extends TestCase
|
|||||||
self::assertEquals($expectedMiddlewares, $middlewares);
|
self::assertEquals($expectedMiddlewares, $middlewares);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideConfig(): iterable
|
public static function provideConfig(): iterable
|
||||||
{
|
{
|
||||||
yield 'with empty options' => [['routes' => []], [], []];
|
yield 'with empty options' => [['routes' => []], [], []];
|
||||||
yield 'with non-empty options' => [
|
yield 'with non-empty options' => [
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink\Core\Config\PostProcessor;
|
namespace ShlinkioTest\Shlink\Core\Config\PostProcessor;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\Core\Config\PostProcessor\MultiSegmentSlugProcessor;
|
use Shlinkio\Shlink\Core\Config\PostProcessor\MultiSegmentSlugProcessor;
|
||||||
|
|
||||||
@@ -16,16 +18,13 @@ class MultiSegmentSlugProcessorTest extends TestCase
|
|||||||
$this->processor = new MultiSegmentSlugProcessor();
|
$this->processor = new MultiSegmentSlugProcessor();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideConfigs')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideConfigs
|
|
||||||
*/
|
|
||||||
public function parsesRoutesAsExpected(array $config, array $expectedRoutes): void
|
public function parsesRoutesAsExpected(array $config, array $expectedRoutes): void
|
||||||
{
|
{
|
||||||
self::assertEquals($expectedRoutes, ($this->processor)($config)['routes'] ?? []);
|
self::assertEquals($expectedRoutes, ($this->processor)($config)['routes'] ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideConfigs(): iterable
|
public static function provideConfigs(): iterable
|
||||||
{
|
{
|
||||||
yield [[], []];
|
yield [[], []];
|
||||||
yield [['url_shortener' => []], []];
|
yield [['url_shortener' => []], []];
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioTest\Shlink\Core\Config\PostProcessor;
|
namespace ShlinkioTest\Shlink\Core\Config\PostProcessor;
|
||||||
|
|
||||||
use Mezzio\Router\Route;
|
use Mezzio\Router\Route;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
||||||
use Shlinkio\Shlink\Core\Config\PostProcessor\ShortUrlMethodsProcessor;
|
use Shlinkio\Shlink\Core\Config\PostProcessor\ShortUrlMethodsProcessor;
|
||||||
@@ -18,10 +20,7 @@ class ShortUrlMethodsProcessorTest extends TestCase
|
|||||||
$this->processor = new ShortUrlMethodsProcessor();
|
$this->processor = new ShortUrlMethodsProcessor();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideConfigs')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideConfigs
|
|
||||||
*/
|
|
||||||
public function onlyFirstRouteIdentifiedAsRedirectIsEditedWithProperAllowedMethods(
|
public function onlyFirstRouteIdentifiedAsRedirectIsEditedWithProperAllowedMethods(
|
||||||
array $config,
|
array $config,
|
||||||
?array $expectedRoutes,
|
?array $expectedRoutes,
|
||||||
@@ -29,7 +28,7 @@ class ShortUrlMethodsProcessorTest extends TestCase
|
|||||||
self::assertEquals($expectedRoutes, ($this->processor)($config)['routes'] ?? null);
|
self::assertEquals($expectedRoutes, ($this->processor)($config)['routes'] ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideConfigs(): iterable
|
public static function provideConfigs(): iterable
|
||||||
{
|
{
|
||||||
$buildConfigWithStatus = static fn (int $status, ?array $expectedAllowedMethods) => [[
|
$buildConfigWithStatus = static fn (int $status, ?array $expectedAllowedMethods) => [[
|
||||||
'routes' => [
|
'routes' => [
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioTest\Shlink\Core;
|
namespace ShlinkioTest\Shlink\Core;
|
||||||
|
|
||||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Shlinkio\Shlink\Core\ConfigProvider;
|
use Shlinkio\Shlink\Core\ConfigProvider;
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ class ConfigProviderTest extends TestCase
|
|||||||
$this->configProvider = new ConfigProvider();
|
$this->configProvider = new ConfigProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function properConfigIsReturned(): void
|
public function properConfigIsReturned(): void
|
||||||
{
|
{
|
||||||
$config = ($this->configProvider)();
|
$config = ($this->configProvider)();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink\Core\Crawling;
|
namespace ShlinkioTest\Shlink\Core\Crawling;
|
||||||
|
|
||||||
|
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\Core\Crawling\CrawlingHelper;
|
use Shlinkio\Shlink\Core\Crawling\CrawlingHelper;
|
||||||
@@ -20,7 +21,7 @@ class CrawlingHelperTest extends TestCase
|
|||||||
$this->helper = new CrawlingHelper($this->query);
|
$this->helper = new CrawlingHelper($this->query);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function listCrawlableShortCodesDelegatesIntoRepository(): void
|
public function listCrawlableShortCodesDelegatesIntoRepository(): void
|
||||||
{
|
{
|
||||||
$this->query->expects($this->once())->method('__invoke')->willReturn([]);
|
$this->query->expects($this->once())->method('__invoke')->willReturn([]);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
|||||||
namespace ShlinkioTest\Shlink\Core\Domain;
|
namespace ShlinkioTest\Shlink\Core\Domain;
|
||||||
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
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\Core\Config\EmptyNotFoundRedirectConfig;
|
use Shlinkio\Shlink\Core\Config\EmptyNotFoundRedirectConfig;
|
||||||
@@ -29,10 +31,7 @@ class DomainServiceTest extends TestCase
|
|||||||
$this->domainService = new DomainService($this->em, 'default.com');
|
$this->domainService = new DomainService($this->em, 'default.com');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideExcludedDomains')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideExcludedDomains
|
|
||||||
*/
|
|
||||||
public function listDomainsDelegatesIntoRepository(array $domains, array $expectedResult, ?ApiKey $apiKey): void
|
public function listDomainsDelegatesIntoRepository(array $domains, array $expectedResult, ?ApiKey $apiKey): void
|
||||||
{
|
{
|
||||||
$repo = $this->createMock(DomainRepositoryInterface::class);
|
$repo = $this->createMock(DomainRepositoryInterface::class);
|
||||||
@@ -44,7 +43,7 @@ class DomainServiceTest extends TestCase
|
|||||||
self::assertEquals($expectedResult, $result);
|
self::assertEquals($expectedResult, $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideExcludedDomains(): iterable
|
public static function provideExcludedDomains(): iterable
|
||||||
{
|
{
|
||||||
$default = DomainItem::forDefaultDomain('default.com', new EmptyNotFoundRedirectConfig());
|
$default = DomainItem::forDefaultDomain('default.com', new EmptyNotFoundRedirectConfig());
|
||||||
$adminApiKey = ApiKey::create();
|
$adminApiKey = ApiKey::create();
|
||||||
@@ -102,7 +101,7 @@ class DomainServiceTest extends TestCase
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function getDomainThrowsExceptionWhenDomainIsNotFound(): void
|
public function getDomainThrowsExceptionWhenDomainIsNotFound(): void
|
||||||
{
|
{
|
||||||
$this->em->expects($this->once())->method('find')->with(Domain::class, '123')->willReturn(null);
|
$this->em->expects($this->once())->method('find')->with(Domain::class, '123')->willReturn(null);
|
||||||
@@ -112,7 +111,7 @@ class DomainServiceTest extends TestCase
|
|||||||
$this->domainService->getDomain('123');
|
$this->domainService->getDomain('123');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function getDomainReturnsEntityWhenFound(): void
|
public function getDomainReturnsEntityWhenFound(): void
|
||||||
{
|
{
|
||||||
$domain = Domain::withAuthority('');
|
$domain = Domain::withAuthority('');
|
||||||
@@ -123,10 +122,7 @@ class DomainServiceTest extends TestCase
|
|||||||
self::assertSame($domain, $result);
|
self::assertSame($domain, $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideFoundDomains')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideFoundDomains
|
|
||||||
*/
|
|
||||||
public function getOrCreateAlwaysPersistsDomain(?Domain $foundDomain, ?ApiKey $apiKey): void
|
public function getOrCreateAlwaysPersistsDomain(?Domain $foundDomain, ?ApiKey $apiKey): void
|
||||||
{
|
{
|
||||||
$authority = 'example.com';
|
$authority = 'example.com';
|
||||||
@@ -145,7 +141,7 @@ class DomainServiceTest extends TestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function getOrCreateThrowsExceptionForApiKeysWithDomainRole(): void
|
public function getOrCreateThrowsExceptionForApiKeysWithDomainRole(): void
|
||||||
{
|
{
|
||||||
$authority = 'example.com';
|
$authority = 'example.com';
|
||||||
@@ -163,10 +159,7 @@ class DomainServiceTest extends TestCase
|
|||||||
$this->domainService->getOrCreate($authority, $apiKey);
|
$this->domainService->getOrCreate($authority, $apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideFoundDomains')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideFoundDomains
|
|
||||||
*/
|
|
||||||
public function configureNotFoundRedirectsConfiguresFetchedDomain(?Domain $foundDomain, ?ApiKey $apiKey): void
|
public function configureNotFoundRedirectsConfiguresFetchedDomain(?Domain $foundDomain, ?ApiKey $apiKey): void
|
||||||
{
|
{
|
||||||
$authority = 'example.com';
|
$authority = 'example.com';
|
||||||
@@ -190,7 +183,7 @@ class DomainServiceTest extends TestCase
|
|||||||
self::assertEquals('baz.com', $result->invalidShortUrlRedirect());
|
self::assertEquals('baz.com', $result->invalidShortUrlRedirect());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideFoundDomains(): iterable
|
public static function provideFoundDomains(): iterable
|
||||||
{
|
{
|
||||||
$domain = Domain::withAuthority('');
|
$domain = Domain::withAuthority('');
|
||||||
$adminApiKey = ApiKey::create();
|
$adminApiKey = ApiKey::create();
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ namespace ShlinkioTest\Shlink\Core\ErrorHandler;
|
|||||||
|
|
||||||
use Laminas\Diactoros\Response;
|
use Laminas\Diactoros\Response;
|
||||||
use Laminas\Diactoros\ServerRequestFactory;
|
use Laminas\Diactoros\ServerRequestFactory;
|
||||||
|
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 Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
@@ -42,10 +45,7 @@ class NotFoundRedirectHandlerTest extends TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideNonRedirectScenarios')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideNonRedirectScenarios
|
|
||||||
*/
|
|
||||||
public function nextIsCalledWhenNoRedirectIsResolved(callable $setUp): void
|
public function nextIsCalledWhenNoRedirectIsResolved(callable $setUp): void
|
||||||
{
|
{
|
||||||
$expectedResp = new Response();
|
$expectedResp = new Response();
|
||||||
@@ -58,44 +58,43 @@ class NotFoundRedirectHandlerTest extends TestCase
|
|||||||
self::assertSame($expectedResp, $result);
|
self::assertSame($expectedResp, $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideNonRedirectScenarios(): iterable
|
public static function provideNonRedirectScenarios(): iterable
|
||||||
{
|
{
|
||||||
yield 'no domain' => [function (
|
yield 'no domain' => [function (
|
||||||
MockObject&DomainServiceInterface $domainService,
|
MockObject&DomainServiceInterface $domainService,
|
||||||
MockObject&NotFoundRedirectResolverInterface $resolver,
|
MockObject&NotFoundRedirectResolverInterface $resolver,
|
||||||
): void {
|
): void {
|
||||||
$domainService->expects($this->once())->method('findByAuthority')->withAnyParameters()->willReturn(
|
$domainService->expects(self::once())->method('findByAuthority')->withAnyParameters()->willReturn(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
$resolver->expects($this->once())->method('resolveRedirectResponse')->with(
|
$resolver->expects(self::once())->method('resolveRedirectResponse')->with(
|
||||||
$this->isInstanceOf(NotFoundType::class),
|
self::isInstanceOf(NotFoundType::class),
|
||||||
$this->isInstanceOf(NotFoundRedirectOptions::class),
|
self::isInstanceOf(NotFoundRedirectOptions::class),
|
||||||
$this->isInstanceOf(UriInterface::class),
|
self::isInstanceOf(UriInterface::class),
|
||||||
)->willReturn(null);
|
)->willReturn(null);
|
||||||
}];
|
}];
|
||||||
yield 'non-redirecting domain' => [function (
|
yield 'non-redirecting domain' => [function (
|
||||||
MockObject&DomainServiceInterface $domainService,
|
MockObject&DomainServiceInterface $domainService,
|
||||||
MockObject&NotFoundRedirectResolverInterface $resolver,
|
MockObject&NotFoundRedirectResolverInterface $resolver,
|
||||||
): void {
|
): void {
|
||||||
$domainService->expects($this->once())->method('findByAuthority')->withAnyParameters()->willReturn(
|
$domainService->expects(self::once())->method('findByAuthority')->withAnyParameters()->willReturn(
|
||||||
Domain::withAuthority(''),
|
Domain::withAuthority(''),
|
||||||
);
|
);
|
||||||
$resolver->expects($this->exactly(2))->method('resolveRedirectResponse')->withConsecutive(
|
$callCount = 0;
|
||||||
[
|
$resolver->expects(self::exactly(2))->method('resolveRedirectResponse')->willReturnCallback(
|
||||||
$this->isInstanceOf(NotFoundType::class),
|
function (mixed $arg1, mixed $arg2, mixed $arg3) use (&$callCount) {
|
||||||
$this->isInstanceOf(Domain::class),
|
Assert::assertInstanceOf(NotFoundType::class, $arg1);
|
||||||
$this->isInstanceOf(UriInterface::class),
|
Assert::assertInstanceOf($callCount === 0 ? Domain::class : NotFoundRedirectOptions::class, $arg2);
|
||||||
],
|
Assert::assertInstanceOf(UriInterface::class, $arg3);
|
||||||
[
|
|
||||||
$this->isInstanceOf(NotFoundType::class),
|
$callCount++;
|
||||||
$this->isInstanceOf(NotFoundRedirectOptions::class),
|
return null;
|
||||||
$this->isInstanceOf(UriInterface::class),
|
},
|
||||||
],
|
);
|
||||||
)->willReturn(null);
|
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function globalRedirectIsUsedIfDomainRedirectIsNotFound(): void
|
public function globalRedirectIsUsedIfDomainRedirectIsNotFound(): void
|
||||||
{
|
{
|
||||||
$expectedResp = new Response();
|
$expectedResp = new Response();
|
||||||
@@ -113,7 +112,7 @@ class NotFoundRedirectHandlerTest extends TestCase
|
|||||||
self::assertSame($expectedResp, $result);
|
self::assertSame($expectedResp, $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
#[Test]
|
||||||
public function domainRedirectIsUsedIfFound(): void
|
public function domainRedirectIsUsedIfFound(): void
|
||||||
{
|
{
|
||||||
$expectedResp = new Response();
|
$expectedResp = new Response();
|
||||||
|
|||||||
@@ -9,13 +9,16 @@ use Laminas\Diactoros\ServerRequestFactory;
|
|||||||
use Laminas\Diactoros\Uri;
|
use Laminas\Diactoros\Uri;
|
||||||
use Mezzio\Router\Route;
|
use Mezzio\Router\Route;
|
||||||
use Mezzio\Router\RouteResult;
|
use Mezzio\Router\RouteResult;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Server\MiddlewareInterface;
|
|
||||||
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
||||||
use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType;
|
use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType;
|
||||||
use Shlinkio\Shlink\Core\ErrorHandler\NotFoundTemplateHandler;
|
use Shlinkio\Shlink\Core\ErrorHandler\NotFoundTemplateHandler;
|
||||||
|
|
||||||
|
use function Laminas\Stratigility\middleware;
|
||||||
|
|
||||||
class NotFoundTemplateHandlerTest extends TestCase
|
class NotFoundTemplateHandlerTest extends TestCase
|
||||||
{
|
{
|
||||||
private NotFoundTemplateHandler $handler;
|
private NotFoundTemplateHandler $handler;
|
||||||
@@ -31,10 +34,7 @@ class NotFoundTemplateHandlerTest extends TestCase
|
|||||||
$this->handler = new NotFoundTemplateHandler($readFile);
|
$this->handler = new NotFoundTemplateHandler($readFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
#[Test, DataProvider('provideTemplates')]
|
||||||
* @test
|
|
||||||
* @dataProvider provideTemplates
|
|
||||||
*/
|
|
||||||
public function properErrorTemplateIsRendered(ServerRequestInterface $request, string $expectedTemplate): void
|
public function properErrorTemplateIsRendered(ServerRequestInterface $request, string $expectedTemplate): void
|
||||||
{
|
{
|
||||||
$resp = $this->handler->handle($request->withHeader('Accept', 'text/html'));
|
$resp = $this->handler->handle($request->withHeader('Accept', 'text/html'));
|
||||||
@@ -44,19 +44,20 @@ class NotFoundTemplateHandlerTest extends TestCase
|
|||||||
self::assertTrue($this->readFileCalled);
|
self::assertTrue($this->readFileCalled);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideTemplates(): iterable
|
public static function provideTemplates(): iterable
|
||||||
{
|
{
|
||||||
$request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/foo'));
|
$request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/foo'));
|
||||||
|
|
||||||
yield 'base url' => [$this->withNotFoundType($request, '/foo'), NotFoundTemplateHandler::NOT_FOUND_TEMPLATE];
|
yield 'base url' => [self::withNotFoundType($request, '/foo'), NotFoundTemplateHandler::NOT_FOUND_TEMPLATE];
|
||||||
yield 'regular not found' => [$this->withNotFoundType($request), NotFoundTemplateHandler::NOT_FOUND_TEMPLATE];
|
yield 'regular not found' => [self::withNotFoundType($request), NotFoundTemplateHandler::NOT_FOUND_TEMPLATE];
|
||||||
yield 'invalid short code' => [
|
yield 'invalid short code' => [
|
||||||
$this->withNotFoundType($request->withAttribute(
|
self::withNotFoundType($request->withAttribute(
|
||||||
RouteResult::class,
|
RouteResult::class,
|
||||||
RouteResult::fromRoute(
|
RouteResult::fromRoute(
|
||||||
new Route(
|
new Route(
|
||||||
'foo',
|
'foo',
|
||||||
$this->createMock(MiddlewareInterface::class),
|
middleware(function (): void {
|
||||||
|
}),
|
||||||
['GET'],
|
['GET'],
|
||||||
RedirectAction::class,
|
RedirectAction::class,
|
||||||
),
|
),
|
||||||
@@ -66,7 +67,7 @@ class NotFoundTemplateHandlerTest extends TestCase
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function withNotFoundType(ServerRequestInterface $req, string $baseUrl = ''): ServerRequestInterface
|
private static function withNotFoundType(ServerRequestInterface $req, string $baseUrl = ''): ServerRequestInterface
|
||||||
{
|
{
|
||||||
$type = NotFoundType::fromRequest($req, $baseUrl);
|
$type = NotFoundType::fromRequest($req, $baseUrl);
|
||||||
return $req->withAttribute(NotFoundType::class, $type);
|
return $req->withAttribute(NotFoundType::class, $type);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user