From 33fe4f68394d85b1963bba92d008db604d84556e Mon Sep 17 00:00:00 2001 From: Eleazar Resendez Date: Wed, 11 Dec 2024 13:13:52 -0600 Subject: [PATCH] Add ETag cache invalidation tests - Introduced a new test class `HandleEtagCacheInvalidationTest` to verify ETag behavior upon database updates. - Implemented tests to ensure ETag changes when the underlying data is modified and that the correct ETag is returned for subsequent requests. - Updated existing `HandleEtagTest` to include a test for returning 304 Not Modified when the ETag matches the client-provided value. --- .../Http/Resources/Caching/EtagManager.php | 10 +-- .../Etag/HandleEtagCacheInvalidationTest.php | 65 +++++++++++++++++++ tests/Feature/Etag/HandleEtagTest.php | 33 +++++++++- 3 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 tests/Feature/Etag/HandleEtagCacheInvalidationTest.php diff --git a/ProcessMaker/Http/Resources/Caching/EtagManager.php b/ProcessMaker/Http/Resources/Caching/EtagManager.php index edd6a822b6..71d41ad950 100644 --- a/ProcessMaker/Http/Resources/Caching/EtagManager.php +++ b/ProcessMaker/Http/Resources/Caching/EtagManager.php @@ -53,10 +53,12 @@ public static function generateEtagFromTables(array $tables, string $source = 'u // If the source is 'etag_version', use a cached version key as the source of truth. $lastUpdated = collect($tables)->map(function ($table) use ($source) { if ($source === 'etag_version') { - // This is not currently implemented but serves as a placeholder for future flexibility. - // The idea is to use a cached version key (e.g., "etag_version_table_name") as the source of truth. - // This would allow us to version the ETag dynamically and invalidate it using model observers or other mechanisms. - // If implemented, observers can increment this version key whenever the corresponding table is updated. + /** + * This is not currently implemented but serves as a placeholder for future flexibility. + * The idea is to use a cached version key (e.g., "etag_version_table_name") as the source of truth. + * This would allow us to version the ETag dynamically and invalidate it using model observers or other mechanisms. + * If implemented, observers can increment this version key whenever the corresponding table is updated. + */ return Cache::get("etag_version_{$table}", 0); } diff --git a/tests/Feature/Etag/HandleEtagCacheInvalidationTest.php b/tests/Feature/Etag/HandleEtagCacheInvalidationTest.php new file mode 100644 index 0000000000..f9a3663c61 --- /dev/null +++ b/tests/Feature/Etag/HandleEtagCacheInvalidationTest.php @@ -0,0 +1,65 @@ +get(self::TEST_ROUTE, function () { + return response($this->response, 200); + }) + ->defaults('etag_tables', 'processes'); + } + + public function testEtagInvalidatesOnDatabaseUpdate() + { + $user = User::factory()->create(); + $this->actingAs($user); + + // Create a process record to simulate database changes. + $process = Process::factory()->create([ + 'updated_at' => now()->yesterday(), + ]); + + // First request: Get the initial ETag. + $response = $this->get(self::TEST_ROUTE); + $initialEtag = $response->headers->get('ETag'); + $this->assertNotNull($initialEtag, 'Initial ETag was set'); + + // Simulate a database update by changing `updated_at`. + $process->update(['name' => $this->faker->name]); + + // Second request: ETag should change due to the database update. + $responseAfterUpdate = $this->get(self::TEST_ROUTE); + $newEtag = $responseAfterUpdate->headers->get('ETag'); + + $this->assertNotNull($newEtag, 'New ETag was set after database update'); + $this->assertNotEquals($initialEtag, $newEtag, 'ETag changed after database update'); + + // Third request: Simulate a client sending the old ETag. + $responseWithOldEtag = $this->withHeaders(['If-None-Match' => $initialEtag]) + ->get(self::TEST_ROUTE); + + $responseWithOldEtag->assertStatus(200); + $responseWithOldEtag->assertHeader('ETag', $newEtag, 'Response did not return the updated ETag'); + } +} diff --git a/tests/Feature/Etag/HandleEtagTest.php b/tests/Feature/Etag/HandleEtagTest.php index e933260f04..af9c731e29 100644 --- a/tests/Feature/Etag/HandleEtagTest.php +++ b/tests/Feature/Etag/HandleEtagTest.php @@ -3,8 +3,7 @@ namespace ProcessMaker\Tests\Feature\Etag; use Illuminate\Support\Facades\Route; -use ProcessMaker\Http\Middleware\Etag\HandleEtag; -use ProcessMaker\Http\Resources\Caching\EtagManager; +use ProcessMaker\Models\Process; use ProcessMaker\Models\User; use Tests\TestCase; @@ -19,7 +18,7 @@ public function setUp(): void parent::setUp(); // Define a route that uses the HandleEtag middleware. - Route::middleware(HandleEtag::class)->any(self::TEST_ROUTE, function () { + Route::middleware('etag')->any(self::TEST_ROUTE, function () { return response($this->response, 200); }); } @@ -84,4 +83,32 @@ public function testDefaultGetEtagGeneratesCorrectEtagWithUser() $expectedEtag = '"' . md5($user->id . $this->response) . '"'; $response->assertHeader('ETag', $expectedEtag); } + + public function testReturns304NotModifiedWhenEtagMatchesTables() + { + $user = User::factory()->create(); + $this->actingAs($user); + + Process::factory()->create([ + 'updated_at' => now()->yesterday(), + ]); + + Route::middleware('etag')->any(self::TEST_ROUTE, function () { + return response($this->response, 200); + })->defaults('etag_tables', 'processes'); + + // Initial request to get the ETAg. + $response = $this->get(self::TEST_ROUTE); + $etag = $response->headers->get('ETag'); + $this->assertNotNull($etag, 'ETag should be set in the initial response'); + + // Perform a second request sending the `If-None-Match`. + $responseWithMatchingEtag = $this->withHeaders(['If-None-Match' => $etag]) + ->get(self::TEST_ROUTE); + + // Verify response is 304 Not Modified. + $responseWithMatchingEtag->assertStatus(304); + $this->assertEmpty($responseWithMatchingEtag->getContent(), 'Response content is not empty for 304 Not Modified'); + $this->assertEquals($etag, $responseWithMatchingEtag->headers->get('ETag'), 'ETag does not match the client-provided If-None-Match'); + } }