Fixed a lot of entities and improved the character routes

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

@ -8,7 +8,6 @@
<resource class="App\Entity\User"> <resource class="App\Entity\User">
<operations> <operations>
<operation class="ApiPlatform\Metadata\Post" />
<operation class="ApiPlatform\Metadata\Get" /> <operation class="ApiPlatform\Metadata\Get" />
<operation class="ApiPlatform\Metadata\Get" controller="App\Controller\GetUserByName" uriTemplate="users/authName/{authName}" description="Receives a user by its authentication name"> <operation class="ApiPlatform\Metadata\Get" controller="App\Controller\GetUserByName" uriTemplate="users/authName/{authName}" description="Receives a user by its authentication name">
<uriVariables><uriVariable parameterName="authName"></uriVariable></uriVariables> <uriVariables><uriVariable parameterName="authName"></uriVariable></uriVariables>
@ -93,7 +92,7 @@
<resource class="App\Entity\Dungeon"> <resource class="App\Entity\Dungeon">
<operations> <operations>
<operation class="ApiPlatform\Metadata\Get" /> <operation class="ApiPlatform\Metadata\Get" uriTemplate="dungeon/{id}" />
</operations> </operations>
</resource> </resource>
<resource class="App\Entity\Dungeon" uriTemplate="/city/{cityId}/dungeon"> <resource class="App\Entity\Dungeon" uriTemplate="/city/{cityId}/dungeon">
@ -101,7 +100,21 @@
<uriVariable parameterName="cityId" fromClass="App\Entity\City" toProperty="dungeon"></uriVariable> <uriVariable parameterName="cityId" fromClass="App\Entity\City" toProperty="dungeon"></uriVariable>
</uriVariables> </uriVariables>
<operations> <operations>
<operation class="ApiPlatform\Metadata\GetCollection" /> <operation class="ApiPlatform\Metadata\Get" />
</operations>
</resource>
<resource class="App\Entity\Character">
<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>
<operation class="ApiPlatform\Metadata\GetCollection" controller="App\Controller\GetOwnDojoCharacters" uriTemplate="dojo/characters" description="Receives the characters from the users dojo.">
<normalizationContext><values><value name="groups"><values><value>public</value><value>detail</value></values></value></values></normalizationContext>
</operation>
</operations> </operations>
</resource> </resource>

@ -28,7 +28,7 @@ security:
# Note: Only the *first* access control that matches will be used # Note: Only the *first* access control that matches will be used
access_control: access_control:
- { path: ^/admin, roles: ROLE_ADMIN } - { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/api, roles: ROLE_USER } # - { path: ^/api, roles: ROLE_USER }
when@test: when@test:
security: security:

@ -0,0 +1,19 @@
<?php
namespace App\Controller;
use App\Entity\Character;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\Attribute\AsController;
#[AsController]
final class GetDojoCharacters
{
public function __invoke($dojoId, EntityManagerInterface $em): iterable
{
return $em->getRepository(Character::class)->findBy([
'dojo' => $dojoId
]);
}
}

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

@ -1,24 +1,59 @@
<?php <?php
namespace App\Entity; namespace App\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn; use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToMany;
use Doctrine\ORM\Mapping\ManyToOne; use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\Table;
use Symfony\Component\Serializer\Annotation\Groups;
#[ORM\Entity] #[Entity]
#[Table(name: '`character`')]
class Character extends Thing class Character extends Thing
{ {
#[ManyToOne()] #[ManyToOne()]
#[JoinColumn(onDelete: 'cascade')] #[JoinColumn(onDelete: 'cascade')]
#[Groups('public')]
public ?Dojo $dojo; public ?Dojo $dojo;
#[Column]
#[Groups('public')]
public string $name;
#[Column]
#[Groups('detail')]
public int $strength;
#[Column]
#[Groups('detail')]
public int $constitution;
#[Column]
#[Groups('detail')]
public int $agility;
/** @var Technique[] */
#[Groups('detail')]
#[ManyToMany(targetEntity: Technique::class)]
#[JoinColumn(nullable: false)]
public iterable $techniques;
public function __construct()
{
$this->techniques = new ArrayCollection();
}
/** /**
* Calculates the aged based on the ulid value? * Calculates the aged based on the ulid value?
*/ */
public function getAge(): int public function getAge(): int
{ {
return 17; return 21;
} }
public function getDojo(): ?Dojo public function getDojo(): ?Dojo
@ -32,5 +67,96 @@ class Character extends Thing
return $this; return $this;
} }
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getStrength(): ?int
{
return $this->strength;
}
public function setStrength(int $strength): static
{
$this->strength = $strength;
return $this;
}
public function getConstition(): ?int
{
return $this->constition;
}
public function setConstition(int $constition): static
{
$this->constition = $constition;
return $this;
}
public function getAgility(): ?int
{
return $this->agility;
}
public function setAgility(int $agility): static
{
$this->agility = $agility;
return $this;
}
/**
* @return Collection<int, Technique>
*/
public function getTechniques(): Collection
{
return $this->techniques;
}
public function setTechniques(string $techniques): static
{
$this->techniques = $techniques;
return $this;
}
public function addTechnique(Technique $technique): static
{
if (!$this->techniques->contains($technique)) {
$this->techniques->add($technique);
}
return $this;
}
public function removeTechnique(Technique $technique): static
{
$this->techniques->removeElement($technique);
return $this;
}
public function getConstitution(): ?int
{
return $this->constitution;
}
public function setConstitution(int $constitution): static
{
$this->constitution = $constitution;
return $this;
}
} }

@ -26,8 +26,6 @@ class Country extends Thing
$this->prefectures = new ArrayCollection(); $this->prefectures = new ArrayCollection();
} }
// FIXME:Shortcut to its users?
public function getCapital(): ?City public function getCapital(): ?City
{ {
return $this->capital; return $this->capital;

@ -29,6 +29,7 @@ class Dojo extends Thing
#[JoinColumn(onDelete: 'cascade', nullable: true)] #[JoinColumn(onDelete: 'cascade', nullable: true)]
public ?Village $village; public ?Village $village;
#[ApiProperty(writable: false)]
#[OneToOne(inversedBy: 'dojo', cascade: [ #[OneToOne(inversedBy: 'dojo', cascade: [
'persist' 'persist'
])] ])]
@ -61,7 +62,6 @@ class Dojo extends Thing
} }
/** /**
*
* @return Collection<int, Character> * @return Collection<int, Character>
*/ */
public function getMembers(): Collection public function getMembers(): Collection
@ -71,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);
} }

@ -13,6 +13,7 @@ use Doctrine\ORM\Mapping\OneToOne;
class Prefecture extends Thing class Prefecture extends Thing
{ {
// FIXME:Shortcut to its users?
#[OneToOne()] #[OneToOne()]
#[JoinColumn(onDelete: 'cascade', nullable: false)] #[JoinColumn(onDelete: 'cascade', nullable: false)]
public City $capital; public City $capital;
@ -26,7 +27,6 @@ class Prefecture extends Thing
#[JoinColumn(onDelete: 'cascade', nullable: false)] #[JoinColumn(onDelete: 'cascade', nullable: false)]
public iterable $villages; public iterable $villages;
// FIXME:Shortcut to its users?
public function __construct() public function __construct()
{ {
$this->villages = new ArrayCollection(); $this->villages = new ArrayCollection();

@ -0,0 +1,104 @@
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\OneToOne;
#[Entity]
class Technique extends Thing
{
/**
* For now we calculate costs for mastering a technique.
*/
#[Column]
public int $costs;
/**
* Forumula to calculate the damage based on the stats.
*/
#[Column]
public string $damage;
/**
* Forumula to calculate the damage based on the stats.
*/
#[Column]
public string $energy;
/**
* Forumula to calculate the damage based on the stats.
*/
#[Column]
public string $accuracy;
/**
* Technique that is required to be learned before this Technique.
*/
#[OneToOne()]
#[JoinColumn(onDelete: 'SET NULL', nullable: true)]
public ?Technique $prerequisite;
public function getCosts(): ?int
{
return $this->costs;
}
public function setCosts(int $costs): static
{
$this->costs = $costs;
return $this;
}
public function getDamage(): ?string
{
return $this->damage;
}
public function setDamage(string $damage): static
{
$this->damage = $damage;
return $this;
}
public function getEnergy(): ?string
{
return $this->energy;
}
public function setEnergy(string $energy): static
{
$this->energy = $energy;
return $this;
}
public function getAccuracy(): ?string
{
return $this->accuracy;
}
public function setAccuracy(string $accuracy): static
{
$this->accuracy = $accuracy;
return $this;
}
public function getPrerequisite(): ?self
{
return $this->prerequisite;
}
public function setPrerequisite(?self $prerequisite): static
{
$this->prerequisite = $prerequisite;
return $this;
}
}

@ -15,11 +15,12 @@ class User extends Thing implements UserInterface
{ {
// from discord // from discord
#[ApiProperty(writable: true)] #[ApiProperty(writable: false)]
#[Column(type: 'string')] #[Column(type: 'string')]
public string $authName; public string $authName;
#[OneToOne(inversedBy: 'dojo')] #[ApiProperty(writable: false)]
#[OneToOne(inversedBy: 'owner')]
#[JoinColumn(onDelete: 'cascade', nullable: true)] #[JoinColumn(onDelete: 'cascade', nullable: true)]
public ?Dojo $dojo; public ?Dojo $dojo;
@ -74,5 +75,17 @@ class User extends Thing implements UserInterface
"ROLE_USER" "ROLE_USER"
]; ];
} }
public function getAuthName(): ?string
{
return $this->authName;
}
public function setAuthName(string $authName): static
{
$this->authName = $authName;
return $this;
}
} }

@ -3,12 +3,12 @@ namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn; use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne; use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany; use Doctrine\ORM\Mapping\OneToMany;
#[ORM\Entity] #[Entity]
class Village extends Thing class Village extends Thing
{ {

@ -0,0 +1,72 @@
<?php
namespace App\Factory;
use App\Entity\Character;
use Zenstruck\Foundry\ModelFactory;
/**
*
* @extends ModelFactory<Character>
*
* @method Character|Proxy create(array|callable $attributes = [])
* @method static Character|Proxy createOne(array $attributes = [])
* @method static Character|Proxy find(object|array|mixed $criteria)
* @method static Character|Proxy findOrCreate(array $attributes)
* @method static Character|Proxy first(string $sortedField = 'id')
* @method static Character|Proxy last(string $sortedField = 'id')
* @method static Character|Proxy random(array $attributes = [])
* @method static Character|Proxy randomOrCreate(array $attributes = [])
* @method static EntityRepository|RepositoryProxy repository()
* @method static Character[]|Proxy[] all()
* @method static Character[]|Proxy[] createMany(int $number, array|callable $attributes = [])
* @method static Character[]|Proxy[] createSequence(iterable|callable $sequence)
* @method static Character[]|Proxy[] findBy(array $attributes)
* @method static Character[]|Proxy[] randomRange(int $min, int $max, array $attributes = [])
* @method static Character[]|Proxy[] randomSet(int $number, array $attributes = [])
*/
final class CharacterFactory extends ModelFactory
{
/**
*
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services
*
* @todo inject services if required
*/
public function __construct()
{
parent::__construct();
}
/**
*
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories
*
* @todo add your default values here
*/
protected function getDefaults(): array
{
return [
'name' => self::faker()->text(),
'strength' => self::faker()->numberBetween(1, 4),
'constitution' => self::faker()->numberBetween(1, 4),
'agility' => self::faker()->numberBetween(1, 4),
'techniques' => TechniqueFactory::createMany(2)
];
}
/**
*
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
*/
protected function initialize(): self
{
return $this;
// ->afterInstantiate(function(Character $character): void {})
}
protected static function getClass(): string
{
return Character::class;
}
}

@ -0,0 +1,72 @@
<?php
namespace App\Factory;
use App\Entity\Technique;
use Zenstruck\Foundry\ModelFactory;
/**
*
* @extends ModelFactory<Technique>
*
* @method Technique|Proxy create(array|callable $attributes = [])
* @method static Technique|Proxy createOne(array $attributes = [])
* @method static Technique|Proxy find(object|array|mixed $criteria)
* @method static Technique|Proxy findOrCreate(array $attributes)
* @method static Technique|Proxy first(string $sortedField = 'id')
* @method static Technique|Proxy last(string $sortedField = 'id')
* @method static Technique|Proxy random(array $attributes = [])
* @method static Technique|Proxy randomOrCreate(array $attributes = [])
* @method static EntityRepository|RepositoryProxy repository()
* @method static Technique[]|Proxy[] all()
* @method static Technique[]|Proxy[] createMany(int $number, array|callable $attributes = [])
* @method static Technique[]|Proxy[] createSequence(iterable|callable $sequence)
* @method static Technique[]|Proxy[] findBy(array $attributes)
* @method static Technique[]|Proxy[] randomRange(int $min, int $max, array $attributes = [])
* @method static Technique[]|Proxy[] randomSet(int $number, array $attributes = [])
*/
final class TechniqueFactory extends ModelFactory
{
/**
*
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services
*
* @todo inject services if required
*/
public function __construct()
{
parent::__construct();
}
/**
*
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories
*
* @todo add your default values here
*/
protected function getDefaults(): array
{
return [
'accuracy' => self::faker()->text(),
'costs' => self::faker()->numberBetween(1, 2),
'damage' => self::faker()->text(),
'energy' => self::faker()->text(),
'prerequisite' => self::faker()->boolean() ? NULL : self::new()
];
}
/**
*
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
*/
protected function initialize(): self
{
return $this;
// ->afterInstantiate(function(Technique $technique): void {})
}
protected static function getClass(): string
{
return Technique::class;
}
}

@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
podman run --rm --name ag-dojo-postgres -e POSTGRES_USER=foo -e POSTGRES_PASSWORD=bar -e POSTGRES_HOST_AUTH_METHOD=trust -p "5432:5432" docker.io/library/postgres:14.5 podman run --rm --name ag-dojo-postgres -e POSTGRES_USER=foo -e POSTGRES_PASSWORD=bar -e POSTGRES_HOST_AUTH_METHOD=trust -p "5432:5432" docker.io/library/postgres:14.10

@ -0,0 +1,100 @@
<?php
namespace App\Tests;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use App\Factory\CharacterFactory;
use App\Factory\DojoFactory;
use App\Factory\UserFactory;
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase;
use DateTimeZone;
class CharacterTest extends ApiTestCase
{
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!
*/
public function testRetrieveCharactersFromDojoPublic(): void
{
$requestUser = UserFactory::createOne();
$dojo = DojoFactory::createOne();
CharacterFactory::createMany(4, [
'dojo' => $dojo
]);
CharacterFactory::createMany(10);
$response = static::createClient()->request('GET', '/api/dojo/' . $dojo->id . '/characters',
[
'headers' => [
'accept' => 'application/json',
'X-AUTH-TOKEN' => $this->generateAuthToken($requestUser->authName)
]
]);
$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->assertArrayHasKey('name', $chars[0]);
$this->assertArrayHasKey('dojo', $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('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!
*/
public function testRetrieveCharactersFromOwnDojoDetail(): void
{
$dojo = DojoFactory::createOne([
'owner' => UserFactory::createOne()
]);
CharacterFactory::createMany(4, [
'dojo' => $dojo
]);
CharacterFactory::createMany(10);
$response = static::createClient()->request('GET', '/api/dojo/characters',
[
'headers' => [
'accept' => 'application/json',
'X-AUTH-TOKEN' => $this->generateAuthToken($dojo->getOwner()->authName)
]
]);
$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->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
}
}

@ -80,5 +80,37 @@ class DojoTest extends ApiTestCase
$this->assertResponseStatusCodeSame(409); // 409 Conflict $this->assertResponseStatusCodeSame(409); // 409 Conflict
} }
/**
* Requirement: A user should be able to change the dojos name!
* FIXME: Add limitation so users will not do this frequently.
*/
public function testChangeDojoName(): void
{
$userName = "FooBarFigher";
$dojoName = "BigFightDojo";
$newDojoName = "BigFightDojo";
$dojo = DojoFactory::createOne(
[
'name' => $dojoName,
'owner' => UserFactory::createOne([
'authName' => $userName
])
]);
static::createClient()->request('PATCH', '/api/dojos/' . $dojo->id,
[
'headers' => [
'content-type' => 'application/merge-patch+json',
'accept' => 'application/json',
'X-AUTH-TOKEN' => $this->generateAuthToken($userName)
],
'json' => [
'name' => $newDojoName
]
]);
$this->assertResponseStatusCodeSame(200);
}
} }

Loading…
Cancel
Save