From e030ff08db75cf02c34c7c5367473ca3af3c8d8c Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Wed, 27 Nov 2024 15:25:13 +0100 Subject: [PATCH] API endpoints to delete customer/project/activity (#5181) * added service methods with events to delete customer, project, activity * added API endpoints to delete customer, project, activity * added tests for new API endpoints --- src/API/ActivityController.php | 23 +++- src/API/CustomerController.php | 22 +++- src/API/ProjectController.php | 19 ++++ src/Activity/ActivityService.php | 8 +- src/Customer/CustomerService.php | 7 ++ src/Event/ActivityDeleteEvent.php | 17 +++ src/Event/CustomerDeleteEvent.php | 17 +++ src/Event/ProjectDeleteEvent.php | 17 +++ src/Project/ProjectService.php | 8 +- tests/API/ActivityControllerTest.php | 60 ++++++++++ tests/API/CustomerControllerTest.php | 99 ++++++++++++++++ tests/API/ProjectControllerTest.php | 161 +++++++++++++++++++-------- tests/phpstan.neon | 30 ----- 13 files changed, 408 insertions(+), 80 deletions(-) create mode 100644 src/Event/ActivityDeleteEvent.php create mode 100644 src/Event/CustomerDeleteEvent.php create mode 100644 src/Event/ProjectDeleteEvent.php diff --git a/src/API/ActivityController.php b/src/API/ActivityController.php index d659b8f2..82f7b9e1 100644 --- a/src/API/ActivityController.php +++ b/src/API/ActivityController.php @@ -9,6 +9,7 @@ namespace App\API; +use App\Activity\ActivityService; use App\Entity\Activity; use App\Entity\ActivityRate; use App\Entity\User; @@ -46,7 +47,8 @@ final class ActivityController extends BaseApiController private readonly ViewHandlerInterface $viewHandler, private readonly ActivityRepository $repository, private readonly EventDispatcherInterface $dispatcher, - private readonly ActivityRateRepository $activityRateRepository + private readonly ActivityRateRepository $activityRateRepository, + private readonly ActivityService $activityService ) { } @@ -208,6 +210,25 @@ final class ActivityController extends BaseApiController return $this->viewHandler->handle($view); } + /** + * Delete an existing activity + * + * [DANGER] This will also delete ALL linked timesheets. + * Maybe use `PATCH` instead and mark it as inactive with `visible=false`? + */ + #[IsGranted('delete', 'activity')] + #[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one activity')])] + #[OA\Parameter(name: 'id', description: 'Activity ID to delete', in: 'path', required: true)] + #[Route(path: '/{id}', name: 'delete_activity', requirements: ['id' => '\d+'], methods: ['DELETE'])] + public function deleteAction(Activity $activity): Response + { + $this->activityService->deleteActivity($activity); + + $view = new View(null, Response::HTTP_NO_CONTENT); + + return $this->viewHandler->handle($view); + } + /** * Sets the value of a meta-field for an existing activity */ diff --git a/src/API/CustomerController.php b/src/API/CustomerController.php index 947b299c..3cf899dc 100644 --- a/src/API/CustomerController.php +++ b/src/API/CustomerController.php @@ -46,7 +46,8 @@ final class CustomerController extends BaseApiController private readonly ViewHandlerInterface $viewHandler, private readonly CustomerRepository $repository, private readonly EventDispatcherInterface $dispatcher, - private readonly CustomerRateRepository $customerRateRepository + private readonly CustomerRateRepository $customerRateRepository, + private readonly CustomerService $customerService, ) { } @@ -185,6 +186,25 @@ final class CustomerController extends BaseApiController return $this->viewHandler->handle($view); } + /** + * Delete an existing customer + * + * [DANGER] This will also delete ALL linked projects, project activities and timesheets. + * Maybe use `PATCH` instead and mark it as inactive with `visible=false`? + */ + #[IsGranted('delete', 'customer')] + #[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one customer')])] + #[OA\Parameter(name: 'id', description: 'Customer ID to delete', in: 'path', required: true)] + #[Route(path: '/{id}', name: 'delete_customer', requirements: ['id' => '\d+'], methods: ['DELETE'])] + public function deleteAction(Customer $customer): Response + { + $this->customerService->deleteCustomer($customer); + + $view = new View(null, Response::HTTP_NO_CONTENT); + + return $this->viewHandler->handle($view); + } + /** * Sets the value of a meta-field for an existing customer */ diff --git a/src/API/ProjectController.php b/src/API/ProjectController.php index 6ff5a32b..11da0d9e 100644 --- a/src/API/ProjectController.php +++ b/src/API/ProjectController.php @@ -240,6 +240,25 @@ final class ProjectController extends BaseApiController return $this->viewHandler->handle($view); } + /** + * Delete an existing project + * + * [DANGER] This will also delete ALL linked activities and timesheets. + * Maybe use `PATCH` instead and mark it as inactive with `visible=false`? + */ + #[IsGranted('delete', 'project')] + #[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one project')])] + #[OA\Parameter(name: 'id', description: 'Project ID to delete', in: 'path', required: true)] + #[Route(path: '/{id}', name: 'delete_project', requirements: ['id' => '\d+'], methods: ['DELETE'])] + public function deleteAction(Project $project): Response + { + $this->projectService->deleteProject($project); + + $view = new View(null, Response::HTTP_NO_CONTENT); + + return $this->viewHandler->handle($view); + } + /** * Sets the value of a meta-field for an existing project */ diff --git a/src/Activity/ActivityService.php b/src/Activity/ActivityService.php index 37924a1f..8daf7e94 100644 --- a/src/Activity/ActivityService.php +++ b/src/Activity/ActivityService.php @@ -15,6 +15,7 @@ use App\Entity\Project; use App\Event\ActivityCreateEvent; use App\Event\ActivityCreatePostEvent; use App\Event\ActivityCreatePreEvent; +use App\Event\ActivityDeleteEvent; use App\Event\ActivityMetaDefinitionEvent; use App\Event\ActivityUpdatePostEvent; use App\Event\ActivityUpdatePreEvent; @@ -69,8 +70,13 @@ class ActivityService return $activity; } + public function deleteActivity(Activity $activity): void + { + $this->dispatcher->dispatch(new ActivityDeleteEvent($activity)); + $this->repository->deleteActivity($activity); + } + /** - * @param Activity $activity * @param string[] $groups * @throws ValidationFailedException */ diff --git a/src/Customer/CustomerService.php b/src/Customer/CustomerService.php index 1bbf1c03..f3521201 100644 --- a/src/Customer/CustomerService.php +++ b/src/Customer/CustomerService.php @@ -14,6 +14,7 @@ use App\Entity\Customer; use App\Event\CustomerCreateEvent; use App\Event\CustomerCreatePostEvent; use App\Event\CustomerCreatePreEvent; +use App\Event\CustomerDeleteEvent; use App\Event\CustomerMetaDefinitionEvent; use App\Event\CustomerUpdatePostEvent; use App\Event\CustomerUpdatePreEvent; @@ -73,6 +74,12 @@ final class CustomerService return $customer; } + public function deleteCustomer(Customer $customer): void + { + $this->dispatcher->dispatch(new CustomerDeleteEvent($customer)); + $this->repository->deleteCustomer($customer); + } + /** * @param string[] $groups * @throws ValidationFailedException diff --git a/src/Event/ActivityDeleteEvent.php b/src/Event/ActivityDeleteEvent.php new file mode 100644 index 00000000..199af191 --- /dev/null +++ b/src/Event/ActivityDeleteEvent.php @@ -0,0 +1,17 @@ +dispatcher->dispatch(new ProjectDeleteEvent($project)); + $this->repository->deleteProject($project); + } + /** - * @param Project $project * @param string[] $groups * @throws ValidationFailedException */ diff --git a/tests/API/ActivityControllerTest.php b/tests/API/ActivityControllerTest.php index dd0af1fc..d8e8b330 100644 --- a/tests/API/ActivityControllerTest.php +++ b/tests/API/ActivityControllerTest.php @@ -22,6 +22,8 @@ use App\Repository\ActivityRepository; use App\Repository\Query\VisibilityInterface; use App\Tests\Mocks\ActivityTestMetaFieldSubscriberMock; use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; /** * @group integration @@ -449,4 +451,62 @@ class ActivityControllerTest extends APIControllerBaseTest $activity = $em->getRepository(Activity::class)->find(1); $this->assertEquals('another,testing,bar', $activity->getMetaField('metatestmock')->getValue()); } + + // ------------------------------- [DELETE] ------------------------------- + + public function testDeleteIsSecure(): void + { + $this->assertUrlIsSecured('/api/activities/1', Request::METHOD_DELETE); + } + + public function testDeleteActionWithUnknownTimesheet(): void + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); + $this->assertNotFoundForDelete($client, '/api/activities/' . PHP_INT_MAX); + } + + public function testDeleteEntityIsSecure(): void + { + $this->assertUrlIsSecuredForRole(User::ROLE_USER, '/api/activities/1', Request::METHOD_DELETE); + } + + public function testDeleteActionWithoutAuthorization(): void + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); + $imports = $this->loadActivityTestData(); + + $this->request($client, '/api/activities/' . $imports[2]->getId(), Request::METHOD_DELETE); + + $response = $client->getResponse(); + $this->assertApiResponseAccessDenied($response); + } + + public function testDeleteAction(): void + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); + $imports = $this->loadActivityTestData(); + $getUrl = '/api/activities/' . $imports[2]->getId(); + $this->assertAccessIsGranted($client, $getUrl); + + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); + self::assertApiResponseTypeStructure('ActivityEntity', $result); + self::assertNotEmpty($result['id']); + self::assertIsNumeric($result['id']); + $id = $result['id']; + + $this->request($client, '/api/activities/' . $id, Request::METHOD_DELETE); + $this->assertTrue($client->getResponse()->isSuccessful()); + self::assertEquals(Response::HTTP_NO_CONTENT, $client->getResponse()->getStatusCode()); + $this->assertEmpty($client->getResponse()->getContent()); + + $this->request($client, $getUrl); + $this->assertApiException($client->getResponse(), [ + 'code' => Response::HTTP_NOT_FOUND, + 'message' => 'Not Found' + ]); + } } diff --git a/tests/API/CustomerControllerTest.php b/tests/API/CustomerControllerTest.php index a0b38245..01a56978 100644 --- a/tests/API/CustomerControllerTest.php +++ b/tests/API/CustomerControllerTest.php @@ -22,6 +22,8 @@ use App\Repository\CustomerRateRepository; use App\Repository\CustomerRepository; use App\Tests\Mocks\CustomerTestMetaFieldSubscriberMock; use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; /** * @group integration @@ -88,6 +90,45 @@ class CustomerControllerTest extends APIControllerBaseTest return [$rate1, $rate2]; } + /** + * @return array{0: Customer, 1: Customer} + */ + private function loadCustomerData(): array + { + /** @var CustomerRateRepository $rateRepository */ + $rateRepository = $this->getEntityManager()->getRepository(CustomerRate::class); + /** @var CustomerRepository $repository */ + $repository = $this->getEntityManager()->getRepository(Customer::class); + + $customer1 = new Customer('foooo'); + $customer1->setCountry('DE'); + $customer1->setTimezone('Europe/Paris'); + $repository->saveCustomer($customer1); + + $customer2 = new Customer('baaaaar'); + $customer2->setCountry('RU'); + $customer2->setTimezone('Europe/Moscow'); + $repository->saveCustomer($customer2); + + $rate1 = new CustomerRate(); + $rate1->setCustomer($customer1); + $rate1->setRate(17.45); + $rate1->setIsFixed(false); + + $rateRepository->saveRate($rate1); + + $rate2 = new CustomerRate(); + $rate2->setCustomer($customer1); + $rate2->setRate(99); + $rate2->setInternalRate(9); + $rate2->setIsFixed(true); + $rate2->setUser($this->getUserByName(UserFixtures::USERNAME_USER)); + + $rateRepository->saveRate($rate2); + + return [$customer1, $customer2]; + } + public function testIsSecure(): void { $this->assertUrlIsSecured('/api/customers'); @@ -392,4 +433,62 @@ class CustomerControllerTest extends APIControllerBaseTest $customer = $em->getRepository(Customer::class)->find(1); $this->assertEquals('another,testing,bar', $customer->getMetaField('metatestmock')->getValue()); } + + // ------------------------------- [DELETE] ------------------------------- + + public function testDeleteIsSecure(): void + { + $this->assertUrlIsSecured('/api/customers/1', Request::METHOD_DELETE); + } + + public function testDeleteActionWithUnknownTimesheet(): void + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); + $this->assertNotFoundForDelete($client, '/api/customers/' . PHP_INT_MAX); + } + + public function testDeleteEntityIsSecure(): void + { + $this->assertUrlIsSecuredForRole(User::ROLE_USER, '/api/customers/1', Request::METHOD_DELETE); + } + + public function testDeleteActionWithoutAuthorization(): void + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); + $imports = $this->loadCustomerData(); + + $this->request($client, '/api/customers/' . $imports[1]->getId(), Request::METHOD_DELETE); + + $response = $client->getResponse(); + $this->assertApiResponseAccessDenied($response); + } + + public function testDeleteAction(): void + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); + $imports = $this->loadCustomerData(); + $getUrl = '/api/customers/' . $imports[0]->getId(); + $this->assertAccessIsGranted($client, $getUrl); + + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); + self::assertApiResponseTypeStructure('CustomerEntity', $result); + self::assertNotEmpty($result['id']); + self::assertIsNumeric($result['id']); + $id = $result['id']; + + $this->request($client, '/api/customers/' . $id, Request::METHOD_DELETE); + $this->assertTrue($client->getResponse()->isSuccessful()); + self::assertEquals(Response::HTTP_NO_CONTENT, $client->getResponse()->getStatusCode()); + $this->assertEmpty($client->getResponse()->getContent()); + + $this->request($client, $getUrl); + $this->assertApiException($client->getResponse(), [ + 'code' => Response::HTTP_NOT_FOUND, + 'message' => 'Not Found' + ]); + } } diff --git a/tests/API/ProjectControllerTest.php b/tests/API/ProjectControllerTest.php index 71a7f120..b51814dd 100644 --- a/tests/API/ProjectControllerTest.php +++ b/tests/API/ProjectControllerTest.php @@ -22,7 +22,8 @@ use App\Repository\ProjectRepository; use App\Repository\Query\VisibilityInterface; use App\Tests\Mocks\ProjectTestMetaFieldSubscriberMock; use Symfony\Component\EventDispatcher\EventDispatcher; -use Symfony\Component\HttpKernel\HttpKernelBrowser; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; /** * @group integration @@ -110,10 +111,14 @@ class ProjectControllerTest extends APIControllerBaseTest self::assertApiResponseTypeStructure('ProjectCollection', $result[0]); } - protected function loadProjectTestData(HttpKernelBrowser $client) + /** + * @return array{0: Project, 1: Project, 2: Project, 3: Project, 4: Project} + */ + protected function loadProjectTestData(): array { $em = $this->getEntityManager(); + /** @var Customer $customer */ $customer = $em->getRepository(Customer::class)->find(1); $customer2 = new Customer('first one'); @@ -127,49 +132,49 @@ class ProjectControllerTest extends APIControllerBaseTest $customer3->setTimezone('Europe/Vienna'); $em->persist($customer3); - $project = new Project(); - $project->setName('first'); - $project->setVisible(false); - $project->setCustomer($customer2); - $em->persist($project); + $project1 = new Project(); + $project1->setName('first'); + $project1->setVisible(false); + $project1->setCustomer($customer2); + $em->persist($project1); - $project = new Project(); - $project->setName('second'); - $project->setVisible(false); - $project->setCustomer($customer); - $em->persist($project); + $project2 = new Project(); + $project2->setName('second'); + $project2->setVisible(false); + $project2->setCustomer($customer); + $em->persist($project2); - $project = new Project(); - $project->setName('third'); - $project->setVisible(true); - $project->setCustomer($customer2); - $em->persist($project); + $project3 = new Project(); + $project3->setName('third'); + $project3->setVisible(true); + $project3->setCustomer($customer2); + $em->persist($project3); - $project = new Project(); - $project->setName('fourth'); - $project->setVisible(true); - $project->setCustomer($customer3); - $em->persist($project); + $project4 = new Project(); + $project4->setName('fourth'); + $project4->setVisible(true); + $project4->setCustomer($customer3); + $em->persist($project4); - $project = new Project(); - $project->setName('fifth'); - $project->setVisible(true); - $project->setCustomer($customer); + $project5 = new Project(); + $project5->setName('fifth'); + $project5->setVisible(true); + $project5->setCustomer($customer); // add meta fields $meta = new ProjectMeta(); $meta->setName('bar')->setValue('foo')->setIsVisible(false); - $project->setMetaField($meta); + $project5->setMetaField($meta); $meta = new ProjectMeta(); $meta->setName('foo')->setValue('bar')->setIsVisible(true); - $project->setMetaField($meta); - $em->persist($project); + $project5->setMetaField($meta); + $em->persist($project5); // and a team $team = new Team('Testing project team'); $team->addTeamlead($this->getUserByRole(User::ROLE_USER)); $team->addCustomer($customer); - $team->addProject($project); + $team->addProject($project5); $team->addUser($this->getUserByRole(User::ROLE_TEAMLEAD)); $em->persist($team); @@ -178,18 +183,24 @@ class ProjectControllerTest extends APIControllerBaseTest $em->flush(); - return [$customer, $customer2, $customer3]; + return [ + $project1, + $project2, + $project3, + $project4, + $project5, + ]; } /** * @dataProvider getCollectionTestData */ - public function testGetCollectionWithParams($url, $customer, $parameters, $expected): void + public function testGetCollectionWithParams(string $url, ?int $project, array $parameters, array $expected): void { $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); - $imports = $this->loadProjectTestData($client); + $imports = $this->loadProjectTestData(); - $customerId = $customer !== null ? $imports[$customer]->getId() : null; + $customerId = $project !== null ? $imports[$project]->getCustomer()?->getId() : null; if ($customerId !== null) { if (\array_key_exists('customer', $parameters)) { @@ -236,19 +247,19 @@ class ProjectControllerTest extends APIControllerBaseTest { // if you wonder why: case-sensitive ordering feels strange ... "Title" > "fifth” yield ['/api/projects', null, [], [[true, 1], [false, 1], [false, 3]]]; - yield ['/api/projects', 0, ['customer' => '1'], [[true, 1], [false, 1]]]; - yield ['/api/projects', 0, ['customer' => '1', 'visible' => VisibilityInterface::SHOW_VISIBLE], [[true, 1], [false, 1]]]; - yield ['/api/projects', 0, ['customer' => '1', 'visible' => VisibilityInterface::SHOW_BOTH], [[true, 1], [false, 1], [false, 1]]]; - yield ['/api/projects', 0, ['customer' => '1', 'visible' => VisibilityInterface::SHOW_HIDDEN], [[false, 1]]]; + yield ['/api/projects', 1, ['customer' => '1'], [[true, 1], [false, 1]]]; + yield ['/api/projects', 1, ['customer' => '1', 'visible' => VisibilityInterface::SHOW_VISIBLE], [[true, 1], [false, 1]]]; + yield ['/api/projects', 1, ['customer' => '1', 'visible' => VisibilityInterface::SHOW_BOTH], [[true, 1], [false, 1], [false, 1]]]; + yield ['/api/projects', 1, ['customer' => '1', 'visible' => VisibilityInterface::SHOW_HIDDEN], [[false, 1]]]; // customer is invisible => query only returns results for VisibilityInterface::SHOW_BOTH - yield ['/api/projects', 1, ['customer' => '2', 'visible' => VisibilityInterface::SHOW_VISIBLE], []]; - yield ['/api/projects', 1, ['customer' => '2', 'visible' => VisibilityInterface::SHOW_BOTH], [[false, 2], [false, 2]]]; - yield ['/api/projects', 1, ['customer' => '2', 'customers' => ['2'], 'visible' => VisibilityInterface::SHOW_BOTH], [[false, 2], [false, 2]]]; - yield ['/api/projects', 1, ['customers' => ['2', '2'], 'visible' => VisibilityInterface::SHOW_BOTH], [[false, 2], [false, 2]]]; - yield ['/api/projects', 1, ['customer' => '2', 'visible' => VisibilityInterface::SHOW_HIDDEN], []]; - yield ['/api/projects', 1, ['customer' => '2', 'visible' => VisibilityInterface::SHOW_HIDDEN, 'start' => '2010-12-11', 'end' => '2030-12-11'], []]; - yield ['/api/projects', 1, ['customers' => ['2'], 'visible' => VisibilityInterface::SHOW_HIDDEN, 'start' => '2010-12-11', 'end' => '2030-12-11'], []]; - yield ['/api/projects', 1, ['customers' => ['2', '2'], 'visible' => VisibilityInterface::SHOW_HIDDEN, 'start' => '2010-12-11', 'end' => '2030-12-11'], []]; + yield ['/api/projects', 0, ['customer' => '2', 'visible' => VisibilityInterface::SHOW_VISIBLE], []]; + yield ['/api/projects', 0, ['customer' => '2', 'visible' => VisibilityInterface::SHOW_BOTH], [[false, 2], [false, 2]]]; + yield ['/api/projects', 0, ['customer' => '2', 'customers' => ['2'], 'visible' => VisibilityInterface::SHOW_BOTH], [[false, 2], [false, 2]]]; + yield ['/api/projects', 0, ['customers' => ['2', '2'], 'visible' => VisibilityInterface::SHOW_BOTH], [[false, 2], [false, 2]]]; + yield ['/api/projects', 0, ['customer' => '2', 'visible' => VisibilityInterface::SHOW_HIDDEN], []]; + yield ['/api/projects', 0, ['customer' => '2', 'visible' => VisibilityInterface::SHOW_HIDDEN, 'start' => '2010-12-11', 'end' => '2030-12-11'], []]; + yield ['/api/projects', 0, ['customers' => ['2'], 'visible' => VisibilityInterface::SHOW_HIDDEN, 'start' => '2010-12-11', 'end' => '2030-12-11'], []]; + yield ['/api/projects', 0, ['customers' => ['2', '2'], 'visible' => VisibilityInterface::SHOW_HIDDEN, 'start' => '2010-12-11', 'end' => '2030-12-11'], []]; } public function testGetEntityIsSecure(): void @@ -599,4 +610,62 @@ class ProjectControllerTest extends APIControllerBaseTest $project = $em->getRepository(Project::class)->find(1); $this->assertEquals('another,testing,bar', $project->getMetaField('metatestmock')->getValue()); } + + // ------------------------------- [DELETE] ------------------------------- + + public function testDeleteIsSecure(): void + { + $this->assertUrlIsSecured('/api/projects/1', Request::METHOD_DELETE); + } + + public function testDeleteActionWithUnknownTimesheet(): void + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); + $this->assertNotFoundForDelete($client, '/api/projects/' . PHP_INT_MAX); + } + + public function testDeleteEntityIsSecure(): void + { + $this->assertUrlIsSecuredForRole(User::ROLE_USER, '/api/projects/1', Request::METHOD_DELETE); + } + + public function testDeleteActionWithoutAuthorization(): void + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); + $imports = $this->loadProjectTestData(); + + $this->request($client, '/api/projects/' . $imports[2]->getId(), Request::METHOD_DELETE); + + $response = $client->getResponse(); + $this->assertApiResponseAccessDenied($response); + } + + public function testDeleteAction(): void + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN); + $imports = $this->loadProjectTestData(); + $getUrl = '/api/projects/' . $imports[2]->getId(); + $this->assertAccessIsGranted($client, $getUrl); + + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); + self::assertApiResponseTypeStructure('ProjectEntity', $result); + self::assertNotEmpty($result['id']); + self::assertIsNumeric($result['id']); + $id = $result['id']; + + $this->request($client, '/api/projects/' . $id, Request::METHOD_DELETE); + $this->assertTrue($client->getResponse()->isSuccessful()); + self::assertEquals(Response::HTTP_NO_CONTENT, $client->getResponse()->getStatusCode()); + $this->assertEmpty($client->getResponse()->getContent()); + + $this->request($client, $getUrl); + $this->assertApiException($client->getResponse(), [ + 'code' => Response::HTTP_NOT_FOUND, + 'message' => 'Not Found' + ]); + } } diff --git a/tests/phpstan.neon b/tests/phpstan.neon index 787af099..d8084c5d 100644 --- a/tests/phpstan.neon +++ b/tests/phpstan.neon @@ -335,36 +335,6 @@ parameters: count: 1 path: API/ProjectControllerTest.php - - - message: "#^Method App\\\\Tests\\\\API\\\\ProjectControllerTest\\:\\:loadProjectTestData\\(\\) has no return type specified\\.$#" - count: 1 - path: API/ProjectControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\ProjectControllerTest\\:\\:testGetCollectionWithParams\\(\\) has parameter \\$customer with no type specified\\.$#" - count: 1 - path: API/ProjectControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\ProjectControllerTest\\:\\:testGetCollectionWithParams\\(\\) has parameter \\$expected with no type specified\\.$#" - count: 1 - path: API/ProjectControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\ProjectControllerTest\\:\\:testGetCollectionWithParams\\(\\) has parameter \\$parameters with no type specified\\.$#" - count: 1 - path: API/ProjectControllerTest.php - - - - message: "#^Method App\\\\Tests\\\\API\\\\ProjectControllerTest\\:\\:testGetCollectionWithParams\\(\\) has parameter \\$url with no type specified\\.$#" - count: 1 - path: API/ProjectControllerTest.php - - - - message: "#^Parameter \\#1 \\$customer of method App\\\\Entity\\\\Team\\:\\:addCustomer\\(\\) expects App\\\\Entity\\\\Customer, App\\\\Entity\\\\Customer\\|null given\\.$#" - count: 1 - path: API/ProjectControllerTest.php - - message: "#^Parameter \\#5 \\$content of method App\\\\Tests\\\\API\\\\APIControllerBaseTest\\:\\:request\\(\\) expects string\\|null, string\\|false given\\.$#" count: 15