Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d187bd4
feat(etag): add ETag middleware and customizable ETag generation
eiresendez Nov 26, 2024
d3e4d2e
refactor: combine ETag generation and validation into a single middle…
eiresendez Nov 28, 2024
d5dd9a8
refactor: add tests for combined ETag middleware and remove deprecate…
eiresendez Nov 28, 2024
bc39c24
test: add middleware tests for user-specific ETag generation
eiresendez Nov 28, 2024
ea9795c
Add ETag to Screen Responses
eiresendez Nov 28, 2024
924cd3c
Merge branch 'epic/FOUR-20929' into story/FOUR-20933
eiresendez Nov 29, 2024
c283de6
Merge pull request #7771 from ProcessMaker/story/FOUR-20933
eiresendez Dec 2, 2024
7d33532
Merge branch 'release-2024-fall' into epic/FOUR-20929
eiresendez Dec 2, 2024
f058177
Exclude 'active_at' field from TaskResource and requestor serializati…
eiresendez Dec 2, 2024
48cd7d6
Implement ETag Caching for Task Responses
eiresendez Dec 2, 2024
8b327c8
restrict ETag handling to GET/HEAD and enforce user-specific caching
eiresendez Dec 3, 2024
dae2468
Add ETag middleware to API group to support package routes
eiresendez Dec 5, 2024
8a94369
Apply ETag middleware to v1.1 routes for consistent caching
eiresendez Dec 5, 2024
d748403
feat: add flexibility to ETag generation with sources
eiresendez Dec 5, 2024
cdaa80d
add ETag middleware to startProcesses route
eiresendez Dec 6, 2024
e64f7a7
Merge pull request #7786 from ProcessMaker/story/FOUR-20944
eiresendez Dec 6, 2024
da5ba5d
Add detailed ETag implementation and testing documentation
eiresendez Dec 10, 2024
33fe4f6
Add ETag cache invalidation tests
eiresendez Dec 11, 2024
428b0ee
Enhance ETag middleware to log dynamic endpoint detection
eiresendez Dec 12, 2024
24afdfc
Merge pull request #7817 from ProcessMaker/story/FOUR-20950
eiresendez Dec 13, 2024
f28fcb5
Add configuration support for ETag logging and caching
eiresendez Dec 16, 2024
4e7afdc
Add ETag response time comparison for 200 vs 304
eiresendez Dec 17, 2024
d590373
Add ETag history tracking to detect dynamic endpoints
eiresendez Dec 17, 2024
8086a78
Add long-duration test to validate ETag stability under sustained load
eiresendez Dec 17, 2024
c295567
Merge pull request #7834 from ProcessMaker/story/FOUR-20954
eiresendez Dec 18, 2024
a6b7ee0
Merge branch 'release-2025-winter' into epic/FOUR-20929
eiresendez Jan 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions ProcessMaker/Http/Resources/Caching/EtagManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
65 changes: 65 additions & 0 deletions tests/Feature/Etag/HandleEtagCacheInvalidationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace ProcessMaker\Tests\Feature\Etag;

use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Route;
use ProcessMaker\Models\Process;
use ProcessMaker\Models\User;
use Tests\TestCase;

class HandleEtagCacheInvalidationTest extends TestCase
{
use WithFaker;

protected $faker;

private string $response = 'OK';

private const TEST_ROUTE = '/_test/etag-cache-invalidation';

public function setUp(): void
{
parent::setUp();

// Define a route with the etag middleware and etag_tables default.
Route::middleware('etag')
->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');
}
}
33 changes: 30 additions & 3 deletions tests/Feature/Etag/HandleEtagTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
});
}
Expand Down Expand Up @@ -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');
}
}