Compare commits

...

232 Commits

Author SHA1 Message Date
Alejandro Celaya
067d1cc41c Merge pull request #1637 from shlinkio/develop
Release 3.4.0
2022-12-16 22:55:02 +01:00
Alejandro Celaya
b97af7efb9 Added v3.4.0 to changelog 2022-12-16 22:33:16 +01:00
Alejandro Celaya
fd0ecc05b2 Merge pull request #1634 from acelaya-forks/feature/non-bot-count
Feature/non bot count
2022-12-16 21:02:22 +01:00
Alejandro Celaya
5b934c3f9a Updated changelog 2022-12-16 19:47:17 +01:00
Alejandro Celaya
c7a2f499e0 Added support to order short URLs list by amount of non-bot visits 2022-12-16 19:42:46 +01:00
Alejandro Celaya
713f7e7bc9 Added missing dock block 2022-12-16 18:18:09 +01:00
Alejandro Celaya
09078e4c6a Updated short URL API docs including new visitsSummary 2022-12-16 13:34:40 +01:00
Alejandro Celaya
1f66ec2af5 Fixed API tests 2022-12-16 10:53:44 +01:00
Alejandro Celaya
936e5b3b86 Fixed PublishingUpdatesGeneratorTest 2022-12-16 10:36:09 +01:00
Alejandro Celaya
99f28b569b Created method to get non-bot visits count for a short URL 2022-12-16 10:06:39 +01:00
Alejandro Celaya
0c83dea8b7 Merge pull request #1629 from acelaya-forks/feature/docker-8.2
Feature/docker 8.2
2022-12-14 18:55:37 +01:00
Alejandro Celaya
30edfdbdc5 Updated docker images to PHP 8.2 2022-12-14 15:01:00 +01:00
Alejandro Celaya
60ef98b836 Extracted method to find crawlable short codes to its own query object 2022-12-14 14:38:22 +01:00
Alejandro Celaya
73c8b53882 Split some logic from VisitRepository into its own injectable repository 2022-12-14 12:28:23 +01:00
Alejandro Celaya
425d8f0a3f Merge pull request #1628 from acelaya-forks/feature/split-repos-poc
Split short URL listing capabilities on its own repo and service
2022-12-13 20:48:34 +01:00
Alejandro Celaya
92a83b82a0 Split short URL listing capabilities on its own repo and service 2022-12-13 19:37:02 +01:00
Alejandro Celaya
d1ec15febf Merge pull request #1627 from acelaya-forks/feature/redis-credentials
Feature/redis credentials
2022-12-12 21:00:40 +01:00
Alejandro Celaya
dd345c82ea Updated changelog 2022-12-12 20:51:43 +01:00
Alejandro Celaya
2bf3e6a13b Addedsupport for credentials on redis 2022-12-12 20:50:21 +01:00
Alejandro Celaya
0b04476c99 Merge pull request #1622 from acelaya-forks/feature/filter-out-disabled
Feature/filter out disabled
2022-12-11 18:44:42 +01:00
Alejandro Celaya
229dc93132 Fixed typo 2022-12-11 18:36:46 +01:00
Alejandro Celaya
0952c488be Added exclusion flags to ListShortUrlsCommand 2022-12-11 18:33:40 +01:00
Alejandro Celaya
c4f28b3a32 Renamed ShortUrl::fromMeta to ShortUrl::create 2022-12-11 18:24:47 +01:00
Alejandro Celaya
201f25e0ad Improved API tests to cover exlucding disabled URLs from lists 2022-12-11 13:38:11 +01:00
Alejandro Celaya
0c3523c34a Fixed E2E test suites 2022-12-11 13:22:16 +01:00
Alejandro Celaya
0d7a0ee9ea Fixed more coding styles 2022-12-11 13:11:43 +01:00
Alejandro Celaya
931bdb0cd7 Fixed coding styles 2022-12-11 13:03:19 +01:00
Alejandro Celaya
8807a78463 Improved performance when filtering out shortUrls which reached their limit by using a sub-query 2022-12-11 13:00:06 +01:00
Alejandro Celaya
d832133410 Enhanced db tests for expired short urls filtering 2022-12-11 12:33:17 +01:00
Alejandro Celaya
cdde59b543 Added db test for filtering of disabled short URLs 2022-12-11 11:41:37 +01:00
Alejandro Celaya
463dfe9729 Added support to filter out expired short URLs from list 2022-12-11 10:26:04 +01:00
Alejandro Celaya
805c8c87ba Fixed nasty typo 2022-12-10 19:59:30 +01:00
Alejandro Celaya
7ba2cfc010 Moved true before false in swagger docs 2022-12-10 19:59:30 +01:00
Alejandro Celaya
40794c476f Updated API docs with new short URLs list filters 2022-12-10 19:59:30 +01:00
Alejandro Celaya
c3ab871366 Exposed new short URLs list filtering params 2022-12-10 19:59:30 +01:00
Alejandro Celaya
42a5296f93 Added new params to short URLs list to filter out 'disabled' short ones 2022-12-10 19:59:30 +01:00
Alejandro Celaya
183db4ff80 Merge pull request #1626 from acelaya-forks/feature/fix-ms-sql
Feature/fix ms sql
2022-12-10 19:57:50 +01:00
Alejandro Celaya
0bc9bd9281 Added TrustServerCertificate=true to mssql connections 2022-12-10 19:40:33 +01:00
Alejandro Celaya
9bed7ef156 Updated docker images to MS ODBC 18 for PDO MSSQL 2022-12-10 19:15:38 +01:00
Alejandro Celaya
8f68e4b9f5 Merge pull request #1624 from acelaya-forks/feature/php-8.2-full-support
Feature/php 8.2 full support
2022-12-10 18:37:18 +01:00
Alejandro Celaya
6589c8fce6 Downgraded docker images to latest php 8.1 2022-12-10 17:58:10 +01:00
Alejandro Celaya
38b313a25d Updated changelog 2022-12-10 17:30:35 +01:00
Alejandro Celaya
dab0ebeb99 Updated dockerimages to PHP 8.2 and added full support for this version 2022-12-10 17:29:52 +01:00
Alejandro Celaya
27bf7220b9 Merge pull request #1623 from acelaya-forks/feature/fix-flush-redis
Added missing namespace for cache adapters, causing full cache to be …
2022-12-10 10:21:46 +01:00
Alejandro Celaya
e68ef87c66 Renamed config file from redis to cache 2022-12-10 10:12:56 +01:00
Alejandro Celaya
29b747c192 Added missing namespace for cache adapters, causing full cache to be flushed in some circumstances 2022-12-10 10:11:25 +01:00
Alejandro Celaya
2047d6b772 Merge pull request #1621 from acelaya-forks/feature/default-domain-search
Feature/default domain search
2022-12-08 20:43:42 +01:00
Alejandro Celaya
71e7938b7a Updated changelog 2022-12-08 20:33:59 +01:00
Alejandro Celaya
6bce219eb3 Added test to cover searching short URLs by default domain 2022-12-08 20:32:48 +01:00
Alejandro Celaya
dfcac525bc Enabled search by default domain 2022-12-08 20:22:50 +01:00
Alejandro Celaya
da307aee0a Merge pull request #1620 from acelaya-forks/feature/empty-domain-fix
Feature/empty domain fix
2022-12-07 19:15:29 +01:00
Alejandro Celaya
edf2b5b4c2 Updated changelog 2022-12-07 19:06:58 +01:00
Alejandro Celaya
f41d947cf7 Ensured empty string is ignored as the domain during short URL creation 2022-12-07 19:06:05 +01:00
Alejandro Celaya
54bc169525 Merge pull request #1619 from acelaya-forks/feature/import-orphan-visits
Feature/import orphan visits
2022-12-05 15:03:28 +01:00
Alejandro Celaya
05d55c4000 Added one more case to cover import orphan visits when visits already exist 2022-12-05 14:48:24 +01:00
Alejandro Celaya
739f5eb421 Added test for orphan visits import 2022-12-05 14:42:26 +01:00
Alejandro Celaya
0aab1bdc4e Added test for findMostRecentOrphanVisit 2022-12-04 20:42:28 +01:00
Alejandro Celaya
47f99cf6cc Updated changelog 2022-12-04 20:38:07 +01:00
Alejandro Celaya
55c9773a02 Added logic to import orphan visits 2022-12-04 20:35:38 +01:00
Alejandro Celaya
4b66aaba5c Updated to latest shlink-importer 2022-12-04 12:28:44 +01:00
Alejandro Celaya
4223408090 Updated to common-config with support for valinor 1.0.0 2022-11-28 15:47:59 +01:00
Alejandro Celaya
58e6b0b683 Added badge for Mastodon follow 2022-11-17 19:57:47 +01:00
Alejandro Celaya
891438c672 Updated shlink-config 2022-11-11 16:33:02 +01:00
Alejandro Celaya
910864eaaf Reduced required MSI to 80 2022-11-05 10:54:12 +01:00
Alejandro Celaya
598c0757be Merge pull request #1587 from acelaya-forks/feature/phpstan-phpunit
Feature/phpstan phpunit
2022-10-24 20:34:11 +02:00
Alejandro Celaya
01e0a95e14 Added rest of tests to phpstan check 2022-10-24 20:25:06 +02:00
Alejandro Celaya
f459a99e7e Added db tests to phpstan checks 2022-10-24 20:14:48 +02:00
Alejandro Celaya
85e18a4754 Fixed all phpstan inspections on tests 2022-10-24 20:11:25 +02:00
Alejandro Celaya
1650499a38 Added more stricter types for mocks 2022-10-24 19:59:03 +02:00
Alejandro Celaya
51f243995a Added stricter types for mocks 2022-10-24 19:53:13 +02:00
Alejandro Celaya
aeafb244d9 Merge pull request #1586 from acelaya-forks/feature/phpunit-mocks
Feature/phpunit mocks
2022-10-23 23:18:49 +02:00
Alejandro Celaya
142417dda1 Updated changelog 2022-10-23 23:08:54 +02:00
Alejandro Celaya
da658185c3 Fixed coding styles 2022-10-23 23:07:50 +02:00
Alejandro Celaya
ef82158368 Migrated ApiKeyServiceTest to use PHPUnit mocks 2022-10-23 23:07:17 +02:00
Alejandro Celaya
083ccd36b7 Migrated OverrideDomainMiddlewareTest to use PHPUnit mocks 2022-10-23 23:00:57 +02:00
Alejandro Celaya
d61c79da84 Migrated DropDefaultDomainFromRequestMiddlewareTest to use PHPUnit mocks 2022-10-23 22:56:12 +02:00
Alejandro Celaya
8f76c3e202 Migrated DefaultShortCodesLengthMiddlewareTest to use PHPUnit mocks 2022-10-23 22:55:11 +02:00
Alejandro Celaya
23aa7a015c Migrated CreateShortUrlContentNegotiationMiddlewareTest to use PHPUnit mocks 2022-10-23 22:53:48 +02:00
Alejandro Celaya
674a4416cf Migrated NotConfiguredMercureErrorHandlerTest to use PHPUnit mocks 2022-10-23 22:51:38 +02:00
Alejandro Celaya
db85915c2f Migrated BackwardsCompatibleProblemDetailsHandlerTest to use PHPUnit mocks 2022-10-23 22:48:30 +02:00
Alejandro Celaya
dfc8e8d74e Migrated CrossDomainMiddlewareTest to use PHPUnit mocks 2022-10-23 22:47:34 +02:00
Alejandro Celaya
b2b424a4ed Migrated BodyParserMiddlewareTest to use PHPUnit mocks 2022-10-23 22:45:23 +02:00
Alejandro Celaya
3433899577 Migrated AuthenticationMiddlewareTest to use PHPUnit mocks 2022-10-23 22:40:14 +02:00
Alejandro Celaya
b1f814e118 Migrated InitialApiKeyDelegatorTest to use PHPUnit mocks 2022-10-23 22:36:16 +02:00
Alejandro Celaya
7aa6afeb30 Migrated TagVisitsActionTest to use PHPUnit mocks 2022-10-23 22:28:29 +02:00
Alejandro Celaya
d414496a3c Migrated ShortUrlVisitsActionTest to use PHPUnit mocks 2022-10-23 22:27:27 +02:00
Alejandro Celaya
d4684fd01f Migrated OrphanVisitsActionTest to use PHPUnit mocks 2022-10-23 22:25:54 +02:00
Alejandro Celaya
bb444a02fe Migrated NonOrphanVisitsActionTest to use PHPUnit mocks 2022-10-23 22:24:06 +02:00
Alejandro Celaya
e980a8d121 Migrated GlobalVisitsActionTest to use PHPUnit mocks 2022-10-23 22:23:08 +02:00
Alejandro Celaya
f493baaf2b Migrated DomainVisitsActionTest to use PHPUnit mocks 2022-10-23 22:22:14 +02:00
Alejandro Celaya
28f26920dd Migrated UpdateTagActionTest to use PHPUnit mocks 2022-10-23 22:21:23 +02:00
Alejandro Celaya
69e994c067 Migrated TagsStatsActionTest to use PHPUnit mocks 2022-10-23 22:20:21 +02:00
Alejandro Celaya
656083cb6f Migrated ListTagsActionTest to use PHPUnit mocks 2022-10-23 22:19:14 +02:00
Alejandro Celaya
ab9ea887d2 Migrated DeleteTagsActionTest to use PHPUnit mocks 2022-10-23 22:17:35 +02:00
Alejandro Celaya
9ac6a50e66 Migrated SingleStepCreateShortUrlActionTest to use PHPUnit mocks 2022-10-23 22:16:19 +02:00
Alejandro Celaya
acc9cb94b5 Migrated ResolveShortUrlActionTest to use PHPUnit mocks 2022-10-23 22:14:28 +02:00
Alejandro Celaya
01829c82ee Migrated ListShortUrlsActionTest to use PHPUnit mocks 2022-10-23 22:13:27 +02:00
Alejandro Celaya
9c02ea8799 Migrated EditShortUrlActionTest to use PHPUnit mocks 2022-10-23 22:12:27 +02:00
Alejandro Celaya
d202538581 Migrated DeleteShortUrlActionTest to use PHPUnit mocks 2022-10-23 22:10:41 +02:00
Alejandro Celaya
a84b642ba5 Migrated CreateShortUrlActionTest to use PHPUnit mocks 2022-10-23 22:09:37 +02:00
Alejandro Celaya
74176c298f Migrated ListDomainsActionTest to use PHPUnit mocks 2022-10-23 22:06:48 +02:00
Alejandro Celaya
91e21441f7 Migrated DomainRedirectsActionTest to use PHPUnit mocks 2022-10-23 22:05:51 +02:00
Alejandro Celaya
896b7f2d73 Migrated MercureInfoActionTest to use PHPUnit mocks 2022-10-23 22:04:00 +02:00
Alejandro Celaya
66ed152358 Migrated HealthActionTest to use PHPUnit mocks 2022-10-23 22:02:31 +02:00
Alejandro Celaya
257134cd80 Migrated VisitsForTagPaginatorAdapterTest to use PHPUnit mocks 2022-10-23 21:59:18 +02:00
Alejandro Celaya
a4373aee91 Migrated OrphanVisitsPaginatorAdapterTest to use PHPUnit mocks 2022-10-23 21:56:34 +02:00
Alejandro Celaya
7442905873 Migrated NonOrphanVisitsPaginatorAdapterTest to use PHPUnit mocks 2022-10-23 21:55:06 +02:00
Alejandro Celaya
d3af51f684 Migrated VisitToLocationHelperTest to use PHPUnit mocks 2022-10-23 21:24:30 +02:00
Alejandro Celaya
04419a7242 Migrated VisitLocatorTest to use PHPUnit mocks 2022-10-23 21:21:23 +02:00
Alejandro Celaya
a45d6e6b44 Migrated VisitsTrackerTest to use PHPUnit mocks 2022-10-23 21:08:58 +02:00
Alejandro Celaya
37b1306eb3 Migrated VisitsStatsHelperTest to use PHPUnit mocks 2022-10-23 21:05:13 +02:00
Alejandro Celaya
cff6573767 Migrated RequestTrackerTest to use PHPUnit mocks 2022-10-23 20:45:56 +02:00
Alejandro Celaya
a2f34e02ad Migrated UrlValidatorTest to use PHPUnit mocks 2022-10-23 20:39:06 +02:00
Alejandro Celaya
796543d194 Migrated DoctrineBatchHelperTest to use PHPUnit mocks 2022-10-23 20:32:13 +02:00
Alejandro Celaya
3b25fb27fe Migrated TagsPaginatorAdapterTest to use PHPUnit mocks 2022-10-23 20:28:45 +02:00
Alejandro Celaya
3b20f955ff Migrated TagsInfoPaginatorAdapterTest to use PHPUnit mocks 2022-10-23 20:27:51 +02:00
Alejandro Celaya
c81ae9c40d Migrated TagServiceTest to use PHPUnit mocks 2022-10-23 20:26:44 +02:00
Alejandro Celaya
7ceae7af87 Merge pull request #1585 from acelaya-forks/feature/phpunit-mocks
Feature/phpunit mocks
2022-10-23 20:19:22 +02:00
Alejandro Celaya
5e02cfe375 Fixed coding styles 2022-10-23 18:29:32 +02:00
Alejandro Celaya
6e836b5fd9 Migrated PersistenceShortUrlRelationResolverTest to use PHPUnit mocks 2022-10-23 18:28:28 +02:00
Alejandro Celaya
8753e3a77f Migrated ShortUrlRepositoryAdapterTest to use PHPUnit mocks 2022-10-23 18:17:29 +02:00
Alejandro Celaya
6a2227efc5 Removed all uinnecessary usages of equalsTo param constraint 2022-10-23 18:15:57 +02:00
Alejandro Celaya
1fbcea7a06 Migrated ExtraPathRedirectMiddlewareTest to use PHPUnit mocks 2022-10-23 17:53:09 +02:00
Alejandro Celaya
168c839cf1 Migrated TrimTrailingSlashMiddlewareTest to use PHPUnit mocks 2022-10-23 17:39:57 +02:00
Alejandro Celaya
162e913cc4 Migrated ShortUrlTitleResolutionHelperTest to use PHPUnit mocks 2022-10-23 17:38:04 +02:00
Alejandro Celaya
5aaf50d68e Migrated ShortCodeUniquenessHelperTest to use PHPUnit mocks 2022-10-23 17:35:50 +02:00
Alejandro Celaya
d2f5be1d18 Migrated UrlShortenerTest to use PHPUnit mocks 2022-10-23 11:32:13 +02:00
Alejandro Celaya
36ab455a49 Migrated ShortUrlServiceTest to use PHPUnit mocks 2022-10-23 11:14:01 +02:00
Alejandro Celaya
ee8cab8455 Migrated ShortUrlResolverTest to use PHPUnit mocks 2022-10-23 11:09:40 +02:00
Alejandro Celaya
bd884e85d4 Migrated DeleteShortUrlServiceTest to use PHPUnit mocks 2022-10-23 11:03:47 +02:00
Alejandro Celaya
5ceb6fb740 No longer let pipelines pass on error with PHP 8.2 2022-10-23 11:00:50 +02:00
Alejandro Celaya
0d6155e8bc Merge pull request #1584 from acelaya-forks/feature/phpunit-mocks
Feature/phpunit mocks
2022-10-22 20:51:01 +02:00
Alejandro Celaya
a78c59c11a Fixed coding styles 2022-10-22 20:41:17 +02:00
Alejandro Celaya
173420c608 Migrated ImportedLinksProcessorTest to use PHPUnit mocks 2022-10-22 20:39:55 +02:00
Alejandro Celaya
10b0ec301b Migrated ValidationExceptionTest to use PHPUnit mocks 2022-10-22 20:05:06 +02:00
Alejandro Celaya
1706a869d9 Migrated NotifyVisitToRedisTest to use PHPUnit mocks 2022-10-22 20:04:12 +02:00
Alejandro Celaya
d0393799d2 Migrated NotifyNewShortUrlToRedisTest to use PHPUnit mocks 2022-10-22 19:59:32 +02:00
Alejandro Celaya
739433ba8b Migrated NotifyVisitToRabbitMqTest to use PHPUnit mocks 2022-10-22 19:05:34 +02:00
Alejandro Celaya
a15e9c29c8 Migrated NotifyNewShortUrlToRabbitMqTest to use PHPUnit mocks 2022-10-22 18:49:43 +02:00
Alejandro Celaya
d58f89aa26 Merge pull request #1583 from acelaya-forks/feature/phpunit-mocks
Feature/phpunit mocks
2022-10-22 15:14:57 +02:00
Alejandro Celaya
b7671f70da Fixed coding styles 2022-10-22 14:41:42 +02:00
Alejandro Celaya
52366b9dd4 Removed last reference to prophecytrait in CLI module 2022-10-22 14:41:22 +02:00
Alejandro Celaya
32417e40cb Migrated ShlinkTableTest to use PHPUnit mocks 2022-10-22 14:40:35 +02:00
Alejandro Celaya
4cb44be9a0 Migrated ProcessRunnerTest to use PHPUnit mocks 2022-10-22 14:37:13 +02:00
Alejandro Celaya
a484455b0b Migrated GeolocationDbUpdaterTest to use PHPUnit mocks 2022-10-22 14:27:07 +02:00
Alejandro Celaya
4b3ed2b7ba Migrated LocateVisitsCommandTest to use PHPUnit mocks 2022-10-22 14:16:42 +02:00
Alejandro Celaya
e2986a7b4c Migrated GetOrphanVisitsCommandTest to use PHPUnit mocks 2022-10-22 14:06:54 +02:00
Alejandro Celaya
82e04800aa Migrated GetNonOrphanVisitsCommandTest to use PHPUnit mocks 2022-10-22 14:06:00 +02:00
Alejandro Celaya
5d367da626 Migrated DownloadGeoLiteDbCommandTest to use PHPUnit mocks 2022-10-22 14:02:38 +02:00
Alejandro Celaya
59de5a5f55 Migrated RenameTagCommandTest to use PHPUnit mocks 2022-10-22 13:53:45 +02:00
Alejandro Celaya
0855104068 Migrated ListTagsCommandTest to use PHPUnit mocks 2022-10-22 13:49:11 +02:00
Alejandro Celaya
8c6f97c4e2 Migrated GetTagVisitsCommandTest to use PHPUnit mocks 2022-10-22 13:47:28 +02:00
Alejandro Celaya
2d16856582 Migrated DeleteTagsCommandTest to use PHPUnit mocks 2022-10-22 13:45:11 +02:00
Alejandro Celaya
41e903cf26 Migrated ResolveUrlCommandTest to use PHPUnit mocks 2022-10-22 13:44:10 +02:00
Alejandro Celaya
4872bd3a92 Migrated ListShortUrlsCommandTest to use PHPUnit mocks 2022-10-22 13:42:46 +02:00
Alejandro Celaya
8b675f55cc Migrated GetShortUrlVisitsCommandTest to use PHPUnit mocks 2022-10-22 13:38:46 +02:00
Alejandro Celaya
acda7f02c6 Migrated DeleteShortUrlCommandTest to use PHPUnit mocks 2022-10-22 13:36:33 +02:00
Alejandro Celaya
184ff90b9f Migrated CreateShortUrlCommandTest to use PHPUnit mocks 2022-10-22 13:27:48 +02:00
Alejandro Celaya
d8be3c28cb Migrated ListDomainsCommandTest to use PHPUnit mocks 2022-10-22 13:21:54 +02:00
Alejandro Celaya
3d358ab046 Migrated GetDomainVisitsCommandTest to use PHPUnit mocks 2022-10-22 13:21:00 +02:00
Alejandro Celaya
960bdfc232 Migrated DomainRedirectsCommandTest to use PHPUnit mocks 2022-10-22 13:17:12 +02:00
Alejandro Celaya
101b4daff4 Migrated MigrateDatabaseCommandTest to use PHPUnit mocks 2022-10-22 13:08:05 +02:00
Alejandro Celaya
13431ff8cf Migrated CreateDatabaseCommandTest to use PHPUnit mocks 2022-10-22 13:05:36 +02:00
Alejandro Celaya
4cdcad29df Migrated ListKeysCommandTest to use PHPUnit mocks 2022-10-22 12:53:28 +02:00
Alejandro Celaya
a4c34ff7be Migrated GenerateKeyCommandTest to use PHPUnit mocks 2022-10-22 12:52:11 +02:00
Alejandro Celaya
2b7b5e9a8f Migrated DisableKeyCommandTest to use PHPUnit mocks 2022-10-22 12:48:17 +02:00
Alejandro Celaya
58db902084 Migrated CliTestUtilsTrait to use PHPUnit mocks 2022-10-22 12:46:16 +02:00
Alejandro Celaya
983e3c9eaa Merge pull request #1582 from acelaya-forks/feature/phpunit-mocks
Feature/phpunit mocks
2022-10-22 10:11:30 +02:00
Alejandro Celaya
dbe35cf567 Fixed coding styles 2022-10-22 10:03:28 +02:00
Alejandro Celaya
8298f9d491 Migrated NotifyVisitToMercureTest to use PHPUnit mocks 2022-10-22 10:03:05 +02:00
Alejandro Celaya
16a951b938 Migrated NotifyNewShortUrlToMercureTest to use PHPUnit mocks 2022-10-22 09:50:12 +02:00
Alejandro Celaya
51fcbfb3c2 Migrated UpdateGeoLiteDbTest to use PHPUnit mocks 2022-10-22 09:42:21 +02:00
Alejandro Celaya
e01e370d16 Migrated NotifyVisitToWebHooksTest to use PHPUnit mocks 2022-10-22 08:08:49 +02:00
Alejandro Celaya
736ac8ba90 Migrated LocateVisitTest to use PHPUnit mocks 2022-10-22 07:54:57 +02:00
Alejandro Celaya
d07104b8d9 Migrated LocateUnlocatedVisitsTest to use PHPUnit mocks 2022-10-22 07:34:38 +02:00
Alejandro Celaya
cad53e397a Migrated CloseDbConnectionEventListenerTest to use PHPUnit mocks 2022-10-22 07:32:37 +02:00
Alejandro Celaya
3608a6d068 Migrated CloseDbConnectionEventListenerDelegatorTest to use PHPUnit mocks 2022-10-22 07:28:15 +02:00
Alejandro Celaya
92ddd2eebe Merge pull request #1581 from acelaya-forks/feature/phpunit-mocks
Feature/phpunit mocks
2022-10-21 19:42:58 +02:00
Alejandro Celaya
bf0b58b344 Migrated NotFoundTypeResolverMiddlewareTest to use PHPUnit mocks 2022-10-21 19:32:25 +02:00
Alejandro Celaya
ff543b151c Migrated NotFoundTrackerMiddlewareTest to use PHPUnit mocks 2022-10-21 19:29:02 +02:00
Alejandro Celaya
d842025835 Migrated NotFoundTemplateHandlerTest to use PHPUnit mocks 2022-10-21 19:25:29 +02:00
Alejandro Celaya
230e56370a Migrated NotFoundRedirectHandlerTest to use PHPUnit mocks 2022-10-21 19:24:39 +02:00
Alejandro Celaya
a8514a9ae4 Migrated DomainServiceTest to use PHPUnit mocks 2022-10-21 19:01:41 +02:00
Alejandro Celaya
148f7a9cfe Migrated CrawlingHelperTest to use PHPUnit mocks 2022-10-21 18:49:47 +02:00
Alejandro Celaya
29d50cabc2 Migrated NotFoundRedirectResolverTest to use PHPUnit mocks 2022-10-21 18:47:10 +02:00
Alejandro Celaya
a8f8297131 Migrated RedirectActionTest to use PHPUnit mocks 2022-10-21 18:44:55 +02:00
Alejandro Celaya
cd4b632d75 Migrated QrActionTest to use PHPUnit mocks 2022-10-21 18:39:22 +02:00
Alejandro Celaya
843754b7e7 Migrated PixelActionTest to use PHPUnit mocks 2022-10-21 18:32:34 +02:00
Alejandro Celaya
847cc2bc50 Updated shlink-config 2022-10-19 14:19:03 +02:00
Alejandro Celaya
751bd15785 Fixed merge conflicts 2022-10-18 19:08:20 +02:00
Alejandro Celaya
c12db7567e Merge pull request #1577 from acelaya-forks/feature/fix-enum-hydration
Feature/fix enum hydration
2022-10-18 19:05:07 +02:00
Alejandro Celaya
e8069a10ba Updated changelog 2022-10-18 18:49:52 +02:00
Alejandro Celaya
9742bf13e4 Upgraded to latest doctrine/orm 2022-10-18 18:48:35 +02:00
Alejandro Celaya
6441707c76 Merge branch 'develop' of github.com:shlinkio/shlink into develop 2022-10-18 18:32:02 +02:00
Alejandro Celaya
23bcba4fd9 Updated shlink-ip-geolocation 2022-10-12 19:07:05 +02:00
Alejandro Celaya
9049a205b7 Merge pull request #1570 from acelaya-forks/feature/phpunit-mocks
Migrated to PHPUnit mocks in RobotsActionTest
2022-10-12 18:56:24 +02:00
Alejandro Celaya
8cfa0b595c Migrated to PHPUnit mocks in RobotsActionTest 2022-10-12 18:23:36 +02:00
Alejandro Celaya
4b958e8b87 Merge pull request #1568 from acelaya-forks/feature/phpunit-mocks-experiment
Used PHPUnit mocks in RoleResolverTest instead of prophezy
2022-10-12 12:55:34 +02:00
Alejandro Celaya
bcd5d2848d Used PHPUnit mocks in RoleResolverTest instead of prophezy 2022-10-12 12:47:58 +02:00
Alejandro Celaya
b59cbeceac Updated deps 2022-10-12 08:49:58 +02:00
Alejandro Celaya
46f948a584 Merge pull request #1565 from acelaya-forks/feature/command-reusable-args
Feature/command reusable args
2022-10-06 21:38:19 +02:00
Alejandro Celaya
14bf3a134b Updated changelog 2022-10-06 21:30:23 +02:00
Alejandro Celaya
1557438fdf Moved logic to reuse command options to option classes instead of base abstract command classes 2022-10-06 21:29:27 +02:00
Alejandro Celaya
27b680e0cd Created CLI test for short URLs list 2022-10-06 21:01:11 +02:00
Alejandro Celaya
14314ef939 Updated shlink deps 2022-10-06 19:49:32 +02:00
Alejandro Celaya
bf5c168d7d Merge pull request #1560 from acelaya-forks/feature/openswoole-4.12
Updated to openswoole 4.12
2022-10-03 20:26:25 +02:00
Alejandro Celaya
1e0791416d Downgraded openswoole ide helper 2022-10-03 20:05:43 +02:00
Alejandro Celaya
ab8d42b609 Updated to openswoole 4.12 in main Dockerfile 2022-10-03 20:01:46 +02:00
Alejandro Celaya
96dbdbe7c9 Updated to openswoole 4.12 2022-10-03 20:00:31 +02:00
Alejandro Celaya
6f135ad6ab Fixed typo 2022-09-30 17:45:36 +02:00
Alejandro Celaya
5b9a1e1978 Merge pull request #1559 from shlinkio/develop
Release 3.3.1
2022-09-30 17:37:29 +02:00
Alejandro Celaya
4ba3522e79 Merge pull request #1558 from acelaya-forks/feature/multisegment-trailing-slash
Feature/multisegment trailing slash
2022-09-30 17:35:01 +02:00
Alejandro Celaya
d3faa22b78 Fixed usage of enum where the enum's value should be used 2022-09-30 17:26:22 +02:00
Alejandro Celaya
1daad334a5 Updated changelog 2022-09-30 17:21:27 +02:00
Alejandro Celaya
3dda49dab4 Created middleware which ensures trailing slash and multi-segment features work properly together 2022-09-30 17:19:07 +02:00
Alejandro Celaya
c6c4e5580b Merge pull request #1554 from acelaya-forks/feature/php-8.2
Added PHP 8.2 to build matrixes
2022-09-24 08:26:54 +02:00
Alejandro Celaya
3f808e3813 Updated changelog 2022-09-24 08:17:14 +02:00
Alejandro Celaya
e5107c40f9 Ignored platform req during roadrunner API tests in CI workflow when using PHP 8.2 2022-09-24 08:14:59 +02:00
Alejandro Celaya
0871ca884e Fixed typo 2022-09-24 08:06:41 +02:00
Alejandro Celaya
62ce9311bf Added PHP 8.2 to build matrixes 2022-09-24 08:03:38 +02:00
Alejandro Celaya
70b15a7ab0 Merge pull request #1553 from acelaya-forks/feature/organize-namespaces
Feature/organize namespaces
2022-09-24 07:35:53 +02:00
Alejandro Celaya
708bff20f0 Updated changelog 2022-09-23 19:09:38 +02:00
Alejandro Celaya
369628ee95 Migrated infection config files to json5 2022-09-23 19:08:54 +02:00
Alejandro Celaya
0c6f8f1136 Refactored global entities into their own proper namespaces 2022-09-23 19:03:32 +02:00
Alejandro Celaya
9f9d011d46 Moved ShortCodeUniquenessHelper to ShortUrl\Helper namespace 2022-09-23 18:46:51 +02:00
Alejandro Celaya
e28b73c130 Refactored global services into their own proper namespaces 2022-09-23 18:42:38 +02:00
Alejandro Celaya
56f953ab2f Refactored global validations into their own proper namespaces 2022-09-23 18:30:07 +02:00
Alejandro Celaya
3ad8be175c Refactored global repositories into their own proper namespaces 2022-09-23 18:24:14 +02:00
Alejandro Celaya
f5f990511c Refactored global models into their own proper namespaces 2022-09-23 18:05:17 +02:00
Alejandro Celaya
1e3ccba503 Merge pull request #1552 from acelaya-forks/feature/visit-geolocation-namespace
Feature/visit geolocation namespace
2022-09-23 15:07:42 +02:00
Alejandro Celaya
a842b5b7cd Updated changelog 2022-09-23 14:58:00 +02:00
Alejandro Celaya
909e42b0be Moved services related to geolocating visits to the Visit\Geolocation namespace 2022-09-23 14:50:26 +02:00
332 changed files with 5692 additions and 5303 deletions

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php-version: [ '8.1' ]
php-version: ['8.1', '8.2']
env:
LC_ALL: C
steps:
@@ -27,7 +27,7 @@ jobs:
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
php-extensions: openswoole-4.11.1, pdo_sqlsrv-5.10.1
php-extensions: openswoole-4.12.0, pdo_sqlsrv-5.10.1
extensions-cache-key: db-tests-extensions-${{ matrix.php-version }}-${{ inputs.platform }}
- name: Create test database
if: ${{ inputs.platform == 'ms' }}

View File

@@ -13,13 +13,13 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php-version: [ '8.1' ]
php-version: ['8.1', '8.2']
steps:
- uses: actions/checkout@v3
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
php-extensions: openswoole-4.11.1
php-extensions: openswoole-4.12.0
extensions-cache-key: mutation-tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
- uses: actions/download-artifact@v3
with:

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php-version: ['8.1']
php-version: ['8.1', '8.2']
steps:
- uses: actions/checkout@v3
- name: Start postgres database server
@@ -25,7 +25,7 @@ jobs:
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
php-extensions: openswoole-4.11.1
php-extensions: openswoole-4.12.0
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
- run: composer test:${{ inputs.test-group }}:ci
- uses: actions/upload-artifact@v3

View File

@@ -20,7 +20,7 @@ jobs:
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
php-extensions: openswoole-4.11.1
php-extensions: openswoole-4.12.0
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ matrix.command }}
- run: composer ${{ matrix.command }}
@@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php-version: [ '8.1' ]
php-version: ['8.1', '8.2']
steps:
- uses: actions/checkout@v3
- run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres

View File

@@ -8,7 +8,7 @@ on:
- 'v*'
jobs:
build-openswool:
build-openswoole:
uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main
secrets: inherit
with:

View File

@@ -10,14 +10,14 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php-version: ['8.1']
php-version: ['8.1', '8.2']
swoole: ['yes', 'no']
steps:
- uses: actions/checkout@v3
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
php-extensions: openswoole-4.11.1
php-extensions: openswoole-4.12.0
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
install-deps: 'no'
- if: ${{ matrix.swoole == 'yes' }}
@@ -51,7 +51,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php-version: ['8.1']
php-version: ['8.1', '8.2']
swoole: ['yes', 'no']
steps:
- uses: geekyeggo/delete-artifact@v1

View File

@@ -20,7 +20,7 @@ jobs:
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
php-extensions: openswoole-4.11.1
php-extensions: openswoole-4.12.0
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
- run: composer swagger:inline
- run: mkdir ${{ steps.determine_version.outputs.version }}

View File

@@ -4,6 +4,77 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
## [3.4.0] - 2022-12-16
### Added
* [#1612](https://github.com/shlinkio/shlink/issues/1612) Allowed to filter short URLs out of lists, when `validUntil` date is in the past or have reached their maximum amount of visits.
This can be done by:
* Providing `excludeMaxVisitsReached=true` and/or `excludePastValidUntil=true` to the `GET /short-urls` endpoint.
* Providing `--exclude-max-visits-reached` and/or `--exclude-past-valid-until` to the `short-urls:list` command.
* [#1613](https://github.com/shlinkio/shlink/issues/1613) Added amount of visits coming from bots, non-bots and total to every short URL in the short URLs list.
Additionally, added option to order by non-bot visits, by passing `nonBotVisits-DESC` or `nonBotVisits-ASC`.
* [#1599](https://github.com/shlinkio/shlink/issues/1599) Added support for credentials on redis DSNs, either only password, or both username and password.
* [#1616](https://github.com/shlinkio/shlink/issues/1616) Added support to import orphan visits when importing short URLs from another Shlink instance.
* [#1519](https://github.com/shlinkio/shlink/issues/1519) Allowing to search short URLs by default domain.
* [#1555](https://github.com/shlinkio/shlink/issues/1555) and [#1625](https://github.com/shlinkio/shlink/issues/1625) Added full support for PHP 8.2, updating the docker image to this version.
### Changed
* [#1563](https://github.com/shlinkio/shlink/issues/1563) Moved logic to reuse command options to option classes instead of base abstract command classes.
* [#1569](https://github.com/shlinkio/shlink/issues/1569) Migrated test doubles from phpspec/prophecy to PHPUnit mocks.
* [#1329](https://github.com/shlinkio/shlink/issues/1329) Split some logic from `VisitRepository` and `ShortUrlRepository` into separated repository classes.
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#1618](https://github.com/shlinkio/shlink/issues/1618) Fixed imported short URLs and visits dates not being set to the target server timezone.
* [#1578](https://github.com/shlinkio/shlink/issues/1578) Fixed short URL allowing an empty string as the domain during creation.
* [#1580](https://github.com/shlinkio/shlink/issues/1580) Fixed `FLUSHDB` being run on Shlink docker start-up when using redis, causing full cache to be flushed.
## [3.3.2] - 2022-10-18
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#1576](https://github.com/shlinkio/shlink/issues/1576) Fixed error when trying to retry visits location from CLI.
## [3.3.1] - 2022-09-30
### Added
* *Nothing*
### Changed
* [#1474](https://github.com/shlinkio/shlink/issues/1474) Added preliminary support for PHP 8.2 during CI workflow.
* [#1551](https://github.com/shlinkio/shlink/issues/1551) Moved services related to geolocating visits to the `Visit\Geolocation` namespace.
* [#1550](https://github.com/shlinkio/shlink/issues/1550) Reorganized main namespaces from Core module.
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#1556](https://github.com/shlinkio/shlink/issues/1556) Fixed trailing slash not working when enabling multi-segment slashes.
## [3.3.0] - 2022-09-18
### Added
* [#1221](https://github.com/shlinkio/shlink/issues/1221) Added experimental support to run Shlink with [RoadRunner](https://roadrunner.dev) instead of openswoole.

View File

@@ -1,12 +1,13 @@
FROM php:8.1.9-alpine3.16 as base
FROM php:8.2-alpine3.17 as base
ARG SHLINK_VERSION=latest
ENV SHLINK_VERSION ${SHLINK_VERSION}
ARG SHLINK_RUNTIME=openswoole
ENV SHLINK_RUNTIME ${SHLINK_RUNTIME}
ENV OPENSWOOLE_VERSION 4.11.1
ENV OPENSWOOLE_VERSION 4.12.0
ENV PDO_SQLSRV_VERSION 5.10.1
ENV MS_ODBC_SQL_VERSION 17.5.2.2
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
ENV LC_ALL "C"
WORKDIR /etc/shlink
@@ -14,7 +15,7 @@ WORKDIR /etc/shlink
# Install required PHP extensions
RUN \
# Temp install dev dependencies needed to compile the extensions
apk add --no-cache --virtual .dev-deps sqlite-dev postgresql-dev icu-dev libzip-dev zlib-dev libpng-dev && \
apk add --no-cache --virtual .dev-deps sqlite-dev postgresql-dev icu-dev libzip-dev zlib-dev libpng-dev linux-headers && \
docker-php-ext-install -j"$(nproc)" pdo_mysql pdo_pgsql intl calendar sockets bcmath zip gd && \
apk add --no-cache sqlite-libs && \
docker-php-ext-install -j"$(nproc)" pdo_sqlite && \
@@ -29,11 +30,11 @@ RUN apk add --no-cache --virtual .phpize-deps ${PHPIZE_DEPS} unixodbc-dev && \
docker-php-ext-enable openswoole ; \
fi; \
if [ $(uname -m) == "x86_64" ]; then \
wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
apk add --no-cache --allow-untrusted msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
wget https://download.microsoft.com/download/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
apk add --allow-untrusted msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
pecl install pdo_sqlsrv-${PDO_SQLSRV_VERSION} && \
docker-php-ext-enable pdo_sqlsrv && \
rm msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk ; \
rm msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk ; \
fi; \
apk del .phpize-deps

View File

@@ -7,6 +7,7 @@
[![Docker pulls](https://img.shields.io/docker/pulls/shlinkio/shlink.svg?logo=docker&style=flat-square)](https://hub.docker.com/r/shlinkio/shlink/)
[![License](https://img.shields.io/github/license/shlinkio/shlink.svg?style=flat-square)](https://github.com/shlinkio/shlink/blob/main/LICENSE)
[![Twitter](https://img.shields.io/twitter/follow/shlinkio?color=blue&label=follow&logo=twitter&style=flat-square)](https://twitter.com/shlinkio)
[![Mastodon](https://img.shields.io/mastodon/follow/109329425426175098?color=%236364ff&domain=https%3A%2F%2Ffosstodon.org&label=follow&logo=mastodon&logoColor=white&style=flat-square)](https://fosstodon.org/@shlinkio)
[![Paypal donate](https://img.shields.io/badge/Donate-paypal-blue.svg?style=flat-square&logo=paypal&colorA=aaaaaa)](https://slnk.to/donate)
A PHP-based self-hosted URL shortener that can be used to serve shortened URLs under your own domain.

View File

@@ -20,39 +20,39 @@
"akrabat/ip-address-middleware": "^2.1",
"cakephp/chronos": "^2.3",
"doctrine/migrations": "^3.5",
"doctrine/orm": "^2.12",
"endroid/qr-code": "^4.4",
"geoip2/geoip2": "^2.12",
"guzzlehttp/guzzle": "^7.4",
"doctrine/orm": "^2.13.3",
"endroid/qr-code": "^4.6",
"geoip2/geoip2": "^2.13",
"guzzlehttp/guzzle": "^7.5",
"happyr/doctrine-specification": "^2.0",
"jaybizzle/crawler-detect": "^1.2.110",
"jaybizzle/crawler-detect": "^1.2.112",
"laminas/laminas-config": "^3.7",
"laminas/laminas-config-aggregator": "^1.8",
"laminas/laminas-diactoros": "^2.14",
"laminas/laminas-inputfilter": "^2.19",
"laminas/laminas-servicemanager": "^3.16",
"laminas/laminas-stdlib": "^3.11",
"lcobucci/jwt": "^4.1",
"league/uri": "^6.7",
"laminas/laminas-config-aggregator": "^1.11",
"laminas/laminas-diactoros": "^2.19",
"laminas/laminas-inputfilter": "^2.22",
"laminas/laminas-servicemanager": "^3.19",
"laminas/laminas-stdlib": "^3.15",
"lcobucci/jwt": "^4.2",
"league/uri": "^6.8",
"lstrojny/functional-php": "^1.17",
"mezzio/mezzio": "^3.11",
"mezzio/mezzio-fastroute": "^3.5",
"mezzio/mezzio-problem-details": "^1.6",
"mezzio/mezzio-swoole": "^4.3",
"mezzio/mezzio": "^3.13",
"mezzio/mezzio-fastroute": "^3.7",
"mezzio/mezzio-problem-details": "^1.7",
"mezzio/mezzio-swoole": "^4.5",
"mlocati/ip-lib": "^1.18",
"ocramius/proxy-manager": "^2.14",
"pagerfanta/core": "^3.6",
"php-middleware/request-id": "^4.1",
"pugx/shortid-php": "^1.0",
"ramsey/uuid": "^4.3",
"shlinkio/shlink-common": "^5.1",
"shlinkio/shlink-config": "^2.1",
"pugx/shortid-php": "^1.1",
"ramsey/uuid": "^4.5",
"shlinkio/shlink-common": "^5.2",
"shlinkio/shlink-config": "^2.3",
"shlinkio/shlink-event-dispatcher": "^2.6",
"shlinkio/shlink-importer": "^4.0",
"shlinkio/shlink-importer": "^5.0",
"shlinkio/shlink-installer": "^8.2",
"shlinkio/shlink-ip-geolocation": "^3.1",
"shlinkio/shlink-ip-geolocation": "^3.2",
"spiral/roadrunner": "^2.11",
"spiral/roadrunner-jobs": "^2.3",
"spiral/roadrunner-jobs": "^2.5",
"symfony/console": "^6.1",
"symfony/filesystem": "^6.1",
"symfony/lock": "^6.1",
@@ -64,10 +64,10 @@
"devster/ubench": "^2.1",
"dms/phpunit-arraysubset-asserts": "^0.4.0",
"infection/infection": "^0.26.15",
"openswoole/ide-helper": "~4.11.1",
"phpspec/prophecy-phpunit": "^2.0",
"openswoole/ide-helper": "~4.11.5",
"phpstan/phpstan": "^1.8",
"phpstan/phpstan-doctrine": "^1.3",
"phpstan/phpstan-phpunit": "^1.1",
"phpstan/phpstan-symfony": "^1.2",
"phpunit/php-code-coverage": "^9.2",
"phpunit/phpunit": "^9.5",
@@ -109,7 +109,7 @@
],
"cs": "phpcs",
"cs:fix": "phpcbf",
"stan": "APP_ENV=test php vendor/bin/phpstan analyse module/*/src module/*/config config docker/config data/migrations --level=8",
"stan": "APP_ENV=test php vendor/bin/phpstan analyse module/*/src module/*/test* module/*/config config docker/config data/migrations --level=8",
"test": [
"@parallel test:unit test:db",
"@parallel test:api test:cli"
@@ -132,10 +132,10 @@
"test:cli:ci": "GENERATE_COVERAGE=yes composer test:cli",
"test:cli:pretty": "GENERATE_COVERAGE=pretty composer test:cli",
"infect:ci:base": "infection --threads=max --only-covered --only-covering-test-cases --skip-initial-tests",
"infect:ci:unit": "@infect:ci:base --coverage=build/coverage-unit --min-msi=84",
"infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json",
"infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=80 --configuration=infection-api.json",
"infect:ci:cli": "@infect:ci:base --coverage=build/coverage-cli --min-msi=80 --configuration=infection-cli.json",
"infect:ci:unit": "@infect:ci:base --coverage=build/coverage-unit --min-msi=80",
"infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json5",
"infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=80 --configuration=infection-api.json5",
"infect:ci:cli": "@infect:ci:base --coverage=build/coverage-cli --min-msi=90 --configuration=infection-cli.json5",
"infect:ci": "@parallel infect:ci:unit infect:ci:db infect:ci:api infect:ci:cli",
"infect:test": [
"@parallel test:unit:ci test:db:sqlite:ci test:api:ci",

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Core\Config\EnvVars;
return (static function (): array {
$redisServers = EnvVars::REDIS_SERVERS->loadFromEnv();
$redis = ['pub_sub_enabled' => $redisServers !== null && EnvVars::REDIS_PUB_SUB_ENABLED->loadFromEnv(false)];
$cacheRedisBlock = $redisServers === null ? [] : [
'redis' => [
'servers' => $redisServers,
'sentinel_service' => EnvVars::REDIS_SENTINEL_SERVICE->loadFromEnv(),
],
];
return [
'cache' => [
'namespace' => 'Shlink',
...$cacheRedisBlock,
],
'redis' => $redis,
];
})();

View File

@@ -42,6 +42,9 @@ return (static function (): array {
'port' => EnvVars::DB_PORT->loadFromEnv($resolveDefaultPort()),
'unix_socket' => $isMysqlCompatible ? EnvVars::DB_UNIX_SOCKET->loadFromEnv() : null,
'charset' => $resolveCharset(),
'driverOptions' => $driver !== 'mssql' ? [] : [
'TrustServerCertificate' => 'true',
],
],
};

View File

@@ -1,27 +0,0 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Core\Config\EnvVars;
return (static function (): array {
$redisServers = EnvVars::REDIS_SERVERS->loadFromEnv();
$pubSub = [
'redis' => [
'pub_sub_enabled' => $redisServers !== null && EnvVars::REDIS_PUB_SUB_ENABLED->loadFromEnv(false),
],
];
return match ($redisServers) {
null => $pubSub,
default => [
'cache' => [
'redis' => [
'servers' => $redisServers,
'sentinel_service' => EnvVars::REDIS_SENTINEL_SERVICE->loadFromEnv(),
],
],
...$pubSub,
],
};
})();

View File

@@ -7,6 +7,8 @@ return [
'cache' => [
'redis' => [
'servers' => 'tcp://shlink_redis:6379',
// 'servers' => 'tcp://barbar@shlink_redis_acl:6379',
// 'servers' => 'tcp://foo:bar@shlink_redis_acl:6379',
],
],

View File

@@ -8,6 +8,7 @@ use Fig\Http\Message\RequestMethodInterface;
use RKA\Middleware\IpAddress;
use Shlinkio\Shlink\Core\Action as CoreAction;
use Shlinkio\Shlink\Core\Config\EnvVars;
use Shlinkio\Shlink\Core\ShortUrl\Middleware\TrimTrailingSlashMiddleware;
use Shlinkio\Shlink\Rest\Action;
use Shlinkio\Shlink\Rest\ConfigProvider;
use Shlinkio\Shlink\Rest\Middleware;
@@ -19,6 +20,8 @@ return (static function (): array {
$contentNegotiationMiddleware = Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class;
$dropDomainMiddleware = Middleware\ShortUrl\DropDefaultDomainFromRequestMiddleware::class;
$overrideDomainMiddleware = Middleware\ShortUrl\OverrideDomainMiddleware::class;
// TODO This should be based on config, not the env var
$shortUrlRouteSuffix = EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(false) ? '[/]' : '';
return [
@@ -97,6 +100,7 @@ return (static function (): array {
'path' => sprintf('/{shortCode}%s', $shortUrlRouteSuffix),
'middleware' => [
IpAddress::class,
TrimTrailingSlashMiddleware::class,
CoreAction\RedirectAction::class,
],
'allowed_methods' => [RequestMethodInterface::METHOD_GET],

View File

@@ -24,6 +24,7 @@ return (static function (): array {
'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES->loadFromEnv(false),
'append_extra_path' => (bool) EnvVars::REDIRECT_APPEND_EXTRA_PATH->loadFromEnv(false),
'multi_segment_slugs_enabled' => (bool) EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->loadFromEnv(false),
'trailing_slash_enabled' => (bool) EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(false),
],
];

View File

@@ -18,6 +18,7 @@ return [
],
'auto_resolve_titles' => true,
// 'multi_segment_slugs_enabled' => true,
// 'trailing_slash_enabled' => true,
],
];

View File

@@ -101,6 +101,9 @@ $buildDbConnection = static function (): array {
'user' => 'sa',
'password' => 'Passw0rd!',
'dbname' => 'shlink_test',
'driverOptions' => [
'TrustServerCertificate' => 'true',
],
],
default => [ // mysql and maria
'driver' => 'pdo_mysql',

View File

@@ -1,9 +1,10 @@
FROM php:8.1.9-fpm-alpine3.16
FROM php:8.2-fpm-alpine3.17
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
ENV APCU_VERSION 5.1.21
ENV PDO_SQLSRV_VERSION 5.10.1
ENV MS_ODBC_SQL_VERSION 17.5.2.2
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
RUN apk update
@@ -30,7 +31,9 @@ RUN docker-php-ext-install gd
RUN apk add --no-cache postgresql-dev
RUN docker-php-ext-install pdo_pgsql
RUN docker-php-ext-install sockets
RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS linux-headers && \
docker-php-ext-install sockets && \
apk del .phpize-deps
RUN docker-php-ext-install bcmath
# Install APCu extension
@@ -44,13 +47,13 @@ RUN mkdir -p /usr/src/php/ext/apcu \
&& echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini
# Install pcov and sqlsrv driver
RUN wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
apk add --allow-untrusted msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
RUN wget https://download.microsoft.com/download/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
apk add --allow-untrusted msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \
pecl install pdo_sqlsrv-${PDO_SQLSRV_VERSION} pcov && \
docker-php-ext-enable pdo_sqlsrv pcov && \
apk del .phpize-deps && \
rm msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk
rm msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk
# Install composer
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer

View File

@@ -0,0 +1,2 @@
user foo allcommands allkeys on >bar
requirepass barbar

View File

@@ -1,9 +1,10 @@
FROM php:8.1.9-alpine3.16
FROM php:8.2-alpine3.17
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
ENV APCU_VERSION 5.1.21
ENV PDO_SQLSRV_VERSION 5.10.1
ENV MS_ODBC_SQL_VERSION 17.5.2.2
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
RUN apk update
@@ -30,7 +31,9 @@ RUN docker-php-ext-install gd
RUN apk add --no-cache postgresql-dev
RUN docker-php-ext-install pdo_pgsql
RUN docker-php-ext-install sockets
RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS linux-headers && \
docker-php-ext-install sockets && \
apk del .phpize-deps
RUN docker-php-ext-install bcmath
# Install APCu extension
@@ -44,13 +47,13 @@ RUN mkdir -p /usr/src/php/ext/apcu \
&& echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini
# Install pcov and sqlsrv driver
RUN wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
apk add --allow-untrusted msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
RUN wget https://download.microsoft.com/download/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
apk add --allow-untrusted msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \
pecl install pdo_sqlsrv-${PDO_SQLSRV_VERSION} pcov && \
docker-php-ext-enable pdo_sqlsrv pcov && \
apk del .phpize-deps && \
rm msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk
rm msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk
# Install composer
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer

View File

@@ -1,11 +1,12 @@
FROM php:8.1.9-alpine3.16
FROM php:8.2-alpine3.17
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
ENV APCU_VERSION 5.1.21
ENV INOTIFY_VERSION 3.0.0
ENV OPENSWOOLE_VERSION 4.11.1
ENV OPENSWOOLE_VERSION 4.12.0
ENV PDO_SQLSRV_VERSION 5.10.1
ENV MS_ODBC_SQL_VERSION 17.5.2.2
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
RUN apk update
@@ -32,7 +33,9 @@ RUN docker-php-ext-install gd
RUN apk add --no-cache postgresql-dev
RUN docker-php-ext-install pdo_pgsql
RUN docker-php-ext-install sockets
RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS linux-headers && \
docker-php-ext-install sockets && \
apk del .phpize-deps
RUN docker-php-ext-install bcmath
# Install APCu extension
@@ -54,13 +57,13 @@ RUN mkdir -p /usr/src/php/ext/inotify \
&& rm /tmp/inotify.tar.gz
# Install openswoole, pcov and mssql driver
RUN wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
apk add --allow-untrusted msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
RUN wget https://download.microsoft.com/download/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
apk add --allow-untrusted msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \
pecl install openswoole-${OPENSWOOLE_VERSION} pdo_sqlsrv-${PDO_SQLSRV_VERSION} pcov && \
docker-php-ext-enable openswoole pdo_sqlsrv pcov && \
apk del .phpize-deps && \
rm msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk
rm msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk
# Install composer
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer

View File

@@ -8,7 +8,7 @@ use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
final class Version20210207100807 extends AbstractMigration

View File

@@ -29,6 +29,7 @@ services:
- shlink_db_maria
- shlink_db_ms
- shlink_redis
- shlink_redis_acl
- shlink_mercure
- shlink_mercure_proxy
- shlink_rabbitmq
@@ -65,6 +66,7 @@ services:
- shlink_db_maria
- shlink_db_ms
- shlink_redis
- shlink_redis_acl
- shlink_mercure
- shlink_mercure_proxy
- shlink_rabbitmq
@@ -89,6 +91,7 @@ services:
- shlink_db_maria
- shlink_db_ms
- shlink_redis
- shlink_redis_acl
- shlink_mercure
- shlink_mercure_proxy
- shlink_rabbitmq
@@ -146,10 +149,19 @@ services:
shlink_redis:
container_name: shlink_redis
image: redis:6.0-alpine
image: redis:6.2-alpine
ports:
- "6380:6379"
shlink_redis_acl:
container_name: shlink_redis_acl
image: redis:6.2-alpine
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
ports:
- "6382:6379"
volumes:
- ./data/infra/redis/redis-acl.conf:/usr/local/etc/redis/redis.conf
shlink_mercure_proxy:
container_name: shlink_mercure_proxy
image: nginx:1.19.6-alpine

View File

@@ -6,6 +6,7 @@
"longUrl",
"dateCreated",
"visitsCount",
"visitsSummary",
"tags",
"meta",
"domain",
@@ -32,8 +33,12 @@
"description": "The date in which the short URL was created in ISO format."
},
"visitsCount": {
"deprecated": true,
"type": "integer",
"description": "The number of visits that this short URL has received."
"description": "**[DEPRECATED]** Use `visitsSummary.total` instead."
},
"visitsSummary": {
"$ref": "./ShortUrlVisitsSummary.json"
},
"tags": {
"type": "array",

View File

@@ -0,0 +1,18 @@
{
"type": "object",
"required": ["total", "nonBots", "bots"],
"properties": {
"total": {
"description": "The total amount of visits that this short URL has received.",
"type": "integer"
},
"nonBots": {
"description": "The amount of visits which were not identified as bots.",
"type": "integer"
},
"bots": {
"description": "The amount of visits that were identified as potential bots.",
"type": "integer"
}
}
}

View File

@@ -73,10 +73,12 @@
"shortCode-DESC",
"dateCreated-ASC",
"dateCreated-DESC",
"title-ASC",
"title-DESC",
"visits-ASC",
"visits-DESC",
"title-ASC",
"title-DESC"
"nonBotVisits-ASC",
"nonBotVisits-DESC"
]
}
},
@@ -97,6 +99,32 @@
"schema": {
"type": "string"
}
},
{
"name": "excludeMaxVisitsReached",
"in": "query",
"description": "If true, short URLs which already reached their maximum amount of visits will be excluded.",
"required": false,
"schema": {
"type": "string",
"enum": [
"true",
"false"
]
}
},
{
"name": "excludePastValidUntil",
"in": "query",
"description": "If true, short URLs which validUntil date is on the past will be excluded.",
"required": false,
"schema": {
"type": "string",
"enum": [
"true",
"false"
]
}
}
],
"security": [
@@ -136,7 +164,11 @@
"shortUrl": "https://doma.in/12C18",
"longUrl": "https://store.steampowered.com",
"dateCreated": "2016-08-21T20:34:16+02:00",
"visitsCount": 328,
"visitsSummary": {
"total": 328,
"nonBots": 328,
"bots": 0
},
"tags": [
"games",
"tech"
@@ -155,7 +187,11 @@
"shortUrl": "https://doma.in/12Kb3",
"longUrl": "https://shlink.io",
"dateCreated": "2016-05-01T20:34:16+02:00",
"visitsCount": 1029,
"visitsSummary": {
"total": 1029,
"nonBots": 900,
"bots": 129
},
"tags": [
"shlink"
],
@@ -173,7 +209,11 @@
"shortUrl": "https://example.com/123bA",
"longUrl": "https://www.google.com",
"dateCreated": "2015-10-01T20:34:16+02:00",
"visitsCount": 25,
"visitsSummary": {
"total": 25,
"nonBots": 0,
"bots": 25
},
"tags": [],
"meta": {
"validSince": "2017-01-21T00:00:00+02:00",
@@ -281,7 +321,11 @@
"shortUrl": "https://doma.in/12C18",
"longUrl": "https://store.steampowered.com",
"dateCreated": "2016-08-21T20:34:16+02:00",
"visitsCount": 0,
"visitsSummary": {
"total": 0,
"nonBots": 0,
"bots": 0
},
"tags": [
"games",
"tech"

View File

@@ -55,7 +55,11 @@
"shortUrl": "https://doma.in/abc123",
"shortCode": "abc123",
"dateCreated": "2016-08-21T20:34:16+02:00",
"visitsCount": 0,
"visitsSummary": {
"total": 0,
"nonBots": 0,
"bots": 0
},
"tags": [
"games",
"tech"

View File

@@ -41,7 +41,11 @@
"shortUrl": "https://doma.in/12Kb3",
"longUrl": "https://shlink.io",
"dateCreated": "2016-05-01T20:34:16+02:00",
"visitsCount": 1029,
"visitsSummary": {
"total": 1029,
"nonBots": 820,
"bots": 209
},
"tags": [
"shlink"
],
@@ -159,7 +163,11 @@
"shortUrl": "https://doma.in/12Kb3",
"longUrl": "https://shlink.io",
"dateCreated": "2016-05-01T20:34:16+02:00",
"visitsCount": 1029,
"visitsSummary": {
"total": 1029,
"nonBots": 900,
"bots": 129
},
"tags": [
"shlink"
],

View File

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

View File

@@ -1,24 +0,0 @@
{
"source": {
"directories": [
"module/*/src"
]
},
"timeout": 5,
"logs": {
"text": "build/infection-api/infection-log.txt",
"html": "build/infection-api/infection-log.html",
"summary": "build/infection-api/summary-log.txt",
"debug": "build/infection-api/debug-log.txt"
},
"tmpDir": "build/infection-api/temp",
"phpUnit": {
"configDir": "."
},
"testFrameworkOptions": "--configuration=phpunit-api.xml",
"mutators": {
"@default": true,
"IdenticalEqual": false,
"NotIdenticalNotEqual": false
}
}

24
infection-api.json5 Normal file
View File

@@ -0,0 +1,24 @@
{
source: {
directories: [
'module/*/src'
]
},
timeout: 5,
logs: {
text: 'build/infection-api/infection-log.txt',
html: 'build/infection-api/infection-log.html',
summary: 'build/infection-api/summary-log.txt',
debug: 'build/infection-api/debug-log.txt'
},
tmpDir: 'build/infection-api/temp',
phpUnit: {
configDir: '.'
},
testFrameworkOptions: '--configuration=phpunit-api.xml',
mutators: {
'@default': true,
IdenticalEqual: false,
NotIdenticalNotEqual: false
}
}

View File

@@ -1,24 +0,0 @@
{
"source": {
"directories": [
"module/*/src"
]
},
"timeout": 5,
"logs": {
"text": "build/infection-cli/infection-log.txt",
"html": "build/infection-cli/infection-log.html",
"summary": "build/infection-cli/summary-log.txt",
"debug": "build/infection-cli/debug-log.txt"
},
"tmpDir": "build/infection-cli/temp",
"phpUnit": {
"configDir": "."
},
"testFrameworkOptions": "--configuration=phpunit-cli.xml",
"mutators": {
"@default": true,
"IdenticalEqual": false,
"NotIdenticalNotEqual": false
}
}

24
infection-cli.json5 Normal file
View File

@@ -0,0 +1,24 @@
{
source: {
directories: [
'module/*/src'
]
},
timeout: 5,
logs: {
text: 'build/infection-cli/infection-log.txt',
html: 'build/infection-cli/infection-log.html',
summary: 'build/infection-cli/summary-log.txt',
debug: 'build/infection-cli/debug-log.txt'
},
tmpDir: 'build/infection-cli/temp',
phpUnit: {
configDir: '.'
},
testFrameworkOptions: '--configuration=phpunit-cli.xml',
mutators: {
'@default': true,
IdenticalEqual: false,
NotIdenticalNotEqual: false
}
}

View File

@@ -1,24 +0,0 @@
{
"source": {
"directories": [
"module/*/src"
]
},
"timeout": 5,
"logs": {
"text": "build/infection-db/infection-log.txt",
"html": "build/infection-db/infection-log.html",
"summary": "build/infection-db/summary-log.txt",
"debug": "build/infection-db/debug-log.txt"
},
"tmpDir": "build/infection-db/temp",
"phpUnit": {
"configDir": "."
},
"testFrameworkOptions": "--configuration=phpunit-db.xml",
"mutators": {
"@default": true,
"IdenticalEqual": false,
"NotIdenticalNotEqual": false
}
}

24
infection-db.json5 Normal file
View File

@@ -0,0 +1,24 @@
{
source: {
directories: [
'module/*/src'
]
},
timeout: 5,
logs: {
text: 'build/infection-db/infection-log.txt',
html: 'build/infection-db/infection-log.html',
summary: 'build/infection-db/summary-log.txt',
debug: 'build/infection-db/debug-log.txt'
},
tmpDir: 'build/infection-db/temp',
phpUnit: {
configDir: '.'
},
testFrameworkOptions: '--configuration=phpunit-db.xml',
mutators: {
'@default': true,
IdenticalEqual: false,
NotIdenticalNotEqual: false
}
}

View File

@@ -1,26 +0,0 @@
{
"source": {
"directories": [
"module/*/src"
]
},
"timeout": 5,
"logs": {
"text": "build/infection-unit/infection-log.txt",
"html": "build/infection-unit/infection-log.html",
"summary": "build/infection-unit/summary-log.txt",
"debug": "build/infection-unit/debug-log.txt",
"stryker": {
"report": "develop"
}
},
"tmpDir": "build/infection-unit/temp",
"phpUnit": {
"configDir": "."
},
"mutators": {
"@default": true,
"IdenticalEqual": false,
"NotIdenticalNotEqual": false
}
}

26
infection.json5 Normal file
View File

@@ -0,0 +1,26 @@
{
source: {
directories: [
'module/*/src'
]
},
timeout: 5,
logs: {
text: 'build/infection-unit/infection-log.txt',
html: 'build/infection-unit/infection-log.html',
summary: 'build/infection-unit/summary-log.txt',
debug: 'build/infection-unit/debug-log.txt',
stryker: {
report: 'develop'
}
},
tmpDir: 'build/infection-unit/temp',
phpUnit: {
configDir: '.'
},
mutators: {
'@default': true,
IdenticalEqual: false,
NotIdenticalNotEqual: false
}
}

View File

@@ -12,9 +12,8 @@ use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory;
use Shlinkio\Shlink\Core\Domain\DomainService;
use Shlinkio\Shlink\Core\Options\TrackingOptions;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Core\Service;
use Shlinkio\Shlink\Core\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer;
use Shlinkio\Shlink\Core\Tag\TagService;
use Shlinkio\Shlink\Core\Visit;
use Shlinkio\Shlink\Installer\Factory\ProcessHelperFactory;
@@ -79,22 +78,22 @@ return [
ApiKey\RoleResolver::class => [DomainService::class, 'config.url_shortener.domain.hostname'],
Command\ShortUrl\CreateShortUrlCommand::class => [
Service\UrlShortener::class,
ShortUrl\UrlShortener::class,
ShortUrlStringifier::class,
UrlShortenerOptions::class,
],
Command\ShortUrl\ResolveUrlCommand::class => [Service\ShortUrl\ShortUrlResolver::class],
Command\ShortUrl\ResolveUrlCommand::class => [ShortUrl\ShortUrlResolver::class],
Command\ShortUrl\ListShortUrlsCommand::class => [
Service\ShortUrlService::class,
ShortUrlDataTransformer::class,
ShortUrl\ShortUrlListService::class,
ShortUrl\Transformer\ShortUrlDataTransformer::class,
],
Command\ShortUrl\GetShortUrlVisitsCommand::class => [Visit\VisitsStatsHelper::class],
Command\ShortUrl\DeleteShortUrlCommand::class => [Service\ShortUrl\DeleteShortUrlService::class],
Command\ShortUrl\DeleteShortUrlCommand::class => [ShortUrl\DeleteShortUrlService::class],
Command\Visit\DownloadGeoLiteDbCommand::class => [GeoLite\GeolocationDbUpdater::class],
Command\Visit\LocateVisitsCommand::class => [
Visit\VisitLocator::class,
Visit\VisitToLocationHelper::class,
Visit\Geolocation\VisitLocator::class,
Visit\Geolocation\VisitToLocationHelper::class,
LockFactory::class,
],
Command\Visit\GetOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class],

View File

@@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\CLI\ApiKey;
use Shlinkio\Shlink\CLI\Exception\InvalidRoleConfigException;
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
use Shlinkio\Shlink\Rest\ApiKey\Role;
use Symfony\Component\Console\Input\InputInterface;
use function is_string;
@@ -19,8 +20,8 @@ class RoleResolver implements RoleResolverInterface
public function determineRoles(InputInterface $input): array
{
$domainAuthority = $input->getOption(self::DOMAIN_ONLY_PARAM);
$author = $input->getOption(self::AUTHOR_ONLY_PARAM);
$domainAuthority = $input->getOption(Role::DOMAIN_SPECIFIC->paramName());
$author = $input->getOption(Role::AUTHORED_SHORT_URLS->paramName());
$roleDefinitions = [];
if ($author) {

View File

@@ -9,9 +9,6 @@ use Symfony\Component\Console\Input\InputInterface;
interface RoleResolverInterface
{
public const AUTHOR_ONLY_PARAM = 'author-only';
public const DOMAIN_ONLY_PARAM = 'domain-only';
/**
* @return RoleDefinition[]
*/

View File

@@ -32,8 +32,8 @@ class GenerateKeyCommand extends Command
protected function configure(): void
{
$authorOnly = RoleResolverInterface::AUTHOR_ONLY_PARAM;
$domainOnly = RoleResolverInterface::DOMAIN_ONLY_PARAM;
$authorOnly = Role::AUTHORED_SHORT_URLS->paramName();
$domainOnly = Role::DOMAIN_SPECIFIC->paramName();
$help = <<<HELP
The <info>%command.name%</info> generates a new valid API key.

View File

@@ -62,8 +62,8 @@ class ListKeysCommand extends Command
$rowData[] = $apiKey->isAdmin() ? 'Admin' : implode("\n", $apiKey->mapRoles(
fn (Role $role, array $meta) =>
empty($meta)
? Role::toFriendlyName($role)
: sprintf('%s: %s', Role::toFriendlyName($role), Role::domainAuthorityFromMeta($meta)),
? $role->toFriendlyName()
: sprintf('%s: %s', $role->toFriendlyName(), Role::domainAuthorityFromMeta($meta)),
));
return $rowData;

View File

@@ -7,9 +7,9 @@ namespace Shlinkio\Shlink\CLI\Command\Domain;
use Shlinkio\Shlink\CLI\Command\Visit\AbstractVisitsListCommand;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -25,7 +25,7 @@ class GetDomainVisitsCommand extends AbstractVisitsListCommand
parent::__construct($visitsHelper);
}
protected function doConfigure(): void
protected function configure(): void
{
$this
->setName(self::NAME)

View File

@@ -8,11 +8,11 @@ use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Config\EnvVars;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
use Shlinkio\Shlink\Core\ShortUrl\UrlShortenerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -162,7 +162,7 @@ class CreateShortUrlCommand extends Command
$doValidateUrl = $input->getOption('validate-url');
try {
$shortUrl = $this->urlShortener->shorten(ShortUrlMeta::fromRawData([
$shortUrl = $this->urlShortener->shorten(ShortUrlCreation::fromRawData([
ShortUrlInputFilter::LONG_URL => $longUrl,
ShortUrlInputFilter::VALID_SINCE => $input->getOption('valid-since'),
ShortUrlInputFilter::VALID_UNTIL => $input->getOption('valid-until'),

View File

@@ -6,8 +6,8 @@ namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Exception;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
use Shlinkio\Shlink\Core\ShortUrl\DeleteShortUrlServiceInterface;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -21,7 +21,7 @@ class DeleteShortUrlCommand extends Command
{
public const NAME = 'short-url:delete';
public function __construct(private DeleteShortUrlServiceInterface $deleteShortUrlService)
public function __construct(private readonly DeleteShortUrlServiceInterface $deleteShortUrlService)
{
parent::__construct();
}

View File

@@ -7,9 +7,9 @@ namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Command\Visit\AbstractVisitsListCommand;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -20,7 +20,7 @@ class GetShortUrlVisitsCommand extends AbstractVisitsListCommand
{
public const NAME = 'short-url:visits';
protected function doConfigure(): void
protected function configure(): void
{
$this
->setName(self::NAME)

View File

@@ -4,17 +4,19 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Command\Util\AbstractWithDateRangeCommand;
use Shlinkio\Shlink\CLI\Option\EndDateOption;
use Shlinkio\Shlink\CLI\Option\StartDateOption;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtilsTrait;
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
use Shlinkio\Shlink\Core\Validation\ShortUrlsParamsInputFilter;
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlsParamsInputFilter;
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlListServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@@ -27,20 +29,25 @@ use function Functional\map;
use function implode;
use function sprintf;
class ListShortUrlsCommand extends AbstractWithDateRangeCommand
class ListShortUrlsCommand extends Command
{
use PagerfantaUtilsTrait;
public const NAME = 'short-url:list';
private readonly StartDateOption $startDateOption;
private readonly EndDateOption $endDateOption;
public function __construct(
private ShortUrlServiceInterface $shortUrlService,
private DataTransformerInterface $transformer,
private readonly ShortUrlListServiceInterface $shortUrlService,
private readonly DataTransformerInterface $transformer,
) {
parent::__construct();
$this->startDateOption = new StartDateOption($this, 'short URLs');
$this->endDateOption = new EndDateOption($this, 'short URLs');
}
protected function doConfigure(): void
protected function configure(): void
{
$this
->setName(self::NAME)
@@ -70,6 +77,18 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
InputOption::VALUE_NONE,
'If tags is provided, returns only short URLs having ALL tags.',
)
->addOption(
'exclude-max-visits-reached',
null,
InputOption::VALUE_NONE,
'Excludes short URLs which reached their max amount of visits.',
)
->addOption(
'exclude-past-valid-until',
null,
InputOption::VALUE_NONE,
'Excludes short URLs which have a "validUntil" date in the past.',
)
->addOption(
'order-by',
'o',
@@ -104,16 +123,6 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
);
}
protected function getStartDateDesc(string $optionName): string
{
return sprintf('Allows to filter short URLs, returning only those created after "%s".', $optionName);
}
protected function getEndDateDesc(string $optionName): string
{
return sprintf('Allows to filter short URLs, returning only those created before "%s".', $optionName);
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$io = new SymfonyStyle($input, $output);
@@ -124,8 +133,8 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
$tagsMode = $input->getOption('including-all-tags') === true ? TagsMode::ALL->value : TagsMode::ANY->value;
$tags = ! empty($tags) ? explode(',', $tags) : [];
$all = $input->getOption('all');
$startDate = $this->getStartDateOption($input, $output);
$endDate = $this->getEndDateOption($input, $output);
$startDate = $this->startDateOption->get($input, $output);
$endDate = $this->endDateOption->get($input, $output);
$orderBy = $this->processOrderBy($input);
$columnsMap = $this->resolveColumnsMap($input);
@@ -136,6 +145,8 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
ShortUrlsParamsInputFilter::ORDER_BY => $orderBy,
ShortUrlsParamsInputFilter::START_DATE => $startDate?->toAtomString(),
ShortUrlsParamsInputFilter::END_DATE => $endDate?->toAtomString(),
ShortUrlsParamsInputFilter::EXCLUDE_MAX_VISITS_REACHED => $input->getOption('exclude-max-visits-reached'),
ShortUrlsParamsInputFilter::EXCLUDE_PAST_VALID_UNTIL => $input->getOption('exclude-past-valid-until'),
];
if ($all) {

View File

@@ -6,8 +6,8 @@ namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -21,7 +21,7 @@ class ResolveUrlCommand extends Command
{
public const NAME = 'short-url:parse';
public function __construct(private ShortUrlResolverInterface $urlResolver)
public function __construct(private readonly ShortUrlResolverInterface $urlResolver)
{
parent::__construct();
}

View File

@@ -7,9 +7,9 @@ namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\CLI\Command\Visit\AbstractVisitsListCommand;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -25,7 +25,7 @@ class GetTagVisitsCommand extends AbstractVisitsListCommand
parent::__construct($visitsHelper);
}
protected function doConfigure(): void
protected function configure(): void
{
$this
->setName(self::NAME)

View File

@@ -1,69 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Util;
use Cake\Chronos\Chronos;
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 Throwable;
use function is_string;
use function sprintf;
abstract class AbstractWithDateRangeCommand extends Command
{
private const START_DATE = 'start-date';
private const END_DATE = 'end-date';
final protected function configure(): void
{
$this->doConfigure();
$this
->addOption(self::START_DATE, 's', InputOption::VALUE_REQUIRED, $this->getStartDateDesc(self::START_DATE))
->addOption(self::END_DATE, 'e', InputOption::VALUE_REQUIRED, $this->getEndDateDesc(self::END_DATE));
}
protected function getStartDateOption(InputInterface $input, OutputInterface $output): ?Chronos
{
return $this->getDateOption($input, $output, self::START_DATE);
}
protected function getEndDateOption(InputInterface $input, OutputInterface $output): ?Chronos
{
return $this->getDateOption($input, $output, self::END_DATE);
}
private function getDateOption(InputInterface $input, OutputInterface $output, string $key): ?Chronos
{
$value = $input->getOption($key);
if (empty($value) || ! is_string($value)) {
return null;
}
try {
return Chronos::parse($value);
} catch (Throwable $e) {
$output->writeln(sprintf(
'<comment>> Ignored provided "%s" since its value "%s" is not a valid date. <</comment>',
$key,
$value,
));
if ($output->isVeryVerbose()) {
$this->getApplication()?->renderThrowable($e, $output);
}
return null;
}
}
abstract protected function doConfigure(): void;
abstract protected function getStartDateDesc(string $optionName): string;
abstract protected function getEndDateDesc(string $optionName): string;
}

View File

@@ -4,13 +4,15 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Visit;
use Shlinkio\Shlink\CLI\Command\Util\AbstractWithDateRangeCommand;
use Shlinkio\Shlink\CLI\Option\EndDateOption;
use Shlinkio\Shlink\CLI\Option\StartDateOption;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@@ -19,29 +21,23 @@ use function Functional\map;
use function Functional\select_keys;
use function Shlinkio\Shlink\Common\buildDateRange;
use function Shlinkio\Shlink\Core\camelCaseToHumanFriendly;
use function sprintf;
abstract class AbstractVisitsListCommand extends AbstractWithDateRangeCommand
abstract class AbstractVisitsListCommand extends Command
{
private readonly StartDateOption $startDateOption;
private readonly EndDateOption $endDateOption;
public function __construct(protected readonly VisitsStatsHelperInterface $visitsHelper)
{
parent::__construct();
}
final protected function getStartDateDesc(string $optionName): string
{
return sprintf('Allows to filter visits, returning only those older than "%s".', $optionName);
}
final protected function getEndDateDesc(string $optionName): string
{
return sprintf('Allows to filter visits, returning only those newer than "%s".', $optionName);
$this->startDateOption = new StartDateOption($this, 'visits');
$this->endDateOption = new EndDateOption($this, 'visits');
}
final protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$startDate = $this->getStartDateOption($input, $output);
$endDate = $this->getEndDateOption($input, $output);
$startDate = $this->startDateOption->get($input, $output);
$endDate = $this->endDateOption->get($input, $output);
$paginator = $this->getVisitsPaginator($input, buildDateRange($startDate, $endDate));
[$rows, $headers] = $this->resolveRowsAndHeaders($paginator);

View File

@@ -6,9 +6,9 @@ namespace Shlinkio\Shlink\CLI\Command\Visit;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
use Symfony\Component\Console\Input\InputInterface;
@@ -23,7 +23,7 @@ class GetNonOrphanVisitsCommand extends AbstractVisitsListCommand
parent::__construct($visitsHelper);
}
protected function doConfigure(): void
protected function configure(): void
{
$this
->setName(self::NAME)

View File

@@ -6,15 +6,15 @@ namespace Shlinkio\Shlink\CLI\Command\Visit;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
use Symfony\Component\Console\Input\InputInterface;
class GetOrphanVisitsCommand extends AbstractVisitsListCommand
{
public const NAME = 'visit:orphan';
protected function doConfigure(): void
protected function configure(): void
{
$this
->setName(self::NAME)

View File

@@ -8,13 +8,13 @@ use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand;
use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Common\Util\IpAddress;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Visit\Geolocation\VisitGeolocationHelperInterface;
use Shlinkio\Shlink\Core\Visit\Geolocation\VisitLocatorInterface;
use Shlinkio\Shlink\Core\Visit\Geolocation\VisitToLocationHelperInterface;
use Shlinkio\Shlink\Core\Visit\Model\UnlocatableIpType;
use Shlinkio\Shlink\Core\Visit\VisitGeolocationHelperInterface;
use Shlinkio\Shlink\Core\Visit\VisitLocatorInterface;
use Shlinkio\Shlink\Core\Visit\VisitToLocationHelperInterface;
use Shlinkio\Shlink\IpGeolocation\Model\Location;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\ArrayInput;

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Option;
use Cake\Chronos\Chronos;
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 Throwable;
use function is_string;
use function sprintf;
class DateOption
{
public function __construct(
private readonly Command $command,
private readonly string $name,
string $shortcut,
string $description,
) {
$command->addOption($name, $shortcut, InputOption::VALUE_REQUIRED, $description);
}
public function get(InputInterface $input, OutputInterface $output): ?Chronos
{
$value = $input->getOption($this->name);
if (empty($value) || ! is_string($value)) {
return null;
}
try {
return Chronos::parse($value);
} catch (Throwable $e) {
$output->writeln(sprintf(
'<comment>> Ignored provided "%s" since its value "%s" is not a valid date. <</comment>',
$this->name,
$value,
));
if ($output->isVeryVerbose()) {
$this->command->getApplication()?->renderThrowable($e, $output);
}
return null;
}
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Option;
use Cake\Chronos\Chronos;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function sprintf;
class EndDateOption
{
private readonly DateOption $dateOption;
public function __construct(Command $command, string $descriptionHint)
{
$this->dateOption = new DateOption($command, 'end-date', 'e', sprintf(
'Allows to filter %s, returning only those newer than provided date.',
$descriptionHint,
));
}
public function get(InputInterface $input, OutputInterface $output): ?Chronos
{
return $this->dateOption->get($input, $output);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Option;
use Cake\Chronos\Chronos;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function sprintf;
class StartDateOption
{
private readonly DateOption $dateOption;
public function __construct(Command $command, string $descriptionHint)
{
$this->dateOption = new DateOption($command, 'start-date', 's', sprintf(
'Allows to filter %s, returning only those older than provided date.',
$descriptionHint,
));
}
public function get(InputInterface $input, OutputInterface $output): ?Chronos
{
return $this->dateOption->get($input, $output);
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace ShlinkioCliTest\Shlink\CLI\Command;
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
class ListShortUrlsTest extends CliTestCase
{
/**
* @test
* @dataProvider provideFlagsAndOutput
*/
public function generatesExpectedOutput(array $flags, string $expectedOutput): void
{
[$output] = $this->exec([ListShortUrlsCommand::NAME, ...$flags], ['no']);
self::assertStringContainsString($expectedOutput, $output);
}
public function provideFlagsAndOutput(): iterable
{
// phpcs:disable Generic.Files.LineLength
yield 'no flags' => [[], <<<OUTPUT
+--------------------+---------------+-------------------------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+--------------------+---------------+-------------------------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| ghi789 | | http://example.com/ghi789 | https://blog.alejandrocelaya.com/2019/04/27/considerations-to-properly-use-open-source-software-projects/ | 2019-01-01T00:00:30+00:00 | 0 |
| custom | | http://doma.in/custom | https://shlink.io | 2019-01-01T00:00:20+00:00 | 0 |
| def456 | | http://doma.in/def456 | https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/ | 2019-01-01T00:00:10+00:00 | 2 |
| custom-with-domain | | http://some-domain.com/custom-with-domain | https://google.com | 2018-10-20T00:00:00+00:00 | 0 |
| abc123 | My cool title | http://doma.in/abc123 | https://shlink.io | 2018-05-01T00:00:00+00:00 | 3 |
| ghi789 | | http://doma.in/ghi789 | https://shlink.io/documentation/ | 2018-05-01T00:00:00+00:00 | 2 |
+--------------------+---------------+-------------------------------------------+---------------------------- Page 1 of 1 ------------------------------------------------------------------+---------------------------+--------------+
OUTPUT];
yield 'start date' => [['--start-date=2019-01'], <<<OUTPUT
+------------+-------+---------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+------------+-------+---------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| ghi789 | | http://example.com/ghi789 | https://blog.alejandrocelaya.com/2019/04/27/considerations-to-properly-use-open-source-software-projects/ | 2019-01-01T00:00:30+00:00 | 0 |
| custom | | http://doma.in/custom | https://shlink.io | 2019-01-01T00:00:20+00:00 | 0 |
| def456 | | http://doma.in/def456 | https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/ | 2019-01-01T00:00:10+00:00 | 2 |
+------------+-------+---------------------------+-------------------------------------------- Page 1 of 1 --------------------------------------------------+---------------------------+--------------+
OUTPUT];
yield 'end date' => [['-e 2018-12-01'], <<<OUTPUT
+--------------------+---------------+-------------------------------------------+----------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+--------------------+---------------+-------------------------------------------+----------------------------------+---------------------------+--------------+
| custom-with-domain | | http://some-domain.com/custom-with-domain | https://google.com | 2018-10-20T00:00:00+00:00 | 0 |
| abc123 | My cool title | http://doma.in/abc123 | https://shlink.io | 2018-05-01T00:00:00+00:00 | 3 |
| ghi789 | | http://doma.in/ghi789 | https://shlink.io/documentation/ | 2018-05-01T00:00:00+00:00 | 2 |
+--------------------+---------------+----------------------------------- Page 1 of 1 ------------------------------+---------------------------+--------------+
OUTPUT];
yield 'start and end date' => [['-s 2018-06-20', '--end-date=2019-01-01T00:00:20+00:00'], <<<OUTPUT
+--------------------+-------+-------------------------------------------+-----------------------------------------------------------------------------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+--------------------+-------+-------------------------------------------+-----------------------------------------------------------------------------------------------------+---------------------------+--------------+
| custom | | http://doma.in/custom | https://shlink.io | 2019-01-01T00:00:20+00:00 | 0 |
| def456 | | http://doma.in/def456 | https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/ | 2019-01-01T00:00:10+00:00 | 2 |
| custom-with-domain | | http://some-domain.com/custom-with-domain | https://google.com | 2018-10-20T00:00:00+00:00 | 0 |
+--------------------+-------+-------------------------------------------+----------------------------- Page 1 of 1 -----------------------------------------------------------+---------------------------+--------------+
OUTPUT];
yield 'expired excluded' => [['--exclude-max-visits-reached', '--exclude-past-valid-until'], <<<OUTPUT
+--------------------+-------+-------------------------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+--------------------+-------+-------------------------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| ghi789 | | http://example.com/ghi789 | https://blog.alejandrocelaya.com/2019/04/27/considerations-to-properly-use-open-source-software-projects/ | 2019-01-01T00:00:30+00:00 | 0 |
| custom | | http://doma.in/custom | https://shlink.io | 2019-01-01T00:00:20+00:00 | 0 |
| def456 | | http://doma.in/def456 | https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/ | 2019-01-01T00:00:10+00:00 | 2 |
| custom-with-domain | | http://some-domain.com/custom-with-domain | https://google.com | 2018-10-20T00:00:00+00:00 | 0 |
+--------------------+-------+-------------------------------------------+-------------------------------- Page 1 of 1 --------------------------------------------------------------+---------------------------+--------------+
OUTPUT];
// phpcs:enable
}
}

View File

@@ -4,27 +4,27 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\ApiKey;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\ApiKey\RoleResolver;
use Shlinkio\Shlink\CLI\Exception\InvalidRoleConfigException;
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
use Shlinkio\Shlink\Core\Entity\Domain;
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
use Shlinkio\Shlink\Rest\ApiKey\Role;
use Symfony\Component\Console\Input\InputInterface;
use function Functional\map;
class RoleResolverTest extends TestCase
{
use ProphecyTrait;
private RoleResolver $resolver;
private ObjectProphecy $domainService;
private MockObject & DomainServiceInterface $domainService;
protected function setUp(): void
{
$this->domainService = $this->prophesize(DomainServiceInterface::class);
$this->resolver = new RoleResolver($this->domainService->reveal(), 'default.com');
$this->domainService = $this->createMock(DomainServiceInterface::class);
$this->resolver = new RoleResolver($this->domainService, 'default.com');
}
/**
@@ -36,61 +36,63 @@ class RoleResolverTest extends TestCase
array $expectedRoles,
int $expectedDomainCalls,
): void {
$getDomain = $this->domainService->getOrCreate('example.com')->willReturn(
Domain::withAuthority('example.com')->setId('1'),
);
$this->domainService->expects($this->exactly($expectedDomainCalls))->method('getOrCreate')->with(
'example.com',
)->willReturn($this->domainWithId(Domain::withAuthority('example.com')));
$result = $this->resolver->determineRoles($input);
self::assertEquals($expectedRoles, $result);
$getDomain->shouldHaveBeenCalledTimes($expectedDomainCalls);
}
public function provideRoles(): iterable
{
$domain = Domain::withAuthority('example.com')->setId('1');
$domain = $this->domainWithId(Domain::withAuthority('example.com'));
$buildInput = function (array $definition): InputInterface {
$input = $this->prophesize(InputInterface::class);
$input = $this->createStub(InputInterface::class);
$input->method('getOption')->willReturnMap(
map($definition, static fn (mixed $returnValue, string $param) => [$param, $returnValue]),
);
foreach ($definition as $name => $value) {
$input->getOption($name)->willReturn($value);
}
return $input->reveal();
return $input;
};
yield 'no roles' => [
$buildInput([RoleResolver::DOMAIN_ONLY_PARAM => null, RoleResolver::AUTHOR_ONLY_PARAM => false]),
$buildInput([Role::DOMAIN_SPECIFIC->paramName() => null, Role::AUTHORED_SHORT_URLS->paramName() => false]),
[],
0,
];
yield 'domain role only' => [
$buildInput([RoleResolver::DOMAIN_ONLY_PARAM => 'example.com', RoleResolver::AUTHOR_ONLY_PARAM => false]),
$buildInput(
[Role::DOMAIN_SPECIFIC->paramName() => 'example.com', Role::AUTHORED_SHORT_URLS->paramName() => false],
),
[RoleDefinition::forDomain($domain)],
1,
];
yield 'false domain role' => [
$buildInput([RoleResolver::DOMAIN_ONLY_PARAM => false]),
$buildInput([Role::DOMAIN_SPECIFIC->paramName() => false]),
[],
0,
];
yield 'true domain role' => [
$buildInput([RoleResolver::DOMAIN_ONLY_PARAM => true]),
$buildInput([Role::DOMAIN_SPECIFIC->paramName() => true]),
[],
0,
];
yield 'string array domain role' => [
$buildInput([RoleResolver::DOMAIN_ONLY_PARAM => ['foo', 'bar']]),
$buildInput([Role::DOMAIN_SPECIFIC->paramName() => ['foo', 'bar']]),
[],
0,
];
yield 'author role only' => [
$buildInput([RoleResolver::DOMAIN_ONLY_PARAM => null, RoleResolver::AUTHOR_ONLY_PARAM => true]),
$buildInput([Role::DOMAIN_SPECIFIC->paramName() => null, Role::AUTHORED_SHORT_URLS->paramName() => true]),
[RoleDefinition::forAuthoredShortUrls()],
0,
];
yield 'both roles' => [
$buildInput([RoleResolver::DOMAIN_ONLY_PARAM => 'example.com', RoleResolver::AUTHOR_ONLY_PARAM => true]),
$buildInput(
[Role::DOMAIN_SPECIFIC->paramName() => 'example.com', Role::AUTHORED_SHORT_URLS->paramName() => true],
),
[RoleDefinition::forAuthoredShortUrls(), RoleDefinition::forDomain($domain)],
1,
];
@@ -99,12 +101,22 @@ class RoleResolverTest extends TestCase
/** @test */
public function exceptionIsThrownWhenTryingToAddDomainOnlyLinkedToDefaultDomain(): void
{
$input = $this->prophesize(InputInterface::class);
$input->getOption(RoleResolver::DOMAIN_ONLY_PARAM)->willReturn('default.com');
$input->getOption(RoleResolver::AUTHOR_ONLY_PARAM)->willReturn(null);
$input = $this->createStub(InputInterface::class);
$input
->method('getOption')
->willReturnMap([
[Role::DOMAIN_SPECIFIC->paramName(), 'default.com'],
[Role::AUTHORED_SHORT_URLS->paramName(), null],
]);
$this->expectException(InvalidRoleConfigException::class);
$this->resolver->determineRoles($input->reveal());
$this->resolver->determineRoles($input);
}
private function domainWithId(Domain $domain): Domain
{
$domain->setId('1');
return $domain;
}
}

View File

@@ -4,9 +4,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputDefinition;
@@ -14,21 +13,14 @@ use Symfony\Component\Console\Tester\CommandTester;
trait CliTestUtilsTrait
{
use ProphecyTrait;
/**
* @return ObjectProphecy|Command
*/
private function createCommandMock(string $name): ObjectProphecy
private function createCommandMock(string $name): MockObject & Command
{
$command = $this->prophesize(Command::class);
$command->getName()->willReturn($name);
$command->getDefinition()->willReturn($name);
$command->isEnabled()->willReturn(true);
$command->getAliases()->willReturn([]);
$command->getDefinition()->willReturn(new InputDefinition());
$command->setApplication(Argument::type(Application::class))->willReturn(function (): void {
});
$command = $this->createMock(Command::class);
$command->method('getName')->willReturn($name);
$command->method('isEnabled')->willReturn(true);
$command->method('getAliases')->willReturn([]);
$command->method('getDefinition')->willReturn(new InputDefinition());
$command->method('setApplication')->with(Assert::isInstanceOf(Application::class));
return $command;
}

View File

@@ -4,8 +4,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Api;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Api\DisableKeyCommand;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
@@ -17,19 +17,19 @@ class DisableKeyCommandTest extends TestCase
use CliTestUtilsTrait;
private CommandTester $commandTester;
private ObjectProphecy $apiKeyService;
private MockObject & ApiKeyServiceInterface $apiKeyService;
protected function setUp(): void
{
$this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
$this->commandTester = $this->testerForCommand(new DisableKeyCommand($this->apiKeyService->reveal()));
$this->apiKeyService = $this->createMock(ApiKeyServiceInterface::class);
$this->commandTester = $this->testerForCommand(new DisableKeyCommand($this->apiKeyService));
}
/** @test */
public function providedApiKeyIsDisabled(): void
{
$apiKey = 'abcd1234';
$this->apiKeyService->disable($apiKey)->shouldBeCalledOnce();
$this->apiKeyService->expects($this->once())->method('disable')->with($apiKey);
$this->commandTester->execute([
'apiKey' => $apiKey,
@@ -44,7 +44,9 @@ class DisableKeyCommandTest extends TestCase
{
$apiKey = 'abcd1234';
$expectedMessage = 'API key "abcd1234" does not exist.';
$disable = $this->apiKeyService->disable($apiKey)->willThrow(new InvalidArgumentException($expectedMessage));
$this->apiKeyService->expects($this->once())->method('disable')->with($apiKey)->willThrowException(
new InvalidArgumentException($expectedMessage),
);
$this->commandTester->execute([
'apiKey' => $apiKey,
@@ -52,6 +54,5 @@ class DisableKeyCommandTest extends TestCase
$output = $this->commandTester->getDisplay();
self::assertStringContainsString($expectedMessage, $output);
$disable->shouldHaveBeenCalledOnce();
}
}

View File

@@ -5,9 +5,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Api;
use Cake\Chronos\Chronos;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\ApiKey\RoleResolverInterface;
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
@@ -21,22 +20,25 @@ class GenerateKeyCommandTest extends TestCase
use CliTestUtilsTrait;
private CommandTester $commandTester;
private ObjectProphecy $apiKeyService;
private MockObject & ApiKeyServiceInterface $apiKeyService;
protected function setUp(): void
{
$this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
$roleResolver = $this->prophesize(RoleResolverInterface::class);
$roleResolver->determineRoles(Argument::type(InputInterface::class))->willReturn([]);
$this->apiKeyService = $this->createMock(ApiKeyServiceInterface::class);
$roleResolver = $this->createMock(RoleResolverInterface::class);
$roleResolver->method('determineRoles')->with($this->isInstanceOf(InputInterface::class))->willReturn([]);
$command = new GenerateKeyCommand($this->apiKeyService->reveal(), $roleResolver->reveal());
$command = new GenerateKeyCommand($this->apiKeyService, $roleResolver);
$this->commandTester = $this->testerForCommand($command);
}
/** @test */
public function noExpirationDateIsDefinedIfNotProvided(): void
{
$this->apiKeyService->create(null, null)->shouldBeCalledOnce()->willReturn(ApiKey::create());
$this->apiKeyService->expects($this->once())->method('create')->with(
$this->isNull(),
$this->isNull(),
)->willReturn(ApiKey::create());
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
@@ -47,9 +49,10 @@ class GenerateKeyCommandTest extends TestCase
/** @test */
public function expirationDateIsDefinedIfProvided(): void
{
$this->apiKeyService->create(Argument::type(Chronos::class), null)->shouldBeCalledOnce()->willReturn(
ApiKey::create(),
);
$this->apiKeyService->expects($this->once())->method('create')->with(
$this->isInstanceOf(Chronos::class),
$this->isNull(),
)->willReturn(ApiKey::create());
$this->commandTester->execute([
'--expiration-date' => '2016-01-01',
@@ -59,9 +62,10 @@ class GenerateKeyCommandTest extends TestCase
/** @test */
public function nameIsDefinedIfProvided(): void
{
$this->apiKeyService->create(null, Argument::type('string'))->shouldBeCalledOnce()->willReturn(
ApiKey::create(),
);
$this->apiKeyService->expects($this->once())->method('create')->with(
$this->isNull(),
$this->isType('string'),
)->willReturn(ApiKey::create());
$this->commandTester->execute([
'--name' => 'Alice',

View File

@@ -5,10 +5,10 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Api;
use Cake\Chronos\Chronos;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
use Shlinkio\Shlink\Core\Entity\Domain;
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
@@ -21,12 +21,12 @@ class ListKeysCommandTest extends TestCase
use CliTestUtilsTrait;
private CommandTester $commandTester;
private ObjectProphecy $apiKeyService;
private MockObject & ApiKeyServiceInterface $apiKeyService;
protected function setUp(): void
{
$this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
$this->commandTester = $this->testerForCommand(new ListKeysCommand($this->apiKeyService->reveal()));
$this->apiKeyService = $this->createMock(ApiKeyServiceInterface::class);
$this->commandTester = $this->testerForCommand(new ListKeysCommand($this->apiKeyService));
}
/**
@@ -35,13 +35,12 @@ class ListKeysCommandTest extends TestCase
*/
public function returnsExpectedOutput(array $keys, bool $enabledOnly, string $expected): void
{
$listKeys = $this->apiKeyService->listKeys($enabledOnly)->willReturn($keys);
$this->apiKeyService->expects($this->once())->method('listKeys')->with($enabledOnly)->willReturn($keys);
$this->commandTester->execute(['--enabled-only' => $enabledOnly]);
$output = $this->commandTester->getDisplay();
self::assertEquals($expected, $output);
$listKeys->shouldHaveBeenCalledOnce();
}
public function provideKeysAndOutputs(): iterable
@@ -87,12 +86,12 @@ class ListKeysCommandTest extends TestCase
$apiKey1 = ApiKey::create(),
$apiKey2 = $this->apiKeyWithRoles([RoleDefinition::forAuthoredShortUrls()]),
$apiKey3 = $this->apiKeyWithRoles(
[RoleDefinition::forDomain(Domain::withAuthority('example.com')->setId('1'))],
[RoleDefinition::forDomain($this->domainWithId(Domain::withAuthority('example.com')))],
),
$apiKey4 = ApiKey::create(),
$apiKey5 = $this->apiKeyWithRoles([
RoleDefinition::forAuthoredShortUrls(),
RoleDefinition::forDomain(Domain::withAuthority('example.com')->setId('1')),
RoleDefinition::forDomain($this->domainWithId(Domain::withAuthority('example.com'))),
]),
$apiKey6 = ApiKey::create(),
],
@@ -151,4 +150,10 @@ class ListKeysCommandTest extends TestCase
return $apiKey;
}
private function domainWithId(Domain $domain): Domain
{
$domain->setId('1');
return $domain;
}
}

View File

@@ -9,9 +9,8 @@ use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\SqlitePlatform;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Db\CreateDatabaseCommand;
use Shlinkio\Shlink\CLI\Util\ProcessRunnerInterface;
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
@@ -28,40 +27,37 @@ class CreateDatabaseCommandTest extends TestCase
use CliTestUtilsTrait;
private CommandTester $commandTester;
private ObjectProphecy $processHelper;
private ObjectProphecy $regularConn;
private ObjectProphecy $schemaManager;
private ObjectProphecy $driver;
private MockObject & ProcessRunnerInterface $processHelper;
private MockObject & Connection $regularConn;
private MockObject & AbstractSchemaManager $schemaManager;
private MockObject & Driver $driver;
protected function setUp(): void
{
$locker = $this->prophesize(LockFactory::class);
$lock = $this->prophesize(LockInterface::class);
$lock->acquire(Argument::any())->willReturn(true);
$lock->release()->will(function (): void {
});
$locker->createLock(Argument::cetera())->willReturn($lock->reveal());
$locker = $this->createMock(LockFactory::class);
$lock = $this->createMock(LockInterface::class);
$lock->method('acquire')->withAnyParameters()->willReturn(true);
$locker->method('createLock')->withAnyParameters()->willReturn($lock);
$phpExecutableFinder = $this->prophesize(PhpExecutableFinder::class);
$phpExecutableFinder->find(false)->willReturn('/usr/local/bin/php');
$phpExecutableFinder = $this->createMock(PhpExecutableFinder::class);
$phpExecutableFinder->method('find')->with($this->isFalse())->willReturn('/usr/local/bin/php');
$this->processHelper = $this->prophesize(ProcessRunnerInterface::class);
$this->schemaManager = $this->prophesize(AbstractSchemaManager::class);
$this->processHelper = $this->createMock(ProcessRunnerInterface::class);
$this->schemaManager = $this->createMock(AbstractSchemaManager::class);
$this->regularConn = $this->prophesize(Connection::class);
$this->regularConn->createSchemaManager()->willReturn($this->schemaManager->reveal());
$this->driver = $this->prophesize(Driver::class);
$this->regularConn->getDriver()->willReturn($this->driver->reveal());
$this->driver->getDatabasePlatform()->willReturn($this->prophesize(AbstractPlatform::class)->reveal());
$noDbNameConn = $this->prophesize(Connection::class);
$noDbNameConn->createSchemaManager()->willReturn($this->schemaManager->reveal());
$this->regularConn = $this->createMock(Connection::class);
$this->regularConn->method('createSchemaManager')->willReturn($this->schemaManager);
$this->driver = $this->createMock(Driver::class);
$this->regularConn->method('getDriver')->willReturn($this->driver);
$noDbNameConn = $this->createMock(Connection::class);
$noDbNameConn->method('createSchemaManager')->withAnyParameters()->willReturn($this->schemaManager);
$command = new CreateDatabaseCommand(
$locker->reveal(),
$this->processHelper->reveal(),
$phpExecutableFinder->reveal(),
$this->regularConn->reveal(),
$noDbNameConn->reveal(),
$locker,
$this->processHelper,
$phpExecutableFinder,
$this->regularConn,
$noDbNameConn,
);
$this->commandTester = $this->testerForCommand($command);
@@ -71,38 +67,33 @@ class CreateDatabaseCommandTest extends TestCase
public function successMessageIsPrintedIfDatabaseAlreadyExists(): void
{
$shlinkDatabase = 'shlink_database';
$getDatabase = $this->regularConn->getParams()->willReturn(['dbname' => $shlinkDatabase]);
$listDatabases = $this->schemaManager->listDatabases()->willReturn(['foo', $shlinkDatabase, 'bar']);
$createDatabase = $this->schemaManager->createDatabase($shlinkDatabase)->will(function (): void {
});
$listTables = $this->schemaManager->listTableNames()->willReturn(['foo_table', 'bar_table']);
$this->regularConn->expects($this->once())->method('getParams')->willReturn(['dbname' => $shlinkDatabase]);
$this->schemaManager->expects($this->once())->method('listDatabases')->willReturn(
['foo', $shlinkDatabase, 'bar'],
);
$this->schemaManager->expects($this->never())->method('createDatabase');
$this->schemaManager->expects($this->once())->method('listTableNames')->willReturn(['foo_table', 'bar_table']);
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(AbstractPlatform::class));
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
self::assertStringContainsString('Database already exists. Run "db:migrate" command', $output);
$getDatabase->shouldHaveBeenCalledOnce();
$listDatabases->shouldHaveBeenCalledOnce();
$createDatabase->shouldNotHaveBeenCalled();
$listTables->shouldHaveBeenCalledOnce();
}
/** @test */
public function databaseIsCreatedIfItDoesNotExist(): void
{
$shlinkDatabase = 'shlink_database';
$getDatabase = $this->regularConn->getParams()->willReturn(['dbname' => $shlinkDatabase]);
$listDatabases = $this->schemaManager->listDatabases()->willReturn(['foo', 'bar']);
$createDatabase = $this->schemaManager->createDatabase($shlinkDatabase)->will(function (): void {
});
$listTables = $this->schemaManager->listTableNames()->willReturn(['foo_table', 'bar_table', MIGRATIONS_TABLE]);
$this->regularConn->expects($this->once())->method('getParams')->willReturn(['dbname' => $shlinkDatabase]);
$this->schemaManager->expects($this->once())->method('listDatabases')->willReturn(['foo', 'bar']);
$this->schemaManager->expects($this->once())->method('createDatabase')->with($shlinkDatabase);
$this->schemaManager->expects($this->once())->method('listTableNames')->willReturn(
['foo_table', 'bar_table', MIGRATIONS_TABLE],
);
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(AbstractPlatform::class));
$this->commandTester->execute([]);
$getDatabase->shouldHaveBeenCalledOnce();
$listDatabases->shouldHaveBeenCalledOnce();
$createDatabase->shouldHaveBeenCalledOnce();
$listTables->shouldHaveBeenCalledOnce();
}
/**
@@ -112,28 +103,25 @@ class CreateDatabaseCommandTest extends TestCase
public function tablesAreCreatedIfDatabaseIsEmpty(array $tables): void
{
$shlinkDatabase = 'shlink_database';
$getDatabase = $this->regularConn->getParams()->willReturn(['dbname' => $shlinkDatabase]);
$listDatabases = $this->schemaManager->listDatabases()->willReturn(['foo', $shlinkDatabase, 'bar']);
$createDatabase = $this->schemaManager->createDatabase($shlinkDatabase)->will(function (): void {
});
$listTables = $this->schemaManager->listTableNames()->willReturn($tables);
$runCommand = $this->processHelper->run(Argument::type(OutputInterface::class), [
$this->regularConn->expects($this->once())->method('getParams')->willReturn(['dbname' => $shlinkDatabase]);
$this->schemaManager->expects($this->once())->method('listDatabases')->willReturn(
['foo', $shlinkDatabase, 'bar'],
);
$this->schemaManager->expects($this->never())->method('createDatabase');
$this->schemaManager->expects($this->once())->method('listTableNames')->willReturn($tables);
$this->processHelper->expects($this->once())->method('run')->with($this->isInstanceOf(OutputInterface::class), [
'/usr/local/bin/php',
CreateDatabaseCommand::DOCTRINE_SCRIPT,
CreateDatabaseCommand::DOCTRINE_CREATE_SCHEMA_COMMAND,
'--no-interaction',
]);
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(AbstractPlatform::class));
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
self::assertStringContainsString('Creating database tables...', $output);
self::assertStringContainsString('Database properly created!', $output);
$getDatabase->shouldHaveBeenCalledOnce();
$listDatabases->shouldHaveBeenCalledOnce();
$createDatabase->shouldNotHaveBeenCalled();
$listTables->shouldHaveBeenCalledOnce();
$runCommand->shouldHaveBeenCalledOnce();
}
public function provideEmptyDatabase(): iterable
@@ -145,20 +133,13 @@ class CreateDatabaseCommandTest extends TestCase
/** @test */
public function databaseCheckIsSkippedForSqlite(): void
{
$this->driver->getDatabasePlatform()->willReturn($this->prophesize(SqlitePlatform::class)->reveal());
$this->driver->method('getDatabasePlatform')->willReturn($this->createMock(SqlitePlatform::class));
$shlinkDatabase = 'shlink_database';
$getDatabase = $this->regularConn->getParams()->willReturn(['dbname' => $shlinkDatabase]);
$listDatabases = $this->schemaManager->listDatabases()->willReturn(['foo', 'bar']);
$createDatabase = $this->schemaManager->createDatabase($shlinkDatabase)->will(function (): void {
});
$listTables = $this->schemaManager->listTableNames()->willReturn(['foo_table', 'bar_table']);
$this->regularConn->expects($this->never())->method('getParams');
$this->schemaManager->expects($this->never())->method('listDatabases');
$this->schemaManager->expects($this->never())->method('createDatabase');
$this->schemaManager->expects($this->once())->method('listTableNames')->willReturn(['foo_table', 'bar_table']);
$this->commandTester->execute([]);
$getDatabase->shouldNotHaveBeenCalled();
$listDatabases->shouldNotHaveBeenCalled();
$createDatabase->shouldNotHaveBeenCalled();
$listTables->shouldHaveBeenCalledOnce();
}
}

View File

@@ -4,9 +4,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Db;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Db\MigrateDatabaseCommand;
use Shlinkio\Shlink\CLI\Util\ProcessRunnerInterface;
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
@@ -21,34 +20,28 @@ class MigrateDatabaseCommandTest extends TestCase
use CliTestUtilsTrait;
private CommandTester $commandTester;
private ObjectProphecy $processHelper;
private MockObject & ProcessRunnerInterface $processHelper;
protected function setUp(): void
{
$locker = $this->prophesize(LockFactory::class);
$lock = $this->prophesize(LockInterface::class);
$lock->acquire(Argument::any())->willReturn(true);
$lock->release()->will(function (): void {
});
$locker->createLock(Argument::cetera())->willReturn($lock->reveal());
$locker = $this->createMock(LockFactory::class);
$lock = $this->createMock(LockInterface::class);
$lock->method('acquire')->withAnyParameters()->willReturn(true);
$locker->method('createLock')->withAnyParameters()->willReturn($lock);
$phpExecutableFinder = $this->prophesize(PhpExecutableFinder::class);
$phpExecutableFinder->find(false)->willReturn('/usr/local/bin/php');
$phpExecutableFinder = $this->createMock(PhpExecutableFinder::class);
$phpExecutableFinder->method('find')->with($this->isFalse())->willReturn('/usr/local/bin/php');
$this->processHelper = $this->prophesize(ProcessRunnerInterface::class);
$this->processHelper = $this->createMock(ProcessRunnerInterface::class);
$command = new MigrateDatabaseCommand(
$locker->reveal(),
$this->processHelper->reveal(),
$phpExecutableFinder->reveal(),
);
$command = new MigrateDatabaseCommand($locker, $this->processHelper, $phpExecutableFinder);
$this->commandTester = $this->testerForCommand($command);
}
/** @test */
public function migrationsCommandIsRunWithProperVerbosity(): void
{
$runCommand = $this->processHelper->run(Argument::type(OutputInterface::class), [
$this->processHelper->expects($this->once())->method('run')->with($this->isInstanceOf(OutputInterface::class), [
'/usr/local/bin/php',
MigrateDatabaseCommand::DOCTRINE_MIGRATIONS_SCRIPT,
MigrateDatabaseCommand::DOCTRINE_MIGRATE_COMMAND,
@@ -60,6 +53,5 @@ class MigrateDatabaseCommandTest extends TestCase
self::assertStringContainsString('Migrating database...', $output);
self::assertStringContainsString('Database properly migrated!', $output);
$runCommand->shouldHaveBeenCalledOnce();
}
}

View File

@@ -4,13 +4,13 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Domain;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Domain\DomainRedirectsCommand;
use Shlinkio\Shlink\Core\Config\NotFoundRedirects;
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
use Shlinkio\Shlink\Core\Domain\Model\DomainItem;
use Shlinkio\Shlink\Core\Entity\Domain;
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
use Symfony\Component\Console\Tester\CommandTester;
@@ -22,12 +22,12 @@ class DomainRedirectsCommandTest extends TestCase
use CliTestUtilsTrait;
private CommandTester $commandTester;
private ObjectProphecy $domainService;
private MockObject & DomainServiceInterface $domainService;
protected function setUp(): void
{
$this->domainService = $this->prophesize(DomainServiceInterface::class);
$this->commandTester = $this->testerForCommand(new DomainRedirectsCommand($this->domainService->reveal()));
$this->domainService = $this->createMock(DomainServiceInterface::class);
$this->commandTester = $this->testerForCommand(new DomainRedirectsCommand($this->domainService));
}
/**
@@ -37,11 +37,14 @@ class DomainRedirectsCommandTest extends TestCase
public function onlyPlainQuestionsAreAskedForNewDomainsAndDomainsWithNoRedirects(?Domain $domain): void
{
$domainAuthority = 'my-domain.com';
$findDomain = $this->domainService->findByAuthority($domainAuthority)->willReturn($domain);
$configureRedirects = $this->domainService->configureNotFoundRedirects(
$this->domainService->expects($this->once())->method('findByAuthority')->with($domainAuthority)->willReturn(
$domain,
);
$this->domainService->expects($this->once())->method('configureNotFoundRedirects')->with(
$domainAuthority,
NotFoundRedirects::withRedirects('foo.com', null, 'baz.com'),
)->willReturn(Domain::withAuthority(''));
$this->domainService->expects($this->never())->method('listDomains');
$this->commandTester->setInputs(['foo.com', '', 'baz.com']);
$this->commandTester->execute(['domain' => $domainAuthority]);
@@ -55,9 +58,6 @@ class DomainRedirectsCommandTest extends TestCase
);
self::assertStringContainsString('URL to redirect to when a user hits an invalid short URL', $output);
self::assertEquals(3, substr_count($output, '(Leave empty for no redirect)'));
$findDomain->shouldHaveBeenCalledOnce();
$configureRedirects->shouldHaveBeenCalledOnce();
$this->domainService->listDomains()->shouldNotHaveBeenCalled();
}
public function provideDomains(): iterable
@@ -73,11 +73,14 @@ class DomainRedirectsCommandTest extends TestCase
$domain = Domain::withAuthority($domainAuthority);
$domain->configureNotFoundRedirects(NotFoundRedirects::withRedirects('foo.com', 'bar.com', 'baz.com'));
$findDomain = $this->domainService->findByAuthority($domainAuthority)->willReturn($domain);
$configureRedirects = $this->domainService->configureNotFoundRedirects(
$this->domainService->expects($this->once())->method('findByAuthority')->with($domainAuthority)->willReturn(
$domain,
);
$this->domainService->expects($this->once())->method('configureNotFoundRedirects')->with(
$domainAuthority,
NotFoundRedirects::withRedirects(null, 'edited.com', 'baz.com'),
)->willReturn($domain);
$this->domainService->expects($this->never())->method('listDomains');
$this->commandTester->setInputs(['2', '1', 'edited.com', '0']);
$this->commandTester->execute(['domain' => $domainAuthority]);
@@ -90,9 +93,6 @@ class DomainRedirectsCommandTest extends TestCase
self::assertStringNotContainsStringIgnoringCase('(Leave empty for no redirect)', $output);
self::assertEquals(3, substr_count($output, 'Set new redirect URL'));
self::assertEquals(3, substr_count($output, 'Remove redirect'));
$findDomain->shouldHaveBeenCalledOnce();
$configureRedirects->shouldHaveBeenCalledOnce();
$this->domainService->listDomains()->shouldNotHaveBeenCalled();
}
/** @test */
@@ -101,9 +101,11 @@ class DomainRedirectsCommandTest extends TestCase
$domainAuthority = 'example.com';
$domain = Domain::withAuthority($domainAuthority);
$listDomains = $this->domainService->listDomains()->willReturn([]);
$findDomain = $this->domainService->findByAuthority($domainAuthority)->willReturn($domain);
$configureRedirects = $this->domainService->configureNotFoundRedirects(
$this->domainService->expects($this->once())->method('listDomains')->with()->willReturn([]);
$this->domainService->expects($this->once())->method('findByAuthority')->with($domainAuthority)->willReturn(
$domain,
);
$this->domainService->expects($this->once())->method('configureNotFoundRedirects')->with(
$domainAuthority,
NotFoundRedirects::withoutRedirects(),
)->willReturn($domain);
@@ -113,9 +115,6 @@ class DomainRedirectsCommandTest extends TestCase
$output = $this->commandTester->getDisplay();
self::assertStringContainsString('Domain authority for which you want to set specific redirects', $output);
$listDomains->shouldHaveBeenCalledOnce();
$findDomain->shouldHaveBeenCalledOnce();
$configureRedirects->shouldHaveBeenCalledOnce();
}
/** @test */
@@ -124,13 +123,15 @@ class DomainRedirectsCommandTest extends TestCase
$domainAuthority = 'existing-two.com';
$domain = Domain::withAuthority($domainAuthority);
$listDomains = $this->domainService->listDomains()->willReturn([
$this->domainService->expects($this->once())->method('listDomains')->with()->willReturn([
DomainItem::forDefaultDomain('default-domain.com', new NotFoundRedirectOptions()),
DomainItem::forNonDefaultDomain(Domain::withAuthority('existing-one.com')),
DomainItem::forNonDefaultDomain(Domain::withAuthority($domainAuthority)),
]);
$findDomain = $this->domainService->findByAuthority($domainAuthority)->willReturn($domain);
$configureRedirects = $this->domainService->configureNotFoundRedirects(
$this->domainService->expects($this->once())->method('findByAuthority')->with($domainAuthority)->willReturn(
$domain,
);
$this->domainService->expects($this->once())->method('configureNotFoundRedirects')->with(
$domainAuthority,
NotFoundRedirects::withoutRedirects(),
)->willReturn($domain);
@@ -143,9 +144,6 @@ class DomainRedirectsCommandTest extends TestCase
self::assertStringNotContainsString('default-domain.com', $output);
self::assertStringContainsString('existing-one.com', $output);
self::assertStringContainsString($domainAuthority, $output);
$listDomains->shouldHaveBeenCalledOnce();
$findDomain->shouldHaveBeenCalledOnce();
$configureRedirects->shouldHaveBeenCalledOnce();
}
/** @test */
@@ -154,13 +152,15 @@ class DomainRedirectsCommandTest extends TestCase
$domainAuthority = 'new-domain.com';
$domain = Domain::withAuthority($domainAuthority);
$listDomains = $this->domainService->listDomains()->willReturn([
$this->domainService->expects($this->once())->method('listDomains')->with()->willReturn([
DomainItem::forDefaultDomain('default-domain.com', new NotFoundRedirectOptions()),
DomainItem::forNonDefaultDomain(Domain::withAuthority('existing-one.com')),
DomainItem::forNonDefaultDomain(Domain::withAuthority('existing-two.com')),
]);
$findDomain = $this->domainService->findByAuthority($domainAuthority)->willReturn($domain);
$configureRedirects = $this->domainService->configureNotFoundRedirects(
$this->domainService->expects($this->once())->method('findByAuthority')->with($domainAuthority)->willReturn(
$domain,
);
$this->domainService->expects($this->once())->method('configureNotFoundRedirects')->with(
$domainAuthority,
NotFoundRedirects::withoutRedirects(),
)->willReturn($domain);
@@ -173,8 +173,5 @@ class DomainRedirectsCommandTest extends TestCase
self::assertStringNotContainsString('default-domain.com', $output);
self::assertStringContainsString('existing-one.com', $output);
self::assertStringContainsString('existing-two.com', $output);
$listDomains->shouldHaveBeenCalledOnce();
$findDomain->shouldHaveBeenCalledOnce();
$configureRedirects->shouldHaveBeenCalledOnce();
}
}

View File

@@ -5,16 +5,15 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Domain;
use Pagerfanta\Adapter\ArrayAdapter;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Domain\GetDomainVisitsCommand;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
use Shlinkio\Shlink\IpGeolocation\Model\Location;
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
@@ -25,16 +24,16 @@ class GetDomainVisitsCommandTest extends TestCase
use CliTestUtilsTrait;
private CommandTester $commandTester;
private ObjectProphecy $visitsHelper;
private ObjectProphecy $stringifier;
private MockObject & VisitsStatsHelperInterface $visitsHelper;
private MockObject & ShortUrlStringifierInterface $stringifier;
protected function setUp(): void
{
$this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class);
$this->stringifier = $this->prophesize(ShortUrlStringifierInterface::class);
$this->visitsHelper = $this->createMock(VisitsStatsHelperInterface::class);
$this->stringifier = $this->createMock(ShortUrlStringifierInterface::class);
$this->commandTester = $this->testerForCommand(
new GetDomainVisitsCommand($this->visitsHelper->reveal(), $this->stringifier->reveal()),
new GetDomainVisitsCommand($this->visitsHelper, $this->stringifier),
);
}
@@ -46,10 +45,13 @@ class GetDomainVisitsCommandTest extends TestCase
VisitLocation::fromGeolocation(new Location('', 'Spain', '', 'Madrid', 0, 0, '')),
);
$domain = 'doma.in';
$getVisits = $this->visitsHelper->visitsForDomain($domain, Argument::any())->willReturn(
new Paginator(new ArrayAdapter([$visit])),
$this->visitsHelper->expects($this->once())->method('visitsForDomain')->with(
$domain,
$this->anything(),
)->willReturn(new Paginator(new ArrayAdapter([$visit])));
$this->stringifier->expects($this->once())->method('stringify')->with($shortUrl)->willReturn(
'the_short_url',
);
$stringify = $this->stringifier->stringify($shortUrl)->willReturn('the_short_url');
$this->commandTester->execute(['domain' => $domain]);
$output = $this->commandTester->getDisplay();
@@ -65,7 +67,5 @@ class GetDomainVisitsCommandTest extends TestCase
OUTPUT,
$output,
);
$getVisits->shouldHaveBeenCalledOnce();
$stringify->shouldHaveBeenCalledOnce();
}
}

View File

@@ -4,14 +4,14 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Domain;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Domain\ListDomainsCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Config\NotFoundRedirects;
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
use Shlinkio\Shlink\Core\Domain\Model\DomainItem;
use Shlinkio\Shlink\Core\Entity\Domain;
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
use Symfony\Component\Console\Tester\CommandTester;
@@ -21,12 +21,12 @@ class ListDomainsCommandTest extends TestCase
use CliTestUtilsTrait;
private CommandTester $commandTester;
private ObjectProphecy $domainService;
private MockObject & DomainServiceInterface $domainService;
protected function setUp(): void
{
$this->domainService = $this->prophesize(DomainServiceInterface::class);
$this->commandTester = $this->testerForCommand(new ListDomainsCommand($this->domainService->reveal()));
$this->domainService = $this->createMock(DomainServiceInterface::class);
$this->commandTester = $this->testerForCommand(new ListDomainsCommand($this->domainService));
}
/**
@@ -42,7 +42,7 @@ class ListDomainsCommandTest extends TestCase
'https://foo.com/baz-domain/invalid',
));
$listDomains = $this->domainService->listDomains()->willReturn([
$this->domainService->expects($this->once())->method('listDomains')->with()->willReturn([
DomainItem::forDefaultDomain('foo.com', new NotFoundRedirectOptions(
invalidShortUrl: 'https://foo.com/default/invalid',
baseUrl: 'https://foo.com/default/base',
@@ -55,7 +55,6 @@ class ListDomainsCommandTest extends TestCase
self::assertEquals($expectedOutput, $this->commandTester->getDisplay());
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
$listDomains->shouldHaveBeenCalledOnce();
}
public function provideInputsAndOutputs(): iterable

View File

@@ -5,18 +5,17 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\ShortUrl\CreateShortUrlCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
use Shlinkio\Shlink\Core\ShortUrl\UrlShortenerInterface;
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
use Symfony\Component\Console\Tester\CommandTester;
@@ -27,19 +26,21 @@ class CreateShortUrlCommandTest extends TestCase
private const DEFAULT_DOMAIN = 'default.com';
private CommandTester $commandTester;
private ObjectProphecy $urlShortener;
private ObjectProphecy $stringifier;
private MockObject & UrlShortenerInterface $urlShortener;
private MockObject & ShortUrlStringifierInterface $stringifier;
protected function setUp(): void
{
$this->urlShortener = $this->prophesize(UrlShortener::class);
$this->stringifier = $this->prophesize(ShortUrlStringifierInterface::class);
$this->stringifier->stringify(Argument::type(ShortUrl::class))->willReturn('');
$this->urlShortener = $this->createMock(UrlShortenerInterface::class);
$this->stringifier = $this->createMock(ShortUrlStringifierInterface::class);
$command = new CreateShortUrlCommand(
$this->urlShortener->reveal(),
$this->stringifier->reveal(),
new UrlShortenerOptions(domain: ['hostname' => self::DEFAULT_DOMAIN], defaultShortCodesLength: 5),
$this->urlShortener,
$this->stringifier,
new UrlShortenerOptions(
domain: ['hostname' => self::DEFAULT_DOMAIN, 'schema' => ''],
defaultShortCodesLength: 5,
),
);
$this->commandTester = $this->testerForCommand($command);
}
@@ -48,8 +49,10 @@ class CreateShortUrlCommandTest extends TestCase
public function properShortCodeIsCreatedIfLongUrlIsCorrect(): void
{
$shortUrl = ShortUrl::createEmpty();
$urlToShortCode = $this->urlShortener->shorten(Argument::cetera())->willReturn($shortUrl);
$stringify = $this->stringifier->stringify($shortUrl)->willReturn('stringified_short_url');
$this->urlShortener->expects($this->once())->method('shorten')->withAnyParameters()->willReturn($shortUrl);
$this->stringifier->expects($this->once())->method('stringify')->with($shortUrl)->willReturn(
'stringified_short_url',
);
$this->commandTester->execute([
'longUrl' => 'http://domain.com/foo/bar',
@@ -59,16 +62,16 @@ class CreateShortUrlCommandTest extends TestCase
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
self::assertStringContainsString('stringified_short_url', $output);
$urlToShortCode->shouldHaveBeenCalledOnce();
$stringify->shouldHaveBeenCalledOnce();
}
/** @test */
public function exceptionWhileParsingLongUrlOutputsError(): void
{
$url = 'http://domain.com/invalid';
$this->urlShortener->shorten(Argument::cetera())->willThrow(InvalidUrlException::fromUrl($url))
->shouldBeCalledOnce();
$this->urlShortener->expects($this->once())->method('shorten')->withAnyParameters()->willThrowException(
InvalidUrlException::fromUrl($url),
);
$this->stringifier->method('stringify')->with($this->isInstanceOf(ShortUrl::class))->willReturn('');
$this->commandTester->execute(['longUrl' => $url]);
$output = $this->commandTester->getDisplay();
@@ -80,30 +83,32 @@ class CreateShortUrlCommandTest extends TestCase
/** @test */
public function providingNonUniqueSlugOutputsError(): void
{
$urlToShortCode = $this->urlShortener->shorten(Argument::cetera())->willThrow(
$this->urlShortener->expects($this->once())->method('shorten')->withAnyParameters()->willThrowException(
NonUniqueSlugException::fromSlug('my-slug'),
);
$this->stringifier->method('stringify')->with($this->isInstanceOf(ShortUrl::class))->willReturn('');
$this->commandTester->execute(['longUrl' => 'http://domain.com/invalid', '--custom-slug' => 'my-slug']);
$output = $this->commandTester->getDisplay();
self::assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
self::assertStringContainsString('Provided slug "my-slug" is already in use', $output);
$urlToShortCode->shouldHaveBeenCalledOnce();
}
/** @test */
public function properlyProcessesProvidedTags(): void
{
$shortUrl = ShortUrl::createEmpty();
$urlToShortCode = $this->urlShortener->shorten(
Argument::that(function (ShortUrlMeta $meta) {
$this->urlShortener->expects($this->once())->method('shorten')->with(
$this->callback(function (ShortUrlCreation $meta) {
$tags = $meta->getTags();
Assert::assertEquals(['foo', 'bar', 'baz', 'boo', 'zar'], $tags);
return true;
}),
)->willReturn($shortUrl);
$stringify = $this->stringifier->stringify($shortUrl)->willReturn('stringified_short_url');
$this->stringifier->expects($this->once())->method('stringify')->with($shortUrl)->willReturn(
'stringified_short_url',
);
$this->commandTester->execute([
'longUrl' => 'http://domain.com/foo/bar',
@@ -113,8 +118,6 @@ class CreateShortUrlCommandTest extends TestCase
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
self::assertStringContainsString('stringified_short_url', $output);
$urlToShortCode->shouldHaveBeenCalledOnce();
$stringify->shouldHaveBeenCalledOnce();
}
/**
@@ -123,18 +126,18 @@ class CreateShortUrlCommandTest extends TestCase
*/
public function properlyProcessesProvidedDomain(array $input, ?string $expectedDomain): void
{
$shorten = $this->urlShortener->shorten(
Argument::that(function (ShortUrlMeta $meta) use ($expectedDomain) {
$this->urlShortener->expects($this->once())->method('shorten')->with(
$this->callback(function (ShortUrlCreation $meta) use ($expectedDomain) {
Assert::assertEquals($expectedDomain, $meta->getDomain());
return true;
}),
)->willReturn(ShortUrl::createEmpty());
$this->stringifier->method('stringify')->with($this->isInstanceOf(ShortUrl::class))->willReturn('');
$input['longUrl'] = 'http://domain.com/foo/bar';
$this->commandTester->execute($input);
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
$shorten->shouldHaveBeenCalledOnce();
}
public function provideDomains(): iterable
@@ -152,17 +155,16 @@ class CreateShortUrlCommandTest extends TestCase
public function urlValidationHasExpectedValueBasedOnProvidedFlags(array $options, ?bool $expectedValidateUrl): void
{
$shortUrl = ShortUrl::createEmpty();
$urlToShortCode = $this->urlShortener->shorten(
Argument::that(function (ShortUrlMeta $meta) use ($expectedValidateUrl) {
$this->urlShortener->expects($this->once())->method('shorten')->with(
$this->callback(function (ShortUrlCreation $meta) use ($expectedValidateUrl) {
Assert::assertEquals($expectedValidateUrl, $meta->doValidateUrl());
return $meta;
return true;
}),
)->willReturn($shortUrl);
$this->stringifier->method('stringify')->with($this->isInstanceOf(ShortUrl::class))->willReturn('');
$options['longUrl'] = 'http://domain.com/foo/bar';
$this->commandTester->execute($options);
$urlToShortCode->shouldHaveBeenCalledOnce();
}
public function provideFlags(): iterable

View File

@@ -4,17 +4,15 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\ShortUrl\DeleteShortUrlCommand;
use Shlinkio\Shlink\Core\Exception;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
use Shlinkio\Shlink\Core\ShortUrl\DeleteShortUrlServiceInterface;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
use Symfony\Component\Console\Tester\CommandTester;
use function array_pop;
use function sprintf;
use const PHP_EOL;
@@ -24,23 +22,22 @@ class DeleteShortUrlCommandTest extends TestCase
use CliTestUtilsTrait;
private CommandTester $commandTester;
private ObjectProphecy $service;
private MockObject & DeleteShortUrlServiceInterface $service;
protected function setUp(): void
{
$this->service = $this->prophesize(DeleteShortUrlServiceInterface::class);
$this->commandTester = $this->testerForCommand(new DeleteShortUrlCommand($this->service->reveal()));
$this->service = $this->createMock(DeleteShortUrlServiceInterface::class);
$this->commandTester = $this->testerForCommand(new DeleteShortUrlCommand($this->service));
}
/** @test */
public function successMessageIsPrintedIfUrlIsProperlyDeleted(): void
{
$shortCode = 'abc123';
$deleteByShortCode = $this->service->deleteByShortCode(
$this->service->expects($this->once())->method('deleteByShortCode')->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
false,
)->will(function (): void {
});
$this->isFalse(),
);
$this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay();
@@ -49,7 +46,6 @@ class DeleteShortUrlCommandTest extends TestCase
sprintf('Short URL with short code "%s" successfully deleted.', $shortCode),
$output,
);
$deleteByShortCode->shouldHaveBeenCalledOnce();
}
/** @test */
@@ -57,15 +53,15 @@ class DeleteShortUrlCommandTest extends TestCase
{
$shortCode = 'abc123';
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain($shortCode);
$deleteByShortCode = $this->service->deleteByShortCode($identifier, false)->willThrow(
Exception\ShortUrlNotFoundException::fromNotFound($identifier),
);
$this->service->expects($this->once())->method('deleteByShortCode')->with(
$identifier,
$this->isFalse(),
)->willThrowException(Exception\ShortUrlNotFoundException::fromNotFound($identifier));
$this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay();
self::assertStringContainsString(sprintf('No URL found with short code "%s"', $shortCode), $output);
$deleteByShortCode->shouldHaveBeenCalledOnce();
}
/**
@@ -79,18 +75,17 @@ class DeleteShortUrlCommandTest extends TestCase
): void {
$shortCode = 'abc123';
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain($shortCode);
$deleteByShortCode = $this->service->deleteByShortCode($identifier, Argument::type('bool'))->will(
function (array $args) use ($shortCode): void {
$ignoreThreshold = array_pop($args);
if (!$ignoreThreshold) {
throw Exception\DeleteShortUrlException::fromVisitsThreshold(
10,
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
);
}
},
);
$this->service->expects($this->exactly($expectedDeleteCalls))->method('deleteByShortCode')->with(
$identifier,
$this->isType('bool'),
)->willReturnCallback(function ($_, bool $ignoreThreshold) use ($shortCode): void {
if (!$ignoreThreshold) {
throw Exception\DeleteShortUrlException::fromVisitsThreshold(
10,
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
);
}
});
$this->commandTester->setInputs($retryAnswer);
$this->commandTester->execute(['shortCode' => $shortCode]);
@@ -101,7 +96,6 @@ class DeleteShortUrlCommandTest extends TestCase
$shortCode,
), $output);
self::assertStringContainsString($expectedMessage, $output);
$deleteByShortCode->shouldHaveBeenCalledTimes($expectedDeleteCalls);
}
public function provideRetryDeleteAnswers(): iterable
@@ -115,10 +109,10 @@ class DeleteShortUrlCommandTest extends TestCase
public function deleteIsNotRetriedWhenThresholdIsReachedAndQuestionIsDeclined(): void
{
$shortCode = 'abc123';
$deleteByShortCode = $this->service->deleteByShortCode(
$this->service->expects($this->once())->method('deleteByShortCode')->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
false,
)->willThrow(Exception\DeleteShortUrlException::fromVisitsThreshold(
$this->isFalse(),
)->willThrowException(Exception\DeleteShortUrlException::fromVisitsThreshold(
10,
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
));
@@ -132,6 +126,5 @@ class DeleteShortUrlCommandTest extends TestCase
$shortCode,
), $output);
self::assertStringContainsString('Short URL was not deleted.', $output);
$deleteByShortCode->shouldHaveBeenCalledOnce();
}
}

View File

@@ -6,18 +6,17 @@ namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
use Cake\Chronos\Chronos;
use Pagerfanta\Adapter\ArrayAdapter;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\ShortUrl\GetShortUrlVisitsCommand;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
use Shlinkio\Shlink\IpGeolocation\Model\Location;
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
@@ -31,12 +30,12 @@ class GetShortUrlVisitsCommandTest extends TestCase
use CliTestUtilsTrait;
private CommandTester $commandTester;
private ObjectProphecy $visitsHelper;
private MockObject & VisitsStatsHelperInterface $visitsHelper;
protected function setUp(): void
{
$this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class);
$command = new GetShortUrlVisitsCommand($this->visitsHelper->reveal());
$this->visitsHelper = $this->createMock(VisitsStatsHelperInterface::class);
$command = new GetShortUrlVisitsCommand($this->visitsHelper);
$this->commandTester = $this->testerForCommand($command);
}
@@ -44,12 +43,10 @@ class GetShortUrlVisitsCommandTest extends TestCase
public function noDateFlagsTriesToListWithoutDateRange(): void
{
$shortCode = 'abc123';
$this->visitsHelper->visitsForShortUrl(
$this->visitsHelper->expects($this->once())->method('visitsForShortUrl')->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
new VisitsParams(DateRange::allTime()),
)
->willReturn(new Paginator(new ArrayAdapter([])))
->shouldBeCalledOnce();
)->willReturn(new Paginator(new ArrayAdapter([])));
$this->commandTester->execute(['shortCode' => $shortCode]);
}
@@ -60,12 +57,10 @@ class GetShortUrlVisitsCommandTest extends TestCase
$shortCode = 'abc123';
$startDate = '2016-01-01';
$endDate = '2016-02-01';
$this->visitsHelper->visitsForShortUrl(
$this->visitsHelper->expects($this->once())->method('visitsForShortUrl')->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
new VisitsParams(buildDateRange(Chronos::parse($startDate), Chronos::parse($endDate))),
)
->willReturn(new Paginator(new ArrayAdapter([])))
->shouldBeCalledOnce();
)->willReturn(new Paginator(new ArrayAdapter([])));
$this->commandTester->execute([
'shortCode' => $shortCode,
@@ -79,7 +74,7 @@ class GetShortUrlVisitsCommandTest extends TestCase
{
$shortCode = 'abc123';
$startDate = 'foo';
$info = $this->visitsHelper->visitsForShortUrl(
$this->visitsHelper->expects($this->once())->method('visitsForShortUrl')->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
new VisitsParams(DateRange::allTime()),
)->willReturn(new Paginator(new ArrayAdapter([])));
@@ -90,7 +85,6 @@ class GetShortUrlVisitsCommandTest extends TestCase
]);
$output = $this->commandTester->getDisplay();
$info->shouldHaveBeenCalledOnce();
self::assertStringContainsString(
sprintf('Ignored provided "start-date" since its value "%s" is not a valid date', $startDate),
$output,
@@ -104,12 +98,10 @@ class GetShortUrlVisitsCommandTest extends TestCase
VisitLocation::fromGeolocation(new Location('', 'Spain', '', 'Madrid', 0, 0, '')),
);
$shortCode = 'abc123';
$this->visitsHelper->visitsForShortUrl(
$this->visitsHelper->expects($this->once())->method('visitsForShortUrl')->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
Argument::any(),
)->willReturn(
new Paginator(new ArrayAdapter([$visit])),
)->shouldBeCalledOnce();
$this->anything(),
)->willReturn(new Paginator(new ArrayAdapter([$visit])));
$this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay();

View File

@@ -6,17 +6,16 @@ namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
use Cake\Chronos\Chronos;
use Pagerfanta\Adapter\ArrayAdapter;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlListServiceInterface;
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer;
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
@@ -31,12 +30,12 @@ class ListShortUrlsCommandTest extends TestCase
use CliTestUtilsTrait;
private CommandTester $commandTester;
private ObjectProphecy $shortUrlService;
private MockObject & ShortUrlListServiceInterface $shortUrlService;
protected function setUp(): void
{
$this->shortUrlService = $this->prophesize(ShortUrlServiceInterface::class);
$command = new ListShortUrlsCommand($this->shortUrlService->reveal(), new ShortUrlDataTransformer(
$this->shortUrlService = $this->createMock(ShortUrlListServiceInterface::class);
$command = new ListShortUrlsCommand($this->shortUrlService, new ShortUrlDataTransformer(
new ShortUrlStringifier([]),
));
$this->commandTester = $this->testerForCommand($command);
@@ -51,9 +50,8 @@ class ListShortUrlsCommandTest extends TestCase
$data[] = ShortUrl::withLongUrl('url_' . $i);
}
$this->shortUrlService->listShortUrls(Argument::cetera())
->will(fn () => new Paginator(new ArrayAdapter($data)))
->shouldBeCalledTimes(3);
$this->shortUrlService->expects($this->exactly(3))->method('listShortUrls')->withAnyParameters()
->willReturnCallback(fn () => new Paginator(new ArrayAdapter($data)));
$this->commandTester->setInputs(['y', 'y', 'n']);
$this->commandTester->execute([]);
@@ -74,9 +72,9 @@ class ListShortUrlsCommandTest extends TestCase
$data[] = ShortUrl::withLongUrl('url_' . $i);
}
$this->shortUrlService->listShortUrls(ShortUrlsParams::emptyInstance())
->willReturn(new Paginator(new ArrayAdapter($data)))
->shouldBeCalledOnce();
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(
ShortUrlsParams::emptyInstance(),
)->willReturn(new Paginator(new ArrayAdapter($data)));
$this->commandTester->setInputs(['n']);
$this->commandTester->execute([]);
@@ -95,9 +93,9 @@ class ListShortUrlsCommandTest extends TestCase
public function passingPageWillMakeListStartOnThatPage(): void
{
$page = 5;
$this->shortUrlService->listShortUrls(ShortUrlsParams::fromRawData(['page' => $page]))
->willReturn(new Paginator(new ArrayAdapter([])))
->shouldBeCalledOnce();
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(
ShortUrlsParams::fromRawData(['page' => $page]),
)->willReturn(new Paginator(new ArrayAdapter([])));
$this->commandTester->setInputs(['y']);
$this->commandTester->execute(['--page' => $page]);
@@ -113,15 +111,15 @@ class ListShortUrlsCommandTest extends TestCase
array $notExpectedContents,
ApiKey $apiKey,
): void {
$this->shortUrlService->listShortUrls(ShortUrlsParams::emptyInstance())
->willReturn(new Paginator(new ArrayAdapter([
ShortUrl::fromMeta(ShortUrlMeta::fromRawData([
'longUrl' => 'foo.com',
'tags' => ['foo', 'bar', 'baz'],
'apiKey' => $apiKey,
])),
])))
->shouldBeCalledOnce();
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(
ShortUrlsParams::emptyInstance(),
)->willReturn(new Paginator(new ArrayAdapter([
ShortUrl::create(ShortUrlCreation::fromRawData([
'longUrl' => 'foo.com',
'tags' => ['foo', 'bar', 'baz'],
'apiKey' => $apiKey,
])),
])));
$this->commandTester->setInputs(['y']);
$this->commandTester->execute($input);
@@ -189,7 +187,7 @@ class ListShortUrlsCommandTest extends TestCase
?string $startDate = null,
?string $endDate = null,
): void {
$listShortUrls = $this->shortUrlService->listShortUrls(ShortUrlsParams::fromRawData([
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(ShortUrlsParams::fromRawData([
'page' => $page,
'searchTerm' => $searchTerm,
'tags' => $tags,
@@ -200,8 +198,6 @@ class ListShortUrlsCommandTest extends TestCase
$this->commandTester->setInputs(['n']);
$this->commandTester->execute($commandArgs);
$listShortUrls->shouldHaveBeenCalledOnce();
}
public function provideArgs(): iterable
@@ -251,14 +247,12 @@ class ListShortUrlsCommandTest extends TestCase
*/
public function orderByIsProperlyComputed(array $commandArgs, ?string $expectedOrderBy): void
{
$listShortUrls = $this->shortUrlService->listShortUrls(ShortUrlsParams::fromRawData([
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(ShortUrlsParams::fromRawData([
'orderBy' => $expectedOrderBy,
]))->willReturn(new Paginator(new ArrayAdapter([])));
$this->commandTester->setInputs(['n']);
$this->commandTester->execute($commandArgs);
$listShortUrls->shouldHaveBeenCalledOnce();
}
public function provideOrderBy(): iterable
@@ -273,7 +267,7 @@ class ListShortUrlsCommandTest extends TestCase
/** @test */
public function requestingAllElementsWillSetItemsPerPage(): void
{
$listShortUrls = $this->shortUrlService->listShortUrls(ShortUrlsParams::fromRawData([
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(ShortUrlsParams::fromRawData([
'page' => 1,
'searchTerm' => null,
'tags' => [],
@@ -285,7 +279,5 @@ class ListShortUrlsCommandTest extends TestCase
]))->willReturn(new Paginator(new ArrayAdapter([])));
$this->commandTester->execute(['--all' => true]);
$listShortUrls->shouldHaveBeenCalledOnce();
}
}

View File

@@ -4,13 +4,13 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\ShortUrl\ResolveUrlCommand;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
use Symfony\Component\Console\Tester\CommandTester;
@@ -23,12 +23,12 @@ class ResolveUrlCommandTest extends TestCase
use CliTestUtilsTrait;
private CommandTester $commandTester;
private ObjectProphecy $urlResolver;
private MockObject & ShortUrlResolverInterface $urlResolver;
protected function setUp(): void
{
$this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class);
$this->commandTester = $this->testerForCommand(new ResolveUrlCommand($this->urlResolver->reveal()));
$this->urlResolver = $this->createMock(ShortUrlResolverInterface::class);
$this->commandTester = $this->testerForCommand(new ResolveUrlCommand($this->urlResolver));
}
/** @test */
@@ -37,9 +37,9 @@ class ResolveUrlCommandTest extends TestCase
$shortCode = 'abc123';
$expectedUrl = 'http://domain.com/foo/bar';
$shortUrl = ShortUrl::withLongUrl($expectedUrl);
$this->urlResolver->resolveShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode))->willReturn(
$shortUrl,
)->shouldBeCalledOnce();
$this->urlResolver->expects($this->once())->method('resolveShortUrl')->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
)->willReturn($shortUrl);
$this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay();
@@ -52,9 +52,9 @@ class ResolveUrlCommandTest extends TestCase
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain('abc123');
$shortCode = $identifier->shortCode;
$this->urlResolver->resolveShortUrl($identifier)
->willThrow(ShortUrlNotFoundException::fromNotFound($identifier))
->shouldBeCalledOnce();
$this->urlResolver->expects($this->once())->method('resolveShortUrl')->with($identifier)->willThrowException(
ShortUrlNotFoundException::fromNotFound($identifier),
);
$this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay();

View File

@@ -4,8 +4,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Tag\DeleteTagsCommand;
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
@@ -16,12 +16,12 @@ class DeleteTagsCommandTest extends TestCase
use CliTestUtilsTrait;
private CommandTester $commandTester;
private ObjectProphecy $tagService;
private MockObject & TagServiceInterface $tagService;
protected function setUp(): void
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
$this->commandTester = $this->testerForCommand(new DeleteTagsCommand($this->tagService->reveal()));
$this->tagService = $this->createMock(TagServiceInterface::class);
$this->commandTester = $this->testerForCommand(new DeleteTagsCommand($this->tagService));
}
/** @test */
@@ -37,8 +37,7 @@ class DeleteTagsCommandTest extends TestCase
public function serviceIsInvokedOnSuccess(): void
{
$tagNames = ['foo', 'bar'];
$deleteTags = $this->tagService->deleteTags($tagNames)->will(function (): void {
});
$this->tagService->expects($this->once())->method('deleteTags')->with($tagNames);
$this->commandTester->execute([
'--name' => $tagNames,
@@ -46,6 +45,5 @@ class DeleteTagsCommandTest extends TestCase
$output = $this->commandTester->getDisplay();
self::assertStringContainsString('Tags properly deleted', $output);
$deleteTags->shouldHaveBeenCalled();
}
}

View File

@@ -5,16 +5,15 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
use Pagerfanta\Adapter\ArrayAdapter;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Tag\GetTagVisitsCommand;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
use Shlinkio\Shlink\IpGeolocation\Model\Location;
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
@@ -25,16 +24,16 @@ class GetTagVisitsCommandTest extends TestCase
use CliTestUtilsTrait;
private CommandTester $commandTester;
private ObjectProphecy $visitsHelper;
private ObjectProphecy $stringifier;
private MockObject & VisitsStatsHelperInterface $visitsHelper;
private MockObject & ShortUrlStringifierInterface $stringifier;
protected function setUp(): void
{
$this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class);
$this->stringifier = $this->prophesize(ShortUrlStringifierInterface::class);
$this->visitsHelper = $this->createMock(VisitsStatsHelperInterface::class);
$this->stringifier = $this->createMock(ShortUrlStringifierInterface::class);
$this->commandTester = $this->testerForCommand(
new GetTagVisitsCommand($this->visitsHelper->reveal(), $this->stringifier->reveal()),
new GetTagVisitsCommand($this->visitsHelper, $this->stringifier),
);
}
@@ -46,10 +45,10 @@ class GetTagVisitsCommandTest extends TestCase
VisitLocation::fromGeolocation(new Location('', 'Spain', '', 'Madrid', 0, 0, '')),
);
$tag = 'abc123';
$getVisits = $this->visitsHelper->visitsForTag($tag, Argument::any())->willReturn(
$this->visitsHelper->expects($this->once())->method('visitsForTag')->with($tag, $this->anything())->willReturn(
new Paginator(new ArrayAdapter([$visit])),
);
$stringify = $this->stringifier->stringify($shortUrl)->willReturn('the_short_url');
$this->stringifier->expects($this->once())->method('stringify')->with($shortUrl)->willReturn('the_short_url');
$this->commandTester->execute(['tag' => $tag]);
$output = $this->commandTester->getDisplay();
@@ -65,7 +64,5 @@ class GetTagVisitsCommandTest extends TestCase
OUTPUT,
$output,
);
$getVisits->shouldHaveBeenCalledOnce();
$stringify->shouldHaveBeenCalledOnce();
}
}

View File

@@ -5,9 +5,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
use Pagerfanta\Adapter\ArrayAdapter;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Tag\ListTagsCommand;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
@@ -20,33 +19,36 @@ class ListTagsCommandTest extends TestCase
use CliTestUtilsTrait;
private CommandTester $commandTester;
private ObjectProphecy $tagService;
private MockObject & TagServiceInterface $tagService;
protected function setUp(): void
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
$this->commandTester = $this->testerForCommand(new ListTagsCommand($this->tagService->reveal()));
$this->tagService = $this->createMock(TagServiceInterface::class);
$this->commandTester = $this->testerForCommand(new ListTagsCommand($this->tagService));
}
/** @test */
public function noTagsPrintsEmptyMessage(): void
{
$tagsInfo = $this->tagService->tagsInfo(Argument::any())->willReturn(new Paginator(new ArrayAdapter([])));
$this->tagService->expects($this->once())->method('tagsInfo')->withAnyParameters()->willReturn(
new Paginator(new ArrayAdapter([])),
);
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
self::assertStringContainsString('No tags found', $output);
$tagsInfo->shouldHaveBeenCalled();
}
/** @test */
public function listOfTagsIsPrinted(): void
{
$tagsInfo = $this->tagService->tagsInfo(Argument::any())->willReturn(new Paginator(new ArrayAdapter([
new TagInfo('foo', 10, 2),
new TagInfo('bar', 7, 32),
])));
$this->tagService->expects($this->once())->method('tagsInfo')->withAnyParameters()->willReturn(
new Paginator(new ArrayAdapter([
new TagInfo('foo', 10, 2),
new TagInfo('bar', 7, 32),
])),
);
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
@@ -63,6 +65,5 @@ class ListTagsCommandTest extends TestCase
OUTPUT,
$output,
);
$tagsInfo->shouldHaveBeenCalled();
}
}

View File

@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Tag\RenameTagCommand;
use Shlinkio\Shlink\Core\Entity\Tag;
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
use Shlinkio\Shlink\Core\Tag\Entity\Tag;
use Shlinkio\Shlink\Core\Tag\Model\TagRenaming;
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
@@ -19,12 +19,12 @@ class RenameTagCommandTest extends TestCase
use CliTestUtilsTrait;
private CommandTester $commandTester;
private ObjectProphecy $tagService;
private MockObject & TagServiceInterface $tagService;
protected function setUp(): void
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
$this->commandTester = $this->testerForCommand(new RenameTagCommand($this->tagService->reveal()));
$this->tagService = $this->createMock(TagServiceInterface::class);
$this->commandTester = $this->testerForCommand(new RenameTagCommand($this->tagService));
}
/** @test */
@@ -32,9 +32,9 @@ class RenameTagCommandTest extends TestCase
{
$oldName = 'foo';
$newName = 'bar';
$renameTag = $this->tagService->renameTag(TagRenaming::fromNames($oldName, $newName))->willThrow(
TagNotFoundException::fromTag('foo'),
);
$this->tagService->expects($this->once())->method('renameTag')->with(
TagRenaming::fromNames($oldName, $newName),
)->willThrowException(TagNotFoundException::fromTag('foo'));
$this->commandTester->execute([
'oldName' => $oldName,
@@ -43,7 +43,6 @@ class RenameTagCommandTest extends TestCase
$output = $this->commandTester->getDisplay();
self::assertStringContainsString('Tag with name "foo" could not be found', $output);
$renameTag->shouldHaveBeenCalled();
}
/** @test */
@@ -51,9 +50,9 @@ class RenameTagCommandTest extends TestCase
{
$oldName = 'foo';
$newName = 'bar';
$renameTag = $this->tagService->renameTag(TagRenaming::fromNames($oldName, $newName))->willReturn(
new Tag($newName),
);
$this->tagService->expects($this->once())->method('renameTag')->with(
TagRenaming::fromNames($oldName, $newName),
)->willReturn(new Tag($newName));
$this->commandTester->execute([
'oldName' => $oldName,
@@ -62,6 +61,5 @@ class RenameTagCommandTest extends TestCase
$output = $this->commandTester->getDisplay();
self::assertStringContainsString('Tag properly renamed', $output);
$renameTag->shouldHaveBeenCalled();
}
}

View File

@@ -4,9 +4,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface;
@@ -22,12 +21,12 @@ class DownloadGeoLiteDbCommandTest extends TestCase
use CliTestUtilsTrait;
private CommandTester $commandTester;
private ObjectProphecy $dbUpdater;
private MockObject & GeolocationDbUpdaterInterface $dbUpdater;
protected function setUp(): void
{
$this->dbUpdater = $this->prophesize(GeolocationDbUpdaterInterface::class);
$this->commandTester = $this->testerForCommand(new DownloadGeoLiteDbCommand($this->dbUpdater->reveal()));
$this->dbUpdater = $this->createMock(GeolocationDbUpdaterInterface::class);
$this->commandTester = $this->testerForCommand(new DownloadGeoLiteDbCommand($this->dbUpdater));
}
/**
@@ -39,10 +38,8 @@ class DownloadGeoLiteDbCommandTest extends TestCase
string $expectedMessage,
int $expectedExitCode,
): void {
$checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will(
function (array $args) use ($olderDbExists): void {
[$beforeDownload, $handleProgress] = $args;
$this->dbUpdater->expects($this->once())->method('checkDbUpdate')->withAnyParameters()->willReturnCallback(
function (callable $beforeDownload, callable $handleProgress) use ($olderDbExists): void {
$beforeDownload($olderDbExists);
$handleProgress(100, 50);
@@ -62,7 +59,6 @@ class DownloadGeoLiteDbCommandTest extends TestCase
);
self::assertStringContainsString($expectedMessage, $output);
self::assertSame($expectedExitCode, $exitCode);
$checkDbUpdate->shouldHaveBeenCalledOnce();
}
public function provideFailureParams(): iterable
@@ -85,7 +81,9 @@ class DownloadGeoLiteDbCommandTest extends TestCase
*/
public function printsExpectedMessageWhenNoErrorOccurs(callable $checkUpdateBehavior, string $expectedMessage): void
{
$checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will($checkUpdateBehavior);
$this->dbUpdater->expects($this->once())->method('checkDbUpdate')->withAnyParameters()->willReturnCallback(
$checkUpdateBehavior,
);
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
@@ -93,16 +91,13 @@ class DownloadGeoLiteDbCommandTest extends TestCase
self::assertStringContainsString($expectedMessage, $output);
self::assertSame(ExitCodes::EXIT_SUCCESS, $exitCode);
$checkDbUpdate->shouldHaveBeenCalledOnce();
}
public function provideSuccessParams(): iterable
{
yield 'up to date db' => [fn () => GeolocationResult::CHECK_SKIPPED, '[INFO] GeoLite2 db file is up to date.'];
yield 'outdated db' => [function (array $args): GeolocationResult {
[$beforeDownload] = $args;
yield 'outdated db' => [function (callable $beforeDownload): GeolocationResult {
$beforeDownload(true);
return GeolocationResult::DB_CREATED;
}, '[OK] GeoLite2 db file properly downloaded.'];
}

View File

@@ -5,16 +5,15 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
use Pagerfanta\Adapter\ArrayAdapter;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Visit\GetNonOrphanVisitsCommand;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
use Shlinkio\Shlink\IpGeolocation\Model\Location;
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
@@ -25,16 +24,16 @@ class GetNonOrphanVisitsCommandTest extends TestCase
use CliTestUtilsTrait;
private CommandTester $commandTester;
private ObjectProphecy $visitsHelper;
private ObjectProphecy $stringifier;
private MockObject & VisitsStatsHelperInterface $visitsHelper;
private MockObject & ShortUrlStringifierInterface $stringifier;
protected function setUp(): void
{
$this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class);
$this->stringifier = $this->prophesize(ShortUrlStringifierInterface::class);
$this->visitsHelper = $this->createMock(VisitsStatsHelperInterface::class);
$this->stringifier = $this->createMock(ShortUrlStringifierInterface::class);
$this->commandTester = $this->testerForCommand(
new GetNonOrphanVisitsCommand($this->visitsHelper->reveal(), $this->stringifier->reveal()),
new GetNonOrphanVisitsCommand($this->visitsHelper, $this->stringifier),
);
}
@@ -45,10 +44,10 @@ class GetNonOrphanVisitsCommandTest extends TestCase
$visit = Visit::forValidShortUrl($shortUrl, new Visitor('bar', 'foo', '', ''))->locate(
VisitLocation::fromGeolocation(new Location('', 'Spain', '', 'Madrid', 0, 0, '')),
);
$getVisits = $this->visitsHelper->nonOrphanVisits(Argument::any())->willReturn(
$this->visitsHelper->expects($this->once())->method('nonOrphanVisits')->withAnyParameters()->willReturn(
new Paginator(new ArrayAdapter([$visit])),
);
$stringify = $this->stringifier->stringify($shortUrl)->willReturn('the_short_url');
$this->stringifier->expects($this->once())->method('stringify')->with($shortUrl)->willReturn('the_short_url');
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
@@ -64,7 +63,5 @@ class GetNonOrphanVisitsCommandTest extends TestCase
OUTPUT,
$output,
);
$getVisits->shouldHaveBeenCalledOnce();
$stringify->shouldHaveBeenCalledOnce();
}
}

View File

@@ -5,14 +5,13 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
use Pagerfanta\Adapter\ArrayAdapter;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Visit\GetOrphanVisitsCommand;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
use Shlinkio\Shlink\IpGeolocation\Model\Location;
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
@@ -23,12 +22,12 @@ class GetOrphanVisitsCommandTest extends TestCase
use CliTestUtilsTrait;
private CommandTester $commandTester;
private ObjectProphecy $visitsHelper;
private MockObject & VisitsStatsHelperInterface $visitsHelper;
protected function setUp(): void
{
$this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class);
$this->commandTester = $this->testerForCommand(new GetOrphanVisitsCommand($this->visitsHelper->reveal()));
$this->visitsHelper = $this->createMock(VisitsStatsHelperInterface::class);
$this->commandTester = $this->testerForCommand(new GetOrphanVisitsCommand($this->visitsHelper));
}
/** @test */
@@ -37,7 +36,7 @@ class GetOrphanVisitsCommandTest extends TestCase
$visit = Visit::forBasePath(new Visitor('bar', 'foo', '', ''))->locate(
VisitLocation::fromGeolocation(new Location('', 'Spain', '', 'Madrid', 0, 0, '')),
);
$getVisits = $this->visitsHelper->orphanVisits(Argument::any())->willReturn(
$this->visitsHelper->expects($this->once())->method('orphanVisits')->withAnyParameters()->willReturn(
new Paginator(new ArrayAdapter([$visit])),
);
@@ -55,6 +54,5 @@ class GetOrphanVisitsCommandTest extends TestCase
OUTPUT,
$output,
);
$getVisits->shouldHaveBeenCalledOnce();
}
}

View File

@@ -4,23 +4,23 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand;
use Shlinkio\Shlink\CLI\Command\Visit\LocateVisitsCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
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\Visit\VisitGeolocationHelperInterface;
use Shlinkio\Shlink\Core\Visit\VisitLocator;
use Shlinkio\Shlink\Core\Visit\VisitToLocationHelperInterface;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Visit\Geolocation\VisitGeolocationHelperInterface;
use Shlinkio\Shlink\Core\Visit\Geolocation\VisitLocatorInterface;
use Shlinkio\Shlink\Core\Visit\Geolocation\VisitToLocationHelperInterface;
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
use Shlinkio\Shlink\IpGeolocation\Model\Location;
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Tester\CommandTester;
@@ -35,33 +35,24 @@ class LocateVisitsCommandTest extends TestCase
use CliTestUtilsTrait;
private CommandTester $commandTester;
private ObjectProphecy $visitService;
private ObjectProphecy $visitToLocation;
private ObjectProphecy $lock;
private ObjectProphecy $downloadDbCommand;
private MockObject & VisitLocatorInterface $visitService;
private MockObject & VisitToLocationHelperInterface $visitToLocation;
private MockObject & Lock\LockInterface $lock;
private MockObject & Command $downloadDbCommand;
protected function setUp(): void
{
$this->visitService = $this->prophesize(VisitLocator::class);
$this->visitToLocation = $this->prophesize(VisitToLocationHelperInterface::class);
$this->visitService = $this->createMock(VisitLocatorInterface::class);
$this->visitToLocation = $this->createMock(VisitToLocationHelperInterface::class);
$locker = $this->prophesize(Lock\LockFactory::class);
$this->lock = $this->prophesize(Lock\LockInterface::class);
$this->lock->acquire(false)->willReturn(true);
$this->lock->release()->will(function (): void {
});
$locker->createLock(Argument::type('string'), 600.0, false)->willReturn($this->lock->reveal());
$locker = $this->createMock(Lock\LockFactory::class);
$this->lock = $this->createMock(Lock\LockInterface::class);
$locker->method('createLock')->with($this->isType('string'), 600.0, false)->willReturn($this->lock);
$command = new LocateVisitsCommand(
$this->visitService->reveal(),
$this->visitToLocation->reveal(),
$locker->reveal(),
);
$command = new LocateVisitsCommand($this->visitService, $this->visitToLocation, $locker);
$this->downloadDbCommand = $this->createCommandMock(DownloadGeoLiteDbCommand::NAME);
$this->downloadDbCommand->run(Argument::cetera())->willReturn(ExitCodes::EXIT_SUCCESS);
$this->commandTester = $this->testerForCommand($command, $this->downloadDbCommand->reveal());
$this->commandTester = $this->testerForCommand($command, $this->downloadDbCommand);
}
/**
@@ -79,14 +70,23 @@ class LocateVisitsCommandTest extends TestCase
$location = VisitLocation::fromGeolocation(Location::emptyInstance());
$mockMethodBehavior = $this->invokeHelperMethods($visit, $location);
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will($mockMethodBehavior);
$locateEmptyVisits = $this->visitService->locateVisitsWithEmptyLocation(Argument::cetera())->will(
$mockMethodBehavior,
);
$locateAllVisits = $this->visitService->locateAllVisits(Argument::cetera())->will($mockMethodBehavior);
$resolveIpLocation = $this->visitToLocation->resolveVisitLocation(Argument::any())->willReturn(
Location::emptyInstance(),
);
$this->lock->method('acquire')->with($this->isFalse())->willReturn(true);
$this->visitService->expects($this->exactly($expectedUnlocatedCalls))
->method('locateUnlocatedVisits')
->withAnyParameters()
->willReturnCallback($mockMethodBehavior);
$this->visitService->expects($this->exactly($expectedEmptyCalls))
->method('locateVisitsWithEmptyLocation')
->withAnyParameters()
->willReturnCallback($mockMethodBehavior);
$this->visitService->expects($this->exactly($expectedAllCalls))
->method('locateAllVisits')
->withAnyParameters()
->willReturnCallback($mockMethodBehavior);
$this->visitToLocation->expects(
$this->exactly($expectedUnlocatedCalls + $expectedEmptyCalls + $expectedAllCalls),
)->method('resolveVisitLocation')->withAnyParameters()->willReturn(Location::emptyInstance());
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS);
$this->commandTester->setInputs(['y']);
$this->commandTester->execute($args);
@@ -98,12 +98,6 @@ class LocateVisitsCommandTest extends TestCase
} else {
self::assertStringNotContainsString('Continue at your own', $output);
}
$locateVisits->shouldHaveBeenCalledTimes($expectedUnlocatedCalls);
$locateEmptyVisits->shouldHaveBeenCalledTimes($expectedEmptyCalls);
$locateAllVisits->shouldHaveBeenCalledTimes($expectedAllCalls);
$resolveIpLocation->shouldHaveBeenCalledTimes(
$expectedUnlocatedCalls + $expectedEmptyCalls + $expectedAllCalls,
);
}
public function provideArgs(): iterable
@@ -122,18 +116,19 @@ class LocateVisitsCommandTest extends TestCase
$visit = Visit::forValidShortUrl(ShortUrl::createEmpty(), Visitor::emptyInstance());
$location = VisitLocation::fromGeolocation(Location::emptyInstance());
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(
$this->invokeHelperMethods($visit, $location),
);
$resolveIpLocation = $this->visitToLocation->resolveVisitLocation(Argument::any())->willThrow($e);
$this->lock->method('acquire')->with($this->isFalse())->willReturn(true);
$this->visitService->expects($this->once())
->method('locateUnlocatedVisits')
->withAnyParameters()
->willReturnCallback($this->invokeHelperMethods($visit, $location));
$this->visitToLocation->expects($this->once())->method('resolveVisitLocation')->willThrowException($e);
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS);
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
$output = $this->commandTester->getDisplay();
self::assertStringContainsString('Processing IP', $output);
self::assertStringContainsString($message, $output);
$locateVisits->shouldHaveBeenCalledOnce();
$resolveIpLocation->shouldHaveBeenCalledOnce();
}
public function provideIgnoredAddresses(): iterable
@@ -148,28 +143,26 @@ class LocateVisitsCommandTest extends TestCase
$visit = Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('', '', '1.2.3.4', ''));
$location = VisitLocation::fromGeolocation(Location::emptyInstance());
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(
$this->invokeHelperMethods($visit, $location),
);
$resolveIpLocation = $this->visitToLocation->resolveVisitLocation(Argument::any())->willThrow(
$this->lock->method('acquire')->with($this->isFalse())->willReturn(true);
$this->visitService->expects($this->once())
->method('locateUnlocatedVisits')
->withAnyParameters()
->willReturnCallback($this->invokeHelperMethods($visit, $location));
$this->visitToLocation->expects($this->once())->method('resolveVisitLocation')->willThrowException(
IpCannotBeLocatedException::forError(WrongIpException::fromIpAddress('1.2.3.4')),
);
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS);
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
$output = $this->commandTester->getDisplay();
self::assertStringContainsString('An error occurred while locating IP. Skipped', $output);
$locateVisits->shouldHaveBeenCalledOnce();
$resolveIpLocation->shouldHaveBeenCalledOnce();
}
private function invokeHelperMethods(Visit $visit, VisitLocation $location): callable
{
return function (array $args) use ($visit, $location): void {
/** @var VisitGeolocationHelperInterface $helper */
[$helper] = $args;
return static function (VisitGeolocationHelperInterface $helper) use ($visit, $location): void {
$helper->geolocateVisit($visit);
$helper->onVisitLocated($location, $visit);
};
@@ -178,11 +171,11 @@ class LocateVisitsCommandTest extends TestCase
/** @test */
public function noActionIsPerformedIfLockIsAcquired(): void
{
$this->lock->acquire(false)->willReturn(false);
$this->lock->method('acquire')->with($this->isFalse())->willReturn(false);
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(function (): void {
});
$resolveIpLocation = $this->visitToLocation->resolveVisitLocation(Argument::any());
$this->visitService->expects($this->never())->method('locateUnlocatedVisits');
$this->visitToLocation->expects($this->never())->method('resolveVisitLocation');
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS);
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
$output = $this->commandTester->getDisplay();
@@ -191,25 +184,27 @@ class LocateVisitsCommandTest extends TestCase
sprintf('Command "%s" is already in progress. Skipping.', LocateVisitsCommand::NAME),
$output,
);
$locateVisits->shouldNotHaveBeenCalled();
$resolveIpLocation->shouldNotHaveBeenCalled();
}
/** @test */
public function showsProperMessageWhenGeoLiteUpdateFails(): void
{
$this->downloadDbCommand->run(Argument::cetera())->willReturn(ExitCodes::EXIT_FAILURE);
$this->lock->method('acquire')->with($this->isFalse())->willReturn(true);
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_FAILURE);
$this->visitService->expects($this->never())->method('locateUnlocatedVisits');
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
self::assertStringContainsString('It is not possible to locate visits without a GeoLite2 db file.', $output);
$this->visitService->locateUnlocatedVisits(Argument::cetera())->shouldNotHaveBeenCalled();
}
/** @test */
public function providingAllFlagOnItsOwnDisplaysNotice(): void
{
$this->lock->method('acquire')->with($this->isFalse())->willReturn(true);
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS);
$this->commandTester->execute(['--all' => true]);
$output = $this->commandTester->getDisplay();
@@ -222,6 +217,8 @@ class LocateVisitsCommandTest extends TestCase
*/
public function processingAllCancelsCommandIfUserDoesNotActivelyAgreeToConfirmation(array $inputs): void
{
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Execution aborted');

View File

@@ -31,8 +31,8 @@ class ApplicationFactoryTest extends TestCase
'baz' => 'baz',
],
]);
$sm->setService('foo', $this->createCommandMock('foo')->reveal());
$sm->setService('bar', $this->createCommandMock('bar')->reveal());
$sm->setService('foo', $this->createCommandMock('foo'));
$sm->setService('bar', $this->createCommandMock('bar'));
$instance = ($this->factory)($sm);

View File

@@ -7,10 +7,8 @@ namespace ShlinkioTest\Shlink\CLI\GeoLite;
use Cake\Chronos\Chronos;
use GeoIp2\Database\Reader;
use MaxMind\Db\Reader\Metadata;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdater;
use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult;
@@ -25,23 +23,16 @@ use function range;
class GeolocationDbUpdaterTest extends TestCase
{
use ProphecyTrait;
private GeolocationDbUpdater $geolocationDbUpdater;
private ObjectProphecy $dbUpdater;
private ObjectProphecy $geoLiteDbReader;
private ObjectProphecy $lock;
private MockObject & DbUpdaterInterface $dbUpdater;
private MockObject & Reader $geoLiteDbReader;
private MockObject & Lock\LockInterface $lock;
protected function setUp(): void
{
$this->dbUpdater = $this->prophesize(DbUpdaterInterface::class);
$this->geoLiteDbReader = $this->prophesize(Reader::class);
$this->trackingOptions = new TrackingOptions();
$this->lock = $this->prophesize(Lock\LockInterface::class);
$this->lock->acquire(true)->willReturn(true);
$this->lock->release()->will(function (): void {
});
$this->dbUpdater = $this->createMock(DbUpdaterInterface::class);
$this->geoLiteDbReader = $this->createMock(Reader::class);
$this->lock = $this->createMock(Lock\LockInterface::class);
$this->lock->method('acquire')->with($this->isTrue())->willReturn(true);
}
/** @test */
@@ -50,25 +41,21 @@ class GeolocationDbUpdaterTest extends TestCase
$mustBeUpdated = fn () => self::assertTrue(true);
$prev = new DbUpdateException('');
$fileExists = $this->dbUpdater->databaseFileExists()->willReturn(false);
$getMeta = $this->geoLiteDbReader->metadata();
$download = $this->dbUpdater->downloadFreshCopy(null)->willThrow($prev);
$this->dbUpdater->expects($this->once())->method('databaseFileExists')->willReturn(false);
$this->dbUpdater->expects($this->once())->method('downloadFreshCopy')->with(
$this->isNull(),
)->willThrowException($prev);
$this->geoLiteDbReader->expects($this->never())->method('metadata');
try {
$this->geolocationDbUpdater()->checkDbUpdate($mustBeUpdated);
self::assertTrue(false); // If this is reached, the test will fail
self::fail();
} catch (Throwable $e) {
/** @var GeolocationDbUpdateFailedException $e */
self::assertInstanceOf(GeolocationDbUpdateFailedException::class, $e);
self::assertSame($prev, $e->getPrevious());
self::assertFalse($e->olderDbExists());
}
$fileExists->shouldHaveBeenCalledOnce();
$getMeta->shouldNotHaveBeenCalled();
$download->shouldHaveBeenCalledOnce();
$this->lock->acquire(true)->shouldHaveBeenCalledOnce();
$this->lock->release()->shouldHaveBeenCalledOnce();
}
/**
@@ -77,26 +64,24 @@ class GeolocationDbUpdaterTest extends TestCase
*/
public function exceptionIsThrownWhenOlderDbIsTooOldAndDownloadFails(int $days): void
{
$fileExists = $this->dbUpdater->databaseFileExists()->willReturn(true);
$getMeta = $this->geoLiteDbReader->metadata()->willReturn($this->buildMetaWithBuildEpoch(
Chronos::now()->subDays($days)->getTimestamp(),
));
$prev = new DbUpdateException('');
$download = $this->dbUpdater->downloadFreshCopy(null)->willThrow($prev);
$this->dbUpdater->expects($this->once())->method('databaseFileExists')->willReturn(true);
$this->dbUpdater->expects($this->once())->method('downloadFreshCopy')->with(
$this->isNull(),
)->willThrowException($prev);
$this->geoLiteDbReader->expects($this->once())->method('metadata')->with()->willReturn(
$this->buildMetaWithBuildEpoch(Chronos::now()->subDays($days)->getTimestamp()),
);
try {
$this->geolocationDbUpdater()->checkDbUpdate();
self::assertTrue(false); // If this is reached, the test will fail
self::fail();
} catch (Throwable $e) {
/** @var GeolocationDbUpdateFailedException $e */
self::assertInstanceOf(GeolocationDbUpdateFailedException::class, $e);
self::assertSame($prev, $e->getPrevious());
self::assertTrue($e->olderDbExists());
}
$fileExists->shouldHaveBeenCalledOnce();
$getMeta->shouldHaveBeenCalledOnce();
$download->shouldHaveBeenCalledOnce();
}
public function provideBigDays(): iterable
@@ -113,17 +98,15 @@ class GeolocationDbUpdaterTest extends TestCase
*/
public function databaseIsNotUpdatedIfItIsNewEnough(string|int $buildEpoch): void
{
$fileExists = $this->dbUpdater->databaseFileExists()->willReturn(true);
$getMeta = $this->geoLiteDbReader->metadata()->willReturn($this->buildMetaWithBuildEpoch($buildEpoch));
$download = $this->dbUpdater->downloadFreshCopy(null)->will(function (): void {
});
$this->dbUpdater->expects($this->once())->method('databaseFileExists')->willReturn(true);
$this->dbUpdater->expects($this->never())->method('downloadFreshCopy');
$this->geoLiteDbReader->expects($this->once())->method('metadata')->with()->willReturn(
$this->buildMetaWithBuildEpoch($buildEpoch),
);
$result = $this->geolocationDbUpdater()->checkDbUpdate();
self::assertEquals(GeolocationResult::DB_IS_UP_TO_DATE, $result);
$fileExists->shouldHaveBeenCalledOnce();
$getMeta->shouldHaveBeenCalledOnce();
$download->shouldNotHaveBeenCalled();
}
public function provideSmallDays(): iterable
@@ -139,18 +122,16 @@ class GeolocationDbUpdaterTest extends TestCase
/** @test */
public function exceptionIsThrownWhenCheckingExistingDatabaseWithInvalidBuildEpoch(): void
{
$fileExists = $this->dbUpdater->databaseFileExists()->willReturn(true);
$getMeta = $this->geoLiteDbReader->metadata()->willReturn($this->buildMetaWithBuildEpoch('invalid'));
$download = $this->dbUpdater->downloadFreshCopy(null)->will(function (): void {
});
$this->dbUpdater->expects($this->once())->method('databaseFileExists')->willReturn(true);
$this->dbUpdater->expects($this->never())->method('downloadFreshCopy');
$this->geoLiteDbReader->expects($this->once())->method('metadata')->with()->willReturn(
$this->buildMetaWithBuildEpoch('invalid'),
);
$this->expectException(GeolocationDbUpdateFailedException::class);
$this->expectExceptionMessage(
'Build epoch with value "invalid" from existing geolocation database, could not be parsed to integer.',
);
$fileExists->shouldBeCalledOnce();
$getMeta->shouldBeCalledOnce();
$download->shouldNotBeCalled();
$this->geolocationDbUpdater()->checkDbUpdate();
}
@@ -177,10 +158,10 @@ class GeolocationDbUpdaterTest extends TestCase
public function downloadDbIsSkippedIfTrackingIsDisabled(TrackingOptions $options): void
{
$result = $this->geolocationDbUpdater($options)->checkDbUpdate();
$this->dbUpdater->expects($this->never())->method('databaseFileExists');
$this->geoLiteDbReader->expects($this->never())->method('metadata');
self::assertEquals(GeolocationResult::CHECK_SKIPPED, $result);
$this->dbUpdater->databaseFileExists(Argument::cetera())->shouldNotHaveBeenCalled();
$this->geoLiteDbReader->metadata(Argument::cetera())->shouldNotHaveBeenCalled();
}
public function provideTrackingOptions(): iterable
@@ -192,13 +173,13 @@ class GeolocationDbUpdaterTest extends TestCase
private function geolocationDbUpdater(?TrackingOptions $options = null): GeolocationDbUpdater
{
$locker = $this->prophesize(Lock\LockFactory::class);
$locker->createLock(Argument::type('string'))->willReturn($this->lock->reveal());
$locker = $this->createMock(Lock\LockFactory::class);
$locker->method('createLock')->with($this->isType('string'))->willReturn($this->lock);
return new GeolocationDbUpdater(
$this->dbUpdater->reveal(),
$this->geoLiteDbReader->reveal(),
$locker->reveal(),
$this->dbUpdater,
$this->geoLiteDbReader,
$locker,
$options ?? new TrackingOptions(),
);
}

View File

@@ -4,10 +4,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Util;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Util\ProcessRunner;
use Symfony\Component\Console\Helper\DebugFormatterHelper;
use Symfony\Component\Console\Helper\HelperSet;
@@ -17,90 +15,73 @@ use Symfony\Component\Process\Process;
class ProcessRunnerTest extends TestCase
{
use ProphecyTrait;
private ProcessRunner $runner;
private ObjectProphecy $helper;
private ObjectProphecy $formatter;
private ObjectProphecy $process;
private ObjectProphecy $output;
private MockObject & ProcessHelper $helper;
private MockObject & DebugFormatterHelper $formatter;
private MockObject & Process $process;
private MockObject & OutputInterface $output;
protected function setUp(): void
{
$this->helper = $this->prophesize(ProcessHelper::class);
$this->formatter = $this->prophesize(DebugFormatterHelper::class);
$helperSet = $this->prophesize(HelperSet::class);
$helperSet->get('debug_formatter')->willReturn($this->formatter->reveal());
$this->helper->getHelperSet()->willReturn($helperSet->reveal());
$this->process = $this->prophesize(Process::class);
$this->helper = $this->createMock(ProcessHelper::class);
$this->formatter = $this->createMock(DebugFormatterHelper::class);
$helperSet = $this->createMock(HelperSet::class);
$helperSet->method('get')->with('debug_formatter')->willReturn($this->formatter);
$this->helper->method('getHelperSet')->with()->willReturn($helperSet);
$this->process = $this->createMock(Process::class);
$this->output = $this->createMock(OutputInterface::class);
$this->runner = new ProcessRunner($this->helper->reveal(), fn () => $this->process->reveal());
$this->output = $this->prophesize(OutputInterface::class);
$this->runner = new ProcessRunner($this->helper, fn () => $this->process);
}
/** @test */
public function noMessagesAreWrittenWhenOutputIsNotVerbose(): void
{
$isVeryVerbose = $this->output->isVeryVerbose()->willReturn(false);
$isDebug = $this->output->isDebug()->willReturn(false);
$mustRun = $this->process->mustRun(Argument::cetera())->willReturn($this->process->reveal());
$this->output->expects($this->exactly(2))->method('isVeryVerbose')->with()->willReturn(false);
$this->output->expects($this->once())->method('isDebug')->with()->willReturn(false);
$this->output->expects($this->never())->method('write');
$this->process->expects($this->once())->method('mustRun')->withAnyParameters()->willReturnSelf();
$this->process->expects($this->never())->method('isSuccessful');
$this->process->expects($this->never())->method('getCommandLine');
$this->helper->expects($this->never())->method('wrapCallback');
$this->formatter->expects($this->never())->method('start');
$this->formatter->expects($this->never())->method('stop');
$this->runner->run($this->output->reveal(), []);
$isVeryVerbose->shouldHaveBeenCalledTimes(2);
$isDebug->shouldHaveBeenCalledOnce();
$mustRun->shouldHaveBeenCalledOnce();
$this->process->isSuccessful()->shouldNotHaveBeenCalled();
$this->process->getCommandLine()->shouldNotHaveBeenCalled();
$this->output->write(Argument::cetera())->shouldNotHaveBeenCalled();
$this->helper->wrapCallback(Argument::cetera())->shouldNotHaveBeenCalled();
$this->formatter->start(Argument::cetera())->shouldNotHaveBeenCalled();
$this->formatter->stop(Argument::cetera())->shouldNotHaveBeenCalled();
$this->runner->run($this->output, []);
}
/** @test */
public function someMessagesAreWrittenWhenOutputIsVerbose(): void
{
$isVeryVerbose = $this->output->isVeryVerbose()->willReturn(true);
$isDebug = $this->output->isDebug()->willReturn(false);
$mustRun = $this->process->mustRun(Argument::cetera())->willReturn($this->process->reveal());
$isSuccessful = $this->process->isSuccessful()->willReturn(true);
$getCommandLine = $this->process->getCommandLine()->willReturn('true');
$start = $this->formatter->start(Argument::cetera())->willReturn('');
$stop = $this->formatter->stop(Argument::cetera())->willReturn('');
$this->output->expects($this->exactly(2))->method('isVeryVerbose')->with()->willReturn(true);
$this->output->expects($this->once())->method('isDebug')->with()->willReturn(false);
$this->output->expects($this->exactly(2))->method('write')->withAnyParameters();
$this->process->expects($this->once())->method('mustRun')->withAnyParameters()->willReturnSelf();
$this->process->expects($this->exactly(2))->method('isSuccessful')->with()->willReturn(true);
$this->process->expects($this->once())->method('getCommandLine')->with()->willReturn('true');
$this->formatter->expects($this->once())->method('start')->withAnyParameters()->willReturn('');
$this->formatter->expects($this->once())->method('stop')->withAnyParameters()->willReturn('');
$this->helper->expects($this->never())->method('wrapCallback');
$this->runner->run($this->output->reveal(), []);
$isVeryVerbose->shouldHaveBeenCalledTimes(2);
$isDebug->shouldHaveBeenCalledOnce();
$mustRun->shouldHaveBeenCalledOnce();
$this->output->write(Argument::cetera())->shouldHaveBeenCalledTimes(2);
$this->helper->wrapCallback(Argument::cetera())->shouldNotHaveBeenCalled();
$isSuccessful->shouldHaveBeenCalledTimes(2);
$getCommandLine->shouldHaveBeenCalledOnce();
$start->shouldHaveBeenCalledOnce();
$stop->shouldHaveBeenCalledOnce();
$this->runner->run($this->output, []);
}
/** @test */
public function wrapsCallbackWhenOutputIsDebug(): void
{
$isVeryVerbose = $this->output->isVeryVerbose()->willReturn(false);
$isDebug = $this->output->isDebug()->willReturn(true);
$mustRun = $this->process->mustRun(Argument::cetera())->willReturn($this->process->reveal());
$wrapCallback = $this->helper->wrapCallback(Argument::cetera())->willReturn(function (): void {
});
$this->output->expects($this->exactly(2))->method('isVeryVerbose')->with()->willReturn(false);
$this->output->expects($this->once())->method('isDebug')->with()->willReturn(true);
$this->output->expects($this->never())->method('write');
$this->process->expects($this->once())->method('mustRun')->withAnyParameters()->willReturnSelf();
$this->process->expects($this->never())->method('isSuccessful');
$this->process->expects($this->never())->method('getCommandLine');
$this->helper->expects($this->once())->method('wrapCallback')->withAnyParameters()->willReturn(
function (): void {
},
);
$this->formatter->expects($this->never())->method('start');
$this->formatter->expects($this->never())->method('stop');
$this->runner->run($this->output->reveal(), []);
$isVeryVerbose->shouldHaveBeenCalledTimes(2);
$isDebug->shouldHaveBeenCalledOnce();
$mustRun->shouldHaveBeenCalledOnce();
$wrapCallback->shouldHaveBeenCalledOnce();
$this->process->isSuccessful()->shouldNotHaveBeenCalled();
$this->process->getCommandLine()->shouldNotHaveBeenCalled();
$this->output->write(Argument::cetera())->shouldNotHaveBeenCalled();
$this->formatter->start(Argument::cetera())->shouldNotHaveBeenCalled();
$this->formatter->stop(Argument::cetera())->shouldNotHaveBeenCalled();
$this->runner->run($this->output, []);
}
}

View File

@@ -4,10 +4,8 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Util;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use ReflectionObject;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Symfony\Component\Console\Helper\Table;
@@ -16,15 +14,13 @@ use Symfony\Component\Console\Output\OutputInterface;
class ShlinkTableTest extends TestCase
{
use ProphecyTrait;
private ShlinkTable $shlinkTable;
private ObjectProphecy $baseTable;
private MockObject & Table $baseTable;
protected function setUp(): void
{
$this->baseTable = $this->prophesize(Table::class);
$this->shlinkTable = ShlinkTable::fromBaseTable($this->baseTable->reveal());
$this->baseTable = $this->createMock(Table::class);
$this->shlinkTable = ShlinkTable::fromBaseTable($this->baseTable);
}
/** @test */
@@ -35,29 +31,22 @@ class ShlinkTableTest extends TestCase
$headerTitle = 'Header';
$footerTitle = 'Footer';
$setStyle = $this->baseTable->setStyle(Argument::type(TableStyle::class))->willReturn(
$this->baseTable->reveal(),
);
$setHeaders = $this->baseTable->setHeaders($headers)->willReturn($this->baseTable->reveal());
$setRows = $this->baseTable->setRows($rows)->willReturn($this->baseTable->reveal());
$setFooterTitle = $this->baseTable->setFooterTitle($footerTitle)->willReturn($this->baseTable->reveal());
$setHeaderTitle = $this->baseTable->setHeaderTitle($headerTitle)->willReturn($this->baseTable->reveal());
$render = $this->baseTable->render()->willReturn($this->baseTable->reveal());
$this->baseTable->expects($this->once())->method('setStyle')->with(
$this->isInstanceOf(TableStyle::class),
)->willReturnSelf();
$this->baseTable->expects($this->once())->method('setHeaders')->with($headers)->willReturnSelf();
$this->baseTable->expects($this->once())->method('setRows')->with($rows)->willReturnSelf();
$this->baseTable->expects($this->once())->method('setFooterTitle')->with($footerTitle)->willReturnSelf();
$this->baseTable->expects($this->once())->method('setHeaderTitle')->with($headerTitle)->willReturnSelf();
$this->baseTable->expects($this->once())->method('render')->with()->willReturnSelf();
$this->shlinkTable->render($headers, $rows, $footerTitle, $headerTitle);
$setStyle->shouldHaveBeenCalledOnce();
$setHeaders->shouldHaveBeenCalledOnce();
$setRows->shouldHaveBeenCalledOnce();
$setFooterTitle->shouldHaveBeenCalledOnce();
$setHeaderTitle->shouldHaveBeenCalledOnce();
$render->shouldHaveBeenCalledOnce();
}
/** @test */
public function newTableIsCreatedForFactoryMethod(): void
{
$instance = ShlinkTable::default($this->prophesize(OutputInterface::class)->reveal());
$instance = ShlinkTable::default($this->createMock(OutputInterface::class));
$ref = new ReflectionObject($instance);
$baseTable = $ref->getProperty('baseTable');

View File

@@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core;
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Laminas\ServiceManager\Factory\InvokableFactory;
use Psr\EventDispatcher\EventDispatcherInterface;
use Shlinkio\Shlink\Common\Doctrine\EntityRepositoryFactory;
use Shlinkio\Shlink\Config\Factory\ValinorConfigFactory;
use Shlinkio\Shlink\Core\ErrorHandler;
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
@@ -32,11 +33,27 @@ return [
Options\RabbitMqOptions::class => [ValinorConfigFactory::class, 'config.rabbitmq'],
Options\WebhookOptions::class => ConfigAbstractFactory::class,
Service\UrlShortener::class => ConfigAbstractFactory::class,
Service\ShortUrlService::class => ConfigAbstractFactory::class,
Service\ShortUrl\DeleteShortUrlService::class => ConfigAbstractFactory::class,
Service\ShortUrl\ShortUrlResolver::class => ConfigAbstractFactory::class,
Service\ShortUrl\ShortCodeUniquenessHelper::class => ConfigAbstractFactory::class,
ShortUrl\UrlShortener::class => ConfigAbstractFactory::class,
ShortUrl\ShortUrlService::class => ConfigAbstractFactory::class,
ShortUrl\ShortUrlListService::class => ConfigAbstractFactory::class,
ShortUrl\DeleteShortUrlService::class => ConfigAbstractFactory::class,
ShortUrl\ShortUrlResolver::class => ConfigAbstractFactory::class,
ShortUrl\Helper\ShortCodeUniquenessHelper::class => ConfigAbstractFactory::class,
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class => ConfigAbstractFactory::class,
ShortUrl\Helper\ShortUrlStringifier::class => ConfigAbstractFactory::class,
ShortUrl\Helper\ShortUrlTitleResolutionHelper::class => ConfigAbstractFactory::class,
ShortUrl\Helper\ShortUrlRedirectionBuilder::class => ConfigAbstractFactory::class,
ShortUrl\Transformer\ShortUrlDataTransformer::class => ConfigAbstractFactory::class,
ShortUrl\Middleware\ExtraPathRedirectMiddleware::class => ConfigAbstractFactory::class,
ShortUrl\Middleware\TrimTrailingSlashMiddleware::class => ConfigAbstractFactory::class,
ShortUrl\Repository\ShortUrlListRepository::class => [
EntityRepositoryFactory::class,
ShortUrl\Entity\ShortUrl::class,
],
ShortUrl\Repository\CrawlableShortCodesQuery::class => [
EntityRepositoryFactory::class,
ShortUrl\Entity\ShortUrl::class,
],
Tag\TagService::class => ConfigAbstractFactory::class,
@@ -44,10 +61,14 @@ return [
Visit\VisitsTracker::class => ConfigAbstractFactory::class,
Visit\RequestTracker::class => ConfigAbstractFactory::class,
Visit\VisitLocator::class => ConfigAbstractFactory::class,
Visit\VisitToLocationHelper::class => ConfigAbstractFactory::class,
Visit\Geolocation\VisitLocator::class => ConfigAbstractFactory::class,
Visit\Geolocation\VisitToLocationHelper::class => ConfigAbstractFactory::class,
Visit\VisitsStatsHelper::class => ConfigAbstractFactory::class,
Visit\Transformer\OrphanVisitDataTransformer::class => InvokableFactory::class,
Visit\Repository\VisitLocationRepository::class => [
EntityRepositoryFactory::class,
Visit\Entity\Visit::class,
],
Util\UrlValidator::class => ConfigAbstractFactory::class,
Util\DoctrineBatchHelper::class => ConfigAbstractFactory::class,
@@ -60,13 +81,6 @@ return [
Action\QrCodeAction::class => ConfigAbstractFactory::class,
Action\RobotsAction::class => ConfigAbstractFactory::class,
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class => ConfigAbstractFactory::class,
ShortUrl\Helper\ShortUrlStringifier::class => ConfigAbstractFactory::class,
ShortUrl\Helper\ShortUrlTitleResolutionHelper::class => ConfigAbstractFactory::class,
ShortUrl\Helper\ShortUrlRedirectionBuilder::class => ConfigAbstractFactory::class,
ShortUrl\Transformer\ShortUrlDataTransformer::class => ConfigAbstractFactory::class,
ShortUrl\Middleware\ExtraPathRedirectMiddleware::class => ConfigAbstractFactory::class,
EventDispatcher\PublishingUpdatesGenerator::class => ConfigAbstractFactory::class,
Importer\ImportedLinksProcessor::class => ConfigAbstractFactory::class,
@@ -90,11 +104,11 @@ return [
Options\WebhookOptions::class => ['config.visits_webhooks'],
Service\UrlShortener::class => [
ShortUrl\UrlShortener::class => [
ShortUrl\Helper\ShortUrlTitleResolutionHelper::class,
'em',
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class,
Service\ShortUrl\ShortCodeUniquenessHelper::class,
ShortUrl\Helper\ShortCodeUniquenessHelper::class,
EventDispatcherInterface::class,
],
Visit\VisitsTracker::class => [
@@ -103,23 +117,27 @@ return [
Options\TrackingOptions::class,
],
Visit\RequestTracker::class => [Visit\VisitsTracker::class, Options\TrackingOptions::class],
Service\ShortUrlService::class => [
ShortUrl\ShortUrlService::class => [
'em',
Service\ShortUrl\ShortUrlResolver::class,
ShortUrl\ShortUrlResolver::class,
ShortUrl\Helper\ShortUrlTitleResolutionHelper::class,
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class,
],
Visit\VisitLocator::class => ['em'],
Visit\VisitToLocationHelper::class => [IpLocationResolverInterface::class],
ShortUrl\ShortUrlListService::class => [
ShortUrl\Repository\ShortUrlListRepository::class,
Options\UrlShortenerOptions::class,
],
Visit\Geolocation\VisitLocator::class => ['em', Visit\Repository\VisitLocationRepository::class],
Visit\Geolocation\VisitToLocationHelper::class => [IpLocationResolverInterface::class],
Visit\VisitsStatsHelper::class => ['em'],
Tag\TagService::class => ['em'],
Service\ShortUrl\DeleteShortUrlService::class => [
ShortUrl\DeleteShortUrlService::class => [
'em',
Options\DeleteShortUrlsOptions::class,
Service\ShortUrl\ShortUrlResolver::class,
ShortUrl\ShortUrlResolver::class,
],
Service\ShortUrl\ShortUrlResolver::class => ['em'],
Service\ShortUrl\ShortCodeUniquenessHelper::class => ['em'],
ShortUrl\ShortUrlResolver::class => ['em'],
ShortUrl\Helper\ShortCodeUniquenessHelper::class => ['em'],
Domain\DomainService::class => ['em', 'config.url_shortener.domain.hostname'],
Util\UrlValidator::class => ['httpClient', Options\UrlShortenerOptions::class],
@@ -129,14 +147,14 @@ return [
Config\NotFoundRedirectResolver::class => [Util\RedirectResponseHelper::class, 'Logger_Shlink'],
Action\RedirectAction::class => [
Service\ShortUrl\ShortUrlResolver::class,
ShortUrl\ShortUrlResolver::class,
Visit\RequestTracker::class,
ShortUrl\Helper\ShortUrlRedirectionBuilder::class,
Util\RedirectResponseHelper::class,
],
Action\PixelAction::class => [Service\ShortUrl\ShortUrlResolver::class, Visit\RequestTracker::class],
Action\PixelAction::class => [ShortUrl\ShortUrlResolver::class, Visit\RequestTracker::class],
Action\QrCodeAction::class => [
Service\ShortUrl\ShortUrlResolver::class,
ShortUrl\ShortUrlResolver::class,
ShortUrl\Helper\ShortUrlStringifier::class,
'Logger_Shlink',
Options\QrCodeOptions::class,
@@ -149,12 +167,13 @@ return [
ShortUrl\Helper\ShortUrlRedirectionBuilder::class => [Options\TrackingOptions::class],
ShortUrl\Transformer\ShortUrlDataTransformer::class => [ShortUrl\Helper\ShortUrlStringifier::class],
ShortUrl\Middleware\ExtraPathRedirectMiddleware::class => [
Service\ShortUrl\ShortUrlResolver::class,
ShortUrl\ShortUrlResolver::class,
Visit\RequestTracker::class,
ShortUrl\Helper\ShortUrlRedirectionBuilder::class,
Util\RedirectResponseHelper::class,
Options\UrlShortenerOptions::class,
],
ShortUrl\Middleware\TrimTrailingSlashMiddleware::class => [Options\UrlShortenerOptions::class],
EventDispatcher\PublishingUpdatesGenerator::class => [
ShortUrl\Transformer\ShortUrlDataTransformer::class,
@@ -164,7 +183,7 @@ return [
Importer\ImportedLinksProcessor::class => [
'em',
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class,
Service\ShortUrl\ShortCodeUniquenessHelper::class,
ShortUrl\Helper\ShortCodeUniquenessHelper::class,
Util\DoctrineBatchHelper::class,
],

View File

@@ -14,7 +14,7 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable(determineTableName('short_urls', $emConfig))
->setCustomRepositoryClass(Repository\ShortUrlRepository::class);
->setCustomRepositoryClass(ShortUrl\Repository\ShortUrlRepository::class);
$builder->createField('id', Types::BIGINT)
->columnName('id')
@@ -62,19 +62,19 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
->nullable()
->build();
$builder->createOneToMany('visits', Entity\Visit::class)
$builder->createOneToMany('visits', Visit\Entity\Visit::class)
->mappedBy('shortUrl')
->fetchExtraLazy()
->build();
$builder->createManyToMany('tags', Entity\Tag::class)
$builder->createManyToMany('tags', Tag\Entity\Tag::class)
->setJoinTable(determineTableName('short_urls_in_tags', $emConfig))
->addInverseJoinColumn('tag_id', 'id', true, false, 'CASCADE')
->addJoinColumn('short_url_id', 'id', true, false, 'CASCADE')
->setOrderBy(['name' => 'ASC'])
->build();
$builder->createManyToOne('domain', Entity\Domain::class)
$builder->createManyToOne('domain', Domain\Entity\Domain::class)
->addJoinColumn('domain_id', 'id', true, false, 'RESTRICT')
->cascadePersist()
->build();

View File

@@ -12,7 +12,7 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable(determineTableName('tags', $emConfig))
->setCustomRepositoryClass(Repository\TagRepository::class);
->setCustomRepositoryClass(Tag\Repository\TagRepository::class);
$builder->createField('id', Types::BIGINT)
->columnName('id')
@@ -25,5 +25,5 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
->unique()
->build();
$builder->addInverseManyToMany('shortUrls', Entity\ShortUrl::class, 'tags');
$builder->addInverseManyToMany('shortUrls', ShortUrl\Entity\ShortUrl::class, 'tags');
};

View File

@@ -9,14 +9,14 @@ use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
use Doctrine\ORM\Mapping\Builder\FieldBuilder;
use Doctrine\ORM\Mapping\ClassMetadata;
use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
return static function (ClassMetadata $metadata, array $emConfig): void {
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable(determineTableName('visits', $emConfig))
->setCustomRepositoryClass(Repository\VisitRepository::class);
->setCustomRepositoryClass(Visit\Repository\VisitRepository::class);
$builder->createField('id', Types::BIGINT)
->columnName('id')
@@ -48,11 +48,11 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
->nullable()
->build();
$builder->createManyToOne('shortUrl', Entity\ShortUrl::class)
$builder->createManyToOne('shortUrl', ShortUrl\Entity\ShortUrl::class)
->addJoinColumn('short_url_id', 'id', true, false, 'CASCADE')
->build();
$builder->createManyToOne('visitLocation', Entity\VisitLocation::class)
$builder->createManyToOne('visitLocation', Visit\Entity\VisitLocation::class)
->addJoinColumn('visit_location_id', 'id', true, false, 'Set NULL')
->cascadePersist()
->build();

View File

@@ -10,8 +10,8 @@ use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdater;
use Shlinkio\Shlink\Common\Cache\RedisPublishingHelper;
use Shlinkio\Shlink\Common\Mercure\MercureHubPublishingHelper;
use Shlinkio\Shlink\Common\RabbitMq\RabbitMqPublishingHelper;
use Shlinkio\Shlink\Core\Visit\VisitLocator;
use Shlinkio\Shlink\Core\Visit\VisitToLocationHelper;
use Shlinkio\Shlink\Core\Visit\Geolocation\VisitLocator;
use Shlinkio\Shlink\Core\Visit\Geolocation\VisitToLocationHelper;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core;
use Cake\Chronos\Chronos;
use Cake\Chronos\ChronosInterface;
use DateTimeInterface;
use Doctrine\ORM\Mapping\Builder\FieldBuilder;
use Jaybizzle\CrawlerDetect\CrawlerDetect;
@@ -35,7 +36,7 @@ function generateRandomShortCode(int $length): string
function parseDateFromQuery(array $query, string $dateName): ?Chronos
{
return normalizeDate(empty($query[$dateName] ?? null) ? null : Chronos::parse($query[$dateName]));
return normalizeOptionalDate(empty($query[$dateName] ?? null) ? null : Chronos::parse($query[$dateName]));
}
function parseDateRangeFromQuery(array $query, string $startDateName, string $endDateName): DateRange
@@ -46,7 +47,10 @@ function parseDateRangeFromQuery(array $query, string $startDateName, string $en
return buildDateRange($startDate, $endDate);
}
function normalizeDate(string|DateTimeInterface|Chronos|null $date): ?Chronos
/**
* @return ($date is null ? null : Chronos)
*/
function normalizeOptionalDate(string|DateTimeInterface|ChronosInterface|null $date): ?Chronos
{
$parsedDate = match (true) {
$date === null || $date instanceof Chronos => $date,
@@ -57,6 +61,11 @@ function normalizeDate(string|DateTimeInterface|Chronos|null $date): ?Chronos
return $parsedDate?->setTimezone(date_default_timezone_get());
}
function normalizeDate(string|DateTimeInterface|ChronosInterface $date): Chronos
{
return normalizeOptionalDate($date);
}
function getOptionalIntFromInputFilter(InputFilter $inputFilter, string $fieldName): ?int
{
$value = $inputFilter->getValue($fieldName);
@@ -69,6 +78,12 @@ function getOptionalBoolFromInputFilter(InputFilter $inputFilter, string $fieldN
return $value !== null ? (bool) $value : null;
}
function getNonEmptyOptionalValueFromInputFilter(InputFilter $inputFilter, string $fieldName): mixed
{
$value = $inputFilter->getValue($fieldName);
return empty($value) ? null : $value;
}
function arrayToString(array $array, int $indentSize = 4): string
{
$indent = str_repeat(' ', $indentSize);

View File

@@ -10,10 +10,10 @@ use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
use Shlinkio\Shlink\Core\Visit\RequestTrackerInterface;
abstract class AbstractTrackingAction implements MiddlewareInterface, RequestMethodInterface

View File

@@ -8,7 +8,7 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Common\Response\PixelResponse;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
class PixelAction extends AbstractTrackingAction
{

View File

@@ -13,10 +13,10 @@ use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Common\Response\QrCodeResponse;
use Shlinkio\Shlink\Core\Action\Model\QrCodeParams;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Options\QrCodeOptions;
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
class QrCodeAction implements MiddlewareInterface
{

View File

@@ -7,9 +7,9 @@ namespace Shlinkio\Shlink\Core\Action;
use Fig\Http\Message\StatusCodeInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlRedirectionBuilderInterface;
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface;
use Shlinkio\Shlink\Core\Visit\RequestTrackerInterface;

View File

@@ -17,7 +17,7 @@ use const PHP_EOL;
class RobotsAction implements RequestHandlerInterface, StatusCodeInterface
{
public function __construct(private CrawlingHelperInterface $crawlingHelper)
public function __construct(private readonly CrawlingHelperInterface $crawlingHelper)
{
}

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