diff --git a/README.md b/README.md index d8c4901..af6f0f3 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,18 @@ class CacheIntegrationTest extends SimpleCacheTest } ``` +If your implementation uses **psr/simple-cache ^3.0** (with strict PHP type hints), extend `SimpleCacheV3Test` instead. It provides the same test coverage with data providers adapted for v3's typed interface: + +```php +class CacheV3IntegrationTest extends SimpleCacheV3Test +{ + public function createSimpleCache() + { + return new SimpleCache(); + } +} +``` + ### Contribute Contributions are very welcome! Send a pull request or diff --git a/composer.json b/composer.json index f223b78..874f5fb 100755 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "php": ">=5.5.9" }, "require-dev": { + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", "cache/cache": "^1.0", "symfony/cache": "^3.4.31|^4.3.4|^5.0", "symfony/phpunit-bridge": "^5.1,<5.3", diff --git a/src/SimpleCacheV3Test.php b/src/SimpleCacheV3Test.php new file mode 100644 index 0000000..f371a33 --- /dev/null +++ b/src/SimpleCacheV3Test.php @@ -0,0 +1,328 @@ +, Tobias Nyholm + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Cache\IntegrationTests; + +use PHPUnit\Framework\Attributes\DataProvider; + +/** + * PSR-16 integration test suite for implementations using psr/simple-cache ^3.0. + * + * PSR-16 v3 introduced strict PHP type hints (string $key, iterable $keys, + * null|int|DateInterval $ttl). These type hints cause PHP to throw \TypeError + * before the library code ever runs when clearly-invalid argument types are + * passed. This makes many of the original SimpleCacheTest data providers + * incompatible with v3. + * + * This subclass: + * - Filters data providers to only values that survive strict/coercion checks + * - Accepts both \TypeError (PHP-level rejection) and + * \Psr\SimpleCache\InvalidArgumentException (library-level validation) as + * valid failure modes + * - Is fully backward-compatible: existing v1/v2 consumers keep using + * SimpleCacheTest; v3 consumers switch to SimpleCacheV3Test + * + * @see \Cache\IntegrationTests\SimpleCacheTest + */ +abstract class SimpleCacheV3Test extends SimpleCacheTest +{ + /** + * Data provider for invalid array keys in setMultiple. + * + * Contains only string keys that are invalid per PSR-16 spec. + * Non-string key types (bool, null, float, int, object, array) are removed + * because PSR-16 v3's string type hint rejects them at PHP level before the + * implementation is invoked. + * + * @return list> + */ + public static function invalidArrayKeys() + { + return [ + [''], + ['{str'], + ['rand{'], + ['rand{str'], + ['rand}str'], + ['rand(str'], + ['rand)str'], + ['rand/str'], + ['rand\\str'], + ['rand@str'], + ['rand:str'], + ]; + } + + /** + * Data provider for invalid cache keys. + * + * Keeps the parent's structure (array_merge around invalidArrayKeys) but + * adds no extra items, since the parent's extra `[2]` is an integer and + * PSR-16 v3's string type hint rejects it at PHP level. + * + * @return list> + */ + public static function invalidKeys() + { + return array_merge( + self::invalidArrayKeys(), + [] + ); + } + + /** + * Data provider for invalid TTL values. + * + * PSR-16 v3 accepts only null|int|DateInterval for TTL. Values that coerce + * to valid types in PHP weak mode (e.g. true→1, false→0, ' 1'→1, '025'→25) + * are removed because they would not exercise invalid-TTL handling. + * + * @return list> + */ + public static function invalidTtl() + { + return [ + [''], + ['abc'], + ['12foo'], + [new \stdClass()], + [['array']], + ]; + } + + /** + * Helper that runs the supplied callable inside a try/catch. + * + * With PSR-16 v3 strict type hints a non-string key, non-iterable iterable, + * or invalid TTL will cause a \TypeError before the library sees the data. + * The PSR-16 spec says a key MUST be a string, so that is consistent. + * When the implementation is also typed we accept TypeError as a valid + * failure indicator for clearly-invalid arguments. + * + * @param callable(): void $callable + */ + private function assertCacheExceptionOrTypeError(callable $callable): void + { + try { + $callable(); + $this->fail('Expected exception to be thrown.'); + } catch (\TypeError $e) { + // PSR-16 v3 typed interfaces throw TypeError for clearly-invalid + // argument types (e.g. null key, object key, string for iterable). + $this->assertTrue(true); + } catch (\Psr\SimpleCache\InvalidArgumentException $e) { + // Explicit library-level validation. + $this->assertTrue(true); + } + } + + /** + * @dataProvider invalidKeys + */ + #[DataProvider('invalidKeys')] + public function testGetInvalidKeys($key) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->assertCacheExceptionOrTypeError(function () use ($key) { + $this->cache->get($key); + }); + } + + /** + * @dataProvider invalidKeys + */ + #[DataProvider('invalidKeys')] + public function testGetMultipleInvalidKeys($key) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->assertCacheExceptionOrTypeError(function () use ($key) { + $this->cache->getMultiple(['key1', $key, 'key2']); + }); + } + + public function testGetMultipleNoIterable() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->assertCacheExceptionOrTypeError(function () { + $this->cache->getMultiple('key'); + }); + } + + /** + * @dataProvider invalidKeys + */ + #[DataProvider('invalidKeys')] + public function testSetInvalidKeys($key) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->assertCacheExceptionOrTypeError(function () use ($key) { + $this->cache->set($key, 'foobar'); + }); + } + + /** + * @dataProvider invalidArrayKeys + */ + #[DataProvider('invalidArrayKeys')] + public function testSetMultipleInvalidKeys($key) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->assertCacheExceptionOrTypeError(function () use ($key) { + $values = function () use ($key) { + yield 'key1' => 'foo'; + yield $key => 'bar'; + yield 'key2' => 'baz'; + }; + $this->cache->setMultiple($values()); + }); + } + + public function testSetMultipleNoIterable() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->assertCacheExceptionOrTypeError(function () { + $this->cache->setMultiple('key'); + }); + } + + /** + * @dataProvider invalidKeys + */ + #[DataProvider('invalidKeys')] + public function testHasInvalidKeys($key) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->assertCacheExceptionOrTypeError(function () use ($key) { + $this->cache->has($key); + }); + } + + /** + * @dataProvider invalidKeys + */ + #[DataProvider('invalidKeys')] + public function testDeleteInvalidKeys($key) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->assertCacheExceptionOrTypeError(function () use ($key) { + $this->cache->delete($key); + }); + } + + /** + * @dataProvider invalidKeys + */ + #[DataProvider('invalidKeys')] + public function testDeleteMultipleInvalidKeys($key) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->assertCacheExceptionOrTypeError(function () use ($key) { + $this->cache->deleteMultiple(['key1', $key, 'key2']); + }); + } + + public function testDeleteMultipleNoIterable() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->assertCacheExceptionOrTypeError(function () { + $this->cache->deleteMultiple('key'); + }); + } + + /** + * @dataProvider invalidTtl + */ + #[DataProvider('invalidTtl')] + public function testSetInvalidTtl($ttl) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->assertCacheExceptionOrTypeError(function () use ($ttl) { + $this->cache->set('key', 'value', $ttl); + }); + } + + /** + * @dataProvider invalidTtl + */ + #[DataProvider('invalidTtl')] + public function testSetMultipleInvalidTtl($ttl) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $this->assertCacheExceptionOrTypeError(function () use ($ttl) { + $this->cache->setMultiple(['key' => 'value'], $ttl); + }); + } + + /** + * Tests the PSR-16 mandated minimum key length of 64 characters. + * + * PSR-16 explicitly states: "The key length MUST be at least 64 characters." + * This test ensures every compliant implementation supports at least 64 chars. + * + * @see https://www.php-fig.org/psr/psr-16/ + */ + public function testBasicUsageWithLongKey64() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $key = str_repeat('a', 64); + + $this->assertFalse($this->cache->has($key)); + $this->assertTrue($this->cache->set($key, 'value')); + + $this->assertTrue($this->cache->has($key)); + $this->assertSame('value', $this->cache->get($key)); + + $this->assertTrue($this->cache->delete($key)); + + $this->assertFalse($this->cache->has($key)); + } +}