diff --git a/VERSIONS_HOTLINE/194779 b/VERSIONS_HOTLINE/194779
new file mode 100644
index 0000000000000000000000000000000000000000..90d7ee59cf24bc686cf6f1f392a1f5bd7a14f3cb
--- /dev/null
+++ b/VERSIONS_HOTLINE/194779
@@ -0,0 +1 @@
+ - correctif #194779 : SIGB Koha : la mise à jour des informations personnelles n'écrit plus d'informations vides dans le compte lecteur Koha.
\ No newline at end of file
diff --git a/library/Class/AdminVar.php b/library/Class/AdminVar.php
index a7aab441367087b59238c60a7f8a5757e8f87fae..5b1c3dd0d70db50462e995f495460d9a5ca17d2b 100644
--- a/library/Class/AdminVar.php
+++ b/library/Class/AdminVar.php
@@ -1154,7 +1154,8 @@ Pour vous désabonner de la lettre d\'information, merci de cliquer sur le lien
   }
 
 
-  public function getChampsFicheUtilisateur() {
+  public function getChampsFicheUtilisateur(): array
+  {
     return array_filter(explode(';', trim(Class_AdminVar::get('CHAMPS_FICHE_UTILISATEUR'))));
   }
 
diff --git a/library/Class/CommSigb.php b/library/Class/CommSigb.php
index f81a9c6e80eccadf69005706ff6b45c7cde378a5..640bc36ebc6a38b966252b3d5fcf323796cb1c17 100644
--- a/library/Class/CommSigb.php
+++ b/library/Class/CommSigb.php
@@ -21,6 +21,8 @@
 class Class_CommSigb {
   use Trait_Translator, Trait_Errors;
 
+  protected static bool $_should_throw_error = false;
+
   protected static $_instance = null;
   protected static $_logger;
 
@@ -96,8 +98,12 @@ class Class_CommSigb {
     $cache = Class_WebService_SIGB_EmprunteurCache::newInstance();
 
     $ficheAbonneClosure = function ($user, $sigb) use ($cache) {
-      try {
+      $emprunteur = null;
+      if (static::$_should_throw_error)
         $emprunteur = $cache->loadFromCacheOrSIGB($user, $sigb);
+
+      try {
+        $emprunteur ??= $cache->loadFromCacheOrSIGB($user, $sigb);
         if (!$emprunteur || $emprunteur->isNullInstance()){
           $cache->remove($user);
           return $this->_error($this->_('Abonné inconnu dans le SIGB'));
@@ -344,4 +350,10 @@ class Class_CommSigb {
     $this->addError($message);
     return ['erreur' => $message];
   }
+
+
+  public static function shouldThrowError(bool $should): void
+  {
+    static::$_should_throw_error = $should;
+  }
 }
diff --git a/library/Class/CosmoVar.php b/library/Class/CosmoVar.php
index a5066c8003233f43835b09eb4493fd8f89f0eaf6..6d07ccc82e164588fcc924ba862c88f41b616e11 100644
--- a/library/Class/CosmoVar.php
+++ b/library/Class/CosmoVar.php
@@ -103,6 +103,19 @@ class Class_CosmoVarLoader extends Storm_Model_Loader {
   public function getWorkKeyComposition() {
     return explode(';', Class_CosmoVar::get('work_key_composition'));
   }
+
+
+  public function get(string $name)
+  {
+    return Class_CosmoVar::getValueOf($name);
+  }
+
+
+  public function set(string $name, $value)
+  {
+    return Class_CosmoVar::setValueOf($name, $value);
+  }
+
 }
 
 
@@ -145,16 +158,6 @@ class Class_CosmoVar extends Storm_Model_Abstract {
     $_meta;
 
 
-  public static function get($name) {
-    return Class_CosmoVar::getValueOf($name);
-  }
-
-
-  public function set($name, $value) {
-    return Class_CosmoVar::setValueOf($name, $value);
-  }
-
-
   public function getListAsArray() {
     $result = [];
 
diff --git a/library/Class/HttpClientFactory.php b/library/Class/HttpClientFactory.php
index d9cdca58e34946da1fe444f1ab7953249f8fea08..9f49dc9f46541b4ca8bac8c5b921f9a0baa08d60 100644
--- a/library/Class/HttpClientFactory.php
+++ b/library/Class/HttpClientFactory.php
@@ -20,21 +20,51 @@
  */
 
 
-class Class_HttpClientFactory {
+class Class_HttpClientFactory
+{
+
   use Trait_Singleton;
 
-  protected static $_adapter;
-  protected $_last_client;
+
+  protected static Zend_Http_Client_Adapter_Interface $_adapter;
+  protected Zend_Http_Client $_last_client;
 
 
-  public static function setAdapter($adapter) {
+  public static function setAdapter(Zend_Http_Client_Adapter_Interface $adapter): void
+  {
     static::$_adapter = $adapter;
   }
 
 
-  public function newHttpClient($config=[]) {
-    $client = new Zend_Http_Client();
-    if (static::$_adapter)
+  public static function forTest(): Zend_Http_Client
+  {
+    Class_WebService_SIGB_AbstractRESTService::shouldThrowError(true);
+    Class_CommSigb::shouldThrowError(true);
+    static::setAdapter(new Zend_Http_Client_Adapter_Test);
+    $instance = new static;
+    $client = $instance->newHttpClientForTest();
+    static::setInstance($instance);
+    return $client;
+  }
+
+
+  public function newHttpClientForTest(): Zend_Http_Client
+  {
+    return $this->_setClient(new HttpClientForTest);
+  }
+
+
+  public function newHttpClient(array $config = []): Zend_Http_Client
+  {
+    return ( isset($this->_last_client) && $this->_last_client instanceof HttpClientForTest )
+      ? $this->_last_client
+      : $this->_setClient(new Zend_Http_Client, $config);
+  }
+
+
+  public function _setClient(Zend_Http_Client $client, array $config = []): Zend_Http_Client
+  {
+    if (isset(static::$_adapter))
       $client->setAdapter(static::$_adapter);
 
     $client->setConfig(array_merge(['timeout' => 2], $config));
@@ -42,7 +72,8 @@ class Class_HttpClientFactory {
   }
 
 
-  public function getLastHttpClient() {
-    return $this->_last_client;
+  public function getLastHttpClient(): ?Zend_Http_Client
+  {
+    return $this->_last_client ?? null;
   }
 }
diff --git a/library/Class/User/Cards.php b/library/Class/User/Cards.php
index 13c1d83d2d24d8ce450150245991036466409d78..0a843c1693ff1aa9c51139af3bd40b8ce74c0cfb 100644
--- a/library/Class/User/Cards.php
+++ b/library/Class/User/Cards.php
@@ -60,7 +60,7 @@ class Class_User_Cards extends Storm_Model_Collection {
 
 
   public function getSuggestions() :Storm_Collection {
-    return $this->_decorateOperationFrom(function($card) { return new Storm_Collection($card->getSuggestionAchat()); });
+    return $this->_decorateOperationFrom(fn($card) => new Storm_Collection($card->getSuggestionAchat()));
   }
 
 
@@ -125,8 +125,9 @@ class Class_User_Cards extends Storm_Model_Collection {
   }
 
 
-  public function getLoansCount() :int {
-    $count=0;
+  public function getLoansCount(): int
+  {
+    $count = 0;
     return $this->injectInto(0,
                              fn ($count, $card) => $count + $card->getLoansCount());
   }
diff --git a/library/Class/User/EditFormHelper.php b/library/Class/User/EditFormHelper.php
index 7f6d00d03d9611b0d8260800cba5c3c527ee2c9d..5cbcfe9a73f998847853d5eb307acd437a881614 100644
--- a/library/Class/User/EditFormHelper.php
+++ b/library/Class/User/EditFormHelper.php
@@ -25,25 +25,25 @@ class Class_User_EditFormHelper {
   use Trait_Translator;
 
 
-  protected
-    $_user,
-    $_view,
-    $_web_request,
-    $_form,
-    $_old_password,
-    $_callback,
-    $_fields_to_show,
-    $_favorite_sending_channel = '';
-
-
-  public function __construct($user, $view, $request) {
+  protected Class_Users $_user;
+  protected Zend_View $_view;
+  protected Zend_Controller_Request_Http $_web_request;
+  protected Closure $_callback;
+  protected array $_fields_to_show;
+  protected string $_favorite_sending_channel;
+  protected string $_old_password;
+
+
+  public function __construct(Class_Users $user, Zend_View $view, Zend_Controller_Request_Http $request)
+  {
     $this->_user = $user;
     $this->_view = $view;
     $this->_web_request = $request;
   }
 
 
-  public function beChangePassword() {
+  public function beChangePassword(): self
+  {
     $this->_form = new ZendAfi_Form;
 
     $this->_form
@@ -74,7 +74,8 @@ class Class_User_EditFormHelper {
   }
 
 
-  public function beEditUser() {
+  public function beEditUser(): self
+  {
     $this->_form = new ZendAfi_Form;
     $this->_fields_to_show = Class_AdminVar::getChampsFicheUtilisateur();
     $this
@@ -109,8 +110,7 @@ class Class_User_EditFormHelper {
                                                                     Class_Users::MODE_CONTACT_MAIL => $this->_(' par E-Mail'),
                                                                     Class_Users::MODE_CONTACT_SMS => $this->_(' par SMS')]])],
                               'mode_de_contact',
-                              $this->_('Recevoir mes notifications de réservation et de rappel'))
-      ;
+                              $this->_('Recevoir mes notifications de réservation et de rappel'));
 
     if ($this->_user->canChooseSendingChannel())
       $this->_filteredDisplayGroup(['favoriteSendingChannel' => $this->_radioForSendingChannels()],
@@ -129,15 +129,16 @@ class Class_User_EditFormHelper {
       unset($values['favoriteSendingChannel']);
 
       $this->_user
-      ->updateAttributes($values)
-      ->setPassword($password);
+        ->updateAttributes($values)
+        ->setPassword($password);
     };
 
     return $this;
   }
 
 
-  protected function _radioForSendingChannels() : Zend_Form_Element_Radio {
+  protected function _radioForSendingChannels(): Zend_Form_Element_Radio
+  {
     return $this->_createElement('radio', 'favoriteSendingChannel',
                                  ['label' => $this->_('Préférence de communication'),
                                   'multiOptions' => $this->_getSendingChannels()])
@@ -145,7 +146,8 @@ class Class_User_EditFormHelper {
   }
 
 
-  protected function _getSendingChannels() : array {
+  protected function _getSendingChannels(): array
+  {
     $sending_channel = [];
     foreach ($this->_user->getSendingChannels() as $channel)
       $sending_channel[$channel->getId()] = $channel->getLabel();
@@ -154,12 +156,14 @@ class Class_User_EditFormHelper {
   }
 
 
-  protected function _createElement($type, $name, $options) {
+  protected function _createElement($type, $name, $options): Zend_Form_Element
+  {
     return $this->_form->createElement($type, $name, $options);
   }
 
 
-  protected function _filteredDisplayGroup($fields, $group_name, $group_label) {
+  protected function _filteredDisplayGroup(array $fields, string $group_name, string $group_label): self
+  {
     if (!$fields = array_intersect_key($fields,
                                        array_fill_keys($this->_fields_to_show, 1)))
       return $this;
@@ -178,7 +182,8 @@ class Class_User_EditFormHelper {
   }
 
 
-  public function proceed() {
+  public function proceed(): bool
+  {
     $this->_form
       ->setAction($this->_view->url())
       ->setMethod('POST')
@@ -189,9 +194,20 @@ class Class_User_EditFormHelper {
     if ( ! $this->_web_request->isPost())
       return false;
 
-    if ( ! $this->_form->isValid($this->_web_request->getPost()))
+    $post_values = $this->_web_request->getPost();
+
+    if ( ! $this->_form->isValid($post_values))
       return false;
 
+    if (isset($this->_fields_to_show)
+        && $this->_fields_to_show
+        && array_diff(array_keys($post_values),
+                      $this->_fields_to_show))
+      return $this->_cantEditFormError();
+
+    if ($this->_form->isEmpty())
+      return $this->_cantEditFormError();
+
     $callback = $this->_callback;
     $callback();
 
@@ -207,22 +223,31 @@ class Class_User_EditFormHelper {
   }
 
 
-  protected function _updateUser() {
+  protected function _cantEditFormError(): bool
+  {
+    $this->_form->addDecorator('Errors')
+                ->addError($this->_('Une erreur c\'est produite. Vous ne pouvez pas modifier ce formulaire.'));
+    return false;
+  }
+
+
+  protected function _updateUser(): bool
+  {
     $patron = $this->_user->getEmprunteur();
     $patron->updateFromUser($this->_user);
-    $patron->setFavoriteSendingChannel($this->_favorite_sending_channel);
-    $patron->setPreviousPassword($this->_old_password);
 
-    try {
-      $patron->ensureAndSave($this->_user);
-    } catch(Exception $e) {
-      $this->_form->addError($e->getMessage());
+    if ( isset($this->_favorite_sending_channel))
+      $patron->setFavoriteSendingChannel($this->_favorite_sending_channel);
+
+    if ( isset($this->_old_password))
+      $patron->setPreviousPassword($this->_old_password);
+
+    if ( ! $patron->ensureAndSave($this->_user)) {
+      $this->_form->addErrors($patron->getErrors());
       $this->_form->addDecorator('Errors');
       return false;
     }
 
-    Class_WebService_SIGB_EmprunteurCache::newInstance()->save($this->_user, $patron);
-
     return $this->_user->save();
   }
 }
diff --git a/library/Class/Users.php b/library/Class/Users.php
index 1ba6efc33aea3949261fb22ba5b420dcce74f323..44b65ded77dcc821fdf7c5184598b69831f655d7 100644
--- a/library/Class/Users.php
+++ b/library/Class/Users.php
@@ -595,9 +595,7 @@ class Class_Users extends Storm_Model_Abstract {
 
   public function getUserIdSite() {
     $comm = new Class_CommSigb();
-    return
-      (($id = $comm->getUserAnnexe($this))
-        && (!is_array( $id)))
+    return (($id = $comm->getUserAnnexe($this)) && (!is_array( $id)))
       ? $id
       : $this->getLibraryCode();
   }
@@ -1043,9 +1041,7 @@ class Class_Users extends Storm_Model_Abstract {
       return null;
 
     $avis = $notice->getAvisByUser($this);
-    return count($avis) > 0
-      ? $avis[0]
-      : null;
+    return count($avis) ? reset($avis) : null;
   }
 
 
@@ -1225,7 +1221,8 @@ class Class_Users extends Storm_Model_Abstract {
   }
 
 
-  public function getLoans(array $params = []) : Class_User_Loans {
+  public function getLoans(array $params = []): Class_User_Loans
+  {
     if (!isset($this->_loans) || isset($params['nocache'])) {
       $loans = $this->getIlsLoans($params);
       $loans->addAll($this->getPNBLoans());
@@ -1235,19 +1232,18 @@ class Class_Users extends Storm_Model_Abstract {
   }
 
 
-   public function getIlsLoans($params = []) {
+  public function getIlsLoans($params = []) {
     if ($emprunteur =  $this
-         ->getEmprunteur()
-         ->ensureService($this))
+        ->getEmprunteur()
+        ->ensureService($this))
       return new Storm_Collection($emprunteur->getLoans($params));
     return new Storm_Collection();
   }
 
 
-  public function getPNBLoans() : Storm_Collection {
-     if (isset($this->_pnb_loans))
-       return $this->_pnb_loans;
-    return $this->_pnb_loans = new Storm_Collection(Class_Loan_Pnb::findAllOngoingOfUser($this));
+  public function getPNBLoans(): Storm_Collection
+  {
+    return $this->_pnb_loans ??= new Storm_Collection(Class_Loan_Pnb::findAllOngoingOfUser($this));
   }
 
 
@@ -1278,29 +1274,33 @@ class Class_Users extends Storm_Model_Abstract {
   }
 
 
-  public function getEmprunteur() : Class_WebService_SIGB_Emprunteur{
+  public function getEmprunteur(): Class_WebService_SIGB_Emprunteur
+  {
     return $this->getFicheSigb()['fiche'] ;
   }
 
 
   public function getEmprunteurId() {
-      return ($emprunteur = $this->getEmprunteur())
+    return ($emprunteur = $this->getEmprunteur())
       ? $emprunteur->getId()
       : null;
   }
 
 
-  public function getIlsLoansCount() :int {
+  public function getIlsLoansCount(): int
+  {
     if ($emprunteur =  $this
-         ->getEmprunteur()
-         ->ensureService($this))
+        ->getEmprunteur()
+        ->ensureService($this))
       return $emprunteur->getNbEmprunts() ?? 0;
+
     return 0;
   }
 
 
   public function getLoansCount() :int {
-    return $this->getIlsLoansCount()+ $this->getPNBLoansCount();
+    return $this->getIlsLoansCount()
+      + $this->getPNBLoansCount();
   }
 
 
@@ -1370,19 +1370,19 @@ class Class_Users extends Storm_Model_Abstract {
   }
 
 
-  public function getConsultations() {
+  public function getConsultations(): array
+  {
     if (!$this->isAbonne())
-      return[];
+      return [];
 
     $consultations = Class_CommSigb::getInstance()->getOnPlaceConsultationBookings($this);
 
-    return isset($consultations['erreur'])
-      ? []
-      : $consultations;
+    return isset($consultations['erreur']) ? [] : $consultations;
   }
 
 
-  public function getNbConsultations() {
+  public function getNbConsultations(): int
+  {
     return count($this->getConsultations());
   }
 
@@ -1777,10 +1777,13 @@ class Class_Users extends Storm_Model_Abstract {
   }
 
 
-  public function getSuggestionAchat() {
-    return (($sigb_com = $this->getSIGBComm()) && $sigb_com->providesSuggestions())
+  public function getSuggestionAchat(): array
+  {
+    $suggestions = (($sigb_com = $this->getSIGBComm()) && $sigb_com->providesSuggestions())
       ? $sigb_com->suggestionsOf($this)
       : parent::_get('suggestion_achat');
+
+    return ($suggestions['erreur'] ?? '') ? [] : $suggestions;
   }
 
 
@@ -1895,13 +1898,12 @@ class Class_Users extends Storm_Model_Abstract {
   }
 
 
-  public function getLibelleCivilite() {
+  public function getLibelleCivilite()
+  {
     $title = $this->getCivilite();
     $labels = $this->getLoader()->getCivilitiesLabels();
 
-    return (static::CIVILITE_INDEFINIE != $title || array_key_exists($title, $labels))
-      ? $labels[$title]
-      : '';
+    return (static::CIVILITE_INDEFINIE != $title || array_key_exists($title, $labels)) ? $labels[$title] : '';
   }
 
 
@@ -2111,13 +2113,10 @@ class Class_Users extends Storm_Model_Abstract {
       return [];
 
     usort($user_memberships,
-          function($a,$b){
-            return $b->getEndDate() <=> $a->getEndDate();
-          });
+          fn($a,$b) => $b->getEndDate() <=> $a->getEndDate());
+
     return ($array_filtered = array_filter($user_memberships,
-                        function($element){
-                          return $element->isValidSubscription();
-                        }))
+                                           fn($element) => $element->isValidSubscription()))
       ? $array_filtered
       : [reset($user_memberships)];
   }
diff --git a/library/Class/WebService/C3rb.php b/library/Class/WebService/C3rb.php
index d492e8cfe0e5e7da26c0fd826550f43c9dbd8f0d..e1e75d94e7fafcc3a1aada95b38bdd2369e011c2 100644
--- a/library/Class/WebService/C3rb.php
+++ b/library/Class/WebService/C3rb.php
@@ -33,7 +33,8 @@ class Class_WebService_C3rb extends Class_WebService_Cas2 {
   }
 
 
-  public function updateUser(Class_Users $user) : self {
+  public function updateUser(Class_Users $user) : self
+  {
     foreach ($this->_user_attributes as  $function_name => $value)
       $user->callSetterByAttributeName($function_name,
                                        $this->_validateValue($value, $function_name));
diff --git a/library/Class/WebService/Cas3.php b/library/Class/WebService/Cas3.php
index 0d543949afc1f4be1d4de2e1a2587856405771a4..34a1fc12a0d9383dee2877aacd9bcdf60e2bf254 100644
--- a/library/Class/WebService/Cas3.php
+++ b/library/Class/WebService/Cas3.php
@@ -54,7 +54,8 @@ class Class_WebService_Cas3 extends Class_WebService_Cas2 {
   }
 
 
-  public function updateUser(Class_Users $user) : self {
+  public function updateUser(Class_Users $user): self
+  {
     foreach ($this->_user_attributes as  $function_name => $value)
       $user->callSetterByAttributeName($function_name, $this->_validateValue($value, $function_name)) ;
     return $this;
diff --git a/library/Class/WebService/IdentityProvider.php b/library/Class/WebService/IdentityProvider.php
index f74eb72c841c27a030df53fcb7c7809fe49d62e4..917f6df997683420551c930c2a6d02f45b6548df 100644
--- a/library/Class/WebService/IdentityProvider.php
+++ b/library/Class/WebService/IdentityProvider.php
@@ -101,7 +101,8 @@ abstract class Class_WebService_IdentityProvider {
   }
 
 
-  public function updateUser( Class_Users $user) : self {
+  public function updateUser(Class_Users $user): self
+  {
     $user->setNom($this->_provider->getLabel());
     return $this;
   }
diff --git a/library/Class/WebService/OpenId.php b/library/Class/WebService/OpenId.php
index 1fcb4cbf3b26e2004bd49a6b6b6390361386c134..e874114cdc72dfc3a0fc905c7b0b269b2e75b38e 100644
--- a/library/Class/WebService/OpenId.php
+++ b/library/Class/WebService/OpenId.php
@@ -95,7 +95,8 @@ class Class_WebService_OpenId extends Class_WebService_IdentityProvider {
   }
 
 
-  public function updateUser( Class_Users $user) : self {
+  public function updateUser(Class_Users $user): self
+  {
     $infos = $this->getSession()->userinfo;
     $user
       ->setPseudo($this->remoteName())
diff --git a/library/Class/WebService/SIGB/AbstractService.php b/library/Class/WebService/SIGB/AbstractService.php
index 71aa56daa843ea4cfa3c5423142338a46e8e32c8..b6e6e42791a51f8a79bb32d569df59c5a79c8406 100644
--- a/library/Class/WebService/SIGB/AbstractService.php
+++ b/library/Class/WebService/SIGB/AbstractService.php
@@ -262,8 +262,9 @@ abstract class Class_WebService_SIGB_AbstractService {
   }
 
 
-  public function alreadySort() : bool {
-    return false;
+  public function shouldSortLoans(): bool
+  {
+    return true;
   }
 
 
@@ -272,7 +273,8 @@ abstract class Class_WebService_SIGB_AbstractService {
   }
 
 
-  public function suggestionsOf($user) {
+  public function suggestionsOf(Class_Users $user): array
+  {
     return [];
   }
 
@@ -302,12 +304,14 @@ abstract class Class_WebService_SIGB_AbstractService {
   }
 
 
-  protected function _success() :array {
+  protected function _success(): array
+  {
     return ['statut' => true, 'erreur' => ''];
   }
 
 
-  protected function _error($message) {
+  protected function _error(string $message): array
+  {
     $this->addError($message);
     return ['statut' => false, 'erreur' => $message];
   }
@@ -343,7 +347,8 @@ abstract class Class_WebService_SIGB_AbstractService {
   }
 
 
-  public function resetPassword($emprunteur) {
+  public function resetPassword(Class_WebService_SIGB_Emprunteur $emprunteur): array
+  {
     return $this->_error($this->_('Impossible de metttre à jour le mot de passe: fonctionnalité non implémentée.' ));
   }
 
diff --git a/library/Class/WebService/SIGB/Decalog/Service.php b/library/Class/WebService/SIGB/Decalog/Service.php
index ab3bb1db794e76613d40225260272e03ad733782..979e10756aa309f89c129a30d8dab1c4d7435095 100644
--- a/library/Class/WebService/SIGB/Decalog/Service.php
+++ b/library/Class/WebService/SIGB/Decalog/Service.php
@@ -177,7 +177,8 @@ class Class_WebService_SIGB_Decalog_Service extends Class_WebService_SIGB_Abstra
   }
 
 
-  public function resetPassword($emprunteur){
+  public function resetPassword(Class_WebService_SIGB_Emprunteur $emprunteur): array
+  {
     $json = $this->_callAction('resetpassword', [], ['email' => $emprunteur->getEmail()]);
 
     if ((new Class_WebService_SIGB_Decalog_AccountResponseReader($json, $this))
diff --git a/library/Class/WebService/SIGB/Emprunteur.php b/library/Class/WebService/SIGB/Emprunteur.php
index aa07360a447512c7fa4cda87a6c28cf35803b013..acc37f26d7c2403053820cb53d01ef234bd394bd 100644
--- a/library/Class/WebService/SIGB/Emprunteur.php
+++ b/library/Class/WebService/SIGB/Emprunteur.php
@@ -19,54 +19,57 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
  */
 
-class Class_WebService_SIGB_Emprunteur {
+class Class_WebService_SIGB_Emprunteur
+{
   use Trait_Errors;
   use Trait_Translator;
 
 
   protected string $_id = '';
+  protected string $_name = '';
+  protected string $_nom = '';
+  protected string $_prenom = '';
+  protected string $_login = '';
+  protected string $_old_login = '';
+  protected string $_password = '';
+  protected string $_previous_password = '';
+  protected string $_email = '';
+  protected string $_end_date = '';
+  protected string $_ordre = '';
+  protected string $_code_barres = '';
+  protected string $_adresse = '';
+  protected string $_ville = '';
+  protected string $_code_postal = '';
+  protected string $_telephone = '';
+  protected string $_mobile = '';
+  protected string $_date_naissance = '';
+  protected string $_library_code = '';
+  protected string $_id_int_bib = '';
+  protected string $_public_comment = '';
+  protected string $_favorite_sending_channel = '';
 
-  protected
-    $_name,
-    $_emprunts = [],
-    $_subscriptions,
-    $_debts = [],
-    $_reservations = [],
-    $_waiting_holds = [],
-    $_email = '',
-    $_nom = null,
-    $_prenom = null,
-    $_login = null,
-    $_old_login = null,
-    $_password = null,
-    $_previous_password = null,
-    $_nb_reservations = null,
-    $_nb_emprunts = null,
-    $_nb_retards = null,
-    $_service = null,
-    $_valid = false,
-    $_blocked = false,
-    $_end_date = null,
-    $_ordre,
-    $_code_barres,
-    $_adresse,
-    $_ville,
-    $_code_postal,
-    $_telephone,
-    $_mobile,
-    $_date_naissance,
-    $_is_contact_email= 0,
-    $_is_contact_sms= 0,
-    $_library_code,
-    $_id_int_bib,
-    $_loans_history = [],
-    $_multimedia_access = -1,
-    $_parental_authorization = -1,
-    $_public_comment = '',
-    $_public_comment_read = true;
-
+  protected array $_emprunts = [];
+  protected array $_debts = [];
+  protected array $_reservations = [];
+  protected array $_waiting_holds = [];
+  protected array $_loans_history = [];
   protected array $_sending_channels = [];
-  protected string $_favorite_sending_channel = '';
+
+  protected int $_nb_reservations;
+  protected int $_nb_emprunts;
+  protected int $_nb_retards;
+  protected int $_multimedia_access = -1;
+  protected int $_parental_authorization = -1;
+
+  protected bool $_valid = false;
+  protected bool $_blocked = false;
+  protected bool $_is_contact_email = false;
+  protected bool $_is_contact_sms = false;
+  protected bool $_public_comment_read = true;
+
+  protected $_service;
+
+  protected Storm_Collection $_subscriptions;
 
 
   public function __sleep() {
@@ -106,305 +109,275 @@ class Class_WebService_SIGB_Emprunteur {
   }
 
 
-  /**
-   * @param mixed $id
-   * @param string $name
-   * @return Class_WebService_SIGB_Emprunteur
-   */
-  public static function newInstance($id = '', $name = '') {
+  public static function newInstance(string $id = '', string $name = ''): self
+  {
     return new static($id, $name);
   }
 
 
-  /**
-   * Return an empty emprunteur
-   * @return Class_WebService_SIGB_EmprunteurNull
-   */
-  public static function nullInstance() {
+  public static function nullInstance(): Class_WebService_SIGB_EmprunteurNull
+  {
     return Class_WebService_SIGB_EmprunteurNull::newInstance()->empruntsAddAll([])->reservationsAddAll([]);
   }
 
 
-  public function isNullInstance() : bool {
+  public function isNullInstance(): bool
+  {
     return false;
   }
 
 
-  /**
-   * @param string $id
-   * @param string $name
-   */
-  function __construct($id, $name){
-    $this->_id = (string) $id;
+  function __construct(string $id, string $name)
+  {
+    $this->_id = $id;
     $this->_name = $name;
     $this->_emprunts = [];
     $this->_reservations = [];
   }
 
 
-  public function updateUserRelations($user) {}
+  public function updateUserRelations(Class_Users $user): void
+  {
+  }
 
 
-  /**
-   * @param string $id
-   * @return Class_WebService_SIGB_Emprunteur
-   */
-  public function setId($id) {
-    $this->_id = (string) $id;
+  public function setId(string $id): self
+  {
+    $this->_id = $id;
     return $this;
   }
 
 
-  /**
-   * @param string $name
-   * @return Class_WebService_SIGB_Emprunteur
-   */
-  public function setName($name) {
+  public function setName(string $name): self
+  {
     $this->_name = $name;
     return $this;
   }
 
 
-  /**
-   * @param string $code_barres
-   * @return Class_WebService_SIGB_Emprunteur
-   */
-  public function setCodeBarres($code_barres) {
+  public function setCodeBarres(string $code_barres): self
+  {
     $this->_code_barres = $code_barres;
     return $this;
   }
 
 
-  /**
-   * @return string
-   */
-  public function getCodeBarres(){
+  public function getCodeBarres(): string
+  {
     return $this->_code_barres;
   }
 
 
-  /**
-   * @param string $adresse
-   * @return Class_WebService_SIGB_Emprunteur
-   */
-  public function setAdresse($adresse) {
+  public function setAdresse(string $adresse): self
+  {
     $this->_adresse = $adresse;
     return $this;
   }
 
 
-  /**
-   * @return string
-   */
-  public function getAdresse(){
+  public function getAdresse(): string
+  {
     return $this->_adresse;
   }
 
 
-  /**
-   * @param string $code_postal
-   * @return Class_WebService_SIGB_Emprunteur
-   */
-  public function setCodePostal($code_postal) {
+  public function setCodePostal(string $code_postal): self {
     $this->_code_postal = $code_postal;
     return $this;
   }
 
 
-  /**
-   * @return string
-   */
-  public function getCodePostal(){
+
+  public function getCodePostal(): string
+  {
     return $this->_code_postal;
   }
 
 
-  /**
-   * @param string $ville
-   * @return Class_WebService_SIGB_Emprunteur
-   */
-  public function setVille($ville) {
+  public function setVille(string $ville): self
+  {
     $this->_ville = $ville;
     return $this;
   }
 
 
-  /**
-   * @return string
-   */
-  public function getVille(){
+  public function getVille(): string
+  {
     return $this->_ville;
   }
 
 
-  /**
-   * @param string $ordre
-   * @return Class_WebService_SIGB_Emprunteur
-   */
-  public function setOrdre($ordre) {
+  public function setOrdre(string $ordre): self
+  {
     $this->_ordre = $ordre;
     return $this;
   }
 
 
-  /**
-   * @return string
-   */
-  public function getOrdre(){
+  public function getOrdre(): string
+  {
     return $this->_ordre;
   }
 
 
-  /**
-   * @param string $telephone
-   * @return Class_WebService_SIGB_Emprunteur
-   */
-  public function setTelephone($telephone) {
+  public function setTelephone(string $telephone): self
+  {
     $this->_telephone = $telephone;
     return $this;
   }
 
 
-  public function getTelephone(): string {
+  public function getTelephone(): string
+  {
     return $this->_telephone ?? '';
   }
 
 
-  public function setMobile( ?string $mobile) : self {
+  public function setMobile(string $mobile): self
+  {
     $this->_mobile = $mobile;
     return $this;
   }
 
 
-  public function getIntBib() {
+  public function getIntBib(): ?Class_IntBib
+  {
     return $this->getIdIntBib()
       ? Class_IntBib::find($this->getIdIntBib())
       : null;
   }
 
 
-  public function getMobile(): string{
-    return $this->_mobile ?? '';
+  public function getMobile(): string
+  {
+    return $this->_mobile;
   }
 
 
-  /**
-   * @param string $is_contact_email
-   * @return Class_WebService_SIGB_Emprunteur
-   */
-  public function setIsContactEmail($is_contact_email) {
+  public function setIsContactEmail(string $is_contact_email): self
+  {
     $this->_is_contact_email = $is_contact_email;
     return $this;
   }
 
 
-  /**
-   * @return string
-   */
-  public function isContactEmail(){
+  public function isContactEmail(): string
+  {
     return $this->_is_contact_email;
   }
 
 
 
-  public function setIsContactSms($is_contact_sms) : self {
+  public function setIsContactSms(bool $is_contact_sms): self
+  {
     $this->_is_contact_sms = $is_contact_sms;
     return $this;
   }
 
 
-  public function isContactSms(): bool {
-    return $this->_is_contact_sms ?? false;
+  public function isContactSms(): bool
+  {
+    return $this->_is_contact_sms;
   }
 
 
-  /**
-   * @param string $date_naissance
-   * @return Class_WebService_SIGB_Emprunteur
-   */
-  public function setDateNaissance($date_naissance) {
+  public function setDateNaissance(string $date_naissance): self
+  {
     $this->_date_naissance = $date_naissance;
     return $this;
   }
 
 
-  public function getDateNaissance() : string{
-    return $this->_date_naissance ?? '';
+  public function getDateNaissance(): string
+  {
+    return $this->_date_naissance;
   }
 
 
-  public function subscriptionAdd($subscription_id, $subscription_label) : self {
-    $this->getSubscriptions()->add( (new Class_WebService_SIGB_Subscription)
-                                    ->setId($subscription_id)
-                                    ->setLabel($subscription_label));
+  public function subscriptionAdd(string $subscription_id, string $subscription_label): self
+  {
+    $this->getSubscriptions()
+         ->add( (new Class_WebService_SIGB_Subscription)
+                ->setId($subscription_id)
+                ->setLabel($subscription_label));
     return $this;
   }
 
 
-  public function getSubscriptions() : Storm_Collection {
-    if (!isset($this->_subscriptions))
-      $this->_subscriptions = new Storm_Collection;
-    return $this->_subscriptions;
+  public function getSubscriptions(): Storm_Collection
+  {
+    return $this->_subscriptions ??= new Storm_Collection;
   }
 
 
-  public function empruntsAddAll( array $emprunts): self {
+  public function empruntsAddAll(array $emprunts): self
+  {
     $this->_emprunts = array_merge($this->_emprunts, $emprunts);
 
-    if (!$this->_service || ! $this->_service->alreadySort())
-      $this->sortByDateRetour($this->_emprunts);
+    if( (isset($this->_service) && $this->_service->shouldSortLoans())
+        || ! isset($this->_service))
+      $this->_emprunts = $this->_sortByDateRetour($this->_emprunts);
 
     return $this;
   }
 
 
 
-  public function empruntsAdd(Class_WebService_SIGB_Emprunt $emprunt) {
-    $this->empruntsAddAll([$emprunt]);
+  public function empruntsAdd(Class_WebService_SIGB_Emprunt $emprunt): self
+  {
+    return $this->empruntsAddAll([$emprunt]);
   }
 
 
-  public function reservationsAddAll(array $reservations) : self {
+  public function reservationsAddAll(array $reservations): self
+  {
     $this->_reservations = array_merge($this->_reservations, $reservations);
     $this->_nb_reservations = count($this->_reservations);
     return $this;
   }
 
 
-  public function reservationsAdd(Class_WebService_SIGB_Reservation $reservation)  : self {
-    $this->reservationsAddAll(array($reservation));
+  public function reservationsAdd(Class_WebService_SIGB_Reservation $reservation): self
+  {
+    $this->reservationsAddAll([$reservation]);
     return $this;
   }
 
 
-  public function addSendingChannel(Class_WebService_SIGB_SendingChannel $sending_channel) : self {
+  public function addSendingChannel(Class_WebService_SIGB_SendingChannel $sending_channel): self
+  {
     $this->_sending_channels[] = $sending_channel;
     return $this;
   }
 
 
-  public function getSendingChannels() : array {
+  public function getSendingChannels(): array
+  {
     return $this->_sending_channels;
   }
 
 
-  public function canChooseSendingChannel() : bool {
-    return (count($this->_sending_channels));
+  public function canChooseSendingChannel(): bool
+  {
+    return 0 < count($this->_sending_channels);
   }
 
 
-  public function setFavoriteSendingChannel(string $favorite) : self {
+  public function setFavoriteSendingChannel(string $favorite): self
+  {
     $this->_favorite_sending_channel = $favorite;
     return $this;
   }
 
 
-  public function favoriteSendingChannel() : string {
+  public function favoriteSendingChannel(): string
+  {
     return $this->_favorite_sending_channel;
   }
 
 
-  public function getReservations() : array{
+  public function getReservations(): array
+  {
     if (!empty($this->_reservations))
       return $this->_reservations;
 
@@ -416,7 +389,8 @@ class Class_WebService_SIGB_Emprunteur {
   }
 
 
-  public function getHoldsWaitingToBePulled() {
+  public function getHoldsWaitingToBePulled(): array
+  {
     if(!empty($this->_waiting_holds))
       return $this->_waiting_holds;
 
@@ -428,109 +402,101 @@ class Class_WebService_SIGB_Emprunteur {
   }
 
 
-  /**
-   * @param int $index
-   * @return Class_WebService_SIGB_Reservation
-   */
-  public function getReservationAt($index){
+  public function getReservationAt(string $index): ?Class_WebService_SIGB_Reservation
+  {
     $reservations = $this->getReservations();
-    return $reservations[$index];
+    return $reservations[$index] ?? null;
   }
 
 
-  public function getNbReservations(): int{
-    if (isset($this->_nb_reservations))
-      return $this->_nb_reservations;
-
-    return $this->_nb_reservations = count($this->getReservations());
+  public function getNbReservations(): int
+  {
+    return $this->_nb_reservations ??= count($this->getReservations());
   }
 
 
-  /**
-   * @param int $nb_reservations
-   * @return Class_WebService_SIGB_Emprunteur
-   */
-  public function setNbReservations($nb_reservations) {
+  public function setNbReservations(int $nb_reservations): self
+  {
     $this->_nb_reservations = $nb_reservations;
     return $this;
   }
 
 
-  public function getPretsEnRetard() :array {
-    return array_filter( $this->getLoans(), fn( $loan) => $loan->enRetard());
+  public function getPretsEnRetard(): array
+  {
+    return array_filter($this->getLoans(), fn( $loan) => $loan->enRetard());
   }
 
 
-  /**
-   * @return int
-   */
-  public function getNbPretsEnRetard()  :int {
-    if ($this->_nb_retards)
-      return $this->_nb_retards;
-    return count($this->getPretsEnRetard());
+  public function getNbPretsEnRetard(): int
+  {
+    return $this->_nb_retards ??= count($this->getPretsEnRetard());
   }
 
 
-  public function setLatesNumber(int $nb_retards = 0) :self {
+  public function setLatesNumber(int $nb_retards): self
+  {
     $this->_nb_retards = $nb_retards;
     return $this;
   }
 
 
-  /**
-   * @param array $items
-   */
-  public function sortByDateRetour(&$items) {
-    $keys = array();
-    foreach($items as $item) {
-      $keys []= $item->getDateRetourTimestamp();
-    }
-
+  protected function _sortByDateRetour(array $items): array
+  {
+    $keys = array_map(fn($item) => $item->getDateRetourTimestamp(), $items);
     array_multisort($keys,
                     SORT_ASC,
                     SORT_NUMERIC,
                     $items);
+    return $items;
   }
 
 
-  public function providesLoansHistory() :bool {
-    return $this->_service
+  public function providesLoansHistory(): bool
+  {
+    return isset($this->_service)
       ? $this->_service->providesLoansHistory()
       : false;
   }
 
 
-  public function providesLoansSearch() :bool {
-    return $this->_service
+  public function providesLoansSearch(): bool
+  {
+    return isset($this->_service)
       ? $this->_service->providesLoansSearch()
       : false;
   }
 
 
-  public function loansHistoryGetAll() {
+  public function loansHistoryGetAll(): array
+  {
     return $this->_loans_history;
   }
 
 
-  public function getLoansHistory() {
-    return $this->_service
+  public function getLoansHistory(): Class_WebService_SIGB_LoansHistory
+  {
+    return isset($this->_service)
       ? $this->_service->loansHistory($this, 1)
       : new Class_WebService_SIGB_LoansHistory($this, 1);
   }
 
 
-  public function addLoansHistory($loans) {
-    return $this->loansHistoryAddAll([$loans]);
+  public function addLoansHistory(Class_WebService_SIGB_Emprunt $loan): self
+  {
+    return $this->loansHistoryAddAll([$loan]);
   }
 
 
-  public function loansHistoryAddAll($loans) {
+  public function loansHistoryAddAll(array $loans): self
+  {
     $this->_loans_history = array_merge($this->_loans_history, $loans);
     return $this;
   }
 
 
-  public function hasLoansPerPage() {
+  public function hasLoansPerPage(): bool
+  {
     if(!isset($this->_service))
       return false;
 
@@ -548,7 +514,8 @@ class Class_WebService_SIGB_Emprunteur {
   }
 
 
-  protected function _filterSearchParams(array $params) :array {
+  protected function _filterSearchParams(array $params): array
+  {
     $filtered= [];
     foreach ($params as $key=>$value)
       if (in_array($key, (new Class_User_Loans())->authorizedParams()))
@@ -557,7 +524,8 @@ class Class_WebService_SIGB_Emprunteur {
   }
 
 
-  public function getLoans(array $params = []) :array {
+  public function getLoans(array $params = []): array
+  {
     if (isset($params['nocache']))
       $this->_emprunts =[];
     $params = $this->_filterSearchParams($params);
@@ -573,109 +541,109 @@ class Class_WebService_SIGB_Emprunteur {
   }
 
 
-  /**
-   * @param int $index
-   * @return Class_WebService_SIGB_Emprunt
-   */
-  public function getEmpruntAt(int $index){
+  public function getEmpruntAt(int $index): ?Class_WebService_SIGB_Emprunt{
     $emprunts = $this->getLoans();
-
-    return isset($emprunts[$index])
-      ? $emprunts[$index]
-    : new Class_Entity();
+    return $emprunts[$index] ?? null;
   }
 
 
-  public function getNbEmprunts() :int {
-    if (isset($this->_nb_emprunts))
-      return $this->_nb_emprunts;
-
-    return  count($this->getLoans());
+  public function getNbEmprunts():int {
+    return $this->_nb_emprunts ??= count($this->getLoans());
   }
 
 
-  public function setNbEmprunts(int $nb_emprunts =0) :self{
+  public function setNbEmprunts(int $nb_emprunts): self
+  {
     $this->_nb_emprunts = $nb_emprunts;
     return $this;
   }
 
 
-  /**
-   * @return string
-   */
-  public function getName() :string {
-    return $this->_name ?? '';
+  public function getName(): string
+  {
+    return $this->_name;
   }
 
 
-  /**
-   * @return string
-   */
-  public function getId() :string{
+  public function getId(): string
+  {
     return $this->_id;
   }
 
 
-  public function setEmail($email): self {
+  public function setEmail(string $email): self
+  {
     $this->_email = $email;
     return $this;
   }
 
 
-  public function getEmail() :string {
-    return $this->_email ?? '';
+  public function getEmail(): string
+  {
+    return $this->_email;
+  }
+
+
+  public function getMail(): string
+  {
+    return $this->_email;
   }
 
 
-  public function setNom( string $nom) : self {
+  public function setNom(string $nom): self
+  {
     $this->_nom = $nom;
     return $this;
   }
 
 
-  public function getNom() : string {
-    return $this->_nom ?? '';
+  public function getNom(): string
+  {
+    return $this->_nom;
   }
 
 
-  public function setPrenom( string $prenom) : self {
+  public function setPrenom(string $prenom): self
+  {
     $this->_prenom = $prenom;
     return $this;
   }
 
 
-  public function getPrenom() : string {
-    return $this->_prenom ?? '';
+  public function getPrenom(): string
+  {
+    return $this->_prenom;
   }
 
 
-  public function setLogin( string $login) : self {
+  public function setLogin(string $login): self
+  {
     $this->_login = $login;
     return $this;
   }
 
 
-  public function getLogin() : string {
-    return $this->_login ?? '';
+  public function getLogin(): string
+  {
+    return $this->_login;
   }
 
 
-  public function setPreviousLogin(string $old_login) : self {
+  public function setPreviousLogin(string $old_login): self
+  {
     $this->_old_login = $old_login;
     return $this;
   }
 
 
-  public function getPreviousLogin() :string {
-    return $this->_old_login ?? '';
+  public function getPreviousLogin():string
+  {
+    return $this->_old_login;
   }
 
 
-  /**
-   * @param string $password
-   * @return Class_WebService_SIGB_Emprunteur
-   */
-    public function setPassword($password) : self{
+  public function setPassword(string $password): self
+  {
     $this->_previous_password = (null == $this->_previous_password)
       ? $password
       : $this->_password;
@@ -685,128 +653,148 @@ class Class_WebService_SIGB_Emprunteur {
   }
 
 
-  public function setPreviousPassword($password) : self {
+  public function setPreviousPassword(string $password): self
+  {
     $this->_previous_password = $password;
     return $this;
   }
 
 
-  public function getPreviousPassword()  {
+  public function getPreviousPassword(): string
+  {
     return $this->_previous_password;
   }
 
 
-  public function getPassword() {
+  public function getPassword(): string
+  {
     return $this->_password;
   }
 
 
   /**
    * @param $date string YYYY-MM-DD format
-   * @return Class_WebService_SIGB_Emprunteur
    */
-  public function setEndDate($date) : self {
+  public function setEndDate(string $date): self
+  {
     $this->_end_date = $date;
     return $this;
   }
 
 
   /** @return string YYYY-MM-DD format */
-  public function getEndDate() : ?string{
+  public function getEndDate(): string
+  {
     return $this->_end_date;
   }
 
 
-  public function setService(?Class_WebService_SIGB_AbstractService $service) : self {
+  public function setService(Class_WebService_SIGB_AbstractService $service): self
+  {
     $this->_service = $service;
     return $this;
   }
 
 
-  public function ensureService(Class_Users $user) : self {
-    if (!$this->_service)
+  public function ensureService(Class_Users $user): self
+  {
+    if (! isset($this->_service))
       $this->_service = $user->getSIGBComm();
 
     return $this;
   }
 
 
-  public function ensureAndSave(Class_Users $user) : bool {
+  public function ensureAndSave(Class_Users $user): bool
+  {
     $this->ensureService($user);
-    if ($user->isAbonne() && $this->_service)
+    if ($user->isAbonne() && isset($this->_service))
       return $this->save($user);
 
     return $this->isNullInstance();
   }
 
 
-  public function setLibraryCode($library_code) : self {
+  public function setLibraryCode(string $library_code): self
+  {
     $this->_library_code = $library_code;
     return $this;
   }
 
 
-  public function getLibrary() : Class_CodifAnnexe {
+  public function getLibrary(): Class_CodifAnnexe
+  {
     return ($library = Class_CodifAnnexe::findFirstBy(['id_origine' => $this->_library_code]))
       ? $library
       : Class_CodifAnnexe::newInstance();
   }
 
 
-  public function getLibraryCode() {
+  public function getLibraryCode()
+  {
     return $this->_library_code;
   }
 
 
-  public function setPublicComment(string $comment) : self {
+  public function setPublicComment(string $comment): self
+  {
     $this->_public_comment = $comment;
     return $this;
   }
 
 
-  public function getPublicComment() : string {
+  public function getPublicComment(): string
+  {
     return $this->_public_comment;
   }
 
 
-  public function hasUnreadPublicComment() : bool {
+  public function hasUnreadPublicComment(): bool
+  {
     return ($this->_public_comment && !$this->_public_comment_read);
   }
 
 
-  public function setPublicCommentRead(bool $read) : self {
+  public function setPublicCommentRead(bool $read): self
+  {
     $this->_public_comment_read = $read;
     return $this;
   }
 
 
-  public function useIlsPaginatedLoans(): bool {
+  public function useIlsPaginatedLoans(): bool
+  {
     return false;
   }
 
 
-  public function getLibraryLabel() : string {
+  public function getLibraryLabel(): string
+  {
     return $this->getLibrary()->getLibelle();
   }
 
 
-  public function getIdAbon() : string {
+  public function getIdAbon(): string
+  {
     return $this->_code_barres ? $this->_code_barres : $this->getId();
   }
 
 
-  public function setIdIntBib(int $id) : self {
+  public function setIdIntBib(int $id): self
+  {
     $this->_id_int_bib = $id;
     return $this;
   }
 
 
-  public function getIdIntBib(){
+  public function getIdIntBib()
+  {
     return $this->_id_int_bib;
   }
 
 
-  public function save(?Class_Users $user = null) : bool {
+  public function save(Class_Users $user): bool
+  {
     if ( ! isset($this->_service))
       return false;
 
@@ -819,57 +807,57 @@ class Class_WebService_SIGB_Emprunteur {
   }
 
 
-    protected function _saveCache(?Class_Users $user = null): bool
-    {
-      if ($user)
-        Class_WebService_SIGB_EmprunteurCache::newInstance()->save($user, $this);
-
-      return true;
-    }
-
+  protected function _saveCache(Class_Users $user): bool
+  {
+    Class_WebService_SIGB_EmprunteurCache::newInstance()->save($user, $this);
+    return true;
+  }
 
-  public function resetPassword() {
-    if (!isset($this->_service))
-      return false;
 
-    return $this->_service->resetPassword($this);
+  public function resetPassword(): array
+  {
+    return isset($this->_service)
+      ? $this->_service->resetPassword($this)
+      : [];
   }
 
 
-  public function getUserInformationsPopupUrl(Class_Users $user) : string{
-    if (!isset($this->_service))
-      return '';
-
-    return $this->_service->getPopupUrlForUserInformations($user) ?? '';
+  public function getUserInformationsPopupUrl(Class_Users $user): string
+  {
+    return isset($this->_service)
+      ? ($this->_service->getPopupUrlForUserInformations($user) ?? '')
+      : '';
   }
 
 
-  public function beValid() : self {
+  public function beValid(): self
+  {
     $this->_valid = true;
     return $this;
   }
 
 
-  public function isValid() : bool{
+  public function isValid(): bool
+  {
     return $this->_valid;
   }
 
 
-  public function beBlocked() : self{
+  public function beBlocked(): self
+  {
     $this->_blocked = true;
     return $this;
   }
 
 
-  public function isBlocked() : bool{
+  public function isBlocked(): bool
+  {
     return $this->_blocked;
   }
 
 
-  /**
-   * @param $user Class_Users
-   */
-  public function updateUser( Class_Users $user) {
+  public function updateUser(Class_Users $user): self
+  {
     $user
       ->setIdabon($this->getIdAbon())
       ->setNom($this->getNom())
@@ -914,33 +902,38 @@ class Class_WebService_SIGB_Emprunteur {
 
     if ( Class_Users::UNDEFINED_DATA !== $this->_parental_authorization )
       $user->setParentalAuthorization($this->_parental_authorization);
+
+    return $this;
   }
 
 
-  public function updateFromUser($user) : self {
+  public function updateFromUser(Class_Users $user): self
+  {
     $this
-      ->setId($user->getIdSigb())
-      ->setNom($user->getNom())
-      ->setPrenom($user->getPrenom())
-      ->setEMail($user->getMail())
-      ->setAdresse($user->getAdresse())
-      ->setVille($user->getVille())
-      ->setCodePostal($user->getCodePostal())
-      ->setPassword($user->getPassword())
-      ->setTelephone($user->getTelephone())
-      ->setMobile($user->getMobile())
-      ->setIsContactSms($user->getIsContactSms())
-      ->setIsContactEmail($user->getIsContactMail())
-      ->setLogin($user->getLogin());
+      ->setId($user->getIdSigb() ?? '')
+      ->setNom($user->getNom() ?? '')
+      ->setPrenom($user->getPrenom() ?? '')
+      ->setLogin($user->getLogin() ?? '')
+      ->setEMail($user->getMail() ?? '')
+      ->setAdresse($user->getAdresse() ?? '')
+      ->setVille($user->getVille() ?? '')
+      ->setCodePostal($user->getCodePostal() ?? '')
+      ->setPassword($user->getPassword() ?? '')
+      ->setTelephone($user->getTelephone() ?? '')
+      ->setMobile($user->getMobile() ?? '')
+      ->setIsContactSms($user->getIsContactSms() ?? false)
+      ->setIsContactEmail($user->getIsContactMail() ?? false);
+
+
     return $this;
   }
 
 
   /**
    * @codeCoverageIgnore
-   * @return string
    */
-  public function __toString(): string{
+  public function __toString(): string
+  {
     $str = 'Emprunteur ['.$this->_id.'] '.$this->_name."\n";
 
     $str .= "  Emprunts\n";
@@ -955,59 +948,71 @@ class Class_WebService_SIGB_Emprunteur {
   }
 
 
-  public function setHolds($holds) : self {
+  public function setHolds(array $holds): self
+  {
     $this->_reservations = [];
     return $this->reservationsAddAll($holds);
   }
 
 
-  public function setMultimediaAccess(int $data) : self {
+  public function setMultimediaAccess(int $data): self
+  {
     $this->_multimedia_access = $data;
     return $this;
   }
 
 
-  public function setParentalAuthorization(int $data) : self {
+  public function setParentalAuthorization(int $data): self
+  {
     $this->_parental_authorization = $data;
     return $this;
   }
 
 
-  public function isWarehouse() : bool {
+  public function isWarehouse(): bool
+  {
     return false;
   }
 
-  public function isUniquePatron() : bool {
-    return false;
-  }
 
 
-  public function numberOfDebts() : int {
+  public function numberOfDebts(): int
+  {
     return count($this->getDebts());
   }
 
 
-  public function getDebts() : array {
+  public function getDebts(): array
+  {
     return array_filter($this->_debts, fn($debt) => (new Class_AdminVar_Payfip)->displayDebtForLogin( Class_Users::getIdentity()->getLogin(), $debt->getClientId()));
+
+  }
+
+
+  public function isUniquePatron(): bool
+  {
+    return false;
   }
 
 
-  public function getValidDebts(array $ids) : array {
+  public function getValidDebts(array $ids): array
+  {
     return
       array_values(array_filter($this->_debts,
                                 fn($debt) => in_array($debt->getId(), $ids)));
   }
 
 
-  public function addDebt(Class_WebService_SIGB_Debt $debt) : self {
+  public function addDebt(Class_WebService_SIGB_Debt $debt): self
+  {
     if (!$debt->getPaymentId())
       $this->_debts[] = $debt;
     return $this;
   }
 
 
-  public function startPayment(Class_Users $user,
-                               array $debts) : Class_Payfip_PaymentResult {
+  public function startPayment(Class_Users $user, array $debts): Class_Payfip_PaymentResult
+  {
     $total_amount = array_reduce($debts,
                                  fn($carry, $debt) => $carry + $debt->getAmount(),
                                  0);
@@ -1019,8 +1024,8 @@ class Class_WebService_SIGB_Emprunteur {
 
 
 
-  public function endPayment(Class_Users $user,
-                             Class_Payfip $payment) : Class_Payfip_PaymentResult {
+  public function endPayment(Class_Users $user, Class_Payfip $payment): Class_Payfip_PaymentResult
+  {
     return isset($this->_service)
       ? $this->_service->endPayment($user, $payment)
       : new Class_Payfip_NoSigb;
@@ -1031,9 +1036,11 @@ class Class_WebService_SIGB_Emprunteur {
 
 
 
-class Class_WebService_SIGB_EmprunteurNull extends Class_WebService_SIGB_Emprunteur {
+class Class_WebService_SIGB_EmprunteurNull extends Class_WebService_SIGB_Emprunteur
+{
 
-  public function isNullInstance() : bool {
+  public function isNullInstance(): bool
+  {
     return true;
   }
 }
diff --git a/library/Class/WebService/SIGB/Flora/Service.php b/library/Class/WebService/SIGB/Flora/Service.php
index d395ed6bf63fee3e00c62ebc28c41ff0dde2a100..5a54871cba88a0a88f3963c8d412268aba2f215a 100644
--- a/library/Class/WebService/SIGB/Flora/Service.php
+++ b/library/Class/WebService/SIGB/Flora/Service.php
@@ -145,7 +145,8 @@ class Class_WebService_SIGB_Flora_Service extends Class_WebService_SIGB_Abstract
     return true;
   }
 
-  public function suggestionsOf($user) {
+  public function suggestionsOf(Class_Users $user): array
+  {
     return [];
   }
 
diff --git a/library/Class/WebService/SIGB/Koha/CommunityService.php b/library/Class/WebService/SIGB/Koha/CommunityService.php
index 4b2904b492abf516ef85660054e17aad124b3dcd..3adc494fb8f12c81158432ce70e93112b84a7547 100644
--- a/library/Class/WebService/SIGB/Koha/CommunityService.php
+++ b/library/Class/WebService/SIGB/Koha/CommunityService.php
@@ -73,33 +73,45 @@ class Class_WebService_SIGB_Koha_CommunityService
   }
 
 
-  protected function _prepareData(Class_WebService_SIGB_Emprunteur $emprunteur)
-    : array {
-    if (empty($emprunteur->getSubscriptions()))
-      throw new Class_WebService_Exception($this->_('L\'emprunteur n\'est pas rattaché à une catégorie d\'abonnés valide.'));
-    $last_subscription = $emprunteur->getSubscriptions()->last();
-    $category_id = $last_subscription ? $last_subscription->getId(): '';
+  protected function _prepareData(Class_WebService_SIGB_Emprunteur $emprunteur): array
+  {
+    if ( ! $subscriptions = $emprunteur->getSubscriptions())
+      return [];
+
+    $last_subscription = $subscriptions->last();
+    $category_id = $last_subscription ? $last_subscription->getId() : '';
+
+    /* https://api.koha-community.org/22.11.html#tag/patrons/operation/updatePatron */
 
-    $mandatory_params =  ['surname' => $emprunteur->getNom(),
-                          'firstname' => $emprunteur->getPrenom(),
-                          'address' => $emprunteur->getAdresse(),
-                          'city' => $emprunteur->getVille(),
-                          'postal_code' => $emprunteur->getCodePostal(),
-                          'email' => $emprunteur->getEmail(),
-                          'phone' => $emprunteur->getTelephone(),
-                          'mobile' => $emprunteur->getMobile(),
-                          'library_id' => $emprunteur->getLibraryCode(),
-                          'category_id' => $category_id];
+    $required_params = array_filter(['surname' => $emprunteur->getNom(),
+                                     'library_id' => $emprunteur->getLibraryCode(),
+                                     'category_id' => $category_id]);
 
-    if ($date_of_birth = $emprunteur->getDateNaissance())
-      $mandatory_params['date_of_birth'] = $date_of_birth;
+    if ( ! $form_fields = Class_AdminVar::getChampsFicheUtilisateur())
+      return $required_params;
+
+    $map =
+      ['firstname' => 'getPrenom',
+       'address' => 'getAdresse',
+       'city' => 'getVille',
+       'postal_code' => 'getCode_postal',
+       'email' => 'getMail',
+       'mobile' => 'getTelephone'];
+
+    foreach ($map as $koha_field => $getter) {
+      $field = strtolower(str_replace('get', '', $getter));
+      if (false !== array_search($field, $form_fields, true))
+        $required_params [$koha_field] = call_user_func([$emprunteur, Storm_Inflector::camelize($getter)]);
+    }
 
-    return $mandatory_params;
+    return $required_params;
   }
 
 
   protected function _updateSigbUser(Class_WebService_SIGB_Emprunteur $emprunteur) : bool {
-    $data = $this->_prepareData($emprunteur);
+    if ( !$data = $this->_prepareData($emprunteur))
+      return false;
+
     $url = $this->_buildUrlForEndpointWithParams('patrons/' . $emprunteur->getId());
     $response = $this->getWebClient()
                      ->putRawData($url,
@@ -124,7 +136,8 @@ class Class_WebService_SIGB_Koha_CommunityService
   }
 
 
-  protected function _setPassword(Class_WebService_SIGB_Emprunteur $emprunteur) : bool {
+  protected function _setPassword(Class_WebService_SIGB_Emprunteur $emprunteur): bool
+  {
     $url = $this->_buildUrlForEndpointWithParams('patrons/' . $emprunteur->getId() . '/password');
 
     $password = $emprunteur->getPassword();
@@ -152,6 +165,8 @@ class Class_WebService_SIGB_Koha_CommunityService
       $this->addError($this->_('Erreur de mise à jour du mot de passe%s', $error));
       return false;
     }
+
+    return true;
   }
 
 
@@ -162,8 +177,9 @@ class Class_WebService_SIGB_Koha_CommunityService
       return false;
     }
 
-    if($emprunteur->getPassword() != $emprunteur->getPreviousPassword())
-      $this->_setPassword($emprunteur);
+    if((null !== $emprunteur->getPreviousPassword())
+       && $emprunteur->getPassword() != $emprunteur->getPreviousPassword())
+      return $this->_setPassword($emprunteur);
 
     return ($this->hasErrors())
       ? false
@@ -300,7 +316,8 @@ class Class_WebService_SIGB_Koha_CommunityService
   }
 
 
-  public function suggestionsOf($user) {
+  public function suggestionsOf(Class_Users $user): array
+  {
     if (!$patron_id = $user->getEmprunteurId())
       return $this->_error($this->_('Échec de l\'authentification par le webservice'));
 
diff --git a/library/Class/WebService/SIGB/Koha/Emprunteur.php b/library/Class/WebService/SIGB/Koha/Emprunteur.php
index 2429296cd4354bd26c60079ca02ac96866a134ad..0193ba01eeb0b2319297b1d50eb7921f2102e126 100644
--- a/library/Class/WebService/SIGB/Koha/Emprunteur.php
+++ b/library/Class/WebService/SIGB/Koha/Emprunteur.php
@@ -23,7 +23,8 @@
 class Class_WebService_SIGB_Koha_Emprunteur extends Class_WebService_SIGB_Emprunteur {
   const CATEGORY_KOHA = 'Koha';
 
-  public function updateUserRelations($user) {
+  public function updateUserRelations(Class_Users $user): void
+  {
     if (! $user->getId())
       return;
 
diff --git a/library/Class/WebService/SIGB/Koha/Service.php b/library/Class/WebService/SIGB/Koha/Service.php
index 313c3d51810a34b6c047ec41eb7ec4bd9e563038..e3060f0e30b55dc113b8a2a5bf875452d399ff15 100644
--- a/library/Class/WebService/SIGB/Koha/Service.php
+++ b/library/Class/WebService/SIGB/Koha/Service.php
@@ -483,7 +483,8 @@ class Class_WebService_SIGB_Koha_Service extends Class_WebService_SIGB_AbstractR
   }
 
 
-  public function suggestionsOf($user) {
+  public function suggestionsOf(Class_Users $user): array
+  {
     if (!$this->providesSuggestions())
       return parent::suggestionsOf($user);
 
diff --git a/library/Class/WebService/SIGB/KohaLegacy/RestfulService.php b/library/Class/WebService/SIGB/KohaLegacy/RestfulService.php
index 6073af550fd7484a0ea76518f0f5b348809ac682..0ececfe935d77fe776803c56eaa51f630d45176d 100644
--- a/library/Class/WebService/SIGB/KohaLegacy/RestfulService.php
+++ b/library/Class/WebService/SIGB/KohaLegacy/RestfulService.php
@@ -61,7 +61,8 @@ class Class_WebService_SIGB_KohaLegacy_RestfulService
   }
 
 
-  public function suggestionsOf($user) {
+  public function suggestionsOf(Class_Users $user):array
+  {
     if (!$patron_id = $user->getIdSigb())
       return [];
 
diff --git a/library/Class/WebService/SIGB/KohaLegacy/Service.php b/library/Class/WebService/SIGB/KohaLegacy/Service.php
index 3e0d212e0223777099ba4edbb5d921fb49842522..6c7568f2d67612e38a0665f01a3cfb00d808daa1 100644
--- a/library/Class/WebService/SIGB/KohaLegacy/Service.php
+++ b/library/Class/WebService/SIGB/KohaLegacy/Service.php
@@ -104,7 +104,8 @@ class Class_WebService_SIGB_KohaLegacy_Service extends Class_WebService_SIGB_Koh
   }
 
 
-  public function suggestionsOf($user) {
+  public function suggestionsOf(Class_Users $user): array
+  {
     if (!$this->providesSuggestions())
       return parent::suggestionsOf($user);
 
diff --git a/library/Class/WebService/SIGB/Nanook/Emprunteur.php b/library/Class/WebService/SIGB/Nanook/Emprunteur.php
index afe88f0fcbeabea1478835edb1901c28007f4aff..5857e0e29a1f9cc99b3b113228d32a1f5296e40c 100644
--- a/library/Class/WebService/SIGB/Nanook/Emprunteur.php
+++ b/library/Class/WebService/SIGB/Nanook/Emprunteur.php
@@ -21,7 +21,8 @@
 
 
 class Class_WebService_SIGB_Nanook_Emprunteur extends Class_WebService_SIGB_Emprunteur {
-  public function updateUserRelations($user) {
+  public function updateUserRelations(Class_Users $user): void
+  {
     if ($this->_mustProcessSubscription($user))
       $this->_processSubscriptions($user);
   }
diff --git a/library/Class/WebService/SIGB/Nanook/Service.php b/library/Class/WebService/SIGB/Nanook/Service.php
index fec8f78e2ec3d283e25cd90cce29920458ef73ec..af5b472c01daa9a1ee52a1dd343d2384f1efeff7 100644
--- a/library/Class/WebService/SIGB/Nanook/Service.php
+++ b/library/Class/WebService/SIGB/Nanook/Service.php
@@ -406,7 +406,8 @@ class Class_Webservice_SIGB_Nanook_Service
   }
 
 
-  public function suggestionsOf($user) {
+  public function suggestionsOf(Class_Users $user): array
+  {
     return
       $this->getSuggestionsFromilsdiPatronInfo(['patronId' => $user->getIdSigb()],
                                                Class_WebService_SIGB_Nanook_PatronInfoReader::newInstance()
diff --git a/library/Class/WebService/SIGB/Orphee/Emprunteur.php b/library/Class/WebService/SIGB/Orphee/Emprunteur.php
index 475794aedd5a2ccc1d3fc4ffabaea2ae570aa673..c21c2220ccc85867a89374beafdb317bd6d5324a 100644
--- a/library/Class/WebService/SIGB/Orphee/Emprunteur.php
+++ b/library/Class/WebService/SIGB/Orphee/Emprunteur.php
@@ -48,7 +48,8 @@ class Class_WebService_SIGB_Orphee_Emprunteur
   }
 
 
-  public function getLoans(array $params = []) :array {
+  public function getLoans(array $params = []):array
+  {
     $this->_emprunts =[];
     $params = $this->_filterSearchParams( $params);
 
@@ -99,8 +100,9 @@ class Class_WebService_SIGB_Orphee_Emprunteur
 
 
   public function getNbPretsEnRetard() : int {
-    if ($this->_nb_retards)
+    if (isset($this->_nb_retards))
       return $this->_nb_retards;
+
     if ( !$this->_service)
       return 0;
     if ($this->useIlsPaginatedLoans()){
@@ -116,7 +118,7 @@ class Class_WebService_SIGB_Orphee_Emprunteur
 
 
   public function getNbEmprunts() :int{
-    if ($this->_nb_emprunts)
+    if (isset($this->_nb_emprunts))
       return $this->_nb_emprunts;
 
     if ($this->_service)
diff --git a/library/Class/WebService/SIGB/Orphee/Service.php b/library/Class/WebService/SIGB/Orphee/Service.php
index 85ddfe9d51cab172184db118d6e16ffc8f2379ff..dbfd1f52d3e2702848b1e05a1bb02b1754673446 100644
--- a/library/Class/WebService/SIGB/Orphee/Service.php
+++ b/library/Class/WebService/SIGB/Orphee/Service.php
@@ -88,8 +88,9 @@ class Class_WebService_SIGB_Orphee_Service extends Class_WebService_SIGB_Abstrac
   }
 
 
-  public function alreadySort() : bool{
-    return true;
+  public function shouldSortLoans(): bool
+  {
+    return false;
   }
 
 
diff --git a/library/Class/WebService/SIGB/UniquePatron.php b/library/Class/WebService/SIGB/UniquePatron.php
index 4e0d46d79b7fd5f1dcb2f2cb5922e73a6852b2fa..6fc77942863191fc32088f482b9f15d326ddb28d 100644
--- a/library/Class/WebService/SIGB/UniquePatron.php
+++ b/library/Class/WebService/SIGB/UniquePatron.php
@@ -61,8 +61,9 @@ class Class_WebService_SIGB_UniquePatron extends Class_WebService_SIGB_Emprunteu
   }
 
 
-  /* should Not Update User */
-  public function updateUser( Class_Users $user) {
+  public function updateUser(Class_Users $user): self
+  {
+    return $this;
   }
 
 
diff --git a/library/Class/WebService/SimpleWebClient.php b/library/Class/WebService/SimpleWebClient.php
index 94f02eedd256a3c5d3f6fd70a1007c2d3e31892b..8f2e2be1ed3c83868a173ad72e60aa58ac05e10b 100644
--- a/library/Class/WebService/SimpleWebClient.php
+++ b/library/Class/WebService/SimpleWebClient.php
@@ -41,7 +41,8 @@ class Class_WebService_SimpleWebClient {
   }
 
 
-  protected function _setHttpOptionsForClient(array $options, Zend_Http_Client $httpClient) {
+  protected function _setHttpOptionsForClient(array $options, Zend_Http_Client $httpClient): Zend_Http_Client
+  {
     if ($enctype = $options['enctype'] ?? '')
       $httpClient->setEncType($enctype);
 
diff --git a/library/ZendAfi/Form.php b/library/ZendAfi/Form.php
index e033664afb3e38b5b616902248ef315a318335db..5ed1c9f60da2aa7b9e49901afac1690329419bba 100644
--- a/library/ZendAfi/Form.php
+++ b/library/ZendAfi/Form.php
@@ -491,4 +491,10 @@ class ZendAfi_Form extends Zend_Form {
     $options['data-level'] = static::EXPERT_LEVEL;
     return parent::addElement($element, $name, $options);
   }
+
+
+  public function isEmpty(): bool
+  {
+    return 0 === count($this->getElements());
+  }
 }
diff --git a/library/templates/Intonation/Library/View/Wrapper/User/RichContent/Messages.php b/library/templates/Intonation/Library/View/Wrapper/User/RichContent/Messages.php
index 628c9496ad769344d848371744d5229e7f4a55eb..b562afe0d54ed3445c347dba76f858d02a97c7f3 100644
--- a/library/templates/Intonation/Library/View/Wrapper/User/RichContent/Messages.php
+++ b/library/templates/Intonation/Library/View/Wrapper/User/RichContent/Messages.php
@@ -38,21 +38,18 @@ class Intonation_Library_View_Wrapper_User_RichContent_Messages extends Intonati
                           $comment)
       : $this->_('Vous n\'avez pas de message.');
 
-    if ($comment)
+    if ($comment && $this->_model->hasUnreadPublicComment())
       $this->_updateILSPatron();
 
     return $content;
   }
 
 
-  protected function _updateILSPatron() : void {
-    $patron = $this->_model->getEmprunteur();
-    $patron->setPublicCommentRead(true);
-
-    try {
-      $patron->ensureAndSave($this->_model);
-    } catch(Exception $e) {
-    }
+  protected function _updateILSPatron(): void
+  {
+    $this->_model->getEmprunteur()
+                 ->setPublicCommentRead(true)
+                 ->ensureAndSave($this->_model);
   }
 
 
diff --git a/tests/TearDown.php b/tests/TearDown.php
index 3004f90480ae41e8a1df886336e5dc91b04cdd42..4d28fa8d1943570d3b69124523c4acb2b1b9fae0 100644
--- a/tests/TearDown.php
+++ b/tests/TearDown.php
@@ -113,5 +113,8 @@ class TearDown {
 
     Class_Systeme_Report_Portal::setIp(null);
     ZendAfi_Auth::setInstance(null);
+
+    Class_HttpClientFactory::resetInstance();
+    Class_CommSigb::shouldThrowError(false);
   }
 }
diff --git a/tests/application/modules/AbstractControllerTestCase.php b/tests/application/modules/AbstractControllerTestCase.php
index f2fce49686fabda0bc5dcf39010f8121a95b5a5f..648099503350a2d16f4b48c004d5592e0b863e28 100644
--- a/tests/application/modules/AbstractControllerTestCase.php
+++ b/tests/application/modules/AbstractControllerTestCase.php
@@ -734,4 +734,28 @@ abstract class AbstractControllerTestCase extends Zend_Test_PHPUnit_ControllerTe
   public function assertBodyContains(string $value, string $message='') {
     $this->assertContains($value, $this->_response->getBody());
   }
+
+
+  public function assertXPathContentContainsFromJson(string $path, string $match, string $message = ''): void
+  {
+    $this->_assertXPathFromJson('assertXpathContentContains', $path, $match, $message);
+  }
+
+
+  public function assertNotXpathContentContainsFromJson(string $path, string $match, string $message = ''): void
+  {
+    $this->_assertXPathFromJson('assertNotXpathContentContains', $path, $match, $message);
+  }
+
+
+  protected function _assertXPathFromJson(string $method, string $path, string $match, string $message): void
+  {
+    require_once 'Zend/Test/PHPUnit/Constraint/DomQuery.php';
+    $constraint = new Zend_Test_PHPUnit_Constraint_DomQuery($path);
+    $content    = json_decode($this->response->getBody())->content;
+    $message = ($message ? json_decode($message)->content : '');
+
+    if (!$constraint->evaluate($content, $method, Class_CharSet::fromISOtoUTF8($match)))
+      $constraint->fail($path, $message);
+  }
 }
diff --git a/tests/application/modules/opac/controllers/AbonneControllerFicheWithSendingChannelTest.php b/tests/application/modules/opac/controllers/AbonneControllerFicheWithSendingChannelTest.php
index 02c0556f36b643954dbaa2f438197d76ca2c5a7b..dd7d9dfcedfc82209ad7966399fdb90da799497d 100644
--- a/tests/application/modules/opac/controllers/AbonneControllerFicheWithSendingChannelTest.php
+++ b/tests/application/modules/opac/controllers/AbonneControllerFicheWithSendingChannelTest.php
@@ -32,45 +32,45 @@ abstract class AbonneControllerFicheWithSendingChannelTestCase extends AbstractC
 
     $this->_buildTemplateProfil(['id' => 999999999999]);
 
+    Class_HttpClientFactory::forTest()
+      ->addRequestWithResponse('http://nanookService:80/service/GetPatronInfo/patronId/28407',
+                               NanookFixtures::xmlGetPatronInfoNassimoAarafa());
+
     $this->fixture(Class_IntBib::class,
                    ['id' => 1,
                     'comm_params' => ['url_serveur' => 'nanookService'],
                     'comm_sigb' => Class_IntBib::COM_NANOOK
                    ]);
 
+    $this->_nassimo =
+      $this->fixture(Class_Users::class,
+                     ['id' => 11,
+                      'login' => 456789,
+                      'idabon' => 456789,
+                      'password' => 2009,
+                      'prenom' => 'Nassimo',
+                      'nom' => 'Aarafa',
+                      'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
+                      'id_sigb' => 28407,
+                      'id_int_bib' => 1,
+                      'id_site' => 1]);
 
-    $this->_mock_web_client = $this->mock()
-                                   ->whenCalled('open_url')
-                                   ->with('http://nanookService/service/AuthenticatePatron/username/456789/password/2009')
-                                   ->answers(NanookFixtures::xmlAuthenticatePatronNassimoAarafa())
-
-                                   ->whenCalled('open_url')
-                                   ->with('http://nanookService/service/GetPatronInfo/patronId/28407')
-                                   ->answers(NanookFixtures::xmlGetPatronInfoNassimoAarafa());
-
-    $sigb_comm = Class_IntBib::find(1)->getSigbComm();
-    $sigb_comm->setWebClient($this->_mock_web_client);
+    ZendAfi_Auth::getInstance()->logUser($this->_nassimo);
+  }
 
-    $this->_nassimo = $this->fixture(Class_Users::class,
-                                     ['id' => 11,
-                                      'login' => 456789,
-                                      'idabon' => 456789,
-                                      'password' => 2009,
-                                      'prenom' => 'Nassimo',
-                                      'nom' => 'Aarafa',
-                                      'id_sigb' => 28407,
-                                      'id_int_bib' => 1,
-                                      'id_site' => 1]);
 
-    $this->_nassimo->beAbonneSIGB();
-    ZendAfi_Auth::getInstance()->logUser($this->_nassimo);
+  /** @test */
+  public function allHttpCallsShouldHaveBeenCalled() {
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->checkCalls($this);
   }
 }
 
 
 
 
-class AbonneControllerFicheWithSendingChannelTest
+class AbonneControllerFicheWithSendingChannelDispatchTest
   extends AbonneControllerFicheWithSendingChannelTestCase {
 
   public function setUp() {
@@ -136,29 +136,22 @@ class AbonneControllerFicheWithSendingChannelPostEditTest
   public function setUp() {
     parent::setUp();
 
-    Storm_Cache::beVolatile();
-
     Class_AdminVar::set('CHAMPS_FICHE_UTILISATEUR', 'nom;prenom;favoriteSendingChannel');
 
-    $this->_mock_web_client
-      ->whenCalled('postData')
-      ->with('http://nanookService/service/UpdatePatronInfo/patronId/28407',
-             ['password' => '2009',
-              'mail' => null,
-              'phoneNumber' => null,
-              'favoriteSendingChannel' => '3'])
-      ->answers('plop');
-
-
-    $this->postDispatch('/opac/abonne/modifier', ['favoriteSendingChannel' => 3]);
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addPostRequestWithResponse('http://nanookService:80/service/UpdatePatronInfo/patronId/28407',
+                                   'password=2009&mail=&phoneNumber=&favoriteSendingChannel=3',
+                                   '<?xml version="1.0" encoding="UTF-8"?>
+              <UpdatePatronInfo></UpdatePatronInfo>');
 
-    $this->_emprunteur = Class_WebService_SIGB_EmprunteurCache::newInstance()
-      ->load($this->_nassimo);
+    $this->postDispatch('/opac/abonne/modifier',
+                        ['favoriteSendingChannel' => 3]);
   }
 
 
   /** @test */
   public function nasimoFavoriteSendingChannelShouldBeUpdatedTo3() {
-    $this->assertEquals(3, $this->_emprunteur->favoriteSendingChannel());
+    $this->assertEquals(3, Class_Users::find(11)->getEmprunteur()->favoriteSendingChannel());
   }
 }
diff --git a/tests/application/modules/opac/controllers/AbonneControllerPublicCommentTest.php b/tests/application/modules/opac/controllers/AbonneControllerPublicCommentTest.php
index 21e9815a2d4942e6bf5f477bf5e61e4b30c30b10..84cd65e178607a808e667797d27e0973b31f633d 100644
--- a/tests/application/modules/opac/controllers/AbonneControllerPublicCommentTest.php
+++ b/tests/application/modules/opac/controllers/AbonneControllerPublicCommentTest.php
@@ -23,36 +23,40 @@ include_once 'tests/fixtures/NanookFixtures.php';
 
 abstract class AbonneControllerPublicCommentTestCase extends AbstractControllerTestCase {
 
-  const BASE_URL = 'http://localhost/afi_Nanook/ilsdi/';
-
-  protected $_emprunteur;
-
   public function setUp() {
     parent::setUp();
 
     $this->_buildTemplateProfil(['id' => 7373]);
 
-    $library = $this->fixture(Class_IntBib::class,
-                              ['id' => 1,
-                               'comm_sigb' => Class_IntBib::COM_NANOOK,
-                               'comm_params' => ['url_serveur' => static::BASE_URL]]);
+    Class_HttpClientFactory::forTest();
+
+    $this->fixture(Class_IntBib::class,
+                   ['id' => 1,
+                    'comm_sigb' => Class_IntBib::COM_NANOOK,
+                    'comm_params' => ['url_serveur' => 'https://localhost/afi_Nanook/ilsdi/']]);
+
+    $valentin =
+      $this->fixture(Class_Users::class,
+                     ['id' => 12,
+                      'prenom' => 'Valentin',
+                      'nom' => 'Acloque',
+                      'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
+                      'id_site' => 1,
+                      'idabon' => '45789',
+                      'id_sigb' => '476',
+                      'login' => 'LEC000476',
+                      'password' => '2006',
+                      'id_int_bib' => 1]);
 
+    ZendAfi_Auth::getInstance()->logUser($valentin);
+  }
 
-    $valentin = $this->fixture(Class_Users::class, ['id' => 12,
-                                                    'prenom' => 'Valentin',
-                                                    'nom' => 'Acloque',
-                                                    'login' => 'LEC000476',
-                                                    'password' => '2006',
-                                                    'int_bib' => $library]);
-
-    $this->_emprunteur = (new Class_WebService_SIGB_Emprunteur('23', 'Acloque'))
-      ->setPublicComment('Ceci est un message')
-      ->setPublicCommentRead(false);
 
-    $valentin->setFicheSigb(['type_comm' => Class_IntBib::COM_NANOOK,
-                             'fiche' => $this->_emprunteur,
-                             'int_bib' => $library]);
-    ZendAfi_Auth::getInstance()->logUser($valentin);
+  /** @test */
+  public function allHttpCallsShouldHaveBeenCalled() {
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->checkCalls($this);
   }
 }
 
@@ -63,6 +67,12 @@ class AbonneControllerPublicCommentInformationsTest extends AbonneControllerPubl
   public function setUp() {
     parent::setUp();
 
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+
+      ->addRequestWithResponse('https://localhost:443/afi_Nanook/ilsdi/service/GetPatronInfo/patronId/476',
+                               NanookFixtures::xmlGetPatronInfoValentinAcloque());
+
     $this->dispatch('/opac/abonne/informations');
   }
 
@@ -74,13 +84,19 @@ class AbonneControllerPublicCommentInformationsTest extends AbonneControllerPubl
 }
 
 
+
+
 class AbonneControllerPublicCommentReadInformationsTest
   extends AbonneControllerPublicCommentTestCase {
 
   public function setUp() {
     parent::setUp();
 
-    $this->_emprunteur->setPublicCommentRead(true);
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+
+      ->addRequestWithResponse('https://localhost:443/afi_Nanook/ilsdi/service/GetPatronInfo/patronId/476',
+                               NanookFixtures::xmlGetPatronInfoValentinAcloqueRead());
 
     $this->dispatch('/opac/abonne/informations');
   }
@@ -95,14 +111,17 @@ class AbonneControllerPublicCommentReadInformationsTest
 
 
 
-class AbonneControllerNoPublicCommentInformationsTest
+class AbonneControllerPublicCommentNoInformationsTest
   extends AbonneControllerPublicCommentTestCase {
 
   public function setUp() {
     parent::setUp();
 
-    $this->_emprunteur->setPublicCommentRead(true);
-    $this->_emprunteur->setPublicComment('');
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+
+      ->addRequestWithResponse('https://localhost:443/afi_Nanook/ilsdi/service/GetPatronInfo/patronId/476',
+                               NanookFixtures::xmlGetPatronInfoValentinAcloqueWithoutMessage());
 
     $this->dispatch('/opac/abonne/informations');
   }
@@ -117,14 +136,17 @@ class AbonneControllerNoPublicCommentInformationsTest
 
 
 
-class AbonneControllerNoPublicCommentViewTest
+class AbonneControllerPublicCommentNoViewTest
   extends AbonneControllerPublicCommentTestCase {
 
   public function setUp() {
     parent::setUp();
 
-    $this->_emprunteur->setPublicCommentRead(true);
-    $this->_emprunteur->setPublicComment('');
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+
+      ->addRequestWithResponse('https://localhost:443/afi_Nanook/ilsdi/service/GetPatronInfo/patronId/476',
+                               NanookFixtures::xmlGetPatronInfoValentinAcloqueWithoutMessage());
 
     $this->dispatch('/opac/abonne/messages');
   }
@@ -139,14 +161,22 @@ class AbonneControllerNoPublicCommentViewTest
 
 
 
-class AbonneControllerUnreadPublicCommentViewTest
+class AbonneControllerPublicCommentUnreadViewTest
   extends AbonneControllerPublicCommentTestCase {
 
   public function setUp() {
     parent::setUp();
 
-    $this->_emprunteur->setPublicCommentRead(false);
-    $this->_emprunteur->setPublicComment('Vous avez oublié votre chien');
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+
+      ->addRequestWithResponse('https://localhost:443/afi_Nanook/ilsdi/service/GetPatronInfo/patronId/476',
+                               NanookFixtures::xmlGetPatronInfoValentinAcloque())
+
+      ->addPostRequestWithResponse('https://localhost:443/afi_Nanook/ilsdi/service/UpdatePatronInfo/patronId/12478',
+                                   'password=2006&mail=test%40orange.fr&phoneNumber=06+12+34+56+78&publicCommentRead=1',
+                                   '<?xml version="1.0" encoding="UTF-8"?>
+              <UpdatePatronInfo></UpdatePatronInfo>');
 
     $this->dispatch('/opac/abonne/messages');
   }
@@ -157,19 +187,29 @@ class AbonneControllerUnreadPublicCommentViewTest
     $this->assertXPathContentContains('//div[contains(@class, "ils_unread_message_notify")]',
                                       'Vous avez oublié votre chien');
   }
+
+
+  /** @test */
+  public function valentinShouldHaveReadPulicComment()
+  {
+    $this->assertFalse(Class_Users::find(12)->getEmprunteur()->hasUnreadPublicComment());
+  }
 }
 
 
 
 
-class AbonneControllerReadPublicCommentViewTest
+class AbonneControllerPublicCommentReadViewTest
   extends AbonneControllerPublicCommentTestCase {
 
   public function setUp() {
     parent::setUp();
 
-    $this->_emprunteur->setPublicCommentRead(true);
-    $this->_emprunteur->setPublicComment('Vous avez oublié votre chien');
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+
+      ->addRequestWithResponse('https://localhost:443/afi_Nanook/ilsdi/service/GetPatronInfo/patronId/476',
+                               NanookFixtures::xmlGetPatronInfoValentinAcloqueRead());
 
     $this->dispatch('/opac/abonne/messages');
   }
@@ -180,4 +220,11 @@ class AbonneControllerReadPublicCommentViewTest
     $this->assertXPathContentContains('//div[contains(@class, "ils_read_message_notify")]',
                                       'Vous avez oublié votre chien');
   }
+
+
+  /** @test */
+  public function valentinShouldHaveReadPulicComment()
+  {
+    $this->assertFalse(Class_Users::find(12)->getEmprunteur()->hasUnreadPublicComment());
+  }
 }
diff --git a/tests/fixtures/GetPatronInfoValentinAcloqueRead.xml b/tests/fixtures/GetPatronInfoValentinAcloqueRead.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1c7ce9540009614d40eaf91d48ec9d4dddafcc25
--- /dev/null
+++ b/tests/fixtures/GetPatronInfoValentinAcloqueRead.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<GetPatronInfo>
+  <patronId>12478</patronId>
+  <siteId>19</siteId>
+  <barcode>LEC000476</barcode>
+  <cardType>Individuel</cardType>
+  <cardStatus>Carte active</cardStatus>
+  <notes>Carte oubliéeAbonné majeur - Lien famille rompu - Vérifier les coordonnées. </notes>
+  <newsletter>0</newsletter>
+  <alertPreviousLoanOnDocument>1</alertPreviousLoanOnDocument>
+  <noAnonymization>0</noAnonymization>
+  <loanForbidden>0</loanForbidden>
+  <holdForbidden>0</holdForbidden>
+  <parentalAuthorization>1</parentalAuthorization>
+  <multimediaAccess>0</multimediaAccess>
+  <publicComment>Vous avez oublié votre chien à la médiathèque. Merci de venir le récupérer très rapidement.</publicComment>
+  <publicCommentRead>1</publicCommentRead>
+  <lastName>ACLOQUE</lastName>
+  <firstName>Valentin</firstName>
+  <displayOrder>1</displayOrder>
+  <birthDate>2006-02-16</birthDate>
+  <phoneNumber>06 12 34 56 78</phoneNumber>
+  <mail>test@orange.fr</mail>
+  <endDate>2024-05-29</endDate>
+  <town>CERIZAY</town>
+  <zipcode>79140</zipcode>
+  <address>8 , rue des caillères</address>
+</GetPatronInfo>
diff --git a/tests/fixtures/GetPatronInfoValentinAcloqueWithoutMessage.xml b/tests/fixtures/GetPatronInfoValentinAcloqueWithoutMessage.xml
new file mode 100644
index 0000000000000000000000000000000000000000..70f2d818c7fc8d1348920818ed41868d29de9e96
--- /dev/null
+++ b/tests/fixtures/GetPatronInfoValentinAcloqueWithoutMessage.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<GetPatronInfo>
+  <patronId>12478</patronId>
+  <siteId>19</siteId>
+  <barcode>LEC000476</barcode>
+  <cardType>Individuel</cardType>
+  <cardStatus>Carte active</cardStatus>
+  <notes>Carte oubliéeAbonné majeur - Lien famille rompu - Vérifier les coordonnées. </notes>
+  <newsletter>0</newsletter>
+  <alertPreviousLoanOnDocument>1</alertPreviousLoanOnDocument>
+  <noAnonymization>0</noAnonymization>
+  <loanForbidden>0</loanForbidden>
+  <holdForbidden>0</holdForbidden>
+  <parentalAuthorization>1</parentalAuthorization>
+  <multimediaAccess>0</multimediaAccess>
+  <publicComment></publicComment>
+  <publicCommentRead>1</publicCommentRead>
+  <lastName>ACLOQUE</lastName>
+  <firstName>Valentin</firstName>
+  <displayOrder>1</displayOrder>
+  <birthDate>2006-02-16</birthDate>
+  <phoneNumber>06 12 34 56 78</phoneNumber>
+  <mail>test@orange.fr</mail>
+  <endDate>2024-05-29</endDate>
+  <town>CERIZAY</town>
+  <zipcode>79140</zipcode>
+  <address>8 , rue des caillères</address>
+</GetPatronInfo>
diff --git a/tests/fixtures/NanookFixtures.php b/tests/fixtures/NanookFixtures.php
index 6a25f57619bed60e9e6e59edad8d85f9b278655c..b7a601467fe2c25e33e534eb122b02de4917f8de 100644
--- a/tests/fixtures/NanookFixtures.php
+++ b/tests/fixtures/NanookFixtures.php
@@ -498,6 +498,17 @@ class NanookFixtures {
     return file_get_contents(dirname(__FILE__) . '/GetPatronInfoValentinAcloque.xml');
   }
 
+
+  public static function xmlGetPatronInfoValentinAcloqueRead() {
+    return file_get_contents(dirname(__FILE__) . '/GetPatronInfoValentinAcloqueRead.xml');
+  }
+
+
+  public static function xmlGetPatronInfoValentinAcloqueWithoutMessage() {
+    return file_get_contents(dirname(__FILE__) . '/GetPatronInfoValentinAcloqueWithoutMessage.xml');
+  }
+
+
   /**
    * @return string
    */
diff --git a/tests/library/Class/HttpClientForTest.php b/tests/library/Class/HttpClientForTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..8b33b123d18bfc785497ca92744398410bc1f531
--- /dev/null
+++ b/tests/library/Class/HttpClientForTest.php
@@ -0,0 +1,153 @@
+<?php
+/**
+ * Copyright (c) 2012-2024, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class HttpClientForTest extends Zend_Http_Client
+{
+
+  protected array $_expected_calls = [];
+  protected array $_expected_calls_eated = [];
+
+
+  public function request($method = null)
+  {
+    $response = parent::request($method);
+
+    foreach ($this->_expected_calls as $request_url => $data)
+      if ( ! ($match = $this->_match($request_url, $data))->isError())
+        return $match;
+
+    if ( $response->isError())
+      throw new Exception("\n\nREQUEST send by BOKEH to\n"
+                          . "URL : " . $this->uri->getUri()
+                          . "\n\nREQUEST Headers : \n"
+                          . $this->last_request
+                          . "\nRESPONSE Headers : "
+                          . $response->getHeadersAsString());
+
+    return $response;
+  }
+
+
+  protected function _match(string $url, array $data): Zend_Http_Response
+  {
+    if ( ($data['PUT'] ?? '') || ($data['POST'] ?? ''))
+      return $this->_matchWithParams($url, $data['POST'] ?? $data['PUT'] ?? '', $data['RESPONSE'] ?? '', $data['HEADERS'] ?? []);
+
+    if (  $url != $this->uri->getUri())
+      return new Zend_Http_Response(500, []);
+
+    return $this->_matchHeaders($url, $data['HEADERS'], $data['RESPONSE'] ?? '');
+  }
+
+
+  protected function _matchHeaders(string $url, array $headers, string $response): Zend_Http_Response
+  {
+    if ( ! $headers)
+      return  $this->_successResponse($url, $response);
+
+    foreach ( $headers as $key => $value)
+      if ( ! $this->_matchHeader($key, $value))
+        return new Zend_Http_Response(500, []);
+
+    return  $this->_successResponse($url, $response);
+  }
+
+
+  protected function _matchHeader(string $key, string $value): bool
+  {
+    return false !== strpos($this->last_request, sprintf('%s: %s',
+                                                         $key,
+                                                         $value));
+  }
+
+
+  protected function _matchWithParams(string $url,
+                                      string $params,
+                                      string $response,
+                                      array $headers): Zend_Http_Response
+  {
+    if ( $url != $this->uri->getUri())
+      return new Zend_Http_Response(500, []);
+
+    if ( $this->_matchHeaders($url, $headers, $response)->isError())
+      return new Zend_Http_Response(500, []);
+
+    if ($this->raw_post_data && $params ===  $this->raw_post_data)
+      return $this->_successResponse($url, $response);
+
+    if ( $this->paramsPost && $params ==  http_build_query($this->paramsPost))
+      return $this->_successResponse($url, $response);
+
+    return new Zend_Http_Response(500, []);
+  }
+
+
+  protected function _successResponse(string $url, string $data): Zend_Http_Response
+  {
+    unset($this->_expected_calls_eated[$url]);
+    return new Zend_Http_Response(200, [], $data);
+  }
+
+
+  public function isForTesting(): bool
+  {
+    return true;
+  }
+
+
+  public function addRequestWithResponse(string $request, string $response, array $headers = []): self
+  {
+    $this->_expected_calls [$request] = $this->_expected_calls_eated [$request] = ['RESPONSE' => $response,
+                                                                                   'HEADERS' => $headers];
+    return $this;
+  }
+
+
+  public function addPutRequestWithResponse(string $request,
+                                            string $params,
+                                            string $response,
+                                            array $headers = []): self
+  {
+    $this->_expected_calls [$request] = $this->_expected_calls_eated [$request] = ['PUT' => $params,
+                                                                                   'RESPONSE' => $response,
+                                                                                   'HEADERS' => $headers];
+    return $this;
+  }
+
+
+  public function addPostRequestWithResponse(string $request,
+                                             string $params,
+                                             string $response,
+                                             array $headers = []): self
+  {
+    $this->_expected_calls [$request] = $this->_expected_calls_eated [$request] = ['POST' => $params,
+                                                                                   'RESPONSE' => $response,
+                                                                                   'HEADERS' => $headers];
+    return $this;
+  }
+
+
+  public function checkCalls(PHPUnit_Framework_TestCase $test): void
+  {
+    $test->assertEquals([], $this->_expected_calls_eated);
+  }
+}
diff --git a/tests/library/Class/Testing/WebService/SIGB/Emprunteur.php b/tests/library/Class/Testing/WebService/SIGB/Emprunteur.php
index 618c96d340cddde5d63057c2b35a43e18ed65675..b55b48f92cc1dec83dec424dc1acfff813e0410b 100644
--- a/tests/library/Class/Testing/WebService/SIGB/Emprunteur.php
+++ b/tests/library/Class/Testing/WebService/SIGB/Emprunteur.php
@@ -22,7 +22,8 @@
 
 class Class_Testing_WebService_SIGB_Emprunteur extends Class_WebService_SIGB_Emprunteur {
   use Trait_Mock;
-  public function getLoans(array $params=[] ) : array{
+  public function getLoans(array $params=[] ): array
+  {
     return $this->getMockMethod('getLoans', [$params])->do() ?? [] ;
   }
 
diff --git a/tests/library/Class/WebService/SIGB/KohaCommunityTest.php b/tests/library/Class/WebService/SIGB/KohaCommunityTest.php
index a985a37a3f4bad8668bddc57a753d368de7da44f..fd92e3f13bcf2b342bec0a384a69093348eba3d6 100644
--- a/tests/library/Class/WebService/SIGB/KohaCommunityTest.php
+++ b/tests/library/Class/WebService/SIGB/KohaCommunityTest.php
@@ -21,160 +21,212 @@
 
 require_once 'tests/fixtures/KohaFixtures.php';
 
-abstract class KohaCommunityTestCase extends ModelTestCase {
-  const BASE_URL = 'http://cat-aficg55.biblibre.com/api/v1/';
-
-  protected
-    $_storm_default_to_volatile = true,
-    $mock_web_client,
-    $service,
-    $user,
-    $borrower;
+abstract class KohaCommunityTestCase extends AbstractControllerTestCase {
 
   public function setUp() {
     parent::setUp();
 
-    Class_AdminVar::set('KOHA_MULTI_SITES', '');
+    $this->_buildTemplateProfil(['id' => 8]);
+
+    Class_WebService_SIGB_Koha_CommunityService::setTimeSource(new TimeSourceForTest('2024-04-03'));
+
+    $this->fixture(Class_IntBib::class,
+                   ['id' => 8,
+                    'comm_sigb' => Class_IntBib::COM_KOHA,
+                    'id_bib' => 8,
+                    'comm_params' =>
+                    serialize(['url_serveur' => 'https://koha-community/ilsdi.pl',
+                               'api_user' => 'koha_admin',
+                               'api_pass' => 'k0h@_P455'])]);
+
+    $user =
+      $this->fixture(Class_Users::class,
+                     ['id' => 34,
+                      'login' => 'dupont',
+                      'password' => 'arcadia',
+                      'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
+                      'nom' => 'Super bien',
+                      'prenom' => 'Olivier',
+                      'mail' => 'old@mail.org',
+                      'id_sigb' => '96138',
+                      'idabon' => '11111111',
+                      'id_site' => 8,
+                      'id_int_bib' => 8]);
+
+    ZendAfi_Auth::getInstance()->logUser($user);
+
+    Class_HttpClientFactory::forTest();
+  }
 
-    $this->mock_web_client = $this->mock();
 
-    $this->fixture(Class_Profil::class, ['id' => 1])
-         ->beCurrentProfil();
+  /** @test */
+  public function allHttpCallsShouldHaveBeenCalled() {
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->checkCalls($this);
+  }
+}
+
 
-    $this->user = $this->fixture(Class_Users::class,
-                                 ['id' => 34,
-                                  'login' => 'harlock',
-                                  'password' => 'arcadia',
-                                  'id_sigb' => '32007',
-                                  'idabon' => 'AO989IE']);
 
-    $istres = $this->fixture(Class_CodifAnnexe::class,
-                             ['id' => 15,
-                              'libelle' => 'Istres',
-                              'id_origine' => 'IST']);
 
-    $this->borrower = new Class_WebService_SIGB_Emprunteur('32007', 'harlock');
-    $this->borrower->setLibraryCode('IST');
-    $this->user->setFicheSIGB(['type_comm' => 0,
-                               'fiche' => $this->borrower]);
+abstract class KohaCommunityChangePasswordTestCase extends KohaCommunityTestCase
+{
 
-    $logger = $this->mock()
-                   ->whenCalled('log')->answers(true)
+  public function setUp() {
+    parent::setUp();
 
-                   ->whenCalled('logError')
-                   ->willDo(function($url, $message) {
-                     throw new RuntimeException($url . ' :: ' . $message);
-                   });
-    Class_WebService_SIGB_AbstractService::setLogger($logger);
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoDupont());
+  }
+}
+
+
+
+
+class KohaCommunityChangePasswordSuccesTest extends KohaCommunityChangePasswordTestCase
+{
+
+  public function setUp()
+  {
+    parent::setUp();
 
-    $params = ['url_serveur' => 'http://cat-aficg55.biblibre.com/cgi-bin/koha/ilsdi.pl',
-               'api_user' => 'koha_admin',
-               'api_pass' => 'k0h@_P455'];
-    $this->service = Class_WebService_SIGB_Koha::getService($params);
-    $this->service->setWebClient($this->mock_web_client);
-    $this->borrower->setService($this->service);
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addPostRequestWithResponse('https://koha-community:443/ilsdi.pl/patrons/96138/password',
+                                   '{"password":"newsecret","password_2":"newsecret"}',
+                                   KohaCommunityFixtures::successUpdateUser(),
+                                   ['Authorization' =>'Basic a29oYV9hZG1pbjprMGhAX1A0NTU=']);
+
+    $this->postDispatch('/abonne/changer-mon-mot-de-passe',
+                        ['current_password' => 'arcadia',
+                         'new_password' => 'newsecret',
+                         'confirm_new_password' => 'newsecret']);
   }
 
 
-  public function tearDown() {
-    Class_WebService_SIGB_AbstractService::setLogger(null);
-    parent::tearDown();
+  /** @test */
+  public function dupondPasswordShourdBeNewSecret()
+  {
+    $this->assertEquals('newsecret', Class_Users::find(34)->getPassword());
   }
 }
 
 
 
 
-class KohaCommunityServiceChangePasswordWSTest extends KohaCommunityTestCase {
-  public function setUp() {
+class KohaCommunityChangePasswordEmptyJsonSuccesTest extends KohaCommunityChangePasswordTestCase
+{
+
+  public function setUp()
+  {
     parent::setUp();
-    $this->borrower = new Class_WebService_SIGB_Emprunteur('john', 'john');
-    $this->borrower->setService($this->service);
-
-    $this->user = $this->fixture(Class_Users::class,
-                                 ['id' => 10,
-                                  'login' => 'john',
-                                  'password' => '1989',
-                                  'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
-                                  'idabon' => 'john',
-                                  'id_sigb' => '96138',
-                                  'id_site' => 3]);
-    $this->user->setFicheSIGB(['fiche' => $this->borrower]);
-    $this->borrower->updateFromUser($this->user);
-    $this->borrower->subscriptionAdd('B','B');
-  }
-
-
-  public function setParamsAndResponse($password, $response_body) {
-
-    $this->mock_web_client
-      ->whenCalled('putRawData')
-      ->answers(KohaCommunityFixtures::successUpdateUser())
-      ->whenCalled('postRawData')
-      ->with(self::BASE_URL . 'patrons/96138/password',
-             json_encode(['password' => $password, 'password_2' => $password]),
-             Class_WebService_SIGB_Koha_CommunityService::JSON_ENCODED,
-             ['auth'=> ['user' => 'koha_admin', 'password' => 'k0h@_P455']])
-      ->answers($response_body)
-      ->whenCalled('open_url')
-      ->with('http://cat-aficg55.biblibre.com/cgi-bin/koha/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1')
-      ->answers(KohaFixtures::xmlGetPatronInfoDupont());
-    $this->borrower->setService($this->service);
-
-    $this->user->setPassword($password);
-    $this->borrower->updateFromUser($this->user);
-    Class_Users::clearCache();
-    if ($this->borrower->save())
-      $this->user->save();
+
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addPostRequestWithResponse('https://koha-community:443/ilsdi.pl/patrons/96138/password',
+                                   '{"password":"newsecret","password_2":"newsecret"}',
+                                   json_encode(''));
+
+    $this->postDispatch('/abonne/changer-mon-mot-de-passe',
+                        ['current_password' => 'arcadia',
+                         'new_password' => 'newsecret',
+                         'confirm_new_password' => 'newsecret']);
   }
 
 
   /** @test */
-  public function userValidPasswordShouldBeUpdated() {
-    $response = '';
-    $this->setParamsAndResponse('7estIng!',$response);
+  public function dupondPasswordShouldBeNewSecret()
+  {
+    $this->assertEquals('newsecret', Class_Users::find(34)->getPassword());
+  }
+}
+
+
+
+
+class KohaCommunityChangePasswordTooWeakTest extends KohaCommunityChangePasswordTestCase
+{
+
+  public function setUp()
+  {
+    parent::setUp();
 
-    $this->assertEmpty($this->borrower->getErrors());
-    $this->assertEquals('7estIng!',Class_Users::find(10)->getPassword());
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
 
-    $this->assertTrue($this->mock_web_client->methodHasBeenCalled('postRawData'));
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=0&show_loans=1&show_holds=0',
+                               KohaFixtures::xmlGetPatronInfoDupont())
+
+      ->addPostRequestWithResponse('https://koha-community:443/ilsdi.pl/patrons/96138/password',
+                                   '{"password":"newsecret","password_2":"newsecret"}',
+                                   json_encode(['error' => '[Passwords is too weak]']));
+
+    $this->postDispatch('/abonne/changer-mon-mot-de-passe',
+                        ['current_password' => 'arcadia',
+                         'new_password' => 'newsecret',
+                         'confirm_new_password' => 'newsecret']);
   }
 
 
   /** @test */
-  public function userShortPasswordShouldNotBeUpdatedAndTriggerException(){
-    $response = json_encode(['error' => 'Password length (4) is shorter than required (5)']);
-    $this->setParamsAndResponse('test', $response);
-    $this->assertEquals(['Erreur de mise à jour du mot de passe : Mot de passe trop court'], $this->borrower->getErrors());
-    $this->assertEquals('1989',Class_Users::find(10)->getPassword());
+  public function dupondPasswordShouldBeArcadia()
+  {
+    Class_Users::clearCache();
+    $this->assertEquals('arcadia', Class_Users::find(34)->getPassword());
   }
 
 
   /** @test */
-  public function userWeakPasswordShouldNotBeUpdatedAndTriggerException() {
-    $response = json_encode(['error' => '[Passwords is too weak]']);
-    $this->setParamsAndResponse('testing', $response);
-    $this->assertEquals(['Erreur de mise à jour du mot de passe : Mot de passe trop faible'], $this->borrower->getErrors());
+  public function formShouldContainsTooWeakPasswordMessage()
+  {
+    $this->assertXPathContentContains('//ul[@class="errors"]/li',
+                                      'Erreur de mise à jour du mot de passe : Mot de passe trop faible');
+  }
+}
+
+
+
+
+class KohaCommunityChangePasswordTooShortTest extends KohaCommunityChangePasswordTestCase
+{
+
+  public function setUp()
+  {
+    parent::setUp();
+
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=0&show_loans=1&show_holds=0',
+                               KohaFixtures::xmlGetPatronInfoDupont())
+
+      ->addPostRequestWithResponse('https://koha-community:443/ilsdi.pl/patrons/96138/password',
+                                   '{"password":"four","password_2":"four"}',
+                                   json_encode(['error' => '[Password length (4) is shorter than required (5)]']));
 
-    $this->assertEquals('1989',Class_Users::find(10)->getPassword());
+    $this->postDispatch('/abonne/changer-mon-mot-de-passe',
+                        ['current_password' => 'arcadia',
+                         'new_password' => 'four',
+                         'confirm_new_password' => 'four']);
   }
 
 
   /** @test */
-  public function withNotJsonNorEmptyReplyPasswordShouldNotBeUpdatedAndTriggerException() {
-    $response = 'Jibberish&MeaninglessBrokenReply';
-
-    $this->setParamsAndResponse('testing', $response);
-    $this->assertEquals(['saveEmprunteur() s\'attends à une réponse au format JSON'], $this->borrower->getErrors());
-    $this->assertEquals('1989', Class_Users::find(10)->getPassword());
+  public function dupondPasswordShouldBeArcadia()
+  {
+    Class_Users::clearCache();
+    $this->assertEquals('arcadia', Class_Users::find(34)->getPassword());
   }
 
 
   /** @test */
-  public function kohaShouldHaveBeenCalled() {
-    $response = '';
-    $this->setParamsAndResponse('7estIng!', $response);
-    $this->assertTrue($this->mock_web_client->methodHasBeenCalled('postRawData'));
+  public function formShouldContainsTooWeakPasswordMessage()
+  {
+    $this->assertXPathContentContains('//ul[@class="errors"]/li',
+                                      'Erreur de mise à jour du mot de passe : Mot de passe trop court');
   }
 }
 
@@ -182,64 +234,47 @@ class KohaCommunityServiceChangePasswordWSTest extends KohaCommunityTestCase {
 
 
 class KohaCommunitySuggestionsOfUserTest extends KohaCommunityTestCase {
-  protected
-    $borrower,
-    $user;
+
 
   public function setUp() {
     parent::setUp();
-    $this->borrower = new Class_WebService_SIGB_Emprunteur('32007', 'john');
-    $this->borrower->setService($this->service);
 
-    $this->fixture(Class_CodifAnnexe::class,
-                   ['id' => 15,
-                    'libelle' => 'Istres',
-                    'id_origine' => 'IST']);
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoDupont())
+
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl/suggestions?q=%7B%22suggested_by%22%3A%2296138%22%7D',
+                               file_get_contents(__DIR__ . '/../../../../fixtures/koha_suggestions_32007.json'))
 
-    $this->user = $this->fixture(Class_Users::class,
-                                 ['id' => 10,
-                                  'login' => 'john',
-                                  'password' => '1989',
-                                  'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
-                                  'idabon' => 'john',
-                                  'id_sigb' => 'john',
-                                  'id_site' => 3]);
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=0&show_loans=1&show_holds=0',
+                               KohaFixtures::xmlGetPatronInfoDupont());
 
-    $this->user->setFicheSIGB(['fiche' => $this->borrower]);
-    $this->service->setWebClient($this->mock_web_client);
-    KohaCommunityFixtures::mockWebClientForSuggestionsOf(static::BASE_URL,$this->mock_web_client);
-    $this->suggestions = $this->service->suggestionsOf($this->user);
+
+
+    $this->dispatch('abonne/suggestions');
   }
 
 
   /** @test */
   public function shouldHaveThreeSuggestions() {
-    $this->assertEquals(3, count($this->suggestions));
+    $this->assertXPathCount('//div[@class="rich_content jumbotron_rich_content user_suggestions wrapper_user_suggestions wrapper_active col-12 border-bottom border-primary mb-3 pb-3"]//div[@class="collection_truncate_list col-12"]/div[@class="list-group bg-transparent no_border"]/div[@class="list-group-item bg-transparent px-0 mb-3"]', 3);
   }
 
 
-  public function datas() {
-    return [[0,
-             'En piste ! : Créations en couture pour petits et grands Enfant',
-             'Laëtitia Gheno',
-             '',
-             'Istres',
+  public function suggestions() {
+    return [['En piste ! : Créations en couture pour petits et grands Enfant',
              '2013-11-14',
              'D.L.',
              'Disponible'],
-            [1,
-             'Un été couture made in France',
-             'Géraldine Debeauvais',
-             '',
-             'Istres',
+
+            ['Un été couture made in France',
              '2013-12-12',
              '',
              'Disponible'],
-            [2,
-             'Couture pour enfants branchés ! Vêtements et accessoires de la naissance à 2 ans et plus',
-             'Marie-Eve Dollat',
-             '2012',
-             'Istres',
+
+            ['Couture pour enfants branchés ! Vêtements et accessoires de la naissance à 2 ans et plus',
              '2014-04-16',
              '',
              'Rejetée (Sujet largement couvert dans la collection)']];
@@ -248,222 +283,232 @@ class KohaCommunitySuggestionsOfUserTest extends KohaCommunityTestCase {
 
   /**
    * @test
-   * @dataProvider datas
+   * @dataProvider suggestions
    */
-  public function datasShouldBeImported($pos, $title, $author, $publication_year,
-                                        $library, $date, $note, $status) {
-    $this->assertEquals($title, $this->suggestions[$pos]->getTitle());
-    $this->assertEquals($author, $this->suggestions[$pos]->getAuthor());
-    $this->assertEquals($publication_year,
-                        $this->suggestions[$pos]->getPublicationYear());
-    $this->assertEquals($library, $this->suggestions[$pos]->getLibrary());
-    $this->assertEquals($date, $this->suggestions[$pos]->getDate());
-    $this->assertEquals($note, $this->suggestions[$pos]->getNote());
-    $this->assertEquals($status, $this->suggestions[$pos]->getStatus());
+  public function pageShouldContainsSuggestionsTitle(string $title,
+                                                     string $date,
+                                                     string $note,
+                                                     string $status): void
+  {
+    $this->assertXPathContentContains('//div[@class="rich_content jumbotron_rich_content user_suggestions wrapper_user_suggestions wrapper_active col-12 border-bottom border-primary mb-3 pb-3"]//div[@class="list-group-item bg-transparent px-0 mb-3"]//div[@role="heading"][@aria-level="3"][@class="card-title card_title card_title_Intonation_Library_View_Wrapper_Suggestion"]', $title);
   }
-}
 
 
+  /**
+   * @test
+   * @dataProvider suggestions
+   */
+  public function pageShouldContainsSuggestionsDate(string $title,
+                                                    string $date,
+                                                    string $note,
+                                                    string $status): void
+  {
+    $this->assertXPathContentContains('//div[@class="rich_content jumbotron_rich_content user_suggestions wrapper_user_suggestions wrapper_active col-12 border-bottom border-primary mb-3 pb-3"]//div[@class="list-group-item bg-transparent px-0 mb-3"]', $date);
+  }
 
 
-class KohaCommunitySuggestionsOfUserWithWrongAPIUserTest extends KohaCommunityTestCase {
-  public function setUp() {
-    parent::setUp();
-
-    $user = $this->fixture(Class_Users::class,
-                           ['id' => 34,
-                            'login' => 'harlock',
-                            'password' => 'arcadia',
-                            'idabon' => 'AO989IE']);
+  /**
+   * @test
+   * @dataProvider suggestions
+   */
+  public function pageShouldContainsSuggestionsNote(string $title,
+                                                    string $date,
+                                                    string $note,
+                                                    string $status): void
+  {
+    $this->assertXPathContentContains('//div[@class="rich_content jumbotron_rich_content user_suggestions wrapper_user_suggestions wrapper_active col-12 border-bottom border-primary mb-3 pb-3"]//div[@class="list-group-item bg-transparent px-0 mb-3"]', $note);
+  }
 
-    $this->mock_web_client
-      ->whenCalled('open_url')
-      ->with(self::BASE_URL . 'suggestions?q=' . urlencode('{"suggested_by":32007}'),
-             ['auth' => ['koha_admin', 'k0h@_P455']])
-      ->answers('{"error": "Invalid password","required_permissions": null}');
 
-    $this->response = $this->service->suggestionsOf($user);
+  /**
+   * @test
+   * @dataProvider suggestions
+   */
+  public function pageShouldContainsSuggestionsStatus(string $title,
+                                                      string $date,
+                                                      string $note,
+                                                      string $status): void
+  {
+    $this->assertXPathContentContains('//div[@class="rich_content jumbotron_rich_content user_suggestions wrapper_user_suggestions wrapper_active col-12 border-bottom border-primary mb-3 pb-3"]//div[@class="list-group-item bg-transparent px-0 mb-3"]', $status);
   }
+}
 
 
-  /** @test */
-  public function shouldHaveError() {
-    $this->assertFalse($this->response['statut']);
+
+
+class KohaCommunitySuggestionsErrorResponseTest extends KohaCommunityTestCase {
+  public function setUp() {
+    parent::setUp();
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoDupont())
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl/suggestions?q=%7B%22suggested_by%22%3A%2296138%22%7D',
+                               json_encode(['error' => 'Invalid password',
+                                            'required_permissions' => null]))
+
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=0&show_loans=1&show_holds=0',
+                               KohaFixtures::xmlGetPatronInfoDupont());
+
+    $this->dispatch('abonne/suggestions');
   }
 
 
   /** @test */
-  public function errorShouldBeUnknown() {
-    $this->assertEquals('Échec de l\'authentification par le webservice',
-                        $this->response['erreur']);
+  public function pageShouldRenderNoSuggestionMessage()
+  {
+    $this->assertXPathContentContains('//div', 'Vous n\'avez pas encore fait de suggestion.');
   }
 }
 
 
 
 
-class KohaCommunitySuggestionsOfUserWithBasicAuthNotAllowedUserTest extends KohaCommunityTestCase {
+class KohaCommunitySuggestionsAnOtherErrorResponseTest extends KohaCommunityTestCase {
   public function setUp() {
     parent::setUp();
 
-    $user = $this->fixture(Class_Users::class,
-                           ['id' => 34,
-                            'login' => 'harlock',
-                            'password' => 'arcadia',
-                            'idabon' => 'AO989IE']);
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithresponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoDupont())
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl/suggestions?q=%7B%22suggested_by%22%3A%2296138%22%7D',
+                               json_encode(['error' => 'Basic authentication not allowed']))
 
-    $this->mock_web_client
-      ->whenCalled('open_url')
-      ->with(self::BASE_URL . 'suggestions?q=' . urlencode('{"suggested_by":32007}'),
-             ['auth' => ['koha_admin', 'k0h@_P455']])
-      ->answers('{"error": "Basic authentication not allowed"}');
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=0&show_loans=1&show_holds=0',
+                               KohaFixtures::xmlGetPatronInfoDupont());
 
-    $this->response = $this->service->suggestionsOf($user);
+    $this->dispatch('abonne/suggestions');
   }
 
 
   /** @test */
-  public function shouldHaveError() {
-    $this->assertFalse($this->response['statut']);
-  }
-
-
-  /** @test */
-  public function errorShouldBeUnknown() {
-    $this->assertEquals('Échec de l\'authentification par le webservice',
-                        $this->response['erreur']);
+  public function pageShouldRenderNoSuggestionMessage()
+  {
+    $this->assertXPathContentContains('//div', 'Vous n\'avez pas encore fait de suggestion.');
   }
 }
 
 
 
 
-abstract class KohaCommunitySuggestTestCase extends KohaCommunityTestCase {
+class KohaCommunityPostSuggestTest extends KohaCommunityTestCase {
   public function setUp() {
     parent::setUp();
 
-    $this->mock_web_client
-      ->whenCalled('postRawData')
-      ->with(self::BASE_URL . 'suggestions',
-             '{"suggested_by":"32007","title":"CommitStrip - Le Livre",'
-             . '"item_type":"Livres","author":"CommitStrip","isbn":"","note":"",'
-             . '"publication_year":"","library_id":"IST"}',
-             Class_WebService_SIGB_Koha_CommunityService::JSON_ENCODED,
-             ['auth'=> ['user' => 'koha_admin', 'password' => 'k0h@_P455']])
-      ->answers($this->_postAnswer())
-      ->beStrict();
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoDupont())
 
-    $suggestion = new Class_WebService_SIGB_Suggestion();
-    $suggestion->updateAttributes(['Title' => 'CommitStrip - Le Livre',
-                                   'Author' => 'CommitStrip',
-                                   'DocType' => Class_TypeDoc::LIVRE,
-                                   'User' => $this->user]);
-    $this->response = $this->service->suggest($suggestion);
-  }
+      ->addPostRequestWithResponse('https://koha-community:443/ilsdi.pl/suggestions',
+                                   '{"suggested_by":"96138","title":"CommitStrip - Le Livre","item_type":"Livres","author":"CommitStrip","isbn":"","note":"","publication_year":"","library_id":"MIR"}',
+                                   KohaCommunityFixtures::suggestionSuccess());
 
-
-  abstract protected function _postAnswer();
-}
+    $this->postDispatch('/abonne/suggestion-achat-add',
+                        ['Title' => 'CommitStrip - Le Livre',
+                         'Author' => 'CommitStrip',
+                         'DocType' => Class_TypeDoc::LIVRE]);
+  }
 
 
+  /** @test */
+  public function flashMessengerShouldContainsSuccess()
+  {
+    $this->assertFlashMessengerContentContains('Suggestion d\'achat enregistrée');
+  }
 
 
-class KohaCommunitySuccessfulSuggestTest extends KohaCommunitySuggestTestCase {
-  protected function _postAnswer() {
-    return '{
-    "accepted_by": null,
-    "accepted_date": null,
-    "archived": false,
-    "author": "CommitStrip",
-    "biblio_id": null,
-    "budget_id": null,
-    "collection_title": null,
-    "copyright_date": null,
-    "currency": null,
-    "isbn": null,
-    "item_price": null,
-    "item_type": "Livres",
-    "last_status_change_by": null,
-    "last_status_change_date": null,
-    "library_id": null,
-    "managed_by": null,
-    "managed_date": null,
-    "note": null,
-    "patron_reason": null,
-    "publication_place": null,
-    "publication_year": "0",
-    "publisher_code": null,
-    "quantity": null,
-    "reason": null,
-    "rejected_by": null,
-    "rejected_date": null,
-    "status": "ASKED",
-    "suggested_by": 32007,
-    "suggestion_date": "2022-08-17",
-    "suggestion_id": 6,
-    "timestamp": "2022-08-17T11:25:02+02:00",
-    "title": "CommitStrip - Le Livre",
-    "total_price": null,
-    "volume_desc": null}';
-  }
-
-
-  /** @test */
-  public function shouldHaveNoError() {
-    $this->assertTrue($this->response['statut']);
+  /** @test */
+  public function responeShouldRedirectToAbonneSuggestions() {
+    $this->assertRedirectTo(Class_Url::absolute('/abonne/suggestions'));
   }
 }
 
 
 
 
-class KohaCommunityEmptyResponseSuggestTest extends KohaCommunitySuggestTestCase {
-  protected function _postAnswer() {
-    return '';
+class KohaCommunityEmptyResponseSuggestTest extends KohaCommunityTestCase {
+
+  public function setUp()
+  {
+    parent::setUp();
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoDupont())
+
+      ->addPostRequestWithResponse('https://koha-community:443/ilsdi.pl/suggestions',
+                                   '{"suggested_by":"96138","title":"CommitStrip - Le Livre","item_type":"Livres","author":"CommitStrip","isbn":"","note":"","publication_year":"","library_id":"MIR"}',
+                                   '');
+
+    $this->postDispatch('/abonne/suggestion-achat-add',
+                        ['Title' => 'CommitStrip - Le Livre',
+                         'Author' => 'CommitStrip',
+                         'DocType' => Class_TypeDoc::LIVRE]);
   }
 
 
   /** @test */
-  public function shouldHaveError() {
-    $this->assertFalse($this->response['statut']);
+  public function flashMessengerShouldContainsErrorMessage()
+  {
+    $this->assertFlashMessengerContentContains('Échec de la suggestion, une erreur inconnue est survenue.');
   }
 
 
   /** @test */
-  public function errorShouldBeUnknown() {
-    $this->assertEquals('Échec de la suggestion, une erreur inconnue est survenue.',
-                        $this->response['erreur']);
+  public function responeShouldRedirectToAbonneSuggestions() {
+    $this->assertRedirectTo(Class_Url::absolute('/abonne/suggestions'));
   }
 }
 
 
 
 
-class KohaCommunityErrorBasicAuthenticationResponseSuggestTest extends KohaCommunitySuggestTestCase {
-  protected function _postAnswer() {
-    return '{"error": "Basic authentication not allowed"}';
+class KohaCommunityErrorResponseOnSuggestTest extends KohaCommunityTestCase {
+
+  public function setUp()
+  {
+    parent::setUp();
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoDupont())
+
+      ->addPostRequestWithResponse('https://koha-community:443/ilsdi.pl/suggestions',
+                                   '{"suggested_by":"96138","title":"CommitStrip - Le Livre","item_type":"Livres","author":"CommitStrip","isbn":"","note":"","publication_year":"","library_id":"MIR"}',
+                                   json_encode(['error' => 'Basic authentication not allowed']));
+
+    $this->postDispatch('/abonne/suggestion-achat-add',
+                        ['Title' => 'CommitStrip - Le Livre',
+                         'Author' => 'CommitStrip',
+                         'DocType' => Class_TypeDoc::LIVRE]);
   }
 
 
   /** @test */
-  public function shouldHaveError() {
-    $this->assertFalse($this->response['statut']);
+  public function flashMessengerShouldContainsILSErrorMessage()
+  {
+    $this->assertFlashMessengerContentContains('Échec de la suggestion, le webservice a répondu "Basic authentication not allowed"');
   }
 
 
   /** @test */
-  public function errorShouldBeLeWebServiceARepondu() {
-    $this->assertEquals('Échec de la suggestion, le webservice a répondu "Basic authentication not allowed"',
-                        $this->response['erreur']);
+  public function responeShouldRedirectToAbonneSuggestions() {
+    $this->assertRedirectTo(Class_Url::absolute('/abonne/suggestions'));
   }
 }
 
 
 
 
-class KohaCommunitySoftwareErrorResponseSuggestTest extends KohaCommunitySuggestTestCase {
-  protected function _postAnswer() {
-    return '<h1>Software error:</h1>
+class KohaCommunitySoftwareErrorResponseOnSuggestTest extends KohaCommunityTestCase {
+
+
+  public function setUp()
+  {
+    parent::setUp();
+    $answers = '<h1>Software error:</h1>
 <pre>Can\'t locate object method &quot;error&quot; via package &quot;Error executing run mode \'create_suggestion\': DBIx::Class::ResultSet::create(): Column \'suggestedby\' cannot be null at /home/koha/src/C4/Suggestions.pm line 450
  at /usr/share/perl5/CGI/Application/Dispatch.pm line 707
 &quot; (perhaps you forgot to load &quot;Error executing run mode \'create_suggestion\': DBIx::Class::ResultSet::create(): Column \'suggestedby\' cannot be null at /home/koha/src/C4/Suggestions.pm line 450
@@ -473,43 +518,71 @@ class KohaCommunitySoftwareErrorResponseSuggestTest extends KohaCommunitySuggest
 <p>
 For help, please send mail to the webmaster (<a href="mailto:support@server.com">support@server.com</a>), giving this error message
 and the time and date of the error.
-
 </p>';
+
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoDupont())
+
+      ->addPostRequestWithResponse('https://koha-community:443/ilsdi.pl/suggestions',
+                                   '{"suggested_by":"96138","title":"CommitStrip - Le Livre","item_type":"Livres","author":"CommitStrip","isbn":"","note":"","publication_year":"","library_id":"MIR"}',
+                                   $answers);
+
+    $this->postDispatch('/abonne/suggestion-achat-add',
+                        ['Title' => 'CommitStrip - Le Livre',
+                         'Author' => 'CommitStrip',
+                         'DocType' => Class_TypeDoc::LIVRE]);
   }
 
 
   /** @test */
-  public function shouldHaveError() {
-    $this->assertFalse($this->response['statut']);
+  public function flashMessengerShouldContainsUnknownErrorMessage()
+  {
+    $this->assertFlashMessengerContentContains('Échec de la suggestion, une erreur inconnue est survenue.');
   }
 
 
   /** @test */
-  public function errorShouldBeUnknown() {
-    $this->assertEquals('Échec de la suggestion, une erreur inconnue est survenue.',
-                        $this->response['erreur']);
+  public function responeShouldRedirectToAbonneSuggestions() {
+    $this->assertRedirectTo(Class_Url::absolute('/abonne/suggestions'));
   }
 }
 
 
 
 
-class KohaCommunityJsonErrorSuggestTest extends KohaCommunitySuggestTestCase {
-  protected function _postAnswer() {
-    return json_encode(['error' => "Failed to parse data parameter: malformed JSON string, neither array, object, number, string or atom, at character offset 0 (before \"(end of string)\") at /usr/share/perl5/JSON.pm line 171.\n"]);
+class KohaCommunityAnOtherJsonErrorOnSuggestTest extends KohaCommunityTestCase
+{
+
+  public function setUp() {
+    parent::setUp();
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoDupont())
+
+      ->addPostRequestWithResponse('https://koha-community:443/ilsdi.pl/suggestions',
+                                   '{"suggested_by":"96138","title":"CommitStrip - Le Livre","item_type":"Livres","author":"CommitStrip","isbn":"","note":"","publication_year":"","library_id":"MIR"}',
+                                   json_encode(['error' => "Failed to parse data parameter: malformed JSON string, neither array, object, number, string or atom, at character offset 0 (before \"(end of string)\") at /usr/share/perl5/JSON.pm line 171.\n"]));
+
+    $this->postDispatch('/abonne/suggestion-achat-add',
+                        ['Title' => 'CommitStrip - Le Livre',
+                         'Author' => 'CommitStrip',
+                         'DocType' => Class_TypeDoc::LIVRE]);
   }
 
 
   /** @test */
-  public function shouldHaveError() {
-    $this->assertFalse($this->response['statut']);
+  public function flashMessengerShouldContainsILSErrorMessage()
+  {
+    $this->assertFlashMessengerContentContains('Échec de la suggestion, le webservice a répondu "Failed to parse data parameter: malformed JSON string, neither array, object, number, string or atom, at character offset 0 (before "(end of string)") at /usr/share/perl5/JSON.pm line 171.');
   }
 
 
   /** @test */
-  public function errorShouldBeKnown() {
-    $this->assertEquals('Échec de la suggestion, le webservice a répondu "Failed to parse data parameter: malformed JSON string, neither array, object, number, string or atom, at character offset 0 (before "(end of string)") at /usr/share/perl5/JSON.pm line 171."',
-                        $this->response['erreur']);
+  public function responeShouldRedirectToAbonneSuggestions() {
+    $this->assertRedirectTo(Class_Url::absolute('/abonne/suggestions'));
   }
 }
 
@@ -517,302 +590,489 @@ class KohaCommunityJsonErrorSuggestTest extends KohaCommunitySuggestTestCase {
 
 
 class KohaCommunityGetUserHistoryTest extends KohaCommunityTestCase {
-  protected $_loans_history;
+
   public function setUp() {
     parent::setUp();
-    $this->mock_web_client = KohaCommunityFixtures::mockWebClientForLoansHistory(static::BASE_URL,
-                                                                                 $this);
 
-    $this->service->setWebClient($this->mock_web_client);
-    $this->loans_history = $this->service->loansHistory($this->user->getEmprunteur());
-  }
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoDupont())
 
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl/checkouts?q=%7B%22item_id%22%3A%7B%22-not_like%22%3A%22null%22%7D%2C%22checkin_date%22%3A%7B%22%3E%22%3A%222023-12-03%22%7D%7D&_match=exact&_page=1&_per_page=50&checked_in=1&patron_id=96138',
+                               file_get_contents(__DIR__ . '/../../../../fixtures/koha_issues_history.json'))
 
-  /** @test */
-  public function userShouldHaveTwoLoans() {
-    $this->assertCount(2, $this->loans_history->getLoans());
-  }
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl/items/203',
+                               file_get_contents(__DIR__ . '/../../../../fixtures/koha_item_203.json'))
 
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl/items/192',
+                               file_get_contents(__DIR__ . '/../../../../fixtures/koha_item_192.json'))
+      ;
 
-  /** @test */
-  public function oneLoanShouldContainsTitleAngelsAndFantasy() {
-    $this->assertEquals("Angels and Fantasy", $this->loans_history->getLoans()[0]->getTitre());
+    $this->fixture(Class_Exemplaire::class,
+                   ['id' => 1,
+                    'code_barres' => 111203,
+                    'id_notice' => 1
+                   ]);
+
+    $this->fixture(Class_Notice::class,
+                   ['id' => 1]);
+
+    $this->fixture(Class_Exemplaire::class,
+                   ['id' => 2,
+                    'code_barres' => 111192,
+                    'id_notice' => 2]);
+
+    $this->fixture(Class_Notice::class,
+                   ['id' => 2]);
+
+    /* Historic Template */
+    $this->dispatch('abonne/loans-history');
   }
 
 
   /** @test */
-  public function returnDateShouldBe17_08_2022() {
-    $this->assertEquals('17/08/2022', $this->loans_history->getLoans()[1]->getDateRetour());
+  public function pageShouldContainsTwoHistoryLoans() {
+    $this->assertXPathCount('//table//tr', 3);
   }
 
 
   /** @test */
-  public function firstLoanShouldNotBeLate() {
-    $this->assertFalse($this->loans_history->getLoans()[0]->isLate());
+  public function returnDateShouldBe17_08_2022() {
+    $this->assertXPathContentContains('//td', '17/08/2022');
   }
 }
 
 
 
 
-class KohaCommunityGetUserHistoryErrorAuthenticationTest extends KohaCommunityTestCase {
-  protected $_loans_history;
+class KohaCommunityHistoryLoansErrorAuthenticationTest extends KohaCommunityTestCase {
   public function setUp() {
     parent::setUp();
-    $this->mock_web_client = $this->mock()
-                                  ->whenCalled('open_url')
-                                  ->answers('{"error":"Basic authentication not allowed"}');
-
-    $this->service->setWebClient($this->mock_web_client);
-    $this->loans_history = $this->service->loansHistory($this->user->getEmprunteur());
-  }
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoDupont())
 
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl/checkouts?q=%7B%22item_id%22%3A%7B%22-not_like%22%3A%22null%22%7D%2C%22checkin_date%22%3A%7B%22%3E%22%3A%222023-12-03%22%7D%7D&_match=exact&_page=1&_per_page=50&checked_in=1&patron_id=96138',
+                               json_encode(['error' => "Basic authentication not allowed"]))
+      ;
 
-  /** @test */
-  public function userShouldHaveZeroLoans() {
-    $this->assertCount(0, $this->loans_history->getLoans());
+    /* Historic Template */
+    $this->dispatch('abonne/loans-history');
   }
 
 
   /** @test */
   public function historyLoansShouldBeEmpty() {
-    $this->assertTrue($this->loans_history->getLoans()->isEmpty());
+    $this->assertXPath('//div//p', 'Pas de prêts dans l\'historique');
   }
 }
 
 
 
 
-class KohaCommunityHoldsTest extends KohaCommunityTestCase {
+class KohaCommunityHoldsTest extends KohaCommunityTestCase
+{
+
   protected
     $_response,
     $_holds;
 
   public function setUp() {
     parent::setUp();
-    $this->mock_web_client->whenCalled('open_url')
-                          ->with(static::BASE_URL . 'holds?biblio_id=96629',
-                                 ['auth'=> ['user' => 'koha_admin', 'password' => 'k0h@_P455']])
-                          ->answers(KohaCommunityFixtures::elementaireMonCherPolarHolds());
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoLaure())
 
-    $item = $this->fixture(Class_Exemplaire::class,
-                           ['id' => 2,
-                            'id_origine' => 96629]);
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=572&show_contact=0&show_loans=1&show_holds=0',
+                               KohaFixtures::xmlGetPatronInfoLaure());
 
-    $this->_response = $this->service->holdsForItem($item);
-    $this->_holds = $this->_response['holds'];
+    /* no call to community api ? lol */
+    $this->dispatch('/abonne/reservations');
   }
 
 
   /** @test */
-  public function responseStatusShouldBeTrue() {
-    $this->assertTrue($this->_response['statut']);
+  public function pageShouldContainsFourHolds() {
+    $this->assertXPathCount('//div[contains(@class, "card_title_Intonation_Library_View_Wrapper_Hold")]', 4);
   }
 
 
   /** @test */
-  public function firstHoldReserveDateShouldBe2019_07_26() {
-    $this->assertEquals('2019-07-26',$this->_holds[0]->getReserveDate());
+  public function thirdHoldExpirationDateShouldBe2014_05_17() {
+    $this->assertEquals('2014-05-17', Class_Users::find(34)->getReservations()[2]->getExpirationDate());
   }
 
 
   /** @test */
-  public function firstHoldExpirationDateShouldBe2019_12_31() {
-    $this->assertEquals('2019-12-31', $this->_holds[0]->getExpirationDate());
+  public function firstHoldIdShouldBe27136() {
+    $this->assertEquals(27136, Class_Users::find(34)->getReservations()[0]->getId());
+  }
+}
+
+
+
+
+class KohaCommunityPickupLocationsWithAnnexeTest extends KohaCommunityTestCase
+{
+
+  public function setUp()
+  {
+    parent::setUp();
+
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoLaure())
+
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl/items/96630/pickup_locations?_match=exact&patron_id=572',
+                               KohaCommunityFixtures::getPickupLocations());
+
+    Class_CosmoVar::set('site_retrait_resa', 1);
+
+    $this->fixture(Class_Exemplaire::class,
+                   ['id' => 2,
+                    'id_origine' => 96629,
+                    'zone995' => serialize([['clef' => '9',
+                                             'valeur' => '96630']])]);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 15,
+                    'libelle' => 'IEP Grenoble',
+                    'id_origine' => 'IEPG',
+                    'no_pickup' => false]);
+
+    $this->fixture(Class_CodifAnnexe::class,
+                   ['id' => 16,
+                    'libelle' => 'Bibliobus',
+                    'id_origine' => 'BIBLIOBUS',
+                    'no_pickup' => true]);
+
+    $this->dispatch('recherche/reservation-pickup-ajax/copy_id/2/code_annexe/IEPG');
   }
 
 
   /** @test */
-  public function firstHoldIdShouldBe1124() {
-    $this->assertEquals(1124, $this->_holds[0]->getId());
+  public function sciencePoShouldBePresent()
+  {
+    $this->assertXPathContentContainsFromJson('//form', 'Bibliothèque de Sciences Po Grenoble', $this->_response->getBody());
   }
 
 
   /** @test */
-  public function holdsCountShouldBeThree() {
-    $this->assertCount(3, $this->_holds);
+  public function bibliobusShouldNotBePresent()
+  {
+    $this->assertNotXPathContentContainsFromJson('//form', 'Bibliobus', $this->_response->getBody());
   }
 }
 
 
 
 
-abstract class AbstractKohaCommunityPickupLocationsTestCase extends KohaCommunityTestCase
+class KohaCommunityPickupLocationsWithoutAnnexeTest extends KohaCommunityTestCase
 {
-
   public function setUp()
   {
     parent::setUp();
 
-    $this->_createAnnexes();
 
-    $this->mock_web_client->whenCalled('open_url')
-                          ->with(static::BASE_URL . 'items/96630/pickup_locations?_match=exact&patron_id=32007',
-                                 ['auth'=> ['user' => 'koha_admin', 'password' => 'k0h@_P455']])
-                          ->answers(KohaCommunityFixtures::getPickupLocations())
-                          ->beStrict();
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoLaure())
+
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl/items/96630/pickup_locations?_match=exact&patron_id=572',
+                               KohaCommunityFixtures::getPickupLocations());
 
-    $this->item = $this->fixture(Class_Exemplaire::class,
-                           ['id' => 2,
-                            'id_origine' => 96629,
-                            'zone995' => serialize([['clef' => '9',
-                                                     'valeur' => '96630']])]);
+    Class_CosmoVar::set('site_retrait_resa', 1);
+
+    $this->fixture(Class_Exemplaire::class,
+                   ['id' => 2,
+                    'id_origine' => 96629,
+                    'zone995' => serialize([['clef' => '9',
+                                             'valeur' => '96630']])]);
+
+    $this->dispatch('recherche/reservation-pickup-ajax/copy_id/2');
+  }
+
+
+  /** @test */
+  public function sciencePoShouldBePresent()
+  {
+    $this->assertXPathContentContainsFromJson('//form', 'Bibliothèque de Sciences Po Grenoble');
   }
 
 
-  protected function _createAnnexes(): self
+  /** @test */
+  public function bibliobusShouldBePresent()
   {
-    return $this;
+    $this->assertXPathContentContainsFromJson('//form', 'Bibliobus');
   }
 }
 
 
 
 
-class KohaCommunityPickupLocationsWithAnnexeTest
-  extends AbstractKohaCommunityPickupLocationsTestCase
-{
+class KohaCommunityEditUserTest extends KohaCommunityTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    Class_AdminVar::set('CHAMPS_FICHE_UTILISATEUR', 'nom;prenom;mail');
+
+    Class_HttpClientFactory::getInstance()
+      ->getLastHttpClient()
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoLaure())
+
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=572&show_contact=0&show_loans=1&show_holds=0',
+                               KohaFixtures::xmlGetPatronInfoLaure());
+
+    $this->dispatch('/abonne/modifier');
+  }
+
 
-  protected function _createAnnexes(): self
+  public function formElements(): array
   {
-    $this->fixture(Class_CodifAnnexe::class,
-                   ['id' => 15,
-                    'libelle' => 'IEP Grenoble',
-                    'id_origine' => 'IEPG',
-                    'no_pickup' => false]);
+    return [['nom', 'Super bien'],
+            ['prenom', 'Olivier'],
+            ['mail', 'old@mail.org']];
+  }
 
-    $this->fixture(Class_CodifAnnexe::class,
-                   ['id' => 16,
-                    'libelle' => 'Bibliobus',
-                    'id_origine' => 'BIBLIOBUS',
-                    'no_pickup' => true]);
 
-    return $this;
+  /**
+   * @test
+   * @dataProvider formElements
+   */
+  public function formShouldContainsDefinedElements(string $element_id, string $element_value): void
+  {
+    $this->assertXPath('//form//input[@id="' . $element_id . '"][@value="' . $element_value . '"]');
+  }
+}
+
+
+
+
+class KohaCommunityPostEditUserWithEmptyValueTest extends KohaCommunityTestCase
+{
+
+  public function setUp() {
+    parent::setUp();
+
+    Class_AdminVar::set('CHAMPS_FICHE_UTILISATEUR', 'nom;prenom;mail');
+
+    Class_HttpClientFactory::forTest()
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoLaure())
+
+      ->addPutRequestWithResponse('https://koha-community:443/ilsdi.pl/patrons/96138',
+                                  '{"library_id":"BDM","category_id":"INDIVIDU","firstname":"","email":""}',
+                                  KohaCommunityFixtures::successUpdateUser());
+
+    $this->postDispatch('/abonne/modifier',
+                        ['nom' => '',
+                         'prenom' => '',
+                         'mail' => '']);
   }
 
 
   /** @test */
-  public function responseStatusShouldBeIEGPBibliothequeSciencePo()
+  public function borrowerCanSendBlankField(): void
   {
-    $this->assertEquals(["IEPG" => "Bibliothèque de Sciences Po Grenoble"],
-                        $this->service->pickupLocationsFor($this->user, $this->item));
+    $this->assertFlashMessengerContentContains('Vos informations ont bien été modifiées.');
   }
 }
 
 
 
 
-class KohaCommunityPickupLocationsWithoutAnnexeTest
-  extends AbstractKohaCommunityPickupLocationsTestCase
+class KohaCommunityPostEditUserTest extends KohaCommunityTestCase
 {
 
-  protected function _createAnnexes(): self
-  {
-    Class_CodifAnnexe::deleteBy([]);
-    Class_CodifAnnexe::clearCache();
+  public function setUp() {
+    parent::setUp();
+
+    Class_AdminVar::set('CHAMPS_FICHE_UTILISATEUR', 'nom;prenom;mail');
+
+    Class_HttpClientFactory::forTest()
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoLaure())
 
-    return $this;
+      ->addPutRequestWithResponse('https://koha-community:443/ilsdi.pl/patrons/96138',
+                                  '{"surname":"Maxwell","library_id":"BDM","category_id":"INDIVIDU","firstname":"Oliver","email":"new@mail.org"}',
+                                  KohaCommunityFixtures::successUpdateUser());
+
+    $this->postDispatch('/abonne/modifier',
+                        ['nom' => 'Maxwell',
+                         'prenom' => 'Oliver',
+                         'mail' => 'new@mail.org']);
   }
 
 
   /** @test */
-  public function responseStatusShouldContainAllAnnexeFromSIGB()
+  public function borrowerCanUpdateFields(): void
   {
-    $this->assertEquals(['IEPG' => 'Bibliothèque de Sciences Po Grenoble',
-                         'BIBLIOBUS' => 'Bibliobus'],
-                        $this->service->pickupLocationsFor($this->user, $this->item));
+    $this->assertFlashMessengerContentContains('Vos informations ont bien été modifiées.');
   }
 }
 
 
 
 
-class KohaCommunityEditUserTest extends KohaCommunityTestCase {
+class KohaCommunityPostEditUserOnlyMailTest extends KohaCommunityTestCase
+{
 
-  protected $_patron = ['surname' => 'haddock',
-                        'firstname' => 'captain',
-                        'address' => '',
-                        'city' => '',
-                        'postal_code' => '',
-                        'email' => '',
-                        'phone' => '',
-                        'mobile' => '',
-                        'library_id' => 'IST',
-                        'category_id' => 'B'];
-
-  public function setUserInfoWaitReply($user_info,$response){
-    $this->mock_web_client
-      ->whenCalled('postRawData')
-      ->answers('""')
-      ->whenCalled('putRawData')
-      ->with(self::BASE_URL . 'patrons/32007',
-             json_encode($user_info),
-             Class_WebService_SIGB_Koha_CommunityService::JSON_ENCODED,
-             ['auth'=> ['user' => 'koha_admin', 'password' => 'k0h@_P455']])
-      ->answers($response);
-
-    $this->borrower->updateFromUser($this->user);
-    $this->borrower->subscriptionAdd('B','B');
-    Class_Users::clearCache();
-    if ($this->borrower->save())
-      $this->user->save();
+  public function setUp() {
+    parent::setUp();
+
+    Class_AdminVar::set('CHAMPS_FICHE_UTILISATEUR', 'mail');
+
+    Class_HttpClientFactory::forTest()
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoLaure())
+
+      ->addPutRequestWithResponse('https://koha-community:443/ilsdi.pl/patrons/96138',
+                                  '{"surname":"Super bien","library_id":"BDM","category_id":"INDIVIDU","email":"new@mail.org"}',
+                                  KohaCommunityFixtures::successUpdateUser());
+
+    $this->postDispatch('/abonne/modifier',
+                        ['mail' => 'new@mail.org']);
   }
 
 
   /** @test */
-  public function withoutResponseShouldNotUpdatePasswordAndReturnError() {
-    $this->user->setPrenom('captain');
-    $this->user->setNom('haddock');
-    $this->setUserInfoWaitReply($this->_patron,
-                                '');
-    $this->assertEquals(['Le format de la réponse envoyé par Koha à la demande de mise à jour des informations personnelles est incorrect.'], $this->borrower->getErrors());
+  public function borrowerCanUpdateFields(): void
+  {
+    $this->assertFlashMessengerContentContains('Vos informations ont bien été modifiées.');
+  }
+}
+
+
+
+
+class KohaCommunityPostEditUserWithNoFieldTest extends KohaCommunityTestCase
+{
+
+  public function setUp() {
+    parent::setUp();
+
+    Class_AdminVar::set('CHAMPS_FICHE_UTILISATEUR', '');
+
+    Class_HttpClientFactory::forTest()
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoLaure())
 
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=572&show_contact=0&show_loans=1&show_holds=0',
+                               KohaFixtures::xmlGetPatronInfoLaure());
+
+    $this->postDispatch('/abonne/modifier',
+                        ['nom' => 'Maxwell']);
   }
 
 
   /** @test */
-  public function withErrorBorrowerNotFoundShouldFailAndNoticeUser() {
-    $this->user->setPrenom('captain');
-    $this->user->setNom('haddock');
-    $this->setUserInfoWaitReply($this->_patron,
-                                '{"error": "Patron not found."}');
-    $this->assertEquals(['Erreur de mise à jour des informations : Patron not found.'], $this->borrower->getErrors());
+  public function borrowerCannotUpdateData(): void
+  {
+    $this->assertXPathContentContains('//li', 'Une erreur c\'est produite. Vous ne pouvez pas modifier ce formulaire.');
+  }
+}
+
 
+
+
+class KohaCommunityPostEditUserWithNotAllowedFieldTest extends KohaCommunityTestCase
+{
+
+  public function setUp() {
+    parent::setUp();
+
+    Class_AdminVar::set('CHAMPS_FICHE_UTILISATEUR', 'nom;prenom');
+
+    Class_HttpClientFactory::forTest()
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoLaure())
+
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=572&show_contact=0&show_loans=1&show_holds=0',
+                               KohaFixtures::xmlGetPatronInfoLaure());
+
+    $this->postDispatch('/abonne/modifier',
+                        ['nom' => 'Maxwell',
+                         'prenom' => 'Oliver',
+                         'mail' => 'new']);
   }
 
 
   /** @test */
-  public function withValidResponseShouldUpdateUser() {
-    $this->user->setPrenom('captain');
-    $this->user->setNom('haddock');
-    $this->user->setMail('captain.haddock@tonnerre.bzh');
-    $this->_patron['email'] = 'captain.haddock@tonnerre.bzh';
-    $this->setUserInfoWaitReply($this->_patron,
-                                KohaCommunityFixtures::successUpdateUser());
-    $this->assertEquals('captain', Class_Users::find(34)->getPrenom());
-    $this->assertEquals([], $this->borrower->getErrors());
+  public function borrowerCannotUpdateData(): void
+  {
+    $this->assertXPathContentContains('//li', 'Une erreur c\'est produite. Vous ne pouvez pas modifier ce formulaire.');
   }
 }
 
 
 
+class KohaCommunityPostEditUserFullMapTest extends KohaCommunityTestCase
+{
+
+  public function setUp() {
+    parent::setUp();
+
+    Class_AdminVar::set('CHAMPS_FICHE_UTILISATEUR', 'nom;prenom;pseudo;adresse;ville;code_postal;mail;telephone');
+
+    Class_HttpClientFactory::forTest()
+      ->addRequestWithResponse('https://koha-community:443/ilsdi.pl?service=GetPatronInfo&patron_id=96138&show_contact=1&show_loans=0&show_holds=1',
+                               KohaFixtures::xmlGetPatronInfoLaure())
+
+      ->addPutRequestWithResponse('https://koha-community:443/ilsdi.pl/patrons/96138',
+                                  '{"surname":"Maxwell","library_id":"BDM","category_id":"INDIVIDU","firstname":"Olivier","address":"23 lambard street","city":"Pira","postal_code":"9929","email":"monmail@mock.org","mobile":"+41990001223"}',
+                                  KohaCommunityFixtures::successUpdateUser());
+
+    $this->postDispatch('/abonne/modifier',
+                        ['nom' => 'Maxwell',
+                         'prenom' => 'Olivier',
+                         'pseudo' => 'Garage',
+                         'adresse' => '23 lambard street',
+                         'ville' => 'Pira',
+                         'code_postal' => '9929',
+                         'mail' => 'monmail@mock.org',
+                         'telephone' => '+41990001223']);
+  }
+
+
+  /** @test */
+  public function borrowerCanUpdateFields(): void
+  {
+    $this->assertFlashMessengerContentContains('Vos informations ont bien été modifiées.');
+  }
+}
+
+
 
 /* @see https://forge.afi-sa.net/issues/191394 */
-class KohaCommunityLoansPerPageTest extends KohaCommunityTestCase {
-  public function setUp(){
+class KohaCommunityLoansPerPageTest extends ModelTestCase {
+
+  protected $_service;
+
+
+  public function setUp()
+  {
     parent::setUp();
-    $this->service->setLoansPerPage('50');
+    $this->_service = (new Class_WebService_SIGB_Koha_Service)->setLoansPerPage('50');
   }
 
 
   /** @test */
-  public function loansPerPageShouldBeInt() {
-    $this->assertEquals('integer', gettype($this->service->getLoansPerPage()));
+  public function loansPerPageShouldBeInt()
+  {
+    $this->assertEquals('integer', gettype($this->_service->getLoansPerPage()));
   }
 
 
   /** @test */
-  public function loansPerPageServiceShouldBe50() {
-    $this->assertEquals(50, $this->service->getLoansPerPage());
+  public function loansPerPageServiceShouldBe50()
+  {
+    $this->assertEquals(50, $this->_service->getLoansPerPage());
   }
 }
diff --git a/tests/scenarios/MobileApplication/RestfulApiTest.php b/tests/scenarios/MobileApplication/RestfulApiTest.php
index 65f8e396bac363367cdc5c9f8222e23b269bc02e..784d31718840d4129cfcb654121936aabe717daf 100644
--- a/tests/scenarios/MobileApplication/RestfulApiTest.php
+++ b/tests/scenarios/MobileApplication/RestfulApiTest.php
@@ -23,7 +23,6 @@ require_once 'tests/fixtures/DilicomFixtures.php';
 
 abstract class Scenario_MobileApplication_RestfulApi_UserAccountTestCase extends AbstractControllerTestCase {
   protected
-    $_storm_default_to_volatile = true,
     $_potter,
     $_sigb;
 
@@ -48,6 +47,7 @@ abstract class Scenario_MobileApplication_RestfulApi_UserAccountTestCase extends
                              'role_level' => ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB,
                              'idabon' => '234',
                              'id_site' => 1]);
+
     $this->fixture( Class_IdentityClient::class,
                    [ 'id' => 1,
                     'client_id'=>'MyBibApp',
@@ -61,6 +61,7 @@ abstract class Scenario_MobileApplication_RestfulApi_UserAccountTestCase extends
                        'disable_response_type',
                        'scope'}"
                    ]);
+
     $this->fixture( Class_IdentityClient::class,
                    [ 'id' => 2,
                     'client_id'=>'PNB',
@@ -311,12 +312,25 @@ class Scenario_MobileApplication_RestfulApi_UserAccountLoansWithTokenTest extend
                          'loaned_by' => 'puppy',
                          'library' => 'Annecy',
                          'record' => [ 'id' => '34',
-                                      'thumbnail' => 'http://img.com/potter.jpg' ]
+                                       'thumbnail' => 'http://img.com/potter.jpg' ]
                          ],
                         $this->_json['loans'][1]);
   }
 
 
+  /** @test */
+  public function responseShouldContainsAlice() {
+    $this->assertEquals(['id' => '345_13',
+                         'title' => 'Alice',
+                         'author' => '',
+                         'date_due' => '01-03-2022',
+                         'loaned_by' => 'puppy',
+                         'library' => ''
+                         ],
+                        $this->_json['loans'][0]);
+  }
+
+
   /** @test */
   public function responseShouldContainsPinocchioPnbLoan() {
     $this->assertEquals(['id' => '345_5',
diff --git a/tests/scenarios/Templates/ChiliLoginTest.php b/tests/scenarios/Templates/ChiliLoginTest.php
index ddd188a52860f71f98dccc31176f944cb4b1ce6e..57d79d47133553fb6ffd282246fc075a8580600f 100644
--- a/tests/scenarios/Templates/ChiliLoginTest.php
+++ b/tests/scenarios/Templates/ChiliLoginTest.php
@@ -21,7 +21,6 @@
 
 
 abstract class ChiliLoginWidgetTestCase extends Admin_AbstractControllerTestCase {
-  protected $_storm_default_to_volatile = true;
 
   public function setUp() {
     parent::setUp();
@@ -198,7 +197,7 @@ class ChiliLoginWidgetFrontTest extends ChiliLoginWidgetTestCase {
 
   /** @test */
   public function menuShouldLinkToMyLoansWithCounterTwo() {
-    $this->assertXPathContentContains('//a[contains(@class, "nav-link")][contains(@href, "/abonne/prets")]//span[contains(@class, "badge_tag")]', '2');
+    $this->assertXPathContentContains('//a[contains(@class, "nav-link")][contains(@href, "/abonne/prets")]//span[contains(@class, "badge_tag")]', '2', $this->_response->getBody());
   }
 
 
@@ -316,4 +315,4 @@ class ChiliLoginDispatchMenuArticlesFailureTest extends ChiliLoginWidgetTestCase
     $this->assertFlashMessengerEquals([['notification' => ['message' => 'Une erreur c\'est produite. Il est impossible d\'afficher cette page',
                                                            'status' => 'error']]]);
   }
-}
\ No newline at end of file
+}
diff --git a/tests/scenarios/Templates/TemplatesAbonneTest.php b/tests/scenarios/Templates/TemplatesAbonneTest.php
index 9a4b204dfc4d7a8e015322da28766a7b71a3c443..ea1d1e5b89cde46c7a83b70da53cb5fd1d7b2723 100644
--- a/tests/scenarios/Templates/TemplatesAbonneTest.php
+++ b/tests/scenarios/Templates/TemplatesAbonneTest.php
@@ -831,12 +831,12 @@ class TemplatesAbonneDispatchMonHistoriqueTest extends TemplatesIntonationAccoun
     $pottifar_old->addLoan($pottifar_old_issue);
 
     $this->_mock_emprunts = (new Class_Testing_WebService_SIGB_Koha_Service())
-                                 ->whenCalled('loansHistory')
-                                 ->answers($pottifar_old)
-                                 ->whenCalled('getLoansPerPage')
-                                 ->answers(3)
-                                 ->whenCalled('providesPagedLoans')
-                                 ->answers(false);
+      ->whenCalled('loansHistory')
+      ->answers($pottifar_old)
+      ->whenCalled('getLoansPerPage')
+      ->answers(3)
+      ->whenCalled('providesPagedLoans')
+      ->answers(false);
 
     $this->_emprunteur->setService($this->_mock_emprunts);
 
@@ -1369,58 +1369,6 @@ class TemplatesAbonneAccountEditTest extends TemplatesIntonationAccountTestCase
                                                                                'confirm_new_password' => '']);
     $this->assertXPathContentContains('//div', 'Une valeur est requise');
   }
-
-
-  /** @test */
-  public function changePasswordPostNewsecretShouldUpdatePasswordUser() {
-    Class_Crypt::setPhpCommand($this->mock()
-                               ->whenCalled('password_hash')
-                               ->with('test', PASSWORD_BCRYPT)
-                               ->willDo(function($pass, $crypt) { return '$2a$08$gPuCFG7FU2psZ52z5R.ACeW295qSfRJuTd04i/zwjjNI67ZUmVIHe'; })
-
-                               ->whenCalled('password_hash')
-                               ->with('newsecret', PASSWORD_BCRYPT)
-                               ->willDo(function($pass, $crypt) { return '$2a$08$gPuCFG7FU2psZ52z5R'; })
-
-                               ->whenCalled('password_verify')
-                               ->answers(true));
-
-    $int_bib = $this->fixture(Class_IntBib::class,
-                              ['id' => 1111,
-                               'comm_params' => ['url_serveur' => 'nanookService'],
-                               'comm_sigb' => Class_IntBib::COM_NANOOK]);
-    $sigb_comm = $int_bib->getSigbComm();
-    $sigb_comm->setWebClient($this->mock()
-
-                             ->whenCalled('postData')
-                             ->with('http://nanookService/service/UpdatePatronInfo/patronId/',
-                                    ['password' => 'newsecret',
-                                     'mail' => '',
-                                     'phoneNumber' => ''])
-                             ->answers(true)
-
-                             ->beStrict());
-
-    $user = Class_Users::getIdentity();
-
-    $user
-      ->setIntBib($int_bib)
-      ->setRoleLevel(6)
-      ->save();
-
-    $blowfish = (new Class_User_Password($user))->format();
-
-    $user
-      ->setPassword($blowfish)
-      ->save();
-
-    $this->postDispatch('/opac/abonne/changer-mon-mot-de-passe/id_profil/72',
-                        ['current_password' => 'test',
-                         'new_password' => 'newsecret',
-                         'confirm_new_password' => 'newsecret']);
-
-    $this->assertEquals('$2a$08$gPuCFG7FU2psZ52z5R', Class_Users::getIdentity()->getPassword());
-  }
 }
 
 
@@ -2543,13 +2491,13 @@ abstract class TemplatesAbonnePaginatedLoansSortByTestCase
   protected function _getModels() : array {
     Class_Users::getIdentity()
       ->addChildCard($this->fixture(Class_Users::class,
-                                     [ 'id' => 123,
-                                       'nom' => 'Souricier',
-                                       'prenom' => 'Gris',
-                                       'login' => '01234',
-                                       'date_fin' => '2024-01-01',
-                                       'password' => 's3cr3t',
-                                       'idAbon' => '091234']));
+                                    [ 'id' => 123,
+                                      'nom' => 'Souricier',
+                                      'prenom' => 'Gris',
+                                      'login' => '01234',
+                                      'date_fin' => '2024-01-01',
+                                      'password' => 's3cr3t',
+                                      'idAbon' => '091234']));
     $cards = new Class_User_Cards(Class_Users::getIdentity());
 
     $item = $this->fixture(Class_Exemplaire::class,
@@ -2566,10 +2514,10 @@ abstract class TemplatesAbonnePaginatedLoansSortByTestCase
                                                         'url_image' => 'https://monimage.org/chr',
                                                         'unimarc' => (new Class_NoticeUnimarc_Fluent)
                                                         ->zoneWithChildren('200', ['a' => 'Chroniques de la lune Noire',
-                                                                                  'f' => 'Ledroit, froideval',
-                                                                                  'h' => 't. 1'])
+                                                                                   'f' => 'Ledroit, froideval',
+                                                                                   'h' => 't. 1'])
                                                         ->render()
-                                                        ])]);
+                                                       ])]);
 
     $chroniques = new Class_WebService_SIGB_Emprunt('15', new Class_WebService_SIGB_Exemplaire(12390));
     $chroniques
@@ -2607,8 +2555,8 @@ abstract class TemplatesAbonnePaginatedLoansSortByTestCase
                                                                       ->zoneWithChildren('200', ['a' => 'Elric le nécromancien',
                                                                                                  'f' => 'Michael Moorcock',
                                                                                                  'h' => 't. 1'])
-                                                        ->render()
-])]));
+                                                                      ->render()
+                                                                     ])]));
 
     $elric->parseExtraAttributes(['Dateretourprevue' => '25/10/2010',
                                   'Section' => 'Espace jeunesse',
@@ -2681,3 +2629,52 @@ class TemplatesAbonnePretsSortByIssueDateDescTest
             [4, 'Elric le n', '06/09/2001' ]];
   }
 }
+
+
+
+
+class TemplatesAbonneChangePasswordAsAdminTest extends AbstractControllerTestCase
+{
+
+  public function setUp()
+  {
+    parent::setUp();
+    Class_Crypt::setPhpCommand($this->mock()
+
+                               ->whenCalled('password_hash')
+                               ->with('mdp', PASSWORD_BCRYPT)
+                               ->willDo(fn($pass, $crypt) => '$2a$08$gPuCFG7FU2psZ52z5R.ACeW295qSfRJuTd04i/zwjjNI67ZUmVIHe')
+
+                               ->whenCalled('password_hash')
+                               ->with('test', PASSWORD_BCRYPT)
+                               ->willDo(fn($pass, $crypt) => '$2a$08$gPuCFG7FU2psZ52z5R.ACeW295qSfRJuTd04i/zwjjNI67ZUmVIHe')
+
+                               ->whenCalled('password_hash')
+                               ->with('newsecret', PASSWORD_BCRYPT)
+                               ->willDo(fn($pass, $crypt) => '$2a$08$gPuCFG7FU2psZ52z5R')
+
+                               ->whenCalled('password_verify')
+                               ->answers(true));
+
+    $user = $this->fixture(Class_Users::class,
+                           ['id' => 789789,
+                            'login' => 'admin Jojo',
+                            'password' => 'test',
+                            'role_level' => ZendAfi_Acl_AdminControllerRoles::ADMIN_PORTAIL
+                           ]);
+
+    ZendAfi_Auth::getInstance()->logUser($user);
+
+    $this->postDispatch('/opac/abonne/changer-mon-mot-de-passe/id_profil/72',
+                        ['current_password' => 'test',
+                         'new_password' => 'newsecret',
+                         'confirm_new_password' => 'newsecret']);
+  }
+
+
+  /** @test */
+  public function changePasswordPostNewsecretShouldUpdatePasswordUser()
+  {
+    $this->assertEquals('$2a$08$gPuCFG7FU2psZ52z5R', Class_Users::getIdentity()->getPassword());
+  }
+}