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
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\API;
|
namespace App\API;
|
||||||
|
|
||||||
|
use App\Activity\ActivityService;
|
||||||
use App\Entity\Activity;
|
use App\Entity\Activity;
|
||||||
use App\Entity\ActivityRate;
|
use App\Entity\ActivityRate;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
@@ -46,7 +47,8 @@ final class ActivityController extends BaseApiController
|
|||||||
private readonly ViewHandlerInterface $viewHandler,
|
private readonly ViewHandlerInterface $viewHandler,
|
||||||
private readonly ActivityRepository $repository,
|
private readonly ActivityRepository $repository,
|
||||||
private readonly EventDispatcherInterface $dispatcher,
|
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);
|
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
|
* Sets the value of a meta-field for an existing activity
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ final class CustomerController extends BaseApiController
|
|||||||
private readonly ViewHandlerInterface $viewHandler,
|
private readonly ViewHandlerInterface $viewHandler,
|
||||||
private readonly CustomerRepository $repository,
|
private readonly CustomerRepository $repository,
|
||||||
private readonly EventDispatcherInterface $dispatcher,
|
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);
|
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
|
* Sets the value of a meta-field for an existing customer
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -240,6 +240,25 @@ final class ProjectController extends BaseApiController
|
|||||||
return $this->viewHandler->handle($view);
|
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
|
* Sets the value of a meta-field for an existing project
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use App\Entity\Project;
|
|||||||
use App\Event\ActivityCreateEvent;
|
use App\Event\ActivityCreateEvent;
|
||||||
use App\Event\ActivityCreatePostEvent;
|
use App\Event\ActivityCreatePostEvent;
|
||||||
use App\Event\ActivityCreatePreEvent;
|
use App\Event\ActivityCreatePreEvent;
|
||||||
|
use App\Event\ActivityDeleteEvent;
|
||||||
use App\Event\ActivityMetaDefinitionEvent;
|
use App\Event\ActivityMetaDefinitionEvent;
|
||||||
use App\Event\ActivityUpdatePostEvent;
|
use App\Event\ActivityUpdatePostEvent;
|
||||||
use App\Event\ActivityUpdatePreEvent;
|
use App\Event\ActivityUpdatePreEvent;
|
||||||
@@ -69,8 +70,13 @@ class ActivityService
|
|||||||
return $activity;
|
return $activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function deleteActivity(Activity $activity): void
|
||||||
|
{
|
||||||
|
$this->dispatcher->dispatch(new ActivityDeleteEvent($activity));
|
||||||
|
$this->repository->deleteActivity($activity);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Activity $activity
|
|
||||||
* @param string[] $groups
|
* @param string[] $groups
|
||||||
* @throws ValidationFailedException
|
* @throws ValidationFailedException
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use App\Entity\Customer;
|
|||||||
use App\Event\CustomerCreateEvent;
|
use App\Event\CustomerCreateEvent;
|
||||||
use App\Event\CustomerCreatePostEvent;
|
use App\Event\CustomerCreatePostEvent;
|
||||||
use App\Event\CustomerCreatePreEvent;
|
use App\Event\CustomerCreatePreEvent;
|
||||||
|
use App\Event\CustomerDeleteEvent;
|
||||||
use App\Event\CustomerMetaDefinitionEvent;
|
use App\Event\CustomerMetaDefinitionEvent;
|
||||||
use App\Event\CustomerUpdatePostEvent;
|
use App\Event\CustomerUpdatePostEvent;
|
||||||
use App\Event\CustomerUpdatePreEvent;
|
use App\Event\CustomerUpdatePreEvent;
|
||||||
@@ -73,6 +74,12 @@ final class CustomerService
|
|||||||
return $customer;
|
return $customer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function deleteCustomer(Customer $customer): void
|
||||||
|
{
|
||||||
|
$this->dispatcher->dispatch(new CustomerDeleteEvent($customer));
|
||||||
|
$this->repository->deleteCustomer($customer);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string[] $groups
|
* @param string[] $groups
|
||||||
* @throws ValidationFailedException
|
* @throws ValidationFailedException
|
||||||
|
|||||||
17
src/Event/ActivityDeleteEvent.php
Normal file
17
src/Event/ActivityDeleteEvent.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggered right before a activity will be deleted.
|
||||||
|
*/
|
||||||
|
final class ActivityDeleteEvent extends AbstractActivityEvent
|
||||||
|
{
|
||||||
|
}
|
||||||
17
src/Event/CustomerDeleteEvent.php
Normal file
17
src/Event/CustomerDeleteEvent.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggered right before a customer will be deleted.
|
||||||
|
*/
|
||||||
|
final class CustomerDeleteEvent extends AbstractCustomerEvent
|
||||||
|
{
|
||||||
|
}
|
||||||
17
src/Event/ProjectDeleteEvent.php
Normal file
17
src/Event/ProjectDeleteEvent.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggered right before a project will be deleted.
|
||||||
|
*/
|
||||||
|
final class ProjectDeleteEvent extends AbstractProjectEvent
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ use App\Entity\Project;
|
|||||||
use App\Event\ProjectCreateEvent;
|
use App\Event\ProjectCreateEvent;
|
||||||
use App\Event\ProjectCreatePostEvent;
|
use App\Event\ProjectCreatePostEvent;
|
||||||
use App\Event\ProjectCreatePreEvent;
|
use App\Event\ProjectCreatePreEvent;
|
||||||
|
use App\Event\ProjectDeleteEvent;
|
||||||
use App\Event\ProjectMetaDefinitionEvent;
|
use App\Event\ProjectMetaDefinitionEvent;
|
||||||
use App\Event\ProjectUpdatePostEvent;
|
use App\Event\ProjectUpdatePostEvent;
|
||||||
use App\Event\ProjectUpdatePreEvent;
|
use App\Event\ProjectUpdatePreEvent;
|
||||||
@@ -77,8 +78,13 @@ final class ProjectService
|
|||||||
return $project;
|
return $project;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function deleteProject(Project $project): void
|
||||||
|
{
|
||||||
|
$this->dispatcher->dispatch(new ProjectDeleteEvent($project));
|
||||||
|
$this->repository->deleteProject($project);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Project $project
|
|
||||||
* @param string[] $groups
|
* @param string[] $groups
|
||||||
* @throws ValidationFailedException
|
* @throws ValidationFailedException
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ use App\Repository\ActivityRepository;
|
|||||||
use App\Repository\Query\VisibilityInterface;
|
use App\Repository\Query\VisibilityInterface;
|
||||||
use App\Tests\Mocks\ActivityTestMetaFieldSubscriberMock;
|
use App\Tests\Mocks\ActivityTestMetaFieldSubscriberMock;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @group integration
|
* @group integration
|
||||||
@@ -449,4 +451,62 @@ class ActivityControllerTest extends APIControllerBaseTest
|
|||||||
$activity = $em->getRepository(Activity::class)->find(1);
|
$activity = $em->getRepository(Activity::class)->find(1);
|
||||||
$this->assertEquals('another,testing,bar', $activity->getMetaField('metatestmock')->getValue());
|
$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'
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ use App\Repository\CustomerRateRepository;
|
|||||||
use App\Repository\CustomerRepository;
|
use App\Repository\CustomerRepository;
|
||||||
use App\Tests\Mocks\CustomerTestMetaFieldSubscriberMock;
|
use App\Tests\Mocks\CustomerTestMetaFieldSubscriberMock;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @group integration
|
* @group integration
|
||||||
@@ -88,6 +90,45 @@ class CustomerControllerTest extends APIControllerBaseTest
|
|||||||
return [$rate1, $rate2];
|
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
|
public function testIsSecure(): void
|
||||||
{
|
{
|
||||||
$this->assertUrlIsSecured('/api/customers');
|
$this->assertUrlIsSecured('/api/customers');
|
||||||
@@ -392,4 +433,62 @@ class CustomerControllerTest extends APIControllerBaseTest
|
|||||||
$customer = $em->getRepository(Customer::class)->find(1);
|
$customer = $em->getRepository(Customer::class)->find(1);
|
||||||
$this->assertEquals('another,testing,bar', $customer->getMetaField('metatestmock')->getValue());
|
$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'
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ use App\Repository\ProjectRepository;
|
|||||||
use App\Repository\Query\VisibilityInterface;
|
use App\Repository\Query\VisibilityInterface;
|
||||||
use App\Tests\Mocks\ProjectTestMetaFieldSubscriberMock;
|
use App\Tests\Mocks\ProjectTestMetaFieldSubscriberMock;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||||
use Symfony\Component\HttpKernel\HttpKernelBrowser;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @group integration
|
* @group integration
|
||||||
@@ -110,10 +111,14 @@ class ProjectControllerTest extends APIControllerBaseTest
|
|||||||
self::assertApiResponseTypeStructure('ProjectCollection', $result[0]);
|
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();
|
$em = $this->getEntityManager();
|
||||||
|
|
||||||
|
/** @var Customer $customer */
|
||||||
$customer = $em->getRepository(Customer::class)->find(1);
|
$customer = $em->getRepository(Customer::class)->find(1);
|
||||||
|
|
||||||
$customer2 = new Customer('first one');
|
$customer2 = new Customer('first one');
|
||||||
@@ -127,49 +132,49 @@ class ProjectControllerTest extends APIControllerBaseTest
|
|||||||
$customer3->setTimezone('Europe/Vienna');
|
$customer3->setTimezone('Europe/Vienna');
|
||||||
$em->persist($customer3);
|
$em->persist($customer3);
|
||||||
|
|
||||||
$project = new Project();
|
$project1 = new Project();
|
||||||
$project->setName('first');
|
$project1->setName('first');
|
||||||
$project->setVisible(false);
|
$project1->setVisible(false);
|
||||||
$project->setCustomer($customer2);
|
$project1->setCustomer($customer2);
|
||||||
$em->persist($project);
|
$em->persist($project1);
|
||||||
|
|
||||||
$project = new Project();
|
$project2 = new Project();
|
||||||
$project->setName('second');
|
$project2->setName('second');
|
||||||
$project->setVisible(false);
|
$project2->setVisible(false);
|
||||||
$project->setCustomer($customer);
|
$project2->setCustomer($customer);
|
||||||
$em->persist($project);
|
$em->persist($project2);
|
||||||
|
|
||||||
$project = new Project();
|
$project3 = new Project();
|
||||||
$project->setName('third');
|
$project3->setName('third');
|
||||||
$project->setVisible(true);
|
$project3->setVisible(true);
|
||||||
$project->setCustomer($customer2);
|
$project3->setCustomer($customer2);
|
||||||
$em->persist($project);
|
$em->persist($project3);
|
||||||
|
|
||||||
$project = new Project();
|
$project4 = new Project();
|
||||||
$project->setName('fourth');
|
$project4->setName('fourth');
|
||||||
$project->setVisible(true);
|
$project4->setVisible(true);
|
||||||
$project->setCustomer($customer3);
|
$project4->setCustomer($customer3);
|
||||||
$em->persist($project);
|
$em->persist($project4);
|
||||||
|
|
||||||
$project = new Project();
|
$project5 = new Project();
|
||||||
$project->setName('fifth');
|
$project5->setName('fifth');
|
||||||
$project->setVisible(true);
|
$project5->setVisible(true);
|
||||||
$project->setCustomer($customer);
|
$project5->setCustomer($customer);
|
||||||
|
|
||||||
// add meta fields
|
// add meta fields
|
||||||
$meta = new ProjectMeta();
|
$meta = new ProjectMeta();
|
||||||
$meta->setName('bar')->setValue('foo')->setIsVisible(false);
|
$meta->setName('bar')->setValue('foo')->setIsVisible(false);
|
||||||
$project->setMetaField($meta);
|
$project5->setMetaField($meta);
|
||||||
$meta = new ProjectMeta();
|
$meta = new ProjectMeta();
|
||||||
$meta->setName('foo')->setValue('bar')->setIsVisible(true);
|
$meta->setName('foo')->setValue('bar')->setIsVisible(true);
|
||||||
$project->setMetaField($meta);
|
$project5->setMetaField($meta);
|
||||||
$em->persist($project);
|
$em->persist($project5);
|
||||||
|
|
||||||
// and a team
|
// and a team
|
||||||
$team = new Team('Testing project team');
|
$team = new Team('Testing project team');
|
||||||
$team->addTeamlead($this->getUserByRole(User::ROLE_USER));
|
$team->addTeamlead($this->getUserByRole(User::ROLE_USER));
|
||||||
$team->addCustomer($customer);
|
$team->addCustomer($customer);
|
||||||
$team->addProject($project);
|
$team->addProject($project5);
|
||||||
$team->addUser($this->getUserByRole(User::ROLE_TEAMLEAD));
|
$team->addUser($this->getUserByRole(User::ROLE_TEAMLEAD));
|
||||||
$em->persist($team);
|
$em->persist($team);
|
||||||
|
|
||||||
@@ -178,18 +183,24 @@ class ProjectControllerTest extends APIControllerBaseTest
|
|||||||
|
|
||||||
$em->flush();
|
$em->flush();
|
||||||
|
|
||||||
return [$customer, $customer2, $customer3];
|
return [
|
||||||
|
$project1,
|
||||||
|
$project2,
|
||||||
|
$project3,
|
||||||
|
$project4,
|
||||||
|
$project5,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dataProvider getCollectionTestData
|
* @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);
|
$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 ($customerId !== null) {
|
||||||
if (\array_key_exists('customer', $parameters)) {
|
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”
|
// if you wonder why: case-sensitive ordering feels strange ... "Title" > "fifth”
|
||||||
yield ['/api/projects', null, [], [[true, 1], [false, 1], [false, 3]]];
|
yield ['/api/projects', null, [], [[true, 1], [false, 1], [false, 3]]];
|
||||||
yield ['/api/projects', 0, ['customer' => '1'], [[true, 1], [false, 1]]];
|
yield ['/api/projects', 1, ['customer' => '1'], [[true, 1], [false, 1]]];
|
||||||
yield ['/api/projects', 0, ['customer' => '1', 'visible' => VisibilityInterface::SHOW_VISIBLE], [[true, 1], [false, 1]]];
|
yield ['/api/projects', 1, ['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', 1, ['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', 'visible' => VisibilityInterface::SHOW_HIDDEN], [[false, 1]]];
|
||||||
// customer is invisible => query only returns results for VisibilityInterface::SHOW_BOTH
|
// customer is invisible => query only returns results for VisibilityInterface::SHOW_BOTH
|
||||||
yield ['/api/projects', 1, ['customer' => '2', 'visible' => VisibilityInterface::SHOW_VISIBLE], []];
|
yield ['/api/projects', 0, ['customer' => '2', 'visible' => VisibilityInterface::SHOW_VISIBLE], []];
|
||||||
yield ['/api/projects', 1, ['customer' => '2', 'visible' => VisibilityInterface::SHOW_BOTH], [[false, 2], [false, 2]]];
|
yield ['/api/projects', 0, ['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', 0, ['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', 0, ['customers' => ['2', '2'], 'visible' => VisibilityInterface::SHOW_BOTH], [[false, 2], [false, 2]]];
|
||||||
yield ['/api/projects', 1, ['customer' => '2', 'visible' => VisibilityInterface::SHOW_HIDDEN], []];
|
yield ['/api/projects', 0, ['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', 0, ['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', 0, ['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, ['customers' => ['2', '2'], 'visible' => VisibilityInterface::SHOW_HIDDEN, 'start' => '2010-12-11', 'end' => '2030-12-11'], []];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetEntityIsSecure(): void
|
public function testGetEntityIsSecure(): void
|
||||||
@@ -599,4 +610,62 @@ class ProjectControllerTest extends APIControllerBaseTest
|
|||||||
$project = $em->getRepository(Project::class)->find(1);
|
$project = $em->getRepository(Project::class)->find(1);
|
||||||
$this->assertEquals('another,testing,bar', $project->getMetaField('metatestmock')->getValue());
|
$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'
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,36 +335,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: API/ProjectControllerTest.php
|
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\\.$#"
|
message: "#^Parameter \\#5 \\$content of method App\\\\Tests\\\\API\\\\APIControllerBaseTest\\:\\:request\\(\\) expects string\\|null, string\\|false given\\.$#"
|
||||||
count: 15
|
count: 15
|
||||||
|
|||||||
Reference in New Issue
Block a user