Files
XC_VM/tests/Unit/AttributionGuardTest.php
T
Divarion_D bd54afa16b feat(integrity): lock panel UI when AGPL attribution is removed
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.
2026-09-18 15:10:43 +03:00

93 lines
2.9 KiB
PHP

<?php
use PHPUnit\Framework\TestCase;
use XcVm\Core\Bootstrap\BootState;
use XcVm\Core\Bootstrap\Stage\AttributionVerificationStage;
use XcVm\Core\Bootstrap\StageProfiles;
use XcVm\Core\Container\ServiceContainer;
use XcVm\Core\Enum\BootContext;
use XcVm\Core\Error\ErrorResponder;
use XcVm\Core\Integrity\AttributionGuard;
use XcVm\Core\Util\AdminHelpers;
/**
* Covers the AGPL §7(b) attribution guard: the single-source notice keeps its
* markers, hasMarkers() detects tampering, the boot stage self-skips on CLI, and
* the stage is wired to UI scopes only (not streaming).
*/
final class AttributionGuardTest extends TestCase {
private const REQUIRED = [
'https://github.com/Vateron-Media/XC_VM',
'Vateron Media',
'https://www.gnu.org/licenses/agpl-3.0.html',
];
public function testAttributionSourceCarriesEveryMarker(): void {
$rHtml = AdminHelpers::getAttribution();
foreach (self::REQUIRED as $rMarker) {
$this->assertStringContainsString($rMarker, $rHtml);
}
}
public function testVerifyPassesForIntactAttribution(): void {
$this->assertTrue(AttributionGuard::verify());
}
public function testHasMarkersTrueWhenAllPresent(): void {
$this->assertTrue(AttributionGuard::hasMarkers(AdminHelpers::getAttribution()));
}
public function testHasMarkersFalseWhenAnyMarkerStripped(): void {
// Remove each required marker in turn; every removal must fail the check.
$rBase = AdminHelpers::getAttribution();
foreach (self::REQUIRED as $rMarker) {
$rTampered = str_replace($rMarker, 'Acme Panel', $rBase);
$this->assertFalse(
AttributionGuard::hasMarkers($rTampered),
"Stripping '{$rMarker}' should fail the attribution check"
);
}
}
public function testHasMarkersFalseOnEmptyString(): void {
$this->assertFalse(AttributionGuard::hasMarkers(''));
}
/** The stage is inert on CLI so the operator can always restore the notice. */
public function testStageSelfSkipsOnCli(): void {
$this->expectNotToPerformAssertions();
$state = new BootState(BootContext::Minimal, [], ServiceContainer::getInstance());
(new AttributionVerificationStage())->run($state);
ServiceContainer::resetInstance();
}
public function testErrorCodeRegistered(): void {
$this->assertArrayHasKey('ATTRIBUTION_REMOVED', ErrorResponder::codes());
}
/** Enforcement is scoped to the UI (Admin) boot profile, never to streaming. */
public function testStageWiredToAdminScopeOnly(): void {
$rAdmin = StageProfiles::for(BootContext::Admin, []);
$rStream = StageProfiles::for(BootContext::Stream, []);
$this->assertTrue($this->hasAttributionStage($rAdmin), 'Admin UI must enforce attribution');
$this->assertFalse($this->hasAttributionStage($rStream), 'Streaming must NOT be gated by attribution');
}
/**
* @param list<object> $rStages
*/
private function hasAttributionStage(array $rStages): bool {
foreach ($rStages as $rStage) {
if ($rStage instanceof AttributionVerificationStage) {
return true;
}
}
return false;
}
}