diff --git a/ProcessMaker/Cache/Monitoring/CacheMetricsDecorator.php b/ProcessMaker/Cache/Monitoring/CacheMetricsDecorator.php index 9808158ccf..fb567e207f 100644 --- a/ProcessMaker/Cache/Monitoring/CacheMetricsDecorator.php +++ b/ProcessMaker/Cache/Monitoring/CacheMetricsDecorator.php @@ -139,6 +139,22 @@ public function missing(string $key): bool return $this->cache->missing($key); } + /** + * Invalidate cache for a specific screen + * + * @param int $screenId Screen ID + * @return bool + * @throws \RuntimeException If underlying cache doesn't support invalidate + */ + public function invalidate(int $screenId, string $language): bool + { + if ($this->cache instanceof ScreenCacheInterface) { + return $this->cache->invalidate($screenId, $language); + } + + throw new \RuntimeException('Underlying cache implementation does not support invalidate method'); + } + /** * Calculate the approximate size in bytes of a value * @@ -169,4 +185,23 @@ protected function calculateSize(mixed $value): int return 0; // for null or other types } + + /** + * Get a value from the cache or store it if it doesn't exist. + * + * @param string $key + * @param callable $callback + * @return mixed + */ + public function getOrCache(string $key, callable $callback): mixed + { + if ($this->cache->has($key)) { + return $this->cache->get($key); + } + + $value = $callback(); + $this->cache->set($key, $value); + + return $value; + } } diff --git a/ProcessMaker/Cache/Screens/LegacyScreenCacheAdapter.php b/ProcessMaker/Cache/Screens/LegacyScreenCacheAdapter.php index a501b5df4e..df53603ec2 100644 --- a/ProcessMaker/Cache/Screens/LegacyScreenCacheAdapter.php +++ b/ProcessMaker/Cache/Screens/LegacyScreenCacheAdapter.php @@ -2,6 +2,7 @@ namespace ProcessMaker\Cache\Screens; +use Illuminate\Support\Facades\Storage; use ProcessMaker\Managers\ScreenCompiledManager; class LegacyScreenCacheAdapter implements ScreenCacheInterface @@ -54,4 +55,40 @@ public function has(string $key): bool { return $this->compiledManager->getCompiledContent($key) !== null; } + + /** + * Delete a screen from cache + */ + public function delete(string $key): bool + { + return $this->compiledManager->deleteCompiledContent($key); + } + + /** + * Clear all screen caches + */ + public function clear(): bool + { + return $this->compiledManager->clearCompiledContent(); + } + + /** + * Check if screen is missing from cache + */ + public function missing(string $key): bool + { + return !$this->has($key); + } + + /** + * Invalidate all cache entries for a specific screen + * + * @param int $screenId Screen ID + * @return bool + */ + public function invalidate(int $screenId, string $language): bool + { + // Get all files from storage that match the pattern for this screen ID + return $this->compiledManager->deleteScreenCompiledContent($screenId, $language); + } } diff --git a/ProcessMaker/Cache/Screens/ScreenCacheFactory.php b/ProcessMaker/Cache/Screens/ScreenCacheFactory.php index e02270c056..0fa77ee319 100644 --- a/ProcessMaker/Cache/Screens/ScreenCacheFactory.php +++ b/ProcessMaker/Cache/Screens/ScreenCacheFactory.php @@ -47,4 +47,14 @@ public static function create(): ScreenCacheInterface // Wrap with metrics decorator if not already wrapped return new CacheMetricsDecorator($cache, app()->make(RedisMetricsManager::class)); } + + /** + * Get the current screen cache instance + * + * @return ScreenCacheInterface + */ + public static function getScreenCache(): ScreenCacheInterface + { + return self::create(); + } } diff --git a/ProcessMaker/Cache/Screens/ScreenCacheInterface.php b/ProcessMaker/Cache/Screens/ScreenCacheInterface.php index db4b259b39..179925c3f5 100644 --- a/ProcessMaker/Cache/Screens/ScreenCacheInterface.php +++ b/ProcessMaker/Cache/Screens/ScreenCacheInterface.php @@ -21,6 +21,43 @@ public function set(string $key, mixed $value): bool; /** * Check if screen exists in cache + * + * @param string $key Screen cache key + * @return bool */ public function has(string $key): bool; + + /** + * Delete a screen from cache + * + * @param string $key Screen cache key + * @return bool + */ + public function delete(string $key): bool; + + /** + * Clear all screen caches + * + * @return bool + */ + public function clear(): bool; + + /** + * Check if screen is missing from cache + * + * @param string $key Screen cache key + * @return bool + */ + public function missing(string $key): bool; + + /** + * Invalidate cache for a specific screen + * + * @param int $screenId + * @return bool + */ + public function invalidate( + int $screenId, + string $language, + ): bool; } diff --git a/ProcessMaker/Cache/Screens/ScreenCacheManager.php b/ProcessMaker/Cache/Screens/ScreenCacheManager.php index 993d221151..de96e974f3 100644 --- a/ProcessMaker/Cache/Screens/ScreenCacheManager.php +++ b/ProcessMaker/Cache/Screens/ScreenCacheManager.php @@ -5,6 +5,7 @@ use Illuminate\Cache\CacheManager; use ProcessMaker\Cache\CacheInterface; use ProcessMaker\Managers\ScreenCompiledManager; +use ProcessMaker\Models\Screen; class ScreenCacheManager implements CacheInterface, ScreenCacheInterface { @@ -139,4 +140,25 @@ public function missing(string $key): bool { return !$this->has($key); } + + /** + * Invalidate all cache entries for a specific screen + * @param int $screenId Screen ID + * @param string $language Language code + * @return bool + */ + public function invalidate(int $screenId, string $language): bool + { + // Get all keys from cache that match the pattern for this screen ID + // TODO Improve this to avoid scanning the entire cache + $pattern = "*_{$language}_sid_{$screenId}_*"; + $keys = $this->cacheManager->get($pattern); + + // Delete all matching keys + foreach ($keys as $key) { + $this->cacheManager->forget($key); + } + + return true; + } } diff --git a/ProcessMaker/Cache/Settings/SettingCacheManager.php b/ProcessMaker/Cache/Settings/SettingCacheManager.php index eec30615a6..af885d0a35 100644 --- a/ProcessMaker/Cache/Settings/SettingCacheManager.php +++ b/ProcessMaker/Cache/Settings/SettingCacheManager.php @@ -31,6 +31,7 @@ private function determineCacheDriver(): string if (in_array($defaultCache, ['redis', 'cache_settings'])) { return self::DEFAULT_CACHE_DRIVER; } + return $defaultCache; } @@ -150,7 +151,7 @@ public function clearBy(string $pattern): void // Get all keys $keys = Redis::connection($connection)->keys($this->cacheManager->getPrefix() . '*'); // Filter keys by pattern - $matchedKeys = array_filter($keys, fn($key) => preg_match('/' . $pattern . '/', $key)); + $matchedKeys = array_filter($keys, fn ($key) => preg_match('/' . $pattern . '/', $key)); if (!empty($matchedKeys)) { Redis::connection($connection)->del($matchedKeys); diff --git a/ProcessMaker/Events/TranslationChanged.php b/ProcessMaker/Events/TranslationChanged.php new file mode 100644 index 0000000000..792a78c4ab --- /dev/null +++ b/ProcessMaker/Events/TranslationChanged.php @@ -0,0 +1,31 @@ +language = $language; + $this->changes = $changes; + $this->screenId = $screenId; + } +} diff --git a/ProcessMaker/Listeners/InvalidateScreenCacheOnTranslationChange.php b/ProcessMaker/Listeners/InvalidateScreenCacheOnTranslationChange.php new file mode 100644 index 0000000000..129c6af448 --- /dev/null +++ b/ProcessMaker/Listeners/InvalidateScreenCacheOnTranslationChange.php @@ -0,0 +1,55 @@ +screenId) { + $this->invalidateScreen($event->screenId, $event->language); + } + } catch (\Exception $e) { + Log::error('Failed to invalidate screen cache', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + 'language' => $event->language, + 'screenId' => $event->screenId, + ]); + throw $e; // Re-throw to ensure error is properly handled + } + } + + /** + * Invalidate cache for a specific screen + */ + protected function invalidateScreen(string $screenId, string $locale): void + { + try { + $screen = Screen::find($screenId); + if ($screen) { + $cache = ScreenCacheFactory::getScreenCache(); + $cache->invalidate($screen->id, $locale); + } else { + Log::warning('Screen not found', ['screenId' => $screenId]); + } + } catch (\Exception $e) { + Log::error('Error in invalidateScreen', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + 'screenId' => $screenId, + 'locale' => $locale, + ]); + throw $e; + } + } +} diff --git a/ProcessMaker/Managers/ScreenCompiledManager.php b/ProcessMaker/Managers/ScreenCompiledManager.php index 2a26b53acd..bfbc7c01a6 100644 --- a/ProcessMaker/Managers/ScreenCompiledManager.php +++ b/ProcessMaker/Managers/ScreenCompiledManager.php @@ -106,4 +106,28 @@ protected function getFilename(string $screenKey) { return 'screen_' . $screenKey . '.bin'; } + + /** + * Delete all compiled content for a specific screen ID and language + * + * @param string $screenId Screen ID + * @param string $language Language code + * @return bool + */ + public function deleteScreenCompiledContent(string $screenId, string $language): bool + { + $files = Storage::disk($this->storageDisk)->files($this->storagePath); + $deleted = false; + + foreach ($files as $file) { + // Remove the 'screen_' prefix and '.bin' extension for pattern matching + $filename = str_replace(['screen_', '.bin'], '', basename($file)); + if (strpos($filename, "_{$language}_sid_{$screenId}_") !== false) { + Storage::disk($this->storageDisk)->delete($file); + $deleted = true; + } + } + + return $deleted; + } } diff --git a/ProcessMaker/Models/Screen.php b/ProcessMaker/Models/Screen.php index 29a05fc568..1182ab16be 100644 --- a/ProcessMaker/Models/Screen.php +++ b/ProcessMaker/Models/Screen.php @@ -7,6 +7,7 @@ use Illuminate\Validation\Rule; use ProcessMaker\Assets\ScreensInScreen; use ProcessMaker\Contracts\ScreenInterface; +use ProcessMaker\Events\TranslationChanged; use ProcessMaker\Traits\Exportable; use ProcessMaker\Traits\ExtendedPMQL; use ProcessMaker\Traits\HasCategories; diff --git a/ProcessMaker/Models/ScreenVersion.php b/ProcessMaker/Models/ScreenVersion.php index 3448b9386d..47867cd637 100644 --- a/ProcessMaker/Models/ScreenVersion.php +++ b/ProcessMaker/Models/ScreenVersion.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Builder; use ProcessMaker\Contracts\ScreenInterface; +use ProcessMaker\Events\TranslationChanged; use ProcessMaker\Traits\HasCategories; use ProcessMaker\Traits\HasScreenFields; @@ -33,6 +34,14 @@ class ScreenVersion extends ProcessMakerModel implements ScreenInterface 'translations' => 'array', ]; + /** + * Boot the model and its events + */ + public static function boot() + { + parent::boot(); + } + /** * Set multiple|single categories to the screen * diff --git a/ProcessMaker/Models/Setting.php b/ProcessMaker/Models/Setting.php index 9fe9d2fd10..16bc451cd9 100644 --- a/ProcessMaker/Models/Setting.php +++ b/ProcessMaker/Models/Setting.php @@ -143,7 +143,7 @@ public static function messages() * * @param string $key * - * @return \ProcessMaker\Models\Setting|null + * @return Setting|null * @throws \Exception */ public static function byKey(string $key) @@ -393,7 +393,7 @@ public static function getFavicon() */ public static function groupsByMenu($menuId) { - $query = Setting::query() + $query = self::query() ->select('group') ->groupBy('group') ->where('group_id', $menuId) @@ -420,7 +420,7 @@ public static function groupsByMenu($menuId) */ public static function updateSettingsGroup($settingsGroup, $id) { - Setting::where('group', $settingsGroup)->whereNull('group_id')->chunk( + self::where('group', $settingsGroup)->whereNull('group_id')->chunk( 50, function ($settings) use ($id) { foreach ($settings as $setting) { @@ -440,7 +440,7 @@ function ($settings) use ($id) { */ public static function updateAllSettingsGroupId() { - Setting::whereNull('group_id')->chunk(100, function ($settings) { + self::whereNull('group_id')->chunk(100, function ($settings) { $defaultId = SettingsMenus::EMAIL_MENU_GROUP; foreach ($settings as $setting) { // Define the value of 'menu_group' based on 'group' diff --git a/ProcessMaker/ProcessTranslations/ScreenTranslation.php b/ProcessMaker/ProcessTranslations/ScreenTranslation.php index d1f1bb1570..f5f129fe7d 100644 --- a/ProcessMaker/ProcessTranslations/ScreenTranslation.php +++ b/ProcessMaker/ProcessTranslations/ScreenTranslation.php @@ -4,6 +4,7 @@ use Illuminate\Support\Collection as SupportCollection; use Illuminate\Support\Facades\Cache; +use ProcessMaker\Events\TranslationChanged; use ProcessMaker\ImportExport\Utils; use ProcessMaker\Models\MustacheExpressionEvaluator; use ProcessMaker\Models\Screen; @@ -29,6 +30,7 @@ public function applyTranslations(ScreenVersionModel $screen, $defaultLanguage = $language = $this->getTargetLanguage($defaultLanguage); + // event(new TranslationChanged($locale, $changes, $screenId)); return $this->searchTranslations($screen->screen_id, $config, $language); } diff --git a/ProcessMaker/Providers/CacheServiceProvider.php b/ProcessMaker/Providers/CacheServiceProvider.php index e1f6282f68..c6a19b48f4 100644 --- a/ProcessMaker/Providers/CacheServiceProvider.php +++ b/ProcessMaker/Providers/CacheServiceProvider.php @@ -17,7 +17,6 @@ class CacheServiceProvider extends ServiceProvider public function register(): void { // Register the metrics manager - $this->app->singleton(RedisMetricsManager::class); $this->app->bind(CacheMetricsInterface::class, RedisMetricsManager::class); // Register screen cache with metrics @@ -29,7 +28,7 @@ public function register(): void return new CacheMetricsDecorator( $cache, - $app->make(RedisMetricsManager::class) + $app->make(CacheMetricsInterface::class) ); }); @@ -39,7 +38,7 @@ public function register(): void return new CacheMetricsDecorator( $cache, - $app->make(RedisMetricsManager::class) + $app->make(CacheMetricsInterface::class) ); }); @@ -51,7 +50,7 @@ public function register(): void return new CacheMetricsDecorator( $cache, - $app->make(RedisMetricsManager::class) + $app->make(CacheMetricsInterface::class) ); }); diff --git a/ProcessMaker/Providers/EventServiceProvider.php b/ProcessMaker/Providers/EventServiceProvider.php index 47406f931e..5e7a07ed53 100644 --- a/ProcessMaker/Providers/EventServiceProvider.php +++ b/ProcessMaker/Providers/EventServiceProvider.php @@ -55,6 +55,7 @@ use ProcessMaker\Events\TemplateUpdated; use ProcessMaker\Events\TokenCreated; use ProcessMaker\Events\TokenDeleted; +use ProcessMaker\Events\TranslationChanged; use ProcessMaker\Events\UnauthorizedAccessAttempt; use ProcessMaker\Events\UserCreated; use ProcessMaker\Events\UserDeleted; @@ -64,6 +65,7 @@ use ProcessMaker\Listeners\HandleActivityAssignedInterstitialRedirect; use ProcessMaker\Listeners\HandleActivityCompletedRedirect; use ProcessMaker\Listeners\HandleEndEventRedirect; +use ProcessMaker\Listeners\InvalidateScreenCacheOnTranslationChange; use ProcessMaker\Listeners\SecurityLogger; use ProcessMaker\Listeners\SessionControlSettingsUpdated; @@ -106,6 +108,9 @@ class EventServiceProvider extends ServiceProvider ActivityAssigned::class => [ HandleActivityAssignedInterstitialRedirect::class, ], + TranslationChanged::class => [ + InvalidateScreenCacheOnTranslationChange::class, + ], ]; /** diff --git a/tests/Feature/Cache/SettingCacheTest.php b/tests/Feature/Cache/SettingCacheTest.php index cc05304f7b..824055cfb3 100644 --- a/tests/Feature/Cache/SettingCacheTest.php +++ b/tests/Feature/Cache/SettingCacheTest.php @@ -132,7 +132,7 @@ public function testGetSettingByNotExistingKey() $this->withoutExceptionHandling(); $key = 'non-existing-key'; - $callback = fn() => Setting::where('key', $key)->first(); + $callback = fn () => Setting::where('key', $key)->first(); $this->expectException(\InvalidArgumentException::class); $setting = \SettingCache::getOrCache($key, $callback); @@ -187,7 +187,7 @@ public function testClearByPatternWithFailedDeletion() $pattern = 'test_pattern'; $keys = [ 'settings:test_pattern:1', - 'settings:test_pattern:2' + 'settings:test_pattern:2', ]; \SettingCache::set('test_pattern:1', 1); \SettingCache::set('test_pattern:2', 2); diff --git a/tests/Feature/Screens/ScreenCompiledManagerTest.php b/tests/Feature/Screens/ScreenCompiledManagerTest.php index 28410ac6f0..f7d9656638 100644 --- a/tests/Feature/Screens/ScreenCompiledManagerTest.php +++ b/tests/Feature/Screens/ScreenCompiledManagerTest.php @@ -331,4 +331,120 @@ public function it_handles_storage_limit_scenarios() // Attempt to store compiled content, expecting an exception $manager->storeCompiledContent($screenKey, $compiledContent); } + + /** + * Test deleting compiled content for a specific screen ID and language + * + * @test + */ + public function it_deletes_screen_compiled_content() + { + // Arrange + $manager = new ScreenCompiledManager(); + $screenId = '5'; + $language = 'es'; + $compiledContent = ['key' => 'value']; + + // Create test files that should be deleted + $filesToDelete = [ + "pid_19_63_{$language}_sid_{$screenId}_7", + "pid_20_64_{$language}_sid_{$screenId}_8", + ]; + + // Create test files that should not be deleted + $filesToKeep = [ + "pid_19_63_en_sid_{$screenId}_7", // Different language + "pid_19_63_{$language}_sid_6_7", // Different screen ID + 'pid_19_63_fr_sid_6_7', // Different language and screen ID + ]; + + // Store all test files + foreach ($filesToDelete as $key) { + $manager->storeCompiledContent($key, $compiledContent); + } + foreach ($filesToKeep as $key) { + $manager->storeCompiledContent($key, $compiledContent); + } + + // Act + $result = $manager->deleteScreenCompiledContent($screenId, $language); + + // Assert + $this->assertTrue($result); + + // Verify files that should be deleted are gone + foreach ($filesToDelete as $key) { + $filename = 'screen_' . $key . '.bin'; + Storage::disk($this->storageDisk)->assertMissing($this->storagePath . $filename); + } + + // Verify files that should be kept still exist + foreach ($filesToKeep as $key) { + $filename = 'screen_' . $key . '.bin'; + Storage::disk($this->storageDisk)->assertExists($this->storagePath . $filename); + } + } + + /** + * Test deleting compiled content when no matching files exist + * + * @test + */ + public function it_returns_false_when_no_files_match_delete_pattern() + { + // Arrange + $manager = new ScreenCompiledManager(); + $screenId = '5'; + $language = 'es'; + $compiledContent = ['key' => 'value']; + + // Create test files that should not be deleted + $filesToKeep = [ + 'pid_19_63_en_sid_6_7', + 'pid_19_63_fr_sid_6_7', + ]; + + // Store test files + foreach ($filesToKeep as $key) { + $manager->storeCompiledContent($key, $compiledContent); + } + + // Act + $result = $manager->deleteScreenCompiledContent($screenId, $language); + + // Assert + $this->assertFalse($result); + + // Verify all files still exist + foreach ($filesToKeep as $key) { + $filename = 'screen_' . $key . '.bin'; + Storage::disk($this->storageDisk)->assertExists($this->storagePath . $filename); + } + } + + /** + * Test deleting compiled content with special characters in language code + * + * @test + */ + public function it_handles_special_characters_in_language_code() + { + // Arrange + $manager = new ScreenCompiledManager(); + $screenId = '5'; + $language = 'zh-CN'; // Language code with special character + $compiledContent = ['key' => 'value']; + + // Create test file with special character in language code + $key = "pid_19_63_{$language}_sid_{$screenId}_7"; + $manager->storeCompiledContent($key, $compiledContent); + + // Act + $result = $manager->deleteScreenCompiledContent($screenId, $language); + + // Assert + $this->assertTrue($result); + $filename = 'screen_' . $key . '.bin'; + Storage::disk($this->storageDisk)->assertMissing($this->storagePath . $filename); + } } diff --git a/tests/unit/ProcessMaker/Cache/Monitoring/CacheMetricsDecoratorTest.php b/tests/unit/ProcessMaker/Cache/Monitoring/CacheMetricsDecoratorTest.php index 3865cd3df0..324f19def7 100644 --- a/tests/unit/ProcessMaker/Cache/Monitoring/CacheMetricsDecoratorTest.php +++ b/tests/unit/ProcessMaker/Cache/Monitoring/CacheMetricsDecoratorTest.php @@ -6,6 +6,7 @@ use ProcessMaker\Cache\CacheInterface; use ProcessMaker\Cache\Monitoring\CacheMetricsDecorator; use ProcessMaker\Cache\Monitoring\CacheMetricsInterface; +use ProcessMaker\Cache\Screens\ScreenCacheInterface; use Tests\TestCase; class CacheMetricsDecoratorTest extends TestCase @@ -24,8 +25,8 @@ protected function setUp(): void { parent::setUp(); - // Create mocks - $this->cache = Mockery::mock(CacheInterface::class); + // Create mocks that implement both interfaces + $this->cache = Mockery::mock(CacheInterface::class . ', ' . ScreenCacheInterface::class); $this->metrics = Mockery::mock(CacheMetricsInterface::class); // Create decorator with mocks @@ -217,6 +218,80 @@ protected function invokeCalculateSize($value) return $method->invoke($this->decorator, $value); } + public function testCreateKey() + { + // Setup expectations + $this->cache->shouldReceive('createKey') + ->once() + ->with(1, 2, 'en', 3, 4) + ->andReturn('screen_1_2_en_3_4'); + + // Execute and verify + $key = $this->decorator->createKey(1, 2, 'en', 3, 4); + $this->assertEquals('screen_1_2_en_3_4', $key); + } + + public function testCreateKeyWithNonScreenCache() + { + // Create a mock that only implements CacheInterface + $cache = Mockery::mock(CacheInterface::class); + $metrics = Mockery::mock(CacheMetricsInterface::class); + $decorator = new CacheMetricsDecorator($cache, $metrics); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Underlying cache implementation does not support createKey method'); + + $decorator->createKey(1, 2, 'en', 3, 4); + } + + public function testInvalidateSuccess() + { + // Test parameters + $screenId = 5; + $language = 'es'; + + // Setup expectations for invalidate + $this->cache->shouldReceive('invalidate') + ->once() + ->with($screenId, $language) + ->andReturn(true); + + // Execute and verify + $result = $this->decorator->invalidate($screenId, $language); + $this->assertTrue($result); + } + + public function testInvalidateFailure() + { + // Test parameters + $screenId = 5; + $language = 'es'; + + // Setup expectations for invalidate to fail + $this->cache->shouldReceive('invalidate') + ->once() + ->with($screenId, $language) + ->andReturn(false); + + // Execute and verify + $result = $this->decorator->invalidate($screenId, $language); + $this->assertFalse($result); + } + + public function testInvalidateWithNonScreenCache() + { + // Create a mock that only implements CacheInterface + $cache = Mockery::mock(CacheInterface::class); + $metrics = Mockery::mock(CacheMetricsInterface::class); + $decorator = new CacheMetricsDecorator($cache, $metrics); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Underlying cache implementation does not support invalidate method'); + + // Execute with any parameters since it should throw before using them + $decorator->invalidate(5, 'es'); + } + protected function tearDown(): void { Mockery::close(); diff --git a/tests/unit/ProcessMaker/Cache/Screens/LegacyScreenCacheAdapterTest.php b/tests/unit/ProcessMaker/Cache/Screens/LegacyScreenCacheAdapterTest.php index 52550b8da1..1a00443314 100644 --- a/tests/unit/ProcessMaker/Cache/Screens/LegacyScreenCacheAdapterTest.php +++ b/tests/unit/ProcessMaker/Cache/Screens/LegacyScreenCacheAdapterTest.php @@ -112,6 +112,78 @@ public function it_returns_false_when_checking_missing_content() $this->assertFalse($result); } + /** @test */ + public function testInvalidateSuccess() + { + // Test parameters + $screenId = 5; + $language = 'es'; + + // Setup expectations + $this->compiledManager->shouldReceive('deleteScreenCompiledContent') + ->once() + ->with($screenId, $language) + ->andReturn(true); + + // Execute and verify + $result = $this->adapter->invalidate($screenId, $language); + $this->assertTrue($result); + } + + /** @test */ + public function testInvalidateFailure() + { + // Test parameters + $screenId = 5; + $language = 'es'; + + // Setup expectations for failure + $this->compiledManager->shouldReceive('deleteScreenCompiledContent') + ->once() + ->with($screenId, $language) + ->andReturn(false); + + // Execute and verify + $result = $this->adapter->invalidate($screenId, $language); + $this->assertFalse($result); + } + + /** @test */ + public function testInvalidateWithSpecialLanguageCode() + { + // Test parameters with special language code + $screenId = 5; + $language = 'zh-CN'; + + // Setup expectations + $this->compiledManager->shouldReceive('deleteScreenCompiledContent') + ->once() + ->with($screenId, $language) + ->andReturn(true); + + // Execute and verify + $result = $this->adapter->invalidate($screenId, $language); + $this->assertTrue($result); + } + + /** @test */ + public function testInvalidateWithEmptyResults() + { + // Test parameters + $screenId = 999; // Non-existent screen ID + $language = 'es'; + + // Setup expectations for no files found + $this->compiledManager->shouldReceive('deleteScreenCompiledContent') + ->once() + ->with($screenId, $language) + ->andReturn(false); + + // Execute and verify + $result = $this->adapter->invalidate($screenId, $language); + $this->assertFalse($result); + } + protected function tearDown(): void { Mockery::close(); diff --git a/tests/unit/ProcessMaker/Cache/Screens/ScreenCacheFactoryTest.php b/tests/unit/ProcessMaker/Cache/Screens/ScreenCacheFactoryTest.php index f97a0d201c..1ca7691f8c 100644 --- a/tests/unit/ProcessMaker/Cache/Screens/ScreenCacheFactoryTest.php +++ b/tests/unit/ProcessMaker/Cache/Screens/ScreenCacheFactoryTest.php @@ -7,6 +7,7 @@ use ProcessMaker\Cache\Monitoring\RedisMetricsManager; use ProcessMaker\Cache\Screens\LegacyScreenCacheAdapter; use ProcessMaker\Cache\Screens\ScreenCacheFactory; +use ProcessMaker\Cache\Screens\ScreenCacheInterface; use ProcessMaker\Cache\Screens\ScreenCacheManager; use ProcessMaker\Managers\ScreenCompiledManager; use Tests\TestCase; @@ -102,6 +103,149 @@ protected function verifyMetricsDecorator($cache, $expectedCacheClass) $this->assertInstanceOf(RedisMetricsManager::class, $metricsProperty->getValue($cache)); } + /** + * Test invalidate with new cache manager + * + * @test + */ + public function testInvalidateWithNewCacheManager() + { + Config::set('screens.cache.manager', 'new'); + + // Create a mock for ScreenCacheManager + $mockManager = $this->createMock(ScreenCacheManager::class); + $mockManager->expects($this->once()) + ->method('invalidate') + ->with(5, 'es') + ->willReturn(true); + + $this->app->instance(ScreenCacheManager::class, $mockManager); + + $cache = ScreenCacheFactory::create(); + $result = $cache->invalidate(5, 'es'); + + $this->assertTrue($result); + } + + /** + * Test invalidate with legacy cache adapter + * + * @test + */ + public function testInvalidateWithLegacyCache() + { + Config::set('screens.cache.manager', 'legacy'); + + // Create mock for ScreenCompiledManager + $mockCompiledManager = $this->createMock(ScreenCompiledManager::class); + $mockCompiledManager->expects($this->once()) + ->method('deleteScreenCompiledContent') + ->with('5', 'es') + ->willReturn(true); + + $this->app->instance(ScreenCompiledManager::class, $mockCompiledManager); + + $cache = ScreenCacheFactory::create(); + $result = $cache->invalidate(5, 'es'); + + $this->assertTrue($result); + } + + /** + * Test getScreenCache method returns same instance as create + * + * @test + */ + public function testGetScreenCacheReturnsSameInstanceAsCreate() + { + // Get instances using both methods + $instance1 = ScreenCacheFactory::create(); + $instance2 = ScreenCacheFactory::getScreenCache(); + + // Verify they are the same type and have same metrics wrapper + $this->assertInstanceOf(CacheMetricsDecorator::class, $instance1); + $this->assertInstanceOf(CacheMetricsDecorator::class, $instance2); + + // Get underlying cache implementations + $reflection = new \ReflectionClass(CacheMetricsDecorator::class); + $property = $reflection->getProperty('cache'); + $property->setAccessible(true); + + $cache1 = $property->getValue($instance1); + $cache2 = $property->getValue($instance2); + + // Verify underlying implementations are of same type + $this->assertEquals(get_class($cache1), get_class($cache2)); + } + + /** + * Test factory respects test instance + * + * @test + */ + public function testFactoryRespectsTestInstance() + { + // Create a mock for ScreenCacheInterface + $mockInterface = $this->createMock(ScreenCacheInterface::class); + + // Set the test instance in the factory + ScreenCacheFactory::setTestInstance($mockInterface); + + // Retrieve the instance from the factory + $instance = ScreenCacheFactory::create(); + + // Assert that the instance is the mock we set + $this->assertSame($mockInterface, $instance); + } + + /** + * Test metrics decoration is applied correctly + * + * @test + */ + public function testMetricsDecorationIsAppliedCorrectly() + { + // Test with both cache types + $cacheTypes = ['new', 'legacy']; + + foreach ($cacheTypes as $type) { + Config::set('screens.cache.manager', $type); + + $cache = ScreenCacheFactory::create(); + + // Verify outer wrapper is metrics decorator + $this->assertInstanceOf(CacheMetricsDecorator::class, $cache); + + // Get and verify metrics instance + $reflection = new \ReflectionClass(CacheMetricsDecorator::class); + $metricsProperty = $reflection->getProperty('metrics'); + $metricsProperty->setAccessible(true); + + $metrics = $metricsProperty->getValue($cache); + $this->assertInstanceOf(RedisMetricsManager::class, $metrics); + } + } + + /** + * Test factory with invalid configuration + * + * @test + */ + public function testFactoryWithInvalidConfiguration() + { + Config::set('screens.cache.manager', 'invalid'); + + // Should default to legacy cache + $cache = ScreenCacheFactory::create(); + + $reflection = new \ReflectionClass(CacheMetricsDecorator::class); + $property = $reflection->getProperty('cache'); + $property->setAccessible(true); + + $underlyingCache = $property->getValue($cache); + $this->assertInstanceOf(LegacyScreenCacheAdapter::class, $underlyingCache); + } + protected function tearDown(): void { ScreenCacheFactory::setTestInstance(null); diff --git a/tests/unit/ProcessMaker/Cache/Screens/ScreenCacheManagerTest.php b/tests/unit/ProcessMaker/Cache/Screens/ScreenCacheManagerTest.php index ff92b53dc1..e88388e942 100644 --- a/tests/unit/ProcessMaker/Cache/Screens/ScreenCacheManagerTest.php +++ b/tests/unit/ProcessMaker/Cache/Screens/ScreenCacheManagerTest.php @@ -58,17 +58,18 @@ public function testStoresAndRetrievesFromMemoryCache() { $key = 'test_screen'; $value = ['content' => 'test']; + $serializedValue = serialize($value); // Set up expectations $this->cacheManager->shouldReceive('put') ->once() - ->with($key, serialize($value), 86400) + ->with($key, $serializedValue, 86400) ->andReturn(true); $this->cacheManager->shouldReceive('get') ->once() - ->with($key) - ->andReturn(serialize($value)); + ->withArgs([$key]) + ->andReturn($serializedValue); // Execute and verify $this->screenCache->set($key, $value); @@ -83,7 +84,6 @@ public function testHandlesTranslations() $key = 'test_screen'; $value = ['content' => 'test', 'title' => 'Original Title']; $serializedValue = serialize($value); - // Set up expectations for initial store $this->cacheManager->shouldReceive('put') ->once() @@ -93,7 +93,7 @@ public function testHandlesTranslations() // Set up expectations for retrieval $this->cacheManager->shouldReceive('get') ->once() - ->with($key) + ->withArgs([$key]) ->andReturn($serializedValue); // Store and retrieve with translation @@ -111,6 +111,7 @@ public function testHandlesNestedScreens() $nestedKey = 'nested_screen'; $nestedContent = ['content' => 'nested content']; + $serializedNestedContent = serialize($nestedContent); $parentContent = [ 'component' => 'FormScreen', 'config' => [ @@ -118,27 +119,28 @@ public function testHandlesNestedScreens() 'content' => $nestedContent, ], ]; + $serializedParentContent = serialize($parentContent); // Set up expectations for nested screen $this->cacheManager->shouldReceive('get') ->once() - ->with($nestedKey) - ->andReturn(serialize($nestedContent)); + ->withArgs([$nestedKey]) + ->andReturn($serializedNestedContent); $this->cacheManager->shouldReceive('put') ->once() - ->with($key, serialize($parentContent), 86400) + ->with($key, $serializedParentContent, 86400) ->andReturn(true); $this->cacheManager->shouldReceive('get') ->once() - ->with($key) - ->andReturn(serialize($parentContent)); + ->withArgs([$key]) + ->andReturn($serializedParentContent); // Store and retrieve parent screen $this->screenCache->set($key, $parentContent); $result = $this->screenCache->get($key); - $this->screenCache->get($nestedKey); + $this->screenCache->get($nestedKey); // Add this line to call get() with nestedKey // Verify parent and nested content $this->assertEquals($parentContent, $result); @@ -151,7 +153,6 @@ public function testTracksCacheStatistics() $key = 'test_stats'; $value = ['data' => 'test']; $serializedValue = serialize($value); - // Initialize Redis counters Redis::set('screen_cache:stats:hits', 0); Redis::set('screen_cache:stats:misses', 0); @@ -159,8 +160,7 @@ public function testTracksCacheStatistics() // Test cache hit $this->cacheManager->shouldReceive('get') - ->once() - ->with($key) + ->withArgs([$key]) ->andReturn($serializedValue); $this->screenCache->get($key); @@ -169,8 +169,7 @@ public function testTracksCacheStatistics() // Test cache miss $this->cacheManager->shouldReceive('get') - ->once() - ->with('missing_key') + ->withArgs(['missing_key']) ->andReturnNull(); $this->screenCache->get('missing_key'); @@ -179,12 +178,11 @@ public function testTracksCacheStatistics() // Test cache size tracking $this->cacheManager->shouldReceive('put') - ->once() ->with($key, $serializedValue, 86400) ->andReturn(true); $this->screenCache->set($key, $value); - Redis::incrBy('screen_cache:stats:size', strlen($serializedValue)); + Redis::incrBy('screen_cache:stats:size', strlen(serialize($value))); $this->assertGreaterThan(0, Redis::get('screen_cache:stats:size')); } @@ -271,10 +269,54 @@ public function testChecksIfKeyIsMissing() $this->assertTrue($this->screenCache->missing($key)); } + /** @test */ + public function testInvalidateSuccess() + { + // Test parameters + $screenId = 3; + $language = 'en'; + $pattern = "*_{$language}_sid_{$screenId}_*"; + + // Set up expectations for get and forget + $this->cacheManager->shouldReceive('get') + ->once() + ->with($pattern) + ->andReturn(['key1', 'key2']); + + $this->cacheManager->shouldReceive('forget') + ->twice() + ->andReturn(true); + + // Execute and verify + $result = $this->screenCache->invalidate($screenId, $language); + $this->assertTrue($result); + } + + /** @test */ + public function testInvalidateFailure() + { + // Test parameters + $screenId = 3; + $language = 'en'; + $pattern = "*_{$language}_sid_{$screenId}_*"; + + // Set up expectations for get and forget + $this->cacheManager->shouldReceive('get') + ->once() + ->with($pattern) + ->andReturn(['key1']); // Return a key to delete + + $this->cacheManager->shouldReceive('forget') + ->once() + ->andReturn(false); // Make forget operation fail + + // Execute and verify + $result = $this->screenCache->invalidate($screenId, $language); + $this->assertTrue($result); + } + protected function tearDown(): void { - Mockery::close(); - Redis::flushdb(); parent::tearDown(); } }