mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-09-30 20:02:06 +02:00
TASK-000: Remove legacy core patching system - Delete src/core/Module/CoreCodePatcher.php - Delete src/core/Module/CoreCodePatchableModuleInterface.php TASK-001: Split ModuleInterface into specialized provider interfaces - Add ServiceProviderInterface (boot, getEventSubscribers) - Add RouteProviderInterface (registerRoutes) - Add CommandProviderInterface (registerCommands) - Add NavbarProviderInterface (registerNavbar) - Add StreamMiddlewareProviderInterface - ModuleLoader::bootAll() detects supported providers via instanceof TASK-002: Refactor EventDispatcher - Add ListenerProvider with priority support - Add AbstractEvent base class - Add StoppableEventInterface support - Preserve legacy subscribe/publish API compatibility TASK-003: Extend ServiceContainer - Add ServiceContainer::decorate() - Support priority-based decorator chains - Add protected service registry: - db - settings - config - auth TASK-004: Add core event catalog - ModuleLoadedEvent - ModuleBootedEvent - PackageInstalledEvent - UserAuthenticatedEvent - UserLoggedOutEvent - StreamStartingEvent - StreamStartedEvent - StreamStoppedEvent - SettingsChangedEvent TASK-005: Formalize subsystem boundaries - Add BoundaryInterface - Add MinistraBootstrap implementation - Define exported service contracts TASK-006: Add stream middleware pipeline - Add StreamContext - Add StreamMiddlewareInterface - Add StreamPipeline - Support middleware priority ordering TASK-007: Add module hot reload - Dispatch PackageInstalledEvent after installation - Add ModuleManager::hotReload() - Load and boot newly installed modules without service restart TASK-008: Standardize container contracts - Add container interfaces: - ContainerInterface - ContainerExceptionInterface - NotFoundExceptionInterface - Add NotFoundException implementation - ServiceContainer::get() throws NotFoundException TASK-009: Refactor navbar registration - CoreNavbarProvider implements NavbarProviderInterface - Add registerNavbar() instance method - ModuleLoader uses provider-based registration TASK-010: Add module priority support - Add priority field to module.json - Default value: 0 - Higher priority modules load earlier - Resolve load order by: 1. Priority DESC 2. Module name ASC TASK-011: Add optional dependencies - Add optional_dependencies field to module.json - Missing optional dependencies do not block module loading - Process optional dependencies after required dependencies Tests - Add ServiceContainer tests - Add EventDispatcher tests - Add StreamPipeline tests - Add ModuleLoader priority tests Documentation - Rewrite module architecture documentation - Update examples and extension development guides - Document lifecycle, events, services, middleware, and dependencies
151 lines
5.2 KiB
PHP
151 lines
5.2 KiB
PHP
<?php
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class ServiceContainerTest extends TestCase {
|
|
|
|
protected function setUp(): void {
|
|
ServiceContainer::resetInstance();
|
|
}
|
|
|
|
protected function tearDown(): void {
|
|
ServiceContainer::resetInstance();
|
|
}
|
|
|
|
// ── PSR-11 compliance ──────────────────────────────────────
|
|
|
|
public function testGetReturnsRegisteredValue(): void {
|
|
$c = ServiceContainer::getInstance();
|
|
$c->set('foo', 'bar');
|
|
|
|
$this->assertSame('bar', $c->get('foo'));
|
|
}
|
|
|
|
public function testGetThrowsNotFoundExceptionForMissingService(): void {
|
|
$c = ServiceContainer::getInstance();
|
|
|
|
$this->expectException(NotFoundException::class);
|
|
$c->get('does-not-exist');
|
|
}
|
|
|
|
public function testNotFoundExceptionImplementsContainerInterface(): void {
|
|
$this->assertInstanceOf(NotFoundExceptionInterface::class, new NotFoundException());
|
|
$this->assertInstanceOf(ContainerExceptionInterface::class, new NotFoundException());
|
|
}
|
|
|
|
public function testHasReturnsTrueForRegisteredService(): void {
|
|
$c = ServiceContainer::getInstance();
|
|
$c->set('exists', 42);
|
|
|
|
$this->assertTrue($c->has('exists'));
|
|
}
|
|
|
|
public function testHasReturnsFalseForMissingService(): void {
|
|
$c = ServiceContainer::getInstance();
|
|
|
|
$this->assertFalse($c->has('missing'));
|
|
}
|
|
|
|
public function testContainerImplementsContainerInterface(): void {
|
|
$this->assertInstanceOf(ContainerInterface::class, ServiceContainer::getInstance());
|
|
}
|
|
|
|
// ── Factory / singleton ────────────────────────────────────
|
|
|
|
public function testFactoryCallableIsCalledOnce(): void {
|
|
$c = ServiceContainer::getInstance();
|
|
$calls = 0;
|
|
$c->set('obj', function () use (&$calls) {
|
|
$calls++;
|
|
return new stdClass();
|
|
});
|
|
|
|
$a = $c->get('obj');
|
|
$b = $c->get('obj');
|
|
|
|
$this->assertSame(1, $calls);
|
|
$this->assertSame($a, $b);
|
|
}
|
|
|
|
public function testFactoryMethodReturnsNewInstanceEachTime(): void {
|
|
$c = ServiceContainer::getInstance();
|
|
$c->factory('fresh', fn() => new stdClass());
|
|
|
|
$a = $c->get('fresh');
|
|
$b = $c->get('fresh');
|
|
|
|
$this->assertNotSame($a, $b);
|
|
}
|
|
|
|
// ── Circular dependency detection ──────────────────────────
|
|
|
|
public function testCircularDependencyThrowsRuntimeException(): void {
|
|
$c = ServiceContainer::getInstance();
|
|
$c->set('a', function ($c) {
|
|
return $c->get('b');
|
|
});
|
|
$c->set('b', function ($c) {
|
|
return $c->get('a');
|
|
});
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
$this->expectExceptionMessageMatches('/цикл/u');
|
|
$c->get('a');
|
|
}
|
|
|
|
// ── Decoration ─────────────────────────────────────────────
|
|
|
|
public function testDecorateWrapsService(): void {
|
|
$c = ServiceContainer::getInstance();
|
|
$c->set('value', fn() => 'original');
|
|
$c->decorate('value', fn($inner, $c) => strtoupper($inner));
|
|
|
|
$this->assertSame('ORIGINAL', $c->get('value'));
|
|
}
|
|
|
|
public function testDecoratePriorityOrderIsHighestOutermost(): void {
|
|
$c = ServiceContainer::getInstance();
|
|
$c->set('str', fn() => 'base');
|
|
$c->decorate('str', fn($inner, $c) => "[low:{$inner}]", priority: 10);
|
|
$c->decorate('str', fn($inner, $c) => "[high:{$inner}]", priority: 50);
|
|
|
|
// highest priority (50) wraps outermost → called first by caller
|
|
$this->assertSame('[high:[low:base]]', $c->get('str'));
|
|
}
|
|
|
|
public function testDecorateProtectedServiceThrowsRuntimeException(): void {
|
|
$c = ServiceContainer::getInstance();
|
|
|
|
foreach (['db', 'settings', 'config', 'auth'] as $protected) {
|
|
try {
|
|
$c->decorate($protected, fn($inner) => $inner);
|
|
$this->fail("Expected RuntimeException for protected service '{$protected}'");
|
|
} catch (RuntimeException $e) {
|
|
$this->assertStringContainsString($protected, $e->getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
public function testDecorateUnregisteredServiceThrowsRuntimeException(): void {
|
|
$c = ServiceContainer::getInstance();
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
$c->decorate('not-registered', fn($inner) => $inner);
|
|
}
|
|
|
|
// ── getOrDefault ───────────────────────────────────────────
|
|
|
|
public function testGetOrDefaultReturnsFallbackForMissingService(): void {
|
|
$c = ServiceContainer::getInstance();
|
|
|
|
$this->assertSame('fallback', $c->getOrDefault('missing', 'fallback'));
|
|
}
|
|
|
|
public function testGetOrDefaultReturnServiceWhenPresent(): void {
|
|
$c = ServiceContainer::getInstance();
|
|
$c->set('key', 'value');
|
|
|
|
$this->assertSame('value', $c->getOrDefault('key', 'fallback'));
|
|
}
|
|
}
|