first commit
This commit is contained in:
441
tests/User/UserControllerTest.php
Normal file
441
tests/User/UserControllerTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user