| @@ -78,7 +78,9 @@ class ContactApi | |||||
| /** | /** | ||||
| * @var array<int, PostingApi> | * @var array<int, PostingApi> | ||||
| */ | */ | ||||
| #[ApiProperty(writable: false)] | |||||
| public array $postings = []; | public array $postings = []; | ||||
| #[ApiProperty(writable: false)] | |||||
| public ?\DateTimeImmutable $createdAt = null; | public ?\DateTimeImmutable $createdAt = null; | ||||
| } | } | ||||
| @@ -60,7 +60,8 @@ class PostingApi | |||||
| #[NotBlank] | #[NotBlank] | ||||
| public ?string $message = null; | public ?string $message = null; | ||||
| #[IsValidOwner] | |||||
| //#[IsValidOwner] | |||||
| #[ApiProperty(writable: false)] | |||||
| public ?UserApi $owner = null; | public ?UserApi $owner = null; | ||||
| #[ApiProperty(writable: false)] | #[ApiProperty(writable: false)] | ||||
| @@ -29,7 +29,7 @@ use Symfony\Component\Validator\Constraints as Assert; | |||||
| security: 'is_granted("ROLE_USER")' | security: 'is_granted("ROLE_USER")' | ||||
| ), | ), | ||||
| new Post( | new Post( | ||||
| security: 'is_granted("PUBLIC_ACCESS")', | |||||
| security: 'is_granted("ROLE_ADMIN")', | |||||
| validationContext: ['groups' => ['Default', 'postValidation']], | validationContext: ['groups' => ['Default', 'postValidation']], | ||||
| ), | ), | ||||
| new Patch( | new Patch( | ||||
| @@ -48,8 +48,9 @@ class Contact | |||||
| #[ORM\OneToMany(mappedBy: 'contact', targetEntity: Posting::class, orphanRemoval: true)] | #[ORM\OneToMany(mappedBy: 'contact', targetEntity: Posting::class, orphanRemoval: true)] | ||||
| private Collection $postings; | private Collection $postings; | ||||
| public function __construct() | |||||
| public function __construct(Partner $partner) | |||||
| { | { | ||||
| $this->partner = $partner; | |||||
| $this->createdAt = new \DateTimeImmutable(); | $this->createdAt = new \DateTimeImmutable(); | ||||
| $this->postings = new ArrayCollection(); | $this->postings = new ArrayCollection(); | ||||
| } | } | ||||
| @@ -39,8 +39,11 @@ class Posting | |||||
| #[ORM\OneToMany(mappedBy: 'posting', targetEntity: Comment::class, orphanRemoval: true)] | #[ORM\OneToMany(mappedBy: 'posting', targetEntity: Comment::class, orphanRemoval: true)] | ||||
| private Collection $comments; | private Collection $comments; | ||||
| public function __construct() | |||||
| public function __construct(User $owner, Partner $partner, Contact $contact = null) | |||||
| { | { | ||||
| $this->owner = $owner; | |||||
| $this->partner = $partner; | |||||
| $this->contact = $contact; | |||||
| $this->createdAt = new \DateTimeImmutable(); | $this->createdAt = new \DateTimeImmutable(); | ||||
| $this->comments = new ArrayCollection(); | $this->comments = new ArrayCollection(); | ||||
| } | } | ||||
| @@ -72,13 +75,6 @@ class Posting | |||||
| return $this->owner; | return $this->owner; | ||||
| } | } | ||||
| public function setOwner(?User $owner): static | |||||
| { | |||||
| $this->owner = $owner; | |||||
| return $this; | |||||
| } | |||||
| public function getHeadline(): ?string | public function getHeadline(): ?string | ||||
| { | { | ||||
| return $this->headline; | return $this->headline; | ||||
| @@ -4,6 +4,8 @@ namespace App\Mapper; | |||||
| use App\ApiResource\ContactApi; | use App\ApiResource\ContactApi; | ||||
| use App\Entity\Contact; | use App\Entity\Contact; | ||||
| use App\Entity\Partner; | |||||
| use App\Repository\ContactRepository; | |||||
| use App\Repository\PartnerRepository; | use App\Repository\PartnerRepository; | ||||
| use Symfony\Bundle\SecurityBundle\Security; | use Symfony\Bundle\SecurityBundle\Security; | ||||
| use Symfonycasts\MicroMapper\AsMapper; | use Symfonycasts\MicroMapper\AsMapper; | ||||
| @@ -14,7 +16,7 @@ use Symfonycasts\MicroMapper\MicroMapperInterface; | |||||
| class ContactApiToEntityMapper implements MapperInterface | class ContactApiToEntityMapper implements MapperInterface | ||||
| { | { | ||||
| public function __construct( | public function __construct( | ||||
| private PartnerRepository $repository, | |||||
| private ContactRepository $repository, | |||||
| private Security $security, | private Security $security, | ||||
| private MicroMapperInterface $microMapper, | private MicroMapperInterface $microMapper, | ||||
| ) | ) | ||||
| @@ -27,9 +29,21 @@ class ContactApiToEntityMapper implements MapperInterface | |||||
| $dto = $from; | $dto = $from; | ||||
| assert($dto instanceof ContactApi); | assert($dto instanceof ContactApi); | ||||
| $entity = $dto->id ? $this->repository->find($dto->id) : new Contact(); | |||||
| if ($dto->id) { | |||||
| $entity = $this->repository->find($dto->id); | |||||
| } else { | |||||
| if ($dto->partner === null) { | |||||
| throw new \Exception('Partner missing'); | |||||
| } | |||||
| $partner = $this->microMapper->map($dto->partner, Partner::class, [ | |||||
| MicroMapperInterface::MAX_DEPTH => 0, | |||||
| ]); | |||||
| $entity = new Contact($partner); | |||||
| } | |||||
| if (!$entity) { | if (!$entity) { | ||||
| throw new \Exception('Partner not found'); | |||||
| throw new \Exception('Contact not found'); | |||||
| } | } | ||||
| return $entity; | return $entity; | ||||
| @@ -2,12 +2,12 @@ | |||||
| namespace App\Mapper; | namespace App\Mapper; | ||||
| use App\ApiResource\DragonTreasureApi; | |||||
| use App\ApiResource\ContactApi; | |||||
| use App\ApiResource\PostingApi; | use App\ApiResource\PostingApi; | ||||
| use App\Entity\DragonTreasure; | |||||
| use App\Entity\Contact; | |||||
| use App\Entity\Partner; | |||||
| use App\Entity\User; | use App\Entity\User; | ||||
| use App\Entity\Posting; | use App\Entity\Posting; | ||||
| use App\Repository\DragonTreasureRepository; | |||||
| use App\Repository\PostingRepository; | use App\Repository\PostingRepository; | ||||
| use Symfony\Bundle\SecurityBundle\Security; | use Symfony\Bundle\SecurityBundle\Security; | ||||
| use Symfonycasts\MicroMapper\AsMapper; | use Symfonycasts\MicroMapper\AsMapper; | ||||
| @@ -31,7 +31,29 @@ class PostingApiToEntityMapper implements MapperInterface | |||||
| $dto = $from; | $dto = $from; | ||||
| assert($dto instanceof PostingApi); | assert($dto instanceof PostingApi); | ||||
| $entity = $dto->id ? $this->repository->find($dto->id) : new Posting(); | |||||
| if ($dto->id) { | |||||
| $entity = $this->repository->find($dto->id); | |||||
| } else { | |||||
| $user = $this->security->getUser(); | |||||
| assert($user instanceof User); | |||||
| if ($dto->partner === null) { | |||||
| throw new \Exception('Partner missing'); | |||||
| } | |||||
| $partner = $this->microMapper->map($dto->partner, Partner::class, [ | |||||
| MicroMapperInterface::MAX_DEPTH => 0, | |||||
| ]); | |||||
| $contact = null; | |||||
| if ($dto->contact) { | |||||
| assert($dto->contact instanceof ContactApi); | |||||
| $contact = $this->microMapper->map($dto->contact, Contact::class, [ | |||||
| MicroMapperInterface::MAX_DEPTH => 0, | |||||
| ]); | |||||
| } | |||||
| assert($partner instanceof Partner); | |||||
| assert($contact === null || $contact instanceof Contact); | |||||
| $entity = new Posting($user, $partner, $contact); | |||||
| } | |||||
| if (!$entity) { | if (!$entity) { | ||||
| throw new \Exception('Posting not found'); | throw new \Exception('Posting not found'); | ||||
| } | } | ||||
| @@ -46,16 +68,7 @@ class PostingApiToEntityMapper implements MapperInterface | |||||
| $entity = $to; | $entity = $to; | ||||
| assert($entity instanceof Posting); | assert($entity instanceof Posting); | ||||
| if ($dto->owner) { | |||||
| $entity->setOwner($this->microMapper->map($dto->owner, User::class, [ | |||||
| MicroMapperInterface::MAX_DEPTH => 0, | |||||
| ])); | |||||
| } else { | |||||
| $entity->setOwner($this->security->getUser()); | |||||
| } | |||||
| $entity->setPartner($dto->partner); | |||||
| $entity->setContact($dto->contact); | |||||
| $entity->setHeadline($dto->headline); | |||||
| $entity->setMessage($dto->message); | $entity->setMessage($dto->message); | ||||
| return $entity; | return $entity; | ||||
| @@ -0,0 +1,81 @@ | |||||
| <?php | |||||
| /** | |||||
| * @author Daniel Knudsen <d.knudsen@spawntree.de> | |||||
| * @date 12.12.23 | |||||
| */ | |||||
| namespace App\Tests\Functional; | |||||
| use App\Factory\ContactFactory; | |||||
| use App\Factory\MediaObjectLogoFactory; | |||||
| use App\Factory\MediaObjectProfileFactory; | |||||
| use App\Factory\PartnerFactory; | |||||
| use App\Factory\PostingFactory; | |||||
| use App\Factory\UserFactory; | |||||
| use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | |||||
| use Zenstruck\Browser\Test\HasBrowser; | |||||
| use Zenstruck\Foundry\Test\Factories; | |||||
| use Zenstruck\Foundry\Test\ResetDatabase; | |||||
| class PostingResourceTest extends KernelTestCase | |||||
| { | |||||
| use HasBrowser; | |||||
| use ResetDatabase; | |||||
| use Factories; | |||||
| public function testPostPosting(): void | |||||
| { | |||||
| UserFactory::createOne( | |||||
| [ | |||||
| 'email' => 'peter@test.de', | |||||
| 'firstName' => 'Peter', | |||||
| 'lastName' => 'Test', | |||||
| 'password' => 'test', | |||||
| ] | |||||
| ); | |||||
| MediaObjectProfileFactory::createOne(); | |||||
| MediaObjectLogoFactory::createOne(); | |||||
| $partner = PartnerFactory::createOne(); | |||||
| $contact = ContactFactory::createOne(); | |||||
| PostingFactory::createOne(); | |||||
| $response = $this->browser() | |||||
| ->post('/auth', [ | |||||
| 'json' => [ | |||||
| 'email' => 'peter@test.de', | |||||
| 'password' => 'test', | |||||
| ] | |||||
| ]) | |||||
| ->assertSuccessful() | |||||
| ->content(); | |||||
| $authResponseData = json_decode($response, true); | |||||
| $token = $authResponseData['token']; | |||||
| $this->browser() | |||||
| ->post('/api/posts' , [ | |||||
| 'json' => [ | |||||
| 'headline' => 'hello', | |||||
| 'message' => 'world', | |||||
| 'partner' => '/api/partners/' . $partner->getId(), | |||||
| 'contact' => '/api/contacts/' . $contact->getId(), | |||||
| ], | |||||
| 'headers' => [ | |||||
| 'Authorization' => 'Bearer ' . $token, | |||||
| ] | |||||
| ]) | |||||
| ->assertSuccessful() | |||||
| ; | |||||
| $this->browser() | |||||
| ->get('/api/posts', [ | |||||
| 'headers' => [ | |||||
| 'Authorization' => 'Bearer ' . $token, | |||||
| ], | |||||
| ]) | |||||
| ->assertSuccessful() | |||||
| ->assertJsonMatches('"hydra:totalItems"', 2) | |||||
| ; | |||||
| } | |||||
| } | |||||
| @@ -20,95 +20,98 @@ class UserResourceTest extends KernelTestCase | |||||
| use ResetDatabase; | use ResetDatabase; | ||||
| use Factories; | use Factories; | ||||
| public function testPostToCreateUser(): void | |||||
| public function testPostUser(): void | |||||
| { | { | ||||
| $this->browser() | |||||
| ->post('/api/users', [ | |||||
| UserFactory::createOne( | |||||
| [ | |||||
| 'email' => 'peter@test.de', | |||||
| 'firstName' => 'Peter', | |||||
| 'lastName' => 'Test', | |||||
| 'password' => 'test', | |||||
| 'roles' => ["ROLE_ADMIN"] | |||||
| ] | |||||
| ); | |||||
| $response = $this->browser() | |||||
| ->post('/auth', [ | |||||
| 'json' => [ | 'json' => [ | ||||
| 'email' => 'draggin_in_the_morning@coffee.com', | |||||
| 'firstName' => 'Danny', | |||||
| 'lastName' => 'Boy', | |||||
| 'password' => 'password', | |||||
| 'email' => 'peter@test.de', | |||||
| 'password' => 'test', | |||||
| ] | ] | ||||
| ]) | ]) | ||||
| ->assertStatus(201) | |||||
| ->use(function (Json $json) { | ->use(function (Json $json) { | ||||
| $json->assertMissing('password'); | |||||
| $json->assertMissing('id'); | |||||
| $json->assertHas('token'); | |||||
| $json->assertHas('id'); | |||||
| $json->assertHas('email'); | |||||
| $json->assertHas('firstName'); | |||||
| $json->assertHas('lastName'); | |||||
| $json->assertHas('roles'); | |||||
| }) | }) | ||||
| ->post('/auth', [ | |||||
| ->assertSuccessful() | |||||
| ->content(); | |||||
| $authResponseData = json_decode($response, true); | |||||
| $token = $authResponseData['token']; | |||||
| $this->browser() | |||||
| ->post('/api/users' , [ | |||||
| 'json' => [ | 'json' => [ | ||||
| 'email' => 'draggin_in_the_morning@coffee.com', | |||||
| 'password' => 'password', | |||||
| 'email' => 'norbert@test.de', | |||||
| 'firstName' => 'Norbert', | |||||
| 'lastName' => 'Test', | |||||
| 'password' => 'sehr geheim', | |||||
| ], | |||||
| 'headers' => [ | |||||
| 'Authorization' => 'Bearer ' . $token, | |||||
| ] | ] | ||||
| ]) | ]) | ||||
| ->assertSuccessful() | ->assertSuccessful() | ||||
| ; | ; | ||||
| } | |||||
| public function testGetUsersWithoutAuthentication() | |||||
| { | |||||
| UserFactory::createMany(2); | |||||
| $this->browser() | |||||
| ->get('/api/users') | |||||
| ->assertStatus(401) | |||||
| ; | |||||
| } | |||||
| public function testGetOneUserWithoutAuthentication() | |||||
| { | |||||
| UserFactory::createOne(); | |||||
| $this->browser() | |||||
| ->get('/api/users/1') | |||||
| ->assertStatus(401) | |||||
| ; | |||||
| } | |||||
| public function testPatchUserAsSameUser() | |||||
| { | |||||
| $user = UserFactory::createOne( | |||||
| [ | |||||
| 'firstName' => 'John', | |||||
| 'lastName' => 'Doe' | |||||
| ] | |||||
| ); | |||||
| $this->browser() | $this->browser() | ||||
| ->actingAs($user) | |||||
| ->patch('/api/users/' . $user->getId(), [ | |||||
| ->post('/auth', [ | |||||
| 'json' => [ | 'json' => [ | ||||
| 'firstName' => 'Joe', | |||||
| 'lastName' => 'Black' | |||||
| ], | |||||
| 'headers' => ['Content-Type' => 'application/merge-patch+json'] | |||||
| 'email' => 'norbert@test.de', | |||||
| 'password' => 'sehr geheim', | |||||
| ] | |||||
| ]) | ]) | ||||
| ->assertStatus(200) | |||||
| ->get('/api/users/' . $user->getId()) | |||||
| ->assertStatus(200) | |||||
| ->assertJsonMatches('firstName', 'Joe') | |||||
| ->assertJsonMatches('lastName', 'Black') | |||||
| ; | |||||
| ->assertSuccessful() | |||||
| ->assertJsonMatches('email', 'norbert@test.de'); | |||||
| } | } | ||||
| public function testPatchUserInactiveAsSameUser() | |||||
| public function testPostUserNoAdmin(): void | |||||
| { | { | ||||
| $user = UserFactory::createOne( | |||||
| UserFactory::createOne( | |||||
| [ | [ | ||||
| 'firstName' => 'John' | |||||
| 'email' => 'peter@test.de', | |||||
| 'firstName' => 'Peter', | |||||
| 'lastName' => 'Test', | |||||
| 'password' => 'test', | |||||
| ] | ] | ||||
| ); | ); | ||||
| $response = $this->browser() | |||||
| ->post('/auth', [ | |||||
| 'json' => [ | |||||
| 'email' => 'peter@test.de', | |||||
| 'password' => 'test', | |||||
| ] | |||||
| ]) | |||||
| ->assertSuccessful() | |||||
| ->content(); | |||||
| $authResponseData = json_decode($response, true); | |||||
| $token = $authResponseData['token']; | |||||
| $this->browser() | $this->browser() | ||||
| ->actingAs($user) | |||||
| ->patch('/api/users/' . $user->getId(), [ | |||||
| ->post('/api/users' , [ | |||||
| 'json' => [ | 'json' => [ | ||||
| 'firstName' => 'Joe' | |||||
| 'email' => 'norbert@test.de', | |||||
| 'firstName' => 'Norbert', | |||||
| 'lastName' => 'Test', | |||||
| 'password' => 'sehr geheim', | |||||
| ], | ], | ||||
| 'headers' => ['Content-Type' => 'application/merge-patch+json'] | |||||
| 'headers' => [ | |||||
| 'Authorization' => 'Bearer ' . $token, | |||||
| ] | |||||
| ]) | ]) | ||||
| ->assertStatus(200) | |||||
| ->get('/api/users/' . $user->getId()) | |||||
| ->assertJsonMatches('firstName', 'A shiny thing') | |||||
| ->assertStatus(403); | |||||
| ; | ; | ||||
| } | } | ||||
| } | } | ||||