mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-11-01 03:03:23 +01:00
Leads of the 2026-10-10 scan, re-read and confirmed: - transactional() over a DDL statement: the statement commits on its own, PDO then refused the commit and the rollback, so a module migration that had done its work threw "There is no active transaction", the callback's own error was hidden, and the handler stayed "in a transaction" for the rest of the process. commit() and rollback() leave the transaction first and ask PDO only for one that is still open. - A module updated at the second attempt stayed Failed, and unloaded. - A module archive is told apart by its content: a stored one is named .zip whatever it is and an upload has no extension, so a tar.gz update could not be extracted again or fetched by a load balancer, and a plain .tar upload failed. - A 404 page, a redirect's body or a page answered 200 was cached under an image's name and never fetched again: only a 2xx answer that is an image is cached. - A prebuffer shorter than a segment returned the whole playlist (array_slice(..., -0)). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
126 lines
3.7 KiB
PHP
126 lines
3.7 KiB
PHP
<?php
|
|
|
|
namespace XcVm\Tests\Unit;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use XcVm\Core\Database\DatabaseHandler;
|
|
|
|
/** A PDO stand-in that records what the handler asked of it. */
|
|
class TransactionRecorder {
|
|
public array $calls = [];
|
|
|
|
private bool $rOpen = false;
|
|
|
|
public function beginTransaction() {
|
|
$this->calls[] = 'begin';
|
|
$this->rOpen = true;
|
|
return true;
|
|
}
|
|
|
|
public function inTransaction(): bool {
|
|
return $this->rOpen;
|
|
}
|
|
|
|
public function commit() {
|
|
$this->calls[] = 'commit';
|
|
$this->rOpen = false;
|
|
return true;
|
|
}
|
|
|
|
public function rollBack() {
|
|
$this->calls[] = 'rollback';
|
|
$this->rOpen = false;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/** The production handler over a real connection (TestDb overrides the transaction methods). */
|
|
class RealPdoDb extends DatabaseHandler {
|
|
public function __construct(\PDO $rPDO) {
|
|
$this->dbh = $rPDO;
|
|
}
|
|
}
|
|
|
|
class TransactionalDb extends DatabaseHandler {
|
|
public function __construct(TransactionRecorder $rPDO) {
|
|
$this->dbh = $rPDO;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* transactional() must roll back whatever the callback throws. It caught only
|
|
* \Exception, so a PHP Error (a TypeError, an undefined method) skipped the
|
|
* rollback: the transaction stayed open on the connection and the handler
|
|
* still believed itself inside one, which also switched off reconnects.
|
|
*/
|
|
class DatabaseTransactionalTest extends TestCase {
|
|
public function testAnErrorInTheCallbackRollsBack(): void {
|
|
$rPDO = new TransactionRecorder();
|
|
$rDB = new TransactionalDb($rPDO);
|
|
|
|
try {
|
|
$rDB->transactional(function () {
|
|
throw new \TypeError('bad argument');
|
|
});
|
|
$this->assertTrue(false, 'the Error was swallowed');
|
|
} catch (\TypeError $e) {
|
|
$this->assertSame('bad argument', $e->getMessage());
|
|
}
|
|
|
|
$this->assertSame(['begin', 'rollback'], $rPDO->calls);
|
|
$this->assertFalse($rDB->isInTransaction());
|
|
}
|
|
|
|
public function testAnExceptionStillRollsBackAndSuccessCommits(): void {
|
|
$rPDO = new TransactionRecorder();
|
|
$rDB = new TransactionalDb($rPDO);
|
|
try {
|
|
$rDB->transactional(function () {
|
|
throw new \RuntimeException('no');
|
|
});
|
|
} catch (\RuntimeException $e) {
|
|
}
|
|
$this->assertSame(7, $rDB->transactional(fn() => 7));
|
|
$this->assertSame(['begin', 'rollback', 'begin', 'commit'], $rPDO->calls);
|
|
}
|
|
|
|
/**
|
|
* A DDL statement commits the transaction on its own (a module migration
|
|
* runs in transactional()). PDO then refuses the commit, and the rollback
|
|
* that followed: the callback's work was done, yet transactional() threw
|
|
* "There is no active transaction" and the handler stayed in a transaction.
|
|
*/
|
|
public function testAStatementThatCommitsOnItsOwnLeavesTheHandlerUsable(): void {
|
|
$rTest = new \TestDb();
|
|
$rDB = new RealPdoDb($rTest->pdo);
|
|
|
|
$this->assertSame('done', $rDB->transactional(function () use ($rTest) {
|
|
$rTest->pdo->exec('CREATE TABLE `ddl_in_tx` (`id` int)');
|
|
return 'done';
|
|
}));
|
|
$this->assertFalse($rDB->isInTransaction());
|
|
|
|
// The error the callback threw is the one the caller sees.
|
|
try {
|
|
$rDB->transactional(function () use ($rTest) {
|
|
$rTest->pdo->exec('ALTER TABLE `ddl_in_tx` ADD COLUMN `more` int');
|
|
throw new \RuntimeException('the real cause');
|
|
});
|
|
$this->fail('the callback\'s error was swallowed');
|
|
} catch (\RuntimeException $e) {
|
|
$this->assertSame('the real cause', $e->getMessage());
|
|
}
|
|
$this->assertFalse($rDB->isInTransaction());
|
|
|
|
// And the next transaction is one: its rollback undoes its row.
|
|
try {
|
|
$rDB->transactional(function () use ($rTest) {
|
|
$rTest->pdo->exec('INSERT INTO `ddl_in_tx` (`id`) VALUES (1)');
|
|
throw new \RuntimeException('undo');
|
|
});
|
|
} catch (\RuntimeException) {
|
|
}
|
|
$this->assertSame(0, (int) $rTest->pdo->query('SELECT COUNT(*) FROM `ddl_in_tx`')->fetchColumn());
|
|
}
|
|
}
|