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(); }); }); });