Added the tournament routes and tests

pull/1/head
Hecht 2 months ago
parent 91b0f98c06
commit dda773dbce

@ -143,14 +143,22 @@
<resource class="App\Entity\TournamentRegistration">
<operations>
<operation class="ApiPlatform\Metadata\Post" />
<operation class="ApiPlatform\Metadata\Post" processor="App\State\TournamentRegistrationStateProcessor"/>
</operations>
</resource>
<resource class="App\Entity\Fight">
<operations>
<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>
</resource>

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

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

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

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

@ -23,6 +23,9 @@ class Tournament extends Thing
#[ORM\OneToMany(mappedBy: 'tournament', targetEntity: Fight::class)]
private Collection $fights;
#[ORM\ManyToOne]
private ?Character $winner = null;
public function __construct()
{
$this->characters = new ArrayCollection();
@ -108,4 +111,16 @@ class Tournament extends Thing
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
namespace App\Entity;
use App\Validator\CharacterOwned;
use App\Validator\StartDateInFuture;
class TournamentRegistration
{
#[StartDateInFuture()]
public Tournament $tournament;
#[CharacterOwned()]
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\Client;
use App\Entity\Thing;
use Doctrine\ORM\EntityManagerInterface;
use Zenstruck\Foundry\Proxy;
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase;
@ -48,5 +49,10 @@ abstract class AbstractTest extends ApiTestCase
}
return $this->getIriFromResource($thing);
}
protected function getEntityManager(): EntityManagerInterface
{
return static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
}
}

@ -1,29 +1,53 @@
<?php
namespace App\Tests;
use App\Entity\Character;
use App\Entity\Tournament;
use App\Entity\User;
use App\Factory\CharacterFactory;
use App\Factory\DojoFactory;
use App\Factory\FightFactory;
use App\Factory\TournamentFactory;
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
{
// No need to create tournaments via API Endpoint ... will be done in cronjob
public function testRegisterCharacter(): void
private function createTournament(string $offset, array $characters = array()): Tournament|Proxy
{
$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([
'owner' => UserFactory::createOne()
'owner' => $user
]);
$character = CharacterFactory::createOne([
'dojo' => $dojo
]);
$response = static::createClientWithToken($dojo->getOwner()->authName)->request('POST',
'/api/tournament_registrations',
return $character;
}
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' => [
'tournament' => $this->getIri($tournament),
@ -32,40 +56,142 @@ class TournamentTest extends AbstractTest
]);
$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
{}
{
$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
{}
{
$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
{}
{
TournamentFactory::createMany(5);
$response = static::createClientWithToken()->request('GET', '/api/tournaments');
$this->assertResponseStatusCodeSame(200);
$this->assertCount(5, $response->toArray());
}
/**
* Status is ...
* meta data like when it is starting, name, "location", Winner (nullable), etc.
*/
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
{}
{
$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
{}
{
$characters = CharacterFactory::createMany(16);
$tournament = TournamentFactory::createOne([
'characters' => $characters
]);
// /api/tournament/{id}/ranking
public function testTournamentRanking(): void
{}
$winner = $characters[0];
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