feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready

This commit is contained in:
2025-10-31 01:39:24 +01:00
parent 55c04e4fd0
commit e26eb2aa12
601 changed files with 44184 additions and 32477 deletions

View File

@@ -0,0 +1,320 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Framework\ErrorHandling;
use App\Framework\ErrorHandling\ErrorHandlerManager;
use App\Framework\ErrorHandling\ErrorHandlerRegistry;
use App\Framework\ErrorHandling\Handlers\ErrorHandlerInterface;
use App\Framework\ErrorHandling\Handlers\ErrorHandlerPriority;
use App\Framework\ErrorHandling\Handlers\HandlerResult;
use App\Framework\Logging\Logger;
describe('ErrorHandlerManager', function () {
beforeEach(function () {
$this->registry = new ErrorHandlerRegistry();
$this->manager = new ErrorHandlerManager($this->registry);
});
it('executes handlers in priority order', function () {
$executionOrder = [];
$highPriorityHandler = new class ($executionOrder) implements ErrorHandlerInterface {
public function __construct(private array &$executionOrder) {}
public function canHandle(\Throwable $exception): bool
{
return true;
}
public function handle(\Throwable $exception): HandlerResult
{
$this->executionOrder[] = 'high';
return HandlerResult::create(
handled: true,
message: 'High priority handler'
);
}
public function getName(): string
{
return 'high_priority';
}
public function getPriority(): ErrorHandlerPriority
{
return ErrorHandlerPriority::HIGH;
}
};
$lowPriorityHandler = new class ($executionOrder) implements ErrorHandlerInterface {
public function __construct(private array &$executionOrder) {}
public function canHandle(\Throwable $exception): bool
{
return true;
}
public function handle(\Throwable $exception): HandlerResult
{
$this->executionOrder[] = 'low';
return HandlerResult::create(
handled: true,
message: 'Low priority handler'
);
}
public function getName(): string
{
return 'low_priority';
}
public function getPriority(): ErrorHandlerPriority
{
return ErrorHandlerPriority::LOW;
}
};
$this->manager = $this->manager->register($lowPriorityHandler, $highPriorityHandler);
$exception = new \Exception('Test');
$this->manager->handle($exception);
expect($executionOrder)->toBe(['high', 'low']);
});
it('stops propagation when handler marks as final', function () {
$called = [];
$finalHandler = new class ($called) implements ErrorHandlerInterface {
public function __construct(private array &$called) {}
public function canHandle(\Throwable $exception): bool
{
return true;
}
public function handle(\Throwable $exception): HandlerResult
{
$this->called[] = 'final';
return HandlerResult::create(
handled: true,
message: 'Final handler',
isFinal: true
);
}
public function getName(): string
{
return 'final_handler';
}
public function getPriority(): ErrorHandlerPriority
{
return ErrorHandlerPriority::HIGH;
}
};
$afterHandler = new class ($called) implements ErrorHandlerInterface {
public function __construct(private array &$called) {}
public function canHandle(\Throwable $exception): bool
{
return true;
}
public function handle(\Throwable $exception): HandlerResult
{
$this->called[] = 'after';
return HandlerResult::create(
handled: true,
message: 'After handler'
);
}
public function getName(): string
{
return 'after_handler';
}
public function getPriority(): ErrorHandlerPriority
{
return ErrorHandlerPriority::LOW;
}
};
$this->manager = $this->manager->register($finalHandler, $afterHandler);
$exception = new \Exception('Test');
$result = $this->manager->handle($exception);
expect($called)->toBe(['final']);
expect($result->handled)->toBeTrue();
});
it('skips handlers that cannot handle exception', function () {
$specificHandler = new class implements ErrorHandlerInterface {
public function canHandle(\Throwable $exception): bool
{
return $exception instanceof \InvalidArgumentException;
}
public function handle(\Throwable $exception): HandlerResult
{
return HandlerResult::create(
handled: true,
message: 'Specific handler'
);
}
public function getName(): string
{
return 'specific';
}
public function getPriority(): ErrorHandlerPriority
{
return ErrorHandlerPriority::HIGH;
}
};
$this->manager = $this->manager->register($specificHandler);
$exception = new \RuntimeException('Test');
$result = $this->manager->handle($exception);
expect($result->handled)->toBeFalse();
expect($result->results)->toBeEmpty();
});
it('continues chain even if handler throws exception', function () {
$called = [];
$failingHandler = new class ($called) implements ErrorHandlerInterface {
public function __construct(private array &$called) {}
public function canHandle(\Throwable $exception): bool
{
return true;
}
public function handle(\Throwable $exception): HandlerResult
{
$this->called[] = 'failing';
throw new \RuntimeException('Handler failed');
}
public function getName(): string
{
return 'failing';
}
public function getPriority(): ErrorHandlerPriority
{
return ErrorHandlerPriority::HIGH;
}
};
$workingHandler = new class ($called) implements ErrorHandlerInterface {
public function __construct(private array &$called) {}
public function canHandle(\Throwable $exception): bool
{
return true;
}
public function handle(\Throwable $exception): HandlerResult
{
$this->called[] = 'working';
return HandlerResult::create(
handled: true,
message: 'Working handler'
);
}
public function getName(): string
{
return 'working';
}
public function getPriority(): ErrorHandlerPriority
{
return ErrorHandlerPriority::LOW;
}
};
$this->manager = $this->manager->register($failingHandler, $workingHandler);
$exception = new \Exception('Test');
$result = $this->manager->handle($exception);
expect($called)->toBe(['failing', 'working']);
expect($result->handled)->toBeTrue();
});
it('aggregates results from multiple handlers', function () {
$handler1 = new class implements ErrorHandlerInterface {
public function canHandle(\Throwable $exception): bool
{
return true;
}
public function handle(\Throwable $exception): HandlerResult
{
return HandlerResult::create(
handled: true,
message: 'Handler 1',
data: ['from' => 'handler1']
);
}
public function getName(): string
{
return 'handler1';
}
public function getPriority(): ErrorHandlerPriority
{
return ErrorHandlerPriority::HIGH;
}
};
$handler2 = new class implements ErrorHandlerInterface {
public function canHandle(\Throwable $exception): bool
{
return true;
}
public function handle(\Throwable $exception): HandlerResult
{
return HandlerResult::create(
handled: true,
message: 'Handler 2',
data: ['from' => 'handler2']
);
}
public function getName(): string
{
return 'handler2';
}
public function getPriority(): ErrorHandlerPriority
{
return ErrorHandlerPriority::LOW;
}
};
$this->manager = $this->manager->register($handler1, $handler2);
$exception = new \Exception('Test');
$result = $this->manager->handle($exception);
expect($result->results)->toHaveCount(2);
expect($result->getMessages())->toBe(['Handler 1', 'Handler 2']);
$combinedData = $result->getCombinedData();
expect($combinedData)->toHaveKey('from');
});
});

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Framework\ErrorHandling\Handlers;
use App\Framework\Database\Exception\DatabaseException;
use App\Framework\ErrorHandling\Handlers\DatabaseErrorHandler;
use App\Framework\ErrorHandling\Handlers\ErrorHandlerPriority;
use App\Framework\Logging\Logger;
describe('DatabaseErrorHandler', function () {
beforeEach(function () {
$this->logger = $this->createMock(Logger::class);
$this->handler = new DatabaseErrorHandler($this->logger);
});
it('handles DatabaseException', function () {
$exception = DatabaseException::fromContext(
'Connection failed',
\App\Framework\Exception\ExceptionContext::empty()
);
expect($this->handler->canHandle($exception))->toBeTrue();
$this->logger
->expects($this->once())
->method('error')
->with('Database error occurred', $this->anything());
$result = $this->handler->handle($exception);
expect($result->handled)->toBeTrue();
expect($result->statusCode)->toBe(500);
expect($result->data['error_type'])->toBe('database');
expect($result->data['retry_after'])->toBe(60);
});
it('handles PDOException', function () {
$exception = new \PDOException('SQLSTATE[HY000] [2002] Connection refused');
expect($this->handler->canHandle($exception))->toBeTrue();
$result = $this->handler->handle($exception);
expect($result->handled)->toBeTrue();
expect($result->statusCode)->toBe(500);
});
it('does not handle non-database exceptions', function () {
$exception = new \RuntimeException('Some error');
expect($this->handler->canHandle($exception))->toBeFalse();
});
it('has HIGH priority', function () {
expect($this->handler->getPriority())->toBe(ErrorHandlerPriority::HIGH);
});
it('has correct name', function () {
expect($this->handler->getName())->toBe('database_error_handler');
});
});

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Framework\ErrorHandling\Handlers;
use App\Framework\ErrorHandling\Handlers\ErrorHandlerPriority;
use App\Framework\ErrorHandling\Handlers\FallbackErrorHandler;
use App\Framework\Logging\Logger;
describe('FallbackErrorHandler', function () {
beforeEach(function () {
$this->logger = $this->createMock(Logger::class);
$this->handler = new FallbackErrorHandler($this->logger);
});
it('handles any exception', function () {
$exception = new \RuntimeException('Any error');
expect($this->handler->canHandle($exception))->toBeTrue();
});
it('logs exception with full context', function () {
$exception = new \RuntimeException('Test error');
$this->logger
->expects($this->once())
->method('error')
->with('Unhandled exception', $this->callback(function ($context) use ($exception) {
return $context instanceof \App\Framework\Logging\ValueObjects\LogContext
&& $context->structured['exception_class'] === \RuntimeException::class
&& $context->structured['message'] === 'Test error'
&& isset($context->structured['file'])
&& isset($context->structured['line'])
&& isset($context->structured['trace']);
}));
$this->handler->handle($exception);
});
it('returns generic error message', function () {
$exception = new \RuntimeException('Detailed error');
$result = $this->handler->handle($exception);
expect($result->handled)->toBeTrue();
expect($result->message)->toBe('An unexpected error occurred');
expect($result->isFinal)->toBeTrue();
expect($result->statusCode)->toBe(500);
expect($result->data['error_type'])->toBe('unhandled');
expect($result->data['exception_class'])->toBe(\RuntimeException::class);
});
it('marks result as final', function () {
$exception = new \RuntimeException('Test');
$result = $this->handler->handle($exception);
expect($result->isFinal)->toBeTrue();
});
it('has LOWEST priority', function () {
expect($this->handler->getPriority())->toBe(ErrorHandlerPriority::LOWEST);
});
it('has correct name', function () {
expect($this->handler->getName())->toBe('fallback_error_handler');
});
});

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Framework\ErrorHandling\Handlers;
use App\Framework\ErrorHandling\Handlers\ErrorHandlerPriority;
use App\Framework\ErrorHandling\Handlers\HttpErrorHandler;
use App\Framework\Http\Exception\HttpException;
use App\Framework\Http\Status;
describe('HttpErrorHandler', function () {
beforeEach(function () {
$this->handler = new HttpErrorHandler();
});
it('handles HttpException', function () {
$exception = new HttpException(
'Not Found',
Status::NOT_FOUND,
headers: ['X-Custom' => 'value']
);
expect($this->handler->canHandle($exception))->toBeTrue();
$result = $this->handler->handle($exception);
expect($result->handled)->toBeTrue();
expect($result->message)->toBe('Not Found');
expect($result->statusCode)->toBe(404);
expect($result->data['error_type'])->toBe('http');
expect($result->data['headers'])->toBe(['X-Custom' => 'value']);
});
it('handles HttpException with no headers', function () {
$exception = new HttpException(
'Bad Request',
Status::BAD_REQUEST
);
$result = $this->handler->handle($exception);
expect($result->handled)->toBeTrue();
expect($result->statusCode)->toBe(400);
expect($result->data['headers'])->toBe([]);
});
it('does not handle non-HttpException', function () {
$exception = new \RuntimeException('Some error');
expect($this->handler->canHandle($exception))->toBeFalse();
});
it('has NORMAL priority', function () {
expect($this->handler->getPriority())->toBe(ErrorHandlerPriority::NORMAL);
});
it('has correct name', function () {
expect($this->handler->getName())->toBe('http_error_handler');
});
});

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Framework\ErrorHandling\Handlers;
use App\Framework\ErrorHandling\Handlers\ErrorHandlerPriority;
use App\Framework\ErrorHandling\Handlers\ValidationErrorHandler;
use App\Framework\Validation\Exceptions\ValidationException;
describe('ValidationErrorHandler', function () {
beforeEach(function () {
$this->handler = new ValidationErrorHandler();
});
it('handles ValidationException', function () {
$validationResult = new \App\Framework\Validation\ValidationResult();
$validationResult->addErrors('email', ['Email is required', 'Email format is invalid']);
$validationResult->addErrors('password', ['Password must be at least 8 characters']);
$exception = new ValidationException($validationResult);
expect($this->handler->canHandle($exception))->toBeTrue();
$result = $this->handler->handle($exception);
expect($result->handled)->toBeTrue();
expect($result->statusCode)->toBe(422);
expect($result->data)->toHaveKey('errors');
expect($result->data['errors'])->toBe($validationResult->getAll());
expect($result->data['error_type'])->toBe('validation');
});
it('does not handle non-ValidationException', function () {
$exception = new \RuntimeException('Some error');
expect($this->handler->canHandle($exception))->toBeFalse();
});
it('has CRITICAL priority', function () {
expect($this->handler->getPriority())->toBe(ErrorHandlerPriority::CRITICAL);
});
it('has correct name', function () {
expect($this->handler->getName())->toBe('validation_error_handler');
});
});