feat: add GetBundledScenarios handler with manifest-driven list
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace BattleForge\Http\Handlers;
|
||||||
|
|
||||||
|
use BattleForge\Http\Request;
|
||||||
|
use BattleForge\Http\Response;
|
||||||
|
|
||||||
|
final class GetBundledScenarios
|
||||||
|
{
|
||||||
|
private const FILENAME_PATTERN = '/^[a-z0-9-]+\.json$/';
|
||||||
|
|
||||||
|
public function __construct(private readonly string $scenariosDir)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, string> $params */
|
||||||
|
public function handle(Request $request, array $params): Response
|
||||||
|
{
|
||||||
|
$manifestPath = $this->scenariosDir . '/manifest.json';
|
||||||
|
$manifest = [];
|
||||||
|
if (is_file($manifestPath)) {
|
||||||
|
$decoded = json_decode((string) file_get_contents($manifestPath), true);
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
$manifest = $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$entries = [];
|
||||||
|
foreach (scandir($this->scenariosDir) ?: [] as $file) {
|
||||||
|
if (!preg_match(self::FILENAME_PATTERN, $file)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$id = substr($file, 0, -5);
|
||||||
|
$entry = $manifest[$id] ?? null;
|
||||||
|
if (!is_array($entry) || !isset($entry['name'], $entry['summary'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$entries[] = [
|
||||||
|
'id' => $id,
|
||||||
|
'name' => (string) $entry['name'],
|
||||||
|
'summary' => (string) $entry['summary'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($entries, static fn (array $a, array $b): int => strcmp($a['id'], $b['id']));
|
||||||
|
|
||||||
|
return Response::json(200, $entries);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace BattleForge\Tests\Integration;
|
||||||
|
|
||||||
|
use BattleForge\Http\Handlers\GetBundledScenarios;
|
||||||
|
use BattleForge\Http\Request;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class GetBundledScenariosTest extends TestCase
|
||||||
|
{
|
||||||
|
private string $tmpDir;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->tmpDir = sys_get_temp_dir() . '/bf-bundled-' . bin2hex(random_bytes(4));
|
||||||
|
mkdir($this->tmpDir, 0700, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
if (is_dir($this->tmpDir)) {
|
||||||
|
foreach (glob($this->tmpDir . '/*') ?: [] as $file) {
|
||||||
|
unlink($file);
|
||||||
|
}
|
||||||
|
rmdir($this->tmpDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReturnsBundledScenariosFromTheConfiguredDirectory(): void
|
||||||
|
{
|
||||||
|
$this->writeManifest([
|
||||||
|
'skirmish' => ['name' => 'Skirmish', 'summary' => 'Open ground.'],
|
||||||
|
'hold-the-line' => ['name' => 'Hold the Line', 'summary' => 'Defend.'],
|
||||||
|
]);
|
||||||
|
$this->writeFixture('skirmish', '{}');
|
||||||
|
$this->writeFixture('hold-the-line', '{}');
|
||||||
|
|
||||||
|
$handler = new GetBundledScenarios($this->tmpDir);
|
||||||
|
$response = $handler->handle(new Request([], [], [], [], [], '', 'GET', '/scenarios/bundled', null, ''), []);
|
||||||
|
|
||||||
|
self::assertSame(200, $response->status);
|
||||||
|
$body = json_decode($response->body, true);
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
['id' => 'hold-the-line', 'name' => 'Hold the Line', 'summary' => 'Defend.'],
|
||||||
|
['id' => 'skirmish', 'name' => 'Skirmish', 'summary' => 'Open ground.'],
|
||||||
|
],
|
||||||
|
$body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFiltersOutFixturesWithoutAManifestEntry(): void
|
||||||
|
{
|
||||||
|
$this->writeManifest(['skirmish' => ['name' => 'Skirmish', 'summary' => 'Open ground.']]);
|
||||||
|
$this->writeFixture('skirmish', '{}');
|
||||||
|
$this->writeFixture('rogue', '{}');
|
||||||
|
|
||||||
|
$handler = new GetBundledScenarios($this->tmpDir);
|
||||||
|
$response = $handler->handle(new Request([], [], [], [], [], '', 'GET', '/scenarios/bundled', null, ''), []);
|
||||||
|
|
||||||
|
$body = json_decode($response->body, true);
|
||||||
|
self::assertCount(1, $body);
|
||||||
|
self::assertSame('skirmish', $body[0]['id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIgnoresFilesWithDisallowedFilenames(): void
|
||||||
|
{
|
||||||
|
$this->writeManifest(['skirmish' => ['name' => 'Skirmish', 'summary' => 'Open ground.']]);
|
||||||
|
$this->writeFixture('skirmish', '{}');
|
||||||
|
$this->writeFixture('bad..json', '{}');
|
||||||
|
$this->writeFixture('README', 'plain text');
|
||||||
|
|
||||||
|
$handler = new GetBundledScenarios($this->tmpDir);
|
||||||
|
$response = $handler->handle(new Request([], [], [], [], [], '', 'GET', '/scenarios/bundled', null, ''), []);
|
||||||
|
|
||||||
|
$body = json_decode($response->body, true);
|
||||||
|
self::assertCount(1, $body);
|
||||||
|
self::assertSame('skirmish', $body[0]['id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReturnsEmptyArrayWhenManifestIsMissing(): void
|
||||||
|
{
|
||||||
|
$handler = new GetBundledScenarios($this->tmpDir);
|
||||||
|
$response = $handler->handle(new Request([], [], [], [], [], '', 'GET', '/scenarios/bundled', null, ''), []);
|
||||||
|
|
||||||
|
self::assertSame(200, $response->status);
|
||||||
|
self::assertSame([], json_decode($response->body, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, array{name: string, summary: string}> $entries */
|
||||||
|
private function writeManifest(array $entries): void
|
||||||
|
{
|
||||||
|
file_put_contents($this->tmpDir . '/manifest.json', json_encode($entries, JSON_THROW_ON_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writeFixture(string $name, string $body): void
|
||||||
|
{
|
||||||
|
file_put_contents($this->tmpDir . '/' . $name . '.json', $body);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user