Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
176
tests/Framework/Core/ApplicationTest.php
Normal file
176
tests/Framework/Core/ApplicationTest.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Config\AppConfig;
|
||||
use App\Framework\Config\DiscoveryConfig;
|
||||
use App\Framework\Config\External\ExternalApiConfig;
|
||||
use App\Framework\Config\SecurityConfig;
|
||||
use App\Framework\Config\TypedConfiguration;
|
||||
use App\Framework\Core\Application;
|
||||
use App\Framework\Core\Events\EventDispatcherInterface;
|
||||
use App\Framework\Database\Config\DatabaseConfig;
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\DI\DefaultContainer;
|
||||
use App\Framework\Http\HttpMiddlewareChain;
|
||||
use App\Framework\Http\HttpMiddlewareChainInterface;
|
||||
use App\Framework\Http\HttpRequest;
|
||||
use App\Framework\Http\HttpResponse;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Http\MiddlewareContext;
|
||||
use App\Framework\Http\MiddlewareManagerInterface;
|
||||
use App\Framework\Http\Request;
|
||||
use App\Framework\Http\RequestStateManager;
|
||||
use App\Framework\Http\ResponseEmitter;
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Logging\DefaultLogger;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\RateLimit\RateLimitConfig;
|
||||
use App\Framework\Router\HttpRouter;
|
||||
|
||||
// Simple test doubles
|
||||
class TestEventDispatcher implements EventDispatcherInterface
|
||||
{
|
||||
public array $dispatched = [];
|
||||
|
||||
public function dispatch(object $event): array
|
||||
{
|
||||
$this->dispatched[] = $event;
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
class TestMiddleware
|
||||
{
|
||||
public function __invoke(MiddlewareContext $context, HttpMiddlewareChainInterface $next, RequestStateManager $stateManager): MiddlewareContext
|
||||
{
|
||||
$response = new HttpResponse(Status::OK, [], 'Test Response');
|
||||
|
||||
return new MiddlewareContext($context->request, $response);
|
||||
}
|
||||
}
|
||||
|
||||
class TestMiddlewareManager implements MiddlewareManagerInterface
|
||||
{
|
||||
public HttpMiddlewareChain $chain;
|
||||
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
// Register the test middleware class in container
|
||||
$container->bind(TestMiddleware::class, new TestMiddleware());
|
||||
|
||||
// Create the real chain with minimal middlewares
|
||||
$this->chain = new HttpMiddlewareChain(
|
||||
[TestMiddleware::class],
|
||||
$container
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
$this->container = new DefaultContainer();
|
||||
|
||||
// Create a minimal test database config
|
||||
$driverConfig = new \App\Framework\Database\Driver\DriverConfig(
|
||||
driverType: \App\Framework\Database\Driver\DriverType::SQLITE,
|
||||
host: 'localhost',
|
||||
port: 0,
|
||||
database: ':memory:',
|
||||
username: '',
|
||||
password: '',
|
||||
charset: 'utf8mb4'
|
||||
);
|
||||
$poolConfig = new \App\Framework\Database\Config\PoolConfig(
|
||||
enabled: false,
|
||||
maxConnections: 10,
|
||||
minConnections: 1
|
||||
);
|
||||
$readWriteConfig = new \App\Framework\Database\Config\ReadWriteConfig(
|
||||
enabled: false
|
||||
);
|
||||
$databaseConfig = new DatabaseConfig($driverConfig, $poolConfig, $readWriteConfig);
|
||||
|
||||
$this->config = new TypedConfiguration(
|
||||
database: $databaseConfig,
|
||||
app: new AppConfig(
|
||||
name: 'Test App',
|
||||
version: '1.0.0-test',
|
||||
environment: 'testing',
|
||||
debug: true,
|
||||
timezone: \App\Framework\DateTime\Timezone::UTC
|
||||
),
|
||||
security: new SecurityConfig(
|
||||
appKey: 'test',
|
||||
enableSecurityHeaders: false,
|
||||
enableCsrfProtection: false,
|
||||
enableRateLimiting: false
|
||||
),
|
||||
rateLimit: RateLimitConfig::testing(),
|
||||
externalApis: new ExternalApiConfig(
|
||||
shopify: new \App\Framework\Config\External\ShopifyConfig('', '', '', '', false),
|
||||
rapidMail: new \App\Framework\Config\External\RapidMailConfig('', '', true)
|
||||
),
|
||||
discovery: new DiscoveryConfig()
|
||||
);
|
||||
|
||||
$this->responseEmitter = new ResponseEmitter();
|
||||
|
||||
// Register essential dependencies in container
|
||||
$this->container->bind(Logger::class, new DefaultLogger());
|
||||
$this->container->bind(HttpRouter::class, new class () {});
|
||||
$this->container->bind(\App\Framework\Cache\Cache::class, new \App\Framework\Cache\GeneralCache(new \App\Framework\Cache\Driver\InMemoryCache(), new \App\Framework\Serializer\Php\PhpSerializer()));
|
||||
|
||||
// Register Request for handleRequest
|
||||
$this->container->bind(Request::class, new HttpRequest(
|
||||
method: Method::GET,
|
||||
path: '/test'
|
||||
));
|
||||
|
||||
// Create test doubles
|
||||
$this->middlewareManager = new TestMiddlewareManager($this->container);
|
||||
$this->eventDispatcher = new TestEventDispatcher();
|
||||
|
||||
$this->application = new Application(
|
||||
$this->container,
|
||||
$this->responseEmitter,
|
||||
$this->config,
|
||||
$this->middlewareManager,
|
||||
$this->eventDispatcher
|
||||
);
|
||||
});
|
||||
|
||||
it('creates application with dependencies', function () {
|
||||
expect($this->application)->toBeInstanceOf(Application::class);
|
||||
});
|
||||
|
||||
it('gets config values correctly', function () {
|
||||
expect($this->application->config('environment'))->toBe('testing');
|
||||
expect($this->application->config('app.version'))->toBe('1.0.0-test');
|
||||
expect($this->application->config('nonexistent', 'default'))->toBe('default');
|
||||
expect($this->application->config('nonexistent'))->toBeNull();
|
||||
});
|
||||
|
||||
it('can be instantiated with test doubles', function () {
|
||||
// Test that Application can be created with our test doubles
|
||||
// This verifies the interface extraction works for dependency injection
|
||||
expect($this->application)->toBeInstanceOf(Application::class);
|
||||
expect($this->middlewareManager)->toBeInstanceOf(MiddlewareManagerInterface::class);
|
||||
expect($this->eventDispatcher)->toBeInstanceOf(EventDispatcherInterface::class);
|
||||
});
|
||||
|
||||
it('verifies interface extraction allows dependency injection with test doubles', function () {
|
||||
// This test verifies that our interface extraction allows the Application
|
||||
// to be tested without requiring the full container setup.
|
||||
// The fact that we can instantiate it with our simple test doubles
|
||||
// proves that the refactoring achieved its goal.
|
||||
|
||||
expect($this->application)->toBeInstanceOf(Application::class);
|
||||
|
||||
// Verify our test doubles implement the interfaces
|
||||
expect($this->middlewareManager)->toBeInstanceOf(MiddlewareManagerInterface::class);
|
||||
expect($this->eventDispatcher)->toBeInstanceOf(EventDispatcherInterface::class);
|
||||
|
||||
// Verify the Application is using our test doubles (not container-resolved instances)
|
||||
expect($this->application->config('environment'))->toBe('testing');
|
||||
});
|
||||
334
tests/Framework/Core/DynamicRoutingTest.php
Normal file
334
tests/Framework/Core/DynamicRoutingTest.php
Normal file
@@ -0,0 +1,334 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Core\DynamicRoute;
|
||||
use App\Framework\Core\RouteCompiler;
|
||||
use App\Framework\Discovery\Visitors\UnifiedRouteVisitor;
|
||||
use App\Framework\Router\Result\FileResult;
|
||||
use App\Framework\Router\Result\JsonResult;
|
||||
|
||||
// Test Controller Klassen für Dynamic Routing Tests
|
||||
class TestDynamicController
|
||||
{
|
||||
#[Route('/test/{id}')]
|
||||
public function showById(int $id): JsonResult
|
||||
{
|
||||
return new JsonResult(['id' => $id]);
|
||||
}
|
||||
|
||||
#[Route('/images/{filename}')]
|
||||
public function showImage(string $filename): FileResult
|
||||
{
|
||||
return new FileResult("/path/to/{$filename}");
|
||||
}
|
||||
|
||||
#[Route('/user/{userId}/post/{postId}')]
|
||||
public function showUserPost(int $userId, string $postId): JsonResult
|
||||
{
|
||||
return new JsonResult(['userId' => $userId, 'postId' => $postId]);
|
||||
}
|
||||
|
||||
#[Route('/api/search/{query?}')]
|
||||
public function search(?string $query = null): JsonResult
|
||||
{
|
||||
return new JsonResult(['query' => $query]);
|
||||
}
|
||||
}
|
||||
|
||||
describe('Dynamic Routing Parameter Extraction', function () {
|
||||
beforeEach(function () {
|
||||
$this->discoveryVisitor = new UnifiedRouteVisitor();
|
||||
$this->routeCompiler = new RouteCompiler();
|
||||
});
|
||||
|
||||
test('discovers routes with dynamic parameters', function () {
|
||||
$this->discoveryVisitor->onScanStart();
|
||||
// UnifiedRouteVisitor benötigt ClassName und FilePath Value Objects
|
||||
$className = \App\Framework\Core\ValueObjects\ClassName::fromString(TestDynamicController::class);
|
||||
$filePath = \App\Framework\Filesystem\FilePath::create('test-file.php');
|
||||
$reflection = new \App\Framework\Reflection\WrappedReflectionClass(new \ReflectionClass(TestDynamicController::class));
|
||||
|
||||
$this->discoveryVisitor->visitClass($className, $filePath, $reflection);
|
||||
|
||||
$routes = $this->discoveryVisitor->getResults();
|
||||
|
||||
expect($routes)->toHaveCount(4);
|
||||
|
||||
// Test route with single parameter
|
||||
$singleParamRoute = null;
|
||||
foreach ($routes as $route) {
|
||||
if ($route['path'] === '/test/{id}') {
|
||||
$singleParamRoute = $route;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect($singleParamRoute)->not->toBeNull();
|
||||
expect($singleParamRoute['controller'])->toBe(TestDynamicController::class);
|
||||
expect($singleParamRoute['action'])->toBe('showById');
|
||||
|
||||
// Test image route (the problematic one)
|
||||
$imageRoute = null;
|
||||
foreach ($routes as $route) {
|
||||
if ($route['path'] === '/images/{filename}') {
|
||||
$imageRoute = $route;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect($imageRoute)->not->toBeNull();
|
||||
expect($imageRoute['controller'])->toBe(TestDynamicController::class);
|
||||
expect($imageRoute['action'])->toBe('showImage');
|
||||
|
||||
// Test route with multiple parameters
|
||||
$multiParamRoute = null;
|
||||
foreach ($routes as $route) {
|
||||
if ($route['path'] === '/user/{userId}/post/{postId}') {
|
||||
$multiParamRoute = $route;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect($multiParamRoute)->not->toBeNull();
|
||||
});
|
||||
|
||||
test('extracts parameter information correctly', function () {
|
||||
$this->discoveryVisitor->onScanStart();
|
||||
// UnifiedRouteVisitor benötigt ClassName und FilePath Value Objects
|
||||
$className = \App\Framework\Core\ValueObjects\ClassName::fromString(TestDynamicController::class);
|
||||
$filePath = \App\Framework\Filesystem\FilePath::create('test-file.php');
|
||||
$reflection = new \App\Framework\Reflection\WrappedReflectionClass(new \ReflectionClass(TestDynamicController::class));
|
||||
|
||||
$this->discoveryVisitor->visitClass($className, $filePath, $reflection);
|
||||
|
||||
$routes = $this->discoveryVisitor->getResults();
|
||||
|
||||
// Suche die Image Route
|
||||
$imageRoute = null;
|
||||
foreach ($routes as $route) {
|
||||
if ($route['path'] === '/images/{filename}') {
|
||||
$imageRoute = $route;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfe ob Parameter-Informationen vorhanden sind
|
||||
if (isset($imageRoute['parameters'])) {
|
||||
expect($imageRoute['parameters'])->toBeArray();
|
||||
|
||||
// Debug: Zeige Parameter-Struktur
|
||||
error_log('Image Route Parameters: ' . json_encode($imageRoute['parameters']));
|
||||
|
||||
// Suche nach filename Parameter
|
||||
$filenameParam = null;
|
||||
foreach ($imageRoute['parameters'] as $param) {
|
||||
if ($param['name'] === 'filename') {
|
||||
$filenameParam = $param;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($filenameParam) {
|
||||
expect($filenameParam['name'])->toBe('filename');
|
||||
expect($filenameParam['type'])->toBe('string'); // Das ist wahrscheinlich null!
|
||||
expect($filenameParam['isBuiltin'])->toBeTrue();
|
||||
} else {
|
||||
error_log('Filename parameter not found in parameters array');
|
||||
}
|
||||
} else {
|
||||
error_log('No parameters found in route array');
|
||||
expect($imageRoute)->toHaveKey('parameters'); // Dieser Test wird wahrscheinlich fehlschlagen
|
||||
}
|
||||
});
|
||||
|
||||
test('compiles dynamic routes with parameter names', function () {
|
||||
$this->discoveryVisitor->onScanStart();
|
||||
// UnifiedRouteVisitor benötigt ClassName und FilePath Value Objects
|
||||
$className = \App\Framework\Core\ValueObjects\ClassName::fromString(TestDynamicController::class);
|
||||
$filePath = \App\Framework\Filesystem\FilePath::create('test-file.php');
|
||||
$reflection = new \App\Framework\Reflection\WrappedReflectionClass(new \ReflectionClass(TestDynamicController::class));
|
||||
|
||||
$this->discoveryVisitor->visitClass($className, $filePath, $reflection);
|
||||
|
||||
$routes = $this->discoveryVisitor->getResults();
|
||||
$compiledRoutes = $this->routeCompiler->compile($routes);
|
||||
|
||||
// Prüfe GET routes
|
||||
expect($compiledRoutes)->toHaveKey('GET');
|
||||
expect($compiledRoutes['GET'])->toHaveKey('dynamic');
|
||||
|
||||
$dynamicRoutes = $compiledRoutes['GET']['dynamic'];
|
||||
expect($dynamicRoutes)->toBeArray();
|
||||
expect(count($dynamicRoutes))->toBeGreaterThan(0);
|
||||
|
||||
// Suche Image Route in compilierten Routes
|
||||
$imageRoute = null;
|
||||
foreach ($dynamicRoutes as $route) {
|
||||
if ($route instanceof DynamicRoute && $route->path === '/images/{filename}') {
|
||||
$imageRoute = $route;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect($imageRoute)->not->toBeNull();
|
||||
expect($imageRoute)->toBeInstanceOf(DynamicRoute::class);
|
||||
|
||||
// Prüfe Parameter Names
|
||||
expect($imageRoute->paramNames)->toBe(['filename']);
|
||||
|
||||
// Prüfe compiled regex
|
||||
expect($imageRoute->regex)->toBeString();
|
||||
expect($imageRoute->regex)->toMatch('/^~.*\/images\/.*\$~/'); // Regex für /images/{filename}
|
||||
|
||||
// Prüfe Parameter Details
|
||||
error_log('Compiled Image Route Parameters: ' . json_encode($imageRoute->parameters));
|
||||
|
||||
if (! empty($imageRoute->parameters)) {
|
||||
$filenameParam = null;
|
||||
foreach ($imageRoute->parameters as $param) {
|
||||
if ($param['name'] === 'filename') {
|
||||
$filenameParam = $param;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($filenameParam) {
|
||||
expect($filenameParam['type'])->not->toBeNull(); // Das sollte nicht null sein!
|
||||
expect($filenameParam['type'])->toBe('string');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('handles multiple parameter types correctly', function () {
|
||||
$this->discoveryVisitor->onScanStart();
|
||||
// UnifiedRouteVisitor benötigt ClassName und FilePath Value Objects
|
||||
$className = \App\Framework\Core\ValueObjects\ClassName::fromString(TestDynamicController::class);
|
||||
$filePath = \App\Framework\Filesystem\FilePath::create('test-file.php');
|
||||
$reflection = new \App\Framework\Reflection\WrappedReflectionClass(new \ReflectionClass(TestDynamicController::class));
|
||||
|
||||
$this->discoveryVisitor->visitClass($className, $filePath, $reflection);
|
||||
|
||||
$routes = $this->discoveryVisitor->getResults();
|
||||
|
||||
// Test verschiedene Parameter-Typen
|
||||
$userPostRoute = null;
|
||||
foreach ($routes as $route) {
|
||||
if ($route['path'] === '/user/{userId}/post/{postId}') {
|
||||
$userPostRoute = $route;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($userPostRoute['parameters'])) {
|
||||
$parameters = $userPostRoute['parameters'];
|
||||
|
||||
// Suche nach userId (int) und postId (string) Parametern
|
||||
$userIdParam = null;
|
||||
$postIdParam = null;
|
||||
foreach ($parameters as $param) {
|
||||
if ($param['name'] === 'userId') {
|
||||
$userIdParam = $param;
|
||||
} elseif ($param['name'] === 'postId') {
|
||||
$postIdParam = $param;
|
||||
}
|
||||
}
|
||||
|
||||
if ($userIdParam) {
|
||||
expect($userIdParam['type'])->toBe('int');
|
||||
expect($userIdParam['isBuiltin'])->toBeTrue();
|
||||
}
|
||||
|
||||
if ($postIdParam) {
|
||||
expect($postIdParam['type'])->toBe('string');
|
||||
expect($postIdParam['isBuiltin'])->toBeTrue();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('handles optional parameters correctly', function () {
|
||||
$this->discoveryVisitor->onScanStart();
|
||||
// UnifiedRouteVisitor benötigt ClassName und FilePath Value Objects
|
||||
$className = \App\Framework\Core\ValueObjects\ClassName::fromString(TestDynamicController::class);
|
||||
$filePath = \App\Framework\Filesystem\FilePath::create('test-file.php');
|
||||
$reflection = new \App\Framework\Reflection\WrappedReflectionClass(new \ReflectionClass(TestDynamicController::class));
|
||||
|
||||
$this->discoveryVisitor->visitClass($className, $filePath, $reflection);
|
||||
|
||||
$routes = $this->discoveryVisitor->getResults();
|
||||
|
||||
// Test optionale Parameter
|
||||
$searchRoute = null;
|
||||
foreach ($routes as $route) {
|
||||
if ($route['path'] === '/api/search/{query?}') {
|
||||
$searchRoute = $route;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($searchRoute['parameters'])) {
|
||||
$parameters = $searchRoute['parameters'];
|
||||
|
||||
$queryParam = null;
|
||||
foreach ($parameters as $param) {
|
||||
if ($param['name'] === 'query') {
|
||||
$queryParam = $param;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($queryParam) {
|
||||
expect($queryParam['type'])->toBe('string');
|
||||
expect($queryParam['isOptional'])->toBeTrue();
|
||||
expect($queryParam['hasDefault'])->toBeTrue();
|
||||
expect($queryParam['default'])->toBeNull();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dynamic Route Parameter Processing Debug', function () {
|
||||
test('shows current parameter extraction workflow', function () {
|
||||
$discoveryVisitor = new UnifiedRouteVisitor();
|
||||
$discoveryVisitor->onScanStart();
|
||||
$className = \App\Framework\Core\ValueObjects\ClassName::fromString(TestDynamicController::class);
|
||||
$filePath = \App\Framework\Filesystem\FilePath::create('test-file.php');
|
||||
$reflection = new \App\Framework\Reflection\WrappedReflectionClass(new \ReflectionClass(TestDynamicController::class));
|
||||
|
||||
$discoveryVisitor->visitClass($className, $filePath, $reflection);
|
||||
|
||||
$routes = $discoveryVisitor->getResults();
|
||||
|
||||
// Debug: Zeige alle gefundenen Routes
|
||||
error_log('=== All discovered routes ===');
|
||||
foreach ($routes as $i => $route) {
|
||||
error_log("Route {$i}: " . json_encode([
|
||||
'path' => $route['path'],
|
||||
'controller' => $route['controller'],
|
||||
'action' => $route['action'],
|
||||
'has_parameters' => isset($route['parameters']),
|
||||
'parameter_count' => isset($route['parameters']) ? count($route['parameters']) : 0,
|
||||
]));
|
||||
|
||||
if (isset($route['parameters'])) {
|
||||
foreach ($route['parameters'] as $j => $param) {
|
||||
error_log(" Parameter {$j}: " . json_encode($param));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: Zeige RouteDiscoveryVisitor interne Struktur
|
||||
error_log('=== RouteDiscoveryVisitor internals ===');
|
||||
// Wir können nicht auf private Properties zugreifen, aber wir können testen
|
||||
|
||||
expect(true)->toBeTrue(); // Placeholder - der Test ist für Debug-Zwecke
|
||||
});
|
||||
});
|
||||
166
tests/Framework/Core/Events/EventDispatcherTest.php
Normal file
166
tests/Framework/Core/Events/EventDispatcherTest.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Core\Events\EventDispatcher;
|
||||
use App\Framework\Core\Events\OnEvent;
|
||||
use App\Framework\DI\DefaultContainer;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->container = new DefaultContainer();
|
||||
$this->dispatcher = new EventDispatcher($this->container);
|
||||
});
|
||||
|
||||
test('dispatch with no handlers returns empty array', function () {
|
||||
$event = new TestEvent('test');
|
||||
$results = $this->dispatcher->dispatch($event);
|
||||
|
||||
expect($results)->toBeEmpty();
|
||||
});
|
||||
|
||||
test('null event handlers do not cause errors', function () {
|
||||
$dispatcher = new EventDispatcher($this->container, null);
|
||||
$event = new TestEvent('test');
|
||||
$results = $dispatcher->dispatch($event);
|
||||
|
||||
expect($results)->toBeEmpty();
|
||||
});
|
||||
|
||||
test('manual handler registration works', function () {
|
||||
$event = new TestEvent('test message');
|
||||
|
||||
// Register handler manually using the public method
|
||||
$this->dispatcher->addHandler(TestEvent::class, function ($event) {
|
||||
return 'Manual handler: ' . $event->message;
|
||||
});
|
||||
|
||||
$results = $this->dispatcher->dispatch($event);
|
||||
|
||||
expect($results)->toHaveCount(1)
|
||||
->and($results[0])->toBe('Manual handler: test message');
|
||||
});
|
||||
|
||||
test('multiple manual handlers are called', function () {
|
||||
$event = new TestEvent('test');
|
||||
|
||||
$this->dispatcher->addHandler(TestEvent::class, function ($event) {
|
||||
return 'Handler 1: ' . $event->message;
|
||||
});
|
||||
|
||||
$this->dispatcher->addHandler(TestEvent::class, function ($event) {
|
||||
return 'Handler 2: ' . $event->message;
|
||||
});
|
||||
|
||||
$results = $this->dispatcher->dispatch($event);
|
||||
|
||||
expect($results)->toHaveCount(2)
|
||||
->and($results[0])->toBe('Handler 1: test')
|
||||
->and($results[1])->toBe('Handler 2: test');
|
||||
});
|
||||
|
||||
test('event inheritance works with manual handlers', function () {
|
||||
$childEvent = new ChildTestEvent('child message');
|
||||
|
||||
// Register handler for base event
|
||||
$this->dispatcher->addHandler(BaseTestEvent::class, function ($event) {
|
||||
return 'Base handler: ' . $event->message;
|
||||
});
|
||||
|
||||
$results = $this->dispatcher->dispatch($childEvent);
|
||||
|
||||
expect($results)->toHaveCount(1)
|
||||
->and($results[0])->toBe('Base handler: child message');
|
||||
});
|
||||
|
||||
test('class-based handlers work with container', function () {
|
||||
$handler = new TestEventHandler();
|
||||
$this->container->instance(TestEventHandler::class, $handler);
|
||||
|
||||
$eventHandlers = [
|
||||
[
|
||||
'event_class' => TestEvent::class,
|
||||
'class' => TestEventHandler::class,
|
||||
'method' => 'handle',
|
||||
'attribute_data' => ['stopPropagation' => false],
|
||||
],
|
||||
];
|
||||
|
||||
$dispatcher = new EventDispatcher($this->container, $eventHandlers);
|
||||
$event = new TestEvent('test message');
|
||||
$results = $dispatcher->dispatch($event);
|
||||
|
||||
expect($results)->toHaveCount(1)
|
||||
->and($results[0])->toBe('Handled: test message')
|
||||
->and($handler->wasHandled())->toBeTrue();
|
||||
});
|
||||
|
||||
// Test fixtures
|
||||
class TestEvent
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $message
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
class BaseTestEvent
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $message
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
class ChildTestEvent extends BaseTestEvent
|
||||
{
|
||||
}
|
||||
|
||||
class TestEventHandler
|
||||
{
|
||||
private bool $handled = false;
|
||||
|
||||
private int $handleCount = 0;
|
||||
|
||||
#[OnEvent]
|
||||
public function handle(TestEvent $event): string
|
||||
{
|
||||
$this->handled = true;
|
||||
$this->handleCount++;
|
||||
|
||||
return 'Handled: ' . $event->message;
|
||||
}
|
||||
|
||||
#[OnEvent]
|
||||
public function handleBase(BaseTestEvent $event): string
|
||||
{
|
||||
return 'Base handled: ' . $event->message;
|
||||
}
|
||||
|
||||
public function wasHandled(): bool
|
||||
{
|
||||
return $this->handled;
|
||||
}
|
||||
|
||||
public function getHandleCount(): int
|
||||
{
|
||||
return $this->handleCount;
|
||||
}
|
||||
}
|
||||
|
||||
class AnotherTestEventHandler
|
||||
{
|
||||
private bool $processed = false;
|
||||
|
||||
#[OnEvent]
|
||||
public function process(TestEvent $event): string
|
||||
{
|
||||
$this->processed = true;
|
||||
|
||||
return 'Processed: ' . $event->message;
|
||||
}
|
||||
|
||||
public function wasProcessed(): bool
|
||||
{
|
||||
return $this->processed;
|
||||
}
|
||||
}
|
||||
159
tests/Framework/Core/ValueObjects/ClassNameEdgeCasesTest.php
Normal file
159
tests/Framework/Core/ValueObjects/ClassNameEdgeCasesTest.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Core\ValueObjects\ClassName;
|
||||
|
||||
describe('ClassName Edge Cases and Security', function () {
|
||||
|
||||
it('handles potential security issues gracefully', function () {
|
||||
// Test with potentially problematic class names that could cause issues
|
||||
$dangerousCases = [
|
||||
'../../etc/passwd', // Path traversal attempt
|
||||
'<script>alert(1)</script>', // XSS attempt
|
||||
'DROP TABLE users', // SQL injection style
|
||||
"\0null\0byte", // Null byte injection
|
||||
'very.long.dotted.name', // Invalid dots instead of backslashes
|
||||
];
|
||||
|
||||
foreach ($dangerousCases as $dangerous) {
|
||||
expect(function () use ($dangerous) {
|
||||
ClassName::create($dangerous);
|
||||
})->toThrow(InvalidArgumentException::class);
|
||||
}
|
||||
});
|
||||
|
||||
it('handles unicode and special characters', function () {
|
||||
// Some unicode characters are valid in PHP class names (within \x80-\xff range)
|
||||
$validUnicodeCase = 'Ñame'; // Accented characters in \x80-\xff range
|
||||
$className = ClassName::create($validUnicodeCase);
|
||||
expect($className->exists())->toBeFalse(); // Won't exist but should be valid name
|
||||
|
||||
// These should be definitely invalid
|
||||
$definitivelyInvalidCases = [
|
||||
'Name Space', // Space (definitely invalid)
|
||||
'Name-Dash', // Dash (definitely invalid)
|
||||
'Name.Dot', // Dot (definitely invalid)
|
||||
];
|
||||
|
||||
foreach ($definitivelyInvalidCases as $invalid) {
|
||||
expect(function () use ($invalid) {
|
||||
ClassName::create($invalid);
|
||||
})->toThrow(InvalidArgumentException::class);
|
||||
}
|
||||
|
||||
// Test that we can create valid unicode names without errors
|
||||
$potentiallyValidCases = ['Ñame', 'Tëst', 'Clâss'];
|
||||
foreach ($potentiallyValidCases as $case) {
|
||||
$className = ClassName::create($case);
|
||||
expect($className->getFullyQualified())->toBe($case);
|
||||
expect($className->exists())->toBeFalse(); // They won't exist but names should be valid
|
||||
}
|
||||
});
|
||||
|
||||
it('handles very long class names', function () {
|
||||
// Test with extremely long but valid class name
|
||||
$longNamespace = str_repeat('VeryLongNamespace\\', 50);
|
||||
$longClassName = $longNamespace . 'VeryLongClassName';
|
||||
|
||||
$className = ClassName::create($longClassName);
|
||||
expect($className->exists())->toBeFalse(); // Should not exist but should not error
|
||||
expect($className->getFullyQualified())->toBe($longClassName);
|
||||
});
|
||||
|
||||
it('handles constructor edge cases', function () {
|
||||
// Test various whitespace and formatting issues
|
||||
expect(function () {
|
||||
ClassName::create('');
|
||||
})->toThrow(InvalidArgumentException::class, 'Class name cannot be empty');
|
||||
|
||||
expect(function () {
|
||||
ClassName::create(" \t\n ");
|
||||
})->toThrow(InvalidArgumentException::class);
|
||||
|
||||
// Leading backslashes should be handled correctly (but trailing ones are preserved)
|
||||
$className = ClassName::create('\\\\\\App\\Test');
|
||||
expect($className->getFullyQualified())->toBe('App\\Test'); // Leading backslashes removed
|
||||
});
|
||||
|
||||
it('stress test - handles many rapid exists() calls without issues', function () {
|
||||
$startTime = microtime(true);
|
||||
$startMemory = memory_get_usage();
|
||||
|
||||
// Rapid fire test
|
||||
for ($i = 0; $i < 5000; $i++) {
|
||||
$className = ClassName::create('TestClass' . ($i % 100));
|
||||
$className->exists();
|
||||
}
|
||||
|
||||
$endTime = microtime(true);
|
||||
$endMemory = memory_get_usage();
|
||||
|
||||
$duration = $endTime - $startTime;
|
||||
$memoryIncrease = $endMemory - $startMemory;
|
||||
|
||||
// Should complete within reasonable time (less than 1 second)
|
||||
expect($duration)->toBeLessThan(1.0);
|
||||
|
||||
// Should not consume excessive memory (less than 5MB)
|
||||
expect($memoryIncrease)->toBeLessThan(5 * 1024 * 1024);
|
||||
});
|
||||
|
||||
it('validates that exists() is consistent across calls', function () {
|
||||
// Test that multiple calls to exists() on the same instance return consistent results
|
||||
$existingClass = ClassName::create('stdClass');
|
||||
$nonExistingClass = ClassName::create('NonExistentClass123');
|
||||
|
||||
// Multiple calls should return the same result
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
expect($existingClass->exists())->toBeTrue();
|
||||
expect($nonExistingClass->exists())->toBeFalse();
|
||||
}
|
||||
|
||||
// Different instances of the same class name should also be consistent
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$newInstance = ClassName::create('stdClass');
|
||||
expect($newInstance->exists())->toBeTrue();
|
||||
|
||||
$newNonExisting = ClassName::create('NonExistentClass123');
|
||||
expect($newNonExisting->exists())->toBeFalse();
|
||||
}
|
||||
});
|
||||
|
||||
it('handles the exact scenario from the bug report', function () {
|
||||
// This test recreates the exact conditions that caused the original bug:
|
||||
// 1. Container compilation process
|
||||
// 2. Dependency resolution with potentially empty class names
|
||||
// 3. Multiple rapid exists() checks
|
||||
|
||||
$problematicCases = [
|
||||
'App\\Framework\\Filesystem\\AtomicStorage', // The interface that caused issues
|
||||
'App\\Framework\\Filesystem\\Storage', // Parent interface
|
||||
'App\\Framework\\Filesystem\\FileStorage', // Implementation
|
||||
'App\\Framework\\DI\\Container', // Container interface
|
||||
'App\\Framework\\DI\\DefaultContainer', // Container implementation
|
||||
];
|
||||
|
||||
// Simulate rapid container compilation checks
|
||||
foreach ($problematicCases as $case) {
|
||||
$className = ClassName::create($case);
|
||||
$exists = $className->exists();
|
||||
|
||||
// All these should exist and not cause warnings
|
||||
expect($exists)->toBeTrue();
|
||||
expect($className->getFullyQualified())->toBe($case);
|
||||
}
|
||||
|
||||
// Also test with non-existent classes that might be checked during compilation
|
||||
$nonExistentCases = [
|
||||
'App\\NonExistent\\Interface',
|
||||
'App\\Framework\\NonExistent\\Class',
|
||||
'Some\\Random\\ClassName\\That\\DoesNot\\Exist',
|
||||
];
|
||||
|
||||
foreach ($nonExistentCases as $case) {
|
||||
$className = ClassName::create($case);
|
||||
expect($className->exists())->toBeFalse();
|
||||
}
|
||||
});
|
||||
});
|
||||
264
tests/Framework/Core/ValueObjects/ClassNameTest.php
Normal file
264
tests/Framework/Core/ValueObjects/ClassNameTest.php
Normal file
@@ -0,0 +1,264 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Core\ValueObjects\ClassName;
|
||||
|
||||
describe('ClassName::exists()', function () {
|
||||
it('returns false for empty strings', function () {
|
||||
// This should be caught by constructor validation, but let's test the edge case
|
||||
expect(function () {
|
||||
ClassName::create('');
|
||||
})->toThrow(InvalidArgumentException::class, 'Class name cannot be empty');
|
||||
});
|
||||
|
||||
it('returns false for whitespace-only strings', function () {
|
||||
expect(function () {
|
||||
ClassName::create(' ');
|
||||
})->toThrow(InvalidArgumentException::class, 'Invalid class name: ');
|
||||
|
||||
expect(function () {
|
||||
ClassName::create("\t");
|
||||
})->toThrow(InvalidArgumentException::class, 'Invalid class name: ');
|
||||
|
||||
expect(function () {
|
||||
ClassName::create("\n");
|
||||
})->toThrow(InvalidArgumentException::class, 'Invalid class name:
|
||||
');
|
||||
});
|
||||
|
||||
it('returns true for existing classes', function () {
|
||||
$className = ClassName::create('stdClass');
|
||||
expect($className->exists())->toBeTrue();
|
||||
|
||||
$className = ClassName::create('Exception');
|
||||
expect($className->exists())->toBeTrue();
|
||||
|
||||
$className = ClassName::create('DateTime');
|
||||
expect($className->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
it('returns true for existing interfaces', function () {
|
||||
$className = ClassName::create('Countable');
|
||||
expect($className->exists())->toBeTrue();
|
||||
|
||||
$className = ClassName::create('Traversable');
|
||||
expect($className->exists())->toBeTrue();
|
||||
|
||||
$className = ClassName::create('JsonSerializable');
|
||||
expect($className->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
it('returns true for existing traits', function () {
|
||||
// PHP doesn't have many built-in traits, but let's test with a custom one
|
||||
// We'll create a simple trait for testing
|
||||
eval('trait TestTrait { public function testMethod() { return "test"; } }');
|
||||
|
||||
$className = ClassName::create('TestTrait');
|
||||
expect($className->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
it('returns false for non-existent classes', function () {
|
||||
$className = ClassName::create('NonExistentClass');
|
||||
expect($className->exists())->toBeFalse();
|
||||
|
||||
$className = ClassName::create('App\\NonExistent\\Class\\Name');
|
||||
expect($className->exists())->toBeFalse();
|
||||
|
||||
$className = ClassName::create('Some\\Random\\ClassName');
|
||||
expect($className->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('handles fully qualified class names correctly', function () {
|
||||
$className = ClassName::create('\\stdClass');
|
||||
expect($className->exists())->toBeTrue();
|
||||
|
||||
$className = ClassName::create('\\Exception');
|
||||
expect($className->exists())->toBeTrue();
|
||||
|
||||
$className = ClassName::create('\\NonExistentClass');
|
||||
expect($className->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('handles framework classes correctly', function () {
|
||||
$className = ClassName::create('App\\Framework\\Core\\ValueObjects\\ClassName');
|
||||
expect($className->exists())->toBeTrue();
|
||||
|
||||
$className = ClassName::create('App\\Framework\\DI\\Container');
|
||||
expect($className->exists())->toBeTrue();
|
||||
|
||||
$className = ClassName::create('App\\Framework\\NonExistent\\Class');
|
||||
expect($className->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('handles different case variations (PHP classes are case-insensitive)', function () {
|
||||
$className = ClassName::create('stdclass'); // lowercase
|
||||
expect($className->exists())->toBeTrue(); // PHP classes are case-insensitive
|
||||
|
||||
$className = ClassName::create('STDCLASS'); // uppercase
|
||||
expect($className->exists())->toBeTrue(); // PHP classes are case-insensitive
|
||||
|
||||
$className = ClassName::create('stdClass'); // correct case
|
||||
expect($className->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
it('handles invalid class name formats gracefully', function () {
|
||||
// These should be caught by constructor validation
|
||||
expect(function () {
|
||||
ClassName::create('123InvalidName');
|
||||
})->toThrow(InvalidArgumentException::class);
|
||||
|
||||
expect(function () {
|
||||
ClassName::create('Invalid-Name');
|
||||
})->toThrow(InvalidArgumentException::class);
|
||||
|
||||
expect(function () {
|
||||
ClassName::create('Invalid Name');
|
||||
})->toThrow(InvalidArgumentException::class);
|
||||
});
|
||||
|
||||
it('performance test - does not cause memory leaks with many calls', function () {
|
||||
$memoryBefore = memory_get_usage();
|
||||
|
||||
// Test with many exists() calls
|
||||
for ($i = 0; $i < 1000; $i++) {
|
||||
$className = ClassName::create('stdClass');
|
||||
$className->exists();
|
||||
|
||||
$className = ClassName::create('NonExistentClass' . $i);
|
||||
$className->exists();
|
||||
}
|
||||
|
||||
$memoryAfter = memory_get_usage();
|
||||
$memoryIncrease = $memoryAfter - $memoryBefore;
|
||||
|
||||
// Memory increase should be reasonable (less than 1MB for 2000 calls)
|
||||
expect($memoryIncrease)->toBeLessThan(1024 * 1024);
|
||||
});
|
||||
|
||||
it('works correctly with autoloader edge cases', function () {
|
||||
// Test that exists() doesn't trigger warnings or errors with edge cases
|
||||
$className = ClassName::create('App\\NonExistent\\VeryLongClassNameThatDefinitelyDoesNotExist\\WithMultipleNamespaces\\AndMore\\Classes');
|
||||
expect($className->exists())->toBeFalse();
|
||||
|
||||
// Test with special characters that are valid in namespaces
|
||||
$className = ClassName::create('App\\Test\\ClassName123');
|
||||
expect($className->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('handles concurrent access correctly', function () {
|
||||
// Simulate concurrent access to exists() method
|
||||
$results = [];
|
||||
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$className = ClassName::create('stdClass');
|
||||
$results[] = $className->exists();
|
||||
}
|
||||
|
||||
// All results should be true
|
||||
foreach ($results as $result) {
|
||||
expect($result)->toBeTrue();
|
||||
}
|
||||
|
||||
// Test with non-existent class
|
||||
$results = [];
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$className = ClassName::create('NonExistentClass');
|
||||
$results[] = $className->exists();
|
||||
}
|
||||
|
||||
// All results should be false
|
||||
foreach ($results as $result) {
|
||||
expect($result)->toBeFalse();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('ClassName::exists() integration with container', function () {
|
||||
it('prevents empty string issues in DI container context', function () {
|
||||
// This test ensures that ClassName::exists() works correctly when used
|
||||
// in the context that was causing the original "Uninitialized string offset 0" issue
|
||||
|
||||
// Test the exact scenario that was failing
|
||||
$testCases = [
|
||||
'stdClass', // Should exist
|
||||
'Exception', // Should exist
|
||||
'NonExistentClass', // Should not exist
|
||||
'App\\Framework\\DI\\Container', // Should exist (interface)
|
||||
'App\\NonExistent\\Interface', // Should not exist
|
||||
];
|
||||
|
||||
foreach ($testCases as $testClass) {
|
||||
$className = ClassName::create($testClass);
|
||||
$result = $className->exists();
|
||||
|
||||
// The important thing is that no warnings or errors are generated
|
||||
expect($result)->toBeIn([true, false]);
|
||||
}
|
||||
});
|
||||
|
||||
it('handles the AtomicStorage interface case that was failing', function () {
|
||||
// Test the specific case that was causing issues
|
||||
$className = ClassName::create('App\\Framework\\Filesystem\\AtomicStorage');
|
||||
expect($className->exists())->toBeTrue(); // Interface should exist
|
||||
|
||||
$className = ClassName::create('App\\Framework\\Filesystem\\Storage');
|
||||
expect($className->exists())->toBeTrue(); // Interface should exist
|
||||
|
||||
$className = ClassName::create('App\\Framework\\Filesystem\\FileStorage');
|
||||
expect($className->exists())->toBeTrue(); // Implementation should exist
|
||||
});
|
||||
|
||||
it('regression test - ensures no "Uninitialized string offset 0" warnings', function () {
|
||||
// This is a regression test for the specific bug we fixed
|
||||
// The bug occurred when empty strings were passed to class_exists()
|
||||
|
||||
// Capture any warnings or errors
|
||||
$errorLevel = error_reporting(E_ALL);
|
||||
$errors = [];
|
||||
|
||||
set_error_handler(function ($severity, $message, $file, $line) use (&$errors) {
|
||||
$errors[] = [
|
||||
'severity' => $severity,
|
||||
'message' => $message,
|
||||
'file' => $file,
|
||||
'line' => $line,
|
||||
];
|
||||
});
|
||||
|
||||
try {
|
||||
// Test various scenarios that could potentially cause the issue
|
||||
$testCases = [
|
||||
'stdClass',
|
||||
'Exception',
|
||||
'NonExistentClass',
|
||||
'App\\Framework\\Filesystem\\AtomicStorage',
|
||||
'App\\Framework\\Filesystem\\Storage',
|
||||
'App\\Framework\\DI\\Container',
|
||||
'App\\NonExistent\\Class\\Name',
|
||||
'Very\\Long\\NonExistent\\Namespace\\ClassName',
|
||||
];
|
||||
|
||||
foreach ($testCases as $testCase) {
|
||||
$className = ClassName::create($testCase);
|
||||
$result = $className->exists();
|
||||
|
||||
// Result should be boolean, no errors should occur
|
||||
expect($result)->toBeBool();
|
||||
}
|
||||
|
||||
// Ensure no "Uninitialized string offset" or similar warnings occurred
|
||||
foreach ($errors as $error) {
|
||||
expect($error['message'])->not->toContain('Uninitialized string offset');
|
||||
expect($error['message'])->not->toContain('ClassLoader.php');
|
||||
}
|
||||
|
||||
// Should have no errors at all
|
||||
expect($errors)->toBeEmpty();
|
||||
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
error_reporting($errorLevel);
|
||||
}
|
||||
});
|
||||
});
|
||||
75
tests/Framework/Core/ValueObjects/ScoreTest.php
Normal file
75
tests/Framework/Core/ValueObjects/ScoreTest.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Core\ValueObjects\Percentage;
|
||||
use App\Framework\Core\ValueObjects\Score;
|
||||
use App\Framework\Core\ValueObjects\ScoreLevel;
|
||||
|
||||
describe('Score Value Object', function () {
|
||||
|
||||
it('creates score with valid value', function () {
|
||||
$score = new Score(0.75);
|
||||
|
||||
expect($score->value())->toBe(0.75);
|
||||
expect($score->toLevel())->toBe(ScoreLevel::HIGH);
|
||||
});
|
||||
|
||||
it('validates score bounds', function () {
|
||||
expect(fn () => new Score(-0.1))->toThrow(InvalidArgumentException::class);
|
||||
expect(fn () => new Score(1.1))->toThrow(InvalidArgumentException::class);
|
||||
});
|
||||
|
||||
it('creates score from percentage', function () {
|
||||
$percentage = Percentage::from(80.0); // 80%
|
||||
$score = Score::fromPercentage($percentage);
|
||||
|
||||
expect($score->value())->toBe(0.8);
|
||||
});
|
||||
|
||||
it('converts to percentage', function () {
|
||||
$score = new Score(0.6);
|
||||
$percentage = $score->toPercentage();
|
||||
|
||||
expect($percentage->getValue())->toBe(60.0); // 60%
|
||||
});
|
||||
|
||||
it('determines correct levels', function () {
|
||||
expect((new Score(0.95))->toLevel())->toBe(ScoreLevel::CRITICAL);
|
||||
expect((new Score(0.8))->toLevel())->toBe(ScoreLevel::HIGH);
|
||||
expect((new Score(0.5))->toLevel())->toBe(ScoreLevel::MEDIUM);
|
||||
expect((new Score(0.1))->toLevel())->toBe(ScoreLevel::LOW);
|
||||
});
|
||||
|
||||
it('combines scores correctly', function () {
|
||||
$score1 = new Score(0.8);
|
||||
$score2 = new Score(0.4);
|
||||
|
||||
$combined = $score1->combine($score2, 0.6);
|
||||
|
||||
expect($combined->value())->toBe(0.64); // 0.8 * 0.6 + 0.4 * 0.4
|
||||
});
|
||||
|
||||
it('calculates weighted average', function () {
|
||||
$scores = [
|
||||
new Score(0.8),
|
||||
new Score(0.6),
|
||||
new Score(0.4),
|
||||
];
|
||||
$weights = [0.5, 0.3, 0.2];
|
||||
|
||||
$average = Score::weightedAverage($scores, $weights);
|
||||
|
||||
expect($average->value())->toEqualWithDelta(0.66, 0.01); // 0.8*0.5 + 0.6*0.3 + 0.4*0.2
|
||||
});
|
||||
|
||||
it('serializes and deserializes correctly', function () {
|
||||
$score = new Score(0.75);
|
||||
$array = $score->toArray();
|
||||
$restored = Score::fromArray($array);
|
||||
|
||||
expect($restored->value())->toBe($score->value());
|
||||
expect($restored->toLevel())->toBe($score->toLevel());
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user