diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..899d0a0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git/ +.gitignore +var/ +vendor/ +tests/ +.phpunit.cache/ +docs/ +*.md +.env +.env.test +.env.local +composer.phar diff --git a/.gitea/workflows/tests.yaml b/.gitea/workflows/tests.yaml index 80992d3..3eaea61 100644 --- a/.gitea/workflows/tests.yaml +++ b/.gitea/workflows/tests.yaml @@ -29,5 +29,8 @@ jobs: - name: Install dependencies run: composer install --prefer-dist --no-progress + - name: Run php-cs-fixer + run: vendor/bin/php-cs-fixer fix --dry-run --diff + - name: Run tests run: XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text diff --git a/.gitignore b/.gitignore index 8813ff5..afa4fc1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,12 @@ /.phpunit.cache/ /bin/.phpunit.result.cache ###< phpunit/phpunit ### + +###> project-specific ### +/config/reference.php +###< project-specific ### + +###> friendsofphp/php-cs-fixer ### +/.php-cs-fixer.php +/.php-cs-fixer.cache +###< friendsofphp/php-cs-fixer ### diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..b840706 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,18 @@ +in(__DIR__) + ->exclude('var') + ->exclude('vendor') + ->notPath([ + 'config/bundles.php', + 'config/reference.php', + ]) +; + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PSR12' => true, + ]) + ->setFinder($finder) +; diff --git a/bin/franken.sh b/bin/franken.sh index 5c35363..d4b6b52 100755 --- a/bin/franken.sh +++ b/bin/franken.sh @@ -1,7 +1,9 @@ #!/bin/sh +# Dev utility — builds and runs the preauth container locally. +# Not for production use. docker container rm preauth -docker build . -t digtialadapt/preauth:dev +docker build . -t digitaladapt/preauth:dev docker run --name preauth \ -e APP_ENV=dev \ -e APP_DEBUG=true \ @@ -10,4 +12,4 @@ docker run --name preauth \ -e DEFAULT_URI=http://localhost \ -v ./var/share:/app/var/share \ -p 8000:80 \ - digtialadapt/preauth:dev + digitaladapt/preauth:dev diff --git a/composer.json b/composer.json index 83939de..239210b 100644 --- a/composer.json +++ b/composer.json @@ -75,6 +75,7 @@ } }, "require-dev": { + "friendsofphp/php-cs-fixer": "*", "phpunit/phpunit": "^13.2", "symfony/browser-kit": "7.4.*", "symfony/css-selector": "7.4.*" diff --git a/composer.lock b/composer.lock index 506efbb..a570bd2 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": "fda1ef2029360e1c56bfb2dd807e6994", + "content-hash": "2ef28f97b042de062226d51be1857a6f", "packages": [ { "name": "bacon/bacon-qr-code", @@ -3659,6 +3659,571 @@ } ], "packages-dev": [ + { + "name": "clue/ndjson-react", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", + "keywords": [ + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" + }, + { + "name": "composer/pcre", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<2.2.2" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-06-07T11:47:49+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "ergebnis/agent-detector", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/agent-detector.git", + "reference": "e211f17928c8b95a51e06040792d57f5462fb271" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/agent-detector/zipball/e211f17928c8b95a51e06040792d57f5462fb271", + "reference": "e211f17928c8b95a51e06040792d57f5462fb271", + "shasum": "" + }, + "require": { + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.51.0", + "ergebnis/license": "^2.7.0", + "ergebnis/php-cs-fixer-config": "^6.60.2", + "ergebnis/phpstan-rules": "^2.13.1", + "ergebnis/phpunit-slow-test-detector": "^2.24.0", + "ergebnis/rector-rules": "^1.18.1", + "fakerphp/faker": "^1.24.1", + "infection/infection": "^0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.54", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "phpunit/phpunit": "^9.6.34", + "rector/rector": "^2.4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\AgentDetector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a detector for detecting the presence of an agent.", + "homepage": "https://github.com/ergebnis/agent-detector", + "support": { + "issues": "https://github.com/ergebnis/agent-detector/issues", + "security": "https://github.com/ergebnis/agent-detector/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/agent-detector" + }, + "time": "2026-05-07T08:19:07+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-08-14T07:29:31+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.95.18", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "a8b4e4216faabf67f4e96110ee99a48c96e4e683" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/a8b4e4216faabf67f4e96110ee99a48c96e4e683", + "reference": "a8b4e4216faabf67f4e96110ee99a48c96e4e683", + "shasum": "" + }, + "require": { + "clue/ndjson-react": "^1.3", + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.5", + "ergebnis/agent-detector": "^1.2", + "ext-filter": "*", + "ext-hash": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "fidry/cpu-core-counter": "^1.3", + "php": "^7.4 || ^8.0", + "react/child-process": "^0.6.6", + "react/event-loop": "^1.5", + "react/socket": "^1.16", + "react/stream": "^1.4", + "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0 || ^8.0 || ^9.0", + "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.37", + "symfony/polyfill-php80": "^1.37", + "symfony/polyfill-php81": "^1.37", + "symfony/polyfill-php84": "^1.37", + "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2 || ^8.0", + "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0" + }, + "require-dev": { + "facile-it/paraunit": "^1.3.1 || ^2.11.0", + "infection/infection": "^0.32.7", + "justinrainbow/json-schema": "^6.10.0", + "keradus/cli-executor": "^2.3", + "mikey179/vfsstream": "^1.6.12", + "php-coveralls/php-coveralls": "^2.9.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.8", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.8", + "phpunit/phpunit": "^9.6.35 || ^10.5.64 || ^11.5.56 || ^12.5.31 || ^13.0.6", + "symfony/polyfill-php85": "^1.38", + "symfony/var-dumper": "^5.4.48 || ^6.4.36 || ^7.4.8 || ^8.1.1", + "symfony/yaml": "^5.4.53 || ^6.4.41 || ^7.4.13 || ^8.1.1" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + }, + "exclude-from-classmap": [ + "src/**/Internal/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.18" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2026-07-30T15:46:02+00:00" + }, { "name": "masterminds/html5", "version": "2.10.1", @@ -4436,6 +5001,532 @@ ], "time": "2026-07-28T14:00:09+00:00" }, + { + "name": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/child-process", + "version": "v0.6.7", + "source": { + "type": "git", + "url": "https://github.com/reactphp/child-process.git", + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/970f0e71945556422ee4570ccbabaedc3cf04ad3", + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/event-loop": "^1.2", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/socket": "^1.16", + "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\ChildProcess\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven library for executing child processes with ReactPHP.", + "keywords": [ + "event-driven", + "process", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/child-process/issues", + "source": "https://github.com/reactphp/child-process/tree/v0.6.7" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-12-23T15:25:20+00:00" + }, + { + "name": "react/dns", + "version": "v1.14.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.14.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-18T19:34:28+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-17T20:46:25+00:00" + }, + { + "name": "react/promise", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.12.28 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-08-19T18:57:03+00:00" + }, + { + "name": "react/socket", + "version": "v1.17.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.17.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-19T20:47:34+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, { "name": "sebastian/cli-parser", "version": "5.0.0", @@ -5809,6 +6900,217 @@ ], "time": "2026-05-20T07:20:23+00:00" }, + { + "name": "symfony/polyfill-php84", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "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": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T12:51:13+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "f5804be144caceb570f6747519999636b664f24c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/f5804be144caceb570f6747519999636b664f24c", + "reference": "f5804be144caceb570f6747519999636b664f24c", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "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": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-23T16:05:06+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "70a852d72fec4d51efb1f48dcd968efcaf5ccb89" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/70a852d72fec4d51efb1f48dcd968efcaf5ccb89", + "reference": "70a852d72fec4d51efb1f48dcd968efcaf5ccb89", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "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 a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, { "name": "theseer/tokenizer", "version": "2.0.1", @@ -5871,5 +7173,5 @@ "ext-iconv": "*" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/config/reference.php b/config/reference.php deleted file mode 100644 index 515b64f..0000000 --- a/config/reference.php +++ /dev/null @@ -1,844 +0,0 @@ - [ - * 'App\\' => [ - * 'resource' => '../src/', - * ], - * ], - * ]); - * ``` - * - * @psalm-type ImportsConfig = list - * @psalm-type ParametersConfig = array|Param|null>|Param|null> - * @psalm-type ArgumentsType = list|array - * @psalm-type CallType = array|array{0:string, 1?:ArgumentsType, 2?:bool}|array{method:string, arguments?:ArgumentsType, returns_clone?:bool} - * @psalm-type TagsType = list>> // arrays inside the list must have only one element, with the tag name as the key - * @psalm-type CallbackType = string|array{0:string|ReferenceConfigurator,1:string}|\Closure|ReferenceConfigurator - * @psalm-type DeprecationType = array{package: string, version: string, message?: string} - * @psalm-type DefaultsType = array{ - * public?: bool, - * tags?: TagsType, - * resource_tags?: TagsType, - * autowire?: bool, - * autoconfigure?: bool, - * bind?: array, - * } - * @psalm-type InstanceofType = array{ - * shared?: bool, - * lazy?: bool|string, - * public?: bool, - * properties?: array, - * configurator?: CallbackType, - * calls?: list, - * tags?: TagsType, - * resource_tags?: TagsType, - * autowire?: bool, - * bind?: array, - * constructor?: string, - * } - * @psalm-type DefinitionType = array{ - * class?: string, - * file?: string, - * parent?: string, - * shared?: bool, - * synthetic?: bool, - * lazy?: bool|string, - * public?: bool, - * abstract?: bool, - * deprecated?: DeprecationType, - * factory?: CallbackType, - * configurator?: CallbackType, - * arguments?: ArgumentsType, - * properties?: array, - * calls?: list, - * tags?: TagsType, - * resource_tags?: TagsType, - * decorates?: string, - * decoration_inner_name?: string, - * decoration_priority?: int, - * decoration_on_invalid?: 'exception'|'ignore'|null, - * autowire?: bool, - * autoconfigure?: bool, - * bind?: array, - * constructor?: string, - * from_callable?: CallbackType, - * } - * @psalm-type AliasType = string|array{ - * alias: string, - * public?: bool, - * deprecated?: DeprecationType, - * } - * @psalm-type PrototypeType = array{ - * resource: string, - * namespace?: string, - * exclude?: string|list, - * parent?: string, - * shared?: bool, - * lazy?: bool|string, - * public?: bool, - * abstract?: bool, - * deprecated?: DeprecationType, - * factory?: CallbackType, - * arguments?: ArgumentsType, - * properties?: array, - * configurator?: CallbackType, - * calls?: list, - * tags?: TagsType, - * resource_tags?: TagsType, - * autowire?: bool, - * autoconfigure?: bool, - * bind?: array, - * constructor?: string, - * } - * @psalm-type StackType = array{ - * stack: list>, - * public?: bool, - * deprecated?: DeprecationType, - * } - * @psalm-type ServicesConfig = array{ - * _defaults?: DefaultsType, - * _instanceof?: InstanceofType, - * ... - * } - * @psalm-type ExtensionType = array - * @psalm-type FrameworkConfig = array{ - * secret?: scalar|Param|null, - * http_method_override?: bool|Param, // Set true to enable support for the '_method' request parameter to determine the intended HTTP method on POST requests. // Default: false - * allowed_http_method_override?: list|null, - * trust_x_sendfile_type_header?: scalar|Param|null, // Set true to enable support for xsendfile in binary file responses. // Default: "%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%" - * ide?: scalar|Param|null, // Default: "%env(default::SYMFONY_IDE)%" - * test?: bool|Param, - * default_locale?: scalar|Param|null, // Default: "en" - * set_locale_from_accept_language?: bool|Param, // Whether to use the Accept-Language HTTP header to set the Request locale (only when the "_locale" request attribute is not passed). // Default: false - * set_content_language_from_locale?: bool|Param, // Whether to set the Content-Language HTTP header on the Response using the Request locale. // Default: false - * enabled_locales?: list, - * trusted_hosts?: list, - * trusted_proxies?: mixed, // Default: ["%env(default::SYMFONY_TRUSTED_PROXIES)%"] - * trusted_headers?: list, - * error_controller?: scalar|Param|null, // Default: "error_controller" - * handle_all_throwables?: bool|Param, // HttpKernel will handle all kinds of \Throwable. // Default: true - * csrf_protection?: bool|array{ - * enabled?: scalar|Param|null, // Default: null - * stateless_token_ids?: list, - * check_header?: scalar|Param|null, // Whether to check the CSRF token in a header in addition to a cookie when using stateless protection. // Default: false - * cookie_name?: scalar|Param|null, // The name of the cookie to use when using stateless protection. // Default: "csrf-token" - * }, - * form?: bool|array{ // Form configuration - * enabled?: bool|Param, // Default: false - * csrf_protection?: bool|array{ - * enabled?: scalar|Param|null, // Default: null - * token_id?: scalar|Param|null, // Default: null - * field_name?: scalar|Param|null, // Default: "_token" - * field_attr?: array, - * }, - * }, - * http_cache?: bool|array{ // HTTP cache configuration - * enabled?: bool|Param, // Default: false - * debug?: bool|Param, // Default: "%kernel.debug%" - * trace_level?: "none"|"short"|"full"|Param, - * trace_header?: scalar|Param|null, - * default_ttl?: int|Param, - * private_headers?: list, - * skip_response_headers?: list, - * allow_reload?: bool|Param, - * allow_revalidate?: bool|Param, - * stale_while_revalidate?: int|Param, - * stale_if_error?: int|Param, - * terminate_on_cache_hit?: bool|Param, - * }, - * esi?: bool|array{ // ESI configuration - * enabled?: bool|Param, // Default: false - * }, - * ssi?: bool|array{ // SSI configuration - * enabled?: bool|Param, // Default: false - * }, - * fragments?: bool|array{ // Fragments configuration - * enabled?: bool|Param, // Default: false - * hinclude_default_template?: scalar|Param|null, // Default: null - * path?: scalar|Param|null, // Default: "/_fragment" - * }, - * profiler?: bool|array{ // Profiler configuration - * enabled?: bool|Param, // Default: false - * collect?: bool|Param, // Default: true - * collect_parameter?: scalar|Param|null, // The name of the parameter to use to enable or disable collection on a per request basis. // Default: null - * only_exceptions?: bool|Param, // Default: false - * only_main_requests?: bool|Param, // Default: false - * dsn?: scalar|Param|null, // Default: "file:%kernel.cache_dir%/profiler" - * collect_serializer_data?: bool|Param, // Enables the serializer data collector and profiler panel. // Default: false - * }, - * workflows?: bool|array{ - * enabled?: bool|Param, // Default: false - * workflows?: array, - * definition_validators?: list, - * support_strategy?: scalar|Param|null, - * initial_marking?: list, - * events_to_dispatch?: list|null, - * places?: list, - * }>, - * transitions?: list, - * to?: list, - * weight?: int|Param, // Default: 1 - * metadata?: array, - * }>, - * metadata?: array, - * }>, - * }, - * router?: bool|array{ // Router configuration - * enabled?: bool|Param, // Default: false - * resource?: scalar|Param|null, - * type?: scalar|Param|null, - * cache_dir?: scalar|Param|null, // Deprecated: Setting the "framework.router.cache_dir.cache_dir" configuration option is deprecated. It will be removed in version 8.0. // Default: "%kernel.build_dir%" - * default_uri?: scalar|Param|null, // The default URI used to generate URLs in a non-HTTP context. // Default: null - * http_port?: scalar|Param|null, // Default: 80 - * https_port?: scalar|Param|null, // Default: 443 - * strict_requirements?: scalar|Param|null, // set to true to throw an exception when a parameter does not match the requirements set to false to disable exceptions when a parameter does not match the requirements (and return null instead) set to null to disable parameter checks against requirements 'true' is the preferred configuration in development mode, while 'false' or 'null' might be preferred in production // Default: true - * utf8?: bool|Param, // Default: true - * }, - * session?: bool|array{ // Session configuration - * enabled?: bool|Param, // Default: false - * storage_factory_id?: scalar|Param|null, // Default: "session.storage.factory.native" - * handler_id?: scalar|Param|null, // Defaults to using the native session handler, or to the native *file* session handler if "save_path" is not null. - * name?: scalar|Param|null, - * cookie_lifetime?: scalar|Param|null, - * cookie_path?: scalar|Param|null, - * cookie_domain?: scalar|Param|null, - * cookie_secure?: true|false|"auto"|Param, // Default: "auto" - * cookie_httponly?: bool|Param, // Default: true - * cookie_samesite?: null|"lax"|"strict"|"none"|Param, // Default: "lax" - * use_cookies?: bool|Param, - * gc_divisor?: scalar|Param|null, - * gc_probability?: scalar|Param|null, - * gc_maxlifetime?: scalar|Param|null, - * save_path?: scalar|Param|null, // Defaults to "%kernel.cache_dir%/sessions" if the "handler_id" option is not null. - * metadata_update_threshold?: int|Param, // Seconds to wait between 2 session metadata updates. // Default: 0 - * sid_length?: int|Param, // Deprecated: Setting the "framework.session.sid_length.sid_length" configuration option is deprecated. It will be removed in version 8.0. No alternative is provided as PHP 8.4 has deprecated the related option. - * sid_bits_per_character?: int|Param, // Deprecated: Setting the "framework.session.sid_bits_per_character.sid_bits_per_character" configuration option is deprecated. It will be removed in version 8.0. No alternative is provided as PHP 8.4 has deprecated the related option. - * }, - * request?: bool|array{ // Request configuration - * enabled?: bool|Param, // Default: false - * formats?: array>, - * }, - * assets?: bool|array{ // Assets configuration - * enabled?: bool|Param, // Default: false - * strict_mode?: bool|Param, // Throw an exception if an entry is missing from the manifest.json. // Default: false - * version_strategy?: scalar|Param|null, // Default: null - * version?: scalar|Param|null, // Default: null - * version_format?: scalar|Param|null, // Default: "%%s?%%s" - * json_manifest_path?: scalar|Param|null, // Default: null - * base_path?: scalar|Param|null, // Default: "" - * base_urls?: list, - * packages?: array, - * }>, - * }, - * asset_mapper?: bool|array{ // Asset Mapper configuration - * enabled?: bool|Param, // Default: false - * paths?: array, - * excluded_patterns?: list, - * exclude_dotfiles?: bool|Param, // If true, any files starting with "." will be excluded from the asset mapper. // Default: true - * server?: bool|Param, // If true, a "dev server" will return the assets from the public directory (true in "debug" mode only by default). // Default: true - * public_prefix?: scalar|Param|null, // The public path where the assets will be written to (and served from when "server" is true). // Default: "/assets/" - * missing_import_mode?: "strict"|"warn"|"ignore"|Param, // Behavior if an asset cannot be found when imported from JavaScript or CSS files - e.g. "import './non-existent.js'". "strict" means an exception is thrown, "warn" means a warning is logged, "ignore" means the import is left as-is. // Default: "warn" - * extensions?: array, - * importmap_path?: scalar|Param|null, // The path of the importmap.php file. // Default: "%kernel.project_dir%/importmap.php" - * importmap_polyfill?: scalar|Param|null, // The importmap name that will be used to load the polyfill. Set to false to disable. // Default: "es-module-shims" - * importmap_script_attributes?: array, - * vendor_dir?: scalar|Param|null, // The directory to store JavaScript vendors. // Default: "%kernel.project_dir%/assets/vendor" - * precompress?: bool|array{ // Precompress assets with Brotli, Zstandard and gzip. - * enabled?: bool|Param, // Default: false - * formats?: list, - * extensions?: list, - * }, - * }, - * translator?: bool|array{ // Translator configuration - * enabled?: bool|Param, // Default: false - * fallbacks?: list, - * logging?: bool|Param, // Default: false - * formatter?: scalar|Param|null, // Default: "translator.formatter.default" - * cache_dir?: scalar|Param|null, // Default: "%kernel.cache_dir%/translations" - * default_path?: scalar|Param|null, // The default path used to load translations. // Default: "%kernel.project_dir%/translations" - * paths?: list, - * pseudo_localization?: bool|array{ - * enabled?: bool|Param, // Default: false - * accents?: bool|Param, // Default: true - * expansion_factor?: float|Param, // Default: 1.0 - * brackets?: bool|Param, // Default: true - * parse_html?: bool|Param, // Default: false - * localizable_html_attributes?: list, - * }, - * providers?: array, - * locales?: list, - * }>, - * globals?: array, - * domain?: string|Param, - * }>, - * }, - * validation?: bool|array{ // Validation configuration - * enabled?: bool|Param, // Default: false - * cache?: scalar|Param|null, // Deprecated: Setting the "framework.validation.cache.cache" configuration option is deprecated. It will be removed in version 8.0. - * enable_attributes?: bool|Param, // Default: true - * static_method?: list, - * translation_domain?: scalar|Param|null, // Default: "validators" - * email_validation_mode?: "html5"|"html5-allow-no-tld"|"strict"|"loose"|Param, // Default: "html5" - * mapping?: array{ - * paths?: list, - * }, - * not_compromised_password?: bool|array{ - * enabled?: bool|Param, // When disabled, compromised passwords will be accepted as valid. // Default: true - * endpoint?: scalar|Param|null, // API endpoint for the NotCompromisedPassword Validator. // Default: null - * }, - * disable_translation?: bool|Param, // Default: false - * auto_mapping?: array, - * }>, - * }, - * annotations?: bool|array{ - * enabled?: bool|Param, // Default: false - * }, - * serializer?: bool|array{ // Serializer configuration - * enabled?: bool|Param, // Default: false - * enable_attributes?: bool|Param, // Default: true - * name_converter?: scalar|Param|null, - * circular_reference_handler?: scalar|Param|null, - * max_depth_handler?: scalar|Param|null, - * mapping?: array{ - * paths?: list, - * }, - * default_context?: array, - * named_serializers?: array, - * include_built_in_normalizers?: bool|Param, // Whether to include the built-in normalizers // Default: true - * include_built_in_encoders?: bool|Param, // Whether to include the built-in encoders // Default: true - * }>, - * }, - * property_access?: bool|array{ // Property access configuration - * enabled?: bool|Param, // Default: false - * magic_call?: bool|Param, // Default: false - * magic_get?: bool|Param, // Default: true - * magic_set?: bool|Param, // Default: true - * throw_exception_on_invalid_index?: bool|Param, // Default: false - * throw_exception_on_invalid_property_path?: bool|Param, // Default: true - * }, - * type_info?: bool|array{ // Type info configuration - * enabled?: bool|Param, // Default: false - * aliases?: array, - * }, - * property_info?: bool|array{ // Property info configuration - * enabled?: bool|Param, // Default: false - * with_constructor_extractor?: bool|Param, // Registers the constructor extractor. - * }, - * cache?: array{ // Cache configuration - * prefix_seed?: scalar|Param|null, // Used to namespace cache keys when using several apps with the same shared backend. // Default: "_%kernel.project_dir%.%kernel.container_class%" - * app?: scalar|Param|null, // App related cache pools configuration. // Default: "cache.adapter.filesystem" - * system?: scalar|Param|null, // System related cache pools configuration. // Default: "cache.adapter.system" - * directory?: scalar|Param|null, // Default: "%kernel.share_dir%/pools/app" - * default_psr6_provider?: scalar|Param|null, - * default_redis_provider?: scalar|Param|null, // Default: "redis://localhost" - * default_valkey_provider?: scalar|Param|null, // Default: "valkey://localhost" - * default_memcached_provider?: scalar|Param|null, // Default: "memcached://localhost" - * default_doctrine_dbal_provider?: scalar|Param|null, // Default: "database_connection" - * default_pdo_provider?: scalar|Param|null, // Default: null - * pools?: array, - * tags?: scalar|Param|null, // Default: null - * public?: bool|Param, // Default: false - * default_lifetime?: scalar|Param|null, // Default lifetime of the pool. - * provider?: scalar|Param|null, // Overwrite the setting from the default provider for this adapter. - * early_expiration_message_bus?: scalar|Param|null, - * clearer?: scalar|Param|null, - * }>, - * }, - * php_errors?: array{ // PHP errors handling configuration - * log?: mixed, // Use the application logger instead of the PHP logger for logging PHP errors. // Default: true - * throw?: bool|Param, // Throw PHP errors as \ErrorException instances. // Default: true - * }, - * exceptions?: array, - * web_link?: bool|array{ // Web links configuration - * enabled?: bool|Param, // Default: false - * }, - * lock?: bool|string|array{ // Lock configuration - * enabled?: bool|Param, // Default: false - * resources?: array>, - * }, - * semaphore?: bool|string|array{ // Semaphore configuration - * enabled?: bool|Param, // Default: false - * resources?: array, - * }, - * messenger?: bool|array{ // Messenger configuration - * enabled?: bool|Param, // Default: false - * routing?: array, - * }>, - * serializer?: array{ - * default_serializer?: scalar|Param|null, // Service id to use as the default serializer for the transports. // Default: "messenger.transport.native_php_serializer" - * symfony_serializer?: array{ - * format?: scalar|Param|null, // Serialization format for the messenger.transport.symfony_serializer service (which is not the serializer used by default). // Default: "json" - * context?: array, - * }, - * }, - * transports?: array, - * failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null - * retry_strategy?: string|array{ - * service?: scalar|Param|null, // Service id to override the retry strategy entirely. // Default: null - * max_retries?: int|Param, // Default: 3 - * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 - * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: this delay = (delay * (multiple ^ retries)). // Default: 2 - * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 - * jitter?: float|Param, // Randomness to apply to the delay (between 0 and 1). // Default: 0.1 - * }, - * rate_limiter?: scalar|Param|null, // Rate limiter name to use when processing messages. // Default: null - * }>, - * failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null - * stop_worker_on_signals?: list, - * default_bus?: scalar|Param|null, // Default: null - * buses?: array, - * }>, - * }>, - * }, - * scheduler?: bool|array{ // Scheduler configuration - * enabled?: bool|Param, // Default: false - * }, - * disallow_search_engine_index?: bool|Param, // Enabled by default when debug is enabled. // Default: true - * http_client?: bool|array{ // HTTP Client configuration - * enabled?: bool|Param, // Default: false - * max_host_connections?: int|Param, // The maximum number of connections to a single host. - * default_options?: array{ - * headers?: array, - * vars?: array, - * max_redirects?: int|Param, // The maximum number of redirects to follow. - * http_version?: scalar|Param|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version. - * resolve?: array, - * proxy?: scalar|Param|null, // The URL of the proxy to pass requests through or null for automatic detection. - * no_proxy?: scalar|Param|null, // A comma separated list of hosts that do not require a proxy to be reached. - * timeout?: float|Param, // The idle timeout, defaults to the "default_socket_timeout" ini parameter. - * max_duration?: float|Param, // The maximum execution time for the request+response as a whole. - * bindto?: scalar|Param|null, // A network interface name, IP address, a host name or a UNIX socket to bind to. - * verify_peer?: bool|Param, // Indicates if the peer should be verified in a TLS context. - * verify_host?: bool|Param, // Indicates if the host should exist as a certificate common name. - * cafile?: scalar|Param|null, // A certificate authority file. - * capath?: scalar|Param|null, // A directory that contains multiple certificate authority files. - * local_cert?: scalar|Param|null, // A PEM formatted certificate file. - * local_pk?: scalar|Param|null, // A private key file. - * passphrase?: scalar|Param|null, // The passphrase used to encrypt the "local_pk" file. - * ciphers?: scalar|Param|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...) - * peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es). - * sha1?: mixed, - * pin-sha256?: mixed, - * md5?: mixed, - * }, - * crypto_method?: scalar|Param|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants. - * extra?: array, - * rate_limiter?: scalar|Param|null, // Rate limiter name to use for throttling requests. // Default: null - * caching?: bool|array{ // Caching configuration. - * enabled?: bool|Param, // Default: false - * cache_pool?: string|Param, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client" - * shared?: bool|Param, // Indicates whether the cache is shared (public) or private. // Default: true - * max_ttl?: int|Param, // The maximum TTL (in seconds) allowed for cached responses. Null means no cap. // Default: null - * }, - * retry_failed?: bool|array{ - * enabled?: bool|Param, // Default: false - * retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null - * http_codes?: array, - * }>, - * max_retries?: int|Param, // Default: 3 - * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 - * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2 - * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 - * jitter?: float|Param, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1 - * }, - * }, - * mock_response_factory?: scalar|Param|null, // The id of the service that should generate mock responses. It should be either an invokable or an iterable. - * scoped_clients?: array, - * headers?: array, - * max_redirects?: int|Param, // The maximum number of redirects to follow. - * http_version?: scalar|Param|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version. - * resolve?: array, - * proxy?: scalar|Param|null, // The URL of the proxy to pass requests through or null for automatic detection. - * no_proxy?: scalar|Param|null, // A comma separated list of hosts that do not require a proxy to be reached. - * timeout?: float|Param, // The idle timeout, defaults to the "default_socket_timeout" ini parameter. - * max_duration?: float|Param, // The maximum execution time for the request+response as a whole. - * bindto?: scalar|Param|null, // A network interface name, IP address, a host name or a UNIX socket to bind to. - * verify_peer?: bool|Param, // Indicates if the peer should be verified in a TLS context. - * verify_host?: bool|Param, // Indicates if the host should exist as a certificate common name. - * cafile?: scalar|Param|null, // A certificate authority file. - * capath?: scalar|Param|null, // A directory that contains multiple certificate authority files. - * local_cert?: scalar|Param|null, // A PEM formatted certificate file. - * local_pk?: scalar|Param|null, // A private key file. - * passphrase?: scalar|Param|null, // The passphrase used to encrypt the "local_pk" file. - * ciphers?: scalar|Param|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...). - * peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es). - * sha1?: mixed, - * pin-sha256?: mixed, - * md5?: mixed, - * }, - * crypto_method?: scalar|Param|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants. - * extra?: array, - * rate_limiter?: scalar|Param|null, // Rate limiter name to use for throttling requests. // Default: null - * caching?: bool|array{ // Caching configuration. - * enabled?: bool|Param, // Default: false - * cache_pool?: string|Param, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client" - * shared?: bool|Param, // Indicates whether the cache is shared (public) or private. // Default: true - * max_ttl?: int|Param, // The maximum TTL (in seconds) allowed for cached responses. Null means no cap. // Default: null - * }, - * retry_failed?: bool|array{ - * enabled?: bool|Param, // Default: false - * retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null - * http_codes?: array, - * }>, - * max_retries?: int|Param, // Default: 3 - * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 - * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2 - * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 - * jitter?: float|Param, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1 - * }, - * }>, - * }, - * mailer?: bool|array{ // Mailer configuration - * enabled?: bool|Param, // Default: false - * message_bus?: scalar|Param|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null - * dsn?: scalar|Param|null, // Default: null - * transports?: array, - * envelope?: array{ // Mailer Envelope configuration - * sender?: scalar|Param|null, - * recipients?: list, - * allowed_recipients?: list, - * }, - * headers?: array, - * dkim_signer?: bool|array{ // DKIM signer configuration - * enabled?: bool|Param, // Default: false - * key?: scalar|Param|null, // Key content, or path to key (in PEM format with the `file://` prefix) // Default: "" - * domain?: scalar|Param|null, // Default: "" - * select?: scalar|Param|null, // Default: "" - * passphrase?: scalar|Param|null, // The private key passphrase // Default: "" - * options?: array, - * }, - * smime_signer?: bool|array{ // S/MIME signer configuration - * enabled?: bool|Param, // Default: false - * key?: scalar|Param|null, // Path to key (in PEM format) // Default: "" - * certificate?: scalar|Param|null, // Path to certificate (in PEM format without the `file://` prefix) // Default: "" - * passphrase?: scalar|Param|null, // The private key passphrase // Default: null - * extra_certificates?: scalar|Param|null, // Default: null - * sign_options?: int|Param, // Default: null - * }, - * smime_encrypter?: bool|array{ // S/MIME encrypter configuration - * enabled?: bool|Param, // Default: false - * repository?: scalar|Param|null, // S/MIME certificate repository service. This service shall implement the `Symfony\Component\Mailer\EventListener\SmimeCertificateRepositoryInterface`. // Default: "" - * cipher?: int|Param, // A set of algorithms used to encrypt the message // Default: null - * }, - * }, - * secrets?: bool|array{ - * enabled?: bool|Param, // Default: true - * vault_directory?: scalar|Param|null, // Default: "%kernel.project_dir%/config/secrets/%kernel.runtime_environment%" - * local_dotenv_file?: scalar|Param|null, // Default: "%kernel.project_dir%/.env.%kernel.environment%.local" - * decryption_env_var?: scalar|Param|null, // Default: "base64:default::SYMFONY_DECRYPTION_SECRET" - * }, - * notifier?: bool|array{ // Notifier configuration - * enabled?: bool|Param, // Default: false - * message_bus?: scalar|Param|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null - * chatter_transports?: array, - * texter_transports?: array, - * notification_on_failed_messages?: bool|Param, // Default: false - * channel_policy?: array>, - * admin_recipients?: list, - * }, - * rate_limiter?: bool|array{ // Rate limiter configuration - * enabled?: bool|Param, // Default: true - * limiters?: array, - * limit?: int|Param, // The maximum allowed hits in a fixed interval or burst. - * interval?: scalar|Param|null, // Configures the fixed interval if "policy" is set to "fixed_window" or "sliding_window". The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent). - * rate?: array{ // Configures the fill rate if "policy" is set to "token_bucket". - * interval?: scalar|Param|null, // Configures the rate interval. The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent). - * amount?: int|Param, // Amount of tokens to add each interval. // Default: 1 - * }, - * }>, - * }, - * uid?: bool|array{ // Uid configuration - * enabled?: bool|Param, // Default: true - * default_uuid_version?: 7|6|4|1|Param, // Default: 7 - * name_based_uuid_version?: 5|3|Param, // Default: 5 - * name_based_uuid_namespace?: scalar|Param|null, - * time_based_uuid_version?: 7|6|1|Param, // Default: 7 - * time_based_uuid_node?: scalar|Param|null, - * }, - * html_sanitizer?: bool|array{ // HtmlSanitizer configuration - * enabled?: bool|Param, // Default: false - * sanitizers?: array, - * block_elements?: list, - * drop_elements?: list, - * allow_attributes?: array, - * drop_attributes?: array, - * force_attributes?: array>, - * force_https_urls?: bool|Param, // Transforms URLs using the HTTP scheme to use the HTTPS scheme instead. // Default: false - * allowed_link_schemes?: list, - * allowed_link_hosts?: list|null, - * allow_relative_links?: bool|Param, // Allows relative URLs to be used in links href attributes. // Default: false - * allowed_media_schemes?: list, - * allowed_media_hosts?: list|null, - * allow_relative_medias?: bool|Param, // Allows relative URLs to be used in media source attributes (img, audio, video, ...). // Default: false - * with_attribute_sanitizers?: list, - * without_attribute_sanitizers?: list, - * max_input_length?: int|Param, // The maximum length allowed for the sanitized input. // Default: 0 - * }>, - * }, - * webhook?: bool|array{ // Webhook configuration - * enabled?: bool|Param, // Default: false - * message_bus?: scalar|Param|null, // The message bus to use. // Default: "messenger.default_bus" - * routing?: array, - * }, - * remote-event?: bool|array{ // RemoteEvent configuration - * enabled?: bool|Param, // Default: false - * }, - * json_streamer?: bool|array{ // JSON streamer configuration - * enabled?: bool|Param, // Default: false - * }, - * } - * @psalm-type TwigConfig = array{ - * form_themes?: list, - * globals?: array, - * autoescape_service?: scalar|Param|null, // Default: null - * autoescape_service_method?: scalar|Param|null, // Default: null - * base_template_class?: scalar|Param|null, // Deprecated: The child node "base_template_class" at path "twig.base_template_class" is deprecated. - * cache?: scalar|Param|null, // Default: true - * charset?: scalar|Param|null, // Default: "%kernel.charset%" - * debug?: bool|Param, // Default: "%kernel.debug%" - * strict_variables?: bool|Param, // Default: "%kernel.debug%" - * auto_reload?: scalar|Param|null, - * optimizations?: int|Param, - * default_path?: scalar|Param|null, // The default path used to load templates. // Default: "%kernel.project_dir%/templates" - * file_name_pattern?: list, - * paths?: array, - * date?: array{ // The default format options used by the date filter. - * format?: scalar|Param|null, // Default: "F j, Y H:i" - * interval_format?: scalar|Param|null, // Default: "%d days" - * timezone?: scalar|Param|null, // The timezone used when formatting dates, when set to null, the timezone returned by date_default_timezone_get() is used. // Default: null - * }, - * number_format?: array{ // The default format options for the number_format filter. - * decimals?: int|Param, // Default: 0 - * decimal_point?: scalar|Param|null, // Default: "." - * thousands_separator?: scalar|Param|null, // Default: "," - * }, - * mailer?: array{ - * html_to_text_converter?: scalar|Param|null, // A service implementing the "Symfony\Component\Mime\HtmlToTextConverter\HtmlToTextConverterInterface". // Default: null - * }, - * } - * @psalm-type ConfigType = array{ - * imports?: ImportsConfig, - * parameters?: ParametersConfig, - * services?: ServicesConfig, - * framework?: FrameworkConfig, - * twig?: TwigConfig, - * "when@dev"?: array{ - * imports?: ImportsConfig, - * parameters?: ParametersConfig, - * services?: ServicesConfig, - * framework?: FrameworkConfig, - * twig?: TwigConfig, - * }, - * "when@prod"?: array{ - * imports?: ImportsConfig, - * parameters?: ParametersConfig, - * services?: ServicesConfig, - * framework?: FrameworkConfig, - * twig?: TwigConfig, - * }, - * ..., - * }> - * } - */ -final class App -{ - /** - * @param ConfigType $config - * - * @psalm-return ConfigType - */ - public static function config(array $config): array - { - /** @var ConfigType $config */ - $config = AppReference::config($config); - - return $config; - } -} - -namespace Symfony\Component\Routing\Loader\Configurator; - -/** - * This class provides array-shapes for configuring the routes of an application. - * - * Example: - * - * ```php - * // config/routes.php - * namespace Symfony\Component\Routing\Loader\Configurator; - * - * return Routes::config([ - * 'controllers' => [ - * 'resource' => 'routing.controllers', - * ], - * ]); - * ``` - * - * @psalm-type RouteConfig = array{ - * path: string|array, - * controller?: string, - * methods?: string|list, - * requirements?: array, - * defaults?: array, - * options?: array, - * host?: string|array, - * schemes?: string|list, - * condition?: string, - * locale?: string, - * format?: string, - * utf8?: bool, - * stateless?: bool, - * } - * @psalm-type ImportConfig = array{ - * resource: string, - * type?: string, - * exclude?: string|list, - * prefix?: string|array, - * name_prefix?: string, - * trailing_slash_on_root?: bool, - * controller?: string, - * methods?: string|list, - * requirements?: array, - * defaults?: array, - * options?: array, - * host?: string|array, - * schemes?: string|list, - * condition?: string, - * locale?: string, - * format?: string, - * utf8?: bool, - * stateless?: bool, - * } - * @psalm-type AliasConfig = array{ - * alias: string, - * deprecated?: array{package:string, version:string, message?:string}, - * } - * @psalm-type RoutesConfig = array{ - * "when@dev"?: array, - * "when@prod"?: array, - * ... - * } - */ -final class Routes -{ - /** - * @param RoutesConfig $config - * - * @psalm-return RoutesConfig - */ - public static function config(array $config): array - { - return $config; - } -} diff --git a/public/index.php b/public/index.php index fc0c790..97e228d 100644 --- a/public/index.php +++ b/public/index.php @@ -1,4 +1,5 @@ setName('app:generate-backup-codes'); $this->setDescription('Generate single‑use backup codes') ->addArgument('count', InputArgument::OPTIONAL, 'Number of codes to generate', 10); } /** @throws InvalidArgumentException */ - protected function execute(InputInterface $input, OutputInterface $output): int { + protected function execute(InputInterface $input, OutputInterface $output): int + { /* since Kernel::terminate() does not get called, we must boot and persist explicitly */ $this->persistCache->boot(); $count = (int) $input->getArgument('count'); diff --git a/src/ConfigBag.php b/src/ConfigBag.php index 9493e32..afb630c 100644 --- a/src/ConfigBag.php +++ b/src/ConfigBag.php @@ -1,4 +1,5 @@ tooManyTitle = $tooManyTitle; } - public function clock(): ClockInterface { + public function clock(): ClockInterface + { return $this->clock; } - public function cookieTtl(): int { + public function cookieTtl(): int + { return $this->cookieTtl; } - public function totpUri(): string { + public function totpUri(): string + { return $this->totpUri; } - public function ipTtl(): ?int { + public function ipTtl(): ?int + { return $this->ipTtl; } - public function teapot(): bool { + public function teapot(): bool + { return $this->teapot; } - public function errorMessage(): string { + public function errorMessage(): string + { return $this->errorMessage; } - public function teapotTitle(): string { + public function teapotTitle(): string + { return $this->teapotTitle; } - public function tooManyTitle(): string { + public function tooManyTitle(): string + { return $this->tooManyTitle; } } diff --git a/src/Data/Payload.php b/src/Data/Payload.php index 77ca1a3..ede6f7f 100644 --- a/src/Data/Payload.php +++ b/src/Data/Payload.php @@ -1,4 +1,5 @@ has('username') && $input->has('nonce') && $input->has('totp')) { return Payload::create((object)[ @@ -42,7 +46,8 @@ final class Payload { return null; } - public static function create(object $data): ?Payload { + public static function create(object $data): ?Payload + { /* if missing required fields id, nonce, or token */ if (strlen(trim($data->id ?? '')) < 1 || strlen(trim($data->nonce ?? '')) < 1 || @@ -63,11 +68,13 @@ final class Payload { return Payload::constrict($payload); } - public function toString(): string { + public function toString(): string + { return json_encode($this); } - private static function constrict(Payload $payload): Payload { + private static function constrict(Payload $payload): Payload + { /* When scope is None, json will be considered false. */ if ($payload->scope === Scope::None) { $payload->json = false; diff --git a/src/Enum/Scope.php b/src/Enum/Scope.php index 83f085a..d4d0502 100644 --- a/src/Enum/Scope.php +++ b/src/Enum/Scope.php @@ -1,10 +1,12 @@ persistCache = $this->container->get(PersistCache::class); @@ -23,7 +26,8 @@ class Kernel extends BaseKernel { } /** @throws InvalidArgumentException */ - public function terminate(Request $request, Response $response): void { + public function terminate(Request $request, Response $response): void + { $this->persistCache->persist(); parent::terminate($request, $response); diff --git a/src/Listener/AcceptListener.php b/src/Listener/AcceptListener.php index 1e424f4..e9ec54a 100644 --- a/src/Listener/AcceptListener.php +++ b/src/Listener/AcceptListener.php @@ -1,4 +1,5 @@ domainManager->authBase() ?$this->authCookieName() : $this->cookieName(); + $cookieName = $this->domainManager->authBase() ? $this->authCookieName() : $this->cookieName(); if ($event->getRequest()->cookies->has($cookieName)) { $cookie = $event->getRequest()->cookies->get($cookieName); $cookieKey = $this->makeCacheKey("cookie_$cookie"); diff --git a/src/Listener/AllowListener.php b/src/Listener/AllowListener.php index 97d55f6..2785018 100644 --- a/src/Listener/AllowListener.php +++ b/src/Listener/AllowListener.php @@ -1,4 +1,5 @@ config->ipTtl() > 0) { $ipKey = $this->makeCacheKey("ip_{$event->getRequest()->getClientIp()}"); if ($this->sessionCache->hasItem($ipKey)) { diff --git a/src/Listener/InterceptListener.php b/src/Listener/InterceptListener.php index 7fdc936..1aa632c 100644 --- a/src/Listener/InterceptListener.php +++ b/src/Listener/InterceptListener.php @@ -1,4 +1,5 @@ $event->getRequest()->getUri()]); - $event->setResponse(new Response('', Response::HTTP_SEE_OTHER, + $event->setResponse(new Response( + '', + Response::HTTP_SEE_OTHER, ['Location' => "https://{$this->domainManager->getAuthSubdomain()}/?$query"] )); } else { @@ -52,13 +58,16 @@ final readonly class InterceptListener { $hasCookie = (bool) $event->getRequest()->cookies->get( $this->domainManager->authBase() ? $this->authCookieName() : $this->cookieName() ); - $event->setResponse($this->pruneInvalidCookie(new Response($content, - Response::HTTP_UNAUTHORIZED, ['Content-Type' => 'text/html'] + $event->setResponse($this->pruneInvalidCookie(new Response( + $content, + Response::HTTP_UNAUTHORIZED, + ['Content-Type' => 'text/html'] ), $hasCookie, $event->getRequest()->getHost())); } } - private function pruneInvalidCookie(Response $response, bool $hasCookie, string $host): Response { + private function pruneInvalidCookie(Response $response, bool $hasCookie, string $host): Response + { if ($hasCookie) { /* input here must match LoginListener::setCookie() */ $response->headers->clearCookie( diff --git a/src/Listener/LoginListener.php b/src/Listener/LoginListener.php index 1c8ca69..4e9d4a8 100644 --- a/src/Listener/LoginListener.php +++ b/src/Listener/LoginListener.php @@ -1,4 +1,5 @@ rateLimiter = $rateLimiter; } /** @throws InvalidArgumentException|LoaderError|RuntimeError|SyntaxError */ #[AsEventListener(priority: 66)] - public function onKernelRequest(RequestEvent $event): void { + public function onKernelRequest(RequestEvent $event): void + { $payload = null; $response = null; @@ -51,7 +54,7 @@ final readonly class LoginListener { /* if request contains our "X-Preauth" header */ $data = $event->getRequest()->headers->get($this->headerName()); $payload = Payload::decode($data); - } else if ($event->getRequest()->isMethod(Request::METHOD_POST) && + } elseif ($event->getRequest()->isMethod(Request::METHOD_POST) && $this->domainManager->getAuthSubdomain() === $event->getRequest()->getHost() ) { /* if request is a POST to the auth-subdomain */ @@ -76,18 +79,23 @@ final readonly class LoginListener { $limitReached = $this->logFailure($event->getRequest()); $this->logger->debug("logging failure for: {$event->getRequest()->getClientIp()}"); - $event->setResponse($this->makeFailedResponse($limitReached, $payload->json ?? true, - $event->getRequest()->getHost(), $this->makeCacheKey($payload ? $payload->id : '') + $event->setResponse($this->makeFailedResponse( + $limitReached, + $payload->json ?? true, + $event->getRequest()->getHost(), + $this->makeCacheKey($payload ? $payload->id : '') )); } - private function logFailure(Request $request): bool { + private function logFailure(Request $request): bool + { $limiter = $this->rateLimiter->create($request->getClientIp()); return ($limiter->consume(1)->getRemainingTokens() < 1); } /** @throws InvalidArgumentException|RuntimeError|SyntaxError|LoaderError */ - private function makeFailedResponse(bool $limited, bool $json, string $host, string $username): Response { + private function makeFailedResponse(bool $limited, bool $json, string $host, string $username): Response + { if ($limited) { $status = $this->config->teapot() ? Response::HTTP_I_AM_A_TEAPOT : Response::HTTP_TOO_MANY_REQUESTS; diff --git a/src/Listener/RejectListener.php b/src/Listener/RejectListener.php index 643f09e..fb7b704 100644 --- a/src/Listener/RejectListener.php +++ b/src/Listener/RejectListener.php @@ -1,4 +1,5 @@ rateLimiter = $rateLimiter; @@ -32,13 +34,16 @@ final readonly class RejectListener { /** @throws SyntaxError|RuntimeError|LoaderError */ #[AsEventListener(priority: 77)] - public function onKernelRequest(RequestEvent $event): void { + public function onKernelRequest(RequestEvent $event): void + { /* check if they have made too many failed login attempts */ $limiter = $this->rateLimiter->create($event->getRequest()->getClientIp()); if ($limiter->consume(0)->getRemainingTokens() < 1) { $this->logger->debug("already blocked: {$event->getRequest()->getClientIp()}"); $html = $this->twig->render('error.html.twig'); - $event->setResponse(new Response($html, ($this->config->teapot() + $event->setResponse(new Response( + $html, + ($this->config->teapot() ? Response::HTTP_I_AM_A_TEAPOT : Response::HTTP_TOO_MANY_REQUESTS), ['Content-Type' => 'text/html'] )); diff --git a/src/MonitorCacheKeys.php b/src/MonitorCacheKeys.php index a291eca..955c817 100644 --- a/src/MonitorCacheKeys.php +++ b/src/MonitorCacheKeys.php @@ -1,4 +1,5 @@ cache = $cache; $items = $cache->getItems([self::KEY_LIST, self::CHANGE_LIST]); foreach ($items as $item) { - if ( ! $item->isHit()) { + if (! $item->isHit()) { $this->initialize(); break; } @@ -31,7 +34,8 @@ final readonly class MonitorCacheKeys implements CacheItemPoolInterface { } /** @throws InvalidArgumentException */ - private function initialize(): void { + private function initialize(): void + { $keyList = $this->cache->getItem(self::KEY_LIST); $changeList = $this->cache->getItem(self::CHANGE_LIST); $keyList->set([]); @@ -42,42 +46,49 @@ final readonly class MonitorCacheKeys implements CacheItemPoolInterface { } /** @throws InvalidArgumentException */ - public function getKeys(): array { + public function getKeys(): array + { $keyList = $this->cache->getItem(self::KEY_LIST); return array_keys($keyList->get() ?? []); } /** @throws InvalidArgumentException */ - public function getChanges(): array { + public function getChanges(): array + { $changeList = $this->cache->getItem(self::CHANGE_LIST); return $changeList->get() ?? []; } /** @throws InvalidArgumentException */ - public function markClean(): void { + public function markClean(): void + { $changeList = $this->cache->getItem(self::CHANGE_LIST); $changeList->set([]); $this->cache->save($changeList); } - public function getItem(string $key): CacheItemInterface { + public function getItem(string $key): CacheItemInterface + { return $this->cache->getItem($key); } /** @return CacheItemInterface[] * @throws InvalidArgumentException */ - public function getItems(array $keys = []): iterable { + public function getItems(array $keys = []): iterable + { return $this->cache->getItems($keys); } - public function hasItem(string $key): bool { + public function hasItem(string $key): bool + { return $this->cache->hasItem($key); } /** @throws InvalidArgumentException */ - public function clear(): bool { + public function clear(): bool + { /* only bother clearing the pool if it is not empty */ - if ( ! empty($this->getKeys())) { + if (! empty($this->getKeys())) { $response = $this->cache->clear(); $this->initialize(); @@ -86,7 +97,8 @@ final readonly class MonitorCacheKeys implements CacheItemPoolInterface { return true; } - public function deleteItem(string $key): bool { + public function deleteItem(string $key): bool + { $this->isValid($key); $keyList = $this->cache->getItem(self::KEY_LIST); $keyValues = $keyList->get(); @@ -101,7 +113,8 @@ final readonly class MonitorCacheKeys implements CacheItemPoolInterface { return $this->cache->deleteItem($key); } - public function deleteItems(array $keys): bool { + public function deleteItems(array $keys): bool + { $this->allValid($keys); $keyList = $this->cache->getItem(self::KEY_LIST); $keyValues = $keyList->get(); @@ -119,23 +132,27 @@ final readonly class MonitorCacheKeys implements CacheItemPoolInterface { } /** @throws InvalidArgumentException */ - public function save(CacheItemInterface $item): bool { + public function save(CacheItemInterface $item): bool + { $this->update($item); return $this->cache->save($item); } /** @throws InvalidArgumentException */ - public function saveDeferred(CacheItemInterface $item): bool { + public function saveDeferred(CacheItemInterface $item): bool + { $this->update($item); return $this->cache->saveDeferred($item); } - public function commit(): bool { + public function commit(): bool + { return $this->cache->commit(); } /** @throws InvalidArgumentException|OutOfBoundsException */ - private function update(CacheItemInterface $item): void { + private function update(CacheItemInterface $item): void + { $this->isValid($item->getKey()); $keyList = $this->cache->getItem(self::KEY_LIST); $keyValues = $keyList->get(); @@ -147,7 +164,8 @@ final readonly class MonitorCacheKeys implements CacheItemPoolInterface { } /** @throws OutOfBoundsException */ - private function isValid(string $key): void { + private function isValid(string $key): void + { if ($key === self::KEY_LIST || $key === self::CHANGE_LIST) { throw new OutOfBoundsException( 'Can not modify the private key or change lists' @@ -156,7 +174,8 @@ final readonly class MonitorCacheKeys implements CacheItemPoolInterface { } /** @throws OutOfBoundsException */ - private function allValid(array $keys): void { + private function allValid(array $keys): void + { if (in_array(self::KEY_LIST, $keys, true) || in_array(self::CHANGE_LIST, $keys, true) ) { @@ -167,7 +186,8 @@ final readonly class MonitorCacheKeys implements CacheItemPoolInterface { } /** @throws InvalidArgumentException */ - private function logChange(string $key, int $code = MonitorCacheKeys::UPDATED): void { + private function logChange(string $key, int $code = MonitorCacheKeys::UPDATED): void + { $changeList = $this->cache->getItem(self::CHANGE_LIST); $changeValues = $changeList->get(); $changeValues[$key] = $code; diff --git a/src/PersistCache.php b/src/PersistCache.php index 81c780c..d1fcfa4 100644 --- a/src/PersistCache.php +++ b/src/PersistCache.php @@ -1,4 +1,5 @@ boot() */ #[Autoconfigure(public: true)] -final readonly class PersistCache { +final readonly class PersistCache +{ private MonitorCacheKeys $sessionCache; private MonitorCacheKeys $sessionStorage; @@ -23,7 +25,8 @@ final readonly class PersistCache { } /** @throws InvalidArgumentException */ - public function boot(): void { + public function boot(): void + { /* the caches are considered warm as soon as they are not empty */ if (empty($this->sessionCache->getKeys())) { $items = $this->sessionStorage->getItems($this->sessionStorage->getKeys()); @@ -36,7 +39,8 @@ final readonly class PersistCache { } /** @throws InvalidArgumentException */ - public function persist(): void { + public function persist(): void + { /* we only need to persist the changes made to the cache (if any) */ $changes = $this->sessionCache->getChanges(); if ($changes) { diff --git a/src/Service/BackupCodeInterface.php b/src/Service/BackupCodeInterface.php index 9c13709..ac60e91 100644 --- a/src/Service/BackupCodeInterface.php +++ b/src/Service/BackupCodeInterface.php @@ -2,13 +2,13 @@ namespace App\Service; - use Exception; use Psr\Cache\InvalidArgumentException; /** backup-codes are case‑insensitive alphanumeric strings * they are single-use and marked as used after successful authentication */ -interface BackupCodeInterface { +interface BackupCodeInterface +{ /** generate a set of backup-codes and return them * @param int $count Number of codes to generate * @return string[] Generated backup codes diff --git a/src/Service/BackupCodeManager.php b/src/Service/BackupCodeManager.php index e7bf67e..e66e073 100644 --- a/src/Service/BackupCodeManager.php +++ b/src/Service/BackupCodeManager.php @@ -1,4 +1,5 @@ sessionCache = new MonitorCacheKeys($sessionCache); } @@ -34,7 +37,8 @@ final readonly class BackupCodeManager implements BackupCodeInterface { * @param int $count Number of codes to generate * @return string[] Generated backup codes * @throws InvalidArgumentException|Exception */ - public function generate(int $count = self::DEFAULT_COUNT): array { + public function generate(int $count = self::DEFAULT_COUNT): array + { $length = min($this->getTotp()->getDigits() + 2, self::MAX_LENGTH); $codes = []; for ($i = 0; $i < $count; $i++) { @@ -49,7 +53,8 @@ final readonly class BackupCodeManager implements BackupCodeInterface { } /** @throws InvalidArgumentException */ - public function expire(): void { + public function expire(): void + { $itemsToRemove = []; foreach ($this->sessionCache->getKeys() as $key) { if (str_starts_with($key, 'backup_')) { @@ -65,7 +70,8 @@ final readonly class BackupCodeManager implements BackupCodeInterface { * @param string $code Code supplied by the client * @return bool true if the code is valid and unused * @throws InvalidArgumentException */ - public function verifyAndConsume(string $code): bool { + public function verifyAndConsume(string $code): bool + { /* remove unallowed characters, since backup codes are case-insensitive alphanumeric */ $backupKey = 'backup_' . preg_replace('/[^a-z0-9]+/', '', strtolower($code)); $backupItem = $this->sessionCache->getItem($this->makeCacheKey($backupKey)); @@ -77,7 +83,8 @@ final readonly class BackupCodeManager implements BackupCodeInterface { /* per PSR6, if no expiration is set, implementation may set a default, * we want this to keep forever, so a few hundred years should do it */ $backupItem->expiresAt(DateTimeImmutable::createFromFormat( - 'Y-m-d', '2999-12-31' + 'Y-m-d', + '2999-12-31' )); $this->sessionCache->save($backupItem); @@ -87,7 +94,8 @@ final readonly class BackupCodeManager implements BackupCodeInterface { } /** @throws InvalidArgumentException */ - private function saveCodes(array $codes): void { + private function saveCodes(array $codes): void + { foreach ($codes as $code) { $backupItem = $this->sessionCache->getItem($this->makeCacheKey(strtolower("backup_$code"))); /* mark backup code as ready */ @@ -95,7 +103,8 @@ final readonly class BackupCodeManager implements BackupCodeInterface { /* per PSR6, if no expiration is set, implementation may set a default, * we want this to keep forever, so a few hundred years should do it */ $backupItem->expiresAt(DateTimeImmutable::createFromFormat( - 'Y-m-d', '2999-12-31' + 'Y-m-d', + '2999-12-31' )); $this->sessionCache->saveDeferred($backupItem); } diff --git a/src/Service/DomainInterface.php b/src/Service/DomainInterface.php index 29da47a..4b17f15 100644 --- a/src/Service/DomainInterface.php +++ b/src/Service/DomainInterface.php @@ -2,7 +2,8 @@ namespace App\Service; -interface DomainInterface { +interface DomainInterface +{ /** IE: "auth.example.com" or null if not using a separate subdomain * @return ?string Returns auth subdomain if configured, otherwise null */ public function getAuthSubdomain(): ?string; diff --git a/src/Service/DomainManager.php b/src/Service/DomainManager.php index d225382..0e8c288 100644 --- a/src/Service/DomainManager.php +++ b/src/Service/DomainManager.php @@ -1,11 +1,13 @@ ['com','net','off','org'], @@ -38,7 +40,8 @@ final readonly class DomainManager implements DomainInterface { /** IE: "auth.example.com" or null if not using a separate subdomain * @return ?string Returns auth subdomain if configured, otherwise null */ - public function getAuthSubdomain(): ?string { + public function getAuthSubdomain(): ?string + { if ($this->authBase()) { return $this->authSubdomain; } @@ -48,7 +51,8 @@ final readonly class DomainManager implements DomainInterface { /** check if given url is an acceptable url for redirection * @param string $url Where we are thinking of sending the user * @return bool Returns true if it is acceptable to send the user there */ - public function validReturn(string $url): bool { + public function validReturn(string $url): bool + { /* ensure url is valid and, when using an auth subdomain, * that the url host matches the base domain */ if (!filter_var($url, FILTER_VALIDATE_URL)) { @@ -70,7 +74,8 @@ final readonly class DomainManager implements DomainInterface { /** check if host-base matches auth-base * @param string $host * @return bool returns true if and only if host matches base domain of auth */ - public function matchesAuth(string $host): bool { + public function matchesAuth(string $host): bool + { $hostBase = $this->baseDomain($host); $authBase = $this->baseDomain($this->authSubdomain); return $this->subdomainRedirect && $this->authSubdomain && @@ -79,7 +84,8 @@ final readonly class DomainManager implements DomainInterface { /** IE: "example.com" if central auth is something like "auth.example.com" * @return string|null returns base domain if we are doing central auth */ - public function authBase(): ?string { + public function authBase(): ?string + { if ($this->subdomainRedirect && $this->authSubdomain && $this->baseDomain($this->authSubdomain)) { return $this->baseDomain($this->authSubdomain); } @@ -91,7 +97,8 @@ final readonly class DomainManager implements DomainInterface { * things like "localhost" and "8.8.8.8" will return null * @param string $host ip, localhost, or domain with zero or more subdomains * @return ?string returns null if host is ip or localhost otherwise domain with all subdomains removed */ - private function baseDomain(string $host): ?string { + private function baseDomain(string $host): ?string + { /* if host is an ip address (or localhost), leave it as is */ if (filter_var($host, FILTER_VALIDATE_IP) || $host === 'localhost') { return null; @@ -106,12 +113,13 @@ final readonly class DomainManager implements DomainInterface { /** IE: ["www", "example", "com"] or ["www", "example", "co", "uk"] * @param string[] $parts pieces of a domain split by "." dot * @return int typically 2 but sometimes 3 */ - private function baseLength(array $parts): int { + private function baseLength(array $parts): int + { $length = count($parts); $baseLength = min(2, $length); /* check if host should retain 3 parts, due to TLD */ - if (count($parts) > 2 && isset(self::TLD[$parts[$length-1]]) && - in_array($parts[$length-2], self::TLD[$parts[$length-1]], true) + if (count($parts) > 2 && isset(self::TLD[$parts[$length - 1]]) && + in_array($parts[$length - 2], self::TLD[$parts[$length - 1]], true) ) { $baseLength = min(3, $length); } diff --git a/src/Service/LoginInterface.php b/src/Service/LoginInterface.php index 98b58cb..84f3e44 100644 --- a/src/Service/LoginInterface.php +++ b/src/Service/LoginInterface.php @@ -7,7 +7,8 @@ use Psr\Cache\InvalidArgumentException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -interface LoginInterface { +interface LoginInterface +{ /** @throws InvalidArgumentException */ public function checkToken(Payload $payload, Request $request): ?Response; } diff --git a/src/Service/LoginManager.php b/src/Service/LoginManager.php index d0079aa..2937de2 100644 --- a/src/Service/LoginManager.php +++ b/src/Service/LoginManager.php @@ -1,4 +1,5 @@ scope === Scope::Ip && ! $this->config->ipTtl()) { /* requested to grant ip access, but that is not enabled */ @@ -69,7 +72,7 @@ final readonly class LoginManager implements LoginInterface { /* grant access based on the requested scope */ if ($payload->scope === Scope::Cookie) { $response->headers->setCookie($this->setCookie($cleanId, $request->getHost())); - } else if ($payload->scope === Scope::Ip) { + } elseif ($payload->scope === Scope::Ip) { $this->setIp($cleanId, $request->getClientIp()); } @@ -104,7 +107,8 @@ final readonly class LoginManager implements LoginInterface { } /** @throws InvalidArgumentException */ - private function setCookie(string $id, string $host): Cookie { + private function setCookie(string $id, string $host): Cookie + { /* successful auth with token, store session and set the cookie */ $ulid = new Ulid(); $sessionCookie = $this->sessionCache->getItem( @@ -136,7 +140,8 @@ final readonly class LoginManager implements LoginInterface { } /** @throws InvalidArgumentException */ - private function setIp(string $id, string $ip): void { + private function setIp(string $id, string $ip): void + { /* successful auth with token, requested scope of ip (and ip access enabled) */ $ipKey = $this->makeCacheKey("ip_$ip"); diff --git a/src/Trait/CookieNameTrait.php b/src/Trait/CookieNameTrait.php index bdddde4..550b472 100644 --- a/src/Trait/CookieNameTrait.php +++ b/src/Trait/CookieNameTrait.php @@ -1,22 +1,27 @@ config = $config; } - protected function getTotp(): TOTPInterface { + protected function getTotp(): TOTPInterface + { $otp = Factory::loadFromProvisioningUri( - $this->config->totpUri(), $this->config->clock() + $this->config->totpUri(), + $this->config->clock() ); if ($otp instanceof TOTPInterface) { return $otp; diff --git a/src/Trait/HasLoggerTrait.php b/src/Trait/HasLoggerTrait.php index 57344e4..c3c4ef5 100644 --- a/src/Trait/HasLoggerTrait.php +++ b/src/Trait/HasLoggerTrait.php @@ -1,4 +1,5 @@ logger = $logger; } } diff --git a/src/Trait/MakeNonceTrait.php b/src/Trait/MakeNonceTrait.php index 5f2066f..c472e8b 100644 --- a/src/Trait/MakeNonceTrait.php +++ b/src/Trait/MakeNonceTrait.php @@ -1,4 +1,5 @@ nonceCache = $nonceCache; } /** @throws InvalidArgumentException|Exception */ - protected function makeNonce(int $retries = 3): string { + protected function makeNonce(int $retries = 3): string + { /* convert raw binary into base64url */ $nonce = rtrim(strtr(base64_encode(random_bytes( static::NONCE_LENGTH diff --git a/src/Trait/StringTrait.php b/src/Trait/StringTrait.php index e7f6f6c..72e32bb 100644 --- a/src/Trait/StringTrait.php +++ b/src/Trait/StringTrait.php @@ -1,13 +1,16 @@ appPool->hasItem('totp')) { $totp = $this->appPool->getItem('totp')->get(); @@ -31,7 +35,8 @@ final readonly class Utilities { } /** @throws InvalidArgumentException */ - private function makeTotp(): string { + private function makeTotp(): string + { /* we have not stored a totp into the app cache yet */ $totpObj = TOTP::generate($this->clock); $totpObj->setLabel('Preauth-TOTP'); @@ -41,21 +46,25 @@ final readonly class Utilities { /* per PSR6, if no expiration is set, implementation may set a default, * we want this to keep forever, so a few hundred years should do it */ $totpItem->expiresAt(DateTimeImmutable::createFromFormat( - 'Y-m-d', '2999-12-31' + 'Y-m-d', + '2999-12-31' )); $this->appPool->save($totpItem); return $totp; } - private function showTotp(string $totp): void { + private function showTotp(string $totp): void + { $writer = new Writer(new PlainTextRenderer()); file_put_contents( - 'php://stderr', <<writeString($totp)} $totp loading TOTP, because the env is not set, please copy above into TOTP_URI - RAW, FILE_APPEND + RAW, + FILE_APPEND ); } } diff --git a/symfony.lock b/symfony.lock index 90002fc..a53ef73 100644 --- a/symfony.lock +++ b/symfony.lock @@ -1,4 +1,16 @@ { + "friendsofphp/php-cs-fixer": { + "version": "3.95", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.39", + "ref": "97aaf9026490db73b86c23d49e5774bc89d2b232" + }, + "files": [ + ".php-cs-fixer.dist.php" + ] + }, "phpunit/phpunit": { "version": "13.2", "recipe": { diff --git a/tests/Functional/AuthenticationFlowTest.php b/tests/Functional/AuthenticationFlowTest.php index 43b445c..82a1c7d 100644 --- a/tests/Functional/AuthenticationFlowTest.php +++ b/tests/Functional/AuthenticationFlowTest.php @@ -1,4 +1,5 @@ LoginListener -> AllowListener -> * AcceptListener -> InterceptListener and the services they orchestrate. */ -final class AuthenticationFlowTest extends WebTestCase { - +final class AuthenticationFlowTest extends WebTestCase +{ private const string TOTP_SECRET = 'JBSWY3DPEHPK3PXP'; private const string COOKIE_NAME = '__Host-Http-Preauth'; @@ -32,13 +33,15 @@ final class AuthenticationFlowTest extends WebTestCase { return $client; } - private function validTotpCode(): string { + private function validTotpCode(): string + { // the app uses the real system clock, so generate the code for now() return TOTP::createFromSecret(self::TOTP_SECRET)->now(); } /** base64url-encode a payload, matching the client-side JS / X-Preauth header. */ - private function encodePayload(array $data): string { + private function encodePayload(array $data): string + { $json = json_encode($data, JSON_THROW_ON_ERROR); return rtrim(strtr(base64_encode($json), '+/', '-_'), '='); } @@ -59,7 +62,8 @@ final class AuthenticationFlowTest extends WebTestCase { /* ── unauthenticated access ──────────────────────────────────────── */ - public function testUnauthenticatedRequestShowsLoginPage(): void { + public function testUnauthenticatedRequestShowsLoginPage(): void + { $client = static::createClient(); $client->request('GET', '/'); @@ -71,7 +75,8 @@ final class AuthenticationFlowTest extends WebTestCase { self::assertSelectorExists('input[name="totp"]'); } - public function testLoginPageContainsGeneratedNonce(): void { + public function testLoginPageContainsGeneratedNonce(): void + { $client = static::createClient(); $crawler = $client->request('GET', '/'); @@ -81,7 +86,8 @@ final class AuthenticationFlowTest extends WebTestCase { self::assertMatchesRegularExpression('/^[A-Za-z0-9_-]+$/', $nonceInput); } - public function testLoginFormDoesNotUsePostMethodWithoutAuthSubdomain(): void { + public function testLoginFormDoesNotUsePostMethodWithoutAuthSubdomain(): void + { $client = static::createClient(); $crawler = $client->request('GET', '/'); @@ -93,7 +99,8 @@ final class AuthenticationFlowTest extends WebTestCase { /* ── successful TOTP login ────────────────────────────────────────── */ - public function testSuccessfulTotpLoginViaHeaderSetsCookieAndRedirects(): void { + public function testSuccessfulTotpLoginViaHeaderSetsCookieAndRedirects(): void + { $client = static::createClient(); // first, grab a valid nonce from the login page @@ -125,7 +132,8 @@ final class AuthenticationFlowTest extends WebTestCase { self::assertTrue($hasPreauthCookie, 'Expected a preauth cookie to be set after login'); } - public function testSuccessfulLoginReturnsJsonWhenJsonRequested(): void { + public function testSuccessfulLoginReturnsJsonWhenJsonRequested(): void + { $client = static::createClient(); $crawler = $client->request('GET', '/'); @@ -147,7 +155,8 @@ final class AuthenticationFlowTest extends WebTestCase { self::assertSame('Login successful', $body['message']); } - public function testSuccessfulLoginReturnsHtmlWhenJsonFalse(): void { + public function testSuccessfulLoginReturnsHtmlWhenJsonFalse(): void + { $client = static::createClient(); $crawler = $client->request('GET', '/'); @@ -167,7 +176,8 @@ final class AuthenticationFlowTest extends WebTestCase { self::assertStringStartsWith('text/html', $response->headers->get('Content-Type')); } - public function testAuthenticatedCookieAccessAfterLogin(): void { + public function testAuthenticatedCookieAccessAfterLogin(): void + { $client = static::createClient(); // login @@ -204,7 +214,8 @@ final class AuthenticationFlowTest extends WebTestCase { self::assertSame('dave', $response->headers->get('Remote-User')); } - public function testScopeNoneReturnsPlainTextWithoutRedirect(): void { + public function testScopeNoneReturnsPlainTextWithoutRedirect(): void + { $client = static::createClient(); $crawler = $client->request('GET', '/'); @@ -229,7 +240,8 @@ final class AuthenticationFlowTest extends WebTestCase { /* ── failed login ─────────────────────────────────────────────────── */ - public function testFailedLoginReturnsUnauthorizedJsonWithError(): void { + public function testFailedLoginReturnsUnauthorizedJsonWithError(): void + { $client = static::createClient(); $crawler = $client->request('GET', '/'); @@ -254,7 +266,8 @@ final class AuthenticationFlowTest extends WebTestCase { self::assertNotEmpty($body['nonce']); } - public function testFailedLoginReturnsHtmlWhenJsonFalse(): void { + public function testFailedLoginReturnsHtmlWhenJsonFalse(): void + { $client = static::createClient(); $crawler = $client->request('GET', '/'); @@ -275,7 +288,8 @@ final class AuthenticationFlowTest extends WebTestCase { self::assertSelectorExists('form#preauth-form'); } - public function testFailedLoginWithSpentNonceIsRejected(): void { + public function testFailedLoginWithSpentNonceIsRejected(): void + { $client = static::createClient(); $crawler = $client->request('GET', '/'); @@ -309,7 +323,8 @@ final class AuthenticationFlowTest extends WebTestCase { self::assertSame(401, $client->getResponse()->getStatusCode()); } - public function testFailedLoginWithInvalidNonceIsRejected(): void { + public function testFailedLoginWithInvalidNonceIsRejected(): void + { $client = static::createClient(); // skip fetching a real nonce; use one that was never stored @@ -327,7 +342,8 @@ final class AuthenticationFlowTest extends WebTestCase { /* ── invalid payload ──────────────────────────────────────────────── */ - public function testInvalidHeaderPayloadReturnsUnauthorized(): void { + public function testInvalidHeaderPayloadReturnsUnauthorized(): void + { $client = static::createClient(); $client->request('GET', '/', [], [], [ @@ -338,7 +354,8 @@ final class AuthenticationFlowTest extends WebTestCase { self::assertSame(401, $client->getResponse()->getStatusCode()); } - public function testPayloadWithMissingFieldsReturnsUnauthorized(): void { + public function testPayloadWithMissingFieldsReturnsUnauthorized(): void + { $client = static::createClient(); // payload missing token @@ -353,7 +370,8 @@ final class AuthenticationFlowTest extends WebTestCase { /* ── invalid cookie ───────────────────────────────────────────────── */ - public function testInvalidCookieIsClearedAndLoginPageShown(): void { + public function testInvalidCookieIsClearedAndLoginPageShown(): void + { $client = static::createClient(); // the cookie must be set via the CookieJar so that the HttpFoundation @@ -361,8 +379,15 @@ final class AuthenticationFlowTest extends WebTestCase { // not parsed by Request::create) $client->getCookieJar()->set( new \Symfony\Component\BrowserKit\Cookie( - self::COOKIE_NAME, 'invalid-ulid-value', - null, '/', 'localhost', true, true, false, 'Strict', + self::COOKIE_NAME, + 'invalid-ulid-value', + null, + '/', + 'localhost', + true, + true, + false, + 'Strict', ) ); @@ -383,7 +408,8 @@ final class AuthenticationFlowTest extends WebTestCase { /* ── backup code authentication ───────────────────────────────────── */ - public function testBackupCodeAuthenticationWorks(): void { + public function testBackupCodeAuthenticationWorks(): void + { $client = static::createClient(); $container = $client->getContainer(); @@ -407,7 +433,8 @@ final class AuthenticationFlowTest extends WebTestCase { self::assertSame(303, $client->getResponse()->getStatusCode()); } - public function testConsumedBackupCodeCannotBeReused(): void { + public function testConsumedBackupCodeCannotBeReused(): void + { $client = static::createClient(); $container = $client->getContainer(); @@ -442,7 +469,8 @@ final class AuthenticationFlowTest extends WebTestCase { /* ── return URL handling ──────────────────────────────────────────── */ - public function testSuccessfulLoginWithValidReturnUrl(): void { + public function testSuccessfulLoginWithValidReturnUrl(): void + { $client = static::createClient(); $crawler = $client->request('GET', '/?return=https://example.com/app'); @@ -460,7 +488,8 @@ final class AuthenticationFlowTest extends WebTestCase { self::assertSame('https://example.com/app', $response->headers->get('Location')); } - public function testSuccessfulLoginWithInvalidReturnFallsBackToPath(): void { + public function testSuccessfulLoginWithInvalidReturnFallsBackToPath(): void + { $client = static::createClient(); $crawler = $client->request('GET', '/?return=not-a-url'); diff --git a/tests/Support/ListenerTestHelper.php b/tests/Support/ListenerTestHelper.php index bd335b2..e312c93 100644 --- a/tests/Support/ListenerTestHelper.php +++ b/tests/Support/ListenerTestHelper.php @@ -1,4 +1,5 @@ true]); // the templates reference a global `env` object; supply one with the @@ -49,32 +52,43 @@ trait ListenerTestHelper { * A RateLimiterFactoryInterface whose created limiter returns a RateLimit * with the given remaining tokens. */ - private function makeRateLimiterFactory(int $remainingTokens): RateLimiterFactoryInterface { + private function makeRateLimiterFactory(int $remainingTokens): RateLimiterFactoryInterface + { $limiter = $this->makeLimiter($remainingTokens); - return new class($limiter) implements RateLimiterFactoryInterface { - public function __construct(private LimiterInterface $limiter) {} - public function create(?string $key = null): LimiterInterface { + return new class ($limiter) implements RateLimiterFactoryInterface { + public function __construct(private LimiterInterface $limiter) + { + } + public function create(?string $key = null): LimiterInterface + { return $this->limiter; } }; } - private function makeLimiter(int $remainingTokens): LimiterInterface { + private function makeLimiter(int $remainingTokens): LimiterInterface + { $rateLimit = new RateLimit( $remainingTokens, new \DateTimeImmutable('+10 seconds'), $remainingTokens > 0, 10, ); - return new class($rateLimit) implements LimiterInterface { - public function __construct(private RateLimit $rateLimit) {} - public function reserve(int $tokens = 1, ?float $maxTime = null): \Symfony\Component\RateLimiter\Reservation { + return new class ($rateLimit) implements LimiterInterface { + public function __construct(private RateLimit $rateLimit) + { + } + public function reserve(int $tokens = 1, ?float $maxTime = null): \Symfony\Component\RateLimiter\Reservation + { throw new \Symfony\Component\RateLimiter\Exception\ReserveNotSupportedException(); } - public function consume(int $tokens = 1): RateLimit { + public function consume(int $tokens = 1): RateLimit + { return $this->rateLimit; } - public function reset(): void {} + public function reset(): void + { + } }; } @@ -82,14 +96,19 @@ trait ListenerTestHelper { * A factory whose limiter tracks how many consume(1) calls were made and * reports the limit as reached only after $threshold failures. */ - private function makeCountingRateLimiterFactory(int $threshold): RateLimiterFactoryInterface { - $limiter = new class($threshold) implements LimiterInterface { + private function makeCountingRateLimiterFactory(int $threshold): RateLimiterFactoryInterface + { + $limiter = new class ($threshold) implements LimiterInterface { private int $consumed = 0; - public function __construct(private int $threshold) {} - public function reserve(int $tokens = 1, ?float $maxTime = null): \Symfony\Component\RateLimiter\Reservation { + public function __construct(private int $threshold) + { + } + public function reserve(int $tokens = 1, ?float $maxTime = null): \Symfony\Component\RateLimiter\Reservation + { throw new \Symfony\Component\RateLimiter\Exception\ReserveNotSupportedException(); } - public function consume(int $tokens = 1): RateLimit { + public function consume(int $tokens = 1): RateLimit + { $this->consumed += $tokens; $remaining = max(0, $this->threshold - $this->consumed); return new RateLimit( @@ -99,11 +118,17 @@ trait ListenerTestHelper { $this->threshold, ); } - public function reset(): void { $this->consumed = 0; } + public function reset(): void + { + $this->consumed = 0; + } }; - return new class($limiter) implements RateLimiterFactoryInterface { - public function __construct(private LimiterInterface $limiter) {} - public function create(?string $key = null): LimiterInterface { + return new class ($limiter) implements RateLimiterFactoryInterface { + public function __construct(private LimiterInterface $limiter) + { + } + public function create(?string $key = null): LimiterInterface + { return $this->limiter; } }; diff --git a/tests/Support/TotpTestHelper.php b/tests/Support/TotpTestHelper.php index 43ca9d8..9ac0706 100644 --- a/tests/Support/TotpTestHelper.php +++ b/tests/Support/TotpTestHelper.php @@ -1,4 +1,5 @@ time); } }; } /** Provisioning URI built from the well-known secret + frozen clock. */ - private function totpUri(): string { + private function totpUri(): string + { $totp = TOTP::createFromSecret(self::TOTP_SECRET, $this->frozenClock()); $totp->setLabel('Test-TOTP'); return $totp->getProvisioningUri(); } /** The TOTP code that is valid at the frozen timestamp. */ - private function validTotpCode(): string { + private function validTotpCode(): string + { return TOTP::createFromSecret(self::TOTP_SECRET, $this->frozenClock())->now(); } /** A fresh in-memory cache pool suitable for wrapping in MonitorCacheKeys. */ - private function emptyPool(): CacheItemPoolInterface { + private function emptyPool(): CacheItemPoolInterface + { return new ArrayAdapter(); } @@ -67,9 +76,15 @@ trait TotpTestHelper { $clock = $this->frozenClock(); $utilities = $this->createUtilities($clock); return new ConfigBag( - $utilities, $clock, - $cookieTtl, $this->totpUri(), $ipTtl, $teapot, - $errorMessage, $teapotTitle, $tooManyTitle, + $utilities, + $clock, + $cookieTtl, + $this->totpUri(), + $ipTtl, + $teapot, + $errorMessage, + $teapotTitle, + $tooManyTitle, ); } @@ -77,7 +92,8 @@ trait TotpTestHelper { * Minimal Utilities stub that never triggers TOTP generation when * a non-empty totpUri is supplied to ConfigBag. */ - private function createUtilities(?PsrClockInterface $clock = null): Utilities { + private function createUtilities(?PsrClockInterface $clock = null): Utilities + { $clock ??= $this->frozenClock(); $cache = $this->createStub(CacheItemPoolInterface::class); $cache->method('hasItem')->willReturn(false); diff --git a/tests/TestKernel.php b/tests/TestKernel.php index f4cc885..369568d 100644 --- a/tests/TestKernel.php +++ b/tests/TestKernel.php @@ -1,4 +1,5 @@ addCompilerPass(new class implements CompilerPassInterface { + $container->addCompilerPass(new class () implements CompilerPassInterface { public function process(ContainerBuilder $container): void { foreach (['nonceCache', 'rateLimitCache', 'sessionCache', 'sessionStorage'] as $poolId) { diff --git a/tests/Unit/ClockTest.php b/tests/Unit/ClockTest.php index 559cd6a..3a921ca 100644 --- a/tests/Unit/ClockTest.php +++ b/tests/Unit/ClockTest.php @@ -1,4 +1,5 @@ now(); diff --git a/tests/Unit/Command/GenerateBackupCodesCommandTest.php b/tests/Unit/Command/GenerateBackupCodesCommandTest.php index f8b5e62..694a9f6 100644 --- a/tests/Unit/Command/GenerateBackupCodesCommandTest.php +++ b/tests/Unit/Command/GenerateBackupCodesCommandTest.php @@ -1,4 +1,5 @@ createStub(BackupCodeInterface::class); $manager->method('generate')->willReturn($generatedCodes); return $manager; } - public function testGenerateDefaultCountOutputsCodes(): void { + public function testGenerateDefaultCountOutputsCodes(): void + { $codes = ['abc123', 'def456', 'ghi789', 'jkl012', 'mno345', 'pqr678', 'stu901', 'vwx234', 'yzA567', 'bCd890']; $command = new GenerateBackupCodesCommand( - $this->makeManagerStub($codes), $this->makePersistCache() + $this->makeManagerStub($codes), + $this->makePersistCache() ); $command->setName('app:generate-backup-codes'); @@ -42,7 +47,8 @@ final class GenerateBackupCodesCommandTest extends TestCase { } } - public function testGenerateSpecificCountPassesCountToManager(): void { + public function testGenerateSpecificCountPassesCountToManager(): void + { $manager = $this->createMock(BackupCodeInterface::class); $manager->expects(self::once()) ->method('generate') @@ -58,7 +64,8 @@ final class GenerateBackupCodesCommandTest extends TestCase { self::assertSame(0, $exit); } - public function testDefaultCountArgumentIsTen(): void { + public function testDefaultCountArgumentIsTen(): void + { // the configured default for the count argument should be 10 $manager = $this->createMock(BackupCodeInterface::class); $manager->expects(self::once()) @@ -76,13 +83,15 @@ final class GenerateBackupCodesCommandTest extends TestCase { $this->addToAssertionCount(1); } - public function testBootsAndPersistsCache(): void { + public function testBootsAndPersistsCache(): void + { // PersistCache is final and can't be mocked, but we can verify the // command runs end-to-end with a real instance; boot()/persist() // are invoked implicitly. A successful exit confirms both were called // without throwing. $command = new GenerateBackupCodesCommand( - $this->makeManagerStub(['code1']), $this->makePersistCache() + $this->makeManagerStub(['code1']), + $this->makePersistCache() ); $command->setName('app:generate-backup-codes'); @@ -92,9 +101,11 @@ final class GenerateBackupCodesCommandTest extends TestCase { self::assertSame(0, $exit); } - public function testZeroCodesOutputsNothing(): void { + public function testZeroCodesOutputsNothing(): void + { $command = new GenerateBackupCodesCommand( - $this->makeManagerStub([]), $this->makePersistCache() + $this->makeManagerStub([]), + $this->makePersistCache() ); $command->setName('app:generate-backup-codes'); @@ -105,9 +116,11 @@ final class GenerateBackupCodesCommandTest extends TestCase { self::assertSame('', trim($tester->getDisplay())); } - public function testCommandNameAndDescriptionAreConfigured(): void { + public function testCommandNameAndDescriptionAreConfigured(): void + { $command = new GenerateBackupCodesCommand( - $this->makeManagerStub([]), $this->makePersistCache() + $this->makeManagerStub([]), + $this->makePersistCache() ); // configuring via the Application runs the protected configure() $app = new \Symfony\Component\Console\Application(); diff --git a/tests/Unit/ConfigBagTest.php b/tests/Unit/ConfigBagTest.php index acddced..f690f46 100644 --- a/tests/Unit/ConfigBagTest.php +++ b/tests/Unit/ConfigBagTest.php @@ -1,4 +1,5 @@ createStub(ClockInterface::class); $cache = $this->createStub(CacheItemPoolInterface::class); @@ -28,14 +31,21 @@ final class ConfigBagTest extends TestCase { return new Utilities($clock, $cache); } - public function testGettersWithExplicitValues(): void { + public function testGettersWithExplicitValues(): void + { $clock = $this->createStub(ClockInterface::class); $utilities = $this->createUtilities(); $config = new ConfigBag( - $utilities, $clock, - 3600, 'otpauth://totp/test', 1800, true, - 'Error!', 'Teapot!', 'Too Many!' + $utilities, + $clock, + 3600, + 'otpauth://totp/test', + 1800, + true, + 'Error!', + 'Teapot!', + 'Too Many!' ); self::assertSame($clock, $config->clock()); @@ -48,43 +58,63 @@ final class ConfigBagTest extends TestCase { self::assertSame('Too Many!', $config->tooManyTitle()); } - public function testTotpUriFallsBackToUtilitiesWhenEmpty(): void { + public function testTotpUriFallsBackToUtilitiesWhenEmpty(): void + { $clock = $this->createStub(ClockInterface::class); $utilities = $this->createUtilities('fallback-totp'); $config = new ConfigBag( - $utilities, $clock, - 3600, '', 1800, false, - 'Error', 'Teapot', 'Too Many' + $utilities, + $clock, + 3600, + '', + 1800, + false, + 'Error', + 'Teapot', + 'Too Many' ); self::assertSame('fallback-totp', $config->totpUri()); } - public function testIpTtlFallsBackToNullWhenZero(): void { + public function testIpTtlFallsBackToNullWhenZero(): void + { $clock = $this->createStub(ClockInterface::class); $utilities = $this->createUtilities(); $config = new ConfigBag( - $utilities, $clock, - 3600, 'otpauth://totp/test', 0, false, - 'Error', 'Teapot', 'Too Many' + $utilities, + $clock, + 3600, + 'otpauth://totp/test', + 0, + false, + 'Error', + 'Teapot', + 'Too Many' ); self::assertNull($config->ipTtl()); } - public function testIpTtlFallsBackToNullWhenNull(): void { + public function testIpTtlFallsBackToNullWhenNull(): void + { $clock = $this->createStub(ClockInterface::class); $utilities = $this->createUtilities(); $config = new ConfigBag( - $utilities, $clock, - 3600, 'otpauth://totp/test', null, false, - 'Error', 'Teapot', 'Too Many' + $utilities, + $clock, + 3600, + 'otpauth://totp/test', + null, + false, + 'Error', + 'Teapot', + 'Too Many' ); self::assertNull($config->ipTtl()); } } - diff --git a/tests/Unit/Data/PayloadTest.php b/tests/Unit/Data/PayloadTest.php index b86fe30..d9fc4e0 100644 --- a/tests/Unit/Data/PayloadTest.php +++ b/tests/Unit/Data/PayloadTest.php @@ -1,4 +1,5 @@ 'testuser', 'token' => '123456', 'nonce' => 'abc123', 'json' => true, 'scope' => 'cookie', @@ -28,41 +32,50 @@ final class PayloadTest extends TestCase { self::assertSame(Scope::Cookie, $payload->scope); } - public function testDecodeInvalidBase64UrlReturnsNull(): void { + public function testDecodeInvalidBase64UrlReturnsNull(): void + { self::assertNull(Payload::decode('!!!not-valid-base64!!!')); } - public function testDecodeNonObjectJsonReturnsNull(): void { + public function testDecodeNonObjectJsonReturnsNull(): void + { self::assertNull(Payload::decode(self::b64u('"just a string"'))); } - public function testDecodeInvalidJsonReturnsNull(): void { + public function testDecodeInvalidJsonReturnsNull(): void + { // valid base64url but invalid JSON self::assertNull(Payload::decode(self::b64u('{invalid json'))); } - public function testDecodeJsonArrayReturnsNull(): void { + public function testDecodeJsonArrayReturnsNull(): void + { self::assertNull(Payload::decode(self::b64u('[1,2,3]'))); } - public function testDecodeJsonNullReturnsNull(): void { + public function testDecodeJsonNullReturnsNull(): void + { self::assertNull(Payload::decode(self::b64u('null'))); } - public function testDecodeJsonBooleanReturnsNull(): void { + public function testDecodeJsonBooleanReturnsNull(): void + { self::assertNull(Payload::decode(self::b64u('true'))); self::assertNull(Payload::decode(self::b64u('false'))); } - public function testDecodeJsonNumberReturnsNull(): void { + public function testDecodeJsonNumberReturnsNull(): void + { self::assertNull(Payload::decode(self::b64u('42'))); } - public function testDecodeEmptyStringReturnsNull(): void { + public function testDecodeEmptyStringReturnsNull(): void + { self::assertNull(Payload::decode('')); } - public function testLoadWithValidInputBag(): void { + public function testLoadWithValidInputBag(): void + { $input = new InputBag([ 'username' => 'alice', 'nonce' => 'nonce123', 'totp' => '654321', ]); @@ -76,28 +89,33 @@ final class PayloadTest extends TestCase { self::assertSame(Scope::Cookie, $payload->scope); } - public function testLoadMissingUsernameReturnsNull(): void { + public function testLoadMissingUsernameReturnsNull(): void + { $input = new InputBag(['nonce' => 'n', 'totp' => 't']); self::assertNull(Payload::load($input)); } - public function testLoadMissingNonceReturnsNull(): void { + public function testLoadMissingNonceReturnsNull(): void + { $input = new InputBag(['username' => 'u', 'totp' => 't']); self::assertNull(Payload::load($input)); } - public function testLoadMissingTotpReturnsNull(): void { + public function testLoadMissingTotpReturnsNull(): void + { $input = new InputBag(['username' => 'u', 'nonce' => 'n']); self::assertNull(Payload::load($input)); } - public function testLoadWithAllFieldsPresentButEmptyReturnsNull(): void { + public function testLoadWithAllFieldsPresentButEmptyReturnsNull(): void + { // has() returns true for all, but create() rejects empty values $input = new InputBag(['username' => '', 'nonce' => '', 'totp' => '']); self::assertNull(Payload::load($input)); } - public function testCreateWithValidData(): void { + public function testCreateWithValidData(): void + { $data = (object)[ 'id' => 'user1', 'token' => 'tok1', 'nonce' => 'non1', 'json' => false, 'scope' => 'ip', @@ -112,13 +130,15 @@ final class PayloadTest extends TestCase { self::assertSame(Scope::Ip, $payload->scope); } - public function testCreateWithDefaultScope(): void { + public function testCreateWithDefaultScope(): void + { $data = (object)['id' => 'user1', 'token' => 'tok1', 'nonce' => 'non1']; $payload = Payload::create($data); self::assertSame(Scope::Cookie, $payload->scope); } - public function testCreateWithInvalidScopeFallsBackToCookie(): void { + public function testCreateWithInvalidScopeFallsBackToCookie(): void + { $data = (object)[ 'id' => 'user1', 'token' => 'tok1', 'nonce' => 'non1', 'scope' => 'admin', @@ -127,13 +147,15 @@ final class PayloadTest extends TestCase { self::assertSame(Scope::Cookie, $payload->scope); } - public function testCreateWithMissingJsonDefaultsToTrue(): void { + public function testCreateWithMissingJsonDefaultsToTrue(): void + { $data = (object)['id' => 'user1', 'token' => 'tok1', 'nonce' => 'non1']; $payload = Payload::create($data); self::assertTrue($payload->json); } - public function testCreateWithNoneScopeSetsJsonFalse(): void { + public function testCreateWithNoneScopeSetsJsonFalse(): void + { $data = (object)[ 'id' => 'user1', 'token' => 'tok1', 'nonce' => 'non1', 'json' => true, 'scope' => 'none', @@ -143,27 +165,32 @@ final class PayloadTest extends TestCase { self::assertFalse($payload->json); } - public function testCreateWithEmptyIdReturnsNull(): void { + public function testCreateWithEmptyIdReturnsNull(): void + { $data = (object)['id' => '', 'token' => 't', 'nonce' => 'n']; self::assertNull(Payload::create($data)); } - public function testCreateWithWhitespaceIdReturnsNull(): void { + public function testCreateWithWhitespaceIdReturnsNull(): void + { $data = (object)['id' => ' ', 'token' => 't', 'nonce' => 'n']; self::assertNull(Payload::create($data)); } - public function testCreateWithEmptyTokenReturnsNull(): void { + public function testCreateWithEmptyTokenReturnsNull(): void + { $data = (object)['id' => 'u', 'token' => '', 'nonce' => 'n']; self::assertNull(Payload::create($data)); } - public function testCreateWithEmptyNonceReturnsNull(): void { + public function testCreateWithEmptyNonceReturnsNull(): void + { $data = (object)['id' => 'u', 'token' => 't', 'nonce' => '']; self::assertNull(Payload::create($data)); } - public function testCreateTrimsAndTruncatesFields(): void { + public function testCreateTrimsAndTruncatesFields(): void + { $long = str_repeat('a', 200); $data = (object)[ 'id' => ' ' . $long . ' ', @@ -177,7 +204,8 @@ final class PayloadTest extends TestCase { self::assertSame($expected, $payload->nonce); } - public function testToString(): void { + public function testToString(): void + { $payload = new Payload(); $payload->id = 'u'; $payload->token = 't'; diff --git a/tests/Unit/Enum/ScopeTest.php b/tests/Unit/Enum/ScopeTest.php index 071f58d..2ecd532 100644 --- a/tests/Unit/Enum/ScopeTest.php +++ b/tests/Unit/Enum/ScopeTest.php @@ -1,4 +1,5 @@ value); self::assertSame('ip', Scope::Ip->value); self::assertSame('none', Scope::None->value); } - public function testTryFromValid(): void { + public function testTryFromValid(): void + { self::assertSame(Scope::Cookie, Scope::tryFrom('cookie')); self::assertSame(Scope::Ip, Scope::tryFrom('ip')); self::assertSame(Scope::None, Scope::tryFrom('none')); } - public function testTryFromInvalid(): void { + public function testTryFromInvalid(): void + { self::assertNull(Scope::tryFrom('invalid')); self::assertNull(Scope::tryFrom('')); } diff --git a/tests/Unit/Listener/AcceptListenerTest.php b/tests/Unit/Listener/AcceptListenerTest.php index 41b04a9..f360088 100644 --- a/tests/Unit/Listener/AcceptListenerTest.php +++ b/tests/Unit/Listener/AcceptListenerTest.php @@ -1,4 +1,5 @@ setLogger(new NullLogger()); return $listener; } - private function makeEvent(Request $request): RequestEvent { + private function makeEvent(Request $request): RequestEvent + { return new RequestEvent( $this->createStub(\Symfony\Component\HttpKernel\HttpKernelInterface::class), $request, @@ -36,7 +40,8 @@ final class AcceptListenerTest extends TestCase { /* ── valid cookie session ─────────────────────────────────────────── */ - public function testValidCookieSetsResponseWithRemoteUser(): void { + public function testValidCookieSetsResponseWithRemoteUser(): void + { $pool = new ArrayAdapter(); $ulid = '01HXY1234567890ABCDEFGHIJK'; $item = $pool->getItem('cookie_' . $ulid); @@ -59,7 +64,8 @@ final class AcceptListenerTest extends TestCase { self::assertSame('text/plain', $response->headers->get('Content-Type')); } - public function testValidCookieUsesAuthCookieNameWhenUsingCentralAuth(): void { + public function testValidCookieUsesAuthCookieNameWhenUsingCentralAuth(): void + { $pool = new ArrayAdapter(); $ulid = '01HXY1234567890ABCDEFGHIJK'; $item = $pool->getItem('cookie_' . $ulid); @@ -81,7 +87,8 @@ final class AcceptListenerTest extends TestCase { /* ── negative cases ───────────────────────────────────────────────── */ - public function testNoCookieSetsNoResponse(): void { + public function testNoCookieSetsNoResponse(): void + { $pool = new ArrayAdapter(); $domainManager = new DomainManager(false, ''); $listener = $this->makeListener($pool, $domainManager); @@ -92,7 +99,8 @@ final class AcceptListenerTest extends TestCase { self::assertFalse($event->hasResponse()); } - public function testCookieWithoutSessionSetsNoResponse(): void { + public function testCookieWithoutSessionSetsNoResponse(): void + { $pool = new ArrayAdapter(); $domainManager = new DomainManager(false, ''); $listener = $this->makeListener($pool, $domainManager); @@ -106,7 +114,8 @@ final class AcceptListenerTest extends TestCase { self::assertFalse($event->hasResponse()); } - public function testEmptyCookieValueSetsNoResponse(): void { + public function testEmptyCookieValueSetsNoResponse(): void + { $pool = new ArrayAdapter(); $domainManager = new DomainManager(false, ''); $listener = $this->makeListener($pool, $domainManager); diff --git a/tests/Unit/Listener/AllowListenerTest.php b/tests/Unit/Listener/AllowListenerTest.php index 3b4917e..5641447 100644 --- a/tests/Unit/Listener/AllowListenerTest.php +++ b/tests/Unit/Listener/AllowListenerTest.php @@ -1,4 +1,5 @@ setLogger(new NullLogger()); return $listener; } - private function makeEvent(Request $request): RequestEvent { + private function makeEvent(Request $request): RequestEvent + { return new RequestEvent( $this->createStub(HttpKernelInterface::class), $request, @@ -30,7 +34,8 @@ final class AllowListenerTest extends TestCase { ); } - public function testValidIpSessionSetsResponseWithRemoteUser(): void { + public function testValidIpSessionSetsResponseWithRemoteUser(): void + { $pool = new ArrayAdapter(); $item = $pool->getItem('ip_1.2.3.4'); $item->set('carol'); @@ -50,7 +55,8 @@ final class AllowListenerTest extends TestCase { self::assertSame('text/plain', $response->headers->get('Content-Type')); } - public function testNoIpSessionSetsNoResponse(): void { + public function testNoIpSessionSetsNoResponse(): void + { $pool = new ArrayAdapter(); $config = $this->makeConfig(ipTtl: 1800); $listener = $this->makeListener($pool, $config); @@ -62,7 +68,8 @@ final class AllowListenerTest extends TestCase { self::assertFalse($event->hasResponse()); } - public function testIpAccessDisabledSetsNoResponse(): void { + public function testIpAccessDisabledSetsNoResponse(): void + { $pool = new ArrayAdapter(); // even though there's a stored session, ip access is disabled $item = $pool->getItem('ip_1.2.3.4'); @@ -79,7 +86,8 @@ final class AllowListenerTest extends TestCase { self::assertFalse($event->hasResponse()); } - public function testIpAccessDisabledDoesNotCheckCache(): void { + public function testIpAccessDisabledDoesNotCheckCache(): void + { $pool = new ArrayAdapter(); $config = $this->makeConfig(ipTtl: 0); $listener = $this->makeListener($pool, $config); diff --git a/tests/Unit/Listener/InterceptListenerTest.php b/tests/Unit/Listener/InterceptListenerTest.php index 8ca30c1..d12e11f 100644 --- a/tests/Unit/Listener/InterceptListenerTest.php +++ b/tests/Unit/Listener/InterceptListenerTest.php @@ -1,4 +1,5 @@ createStub(HttpKernelInterface::class), $request, @@ -46,7 +49,8 @@ final class InterceptListenerTest extends TestCase { /* ── central-auth redirect branch ─────────────────────────────────── */ - public function testRedirectsToAuthSubdomainWhenHostMatchesBaseDomain(): void { + public function testRedirectsToAuthSubdomainWhenHostMatchesBaseDomain(): void + { $domainManager = new DomainManager(true, 'auth.example.com'); $listener = $this->makeListener($domainManager); @@ -64,7 +68,8 @@ final class InterceptListenerTest extends TestCase { self::assertStringContainsString(urlencode('https://app.example.com/dashboard'), $location); } - public function testDoesNotRedirectWhenAlreadyOnAuthSubdomain(): void { + public function testDoesNotRedirectWhenAlreadyOnAuthSubdomain(): void + { $domainManager = new DomainManager(true, 'auth.example.com'); $listener = $this->makeListener($domainManager); @@ -81,7 +86,8 @@ final class InterceptListenerTest extends TestCase { /* ── login page rendering branch ──────────────────────────────────── */ - public function testPresentsLoginPageWithUnauthorizedStatus(): void { + public function testPresentsLoginPageWithUnauthorizedStatus(): void + { $domainManager = new DomainManager(false, ''); $listener = $this->makeListener($domainManager); @@ -99,7 +105,8 @@ final class InterceptListenerTest extends TestCase { self::assertStringContainsString('name="nonce"', $content); } - public function testGeneratedNonceIsStoredInCache(): void { + public function testGeneratedNonceIsStoredInCache(): void + { $nonceCache = new ArrayAdapter(); $domainManager = new DomainManager(false, ''); $listener = $this->makeListener($domainManager, $nonceCache); @@ -119,7 +126,8 @@ final class InterceptListenerTest extends TestCase { self::assertTrue(count($nonceCache->getValues()) > 0); } - public function testLoginTemplateUsesPostFormWhenOnAuthSubdomain(): void { + public function testLoginTemplateUsesPostFormWhenOnAuthSubdomain(): void + { $domainManager = new DomainManager(true, 'auth.example.com'); $listener = $this->makeListener($domainManager); @@ -132,7 +140,8 @@ final class InterceptListenerTest extends TestCase { self::assertStringContainsString('method="post"', $content); } - public function testLoginTemplateDoesNotUsePostFormWhenNotOnAuthSubdomain(): void { + public function testLoginTemplateDoesNotUsePostFormWhenNotOnAuthSubdomain(): void + { $domainManager = new DomainManager(false, ''); $listener = $this->makeListener($domainManager); @@ -147,7 +156,8 @@ final class InterceptListenerTest extends TestCase { /* ── invalid cookie pruning ───────────────────────────────────────── */ - public function testInvalidCookieIsClearedWhenPresent(): void { + public function testInvalidCookieIsClearedWhenPresent(): void + { $domainManager = new DomainManager(false, ''); $listener = $this->makeListener($domainManager); @@ -171,7 +181,8 @@ final class InterceptListenerTest extends TestCase { self::assertTrue($cleared, 'Expected the invalid cookie to be cleared'); } - public function testNoCookieClearingWhenNoCookiePresent(): void { + public function testNoCookieClearingWhenNoCookiePresent(): void + { $domainManager = new DomainManager(false, ''); $listener = $this->makeListener($domainManager); @@ -183,7 +194,8 @@ final class InterceptListenerTest extends TestCase { self::assertSame([], $response->headers->getCookies()); } - public function testInvalidCookieUsesAuthCookieNameWithCentralAuth(): void { + public function testInvalidCookieUsesAuthCookieNameWithCentralAuth(): void + { $domainManager = new DomainManager(true, 'auth.example.com'); $listener = $this->makeListener($domainManager); diff --git a/tests/Unit/Listener/LoginListenerTest.php b/tests/Unit/Listener/LoginListenerTest.php index d8d1b65..9ca06eb 100644 --- a/tests/Unit/Listener/LoginListenerTest.php +++ b/tests/Unit/Listener/LoginListenerTest.php @@ -1,4 +1,5 @@ createStub(HttpKernelInterface::class), $request, @@ -48,14 +51,16 @@ final class LoginListenerTest extends TestCase { } /** Build a base64url-encoded X-Preauth header value for a payload. */ - private function encodePayload(array $data): string { + private function encodePayload(array $data): string + { $json = json_encode($data, JSON_THROW_ON_ERROR); return rtrim(strtr(base64_encode($json), '+/', '-_'), '='); } /* ── no login attempt ─────────────────────────────────────────────── */ - public function testNoHeaderAndNoPostReturnsEarlyWithoutResponse(): void { + public function testNoHeaderAndNoPostReturnsEarlyWithoutResponse(): void + { $listener = $this->makeListener(); $request = Request::create('https://example.com/', 'GET'); @@ -65,7 +70,8 @@ final class LoginListenerTest extends TestCase { self::assertFalse($event->hasResponse()); } - public function testPostToNonAuthSubdomainReturnsEarlyWithoutResponse(): void { + public function testPostToNonAuthSubdomainReturnsEarlyWithoutResponse(): void + { // POST only counts as a login attempt when on the auth subdomain $domainManager = new DomainManager(true, 'auth.example.com'); $listener = $this->makeListener(domainManager: $domainManager); @@ -79,7 +85,8 @@ final class LoginListenerTest extends TestCase { /* ── successful login via header ──────────────────────────────────── */ - public function testSuccessfulLoginViaHeaderSetsResponseFromManager(): void { + public function testSuccessfulLoginViaHeaderSetsResponseFromManager(): void + { $expected = new Response('hi alice', 200, ['Remote-User' => 'alice']); $loginManager = $this->createStub(LoginInterface::class); $loginManager->method('checkToken')->willReturn($expected); @@ -99,7 +106,8 @@ final class LoginListenerTest extends TestCase { self::assertSame($expected, $event->getResponse()); } - public function testSuccessfulLoginViaPostToAuthSubdomain(): void { + public function testSuccessfulLoginViaPostToAuthSubdomain(): void + { $expected = new Response('hi bob', 303, ['Location' => '/']); $loginManager = $this->createStub(LoginInterface::class); $loginManager->method('checkToken')->willReturn($expected); @@ -120,7 +128,8 @@ final class LoginListenerTest extends TestCase { /* ── failed login ─────────────────────────────────────────────────── */ - public function testFailedLoginReturnsJsonErrorWithNewNonce(): void { + public function testFailedLoginReturnsJsonErrorWithNewNonce(): void + { $loginManager = $this->createStub(LoginInterface::class); $loginManager->method('checkToken')->willReturn(null); @@ -148,7 +157,8 @@ final class LoginListenerTest extends TestCase { self::assertSame('alice', $body['username']); } - public function testFailedLoginHtmlResponseWhenJsonFalse(): void { + public function testFailedLoginHtmlResponseWhenJsonFalse(): void + { $loginManager = $this->createStub(LoginInterface::class); $loginManager->method('checkToken')->willReturn(null); @@ -169,7 +179,8 @@ final class LoginListenerTest extends TestCase { self::assertStringContainsString('getContent()); } - public function testFailedLoginOnAuthSubdomainUsesPostForm(): void { + public function testFailedLoginOnAuthSubdomainUsesPostForm(): void + { $loginManager = $this->createStub(LoginInterface::class); $loginManager->method('checkToken')->willReturn(null); @@ -194,7 +205,8 @@ final class LoginListenerTest extends TestCase { /* ── rate-limited (blocked) login ─────────────────────────────────── */ - public function testRateLimitedLoginReturnsTeapotWhenTeapotEnabled(): void { + public function testRateLimitedLoginReturnsTeapotWhenTeapotEnabled(): void + { $loginManager = $this->createStub(LoginInterface::class); $loginManager->method('checkToken')->willReturn(null); @@ -220,7 +232,8 @@ final class LoginListenerTest extends TestCase { self::assertSame('Teapot', $body['message']); } - public function testRateLimitedLoginReturnsTooManyRequestsWhenTeapotDisabled(): void { + public function testRateLimitedLoginReturnsTooManyRequestsWhenTeapotDisabled(): void + { $loginManager = $this->createStub(LoginInterface::class); $loginManager->method('checkToken')->willReturn(null); @@ -252,7 +265,8 @@ final class LoginListenerTest extends TestCase { /* ── invalid payload handling ─────────────────────────────────────── */ - public function testInvalidHeaderPayloadStillRecordsFailureAndResponds(): void { + public function testInvalidHeaderPayloadStillRecordsFailureAndResponds(): void + { $loginManager = $this->createMock(LoginInterface::class); // checkToken should not be called with a null payload $loginManager->expects(self::never())->method('checkToken'); @@ -272,7 +286,8 @@ final class LoginListenerTest extends TestCase { self::assertSame(Response::HTTP_UNAUTHORIZED, $event->getResponse()->getStatusCode()); } - public function testPostWithoutRequiredFieldsDoesNotAttemptLogin(): void { + public function testPostWithoutRequiredFieldsDoesNotAttemptLogin(): void + { $loginManager = $this->createMock(LoginInterface::class); $loginManager->expects(self::never())->method('checkToken'); diff --git a/tests/Unit/Listener/RejectListenerTest.php b/tests/Unit/Listener/RejectListenerTest.php index 2e8ab5d..d3307f8 100644 --- a/tests/Unit/Listener/RejectListenerTest.php +++ b/tests/Unit/Listener/RejectListenerTest.php @@ -1,4 +1,5 @@ createStub(HttpKernelInterface::class), $request, @@ -37,7 +40,8 @@ final class RejectListenerTest extends TestCase { ); } - public function testBlockedRequestReturnsTeapotWhenTeapotEnabled(): void { + public function testBlockedRequestReturnsTeapotWhenTeapotEnabled(): void + { $listener = $this->makeListener(teapot: true, remainingTokens: 0); $request = Request::create('/', 'GET', [], [], [], ['REMOTE_ADDR' => '1.2.3.4']); @@ -50,7 +54,8 @@ final class RejectListenerTest extends TestCase { self::assertSame('text/html', $response->headers->get('Content-Type')); } - public function testBlockedRequestReturnsTooManyRequestsWhenTeapotDisabled(): void { + public function testBlockedRequestReturnsTooManyRequestsWhenTeapotDisabled(): void + { $listener = $this->makeListener(teapot: false, remainingTokens: 0); $request = Request::create('/', 'GET', [], [], [], ['REMOTE_ADDR' => '1.2.3.4']); @@ -63,7 +68,8 @@ final class RejectListenerTest extends TestCase { self::assertSame('text/html', $response->headers->get('Content-Type')); } - public function testUnblockedRequestSetsNoResponse(): void { + public function testUnblockedRequestSetsNoResponse(): void + { $listener = $this->makeListener(remainingTokens: 5); $request = Request::create('/', 'GET', [], [], [], ['REMOTE_ADDR' => '1.2.3.4']); @@ -74,7 +80,8 @@ final class RejectListenerTest extends TestCase { self::assertFalse($event->hasResponse()); } - public function testBlockedResponseContainsErrorTemplateContent(): void { + public function testBlockedResponseContainsErrorTemplateContent(): void + { $listener = $this->makeListener(teapot: true, remainingTokens: 0); $request = Request::create('/', 'GET', [], [], [], ['REMOTE_ADDR' => '1.2.3.4']); diff --git a/tests/Unit/MonitorCacheKeysTest.php b/tests/Unit/MonitorCacheKeysTest.php index 32105e8..ba8400f 100644 --- a/tests/Unit/MonitorCacheKeysTest.php +++ b/tests/Unit/MonitorCacheKeysTest.php @@ -1,4 +1,5 @@ wrap(); self::assertSame([], $monitor->getKeys()); self::assertSame([], $monitor->getChanges()); } - public function testSaveAddsKeyAndTracksChange(): void { + public function testSaveAddsKeyAndTracksChange(): void + { $monitor = $this->wrap(); $item = $monitor->getItem('alpha'); $item->set('value'); @@ -31,7 +36,8 @@ final class MonitorCacheKeysTest extends TestCase { self::assertSame(['alpha' => MonitorCacheKeys::UPDATED], $monitor->getChanges()); } - public function testSaveDeferredThenCommitAddsKey(): void { + public function testSaveDeferredThenCommitAddsKey(): void + { $monitor = $this->wrap(); $item = $monitor->getItem('beta'); $item->set('value'); @@ -42,7 +48,8 @@ final class MonitorCacheKeysTest extends TestCase { self::assertSame(['beta' => MonitorCacheKeys::UPDATED], $monitor->getChanges()); } - public function testGetItemReturnsUnderlyingItem(): void { + public function testGetItemReturnsUnderlyingItem(): void + { $monitor = $this->wrap(); $item = $monitor->getItem('mykey'); $item->set('data'); @@ -53,7 +60,8 @@ final class MonitorCacheKeysTest extends TestCase { self::assertSame('data', $fetched->get()); } - public function testGetItemsReturnsMultipleItems(): void { + public function testGetItemsReturnsMultipleItems(): void + { $monitor = $this->wrap(); $a = $monitor->getItem('a'); $a->set(1); @@ -70,7 +78,8 @@ final class MonitorCacheKeysTest extends TestCase { self::assertSame(['a' => 1, 'b' => 2], $keys); } - public function testHasItemReturnsTrueForExistingKey(): void { + public function testHasItemReturnsTrueForExistingKey(): void + { $monitor = $this->wrap(); $item = $monitor->getItem('exists'); $item->set('v'); @@ -80,7 +89,8 @@ final class MonitorCacheKeysTest extends TestCase { self::assertFalse($monitor->hasItem('missing')); } - public function testDeleteItemRemovesKeyAndTracksRemoval(): void { + public function testDeleteItemRemovesKeyAndTracksRemoval(): void + { $monitor = $this->wrap(); $item = $monitor->getItem('doomed'); $item->set('v'); @@ -93,7 +103,8 @@ final class MonitorCacheKeysTest extends TestCase { self::assertFalse($monitor->hasItem('doomed')); } - public function testDeleteItemOnMissingKeyIsNoop(): void { + public function testDeleteItemOnMissingKeyIsNoop(): void + { $monitor = $this->wrap(); $result = $monitor->deleteItem('nonexistent'); @@ -102,7 +113,8 @@ final class MonitorCacheKeysTest extends TestCase { self::assertSame([], $monitor->getKeys()); } - public function testDeleteItemsRemovesMultipleKeys(): void { + public function testDeleteItemsRemovesMultipleKeys(): void + { $monitor = $this->wrap(); foreach (['x', 'y', 'z'] as $key) { $item = $monitor->getItem($key); @@ -118,7 +130,8 @@ final class MonitorCacheKeysTest extends TestCase { self::assertSame(MonitorCacheKeys::REMOVED, $changes['y']); } - public function testDeleteItemsWithMissingKeysStillReturnsTrue(): void { + public function testDeleteItemsWithMissingKeysStillReturnsTrue(): void + { $monitor = $this->wrap(); $result = $monitor->deleteItems(['ghost1', 'ghost2']); @@ -126,7 +139,8 @@ final class MonitorCacheKeysTest extends TestCase { self::assertTrue($result); } - public function testClearWipesPoolWhenNotEmpty(): void { + public function testClearWipesPoolWhenNotEmpty(): void + { $monitor = $this->wrap(); $item = $monitor->getItem('keep'); $item->set('v'); @@ -138,7 +152,8 @@ final class MonitorCacheKeysTest extends TestCase { self::assertSame([], $monitor->getKeys()); } - public function testClearIsNoopWhenEmpty(): void { + public function testClearIsNoopWhenEmpty(): void + { $monitor = $this->wrap(); $result = $monitor->clear(); @@ -146,7 +161,8 @@ final class MonitorCacheKeysTest extends TestCase { self::assertTrue($result); } - public function testMarkCleanResetsChangeList(): void { + public function testMarkCleanResetsChangeList(): void + { $monitor = $this->wrap(); $item = $monitor->getItem('temp'); $item->set('v'); @@ -160,13 +176,15 @@ final class MonitorCacheKeysTest extends TestCase { self::assertSame(['temp'], $monitor->getKeys()); } - public function testCommitPassesThrough(): void { + public function testCommitPassesThrough(): void + { $monitor = $this->wrap(); self::assertTrue($monitor->commit()); } - public function testSaveKeyListThrowsOutOfBoundsException(): void { + public function testSaveKeyListThrowsOutOfBoundsException(): void + { $monitor = $this->wrap(); $item = $monitor->getItem('__key_list'); @@ -174,7 +192,8 @@ final class MonitorCacheKeysTest extends TestCase { $monitor->save($item); } - public function testSaveChangeListThrowsOutOfBoundsException(): void { + public function testSaveChangeListThrowsOutOfBoundsException(): void + { $monitor = $this->wrap(); $item = $monitor->getItem('__chg_list'); @@ -182,35 +201,40 @@ final class MonitorCacheKeysTest extends TestCase { $monitor->save($item); } - public function testDeleteKeyListThrowsOutOfBoundsException(): void { + public function testDeleteKeyListThrowsOutOfBoundsException(): void + { $monitor = $this->wrap(); $this->expectException(OutOfBoundsException::class); $monitor->deleteItem('__key_list'); } - public function testDeleteChangeListThrowsOutOfBoundsException(): void { + public function testDeleteChangeListThrowsOutOfBoundsException(): void + { $monitor = $this->wrap(); $this->expectException(OutOfBoundsException::class); $monitor->deleteItem('__chg_list'); } - public function testDeleteItemsWithKeyListThrowsOutOfBoundsException(): void { + public function testDeleteItemsWithKeyListThrowsOutOfBoundsException(): void + { $monitor = $this->wrap(); $this->expectException(OutOfBoundsException::class); $monitor->deleteItems(['safe', '__key_list']); } - public function testDeleteItemsWithChangeListThrowsOutOfBoundsException(): void { + public function testDeleteItemsWithChangeListThrowsOutOfBoundsException(): void + { $monitor = $this->wrap(); $this->expectException(OutOfBoundsException::class); $monitor->deleteItems(['__chg_list']); } - public function testSaveDeferredOnKeyListThrowsOutOfBoundsException(): void { + public function testSaveDeferredOnKeyListThrowsOutOfBoundsException(): void + { $monitor = $this->wrap(); $item = $monitor->getItem('safe'); $item->set('value'); @@ -223,7 +247,8 @@ final class MonitorCacheKeysTest extends TestCase { $monitor->saveDeferred($keyListItem); } - public function testSaveDeferredOnChangeListThrowsOutOfBoundsException(): void { + public function testSaveDeferredOnChangeListThrowsOutOfBoundsException(): void + { $monitor = $this->wrap(); $changeListItem = $monitor->getItem('__chg_list'); @@ -231,7 +256,8 @@ final class MonitorCacheKeysTest extends TestCase { $monitor->saveDeferred($changeListItem); } - public function testGetKeysReturnsEmptyArrayWhenKeyListMissing(): void { + public function testGetKeysReturnsEmptyArrayWhenKeyListMissing(): void + { // If the underlying pool loses its key list, getKeys should return [] $pool = new ArrayAdapter(); $monitor = new MonitorCacheKeys($pool); @@ -249,7 +275,8 @@ final class MonitorCacheKeysTest extends TestCase { self::assertSame([], $monitor2->getKeys()); } - public function testDeleteItemReturnsTrueForExistingKey(): void { + public function testDeleteItemReturnsTrueForExistingKey(): void + { $monitor = $this->wrap(); $item = $monitor->getItem('to-delete'); $item->set('value'); @@ -259,7 +286,8 @@ final class MonitorCacheKeysTest extends TestCase { self::assertNotContains('to-delete', $monitor->getKeys()); } - public function testDeleteItemsReturnsTrue(): void { + public function testDeleteItemsReturnsTrue(): void + { $monitor = $this->wrap(); foreach (['a', 'b', 'c'] as $key) { $item = $monitor->getItem($key); diff --git a/tests/Unit/PersistCacheTest.php b/tests/Unit/PersistCacheTest.php index 34bc45e..e5421b1 100644 --- a/tests/Unit/PersistCacheTest.php +++ b/tests/Unit/PersistCacheTest.php @@ -1,4 +1,5 @@ getKeys()); } - public function testBootLoadsFromStorageIntoCache(): void { + public function testBootLoadsFromStorageIntoCache(): void + { $sessionCache = new ArrayAdapter(); $sessionStorage = new ArrayAdapter(); @@ -43,7 +47,8 @@ final class PersistCacheTest extends TestCase { self::assertSame([], $cacheMonitor->getChanges()); } - public function testBootDoesNotReloadWhenCacheAlreadyWarm(): void { + public function testBootDoesNotReloadWhenCacheAlreadyWarm(): void + { $sessionCache = new ArrayAdapter(); $sessionStorage = new ArrayAdapter(); @@ -68,7 +73,8 @@ final class PersistCacheTest extends TestCase { self::assertNotContains('cookie_new', $monitor->getKeys()); } - public function testPersistWritesChangesToStorage(): void { + public function testPersistWritesChangesToStorage(): void + { $sessionCache = new ArrayAdapter(); $sessionStorage = new ArrayAdapter(); @@ -89,7 +95,8 @@ final class PersistCacheTest extends TestCase { self::assertSame('user2', $storageMonitor->getItem('cookie_xyz')->get()); } - public function testPersistHandlesRemovals(): void { + public function testPersistHandlesRemovals(): void + { $sessionCache = new ArrayAdapter(); $sessionStorage = new ArrayAdapter(); @@ -114,7 +121,8 @@ final class PersistCacheTest extends TestCase { self::assertNotContains('cookie_to_remove', $storageMonitor->getKeys()); } - public function testPersistIsNoopWhenNoChanges(): void { + public function testPersistIsNoopWhenNoChanges(): void + { $sessionCache = new ArrayAdapter(); $sessionStorage = new ArrayAdapter(); @@ -126,7 +134,8 @@ final class PersistCacheTest extends TestCase { self::assertSame([], $storageMonitor->getKeys()); } - public function testFullBootModifyPersistCycle(): void { + public function testFullBootModifyPersistCycle(): void + { $sessionCache = new ArrayAdapter(); $sessionStorage = new ArrayAdapter(); @@ -151,7 +160,8 @@ final class PersistCacheTest extends TestCase { self::assertSame('cycled-user', $monitor->getItem('cookie_cycle')->get()); } - public function testPersistHandlesMixedUpdatesAndRemovals(): void { + public function testPersistHandlesMixedUpdatesAndRemovals(): void + { $sessionCache = new ArrayAdapter(); $sessionStorage = new ArrayAdapter(); @@ -184,7 +194,8 @@ final class PersistCacheTest extends TestCase { self::assertNotContains('cookie_remove', $storageMonitor->getKeys()); } - public function testMultipleBootModifyPersistCycles(): void { + public function testMultipleBootModifyPersistCycles(): void + { $sessionCache = new ArrayAdapter(); $sessionStorage = new ArrayAdapter(); diff --git a/tests/Unit/Service/BackupCodeManagerTest.php b/tests/Unit/Service/BackupCodeManagerTest.php index 32fe766..e6905f7 100644 --- a/tests/Unit/Service/BackupCodeManagerTest.php +++ b/tests/Unit/Service/BackupCodeManagerTest.php @@ -1,4 +1,5 @@ setConfig($this->makeConfig()); @@ -20,7 +23,8 @@ final class BackupCodeManagerTest extends TestCase { return $manager; } - public function testGenerateReturnsRequestedCount(): void { + public function testGenerateReturnsRequestedCount(): void + { $manager = $this->makeManager(); $codes = $manager->generate(5); @@ -33,7 +37,8 @@ final class BackupCodeManagerTest extends TestCase { } } - public function testGenerateDefaultCount(): void { + public function testGenerateDefaultCount(): void + { $manager = $this->makeManager(); $codes = $manager->generate(); @@ -41,7 +46,8 @@ final class BackupCodeManagerTest extends TestCase { self::assertCount(10, $codes); } - public function testGenerateZeroReturnsEmptyArray(): void { + public function testGenerateZeroReturnsEmptyArray(): void + { $manager = $this->makeManager(); $codes = $manager->generate(0); @@ -49,7 +55,8 @@ final class BackupCodeManagerTest extends TestCase { self::assertSame([], $codes); } - public function testGeneratedCodesAreStoredInCache(): void { + public function testGeneratedCodesAreStoredInCache(): void + { $pool = new ArrayAdapter(); $manager = $this->makeManager($pool); @@ -65,7 +72,8 @@ final class BackupCodeManagerTest extends TestCase { } } - public function testGeneratedCodesHaveFarFutureExpiry(): void { + public function testGeneratedCodesHaveFarFutureExpiry(): void + { $pool = new ArrayAdapter(); $manager = $this->makeManager($pool); @@ -77,7 +85,8 @@ final class BackupCodeManagerTest extends TestCase { self::assertGreaterThan((new \DateTimeImmutable('+10 years'))->getTimestamp(), (int) $expiry); } - public function testVerifyAndConsumeValidCode(): void { + public function testVerifyAndConsumeValidCode(): void + { $manager = $this->makeManager(); $codes = $manager->generate(2); @@ -86,7 +95,8 @@ final class BackupCodeManagerTest extends TestCase { self::assertTrue($manager->verifyAndConsume($code)); } - public function testVerifyAndConsumeMarksCodeAsUsed(): void { + public function testVerifyAndConsumeMarksCodeAsUsed(): void + { $pool = new ArrayAdapter(); $manager = $this->makeManager($pool); $codes = $manager->generate(1); @@ -99,13 +109,15 @@ final class BackupCodeManagerTest extends TestCase { self::assertFalse($manager->verifyAndConsume($code)); } - public function testVerifyAndConsumeInvalidCode(): void { + public function testVerifyAndConsumeInvalidCode(): void + { $manager = $this->makeManager(); self::assertFalse($manager->verifyAndConsume('nonexistent_code')); } - public function testVerifyAndConsumeIsCaseInsensitive(): void { + public function testVerifyAndConsumeIsCaseInsensitive(): void + { $manager = $this->makeManager(); $codes = $manager->generate(1); $code = $codes[0]; @@ -114,7 +126,8 @@ final class BackupCodeManagerTest extends TestCase { self::assertTrue($manager->verifyAndConsume(strtoupper($code))); } - public function testVerifyAndConsumeStripsInvalidCharacters(): void { + public function testVerifyAndConsumeStripsInvalidCharacters(): void + { $manager = $this->makeManager(); $codes = $manager->generate(1); $code = $codes[0]; @@ -123,7 +136,8 @@ final class BackupCodeManagerTest extends TestCase { self::assertTrue($manager->verifyAndConsume(' ' . $code . '!!')); } - public function testExpireRemovesAllBackupCodes(): void { + public function testExpireRemovesAllBackupCodes(): void + { $pool = new ArrayAdapter(); $manager = $this->makeManager($pool); $codes = $manager->generate(5); @@ -136,7 +150,8 @@ final class BackupCodeManagerTest extends TestCase { } } - public function testExpireWhenNoBackupCodesIsNoop(): void { + public function testExpireWhenNoBackupCodesIsNoop(): void + { $pool = new ArrayAdapter(); $manager = $this->makeManager($pool); @@ -147,7 +162,8 @@ final class BackupCodeManagerTest extends TestCase { self::assertTrue(true); } - public function testExpireRemovesOnlyBackupPrefixedKeys(): void { + public function testExpireRemovesOnlyBackupPrefixedKeys(): void + { $pool = new ArrayAdapter(); $manager = $this->makeManager($pool); @@ -169,14 +185,16 @@ final class BackupCodeManagerTest extends TestCase { } } - public function testVerifyAndConsumeEmptyStringReturnsFalse(): void { + public function testVerifyAndConsumeEmptyStringReturnsFalse(): void + { $manager = $this->makeManager(); // empty string after preg_replace becomes 'backup_' with nothing after it self::assertFalse($manager->verifyAndConsume('')); } - public function testVerifyAndConsumeCodeWithValueFalseReturnsFalse(): void { + public function testVerifyAndConsumeCodeWithValueFalseReturnsFalse(): void + { $pool = new ArrayAdapter(); $manager = $this->makeManager($pool); $codes = $manager->generate(1); @@ -195,7 +213,8 @@ final class BackupCodeManagerTest extends TestCase { self::assertFalse($manager->verifyAndConsume($code)); } - public function testGenerateProducesUniqueCodes(): void { + public function testGenerateProducesUniqueCodes(): void + { $manager = $this->makeManager(); $codes = $manager->generate(50); @@ -204,7 +223,8 @@ final class BackupCodeManagerTest extends TestCase { self::assertCount(50, array_unique($codes), 'All generated codes should be unique'); } - public function testGenerateCodeLengthIsDigitsPlusTwo(): void { + public function testGenerateCodeLengthIsDigitsPlusTwo(): void + { $manager = $this->makeManager(); $codes = $manager->generate(1); diff --git a/tests/Unit/Service/DomainManagerTest.php b/tests/Unit/Service/DomainManagerTest.php index 4deca4a..67b4996 100644 --- a/tests/Unit/Service/DomainManagerTest.php +++ b/tests/Unit/Service/DomainManagerTest.php @@ -1,4 +1,5 @@ createManager(false, 'auth.example.com'); self::assertNull($manager->authBase()); self::assertNull($manager->getAuthSubdomain()); } - public function testAuthBaseIsNullWhenAuthSubdomainIsEmpty(): void { + public function testAuthBaseIsNullWhenAuthSubdomainIsEmpty(): void + { $manager = $this->createManager(true, ''); self::assertNull($manager->authBase()); self::assertNull($manager->getAuthSubdomain()); } - public function testAuthBaseExtractsSimpleDomain(): void { + public function testAuthBaseExtractsSimpleDomain(): void + { $manager = $this->createManager(true, 'auth.example.com'); self::assertSame('example.com', $manager->authBase()); self::assertSame('auth.example.com', $manager->getAuthSubdomain()); } - public function testAuthBaseExtractsMultiPartTld(): void { + public function testAuthBaseExtractsMultiPartTld(): void + { $manager = $this->createManager(true, 'auth.example.co.uk'); self::assertSame('example.co.uk', $manager->authBase()); self::assertSame('auth.example.co.uk', $manager->getAuthSubdomain()); } - public function testAuthBaseIsNullForLocalhostAuth(): void { + public function testAuthBaseIsNullForLocalhostAuth(): void + { $manager = $this->createManager(true, 'localhost'); self::assertNull($manager->authBase()); self::assertNull($manager->getAuthSubdomain()); } - public function testAuthBaseIsNullForIpAuth(): void { + public function testAuthBaseIsNullForIpAuth(): void + { $manager = $this->createManager(true, '192.168.1.1'); self::assertNull($manager->authBase()); self::assertNull($manager->getAuthSubdomain()); @@ -51,90 +60,105 @@ final class DomainManagerTest extends TestCase { /* ── validReturn ──────────────────────────────────────────────────────── */ - public function testValidReturnAcceptsAnyUrlWhenNoSubdomain(): void { + public function testValidReturnAcceptsAnyUrlWhenNoSubdomain(): void + { $manager = $this->createManager(false, ''); self::assertTrue($manager->validReturn('https://evil.com/page')); self::assertTrue($manager->validReturn('https://example.com/ok')); } - public function testValidReturnRejectsInvalidUrl(): void { + public function testValidReturnRejectsInvalidUrl(): void + { $manager = $this->createManager(true, 'auth.example.com'); self::assertFalse($manager->validReturn('not-a-url')); self::assertFalse($manager->validReturn('')); } - public function testValidReturnAcceptsSameBaseDomain(): void { + public function testValidReturnAcceptsSameBaseDomain(): void + { $manager = $this->createManager(true, 'auth.example.com'); self::assertTrue($manager->validReturn('https://app.example.com/dashboard')); self::assertTrue($manager->validReturn('https://example.com/')); } - public function testValidReturnRejectsDifferentBaseDomain(): void { + public function testValidReturnRejectsDifferentBaseDomain(): void + { $manager = $this->createManager(true, 'auth.example.com'); self::assertFalse($manager->validReturn('https://evil.com/phish')); self::assertFalse($manager->validReturn('https://other-example.com/')); } - public function testValidReturnHandlesCoUkTld(): void { + public function testValidReturnHandlesCoUkTld(): void + { $manager = $this->createManager(true, 'auth.example.co.uk'); self::assertTrue($manager->validReturn('https://www.example.co.uk/')); self::assertFalse($manager->validReturn('https://example.com/')); } - public function testValidReturnRejectsUrlWithoutHost(): void { + public function testValidReturnRejectsUrlWithoutHost(): void + { $manager = $this->createManager(true, 'auth.example.com'); self::assertFalse($manager->validReturn('mailto:test@example.com')); } /* ── matchesAuth ──────────────────────────────────────────────────────── */ - public function testMatchesAuthIsFalseWhenSubdomainRedirectDisabled(): void { + public function testMatchesAuthIsFalseWhenSubdomainRedirectDisabled(): void + { $manager = $this->createManager(false, 'auth.example.com'); self::assertFalse($manager->matchesAuth('example.com')); self::assertFalse($manager->matchesAuth('app.example.com')); } - public function testMatchesAuthIsFalseWhenAuthSubdomainIsEmpty(): void { + public function testMatchesAuthIsFalseWhenAuthSubdomainIsEmpty(): void + { $manager = $this->createManager(true, ''); self::assertFalse($manager->matchesAuth('example.com')); } - public function testMatchesAuthMatchesSameBaseDomain(): void { + public function testMatchesAuthMatchesSameBaseDomain(): void + { $manager = $this->createManager(true, 'auth.example.com'); self::assertTrue($manager->matchesAuth('example.com')); self::assertTrue($manager->matchesAuth('app.example.com')); } - public function testMatchesAuthRejectsDifferentBaseDomain(): void { + public function testMatchesAuthRejectsDifferentBaseDomain(): void + { $manager = $this->createManager(true, 'auth.example.com'); self::assertFalse($manager->matchesAuth('evil.com')); self::assertFalse($manager->matchesAuth('example.org')); } - public function testMatchesAuthHandlesMultiPartTld(): void { + public function testMatchesAuthHandlesMultiPartTld(): void + { $manager = $this->createManager(true, 'auth.example.co.uk'); self::assertTrue($manager->matchesAuth('www.example.co.uk')); self::assertFalse($manager->matchesAuth('example.com')); } - public function testMatchesAuthRejectsIpHost(): void { + public function testMatchesAuthRejectsIpHost(): void + { $manager = $this->createManager(true, 'auth.example.com'); self::assertFalse($manager->matchesAuth('192.168.1.1')); } - public function testMatchesAuthRejectsLocalhost(): void { + public function testMatchesAuthRejectsLocalhost(): void + { $manager = $this->createManager(true, 'auth.example.com'); self::assertFalse($manager->matchesAuth('localhost')); } /* ── baseDomain edge cases via matchesAuth ────────────────────────────── */ - public function testMatchesAuthWithDeepSubdomain(): void { + public function testMatchesAuthWithDeepSubdomain(): void + { $manager = $this->createManager(true, 'auth.example.com'); self::assertTrue($manager->matchesAuth('a.b.c.example.com')); } - public function testMatchesAuthWithTwoPartDomain(): void { + public function testMatchesAuthWithTwoPartDomain(): void + { /* for a 2-part auth subdomain, the baseDomain retains both parts */ $manager = $this->createManager(true, 'auth.local'); self::assertSame('auth.local', $manager->authBase()); @@ -145,7 +169,8 @@ final class DomainManagerTest extends TestCase { /* ── TLD table coverage ──────────────────────────────────────────────── */ - public function testMatchesAuthWithComAuTld(): void { + public function testMatchesAuthWithComAuTld(): void + { // com.au is NOT in the TLD table (table has au? no, it doesn't), // so it's treated as a standard 2-part TLD: base = com.au $manager = $this->createManager(true, 'auth.example.com.au'); @@ -154,7 +179,8 @@ final class DomainManagerTest extends TestCase { self::assertFalse($manager->matchesAuth('example.com')); } - public function testMatchesAuthWithCoJpTld(): void { + public function testMatchesAuthWithCoJpTld(): void + { // co.jp is NOT in the TLD table (table has jpn under com, not jp under co) // so base = co.jp $manager = $this->createManager(true, 'auth.example.co.jp'); @@ -162,7 +188,8 @@ final class DomainManagerTest extends TestCase { self::assertTrue($manager->matchesAuth('www.example.co.jp')); } - public function testMatchesAuthWithComBrTld(): void { + public function testMatchesAuthWithComBrTld(): void + { // com.br: TLD table has com => [br], meaning *.br.com is multi-part // but com.br has last=br, TLD['br'] doesn't exist, so base = com.br $manager = $this->createManager(true, 'auth.example.com.br'); @@ -170,35 +197,40 @@ final class DomainManagerTest extends TestCase { self::assertTrue($manager->matchesAuth('app.example.com.br')); } - public function testMatchesAuthWithCoNzTld(): void { + public function testMatchesAuthWithCoNzTld(): void + { // co.nz is NOT in the TLD table (nz => [co,net,org], so *.co.nz IS multi-part) $manager = $this->createManager(true, 'auth.example.co.nz'); self::assertSame('example.co.nz', $manager->authBase()); self::assertTrue($manager->matchesAuth('sub.example.co.nz')); } - public function testMatchesAuthWithComMxTld(): void { + public function testMatchesAuthWithComMxTld(): void + { // com.mx is NOT in the TLD table (mx => [com,net,org], so *.com.mx IS multi-part) $manager = $this->createManager(true, 'auth.example.com.mx'); self::assertSame('example.com.mx', $manager->authBase()); self::assertTrue($manager->matchesAuth('app.example.com.mx')); } - public function testMatchesAuthWithCoInTld(): void { + public function testMatchesAuthWithCoInTld(): void + { // co.in: in => [co,...], so *.co.in IS multi-part $manager = $this->createManager(true, 'auth.example.co.in'); self::assertSame('example.co.in', $manager->authBase()); self::assertTrue($manager->matchesAuth('app.example.co.in')); } - public function testMatchesAuthWithBrComTld(): void { + public function testMatchesAuthWithBrComTld(): void + { // br.com: TLD table has com => [br], so *.br.com IS multi-part $manager = $this->createManager(true, 'auth.example.br.com'); self::assertSame('example.br.com', $manager->authBase()); self::assertTrue($manager->matchesAuth('app.example.br.com')); } - public function testSimpleTldNotTreatedAsMultiPart(): void { + public function testSimpleTldNotTreatedAsMultiPart(): void + { // example.com is a standard 2-part domain, not multi-part $manager = $this->createManager(true, 'auth.example.com'); self::assertSame('example.com', $manager->authBase()); @@ -208,7 +240,8 @@ final class DomainManagerTest extends TestCase { /* ── baseDomain edge cases ───────────────────────────────────────────── */ - public function testMatchesAuthWithSingleLabelHost(): void { + public function testMatchesAuthWithSingleLabelHost(): void + { // a single-label domain (not localhost, not IP) has baseLength 1 // so 'myhost' has baseDomain 'myhost', while 'auth.local' has base 'auth.local' // they won't match unless the auth subdomain itself is single-label @@ -219,22 +252,26 @@ final class DomainManagerTest extends TestCase { self::assertTrue($manager->matchesAuth('app.auth.local')); } - public function testMatchesAuthWithEmptyStringHost(): void { + public function testMatchesAuthWithEmptyStringHost(): void + { $manager = $this->createManager(true, 'auth.example.com'); self::assertFalse($manager->matchesAuth('')); } - public function testValidReturnAcceptsUrlWithPort(): void { + public function testValidReturnAcceptsUrlWithPort(): void + { $manager = $this->createManager(true, 'auth.example.com'); self::assertTrue($manager->validReturn('https://example.com:8080/path')); } - public function testValidReturnAcceptsUrlWithoutPath(): void { + public function testValidReturnAcceptsUrlWithoutPath(): void + { $manager = $this->createManager(true, 'auth.example.com'); self::assertTrue($manager->validReturn('https://example.com')); } - public function testValidReturnRejectsDifferentDomainWithPort(): void { + public function testValidReturnRejectsDifferentDomainWithPort(): void + { $manager = $this->createManager(true, 'auth.example.com'); self::assertFalse($manager->validReturn('https://evil.com:8080/path')); } diff --git a/tests/Unit/Service/LoginManagerTest.php b/tests/Unit/Service/LoginManagerTest.php index 291cd39..4180947 100644 --- a/tests/Unit/Service/LoginManagerTest.php +++ b/tests/Unit/Service/LoginManagerTest.php @@ -1,4 +1,5 @@ getValue($manager); @@ -74,7 +77,8 @@ final class LoginManagerTest extends TestCase { return $nonce; } - public function testCheckTokenReturnsNullForInvalidTotp(): void { + public function testCheckTokenReturnsNullForInvalidTotp(): void + { $manager = $this->makeLoginManager(); $payload = $this->makePayloadWithNonce($manager, token: 'wrong-code'); @@ -85,7 +89,8 @@ final class LoginManagerTest extends TestCase { self::assertNull($manager->checkToken($payload, $request)); } - public function testCheckTokenReturnsNullForSpentNonce(): void { + public function testCheckTokenReturnsNullForSpentNonce(): void + { $manager = $this->makeLoginManager(); $payload = $this->makePayloadWithNonce($manager); @@ -103,7 +108,8 @@ final class LoginManagerTest extends TestCase { self::assertNull($manager->checkToken($payload, $request)); } - public function testCheckTokenReturnsNullForMissingNonce(): void { + public function testCheckTokenReturnsNullForMissingNonce(): void + { $manager = $this->makeLoginManager(); $this->backupCodeManager->method('verifyAndConsume')->willReturn(false); @@ -120,7 +126,8 @@ final class LoginManagerTest extends TestCase { self::assertNull($manager->checkToken($payload, $request)); } - public function testSuccessfulTotpLoginWithCookieScopeReturnsRedirect(): void { + public function testSuccessfulTotpLoginWithCookieScopeReturnsRedirect(): void + { $manager = $this->makeLoginManager(); $payload = $this->makePayloadWithNonce($manager, scope: Scope::Cookie); @@ -136,7 +143,8 @@ final class LoginManagerTest extends TestCase { self::assertTrue($response->headers->has('Set-Cookie')); } - public function testSuccessfulLoginWithNoneScopeReturnsPlainResponse(): void { + public function testSuccessfulLoginWithNoneScopeReturnsPlainResponse(): void + { $manager = $this->makeLoginManager(); $payload = $this->makePayloadWithNonce($manager, scope: Scope::None); @@ -154,7 +162,8 @@ final class LoginManagerTest extends TestCase { self::assertFalse($response->headers->has('Location')); } - public function testSuccessfulLoginSetsRemoteUserHeader(): void { + public function testSuccessfulLoginSetsRemoteUserHeader(): void + { $manager = $this->makeLoginManager(); $payload = $this->makePayloadWithNonce($manager, id: 'alice', scope: Scope::None); @@ -168,7 +177,8 @@ final class LoginManagerTest extends TestCase { self::assertSame('alice', $response->headers->get('Remote-User')); } - public function testSuccessfulLoginJsonResponse(): void { + public function testSuccessfulLoginJsonResponse(): void + { $manager = $this->makeLoginManager(); $payload = $this->makePayloadWithNonce($manager, scope: Scope::Cookie, token: null); $payload->json = true; @@ -185,7 +195,8 @@ final class LoginManagerTest extends TestCase { self::assertSame('Login successful', $body['message']); } - public function testSuccessfulLoginHtmlResponse(): void { + public function testSuccessfulLoginHtmlResponse(): void + { $manager = $this->makeLoginManager(); $payload = $this->makePayloadWithNonce($manager, scope: Scope::Cookie); $payload->json = false; @@ -200,7 +211,8 @@ final class LoginManagerTest extends TestCase { self::assertSame('text/html', $response->headers->get('Content-Type')); } - public function testSuccessfulLoginWithReturnUrl(): void { + public function testSuccessfulLoginWithReturnUrl(): void + { $manager = $this->makeLoginManager(); $payload = $this->makePayloadWithNonce($manager, scope: Scope::Cookie); @@ -214,7 +226,8 @@ final class LoginManagerTest extends TestCase { self::assertSame('https://example.com/app', $response->headers->get('Location')); } - public function testSuccessfulLoginWithInvalidReturnFallsBackToPath(): void { + public function testSuccessfulLoginWithInvalidReturnFallsBackToPath(): void + { $manager = $this->makeLoginManager(); $payload = $this->makePayloadWithNonce($manager, scope: Scope::Cookie); @@ -229,7 +242,8 @@ final class LoginManagerTest extends TestCase { self::assertStringStartsWith('/login', $location); } - public function testIpScopeDowngradesToCookieWhenIpAccessDisabled(): void { + public function testIpScopeDowngradesToCookieWhenIpAccessDisabled(): void + { $manager = $this->makeLoginManager(ipTtl: 0); $payload = $this->makePayloadWithNonce($manager, scope: Scope::Ip); @@ -244,7 +258,8 @@ final class LoginManagerTest extends TestCase { self::assertTrue($response->headers->has('Set-Cookie')); } - public function testIpScopeWhenEnabledSetsIpSession(): void { + public function testIpScopeWhenEnabledSetsIpSession(): void + { $manager = $this->makeLoginManager(ipTtl: 1800); $payload = $this->makePayloadWithNonce($manager, scope: Scope::Ip); @@ -264,7 +279,8 @@ final class LoginManagerTest extends TestCase { self::assertTrue($sessionCache->hasItem('ip_1.2.3.4')); } - public function testBackupCodeAuthentication(): void { + public function testBackupCodeAuthentication(): void + { $manager = $this->makeLoginManager(); $payload = $this->makePayloadWithNonce($manager, token: 'backup-code-123'); @@ -278,7 +294,8 @@ final class LoginManagerTest extends TestCase { self::assertSame(303, $response->getStatusCode()); } - public function testNonceIsConsumedAfterSuccessfulLogin(): void { + public function testNonceIsConsumedAfterSuccessfulLogin(): void + { $manager = $this->makeLoginManager(); $payload = $this->makePayloadWithNonce($manager); @@ -296,7 +313,8 @@ final class LoginManagerTest extends TestCase { self::assertFalse($nonceItem->get()); } - public function testUlidCollisionThrowsHttpException(): void { + public function testUlidCollisionThrowsHttpException(): void + { // Use a stub pool where every cookie_ key is already a hit (collision) $pool = $this->createStub(CacheItemPoolInterface::class); $item = $this->createStub(CacheItemInterface::class); @@ -358,7 +376,8 @@ final class LoginManagerTest extends TestCase { $manager->checkToken($payload, $request); } - public function testCookieScopeWithCentralAuthSetsDomainOnMatchingHost(): void { + public function testCookieScopeWithCentralAuthSetsDomainOnMatchingHost(): void + { $manager = $this->makeLoginManager( subdomainRedirect: true, authSubdomain: 'auth.example.com', @@ -381,7 +400,8 @@ final class LoginManagerTest extends TestCase { self::assertSame('__Http-Domain-Preauth', $cookies[0]->getName()); } - public function testCookieScopeWithCentralAuthOnNonMatchingHostUsesNullDomain(): void { + public function testCookieScopeWithCentralAuthOnNonMatchingHostUsesNullDomain(): void + { $manager = $this->makeLoginManager( subdomainRedirect: true, authSubdomain: 'auth.example.com', @@ -404,7 +424,8 @@ final class LoginManagerTest extends TestCase { self::assertSame('__Http-Domain-Preauth', $cookies[0]->getName()); } - public function testCheckTokenWithEmptyReturnParameterFallsBackToPath(): void { + public function testCheckTokenWithEmptyReturnParameterFallsBackToPath(): void + { $manager = $this->makeLoginManager(); $payload = $this->makePayloadWithNonce($manager, scope: Scope::Cookie); diff --git a/tests/Unit/Trait/CookieNameTraitTest.php b/tests/Unit/Trait/CookieNameTraitTest.php index cf809e9..648d53a 100644 --- a/tests/Unit/Trait/CookieNameTraitTest.php +++ b/tests/Unit/Trait/CookieNameTraitTest.php @@ -1,4 +1,5 @@ cookieName()); } - public function testAuthCookieName(): void { + public function testAuthCookieName(): void + { self::assertSame('__Http-Domain-Preauth', $this->authCookieName()); } - public function testHeaderName(): void { + public function testHeaderName(): void + { self::assertSame('X-Preauth', $this->headerName()); } } diff --git a/tests/Unit/Trait/GetTotpTraitTest.php b/tests/Unit/Trait/GetTotpTraitTest.php index 03d0937..5428a5a 100644 --- a/tests/Unit/Trait/GetTotpTraitTest.php +++ b/tests/Unit/Trait/GetTotpTraitTest.php @@ -1,4 +1,5 @@ getTotp(); } }; } - public function testSetConfigSetsProperty(): void { + public function testSetConfigSetsProperty(): void + { $obj = $this->makeObject(); $config = $this->makeConfig(); @@ -33,7 +38,8 @@ final class GetTotpTraitTest extends TestCase { self::assertSame($config, $reflection->getValue($obj)); } - public function testGetTotpReturnsTotpInterface(): void { + public function testGetTotpReturnsTotpInterface(): void + { $obj = $this->makeObject(); $obj->setConfig($this->makeConfig()); @@ -42,7 +48,8 @@ final class GetTotpTraitTest extends TestCase { self::assertInstanceOf(TOTPInterface::class, $totp); } - public function testGetTotpReturnsValidCode(): void { + public function testGetTotpReturnsValidCode(): void + { $obj = $this->makeObject(); $obj->setConfig($this->makeConfig()); @@ -52,14 +59,21 @@ final class GetTotpTraitTest extends TestCase { self::assertSame($this->validTotpCode(), $totp->now()); } - public function testGetTotpThrowsOnInvalidUri(): void { + public function testGetTotpThrowsOnInvalidUri(): void + { $obj = $this->makeObject(); $clock = $this->frozenClock(); $utilities = $this->createUtilities($clock); $config = new ConfigBag( - $utilities, $clock, - 3600, 'not-a-valid-uri', 0, false, - 'Error', 'Teapot', 'Too Many' + $utilities, + $clock, + 3600, + 'not-a-valid-uri', + 0, + false, + 'Error', + 'Teapot', + 'Too Many' ); $obj->setConfig($config); @@ -70,16 +84,23 @@ final class GetTotpTraitTest extends TestCase { $obj->publicGetTotp(); } - public function testGetTotpThrowsHttpExceptionWhenNotTotpType(): void { + public function testGetTotpThrowsHttpExceptionWhenNotTotpType(): void + { // A HOTP URI loads successfully as an OTPInterface but is NOT a TOTPInterface, // so the instanceof check in getTotp() should throw an HttpException(500) $obj = $this->makeObject(); $clock = $this->frozenClock(); $utilities = $this->createUtilities($clock); $config = new ConfigBag( - $utilities, $clock, - 3600, 'otpauth://hotp/Test-HOTP?secret=JBSWY3DPEHPK3PXP&counter=0', 0, false, - 'Error', 'Teapot', 'Too Many' + $utilities, + $clock, + 3600, + 'otpauth://hotp/Test-HOTP?secret=JBSWY3DPEHPK3PXP&counter=0', + 0, + false, + 'Error', + 'Teapot', + 'Too Many' ); $obj->setConfig($config); diff --git a/tests/Unit/Trait/HasLoggerTraitTest.php b/tests/Unit/Trait/HasLoggerTraitTest.php index ca480d5..560890a 100644 --- a/tests/Unit/Trait/HasLoggerTraitTest.php +++ b/tests/Unit/Trait/HasLoggerTraitTest.php @@ -1,4 +1,5 @@ createStub(LoggerInterface::class); $this->setLogger($logger); self::assertSame($logger, $this->logger); diff --git a/tests/Unit/Trait/MakeNonceTraitTest.php b/tests/Unit/Trait/MakeNonceTraitTest.php index f0fcd11..b998d2b 100644 --- a/tests/Unit/Trait/MakeNonceTraitTest.php +++ b/tests/Unit/Trait/MakeNonceTraitTest.php @@ -1,4 +1,5 @@ makeNonce($retries); } - public function publicMakeCacheKey(string $name): string { + public function publicMakeCacheKey(string $name): string + { return $this->makeCacheKey($name); } }; } - public function testMakeNonceReturnsBase64UrlString(): void { + public function testMakeNonceReturnsBase64UrlString(): void + { $obj = $this->makeObject(); $obj->setLogger(new NullLogger()); $obj->setNonceCache(new ArrayAdapter()); @@ -44,7 +50,8 @@ final class MakeNonceTraitTest extends TestCase { self::assertMatchesRegularExpression('/^[A-Za-z0-9_-]+$/', $nonce); } - public function testMakeNonceStoresNonceInCache(): void { + public function testMakeNonceStoresNonceInCache(): void + { $pool = new ArrayAdapter(); $obj = $this->makeObject(); $obj->setLogger(new NullLogger()); @@ -59,7 +66,8 @@ final class MakeNonceTraitTest extends TestCase { self::assertTrue($item->get()); } - public function testMakeNonceSetsExpiry(): void { + public function testMakeNonceSetsExpiry(): void + { $pool = new ArrayAdapter(); $obj = $this->makeObject(); $obj->setLogger(new NullLogger()); @@ -74,7 +82,8 @@ final class MakeNonceTraitTest extends TestCase { self::assertGreaterThan(time(), (int) $expiry); } - public function testTwoNoncesAreDifferent(): void { + public function testTwoNoncesAreDifferent(): void + { $pool = new ArrayAdapter(); $obj = $this->makeObject(); $obj->setLogger(new NullLogger()); @@ -86,7 +95,8 @@ final class MakeNonceTraitTest extends TestCase { self::assertNotSame($nonce1, $nonce2); } - public function testMakeNonceThrowsAfterMaxRetries(): void { + public function testMakeNonceThrowsAfterMaxRetries(): void + { // Create a stub pool that always reports every key as a hit (collision) $pool = $this->createStub(CacheItemPoolInterface::class); $item = $this->createStub(CacheItemInterface::class); @@ -105,46 +115,93 @@ final class MakeNonceTraitTest extends TestCase { $obj->publicMakeNonce(); } - public function testMakeNonceRetriesAndSucceedsAfterCollision(): void { + public function testMakeNonceRetriesAndSucceedsAfterCollision(): void + { // Use a spy pool that returns isHit=true on the first getItem call // (simulating a collision), then delegates to a real ArrayAdapter for // subsequent calls so the retry succeeds. $realPool = new ArrayAdapter(); $collisionCount = 0; - $spyPool = new class($realPool, $collisionCount) implements CacheItemPoolInterface { + $spyPool = new class ($realPool, $collisionCount) implements CacheItemPoolInterface { private int $hits = 0; public function __construct( private CacheItemPoolInterface $inner, private int &$hitCounter, - ) {} + ) { + } - public function getItem(string $key): CacheItemInterface { + public function getItem(string $key): CacheItemInterface + { $item = $this->inner->getItem($key); // pretend the first requested key is already a hit (collision) if ($this->hits === 0) { $this->hits++; $this->hitCounter++; - return new class($key) implements CacheItemInterface { - public function __construct(private string $key) {} - public function getKey(): string { return $this->key; } - public function get(): mixed { return true; } - public function isHit(): bool { return true; } - public function set(mixed $value): static { return $this; } - public function expiresAt(?\DateTimeInterface $expiration): static { return $this; } - public function expiresAfter(int|\DateInterval|null $time): static { return $this; } + return new class ($key) implements CacheItemInterface { + public function __construct(private string $key) + { + } + public function getKey(): string + { + return $this->key; + } + public function get(): mixed + { + return true; + } + public function isHit(): bool + { + return true; + } + public function set(mixed $value): static + { + return $this; + } + public function expiresAt(?\DateTimeInterface $expiration): static + { + return $this; + } + public function expiresAfter(int|\DateInterval|null $time): static + { + return $this; + } }; } return $item; } - public function getItems(array $keys = []): iterable { return $this->inner->getItems($keys); } - public function hasItem(string $key): bool { return $this->inner->hasItem($key); } - public function clear(): bool { return $this->inner->clear(); } - public function deleteItem(string $key): bool { return $this->inner->deleteItem($key); } - public function deleteItems(array $keys): bool { return $this->inner->deleteItems($keys); } - public function save(CacheItemInterface $item): bool { return $this->inner->save($item); } - public function saveDeferred(CacheItemInterface $item): bool { return $this->inner->saveDeferred($item); } - public function commit(): bool { return $this->inner->commit(); } + public function getItems(array $keys = []): iterable + { + return $this->inner->getItems($keys); + } + public function hasItem(string $key): bool + { + return $this->inner->hasItem($key); + } + public function clear(): bool + { + return $this->inner->clear(); + } + public function deleteItem(string $key): bool + { + return $this->inner->deleteItem($key); + } + public function deleteItems(array $keys): bool + { + return $this->inner->deleteItems($keys); + } + public function save(CacheItemInterface $item): bool + { + return $this->inner->save($item); + } + public function saveDeferred(CacheItemInterface $item): bool + { + return $this->inner->saveDeferred($item); + } + public function commit(): bool + { + return $this->inner->commit(); + } }; $obj = $this->makeObject(); @@ -158,7 +215,8 @@ final class MakeNonceTraitTest extends TestCase { self::assertSame(1, $collisionCount, 'Expected exactly one collision before success'); } - public function testMakeNonceThrowsImmediatelyWithZeroRetries(): void { + public function testMakeNonceThrowsImmediatelyWithZeroRetries(): void + { $pool = $this->createStub(CacheItemPoolInterface::class); $item = $this->createStub(CacheItemInterface::class); $item->method('isHit')->willReturn(true); diff --git a/tests/Unit/Trait/StringTraitTest.php b/tests/Unit/Trait/StringTraitTest.php index 03c5bb7..876f611 100644 --- a/tests/Unit/Trait/StringTraitTest.php +++ b/tests/Unit/Trait/StringTraitTest.php @@ -1,4 +1,5 @@ makeCacheKey('hello world')); self::assertSame('hello_world', $this->makeCacheKey('hello!world')); self::assertSame('a_b_c_d', $this->makeCacheKey('a/b@c#d')); } - public function testMakeCacheKeyPreservesValidChars(): void { + public function testMakeCacheKeyPreservesValidChars(): void + { self::assertSame('ABC_123.abc', $this->makeCacheKey('ABC_123.abc')); } - public function testMakeCacheKeyTruncatesLongNames(): void { + public function testMakeCacheKeyTruncatesLongNames(): void + { $long = str_repeat('a', 300); $result = $this->makeCacheKey($long); self::assertSame(128, mb_strlen($result)); } - public function testMakeCacheKeyEmptyString(): void { + public function testMakeCacheKeyEmptyString(): void + { self::assertSame('', $this->makeCacheKey('')); } - public function testMakeCacheKeyWithOnlyInvalidChars(): void { + public function testMakeCacheKeyWithOnlyInvalidChars(): void + { // preg_replace with + collapses consecutive invalid chars into one _ self::assertSame('_', $this->makeCacheKey('!!!')); self::assertSame('_', $this->makeCacheKey(' ')); @@ -37,7 +44,8 @@ final class StringTraitTest extends TestCase { self::assertSame('_', $this->makeCacheKey('!@ #')); } - public function testMakeCacheKeyTruncatesToExactly128(): void { + public function testMakeCacheKeyTruncatesToExactly128(): void + { $input = str_repeat('a', 128); self::assertSame(128, mb_strlen($this->makeCacheKey($input))); self::assertSame($input, $this->makeCacheKey($input)); @@ -46,14 +54,16 @@ final class StringTraitTest extends TestCase { self::assertSame(128, mb_strlen($this->makeCacheKey($input129))); } - public function testMakeCacheKeyWithMultibyteChars(): void { + public function testMakeCacheKeyWithMultibyteChars(): void + { // multibyte chars are replaced with a single underscore $result = $this->makeCacheKey('héllo wörld'); // é and ö are not in [A-Za-z0-9_.] so they become _ self::assertSame('h_llo_w_rld', $result); } - public function testMakeCacheKeyWithEmoji(): void { + public function testMakeCacheKeyWithEmoji(): void + { $result = $this->makeCacheKey('a🎉b'); self::assertSame('a_b', $result); } diff --git a/tests/Unit/UtilitiesTest.php b/tests/Unit/UtilitiesTest.php index c50b76c..89e13b3 100644 --- a/tests/Unit/UtilitiesTest.php +++ b/tests/Unit/UtilitiesTest.php @@ -1,4 +1,5 @@ createStub(ClockInterface::class); return new Utilities($clock, $pool); } - public function testLoadTotpReturnsCachedValueWhenPresent(): void { + public function testLoadTotpReturnsCachedValueWhenPresent(): void + { $pool = new ArrayAdapter(); $item = $pool->getItem('totp'); $item->set('otpauth://totp/cached?secret=ABCDEFGH'); @@ -28,7 +32,8 @@ final class UtilitiesTest extends TestCase { self::assertSame('otpauth://totp/cached?secret=ABCDEFGH', $result); } - public function testLoadTotpGeneratesAndStoresWhenMissing(): void { + public function testLoadTotpGeneratesAndStoresWhenMissing(): void + { $pool = new ArrayAdapter(); $utilities = $this->makeUtilities($pool); @@ -43,7 +48,8 @@ final class UtilitiesTest extends TestCase { self::assertSame($result, $cached->get()); } - public function testLoadTotpSetsFarFutureExpiry(): void { + public function testLoadTotpSetsFarFutureExpiry(): void + { $pool = new ArrayAdapter(); $utilities = $this->makeUtilities($pool); @@ -55,7 +61,8 @@ final class UtilitiesTest extends TestCase { self::assertGreaterThan((new \DateTimeImmutable('+10 years'))->getTimestamp(), (int) $expiry); } - public function testLoadTotpIsIdempotentAfterGeneration(): void { + public function testLoadTotpIsIdempotentAfterGeneration(): void + { $pool = new ArrayAdapter(); $utilities = $this->makeUtilities($pool);