getMessage())->toBe('Test error'); expect($exception->getContext())->toBe($context); expect($exception->getErrorCode())->toBeNull(); expect($exception->getRetryAfter())->toBeNull(); expect($exception->isRecoverable())->toBeFalse(); }); it('can be created with ErrorCode', function () { $context = ExceptionContext::empty(); $exception = new FrameworkException( message: 'Database error', context: $context, errorCode: ErrorCode::DB_CONNECTION_FAILED ); expect($exception->getMessage())->toBe('Database error'); expect($exception->getErrorCode())->toBe(ErrorCode::DB_CONNECTION_FAILED); expect($exception->isErrorCode(ErrorCode::DB_CONNECTION_FAILED))->toBeTrue(); expect($exception->isErrorCode(ErrorCode::AUTH_TOKEN_EXPIRED))->toBeFalse(); expect($exception->isCategory('DB'))->toBeTrue(); expect($exception->isCategory('AUTH'))->toBeFalse(); }); it('supports simple factory method', function () { $exception = FrameworkException::simple('Simple error message'); expect($exception->getMessage())->toBe('Simple error message'); expect($exception->getErrorCode())->toBeNull(); expect($exception->getContext()->toArray())->toHaveKey('operation'); }); it('supports create factory method with ErrorCode', function () { $exception = FrameworkException::create( ErrorCode::VAL_BUSINESS_RULE_VIOLATION, 'Custom validation error' ); expect($exception->getMessage())->toBe('Custom validation error'); expect($exception->getErrorCode())->toBe(ErrorCode::VAL_BUSINESS_RULE_VIOLATION); expect($exception->isCategory('VAL'))->toBeTrue(); }); it('supports forOperation factory method', function () { $exception = FrameworkException::forOperation( operation: 'user.create', component: 'UserService', message: 'Failed to create user', errorCode: ErrorCode::VAL_BUSINESS_RULE_VIOLATION ); expect($exception->getMessage())->toBe('Failed to create user'); expect($exception->getErrorCode())->toBe(ErrorCode::VAL_BUSINESS_RULE_VIOLATION); expect($exception->getContext()->operation)->toBe('user.create'); expect($exception->getContext()->component)->toBe('UserService'); }); it('supports fromContext factory method', function () { $context = ExceptionContext::forOperation('payment.process', 'PaymentService') ->withData(['amount' => 100]); $exception = FrameworkException::fromContext( message: 'Payment failed', context: $context, errorCode: ErrorCode::PAYMENT_GATEWAY_ERROR ); expect($exception->getMessage())->toBe('Payment failed'); expect($exception->getContext())->toBe($context); expect($exception->getData())->toBe(['amount' => 100]); }); it('supports immutable transformations', function () { $original = FrameworkException::simple('Original message'); $withData = $original->withData(['key' => 'value']); $withOperation = $withData->withOperation('test.operation', 'TestComponent'); $withDebug = $withOperation->withDebug(['debug' => 'info']); $withMetadata = $withDebug->withMetadata(['meta' => 'data']); // Original should be unchanged expect($original->getData())->toBe([]); expect($original->getContext()->operation)->toBeNull(); // New instance should have all data expect($withMetadata->getData())->toBe(['key' => 'value']); expect($withMetadata->getContext()->operation)->toBe('test.operation'); expect($withMetadata->getContext()->component)->toBe('TestComponent'); expect($withMetadata->getContext()->debug)->toBe(['debug' => 'info']); expect($withMetadata->getContext()->metadata)->toBe(['meta' => 'data']); }); it('supports ErrorCode modifications', function () { $exception = FrameworkException::simple('Test error'); expect($exception->getErrorCode())->toBeNull(); $withErrorCode = $exception->withErrorCode(ErrorCode::DB_QUERY_FAILED); expect($withErrorCode->getErrorCode())->toBe(ErrorCode::DB_QUERY_FAILED); expect($withErrorCode->isCategory('DB'))->toBeTrue(); // Original should be unchanged expect($exception->getErrorCode())->toBeNull(); }); it('supports retry after modifications', function () { $exception = FrameworkException::create(ErrorCode::HTTP_RATE_LIMIT_EXCEEDED); $withRetry = $exception->withRetryAfter(300); expect($withRetry->getRetryAfter())->toBe(300); expect($withRetry->isRecoverable())->toBeTrue(); }); it('converts to array with all relevant data', function () { $context = ExceptionContext::forOperation('test.operation', 'TestComponent') ->withData(['test' => 'data']) ->withDebug(['debug' => 'info']); $exception = FrameworkException::fromContext( message: 'Test exception', context: $context, errorCode: ErrorCode::VAL_BUSINESS_RULE_VIOLATION ); $array = $exception->toArray(); expect($array)->toHaveKey('class'); expect($array)->toHaveKey('message'); expect($array)->toHaveKey('context'); 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['message'])->toBe('Test exception'); expect($array['error_code'])->toBe(ErrorCode::VAL_BUSINESS_RULE_VIOLATION->value); expect($array['error_category'])->toBe('VAL'); }); it('handles string representation correctly', function () { $exception = FrameworkException::create( ErrorCode::DB_CONNECTION_FAILED, 'Connection failed' ); $string = (string) $exception; expect($string)->toContain('FrameworkException'); expect($string)->toContain('[DB001]'); expect($string)->toContain('Connection failed'); expect($string)->toMatch('/\.php:\d+$/'); }); it('supports chaining operations fluently', function () { $exception = FrameworkException::simple('Base error') ->withErrorCode(ErrorCode::VAL_BUSINESS_RULE_VIOLATION) ->withOperation('user.validate', 'UserValidator') ->withData(['field' => 'email', 'value' => 'invalid']) ->withDebug(['rule' => 'email_format']) ->withMetadata(['attempt' => 1]) ->withRetryAfter(60); expect($exception->getMessage())->toBe('Base error'); expect($exception->getErrorCode())->toBe(ErrorCode::VAL_BUSINESS_RULE_VIOLATION); expect($exception->getContext()->operation)->toBe('user.validate'); expect($exception->getContext()->component)->toBe('UserValidator'); expect($exception->getData())->toBe(['field' => 'email', 'value' => 'invalid']); expect($exception->getContext()->debug)->toBe(['rule' => 'email_format']); expect($exception->getContext()->metadata)->toBe(['attempt' => 1]); expect($exception->getRetryAfter())->toBe(60); expect($exception->isRecoverable())->toBeTrue(); expect($exception->isCategory('VAL'))->toBeTrue(); }); });