mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-11-05 19:03:33 +01:00
154 lines
8.8 KiB
PHP
154 lines
8.8 KiB
PHP
<?php
|
|
|
|
use PHPUnit\Framework\Attributes\DataProvider;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/** The real dispatcher and ApiClient, with process/transport stand-ins in a child PHP. */
|
|
final class StreamControlDispatcherTest extends TestCase {
|
|
private const HARNESS = <<<'PHP'
|
|
namespace XcVm\Core\Cluster { class NodeRpc { public static function request($id,$payload,$timeout) { $GLOBALS['remote'][]=[$id,$payload,$timeout]; return $GLOBALS['reply']; } public static function submit($id,$payload) { $GLOBALS['submissions'][]=[$id,$payload]; return $GLOBALS['queued']; } } }
|
|
namespace XcVm\Core\Logging { class FileLogger { public static function log(...$args) {} } }
|
|
namespace XcVm\Domain\Server { class ServerRepository { public static function getAll() { return [1=>['server_online'=>true],2=>['server_online'=>true],3=>['server_online'=>false]]; } } }
|
|
namespace XcVm\Infrastructure\Database { class DatabaseFactory { public static function get() { return new class { public function query(...$args) {return true;} public function get_rows() {return $GLOBALS['assignments'];} }; } } }
|
|
namespace XcVm\Domain\Stream {
|
|
class StreamSource { public static function streamRow(...$args) { return empty($GLOBALS['streamrow_missing']) ? ['id'=>$args[0]] : null; } }
|
|
class StreamProcess {
|
|
const MONITOR_PHP='php', MONITOR_FANOUT='fanout';
|
|
public static function startMonitor(...$args) { $GLOBALS['local'][]=['startMonitor',$args]; return $GLOBALS['monitor']; }
|
|
public static function stopStream(...$args) { $GLOBALS['local'][]=['stopStream',$args]; }
|
|
public static function stopMovie(...$args) { $GLOBALS['local'][]=['stopMovie',$args]; }
|
|
public static function startMovie(...$args) { $GLOBALS['local'][]=['startMovie',$args]; return $GLOBALS['pid']; }
|
|
public static function queueMovie(...$args) { $GLOBALS['local'][]=['queueMovie',$args]; return $GLOBALS['queue']; }
|
|
}
|
|
}
|
|
namespace {
|
|
define('SERVER_ID',1);
|
|
$input=json_decode(stream_get_contents(STDIN),true);
|
|
$GLOBALS['assignments']=[['server_id'=>1,'stream_id'=>10],['server_id'=>2,'stream_id'=>10],['server_id'=>3,'stream_id'=>10]];
|
|
$GLOBALS['monitor']='php'; $GLOBALS['reply']='{"result":true}'; $GLOBALS['streamrow_missing']=false;
|
|
$GLOBALS['queued']=true; $GLOBALS['submissions']=[];
|
|
$GLOBALS['pid']=123; $GLOBALS['queue']=true; $GLOBALS['local']=[]; $GLOBALS['remote']=[];
|
|
foreach ($input['options'] as $key=>$value) { $GLOBALS[$key]=$value; }
|
|
require %DISPATCHER%;
|
|
require %CLIENT%;
|
|
$result=\XcVm\Core\Http\ApiClient::request($input['request']);
|
|
echo json_encode(['answer'=>json_decode($result,true),'local'=>$GLOBALS['local'],'remote'=>$GLOBALS['remote'],'submissions'=>$GLOBALS['submissions']]);
|
|
}
|
|
PHP;
|
|
|
|
private function dispatch(array $rRequest = [], array $rOptions = []): array {
|
|
$rCode = str_replace(['%DISPATCHER%', '%CLIENT%'], [var_export(MAIN_HOME . 'Domain/Stream/StreamControlDispatcher.php', true), var_export(MAIN_HOME . 'Core/Http/ApiClient.php', true)], self::HARNESS);
|
|
$rProcess = proc_open([PHP_BINARY, '-r', $rCode], [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], $rPipes);
|
|
$this->assertIsResource($rProcess);
|
|
fwrite($rPipes[0], json_encode(['request' => array_replace(['action' => 'stream', 'sub' => 'start', 'stream_ids' => [10], 'servers' => [1]], $rRequest), 'options' => $rOptions]));
|
|
fclose($rPipes[0]);
|
|
$rOutput = stream_get_contents($rPipes[1]);
|
|
$rError = stream_get_contents($rPipes[2]);
|
|
fclose($rPipes[1]);
|
|
fclose($rPipes[2]);
|
|
$this->assertSame(0, proc_close($rProcess), $rError . $rOutput);
|
|
$rResult = json_decode($rOutput, true);
|
|
$this->assertIsArray($rResult, $rOutput);
|
|
return $rResult;
|
|
}
|
|
|
|
public function testMainRestartUsesLocalMonitorWithoutHttp(): void {
|
|
$rResult = $this->dispatch(['sub' => 'restart']);
|
|
$this->assertTrue($rResult['answer']['result']);
|
|
$this->assertSame([['startMonitor', [10, 1]]], $rResult['local']);
|
|
$this->assertSame([], $rResult['remote']);
|
|
}
|
|
|
|
public function testRemoteStartUsesRpcWithoutStartingMain(): void {
|
|
$rResult = $this->dispatch(['servers' => [2]]);
|
|
$this->assertTrue($rResult['answer']['result']);
|
|
$this->assertSame([], $rResult['local']);
|
|
$this->assertSame([[2, ['action' => 'stream', 'function' => 'start', 'stream_ids' => [10]], 5]], $rResult['remote']);
|
|
}
|
|
|
|
public function testAsyncRemoteStartSubmitsWithoutWaitingForRpc(): void {
|
|
$rResult = $this->dispatch(['servers' => [2], 'async' => true]);
|
|
$this->assertTrue($rResult['answer']['result']);
|
|
$this->assertSame([], $rResult['remote']);
|
|
$this->assertSame([[2, ['action'=>'stream','function'=>'start','stream_ids'=>[10]]]], $rResult['submissions']);
|
|
$rResult = $this->dispatch(['servers' => [2], 'async' => true], ['queued'=>false]);
|
|
$this->assertFalse($rResult['answer']['result']);
|
|
$this->assertSame([], $rResult['answer']['accepted']);
|
|
}
|
|
|
|
public function testRemoteFailureRetainsAcceptedLocalWork(): void {
|
|
$rResult = $this->dispatch(['servers' => [1, 2]], ['reply' => '<html>404</html>']);
|
|
$this->assertFalse($rResult['answer']['result']);
|
|
$this->assertSame([['server_id' => 1, 'stream_id' => 10]], $rResult['answer']['accepted']);
|
|
$this->assertSame([['server_id' => 2, 'code' => 'REMOTE_CONTROL_FAILED']], $rResult['answer']['errors']);
|
|
}
|
|
|
|
public function testUnlicensedStartFallsBackToLocalMonitor(): void {
|
|
// Enforcement is soft (LicenseGate): a MAIN start must not be hard blocked
|
|
// by the licence — it falls back to the local monitor. The old hard
|
|
// LICENSE_DENIED gate made an unlicensed/expired MAIN unable to start
|
|
// anything; the signed cluster transport still enforces it for remote.
|
|
$rResult = $this->dispatch([]);
|
|
$this->assertTrue($rResult['answer']['result']);
|
|
$this->assertSame([['startMonitor', [10, 1]]], $rResult['local']);
|
|
$rResult = $this->dispatch(['sub' => 'stop']);
|
|
$this->assertTrue($rResult['answer']['result']);
|
|
$this->assertSame([['stopStream', [10, true]]], $rResult['local']);
|
|
}
|
|
|
|
#[DataProvider('refusedControls')]
|
|
public function testRefusalsReturnFailure(array $rRequest, array $rOptions, string $rCode): void {
|
|
$rResult = $this->dispatch($rRequest, $rOptions);
|
|
$this->assertFalse($rResult['answer']['result']);
|
|
$this->assertSame([], $rResult['answer']['accepted']);
|
|
$this->assertSame($rCode, $rResult['answer']['errors'][0]['code']);
|
|
}
|
|
|
|
public static function refusedControls(): array {
|
|
return [
|
|
'zero selection' => [['servers' => 0], [], 'INVALID_SERVER_IDS'],
|
|
'null selection' => [['servers' => null], [], 'INVALID_SERVER_IDS'],
|
|
'false selection' => [['servers' => false], [], 'INVALID_SERVER_IDS'],
|
|
'empty string selection' => [['servers' => ''], [], 'INVALID_SERVER_IDS'],
|
|
'unknown server' => [['servers' => [999]], [], 'UNKNOWN_SERVER'],
|
|
'negative server' => [['servers' => [-1]], [], 'INVALID_SERVER_IDS'],
|
|
'unassigned stream' => [['stream_ids' => [11]], [], 'STREAM_NOT_ASSIGNED_TO_TARGET'],
|
|
'malformed id' => [['stream_ids' => ['1;drop']], [], 'INVALID_STREAM_IDS'],
|
|
'no streams' => [['stream_ids' => []], [], 'INVALID_STREAM_IDS'],
|
|
'offline server' => [['servers' => [3]], [], 'SERVER_OFFLINE'],
|
|
'fenced local' => [[], ['monitor' => 'fenced'], 'LOCAL_CONTROL_REFUSED'],
|
|
'queue refusal' => [['action' => 'vod'], ['queue' => false], 'LOCAL_CONTROL_REFUSED'],
|
|
'encoder refusal' => [['action' => 'vod', 'force' => true], ['pid' => false], 'LOCAL_CONTROL_REFUSED'],
|
|
];
|
|
}
|
|
|
|
public function testVodForceAndQueueUseTheirExistingPaths(): void {
|
|
$rResult = $this->dispatch(['action' => 'vod', 'force' => true]);
|
|
$this->assertTrue($rResult['answer']['result']);
|
|
$this->assertSame([['stopMovie', [10, true]], ['startMovie', [10]]], $rResult['local']);
|
|
$rResult = $this->dispatch(['action' => 'vod']);
|
|
$this->assertTrue($rResult['answer']['result']);
|
|
$this->assertSame([['stopMovie', [10, true]], ['queueMovie', [10]]], $rResult['local']);
|
|
}
|
|
|
|
public function testPartialAssignmentAcceptsAssignedAndReportsUnassigned(): void {
|
|
// One unassigned id must not abort the whole batch: the assigned id still
|
|
// starts and the unassigned one is reported. The old all-or-nothing
|
|
// failure() also silently suppressed every id for the non-AJAX callers.
|
|
$rResult = $this->dispatch(['stream_ids' => [10, 11]]);
|
|
$this->assertFalse($rResult['answer']['result']);
|
|
$this->assertSame([['server_id' => 1, 'stream_id' => 10]], $rResult['answer']['accepted']);
|
|
$this->assertSame([['stream_id' => 11, 'code' => 'STREAM_NOT_ASSIGNED_TO_TARGET']], $rResult['answer']['errors']);
|
|
$this->assertSame([['startMonitor', [10, 1]]], $rResult['local']);
|
|
}
|
|
|
|
public function testDirectSourceStartIsNotRefusedByAStreamRowPreCheck(): void {
|
|
// A direct-source live stream is filtered out by StreamSource::streamRow
|
|
// (direct_source = 0). The dispatcher no longer pre-checks that row, so
|
|
// such a stream still starts via startMonitor, as the former path did.
|
|
$rResult = $this->dispatch([], ['streamrow_missing' => true]);
|
|
$this->assertTrue($rResult['answer']['result']);
|
|
$this->assertSame([['startMonitor', [10, 1]]], $rResult['local']);
|
|
}
|
|
}
|