367 lines
13 KiB
PHP
367 lines
13 KiB
PHP
<?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, '__csrf_token' => $token],
|
|
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, '__csrf_token' => $token],
|
|
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, '__csrf_token' => $token],
|
|
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, '__csrf_token' => $token],
|
|
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, '__csrf_token' => $token],
|
|
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, '__csrf_token' => $token],
|
|
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, '__csrf_token' => $token],
|
|
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, '__csrf_token' => $token],
|
|
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, '__csrf_token' => $token],
|
|
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));
|
|
}
|
|
}
|