91 lines
3.5 KiB
PHP
91 lines
3.5 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
use Illuminate\Database\QueryException;
|
||
|
|
use Illuminate\Support\Facades\Log;
|
||
|
|
use Illuminate\Support\Facades\Route;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Tests for reduced verbosity of QueryException logging when triggered by
|
||
|
|
* a constraint violation (SQLSTATE 23xxx). After incident 2026-05-29, the
|
||
|
|
* default Laravel error report (full stack trace) caused laravel.log to
|
||
|
|
* accumulate 8.7 GB during a webhook storm. Constraint violations are
|
||
|
|
* data-validity errors — they need a warning summary, not a stack trace.
|
||
|
|
*
|
||
|
|
* Ref: docs/incidents/2026-05-29-disk-full-pg-recovery.md §5
|
||
|
|
*/
|
||
|
|
it('logs constraint violation (SQLSTATE 23505) as WARNING with sqlstate code, no stack trace', function () {
|
||
|
|
Log::spy();
|
||
|
|
|
||
|
|
Route::get('/_test/boom-23505', function () {
|
||
|
|
$pdoException = new PDOException('SQLSTATE[23505]: Unique violation: duplicate key value violates unique constraint "uniq_user_email"');
|
||
|
|
$pdoException->errorInfo = ['23505', 7, 'Unique violation'];
|
||
|
|
|
||
|
|
throw new QueryException('pgsql', 'INSERT INTO users ...', [], $pdoException);
|
||
|
|
});
|
||
|
|
|
||
|
|
/* @phpstan-ignore-next-line method.notFound */
|
||
|
|
$this->getJson('/_test/boom-23505');
|
||
|
|
|
||
|
|
// Constraint violation → warning channel, with sqlstate context
|
||
|
|
/* @phpstan-ignore-next-line staticMethod.notFound */
|
||
|
|
Log::shouldHaveReceived('warning')
|
||
|
|
->withArgs(function ($message, $context) {
|
||
|
|
return $message === 'db.constraint_violation'
|
||
|
|
&& ($context['sqlstate'] ?? '') === '23505';
|
||
|
|
})
|
||
|
|
->atLeast()->once();
|
||
|
|
|
||
|
|
// Default behaviour (full error log) is NOT called for constraint violations
|
||
|
|
/* @phpstan-ignore-next-line staticMethod.notFound */
|
||
|
|
Log::shouldNotHaveReceived('error', [
|
||
|
|
Mockery::on(fn ($msg) => $msg === 'db.query_exception'),
|
||
|
|
]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('still logs non-constraint QueryException (SQLSTATE 42P01) as ERROR with full SQL', function () {
|
||
|
|
Log::spy();
|
||
|
|
|
||
|
|
Route::get('/_test/boom-42P01', function () {
|
||
|
|
$pdoException = new PDOException('SQLSTATE[42P01]: relation "missing_table" does not exist');
|
||
|
|
$pdoException->errorInfo = ['42P01', 7, 'Undefined table'];
|
||
|
|
|
||
|
|
throw new QueryException('pgsql', 'SELECT * FROM missing_table', [], $pdoException);
|
||
|
|
});
|
||
|
|
|
||
|
|
/* @phpstan-ignore-next-line method.notFound */
|
||
|
|
$this->getJson('/_test/boom-42P01');
|
||
|
|
|
||
|
|
// Non-constraint → default error logging preserved
|
||
|
|
/* @phpstan-ignore-next-line staticMethod.notFound */
|
||
|
|
Log::shouldHaveReceived('error')
|
||
|
|
->withArgs(function ($message, $context) {
|
||
|
|
return $message === 'db.query_exception'
|
||
|
|
&& isset($context['sql']);
|
||
|
|
})
|
||
|
|
->atLeast()->once();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('logs constraint violation (SQLSTATE 23514) for check_constraint as WARNING', function () {
|
||
|
|
Log::spy();
|
||
|
|
|
||
|
|
Route::get('/_test/boom-23514', function () {
|
||
|
|
$pdoException = new PDOException('SQLSTATE[23514]: Check violation: new row for relation "supplier_projects" violates check constraint "chk_supplier_projects_b1_not_for_sms"');
|
||
|
|
$pdoException->errorInfo = ['23514', 7, 'Check violation'];
|
||
|
|
|
||
|
|
throw new QueryException('pgsql', 'INSERT INTO supplier_projects ...', [], $pdoException);
|
||
|
|
});
|
||
|
|
|
||
|
|
/* @phpstan-ignore-next-line method.notFound */
|
||
|
|
$this->getJson('/_test/boom-23514');
|
||
|
|
|
||
|
|
/* @phpstan-ignore-next-line staticMethod.notFound */
|
||
|
|
Log::shouldHaveReceived('warning')
|
||
|
|
->withArgs(function ($message, $context) {
|
||
|
|
return $message === 'db.constraint_violation'
|
||
|
|
&& ($context['sqlstate'] ?? '') === '23514';
|
||
|
|
})
|
||
|
|
->atLeast()->once();
|
||
|
|
});
|