- 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.
359 lines
14 KiB
PHP
359 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Exception\FrameworkException;
|
|
use App\Framework\Exception\ExceptionContext;
|
|
use App\Framework\Exception\ExceptionMetadata;
|
|
use App\Framework\Exception\Core\DatabaseErrorCode;
|
|
use App\Framework\Exception\Core\AuthErrorCode;
|
|
use App\Framework\Exception\Core\ValidationErrorCode;
|
|
use App\Framework\Exception\Core\ErrorSeverity;
|
|
|
|
describe('FrameworkException', function () {
|
|
describe('Factory Methods', function () {
|
|
it('creates simple exception without error code', function () {
|
|
$exception = FrameworkException::simple('Test error');
|
|
|
|
expect($exception->getMessage())->toBe('Test error');
|
|
expect($exception->getErrorCode())->toBeNull();
|
|
expect($exception->getContext())->toBeInstanceOf(ExceptionContext::class);
|
|
});
|
|
|
|
it('creates exception with error code', function () {
|
|
$exception = FrameworkException::create(
|
|
DatabaseErrorCode::CONNECTION_FAILED,
|
|
'Database connection failed'
|
|
);
|
|
|
|
expect($exception->getMessage())->toBe('Database connection failed');
|
|
expect($exception->getErrorCode())->toBe(DatabaseErrorCode::CONNECTION_FAILED);
|
|
expect($exception->getSeverity())->toBe(ErrorSeverity::CRITICAL);
|
|
});
|
|
|
|
it('uses error code description as default message', function () {
|
|
$exception = FrameworkException::create(
|
|
DatabaseErrorCode::QUERY_FAILED
|
|
);
|
|
|
|
expect($exception->getMessage())->toBe('Database query execution failed');
|
|
});
|
|
|
|
it('creates exception for operation', function () {
|
|
$exception = FrameworkException::forOperation(
|
|
'database.query',
|
|
'UserRepository',
|
|
'Query execution failed',
|
|
DatabaseErrorCode::QUERY_FAILED
|
|
);
|
|
|
|
expect($exception->getMessage())->toBe('Query execution failed');
|
|
expect($exception->getContext()->operation)->toBe('database.query');
|
|
expect($exception->getContext()->component)->toBe('UserRepository');
|
|
});
|
|
|
|
it('creates exception from context', function () {
|
|
$context = ExceptionContext::forOperation('test.operation', 'TestComponent')
|
|
->withData(['key' => 'value']);
|
|
|
|
$exception = FrameworkException::fromContext(
|
|
'Test message',
|
|
$context,
|
|
ValidationErrorCode::INVALID_INPUT
|
|
);
|
|
|
|
expect($exception->getMessage())->toBe('Test message');
|
|
expect($exception->getContext())->toBe($context);
|
|
expect($exception->getErrorCode())->toBe(ValidationErrorCode::INVALID_INPUT);
|
|
});
|
|
});
|
|
|
|
describe('ErrorCode Integration', function () {
|
|
it('provides severity from error code', function () {
|
|
$exception = FrameworkException::create(
|
|
DatabaseErrorCode::CONNECTION_FAILED
|
|
);
|
|
|
|
expect($exception->getSeverity())->toBe(ErrorSeverity::CRITICAL);
|
|
});
|
|
|
|
it('provides recovery hint from error code', function () {
|
|
$exception = FrameworkException::create(
|
|
DatabaseErrorCode::CONNECTION_FAILED
|
|
);
|
|
|
|
expect($exception->getRecoveryHint())
|
|
->toBe('Check database server status and connection settings');
|
|
});
|
|
|
|
it('identifies recoverable exceptions', function () {
|
|
$recoverable = FrameworkException::create(
|
|
DatabaseErrorCode::TIMEOUT
|
|
);
|
|
$nonRecoverable = FrameworkException::create(
|
|
DatabaseErrorCode::MIGRATION_ROLLBACK_FAILED
|
|
);
|
|
|
|
expect($recoverable->isRecoverable())->toBeTrue();
|
|
expect($nonRecoverable->isRecoverable())->toBeFalse();
|
|
});
|
|
|
|
it('provides retry after seconds from error code', function () {
|
|
$exception = FrameworkException::create(
|
|
DatabaseErrorCode::CONNECTION_FAILED
|
|
);
|
|
|
|
expect($exception->getRetryAfter())->toBe(30);
|
|
});
|
|
|
|
it('checks error code identity', function () {
|
|
$exception = FrameworkException::create(
|
|
DatabaseErrorCode::QUERY_FAILED
|
|
);
|
|
|
|
expect($exception->isErrorCode(DatabaseErrorCode::QUERY_FAILED))->toBeTrue();
|
|
expect($exception->isErrorCode(DatabaseErrorCode::CONNECTION_FAILED))->toBeFalse();
|
|
});
|
|
|
|
it('checks error code category', function () {
|
|
$dbException = FrameworkException::create(DatabaseErrorCode::QUERY_FAILED);
|
|
$authException = FrameworkException::create(AuthErrorCode::TOKEN_EXPIRED);
|
|
|
|
expect($dbException->isCategory('DB'))->toBeTrue();
|
|
expect($dbException->isCategory('AUTH'))->toBeFalse();
|
|
expect($authException->isCategory('AUTH'))->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('ExceptionMetadata Integration', function () {
|
|
it('initializes with default metadata', function () {
|
|
$exception = FrameworkException::simple('Test');
|
|
|
|
expect($exception->getMetadata())->toBeInstanceOf(ExceptionMetadata::class);
|
|
expect($exception->shouldAggregate())->toBeTrue();
|
|
expect($exception->shouldReport())->toBeTrue();
|
|
expect($exception->wasReported())->toBeFalse();
|
|
});
|
|
|
|
it('uses retry after from error code', function () {
|
|
$exception = FrameworkException::create(
|
|
DatabaseErrorCode::CONNECTION_FAILED
|
|
);
|
|
|
|
expect($exception->getRetryAfter())->toBe(30);
|
|
});
|
|
|
|
it('allows custom retry after override', function () {
|
|
$exception = FrameworkException::create(
|
|
DatabaseErrorCode::QUERY_FAILED
|
|
)->withRetryAfter(60);
|
|
|
|
expect($exception->getRetryAfter())->toBe(60);
|
|
});
|
|
|
|
it('can skip aggregation', function () {
|
|
$exception = FrameworkException::simple('Test')
|
|
->skipAggregation();
|
|
|
|
expect($exception->shouldAggregate())->toBeFalse();
|
|
expect($exception->shouldReport())->toBeTrue();
|
|
});
|
|
|
|
it('can skip reporting', function () {
|
|
$exception = FrameworkException::simple('Test')
|
|
->skipReporting();
|
|
|
|
expect($exception->shouldAggregate())->toBeTrue();
|
|
expect($exception->shouldReport())->toBeFalse();
|
|
});
|
|
|
|
it('can skip both aggregation and reporting', function () {
|
|
$exception = FrameworkException::simple('Test')
|
|
->skipAll();
|
|
|
|
expect($exception->shouldAggregate())->toBeFalse();
|
|
expect($exception->shouldReport())->toBeFalse();
|
|
});
|
|
|
|
it('tracks reported status', function () {
|
|
$exception = FrameworkException::simple('Test');
|
|
expect($exception->wasReported())->toBeFalse();
|
|
|
|
$exception->markAsReported();
|
|
expect($exception->wasReported())->toBeTrue();
|
|
expect($exception->shouldReport())->toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('Context Enrichment', function () {
|
|
it('enriches exception with operation context', function () {
|
|
$exception = FrameworkException::simple('Test')
|
|
->withOperation('database.query', 'UserRepository');
|
|
|
|
expect($exception->getContext()->operation)->toBe('database.query');
|
|
expect($exception->getContext()->component)->toBe('UserRepository');
|
|
});
|
|
|
|
it('enriches exception with data', function () {
|
|
$exception = FrameworkException::simple('Test')
|
|
->withData(['user_id' => 123, 'action' => 'login']);
|
|
|
|
expect($exception->getContext()->data)->toBe(['user_id' => 123, 'action' => 'login']);
|
|
});
|
|
|
|
it('enriches exception with debug info', function () {
|
|
$exception = FrameworkException::simple('Test')
|
|
->withDebug(['query' => 'SELECT * FROM users', 'params' => [1]]);
|
|
|
|
expect($exception->getContext()->debug)->toHaveKey('query');
|
|
});
|
|
|
|
it('enriches exception with context metadata', function () {
|
|
$exception = FrameworkException::simple('Test')
|
|
->withContextMetadata(['request_id' => 'abc123']);
|
|
|
|
expect($exception->getContext()->metadata)->toBe(['request_id' => 'abc123']);
|
|
});
|
|
|
|
it('chains context enrichment methods', function () {
|
|
$exception = FrameworkException::simple('Test')
|
|
->withOperation('auth.login', 'AuthService')
|
|
->withData(['username' => 'john'])
|
|
->withDebug(['ip' => '127.0.0.1'])
|
|
->skipAggregation();
|
|
|
|
expect($exception->getContext()->operation)->toBe('auth.login');
|
|
expect($exception->getContext()->component)->toBe('AuthService');
|
|
expect($exception->getContext()->data['username'])->toBe('john');
|
|
expect($exception->shouldAggregate())->toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('Immutability', function () {
|
|
it('creates new instance when modifying context', function () {
|
|
$original = FrameworkException::simple('Test');
|
|
$modified = $original->withData(['key' => 'value']);
|
|
|
|
expect($original)->not->toBe($modified);
|
|
expect($original->getContext()->data)->toBe([]);
|
|
expect($modified->getContext()->data)->toBe(['key' => 'value']);
|
|
});
|
|
|
|
it('creates new instance when modifying metadata', function () {
|
|
$original = FrameworkException::simple('Test');
|
|
$modified = $original->skipAggregation();
|
|
|
|
expect($original)->not->toBe($modified);
|
|
expect($original->shouldAggregate())->toBeTrue();
|
|
expect($modified->shouldAggregate())->toBeFalse();
|
|
});
|
|
|
|
it('creates new instance when setting error code', function () {
|
|
$original = FrameworkException::simple('Test');
|
|
$modified = $original->withErrorCode(DatabaseErrorCode::QUERY_FAILED);
|
|
|
|
expect($original)->not->toBe($modified);
|
|
expect($original->getErrorCode())->toBeNull();
|
|
expect($modified->getErrorCode())->toBe(DatabaseErrorCode::QUERY_FAILED);
|
|
});
|
|
});
|
|
|
|
describe('Array Serialization', function () {
|
|
it('serializes to array with error code info', function () {
|
|
$exception = FrameworkException::create(
|
|
DatabaseErrorCode::QUERY_FAILED,
|
|
'Test query failed'
|
|
)->withData(['table' => 'users']);
|
|
|
|
$array = $exception->toArray();
|
|
|
|
expect($array)->toHaveKey('class');
|
|
expect($array)->toHaveKey('message');
|
|
expect($array)->toHaveKey('error_code');
|
|
expect($array)->toHaveKey('error_category');
|
|
expect($array)->toHaveKey('description');
|
|
expect($array)->toHaveKey('recovery_hint');
|
|
expect($array)->toHaveKey('is_recoverable');
|
|
expect($array['error_code'])->toBe('DB002');
|
|
expect($array['error_category'])->toBe('DB');
|
|
});
|
|
|
|
it('serializes without error code', function () {
|
|
$exception = FrameworkException::simple('Test error');
|
|
$array = $exception->toArray();
|
|
|
|
expect($array)->toHaveKey('message');
|
|
expect($array)->not->toHaveKey('error_code');
|
|
});
|
|
});
|
|
|
|
describe('String Representation', function () {
|
|
it('includes error code in string representation', function () {
|
|
$exception = FrameworkException::create(
|
|
DatabaseErrorCode::QUERY_FAILED,
|
|
'Test query failed'
|
|
);
|
|
|
|
$string = (string) $exception;
|
|
|
|
expect($string)->toContain('[DB002]');
|
|
expect($string)->toContain('Test query failed');
|
|
});
|
|
|
|
it('works without error code', function () {
|
|
$exception = FrameworkException::simple('Test error');
|
|
$string = (string) $exception;
|
|
|
|
expect($string)->toContain('Test error');
|
|
expect($string)->not->toContain('[');
|
|
});
|
|
});
|
|
|
|
describe('Previous Exception Chain', function () {
|
|
it('wraps previous exception', function () {
|
|
$previous = new \RuntimeException('Original error');
|
|
$exception = FrameworkException::simple('Wrapped error', $previous);
|
|
|
|
expect($exception->getPrevious())->toBe($previous);
|
|
});
|
|
|
|
it('chains with error code', function () {
|
|
$previous = new \PDOException('Connection refused');
|
|
$exception = FrameworkException::create(
|
|
DatabaseErrorCode::CONNECTION_FAILED,
|
|
'Database unavailable',
|
|
previous: $previous
|
|
);
|
|
|
|
expect($exception->getPrevious())->toBe($previous);
|
|
expect($exception->getErrorCode())->toBe(DatabaseErrorCode::CONNECTION_FAILED);
|
|
});
|
|
});
|
|
|
|
describe('Multiple Error Code Categories', function () {
|
|
it('handles database errors', function () {
|
|
$exception = FrameworkException::create(DatabaseErrorCode::TIMEOUT);
|
|
|
|
expect($exception->isCategory('DB'))->toBeTrue();
|
|
expect($exception->getSeverity())->toBe(ErrorSeverity::ERROR);
|
|
expect($exception->getRetryAfter())->toBe(60);
|
|
});
|
|
|
|
it('handles authentication errors', function () {
|
|
$exception = FrameworkException::create(AuthErrorCode::TOKEN_EXPIRED);
|
|
|
|
expect($exception->isCategory('AUTH'))->toBeTrue();
|
|
expect($exception->getSeverity())->toBe(ErrorSeverity::WARNING);
|
|
expect($exception->getRetryAfter())->toBe(0);
|
|
});
|
|
|
|
it('handles validation errors', function () {
|
|
$exception = FrameworkException::create(ValidationErrorCode::BUSINESS_RULE_VIOLATION);
|
|
|
|
expect($exception->isCategory('VAL'))->toBeTrue();
|
|
expect($exception->getSeverity())->toBe(ErrorSeverity::ERROR);
|
|
expect($exception->isRecoverable())->toBeTrue();
|
|
});
|
|
});
|
|
});
|