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.
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.
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.
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.
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).
-->
#### How Shlink is set-up
#### How Shlink is set up
* Shlink 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)
#### Summary
@@ -31,7 +31,7 @@ With that said, please fill in the information requested next. More information
#### Expected behavior
<!-- How did you expected to behave? -->
<!-- How did you expect it to behave? -->
#### 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.
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.
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.
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.
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).
-->
#### How Shlink is set-up
#### How Shlink is set up
* Shlink 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)
#### Summary

View File

@@ -28,7 +28,7 @@ runs:
extensions: ${{ inputs.php-extensions }}
key: ${{ inputs.extensions-cache-key }}
- name: Cache extensions
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ${{ steps.extcache.outputs.dir }}
key: ${{ steps.extcache.outputs.key }}

View File

@@ -27,14 +27,14 @@ jobs:
path: build
- name: Resolve 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
# run: |
# BRANCH="${GITHUB_REF#refs/heads/}" |
# if [[ $BRANCH == 'main' || $BRANCH == 'develop' ]]; then
# echo "::set-output name=args::--logger-github=false"
# echo "args=--logger-github=false" >> $GITHUB_OUTPUT
# 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;
shell: bash
- 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-api/coverage-api.cov build/coverage-api.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: php phpcov-8.2.1.phar merge build --clover build/clover.xml
- run: wget https://phar.phpunit.de/phpcov-9.0.0.phar
- run: php phpcov-9.0.0.phar merge build --clover build/clover.xml
- name: Publish coverage
uses: codecov/codecov-action@v1
with:

View File

@@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v3
- name: 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
- uses: './.github/actions/ci-setup'
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).
## [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
### 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.
@@ -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_`.
* [#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
* *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)
[![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)
[![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)
[![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",
"cakephp/chronos": "^2.3",
"doctrine/migrations": "^3.5",
"doctrine/orm": "^2.13.3",
"endroid/qr-code": "^4.6",
"doctrine/orm": "^2.14",
"endroid/qr-code": "^4.7",
"geoip2/geoip2": "^2.13",
"guzzlehttp/guzzle": "^7.5",
"happyr/doctrine-specification": "^2.0",
"jaybizzle/crawler-detect": "^1.2.112",
"laminas/laminas-config": "^3.7",
"laminas/laminas-config-aggregator": "^1.11",
"laminas/laminas-diactoros": "^2.19",
"laminas/laminas-inputfilter": "^2.22",
"laminas/laminas-servicemanager": "^3.19",
"laminas/laminas-stdlib": "^3.15",
"lcobucci/jwt": "^4.2",
"laminas/laminas-config": "^3.8",
"laminas/laminas-config-aggregator": "^1.13",
"laminas/laminas-diactoros": "^2.24",
"laminas/laminas-inputfilter": "^2.24",
"laminas/laminas-servicemanager": "^3.20",
"laminas/laminas-stdlib": "^3.16",
"lcobucci/jwt": "^4.3",
"league/uri": "^6.8",
"lstrojny/functional-php": "^1.17",
"mezzio/mezzio": "^3.13",
"mezzio/mezzio-fastroute": "^3.7",
"mezzio/mezzio-problem-details": "^1.7",
"mezzio/mezzio-swoole": "^4.5",
"mezzio/mezzio": "^3.15",
"mezzio/mezzio-fastroute": "^3.8",
"mezzio/mezzio-problem-details": "^1.11",
"mezzio/mezzio-swoole": "^4.6",
"mlocati/ip-lib": "^1.18",
"mobiledetect/mobiledetectlib": "^3.74",
"ocramius/proxy-manager": "^2.14",
"pagerfanta/core": "^3.6",
"pagerfanta/core": "^3.7",
"php-middleware/request-id": "^4.1",
"pugx/shortid-php": "^1.1",
"ramsey/uuid": "^4.5",
"shlinkio/shlink-common": "^5.3",
"ramsey/uuid": "^4.7",
"shlinkio/shlink-common": "^5.3.1",
"shlinkio/shlink-config": "^2.4",
"shlinkio/shlink-event-dispatcher": "^2.6",
"shlinkio/shlink-importer": "^5.0",
"shlinkio/shlink-installer": "^8.3",
"shlinkio/shlink-ip-geolocation": "^3.2",
"spiral/roadrunner": "^2.11",
"spiral/roadrunner-jobs": "^2.5",
"symfony/console": "^6.1",
"symfony/filesystem": "^6.1",
"symfony/lock": "^6.1",
"symfony/process": "^6.1",
"symfony/string": "^6.1"
"spiral/roadrunner": "^2.12",
"spiral/roadrunner-jobs": "^2.7",
"symfony/console": "^6.2",
"symfony/filesystem": "^6.2",
"symfony/lock": "^6.2",
"symfony/process": "^6.2",
"symfony/string": "^6.2"
},
"require-dev": {
"cebe/php-openapi": "^1.7",
"devster/ubench": "^2.1",
"dms/phpunit-arraysubset-asserts": "^0.4.0",
"infection/infection": "^0.26.15",
"infection/infection": "^0.26.19",
"openswoole/ide-helper": "~4.11.5",
"phpstan/phpstan": "^1.8",
"phpstan/phpstan": "^1.9",
"phpstan/phpstan-doctrine": "^1.3",
"phpstan/phpstan-phpunit": "^1.1",
"phpstan/phpstan-phpunit": "^1.3",
"phpstan/phpstan-symfony": "^1.2",
"phpunit/php-code-coverage": "^9.2",
"phpunit/phpunit": "^9.5",
"phpunit/php-code-coverage": "^10.0",
"phpunit/phpunit": "^10.0",
"roave/security-advisories": "dev-master",
"shlinkio/php-coding-standard": "~2.3.0",
"shlinkio/shlink-test-utils": "^3.4",
"symfony/var-dumper": "^6.1",
"veewee/composer-run-parallel": "^1.1"
"shlinkio/shlink-test-utils": "^3.5",
"symfony/var-dumper": "^6.2",
"veewee/composer-run-parallel": "^1.2"
},
"autoload": {
"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:ci": "GENERATE_COVERAGE=yes 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: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",

View File

@@ -6,12 +6,32 @@ return [
'entity_manager' => [
'connection' => [
// MySQL
'user' => 'root',
'password' => 'root',
'driver' => 'pdo_mysql',
'host' => 'shlink_db_mysql',
'dbname' => 'shlink',
// 'dbname' => 'shlink_foo',
'charset' => 'utf8mb4',
// Postgres
// 'user' => 'postgres',
// 'password' => 'root',
// 'driver' => 'pdo_pgsql',
// 'host' => 'shlink_db_postgres',
// 'dbname' => 'shlink_foo',
// 'charset' => 'utf8',
// MSSQL
// 'user' => 'sa',
// 'password' => 'Passw0rd!',
// 'driver' => 'pdo_sqlsrv',
// 'host' => 'shlink_db_ms',
// 'dbname' => 'shlink_foo',
// 'driverOptions' => [
// 'TrustServerCertificate' => 'true',
// ],
],
],

View File

@@ -5,14 +5,16 @@ declare(strict_types=1);
use Monolog\Level;
use Shlinkio\Shlink\Common\Logger\LoggerType;
$isSwoole = extension_loaded('openswoole');
use function Shlinkio\Shlink\Config\runningInOpenswoole;
$logToStream = runningInOpenswoole();
return [
'logger' => [
'Shlink' => [
// For swoole, send logs as stream
'type' => $isSwoole ? LoggerType::STREAM->value : LoggerType::FILE->value,
// For openswoole, send logs as stream
'type' => $logToStream ? LoggerType::STREAM->value : LoggerType::FILE->value,
'level' => Level::Debug->value,
],
],

View File

@@ -14,7 +14,7 @@ return (static function (): array {
MIN_SHORT_CODES_LENGTH,
);
$modeFromEnv = EnvVars::SHORT_URL_MODE->loadFromEnv(ShortUrlMode::STRICT->value);
$mode = ShortUrlMode::tryFrom($modeFromEnv) ?? ShortUrlMode::STRICT;
$mode = ShortUrlMode::tryDeprecated($modeFromEnv) ?? ShortUrlMode::STRICT;
return [

View File

@@ -19,4 +19,3 @@ const DEFAULT_QR_CODE_FORMAT = 'png';
const DEFAULT_QR_CODE_ERROR_CORRECTION = 'l';
const DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = true;
const MIN_TASK_WORKERS = 4;
const MIGRATIONS_TABLE = 'migrations';

View File

@@ -23,10 +23,10 @@ if (file_exists($covFile)) {
}
$testHelper->createTestDb(
['bin/cli', 'db:create'],
['bin/cli', 'db:migrate'],
['bin/doctrine', 'orm:schema-tool:drop'],
['bin/doctrine', 'dbal:run-sql'],
createDbCommand: ['bin/cli', 'db:create'],
migrateDbCommand: ['bin/cli', 'db:migrate'],
dropSchemaCommand: ['bin/doctrine', 'orm:schema-tool:drop'],
runSqlCommand: ['bin/doctrine', 'dbal:run-sql'],
);
CliTest\CliTestCase::setSeedFixturesCallback(
static fn () => $testHelper->seedFixtures($em, $config['data_fixtures'] ?? []),

View File

@@ -3,7 +3,7 @@
set -ex
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
ACCEPT_EULA=Y apt-get install msodbcsql17
apt-get install unixodbc-dev
ACCEPT_EULA=Y apt-get install msodbcsql18
# 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);
use const Shlinkio\Shlink\MIGRATIONS_TABLE;
return [
'migrations_paths' => [
'ShlinkMigrations' => 'data/migrations',
],
'table_storage' => [
'table_name' => MIGRATIONS_TABLE,
'table_name' => 'migrations',
],
'custom_template' => 'data/migrations_template.txt',

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI;
use Doctrine\DBAL\Connection;
use GeoIp2\Database\Reader;
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Laminas\ServiceManager\Factory\InvokableFactory;
@@ -116,7 +115,7 @@ return [
LockFactory::class,
Util\ProcessRunner::class,
PhpExecutableFinder::class,
Connection::class,
'em',
NoDbNameConnectionFactory::SERVICE_NAME,
],
Command\Db\MigrateDatabaseCommand::class => [

View File

@@ -6,6 +6,8 @@ namespace Shlinkio\Shlink\CLI\Command\Db;
use Doctrine\DBAL\Connection;
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\ProcessRunnerInterface;
use Symfony\Component\Console\Input\InputInterface;
@@ -15,12 +17,13 @@ use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Process\PhpExecutableFinder;
use function Functional\contains;
use function Functional\filter;
use const Shlinkio\Shlink\MIGRATIONS_TABLE;
use function Functional\map;
use function Functional\some;
class CreateDatabaseCommand extends AbstractDatabaseCommand
{
private readonly Connection $regularConn;
public const NAME = 'db:create';
public const DOCTRINE_SCRIPT = 'bin/doctrine';
public const DOCTRINE_CREATE_SCHEMA_COMMAND = 'orm:schema-tool:create';
@@ -29,9 +32,10 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
LockFactory $locker,
ProcessRunnerInterface $processRunner,
PhpExecutableFinder $phpFinder,
private Connection $regularConn,
private Connection $noDbNameConn,
private readonly EntityManagerInterface $em,
private readonly Connection $noDbNameConn,
) {
$this->regularConn = $this->em->getConnection();
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
$schemaManager = $this->noDbNameConn->createSchemaManager();
$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;
if ($shlinkDatabase !== null && ! contains($databases, $shlinkDatabase)) {
@@ -83,10 +89,14 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
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();
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;
use PHPUnit\Framework\Attributes\Test;
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
class GenerateApiKeyTest extends CliTestCase
{
/** @test */
#[Test]
public function outputIsCorrect(): void
{
[$output, $exitCode] = $this->exec([GenerateKeyCommand::NAME]);

View File

@@ -5,16 +5,15 @@ declare(strict_types=1);
namespace ShlinkioCliTest\Shlink\CLI\Command;
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\Util\ExitCodes;
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
class ListApiKeysTest extends CliTestCase
{
/**
* @test
* @dataProvider provideFlags
*/
#[Test, DataProvider('provideFlags')]
public function generatesExpectedOutput(array $flags, string $expectedOutput): void
{
[$output, $exitCode] = $this->exec([ListKeysCommand::NAME, ...$flags]);
@@ -23,7 +22,7 @@ class ListApiKeysTest extends CliTestCase
self::assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode);
}
public function provideFlags(): iterable
public static function provideFlags(): iterable
{
$expiredApiKeyDate = Chronos::now()->subDay()->startOfDay()->toAtomString();
$enabledOnlyOutput = <<<OUT

View File

@@ -4,22 +4,21 @@ declare(strict_types=1);
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\TestUtils\CliTest\CliTestCase;
class ListShortUrlsTest extends CliTestCase
{
/**
* @test
* @dataProvider provideFlagsAndOutput
*/
#[Test, DataProvider('provideFlagsAndOutput')]
public function generatesExpectedOutput(array $flags, string $expectedOutput): void
{
[$output] = $this->exec([ListShortUrlsCommand::NAME, ...$flags], ['no']);
self::assertStringContainsString($expectedOutput, $output);
}
public function provideFlagsAndOutput(): iterable
public static function provideFlagsAndOutput(): iterable
{
// phpcs:disable Generic.Files.LineLength
yield 'no flags' => [[], <<<OUTPUT

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\ApiKey;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\ApiKey\RoleResolver;
@@ -27,29 +29,27 @@ class RoleResolverTest extends TestCase
$this->resolver = new RoleResolver($this->domainService, 'default.com');
}
/**
* @test
* @dataProvider provideRoles
*/
#[Test, DataProvider('provideRoles')]
public function properRolesAreResolvedBasedOnInput(
InputInterface $input,
callable $createInput,
array $expectedRoles,
int $expectedDomainCalls,
): void {
$input = $createInput($this);
$this->domainService->expects($this->exactly($expectedDomainCalls))->method('getOrCreate')->with(
'example.com',
)->willReturn($this->domainWithId(Domain::withAuthority('example.com')));
)->willReturn(self::domainWithId(Domain::withAuthority('example.com')));
$result = $this->resolver->determineRoles($input);
self::assertEquals($expectedRoles, $result);
}
public function provideRoles(): iterable
public static function provideRoles(): iterable
{
$domain = $this->domainWithId(Domain::withAuthority('example.com'));
$buildInput = function (array $definition): InputInterface {
$input = $this->createStub(InputInterface::class);
$domain = self::domainWithId(Domain::withAuthority('example.com'));
$buildInput = static fn (array $definition) => function (TestCase $test) use ($definition): InputInterface {
$input = $test->createStub(InputInterface::class);
$input->method('getOption')->willReturnMap(
map($definition, static fn (mixed $returnValue, string $param) => [$param, $returnValue]),
);
@@ -98,7 +98,7 @@ class RoleResolverTest extends TestCase
];
}
/** @test */
#[Test]
public function exceptionIsThrownWhenTryingToAddDomainOnlyLinkedToDefaultDomain(): void
{
$input = $this->createStub(InputInterface::class);
@@ -114,7 +114,7 @@ class RoleResolverTest extends TestCase
$this->resolver->determineRoles($input);
}
private function domainWithId(Domain $domain): Domain
private static function domainWithId(Domain $domain): Domain
{
$domain->setId('1');
return $domain;

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Api;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Api\DisableKeyCommand;
@@ -25,7 +26,7 @@ class DisableKeyCommandTest extends TestCase
$this->commandTester = $this->testerForCommand(new DisableKeyCommand($this->apiKeyService));
}
/** @test */
#[Test]
public function providedApiKeyIsDisabled(): void
{
$apiKey = 'abcd1234';
@@ -39,7 +40,7 @@ class DisableKeyCommandTest extends TestCase
self::assertStringContainsString('API key "abcd1234" properly disabled', $output);
}
/** @test */
#[Test]
public function errorIsReturnedIfServiceThrowsException(): void
{
$apiKey = 'abcd1234';

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Api;
use Cake\Chronos\Chronos;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\ApiKey\RoleResolverInterface;
@@ -32,7 +33,7 @@ class GenerateKeyCommandTest extends TestCase
$this->commandTester = $this->testerForCommand($command);
}
/** @test */
#[Test]
public function noExpirationDateIsDefinedIfNotProvided(): void
{
$this->apiKeyService->expects($this->once())->method('create')->with(
@@ -46,7 +47,7 @@ class GenerateKeyCommandTest extends TestCase
self::assertStringContainsString('Generated API key: ', $output);
}
/** @test */
#[Test]
public function expirationDateIsDefinedIfProvided(): void
{
$this->apiKeyService->expects($this->once())->method('create')->with(
@@ -59,7 +60,7 @@ class GenerateKeyCommandTest extends TestCase
]);
}
/** @test */
#[Test]
public function nameIsDefinedIfProvided(): void
{
$this->apiKeyService->expects($this->once())->method('create')->with(

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Api;
use Cake\Chronos\Chronos;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
@@ -29,10 +31,7 @@ class ListKeysCommandTest extends TestCase
$this->commandTester = $this->testerForCommand(new ListKeysCommand($this->apiKeyService));
}
/**
* @test
* @dataProvider provideKeysAndOutputs
*/
#[Test, DataProvider('provideKeysAndOutputs')]
public function returnsExpectedOutput(array $keys, bool $enabledOnly, string $expected): void
{
$this->apiKeyService->expects($this->once())->method('listKeys')->with($enabledOnly)->willReturn($keys);
@@ -43,7 +42,7 @@ class ListKeysCommandTest extends TestCase
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');
@@ -84,14 +83,14 @@ class ListKeysCommandTest extends TestCase
yield 'with roles' => [
[
$apiKey1 = ApiKey::create(),
$apiKey2 = $this->apiKeyWithRoles([RoleDefinition::forAuthoredShortUrls()]),
$apiKey3 = $this->apiKeyWithRoles(
[RoleDefinition::forDomain($this->domainWithId(Domain::withAuthority('example.com')))],
$apiKey2 = self::apiKeyWithRoles([RoleDefinition::forAuthoredShortUrls()]),
$apiKey3 = self::apiKeyWithRoles(
[RoleDefinition::forDomain(self::domainWithId(Domain::withAuthority('example.com')))],
),
$apiKey4 = ApiKey::create(),
$apiKey5 = $this->apiKeyWithRoles([
$apiKey5 = self::apiKeyWithRoles([
RoleDefinition::forAuthoredShortUrls(),
RoleDefinition::forDomain($this->domainWithId(Domain::withAuthority('example.com'))),
RoleDefinition::forDomain(self::domainWithId(Domain::withAuthority('example.com'))),
]),
$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();
foreach ($roles as $role) {
@@ -151,7 +150,7 @@ class ListKeysCommandTest extends TestCase
return $apiKey;
}
private function domainWithId(Domain $domain): Domain
private static function domainWithId(Domain $domain): Domain
{
$domain->setId('1');
return $domain;

View File

@@ -9,6 +9,11 @@ use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\SqlitePlatform;
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\TestCase;
use Shlinkio\Shlink\CLI\Command\Db\CreateDatabaseCommand;
@@ -20,8 +25,6 @@ use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\LockInterface;
use Symfony\Component\Process\PhpExecutableFinder;
use const Shlinkio\Shlink\MIGRATIONS_TABLE;
class CreateDatabaseCommandTest extends TestCase
{
use CliTestUtilsTrait;
@@ -29,6 +32,7 @@ class CreateDatabaseCommandTest extends TestCase
private CommandTester $commandTester;
private MockObject & ProcessRunnerInterface $processHelper;
private MockObject & Connection $regularConn;
private MockObject & ClassMetadataFactory $metadataFactory;
private MockObject & AbstractSchemaManager $schemaManager;
private MockObject & Driver $driver;
@@ -49,25 +53,27 @@ class CreateDatabaseCommandTest extends TestCase
$this->regularConn->method('createSchemaManager')->willReturn($this->schemaManager);
$this->driver = $this->createMock(Driver::class);
$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->method('createSchemaManager')->withAnyParameters()->willReturn($this->schemaManager);
$command = new CreateDatabaseCommand(
$locker,
$this->processHelper,
$phpExecutableFinder,
$this->regularConn,
$noDbNameConn,
);
$command = new CreateDatabaseCommand($locker, $this->processHelper, $phpExecutableFinder, $em, $noDbNameConn);
$this->commandTester = $this->testerForCommand($command);
}
/** @test */
#[Test]
public function successMessageIsPrintedIfDatabaseAlreadyExists(): void
{
$shlinkDatabase = 'shlink_database';
$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(
['foo', $shlinkDatabase, 'bar'],
);
@@ -81,29 +87,30 @@ class CreateDatabaseCommandTest extends TestCase
self::assertStringContainsString('Database already exists. Run "db:migrate" command', $output);
}
/** @test */
#[Test]
public function databaseIsCreatedIfItDoesNotExist(): void
{
$shlinkDatabase = 'shlink_database';
$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('createDatabase')->with($shlinkDatabase);
$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->commandTester->execute([]);
}
/**
* @test
* @dataProvider provideEmptyDatabase
*/
#[Test, DataProvider('provideEmptyDatabase')]
public function tablesAreCreatedIfDatabaseIsEmpty(array $tables): void
{
$shlinkDatabase = 'shlink_database';
$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(
['foo', $shlinkDatabase, 'bar'],
);
@@ -124,18 +131,19 @@ class CreateDatabaseCommandTest extends TestCase
self::assertStringContainsString('Database properly created!', $output);
}
public function provideEmptyDatabase(): iterable
public static function provideEmptyDatabase(): iterable
{
yield 'no tables' => [[]];
yield 'migrations table' => [[MIGRATIONS_TABLE]];
yield 'migrations table' => [['non_shlink_table']];
}
/** @test */
#[Test]
public function databaseCheckIsSkippedForSqlite(): void
{
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(SqlitePlatform::class));
$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('createDatabase');
$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;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Db\MigrateDatabaseCommand;
@@ -38,7 +39,7 @@ class MigrateDatabaseCommandTest extends TestCase
$this->commandTester = $this->testerForCommand($command);
}
/** @test */
#[Test]
public function migrationsCommandIsRunWithProperVerbosity(): void
{
$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;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Domain\DomainRedirectsCommand;
@@ -30,10 +32,7 @@ class DomainRedirectsCommandTest extends TestCase
$this->commandTester = $this->testerForCommand(new DomainRedirectsCommand($this->domainService));
}
/**
* @test
* @dataProvider provideDomains
*/
#[Test, DataProvider('provideDomains')]
public function onlyPlainQuestionsAreAskedForNewDomainsAndDomainsWithNoRedirects(?Domain $domain): void
{
$domainAuthority = 'my-domain.com';
@@ -60,13 +59,13 @@ class DomainRedirectsCommandTest extends TestCase
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 'domain without redirects' => [Domain::withAuthority('')];
}
/** @test */
#[Test]
public function offersNewOptionsForDomainsWithExistingRedirects(): void
{
$domainAuthority = 'example.com';
@@ -95,7 +94,7 @@ class DomainRedirectsCommandTest extends TestCase
self::assertEquals(3, substr_count($output, 'Remove redirect'));
}
/** @test */
#[Test]
public function authorityIsRequestedWhenNotProvidedAndNoOtherDomainsExist(): void
{
$domainAuthority = 'example.com';
@@ -117,7 +116,7 @@ class DomainRedirectsCommandTest extends TestCase
self::assertStringContainsString('Domain authority for which you want to set specific redirects', $output);
}
/** @test */
#[Test]
public function oneOfTheExistingDomainsCanBeSelected(): void
{
$domainAuthority = 'existing-two.com';
@@ -146,7 +145,7 @@ class DomainRedirectsCommandTest extends TestCase
self::assertStringContainsString($domainAuthority, $output);
}
/** @test */
#[Test]
public function aNewDomainCanBeCreatedEvenIfOthersAlreadyExist(): void
{
$domainAuthority = 'new-domain.com';

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Domain;
use Pagerfanta\Adapter\ArrayAdapter;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Domain\GetDomainVisitsCommand;
@@ -37,7 +38,7 @@ class GetDomainVisitsCommandTest extends TestCase
);
}
/** @test */
#[Test]
public function outputIsProperlyGenerated(): void
{
$shortUrl = ShortUrl::createFake();

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Domain;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Domain\ListDomainsCommand;
@@ -29,10 +31,7 @@ class ListDomainsCommandTest extends TestCase
$this->commandTester = $this->testerForCommand(new ListDomainsCommand($this->domainService));
}
/**
* @test
* @dataProvider provideInputsAndOutputs
*/
#[Test, DataProvider('provideInputsAndOutputs')]
public function allDomainsAreProperlyPrinted(array $input, string $expectedOutput): void
{
$bazDomain = Domain::withAuthority('baz.com');
@@ -57,7 +56,7 @@ class ListDomainsCommandTest extends TestCase
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
}
public function provideInputsAndOutputs(): iterable
public static function provideInputsAndOutputs(): iterable
{
$withoutRedirectsOutput = <<<OUTPUT
+---------+------------+

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\ShortUrl\CreateShortUrlCommand;
@@ -45,7 +47,7 @@ class CreateShortUrlCommandTest extends TestCase
$this->commandTester = $this->testerForCommand($command);
}
/** @test */
#[Test]
public function properShortCodeIsCreatedIfLongUrlIsCorrect(): void
{
$shortUrl = ShortUrl::createFake();
@@ -64,7 +66,7 @@ class CreateShortUrlCommandTest extends TestCase
self::assertStringContainsString('stringified_short_url', $output);
}
/** @test */
#[Test]
public function exceptionWhileParsingLongUrlOutputsError(): void
{
$url = 'http://domain.com/invalid';
@@ -80,7 +82,7 @@ class CreateShortUrlCommandTest extends TestCase
self::assertStringContainsString('Provided URL http://domain.com/invalid is invalid.', $output);
}
/** @test */
#[Test]
public function providingNonUniqueSlugOutputsError(): void
{
$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);
}
/** @test */
#[Test]
public function properlyProcessesProvidedTags(): void
{
$shortUrl = ShortUrl::createFake();
@@ -119,10 +121,7 @@ class CreateShortUrlCommandTest extends TestCase
self::assertStringContainsString('stringified_short_url', $output);
}
/**
* @test
* @dataProvider provideDomains
*/
#[Test, DataProvider('provideDomains')]
public function properlyProcessesProvidedDomain(array $input, ?string $expectedDomain): void
{
$this->urlShortener->expects($this->once())->method('shorten')->with(
@@ -139,7 +138,7 @@ class CreateShortUrlCommandTest extends TestCase
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
}
public function provideDomains(): iterable
public static function provideDomains(): iterable
{
yield 'no domain' => [[], null];
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];
}
/**
* @test
* @dataProvider provideFlags
*/
#[Test, DataProvider('provideFlags')]
public function urlValidationHasExpectedValueBasedOnProvidedFlags(array $options, ?bool $expectedValidateUrl): void
{
$shortUrl = ShortUrl::createFake();
@@ -166,7 +162,7 @@ class CreateShortUrlCommandTest extends TestCase
$this->commandTester->execute($options);
}
public function provideFlags(): iterable
public static function provideFlags(): iterable
{
yield 'no flags' => [[], null];
yield 'validate-url' => [['--validate-url' => true], true];

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\ShortUrl\DeleteShortUrlCommand;
@@ -30,7 +32,7 @@ class DeleteShortUrlCommandTest extends TestCase
$this->commandTester = $this->testerForCommand(new DeleteShortUrlCommand($this->service));
}
/** @test */
#[Test]
public function successMessageIsPrintedIfUrlIsProperlyDeleted(): void
{
$shortCode = 'abc123';
@@ -48,7 +50,7 @@ class DeleteShortUrlCommandTest extends TestCase
);
}
/** @test */
#[Test]
public function invalidShortCodePrintsMessage(): void
{
$shortCode = 'abc123';
@@ -64,10 +66,7 @@ class DeleteShortUrlCommandTest extends TestCase
self::assertStringContainsString(sprintf('No URL found with short code "%s"', $shortCode), $output);
}
/**
* @test
* @dataProvider provideRetryDeleteAnswers
*/
#[Test, DataProvider('provideRetryDeleteAnswers')]
public function deleteIsRetriedWhenThresholdIsReachedAndQuestionIsAccepted(
array $retryAnswer,
int $expectedDeleteCalls,
@@ -98,14 +97,14 @@ class DeleteShortUrlCommandTest extends TestCase
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 no to retry' => [['no'], 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
{
$shortCode = 'abc123';

View File

@@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
use Cake\Chronos\Chronos;
use Pagerfanta\Adapter\ArrayAdapter;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\ShortUrl\GetShortUrlVisitsCommand;
@@ -39,7 +40,7 @@ class GetShortUrlVisitsCommandTest extends TestCase
$this->commandTester = $this->testerForCommand($command);
}
/** @test */
#[Test]
public function noDateFlagsTriesToListWithoutDateRange(): void
{
$shortCode = 'abc123';
@@ -51,7 +52,7 @@ class GetShortUrlVisitsCommandTest extends TestCase
$this->commandTester->execute(['shortCode' => $shortCode]);
}
/** @test */
#[Test]
public function providingDateFlagsTheListGetsFiltered(): void
{
$shortCode = 'abc123';
@@ -69,7 +70,7 @@ class GetShortUrlVisitsCommandTest extends TestCase
]);
}
/** @test */
#[Test]
public function providingInvalidDatesPrintsWarning(): void
{
$shortCode = 'abc123';
@@ -91,7 +92,7 @@ class GetShortUrlVisitsCommandTest extends TestCase
);
}
/** @test */
#[Test]
public function outputIsProperlyGenerated(): void
{
$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 Pagerfanta\Adapter\ArrayAdapter;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
@@ -41,7 +43,7 @@ class ListShortUrlsCommandTest extends TestCase
$this->commandTester = $this->testerForCommand($command);
}
/** @test */
#[Test]
public function loadingMorePagesCallsListMoreTimes(): void
{
// The paginator will return more than one page
@@ -63,7 +65,7 @@ class ListShortUrlsCommandTest extends TestCase
self::assertStringNotContainsString('Continue with page 5?', $output);
}
/** @test */
#[Test]
public function havingMorePagesButAnsweringNoCallsListJustOnce(): void
{
// The paginator will return more than one page
@@ -89,7 +91,7 @@ class ListShortUrlsCommandTest extends TestCase
self::assertStringNotContainsString('Continue with page 3?', $output);
}
/** @test */
#[Test]
public function passingPageWillMakeListStartOnThatPage(): void
{
$page = 5;
@@ -101,10 +103,7 @@ class ListShortUrlsCommandTest extends TestCase
$this->commandTester->execute(['--page' => $page]);
}
/**
* @test
* @dataProvider provideOptionalFlags
*/
#[Test, DataProvider('provideOptionalFlags')]
public function provideOptionalFlagsMakesNewColumnsToBeIncluded(
array $input,
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'));
$key = $apiKey->toString();
@@ -174,10 +173,7 @@ class ListShortUrlsCommandTest extends TestCase
];
}
/**
* @test
* @dataProvider provideArgs
*/
#[Test, DataProvider('provideArgs')]
public function serviceIsInvokedWithProvidedArgs(
array $commandArgs,
?int $page,
@@ -200,7 +196,7 @@ class ListShortUrlsCommandTest extends TestCase
$this->commandTester->execute($commandArgs);
}
public function provideArgs(): iterable
public static function provideArgs(): iterable
{
yield [[], 1, 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
{
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(ShortUrlsParams::fromRawData([
@@ -255,7 +248,7 @@ class ListShortUrlsCommandTest extends TestCase
$this->commandTester->execute($commandArgs);
}
public function provideOrderBy(): iterable
public static function provideOrderBy(): iterable
{
yield [[], null];
yield [['--order-by' => 'visits'], 'visits'];
@@ -264,7 +257,7 @@ class ListShortUrlsCommandTest extends TestCase
yield [['--order-by' => 'title-DESC'], 'title-DESC'];
}
/** @test */
#[Test]
public function requestingAllElementsWillSetItemsPerPage(): void
{
$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;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\ShortUrl\ResolveUrlCommand;
@@ -31,7 +32,7 @@ class ResolveUrlCommandTest extends TestCase
$this->commandTester = $this->testerForCommand(new ResolveUrlCommand($this->urlResolver));
}
/** @test */
#[Test]
public function correctShortCodeResolvesUrl(): void
{
$shortCode = 'abc123';
@@ -46,7 +47,7 @@ class ResolveUrlCommandTest extends TestCase
self::assertEquals('Long URL: ' . $expectedUrl . PHP_EOL, $output);
}
/** @test */
#[Test]
public function incorrectShortCodeOutputsErrorMessage(): void
{
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain('abc123');

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Tag\DeleteTagsCommand;
@@ -24,7 +25,7 @@ class DeleteTagsCommandTest extends TestCase
$this->commandTester = $this->testerForCommand(new DeleteTagsCommand($this->tagService));
}
/** @test */
#[Test]
public function errorIsReturnedWhenNoTagsAreProvided(): void
{
$this->commandTester->execute([]);
@@ -33,7 +34,7 @@ class DeleteTagsCommandTest extends TestCase
self::assertStringContainsString('You have to provide at least one tag name', $output);
}
/** @test */
#[Test]
public function serviceIsInvokedOnSuccess(): void
{
$tagNames = ['foo', 'bar'];

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
use Pagerfanta\Adapter\ArrayAdapter;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Tag\GetTagVisitsCommand;
@@ -37,7 +38,7 @@ class GetTagVisitsCommandTest extends TestCase
);
}
/** @test */
#[Test]
public function outputIsProperlyGenerated(): void
{
$shortUrl = ShortUrl::createFake();

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
use Pagerfanta\Adapter\ArrayAdapter;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Tag\ListTagsCommand;
@@ -27,7 +28,7 @@ class ListTagsCommandTest extends TestCase
$this->commandTester = $this->testerForCommand(new ListTagsCommand($this->tagService));
}
/** @test */
#[Test]
public function noTagsPrintsEmptyMessage(): void
{
$this->tagService->expects($this->once())->method('tagsInfo')->withAnyParameters()->willReturn(
@@ -40,7 +41,7 @@ class ListTagsCommandTest extends TestCase
self::assertStringContainsString('No tags found', $output);
}
/** @test */
#[Test]
public function listOfTagsIsPrinted(): void
{
$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;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Tag\RenameTagCommand;
@@ -27,7 +28,7 @@ class RenameTagCommandTest extends TestCase
$this->commandTester = $this->testerForCommand(new RenameTagCommand($this->tagService));
}
/** @test */
#[Test]
public function errorIsPrintedIfExceptionIsThrown(): void
{
$oldName = 'foo';
@@ -45,7 +46,7 @@ class RenameTagCommandTest extends TestCase
self::assertStringContainsString('Tag with name "foo" could not be found', $output);
}
/** @test */
#[Test]
public function successIsPrintedIfNoErrorOccurs(): void
{
$oldName = 'foo';

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand;
@@ -29,10 +31,7 @@ class DownloadGeoLiteDbCommandTest extends TestCase
$this->commandTester = $this->testerForCommand(new DownloadGeoLiteDbCommand($this->dbUpdater));
}
/**
* @test
* @dataProvider provideFailureParams
*/
#[Test, DataProvider('provideFailureParams')]
public function showsProperMessageWhenGeoLiteUpdateFails(
bool $olderDbExists,
string $expectedMessage,
@@ -61,7 +60,7 @@ class DownloadGeoLiteDbCommandTest extends TestCase
self::assertSame($expectedExitCode, $exitCode);
}
public function provideFailureParams(): iterable
public static function provideFailureParams(): iterable
{
yield 'existing db' => [
true,
@@ -75,10 +74,7 @@ class DownloadGeoLiteDbCommandTest extends TestCase
];
}
/**
* @test
* @dataProvider provideSuccessParams
*/
#[Test, DataProvider('provideSuccessParams')]
public function printsExpectedMessageWhenNoErrorOccurs(callable $checkUpdateBehavior, string $expectedMessage): void
{
$this->dbUpdater->expects($this->once())->method('checkDbUpdate')->withAnyParameters()->willReturnCallback(
@@ -93,7 +89,7 @@ class DownloadGeoLiteDbCommandTest extends TestCase
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 'outdated db' => [function (callable $beforeDownload): GeolocationResult {

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
use Pagerfanta\Adapter\ArrayAdapter;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Visit\GetNonOrphanVisitsCommand;
@@ -37,7 +38,7 @@ class GetNonOrphanVisitsCommandTest extends TestCase
);
}
/** @test */
#[Test]
public function outputIsProperlyGenerated(): void
{
$shortUrl = ShortUrl::createFake();

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
use Pagerfanta\Adapter\ArrayAdapter;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Visit\GetOrphanVisitsCommand;
@@ -30,7 +31,7 @@ class GetOrphanVisitsCommandTest extends TestCase
$this->commandTester = $this->testerForCommand(new GetOrphanVisitsCommand($this->visitsHelper));
}
/** @test */
#[Test]
public function outputIsProperlyGenerated(): void
{
$visit = Visit::forBasePath(new Visitor('bar', 'foo', '', ''))->locate(

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand;
@@ -55,10 +57,7 @@ class LocateVisitsCommandTest extends TestCase
$this->commandTester = $this->testerForCommand($command, $this->downloadDbCommand);
}
/**
* @test
* @dataProvider provideArgs
*/
#[Test, DataProvider('provideArgs')]
public function expectedSetOfVisitsIsProcessedBasedOnArgs(
int $expectedUnlocatedCalls,
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 'retry' => [1, 1, 0, false, ['--retry' => 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
{
$visit = Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance());
@@ -131,13 +127,13 @@ class LocateVisitsCommandTest extends TestCase
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 'localhost address' => [IpCannotBeLocatedException::forLocalhost(), 'Ignored localhost address'];
}
/** @test */
#[Test]
public function errorWhileLocatingIpIsDisplayed(): void
{
$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
{
$this->lock->method('acquire')->with($this->isFalse())->willReturn(false);
@@ -186,7 +182,7 @@ class LocateVisitsCommandTest extends TestCase
);
}
/** @test */
#[Test]
public function showsProperMessageWhenGeoLiteUpdateFails(): void
{
$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);
}
/** @test */
#[Test]
public function providingAllFlagOnItsOwnDisplaysNotice(): void
{
$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);
}
/**
* @test
* @dataProvider provideAbortInputs
*/
#[Test, DataProvider('provideAbortInputs')]
public function processingAllCancelsCommandIfUserDoesNotActivelyAgreeToConfirmation(array $inputs): void
{
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS);
@@ -226,7 +219,7 @@ class LocateVisitsCommandTest extends TestCase
$this->commandTester->execute(['--all' => true, '--retry' => true]);
}
public function provideAbortInputs(): iterable
public static function provideAbortInputs(): iterable
{
yield 'n' => [['n']];
yield 'no' => [['no']];

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI;
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\ConfigProvider;
@@ -17,7 +18,7 @@ class ConfigProviderTest extends TestCase
$this->configProvider = new ConfigProvider();
}
/** @test */
#[Test]
public function configIsProperlyReturned(): void
{
$config = ($this->configProvider)();

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Exception;
use Exception;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
@@ -12,10 +14,7 @@ use Throwable;
class GeolocationDbUpdateFailedExceptionTest extends TestCase
{
/**
* @test
* @dataProvider providePrev
*/
#[Test, DataProvider('providePrev')]
public function withOlderDbBuildsException(?Throwable $prev): void
{
$e = GeolocationDbUpdateFailedException::withOlderDb($prev);
@@ -29,10 +28,7 @@ class GeolocationDbUpdateFailedExceptionTest extends TestCase
self::assertEquals($prev, $e->getPrevious());
}
/**
* @test
* @dataProvider providePrev
*/
#[Test, DataProvider('providePrev')]
public function withoutOlderDbBuildsException(?Throwable $prev): void
{
$e = GeolocationDbUpdateFailedException::withoutOlderDb($prev);
@@ -46,14 +42,14 @@ class GeolocationDbUpdateFailedExceptionTest extends TestCase
self::assertEquals($prev, $e->getPrevious());
}
public function providePrev(): iterable
public static function providePrev(): iterable
{
yield 'no prev' => [null];
yield 'RuntimeException' => [new RuntimeException('prev')];
yield 'Exception' => [new Exception('prev')];
}
/** @test */
#[Test]
public function withInvalidEpochInOldDbBuildsException(): void
{
$e = GeolocationDbUpdateFailedException::withInvalidEpochInOldDb('foobar');

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Exception;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Exception\InvalidRoleConfigException;
use Shlinkio\Shlink\Rest\ApiKey\Role;
@@ -12,7 +13,7 @@ use function sprintf;
class InvalidRoleConfigExceptionTest extends TestCase
{
/** @test */
#[Test]
public function forDomainOnlyWithDefaultDomainGeneratesExpectedException(): void
{
$e = InvalidRoleConfigException::forDomainOnlyWithDefaultDomain();

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Factory;
use Laminas\ServiceManager\ServiceManager;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
use Shlinkio\Shlink\Core\Options\AppOptions;
@@ -21,7 +22,7 @@ class ApplicationFactoryTest extends TestCase
$this->factory = new ApplicationFactory();
}
/** @test */
#[Test]
public function allCommandsWhichAreServicesAreAdded(): void
{
$sm = $this->createServiceManager([

View File

@@ -7,6 +7,8 @@ namespace ShlinkioTest\Shlink\CLI\GeoLite;
use Cake\Chronos\Chronos;
use GeoIp2\Database\Reader;
use MaxMind\Db\Reader\Metadata;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
@@ -35,7 +37,7 @@ class GeolocationDbUpdaterTest extends TestCase
$this->lock->method('acquire')->with($this->isTrue())->willReturn(true);
}
/** @test */
#[Test]
public function exceptionIsThrownWhenOlderDbDoesNotExistAndDownloadFails(): void
{
$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
{
$prev = new DbUpdateException('');
@@ -84,7 +83,7 @@ class GeolocationDbUpdaterTest extends TestCase
}
}
public function provideBigDays(): iterable
public static function provideBigDays(): iterable
{
yield [36];
yield [50];
@@ -92,10 +91,7 @@ class GeolocationDbUpdaterTest extends TestCase
yield [100];
}
/**
* @test
* @dataProvider provideSmallDays
*/
#[Test, DataProvider('provideSmallDays')]
public function databaseIsNotUpdatedIfItIsNewEnough(string|int $buildEpoch): void
{
$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);
}
public function provideSmallDays(): iterable
public static function provideSmallDays(): iterable
{
$generateParamsWithTimestamp = static function (int $days) {
$timestamp = Chronos::now()->subDays($days)->getTimestamp();
@@ -119,7 +115,7 @@ class GeolocationDbUpdaterTest extends TestCase
return map(range(0, 34), $generateParamsWithTimestamp);
}
/** @test */
#[Test]
public function exceptionIsThrownWhenCheckingExistingDatabaseWithInvalidBuildEpoch(): void
{
$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
{
$result = $this->geolocationDbUpdater($options)->checkDbUpdate();
@@ -164,7 +157,7 @@ class GeolocationDbUpdaterTest extends TestCase
self::assertEquals(GeolocationResult::CHECK_SKIPPED, $result);
}
public function provideTrackingOptions(): iterable
public static function provideTrackingOptions(): iterable
{
yield 'disableTracking' => [new TrackingOptions(disableTracking: true)];
yield 'disableIpTracking' => [new TrackingOptions(disableIpTracking: true)];

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Util;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Util\ProcessRunner;
@@ -34,7 +35,7 @@ class ProcessRunnerTest extends TestCase
$this->runner = new ProcessRunner($this->helper, fn () => $this->process);
}
/** @test */
#[Test]
public function noMessagesAreWrittenWhenOutputIsNotVerbose(): void
{
$this->output->expects($this->exactly(2))->method('isVeryVerbose')->with()->willReturn(false);
@@ -50,7 +51,7 @@ class ProcessRunnerTest extends TestCase
$this->runner->run($this->output, []);
}
/** @test */
#[Test]
public function someMessagesAreWrittenWhenOutputIsVerbose(): void
{
$this->output->expects($this->exactly(2))->method('isVeryVerbose')->with()->willReturn(true);
@@ -66,7 +67,7 @@ class ProcessRunnerTest extends TestCase
$this->runner->run($this->output, []);
}
/** @test */
#[Test]
public function wrapsCallbackWhenOutputIsDebug(): void
{
$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;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use ReflectionObject;
@@ -23,7 +24,7 @@ class ShlinkTableTest extends TestCase
$this->shlinkTable = ShlinkTable::fromBaseTable($this->baseTable);
}
/** @test */
#[Test]
public function renderMakesTableToBeRenderedWithProvidedInfo(): void
{
$headers = [];
@@ -43,7 +44,7 @@ class ShlinkTableTest extends TestCase
$this->shlinkTable->render($headers, $rows, $footerTitle, $headerTitle);
}
/** @test */
#[Test]
public function newTableIsCreatedForFactoryMethod(): void
{
$instance = ShlinkTable::default($this->createMock(OutputInterface::class));

View File

@@ -187,7 +187,7 @@ return [
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 $shortCode;
private Chronos $dateCreated;
/** @var Collection<int, Visit> */
private Collection $visits;
/** @var Collection<int, Visit> & Selectable */
private Collection & Selectable $visits;
/** @var Collection<string, DeviceLongUrl> */
private Collection $deviceLongUrls;
/** @var Collection<int, Tag> */
@@ -255,23 +255,19 @@ class ShortUrl extends AbstractEntity
public function mostRecentImportedVisitDate(): ?Chronos
{
/** @var Selectable $visits */
$visits = $this->visits;
$criteria = Criteria::create()->where(Criteria::expr()->eq('type', VisitType::IMPORTED))
->orderBy(['id' => 'DESC'])
->setMaxResults(1);
$visit = $this->visits->matching($criteria)->last();
/** @var Visit|false $visit */
$visit = $visits->matching($criteria)->last();
return $visit === false ? null : $visit->getDate();
return $visit instanceof Visit ? $visit->getDate() : null;
}
/**
* @param Collection<int, Visit> $visits
* @param Collection<int, Visit> & Selectable $visits
* @internal
*/
public function setVisits(Collection $visits): self
public function setVisits(Collection & Selectable $visits): self
{
$this->visits = $visits;
return $this;

View File

@@ -5,5 +5,11 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Model;
enum ShortUrlMode: string
{
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;
}
$value = $this->options->isLooselyMode() ? strtolower($value) : $value;
$value = $this->options->isLooseMode() ? strtolower($value) : $value;
return (match ($this->options->multiSegmentSlugsEnabled) {
true => trim(str_replace(' ', '-', $value), '/'),
false => str_replace([' ', '/'], '-', $value),

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Tag\Repository;
use Doctrine\DBAL\Query\QueryBuilder as NativeQueryBuilder;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
use Happyr\DoctrineSpecification\Spec;
@@ -45,7 +46,6 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
$orderDir = $filtering?->orderBy?->direction;
$orderMainQuery = $orderField !== null && OrderableField::isAggregateField($orderField);
$conn = $this->getEntityManager()->getConnection();
$subQb = $this->createQueryBuilder('t');
$subQb->select('t.id', 't.name');
@@ -53,15 +53,51 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
$subQb->orderBy('t.name', $orderDir ?? 'ASC')
->setMaxResults($filtering?->limit ?? PHP_INT_MAX)
->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;
if ($searchTerm !== null) {
$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;
$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');
$applyApiKeyToNativeQuery($apiKey, $allVisitsSubQuery);
$applyApiKeyToNativeQuery($apiKey, $nonBotVisitsSubQuery);
// 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.
@@ -71,29 +107,22 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
->select(
't.id_0 AS id',
'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 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
->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', 'visits', 'v', $nativeQb->expr()->eq('st.short_url_id', 'v.short_url_id'))
->leftJoin('st', 'visits', 'v2', $nativeQb->expr()->and( // @phpstan-ignore-line
$nativeQb->expr()->eq('st.short_url_id', 'v2.short_url_id'),
$nativeQb->expr()->eq('v2.potential_bot', $conn->quote('0')),
->leftJoin('t', '(' . $allVisitsSubQuery->getSQL() . ')', 'v', $nativeQb->expr()->eq('t.id_0', 'v.tag_id'))
->leftJoin('t', '(' . $nonBotVisitsSubQuery->getSQL() . ')', 'v2', $nativeQb->expr()->eq(
't.id_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
$apiKey?->mapRoles(static fn (Role $role, array $meta) => match ($role) {
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())),
),
});
$applyApiKeyToNativeQuery($apiKey, $nativeQb);
if ($orderMainQuery) {
$nativeQb
@@ -107,9 +136,9 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
$rsm = new ResultSetMappingBuilder($this->getEntityManager());
$rsm->addScalarResult('name', 'tag');
$rsm->addScalarResult('short_urls_count', 'shortUrlsCount');
$rsm->addScalarResult('visits', 'visits');
$rsm->addScalarResult('non_bot_visits', 'nonBotVisits');
$rsm->addScalarResult('short_urls_count', 'shortUrlsCount');
return map(
$this->getEntityManager()->createNativeQuery($nativeQb->getSQL(), $rsm)->getResult(),

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace ShlinkioApiTest\Shlink\Core\Action;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
use const ShlinkioTest\Shlink\ANDROID_USER_AGENT;
@@ -12,17 +14,14 @@ use const ShlinkioTest\Shlink\IOS_USER_AGENT;
class RedirectTest extends ApiTestCase
{
/**
* @test
* @dataProvider provideUserAgents
*/
#[Test, DataProvider('provideUserAgents')]
public function properRedirectHappensBasedOnUserAgent(?string $userAgent, string $expectedRedirect): void
{
$response = $this->callShortUrl('def456', $userAgent);
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 '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\Collection;
use PHPUnit\Framework\Attributes\Test;
use Shlinkio\Shlink\Core\Config\NotFoundRedirects;
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
use Shlinkio\Shlink\Core\Domain\Repository\DomainRepository;
@@ -26,7 +27,7 @@ class DomainRepositoryTest extends DatabaseTestCase
$this->repo = $this->getEntityManager()->getRepository(Domain::class);
}
/** @test */
#[Test]
public function expectedDomainsAreFoundWhenNoApiKeyIsInvolved(): void
{
$fooDomain = Domain::withAuthority('foo.com');
@@ -61,7 +62,7 @@ class DomainRepositoryTest extends DatabaseTestCase
self::assertTrue($this->repo->domainExists('detached.com'));
}
/** @test */
#[Test]
public function expectedDomainsAreFoundWhenApiKeyIsProvided(): void
{
$authorApiKey = ApiKey::fromMeta(ApiKeyMeta::withRoles(RoleDefinition::forAuthoredShortUrls()));

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkioDbTest\Shlink\Core\ShortUrl\Repository;
use PHPUnit\Framework\Attributes\Test;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
use Shlinkio\Shlink\Core\ShortUrl\Repository\CrawlableShortCodesQuery;
@@ -19,7 +20,7 @@ class CrawlableShortCodesQueryTest extends DatabaseTestCase
$this->query = new CrawlableShortCodesQuery($em, $em->getClassMetadata(ShortUrl::class));
}
/** @test */
#[Test]
public function invokingQueryReturnsExpectedResult(): void
{
$createShortUrl = fn (bool $crawlable) => ShortUrl::create(

View File

@@ -6,6 +6,7 @@ namespace ShlinkioDbTest\Shlink\Core\ShortUrl\Repository;
use Cake\Chronos\Chronos;
use Doctrine\Common\Collections\ArrayCollection;
use PHPUnit\Framework\Attributes\Test;
use ReflectionObject;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Model\Ordering;
@@ -37,7 +38,7 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
$this->relationResolver = new PersistenceShortUrlRelationResolver($em);
}
/** @test */
#[Test]
public function countListReturnsProperNumberOfResults(): void
{
$count = 5;
@@ -49,7 +50,7 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
self::assertEquals($count, $this->repo->countList(new ShortUrlsCountFiltering()));
}
/** @test */
#[Test]
public function findListProperlyFiltersResult(): void
{
$foo = ShortUrl::create(
@@ -143,7 +144,7 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
));
}
/** @test */
#[Test]
public function findListProperlyMapsFieldNamesToColumnNamesWhenOrdering(): void
{
$urls = ['a', 'z', 'c', 'b'];
@@ -164,7 +165,7 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
self::assertEquals('z', $result[3]->getLongUrl());
}
/** @test */
#[Test]
public function findListReturnsOnlyThoseWithMatchingTags(): void
{
$shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([
@@ -273,7 +274,7 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
));
}
/** @test */
#[Test]
public function findListReturnsOnlyThoseWithMatchingDomains(): void
{
$shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([
@@ -309,7 +310,7 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
self::assertCount(0, $this->repo->findList($buildFiltering('no results')));
}
/** @test */
#[Test]
public function findListReturnsOnlyThoseWithoutExcludedUrls(): void
{
$shortUrl1 = ShortUrl::create(ShortUrlCreation::fromRawData([

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace ShlinkioDbTest\Shlink\Core\ShortUrl\Repository;
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\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
@@ -31,7 +31,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
$this->relationResolver = new PersistenceShortUrlRelationResolver($this->getEntityManager());
}
/** @test */
#[Test]
public function findOneWithDomainFallbackReturnsProperData(): void
{
$regularOne = ShortUrl::create(ShortUrlCreation::fromRawData(['customSlug' => 'Foo', 'longUrl' => 'foo']));
@@ -55,19 +55,16 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
));
self::assertSame($regularOne, $this->repo->findOneWithDomainFallback(
ShortUrlIdentifier::fromShortCodeAndDomain('foo'),
ShortUrlMode::LOOSELY,
ShortUrlMode::LOOSE,
));
self::assertSame($regularOne, $this->repo->findOneWithDomainFallback(
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(
ShortUrlIdentifier::fromShortCodeAndDomain($withDomainDuplicatingRegular->getShortCode()),
ShortUrlMode::STRICT,
@@ -101,7 +98,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
));
}
/** @test */
#[Test]
public function shortCodeIsInUseLooksForShortUrlInProperSetOfTables(): void
{
$shortUrlWithoutDomain = ShortUrl::create(
@@ -130,7 +127,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
));
}
/** @test */
#[Test]
public function findOneLooksForShortUrlInProperSetOfTables(): void
{
$shortUrlWithoutDomain = ShortUrl::create(
@@ -157,7 +154,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
));
}
/** @test */
#[Test]
public function findOneMatchingReturnsNullForNonExistingShortUrls(): void
{
self::assertNull($this->repo->findOneMatching(ShortUrlCreation::fromRawData(['longUrl' => 'foobar'])));
@@ -172,7 +169,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
])));
}
/** @test */
#[Test]
public function findOneMatchingAppliesProperConditions(): void
{
$start = Chronos::parse('2020-03-05 20:18:30');
@@ -241,7 +238,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
);
}
/** @test */
#[Test]
public function findOneMatchingReturnsOldestOneWhenThereAreMultipleMatches(): void
{
$start = Chronos::parse('2020-03-05 20:18:30');
@@ -269,7 +266,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
self::assertNotSame($shortUrl3, $result);
}
/** @test */
#[Test]
public function findOneMatchingAppliesProvidedApiKeyConditions(): void
{
$start = Chronos::parse('2020-03-05 20:18:30');
@@ -395,7 +392,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
);
}
/** @test */
#[Test]
public function importedShortUrlsAreFoundWhenExpected(): void
{
$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;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Shlinkio\Shlink\Core\Tag\Entity\Tag;
use Shlinkio\Shlink\Core\Tag\Model\TagsParams;
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> $length
* @test
* @dataProvider provideFilters
*/
#[Test, DataProvider('provideFilters')]
public function expectedListOfTagsIsReturned(
?string $searchTerm,
?string $orderBy,
@@ -52,7 +53,7 @@ class TagsPaginatorAdapterTest extends DatabaseTestCase
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, 2, 10, ['baz', 'foo'], 4];

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
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\Model\Ordering;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
@@ -34,13 +36,13 @@ class TagRepositoryTest extends DatabaseTestCase
$this->relationResolver = new PersistenceShortUrlRelationResolver($this->getEntityManager());
}
/** @test */
#[Test]
public function deleteByNameDoesNothingWhenEmptyListIsProvided(): void
{
self::assertEquals(0, $this->repo->deleteByName([]));
}
/** @test */
#[Test]
public function allTagsWhichMatchNameAreDeleted(): void
{
$names = ['foo', 'bar', 'baz'];
@@ -54,10 +56,7 @@ class TagRepositoryTest extends DatabaseTestCase
self::assertEquals(2, $this->repo->deleteByName($toDelete));
}
/**
* @test
* @dataProvider provideFilters
*/
#[Test, DataProvider('provideFilters')]
public function properTagsInfoIsReturned(?TagsListFiltering $filtering, array $expectedList): void
{
$names = ['foo', 'bar', 'baz', 'another'];
@@ -109,7 +108,7 @@ class TagRepositoryTest extends DatabaseTestCase
}
}
public function provideFilters(): iterable
public static function provideFilters(): iterable
{
$defaultList = [
['another', 0, 0, 0],
@@ -221,7 +220,7 @@ class TagRepositoryTest extends DatabaseTestCase
]];
}
/** @test */
#[Test]
public function tagExistsReturnsExpectedResultBasedOnApiKey(): void
{
$domain = Domain::withAuthority('foo.com');

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
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\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
@@ -25,10 +27,7 @@ class VisitLocationRepositoryTest extends DatabaseTestCase
$this->repo = new VisitLocationRepository($em, $em->getClassMetadata(Visit::class));
}
/**
* @test
* @dataProvider provideBlockSize
*/
#[Test, DataProvider('provideBlockSize')]
public function findVisitsReturnsProperVisits(int $blockSize): void
{
$shortUrl = ShortUrl::createFake();
@@ -56,7 +55,7 @@ class VisitLocationRepositoryTest extends DatabaseTestCase
self::assertCount(6, [...$all]);
}
public function provideBlockSize(): iterable
public static function provideBlockSize(): iterable
{
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;
use Cake\Chronos\Chronos;
use PHPUnit\Framework\Attributes\Test;
use ReflectionObject;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
@@ -40,7 +41,7 @@ class VisitRepositoryTest extends DatabaseTestCase
$this->relationResolver = new PersistenceShortUrlRelationResolver($this->getEntityManager());
}
/** @test */
#[Test]
public function findVisitsByShortCodeReturnsProperData(): void
{
[$shortCode, $domain] = $this->createShortUrlsAndVisits();
@@ -89,7 +90,7 @@ class VisitRepositoryTest extends DatabaseTestCase
));
}
/** @test */
#[Test]
public function countVisitsByShortCodeReturnsProperData(): void
{
[$shortCode, $domain] = $this->createShortUrlsAndVisits();
@@ -126,7 +127,7 @@ class VisitRepositoryTest extends DatabaseTestCase
));
}
/** @test */
#[Test]
public function findVisitsByShortCodeReturnsProperDataWhenUsingAPiKeys(): void
{
$adminApiKey = ApiKey::create();
@@ -158,7 +159,7 @@ class VisitRepositoryTest extends DatabaseTestCase
));
}
/** @test */
#[Test]
public function findVisitsByTagReturnsProperData(): void
{
$foo = 'foo';
@@ -183,7 +184,7 @@ class VisitRepositoryTest extends DatabaseTestCase
)));
}
/** @test */
#[Test]
public function countVisitsByTagReturnsProperData(): void
{
$foo = 'foo';
@@ -205,7 +206,7 @@ class VisitRepositoryTest extends DatabaseTestCase
)));
}
/** @test */
#[Test]
public function findVisitsByDomainReturnsProperData(): void
{
$this->createShortUrlsAndVisits('s.test');
@@ -229,7 +230,7 @@ class VisitRepositoryTest extends DatabaseTestCase
)));
}
/** @test */
#[Test]
public function countVisitsByDomainReturnsProperData(): void
{
$this->createShortUrlsAndVisits('s.test');
@@ -253,7 +254,7 @@ class VisitRepositoryTest extends DatabaseTestCase
)));
}
/** @test */
#[Test]
public function countVisitsReturnsExpectedResultBasedOnApiKey(): void
{
$domain = Domain::withAuthority('foo.com');
@@ -316,7 +317,7 @@ class VisitRepositoryTest extends DatabaseTestCase
self::assertEquals(3, $this->repo->countOrphanVisits(new VisitsCountFiltering(null, true)));
}
/** @test */
#[Test]
public function findOrphanVisitsReturnsExpectedResult(): void
{
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => 'longUrl']));
@@ -365,7 +366,7 @@ class VisitRepositoryTest extends DatabaseTestCase
)));
}
/** @test */
#[Test]
public function countOrphanVisitsReturnsExpectedResult(): void
{
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData(['longUrl' => 'longUrl']));
@@ -402,7 +403,7 @@ class VisitRepositoryTest extends DatabaseTestCase
));
}
/** @test */
#[Test]
public function findNonOrphanVisitsReturnsExpectedResult(): void
{
$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)));
}
/** @test */
#[Test]
public function findMostRecentOrphanVisitReturnsExpectedVisit(): void
{
$this->assertNull($this->repo->findMostRecentOrphanVisit());

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Action;
use Laminas\Diactoros\ServerRequest;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Http\Server\RequestHandlerInterface;
@@ -29,7 +30,7 @@ class PixelActionTest extends TestCase
$this->action = new PixelAction($this->urlResolver, $this->requestTracker);
}
/** @test */
#[Test]
public function imageIsReturned(): void
{
$shortCode = 'abc123';

View File

@@ -7,6 +7,8 @@ namespace ShlinkioTest\Shlink\Core\Action;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\ServerRequest;
use Laminas\Diactoros\ServerRequestFactory;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ServerRequestInterface;
@@ -37,7 +39,7 @@ class QrCodeActionTest extends TestCase
$this->urlResolver = $this->createMock(ShortUrlResolverInterface::class);
}
/** @test */
#[Test]
public function aNotFoundShortCodeWillDelegateIntoNextMiddleware(): void
{
$shortCode = 'abc123';
@@ -50,7 +52,7 @@ class QrCodeActionTest extends TestCase
$this->action()->process((new ServerRequest())->withAttribute('shortCode', $shortCode), $delegate);
}
/** @test */
#[Test]
public function aCorrectRequestReturnsTheQrCodeResponse(): void
{
$shortCode = 'abc123';
@@ -66,10 +68,7 @@ class QrCodeActionTest extends TestCase
self::assertEquals(200, $resp->getStatusCode());
}
/**
* @test
* @dataProvider provideQueries
*/
#[Test, DataProvider('provideQueries')]
public function imageIsReturnedWithExpectedContentTypeBasedOnProvidedFormat(
string $defaultFormat,
array $query,
@@ -87,7 +86,7 @@ class QrCodeActionTest extends TestCase
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, 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'];
}
/**
* @test
* @dataProvider provideRequestsWithSize
*/
#[Test, DataProvider('provideRequestsWithSize')]
public function imageIsReturnedWithExpectedSize(
QrCodeOptions $defaultOptions,
ServerRequestInterface $req,
@@ -122,7 +118,7 @@ class QrCodeActionTest extends TestCase
self::assertEquals($expectedSize, $size);
}
public function provideRequestsWithSize(): iterable
public static function provideRequestsWithSize(): iterable
{
yield 'different margin and size defaults' => [
new QrCodeOptions(size: 660, margin: 40),
@@ -188,10 +184,7 @@ class QrCodeActionTest extends TestCase
];
}
/**
* @test
* @dataProvider provideRoundBlockSize
*/
#[Test, DataProvider('provideRoundBlockSize')]
public function imageCanRemoveExtraMarginWhenBlockRoundIsDisabled(
QrCodeOptions $defaultOptions,
?string $roundBlockSize,
@@ -215,7 +208,7 @@ class QrCodeActionTest extends TestCase
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, but disabled by default' => [

View File

@@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\Core\Action;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\ServerRequest;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Http\Server\RequestHandlerInterface;
@@ -44,7 +45,7 @@ class RedirectActionTest extends TestCase
);
}
/** @test */
#[Test]
public function redirectionIsPerformedToLongUrl(): void
{
$shortCode = 'abc123';
@@ -64,7 +65,7 @@ class RedirectActionTest extends TestCase
self::assertSame($expectedResp, $response);
}
/** @test */
#[Test]
public function nextMiddlewareIsInvokedIfLongUrlIsNotFound(): void
{
$shortCode = 'abc123';

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Action;
use Laminas\Diactoros\ServerRequestFactory;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Action\RobotsAction;
@@ -21,10 +23,7 @@ class RobotsActionTest extends TestCase
$this->action = new RobotsAction($this->helper);
}
/**
* @test
* @dataProvider provideShortCodes
*/
#[Test, DataProvider('provideShortCodes')]
public function buildsRobotsLinesFromCrawlableShortCodes(array $shortCodes, string $expected): void
{
$this->helper
@@ -39,7 +38,7 @@ class RobotsActionTest extends TestCase
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
# For more information about the robots.txt standard, see:

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Config;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Config\EmptyNotFoundRedirectConfig;
@@ -16,7 +17,7 @@ class EmptyNotFoundRedirectConfigTest extends TestCase
$this->redirectsConfig = new EmptyNotFoundRedirectConfig();
}
/** @test */
#[Test]
public function allMethodsReturnHardcodedValues(): void
{
self::assertNull($this->redirectsConfig->invalidShortUrlRedirect());

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Config;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Config\EnvVars;
@@ -23,16 +25,13 @@ class EnvVarsTest extends TestCase
putenv(EnvVars::DB_NAME->value . '=');
}
/**
* @test
* @dataProvider provideExistingEnvVars
*/
#[Test, DataProvider('provideExistingEnvVars')]
public function existsInEnvReturnsExpectedValue(EnvVars $envVar, bool $exists): void
{
self::assertEquals($exists, $envVar->existsInEnv());
}
public function provideExistingEnvVars(): iterable
public static function provideExistingEnvVars(): iterable
{
yield 'DB_NAME' => [EnvVars::DB_NAME, 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];
}
/**
* @test
* @dataProvider provideEnvVarsValues
*/
#[Test, DataProvider('provideEnvVarsValues')]
public function expectedValueIsLoadedFromEnv(EnvVars $envVar, mixed $expected, mixed $default): void
{
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 with default' => [EnvVars::DB_NAME, 'shlink', 'foobar'];

View File

@@ -9,11 +9,12 @@ use Laminas\Diactoros\ServerRequestFactory;
use Laminas\Diactoros\Uri;
use Mezzio\Router\Route;
use Mezzio\Router\RouteResult;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Log\NullLogger;
use Shlinkio\Shlink\Core\Action\RedirectAction;
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\Util\RedirectResponseHelperInterface;
use function Laminas\Stratigility\middleware;
class NotFoundRedirectResolverTest extends TestCase
{
private NotFoundRedirectResolver $resolver;
@@ -32,10 +35,7 @@ class NotFoundRedirectResolverTest extends TestCase
$this->resolver = new NotFoundRedirectResolver($this->helper, new NullLogger());
}
/**
* @test
* @dataProvider provideRedirects
*/
#[Test, DataProvider('provideRedirects')]
public function expectedRedirectionIsReturnedDependingOnTheCase(
UriInterface $uri,
NotFoundType $notFoundType,
@@ -52,47 +52,47 @@ class NotFoundRedirectResolverTest extends TestCase
self::assertSame($expectedResp, $resp);
}
public function provideRedirects(): iterable
public static function provideRedirects(): iterable
{
yield 'base URL with trailing slash' => [
$uri = new Uri('/'),
$this->notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
self::notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
new NotFoundRedirectOptions(baseUrl: 'baseUrl'),
'baseUrl',
];
yield 'base URL with domain placeholder' => [
$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}'),
'https://redirect-here.com/s.test',
];
yield 'base URL with domain placeholder in query' => [
$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}'),
'https://redirect-here.com/?domain=s.test',
];
yield 'base URL without trailing slash' => [
$uri = new Uri(''),
$this->notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
self::notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
new NotFoundRedirectOptions(baseUrl: 'baseUrl'),
'baseUrl',
];
yield 'regular 404' => [
$uri = new Uri('/foo/bar'),
$this->notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
self::notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
new NotFoundRedirectOptions(regular404: 'regular404'),
'regular404',
];
yield 'regular 404 with path placeholder in query' => [
$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}'),
'https://redirect-here.com/?path=%2Ffoo%2Fbar',
];
yield 'regular 404 with multiple placeholders' => [
$uri = new Uri('https://s.test/foo/bar'),
$this->notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
self::notFoundType(ServerRequestFactory::fromGlobals()->withUri($uri)),
new NotFoundRedirectOptions(
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' => [
new Uri('/foo'),
$this->notFoundType($this->requestForRoute(RedirectAction::class)),
self::notFoundType(self::requestForRoute(RedirectAction::class)),
new NotFoundRedirectOptions(invalidShortUrl: 'invalidShortUrl'),
'invalidShortUrl',
];
yield 'invalid short URL with path placeholder' => [
new Uri('/foo'),
$this->notFoundType($this->requestForRoute(RedirectAction::class)),
self::notFoundType(self::requestForRoute(RedirectAction::class)),
new NotFoundRedirectOptions(invalidShortUrl: 'https://redirect-here.com/{ORIGINAL_PATH}'),
'https://redirect-here.com/foo',
];
}
/** @test */
#[Test]
public function noResponseIsReturnedIfNoConditionsMatch(): void
{
$notFoundType = $this->notFoundType($this->requestForRoute('foo'));
$notFoundType = self::notFoundType(self::requestForRoute('foo'));
$this->helper->expects($this->never())->method('buildRedirectResponse');
$result = $this->resolver->resolveRedirectResponse($notFoundType, new NotFoundRedirectOptions(), new Uri());
@@ -123,12 +123,12 @@ class NotFoundRedirectResolverTest extends TestCase
self::assertNull($result);
}
private function notFoundType(ServerRequestInterface $req): NotFoundType
private static function notFoundType(ServerRequestInterface $req): NotFoundType
{
return NotFoundType::fromRequest($req, '');
}
private function requestForRoute(string $routeName): ServerRequestInterface
private static function requestForRoute(string $routeName): ServerRequestInterface
{
return ServerRequestFactory::fromGlobals()
->withAttribute(
@@ -136,7 +136,8 @@ class NotFoundRedirectResolverTest extends TestCase
RouteResult::fromRoute(
new Route(
'foo',
$this->createMock(MiddlewareInterface::class),
middleware(function (): void {
}),
['GET'],
$routeName,
),

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Config\PostProcessor;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Config\PostProcessor\BasePathPrefixer;
@@ -16,10 +18,7 @@ class BasePathPrefixerTest extends TestCase
$this->prefixer = new BasePathPrefixer();
}
/**
* @test
* @dataProvider provideConfig
*/
#[Test, DataProvider('provideConfig')]
public function parsesConfigAsExpected(
array $originalConfig,
array $expectedRoutes,
@@ -31,7 +30,7 @@ class BasePathPrefixerTest extends TestCase
self::assertEquals($expectedMiddlewares, $middlewares);
}
public function provideConfig(): iterable
public static function provideConfig(): iterable
{
yield 'with empty options' => [['routes' => []], [], []];
yield 'with non-empty options' => [

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Config\PostProcessor;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Config\PostProcessor\MultiSegmentSlugProcessor;
@@ -16,16 +18,13 @@ class MultiSegmentSlugProcessorTest extends TestCase
$this->processor = new MultiSegmentSlugProcessor();
}
/**
* @test
* @dataProvider provideConfigs
*/
#[Test, DataProvider('provideConfigs')]
public function parsesRoutesAsExpected(array $config, array $expectedRoutes): void
{
self::assertEquals($expectedRoutes, ($this->processor)($config)['routes'] ?? []);
}
public function provideConfigs(): iterable
public static function provideConfigs(): iterable
{
yield [[], []];
yield [['url_shortener' => []], []];

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Config\PostProcessor;
use Mezzio\Router\Route;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Action\RedirectAction;
use Shlinkio\Shlink\Core\Config\PostProcessor\ShortUrlMethodsProcessor;
@@ -18,10 +20,7 @@ class ShortUrlMethodsProcessorTest extends TestCase
$this->processor = new ShortUrlMethodsProcessor();
}
/**
* @test
* @dataProvider provideConfigs
*/
#[Test, DataProvider('provideConfigs')]
public function onlyFirstRouteIdentifiedAsRedirectIsEditedWithProperAllowedMethods(
array $config,
?array $expectedRoutes,
@@ -29,7 +28,7 @@ class ShortUrlMethodsProcessorTest extends TestCase
self::assertEquals($expectedRoutes, ($this->processor)($config)['routes'] ?? null);
}
public function provideConfigs(): iterable
public static function provideConfigs(): iterable
{
$buildConfigWithStatus = static fn (int $status, ?array $expectedAllowedMethods) => [[
'routes' => [

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core;
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\ConfigProvider;
@@ -17,7 +18,7 @@ class ConfigProviderTest extends TestCase
$this->configProvider = new ConfigProvider();
}
/** @test */
#[Test]
public function properConfigIsReturned(): void
{
$config = ($this->configProvider)();

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Crawling;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Crawling\CrawlingHelper;
@@ -20,7 +21,7 @@ class CrawlingHelperTest extends TestCase
$this->helper = new CrawlingHelper($this->query);
}
/** @test */
#[Test]
public function listCrawlableShortCodesDelegatesIntoRepository(): void
{
$this->query->expects($this->once())->method('__invoke')->willReturn([]);

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Domain;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Config\EmptyNotFoundRedirectConfig;
@@ -29,10 +31,7 @@ class DomainServiceTest extends TestCase
$this->domainService = new DomainService($this->em, 'default.com');
}
/**
* @test
* @dataProvider provideExcludedDomains
*/
#[Test, DataProvider('provideExcludedDomains')]
public function listDomainsDelegatesIntoRepository(array $domains, array $expectedResult, ?ApiKey $apiKey): void
{
$repo = $this->createMock(DomainRepositoryInterface::class);
@@ -44,7 +43,7 @@ class DomainServiceTest extends TestCase
self::assertEquals($expectedResult, $result);
}
public function provideExcludedDomains(): iterable
public static function provideExcludedDomains(): iterable
{
$default = DomainItem::forDefaultDomain('default.com', new EmptyNotFoundRedirectConfig());
$adminApiKey = ApiKey::create();
@@ -102,7 +101,7 @@ class DomainServiceTest extends TestCase
];
}
/** @test */
#[Test]
public function getDomainThrowsExceptionWhenDomainIsNotFound(): void
{
$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');
}
/** @test */
#[Test]
public function getDomainReturnsEntityWhenFound(): void
{
$domain = Domain::withAuthority('');
@@ -123,10 +122,7 @@ class DomainServiceTest extends TestCase
self::assertSame($domain, $result);
}
/**
* @test
* @dataProvider provideFoundDomains
*/
#[Test, DataProvider('provideFoundDomains')]
public function getOrCreateAlwaysPersistsDomain(?Domain $foundDomain, ?ApiKey $apiKey): void
{
$authority = 'example.com';
@@ -145,7 +141,7 @@ class DomainServiceTest extends TestCase
}
}
/** @test */
#[Test]
public function getOrCreateThrowsExceptionForApiKeysWithDomainRole(): void
{
$authority = 'example.com';
@@ -163,10 +159,7 @@ class DomainServiceTest extends TestCase
$this->domainService->getOrCreate($authority, $apiKey);
}
/**
* @test
* @dataProvider provideFoundDomains
*/
#[Test, DataProvider('provideFoundDomains')]
public function configureNotFoundRedirectsConfiguresFetchedDomain(?Domain $foundDomain, ?ApiKey $apiKey): void
{
$authority = 'example.com';
@@ -190,7 +183,7 @@ class DomainServiceTest extends TestCase
self::assertEquals('baz.com', $result->invalidShortUrlRedirect());
}
public function provideFoundDomains(): iterable
public static function provideFoundDomains(): iterable
{
$domain = Domain::withAuthority('');
$adminApiKey = ApiKey::create();

View File

@@ -6,6 +6,9 @@ namespace ShlinkioTest\Shlink\Core\ErrorHandler;
use Laminas\Diactoros\Response;
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\TestCase;
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
{
$expectedResp = new Response();
@@ -58,44 +58,43 @@ class NotFoundRedirectHandlerTest extends TestCase
self::assertSame($expectedResp, $result);
}
public function provideNonRedirectScenarios(): iterable
public static function provideNonRedirectScenarios(): iterable
{
yield 'no domain' => [function (
MockObject&DomainServiceInterface $domainService,
MockObject&NotFoundRedirectResolverInterface $resolver,
): void {
$domainService->expects($this->once())->method('findByAuthority')->withAnyParameters()->willReturn(
$domainService->expects(self::once())->method('findByAuthority')->withAnyParameters()->willReturn(
null,
);
$resolver->expects($this->once())->method('resolveRedirectResponse')->with(
$this->isInstanceOf(NotFoundType::class),
$this->isInstanceOf(NotFoundRedirectOptions::class),
$this->isInstanceOf(UriInterface::class),
$resolver->expects(self::once())->method('resolveRedirectResponse')->with(
self::isInstanceOf(NotFoundType::class),
self::isInstanceOf(NotFoundRedirectOptions::class),
self::isInstanceOf(UriInterface::class),
)->willReturn(null);
}];
yield 'non-redirecting domain' => [function (
MockObject&DomainServiceInterface $domainService,
MockObject&NotFoundRedirectResolverInterface $resolver,
): void {
$domainService->expects($this->once())->method('findByAuthority')->withAnyParameters()->willReturn(
$domainService->expects(self::once())->method('findByAuthority')->withAnyParameters()->willReturn(
Domain::withAuthority(''),
);
$resolver->expects($this->exactly(2))->method('resolveRedirectResponse')->withConsecutive(
[
$this->isInstanceOf(NotFoundType::class),
$this->isInstanceOf(Domain::class),
$this->isInstanceOf(UriInterface::class),
],
[
$this->isInstanceOf(NotFoundType::class),
$this->isInstanceOf(NotFoundRedirectOptions::class),
$this->isInstanceOf(UriInterface::class),
],
)->willReturn(null);
$callCount = 0;
$resolver->expects(self::exactly(2))->method('resolveRedirectResponse')->willReturnCallback(
function (mixed $arg1, mixed $arg2, mixed $arg3) use (&$callCount) {
Assert::assertInstanceOf(NotFoundType::class, $arg1);
Assert::assertInstanceOf($callCount === 0 ? Domain::class : NotFoundRedirectOptions::class, $arg2);
Assert::assertInstanceOf(UriInterface::class, $arg3);
$callCount++;
return null;
},
);
}];
}
/** @test */
#[Test]
public function globalRedirectIsUsedIfDomainRedirectIsNotFound(): void
{
$expectedResp = new Response();
@@ -113,7 +112,7 @@ class NotFoundRedirectHandlerTest extends TestCase
self::assertSame($expectedResp, $result);
}
/** @test */
#[Test]
public function domainRedirectIsUsedIfFound(): void
{
$expectedResp = new Response();

View File

@@ -9,13 +9,16 @@ use Laminas\Diactoros\ServerRequestFactory;
use Laminas\Diactoros\Uri;
use Mezzio\Router\Route;
use Mezzio\Router\RouteResult;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Shlinkio\Shlink\Core\Action\RedirectAction;
use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType;
use Shlinkio\Shlink\Core\ErrorHandler\NotFoundTemplateHandler;
use function Laminas\Stratigility\middleware;
class NotFoundTemplateHandlerTest extends TestCase
{
private NotFoundTemplateHandler $handler;
@@ -31,10 +34,7 @@ class NotFoundTemplateHandlerTest extends TestCase
$this->handler = new NotFoundTemplateHandler($readFile);
}
/**
* @test
* @dataProvider provideTemplates
*/
#[Test, DataProvider('provideTemplates')]
public function properErrorTemplateIsRendered(ServerRequestInterface $request, string $expectedTemplate): void
{
$resp = $this->handler->handle($request->withHeader('Accept', 'text/html'));
@@ -44,19 +44,20 @@ class NotFoundTemplateHandlerTest extends TestCase
self::assertTrue($this->readFileCalled);
}
public function provideTemplates(): iterable
public static function provideTemplates(): iterable
{
$request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/foo'));
yield 'base url' => [$this->withNotFoundType($request, '/foo'), NotFoundTemplateHandler::NOT_FOUND_TEMPLATE];
yield 'regular not found' => [$this->withNotFoundType($request), NotFoundTemplateHandler::NOT_FOUND_TEMPLATE];
yield 'base url' => [self::withNotFoundType($request, '/foo'), NotFoundTemplateHandler::NOT_FOUND_TEMPLATE];
yield 'regular not found' => [self::withNotFoundType($request), NotFoundTemplateHandler::NOT_FOUND_TEMPLATE];
yield 'invalid short code' => [
$this->withNotFoundType($request->withAttribute(
self::withNotFoundType($request->withAttribute(
RouteResult::class,
RouteResult::fromRoute(
new Route(
'foo',
$this->createMock(MiddlewareInterface::class),
middleware(function (): void {
}),
['GET'],
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);
return $req->withAttribute(NotFoundType::class, $type);

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\ErrorHandler;
use Laminas\Diactoros\ServerRequestFactory;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ServerRequestInterface;
@@ -32,7 +33,7 @@ class NotFoundTrackerMiddlewareTest extends TestCase
);
}
/** @test */
#[Test]
public function delegatesIntoRequestTracker(): void
{
$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\ServerRequestFactory;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ServerRequestInterface;
@@ -25,7 +26,7 @@ class NotFoundTypeResolverMiddlewareTest extends TestCase
$this->handler = $this->createMock(RequestHandlerInterface::class);
}
/** @test */
#[Test]
public function notFoundTypeIsAddedToRequest(): void
{
$request = ServerRequestFactory::fromGlobals();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,8 @@ use GuzzleHttp\Promise\FulfilledPromise;
use GuzzleHttp\Promise\RejectedPromise;
use GuzzleHttp\RequestOptions;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
@@ -41,7 +43,7 @@ class NotifyVisitToWebHooksTest extends TestCase
$this->logger = $this->createMock(LoggerInterface::class);
}
/** @test */
#[Test]
public function emptyWebhooksMakeNoFurtherActions(): void
{
$this->em->expects($this->never())->method('find');
@@ -49,7 +51,7 @@ class NotifyVisitToWebHooksTest extends TestCase
$this->createListener([])(new VisitLocated('1'));
}
/** @test */
#[Test]
public function invalidVisitDoesNotPerformAnyRequest(): void
{
$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'));
}
/** @test */
#[Test]
public function orphanVisitDoesNotPerformAnyRequestWhenDisabled(): void
{
$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'));
}
/**
* @test
* @dataProvider provideVisits
*/
#[Test, DataProvider('provideVisits')]
public function expectedRequestsArePerformedToWebhooks(Visit $visit, array $expectedResponseKeys): void
{
$webhooks = ['foo', 'invalid', 'bar', 'baz'];
@@ -120,13 +119,13 @@ class NotifyVisitToWebHooksTest extends TestCase
$this->createListener($webhooks)(new VisitLocated('1'));
}
public function provideVisits(): iterable
public static function provideVisits(): iterable
{
yield 'regular visit' => [
Visit::forValidShortUrl(ShortUrl::createFake(), Visitor::emptyInstance()),
['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

View File

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

View File

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

View File

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

View File

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

View File

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

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