feat(mail) : MailLinkTask + MailUnlinkTask + TaskMailsList controllers
- POST /api/mail/messages/{id}/link-task body {taskId} : cree TaskMailLink (idempotent)
- DELETE /api/mail/messages/{id}/link-task/{taskId} : supprime le lien (204)
- GET /api/tasks/{id}/mails : liste les mails lies a une tache
- securite via MailAccessChecker, tests fonctionnels 401/403
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
69
src/Controller/Mail/MailLinkTaskController.php
Normal file
69
src/Controller/Mail/MailLinkTaskController.php
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Mail;
|
||||||
|
|
||||||
|
use App\Entity\Task;
|
||||||
|
use App\Entity\TaskMailLink;
|
||||||
|
use App\Repository\MailMessageRepository;
|
||||||
|
use App\Repository\TaskMailLinkRepository;
|
||||||
|
use App\Security\MailAccessChecker;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[Route('/api/mail/messages/{id}/link-task', name: 'mail_link_task', methods: ['POST'], priority: 1, requirements: ['id' => '\d+'])]
|
||||||
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
|
class MailLinkTaskController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly MailMessageRepository $messageRepository,
|
||||||
|
private readonly TaskMailLinkRepository $linkRepository,
|
||||||
|
private readonly EntityManagerInterface $em,
|
||||||
|
private readonly MailAccessChecker $accessChecker,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __invoke(Request $request, int $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->accessChecker->ensureCanAccessMail($this->getUser());
|
||||||
|
|
||||||
|
$message = $this->messageRepository->find($id);
|
||||||
|
if (null === $message) {
|
||||||
|
throw new NotFoundHttpException('Message not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = json_decode($request->getContent(), true);
|
||||||
|
$taskId = $body['taskId'] ?? null;
|
||||||
|
|
||||||
|
if (null === $taskId) {
|
||||||
|
throw new UnprocessableEntityHttpException('taskId is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$task = $this->em->getRepository(Task::class)->find($taskId);
|
||||||
|
if (null === $task) {
|
||||||
|
throw new NotFoundHttpException('Task not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing = $this->linkRepository->findByTaskAndMessage($task, $message);
|
||||||
|
if (null !== $existing) {
|
||||||
|
return $this->json(['message' => 'Already linked']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$link = new TaskMailLink();
|
||||||
|
$link->setTask($task);
|
||||||
|
$link->setMailMessage($message);
|
||||||
|
$link->setLinkedAt(new DateTimeImmutable());
|
||||||
|
$link->setLinkedBy($this->getUser());
|
||||||
|
$this->em->persist($link);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
return $this->json(['linkId' => $link->getId(), 'taskId' => $task->getId(), 'messageId' => $message->getId()], 201);
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/Controller/Mail/MailUnlinkTaskController.php
Normal file
54
src/Controller/Mail/MailUnlinkTaskController.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Mail;
|
||||||
|
|
||||||
|
use App\Entity\Task;
|
||||||
|
use App\Repository\MailMessageRepository;
|
||||||
|
use App\Repository\TaskMailLinkRepository;
|
||||||
|
use App\Security\MailAccessChecker;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[Route('/api/mail/messages/{id}/link-task/{taskId}', name: 'mail_unlink_task', methods: ['DELETE'], priority: 1, requirements: ['id' => '\d+', 'taskId' => '\d+'])]
|
||||||
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
|
class MailUnlinkTaskController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly MailMessageRepository $messageRepository,
|
||||||
|
private readonly TaskMailLinkRepository $linkRepository,
|
||||||
|
private readonly EntityManagerInterface $em,
|
||||||
|
private readonly MailAccessChecker $accessChecker,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __invoke(int $id, int $taskId): JsonResponse
|
||||||
|
{
|
||||||
|
$this->accessChecker->ensureCanAccessMail($this->getUser());
|
||||||
|
|
||||||
|
$message = $this->messageRepository->find($id);
|
||||||
|
if (null === $message) {
|
||||||
|
throw new NotFoundHttpException('Message not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$task = $this->em->getRepository(Task::class)->find($taskId);
|
||||||
|
if (null === $task) {
|
||||||
|
throw new NotFoundHttpException('Task not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$link = $this->linkRepository->findByTaskAndMessage($task, $message);
|
||||||
|
if (null === $link) {
|
||||||
|
throw new NotFoundHttpException('Link not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->em->remove($link);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
return $this->json(null, Response::HTTP_NO_CONTENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/Controller/Mail/TaskMailsListController.php
Normal file
54
src/Controller/Mail/TaskMailsListController.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Mail;
|
||||||
|
|
||||||
|
use App\Entity\Task;
|
||||||
|
use App\Repository\TaskMailLinkRepository;
|
||||||
|
use App\Security\MailAccessChecker;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
|
||||||
|
#[Route('/api/tasks/{id}/mails', name: 'task_mails_list', methods: ['GET'], priority: 1, requirements: ['id' => '\d+'])]
|
||||||
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
|
class TaskMailsListController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $em,
|
||||||
|
private readonly TaskMailLinkRepository $linkRepository,
|
||||||
|
private readonly MailAccessChecker $accessChecker,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __invoke(int $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->accessChecker->ensureCanAccessMail($this->getUser());
|
||||||
|
|
||||||
|
$task = $this->em->getRepository(Task::class)->find($id);
|
||||||
|
if (null === $task) {
|
||||||
|
throw new NotFoundHttpException('Task not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$links = $this->linkRepository->findByTask($task);
|
||||||
|
|
||||||
|
$data = array_map(static fn ($link) => [
|
||||||
|
'id' => $link->getMailMessage()->getId(),
|
||||||
|
'messageId' => $link->getMailMessage()->getMessageId(),
|
||||||
|
'subject' => $link->getMailMessage()->getSubject(),
|
||||||
|
'fromAddress' => $link->getMailMessage()->getFromAddress(),
|
||||||
|
'fromName' => $link->getMailMessage()->getFromName(),
|
||||||
|
'sentAt' => $link->getMailMessage()->getSentAt()->format(DateTimeInterface::ATOM),
|
||||||
|
'isRead' => $link->getMailMessage()->isRead(),
|
||||||
|
'isFlagged' => $link->getMailMessage()->isFlagged(),
|
||||||
|
'snippet' => $link->getMailMessage()->getSnippet(),
|
||||||
|
'linkedAt' => $link->getLinkedAt()->format(DateTimeInterface::ATOM),
|
||||||
|
], $links);
|
||||||
|
|
||||||
|
return $this->json($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Functional\Controller\Mail;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class MailTaskIntegrationControllerTest extends WebTestCase
|
||||||
|
{
|
||||||
|
public function testLinkTaskReturns401WhenNotAuthenticated(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
$client->request('POST', '/api/mail/messages/1/link-task', [], [], ['CONTENT_TYPE' => 'application/json'], json_encode(['taskId' => 1]));
|
||||||
|
|
||||||
|
self::assertResponseStatusCodeSame(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLinkTaskReturns403ForRoleClient(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
$container = static::getContainer();
|
||||||
|
$em = $container->get('doctrine.orm.entity_manager');
|
||||||
|
|
||||||
|
$clientUser = $em->getRepository(User::class)->findOneBy(['username' => 'client-liot']);
|
||||||
|
$client->loginUser($clientUser);
|
||||||
|
$client->request('POST', '/api/mail/messages/1/link-task', [], [], ['CONTENT_TYPE' => 'application/json'], json_encode(['taskId' => 1]));
|
||||||
|
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnlinkTaskReturns401WhenNotAuthenticated(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
$client->request('DELETE', '/api/mail/messages/1/link-task/1');
|
||||||
|
|
||||||
|
self::assertResponseStatusCodeSame(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTaskMailsListReturns401WhenNotAuthenticated(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
$client->request('GET', '/api/tasks/1/mails');
|
||||||
|
|
||||||
|
self::assertResponseStatusCodeSame(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTaskMailsListReturns403ForRoleClient(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
$container = static::getContainer();
|
||||||
|
$em = $container->get('doctrine.orm.entity_manager');
|
||||||
|
|
||||||
|
$clientUser = $em->getRepository(User::class)->findOneBy(['username' => 'client-liot']);
|
||||||
|
$client->loginUser($clientUser);
|
||||||
|
$client->request('GET', '/api/tasks/1/mails');
|
||||||
|
|
||||||
|
self::assertResponseStatusCodeSame(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCreateTaskReturns401WhenNotAuthenticated(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
$client->request('POST', '/api/mail/messages/1/create-task', [], [], ['CONTENT_TYPE' => 'application/json'], json_encode(['projectId' => 1]));
|
||||||
|
|
||||||
|
self::assertResponseStatusCodeSame(401);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user