mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-10-31 19:03:43 +01:00
Final whole-branch review fixes for the created-channel VOD-reuse + EPG work: - CreatedCommand: builds are incremental, but the copy/transcode target is computed over the whole playlist. A playlist edit that shifted the dominant target rebuilt only the new items, leaving the old .ts at the previous target and mixing codecs/resolutions mid-stream. Track the target in a _.target sidecar and force a full rebuild when it changes. - CreatedCommand: persist the freshly built timeline to cc_info before regenerating the EPG, so an incremental rebuild's guide is not one build behind (regenerate reads cc_info first, _.info only as a fallback). - EpgService: refuse creating OR renaming any source to 'Created Channels' (case/space-insensitive) so the reserved source cannot be duplicated or shadowed; the edit/delete of the reserved id were already guarded. - EpgService: hide the reserved source from getAll() by id (reservedSourceId), not by name, so all three gates use one consistent identity. - ChannelService: write epg_id = NULL, not 0, when the reserved source is missing (0 never matches a real id; NULL reads as "no source"). - EpgCronJob: build the external XMLTV after the created-channel regen so it carries their programmes in the same run instead of one cron cycle late. - CreatedChannelEpg: cap programme projection inside the loop; a very short loop would otherwise build millions of transient rows before the slice. - Tests: EpgReservedSourceGuardTest covers the create/rename/edit refusals and an ordinary create; AuditDecisionEditKeepsOmittedFields now seeds the reserved EPG source and carries the derived epg_id/channel_id in its created-channel baseline, matching the save-time linkage this branch added (the test predates it). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
89 lines
3.7 KiB
PHP
89 lines
3.7 KiB
PHP
<?php
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use XcVm\Core\Config\ConstantsInitializer;
|
|
use XcVm\Domain\Epg\CreatedChannelEpg;
|
|
use XcVm\Domain\Epg\EpgService;
|
|
use XcVm\Infrastructure\Database\DatabaseFactory;
|
|
|
|
/**
|
|
* The reserved created-channel EPG source ('Created Channels') is identified by
|
|
* name. EpgService::process must not let the admin create a colliding source or
|
|
* rename another one onto it — including case/whitespace variants, since MySQL
|
|
* matches the name case- and trailing-space-insensitively.
|
|
*/
|
|
final class EpgReservedSourceGuardTest extends TestCase {
|
|
private TestDb $rDb;
|
|
|
|
protected function setUp(): void {
|
|
foreach (ConstantsInitializer::statuses() as $rName => $rValue) {
|
|
defined($rName) || define($rName, $rValue);
|
|
}
|
|
$this->rDb = new TestDb();
|
|
$this->rDb->exec('CREATE TABLE `epg` (`id` INT AUTO_INCREMENT PRIMARY KEY, `epg_name` VARCHAR(255) COLLATE utf8_unicode_ci, `epg_file` VARCHAR(300), `days_keep` INT)');
|
|
// id 1 = the reserved sentinel, id 2 = an ordinary source.
|
|
$this->rDb->query("INSERT INTO `epg` (`epg_name`, `epg_file`) VALUES ('Created Channels', ''), ('Normal', 'n.xml')");
|
|
DatabaseFactory::set($this->rDb);
|
|
(new ReflectionProperty(EpgService::class, 'db'))->setValue(null, null);
|
|
$GLOBALS['db'] = $this->rDb;
|
|
$GLOBALS['rUserInfo'] = ['id' => 1, 'member_group_id' => 1];
|
|
$GLOBALS['rPermissions'] = ['is_admin' => 1, 'advanced' => []];
|
|
}
|
|
|
|
protected function tearDown(): void {
|
|
(new ReflectionProperty(EpgService::class, 'db'))->setValue(null, null);
|
|
DatabaseFactory::reset();
|
|
unset($GLOBALS['db'], $GLOBALS['rUserInfo'], $GLOBALS['rPermissions']);
|
|
}
|
|
|
|
private function rowCount(): int {
|
|
$this->rDb->query('SELECT COUNT(*) `c` FROM `epg`;');
|
|
return (int) $this->rDb->get_row()['c'];
|
|
}
|
|
|
|
private function nameOf(int $rId): ?string {
|
|
$this->rDb->query('SELECT `epg_name` FROM `epg` WHERE `id` = ?;', $rId);
|
|
return $this->rDb->get_row()['epg_name'] ?? null;
|
|
}
|
|
|
|
public function testCreateWithReservedNameIsRefused(): void {
|
|
$rBefore = $this->rowCount();
|
|
$rResult = EpgService::process(['epg_name' => 'Created Channels', 'epg_file' => 'x.xml']);
|
|
$this->assertSame(STATUS_FAILURE, $rResult['status']);
|
|
$this->assertSame($rBefore, $this->rowCount());
|
|
}
|
|
|
|
public function testCreateWithReservedNameDifferentCaseIsRefused(): void {
|
|
$rBefore = $this->rowCount();
|
|
$rResult = EpgService::process(['epg_name' => 'created CHANNELS', 'epg_file' => 'x.xml']);
|
|
$this->assertSame(STATUS_FAILURE, $rResult['status']);
|
|
$this->assertSame($rBefore, $this->rowCount());
|
|
}
|
|
|
|
public function testCreateWithReservedNameTrailingSpaceIsRefused(): void {
|
|
$rBefore = $this->rowCount();
|
|
$rResult = EpgService::process(['epg_name' => ' Created Channels ', 'epg_file' => 'x.xml']);
|
|
$this->assertSame(STATUS_FAILURE, $rResult['status']);
|
|
$this->assertSame($rBefore, $this->rowCount());
|
|
}
|
|
|
|
public function testEditRenamingAnotherSourceToReservedNameIsRefused(): void {
|
|
$rResult = EpgService::process(['edit' => 2, 'epg_name' => 'Created Channels']);
|
|
$this->assertSame(STATUS_FAILURE, $rResult['status']);
|
|
$this->assertSame('Normal', $this->nameOf(2));
|
|
}
|
|
|
|
public function testEditingTheReservedRowIsRefused(): void {
|
|
$rResult = EpgService::process(['edit' => CreatedChannelEpg::reservedSourceId(), 'epg_name' => 'Hijacked']);
|
|
$this->assertSame(STATUS_FAILURE, $rResult['status']);
|
|
$this->assertSame('Created Channels', $this->nameOf(1));
|
|
}
|
|
|
|
public function testAnOrdinaryCreateStillSucceeds(): void {
|
|
$rBefore = $this->rowCount();
|
|
$rResult = EpgService::process(['epg_name' => 'My Guide', 'epg_file' => 'http://example/epg.xml']);
|
|
$this->assertSame(STATUS_SUCCESS, $rResult['status']);
|
|
$this->assertSame($rBefore + 1, $this->rowCount());
|
|
}
|
|
}
|