- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
327 lines
13 KiB
PHP
327 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Database\Services\SqlStateErrorMapper;
|
|
use App\Framework\Database\ValueObjects\SqlState;
|
|
use App\Framework\Exception\Core\DatabaseErrorCode;
|
|
|
|
describe('SqlStateErrorMapper Service', function () {
|
|
beforeEach(function () {
|
|
$this->mapper = new SqlStateErrorMapper();
|
|
});
|
|
|
|
describe('mapToErrorCode()', function () {
|
|
it('maps connection errors correctly', function () {
|
|
$testCases = [
|
|
'08001' => DatabaseErrorCode::CONNECTION_FAILED,
|
|
'08003' => DatabaseErrorCode::CONNECTION_FAILED,
|
|
'08004' => DatabaseErrorCode::CONNECTION_FAILED,
|
|
'08006' => DatabaseErrorCode::CONNECTION_FAILED,
|
|
];
|
|
|
|
foreach ($testCases as $sqlStateCode => $expectedErrorCode) {
|
|
$sqlState = new SqlState($sqlStateCode);
|
|
$errorCode = $this->mapper->mapToErrorCode($sqlState);
|
|
expect($errorCode)->toBe($expectedErrorCode);
|
|
}
|
|
});
|
|
|
|
it('maps constraint violations correctly', function () {
|
|
$codes = ['23000', '23502', '23503', '23505', '23514'];
|
|
|
|
foreach ($codes as $code) {
|
|
$sqlState = new SqlState($code);
|
|
$errorCode = $this->mapper->mapToErrorCode($sqlState);
|
|
expect($errorCode)->toBe(DatabaseErrorCode::CONSTRAINT_VIOLATION);
|
|
}
|
|
});
|
|
|
|
it('maps transaction errors correctly', function () {
|
|
$deadlock = new SqlState('40001');
|
|
expect($this->mapper->mapToErrorCode($deadlock))
|
|
->toBe(DatabaseErrorCode::DEADLOCK_DETECTED);
|
|
|
|
$transactionError = new SqlState('40002');
|
|
expect($this->mapper->mapToErrorCode($transactionError))
|
|
->toBe(DatabaseErrorCode::TRANSACTION_FAILED);
|
|
});
|
|
|
|
it('maps syntax errors correctly', function () {
|
|
$codes = ['42000', '42501', '42601', '42P01', '42703', '42S02', '42S22'];
|
|
|
|
foreach ($codes as $code) {
|
|
$sqlState = new SqlState($code);
|
|
$errorCode = $this->mapper->mapToErrorCode($sqlState);
|
|
expect($errorCode)->toBe(DatabaseErrorCode::QUERY_SYNTAX_ERROR);
|
|
}
|
|
});
|
|
|
|
it('maps data exceptions correctly', function () {
|
|
$codes = ['22001', '22003', '22007', '22012', '22P02'];
|
|
|
|
foreach ($codes as $code) {
|
|
$sqlState = new SqlState($code);
|
|
$errorCode = $this->mapper->mapToErrorCode($sqlState);
|
|
expect($errorCode)->toBe(DatabaseErrorCode::QUERY_EXECUTION_FAILED);
|
|
}
|
|
});
|
|
|
|
it('falls back to class-based mapping for unknown codes', function () {
|
|
// Unknown code in connection class
|
|
$unknownConnection = new SqlState('08999');
|
|
expect($this->mapper->mapToErrorCode($unknownConnection))
|
|
->toBe(DatabaseErrorCode::CONNECTION_FAILED);
|
|
|
|
// Unknown code in constraint class
|
|
$unknownConstraint = new SqlState('23999');
|
|
expect($this->mapper->mapToErrorCode($unknownConstraint))
|
|
->toBe(DatabaseErrorCode::CONSTRAINT_VIOLATION);
|
|
|
|
// Unknown code in transaction class
|
|
$unknownTransaction = new SqlState('40999');
|
|
expect($this->mapper->mapToErrorCode($unknownTransaction))
|
|
->toBe(DatabaseErrorCode::TRANSACTION_FAILED);
|
|
|
|
// Unknown code in syntax class
|
|
$unknownSyntax = new SqlState('42999');
|
|
expect($this->mapper->mapToErrorCode($unknownSyntax))
|
|
->toBe(DatabaseErrorCode::QUERY_SYNTAX_ERROR);
|
|
});
|
|
});
|
|
|
|
describe('getHumanReadableMessage()', function () {
|
|
it('provides user-friendly messages for connection errors', function () {
|
|
$sqlState = new SqlState('08001');
|
|
$message = $this->mapper->getHumanReadableMessage($sqlState);
|
|
|
|
expect($message)->toContain('Unable to establish database connection');
|
|
expect($message)->toContain('Please check server status');
|
|
});
|
|
|
|
it('provides user-friendly messages for constraint violations', function () {
|
|
$uniqueViolation = new SqlState('23505');
|
|
$message = $this->mapper->getHumanReadableMessage($uniqueViolation);
|
|
|
|
expect($message)->toContain('Duplicate entry');
|
|
expect($message)->toContain('already exists');
|
|
});
|
|
|
|
it('provides user-friendly messages for deadlocks', function () {
|
|
$deadlock = new SqlState('40001');
|
|
$message = $this->mapper->getHumanReadableMessage($deadlock);
|
|
|
|
expect($message)->toContain('Deadlock detected');
|
|
expect($message)->toContain('retry');
|
|
});
|
|
|
|
it('provides user-friendly messages for syntax errors', function () {
|
|
$tableNotFound = new SqlState('42S02');
|
|
$message = $this->mapper->getHumanReadableMessage($tableNotFound);
|
|
|
|
expect($message)->toContain('table not found');
|
|
expect($message)->toContain('schema');
|
|
});
|
|
|
|
it('provides generic message for unknown codes', function () {
|
|
$unknownCode = new SqlState('99999');
|
|
$message = $this->mapper->getHumanReadableMessage($unknownCode);
|
|
|
|
expect($message)->toContain('Database error occurred');
|
|
expect($message)->toContain('99999');
|
|
});
|
|
|
|
it('includes SQLSTATE code in fallback messages', function () {
|
|
$unknownCode = new SqlState('88888');
|
|
$message = $this->mapper->getHumanReadableMessage($unknownCode);
|
|
|
|
expect($message)->toContain('88888');
|
|
});
|
|
});
|
|
|
|
describe('getTechnicalDetails()', function () {
|
|
it('provides complete technical information', function () {
|
|
$sqlState = new SqlState('23505');
|
|
$details = $this->mapper->getTechnicalDetails($sqlState);
|
|
|
|
expect($details)->toHaveKey('sqlstate');
|
|
expect($details)->toHaveKey('class');
|
|
expect($details)->toHaveKey('subclass');
|
|
expect($details)->toHaveKey('category');
|
|
expect($details)->toHaveKey('error_code');
|
|
|
|
expect($details['sqlstate'])->toBe('23505');
|
|
expect($details['class'])->toBe('23');
|
|
expect($details['subclass'])->toBe('505');
|
|
expect($details['category'])->toBe('Integrity Constraint Violation');
|
|
expect($details['error_code'])->toBe('DB003');
|
|
});
|
|
|
|
it('categorizes different SQLSTATE classes', function () {
|
|
$testCases = [
|
|
['code' => '08001', 'category' => 'Connection Exception'],
|
|
['code' => '23505', 'category' => 'Integrity Constraint Violation'],
|
|
['code' => '40001', 'category' => 'Transaction Rollback'],
|
|
['code' => '42000', 'category' => 'Syntax Error or Access Violation'],
|
|
['code' => '22001', 'category' => 'Data Exception'],
|
|
];
|
|
|
|
foreach ($testCases as $testCase) {
|
|
$sqlState = new SqlState($testCase['code']);
|
|
$details = $this->mapper->getTechnicalDetails($sqlState);
|
|
expect($details['category'])->toBe($testCase['category']);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('isRetryable()', function () {
|
|
it('identifies retryable errors', function () {
|
|
$retryableCodes = [
|
|
'40001', // Deadlock
|
|
'08001', // Connection failed
|
|
'08003', // Connection does not exist
|
|
'08006', // Connection failure during transaction
|
|
'40002', // Transaction integrity constraint
|
|
'40003', // Statement completion unknown
|
|
];
|
|
|
|
foreach ($retryableCodes as $code) {
|
|
$sqlState = new SqlState($code);
|
|
expect($this->mapper->isRetryable($sqlState))
|
|
->toBeTrue("SQLSTATE {$code} should be retryable");
|
|
}
|
|
});
|
|
|
|
it('identifies non-retryable errors', function () {
|
|
$nonRetryableCodes = [
|
|
'23505', // Unique constraint
|
|
'42000', // Syntax error
|
|
'22001', // String data truncation
|
|
];
|
|
|
|
foreach ($nonRetryableCodes as $code) {
|
|
$sqlState = new SqlState($code);
|
|
expect($this->mapper->isRetryable($sqlState))
|
|
->toBeFalse("SQLSTATE {$code} should not be retryable");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('getRetryDelay()', function () {
|
|
it('recommends immediate retry for deadlocks', function () {
|
|
$deadlock = new SqlState('40001');
|
|
$delay = $this->mapper->getRetryDelay($deadlock);
|
|
|
|
expect($delay)->toBe(1); // 1 second for quick retry
|
|
});
|
|
|
|
it('recommends longer delay for connection errors', function () {
|
|
$connectionError = new SqlState('08001');
|
|
$delay = $this->mapper->getRetryDelay($connectionError);
|
|
|
|
expect($delay)->toBe(30); // 30 seconds for connection recovery
|
|
});
|
|
|
|
it('recommends moderate delay for transaction errors', function () {
|
|
$transactionError = new SqlState('40002');
|
|
$delay = $this->mapper->getRetryDelay($transactionError);
|
|
|
|
expect($delay)->toBe(5); // 5 seconds for transaction retry
|
|
});
|
|
|
|
it('returns null for non-retryable errors', function () {
|
|
$syntaxError = new SqlState('42000');
|
|
$delay = $this->mapper->getRetryDelay($syntaxError);
|
|
|
|
expect($delay)->toBeNull();
|
|
});
|
|
|
|
it('provides default delay for retryable errors without specific mapping', function () {
|
|
$unknownRetryable = new SqlState('08999');
|
|
// This should map to CONNECTION_FAILED via class-based fallback
|
|
|
|
if ($this->mapper->isRetryable($unknownRetryable)) {
|
|
$delay = $this->mapper->getRetryDelay($unknownRetryable);
|
|
expect($delay)->toBeGreaterThan(0);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('PostgreSQL-specific behavior', function () {
|
|
it('handles PostgreSQL unique violation', function () {
|
|
$pgUniqueViolation = new SqlState('23505');
|
|
|
|
expect($this->mapper->mapToErrorCode($pgUniqueViolation))
|
|
->toBe(DatabaseErrorCode::CONSTRAINT_VIOLATION);
|
|
|
|
$message = $this->mapper->getHumanReadableMessage($pgUniqueViolation);
|
|
expect($message)->toContain('Duplicate entry');
|
|
});
|
|
|
|
it('handles PostgreSQL table not found', function () {
|
|
$pgTableNotFound = new SqlState('42P01');
|
|
|
|
expect($this->mapper->mapToErrorCode($pgTableNotFound))
|
|
->toBe(DatabaseErrorCode::QUERY_SYNTAX_ERROR);
|
|
|
|
$message = $this->mapper->getHumanReadableMessage($pgTableNotFound);
|
|
expect($message)->toContain('table does not exist');
|
|
});
|
|
});
|
|
|
|
describe('MySQL-specific behavior', function () {
|
|
it('handles MySQL table not found', function () {
|
|
$mysqlTableNotFound = new SqlState('42S02');
|
|
|
|
expect($this->mapper->mapToErrorCode($mysqlTableNotFound))
|
|
->toBe(DatabaseErrorCode::QUERY_SYNTAX_ERROR);
|
|
|
|
$message = $this->mapper->getHumanReadableMessage($mysqlTableNotFound);
|
|
expect($message)->toContain('table not found');
|
|
});
|
|
|
|
it('handles MySQL column not found', function () {
|
|
$mysqlColumnNotFound = new SqlState('42S22');
|
|
|
|
expect($this->mapper->mapToErrorCode($mysqlColumnNotFound))
|
|
->toBe(DatabaseErrorCode::QUERY_SYNTAX_ERROR);
|
|
|
|
$message = $this->mapper->getHumanReadableMessage($mysqlColumnNotFound);
|
|
expect($message)->toContain('column not found');
|
|
});
|
|
});
|
|
|
|
describe('comprehensive error coverage', function () {
|
|
it('covers all major SQLSTATE classes', function () {
|
|
$classes = [
|
|
'08' => 'Connection',
|
|
'23' => 'Constraint',
|
|
'40' => 'Transaction',
|
|
'42' => 'Syntax',
|
|
'22' => 'Data',
|
|
];
|
|
|
|
foreach ($classes as $class => $description) {
|
|
$sqlState = new SqlState($class . '000');
|
|
$errorCode = $this->mapper->mapToErrorCode($sqlState);
|
|
|
|
expect($errorCode)->toBeInstanceOf(DatabaseErrorCode::class);
|
|
}
|
|
});
|
|
|
|
it('provides meaningful messages for all error types', function () {
|
|
$testCodes = [
|
|
'08001', '23505', '40001', '42000', '22001'
|
|
];
|
|
|
|
foreach ($testCodes as $code) {
|
|
$sqlState = new SqlState($code);
|
|
$message = $this->mapper->getHumanReadableMessage($sqlState);
|
|
|
|
expect($message)->not->toBeEmpty();
|
|
expect(strlen($message))->toBeGreaterThan(20);
|
|
}
|
|
});
|
|
});
|
|
});
|