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,225 @@
<?php
declare(strict_types=1);
use App\Framework\Exception\Core\ErrorSeverity;
describe('ErrorSeverity', function () {
describe('Retention Policies', function () {
it('provides retention days for critical errors', function () {
expect(ErrorSeverity::CRITICAL->getRetentionDays())->toBe(365); // 1 year for critical errors
});
it('provides retention days for error level', function () {
expect(ErrorSeverity::ERROR->getRetentionDays())->toBe(90);
});
it('provides retention days for warnings', function () {
expect(ErrorSeverity::WARNING->getRetentionDays())->toBe(30);
});
it('provides retention days for notice level', function () {
expect(ErrorSeverity::NOTICE->getRetentionDays())->toBe(14); // 2 weeks for notices
});
it('provides retention days for info', function () {
expect(ErrorSeverity::INFO->getRetentionDays())->toBe(7); // 1 week for info
});
it('provides retention days for debug', function () {
expect(ErrorSeverity::DEBUG->getRetentionDays())->toBe(1); // 1 day for debug
});
it('retention days decrease with severity', function () {
$critical = ErrorSeverity::CRITICAL->getRetentionDays();
$error = ErrorSeverity::ERROR->getRetentionDays();
$warning = ErrorSeverity::WARNING->getRetentionDays();
$notice = ErrorSeverity::NOTICE->getRetentionDays();
$info = ErrorSeverity::INFO->getRetentionDays();
$debug = ErrorSeverity::DEBUG->getRetentionDays();
expect($critical)->toBeGreaterThan($error);
expect($error)->toBeGreaterThan($warning);
expect($warning)->toBeGreaterThan($notice);
expect($notice)->toBeGreaterThan($info);
expect($info)->toBeGreaterThan($debug);
});
});
describe('Alert Configuration', function () {
it('critical errors should trigger alerts', function () {
expect(ErrorSeverity::CRITICAL->shouldAlert())->toBeTrue();
});
it('error level should trigger alerts', function () {
expect(ErrorSeverity::ERROR->shouldAlert())->toBeTrue();
});
it('warnings should not trigger alerts', function () {
expect(ErrorSeverity::WARNING->shouldAlert())->toBeFalse();
});
it('notice should not trigger alerts', function () {
expect(ErrorSeverity::NOTICE->shouldAlert())->toBeFalse();
});
it('info should not trigger alerts', function () {
expect(ErrorSeverity::INFO->shouldAlert())->toBeFalse();
});
it('debug should not trigger alerts', function () {
expect(ErrorSeverity::DEBUG->shouldAlert())->toBeFalse();
});
});
describe('Alert Priority', function () {
it('critical has urgent priority', function () {
expect(ErrorSeverity::CRITICAL->getAlertPriority())->toBe('URGENT');
});
it('error has high priority', function () {
expect(ErrorSeverity::ERROR->getAlertPriority())->toBe('HIGH');
});
it('warning has medium priority', function () {
expect(ErrorSeverity::WARNING->getAlertPriority())->toBe('MEDIUM');
});
it('notice has low priority', function () {
expect(ErrorSeverity::NOTICE->getAlertPriority())->toBe('LOW');
});
it('info has no alert priority', function () {
expect(ErrorSeverity::INFO->getAlertPriority())->toBeNull();
});
it('debug has no alert priority', function () {
expect(ErrorSeverity::DEBUG->getAlertPriority())->toBeNull();
});
});
describe('Response Time SLA', function () {
it('critical requires 15 minute response', function () {
expect(ErrorSeverity::CRITICAL->getRecommendedResponseTime())->toBe(15);
});
it('error requires 1 hour response', function () {
expect(ErrorSeverity::ERROR->getRecommendedResponseTime())->toBe(60);
});
it('warning requires 4 hour response', function () {
expect(ErrorSeverity::WARNING->getRecommendedResponseTime())->toBe(240);
});
it('notice has no response time requirement', function () {
expect(ErrorSeverity::NOTICE->getRecommendedResponseTime())->toBeNull();
});
it('info has no response time requirement', function () {
expect(ErrorSeverity::INFO->getRecommendedResponseTime())->toBeNull();
});
it('debug has no response time requirement', function () {
expect(ErrorSeverity::DEBUG->getRecommendedResponseTime())->toBeNull();
});
});
describe('UI Display', function () {
it('provides consistent color coding', function () {
expect(ErrorSeverity::CRITICAL->getColor())->toBe('#DC2626'); // Red-600
expect(ErrorSeverity::ERROR->getColor())->toBe('#F59E0B'); // Amber-500
expect(ErrorSeverity::WARNING->getColor())->toBe('#FBBF24'); // Yellow-400
expect(ErrorSeverity::NOTICE->getColor())->toBe('#10B981'); // Green-500
expect(ErrorSeverity::INFO->getColor())->toBe('#3B82F6'); // Blue-500
expect(ErrorSeverity::DEBUG->getColor())->toBe('#6B7280'); // Gray-500
});
it('provides icon names for display', function () {
expect(ErrorSeverity::CRITICAL->getIcon())->toBe('exclamation-triangle');
expect(ErrorSeverity::ERROR->getIcon())->toBe('exclamation-circle');
expect(ErrorSeverity::WARNING->getIcon())->toBe('exclamation');
expect(ErrorSeverity::NOTICE->getIcon())->toBe('bell');
expect(ErrorSeverity::INFO->getIcon())->toBe('information-circle');
expect(ErrorSeverity::DEBUG->getIcon())->toBe('bug');
});
});
describe('Comparison', function () {
it('compares severity levels', function () {
expect(ErrorSeverity::CRITICAL->isHigherThan(ErrorSeverity::ERROR))->toBeTrue();
expect(ErrorSeverity::ERROR->isHigherThan(ErrorSeverity::WARNING))->toBeTrue();
expect(ErrorSeverity::WARNING->isHigherThan(ErrorSeverity::NOTICE))->toBeTrue();
expect(ErrorSeverity::NOTICE->isHigherThan(ErrorSeverity::INFO))->toBeTrue();
expect(ErrorSeverity::INFO->isHigherThan(ErrorSeverity::DEBUG))->toBeTrue();
});
it('compares equal severities', function () {
expect(ErrorSeverity::ERROR->isHigherThan(ErrorSeverity::ERROR))->toBeFalse();
expect(ErrorSeverity::WARNING->isHigherThan(ErrorSeverity::WARNING))->toBeFalse();
});
it('reverse comparison works', function () {
expect(ErrorSeverity::DEBUG->isHigherThan(ErrorSeverity::CRITICAL))->toBeFalse();
expect(ErrorSeverity::INFO->isHigherThan(ErrorSeverity::ERROR))->toBeFalse();
});
});
describe('Log Level Mapping', function () {
it('maps to PSR-3 log levels', function () {
expect(ErrorSeverity::CRITICAL->toLogLevel())->toBe('critical');
expect(ErrorSeverity::ERROR->toLogLevel())->toBe('error');
expect(ErrorSeverity::WARNING->toLogLevel())->toBe('warning');
expect(ErrorSeverity::NOTICE->toLogLevel())->toBe('notice');
expect(ErrorSeverity::INFO->toLogLevel())->toBe('info');
expect(ErrorSeverity::DEBUG->toLogLevel())->toBe('debug');
});
});
describe('Integration Scenarios', function () {
it('supports log retention cleanup strategy', function () {
$severities = [
ErrorSeverity::CRITICAL,
ErrorSeverity::ERROR,
ErrorSeverity::WARNING,
ErrorSeverity::NOTICE,
ErrorSeverity::INFO,
ErrorSeverity::DEBUG
];
foreach ($severities as $severity) {
$retentionDays = $severity->getRetentionDays();
expect($retentionDays)->toBeGreaterThan(0);
}
});
it('supports alerting configuration', function () {
$alertableSeverities = [
ErrorSeverity::CRITICAL,
ErrorSeverity::ERROR
];
foreach ($alertableSeverities as $severity) {
expect($severity->shouldAlert())->toBeTrue();
expect($severity->getAlertPriority())->not->toBeNull();
expect($severity->getRecommendedResponseTime())->not->toBeNull();
}
});
it('supports UI error display', function () {
$allSeverities = [
ErrorSeverity::CRITICAL,
ErrorSeverity::ERROR,
ErrorSeverity::WARNING,
ErrorSeverity::NOTICE,
ErrorSeverity::INFO,
ErrorSeverity::DEBUG
];
foreach ($allSeverities as $severity) {
expect($severity->getColor())->toBeString();
expect($severity->getIcon())->toBeString();
expect($severity->toLogLevel())->toBeString();
}
});
});
});

View File

@@ -0,0 +1,247 @@
<?php
declare(strict_types=1);
use App\Framework\Exception\ExceptionMetadata;
describe('ExceptionMetadata', function () {
describe('Factory Methods', function () {
it('creates default metadata', function () {
$metadata = ExceptionMetadata::default();
expect($metadata->skipAggregation)->toBeFalse();
expect($metadata->skipReporting)->toBeFalse();
expect($metadata->reported)->toBeFalse();
expect($metadata->retryAfter)->toBeNull();
});
it('creates metadata with retry', function () {
$metadata = ExceptionMetadata::withRetry(60);
expect($metadata->retryAfter)->toBe(60);
expect($metadata->skipAggregation)->toBeFalse();
});
});
describe('Immutability', function () {
it('withSkipAggregation creates new instance', function () {
$original = ExceptionMetadata::default();
$modified = $original->withSkipAggregation();
expect($original)->not->toBe($modified);
expect($original->skipAggregation)->toBeFalse();
expect($modified->skipAggregation)->toBeTrue();
});
it('withSkipReporting creates new instance', function () {
$original = ExceptionMetadata::default();
$modified = $original->withSkipReporting();
expect($original->skipReporting)->toBeFalse();
expect($modified->skipReporting)->toBeTrue();
});
it('withSkipAll creates new instance', function () {
$original = ExceptionMetadata::default();
$modified = $original->withSkipAll();
expect($modified->skipAggregation)->toBeTrue();
expect($modified->skipReporting)->toBeTrue();
});
it('markAsReported creates new instance', function () {
$original = ExceptionMetadata::default();
$modified = $original->markAsReported();
expect($original->reported)->toBeFalse();
expect($modified->reported)->toBeTrue();
});
it('withRetryAfter creates new instance', function () {
$original = ExceptionMetadata::default();
$modified = $original->withRetryAfter(30);
expect($original->retryAfter)->toBeNull();
expect($modified->retryAfter)->toBe(30);
});
});
describe('Behavior Control', function () {
it('shouldAggregate returns correct value', function () {
$default = ExceptionMetadata::default();
$skipped = $default->withSkipAggregation();
expect($default->shouldAggregate())->toBeTrue();
expect($skipped->shouldAggregate())->toBeFalse();
});
it('shouldReport considers both flags', function () {
$default = ExceptionMetadata::default();
$skipReporting = $default->withSkipReporting();
$reported = $default->markAsReported();
expect($default->shouldReport())->toBeTrue();
expect($skipReporting->shouldReport())->toBeFalse();
expect($reported->shouldReport())->toBeFalse();
});
it('wasReported returns correct value', function () {
$notReported = ExceptionMetadata::default();
$reported = $notReported->markAsReported();
expect($notReported->wasReported())->toBeFalse();
expect($reported->wasReported())->toBeTrue();
});
});
describe('Chaining', function () {
it('chains multiple modifications', function () {
$metadata = ExceptionMetadata::default()
->withSkipAggregation()
->withRetryAfter(60);
expect($metadata->skipAggregation)->toBeTrue();
expect($metadata->retryAfter)->toBe(60);
expect($metadata->skipReporting)->toBeFalse();
});
it('preserves existing values when chaining', function () {
$metadata = ExceptionMetadata::withRetry(30)
->withSkipReporting();
expect($metadata->retryAfter)->toBe(30);
expect($metadata->skipReporting)->toBeTrue();
expect($metadata->skipAggregation)->toBeFalse();
});
});
describe('Serialization', function () {
it('converts to array', function () {
$metadata = ExceptionMetadata::default()
->withSkipAggregation()
->withRetryAfter(60);
$array = $metadata->toArray();
expect($array)->toBe([
'skip_aggregation' => true,
'skip_reporting' => false,
'reported' => false,
'retry_after' => 60,
]);
});
it('creates from array', function () {
$array = [
'skip_aggregation' => true,
'skip_reporting' => true,
'reported' => false,
'retry_after' => 30,
];
$metadata = ExceptionMetadata::fromArray($array);
expect($metadata->skipAggregation)->toBeTrue();
expect($metadata->skipReporting)->toBeTrue();
expect($metadata->reported)->toBeFalse();
expect($metadata->retryAfter)->toBe(30);
});
it('handles missing array keys with defaults', function () {
$metadata = ExceptionMetadata::fromArray([]);
expect($metadata->skipAggregation)->toBeFalse();
expect($metadata->skipReporting)->toBeFalse();
expect($metadata->reported)->toBeFalse();
expect($metadata->retryAfter)->toBeNull();
});
it('roundtrips through array', function () {
$original = ExceptionMetadata::default()
->withSkipAll()
->withRetryAfter(120)
->markAsReported();
$array = $original->toArray();
$restored = ExceptionMetadata::fromArray($array);
expect($restored->skipAggregation)->toBe($original->skipAggregation);
expect($restored->skipReporting)->toBe($original->skipReporting);
expect($restored->reported)->toBe($original->reported);
expect($restored->retryAfter)->toBe($original->retryAfter);
});
});
describe('Edge Cases', function () {
it('handles null retry after', function () {
$metadata = ExceptionMetadata::default()
->withRetryAfter(60)
->withRetryAfter(null);
expect($metadata->retryAfter)->toBeNull();
});
it('handles zero retry after', function () {
$metadata = ExceptionMetadata::withRetry(0);
expect($metadata->retryAfter)->toBe(0);
});
it('handles negative retry after', function () {
$metadata = ExceptionMetadata::withRetry(-1);
expect($metadata->retryAfter)->toBe(-1);
});
it('marking as reported multiple times is idempotent', function () {
$metadata = ExceptionMetadata::default()
->markAsReported()
->markAsReported();
expect($metadata->reported)->toBeTrue();
expect($metadata->shouldReport())->toBeFalse();
});
});
describe('Integration Scenarios', function () {
it('supports typical exception lifecycle', function () {
// 1. Create exception with default metadata
$metadata = ExceptionMetadata::default();
expect($metadata->shouldReport())->toBeTrue();
expect($metadata->shouldAggregate())->toBeTrue();
// 2. Skip aggregation for specific case
$metadata = $metadata->withSkipAggregation();
expect($metadata->shouldAggregate())->toBeFalse();
expect($metadata->shouldReport())->toBeTrue();
// 3. Mark as reported after logging
$metadata = $metadata->markAsReported();
expect($metadata->wasReported())->toBeTrue();
expect($metadata->shouldReport())->toBeFalse();
});
it('supports retry scenario', function () {
// 1. Create with retry hint
$metadata = ExceptionMetadata::withRetry(60);
expect($metadata->retryAfter)->toBe(60);
// 2. Update retry time
$metadata = $metadata->withRetryAfter(30);
expect($metadata->retryAfter)->toBe(30);
// 3. Clear retry
$metadata = $metadata->withRetryAfter(null);
expect($metadata->retryAfter)->toBeNull();
});
it('supports skip all for internal exceptions', function () {
$metadata = ExceptionMetadata::default()
->withSkipAll();
expect($metadata->shouldAggregate())->toBeFalse();
expect($metadata->shouldReport())->toBeFalse();
expect($metadata->wasReported())->toBeFalse();
});
});
});

View File

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