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">
<operations>
<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\Patch" />
</operations>
@ -108,7 +109,6 @@
<normalizationContext><values><value name="groups"><values><value>public</value></values></value></values></normalizationContext>
<operations>
<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.">
<uriVariables><uriVariable parameterName="dojoId" /></uriVariables>
</operation>
@ -118,4 +118,19 @@
</operations>
</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>

@ -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
namespace App\Entity;
use ApiPlatform\Metadata\ApiProperty;
use App\Validator\CharacterStats;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
@ -9,16 +11,18 @@ use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToMany;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\Table;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Mapping\ClassMetadata;
#[Entity]
#[Table(name: '`character`')]
class Character extends Thing
{
#[ManyToOne()]
#[ManyToOne(inversedBy: 'members')]
#[JoinColumn(onDelete: 'cascade')]
#[Groups('public')]
#[ApiProperty(readableLink: false, writableLink: false)]
public ?Dojo $dojo;
#[Column]
@ -37,11 +41,16 @@ class Character extends Thing
#[Groups('detail')]
public int $agility;
/** @var Technique[] */
#[Column]
#[Groups('detail')]
public int $chi;
#[Groups('detail')]
#[ManyToMany(targetEntity: Technique::class)]
#[JoinColumn(nullable: false)]
public iterable $techniques;
#[ManyToMany(targetEntity: Technique::class, cascade: [
'persist'
])]
#[ApiProperty(readableLink: false, writableLink: false)]
public Collection $techniques;
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
@ -92,14 +118,14 @@ class Character extends Thing
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;
}
@ -116,45 +142,48 @@ class Character extends Thing
return $this;
}
/**
* @return Collection<int, Technique>
*/
public function getTechniques(): Collection
public function getChi(): ?int
{
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;
}
public function addTechnique(Technique $technique): static
public function getDojo(): ?Dojo
{
if (!$this->techniques->contains($technique)) {
$this->techniques->add($technique);
}
return $this;
return $this->dojo;
}
public function removeTechnique(Technique $technique): static
public function setDojo(?Dojo $dojo): static
{
$this->techniques->removeElement($technique);
$this->dojo = $dojo;
return $this;
}
public function getConstitution(): ?int
/**
*
* @return Technique[]
*/
public function getTechniques(): mixed
{
return $this->constitution;
return $this->techniques->getValues();
}
public function setConstitution(int $constitution): static
public function addTechnique(Technique $technique): static
{
$this->constitution = $constitution;
$this->techniques[] = $technique;
return $this;
}
public function removeTechnique(Technique $technique): static
{
$this->techniques->removeElement($technique);
return $this;
}

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

@ -3,6 +3,7 @@ namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Types\UlidType;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Uid\Ulid;
abstract class Thing
@ -12,6 +13,12 @@ abstract class Thing
#[ORM\Column(type: UlidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: 'doctrine.ulid_generator')]
#[Groups('public')]
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 Symfony\Component\Security\Core\User\UserInterface;
#[Entity(repositoryClass: "App\Repository\UserRepository")]
#[Entity(repositoryClass: 'App\Repository\UserRepository')]
#[Table(name: '`user`')]
class User extends Thing implements UserInterface
{

@ -47,10 +47,11 @@ final class CharacterFactory extends ModelFactory
protected function getDefaults(): array
{
return [
'name' => self::faker()->text(),
'name' => self::faker()->firstName(),
'strength' => self::faker()->numberBetween(1, 4),
'constitution' => self::faker()->numberBetween(1, 4),
'agility' => self::faker()->numberBetween(1, 4),
'chi' => self::faker()->numberBetween(1, 4),
'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
namespace App\Tests;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use App\Factory\CharacterFactory;
use App\Factory\DojoFactory;
use App\Factory\TechniqueFactory;
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!
@ -36,65 +21,173 @@ class CharacterTest extends ApiTestCase
]);
CharacterFactory::createMany(10);
$response = static::createClient()->request('GET', '/api/dojo/' . $dojo->id . '/characters',
[
'headers' => [
'accept' => 'application/json',
'X-AUTH-TOKEN' => $this->generateAuthToken($requestUser->authName)
]
]);
$response = static::createClientWithToken($requestUser->authName)->request('GET',
'/api/dojo/' . $dojo->id . '/characters');
$this->assertResponseStatusCodeSame(200);
$this->assertNotEquals("[[],[],[],[]]", $response->getContent());
// Because test fixtures are automatically loaded between each test, you can assert on them
$this->assertCount(4, $response->toArray());
$this->assertNotEquals("[[],[],[],[]]", $response->getContent());
$chars = $response->toArray();
$this->assertEquals(4, count($chars[0]));
$this->assertArrayHasKey('id', $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('constitution', $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
}
/**
* 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
{
$dojo = DojoFactory::createOne([
'owner' => UserFactory::createOne()
]);
CharacterFactory::createMany(4, [
'dojo' => $dojo
$technique = TechniqueFactory::createOne();
$foo = CharacterFactory::createMany(4, [
'dojo' => $dojo,
'techniques' => [
$technique
]
]);
$this->assertEquals(1, count($foo[0]->getTechniques()));
CharacterFactory::createMany(10);
$response = static::createClient()->request('GET', '/api/dojo/characters',
[
'headers' => [
'accept' => 'application/json',
'X-AUTH-TOKEN' => $this->generateAuthToken($dojo->getOwner()->authName)
]
]);
$response = static::createClientWithToken($dojo->getOwner()->authName)->request('GET', '/api/dojo/characters');
$this->assertResponseStatusCodeSame(200);
// Because test fixtures are automatically loaded between each test, you can assert on them
$this->assertCount(4, $response->toArray());
$this->assertNotEquals("[[],[],[],[]]", $response->getContent());
$chars = $response->toArray();
$this->assertEquals(10, count($chars[0]));
$this->assertArrayHasKey('id', $chars[0]);
$this->assertArrayHasKey('name', $chars[0]);
$this->assertArrayHasKey('dojo', $chars[0]);
$this->assertArrayHasKey('strength', $chars[0]); // not accessible via this route
$this->assertArrayHasKey('constitution', $chars[0]); // not accessible via this route
$this->assertArrayHasKey('agility', $chars[0]); // not accessible via this route
$this->assertArrayHasKey('techniques', $chars[0]); // not accessible via this route
$this->assertEquals('/api/dojos/' . $dojo->getId()
->toBase32(), $chars[0]['dojo']);
$this->assertArrayHasKey('age', $chars[0]);
$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
namespace App\Tests;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use App\Factory\DojoFactory;
use App\Factory\UserFactory;
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!
@ -37,22 +19,63 @@ class DojoTest extends ApiTestCase
$this->assertCount(0, $userRepository->findByAuthName($userName));
static::createClient()->request('POST', '/api/dojos',
[
'headers' => [
'accept' => 'application/json',
'X-AUTH-TOKEN' => $this->generateAuthToken($userName)
],
'json' => [
'name' => $dojoName
]
]);
static::createClientWithToken($userName)->request('POST', '/api/dojos', [
'json' => [
'name' => $dojoName
]
]);
$this->assertResponseStatusCodeSame(201);
$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!
*/
@ -67,16 +90,11 @@ class DojoTest extends ApiTestCase
])
]);
static::createClient()->request('POST', '/api/dojos',
[
'headers' => [
'accept' => 'application/json',
'X-AUTH-TOKEN' => $this->generateAuthToken($userName)
],
'json' => [
'name' => $dojoName
]
]);
static::createClientWithToken($userName)->request('POST', '/api/dojos', [
'json' => [
'name' => $dojoName
]
]);
$this->assertResponseStatusCodeSame(409); // 409 Conflict
}
@ -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' => [
'content-type' => 'application/merge-patch+json',
'accept' => 'application/json',
'X-AUTH-TOKEN' => $this->generateAuthToken($userName)
'content-type' => 'application/merge-patch+json'
],
'json' => [
'name' => $newDojoName

Loading…
Cancel
Save