Fixes on API level (finally a character can be created)

pull/1/head
Hecht 9 months ago
parent 351f975ad8
commit 9e331be98b

@ -77,6 +77,7 @@
<resource class="App\Entity\Dojo"> <resource class="App\Entity\Dojo">
<operations> <operations>
<operation class="ApiPlatform\Metadata\Get" /> <operation class="ApiPlatform\Metadata\Get" />
<operation class="ApiPlatform\Metadata\Get" uriTemplate="dojo" controller="App\Controller\GetUserDojo" />
<operation class="ApiPlatform\Metadata\Post" processor="App\State\DojoPostProcessor"/> <operation class="ApiPlatform\Metadata\Post" processor="App\State\DojoPostProcessor"/>
<operation class="ApiPlatform\Metadata\Patch" /> <operation class="ApiPlatform\Metadata\Patch" />
</operations> </operations>
@ -108,7 +109,6 @@
<normalizationContext><values><value name="groups"><values><value>public</value></values></value></values></normalizationContext> <normalizationContext><values><value name="groups"><values><value>public</value></values></value></values></normalizationContext>
<operations> <operations>
<operation class="ApiPlatform\Metadata\Get" /> <operation class="ApiPlatform\Metadata\Get" />
<operation class="ApiPlatform\Metadata\Patch" />
<operation class="ApiPlatform\Metadata\GetCollection" controller="App\Controller\GetDojoCharacters" uriTemplate="dojo/{dojoId}/characters" description="Receives the characters from a dojo."> <operation class="ApiPlatform\Metadata\GetCollection" controller="App\Controller\GetDojoCharacters" uriTemplate="dojo/{dojoId}/characters" description="Receives the characters from a dojo.">
<uriVariables><uriVariable parameterName="dojoId" /></uriVariables> <uriVariables><uriVariable parameterName="dojoId" /></uriVariables>
</operation> </operation>
@ -118,4 +118,19 @@
</operations> </operations>
</resource> </resource>
<resource class="App\Entity\Character">
<operations>
<operation class="ApiPlatform\Metadata\Post" />
<operation class="ApiPlatform\Metadata\Patch" />
</operations>
</resource>
<resource class="App\Entity\Technique">
<operations>
<operation class="ApiPlatform\Metadata\Get" />
<operation class="ApiPlatform\Metadata\GetCollection" />
<operation class="ApiPlatform\Metadata\Post" />
</operations>
</resource>
</resources> </resources>

@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240129212818 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
}
}

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240219220159 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE "character" (id UUID NOT NULL, dojo_id UUID DEFAULT NULL, name VARCHAR(255) NOT NULL, strength INT NOT NULL, constitution INT NOT NULL, agility INT NOT NULL, chi INT NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_937AB03432F09E9C ON "character" (dojo_id)');
$this->addSql('COMMENT ON COLUMN "character".id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN "character".dojo_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE character_technique (character_id UUID NOT NULL, technique_id UUID NOT NULL, PRIMARY KEY(character_id, technique_id))');
$this->addSql('CREATE INDEX IDX_506B3A7A1136BE75 ON character_technique (character_id)');
$this->addSql('CREATE INDEX IDX_506B3A7A1F8ACB26 ON character_technique (technique_id)');
$this->addSql('COMMENT ON COLUMN character_technique.character_id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN character_technique.technique_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE city (id UUID NOT NULL, dungeon_id UUID NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_2D5B0234B606863 ON city (dungeon_id)');
$this->addSql('COMMENT ON COLUMN city.id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN city.dungeon_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE country (id UUID NOT NULL, capital_id UUID NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_5373C966FC2D9FF7 ON country (capital_id)');
$this->addSql('COMMENT ON COLUMN country.id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN country.capital_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE dojo (id UUID NOT NULL, village_id UUID DEFAULT NULL, owner_id UUID NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_9494CCB15E237E06 ON dojo (name)');
$this->addSql('CREATE INDEX IDX_9494CCB15E0D5582 ON dojo (village_id)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_9494CCB17E3C61F9 ON dojo (owner_id)');
$this->addSql('COMMENT ON COLUMN dojo.id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN dojo.village_id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN dojo.owner_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE dungeon (id UUID NOT NULL, city_id UUID NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_3FFA1F908BAC62AF ON dungeon (city_id)');
$this->addSql('COMMENT ON COLUMN dungeon.id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN dungeon.city_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE prefecture (id UUID NOT NULL, capital_id UUID NOT NULL, country_id UUID NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_ABE6511AFC2D9FF7 ON prefecture (capital_id)');
$this->addSql('CREATE INDEX IDX_ABE6511AF92F3E70 ON prefecture (country_id)');
$this->addSql('COMMENT ON COLUMN prefecture.id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN prefecture.capital_id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN prefecture.country_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE technique (id UUID NOT NULL, prerequisite_id UUID DEFAULT NULL, costs INT NOT NULL, damage VARCHAR(255) NOT NULL, energy VARCHAR(255) NOT NULL, accuracy VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_D73B9841276AF86B ON technique (prerequisite_id)');
$this->addSql('COMMENT ON COLUMN technique.id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN technique.prerequisite_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE "user" (id UUID NOT NULL, dojo_id UUID DEFAULT NULL, auth_name VARCHAR(255) NOT NULL, properties JSON NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D64932F09E9C ON "user" (dojo_id)');
$this->addSql('COMMENT ON COLUMN "user".id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN "user".dojo_id IS \'(DC2Type:ulid)\'');
$this->addSql('CREATE TABLE village (id UUID NOT NULL, prefecture_id UUID NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_4E6C7FAA9D39C865 ON village (prefecture_id)');
$this->addSql('COMMENT ON COLUMN village.id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN village.prefecture_id IS \'(DC2Type:ulid)\'');
$this->addSql('ALTER TABLE "character" ADD CONSTRAINT FK_937AB03432F09E9C FOREIGN KEY (dojo_id) REFERENCES dojo (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE character_technique ADD CONSTRAINT FK_506B3A7A1136BE75 FOREIGN KEY (character_id) REFERENCES "character" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE character_technique ADD CONSTRAINT FK_506B3A7A1F8ACB26 FOREIGN KEY (technique_id) REFERENCES technique (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE city ADD CONSTRAINT FK_2D5B0234B606863 FOREIGN KEY (dungeon_id) REFERENCES dungeon (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE country ADD CONSTRAINT FK_5373C966FC2D9FF7 FOREIGN KEY (capital_id) REFERENCES city (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE dojo ADD CONSTRAINT FK_9494CCB15E0D5582 FOREIGN KEY (village_id) REFERENCES village (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE dojo ADD CONSTRAINT FK_9494CCB17E3C61F9 FOREIGN KEY (owner_id) REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE dungeon ADD CONSTRAINT FK_3FFA1F908BAC62AF FOREIGN KEY (city_id) REFERENCES city (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE prefecture ADD CONSTRAINT FK_ABE6511AFC2D9FF7 FOREIGN KEY (capital_id) REFERENCES city (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE prefecture ADD CONSTRAINT FK_ABE6511AF92F3E70 FOREIGN KEY (country_id) REFERENCES country (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE technique ADD CONSTRAINT FK_D73B9841276AF86B FOREIGN KEY (prerequisite_id) REFERENCES technique (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE "user" ADD CONSTRAINT FK_8D93D64932F09E9C FOREIGN KEY (dojo_id) REFERENCES dojo (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE village ADD CONSTRAINT FK_4E6C7FAA9D39C865 FOREIGN KEY (prefecture_id) REFERENCES prefecture (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE "character" DROP CONSTRAINT FK_937AB03432F09E9C');
$this->addSql('ALTER TABLE character_technique DROP CONSTRAINT FK_506B3A7A1136BE75');
$this->addSql('ALTER TABLE character_technique DROP CONSTRAINT FK_506B3A7A1F8ACB26');
$this->addSql('ALTER TABLE city DROP CONSTRAINT FK_2D5B0234B606863');
$this->addSql('ALTER TABLE country DROP CONSTRAINT FK_5373C966FC2D9FF7');
$this->addSql('ALTER TABLE dojo DROP CONSTRAINT FK_9494CCB15E0D5582');
$this->addSql('ALTER TABLE dojo DROP CONSTRAINT FK_9494CCB17E3C61F9');
$this->addSql('ALTER TABLE dungeon DROP CONSTRAINT FK_3FFA1F908BAC62AF');
$this->addSql('ALTER TABLE prefecture DROP CONSTRAINT FK_ABE6511AFC2D9FF7');
$this->addSql('ALTER TABLE prefecture DROP CONSTRAINT FK_ABE6511AF92F3E70');
$this->addSql('ALTER TABLE technique DROP CONSTRAINT FK_D73B9841276AF86B');
$this->addSql('ALTER TABLE "user" DROP CONSTRAINT FK_8D93D64932F09E9C');
$this->addSql('ALTER TABLE village DROP CONSTRAINT FK_4E6C7FAA9D39C865');
$this->addSql('DROP TABLE "character"');
$this->addSql('DROP TABLE character_technique');
$this->addSql('DROP TABLE city');
$this->addSql('DROP TABLE country');
$this->addSql('DROP TABLE dojo');
$this->addSql('DROP TABLE dungeon');
$this->addSql('DROP TABLE prefecture');
$this->addSql('DROP TABLE technique');
$this->addSql('DROP TABLE "user"');
$this->addSql('DROP TABLE village');
}
}

@ -0,0 +1,21 @@
<?php
namespace App\Controller;
use App\Entity\Dojo;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpKernel\Attribute\AsController;
#[AsController]
final class GetUserDojo
{
public function __construct(private Security $security)
{}
public function __invoke(EntityManagerInterface $em): ?Dojo
{
return $em->getRepository(Dojo::class)->findOneByOwner($this->security->getUser());
}
}

@ -1,6 +1,8 @@
<?php <?php
namespace App\Entity; namespace App\Entity;
use ApiPlatform\Metadata\ApiProperty;
use App\Validator\CharacterStats;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Column;
@ -9,16 +11,18 @@ use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToMany; use Doctrine\ORM\Mapping\ManyToMany;
use Doctrine\ORM\Mapping\ManyToOne; use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\Table; use Doctrine\ORM\Mapping\Table;
use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Mapping\ClassMetadata;
#[Entity] #[Entity]
#[Table(name: '`character`')] #[Table(name: '`character`')]
class Character extends Thing class Character extends Thing
{ {
#[ManyToOne()] #[ManyToOne(inversedBy: 'members')]
#[JoinColumn(onDelete: 'cascade')] #[JoinColumn(onDelete: 'cascade')]
#[Groups('public')] #[Groups('public')]
#[ApiProperty(readableLink: false, writableLink: false)]
public ?Dojo $dojo; public ?Dojo $dojo;
#[Column] #[Column]
@ -37,11 +41,16 @@ class Character extends Thing
#[Groups('detail')] #[Groups('detail')]
public int $agility; public int $agility;
/** @var Technique[] */ #[Column]
#[Groups('detail')]
public int $chi;
#[Groups('detail')] #[Groups('detail')]
#[ManyToMany(targetEntity: Technique::class)] #[ManyToMany(targetEntity: Technique::class, cascade: [
#[JoinColumn(nullable: false)] 'persist'
public iterable $techniques; ])]
#[ApiProperty(readableLink: false, writableLink: false)]
public Collection $techniques;
public function __construct() public function __construct()
{ {
@ -49,23 +58,40 @@ class Character extends Thing
} }
/** /**
* Calculates the aged based on the ulid value? * MVP: Only
*/ */
public function getAge(): int #[Groups('detail')]
public function getFreeSkillPoints()
{ {
return 21; $available = 32;
$costs = self::skillCosts($this->getStrength());
$costs += self::skillCosts($this->getConstitution());
$costs += self::skillCosts($this->getAgility());
$costs += self::skillCosts($this->getChi());
foreach ($this->getTechniques() as $tech) {
$costs += $tech->getCosts();
}
return $available - $costs;
} }
public function getDojo(): ?Dojo public static function loadValidatorMetadata(ClassMetadata $metadata): void
{ {
return $this->dojo; $metadata->addConstraint(new CharacterStats());
} }
public function setDojo(?Dojo $dojo): static /**
* Calculates the aged based on the ulid value?
*/
#[Groups('public')]
public function getAge(): int
{ {
$this->dojo = $dojo; return 21;
}
return $this; public static function skillCosts(int $value)
{
return pow(2, $value);
} }
public function getName(): ?string public function getName(): ?string
@ -92,14 +118,14 @@ class Character extends Thing
return $this; return $this;
} }
public function getConstition(): ?int public function getConstitution(): ?int
{ {
return $this->constition; return $this->constitution;
} }
public function setConstition(int $constition): static public function setConstitution(int $constitution): static
{ {
$this->constition = $constition; $this->constitution = $constitution;
return $this; return $this;
} }
@ -116,45 +142,48 @@ class Character extends Thing
return $this; return $this;
} }
/** public function getChi(): ?int
* @return Collection<int, Technique>
*/
public function getTechniques(): Collection
{ {
return $this->techniques; return $this->chi;
} }
public function setTechniques(string $techniques): static public function setChi(int $chi): static
{ {
$this->techniques = $techniques; $this->chi = $chi;
return $this; return $this;
} }
public function addTechnique(Technique $technique): static public function getDojo(): ?Dojo
{ {
if (!$this->techniques->contains($technique)) { return $this->dojo;
$this->techniques->add($technique);
} }
public function setDojo(?Dojo $dojo): static
{
$this->dojo = $dojo;
return $this; return $this;
} }
public function removeTechnique(Technique $technique): static /**
*
* @return Technique[]
*/
public function getTechniques(): mixed
{ {
$this->techniques->removeElement($technique); return $this->techniques->getValues();
return $this;
} }
public function getConstitution(): ?int public function addTechnique(Technique $technique): static
{ {
return $this->constitution; $this->techniques[] = $technique;
return $this;
} }
public function setConstitution(int $constitution): static public function removeTechnique(Technique $technique): static
{ {
$this->constitution = $constitution; $this->techniques->removeElement($technique);
return $this; return $this;
} }

@ -6,7 +6,7 @@ use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn; use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\OneToOne; use Doctrine\ORM\Mapping\OneToOne;
#[Entity] #[Entity(repositoryClass: 'App\Repository\TechniqueRepository')]
class Technique extends Thing class Technique extends Thing
{ {
@ -17,19 +17,19 @@ class Technique extends Thing
public int $costs; public int $costs;
/** /**
* Forumula to calculate the damage based on the stats. * Formula to calculate the damage based on the stats.
*/ */
#[Column] #[Column]
public string $damage; public string $damage;
/** /**
* Forumula to calculate the damage based on the stats. * Formula to calculate the consumed energy on use based on the stats.
*/ */
#[Column] #[Column]
public string $energy; public string $energy;
/** /**
* Forumula to calculate the damage based on the stats. * Formula to calculate the hit chance accuracy based on the stats.
*/ */
#[Column] #[Column]
public string $accuracy; public string $accuracy;

@ -3,6 +3,7 @@ namespace App\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Types\UlidType; use Symfony\Bridge\Doctrine\Types\UlidType;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Uid\Ulid; use Symfony\Component\Uid\Ulid;
abstract class Thing abstract class Thing
@ -12,6 +13,12 @@ abstract class Thing
#[ORM\Column(type: UlidType::NAME, unique: true)] #[ORM\Column(type: UlidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')] #[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: 'doctrine.ulid_generator')] #[ORM\CustomIdGenerator(class: 'doctrine.ulid_generator')]
#[Groups('public')]
public ?Ulid $id; public ?Ulid $id;
public function getId(): ?Ulid
{
return $this->id;
}
} }

@ -9,7 +9,7 @@ use Doctrine\ORM\Mapping\OneToOne;
use Doctrine\ORM\Mapping\Table; use Doctrine\ORM\Mapping\Table;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
#[Entity(repositoryClass: "App\Repository\UserRepository")] #[Entity(repositoryClass: 'App\Repository\UserRepository')]
#[Table(name: '`user`')] #[Table(name: '`user`')]
class User extends Thing implements UserInterface class User extends Thing implements UserInterface
{ {

@ -47,10 +47,11 @@ final class CharacterFactory extends ModelFactory
protected function getDefaults(): array protected function getDefaults(): array
{ {
return [ return [
'name' => self::faker()->text(), 'name' => self::faker()->firstName(),
'strength' => self::faker()->numberBetween(1, 4), 'strength' => self::faker()->numberBetween(1, 4),
'constitution' => self::faker()->numberBetween(1, 4), 'constitution' => self::faker()->numberBetween(1, 4),
'agility' => self::faker()->numberBetween(1, 4), 'agility' => self::faker()->numberBetween(1, 4),
'chi' => self::faker()->numberBetween(1, 4),
'techniques' => TechniqueFactory::createMany(2) 'techniques' => TechniqueFactory::createMany(2)
]; ];
} }

@ -0,0 +1,24 @@
<?php
namespace App\Repository;
use App\Entity\Technique;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
*
* @extends ServiceEntityRepository<Technique>
*
* @method Technique|null find($id, $lockMode = null, $lockVersion = null)
* @method Technique|null findOneBy(array $criteria, array $orderBy = null)
* @method Technique[] findAll()
* @method Technique[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class TechniqueRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Technique::class);
}
}

@ -0,0 +1,16 @@
<?php
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
class CharacterStats extends Constraint
{
public string $noMoreSkillPointsMessage = 'Character spent more skill points than available';
public function getTargets(): string
{
return self::CLASS_CONSTRAINT;
}
}

@ -0,0 +1,32 @@
<?php
namespace App\Validator;
use App\Entity\Character;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
class CharacterStatsValidator extends ConstraintValidator
{
public function __construct()
{}
public function validate(mixed $character, Constraint $constraint): void
{
if (! $character instanceof Character) {
throw new UnexpectedValueException($character, Character::class);
}
if (! $constraint instanceof CharacterStats) {
throw new UnexpectedValueException($constraint, CharacterStats::class);
}
if ($character->getFreeSkillPoints() < 0) {
$this->context->buildViolation($constraint->noMoreSkillPointsMessage)
->atPath('character.freeSkillPoints')
->addViolation();
}
}
}

@ -0,0 +1,42 @@
<?php
namespace App\Tests;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use ApiPlatform\Symfony\Bundle\Test\Client;
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase;
use DateTimeZone;
abstract class AbstractTest extends ApiTestCase
{
// This trait provided by Foundry will take care of refreshing the database content to a known state before each test
use ResetDatabase, Factories;
public function setUp(): void
{
self::bootKernel();
}
private static function generateAuthToken(string $authName)
{
$sign_seed = sodium_base642bin($_ENV['AUTH_SEED'], SODIUM_BASE64_VARIANT_ORIGINAL);
$sign_pair = sodium_crypto_sign_seed_keypair($sign_seed);
$sign_secret = sodium_crypto_sign_secretkey($sign_pair);
$now = new \DateTimeImmutable("now", new DateTimeZone("UTC"));
$message = $authName . "|" . $now->format("c");
return sodium_bin2base64(sodium_crypto_sign($message, $sign_secret), SODIUM_BASE64_VARIANT_URLSAFE);
}
protected static function createClientWithToken($authName = null): Client
{
return static::createClient([],
[
'headers' => [
'accept' => 'application/json',
'X-AUTH-TOKEN' => static::generateAuthToken($authName)
]
]);
}
}

@ -1,28 +1,13 @@
<?php <?php
namespace App\Tests; namespace App\Tests;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use App\Factory\CharacterFactory; use App\Factory\CharacterFactory;
use App\Factory\DojoFactory; use App\Factory\DojoFactory;
use App\Factory\TechniqueFactory;
use App\Factory\UserFactory; use App\Factory\UserFactory;
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase;
use DateTimeZone;
class CharacterTest extends ApiTestCase class CharacterTest extends AbstractTest
{ {
use ResetDatabase, Factories;
private function generateAuthToken(string $authName)
{
$sign_seed = sodium_base642bin($_ENV['AUTH_SEED'], SODIUM_BASE64_VARIANT_ORIGINAL);
$sign_pair = sodium_crypto_sign_seed_keypair($sign_seed);
$sign_secret = sodium_crypto_sign_secretkey($sign_pair);
$now = new \DateTimeImmutable("now", new DateTimeZone("UTC"));
$message = $authName . "|" . $now->format("c");
return sodium_bin2base64(sodium_crypto_sign($message, $sign_secret), SODIUM_BASE64_VARIANT_URLSAFE);
}
/** /**
* Requirement: A user should be able see all characters from a dojo, but only the public fields! * Requirement: A user should be able see all characters from a dojo, but only the public fields!
@ -36,65 +21,173 @@ class CharacterTest extends ApiTestCase
]); ]);
CharacterFactory::createMany(10); CharacterFactory::createMany(10);
$response = static::createClient()->request('GET', '/api/dojo/' . $dojo->id . '/characters', $response = static::createClientWithToken($requestUser->authName)->request('GET',
[ '/api/dojo/' . $dojo->id . '/characters');
'headers' => [
'accept' => 'application/json',
'X-AUTH-TOKEN' => $this->generateAuthToken($requestUser->authName)
]
]);
$this->assertResponseStatusCodeSame(200); $this->assertResponseStatusCodeSame(200);
$this->assertNotEquals("[[],[],[],[]]", $response->getContent());
// Because test fixtures are automatically loaded between each test, you can assert on them // Because test fixtures are automatically loaded between each test, you can assert on them
$this->assertCount(4, $response->toArray()); $this->assertCount(4, $response->toArray());
$this->assertNotEquals("[[],[],[],[]]", $response->getContent());
$chars = $response->toArray(); $chars = $response->toArray();
$this->assertEquals(4, count($chars[0]));
$this->assertArrayHasKey('id', $chars[0]);
$this->assertArrayHasKey('name', $chars[0]); $this->assertArrayHasKey('name', $chars[0]);
$this->assertArrayHasKey('dojo', $chars[0]); $this->assertEquals('/api/dojos/' . $dojo->getId()
->toBase32(), $chars[0]['dojo']);
$this->assertArrayHasKey('age', $chars[0]);
$this->assertArrayNotHasKey('freeSkillPoints', $chars[0]);
$this->assertArrayNotHasKey('strength', $chars[0]); // not accessible via this route $this->assertArrayNotHasKey('strength', $chars[0]); // not accessible via this route
$this->assertArrayNotHasKey('constitution', $chars[0]); // not accessible via this route $this->assertArrayNotHasKey('constitution', $chars[0]); // not accessible via this route
$this->assertArrayNotHasKey('agility', $chars[0]); // not accessible via this route $this->assertArrayNotHasKey('agility', $chars[0]); // not accessible via this route
$this->assertArrayNotHasKey('chi', $chars[0]); // not accessible via this route
$this->assertArrayNotHasKey('techniques', $chars[0]); // not accessible via this route $this->assertArrayNotHasKey('techniques', $chars[0]); // not accessible via this route
} }
/** /**
* Requirement: A user should be able see all characters from a dojo, but only the public fields! * Requirement: A user should be able see all of the characters from his dojo, not restricted by public fields!
*/ */
public function testRetrieveCharactersFromOwnDojoDetail(): void public function testRetrieveCharactersFromOwnDojoDetail(): void
{ {
$dojo = DojoFactory::createOne([ $dojo = DojoFactory::createOne([
'owner' => UserFactory::createOne() 'owner' => UserFactory::createOne()
]); ]);
CharacterFactory::createMany(4, [
'dojo' => $dojo
]);
CharacterFactory::createMany(10);
$response = static::createClient()->request('GET', '/api/dojo/characters', $technique = TechniqueFactory::createOne();
[
'headers' => [ $foo = CharacterFactory::createMany(4, [
'accept' => 'application/json', 'dojo' => $dojo,
'X-AUTH-TOKEN' => $this->generateAuthToken($dojo->getOwner()->authName) 'techniques' => [
$technique
] ]
]); ]);
$this->assertResponseStatusCodeSame(200); $this->assertEquals(1, count($foo[0]->getTechniques()));
// Because test fixtures are automatically loaded between each test, you can assert on them CharacterFactory::createMany(10);
$this->assertCount(4, $response->toArray());
$response = static::createClientWithToken($dojo->getOwner()->authName)->request('GET', '/api/dojo/characters');
$this->assertResponseStatusCodeSame(200);
$this->assertNotEquals("[[],[],[],[]]", $response->getContent()); $this->assertNotEquals("[[],[],[],[]]", $response->getContent());
$chars = $response->toArray(); $chars = $response->toArray();
$this->assertEquals(10, count($chars[0]));
$this->assertArrayHasKey('id', $chars[0]);
$this->assertArrayHasKey('name', $chars[0]); $this->assertArrayHasKey('name', $chars[0]);
$this->assertArrayHasKey('dojo', $chars[0]); $this->assertArrayHasKey('dojo', $chars[0]);
$this->assertArrayHasKey('strength', $chars[0]); // not accessible via this route $this->assertEquals('/api/dojos/' . $dojo->getId()
$this->assertArrayHasKey('constitution', $chars[0]); // not accessible via this route ->toBase32(), $chars[0]['dojo']);
$this->assertArrayHasKey('agility', $chars[0]); // not accessible via this route $this->assertArrayHasKey('age', $chars[0]);
$this->assertArrayHasKey('techniques', $chars[0]); // not accessible via this route $this->assertArrayHasKey('freeSkillPoints', $chars[0]);
$this->assertArrayHasKey('strength', $chars[0]);
$this->assertArrayHasKey('constitution', $chars[0]);
$this->assertArrayHasKey('agility', $chars[0]);
$this->assertArrayHasKey('chi', $chars[0]);
$this->assertArrayHasKey('techniques', $chars[0]);
$this->assertEquals('/api/techniques/' . $technique->getId()
->toBase32(), $chars[0]['techniques'][0]);
}
/**
* Requirement: MVP only (in the future the recuitment will be different).
* A user should be able to create a single character.
*/
public function testCreateCharacter(): void
{
$dojo = DojoFactory::createOne([
'owner' => UserFactory::createOne()
]);
$tech = TechniqueFactory::createOne([
'prerequisite' => NULL
]);
$response = static::createClientWithToken($dojo->getOwner()->authName)->request('POST', '/api/characters',
[
'json' => [
'name' => 'Dude',
'strength' => 1,
'constitution' => 1,
'agility' => 1,
'chi' => 1,
'techniques' => [
'/api/techniques/' . $tech->getId()
->toBase32()
]
]
]);
$this->assertResponseStatusCodeSame(201);
$this->assertArrayHasKey('id', $response->toArray());
}
/**
* Requirement: MVP only (in the future the recuitment will be different).
* A user should NOT be able spent more skill points than available.
*/
public function testCreateCharacterStatsTooHigh(): void
{
$dojo = DojoFactory::createOne([
'owner' => UserFactory::createOne()
]);
$tech = TechniqueFactory::createOne([
'prerequisite' => NULL
]);
static::createClientWithToken($dojo->getOwner()->authName)->request('POST', '/api/characters',
[
'json' => [
'name' => 'Dude',
'strength' => 99,
'constitution' => 1,
'agility' => 1,
'chi' => 1,
'techniques' => [
'/api/techniques/' . $tech->getId()
->toBase32()
]
]
]);
$this->assertResponseStatusCodeSame(422);
}
/**
* Requirement: MVP only (in the future the recuitment will be different).
* A user should NOT be able spent more skill points than available.
*/
public function testCreateCharacterTooExpensiveTechnique(): void
{
$dojo = DojoFactory::createOne([
'owner' => UserFactory::createOne()
]);
$tech = TechniqueFactory::createOne([
'costs' => 99
]);
static::createClientWithToken($dojo->getOwner()->authName)->request('POST', '/api/characters',
[
'json' => [
'name' => 'Dude',
'strength' => 1,
'constitution' => 1,
'agility' => 1,
'chi' => 1,
'techniques' => [
'/api/techniques/' . $tech->getId()
->toBase32()
]
]
]);
$this->assertResponseStatusCodeSame(422);
} }
} }

@ -1,30 +1,12 @@
<?php <?php
namespace App\Tests; namespace App\Tests;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use App\Factory\DojoFactory; use App\Factory\DojoFactory;
use App\Factory\UserFactory; use App\Factory\UserFactory;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase;
use DateTimeImmutable;
use DateTimeZone;
class DojoTest extends ApiTestCase class DojoTest extends AbstractTest
{ {
// This trait provided by Foundry will take care of refreshing the database content to a known state before each test
use ResetDatabase, Factories;
private function generateAuthToken(string $authName)
{
$sign_seed = sodium_base642bin($_ENV['AUTH_SEED'], SODIUM_BASE64_VARIANT_ORIGINAL);
$sign_pair = sodium_crypto_sign_seed_keypair($sign_seed);
$sign_secret = sodium_crypto_sign_secretkey($sign_pair);
$now = new DateTimeImmutable("now", new DateTimeZone("UTC"));
$message = $authName . "|" . $now->format("c");
return sodium_bin2base64(sodium_crypto_sign($message, $sign_secret), SODIUM_BASE64_VARIANT_URLSAFE);
}
/** /**
* Requirement: A user should be able to create a dojo! * Requirement: A user should be able to create a dojo!
@ -37,12 +19,7 @@ class DojoTest extends ApiTestCase
$this->assertCount(0, $userRepository->findByAuthName($userName)); $this->assertCount(0, $userRepository->findByAuthName($userName));
static::createClient()->request('POST', '/api/dojos', static::createClientWithToken($userName)->request('POST', '/api/dojos', [
[
'headers' => [
'accept' => 'application/json',
'X-AUTH-TOKEN' => $this->generateAuthToken($userName)
],
'json' => [ 'json' => [
'name' => $dojoName 'name' => $dojoName
] ]
@ -53,6 +30,52 @@ class DojoTest extends ApiTestCase
$this->assertCount(1, $userRepository->findByAuthName($userName)); $this->assertCount(1, $userRepository->findByAuthName($userName));
} }
/**
* Requirement: A user should be request his own dojo!
*/
public function testUserReadDojo(): void
{
$userName = "FooBarFigher";
$dojoName = "BigFightDojo";
$dojo = DojoFactory::createOne(
[
'name' => $dojoName,
'owner' => UserFactory::createOne([
'authName' => $userName
])
]);
static::createClientWithToken($userName)->request('GET', '/api/dojo');
$this->assertResponseStatusCodeSame(200);
$this->assertJsonContains(
[
'name' => 'BigFightDojo',
'members' => [],
'owner' => '/api/users/' . $dojo->owner->getId()
->toBase32(),
'id' => $dojo->getId()
->toBase32()
]);
}
/**
* Requirement: A user should be request his own dojo!
* Fails, if the dojo is not yet created.
*/
public function testUserReadDojoNotYetCreated(): void
{
$userName = "FooBarFigher";
$userRepository = $this->getContainer()->get(UserRepository::class);
$this->assertCount(0, $userRepository->findByAuthName($userName));
static::createClientWithToken($userName)->request('GET', '/api/dojo');
$this->assertResponseStatusCodeSame(404);
}
/** /**
* Requirement: A user should NOT be able to create more than one dojos! * Requirement: A user should NOT be able to create more than one dojos!
*/ */
@ -67,12 +90,7 @@ class DojoTest extends ApiTestCase
]) ])
]); ]);
static::createClient()->request('POST', '/api/dojos', static::createClientWithToken($userName)->request('POST', '/api/dojos', [
[
'headers' => [
'accept' => 'application/json',
'X-AUTH-TOKEN' => $this->generateAuthToken($userName)
],
'json' => [ 'json' => [
'name' => $dojoName 'name' => $dojoName
] ]
@ -98,12 +116,10 @@ class DojoTest extends ApiTestCase
]) ])
]); ]);
static::createClient()->request('PATCH', '/api/dojos/' . $dojo->id, static::createClientWithToken($userName)->request('PATCH', '/api/dojos/' . $dojo->id,
[ [
'headers' => [ 'headers' => [
'content-type' => 'application/merge-patch+json', 'content-type' => 'application/merge-patch+json'
'accept' => 'application/json',
'X-AUTH-TOKEN' => $this->generateAuthToken($userName)
], ],
'json' => [ 'json' => [
'name' => $newDojoName 'name' => $newDojoName

Loading…
Cancel
Save