Added the tournament routes and tests

Hecht 8 months ago
parent 6ba77d3cec
commit f5819870bc

@ -143,14 +143,22 @@
<resource class="App\Entity\TournamentRegistration"> <resource class="App\Entity\TournamentRegistration">
<operations> <operations>
<operation class="ApiPlatform\Metadata\Post" /> <operation class="ApiPlatform\Metadata\Post" processor="App\State\TournamentRegistrationStateProcessor"/>
</operations> </operations>
</resource> </resource>
<resource class="App\Entity\Fight"> <resource class="App\Entity\Fight">
<operations> <operations>
<operation class="ApiPlatform\Metadata\Get" /> <operation class="ApiPlatform\Metadata\Get" />
<operation class="ApiPlatform\Metadata\GetCollection" controller="App\Controller\GetTournamentFights" uriTemplate="tournament/{tournamentId}/fights" description="Receives the fights from a tournament."/> <operation class="ApiPlatform\Metadata\GetCollection" controller="App\Controller\GetTournamentFights" uriTemplate="tournaments/{tournamentId}/fights" description="Receives the fights from a tournament.">
<uriVariables><uriVariable parameterName="tournamentId" fromClass="App\Entity\Tournament"></uriVariable></uriVariables>
</operation>
<operation class="ApiPlatform\Metadata\GetCollection" controller="App\Controller\GetTournamentFights" uriTemplate="tournaments/{tournamentId}/characters/{characterId}/fights" description="Receives the fights from a tournament for a given character.">
<uriVariables>
<uriVariable parameterName="tournamentId" fromClass="App\Entity\Tournament"></uriVariable>
<uriVariable parameterName="characterId" fromClass="App\Entity\Character"></uriVariable>
</uriVariables>
</operation>
</operations> </operations>
</resource> </resource>

@ -13,8 +13,6 @@ class GetTournamentFights
{ {
return $em->getRepository(Fight::class)->findBy([ return $em->getRepository(Fight::class)->findBy([
'tournament' => $tournamentId 'tournament' => $tournamentId
], [
'startDate' => 'ASC'
]); ]);
} }
} }

@ -167,17 +167,19 @@ class Character extends Thing
} }
/** /**
* * @return Collection<int, Technique>
* @return Technique[]
*/ */
public function getTechniques(): mixed public function getTechniques(): Collection
{ {
return $this->techniques->getValues(); return $this->techniques;
} }
public function addTechnique(Technique $technique): static public function addTechnique(Technique $technique): static
{ {
$this->techniques[] = $technique; if (!$this->techniques->contains($technique)) {
$this->techniques->add($technique);
}
return $this; return $this;
} }

@ -62,7 +62,6 @@ class Dojo extends Thing
} }
/** /**
*
* @return Collection<int, Character> * @return Collection<int, Character>
*/ */
public function getMembers(): Collection public function getMembers(): Collection
@ -72,7 +71,7 @@ class Dojo extends Thing
public function addMember(Character $member): static public function addMember(Character $member): static
{ {
if (! $this->members->contains($member)) { if (!$this->members->contains($member)) {
$this->members->add($member); $this->members->add($member);
$member->setDojo($this); $member->setDojo($this);
} }

@ -21,7 +21,6 @@ class Fight extends Thing
private array $events = []; private array $events = [];
#[ORM\ManyToOne] #[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private ?Character $winner = null; private ?Character $winner = null;
#[ORM\ManyToOne(inversedBy: 'fights')] #[ORM\ManyToOne(inversedBy: 'fights')]

@ -23,6 +23,9 @@ class Tournament extends Thing
#[ORM\OneToMany(mappedBy: 'tournament', targetEntity: Fight::class)] #[ORM\OneToMany(mappedBy: 'tournament', targetEntity: Fight::class)]
private Collection $fights; private Collection $fights;
#[ORM\ManyToOne]
private ?Character $winner = null;
public function __construct() public function __construct()
{ {
$this->characters = new ArrayCollection(); $this->characters = new ArrayCollection();
@ -108,4 +111,16 @@ class Tournament extends Thing
return $this; return $this;
} }
public function getWinner(): ?Character
{
return $this->winner;
}
public function setWinner(?Character $winner): static
{
$this->winner = $winner;
return $this;
}
} }

@ -1,11 +1,15 @@
<?php <?php
namespace App\Entity; namespace App\Entity;
use App\Validator\CharacterOwned;
use App\Validator\StartDateInFuture;
class TournamentRegistration class TournamentRegistration
{ {
#[StartDateInFuture()]
public Tournament $tournament; public Tournament $tournament;
#[CharacterOwned()]
public Character $character; public Character $character;
} }

@ -0,0 +1,41 @@
<?php
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\TournamentRegistration;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
class TournamentRegistrationStateProcessor implements ProcessorInterface
{
public function __construct(
#[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] private ProcessorInterface $persistProcessor,
private EntityManagerInterface $em)
{}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): TournamentRegistration
{
$this->updateData($data);
$result = $this->persistProcessor->process($data, $operation, $uriVariables, $context);
return $result;
}
private function updateData(TournamentRegistration $registration): void
{
$tournament = $registration->tournament;
$character = $registration->character;
if ($tournament->getCharacters()->contains($character)) {
throw new BadRequestException("Character is already registered!");
}
$registration->tournament->addCharacter($registration->character);
$this->em->persist($registration->tournament);
$this->em->persist($registration->character);
$this->em->flush();
}
}

@ -0,0 +1,12 @@
<?php
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)]
class CharacterOwned extends Constraint
{
public string $invalidOwnerMessage = 'Character is not from the dojo of the current user';
}

@ -0,0 +1,33 @@
<?php
namespace App\Validator;
use App\Entity\Character;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
class CharacterOwnedValidator extends ConstraintValidator
{
public function __construct(private Security $security)
{}
public function validate($character, Constraint $constraint): void
{
if (! $character instanceof Character) {
throw new UnexpectedValueException($character, Character::class);
}
if (! $constraint instanceof CharacterOwned) {
throw new UnexpectedValueException($constraint, CharacterOwned::class);
}
if ($character->getDojo()
->getOwner()
->getId() != $this->security->getUser()->getUserIdentifier()) {
$this->context->buildViolation($constraint->invalidOwnerMessage)->addViolation();
}
}
}

@ -0,0 +1,12 @@
<?php
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)]
class StartDateInFuture extends Constraint
{
public string $invalidStartDateMessage = 'Start date has already passed';
}

@ -0,0 +1,27 @@
<?php
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
use DateTimeImmutable;
use DateTimeZone;
class StartDateInFutureValidator extends ConstraintValidator
{
public function __construct()
{}
public function validate(mixed $entity, Constraint $constraint): void
{
if (! $constraint instanceof StartDateInFuture) {
throw new UnexpectedValueException($constraint, CharacterStats::class);
}
if ($entity->getStartDate() < new DateTimeImmutable("now", new DateTimeZone("UTC"))) {
$this->context->buildViolation($constraint->invalidStartDateMessage)->addViolation();
}
}
}

@ -4,6 +4,7 @@ namespace App\Tests;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use ApiPlatform\Symfony\Bundle\Test\Client; use ApiPlatform\Symfony\Bundle\Test\Client;
use App\Entity\Thing; use App\Entity\Thing;
use Doctrine\ORM\EntityManagerInterface;
use Zenstruck\Foundry\Proxy; use Zenstruck\Foundry\Proxy;
use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase; use Zenstruck\Foundry\Test\ResetDatabase;
@ -48,5 +49,10 @@ abstract class AbstractTest extends ApiTestCase
} }
return $this->getIriFromResource($thing); return $this->getIriFromResource($thing);
} }
protected function getEntityManager(): EntityManagerInterface
{
return static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
}
} }

@ -1,29 +1,53 @@
<?php <?php
namespace App\Tests; namespace App\Tests;
use App\Entity\Character;
use App\Entity\Tournament;
use App\Entity\User;
use App\Factory\CharacterFactory; use App\Factory\CharacterFactory;
use App\Factory\DojoFactory; use App\Factory\DojoFactory;
use App\Factory\FightFactory;
use App\Factory\TournamentFactory; use App\Factory\TournamentFactory;
use App\Factory\UserFactory; use App\Factory\UserFactory;
use Zenstruck\Foundry\Proxy;
use DateTimeImmutable;
use DateTimeZone;
// @note No need to create tournaments via API Endpoint ... will be done in cronjob
class TournamentTest extends AbstractTest class TournamentTest extends AbstractTest
{ {
// No need to create tournaments via API Endpoint ... will be done in cronjob private function createTournament(string $offset, array $characters = array()): Tournament|Proxy
public function testRegisterCharacter(): void
{ {
$tournament = TournamentFactory::createOne(); $tournamentStartDate = new DateTimeImmutable("now", new DateTimeZone("UTC"));
$tournamentStartDate = $tournamentStartDate->sub(\DateInterval::createFromDateString($offset));
return TournamentFactory::createOne([
'startDate' => $tournamentStartDate,
'characters' => $characters
]);
}
private function createCharacter(User|Proxy $user): Character|Proxy
{
$dojo = DojoFactory::createOne([ $dojo = DojoFactory::createOne([
'owner' => UserFactory::createOne() 'owner' => $user
]); ]);
$character = CharacterFactory::createOne([ $character = CharacterFactory::createOne([
'dojo' => $dojo 'dojo' => $dojo
]); ]);
$response = static::createClientWithToken($dojo->getOwner()->authName)->request('POST', return $character;
'/api/tournament_registrations', }
public function testRegisterCharacter(): void
{
$tournament = $this->createTournament("-5 min");
$user = UserFactory::createOne();
$character = $this->createCharacter($user);
static::createClientWithToken($user->authName)->request('POST', '/api/tournament_registrations',
[ [
'json' => [ 'json' => [
'tournament' => $this->getIri($tournament), 'tournament' => $this->getIri($tournament),
@ -32,40 +56,142 @@ class TournamentTest extends AbstractTest
]); ]);
$this->assertResponseStatusCodeSame(201); $this->assertResponseStatusCodeSame(201);
}
public function testRegisterCharacterDifferentUser(): void
{
$tournament = $this->createTournament("-5 min");
$characterOwner = UserFactory::createOne();
$character = $this->createCharacter($characterOwner);
$user = UserFactory::createOne();
$this->assertArrayHasKey('id', $response->toArray()); static::createClientWithToken($user->authName)->request('POST', '/api/tournament_registrations',
[
'json' => [
'tournament' => $this->getIri($tournament),
'character' => $this->getIri($character)
]
]);
$this->assertResponseStatusCodeSame(422);
}
public function testRegisterCharacterOnPastTournament(): void
{
$tournament = $this->createTournament("5 min");
$user = UserFactory::createOne();
$character = $this->createCharacter($user);
static::createClientWithToken($user->authName)->request('POST', '/api/tournament_registrations',
[
'json' => [
'tournament' => $this->getIri($tournament),
'character' => $this->getIri($character)
]
]);
$this->assertResponseStatusCodeSame(422);
} }
public function testRegisterCharacterNotPossibleTwice(): void public function testRegisterCharacterNotPossibleTwice(): void
{} {
$tournament = $this->createTournament("-5 min");
$user = UserFactory::createOne();
$character = $this->createCharacter($user);
public function testRegisterCharacterOnPastTournament(): void static::createClientWithToken($user->authName)->request('POST', '/api/tournament_registrations',
{} [
'json' => [
'tournament' => $this->getIri($tournament),
'character' => $this->getIri($character)
]
]);
static::createClientWithToken($user->authName)->request('POST', '/api/tournament_registrations',
[
'json' => [
'tournament' => $this->getIri($tournament),
'character' => $this->getIri($character)
]
]);
$this->assertEquals(1,
$this->getEntityManager()
->find(Tournament::class, $tournament->getId())
->getCharacters()
->count());
$this->assertResponseStatusCodeSame(400);
}
public function testShowRegisteredCharacters(): void public function testShowRegisteredCharacters(): void
{} {
$tournament = $this->createTournament("-5 min", CharacterFactory::createMany(4));
$response = static::createClientWithToken()->request('GET', $this->getIri($tournament));
$this->assertResponseStatusCodeSame(200);
$this->assertCount(4, $response->toArray()['characters']);
}
public function testListTournaments(): void public function testListTournaments(): void
{} {
TournamentFactory::createMany(5);
$response = static::createClientWithToken()->request('GET', '/api/tournaments');
$this->assertResponseStatusCodeSame(200);
$this->assertCount(5, $response->toArray());
}
/** /**
* Status is ... * Status is ...
* meta data like when it is starting, name, "location", Winner (nullable), etc. * meta data like when it is starting, name, "location", Winner (nullable), etc.
*/ */
public function testTournamentStatus(): void public function testTournamentStatus(): void
{} {
$tournament = TournamentFactory::createOne([
'winner' => CharacterFactory::createOne()
]);
$response = static::createClientWithToken()->request('GET', $this->getIri($tournament));
$this->assertResponseStatusCodeSame(200);
$arrayResponse = $response->toArray();
$this->assertArrayHasKey('winner', $arrayResponse);
}
// /api/tournament/{id}/fights
// readableLink: true -> participant ids and winner
public function testTournamentFights(): void public function testTournamentFights(): void
{} {
$tournament = TournamentFactory::createOne([
'fights' => FightFactory::createMany(16)
]);
$response = static::createClientWithToken()->request('GET', $this->getIri($tournament) . '/fights');
$this->assertResponseStatusCodeSame(200);
$this->assertCount(16, $response->toArray());
}
// /api/tournament/{id}/character/{id}/fights
public function testTournamentFightsForCharacter(): void public function testTournamentFightsForCharacter(): void
{} {
$characters = CharacterFactory::createMany(16);
$tournament = TournamentFactory::createOne([
'characters' => $characters
]);
// /api/tournament/{id}/ranking $winner = $characters[0];
public function testTournamentRanking(): void for ($i = 1; $i < count($characters); ++ $i) {
{} $tournament->addFight(
FightFactory::createOne([
'winner' => $winner,
'characters' => array(
$winner,
$characters[$i]
)
])->object());
}
$tournament->save();
$response = static::createClientWithToken()->request('GET',
$this->getIri($tournament) . '/characters/' . $winner->getId()
->toBase32() . '/fights');
$this->assertCount(15, $response->toArray());
}
} }

Loading…
Cancel
Save