diff --git a/config/api_platform/resources.xml b/config/api_platform/resources.xml index aaee0ca..5f6a9db 100644 --- a/config/api_platform/resources.xml +++ b/config/api_platform/resources.xml @@ -8,7 +8,6 @@ - @@ -93,7 +92,7 @@ - + @@ -101,7 +100,21 @@ - + + + + + + public + + + + + + + + publicdetail + diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 7ea784a..600e3a0 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -28,7 +28,7 @@ security: # Note: Only the *first* access control that matches will be used access_control: - { path: ^/admin, roles: ROLE_ADMIN } - - { path: ^/api, roles: ROLE_USER } + # - { path: ^/api, roles: ROLE_USER } when@test: security: diff --git a/src/Controller/GetDojoCharacters.php b/src/Controller/GetDojoCharacters.php new file mode 100644 index 0000000..f77fe19 --- /dev/null +++ b/src/Controller/GetDojoCharacters.php @@ -0,0 +1,19 @@ +getRepository(Character::class)->findBy([ + 'dojo' => $dojoId + ]); + } +} + diff --git a/src/Controller/GetOwnDojoCharacters.php b/src/Controller/GetOwnDojoCharacters.php new file mode 100644 index 0000000..34fe843 --- /dev/null +++ b/src/Controller/GetOwnDojoCharacters.php @@ -0,0 +1,26 @@ +getRepository(Dojo::class)->findOneByOwner($this->security->getUser()); + + return $em->getRepository(Character::class)->findBy([ + 'dojo' => $dojo->id + ]); + } +} + diff --git a/src/Entity/Character.php b/src/Entity/Character.php index e3877a1..58569f6 100644 --- a/src/Entity/Character.php +++ b/src/Entity/Character.php @@ -1,24 +1,59 @@ techniques = new ArrayCollection(); + } + /** * Calculates the aged based on the ulid value? */ public function getAge(): int { - return 17; + return 21; } public function getDojo(): ?Dojo @@ -32,5 +67,96 @@ class Character extends Thing 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 + */ + 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; + } } diff --git a/src/Entity/Country.php b/src/Entity/Country.php index 2920b63..dee12a7 100644 --- a/src/Entity/Country.php +++ b/src/Entity/Country.php @@ -26,8 +26,6 @@ class Country extends Thing $this->prefectures = new ArrayCollection(); } - // FIXME:Shortcut to its users? - public function getCapital(): ?City { return $this->capital; diff --git a/src/Entity/Dojo.php b/src/Entity/Dojo.php index c2acf34..dd2ecd6 100644 --- a/src/Entity/Dojo.php +++ b/src/Entity/Dojo.php @@ -29,6 +29,7 @@ class Dojo extends Thing #[JoinColumn(onDelete: 'cascade', nullable: true)] public ?Village $village; + #[ApiProperty(writable: false)] #[OneToOne(inversedBy: 'dojo', cascade: [ 'persist' ])] @@ -61,7 +62,6 @@ class Dojo extends Thing } /** - * * @return Collection */ public function getMembers(): Collection @@ -71,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); } diff --git a/src/Entity/Prefecture.php b/src/Entity/Prefecture.php index 11cf891..b8c8d3f 100644 --- a/src/Entity/Prefecture.php +++ b/src/Entity/Prefecture.php @@ -13,6 +13,7 @@ use Doctrine\ORM\Mapping\OneToOne; class Prefecture extends Thing { + // FIXME:Shortcut to its users? #[OneToOne()] #[JoinColumn(onDelete: 'cascade', nullable: false)] public City $capital; @@ -26,7 +27,6 @@ class Prefecture extends Thing #[JoinColumn(onDelete: 'cascade', nullable: false)] public iterable $villages; - // FIXME:Shortcut to its users? public function __construct() { $this->villages = new ArrayCollection(); diff --git a/src/Entity/Technique.php b/src/Entity/Technique.php new file mode 100644 index 0000000..b16d980 --- /dev/null +++ b/src/Entity/Technique.php @@ -0,0 +1,104 @@ +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; + } +} + diff --git a/src/Entity/User.php b/src/Entity/User.php index d26c7a9..e50aa35 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -15,11 +15,12 @@ class User extends Thing implements UserInterface { // from discord - #[ApiProperty(writable: true)] + #[ApiProperty(writable: false)] #[Column(type: 'string')] public string $authName; - #[OneToOne(inversedBy: 'dojo')] + #[ApiProperty(writable: false)] + #[OneToOne(inversedBy: 'owner')] #[JoinColumn(onDelete: 'cascade', nullable: true)] public ?Dojo $dojo; @@ -74,5 +75,17 @@ class User extends Thing implements UserInterface "ROLE_USER" ]; } + + public function getAuthName(): ?string + { + return $this->authName; + } + + public function setAuthName(string $authName): static + { + $this->authName = $authName; + + return $this; + } } diff --git a/src/Entity/Village.php b/src/Entity/Village.php index d1632b1..ed8d675 100644 --- a/src/Entity/Village.php +++ b/src/Entity/Village.php @@ -3,12 +3,12 @@ namespace App\Entity; use Doctrine\Common\Collections\ArrayCollection; 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\ManyToOne; use Doctrine\ORM\Mapping\OneToMany; -#[ORM\Entity] +#[Entity] class Village extends Thing { diff --git a/src/Factory/CharacterFactory.php b/src/Factory/CharacterFactory.php new file mode 100644 index 0000000..bf9cdbb --- /dev/null +++ b/src/Factory/CharacterFactory.php @@ -0,0 +1,72 @@ + + * + * @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; + } +} diff --git a/src/Factory/TechniqueFactory.php b/src/Factory/TechniqueFactory.php new file mode 100644 index 0000000..c07d1b8 --- /dev/null +++ b/src/Factory/TechniqueFactory.php @@ -0,0 +1,72 @@ + + * + * @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; + } +} diff --git a/start_postgres.sh b/start_postgres.sh index 4749fe9..65dc973 100755 --- a/start_postgres.sh +++ b/start_postgres.sh @@ -1,3 +1,3 @@ #!/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 diff --git a/tests/CharacterTest.php b/tests/CharacterTest.php new file mode 100644 index 0000000..627202a --- /dev/null +++ b/tests/CharacterTest.php @@ -0,0 +1,100 @@ +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 + } +} + diff --git a/tests/DojoTest.php b/tests/DojoTest.php index da20010..0975c82 100644 --- a/tests/DojoTest.php +++ b/tests/DojoTest.php @@ -80,5 +80,37 @@ class DojoTest extends ApiTestCase $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); + } }