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 +38,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(); @@ -57,19 +49,19 @@ public function tryBlock(IdentityInterface $identifier): BlockInterface|null return null; } - $this->storage->touch($block); + $this->storage->touch($block, $ttl ?? $this->defaultTtl); 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,26 +73,14 @@ 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); - if (!$block) { - return false; - } - - $valid = $this->validator->validate($block); - - if ($valid) { - return true; - } - - $this->storage->remove($block); - - return false; + return $block !== null; } - 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..525031d --- /dev/null +++ b/src/Bundle/DependencyInjection/BrainbitsBlockingExtension.php @@ -0,0 +1,81 @@ + $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']); + } + + $container->setParameter('brainbits_blocking.interval', $config['block_interval']); + + 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 ($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..96ea77e --- /dev/null +++ b/src/Bundle/DependencyInjection/Configuration.php @@ -0,0 +1,106 @@ +getRootNode(); + + $storageDrivers = ['filesystem', 'predis', 'in_memory', 'custom']; + $ownerFactoryDrivers = ['symfony_session', 'symfony_token', 'value', '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() + ->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(); + + 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..5e59782 --- /dev/null +++ b/src/Bundle/Resources/config/services.yaml @@ -0,0 +1,19 @@ +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.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/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..5957fde 100644 --- a/src/Storage/FilesystemStorage.php +++ b/src/Storage/FilesystemStorage.php @@ -13,118 +13,168 @@ 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, int $ttl): 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)); + $filename = $this->getFilename($block->getIdentity()); + $metaFilename = $filename . '.meta'; - if (!$updatedAt) { - throw IOException::createTouchFailed($filename); - } + $metaContent = json_encode([ + 'ttl' => $ttl, + 'updatedAt' => $this->clock->now()->format('c'), + ]); - $block->touch($updatedAt); + if (file_put_contents($metaFilename, $metaContent) === false) { + throw IOException::writeFailed($metaFilename); + } 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..657de19 100644 --- a/src/Storage/InMemoryStorage.php +++ b/src/Storage/InMemoryStorage.php @@ -13,41 +13,58 @@ 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, int $ttl): bool { - $this->blocks[(string) $block->getIdentity()]->touch(new DateTimeImmutable()); + 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; } - public function remove(BlockInterface $block): bool + public function remove(Block $block): bool { if (!$this->exists($block->getIdentity())) { return false; @@ -58,21 +75,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..f13cfc8 --- /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, int $ttl): bool + { + $identity = $block->getIdentity(); + + if (!$this->exists($identity)) { + return false; + } + + try { + $this->client->expire($this->createKey($identity), $ttl); + } 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..064c94f 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, int $ttl): 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 deleted file mode 100644 index 565b43d..0000000 --- a/src/Validator/AlwaysInvalidateValidator.php +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 3ca0655..0000000 --- a/src/Validator/ValidatorInterface.php +++ /dev/null @@ -1,29 +0,0 @@ -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..4dda692 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,106 +53,10 @@ public function testBlockReturnsBlockOnNonexistingBlock(): void $blocker = new Blocker( $storage, $this->ownerFactory, - $this->createInvalidValidator() ); $result = $blocker->block($this->identifier); - $this->assertInstanceOf(BlockInterface::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, - $this->createInvalidValidator() - ); - $result = $blocker->block($this->identifier); - - $this->assertInstanceOf(BlockInterface::class, $result); - } - - public function testBlockThrowsExceptionOnExistingAndValidAndNonOwnerBlock(): void - { - $this->expectException(BlockFailedException::class); - - $storage = $this->createExistingStorage(); - $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() - ); - $result = $blocker->block($this->identifier); - - $this->assertNull($result); - } - - public function testBlockUpdatesBlockOnExistingAndValidAndOwnedBlock(): void - { - $storage = $this->createExistingStorage(); - $storage->expects($this->once()) - ->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() - ); - $result = $blocker->block($this->identifier); - - $this->assertInstanceOf(BlockInterface::class, $result); - } - - public function testUnblockReturnsNullOnExistingAndInvalidBlock(): void - { - $storage = $this->createExistingStorage(); - $storage->expects($this->once()) - ->method('remove'); - - $blocker = new Blocker( - $storage, - $this->ownerFactory, - $this->createInvalidValidator() - ); - $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, - $this->createValidValidator() - ); - $result = $blocker->unblock($this->identifier); - - $this->assertSame($this->block, $result); + $this->assertInstanceOf(Block::class, $result); } public function testUnblockReturnsNullOnNonexistingBlock(): void @@ -181,43 +68,17 @@ public function testUnblockReturnsNullOnNonexistingBlock(): void $blocker = new Blocker( $storage, $this->ownerFactory, - $this->createInvalidValidator() ); $result = $blocker->unblock($this->identifier); $this->assertNull($result); } - public function testIsBlockedReturnsFalseOnExistingAndInvalidBlock(): void - { - $storage = $this->createExistingStorage(); - $storage->expects($this->once()) - ->method('remove'); - - $blocker = new Blocker($storage, $this->ownerFactory, $this->createInvalidValidator()); - $result = $blocker->isBlocked($this->identifier); - - $this->assertFalse($result); - } - - public function testIsBlockedReturnsTrueOnExistingAndValidBlock(): void - { - $blocker = new Blocker( - $this->createExistingStorage(), - $this->ownerFactory, - $this->createValidValidator() - ); - $result = $blocker->isBlocked($this->identifier); - - $this->assertTrue($result); - } - public function testIsBlockedReturnsFalseOnNonexistingBlock(): void { $blocker = new Blocker( $this->createNonexistingStorage(), $this->ownerFactory, - $this->createValidValidator() ); $result = $blocker->isBlocked($this->identifier); @@ -229,17 +90,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 +109,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 +121,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..683d28c --- /dev/null +++ b/tests/Bundle/DependencyInjection/BrainbitsBlockingExtensionTest.php @@ -0,0 +1,90 @@ +load(); + + $this->assertContainerBuilderHasService('brainbits_blocking.storage', FilesystemStorage::class); + $this->assertContainerBuilderHasService('brainbits_blocking.owner_factory', SymfonySessionOwnerFactory::class); + $this->assertContainerBuilderHasParameter('brainbits_blocking.interval', 30); + } + + public function testContainerHasCustomParameters(): void + { + $this->load([ + 'storage' => ['driver' => 'in_memory'], + 'owner_factory' => [ + 'driver' => 'value', + 'value' => 'xx', + ], + 'block_interval' => 9, + ]); + + $this->assertContainerBuilderHasService('brainbits_blocking.storage', InMemoryStorage::class); + $this->assertContainerBuilderHasService('brainbits_blocking.owner_factory', ValueOwnerFactory::class); + $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'); + } +} diff --git a/tests/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Bundle/DependencyInjection/ConfigurationTest.php new file mode 100644 index 0000000..6e7ff06 --- /dev/null +++ b/tests/Bundle/DependencyInjection/ConfigurationTest.php @@ -0,0 +1,239 @@ +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'], + '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', + ], + 'block_interval' => 88, + ], + ], + [ + 'storage' => [ + 'driver' => 'in_memory', + 'storage_dir' => 'foo', + 'prefix' => 'block', + ], + 'owner_factory' => [ + 'driver' => 'value', + 'value' => 'bar', + ], + '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'], + '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'], + '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', + ], + '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 deleted file mode 100644 index 4d28b95..0000000 --- a/tests/Validator/AlwaysInvalidateValidatorTest.php +++ /dev/null @@ -1,71 +0,0 @@ -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 - { - $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)); - - $validator = new AlwaysInvalidateValidator(5400); - $this->assertFalse($validator->validate($blockMock)); - } -} 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)); - } -}