Files
XC_VM/tests/Unit/ServiceContainerTest.php
T
Divarion-D 4a0501324c feat(modules): evolve module system to extensible platform architecture
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
2026-06-14 14:25:19 +03:00

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'));
}
}