From 1f9e76afef0d80211143a24e469e440f776eb26c Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Sat, 25 Jul 2026 23:37:27 -0500 Subject: [PATCH] feat: add GetBundledScenarios handler with manifest-driven list --- src/Http/Handlers/GetBundledScenarios.php | 51 +++++++++ tests/Integration/GetBundledScenariosTest.php | 102 ++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 src/Http/Handlers/GetBundledScenarios.php create mode 100644 tests/Integration/GetBundledScenariosTest.php diff --git a/src/Http/Handlers/GetBundledScenarios.php b/src/Http/Handlers/GetBundledScenarios.php new file mode 100644 index 0000000..56c157d --- /dev/null +++ b/src/Http/Handlers/GetBundledScenarios.php @@ -0,0 +1,51 @@ + $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); + } +} diff --git a/tests/Integration/GetBundledScenariosTest.php b/tests/Integration/GetBundledScenariosTest.php new file mode 100644 index 0000000..5a739f8 --- /dev/null +++ b/tests/Integration/GetBundledScenariosTest.php @@ -0,0 +1,102 @@ +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 $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); + } +}