feat(Production): Complete production deployment infrastructure

- 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.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,326 @@
<?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);
}
});
});
});