From 55d0c8213432cb35a5b7951d12b6d0e91cb3ee15 Mon Sep 17 00:00:00 2001 From: Maxime CORNET Date: Fri, 6 Jun 2025 20:52:17 +0200 Subject: [PATCH 01/19] Remove HomeController --- src/Controller/HomeController.php | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 src/Controller/HomeController.php diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php deleted file mode 100644 index 28ae215..0000000 --- a/src/Controller/HomeController.php +++ /dev/null @@ -1,16 +0,0 @@ - 'Hello there']); - } -} From 90981047043edff5452c0b4f8198b5729defc56d Mon Sep 17 00:00:00 2001 From: Maxime CORNET Date: Fri, 6 Jun 2025 20:53:10 +0200 Subject: [PATCH 02/19] Uncomment the passwordStrength for UserRegistrationDto --- src/Dto/UserRegistrationDto.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Dto/UserRegistrationDto.php b/src/Dto/UserRegistrationDto.php index 4e4304e..fc79611 100644 --- a/src/Dto/UserRegistrationDto.php +++ b/src/Dto/UserRegistrationDto.php @@ -21,7 +21,9 @@ class UserRegistrationDto public mixed $username = null; #[Assert\NotBlank(allowNull: false)] - // #[Assert\PasswordStrength] + #[Assert\PasswordStrength( + minScore: Assert\PasswordStrength::STRENGTH_WEAK, + )] #[Groups('user:write')] public mixed $password = null; From 8a2218247f83c855d8a08bba788d666f2dae6dca Mon Sep 17 00:00:00 2001 From: Maxime CORNET Date: Fri, 6 Jun 2025 20:53:58 +0200 Subject: [PATCH 03/19] Maildev is now required to up php container --- compose.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compose.yaml b/compose.yaml index 7116a90..d83286d 100644 --- a/compose.yaml +++ b/compose.yaml @@ -33,6 +33,8 @@ services: depends_on: postgres: condition: service_healthy + maildev: + condition: service_started volumes: - "./:/var/www:rw" environment: From 3f32eb6b183bce04475057ce9025b149a30cfb5d Mon Sep 17 00:00:00 2001 From: Maxime CORNET Date: Fri, 6 Jun 2025 20:56:15 +0200 Subject: [PATCH 04/19] Remove security to get a user --- src/Entity/User.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Entity/User.php b/src/Entity/User.php index c29846f..be24d15 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -17,9 +17,7 @@ #[ApiResource( operations: [ - new Get( - security: "is_granted('ROLE_ADMIN') or object == user" - ), + new Get(), new GetCollection( security: "is_granted('ROLE_ADMIN')" ), From 2dc945690dbe88bb51418eb5b556a86f53400365 Mon Sep 17 00:00:00 2001 From: Maxime CORNET Date: Fri, 6 Jun 2025 20:56:35 +0200 Subject: [PATCH 05/19] Only admins can see email address of user --- src/Entity/User.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/User.php b/src/Entity/User.php index be24d15..1556a8f 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -38,7 +38,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface { use EntityIdTrait; - #[Groups(['user:read'])] + #[Groups(['admin:user:read'])] #[ORM\Column(length: 255, unique: true)] private string $email; From e708e949970c2674427face3125a6722221b20fd Mon Sep 17 00:00:00 2001 From: Maxime CORNET Date: Fri, 6 Jun 2025 20:57:55 +0200 Subject: [PATCH 06/19] Put user default password in UserFixture instead of Factory and change it --- fixtures/Data/UserFixtures.php | 1 + fixtures/Factory/UserFactory.php | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/fixtures/Data/UserFixtures.php b/fixtures/Data/UserFixtures.php index a317a2c..b5d7f46 100644 --- a/fixtures/Data/UserFixtures.php +++ b/fixtures/Data/UserFixtures.php @@ -8,6 +8,7 @@ class UserFixtures extends AbstractFixture { + public const string DEFAULT_PASSWORD = 'esvESV123456'; public const string ADMIN_ULID = '019712ef-fb77-347f-1cd9-2b1c22d259e6'; public const string USER_ULID = '019712ef-fb78-602c-a546-f7c694bd83be'; public const string UNVERIFIED_USER_ULID = '01973bad-1197-b299-2e4d-349108befb29'; diff --git a/fixtures/Factory/UserFactory.php b/fixtures/Factory/UserFactory.php index e7b8bc0..75187e3 100644 --- a/fixtures/Factory/UserFactory.php +++ b/fixtures/Factory/UserFactory.php @@ -10,8 +10,6 @@ */ final class UserFactory extends PersistentProxyObjectFactory { - public const string DEFAULT_PASSWORD = 'esv'; - public static function class(): string { return User::class; From 0e089b248c11ac71c748637d1e805d74bf433a09 Mon Sep 17 00:00:00 2001 From: Maxime CORNET Date: Fri, 6 Jun 2025 20:58:56 +0200 Subject: [PATCH 07/19] Add smoke & functionnal tests - Add smoke & functionnal tests - Add test-pack component - Add Monolog (avoid having exceptions line in phpunit text) - Add tests directory to phpstan - Ignore tests/bootstrap.php in php-cs-fixer - Add dama/doctrine-test-bundle - Add http-client component - Update Makefile to handle tests tasks --- .env.test | 4 + .github/workflows/ci.yaml | 34 + .gitignore | 4 + .php-cs-fixer.dist.php | 3 + Makefile | 28 +- bin/phpunit | 23 + composer.json | 6 + composer.lock | 2495 ++++++++++++++++- config/bundles.php | 2 + config/packages/monolog.yaml | 54 + .../test/dama_doctrine_test_bundle.yaml | 4 + phpstan.neon | 1 + phpunit.xml | 49 + symfony.lock | 36 + tests/Functional/ApiTestCase.php | 35 + tests/Functional/User/RegistrationTest.php | 144 + tests/Functional/User/UserTest.php | 109 + .../ApplicationAvailabilityFunctionalTest.php | 28 + tests/bootstrap.php | 13 + 19 files changed, 2981 insertions(+), 91 deletions(-) create mode 100644 .env.test create mode 100644 .github/workflows/ci.yaml create mode 100755 bin/phpunit create mode 100644 config/packages/monolog.yaml create mode 100644 config/packages/test/dama_doctrine_test_bundle.yaml create mode 100644 phpunit.xml create mode 100644 tests/Functional/ApiTestCase.php create mode 100644 tests/Functional/User/RegistrationTest.php create mode 100644 tests/Functional/User/UserTest.php create mode 100644 tests/Smoke/ApplicationAvailabilityFunctionalTest.php create mode 100644 tests/bootstrap.php diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..2f1e270 --- /dev/null +++ b/.env.test @@ -0,0 +1,4 @@ +# define your env variables for the test env here +KERNEL_CLASS='App\Kernel' +APP_SECRET='$ecretf0rt3st' +FIXTURES_SIZE=none diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..8a026b0 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,34 @@ +name: Execute CI tests + +on: + push: + branches: + - main + pull_request: ~ + +jobs: + test-smoke: + name: Run smoke tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install the project for test environment + run: make ci-install + + - name: Run smoke tests + run: make test-smoke + + test-functional: + name: Run functional tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install the project for test environment + run: make ci-install + + - name: Run functional tests + run: make test-functional diff --git a/.gitignore b/.gitignore index 199a138..dd548c7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,7 @@ compose.override.yaml ###> lexik/jwt-authentication-bundle ### /config/jwt/*.pem ###< lexik/jwt-authentication-bundle ### + +###> phpunit/phpunit ### +/.phpunit.cache/ +###< phpunit/phpunit ### diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 3788194..7a2d762 100755 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -3,6 +3,9 @@ $finder = (new PhpCsFixer\Finder()) ->in(__DIR__) ->exclude('var') + ->notPath([ + 'tests/bootstrap.php', + ]) ; return (new PhpCsFixer\Config()) diff --git a/Makefile b/Makefile index a2523f1..ea64128 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,10 @@ RUN_PHP_ALONE=$(DOCKER_COMPOSE) run --no-deps --rm $(DOCKER_PHP_CONTAINER) COMPOSER=$(EXEC_PHP) composer CONSOLE=$(EXEC_PHP) bin/console +test-%: RUN_PHP=$(DOCKER_COMPOSE) run --rm --env APP_ENV=test --env XDEBUG_MODE=off $(DOCKER_PHP_CONTAINER) +test-%: CONSOLE=$(RUN_PHP) bin/console +test-%: COMPOSER=$(RUN_PHP) composer + ## ###--------------# ### Docker # @@ -54,8 +58,9 @@ start: up ## Alias for up start-all: up-all ## Alias for up-all install: pull compose.override.yaml up vendor db-schema-force generate-keypair db-fixtures ## Install the project +test-install: pull vendor generate-keypair db-create db-schema-force db-fixtures ## Install the project for test environment. -.PHONY: up stop down pull sh build bash start start-all install +.PHONY: up stop down pull sh build bash start start-all install test-install ## ###----------------# @@ -105,6 +110,27 @@ db-execute-down: ## Execute the latest migration versions down manually. .PHONY: db-create db-drop db-migrate db-validate db-schema db-schema-force db-diff db-update .PHONY: db-fixtures fixtures db-execute-up db-execute-down +## +###-------------# +### Tests # +###-------------# +## + +test-smoke: ## Run smoke tests + $(RUN_PHP) bin/phpunit --no-extensions --testsuite smoke + +test-debug: ## Run tests with debug group/tags + $(RUN_PHP) bin/phpunit --group debug + +test-functional: ## Run functional tests + $(RUN_PHP) bin/phpunit --testsuite functional + +tests: test-smoke test-functional ## Execute all tests +ts: test-smoke ## Alias for test-smoke +tf: test-functional ## Alias for test-functional + +.PHONY: test-smoke test-debug tests test-functional ts tf + ## ###-----------------# ### Q&A tools # diff --git a/bin/phpunit b/bin/phpunit new file mode 100755 index 0000000..692bacc --- /dev/null +++ b/bin/phpunit @@ -0,0 +1,23 @@ +#!/usr/bin/env php += 80000) { + require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit'; + } else { + define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php'); + require PHPUNIT_COMPOSER_INSTALL; + PHPUnit\TextUI\Command::main(); + } +} else { + if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) { + echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n"; + exit(1); + } + + require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php'; +} diff --git a/composer.json b/composer.json index 863a253..91869d4 100644 --- a/composer.json +++ b/composer.json @@ -42,6 +42,7 @@ "symfony/framework-bundle": "7.2.*", "symfony/intl": "7.2.*", "symfony/mailer": "7.2.*", + "symfony/monolog-bundle": "^3.10", "symfony/property-access": "7.2.*", "symfony/property-info": "7.2.*", "symfony/runtime": "7.2.*", @@ -52,12 +53,17 @@ "symfony/yaml": "7.2.*" }, "require-dev": { + "dama/doctrine-test-bundle": "^8.3", "doctrine/doctrine-fixtures-bundle": "^4.1", "phpstan/extension-installer": "^1.4.3", "phpstan/phpstan": "^2.1.17", "phpstan/phpstan-doctrine": "^2.0.3", "phpstan/phpstan-strict-rules": "^2.0.4", "phpstan/phpstan-symfony": "^2.0.6", + "phpunit/phpunit": "^12.2", + "symfony/browser-kit": "7.2.*", + "symfony/css-selector": "7.2.*", + "symfony/http-client": "7.2.*", "symfony/maker-bundle": "^1.63", "symfony/stopwatch": "7.2.*", "symfony/web-profiler-bundle": "7.2.*", diff --git a/composer.lock b/composer.lock index 97bb350..187ef81 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "11173d976300c025c5db1074d87862a5", + "content-hash": "b126cc34d457fa83747ad8571976f54c", "packages": [ { "name": "api-platform/doctrine-common", @@ -2826,6 +2826,109 @@ ], "time": "2025-05-24T22:26:38+00:00" }, + { + "name": "monolog/monolog", + "version": "3.9.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2025-03-24T10:02:05+00:00" + }, { "name": "nelmio/cors-bundle", "version": "2.5.0", @@ -5316,6 +5419,165 @@ ], "time": "2025-04-27T13:34:41+00:00" }, + { + "name": "symfony/monolog-bridge", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bridge.git", + "reference": "bbae784f0456c5a87c89d7c1a3fcc9cbee976c1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/bbae784f0456c5a87c89d7c1a3fcc9cbee976c1d", + "reference": "bbae784f0456c5a87c89d7c1a3fcc9cbee976c1d", + "shasum": "" + }, + "require": { + "monolog/monolog": "^3", + "php": ">=8.2", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/console": "<6.4", + "symfony/http-foundation": "<6.4", + "symfony/security-core": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/mailer": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Monolog\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Monolog with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/monolog-bridge/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-14T18:16:08+00:00" + }, + { + "name": "symfony/monolog-bundle", + "version": "v3.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bundle.git", + "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", + "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", + "shasum": "" + }, + "require": { + "monolog/monolog": "^1.25.1 || ^2.0 || ^3.0", + "php": ">=7.2.5", + "symfony/config": "^5.4 || ^6.0 || ^7.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", + "symfony/monolog-bridge": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/phpunit-bridge": "^6.3 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MonologBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony MonologBundle", + "homepage": "https://symfony.com", + "keywords": [ + "log", + "logging" + ], + "support": { + "issues": "https://github.com/symfony/monolog-bundle/issues", + "source": "https://github.com/symfony/monolog-bundle/tree/v3.10.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-06T17:08:13+00:00" + }, { "name": "symfony/password-hasher", "version": "v7.2.0", @@ -7895,6 +8157,75 @@ } ], "packages-dev": [ + { + "name": "dama/doctrine-test-bundle", + "version": "v8.3.0", + "source": { + "type": "git", + "url": "https://github.com/dmaicher/doctrine-test-bundle.git", + "reference": "11846789ca2a86f6277316b42448f7fccb3965ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/11846789ca2a86f6277316b42448f7fccb3965ff", + "reference": "11846789ca2a86f6277316b42448f7fccb3965ff", + "shasum": "" + }, + "require": { + "doctrine/dbal": "^3.3 || ^4.0", + "doctrine/doctrine-bundle": "^2.11.0", + "php": ">= 8.1", + "psr/cache": "^2.0 || ^3.0", + "symfony/cache": "^6.4 || ^7.2", + "symfony/framework-bundle": "^6.4 || ^7.2" + }, + "conflict": { + "phpunit/phpunit": "<10.0" + }, + "require-dev": { + "behat/behat": "^3.0", + "friendsofphp/php-cs-fixer": "^3.27", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "symfony/process": "^6.4 || ^7.2", + "symfony/yaml": "^6.4 || ^7.2" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "DAMA\\DoctrineTestBundle\\": "src/DAMA/DoctrineTestBundle" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "David Maicher", + "email": "mail@dmaicher.de" + } + ], + "description": "Symfony bundle to isolate doctrine database tests and improve test performance", + "keywords": [ + "doctrine", + "isolation", + "performance", + "symfony", + "testing", + "tests" + ], + "support": { + "issues": "https://github.com/dmaicher/doctrine-test-bundle/issues", + "source": "https://github.com/dmaicher/doctrine-test-bundle/tree/v8.3.0" + }, + "time": "2025-03-04T10:07:03+00:00" + }, { "name": "doctrine/data-fixtures", "version": "2.0.2", @@ -8128,70 +8459,315 @@ "time": "2024-11-21T13:46:39+00:00" }, { - "name": "nikic/php-parser", - "version": "v5.5.0", + "name": "masterminds/html5", + "version": "2.9.0", "source": { "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", - "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", "shasum": "" }, "require": { - "ext-ctype": "*", - "ext-json": "*", - "ext-tokenizer": "*", - "php": ">=7.4" + "ext-dom": "*", + "php": ">=5.3.0" }, "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" }, - "bin": [ - "bin/php-parse" - ], "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "2.7-dev" } }, "autoload": { "psr-4": { - "PhpParser\\": "lib/PhpParser" + "Masterminds\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Nikita Popov" + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" } ], - "description": "A PHP parser written in PHP", + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", "keywords": [ + "HTML5", + "dom", + "html", "parser", - "php" + "querypath", + "serializer", + "xml" ], "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.9.0" }, - "time": "2025-05-31T08:24:38+00:00" + "time": "2024-03-31T07:05:07+00:00" }, { - "name": "phpstan/extension-installer", - "version": "1.4.3", + "name": "myclabs/deep-copy", + "version": "1.13.1", "source": { "type": "git", - "url": "https://github.com/phpstan/extension-installer.git", - "reference": "85e90b3942d06b2326fba0403ec24fe912372936" + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-04-29T12:36:36+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.5.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" + }, + "time": "2025-05-31T08:24:38+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpstan/extension-installer", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/extension-installer.git", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936" }, "dist": { "type": "zip", @@ -8483,117 +9059,1806 @@ "time": "2025-05-14T07:00:05+00:00" }, { - "name": "symfony/maker-bundle", - "version": "v1.63.0", + "name": "phpunit/php-code-coverage", + "version": "12.3.0", "source": { "type": "git", - "url": "https://github.com/symfony/maker-bundle.git", - "reference": "69478ab39bc303abfbe3293006a78b09a8512425" + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "9075a8efc66e11bc55c319062e147bdb06777267" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/69478ab39bc303abfbe3293006a78b09a8512425", - "reference": "69478ab39bc303abfbe3293006a78b09a8512425", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/9075a8efc66e11bc55c319062e147bdb06777267", + "reference": "9075a8efc66e11bc55c319062e147bdb06777267", "shasum": "" }, "require": { - "doctrine/inflector": "^2.0", - "nikic/php-parser": "^5.0", - "php": ">=8.1", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/deprecation-contracts": "^2.2|^3", - "symfony/filesystem": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0" - }, - "conflict": { - "doctrine/doctrine-bundle": "<2.10", - "doctrine/orm": "<2.15" + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.4.0", + "php": ">=8.3", + "phpunit/php-file-iterator": "^6.0", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "composer/semver": "^3.0", - "doctrine/doctrine-bundle": "^2.5.0", - "doctrine/orm": "^2.15|^3", - "symfony/http-client": "^6.4|^7.0", - "symfony/phpunit-bridge": "^6.4.1|^7.0", - "symfony/security-core": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0", - "twig/twig": "^3.0|^4.x-dev" + "phpunit/phpunit": "^12.1" }, - "type": "symfony-bundle", + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", "extra": { "branch-alias": { - "dev-main": "1.x-dev" + "dev-main": "12.3.x-dev" } }, "autoload": { - "psr-4": { - "Symfony\\Bundle\\MakerBundle\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.", - "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", "keywords": [ - "code generator", - "dev", - "generator", - "scaffold", - "scaffolding" + "coverage", + "testing", + "xunit" ], "support": { - "issues": "https://github.com/symfony/maker-bundle/issues", - "source": "https://github.com/symfony/maker-bundle/tree/v1.63.0" + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.3.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" + "url": "https://github.com/sebastianbergmann", + "type": "github" }, { - "url": "https://github.com/fabpot", - "type": "github" + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", "type": "tidelift" } ], - "time": "2025-04-26T01:41:37+00:00" + "time": "2025-05-23T15:49:03+00:00" }, { - "name": "symfony/process", - "version": "v7.2.5", + "name": "phpunit/php-file-iterator", + "version": "6.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "87b7c93e57df9d8e39a093d32587702380ff045d" + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "961bc913d42fe24a257bfff826a5068079ac7782" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/87b7c93e57df9d8e39a093d32587702380ff045d", - "reference": "87b7c93e57df9d8e39a093d32587702380ff045d", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/961bc913d42fe24a257bfff826a5068079ac7782", + "reference": "961bc913d42fe24a257bfff826a5068079ac7782", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, "autoload": { - "psr-4": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:37+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:16+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:38+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "12.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "f3f6587947f9f53f009af8d18026776375d59145" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f3f6587947f9f53f009af8d18026776375d59145", + "reference": "f3f6587947f9f53f009af8d18026776375d59145", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.1", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.3.0", + "phpunit/php-file-iterator": "^6.0.0", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.0.0", + "sebastian/comparator": "^7.0.1", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.0.2", + "sebastian/exporter": "^7.0.0", + "sebastian/global-state": "^8.0.0", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/type": "^6.0.2", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.2-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.2.0" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-06-06T02:58:21+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "6d584c727d9114bcdc14c86711cd1cad51778e7c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/6d584c727d9114bcdc14c86711cd1cad51778e7c", + "reference": "6d584c727d9114bcdc14c86711cd1cad51778e7c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:53:50+00:00" + }, + { + "name": "sebastian/comparator", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "b478f34614f934e0291598d0c08cbaba9644bee5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/b478f34614f934e0291598d0c08cbaba9644bee5", + "reference": "b478f34614f934e0291598d0c08cbaba9644bee5", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-07T07:00:32+00:00" + }, + { + "name": "sebastian/complexity", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "8.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "d364b9e5d0d3b18a2573351a1786fbf96b7e0792" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d364b9e5d0d3b18a2573351a1786fbf96b7e0792", + "reference": "d364b9e5d0d3b18a2573351a1786fbf96b7e0792", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T15:05:44+00:00" + }, + { + "name": "sebastian/exporter", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "76432aafc58d50691a00d86d0632f1217a47b688" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/76432aafc58d50691a00d86d0632f1217a47b688", + "reference": "76432aafc58d50691a00d86d0632f1217a47b688", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:56:42+00:00" + }, + { + "name": "sebastian/global-state", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "570a2aeb26d40f057af686d63c4e99b075fb6cbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/570a2aeb26d40f057af686d63c4e99b075fb6cbc", + "reference": "570a2aeb26d40f057af686d63c4e99b075fb6cbc", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:56:59+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:28+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:48+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:17+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "c405ae3a63e01b32eb71577f8ec1604e39858a7c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/c405ae3a63e01b32eb71577f8ec1604e39858a7c", + "reference": "c405ae3a63e01b32eb71577f8ec1604e39858a7c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:01+00:00" + }, + { + "name": "sebastian/type", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "1d7cd6e514384c36d7a390347f57c385d4be6069" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/1d7cd6e514384c36d7a390347f57c385d4be6069", + "reference": "1d7cd6e514384c36d7a390347f57c385d4be6069", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-18T13:37:31+00:00" + }, + { + "name": "sebastian/version", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:38+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/browser-kit", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/browser-kit.git", + "reference": "8ce0ee23857d87d5be493abba2d52d1f9e49da61" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/8ce0ee23857d87d5be493abba2d52d1f9e49da61", + "reference": "8ce0ee23857d87d5be493abba2d52d1f9e49da61", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/dom-crawler": "^6.4|^7.0" + }, + "require-dev": { + "symfony/css-selector": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\BrowserKit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/browser-kit/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-14T14:27:24+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/dom-crawler", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7", + "reference": "19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7", + "shasum": "" + }, + "require": { + "masterminds/html5": "^2.6", + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "symfony/css-selector": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases DOM navigation for HTML and XML documents", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dom-crawler/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-17T15:53:07+00:00" + }, + { + "name": "symfony/http-client", + "version": "v7.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", + "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "amphp/amp": "<2.5", + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/amphp-http-client-meta": "^1.0|^2.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v7.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-13T10:27:23+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "75d7043853a42837e68111812f4d964b01e5101c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c", + "reference": "75d7043853a42837e68111812f4d964b01e5101c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-29T11:18:49+00:00" + }, + { + "name": "symfony/maker-bundle", + "version": "v1.63.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/maker-bundle.git", + "reference": "69478ab39bc303abfbe3293006a78b09a8512425" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/69478ab39bc303abfbe3293006a78b09a8512425", + "reference": "69478ab39bc303abfbe3293006a78b09a8512425", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "nikic/php-parser": "^5.0", + "php": ">=8.1", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.2|^3", + "symfony/filesystem": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0" + }, + "conflict": { + "doctrine/doctrine-bundle": "<2.10", + "doctrine/orm": "<2.15" + }, + "require-dev": { + "composer/semver": "^3.0", + "doctrine/doctrine-bundle": "^2.5.0", + "doctrine/orm": "^2.15|^3", + "symfony/http-client": "^6.4|^7.0", + "symfony/phpunit-bridge": "^6.4.1|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0", + "twig/twig": "^3.0|^4.x-dev" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MakerBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.", + "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", + "keywords": [ + "code generator", + "dev", + "generator", + "scaffold", + "scaffolding" + ], + "support": { + "issues": "https://github.com/symfony/maker-bundle/issues", + "source": "https://github.com/symfony/maker-bundle/tree/v1.63.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-26T01:41:37+00:00" + }, + { + "name": "symfony/process", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "87b7c93e57df9d8e39a093d32587702380ff045d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/87b7c93e57df9d8e39a093d32587702380ff045d", + "reference": "87b7c93e57df9d8e39a093d32587702380ff045d", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { "Symfony\\Component\\Process\\": "" }, "exclude-from-classmap": [ @@ -8717,6 +10982,56 @@ ], "time": "2025-02-14T14:27:24+00:00" }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + }, { "name": "zenstruck/assert", "version": "v1.5.1", diff --git a/config/bundles.php b/config/bundles.php index f4ff40d..139a0e0 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -13,4 +13,6 @@ ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true], Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], Zenstruck\Foundry\ZenstruckFoundryBundle::class => ['dev' => true, 'test' => true], + Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], + DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], ]; diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml new file mode 100644 index 0000000..3733059 --- /dev/null +++ b/config/packages/monolog.yaml @@ -0,0 +1,54 @@ +monolog: + channels: + - deprecation + +when@dev: + monolog: + handlers: + main: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug + channels: ["!event"] + console: + type: console + process_psr_3_messages: false + channels: ["!event", "!doctrine", "!console"] + +when@test: + monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + channels: ["!event"] + nested: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug + +when@prod: + monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + buffer_size: 50 + nested: + type: stream + path: php://stderr + level: debug + formatter: monolog.formatter.json + console: + type: console + process_psr_3_messages: false + channels: ["!event", "!doctrine"] + deprecation: + type: stream + channels: [deprecation] + path: php://stderr + formatter: monolog.formatter.json diff --git a/config/packages/test/dama_doctrine_test_bundle.yaml b/config/packages/test/dama_doctrine_test_bundle.yaml new file mode 100644 index 0000000..80b0091 --- /dev/null +++ b/config/packages/test/dama_doctrine_test_bundle.yaml @@ -0,0 +1,4 @@ +dama_doctrine_test: + enable_static_connection: true + enable_static_meta_data_cache: true + enable_static_query_cache: true diff --git a/phpstan.neon b/phpstan.neon index 618f267..997488d 100755 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,6 +5,7 @@ parameters: - config/ - src/ - fixtures/ + - tests/ symfony: containerXmlPath: var/cache/dev/App_KernelDevDebugContainer.xml scanDirectories: diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..5e37635 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + tests/Smoke + + + tests/Functional + + + + + + src + + + + Doctrine\Deprecations\Deprecation::trigger + Doctrine\Deprecations\Deprecation::delegateTriggerToBackend + trigger_deprecation + + + + + + + diff --git a/symfony.lock b/symfony.lock index b117627..bda41b6 100644 --- a/symfony.lock +++ b/symfony.lock @@ -13,6 +13,15 @@ "src/ApiResource/.gitignore" ] }, + "dama/doctrine-test-bundle": { + "version": "8.3", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "8.3", + "ref": "dfc51177476fb39d014ed89944cde53dc3326d23" + } + }, "doctrine/deprecations": { "version": "1.1", "recipe": { @@ -97,6 +106,21 @@ "phpstan.dist.neon" ] }, + "phpunit/phpunit": { + "version": "12.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "11.1", + "ref": "c6658a60fc9d594805370eacdf542c3d6b5c0869" + }, + "files": [ + ".env.test", + "phpunit.dist.xml", + "tests/bootstrap.php", + "bin/phpunit" + ] + }, "symfony/console": { "version": "7.2", "recipe": { @@ -162,6 +186,18 @@ "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" } }, + "symfony/monolog-bundle": { + "version": "3.10", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.7", + "ref": "aff23899c4440dd995907613c1dd709b6f59503f" + }, + "files": [ + "config/packages/monolog.yaml" + ] + }, "symfony/routing": { "version": "7.2", "recipe": { diff --git a/tests/Functional/ApiTestCase.php b/tests/Functional/ApiTestCase.php new file mode 100644 index 0000000..add1d42 --- /dev/null +++ b/tests/Functional/ApiTestCase.php @@ -0,0 +1,35 @@ +client = self::createClient([], ['headers' => ['Accept' => 'application/ld+json']]); + } + + public function authenticateTestClientAs(string $id): Client + { + $user = static::getContainer()->get('doctrine')->getRepository(User::class)->findOneById($id); + + if (!$user instanceof User) { + throw new \RuntimeException('User not found'); + } + + $tokenManager = static::getContainer()->get(JWTTokenManagerInterface::class); + $token = $tokenManager->create($user); + + $this->client->setDefaultOptions(['headers' => ['Authorization' => 'Bearer '.$token]]); + + return $this->client; + } +} diff --git a/tests/Functional/User/RegistrationTest.php b/tests/Functional/User/RegistrationTest.php new file mode 100644 index 0000000..2760a17 --- /dev/null +++ b/tests/Functional/User/RegistrationTest.php @@ -0,0 +1,144 @@ +client->request('POST', '/users', [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => json_encode([ + 'email' => 'new_account+alias@esports-videos.com', + 'username' => 'new_account', + 'password' => UserFixtures::DEFAULT_PASSWORD, + 'country' => 'FR', + ], JSON_THROW_ON_ERROR), + ]); + + self::assertResponseStatusCodeSame(Response::HTTP_CREATED); + self::assertEmailCount(1); + } + + #[Test] + #[DataProvider('invalidData')] + #[Group('debug')] + public function asAnonymousICannotRegisterWithInvalidPassword(?string $email, ?string $password, ?string $username, ?string $countryCode, array $violations): void + { + $data = array_filter([ + 'email' => $email, + 'password' => $password, + 'username' => $username, + 'country' => $countryCode, + ], static fn ($value) => null !== $value); + + $this->client->request('POST', '/users', [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => json_encode($data, JSON_THROW_ON_ERROR), + ]); + + self::assertResponseStatusCodeSame(Response::HTTP_UNPROCESSABLE_ENTITY); + self::assertResponseHeaderSame('content-type', 'application/problem+json; charset=utf-8'); + self::assertJsonContains([ + '@type' => 'ConstraintViolation', + 'title' => 'An error occurred', + 'status' => 422, + 'violations' => $violations, + ]); + self::assertEmailCount(0); + } + + public static function invalidData(): \Generator + { + // Empty array + yield [ + 'email' => null, + 'password' => null, + 'countryCode' => null, + 'username' => null, + 'violations' => [], + ]; + + // Email is omitted + yield [ + 'email' => null, + 'password' => UserFixtures::DEFAULT_PASSWORD, + 'countryCode' => 'FR', + 'username' => 'correct_username', + 'violations' => [ + [ + 'propertyPath' => 'email', + 'message' => 'This value should not be blank.', + ], + ], + ]; + + // Password is omitted + yield [ + 'email' => 'correct_email@esports-videos.com', + 'password' => null, + 'countryCode' => 'FR', + 'username' => 'correct_username', + 'violations' => [ + [ + 'propertyPath' => 'password', + 'message' => 'This value should not be blank.', + ], + ], + ]; + + // Invalid Email + yield [ + 'email' => 'invalidEmail', + 'password' => UserFixtures::DEFAULT_PASSWORD, + 'countryCode' => 'FR', + 'username' => 'correct_username', + 'violations' => [ + [ + 'propertyPath' => 'email', + 'message' => 'This value is not a valid email address.', + ], + ], + ]; + + // Invalid Country code + yield [ + 'email' => 'correct_email@esports-videos.com', + 'password' => UserFixtures::DEFAULT_PASSWORD, + 'countryCode' => 'BAD', + 'username' => 'correct_username', + 'violations' => [ + [ + 'propertyPath' => 'country', + 'message' => 'This value is not a valid country.', + ], + ], + ]; + + // Invalid Password + yield [ + 'email' => 'correct_email@esports-videos.com', + 'password' => 'badPassword', + 'countryCode' => 'FR', + 'username' => 'correct_username', + 'violations' => [ + [ + 'propertyPath' => 'password', + 'message' => 'The password strength is too low. Please use a stronger password.', + ], + ], + ]; + } +} diff --git a/tests/Functional/User/UserTest.php b/tests/Functional/User/UserTest.php new file mode 100644 index 0000000..a422853 --- /dev/null +++ b/tests/Functional/User/UserTest.php @@ -0,0 +1,109 @@ +authenticateTestClientAs($userUlid) + : $this->client; + + $client->request('GET', '/users'); + + if (Response::HTTP_OK === $expectedStatusCode) { + self::assertResponseIsSuccessful(); + self::assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8'); + } else { + self::assertResponseStatusCodeSame($expectedStatusCode); + } + } + + public static function getUserCollectionAccessCases(): iterable + { + yield 'anonymous cannot access' => [ + 'userUlid' => null, + 'expectedStatusCode' => Response::HTTP_UNAUTHORIZED, + ]; + + yield 'user cannot access' => [ + 'userUlid' => UserFixtures::USER_ULID, + 'expectedStatusCode' => Response::HTTP_FORBIDDEN, + ]; + + yield 'admin can access' => [ + 'userUlid' => UserFixtures::ADMIN_ULID, + 'expectedStatusCode' => Response::HTTP_OK, + ]; + } + + #[Test] + #[DataProvider('getUserAccessCases')] + public function iCanGetAUser(?string $userUlid, array $expectedData, array $forbiddenFields): void + { + $client = $userUlid + ? $this->authenticateTestClientAs($userUlid) + : $this->client; + + $response = $client->request('GET', sprintf('/users/%s', UserFixtures::ADMIN_ULID)); + + self::assertResponseIsSuccessful(); + self::assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8'); + + $data = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR); + + foreach ($forbiddenFields as $field) { + self::assertArrayNotHasKey($field, $data); + } + + self::assertJsonContains($expectedData); + } + + public static function getUserAccessCases(): iterable + { + yield 'anonymous' => [ + 'userUlid' => null, + 'expectedData' => [ + '@context' => '/contexts/User', + '@type' => 'User', + 'username' => 'admin', + 'country' => 'FR', + ], + 'forbiddenFields' => ['email', 'password'], + ]; + + yield 'user' => [ + 'userUlid' => UserFixtures::USER_ULID, + 'expectedData' => [ + '@context' => '/contexts/User', + '@type' => 'User', + 'username' => 'admin', + 'country' => 'FR', + ], + 'forbiddenFields' => ['email', 'password'], + ]; + + yield 'admin' => [ + 'userUlid' => UserFixtures::ADMIN_ULID, + 'expectedData' => [ + '@context' => '/contexts/User', + '@type' => 'User', + 'username' => 'admin', + 'country' => 'FR', + 'email' => 'admin@esports-videos.com', + ], + 'forbiddenFields' => ['password'], + ]; + } +} diff --git a/tests/Smoke/ApplicationAvailabilityFunctionalTest.php b/tests/Smoke/ApplicationAvailabilityFunctionalTest.php new file mode 100644 index 0000000..164b85b --- /dev/null +++ b/tests/Smoke/ApplicationAvailabilityFunctionalTest.php @@ -0,0 +1,28 @@ +request('GET', $url); + + self::assertNotSame(500, $client->getResponse()->getStatusCode()); + } + + public static function urlProvider(): \Generator + { + yield ['/users']; + yield [sprintf('/users/%s', UserFixtures::USER_ULID)]; + yield ['/videos']; + yield [sprintf('/videos/%s', VideoFixtures::COMPLETE_ULID)]; + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..27d6ef2 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,13 @@ +bootEnv(dirname(__DIR__).'/.env'); +} + +if (true === $_SERVER['APP_DEBUG']) { + umask(0000); +} From 8c5b9f2dda2da9d123c853e52c9149152e207e82 Mon Sep 17 00:00:00 2001 From: Maxime CORNET Date: Fri, 6 Jun 2025 21:03:38 +0200 Subject: [PATCH 08/19] test-install instead of ci-install... --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8a026b0..ef430f7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v4 - name: Install the project for test environment - run: make ci-install + run: make test-install - name: Run smoke tests run: make test-smoke @@ -28,7 +28,7 @@ jobs: uses: actions/checkout@v4 - name: Install the project for test environment - run: make ci-install + run: make test-install - name: Run functional tests run: make test-functional From 1b6a88a3ed3148be85b99cee38ffaceb08abfaef Mon Sep 17 00:00:00 2001 From: Maxime CORNET Date: Fri, 6 Jun 2025 21:04:18 +0200 Subject: [PATCH 09/19] Add missing templates directory to php dockerfile --- docker/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index 1da776e..b00c474 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -21,6 +21,7 @@ COPY --chown=www-data:www-data config config/ COPY --chown=www-data:www-data public public/ COPY --chown=www-data:www-data migrations migrations/ COPY --chown=www-data:www-data src src/ +COPY --chown=www-data:www-data templates templates/ RUN set -eux; \ mkdir -p var/cache var/log; \ From 58306ab1fe91876ff9813e12a2c6773b7453ed37 Mon Sep 17 00:00:00 2001 From: Maxime CORNET Date: Fri, 6 Jun 2025 21:09:08 +0200 Subject: [PATCH 10/19] CI tests will be triggered only if QA succeeded --- .github/workflows/ci.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ef430f7..ae955a9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -5,6 +5,10 @@ on: branches: - main pull_request: ~ + workflow_run: + workflows: ["Execute QA tests"] + types: + - completed jobs: test-smoke: From 7521003d9625b8abed0575f6906a4089e63837a4 Mon Sep 17 00:00:00 2001 From: Maxime CORNET Date: Fri, 6 Jun 2025 21:11:04 +0200 Subject: [PATCH 11/19] Oops, I did it again :notes: --- .github/workflows/ci.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ae955a9..da36b70 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,6 +13,7 @@ on: jobs: test-smoke: name: Run smoke tests + if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest steps: - name: Checkout @@ -26,6 +27,7 @@ jobs: test-functional: name: Run functional tests + if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest steps: - name: Checkout From 383c2a19effb69d8267c73e22c80d2d5e5ce6966 Mon Sep 17 00:00:00 2001 From: Maxime CORNET Date: Fri, 6 Jun 2025 21:16:36 +0200 Subject: [PATCH 12/19] fix phpstan errors --- tests/Functional/User/RegistrationTest.php | 5 ++++- tests/Functional/User/UserTest.php | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/Functional/User/RegistrationTest.php b/tests/Functional/User/RegistrationTest.php index 2760a17..40e105a 100644 --- a/tests/Functional/User/RegistrationTest.php +++ b/tests/Functional/User/RegistrationTest.php @@ -30,10 +30,13 @@ public function asAnonymousICanRegisterMyself(): void self::assertEmailCount(1); } + /** + * @param array $violations + */ #[Test] #[DataProvider('invalidData')] #[Group('debug')] - public function asAnonymousICannotRegisterWithInvalidPassword(?string $email, ?string $password, ?string $username, ?string $countryCode, array $violations): void + public function asAnonymousICannotRegisterWithInvalidPassword(?string $email, ?string $password, ?string $username, ?string $countryCode, array $violations = []): void { $data = array_filter([ 'email' => $email, diff --git a/tests/Functional/User/UserTest.php b/tests/Functional/User/UserTest.php index a422853..bccc77a 100644 --- a/tests/Functional/User/UserTest.php +++ b/tests/Functional/User/UserTest.php @@ -16,7 +16,7 @@ final class UserTest extends ApiTestCase #[DataProvider('getUserCollectionAccessCases')] public function iCanOrCannotGetACollectionOfUsers(?string $userUlid, int $expectedStatusCode): void { - $client = $userUlid + $client = null !== $userUlid ? $this->authenticateTestClientAs($userUlid) : $this->client; @@ -30,6 +30,9 @@ public function iCanOrCannotGetACollectionOfUsers(?string $userUlid, int $expect } } + /** + * @return iterable + */ public static function getUserCollectionAccessCases(): iterable { yield 'anonymous cannot access' => [ @@ -48,11 +51,15 @@ public static function getUserCollectionAccessCases(): iterable ]; } + /** + * @param array $expectedData + * @param array $forbiddenFields + */ #[Test] #[DataProvider('getUserAccessCases')] - public function iCanGetAUser(?string $userUlid, array $expectedData, array $forbiddenFields): void + public function iCanGetAUser(?string $userUlid, array $expectedData = [], array $forbiddenFields = []): void { - $client = $userUlid + $client = null !== $userUlid ? $this->authenticateTestClientAs($userUlid) : $this->client; @@ -70,6 +77,9 @@ public function iCanGetAUser(?string $userUlid, array $expectedData, array $forb self::assertJsonContains($expectedData); } + /** + * @return iterable + */ public static function getUserAccessCases(): iterable { yield 'anonymous' => [ From b8addda6007341c1644628ea3af37d2654283943 Mon Sep 17 00:00:00 2001 From: Maxime CORNET Date: Fri, 6 Jun 2025 21:24:43 +0200 Subject: [PATCH 13/19] Remove push & pull_request on 'on' --- .github/workflows/ci.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index da36b70..bfd1b77 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,10 +1,6 @@ name: Execute CI tests on: - push: - branches: - - main - pull_request: ~ workflow_run: workflows: ["Execute QA tests"] types: From 880ac63ea15cb14264a8c14ace83b235ef0e9e2b Mon Sep 17 00:00:00 2001 From: Maxime CORNET Date: Fri, 6 Jun 2025 21:31:26 +0200 Subject: [PATCH 14/19] Let's try without quotes :sob: :sob: --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bfd1b77..d0137ea 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,7 +2,7 @@ name: Execute CI tests on: workflow_run: - workflows: ["Execute QA tests"] + workflows: [Execute QA tests] types: - completed From 00cd459763717a6ef80cb2783d2bef33fce4b4bb Mon Sep 17 00:00:00 2001 From: Maxime CORNET Date: Fri, 6 Jun 2025 21:57:14 +0200 Subject: [PATCH 15/19] Remove workflow_run --- .github/workflows/ci.yaml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d0137ea..ef430f7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,15 +1,14 @@ name: Execute CI tests on: - workflow_run: - workflows: [Execute QA tests] - types: - - completed + push: + branches: + - main + pull_request: ~ jobs: test-smoke: name: Run smoke tests - if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest steps: - name: Checkout @@ -23,7 +22,6 @@ jobs: test-functional: name: Run functional tests - if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest steps: - name: Checkout From c9ab898a1e87bdd93a7c2b8a7f62d9e90d50c187 Mon Sep 17 00:00:00 2001 From: Maxime CORNET Date: Tue, 10 Jun 2025 20:33:31 +0200 Subject: [PATCH 16/19] Use clock to generate the datetime of signedUri --- src/Service/Registration/GenerateSignedUriService.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Service/Registration/GenerateSignedUriService.php b/src/Service/Registration/GenerateSignedUriService.php index 086107e..4be98ab 100644 --- a/src/Service/Registration/GenerateSignedUriService.php +++ b/src/Service/Registration/GenerateSignedUriService.php @@ -3,13 +3,17 @@ namespace App\Service\Registration; use App\Entity\User; +use Symfony\Component\Clock\ClockInterface; use Symfony\Component\HttpFoundation\UriSigner; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; final readonly class GenerateSignedUriService { - public function __construct(private UriSigner $uriSigner, private UrlGeneratorInterface $urlGenerator) - { + public function __construct( + private UriSigner $uriSigner, + private UrlGeneratorInterface $urlGenerator, + private ClockInterface $clock, + ) { } public function generateSignedUri(User $user): string @@ -20,7 +24,7 @@ public function generateSignedUri(User $user): string ['id' => $user->getId()], UrlGeneratorInterface::ABSOLUTE_URL ), - new \DateTimeImmutable('+1 day') + $this->clock->now()->modify('+1 day') ); } } From f0f747e85a5745aec05db26e365284f54fe8981a Mon Sep 17 00:00:00 2001 From: Maxime CORNET Date: Tue, 10 Jun 2025 20:34:41 +0200 Subject: [PATCH 17/19] Throw 400 if hash is expired or invalid and 410 if user is already validated --- src/Controller/RegistrationController.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Controller/RegistrationController.php b/src/Controller/RegistrationController.php index f46ffa2..f16e9c9 100644 --- a/src/Controller/RegistrationController.php +++ b/src/Controller/RegistrationController.php @@ -8,7 +8,8 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\UriSigner; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\GoneHttpException; use Symfony\Component\Routing\Attribute\Route; class RegistrationController extends AbstractController @@ -16,8 +17,12 @@ class RegistrationController extends AbstractController #[Route('/users/{id}/verify/email', name: 'verify_email', methods: ['GET'])] public function index(Request $request, User $user, UriSigner $uriSigner, EntityManagerInterface $entityManager): JsonResponse { - if (!$uriSigner->checkRequest($request) || $user->isEmailVerified()) { - throw new NotFoundHttpException(); + if ($user->isEmailVerified()) { + throw new GoneHttpException(); + } + + if (!$uriSigner->checkRequest($request)) { + throw new BadRequestHttpException(); } $user->setEmailVerified(true); From 53e49ad5f20c6701f7bd9204f369d0331684c225 Mon Sep 17 00:00:00 2001 From: Maxime CORNET Date: Tue, 10 Jun 2025 20:35:42 +0200 Subject: [PATCH 18/19] Update routing config to handle test, dev & prod env default_uri --- config/packages/dev/routing.yaml | 3 +++ config/packages/prod/routing.yaml | 3 +++ config/packages/routing.yaml | 5 ----- config/packages/test/routing.yaml | 3 +++ 4 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 config/packages/dev/routing.yaml create mode 100644 config/packages/prod/routing.yaml create mode 100644 config/packages/test/routing.yaml diff --git a/config/packages/dev/routing.yaml b/config/packages/dev/routing.yaml new file mode 100644 index 0000000..999d5dc --- /dev/null +++ b/config/packages/dev/routing.yaml @@ -0,0 +1,3 @@ +framework: + router: + default_uri: 'http://api.esv.localhost' diff --git a/config/packages/prod/routing.yaml b/config/packages/prod/routing.yaml new file mode 100644 index 0000000..b3e6a0a --- /dev/null +++ b/config/packages/prod/routing.yaml @@ -0,0 +1,3 @@ +framework: + router: + strict_requirements: null diff --git a/config/packages/routing.yaml b/config/packages/routing.yaml index 9437fff..0ab0b53 100644 --- a/config/packages/routing.yaml +++ b/config/packages/routing.yaml @@ -1,8 +1,3 @@ framework: router: default_uri: 'https://www.esports-videos.com/' - -when@prod: - framework: - router: - strict_requirements: null diff --git a/config/packages/test/routing.yaml b/config/packages/test/routing.yaml new file mode 100644 index 0000000..c0af4bd --- /dev/null +++ b/config/packages/test/routing.yaml @@ -0,0 +1,3 @@ +framework: + router: + default_uri: 'http://localhost' From b03007fd9cac4de49949c7dae047196eaf2b85f3 Mon Sep 17 00:00:00 2001 From: Maxime CORNET Date: Tue, 10 Jun 2025 20:35:57 +0200 Subject: [PATCH 19/19] Add more tests for Registration --- tests/Functional/User/RegistrationTest.php | 170 +++++++++++++++++---- 1 file changed, 138 insertions(+), 32 deletions(-) diff --git a/tests/Functional/User/RegistrationTest.php b/tests/Functional/User/RegistrationTest.php index 40e105a..ca20c1f 100644 --- a/tests/Functional/User/RegistrationTest.php +++ b/tests/Functional/User/RegistrationTest.php @@ -2,28 +2,26 @@ namespace App\Tests\Functional\User; +use App\Service\Registration\GenerateSignedUriService; use App\Tests\Functional\ApiTestCase; use DataFixtures\Data\UserFixtures; +use DataFixtures\Factory\UserFactory; use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; +use Symfony\Component\Clock\MockClock; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\UriSigner; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; class RegistrationTest extends ApiTestCase { #[Test] public function asAnonymousICanRegisterMyself(): void { - $this->client->request('POST', '/users', [ - 'headers' => [ - 'Content-Type' => 'application/json', - ], - 'body' => json_encode([ - 'email' => 'new_account+alias@esports-videos.com', - 'username' => 'new_account', - 'password' => UserFixtures::DEFAULT_PASSWORD, - 'country' => 'FR', - ], JSON_THROW_ON_ERROR), + $this->register([ + 'email' => 'new_account+alias@esports-videos.com', + 'username' => 'new_account', + 'password' => UserFixtures::DEFAULT_PASSWORD, ]); self::assertResponseStatusCodeSame(Response::HTTP_CREATED); @@ -35,8 +33,7 @@ public function asAnonymousICanRegisterMyself(): void */ #[Test] #[DataProvider('invalidData')] - #[Group('debug')] - public function asAnonymousICannotRegisterWithInvalidPassword(?string $email, ?string $password, ?string $username, ?string $countryCode, array $violations = []): void + public function asAnonymousICannotRegisterWithInvalidData(?string $email, ?string $password, ?string $username, ?string $countryCode, array $violations = []): void { $data = array_filter([ 'email' => $email, @@ -45,12 +42,7 @@ public function asAnonymousICannotRegisterWithInvalidPassword(?string $email, ?s 'country' => $countryCode, ], static fn ($value) => null !== $value); - $this->client->request('POST', '/users', [ - 'headers' => [ - 'Content-Type' => 'application/json', - ], - 'body' => json_encode($data, JSON_THROW_ON_ERROR), - ]); + $this->register($data); self::assertResponseStatusCodeSame(Response::HTTP_UNPROCESSABLE_ENTITY); self::assertResponseHeaderSame('content-type', 'application/problem+json; charset=utf-8'); @@ -63,19 +55,111 @@ public function asAnonymousICannotRegisterWithInvalidPassword(?string $email, ?s self::assertEmailCount(0); } + #[Test] + public function asAnonymousICanVerifyMyAccount(): void + { + $container = static::getContainer(); + $generateSignedUriService = $container->get(GenerateSignedUriService::class); + $user = UserFactory::find(UserFixtures::UNVERIFIED_USER_ULID); + + $this->client->request('GET', $generateSignedUriService->generateSignedUri($user)); + + self::assertResponseStatusCodeSame(Response::HTTP_OK); + } + + #[Test] + public function asAnonymousICantVerifyWithExpiredLink(): void + { + $container = static::getContainer(); + $clock = new MockClock(new \DateTimeImmutable('-48 hours')); + + $generateSignedUriService = new GenerateSignedUriService( + $container->get(UriSigner::class), + $container->get(UrlGeneratorInterface::class), + $clock + ); + $user = UserFactory::find(UserFixtures::UNVERIFIED_USER_ULID); + + $signedUri = $generateSignedUriService->generateSignedUri($user); + + $this->client->request('GET', $signedUri); + self::assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST); + } + + #[Test] + public function asAnonymousICantVerifyWithSameLinkTwice(): void + { + $container = static::getContainer(); + /** @var GenerateSignedUriService $generateSignedUriService */ + $generateSignedUriService = $container->get(GenerateSignedUriService::class); + $user = UserFactory::find(UserFixtures::UNVERIFIED_USER_ULID); + + $signedUri = $generateSignedUriService->generateSignedUri($user); + + $this->client->request('GET', $signedUri); + self::assertResponseStatusCodeSame(Response::HTTP_OK); + + $this->client->request('GET', $signedUri); + self::assertResponseStatusCodeSame(Response::HTTP_GONE); + } + + #[Test] + public function asAnonymousICantVerifyAnAccountAlreadyValidated(): void + { + $container = static::getContainer(); + $generateSignedUriService = $container->get(GenerateSignedUriService::class); + $user = UserFactory::find(UserFixtures::ADMIN_ULID); + + $this->client->request('GET', $generateSignedUriService->generateSignedUri($user)); + + self::assertResponseStatusCodeSame(Response::HTTP_GONE); + } + + #[Test] + public function asAnonymousICantVerifyAnAccountWithInvalidHash(): void + { + $this->client->request('GET', sprintf('/users/%s/verify/email?_hash=invalid', UserFixtures::UNVERIFIED_USER_ULID)); + + self::assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST); + } + + #[Test] + public function asAnonymousICannotRegisterWithInvalidMimeType(): void + { + $this->client->request('POST', '/users', [ + 'headers' => [ + 'Content-Type' => 'text/plain', + ], + 'body' => '{"email":"foo@example.com"}', + ]); + + self::assertResponseStatusCodeSame(Response::HTTP_UNSUPPORTED_MEDIA_TYPE); + } + public static function invalidData(): \Generator { - // Empty array - yield [ + yield 'empty_values' => [ 'email' => null, 'password' => null, 'countryCode' => null, 'username' => null, - 'violations' => [], + 'violations' => [ + [ + 'propertyPath' => 'email', + 'message' => 'This value should not be blank.', + ], + [ + 'propertyPath' => 'username', + 'message' => 'This value should not be blank.', + ], + [ + 'propertyPath' => 'password', + 'message' => 'This value should not be blank.', + ], + ], ]; - // Email is omitted - yield [ + yield 'omitted_email' => [ 'email' => null, 'password' => UserFixtures::DEFAULT_PASSWORD, 'countryCode' => 'FR', @@ -88,8 +172,7 @@ public static function invalidData(): \Generator ], ]; - // Password is omitted - yield [ + yield 'omitted_password' => [ 'email' => 'correct_email@esports-videos.com', 'password' => null, 'countryCode' => 'FR', @@ -102,8 +185,7 @@ public static function invalidData(): \Generator ], ]; - // Invalid Email - yield [ + yield 'invalid_email' => [ 'email' => 'invalidEmail', 'password' => UserFixtures::DEFAULT_PASSWORD, 'countryCode' => 'FR', @@ -116,8 +198,7 @@ public static function invalidData(): \Generator ], ]; - // Invalid Country code - yield [ + yield 'invalid_country_code' => [ 'email' => 'correct_email@esports-videos.com', 'password' => UserFixtures::DEFAULT_PASSWORD, 'countryCode' => 'BAD', @@ -130,8 +211,7 @@ public static function invalidData(): \Generator ], ]; - // Invalid Password - yield [ + yield 'invalid_password' => [ 'email' => 'correct_email@esports-videos.com', 'password' => 'badPassword', 'countryCode' => 'FR', @@ -143,5 +223,31 @@ public static function invalidData(): \Generator ], ], ]; + + yield 'email_already_exists' => [ + 'email' => 'admin@esports-videos.com', + 'password' => UserFixtures::DEFAULT_PASSWORD, + 'countryCode' => 'FR', + 'username' => 'correct_username', + 'violations' => [ + [ + 'propertyPath' => 'email', + 'message' => 'This value is already used.', + ], + ], + ]; + } + + /** + * @param array $data + */ + private function register(array $data): void + { + $this->client->request('POST', '/users', [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => json_encode($data, JSON_THROW_ON_ERROR), + ]); } }