Compare commits

...

137 Commits

Author SHA1 Message Date
Alejandro Celaya
f563e777cc Merge pull request #369 from acelaya/feature/postgres-query-error
Feature/postgres query error
2019-03-05 14:26:36 +01:00
Alejandro Celaya
a63447b12b Updated changelog 2019-03-05 14:17:47 +01:00
Alejandro Celaya
0f81c3ab92 Fixed error when using postgres in a SELECT count query where a ORDER BY was added by mistake 2019-03-05 13:50:44 +01:00
Alejandro Celaya
425f254453 Added posgres container for development 2019-03-05 13:39:45 +01:00
Alejandro Celaya
a9d9ec5bf9 Merge pull request #365 from acelaya/feature/coding-styles
Feature/coding styles
2019-02-26 23:06:08 +01:00
Alejandro Celaya
0c5c752ffe Updated changelog 2019-02-26 22:58:03 +01:00
Alejandro Celaya
4b556cd79f Updated to shlinkio coding standard 1.1.0 2019-02-26 22:56:43 +01:00
Alejandro Celaya
3d32a90f8e Merge pull request #364 from acelaya/bugfix/non-locatable-addresses
Bugfix/non locatable addresses
2019-02-26 22:53:07 +01:00
Alejandro Celaya
0b4c334163 Fixed typo 2019-02-26 22:42:33 +01:00
Alejandro Celaya
312fc0984b Fixed mutation score by provideing more tests 2019-02-26 22:41:04 +01:00
Alejandro Celaya
30bf1c2641 Added tests for new cases with non-locatable addresses 2019-02-26 22:31:07 +01:00
Alejandro Celaya
2d1d7357a3 Given more semantic cases in which a visit cannot be located 2019-02-26 21:39:45 +01:00
Alejandro Celaya
c70077c525 Merge pull request #361 from acelaya/feature/paginated-visits
Feature/paginated visits
2019-02-23 10:09:46 +01:00
Alejandro Celaya
d2fad0128f Fixed bug missing unprocessed visits while iterating and updating, while drastically improving the performance 2019-02-23 09:58:02 +01:00
Alejandro Celaya
62133c994f Tagged v1.16 in changelog 2019-02-23 08:30:35 +01:00
Alejandro Celaya
091ea974eb Simplified implementation iterating unlocated visits 2019-02-23 07:29:07 +01:00
Alejandro Celaya
955ae00036 Updated changelog 2019-02-22 19:54:23 +01:00
Alejandro Celaya
7d4de590e5 Created ImplicitLoopPaginatorTest 2019-02-22 19:53:10 +01:00
Alejandro Celaya
292937b962 Updated VisitRepository::findUnlocatedVisits methods so that it paginates the amount of elements loaded in memory 2019-02-22 19:31:03 +01:00
Alejandro Celaya
08bd4f131c Merge pull request #359 from acelaya/feature/memory-leak
Feature/memory leak
2019-02-20 18:09:00 +01:00
Alejandro Celaya
38cc83a4ee Removed uneeded inline type hints 2019-02-17 20:32:18 +01:00
Alejandro Celaya
687a1cc9c7 Reduced amount of dead lines in tests 2019-02-17 20:28:34 +01:00
Alejandro Celaya
1bcd03b150 Renamed method 2019-02-17 13:21:07 +01:00
Alejandro Celaya
e2abe23895 Defined stricter model to represent one geo location 2019-02-17 13:01:21 +01:00
Alejandro Celaya
5c5dde48de Ensured install and update script change to the project dir 2019-02-17 10:51:22 +01:00
Alejandro Celaya
d9f11e190f Merge pull request #357 from acelaya/feature/phpstan0.11
Feature/phpstan0.11
2019-02-17 10:19:14 +01:00
Alejandro Celaya
1ab2d7a240 Increased scrutinizer timeout while waiting for code coverage, from 5 min to 10 min 2019-02-17 10:12:13 +01:00
Alejandro Celaya
580050cb7d Updated to phpstan 0.11 2019-02-17 10:06:34 +01:00
Alejandro Celaya
eab5659163 Added status codes returned by CLI commands 2019-02-16 23:21:40 +01:00
Alejandro Celaya
397b350cfc Merge pull request #356 from acelaya/feature/deprecated-commands
Deprecated commands to generate secret and charset
2019-02-16 23:20:55 +01:00
Alejandro Celaya
c0130c997a Deprecated commands to generate secret and charset 2019-02-16 22:53:49 +01:00
Alejandro Celaya
fd7f1b32dd Merge pull request #354 from acelaya/feature/infection
Feature/infection
2019-02-16 22:25:13 +01:00
Alejandro Celaya
0e286d8261 Temporarely downgrading phpstan 2019-02-16 22:17:01 +01:00
Alejandro Celaya
ce7d2d1fb0 Fixed coding styles 2019-02-16 22:04:11 +01:00
Alejandro Celaya
2175b8a7bb Improved tests to increase MSI to 70% 2019-02-16 21:58:14 +01:00
Alejandro Celaya
6c0893cdf8 Improved tests to increase MSI to 69% 2019-02-16 21:24:32 +01:00
Alejandro Celaya
25927a296d Merge pull request #353 from acelaya/feature/testing-tools
Updated testing tools
2019-02-16 20:50:22 +01:00
Alejandro Celaya
ee4db44fe8 Fixed phpcov dep not properly resolved on PHP 7.1 envs 2019-02-16 20:39:46 +01:00
Alejandro Celaya
b8cb38ae5c Updated testing tools 2019-02-16 10:53:45 +01:00
Alejandro Celaya
899bfdce2b Merge pull request #352 from acelaya/feature/configure-installer
Updated required shlink-installer version and added config for instal…
2019-02-10 22:05:03 +01:00
Alejandro Celaya
456960e1f0 Updated required shlink-installer version and added config for installer plugins 2019-02-10 21:57:29 +01:00
Alejandro Celaya
04e03e9b6e Merge pull request #348 from acelaya/feature/external-installer
Feature/external installer
2019-02-06 23:56:10 +01:00
Alejandro Celaya
a7283da016 Updated changelog 2019-02-06 23:31:37 +01:00
Alejandro Celaya
672321abab Removed class which is now part of the installer package 2019-02-04 20:16:29 +01:00
Alejandro Celaya
2059b4050b Removed installer and used external package instead 2019-02-04 20:14:59 +01:00
Alejandro Celaya
171b43c517 Fixed some configs 2019-02-04 19:43:21 +01:00
Alejandro Celaya
ccb7c8f8d9 Merge pull request #345 from acelaya/bugfix/charset-installation
Bugfix/charset installation
2019-02-03 13:19:23 +01:00
Alejandro Celaya
abbc66ac07 Unified config for installer tool 2019-02-03 13:12:17 +01:00
Alejandro Celaya
2d18ef5cee Updated installer so that it no longer asks for a charset and instead just generates one 2019-02-03 13:02:12 +01:00
Alejandro Celaya
79c132219b Merge pull request #343 from acelaya/feature/allow-check-duplicates
Feature/allow check duplicates
2019-02-03 12:22:22 +01:00
Alejandro Celaya
04d4d4a8d7 Updated GenerateShortUrlCommand to accept the findIfExists flag 2019-02-03 12:11:22 +01:00
Alejandro Celaya
a918113ba0 Documented new findIfExists flag 2019-02-03 11:24:26 +01:00
Alejandro Celaya
810b25ff14 Added API tests covering creating short URLs with new findIfExists param 2019-02-03 11:01:38 +01:00
Alejandro Celaya
c4fd8d5120 Implemented feature to optionally return an existing short url when all provided params match an existing one 2019-02-03 09:40:32 +01:00
Alejandro Celaya
772494f46f Moved process of sluggifying custom slug to a filter 2019-02-03 08:17:27 +01:00
Alejandro Celaya
594e7da256 Created new findIfExists meta param 2019-02-02 11:05:28 +01:00
Alejandro Celaya
49668547d7 Fixed typo 2019-02-02 11:05:28 +01:00
Alejandro Celaya
4c46aaead8 Improved API tests and added test for short URLs creation 2019-02-02 11:05:28 +01:00
Alejandro Celaya
d61f5faf59 Refactored UrlShortener public method to receibe DTOs instead of primitive params 2019-02-02 11:05:28 +01:00
Alejandro Celaya
5756609531 Deleted deprecated constant 2019-02-02 11:05:28 +01:00
Alejandro Celaya
ea1b285d52 Little refactopring on tests config file 2019-02-02 11:05:28 +01:00
Alejandro Celaya
bc61b55b94 Merge pull request #344 from acelaya/feature/update-dev-docker
Updated docker containers used in development
2019-02-02 11:04:39 +01:00
Alejandro Celaya
48f6a96da8 Updated docker containers used in development 2019-02-02 10:53:34 +01:00
Alejandro Celaya
967f1657d2 Merge pull request #340 from acelaya/bugfix/preview-error
Bugfix/preview error
2019-01-28 11:00:23 +01:00
Alejandro Celaya
f90a323374 Updated changelog 2019-01-28 10:53:24 +01:00
Alejandro Celaya
d289c62532 Fixed config file being deleted by mistake by build script 2019-01-28 10:52:05 +01:00
Alejandro Celaya
05695e8cd6 Merge pull request #339 from acelaya/feature/api-test
Feature/api test
2019-01-27 12:49:59 +01:00
Alejandro Celaya
d6a7a6ce66 Created new API test 2019-01-27 12:36:22 +01:00
Alejandro Celaya
05c7672de3 Improved API tests by adding fixtures 2019-01-27 12:14:22 +01:00
Alejandro Celaya
ce515767ce Updated changelog 2019-01-27 10:56:14 +01:00
Alejandro Celaya
76d8fd1023 Improved how API tests are executed 2019-01-27 10:54:04 +01:00
Alejandro Celaya
558e259b84 Minor refactorings 2019-01-27 10:30:38 +01:00
Alejandro Celaya
f467bed24c Used multiple commands with && instead of composer array for API tests command 2019-01-27 10:15:48 +01:00
Alejandro Celaya
fa753ad6fb Added api test to test:ci command 2019-01-26 11:04:50 +01:00
Alejandro Celaya
22d61fead7 Prepared configs for API tests 2019-01-26 10:19:20 +01:00
Alejandro Celaya
c4af1471f0 Simplified and united configs for tests 2019-01-26 09:42:08 +01:00
Alejandro Celaya
87ba7a7179 Updated structure for tests config files 2019-01-26 09:09:57 +01:00
Alejandro Celaya
e7c5cf0846 Merge pull request #337 from acelaya/feature/db-tests
Feature/db tests
2019-01-21 21:34:30 +01:00
Alejandro Celaya
1aaedb8d90 Udated changelog 2019-01-21 21:27:16 +01:00
Alejandro Celaya
284de28f76 Removed duplicated code to define testing database connection params 2019-01-20 22:08:32 +01:00
Alejandro Celaya
687d8d91a9 Changed references to functional tests by database tests 2019-01-20 21:49:07 +01:00
Alejandro Celaya
771087c6c6 Happy 2019! 2019-01-05 08:41:48 +01:00
Alejandro Celaya
1fd3e6365e Merge pull request #331 from acelaya/feature/health
Feature/health
2018-12-29 14:51:08 +01:00
Alejandro Celaya
28989296eb Updated changelog 2018-12-29 14:45:20 +01:00
Alejandro Celaya
fd8d73af38 Documented health endpoint 2018-12-29 14:39:31 +01:00
Alejandro Celaya
144a5415da Handled connection exceptions in Health action 2018-12-29 13:50:42 +01:00
Alejandro Celaya
d58e24bce5 Created health action related tests 2018-12-29 11:54:28 +01:00
Alejandro Celaya
0f86123ccb Finished health action implementation 2018-12-29 11:54:28 +01:00
Alejandro Celaya
3f65ef998c Created HealthAction 2018-12-29 11:54:28 +01:00
Alejandro Celaya
29d49dfbf4 Merge pull request #332 from acelaya/feature/php7.3
Do not allow failures on PHP 7.3 build
2018-12-29 11:53:44 +01:00
Alejandro Celaya
701d17f6f2 Do not allow failures on PHP 7.3 build 2018-12-29 11:43:28 +01:00
Alejandro Celaya
642431c43e Reverted to diactoros v2.0.1 while a bug is fixed 2018-12-29 11:13:23 +01:00
Alejandro Celaya
3c5b47784d Merge pull request #329 from PeterDaveHello/Update-Travis-CI-Config
Drop deprecated Travis CI container-based env config
2018-12-29 09:36:02 +01:00
Peter Dave Hello
64d7fe8bbf Drop deprecated Travis CI container-based env config
Ref: https://blog.travis-ci.com/2018-10-04-combining-linux-infrastructures
2018-12-29 16:20:18 +08:00
Alejandro Celaya
32070b1fa7 Do not use ServerRequestFactory::fromGlobals in tests 2018-12-25 23:19:36 +01:00
Alejandro Celaya
8b3324e143 Merge pull request #327 from PeterDaveHello/Add-Table-of-Contents
Add Table of Contents in README.md
2018-12-25 10:52:21 +01:00
Peter Dave Hello
f40a5a029c Add Table of Contents in README.md 2018-12-25 14:12:16 +08:00
Alejandro Celaya
eac82a602c Merge pull request #325 from acelaya/feature/dql
Feature/dql
2018-12-19 17:47:10 +01:00
Alejandro Celaya
d1312e0934 Added unreleased changes to changelog 2018-12-19 14:37:47 +01:00
Alejandro Celaya
58dbee10c5 Used DQL for non-dynamic query in VisitRepository 2018-12-19 14:36:03 +01:00
Alejandro Celaya
f8207994dc Removed superfluous method docs 2018-12-19 14:31:52 +01:00
Alejandro Celaya
2030401859 Migrated non-dynamic query to DQL in ShortUrlRepository 2018-12-19 14:29:43 +01:00
Alejandro Celaya
8966cf9910 Merge pull request #323 from acelaya/feature/docker-build
Feature/docker build
2018-12-16 19:40:32 +01:00
Alejandro Celaya
4eb4df9ca2 Updated changelog 2018-12-16 13:19:44 +01:00
Alejandro Celaya
32861b1c72 Added new travis deployment which will build the docker image 2018-12-16 13:19:17 +01:00
Alejandro Celaya
7248ca2e9b Merge pull request #324 from acelaya/feature/entities-config
Moved entities mappings from annotations to external config files
2018-12-16 13:18:19 +01:00
Alejandro Celaya
a6ec93f883 Updated changelog 2018-12-16 12:14:13 +01:00
Alejandro Celaya
a28c1d17c5 Moved entities mappings from annotations to external config files 2018-12-16 12:08:03 +01:00
Alejandro Celaya
fb705b44a4 Merge pull request #318 from acelaya/feature/document-non-rest
Feature/document non rest
2018-12-09 16:35:59 +01:00
Alejandro Celaya
a32bab9fd0 Updated changelog 2018-12-09 16:25:24 +01:00
Alejandro Celaya
6396e7f964 Added other of non-rest endpoints 2018-12-09 15:43:56 +01:00
Alejandro Celaya
c898cef277 Documented first non-rest endpoint 2018-12-09 15:18:10 +01:00
Alejandro Celaya
baeba54b06 Merge branch 'master' of github.com:shlinkio/shlink 2018-12-09 14:22:40 +01:00
Alejandro Celaya
f5ee5bf7fb Documented that swoole server needs to be restarted when it is being used ot serve shlink 2018-12-09 14:22:21 +01:00
Alejandro Celaya
73605414f9 Merge pull request #316 from acelaya/feature/symfony-42
Feature/symfony 42
2018-12-08 14:30:20 +01:00
Alejandro Celaya
6045c371e1 Updated changelog 2018-12-08 14:12:50 +01:00
Alejandro Celaya
97a9289d5f Created ShlinkTableTest 2018-12-08 14:11:14 +01:00
Alejandro Celaya
1983fc9b67 Added current page message in list short urls CLI command 2018-12-08 12:16:39 +01:00
Alejandro Celaya
bb40d84212 Used ShlinkTable on every location rendering a CLI table 2018-12-08 12:12:11 +01:00
Alejandro Celaya
46a35c553e Created class to wrap CLI table rendering behavior 2018-12-08 11:32:16 +01:00
Alejandro Celaya
080943e810 Updated how Symfony commands are used to fulfill API from v4.2 2018-12-08 10:34:04 +01:00
Alejandro Celaya
62fb3863c6 Merge pull request #315 from acelaya/feature/config
Updated how config is imported and merged, so that it includes any co…
2018-12-07 20:56:57 +01:00
Alejandro Celaya
2db03a163d Updated how config is imported and merged, so that it includes any config file in json format from config/params dir 2018-12-07 20:48:20 +01:00
Alejandro Celaya
9e3dd82efe Merge pull request #314 from acelaya/feature/fix-context
Feature/fix context
2018-12-07 20:30:53 +01:00
Alejandro Celaya
9f1989bfef Updated changelog 2018-12-07 19:49:17 +01:00
Alejandro Celaya
c0bdd8fc77 Removed concept of execution context and piped CloseDbConnectionMiddleware always 2018-12-07 19:46:46 +01:00
Alejandro Celaya
8a23c90e46 Merge pull request #313 from acelaya/feature/favicon
Feature/favicon
2018-12-07 09:45:55 +01:00
Alejandro Celaya
9095e5b057 Enabled static files with swoole, otherwise, robots.txt and favicon.ico are never served when running shlink with swoole 2018-12-07 09:38:07 +01:00
Alejandro Celaya
52c18115af Updated changelog 2018-12-07 09:18:56 +01:00
Alejandro Celaya
737137b19f Added favicon 2018-12-07 09:17:31 +01:00
Alejandro Celaya
7b78bee135 Merge pull request #311 from acelaya/feature/improvements
Feature/improvements
2018-12-07 02:55:27 +01:00
Alejandro Celaya
accda36a7b Updated default secret_key value 2018-12-07 02:49:50 +01:00
Alejandro Celaya
69dd9eb067 Updated readme mentioning docker image 2018-12-07 02:41:06 +01:00
Alejandro Celaya
a562bc661d Improved CacheFactory class 2018-12-06 21:05:11 +01:00
Alejandro Celaya
258f12f684 Merge pull request #303 from acelaya/feature/expressive-swoole-2.2
Feature/expressive swoole 2.2
2018-12-05 21:46:48 +01:00
Alejandro Celaya
4dc8d77a5a Updated changelog 2018-12-05 21:29:16 +01:00
Alejandro Celaya
7c5825d1bc Removed custom AccessLogFactory by updating to zend-expressive-swoole 2.2 2018-12-05 21:26:19 +01:00
313 changed files with 4143 additions and 4478 deletions

10
.gitattributes vendored
View File

@@ -1,12 +1,14 @@
/config/test export-ignore
/data/infra export-ignore
/docs export-ignore
/module/CLI/test export-ignore
/module/CLI/test-resources export-ignore
/module/Common/test export-ignore
/module/Common/test-func export-ignore
/module/Common/test-db export-ignore
/module/Core/test export-ignore
/module/Core/test-func export-ignore
/module/Core/test-db export-ignore
/module/Rest/test export-ignore
/module/Rest/test-api export-ignore
.env.dist export-ignore
.gitattributes export-ignore
.gitignore export-ignore
@@ -17,9 +19,9 @@ build.sh export-ignore
CHANGELOG.md export-ignore
docker-compose.override.yml.dist export-ignore
docker-compose.yml export-ignore
func_tests_bootstrap.php export-ignore
indocker export-ignore
phpcs.xml export-ignore
phpunit.xml.dist export-ignore
phpunit-func.xml export-ignore
phpunit-api.xml export-ignore
phpunit-db.xml export-ignore
phpstan.neon

4
.gitignore vendored
View File

@@ -5,6 +5,8 @@ composer.phar
vendor/
.env
data/database.sqlite
data/shlink-tests.db
data/GeoLite2-City.mmdb
docs/swagger-ui
docs/swagger-ui*
docker-compose.override.yml
.phpunit.result.cache

View File

@@ -2,7 +2,7 @@
namespace PHPSTORM_META;
use Psr\Container\ContainerInterface;
use Zend\ServiceManager\ServiceManager;
use Zend\ServiceManager\ServiceLocatorInterface;
/**
* PhpStorm Container Interop code completion
@@ -17,7 +17,7 @@ $STATIC_METHOD_TYPES = [
ContainerInterface::get('') => [
'' == '@',
],
ServiceManager::build('') => [
ServiceLocatorInterface::build('') => [
'' == '@',
],
];

View File

@@ -1,5 +1,6 @@
tools:
external_code_coverage: true
external_code_coverage:
timeout: 600
checks:
php:
code_rating: true

View File

@@ -1,7 +1,5 @@
language: php
sudo: false # Use containerized environment
branches:
only:
- /.*/
@@ -11,10 +9,6 @@ php:
- 7.2
- 7.3
matrix:
allow_failures:
- php: 7.3
before_install:
- echo 'extension = memcached.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
- echo 'extension = apcu.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
@@ -27,7 +21,7 @@ install:
script:
- mkdir build
- composer check
- composer ci
after_success:
- rm -f build/clover.xml
@@ -41,11 +35,17 @@ before_deploy:
- ./build.sh ${TRAVIS_TAG#?}
deploy:
provider: releases
api_key:
secure: a9dbZchocqeuOViwUeNH54bQR5Sz7rEYXx5b9WPFtnFn9LGKKUaLbA2U91UQ9QKPrcTpsALubUYbw2CnNmvCwzaY+R8lCD3gkU4ohsEnbpnw3deOeixI74sqBHJAuCH9FSaRDGILoBMtUKx2xlzIymFxkIsgIukkGbdkWHDlRWY3oTUUuw1SQ2Xk9KDsbJQtjIc1+G/O6gHaV4qv/R9W8NPmJExKTNDrAZbC1vIUnxqp4UpVo1hst8qPd1at94CndDYM5rG+7imGbdtxTxzamt819qdTO1OfvtctKawNAm7YXZrrWft6c7gI6j6SI4hxd+ZrrPBqbaRFHkZHjnNssO/yn4SaOHFFzccmu0MzvpPCf0qWZwd3sGHVYer1MnR2mHYqU84QPlW3nrHwJjkrpq3+q0JcBY6GsJs+RskHNtkMTKV05Iz6QUI5YZGwTpuXaRm036SmavjGc4IDlMaYCk/NmbB9BKpthJxLdUpczOHpnjXXHziotWD6cfEnbjU3byfD8HY5WrxSjsNT7SKmXN3hRof7bk985ewQVjGT42O3NbnfnqjQQWr/B7/zFTpLR4f526Bkq12CdCyf5lvrbq+POkLVdJ+uFfR7ds248Ue/jBQy6kM1tWmKF9QiwisFlA84eQ4CW3I93Rp97URv+AQa9zmbD0Ve3Udp+g6nF5I=
file: "./build/shlink_${TRAVIS_TAG#?}_dist.zip"
skip_cleanup: true
on:
tags: true
php: 7.1
- provider: releases
api_key:
secure: a9dbZchocqeuOViwUeNH54bQR5Sz7rEYXx5b9WPFtnFn9LGKKUaLbA2U91UQ9QKPrcTpsALubUYbw2CnNmvCwzaY+R8lCD3gkU4ohsEnbpnw3deOeixI74sqBHJAuCH9FSaRDGILoBMtUKx2xlzIymFxkIsgIukkGbdkWHDlRWY3oTUUuw1SQ2Xk9KDsbJQtjIc1+G/O6gHaV4qv/R9W8NPmJExKTNDrAZbC1vIUnxqp4UpVo1hst8qPd1at94CndDYM5rG+7imGbdtxTxzamt819qdTO1OfvtctKawNAm7YXZrrWft6c7gI6j6SI4hxd+ZrrPBqbaRFHkZHjnNssO/yn4SaOHFFzccmu0MzvpPCf0qWZwd3sGHVYer1MnR2mHYqU84QPlW3nrHwJjkrpq3+q0JcBY6GsJs+RskHNtkMTKV05Iz6QUI5YZGwTpuXaRm036SmavjGc4IDlMaYCk/NmbB9BKpthJxLdUpczOHpnjXXHziotWD6cfEnbjU3byfD8HY5WrxSjsNT7SKmXN3hRof7bk985ewQVjGT42O3NbnfnqjQQWr/B7/zFTpLR4f526Bkq12CdCyf5lvrbq+POkLVdJ+uFfR7ds248Ue/jBQy6kM1tWmKF9QiwisFlA84eQ4CW3I93Rp97URv+AQa9zmbD0Ve3Udp+g6nF5I=
file: "./build/shlink_${TRAVIS_TAG#?}_dist.zip"
skip_cleanup: true
on:
tags: true
php: 7.1
- provider: script
script: bash data/travis/trigger_docker_build.sh
skip_cleanup: true
on:
tags: true
php: 7.1

View File

@@ -2,7 +2,140 @@
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
## 1.16.2 - 2019-03-05
#### Added
* *Nothing*
#### Changed
* *Nothing*
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#368](https://github.com/shlinkio/shlink/issues/368) Fixed error produced when running a `SELECT COUNT(...)` with `ORDER BY` in PostgreSQL databases.
## 1.16.1 - 2019-02-26
#### Added
* *Nothing*
#### Changed
* [#363](https://github.com/shlinkio/shlink/issues/363) Updated to `shlinkio/php-coding-standard` version 1.1.0
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#362](https://github.com/shlinkio/shlink/issues/362) Fixed all visits without an IP address being processed every time the `visit:process` command is executed.
## 1.16.0 - 2019-02-23
#### Added
* [#304](https://github.com/shlinkio/shlink/issues/304) Added health endpoint to check healthiness of the service. Useful in container-based infrastructures.
Call [GET /rest/health] in order to get a response like this:
```http
HTTP/1.1 200 OK
Content-Type: application/health+json
Content-Length: 681
{
"status": "pass",
"version": "1.16.0",
"links": {
"about": "https://shlink.io",
"project": "https://github.com/shlinkio/shlink"
}
}
```
The status code can be `200 OK` in case of success or `503 Service Unavailable` in case of error, while the `status` property will be one of `pass` or `fail`, as defined in the [Health check RFC](https://inadarei.github.io/rfc-healthcheck/).
* [#279](https://github.com/shlinkio/shlink/issues/279) Added new `findIfExists` flag to the `[POST /short-url]` REST endpoint and the `short-urls:generate` CLI command. It can be used to return existing short URLs when found, instead of creating new ones.
Thanks to this flag you won't need to remember if you created a short URL for a long one. It will just create it if needed or return the existing one if possible.
The behavior might be a little bit counterintuitive when combined with other params. This is how the endpoint behaves when providing this new flag:
* Only the long URL is provided: It will return the newest match or create a new short URL if none is found.
* Long url and custom slug are provided: It will return the short URL when both params match, return an error when the slug is in use for another long URL, or create a new short URL otherwise.
* Any of the above but including other params (tags, validSince, validUntil, maxVisits): It will behave the same as the previous two cases, but it will try to exactly match existing results using all the params. If any of them does not match, it will try to create a new short URL.
* [#336](https://github.com/shlinkio/shlink/issues/336) Added an API test suite which performs API calls to an actual instance of the web service.
#### Changed
* [#342](https://github.com/shlinkio/shlink/issues/342) The installer no longer asks for a charset to be provided, and instead, it shuffles the base62 charset.
* [#320](https://github.com/shlinkio/shlink/issues/320) Replaced query builder by plain DQL for all queries which do not need to be dynamically generated.
* [#330](https://github.com/shlinkio/shlink/issues/330) No longer allow failures on PHP 7.3 envs during project CI build.
* [#335](https://github.com/shlinkio/shlink/issues/335) Renamed functional test suite to database test suite, since that better describes what it actually does.
* [#346](https://github.com/shlinkio/shlink/issues/346) Extracted installer as an independent tool.
* [#261](https://github.com/shlinkio/shlink/issues/261) Increased mutation score to 70%.
#### Deprecated
* [#351](https://github.com/shlinkio/shlink/issues/351) Deprecated `config:generate-charset` and `config:generate-secret` CLI commands.
#### Removed
* *Nothing*
#### Fixed
* [#317](https://github.com/shlinkio/shlink/issues/317) Fixed error while trying to generate previews because of global config file being deleted by mistake by build script.
* [#307](https://github.com/shlinkio/shlink/issues/307) Fixed memory leak while trying to process huge amounts of visits due to the query not being properly paginated.
## 1.15.1 - 2018-12-16
#### Added
* [#162](https://github.com/shlinkio/shlink/issues/162) Added non-rest endpoints to swagger definition.
#### Changed
* [#312](https://github.com/shlinkio/shlink/issues/312) Now all config files both in `php` and `json` format are loaded from `config/params` folder, easing users to provided customizations to docker image.
* [#226](https://github.com/shlinkio/shlink/issues/226) Updated how table are rendered in CLI commands, making use of new features in Symfony 4.2.
* [#321](https://github.com/shlinkio/shlink/issues/321) Extracted entities mappings from entities to external config files.
* [#308](https://github.com/shlinkio/shlink/issues/308) Automated docker image building.
#### Deprecated
* *Nothing*
#### Removed
* [#301](https://github.com/shlinkio/shlink/issues/301) Removed custom `AccessLogFactory` in favor of the implementation included in [zendframework/zend-expressive-swoole](https://github.com/zendframework/zend-expressive-swoole) v2.2.0
#### Fixed
* [#309](https://github.com/shlinkio/shlink/issues/309) Added missing favicon to prevent 404 errors logged when an error page is loaded in a browser.
* [#310](https://github.com/shlinkio/shlink/issues/310) Fixed execution context not being properly detected, making `CloseDbConnectionMiddlware` to be always piped. Now the check is not even made, which simplifies everything.
## 1.15.0 - 2018-12-02

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2018 Alejandro Celaya
Copyright (c) 2016-2019 Alejandro Celaya
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -9,6 +9,14 @@
A PHP-based self-hosted URL shortener that can be used to serve shortened URLs under your own custom domain.
## Table of Contents
- [Installation](#installation)
- [Update to new version](#update-to-new-version)
- [Using a docker image](#using-a-docker-image)
- [Using shlink](#using-shlink)
- [Shlink CLI Help](#shlink-cli-help)
## Installation
First make sure the host where you are going to run shlink fulfills these requirements:
@@ -202,11 +210,12 @@ In future versions, it is planed that, when using **swoole** to serve shlink, so
When a new Shlink version is available, you don't need to repeat the entire process yourself. Instead, follow these steps:
1. Rename your existing Shlink directory to something else (ie. `shlink` ---> `shlink-old`)
2. Download and extract the new version of Shlink, and set the directories name to that of the old version. (ie. `shlink`)
1. Rename your existing Shlink directory to something else (ie. `shlink` ---> `shlink-old`).
2. Download and extract the new version of Shlink, and set the directories name to that of the old version. (ie. `shlink`).
3. Run the `bin/update` script in the new version's directory to migrate your configuration over.
4. If you are using shlink with swoole, restart the service by running `/etc/init.d/shlink_swoole restart`.
The script will ask you for the location from previous shlink version, and use it in order to import the configuration. It will then update the database and generate some the assets neccessary for Shlink to function.
The `bin/update` script will ask you for the location from previous shlink version, and use it in order to import the configuration. It will then update the database and generate some assets shlink needs to work.
Right now, it does not import cached info (like website previews), but it will. For now you will need to regenerate them again.
@@ -214,11 +223,9 @@ Right now, it does not import cached info (like website previews), but it will.
## Using a docker image
Currently there's no official docker image, but there's a work in progress alpha version you can find [here](https://hub.docker.com/r/shlinkio/shlink/).
Starting with version 1.15.0, an official docker image is provided. You can find the docs on how to use it [here](https://hub.docker.com/r/shlinkio/shlink/).
The idea will be that you can just generate a container using the image and provide predefined config files via volumes or CLI arguments, so that you get shlink up and running.
Currently the image does not expose an entry point which let's you interact with shlink's CLI interface, nor allows configuration to be passed.
The idea is that you can just generate a container using the image and provide custom config via env vars.
## Using shlink
@@ -259,8 +266,8 @@ Available commands:
api-key:generate Generates a new valid API key.
api-key:list Lists all the available API keys.
config
config:generate-charset Generates a character set sample just by shuffling the default one, "123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ". Then it can be set in the SHORTCODE_CHARS environment variable
config:generate-secret Generates a random secret string that can be used for JWT token encryption
config:generate-charset [DEPRECATED] Generates a character set sample just by shuffling the default one, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ". Then it can be set in the SHORTCODE_CHARS environment variable
config:generate-secret [DEPRECATED] Generates a random secret string that can be used for JWT token encryption
short-url
short-url:delete [short-code:delete] Deletes a short URL
short-url:generate [shortcode:generate|short-code:generate] Generates a short URL for provided long URL and returns it

View File

@@ -3,11 +3,8 @@
declare(strict_types=1);
use Interop\Container\ContainerInterface;
use Shlinkio\Shlink\Common\Exec\ExecutionContext;
use Symfony\Component\Console\Application as CliApp;
/** @var ContainerInterface $container */
$container = include __DIR__ . '/../config/container.php';
putenv(sprintf('CURRENT_SHLINK_CONTEXT=%s', ExecutionContext::CLI));
$container->get(CliApp::class)->run();

View File

@@ -2,11 +2,11 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Installer;
namespace Shlinkio\Shlink;
use Symfony\Component\Console\Application;
use Zend\ServiceManager\ServiceLocatorInterface;
use function chdir;
use function dirname;
/** @var ServiceLocatorInterface $container */
$container = include __DIR__ . '/../config/install-container.php';
$container->build(Application::class)->run();
chdir(dirname(__DIR__));
$run = require __DIR__ . '/../vendor/shlinkio/shlink-installer/bin/run.php';
$run(false);

14
bin/test/run-api-tests.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env sh
set -e
export APP_ENV=test
# Try to stop server just in case it hanged in last execution
vendor/bin/zend-expressive-swoole stop
echo 'Starting server...'
vendor/bin/zend-expressive-swoole start -d
sleep 2
vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox
vendor/bin/zend-expressive-swoole stop

View File

@@ -2,11 +2,11 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Installer;
namespace Shlinkio\Shlink;
use Symfony\Component\Console\Application;
use Zend\ServiceManager\ServiceLocatorInterface;
use function chdir;
use function dirname;
/** @var ServiceLocatorInterface $container */
$container = include __DIR__ . '/../config/install-container.php';
$container->build(Application::class, ['isUpdate' => true])->run();
chdir(dirname(__DIR__));
$run = require __DIR__ . '/../vendor/shlinkio/shlink-installer/bin/run.php';
$run(true);

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -e
if [ "$#" -ne 1 ]; then
if [[ "$#" -ne 1 ]]; then
echo "Usage:" >&2
echo " $0 {version}" >&2
exit 1
@@ -10,14 +10,16 @@ fi
version=$1
builtcontent="./build/shlink_${version}_dist"
projectdir=$(pwd)
[ -f ./composer.phar ] && composerBin='./composer.phar' || composerBin='composer'
[[ -f ./composer.phar ]] && composerBin='./composer.phar' || composerBin='composer'
# Copy project content to temp dir
echo 'Copying project files...'
rm -rf "${builtcontent}"
mkdir -p "${builtcontent}"
rsync -av * "${builtcontent}" \
--exclude=bin/test \
--exclude=data/infra \
--exclude=data/travis \
--exclude=data/migrations_template.txt \
--exclude=data/GeoLite2-City.mmdb \
--exclude=**/.gitignore \
@@ -27,11 +29,11 @@ rsync -av * "${builtcontent}" \
--exclude=docs \
--exclude=indocker \
--exclude=docker* \
--exclude=func_tests_bootstrap.php \
--exclude=php* \
--exclude=infection.json \
--exclude=phpstan.neon \
--exclude=config/autoload/*local* \
--exclude=config/test \
--exclude=**/test* \
--exclude=build*
cd "${builtcontent}"
@@ -39,7 +41,7 @@ cd "${builtcontent}"
# Install dependencies
echo "Installing dependencies with $composerBin..."
${composerBin} self-update
${composerBin} install --no-dev --optimize-autoloader --no-progress --no-interaction
${composerBin} install --no-dev --optimize-autoloader --apcu-autoloader --no-progress --no-interaction
# Delete development files
echo 'Deleting dev files...'

View File

@@ -29,20 +29,20 @@
"lstrojny/functional-php": "^1.8",
"mikehaertl/phpwkhtmltopdf": "^2.2",
"monolog/monolog": "^1.21",
"roave/security-advisories": "dev-master",
"symfony/console": "^4.1",
"symfony/filesystem": "^4.1",
"symfony/lock": "^4.1",
"symfony/process": "^4.1",
"shlinkio/shlink-installer": "^1.1",
"symfony/console": "^4.2",
"symfony/filesystem": "^4.2",
"symfony/lock": "^4.2",
"symfony/process": "^4.2",
"theorchard/monolog-cascade": "^0.4",
"zendframework/zend-config": "^3.0",
"zendframework/zend-config-aggregator": "^1.0",
"zendframework/zend-diactoros": "^2.0",
"zendframework/zend-diactoros": "^2.1.1",
"zendframework/zend-expressive": "^3.0",
"zendframework/zend-expressive-fastroute": "^3.0",
"zendframework/zend-expressive-helpers": "^5.0",
"zendframework/zend-expressive-platesrenderer": "^2.0",
"zendframework/zend-expressive-swoole": "^2.1",
"zendframework/zend-expressive-swoole": "^2.2",
"zendframework/zend-i18n": "^2.7",
"zendframework/zend-inputfilter": "^2.8",
"zendframework/zend-paginator": "^2.6",
@@ -51,14 +51,16 @@
},
"require-dev": {
"devster/ubench": "^2.0",
"doctrine/data-fixtures": "^1.3",
"filp/whoops": "^2.0",
"infection/infection": "^0.11.0",
"phpstan/phpstan": "^0.10.0",
"phpunit/phpcov": "^5.0",
"phpunit/phpunit": "^7.3",
"shlinkio/php-coding-standard": "~1.0.0",
"symfony/dotenv": "^4.0",
"symfony/var-dumper": "^4.0",
"infection/infection": "^0.12.2",
"phpstan/phpstan": "^0.11.2",
"phpunit/phpcov": "^6.0@dev || ^5.0",
"phpunit/phpunit": "^8.0 || ^7.5",
"roave/security-advisories": "dev-master",
"shlinkio/php-coding-standard": "~1.1.0",
"symfony/dotenv": "^4.2",
"symfony/var-dumper": "^4.2",
"zendframework/zend-component-installer": "^2.1",
"zendframework/zend-expressive-tooling": "^1.0"
},
@@ -67,8 +69,7 @@
"Shlinkio\\Shlink\\CLI\\": "module/CLI/src",
"Shlinkio\\Shlink\\Rest\\": "module/Rest/src",
"Shlinkio\\Shlink\\Core\\": "module/Core/src",
"Shlinkio\\Shlink\\Common\\": "module/Common/src",
"Shlinkio\\Shlink\\Installer\\": "module/Installer/src"
"Shlinkio\\Shlink\\Common\\": "module/Common/src"
},
"files": [
"module/Common/functions/functions.php"
@@ -78,28 +79,24 @@
"psr-4": {
"ShlinkioTest\\Shlink\\CLI\\": "module/CLI/test",
"ShlinkioTest\\Shlink\\Rest\\": "module/Rest/test",
"ShlinkioApiTest\\Shlink\\Rest\\": "module/Rest/test-api",
"ShlinkioTest\\Shlink\\Core\\": [
"module/Core/test",
"module/Core/test-func"
"module/Core/test-db"
],
"ShlinkioTest\\Shlink\\Common\\": [
"module/Common/test",
"module/Common/test-func"
],
"ShlinkioTest\\Shlink\\Installer\\": "module/Installer/test"
"module/Common/test-db"
]
}
},
"scripts": {
"check": [
"ci": [
"@cs",
"@stan",
"@test:ci",
"@infect:ci"
],
"ci": [
"echo \"This command is DEPRECATED. Use check instead\"",
"@check"
],
"cs": "phpcs",
"cs:fix": "phpcbf",
@@ -107,15 +104,18 @@
"test": [
"@test:unit",
"@test:func"
"@test:db",
"@test:api"
],
"test:ci": [
"@test:unit:ci",
"@test:func"
"@test:db",
"@test:api"
],
"test:unit": "phpdbg -qrr vendor/bin/phpunit --order-by=random --coverage-php build/coverage-unit.cov",
"test:unit:ci": "phpdbg -qrr vendor/bin/phpunit --order-by=random --coverage-php build/coverage-unit.cov --coverage-clover=build/clover.xml --coverage-xml=build/coverage-xml --log-junit=build/phpunit.junit.xml",
"test:func": "phpdbg -qrr vendor/bin/phpunit --order-by=random -c phpunit-func.xml --coverage-php build/coverage-func.cov",
"test:unit": "phpdbg -qrr vendor/bin/phpunit --order-by=random --coverage-php build/coverage-unit.cov --testdox",
"test:unit:ci": "phpdbg -qrr vendor/bin/phpunit --order-by=random --coverage-php build/coverage-unit.cov --coverage-clover=build/clover.xml --coverage-xml=build/coverage-xml --log-junit=build/phpunit.junit.xml --testdox",
"test:db": "APP_ENV=test phpdbg -qrr vendor/bin/phpunit --order-by=random -c phpunit-db.xml --coverage-php build/coverage-db.cov --testdox",
"test:api": "bin/test/run-api-tests.sh",
"test:pretty": [
"@test",
@@ -123,9 +123,9 @@
],
"test:unit:pretty": "phpdbg -qrr vendor/bin/phpunit --coverage-html build/coverage --order-by=random",
"infect": "infection --threads=4 --min-msi=65 --log-verbosity=2 --only-covered",
"infect:ci": "infection --threads=4 --min-msi=65 --log-verbosity=2 --only-covered --coverage=build",
"infect:show": "infection --threads=4 --min-msi=65 --log-verbosity=2 --only-covered --show-mutations",
"infect": "infection --threads=4 --min-msi=70 --log-verbosity=default --only-covered",
"infect:ci": "infection --threads=4 --min-msi=70 --log-verbosity=default --only-covered --coverage=build",
"infect:show": "infection --threads=4 --min-msi=70 --log-verbosity=default --only-covered --show-mutations",
"infect:test": [
"@test:unit:ci",
"@infect:ci"
@@ -141,12 +141,14 @@
"test:ci": "<fg=blue;options=bold>Runs all test suites, generating all needed reports and logs for CI envs</>",
"test:unit": "<fg=blue;options=bold>Runs unit test suites</>",
"test:unit:ci": "<fg=blue;options=bold>Runs unit test suites, generating all needed reports and logs for CI envs</>",
"test:func": "<fg=blue;options=bold>Runs functional test suites (covering entity repositories)</>",
"test:db": "<fg=blue;options=bold>Runs database test suites (covering entity repositories)</>",
"test:api": "<fg=blue;options=bold>Runs API test suites</>",
"test:pretty": "<fg=blue;options=bold>Runs all test suites and generates an HTML code coverage report</>",
"test:unit:pretty": "<fg=blue;options=bold>Runs unit test suites and generates an HTML code coverage report</>",
"infect": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing</>",
"infect:ci": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing with existing reports and logs</>",
"infect:show": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing and shows applied mutators</>"
"infect:show": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing and shows applied mutators</>",
"infect:test": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing</>"
},
"config": {
"sort-packages": true

View File

@@ -8,7 +8,7 @@ return [
'app_options' => [
'name' => 'Shlink',
'version' => '%SHLINK_VERSION%',
'secret_key' => env('SECRET_KEY'),
'secret_key' => env('SECRET_KEY', ''),
'disable_track_param' => null,
],

View File

@@ -1,12 +1,23 @@
<?php
declare(strict_types=1);
use Psr\Container\ContainerInterface;
use Psr\Log;
return [
'dependencies' => [
'lazy_services' => [
'write_proxy_files' => false,
],
'initializers' => [
function (ContainerInterface $container, $instance) {
if ($instance instanceof Log\LoggerAwareInterface) {
$instance->setLogger($container->get(Log\LoggerInterface::class));
}
},
],
],
];

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
use Zend\Expressive\Container\WhoopsErrorResponseGeneratorFactory;
return [

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Installer\Config\Plugin;
return [
'installer_plugins_expected_config' => [
Plugin\LanguageConfigCustomizer::class => [
Plugin\LanguageConfigCustomizer::DEFAULT_LANG,
],
Plugin\UrlShortenerConfigCustomizer::class => [
Plugin\UrlShortenerConfigCustomizer::SCHEMA,
Plugin\UrlShortenerConfigCustomizer::HOSTNAME,
Plugin\UrlShortenerConfigCustomizer::CHARS,
Plugin\UrlShortenerConfigCustomizer::VALIDATE_URL,
Plugin\UrlShortenerConfigCustomizer::ENABLE_NOT_FOUND_REDIRECTION,
Plugin\UrlShortenerConfigCustomizer::NOT_FOUND_REDIRECT_TO,
],
Plugin\ApplicationConfigCustomizer::class => [
Plugin\ApplicationConfigCustomizer::SECRET,
Plugin\ApplicationConfigCustomizer::DISABLE_TRACK_PARAM,
Plugin\ApplicationConfigCustomizer::CHECK_VISITS_THRESHOLD,
Plugin\ApplicationConfigCustomizer::VISITS_THRESHOLD,
],
Plugin\DatabaseConfigCustomizer::class => [
Plugin\DatabaseConfigCustomizer::DRIVER,
Plugin\DatabaseConfigCustomizer::NAME,
Plugin\DatabaseConfigCustomizer::USER,
Plugin\DatabaseConfigCustomizer::PASSWORD,
Plugin\DatabaseConfigCustomizer::HOST,
Plugin\DatabaseConfigCustomizer::PORT,
],
],
];

View File

@@ -7,7 +7,7 @@ use Monolog\Handler\RotatingFileHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Monolog\Processor;
use Zend\Expressive\Swoole\Log\AccessLogInterface;
use const PHP_EOL;
return [
@@ -32,7 +32,6 @@ return [
'class' => StreamHandler::class,
'level' => Logger::INFO,
'stream' => 'php://stdout',
'formatter' => 'dashed',
],
],
@@ -61,15 +60,13 @@ return [
'factories' => [
'Logger_Shlink' => Common\Factory\LoggerFactory::class,
'Logger_Swoole' => Common\Factory\LoggerFactory::class,
AccessLogInterface::class => Common\Logger\Swoole\AccessLogFactory::class,
],
],
'zend-expressive-swoole' => [
'swoole-http-server' => [
'logger' => [
'logger_name' => 'Logger_Swoole',
'logger-name' => 'Logger_Swoole',
],
],
],

View File

@@ -10,18 +10,11 @@ return [
'middleware_pipeline' => [
'pre-routing' => [
'middleware' => (function () {
$middleware = [
ErrorHandler::class,
Expressive\Helper\ContentLengthMiddleware::class,
];
if (Common\Exec\ExecutionContext::currentContextIsSwoole()) {
$middleware[] = Common\Middleware\CloseDbConnectionMiddleware::class;
}
return $middleware;
})(),
'middleware' => [
ErrorHandler::class,
Expressive\Helper\ContentLengthMiddleware::class,
Common\Middleware\CloseDbConnectionMiddleware::class,
],
'priority' => 12,
],
'pre-routing-rest' => [

View File

@@ -1,23 +0,0 @@
<?php
declare(strict_types=1);
use Cocur\Slugify\Slugify;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
return [
'slugify_options' => [
'lowercase' => false,
],
'dependencies' => [
'factories' => [
Slugify::class => ConfigAbstractFactory::class,
],
],
ConfigAbstractFactory::class => [
Slugify::class => ['config.slugify_options'],
],
];

View File

@@ -9,9 +9,6 @@ return [
'swoole-http-server' => [
'host' => '0.0.0.0',
'process-name' => 'shlink',
'static-files' => [
'enable' => false,
],
],
],

View File

@@ -1,7 +1,8 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use function Shlinkio\Shlink\Common\env;
return [
@@ -11,7 +12,7 @@ return [
'schema' => env('SHORTENED_URL_SCHEMA', 'http'),
'hostname' => env('SHORTENED_URL_HOSTNAME'),
],
'shortcode_chars' => env('SHORTCODE_CHARS', UrlShortener::DEFAULT_CHARS),
'shortcode_chars' => env('SHORTCODE_CHARS', UrlShortenerOptions::DEFAULT_CHARS),
'validate_url' => true,
'not_found_short_url' => [
'enable_redirection' => false,

View File

@@ -3,9 +3,9 @@ declare(strict_types=1);
return [
'phpwkhtmltopdf' => [
'wkhtmltopdf' => [
'images' => [
'binary' => 'bin/wkhtmltoimage',
'binary' => __DIR__ . '/../../bin/wkhtmltoimage',
'type' => 'jpg',
],
],

View File

@@ -6,30 +6,8 @@ use Doctrine\ORM\Tools\Console\ConsoleRunner;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\ServiceManager;
$isTest = false;
foreach ($_SERVER['argv'] as $i => $arg) {
if ($arg === '--test') {
unset($_SERVER['argv'][$i]);
$isTest = true;
break;
}
}
/** @var ContainerInterface|ServiceManager $container */
$container = include __DIR__ . '/container.php';
// If in testing env, override DB connection to use an in-memory sqlite database
if ($isTest) {
$container->setAllowOverride(true);
$config = $container->get('config');
$config['entity_manager']['connection'] = [
'driver' => 'pdo_sqlite',
'path' => realpath(sys_get_temp_dir()) . '/shlink-tests.db',
];
$container->setService('config', $config);
}
/** @var EntityManager $em */
$em = $container->get(EntityManager::class);
return ConsoleRunner::createHelperSet($em);

View File

@@ -7,6 +7,8 @@ use Acelaya\ExpressiveErrorHandler;
use Zend\ConfigAggregator;
use Zend\Expressive;
use function Shlinkio\Shlink\Common\env;
return (new ConfigAggregator\ConfigAggregator([
Expressive\ConfigProvider::class,
Expressive\Router\ConfigProvider::class,
@@ -17,7 +19,10 @@ return (new ConfigAggregator\ConfigAggregator([
Common\ConfigProvider::class,
Core\ConfigProvider::class,
CLI\ConfigProvider::class,
Installer\ConfigProvider::class,
Rest\ConfigProvider::class,
new ConfigAggregator\ZendConfigProvider('config/{autoload/{{,*.}global,{,*.}local},params/generated_config}.php'),
new ConfigAggregator\PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
new ConfigAggregator\ZendConfigProvider('config/params/{generated_config.php,*.config.{php,json}}'),
env('APP_ENV') === 'test'
? new ConfigAggregator\PhpFileProvider('config/test/*.global.php')
: new ConfigAggregator\ArrayProvider([]),
], 'data/cache/app_config.php'))->getMergedConfig();

View File

@@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Installer\Config\Plugin\DatabaseConfigCustomizer;
use Shlinkio\Shlink\Installer\Factory\InstallApplicationFactory;
use Symfony\Component\Console\Application;
use Symfony\Component\Filesystem\Filesystem;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Zend\ServiceManager\Factory\InvokableFactory;
use Zend\ServiceManager\ServiceManager;
chdir(dirname(__DIR__));
require __DIR__ . '/../vendor/autoload.php';
$container = new ServiceManager([
'factories' => [
Application::class => InstallApplicationFactory::class,
Filesystem::class => InvokableFactory::class,
],
'services' => [
'config' => [
ConfigAbstractFactory::class => [
DatabaseConfigCustomizer::class => [Filesystem::class],
],
],
],
]);
return $container;

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common;
use Doctrine\ORM\EntityManager;
use Psr\Container\ContainerInterface;
use function file_exists;
use function touch;
// Create an empty .env file
if (! file_exists('.env')) {
touch('.env');
}
/** @var ContainerInterface $container */
$container = require __DIR__ . '/../container.php';
$testHelper = $container->get(TestHelper::class);
$config = $container->get('config');
$em = $container->get(EntityManager::class);
$testHelper->createTestDb($config['entity_manager']['connection']['path']);
ApiTest\ApiTestCase::setApiClient($container->get('shlink_test_api_client'));
ApiTest\ApiTestCase::setSeedFixturesCallback(function () use ($testHelper, $em, $config) {
$testHelper->seedFixtures($em, $config['data_fixtures'] ?? []);
});

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common;
use Psr\Container\ContainerInterface;
use function file_exists;
use function touch;
// Create an empty .env file
if (! file_exists('.env')) {
touch('.env');
}
/** @var ContainerInterface $container */
$container = require __DIR__ . '/../container.php';
$config = $container->get('config');
$container->get(TestHelper::class)->createTestDb($config['entity_manager']['connection']['path']);
DbTest\DatabaseTestCase::setEntityManager($container->get('em'));

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink;
use GuzzleHttp\Client;
use Zend\ConfigAggregator\ConfigAggregator;
use Zend\ServiceManager\Factory\InvokableFactory;
use function sprintf;
use function sys_get_temp_dir;
$swooleTestingHost = '127.0.0.1';
$swooleTestingPort = 9999;
return [
'debug' => true,
ConfigAggregator::ENABLE_CACHE => false,
'url_shortener' => [
'domain' => [
'schema' => 'http',
'hostname' => 'doma.in',
],
],
'zend-expressive-swoole' => [
'swoole-http-server' => [
'host' => $swooleTestingHost,
'port' => $swooleTestingPort,
'process-name' => 'shlink_test',
'options' => [
'pid_file' => sys_get_temp_dir() . '/shlink-test-swoole.pid',
],
],
],
'dependencies' => [
'services' => [
'shlink_test_api_client' => new Client([
'base_uri' => sprintf('http://%s:%s/', $swooleTestingHost, $swooleTestingPort),
'http_errors' => false,
]),
],
'factories' => [
Common\TestHelper::class => InvokableFactory::class,
],
],
'entity_manager' => [
'connection' => [
'driver' => 'pdo_sqlite',
'path' => sys_get_temp_dir() . '/shlink-tests.db',
// 'path' => __DIR__ . '/../../data/shlink-tests.db',
],
],
'data_fixtures' => [
'paths' => [
__DIR__ . '/../../module/Rest/test-api/Fixtures',
],
],
];

2
data/infra/database_pg/.gitignore vendored Executable file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

@@ -1,6 +0,0 @@
FROM mysql:5.7
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
# Enable remote access (default is localhost only, we change this
# otherwise our database would not be reachable from outside the container)
RUN sed -i -e"s/^bind-address\s*=\s*127.0.0.1/bind-address = 0.0.0.0/" /etc/mysql/my.cnf

View File

@@ -1,5 +0,0 @@
FROM nginx:1.11.6-alpine
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
# Delete default nginx vhost
RUN rm /etc/nginx/conf.d/default.conf

View File

@@ -1,6 +1,12 @@
FROM php:7.1.22-fpm-alpine
FROM php:7.3.1-fpm-alpine3.8
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
ENV PREDIS_VERSION 4.2.0
ENV MEMCACHED_VERSION 3.1.3
ENV APCU_VERSION 5.1.16
ENV APCU_BC_VERSION 1.0.4
ENV XDEBUG_VERSION "2.7.0RC1"
RUN apk update
# Install common php extensions
@@ -16,17 +22,17 @@ RUN docker-php-ext-install pdo_sqlite
RUN apk add --no-cache --virtual icu-dev
RUN docker-php-ext-install intl
RUN apk add --no-cache --virtual zlib-dev
RUN apk add --no-cache --virtual libzip-dev zlib-dev
RUN docker-php-ext-install zip
RUN apk add --no-cache --virtual libmcrypt-dev
RUN docker-php-ext-install mcrypt
RUN apk add --no-cache --virtual libpng-dev
RUN docker-php-ext-install gd
RUN apk add --no-cache postgresql-dev
RUN docker-php-ext-install pdo_pgsql
# Install redis extension
ADD https://github.com/phpredis/phpredis/archive/3.1.4.tar.gz /tmp/phpredis.tar.gz
ADD https://github.com/phpredis/phpredis/archive/$PREDIS_VERSION.tar.gz /tmp/phpredis.tar.gz
RUN mkdir -p /usr/src/php/ext/redis\
&& tar xf /tmp/phpredis.tar.gz -C /usr/src/php/ext/redis --strip-components=1
# configure and install
@@ -38,7 +44,7 @@ RUN rm /tmp/phpredis.tar.gz
# Install memcached extension
RUN apk add --no-cache --virtual cyrus-sasl-dev
RUN apk add --no-cache --virtual libmemcached-dev
ADD https://github.com/php-memcached-dev/php-memcached/archive/php7.tar.gz /tmp/memcached.tar.gz
ADD https://github.com/php-memcached-dev/php-memcached/archive/v$MEMCACHED_VERSION.tar.gz /tmp/memcached.tar.gz
RUN mkdir -p /usr/src/php/ext/memcached\
&& tar xf /tmp/memcached.tar.gz -C /usr/src/php/ext/memcached --strip-components=1
# configure and install
@@ -48,7 +54,7 @@ RUN docker-php-ext-configure memcached\
RUN rm /tmp/memcached.tar.gz
# Install APCu extension
ADD https://pecl.php.net/get/apcu-5.1.3.tgz /tmp/apcu.tar.gz
ADD https://pecl.php.net/get/apcu-$APCU_VERSION.tgz /tmp/apcu.tar.gz
RUN mkdir -p /usr/src/php/ext/apcu\
&& tar xf /tmp/apcu.tar.gz -C /usr/src/php/ext/apcu --strip-components=1
# configure and install
@@ -58,7 +64,7 @@ RUN docker-php-ext-configure apcu\
RUN rm /tmp/apcu.tar.gz
# Install APCu-BC extension
ADD https://pecl.php.net/get/apcu_bc-1.0.3.tgz /tmp/apcu_bc.tar.gz
ADD https://pecl.php.net/get/apcu_bc-$APCU_BC_VERSION.tgz /tmp/apcu_bc.tar.gz
RUN mkdir -p /usr/src/php/ext/apcu-bc\
&& tar xf /tmp/apcu_bc.tar.gz -C /usr/src/php/ext/apcu-bc --strip-components=1
# configure and install
@@ -72,7 +78,7 @@ RUN rm /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini
RUN echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini
# Install xdebug
ADD https://pecl.php.net/get/xdebug-2.5.0 /tmp/xdebug.tar.gz
ADD https://pecl.php.net/get/xdebug-$XDEBUG_VERSION /tmp/xdebug.tar.gz
RUN mkdir -p /usr/src/php/ext/xdebug\
&& tar xf /tmp/xdebug.tar.gz -C /usr/src/php/ext/xdebug --strip-components=1
# configure and install

View File

@@ -1,6 +1,11 @@
FROM php:7.1.22-cli-alpine3.7
FROM php:7.3.1-cli-alpine3.8
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
ENV PREDIS_VERSION 4.2.0
ENV MEMCACHED_VERSION 3.1.3
ENV APCU_VERSION 5.1.16
ENV APCU_BC_VERSION 1.0.4
RUN apk update
# Install common php extensions
@@ -16,17 +21,17 @@ RUN docker-php-ext-install pdo_sqlite
RUN apk add --no-cache --virtual icu-dev
RUN docker-php-ext-install intl
RUN apk add --no-cache --virtual zlib-dev
RUN apk add --no-cache --virtual libzip-dev zlib-dev
RUN docker-php-ext-install zip
RUN apk add --no-cache --virtual libmcrypt-dev
RUN docker-php-ext-install mcrypt
RUN apk add --no-cache --virtual libpng-dev
RUN docker-php-ext-install gd
RUN apk add --no-cache postgresql-dev
RUN docker-php-ext-install pdo_pgsql
# Install redis extension
ADD https://github.com/phpredis/phpredis/archive/3.1.4.tar.gz /tmp/phpredis.tar.gz
ADD https://github.com/phpredis/phpredis/archive/$PREDIS_VERSION.tar.gz /tmp/phpredis.tar.gz
RUN mkdir -p /usr/src/php/ext/redis\
&& tar xf /tmp/phpredis.tar.gz -C /usr/src/php/ext/redis --strip-components=1
# configure and install
@@ -38,7 +43,7 @@ RUN rm /tmp/phpredis.tar.gz
# Install memcached extension
RUN apk add --no-cache --virtual cyrus-sasl-dev
RUN apk add --no-cache --virtual libmemcached-dev
ADD https://github.com/php-memcached-dev/php-memcached/archive/php7.tar.gz /tmp/memcached.tar.gz
ADD https://github.com/php-memcached-dev/php-memcached/archive/v$MEMCACHED_VERSION.tar.gz /tmp/memcached.tar.gz
RUN mkdir -p /usr/src/php/ext/memcached\
&& tar xf /tmp/memcached.tar.gz -C /usr/src/php/ext/memcached --strip-components=1
# configure and install
@@ -48,7 +53,7 @@ RUN docker-php-ext-configure memcached\
RUN rm /tmp/memcached.tar.gz
# Install APCu extension
ADD https://pecl.php.net/get/apcu-5.1.3.tgz /tmp/apcu.tar.gz
ADD https://pecl.php.net/get/apcu-$APCU_VERSION.tgz /tmp/apcu.tar.gz
RUN mkdir -p /usr/src/php/ext/apcu\
&& tar xf /tmp/apcu.tar.gz -C /usr/src/php/ext/apcu --strip-components=1
# configure and install
@@ -58,7 +63,7 @@ RUN docker-php-ext-configure apcu\
RUN rm /tmp/apcu.tar.gz
# Install APCu-BC extension
ADD https://pecl.php.net/get/apcu_bc-1.0.3.tgz /tmp/apcu_bc.tar.gz
ADD https://pecl.php.net/get/apcu_bc-$APCU_BC_VERSION.tgz /tmp/apcu_bc.tar.gz
RUN mkdir -p /usr/src/php/ext/apcu-bc\
&& tar xf /tmp/apcu_bc.tar.gz -C /usr/src/php/ext/apcu-bc --strip-components=1
# configure and install

View File

@@ -0,0 +1,20 @@
#!/bin/bash
set -e
# Get latest commit in master, in plain text
LATEST_MASTER_COMMIT=$(curl -H "Accept: application/vnd.github.sha" -X GET https://api.github.com/repos/shlinkio/shlink-docker-image/commits/master)
# Create new tag and a ref to the tag, which will trigger image build on it
curl -u acelaya:${GITHUB_OAUTH_KEY} \
-H "Content-Type: application/json" \
--data "{ \"tag\": \"${TRAVIS_TAG}\", \"message\": \"${TRAVIS_TAG}\", \"object\": \"${LATEST_MASTER_COMMIT}\", \"type\": \"commit\" }" \
-X POST https://api.github.com/repos/shlinkio/shlink-docker-image/git/tags
curl -u acelaya:${GITHUB_OAUTH_KEY} \
-H "Content-Type: application/json" \
--data "{ \"ref\": \"refs/tags/${TRAVIS_TAG}\", \"sha\": \"${LATEST_MASTER_COMMIT}\" }" \
-X POST https://api.github.com/repos/shlinkio/shlink-docker-image/git/refs
# Trigger image build for "latest
curl -H "Content-Type: application/json" \
--data '{ "docker_tag": "latest" }' \
-X POST https://registry.hub.docker.com/u/shlinkio/shlink/trigger/${DOCKER_TRIGGER_TOKEN}/

View File

@@ -1,4 +1,4 @@
version: '2'
version: '3'
services:
shlink_php:
@@ -12,3 +12,15 @@ services:
volumes:
- /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group:ro
shlink_db:
user: 1000:1000
volumes:
- /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group:ro
shlink_db_postgres:
user: 1000:1000
volumes:
- /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group:ro

View File

@@ -1,17 +1,15 @@
version: '2'
version: '3'
services:
shlink_nginx:
container_name: shlink_nginx
build:
context: .
dockerfile: ./data/infra/nginx.Dockerfile
image: nginx:1.15.9-alpine
ports:
- "8000:80"
volumes:
- ./:/home/shlink/www
- ./docs:/home/shlink/www/public/docs
- ./data/infra/vhost.conf:/etc/nginx/conf.d/shlink-vhost.conf
- ./data/infra/vhost.conf:/etc/nginx/conf.d/default.conf
links:
- shlink_php
@@ -25,6 +23,7 @@ services:
- ./data/infra/php.ini:/usr/local/etc/php/php.ini
links:
- shlink_db
- shlink_db_postgres
shlink_swoole:
container_name: shlink_swoole
@@ -37,12 +36,11 @@ services:
- ./:/home/shlink
links:
- shlink_db
- shlink_db_postgres
shlink_db:
container_name: shlink_db
build:
context: .
dockerfile: ./data/infra/db.Dockerfile
image: mysql:5.7
ports:
- "3307:3306"
volumes:
@@ -51,3 +49,16 @@ services:
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: shlink
shlink_db_postgres:
container_name: shlink_db_postgres
image: postgres:10.7-alpine
ports:
- "5433:5432"
volumes:
- ./:/home/shlink/www
- ./data/infra/database_pg:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: root
POSTGRES_DB: shlink
PGDATA: /var/lib/postgresql/data/pgdata

View File

@@ -0,0 +1,31 @@
{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"pass",
"fail"
],
"description": "The status of the service"
},
"version": {
"type": "string",
"description": "Shlink version"
},
"links": {
"type": "object",
"properties": {
"about": {
"type": "string",
"description": "About shlink"
},
"project": {
"type": "string",
"description": "Shlink project repository"
}
},
"description": "A list of links"
}
}
}

View File

@@ -0,0 +1,62 @@
{
"get": {
"operationId": "health",
"tags": [
"Monitoring"
],
"summary": "Check healthiness",
"description": "Checks the healthiness of the service, making sure it can access required resources.",
"responses": {
"200": {
"description": "The passing health status",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Health.json"
}
}
},
"examples": {
"application/json": {
"status": "pass",
"version": "1.16.0",
"links": {
"about": "https://shlink.io",
"project": "https://github.com/shlinkio/shlink"
}
}
}
},
"503": {
"description": "The failing health status",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Health.json"
}
}
},
"examples": {
"application/json": {
"status": "fail",
"version": "1.16.0",
"links": {
"about": "https://shlink.io",
"project": "https://github.com/shlinkio/shlink"
}
}
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
}
}

View File

@@ -151,7 +151,7 @@
"Short URLs"
],
"summary": "Create short URL",
"description": "Creates a new short URL.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
"description": "Creates a new short URL.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.<br></br>**Param findIfExists:**: Starting with v1.16, this new param allows to force shlink to return existing short URLs when found based on provided params, instead of creating a new one. However, it might add complexity and have unexpected outputs.\n\nThese are the use cases:\n* Only the long URL is provided: It will return the newest match or create a new short URL if none is found.\n* Long url and custom slug are provided: It will return the short URL when both params match, return an error when the slug is in use for another long URL, or create a new short URL otherwise.\n* Any of the above but including other params (tags, validSince, validUntil, maxVisits): It will behave the same as the previous two cases, but it will try to exactly match existing results using all the params. If any of them does not match, it will try to create a new short URL.",
"security": [
{
"ApiKey": []
@@ -197,6 +197,10 @@
"maxVisits": {
"description": "The maximum number of allowed visits for this short code",
"type": "number"
},
"findIfExists": {
"description": "Will force existing matching URL to be returned if found, instead of creating a new one",
"type": "boolean"
}
}
}

View File

@@ -0,0 +1,36 @@
{
"get": {
"operationId": "shortUrl",
"tags": [
"URL Shortener"
],
"summary": "Short URL",
"description": "Represents a short URL. Tracks the visit and redirects tio the corresponding long URL",
"parameters": [
{
"name": "shortCode",
"in": "path",
"description": "The short code to resolve.",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"302": {
"description": "Visit properly tracked and redirected"
},
"500": {
"description": "Unexpected error.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,44 @@
{
"get": {
"operationId": "shortUrlPreview",
"tags": [
"URL Shortener"
],
"summary": "Short URL preview image",
"description": "Returns the preview of the page behind a short URL",
"parameters": [
{
"name": "shortCode",
"in": "path",
"description": "The short code to resolve.",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Image in PNG format",
"content": {
"image/png": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,56 @@
{
"get": {
"operationId": "shortUrlQrCode",
"tags": [
"URL Shortener"
],
"summary": "Short URL QR code",
"description": "Generates a QR code image pointing to a short URL",
"parameters": [
{
"name": "shortCode",
"in": "path",
"description": "The short code to resolve.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "size",
"in": "path",
"description": "The size of the image to be returned.",
"required": false,
"schema": {
"type": "integer",
"minimum": 50,
"maximum": 1000,
"default": 300
}
}
],
"responses": {
"200": {
"description": "QR code in PNG format",
"content": {
"image/png": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,44 @@
{
"get": {
"operationId": "trackShortUrl",
"tags": [
"URL Shortener"
],
"summary": "Short URL tracking pixel",
"description": "Generates a 1px transparent image which can be used to track emails with a short URL",
"parameters": [
{
"name": "shortCode",
"in": "path",
"description": "The short code to resolve.",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Image in GIF format",
"content": {
"image/gif": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
}
}

View File

@@ -6,15 +6,20 @@
"version": "1.0"
},
"externalDocs": {
"url": "https://shlink.io/api-docs",
"description": "Find more info on how to start using this API here"
},
"servers": [
{
"url": "{schema}://{server}/rest",
"url": "{scheme}://{host}",
"variables": {
"schema": {
"scheme": {
"default": "https",
"enum": ["https", "http"]
},
"server": {
"host": {
"default": ""
}
}
@@ -51,35 +56,60 @@
"name": "Visits",
"description": "Operations to manage visits on short URLs"
},
{
"name": "Monitoring",
"description": "Public endpoints designed to monitor the service"
},
{
"name": "URL Shortener",
"description": "Non-rest endpoints, used to be publicly exposed"
},
{
"name": "Authentication",
"description": "Authentication-related endpoints"
"description": "**[Deprecated]** Authentication-related endpoints"
}
],
"paths": {
"/v1/short-urls": {
"/rest/v1/short-urls": {
"$ref": "paths/v1_short-urls.json"
},
"/v1/short-urls/shorten": {
"/rest/v1/short-urls/shorten": {
"$ref": "paths/v1_short-urls_shorten.json"
},
"/v1/short-urls/{shortCode}": {
"/rest/v1/short-urls/{shortCode}": {
"$ref": "paths/v1_short-urls_{shortCode}.json"
},
"/v1/short-urls/{shortCode}/tags": {
"/rest/v1/short-urls/{shortCode}/tags": {
"$ref": "paths/v1_short-urls_{shortCode}_tags.json"
},
"/v1/tags": {
"/rest/v1/tags": {
"$ref": "paths/v1_tags.json"
},
"/v1/short-urls/{shortCode}/visits": {
"/rest/v1/short-urls/{shortCode}/visits": {
"$ref": "paths/v1_short-urls_{shortCode}_visits.json"
},
"/v1/authenticate": {
"/rest/health": {
"$ref": "paths/health.json"
},
"/{shortCode}": {
"$ref": "paths/{shortCode}.json"
},
"/{shortCode}/track": {
"$ref": "paths/{shortCode}_track.json"
},
"/{shortCode}/qr-code": {
"$ref": "paths/{shortCode}_qr-code.json"
},
"/{shortCode}/preview": {
"$ref": "paths/{shortCode}_preview.json"
},
"/rest/v1/authenticate": {
"$ref": "paths/v1_authenticate.json"
}
}

View File

@@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
use ShlinkioTest\Shlink\Common\DbUnit\DatabaseTestCase;
use Symfony\Component\Process\Process;
use Zend\ServiceManager\ServiceManager;
// Create an empty .env file
if (! file_exists('.env')) {
touch('.env');
}
$shlinkDbPath = realpath(sys_get_temp_dir()) . '/shlink-tests.db';
if (file_exists($shlinkDbPath)) {
unlink($shlinkDbPath);
}
/** @var ServiceManager $sm */
$sm = require __DIR__ . '/config/container.php';
$sm->setAllowOverride(true);
$config = $sm->get('config');
$config['entity_manager']['connection'] = [
'driver' => 'pdo_sqlite',
'path' => $shlinkDbPath,
];
$sm->setService('config', $config);
// Create database
$process = new Process('vendor/bin/doctrine orm:schema-tool:create --no-interaction -q --test', __DIR__);
$process->inheritEnvironmentVariables()
->mustRun();
DatabaseTestCase::$em = $sm->get('em');

View File

@@ -1,8 +1,7 @@
#!/usr/bin/env bash
# Run docker containers if they are not up yet
if [[ $(docker ps | grep shlink_swoole) ]]; then :
else
if ! [[ $(docker ps | grep shlink_swoole) ]]; then
docker-compose up -d
fi

View File

@@ -4,12 +4,14 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Api;
use InvalidArgumentException;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
class DisableKeyCommand extends Command
@@ -32,7 +34,7 @@ class DisableKeyCommand extends Command
->addArgument('apiKey', InputArgument::REQUIRED, 'The API key to disable');
}
protected function execute(InputInterface $input, OutputInterface $output): void
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$apiKey = $input->getArgument('apiKey');
$io = new SymfonyStyle($input, $output);
@@ -40,8 +42,10 @@ class DisableKeyCommand extends Command
try {
$this->apiKeyService->disable($apiKey);
$io->success(sprintf('API key "%s" properly disabled', $apiKey));
return ExitCodes::EXIT_SUCCESS;
} catch (InvalidArgumentException $e) {
$io->error(sprintf('API key "%s" does not exist.', $apiKey));
return ExitCodes::EXIT_FAILURE;
}
}
}

View File

@@ -4,12 +4,14 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Api;
use Cake\Chronos\Chronos;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
class GenerateKeyCommand extends Command
@@ -38,11 +40,12 @@ class GenerateKeyCommand extends Command
);
}
protected function execute(InputInterface $input, OutputInterface $output): void
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$expirationDate = $input->getOption('expirationDate');
$apiKey = $this->apiKeyService->create(isset($expirationDate) ? Chronos::parse($expirationDate) : null);
(new SymfonyStyle($input, $output))->success(sprintf('Generated API key: "%s"', $apiKey));
return ExitCodes::EXIT_SUCCESS;
}
}

View File

@@ -3,13 +3,15 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Api;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Common\Console\ShlinkTable;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function array_filter;
use function array_map;
use function sprintf;
@@ -44,9 +46,8 @@ class ListKeysCommand extends Command
);
}
protected function execute(InputInterface $input, OutputInterface $output): void
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$io = new SymfonyStyle($input, $output);
$enabledOnly = $input->getOption('enabledOnly');
$rows = array_map(function (ApiKey $apiKey) use ($enabledOnly) {
@@ -62,11 +63,12 @@ class ListKeysCommand extends Command
return $rowData;
}, $this->apiKeyService->listKeys($enabledOnly));
$io->table(array_filter([
ShlinkTable::fromOutput($output)->render(array_filter([
'Key',
! $enabledOnly ? 'Is enabled' : null,
'Expiration date',
]), $rows);
return ExitCodes::EXIT_SUCCESS;
}
private function determineMessagePattern(ApiKey $apiKey): string

View File

@@ -3,14 +3,17 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Config;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
use function str_shuffle;
/** @deprecated */
class GenerateCharsetCommand extends Command
{
public const NAME = 'config:generate-charset';
@@ -20,15 +23,17 @@ class GenerateCharsetCommand extends Command
$this
->setName(self::NAME)
->setDescription(sprintf(
'Generates a character set sample just by shuffling the default one, "%s". '
'[DEPRECATED] Generates a character set sample just by shuffling the default one, "%s". '
. 'Then it can be set in the SHORTCODE_CHARS environment variable',
UrlShortener::DEFAULT_CHARS
));
UrlShortenerOptions::DEFAULT_CHARS
))
->setHelp('<fg=red;options=bold>This command is deprecated. Better leave shlink generate the charset.</>');
}
protected function execute(InputInterface $input, OutputInterface $output): void
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$charSet = str_shuffle(UrlShortener::DEFAULT_CHARS);
$charSet = str_shuffle(UrlShortenerOptions::DEFAULT_CHARS);
(new SymfonyStyle($input, $output))->success(sprintf('Character set: "%s"', $charSet));
return ExitCodes::EXIT_SUCCESS;
}
}

View File

@@ -3,13 +3,16 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Config;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
/** @deprecated */
class GenerateSecretCommand extends Command
{
use StringUtilsTrait;
@@ -20,12 +23,16 @@ class GenerateSecretCommand extends Command
{
$this
->setName(self::NAME)
->setDescription('[DEPRECATED] Generates a random secret string that can be used for JWT token encryption');
->setDescription('[DEPRECATED] Generates a random secret string that can be used for JWT token encryption')
->setHelp(
'<fg=red;options=bold>This command is deprecated. Better leave shlink generate the secret key.</>'
);
}
protected function execute(InputInterface $input, OutputInterface $output): void
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$secret = $this->generateRandomString(32);
(new SymfonyStyle($input, $output))->success(sprintf('Secret key: "%s"', $secret));
return ExitCodes::EXIT_SUCCESS;
}
}

View File

@@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Exception;
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
use Symfony\Component\Console\Command\Command;
@@ -11,6 +12,7 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
class DeleteShortUrlCommand extends Command
@@ -43,7 +45,7 @@ class DeleteShortUrlCommand extends Command
);
}
protected function execute(InputInterface $input, OutputInterface $output): void
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$io = new SymfonyStyle($input, $output);
$shortCode = $input->getArgument('shortCode');
@@ -51,14 +53,16 @@ class DeleteShortUrlCommand extends Command
try {
$this->runDelete($io, $shortCode, $ignoreThreshold);
return ExitCodes::EXIT_SUCCESS;
} catch (Exception\InvalidShortCodeException $e) {
$io->error(sprintf('Provided short code "%s" could not be found.', $shortCode));
return ExitCodes::EXIT_FAILURE;
} catch (Exception\DeleteShortUrlException $e) {
$this->retry($io, $shortCode, $e);
return $this->retry($io, $shortCode, $e);
}
}
private function retry(SymfonyStyle $io, string $shortCode, Exception\DeleteShortUrlException $e): void
private function retry(SymfonyStyle $io, string $shortCode, Exception\DeleteShortUrlException $e): int
{
$warningMsg = sprintf(
'It was not possible to delete the short URL with short code "%s" because it has more than %s visits.',
@@ -73,6 +77,8 @@ class DeleteShortUrlCommand extends Command
} else {
$io->warning('Short URL was not deleted.');
}
return $forceDelete ? ExitCodes::EXIT_SUCCESS : ExitCodes::EXIT_WARNING;
}
private function runDelete(SymfonyStyle $io, string $shortCode, bool $ignoreThreshold): void

View File

@@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
@@ -10,6 +11,7 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
class GeneratePreviewCommand extends Command
@@ -39,7 +41,7 @@ class GeneratePreviewCommand extends Command
);
}
protected function execute(InputInterface $input, OutputInterface $output): void
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$page = 1;
do {
@@ -52,6 +54,7 @@ class GeneratePreviewCommand extends Command
} while ($page <= $shortUrls->count());
(new SymfonyStyle($input, $output))->success('Finished processing all URLs');
return ExitCodes::EXIT_SUCCESS;
}
private function processUrl($url, OutputInterface $output): void

View File

@@ -4,8 +4,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Cake\Chronos\Chronos;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Core\Util\ShortUrlBuilderTrait;
use Symfony\Component\Console\Command\Command;
@@ -15,8 +17,11 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\Diactoros\Uri;
use function array_merge;
use function explode;
use function array_map;
use function Functional\curry;
use function Functional\flatten;
use function Functional\unique;
use function sprintf;
class GenerateShortUrlCommand extends Command
@@ -76,6 +81,12 @@ class GenerateShortUrlCommand extends Command
'm',
InputOption::VALUE_REQUIRED,
'This will limit the number of visits for this short URL.'
)
->addOption(
'findIfExists',
'f',
InputOption::VALUE_NONE,
'This will force existing matching URL to be returned if found, instead of creating a new one.'
);
}
@@ -93,22 +104,17 @@ class GenerateShortUrlCommand extends Command
}
}
protected function execute(InputInterface $input, OutputInterface $output): void
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$io = new SymfonyStyle($input, $output);
$longUrl = $input->getArgument('longUrl');
if (empty($longUrl)) {
$io->error('A URL was not provided!');
return;
return ExitCodes::EXIT_FAILURE;
}
$tags = $input->getOption('tags');
$processedTags = [];
foreach ($tags as $key => $tag) {
$explodedTags = explode(',', $tag);
$processedTags = array_merge($processedTags, $explodedTags);
}
$tags = $processedTags;
$explodeWithComma = curry('explode')(',');
$tags = unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
$customSlug = $input->getOption('customSlug');
$maxVisits = $input->getOption('maxVisits');
@@ -116,10 +122,13 @@ class GenerateShortUrlCommand extends Command
$shortCode = $this->urlShortener->urlToShortCode(
new Uri($longUrl),
$tags,
$this->getOptionalDate($input, 'validSince'),
$this->getOptionalDate($input, 'validUntil'),
$customSlug,
$maxVisits !== null ? (int) $maxVisits : null
ShortUrlMeta::createFromParams(
$this->getOptionalDate($input, 'validSince'),
$this->getOptionalDate($input, 'validUntil'),
$customSlug,
$maxVisits !== null ? (int) $maxVisits : null,
$input->getOption('findIfExists')
)
)->getShortCode();
$shortUrl = $this->buildShortUrl($this->domainConfig, $shortCode);
@@ -127,12 +136,15 @@ class GenerateShortUrlCommand extends Command
sprintf('Processed long URL: <info>%s</info>', $longUrl),
sprintf('Generated short URL: <info>%s</info>', $shortUrl),
]);
return ExitCodes::EXIT_SUCCESS;
} catch (InvalidUrlException $e) {
$io->error(sprintf('Provided URL "%s" is invalid. Try with a different one.', $longUrl));
return ExitCodes::EXIT_FAILURE;
} catch (NonUniqueSlugException $e) {
$io->error(
sprintf('Provided slug "%s" is already in use by another URL. Try with a different one.', $customSlug)
);
return ExitCodes::EXIT_FAILURE;
}
}

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Cake\Chronos\Chronos;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Common\Console\ShlinkTable;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Model\VisitsParams;
@@ -15,6 +17,7 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\Stdlib\ArrayUtils;
use function array_map;
use function Functional\select_keys;
@@ -67,9 +70,8 @@ class GetVisitsCommand extends Command
}
}
protected function execute(InputInterface $input, OutputInterface $output): void
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$io = new SymfonyStyle($input, $output);
$shortCode = $input->getArgument('shortCode');
$startDate = $this->getDateOption($input, 'startDate');
$endDate = $this->getDateOption($input, 'endDate');
@@ -82,7 +84,8 @@ class GetVisitsCommand extends Command
$rowData['country'] = $visit->getVisitLocation()->getCountryName();
return select_keys($rowData, ['referer', 'date', 'userAgent', 'country']);
}, $visits);
$io->table(['Referer', 'Date', 'User agent', 'Country'], $rows);
ShlinkTable::fromOutput($output)->render(['Referer', 'Date', 'User agent', 'Country'], $rows);
return ExitCodes::EXIT_SUCCESS;
}
private function getDateOption(InputInterface $input, $key)

View File

@@ -3,8 +3,11 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Common\Console\ShlinkTable;
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
use Symfony\Component\Console\Command\Command;
@@ -12,6 +15,8 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\Paginator\Paginator;
use function array_values;
use function count;
use function explode;
@@ -71,46 +76,64 @@ class ListShortUrlsCommand extends Command
->addOption('showTags', null, InputOption::VALUE_NONE, 'Whether to display the tags or not');
}
protected function execute(InputInterface $input, OutputInterface $output): void
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$io = new SymfonyStyle($input, $output);
$page = (int) $input->getOption('page');
$searchTerm = $input->getOption('searchTerm');
$tags = $input->getOption('tags');
$tags = ! empty($tags) ? explode(',', $tags) : [];
$showTags = $input->getOption('showTags');
$showTags = (bool) $input->getOption('showTags');
$transformer = new ShortUrlDataTransformer($this->domainConfig);
do {
$result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, $this->processOrderBy($input));
$result = $this->renderPage($input, $output, $page, $searchTerm, $tags, $showTags, $transformer);
$page++;
$headers = ['Short code', 'Short URL', 'Long URL', 'Date created', 'Visits count'];
if ($showTags) {
$headers[] = 'Tags';
}
$rows = [];
foreach ($result as $row) {
$shortUrl = $transformer->transform($row);
if ($showTags) {
$shortUrl['tags'] = implode(', ', $shortUrl['tags']);
} else {
unset($shortUrl['tags']);
}
unset($shortUrl['originalUrl']);
$rows[] = array_values($shortUrl);
}
$io->table($headers, $rows);
if ($this->isLastPage($result)) {
$continue = false;
$io->success('Short URLs properly listed');
} else {
$continue = $io->confirm(sprintf('Continue with page <options=bold>%s</>?', $page), false);
}
$continue = $this->isLastPage($result)
? false
: $io->confirm(sprintf('Continue with page <options=bold>%s</>?', $page), false);
} while ($continue);
$io->newLine();
$io->success('Short URLs properly listed');
return ExitCodes::EXIT_SUCCESS;
}
private function renderPage(
InputInterface $input,
OutputInterface $output,
int $page,
?string $searchTerm,
array $tags,
bool $showTags,
DataTransformerInterface $transformer
): Paginator {
$result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, $this->processOrderBy($input));
$headers = ['Short code', 'Short URL', 'Long URL', 'Date created', 'Visits count'];
if ($showTags) {
$headers[] = 'Tags';
}
$rows = [];
foreach ($result as $row) {
$shortUrl = $transformer->transform($row);
if ($showTags) {
$shortUrl['tags'] = implode(', ', $shortUrl['tags']);
} else {
unset($shortUrl['tags']);
}
unset($shortUrl['originalUrl']);
$rows[] = array_values($shortUrl);
}
ShlinkTable::fromOutput($output)->render($headers, $rows, $this->formatCurrentPageMessage(
$result,
'Page %s of %s'
));
return $result;
}
private function processOrderBy(InputInterface $input)

View File

@@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
@@ -11,6 +12,7 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
class ResolveUrlCommand extends Command
@@ -50,7 +52,7 @@ class ResolveUrlCommand extends Command
}
}
protected function execute(InputInterface $input, OutputInterface $output): void
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$io = new SymfonyStyle($input, $output);
$shortCode = $input->getArgument('shortCode');
@@ -58,10 +60,13 @@ class ResolveUrlCommand extends Command
try {
$url = $this->urlShortener->shortCodeToUrl($shortCode);
$output->writeln(sprintf('Long URL: <info>%s</info>', $url->getLongUrl()));
return ExitCodes::EXIT_SUCCESS;
} catch (InvalidShortCodeException $e) {
$io->error(sprintf('Provided short code "%s" has an invalid format.', $shortCode));
return ExitCodes::EXIT_FAILURE;
} catch (EntityDoesNotExistException $e) {
$io->error(sprintf('Provided short code "%s" could not be found.', $shortCode));
return ExitCodes::EXIT_FAILURE;
}
}
}

View File

@@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
@@ -36,17 +37,18 @@ class CreateTagCommand extends Command
);
}
protected function execute(InputInterface $input, OutputInterface $output): void
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$io = new SymfonyStyle($input, $output);
$tagNames = $input->getOption('name');
if (empty($tagNames)) {
$io->warning('You have to provide at least one tag name');
return;
return ExitCodes::EXIT_WARNING;
}
$this->tagService->createTags($tagNames);
$io->success('Tags properly created');
return ExitCodes::EXIT_SUCCESS;
}
}

View File

@@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
@@ -36,17 +37,18 @@ class DeleteTagsCommand extends Command
);
}
protected function execute(InputInterface $input, OutputInterface $output): void
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$io = new SymfonyStyle($input, $output);
$tagNames = $input->getOption('name');
if (empty($tagNames)) {
$io->warning('You have to provide at least one tag name');
return;
return ExitCodes::EXIT_WARNING;
}
$this->tagService->deleteTags($tagNames);
$io->success('Tags properly deleted');
return ExitCodes::EXIT_SUCCESS;
}
}

View File

@@ -3,12 +3,14 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Common\Console\ShlinkTable;
use Shlinkio\Shlink\Core\Entity\Tag;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function Functional\map;
class ListTagsCommand extends Command
@@ -31,10 +33,10 @@ class ListTagsCommand extends Command
->setDescription('Lists existing tags.');
}
protected function execute(InputInterface $input, OutputInterface $output): void
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$io = new SymfonyStyle($input, $output);
$io->table(['Name'], $this->getTagsRows());
ShlinkTable::fromOutput($output)->render(['Name'], $this->getTagsRows());
return ExitCodes::EXIT_SUCCESS;
}
private function getTagsRows(): array

View File

@@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Symfony\Component\Console\Command\Command;
@@ -10,6 +11,7 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
class RenameTagCommand extends Command
@@ -34,7 +36,7 @@ class RenameTagCommand extends Command
->addArgument('newName', InputArgument::REQUIRED, 'New name of the tag.');
}
protected function execute(InputInterface $input, OutputInterface $output): void
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$io = new SymfonyStyle($input, $output);
$oldName = $input->getArgument('oldName');
@@ -43,8 +45,10 @@ class RenameTagCommand extends Command
try {
$this->tagService->renameTag($oldName, $newName);
$io->success('Tag properly renamed.');
return ExitCodes::EXIT_SUCCESS;
} catch (EntityDoesNotExistException $e) {
$io->error(sprintf('A tag with name "%s" was not found', $oldName));
return ExitCodes::EXIT_FAILURE;
}
}
}

View File

@@ -3,8 +3,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Visit;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
use Shlinkio\Shlink\Common\Util\IpAddress;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation;
@@ -15,6 +17,7 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Lock\Factory as Locker;
use function sprintf;
class ProcessVisitsCommand extends Command
@@ -48,7 +51,7 @@ class ProcessVisitsCommand extends Command
->setDescription('Processes visits where location is not set yet');
}
protected function execute(InputInterface $input, OutputInterface $output): void
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$this->output = $output;
$io = new SymfonyStyle($input, $output);
@@ -56,38 +59,43 @@ class ProcessVisitsCommand extends Command
$lock = $this->locker->createLock(self::NAME);
if (! $lock->acquire()) {
$io->warning(sprintf('There is already an instance of the "%s" command in execution', self::NAME));
return;
return ExitCodes::EXIT_WARNING;
}
try {
$this->visitService->locateVisits(
$this->visitService->locateUnlocatedVisits(
[$this, 'getGeolocationDataForVisit'],
function (VisitLocation $location) use ($output) {
$output->writeln(sprintf(' [<info>Address located at "%s"</info>]', $location->getCountryName()));
if (! $location->isEmpty()) {
$output->writeln(
sprintf(' [<info>Address located at "%s"</info>]', $location->getCountryName())
);
}
}
);
$io->success('Finished processing all IPs');
} finally {
$lock->release();
return ExitCodes::EXIT_SUCCESS;
}
}
public function getGeolocationDataForVisit(Visit $visit): array
public function getGeolocationDataForVisit(Visit $visit): Location
{
if (! $visit->hasRemoteAddr()) {
$this->output->writeln(
'<comment>Ignored visit with no IP address</comment>',
OutputInterface::VERBOSITY_VERBOSE
);
throw new IpCannotBeLocatedException('Ignored visit with no IP address');
throw IpCannotBeLocatedException::forEmptyAddress();
}
$ipAddr = $visit->getRemoteAddr();
$this->output->write(sprintf('Processing IP <fg=blue>%s</>', $ipAddr));
if ($ipAddr === IpAddress::LOCALHOST) {
$this->output->writeln(' [<comment>Ignored localhost address</comment>]');
throw new IpCannotBeLocatedException('Ignored localhost address');
throw IpCannotBeLocatedException::forLocalhost();
}
try {
@@ -98,7 +106,7 @@ class ProcessVisitsCommand extends Command
$this->getApplication()->renderException($e, $this->output);
}
throw new IpCannotBeLocatedException('An error occurred while locating IP', $e->getCode(), $e);
throw IpCannotBeLocatedException::forError($e);
}
}
}

View File

@@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Visit;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Common\Exception\RuntimeException;
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdaterInterface;
use Symfony\Component\Console\Command\Command;
@@ -35,7 +36,7 @@ class UpdateDbCommand extends Command
);
}
protected function execute(InputInterface $input, OutputInterface $output): void
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$io = new SymfonyStyle($input, $output);
$progressBar = new ProgressBar($output);
@@ -51,6 +52,7 @@ class UpdateDbCommand extends Command
$io->writeln('');
$io->success('GeoLite2 database properly updated');
return ExitCodes::EXIT_SUCCESS;
} catch (RuntimeException $e) {
$progressBar->finish();
$io->writeln('');
@@ -59,6 +61,7 @@ class UpdateDbCommand extends Command
if ($io->isVerbose()) {
$this->getApplication()->renderException($e, $output);
}
return ExitCodes::EXIT_FAILURE;
}
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Util;
final class ExitCodes
{
public const EXIT_SUCCESS = 0;
public const EXIT_FAILURE = -1;
public const EXIT_WARNING = 1;
}

View File

@@ -18,7 +18,7 @@ class DisableKeyCommandTest extends TestCase
/** @var ObjectProphecy */
private $apiKeyService;
public function setUp()
public function setUp(): void
{
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
$command = new DisableKeyCommand($this->apiKeyService->reveal());
@@ -27,9 +27,7 @@ class DisableKeyCommandTest extends TestCase
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
/** @test */
public function providedApiKeyIsDisabled()
{
$apiKey = 'abcd1234';
@@ -41,12 +39,10 @@ class DisableKeyCommandTest extends TestCase
]);
$output = $this->commandTester->getDisplay();
$this->assertContains('API key "abcd1234" properly disabled', $output);
$this->assertStringContainsString('API key "abcd1234" properly disabled', $output);
}
/**
* @test
*/
/** @test */
public function errorIsReturnedIfServiceThrowsException()
{
$apiKey = 'abcd1234';
@@ -58,7 +54,7 @@ class DisableKeyCommandTest extends TestCase
]);
$output = $this->commandTester->getDisplay();
$this->assertContains('API key "abcd1234" does not exist.', $output);
$this->assertStringContainsString('API key "abcd1234" does not exist.', $output);
$disable->shouldHaveBeenCalledOnce();
}
}

View File

@@ -20,7 +20,7 @@ class GenerateKeyCommandTest extends TestCase
/** @var ObjectProphecy */
private $apiKeyService;
public function setUp()
public function setUp(): void
{
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
$command = new GenerateKeyCommand($this->apiKeyService->reveal());
@@ -29,9 +29,7 @@ class GenerateKeyCommandTest extends TestCase
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
/** @test */
public function noExpirationDateIsDefinedIfNotProvided()
{
$create = $this->apiKeyService->create(null)->willReturn(new ApiKey());
@@ -41,13 +39,11 @@ class GenerateKeyCommandTest extends TestCase
]);
$output = $this->commandTester->getDisplay();
$this->assertContains('Generated API key: ', $output);
$this->assertStringContainsString('Generated API key: ', $output);
$create->shouldHaveBeenCalledOnce();
}
/**
* @test
*/
/** @test */
public function expirationDateIsDefinedIfProvided()
{
$this->apiKeyService->create(Argument::type(Chronos::class))->shouldBeCalledOnce()

View File

@@ -18,7 +18,7 @@ class ListKeysCommandTest extends TestCase
/** @var ObjectProphecy */
private $apiKeyService;
public function setUp()
public function setUp(): void
{
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
$command = new ListKeysCommand($this->apiKeyService->reveal());
@@ -27,10 +27,8 @@ class ListKeysCommandTest extends TestCase
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function everythingIsListedIfEnabledOnlyIsNotProvided()
/** @test */
public function everythingIsListedIfEnabledOnlyIsNotProvided(): void
{
$this->apiKeyService->listKeys(false)->willReturn([
new ApiKey(),
@@ -38,22 +36,18 @@ class ListKeysCommandTest extends TestCase
new ApiKey(),
])->shouldBeCalledOnce();
$this->commandTester->execute([
'command' => ListKeysCommand::NAME,
]);
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$this->assertContains('Key', $output);
$this->assertContains('Is enabled', $output);
$this->assertContains(' +++ ', $output);
$this->assertNotContains(' --- ', $output);
$this->assertContains('Expiration date', $output);
$this->assertStringContainsString('Key', $output);
$this->assertStringContainsString('Is enabled', $output);
$this->assertStringContainsString(' +++ ', $output);
$this->assertStringNotContainsString(' --- ', $output);
$this->assertStringContainsString('Expiration date', $output);
}
/**
* @test
*/
public function onlyEnabledKeysAreListedIfEnabledOnlyIsProvided()
/** @test */
public function onlyEnabledKeysAreListedIfEnabledOnlyIsProvided(): void
{
$this->apiKeyService->listKeys(true)->willReturn([
(new ApiKey())->disable(),
@@ -61,15 +55,14 @@ class ListKeysCommandTest extends TestCase
])->shouldBeCalledOnce();
$this->commandTester->execute([
'command' => ListKeysCommand::NAME,
'--enabledOnly' => true,
]);
$output = $this->commandTester->getDisplay();
$this->assertContains('Key', $output);
$this->assertNotContains('Is enabled', $output);
$this->assertNotContains(' +++ ', $output);
$this->assertNotContains(' --- ', $output);
$this->assertContains('Expiration date', $output);
$this->assertStringContainsString('Key', $output);
$this->assertStringNotContainsString('Is enabled', $output);
$this->assertStringNotContainsString(' +++ ', $output);
$this->assertStringNotContainsString(' --- ', $output);
$this->assertStringContainsString('Expiration date', $output);
}
}

View File

@@ -7,6 +7,7 @@ use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Config\GenerateCharsetCommand;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use function implode;
use function sort;
use function str_split;
@@ -16,7 +17,7 @@ class GenerateCharsetCommandTest extends TestCase
/** @var CommandTester */
private $commandTester;
public function setUp()
public function setUp(): void
{
$command = new GenerateCharsetCommand();
$app = new Application();
@@ -25,9 +26,7 @@ class GenerateCharsetCommandTest extends TestCase
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
/** @test */
public function charactersAreGeneratedFromDefault()
{
$prefix = 'Character set: ';
@@ -38,7 +37,7 @@ class GenerateCharsetCommandTest extends TestCase
$output = $this->commandTester->getDisplay();
// Both default character set and the new one should have the same length
$this->assertContains($prefix, $output);
$this->assertStringContainsString($prefix, $output);
}
protected function orderStringLetters($string)

View File

@@ -11,17 +11,20 @@ use Shlinkio\Shlink\Core\Exception;
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use const PHP_EOL;
use function array_pop;
use function sprintf;
class DeleteShortCodeCommandTest extends TestCase
class DeleteShortUrlCommandTest extends TestCase
{
/** @var CommandTester */
private $commandTester;
/** @var ObjectProphecy */
private $service;
public function setUp()
public function setUp(): void
{
$this->service = $this->prophesize(DeleteShortUrlServiceInterface::class);
@@ -32,10 +35,8 @@ class DeleteShortCodeCommandTest extends TestCase
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function successMessageIsPrintedIfUrlIsProperlyDeleted()
/** @test */
public function successMessageIsPrintedIfUrlIsProperlyDeleted(): void
{
$shortCode = 'abc123';
$deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->will(function () {
@@ -44,14 +45,15 @@ class DeleteShortCodeCommandTest extends TestCase
$this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay();
$this->assertContains(sprintf('Short URL with short code "%s" successfully deleted.', $shortCode), $output);
$this->assertStringContainsString(
sprintf('Short URL with short code "%s" successfully deleted.', $shortCode),
$output
);
$deleteByShortCode->shouldHaveBeenCalledOnce();
}
/**
* @test
*/
public function invalidShortCodePrintsMessage()
/** @test */
public function invalidShortCodePrintsMessage(): void
{
$shortCode = 'abc123';
$deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->willThrow(
@@ -61,15 +63,19 @@ class DeleteShortCodeCommandTest extends TestCase
$this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay();
$this->assertContains(sprintf('Provided short code "%s" could not be found.', $shortCode), $output);
$this->assertStringContainsString(sprintf('Provided short code "%s" could not be found.', $shortCode), $output);
$deleteByShortCode->shouldHaveBeenCalledOnce();
}
/**
* @test
* @dataProvider provideRetryDeleteAnswers
*/
public function deleteIsRetriedWhenThresholdIsReachedAndQuestionIsAccepted()
{
public function deleteIsRetriedWhenThresholdIsReachedAndQuestionIsAccepted(
array $retryAnswer,
int $expectedDeleteCalls,
string $expectedMessage
): void {
$shortCode = 'abc123';
$deleteByShortCode = $this->service->deleteByShortCode($shortCode, Argument::type('bool'))->will(
function (array $args) {
@@ -80,23 +86,28 @@ class DeleteShortCodeCommandTest extends TestCase
}
}
);
$this->commandTester->setInputs(['yes']);
$this->commandTester->setInputs($retryAnswer);
$this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay();
$this->assertContains(sprintf(
$this->assertStringContainsString(sprintf(
'It was not possible to delete the short URL with short code "%s" because it has more than 10 visits.',
$shortCode
), $output);
$this->assertContains(sprintf('Short URL with short code "%s" successfully deleted.', $shortCode), $output);
$deleteByShortCode->shouldHaveBeenCalledTimes(2);
$this->assertStringContainsString($expectedMessage, $output);
$deleteByShortCode->shouldHaveBeenCalledTimes($expectedDeleteCalls);
}
/**
* @test
*/
public function deleteIsNotRetriedWhenThresholdIsReachedAndQuestionIsDeclined()
public 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 */
public function deleteIsNotRetriedWhenThresholdIsReachedAndQuestionIsDeclined(): void
{
$shortCode = 'abc123';
$deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->willThrow(
@@ -107,11 +118,11 @@ class DeleteShortCodeCommandTest extends TestCase
$this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay();
$this->assertContains(sprintf(
$this->assertStringContainsString(sprintf(
'It was not possible to delete the short URL with short code "%s" because it has more than 10 visits.',
$shortCode
), $output);
$this->assertContains('Short URL was not deleted.', $output);
$this->assertStringContainsString('Short URL was not deleted.', $output);
$deleteByShortCode->shouldHaveBeenCalledOnce();
}
}

View File

@@ -15,6 +15,7 @@ use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\Paginator\Adapter\ArrayAdapter;
use Zend\Paginator\Paginator;
use function count;
use function substr_count;
@@ -27,7 +28,7 @@ class GeneratePreviewCommandTest extends TestCase
/** @var ObjectProphecy */
private $shortUrlService;
public function setUp()
public function setUp(): void
{
$this->previewGenerator = $this->prophesize(PreviewGenerator::class);
$this->shortUrlService = $this->prophesize(ShortUrlService::class);
@@ -39,9 +40,7 @@ class GeneratePreviewCommandTest extends TestCase
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
/** @test */
public function previewsForEveryUrlAreGenerated()
{
$paginator = $this->createPaginator([
@@ -60,18 +59,16 @@ class GeneratePreviewCommandTest extends TestCase
]);
$output = $this->commandTester->getDisplay();
$this->assertContains('Processing URL http://foo.com', $output);
$this->assertContains('Processing URL https://bar.com', $output);
$this->assertContains('Processing URL http://baz.com/something', $output);
$this->assertContains('Finished processing all URLs', $output);
$this->assertStringContainsString('Processing URL http://foo.com', $output);
$this->assertStringContainsString('Processing URL https://bar.com', $output);
$this->assertStringContainsString('Processing URL http://baz.com/something', $output);
$this->assertStringContainsString('Finished processing all URLs', $output);
$generatePreview1->shouldHaveBeenCalledOnce();
$generatePreview2->shouldHaveBeenCalledOnce();
$generatePreview3->shouldHaveBeenCalledOnce();
}
/**
* @test
*/
/** @test */
public function exceptionWillOutputError()
{
$items = [

View File

@@ -22,7 +22,7 @@ class GenerateShortUrlCommandTest extends TestCase
/** @var ObjectProphecy */
private $urlShortener;
public function setUp()
public function setUp(): void
{
$this->urlShortener = $this->prophesize(UrlShortener::class);
$command = new GenerateShortUrlCommand($this->urlShortener->reveal(), [
@@ -34,9 +34,7 @@ class GenerateShortUrlCommandTest extends TestCase
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
/** @test */
public function properShortCodeIsCreatedIfLongUrlIsCorrect()
{
$urlToShortCode = $this->urlShortener->urlToShortCode(Argument::cetera())->willReturn(
@@ -50,13 +48,11 @@ class GenerateShortUrlCommandTest extends TestCase
]);
$output = $this->commandTester->getDisplay();
$this->assertContains('http://foo.com/abc123', $output);
$this->assertStringContainsString('http://foo.com/abc123', $output);
$urlToShortCode->shouldHaveBeenCalledOnce();
}
/**
* @test
*/
/** @test */
public function exceptionWhileParsingLongUrlOutputsError()
{
$this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(new InvalidUrlException())
@@ -67,15 +63,13 @@ class GenerateShortUrlCommandTest extends TestCase
'longUrl' => 'http://domain.com/invalid',
]);
$output = $this->commandTester->getDisplay();
$this->assertContains(
$this->assertStringContainsString(
'Provided URL "http://domain.com/invalid" is invalid.',
$output
);
}
/**
* @test
*/
/** @test */
public function properlyProcessesProvidedTags()
{
$urlToShortCode = $this->urlShortener->urlToShortCode(
@@ -90,11 +84,11 @@ class GenerateShortUrlCommandTest extends TestCase
$this->commandTester->execute([
'command' => 'shortcode:generate',
'longUrl' => 'http://domain.com/foo/bar',
'--tags' => ['foo,bar', 'baz', 'boo,zar'],
'--tags' => ['foo,bar', 'baz', 'boo,zar,baz'],
]);
$output = $this->commandTester->getDisplay();
$this->assertContains('http://foo.com/abc123', $output);
$this->assertStringContainsString('http://foo.com/abc123', $output);
$urlToShortCode->shouldHaveBeenCalledOnce();
}
}

View File

@@ -8,6 +8,7 @@ use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\ShortUrl\GetVisitsCommand;
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit;
@@ -27,7 +28,7 @@ class GetVisitsCommandTest extends TestCase
/** @var ObjectProphecy */
private $visitsTracker;
public function setUp()
public function setUp(): void
{
$this->visitsTracker = $this->prophesize(VisitsTrackerInterface::class);
$command = new GetVisitsCommand($this->visitsTracker->reveal());
@@ -36,9 +37,7 @@ class GetVisitsCommandTest extends TestCase
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
/** @test */
public function noDateFlagsTriesToListWithoutDateRange()
{
$shortCode = 'abc123';
@@ -52,9 +51,7 @@ class GetVisitsCommandTest extends TestCase
]);
}
/**
* @test
*/
/** @test */
public function providingDateFlagsTheListGetsFiltered()
{
$shortCode = 'abc123';
@@ -75,16 +72,14 @@ class GetVisitsCommandTest extends TestCase
]);
}
/**
* @test
*/
public function outputIsProperlyGenerated()
/** @test */
public function outputIsProperlyGenerated(): void
{
$shortCode = 'abc123';
$this->visitsTracker->info($shortCode, Argument::any())->willReturn(
new Paginator(new ArrayAdapter([
(new Visit(new ShortUrl(''), new Visitor('bar', 'foo', '')))->locate(
new VisitLocation(['country_name' => 'Spain'])
new VisitLocation(new Location('', 'Spain', '', '', 0, 0, ''))
),
]))
)->shouldBeCalledOnce();
@@ -94,8 +89,8 @@ class GetVisitsCommandTest extends TestCase
'shortCode' => $shortCode,
]);
$output = $this->commandTester->getDisplay();
$this->assertContains('foo', $output);
$this->assertContains('Spain', $output);
$this->assertContains('bar', $output);
$this->assertStringContainsString('foo', $output);
$this->assertStringContainsString('Spain', $output);
$this->assertStringContainsString('bar', $output);
}
}

View File

@@ -21,7 +21,7 @@ class ListShortUrlsCommandTest extends TestCase
/** @var ObjectProphecy */
private $shortUrlService;
public function setUp()
public function setUp(): void
{
$this->shortUrlService = $this->prophesize(ShortUrlServiceInterface::class);
$app = new Application();
@@ -30,9 +30,7 @@ class ListShortUrlsCommandTest extends TestCase
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
/** @test */
public function noInputCallsListJustOnce()
{
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
@@ -42,9 +40,7 @@ class ListShortUrlsCommandTest extends TestCase
$this->commandTester->execute(['command' => 'shortcode:list']);
}
/**
* @test
*/
/** @test */
public function loadingMorePagesCallsListMoreTimes()
{
// The paginator will return more than one page
@@ -61,14 +57,12 @@ class ListShortUrlsCommandTest extends TestCase
$this->commandTester->execute(['command' => 'shortcode:list']);
$output = $this->commandTester->getDisplay();
$this->assertContains('Continue with page 2?', $output);
$this->assertContains('Continue with page 3?', $output);
$this->assertContains('Continue with page 4?', $output);
$this->assertStringContainsString('Continue with page 2?', $output);
$this->assertStringContainsString('Continue with page 3?', $output);
$this->assertStringContainsString('Continue with page 4?', $output);
}
/**
* @test
*/
/** @test */
public function havingMorePagesButAnsweringNoCallsListJustOnce()
{
// The paginator will return more than one page
@@ -84,18 +78,16 @@ class ListShortUrlsCommandTest extends TestCase
$this->commandTester->execute(['command' => 'shortcode:list']);
$output = $this->commandTester->getDisplay();
$this->assertContains('url_1', $output);
$this->assertContains('url_9', $output);
$this->assertNotContains('url_10', $output);
$this->assertNotContains('url_20', $output);
$this->assertNotContains('url_30', $output);
$this->assertContains('Continue with page 2?', $output);
$this->assertNotContains('Continue with page 3?', $output);
$this->assertStringContainsString('url_1', $output);
$this->assertStringContainsString('url_9', $output);
$this->assertStringNotContainsString('url_10', $output);
$this->assertStringNotContainsString('url_20', $output);
$this->assertStringNotContainsString('url_30', $output);
$this->assertStringContainsString('Continue with page 2?', $output);
$this->assertStringNotContainsString('Continue with page 3?', $output);
}
/**
* @test
*/
/** @test */
public function passingPageWillMakeListStartOnThatPage()
{
$page = 5;
@@ -109,9 +101,7 @@ class ListShortUrlsCommandTest extends TestCase
]);
}
/**
* @test
*/
/** @test */
public function ifTagsFlagIsProvidedTagsColumnIsIncluded()
{
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
@@ -123,6 +113,6 @@ class ListShortUrlsCommandTest extends TestCase
'--showTags' => true,
]);
$output = $this->commandTester->getDisplay();
$this->assertContains('Tags', $output);
$this->assertStringContainsString('Tags', $output);
}
}

View File

@@ -12,6 +12,7 @@ use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use const PHP_EOL;
class ResolveUrlCommandTest extends TestCase
@@ -21,7 +22,7 @@ class ResolveUrlCommandTest extends TestCase
/** @var ObjectProphecy */
private $urlShortener;
public function setUp()
public function setUp(): void
{
$this->urlShortener = $this->prophesize(UrlShortener::class);
$command = new ResolveUrlCommand($this->urlShortener->reveal());
@@ -31,9 +32,7 @@ class ResolveUrlCommandTest extends TestCase
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
/** @test */
public function correctShortCodeResolvesUrl()
{
$shortCode = 'abc123';
@@ -50,9 +49,7 @@ class ResolveUrlCommandTest extends TestCase
$this->assertEquals('Long URL: ' . $expectedUrl . PHP_EOL, $output);
}
/**
* @test
*/
/** @test */
public function incorrectShortCodeOutputsErrorMessage()
{
$shortCode = 'abc123';
@@ -64,12 +61,10 @@ class ResolveUrlCommandTest extends TestCase
'shortCode' => $shortCode,
]);
$output = $this->commandTester->getDisplay();
$this->assertContains('Provided short code "' . $shortCode . '" could not be found.', $output);
$this->assertStringContainsString('Provided short code "' . $shortCode . '" could not be found.', $output);
}
/**
* @test
*/
/** @test */
public function wrongShortCodeFormatOutputsErrorMessage()
{
$shortCode = 'abc123';
@@ -81,6 +76,6 @@ class ResolveUrlCommandTest extends TestCase
'shortCode' => $shortCode,
]);
$output = $this->commandTester->getDisplay();
$this->assertContains('Provided short code "' . $shortCode . '" has an invalid format.', $output);
$this->assertStringContainsString('Provided short code "' . $shortCode . '" has an invalid format.', $output);
}
}

View File

@@ -5,7 +5,6 @@ namespace ShlinkioTest\Shlink\CLI\Command\Tag;
use Doctrine\Common\Collections\ArrayCollection;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Tag\CreateTagCommand;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
@@ -19,7 +18,7 @@ class CreateTagCommandTest extends TestCase
/** @var ObjectProphecy */
private $tagService;
public function setUp()
public function setUp(): void
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
@@ -30,24 +29,19 @@ class CreateTagCommandTest extends TestCase
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
/** @test */
public function errorIsReturnedWhenNoTagsAreProvided()
{
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$this->assertContains('You have to provide at least one tag name', $output);
$this->assertStringContainsString('You have to provide at least one tag name', $output);
}
/**
* @test
*/
/** @test */
public function serviceIsInvokedOnSuccess()
{
$tagNames = ['foo', 'bar'];
/** @var MethodProphecy $createTags */
$createTags = $this->tagService->createTags($tagNames)->willReturn(new ArrayCollection());
$this->commandTester->execute([
@@ -55,7 +49,7 @@ class CreateTagCommandTest extends TestCase
]);
$output = $this->commandTester->getDisplay();
$this->assertContains('Tags properly created', $output);
$this->assertStringContainsString('Tags properly created', $output);
$createTags->shouldHaveBeenCalled();
}
}

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Tag\DeleteTagsCommand;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
@@ -20,7 +19,7 @@ class DeleteTagsCommandTest extends TestCase
/** @var ObjectProphecy */
private $tagService;
public function setUp()
public function setUp(): void
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
@@ -31,24 +30,19 @@ class DeleteTagsCommandTest extends TestCase
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
/** @test */
public function errorIsReturnedWhenNoTagsAreProvided()
{
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$this->assertContains('You have to provide at least one tag name', $output);
$this->assertStringContainsString('You have to provide at least one tag name', $output);
}
/**
* @test
*/
/** @test */
public function serviceIsInvokedOnSuccess()
{
$tagNames = ['foo', 'bar'];
/** @var MethodProphecy $deleteTags */
$deleteTags = $this->tagService->deleteTags($tagNames)->will(function () {
});
@@ -57,7 +51,7 @@ class DeleteTagsCommandTest extends TestCase
]);
$output = $this->commandTester->getDisplay();
$this->assertContains('Tags properly deleted', $output);
$this->assertStringContainsString('Tags properly deleted', $output);
$deleteTags->shouldHaveBeenCalled();
}
}

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Tag\ListTagsCommand;
use Shlinkio\Shlink\Core\Entity\Tag;
@@ -21,7 +20,7 @@ class ListTagsCommandTest extends TestCase
/** @var ObjectProphecy */
private $tagService;
public function setUp()
public function setUp(): void
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
@@ -32,27 +31,21 @@ class ListTagsCommandTest extends TestCase
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
/** @test */
public function noTagsPrintsEmptyMessage()
{
/** @var MethodProphecy $listTags */
$listTags = $this->tagService->listTags()->willReturn([]);
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$this->assertContains('No tags yet', $output);
$this->assertStringContainsString('No tags yet', $output);
$listTags->shouldHaveBeenCalled();
}
/**
* @test
*/
/** @test */
public function listOfTagsIsPrinted()
{
/** @var MethodProphecy $listTags */
$listTags = $this->tagService->listTags()->willReturn([
new Tag('foo'),
new Tag('bar'),
@@ -61,8 +54,8 @@ class ListTagsCommandTest extends TestCase
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$this->assertContains('foo', $output);
$this->assertContains('bar', $output);
$this->assertStringContainsString('foo', $output);
$this->assertStringContainsString('bar', $output);
$listTags->shouldHaveBeenCalled();
}
}

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Tag\RenameTagCommand;
use Shlinkio\Shlink\Core\Entity\Tag;
@@ -22,7 +21,7 @@ class RenameTagCommandTest extends TestCase
/** @var ObjectProphecy */
private $tagService;
public function setUp()
public function setUp(): void
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
@@ -33,14 +32,11 @@ class RenameTagCommandTest extends TestCase
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
/** @test */
public function errorIsPrintedIfExceptionIsThrown()
{
$oldName = 'foo';
$newName = 'bar';
/** @var MethodProphecy $renameTag */
$renameTag = $this->tagService->renameTag($oldName, $newName)->willThrow(EntityDoesNotExistException::class);
$this->commandTester->execute([
@@ -49,18 +45,15 @@ class RenameTagCommandTest extends TestCase
]);
$output = $this->commandTester->getDisplay();
$this->assertContains('A tag with name "foo" was not found', $output);
$this->assertStringContainsString('A tag with name "foo" was not found', $output);
$renameTag->shouldHaveBeenCalled();
}
/**
* @test
*/
/** @test */
public function successIsPrintedIfNoErrorOccurs()
{
$oldName = 'foo';
$newName = 'bar';
/** @var MethodProphecy $renameTag */
$renameTag = $this->tagService->renameTag($oldName, $newName)->willReturn(new Tag($newName));
$this->commandTester->execute([
@@ -69,7 +62,7 @@ class RenameTagCommandTest extends TestCase
]);
$output = $this->commandTester->getDisplay();
$this->assertContains('Tag properly renamed', $output);
$this->assertStringContainsString('Tag properly renamed', $output);
$renameTag->shouldHaveBeenCalled();
}
}

View File

@@ -9,18 +9,18 @@ use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Visit\ProcessVisitsCommand;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Common\IpGeolocation\IpApiLocationResolver;
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
use Shlinkio\Shlink\Common\Util\IpAddress;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Service\VisitService;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Lock;
use Throwable;
use function array_shift;
use function sprintf;
@@ -37,7 +37,7 @@ class ProcessVisitsCommandTest extends TestCase
/** @var ObjectProphecy */
private $lock;
public function setUp()
public function setUp(): void
{
$this->visitService = $this->prophesize(VisitService::class);
$this->ipResolver = $this->prophesize(IpApiLocationResolver::class);
@@ -60,15 +60,13 @@ class ProcessVisitsCommandTest extends TestCase
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function allPendingVisitsAreProcessed()
/** @test */
public function allPendingVisitsAreProcessed(): void
{
$visit = new Visit(new ShortUrl(''), new Visitor('', '', '1.2.3.4'));
$location = new VisitLocation([]);
$location = new VisitLocation(Location::emptyInstance());
$locateVisits = $this->visitService->locateVisits(Argument::cetera())->will(
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(
function (array $args) use ($visit, $location) {
$firstCallback = array_shift($args);
$firstCallback($visit);
@@ -77,14 +75,16 @@ class ProcessVisitsCommandTest extends TestCase
$secondCallback($location, $visit);
}
);
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([]);
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn(
Location::emptyInstance()
);
$this->commandTester->execute([
'command' => 'visit:process',
]);
$output = $this->commandTester->getDisplay();
$this->assertContains('Processing IP 1.2.3.0', $output);
$this->assertStringContainsString('Processing IP 1.2.3.0', $output);
$locateVisits->shouldHaveBeenCalledOnce();
$resolveIpLocation->shouldHaveBeenCalledOnce();
}
@@ -93,12 +93,12 @@ class ProcessVisitsCommandTest extends TestCase
* @test
* @dataProvider provideIgnoredAddresses
*/
public function localhostAndEmptyAddressesAreIgnored(?string $address, string $message)
public function localhostAndEmptyAddressesAreIgnored(?string $address, string $message): void
{
$visit = new Visit(new ShortUrl(''), new Visitor('', '', $address));
$location = new VisitLocation([]);
$location = new VisitLocation(Location::emptyInstance());
$locateVisits = $this->visitService->locateVisits(Argument::cetera())->will(
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(
function (array $args) use ($visit, $location) {
$firstCallback = array_shift($args);
$firstCallback($visit);
@@ -107,41 +107,39 @@ class ProcessVisitsCommandTest extends TestCase
$secondCallback($location, $visit);
}
);
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([]);
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn(
Location::emptyInstance()
);
try {
$this->commandTester->execute([
'command' => 'visit:process',
], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
} catch (Throwable $e) {
$output = $this->commandTester->getDisplay();
$this->commandTester->execute([
'command' => 'visit:process',
], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
$this->assertInstanceOf(IpCannotBeLocatedException::class, $e);
$this->assertContains($message, $output);
$locateVisits->shouldHaveBeenCalledOnce();
$resolveIpLocation->shouldNotHaveBeenCalled();
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString($message, $output);
if (empty($address)) {
$this->assertStringNotContainsString('Processing IP', $output);
} else {
$this->assertStringContainsString('Processing IP', $output);
}
$locateVisits->shouldHaveBeenCalledOnce();
$resolveIpLocation->shouldNotHaveBeenCalled();
}
public function provideIgnoredAddresses(): array
public function provideIgnoredAddresses(): iterable
{
return [
['', 'Ignored visit with no IP address'],
[null, 'Ignored visit with no IP address'],
[IpAddress::LOCALHOST, 'Ignored localhost address'],
];
yield 'with empty address' => ['', 'Ignored visit with no IP address'];
yield 'with null address' => [null, 'Ignored visit with no IP address'];
yield 'with localhost address' => [IpAddress::LOCALHOST, 'Ignored localhost address'];
}
/**
* @test
*/
public function errorWhileLocatingIpIsDisplayed()
/** @test */
public function errorWhileLocatingIpIsDisplayed(): void
{
$visit = new Visit(new ShortUrl(''), new Visitor('', '', '1.2.3.4'));
$location = new VisitLocation([]);
$location = new VisitLocation(Location::emptyInstance());
$locateVisits = $this->visitService->locateVisits(Argument::cetera())->will(
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(
function (array $args) use ($visit, $location) {
$firstCallback = array_shift($args);
$firstCallback($visit);
@@ -152,29 +150,23 @@ class ProcessVisitsCommandTest extends TestCase
);
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willThrow(WrongIpException::class);
try {
$this->commandTester->execute([
'command' => 'visit:process',
], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
} catch (Throwable $e) {
$output = $this->commandTester->getDisplay();
$this->commandTester->execute([
'command' => 'visit:process',
], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
$this->assertInstanceOf(IpCannotBeLocatedException::class, $e);
$output = $this->commandTester->getDisplay();
$this->assertContains('An error occurred while locating IP. Skipped', $output);
$locateVisits->shouldHaveBeenCalledOnce();
$resolveIpLocation->shouldHaveBeenCalledOnce();
}
$this->assertStringContainsString('An error occurred while locating IP. Skipped', $output);
$locateVisits->shouldHaveBeenCalledOnce();
$resolveIpLocation->shouldHaveBeenCalledOnce();
}
/**
* @test
*/
/** @test */
public function noActionIsPerformedIfLockIsAcquired()
{
$this->lock->acquire()->willReturn(false);
$locateVisits = $this->visitService->locateVisits(Argument::cetera())->will(function () {
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(function () {
});
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([]);
@@ -183,7 +175,7 @@ class ProcessVisitsCommandTest extends TestCase
], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
$output = $this->commandTester->getDisplay();
$this->assertContains(
$this->assertStringContainsString(
sprintf('There is already an instance of the "%s" command', ProcessVisitsCommand::NAME),
$output
);

View File

@@ -19,7 +19,7 @@ class UpdateDbCommandTest extends TestCase
/** @var ObjectProphecy */
private $dbUpdater;
public function setUp()
public function setUp(): void
{
$this->dbUpdater = $this->prophesize(DbUpdaterInterface::class);
@@ -30,9 +30,7 @@ class UpdateDbCommandTest extends TestCase
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
/** @test */
public function successMessageIsPrintedIfEverythingWorks()
{
$download = $this->dbUpdater->downloadFreshCopy(Argument::type('callable'))->will(function () {
@@ -41,13 +39,11 @@ class UpdateDbCommandTest extends TestCase
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$this->assertContains('GeoLite2 database properly updated', $output);
$this->assertStringContainsString('GeoLite2 database properly updated', $output);
$download->shouldHaveBeenCalledOnce();
}
/**
* @test
*/
/** @test */
public function errorMessageIsPrintedIfAnExceptionIsThrown()
{
$download = $this->dbUpdater->downloadFreshCopy(Argument::type('callable'))->willThrow(RuntimeException::class);
@@ -55,7 +51,7 @@ class UpdateDbCommandTest extends TestCase
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$this->assertContains('An error occurred while updating GeoLite2 database', $output);
$this->assertStringContainsString('An error occurred while updating GeoLite2 database', $output);
$download->shouldHaveBeenCalledOnce();
}
}

View File

@@ -11,14 +11,12 @@ class ConfigProviderTest extends TestCase
/** @var ConfigProvider */
private $configProvider;
public function setUp()
public function setUp(): void
{
$this->configProvider = new ConfigProvider();
}
/**
* @test
*/
/** @test */
public function confiIsProperlyReturned()
{
$config = ($this->configProvider)();

View File

@@ -11,6 +11,7 @@ use Shlinkio\Shlink\Core\Options\AppOptions;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Zend\ServiceManager\ServiceManager;
use function array_merge;
class ApplicationFactoryTest extends TestCase
@@ -18,23 +19,19 @@ class ApplicationFactoryTest extends TestCase
/** @var ApplicationFactory */
private $factory;
public function setUp()
public function setUp(): void
{
$this->factory = new ApplicationFactory();
}
/**
* @test
*/
/** @test */
public function serviceIsCreated()
{
$instance = ($this->factory)($this->createServiceManager(), '');
$this->assertInstanceOf(Application::class, $instance);
}
/**
* @test
*/
/** @test */
public function allCommandsWhichAreServicesAreAdded()
{
$sm = $this->createServiceManager([

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Common;
use const JSON_ERROR_NONE;
use function getenv;
use function json_decode as spl_json_decode;
use function json_last_error;

View File

@@ -1,65 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Collection;
use function array_key_exists;
use function array_shift;
use function is_array;
final class PathCollection
{
/** @var array */
private $array;
public function __construct(array $array)
{
$this->array = $array;
}
public function pathExists(array $path): bool
{
return $this->checkPathExists($path, $this->array);
}
private function checkPathExists(array $path, array $array): bool
{
// As soon as a step is not found, the path does not exist
$step = array_shift($path);
if (! array_key_exists($step, $array)) {
return false;
}
// Once the path is empty, we have found all the parts in the path
if (empty($path)) {
return true;
}
// If current value is not an array, then we have not found the path
$newArray = $array[$step];
if (! is_array($newArray)) {
return false;
}
return $this->checkPathExists($path, $newArray);
}
/**
* @return mixed
*/
public function getValueInPath(array $path)
{
$array = $this->array;
do {
$step = array_shift($path);
if (! is_array($array) || ! array_key_exists($step, $array)) {
return null;
}
$array = $array[$step];
} while (! empty($path));
return $array;
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Console;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Output\OutputInterface;
final class ShlinkTable
{
private const DEFAULT_STYLE_NAME = 'default';
private const TABLE_TITLE_STYLE = '<options=bold> %s </>';
/** @var Table|null */
private $baseTable;
public function __construct(Table $baseTable)
{
$this->baseTable = $baseTable;
}
public static function fromOutput(OutputInterface $output): self
{
return new self(new Table($output));
}
public function render(array $headers, array $rows, ?string $footerTitle = null, ?string $headerTitle = null): void
{
$style = Table::getStyleDefinition(self::DEFAULT_STYLE_NAME);
$style->setFooterTitleFormat(self::TABLE_TITLE_STYLE)
->setHeaderTitleFormat(self::TABLE_TITLE_STYLE);
$table = clone $this->baseTable;
$table->setStyle($style)
->setHeaders($headers)
->setRows($rows)
->setFooterTitle($footerTitle)
->setHeaderTitle($headerTitle)
->render();
}
}

View File

@@ -7,7 +7,7 @@ use function sprintf;
class PreviewGenerationException extends RuntimeException
{
public static function fromImageError($error)
public static function fromImageError(string $error): self
{
return new self(sprintf('Error generating a preview image with error: %s', $error));
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Exception;
use Throwable;
use function sprintf;
class WrongIpException extends RuntimeException

View File

@@ -1,18 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Exec;
use const PHP_SAPI;
use function Shlinkio\Shlink\Common\env;
abstract class ExecutionContext
{
public const WEB = 'shlink_web';
public const CLI = 'shlink_cli';
public static function currentContextIsSwoole(): bool
{
return PHP_SAPI === 'cli' && env('CURRENT_SHLINK_CONTEXT', self::WEB) === self::WEB;
}
}

View File

@@ -11,8 +11,10 @@ use Shlinkio\Shlink\Core\Options\AppOptions;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
use function Functional\contains;
use function Shlinkio\Shlink\Common\env;
use function sys_get_temp_dir;
class CacheFactory implements FactoryInterface
{
@@ -23,6 +25,7 @@ class CacheFactory implements FactoryInterface
Cache\PhpFileCache::class,
Cache\MemcachedCache::class,
];
private const DEFAULT_MEMCACHED_PORT = 11211;
/**
* Create an object
@@ -40,7 +43,7 @@ class CacheFactory implements FactoryInterface
{
$appOptions = $container->get(AppOptions::class);
$adapter = $this->getAdapter($container);
$adapter->setNamespace($appOptions->__toString());
$adapter->setNamespace((string) $appOptions);
return $adapter;
}
@@ -65,25 +68,35 @@ class CacheFactory implements FactoryInterface
return new $cacheConfig['adapter']();
case Cache\FilesystemCache::class:
case Cache\PhpFileCache::class:
return new $cacheConfig['adapter']($cacheConfig['options']['dir']);
return new $cacheConfig['adapter']($cacheConfig['options']['dir'] ?? sys_get_temp_dir());
case Cache\MemcachedCache::class:
$memcached = new Memcached();
$servers = $cacheConfig['options']['servers'] ?? [];
foreach ($servers as $server) {
if (! isset($server['host'])) {
continue;
}
$port = isset($server['port']) ? (int) $server['port'] : 11211;
$memcached->addServer($server['host'], $port);
}
$cache = new Cache\MemcachedCache();
$cache->setMemcached($memcached);
$cache->setMemcached($this->buildMemcached($cacheConfig));
return $cache;
default:
return new Cache\ArrayCache();
}
}
private function buildMemcached(array $cacheConfig): Memcached
{
$memcached = new Memcached();
$servers = $cacheConfig['options']['servers'] ?? [];
foreach ($servers as $server) {
$this->addMemcachedServer($memcached, $server);
}
return $memcached;
}
private function addMemcachedServer(Memcached $memcached, array $server): void
{
if (! isset($server['host'])) {
return;
}
$port = (int) ($server['port'] ?? self::DEFAULT_MEMCACHED_PORT);
$memcached->addServer($server['host'], $port);
}
}

View File

@@ -10,6 +10,7 @@ use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\AbstractFactoryInterface;
use function array_shift;
use function explode;
use function is_array;

View File

@@ -5,6 +5,7 @@ namespace Shlinkio\Shlink\Common\Factory;
use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\Cache\Cache;
use Doctrine\Common\Persistence\Mapping\Driver\PHPDriver;
use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\EntityManager;
@@ -37,12 +38,9 @@ class EntityManagerFactory implements FactoryInterface
Type::addType(ChronosDateTimeType::CHRONOS_DATETIME, ChronosDateTimeType::class);
}
return EntityManager::create($connectionConfig, Setup::createAnnotationMetadataConfiguration(
$ormConfig['entities_paths'] ?? [],
$isDevMode,
$ormConfig['proxies_dir'] ?? null,
$cache,
false
));
$config = Setup::createConfiguration($isDevMode, $ormConfig['proxies_dir'] ?? null, $cache);
$config->setMetadataDriverImpl(new PHPDriver($ormConfig['entities_mappings'] ?? []));
return EntityManager::create($connectionConfig, $config);
}
}

View File

@@ -9,6 +9,7 @@ use Interop\Container\Exception\ContainerException;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
use function count;
use function explode;

View File

@@ -26,7 +26,7 @@ class ImageFactory implements FactoryInterface
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$config = $container->get('config')['phpwkhtmltopdf'];
$config = $container->get('config')['wkhtmltopdf'];
$image = new Image($config['images'] ?? null);
if ($options['url'] ?? null) {

View File

@@ -18,7 +18,7 @@ class ChainIpLocationResolver implements IpLocationResolverInterface
/**
* @throws WrongIpException
*/
public function resolveIpLocation(string $ipAddress): array
public function resolveIpLocation(string $ipAddress): Model\Location
{
$error = null;

View File

@@ -10,16 +10,8 @@ class EmptyIpLocationResolver implements IpLocationResolverInterface
/**
* @throws WrongIpException
*/
public function resolveIpLocation(string $ipAddress): array
public function resolveIpLocation(string $ipAddress): Model\Location
{
return [
'country_code' => '',
'country_name' => '',
'region_name' => '',
'city' => '',
'latitude' => '',
'longitude' => '',
'time_zone' => '',
];
return Model\Location::emptyInstance();
}
}

View File

@@ -12,6 +12,7 @@ use Shlinkio\Shlink\Common\Exception\RuntimeException;
use Symfony\Component\Filesystem\Exception as FilesystemException;
use Symfony\Component\Filesystem\Filesystem;
use Throwable;
use function sprintf;
class DbUpdater implements DbUpdaterInterface

View File

@@ -9,6 +9,7 @@ use GeoIp2\Model\City;
use GeoIp2\Record\Subdivision;
use MaxMind\Db\Reader\InvalidDatabaseException;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use function Functional\first;
class GeoLite2LocationResolver implements IpLocationResolverInterface
@@ -24,7 +25,7 @@ class GeoLite2LocationResolver implements IpLocationResolverInterface
/**
* @throws WrongIpException
*/
public function resolveIpLocation(string $ipAddress): array
public function resolveIpLocation(string $ipAddress): Model\Location
{
try {
$city = $this->geoLiteDbReader->city($ipAddress);
@@ -36,19 +37,19 @@ class GeoLite2LocationResolver implements IpLocationResolverInterface
}
}
private function mapFields(City $city): array
private function mapFields(City $city): Model\Location
{
/** @var Subdivision $region */
$region = first($city->subdivisions);
return [
'country_code' => $city->country->isoCode ?? '',
'country_name' => $city->country->name ?? '',
'region_name' => $region->name ?? '',
'city' => $city->city->name ?? '',
'latitude' => $city->location->latitude ?? '',
'longitude' => $city->location->longitude ?? '',
'time_zone' => $city->location->timeZone ?? '',
];
return new Model\Location(
$city->country->isoCode ?? '',
$city->country->name ?? '',
$region->name ?? '',
$city->city->name ?? '',
(float) ($city->location->latitude ?? ''),
(float) ($city->location->longitude ?? ''),
$city->location->timeZone ?? ''
);
}
}

View File

@@ -7,6 +7,7 @@ use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use function Shlinkio\Shlink\Common\json_decode;
use function sprintf;
@@ -25,7 +26,7 @@ class IpApiLocationResolver implements IpLocationResolverInterface
/**
* @throws WrongIpException
*/
public function resolveIpLocation(string $ipAddress): array
public function resolveIpLocation(string $ipAddress): Model\Location
{
try {
$response = $this->httpClient->get(sprintf(self::SERVICE_PATTERN, $ipAddress));
@@ -37,16 +38,16 @@ class IpApiLocationResolver implements IpLocationResolverInterface
}
}
private function mapFields(array $entry): array
private function mapFields(array $entry): Model\Location
{
return [
'country_code' => $entry['countryCode'] ?? '',
'country_name' => $entry['country'] ?? '',
'region_name' => $entry['regionName'] ?? '',
'city' => $entry['city'] ?? '',
'latitude' => $entry['lat'] ?? '',
'longitude' => $entry['lon'] ?? '',
'time_zone' => $entry['timezone'] ?? '',
];
return new Model\Location(
(string) ($entry['countryCode'] ?? ''),
(string) ($entry['country'] ?? ''),
(string) ($entry['regionName'] ?? ''),
(string) ($entry['city'] ?? ''),
(float) ($entry['lat'] ?? 0.0),
(float) ($entry['lon'] ?? 0.0),
(string) ($entry['timezone'] ?? '')
);
}
}

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