From 7fef504f435a576d3c4ec154df48d450081a6e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 2 Jul 2025 18:41:09 +0200 Subject: [PATCH] Add ticket listing API endpoint with pagination and filters - Introduced `/api/1.0/ticket/ticket/list` endpoint for ticket retrieval. - Added support for filtering by person and applied access control checks. - Defined `TicketSimple` and updated `Collection` schemas in API specifications. - Implemented `TicketListApiController` to handle ticket listing logic. - Added unit tests for `TicketListApiController`. --- .../ChillMainBundle/chill.api.specs.yaml | 25 ++ .../ChillTicketBundle/chill.api.specs.yaml | 39 ++++ .../src/Controller/TicketControllerApi.php | 2 +- .../Controller/TicketListApiController.php | 68 ++++++ .../TicketListApiControllerTest.php | 221 ++++++++++++++++++ 5 files changed, 354 insertions(+), 1 deletion(-) create mode 100644 src/Bundle/ChillTicketBundle/src/Controller/TicketListApiController.php create mode 100644 src/Bundle/ChillTicketBundle/tests/Controller/TicketListApiControllerTest.php diff --git a/src/Bundle/ChillMainBundle/chill.api.specs.yaml b/src/Bundle/ChillMainBundle/chill.api.specs.yaml index 8d204803f..326918c61 100644 --- a/src/Bundle/ChillMainBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillMainBundle/chill.api.specs.yaml @@ -10,6 +10,31 @@ servers: components: schemas: + Collection: + type: object + properties: + count: + type: number + format: u64 + pagination: + type: object + properties: + first: + type: number + format: u64 + items_per_page: + type: number + format: u64 + next: + type: string + format: uri + nullable: true + previous: + type: string + format: uri + nullable: true + more: + type: boolean EntityWorkflowAttachment: type: object properties: diff --git a/src/Bundle/ChillTicketBundle/chill.api.specs.yaml b/src/Bundle/ChillTicketBundle/chill.api.specs.yaml index 3e40cab66..c9f1aacea 100644 --- a/src/Bundle/ChillTicketBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillTicketBundle/chill.api.specs.yaml @@ -1,5 +1,10 @@ components: schemas: + TicketSimple: + type: object + properties: + id: + type: integer Motive: type: object properties: @@ -44,6 +49,40 @@ paths: responses: 200: description: "OK" + /1.0/ticket/ticket/list: + get: + tags: + - ticket + summary: List of tickets + parameters: + - name: byPerson + in: query + description: the id of the person + required: false + style: form + explode: false + schema: + type: array + items: + type: integer + format: integer + minimum: 1 + responses: + 200: + description: OK + content: + application/json: + schema: + allOf: + - $ref: '#components/schemas/Collection' + - type: object + properties: + results: + type: array + items: + $ref: '#component/schema/TicketSimple' + + /1.0/ticket/motive.json: get: tags: diff --git a/src/Bundle/ChillTicketBundle/src/Controller/TicketControllerApi.php b/src/Bundle/ChillTicketBundle/src/Controller/TicketControllerApi.php index 36cde311a..aa9b16a59 100644 --- a/src/Bundle/ChillTicketBundle/src/Controller/TicketControllerApi.php +++ b/src/Bundle/ChillTicketBundle/src/Controller/TicketControllerApi.php @@ -20,7 +20,7 @@ class TicketControllerApi { public function __construct(private readonly SerializerInterface $serializer) {} - #[Route('/api/1.0/ticket/ticket/{id}', methods: ['GET'])] + #[Route('/api/1.0/ticket/ticket/{id}', requirements: ['id' => '\d+'], methods: ['GET'])] public function get(Ticket $ticket): JsonResponse { return new JsonResponse( diff --git a/src/Bundle/ChillTicketBundle/src/Controller/TicketListApiController.php b/src/Bundle/ChillTicketBundle/src/Controller/TicketListApiController.php new file mode 100644 index 000000000..bff459e41 --- /dev/null +++ b/src/Bundle/ChillTicketBundle/src/Controller/TicketListApiController.php @@ -0,0 +1,68 @@ +security->isGranted('ROLE_USER')) { + throw new AccessDeniedHttpException('Only users are allowed to list tickets.'); + } + + $params = []; + + if ($request->query->has('byPerson')) { + $params = explode(',', $request->query->get('byPerson')); + foreach ($params as $id) { + $params['byPerson'][] = $person = $this->personRepository->find($id); + + if (!$this->security->isGranted(PersonVoter::SEE, $person)) { + throw new AccessDeniedHttpException(sprintf('Not allowed to see a person with id %d', $id)); + } + } + } + + $nb = $this->ticketRepository->countTickets($params); + $paginator = $this->paginatorFactory->create($nb); + + $tickets = $this->ticketRepository->findTickets($params, $paginator->getCurrentPageFirstItemNumber(), $paginator->getItemsPerPage()); + + $collection = new Collection($tickets, $paginator); + + return new JsonResponse( + $this->serializer->serialize($collection, 'json', ['groups' => 'read']), + json: true + ); + } +} diff --git a/src/Bundle/ChillTicketBundle/tests/Controller/TicketListApiControllerTest.php b/src/Bundle/ChillTicketBundle/tests/Controller/TicketListApiControllerTest.php new file mode 100644 index 000000000..8cefd3dfb --- /dev/null +++ b/src/Bundle/ChillTicketBundle/tests/Controller/TicketListApiControllerTest.php @@ -0,0 +1,221 @@ +prophesize(Security::class); + $security->isGranted('ROLE_USER')->willReturn(true); + + $ticketRepository = $this->prophesize(TicketACLAwareRepositoryInterface::class); + $tickets = [new Ticket(), new Ticket()]; + $ticketRepository->countTickets([])->willReturn(2); + $ticketRepository->findTickets([], 0, 10)->willReturn($tickets); + + $paginator = $this->prophesize(PaginatorInterface::class); + $paginator->getCurrentPageFirstItemNumber()->willReturn(0); + $paginator->getItemsPerPage()->willReturn(10); + + $paginatorFactory = $this->prophesize(PaginatorFactoryInterface::class); + $paginatorFactory->create(2)->willReturn($paginator->reveal()); + + $serializer = $this->prophesize(SerializerInterface::class); + $serializer->serialize( + Argument::that(function (Collection $collection) use ($tickets) { + return $collection->getItems() === $tickets; + }), + 'json', + ['groups' => 'read'] + )->willReturn('{"items":[{},{}],"pagination":{}}'); + + $personRepository = $this->prophesize(PersonRepository::class); + + // Create controller + $controller = new TicketListApiController( + $security->reveal(), + $ticketRepository->reveal(), + $paginatorFactory->reveal(), + $serializer->reveal(), + $personRepository->reveal() + ); + + // Create request + $request = new Request(); + + // Call controller method + $response = $controller->listTicket($request); + + // Assert response + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals('{"items":[{},{}],"pagination":{}}', $response->getContent()); + } + + public function testListTicketWithPersonFilter(): void + { + // Mock dependencies + $security = $this->prophesize(Security::class); + $security->isGranted('ROLE_USER')->willReturn(true); + + $person = new Person(); + $security->isGranted(PersonVoter::SEE, $person)->willReturn(true); + + $ticketRepository = $this->prophesize(TicketACLAwareRepositoryInterface::class); + $tickets = [new Ticket(), new Ticket()]; + $ticketRepository->countTickets(Argument::that(function ($params) use ($person) { + return isset($params['byPerson']) && in_array($person, $params['byPerson']); + }))->willReturn(2); + $ticketRepository->findTickets( + Argument::that(function ($params) use ($person) { + return isset($params['byPerson']) && in_array($person, $params['byPerson']); + }), + 0, + 10 + )->willReturn($tickets); + + $paginator = $this->prophesize(PaginatorInterface::class); + $paginator->getCurrentPageFirstItemNumber()->willReturn(0); + $paginator->getItemsPerPage()->willReturn(10); + + $paginatorFactory = $this->prophesize(PaginatorFactoryInterface::class); + $paginatorFactory->create(2)->willReturn($paginator->reveal()); + + $serializer = $this->prophesize(SerializerInterface::class); + $serializer->serialize( + Argument::that(function (Collection $collection) use ($tickets) { + return $collection->getItems() === $tickets; + }), + 'json', + ['groups' => 'read'] + )->willReturn('{"items":[{},{}],"pagination":{}}'); + + $personRepository = $this->prophesize(PersonRepository::class); + $personRepository->find(123)->willReturn($person); + + // Create controller + $controller = new TicketListApiController( + $security->reveal(), + $ticketRepository->reveal(), + $paginatorFactory->reveal(), + $serializer->reveal(), + $personRepository->reveal() + ); + + // Create request with person filter + $request = new Request( + query: ['byPerson' => '123'] + ); + + // Call controller method + $response = $controller->listTicket($request); + + // Assert response + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals('{"items":[{},{}],"pagination":{}}', $response->getContent()); + } + + public function testListTicketWithoutUserRole(): void + { + // Mock dependencies + $security = $this->prophesize(Security::class); + $security->isGranted('ROLE_USER')->willReturn(false); + + $ticketRepository = $this->prophesize(TicketACLAwareRepositoryInterface::class); + $paginatorFactory = $this->prophesize(PaginatorFactoryInterface::class); + $serializer = $this->prophesize(SerializerInterface::class); + $personRepository = $this->prophesize(PersonRepository::class); + + // Create controller + $controller = new TicketListApiController( + $security->reveal(), + $ticketRepository->reveal(), + $paginatorFactory->reveal(), + $serializer->reveal(), + $personRepository->reveal() + ); + + // Create request + $request = new Request(); + + // Expect exception + $this->expectException(AccessDeniedHttpException::class); + $this->expectExceptionMessage('Only users are allowed to list tickets.'); + + // Call controller method + $controller->listTicket($request); + } + + public function testListTicketWithPersonWithoutAccess(): void + { + // Mock dependencies + $security = $this->prophesize(Security::class); + $security->isGranted('ROLE_USER')->willReturn(true); + + $person = new Person(); + $security->isGranted(PersonVoter::SEE, $person)->willReturn(false); + + $ticketRepository = $this->prophesize(TicketACLAwareRepositoryInterface::class); + $paginatorFactory = $this->prophesize(PaginatorFactoryInterface::class); + $serializer = $this->prophesize(SerializerInterface::class); + + $personRepository = $this->prophesize(PersonRepository::class); + $personRepository->find(123)->willReturn($person); + + // Create controller + $controller = new TicketListApiController( + $security->reveal(), + $ticketRepository->reveal(), + $paginatorFactory->reveal(), + $serializer->reveal(), + $personRepository->reveal() + ); + + // Create request with person filter + $request = new Request( + query: ['byPerson' => '123'] + ); + + // Expect exception + $this->expectException(AccessDeniedHttpException::class); + $this->expectExceptionMessage('Not allowed to see a person with id 123'); + + // Call controller method + $controller->listTicket($request); + } +}