236 lines
8.0 KiB
PHP
236 lines
8.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Plugin\AzuraCastOnDemandHls\Tests\Service;
|
|
|
|
use PHPUnit\Framework\Attributes\CoversClass;
|
|
use Plugin\AzuraCastOnDemandHls\Config;
|
|
use Plugin\AzuraCastOnDemandHls\Domain\PlaybackSession;
|
|
use Plugin\AzuraCastOnDemandHls\Security\OpaqueToken;
|
|
use Plugin\AzuraCastOnDemandHls\Service\CachePaths;
|
|
use Plugin\AzuraCastOnDemandHls\Service\SessionRepository;
|
|
use Plugin\AzuraCastOnDemandHls\Tests\Support\TemporaryDirectoryTestCase;
|
|
use RuntimeException;
|
|
use Symfony\Component\Process\Process;
|
|
|
|
#[CoversClass(SessionRepository::class)]
|
|
#[CoversClass(PlaybackSession::class)]
|
|
final class SessionRepositoryTest extends TemporaryDirectoryTestCase
|
|
{
|
|
public function testValidSessionCreationAndLookup(): void
|
|
{
|
|
$repository = $this->repository();
|
|
$created = $repository->create(
|
|
$this->temporaryDirectory,
|
|
7,
|
|
'abcdef123456abcdef123456',
|
|
str_repeat('b', 64),
|
|
str_repeat('c', 64),
|
|
1000,
|
|
);
|
|
|
|
self::assertTrue(OpaqueToken::isValid($created['token']));
|
|
self::assertSame(1060, $created['session']->expiresAt);
|
|
self::assertEquals($created['session'], $repository->find($this->temporaryDirectory, $created['token']));
|
|
}
|
|
|
|
public function testInvalidAndModifiedTokensCannotLoadSession(): void
|
|
{
|
|
$repository = $this->repository();
|
|
$created = $repository->create(
|
|
$this->temporaryDirectory,
|
|
7,
|
|
'abcdef123456abcdef123456',
|
|
str_repeat('b', 64),
|
|
str_repeat('c', 64),
|
|
1000,
|
|
);
|
|
$token = $created['token'];
|
|
$mutated = ('A' === $token[0] ? 'B' : 'A') . substr($token, 1);
|
|
|
|
self::assertNull($repository->find($this->temporaryDirectory, 'invalid'));
|
|
self::assertNull($repository->find($this->temporaryDirectory, $mutated));
|
|
}
|
|
|
|
public function testConcurrentSessionLimitIsEnforced(): void
|
|
{
|
|
$repository = $this->repository(maxSessions: 2);
|
|
for ($i = 0; $i < 2; ++$i) {
|
|
$repository->create(
|
|
$this->temporaryDirectory,
|
|
7,
|
|
'abcdef123456abcdef123456',
|
|
hash('sha256', 'cache-' . $i),
|
|
str_repeat('c', 64),
|
|
1000,
|
|
);
|
|
}
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
$this->expectExceptionMessage('Maximum concurrent playback sessions reached.');
|
|
$repository->create(
|
|
$this->temporaryDirectory,
|
|
7,
|
|
'abcdef123456abcdef123456',
|
|
str_repeat('d', 64),
|
|
str_repeat('c', 64),
|
|
1000,
|
|
);
|
|
}
|
|
|
|
public function testDuplicateUrlsForTheSameRenditionUseOnePlaybackSlot(): void
|
|
{
|
|
$repository = $this->repository(maxSessions: 1);
|
|
$principalHash = str_repeat('c', 64);
|
|
$cacheKey = str_repeat('b', 64);
|
|
|
|
$first = $repository->create(
|
|
$this->temporaryDirectory,
|
|
7,
|
|
'abcdef123456abcdef123456',
|
|
$cacheKey,
|
|
$principalHash,
|
|
1000,
|
|
);
|
|
$second = $repository->create(
|
|
$this->temporaryDirectory,
|
|
7,
|
|
'abcdef123456abcdef123456',
|
|
$cacheKey,
|
|
$principalHash,
|
|
1001,
|
|
);
|
|
|
|
self::assertNotSame($first['token'], $second['token']);
|
|
self::assertSame(1, $repository->countActiveForPrincipal($this->temporaryDirectory, $principalHash, 1001));
|
|
}
|
|
|
|
public function testInactiveSessionsDoNotConsumePlaybackSlots(): void
|
|
{
|
|
$config = new Config(sessionTtl: 1800, cacheTtl: 1800, maxSessionsPerPrincipal: 1);
|
|
$repository = new SessionRepository($config, new CachePaths($config));
|
|
$principalHash = str_repeat('c', 64);
|
|
$repository->create(
|
|
$this->temporaryDirectory,
|
|
7,
|
|
'abcdef123456abcdef123456',
|
|
str_repeat('b', 64),
|
|
$principalHash,
|
|
1000,
|
|
);
|
|
|
|
self::assertSame(0, $repository->countActiveForPrincipal($this->temporaryDirectory, $principalHash, 1091));
|
|
$created = $repository->create(
|
|
$this->temporaryDirectory,
|
|
7,
|
|
'abcdef123456abcdef123457',
|
|
str_repeat('d', 64),
|
|
$principalHash,
|
|
1091,
|
|
);
|
|
self::assertTrue(OpaqueToken::isValid($created['token']));
|
|
}
|
|
|
|
public function testAuthorizedAssetAccessKeepsAPlaybackSlotActive(): void
|
|
{
|
|
$config = new Config(sessionTtl: 1800, cacheTtl: 1800, maxSessionsPerPrincipal: 1);
|
|
$repository = new SessionRepository($config, new CachePaths($config));
|
|
$principalHash = str_repeat('c', 64);
|
|
$created = $repository->create(
|
|
$this->temporaryDirectory,
|
|
7,
|
|
'abcdef123456abcdef123456',
|
|
str_repeat('b', 64),
|
|
$principalHash,
|
|
1000,
|
|
);
|
|
|
|
$repository->markAccessed($this->temporaryDirectory, $created['token'], 1080);
|
|
|
|
self::assertSame(1, $repository->countActiveForPrincipal($this->temporaryDirectory, $principalHash, 1169));
|
|
self::assertSame(0, $repository->countActiveForPrincipal($this->temporaryDirectory, $principalHash, 1171));
|
|
}
|
|
|
|
public function testParallelCreatorsCannotExceedSessionLimit(): void
|
|
{
|
|
$worker = $this->temporaryDirectory . '/session-worker.php';
|
|
$autoload = dirname(__DIR__, 2) . '/vendor/autoload.php';
|
|
file_put_contents($worker, <<<'PHP'
|
|
<?php
|
|
declare(strict_types=1);
|
|
|
|
require $argv[1];
|
|
|
|
use Plugin\AzuraCastOnDemandHls\Config;
|
|
use Plugin\AzuraCastOnDemandHls\Exception\SessionLimitException;
|
|
use Plugin\AzuraCastOnDemandHls\Service\CachePaths;
|
|
use Plugin\AzuraCastOnDemandHls\Service\SessionRepository;
|
|
|
|
$config = new Config(sessionTtl: 60, cacheTtl: 60, maxSessionsPerPrincipal: 1);
|
|
$repository = new SessionRepository($config, new CachePaths($config));
|
|
touch($argv[5] . '/ready-' . $argv[6]);
|
|
while (!is_file($argv[4])) {
|
|
usleep(1000);
|
|
}
|
|
|
|
try {
|
|
$repository->create(
|
|
$argv[2],
|
|
7,
|
|
'abcdef123456abcdef123456',
|
|
hash('sha256', 'cache-' . $argv[6]),
|
|
str_repeat('c', 64),
|
|
1000,
|
|
);
|
|
echo 'created';
|
|
} catch (SessionLimitException) {
|
|
echo 'limited';
|
|
}
|
|
PHP);
|
|
|
|
$marker = $this->temporaryDirectory . '/start';
|
|
$processes = [];
|
|
foreach (['one', 'two'] as $id) {
|
|
$process = new Process([
|
|
PHP_BINARY,
|
|
$worker,
|
|
$autoload,
|
|
$this->temporaryDirectory,
|
|
$marker,
|
|
$this->temporaryDirectory,
|
|
$id,
|
|
]);
|
|
$process->start();
|
|
$processes[] = $process;
|
|
}
|
|
|
|
$deadline = microtime(true) + 5;
|
|
while ((!is_file($this->temporaryDirectory . '/ready-one')
|
|
|| !is_file($this->temporaryDirectory . '/ready-two'))
|
|
&& microtime(true) < $deadline) {
|
|
usleep(1000);
|
|
}
|
|
self::assertFileExists($this->temporaryDirectory . '/ready-one');
|
|
self::assertFileExists($this->temporaryDirectory . '/ready-two');
|
|
touch($marker);
|
|
|
|
$results = [];
|
|
foreach ($processes as $process) {
|
|
self::assertSame(0, $process->wait(), $process->getErrorOutput());
|
|
$results[] = trim($process->getOutput());
|
|
}
|
|
sort($results);
|
|
self::assertSame(['created', 'limited'], $results);
|
|
|
|
$sessionFiles = glob($this->temporaryDirectory . '/ondemand-hls/sessions/*.json') ?: [];
|
|
self::assertCount(1, $sessionFiles);
|
|
}
|
|
|
|
private function repository(int $maxSessions = 10): SessionRepository
|
|
{
|
|
$config = new Config(sessionTtl: 60, cacheTtl: 60, maxSessionsPerPrincipal: $maxSessions);
|
|
return new SessionRepository($config, new CachePaths($config));
|
|
}
|
|
}
|