feat: add four per-verb match action handlers
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BattleForge\Tests\Integration;
|
||||
|
||||
use BattleForge\Application\ScenarioSerializer;
|
||||
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\Handlers\PostMatchAbility;
|
||||
use BattleForge\Http\Handlers\PostMatchAttack;
|
||||
use BattleForge\Http\Handlers\PostMatchEndTurn;
|
||||
use BattleForge\Http\Handlers\PostMatchMove;
|
||||
use BattleForge\Http\Request;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class PostMatchActionTest extends TestCase
|
||||
{
|
||||
private const SECRET = 'match-action-test-secret';
|
||||
private const MATCH_ID = '0123456789abcdef';
|
||||
private const HEAL = ['heal'];
|
||||
|
||||
public function testMoveHappyPathReturnsTheUpdatedMatchAndAFreshTurnToken(): void
|
||||
{
|
||||
$match = $this->freshMatch();
|
||||
$token = $this->tokenFor($match);
|
||||
[$csrf, $cookie] = CsrfToken::issue(self::SECRET);
|
||||
|
||||
$response = (new PostMatchMove(self::SECRET))->handle(
|
||||
$this->buildRequest(
|
||||
match: $match,
|
||||
csrf: $csrf,
|
||||
turnToken: $token,
|
||||
cookies: ['__csrf' => $cookie],
|
||||
body: [
|
||||
'matchId' => self::MATCH_ID,
|
||||
'match' => ScenarioSerializer::matchToArray($match),
|
||||
'unitId' => 'alpha-1',
|
||||
'x' => 1,
|
||||
'y' => 0,
|
||||
],
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
self::assertSame(200, $response->status);
|
||||
$body = json_decode($response->body, true);
|
||||
$posKey = $body['match']['units'][0]['position']['x'] . ':' . $body['match']['units'][0]['position']['y'];
|
||||
self::assertSame('1:0', $posKey);
|
||||
self::assertMatchesRegularExpression('/^[a-f0-9]{32}$/', $body['turnToken']);
|
||||
self::assertNull($body['winnerTeamId']);
|
||||
}
|
||||
|
||||
public function testMoveRejectsAnUnreachableDestinationAndDoesNotMutateTheMatch(): void
|
||||
{
|
||||
$match = $this->freshMatch();
|
||||
$token = $this->tokenFor($match);
|
||||
[$csrf, $cookie] = CsrfToken::issue(self::SECRET);
|
||||
|
||||
$response = (new PostMatchMove(self::SECRET))->handle(
|
||||
$this->buildRequest(
|
||||
match: $match,
|
||||
csrf: $csrf,
|
||||
turnToken: $token,
|
||||
cookies: ['__csrf' => $cookie],
|
||||
body: [
|
||||
'matchId' => self::MATCH_ID,
|
||||
'match' => ScenarioSerializer::matchToArray($match),
|
||||
'unitId' => 'alpha-1',
|
||||
'x' => 0,
|
||||
'y' => 0,
|
||||
],
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
self::assertSame(409, $response->status);
|
||||
$body = json_decode($response->body, true);
|
||||
self::assertStringContainsString('not reachable', $body['error']);
|
||||
$posKey = $body['match']['units'][0]['position']['x'] . ':' . $body['match']['units'][0]['position']['y'];
|
||||
self::assertSame('0:0', $posKey);
|
||||
}
|
||||
|
||||
public function testAttackHappyPathReducesTargetHealth(): void
|
||||
{
|
||||
$match = $this->freshMatch();
|
||||
$matchArray = ScenarioSerializer::matchToArray($match);
|
||||
$matchArray['units'][0]['position'] = ['x' => 6, 'y' => 7];
|
||||
$match = ScenarioSerializer::matchFromArray($matchArray);
|
||||
$token = $this->tokenFor($match);
|
||||
[$csrf, $cookie] = CsrfToken::issue(self::SECRET);
|
||||
|
||||
$response = (new PostMatchAttack(self::SECRET))->handle(
|
||||
$this->buildRequest(
|
||||
match: $match,
|
||||
csrf: $csrf,
|
||||
turnToken: $token,
|
||||
cookies: ['__csrf' => $cookie],
|
||||
body: [
|
||||
'matchId' => self::MATCH_ID,
|
||||
'match' => ScenarioSerializer::matchToArray($match),
|
||||
'attackerId' => 'alpha-1',
|
||||
'targetId' => 'bravo-1',
|
||||
],
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
self::assertSame(200, $response->status);
|
||||
$body = json_decode($response->body, true);
|
||||
$bravo = null;
|
||||
foreach ($body['match']['units'] as $unit) {
|
||||
if ($unit['id'] === 'bravo-1') {
|
||||
$bravo = $unit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
self::assertNotNull($bravo);
|
||||
self::assertLessThan(10, $bravo['health']);
|
||||
}
|
||||
|
||||
public function testAttackRejectsAnOutOfRangeTarget(): void
|
||||
{
|
||||
$match = $this->freshMatch();
|
||||
$token = $this->tokenFor($match);
|
||||
[$csrf, $cookie] = CsrfToken::issue(self::SECRET);
|
||||
|
||||
$response = (new PostMatchAttack(self::SECRET))->handle(
|
||||
$this->buildRequest(
|
||||
match: $match,
|
||||
csrf: $csrf,
|
||||
turnToken: $token,
|
||||
cookies: ['__csrf' => $cookie],
|
||||
body: [
|
||||
'matchId' => self::MATCH_ID,
|
||||
'match' => ScenarioSerializer::matchToArray($match),
|
||||
'attackerId' => 'alpha-1',
|
||||
'targetId' => 'bravo-1',
|
||||
],
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
self::assertSame(409, $response->status);
|
||||
}
|
||||
|
||||
public function testAbilityHappyPathHealsAnAlly(): void
|
||||
{
|
||||
$match = $this->freshMatch();
|
||||
$matchArray = ScenarioSerializer::matchToArray($match);
|
||||
foreach ($matchArray['units'] as &$unit) {
|
||||
if ($unit['id'] === 'alpha-3') {
|
||||
$unit['archetype'] = 'support';
|
||||
$unit['abilities'] = ['heal'];
|
||||
}
|
||||
}
|
||||
unset($unit);
|
||||
foreach ($matchArray['units'] as &$unit) {
|
||||
if ($unit['id'] === 'alpha-2') {
|
||||
$unit['health'] = 2;
|
||||
}
|
||||
}
|
||||
unset($unit);
|
||||
$match = ScenarioSerializer::matchFromArray($matchArray);
|
||||
$token = $this->tokenFor($match);
|
||||
[$csrf, $cookie] = CsrfToken::issue(self::SECRET);
|
||||
|
||||
$response = (new PostMatchAbility(self::SECRET))->handle(
|
||||
$this->buildRequest(
|
||||
match: $match,
|
||||
csrf: $csrf,
|
||||
turnToken: $token,
|
||||
cookies: ['__csrf' => $cookie],
|
||||
body: [
|
||||
'matchId' => self::MATCH_ID,
|
||||
'match' => ScenarioSerializer::matchToArray($match),
|
||||
'unitId' => 'alpha-3',
|
||||
'abilityId' => 'heal',
|
||||
'x' => 0,
|
||||
'y' => 1,
|
||||
],
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
self::assertSame(200, $response->status);
|
||||
$body = json_decode($response->body, true);
|
||||
$healed = null;
|
||||
foreach ($body['match']['units'] as $unit) {
|
||||
if ($unit['id'] === 'alpha-2') {
|
||||
$healed = $unit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
self::assertGreaterThan(2, $healed['health']);
|
||||
}
|
||||
|
||||
public function testAbilityRejectsATargetOutsideRange(): void
|
||||
{
|
||||
$match = $this->freshMatch();
|
||||
$matchArray = ScenarioSerializer::matchToArray($match);
|
||||
foreach ($matchArray['units'] as &$unit) {
|
||||
if ($unit['id'] === 'alpha-3') {
|
||||
$unit['archetype'] = 'support';
|
||||
$unit['abilities'] = ['heal'];
|
||||
}
|
||||
}
|
||||
unset($unit);
|
||||
$match = ScenarioSerializer::matchFromArray($matchArray);
|
||||
$token = $this->tokenFor($match);
|
||||
[$csrf, $cookie] = CsrfToken::issue(self::SECRET);
|
||||
|
||||
$response = (new PostMatchAbility(self::SECRET))->handle(
|
||||
$this->buildRequest(
|
||||
match: $match,
|
||||
csrf: $csrf,
|
||||
turnToken: $token,
|
||||
cookies: ['__csrf' => $cookie],
|
||||
body: [
|
||||
'matchId' => self::MATCH_ID,
|
||||
'match' => ScenarioSerializer::matchToArray($match),
|
||||
'unitId' => 'alpha-3',
|
||||
'abilityId' => 'heal',
|
||||
'x' => 7,
|
||||
'y' => 7,
|
||||
],
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
self::assertSame(409, $response->status);
|
||||
}
|
||||
|
||||
public function testEndTurnHappyPathSwitchesTheActiveTeamAndIncrementsTheRoundAfterBothSides(): void
|
||||
{
|
||||
$match = $this->freshMatch();
|
||||
$token = $this->tokenFor($match);
|
||||
[$csrf, $cookie] = CsrfToken::issue(self::SECRET);
|
||||
|
||||
$response = (new PostMatchEndTurn(self::SECRET))->handle(
|
||||
$this->buildRequest(
|
||||
match: $match,
|
||||
csrf: $csrf,
|
||||
turnToken: $token,
|
||||
cookies: ['__csrf' => $cookie],
|
||||
body: [
|
||||
'matchId' => self::MATCH_ID,
|
||||
'match' => ScenarioSerializer::matchToArray($match),
|
||||
],
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
self::assertSame(200, $response->status);
|
||||
$body = json_decode($response->body, true);
|
||||
self::assertSame('bravo', $body['match']['activeTeamId']);
|
||||
self::assertSame(1, $body['match']['round']);
|
||||
}
|
||||
|
||||
public function testEndTurnRejectsARequestWithAStaleTurnToken(): void
|
||||
{
|
||||
$match = $this->freshMatch();
|
||||
$stale = TurnToken::issue(self::SECRET, self::MATCH_ID, 'alpha', 99, 0);
|
||||
$wrongRound = TurnToken::issue(self::SECRET, self::MATCH_ID, 'alpha', 2, 0);
|
||||
$good = $this->tokenFor($match);
|
||||
[$csrf, $cookie] = CsrfToken::issue(self::SECRET);
|
||||
|
||||
$staleResponse = (new PostMatchEndTurn(self::SECRET))->handle(
|
||||
$this->buildRequest(
|
||||
match: $match,
|
||||
csrf: $csrf,
|
||||
turnToken: $stale,
|
||||
cookies: ['__csrf' => $cookie],
|
||||
body: [
|
||||
'matchId' => self::MATCH_ID,
|
||||
'match' => ScenarioSerializer::matchToArray($match),
|
||||
],
|
||||
),
|
||||
[],
|
||||
);
|
||||
self::assertSame(409, $staleResponse->status);
|
||||
$body = json_decode($staleResponse->body, true);
|
||||
self::assertSame('turn', $body['error'] ?? null);
|
||||
|
||||
$wrongRoundResponse = (new PostMatchEndTurn(self::SECRET))->handle(
|
||||
$this->buildRequest(
|
||||
match: $match,
|
||||
csrf: $csrf,
|
||||
turnToken: $wrongRound,
|
||||
cookies: ['__csrf' => $cookie],
|
||||
body: [
|
||||
'matchId' => self::MATCH_ID,
|
||||
'match' => ScenarioSerializer::matchToArray($match),
|
||||
],
|
||||
),
|
||||
[],
|
||||
);
|
||||
self::assertSame(409, $wrongRoundResponse->status);
|
||||
}
|
||||
|
||||
private function freshMatch(): MatchState
|
||||
{
|
||||
$support = new UnitState(
|
||||
'alpha-3',
|
||||
'alpha',
|
||||
new Position(1, 1),
|
||||
10,
|
||||
10,
|
||||
3,
|
||||
3,
|
||||
2,
|
||||
2,
|
||||
false,
|
||||
Archetype::Support,
|
||||
self::HEAL,
|
||||
);
|
||||
|
||||
return new MatchState(
|
||||
new Battlefield(8, 8),
|
||||
[
|
||||
new UnitState('alpha-1', 'alpha', new Position(0, 0), 10, 10, 3, 3, 2, 2, false, Archetype::Defender),
|
||||
new UnitState('alpha-2', 'alpha', new Position(0, 1), 10, 10, 3, 3, 2, 2, false, Archetype::Defender),
|
||||
$support,
|
||||
new UnitState('bravo-1', 'bravo', new Position(7, 7), 10, 10, 3, 3, 2, 2, false, Archetype::Striker),
|
||||
new UnitState('bravo-2', 'bravo', new Position(6, 6), 10, 10, 3, 3, 2, 2, false, Archetype::Striker),
|
||||
new UnitState('bravo-3', 'bravo', new Position(5, 7), 10, 10, 3, 3, 2, 2, false, Archetype::Striker),
|
||||
],
|
||||
'alpha',
|
||||
);
|
||||
}
|
||||
|
||||
private function tokenFor(MatchState $match): string
|
||||
{
|
||||
return TurnToken::issue(
|
||||
self::SECRET,
|
||||
self::MATCH_ID,
|
||||
$match->activeTeamId,
|
||||
$match->round,
|
||||
count($match->actionLog),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $cookies
|
||||
* @param array<string, mixed> $body
|
||||
*/
|
||||
private function buildRequest(
|
||||
MatchState $match,
|
||||
string $csrf,
|
||||
string $turnToken,
|
||||
array $cookies,
|
||||
array $body
|
||||
): Request {
|
||||
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/test', 'application/json', json_encode($body, JSON_THROW_ON_ERROR));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user