first commit

This commit is contained in:
julien
2026-03-16 01:47:07 +01:00
commit 8f7e61bda0
185 changed files with 27731 additions and 0 deletions

View File

@@ -0,0 +1,441 @@
<?php
declare(strict_types=1);
namespace Tests\User;
use App\Shared\Http\FlashServiceInterface;
use App\Shared\Http\SessionManagerInterface;
use App\User\Exception\DuplicateEmailException;
use App\User\Exception\DuplicateUsernameException;
use App\User\Exception\WeakPasswordException;
use App\User\User;
use App\User\UserController;
use App\User\UserServiceInterface;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\ControllerTestCase;
/**
* Tests unitaires pour UserController.
*
* Couvre les 5 actions publiques :
* - index() : rendu de la liste
* - showCreate() : rendu du formulaire
* - create() : mismatch, username dupliqué, email dupliqué, mot de passe faible, succès
* - updateRole() : introuvable, propre rôle, cible admin, rôle invalide, succès
* - delete() : introuvable, cible admin, soi-même, succès
*/
final class UserControllerTest extends ControllerTestCase
{
/** @var \Slim\Views\Twig&MockObject */
private \Slim\Views\Twig $view;
/** @var UserServiceInterface&MockObject */
private UserServiceInterface $userService;
/** @var FlashServiceInterface&MockObject */
private FlashServiceInterface $flash;
/** @var SessionManagerInterface&MockObject */
private SessionManagerInterface $sessionManager;
private UserController $controller;
protected function setUp(): void
{
$this->view = $this->makeTwigMock();
$this->userService = $this->createMock(UserServiceInterface::class);
$this->flash = $this->createMock(FlashServiceInterface::class);
$this->sessionManager = $this->createMock(SessionManagerInterface::class);
$this->controller = new UserController(
$this->view,
$this->userService,
$this->flash,
$this->sessionManager,
);
}
// ── index ────────────────────────────────────────────────────────
/**
* index() doit rendre la vue avec la liste des utilisateurs.
*/
public function testIndexRendersWithUserList(): void
{
$this->userService->method('findAll')->willReturn([]);
$this->sessionManager->method('getUserId')->willReturn(1);
$this->view->expects($this->once())
->method('render')
->with($this->anything(), 'admin/users/index.twig', $this->anything())
->willReturnArgument(0);
$res = $this->controller->index($this->makeGet('/admin/users'), $this->makeResponse());
$this->assertStatus($res, 200);
}
// ── showCreate ───────────────────────────────────────────────────
/**
* showCreate() doit rendre le formulaire de création.
*/
public function testShowCreateRendersForm(): void
{
$this->view->expects($this->once())
->method('render')
->with($this->anything(), 'admin/users/form.twig', $this->anything())
->willReturnArgument(0);
$res = $this->controller->showCreate($this->makeGet('/admin/users/create'), $this->makeResponse());
$this->assertStatus($res, 200);
}
// ── create ───────────────────────────────────────────────────────
/**
* create() doit rediriger avec une erreur si les mots de passe ne correspondent pas.
*/
public function testCreateRedirectsWhenPasswordMismatch(): void
{
$this->flash->expects($this->once())->method('set')
->with('user_error', 'Les mots de passe ne correspondent pas');
$req = $this->makePost('/admin/users/create', [
'username' => 'alice',
'email' => 'alice@example.com',
'password' => 'pass1',
'password_confirm' => 'pass2',
]);
$res = $this->controller->create($req, $this->makeResponse());
$this->assertRedirectTo($res, '/admin/users/create');
}
/**
* create() ne doit pas appeler userService si les mots de passe ne correspondent pas.
*/
public function testCreateDoesNotCallServiceOnMismatch(): void
{
$this->userService->expects($this->never())->method('createUser');
$this->flash->method('set');
$req = $this->makePost('/admin/users/create', [
'username' => 'alice',
'email' => 'alice@example.com',
'password' => 'aaa',
'password_confirm' => 'bbb',
]);
$this->controller->create($req, $this->makeResponse());
}
/**
* create() doit rediriger avec une erreur si le nom d'utilisateur est déjà pris.
*/
public function testCreateRedirectsOnDuplicateUsername(): void
{
$this->userService->method('createUser')
->willThrowException(new DuplicateUsernameException('alice'));
$this->flash->expects($this->once())->method('set')
->with('user_error', $this->stringContains("nom d'utilisateur est déjà pris"));
$req = $this->makePost('/admin/users/create', [
'username' => 'alice',
'email' => 'alice@example.com',
'password' => 'password123',
'password_confirm' => 'password123',
]);
$res = $this->controller->create($req, $this->makeResponse());
$this->assertRedirectTo($res, '/admin/users/create');
}
/**
* create() doit rediriger avec une erreur si l'email est déjà utilisé.
*/
public function testCreateRedirectsOnDuplicateEmail(): void
{
$this->userService->method('createUser')
->willThrowException(new DuplicateEmailException('alice@example.com'));
$this->flash->expects($this->once())->method('set')
->with('user_error', $this->stringContains('e-mail est déjà utilisée'));
$req = $this->makePost('/admin/users/create', [
'username' => 'alice',
'email' => 'alice@example.com',
'password' => 'password123',
'password_confirm' => 'password123',
]);
$res = $this->controller->create($req, $this->makeResponse());
$this->assertRedirectTo($res, '/admin/users/create');
}
/**
* create() doit rediriger avec une erreur si le mot de passe est trop court.
*/
public function testCreateRedirectsOnWeakPassword(): void
{
$this->userService->method('createUser')
->willThrowException(new WeakPasswordException());
$this->flash->expects($this->once())->method('set')
->with('user_error', $this->stringContains('8 caractères'));
$req = $this->makePost('/admin/users/create', [
'username' => 'alice',
'email' => 'alice@example.com',
'password' => 'short',
'password_confirm' => 'short',
]);
$res = $this->controller->create($req, $this->makeResponse());
$this->assertRedirectTo($res, '/admin/users/create');
}
/**
* create() doit flasher un succès et rediriger vers /admin/users en cas de succès.
*/
public function testCreateRedirectsToUsersListOnSuccess(): void
{
$this->userService->method('createUser')->willReturn($this->makeUser(99, 'alice', User::ROLE_USER));
$this->flash->expects($this->once())->method('set')
->with('user_success', $this->stringContains('alice'));
$req = $this->makePost('/admin/users/create', [
'username' => 'alice',
'email' => 'alice@example.com',
'password' => 'password123',
'password_confirm' => 'password123',
]);
$res = $this->controller->create($req, $this->makeResponse());
$this->assertRedirectTo($res, '/admin/users');
}
/**
* create() doit forcer le rôle 'user' si un rôle admin est soumis dans le formulaire.
*/
public function testCreateForcesRoleUserWhenAdminRoleSubmitted(): void
{
$this->userService->expects($this->once())
->method('createUser')
->with('alice', 'alice@example.com', 'password123', User::ROLE_USER);
$this->flash->method('set');
$req = $this->makePost('/admin/users/create', [
'username' => 'alice',
'email' => 'alice@example.com',
'password' => 'password123',
'password_confirm' => 'password123',
'role' => User::ROLE_ADMIN, // rôle injecté par l'attaquant
]);
$this->controller->create($req, $this->makeResponse());
}
// ── updateRole ───────────────────────────────────────────────────
/**
* updateRole() doit rediriger avec une erreur si l'utilisateur est introuvable.
*/
public function testUpdateRoleRedirectsWhenUserNotFound(): void
{
$this->userService->method('findById')->willReturn(null);
$this->flash->expects($this->once())->method('set')
->with('user_error', 'Utilisateur introuvable');
$res = $this->controller->updateRole(
$this->makePost('/admin/users/role/99', ['role' => User::ROLE_EDITOR]),
$this->makeResponse(),
['id' => '99'],
);
$this->assertRedirectTo($res, '/admin/users');
}
/**
* updateRole() doit rediriger avec une erreur si l'admin tente de changer son propre rôle.
*/
public function testUpdateRoleRedirectsWhenAdminTriesToChangeOwnRole(): void
{
$user = $this->makeUser(1, 'admin', User::ROLE_USER);
$this->userService->method('findById')->willReturn($user);
$this->sessionManager->method('getUserId')->willReturn(1); // même ID
$this->flash->expects($this->once())->method('set')
->with('user_error', $this->stringContains('propre rôle'));
$res = $this->controller->updateRole(
$this->makePost('/admin/users/role/1', ['role' => User::ROLE_EDITOR]),
$this->makeResponse(),
['id' => '1'],
);
$this->assertRedirectTo($res, '/admin/users');
}
/**
* updateRole() doit rediriger avec une erreur si l'utilisateur cible est déjà admin.
*/
public function testUpdateRoleRedirectsWhenTargetIsAdmin(): void
{
$user = $this->makeUser(2, 'superadmin', User::ROLE_ADMIN);
$this->userService->method('findById')->willReturn($user);
$this->sessionManager->method('getUserId')->willReturn(1);
$this->flash->expects($this->once())->method('set')
->with('user_error', $this->stringContains("administrateur ne peut pas être modifié"));
$res = $this->controller->updateRole(
$this->makePost('/admin/users/role/2', ['role' => User::ROLE_EDITOR]),
$this->makeResponse(),
['id' => '2'],
);
$this->assertRedirectTo($res, '/admin/users');
}
/**
* updateRole() doit rediriger avec une erreur si le rôle soumis est invalide.
*/
public function testUpdateRoleRedirectsOnInvalidRole(): void
{
$user = $this->makeUser(2, 'bob', User::ROLE_USER);
$this->userService->method('findById')->willReturn($user);
$this->sessionManager->method('getUserId')->willReturn(1);
$this->flash->expects($this->once())->method('set')
->with('user_error', 'Rôle invalide');
$res = $this->controller->updateRole(
$this->makePost('/admin/users/role/2', ['role' => 'superuser']),
$this->makeResponse(),
['id' => '2'],
);
$this->assertRedirectTo($res, '/admin/users');
}
/**
* updateRole() doit appeler userService et rediriger avec succès.
*/
public function testUpdateRoleRedirectsWithSuccessFlash(): void
{
$user = $this->makeUser(2, 'bob', User::ROLE_USER);
$this->userService->method('findById')->willReturn($user);
$this->sessionManager->method('getUserId')->willReturn(1);
$this->userService->expects($this->once())->method('updateRole')->with(2, User::ROLE_EDITOR);
$this->flash->expects($this->once())->method('set')
->with('user_success', $this->stringContains('bob'));
$res = $this->controller->updateRole(
$this->makePost('/admin/users/role/2', ['role' => User::ROLE_EDITOR]),
$this->makeResponse(),
['id' => '2'],
);
$this->assertRedirectTo($res, '/admin/users');
}
// ── delete ───────────────────────────────────────────────────────
/**
* delete() doit rediriger avec une erreur si l'utilisateur est introuvable.
*/
public function testDeleteRedirectsWhenUserNotFound(): void
{
$this->userService->method('findById')->willReturn(null);
$this->flash->expects($this->once())->method('set')
->with('user_error', 'Utilisateur introuvable');
$res = $this->controller->delete(
$this->makePost('/admin/users/delete/99'),
$this->makeResponse(),
['id' => '99'],
);
$this->assertRedirectTo($res, '/admin/users');
}
/**
* delete() doit rediriger avec une erreur si la cible est administrateur.
*/
public function testDeleteRedirectsWhenTargetIsAdmin(): void
{
$user = $this->makeUser(2, 'superadmin', User::ROLE_ADMIN);
$this->userService->method('findById')->willReturn($user);
$this->sessionManager->method('getUserId')->willReturn(1);
$this->flash->expects($this->once())->method('set')
->with('user_error', $this->stringContains('administrateur ne peut pas être supprimé'));
$res = $this->controller->delete(
$this->makePost('/admin/users/delete/2'),
$this->makeResponse(),
['id' => '2'],
);
$this->assertRedirectTo($res, '/admin/users');
}
/**
* delete() doit rediriger avec une erreur si l'admin tente de supprimer son propre compte.
*/
public function testDeleteRedirectsWhenAdminTriesToDeleteOwnAccount(): void
{
$user = $this->makeUser(1, 'alice', User::ROLE_USER);
$this->userService->method('findById')->willReturn($user);
$this->sessionManager->method('getUserId')->willReturn(1); // même ID
$this->flash->expects($this->once())->method('set')
->with('user_error', $this->stringContains('propre compte'));
$res = $this->controller->delete(
$this->makePost('/admin/users/delete/1'),
$this->makeResponse(),
['id' => '1'],
);
$this->assertRedirectTo($res, '/admin/users');
}
/**
* delete() doit appeler userService et rediriger avec succès.
*/
public function testDeleteRedirectsWithSuccessFlash(): void
{
$user = $this->makeUser(2, 'bob', User::ROLE_USER);
$this->userService->method('findById')->willReturn($user);
$this->sessionManager->method('getUserId')->willReturn(1);
$this->userService->expects($this->once())->method('delete')->with(2);
$this->flash->expects($this->once())->method('set')
->with('user_success', $this->stringContains('bob'));
$res = $this->controller->delete(
$this->makePost('/admin/users/delete/2'),
$this->makeResponse(),
['id' => '2'],
);
$this->assertRedirectTo($res, '/admin/users');
}
// ── Helpers ──────────────────────────────────────────────────────
/**
* Crée un utilisateur de test avec les paramètres minimaux.
*/
private function makeUser(int $id, string $username, string $role): User
{
return new User($id, $username, "{$username}@example.com", password_hash('secret', PASSWORD_BCRYPT), $role);
}
}