fix: Gitea Traefik routing and connection pool optimization
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
This commit is contained in:
@@ -1,340 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\ErrorHandling;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Cache\CacheIdentifier;
|
||||
use App\Framework\Cache\CacheItem;
|
||||
use App\Framework\Cache\CacheKey;
|
||||
use App\Framework\Cache\CacheResult;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\DateTime\Clock;
|
||||
use App\Framework\ErrorAggregation\ErrorAggregator;
|
||||
use App\Framework\ErrorAggregation\ErrorEvent;
|
||||
use App\Framework\ErrorAggregation\Storage\InMemoryErrorStorage;
|
||||
use App\Framework\Exception\Core\CacheErrorCode;
|
||||
use App\Framework\Exception\Core\DatabaseErrorCode;
|
||||
use App\Framework\Exception\Core\ErrorSeverity;
|
||||
use App\Framework\Exception\Core\SystemErrorCode;
|
||||
use App\Framework\Exception\ErrorHandlerContext;
|
||||
use App\Framework\Exception\ExceptionContext;
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
use App\Framework\Exception\RequestContext;
|
||||
use App\Framework\Exception\SystemContext;
|
||||
use App\Framework\Queue\InMemoryQueue;
|
||||
|
||||
// Helper function für Mock-Objekte
|
||||
function createTestCache(): Cache
|
||||
{
|
||||
return new class implements Cache {
|
||||
private array $data = [];
|
||||
|
||||
public function get(CacheIdentifier ...$identifiers): CacheResult
|
||||
{
|
||||
$items = [];
|
||||
|
||||
foreach ($identifiers as $identifier) {
|
||||
if (isset($this->data[$identifier->toString()])) {
|
||||
$items[] = $this->data[$identifier->toString()];
|
||||
} else {
|
||||
$items[] = CacheItem::miss($identifier instanceof CacheKey ? $identifier : CacheKey::fromString($identifier->toString()));
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
$key = (string) $identifier;
|
||||
$result[$key] = isset($this->data[$key]);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function forget(CacheIdentifier ...$identifiers): bool
|
||||
{
|
||||
foreach ($identifiers as $identifier) {
|
||||
$key = (string) $identifier;
|
||||
unset($this->data[$key]);
|
||||
}
|
||||
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 = CacheItem::forSetting($key, $value, $ttl);
|
||||
$this->data[$keyStr] = $item;
|
||||
|
||||
return $item;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createTestClock(): Clock
|
||||
{
|
||||
return new class implements Clock {
|
||||
public function now(): \DateTimeImmutable
|
||||
{
|
||||
return new \DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function fromTimestamp(Timestamp $timestamp): \DateTimeImmutable
|
||||
{
|
||||
return $timestamp->toDateTime();
|
||||
}
|
||||
|
||||
public function fromString(string $dateTime, ?string $format = null): \DateTimeImmutable
|
||||
{
|
||||
return new \DateTimeImmutable($dateTime);
|
||||
}
|
||||
|
||||
public function today(): \DateTimeImmutable
|
||||
{
|
||||
return new \DateTimeImmutable('today');
|
||||
}
|
||||
|
||||
public function yesterday(): \DateTimeImmutable
|
||||
{
|
||||
return new \DateTimeImmutable('yesterday');
|
||||
}
|
||||
|
||||
public function tomorrow(): \DateTimeImmutable
|
||||
{
|
||||
return new \DateTimeImmutable('tomorrow');
|
||||
}
|
||||
|
||||
public function time(): Timestamp
|
||||
{
|
||||
return Timestamp::now();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('ErrorHandler ErrorAggregator Integration', function () {
|
||||
it('processes ErrorHandlerContext through ErrorAggregator', function () {
|
||||
// Setup ErrorAggregator
|
||||
$storage = new InMemoryErrorStorage();
|
||||
$cache = createTestCache();
|
||||
$clock = createTestClock();
|
||||
$alertQueue = new InMemoryQueue();
|
||||
|
||||
$errorAggregator = new ErrorAggregator(
|
||||
storage: $storage,
|
||||
cache: $cache,
|
||||
clock: $clock,
|
||||
alertQueue: $alertQueue
|
||||
);
|
||||
|
||||
// Create ErrorHandlerContext (simulates what ErrorHandler does)
|
||||
$exception = FrameworkException::create(
|
||||
DatabaseErrorCode::QUERY_FAILED,
|
||||
'Test database query failed'
|
||||
);
|
||||
|
||||
$exceptionContext = ExceptionContext::empty()
|
||||
->withOperation('test_operation', 'TestComponent')
|
||||
->withData([
|
||||
'test_key' => 'test_value',
|
||||
'original_exception' => $exception, // Store exception for message extraction
|
||||
'exception_message' => $exception->getMessage()
|
||||
]);
|
||||
|
||||
$requestContext = RequestContext::fromGlobals();
|
||||
$systemContext = SystemContext::current();
|
||||
|
||||
$errorHandlerContext = ErrorHandlerContext::create(
|
||||
$exceptionContext,
|
||||
$requestContext,
|
||||
$systemContext,
|
||||
['http_status' => 500]
|
||||
);
|
||||
|
||||
// Process through ErrorAggregator (simulates ErrorHandler calling it)
|
||||
$errorAggregator->processError($errorHandlerContext);
|
||||
|
||||
// Verify error was stored
|
||||
$recentEvents = $errorAggregator->getRecentEvents(10);
|
||||
expect($recentEvents)->toHaveCount(1);
|
||||
|
||||
$event = $recentEvents[0];
|
||||
expect($event->errorMessage)->toBe('Test database query failed');
|
||||
expect($event->severity)->toBe(ErrorSeverity::ERROR);
|
||||
expect($event->component)->toBe('TestComponent');
|
||||
expect($event->operation)->toBe('test_operation');
|
||||
|
||||
// Verify pattern was created
|
||||
$activePatterns = $errorAggregator->getActivePatterns(10);
|
||||
expect($activePatterns)->toHaveCount(1);
|
||||
|
||||
$pattern = $activePatterns[0];
|
||||
expect($pattern->occurrenceCount)->toBe(1);
|
||||
expect($pattern->severity)->toBe(ErrorSeverity::ERROR);
|
||||
expect($pattern->component)->toBe('TestComponent');
|
||||
});
|
||||
|
||||
it('creates error patterns from multiple identical errors', function () {
|
||||
$storage = new InMemoryErrorStorage();
|
||||
$cache = createTestCache();
|
||||
$clock = createTestClock();
|
||||
$alertQueue = new InMemoryQueue();
|
||||
|
||||
$errorAggregator = new ErrorAggregator(
|
||||
storage: $storage,
|
||||
cache: $cache,
|
||||
clock: $clock,
|
||||
alertQueue: $alertQueue
|
||||
);
|
||||
|
||||
// Process same error 3 times
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$exception = FrameworkException::create(
|
||||
DatabaseErrorCode::QUERY_FAILED,
|
||||
'Repeated database error'
|
||||
);
|
||||
|
||||
$exceptionContext = ExceptionContext::empty()
|
||||
->withOperation('query_execution', 'DatabaseManager')
|
||||
->withData([
|
||||
'query' => 'SELECT * FROM users',
|
||||
'original_exception' => $exception,
|
||||
'exception_message' => $exception->getMessage()
|
||||
]);
|
||||
|
||||
$requestContext = RequestContext::fromGlobals();
|
||||
$systemContext = SystemContext::current();
|
||||
|
||||
$errorHandlerContext = ErrorHandlerContext::create(
|
||||
$exceptionContext,
|
||||
$requestContext,
|
||||
$systemContext,
|
||||
['http_status' => 500]
|
||||
);
|
||||
|
||||
$errorAggregator->processError($errorHandlerContext);
|
||||
}
|
||||
|
||||
// Verify all 3 events were stored
|
||||
$recentEvents = $errorAggregator->getRecentEvents(10);
|
||||
expect($recentEvents)->toHaveCount(3);
|
||||
|
||||
// Verify single pattern with 3 occurrences (same fingerprint)
|
||||
$activePatterns = $errorAggregator->getActivePatterns(10);
|
||||
expect($activePatterns)->toHaveCount(1);
|
||||
|
||||
$pattern = $activePatterns[0];
|
||||
expect($pattern->occurrenceCount)->toBe(3);
|
||||
expect($pattern->component)->toBe('DatabaseManager');
|
||||
expect($pattern->operation)->toBe('query_execution');
|
||||
});
|
||||
|
||||
it('handles ErrorAggregator being null (optional dependency)', function () {
|
||||
$errorAggregator = null;
|
||||
|
||||
// Simulate the nullable call pattern used in ErrorHandler
|
||||
$exception = FrameworkException::create(
|
||||
DatabaseErrorCode::QUERY_FAILED,
|
||||
'Test error'
|
||||
);
|
||||
|
||||
$exceptionContext = ExceptionContext::empty();
|
||||
$requestContext = RequestContext::fromGlobals();
|
||||
$systemContext = SystemContext::current();
|
||||
|
||||
$errorHandlerContext = ErrorHandlerContext::create(
|
||||
$exceptionContext,
|
||||
$requestContext,
|
||||
$systemContext,
|
||||
[]
|
||||
);
|
||||
|
||||
// This should not throw an error
|
||||
$errorAggregator?->processError($errorHandlerContext);
|
||||
|
||||
// If we get here, the null-safe operator worked correctly
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
it('processes errors with different severities correctly', function () {
|
||||
$storage = new InMemoryErrorStorage();
|
||||
$cache = createTestCache();
|
||||
$clock = createTestClock();
|
||||
$alertQueue = new InMemoryQueue();
|
||||
|
||||
$errorAggregator = new ErrorAggregator(
|
||||
storage: $storage,
|
||||
cache: $cache,
|
||||
clock: $clock,
|
||||
alertQueue: $alertQueue
|
||||
);
|
||||
|
||||
// Process errors with different severities
|
||||
$errorCodes = [
|
||||
SystemErrorCode::RESOURCE_EXHAUSTED, // CRITICAL
|
||||
DatabaseErrorCode::QUERY_FAILED, // ERROR
|
||||
CacheErrorCode::READ_FAILED, // WARNING
|
||||
];
|
||||
|
||||
foreach ($errorCodes as $errorCode) {
|
||||
$exception = FrameworkException::create($errorCode, 'Test message');
|
||||
|
||||
$exceptionContext = ExceptionContext::empty()
|
||||
->withOperation('test_op', 'TestComponent')
|
||||
->withData([
|
||||
'original_exception' => $exception,
|
||||
'exception_message' => $exception->getMessage()
|
||||
]);
|
||||
|
||||
$requestContext = RequestContext::fromGlobals();
|
||||
$systemContext = SystemContext::current();
|
||||
|
||||
$errorHandlerContext = ErrorHandlerContext::create(
|
||||
$exceptionContext,
|
||||
$requestContext,
|
||||
$systemContext,
|
||||
[]
|
||||
);
|
||||
|
||||
$errorAggregator->processError($errorHandlerContext);
|
||||
}
|
||||
|
||||
// Verify all events were stored
|
||||
$recentEvents = $errorAggregator->getRecentEvents(10);
|
||||
expect($recentEvents)->toHaveCount(3);
|
||||
|
||||
// Verify patterns reflect correct severities
|
||||
$activePatterns = $errorAggregator->getActivePatterns(10);
|
||||
expect($activePatterns)->toHaveCount(3);
|
||||
|
||||
$severities = array_map(fn($p) => $p->severity, $activePatterns);
|
||||
expect($severities)->toContain(ErrorSeverity::CRITICAL);
|
||||
expect($severities)->toContain(ErrorSeverity::ERROR);
|
||||
expect($severities)->toContain(ErrorSeverity::WARNING);
|
||||
});
|
||||
});
|
||||
@@ -1,298 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Cache\CacheIdentifier;
|
||||
use App\Framework\Cache\CacheKey;
|
||||
use App\Framework\Cache\CacheItem;
|
||||
use App\Framework\Cache\CacheResult;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\DateTime\SystemClock;
|
||||
use App\Framework\ErrorAggregation\ErrorAggregator;
|
||||
use App\Framework\ErrorAggregation\ErrorAggregatorInterface;
|
||||
use App\Framework\ErrorAggregation\Storage\InMemoryErrorStorage;
|
||||
use App\Framework\ErrorHandling\ErrorHandler;
|
||||
use App\Framework\ErrorHandling\ErrorHandlerManager;
|
||||
use App\Framework\ErrorHandling\ErrorHandlerRegistry;
|
||||
use App\Framework\ErrorReporting\ErrorReporter;
|
||||
use App\Framework\ErrorReporting\ErrorReporterInterface;
|
||||
use App\Framework\ErrorReporting\Storage\InMemoryErrorReportStorage;
|
||||
use App\Framework\Exception\Core\DatabaseErrorCode;
|
||||
use App\Framework\Exception\Core\ErrorSeverity;
|
||||
use App\Framework\Exception\Core\SystemErrorCode;
|
||||
use App\Framework\Exception\DatabaseException;
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
use App\Framework\Http\RequestIdGenerator;
|
||||
use App\Framework\Http\ResponseEmitter;
|
||||
use App\Framework\DI\DefaultContainer;
|
||||
use App\Framework\Queue\InMemoryQueue;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
use App\Framework\Logging\ValueObjects\LogContext;
|
||||
use App\Framework\Logging\InMemoryLogger;
|
||||
|
||||
describe('ErrorHandler Full Pipeline Integration', function () {
|
||||
beforeEach(function () {
|
||||
// Create all dependencies
|
||||
$this->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);
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,8 @@ use App\Framework\Logging\HasChannel;
|
||||
use App\Framework\Logging\LogChannel;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Logging\SupportsChannels;
|
||||
use App\Framework\Reflection\CachedReflectionProvider;
|
||||
use App\Framework\Reflection\ReflectionService;
|
||||
use App\Framework\Reflection\SimpleReflectionService;
|
||||
use App\Framework\Config\Env as EnvAttribute;
|
||||
use App\Framework\Config\EnvKey;
|
||||
use App\Framework\Config\Environment;
|
||||
@@ -116,8 +117,8 @@ final class ServiceWithEnvAttribute
|
||||
|
||||
beforeEach(function () {
|
||||
$this->container = $this->createMock(Container::class);
|
||||
$this->reflectionProvider = new CachedReflectionProvider();
|
||||
$this->resolver = new ParameterResolver($this->container, $this->reflectionProvider);
|
||||
$this->reflectionService = new SimpleReflectionService();
|
||||
$this->resolver = new ParameterResolver($this->container, $this->reflectionService);
|
||||
});
|
||||
|
||||
describe('ParameterResolver', function () {
|
||||
|
||||
@@ -160,12 +160,12 @@ describe('SslCertificateHealthCheck', function () {
|
||||
});
|
||||
|
||||
it('has correct name and category', function () {
|
||||
expect($this->healthCheck->getName())->toBe('SSL Certificate');
|
||||
expect($this->healthCheck->name)->toBe('SSL Certificate');
|
||||
expect($this->healthCheck->getCategory())->toBe(HealthCheckCategory::SECURITY);
|
||||
});
|
||||
|
||||
it('has reasonable timeout', function () {
|
||||
$timeout = $this->healthCheck->getTimeout();
|
||||
$timeout = $this->healthCheck->timeout;
|
||||
|
||||
expect($timeout)->toBeInt();
|
||||
expect($timeout)->toBeGreaterThan(0);
|
||||
|
||||
@@ -1,320 +0,0 @@
|
||||
<?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');
|
||||
});
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
<?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');
|
||||
});
|
||||
});
|
||||
@@ -1,69 +0,0 @@
|
||||
<?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');
|
||||
});
|
||||
});
|
||||
@@ -1,61 +0,0 @@
|
||||
<?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');
|
||||
});
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
<?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');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,301 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Audit\AuditLogger;
|
||||
use App\Framework\Audit\InMemoryAuditLogger;
|
||||
use App\Framework\Audit\ValueObjects\AuditEntry;
|
||||
use App\Framework\Audit\ValueObjects\AuditableAction;
|
||||
use App\Framework\DateTime\Clock;
|
||||
use App\Framework\ExceptionHandling\Audit\ExceptionAuditLogger;
|
||||
use App\Framework\ExceptionHandling\Context\ExceptionContextData;
|
||||
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
|
||||
|
||||
describe('ExceptionAuditLogger', function () {
|
||||
beforeEach(function () {
|
||||
$this->auditLogger = new InMemoryAuditLogger();
|
||||
$this->clock = new class implements Clock {
|
||||
private DateTimeImmutable $now;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->now = new DateTimeImmutable('2024-01-01 12:00:00');
|
||||
}
|
||||
|
||||
public function now(): DateTimeImmutable
|
||||
{
|
||||
return $this->now;
|
||||
}
|
||||
|
||||
public function fromTimestamp(\App\Framework\Core\ValueObjects\Timestamp $timestamp): DateTimeImmutable
|
||||
{
|
||||
return DateTimeImmutable::createFromTimestamp($timestamp->value);
|
||||
}
|
||||
|
||||
public function fromString(string $dateTime, ?string $format = null): DateTimeImmutable
|
||||
{
|
||||
return DateTimeImmutable::createFromFormat($format ?? 'Y-m-d H:i:s', $dateTime);
|
||||
}
|
||||
|
||||
public function today(): DateTimeImmutable
|
||||
{
|
||||
return $this->now;
|
||||
}
|
||||
|
||||
public function yesterday(): DateTimeImmutable
|
||||
{
|
||||
return $this->now->modify('-1 day');
|
||||
}
|
||||
|
||||
public function tomorrow(): DateTimeImmutable
|
||||
{
|
||||
return $this->now->modify('+1 day');
|
||||
}
|
||||
|
||||
public function time(): \App\Framework\Core\ValueObjects\Timestamp
|
||||
{
|
||||
return new \App\Framework\Core\ValueObjects\Timestamp($this->now->getTimestamp());
|
||||
}
|
||||
};
|
||||
$this->contextProvider = new ExceptionContextProvider();
|
||||
$this->exceptionAuditLogger = new ExceptionAuditLogger(
|
||||
$this->auditLogger,
|
||||
$this->clock,
|
||||
$this->contextProvider
|
||||
);
|
||||
});
|
||||
|
||||
it('logs auditable exception as audit entry', function () {
|
||||
$exception = new RuntimeException('User not found');
|
||||
$context = ExceptionContextData::forOperation('user.lookup', 'UserRepository')
|
||||
->addData(['user_id' => '123'])
|
||||
->withUserId('user-456')
|
||||
->withAuditable(true);
|
||||
|
||||
$this->contextProvider->attach($exception, $context);
|
||||
|
||||
$this->exceptionAuditLogger->logIfAuditable($exception);
|
||||
|
||||
// Check that audit entry was created
|
||||
$entries = $this->getAllAuditEntries($this->auditLogger);
|
||||
expect($entries)->toHaveCount(1);
|
||||
|
||||
$entry = $entries[0];
|
||||
expect($entry->success)->toBeFalse();
|
||||
expect($entry->errorMessage)->toBe('User not found');
|
||||
expect($entry->userId)->toBe('user-456');
|
||||
expect($entry->entityType)->toBe('userrepository');
|
||||
expect($entry->action)->toBeInstanceOf(AuditableAction::class);
|
||||
});
|
||||
|
||||
it('does not log non-auditable exception', function () {
|
||||
$exception = new RuntimeException('Validation error');
|
||||
$context = ExceptionContextData::forOperation('validation.check')
|
||||
->withAuditable(false);
|
||||
|
||||
$this->contextProvider->attach($exception, $context);
|
||||
|
||||
$this->exceptionAuditLogger->logIfAuditable($exception);
|
||||
|
||||
// Check that no audit entry was created
|
||||
$entries = $this->getAllAuditEntries($this->auditLogger);
|
||||
expect($entries)->toHaveCount(0);
|
||||
});
|
||||
|
||||
it('logs exception without context as auditable by default', function () {
|
||||
$exception = new RuntimeException('Generic error');
|
||||
|
||||
$this->exceptionAuditLogger->logIfAuditable($exception);
|
||||
|
||||
// Check that audit entry was created (default: auditable)
|
||||
$entries = $this->getAllAuditEntries($this->auditLogger);
|
||||
expect($entries)->toHaveCount(1);
|
||||
});
|
||||
|
||||
it('includes context data in audit entry metadata', function () {
|
||||
$exception = new RuntimeException('Operation failed');
|
||||
$context = ExceptionContextData::forOperation('order.process', 'OrderService')
|
||||
->addData(['order_id' => 'order-789', 'amount' => 1000])
|
||||
->withUserId('user-123')
|
||||
->withRequestId('req-456')
|
||||
->withTags('payment', 'external_api');
|
||||
|
||||
$this->contextProvider->attach($exception, $context);
|
||||
|
||||
$this->exceptionAuditLogger->logIfAuditable($exception);
|
||||
|
||||
$entries = $this->getAllAuditEntries($this->auditLogger);
|
||||
$entry = $entries[0];
|
||||
|
||||
expect($entry->metadata)->toHaveKey('operation');
|
||||
expect($entry->metadata['operation'])->toBe('order.process');
|
||||
expect($entry->metadata)->toHaveKey('component');
|
||||
expect($entry->metadata['component'])->toBe('OrderService');
|
||||
expect($entry->metadata)->toHaveKey('context_data');
|
||||
expect($entry->metadata['context_data']['order_id'])->toBe('order-789');
|
||||
expect($entry->metadata)->toHaveKey('tags');
|
||||
expect($entry->metadata['tags'])->toBe(['payment', 'external_api']);
|
||||
});
|
||||
|
||||
it('determines audit action from operation name', function () {
|
||||
$testCases = [
|
||||
['operation' => 'user.create', 'expected' => AuditableAction::CREATE],
|
||||
['operation' => 'order.update', 'expected' => AuditableAction::UPDATE],
|
||||
['operation' => 'product.delete', 'expected' => AuditableAction::DELETE],
|
||||
['operation' => 'data.read', 'expected' => AuditableAction::READ],
|
||||
];
|
||||
|
||||
foreach ($testCases as $testCase) {
|
||||
$exception = new RuntimeException('Test');
|
||||
$context = ExceptionContextData::forOperation($testCase['operation'])
|
||||
->withAuditable(true);
|
||||
|
||||
$this->contextProvider->attach($exception, $context);
|
||||
$this->exceptionAuditLogger->logIfAuditable($exception);
|
||||
|
||||
$entries = $this->getAllAuditEntries($this->auditLogger);
|
||||
$entry = end($entries);
|
||||
|
||||
expect($entry->action)->toBe($testCase['expected']);
|
||||
}
|
||||
});
|
||||
|
||||
it('extracts entity ID from context data', function () {
|
||||
$exception = new RuntimeException('Entity not found');
|
||||
$context = ExceptionContextData::forOperation('entity.get')
|
||||
->addData(['entity_id' => 'entity-123'])
|
||||
->withAuditable(true);
|
||||
|
||||
$this->contextProvider->attach($exception, $context);
|
||||
|
||||
$this->exceptionAuditLogger->logIfAuditable($exception);
|
||||
|
||||
$entries = $this->getAllAuditEntries($this->auditLogger);
|
||||
$entry = $entries[0];
|
||||
|
||||
expect($entry->entityId)->toBe('entity-123');
|
||||
});
|
||||
|
||||
it('handles IP address and user agent from context with Value Objects', function () {
|
||||
$exception = new RuntimeException('Security violation');
|
||||
$context = ExceptionContextData::forOperation('security.check')
|
||||
->withClientIp(\App\Framework\Http\IpAddress::from('192.168.1.1'))
|
||||
->withUserAgent(\App\Framework\UserAgent\UserAgent::fromString('Mozilla/5.0'))
|
||||
->withAuditable(true);
|
||||
|
||||
$this->contextProvider->attach($exception, $context);
|
||||
|
||||
$this->exceptionAuditLogger->logIfAuditable($exception);
|
||||
|
||||
$entries = $this->getAllAuditEntries($this->auditLogger);
|
||||
$entry = $entries[0];
|
||||
|
||||
expect($entry->ipAddress)->not->toBeNull();
|
||||
expect((string) $entry->ipAddress)->toBe('192.168.1.1');
|
||||
expect($entry->userAgent)->not->toBeNull();
|
||||
expect($entry->userAgent->value)->toBe('Mozilla/5.0');
|
||||
});
|
||||
|
||||
it('handles IP address and user agent from context with strings (backward compatibility)', function () {
|
||||
$exception = new RuntimeException('Security violation');
|
||||
$context = ExceptionContextData::forOperation('security.check')
|
||||
->withClientIp('192.168.1.1')
|
||||
->withUserAgent('Mozilla/5.0')
|
||||
->withAuditable(true);
|
||||
|
||||
$this->contextProvider->attach($exception, $context);
|
||||
|
||||
$this->exceptionAuditLogger->logIfAuditable($exception);
|
||||
|
||||
$entries = $this->getAllAuditEntries($this->auditLogger);
|
||||
$entry = $entries[0];
|
||||
|
||||
expect($entry->ipAddress)->not->toBeNull();
|
||||
expect((string) $entry->ipAddress)->toBe('192.168.1.1');
|
||||
expect($entry->userAgent)->not->toBeNull();
|
||||
expect($entry->userAgent->value)->toBe('Mozilla/5.0');
|
||||
});
|
||||
|
||||
it('does not throw when audit logging fails', function () {
|
||||
$failingAuditLogger = new class implements AuditLogger {
|
||||
public function log(AuditEntry $entry): void
|
||||
{
|
||||
throw new RuntimeException('Audit logging failed');
|
||||
}
|
||||
|
||||
public function find(\App\Framework\Audit\ValueObjects\AuditId $id): ?AuditEntry
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function query(\App\Framework\Audit\ValueObjects\AuditQuery $query): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function count(\App\Framework\Audit\ValueObjects\AuditQuery $query): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function purgeOlderThan(DateTimeImmutable $date): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
$logger = new ExceptionAuditLogger(
|
||||
$failingAuditLogger,
|
||||
$this->clock,
|
||||
$this->contextProvider
|
||||
);
|
||||
|
||||
$exception = new RuntimeException('Test error');
|
||||
$context = ExceptionContextData::forOperation('test')
|
||||
->withAuditable(true);
|
||||
|
||||
$this->contextProvider->attach($exception, $context);
|
||||
|
||||
// Should not throw
|
||||
expect(fn() => $logger->logIfAuditable($exception))->not->toThrow();
|
||||
});
|
||||
|
||||
it('includes previous exception in metadata', function () {
|
||||
$previous = new InvalidArgumentException('Invalid input');
|
||||
$exception = new RuntimeException('Operation failed', 0, $previous);
|
||||
$context = ExceptionContextData::forOperation('operation.execute')
|
||||
->withAuditable(true);
|
||||
|
||||
$this->contextProvider->attach($exception, $context);
|
||||
|
||||
$this->exceptionAuditLogger->logIfAuditable($exception);
|
||||
|
||||
$entries = $this->getAllAuditEntries($this->auditLogger);
|
||||
$entry = $entries[0];
|
||||
|
||||
expect($entry->metadata)->toHaveKey('previous_exception');
|
||||
expect($entry->metadata['previous_exception']['class'])->toBe(InvalidArgumentException::class);
|
||||
expect($entry->metadata['previous_exception']['message'])->toBe('Invalid input');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper to get all audit entries from InMemoryAuditLogger
|
||||
*
|
||||
* @return array<AuditEntry>
|
||||
*/
|
||||
function getAllAuditEntries(AuditLogger $logger): array
|
||||
{
|
||||
if ($logger instanceof InMemoryAuditLogger) {
|
||||
// Use reflection to access private entries property
|
||||
$reflection = new ReflectionClass($logger);
|
||||
$property = $reflection->getProperty('entries');
|
||||
$property->setAccessible(true);
|
||||
return array_values($property->getValue($logger));
|
||||
}
|
||||
|
||||
// For other implementations, query all entries
|
||||
$query = new \App\Framework\Audit\ValueObjects\AuditQuery();
|
||||
return $logger->query($query);
|
||||
}
|
||||
|
||||
@@ -174,6 +174,36 @@ describe('Exception Context Integration', function () {
|
||||
expect($context2->data['query'])->toBe('SELECT * FROM users');
|
||||
});
|
||||
|
||||
it('serializes Value Objects to strings in toArray()', function () {
|
||||
$context = ExceptionContextData::forOperation('test.operation')
|
||||
->withClientIp(\App\Framework\Http\IpAddress::from('192.168.1.1'))
|
||||
->withUserAgent(\App\Framework\UserAgent\UserAgent::fromString('Mozilla/5.0'))
|
||||
->withSessionId(\App\Framework\Http\Session\SessionId::fromString('session-12345678901234567890123456789012'))
|
||||
->withRequestId('req-123');
|
||||
|
||||
$array = $context->toArray();
|
||||
|
||||
expect($array['client_ip'])->toBe('192.168.1.1');
|
||||
expect($array['user_agent'])->toBe('Mozilla/5.0');
|
||||
expect($array['session_id'])->toBe('session-12345678901234567890123456789012');
|
||||
expect($array['request_id'])->toBe('req-123');
|
||||
});
|
||||
|
||||
it('serializes strings to strings in toArray() (backward compatibility)', function () {
|
||||
$context = ExceptionContextData::forOperation('test.operation')
|
||||
->withClientIp('192.168.1.1')
|
||||
->withUserAgent('Mozilla/5.0')
|
||||
->withSessionId('session-123')
|
||||
->withRequestId('req-123');
|
||||
|
||||
$array = $context->toArray();
|
||||
|
||||
expect($array['client_ip'])->toBe('192.168.1.1');
|
||||
expect($array['user_agent'])->toBe('Mozilla/5.0');
|
||||
expect($array['session_id'])->toBe('session-123');
|
||||
expect($array['request_id'])->toBe('req-123');
|
||||
});
|
||||
|
||||
it('handles nested error scopes correctly', function () {
|
||||
// Outer scope
|
||||
$outerScope = ErrorScopeContext::http(
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\String\ValueObjects\PosixAlphaString;
|
||||
use App\Framework\String\ValueObjects\TypedString;
|
||||
use App\Framework\String\ValueObjects\PosixString;
|
||||
|
||||
describe('PosixAlphaString', function () {
|
||||
describe('Validation', function () {
|
||||
it('accepts valid alphabetic strings', function () {
|
||||
$alpha1 = PosixAlphaString::fromString('abcdefghijklmnopqrstuvwxyz');
|
||||
$alpha2 = PosixAlphaString::fromString('ABCDEFGHIJKLMNOPQRSTUVWXYZ');
|
||||
$alpha3 = PosixAlphaString::fromString('HelloWorld');
|
||||
|
||||
expect($alpha1->value)->toBe('abcdefghijklmnopqrstuvwxyz');
|
||||
expect($alpha2->value)->toBe('ABCDEFGHIJKLMNOPQRSTUVWXYZ');
|
||||
expect($alpha3->value)->toBe('HelloWorld');
|
||||
});
|
||||
|
||||
it('rejects strings with digits', function () {
|
||||
PosixAlphaString::fromString('abc123');
|
||||
})->throws(\InvalidArgumentException::class, 'String must contain only POSIX alphabetic characters');
|
||||
|
||||
it('rejects strings with punctuation', function () {
|
||||
PosixAlphaString::fromString('hello!world');
|
||||
})->throws(\InvalidArgumentException::class, 'String must contain only POSIX alphabetic characters');
|
||||
|
||||
it('rejects strings with whitespace', function () {
|
||||
PosixAlphaString::fromString('hello world');
|
||||
})->throws(\InvalidArgumentException::class, 'String must contain only POSIX alphabetic characters');
|
||||
|
||||
it('rejects strings with hyphens', function () {
|
||||
PosixAlphaString::fromString('hello-world');
|
||||
})->throws(\InvalidArgumentException::class, 'String must contain only POSIX alphabetic characters');
|
||||
|
||||
it('rejects strings with underscores', function () {
|
||||
PosixAlphaString::fromString('hello_world');
|
||||
})->throws(\InvalidArgumentException::class, 'String must contain only POSIX alphabetic characters');
|
||||
|
||||
it('rejects empty strings', function () {
|
||||
PosixAlphaString::fromString('');
|
||||
})->throws(\InvalidArgumentException::class, 'String must not be empty');
|
||||
|
||||
it('rejects strings with special characters', function () {
|
||||
PosixAlphaString::fromString('hello@world');
|
||||
})->throws(\InvalidArgumentException::class, 'String must contain only POSIX alphabetic characters');
|
||||
});
|
||||
|
||||
describe('International Character Support', function () {
|
||||
it('validates German characters with locale', function () {
|
||||
// Note: Requires locale to be set
|
||||
// setlocale(LC_CTYPE, 'de_DE.UTF-8');
|
||||
|
||||
$german1 = PosixAlphaString::fromString('Müller');
|
||||
$german2 = PosixAlphaString::fromString('Schön');
|
||||
$german3 = PosixAlphaString::fromString('Größe');
|
||||
|
||||
expect($german1->value)->toBe('Müller');
|
||||
expect($german2->value)->toBe('Schön');
|
||||
expect($german3->value)->toBe('Größe');
|
||||
})->skip('Requires locale configuration');
|
||||
|
||||
it('validates French characters with locale', function () {
|
||||
// Note: Requires locale to be set
|
||||
// setlocale(LC_CTYPE, 'fr_FR.UTF-8');
|
||||
|
||||
$french1 = PosixAlphaString::fromString('Dupré');
|
||||
$french2 = PosixAlphaString::fromString('Château');
|
||||
$french3 = PosixAlphaString::fromString('Garçon');
|
||||
|
||||
expect($french1->value)->toBe('Dupré');
|
||||
expect($french2->value)->toBe('Château');
|
||||
expect($french3->value)->toBe('Garçon');
|
||||
})->skip('Requires locale configuration');
|
||||
|
||||
it('validates Spanish characters with locale', function () {
|
||||
// Note: Requires locale to be set
|
||||
// setlocale(LC_CTYPE, 'es_ES.UTF-8');
|
||||
|
||||
$spanish1 = PosixAlphaString::fromString('Señor');
|
||||
$spanish2 = PosixAlphaString::fromString('Niño');
|
||||
$spanish3 = PosixAlphaString::fromString('José');
|
||||
|
||||
expect($spanish1->value)->toBe('Señor');
|
||||
expect($spanish2->value)->toBe('Niño');
|
||||
expect($spanish3->value)->toBe('José');
|
||||
})->skip('Requires locale configuration');
|
||||
});
|
||||
|
||||
describe('Conversion Methods', function () {
|
||||
it('converts to TypedString', function () {
|
||||
$alpha = PosixAlphaString::fromString('Hello');
|
||||
$typed = $alpha->toTypedString();
|
||||
|
||||
expect($typed)->toBeInstanceOf(TypedString::class);
|
||||
expect($typed->value)->toBe('Hello');
|
||||
expect($typed->isAlphabetic())->toBeTrue();
|
||||
});
|
||||
|
||||
it('converts to PosixString', function () {
|
||||
$alpha = PosixAlphaString::fromString('World');
|
||||
$posix = $alpha->toPosixString();
|
||||
|
||||
expect($posix)->toBeInstanceOf(PosixString::class);
|
||||
expect($posix->value)->toBe('World');
|
||||
expect($posix->isAlpha())->toBeTrue();
|
||||
});
|
||||
|
||||
it('converts to string', function () {
|
||||
$alpha = PosixAlphaString::fromString('Test');
|
||||
|
||||
expect($alpha->toString())->toBe('Test');
|
||||
expect((string) $alpha)->toBe('Test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Comparison Methods', function () {
|
||||
it('checks equality', function () {
|
||||
$alpha1 = PosixAlphaString::fromString('Hello');
|
||||
$alpha2 = PosixAlphaString::fromString('Hello');
|
||||
$alpha3 = PosixAlphaString::fromString('World');
|
||||
|
||||
expect($alpha1->equals($alpha2))->toBeTrue();
|
||||
expect($alpha1->equals($alpha3))->toBeFalse();
|
||||
});
|
||||
|
||||
it('compares case-sensitive', function () {
|
||||
$lower = PosixAlphaString::fromString('hello');
|
||||
$upper = PosixAlphaString::fromString('HELLO');
|
||||
|
||||
expect($lower->equals($upper))->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Real World Use Cases', function () {
|
||||
it('validates first names', function () {
|
||||
$firstName1 = PosixAlphaString::fromString('John');
|
||||
$firstName2 = PosixAlphaString::fromString('Mary');
|
||||
$firstName3 = PosixAlphaString::fromString('Alexander');
|
||||
|
||||
expect($firstName1->value)->toBe('John');
|
||||
expect($firstName2->value)->toBe('Mary');
|
||||
expect($firstName3->value)->toBe('Alexander');
|
||||
});
|
||||
|
||||
it('validates last names', function () {
|
||||
$lastName1 = PosixAlphaString::fromString('Smith');
|
||||
$lastName2 = PosixAlphaString::fromString('Johnson');
|
||||
$lastName3 = PosixAlphaString::fromString('Williams');
|
||||
|
||||
expect($lastName1->value)->toBe('Smith');
|
||||
expect($lastName2->value)->toBe('Johnson');
|
||||
expect($lastName3->value)->toBe('Williams');
|
||||
});
|
||||
|
||||
it('rejects names with numbers', function () {
|
||||
PosixAlphaString::fromString('John123');
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
|
||||
it('rejects compound names with hyphens', function () {
|
||||
// Note: Real-world compound names like "Jean-Pierre" would need
|
||||
// a different validation strategy or preprocessing
|
||||
PosixAlphaString::fromString('Jean-Pierre');
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
|
||||
it('rejects names with titles', function () {
|
||||
PosixAlphaString::fromString('Dr. Smith');
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
});
|
||||
|
||||
describe('Single Character Strings', function () {
|
||||
it('accepts single letter', function () {
|
||||
$a = PosixAlphaString::fromString('A');
|
||||
$z = PosixAlphaString::fromString('z');
|
||||
|
||||
expect($a->value)->toBe('A');
|
||||
expect($z->value)->toBe('z');
|
||||
});
|
||||
|
||||
it('rejects single digit', function () {
|
||||
PosixAlphaString::fromString('5');
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
|
||||
it('rejects single special character', function () {
|
||||
PosixAlphaString::fromString('!');
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
});
|
||||
|
||||
describe('Edge Cases', function () {
|
||||
it('handles very long alphabetic strings', function () {
|
||||
$longString = str_repeat('abcdefghijklmnopqrstuvwxyz', 100);
|
||||
$alpha = PosixAlphaString::fromString($longString);
|
||||
|
||||
expect($alpha->value)->toBe($longString);
|
||||
expect(strlen($alpha->value))->toBe(2600);
|
||||
});
|
||||
|
||||
it('handles mixed case correctly', function () {
|
||||
$mixed = PosixAlphaString::fromString('AbCdEfGhIjKlMnOpQrStUvWxYz');
|
||||
|
||||
expect($mixed->value)->toBe('AbCdEfGhIjKlMnOpQrStUvWxYz');
|
||||
});
|
||||
|
||||
it('rejects numeric-looking strings', function () {
|
||||
PosixAlphaString::fromString('one23');
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
});
|
||||
});
|
||||
278
tests/Unit/Framework/String/ValueObjects/PosixStringTest.php
Normal file
278
tests/Unit/Framework/String/ValueObjects/PosixStringTest.php
Normal file
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\String\ValueObjects\PosixString;
|
||||
|
||||
describe('PosixString', function () {
|
||||
describe('Factory Method', function () {
|
||||
it('creates from string', function () {
|
||||
$str = PosixString::fromString('test123');
|
||||
|
||||
expect($str->value)->toBe('test123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POSIX Character Class Validators', function () {
|
||||
it('validates alnum (alphanumeric)', function () {
|
||||
$valid = PosixString::fromString('abc123XYZ');
|
||||
$invalid = PosixString::fromString('abc-123');
|
||||
|
||||
expect($valid->isAlnum())->toBeTrue();
|
||||
expect($invalid->isAlnum())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates alpha (alphabetic)', function () {
|
||||
$valid = PosixString::fromString('abcXYZ');
|
||||
$invalid = PosixString::fromString('abc123');
|
||||
|
||||
expect($valid->isAlpha())->toBeTrue();
|
||||
expect($invalid->isAlpha())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates alpha with international characters (locale-aware)', function () {
|
||||
// Note: Locale must be set for international character support
|
||||
// setlocale(LC_CTYPE, 'de_DE.UTF-8');
|
||||
|
||||
$german = PosixString::fromString('Müller');
|
||||
$french = PosixString::fromString('Dupré');
|
||||
$spanish = PosixString::fromString('Señor');
|
||||
|
||||
// These work if locale is set
|
||||
// expect($german->isAlpha())->toBeTrue();
|
||||
// expect($french->isAlpha())->toBeTrue();
|
||||
// expect($spanish->isAlpha())->toBeTrue();
|
||||
})->skip('Requires locale configuration');
|
||||
|
||||
it('validates ascii', function () {
|
||||
$valid = PosixString::fromString('Hello World!');
|
||||
$invalid = PosixString::fromString('Hëllö Wörld!');
|
||||
|
||||
expect($valid->isAscii())->toBeTrue();
|
||||
expect($invalid->isAscii())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates blank (space and tab only)', function () {
|
||||
$valid = PosixString::fromString(" \t ");
|
||||
$invalid = PosixString::fromString(" \n ");
|
||||
|
||||
expect($valid->isBlank())->toBeTrue();
|
||||
expect($invalid->isBlank())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates cntrl (control characters)', function () {
|
||||
$valid = PosixString::fromString("\x00\x01\x02");
|
||||
$invalid = PosixString::fromString("abc");
|
||||
|
||||
expect($valid->isCntrl())->toBeTrue();
|
||||
expect($invalid->isCntrl())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates digit', function () {
|
||||
$valid = PosixString::fromString('123456');
|
||||
$invalid = PosixString::fromString('123abc');
|
||||
|
||||
expect($valid->isDigit())->toBeTrue();
|
||||
expect($invalid->isDigit())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates graph (visible characters, excluding space)', function () {
|
||||
$valid = PosixString::fromString('Hello!');
|
||||
$invalid = PosixString::fromString('Hello World');
|
||||
|
||||
expect($valid->isGraph())->toBeTrue();
|
||||
expect($invalid->isGraph())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates lower (lowercase)', function () {
|
||||
$valid = PosixString::fromString('hello');
|
||||
$invalid = PosixString::fromString('Hello');
|
||||
|
||||
expect($valid->isLower())->toBeTrue();
|
||||
expect($invalid->isLower())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates print (printable, including space)', function () {
|
||||
$valid = PosixString::fromString('Hello World!');
|
||||
$invalid = PosixString::fromString("Hello\x00World");
|
||||
|
||||
expect($valid->isPrint())->toBeTrue();
|
||||
expect($invalid->isPrint())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates punct (punctuation)', function () {
|
||||
$valid = PosixString::fromString('!@#$%^&*()');
|
||||
$invalid = PosixString::fromString('abc!@#');
|
||||
|
||||
expect($valid->isPunct())->toBeTrue();
|
||||
expect($invalid->isPunct())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates space (whitespace)', function () {
|
||||
$valid = PosixString::fromString(" \t\n\r");
|
||||
$invalid = PosixString::fromString('abc');
|
||||
|
||||
expect($valid->isSpace())->toBeTrue();
|
||||
expect($invalid->isSpace())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates upper (uppercase)', function () {
|
||||
$valid = PosixString::fromString('HELLO');
|
||||
$invalid = PosixString::fromString('Hello');
|
||||
|
||||
expect($valid->isUpper())->toBeTrue();
|
||||
expect($invalid->isUpper())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates xdigit (hexadecimal)', function () {
|
||||
$valid = PosixString::fromString('0123456789abcdefABCDEF');
|
||||
$invalid = PosixString::fromString('0123xyz');
|
||||
|
||||
expect($valid->isXdigit())->toBeTrue();
|
||||
expect($invalid->isXdigit())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates word (alphanumeric + underscore)', function () {
|
||||
$valid = PosixString::fromString('user_name123');
|
||||
$invalid = PosixString::fromString('user-name');
|
||||
|
||||
expect($valid->isWord())->toBeTrue();
|
||||
expect($invalid->isWord())->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Helper Methods', function () {
|
||||
it('checks if string contains POSIX class characters', function () {
|
||||
$str = PosixString::fromString('hello123world');
|
||||
|
||||
expect($str->containsPosixClass('alpha'))->toBeTrue();
|
||||
expect($str->containsPosixClass('digit'))->toBeTrue();
|
||||
expect($str->containsPosixClass('punct'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('counts POSIX class characters', function () {
|
||||
$str = PosixString::fromString('abc123xyz789');
|
||||
|
||||
expect($str->countPosixClass('alpha'))->toBe(6);
|
||||
expect($str->countPosixClass('digit'))->toBe(6);
|
||||
expect($str->countPosixClass('punct'))->toBe(0);
|
||||
});
|
||||
|
||||
it('filters by POSIX class', function () {
|
||||
$str = PosixString::fromString('abc123xyz');
|
||||
|
||||
$alphaOnly = $str->filterByPosixClass('alpha');
|
||||
$digitOnly = $str->filterByPosixClass('digit');
|
||||
|
||||
expect($alphaOnly->value)->toBe('abcxyz');
|
||||
expect($digitOnly->value)->toBe('123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Conversion Methods', function () {
|
||||
it('converts to TypedString', function () {
|
||||
$posix = PosixString::fromString('test');
|
||||
$typed = $posix->toTypedString();
|
||||
|
||||
expect($typed->value)->toBe('test');
|
||||
expect($typed->isAlphabetic())->toBeTrue();
|
||||
});
|
||||
|
||||
it('converts to string', function () {
|
||||
$str = PosixString::fromString('hello');
|
||||
|
||||
expect($str->toString())->toBe('hello');
|
||||
expect((string) $str)->toBe('hello');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Comparison Methods', function () {
|
||||
it('checks equality', function () {
|
||||
$str1 = PosixString::fromString('test');
|
||||
$str2 = PosixString::fromString('test');
|
||||
$str3 = PosixString::fromString('other');
|
||||
|
||||
expect($str1->equals($str2))->toBeTrue();
|
||||
expect($str1->equals($str3))->toBeFalse();
|
||||
});
|
||||
|
||||
it('checks if empty', function () {
|
||||
$empty = PosixString::fromString('');
|
||||
$notEmpty = PosixString::fromString('test');
|
||||
|
||||
expect($empty->isEmpty())->toBeTrue();
|
||||
expect($notEmpty->isEmpty())->toBeFalse();
|
||||
});
|
||||
|
||||
it('checks if not empty', function () {
|
||||
$empty = PosixString::fromString('');
|
||||
$notEmpty = PosixString::fromString('test');
|
||||
|
||||
expect($empty->isNotEmpty())->toBeFalse();
|
||||
expect($notEmpty->isNotEmpty())->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', function () {
|
||||
it('handles empty string validation', function () {
|
||||
$empty = PosixString::fromString('');
|
||||
|
||||
expect($empty->isAlpha())->toBeFalse();
|
||||
expect($empty->isAlnum())->toBeFalse();
|
||||
expect($empty->isDigit())->toBeFalse();
|
||||
});
|
||||
|
||||
it('handles single character validation', function () {
|
||||
$alpha = PosixString::fromString('a');
|
||||
$digit = PosixString::fromString('5');
|
||||
$punct = PosixString::fromString('!');
|
||||
|
||||
expect($alpha->isAlpha())->toBeTrue();
|
||||
expect($digit->isDigit())->toBeTrue();
|
||||
expect($punct->isPunct())->toBeTrue();
|
||||
});
|
||||
|
||||
it('handles mixed character sets', function () {
|
||||
$mixed = PosixString::fromString('abc123!@#');
|
||||
|
||||
expect($mixed->isAlnum())->toBeFalse(); // Contains punctuation
|
||||
expect($mixed->containsPosixClass('alpha'))->toBeTrue();
|
||||
expect($mixed->containsPosixClass('digit'))->toBeTrue();
|
||||
expect($mixed->containsPosixClass('punct'))->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Real World Use Cases', function () {
|
||||
it('validates usernames', function () {
|
||||
$validUsername = PosixString::fromString('john_doe_123');
|
||||
$invalidUsername = PosixString::fromString('john-doe@example');
|
||||
|
||||
expect($validUsername->isWord())->toBeTrue();
|
||||
expect($invalidUsername->isWord())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates identifiers', function () {
|
||||
$validId = PosixString::fromString('MAX_TIMEOUT_VALUE');
|
||||
$invalidId = PosixString::fromString('max-timeout-value');
|
||||
|
||||
expect($validId->isWord())->toBeTrue();
|
||||
expect($invalidId->isWord())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates hexadecimal color codes', function () {
|
||||
$validColor = PosixString::fromString('FF5733');
|
||||
$invalidColor = PosixString::fromString('GG5733');
|
||||
|
||||
expect($validColor->isXdigit())->toBeTrue();
|
||||
expect($invalidColor->isXdigit())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates numeric strings', function () {
|
||||
$validNum = PosixString::fromString('123456');
|
||||
$invalidNum = PosixString::fromString('12.34');
|
||||
|
||||
expect($validNum->isDigit())->toBeTrue();
|
||||
expect($invalidNum->isDigit())->toBeFalse();
|
||||
});
|
||||
});
|
||||
});
|
||||
296
tests/Unit/Framework/String/ValueObjects/PosixWordStringTest.php
Normal file
296
tests/Unit/Framework/String/ValueObjects/PosixWordStringTest.php
Normal file
@@ -0,0 +1,296 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\String\ValueObjects\PosixWordString;
|
||||
use App\Framework\String\ValueObjects\TypedString;
|
||||
use App\Framework\String\ValueObjects\PosixString;
|
||||
|
||||
describe('PosixWordString', function () {
|
||||
describe('Validation', function () {
|
||||
it('accepts valid word strings', function () {
|
||||
$word1 = PosixWordString::fromString('hello');
|
||||
$word2 = PosixWordString::fromString('Hello123');
|
||||
$word3 = PosixWordString::fromString('user_name');
|
||||
$word4 = PosixWordString::fromString('MAX_VALUE');
|
||||
$word5 = PosixWordString::fromString('_private');
|
||||
|
||||
expect($word1->value)->toBe('hello');
|
||||
expect($word2->value)->toBe('Hello123');
|
||||
expect($word3->value)->toBe('user_name');
|
||||
expect($word4->value)->toBe('MAX_VALUE');
|
||||
expect($word5->value)->toBe('_private');
|
||||
});
|
||||
|
||||
it('rejects strings with hyphens', function () {
|
||||
PosixWordString::fromString('user-name');
|
||||
})->throws(\InvalidArgumentException::class, 'String must contain only POSIX word characters');
|
||||
|
||||
it('rejects strings with spaces', function () {
|
||||
PosixWordString::fromString('hello world');
|
||||
})->throws(\InvalidArgumentException::class, 'String must contain only POSIX word characters');
|
||||
|
||||
it('rejects strings with special characters', function () {
|
||||
PosixWordString::fromString('hello@world');
|
||||
})->throws(\InvalidArgumentException::class, 'String must contain only POSIX word characters');
|
||||
|
||||
it('rejects strings with punctuation', function () {
|
||||
PosixWordString::fromString('hello.world');
|
||||
})->throws(\InvalidArgumentException::class, 'String must contain only POSIX word characters');
|
||||
|
||||
it('rejects strings with operators', function () {
|
||||
PosixWordString::fromString('a+b');
|
||||
})->throws(\InvalidArgumentException::class, 'String must contain only POSIX word characters');
|
||||
|
||||
it('rejects empty strings', function () {
|
||||
PosixWordString::fromString('');
|
||||
})->throws(\InvalidArgumentException::class, 'String must not be empty');
|
||||
|
||||
it('rejects strings with parentheses', function () {
|
||||
PosixWordString::fromString('function()');
|
||||
})->throws(\InvalidArgumentException::class, 'String must contain only POSIX word characters');
|
||||
|
||||
it('rejects strings with brackets', function () {
|
||||
PosixWordString::fromString('array[0]');
|
||||
})->throws(\InvalidArgumentException::class, 'String must contain only POSIX word characters');
|
||||
});
|
||||
|
||||
describe('Conversion Methods', function () {
|
||||
it('converts to TypedString', function () {
|
||||
$word = PosixWordString::fromString('user_id');
|
||||
$typed = $word->toTypedString();
|
||||
|
||||
expect($typed)->toBeInstanceOf(TypedString::class);
|
||||
expect($typed->value)->toBe('user_id');
|
||||
expect($typed->isAlphanumeric())->toBeFalse(); // Contains underscore
|
||||
});
|
||||
|
||||
it('converts to PosixString', function () {
|
||||
$word = PosixWordString::fromString('MAX_VALUE');
|
||||
$posix = $word->toPosixString();
|
||||
|
||||
expect($posix)->toBeInstanceOf(PosixString::class);
|
||||
expect($posix->value)->toBe('MAX_VALUE');
|
||||
expect($posix->isWord())->toBeTrue();
|
||||
});
|
||||
|
||||
it('converts to string', function () {
|
||||
$word = PosixWordString::fromString('username');
|
||||
|
||||
expect($word->toString())->toBe('username');
|
||||
expect((string) $word)->toBe('username');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Comparison Methods', function () {
|
||||
it('checks equality', function () {
|
||||
$word1 = PosixWordString::fromString('user_id');
|
||||
$word2 = PosixWordString::fromString('user_id');
|
||||
$word3 = PosixWordString::fromString('user_name');
|
||||
|
||||
expect($word1->equals($word2))->toBeTrue();
|
||||
expect($word1->equals($word3))->toBeFalse();
|
||||
});
|
||||
|
||||
it('compares case-sensitive', function () {
|
||||
$lower = PosixWordString::fromString('username');
|
||||
$upper = PosixWordString::fromString('USERNAME');
|
||||
|
||||
expect($lower->equals($upper))->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Real World Use Cases', function () {
|
||||
describe('PHP Identifiers', function () {
|
||||
it('validates variable names', function () {
|
||||
$var1 = PosixWordString::fromString('userName');
|
||||
$var2 = PosixWordString::fromString('user_id');
|
||||
$var3 = PosixWordString::fromString('_privateVar');
|
||||
|
||||
expect($var1->value)->toBe('userName');
|
||||
expect($var2->value)->toBe('user_id');
|
||||
expect($var3->value)->toBe('_privateVar');
|
||||
});
|
||||
|
||||
it('validates constant names', function () {
|
||||
$const1 = PosixWordString::fromString('MAX_SIZE');
|
||||
$const2 = PosixWordString::fromString('API_KEY');
|
||||
$const3 = PosixWordString::fromString('DEFAULT_TIMEOUT');
|
||||
|
||||
expect($const1->value)->toBe('MAX_SIZE');
|
||||
expect($const2->value)->toBe('API_KEY');
|
||||
expect($const3->value)->toBe('DEFAULT_TIMEOUT');
|
||||
});
|
||||
|
||||
it('validates function names', function () {
|
||||
$func1 = PosixWordString::fromString('calculateTotal');
|
||||
$func2 = PosixWordString::fromString('get_user_by_id');
|
||||
$func3 = PosixWordString::fromString('__construct');
|
||||
|
||||
expect($func1->value)->toBe('calculateTotal');
|
||||
expect($func2->value)->toBe('get_user_by_id');
|
||||
expect($func3->value)->toBe('__construct');
|
||||
});
|
||||
|
||||
it('rejects invalid PHP identifiers', function () {
|
||||
PosixWordString::fromString('my-function');
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
});
|
||||
|
||||
describe('Database Column Names', function () {
|
||||
it('validates snake_case column names', function () {
|
||||
$col1 = PosixWordString::fromString('user_id');
|
||||
$col2 = PosixWordString::fromString('created_at');
|
||||
$col3 = PosixWordString::fromString('email_verified_at');
|
||||
|
||||
expect($col1->value)->toBe('user_id');
|
||||
expect($col2->value)->toBe('created_at');
|
||||
expect($col3->value)->toBe('email_verified_at');
|
||||
});
|
||||
|
||||
it('rejects kebab-case column names', function () {
|
||||
PosixWordString::fromString('user-id');
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
});
|
||||
|
||||
describe('Usernames', function () {
|
||||
it('validates valid usernames', function () {
|
||||
$user1 = PosixWordString::fromString('john_doe');
|
||||
$user2 = PosixWordString::fromString('user123');
|
||||
$user3 = PosixWordString::fromString('admin_user');
|
||||
|
||||
expect($user1->value)->toBe('john_doe');
|
||||
expect($user2->value)->toBe('user123');
|
||||
expect($user3->value)->toBe('admin_user');
|
||||
});
|
||||
|
||||
it('rejects usernames with special characters', function () {
|
||||
PosixWordString::fromString('john@doe');
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
|
||||
it('rejects usernames with hyphens', function () {
|
||||
PosixWordString::fromString('john-doe');
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
|
||||
it('rejects usernames with spaces', function () {
|
||||
PosixWordString::fromString('john doe');
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
});
|
||||
|
||||
describe('URL Slugs (with underscore)', function () {
|
||||
it('validates underscore slugs', function () {
|
||||
$slug1 = PosixWordString::fromString('my_blog_post');
|
||||
$slug2 = PosixWordString::fromString('product_category_1');
|
||||
$slug3 = PosixWordString::fromString('about_us');
|
||||
|
||||
expect($slug1->value)->toBe('my_blog_post');
|
||||
expect($slug2->value)->toBe('product_category_1');
|
||||
expect($slug3->value)->toBe('about_us');
|
||||
});
|
||||
|
||||
it('rejects hyphenated slugs', function () {
|
||||
// Note: Hyphenated slugs need different validation
|
||||
PosixWordString::fromString('my-blog-post');
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Single Character Strings', function () {
|
||||
it('accepts single letter', function () {
|
||||
$a = PosixWordString::fromString('a');
|
||||
$Z = PosixWordString::fromString('Z');
|
||||
|
||||
expect($a->value)->toBe('a');
|
||||
expect($Z->value)->toBe('Z');
|
||||
});
|
||||
|
||||
it('accepts single digit', function () {
|
||||
$five = PosixWordString::fromString('5');
|
||||
|
||||
expect($five->value)->toBe('5');
|
||||
});
|
||||
|
||||
it('accepts single underscore', function () {
|
||||
$underscore = PosixWordString::fromString('_');
|
||||
|
||||
expect($underscore->value)->toBe('_');
|
||||
});
|
||||
|
||||
it('rejects single special character', function () {
|
||||
PosixWordString::fromString('!');
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
|
||||
it('rejects single hyphen', function () {
|
||||
PosixWordString::fromString('-');
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
});
|
||||
|
||||
describe('Edge Cases', function () {
|
||||
it('handles very long word strings', function () {
|
||||
$longString = str_repeat('abc123_', 100);
|
||||
$word = PosixWordString::fromString($longString);
|
||||
|
||||
expect($word->value)->toBe($longString);
|
||||
expect(strlen($word->value))->toBe(700);
|
||||
});
|
||||
|
||||
it('handles mixed case correctly', function () {
|
||||
$mixed = PosixWordString::fromString('CamelCase_With_Underscore123');
|
||||
|
||||
expect($mixed->value)->toBe('CamelCase_With_Underscore123');
|
||||
});
|
||||
|
||||
it('accepts strings starting with underscore', function () {
|
||||
$leading = PosixWordString::fromString('_privateMethod');
|
||||
|
||||
expect($leading->value)->toBe('_privateMethod');
|
||||
});
|
||||
|
||||
it('accepts strings starting with digit', function () {
|
||||
// Note: Valid for word class, but not valid PHP identifiers
|
||||
$digit = PosixWordString::fromString('123abc');
|
||||
|
||||
expect($digit->value)->toBe('123abc');
|
||||
});
|
||||
|
||||
it('accepts strings with only digits', function () {
|
||||
$digits = PosixWordString::fromString('123456');
|
||||
|
||||
expect($digits->value)->toBe('123456');
|
||||
});
|
||||
|
||||
it('accepts strings with only underscores', function () {
|
||||
$underscores = PosixWordString::fromString('___');
|
||||
|
||||
expect($underscores->value)->toBe('___');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Boundary Cases', function () {
|
||||
it('handles maximum practical length', function () {
|
||||
// 255 characters - typical database VARCHAR limit
|
||||
$longIdentifier = str_repeat('a', 255);
|
||||
$word = PosixWordString::fromString($longIdentifier);
|
||||
|
||||
expect(strlen($word->value))->toBe(255);
|
||||
});
|
||||
|
||||
it('handles alphanumeric only', function () {
|
||||
$alnum = PosixWordString::fromString('abc123XYZ789');
|
||||
|
||||
expect($alnum->value)->toBe('abc123XYZ789');
|
||||
});
|
||||
|
||||
it('handles underscores only', function () {
|
||||
$underscores = PosixWordString::fromString('_____');
|
||||
|
||||
expect($underscores->value)->toBe('_____');
|
||||
});
|
||||
|
||||
it('handles mixed content', function () {
|
||||
$mixed = PosixWordString::fromString('a1_B2_c3_D4');
|
||||
|
||||
expect($mixed->value)->toBe('a1_B2_c3_D4');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\String\ValueObjects\{
|
||||
AlphanumericString,
|
||||
NumericString,
|
||||
HexadecimalString,
|
||||
PrintableString
|
||||
};
|
||||
|
||||
describe('AlphanumericString', function () {
|
||||
it('creates from valid alphanumeric string', function () {
|
||||
$str = AlphanumericString::fromString('abc123');
|
||||
|
||||
expect($str->value)->toBe('abc123');
|
||||
});
|
||||
|
||||
it('rejects non-alphanumeric strings', function () {
|
||||
AlphanumericString::fromString('abc-123');
|
||||
})->throws(InvalidArgumentException::class, 'alphanumeric');
|
||||
|
||||
it('rejects empty strings', function () {
|
||||
AlphanumericString::fromString('');
|
||||
})->throws(InvalidArgumentException::class);
|
||||
|
||||
it('converts to TypedString', function () {
|
||||
$str = AlphanumericString::fromString('test123');
|
||||
$typed = $str->toTypedString();
|
||||
|
||||
expect($typed->value)->toBe('test123');
|
||||
expect($typed->isAlphanumeric())->toBeTrue();
|
||||
});
|
||||
|
||||
it('checks equality', function () {
|
||||
$str1 = AlphanumericString::fromString('abc123');
|
||||
$str2 = AlphanumericString::fromString('abc123');
|
||||
$str3 = AlphanumericString::fromString('xyz789');
|
||||
|
||||
expect($str1->equals($str2))->toBeTrue();
|
||||
expect($str1->equals($str3))->toBeFalse();
|
||||
});
|
||||
|
||||
it('converts to string', function () {
|
||||
$str = AlphanumericString::fromString('test123');
|
||||
|
||||
expect((string) $str)->toBe('test123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('NumericString', function () {
|
||||
it('creates from valid numeric string', function () {
|
||||
$str = NumericString::fromString('12345');
|
||||
|
||||
expect($str->value)->toBe('12345');
|
||||
});
|
||||
|
||||
it('rejects non-numeric strings', function () {
|
||||
NumericString::fromString('12.34');
|
||||
})->throws(InvalidArgumentException::class, 'digits');
|
||||
|
||||
it('rejects alphanumeric strings', function () {
|
||||
NumericString::fromString('123abc');
|
||||
})->throws(InvalidArgumentException::class);
|
||||
|
||||
it('rejects empty strings', function () {
|
||||
NumericString::fromString('');
|
||||
})->throws(InvalidArgumentException::class);
|
||||
|
||||
it('converts to integer', function () {
|
||||
$str = NumericString::fromString('12345');
|
||||
|
||||
expect($str->toInt())->toBe(12345);
|
||||
});
|
||||
|
||||
it('converts to float', function () {
|
||||
$str = NumericString::fromString('12345');
|
||||
|
||||
expect($str->toFloat())->toBe(12345.0);
|
||||
});
|
||||
|
||||
it('handles large numbers', function () {
|
||||
$str = NumericString::fromString('999999999');
|
||||
|
||||
expect($str->toInt())->toBe(999999999);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HexadecimalString', function () {
|
||||
it('creates from valid hexadecimal string', function () {
|
||||
$str = HexadecimalString::fromString('deadbeef');
|
||||
|
||||
expect($str->value)->toBe('deadbeef');
|
||||
});
|
||||
|
||||
it('accepts uppercase hexadecimal', function () {
|
||||
$str = HexadecimalString::fromString('DEADBEEF');
|
||||
|
||||
expect($str->value)->toBe('DEADBEEF');
|
||||
});
|
||||
|
||||
it('accepts mixed case hexadecimal', function () {
|
||||
$str = HexadecimalString::fromString('DeAdBeEf');
|
||||
|
||||
expect($str->value)->toBe('DeAdBeEf');
|
||||
});
|
||||
|
||||
it('rejects non-hexadecimal strings', function () {
|
||||
HexadecimalString::fromString('ghijk');
|
||||
})->throws(InvalidArgumentException::class, 'hexadecimal');
|
||||
|
||||
it('rejects empty strings', function () {
|
||||
HexadecimalString::fromString('');
|
||||
})->throws(InvalidArgumentException::class);
|
||||
|
||||
it('converts to binary', function () {
|
||||
$str = HexadecimalString::fromString('48656c6c6f'); // "Hello"
|
||||
$binary = $str->toBinary();
|
||||
|
||||
expect($binary)->toBe('Hello');
|
||||
});
|
||||
|
||||
it('converts to integer', function () {
|
||||
$str = HexadecimalString::fromString('ff');
|
||||
|
||||
expect($str->toInt())->toBe(255);
|
||||
});
|
||||
|
||||
it('handles long hexadecimal strings', function () {
|
||||
$str = HexadecimalString::fromString('0123456789abcdef');
|
||||
|
||||
expect($str->toInt())->toBe(81985529216486895);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PrintableString', function () {
|
||||
it('creates from valid printable string', function () {
|
||||
$str = PrintableString::fromString('Hello World!');
|
||||
|
||||
expect($str->value)->toBe('Hello World!');
|
||||
});
|
||||
|
||||
it('accepts strings with whitespace', function () {
|
||||
$str = PrintableString::fromString("Hello\nWorld\tTest");
|
||||
|
||||
expect($str->value)->toContain('Hello');
|
||||
});
|
||||
|
||||
it('rejects strings with control characters', function () {
|
||||
PrintableString::fromString("Hello\x00World");
|
||||
})->throws(InvalidArgumentException::class, 'printable');
|
||||
|
||||
it('rejects empty strings', function () {
|
||||
PrintableString::fromString('');
|
||||
})->throws(InvalidArgumentException::class);
|
||||
|
||||
it('accepts special characters', function () {
|
||||
$str = PrintableString::fromString('!@#$%^&*()_+-=[]{}|;:,.<>?');
|
||||
|
||||
expect($str->value)->toContain('!@#$');
|
||||
});
|
||||
|
||||
it('converts to TypedString', function () {
|
||||
$str = PrintableString::fromString('Test String');
|
||||
$typed = $str->toTypedString();
|
||||
|
||||
expect($typed->value)->toBe('Test String');
|
||||
expect($typed->isPrintable())->toBeTrue();
|
||||
});
|
||||
});
|
||||
170
tests/Unit/Framework/String/ValueObjects/TypedStringTest.php
Normal file
170
tests/Unit/Framework/String/ValueObjects/TypedStringTest.php
Normal file
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\String\ValueObjects\TypedString;
|
||||
|
||||
describe('TypedString - Character Type Checks', function () {
|
||||
it('validates alphanumeric strings', function () {
|
||||
expect(TypedString::fromString('abc123')->isAlphanumeric())->toBeTrue();
|
||||
expect(TypedString::fromString('ABC123')->isAlphanumeric())->toBeTrue();
|
||||
expect(TypedString::fromString('abc-123')->isAlphanumeric())->toBeFalse();
|
||||
expect(TypedString::fromString('')->isAlphanumeric())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates alphabetic strings', function () {
|
||||
expect(TypedString::fromString('abcABC')->isAlphabetic())->toBeTrue();
|
||||
expect(TypedString::fromString('abc123')->isAlphabetic())->toBeFalse();
|
||||
expect(TypedString::fromString('')->isAlphabetic())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates digit strings', function () {
|
||||
expect(TypedString::fromString('123456')->isDigits())->toBeTrue();
|
||||
expect(TypedString::fromString('12.34')->isDigits())->toBeFalse();
|
||||
expect(TypedString::fromString('abc')->isDigits())->toBeFalse();
|
||||
expect(TypedString::fromString('')->isDigits())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates lowercase strings', function () {
|
||||
expect(TypedString::fromString('abc')->isLowercase())->toBeTrue();
|
||||
expect(TypedString::fromString('Abc')->isLowercase())->toBeFalse();
|
||||
expect(TypedString::fromString('')->isLowercase())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates uppercase strings', function () {
|
||||
expect(TypedString::fromString('ABC')->isUppercase())->toBeTrue();
|
||||
expect(TypedString::fromString('Abc')->isUppercase())->toBeFalse();
|
||||
expect(TypedString::fromString('')->isUppercase())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates hexadecimal strings', function () {
|
||||
expect(TypedString::fromString('deadbeef')->isHexadecimal())->toBeTrue();
|
||||
expect(TypedString::fromString('DEADBEEF')->isHexadecimal())->toBeTrue();
|
||||
expect(TypedString::fromString('0123456789abcdef')->isHexadecimal())->toBeTrue();
|
||||
expect(TypedString::fromString('ghijk')->isHexadecimal())->toBeFalse();
|
||||
expect(TypedString::fromString('')->isHexadecimal())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates whitespace strings', function () {
|
||||
expect(TypedString::fromString(' ')->isWhitespace())->toBeTrue();
|
||||
expect(TypedString::fromString("\t\n")->isWhitespace())->toBeTrue();
|
||||
expect(TypedString::fromString('abc')->isWhitespace())->toBeFalse();
|
||||
expect(TypedString::fromString('')->isWhitespace())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates printable strings', function () {
|
||||
expect(TypedString::fromString('Hello World')->isPrintable())->toBeTrue();
|
||||
expect(TypedString::fromString("Hello\x00World")->isPrintable())->toBeFalse();
|
||||
expect(TypedString::fromString('')->isPrintable())->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates visible strings', function () {
|
||||
expect(TypedString::fromString('abc123')->isVisible())->toBeTrue();
|
||||
expect(TypedString::fromString('abc 123')->isVisible())->toBeFalse();
|
||||
expect(TypedString::fromString('')->isVisible())->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TypedString - Length Checks', function () {
|
||||
it('checks if string is empty', function () {
|
||||
expect(TypedString::fromString('')->isEmpty())->toBeTrue();
|
||||
expect(TypedString::fromString('abc')->isEmpty())->toBeFalse();
|
||||
});
|
||||
|
||||
it('checks if string is not empty', function () {
|
||||
expect(TypedString::fromString('abc')->isNotEmpty())->toBeTrue();
|
||||
expect(TypedString::fromString('')->isNotEmpty())->toBeFalse();
|
||||
});
|
||||
|
||||
it('returns string length', function () {
|
||||
expect(TypedString::fromString('abc')->length())->toBe(3);
|
||||
expect(TypedString::fromString('')->length())->toBe(0);
|
||||
expect(TypedString::fromString('hello world')->length())->toBe(11);
|
||||
});
|
||||
|
||||
it('checks minimum length', function () {
|
||||
$str = TypedString::fromString('hello');
|
||||
|
||||
expect($str->hasMinLength(3))->toBeTrue();
|
||||
expect($str->hasMinLength(5))->toBeTrue();
|
||||
expect($str->hasMinLength(6))->toBeFalse();
|
||||
});
|
||||
|
||||
it('checks maximum length', function () {
|
||||
$str = TypedString::fromString('hello');
|
||||
|
||||
expect($str->hasMaxLength(10))->toBeTrue();
|
||||
expect($str->hasMaxLength(5))->toBeTrue();
|
||||
expect($str->hasMaxLength(4))->toBeFalse();
|
||||
});
|
||||
|
||||
it('checks length range', function () {
|
||||
$str = TypedString::fromString('hello');
|
||||
|
||||
expect($str->hasLengthBetween(3, 10))->toBeTrue();
|
||||
expect($str->hasLengthBetween(5, 5))->toBeTrue();
|
||||
expect($str->hasLengthBetween(6, 10))->toBeFalse();
|
||||
expect($str->hasLengthBetween(1, 4))->toBeFalse();
|
||||
});
|
||||
|
||||
it('checks exact length', function () {
|
||||
$str = TypedString::fromString('hello');
|
||||
|
||||
expect($str->hasExactLength(5))->toBeTrue();
|
||||
expect($str->hasExactLength(4))->toBeFalse();
|
||||
expect($str->hasExactLength(6))->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TypedString - Pattern Matching', function () {
|
||||
it('matches regular expressions', function () {
|
||||
$str = TypedString::fromString('hello123');
|
||||
|
||||
expect($str->matches('/^hello/'))->toBeTrue();
|
||||
expect($str->matches('/\d+$/'))->toBeTrue();
|
||||
expect($str->matches('/^goodbye/'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('checks if starts with prefix', function () {
|
||||
$str = TypedString::fromString('hello world');
|
||||
|
||||
expect($str->startsWith('hello'))->toBeTrue();
|
||||
expect($str->startsWith('h'))->toBeTrue();
|
||||
expect($str->startsWith('world'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('checks if ends with suffix', function () {
|
||||
$str = TypedString::fromString('hello world');
|
||||
|
||||
expect($str->endsWith('world'))->toBeTrue();
|
||||
expect($str->endsWith('d'))->toBeTrue();
|
||||
expect($str->endsWith('hello'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('checks if contains substring', function () {
|
||||
$str = TypedString::fromString('hello world');
|
||||
|
||||
expect($str->contains('hello'))->toBeTrue();
|
||||
expect($str->contains('world'))->toBeTrue();
|
||||
expect($str->contains('lo wo'))->toBeTrue();
|
||||
expect($str->contains('goodbye'))->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TypedString - Comparison & Conversion', function () {
|
||||
it('checks equality', function () {
|
||||
$str1 = TypedString::fromString('hello');
|
||||
$str2 = TypedString::fromString('hello');
|
||||
$str3 = TypedString::fromString('world');
|
||||
|
||||
expect($str1->equals($str2))->toBeTrue();
|
||||
expect($str1->equals($str3))->toBeFalse();
|
||||
});
|
||||
|
||||
it('converts to string', function () {
|
||||
$str = TypedString::fromString('hello');
|
||||
|
||||
expect($str->toString())->toBe('hello');
|
||||
expect((string) $str)->toBe('hello');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,440 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\String\ValueObjects\TypedString;
|
||||
|
||||
describe('TypedStringValidator - Fluent API', function () {
|
||||
it('validates alphanumeric with fluent interface', function () {
|
||||
$result = TypedString::fromString('abc123')
|
||||
->validate()
|
||||
->alphanumeric()
|
||||
->orNull();
|
||||
|
||||
expect($result)->not->toBeNull();
|
||||
expect($result->value)->toBe('abc123');
|
||||
});
|
||||
|
||||
it('rejects non-alphanumeric strings', function () {
|
||||
$result = TypedString::fromString('abc-123')
|
||||
->validate()
|
||||
->alphanumeric()
|
||||
->orNull();
|
||||
|
||||
expect($result)->toBeNull();
|
||||
});
|
||||
|
||||
it('validates length constraints', function () {
|
||||
$result = TypedString::fromString('hello')
|
||||
->validate()
|
||||
->minLength(3)
|
||||
->maxLength(10)
|
||||
->orNull();
|
||||
|
||||
expect($result)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('rejects strings below minimum length', function () {
|
||||
$result = TypedString::fromString('ab')
|
||||
->validate()
|
||||
->minLength(5)
|
||||
->orNull();
|
||||
|
||||
expect($result)->toBeNull();
|
||||
});
|
||||
|
||||
it('rejects strings above maximum length', function () {
|
||||
$result = TypedString::fromString('hello world')
|
||||
->validate()
|
||||
->maxLength(5)
|
||||
->orNull();
|
||||
|
||||
expect($result)->toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TypedStringValidator - Complex Chains', function () {
|
||||
it('validates complex username format', function () {
|
||||
$username = TypedString::fromString('user123')
|
||||
->validate()
|
||||
->alphanumeric()
|
||||
->minLength(3)
|
||||
->maxLength(20)
|
||||
->orThrow('Invalid username');
|
||||
|
||||
expect($username->value)->toBe('user123');
|
||||
});
|
||||
|
||||
it('validates password with custom rules', function () {
|
||||
$password = TypedString::fromString('Pass123!')
|
||||
->validate()
|
||||
->minLength(8)
|
||||
->custom(
|
||||
fn($ts) => preg_match('/[A-Z]/', $ts->value) === 1,
|
||||
'Must contain uppercase letter'
|
||||
)
|
||||
->custom(
|
||||
fn($ts) => preg_match('/[0-9]/', $ts->value) === 1,
|
||||
'Must contain digit'
|
||||
)
|
||||
->orThrow();
|
||||
|
||||
expect($password->value)->toBe('Pass123!');
|
||||
});
|
||||
|
||||
it('throws on invalid password', function () {
|
||||
TypedString::fromString('weak')
|
||||
->validate()
|
||||
->minLength(8)
|
||||
->orThrow('Password too weak');
|
||||
})->throws(InvalidArgumentException::class, 'Password too weak');
|
||||
});
|
||||
|
||||
describe('TypedStringValidator - Error Handling', function () {
|
||||
it('collects multiple validation errors', function () {
|
||||
$validator = TypedString::fromString('ab')
|
||||
->validate()
|
||||
->alphanumeric()
|
||||
->minLength(5);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
|
||||
$errors = $validator->getErrors();
|
||||
expect($errors)->toHaveCount(1);
|
||||
expect($errors[0])->toContain('at least 5 characters');
|
||||
});
|
||||
|
||||
it('returns first error message', function () {
|
||||
$validator = TypedString::fromString('ab')
|
||||
->validate()
|
||||
->minLength(5)
|
||||
->maxLength(3);
|
||||
|
||||
$firstError = $validator->getFirstError();
|
||||
expect($firstError)->toContain('at least 5 characters');
|
||||
});
|
||||
|
||||
it('returns custom error messages', function () {
|
||||
$validator = TypedString::fromString('abc-123')
|
||||
->validate()
|
||||
->alphanumeric('Username must be alphanumeric');
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->getFirstError())->toBe('Username must be alphanumeric');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TypedStringValidator - Pattern Validators', function () {
|
||||
it('validates regex patterns', function () {
|
||||
$email = TypedString::fromString('test@example.com')
|
||||
->validate()
|
||||
->matches('/^[\w\.\-]+@[\w\.\-]+\.\w+$/', 'Invalid email format')
|
||||
->orThrow();
|
||||
|
||||
expect($email->value)->toBe('test@example.com');
|
||||
});
|
||||
|
||||
it('validates start with prefix', function () {
|
||||
$result = TypedString::fromString('user_123')
|
||||
->validate()
|
||||
->startsWith('user_')
|
||||
->orNull();
|
||||
|
||||
expect($result)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('validates end with suffix', function () {
|
||||
$result = TypedString::fromString('file.txt')
|
||||
->validate()
|
||||
->endsWith('.txt')
|
||||
->orNull();
|
||||
|
||||
expect($result)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('validates contains substring', function () {
|
||||
$result = TypedString::fromString('hello_world')
|
||||
->validate()
|
||||
->contains('_')
|
||||
->orNull();
|
||||
|
||||
expect($result)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('validates does not contain substring', function () {
|
||||
$result = TypedString::fromString('helloworld')
|
||||
->validate()
|
||||
->doesNotContain(' ')
|
||||
->orNull();
|
||||
|
||||
expect($result)->not->toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TypedStringValidator - Termination Methods', function () {
|
||||
it('throws exception on validation failure', function () {
|
||||
TypedString::fromString('invalid')
|
||||
->validate()
|
||||
->digits()
|
||||
->orThrow('Must be numeric');
|
||||
})->throws(InvalidArgumentException::class, 'Must be numeric');
|
||||
|
||||
it('returns null on validation failure', function () {
|
||||
$result = TypedString::fromString('invalid')
|
||||
->validate()
|
||||
->digits()
|
||||
->orNull();
|
||||
|
||||
expect($result)->toBeNull();
|
||||
});
|
||||
|
||||
it('returns default value on validation failure', function () {
|
||||
$result = TypedString::fromString('invalid')
|
||||
->validate()
|
||||
->digits()
|
||||
->orDefault('123');
|
||||
|
||||
expect($result->value)->toBe('123');
|
||||
});
|
||||
|
||||
it('passes validation and returns original value', function () {
|
||||
$result = TypedString::fromString('valid123')
|
||||
->validate()
|
||||
->alphanumeric()
|
||||
->orDefault('fallback');
|
||||
|
||||
expect($result->value)->toBe('valid123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TypedStringValidator - POSIX Validators', function () {
|
||||
describe('posixAlnum', function () {
|
||||
it('validates alphanumeric POSIX strings', function () {
|
||||
$result = TypedString::fromString('abc123XYZ')
|
||||
->validate()
|
||||
->posixAlnum()
|
||||
->orThrow();
|
||||
|
||||
expect($result->value)->toBe('abc123XYZ');
|
||||
});
|
||||
|
||||
it('rejects strings with non-alphanumeric characters', function () {
|
||||
TypedString::fromString('abc-123')
|
||||
->validate()
|
||||
->posixAlnum()
|
||||
->orThrow();
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
|
||||
it('works in validation chain', function () {
|
||||
$result = TypedString::fromString('test123')
|
||||
->validate()
|
||||
->notEmpty()
|
||||
->posixAlnum()
|
||||
->minLength(5)
|
||||
->orThrow();
|
||||
|
||||
expect($result->value)->toBe('test123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('posixAlpha', function () {
|
||||
it('validates alphabetic POSIX strings', function () {
|
||||
$result = TypedString::fromString('abcXYZ')
|
||||
->validate()
|
||||
->posixAlpha()
|
||||
->orThrow();
|
||||
|
||||
expect($result->value)->toBe('abcXYZ');
|
||||
});
|
||||
|
||||
it('rejects strings with digits', function () {
|
||||
TypedString::fromString('abc123')
|
||||
->validate()
|
||||
->posixAlpha()
|
||||
->orThrow();
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
|
||||
it('rejects strings with special characters', function () {
|
||||
TypedString::fromString('hello-world')
|
||||
->validate()
|
||||
->posixAlpha()
|
||||
->orThrow();
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
});
|
||||
|
||||
describe('posixPunct', function () {
|
||||
it('validates punctuation POSIX strings', function () {
|
||||
$result = TypedString::fromString('!@#$%^&*()')
|
||||
->validate()
|
||||
->posixPunct()
|
||||
->orThrow();
|
||||
|
||||
expect($result->value)->toBe('!@#$%^&*()');
|
||||
});
|
||||
|
||||
it('rejects strings with alphanumeric characters', function () {
|
||||
TypedString::fromString('abc!@#')
|
||||
->validate()
|
||||
->posixPunct()
|
||||
->orThrow();
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
});
|
||||
|
||||
describe('posixWord', function () {
|
||||
it('validates word POSIX strings (alphanumeric + underscore)', function () {
|
||||
$result = TypedString::fromString('user_name_123')
|
||||
->validate()
|
||||
->posixWord()
|
||||
->orThrow();
|
||||
|
||||
expect($result->value)->toBe('user_name_123');
|
||||
});
|
||||
|
||||
it('rejects strings with hyphens', function () {
|
||||
TypedString::fromString('user-name')
|
||||
->validate()
|
||||
->posixWord()
|
||||
->orThrow();
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
|
||||
it('validates PHP identifiers', function () {
|
||||
$result = TypedString::fromString('_privateVar')
|
||||
->validate()
|
||||
->posixWord()
|
||||
->orThrow();
|
||||
|
||||
expect($result->value)->toBe('_privateVar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('posixLower', function () {
|
||||
it('validates lowercase POSIX strings', function () {
|
||||
$result = TypedString::fromString('hello')
|
||||
->validate()
|
||||
->posixLower()
|
||||
->orThrow();
|
||||
|
||||
expect($result->value)->toBe('hello');
|
||||
});
|
||||
|
||||
it('rejects strings with uppercase characters', function () {
|
||||
TypedString::fromString('Hello')
|
||||
->validate()
|
||||
->posixLower()
|
||||
->orThrow();
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
|
||||
it('rejects strings with digits', function () {
|
||||
TypedString::fromString('hello123')
|
||||
->validate()
|
||||
->posixLower()
|
||||
->orThrow();
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
});
|
||||
|
||||
describe('posixUpper', function () {
|
||||
it('validates uppercase POSIX strings', function () {
|
||||
$result = TypedString::fromString('HELLO')
|
||||
->validate()
|
||||
->posixUpper()
|
||||
->orThrow();
|
||||
|
||||
expect($result->value)->toBe('HELLO');
|
||||
});
|
||||
|
||||
it('rejects strings with lowercase characters', function () {
|
||||
TypedString::fromString('Hello')
|
||||
->validate()
|
||||
->posixUpper()
|
||||
->orThrow();
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
|
||||
it('validates constant naming', function () {
|
||||
$result = TypedString::fromString('MAX_VALUE')
|
||||
->validate()
|
||||
->posixUpper()
|
||||
->orThrow();
|
||||
|
||||
expect($result->value)->toBe('MAX_VALUE');
|
||||
});
|
||||
});
|
||||
|
||||
describe('posixPrint', function () {
|
||||
it('validates printable POSIX strings (includes space)', function () {
|
||||
$result = TypedString::fromString('Hello World!')
|
||||
->validate()
|
||||
->posixPrint()
|
||||
->orThrow();
|
||||
|
||||
expect($result->value)->toBe('Hello World!');
|
||||
});
|
||||
|
||||
it('rejects strings with control characters', function () {
|
||||
TypedString::fromString("Hello\x00World")
|
||||
->validate()
|
||||
->posixPrint()
|
||||
->orThrow();
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
});
|
||||
|
||||
describe('posixGraph', function () {
|
||||
it('validates visible POSIX strings (excludes space)', function () {
|
||||
$result = TypedString::fromString('Hello!')
|
||||
->validate()
|
||||
->posixGraph()
|
||||
->orThrow();
|
||||
|
||||
expect($result->value)->toBe('Hello!');
|
||||
});
|
||||
|
||||
it('rejects strings with spaces', function () {
|
||||
TypedString::fromString('Hello World')
|
||||
->validate()
|
||||
->posixGraph()
|
||||
->orThrow();
|
||||
})->throws(\InvalidArgumentException::class);
|
||||
|
||||
it('validates URLs without spaces', function () {
|
||||
$result = TypedString::fromString('https://example.com')
|
||||
->validate()
|
||||
->posixGraph()
|
||||
->orThrow();
|
||||
|
||||
expect($result->value)->toBe('https://example.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complex POSIX Validation Chains', function () {
|
||||
it('combines POSIX validators with length checks', function () {
|
||||
$result = TypedString::fromString('user123')
|
||||
->validate()
|
||||
->posixAlnum()
|
||||
->minLength(5)
|
||||
->maxLength(20)
|
||||
->orThrow();
|
||||
|
||||
expect($result->value)->toBe('user123');
|
||||
});
|
||||
|
||||
it('combines POSIX with pattern matching', function () {
|
||||
$result = TypedString::fromString('test_value')
|
||||
->validate()
|
||||
->posixWord()
|
||||
->matches('/^[a-z_]+$/')
|
||||
->orThrow();
|
||||
|
||||
expect($result->value)->toBe('test_value');
|
||||
});
|
||||
|
||||
it('uses POSIX validators in complex validation scenarios', function () {
|
||||
$result = TypedString::fromString('AdminUser')
|
||||
->validate()
|
||||
->notEmpty()
|
||||
->posixAlpha()
|
||||
->minLength(5)
|
||||
->orDefault('Guest');
|
||||
|
||||
expect($result->value)->toBe('AdminUser');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user