229 lines
8.4 KiB
PHP
229 lines
8.4 KiB
PHP
<?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);
|
|
// Use a deterministic secret so the upload token HMAC matches between upload and fetch.
|
|
putenv('BATTLEFORGE_SECRET=' . self::SECRET);
|
|
|
|
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;
|
|
$_COOKIE['__csrf_token'] = $token;
|
|
$_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'] ?? []);
|
|
|
|
// Step 5: POST image upload, then GET the returned URL.
|
|
$this->uploadAndFetchAsset();
|
|
} finally {
|
|
// Always purge the per-install uploads dir after the test.
|
|
$uploadsRoot = __DIR__ . '/../../var/uploads';
|
|
if (is_dir($uploadsRoot)) {
|
|
foreach (glob($uploadsRoot . '/*/*') as $file) {
|
|
@unlink($file);
|
|
}
|
|
foreach (glob($uploadsRoot . '/*') as $dir) {
|
|
@rmdir($dir);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private function uploadAndFetchAsset(): void
|
|
{
|
|
$expectedToken = hash_hmac('sha256', self::SECRET, 'bf-uploads');
|
|
$expectedDir = realpath(__DIR__ . '/../../var/uploads') ?: (__DIR__ . '/../../var/uploads');
|
|
|
|
// Create the multipart body manually so we can pre-compute the file we expect to find.
|
|
$png = base64_decode(
|
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==',
|
|
true,
|
|
);
|
|
$tmp = tempnam(sys_get_temp_dir(), 'bf-upload-');
|
|
file_put_contents($tmp, $png);
|
|
|
|
try {
|
|
[$token, $cookie] = CsrfToken::issue(self::SECRET);
|
|
$_COOKIE['__csrf'] = $cookie;
|
|
$_COOKIE['__csrf_token'] = $token;
|
|
$_SERVER['REQUEST_METHOD'] = 'POST';
|
|
$_SERVER['REQUEST_URI'] = '/assets/upload';
|
|
$_SERVER['CONTENT_TYPE'] = 'multipart/form-data; boundary=----test';
|
|
$_SERVER['__csrf_secret'] = self::SECRET;
|
|
$_POST = ['_csrf' => $token];
|
|
$_FILES = [
|
|
'image' => [
|
|
'name' => 'test.png',
|
|
'type' => 'image/png',
|
|
'tmp_name' => $tmp,
|
|
'error' => 0,
|
|
'size' => strlen($png),
|
|
],
|
|
];
|
|
$this->rawBody = '';
|
|
$_SERVER['__raw_body'] = '';
|
|
$body = $this->runFrontController();
|
|
self::assertSame(200, $this->lastStatus, 'upload response: ' . $body);
|
|
$payload = json_decode($body, true);
|
|
$url = is_array($payload) ? (string) ($payload['url'] ?? '') : '';
|
|
self::assertNotSame('', $url);
|
|
self::assertStringStartsWith('/assets/uploads/' . $expectedToken . '/', $url);
|
|
|
|
// Now fetch the asset back through the front controller.
|
|
$_COOKIE = [];
|
|
$_POST = [];
|
|
$_FILES = [];
|
|
$_SERVER['REQUEST_METHOD'] = 'GET';
|
|
$_SERVER['REQUEST_URI'] = $url;
|
|
unset($_SERVER['CONTENT_TYPE'], $_SERVER['HTTP_X_CSRF_TOKEN'], $_SERVER['__raw_body']);
|
|
$this->rawBody = '';
|
|
$fetchBody = $this->runFrontController();
|
|
self::assertSame(200, $this->lastStatus, 'fetch response: ' . $fetchBody);
|
|
self::assertSame($png, $fetchBody);
|
|
|
|
// Clean up the uploaded file so the outer finally does not loop on it.
|
|
$filename = substr($url, strlen('/assets/uploads/' . $expectedToken . '/'));
|
|
@unlink($expectedDir . '/' . $expectedToken . '/' . $filename);
|
|
} finally {
|
|
@unlink($tmp);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|