From 994c71e8a264f91caac869e886dc5ea0f7f8fc5c Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 12 Mar 2026 17:21:15 -0600 Subject: [PATCH 1/2] test(sso): add comprehensive PSR-16 cache tests Add unit tests and integration tests for WordPress_Simple_Cache: - WordPress_Simple_Cache_Test.php: 20+ PHPUnit tests covering all PSR-16 interface methods (get, set, delete, clear, has, getMultiple, setMultiple, deleteMultiple) - WordPress_Simple_Cache_Integration_Test.php: Standalone tests that can run without full WordPress test suite Tests cover: - Basic CRUD operations - TTL handling (integer, null, DateInterval) - Data type preservation (string, int, float, bool, array, object) - Cache prefix isolation - Error cases and edge cases - PSR-16 interface compliance Cherry-picked from PR #357 --- ...ordPress_Simple_Cache_Integration_Test.php | 332 +++++++++++++++ .../SSO/WordPress_Simple_Cache_Test.php | 381 ++++++++++++++++++ 2 files changed, 713 insertions(+) create mode 100644 tests/WP_Ultimo/SSO/WordPress_Simple_Cache_Integration_Test.php create mode 100644 tests/WP_Ultimo/SSO/WordPress_Simple_Cache_Test.php diff --git a/tests/WP_Ultimo/SSO/WordPress_Simple_Cache_Integration_Test.php b/tests/WP_Ultimo/SSO/WordPress_Simple_Cache_Integration_Test.php new file mode 100644 index 00000000..5045589a --- /dev/null +++ b/tests/WP_Ultimo/SSO/WordPress_Simple_Cache_Integration_Test.php @@ -0,0 +1,332 @@ +clear(); + + $result = $cache->set('key1', 'value1'); + self::assert($result === true, 'set() returns true'); + + $value = $cache->get('key1'); + self::assert($value === 'value1', 'get() returns correct value'); + + $cache->clear(); + } + + /** + * Test get with default value. + */ + private static function test_get_with_default() { + $cache = new WordPress_Simple_Cache('test_'); + $cache->clear(); + + $value = $cache->get('nonexistent', 'default'); + self::assert($value === 'default', 'get() returns default for nonexistent key'); + + $cache->clear(); + } + + /** + * Test delete operation. + */ + private static function test_delete() { + $cache = new WordPress_Simple_Cache('test_'); + $cache->clear(); + + $cache->set('delete_key', 'delete_value'); + $result = $cache->delete('delete_key'); + self::assert($result === true, 'delete() returns true'); + + $value = $cache->get('delete_key'); + self::assert($value === null, 'Deleted key returns null'); + + $cache->clear(); + } + + /** + * Test has operation. + */ + private static function test_has() { + $cache = new WordPress_Simple_Cache('test_'); + $cache->clear(); + + $has = $cache->has('has_key'); + self::assert($has === false, 'has() returns false for nonexistent key'); + + $cache->set('has_key', 'has_value'); + $has = $cache->has('has_key'); + self::assert($has === true, 'has() returns true for existing key'); + + $cache->clear(); + } + + /** + * Test clear operation. + */ + private static function test_clear() { + $cache = new WordPress_Simple_Cache('test_'); + $cache->clear(); + + $cache->set('clear1', 'value1'); + $cache->set('clear2', 'value2'); + + $result = $cache->clear(); + self::assert($result === true, 'clear() returns true'); + + $has1 = $cache->has('clear1'); + $has2 = $cache->has('clear2'); + self::assert($has1 === false && $has2 === false, 'clear() removes all keys'); + + $cache->clear(); + } + + /** + * Test multiple operations. + */ + private static function test_multiple_operations() { + $cache = new WordPress_Simple_Cache('test_'); + $cache->clear(); + + // Set multiple. + $values = array('m1' => 'v1', 'm2' => 'v2'); + $result = $cache->setMultiple($values); + self::assert($result === true, 'setMultiple() returns true'); + + // Get multiple. + $retrieved = $cache->getMultiple(array('m1', 'm2', 'm3'), 'default'); + self::assert( + $retrieved['m1'] === 'v1' && $retrieved['m2'] === 'v2' && $retrieved['m3'] === 'default', + 'getMultiple() returns correct values' + ); + + // Delete multiple. + $result = $cache->deleteMultiple(array('m1', 'm2')); + self::assert($result === true, 'deleteMultiple() returns true'); + + $cache->clear(); + } + + /** + * Test different data types. + */ + private static function test_data_types() { + $cache = new WordPress_Simple_Cache('test_'); + $cache->clear(); + + // String. + $cache->set('string', 'test'); + self::assert($cache->get('string') === 'test', 'Stores and retrieves string'); + + // Integer. + $cache->set('int', 42); + self::assert($cache->get('int') === 42, 'Stores and retrieves integer'); + + // Float. + $cache->set('float', 3.14); + self::assert($cache->get('float') === 3.14, 'Stores and retrieves float'); + + // Boolean. + $cache->set('bool', true); + self::assert($cache->get('bool') === true, 'Stores and retrieves boolean'); + + // Array. + $array = array('foo' => 'bar'); + $cache->set('array', $array); + self::assert($cache->get('array') === $array, 'Stores and retrieves array'); + + $cache->clear(); + } + + /** + * Test prefix isolation. + */ + private static function test_prefix_isolation() { + $cache1 = new WordPress_Simple_Cache('prefix1_'); + $cache2 = new WordPress_Simple_Cache('prefix2_'); + + $cache1->clear(); + $cache2->clear(); + + $cache1->set('key', 'value1'); + $cache2->set('key', 'value2'); + + self::assert( + $cache1->get('key') === 'value1' && $cache2->get('key') === 'value2', + 'Different prefixes isolate cache data' + ); + + $cache1->clear(); + $cache2->clear(); + } + + /** + * Test TTL handling. + */ + private static function test_ttl_handling() { + $cache = new WordPress_Simple_Cache('test_'); + $cache->clear(); + + // Integer TTL. + $result = $cache->set('ttl_int', 'value', 60); + self::assert($result === true, 'set() with integer TTL returns true'); + + // Null TTL. + $result = $cache->set('ttl_null', 'value', null); + self::assert($result === true, 'set() with null TTL returns true'); + + // DateInterval TTL. + $interval = new \DateInterval('PT1H'); + $result = $cache->set('ttl_interval', 'value', $interval); + self::assert($result === true, 'set() with DateInterval TTL returns true'); + + $cache->clear(); + } +} + +// Auto-run if this file is executed directly (not included by PHPUnit). +if (php_sapi_name() === 'cli' && !defined('PHPUNIT_COMPOSER_INSTALL')) { + // Load autoloader. + require_once dirname(dirname(dirname(__DIR__))) . '/vendor/autoload.php'; + require_once dirname(dirname(dirname(__DIR__))) . '/inc/sso/class-wordpress-simple-cache.php'; + + WordPress_Simple_Cache_Integration_Test::run_all_tests(); +} diff --git a/tests/WP_Ultimo/SSO/WordPress_Simple_Cache_Test.php b/tests/WP_Ultimo/SSO/WordPress_Simple_Cache_Test.php new file mode 100644 index 00000000..df6aef48 --- /dev/null +++ b/tests/WP_Ultimo/SSO/WordPress_Simple_Cache_Test.php @@ -0,0 +1,381 @@ +cache = new WordPress_Simple_Cache($this->prefix); + + // Clean up any existing test cache entries. + $this->cache->clear(); + } + + /** + * Tear down test fixtures. + */ + protected function tearDown(): void { + // Clean up test cache entries. + if ($this->cache) { + $this->cache->clear(); + } + + parent::tearDown(); + } + + /** + * Test that the cache implements PSR-16 CacheInterface. + */ + public function test_implements_psr16_cache_interface(): void { + $this->assertInstanceOf(CacheInterface::class, $this->cache); + } + + /** + * Test basic get/set operations. + */ + public function test_get_and_set(): void { + $key = 'test_key'; + $value = 'test_value'; + + // Set a value. + $result = $this->cache->set($key, $value); + $this->assertTrue($result, 'set() should return true on success'); + + // Get the value. + $retrieved = $this->cache->get($key); + $this->assertSame($value, $retrieved, 'Retrieved value should match set value'); + } + + /** + * Test get with default value when key doesn't exist. + */ + public function test_get_with_default_value(): void { + $key = 'nonexistent_key'; + $default = 'default_value'; + + $result = $this->cache->get($key, $default); + $this->assertSame($default, $result, 'Should return default value for nonexistent key'); + } + + /** + * Test delete operation. + */ + public function test_delete(): void { + $key = 'test_delete_key'; + $value = 'test_value'; + + // Set and verify. + $this->cache->set($key, $value); + $this->assertSame($value, $this->cache->get($key)); + + // Delete and verify. + $result = $this->cache->delete($key); + $this->assertTrue($result, 'delete() should return true on success'); + + // Verify it's gone. + $this->assertNull($this->cache->get($key), 'Deleted key should return null'); + } + + /** + * Test has() method. + */ + public function test_has(): void { + $key = 'test_has_key'; + $value = 'test_value'; + + // Key shouldn't exist initially. + $this->assertFalse($this->cache->has($key), 'has() should return false for nonexistent key'); + + // Set value. + $this->cache->set($key, $value); + + // Now it should exist. + $this->assertTrue($this->cache->has($key), 'has() should return true for existing key'); + } + + /** + * Test clear operation. + */ + public function test_clear(): void { + // Set multiple values. + $this->cache->set('key1', 'value1'); + $this->cache->set('key2', 'value2'); + $this->cache->set('key3', 'value3'); + + // Verify they exist. + $this->assertTrue($this->cache->has('key1')); + $this->assertTrue($this->cache->has('key2')); + $this->assertTrue($this->cache->has('key3')); + + // Clear cache. + $result = $this->cache->clear(); + $this->assertTrue($result, 'clear() should return true on success'); + + // Verify all are gone. + $this->assertFalse($this->cache->has('key1')); + $this->assertFalse($this->cache->has('key2')); + $this->assertFalse($this->cache->has('key3')); + } + + /** + * Test getMultiple operation. + */ + public function test_get_multiple(): void { + // Set multiple values. + $this->cache->set('key1', 'value1'); + $this->cache->set('key2', 'value2'); + $this->cache->set('key3', 'value3'); + + // Get multiple. + $keys = array('key1', 'key2', 'nonexistent'); + $values = $this->cache->getMultiple($keys, 'default'); + + $this->assertIsArray($values); + $this->assertSame('value1', $values['key1']); + $this->assertSame('value2', $values['key2']); + $this->assertSame('default', $values['nonexistent']); + } + + /** + * Test setMultiple operation. + */ + public function test_set_multiple(): void { + $values = array( + 'multi1' => 'value1', + 'multi2' => 'value2', + 'multi3' => 'value3', + ); + + // Set multiple. + $result = $this->cache->setMultiple($values); + $this->assertTrue($result, 'setMultiple() should return true on success'); + + // Verify all were set. + $this->assertSame('value1', $this->cache->get('multi1')); + $this->assertSame('value2', $this->cache->get('multi2')); + $this->assertSame('value3', $this->cache->get('multi3')); + } + + /** + * Test deleteMultiple operation. + */ + public function test_delete_multiple(): void { + // Set multiple values. + $this->cache->set('del1', 'value1'); + $this->cache->set('del2', 'value2'); + $this->cache->set('del3', 'value3'); + + // Delete multiple. + $keys = array('del1', 'del2'); + $result = $this->cache->deleteMultiple($keys); + $this->assertTrue($result, 'deleteMultiple() should return true on success'); + + // Verify deleted keys are gone. + $this->assertNull($this->cache->get('del1')); + $this->assertNull($this->cache->get('del2')); + + // Verify remaining key still exists. + $this->assertSame('value3', $this->cache->get('del3')); + } + + /** + * Test TTL with integer seconds. + */ + public function test_ttl_with_integer_seconds(): void { + $key = 'ttl_test_key'; + $value = 'ttl_test_value'; + $ttl = 60; // 60 seconds. + + $result = $this->cache->set($key, $value, $ttl); + $this->assertTrue($result); + + // Value should be retrievable. + $retrieved = $this->cache->get($key); + $this->assertSame($value, $retrieved); + + // Note: We can't easily test expiration in unit tests without time manipulation. + // The transient is set with the correct expiration time. + } + + /** + * Test TTL with null (no expiration). + */ + public function test_ttl_with_null(): void { + $key = 'no_ttl_key'; + $value = 'no_ttl_value'; + + $result = $this->cache->set($key, $value, null); + $this->assertTrue($result); + + // Value should be retrievable. + $retrieved = $this->cache->get($key); + $this->assertSame($value, $retrieved); + } + + /** + * Test TTL with DateInterval. + */ + public function test_ttl_with_date_interval(): void { + $key = 'dateinterval_key'; + $value = 'dateinterval_value'; + $interval = new \DateInterval('PT1H'); // 1 hour. + + $result = $this->cache->set($key, $value, $interval); + $this->assertTrue($result); + + // Value should be retrievable. + $retrieved = $this->cache->get($key); + $this->assertSame($value, $retrieved); + } + + /** + * Test storing different data types. + */ + public function test_stores_different_data_types(): void { + // String. + $this->cache->set('string', 'test string'); + $this->assertSame('test string', $this->cache->get('string')); + + // Integer. + $this->cache->set('integer', 42); + $this->assertSame(42, $this->cache->get('integer')); + + // Float. + $this->cache->set('float', 3.14); + $this->assertSame(3.14, $this->cache->get('float')); + + // Boolean. + $this->cache->set('bool_true', true); + $this->assertTrue($this->cache->get('bool_true')); + + $this->cache->set('bool_false', false); + $this->assertFalse($this->cache->get('bool_false')); + + // Array. + $array = array('foo' => 'bar', 'baz' => 123); + $this->cache->set('array', $array); + $this->assertSame($array, $this->cache->get('array')); + + // Object. + $object = new \stdClass(); + $object->prop = 'value'; + $this->cache->set('object', $object); + $retrieved = $this->cache->get('object'); + $this->assertInstanceOf(\stdClass::class, $retrieved); + $this->assertSame('value', $retrieved->prop); + } + + /** + * Test cache prefix isolation. + */ + public function test_cache_prefix_isolation(): void { + $cache1 = new WordPress_Simple_Cache('prefix1_'); + $cache2 = new WordPress_Simple_Cache('prefix2_'); + + // Set same key in both caches with different values. + $cache1->set('test', 'value1'); + $cache2->set('test', 'value2'); + + // Each should retrieve its own value. + $this->assertSame('value1', $cache1->get('test')); + $this->assertSame('value2', $cache2->get('test')); + + // Clear one cache shouldn't affect the other. + $cache1->clear(); + $this->assertNull($cache1->get('test')); + $this->assertSame('value2', $cache2->get('test')); + + // Cleanup. + $cache2->clear(); + } + + /** + * Test that cache works with SSO integration. + */ + public function test_cache_integration_with_sso(): void { + // Create cache instance with SSO prefix. + $sso_cache = new WordPress_Simple_Cache('wu_sso_'); + + // Simulate SSO session data. + $broker_id = 'test_broker_123'; + $session_id = 'session_abc_456'; + $user_id = 42; + + // Store session mapping (broker + token -> session_id). + $cache_key = $broker_id . '_' . 'test_token'; + $sso_cache->set($cache_key, $session_id, 3600); // 1 hour TTL. + + // Verify retrieval. + $retrieved = $sso_cache->get($cache_key); + $this->assertSame($session_id, $retrieved); + + // Simulate cleanup. + $sso_cache->delete($cache_key); + $this->assertNull($sso_cache->get($cache_key)); + } + + /** + * Test compatibility with jasny/sso CacheInterface usage. + */ + public function test_jasny_sso_compatibility(): void { + // Jasny SSO uses PSR-16 CacheInterface methods. + $cache = new WordPress_Simple_Cache('jasny_test_'); + + // Simulate broker token storage. + $broker_id = 'broker_123'; + $token = 'token_abc'; + $key = 'sso_' . $broker_id . '_' . $token; + $value = 'session_xyz'; + $ttl = 180; // 3 minutes, typical for SSO. + + // Store. + $result = $cache->set($key, $value, $ttl); + $this->assertTrue($result); + + // Retrieve. + $retrieved = $cache->get($key); + $this->assertSame($value, $retrieved); + + // Verify has(). + $this->assertTrue($cache->has($key)); + + // Delete. + $cache->delete($key); + $this->assertFalse($cache->has($key)); + + // Cleanup. + $cache->clear(); + } +} From 054e5494af383e167697602c8dd003dbf2711f0b Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 12 Mar 2026 17:28:34 -0600 Subject: [PATCH 2/2] fix(sso): use site transients and wrap values for proper false handling Update WordPress_Simple_Cache to: - Use site transients (get_site_transient, set_site_transient, delete_site_transient) for network-wide SSO session storage in multisite - Wrap values in array structure ['v' => value] to distinguish false values from missing keys (WordPress returns false for non-existent transients) - Update has() to check for wrapped structure - Update clear() to use wp_sitemeta table Fixes failing test: test_stores_different_data_types --- inc/sso/class-wordpress-simple-cache.php | 30 +++++++++++++++--------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/inc/sso/class-wordpress-simple-cache.php b/inc/sso/class-wordpress-simple-cache.php index 82e3f9d1..6fddc638 100644 --- a/inc/sso/class-wordpress-simple-cache.php +++ b/inc/sso/class-wordpress-simple-cache.php @@ -57,8 +57,14 @@ public function __construct(string $prefix = '') { */ public function get($key, $default = null) { $this->validateKey($key); - $value = get_transient($this->prefix . $key); - return false !== $value ? $value : $default; + $raw = get_site_transient($this->prefix . $key); + + // Check if key exists by looking for wrapped structure. + if (false === $raw || ! is_array($raw) || ! array_key_exists('v', $raw)) { + return $default; + } + + return $raw['v']; } /** @@ -75,7 +81,8 @@ public function get($key, $default = null) { public function set($key, $value, $ttl = null) { $this->validateKey($key); $expiration = $this->ttlToSeconds($ttl); - return set_transient($this->prefix . $key, $value, $expiration); + // Wrap value in array to distinguish false values from missing keys. + return set_site_transient($this->prefix . $key, array('v' => $value), $expiration); } /** @@ -89,7 +96,7 @@ public function set($key, $value, $ttl = null) { */ public function delete($key) { $this->validateKey($key); - return delete_transient($this->prefix . $key); + return delete_site_transient($this->prefix . $key); } /** @@ -106,21 +113,21 @@ public function clear() { // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $results = $wpdb->get_results( $wpdb->prepare( - "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", - $wpdb->esc_like('_transient_' . $this->prefix) . '%' + "SELECT meta_key FROM {$wpdb->sitemeta} WHERE meta_key LIKE %s", + $wpdb->esc_like('_site_transient_' . $this->prefix) . '%' ) ); // phpcs:enable - if (!is_array($results)) { + if (! is_array($results)) { return false; } $success = true; foreach ($results as $result) { - // Extract the transient name from option_name - $transient_name = str_replace('_transient_', '', $result->option_name); - if (!delete_transient($transient_name)) { + // Extract the transient name from meta_key. + $transient_name = str_replace('_site_transient_', '', $result->meta_key); + if (! delete_site_transient($transient_name)) { $success = false; } } @@ -211,7 +218,8 @@ public function deleteMultiple($keys) { */ public function has($key) { $this->validateKey($key); - return false !== get_transient($this->prefix . $key); + $raw = get_site_transient($this->prefix . $key); + return false !== $raw && is_array($raw) && array_key_exists('v', $raw); } /**