container = new DefaultContainer(); // Create and bind InMemoryLogger for testing $this->logger = new InMemoryLogger(); $this->container->bind(Logger::class, fn() => $this->logger); $this->emitter = new ResponseEmitter(); $this->requestIdGenerator = new RequestIdGenerator(); // Error Aggregation setup $this->errorStorage = new InMemoryErrorStorage(); $this->cache = new class implements Cache { private array $data = []; public function get(CacheIdentifier ...$identifiers): CacheResult { $items = []; foreach ($identifiers as $identifier) { $keyStr = $identifier->toString(); if (isset($this->data[$keyStr])) { $items[] = $this->data[$keyStr]; } else { $items[] = CacheItem::miss($identifier instanceof CacheKey ? $identifier : CacheKey::fromString($keyStr)); } } return CacheResult::fromItems(...$items); } public function set(CacheItem ...$items): bool { foreach ($items as $item) { $this->data[$item->key->toString()] = $item; } return true; } public function has(CacheIdentifier ...$identifiers): array { $result = []; foreach ($identifiers as $identifier) { $result[] = isset($this->data[$identifier->toString()]); } return $result; } public function forget(CacheIdentifier ...$identifiers): bool { foreach ($identifiers as $identifier) { unset($this->data[$identifier->toString()]); } return true; } public function clear(): bool { $this->data = []; return true; } public function remember(CacheKey $key, callable $callback, ?Duration $ttl = null): CacheItem { $keyStr = $key->toString(); if (isset($this->data[$keyStr])) { return $this->data[$keyStr]; } $value = $callback(); $item = $ttl ? CacheItem::forSet($key, $value, $ttl) : CacheItem::miss($key); $this->data[$keyStr] = $item; return $item; } }; $this->clock = new SystemClock(); $this->alertQueue = new InMemoryQueue(); $this->errorAggregator = new ErrorAggregator( storage: $this->errorStorage, cache: $this->cache, clock: $this->clock, alertQueue: $this->alertQueue, logger: $this->logger, batchSize: 100, maxRetentionDays: 90 ); // Error Reporting setup $this->errorReportStorage = new InMemoryErrorReportStorage(); $this->reportQueue = new InMemoryQueue(); $this->errorReporter = new ErrorReporter( storage: $this->errorReportStorage, clock: $this->clock, logger: $this->logger, queue: $this->reportQueue, asyncProcessing: false, // Synchronous for testing processors: [], filters: [] ); // Create ErrorHandlerManager $registry = new ErrorHandlerRegistry(); $this->handlerManager = new ErrorHandlerManager($registry); // Create ErrorHandler with full pipeline $this->errorHandler = new ErrorHandler( emitter: $this->emitter, container: $this->container, requestIdGenerator: $this->requestIdGenerator, errorAggregator: $this->errorAggregator, errorReporter: $this->errorReporter, handlerManager: $this->handlerManager, logger: $this->logger, isDebugMode: true, securityHandler: null ); }); it('processes errors through complete pipeline: ErrorHandler → ErrorAggregator → ErrorReporter', function () { // Create a test exception $exception = FrameworkException::create( DatabaseErrorCode::QUERY_FAILED, 'Test database error' ); // Create HTTP response (triggers processing through all systems) $response = $this->errorHandler->createHttpResponse($exception); // Verify ErrorAggregator processed the error $events = $this->errorStorage->getRecentEvents(10); expect($events)->toHaveCount(1); expect($events[0]->errorMessage)->toBe('Test database error'); expect($events[0]->severity)->toBe(ErrorSeverity::ERROR); // Verify ErrorReporter created a report $reports = $this->errorReportStorage->findRecent(10); expect($reports)->toHaveCount(1); expect($reports[0]->exception)->toBe('App\Framework\Exception\FrameworkException'); expect($reports[0]->message)->toContain('Test database error'); expect($reports[0]->level)->toBe('error'); }); it('creates error patterns and error reports simultaneously', function () { // Create multiple identical errors $exception1 = FrameworkException::create( SystemErrorCode::RESOURCE_EXHAUSTED, 'Memory limit exceeded' ); $exception2 = FrameworkException::create( SystemErrorCode::RESOURCE_EXHAUSTED, 'Memory limit exceeded' ); $exception3 = FrameworkException::create( SystemErrorCode::RESOURCE_EXHAUSTED, 'Memory limit exceeded' ); // Process all exceptions $this->errorHandler->createHttpResponse($exception1); $this->errorHandler->createHttpResponse($exception2); $this->errorHandler->createHttpResponse($exception3); // Verify ErrorAggregator created patterns $patterns = $this->errorStorage->getActivePatterns(10); expect($patterns)->toHaveCount(1); expect($patterns[0]->occurrenceCount)->toBe(3); expect($patterns[0]->severity)->toBe(ErrorSeverity::CRITICAL); // Verify ErrorReporter created individual reports $reports = $this->errorReportStorage->findRecent(10); expect($reports)->toHaveCount(3); foreach ($reports as $report) { expect($report->message)->toContain('Memory limit exceeded'); } }); it('handles different error types through pipeline', function () { // Database error using migrated DatabaseException $dbException = DatabaseException::queryFailed( sql: 'SELECT * FROM users WHERE id = ?', error: 'Table users does not exist' ); // System error $sysException = FrameworkException::create( SystemErrorCode::RESOURCE_EXHAUSTED, 'CPU limit exceeded' ); // Process both $this->errorHandler->createHttpResponse($dbException); $this->errorHandler->createHttpResponse($sysException); // Verify ErrorAggregator $events = $this->errorStorage->getRecentEvents(10); expect($events)->toHaveCount(2); $severities = array_map(fn($e) => $e->severity, $events); expect($severities)->toContain(ErrorSeverity::ERROR); // Database expect($severities)->toContain(ErrorSeverity::CRITICAL); // System // Verify ErrorReporter $reports = $this->errorReportStorage->findRecent(10); expect($reports)->toHaveCount(2); }); it('propagates error context through entire pipeline', function () { $exception = FrameworkException::create( DatabaseErrorCode::QUERY_FAILED, 'Complex query failed' )->withData([ 'query' => 'SELECT * FROM large_table WHERE id IN (...)', 'execution_time' => 5.2 ]); // Process exception $this->errorHandler->createHttpResponse($exception); // Verify ErrorAggregator has context $events = $this->errorStorage->getRecentEvents(10); expect($events[0]->context)->toBeArray(); expect($events[0]->context)->toHaveKey('query'); expect($events[0]->context)->toHaveKey('execution_time'); // Verify ErrorReporter has full context $reports = $this->errorReportStorage->findRecent(10); expect($reports[0]->context)->toBeArray(); expect($reports[0]->context)->toHaveKey('exception'); }); it('uses interfaces for dependency injection', function () { // Verify ErrorHandler accepts interfaces expect($this->errorHandler) ->toBeInstanceOf(ErrorHandler::class); // Verify dependencies are interface-based (via reflection) $reflection = new \ReflectionClass(ErrorHandler::class); $constructor = $reflection->getConstructor(); $aggregatorParam = null; $reporterParam = null; foreach ($constructor->getParameters() as $param) { if ($param->getName() === 'errorAggregator') { $aggregatorParam = $param; } if ($param->getName() === 'errorReporter') { $reporterParam = $param; } } // Verify parameters use interfaces expect($aggregatorParam->getType()->getName()) ->toBe(ErrorAggregatorInterface::class); expect($reporterParam->getType()->getName()) ->toBe(ErrorReporterInterface::class); }); });