Files
Lesstime/tests/Functional/Module/Core/UserArchiveApiTest.php
T
Matthieu 133f205393
Pull Request — Quality gate / Frontend (build) (pull_request) Successful in 39s
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 1m39s
test(user) : couvre le soft-delete + désarchivage admin et corrige les retours de review
- ajoute des tests fonctionnels (archive au DELETE, exclusion de la
  collection, listing/désarchivage admin, anti-auto-archivage) et un test
  unitaire du ArchivedUserChecker
- expose un filtre BooleanFilter `archived` + bypass admin dans
  ExcludeArchivedUserExtension pour lister les archivés (?archived=true)
- rend `archived` modifiable par un admin (groupe user:write + ApiProperty
  ROLE_ADMIN) → désarchivage possible via PATCH /api/users/{id}
- RestoreMissingUsersCommand : ne compte que les insertions réelles
  (ON CONFLICT DO NOTHING n'est plus comptabilisé à tort)
- relève memory_limit des tests à 512M (boot sérialiseur API Platform)
2026-06-26 16:14:11 +02:00

146 lines
5.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Functional\Module\Core;
use App\Module\Core\Domain\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/**
* Covers the soft-delete behaviour: deleting a user archives it (the row is
* kept so referencing tasks/time entries still serialize), archived users are
* hidden from the default collection but an admin can list and restore them.
*
* @internal
*/
final class UserArchiveApiTest extends WebTestCase
{
public function testDeleteArchivesUserInsteadOfRemovingIt(): void
{
$client = self::createClient();
$em = self::getContainer()->get(EntityManagerInterface::class);
$target = $this->createUser($em, 'archive-target-'.uniqid());
$em->flush();
$targetId = $target->getId();
$this->loginAdmin($client);
$client->request('DELETE', '/api/users/'.$targetId);
self::assertResponseStatusCodeSame(204);
$em->clear();
$reloaded = $em->getRepository(User::class)->find($targetId);
self::assertInstanceOf(User::class, $reloaded, 'Row must still exist (soft delete)');
self::assertTrue($reloaded->isArchived(), 'User must be flagged archived');
self::assertNull($reloaded->getApiToken(), 'API token must be cleared on archive');
}
public function testAdminCannotArchiveOwnAccount(): void
{
$client = self::createClient();
$em = self::getContainer()->get(EntityManagerInterface::class);
$this->loginAdmin($client);
$adminId = $this->userId('admin');
$client->request('DELETE', '/api/users/'.$adminId);
self::assertResponseStatusCodeSame(403);
$em->clear();
$admin = $em->getRepository(User::class)->find($adminId);
self::assertFalse($admin->isArchived(), 'Admin must not have archived itself');
}
public function testArchivedUserIsHiddenFromDefaultCollection(): void
{
$client = self::createClient();
$username = $this->createArchivedUser();
$this->loginAdmin($client);
$client->request('GET', '/api/users', server: ['HTTP_ACCEPT' => 'application/json']);
self::assertResponseIsSuccessful();
$usernames = array_column(json_decode($client->getResponse()->getContent(), true), 'username');
self::assertNotContains($username, $usernames, 'Archived user must not appear in the default list');
}
public function testAdminCanListArchivedUsersViaFilter(): void
{
$client = self::createClient();
$username = $this->createArchivedUser();
$this->loginAdmin($client);
$client->request('GET', '/api/users?archived=true', server: ['HTTP_ACCEPT' => 'application/json']);
self::assertResponseIsSuccessful();
$usernames = array_column(json_decode($client->getResponse()->getContent(), true), 'username');
self::assertContains($username, $usernames, 'Admin must be able to list archived users via ?archived=true');
}
public function testAdminCanRestoreUserViaPatch(): void
{
$client = self::createClient();
$em = self::getContainer()->get(EntityManagerInterface::class);
$user = $this->createUser($em, 'restore-target-'.uniqid());
$user->setArchived(true);
$em->flush();
$userId = $user->getId();
$em->clear();
$this->loginAdmin($client);
$client->request('PATCH', '/api/users/'.$userId, server: [
'CONTENT_TYPE' => 'application/merge-patch+json',
], content: json_encode(['archived' => false]));
self::assertResponseIsSuccessful();
$em->clear();
$reloaded = $em->getRepository(User::class)->find($userId);
self::assertFalse($reloaded->isArchived(), 'Admin PATCH must be able to un-archive a user');
}
private function createArchivedUser(): string
{
$em = self::getContainer()->get(EntityManagerInterface::class);
$username = 'archived-'.uniqid();
$user = $this->createUser($em, $username);
$user->setArchived(true);
$em->flush();
$em->clear();
return $username;
}
private function createUser(EntityManagerInterface $em, string $username): User
{
$user = new User();
$user->setUsername($username);
$user->setPassword('x');
$user->setRoles(['ROLE_USER']);
$em->persist($user);
return $user;
}
private function loginAdmin(KernelBrowser $client): void
{
$em = self::getContainer()->get(EntityManagerInterface::class);
$user = $em->getRepository(User::class)->findOneBy(['username' => 'admin']);
self::assertInstanceOf(User::class, $user);
$client->loginUser($user);
}
private function userId(string $username): int
{
$em = self::getContainer()->get(EntityManagerInterface::class);
$user = $em->getRepository(User::class)->findOneBy(['username' => $username]);
self::assertInstanceOf(User::class, $user);
return $user->getId();
}
}