From 2d17f25770a4cd7be65518912693112989c56cce Mon Sep 17 00:00:00 2001
From: Patrick Barroca <pbarroca@afi-sa.fr>
Date: Wed, 29 Sep 2021 11:14:07 +0200
Subject: [PATCH] dev #141922 : enhance activities registration mails

* add ical attachment
* add details in b
* add details in body
* subject and body are now admin vars
---
 VERSIONS_WIP/141922                           |   1 +
 library/Class/Activity/AbstractMail.php       |  20 ++-
 library/Class/Activity/RegistrationMail.php   |  71 ++++++++-
 library/Class/AdminVar.php                    |  13 +-
 library/Class/ICal/SessionActivity.php        |  74 ++++++++++
 library/Class/SessionActivity.php             |  18 ++-
 library/ZendAfi/Mail.php                      |   5 +
 .../ZendAfi/View/Helper/Admin/AdminVar.php    |   9 ++
 .../AbonneControllerActivitiesTest.php        | 137 +++++++++++++-----
 .../ActivitiesWithQueueAdminTest.php          |  30 +++-
 .../scenarios/Activities/ActivityIcalTest.php | 126 ++++++++++++++++
 11 files changed, 451 insertions(+), 53 deletions(-)
 create mode 100644 VERSIONS_WIP/141922
 create mode 100644 library/Class/ICal/SessionActivity.php
 rename tests/{application/modules/opac/controllers => scenarios/Activities}/AbonneControllerActivitiesTest.php (90%)
 create mode 100644 tests/scenarios/Activities/ActivityIcalTest.php

diff --git a/VERSIONS_WIP/141922 b/VERSIONS_WIP/141922
new file mode 100644
index 00000000000..20ef5ad5092
--- /dev/null
+++ b/VERSIONS_WIP/141922
@@ -0,0 +1 @@
+ - ticket #141922 : Activités : Améliorations des courriels de confirmation d'inscription
\ No newline at end of file
diff --git a/library/Class/Activity/AbstractMail.php b/library/Class/Activity/AbstractMail.php
index f52950e0516..8c8f11cceb6 100644
--- a/library/Class/Activity/AbstractMail.php
+++ b/library/Class/Activity/AbstractMail.php
@@ -69,7 +69,8 @@ abstract class Class_Activity_AbstractMail {
   protected function _sendMail($recipients, $subject, $body) {
     if (!$recipients = array_filter($recipients))
       return $this;
-    $mail = new ZendAfi_Mail();
+
+    $mail = new ZendAfi_Mail;
 
     foreach($recipients as  $recipient)
       $mail->addTo($recipient);
@@ -77,13 +78,25 @@ abstract class Class_Activity_AbstractMail {
     $mail
       ->setFrom('no-reply@' . Class_AdminVar::getNomDomaine())
       ->setSubject($subject)
-      ->setBodyHtml($body)
-      ->send();
+      ->setBodyHtml($body);
+
+    if ($ical = $this->_ical()) {
+      $attachment = $mail->createAttachment($ical, Class_ICal_SessionActivity::MIME_TYPE);
+      $attachment->filename = 'calendar.ics';
+    }
+
+    $mail->send();
 
     return $this;
   }
 
 
+  protected function _ical() {
+    return (new Class_ICal_SessionActivity($this->_training_session))
+      ->renderCalendar(Class_Profil::getCurrentProfil());
+  }
+
+
   protected function _sendMailToSubscriber() {
     return $this->_sendMail([$this->_subscriber->getMail()],
                             $this->_getSubscriberSubject(),
@@ -114,4 +127,3 @@ abstract class Class_Activity_AbstractMail {
   abstract protected function _getTeachersBody();
 
 }
-?>
\ No newline at end of file
diff --git a/library/Class/Activity/RegistrationMail.php b/library/Class/Activity/RegistrationMail.php
index 4d3fdcbe082..9d35bdcfd2c 100644
--- a/library/Class/Activity/RegistrationMail.php
+++ b/library/Class/Activity/RegistrationMail.php
@@ -22,15 +22,20 @@
 
 class Class_Activity_RegistrationMail extends Class_Activity_AbstractMail {
   protected function _getSubscriberSubject() {
-    return $this->_('Confirmation d\'inscription à l\'activité "%s"',
-                    $this->_training_session->getLibelleActivity());
+    return $this->_fusion('ACTIVITY_REGISTRATION_SUBJECT');
   }
 
 
   protected function _getSubscriberBody() {
-    return $this->_('Bonjour,<br><br>nous vous confirmons votre inscription à l\'activité <a href="%s">"%s"</a>',
-                    $this->_getSessionUrl(),
-                    $this->_training_session->getLibelleActivity());
+    return $this->_fusion('ACTIVITY_REGISTRATION_BODY');
+  }
+
+
+  protected function _fusion($var_key) {
+    return (new Class_ModeleFusion)
+      ->setContenu(Class_AdminVar::getValueOrDefault($var_key))
+      ->setDataSource(['session' => new Class_Activity_RegistrationMail_SessionActivity($this->_training_session, $this->_getSessionUrl())])
+      ->getContenuFusionne();
   }
 
 
@@ -48,6 +53,60 @@ class Class_Activity_RegistrationMail extends Class_Activity_AbstractMail {
                     $this->_getSessionUrl(),
                     $this->_training_session->getLibelleActivity());
   }
+}
+
+
+
+
+class Class_Activity_RegistrationMail_SessionActivity {
+  use Trait_GetterByAttributeName, Trait_Translator;
+
+  protected
+    $_training_session,
+    $_url;
+
+  public function __construct($session, $url) {
+    $this->_training_session = $session;
+    $this->_url = $url;
+  }
+
+
+  public function getUrl() {
+    return $this->_url;
+  }
+
+
+  public function getLibelleActivity() {
+    return $this->_training_session->getLibelleActivity();
+  }
+
+
+  public function getStartLabel() {
+    $start_timestamp = $this->_training_session->getStartTimestamp();
+    $date_start = strftime($this->_('le %d %B %Y'), $start_timestamp);
+    if ('0000' === strftime('%H%M', $start_timestamp))
+      return $date_start;
+
+    return $this->_('%s à %s',
+                    $date_start,
+                    strftime($this->_('%Hh%M'), $start_timestamp));
+  }
 
+
+  public function getLocationLabel() {
+    return ($location = $this->_training_session->getLieu())
+      ? $this->_(' à %s', $location->getLibelle())
+      : '';
+  }
+
+
+  public function getLocationDetails() {
+    return (new ZendAfi_Controller_Action_Helper_View)
+      ->renderLieu($this->_training_session->getLieu());
+  }
+
+
+  public function getContentOrActivityDescription() {
+    return $this->_training_session->getContentOrActivityDescription();
+  }
 }
-?>
\ No newline at end of file
diff --git a/library/Class/AdminVar.php b/library/Class/AdminVar.php
index b371cf2c957..372c917288b 100644
--- a/library/Class/AdminVar.php
+++ b/library/Class/AdminVar.php
@@ -609,7 +609,13 @@ Pour vous désabonner de la lettre d\'information, merci de cliquer sur le lien
             'ACTIVITY_NOTIFICATION_DELAY' => Class_AdminVar_Meta::newDefault($this->_('Délai d\'envoi des rappels pour les activités (en jours)'), ['value' => '0']),
             'ACTIVITY_NOTIFICATION_SUBJECT' => Class_AdminVar_Meta::newDefault($this->_('Sujet de l\'email de rappel pour les activités'), ['value'=> 'Rappel : {session.libelle_activity}']),
             'ACTIVITY_NOTIFICATION_BODY' => Class_AdminVar_Meta::newEditor($this->_('Contenu de l\'email de rappel pour les activités'), ['value' => '<p>Bonjour {stagiaire.nom_complet},</p><p>L\'activité {session.libelle_activity} à laquelle vous êtes inscrit, commencera le {session.date_debut_texte}.</p><p>Cordialement</p>']),
-            'ACTIVITY_NOTIFICATION_IN_QUEUE' => Class_AdminVar_Meta::newDefault($this->_('Message de notification lors de la validation en liste d\'attente'), ['value' => 'Attention: vous avez été placé en liste d\'attente, si des places se libèrent, vous serez averti par Email'])
+            'ACTIVITY_NOTIFICATION_IN_QUEUE' => Class_AdminVar_Meta::newDefault($this->_('Message de notification lors de la validation en liste d\'attente'), ['value' => 'Attention: vous avez été placé en liste d\'attente, si des places se libèrent, vous serez averti par Email']),
+            'ACTIVITY_REGISTRATION_SUBJECT' => Class_AdminVar_Meta::newDefault($this->_('Sujet de l\'email de confirmation d\'inscription'), ['value'=> 'Confirmation d\'inscription à l\'activité "{session.libelle_activity}"']),
+            'ACTIVITY_REGISTRATION_BODY' => Class_AdminVar_Meta::newEditor($this->_('Contenu du mail de confirmation d\'inscription'),
+                                                                           ['value' => 'Bonjour,<br><br>nous vous confirmons votre inscription à l\'activité <a href="{session.url}">"{session.libelle_activity}"</a>.'
+                                                                            . '<p>Elle débutera {session.start_label}{session.location_label}.</p>'
+                                                                            . '<h2>Lieu</h2> {session.location_details}'
+                                                                            . '<h2>Contenu de la session</h2> {session.content_or_activity_description}']),
     ];
   }
 
@@ -1299,6 +1305,11 @@ class Class_AdminVar extends Storm_Model_Abstract {
   }
 
 
+  public function isEditor() {
+    return $this->getMeta()->isEditor();
+  }
+
+
   public function validate() {
     $this->getMeta()->validate($this);
   }
diff --git a/library/Class/ICal/SessionActivity.php b/library/Class/ICal/SessionActivity.php
new file mode 100644
index 00000000000..e8139a554f6
--- /dev/null
+++ b/library/Class/ICal/SessionActivity.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * Copyright (c) 2012-2021, 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 Class_ICal_SessionActivity extends Class_ICal_Abstract {
+  const PREFIX_ID = '//session-activity:';
+
+  public function renderCalendar($profil) {
+    Class_ICal_Autoloader::getInstance()->ensureAutoload();
+
+    if (!$this->_model
+        || !$profil
+        || (!$event = $this->_event($profil)))
+      return '';
+
+    $calendar = $this->_calendar();
+    $calendar->addComponent($event);
+
+    return $calendar->render();
+  }
+
+
+  protected function _calendar() {
+    return new \Eluceo\iCal\Component\Calendar(static::PRODUCT_ID);
+  }
+
+
+  protected function _event($profil) {
+    $dt_start = (new DateTime())->setTimestamp($this->_model->getStartTimestamp());
+
+    $unique_id_on_universe = Class_Url::absolute([], null, true)
+                             . static::PREFIX_ID
+                             . $this->_model->getId();
+    $event = (new \Eluceo\iCal\Component\Event($unique_id_on_universe))
+      ->setUseTimezone(true)
+      ->setDtStart($dt_start)
+      ->setSummary($this->_model->getLibelleActivity())
+      ->setUrl(Class_Url::absolute(['controller' => 'abonne',
+                                    'action' => Class_Template::current()->isLegacy()
+                                    ? 'activities-registered'
+                                    : 'agenda',
+                                    'id_profil' => $profil->getId()],
+                                   null, true));
+
+    if (!$location = $this->_model->getLieu())
+      return $event;
+
+    $event->setLocation($location->getLibelle());
+    $latitude = (string)$location->getLatitude();
+    $longitude = (string)$location->getLongitude();
+    if (strlen($latitude) > 0 && strlen($longitude) > 0)
+      $event->setGeoLocation(new \Eluceo\iCal\Property\Event\Geo($latitude, $longitude));
+
+    return $event;
+  }
+}
diff --git a/library/Class/SessionActivity.php b/library/Class/SessionActivity.php
index d2feca5ea31..a4565470d1b 100644
--- a/library/Class/SessionActivity.php
+++ b/library/Class/SessionActivity.php
@@ -168,6 +168,13 @@ class Class_SessionActivity extends Storm_Model_Abstract {
 	}
 
 
+  public function getContentOrActivityDescription() {
+    return ($content = $this->getContenu())
+      ? $content
+      : $this->getActivityDescription();
+  }
+
+
   public function beforeSave() {
     if (!$this->getDateLimiteFin())
       $this->setDatelimiteFin($this->getDateDebut());
@@ -320,6 +327,11 @@ class Class_SessionActivity extends Storm_Model_Abstract {
   }
 
 
+  public function getStartTimestamp() {
+    return strtotime(parent::_get('date_debut'));
+  }
+
+
   public function getDateFin() {
     return $this->_rangeDateFormat(parent::_get('date_fin'));
   }
@@ -741,11 +753,7 @@ class Class_SessionActivity_Article
 
 
   protected function _contentFrom($model) {
-    $content = ($content = $model->getContenu())
-      ? $content
-      : $model->getActivityDescription();
-
-    return $content
+    return $model->getContentOrActivityDescription()
       . $this->_getInformations($model)
       . $this->_getRegisterLink($model);
   }
diff --git a/library/ZendAfi/Mail.php b/library/ZendAfi/Mail.php
index 8aec63b477f..7ec8adf3860 100644
--- a/library/ZendAfi/Mail.php
+++ b/library/ZendAfi/Mail.php
@@ -77,4 +77,9 @@ class ZendAfi_Mail extends Zend_Mail {
   public function getDecodedSubject() {
     return iconv_mime_decode($this->getSubject());
   }
+
+
+  public function getParts() {
+    return $this->_parts;
+  }
 }
diff --git a/library/ZendAfi/View/Helper/Admin/AdminVar.php b/library/ZendAfi/View/Helper/Admin/AdminVar.php
index c6a1a4eabcb..1161a155f92 100644
--- a/library/ZendAfi/View/Helper/Admin/AdminVar.php
+++ b/library/ZendAfi/View/Helper/Admin/AdminVar.php
@@ -28,6 +28,9 @@ class ZendAfi_View_Helper_Admin_AdminVar extends ZendAfi_View_Helper_BaseHelper
     if ($var->isMultiInput())
       return $this->renderMultiInput($var);
 
+    if ($var->isEditor())
+      return $this->renderEditor($var);
+
     return $this->renderSimple($var);
   }
 
@@ -62,6 +65,12 @@ class ZendAfi_View_Helper_Admin_AdminVar extends ZendAfi_View_Helper_BaseHelper
   }
 
 
+  protected function renderEditor($var) {
+    return $this->_tag('div', $this->view->escape($var->getValeur()),
+                       ['style' => 'width:20em;']);
+  }
+
+
   protected function renderSimple($var) {
     return wordwrap($this->cleanValue($var), 35, '<br />', true);
   }
diff --git a/tests/application/modules/opac/controllers/AbonneControllerActivitiesTest.php b/tests/scenarios/Activities/AbonneControllerActivitiesTest.php
similarity index 90%
rename from tests/application/modules/opac/controllers/AbonneControllerActivitiesTest.php
rename to tests/scenarios/Activities/AbonneControllerActivitiesTest.php
index 5a1f4b939e6..8bfa201fb84 100644
--- a/tests/application/modules/opac/controllers/AbonneControllerActivitiesTest.php
+++ b/tests/scenarios/Activities/AbonneControllerActivitiesTest.php
@@ -1,6 +1,6 @@
 <?php
 /**
- * Copyright (c) 2012, Agence Française Informatique (AFI). All rights reserved.
+ * Copyright (c) 2021, 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
@@ -35,7 +35,7 @@ abstract class AbstractAbonneControllerActivitiesTestCase
     $_session_java_fevrier,
     $_session_java_septembre,
     $_session_python_juillet,
-    $_gallice_cafe,
+    $_galice_cafe,
     $_bib_romains,
     $_bonlieu,
     $_mail_transport;
@@ -57,7 +57,7 @@ abstract class AbstractAbonneControllerActivitiesTestCase
     $this->_mail_transport = new MockMailTransport();
     Zend_Mail::setDefaultTransport($this->_mail_transport);
 
-    $this->_amadou = $this->fixture('Class_Users',
+    $this->_amadou = $this->fixture(Class_Users::class,
                                     ['id' => 435,
                                      'nom' => 'Dou',
                                      'prenom' => 'Ama',
@@ -75,17 +75,17 @@ abstract class AbstractAbonneControllerActivitiesTestCase
 
     ZendAfi_Auth::getInstance()->logUser($this->_amadou);
 
-    $this->_gallice_cafe = $this->fixture('Class_Lieu',
+    $this->_galice_cafe = $this->fixture(Class_Lieu::class,
                                           ['id' => 98,
                                            'libelle' => 'Galice']);
 
 
-    $this->_bib_romains = $this->fixture('Class_Lieu',
+    $this->_bib_romains = $this->fixture(Class_Lieu::class,
                                          ['id' => '99',
                                           'libelle' => 'Bibliothèque des romains']);
 
 
-    $this->_bonlieu = $this->fixture('Class_Lieu',
+    $this->_bonlieu = $this->fixture(Class_Lieu::class,
                                      ['id' => 100,
                                       'libelle' => 'Bonlieu',
                                       'adresse' => "1, rue Jean-Jaures\nBP 294",
@@ -95,29 +95,29 @@ abstract class AbstractAbonneControllerActivitiesTestCase
                                       'longitude' => '6.128715']);
 
     $this->_learn_smalltalk = $this
-      ->fixture('Class_Activity',
+      ->fixture(Class_Activity::class,
                 ['id' => 1,
                  'libelle' => 'Learn Smalltalk']);
 
     $this->_session_smalltalk_janvier = $this
-      ->fixture('Class_SessionActivity',
+      ->fixture(Class_SessionActivity::class,
                 ['id' => 11,
                  'activity' => $this->_learn_smalltalk,
                  'effectif_min' => 1,
                  'effectif_max' => 10,
-                 'lieu' => $this->_gallice_cafe,
+                 'lieu' => $this->_galice_cafe,
                  'date_debut' => '2015-01-10',
                  'date_fin' => '2015-01-10',
                  'date_limite_fin' => '2015-01-10',
                  'stagiaires' => []]);
 
     $this->_session_smalltalk_juillet = $this
-      ->fixture('Class_SessionActivity',
+      ->fixture(Class_SessionActivity::class,
                 ['id' => 12,
                  'activity' => $this->_learn_smalltalk,
                  'effectif_min' => 1,
                  'effectif_max' => 10,
-                 'lieu' => $this->_gallice_cafe,
+                 'lieu' => $this->_galice_cafe,
                  'stagiaires' => [],
                  'date_debut' => '2014-07-11',
                  'date_fin' => '2014-07-15',
@@ -129,13 +129,13 @@ abstract class AbstractAbonneControllerActivitiesTestCase
       ->assertSave();
 
 
-    $this->_learn_java = $this->fixture('Class_Activity',
+    $this->_learn_java = $this->fixture(Class_Activity::class,
                                         ['id' => 3,
                                          'libelle' => 'Learn Java',
                                          'description' => 'whaaat ?']);
 
 
-    $this->_session_java_fevrier = $this->fixture('Class_SessionActivity',
+    $this->_session_java_fevrier = $this->fixture(Class_SessionActivity::class,
                                                   ['id' => 31,
                                                    'activity' => $this->_learn_java,
                                                    'effectif_min' => 2,
@@ -146,7 +146,7 @@ abstract class AbstractAbonneControllerActivitiesTestCase
                                                    'stagiaires' => [],
                                                    'date_limite_fin' => '2015-01-20']);
 
-    $this->_session_java_fevrier->setArticle($this->fixture('Class_Article',
+    $this->_session_java_fevrier->setArticle($this->fixture(Class_Article::class,
                                                             ['id' => 10,
                                                              'titre' => 'Java est mort, vive python !',
                                                              'contenu' => 'Java has been'])
@@ -156,17 +156,18 @@ abstract class AbstractAbonneControllerActivitiesTestCase
                                 ->save();
 
 
-    $this->_session_java_mars = $this->fixture('Class_SessionActivity',
+    $this->_session_java_mars = $this->fixture(Class_SessionActivity::class,
                                                ['id' => 32,
                                                 'activity' => $this->_learn_java,
                                                 'effectif_min' => 2,
                                                 'effectif_max' => 5,
-                                                'lieu' => $this->_gallice_cafe,
-                                                'date_debut' => '2015-03-01',
+                                                'lieu' => $this->_galice_cafe,
+                                                'date_debut' => '2015-03-01 10:00:00',
                                                 'stagiaires' => [],
+                                                'contenu' => 'Comment sortir d\'un environnement toxique',
                                                 'date_limite_fin' => '2015-03-01']);
 
-    $this->_session_java_septembre = $this->fixture('Class_SessionActivity',
+    $this->_session_java_septembre = $this->fixture(Class_SessionActivity::class,
                                                     ['id' => 30,
                                                      'activity' => $this->_learn_java,
                                                      'effectif_min' => 2,
@@ -174,7 +175,7 @@ abstract class AbstractAbonneControllerActivitiesTestCase
                                                      'date_debut' => '2014-09-10',
                                                      'date_fin' => '2014-09-10',
                                                      'stagiaires' => [],
-                                                     'lieu' => $this->_gallice_cafe,
+                                                     'lieu' => $this->_galice_cafe,
                                                      'date_limite_fin' => '2014-09-10']);
     $this->_session_java_septembre->beAnnule();
 
@@ -185,12 +186,12 @@ abstract class AbstractAbonneControllerActivitiesTestCase
       ->assertSave();
 
 
-    $this->_learn_python = $this->fixture('Class_Activity',
+    $this->_learn_python = $this->fixture(Class_Activity::class,
                                           ['id' => 12,
                                            'libelle' => 'Learn Python']);
 
     $this->_session_python_juillet = $this
-      ->fixture('Class_SessionActivity',
+      ->fixture(Class_SessionActivity::class,
                 ['id' => 121,
                  'activity' => $this->_learn_python,
                  'date_debut' => '2014-07-10',
@@ -204,7 +205,7 @@ abstract class AbstractAbonneControllerActivitiesTestCase
                  'horaires' => '8h-12h, 14h-18h',
                  'lieu' => $this->_bib_romains,
                  'stagiaires' => [$this->_amadou],
-                 'intervenants' => [$this->fixture('Class_Users',
+                 'intervenants' => [$this->fixture(Class_Users::class,
                                                    ['id' =>76,
                                                     'login' => 'jpp',
                                                     'password' => 'pwd',
@@ -563,7 +564,7 @@ class AbonneControllerActivitiesFicheAbonneWithRightRegisterTest
                             ['id' => 7987,
                              'rights' => [Class_UserGroup::RIGHT_REGISTER_ACTIVITY]]);
 
-    $redacteur = $this->fixture('Class_Users',
+    $redacteur = $this->fixture(Class_Users::class,
                                 ['id' => 1654,
                                  'login' => 'redacteur',
                                  'password' => 'redacteur',
@@ -757,6 +758,36 @@ class AbonneControllerActivitiesAmadouInscritSessionMarsJavaClosedTest
 
 
 
+class AbonneControllerActivitiesAmadouInscritSessionMarsJavaOpenedTest extends AbstractAbonneControllerActivitiesTestCase {
+  protected $_first_mail_body;
+
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/opac/abonne/inscrire-session/id/32');
+    $this->_first_mail_body = quoted_printable_decode($this
+                                                      ->_mail_transport
+                                                      ->getSentMails()[0]
+                                                      ->getBodyHtml(true));
+  }
+
+
+  /** @test */
+  public function firstMailBodyShouldContainsElleDebuteraLe01Mars2015A10h00() {
+    $this->assertContains('Elle débutera le 01 mars 2015 à 10h00 à Galice',
+                          $this->_first_mail_body);
+  }
+
+
+  /** @test */
+  public function firstMailBodyShouldContainsCommentSortirDunEnvironnementToxique() {
+    $this->assertContains('Comment sortir d\'un environnement toxique',
+                          $this->_first_mail_body);
+  }
+}
+
+
+
+
 class AbonneControllerActivitiesAmadouInscritSessionMarsJavaNotOpenTest
   extends AbstractAbonneControllerActivitiesTestCase {
 
@@ -793,9 +824,13 @@ class AbonneControllerActivitiesAmadouInscritSessionMarsJavaNotOpenTest
 
 
 
-class AbonneControllerActivitiesAmadouInscritSessionFebruaryJavaOpenTest extends AbstractAbonneControllerActivitiesTestCase {
+class AbonneControllerActivitiesAmadouInscritSessionFebruaryJavaOpenTest
+  extends AbstractAbonneControllerActivitiesTestCase {
+
   protected
-    $_mails;
+    $_mails,
+    $_first_mail_body;
+
 
   public function setUp() {
     parent::setUp();
@@ -804,12 +839,13 @@ class AbonneControllerActivitiesAmadouInscritSessionFebruaryJavaOpenTest extends
       ->setIntervenants($this->_session_python_juillet
                         ->getIntervenants())->save();
 
-    $this->dispatch('/opac/abonne/inscrire-session/id/31', true);
+    $this->dispatch('/opac/abonne/inscrire-session/id/31');
 
     Class_SessionActivity::clearCache();
     Class_Article::clearCache();
 
     $this->_mails = $this->_mail_transport->getSentMails();
+    $this->_first_mail_body = quoted_printable_decode($this->_mails[0]->getBodyHtml(true));
   }
 
 
@@ -829,7 +865,7 @@ class AbonneControllerActivitiesAmadouInscritSessionFebruaryJavaOpenTest extends
 
 
   /** @test */
-  function answerShouldRedirectToActivityList() {
+  public function answerShouldRedirectToActivityList() {
     $this->assertRedirectTo('/activities');
   }
 
@@ -843,7 +879,7 @@ class AbonneControllerActivitiesAmadouInscritSessionFebruaryJavaOpenTest extends
 
 
   /** @test */
-  function aNewInscriptionShouldHaveBeenCreated() {
+  public function aNewInscriptionShouldHaveBeenCreated() {
     $this->assertCount(1, Class_SessionActivityInscription::findAllBy(['stagiaire_id' => 435,
                                                                         'session_activity_id' => 31]));
   }
@@ -862,7 +898,7 @@ class AbonneControllerActivitiesAmadouInscritSessionFebruaryJavaOpenTest extends
 
 
   /** @test */
-  public function firtMailFromShouldBeNoReplyAtBokeh() {
+  public function firstMailFromShouldBeNoReplyAtBokeh() {
     $this->assertEquals('no-reply@bokeh.fr', $this->_mails[0]->getFrom());
   }
 
@@ -876,16 +912,49 @@ class AbonneControllerActivitiesAmadouInscritSessionFebruaryJavaOpenTest extends
 
 
   /** @test */
-  public function firstMailBodyShouldBeWeConfirm() {
+  public function firstMailBodyShouldContainsNousVousConfirmons() {
     $this->assertContains('nous vous confirmons',
-                          $this->_mails[0]->getBodyHtml(true));
+                          $this->_first_mail_body);
   }
 
 
   /** @test */
   public function firstMailBodyShouldContainsUrlToDetailSession() {
     $this->assertContains('http://localhost' . BASE_URL . '/activities/detail-session/id/31"',
-                          $this->_mails[0]->getBodyHtml(true));
+                          $this->_first_mail_body);
+  }
+
+
+  /** @test */
+  public function firstMailBodyShouldContainsElleDebuteraLe10fevrier2015() {
+    $this->assertContains('Elle débutera le 10 février 2015 à Bonlieu',
+                          $this->_first_mail_body);
+  }
+
+
+  /** @test */
+  public function firstMailBodyShouldContainsMapForBonlieu() {
+    $this->assertContains('1, rue Jean-Jaures<br />',
+                          $this->_first_mail_body);
+  }
+
+
+  /** @test */
+  public function firstMailBodyShouldContainsActivityDescriptionWhaaat() {
+    $this->assertContains('whaaat ?', $this->_first_mail_body);
+  }
+
+
+  /** @test */
+  public function firstMailShouldHaveIcalAttachment() {
+    $this->assertEquals(1, count($parts = $this->_mails[0]->getParts()));
+    $part = $parts[0];
+    $this->assertEquals(['text/calendar;charset=utf-8',
+                         'base64',
+                         'attachment'],
+                        [$part->type,
+                         $part->encoding,
+                         $part->disposition]);
   }
 
 
@@ -927,14 +996,14 @@ class AbonneControllerActivitiesAmadouWithoutMailInscritSessionFebruaryJavaOpenT
     parent::setUp();
 
     $this->_session_java_fevrier
-      ->addIntervenant($this->fixture('Class_Users',
+      ->addIntervenant($this->fixture(Class_Users::class,
                                       ['id' =>78,
                                        'login' => 'pat',
                                        'password' => 'bator',
                                        'nom' => 'Bator',
                                        'prenom' => 'Pat',
                                        'mail' => 'pat@bat.org']))
-      ->addIntervenant($this->fixture('Class_Users',
+      ->addIntervenant($this->fixture(Class_Users::class,
                                       ['id' =>79,
                                        'login' => 'captain',
                                        'password' => 'flam',
diff --git a/tests/scenarios/Activities/ActivitiesWithQueueAdminTest.php b/tests/scenarios/Activities/ActivitiesWithQueueAdminTest.php
index 00a05fb5a21..f96f667c0b5 100644
--- a/tests/scenarios/Activities/ActivitiesWithQueueAdminTest.php
+++ b/tests/scenarios/Activities/ActivitiesWithQueueAdminTest.php
@@ -404,9 +404,33 @@ class ActivitiesWithQueueAdminEditQuotasOnSessionTest
 
 
   /** @test */
-  public function mailShouldBeSentToChichiro() {
-    $mail = $this->_mail_transport->getSentMails()[0];
-    $this->assertContains('nous vous confirmons votre inscription', $mail->getBodyHtml(true));
+  public function shouldHaveSent2Mails() {
+    $this->assertEquals(2, count($this->_mail_transport->getSentMails()));
+  }
+
+  /** @test */
+  public function firstMailShouldContainsNousVousConfirmonsVotreInscription() {
+    $this->assertContains('nous vous confirmons votre inscription',
+                          $this->_mail_transport->getSentMails()[0]->getBodyHtml(true));
+  }
+
+
+  public function mailRecipients() {
+    return [['chichi@ro.org'],
+            ['po@nyo.org']];
+  }
+
+
+  /**
+   * @test
+   * @dataProvider mailRecipients
+   */
+  public function mailShouldBeSentToRecipient($recipient) {
+    $recipients = [];
+    foreach($this->_mail_transport->getSentMails() as $mail)
+      $recipients = array_merge($recipients, $mail->getRecipients());
+
+    $this->assertContains($recipient, $recipients, json_encode($recipients, JSON_PRETTY_PRINT));
   }
 }
 
diff --git a/tests/scenarios/Activities/ActivityIcalTest.php b/tests/scenarios/Activities/ActivityIcalTest.php
new file mode 100644
index 00000000000..6cbb002e41e
--- /dev/null
+++ b/tests/scenarios/Activities/ActivityIcalTest.php
@@ -0,0 +1,126 @@
+<?php
+/**
+ * Copyright (c) 2012-2021, 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 ActivityIcalTest extends ModelTestCase {
+  protected $_ical;
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixture(Class_Profil::class, ['id' => 22])
+         ->beCurrentProfil();
+
+    $activity = $this->fixture(Class_Activity::class,
+                               ['id' => 333,
+                                'libelle' => 'Festival animation']);
+
+    $session_activity = $this
+      ->fixture(Class_SessionActivity::class,
+                ['id' => 444,
+                 'activity' => $activity,
+                 'date_debut' => '2012-03-27 13:00:00',
+                 'date_fin' => '2012-03-27 18:00:00',
+                 'intervenants' => [],
+                 'lieu' => $this->fixture(Class_Lieu::class,
+                                          ['id' => 3,
+                                           'libelle' => 'Annecy',
+                                           'latitude' => '45.916',
+                                           'longitude' => '6.133']),
+                ]);
+
+    $activity
+      ->setSessions([$session_activity])
+      ->assertSave();
+
+    $this->_ical = (new Class_ICal_SessionActivity($session_activity))
+      ->renderCalendar(Class_Profil::getCurrentProfil());
+  }
+
+
+  /** @test */
+  public function uidShouldBeSessionActivity444() {
+    $this->assertRegExp('/^UID:.*session-activity:444/m', $this->_ical);
+  }
+
+
+  /** @test */
+  public function summaryShouldBeFestivalAnimation() {
+    $this->assertRegExp('/^SUMMARY:Festival animation/m', $this->_ical);
+  }
+
+
+  /** @test */
+  public function dtstartShouldBe20120327T000000InTimeZoneParis() {
+    $this->assertRegExp('/^DTSTART;TZID=Europe\/Paris:20120327T130000/m', $this->_ical);
+  }
+
+
+  /** @test */
+  public function urlShouldBeAbonneActivitiesRegistered() {
+    $this->assertRegExp('#^URL:.*/abonne/activities-registered/id_profil/22#m', $this->_ical);
+  }
+
+
+  /** @test */
+  public function locationShouldBeAnnecy() {
+    $this->assertRegExp('/^LOCATION:Annecy/m', $this->_ical);
+  }
+
+
+  /** @test */
+  public function geoShouldBeLat45Long6() {
+    $this->assertRegExp('/^GEO:45.916000;6.133000/m', $this->_ical);
+  }
+
+
+
+  protected function _renderICalWithLatitudeAndLongitude($lat, $long) {
+    $session = Class_SessionActivity::find(444);
+    $session->getLieu()
+            ->setLatitude($lat)
+            ->setLongitude($long);
+
+    return (new Class_ICal_SessionActivity($session))
+      ->renderCalendar(Class_Profil::getCurrentProfil());
+  }
+
+
+  /** @test */
+  public function withLongitudeAndLatitudeZeroICalGE0ShouldBeZeroAndZero() {
+    $this->assertRegExp('/^GEO:0.000000;0.000000/m',
+                        $this->_renderICalWithLatitudeAndLongitude('0', '0'));
+  }
+
+
+  /** @test */
+  public function withLongitudeAndLatitudeEmptyICalShouldNotContainsGEO() {
+    $this->assertNotContains('GEO',
+                             $this->_renderICalWithLatitudeAndLongitude('', ''));
+  }
+
+
+  /** @test */
+  public function withLongitudeAndLatitudeNullICalShouldNotContainsGEO() {
+    $this->assertNotContains('GEO',
+                             $this->_renderICalWithLatitudeAndLongitude(null, null));
+  }
+}
-- 
GitLab