Files
XC_VM/tests/Unit/SignalQueueTest.php
T
Divarion_D 2f8b36d1f3 refactor(signal): unify deferred-work signals behind SignalQueue
Signals — the file-backed queue of deferred DB writes drained by the
cache-handler daemon — were produced two different ways: five call sites went
through RedisManager::setSignal() while getStreamingUserInfo inlined the same
file_put_contents(). Worse, the writer lived on RedisManager yet wrote a *file*,
not Redis — a misleading home that made the two paths look like different
transports (they were byte-identical).

Introduce XcVm\Infrastructure\Signal\SignalQueue as the single owner of the
on-disk contract (cache_<md5(key)> holding [key, data]):
- push()    - the one producer API; all 7 call sites use it now
  (UserRepository x4, auth.php x2, BruteforceGuard x1)
- pending() - the drain API; CacheHandlerCommand reads through it instead of its
  own glob + json_decode
- pathFor() / PREFIX - path helpers

RedisManager::setSignal() stays as a thin @deprecated alias delegating to
SignalQueue::push() for any out-of-tree callers.

Behaviour unchanged (same files, same format, same consumer switch). Adds
SignalQueueTest (format / idempotency / drain / malformed). Validated live:
player_api/testxc still authenticates (auth=1) with SignalQueue autoloaded.

Verified: php -l, phpunit (435 tests, 973 assertions), make gates.
2026-08-09 13:01:57 +03:00

68 lines
2.3 KiB
PHP

<?php
use PHPUnit\Framework\TestCase;
use XcVm\Infrastructure\Signal\SignalQueue;
/**
* Unit tests for SignalQueue — the file-backed deferred-work queue that
* producers (auth / ISP / forced_country / flood) push to and the cache-handler
* daemon drains. Locks the on-disk contract (cache_<md5(key)> holding
* [key, data]) that the consumer's switch depends on.
*/
final class SignalQueueTest extends TestCase {
public static function setUpBeforeClass(): void {
if (!defined('SIGNALS_TMP_PATH')) {
define('SIGNALS_TMP_PATH', sys_get_temp_dir() . '/xcvm-sig-test/');
}
if (!is_dir(SIGNALS_TMP_PATH)) {
mkdir(SIGNALS_TMP_PATH, 0777, true);
}
}
protected function setUp(): void {
foreach (glob(SIGNALS_TMP_PATH . 'cache_*') ?: array() as $rFile) {
unlink($rFile);
}
}
public function testPushWritesCanonicalFileFormat(): void {
SignalQueue::push('isp/42', json_encode(array('Comcast', 7922)));
$rPath = SIGNALS_TMP_PATH . 'cache_' . md5('isp/42');
$this->assertFileExists($rPath);
$this->assertSame(array('isp/42', json_encode(array('Comcast', 7922))), json_decode(file_get_contents($rPath), true));
}
public function testPathForMatchesPushLocation(): void {
$this->assertSame(SIGNALS_TMP_PATH . 'cache_' . md5('forced_country/5'), SignalQueue::pathFor('forced_country/5'));
}
public function testPushIsIdempotentPerKey(): void {
SignalQueue::push('expiring/9', 100);
SignalQueue::push('expiring/9', 200); // same key overwrites the pending record
$rPending = SignalQueue::pending();
$this->assertCount(1, $rPending);
$this->assertSame(200, $rPending[0][2]);
}
public function testPendingReturnsFileKeyDataTuples(): void {
SignalQueue::push('forced_country/1', 'DE');
SignalQueue::push('isp/2', json_encode(array('ISP', 1)));
$rByKey = array();
foreach (SignalQueue::pending() as list($rFile, $rKey, $rData)) {
$this->assertFileExists($rFile);
$rByKey[$rKey] = $rData;
}
$this->assertSame('DE', $rByKey['forced_country/1']);
$this->assertSame(json_encode(array('ISP', 1)), $rByKey['isp/2']);
}
public function testPendingSkipsMalformedRecords(): void {
file_put_contents(SIGNALS_TMP_PATH . 'cache_bad', 'not-json');
SignalQueue::push('isp/3', 1);
$rPending = SignalQueue::pending();
$this->assertCount(1, $rPending);
$this->assertSame('isp/3', $rPending[0][1]);
}
}