- Move 12 markdown files from root to docs/ subdirectories - Organize documentation by category: • docs/troubleshooting/ (1 file) - Technical troubleshooting guides • docs/deployment/ (4 files) - Deployment and security documentation • docs/guides/ (3 files) - Feature-specific guides • docs/planning/ (4 files) - Planning and improvement proposals Root directory cleanup: - Reduced from 16 to 4 markdown files in root - Only essential project files remain: • CLAUDE.md (AI instructions) • README.md (Main project readme) • CLEANUP_PLAN.md (Current cleanup plan) • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements) This improves: ✅ Documentation discoverability ✅ Logical organization by purpose ✅ Clean root directory ✅ Better maintainability
192 lines
7.9 KiB
PHP
192 lines
7.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Framework\Exception;
|
|
|
|
use App\Framework\Exception\ErrorCode;
|
|
use App\Framework\Exception\ExceptionContext;
|
|
use App\Framework\Exception\FrameworkException;
|
|
|
|
describe('FrameworkException', function () {
|
|
it('can be created with basic message and context', function () {
|
|
$context = ExceptionContext::empty();
|
|
$exception = new FrameworkException(
|
|
message: 'Test error',
|
|
context: $context
|
|
);
|
|
|
|
expect($exception->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();
|
|
});
|
|
});
|