From 2e612923f7367e38b1dd433d7658ccb7a9adc33e Mon Sep 17 00:00:00 2001 From: Stephan Wentz Date: Wed, 19 Jul 2023 15:40:54 +0200 Subject: [PATCH 1/3] fix: Add redis storage support, add bundle and deprecate brainbits/blocking-bundle, require psr/clock, rework expiration to ttl mechanism implemented by storage BREAKING CHANGE: Due to clock support several constructors have changed, block, block identifier and owner interfaces are removed --- .github/workflows/test-redis.yml | 53 +++ .github/workflows/test.yml | 3 +- .gitignore | 1 + composer.json | 20 +- phpcs.xml.dist | 23 +- phpstan.neon.dist | 3 + phpunit.xml.dist | 2 +- src/Block.php | 39 +-- src/BlockInterface.php | 36 -- src/BlockableInterface.php | 27 -- src/Blocker.php | 36 +- .../BrainbitsBlockingBundle.php} | 12 +- src/Bundle/Controller/BlockingController.php | 47 +++ .../BrainbitsBlockingExtension.php | 93 +++++ .../DependencyInjection/Configuration.php | 133 ++++++++ .../config/owner_factory/symfony_session.xml | 15 + .../config/owner_factory/symfony_token.xml | 15 + .../Resources/config/owner_factory/value.xml | 15 + src/Bundle/Resources/config/routing.yaml | 8 + src/Bundle/Resources/config/services.yaml | 20 ++ .../Resources/config/storage/filesystem.xml | 16 + .../Resources/config/storage/in_memory.xml | 13 + .../Resources/config/storage/predis.xml | 16 + .../config/validator/always_invalidate.xml | 13 + .../Resources/config/validator/expired.xml | 16 + src/Exception/BlockFailedException.php | 9 +- .../DirectoryNotWritableException.php | 3 - src/Exception/ExceptionInterface.php | 3 - src/Exception/FileNotWritableException.php | 3 - src/Exception/IOException.php | 20 +- src/Exception/NoTokenFoundException.php | 3 - src/Exception/NoUserFoundException.php | 3 - src/Exception/RuntimeException.php | 3 - .../{Identity.php => BlockIdentity.php} | 7 +- src/Identity/IdentityInterface.php | 24 -- src/Owner/Owner.php | 4 +- src/Owner/OwnerFactoryInterface.php | 5 +- src/Owner/SymfonySessionOwnerFactory.php | 7 +- src/Owner/SymfonyTokenOwnerFactory.php | 7 +- src/Owner/ValueOwnerFactory.php | 7 +- src/Storage/FilesystemStorage.php | 135 +++++--- src/Storage/InMemoryStorage.php | 66 ++-- src/Storage/PredisStorage.php | 129 +++++++ src/Storage/StorageInterface.php | 14 +- src/Validator/AlwaysInvalidateValidator.php | 6 +- src/Validator/AlwaysValidateValidator.php | 28 ++ src/Validator/ExpiredValidator.php | 64 ---- src/Validator/ValidatorInterface.php | 4 +- tests/BlockTest.php | 84 +---- tests/BlockerTest.php | 120 ++----- tests/Bundle/BrainbitsBlockingBundleTest.php | 27 ++ .../Controller/BlockingControllerTest.php | 98 ++++++ .../BrainbitsBlockingExtensionTest.php | 108 ++++++ .../DependencyInjection/ConfigurationTest.php | 323 ++++++++++++++++++ tests/Functional/FilesystemTest.php | 155 +++++++++ tests/Functional/InMemoryTest.php | 144 ++++++++ tests/Functional/PredisTest.php | 145 ++++++++ ...tifierTest.php => BlockIdentifierTest.php} | 21 +- tests/Owner/OwnerTest.php | 7 +- .../Owner/SymfonySessionOwnerFactoryTest.php | 9 +- tests/Owner/SymfonyTokenOwnerFactoryTest.php | 21 +- tests/Owner/ValueOwnerFactoryTest.php | 7 +- tests/Storage/FilesystemStorageTest.php | 108 +++--- tests/Storage/InMemoryStorageTest.php | 86 +++-- .../AlwaysInvalidateValidatorTest.php | 60 +--- .../Validator/AlwaysValidateValidatorTest.php | 31 ++ tests/Validator/ExpiredValidatorTest.php | 74 ---- 67 files changed, 2086 insertions(+), 771 deletions(-) create mode 100644 .github/workflows/test-redis.yml delete mode 100644 src/BlockInterface.php delete mode 100644 src/BlockableInterface.php rename src/{Owner/OwnerInterface.php => Bundle/BrainbitsBlockingBundle.php} (59%) create mode 100644 src/Bundle/Controller/BlockingController.php create mode 100644 src/Bundle/DependencyInjection/BrainbitsBlockingExtension.php create mode 100644 src/Bundle/DependencyInjection/Configuration.php create mode 100644 src/Bundle/Resources/config/owner_factory/symfony_session.xml create mode 100644 src/Bundle/Resources/config/owner_factory/symfony_token.xml create mode 100644 src/Bundle/Resources/config/owner_factory/value.xml create mode 100644 src/Bundle/Resources/config/routing.yaml create mode 100644 src/Bundle/Resources/config/services.yaml create mode 100644 src/Bundle/Resources/config/storage/filesystem.xml create mode 100644 src/Bundle/Resources/config/storage/in_memory.xml create mode 100644 src/Bundle/Resources/config/storage/predis.xml create mode 100644 src/Bundle/Resources/config/validator/always_invalidate.xml create mode 100644 src/Bundle/Resources/config/validator/expired.xml rename src/Identity/{Identity.php => BlockIdentity.php} (79%) delete mode 100644 src/Identity/IdentityInterface.php create mode 100644 src/Storage/PredisStorage.php create mode 100644 src/Validator/AlwaysValidateValidator.php delete mode 100644 src/Validator/ExpiredValidator.php create mode 100644 tests/Bundle/BrainbitsBlockingBundleTest.php create mode 100644 tests/Bundle/Controller/BlockingControllerTest.php create mode 100644 tests/Bundle/DependencyInjection/BrainbitsBlockingExtensionTest.php create mode 100644 tests/Bundle/DependencyInjection/ConfigurationTest.php create mode 100644 tests/Functional/FilesystemTest.php create mode 100644 tests/Functional/InMemoryTest.php create mode 100644 tests/Functional/PredisTest.php rename tests/Identifier/{IdentifierTest.php => BlockIdentifierTest.php} (62%) create mode 100644 tests/Validator/AlwaysValidateValidatorTest.php delete mode 100644 tests/Validator/ExpiredValidatorTest.php diff --git a/.github/workflows/test-redis.yml b/.github/workflows/test-redis.yml new file mode 100644 index 0000000..214f16b --- /dev/null +++ b/.github/workflows/test-redis.yml @@ -0,0 +1,53 @@ +name: "Test Redis" + +on: + push: + pull_request: + schedule: + - cron: '0 03 * * 1' # At 03:00 on Monday. + +jobs: + tests: + name: "Tests" + + runs-on: ${{ matrix.operating-system }} + + strategy: + matrix: + dependencies: ["lowest", "highest"] + php-version: + - "8.2" + operating-system: ["ubuntu-latest"] + + steps: + - name: "Checkout" + uses: "actions/checkout@v3" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + + - name: "Cache dependencies" + uses: "actions/cache@v3" + with: + path: "~/.composer/cache" + key: "php-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }}" + restore-keys: "php-${{ matrix.php-version }}-composer-" + + - name: "Install lowest dependencies" + if: ${{ matrix.dependencies == 'lowest' }} + run: "composer update --prefer-lowest --prefer-dist --no-interaction --no-progress --no-suggest" + + - name: "Install highest dependencies" + if: ${{ matrix.dependencies == 'highest' }} + run: "composer update --prefer-dist --no-interaction --no-progress --no-suggest" + + - name: "Start Redis" + uses: "supercharge/redis-github-action@1.5.0" + with: + redis-version: "6" + + - name: "Unit tests" + run: "REDIS_DSN='redis://127.0.0.1:6379' vendor/bin/phpunit --group redis --fail-on-skipped" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0d55f80..c0f3527 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,6 @@ jobs: matrix: dependencies: ["lowest", "highest"] php-version: - - "8.1" - "8.2" operating-system: ["ubuntu-latest"] @@ -46,7 +45,7 @@ jobs: run: "composer update --prefer-dist --no-interaction --no-progress --no-suggest" - name: "Unit tests" - run: "vendor/bin/phpunit" + run: "vendor/bin/phpunit --exclude-group redis" - name: "Coding style" run: "vendor/bin/phpcs --report=summary" diff --git a/.gitignore b/.gitignore index 538184a..e4a21fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.idea/ +/.phpcs-cache /.phpunit.cache /composer.lock /phpcs.xml diff --git a/composer.json b/composer.json index 5373071..288357e 100644 --- a/composer.json +++ b/composer.json @@ -11,18 +11,30 @@ } ], "require": { - "php": "^8.1" + "php": "^8.2", + "psr/clock": "^1.0" }, "require-dev": { + "brainbits/phpcs-standard": "^7.0", + "brainbits/phpstan-rules": "^3.0", + "matthiasnoback/symfony-config-test": "^4.3", + "matthiasnoback/symfony-dependency-injection-test": "^4.3", "mikey179/vfsstream": "^1.6.10", + "phpstan/phpstan": "^1.0", "phpunit/phpunit": "^10.1", + "predis/predis": "^2.2", + "symfony/clock": "^6.3", + "symfony/config": "^6.0", + "symfony/dependency-injection": "^6.0", "symfony/http-foundation": "^6.0", + "symfony/http-kernel": "^6.0", + "symfony/routing": "^6.0", "symfony/security-core": "^6.0", - "brainbits/phpcs-standard": "^7.0", - "phpstan/phpstan": "^1.0", - "brainbits/phpstan-rules": "^3.0" + "phpstan/phpstan-phpunit": "^1.3", + "phpstan/phpstan-symfony": "^1.3" }, "suggest": { + "predis/predis": "If you want to use the PredisStorage", "symfony/http-foundation": "If you want to use the SymfonySessionOwnerFactory", "symfony/security-core": "If you want to use the SymfonyTokenOwnerFactory" }, diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 2ab273e..3f4fbb3 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -1,9 +1,26 @@ - src/ - - + + src + tests + + + + + + + + + + + + + + + + + diff --git a/phpstan.neon.dist b/phpstan.neon.dist index fe6b9b1..ea8fbc6 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -6,3 +6,6 @@ parameters: - vendor/autoload.php includes: - vendor/brainbits/phpstan-rules/rules.neon + - vendor/phpstan/phpstan-phpunit/rules.neon + - vendor/phpstan/phpstan-phpunit/extension.neon + - vendor/phpstan/phpstan-symfony/extension.neon diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 8d5b103..920787e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,5 @@ - + tests diff --git a/src/Block.php b/src/Block.php index 81d631f..6634648 100644 --- a/src/Block.php +++ b/src/Block.php @@ -13,52 +13,29 @@ namespace Brainbits\Blocking; -use Brainbits\Blocking\Identity\IdentityInterface; -use Brainbits\Blocking\Owner\OwnerInterface; -use DateTimeImmutable; +use Brainbits\Blocking\Identity\BlockIdentity; +use Brainbits\Blocking\Owner\Owner; -/** - * Standard block. - */ -class Block implements BlockInterface +final class Block { - private DateTimeImmutable $updatedAt; - public function __construct( - private IdentityInterface $identifier, - private OwnerInterface $owner, - private DateTimeImmutable $createdAt, + private BlockIdentity $identifier, + private Owner $owner, ) { - $this->updatedAt = $createdAt; } - public function getIdentity(): IdentityInterface + public function getIdentity(): BlockIdentity { return $this->identifier; } - public function getOwner(): OwnerInterface + public function getOwner(): Owner { return $this->owner; } - public function isOwnedBy(OwnerInterface $owner): bool + public function isOwnedBy(Owner $owner): bool { return $this->owner->equals($owner); } - - public function getCreatedAt(): DateTimeImmutable - { - return $this->createdAt; - } - - public function getUpdatedAt(): DateTimeImmutable - { - return $this->updatedAt; - } - - public function touch(DateTimeImmutable $updatedAt): void - { - $this->updatedAt = $updatedAt; - } } diff --git a/src/BlockInterface.php b/src/BlockInterface.php deleted file mode 100644 index 04ac405..0000000 --- a/src/BlockInterface.php +++ /dev/null @@ -1,36 +0,0 @@ -storage = $adapter; } - public function block(IdentityInterface $identifier): BlockInterface + public function block(BlockIdentity $identifier, int|null $ttl = null): Block { - $block = $this->tryBlock($identifier); + $block = $this->tryBlock($identifier, $ttl); if ($block === null) { throw BlockFailedException::createAlreadyBlocked($identifier); @@ -46,7 +40,7 @@ public function block(IdentityInterface $identifier): BlockInterface return $block; } - public function tryBlock(IdentityInterface $identifier): BlockInterface|null + public function tryBlock(BlockIdentity $identifier, int|null $ttl = null): Block|null { $owner = $this->ownerFactory->createOwner(); @@ -62,14 +56,14 @@ public function tryBlock(IdentityInterface $identifier): BlockInterface|null return $block; } - $block = new Block($identifier, $owner, new DateTimeImmutable()); + $block = new Block($identifier, $owner); - $this->storage->write($block); + $this->storage->write($block, $ttl ?? $this->defaultTtl); return $block; } - public function unblock(IdentityInterface $identifier): BlockInterface|null + public function unblock(BlockIdentity $identifier): Block|null { $block = $this->getBlock($identifier); if ($block === null) { @@ -81,7 +75,7 @@ public function unblock(IdentityInterface $identifier): BlockInterface|null return $block; } - public function isBlocked(IdentityInterface $identifier): bool + public function isBlocked(BlockIdentity $identifier): bool { $block = $this->storage->get($identifier); @@ -89,6 +83,10 @@ public function isBlocked(IdentityInterface $identifier): bool return false; } + if (!$this->validator) { + return true; + } + $valid = $this->validator->validate($block); if ($valid) { @@ -100,7 +98,7 @@ public function isBlocked(IdentityInterface $identifier): bool return false; } - public function getBlock(IdentityInterface $identifier): BlockInterface|null + public function getBlock(BlockIdentity $identifier): Block|null { if (!$this->isBlocked($identifier)) { return null; diff --git a/src/Owner/OwnerInterface.php b/src/Bundle/BrainbitsBlockingBundle.php similarity index 59% rename from src/Owner/OwnerInterface.php rename to src/Bundle/BrainbitsBlockingBundle.php index da9a576..1843890 100644 --- a/src/Owner/OwnerInterface.php +++ b/src/Bundle/BrainbitsBlockingBundle.php @@ -11,14 +11,10 @@ * file that was distributed with this source code. */ -namespace Brainbits\Blocking\Owner; +namespace Brainbits\Blocking\Bundle; -/** - * Block owner interface. - */ -interface OwnerInterface -{ - public function equals(OwnerInterface $owner): bool; +use Symfony\Component\HttpKernel\Bundle\Bundle; - public function __toString(): string; +final class BrainbitsBlockingBundle extends Bundle +{ } diff --git a/src/Bundle/Controller/BlockingController.php b/src/Bundle/Controller/BlockingController.php new file mode 100644 index 0000000..186e89e --- /dev/null +++ b/src/Bundle/Controller/BlockingController.php @@ -0,0 +1,47 @@ +blocker->tryBlock($identity); + + return new JsonResponse([ + 'success' => !!$block, + ]); + } + + public function unblockAction(string $identifier): JsonResponse + { + $identity = new BlockIdentity($identifier); + + $block = $this->blocker->unblock($identity); + + return new JsonResponse([ + 'success' => !!$block, + ]); + } +} diff --git a/src/Bundle/DependencyInjection/BrainbitsBlockingExtension.php b/src/Bundle/DependencyInjection/BrainbitsBlockingExtension.php new file mode 100644 index 0000000..23ef17c --- /dev/null +++ b/src/Bundle/DependencyInjection/BrainbitsBlockingExtension.php @@ -0,0 +1,93 @@ + $configs */ + public function load(array $configs, ContainerBuilder $container): void + { + $xmlLoader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); + $yamlLoader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); + $yamlLoader->load('services.yaml'); + $configuration = $this->getConfiguration($configs, $container); + + if ($configuration === null) { + // phpcs:ignore Brainbits.Exception.GlobalException.GlobalException + throw new InvalidArgumentException('Configuration not found.'); + } + + $config = $this->processConfiguration($configuration, $configs); + + if (isset($config['predis'])) { + $container->setAlias('brainbits_blocking.predis', $config['predis']); + } + + if (isset($config['storage']['storage_dir'])) { + $container->setParameter( + 'brainbits_blocking.storage.storage_dir', + $config['storage']['storage_dir'], + ); + } + + if (isset($config['storage']['prefix'])) { + $container->setParameter( + 'brainbits_blocking.storage.prefix', + $config['storage']['prefix'], + ); + } + + if (isset($config['owner_factory']['value'])) { + $container->setParameter( + 'brainbits_blocking.owner_factory.value', + $config['owner_factory']['value'], + ); + } + + if (isset($config['validator']['expiration_time'])) { + $container->setParameter( + 'brainbits_blocking.validator.expiration_time', + $config['validator']['expiration_time'], + ); + $container->setParameter('brainbits_blocking.interval', $config['block_interval']); + } + + if ($config['validator']['driver'] !== 'custom') { + $xmlLoader->load(sprintf('validator/%s.xml', $config['validator']['driver'])); + } else { + $container->setAlias('brainbits_blocking.validator', $config['validator']['service']); + } + + if ($config['storage']['driver'] !== 'custom') { + $xmlLoader->load(sprintf('storage/%s.xml', $config['storage']['driver'])); + } else { + $container->setAlias('brainbits_blocking.storage', $config['storage']['service']); + } + + if ($config['owner_factory']['driver'] !== 'custom') { + $xmlLoader->load(sprintf('owner_factory/%s.xml', $config['owner_factory']['driver'])); + } else { + $container->setAlias('brainbits_blocking.owner_factory', $config['owner_factory']['service']); + } + } +} diff --git a/src/Bundle/DependencyInjection/Configuration.php b/src/Bundle/DependencyInjection/Configuration.php new file mode 100644 index 0000000..3ac154d --- /dev/null +++ b/src/Bundle/DependencyInjection/Configuration.php @@ -0,0 +1,133 @@ +getRootNode(); + + $storageDrivers = ['filesystem', 'predis', 'in_memory', 'custom']; + $ownerFactoryDrivers = ['symfony_session', 'symfony_token', 'value', 'custom']; + $validatorDrivers = ['expired', 'always_invalidate', 'custom']; + + $rootNode + ->beforeNormalization() + ->ifTrue(static function ($v) { + if (($v['storage']['driver'] ?? '') !== 'predis') { + return false; + } + + return ($v['predis'] ?? '') === ''; + }) + ->thenInvalid( + 'A predis alias has to be set for the predis storage driver.', + ) + ->end() + ->children() + ->integerNode('block_interval')->defaultValue(30)->end() + ->scalarNode('clock')->end() + ->scalarNode('predis')->end() + ->arrayNode('storage') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('driver') + ->validate() + ->ifNotInArray($storageDrivers) + ->thenInvalid( + 'The storage driver %s is not supported. Please choose one of ' . + json_encode($storageDrivers), + ) + ->end() + ->defaultValue('filesystem') + ->cannotBeEmpty() + ->end() + ->scalarNode('service')->end() + ->scalarNode('storage_dir')->defaultValue('%kernel.cache_dir%/blocking/')->end() + ->scalarNode('prefix')->defaultValue('block')->end() + ->end() + ->end() + ->arrayNode('owner_factory') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('driver') + ->validate() + ->ifNotInArray($ownerFactoryDrivers) + ->thenInvalid( + 'The owner_factory driver %s is not supported. Please choose one of ' . + json_encode($ownerFactoryDrivers), + ) + ->end() + ->defaultValue('symfony_session') + ->cannotBeEmpty() + ->end() + ->scalarNode('service')->end() + ->scalarNode('value')->end() + ->end() + ->end() + ->arrayNode('validator') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('driver') + ->validate() + ->ifNotInArray($validatorDrivers) + ->thenInvalid( + 'The validator driver %s is not supported. Please choose one of ' . + json_encode($validatorDrivers), + ) + ->end() + ->defaultValue('expired') + ->cannotBeEmpty() + ->end() + ->scalarNode('service')->end() + ->integerNode('expiration_time')->defaultValue(300)->end() + ->end() + ->end() + ->end() + ->validate() + ->ifTrue(static function ($v) { + return $v['storage']['driver'] === 'custom' && empty($v['storage']['service']); + }) + ->thenInvalid('You need to specify your own storage service when using the "custom" storage driver.') + ->end() + ->validate() + ->ifTrue(static function ($v) { + return $v['owner_factory']['driver'] === 'custom' && empty($v['owner_factory']['service']); + }) + ->thenInvalid( + 'You need to specify your own owner_factory service when using the "custom" owner_factory driver.', + ) + ->end() + ->validate() + ->ifTrue(static function ($v) { + return $v['validator']['driver'] === 'custom' && empty($v['validator']['service']); + }) + ->thenInvalid( + 'You need to specify your own validator service when using the "custom" validator driver.', + ) + ->end(); + + return $treeBuilder; + } +} diff --git a/src/Bundle/Resources/config/owner_factory/symfony_session.xml b/src/Bundle/Resources/config/owner_factory/symfony_session.xml new file mode 100644 index 0000000..7a9b802 --- /dev/null +++ b/src/Bundle/Resources/config/owner_factory/symfony_session.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/src/Bundle/Resources/config/owner_factory/symfony_token.xml b/src/Bundle/Resources/config/owner_factory/symfony_token.xml new file mode 100644 index 0000000..3243b41 --- /dev/null +++ b/src/Bundle/Resources/config/owner_factory/symfony_token.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/src/Bundle/Resources/config/owner_factory/value.xml b/src/Bundle/Resources/config/owner_factory/value.xml new file mode 100644 index 0000000..4268e60 --- /dev/null +++ b/src/Bundle/Resources/config/owner_factory/value.xml @@ -0,0 +1,15 @@ + + + + + + + + %brainbits_blocking.owner_factory.value% + + + + + diff --git a/src/Bundle/Resources/config/routing.yaml b/src/Bundle/Resources/config/routing.yaml new file mode 100644 index 0000000..6c4f49c --- /dev/null +++ b/src/Bundle/Resources/config/routing.yaml @@ -0,0 +1,8 @@ + +brainbits_blocking_block: + path: /blocking/block/{identifier} + controller: brainbits_blocking.controller::blockAction + +brainbits_blocking_unblock: + path: /blocking/unblock/{identifier} + controller: brainbits_blocking.controller::unblockAction diff --git a/src/Bundle/Resources/config/services.yaml b/src/Bundle/Resources/config/services.yaml new file mode 100644 index 0000000..d64b0b5 --- /dev/null +++ b/src/Bundle/Resources/config/services.yaml @@ -0,0 +1,20 @@ +services: + brainbits_blocking.filesystem_storage: + class: Brainbits\Blocking\Storage\FilesystemStorage + arguments: + - '%kernel.cache_dir%/blocks/' + + brainbits_blocking.blocker: + class: Brainbits\Blocking\Blocker + arguments: + - '@brainbits_blocking.storage' + - '@brainbits_blocking.owner_factory' + - '@brainbits_blocking.validator' + + + brainbits_blocking.controller: + class: Brainbits\Blocking\Bundle\Controller\BlockingController + tags: ['controller.service_arguments'] + arguments: + - '@brainbits_blocking.blocker' + diff --git a/src/Bundle/Resources/config/storage/filesystem.xml b/src/Bundle/Resources/config/storage/filesystem.xml new file mode 100644 index 0000000..e805424 --- /dev/null +++ b/src/Bundle/Resources/config/storage/filesystem.xml @@ -0,0 +1,16 @@ + + + + + + + + + %brainbits_blocking.storage.storage_dir% + + + + + diff --git a/src/Bundle/Resources/config/storage/in_memory.xml b/src/Bundle/Resources/config/storage/in_memory.xml new file mode 100644 index 0000000..ccd56f9 --- /dev/null +++ b/src/Bundle/Resources/config/storage/in_memory.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/src/Bundle/Resources/config/storage/predis.xml b/src/Bundle/Resources/config/storage/predis.xml new file mode 100644 index 0000000..279d313 --- /dev/null +++ b/src/Bundle/Resources/config/storage/predis.xml @@ -0,0 +1,16 @@ + + + + + + + + + %brainbits_blocking.storage.prefix% + + + + + diff --git a/src/Bundle/Resources/config/validator/always_invalidate.xml b/src/Bundle/Resources/config/validator/always_invalidate.xml new file mode 100644 index 0000000..44a34c5 --- /dev/null +++ b/src/Bundle/Resources/config/validator/always_invalidate.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/src/Bundle/Resources/config/validator/expired.xml b/src/Bundle/Resources/config/validator/expired.xml new file mode 100644 index 0000000..11a398a --- /dev/null +++ b/src/Bundle/Resources/config/validator/expired.xml @@ -0,0 +1,16 @@ + + + + + + + + + %brainbits_blocking.validator.expiration_time% + + + + + diff --git a/src/Exception/BlockFailedException.php b/src/Exception/BlockFailedException.php index 90c5c28..931d02f 100644 --- a/src/Exception/BlockFailedException.php +++ b/src/Exception/BlockFailedException.php @@ -13,17 +13,14 @@ namespace Brainbits\Blocking\Exception; -use Brainbits\Blocking\Identity\IdentityInterface; +use Brainbits\Blocking\Identity\BlockIdentity; use function sprintf; -/** - * Block failed exception. - */ class BlockFailedException extends RuntimeException { - public static function createAlreadyBlocked(IdentityInterface $identifier): self + public static function createAlreadyBlocked(BlockIdentity $identity): self { - return new self(sprintf('Identifier %s is already blocked.', $identifier)); + return new self(sprintf('Identifier %s is already blocked.', $identity)); } } diff --git a/src/Exception/DirectoryNotWritableException.php b/src/Exception/DirectoryNotWritableException.php index 8cffad8..386d769 100644 --- a/src/Exception/DirectoryNotWritableException.php +++ b/src/Exception/DirectoryNotWritableException.php @@ -15,9 +15,6 @@ use function sprintf; -/** - * Directory not writable exception. - */ class DirectoryNotWritableException extends RuntimeException { public static function create(string $dirname): self diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php index 12ada23..51deef2 100644 --- a/src/Exception/ExceptionInterface.php +++ b/src/Exception/ExceptionInterface.php @@ -15,9 +15,6 @@ use Throwable; -/** - * Exception interface. - */ interface ExceptionInterface extends Throwable { } diff --git a/src/Exception/FileNotWritableException.php b/src/Exception/FileNotWritableException.php index a57a551..b472d85 100644 --- a/src/Exception/FileNotWritableException.php +++ b/src/Exception/FileNotWritableException.php @@ -15,9 +15,6 @@ use function sprintf; -/** - * File not writable exception. - */ class FileNotWritableException extends RuntimeException { public static function create(string $filename): self diff --git a/src/Exception/IOException.php b/src/Exception/IOException.php index deaed70..e9d5697 100644 --- a/src/Exception/IOException.php +++ b/src/Exception/IOException.php @@ -15,23 +15,25 @@ use function sprintf; -/** - * Input/output exception. - */ class IOException extends RuntimeException { - public static function createWriteFailed(string $filename): self + public static function getFailed(string $identifier): self + { + return new self(sprintf('Get %s failed.', $identifier)); + } + + public static function writeFailed(string $identifier): self { - return new self(sprintf('Write file %s failed.', $filename)); + return new self(sprintf('Write %s failed.', $identifier)); } - public static function createTouchFailed(string $filename): self + public static function touchFailed(string $identifier): self { - return new self(sprintf('Touch file %s failed.', $filename)); + return new self(sprintf('Touch %s failed.', $identifier)); } - public static function createUnlinkFailed(string $filename): self + public static function removeFailed(string $identifier): self { - return new self(sprintf('Unlink file %s failed.', $filename)); + return new self(sprintf('Unlink %s failed.', $identifier)); } } diff --git a/src/Exception/NoTokenFoundException.php b/src/Exception/NoTokenFoundException.php index 0ee94e2..2a8299d 100644 --- a/src/Exception/NoTokenFoundException.php +++ b/src/Exception/NoTokenFoundException.php @@ -13,9 +13,6 @@ namespace Brainbits\Blocking\Exception; -/** - * No token found exception. - */ class NoTokenFoundException extends RuntimeException { public static function create(): self diff --git a/src/Exception/NoUserFoundException.php b/src/Exception/NoUserFoundException.php index a4741a3..0edfc0f 100644 --- a/src/Exception/NoUserFoundException.php +++ b/src/Exception/NoUserFoundException.php @@ -13,9 +13,6 @@ namespace Brainbits\Blocking\Exception; -/** - * No user found exception. - */ class NoUserFoundException extends RuntimeException { public static function create(): self diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php index 46bfc32..04097b8 100644 --- a/src/Exception/RuntimeException.php +++ b/src/Exception/RuntimeException.php @@ -13,9 +13,6 @@ namespace Brainbits\Blocking\Exception; -/** - * Runtime exception. - */ class RuntimeException extends \RuntimeException implements ExceptionInterface { } diff --git a/src/Identity/Identity.php b/src/Identity/BlockIdentity.php similarity index 79% rename from src/Identity/Identity.php rename to src/Identity/BlockIdentity.php index 116d9da..d4fef67 100644 --- a/src/Identity/Identity.php +++ b/src/Identity/BlockIdentity.php @@ -13,16 +13,13 @@ namespace Brainbits\Blocking\Identity; -/** - * Standard identifier. - */ -class Identity implements IdentityInterface +final class BlockIdentity { public function __construct(private string $identityValue) { } - public function equals(IdentityInterface $identifier): bool + public function equals(self $identifier): bool { return (string) $identifier === (string) $this; } diff --git a/src/Identity/IdentityInterface.php b/src/Identity/IdentityInterface.php deleted file mode 100644 index 5284528..0000000 --- a/src/Identity/IdentityInterface.php +++ /dev/null @@ -1,24 +0,0 @@ -requestStack->getCurrentRequest(); if (!$request instanceof Request) { diff --git a/src/Owner/SymfonyTokenOwnerFactory.php b/src/Owner/SymfonyTokenOwnerFactory.php index d16e4de..8b58b9b 100644 --- a/src/Owner/SymfonyTokenOwnerFactory.php +++ b/src/Owner/SymfonyTokenOwnerFactory.php @@ -18,16 +18,13 @@ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\User\UserInterface; -/** - * Symfony token owner. - */ -class SymfonyTokenOwnerFactory implements OwnerFactoryInterface +final readonly class SymfonyTokenOwnerFactory implements OwnerFactoryInterface { public function __construct(private TokenStorageInterface $tokenStorage) { } - public function createOwner(): OwnerInterface + public function createOwner(): Owner { $token = $this->tokenStorage->getToken(); if (!$token) { diff --git a/src/Owner/ValueOwnerFactory.php b/src/Owner/ValueOwnerFactory.php index 50d06bb..e91198f 100644 --- a/src/Owner/ValueOwnerFactory.php +++ b/src/Owner/ValueOwnerFactory.php @@ -13,16 +13,13 @@ namespace Brainbits\Blocking\Owner; -/** - * Value owner factory. - */ -class ValueOwnerFactory implements OwnerFactoryInterface +final class ValueOwnerFactory implements OwnerFactoryInterface { public function __construct(private string $value) { } - public function createOwner(): OwnerInterface + public function createOwner(): Owner { return new Owner($this->value); } diff --git a/src/Storage/FilesystemStorage.php b/src/Storage/FilesystemStorage.php index a84ef52..8e42cf0 100644 --- a/src/Storage/FilesystemStorage.php +++ b/src/Storage/FilesystemStorage.php @@ -13,118 +13,171 @@ namespace Brainbits\Blocking\Storage; -use Brainbits\Blocking\BlockInterface; +use Brainbits\Blocking\Block; use Brainbits\Blocking\Exception\DirectoryNotWritableException; use Brainbits\Blocking\Exception\FileNotWritableException; use Brainbits\Blocking\Exception\IOException; use Brainbits\Blocking\Exception\UnserializeFailedException; -use Brainbits\Blocking\Identity\IdentityInterface; +use Brainbits\Blocking\Identity\BlockIdentity; +use Brainbits\Blocking\Owner\Owner; use DateTimeImmutable; +use Psr\Clock\ClockInterface; +use function assert; use function dirname; use function file_exists; use function file_get_contents; use function file_put_contents; -use function filemtime; +use function is_array; +use function is_string; use function is_writable; +use function json_decode; +use function json_encode; use function mkdir; use function rtrim; -use function serialize; -use function touch; use function unlink; -use function unserialize; /** * Filesystem block storage. * Uses files for storing block information. */ -class FilesystemStorage implements StorageInterface +final class FilesystemStorage implements StorageInterface { - private string $root; - - public function __construct(string $root) - { + public function __construct( + private ClockInterface $clock, + private string $root, + ) { $this->root = rtrim($root, '/'); } - public function write(BlockInterface $block): bool + public function write(Block $block, int $ttl): bool { - $filename = $this->getFilename($block->getIdentity()); + $identity = $block->getIdentity(); + + $filename = $this->getFilename($identity); + $metaFilename = $filename . '.meta'; - if (file_put_contents($filename, serialize($block)) === false) { - throw IOException::createWriteFailed($filename); + $content = json_encode([ + 'identity' => (string) $identity, + 'owner' => (string) $block->getOwner(), + ]); + + $metaContent = json_encode([ + 'ttl' => $ttl, + 'updatedAt' => $this->clock->now()->format('c'), + ]); + + if (file_put_contents($filename, $content) === false) { + throw IOException::writeFailed($filename); + } + + if (file_put_contents($metaFilename, $metaContent) === false) { + throw IOException::writeFailed($metaFilename); } return true; } - public function touch(BlockInterface $block): bool + public function touch(Block $block): bool { - $filename = $this->getFilename($block->getIdentity()); + $identity = $block->getIdentity(); - if (touch($filename) === false) { - throw IOException::createTouchFailed($filename); + if (!$this->exists($identity)) { + return false; } - $updatedAt = DateTimeImmutable::createFromFormat('U', (string) filemtime($filename)); - - if (!$updatedAt) { - throw IOException::createTouchFailed($filename); + $filename = $this->getFilename($block->getIdentity()); + $metaFilename = $filename . '.meta'; + + $metaContent = file_get_contents($metaFilename); + assert(is_string($metaContent)); + assert($metaContent !== ''); + $metaData = json_decode($metaContent, true); + assert(is_array($metaData)); + $metaData['updatedAt'] = $this->clock->now()->format('c'); + $metaContent = json_encode($metaData); + + if (file_put_contents($metaFilename, $metaContent) === false) { + throw IOException::writeFailed($metaFilename); } - $block->touch($updatedAt); - return true; } - public function remove(BlockInterface $block): bool + public function remove(Block $block): bool { if (!$this->exists($block->getIdentity())) { return false; } $filename = $this->getFilename($block->getIdentity()); + $metaFilename = $filename . '.meta'; + if (unlink($filename) === false) { if (file_exists($filename)) { - throw IOException::createUnlinkFailed($filename); + throw IOException::removeFailed($filename); + } + } + + if (unlink($metaFilename) === false) { + if (file_exists($metaFilename)) { + throw IOException::removeFailed($metaFilename); } } return true; } - public function exists(IdentityInterface $identifier): bool + public function exists(BlockIdentity $identity): bool { - $filename = $this->getFilename($identifier); + $filename = $this->getFilename($identity); + $metaFilename = $filename . '.meta'; + + if (!file_exists($filename) || !file_exists($metaFilename)) { + return false; + } - return file_exists($filename); + $metaContent = file_get_contents($metaFilename); + assert(is_string($metaContent)); + assert($metaContent !== ''); + $metaData = json_decode($metaContent, true); + assert(is_array($metaData)); + + $now = $this->clock->now(); + + $expiresAt = (new DateTimeImmutable((string) $metaData['updatedAt'], $now->getTimezone())) + ->modify('+' . $metaData['ttl'] . ' seconds'); + + return $expiresAt > $now; } - public function get(IdentityInterface $identifier): BlockInterface|null + public function get(BlockIdentity $identity): Block|null { - if (!$this->exists($identifier)) { + if (!$this->exists($identity)) { return null; } - $filename = $this->getFilename($identifier); + $filename = $this->getFilename($identity); + $content = file_get_contents($filename); if (!$content) { throw UnserializeFailedException::createFromInput($content); } - $updatedAt = DateTimeImmutable::createFromFormat('U', (string) filemtime($filename)); - $block = unserialize($content); - if (!$block instanceof BlockInterface || !$updatedAt) { - throw UnserializeFailedException::createFromInput($content); - } + $data = json_decode($content, true); - $block->touch($updatedAt); + assert(is_array($data)); + assert($data['identity'] ?? false); + assert($data['owner'] ?? false); - return $block; + return new Block( + new BlockIdentity($data['identity']), + new Owner($data['owner']), + ); } - private function getFilename(IdentityInterface $identifier): string + private function getFilename(BlockIdentity $identifier): string { return $this->ensureFileIsWritable($this->ensureDirectoryExists($this->root) . '/' . $identifier); } diff --git a/src/Storage/InMemoryStorage.php b/src/Storage/InMemoryStorage.php index d5687da..aaf3c1f 100644 --- a/src/Storage/InMemoryStorage.php +++ b/src/Storage/InMemoryStorage.php @@ -13,41 +13,57 @@ namespace Brainbits\Blocking\Storage; -use Brainbits\Blocking\BlockInterface; -use Brainbits\Blocking\Identity\IdentityInterface; +use Brainbits\Blocking\Block; +use Brainbits\Blocking\Identity\BlockIdentity; use DateTimeImmutable; +use Psr\Clock\ClockInterface; /** * In memory block storage. * Uses an internal array for storing block information. */ -class InMemoryStorage implements StorageInterface +final class InMemoryStorage implements StorageInterface { - /** @var BlockInterface[] */ + /** @var array */ private array $blocks; - public function __construct(BlockInterface ...$blocks) + public function __construct( + private ClockInterface $clock, + ) { + } + + public function addBlock(Block $block, int $ttl, DateTimeImmutable $updatedAt): void { - foreach ($blocks as $block) { - $this->blocks[(string) $block->getIdentity()] = $block; - } + $this->blocks[(string) $block->getIdentity()] = [ + 'block' => $block, + 'ttl' => $ttl, + 'updatedAt' => $updatedAt, + ]; } - public function write(BlockInterface $block): bool + public function write(Block $block, int $ttl): bool { - $this->blocks[(string) $block->getIdentity()] = $block; + $this->blocks[(string) $block->getIdentity()] = [ + 'block' => $block, + 'ttl' => $ttl, + 'updatedAt' => $this->clock->now(), + ]; return true; } - public function touch(BlockInterface $block): bool + public function touch(Block $block): bool { - $this->blocks[(string) $block->getIdentity()]->touch(new DateTimeImmutable()); + if (!$this->exists($block->getIdentity())) { + return false; + } + + $this->blocks[(string) $block->getIdentity()]['updatedAt'] = $this->clock->now(); return true; } - public function remove(BlockInterface $block): bool + public function remove(Block $block): bool { if (!$this->exists($block->getIdentity())) { return false; @@ -58,21 +74,27 @@ public function remove(BlockInterface $block): bool return true; } - public function exists(IdentityInterface $identifier): bool + public function exists(BlockIdentity $identity): bool { - return isset($this->blocks[(string) $identifier]); + if (!isset($this->blocks[(string) $identity])) { + return false; + } + + $now = $this->clock->now(); + + $metaData = $this->blocks[(string) $identity]; + + $expiresAt = $metaData['updatedAt']->modify('+' . $metaData['ttl'] . ' seconds'); + + return $expiresAt > $now; } - public function get(IdentityInterface $identifier): BlockInterface|null + public function get(BlockIdentity $identity): Block|null { - if (!$this->exists($identifier)) { + if (!$this->exists($identity)) { return null; } - $block = $this->blocks[(string) $identifier]; - - $block->touch(new DateTimeImmutable()); - - return $block; + return $this->blocks[(string) $identity]['block']; } } diff --git a/src/Storage/PredisStorage.php b/src/Storage/PredisStorage.php new file mode 100644 index 0000000..7fd41ce --- /dev/null +++ b/src/Storage/PredisStorage.php @@ -0,0 +1,129 @@ +getIdentity(); + + $data = json_encode([ + 'identity' => (string) $identity, + 'owner' => (string) $block->getOwner(), + ]); + + try { + $this->client->set($this->createKey($identity), $data, 'EX', $ttl); + } catch (PredisException) { + throw IOException::writeFailed((string) $identity); + } + + return true; + } + + public function touch(Block $block): bool + { + $identity = $block->getIdentity(); + + if (!$this->exists($identity)) { + return false; + } + + try { + $this->client->touch($this->createKey($identity)); + } catch (PredisException) { + throw IOException::touchFailed((string) $identity); + } + + return true; + } + + public function remove(Block $block): bool + { + $identity = $block->getIdentity(); + + if (!$this->exists($identity)) { + return false; + } + + try { + $this->client->del($this->createKey($identity)); + } catch (PredisException) { + throw IOException::removeFailed((string) $block->getIdentity()); + } + + return true; + } + + public function exists(BlockIdentity $identity): bool + { + return (bool) $this->client->exists($this->createKey($identity)); + } + + public function get(BlockIdentity $identity): Block|null + { + if (!$this->exists($identity)) { + return null; + } + + try { + $content = $this->client->get($this->createKey($identity)); + } catch (PredisException) { + throw IOException::getFailed((string) $identity); + } + + if (!$content) { + throw IOException::getFailed((string) $identity); + } + + $data = json_decode($content, true); + + assert(is_array($data)); + assert($data['identity'] ?? false); + assert($data['owner'] ?? false); + + return new Block( + new BlockIdentity($data['identity']), + new Owner($data['owner']), + ); + } + + private function createKey(BlockIdentity $identity): string + { + return $this->prefix . ':' . $identity; + } +} diff --git a/src/Storage/StorageInterface.php b/src/Storage/StorageInterface.php index 11c9e66..ef93c4e 100644 --- a/src/Storage/StorageInterface.php +++ b/src/Storage/StorageInterface.php @@ -13,21 +13,21 @@ namespace Brainbits\Blocking\Storage; -use Brainbits\Blocking\BlockInterface; -use Brainbits\Blocking\Identity\IdentityInterface; +use Brainbits\Blocking\Block; +use Brainbits\Blocking\Identity\BlockIdentity; /** * Block storage interface. */ interface StorageInterface { - public function write(BlockInterface $block): bool; + public function write(Block $block, int $ttl): bool; - public function touch(BlockInterface $block): bool; + public function touch(Block $block): bool; - public function remove(BlockInterface $block): bool; + public function remove(Block $block): bool; - public function exists(IdentityInterface $identifier): bool; + public function exists(BlockIdentity $identity): bool; - public function get(IdentityInterface $identifier): BlockInterface|null; + public function get(BlockIdentity $identity): Block|null; } diff --git a/src/Validator/AlwaysInvalidateValidator.php b/src/Validator/AlwaysInvalidateValidator.php index 565b43d..55cfcba 100644 --- a/src/Validator/AlwaysInvalidateValidator.php +++ b/src/Validator/AlwaysInvalidateValidator.php @@ -13,15 +13,15 @@ namespace Brainbits\Blocking\Validator; -use Brainbits\Blocking\BlockInterface; +use Brainbits\Blocking\Block; /** * Always invalidate validator. * This validator always invalidates an existing block. */ -class AlwaysInvalidateValidator implements ValidatorInterface +final readonly class AlwaysInvalidateValidator implements ValidatorInterface { - public function validate(BlockInterface $block): bool + public function validate(Block $block): bool { return false; } diff --git a/src/Validator/AlwaysValidateValidator.php b/src/Validator/AlwaysValidateValidator.php new file mode 100644 index 0000000..d6c18c6 --- /dev/null +++ b/src/Validator/AlwaysValidateValidator.php @@ -0,0 +1,28 @@ +getUpdatedAt(); - - $interval = $updatedAt->diff($now); - $diffInSeconds = $this->intervalToSeconds($interval); - - return $this->expireSeconds > $diffInSeconds; - } - - /** - * Calculate seconds from interval - */ - private function intervalToSeconds(DateInterval $interval): int - { - $seconds = (int) $interval->format('%s'); - - $multiplier = 60; - $seconds += (int) $interval->format('%i') * $multiplier; - - $multiplier *= 60; - $seconds += (int) $interval->format('%h') * $multiplier; - - $multiplier *= 24; - $seconds += (int) $interval->format('%d') * $multiplier; - - $multiplier *= 30; - $seconds += (int) $interval->format('%m') * $multiplier; - - $multiplier *= 12; - - return $seconds + (int) $interval->format('%y') * $multiplier; - } -} diff --git a/src/Validator/ValidatorInterface.php b/src/Validator/ValidatorInterface.php index 3ca0655..d09070c 100644 --- a/src/Validator/ValidatorInterface.php +++ b/src/Validator/ValidatorInterface.php @@ -13,7 +13,7 @@ namespace Brainbits\Blocking\Validator; -use Brainbits\Blocking\BlockInterface; +use Brainbits\Blocking\Block; /** * Block validator interface. @@ -25,5 +25,5 @@ interface ValidatorInterface * Validate block. * Return true if an existing block is valid, false if invalid. */ - public function validate(BlockInterface $block): bool; + public function validate(Block $block): bool; } diff --git a/tests/BlockTest.php b/tests/BlockTest.php index 25dcb28..cdb793a 100644 --- a/tests/BlockTest.php +++ b/tests/BlockTest.php @@ -1,5 +1,7 @@ identifier = $this->createMock(IdentityInterface::class); - - $this->owner = $this->createMock(OwnerInterface::class); - $this->owner->expects($this->any()) - ->method('__toString') - ->willReturn('dummyOwner'); + $this->identifier = new BlockIdentity('foo'); + $this->owner = new Owner('dummyOwner'); } public function testConstruct(): void @@ -65,66 +56,13 @@ public function testIsOwnedByReturnsTrue(): void { $block = new Block($this->identifier, $this->owner, new DateTimeImmutable()); - $this->owner->expects($this->once()) - ->method('equals') - ->with($this->owner) - ->willReturn(true); - $this->assertTrue($block->isOwnedBy($this->owner)); } public function testIsOwnedByReturnsFalse(): void { - $block = new Block($this->identifier, $this->owner, new DateTimeImmutable()); - - $owner = $this->createMock(OwnerInterface::class); - $this->owner->expects($this->any()) - ->method('__toString') - ->willReturn('dummyOwner'); - - $this->owner->expects($this->once()) - ->method('equals') - ->with($owner) - ->willReturn(false); - - $this->assertFalse($block->isOwnedBy($owner)); - } - - public function testGetCreatedAtReturnsCorrectValue(): void - { - $createdAt = new DateTimeImmutable(); - - $block = new Block($this->identifier, $this->owner, $createdAt); - $result = $block->getCreatedAt(); - - $this->assertInstanceOf(DateTimeImmutable::class, $result); - $this->assertSame($createdAt, $result); - } - - public function testGetUpdatedAtReturnsCreatedAtValueAfterInstanciation(): void - { - $createdAt = new DateTimeImmutable(); - - $block = new Block($this->identifier, $this->owner, $createdAt); - $result = $block->getUpdatedAt(); - - $this->assertInstanceOf(DateTimeImmutable::class, $result); - $this->assertSame($createdAt, $result); - } - - public function testTouchUpdatesValue(): void - { - $createdAt = new DateTimeImmutable(); - - $block = new Block($this->identifier, $this->owner, $createdAt); - - $updatedAt = new DateTimeImmutable(); - - $block->touch($updatedAt); - $result = $block->getUpdatedAt(); + $block = new Block($this->identifier, $this->owner); - $this->assertInstanceOf(DateTimeImmutable::class, $result); - $this->assertNotSame($createdAt, $result); - $this->assertSame($updatedAt, $result); + $this->assertFalse($block->isOwnedBy(new Owner('otherOwner'))); } } diff --git a/tests/BlockerTest.php b/tests/BlockerTest.php index 8615bd5..23efbdc 100644 --- a/tests/BlockerTest.php +++ b/tests/BlockerTest.php @@ -1,5 +1,7 @@ owner = new Owner('bar'); - $this->identifier = $this->createMock(IdentityInterface::class); - $this->identifier->expects($this->any()) - ->method('__toString') - ->willReturn('foo'); + $this->identifier = new BlockIdentity('foo'); - $this->ownerFactory = $this->createMock(OwnerFactoryInterface::class); - $this->ownerFactory->expects($this->any()) - ->method('createOwner') - ->willReturn($this->owner); + $this->ownerFactory = new ValueOwnerFactory('bar'); - $this->block = $this->createMock(BlockInterface::class); - $this->block->expects($this->any()) - ->method('getOwner') - ->willReturn(new Owner('baz')); + $this->block = new Block($this->identifier, new Owner('baz')); } public function testBlockReturnsBlockOnNonexistingBlock(): void @@ -70,11 +56,10 @@ public function testBlockReturnsBlockOnNonexistingBlock(): void $blocker = new Blocker( $storage, $this->ownerFactory, - $this->createInvalidValidator() ); $result = $blocker->block($this->identifier); - $this->assertInstanceOf(BlockInterface::class, $result); + $this->assertInstanceOf(Block::class, $result); } public function testBlockReturnsBlockOnExistingAndInvalidBlock(): void @@ -88,11 +73,11 @@ public function testBlockReturnsBlockOnExistingAndInvalidBlock(): void $blocker = new Blocker( $storage, $this->ownerFactory, - $this->createInvalidValidator() + new AlwaysInvalidateValidator(), ); $result = $blocker->block($this->identifier); - $this->assertInstanceOf(BlockInterface::class, $result); + $this->assertInstanceOf(Block::class, $result); } public function testBlockThrowsExceptionOnExistingAndValidAndNonOwnerBlock(): void @@ -103,15 +88,10 @@ public function testBlockThrowsExceptionOnExistingAndValidAndNonOwnerBlock(): vo $storage->expects($this->never()) ->method('write'); - $this->block->expects($this->once()) - ->method('isOwnedBy') - ->with($this->owner) - ->willReturn(false); - $blocker = new Blocker( $storage, $this->ownerFactory, - $this->createValidValidator() + new AlwaysValidateValidator(), ); $result = $blocker->block($this->identifier); @@ -125,19 +105,14 @@ public function testBlockUpdatesBlockOnExistingAndValidAndOwnedBlock(): void ->method('touch') ->with($this->block); - $this->block->expects($this->once()) - ->method('isOwnedBy') - ->with($this->owner) - ->willReturn(true); - $blocker = new Blocker( $storage, - $this->ownerFactory, - $this->createValidValidator() + new ValueOwnerFactory('baz'), + new AlwaysValidateValidator(), ); $result = $blocker->block($this->identifier); - $this->assertInstanceOf(BlockInterface::class, $result); + $this->assertInstanceOf(Block::class, $result); } public function testUnblockReturnsNullOnExistingAndInvalidBlock(): void @@ -149,7 +124,7 @@ public function testUnblockReturnsNullOnExistingAndInvalidBlock(): void $blocker = new Blocker( $storage, $this->ownerFactory, - $this->createInvalidValidator() + new AlwaysInvalidateValidator(), ); $result = $blocker->unblock($this->identifier); @@ -165,7 +140,6 @@ public function testUnblockReturnsBlockOnExistingAndValidBlock(): void $blocker = new Blocker( $storage, $this->ownerFactory, - $this->createValidValidator() ); $result = $blocker->unblock($this->identifier); @@ -181,7 +155,6 @@ public function testUnblockReturnsNullOnNonexistingBlock(): void $blocker = new Blocker( $storage, $this->ownerFactory, - $this->createInvalidValidator() ); $result = $blocker->unblock($this->identifier); @@ -194,7 +167,11 @@ public function testIsBlockedReturnsFalseOnExistingAndInvalidBlock(): void $storage->expects($this->once()) ->method('remove'); - $blocker = new Blocker($storage, $this->ownerFactory, $this->createInvalidValidator()); + $blocker = new Blocker( + $storage, + $this->ownerFactory, + new AlwaysInvalidateValidator(), + ); $result = $blocker->isBlocked($this->identifier); $this->assertFalse($result); @@ -205,7 +182,6 @@ public function testIsBlockedReturnsTrueOnExistingAndValidBlock(): void $blocker = new Blocker( $this->createExistingStorage(), $this->ownerFactory, - $this->createValidValidator() ); $result = $blocker->isBlocked($this->identifier); @@ -217,7 +193,6 @@ public function testIsBlockedReturnsFalseOnNonexistingBlock(): void $blocker = new Blocker( $this->createNonexistingStorage(), $this->ownerFactory, - $this->createValidValidator() ); $result = $blocker->isBlocked($this->identifier); @@ -229,17 +204,13 @@ public function testGetBlockReturnsBlockOnExistingBlock(): void $blocker = new Blocker( $this->createExistingStorage(), $this->ownerFactory, - $this->createValidValidator() ); $result = $blocker->getBlock($this->identifier); - $this->assertInstanceOf(BlockInterface::class, $result); + $this->assertInstanceOf(Block::class, $result); } - /** - * @return StorageInterface|MockObject - */ - private function createExistingStorage() + private function createExistingStorage(): StorageInterface|MockObject { $storage = $this->createMock(StorageInterface::class); $storage->expects($this->any()) @@ -252,10 +223,7 @@ private function createExistingStorage() return $storage; } - /** - * @return StorageInterface|MockObject - */ - private function createNonexistingStorage() + private function createNonexistingStorage(): StorageInterface|MockObject { $storage = $this->createMock(StorageInterface::class); $storage->expects($this->any()) @@ -267,30 +235,4 @@ private function createNonexistingStorage() return $storage; } - - /** - * @return ValidatorInterface|MockObject - */ - private function createInvalidValidator() - { - $validator = $this->createMock(ValidatorInterface::class); - $validator->expects($this->any()) - ->method('validate') - ->willReturn(false); - - return $validator; - } - - /** - * @return ValidatorInterface|MockObject - */ - private function createValidValidator() - { - $validator = $this->createMock(ValidatorInterface::class); - $validator->expects($this->any()) - ->method('validate') - ->willReturn(true); - - return $validator; - } } diff --git a/tests/Bundle/BrainbitsBlockingBundleTest.php b/tests/Bundle/BrainbitsBlockingBundleTest.php new file mode 100644 index 0000000..e5082ff --- /dev/null +++ b/tests/Bundle/BrainbitsBlockingBundleTest.php @@ -0,0 +1,27 @@ +assertInstanceOf(BrainbitsBlockingBundle::class, $bundle); + } +} diff --git a/tests/Bundle/Controller/BlockingControllerTest.php b/tests/Bundle/Controller/BlockingControllerTest.php new file mode 100644 index 0000000..6db74aa --- /dev/null +++ b/tests/Bundle/Controller/BlockingControllerTest.php @@ -0,0 +1,98 @@ +clock = new MockClock('now', new DateTimeZone('UTC')); + + $block = new Block( + new BlockIdentity('foo'), + new Owner('baz'), + ); + + $storage = new InMemoryStorage($this->clock); + $storage->addBlock($block, 10, $this->clock->now()); + $blocker = new Blocker( + $storage, + new ValueOwnerFactory('bar'), + ); + + $this->controller = new BlockingController($blocker); + } + + public function testBlockSuccessAction(): void + { + $response = $this->controller->blockAction('new'); + + $this->assertInstanceOf(JsonResponse::class, $response); + + $result = (array) json_decode((string) $response->getContent(), true); + + $this->assertTrue($result['success'] ?? null); + } + + public function testBlockFailureAction(): void + { + $response = $this->controller->blockAction('foo'); + + $this->assertInstanceOf(JsonResponse::class, $response); + + $result = (array) json_decode((string) $response->getContent(), true); + + $this->assertFalse($result['success'] ?? null); + } + + public function testUnblockSuccessAction(): void + { + $response = $this->controller->unblockAction('foo'); + + $this->assertInstanceOf(JsonResponse::class, $response); + + $result = (array) json_decode((string) $response->getContent(), true); + + $this->assertTrue($result['success'] ?? null); + } + + public function testUnblockFailureAction(): void + { + $response = $this->controller->unblockAction('new'); + + $this->assertInstanceOf(JsonResponse::class, $response); + + $result = (array) json_decode((string) $response->getContent(), true); + + $this->assertFalse($result['success'] ?? null); + } +} diff --git a/tests/Bundle/DependencyInjection/BrainbitsBlockingExtensionTest.php b/tests/Bundle/DependencyInjection/BrainbitsBlockingExtensionTest.php new file mode 100644 index 0000000..2c39b6f --- /dev/null +++ b/tests/Bundle/DependencyInjection/BrainbitsBlockingExtensionTest.php @@ -0,0 +1,108 @@ +load(); + + $this->assertContainerBuilderHasService('brainbits_blocking.storage', FilesystemStorage::class); + $this->assertContainerBuilderHasService('brainbits_blocking.owner_factory', SymfonySessionOwnerFactory::class); + $this->assertContainerBuilderHasService('brainbits_blocking.validator', ExpiredValidator::class); + $this->assertContainerBuilderHasParameter('brainbits_blocking.validator.expiration_time', 300); + $this->assertContainerBuilderHasParameter('brainbits_blocking.interval', 30); + } + + public function testContainerHasCustomParameters(): void + { + $this->load([ + 'storage' => ['driver' => 'in_memory'], + 'owner_factory' => [ + 'driver' => 'value', + 'value' => 'xx', + ], + 'validator' => ['expiration_time' => 8], + 'block_interval' => 9, + ]); + + $this->assertContainerBuilderHasService('brainbits_blocking.storage', InMemoryStorage::class); + $this->assertContainerBuilderHasService('brainbits_blocking.owner_factory', ValueOwnerFactory::class); + $this->assertContainerBuilderHasService('brainbits_blocking.validator', ExpiredValidator::class); + $this->assertContainerBuilderHasParameter('brainbits_blocking.validator.expiration_time', 8); + $this->assertContainerBuilderHasParameter('brainbits_blocking.interval', 9); + } + + public function testCustomStorageService(): void + { + $this->load([ + 'storage' => [ + 'driver' => 'custom', + 'service' => 'foo', + ], + ]); + + $this->assertContainerBuilderHasAlias('brainbits_blocking.storage', 'foo'); + } + + public function testPredisStorage(): void + { + $this->load([ + 'predis' => 'my_predis', + 'storage' => ['driver' => 'predis'], + ]); + + $this->assertContainerBuilderHasAlias('brainbits_blocking.predis', 'my_predis'); + } + + public function testCustomOwnerService(): void + { + $this->load([ + 'owner_factory' => [ + 'driver' => 'custom', + 'service' => 'bar', + ], + ]); + + $this->assertContainerBuilderHasAlias('brainbits_blocking.owner_factory', 'bar'); + } + + public function testCustomValidatorService(): void + { + $this->load([ + 'validator' => [ + 'driver' => 'custom', + 'service' => 'baz', + ], + ]); + + $this->assertContainerBuilderHasAlias('brainbits_blocking.validator', 'baz'); + } +} diff --git a/tests/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Bundle/DependencyInjection/ConfigurationTest.php new file mode 100644 index 0000000..25ae144 --- /dev/null +++ b/tests/Bundle/DependencyInjection/ConfigurationTest.php @@ -0,0 +1,323 @@ +assertConfigurationIsValid( + [ + [], // no values at all + ], + ); + } + + public function testDefaultValues(): void + { + $this->assertProcessedConfigurationEquals( + [ + [], // no values at all + ], + [ + 'storage' => [ + 'driver' => 'filesystem', + 'storage_dir' => '%kernel.cache_dir%/blocking/', + 'prefix' => 'block', + ], + 'owner_factory' => ['driver' => 'symfony_session'], + 'validator' => [ + 'driver' => 'expired', + 'expiration_time' => 300, + ], + 'block_interval' => 30, + ], + ); + } + + public function testProvidedValues(): void + { + $this->assertProcessedConfigurationEquals( + [ + [ + 'predis' => 'foo', + 'storage' => [ + 'driver' => 'in_memory', + 'storage_dir' => 'foo', + 'prefix' => 'block', + ], + 'owner_factory' => [ + 'driver' => 'value', + 'value' => 'bar', + ], + 'validator' => [ + 'driver' => 'always_invalidate', + 'expiration_time' => 99, + ], + 'block_interval' => 88, + ], + ], + [ + 'storage' => [ + 'driver' => 'in_memory', + 'storage_dir' => 'foo', + 'prefix' => 'block', + ], + 'owner_factory' => [ + 'driver' => 'value', + 'value' => 'bar', + ], + 'validator' => [ + 'driver' => 'always_invalidate', + 'expiration_time' => 99, + ], + 'block_interval' => 88, + 'predis' => 'foo', + ], + ); + } + + public function testInvalidStorageDriver(): void + { + $this->expectException(InvalidConfigurationException::class); + + $this->assertProcessedConfigurationEquals( + [ + [ + 'storage' => ['driver' => 'test'], + ], + ], + [], + ); + } + + public function testMissingCustomStorageService(): void + { + $this->expectException(InvalidConfigurationException::class); + // phpcs:ignore Generic.Files.LineLength.TooLong + $this->expectExceptionMessage('Invalid configuration for path "brainbits_blocking": You need to specify your own storage service when using the "custom" storage driver.'); + + $this->assertProcessedConfigurationEquals( + [ + [ + 'storage' => ['driver' => 'custom'], + ], + ], + [], + ); + } + + public function testCustomStorageService(): void + { + $this->assertProcessedConfigurationEquals( + [ + [ + 'storage' => [ + 'driver' => 'custom', + 'service' => 'my_service', + ], + ], + ], + [ + 'storage' => [ + 'driver' => 'custom', + 'storage_dir' => '%kernel.cache_dir%/blocking/', + 'service' => 'my_service', + 'prefix' => 'block', + ], + 'owner_factory' => ['driver' => 'symfony_session'], + 'validator' => [ + 'driver' => 'expired', + 'expiration_time' => 300, + ], + 'block_interval' => 30, + ], + ); + } + + public function testMissingPredisAlias(): void + { + $this->expectException(InvalidArgumentException::class); + // phpcs:ignore Generic.Files.LineLength.TooLong + $this->expectExceptionMessage('A predis alias has to be set for the predis storage driver.'); + + $this->assertProcessedConfigurationEquals( + [ + [ + 'storage' => ['driver' => 'predis'], + ], + ], + [], + ); + } + + public function testPredisAlias(): void + { + $this->assertProcessedConfigurationEquals( + [ + [ + 'predis' => 'my_predis', + 'storage' => ['driver' => 'predis'], + ], + ], + [ + 'storage' => [ + 'driver' => 'predis', + 'storage_dir' => '%kernel.cache_dir%/blocking/', + 'prefix' => 'block', + ], + 'owner_factory' => ['driver' => 'symfony_session'], + 'validator' => [ + 'driver' => 'expired', + 'expiration_time' => 300, + ], + 'block_interval' => 30, + 'predis' => 'my_predis', + ], + ); + } + + public function testInvalidOwnerDriver(): void + { + $this->expectException(InvalidConfigurationException::class); + + $this->assertProcessedConfigurationEquals( + [ + [ + 'owner_factory' => ['driver' => 'test'], + ], + ], + [], + ); + } + + public function testMissingCustomOwnerService(): void + { + $this->expectException(InvalidConfigurationException::class); + // phpcs:ignore Generic.Files.LineLength.TooLong + $this->expectExceptionMessage('Invalid configuration for path "brainbits_blocking": You need to specify your own owner_factory service when using the "custom" owner_factory driver.'); + + $this->assertProcessedConfigurationEquals( + [ + [ + 'owner_factory' => ['driver' => 'custom'], + ], + ], + [], + ); + } + + public function testCustomOwnerService(): void + { + $this->assertProcessedConfigurationEquals( + [ + [ + 'owner_factory' => [ + 'driver' => 'custom', + 'service' => 'foo', + ], + ], + ], + [ + 'storage' => [ + 'driver' => 'filesystem', + 'storage_dir' => '%kernel.cache_dir%/blocking/', + 'prefix' => 'block', + ], + 'owner_factory' => [ + 'driver' => 'custom', + 'service' => 'foo', + ], + 'validator' => [ + 'driver' => 'expired', + 'expiration_time' => 300, + ], + 'block_interval' => 30, + ], + ); + } + + public function testInvalidValidatorDriver(): void + { + $this->expectException(InvalidConfigurationException::class); + // phpcs:ignore Generic.Files.LineLength.TooLong + $this->expectExceptionMessage('Invalid configuration for path "brainbits_blocking.validator.driver": The validator driver "test" is not supported. Please choose one of ["expired","always_invalidate","custom"]'); + + $this->assertProcessedConfigurationEquals( + [ + [ + 'validator' => ['driver' => 'test'], + ], + ], + [], + ); + } + + public function testMissingCustomValidatorService(): void + { + $this->expectException(InvalidConfigurationException::class); + // phpcs:ignore Generic.Files.LineLength.TooLong + $this->expectExceptionMessage('Invalid configuration for path "brainbits_blocking": You need to specify your own validator service when using the "custom" validator driver.'); + + $this->assertProcessedConfigurationEquals( + [ + [ + 'validator' => ['driver' => 'custom'], + ], + ], + [], + ); + } + + public function testCustomValidatorService(): void + { + $this->assertProcessedConfigurationEquals( + [ + [ + 'validator' => [ + 'driver' => 'custom', + 'service' => 'foo', + ], + ], + ], + [ + 'storage' => [ + 'driver' => 'filesystem', + 'storage_dir' => '%kernel.cache_dir%/blocking/', + 'prefix' => 'block', + ], + 'owner_factory' => ['driver' => 'symfony_session'], + 'validator' => [ + 'driver' => 'custom', + 'service' => 'foo', + 'expiration_time' => 300, + ], + 'block_interval' => 30, + ], + ); + } +} diff --git a/tests/Functional/FilesystemTest.php b/tests/Functional/FilesystemTest.php new file mode 100644 index 0000000..687ed60 --- /dev/null +++ b/tests/Functional/FilesystemTest.php @@ -0,0 +1,155 @@ +clock = new MockClock(); + + vfsStream::setup('blockDir'); + $this->root = vfsStream::url('blockDir'); + } + + public function testSelfBlock(): void + { + $blocker = new Blocker( + new FilesystemStorage($this->clock, $this->root), + new ValueOwnerFactory('my_owner'), + ); + + $this->assertFalse($blocker->isBlocked(new BlockIdentity('my_item'))); + + $blocker->block(new BlockIdentity('my_item')); + $blocker->block(new BlockIdentity('my_item')); + $blocker->block(new BlockIdentity('my_item')); + + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item'))); + + $blocker->unblock(new BlockIdentity('my_item')); + + $this->assertFalse($blocker->isBlocked(new BlockIdentity('my_item'))); + } + + public function testOtherBlock(): void + { + $this->expectException(BlockFailedException::class); + $this->expectExceptionMessage('Identifier my_item is already blocked.'); + + file_put_contents( + $this->root . '/my_item', + json_encode(['identity' => 'my_item', 'owner' => 'other_owner']), + ); + file_put_contents( + $this->root . '/my_item.meta', + json_encode(['ttl' => 10, 'updatedAt' => $this->clock->now()->format('c')]), + ); + + $blocker = new Blocker( + new FilesystemStorage($this->clock, $this->root), + new ValueOwnerFactory('my_owner'), + ); + + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item'))); + + $blocker->block(new BlockIdentity('my_item')); + } + + public function testOtherBlockWithRemove(): void + { + file_put_contents( + $this->root . '/my_item', + json_encode(['identity' => 'my_item', 'owner' => 'other_owner']), + ); + file_put_contents( + $this->root . '/my_item.meta', + json_encode(['ttl' => 10, 'updatedAt' => $this->clock->now()->format('c')]), + ); + + $blocker = new Blocker( + new FilesystemStorage($this->clock, $this->root), + new ValueOwnerFactory('my_owner'), + ); + + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item'))); + + $blocker->unblock(new BlockIdentity('my_item')); + + $this->assertFalse($blocker->isBlocked(new BlockIdentity('my_item'))); + + $blocker->block(new BlockIdentity('my_item')); + + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item'))); + } + + public function testMultipleBlocks(): void + { + $blocker = new Blocker( + new FilesystemStorage($this->clock, $this->root), + new ValueOwnerFactory('my_owner'), + ); + + $this->assertFalse($blocker->isBlocked(new BlockIdentity('my_item_1'))); + $this->assertFalse($blocker->isBlocked(new BlockIdentity('my_item_2'))); + $this->assertFalse($blocker->isBlocked(new BlockIdentity('my_item_3'))); + + $blocker->block(new BlockIdentity('my_item_1')); + $blocker->block(new BlockIdentity('my_item_2')); + $blocker->block(new BlockIdentity('my_item_3')); + + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item_1'))); + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item_2'))); + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item_3'))); + } + + public function testExpiredBlock(): void + { + file_put_contents( + $this->root . '/my_item', + json_encode(['identity' => 'my_item', 'owner' => 'other_owner']), + ); + file_put_contents( + $this->root . '/my_item.meta', + json_encode(['ttl' => 10, 'updatedAt' => $this->clock->now()->modify('-1 minute')->format('c')]), + ); + + $blocker = new Blocker( + new FilesystemStorage($this->clock, $this->root), + new ValueOwnerFactory('my_owner'), + ); + + $this->assertFalse($blocker->isBlocked(new BlockIdentity('my_item'))); + + $blocker->block(new BlockIdentity('my_item')); + + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item'))); + } +} diff --git a/tests/Functional/InMemoryTest.php b/tests/Functional/InMemoryTest.php new file mode 100644 index 0000000..de22c5a --- /dev/null +++ b/tests/Functional/InMemoryTest.php @@ -0,0 +1,144 @@ +clock = new MockClock('now', new DateTimeZone('UTC')); + } + + public function testSelfBlock(): void + { + $blocker = new Blocker( + new InMemoryStorage($this->clock), + new ValueOwnerFactory('my_owner'), + ); + + $this->assertFalse($blocker->isBlocked(new BlockIdentity('my_item'))); + + $blocker->block(new BlockIdentity('my_item')); + $blocker->block(new BlockIdentity('my_item')); + $blocker->block(new BlockIdentity('my_item')); + + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item'))); + + $blocker->unblock(new BlockIdentity('my_item')); + + $this->assertFalse($blocker->isBlocked(new BlockIdentity('my_item'))); + } + + public function testOtherBlock(): void + { + $this->expectException(BlockFailedException::class); + $this->expectExceptionMessage('Identifier my_item is already blocked.'); + + $storage = new InMemoryStorage($this->clock); + $storage->addBlock( + new Block(new BlockIdentity('my_item'), new Owner('other_owner')), + 10, + $this->clock->now(), + ); + + $blocker = new Blocker( + $storage, + new ValueOwnerFactory('my_owner'), + ); + + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item'))); + + $blocker->block(new BlockIdentity('my_item')); + } + + public function testOtherBlockWithRemove(): void + { + $storage = new InMemoryStorage($this->clock); + $storage->addBlock( + new Block(new BlockIdentity('my_item'), new Owner('other_owner')), + 10, + $this->clock->now(), + ); + + $blocker = new Blocker( + $storage, + new ValueOwnerFactory('my_owner'), + ); + + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item'))); + + $blocker->unblock(new BlockIdentity('my_item')); + + $this->assertFalse($blocker->isBlocked(new BlockIdentity('my_item'))); + + $blocker->block(new BlockIdentity('my_item')); + + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item'))); + } + + public function testMultipleBlocks(): void + { + $blocker = new Blocker( + new InMemoryStorage($this->clock), + new ValueOwnerFactory('my_owner'), + ); + + $this->assertFalse($blocker->isBlocked(new BlockIdentity('my_item_1'))); + $this->assertFalse($blocker->isBlocked(new BlockIdentity('my_item_2'))); + $this->assertFalse($blocker->isBlocked(new BlockIdentity('my_item_3'))); + + $blocker->block(new BlockIdentity('my_item_1')); + $blocker->block(new BlockIdentity('my_item_2')); + $blocker->block(new BlockIdentity('my_item_3')); + + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item_1'))); + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item_2'))); + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item_3'))); + } + + public function testExpiredBlock(): void + { + $storage = new InMemoryStorage($this->clock); + $storage->addBlock( + new Block(new BlockIdentity('my_item'), new Owner('other_owner')), + 10, + $this->clock->now()->modify('-1 minute'), + ); + + $blocker = new Blocker( + $storage, + new ValueOwnerFactory('my_owner'), + ); + + $this->assertFalse($blocker->isBlocked(new BlockIdentity('my_item'))); + + $blocker->block(new BlockIdentity('my_item')); + + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item'))); + } +} diff --git a/tests/Functional/PredisTest.php b/tests/Functional/PredisTest.php new file mode 100644 index 0000000..e58154f --- /dev/null +++ b/tests/Functional/PredisTest.php @@ -0,0 +1,145 @@ +clock = new MockClock(); + + if (!getenv('REDIS_DSN')) { + $this->markTestSkipped('REDIS_DSN is needed for PredisTest'); + } + + $this->client = new Client(getenv('REDIS_DSN')); + $this->client->flushall(); + } + + public function testSelfBlock(): void + { + $blocker = new Blocker( + new PredisStorage($this->client), + new ValueOwnerFactory('my_owner'), + ); + + $this->assertFalse($blocker->isBlocked(new BlockIdentity('my_item'))); + + $blocker->block(new BlockIdentity('my_item')); + $blocker->block(new BlockIdentity('my_item')); + $blocker->block(new BlockIdentity('my_item')); + + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item'))); + + $blocker->unblock(new BlockIdentity('my_item')); + + $this->assertFalse($blocker->isBlocked(new BlockIdentity('my_item'))); + } + + public function testOtherBlock(): void + { + $this->expectException(BlockFailedException::class); + $this->expectExceptionMessage('Identifier my_item is already blocked.'); + + $this->client->set('block:my_item', json_encode(['identity' => 'my_item', 'owner' => 'other_owner'])); + + $blocker = new Blocker( + new PredisStorage($this->client), + new ValueOwnerFactory('my_owner'), + ); + + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item'))); + + $blocker->block(new BlockIdentity('my_item')); + } + + public function testOtherBlockWithRemove(): void + { + $this->client->set('block:my_item', json_encode(['identity' => 'my_item', 'owner' => 'other_owner'])); + + $blocker = new Blocker( + new PredisStorage($this->client), + new ValueOwnerFactory('my_owner'), + ); + + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item'))); + + $blocker->unblock(new BlockIdentity('my_item')); + + $this->assertFalse($blocker->isBlocked(new BlockIdentity('my_item'))); + + $blocker->block(new BlockIdentity('my_item')); + + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item'))); + } + + public function testMultipleBlocks(): void + { + $blocker = new Blocker( + new PredisStorage($this->client), + new ValueOwnerFactory('my_owner'), + ); + + $this->assertFalse($blocker->isBlocked(new BlockIdentity('my_item_1'))); + $this->assertFalse($blocker->isBlocked(new BlockIdentity('my_item_2'))); + $this->assertFalse($blocker->isBlocked(new BlockIdentity('my_item_3'))); + + $blocker->block(new BlockIdentity('my_item_1')); + $blocker->block(new BlockIdentity('my_item_2')); + $blocker->block(new BlockIdentity('my_item_3')); + + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item_1'))); + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item_2'))); + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item_3'))); + } + + public function testExpiredBlock(): void + { + $this->client->set( + 'block:my_item', + json_encode(['identity' => 'my_item', 'owner' => 'other_owner']), + 'EXAT', + $this->clock->now()->modify('-1 minute')->format('U'), + ); + + $blocker = new Blocker( + new PredisStorage($this->client), + new ValueOwnerFactory('my_owner'), + ); + + $this->assertFalse($blocker->isBlocked(new BlockIdentity('my_item'))); + + $blocker->block(new BlockIdentity('my_item')); + + $this->assertTrue($blocker->isBlocked(new BlockIdentity('my_item'))); + } +} diff --git a/tests/Identifier/IdentifierTest.php b/tests/Identifier/BlockIdentifierTest.php similarity index 62% rename from tests/Identifier/IdentifierTest.php rename to tests/Identifier/BlockIdentifierTest.php index 1efde8f..4d0f1ce 100644 --- a/tests/Identifier/IdentifierTest.php +++ b/tests/Identifier/BlockIdentifierTest.php @@ -1,5 +1,7 @@ assertInstanceOf(Identity::class, $identifier); + $this->assertInstanceOf(BlockIdentity::class, $identifier); } public function testEquals(): void { - $identifier1 = new Identity('foo'); - $identifier2 = new Identity('foo'); - $identifier3 = new Identity('bar'); + $identifier1 = new BlockIdentity('foo'); + $identifier2 = new BlockIdentity('foo'); + $identifier3 = new BlockIdentity('bar'); $this->assertTrue($identifier1->equals($identifier2)); $this->assertFalse($identifier1->equals($identifier3)); @@ -39,7 +38,7 @@ public function testEquals(): void public function testToString(): void { - $identifier = new Identity('test_123'); + $identifier = new BlockIdentity('test_123'); $this->assertEquals('test_123', $identifier); } diff --git a/tests/Owner/OwnerTest.php b/tests/Owner/OwnerTest.php index c000997..661daf9 100644 --- a/tests/Owner/OwnerTest.php +++ b/tests/Owner/OwnerTest.php @@ -1,5 +1,7 @@ assertEquals($owner, new Owner('foo')); } - /** - * @return SessionInterface|MockObject - */ - private function createSession($sessionId) + private function createSession(string $sessionId): SessionInterface|MockObject { $session = $this->createMock(SessionInterface::class); $session->expects($this->once()) diff --git a/tests/Owner/SymfonyTokenOwnerFactoryTest.php b/tests/Owner/SymfonyTokenOwnerFactoryTest.php index 2941365..6bc6737 100644 --- a/tests/Owner/SymfonyTokenOwnerFactoryTest.php +++ b/tests/Owner/SymfonyTokenOwnerFactoryTest.php @@ -1,5 +1,7 @@ assertEquals($owner, new Owner('foo')); } - public function testE(): void + public function testNotTokenFoundExceptionIsThrown(): void { $this->expectException(NoTokenFoundException::class); @@ -42,7 +41,7 @@ public function testE(): void $factory->createOwner(); } - public function testE2(): void + public function testNotUserFoundExceptionIsThrown(): void { $this->expectException(NoUserFoundException::class); @@ -51,11 +50,11 @@ public function testE2(): void $factory->createOwner(); } - /** - * @return TokenStorageInterface|MockObject - */ - private function createTokenStorage(string $username, bool $createToken = true, bool $createUser = true) - { + private function createTokenStorage( + string $username, + bool $createToken = true, + bool $createUser = true, + ): TokenStorageInterface|MockObject { $user = $this->createMock(UserInterface::class); $user->expects($this->any()) ->method('getUserIdentifier') diff --git a/tests/Owner/ValueOwnerFactoryTest.php b/tests/Owner/ValueOwnerFactoryTest.php index b2ce108..08d5293 100644 --- a/tests/Owner/ValueOwnerFactoryTest.php +++ b/tests/Owner/ValueOwnerFactoryTest.php @@ -1,5 +1,7 @@ clock = new MockClock(); + vfsStream::setup('blockDir'); $this->root = vfsStream::url('blockDir'); - $this->storage = new FilesystemStorage($this->root); + $this->storage = new FilesystemStorage($this->clock, $this->root); $this->owner = new Owner('dummyOwner'); } public function testWriteSucceedesOnNewFile(): void { - $identifier = new Identity('test_block'); - $block = new Block($identifier, $this->owner, new DateTimeImmutable()); + $identifier = new BlockIdentity('test_block'); + $block = new Block($identifier, $this->owner); - $result = $this->storage->write($block); + $result = $this->storage->write($block, 10); $this->assertTrue($result); $this->assertTrue(file_exists(vfsStream::url('blockDir/' . $identifier))); @@ -69,12 +65,15 @@ public function testWriteFailsOnNonExistantDirectoryInNonWritableDirectory(): vo $this->expectException(DirectoryNotWritableException::class); vfsStream::setup('nonWritableDir', 0); - $adapter = new FilesystemStorage(vfsStream::url('nonWritableDir/blockDir')); + $adapter = new FilesystemStorage( + $this->clock, + vfsStream::url('nonWritableDir/blockDir'), + ); - $identifier = new Identity('test_lock'); - $block = new Block($identifier, $this->owner, new DateTimeImmutable()); + $identifier = new BlockIdentity('test_lock'); + $block = new Block($identifier, $this->owner); - $adapter->write($block); + $adapter->write($block, 10); } public function testWriteFailsOnNonWritableDirectory(): void @@ -84,21 +83,24 @@ public function testWriteFailsOnNonWritableDirectory(): void vfsStream::setup('writableDir'); mkdir(vfsStream::url('writableDir/nonWritableBlockDir'), 0); - $adapter = new FilesystemStorage(vfsStream::url('writableDir/nonWritableBlockDir')); + $adapter = new FilesystemStorage( + $this->clock, + vfsStream::url('writableDir/nonWritableBlockDir'), + ); - $identifier = new Identity('test_lock'); - $block = new Block($identifier, $this->owner, new DateTimeImmutable()); + $identifier = new BlockIdentity('test_lock'); + $block = new Block($identifier, $this->owner); - $adapter->write($block); + $adapter->write($block, 10); } public function testTouchSucceedesOnExistingFile(): void { - $identifier = new Identity('test_lock'); - $block = new Block($identifier, $this->owner, new DateTimeImmutable()); + $identifier = new BlockIdentity('test_lock'); + $block = new Block($identifier, $this->owner); - $this->storage->write($block); - $result = $this->storage->touch($block); + $this->storage->write($block, 10); + $result = $this->storage->touch($block, 10); $this->assertTrue($result); $this->assertTrue(file_exists(vfsStream::url('blockDir/' . $identifier))); @@ -106,8 +108,8 @@ public function testTouchSucceedesOnExistingFile(): void public function testRemoveReturnsFalseOnNonexistingFile(): void { - $identifier = new Identity('test_unlock'); - $block = new Block($identifier, $this->owner, new DateTimeImmutable()); + $identifier = new BlockIdentity('test_unlock'); + $block = new Block($identifier, $this->owner); $result = $this->storage->remove($block); @@ -117,10 +119,10 @@ public function testRemoveReturnsFalseOnNonexistingFile(): void public function testUnblockReturnsTrueOnExistingFile(): void { - $identifier = new Identity('test_unlock'); - $block = new Block($identifier, $this->owner, new DateTimeImmutable()); + $identifier = new BlockIdentity('test_unlock'); + $block = new Block($identifier, $this->owner); - $this->storage->write($block); + $this->storage->write($block, 10); $result = $this->storage->remove($block); $this->assertTrue($result); @@ -129,7 +131,7 @@ public function testUnblockReturnsTrueOnExistingFile(): void public function testExistsReturnsFalseOnNonexistingFile(): void { - $identifier = new Identity('test_isblocked'); + $identifier = new BlockIdentity('test_isblocked'); $result = $this->storage->exists($identifier); @@ -138,10 +140,10 @@ public function testExistsReturnsFalseOnNonexistingFile(): void public function testIsBlockedReturnsTrueOnExistingBlock(): void { - $identifier = new Identity('test_isblocked'); - $block = new Block($identifier, $this->owner, new DateTimeImmutable()); + $identifier = new BlockIdentity('test_isblocked'); + $block = new Block($identifier, $this->owner); - $this->storage->write($block); + $this->storage->write($block, 10); $result = $this->storage->exists($identifier); $this->assertTrue($result); @@ -149,7 +151,7 @@ public function testIsBlockedReturnsTrueOnExistingBlock(): void public function testGetReturnsNullOnNonexistingFile(): void { - $identifier = new Identity('test_isblocked'); + $identifier = new BlockIdentity('test_isblocked'); $result = $this->storage->get($identifier); @@ -158,13 +160,13 @@ public function testGetReturnsNullOnNonexistingFile(): void public function testGetReturnsBlockOnExistingFile(): void { - $identifier = new Identity('test_isblocked'); - $block = new Block($identifier, $this->owner, new DateTimeImmutable()); + $identifier = new BlockIdentity('test_isblocked'); + $block = new Block($identifier, $this->owner); - $this->storage->write($block); + $this->storage->write($block, 10); $result = $this->storage->get($identifier); $this->assertNotNull($result); - $this->assertInstanceOf(BlockInterface::class, $result); + $this->assertInstanceOf(Block::class, $result); } } diff --git a/tests/Storage/InMemoryStorageTest.php b/tests/Storage/InMemoryStorageTest.php index f1838ed..e601806 100644 --- a/tests/Storage/InMemoryStorageTest.php +++ b/tests/Storage/InMemoryStorageTest.php @@ -1,5 +1,7 @@ storage = new InMemoryStorage(); + $this->clock = new MockClock(); + + $this->storage = new InMemoryStorage($this->clock); - $this->owner = $this->createMock(OwnerInterface::class); - $this->owner->expects($this->any()) - ->method('__toString') - ->willReturn('dummyOwner'); + $this->owner = new Owner('dummyOwner'); } - public function testConstructWithBlock(): void + public function testConstructWithAdd(): void { - $identifier = new Identity('test_block'); - $block = new Block($identifier, $this->owner, new DateTimeImmutable()); + $identifier = new BlockIdentity('test_block'); + $block = new Block($identifier, $this->owner); - $storage = new InMemoryStorage($block); + $storage = new InMemoryStorage($this->clock); + $storage->addBlock($block, 60, new DateTimeImmutable()); $result = $storage->exists($identifier); @@ -56,29 +54,29 @@ public function testConstructWithBlock(): void public function testWriteSucceedesOnNewBlock(): void { - $identifier = new Identity('test_block'); - $block = new Block($identifier, $this->owner, new DateTimeImmutable()); + $identifier = new BlockIdentity('test_block'); + $block = new Block($identifier, $this->owner); - $result = $this->storage->write($block); + $result = $this->storage->write($block, 10); $this->assertTrue($result); } public function testTouchSucceedesOnExistingBlock(): void { - $identifier = new Identity('test_lock'); - $block = new Block($identifier, $this->owner, new DateTimeImmutable()); + $identifier = new BlockIdentity('test_lock'); + $block = new Block($identifier, $this->owner); - $this->storage->write($block); - $result = $this->storage->touch($block); + $this->storage->write($block, 10); + $result = $this->storage->touch($block, 10); $this->assertTrue($result); } public function testRemoveReturnsFalseOnNonexistingBlock(): void { - $identifier = new Identity('test_unlock'); - $block = new Block($identifier, $this->owner, new DateTimeImmutable()); + $identifier = new BlockIdentity('test_unlock'); + $block = new Block($identifier, $this->owner); $result = $this->storage->remove($block); @@ -87,10 +85,10 @@ public function testRemoveReturnsFalseOnNonexistingBlock(): void public function testUnblockReturnsTrueOnExistingBlock(): void { - $identifier = new Identity('test_unlock'); - $block = new Block($identifier, $this->owner, new DateTimeImmutable()); + $identifier = new BlockIdentity('test_unlock'); + $block = new Block($identifier, $this->owner); - $this->storage->write($block); + $this->storage->write($block, 10); $result = $this->storage->remove($block); $this->assertTrue($result); @@ -98,7 +96,7 @@ public function testUnblockReturnsTrueOnExistingBlock(): void public function testExistsReturnsFalseOnNonexistingBlock(): void { - $identifier = new Identity('test_isblocked'); + $identifier = new BlockIdentity('test_isblocked'); $result = $this->storage->exists($identifier); @@ -107,10 +105,10 @@ public function testExistsReturnsFalseOnNonexistingBlock(): void public function testIsBlockedReturnsTrueOnExistingBlock(): void { - $identifier = new Identity('test_isblocked'); - $block = new Block($identifier, $this->owner, new DateTimeImmutable()); + $identifier = new BlockIdentity('test_isblocked'); + $block = new Block($identifier, $this->owner); - $this->storage->write($block); + $this->storage->write($block, 10); $result = $this->storage->exists($identifier); $this->assertTrue($result); @@ -118,7 +116,7 @@ public function testIsBlockedReturnsTrueOnExistingBlock(): void public function testGetReturnsNullOnNonexistingFile(): void { - $identifier = new Identity('test_isblocked'); + $identifier = new BlockIdentity('test_isblocked'); $result = $this->storage->get($identifier); @@ -127,13 +125,13 @@ public function testGetReturnsNullOnNonexistingFile(): void public function testGetReturnsBlockOnExistingFile(): void { - $identifier = new Identity('test_isblocked'); - $block = new Block($identifier, $this->owner, new DateTimeImmutable()); + $identifier = new BlockIdentity('test_isblocked'); + $block = new Block($identifier, $this->owner); - $this->storage->write($block); + $this->storage->write($block, 10); $result = $this->storage->get($identifier); $this->assertNotNull($result); - $this->assertInstanceOf(BlockInterface::class, $result); + $this->assertInstanceOf(Block::class, $result); } } diff --git a/tests/Validator/AlwaysInvalidateValidatorTest.php b/tests/Validator/AlwaysInvalidateValidatorTest.php index 4d28b95..7d53a66 100644 --- a/tests/Validator/AlwaysInvalidateValidatorTest.php +++ b/tests/Validator/AlwaysInvalidateValidatorTest.php @@ -1,5 +1,7 @@ createMock(BlockInterface::class); - $blockMock->expects($this->any()) - ->method('getUpdatedAt') - ->willReturn(new DateTimeImmutable('30 seconds ago')); - - $validator = new AlwaysInvalidateValidator(20); - $this->assertFalse($validator->validate($blockMock)); - - $validator = new AlwaysInvalidateValidator(30); - $this->assertFalse($validator->validate($blockMock)); - - $validator = new AlwaysInvalidateValidator(60); - $this->assertFalse($validator->validate($blockMock)); - } - - public function testValidateBlockLastUpdatedOneMinuteAgo(): void + public function testValidateBlock(): void { - $blockMock = $this->createMock(BlockInterface::class); - $blockMock->expects($this->any()) - ->method('getUpdatedAt') - ->willReturn(new DateTimeImmutable('1 minute ago')); - - $validator = new AlwaysInvalidateValidator(30); - $this->assertFalse($validator->validate($blockMock)); - - $validator = new AlwaysInvalidateValidator(60); - $this->assertFalse($validator->validate($blockMock)); - - $validator = new AlwaysInvalidateValidator(90); - $this->assertFalse($validator->validate($blockMock)); - } - - public function testValidateBlockLastUpdatedOneHourAo(): void - { - $blockMock = $this->createMock(BlockInterface::class); - $blockMock->expects($this->any()) - ->method('getUpdatedAt') - ->willReturn(new DateTimeImmutable('1 hour ago')); - - $validator = new AlwaysInvalidateValidator(1800); - $this->assertFalse($validator->validate($blockMock)); - - $validator = new AlwaysInvalidateValidator(3600); - $this->assertFalse($validator->validate($blockMock)); + $block = new Block(new BlockIdentity('foo'), new Owner('bar')); - $validator = new AlwaysInvalidateValidator(5400); - $this->assertFalse($validator->validate($blockMock)); + $validator = new AlwaysInvalidateValidator(); + $this->assertFalse($validator->validate($block)); } } diff --git a/tests/Validator/AlwaysValidateValidatorTest.php b/tests/Validator/AlwaysValidateValidatorTest.php new file mode 100644 index 0000000..05c4483 --- /dev/null +++ b/tests/Validator/AlwaysValidateValidatorTest.php @@ -0,0 +1,31 @@ +assertTrue($validator->validate($block)); + } +} diff --git a/tests/Validator/ExpiredValidatorTest.php b/tests/Validator/ExpiredValidatorTest.php deleted file mode 100644 index 907358e..0000000 --- a/tests/Validator/ExpiredValidatorTest.php +++ /dev/null @@ -1,74 +0,0 @@ -createMock(BlockInterface::class); - $block->expects($this->any()) - ->method('getUpdatedAt') - ->willReturn(new DateTimeImmutable('30 seconds ago')); - - $validator = new ExpiredValidator(20); - $this->assertFalse($validator->validate($block)); - - $validator = new ExpiredValidator(30); - $this->assertFalse($validator->validate($block)); - - $validator = new ExpiredValidator(60); - $this->assertTrue($validator->validate($block)); - } - - public function testValidateBlockLastUpdatedOneMinuteAgo(): void - { - $block = $this->createMock(BlockInterface::class); - $block->expects($this->any()) - ->method('getUpdatedAt') - ->willReturn(new DateTimeImmutable('1 minute ago')); - - $validator = new ExpiredValidator(30); - $this->assertFalse($validator->validate($block)); - - $validator = new ExpiredValidator(60); - $this->assertFalse($validator->validate($block)); - - $validator = new ExpiredValidator(90); - $this->assertTrue($validator->validate($block)); - } - - public function testValidateBlockLastUpdatedOneHourAo(): void - { - $block = $this->createMock(BlockInterface::class); - $block->expects($this->any()) - ->method('getUpdatedAt') - ->willReturn(new DateTimeImmutable('1 hour ago')); - - $validator = new ExpiredValidator(1800); - $this->assertFalse($validator->validate($block)); - - $validator = new ExpiredValidator(3600); - $this->assertFalse($validator->validate($block)); - - $validator = new ExpiredValidator(5400); - $this->assertTrue($validator->validate($block)); - } -} From d72306c76a0bf3029ee1b3df59fd5f9373bc50ec Mon Sep 17 00:00:00 2001 From: Stephan Wentz Date: Thu, 20 Jul 2023 15:00:23 +0200 Subject: [PATCH 2/3] fixup! fix: Add redis storage support, add bundle and deprecate brainbits/blocking-bundle, require psr/clock, rework expiration to ttl mechanism implemented by storage --- src/Blocker.php | 20 +-- .../BrainbitsBlockingExtension.php | 16 +-- .../DependencyInjection/Configuration.php | 27 ----- src/Bundle/Resources/config/services.yaml | 1 - .../config/validator/always_invalidate.xml | 13 -- .../Resources/config/validator/expired.xml | 16 --- src/Validator/AlwaysInvalidateValidator.php | 28 ----- src/Validator/AlwaysValidateValidator.php | 28 ----- src/Validator/ValidatorInterface.php | 29 ----- tests/BlockerTest.php | 114 ------------------ .../BrainbitsBlockingExtensionTest.php | 18 --- .../DependencyInjection/ConfigurationTest.php | 84 ------------- .../AlwaysInvalidateValidatorTest.php | 31 ----- .../Validator/AlwaysValidateValidatorTest.php | 31 ----- 14 files changed, 3 insertions(+), 453 deletions(-) delete mode 100644 src/Bundle/Resources/config/validator/always_invalidate.xml delete mode 100644 src/Bundle/Resources/config/validator/expired.xml delete mode 100644 src/Validator/AlwaysInvalidateValidator.php delete mode 100644 src/Validator/AlwaysValidateValidator.php delete mode 100644 src/Validator/ValidatorInterface.php delete mode 100644 tests/Validator/AlwaysInvalidateValidatorTest.php delete mode 100644 tests/Validator/AlwaysValidateValidatorTest.php diff --git a/src/Blocker.php b/src/Blocker.php index 44881d6..8f3d9c1 100644 --- a/src/Blocker.php +++ b/src/Blocker.php @@ -17,14 +17,12 @@ use Brainbits\Blocking\Identity\BlockIdentity; use Brainbits\Blocking\Owner\OwnerFactoryInterface; use Brainbits\Blocking\Storage\StorageInterface; -use Brainbits\Blocking\Validator\ValidatorInterface; final readonly class Blocker { public function __construct( private StorageInterface $storage, private OwnerFactoryInterface $ownerFactory, - private ValidatorInterface|null $validator = null, private int $defaultTtl = 60, ) { } @@ -79,23 +77,7 @@ public function isBlocked(BlockIdentity $identifier): bool { $block = $this->storage->get($identifier); - if (!$block) { - return false; - } - - if (!$this->validator) { - return true; - } - - $valid = $this->validator->validate($block); - - if ($valid) { - return true; - } - - $this->storage->remove($block); - - return false; + return $block !== null; } public function getBlock(BlockIdentity $identifier): Block|null diff --git a/src/Bundle/DependencyInjection/BrainbitsBlockingExtension.php b/src/Bundle/DependencyInjection/BrainbitsBlockingExtension.php index 23ef17c..525031d 100644 --- a/src/Bundle/DependencyInjection/BrainbitsBlockingExtension.php +++ b/src/Bundle/DependencyInjection/BrainbitsBlockingExtension.php @@ -43,6 +43,8 @@ public function load(array $configs, ContainerBuilder $container): void $container->setAlias('brainbits_blocking.predis', $config['predis']); } + $container->setParameter('brainbits_blocking.interval', $config['block_interval']); + if (isset($config['storage']['storage_dir'])) { $container->setParameter( 'brainbits_blocking.storage.storage_dir', @@ -64,20 +66,6 @@ public function load(array $configs, ContainerBuilder $container): void ); } - if (isset($config['validator']['expiration_time'])) { - $container->setParameter( - 'brainbits_blocking.validator.expiration_time', - $config['validator']['expiration_time'], - ); - $container->setParameter('brainbits_blocking.interval', $config['block_interval']); - } - - if ($config['validator']['driver'] !== 'custom') { - $xmlLoader->load(sprintf('validator/%s.xml', $config['validator']['driver'])); - } else { - $container->setAlias('brainbits_blocking.validator', $config['validator']['service']); - } - if ($config['storage']['driver'] !== 'custom') { $xmlLoader->load(sprintf('storage/%s.xml', $config['storage']['driver'])); } else { diff --git a/src/Bundle/DependencyInjection/Configuration.php b/src/Bundle/DependencyInjection/Configuration.php index 3ac154d..96ea77e 100644 --- a/src/Bundle/DependencyInjection/Configuration.php +++ b/src/Bundle/DependencyInjection/Configuration.php @@ -30,7 +30,6 @@ public function getConfigTreeBuilder(): TreeBuilder $storageDrivers = ['filesystem', 'predis', 'in_memory', 'custom']; $ownerFactoryDrivers = ['symfony_session', 'symfony_token', 'value', 'custom']; - $validatorDrivers = ['expired', 'always_invalidate', 'custom']; $rootNode ->beforeNormalization() @@ -86,24 +85,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->scalarNode('value')->end() ->end() ->end() - ->arrayNode('validator') - ->addDefaultsIfNotSet() - ->children() - ->scalarNode('driver') - ->validate() - ->ifNotInArray($validatorDrivers) - ->thenInvalid( - 'The validator driver %s is not supported. Please choose one of ' . - json_encode($validatorDrivers), - ) - ->end() - ->defaultValue('expired') - ->cannotBeEmpty() - ->end() - ->scalarNode('service')->end() - ->integerNode('expiration_time')->defaultValue(300)->end() - ->end() - ->end() ->end() ->validate() ->ifTrue(static function ($v) { @@ -118,14 +99,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->thenInvalid( 'You need to specify your own owner_factory service when using the "custom" owner_factory driver.', ) - ->end() - ->validate() - ->ifTrue(static function ($v) { - return $v['validator']['driver'] === 'custom' && empty($v['validator']['service']); - }) - ->thenInvalid( - 'You need to specify your own validator service when using the "custom" validator driver.', - ) ->end(); return $treeBuilder; diff --git a/src/Bundle/Resources/config/services.yaml b/src/Bundle/Resources/config/services.yaml index d64b0b5..5e59782 100644 --- a/src/Bundle/Resources/config/services.yaml +++ b/src/Bundle/Resources/config/services.yaml @@ -9,7 +9,6 @@ services: arguments: - '@brainbits_blocking.storage' - '@brainbits_blocking.owner_factory' - - '@brainbits_blocking.validator' brainbits_blocking.controller: diff --git a/src/Bundle/Resources/config/validator/always_invalidate.xml b/src/Bundle/Resources/config/validator/always_invalidate.xml deleted file mode 100644 index 44a34c5..0000000 --- a/src/Bundle/Resources/config/validator/always_invalidate.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/src/Bundle/Resources/config/validator/expired.xml b/src/Bundle/Resources/config/validator/expired.xml deleted file mode 100644 index 11a398a..0000000 --- a/src/Bundle/Resources/config/validator/expired.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - %brainbits_blocking.validator.expiration_time% - - - - - diff --git a/src/Validator/AlwaysInvalidateValidator.php b/src/Validator/AlwaysInvalidateValidator.php deleted file mode 100644 index 55cfcba..0000000 --- a/src/Validator/AlwaysInvalidateValidator.php +++ /dev/null @@ -1,28 +0,0 @@ -assertInstanceOf(Block::class, $result); } - public function testBlockReturnsBlockOnExistingAndInvalidBlock(): void - { - $storage = $this->createExistingStorage(); - $storage->expects($this->once()) - ->method('remove'); - $storage->expects($this->once()) - ->method('write'); - - $blocker = new Blocker( - $storage, - $this->ownerFactory, - new AlwaysInvalidateValidator(), - ); - $result = $blocker->block($this->identifier); - - $this->assertInstanceOf(Block::class, $result); - } - - public function testBlockThrowsExceptionOnExistingAndValidAndNonOwnerBlock(): void - { - $this->expectException(BlockFailedException::class); - - $storage = $this->createExistingStorage(); - $storage->expects($this->never()) - ->method('write'); - - $blocker = new Blocker( - $storage, - $this->ownerFactory, - new AlwaysValidateValidator(), - ); - $result = $blocker->block($this->identifier); - - $this->assertNull($result); - } - - public function testBlockUpdatesBlockOnExistingAndValidAndOwnedBlock(): void - { - $storage = $this->createExistingStorage(); - $storage->expects($this->once()) - ->method('touch') - ->with($this->block); - - $blocker = new Blocker( - $storage, - new ValueOwnerFactory('baz'), - new AlwaysValidateValidator(), - ); - $result = $blocker->block($this->identifier); - - $this->assertInstanceOf(Block::class, $result); - } - - public function testUnblockReturnsNullOnExistingAndInvalidBlock(): void - { - $storage = $this->createExistingStorage(); - $storage->expects($this->once()) - ->method('remove'); - - $blocker = new Blocker( - $storage, - $this->ownerFactory, - new AlwaysInvalidateValidator(), - ); - $result = $blocker->unblock($this->identifier); - - $this->assertNull($result); - } - - public function testUnblockReturnsBlockOnExistingAndValidBlock(): void - { - $storage = $this->createExistingStorage(); - $storage->expects($this->once()) - ->method('remove'); - - $blocker = new Blocker( - $storage, - $this->ownerFactory, - ); - $result = $blocker->unblock($this->identifier); - - $this->assertSame($this->block, $result); - } - public function testUnblockReturnsNullOnNonexistingBlock(): void { $storage = $this->createNonexistingStorage(); @@ -161,33 +74,6 @@ public function testUnblockReturnsNullOnNonexistingBlock(): void $this->assertNull($result); } - public function testIsBlockedReturnsFalseOnExistingAndInvalidBlock(): void - { - $storage = $this->createExistingStorage(); - $storage->expects($this->once()) - ->method('remove'); - - $blocker = new Blocker( - $storage, - $this->ownerFactory, - new AlwaysInvalidateValidator(), - ); - $result = $blocker->isBlocked($this->identifier); - - $this->assertFalse($result); - } - - public function testIsBlockedReturnsTrueOnExistingAndValidBlock(): void - { - $blocker = new Blocker( - $this->createExistingStorage(), - $this->ownerFactory, - ); - $result = $blocker->isBlocked($this->identifier); - - $this->assertTrue($result); - } - public function testIsBlockedReturnsFalseOnNonexistingBlock(): void { $blocker = new Blocker( diff --git a/tests/Bundle/DependencyInjection/BrainbitsBlockingExtensionTest.php b/tests/Bundle/DependencyInjection/BrainbitsBlockingExtensionTest.php index 2c39b6f..683d28c 100644 --- a/tests/Bundle/DependencyInjection/BrainbitsBlockingExtensionTest.php +++ b/tests/Bundle/DependencyInjection/BrainbitsBlockingExtensionTest.php @@ -18,7 +18,6 @@ use Brainbits\Blocking\Owner\ValueOwnerFactory; use Brainbits\Blocking\Storage\FilesystemStorage; use Brainbits\Blocking\Storage\InMemoryStorage; -use Brainbits\Blocking\Validator\ExpiredValidator; use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase; use Symfony\Component\DependencyInjection\Extension\Extension; @@ -36,8 +35,6 @@ public function testContainerHasDefaultParameters(): void $this->assertContainerBuilderHasService('brainbits_blocking.storage', FilesystemStorage::class); $this->assertContainerBuilderHasService('brainbits_blocking.owner_factory', SymfonySessionOwnerFactory::class); - $this->assertContainerBuilderHasService('brainbits_blocking.validator', ExpiredValidator::class); - $this->assertContainerBuilderHasParameter('brainbits_blocking.validator.expiration_time', 300); $this->assertContainerBuilderHasParameter('brainbits_blocking.interval', 30); } @@ -49,14 +46,11 @@ public function testContainerHasCustomParameters(): void 'driver' => 'value', 'value' => 'xx', ], - 'validator' => ['expiration_time' => 8], 'block_interval' => 9, ]); $this->assertContainerBuilderHasService('brainbits_blocking.storage', InMemoryStorage::class); $this->assertContainerBuilderHasService('brainbits_blocking.owner_factory', ValueOwnerFactory::class); - $this->assertContainerBuilderHasService('brainbits_blocking.validator', ExpiredValidator::class); - $this->assertContainerBuilderHasParameter('brainbits_blocking.validator.expiration_time', 8); $this->assertContainerBuilderHasParameter('brainbits_blocking.interval', 9); } @@ -93,16 +87,4 @@ public function testCustomOwnerService(): void $this->assertContainerBuilderHasAlias('brainbits_blocking.owner_factory', 'bar'); } - - public function testCustomValidatorService(): void - { - $this->load([ - 'validator' => [ - 'driver' => 'custom', - 'service' => 'baz', - ], - ]); - - $this->assertContainerBuilderHasAlias('brainbits_blocking.validator', 'baz'); - } } diff --git a/tests/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Bundle/DependencyInjection/ConfigurationTest.php index 25ae144..6e7ff06 100644 --- a/tests/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Bundle/DependencyInjection/ConfigurationTest.php @@ -50,10 +50,6 @@ public function testDefaultValues(): void 'prefix' => 'block', ], 'owner_factory' => ['driver' => 'symfony_session'], - 'validator' => [ - 'driver' => 'expired', - 'expiration_time' => 300, - ], 'block_interval' => 30, ], ); @@ -74,10 +70,6 @@ public function testProvidedValues(): void 'driver' => 'value', 'value' => 'bar', ], - 'validator' => [ - 'driver' => 'always_invalidate', - 'expiration_time' => 99, - ], 'block_interval' => 88, ], ], @@ -91,10 +83,6 @@ public function testProvidedValues(): void 'driver' => 'value', 'value' => 'bar', ], - 'validator' => [ - 'driver' => 'always_invalidate', - 'expiration_time' => 99, - ], 'block_interval' => 88, 'predis' => 'foo', ], @@ -150,10 +138,6 @@ public function testCustomStorageService(): void 'prefix' => 'block', ], 'owner_factory' => ['driver' => 'symfony_session'], - 'validator' => [ - 'driver' => 'expired', - 'expiration_time' => 300, - ], 'block_interval' => 30, ], ); @@ -191,10 +175,6 @@ public function testPredisAlias(): void 'prefix' => 'block', ], 'owner_factory' => ['driver' => 'symfony_session'], - 'validator' => [ - 'driver' => 'expired', - 'expiration_time' => 300, - ], 'block_interval' => 30, 'predis' => 'my_predis', ], @@ -252,70 +232,6 @@ public function testCustomOwnerService(): void 'driver' => 'custom', 'service' => 'foo', ], - 'validator' => [ - 'driver' => 'expired', - 'expiration_time' => 300, - ], - 'block_interval' => 30, - ], - ); - } - - public function testInvalidValidatorDriver(): void - { - $this->expectException(InvalidConfigurationException::class); - // phpcs:ignore Generic.Files.LineLength.TooLong - $this->expectExceptionMessage('Invalid configuration for path "brainbits_blocking.validator.driver": The validator driver "test" is not supported. Please choose one of ["expired","always_invalidate","custom"]'); - - $this->assertProcessedConfigurationEquals( - [ - [ - 'validator' => ['driver' => 'test'], - ], - ], - [], - ); - } - - public function testMissingCustomValidatorService(): void - { - $this->expectException(InvalidConfigurationException::class); - // phpcs:ignore Generic.Files.LineLength.TooLong - $this->expectExceptionMessage('Invalid configuration for path "brainbits_blocking": You need to specify your own validator service when using the "custom" validator driver.'); - - $this->assertProcessedConfigurationEquals( - [ - [ - 'validator' => ['driver' => 'custom'], - ], - ], - [], - ); - } - - public function testCustomValidatorService(): void - { - $this->assertProcessedConfigurationEquals( - [ - [ - 'validator' => [ - 'driver' => 'custom', - 'service' => 'foo', - ], - ], - ], - [ - 'storage' => [ - 'driver' => 'filesystem', - 'storage_dir' => '%kernel.cache_dir%/blocking/', - 'prefix' => 'block', - ], - 'owner_factory' => ['driver' => 'symfony_session'], - 'validator' => [ - 'driver' => 'custom', - 'service' => 'foo', - 'expiration_time' => 300, - ], 'block_interval' => 30, ], ); diff --git a/tests/Validator/AlwaysInvalidateValidatorTest.php b/tests/Validator/AlwaysInvalidateValidatorTest.php deleted file mode 100644 index 7d53a66..0000000 --- a/tests/Validator/AlwaysInvalidateValidatorTest.php +++ /dev/null @@ -1,31 +0,0 @@ -assertFalse($validator->validate($block)); - } -} diff --git a/tests/Validator/AlwaysValidateValidatorTest.php b/tests/Validator/AlwaysValidateValidatorTest.php deleted file mode 100644 index 05c4483..0000000 --- a/tests/Validator/AlwaysValidateValidatorTest.php +++ /dev/null @@ -1,31 +0,0 @@ -assertTrue($validator->validate($block)); - } -} From e39059a5fffd403b8ed9209f7c570d6bc277743f Mon Sep 17 00:00:00 2001 From: Stephan Wentz Date: Fri, 21 Jul 2023 11:53:54 +0200 Subject: [PATCH 3/3] fixup! fix: Add redis storage support, add bundle and deprecate brainbits/blocking-bundle, require psr/clock, rework expiration to ttl mechanism implemented by storage --- src/Blocker.php | 2 +- src/Storage/FilesystemStorage.php | 13 +++++-------- src/Storage/InMemoryStorage.php | 3 ++- src/Storage/PredisStorage.php | 4 ++-- src/Storage/StorageInterface.php | 2 +- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/Blocker.php b/src/Blocker.php index 8f3d9c1..f047ae2 100644 --- a/src/Blocker.php +++ b/src/Blocker.php @@ -49,7 +49,7 @@ public function tryBlock(BlockIdentity $identifier, int|null $ttl = null): Block return null; } - $this->storage->touch($block); + $this->storage->touch($block, $ttl ?? $this->defaultTtl); return $block; } diff --git a/src/Storage/FilesystemStorage.php b/src/Storage/FilesystemStorage.php index 8e42cf0..5957fde 100644 --- a/src/Storage/FilesystemStorage.php +++ b/src/Storage/FilesystemStorage.php @@ -78,7 +78,7 @@ public function write(Block $block, int $ttl): bool return true; } - public function touch(Block $block): bool + public function touch(Block $block, int $ttl): bool { $identity = $block->getIdentity(); @@ -89,13 +89,10 @@ public function touch(Block $block): bool $filename = $this->getFilename($block->getIdentity()); $metaFilename = $filename . '.meta'; - $metaContent = file_get_contents($metaFilename); - assert(is_string($metaContent)); - assert($metaContent !== ''); - $metaData = json_decode($metaContent, true); - assert(is_array($metaData)); - $metaData['updatedAt'] = $this->clock->now()->format('c'); - $metaContent = json_encode($metaData); + $metaContent = json_encode([ + 'ttl' => $ttl, + 'updatedAt' => $this->clock->now()->format('c'), + ]); if (file_put_contents($metaFilename, $metaContent) === false) { throw IOException::writeFailed($metaFilename); diff --git a/src/Storage/InMemoryStorage.php b/src/Storage/InMemoryStorage.php index aaf3c1f..657de19 100644 --- a/src/Storage/InMemoryStorage.php +++ b/src/Storage/InMemoryStorage.php @@ -52,12 +52,13 @@ public function write(Block $block, int $ttl): bool return true; } - public function touch(Block $block): bool + public function touch(Block $block, int $ttl): bool { if (!$this->exists($block->getIdentity())) { return false; } + $this->blocks[(string) $block->getIdentity()]['ttl'] = $ttl; $this->blocks[(string) $block->getIdentity()]['updatedAt'] = $this->clock->now(); return true; diff --git a/src/Storage/PredisStorage.php b/src/Storage/PredisStorage.php index 7fd41ce..f13cfc8 100644 --- a/src/Storage/PredisStorage.php +++ b/src/Storage/PredisStorage.php @@ -55,7 +55,7 @@ public function write(Block $block, int $ttl): bool return true; } - public function touch(Block $block): bool + public function touch(Block $block, int $ttl): bool { $identity = $block->getIdentity(); @@ -64,7 +64,7 @@ public function touch(Block $block): bool } try { - $this->client->touch($this->createKey($identity)); + $this->client->expire($this->createKey($identity), $ttl); } catch (PredisException) { throw IOException::touchFailed((string) $identity); } diff --git a/src/Storage/StorageInterface.php b/src/Storage/StorageInterface.php index ef93c4e..064c94f 100644 --- a/src/Storage/StorageInterface.php +++ b/src/Storage/StorageInterface.php @@ -23,7 +23,7 @@ interface StorageInterface { public function write(Block $block, int $ttl): bool; - public function touch(Block $block): bool; + public function touch(Block $block, int $ttl): bool; public function remove(Block $block): bool;