mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-09-26 12:01:57 +02:00
Atomic case-rename of the seven class-holding source directories to match the PSR-4 namespace casing, plus every consequent path reference. No code logic changes — pure structural rename. (config/content/resources/signals/migrations/ ministra/storage/tmp/vendor/www/bin stay lowercase.) - git mv: core→Core, domain→Domain, infrastructure→Infrastructure, streaming→Streaming, modules→Modules, cli→Cli, public→Public (module subdirs like Modules/plex stay lowercase; loaded by ModuleLoader, not Composer). - PHP filesystem paths updated across src/ + tests/: require/include, glob/scandir bases, view includes, asset/nginx-alias paths, autoload.php registerDirectories(), ModuleLoader/ModuleManager module roots, console.php discovery dirs. - src/composer.json PSR-4 vendored-lib paths → Core/Parsing/...; vendor/ regenerated. - nginx.conf: docroot + SCRIPT_FILENAME → Public/. SCRIPT_NAME and the public-facing /streaming/*.php compat routes are URLs, left unchanged. - Build/CI: Makefile LB_DIRS / LB_DIRS_TO_REMOVE / LB_FILES_TO_REMOVE PascalCased (closes the LB-archive privileged-leak blocker); phpstan.dist.neon paths/ scanDirectories/excludePaths; phpunit.xml.dist coverage; phpstan-baseline.neon regenerated (294→294 errors, no new resolution failures). - migrations/deleted_files.txt: old lowercase trees listed for client cleanup (assumes case-sensitive FS — the supported Linux target). Verified: grep-gate 0 live lowercase-dir require/include in src+tests; php -l clean; PHPUnit 292/292; PHPStan no errors; Composer first / XC_Autoloader last in SPL stack; global classes resolve from the renamed dirs.
364 lines
15 KiB
PHP
364 lines
15 KiB
PHP
<?php
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* Docs-in-CI: verify that all documented interface methods exist with
|
|
* the expected signatures. Fails immediately if an interface is renamed
|
|
* or a method is removed, so documentation and code can never silently diverge.
|
|
*
|
|
* Each test is named after the interface/class it protects. Adding a new
|
|
* public method to an interface requires adding it to the corresponding
|
|
* assertion here — this serves as the authoritative method manifest.
|
|
*/
|
|
final class InterfaceContractTest extends TestCase {
|
|
|
|
// ── ModuleInterface ───────────────────────────────────────────
|
|
|
|
public function testModuleInterfaceExtendsAllSubInterfaces(): void {
|
|
$rc = new ReflectionClass(ModuleInterface::class);
|
|
$this->assertTrue($rc->isInterface());
|
|
|
|
$parents = array_map(
|
|
fn(ReflectionClass $p) => $p->getName(),
|
|
$rc->getInterfaces()
|
|
);
|
|
|
|
foreach ([
|
|
ServiceProviderInterface::class,
|
|
RouteProviderInterface::class,
|
|
CommandProviderInterface::class,
|
|
NavbarProviderInterface::class,
|
|
] as $expected) {
|
|
$this->assertContains(
|
|
$expected,
|
|
$parents,
|
|
"ModuleInterface must extend {$expected}"
|
|
);
|
|
}
|
|
}
|
|
|
|
public function testModuleInterfaceHasIdentityMethods(): void {
|
|
$rc = new ReflectionClass(ModuleInterface::class);
|
|
|
|
$this->assertInterfaceMethod($rc, 'getName', [], 'string');
|
|
$this->assertInterfaceMethod($rc, 'getVersion', [], 'string');
|
|
}
|
|
|
|
public function testModuleInterfaceHasLifecycleMethods(): void {
|
|
$rc = new ReflectionClass(ModuleInterface::class);
|
|
|
|
$this->assertInterfaceMethod($rc, 'install', [], 'void');
|
|
$this->assertInterfaceMethod($rc, 'uninstall', [], 'void');
|
|
}
|
|
|
|
// ── ServiceProviderInterface ──────────────────────────────────
|
|
|
|
public function testServiceProviderInterfaceHasBoot(): void {
|
|
$rc = new ReflectionClass(ServiceProviderInterface::class);
|
|
$this->assertInterfaceMethod($rc, 'boot', ['ServiceContainer'], 'void');
|
|
}
|
|
|
|
public function testServiceProviderInterfaceHasGetEventSubscribers(): void {
|
|
$rc = new ReflectionClass(ServiceProviderInterface::class);
|
|
$this->assertInterfaceMethod($rc, 'getEventSubscribers', [], 'array');
|
|
}
|
|
|
|
// ── RouteProviderInterface ────────────────────────────────────
|
|
|
|
public function testRouteProviderInterfaceHasRegisterRoutes(): void {
|
|
$rc = new ReflectionClass(RouteProviderInterface::class);
|
|
$this->assertInterfaceMethod($rc, 'registerRoutes', ['Router'], 'void');
|
|
}
|
|
|
|
// ── CommandProviderInterface ──────────────────────────────────
|
|
|
|
public function testCommandProviderInterfaceHasRegisterCommands(): void {
|
|
$rc = new ReflectionClass(CommandProviderInterface::class);
|
|
$this->assertInterfaceMethod($rc, 'registerCommands', ['CommandRegistry'], 'void');
|
|
}
|
|
|
|
// ── NavbarProviderInterface ───────────────────────────────────
|
|
|
|
public function testNavbarProviderInterfaceHasRegisterNavbar(): void {
|
|
$rc = new ReflectionClass(NavbarProviderInterface::class);
|
|
$this->assertInterfaceMethod($rc, 'registerNavbar', ['NavbarRegistry'], 'void');
|
|
}
|
|
|
|
// ── StreamMiddlewareProviderInterface ─────────────────────────
|
|
|
|
public function testStreamMiddlewareProviderInterfaceHasGetStreamMiddleware(): void {
|
|
$rc = new ReflectionClass(StreamMiddlewareProviderInterface::class);
|
|
$this->assertInterfaceMethod($rc, 'getStreamMiddleware', [], 'array');
|
|
}
|
|
|
|
// ── BoundaryInterface ─────────────────────────────────────────
|
|
|
|
public function testBoundaryInterfaceHasCorrectMethods(): void {
|
|
$rc = new ReflectionClass(BoundaryInterface::class);
|
|
$this->assertTrue($rc->isInterface());
|
|
|
|
$this->assertInterfaceMethod($rc, 'getName', [], 'string');
|
|
$this->assertInterfaceMethod($rc, 'getEntryPoint', [], 'string');
|
|
$this->assertInterfaceMethod($rc, 'isIsolated', [], 'bool');
|
|
}
|
|
|
|
public function testBoundaryInterfaceHasExactlyThreeMethods(): void {
|
|
$rc = new ReflectionClass(BoundaryInterface::class);
|
|
$own = array_filter(
|
|
$rc->getMethods(ReflectionMethod::IS_PUBLIC),
|
|
fn(ReflectionMethod $m) => $m->getDeclaringClass()->getName() === BoundaryInterface::class
|
|
);
|
|
$this->assertCount(
|
|
3,
|
|
$own,
|
|
'BoundaryInterface must have exactly 3 methods: getName, getEntryPoint, isIsolated'
|
|
);
|
|
}
|
|
|
|
// ── MigratableInterface ───────────────────────────────────────
|
|
|
|
public function testMigratableInterfaceHasGetMigrations(): void {
|
|
$rc = new ReflectionClass(MigratableInterface::class);
|
|
$this->assertTrue($rc->isInterface());
|
|
$this->assertInterfaceMethod($rc, 'getMigrations', [], 'array');
|
|
}
|
|
|
|
// ── CronProviderInterface ─────────────────────────────────────
|
|
|
|
public function testCronProviderInterfaceHasGetCronEntries(): void {
|
|
$rc = new ReflectionClass(CronProviderInterface::class);
|
|
$this->assertTrue($rc->isInterface());
|
|
$this->assertInterfaceMethod($rc, 'getCronEntries', [], 'array');
|
|
}
|
|
|
|
// ── BaseModule ────────────────────────────────────────────────
|
|
|
|
public function testBaseModuleIsAbstract(): void {
|
|
$rc = new ReflectionClass(BaseModule::class);
|
|
$this->assertTrue($rc->isAbstract(), 'BaseModule must be abstract');
|
|
}
|
|
|
|
public function testBaseModuleImplementsModuleInterface(): void {
|
|
$rc = new ReflectionClass(BaseModule::class);
|
|
$this->assertTrue(
|
|
$rc->implementsInterface(ModuleInterface::class),
|
|
'BaseModule must implement ModuleInterface'
|
|
);
|
|
}
|
|
|
|
public function testBaseModuleImplementsMigratableInterface(): void {
|
|
$rc = new ReflectionClass(BaseModule::class);
|
|
$this->assertTrue(
|
|
$rc->implementsInterface(MigratableInterface::class),
|
|
'BaseModule must implement MigratableInterface'
|
|
);
|
|
}
|
|
|
|
public function testBaseModuleImplementsCronProviderInterface(): void {
|
|
$rc = new ReflectionClass(BaseModule::class);
|
|
$this->assertTrue(
|
|
$rc->implementsInterface(CronProviderInterface::class),
|
|
'BaseModule must implement CronProviderInterface'
|
|
);
|
|
}
|
|
|
|
public function testBaseModuleHasGetNameAndGetVersionAbstract(): void {
|
|
$rc = new ReflectionClass(BaseModule::class);
|
|
|
|
foreach (['getName', 'getVersion'] as $method) {
|
|
$rm = $rc->getMethod($method);
|
|
$this->assertTrue(
|
|
$rm->isAbstract(),
|
|
"BaseModule::{$method}() must be abstract (identity contract)"
|
|
);
|
|
}
|
|
}
|
|
|
|
public function testBaseModuleProvidesConcreteDefaultForOptionalMethods(): void {
|
|
$rc = new ReflectionClass(BaseModule::class);
|
|
|
|
$optionalMethods = [
|
|
'boot', 'getEventSubscribers', 'registerRoutes', 'registerCommands',
|
|
'registerNavbar', 'install', 'uninstall', 'getMigrations', 'getCronEntries',
|
|
];
|
|
|
|
foreach ($optionalMethods as $method) {
|
|
$rm = $rc->getMethod($method);
|
|
$this->assertFalse(
|
|
$rm->isAbstract(),
|
|
"BaseModule::{$method}() must provide a concrete default implementation"
|
|
);
|
|
}
|
|
}
|
|
|
|
public function testBaseModuleGetMigrationsReturnsEmptyArray(): void {
|
|
$module = new class extends BaseModule {
|
|
public function getName(): string { return 'test'; }
|
|
public function getVersion(): string { return '1.0.0'; }
|
|
};
|
|
|
|
$this->assertSame([], $module->getMigrations());
|
|
}
|
|
|
|
public function testBaseModuleGetCronEntriesReturnsEmptyArray(): void {
|
|
$module = new class extends BaseModule {
|
|
public function getName(): string { return 'test'; }
|
|
public function getVersion(): string { return '1.0.0'; }
|
|
};
|
|
|
|
$this->assertSame([], $module->getCronEntries());
|
|
}
|
|
|
|
// ── EventDispatcher ───────────────────────────────────────────
|
|
|
|
public function testEventDispatcherIsInstantiable(): void {
|
|
$rc = new ReflectionClass(EventDispatcher::class);
|
|
$this->assertFalse($rc->isAbstract(), 'EventDispatcher must be concrete');
|
|
$this->assertTrue($rc->hasMethod('__construct'));
|
|
}
|
|
|
|
public function testEventDispatcherHasSingletonManagement(): void {
|
|
$rc = new ReflectionClass(EventDispatcher::class);
|
|
|
|
foreach (['getInstance', 'setInstance', 'resetInstance'] as $method) {
|
|
$rm = $rc->getMethod($method);
|
|
$this->assertTrue(
|
|
$rm->isStatic(),
|
|
"EventDispatcher::{$method}() must be static"
|
|
);
|
|
}
|
|
}
|
|
|
|
public function testEventDispatcherCoreApiIsStatic(): void {
|
|
$rc = new ReflectionClass(EventDispatcher::class);
|
|
|
|
foreach (['dispatch', 'listen', 'hasListeners', 'unlisten', 'clear'] as $method) {
|
|
$rm = $rc->getMethod($method);
|
|
$this->assertTrue(
|
|
$rm->isStatic(),
|
|
"EventDispatcher::{$method}() must be static (backward-compat bridge)"
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── MinistraModule ────────────────────────────────────────────
|
|
|
|
public function testMinistraModuleImplementsBoundaryInterface(): void {
|
|
if (!class_exists(\XcVm\Module\Ministra\MinistraModule::class)) {
|
|
$file = defined('MAIN_HOME')
|
|
? MAIN_HOME . 'Modules/ministra/MinistraModule.php'
|
|
: __DIR__ . '/../../src/Modules/ministra/MinistraModule.php';
|
|
require_once $file;
|
|
}
|
|
$rc = new ReflectionClass(\XcVm\Module\Ministra\MinistraModule::class);
|
|
$this->assertTrue(
|
|
$rc->implementsInterface(BoundaryInterface::class),
|
|
'MinistraModule must implement BoundaryInterface'
|
|
);
|
|
}
|
|
|
|
// ── ModuleState enum ──────────────────────────────────────────
|
|
|
|
public function testModuleStateEnumExists(): void {
|
|
$rc = new \ReflectionEnum(ModuleState::class);
|
|
$this->assertTrue($rc->isBacked());
|
|
$this->assertSame('string', (string) $rc->getBackingType());
|
|
}
|
|
|
|
public function testModuleStateEnumHasAllFourCases(): void {
|
|
$cases = ModuleState::cases();
|
|
$names = array_map(fn($c) => $c->name, $cases);
|
|
$this->assertContains('Enabled', $names);
|
|
$this->assertContains('Disabled', $names);
|
|
$this->assertContains('Installing', $names);
|
|
$this->assertContains('Failed', $names);
|
|
}
|
|
|
|
public function testModuleStateHasIsLoadable(): void {
|
|
$rc = new \ReflectionEnum(ModuleState::class);
|
|
$this->assertTrue($rc->hasMethod('isLoadable'));
|
|
$rm = $rc->getMethod('isLoadable');
|
|
$this->assertSame('bool', (string) $rm->getReturnType());
|
|
}
|
|
|
|
public function testModuleStateHasFromRaw(): void {
|
|
$rc = new \ReflectionEnum(ModuleState::class);
|
|
$this->assertTrue($rc->hasMethod('fromRaw'));
|
|
$rm = $rc->getMethod('fromRaw');
|
|
$this->assertTrue($rm->isStatic());
|
|
}
|
|
|
|
// ── ListensTo attribute ───────────────────────────────────────
|
|
|
|
public function testListensToAttributeExists(): void {
|
|
$rc = new ReflectionClass(ListensTo::class);
|
|
$attrs = $rc->getAttributes(\Attribute::class);
|
|
$this->assertNotEmpty($attrs, 'ListensTo must carry #[\Attribute]');
|
|
}
|
|
|
|
public function testListensToIsRepeatableAndTargetsMethod(): void {
|
|
$rc = new ReflectionClass(ListensTo::class);
|
|
$attrArgs = $rc->getAttributes(\Attribute::class)[0]->getArguments();
|
|
$flags = $attrArgs[0] ?? 0;
|
|
$this->assertSame(
|
|
\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE,
|
|
$flags,
|
|
);
|
|
}
|
|
|
|
public function testListensToHasEventClassAndPriorityProperties(): void {
|
|
$rc = new ReflectionClass(ListensTo::class);
|
|
$this->assertTrue($rc->hasProperty('eventClass'));
|
|
$this->assertTrue($rc->hasProperty('priority'));
|
|
}
|
|
|
|
// ── Helper ────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Assert that a method exists on the interface with the given parameter
|
|
* type-hint short names and return type name.
|
|
*
|
|
* @param string[] $paramTypes Short class names of parameter types (in order).
|
|
* Pass [] to assert no parameters.
|
|
*/
|
|
private function assertInterfaceMethod(
|
|
ReflectionClass $rc,
|
|
string $methodName,
|
|
array $paramTypes,
|
|
string $returnType
|
|
): void {
|
|
$this->assertTrue(
|
|
$rc->hasMethod($methodName),
|
|
"{$rc->getShortName()}::{$methodName}() must exist"
|
|
);
|
|
|
|
$rm = $rc->getMethod($methodName);
|
|
|
|
// Return type
|
|
$actual = (string) ($rm->getReturnType() ?? '');
|
|
$this->assertSame(
|
|
$returnType,
|
|
$actual,
|
|
"{$rc->getShortName()}::{$methodName}() must return {$returnType}"
|
|
);
|
|
|
|
// Parameter types (short names)
|
|
$params = $rm->getParameters();
|
|
$this->assertCount(
|
|
count($paramTypes),
|
|
$params,
|
|
"{$rc->getShortName()}::{$methodName}() must have " . count($paramTypes) . " parameter(s)"
|
|
);
|
|
|
|
foreach ($paramTypes as $i => $expectedType) {
|
|
$actualType = (string) ($params[$i]->getType() ?? '');
|
|
$this->assertSame(
|
|
$expectedType,
|
|
$actualType,
|
|
"{$rc->getShortName()}::{$methodName}() parameter {$i} must be typed {$expectedType}"
|
|
);
|
|
}
|
|
}
|
|
}
|