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); } }