mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-10-03 04:02:10 +02:00
Adds a disclosed, reversible attribution-integrity check (AGPL-3.0 §7(b)).
The "Vateron Media · AGPL-3.0" credit now has a single source
(AdminHelpers::getAttribution(), reused by getFooter). AttributionGuard
verifies its markers; AttributionVerificationStage runs the check on the
Admin (UI) boot profile only — admin/reseller/player panels — and calls
generateError('ATTRIBUTION_REMOVED') when the notice is gone.
Scope is deliberately UI-only: streaming is NOT gated, so removing a UI
footer cannot cut off a reseller's end-viewers (that is the licence
layer's concern). The lock is non-destructive and auto-reverses — the CLI
stays usable and restoring the notice unlocks the panel on the next
request. Enforcement is documented in README (License Enforcement).
This is a readable-PHP deterrent (a determined actor can remove the check
with the notice); tamper-resistant enforcement belongs in the compiled
xcvm_core extension (planned).
Tests: attribution markers, hasMarkers tamper detection, CLI self-skip,
UI-only wiring, error-code registration. Full suite + gates + CRAP green.
171 lines
5.6 KiB
PHP
171 lines
5.6 KiB
PHP
<?php
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use XcVm\Core\Bootstrap\BootKernel;
|
|
use XcVm\Core\Bootstrap\BootPipeline;
|
|
use XcVm\Core\Bootstrap\BootStageInterface;
|
|
use XcVm\Core\Bootstrap\BootState;
|
|
use XcVm\Core\Bootstrap\StageProfiles;
|
|
use XcVm\Core\Bootstrap\Stage\AdminApiStage;
|
|
use XcVm\Core\Bootstrap\Stage\AttributionVerificationStage;
|
|
use XcVm\Core\Bootstrap\Stage\AdminGlobalsStage;
|
|
use XcVm\Core\Bootstrap\Stage\AdminShutdownStage;
|
|
use XcVm\Core\Bootstrap\Stage\ConfigStage;
|
|
use XcVm\Core\Bootstrap\Stage\ConstantsStage;
|
|
use XcVm\Core\Bootstrap\Stage\ContainerPopulateStage;
|
|
use XcVm\Core\Bootstrap\Stage\DatabaseStage;
|
|
use XcVm\Core\Bootstrap\Stage\FloodProtectionStage;
|
|
use XcVm\Core\Bootstrap\Stage\HealthCheckStage;
|
|
use XcVm\Core\Bootstrap\Stage\HostVerificationStage;
|
|
use XcVm\Core\Bootstrap\Stage\LegacyCoreStage;
|
|
use XcVm\Core\Bootstrap\Stage\ProcessTitleStage;
|
|
use XcVm\Core\Bootstrap\Stage\RedisStage;
|
|
use XcVm\Core\Bootstrap\Stage\SessionStage;
|
|
use XcVm\Core\Bootstrap\Stage\StatusConstantsStage;
|
|
use XcVm\Core\Bootstrap\Stage\TranslatorStage;
|
|
use XcVm\Core\Container\ServiceContainer;
|
|
use XcVm\Core\Enum\BootContext;
|
|
|
|
/**
|
|
* Pipeline mechanics and per-context profile composition. A full boot cannot run
|
|
* in-process (ConfigStage calls the \XC_VM config extension, absent in tests), so
|
|
* this covers ordering and composition with fakes/pure inspection.
|
|
*/
|
|
final class BootPipelineTest extends TestCase {
|
|
protected function tearDown(): void {
|
|
ServiceContainer::resetInstance();
|
|
}
|
|
|
|
private function state(BootContext $ctx = BootContext::Minimal): BootState {
|
|
return new BootState($ctx, [], ServiceContainer::getInstance());
|
|
}
|
|
|
|
private function recordingStage(string $name, ArrayObject $log): BootStageInterface {
|
|
return new class ($name, $log) implements BootStageInterface {
|
|
public function __construct(private string $name, private ArrayObject $log) {
|
|
}
|
|
|
|
public function run(BootState $state): void {
|
|
$this->log->append($this->name);
|
|
}
|
|
};
|
|
}
|
|
|
|
public function testPipelineRunsStagesInOrder(): void {
|
|
$log = new ArrayObject();
|
|
|
|
(new BootPipeline([
|
|
$this->recordingStage('a', $log),
|
|
$this->recordingStage('b', $log),
|
|
$this->recordingStage('c', $log),
|
|
]))->run($this->state());
|
|
|
|
$this->assertSame(['a', 'b', 'c'], $log->getArrayCopy());
|
|
}
|
|
|
|
public function testPipelineAbortsOnThrow(): void {
|
|
$log = new ArrayObject();
|
|
$boom = new class implements BootStageInterface {
|
|
public function run(BootState $state): void {
|
|
throw new RuntimeException('boom');
|
|
}
|
|
};
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
try {
|
|
(new BootPipeline([$boom, $this->recordingStage('after', $log)]))->run($this->state());
|
|
} finally {
|
|
$this->assertNotContains('after', $log->getArrayCopy(), 'stage after a throwing stage must not run');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string,mixed> $options
|
|
* @return list<class-string>
|
|
*/
|
|
private function classesFor(BootContext $ctx, array $options = []): array {
|
|
$opts = array_merge(BootKernel::defaults($ctx), $options);
|
|
return array_map('get_class', StageProfiles::for($ctx, $opts));
|
|
}
|
|
|
|
public function testMinimalProfile(): void {
|
|
$this->assertSame([
|
|
ConstantsStage::class,
|
|
ConfigStage::class,
|
|
FloodProtectionStage::class,
|
|
HostVerificationStage::class,
|
|
ContainerPopulateStage::class,
|
|
], $this->classesFor(BootContext::Minimal));
|
|
}
|
|
|
|
public function testCliProfileDefaultsOmitRedisAndProcessTitle(): void {
|
|
$this->assertSame([
|
|
ConstantsStage::class,
|
|
ConfigStage::class,
|
|
FloodProtectionStage::class,
|
|
HostVerificationStage::class,
|
|
DatabaseStage::class,
|
|
LegacyCoreStage::class,
|
|
ContainerPopulateStage::class,
|
|
HealthCheckStage::class,
|
|
], $this->classesFor(BootContext::Cli));
|
|
}
|
|
|
|
public function testCliProfileAddsRedisAndProcessTitleWhenRequested(): void {
|
|
$classes = $this->classesFor(BootContext::Cli, ['redis' => true, 'process' => 'worker']);
|
|
|
|
$this->assertContains(RedisStage::class, $classes);
|
|
$this->assertContains(ProcessTitleStage::class, $classes);
|
|
}
|
|
|
|
public function testStreamProfileIsDatabaseOnly(): void {
|
|
$this->assertSame([
|
|
ConstantsStage::class,
|
|
ConfigStage::class,
|
|
FloodProtectionStage::class,
|
|
HostVerificationStage::class,
|
|
DatabaseStage::class,
|
|
ContainerPopulateStage::class,
|
|
HealthCheckStage::class,
|
|
], $this->classesFor(BootContext::Stream));
|
|
}
|
|
|
|
public function testAdminProfileFullSequence(): void {
|
|
$this->assertSame([
|
|
ConstantsStage::class,
|
|
ConfigStage::class,
|
|
FloodProtectionStage::class,
|
|
HostVerificationStage::class,
|
|
AttributionVerificationStage::class,
|
|
SessionStage::class,
|
|
DatabaseStage::class,
|
|
LegacyCoreStage::class,
|
|
RedisStage::class,
|
|
AdminApiStage::class,
|
|
TranslatorStage::class,
|
|
AdminShutdownStage::class,
|
|
StatusConstantsStage::class,
|
|
AdminGlobalsStage::class,
|
|
ContainerPopulateStage::class,
|
|
HealthCheckStage::class,
|
|
], $this->classesFor(BootContext::Admin));
|
|
}
|
|
|
|
public function testMinimalHasNoHealthCheck(): void {
|
|
$this->assertNotContains(HealthCheckStage::class, $this->classesFor(BootContext::Minimal));
|
|
}
|
|
|
|
/** WebApi has no kernel profile — it boots via WebApiBootstrap, so misuse must fail loudly. */
|
|
public function testWebApiContextHasNoKernelProfile(): void {
|
|
$this->expectException(\LogicException::class);
|
|
StageProfiles::for(BootContext::WebApi, []);
|
|
}
|
|
|
|
public function testDefaultsPerContext(): void {
|
|
$this->assertTrue(BootKernel::defaults(BootContext::Admin)['redis']);
|
|
$this->assertFalse(BootKernel::defaults(BootContext::Cli)['redis']);
|
|
$this->assertTrue(BootKernel::defaults(BootContext::Stream)['cached']);
|
|
$this->assertFalse(BootKernel::defaults(BootContext::Admin)['cached']);
|
|
}
|
|
}
|