Skip to content

Commit 24afdfc

Browse files
authored
Merge pull request #7817 from ProcessMaker/story/FOUR-20950
2 parents da5ba5d + 33fe4f6 commit 24afdfc

3 files changed

Lines changed: 101 additions & 7 deletions

File tree

‎ProcessMaker/Http/Resources/Caching/EtagManager.php‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,12 @@ public static function generateEtagFromTables(array $tables, string $source = 'u
5353
// If the source is 'etag_version', use a cached version key as the source of truth.
5454
$lastUpdated = collect($tables)->map(function ($table) use ($source) {
5555
if ($source === 'etag_version') {
56-
// This is not currently implemented but serves as a placeholder for future flexibility.
57-
// The idea is to use a cached version key (e.g., "etag_version_table_name") as the source of truth.
58-
// This would allow us to version the ETag dynamically and invalidate it using model observers or other mechanisms.
59-
// If implemented, observers can increment this version key whenever the corresponding table is updated.
56+
/**
57+
* This is not currently implemented but serves as a placeholder for future flexibility.
58+
* The idea is to use a cached version key (e.g., "etag_version_table_name") as the source of truth.
59+
* This would allow us to version the ETag dynamically and invalidate it using model observers or other mechanisms.
60+
* If implemented, observers can increment this version key whenever the corresponding table is updated.
61+
*/
6062
return Cache::get("etag_version_{$table}", 0);
6163
}
6264

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
namespace ProcessMaker\Tests\Feature\Etag;
4+
5+
use Illuminate\Foundation\Testing\WithFaker;
6+
use Illuminate\Support\Facades\Route;
7+
use ProcessMaker\Models\Process;
8+
use ProcessMaker\Models\User;
9+
use Tests\TestCase;
10+
11+
class HandleEtagCacheInvalidationTest extends TestCase
12+
{
13+
use WithFaker;
14+
15+
protected $faker;
16+
17+
private string $response = 'OK';
18+
19+
private const TEST_ROUTE = '/_test/etag-cache-invalidation';
20+
21+
public function setUp(): void
22+
{
23+
parent::setUp();
24+
25+
// Define a route with the etag middleware and etag_tables default.
26+
Route::middleware('etag')
27+
->get(self::TEST_ROUTE, function () {
28+
return response($this->response, 200);
29+
})
30+
->defaults('etag_tables', 'processes');
31+
}
32+
33+
public function testEtagInvalidatesOnDatabaseUpdate()
34+
{
35+
$user = User::factory()->create();
36+
$this->actingAs($user);
37+
38+
// Create a process record to simulate database changes.
39+
$process = Process::factory()->create([
40+
'updated_at' => now()->yesterday(),
41+
]);
42+
43+
// First request: Get the initial ETag.
44+
$response = $this->get(self::TEST_ROUTE);
45+
$initialEtag = $response->headers->get('ETag');
46+
$this->assertNotNull($initialEtag, 'Initial ETag was set');
47+
48+
// Simulate a database update by changing `updated_at`.
49+
$process->update(['name' => $this->faker->name]);
50+
51+
// Second request: ETag should change due to the database update.
52+
$responseAfterUpdate = $this->get(self::TEST_ROUTE);
53+
$newEtag = $responseAfterUpdate->headers->get('ETag');
54+
55+
$this->assertNotNull($newEtag, 'New ETag was set after database update');
56+
$this->assertNotEquals($initialEtag, $newEtag, 'ETag changed after database update');
57+
58+
// Third request: Simulate a client sending the old ETag.
59+
$responseWithOldEtag = $this->withHeaders(['If-None-Match' => $initialEtag])
60+
->get(self::TEST_ROUTE);
61+
62+
$responseWithOldEtag->assertStatus(200);
63+
$responseWithOldEtag->assertHeader('ETag', $newEtag, 'Response did not return the updated ETag');
64+
}
65+
}

‎tests/Feature/Etag/HandleEtagTest.php‎

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
namespace ProcessMaker\Tests\Feature\Etag;
44

55
use Illuminate\Support\Facades\Route;
6-
use ProcessMaker\Http\Middleware\Etag\HandleEtag;
7-
use ProcessMaker\Http\Resources\Caching\EtagManager;
6+
use ProcessMaker\Models\Process;
87
use ProcessMaker\Models\User;
98
use Tests\TestCase;
109

@@ -19,7 +18,7 @@ public function setUp(): void
1918
parent::setUp();
2019

2120
// Define a route that uses the HandleEtag middleware.
22-
Route::middleware(HandleEtag::class)->any(self::TEST_ROUTE, function () {
21+
Route::middleware('etag')->any(self::TEST_ROUTE, function () {
2322
return response($this->response, 200);
2423
});
2524
}
@@ -84,4 +83,32 @@ public function testDefaultGetEtagGeneratesCorrectEtagWithUser()
8483
$expectedEtag = '"' . md5($user->id . $this->response) . '"';
8584
$response->assertHeader('ETag', $expectedEtag);
8685
}
86+
87+
public function testReturns304NotModifiedWhenEtagMatchesTables()
88+
{
89+
$user = User::factory()->create();
90+
$this->actingAs($user);
91+
92+
Process::factory()->create([
93+
'updated_at' => now()->yesterday(),
94+
]);
95+
96+
Route::middleware('etag')->any(self::TEST_ROUTE, function () {
97+
return response($this->response, 200);
98+
})->defaults('etag_tables', 'processes');
99+
100+
// Initial request to get the ETAg.
101+
$response = $this->get(self::TEST_ROUTE);
102+
$etag = $response->headers->get('ETag');
103+
$this->assertNotNull($etag, 'ETag should be set in the initial response');
104+
105+
// Perform a second request sending the `If-None-Match`.
106+
$responseWithMatchingEtag = $this->withHeaders(['If-None-Match' => $etag])
107+
->get(self::TEST_ROUTE);
108+
109+
// Verify response is 304 Not Modified.
110+
$responseWithMatchingEtag->assertStatus(304);
111+
$this->assertEmpty($responseWithMatchingEtag->getContent(), 'Response content is not empty for 304 Not Modified');
112+
$this->assertEquals($etag, $responseWithMatchingEtag->headers->get('ETag'), 'ETag does not match the client-provided If-None-Match');
113+
}
87114
}

0 commit comments

Comments
 (0)