loginClient($client, 'client-liot'); $client->request('GET', '/api/tasks'); self::assertResponseStatusCodeSame(403); } public function testClientUserCannotListProjects(): void { $client = self::createClient(); $this->loginClient($client, 'client-liot'); $client->request('GET', '/api/projects'); self::assertResponseStatusCodeSame(403); } public function testClientUserCanListOwnClientTickets(): void { $client = self::createClient(); $this->loginClient($client, 'client-liot'); $client->request('GET', '/api/client_tickets'); self::assertResponseIsSuccessful(); $data = json_decode($client->getResponse()->getContent(), true); self::assertArrayHasKey('member', $data); self::assertNotEmpty($data['member']); // Tenancy invariant (robust to POST tests accumulating tickets in the // shared test DB): client-liot sees its own SIRH ticket but NEVER the // ticket submitted by another client (ACME, on the CRM project). $titles = array_column($data['member'], 'title'); self::assertContains('Erreur lors de l\'export des congés', $titles); self::assertNotContains('Ajouter un filtre par commercial', $titles); } public function testClientUserSeesOnlyOwnTickets(): void { $client = self::createClient(); $this->loginClient($client, 'client-acme'); $client->request('GET', '/api/client_tickets'); self::assertResponseIsSuccessful(); $data = json_decode($client->getResponse()->getContent(), true); self::assertNotEmpty($data['member']); // Tenancy invariant: client-acme sees its own CRM ticket but NEVER the // ticket submitted by client-liot (on the SIRH project). $titles = array_column($data['member'], 'title'); self::assertContains('Ajouter un filtre par commercial', $titles); self::assertNotContains('Erreur lors de l\'export des congés', $titles); } public function testInternalUserCannotListClientTickets(): void { $client = self::createClient(); $this->loginClient($client, 'alice'); $client->request('GET', '/api/client_tickets'); self::assertResponseStatusCodeSame(403); } public function testAdminCanListAllClientTickets(): void { $client = self::createClient(); $this->loginClient($client, 'admin'); $client->request('GET', '/api/client_tickets'); self::assertResponseIsSuccessful(); $data = json_decode($client->getResponse()->getContent(), true); self::assertGreaterThanOrEqual(2, count($data['member'])); } public function testClientCanCreateTicketAndNumberIsGenerated(): void { $client = self::createClient(); $em = self::getContainer()->get(EntityManagerInterface::class); $projectIri = $this->sirhProjectIri($em); $this->loginClient($client, 'client-liot'); $client->request('POST', '/api/client_tickets', server: [ 'CONTENT_TYPE' => 'application/ld+json', ], content: json_encode([ 'type' => 'other', 'title' => 'Demande de fonctionnalité', 'description' => 'Une nouvelle option serait utile.', 'project' => $projectIri, ])); self::assertResponseStatusCodeSame(201); $data = json_decode($client->getResponse()->getContent(), true); self::assertSame('new', $data['status']); self::assertGreaterThanOrEqual(1, $data['number']); } public function testAdminCannotCreateTicket(): void { $client = self::createClient(); $em = self::getContainer()->get(EntityManagerInterface::class); $projectIri = $this->sirhProjectIri($em); $this->loginClient($client, 'admin'); $client->request('POST', '/api/client_tickets', server: [ 'CONTENT_TYPE' => 'application/ld+json', ], content: json_encode([ 'type' => 'other', 'title' => 'Admin attempt', 'description' => 'Should be forbidden.', 'project' => $projectIri, ])); // Admin has no client, so ticket creation is denied. self::assertResponseStatusCodeSame(403); } public function testRejectingTicketRequiresStatusComment(): void { $client = self::createClient(); $em = self::getContainer()->get(EntityManagerInterface::class); $ticket = $em->getRepository(ClientTicket::class) ->findOneBy(['status' => ClientTicketStatus::New]) ; self::assertNotNull($ticket); $id = $ticket->getId(); $this->loginClient($client, 'admin'); $client->request('PATCH', '/api/client_tickets/'.$id, server: [ 'CONTENT_TYPE' => 'application/merge-patch+json', ], content: json_encode(['status' => 'rejected'])); self::assertResponseStatusCodeSame(422); } private function sirhProjectIri(EntityManagerInterface $em): string { $project = $em->getRepository(Project::class) ->findOneBy(['code' => 'SIRH']) ; self::assertNotNull($project); return '/api/projects/'.$project->getId(); } private function loginClient(KernelBrowser $client, string $username): void { $em = self::getContainer()->get(EntityManagerInterface::class); $user = $em->getRepository(User::class)->findOneBy(['username' => $username]); self::assertInstanceOf(User::class, $user); $client->loginUser($user); } }