Files
portal/app/tests/Feature/Audit/AuditChainRaceConditionTest.php
T
Дмитрий 15df5b4a46 test(audit): failing test for audit_chain_hash race condition
Two tests:
1. pcntl_fork concurrent-INSERT test (skipped on Windows/no pcntl) —
   demonstrates chain branch when 5 workers insert into the same partition
   simultaneously; passes after advisory-lock migration.
2. pg_locks advisory lock presence test (Windows-compatible) —
   verifies that pg_advisory_xact_lock is actually held in pg_locks
   during an INSERT transaction, proving the migration works.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 09:20:55 +03:00

127 lines
4.8 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\Tenant;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
uses(DatabaseTransactions::class);
/**
* Race-condition reproduction test for audit_chain_hash() trigger.
*
* Two tests:
* 1. pcntl_fork-based concurrent INSERT test — skipped on Windows (no pcntl).
* Expected: FAIL before migration (concurrent inserts branch the chain),
* PASS after migration (advisory lock serialises inserts).
*
* 2. pg_locks advisory lock presence test — runs on Windows.
* Asserts that within an INSERT transaction the advisory lock key derived
* from the partition OID is held (proves the lock is actually acquired).
*/
it(
'audit_chain_hash trigger preserves sequential chain under concurrent INSERTs',
function (): void {
$tenant = Tenant::factory()->create();
DB::statement('SET LOCAL app.current_tenant_id = '.$tenant->id);
$startCount = DB::table('activity_log')
->where('tenant_id', $tenant->id)
->count();
// Spawn 5 concurrent processes each inserting into activity_log for the same tenant.
// Without advisory lock, concurrent reads of prev_hash return the same value
// → multiple rows hash to the same prev → chain branch → validator fails.
$pids = [];
for ($i = 0; $i < 5; $i++) {
$pid = pcntl_fork();
if ($pid === 0) {
// Child: own DB connection, own transaction
DB::reconnect();
DB::statement('SET LOCAL app.current_tenant_id = '.$tenant->id);
DB::table('activity_log')->insert([
'tenant_id' => $tenant->id,
'event' => 'deal.created',
'context' => json_encode(['worker' => $i]),
'created_at' => now(),
]);
exit(0);
}
$pids[] = $pid;
}
foreach ($pids as $pid) {
pcntl_waitpid($pid, $status);
}
$rows = DB::table('activity_log')
->where('tenant_id', $tenant->id)
->orderBy('id')
->get(['id', 'log_hash']);
expect($rows->count())->toBe($startCount + 5);
// Run the chain validator; it should find no mismatches (after migration).
$exitCode = $this->artisan('audit:verify-chains')->run();
expect($exitCode)->toBe(0);
}
)->skip(! function_exists('pcntl_fork'), 'pcntl required for race-condition test (not available on Windows)');
it('audit_chain_hash holds pg_advisory_xact_lock on the partition OID during INSERT', function (): void {
$tenant = Tenant::factory()->create();
DB::statement('SET LOCAL app.current_tenant_id = '.$tenant->id);
// Resolve the OID of the current-month activity_log partition (or parent).
$partitionName = 'activity_log_y'.date('Y').'_m'.date('m');
$oid = DB::selectOne(
"SELECT COALESCE(
(SELECT c.oid FROM pg_class c WHERE c.relname = ?),
(SELECT c.oid FROM pg_class c WHERE c.relname = 'activity_log')
) AS oid",
[$partitionName]
)?->oid;
expect($oid)->not->toBeNull('Could not resolve partition/parent OID');
// Compute the lock key using the same formula as the trigger:
// ('x' || lpad(to_hex(TG_RELID::int), 16, '0'))::bit(64)::bigint
$lockKeyRow = DB::selectOne(
"SELECT ('x' || lpad(to_hex(?::int), 16, '0'))::bit(64)::bigint AS lock_key",
[(int) $oid]
);
$lockKey = $lockKeyRow?->lock_key;
expect($lockKey)->not->toBeNull();
// Wrap an INSERT in a transaction and check pg_locks DURING that transaction.
$lockHeld = false;
DB::transaction(function () use ($tenant, $lockKey, &$lockHeld): void {
DB::table('activity_log')->insert([
'tenant_id' => $tenant->id,
'event' => 'deal.created',
'context' => json_encode(['test' => 'advisory_lock_check']),
'created_at' => now(),
]);
// pg_advisory_xact_lock releases at END of transaction — still held here.
$held = DB::selectOne(
'SELECT EXISTS (
SELECT 1
FROM pg_locks
WHERE locktype = \'advisory\'
AND classid = (? >> 32)::int
AND objid = (? & x\'ffffffff\'::bigint)::int
AND granted = true
AND pid = pg_backend_pid()
) AS held',
[(int) $lockKey, (int) $lockKey]
);
$lockHeld = (bool) ($held->held ?? false);
});
expect($lockHeld)->toBeTrue(
'pg_advisory_xact_lock was not observed in pg_locks during the INSERT transaction. '
.'This means the migration has not been applied or the lock key formula is wrong.'
);
});