444 lines
16 KiB
PHP
444 lines
16 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\User;
|
|
|
|
use App\Shared\Http\FlashServiceInterface;
|
|
use App\Shared\Http\SessionManagerInterface;
|
|
use App\Shared\Pagination\PaginatedResult;
|
|
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\ControllerTestBase;
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
|
final class UserControllerTest extends ControllerTestBase
|
|
{
|
|
/** @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('findPaginated')->willReturn(new PaginatedResult([], 0, 1, 20));
|
|
$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);
|
|
}
|
|
}
|