mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-04 06:13:12 +08:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20c3bde036 | ||
|
|
e77e37076f | ||
|
|
734fdf83c1 | ||
|
|
2906d42f97 | ||
|
|
0135f205df | ||
|
|
781c6e94a0 | ||
|
|
1d64dc8a26 | ||
|
|
34ff831473 | ||
|
|
3734160cb4 | ||
|
|
21234cacfb | ||
|
|
eb4dc85006 | ||
|
|
249b8a4768 | ||
|
|
1a1868c7f4 | ||
|
|
487659d5b4 | ||
|
|
f46de4d3e1 | ||
|
|
6314315db7 | ||
|
|
a22beeed08 | ||
|
|
840e377245 | ||
|
|
6fa255386b | ||
|
|
f563e777cc | ||
|
|
a63447b12b | ||
|
|
0f81c3ab92 | ||
|
|
425f254453 | ||
|
|
a9d9ec5bf9 | ||
|
|
0c5c752ffe | ||
|
|
4b556cd79f | ||
|
|
3d32a90f8e | ||
|
|
0b4c334163 | ||
|
|
312fc0984b | ||
|
|
30bf1c2641 | ||
|
|
2d1d7357a3 |
10
.travis.yml
10
.travis.yml
@@ -9,6 +9,10 @@ php:
|
||||
- 7.2
|
||||
- 7.3
|
||||
|
||||
services:
|
||||
- mysql
|
||||
- postgresql
|
||||
|
||||
before_install:
|
||||
- echo 'extension = memcached.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
|
||||
- echo 'extension = apcu.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
|
||||
@@ -19,8 +23,12 @@ install:
|
||||
- composer self-update
|
||||
- composer install --no-interaction
|
||||
|
||||
script:
|
||||
before_script:
|
||||
- mysql -e 'CREATE DATABASE shlink_test;'
|
||||
- psql -c 'create database shlink_test;' -U postgres
|
||||
- mkdir build
|
||||
|
||||
script:
|
||||
- composer ci
|
||||
|
||||
after_success:
|
||||
|
||||
71
CHANGELOG.md
71
CHANGELOG.md
@@ -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).
|
||||
|
||||
## 1.16.3 - 2019-03-30
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#153](https://github.com/shlinkio/shlink/issues/153) Updated to [doctrine/migrations](https://github.com/doctrine/migrations) version 2.0.0
|
||||
* [#376](https://github.com/shlinkio/shlink/issues/376) Allowed `visit:update-db` command to not return an error exit code even if download fails, by passing the `-i` flag.
|
||||
* [#341](https://github.com/shlinkio/shlink/issues/341) Improved database tests so that they are executed against all supported database engines.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#382](https://github.com/shlinkio/shlink/issues/382) Fixed existing short URLs not properly checked when providing the `findIfExists` flag.
|
||||
|
||||
|
||||
## 1.16.2 - 2019-03-05
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#368](https://github.com/shlinkio/shlink/issues/368) Fixed error produced when running a `SELECT COUNT(...)` with `ORDER BY` in PostgreSQL databases.
|
||||
|
||||
|
||||
## 1.16.1 - 2019-02-26
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#363](https://github.com/shlinkio/shlink/issues/363) Updated to `shlinkio/php-coding-standard` version 1.1.0
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#362](https://github.com/shlinkio/shlink/issues/362) Fixed all visits without an IP address being processed every time the `visit:process` command is executed.
|
||||
|
||||
|
||||
## 1.16.0 - 2019-02-23
|
||||
|
||||
#### Added
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"cakephp/chronos": "^1.2",
|
||||
"cocur/slugify": "^3.0",
|
||||
"doctrine/cache": "^1.6",
|
||||
"doctrine/migrations": "^1.4",
|
||||
"doctrine/migrations": "^2.0",
|
||||
"doctrine/orm": "^2.5",
|
||||
"endroid/qr-code": "^1.7",
|
||||
"firebase/php-jwt": "^4.0",
|
||||
@@ -42,7 +42,7 @@
|
||||
"zendframework/zend-expressive-fastroute": "^3.0",
|
||||
"zendframework/zend-expressive-helpers": "^5.0",
|
||||
"zendframework/zend-expressive-platesrenderer": "^2.0",
|
||||
"zendframework/zend-expressive-swoole": "^2.2",
|
||||
"zendframework/zend-expressive-swoole": "^2.4",
|
||||
"zendframework/zend-i18n": "^2.7",
|
||||
"zendframework/zend-inputfilter": "^2.8",
|
||||
"zendframework/zend-paginator": "^2.6",
|
||||
@@ -55,10 +55,10 @@
|
||||
"filp/whoops": "^2.0",
|
||||
"infection/infection": "^0.12.2",
|
||||
"phpstan/phpstan": "^0.11.2",
|
||||
"phpunit/phpcov": "^6.0@dev || ^5.0",
|
||||
"phpunit/phpcov": "^6.0 || ^5.0",
|
||||
"phpunit/phpunit": "^8.0 || ^7.5",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"shlinkio/php-coding-standard": "~1.0.0",
|
||||
"shlinkio/php-coding-standard": "~1.1.0",
|
||||
"symfony/dotenv": "^4.2",
|
||||
"symfony/var-dumper": "^4.2",
|
||||
"zendframework/zend-component-installer": "^2.1",
|
||||
@@ -110,11 +110,15 @@
|
||||
"test:ci": [
|
||||
"@test:unit:ci",
|
||||
"@test:db",
|
||||
"@test:db:mysql",
|
||||
"@test:db:postgres",
|
||||
"@test:api"
|
||||
],
|
||||
"test:unit": "phpdbg -qrr vendor/bin/phpunit --order-by=random --coverage-php build/coverage-unit.cov --testdox",
|
||||
"test:unit:ci": "phpdbg -qrr vendor/bin/phpunit --order-by=random --coverage-php build/coverage-unit.cov --coverage-clover=build/clover.xml --coverage-xml=build/coverage-xml --log-junit=build/phpunit.junit.xml --testdox",
|
||||
"test:db": "APP_ENV=test phpdbg -qrr vendor/bin/phpunit --order-by=random -c phpunit-db.xml --coverage-php build/coverage-db.cov --testdox",
|
||||
"test:db:mysql": "DB_DRIVER=mysql composer test:db",
|
||||
"test:db:postgres": "DB_DRIVER=postgres composer test:db",
|
||||
"test:api": "bin/test/run-api-tests.sh",
|
||||
|
||||
"test:pretty": [
|
||||
@@ -141,7 +145,9 @@
|
||||
"test:ci": "<fg=blue;options=bold>Runs all test suites, generating all needed reports and logs for CI envs</>",
|
||||
"test:unit": "<fg=blue;options=bold>Runs unit test suites</>",
|
||||
"test:unit:ci": "<fg=blue;options=bold>Runs unit test suites, generating all needed reports and logs for CI envs</>",
|
||||
"test:db": "<fg=blue;options=bold>Runs database test suites (covering entity repositories)</>",
|
||||
"test:db": "<fg=blue;options=bold>Runs database test suites on a SQLite database</>",
|
||||
"test:db:mysql": "<fg=blue;options=bold>Runs database test suites on a MySQL database</>",
|
||||
"test:db:postgres": "<fg=blue;options=bold>Runs database test suites on a PostgreSQL database</>",
|
||||
"test:api": "<fg=blue;options=bold>Runs API test suites</>",
|
||||
"test:pretty": "<fg=blue;options=bold>Runs all test suites and generates an HTML code coverage report</>",
|
||||
"test:unit:pretty": "<fg=blue;options=bold>Runs unit test suites and generates an HTML code coverage report</>",
|
||||
|
||||
@@ -7,6 +7,7 @@ use Monolog\Handler\RotatingFileHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Logger;
|
||||
use Monolog\Processor;
|
||||
|
||||
use const PHP_EOL;
|
||||
|
||||
return [
|
||||
|
||||
21
config/autoload/swoole.local.php.dist
Normal file
21
config/autoload/swoole.local.php.dist
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Zend\Expressive\Swoole\HotCodeReload\FileWatcher\InotifyFileWatcher;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
|
||||
return [
|
||||
|
||||
'zend-expressive-swoole' => [
|
||||
'hot-code-reload' => [
|
||||
'enable' => true,
|
||||
],
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
InotifyFileWatcher::class => InvokableFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -2,6 +2,7 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
return [
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Shlinkio\Shlink;
|
||||
use Acelaya\ExpressiveErrorHandler;
|
||||
use Zend\ConfigAggregator;
|
||||
use Zend\Expressive;
|
||||
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
return (new ConfigAggregator\ConfigAggregator([
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace ShlinkioTest\Shlink\Common;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
use function file_exists;
|
||||
use function touch;
|
||||
|
||||
@@ -19,7 +20,7 @@ $testHelper = $container->get(TestHelper::class);
|
||||
$config = $container->get('config');
|
||||
$em = $container->get(EntityManager::class);
|
||||
|
||||
$testHelper->createTestDb($config['entity_manager']['connection']['path']);
|
||||
$testHelper->createTestDb();
|
||||
ApiTest\ApiTestCase::setApiClient($container->get('shlink_test_api_client'));
|
||||
ApiTest\ApiTestCase::setSeedFixturesCallback(function () use ($testHelper, $em, $config) {
|
||||
$testHelper->seedFixtures($em, $config['data_fixtures'] ?? []);
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ShlinkioTest\Shlink\Common;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
use function file_exists;
|
||||
use function touch;
|
||||
|
||||
@@ -14,7 +15,5 @@ if (! file_exists('.env')) {
|
||||
|
||||
/** @var ContainerInterface $container */
|
||||
$container = require __DIR__ . '/../container.php';
|
||||
$config = $container->get('config');
|
||||
|
||||
$container->get(TestHelper::class)->createTestDb($config['entity_manager']['connection']['path']);
|
||||
$container->get(TestHelper::class)->createTestDb();
|
||||
DbTest\DatabaseTestCase::setEntityManager($container->get('em'));
|
||||
|
||||
@@ -4,14 +4,53 @@ declare(strict_types=1);
|
||||
namespace ShlinkioTest\Shlink;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use PDO;
|
||||
use Zend\ConfigAggregator\ConfigAggregator;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
use function sprintf;
|
||||
use function sys_get_temp_dir;
|
||||
|
||||
$swooleTestingHost = '127.0.0.1';
|
||||
$swooleTestingPort = 9999;
|
||||
|
||||
$buildDbConnection = function () {
|
||||
$driver = env('DB_DRIVER', 'sqlite');
|
||||
$isCi = env('TRAVIS', false);
|
||||
|
||||
switch ($driver) {
|
||||
case 'sqlite':
|
||||
return [
|
||||
'driver' => 'pdo_sqlite',
|
||||
'path' => sys_get_temp_dir() . '/shlink-tests.db',
|
||||
];
|
||||
case 'mysql':
|
||||
return [
|
||||
'driver' => 'pdo_mysql',
|
||||
'host' => $isCi ? '127.0.0.1' : 'shlink_db',
|
||||
'user' => 'root',
|
||||
'password' => $isCi ? '' : 'root',
|
||||
'dbname' => 'shlink_test',
|
||||
'charset' => 'utf8',
|
||||
'driverOptions' => [
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
|
||||
],
|
||||
];
|
||||
case 'postgres':
|
||||
return [
|
||||
'driver' => 'pdo_pgsql',
|
||||
'host' => $isCi ? '127.0.0.1' : 'shlink_db_postgres',
|
||||
'user' => 'postgres',
|
||||
'password' => $isCi ? '' : 'root',
|
||||
'dbname' => 'shlink_test',
|
||||
'charset' => 'utf8',
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return [
|
||||
|
||||
'debug' => true,
|
||||
@@ -48,11 +87,7 @@ return [
|
||||
],
|
||||
|
||||
'entity_manager' => [
|
||||
'connection' => [
|
||||
'driver' => 'pdo_sqlite',
|
||||
'path' => sys_get_temp_dir() . '/shlink-tests.db',
|
||||
// 'path' => __DIR__ . '/../../data/shlink-tests.db',
|
||||
],
|
||||
'connection' => $buildDbConnection(),
|
||||
],
|
||||
|
||||
'data_fixtures' => [
|
||||
|
||||
2
data/infra/database_pg/.gitignore
vendored
Executable file
2
data/infra/database_pg/.gitignore
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -1,6 +0,0 @@
|
||||
FROM mysql:5.7
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
# Enable remote access (default is localhost only, we change this
|
||||
# otherwise our database would not be reachable from outside the container)
|
||||
RUN sed -i -e"s/^bind-address\s*=\s*127.0.0.1/bind-address = 0.0.0.0/" /etc/mysql/my.cnf
|
||||
@@ -1,5 +0,0 @@
|
||||
FROM nginx:1.11.6-alpine
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
# Delete default nginx vhost
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
@@ -28,6 +28,9 @@ RUN docker-php-ext-install zip
|
||||
RUN apk add --no-cache --virtual libpng-dev
|
||||
RUN docker-php-ext-install gd
|
||||
|
||||
RUN apk add --no-cache postgresql-dev
|
||||
RUN docker-php-ext-install pdo_pgsql
|
||||
|
||||
# Install redis extension
|
||||
ADD https://github.com/phpredis/phpredis/archive/$PREDIS_VERSION.tar.gz /tmp/phpredis.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/redis\
|
||||
|
||||
@@ -5,6 +5,7 @@ ENV PREDIS_VERSION 4.2.0
|
||||
ENV MEMCACHED_VERSION 3.1.3
|
||||
ENV APCU_VERSION 5.1.16
|
||||
ENV APCU_BC_VERSION 1.0.4
|
||||
ENV INOTIFY_VERSION 2.0.0
|
||||
|
||||
RUN apk update
|
||||
|
||||
@@ -27,6 +28,9 @@ RUN docker-php-ext-install zip
|
||||
RUN apk add --no-cache --virtual libpng-dev
|
||||
RUN docker-php-ext-install gd
|
||||
|
||||
RUN apk add --no-cache postgresql-dev
|
||||
RUN docker-php-ext-install pdo_pgsql
|
||||
|
||||
# Install redis extension
|
||||
ADD https://github.com/phpredis/phpredis/archive/$PREDIS_VERSION.tar.gz /tmp/phpredis.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/redis\
|
||||
@@ -73,6 +77,16 @@ RUN rm /tmp/apcu_bc.tar.gz
|
||||
RUN rm /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini
|
||||
RUN echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini
|
||||
|
||||
# Install inotify extension
|
||||
ADD https://pecl.php.net/get/inotify-$INOTIFY_VERSION.tgz /tmp/inotify.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/inotify\
|
||||
&& tar xf /tmp/inotify.tar.gz -C /usr/src/php/ext/inotify --strip-components=1
|
||||
# configure and install
|
||||
RUN docker-php-ext-configure inotify\
|
||||
&& docker-php-ext-install inotify
|
||||
# cleanup
|
||||
RUN rm /tmp/inotify.tar.gz
|
||||
|
||||
# Install swoole
|
||||
# First line fixes an error when installing pecl extensions. Found in https://github.com/docker-library/php/issues/233
|
||||
RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS && \
|
||||
|
||||
@@ -3,21 +3,24 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Migrations\AbstractMigration;
|
||||
use Doctrine\DBAL\DBALException;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
class Version20160819142757 extends AbstractMigration
|
||||
{
|
||||
const MYSQL = 'mysql';
|
||||
const SQLITE = 'sqlite';
|
||||
private const MYSQL = 'mysql';
|
||||
private const SQLITE = 'sqlite';
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws DBALException
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema)
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$db = $this->connection->getDatabasePlatform()->getName();
|
||||
$table = $schema->getTable('short_urls');
|
||||
@@ -31,9 +34,9 @@ class Version20160819142757 extends AbstractMigration
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws DBALException
|
||||
*/
|
||||
public function down(Schema $schema)
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$db = $this->connection->getDatabasePlatform()->getName();
|
||||
}
|
||||
|
||||
@@ -3,19 +3,16 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Migrations\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
class Version20160820191203 extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* @param Schema $schema
|
||||
*/
|
||||
public function up(Schema $schema)
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// Check if the tables already exist
|
||||
$tables = $schema->getTables();
|
||||
@@ -29,7 +26,7 @@ class Version20160820191203 extends AbstractMigration
|
||||
$this->createShortUrlsInTagsTable($schema);
|
||||
}
|
||||
|
||||
protected function createTagsTable(Schema $schema)
|
||||
private function createTagsTable(Schema $schema): void
|
||||
{
|
||||
$table = $schema->createTable('tags');
|
||||
$table->addColumn('id', Type::BIGINT, [
|
||||
@@ -46,7 +43,7 @@ class Version20160820191203 extends AbstractMigration
|
||||
$table->setPrimaryKey(['id']);
|
||||
}
|
||||
|
||||
protected function createShortUrlsInTagsTable(Schema $schema)
|
||||
private function createShortUrlsInTagsTable(Schema $schema): void
|
||||
{
|
||||
$table = $schema->createTable('short_urls_in_tags');
|
||||
$table->addColumn('short_url_id', Type::BIGINT, [
|
||||
@@ -70,10 +67,7 @@ class Version20160820191203 extends AbstractMigration
|
||||
$table->setPrimaryKey(['short_url_id', 'tag_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
*/
|
||||
public function down(Schema $schema)
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$schema->dropTable('short_urls_in_tags');
|
||||
$schema->dropTable('tags');
|
||||
|
||||
@@ -3,10 +3,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Migrations\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
@@ -14,10 +14,9 @@ use Doctrine\DBAL\Types\Type;
|
||||
class Version20171021093246 extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema)
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
if ($shortUrls->hasColumn('valid_since')) {
|
||||
@@ -33,10 +32,9 @@ class Version20171021093246 extends AbstractMigration
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function down(Schema $schema)
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
if (! $shortUrls->hasColumn('valid_since')) {
|
||||
|
||||
@@ -3,10 +3,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Migrations\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
@@ -14,10 +14,9 @@ use Doctrine\DBAL\Types\Type;
|
||||
class Version20171022064541 extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema)
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
if ($shortUrls->hasColumn('max_visits')) {
|
||||
@@ -31,10 +30,9 @@ class Version20171022064541 extends AbstractMigration
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function down(Schema $schema)
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
if (! $shortUrls->hasColumn('max_visits')) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
version: '2'
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
shlink_php:
|
||||
@@ -12,3 +12,15 @@ services:
|
||||
volumes:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
|
||||
shlink_db:
|
||||
user: 1000:1000
|
||||
volumes:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
|
||||
shlink_db_postgres:
|
||||
user: 1000:1000
|
||||
volumes:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
version: '2'
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
shlink_nginx:
|
||||
container_name: shlink_nginx
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./data/infra/nginx.Dockerfile
|
||||
image: nginx:1.15.9-alpine
|
||||
ports:
|
||||
- "8000:80"
|
||||
volumes:
|
||||
- ./:/home/shlink/www
|
||||
- ./docs:/home/shlink/www/public/docs
|
||||
- ./data/infra/vhost.conf:/etc/nginx/conf.d/shlink-vhost.conf
|
||||
- ./data/infra/vhost.conf:/etc/nginx/conf.d/default.conf
|
||||
links:
|
||||
- shlink_php
|
||||
|
||||
@@ -25,6 +23,7 @@ services:
|
||||
- ./data/infra/php.ini:/usr/local/etc/php/php.ini
|
||||
links:
|
||||
- shlink_db
|
||||
- shlink_db_postgres
|
||||
|
||||
shlink_swoole:
|
||||
container_name: shlink_swoole
|
||||
@@ -37,12 +36,11 @@ services:
|
||||
- ./:/home/shlink
|
||||
links:
|
||||
- shlink_db
|
||||
- shlink_db_postgres
|
||||
|
||||
shlink_db:
|
||||
container_name: shlink_db
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./data/infra/db.Dockerfile
|
||||
image: mysql:5.7
|
||||
ports:
|
||||
- "3307:3306"
|
||||
volumes:
|
||||
@@ -51,3 +49,16 @@ services:
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_DATABASE: shlink
|
||||
|
||||
shlink_db_postgres:
|
||||
container_name: shlink_db_postgres
|
||||
image: postgres:10.7-alpine
|
||||
ports:
|
||||
- "5433:5432"
|
||||
volumes:
|
||||
- ./:/home/shlink/www
|
||||
- ./data/infra/database_pg:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_PASSWORD: root
|
||||
POSTGRES_DB: shlink
|
||||
PGDATA: /var/lib/postgresql/data/pgdata
|
||||
|
||||
@@ -11,6 +11,7 @@ use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class DisableKeyCommand extends Command
|
||||
|
||||
@@ -11,6 +11,7 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class GenerateKeyCommand extends Command
|
||||
|
||||
@@ -11,6 +11,7 @@ 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 function array_filter;
|
||||
use function array_map;
|
||||
use function sprintf;
|
||||
|
||||
@@ -9,6 +9,7 @@ use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function sprintf;
|
||||
use function str_shuffle;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/** @deprecated */
|
||||
|
||||
@@ -12,6 +12,7 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class DeleteShortUrlCommand extends Command
|
||||
|
||||
@@ -11,6 +11,7 @@ use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class GeneratePreviewCommand extends Command
|
||||
|
||||
@@ -17,6 +17,7 @@ use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\Diactoros\Uri;
|
||||
|
||||
use function array_map;
|
||||
use function Functional\curry;
|
||||
use function Functional\flatten;
|
||||
|
||||
@@ -17,6 +17,7 @@ use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\Stdlib\ArrayUtils;
|
||||
|
||||
use function array_map;
|
||||
use function Functional\select_keys;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function explode;
|
||||
|
||||
@@ -12,6 +12,7 @@ use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class ResolveUrlCommand extends Command
|
||||
|
||||
@@ -10,6 +10,7 @@ use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function Functional\map;
|
||||
|
||||
class ListTagsCommand extends Command
|
||||
|
||||
@@ -11,6 +11,7 @@ use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class RenameTagCommand extends Command
|
||||
|
||||
@@ -17,6 +17,7 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Lock\Factory as Locker;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class ProcessVisitsCommand extends Command
|
||||
@@ -65,7 +66,11 @@ class ProcessVisitsCommand extends Command
|
||||
$this->visitService->locateUnlocatedVisits(
|
||||
[$this, 'getGeolocationDataForVisit'],
|
||||
function (VisitLocation $location) use ($output) {
|
||||
$output->writeln(sprintf(' [<info>Address located at "%s"</info>]', $location->getCountryName()));
|
||||
if (! $location->isEmpty()) {
|
||||
$output->writeln(
|
||||
sprintf(' [<info>Address located at "%s"</info>]', $location->getCountryName())
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -83,14 +88,14 @@ class ProcessVisitsCommand extends Command
|
||||
'<comment>Ignored visit with no IP address</comment>',
|
||||
OutputInterface::VERBOSITY_VERBOSE
|
||||
);
|
||||
throw new IpCannotBeLocatedException('Ignored visit with no IP address');
|
||||
throw IpCannotBeLocatedException::forEmptyAddress();
|
||||
}
|
||||
|
||||
$ipAddr = $visit->getRemoteAddr();
|
||||
$this->output->write(sprintf('Processing IP <fg=blue>%s</>', $ipAddr));
|
||||
if ($ipAddr === IpAddress::LOCALHOST) {
|
||||
$this->output->writeln(' [<comment>Ignored localhost address</comment>]');
|
||||
throw new IpCannotBeLocatedException('Ignored localhost address');
|
||||
throw IpCannotBeLocatedException::forLocalhost();
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -101,7 +106,7 @@ class ProcessVisitsCommand extends Command
|
||||
$this->getApplication()->renderException($e, $this->output);
|
||||
}
|
||||
|
||||
throw new IpCannotBeLocatedException('An error occurred while locating IP', $e->getCode(), $e);
|
||||
throw IpCannotBeLocatedException::forError($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,12 @@ use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class UpdateDbCommand extends Command
|
||||
{
|
||||
public const NAME = 'visit:update-db';
|
||||
@@ -33,6 +36,12 @@ class UpdateDbCommand extends Command
|
||||
->setHelp(
|
||||
'The GeoLite2 database is updated first Tuesday every month, so this command should be ideally run '
|
||||
. 'every first Wednesday'
|
||||
)
|
||||
->addOption(
|
||||
'ignoreErrors',
|
||||
'i',
|
||||
InputOption::VALUE_NONE,
|
||||
'Makes the command success even iof the update fails.'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,19 +58,32 @@ class UpdateDbCommand extends Command
|
||||
});
|
||||
|
||||
$progressBar->finish();
|
||||
$io->writeln('');
|
||||
$io->newLine();
|
||||
|
||||
$io->success('GeoLite2 database properly updated');
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
} catch (RuntimeException $e) {
|
||||
$progressBar->finish();
|
||||
$io->writeln('');
|
||||
$io->newLine();
|
||||
|
||||
$io->error('An error occurred while updating GeoLite2 database');
|
||||
if ($io->isVerbose()) {
|
||||
$this->getApplication()->renderException($e, $output);
|
||||
}
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
return $this->handleError($e, $io, $input);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleError(RuntimeException $e, SymfonyStyle $io, InputInterface $input): int
|
||||
{
|
||||
$ignoreErrors = $input->getOption('ignoreErrors');
|
||||
$baseErrorMsg = 'An error occurred while updating GeoLite2 database';
|
||||
|
||||
if ($ignoreErrors) {
|
||||
$io->warning(sprintf('%s, but it was ignored', $baseErrorMsg));
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
$io->error($baseErrorMsg);
|
||||
if ($io->isVerbose()) {
|
||||
$this->getApplication()->renderException($e, $io);
|
||||
}
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\Config\GenerateCharsetCommand;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use function implode;
|
||||
use function sort;
|
||||
use function str_split;
|
||||
|
||||
@@ -11,7 +11,9 @@ use Shlinkio\Shlink\Core\Exception;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use const PHP_EOL;
|
||||
|
||||
use function array_pop;
|
||||
use function sprintf;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\Paginator\Adapter\ArrayAdapter;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
use function count;
|
||||
use function substr_count;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use const PHP_EOL;
|
||||
|
||||
class ResolveUrlCommandTest extends TestCase
|
||||
|
||||
@@ -20,6 +20,7 @@ use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\Lock;
|
||||
|
||||
use function array_shift;
|
||||
use function sprintf;
|
||||
|
||||
@@ -116,6 +117,11 @@ class ProcessVisitsCommandTest extends TestCase
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertStringContainsString($message, $output);
|
||||
if (empty($address)) {
|
||||
$this->assertStringNotContainsString('Processing IP', $output);
|
||||
} else {
|
||||
$this->assertStringContainsString('Processing IP', $output);
|
||||
}
|
||||
$locateVisits->shouldHaveBeenCalledOnce();
|
||||
$resolveIpLocation->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Visit\UpdateDbCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
@@ -31,27 +32,45 @@ class UpdateDbCommandTest extends TestCase
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function successMessageIsPrintedIfEverythingWorks()
|
||||
public function successMessageIsPrintedIfEverythingWorks(): void
|
||||
{
|
||||
$download = $this->dbUpdater->downloadFreshCopy(Argument::type('callable'))->will(function () {
|
||||
});
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$exitCode = $this->commandTester->getStatusCode();
|
||||
|
||||
$this->assertStringContainsString('GeoLite2 database properly updated', $output);
|
||||
$this->assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode);
|
||||
$download->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function errorMessageIsPrintedIfAnExceptionIsThrown()
|
||||
public function errorMessageIsPrintedIfAnExceptionIsThrown(): void
|
||||
{
|
||||
$download = $this->dbUpdater->downloadFreshCopy(Argument::type('callable'))->willThrow(RuntimeException::class);
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$exitCode = $this->commandTester->getStatusCode();
|
||||
|
||||
$this->assertStringContainsString('An error occurred while updating GeoLite2 database', $output);
|
||||
$this->assertEquals(ExitCodes::EXIT_FAILURE, $exitCode);
|
||||
$download->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function warningMessageIsPrintedIfAnExceptionIsThrownAndErrorsAreIgnored(): void
|
||||
{
|
||||
$download = $this->dbUpdater->downloadFreshCopy(Argument::type('callable'))->willThrow(RuntimeException::class);
|
||||
|
||||
$this->commandTester->execute(['--ignoreErrors' => true]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$exitCode = $this->commandTester->getStatusCode();
|
||||
|
||||
$this->assertStringContainsString('ignored', $output);
|
||||
$this->assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode);
|
||||
$download->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
use function array_merge;
|
||||
|
||||
class ApplicationFactoryTest extends TestCase
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Common;
|
||||
|
||||
use const JSON_ERROR_NONE;
|
||||
|
||||
use function getenv;
|
||||
use function json_decode as spl_json_decode;
|
||||
use function json_last_error;
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Common\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class WrongIpException extends RuntimeException
|
||||
|
||||
@@ -11,6 +11,7 @@ use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
|
||||
use function Functional\contains;
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
use function sys_get_temp_dir;
|
||||
|
||||
@@ -10,6 +10,7 @@ use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\AbstractFactoryInterface;
|
||||
|
||||
use function array_shift;
|
||||
use function explode;
|
||||
use function is_array;
|
||||
|
||||
@@ -9,6 +9,7 @@ use Interop\Container\Exception\ContainerException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
|
||||
use function count;
|
||||
use function explode;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
||||
use Symfony\Component\Filesystem\Exception as FilesystemException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Throwable;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class DbUpdater implements DbUpdaterInterface
|
||||
|
||||
@@ -9,6 +9,7 @@ use GeoIp2\Model\City;
|
||||
use GeoIp2\Record\Subdivision;
|
||||
use MaxMind\Db\Reader\InvalidDatabaseException;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
|
||||
use function Functional\first;
|
||||
|
||||
class GeoLite2LocationResolver implements IpLocationResolverInterface
|
||||
|
||||
@@ -7,6 +7,7 @@ use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
|
||||
use function Shlinkio\Shlink\Common\json_decode;
|
||||
use function sprintf;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Common\Logger\Processor;
|
||||
|
||||
use const PHP_EOL;
|
||||
|
||||
use function str_replace;
|
||||
use function strpos;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface as DelegateInterface;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
use function count;
|
||||
use function explode;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Shlinkio\Shlink\Common\Paginator\Adapter;
|
||||
|
||||
use Shlinkio\Shlink\Common\Repository\PaginableRepositoryInterface;
|
||||
use Zend\Paginator\Adapter\AdapterInterface;
|
||||
|
||||
use function strip_tags;
|
||||
use function trim;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Common\Paginator\Util;
|
||||
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
|
||||
use Zend\Paginator\Paginator;
|
||||
use Zend\Stdlib\ArrayUtils;
|
||||
|
||||
use function array_map;
|
||||
use function sprintf;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use Fig\Http\Message\StatusCodeInterface as StatusCode;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Zend\Diactoros\Response;
|
||||
use Zend\Diactoros\Stream;
|
||||
|
||||
use function base64_decode;
|
||||
|
||||
class PixelResponse extends Response
|
||||
|
||||
@@ -7,6 +7,7 @@ use mikehaertl\wkhtmlto\Image;
|
||||
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
|
||||
use Shlinkio\Shlink\Common\Image\ImageBuilderInterface;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
use function sprintf;
|
||||
use function urlencode;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Common\Util;
|
||||
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
|
||||
use function count;
|
||||
use function explode;
|
||||
use function implode;
|
||||
|
||||
@@ -9,6 +9,7 @@ use Psr\Http\Message\ResponseInterface;
|
||||
use Zend\Diactoros\Response;
|
||||
use Zend\Diactoros\Stream;
|
||||
use Zend\Stdlib\ArrayUtils;
|
||||
|
||||
use const FILEINFO_MIME;
|
||||
|
||||
trait ResponseUtilsTrait
|
||||
|
||||
@@ -10,6 +10,7 @@ use GuzzleHttp\RequestOptions;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Shlinkio\Shlink\Rest\Authentication\Plugin\ApiKeyHeaderPlugin;
|
||||
|
||||
use function Shlinkio\Shlink\Common\json_decode;
|
||||
use function sprintf;
|
||||
|
||||
|
||||
@@ -8,16 +8,14 @@ use Doctrine\Common\DataFixtures\Loader;
|
||||
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Process\Process;
|
||||
use function file_exists;
|
||||
use function unlink;
|
||||
|
||||
class TestHelper
|
||||
{
|
||||
public function createTestDb(string $shlinkDbPath): void
|
||||
public function createTestDb(): void
|
||||
{
|
||||
if (file_exists($shlinkDbPath)) {
|
||||
unlink($shlinkDbPath);
|
||||
}
|
||||
$process = new Process(['vendor/bin/doctrine', 'orm:schema-tool:drop', '--force', '--no-interaction', '-q']);
|
||||
$process->inheritEnvironmentVariables()
|
||||
->mustRun();
|
||||
|
||||
$process = new Process(['vendor/bin/doctrine', 'orm:schema-tool:create', '--no-interaction', '-q']);
|
||||
$process->inheritEnvironmentVariables()
|
||||
|
||||
@@ -12,6 +12,7 @@ use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Common\Factory\CacheFactory;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
use function count;
|
||||
use function putenv;
|
||||
use function realpath;
|
||||
|
||||
@@ -7,6 +7,7 @@ use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\EmptyIpLocationResolver;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
|
||||
use function Functional\map;
|
||||
use function range;
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\IpApiLocationResolver;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
|
||||
|
||||
use function json_encode;
|
||||
|
||||
class IpApiLocationResolverTest extends TestCase
|
||||
|
||||
@@ -6,7 +6,9 @@ namespace ShlinkioTest\Shlink\Common\Logger\Processor;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Common\Logger\Processor\ExceptionWithNewLineProcessor;
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
|
||||
use const PHP_EOL;
|
||||
|
||||
use function Functional\map;
|
||||
use function range;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ use Shlinkio\Shlink\Common\Image\ImageBuilder;
|
||||
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
use function sprintf;
|
||||
use function urlencode;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace ShlinkioTest\Shlink\Common\Util;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
|
||||
use function Functional\map;
|
||||
use function range;
|
||||
use function strlen;
|
||||
|
||||
@@ -58,6 +58,6 @@ $builder->createOneToMany('visits', Entity\Visit::class)
|
||||
|
||||
$builder->createManyToMany('tags', Entity\Tag::class)
|
||||
->setJoinTable('short_urls_in_tags')
|
||||
->addInverseJoinColumn('tag_id', 'id')
|
||||
->addJoinColumn('short_url_id', 'id')
|
||||
->addInverseJoinColumn('tag_id', 'id', true, false, 'CASCADE')
|
||||
->addJoinColumn('short_url_id', 'id', true, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
@@ -43,10 +43,10 @@ $builder->createField('userAgent', Type::STRING)
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('shortUrl', Entity\ShortUrl::class)
|
||||
->addJoinColumn('short_url_id', 'id', false)
|
||||
->addJoinColumn('short_url_id', 'id', false, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('visitLocation', Entity\VisitLocation::class)
|
||||
->addJoinColumn('visit_location_id', 'id')
|
||||
->addJoinColumn('visit_location_id', 'id', true, false, 'Set NULL')
|
||||
->cascadePersist()
|
||||
->build();
|
||||
|
||||
@@ -15,6 +15,7 @@ use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||
|
||||
use function array_key_exists;
|
||||
|
||||
abstract class AbstractTrackingAction implements MiddlewareInterface
|
||||
|
||||
@@ -8,6 +8,7 @@ use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
|
||||
use function count;
|
||||
|
||||
class ShortUrl extends AbstractEntity
|
||||
|
||||
@@ -72,4 +72,16 @@ class VisitLocation extends AbstractEntity implements VisitLocationInterface
|
||||
'timezone' => $this->timezone,
|
||||
];
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return
|
||||
$this->countryCode === '' &&
|
||||
$this->countryName === '' &&
|
||||
$this->regionName === '' &&
|
||||
$this->cityName === '' &&
|
||||
((float) $this->latitude) === 0.0 &&
|
||||
((float) $this->longitude) === 0.0 &&
|
||||
$this->timezone === '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Core\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class DeleteShortUrlException extends RuntimeException
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Core\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class InvalidShortCodeException extends RuntimeException
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Core\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class InvalidUrlException extends RuntimeException
|
||||
|
||||
@@ -3,6 +3,43 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
class IpCannotBeLocatedException extends RuntimeException
|
||||
{
|
||||
/** @var bool */
|
||||
private $isNonLocatableAddress;
|
||||
|
||||
public function __construct(
|
||||
bool $isNonLocatableAddress,
|
||||
string $message,
|
||||
int $code = 0,
|
||||
?Throwable $previous = null
|
||||
) {
|
||||
$this->isNonLocatableAddress = $isNonLocatableAddress;
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public static function forEmptyAddress(): self
|
||||
{
|
||||
return new self(true, 'Ignored visit with no IP address');
|
||||
}
|
||||
|
||||
public static function forLocalhost(): self
|
||||
{
|
||||
return new self(true, 'Ignored localhost address');
|
||||
}
|
||||
|
||||
public static function forError(Throwable $e): self
|
||||
{
|
||||
return new self(false, 'An error occurred while locating IP', $e->getCode(), $e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if this error belongs to an address that will never be possible locate
|
||||
*/
|
||||
public function isNonLocatableAddress(): bool
|
||||
{
|
||||
return $this->isNonLocatableAddress;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ namespace Shlinkio\Shlink\Core\Exception;
|
||||
|
||||
use Throwable;
|
||||
use Zend\InputFilter\InputFilterInterface;
|
||||
|
||||
use const PHP_EOL;
|
||||
|
||||
use function is_array;
|
||||
use function print_r;
|
||||
use function sprintf;
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Shlinkio\Shlink\Core\Options;
|
||||
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
use Zend\Stdlib\AbstractOptions;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class AppOptions extends AbstractOptions
|
||||
|
||||
@@ -7,6 +7,7 @@ use Cake\Chronos\Chronos;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
|
||||
use function array_column;
|
||||
use function array_key_exists;
|
||||
use function Functional\contains;
|
||||
|
||||
@@ -50,7 +50,8 @@ DQL;
|
||||
?int $offset = null
|
||||
): array {
|
||||
$qb = $this->createVisitsByShortCodeQueryBuilder($shortCode, $dateRange);
|
||||
$qb->select('v');
|
||||
$qb->select('v')
|
||||
->orderBy('v.date', 'DESC');
|
||||
|
||||
if ($limit !== null) {
|
||||
$qb->setMaxResults($limit);
|
||||
@@ -76,8 +77,7 @@ DQL;
|
||||
$qb->from(Visit::class, 'v')
|
||||
->join('v.shortUrl', 'su')
|
||||
->where($qb->expr()->eq('su.shortCode', ':shortCode'))
|
||||
->setParameter('shortCode', $shortCode)
|
||||
->orderBy('v.date', 'DESC') ;
|
||||
->setParameter('shortCode', $shortCode);
|
||||
|
||||
// Apply date range filtering
|
||||
if ($dateRange !== null && $dateRange->getStartDate() !== null) {
|
||||
|
||||
@@ -9,6 +9,7 @@ use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Zend\Diactoros\Response;
|
||||
use Zend\Expressive\Template\TemplateRendererInterface;
|
||||
|
||||
use function array_shift;
|
||||
use function explode;
|
||||
use function Functional\contains;
|
||||
|
||||
@@ -20,6 +20,7 @@ use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
|
||||
use Throwable;
|
||||
|
||||
use function array_reduce;
|
||||
use function count;
|
||||
use function floor;
|
||||
@@ -111,32 +112,39 @@ class UrlShortener implements UrlShortenerInterface
|
||||
if ($meta->hasCustomSlug()) {
|
||||
$criteria['shortCode'] = $meta->getCustomSlug();
|
||||
}
|
||||
/** @var ShortUrl|null $shortUrl */
|
||||
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy($criteria);
|
||||
if ($shortUrl === null) {
|
||||
/** @var ShortUrl[] $shortUrls */
|
||||
$shortUrls = $this->em->getRepository(ShortUrl::class)->findBy($criteria);
|
||||
if (empty($shortUrls)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($meta->hasMaxVisits() && $meta->getMaxVisits() !== $shortUrl->getMaxVisits()) {
|
||||
return null;
|
||||
}
|
||||
if ($meta->hasValidSince() && ! $meta->getValidSince()->eq($shortUrl->getValidSince())) {
|
||||
return null;
|
||||
}
|
||||
if ($meta->hasValidUntil() && ! $meta->getValidUntil()->eq($shortUrl->getValidUntil())) {
|
||||
return null;
|
||||
}
|
||||
// Iterate short URLs until one that matches is found, or return null otherwise
|
||||
return array_reduce($shortUrls, function (?ShortUrl $found, ShortUrl $shortUrl) use ($tags, $meta) {
|
||||
if ($found) {
|
||||
return $found;
|
||||
}
|
||||
|
||||
$shortUrlTags = invoke($shortUrl->getTags(), '__toString');
|
||||
$hasAllTags = count($shortUrlTags) === count($tags) && array_reduce(
|
||||
$tags,
|
||||
function (bool $hasAllTags, string $tag) use ($shortUrlTags) {
|
||||
return $hasAllTags && contains($shortUrlTags, $tag);
|
||||
},
|
||||
true
|
||||
);
|
||||
if ($meta->hasMaxVisits() && $meta->getMaxVisits() !== $shortUrl->getMaxVisits()) {
|
||||
return null;
|
||||
}
|
||||
if ($meta->hasValidSince() && ! $meta->getValidSince()->eq($shortUrl->getValidSince())) {
|
||||
return null;
|
||||
}
|
||||
if ($meta->hasValidUntil() && ! $meta->getValidUntil()->eq($shortUrl->getValidUntil())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $hasAllTags ? $shortUrl : null;
|
||||
$shortUrlTags = invoke($shortUrl->getTags(), '__toString');
|
||||
$hasAllTags = count($shortUrlTags) === count($tags) && array_reduce(
|
||||
$tags,
|
||||
function (bool $hasAllTags, string $tag) use ($shortUrlTags) {
|
||||
return $hasAllTags && contains($shortUrlTags, $tag);
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
return $hasAllTags ? $shortUrl : null;
|
||||
});
|
||||
}
|
||||
|
||||
private function checkUrlExists(string $url): void
|
||||
|
||||
@@ -30,12 +30,18 @@ class VisitService implements VisitServiceInterface
|
||||
|
||||
foreach ($results as $visit) {
|
||||
$count++;
|
||||
|
||||
try {
|
||||
/** @var Location $location */
|
||||
$location = $geolocateVisit($visit);
|
||||
} catch (IpCannotBeLocatedException $e) {
|
||||
// Skip if the visit's IP could not be located
|
||||
continue;
|
||||
if (! $e->isNonLocatableAddress()) {
|
||||
// Skip if the visit's IP could not be located because of an error
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the IP address is non-locatable, locate it as empty to prevent next processes to pick it again
|
||||
$location = Location::emptyInstance();
|
||||
}
|
||||
|
||||
$location = new VisitLocation($location);
|
||||
|
||||
@@ -12,6 +12,7 @@ use Shlinkio\Shlink\Core\Model\VisitsParams;
|
||||
use Shlinkio\Shlink\Core\Paginator\Adapter\VisitsPaginatorAdapter;
|
||||
use Shlinkio\Shlink\Core\Repository\VisitRepository;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class VisitsTracker implements VisitsTrackerInterface
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core\Transformer;
|
||||
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Util\ShortUrlBuilderTrait;
|
||||
|
||||
use function Functional\invoke;
|
||||
|
||||
class ShortUrlDataTransformer implements DataTransformerInterface
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core\Util;
|
||||
use Doctrine\Common\Collections;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
|
||||
use function str_replace;
|
||||
use function strtolower;
|
||||
use function trim;
|
||||
|
||||
@@ -12,14 +12,15 @@ use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
use ShlinkioTest\Shlink\Common\DbTest\DatabaseTestCase;
|
||||
|
||||
use function count;
|
||||
|
||||
class ShortUrlRepositoryTest extends DatabaseTestCase
|
||||
{
|
||||
protected const ENTITIES_TO_EMPTY = [
|
||||
ShortUrl::class,
|
||||
Visit::class,
|
||||
Tag::class,
|
||||
Visit::class,
|
||||
ShortUrl::class,
|
||||
];
|
||||
|
||||
/** @var ShortUrlRepository */
|
||||
|
||||
@@ -12,6 +12,7 @@ use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Repository\VisitRepository;
|
||||
use ShlinkioTest\Shlink\Common\DbTest\DatabaseTestCase;
|
||||
|
||||
use function Functional\map;
|
||||
use function range;
|
||||
use function sprintf;
|
||||
|
||||
@@ -17,7 +17,9 @@ use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use ShlinkioTest\Shlink\Common\Util\TestUtils;
|
||||
use Zend\Diactoros\Response;
|
||||
use Zend\Diactoros\ServerRequest;
|
||||
|
||||
use const FILEINFO_MIME;
|
||||
|
||||
use function filesize;
|
||||
|
||||
class PreviewActionTest extends TestCase
|
||||
|
||||
@@ -18,4 +18,28 @@ class VisitLocationTest extends TestCase
|
||||
$this->assertSame('1000.7', $location->getLatitude());
|
||||
$this->assertSame('-2000.4', $location->getLongitude());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideArgs
|
||||
*/
|
||||
public function isEmptyReturnsTrueWhenAllValuesAreEmpty(array $args, bool $isEmpty): void
|
||||
{
|
||||
$payload = new Location(...$args);
|
||||
$location = new VisitLocation($payload);
|
||||
|
||||
$this->assertEquals($isEmpty, $location->isEmpty());
|
||||
}
|
||||
|
||||
public function provideArgs(): iterable
|
||||
{
|
||||
yield [['', '', '', '', 0.0, 0.0, ''], true];
|
||||
yield [['', '', '', '', 0.0, 0.0, 'dd'], false];
|
||||
yield [['', '', '', 'dd', 0.0, 0.0, ''], false];
|
||||
yield [['', '', 'dd', '', 0.0, 0.0, ''], false];
|
||||
yield [['', 'dd', '', '', 0.0, 0.0, ''], false];
|
||||
yield [['dd', '', '', '', 0.0, 0.0, ''], false];
|
||||
yield [['', '', '', '', 1.0, 0.0, ''], false];
|
||||
yield [['', '', '', '', 0.0, 1.0, ''], false];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\Core\Exception;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
use Shlinkio\Shlink\Core\Exception\DeleteShortUrlException;
|
||||
|
||||
use function Functional\map;
|
||||
use function range;
|
||||
use function sprintf;
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Exception;
|
||||
|
||||
use Exception;
|
||||
use LogicException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||
use Shlinkio\Shlink\Core\Exception\RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
use function count;
|
||||
use function func_get_args;
|
||||
use function random_int;
|
||||
|
||||
class IpCannotBeLocatedExceptionTest extends TestCase
|
||||
{
|
||||
/** @test */
|
||||
public function forEmptyAddressInitializesException(): void
|
||||
{
|
||||
$e = IpCannotBeLocatedException::forEmptyAddress();
|
||||
|
||||
$this->assertTrue($e->isNonLocatableAddress());
|
||||
$this->assertEquals('Ignored visit with no IP address', $e->getMessage());
|
||||
$this->assertEquals(0, $e->getCode());
|
||||
$this->assertNull($e->getPrevious());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function forLocalhostInitializesException(): void
|
||||
{
|
||||
$e = IpCannotBeLocatedException::forLocalhost();
|
||||
|
||||
$this->assertTrue($e->isNonLocatableAddress());
|
||||
$this->assertEquals('Ignored localhost address', $e->getMessage());
|
||||
$this->assertEquals(0, $e->getCode());
|
||||
$this->assertNull($e->getPrevious());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideErrors
|
||||
*/
|
||||
public function forErrorInitializesException(Throwable $prev): void
|
||||
{
|
||||
$e = IpCannotBeLocatedException::forError($prev);
|
||||
|
||||
$this->assertFalse($e->isNonLocatableAddress());
|
||||
$this->assertEquals('An error occurred while locating IP', $e->getMessage());
|
||||
$this->assertEquals($prev->getCode(), $e->getCode());
|
||||
$this->assertSame($prev, $e->getPrevious());
|
||||
}
|
||||
|
||||
public function provideErrors(): iterable
|
||||
{
|
||||
yield 'Simple exception with positive code' => [new Exception('Some message', 100)];
|
||||
yield 'Runtime exception with negative code' => [new RuntimeException('Something went wrong', -50)];
|
||||
yield 'Logic exception with default code' => [new LogicException('Conditions unmet')];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideConstructorArgs
|
||||
*/
|
||||
public function constructorInitializesException(): void
|
||||
{
|
||||
$args = func_get_args();
|
||||
[$isNonLocatableAddress, $message] = $args;
|
||||
$code = $args[2] ?? 0;
|
||||
$prev = $args[3] ?? null;
|
||||
|
||||
switch (count($args)) {
|
||||
case 2:
|
||||
$e = new IpCannotBeLocatedException($isNonLocatableAddress, $message);
|
||||
break;
|
||||
case 3:
|
||||
$e = new IpCannotBeLocatedException($isNonLocatableAddress, $message, $code);
|
||||
break;
|
||||
default:
|
||||
$e = new IpCannotBeLocatedException($isNonLocatableAddress, $message, $code, $prev);
|
||||
}
|
||||
|
||||
$this->assertEquals($isNonLocatableAddress, $e->isNonLocatableAddress());
|
||||
$this->assertEquals($message, $e->getMessage());
|
||||
$this->assertEquals($code, $e->getCode());
|
||||
$this->assertEquals($prev, $e->getPrevious());
|
||||
}
|
||||
|
||||
public function provideConstructorArgs(): iterable
|
||||
{
|
||||
yield 'without default args' => [true, 'Message'];
|
||||
yield 'without prev' => [true, 'Message', random_int(1, 100)];
|
||||
yield 'without all args' => [false, 'Foo', random_int(1, 100), new RuntimeException('Foo')];
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Options\DeleteShortUrlsOptions;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlService;
|
||||
|
||||
use function Functional\map;
|
||||
use function range;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
||||
|
||||
use function count;
|
||||
|
||||
class ShortUrlServiceTest extends TestCase
|
||||
|
||||
@@ -27,6 +27,8 @@ use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Zend\Diactoros\Uri;
|
||||
|
||||
use function array_map;
|
||||
|
||||
class UrlShortenerTest extends TestCase
|
||||
{
|
||||
/** @var UrlShortener */
|
||||
@@ -121,7 +123,7 @@ class UrlShortenerTest extends TestCase
|
||||
{
|
||||
$repo = $this->prophesize(ShortUrlRepository::class);
|
||||
$countBySlug = $repo->count(['shortCode' => 'custom-slug'])->willReturn(1);
|
||||
$repo->findOneBy(Argument::cetera())->willReturn(null);
|
||||
$repo->findBy(Argument::cetera())->willReturn([]);
|
||||
$getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||
|
||||
$countBySlug->shouldBeCalledOnce();
|
||||
@@ -146,20 +148,23 @@ class UrlShortenerTest extends TestCase
|
||||
?ShortUrl $expected
|
||||
): void {
|
||||
$repo = $this->prophesize(ShortUrlRepository::class);
|
||||
$findExisting = $repo->findOneBy(Argument::any())->willReturn($expected);
|
||||
$findExisting = $repo->findBy(Argument::any())->willReturn($expected !== null ? [$expected] : []);
|
||||
$getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||
|
||||
$result = $this->urlShortener->urlToShortCode(new Uri($url), $tags, $meta);
|
||||
|
||||
$this->assertSame($expected, $result);
|
||||
$findExisting->shouldHaveBeenCalledOnce();
|
||||
$getRepo->shouldHaveBeenCalledOnce();
|
||||
if ($expected) {
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
}
|
||||
|
||||
public function provideExistingShortUrls(): iterable
|
||||
{
|
||||
$url = 'http://foo.com';
|
||||
|
||||
yield [$url, [], ShortUrlMeta::createFromRawData(['findIfExists' => true]), null];
|
||||
yield [$url, [], ShortUrlMeta::createFromRawData(['findIfExists' => true]), new ShortUrl($url)];
|
||||
yield [$url, [], ShortUrlMeta::createFromRawData(
|
||||
['findIfExists' => true, 'customSlug' => 'foo']
|
||||
@@ -203,6 +208,37 @@ class UrlShortenerTest extends TestCase
|
||||
];
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function properExistingShortUrlIsReturnedWhenMultipleMatch(): void
|
||||
{
|
||||
$url = 'http://foo.com';
|
||||
$tags = ['baz', 'foo', 'bar'];
|
||||
$meta = ShortUrlMeta::createFromRawData([
|
||||
'findIfExists' => true,
|
||||
'validUntil' => Chronos::parse('2017-01-01'),
|
||||
'maxVisits' => 4,
|
||||
]);
|
||||
$tagsCollection = new ArrayCollection(array_map(function (string $tag) {
|
||||
return new Tag($tag);
|
||||
}, $tags));
|
||||
$expected = (new ShortUrl($url, $meta))->setTags($tagsCollection);
|
||||
|
||||
$repo = $this->prophesize(ShortUrlRepository::class);
|
||||
$findExisting = $repo->findBy(Argument::any())->willReturn([
|
||||
new ShortUrl($url),
|
||||
new ShortUrl($url, $meta),
|
||||
$expected,
|
||||
(new ShortUrl($url))->setTags($tagsCollection),
|
||||
]);
|
||||
$getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||
|
||||
$result = $this->urlShortener->urlToShortCode(new Uri($url), $tags, $meta);
|
||||
|
||||
$this->assertSame($expected, $result);
|
||||
$findExisting->shouldHaveBeenCalledOnce();
|
||||
$getRepo->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function shortCodeIsProperlyParsed(): void
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Repository\VisitRepository;
|
||||
use Shlinkio\Shlink\Core\Service\VisitService;
|
||||
|
||||
use function array_shift;
|
||||
use function count;
|
||||
use function floor;
|
||||
@@ -70,8 +71,11 @@ class VisitServiceTest extends TestCase
|
||||
$clear->shouldHaveBeenCalledTimes(floor(count($unlocatedVisits) / 200) + 1);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function visitsWhichCannotBeLocatedAreIgnored(): void
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideIsNonLocatableAddress
|
||||
*/
|
||||
public function visitsWhichCannotBeLocatedAreIgnoredOrLocatedAsEmpty(bool $isNonLocatableAddress): void
|
||||
{
|
||||
$unlocatedVisits = [
|
||||
new Visit(new ShortUrl('foo'), Visitor::emptyInstance()),
|
||||
@@ -88,14 +92,20 @@ class VisitServiceTest extends TestCase
|
||||
$clear = $this->em->clear()->will(function () {
|
||||
});
|
||||
|
||||
$this->visitService->locateUnlocatedVisits(function () {
|
||||
throw new IpCannotBeLocatedException('Cannot be located');
|
||||
$this->visitService->locateUnlocatedVisits(function () use ($isNonLocatableAddress) {
|
||||
throw new IpCannotBeLocatedException($isNonLocatableAddress, 'Cannot be located');
|
||||
});
|
||||
|
||||
$findUnlocatedVisits->shouldHaveBeenCalledOnce();
|
||||
$getRepo->shouldHaveBeenCalledOnce();
|
||||
$persist->shouldNotHaveBeenCalled();
|
||||
$persist->shouldHaveBeenCalledTimes($isNonLocatableAddress ? 1 : 0);
|
||||
$flush->shouldHaveBeenCalledOnce();
|
||||
$clear->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideIsNonLocatableAddress(): iterable
|
||||
{
|
||||
yield 'The address is locatable' => [false];
|
||||
yield 'The address is non-locatable' => [true];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use Fig\Http\Message\StatusCodeInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
use function array_merge;
|
||||
|
||||
abstract class AbstractRestAction implements RequestHandlerInterface, RequestMethodInterface, StatusCodeInterface
|
||||
|
||||
@@ -16,6 +16,7 @@ use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||
use Throwable;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
abstract class AbstractCreateShortUrlAction extends AbstractRestAction
|
||||
|
||||
@@ -12,6 +12,7 @@ use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class DeleteShortUrlAction extends AbstractRestAction
|
||||
|
||||
@@ -13,6 +13,7 @@ use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class EditShortUrlAction extends AbstractRestAction
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user