Compare commits

...

37 Commits

Author SHA1 Message Date
Alejandro Celaya
0e9ea5027c Merge pull request #1705 from shlinkio/develop
Release 3.5.2
2023-02-16 19:36:59 +01:00
Alejandro Celaya
658303d375 Merge pull request #1706 from acelaya-forks/feature/fix-ms-ci
Comment-out unixodbc-dev installation in CI, as it's already present …
2023-02-16 19:33:08 +01:00
Alejandro Celaya
ccc3a4b584 Comment-out unixodbc-dev installation in CI, as it's already present in Ubuntu 22.04 2023-02-16 19:24:09 +01:00
Alejandro Celaya
ef5ac86e0a Add v3.5.2 to changelog 2023-02-15 20:25:55 +01:00
Alejandro Celaya
91b90b276a Merge pull request #1704 from acelaya-forks/feature/stronger-db-detection
Feature/stronger db detection
2023-02-15 19:19:11 +01:00
Alejandro Celaya
85c32c3c9a Fix CreateDatabaseCommandTest 2023-02-15 18:55:25 +01:00
Alejandro Celaya
40838255a7 Make sure database detection is not affected by the existence of foreign tables 2023-02-15 08:52:17 +01:00
Alejandro Celaya
a67ccb384f Merge pull request #1697 from acelaya-forks/feature/phpunit-10
Feature/phpunit 10
2023-02-13 19:15:33 +01:00
Alejandro Celaya
cb31e5a581 Update to phpcov 9 2023-02-13 19:05:27 +01:00
Alejandro Celaya
3c12a55872 Merge branch 'develop' into feature/phpunit-10 2023-02-13 11:54:49 +01:00
Alejandro Celaya
6da8b11674 Update changelog 2023-02-12 19:52:22 +01:00
Alejandro Celaya
552489611f Merge pull request #1700 from acelaya-forks/feature/optimize-tags-query
Feature/optimize tags query
2023-02-12 19:50:23 +01:00
Alejandro Celaya
e48d0f4f0c Upgrade deps for MSSQL tests 2023-02-12 19:08:20 +01:00
Alejandro Celaya
49b6063501 Fix ordering on Postgres 2023-02-12 13:35:05 +01:00
Alejandro Celaya
dd049feb40 Add migration with new index for short_url_id+potential_bot on visits table 2023-02-12 13:12:09 +01:00
Alejandro Celaya
76a86c452e Optimize tags list query performance by using more subqueries 2023-02-12 13:09:24 +01:00
Alejandro Celaya
41aec15fab Migrate new test to PHPUnit 10 2023-02-10 20:45:09 +01:00
Alejandro Celaya
245cb0e35d Fixed merge conflicts 2023-02-10 20:44:05 +01:00
Alejandro Celaya
7a0b1e8494 Merge pull request #1699 from acelaya-forks/feature/fix-robots-txt
Fix dependency injected in CrawlingHelper
2023-02-10 20:41:10 +01:00
Alejandro Celaya
70c1c9f018 Fix dependency injected in CrawlingHelper 2023-02-10 20:26:18 +01:00
Alejandro Celaya
97e965157b Update changelog 2023-02-09 20:43:07 +01:00
Alejandro Celaya
04bbd471ff Migrate from PHPUnit annotations to native attributes 2023-02-09 20:42:18 +01:00
Alejandro Celaya
650a286982 Update to PHPUnit 10 2023-02-09 09:32:38 +01:00
Alejandro Celaya
ad44a8441a Merge pull request #1694 from acelaya-forks/feature/fix-gha-deprecations
Fix usage of deprecated GitHub actions practices
2023-02-06 21:56:35 +01:00
Alejandro Celaya
b339cf2429 Fix usage of deprecated GitHub actions practices 2023-02-06 21:47:04 +01:00
Alejandro Celaya
9cd97c2f1e Merge pull request #1691 from shlinkio/develop
Release 3.5.1
2023-02-04 17:58:27 +01:00
Alejandro Celaya
a7f6b60cba Merge pull request #1690 from acelaya-forks/feature/uninitialized-prop
Update to latest shlink-common including the cache clear fix for redis replication
2023-02-04 17:51:04 +01:00
Alejandro Celaya
0d7dc50670 Update to latest shlink-common including the cache clear fix for redis replication 2023-02-04 17:40:44 +01:00
Alejandro Celaya
4bc5b9261f Merge pull request #1687 from acelaya-forks/feature/ms-case-sensitive
Feature/ms case sensitive
2023-01-30 11:16:13 +01:00
Alejandro Celaya
fb572d5abb Fix accidentally removed statement in new migration 2023-01-30 10:52:07 +01:00
Alejandro Celaya
8fa4219b30 Update changelog 2023-01-30 10:50:47 +01:00
Alejandro Celaya
a52d0cd419 Ensure short_code column is case sensitive in Microsoft SQL server 2023-01-30 10:49:47 +01:00
Alejandro Celaya
0080ab5132 Merge pull request #1686 from acelaya-forks/feature/loose-mode
Rename loosely mode to loose mode
2023-01-29 11:42:52 +01:00
Alejandro Celaya
8afa582aa5 Create ShortUrlModeTest 2023-01-29 11:32:13 +01:00
Alejandro Celaya
d847c7648e Rename loosely mode to loose mode 2023-01-29 10:30:34 +01:00
Alejandro Celaya
c140db16d1 Improve issue templates requesting roadrunner when appropriate 2023-01-29 09:53:47 +01:00
Alejandro Celaya
adbf7c6f5e Fix twitter badge 2023-01-28 11:15:46 +01:00
225 changed files with 1454 additions and 1500 deletions

View File

@@ -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.
--> -->

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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' }}

View File

@@ -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:

View File

@@ -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:

View File

@@ -4,6 +4,43 @@ 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.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 +62,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*

View File

@@ -6,7 +6,7 @@
[![Latest Stable Version](https://img.shields.io/github/release/shlinkio/shlink.svg?style=flat-square)](https://packagist.org/packages/shlinkio/shlink) [![Latest Stable Version](https://img.shields.io/github/release/shlinkio/shlink.svg?style=flat-square)](https://packagist.org/packages/shlinkio/shlink)
[![Docker pulls](https://img.shields.io/docker/pulls/shlinkio/shlink.svg?logo=docker&style=flat-square)](https://hub.docker.com/r/shlinkio/shlink/) [![Docker pulls](https://img.shields.io/docker/pulls/shlinkio/shlink.svg?logo=docker&style=flat-square)](https://hub.docker.com/r/shlinkio/shlink/)
[![License](https://img.shields.io/github/license/shlinkio/shlink.svg?style=flat-square)](https://github.com/shlinkio/shlink/blob/main/LICENSE) [![License](https://img.shields.io/github/license/shlinkio/shlink.svg?style=flat-square)](https://github.com/shlinkio/shlink/blob/main/LICENSE)
[![Twitter](https://img.shields.io/twitter/follow/shlinkio?color=blue&label=follow&logo=twitter&style=flat-square)](https://twitter.com/shlinkio) [![Twitter](https://img.shields.io/badge/follow-shlinkio-blue.svg?style=flat-square&logo=twitter&color=blue)](https://twitter.com/shlinkio)
[![Mastodon](https://img.shields.io/mastodon/follow/109329425426175098?color=%236364ff&domain=https%3A%2F%2Ffosstodon.org&label=follow&logo=mastodon&logoColor=white&style=flat-square)](https://fosstodon.org/@shlinkio) [![Mastodon](https://img.shields.io/mastodon/follow/109329425426175098?color=%236364ff&domain=https%3A%2F%2Ffosstodon.org&label=follow&logo=mastodon&logoColor=white&style=flat-square)](https://fosstodon.org/@shlinkio)
[![Paypal donate](https://img.shields.io/badge/Donate-paypal-blue.svg?style=flat-square&logo=paypal&colorA=aaaaaa)](https://slnk.to/donate) [![Paypal donate](https://img.shields.io/badge/Donate-paypal-blue.svg?style=flat-square&logo=paypal&colorA=aaaaaa)](https://slnk.to/donate)

View File

@@ -20,63 +20,62 @@
"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", "lcobucci/jwt": "^4.3",
"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.3.1",
"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 +132,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",

View File

@@ -6,12 +6,32 @@ 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',
// 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',
// ],
], ],
], ],

View File

@@ -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,
], ],
], ],

View File

@@ -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 [

View File

@@ -19,4 +19,3 @@ 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';

View File

@@ -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'] ?? []),

View File

@@ -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

View 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;
}
}

View 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);
}
}

View File

@@ -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',

View File

@@ -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 => [

View File

@@ -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));
} }
} }

View File

@@ -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]);

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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';

View File

@@ -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(

View File

@@ -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;

View File

@@ -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']);

View File

@@ -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), [

View File

@@ -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';

View File

@@ -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();

View File

@@ -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
+---------+------------+ +---------+------------+

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl; namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
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;
@@ -45,7 +47,7 @@ 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();
@@ -64,7 +66,7 @@ class CreateShortUrlCommandTest extends TestCase
self::assertStringContainsString('stringified_short_url', $output); self::assertStringContainsString('stringified_short_url', $output);
} }
/** @test */ #[Test]
public function exceptionWhileParsingLongUrlOutputsError(): void public function exceptionWhileParsingLongUrlOutputsError(): void
{ {
$url = 'http://domain.com/invalid'; $url = 'http://domain.com/invalid';
@@ -80,7 +82,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 +97,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();
@@ -119,10 +121,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(
@@ -139,7 +138,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 +146,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();
@@ -166,7 +162,7 @@ class CreateShortUrlCommandTest extends TestCase
$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];

View File

@@ -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';

View File

@@ -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(

View File

@@ -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,7 +43,7 @@ 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
@@ -63,7 +65,7 @@ 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
@@ -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,
@@ -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([

View File

@@ -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');

View File

@@ -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'];

View File

@@ -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();

View File

@@ -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(

View File

@@ -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';

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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(

View File

@@ -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']];

View File

@@ -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)();

View File

@@ -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');

View File

@@ -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();

View File

@@ -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([

View File

@@ -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)];

View File

@@ -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);

View File

@@ -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));

View File

@@ -187,7 +187,7 @@ return [
Util\DoctrineBatchHelper::class, Util\DoctrineBatchHelper::class,
], ],
Crawling\CrawlingHelper::class => ['em'], Crawling\CrawlingHelper::class => [ShortUrl\Repository\CrawlableShortCodesQuery::class],
], ],
]; ];

View File

@@ -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;
} }
} }

View File

@@ -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> */
@@ -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;

View File

@@ -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);
}
} }

View File

@@ -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),

View File

@@ -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;
@@ -45,7 +46,6 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
$orderDir = $filtering?->orderBy?->direction; $orderDir = $filtering?->orderBy?->direction;
$orderMainQuery = $orderField !== null && OrderableField::isAggregateField($orderField); $orderMainQuery = $orderField !== null && OrderableField::isAggregateField($orderField);
$conn = $this->getEntityManager()->getConnection();
$subQb = $this->createQueryBuilder('t'); $subQb = $this->createQueryBuilder('t');
$subQb->select('t.id', 't.name'); $subQb->select('t.id', 't.name');
@@ -53,15 +53,51 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
$subQb->orderBy('t.name', $orderDir ?? 'ASC') $subQb->orderBy('t.name', $orderDir ?? 'ASC')
->setMaxResults($filtering?->limit ?? PHP_INT_MAX) ->setMaxResults($filtering?->limit ?? PHP_INT_MAX)
->setFirstResult($filtering?->offset ?? 0); ->setFirstResult($filtering?->offset ?? 0);
// TODO Check if applying limit/offset ot visits sub-queries is needed with large amounts of tags
} }
$conn = $this->getEntityManager()->getConnection();
$buildVisitsSubQuery = static function (bool $excludeBots, string $aggregateAlias) use ($conn) {
$visitsSubQuery = $conn->createQueryBuilder();
$commonJoinCondition = $visitsSubQuery->expr()->eq('v.short_url_id', 's.id');
$visitsJoin = ! $excludeBots
? $commonJoinCondition
: $visitsSubQuery->expr()->and(
$commonJoinCondition,
$visitsSubQuery->expr()->eq('v.potential_bot', $conn->quote('0')),
);
return $visitsSubQuery
->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', $visitsSubQuery->expr()->eq('st.short_url_id', 's.id'))
->groupBy('st.tag_id');
};
$allVisitsSubQuery = $buildVisitsSubQuery(false, 'visits');
$nonBotVisitsSubQuery = $buildVisitsSubQuery(true, 'non_bot_visits');
$searchTerm = $filtering?->searchTerm; $searchTerm = $filtering?->searchTerm;
if ($searchTerm !== null) { if ($searchTerm !== null) {
$subQb->andWhere($subQb->expr()->like('t.name', $conn->quote('%' . $searchTerm . '%'))); $subQb->andWhere($subQb->expr()->like('t.name', $conn->quote('%' . $searchTerm . '%')));
// TODO Check if applying this to all sub-queries makes it faster or slower
} }
$apiKey = $filtering?->apiKey; $apiKey = $filtering?->apiKey;
$applyApiKeyToNativeQuery = static fn (?ApiKey $apiKey, NativeQueryBuilder $nativeQueryBuilder) =>
$apiKey?->mapRoles(static fn (Role $role, array $meta) => match ($role) {
Role::DOMAIN_SPECIFIC => $nativeQueryBuilder->andWhere(
$nativeQueryBuilder->expr()->eq('s.domain_id', $conn->quote(Role::domainIdFromMeta($meta))),
),
Role::AUTHORED_SHORT_URLS => $nativeQueryBuilder->andWhere(
$nativeQueryBuilder->expr()->eq('s.author_api_key_id', $conn->quote($apiKey->getId())),
),
});
// Apply API key specification to all sub-queries
$this->applySpecification($subQb, new WithInlinedApiKeySpecsEnsuringJoin($apiKey), 't'); $this->applySpecification($subQb, new WithInlinedApiKeySpecsEnsuringJoin($apiKey), 't');
$applyApiKeyToNativeQuery($apiKey, $allVisitsSubQuery);
$applyApiKeyToNativeQuery($apiKey, $nonBotVisitsSubQuery);
// 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.
@@ -71,29 +107,22 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
->select( ->select(
't.id_0 AS id', 't.id_0 AS id',
't.name_1 AS name', 't.name_1 AS name',
'COALESCE(v.visits, 0) AS visits', // COALESCE required for postgres to properly order
'COALESCE(v2.non_bot_visits, 0) AS non_bot_visits', // COALESCE required for postgres to properly order
'COUNT(DISTINCT s.id) AS short_urls_count', 'COUNT(DISTINCT s.id) AS short_urls_count',
'COUNT(DISTINCT v.id) AS visits', // Native queries require snake_case for cross-db compatibility
'COUNT(DISTINCT v2.id) AS non_bot_visits',
) )
->from('(' . $subQb->getQuery()->getSQL() . ')', 't') // @phpstan-ignore-line ->from('(' . $subQb->getQuery()->getSQL() . ')', 't') // @phpstan-ignore-line
->leftJoin('t', 'short_urls_in_tags', 'st', $nativeQb->expr()->eq('t.id_0', 'st.tag_id')) ->leftJoin('t', 'short_urls_in_tags', 'st', $nativeQb->expr()->eq('t.id_0', 'st.tag_id'))
->leftJoin('st', 'short_urls', 's', $nativeQb->expr()->eq('s.id', 'st.short_url_id')) ->leftJoin('st', 'short_urls', 's', $nativeQb->expr()->eq('s.id', 'st.short_url_id'))
->leftJoin('st', 'visits', 'v', $nativeQb->expr()->eq('st.short_url_id', 'v.short_url_id')) ->leftJoin('t', '(' . $allVisitsSubQuery->getSQL() . ')', 'v', $nativeQb->expr()->eq('t.id_0', 'v.tag_id'))
->leftJoin('st', 'visits', 'v2', $nativeQb->expr()->and( // @phpstan-ignore-line ->leftJoin('t', '(' . $nonBotVisitsSubQuery->getSQL() . ')', 'v2', $nativeQb->expr()->eq(
$nativeQb->expr()->eq('st.short_url_id', 'v2.short_url_id'), 't.id_0',
$nativeQb->expr()->eq('v2.potential_bot', $conn->quote('0')), 'v2.tag_id',
)) ))
->groupBy('t.id_0', 't.name_1'); ->groupBy('t.id_0', 't.name_1', 'v.visits', 'v2.non_bot_visits');
// Apply API key role conditions to the native query too, as they will affect the amounts on the aggregates // Apply API key role conditions to the native query too, as they will affect the amounts on the aggregates
$apiKey?->mapRoles(static fn (Role $role, array $meta) => match ($role) { $applyApiKeyToNativeQuery($apiKey, $nativeQb);
Role::DOMAIN_SPECIFIC => $nativeQb->andWhere(
$nativeQb->expr()->eq('s.domain_id', $conn->quote(Role::domainIdFromMeta($meta))),
),
Role::AUTHORED_SHORT_URLS => $nativeQb->andWhere(
$nativeQb->expr()->eq('s.author_api_key_id', $conn->quote($apiKey->getId())),
),
});
if ($orderMainQuery) { if ($orderMainQuery) {
$nativeQb $nativeQb
@@ -107,9 +136,9 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
$rsm = new ResultSetMappingBuilder($this->getEntityManager()); $rsm = new ResultSetMappingBuilder($this->getEntityManager());
$rsm->addScalarResult('name', 'tag'); $rsm->addScalarResult('name', '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($nativeQb->getSQL(), $rsm)->getResult(),

View File

@@ -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'];

View 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: /custom
Allow: /abc123
Disallow: /
ROBOTS,
$body,
);
}
}

View File

@@ -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()));

View File

@@ -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,7 +20,7 @@ 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(

View File

@@ -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,7 +38,7 @@ 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;
@@ -49,7 +50,7 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
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(
@@ -143,7 +144,7 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
)); ));
} }
/** @test */ #[Test]
public function findListProperlyMapsFieldNamesToColumnNamesWhenOrdering(): void public function findListProperlyMapsFieldNamesToColumnNamesWhenOrdering(): void
{ {
$urls = ['a', 'z', 'c', 'b']; $urls = ['a', 'z', 'c', 'b'];
@@ -164,7 +165,7 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
self::assertEquals('z', $result[3]->getLongUrl()); self::assertEquals('z', $result[3]->getLongUrl());
} }
/** @test */ #[Test]
public function findListReturnsOnlyThoseWithMatchingTags(): void public function findListReturnsOnlyThoseWithMatchingTags(): void
{ {
$shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([ $shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([
@@ -273,7 +274,7 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
)); ));
} }
/** @test */ #[Test]
public function findListReturnsOnlyThoseWithMatchingDomains(): void public function findListReturnsOnlyThoseWithMatchingDomains(): void
{ {
$shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([ $shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([
@@ -309,7 +310,7 @@ 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([

View File

@@ -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,7 +31,7 @@ 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' => 'foo']));
@@ -55,19 +55,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,7 +98,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
)); ));
} }
/** @test */ #[Test]
public function shortCodeIsInUseLooksForShortUrlInProperSetOfTables(): void public function shortCodeIsInUseLooksForShortUrlInProperSetOfTables(): void
{ {
$shortUrlWithoutDomain = ShortUrl::create( $shortUrlWithoutDomain = ShortUrl::create(
@@ -130,7 +127,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
)); ));
} }
/** @test */ #[Test]
public function findOneLooksForShortUrlInProperSetOfTables(): void public function findOneLooksForShortUrlInProperSetOfTables(): void
{ {
$shortUrlWithoutDomain = ShortUrl::create( $shortUrlWithoutDomain = ShortUrl::create(
@@ -157,7 +154,7 @@ 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' => 'foobar'])));
@@ -172,7 +169,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
]))); ])));
} }
/** @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');
@@ -241,7 +238,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
); );
} }
/** @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');
@@ -269,7 +266,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');
@@ -395,7 +392,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
); );
} }
/** @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) =>

View File

@@ -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];

View File

@@ -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'];
@@ -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');

View File

@@ -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]);
} }

View File

@@ -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');
@@ -316,7 +317,7 @@ 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' => 'longUrl']));
@@ -365,7 +366,7 @@ 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' => 'longUrl']));
@@ -402,7 +403,7 @@ 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' => '1']));
@@ -445,7 +446,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());

View File

@@ -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';

View File

@@ -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' => [

View File

@@ -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';

View File

@@ -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:

View File

@@ -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());

View File

@@ -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'];

View File

@@ -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,
), ),

View File

@@ -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' => [

View File

@@ -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' => []], []];

View File

@@ -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' => [

View File

@@ -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)();

View File

@@ -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([]);

View File

@@ -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();

View File

@@ -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();

View File

@@ -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);

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\ErrorHandler; namespace ShlinkioTest\Shlink\Core\ErrorHandler;
use Laminas\Diactoros\ServerRequestFactory; use Laminas\Diactoros\ServerRequestFactory;
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;
@@ -32,7 +33,7 @@ class NotFoundTrackerMiddlewareTest extends TestCase
); );
} }
/** @test */ #[Test]
public function delegatesIntoRequestTracker(): void public function delegatesIntoRequestTracker(): void
{ {
$this->handler->expects($this->once())->method('handle')->with($this->request); $this->handler->expects($this->once())->method('handle')->with($this->request);

View File

@@ -7,6 +7,7 @@ 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\Assert;
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;
@@ -25,7 +26,7 @@ class NotFoundTypeResolverMiddlewareTest extends TestCase
$this->handler = $this->createMock(RequestHandlerInterface::class); $this->handler = $this->createMock(RequestHandlerInterface::class);
} }
/** @test */ #[Test]
public function notFoundTypeIsAddedToRequest(): void public function notFoundTypeIsAddedToRequest(): void
{ {
$request = ServerRequestFactory::fromGlobals(); $request = ServerRequestFactory::fromGlobals();

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\EventDispatcher; namespace ShlinkioTest\Shlink\Core\EventDispatcher;
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\Container\ContainerInterface; use Psr\Container\ContainerInterface;
@@ -21,7 +22,7 @@ class CloseDbConnectionEventListenerDelegatorTest extends TestCase
$this->delegator = new CloseDbConnectionEventListenerDelegator(); $this->delegator = new CloseDbConnectionEventListenerDelegator();
} }
/** @test */ #[Test]
public function properDependenciesArePassed(): void public function properDependenciesArePassed(): void
{ {
$callbackInvoked = false; $callbackInvoked = false;

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\EventDispatcher; namespace ShlinkioTest\Shlink\Core\EventDispatcher;
use Doctrine\DBAL\Connection; use Doctrine\DBAL\Connection;
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 RuntimeException; use RuntimeException;
@@ -22,10 +24,7 @@ class CloseDbConnectionEventListenerTest extends TestCase
$this->em = $this->createMock(ReopeningEntityManagerInterface::class); $this->em = $this->createMock(ReopeningEntityManagerInterface::class);
} }
/** #[Test, DataProvider('provideWrapped')]
* @test
* @dataProvider provideWrapped
*/
public function connectionIsOpenedBeforeAndClosedAfter(callable $wrapped, bool &$wrappedWasCalled): void public function connectionIsOpenedBeforeAndClosedAfter(callable $wrapped, bool &$wrappedWasCalled): void
{ {
$conn = $this->createMock(Connection::class); $conn = $this->createMock(Connection::class);
@@ -46,7 +45,7 @@ class CloseDbConnectionEventListenerTest extends TestCase
self::assertTrue($wrappedWasCalled); self::assertTrue($wrappedWasCalled);
} }
public function provideWrapped(): iterable public static function provideWrapped(): iterable
{ {
yield 'does not throw exception' => (static function (): array { yield 'does not throw exception' => (static function (): array {
$wrappedWasCalled = false; $wrappedWasCalled = false;

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\EventDispatcher; namespace ShlinkioTest\Shlink\Core\EventDispatcher;
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\EventDispatcher\Event\GeoLiteDbCreated; use Shlinkio\Shlink\Core\EventDispatcher\Event\GeoLiteDbCreated;
@@ -28,14 +29,14 @@ class LocateUnlocatedVisitsTest extends TestCase
$this->listener = new LocateUnlocatedVisits($this->locator, $this->visitToLocation); $this->listener = new LocateUnlocatedVisits($this->locator, $this->visitToLocation);
} }
/** @test */ #[Test]
public function locatorIsCalledWhenInvoked(): void public function locatorIsCalledWhenInvoked(): void
{ {
$this->locator->expects($this->once())->method('locateUnlocatedVisits')->with($this->listener); $this->locator->expects($this->once())->method('locateUnlocatedVisits')->with($this->listener);
($this->listener)(new GeoLiteDbCreated()); ($this->listener)(new GeoLiteDbCreated());
} }
/** @test */ #[Test]
public function visitToLocationHelperIsCalledToGeolocateVisits(): void public function visitToLocationHelperIsCalledToGeolocateVisits(): void
{ {
$visit = Visit::forBasePath(Visitor::emptyInstance()); $visit = Visit::forBasePath(Visitor::emptyInstance());

View File

@@ -6,6 +6,8 @@ namespace ShlinkioTest\Shlink\Core\EventDispatcher;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use OutOfRangeException; use OutOfRangeException;
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\EventDispatcher\EventDispatcherInterface; use Psr\EventDispatcher\EventDispatcherInterface;
@@ -49,7 +51,7 @@ class LocateVisitTest extends TestCase
); );
} }
/** @test */ #[Test]
public function invalidVisitLogsWarning(): void public function invalidVisitLogsWarning(): void
{ {
$event = new UrlVisited('123'); $event = new UrlVisited('123');
@@ -65,7 +67,7 @@ class LocateVisitTest extends TestCase
($this->locateVisit)($event); ($this->locateVisit)($event);
} }
/** @test */ #[Test]
public function nonExistingGeoLiteDbLogsWarning(): void public function nonExistingGeoLiteDbLogsWarning(): void
{ {
$event = new UrlVisited('123'); $event = new UrlVisited('123');
@@ -84,7 +86,7 @@ class LocateVisitTest extends TestCase
($this->locateVisit)($event); ($this->locateVisit)($event);
} }
/** @test */ #[Test]
public function invalidAddressLogsWarning(): void public function invalidAddressLogsWarning(): void
{ {
$event = new UrlVisited('123'); $event = new UrlVisited('123');
@@ -105,7 +107,7 @@ class LocateVisitTest extends TestCase
($this->locateVisit)($event); ($this->locateVisit)($event);
} }
/** @test */ #[Test]
public function unhandledExceptionLogsError(): void public function unhandledExceptionLogsError(): void
{ {
$event = new UrlVisited('123'); $event = new UrlVisited('123');
@@ -126,10 +128,7 @@ class LocateVisitTest extends TestCase
($this->locateVisit)($event); ($this->locateVisit)($event);
} }
/** #[Test, DataProvider('provideNonLocatableVisits')]
* @test
* @dataProvider provideNonLocatableVisits
*/
public function nonLocatableVisitsResolveToEmptyLocations(Visit $visit): void public function nonLocatableVisitsResolveToEmptyLocations(Visit $visit): void
{ {
$event = new UrlVisited('123'); $event = new UrlVisited('123');
@@ -146,7 +145,7 @@ class LocateVisitTest extends TestCase
self::assertEquals($visit->getVisitLocation(), VisitLocation::fromGeolocation(Location::emptyInstance())); self::assertEquals($visit->getVisitLocation(), VisitLocation::fromGeolocation(Location::emptyInstance()));
} }
public function provideNonLocatableVisits(): iterable public static function provideNonLocatableVisits(): iterable
{ {
$shortUrl = ShortUrl::createFake(); $shortUrl = ShortUrl::createFake();
@@ -155,10 +154,7 @@ class LocateVisitTest extends TestCase
yield 'localhost' => [Visit::forValidShortUrl($shortUrl, new Visitor('', '', IpAddress::LOCALHOST, ''))]; yield 'localhost' => [Visit::forValidShortUrl($shortUrl, new Visitor('', '', IpAddress::LOCALHOST, ''))];
} }
/** #[Test, DataProvider('provideIpAddresses')]
* @test
* @dataProvider provideIpAddresses
*/
public function locatableVisitsResolveToLocation(Visit $visit, ?string $originalIpAddress): void public function locatableVisitsResolveToLocation(Visit $visit, ?string $originalIpAddress): void
{ {
$ipAddr = $originalIpAddress ?? $visit->getRemoteAddr(); $ipAddr = $originalIpAddress ?? $visit->getRemoteAddr();
@@ -180,7 +176,7 @@ class LocateVisitTest extends TestCase
self::assertEquals($visit->getVisitLocation(), VisitLocation::fromGeolocation($location)); self::assertEquals($visit->getVisitLocation(), VisitLocation::fromGeolocation($location));
} }
public function provideIpAddresses(): iterable public static function provideIpAddresses(): iterable
{ {
yield 'no original IP address' => [ yield 'no original IP address' => [
Visit::forValidShortUrl(ShortUrl::createFake(), new Visitor('', '', '1.2.3.4', '')), Visit::forValidShortUrl(ShortUrl::createFake(), new Visitor('', '', '1.2.3.4', '')),

View File

@@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\Core\EventDispatcher\Mercure;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Exception; use Exception;
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\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@@ -39,7 +40,7 @@ class NotifyNewShortUrlToMercureTest extends TestCase
); );
} }
/** @test */ #[Test]
public function messageIsLoggedWhenShortUrlIsNotFound(): void public function messageIsLoggedWhenShortUrlIsNotFound(): void
{ {
$this->em->expects($this->once())->method('find')->with(ShortUrl::class, '123')->willReturn(null); $this->em->expects($this->once())->method('find')->with(ShortUrl::class, '123')->willReturn(null);
@@ -54,7 +55,7 @@ class NotifyNewShortUrlToMercureTest extends TestCase
($this->listener)(new ShortUrlCreated('123')); ($this->listener)(new ShortUrlCreated('123'));
} }
/** @test */ #[Test]
public function expectedNotificationIsPublished(): void public function expectedNotificationIsPublished(): void
{ {
$shortUrl = ShortUrl::withLongUrl('longUrl'); $shortUrl = ShortUrl::withLongUrl('longUrl');
@@ -71,7 +72,7 @@ class NotifyNewShortUrlToMercureTest extends TestCase
($this->listener)(new ShortUrlCreated('123')); ($this->listener)(new ShortUrlCreated('123'));
} }
/** @test */ #[Test]
public function messageIsPrintedIfPublishingFails(): void public function messageIsPrintedIfPublishingFails(): void
{ {
$shortUrl = ShortUrl::withLongUrl('longUrl'); $shortUrl = ShortUrl::withLongUrl('longUrl');

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\EventDispatcher\Mercure; namespace ShlinkioTest\Shlink\Core\EventDispatcher\Mercure;
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 Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@@ -37,7 +39,7 @@ class NotifyVisitToMercureTest extends TestCase
$this->listener = new NotifyVisitToMercure($this->helper, $this->updatesGenerator, $this->em, $this->logger); $this->listener = new NotifyVisitToMercure($this->helper, $this->updatesGenerator, $this->em, $this->logger);
} }
/** @test */ #[Test]
public function notificationsAreNotSentWhenVisitCannotBeFound(): void public function notificationsAreNotSentWhenVisitCannotBeFound(): void
{ {
$visitId = '123'; $visitId = '123';
@@ -55,7 +57,7 @@ class NotifyVisitToMercureTest extends TestCase
($this->listener)(new VisitLocated($visitId)); ($this->listener)(new VisitLocated($visitId));
} }
/** @test */ #[Test]
public function notificationsAreSentWhenVisitIsFound(): void public function notificationsAreSentWhenVisitIsFound(): void
{ {
$visitId = '123'; $visitId = '123';
@@ -75,7 +77,7 @@ class NotifyVisitToMercureTest extends TestCase
($this->listener)(new VisitLocated($visitId)); ($this->listener)(new VisitLocated($visitId));
} }
/** @test */ #[Test]
public function debugIsLoggedWhenExceptionIsThrown(): void public function debugIsLoggedWhenExceptionIsThrown(): void
{ {
$visitId = '123'; $visitId = '123';
@@ -99,10 +101,7 @@ class NotifyVisitToMercureTest extends TestCase
($this->listener)(new VisitLocated($visitId)); ($this->listener)(new VisitLocated($visitId));
} }
/** #[Test, DataProvider('provideOrphanVisits')]
* @test
* @dataProvider provideOrphanVisits
*/
public function notificationsAreSentForOrphanVisits(Visit $visit): void public function notificationsAreSentForOrphanVisits(Visit $visit): void
{ {
$visitId = '123'; $visitId = '123';
@@ -121,7 +120,7 @@ class NotifyVisitToMercureTest extends TestCase
($this->listener)(new VisitLocated($visitId)); ($this->listener)(new VisitLocated($visitId));
} }
public function provideOrphanVisits(): iterable public static function provideOrphanVisits(): iterable
{ {
$visitor = Visitor::emptyInstance(); $visitor = Visitor::emptyInstance();

View File

@@ -12,6 +12,8 @@ use GuzzleHttp\Promise\FulfilledPromise;
use GuzzleHttp\Promise\RejectedPromise; use GuzzleHttp\Promise\RejectedPromise;
use GuzzleHttp\RequestOptions; use GuzzleHttp\RequestOptions;
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 Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@@ -41,7 +43,7 @@ class NotifyVisitToWebHooksTest extends TestCase
$this->logger = $this->createMock(LoggerInterface::class); $this->logger = $this->createMock(LoggerInterface::class);
} }
/** @test */ #[Test]
public function emptyWebhooksMakeNoFurtherActions(): void public function emptyWebhooksMakeNoFurtherActions(): void
{ {
$this->em->expects($this->never())->method('find'); $this->em->expects($this->never())->method('find');
@@ -49,7 +51,7 @@ class NotifyVisitToWebHooksTest extends TestCase
$this->createListener([])(new VisitLocated('1')); $this->createListener([])(new VisitLocated('1'));
} }
/** @test */ #[Test]
public function invalidVisitDoesNotPerformAnyRequest(): void public function invalidVisitDoesNotPerformAnyRequest(): void
{ {
$this->em->expects($this->once())->method('find')->with(Visit::class, '1')->willReturn(null); $this->em->expects($this->once())->method('find')->with(Visit::class, '1')->willReturn(null);
@@ -62,7 +64,7 @@ class NotifyVisitToWebHooksTest extends TestCase
$this->createListener(['foo', 'bar'])(new VisitLocated('1')); $this->createListener(['foo', 'bar'])(new VisitLocated('1'));
} }
/** @test */ #[Test]
public function orphanVisitDoesNotPerformAnyRequestWhenDisabled(): void public function orphanVisitDoesNotPerformAnyRequestWhenDisabled(): void
{ {
$this->em->expects($this->once())->method('find')->with(Visit::class, '1')->willReturn( $this->em->expects($this->once())->method('find')->with(Visit::class, '1')->willReturn(
@@ -74,10 +76,7 @@ class NotifyVisitToWebHooksTest extends TestCase
$this->createListener(['foo', 'bar'], false)(new VisitLocated('1')); $this->createListener(['foo', 'bar'], false)(new VisitLocated('1'));
} }
/** #[Test, DataProvider('provideVisits')]
* @test
* @dataProvider provideVisits
*/
public function expectedRequestsArePerformedToWebhooks(Visit $visit, array $expectedResponseKeys): void public function expectedRequestsArePerformedToWebhooks(Visit $visit, array $expectedResponseKeys): void
{ {
$webhooks = ['foo', 'invalid', 'bar', 'baz']; $webhooks = ['foo', 'invalid', 'bar', 'baz'];
@@ -120,13 +119,13 @@ class NotifyVisitToWebHooksTest extends TestCase
$this->createListener($webhooks)(new VisitLocated('1')); $this->createListener($webhooks)(new VisitLocated('1'));
} }
public function provideVisits(): iterable public static function provideVisits(): iterable
{ {
yield 'regular visit' => [ yield 'regular visit' => [
Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()), Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()),
['shortUrl', 'visit'], ['shortUrl', 'visit'],
]; ];
yield 'orphan visit' => [Visit::forBasePath(Visitor::emptyInstance()), ['visit'],]; yield 'orphan visit' => [Visit::forBasePath(Visitor::emptyInstance()), ['visit']];
} }
private function createListener(array $webhooks, bool $notifyOrphanVisits = true): NotifyVisitToWebHooks private function createListener(array $webhooks, bool $notifyOrphanVisits = true): NotifyVisitToWebHooks

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\EventDispatcher; namespace ShlinkioTest\Shlink\Core\EventDispatcher;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\UpdatePublishing\Update; use Shlinkio\Shlink\Common\UpdatePublishing\Update;
use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGenerator; use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGenerator;
@@ -30,10 +32,7 @@ class PublishingUpdatesGeneratorTest extends TestCase
); );
} }
/** #[Test, DataProvider('provideMethod')]
* @test
* @dataProvider provideMethod
*/
public function visitIsProperlySerializedIntoUpdate(string $method, string $expectedTopic, ?string $title): void public function visitIsProperlySerializedIntoUpdate(string $method, string $expectedTopic, ?string $title): void
{ {
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([ $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([
@@ -77,16 +76,13 @@ class PublishingUpdatesGeneratorTest extends TestCase
], $update->payload); ], $update->payload);
} }
public function provideMethod(): iterable public static function provideMethod(): iterable
{ {
yield 'newVisitUpdate' => ['newVisitUpdate', 'https://shlink.io/new-visit', 'the cool title']; yield 'newVisitUpdate' => ['newVisitUpdate', 'https://shlink.io/new-visit', 'the cool title'];
yield 'newShortUrlVisitUpdate' => ['newShortUrlVisitUpdate', 'https://shlink.io/new-visit/foo', null]; yield 'newShortUrlVisitUpdate' => ['newShortUrlVisitUpdate', 'https://shlink.io/new-visit/foo', null];
} }
/** #[Test, DataProvider('provideOrphanVisits')]
* @test
* @dataProvider provideOrphanVisits
*/
public function orphanVisitIsProperlySerializedIntoUpdate(Visit $orphanVisit): void public function orphanVisitIsProperlySerializedIntoUpdate(Visit $orphanVisit): void
{ {
$update = $this->generator->newOrphanVisitUpdate($orphanVisit); $update = $this->generator->newOrphanVisitUpdate($orphanVisit);
@@ -105,7 +101,7 @@ class PublishingUpdatesGeneratorTest extends TestCase
], $update->payload); ], $update->payload);
} }
public function provideOrphanVisits(): iterable public static function provideOrphanVisits(): iterable
{ {
$visitor = Visitor::emptyInstance(); $visitor = Visitor::emptyInstance();
@@ -114,7 +110,7 @@ class PublishingUpdatesGeneratorTest extends TestCase
yield VisitType::BASE_URL->value => [Visit::forBasePath($visitor)]; yield VisitType::BASE_URL->value => [Visit::forBasePath($visitor)];
} }
/** @test */ #[Test]
public function shortUrlIsProperlySerializedIntoUpdate(): void public function shortUrlIsProperlySerializedIntoUpdate(): void
{ {
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([ $shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([

View File

@@ -7,6 +7,8 @@ namespace ShlinkioTest\Shlink\Core\EventDispatcher\RabbitMq;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use DomainException; use DomainException;
use Exception; use Exception;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@@ -36,7 +38,7 @@ class NotifyNewShortUrlToRabbitMqTest extends TestCase
$this->logger = $this->createMock(LoggerInterface::class); $this->logger = $this->createMock(LoggerInterface::class);
} }
/** @test */ #[Test]
public function doesNothingWhenTheFeatureIsNotEnabled(): void public function doesNothingWhenTheFeatureIsNotEnabled(): void
{ {
$this->helper->expects($this->never())->method('publishUpdate'); $this->helper->expects($this->never())->method('publishUpdate');
@@ -47,7 +49,7 @@ class NotifyNewShortUrlToRabbitMqTest extends TestCase
($this->listener(false))(new ShortUrlCreated('123')); ($this->listener(false))(new ShortUrlCreated('123'));
} }
/** @test */ #[Test]
public function notificationsAreNotSentWhenShortUrlCannotBeFound(): void public function notificationsAreNotSentWhenShortUrlCannotBeFound(): void
{ {
$shortUrlId = '123'; $shortUrlId = '123';
@@ -62,7 +64,7 @@ class NotifyNewShortUrlToRabbitMqTest extends TestCase
($this->listener())(new ShortUrlCreated($shortUrlId)); ($this->listener())(new ShortUrlCreated($shortUrlId));
} }
/** @test */ #[Test]
public function expectedChannelIsNotified(): void public function expectedChannelIsNotified(): void
{ {
$shortUrlId = '123'; $shortUrlId = '123';
@@ -79,10 +81,7 @@ class NotifyNewShortUrlToRabbitMqTest extends TestCase
($this->listener())(new ShortUrlCreated($shortUrlId)); ($this->listener())(new ShortUrlCreated($shortUrlId));
} }
/** #[Test, DataProvider('provideExceptions')]
* @test
* @dataProvider provideExceptions
*/
public function printsDebugMessageInCaseOfError(Throwable $e): void public function printsDebugMessageInCaseOfError(Throwable $e): void
{ {
$shortUrlId = '123'; $shortUrlId = '123';
@@ -102,7 +101,7 @@ class NotifyNewShortUrlToRabbitMqTest extends TestCase
($this->listener())(new ShortUrlCreated($shortUrlId)); ($this->listener())(new ShortUrlCreated($shortUrlId));
} }
public function provideExceptions(): iterable public static function provideExceptions(): iterable
{ {
yield [new RuntimeException('RuntimeException Error')]; yield [new RuntimeException('RuntimeException Error')];
yield [new Exception('Exception Error')]; yield [new Exception('Exception Error')];

View File

@@ -8,6 +8,8 @@ use Doctrine\ORM\EntityManagerInterface;
use DomainException; use DomainException;
use Exception; use Exception;
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 Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@@ -44,7 +46,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
$this->logger = $this->createMock(LoggerInterface::class); $this->logger = $this->createMock(LoggerInterface::class);
} }
/** @test */ #[Test]
public function doesNothingWhenTheFeatureIsNotEnabled(): void public function doesNothingWhenTheFeatureIsNotEnabled(): void
{ {
$this->helper->expects($this->never())->method('publishUpdate'); $this->helper->expects($this->never())->method('publishUpdate');
@@ -55,7 +57,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
($this->listener(new RabbitMqOptions(enabled: false)))(new VisitLocated('123')); ($this->listener(new RabbitMqOptions(enabled: false)))(new VisitLocated('123'));
} }
/** @test */ #[Test]
public function notificationsAreNotSentWhenVisitCannotBeFound(): void public function notificationsAreNotSentWhenVisitCannotBeFound(): void
{ {
$visitId = '123'; $visitId = '123';
@@ -70,10 +72,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
($this->listener())(new VisitLocated($visitId)); ($this->listener())(new VisitLocated($visitId));
} }
/** #[Test, DataProvider('provideVisits')]
* @test
* @dataProvider provideVisits
*/
public function expectedChannelsAreNotifiedBasedOnTheVisitType(Visit $visit, array $expectedChannels): void public function expectedChannelsAreNotifiedBasedOnTheVisitType(Visit $visit, array $expectedChannels): void
{ {
$visitId = '123'; $visitId = '123';
@@ -91,7 +90,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
($this->listener())(new VisitLocated($visitId)); ($this->listener())(new VisitLocated($visitId));
} }
public function provideVisits(): iterable public static function provideVisits(): iterable
{ {
$visitor = Visitor::emptyInstance(); $visitor = Visitor::emptyInstance();
@@ -108,10 +107,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
]; ];
} }
/** #[Test, DataProvider('provideExceptions')]
* @test
* @dataProvider provideExceptions
*/
public function printsDebugMessageInCaseOfError(Throwable $e): void public function printsDebugMessageInCaseOfError(Throwable $e): void
{ {
$visitId = '123'; $visitId = '123';
@@ -130,17 +126,14 @@ class NotifyVisitToRabbitMqTest extends TestCase
($this->listener())(new VisitLocated($visitId)); ($this->listener())(new VisitLocated($visitId));
} }
public function provideExceptions(): iterable public static function provideExceptions(): iterable
{ {
yield [new RuntimeException('RuntimeException Error')]; yield [new RuntimeException('RuntimeException Error')];
yield [new Exception('Exception Error')]; yield [new Exception('Exception Error')];
yield [new DomainException('DomainException Error')]; yield [new DomainException('DomainException Error')];
} }
/** #[Test, DataProvider('provideLegacyPayloads')]
* @test
* @dataProvider provideLegacyPayloads
*/
public function expectedPayloadIsPublishedDependingOnConfig( public function expectedPayloadIsPublishedDependingOnConfig(
bool $legacy, bool $legacy,
Visit $visit, Visit $visit,
@@ -155,14 +148,14 @@ class NotifyVisitToRabbitMqTest extends TestCase
($this->listener(new RabbitMqOptions(true, $legacy)))(new VisitLocated($visitId)); ($this->listener(new RabbitMqOptions(true, $legacy)))(new VisitLocated($visitId));
} }
public function provideLegacyPayloads(): iterable public static function provideLegacyPayloads(): iterable
{ {
yield 'legacy non-orphan visit' => [ yield 'legacy non-orphan visit' => [
true, true,
$visit = Visit::forValidShortUrl(ShortUrl::withLongUrl('longUrl'), Visitor::emptyInstance()), $visit = Visit::forValidShortUrl(ShortUrl::withLongUrl('longUrl'), Visitor::emptyInstance()),
noop(...), noop(...),
function (MockObject & PublishingHelperInterface $helper) use ($visit): void { function (MockObject & PublishingHelperInterface $helper) use ($visit): void {
$helper->method('publishUpdate')->with($this->callback(function (Update $update) use ($visit): bool { $helper->method('publishUpdate')->with(self::callback(function (Update $update) use ($visit): bool {
$payload = $update->payload; $payload = $update->payload;
Assert::assertEquals($payload, $visit->jsonSerialize()); Assert::assertEquals($payload, $visit->jsonSerialize());
Assert::assertArrayNotHasKey('visitedUrl', $payload); Assert::assertArrayNotHasKey('visitedUrl', $payload);
@@ -179,7 +172,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
Visit::forBasePath(Visitor::emptyInstance()), Visit::forBasePath(Visitor::emptyInstance()),
noop(...), noop(...),
function (MockObject & PublishingHelperInterface $helper): void { function (MockObject & PublishingHelperInterface $helper): void {
$helper->method('publishUpdate')->with($this->callback(function (Update $update): bool { $helper->method('publishUpdate')->with(self::callback(function (Update $update): bool {
$payload = $update->payload; $payload = $update->payload;
Assert::assertArrayHasKey('visitedUrl', $payload); Assert::assertArrayHasKey('visitedUrl', $payload);
Assert::assertArrayHasKey('type', $payload); Assert::assertArrayHasKey('type', $payload);
@@ -193,14 +186,14 @@ class NotifyVisitToRabbitMqTest extends TestCase
Visit::forValidShortUrl(ShortUrl::withLongUrl('longUrl'), Visitor::emptyInstance()), Visit::forValidShortUrl(ShortUrl::withLongUrl('longUrl'), Visitor::emptyInstance()),
function (MockObject & PublishingUpdatesGeneratorInterface $updatesGenerator): void { function (MockObject & PublishingUpdatesGeneratorInterface $updatesGenerator): void {
$update = Update::forTopicAndPayload('', []); $update = Update::forTopicAndPayload('', []);
$updatesGenerator->expects($this->never())->method('newOrphanVisitUpdate'); $updatesGenerator->expects(self::never())->method('newOrphanVisitUpdate');
$updatesGenerator->expects($this->once())->method('newVisitUpdate')->withAnyParameters()->willReturn( $updatesGenerator->expects(self::once())->method('newVisitUpdate')->withAnyParameters()->willReturn(
$update, $update,
); );
$updatesGenerator->expects($this->once())->method('newShortUrlVisitUpdate')->willReturn($update); $updatesGenerator->expects(self::once())->method('newShortUrlVisitUpdate')->willReturn($update);
}, },
function (MockObject & PublishingHelperInterface $helper): void { function (MockObject & PublishingHelperInterface $helper): void {
$helper->expects($this->exactly(2))->method('publishUpdate')->with($this->isInstanceOf(Update::class)); $helper->expects(self::exactly(2))->method('publishUpdate')->with(self::isInstanceOf(Update::class));
}, },
]; ];
yield 'non-legacy orphan visit' => [ yield 'non-legacy orphan visit' => [
@@ -208,12 +201,12 @@ class NotifyVisitToRabbitMqTest extends TestCase
Visit::forBasePath(Visitor::emptyInstance()), Visit::forBasePath(Visitor::emptyInstance()),
function (MockObject & PublishingUpdatesGeneratorInterface $updatesGenerator): void { function (MockObject & PublishingUpdatesGeneratorInterface $updatesGenerator): void {
$update = Update::forTopicAndPayload('', []); $update = Update::forTopicAndPayload('', []);
$updatesGenerator->expects($this->once())->method('newOrphanVisitUpdate')->willReturn($update); $updatesGenerator->expects(self::once())->method('newOrphanVisitUpdate')->willReturn($update);
$updatesGenerator->expects($this->never())->method('newVisitUpdate'); $updatesGenerator->expects(self::never())->method('newVisitUpdate');
$updatesGenerator->expects($this->never())->method('newShortUrlVisitUpdate'); $updatesGenerator->expects(self::never())->method('newShortUrlVisitUpdate');
}, },
function (MockObject & PublishingHelperInterface $helper): void { function (MockObject & PublishingHelperInterface $helper): void {
$helper->expects($this->once())->method('publishUpdate')->with($this->isInstanceOf(Update::class)); $helper->expects(self::once())->method('publishUpdate')->with(self::isInstanceOf(Update::class));
}, },
]; ];
} }

View File

@@ -7,6 +7,8 @@ namespace ShlinkioTest\Shlink\Core\EventDispatcher\RedisPubSub;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use DomainException; use DomainException;
use Exception; use Exception;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@@ -35,7 +37,7 @@ class NotifyNewShortUrlToRedisTest extends TestCase
$this->logger = $this->createMock(LoggerInterface::class); $this->logger = $this->createMock(LoggerInterface::class);
} }
/** @test */ #[Test]
public function doesNothingWhenTheFeatureIsNotEnabled(): void public function doesNothingWhenTheFeatureIsNotEnabled(): void
{ {
$this->helper->expects($this->never())->method('publishUpdate'); $this->helper->expects($this->never())->method('publishUpdate');
@@ -46,10 +48,7 @@ class NotifyNewShortUrlToRedisTest extends TestCase
$this->createListener(false)(new ShortUrlCreated('123')); $this->createListener(false)(new ShortUrlCreated('123'));
} }
/** #[Test, DataProvider('provideExceptions')]
* @test
* @dataProvider provideExceptions
*/
public function printsDebugMessageInCaseOfError(Throwable $e): void public function printsDebugMessageInCaseOfError(Throwable $e): void
{ {
$shortUrlId = '123'; $shortUrlId = '123';
@@ -69,7 +68,7 @@ class NotifyNewShortUrlToRedisTest extends TestCase
$this->createListener()(new ShortUrlCreated($shortUrlId)); $this->createListener()(new ShortUrlCreated($shortUrlId));
} }
public function provideExceptions(): iterable public static function provideExceptions(): iterable
{ {
yield [new RuntimeException('RuntimeException Error')]; yield [new RuntimeException('RuntimeException Error')];
yield [new Exception('Exception Error')]; yield [new Exception('Exception Error')];

View File

@@ -7,6 +7,8 @@ namespace ShlinkioTest\Shlink\Core\EventDispatcher\RedisPubSub;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use DomainException; use DomainException;
use Exception; use Exception;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@@ -35,7 +37,7 @@ class NotifyVisitToRedisTest extends TestCase
$this->logger = $this->createMock(LoggerInterface::class); $this->logger = $this->createMock(LoggerInterface::class);
} }
/** @test */ #[Test]
public function doesNothingWhenTheFeatureIsNotEnabled(): void public function doesNothingWhenTheFeatureIsNotEnabled(): void
{ {
$this->helper->expects($this->never())->method('publishUpdate'); $this->helper->expects($this->never())->method('publishUpdate');
@@ -46,10 +48,7 @@ class NotifyVisitToRedisTest extends TestCase
$this->createListener(false)(new VisitLocated('123')); $this->createListener(false)(new VisitLocated('123'));
} }
/** #[Test, DataProvider('provideExceptions')]
* @test
* @dataProvider provideExceptions
*/
public function printsDebugMessageInCaseOfError(Throwable $e): void public function printsDebugMessageInCaseOfError(Throwable $e): void
{ {
$visitId = '123'; $visitId = '123';
@@ -68,7 +67,7 @@ class NotifyVisitToRedisTest extends TestCase
$this->createListener()(new VisitLocated($visitId)); $this->createListener()(new VisitLocated($visitId));
} }
public function provideExceptions(): iterable public static function provideExceptions(): iterable
{ {
yield [new RuntimeException('RuntimeException Error')]; yield [new RuntimeException('RuntimeException Error')];
yield [new Exception('Exception Error')]; yield [new Exception('Exception Error')];

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\EventDispatcher; namespace ShlinkioTest\Shlink\Core\EventDispatcher;
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\EventDispatcher\EventDispatcherInterface; use Psr\EventDispatcher\EventDispatcherInterface;
@@ -32,7 +34,7 @@ class UpdateGeoLiteDbTest extends TestCase
$this->listener = new UpdateGeoLiteDb($this->dbUpdater, $this->logger, $this->eventDispatcher); $this->listener = new UpdateGeoLiteDb($this->dbUpdater, $this->logger, $this->eventDispatcher);
} }
/** @test */ #[Test]
public function exceptionWhileUpdatingDbLogsError(): void public function exceptionWhileUpdatingDbLogsError(): void
{ {
$e = new RuntimeException(); $e = new RuntimeException();
@@ -48,10 +50,7 @@ class UpdateGeoLiteDbTest extends TestCase
($this->listener)(); ($this->listener)();
} }
/** #[Test, DataProvider('provideFlags')]
* @test
* @dataProvider provideFlags
*/
public function noticeMessageIsPrintedWhenFirstCallbackIsInvoked(bool $oldDbExists, string $expectedMessage): void public function noticeMessageIsPrintedWhenFirstCallbackIsInvoked(bool $oldDbExists, string $expectedMessage): void
{ {
$this->dbUpdater->expects($this->once())->method('checkDbUpdate')->withAnyParameters()->willReturnCallback( $this->dbUpdater->expects($this->once())->method('checkDbUpdate')->withAnyParameters()->willReturnCallback(
@@ -67,16 +66,13 @@ class UpdateGeoLiteDbTest extends TestCase
($this->listener)(); ($this->listener)();
} }
public function provideFlags(): iterable public static function provideFlags(): iterable
{ {
yield 'existing old db' => [true, 'Updating GeoLite2 db file...']; yield 'existing old db' => [true, 'Updating GeoLite2 db file...'];
yield 'not existing old db' => [false, 'Downloading GeoLite2 db file...']; yield 'not existing old db' => [false, 'Downloading GeoLite2 db file...'];
} }
/** #[Test, DataProvider('provideDownloaded')]
* @test
* @dataProvider provideDownloaded
*/
public function noticeMessageIsPrintedWhenSecondCallbackIsInvoked( public function noticeMessageIsPrintedWhenSecondCallbackIsInvoked(
int $total, int $total,
int $downloaded, int $downloaded,
@@ -101,7 +97,7 @@ class UpdateGeoLiteDbTest extends TestCase
($this->listener)(); ($this->listener)();
} }
public function provideDownloaded(): iterable public static function provideDownloaded(): iterable
{ {
yield [100, 0, true, null]; yield [100, 0, true, null];
yield [100, 0, false, null]; yield [100, 0, false, null];
@@ -113,10 +109,7 @@ class UpdateGeoLiteDbTest extends TestCase
yield [100, 101, false, 'Finished downloading GeoLite2 db file']; yield [100, 101, false, 'Finished downloading GeoLite2 db file'];
} }
/** #[Test, DataProvider('provideGeolocationResults')]
* @test
* @dataProvider provideGeolocationResults
*/
public function dispatchesEventOnlyWhenDbFileHasBeenCreatedForTheFirstTime( public function dispatchesEventOnlyWhenDbFileHasBeenCreatedForTheFirstTime(
GeolocationResult $result, GeolocationResult $result,
int $expectedDispatches, int $expectedDispatches,
@@ -129,7 +122,7 @@ class UpdateGeoLiteDbTest extends TestCase
($this->listener)(); ($this->listener)();
} }
public function provideGeolocationResults(): iterable public static function provideGeolocationResults(): iterable
{ {
return map(GeolocationResult::cases(), static fn (GeolocationResult $value) => [ return map(GeolocationResult::cases(), static fn (GeolocationResult $value) => [
$value, $value,

Some files were not shown because too many files have changed in this diff Show More