mirror of
https://github.com/shlinkio/shlink.git
synced 2025-03-14 04:00:57 +03:00
commit
c8acb5de68
243 changed files with 2940 additions and 1244 deletions
|
@ -1,3 +1,4 @@
|
|||
bin/rr
|
||||
config/autoload/*local*
|
||||
data/infra
|
||||
data/cache/*
|
||||
|
@ -22,4 +23,4 @@ infection*
|
|||
**/test*
|
||||
build*
|
||||
**/.*
|
||||
bin/helper
|
||||
!config/roadrunner/.rr.yml
|
||||
|
|
47
.github/actions/ci-setup/action.yml
vendored
Normal file
47
.github/actions/ci-setup/action.yml
vendored
Normal file
|
@ -0,0 +1,47 @@
|
|||
name: CI setup
|
||||
description: 'Sets up the environment to run CI actions for Shlink'
|
||||
|
||||
inputs:
|
||||
install-deps:
|
||||
description: 'Tells if dependencies should be installed with composer. Default value is "yes"'
|
||||
required: true
|
||||
default: 'yes'
|
||||
php-version:
|
||||
description: 'The PHP version to be setup'
|
||||
required: true
|
||||
php-extensions:
|
||||
description: 'The PHP extensions to install'
|
||||
required: false
|
||||
default: ''
|
||||
extensions-cache-key:
|
||||
description: 'The key used to cache PHP extensions. If empty value is provided, extension caching is disabled'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Setup cache environment
|
||||
id: extcache
|
||||
uses: shivammathur/cache-extensions@v1
|
||||
with:
|
||||
php-version: ${{ inputs.php-version }}
|
||||
extensions: ${{ inputs.php-extensions }}
|
||||
key: ${{ inputs.extensions-cache-key }}
|
||||
- name: Cache extensions
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.extcache.outputs.dir }}
|
||||
key: ${{ steps.extcache.outputs.key }}
|
||||
restore-keys: ${{ steps.extcache.outputs.key }}
|
||||
- name: Use PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ inputs.php-version }}
|
||||
tools: composer
|
||||
extensions: ${{ inputs.php-extensions }}
|
||||
coverage: pcov
|
||||
ini-values: pcov.directory=module
|
||||
- name: Install dependencies
|
||||
if: ${{ inputs.install-deps == 'yes' }}
|
||||
run: composer install --no-interaction --prefer-dist
|
||||
shell: bash
|
44
.github/workflows/ci-db-tests.yml
vendored
Normal file
44
.github/workflows/ci-db-tests.yml
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
name: Database tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
platform:
|
||||
type: string
|
||||
required: true
|
||||
description: One of sqlite:ci, mysql, maria, postgres or ms
|
||||
|
||||
jobs:
|
||||
db-tests:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: [ '8.1' ]
|
||||
env:
|
||||
LC_ALL: C
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install MSSQL ODBC
|
||||
if: ${{ inputs.platform == 'ms' }}
|
||||
run: sudo ./data/infra/ci/install-ms-odbc.sh
|
||||
- name: Start database server
|
||||
if: ${{ inputs.platform != 'sqlite:ci' }}
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_${{ inputs.platform }}
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-4.11.1, pdo_sqlsrv-5.10.1
|
||||
extensions-cache-key: db-tests-extensions-${{ matrix.php-version }}-${{ inputs.platform }}
|
||||
- name: Create test database
|
||||
if: ${{ inputs.platform == 'ms' }}
|
||||
run: docker-compose exec -T shlink_db_ms /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'Passw0rd!' -Q "CREATE DATABASE shlink_test;"
|
||||
- name: Run tests
|
||||
run: composer test:db:${{ inputs.platform }}
|
||||
- name: Upload code coverage
|
||||
uses: actions/upload-artifact@v3
|
||||
if: ${{ matrix.php-version == '8.1' && inputs.platform == 'sqlite:ci' }}
|
||||
with:
|
||||
name: coverage-db
|
||||
path: |
|
||||
build/coverage-db
|
||||
build/coverage-db.cov
|
45
.github/workflows/ci-mutation-tests.yml
vendored
Normal file
45
.github/workflows/ci-mutation-tests.yml
vendored
Normal file
|
@ -0,0 +1,45 @@
|
|||
name: Mutation tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
test-group:
|
||||
type: string
|
||||
required: true
|
||||
description: One of unit, db, api or cli
|
||||
|
||||
jobs:
|
||||
mutation-tests:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: [ '8.1' ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-4.11.1
|
||||
extensions-cache-key: mutation-tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: coverage-${{ inputs.test-group }}
|
||||
path: build
|
||||
- name: Resolve infection args
|
||||
id: infection_args
|
||||
run: echo "::set-output name=args::--logger-github=false"
|
||||
# TODO Try to filter mutation tests to improve execution times. Investigate why --git-diff-lines --git-diff-base=develop does not work
|
||||
# run: |
|
||||
# BRANCH="${GITHUB_REF#refs/heads/}" |
|
||||
# if [[ $BRANCH == 'main' || $BRANCH == 'develop' ]]; then
|
||||
# echo "::set-output name=args::--logger-github=false"
|
||||
# else
|
||||
# echo "::set-output name=args::--logger-github=false --git-diff-lines --git-diff-base=develop"
|
||||
# fi;
|
||||
shell: bash
|
||||
- if: ${{ inputs.test-group == 'unit' }}
|
||||
run: composer infect:ci:unit -- ${{ steps.infection_args.outputs.args }}
|
||||
env:
|
||||
INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }}
|
||||
- if: ${{ inputs.test-group != 'unit' }}
|
||||
run: composer infect:ci:${{ inputs.test-group }} -- ${{ steps.infection_args.outputs.args }}
|
37
.github/workflows/ci-tests.yml
vendored
Normal file
37
.github/workflows/ci-tests.yml
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
name: Tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
test-group:
|
||||
type: string
|
||||
required: true
|
||||
description: One of unit, api or cli
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Start postgres database server
|
||||
if: ${{ inputs.test-group == 'api' }}
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres
|
||||
- name: Start maria database server
|
||||
if: ${{ inputs.test-group == 'cli' }}
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_maria
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-4.11.1
|
||||
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
|
||||
- run: composer test:${{ inputs.test-group }}:ci
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ matrix.php-version == '8.1' }}
|
||||
with:
|
||||
name: coverage-${{ inputs.test-group }}
|
||||
path: |
|
||||
build/coverage-${{ inputs.test-group }}
|
||||
build/coverage-${{ inputs.test-group }}.cov
|
198
.github/workflows/ci.yml
vendored
198
.github/workflows/ci.yml
vendored
|
@ -16,146 +16,124 @@ jobs:
|
|||
php-version: ['8.1']
|
||||
command: ['cs', 'stan', 'swagger:validate']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Use PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
tools: composer
|
||||
extensions: openswoole-4.11.1
|
||||
coverage: none
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --prefer-dist
|
||||
php-extensions: openswoole-4.11.1
|
||||
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ matrix.command }}
|
||||
- run: composer ${{ matrix.command }}
|
||||
|
||||
tests:
|
||||
unit-tests:
|
||||
uses: './.github/workflows/ci-tests.yml'
|
||||
with:
|
||||
test-group: unit
|
||||
|
||||
cli-tests:
|
||||
uses: './.github/workflows/ci-tests.yml'
|
||||
with:
|
||||
test-group: cli
|
||||
|
||||
openswoole-api-tests:
|
||||
uses: './.github/workflows/ci-tests.yml'
|
||||
with:
|
||||
test-group: api
|
||||
|
||||
roadrunner-api-tests:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1']
|
||||
test-group: ['unit', 'api']
|
||||
php-version: [ '8.1' ]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Start database server
|
||||
if: ${{ matrix.test-group == 'api' }}
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres
|
||||
- name: Use PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
- uses: actions/checkout@v3
|
||||
- run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
tools: composer
|
||||
extensions: openswoole-4.11.1
|
||||
coverage: pcov
|
||||
ini-values: pcov.directory=module
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --prefer-dist
|
||||
- run: composer test:${{ matrix.test-group }}:ci
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: ${{ matrix.php-version == '8.1' }}
|
||||
with:
|
||||
name: coverage-${{ matrix.test-group }}
|
||||
path: |
|
||||
build/coverage-${{ matrix.test-group }}
|
||||
build/coverage-${{ matrix.test-group }}.cov
|
||||
- run: composer install --no-interaction --prefer-dist
|
||||
- run: ./vendor/bin/rr get --no-interaction --location bin/ && chmod +x bin/rr
|
||||
- run: composer test:api:rr
|
||||
|
||||
db-tests:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1']
|
||||
platform: ['sqlite:ci', 'mysql', 'maria', 'postgres', 'ms']
|
||||
env:
|
||||
LC_ALL: C
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install MSSQL ODBC
|
||||
if: ${{ matrix.platform == 'ms' }}
|
||||
run: sudo ./data/infra/ci/install-ms-odbc.sh
|
||||
- name: Start database server
|
||||
if: ${{ matrix.platform != 'sqlite:ci' }}
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_${{ matrix.platform }}
|
||||
- name: Use PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
tools: composer
|
||||
extensions: openswoole-4.11.1, pdo_sqlsrv-5.10.1
|
||||
coverage: pcov
|
||||
ini-values: pcov.directory=module
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --prefer-dist
|
||||
- name: Create test database
|
||||
if: ${{ matrix.platform == 'ms' }}
|
||||
run: docker-compose exec -T shlink_db_ms /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'Passw0rd!' -Q "CREATE DATABASE shlink_test;"
|
||||
- name: Run tests
|
||||
run: composer test:db:${{ matrix.platform }}
|
||||
- name: Upload code coverage
|
||||
uses: actions/upload-artifact@v2
|
||||
if: ${{ matrix.php-version == '8.1' && matrix.platform == 'sqlite:ci' }}
|
||||
with:
|
||||
name: coverage-db
|
||||
path: |
|
||||
build/coverage-db
|
||||
build/coverage-db.cov
|
||||
sqlite-db-tests:
|
||||
uses: './.github/workflows/ci-db-tests.yml'
|
||||
with:
|
||||
platform: 'sqlite:ci'
|
||||
|
||||
mutation-tests:
|
||||
mysql-db-tests:
|
||||
uses: './.github/workflows/ci-db-tests.yml'
|
||||
with:
|
||||
platform: 'mysql'
|
||||
|
||||
maria-db-tests:
|
||||
uses: './.github/workflows/ci-db-tests.yml'
|
||||
with:
|
||||
platform: 'maria'
|
||||
|
||||
postgres-db-tests:
|
||||
uses: './.github/workflows/ci-db-tests.yml'
|
||||
with:
|
||||
platform: 'postgres'
|
||||
|
||||
ms-db-tests:
|
||||
uses: './.github/workflows/ci-db-tests.yml'
|
||||
with:
|
||||
platform: 'ms'
|
||||
|
||||
unit-mutation-tests:
|
||||
needs:
|
||||
- tests
|
||||
- db-tests
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1']
|
||||
test-group: ['unit', 'db', 'api']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Use PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
tools: composer
|
||||
extensions: openswoole-4.11.1
|
||||
coverage: pcov
|
||||
ini-values: pcov.directory=module
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --prefer-dist
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
path: build
|
||||
- if: ${{ matrix.test-group == 'unit' }}
|
||||
run: composer infect:ci:unit
|
||||
env:
|
||||
INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }}
|
||||
- if: ${{ matrix.test-group != 'unit' }}
|
||||
run: composer infect:ci:${{ matrix.test-group }}
|
||||
- unit-tests
|
||||
uses: './.github/workflows/ci-mutation-tests.yml'
|
||||
with:
|
||||
test-group: unit
|
||||
|
||||
db-mutation-tests:
|
||||
needs:
|
||||
- sqlite-db-tests
|
||||
uses: './.github/workflows/ci-mutation-tests.yml'
|
||||
with:
|
||||
test-group: db
|
||||
|
||||
api-mutation-tests:
|
||||
needs:
|
||||
- openswoole-api-tests
|
||||
uses: './.github/workflows/ci-mutation-tests.yml'
|
||||
with:
|
||||
test-group: api
|
||||
|
||||
cli-mutation-tests:
|
||||
needs:
|
||||
- cli-tests
|
||||
uses: './.github/workflows/ci-mutation-tests.yml'
|
||||
with:
|
||||
test-group: cli
|
||||
|
||||
upload-coverage:
|
||||
needs:
|
||||
- tests
|
||||
- db-tests
|
||||
- unit-tests
|
||||
- openswoole-api-tests
|
||||
- cli-tests
|
||||
- sqlite-db-tests
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Use PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
coverage: pcov
|
||||
ini-values: pcov.directory=module
|
||||
- uses: actions/download-artifact@v2
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: build
|
||||
- run: mv build/coverage-unit/coverage-unit.cov build/coverage-unit.cov
|
||||
- run: mv build/coverage-db/coverage-db.cov build/coverage-db.cov
|
||||
- run: mv build/coverage-api/coverage-api.cov build/coverage-api.cov
|
||||
- run: mv build/coverage-cli/coverage-cli.cov build/coverage-cli.cov
|
||||
- run: wget https://phar.phpunit.de/phpcov-8.2.1.phar
|
||||
- run: php phpcov-8.2.1.phar merge build --clover build/clover.xml
|
||||
- name: Publish coverage
|
||||
|
@ -165,7 +143,10 @@ jobs:
|
|||
|
||||
delete-artifacts:
|
||||
needs:
|
||||
- mutation-tests
|
||||
- unit-mutation-tests
|
||||
- db-mutation-tests
|
||||
- api-mutation-tests
|
||||
- cli-mutation-tests
|
||||
- upload-coverage
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
|
@ -175,12 +156,13 @@ jobs:
|
|||
coverage-unit
|
||||
coverage-db
|
||||
coverage-api
|
||||
coverage-cli
|
||||
|
||||
build-docker-image:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 100
|
||||
- uses: marceloprado/has-changed-path@v1
|
||||
|
|
37
.github/workflows/docker-image-build.yml
vendored
37
.github/workflows/docker-image-build.yml
vendored
|
@ -1,4 +1,4 @@
|
|||
name: Build docker image
|
||||
name: Build and publish docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
|
@ -8,21 +8,20 @@ on:
|
|||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
with:
|
||||
version: latest
|
||||
- name: Login to docker hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build the image
|
||||
run: bash ./docker/build
|
||||
build-openswool:
|
||||
uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main
|
||||
secrets: inherit
|
||||
with:
|
||||
image-name: shlinkio/shlink
|
||||
version-arg-name: SHLINK_VERSION
|
||||
|
||||
build-roadrunner:
|
||||
uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main
|
||||
secrets: inherit
|
||||
with:
|
||||
image-name: shlinkio/shlink
|
||||
version-arg-name: SHLINK_VERSION
|
||||
platforms: 'linux/arm64/v8,linux/amd64'
|
||||
tags-suffix: roadrunner
|
||||
extra-build-args: |
|
||||
SHLINK_RUNTIME=rr
|
||||
|
|
18
.github/workflows/publish-release.yml
vendored
18
.github/workflows/publish-release.yml
vendored
|
@ -13,19 +13,18 @@ jobs:
|
|||
php-version: ['8.1']
|
||||
swoole: ['yes', 'no']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Use PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
tools: composer
|
||||
extensions: openswoole-4.11.1
|
||||
php-extensions: openswoole-4.11.1
|
||||
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
|
||||
install-deps: 'no'
|
||||
- if: ${{ matrix.swoole == 'yes' }}
|
||||
run: ./build.sh ${GITHUB_REF#refs/tags/v}
|
||||
- if: ${{ matrix.swoole == 'no' }}
|
||||
run: ./build.sh ${GITHUB_REF#refs/tags/v} --no-swoole
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: dist-files-${{ matrix.php-version }}-${{ matrix.swoole }}
|
||||
path: build
|
||||
|
@ -34,9 +33,8 @@ jobs:
|
|||
needs: ['build']
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/download-artifact@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: build
|
||||
- name: Publish release with assets
|
||||
|
|
12
.github/workflows/publish-swagger-spec.yml
vendored
12
.github/workflows/publish-swagger-spec.yml
vendored
|
@ -12,20 +12,16 @@ jobs:
|
|||
matrix:
|
||||
php-version: ['8.1']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Determine version
|
||||
id: determine_version
|
||||
run: echo "::set-output name=version::${GITHUB_REF#refs/tags/}"
|
||||
shell: bash
|
||||
- name: Use PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
tools: composer
|
||||
extensions: openswoole-4.11.1
|
||||
coverage: none
|
||||
- run: composer install --no-interaction --prefer-dist
|
||||
php-extensions: openswoole-4.11.1
|
||||
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
|
||||
- run: composer swagger:inline
|
||||
- run: mkdir ${{ steps.determine_version.outputs.version }}
|
||||
- run: mv docs/swagger/swagger-inlined.json ${{ steps.determine_version.outputs.version }}/open-api-spec.json
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,4 +1,7 @@
|
|||
.idea
|
||||
bin/.rr.*
|
||||
bin/rr
|
||||
config/roadrunner/.pid
|
||||
build
|
||||
!docker/build
|
||||
composer.lock
|
||||
|
|
50
CHANGELOG.md
50
CHANGELOG.md
|
@ -4,6 +4,56 @@ 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.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.
|
||||
* [#1531](https://github.com/shlinkio/shlink/issues/1531) and [#1090](https://github.com/shlinkio/shlink/issues/1090) Added support for trailing slashes in short URLs.
|
||||
* [#1406](https://github.com/shlinkio/shlink/issues/1406) Added new REST API version 3.
|
||||
|
||||
When making requests to the REST API with `/rest/v3/...` and an error occurs, all error types will be different, with the next correlation:
|
||||
|
||||
* `INVALID_ARGUMENT` -> `https://shlink.io/api/error/invalid-data`
|
||||
* `INVALID_SHORT_URL_DELETION` -> `https://shlink.io/api/error/invalid-short-url-deletion`
|
||||
* `DOMAIN_NOT_FOUND` -> `https://shlink.io/api/error/domain-not-found`
|
||||
* `FORBIDDEN_OPERATION` -> `https://shlink.io/api/error/forbidden-tag-operation`
|
||||
* `INVALID_URL` -> `https://shlink.io/api/error/invalid-url`
|
||||
* `INVALID_SLUG` -> `https://shlink.io/api/error/non-unique-slug`
|
||||
* `INVALID_SHORTCODE` -> `https://shlink.io/api/error/short-url-not-found`
|
||||
* `TAG_CONFLICT` -> `https://shlink.io/api/error/tag-conflict`
|
||||
* `TAG_NOT_FOUND` -> `https://shlink.io/api/error/tag-not-found`
|
||||
* `MERCURE_NOT_CONFIGURED` -> `https://shlink.io/api/error/mercure-not-configured`
|
||||
* `INVALID_AUTHORIZATION` -> `https://shlink.io/api/error/missing-authentication`
|
||||
* `INVALID_API_KEY` -> `https://shlink.io/api/error/invalid-api-key`
|
||||
|
||||
If you make a request to the API with v2 or v1, the old error types will be returned, until Shlink 4 is released, when only the new ones will be used.
|
||||
|
||||
Non-error responses are not affected.
|
||||
|
||||
* [#1513](https://github.com/shlinkio/shlink/issues/1513) Added publishing of the docker image in GHCR.
|
||||
* [#1114](https://github.com/shlinkio/shlink/issues/1114) Added support to provide an initial API key via `INITIAL_API_KEY` env var, when running Shlink with openswoole or RoadRunner.
|
||||
|
||||
Also, the installer tool now allows to generate an initial API key that can be copy-pasted (this tool is run interactively), in case you use php-fpm or you don't want to use env vars.
|
||||
|
||||
* [#1528](https://github.com/shlinkio/shlink/issues/1528) Added support to delay when the GeoLite2 DB file is downloaded in docker images, speeding up its startup time.
|
||||
|
||||
In order to do it, pass `SKIP_INITIAL_GEOLITE_DOWNLOAD=true` when creating the container.
|
||||
|
||||
### Changed
|
||||
* [#1339](https://github.com/shlinkio/shlink/issues/1339) Added new test suite for CLI E2E tests.
|
||||
* [#1503](https://github.com/shlinkio/shlink/issues/1503) Drastically improved build time in GitHub Actions, by optimizing parallelization and adding php extensions cache.
|
||||
* [#1525](https://github.com/shlinkio/shlink/issues/1525) Migrated to custom doctrine CLI entry point.
|
||||
* [#1492](https://github.com/shlinkio/shlink/issues/1492) Migrated to immutable options objects, mapped with [cuyz/valinor](https://github.com/CuyZ/Valinor).
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## [3.2.1] - 2022-08-08
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
|
|
@ -102,7 +102,9 @@ In order to ensure stability and no regressions are introduced while developing
|
|||
|
||||
Since the app instance is run on a process different from the one running the tests, when a test fails it might not be obvious why. To help debugging that, the app will dump all its logs inside `data/log/api-tests`, where you will find the `shlink.log` and `access.log` files.
|
||||
|
||||
* **CLI tests**: *TBD. Once included, its purpose will be the same as API tests, but running through the command line*
|
||||
* **CLI tests**: These are E2E tests too, but they test console commands instead of REST endpoints.
|
||||
|
||||
They use Maria DB as the database engine, and include the same fixtures as the API tests, that ensure the same data exists at the beginning of the execution.
|
||||
|
||||
Depending on the kind of contribution, maybe not all kinds of tests are needed, but the more you provide, the better.
|
||||
|
||||
|
@ -119,9 +121,9 @@ Depending on the kind of contribution, maybe not all kinds of tests are needed,
|
|||
For example, `test:db:postgres`.
|
||||
|
||||
* Run `./indocker composer test:api` to run API E2E tests. For these, the Postgres database engine is used.
|
||||
* Run `./indocker composer test:cli` to run CLI E2E tests. For these, the Maria DB database engine is used.
|
||||
* Run `./indocker composer infect:test` to run both unit and database tests (over sqlite) and then apply mutations to them with [infection](https://infection.github.io/).
|
||||
* Run `./indocker composer ci` to run all previous commands together. This command is run during the project's continuous integration.
|
||||
* Run `./indocker composer ci:parallel` to do the same as in previous case, but parallelizing non-conflicting tasks as much as possible.
|
||||
* Run `./indocker composer ci` to run all previous commands together, parallelizing non-conflicting tasks as much as possible.
|
||||
|
||||
## Pull request process
|
||||
|
||||
|
@ -133,7 +135,7 @@ Once everything is clear, to provide a pull request to this project, you should
|
|||
|
||||
The base branch should always be `develop`, and the target branch for the pull request should also be `develop`.
|
||||
|
||||
Before your branch can be merged, all the checks described in [Running code checks](#running-code-checks) have to be passing. You can verify that manually by running `./indocker composer ci:parallel`, or wait for the build to be run automatically after the pull request is created.
|
||||
Before your branch can be merged, all the checks described in [Running code checks](#running-code-checks) have to be passing. You can verify that manually by running `./indocker composer ci`, or wait for the build to be run automatically after the pull request is created.
|
||||
|
||||
## Architectural Decision Records
|
||||
|
||||
|
|
22
Dockerfile
22
Dockerfile
|
@ -2,6 +2,8 @@ FROM php:8.1.9-alpine3.16 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 PDO_SQLSRV_VERSION 5.10.1
|
||||
ENV MS_ODBC_SQL_VERSION 17.5.2.2
|
||||
|
@ -22,8 +24,10 @@ RUN \
|
|||
|
||||
# Install openswoole and sqlsrv driver for x86_64 builds
|
||||
RUN apk add --no-cache --virtual .phpize-deps ${PHPIZE_DEPS} unixodbc-dev && \
|
||||
pecl install openswoole-${OPENSWOOLE_VERSION} && \
|
||||
docker-php-ext-enable openswoole && \
|
||||
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \
|
||||
pecl install openswoole-${OPENSWOOLE_VERSION} && \
|
||||
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 && \
|
||||
|
@ -38,7 +42,12 @@ FROM base as builder
|
|||
COPY . .
|
||||
COPY --from=composer:2 /usr/bin/composer ./composer.phar
|
||||
RUN apk add --no-cache git && \
|
||||
php composer.phar install --no-dev --optimize-autoloader --prefer-dist --no-progress --no-interaction && \
|
||||
php composer.phar install --no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction && \
|
||||
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \
|
||||
php composer.phar remove spiral/roadrunner spiral/roadrunner-jobs --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interactionc ; \
|
||||
elif [ $SHLINK_RUNTIME == 'rr' ]; then \
|
||||
php composer.phar remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interaction ; \
|
||||
fi; \
|
||||
php composer.phar clear-cache && \
|
||||
rm -r docker composer.* && \
|
||||
sed -i "s/%SHLINK_VERSION%/${SHLINK_VERSION}/g" config/autoload/app_options.global.php
|
||||
|
@ -49,9 +58,12 @@ FROM base
|
|||
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
||||
|
||||
COPY --from=builder /etc/shlink .
|
||||
RUN ln -s /etc/shlink/bin/cli /usr/local/bin/shlink
|
||||
RUN ln -s /etc/shlink/bin/cli /usr/local/bin/shlink && \
|
||||
if [ "$SHLINK_RUNTIME" == 'rr' ]; then \
|
||||
php ./vendor/bin/rr get --no-interaction --location bin/ && chmod +x bin/rr ; \
|
||||
fi;
|
||||
|
||||
# Expose default openswoole port
|
||||
# Expose default port
|
||||
EXPOSE 8080
|
||||
|
||||
# Copy config specific for the image
|
||||
|
|
|
@ -15,7 +15,7 @@ A PHP-based self-hosted URL shortener that can be used to serve shortened URLs u
|
|||
|
||||
- [Full documentation](#full-documentation)
|
||||
- [Docker image](#docker-image)
|
||||
- [Self hosted](#self-hosted)
|
||||
- [Self-hosted](#self-hosted)
|
||||
- [Download](#download)
|
||||
- [Configure](#configure)
|
||||
- [Using shlink](#using-shlink)
|
||||
|
|
12
bin/doctrine
Executable file
12
bin/doctrine
Executable file
|
@ -0,0 +1,12 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\Tools\Console\ConsoleRunner;
|
||||
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
|
||||
|
||||
/** @var EntityManager $app */
|
||||
$em = require __DIR__ . '/../config/entity-manager.php';
|
||||
ConsoleRunner::run(new SingleManagerProvider($em));
|
32
bin/roadrunner-worker.php
Normal file
32
bin/roadrunner-worker.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Mezzio\Application;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Shlinkio\Shlink\EventDispatcher\RoadRunner\RoadRunnerTaskConsumerToListener;
|
||||
use Spiral\RoadRunner\Http\PSR7Worker;
|
||||
|
||||
use function Shlinkio\Shlink\Config\env;
|
||||
|
||||
(static function (): void {
|
||||
/** @var ContainerInterface $container */
|
||||
$container = include __DIR__ . '/../config/container.php';
|
||||
$rrMode = env('RR_MODE');
|
||||
|
||||
if ($rrMode === 'http') {
|
||||
// This was spin-up as a web worker
|
||||
$app = $container->get(Application::class);
|
||||
$worker = $container->get(PSR7Worker::class);
|
||||
|
||||
while ($req = $worker->waitRequest()) {
|
||||
try {
|
||||
$worker->respond($app->handle($req));
|
||||
} catch (Throwable $e) {
|
||||
$worker->getWorker()->error((string) $e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$container->get(RoadRunnerTaskConsumerToListener::class)->listenForTasks();
|
||||
}
|
||||
})();
|
|
@ -1,25 +1,38 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
export APP_ENV=test
|
||||
export DB_DRIVER=postgres
|
||||
export TEST_ENV=api
|
||||
export GENERATE_COVERAGE=${GENERATE_COVERAGE:-"no"}
|
||||
export TEST_RUNTIME="${TEST_RUNTIME:-"openswoole"}"
|
||||
export DB_DRIVER="${DB_DRIVER:-"postgres"}"
|
||||
export GENERATE_COVERAGE="${GENERATE_COVERAGE:-"no"}"
|
||||
|
||||
# Reset logs
|
||||
OUTPUT_LOGS=data/log/api-tests/output.log
|
||||
rm -rf data/log/api-tests
|
||||
mkdir data/log/api-tests
|
||||
touch data/log/api-tests/output.log
|
||||
touch $OUTPUT_LOGS
|
||||
|
||||
# Try to stop server just in case it hanged in last execution
|
||||
vendor/bin/laminas mezzio:swoole:stop
|
||||
[ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:stop
|
||||
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr stop -f
|
||||
|
||||
echo 'Starting server...'
|
||||
vendor/bin/laminas mezzio:swoole:start -d
|
||||
sleep 2
|
||||
[ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:start -d
|
||||
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr serve -p -c=config/roadrunner/.rr.dev.yml \
|
||||
-o=http.address=0.0.0.0:9999 \
|
||||
-o=logs.encoding=json \
|
||||
-o=logs.channels.http.encoding=json \
|
||||
-o=logs.channels.server.encoding=json \
|
||||
-o=logs.output="${PWD}/${OUTPUT_LOGS}" \
|
||||
-o=logs.channels.http.output="${PWD}/${OUTPUT_LOGS}" \
|
||||
-o=logs.channels.server.output="${PWD}/${OUTPUT_LOGS}" &
|
||||
sleep 2 # Let's give the server a couple of seconds to start
|
||||
|
||||
vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always --log-junit=build/coverage-api/junit.xml $*
|
||||
testsExitCode=$?
|
||||
|
||||
vendor/bin/laminas mezzio:swoole:stop
|
||||
[ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:stop
|
||||
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr stop -c config/roadrunner/.rr.dev.yml -o=http.address=0.0.0.0:9999
|
||||
|
||||
# Exit this script with the same code as the tests. If tests failed, this script has to fail
|
||||
exit $testsExitCode
|
||||
|
|
4
build.sh
4
build.sh
|
@ -24,6 +24,7 @@ rsync -av * "${builtContent}" \
|
|||
--exclude=*docker* \
|
||||
--exclude=Dockerfile \
|
||||
--include=.htaccess \
|
||||
--include=config/roadrunner/.rr.yml \
|
||||
--exclude-from=./.dockerignore
|
||||
cd "${builtContent}"
|
||||
|
||||
|
@ -36,6 +37,9 @@ ${composerBin} install --no-dev --prefer-dist $composerFlags
|
|||
if [[ $noSwoole ]]; then
|
||||
# If generating a dist not for openswoole, uninstall mezzio-swoole
|
||||
${composerBin} remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev $composerFlags
|
||||
else
|
||||
# If generating a dist for openswoole, uninstall RoadRunner
|
||||
${composerBin} remove spiral/roadrunner spiral/roadrunner-jobs --with-all-dependencies --update-no-dev $composerFlags
|
||||
fi
|
||||
|
||||
# Delete development files
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
],
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"ext-curl": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"akrabat/ip-address-middleware": "^2.1",
|
||||
|
@ -43,12 +45,14 @@
|
|||
"php-middleware/request-id": "^4.1",
|
||||
"pugx/shortid-php": "^1.0",
|
||||
"ramsey/uuid": "^4.3",
|
||||
"shlinkio/shlink-common": "^5.0",
|
||||
"shlinkio/shlink-config": "^2.0",
|
||||
"shlinkio/shlink-event-dispatcher": "^2.5",
|
||||
"shlinkio/shlink-common": "^5.1",
|
||||
"shlinkio/shlink-config": "^2.1",
|
||||
"shlinkio/shlink-event-dispatcher": "^2.6",
|
||||
"shlinkio/shlink-importer": "^4.0",
|
||||
"shlinkio/shlink-installer": "^8.1",
|
||||
"shlinkio/shlink-ip-geolocation": "^3.0",
|
||||
"shlinkio/shlink-installer": "^8.2",
|
||||
"shlinkio/shlink-ip-geolocation": "^3.1",
|
||||
"spiral/roadrunner": "^2.11",
|
||||
"spiral/roadrunner-jobs": "^2.3",
|
||||
"symfony/console": "^6.1",
|
||||
"symfony/filesystem": "^6.1",
|
||||
"symfony/lock": "^6.1",
|
||||
|
@ -58,8 +62,8 @@
|
|||
"require-dev": {
|
||||
"cebe/php-openapi": "^1.7",
|
||||
"devster/ubench": "^2.1",
|
||||
"dms/phpunit-arraysubset-asserts": "^0.3.0",
|
||||
"infection/infection": "^0.26.5",
|
||||
"dms/phpunit-arraysubset-asserts": "^0.4.0",
|
||||
"infection/infection": "^0.26.15",
|
||||
"openswoole/ide-helper": "~4.11.1",
|
||||
"phpspec/prophecy-phpunit": "^2.0",
|
||||
"phpstan/phpstan": "^1.8",
|
||||
|
@ -69,7 +73,7 @@
|
|||
"phpunit/phpunit": "^9.5",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"shlinkio/php-coding-standard": "~2.3.0",
|
||||
"shlinkio/shlink-test-utils": "^3.1.0",
|
||||
"shlinkio/shlink-test-utils": "^3.3",
|
||||
"symfony/var-dumper": "^6.1",
|
||||
"veewee/composer-run-parallel": "^1.1"
|
||||
},
|
||||
|
@ -87,8 +91,10 @@
|
|||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"ShlinkioTest\\Shlink\\CLI\\": "module/CLI/test",
|
||||
"ShlinkioCliTest\\Shlink\\CLI\\": "module/CLI/test-cli",
|
||||
"ShlinkioTest\\Shlink\\Rest\\": "module/Rest/test",
|
||||
"ShlinkioApiTest\\Shlink\\Rest\\": "module/Rest/test-api",
|
||||
"ShlinkioDbTest\\Shlink\\Rest\\": "module/Rest/test-db",
|
||||
"ShlinkioTest\\Shlink\\Core\\": "module/Core/test",
|
||||
"ShlinkioDbTest\\Shlink\\Core\\": "module/Core/test-db"
|
||||
},
|
||||
|
@ -98,31 +104,18 @@
|
|||
},
|
||||
"scripts": {
|
||||
"ci": [
|
||||
"@cs",
|
||||
"@stan",
|
||||
"@swagger:validate",
|
||||
"@test:ci",
|
||||
"@infect:ci"
|
||||
],
|
||||
"ci:parallel": [
|
||||
"@parallel cs stan swagger:validate test:unit:ci test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms",
|
||||
"@parallel infect:test:api infect:ci:unit infect:ci:db"
|
||||
"@parallel infect:test:api infect:test:cli infect:ci:unit infect:ci:db"
|
||||
],
|
||||
"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",
|
||||
"test": [
|
||||
"@test:unit",
|
||||
"@test:db",
|
||||
"@test:api"
|
||||
"@parallel test:unit test:db",
|
||||
"@parallel test:api test:cli"
|
||||
],
|
||||
"test:ci": [
|
||||
"@test:unit:ci",
|
||||
"@test:db",
|
||||
"@test:api:ci"
|
||||
],
|
||||
"test:unit": "@php vendor/bin/phpunit --order-by=random --colors=always --coverage-php build/coverage-unit.cov --testdox",
|
||||
"test:unit:ci": "@test:unit --coverage-xml=build/coverage-unit/coverage-xml --log-junit=build/coverage-unit/junit.xml",
|
||||
"test:unit": "@php vendor/bin/phpunit --order-by=random --colors=always --testdox",
|
||||
"test:unit:ci": "@test:unit --coverage-php=build/coverage-unit.cov --coverage-xml=build/coverage-unit/coverage-xml --log-junit=build/coverage-unit/junit.xml",
|
||||
"test:unit:pretty": "@test:unit --coverage-html build/coverage-unit/coverage-html",
|
||||
"test:db": "@parallel test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms",
|
||||
"test:db:sqlite": "APP_ENV=test php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-db.xml",
|
||||
|
@ -132,12 +125,18 @@
|
|||
"test:db:postgres": "DB_DRIVER=postgres composer test:db:sqlite",
|
||||
"test:db:ms": "DB_DRIVER=mssql composer test:db:sqlite",
|
||||
"test:api": "bin/test/run-api-tests.sh",
|
||||
"test:api:rr": "TEST_RUNTIME=rr bin/test/run-api-tests.sh",
|
||||
"test:api:ci": "GENERATE_COVERAGE=yes composer test:api",
|
||||
"infect:ci:base": "infection --threads=4 --log-verbosity=default --only-covered --only-covering-test-cases --skip-initial-tests",
|
||||
"test:api:pretty": "GENERATE_COVERAGE=pretty composer test:api",
|
||||
"test:cli": "APP_ENV=test DB_DRIVER=maria TEST_ENV=cli php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-cli.xml --log-junit=build/coverage-cli/junit.xml",
|
||||
"test:cli:ci": "GENERATE_COVERAGE=yes composer test:cli",
|
||||
"test:cli:pretty": "GENERATE_COVERAGE=pretty composer test:cli",
|
||||
"infect:ci:base": "infection --threads=max --only-covered --only-covering-test-cases --skip-initial-tests",
|
||||
"infect:ci: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": "@parallel infect:ci:unit infect:ci:db infect:ci:api",
|
||||
"infect:ci:cli": "@infect:ci:base --coverage=build/coverage-cli --min-msi=80 --configuration=infection-cli.json",
|
||||
"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",
|
||||
"@infect:ci"
|
||||
|
@ -150,18 +149,20 @@
|
|||
"@test:api:ci",
|
||||
"@infect:ci:api"
|
||||
],
|
||||
"infect:test:cli": [
|
||||
"@test:cli:ci",
|
||||
"@infect:ci:cli"
|
||||
],
|
||||
"swagger:validate": "php-openapi validate docs/swagger/swagger.json",
|
||||
"swagger:inline": "php-openapi inline docs/swagger/swagger.json docs/swagger/swagger-inlined.json",
|
||||
"clean:dev": "rm -f data/database.sqlite && rm -f config/params/generated_config.php"
|
||||
},
|
||||
"scripts-descriptions": {
|
||||
"ci": "<fg=blue;options=bold>Alias for \"cs\", \"stan\", \"swagger:validate\", \"test:ci\" and \"infect:ci\"</>",
|
||||
"ci:parallel": "<fg=blue;options=bold>Same as \"ci\", but parallelizing tasks as much as possible</>",
|
||||
"cs": "<fg=blue;options=bold>Checks coding styles</>",
|
||||
"cs:fix": "<fg=blue;options=bold>Fixes coding styles, when possible</>",
|
||||
"stan": "<fg=blue;options=bold>Inspects code with phpstan</>",
|
||||
"test": "<fg=blue;options=bold>Runs all test suites</>",
|
||||
"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:unit:pretty": "<fg=blue;options=bold>Runs unit test suites and generates an HTML code coverage report</>",
|
||||
|
@ -173,7 +174,11 @@
|
|||
"test:db:postgres": "<fg=blue;options=bold>Runs database test suites on a PostgreSQL database</>",
|
||||
"test:db:ms": "<fg=blue;options=bold>Runs database test suites on a Microsoft SQL Server database</>",
|
||||
"test:api": "<fg=blue;options=bold>Runs API test suites</>",
|
||||
"test:api:ci": "<fg=blue;options=bold>Runs API test suites, and generates code coverage reports</>",
|
||||
"test:api:ci": "<fg=blue;options=bold>Runs API test suites, and generates code coverage for CI</>",
|
||||
"test:api:pretty": "<fg=blue;options=bold>Runs API test suites, and generates code coverage in HTML format</>",
|
||||
"test:cli": "<fg=blue;options=bold>Runs CLI test suites</>",
|
||||
"test:cli:ci": "<fg=blue;options=bold>Runs CLI test suites, and generates code coverage for CI</>",
|
||||
"test:cli:pretty": "<fg=blue;options=bold>Runs CLI test suites, and generates code coverage in HTML format</>",
|
||||
"infect:ci": "<fg=blue;options=bold>Checks unit and db tests quality applying mutation testing with existing reports and logs</>",
|
||||
"infect:ci:unit": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing with existing reports and logs</>",
|
||||
"infect:ci:db": "<fg=blue;options=bold>Checks db tests quality applying mutation testing with existing reports and logs</>",
|
||||
|
|
|
@ -8,8 +8,8 @@ return [
|
|||
|
||||
'debug' => false,
|
||||
|
||||
// Disabling config cache for cli, ensures it's never used for openswoole and also that console commands don't
|
||||
// generate a cache file that's then used by non-openswoole web executions
|
||||
// Disabling config cache for cli, ensures it's never used for openswoole/RoadRunner, and also that console
|
||||
// commands don't generate a cache file that's then used by php-fpm web executions
|
||||
ConfigAggregator::ENABLE_CACHE => PHP_SAPI !== 'cli',
|
||||
|
||||
];
|
||||
|
|
|
@ -3,12 +3,22 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Mezzio\Container;
|
||||
use Psr\Http\Client\ClientInterface;
|
||||
use Psr\Http\Message\ServerRequestFactoryInterface;
|
||||
use Psr\Http\Message\StreamFactoryInterface;
|
||||
use Psr\Http\Message\UploadedFileFactoryInterface;
|
||||
use Spiral\RoadRunner\Http\PSR7Worker;
|
||||
use Spiral\RoadRunner\WorkerInterface;
|
||||
|
||||
return [
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
PSR7Worker::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
|
||||
'delegators' => [
|
||||
Mezzio\Application::class => [
|
||||
Container\ApplicationConfigInjectionDelegator::class,
|
||||
|
@ -26,4 +36,13 @@ return [
|
|||
],
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
PSR7Worker::class => [
|
||||
WorkerInterface::class,
|
||||
ServerRequestFactoryInterface::class,
|
||||
StreamFactoryInterface::class,
|
||||
UploadedFileFactoryInterface::class,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
@ -6,12 +6,14 @@ use Laminas\Stratigility\Middleware\ErrorHandler;
|
|||
use Mezzio\ProblemDetails\ProblemDetailsMiddleware;
|
||||
use Shlinkio\Shlink\Common\Logger;
|
||||
|
||||
use function Shlinkio\Shlink\Core\toProblemDetailsType;
|
||||
|
||||
return [
|
||||
|
||||
'problem-details' => [
|
||||
'default_types_map' => [
|
||||
404 => 'NOT_FOUND',
|
||||
500 => 'INTERNAL_SERVER_ERROR',
|
||||
404 => toProblemDetailsType('not-found'),
|
||||
500 => toProblemDetailsType('internal-server-error'),
|
||||
],
|
||||
],
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ return [
|
|||
Option\UrlShortener\AutoResolveTitlesConfigOption::class,
|
||||
Option\UrlShortener\AppendExtraPathConfigOption::class,
|
||||
Option\UrlShortener\EnableMultiSegmentSlugsConfigOption::class,
|
||||
Option\UrlShortener\EnableTrailingSlashConfigOption::class,
|
||||
Option\Tracking\IpAnonymizationConfigOption::class,
|
||||
Option\Tracking\OrphanVisitsTrackingConfigOption::class,
|
||||
Option\Tracking\DisableTrackParamConfigOption::class,
|
||||
|
@ -72,9 +73,18 @@ return [
|
|||
InstallationCommand::DB_MIGRATE->value => [
|
||||
'command' => 'bin/cli ' . Command\Db\MigrateDatabaseCommand::NAME,
|
||||
],
|
||||
InstallationCommand::ORM_PROXIES->value => [
|
||||
'command' => 'bin/doctrine orm:generate-proxies',
|
||||
],
|
||||
InstallationCommand::ORM_CLEAR_CACHE->value => [
|
||||
'command' => 'bin/doctrine orm:clear-cache:metadata',
|
||||
],
|
||||
InstallationCommand::GEOLITE_DOWNLOAD_DB->value => [
|
||||
'command' => 'bin/cli ' . Command\Visit\DownloadGeoLiteDbCommand::NAME,
|
||||
],
|
||||
InstallationCommand::API_KEY_GENERATE->value => [
|
||||
'command' => 'bin/cli ' . Command\Api\GenerateKeyCommand::NAME,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ return [
|
|||
'mercure' => [
|
||||
'public_hub_url' => 'http://localhost:8001',
|
||||
'internal_hub_url' => 'http://shlink_mercure_proxy',
|
||||
'jwt_secret' => 'mercure_jwt_key',
|
||||
'jwt_secret' => 'mercure_jwt_key_long_enough_to_avoid_error',
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
@ -45,6 +45,7 @@ return [
|
|||
'rest' => [
|
||||
'path' => '/rest',
|
||||
'middleware' => [
|
||||
Rest\Middleware\ErrorHandler\BackwardsCompatibleProblemDetailsHandler::class,
|
||||
Router\Middleware\ImplicitOptionsMiddleware::class,
|
||||
Rest\Middleware\BodyParserMiddleware::class,
|
||||
Rest\Middleware\AuthenticationMiddleware::class,
|
||||
|
|
|
@ -7,15 +7,19 @@ namespace Shlinkio\Shlink;
|
|||
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\Rest\Action;
|
||||
use Shlinkio\Shlink\Rest\ConfigProvider;
|
||||
use Shlinkio\Shlink\Rest\Middleware;
|
||||
use Shlinkio\Shlink\Rest\Middleware\Mercure\NotConfiguredMercureErrorHandler;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
return (static function (): array {
|
||||
$contentNegotiationMiddleware = Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class;
|
||||
$dropDomainMiddleware = Middleware\ShortUrl\DropDefaultDomainFromRequestMiddleware::class;
|
||||
$overrideDomainMiddleware = Middleware\ShortUrl\OverrideDomainMiddleware::class;
|
||||
$shortUrlRouteSuffix = EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(false) ? '[/]' : '';
|
||||
|
||||
return [
|
||||
|
||||
|
@ -90,7 +94,7 @@ return (static function (): array {
|
|||
],
|
||||
[
|
||||
'name' => CoreAction\RedirectAction::class,
|
||||
'path' => '/{shortCode}',
|
||||
'path' => sprintf('/{shortCode}%s', $shortUrlRouteSuffix),
|
||||
'middleware' => [
|
||||
IpAddress::class,
|
||||
CoreAction\RedirectAction::class,
|
||||
|
|
|
@ -4,33 +4,40 @@ declare(strict_types=1);
|
|||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
return [
|
||||
return (static function (): array {
|
||||
/** @var string|null $disableTrackingFrom */
|
||||
$disableTrackingFrom = EnvVars::DISABLE_TRACKING_FROM->loadFromEnv();
|
||||
|
||||
'tracking' => [
|
||||
// Tells if IP addresses should be anonymized before persisting, to fulfil data protection regulations
|
||||
// This applies only if IP address tracking is enabled
|
||||
'anonymize_remote_addr' => (bool) EnvVars::ANONYMIZE_REMOTE_ADDR->loadFromEnv(true),
|
||||
return [
|
||||
|
||||
// Tells if visits to not-found URLs should be tracked. The disable_tracking option takes precedence
|
||||
'track_orphan_visits' => (bool) EnvVars::TRACK_ORPHAN_VISITS->loadFromEnv(true),
|
||||
'tracking' => [
|
||||
// Tells if IP addresses should be anonymized before persisting, to fulfil data protection regulations
|
||||
// This applies only if IP address tracking is enabled
|
||||
'anonymize_remote_addr' => (bool) EnvVars::ANONYMIZE_REMOTE_ADDR->loadFromEnv(true),
|
||||
|
||||
// A query param that, if provided, will disable tracking of one particular visit. Always takes precedence
|
||||
'disable_track_param' => EnvVars::DISABLE_TRACK_PARAM->loadFromEnv(),
|
||||
// Tells if visits to not-found URLs should be tracked. The disable_tracking option takes precedence
|
||||
'track_orphan_visits' => (bool) EnvVars::TRACK_ORPHAN_VISITS->loadFromEnv(true),
|
||||
|
||||
// If true, visits will not be tracked at all
|
||||
'disable_tracking' => (bool) EnvVars::DISABLE_TRACKING->loadFromEnv(false),
|
||||
// A query param that, if provided, will disable tracking of one particular visit. Always takes precedence
|
||||
'disable_track_param' => EnvVars::DISABLE_TRACK_PARAM->loadFromEnv(),
|
||||
|
||||
// If true, visits will be tracked, but neither the IP address, nor the location will be resolved
|
||||
'disable_ip_tracking' => (bool) EnvVars::DISABLE_IP_TRACKING->loadFromEnv(false),
|
||||
// If true, visits will not be tracked at all
|
||||
'disable_tracking' => (bool) EnvVars::DISABLE_TRACKING->loadFromEnv(false),
|
||||
|
||||
// If true, the referrer will not be tracked
|
||||
'disable_referrer_tracking' => (bool) EnvVars::DISABLE_REFERRER_TRACKING->loadFromEnv(false),
|
||||
// If true, visits will be tracked, but neither the IP address, nor the location will be resolved
|
||||
'disable_ip_tracking' => (bool) EnvVars::DISABLE_IP_TRACKING->loadFromEnv(false),
|
||||
|
||||
// If true, the user agent will not be tracked
|
||||
'disable_ua_tracking' => (bool) EnvVars::DISABLE_UA_TRACKING->loadFromEnv(false),
|
||||
// If true, the referrer will not be tracked
|
||||
'disable_referrer_tracking' => (bool) EnvVars::DISABLE_REFERRER_TRACKING->loadFromEnv(false),
|
||||
|
||||
// A list of IP addresses, patterns or CIDR blocks from which tracking is disabled by default
|
||||
'disable_tracking_from' => EnvVars::DISABLE_TRACKING_FROM->loadFromEnv(),
|
||||
],
|
||||
// If true, the user agent will not be tracked
|
||||
'disable_ua_tracking' => (bool) EnvVars::DISABLE_UA_TRACKING->loadFromEnv(false),
|
||||
|
||||
];
|
||||
// A list of IP addresses, patterns or CIDR blocks from which tracking is disabled by default
|
||||
'disable_tracking_from' => $disableTrackingFrom === null
|
||||
? []
|
||||
: array_map(trim(...), explode(',', $disableTrackingFrom)),
|
||||
],
|
||||
|
||||
];
|
||||
})();
|
||||
|
|
|
@ -2,14 +2,19 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
$isSwoole = extension_loaded('openswoole');
|
||||
use function Shlinkio\Shlink\Config\runningInOpenswoole;
|
||||
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
||||
|
||||
return [
|
||||
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
'schema' => 'http',
|
||||
'hostname' => sprintf('localhost:%s', $isSwoole ? '8080' : '8000'),
|
||||
'hostname' => sprintf('localhost:%s', match (true) {
|
||||
runningInRoadRunner() => '8800',
|
||||
runningInOpenswoole() => '8080',
|
||||
default => '8000',
|
||||
}),
|
||||
],
|
||||
'auto_resolve_titles' => true,
|
||||
// 'multi_segment_slugs_enabled' => true,
|
||||
|
|
|
@ -13,11 +13,13 @@ use Shlinkio\Shlink\Config\ConfigAggregator\EnvVarLoaderProvider;
|
|||
|
||||
use function class_exists;
|
||||
use function Shlinkio\Shlink\Config\env;
|
||||
use function Shlinkio\Shlink\Config\openswooleIsInstalled;
|
||||
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
||||
|
||||
use const PHP_SAPI;
|
||||
|
||||
$isCli = PHP_SAPI === 'cli';
|
||||
$isTestEnv = env('APP_ENV') === 'test';
|
||||
$enableSwoole = PHP_SAPI === 'cli' && openswooleIsInstalled() && ! runningInRoadRunner();
|
||||
|
||||
return (new ConfigAggregator\ConfigAggregator([
|
||||
! $isTestEnv
|
||||
|
@ -26,7 +28,7 @@ return (new ConfigAggregator\ConfigAggregator([
|
|||
Mezzio\ConfigProvider::class,
|
||||
Mezzio\Router\ConfigProvider::class,
|
||||
Mezzio\Router\FastRouteRouter\ConfigProvider::class,
|
||||
$isCli && class_exists(Swoole\ConfigProvider::class)
|
||||
$enableSwoole && class_exists(Swoole\ConfigProvider::class)
|
||||
? Swoole\ConfigProvider::class
|
||||
: new ConfigAggregator\ArrayProvider([]),
|
||||
ProblemDetails\ConfigProvider::class,
|
||||
|
|
49
config/roadrunner/.rr.dev.yml
Normal file
49
config/roadrunner/.rr.dev.yml
Normal file
|
@ -0,0 +1,49 @@
|
|||
version: '2.7'
|
||||
|
||||
rpc:
|
||||
listen: tcp://127.0.0.1:6001
|
||||
|
||||
server:
|
||||
command: 'php ../../bin/roadrunner-worker.php'
|
||||
|
||||
http:
|
||||
address: '0.0.0.0:8080'
|
||||
middleware: ['static']
|
||||
static:
|
||||
dir: '../../public'
|
||||
forbid: ['.php', '.htaccess']
|
||||
pool:
|
||||
num_workers: 1
|
||||
|
||||
jobs:
|
||||
pool:
|
||||
num_workers: 1
|
||||
timeout: 300
|
||||
consume: ['shlink']
|
||||
pipelines:
|
||||
shlink:
|
||||
driver: memory
|
||||
config:
|
||||
priority: 10
|
||||
prefetch: 10
|
||||
|
||||
logs:
|
||||
mode: development
|
||||
channels:
|
||||
http:
|
||||
level: debug
|
||||
server:
|
||||
level: debug
|
||||
metrics:
|
||||
level: debug
|
||||
|
||||
reload:
|
||||
interval: 1s
|
||||
patterns: ['.php']
|
||||
services:
|
||||
http:
|
||||
dirs: ['../../bin', '../../config', '../../data/migrations', '../../module', '../../vendor']
|
||||
recursive: true
|
||||
jobs:
|
||||
dirs: ['../../bin', '../../config', '../../data/migrations', '../../module', '../../vendor']
|
||||
recursive: true
|
36
config/roadrunner/.rr.yml
Normal file
36
config/roadrunner/.rr.yml
Normal file
|
@ -0,0 +1,36 @@
|
|||
version: '2.7'
|
||||
|
||||
rpc:
|
||||
listen: tcp://127.0.0.1:6001
|
||||
|
||||
server:
|
||||
command: 'php -dopcache.enable_cli=1 -dopcache.validate_timestamps=0 ../../bin/roadrunner-worker.php'
|
||||
|
||||
http:
|
||||
address: '0.0.0.0:${PORT}'
|
||||
middleware: ['static']
|
||||
static:
|
||||
dir: '../../public'
|
||||
forbid: ['.php', '.htaccess']
|
||||
pool:
|
||||
num_workers: ${WEB_WORKER_NUM}
|
||||
|
||||
jobs:
|
||||
timeout: 300 # 5 minutes
|
||||
pool:
|
||||
num_workers: ${TASK_WORKER_NUM}
|
||||
consume: ['shlink']
|
||||
pipelines:
|
||||
shlink:
|
||||
driver: memory
|
||||
config:
|
||||
priority: 10
|
||||
prefetch: 10
|
||||
|
||||
logs:
|
||||
mode: production
|
||||
channels:
|
||||
http:
|
||||
level: info # Log all http requests, set to info to disable
|
||||
server:
|
||||
level: debug # Everything written to worker stderr is logged
|
|
@ -10,8 +10,8 @@ use Psr\Container\ContainerInterface;
|
|||
use function register_shutdown_function;
|
||||
use function sprintf;
|
||||
|
||||
use const ShlinkioTest\Shlink\SWOOLE_TESTING_HOST;
|
||||
use const ShlinkioTest\Shlink\SWOOLE_TESTING_PORT;
|
||||
use const ShlinkioTest\Shlink\API_TESTS_HOST;
|
||||
use const ShlinkioTest\Shlink\API_TESTS_PORT;
|
||||
|
||||
/** @var ContainerInterface $container */
|
||||
$container = require __DIR__ . '/../container.php';
|
||||
|
@ -24,10 +24,15 @@ $httpClient = $container->get('shlink_test_api_client');
|
|||
register_shutdown_function(function () use ($httpClient): void {
|
||||
$httpClient->request(
|
||||
'GET',
|
||||
sprintf('http://%s:%s/api-tests/stop-coverage', SWOOLE_TESTING_HOST, SWOOLE_TESTING_PORT),
|
||||
sprintf('http://%s:%s/api-tests/stop-coverage', API_TESTS_HOST, API_TESTS_PORT),
|
||||
);
|
||||
});
|
||||
|
||||
$testHelper->createTestDb(['bin/cli', 'db:create'], ['bin/cli', 'db:migrate']);
|
||||
$testHelper->createTestDb(
|
||||
['bin/cli', 'db:create'],
|
||||
['bin/cli', 'db:migrate'],
|
||||
['bin/doctrine', 'orm:schema-tool:drop'],
|
||||
['bin/doctrine', 'dbal:run-sql'],
|
||||
);
|
||||
ApiTest\ApiTestCase::setApiClient($httpClient);
|
||||
ApiTest\ApiTestCase::setSeedFixturesCallback(fn () => $testHelper->seedFixtures($em, $config['data_fixtures'] ?? []));
|
||||
|
|
33
config/test/bootstrap_cli_tests.php
Normal file
33
config/test/bootstrap_cli_tests.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\TestUtils;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
use function file_exists;
|
||||
use function unlink;
|
||||
|
||||
/** @var ContainerInterface $container */
|
||||
$container = require __DIR__ . '/../container.php';
|
||||
$testHelper = $container->get(Helper\TestHelper::class);
|
||||
$config = $container->get('config');
|
||||
$em = $container->get(EntityManager::class);
|
||||
|
||||
// Delete old coverage in PHP, to avoid merging older executions with current one
|
||||
$covFile = __DIR__ . '/../../build/coverage-cli.cov';
|
||||
if (file_exists($covFile)) {
|
||||
unlink($covFile);
|
||||
}
|
||||
|
||||
$testHelper->createTestDb(
|
||||
['bin/cli', 'db:create'],
|
||||
['bin/cli', 'db:migrate'],
|
||||
['bin/doctrine', 'orm:schema-tool:drop'],
|
||||
['bin/doctrine', 'dbal:run-sql'],
|
||||
);
|
||||
CliTest\CliTestCase::setSeedFixturesCallback(
|
||||
static fn () => $testHelper->seedFixtures($em, $config['data_fixtures'] ?? []),
|
||||
);
|
|
@ -8,5 +8,10 @@ use Psr\Container\ContainerInterface;
|
|||
|
||||
/** @var ContainerInterface $container */
|
||||
$container = require __DIR__ . '/../container.php';
|
||||
$container->get(Helper\TestHelper::class)->createTestDb(['bin/cli', 'db:create'], ['bin/cli', 'db:migrate']);
|
||||
$container->get(Helper\TestHelper::class)->createTestDb(
|
||||
['bin/cli', 'db:create'],
|
||||
['bin/cli', 'db:migrate'],
|
||||
['bin/doctrine', 'orm:schema-tool:drop'],
|
||||
['bin/doctrine', 'dbal:run-sql'],
|
||||
);
|
||||
DbTest\DatabaseTestCase::setEntityManager($container->get('em'));
|
||||
|
|
|
@ -4,5 +4,5 @@ declare(strict_types=1);
|
|||
|
||||
namespace ShlinkioTest\Shlink;
|
||||
|
||||
const SWOOLE_TESTING_HOST = '127.0.0.1';
|
||||
const SWOOLE_TESTING_PORT = 9999;
|
||||
const API_TESTS_HOST = '127.0.0.1';
|
||||
const API_TESTS_PORT = 9999;
|
||||
|
|
|
@ -8,8 +8,10 @@ use GuzzleHttp\Client;
|
|||
use Laminas\ConfigAggregator\ConfigAggregator;
|
||||
use Laminas\Diactoros\Response\EmptyResponse;
|
||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||
use League\Event\EventDispatcher;
|
||||
use Monolog\Level;
|
||||
use PHPUnit\Runner\Version;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
@ -20,24 +22,60 @@ use SebastianBergmann\CodeCoverage\Report\Html\Facade as Html;
|
|||
use SebastianBergmann\CodeCoverage\Report\PHP;
|
||||
use SebastianBergmann\CodeCoverage\Report\Xml\Facade as Xml;
|
||||
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Event\ConsoleCommandEvent;
|
||||
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
use function file_exists;
|
||||
use function Functional\contains;
|
||||
use function Laminas\Stratigility\middleware;
|
||||
use function Shlinkio\Shlink\Config\env;
|
||||
use function sprintf;
|
||||
use function sys_get_temp_dir;
|
||||
|
||||
use const ShlinkioTest\Shlink\SWOOLE_TESTING_HOST;
|
||||
use const ShlinkioTest\Shlink\SWOOLE_TESTING_PORT;
|
||||
use const ShlinkioTest\Shlink\API_TESTS_HOST;
|
||||
use const ShlinkioTest\Shlink\API_TESTS_PORT;
|
||||
|
||||
$isApiTest = env('TEST_ENV') === 'api';
|
||||
$generateCoverage = env('GENERATE_COVERAGE') === 'yes';
|
||||
if ($isApiTest && $generateCoverage) {
|
||||
$isCliTest = env('TEST_ENV') === 'cli';
|
||||
$isE2eTest = $isApiTest || $isCliTest;
|
||||
$coverageType = env('GENERATE_COVERAGE');
|
||||
$generateCoverage = contains(['yes', 'pretty'], $coverageType);
|
||||
|
||||
$coverage = null;
|
||||
if ($isE2eTest && $generateCoverage) {
|
||||
$filter = new Filter();
|
||||
$filter->includeDirectory(__DIR__ . '/../../module/Core/src');
|
||||
$filter->includeDirectory(__DIR__ . '/../../module/Rest/src');
|
||||
$filter->includeDirectory(__DIR__ . '/../../module/' . ($isApiTest ? 'Rest' : 'CLI') . '/src');
|
||||
$coverage = new CodeCoverage((new Selector())->forLineCoverage($filter), $filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param 'api'|'cli' $type
|
||||
*/
|
||||
$exportCoverage = static function (string $type = 'api') use (&$coverage, $coverageType): void {
|
||||
if ($coverage === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$basePath = __DIR__ . '/../../build/coverage-' . $type;
|
||||
$covPath = $basePath . '.cov';
|
||||
|
||||
// Every CLI test runs on its own process and dumps the coverage afterwards.
|
||||
// Try to load it and merge it, so that we end up with the whole coverage at the end.
|
||||
if ($type === 'cli' && file_exists($covPath)) {
|
||||
$coverage->merge(require $covPath);
|
||||
}
|
||||
|
||||
if ($coverageType === 'pretty') {
|
||||
(new Html())->process($coverage, $basePath . '/coverage-html');
|
||||
} else {
|
||||
(new PHP())->process($coverage, $covPath);
|
||||
(new Xml(Version::getVersionString()))->process($coverage, $basePath . '/coverage-xml');
|
||||
}
|
||||
};
|
||||
|
||||
$buildDbConnection = static function (): array {
|
||||
$driver = env('DB_DRIVER', 'sqlite');
|
||||
$isCi = env('CI', false);
|
||||
|
@ -92,14 +130,13 @@ return [
|
|||
'schema' => 'http',
|
||||
'hostname' => 'doma.in',
|
||||
],
|
||||
'validate_url' => true,
|
||||
],
|
||||
|
||||
'mezzio-swoole' => [
|
||||
'enable_coroutine' => false,
|
||||
'swoole-http-server' => [
|
||||
'host' => SWOOLE_TESTING_HOST,
|
||||
'port' => SWOOLE_TESTING_PORT,
|
||||
'host' => API_TESTS_HOST,
|
||||
'port' => API_TESTS_PORT,
|
||||
'process-name' => 'shlink_test',
|
||||
'options' => [
|
||||
'pid_file' => sys_get_temp_dir() . '/shlink-test-swoole.pid',
|
||||
|
@ -113,17 +150,10 @@ return [
|
|||
[
|
||||
'name' => 'dump_coverage',
|
||||
'path' => '/api-tests/stop-coverage',
|
||||
'middleware' => middleware(static function () use (&$coverage) {
|
||||
'middleware' => middleware(static function () use ($exportCoverage) {
|
||||
// TODO I have tried moving this block to a listener so that it's invoked automatically,
|
||||
// but then the coverage is generated empty ¯\_(ツ)_/¯
|
||||
if ($coverage) { // @phpstan-ignore-line
|
||||
$basePath = __DIR__ . '/../../build/coverage-api';
|
||||
|
||||
(new PHP())->process($coverage, $basePath . '.cov');
|
||||
(new Xml(Version::getVersionString()))->process($coverage, $basePath . '/coverage-xml');
|
||||
(new Html())->process($coverage, $basePath . '/coverage-html');
|
||||
}
|
||||
|
||||
$exportCoverage();
|
||||
return new EmptyResponse();
|
||||
}),
|
||||
'allowed_methods' => ['GET'],
|
||||
|
@ -157,13 +187,69 @@ return [
|
|||
'dependencies' => [
|
||||
'services' => [
|
||||
'shlink_test_api_client' => new Client([
|
||||
'base_uri' => sprintf('http://%s:%s/', SWOOLE_TESTING_HOST, SWOOLE_TESTING_PORT),
|
||||
'base_uri' => sprintf('http://%s:%s/', API_TESTS_HOST, API_TESTS_PORT),
|
||||
'http_errors' => false,
|
||||
]),
|
||||
],
|
||||
'factories' => [
|
||||
TestUtils\Helper\TestHelper::class => InvokableFactory::class,
|
||||
],
|
||||
'delegators' => $isCliTest ? [
|
||||
Application::class => [
|
||||
static function (
|
||||
ContainerInterface $c,
|
||||
string $serviceName,
|
||||
callable $callback,
|
||||
) use (
|
||||
&$coverage,
|
||||
$exportCoverage,
|
||||
) {
|
||||
/** @var Application $app */
|
||||
$app = $callback();
|
||||
$wrappedEventDispatcher = new EventDispatcher();
|
||||
|
||||
// When the command starts, start collecting coverage
|
||||
$wrappedEventDispatcher->subscribeTo(
|
||||
ConsoleCommandEvent::class,
|
||||
static function () use (&$coverage): void {
|
||||
$id = env('COVERAGE_ID');
|
||||
if ($id === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$coverage?->start($id);
|
||||
},
|
||||
);
|
||||
// When the command ends, stop collecting coverage
|
||||
$wrappedEventDispatcher->subscribeTo(
|
||||
ConsoleTerminateEvent::class,
|
||||
static function () use (&$coverage, $exportCoverage): void {
|
||||
$id = env('COVERAGE_ID');
|
||||
if ($id === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$coverage?->stop();
|
||||
$exportCoverage('cli');
|
||||
},
|
||||
);
|
||||
|
||||
$app->setDispatcher(new class ($wrappedEventDispatcher) implements EventDispatcherInterface {
|
||||
public function __construct(private EventDispatcher $wrappedDispatcher)
|
||||
{
|
||||
}
|
||||
|
||||
public function dispatch(object $event, ?string $eventName = null): object
|
||||
{
|
||||
$this->wrappedDispatcher->dispatch($event);
|
||||
return $event;
|
||||
}
|
||||
});
|
||||
|
||||
return $app;
|
||||
},
|
||||
],
|
||||
] : [],
|
||||
],
|
||||
|
||||
'entity_manager' => [
|
||||
|
@ -172,6 +258,7 @@ return [
|
|||
|
||||
'data_fixtures' => [
|
||||
'paths' => [
|
||||
// TODO These are used for CLI tests too, so maybe should be somewhere else
|
||||
__DIR__ . '/../../module/Rest/test-api/Fixtures',
|
||||
],
|
||||
],
|
||||
|
|
73
data/infra/roadrunner.Dockerfile
Normal file
73
data/infra/roadrunner.Dockerfile
Normal file
|
@ -0,0 +1,73 @@
|
|||
FROM php:8.1.9-alpine3.16
|
||||
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
|
||||
|
||||
RUN apk update
|
||||
|
||||
# Install common php extensions
|
||||
RUN docker-php-ext-install pdo_mysql
|
||||
RUN docker-php-ext-install calendar
|
||||
|
||||
RUN apk add --no-cache oniguruma-dev
|
||||
RUN docker-php-ext-install mbstring
|
||||
|
||||
RUN apk add --no-cache sqlite-libs
|
||||
RUN apk add --no-cache sqlite-dev
|
||||
RUN docker-php-ext-install pdo_sqlite
|
||||
|
||||
RUN apk add --no-cache icu-dev
|
||||
RUN docker-php-ext-install intl
|
||||
|
||||
RUN apk add --no-cache libzip-dev zlib-dev
|
||||
RUN docker-php-ext-install zip
|
||||
|
||||
RUN apk add --no-cache libpng-dev
|
||||
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 docker-php-ext-install bcmath
|
||||
|
||||
# Install APCu extension
|
||||
ADD https://pecl.php.net/get/apcu-$APCU_VERSION.tgz /tmp/apcu.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/apcu \
|
||||
&& tar xf /tmp/apcu.tar.gz -C /usr/src/php/ext/apcu --strip-components=1 \
|
||||
&& docker-php-ext-configure apcu \
|
||||
&& docker-php-ext-install apcu \
|
||||
&& rm /tmp/apcu.tar.gz \
|
||||
&& rm /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini \
|
||||
&& 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 && \
|
||||
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
|
||||
|
||||
# Install composer
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
|
||||
|
||||
# Make home directory writable by anyone
|
||||
RUN chmod 777 /home
|
||||
|
||||
VOLUME /home/shlink
|
||||
WORKDIR /home/shlink
|
||||
|
||||
# Expose roadrunner port
|
||||
EXPOSE 8080
|
||||
|
||||
CMD \
|
||||
# Install dependencies if the vendor dir does not exist
|
||||
if [[ ! -d "./vendor" ]]; then /usr/local/bin/composer install ; fi && \
|
||||
# Download roadrunner binary
|
||||
if [[ ! -f "./bin/rr" ]]; then ./vendor/bin/rr get --no-interaction --location bin/ && chmod +x bin/rr ; fi && \
|
||||
# This forces the app to be started every second until the exit code is 0
|
||||
until ./bin/rr serve -c config/roadrunner/.rr.dev.yml; do sleep 1 ; done
|
|
@ -13,6 +13,12 @@ services:
|
|||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
|
||||
shlink_roadrunner:
|
||||
user: 1000:1000
|
||||
volumes:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
|
||||
shlink_db_mysql:
|
||||
user: 1000:1000
|
||||
volumes:
|
||||
|
|
|
@ -73,6 +73,30 @@ services:
|
|||
extra_hosts:
|
||||
- 'host.docker.internal:host-gateway'
|
||||
|
||||
shlink_roadrunner:
|
||||
container_name: shlink_roadrunner
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./data/infra/roadrunner.Dockerfile
|
||||
ports:
|
||||
- "8800:8080"
|
||||
volumes:
|
||||
- ./:/home/shlink
|
||||
- ./data/infra/php.ini:/usr/local/etc/php/php.ini
|
||||
links:
|
||||
- shlink_db_mysql
|
||||
- shlink_db_postgres
|
||||
- shlink_db_maria
|
||||
- shlink_db_ms
|
||||
- shlink_redis
|
||||
- shlink_mercure
|
||||
- shlink_mercure_proxy
|
||||
- shlink_rabbitmq
|
||||
environment:
|
||||
LC_ALL: C
|
||||
extra_hosts:
|
||||
- 'host.docker.internal:host-gateway'
|
||||
|
||||
shlink_db_mysql:
|
||||
container_name: shlink_db_mysql
|
||||
image: mysql:5.7
|
||||
|
@ -144,8 +168,8 @@ services:
|
|||
- "3080:80"
|
||||
environment:
|
||||
SERVER_NAME: ":80"
|
||||
MERCURE_PUBLISHER_JWT_KEY: mercure_jwt_key
|
||||
MERCURE_SUBSCRIBER_JWT_KEY: mercure_jwt_key
|
||||
MERCURE_PUBLISHER_JWT_KEY: mercure_jwt_key_long_enough_to_avoid_error
|
||||
MERCURE_SUBSCRIBER_JWT_KEY: mercure_jwt_key_long_enough_to_avoid_error
|
||||
MERCURE_EXTRA_DIRECTIVES: "cors_origins https://app.shlink.io http://localhost:3000 http://127.0.0.1:3000"
|
||||
|
||||
shlink_rabbitmq:
|
||||
|
|
25
docker/build
25
docker/build
|
@ -1,25 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
PLATFORMS="linux/arm/v7,linux/arm64/v8,linux/amd64"
|
||||
DOCKER_IMAGE="shlinkio/shlink"
|
||||
|
||||
# If ref is not develop, then this is a tag. Build that docker tag and also "stable"
|
||||
if [[ "$GITHUB_REF" != *"develop"* ]]; then
|
||||
VERSION=${GITHUB_REF#refs/tags/v}
|
||||
TAGS="-t ${DOCKER_IMAGE}:${VERSION}"
|
||||
# Push stable tag only if this is not an alpha or beta tag
|
||||
[[ $GITHUB_REF != *"alpha"* && $GITHUB_REF != *"beta"* ]] && TAGS="${TAGS} -t ${DOCKER_IMAGE}:stable"
|
||||
|
||||
docker buildx build --push \
|
||||
--build-arg SHLINK_VERSION=${VERSION} \
|
||||
--platform ${PLATFORMS} \
|
||||
${TAGS} .
|
||||
|
||||
# If build branch is develop, build latest
|
||||
elif [[ "$GITHUB_REF" == *"develop"* ]]; then
|
||||
docker buildx build --push \
|
||||
--platform ${PLATFORMS} \
|
||||
-t ${DOCKER_IMAGE}:latest .
|
||||
fi
|
|
@ -6,11 +6,14 @@ namespace Shlinkio\Shlink;
|
|||
|
||||
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
||||
|
||||
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
||||
|
||||
return [
|
||||
|
||||
'logger' => [
|
||||
'Shlink' => [
|
||||
'type' => LoggerType::STREAM->value,
|
||||
'destination' => runningInRoadRunner() ? 'php://stderr' : 'php://stdout',
|
||||
],
|
||||
],
|
||||
|
||||
|
|
|
@ -13,24 +13,36 @@ echo "Updating database..."
|
|||
php bin/cli db:migrate -n ${flags}
|
||||
|
||||
echo "Generating proxies..."
|
||||
php vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies -n ${flags}
|
||||
php bin/doctrine orm:generate-proxies -n ${flags}
|
||||
|
||||
echo "Clearing entities cache..."
|
||||
php vendor/doctrine/orm/bin/doctrine.php orm:clear-cache:metadata -n ${flags}
|
||||
php bin/doctrine orm:clear-cache:metadata -n ${flags}
|
||||
|
||||
# Try to download GeoLite2 db file only if the license key env var was defined
|
||||
if [ ! -z "${GEOLITE_LICENSE_KEY}" ]; then
|
||||
# Try to download GeoLite2 db file only if the license key env var was defined and skipping was not explicitly set
|
||||
if [ ! -z "${GEOLITE_LICENSE_KEY}" ] && [ "${SKIP_INITIAL_GEOLITE_DOWNLOAD}" != "true" ]; then
|
||||
echo "Downloading GeoLite2 db file..."
|
||||
php bin/cli visit:download-db -n ${flags}
|
||||
fi
|
||||
|
||||
# Periodically run visit:locate every hour, if ENABLE_PERIODIC_VISIT_LOCATE=true was provided
|
||||
if [ $ENABLE_PERIODIC_VISIT_LOCATE ]; then
|
||||
if [ "${ENABLE_PERIODIC_VISIT_LOCATE}" = "true" ]; then
|
||||
echo "Configuring periodic visit location..."
|
||||
echo "0 * * * * php /etc/shlink/bin/cli visit:locate -q" > /etc/crontabs/root
|
||||
/usr/sbin/crond &
|
||||
fi
|
||||
|
||||
# When restarting the container, openswoole might think it is already in execution
|
||||
# This forces the app to be started every second until the exit code is 0
|
||||
until php vendor/bin/laminas mezzio:swoole:start; do sleep 1 ; done
|
||||
# RoadRunner config needs these to have been set, so falling back to default values if not set yet
|
||||
if [ "$SHLINK_RUNTIME" == 'rr' ]; then
|
||||
export PORT="${PORT:-"8080"}"
|
||||
# Default to 0 so that RoadRunner decides the number of workers based on the amount of logical CPUs
|
||||
export WEB_WORKER_NUM="${WEB_WORKER_NUM:-"0"}"
|
||||
export TASK_WORKER_NUM="${TASK_WORKER_NUM:-"0"}"
|
||||
fi
|
||||
|
||||
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then
|
||||
# When restarting the container, openswoole might think it is already in execution
|
||||
# This forces the app to be started every second until the exit code is 0
|
||||
until php vendor/bin/laminas mezzio:swoole:start; do sleep 1 ; done
|
||||
elif [ "$SHLINK_RUNTIME" == 'rr' ]; then
|
||||
./bin/rr serve -c config/roadrunner/.rr.yml
|
||||
fi
|
||||
|
|
9
docs/swagger/examples/short-url-invalid-args-v3.json
Normal file
9
docs/swagger/examples/short-url-invalid-args-v3.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"value": {
|
||||
"title": "Invalid data",
|
||||
"type": "https://shlink.io/api/error/invalid-data",
|
||||
"detail": "Provided data is not valid",
|
||||
"status": 400,
|
||||
"invalidElements": ["maxVisits", "validSince"]
|
||||
}
|
||||
}
|
9
docs/swagger/examples/short-url-not-found-v3.json
Normal file
9
docs/swagger/examples/short-url-not-found-v3.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"value": {
|
||||
"detail": "No URL found with short code \"abc123\"",
|
||||
"title": "Short URL not found",
|
||||
"type": "https://shlink.io/api/error/short-url-not-found",
|
||||
"status": 404,
|
||||
"shortCode": "abc123"
|
||||
}
|
||||
}
|
9
docs/swagger/examples/tag-not-found-v3.json
Normal file
9
docs/swagger/examples/tag-not-found-v3.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"value": {
|
||||
"detail": "Tag with name \"foo\" could not be found",
|
||||
"title": "Tag not found",
|
||||
"type": "https://shlink.io/api/error/tag-not-found",
|
||||
"status": 404,
|
||||
"tag": "foo"
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"3",
|
||||
"2",
|
||||
"1"
|
||||
]
|
||||
|
|
|
@ -327,11 +327,11 @@
|
|||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "A URL that could not be verified, if the error type is INVALID_URL"
|
||||
"description": "A URL that could not be verified, if the error type is https://shlink.io/api/error/invalid-url"
|
||||
},
|
||||
"customSlug": {
|
||||
"type": "string",
|
||||
"description": "Provided custom slug when the error type is INVALID_SLUG"
|
||||
"description": "Provided custom slug when the error type is https://shlink.io/api/error/non-unique-slug"
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
|
@ -342,10 +342,31 @@
|
|||
]
|
||||
},
|
||||
"examples": {
|
||||
"Invalid arguments": {
|
||||
"$ref": "../examples/short-url-invalid-args.json"
|
||||
"Invalid arguments with API v3 and newer": {
|
||||
"$ref": "../examples/short-url-invalid-args-v3.json"
|
||||
},
|
||||
"Invalid long URL": {
|
||||
"Invalid long URL with API v3 and newer": {
|
||||
"value": {
|
||||
"title": "Invalid URL",
|
||||
"type": "https://shlink.io/api/error/invalid-url",
|
||||
"detail": "Provided URL foo is invalid. Try with a different one.",
|
||||
"status": 400,
|
||||
"url": "https://invalid-url.com"
|
||||
}
|
||||
},
|
||||
"Non-unique slug with API v3 and newer": {
|
||||
"value": {
|
||||
"title": "Invalid custom slug",
|
||||
"type": "https://shlink.io/api/error/non-unique-slug",
|
||||
"detail": "Provided slug \"my-slug\" is already in use.",
|
||||
"status": 400,
|
||||
"customSlug": "my-slug"
|
||||
}
|
||||
},
|
||||
"Invalid arguments previous to API v3": {
|
||||
"$ref": "../examples/short-url-invalid-args-v2.json"
|
||||
},
|
||||
"Invalid long URL previous to API v3": {
|
||||
"value": {
|
||||
"title": "Invalid URL",
|
||||
"type": "INVALID_URL",
|
||||
|
@ -354,7 +375,7 @@
|
|||
"url": "https://invalid-url.com"
|
||||
}
|
||||
},
|
||||
"Non-unique slug": {
|
||||
"Non-unique slug previous to API v3": {
|
||||
"value": {
|
||||
"title": "Invalid custom slug",
|
||||
"type": "INVALID_SLUG",
|
||||
|
|
|
@ -85,19 +85,39 @@
|
|||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
"example": {
|
||||
"title": "Invalid URL",
|
||||
"type": "INVALID_URL",
|
||||
"detail": "Provided URL foo is invalid. Try with a different one.",
|
||||
"status": 400,
|
||||
"url": "https://invalid-url.com"
|
||||
"examples": {
|
||||
"API v3 and newer": {
|
||||
"value": {
|
||||
"title": "Invalid URL",
|
||||
"type": "https://shlink.io/api/error/invalid-url",
|
||||
"detail": "Provided URL foo is invalid. Try with a different one.",
|
||||
"status": 400,
|
||||
"url": "https://invalid-url.com"
|
||||
}
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"value": {
|
||||
"title": "Invalid URL",
|
||||
"type": "INVALID_URL",
|
||||
"detail": "Provided URL foo is invalid. Try with a different one.",
|
||||
"status": 400,
|
||||
"url": "https://invalid-url.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "INVALID_URL"
|
||||
"examples": {
|
||||
"API v3 and newer": {
|
||||
"value": "https://shlink.io/api/error/invalid-url"
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"value": "INVALID_URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -83,8 +83,11 @@
|
|||
]
|
||||
},
|
||||
"examples": {
|
||||
"Not found": {
|
||||
"$ref": "../examples/short-url-not-found.json"
|
||||
"API v3 and newer": {
|
||||
"$ref": "../examples/short-url-not-found-v3.json"
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"$ref": "../examples/short-url-not-found-v2.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -203,8 +206,11 @@
|
|||
]
|
||||
},
|
||||
"examples": {
|
||||
"Invalid arguments": {
|
||||
"$ref": "../examples/short-url-invalid-args.json"
|
||||
"API v3 and newer": {
|
||||
"$ref": "../examples/short-url-invalid-args-v3.json"
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"$ref": "../examples/short-url-invalid-args-v2.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -236,8 +242,11 @@
|
|||
]
|
||||
},
|
||||
"examples": {
|
||||
"Not found": {
|
||||
"$ref": "../examples/short-url-not-found.json"
|
||||
"API v3 and newer": {
|
||||
"$ref": "../examples/short-url-not-found-v3.json"
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"$ref": "../examples/short-url-not-found-v2.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -318,13 +327,27 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"example": {
|
||||
"title": "Cannot delete short URL",
|
||||
"type": "INVALID_SHORT_URL_DELETION",
|
||||
"detail": "Impossible to delete short URL with short code \"abc123\", since it has more than \"15\" visits.",
|
||||
"status": 422,
|
||||
"shortCode": "abc123",
|
||||
"threshold": 15
|
||||
"examples": {
|
||||
"API v3 and newer": {
|
||||
"value": {
|
||||
"title": "Cannot delete short URL",
|
||||
"type": "https://shlink.io/api/error/invalid-short-url-deletion",
|
||||
"detail": "Impossible to delete short URL with short code \"abc123\", since it has more than \"15\" visits.",
|
||||
"status": 422,
|
||||
"shortCode": "abc123",
|
||||
"threshold": 15
|
||||
}
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"value": {
|
||||
"title": "Cannot delete short URL",
|
||||
"type": "INVALID_SHORT_URL_DELETION",
|
||||
"detail": "Impossible to delete short URL with short code \"abc123\", since it has more than \"15\" visits.",
|
||||
"status": 422,
|
||||
"shortCode": "abc123",
|
||||
"threshold": 15
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -355,8 +378,11 @@
|
|||
]
|
||||
},
|
||||
"examples": {
|
||||
"Not found": {
|
||||
"$ref": "../examples/short-url-not-found.json"
|
||||
"API v3 and newer": {
|
||||
"$ref": "../examples/short-url-not-found-v3.json"
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"$ref": "../examples/short-url-not-found-v2.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -151,8 +151,11 @@
|
|||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
"examples": {
|
||||
"Short URL not found": {
|
||||
"$ref": "../examples/short-url-not-found.json"
|
||||
"Short URL not found with API v3 and newer": {
|
||||
"$ref": "../examples/short-url-not-found-v3.json"
|
||||
},
|
||||
"Short URL not found previous to API v3": {
|
||||
"$ref": "../examples/short-url-not-found-v2.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -188,12 +188,25 @@
|
|||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
"example": {
|
||||
"title": "Invalid data",
|
||||
"type": "INVALID_ARGUMENT",
|
||||
"detail": "Provided data is not valid",
|
||||
"status": 400,
|
||||
"invalidElements": ["oldName", "newName"]
|
||||
"examples": {
|
||||
"API v3 and newer": {
|
||||
"value": {
|
||||
"title": "Invalid data",
|
||||
"type": "https://shlink.io/api/error/invalid-data",
|
||||
"detail": "Provided data is not valid",
|
||||
"status": 400,
|
||||
"invalidElements": ["oldName", "newName"]
|
||||
}
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"value": {
|
||||
"title": "Invalid data",
|
||||
"type": "INVALID_ARGUMENT",
|
||||
"detail": "Provided data is not valid",
|
||||
"status": 400,
|
||||
"invalidElements": ["oldName", "newName"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -205,11 +218,23 @@
|
|||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
"example": {
|
||||
"detail": "You are not allowed to rename tags",
|
||||
"title": "Forbidden tag operation",
|
||||
"type": "FORBIDDEN_OPERATION",
|
||||
"status": 403
|
||||
"examples": {
|
||||
"API v3 and newer": {
|
||||
"value": {
|
||||
"detail": "You are not allowed to rename tags",
|
||||
"title": "Forbidden tag operation",
|
||||
"type": "https://shlink.io/api/error/forbidden-tag-operation",
|
||||
"status": 403
|
||||
}
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"value": {
|
||||
"detail": "You are not allowed to rename tags",
|
||||
"title": "Forbidden tag operation",
|
||||
"type": "FORBIDDEN_OPERATION",
|
||||
"status": 403
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -222,8 +247,11 @@
|
|||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
"examples": {
|
||||
"Tag not found": {
|
||||
"$ref": "../examples/tag-not-found.json"
|
||||
"API v3 and newer": {
|
||||
"$ref": "../examples/tag-not-found-v3.json"
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"$ref": "../examples/tag-not-found-v2.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -236,13 +264,27 @@
|
|||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
"example": {
|
||||
"detail": "You cannot rename tag foo, because it already exists",
|
||||
"title": "Tag conflict",
|
||||
"type": "TAG_CONFLICT",
|
||||
"status": 409,
|
||||
"oldName": "bar",
|
||||
"newName": "foo"
|
||||
"examples": {
|
||||
"API v3 and newer": {
|
||||
"value": {
|
||||
"detail": "You cannot rename tag foo, because it already exists",
|
||||
"title": "Tag conflict",
|
||||
"type": "https://shlink.io/api/error/tag-conflict",
|
||||
"status": 409,
|
||||
"oldName": "bar",
|
||||
"newName": "foo"
|
||||
}
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"value": {
|
||||
"detail": "You cannot rename tag foo, because it already exists",
|
||||
"title": "Tag conflict",
|
||||
"type": "TAG_CONFLICT",
|
||||
"status": 409,
|
||||
"oldName": "bar",
|
||||
"newName": "foo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -300,11 +342,23 @@
|
|||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
"example": {
|
||||
"detail": "You are not allowed to delete tags",
|
||||
"title": "Forbidden tag operation",
|
||||
"type": "FORBIDDEN_OPERATION",
|
||||
"status": 403
|
||||
"examples": {
|
||||
"API v3 and newer": {
|
||||
"value": {
|
||||
"detail": "You are not allowed to delete tags",
|
||||
"title": "Forbidden tag operation",
|
||||
"type": "https://shlink.io/api/error/forbidden-tag-operation",
|
||||
"status": 403
|
||||
}
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"value": {
|
||||
"detail": "You are not allowed to delete tags",
|
||||
"title": "Forbidden tag operation",
|
||||
"type": "FORBIDDEN_OPERATION",
|
||||
"status": 403
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,12 +94,25 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"example": {
|
||||
"title": "Invalid data",
|
||||
"type": "INVALID_ARGUMENT",
|
||||
"detail": "Provided data is not valid",
|
||||
"status": 400,
|
||||
"invalidElements": ["domain", "invalidShortUrlRedirect"]
|
||||
"examples": {
|
||||
"API v3 and newer": {
|
||||
"value": {
|
||||
"title": "Invalid data",
|
||||
"type": "https://shlink.io/api/error/invalid-data",
|
||||
"detail": "Provided data is not valid",
|
||||
"status": 400,
|
||||
"invalidElements": ["domain", "invalidShortUrlRedirect"]
|
||||
}
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"value": {
|
||||
"title": "Invalid data",
|
||||
"type": "INVALID_ARGUMENT",
|
||||
"detail": "Provided data is not valid",
|
||||
"status": 400,
|
||||
"invalidElements": ["domain", "invalidShortUrlRedirect"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -147,12 +147,25 @@
|
|||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
"example": {
|
||||
"detail": "Domain with authority \"example.com\" could not be found",
|
||||
"title": "Domain not found",
|
||||
"type": "DOMAIN_NOT_FOUND",
|
||||
"status": 404,
|
||||
"authority": "example.com"
|
||||
"examples": {
|
||||
"API v3 and newer": {
|
||||
"value": {
|
||||
"detail": "Domain with authority \"example.com\" could not be found",
|
||||
"title": "Domain not found",
|
||||
"type": "https://shlink.io/api/error/domain-not-found",
|
||||
"status": 404,
|
||||
"authority": "example.com"
|
||||
}
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"value": {
|
||||
"detail": "Domain with authority \"example.com\" could not be found",
|
||||
"title": "Domain not found",
|
||||
"type": "DOMAIN_NOT_FOUND",
|
||||
"status": 404,
|
||||
"authority": "example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,11 +39,23 @@
|
|||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
"example": {
|
||||
"title": "Mercure integration not configured",
|
||||
"type": "MERCURE_NOT_CONFIGURED",
|
||||
"detail": "This Shlink instance is not integrated with a mercure hub.",
|
||||
"status": 501
|
||||
"examples": {
|
||||
"API v3 and newer": {
|
||||
"value": {
|
||||
"title": "Mercure integration not configured",
|
||||
"type": "https://shlink.io/api/error/mercure-not-configured",
|
||||
"detail": "This Shlink instance is not integrated with a mercure hub.",
|
||||
"status": 501
|
||||
}
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"value": {
|
||||
"title": "Mercure integration not configured",
|
||||
"type": "MERCURE_NOT_CONFIGURED",
|
||||
"detail": "This Shlink instance is not integrated with a mercure hub.",
|
||||
"status": 501
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -148,8 +148,12 @@
|
|||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
"examples": {
|
||||
"Tag not found": {
|
||||
"$ref": "../examples/tag-not-found.json"
|
||||
|
||||
"API v3 and newer": {
|
||||
"$ref": "../examples/tag-not-found-v3.json"
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"$ref": "../examples/tag-not-found-v2.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"info": {
|
||||
"title": "Shlink",
|
||||
"description": "Shlink, the self-hosted URL shortener",
|
||||
"version": "2.0"
|
||||
"version": "3.0"
|
||||
},
|
||||
|
||||
"externalDocs": {
|
||||
|
|
2
indocker
2
indocker
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Run docker containers if they are not up yet
|
||||
if ! [[ $(docker ps | grep shlink_swoole) ]]; then
|
||||
if ! [[ $(docker ps | grep shlink) ]]; then
|
||||
docker-compose up -d
|
||||
fi
|
||||
|
||||
|
|
24
infection-cli.json
Normal file
24
infection-cli.json
Normal 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
|
||||
}
|
||||
}
|
|
@ -19,7 +19,6 @@ use Shlinkio\Shlink\Core\Tag\TagService;
|
|||
use Shlinkio\Shlink\Core\Visit;
|
||||
use Shlinkio\Shlink\Installer\Factory\ProcessHelperFactory;
|
||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
|
||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Symfony\Component\Console as SymfonyCli;
|
||||
use Symfony\Component\Lock\LockFactory;
|
||||
|
@ -35,7 +34,7 @@ return [
|
|||
SymfonyCli\Helper\ProcessHelper::class => ProcessHelperFactory::class,
|
||||
PhpExecutableFinder::class => InvokableFactory::class,
|
||||
|
||||
Util\GeolocationDbUpdater::class => ConfigAbstractFactory::class,
|
||||
GeoLite\GeolocationDbUpdater::class => ConfigAbstractFactory::class,
|
||||
Util\ProcessRunner::class => ConfigAbstractFactory::class,
|
||||
|
||||
ApiKey\RoleResolver::class => ConfigAbstractFactory::class,
|
||||
|
@ -70,7 +69,7 @@ return [
|
|||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
Util\GeolocationDbUpdater::class => [
|
||||
GeoLite\GeolocationDbUpdater::class => [
|
||||
DbUpdater::class,
|
||||
Reader::class,
|
||||
LOCAL_LOCK_FACTORY,
|
||||
|
@ -92,10 +91,10 @@ return [
|
|||
Command\ShortUrl\GetShortUrlVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
||||
Command\ShortUrl\DeleteShortUrlCommand::class => [Service\ShortUrl\DeleteShortUrlService::class],
|
||||
|
||||
Command\Visit\DownloadGeoLiteDbCommand::class => [Util\GeolocationDbUpdater::class],
|
||||
Command\Visit\DownloadGeoLiteDbCommand::class => [GeoLite\GeolocationDbUpdater::class],
|
||||
Command\Visit\LocateVisitsCommand::class => [
|
||||
Visit\VisitLocator::class,
|
||||
IpLocationResolverInterface::class,
|
||||
Visit\VisitToLocationHelper::class,
|
||||
LockFactory::class,
|
||||
],
|
||||
Command\Visit\GetOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
||||
|
|
|
@ -22,7 +22,7 @@ use const Shlinkio\Shlink\MIGRATIONS_TABLE;
|
|||
class CreateDatabaseCommand extends AbstractDatabaseCommand
|
||||
{
|
||||
public const NAME = 'db:create';
|
||||
public const DOCTRINE_SCRIPT = 'vendor/doctrine/orm/bin/doctrine.php';
|
||||
public const DOCTRINE_SCRIPT = 'bin/doctrine';
|
||||
public const DOCTRINE_CREATE_SCHEMA_COMMAND = 'orm:schema-tool:create';
|
||||
|
||||
public function __construct(
|
||||
|
|
|
@ -40,7 +40,7 @@ class CreateShortUrlCommand extends Command
|
|||
private readonly UrlShortenerOptions $options,
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->defaultDomain = $this->options->domain()['hostname'] ?? '';
|
||||
$this->defaultDomain = $this->options->domain['hostname'] ?? '';
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
|
@ -158,7 +158,7 @@ class CreateShortUrlCommand extends Command
|
|||
$tags = unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
|
||||
$customSlug = $input->getOption('custom-slug');
|
||||
$maxVisits = $input->getOption('max-visits');
|
||||
$shortCodeLength = $input->getOption('short-code-length') ?? $this->options->defaultShortCodesLength();
|
||||
$shortCodeLength = $input->getOption('short-code-length') ?? $this->options->defaultShortCodesLength;
|
||||
$doValidateUrl = $input->getOption('validate-url');
|
||||
|
||||
try {
|
||||
|
@ -175,7 +175,7 @@ class CreateShortUrlCommand extends Command
|
|||
ShortUrlInputFilter::TAGS => $tags,
|
||||
ShortUrlInputFilter::CRAWLABLE => $input->getOption('crawlable'),
|
||||
ShortUrlInputFilter::FORWARD_QUERY => !$input->getOption('no-forward-query'),
|
||||
EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->value => $this->options->multiSegmentSlugsEnabled(),
|
||||
EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->value => $this->options->multiSegmentSlugsEnabled,
|
||||
]));
|
||||
|
||||
$io->writeln([
|
||||
|
|
|
@ -5,8 +5,8 @@ declare(strict_types=1);
|
|||
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
|
|
@ -11,11 +11,11 @@ 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\Model\UnlocatableIpType;
|
||||
use Shlinkio\Shlink\Core\Visit\VisitGeolocationHelperInterface;
|
||||
use Shlinkio\Shlink\Core\Visit\VisitLocatorInterface;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\Core\Visit\VisitToLocationHelperInterface;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
@ -34,8 +34,8 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
|||
private SymfonyStyle $io;
|
||||
|
||||
public function __construct(
|
||||
private VisitLocatorInterface $visitLocator,
|
||||
private IpLocationResolverInterface $ipLocationResolver,
|
||||
private readonly VisitLocatorInterface $visitLocator,
|
||||
private readonly VisitToLocationHelperInterface $visitToLocation,
|
||||
LockFactory $locker,
|
||||
) {
|
||||
parent::__construct($locker);
|
||||
|
@ -132,39 +132,33 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
|||
*/
|
||||
public function geolocateVisit(Visit $visit): Location
|
||||
{
|
||||
if (! $visit->hasRemoteAddr()) {
|
||||
$this->io->writeln(
|
||||
'<comment>Ignored visit with no IP address</comment>',
|
||||
OutputInterface::VERBOSITY_VERBOSE,
|
||||
);
|
||||
throw IpCannotBeLocatedException::forEmptyAddress();
|
||||
}
|
||||
|
||||
$ipAddr = $visit->getRemoteAddr() ?? '';
|
||||
$ipAddr = $visit->getRemoteAddr() ?? '?';
|
||||
$this->io->write(sprintf('Processing IP <fg=blue>%s</>', $ipAddr));
|
||||
if ($ipAddr === IpAddress::LOCALHOST) {
|
||||
$this->io->writeln(' [<comment>Ignored localhost address</comment>]');
|
||||
throw IpCannotBeLocatedException::forLocalhost();
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->ipLocationResolver->resolveIpLocation($ipAddr);
|
||||
} catch (WrongIpException $e) {
|
||||
$this->io->writeln(' [<fg=red>An error occurred while locating IP. Skipped</>]');
|
||||
if ($this->io->isVerbose()) {
|
||||
return $this->visitToLocation->resolveVisitLocation($visit);
|
||||
} catch (IpCannotBeLocatedException $e) {
|
||||
$this->io->writeln(match ($e->type) {
|
||||
UnlocatableIpType::EMPTY_ADDRESS => ' [<comment>Ignored visit with no IP address</comment>]',
|
||||
UnlocatableIpType::LOCALHOST => ' [<comment>Ignored localhost address</comment>]',
|
||||
UnlocatableIpType::ERROR => ' [<fg=red>An error occurred while locating IP. Skipped</>]',
|
||||
});
|
||||
|
||||
if ($e->type === UnlocatableIpType::ERROR && $this->io->isVerbose()) {
|
||||
$this->getApplication()?->renderThrowable($e, $this->io);
|
||||
}
|
||||
|
||||
throw IpCannotBeLocatedException::forError($e);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function onVisitLocated(VisitLocation $visitLocation, Visit $visit): void
|
||||
{
|
||||
$message = ! $visitLocation->isEmpty()
|
||||
? sprintf(' [<info>Address located in "%s"</info>]', $visitLocation->getCountryName())
|
||||
: ' [<comment>Address not found</comment>]';
|
||||
$this->io->writeln($message);
|
||||
if (! $visitLocation->isEmpty()) {
|
||||
$this->io->writeln(sprintf(' [<info>Address located in "%s"</info>]', $visitLocation->getCountryName()));
|
||||
} elseif ($visit->hasRemoteAddr() && $visit->getRemoteAddr() !== IpAddress::LOCALHOST) {
|
||||
$this->io->writeln(' <comment>[Could not locate address]</comment>');
|
||||
}
|
||||
}
|
||||
|
||||
private function checkDbUpdate(): void
|
||||
|
|
|
@ -13,16 +13,15 @@ class GeolocationDbUpdateFailedException extends RuntimeException implements Exc
|
|||
{
|
||||
private bool $olderDbExists;
|
||||
|
||||
private function __construct(string $message, int $code, ?Throwable $previous)
|
||||
private function __construct(string $message, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
parent::__construct($message, 0, $previous);
|
||||
}
|
||||
|
||||
public static function withOlderDb(?Throwable $prev = null): self
|
||||
{
|
||||
$e = new self(
|
||||
'An error occurred while updating geolocation database, but an older DB is already present.',
|
||||
0,
|
||||
$prev,
|
||||
);
|
||||
$e->olderDbExists = true;
|
||||
|
@ -34,7 +33,6 @@ class GeolocationDbUpdateFailedException extends RuntimeException implements Exc
|
|||
{
|
||||
$e = new self(
|
||||
'An error occurred while updating geolocation database, and an older version could not be found.',
|
||||
0,
|
||||
$prev,
|
||||
);
|
||||
$e->olderDbExists = false;
|
||||
|
@ -47,7 +45,7 @@ class GeolocationDbUpdateFailedException extends RuntimeException implements Exc
|
|||
$e = new self(sprintf(
|
||||
'Build epoch with value "%s" from existing geolocation database, could not be parsed to integer.',
|
||||
$buildEpoch,
|
||||
), 0, null);
|
||||
));
|
||||
$e->olderDbExists = true;
|
||||
|
||||
return $e;
|
||||
|
|
|
@ -17,7 +17,7 @@ class ApplicationFactory
|
|||
$appOptions = $container->get(AppOptions::class);
|
||||
|
||||
$commands = $config['commands'] ?? [];
|
||||
$app = new CliApp($appOptions->getName(), $appOptions->getVersion());
|
||||
$app = new CliApp($appOptions->name, $appOptions->version);
|
||||
$app->setCommandLoader(new ContainerCommandLoader($container, $commands));
|
||||
|
||||
return $app;
|
||||
|
|
|
@ -2,14 +2,16 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Util;
|
||||
namespace Shlinkio\Shlink\CLI\GeoLite;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use GeoIp2\Database\Reader;
|
||||
use MaxMind\Db\Reader\Metadata;
|
||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
use Shlinkio\Shlink\Core\Options\TrackingOptions;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\DbUpdateException;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\MissingLicenseException;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||
use Symfony\Component\Lock\LockFactory;
|
||||
|
||||
|
@ -20,27 +22,27 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
|||
private const LOCK_NAME = 'geolocation-db-update';
|
||||
|
||||
public function __construct(
|
||||
private DbUpdaterInterface $dbUpdater,
|
||||
private Reader $geoLiteDbReader,
|
||||
private LockFactory $locker,
|
||||
private TrackingOptions $trackingOptions,
|
||||
private readonly DbUpdaterInterface $dbUpdater,
|
||||
private readonly Reader $geoLiteDbReader,
|
||||
private readonly LockFactory $locker,
|
||||
private readonly TrackingOptions $trackingOptions,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws GeolocationDbUpdateFailedException
|
||||
*/
|
||||
public function checkDbUpdate(?callable $beforeDownload = null, ?callable $handleProgress = null): void
|
||||
public function checkDbUpdate(?callable $beforeDownload = null, ?callable $handleProgress = null): GeolocationResult
|
||||
{
|
||||
if ($this->trackingOptions->disableTracking() || $this->trackingOptions->disableIpTracking()) {
|
||||
return;
|
||||
if ($this->trackingOptions->disableTracking || $this->trackingOptions->disableIpTracking) {
|
||||
return GeolocationResult::CHECK_SKIPPED;
|
||||
}
|
||||
|
||||
$lock = $this->locker->createLock(self::LOCK_NAME);
|
||||
$lock->acquire(true); // Block until lock is released
|
||||
|
||||
try {
|
||||
$this->downloadIfNeeded($beforeDownload, $handleProgress);
|
||||
return $this->downloadIfNeeded($beforeDownload, $handleProgress);
|
||||
} finally {
|
||||
$lock->release();
|
||||
}
|
||||
|
@ -49,17 +51,18 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
|||
/**
|
||||
* @throws GeolocationDbUpdateFailedException
|
||||
*/
|
||||
private function downloadIfNeeded(?callable $beforeDownload, ?callable $handleProgress): void
|
||||
private function downloadIfNeeded(?callable $beforeDownload, ?callable $handleProgress): GeolocationResult
|
||||
{
|
||||
if (! $this->dbUpdater->databaseFileExists()) {
|
||||
$this->downloadNewDb(false, $beforeDownload, $handleProgress);
|
||||
return;
|
||||
return $this->downloadNewDb(false, $beforeDownload, $handleProgress);
|
||||
}
|
||||
|
||||
$meta = $this->geoLiteDbReader->metadata();
|
||||
if ($this->buildIsTooOld($meta)) {
|
||||
$this->downloadNewDb(true, $beforeDownload, $handleProgress);
|
||||
return $this->downloadNewDb(true, $beforeDownload, $handleProgress);
|
||||
}
|
||||
|
||||
return GeolocationResult::DB_IS_UP_TO_DATE;
|
||||
}
|
||||
|
||||
private function buildIsTooOld(Metadata $meta): bool
|
||||
|
@ -92,15 +95,22 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
|||
/**
|
||||
* @throws GeolocationDbUpdateFailedException
|
||||
*/
|
||||
private function downloadNewDb(bool $olderDbExists, ?callable $beforeDownload, ?callable $handleProgress): void
|
||||
{
|
||||
private function downloadNewDb(
|
||||
bool $olderDbExists,
|
||||
?callable $beforeDownload,
|
||||
?callable $handleProgress,
|
||||
): GeolocationResult {
|
||||
if ($beforeDownload !== null) {
|
||||
$beforeDownload($olderDbExists);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->dbUpdater->downloadFreshCopy($this->wrapHandleProgressCallback($handleProgress, $olderDbExists));
|
||||
} catch (RuntimeException $e) {
|
||||
return $olderDbExists ? GeolocationResult::DB_UPDATED : GeolocationResult::DB_CREATED;
|
||||
} catch (MissingLicenseException) {
|
||||
// If there's no license key, just ignore the error
|
||||
return GeolocationResult::CHECK_SKIPPED;
|
||||
} catch (DbUpdateException | WrongIpException $e) {
|
||||
throw $olderDbExists
|
||||
? GeolocationDbUpdateFailedException::withOlderDb($e)
|
||||
: GeolocationDbUpdateFailedException::withoutOlderDb($e);
|
||||
|
@ -113,6 +123,6 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
|||
return null;
|
||||
}
|
||||
|
||||
return fn (int $total, int $downloaded) => $handleProgress($total, $downloaded, $olderDbExists);
|
||||
return static fn (int $total, int $downloaded) => $handleProgress($total, $downloaded, $olderDbExists);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Util;
|
||||
namespace Shlinkio\Shlink\CLI\GeoLite;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
|
||||
|
@ -11,5 +11,8 @@ interface GeolocationDbUpdaterInterface
|
|||
/**
|
||||
* @throws GeolocationDbUpdateFailedException
|
||||
*/
|
||||
public function checkDbUpdate(?callable $beforeDownload = null, ?callable $handleProgress = null): void;
|
||||
public function checkDbUpdate(
|
||||
?callable $beforeDownload = null,
|
||||
?callable $handleProgress = null,
|
||||
): GeolocationResult;
|
||||
}
|
11
module/CLI/src/GeoLite/GeolocationResult.php
Normal file
11
module/CLI/src/GeoLite/GeolocationResult.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\GeoLite;
|
||||
|
||||
enum GeolocationResult
|
||||
{
|
||||
case CHECK_SKIPPED;
|
||||
case DB_CREATED;
|
||||
case DB_UPDATED;
|
||||
case DB_IS_UP_TO_DATE;
|
||||
}
|
21
module/CLI/test-cli/Command/GenerateApiKeyTest.php
Normal file
21
module/CLI/test-cli/Command/GenerateApiKeyTest.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioCliTest\Shlink\CLI\Command;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
|
||||
|
||||
class GenerateApiKeyTest extends CliTestCase
|
||||
{
|
||||
/** @test */
|
||||
public function outputIsCorrect(): void
|
||||
{
|
||||
[$output, $exitCode] = $this->exec([GenerateKeyCommand::NAME]);
|
||||
|
||||
self::assertStringContainsString('[OK] Generated API key', $output);
|
||||
self::assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode);
|
||||
}
|
||||
}
|
63
module/CLI/test-cli/Command/ListApiKeysTest.php
Normal file
63
module/CLI/test-cli/Command/ListApiKeysTest.php
Normal file
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioCliTest\Shlink\CLI\Command;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
|
||||
|
||||
class ListApiKeysTest extends CliTestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideFlags
|
||||
*/
|
||||
public function generatesExpectedOutput(array $flags, string $expectedOutput): void
|
||||
{
|
||||
[$output, $exitCode] = $this->exec([ListKeysCommand::NAME, ...$flags]);
|
||||
|
||||
self::assertEquals($expectedOutput, $output);
|
||||
self::assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode);
|
||||
}
|
||||
|
||||
public function provideFlags(): iterable
|
||||
{
|
||||
$expiredApiKeyDate = Chronos::now()->subDay()->startOfDay()->toAtomString();
|
||||
$enabledOnlyOutput = <<<OUT
|
||||
+-----------------+------+---------------------------+--------------------------+
|
||||
| Key | Name | Expiration date | Roles |
|
||||
+-----------------+------+---------------------------+--------------------------+
|
||||
| valid_api_key | - | - | Admin |
|
||||
+-----------------+------+---------------------------+--------------------------+
|
||||
| expired_api_key | - | {$expiredApiKeyDate} | Admin |
|
||||
+-----------------+------+---------------------------+--------------------------+
|
||||
| author_api_key | - | - | Author only |
|
||||
+-----------------+------+---------------------------+--------------------------+
|
||||
| domain_api_key | - | - | Domain only: example.com |
|
||||
+-----------------+------+---------------------------+--------------------------+
|
||||
|
||||
OUT;
|
||||
|
||||
yield 'no flags' => [[], <<<OUT
|
||||
+------------------+------+------------+---------------------------+--------------------------+
|
||||
| Key | Name | Is enabled | Expiration date | Roles |
|
||||
+------------------+------+------------+---------------------------+--------------------------+
|
||||
| valid_api_key | - | +++ | - | Admin |
|
||||
+------------------+------+------------+---------------------------+--------------------------+
|
||||
| disabled_api_key | - | --- | - | Admin |
|
||||
+------------------+------+------------+---------------------------+--------------------------+
|
||||
| expired_api_key | - | --- | {$expiredApiKeyDate} | Admin |
|
||||
+------------------+------+------------+---------------------------+--------------------------+
|
||||
| author_api_key | - | +++ | - | Author only |
|
||||
+------------------+------+------------+---------------------------+--------------------------+
|
||||
| domain_api_key | - | +++ | - | Domain only: example.com |
|
||||
+------------------+------+------------+---------------------------+--------------------------+
|
||||
|
||||
OUT];
|
||||
yield '-e' => [['-e'], $enabledOnlyOutput];
|
||||
yield '--enabled-only' => [['--enabled-only'], $enabledOnlyOutput];
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ class DisableKeyCommandTest extends TestCase
|
|||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $apiKeyService;
|
||||
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new DisableKeyCommand($this->apiKeyService->reveal()));
|
||||
|
|
|
@ -23,7 +23,7 @@ class GenerateKeyCommandTest extends TestCase
|
|||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $apiKeyService;
|
||||
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
|
||||
$roleResolver = $this->prophesize(RoleResolverInterface::class);
|
||||
|
|
|
@ -23,7 +23,7 @@ class ListKeysCommandTest extends TestCase
|
|||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $apiKeyService;
|
||||
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new ListKeysCommand($this->apiKeyService->reveal()));
|
||||
|
|
|
@ -33,7 +33,7 @@ class CreateDatabaseCommandTest extends TestCase
|
|||
private ObjectProphecy $schemaManager;
|
||||
private ObjectProphecy $driver;
|
||||
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
$locker = $this->prophesize(LockFactory::class);
|
||||
$lock = $this->prophesize(LockInterface::class);
|
||||
|
|
|
@ -23,7 +23,7 @@ class MigrateDatabaseCommandTest extends TestCase
|
|||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $processHelper;
|
||||
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
$locker = $this->prophesize(LockFactory::class);
|
||||
$lock = $this->prophesize(LockInterface::class);
|
||||
|
|
|
@ -24,7 +24,7 @@ class DomainRedirectsCommandTest extends TestCase
|
|||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $domainService;
|
||||
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->domainService = $this->prophesize(DomainServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new DomainRedirectsCommand($this->domainService->reveal()));
|
||||
|
|
|
@ -23,7 +23,7 @@ class ListDomainsCommandTest extends TestCase
|
|||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $domainService;
|
||||
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->domainService = $this->prophesize(DomainServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new ListDomainsCommand($this->domainService->reveal()));
|
||||
|
@ -43,10 +43,10 @@ class ListDomainsCommandTest extends TestCase
|
|||
));
|
||||
|
||||
$listDomains = $this->domainService->listDomains()->willReturn([
|
||||
DomainItem::forDefaultDomain('foo.com', new NotFoundRedirectOptions([
|
||||
'base_url' => 'https://foo.com/default/base',
|
||||
'invalid_short_url' => 'https://foo.com/default/invalid',
|
||||
])),
|
||||
DomainItem::forDefaultDomain('foo.com', new NotFoundRedirectOptions(
|
||||
invalidShortUrl: 'https://foo.com/default/invalid',
|
||||
baseUrl: 'https://foo.com/default/base',
|
||||
)),
|
||||
DomainItem::forNonDefaultDomain(Domain::withAuthority('bar.com')),
|
||||
DomainItem::forNonDefaultDomain($bazDomain),
|
||||
]);
|
||||
|
|
|
@ -30,7 +30,7 @@ class CreateShortUrlCommandTest extends TestCase
|
|||
private ObjectProphecy $urlShortener;
|
||||
private ObjectProphecy $stringifier;
|
||||
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->urlShortener = $this->prophesize(UrlShortener::class);
|
||||
$this->stringifier = $this->prophesize(ShortUrlStringifierInterface::class);
|
||||
|
@ -39,7 +39,7 @@ class CreateShortUrlCommandTest extends TestCase
|
|||
$command = new CreateShortUrlCommand(
|
||||
$this->urlShortener->reveal(),
|
||||
$this->stringifier->reveal(),
|
||||
new UrlShortenerOptions(['defaultShortCodesLength' => 5, 'domain' => ['hostname' => self::DEFAULT_DOMAIN]]),
|
||||
new UrlShortenerOptions(domain: ['hostname' => self::DEFAULT_DOMAIN], defaultShortCodesLength: 5),
|
||||
);
|
||||
$this->commandTester = $this->testerForCommand($command);
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $service;
|
||||
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->service = $this->prophesize(DeleteShortUrlServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new DeleteShortUrlCommand($this->service->reveal()));
|
||||
|
|
|
@ -33,7 +33,7 @@ class GetShortUrlVisitsCommandTest extends TestCase
|
|||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $visitsHelper;
|
||||
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class);
|
||||
$command = new GetShortUrlVisitsCommand($this->visitsHelper->reveal());
|
||||
|
|
|
@ -33,7 +33,7 @@ class ListShortUrlsCommandTest extends TestCase
|
|||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $shortUrlService;
|
||||
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->shortUrlService = $this->prophesize(ShortUrlServiceInterface::class);
|
||||
$command = new ListShortUrlsCommand($this->shortUrlService->reveal(), new ShortUrlDataTransformer(
|
||||
|
|
|
@ -25,7 +25,7 @@ class ResolveUrlCommandTest extends TestCase
|
|||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $urlResolver;
|
||||
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new ResolveUrlCommand($this->urlResolver->reveal()));
|
||||
|
|
|
@ -18,7 +18,7 @@ class DeleteTagsCommandTest extends TestCase
|
|||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $tagService;
|
||||
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tagService = $this->prophesize(TagServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new DeleteTagsCommand($this->tagService->reveal()));
|
||||
|
|
|
@ -22,7 +22,7 @@ class ListTagsCommandTest extends TestCase
|
|||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $tagService;
|
||||
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tagService = $this->prophesize(TagServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new ListTagsCommand($this->tagService->reveal()));
|
||||
|
|
|
@ -21,7 +21,7 @@ class RenameTagCommandTest extends TestCase
|
|||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $tagService;
|
||||
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tagService = $this->prophesize(TagServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new RenameTagCommand($this->tagService->reveal()));
|
||||
|
|
|
@ -9,8 +9,9 @@ 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;
|
||||
use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
|
@ -97,11 +98,12 @@ class DownloadGeoLiteDbCommandTest extends TestCase
|
|||
|
||||
public function provideSuccessParams(): iterable
|
||||
{
|
||||
yield 'up to date db' => [function (): void {
|
||||
}, '[INFO] GeoLite2 db file is up to date.'];
|
||||
yield 'outdated db' => [function (array $args): void {
|
||||
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;
|
||||
$beforeDownload(true);
|
||||
|
||||
return GeolocationResult::DB_CREATED;
|
||||
}, '[OK] GeoLite2 db file properly downloaded.'];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,16 +10,16 @@ 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\Common\Util\IpAddress;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Visit\VisitGeolocationHelperInterface;
|
||||
use Shlinkio\Shlink\Core\Visit\VisitLocator;
|
||||
use Shlinkio\Shlink\Core\Visit\VisitToLocationHelperInterface;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
@ -36,14 +36,14 @@ class LocateVisitsCommandTest extends TestCase
|
|||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $visitService;
|
||||
private ObjectProphecy $ipResolver;
|
||||
private ObjectProphecy $visitToLocation;
|
||||
private ObjectProphecy $lock;
|
||||
private ObjectProphecy $downloadDbCommand;
|
||||
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->visitService = $this->prophesize(VisitLocator::class);
|
||||
$this->ipResolver = $this->prophesize(IpLocationResolverInterface::class);
|
||||
$this->visitToLocation = $this->prophesize(VisitToLocationHelperInterface::class);
|
||||
|
||||
$locker = $this->prophesize(Lock\LockFactory::class);
|
||||
$this->lock = $this->prophesize(Lock\LockInterface::class);
|
||||
|
@ -54,7 +54,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||
|
||||
$command = new LocateVisitsCommand(
|
||||
$this->visitService->reveal(),
|
||||
$this->ipResolver->reveal(),
|
||||
$this->visitToLocation->reveal(),
|
||||
$locker->reveal(),
|
||||
);
|
||||
|
||||
|
@ -84,7 +84,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||
$mockMethodBehavior,
|
||||
);
|
||||
$locateAllVisits = $this->visitService->locateAllVisits(Argument::cetera())->will($mockMethodBehavior);
|
||||
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn(
|
||||
$resolveIpLocation = $this->visitToLocation->resolveVisitLocation(Argument::any())->willReturn(
|
||||
Location::emptyInstance(),
|
||||
);
|
||||
|
||||
|
@ -117,36 +117,29 @@ class LocateVisitsCommandTest extends TestCase
|
|||
* @test
|
||||
* @dataProvider provideIgnoredAddresses
|
||||
*/
|
||||
public function localhostAndEmptyAddressesAreIgnored(?string $address, string $message): void
|
||||
public function localhostAndEmptyAddressesAreIgnored(IpCannotBeLocatedException $e, string $message): void
|
||||
{
|
||||
$visit = Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('', '', $address, ''));
|
||||
$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->ipResolver->resolveIpLocation(Argument::any())->willReturn(
|
||||
Location::emptyInstance(),
|
||||
);
|
||||
$resolveIpLocation = $this->visitToLocation->resolveVisitLocation(Argument::any())->willThrow($e);
|
||||
|
||||
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
self::assertStringContainsString('Processing IP', $output);
|
||||
self::assertStringContainsString($message, $output);
|
||||
if (empty($address)) {
|
||||
self::assertStringNotContainsString('Processing IP', $output);
|
||||
} else {
|
||||
self::assertStringContainsString('Processing IP', $output);
|
||||
}
|
||||
$locateVisits->shouldHaveBeenCalledOnce();
|
||||
$resolveIpLocation->shouldNotHaveBeenCalled();
|
||||
$resolveIpLocation->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideIgnoredAddresses(): iterable
|
||||
{
|
||||
yield 'with empty address' => ['', 'Ignored visit with no IP address'];
|
||||
yield 'with null address' => [null, 'Ignored visit with no IP address'];
|
||||
yield 'with localhost address' => [IpAddress::LOCALHOST, 'Ignored localhost address'];
|
||||
yield 'empty address' => [IpCannotBeLocatedException::forEmptyAddress(), 'Ignored visit with no IP address'];
|
||||
yield 'localhost address' => [IpCannotBeLocatedException::forLocalhost(), 'Ignored localhost address'];
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -158,7 +151,9 @@ class LocateVisitsCommandTest extends TestCase
|
|||
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(
|
||||
$this->invokeHelperMethods($visit, $location),
|
||||
);
|
||||
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willThrow(WrongIpException::class);
|
||||
$resolveIpLocation = $this->visitToLocation->resolveVisitLocation(Argument::any())->willThrow(
|
||||
IpCannotBeLocatedException::forError(WrongIpException::fromIpAddress('1.2.3.4')),
|
||||
);
|
||||
|
||||
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||
|
||||
|
@ -187,7 +182,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||
|
||||
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(function (): void {
|
||||
});
|
||||
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([]);
|
||||
$resolveIpLocation = $this->visitToLocation->resolveVisitLocation(Argument::any());
|
||||
|
||||
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
|
|
@ -12,7 +12,7 @@ class ConfigProviderTest extends TestCase
|
|||
{
|
||||
private ConfigProvider $configProvider;
|
||||
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configProvider = new ConfigProvider();
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ class ApplicationFactoryTest extends TestCase
|
|||
|
||||
private ApplicationFactory $factory;
|
||||
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->factory = new ApplicationFactory();
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Util;
|
||||
namespace ShlinkioTest\Shlink\CLI\GeoLite;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use GeoIp2\Database\Reader;
|
||||
|
@ -12,9 +12,10 @@ use Prophecy\Argument;
|
|||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
||||
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdater;
|
||||
use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult;
|
||||
use Shlinkio\Shlink\Core\Options\TrackingOptions;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\DbUpdateException;
|
||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||
use Symfony\Component\Lock;
|
||||
use Throwable;
|
||||
|
@ -29,42 +30,32 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||
private GeolocationDbUpdater $geolocationDbUpdater;
|
||||
private ObjectProphecy $dbUpdater;
|
||||
private ObjectProphecy $geoLiteDbReader;
|
||||
private TrackingOptions $trackingOptions;
|
||||
private ObjectProphecy $lock;
|
||||
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->dbUpdater = $this->prophesize(DbUpdaterInterface::class);
|
||||
$this->geoLiteDbReader = $this->prophesize(Reader::class);
|
||||
$this->trackingOptions = new TrackingOptions();
|
||||
|
||||
$locker = $this->prophesize(Lock\LockFactory::class);
|
||||
$this->lock = $this->prophesize(Lock\LockInterface::class);
|
||||
$this->lock->acquire(true)->willReturn(true);
|
||||
$this->lock->release()->will(function (): void {
|
||||
});
|
||||
$locker->createLock(Argument::type('string'))->willReturn($this->lock->reveal());
|
||||
|
||||
$this->geolocationDbUpdater = new GeolocationDbUpdater(
|
||||
$this->dbUpdater->reveal(),
|
||||
$this->geoLiteDbReader->reveal(),
|
||||
$locker->reveal(),
|
||||
$this->trackingOptions,
|
||||
);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function exceptionIsThrownWhenOlderDbDoesNotExistAndDownloadFails(): void
|
||||
{
|
||||
$mustBeUpdated = fn () => self::assertTrue(true);
|
||||
$prev = new RuntimeException('');
|
||||
$prev = new DbUpdateException('');
|
||||
|
||||
$fileExists = $this->dbUpdater->databaseFileExists()->willReturn(false);
|
||||
$getMeta = $this->geoLiteDbReader->metadata();
|
||||
$download = $this->dbUpdater->downloadFreshCopy(null)->willThrow($prev);
|
||||
|
||||
try {
|
||||
$this->geolocationDbUpdater->checkDbUpdate($mustBeUpdated);
|
||||
$this->geolocationDbUpdater()->checkDbUpdate($mustBeUpdated);
|
||||
self::assertTrue(false); // If this is reached, the test will fail
|
||||
} catch (Throwable $e) {
|
||||
/** @var GeolocationDbUpdateFailedException $e */
|
||||
|
@ -90,11 +81,11 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||
$getMeta = $this->geoLiteDbReader->metadata()->willReturn($this->buildMetaWithBuildEpoch(
|
||||
Chronos::now()->subDays($days)->getTimestamp(),
|
||||
));
|
||||
$prev = new RuntimeException('');
|
||||
$prev = new DbUpdateException('');
|
||||
$download = $this->dbUpdater->downloadFreshCopy(null)->willThrow($prev);
|
||||
|
||||
try {
|
||||
$this->geolocationDbUpdater->checkDbUpdate();
|
||||
$this->geolocationDbUpdater()->checkDbUpdate();
|
||||
self::assertTrue(false); // If this is reached, the test will fail
|
||||
} catch (Throwable $e) {
|
||||
/** @var GeolocationDbUpdateFailedException $e */
|
||||
|
@ -120,15 +111,16 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||
* @test
|
||||
* @dataProvider provideSmallDays
|
||||
*/
|
||||
public function databaseIsNotUpdatedIfItIsYoungerThanOneWeek(string|int $buildEpoch): void
|
||||
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->geolocationDbUpdater->checkDbUpdate();
|
||||
$result = $this->geolocationDbUpdater()->checkDbUpdate();
|
||||
|
||||
self::assertEquals(GeolocationResult::DB_IS_UP_TO_DATE, $result);
|
||||
$fileExists->shouldHaveBeenCalledOnce();
|
||||
$getMeta->shouldHaveBeenCalledOnce();
|
||||
$download->shouldNotHaveBeenCalled();
|
||||
|
@ -160,7 +152,7 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||
$getMeta->shouldBeCalledOnce();
|
||||
$download->shouldNotBeCalled();
|
||||
|
||||
$this->geolocationDbUpdater->checkDbUpdate();
|
||||
$this->geolocationDbUpdater()->checkDbUpdate();
|
||||
}
|
||||
|
||||
private function buildMetaWithBuildEpoch(string|int $buildEpoch): Metadata
|
||||
|
@ -182,22 +174,32 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||
* @test
|
||||
* @dataProvider provideTrackingOptions
|
||||
*/
|
||||
public function downloadDbIsSkippedIfTrackingIsDisabled(array $props): void
|
||||
public function downloadDbIsSkippedIfTrackingIsDisabled(TrackingOptions $options): void
|
||||
{
|
||||
foreach ($props as $prop) {
|
||||
$this->trackingOptions->{$prop} = true;
|
||||
}
|
||||
|
||||
$this->geolocationDbUpdater->checkDbUpdate();
|
||||
$result = $this->geolocationDbUpdater($options)->checkDbUpdate();
|
||||
|
||||
self::assertEquals(GeolocationResult::CHECK_SKIPPED, $result);
|
||||
$this->dbUpdater->databaseFileExists(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$this->geoLiteDbReader->metadata(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function provideTrackingOptions(): iterable
|
||||
{
|
||||
yield 'disableTracking' => [['disableTracking']];
|
||||
yield 'disableIpTracking' => [['disableIpTracking']];
|
||||
yield 'both' => [['disableTracking', 'disableIpTracking']];
|
||||
yield 'disableTracking' => [new TrackingOptions(disableTracking: true)];
|
||||
yield 'disableIpTracking' => [new TrackingOptions(disableIpTracking: true)];
|
||||
yield 'both' => [new TrackingOptions(disableTracking: true, disableIpTracking: true)];
|
||||
}
|
||||
|
||||
private function geolocationDbUpdater(?TrackingOptions $options = null): GeolocationDbUpdater
|
||||
{
|
||||
$locker = $this->prophesize(Lock\LockFactory::class);
|
||||
$locker->createLock(Argument::type('string'))->willReturn($this->lock->reveal());
|
||||
|
||||
return new GeolocationDbUpdater(
|
||||
$this->dbUpdater->reveal(),
|
||||
$this->geoLiteDbReader->reveal(),
|
||||
$locker->reveal(),
|
||||
$options ?? new TrackingOptions(),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ class ShlinkTableTest extends TestCase
|
|||
private ShlinkTable $shlinkTable;
|
||||
private ObjectProphecy $baseTable;
|
||||
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->baseTable = $this->prophesize(Table::class);
|
||||
$this->shlinkTable = ShlinkTable::fromBaseTable($this->baseTable->reveal());
|
||||
|
|
|
@ -7,9 +7,11 @@ namespace Shlinkio\Shlink\Core;
|
|||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Shlinkio\Shlink\Config\Factory\ValinorConfigFactory;
|
||||
use Shlinkio\Shlink\Core\ErrorHandler;
|
||||
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||
use Shlinkio\Shlink\Importer\ImportedLinksProcessorInterface;
|
||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||
|
||||
return [
|
||||
|
||||
|
@ -20,14 +22,14 @@ return [
|
|||
ErrorHandler\NotFoundRedirectHandler::class => ConfigAbstractFactory::class,
|
||||
ErrorHandler\NotFoundTemplateHandler::class => InvokableFactory::class,
|
||||
|
||||
Options\AppOptions::class => ConfigAbstractFactory::class,
|
||||
Options\DeleteShortUrlsOptions::class => ConfigAbstractFactory::class,
|
||||
Options\NotFoundRedirectOptions::class => ConfigAbstractFactory::class,
|
||||
Options\RedirectOptions::class => ConfigAbstractFactory::class,
|
||||
Options\UrlShortenerOptions::class => ConfigAbstractFactory::class,
|
||||
Options\TrackingOptions::class => ConfigAbstractFactory::class,
|
||||
Options\QrCodeOptions::class => ConfigAbstractFactory::class,
|
||||
Options\RabbitMqOptions::class => ConfigAbstractFactory::class,
|
||||
Options\AppOptions::class => [ValinorConfigFactory::class, 'config.app_options'],
|
||||
Options\DeleteShortUrlsOptions::class => [ValinorConfigFactory::class, 'config.delete_short_urls'],
|
||||
Options\NotFoundRedirectOptions::class => [ValinorConfigFactory::class, 'config.not_found_redirects'],
|
||||
Options\RedirectOptions::class => [ValinorConfigFactory::class, 'config.redirects'],
|
||||
Options\UrlShortenerOptions::class => [ValinorConfigFactory::class, 'config.url_shortener'],
|
||||
Options\TrackingOptions::class => [ValinorConfigFactory::class, 'config.tracking'],
|
||||
Options\QrCodeOptions::class => [ValinorConfigFactory::class, 'config.qr_codes'],
|
||||
Options\RabbitMqOptions::class => [ValinorConfigFactory::class, 'config.rabbitmq'],
|
||||
Options\WebhookOptions::class => ConfigAbstractFactory::class,
|
||||
|
||||
Service\UrlShortener::class => ConfigAbstractFactory::class,
|
||||
|
@ -43,6 +45,7 @@ return [
|
|||
Visit\VisitsTracker::class => ConfigAbstractFactory::class,
|
||||
Visit\RequestTracker::class => ConfigAbstractFactory::class,
|
||||
Visit\VisitLocator::class => ConfigAbstractFactory::class,
|
||||
Visit\VisitToLocationHelper::class => ConfigAbstractFactory::class,
|
||||
Visit\VisitsStatsHelper::class => ConfigAbstractFactory::class,
|
||||
Visit\Transformer\OrphanVisitDataTransformer::class => InvokableFactory::class,
|
||||
|
||||
|
@ -85,14 +88,6 @@ return [
|
|||
Domain\DomainService::class,
|
||||
],
|
||||
|
||||
Options\AppOptions::class => ['config.app_options'],
|
||||
Options\DeleteShortUrlsOptions::class => ['config.delete_short_urls'],
|
||||
Options\NotFoundRedirectOptions::class => ['config.not_found_redirects'],
|
||||
Options\RedirectOptions::class => ['config.redirects'],
|
||||
Options\UrlShortenerOptions::class => ['config.url_shortener'],
|
||||
Options\TrackingOptions::class => ['config.tracking'],
|
||||
Options\QrCodeOptions::class => ['config.qr_codes'],
|
||||
Options\RabbitMqOptions::class => ['config.rabbitmq'],
|
||||
Options\WebhookOptions::class => ['config.visits_webhooks'],
|
||||
|
||||
Service\UrlShortener::class => [
|
||||
|
@ -115,6 +110,7 @@ return [
|
|||
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class,
|
||||
],
|
||||
Visit\VisitLocator::class => ['em'],
|
||||
Visit\VisitToLocationHelper::class => [IpLocationResolverInterface::class],
|
||||
Visit\VisitsStatsHelper::class => ['em'],
|
||||
Tag\TagService::class => ['em'],
|
||||
Service\ShortUrl\DeleteShortUrlService::class => [
|
||||
|
|
|
@ -6,10 +6,12 @@ namespace Shlinkio\Shlink\Core;
|
|||
|
||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
||||
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\IpGeolocation\GeoLite2\DbUpdater;
|
||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||
|
||||
|
@ -20,6 +22,9 @@ return [
|
|||
EventDispatcher\Event\UrlVisited::class => [
|
||||
EventDispatcher\LocateVisit::class,
|
||||
],
|
||||
EventDispatcher\Event\GeoLiteDbCreated::class => [
|
||||
EventDispatcher\LocateUnlocatedVisits::class,
|
||||
],
|
||||
],
|
||||
'async' => [
|
||||
EventDispatcher\Event\VisitLocated::class => [
|
||||
|
@ -40,6 +45,7 @@ return [
|
|||
'dependencies' => [
|
||||
'factories' => [
|
||||
EventDispatcher\LocateVisit::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\LocateUnlocatedVisits::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => ConfigAbstractFactory::class,
|
||||
|
@ -69,6 +75,9 @@ return [
|
|||
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\LocateUnlocatedVisits::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
|
@ -83,6 +92,7 @@ return [
|
|||
DbUpdater::class,
|
||||
EventDispatcherInterface::class,
|
||||
],
|
||||
EventDispatcher\LocateUnlocatedVisits::class => [VisitLocator::class, VisitToLocationHelper::class],
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => [
|
||||
'httpClient',
|
||||
'em',
|
||||
|
@ -132,7 +142,11 @@ return [
|
|||
'Logger_Shlink',
|
||||
'config.redis.pub_sub_enabled',
|
||||
],
|
||||
EventDispatcher\UpdateGeoLiteDb::class => [GeolocationDbUpdater::class, 'Logger_Shlink'],
|
||||
EventDispatcher\UpdateGeoLiteDb::class => [
|
||||
GeolocationDbUpdater::class,
|
||||
'Logger_Shlink',
|
||||
EventDispatcherInterface::class,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
@ -127,3 +127,8 @@ function camelCaseToHumanFriendly(string $value): string
|
|||
|
||||
return ucfirst($filter->filter($value));
|
||||
}
|
||||
|
||||
function toProblemDetailsType(string $errorCode): string
|
||||
{
|
||||
return sprintf('https://shlink.io/api/error/%s', $errorCode);
|
||||
}
|
||||
|
|
|
@ -19,8 +19,8 @@ use Shlinkio\Shlink\Core\Visit\RequestTrackerInterface;
|
|||
abstract class AbstractTrackingAction implements MiddlewareInterface, RequestMethodInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ShortUrlResolverInterface $urlResolver,
|
||||
private RequestTrackerInterface $requestTracker,
|
||||
private readonly ShortUrlResolverInterface $urlResolver,
|
||||
private readonly RequestTrackerInterface $requestTracker,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ final class QrCodeParams
|
|||
|
||||
private static function resolveSize(array $query, QrCodeOptions $defaults): int
|
||||
{
|
||||
$size = (int) ($query['size'] ?? $defaults->size());
|
||||
$size = (int) ($query['size'] ?? $defaults->size);
|
||||
if ($size < self::MIN_SIZE) {
|
||||
return self::MIN_SIZE;
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ final class QrCodeParams
|
|||
|
||||
private static function resolveMargin(array $query, QrCodeOptions $defaults): int
|
||||
{
|
||||
$margin = $query['margin'] ?? (string) $defaults->margin();
|
||||
$margin = $query['margin'] ?? (string) $defaults->margin;
|
||||
$intMargin = (int) $margin;
|
||||
if ($margin !== (string) $intMargin) {
|
||||
return 0;
|
||||
|
@ -74,7 +74,7 @@ final class QrCodeParams
|
|||
private static function resolveWriter(array $query, QrCodeOptions $defaults): WriterInterface
|
||||
{
|
||||
$qFormat = self::normalizeParam($query['format'] ?? '');
|
||||
$format = contains(self::SUPPORTED_FORMATS, $qFormat) ? $qFormat : self::normalizeParam($defaults->format());
|
||||
$format = contains(self::SUPPORTED_FORMATS, $qFormat) ? $qFormat : self::normalizeParam($defaults->format);
|
||||
|
||||
return match ($format) {
|
||||
'svg' => new SvgWriter(),
|
||||
|
@ -84,7 +84,7 @@ final class QrCodeParams
|
|||
|
||||
private static function resolveErrorCorrection(array $query, QrCodeOptions $defaults): ErrorCorrectionLevelInterface
|
||||
{
|
||||
$errorCorrectionLevel = self::normalizeParam($query['errorCorrection'] ?? $defaults->errorCorrection());
|
||||
$errorCorrectionLevel = self::normalizeParam($query['errorCorrection'] ?? $defaults->errorCorrection);
|
||||
return match ($errorCorrectionLevel) {
|
||||
'h' => new ErrorCorrectionLevelHigh(),
|
||||
'q' => new ErrorCorrectionLevelQuartile(),
|
||||
|
@ -97,7 +97,7 @@ final class QrCodeParams
|
|||
{
|
||||
$doNotRoundBlockSize = isset($query['roundBlockSize'])
|
||||
? $query['roundBlockSize'] === 'false'
|
||||
: ! $defaults->roundBlockSize();
|
||||
: ! $defaults->roundBlockSize;
|
||||
return $doNotRoundBlockSize ? new RoundBlockSizeModeNone() : new RoundBlockSizeModeMargin();
|
||||
}
|
||||
|
||||
|
|
|
@ -43,9 +43,11 @@ enum EnvVars: string
|
|||
case REDIRECT_STATUS_CODE = 'REDIRECT_STATUS_CODE';
|
||||
case REDIRECT_CACHE_LIFETIME = 'REDIRECT_CACHE_LIFETIME';
|
||||
case BASE_PATH = 'BASE_PATH';
|
||||
case SHORT_URL_TRAILING_SLASH = 'SHORT_URL_TRAILING_SLASH';
|
||||
case PORT = 'PORT';
|
||||
case TASK_WORKER_NUM = 'TASK_WORKER_NUM';
|
||||
case WEB_WORKER_NUM = 'WEB_WORKER_NUM';
|
||||
case INITIAL_API_KEY = 'INITIAL_API_KEY';
|
||||
case ANONYMIZE_REMOTE_ADDR = 'ANONYMIZE_REMOTE_ADDR';
|
||||
case TRACK_ORPHAN_VISITS = 'TRACK_ORPHAN_VISITS';
|
||||
case DISABLE_TRACK_PARAM = 'DISABLE_TRACK_PARAM';
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue