feat: add MatchActionSupport for shared CSRF/turn/state verification
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BattleForge\Tests\Unit\Http;
|
||||
|
||||
use BattleForge\Application\TurnToken;
|
||||
use BattleForge\Domain\Archetype;
|
||||
use BattleForge\Domain\Battlefield;
|
||||
use BattleForge\Domain\MatchState;
|
||||
use BattleForge\Domain\Position;
|
||||
use BattleForge\Domain\UnitState;
|
||||
use BattleForge\Http\CsrfToken;
|
||||
use BattleForge\Http\MatchActionSupport;
|
||||
use BattleForge\Http\Request;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class MatchActionSupportTest extends TestCase
|
||||
{
|
||||
private const SECRET = 'support-test-secret';
|
||||
|
||||
public function testItRejectsARequestWithAMissingCsrfToken(): void
|
||||
{
|
||||
$support = new MatchActionSupport(self::SECRET);
|
||||
$request = $this->buildRequest(
|
||||
match: $this->validMatchArray(),
|
||||
csrf: '',
|
||||
turnToken: 'irrelevant',
|
||||
);
|
||||
|
||||
$result = $support->verify($request);
|
||||
|
||||
self::assertNotNull($result->response);
|
||||
self::assertSame(403, $result->response->status);
|
||||
self::assertNull($result->matchIdAndMatch);
|
||||
}
|
||||
|
||||
public function testItRejectsARequestWithABadTurnToken(): void
|
||||
{
|
||||
[$csrf, $cookie] = CsrfToken::issue(self::SECRET);
|
||||
$match = $this->validMatch();
|
||||
$matchId = '0123456789abcdef';
|
||||
$badToken = TurnToken::issue(self::SECRET, $matchId, 'bravo', 1, 0);
|
||||
|
||||
$support = new MatchActionSupport(self::SECRET);
|
||||
$request = $this->buildRequest(
|
||||
match: $this->validMatchArray(),
|
||||
csrf: $csrf,
|
||||
turnToken: $badToken,
|
||||
cookies: ['__csrf' => $cookie],
|
||||
);
|
||||
|
||||
$result = $support->verify($request);
|
||||
|
||||
self::assertNotNull($result->response);
|
||||
self::assertSame(409, $result->response->status);
|
||||
$body = json_decode($result->response->body, true);
|
||||
self::assertSame('turn', $body['error'] ?? null);
|
||||
}
|
||||
|
||||
public function testItRejectsAMalformedMatchState(): void
|
||||
{
|
||||
[$csrf, $cookie] = CsrfToken::issue(self::SECRET);
|
||||
$support = new MatchActionSupport(self::SECRET);
|
||||
$request = $this->buildRequest(
|
||||
match: ['this' => 'is not a match'],
|
||||
csrf: $csrf,
|
||||
turnToken: 'whatever',
|
||||
cookies: ['__csrf' => $cookie],
|
||||
);
|
||||
|
||||
$result = $support->verify($request);
|
||||
|
||||
self::assertNotNull($result->response);
|
||||
self::assertSame(400, $result->response->status);
|
||||
}
|
||||
|
||||
public function testItReturnsTheValidatedPairForAValidRequest(): void
|
||||
{
|
||||
$match = $this->validMatch();
|
||||
$matchArray = $this->validMatchArray();
|
||||
$matchId = '0123456789abcdef';
|
||||
$token = TurnToken::issue(
|
||||
self::SECRET,
|
||||
$matchId,
|
||||
$match->activeTeamId,
|
||||
$match->round,
|
||||
count($match->actionLog)
|
||||
);
|
||||
[$csrf, $cookie] = CsrfToken::issue(self::SECRET);
|
||||
|
||||
$support = new MatchActionSupport(self::SECRET);
|
||||
$body = json_encode(['matchId' => $matchId, 'match' => $matchArray], JSON_THROW_ON_ERROR);
|
||||
$request = new Request(
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
['__csrf' => $cookie],
|
||||
[
|
||||
'HTTP_X_CSRF_TOKEN' => $csrf,
|
||||
'HTTP_X_TURN_TOKEN' => $token,
|
||||
'__csrf_secret' => self::SECRET,
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
],
|
||||
'',
|
||||
'POST',
|
||||
'/matches/current/move',
|
||||
'application/json',
|
||||
$body
|
||||
);
|
||||
|
||||
$result = $support->verify($request);
|
||||
|
||||
self::assertNull($result->response);
|
||||
self::assertNotNull($result->matchIdAndMatch);
|
||||
self::assertSame($matchId, $result->matchIdAndMatch['matchId']);
|
||||
self::assertInstanceOf(MatchState::class, $result->matchIdAndMatch['match']);
|
||||
}
|
||||
|
||||
private function validMatch(): MatchState
|
||||
{
|
||||
return new MatchState(
|
||||
new Battlefield(8, 8),
|
||||
[
|
||||
$this->makeUnit('alpha-1', 'alpha', 0, 0, Archetype::Defender),
|
||||
$this->makeUnit('alpha-2', 'alpha', 1, 0, Archetype::Defender),
|
||||
$this->makeUnit('alpha-3', 'alpha', 2, 0, Archetype::Defender),
|
||||
$this->makeUnit('bravo-1', 'bravo', 7, 7, Archetype::Striker),
|
||||
$this->makeUnit('bravo-2', 'bravo', 6, 7, Archetype::Striker),
|
||||
$this->makeUnit('bravo-3', 'bravo', 5, 7, Archetype::Striker),
|
||||
],
|
||||
'alpha',
|
||||
);
|
||||
}
|
||||
|
||||
private function makeUnit(string $id, string $teamId, int $x, int $y, Archetype $archetype): UnitState
|
||||
{
|
||||
return new UnitState(
|
||||
$id,
|
||||
$teamId,
|
||||
new Position($x, $y),
|
||||
10,
|
||||
10,
|
||||
3,
|
||||
3,
|
||||
2,
|
||||
2,
|
||||
false,
|
||||
$archetype,
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function validMatchArray(): array
|
||||
{
|
||||
return \BattleForge\Application\ScenarioSerializer::matchToArray($this->validMatch());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $match
|
||||
* @param array<string, string> $cookies
|
||||
*/
|
||||
private function buildRequest(array $match, string $csrf, string $turnToken, array $cookies = []): Request
|
||||
{
|
||||
$body = json_encode(['matchId' => '0123456789abcdef', 'match' => $match], JSON_THROW_ON_ERROR);
|
||||
return new Request(
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
$cookies,
|
||||
[
|
||||
'HTTP_X_CSRF_TOKEN' => $csrf,
|
||||
'HTTP_X_TURN_TOKEN' => $turnToken,
|
||||
'__csrf_secret' => self::SECRET,
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
],
|
||||
'',
|
||||
'POST',
|
||||
'/matches/current/move',
|
||||
'application/json',
|
||||
$body
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user