test: cover full create-and-save flow end-to-end
Add FullFlowTest, a single end-to-end test that exercises the home page, the team editor POST, the battlefield editor POST, and the start-match POST through the front controller (no php -S). Wiring fixes in public/index.php (called out in the test brief): - Honor $_SERVER['__csrf_secret'] when set so the test can supply its own secret; fall back to BATTLEFORGE_SECRET / var/secret.key otherwise. - Read the raw body from $_SERVER['__raw_body'] when set; fall back to php://input otherwise. PHP CLI's php://input is empty, so an explicit hook is needed for JSON bodies. - Return the response status from the front controller so the test can assert the HTTP status it set via http_response_code(). Test fixes: - Drop the static keyword on the runFrontController closure; static forbids $this access and the closure must capture $this to record lastStatus. - Drop the redundant '?? ''' on the ob_get_clean() result; (string) ob_get_clean() is always a string. - Set $_SERVER['__raw_body'] alongside $this->rawBody for the JSON requests so the front controller can read the body.
This commit is contained in:
+6
-2
@@ -70,11 +70,13 @@ if ($contentType !== null) {
|
||||
}
|
||||
|
||||
// 5. Read the raw body for fetch POSTs that submit JSON.
|
||||
$rawBody = (string) file_get_contents('php://input');
|
||||
$rawBody = (string) ($_SERVER['__raw_body'] ?? file_get_contents('php://input'));
|
||||
|
||||
// 6. Build a Request with the request data, the CSRF secret, and the upload token.
|
||||
$server = $_SERVER;
|
||||
$server['__csrf_secret'] = $secret;
|
||||
if (!isset($server['__csrf_secret'])) {
|
||||
$server['__csrf_secret'] = $secret;
|
||||
}
|
||||
$server['__uploads_token'] = $uploadsToken;
|
||||
|
||||
$request = new Request(
|
||||
@@ -123,3 +125,5 @@ foreach ($response->headers as $name => $value) {
|
||||
header($name . ': ' . $value);
|
||||
}
|
||||
echo $response->body;
|
||||
|
||||
return $response->status;
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BattleForge\Tests\Integration;
|
||||
|
||||
use BattleForge\Http\CsrfToken;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class FullFlowTest extends TestCase
|
||||
{
|
||||
private const SECRET = 'unit-test-secret';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
// Reset superglobals between requests.
|
||||
$_GET = [];
|
||||
$_POST = [];
|
||||
$_FILES = [];
|
||||
$_COOKIE = [];
|
||||
$_SERVER = [];
|
||||
}
|
||||
|
||||
public function testTheFullCreateAndSaveFlow(): void
|
||||
{
|
||||
[$token, $cookie] = CsrfToken::issue(self::SECRET);
|
||||
$uploadsRoot = sys_get_temp_dir() . '/bf-fullflow-' . bin2hex(random_bytes(4));
|
||||
mkdir($uploadsRoot, 0700, true);
|
||||
|
||||
try {
|
||||
// Step 1: GET home page.
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_SERVER['REQUEST_URI'] = '/';
|
||||
$homeBody = $this->runFrontController();
|
||||
self::assertStringContainsString('New scenario', $homeBody);
|
||||
self::assertStringContainsString('name="csrf-token"', $homeBody);
|
||||
|
||||
// Step 2: POST team editor.
|
||||
$_COOKIE['__csrf'] = $cookie;
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
$_SERVER['REQUEST_URI'] = '/scenarios/demo/edit/team';
|
||||
$_SERVER['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
|
||||
$_SERVER['__csrf_secret'] = self::SECRET;
|
||||
$_POST = $this->validTeamPost($token);
|
||||
$teamBody = $this->runFrontController();
|
||||
self::assertSame(200, $this->lastStatus);
|
||||
self::assertStringContainsString('localStorage.setItem', $teamBody);
|
||||
self::assertStringContainsString('scenario:demo', $teamBody);
|
||||
|
||||
// Step 3: POST battlefield editor.
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
$_SERVER['REQUEST_URI'] = '/scenarios/demo/edit/battlefield';
|
||||
$_SERVER['CONTENT_TYPE'] = 'application/json';
|
||||
$_SERVER['HTTP_X_CSRF_TOKEN'] = $token;
|
||||
$_POST = [];
|
||||
$this->rawBody = json_encode($this->validBattlefieldPayload(), JSON_THROW_ON_ERROR);
|
||||
$_SERVER['__raw_body'] = $this->rawBody;
|
||||
$bfBody = $this->runFrontController();
|
||||
self::assertSame(200, $this->lastStatus);
|
||||
$bfJson = json_decode($bfBody, true);
|
||||
self::assertSame(true, $bfJson['ok'] ?? null);
|
||||
|
||||
// Step 4: POST start-match.
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
$_SERVER['REQUEST_URI'] = '/scenarios/demo/start';
|
||||
$this->rawBody = json_encode($this->validBattlefieldPayload(), JSON_THROW_ON_ERROR);
|
||||
$_SERVER['__raw_body'] = $this->rawBody;
|
||||
$startBody = $this->runFrontController();
|
||||
self::assertSame(200, $this->lastStatus);
|
||||
$startJson = json_decode($startBody, true);
|
||||
self::assertSame('alpha', $startJson['match']['activeTeamId'] ?? null);
|
||||
self::assertSame(1, $startJson['match']['round'] ?? null);
|
||||
self::assertCount(6, $startJson['match']['units'] ?? []);
|
||||
} finally {
|
||||
if (is_dir($uploadsRoot)) {
|
||||
foreach (glob($uploadsRoot . '/*/*') as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
foreach (glob($uploadsRoot . '/*') as $dir) {
|
||||
rmdir($dir);
|
||||
}
|
||||
rmdir($uploadsRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string $rawBody = '';
|
||||
private int $lastStatus = 0;
|
||||
|
||||
private function runFrontController(): string
|
||||
{
|
||||
$this->lastStatus = 0;
|
||||
$body = $this->captureOutput(function (): void {
|
||||
$this->lastStatus = (require __DIR__ . '/../../public/index.php') ?? http_response_code();
|
||||
});
|
||||
return $body;
|
||||
}
|
||||
|
||||
/** @param callable(): void $fn */
|
||||
private function captureOutput(callable $fn): string
|
||||
{
|
||||
ob_start();
|
||||
try {
|
||||
$fn();
|
||||
} finally {
|
||||
$output = (string) ob_get_clean();
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function validTeamPost(string $token): array
|
||||
{
|
||||
return [
|
||||
'_csrf' => $token,
|
||||
'id' => 'demo',
|
||||
'name' => 'Demo',
|
||||
'battlefieldWidth' => '8',
|
||||
'battlefieldHeight' => '8',
|
||||
'teamA' => ['units' => $this->unitRows('a', 0, 2, 0)],
|
||||
'teamB' => ['units' => $this->unitRows('b', 5, 7, 7)],
|
||||
'victoryCondition' => 'eliminate_all',
|
||||
'holdRoundsRequired' => '1',
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function validBattlefieldPayload(): array
|
||||
{
|
||||
return [
|
||||
'id' => 'demo',
|
||||
'name' => 'Demo',
|
||||
'battlefieldWidth' => 8,
|
||||
'battlefieldHeight' => 8,
|
||||
'battlefieldTerrain' => [],
|
||||
'teamA' => ['units' => $this->unitRows('a', 0, 2, 0)],
|
||||
'teamB' => ['units' => $this->unitRows('b', 5, 7, 7)],
|
||||
'victoryCondition' => 'eliminate_all',
|
||||
'holdRoundsRequired' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, string|int>>
|
||||
*/
|
||||
private function unitRows(string $teamPrefix, int $xStart, int $xEnd, int $y): array
|
||||
{
|
||||
$rows = [];
|
||||
for ($i = 0; $i <= $xEnd - $xStart; $i++) {
|
||||
$rows[] = [
|
||||
'id' => "{$teamPrefix}{$i}",
|
||||
'archetype' => 'defender',
|
||||
'maxHealth' => 12,
|
||||
'attack' => 3,
|
||||
'defense' => 4,
|
||||
'speed' => 2,
|
||||
'x' => $xStart + $i,
|
||||
'y' => $y,
|
||||
];
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user