From 719d45e1fe5a32d039713992311ff76b36460a3f Mon Sep 17 00:00:00 2001
From: dahoud <41957584+DahoudG@users.noreply.github.com>
Date: Wed, 15 Apr 2026 20:23:04 +0000
Subject: [PATCH] =?UTF-8?q?feat(messaging):=20module=20messagerie=20unifi?=
=?UTF-8?q?=C3=A9=20avec=20contact=20policies=20+=20member=20blocks?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Refactor complet : fusion de Conversation + Message en un module Messaging unique
avec ContactPolicy (règles qui-peut-parler-à-qui) et MemberBlock (blocages utilisateur).
- Migration V28 : tables conversations/conversation_participants/messages/
contact_policies/member_blocks
- Nouvelles entités : ContactPolicy, ConversationParticipant, MemberBlock
(Conversation/Message mises à jour avec relations)
- Nouvelles repositories : ContactPolicyRepository, ConversationParticipantRepository,
MemberBlockRepository
- MessagingResource (nouveau) remplace ConversationResource + MessageResource
- MessagingService (nouveau) remplace ConversationService + MessageService
avec vérifications appartenance org + policies + blocages avant envoi
- Anciens fichiers Conversation/Message Resource/Service/Tests supprimés
---
.../server/entity/ContactPolicy.java | 91 +++
.../unionflow/server/entity/Conversation.java | 190 ++---
.../entity/ConversationParticipant.java | 91 +++
.../unionflow/server/entity/MemberBlock.java | 68 ++
.../unionflow/server/entity/Message.java | 220 +++---
.../repository/ContactPolicyRepository.java | 27 +
.../ConversationParticipantRepository.java | 44 ++
.../repository/ConversationRepository.java | 96 +--
.../repository/MemberBlockRepository.java | 51 ++
.../server/repository/MessageRepository.java | 82 ++-
.../server/resource/ConversationResource.java | 145 ----
.../server/resource/MessageResource.java | 120 ----
.../server/resource/MessagingResource.java | 200 ++++++
.../server/service/ConversationService.java | 208 ------
.../server/service/MessageService.java | 203 ------
.../server/service/MessagingService.java | 653 ++++++++++++++++++
.../V28__Create_Messagerie_Tables.sql | 237 +++++++
.../server/entity/ConversationTest.java | 197 ++++--
.../unionflow/server/entity/MessageTest.java | 174 ++++-
.../ConversationRepositoryTest.java | 150 ++--
.../repository/MessageRepositoryTest.java | 237 +------
.../resource/ConversationResourceTest.java | 377 ----------
.../server/resource/MessageResourceTest.java | 374 ----------
.../service/ConversationServiceTest.java | 562 ---------------
.../server/service/MessageServiceTest.java | 621 -----------------
25 files changed, 2120 insertions(+), 3298 deletions(-)
create mode 100644 src/main/java/dev/lions/unionflow/server/entity/ContactPolicy.java
create mode 100644 src/main/java/dev/lions/unionflow/server/entity/ConversationParticipant.java
create mode 100644 src/main/java/dev/lions/unionflow/server/entity/MemberBlock.java
create mode 100644 src/main/java/dev/lions/unionflow/server/repository/ContactPolicyRepository.java
create mode 100644 src/main/java/dev/lions/unionflow/server/repository/ConversationParticipantRepository.java
create mode 100644 src/main/java/dev/lions/unionflow/server/repository/MemberBlockRepository.java
delete mode 100644 src/main/java/dev/lions/unionflow/server/resource/ConversationResource.java
delete mode 100644 src/main/java/dev/lions/unionflow/server/resource/MessageResource.java
create mode 100644 src/main/java/dev/lions/unionflow/server/resource/MessagingResource.java
delete mode 100644 src/main/java/dev/lions/unionflow/server/service/ConversationService.java
delete mode 100644 src/main/java/dev/lions/unionflow/server/service/MessageService.java
create mode 100644 src/main/java/dev/lions/unionflow/server/service/MessagingService.java
create mode 100644 src/main/resources/db/migration/V28__Create_Messagerie_Tables.sql
delete mode 100644 src/test/java/dev/lions/unionflow/server/resource/ConversationResourceTest.java
delete mode 100644 src/test/java/dev/lions/unionflow/server/resource/MessageResourceTest.java
delete mode 100644 src/test/java/dev/lions/unionflow/server/service/ConversationServiceTest.java
delete mode 100644 src/test/java/dev/lions/unionflow/server/service/MessageServiceTest.java
diff --git a/src/main/java/dev/lions/unionflow/server/entity/ContactPolicy.java b/src/main/java/dev/lions/unionflow/server/entity/ContactPolicy.java
new file mode 100644
index 0000000..33d5ac9
--- /dev/null
+++ b/src/main/java/dev/lions/unionflow/server/entity/ContactPolicy.java
@@ -0,0 +1,91 @@
+package dev.lions.unionflow.server.entity;
+
+import dev.lions.unionflow.server.api.enums.messagerie.TypePolitiqueCommunication;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.Index;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.PrePersist;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+/**
+ * Politique de communication d'une organisation.
+ *
+ *
Chaque organisation possède exactement une politique, créée automatiquement
+ * lors de la création de l'organisation avec les valeurs par défaut.
+ * L'administrateur peut la modifier via l'API.
+ *
+ *
Table : {@code contact_policies}
+ *
+ * @author UnionFlow Team
+ * @version 4.0
+ * @since 2026-04-13
+ */
+@Entity
+@Table(
+ name = "contact_policies",
+ indexes = {
+ @Index(name = "idx_contact_policies_org", columnList = "organisation_id")
+ },
+ uniqueConstraints = {
+ @UniqueConstraint(name = "uk_contact_policy_org", columnNames = "organisation_id")
+ }
+)
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@EqualsAndHashCode(callSuper = true)
+public class ContactPolicy extends BaseEntity {
+
+ @NotNull
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "organisation_id", nullable = false)
+ private Organisation organisation;
+
+ @Enumerated(EnumType.STRING)
+ @Builder.Default
+ @Column(name = "type_politique", nullable = false, length = 30)
+ private TypePolitiqueCommunication typePolitique = TypePolitiqueCommunication.OUVERT;
+
+ @Builder.Default
+ @Column(name = "autoriser_membre_vers_membre", nullable = false)
+ private Boolean autoriserMembreVersMembre = Boolean.TRUE;
+
+ @Builder.Default
+ @Column(name = "autoriser_membre_vers_role", nullable = false)
+ private Boolean autoriserMembreVersRole = Boolean.TRUE;
+
+ @Builder.Default
+ @Column(name = "autoriser_notes_vocales", nullable = false)
+ private Boolean autoriserNotesVocales = Boolean.TRUE;
+
+ @PrePersist
+ @Override
+ protected void onCreate() {
+ super.onCreate();
+ if (typePolitique == null) {
+ typePolitique = TypePolitiqueCommunication.OUVERT;
+ }
+ if (autoriserMembreVersMembre == null) {
+ autoriserMembreVersMembre = Boolean.TRUE;
+ }
+ if (autoriserMembreVersRole == null) {
+ autoriserMembreVersRole = Boolean.TRUE;
+ }
+ if (autoriserNotesVocales == null) {
+ autoriserNotesVocales = Boolean.TRUE;
+ }
+ }
+}
diff --git a/src/main/java/dev/lions/unionflow/server/entity/Conversation.java b/src/main/java/dev/lions/unionflow/server/entity/Conversation.java
index 01a7300..cba0ab8 100644
--- a/src/main/java/dev/lions/unionflow/server/entity/Conversation.java
+++ b/src/main/java/dev/lions/unionflow/server/entity/Conversation.java
@@ -1,119 +1,129 @@
package dev.lions.unionflow.server.entity;
-import dev.lions.unionflow.server.api.enums.communication.ConversationType;
-import jakarta.persistence.*;
-import lombok.Getter;
-import lombok.Setter;
-
+import dev.lions.unionflow.server.api.enums.messagerie.StatutConversation;
+import dev.lions.unionflow.server.api.enums.messagerie.TypeConversation;
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.Index;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.PrePersist;
+import jakarta.persistence.Table;
+import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
/**
- * Entité Conversation pour le système de messagerie UnionFlow.
- * Représente un fil de discussion entre membres.
+ * Fil de discussion entre membres d'une organisation.
+ *
+ *
Deux types sont supportés en V1 :
+ *
+ * - {@link TypeConversation#DIRECTE} — 1-1 entre deux membres
+ * - {@link TypeConversation#ROLE_CANAL} — membre vers un rôle officiel
+ * (PRESIDENT, TRESORIER, SECRETAIRE…). Tous les porteurs du rôle répondent.
+ *
+ *
+ * Table : {@code conversations}
*
* @author UnionFlow Team
- * @version 1.0
- * @since 2026-03-16
+ * @version 4.0
+ * @since 2026-04-13
*/
@Entity
-@Table(name = "conversations", indexes = {
- @Index(name = "idx_conversation_organisation", columnList = "organisation_id"),
- @Index(name = "idx_conversation_type", columnList = "type"),
- @Index(name = "idx_conversation_archived", columnList = "is_archived"),
- @Index(name = "idx_conversation_created", columnList = "date_creation")
-})
-@Getter
-@Setter
+@Table(
+ name = "conversations",
+ indexes = {
+ @Index(name = "idx_conversations_organisation", columnList = "organisation_id"),
+ @Index(name = "idx_conversations_statut", columnList = "statut"),
+ @Index(name = "idx_conversations_dernier_msg", columnList = "dernier_message_at")
+ }
+)
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@EqualsAndHashCode(callSuper = true)
public class Conversation extends BaseEntity {
- /**
- * Nom de la conversation
- */
- @Column(name = "name", nullable = false, length = 255)
- private String name;
-
- /**
- * Description optionnelle
- */
- @Column(name = "description", length = 1000)
- private String description;
-
- /**
- * Type de conversation (INDIVIDUAL, GROUP, BROADCAST, ANNOUNCEMENT)
- */
- @Enumerated(EnumType.STRING)
- @Column(name = "type", nullable = false, length = 20)
- private ConversationType type;
-
- /**
- * Organisation associée (optionnelle)
- */
+ @NotNull
@ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "organisation_id")
+ @JoinColumn(name = "organisation_id", nullable = false)
private Organisation organisation;
- /**
- * URL de l'avatar de la conversation
- */
- @Column(name = "avatar_url", length = 500)
- private String avatarUrl;
+ @NotNull
+ @Enumerated(EnumType.STRING)
+ @Column(name = "type_conversation", nullable = false, length = 30)
+ private TypeConversation typeConversation;
/**
- * Conversation muette
+ * Rôle cible pour les ROLE_CANAL (ex : "TRESORIER", "PRESIDENT").
+ * Null pour les conversations DIRECTE.
*/
- @Column(name = "is_muted", nullable = false)
- private Boolean isMuted = false;
+ @Column(name = "role_cible", length = 50)
+ private String roleCible;
- /**
- * Conversation épinglée
- */
- @Column(name = "is_pinned", nullable = false)
- private Boolean isPinned = false;
+ /** Titre affiché (nom du rôle ou du groupe, null pour DIRECTE). */
+ @Column(name = "titre", length = 200)
+ private String titre;
- /**
- * Conversation archivée
- */
- @Column(name = "is_archived", nullable = false)
- private Boolean isArchived = false;
+ @Enumerated(EnumType.STRING)
+ @Builder.Default
+ @Column(name = "statut", nullable = false, length = 20)
+ private StatutConversation statut = StatutConversation.ACTIVE;
- /**
- * Métadonnées additionnelles (JSON)
- */
- @Column(name = "metadata", columnDefinition = "TEXT")
- private String metadata;
+ @Column(name = "dernier_message_at")
+ private LocalDateTime dernierMessageAt;
- /**
- * Date de dernière mise à jour
- */
- @Column(name = "updated_at")
- private LocalDateTime updatedAt;
+ @Builder.Default
+ @Column(name = "nombre_messages", nullable = false)
+ private Integer nombreMessages = 0;
- /**
- * Participants de la conversation (many-to-many)
- */
- @ManyToMany(fetch = FetchType.LAZY)
- @JoinTable(
- name = "conversation_participants",
- joinColumns = @JoinColumn(name = "conversation_id"),
- inverseJoinColumns = @JoinColumn(name = "membre_id")
- )
- private List participants = new ArrayList<>();
+ @Builder.Default
+ @OneToMany(mappedBy = "conversation", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
+ private List participants = new ArrayList<>();
- /**
- * Messages de la conversation (one-to-many)
- */
- @OneToMany(mappedBy = "conversation", cascade = CascadeType.ALL, orphanRemoval = true)
+ @Builder.Default
+ @OneToMany(mappedBy = "conversation", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List messages = new ArrayList<>();
- /**
- * Met à jour le timestamp
- */
- @PreUpdate
- protected void onUpdate() {
- this.updatedAt = LocalDateTime.now();
+ @PrePersist
+ @Override
+ protected void onCreate() {
+ super.onCreate();
+ if (statut == null) {
+ statut = StatutConversation.ACTIVE;
+ }
+ if (nombreMessages == null) {
+ nombreMessages = 0;
+ }
+ }
+
+ // ── Méthodes métier ───────────────────────────────────────────────────────
+
+ /** Retourne true si la conversation accepte encore de nouveaux messages. */
+ public boolean estActive() {
+ return StatutConversation.ACTIVE.equals(statut);
+ }
+
+ /** Archive la conversation — plus aucun message n'est accepté. */
+ public void archiver() {
+ this.statut = StatutConversation.ARCHIVEE;
+ }
+
+ /** Incrémente le compteur et met à jour l'horodatage du dernier message. */
+ public void enregistrerNouveauMessage() {
+ this.nombreMessages = (this.nombreMessages == null ? 0 : this.nombreMessages) + 1;
+ this.dernierMessageAt = LocalDateTime.now();
}
}
diff --git a/src/main/java/dev/lions/unionflow/server/entity/ConversationParticipant.java b/src/main/java/dev/lions/unionflow/server/entity/ConversationParticipant.java
new file mode 100644
index 0000000..f160e86
--- /dev/null
+++ b/src/main/java/dev/lions/unionflow/server/entity/ConversationParticipant.java
@@ -0,0 +1,91 @@
+package dev.lions.unionflow.server.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.Index;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import jakarta.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+/**
+ * Participation d'un membre à une conversation.
+ *
+ * Stocke l'état de lecture individuel ({@code luJusqua}) et
+ * les préférences de notification du participant.
+ *
+ *
Table : {@code conversation_participants}
+ *
+ * @author UnionFlow Team
+ * @version 4.0
+ * @since 2026-04-13
+ */
+@Entity
+@Table(
+ name = "conversation_participants",
+ indexes = {
+ @Index(name = "idx_conv_part_conversation", columnList = "conversation_id"),
+ @Index(name = "idx_conv_part_membre", columnList = "membre_id")
+ },
+ uniqueConstraints = {
+ @UniqueConstraint(name = "uk_conv_participant",
+ columnNames = {"conversation_id", "membre_id"})
+ }
+)
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@EqualsAndHashCode(callSuper = true)
+public class ConversationParticipant extends BaseEntity {
+
+ @NotNull
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "conversation_id", nullable = false)
+ private Conversation conversation;
+
+ @NotNull
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "membre_id", nullable = false)
+ private Membre membre;
+
+ /**
+ * Rôle de ce participant dans la conversation.
+ * Ex : INITIATEUR, PARTICIPANT, MODERATEUR.
+ */
+ @Builder.Default
+ @Column(name = "role_dans_conversation", length = 50)
+ private String roleDansConversation = "PARTICIPANT";
+
+ /**
+ * Horodatage du dernier message lu.
+ * Permet de calculer le nombre de messages non lus.
+ */
+ @Column(name = "lu_jusqu_a")
+ private LocalDateTime luJusqua;
+
+ /** Si false, ce participant ne reçoit plus de notifications pour cette conversation. */
+ @Builder.Default
+ @Column(name = "notifier", nullable = false)
+ private Boolean notifier = Boolean.TRUE;
+
+ // ── Méthodes métier ───────────────────────────────────────────────────────
+
+ /** Marque tous les messages jusqu'à maintenant comme lus. */
+ public void marquerLu() {
+ this.luJusqua = LocalDateTime.now();
+ }
+
+ /** Retourne true si ce participant est l'initiateur de la conversation. */
+ public boolean estInitiateur() {
+ return "INITIATEUR".equals(roleDansConversation);
+ }
+}
diff --git a/src/main/java/dev/lions/unionflow/server/entity/MemberBlock.java b/src/main/java/dev/lions/unionflow/server/entity/MemberBlock.java
new file mode 100644
index 0000000..b1eea21
--- /dev/null
+++ b/src/main/java/dev/lions/unionflow/server/entity/MemberBlock.java
@@ -0,0 +1,68 @@
+package dev.lions.unionflow.server.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.Index;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import java.util.UUID;
+
+/**
+ * Blocage unilatéral entre deux membres au sein d'une organisation.
+ *
+ *
Un membre bloqué ne peut plus envoyer de messages au bloqueur.
+ * Le blocage est limité à une organisation (un membre bloqué dans l'asso X
+ * peut encore écrire dans la tontine Y).
+ *
+ *
Table : {@code member_blocks}
+ *
+ * @author UnionFlow Team
+ * @version 4.0
+ * @since 2026-04-13
+ */
+@Entity
+@Table(
+ name = "member_blocks",
+ indexes = {
+ @Index(name = "idx_member_blocks_bloqueur", columnList = "bloqueur_id"),
+ @Index(name = "idx_member_blocks_bloque", columnList = "bloque_id, organisation_id")
+ },
+ uniqueConstraints = {
+ @UniqueConstraint(name = "uk_member_block",
+ columnNames = {"bloqueur_id", "bloque_id", "organisation_id"})
+ }
+)
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@EqualsAndHashCode(callSuper = true)
+public class MemberBlock extends BaseEntity {
+
+ /** Membre qui effectue le blocage */
+ @NotNull
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "bloqueur_id", nullable = false)
+ private Membre bloqueur;
+
+ /** Membre qui est bloqué */
+ @NotNull
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "bloque_id", nullable = false)
+ private Membre bloque;
+
+ /** Organisation dans laquelle le blocage est actif */
+ @NotNull
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "organisation_id", nullable = false)
+ private Organisation organisation;
+}
diff --git a/src/main/java/dev/lions/unionflow/server/entity/Message.java b/src/main/java/dev/lions/unionflow/server/entity/Message.java
index 3991018..e283000 100644
--- a/src/main/java/dev/lions/unionflow/server/entity/Message.java
+++ b/src/main/java/dev/lions/unionflow/server/entity/Message.java
@@ -1,156 +1,140 @@
package dev.lions.unionflow.server.entity;
-import dev.lions.unionflow.server.api.enums.communication.MessagePriority;
-import dev.lions.unionflow.server.api.enums.communication.MessageStatus;
-import dev.lions.unionflow.server.api.enums.communication.MessageType;
-import jakarta.persistence.*;
-import lombok.Getter;
-import lombok.Setter;
-
+import dev.lions.unionflow.server.api.enums.messagerie.TypeContenu;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.Index;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.PrePersist;
+import jakarta.persistence.Table;
+import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
/**
- * Entité Message pour le système de messagerie UnionFlow.
- * Représente un message individuel dans une conversation.
+ * Message envoyé dans une conversation.
+ *
+ *
Supporte trois types de contenu :
+ *
+ * - {@link TypeContenu#TEXTE} — message texte classique
+ * - {@link TypeContenu#VOCAL} — note vocale (Opus/AAC), stockée sur object storage.
+ * Champs {@code urlFichier} + {@code dureeAudio} obligatoires.
+ * - {@link TypeContenu#IMAGE} — image JPEG/PNG. Champ {@code urlFichier} obligatoire.
+ *
+ *
+ * La suppression est douce : {@code supprimeLe} est renseigné au lieu de
+ * supprimer la ligne. Le contenu devient {@code "[Message supprimé]"}.
+ *
+ *
Table : {@code messages}
*
* @author UnionFlow Team
- * @version 1.0
- * @since 2026-03-16
+ * @version 4.0
+ * @since 2026-04-13
*/
@Entity
-@Table(name = "messages", indexes = {
- @Index(name = "idx_message_conversation", columnList = "conversation_id"),
- @Index(name = "idx_message_sender", columnList = "sender_id"),
- @Index(name = "idx_message_organisation", columnList = "organisation_id"),
- @Index(name = "idx_message_status", columnList = "status"),
- @Index(name = "idx_message_created", columnList = "date_creation"),
- @Index(name = "idx_message_deleted", columnList = "is_deleted")
-})
-@Getter
-@Setter
+@Table(
+ name = "messages",
+ indexes = {
+ @Index(name = "idx_messages_conversation", columnList = "conversation_id"),
+ @Index(name = "idx_messages_expediteur", columnList = "expediteur_id"),
+ @Index(name = "idx_messages_date_creation", columnList = "date_creation"),
+ @Index(name = "idx_messages_parent", columnList = "message_parent_id")
+ }
+)
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@EqualsAndHashCode(callSuper = true)
public class Message extends BaseEntity {
- /**
- * Conversation parente
- */
+ @NotNull
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "conversation_id", nullable = false)
private Conversation conversation;
- /**
- * Expéditeur du message
- */
+ @NotNull
@ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "sender_id", nullable = false)
- private Membre sender;
+ @JoinColumn(name = "expediteur_id", nullable = false)
+ private Membre expediteur;
- /**
- * Nom de l'expéditeur (dénormalisé pour performance)
- */
- @Column(name = "sender_name", nullable = false, length = 255)
- private String senderName;
-
- /**
- * Avatar de l'expéditeur (dénormalisé)
- */
- @Column(name = "sender_avatar", length = 500)
- private String senderAvatar;
-
- /**
- * Contenu du message
- */
- @Column(name = "content", nullable = false, columnDefinition = "TEXT")
- private String content;
-
- /**
- * Type de message (INDIVIDUAL, BROADCAST, TARGETED, SYSTEM)
- */
@Enumerated(EnumType.STRING)
- @Column(name = "type", nullable = false, length = 20)
- private MessageType type;
+ @Builder.Default
+ @Column(name = "type_message", nullable = false, length = 20)
+ private TypeContenu typeMessage = TypeContenu.TEXTE;
+
+ /** Texte du message — null pour les vocaux/images. */
+ @Column(name = "contenu", columnDefinition = "TEXT")
+ private String contenu;
/**
- * Statut du message (SENT, DELIVERED, READ, FAILED)
+ * URL du fichier audio (notes vocales) ou image.
+ * Format : https://storage.lions.dev/chat/{conversationId}/{messageId}.opus
*/
- @Enumerated(EnumType.STRING)
- @Column(name = "status", nullable = false, length = 20)
- private MessageStatus status;
+ @Column(name = "url_fichier", length = 500)
+ private String urlFichier;
+
+ /** Durée en secondes pour les notes vocales. */
+ @Column(name = "duree_audio")
+ private Integer dureeAudio;
/**
- * Priorité du message (NORMAL, HIGH, URGENT)
+ * Transcription automatique du vocal — null en V1.
+ * Sera renseigné par un service Speech-to-Text en V2.
*/
- @Enumerated(EnumType.STRING)
- @Column(name = "priority", nullable = false, length = 20)
- private MessagePriority priority = MessagePriority.NORMAL;
+ @Column(name = "transcription", columnDefinition = "TEXT")
+ private String transcription;
- /**
- * IDs des destinataires (CSV pour targeted messages)
- */
- @Column(name = "recipient_ids", length = 2000)
- private String recipientIds;
-
- /**
- * Rôles destinataires (CSV pour role-based messaging)
- */
- @Column(name = "recipient_roles", length = 500)
- private String recipientRoles;
-
- /**
- * Organisation associée (optionnelle)
- */
+ /** Message auquel celui-ci répond (threading léger). */
@ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "organisation_id")
- private Organisation organisation;
+ @JoinColumn(name = "message_parent_id")
+ private Message messageParent;
- /**
- * Date de lecture du message
- */
- @Column(name = "read_at")
- private LocalDateTime readAt;
+ /** Date de suppression douce (null = message actif). */
+ @Column(name = "supprime_le")
+ private LocalDateTime supprimeLe;
- /**
- * Métadonnées additionnelles (JSON)
- */
- @Column(name = "metadata", columnDefinition = "TEXT")
- private String metadata;
+ @PrePersist
+ @Override
+ protected void onCreate() {
+ super.onCreate();
+ if (typeMessage == null) {
+ typeMessage = TypeContenu.TEXTE;
+ }
+ }
- /**
- * Pièces jointes (CSV URLs)
- */
- @Column(name = "attachments", length = 2000)
- private String attachments;
+ // ── Méthodes métier ───────────────────────────────────────────────────────
- /**
- * Message édité
- */
- @Column(name = "is_edited", nullable = false)
- private Boolean isEdited = false;
+ /** Retourne true si le message a été supprimé par son auteur. */
+ public boolean estSupprime() {
+ return supprimeLe != null;
+ }
- /**
- * Date d'édition
- */
- @Column(name = "edited_at")
- private LocalDateTime editedAt;
+ /** Retourne true si c'est un message texte. */
+ public boolean estTextuel() {
+ return TypeContenu.TEXTE.equals(typeMessage);
+ }
- /**
- * Message supprimé (soft delete)
- */
- @Column(name = "is_deleted", nullable = false)
- private Boolean isDeleted = false;
-
- /**
- * Marque le message comme lu
- */
- public void markAsRead() {
- this.status = MessageStatus.READ;
- this.readAt = LocalDateTime.now();
+ /** Retourne true si c'est une note vocale. */
+ public boolean estVocal() {
+ return TypeContenu.VOCAL.equals(typeMessage);
}
/**
- * Marque le message comme édité
+ * Supprime le message de façon douce.
+ * Le contenu original est remplacé par un marqueur.
*/
- public void markAsEdited() {
- this.isEdited = true;
- this.editedAt = LocalDateTime.now();
+ public void supprimer() {
+ this.supprimeLe = LocalDateTime.now();
+ this.contenu = "[Message supprimé]";
+ this.urlFichier = null;
}
}
diff --git a/src/main/java/dev/lions/unionflow/server/repository/ContactPolicyRepository.java b/src/main/java/dev/lions/unionflow/server/repository/ContactPolicyRepository.java
new file mode 100644
index 0000000..5051e6c
--- /dev/null
+++ b/src/main/java/dev/lions/unionflow/server/repository/ContactPolicyRepository.java
@@ -0,0 +1,27 @@
+package dev.lions.unionflow.server.repository;
+
+import dev.lions.unionflow.server.entity.ContactPolicy;
+import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
+import jakarta.enterprise.context.ApplicationScoped;
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * Repository pour les politiques de communication des organisations.
+ *
+ * @author UnionFlow Team
+ * @version 4.0
+ * @since 2026-04-13
+ */
+@ApplicationScoped
+public class ContactPolicyRepository implements PanacheRepositoryBase {
+
+ /**
+ * Trouve la politique de communication d'une organisation.
+ * Chaque organisation a exactement une politique.
+ */
+ public Optional findByOrganisationId(UUID organisationId) {
+ return find("organisation.id = ?1 AND actif = true", organisationId)
+ .firstResultOptional();
+ }
+}
diff --git a/src/main/java/dev/lions/unionflow/server/repository/ConversationParticipantRepository.java b/src/main/java/dev/lions/unionflow/server/repository/ConversationParticipantRepository.java
new file mode 100644
index 0000000..fe9f736
--- /dev/null
+++ b/src/main/java/dev/lions/unionflow/server/repository/ConversationParticipantRepository.java
@@ -0,0 +1,44 @@
+package dev.lions.unionflow.server.repository;
+
+import dev.lions.unionflow.server.entity.ConversationParticipant;
+import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
+import jakarta.enterprise.context.ApplicationScoped;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * Repository pour les participants aux conversations.
+ *
+ * @author UnionFlow Team
+ * @version 4.0
+ * @since 2026-04-13
+ */
+@ApplicationScoped
+public class ConversationParticipantRepository
+ implements PanacheRepositoryBase {
+
+ /**
+ * Trouve la participation d'un membre à une conversation.
+ */
+ public Optional findParticipant(UUID conversationId, UUID membreId) {
+ return find("conversation.id = ?1 AND membre.id = ?2 AND actif = true",
+ conversationId, membreId
+ ).firstResultOptional();
+ }
+
+ /**
+ * Liste tous les participants actifs d'une conversation.
+ */
+ public List findByConversation(UUID conversationId) {
+ return find("conversation.id = ?1 AND actif = true", conversationId).list();
+ }
+
+ /**
+ * Vérifie si un membre est participant à une conversation.
+ */
+ public boolean estParticipant(UUID conversationId, UUID membreId) {
+ return count("conversation.id = ?1 AND membre.id = ?2 AND actif = true",
+ conversationId, membreId) > 0;
+ }
+}
diff --git a/src/main/java/dev/lions/unionflow/server/repository/ConversationRepository.java b/src/main/java/dev/lions/unionflow/server/repository/ConversationRepository.java
index 616cef9..4c5341d 100644
--- a/src/main/java/dev/lions/unionflow/server/repository/ConversationRepository.java
+++ b/src/main/java/dev/lions/unionflow/server/repository/ConversationRepository.java
@@ -1,72 +1,80 @@
package dev.lions.unionflow.server.repository;
import dev.lions.unionflow.server.entity.Conversation;
-import dev.lions.unionflow.server.entity.Membre;
+import dev.lions.unionflow.server.api.enums.messagerie.StatutConversation;
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
-
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
- * Repository pour Conversation
+ * Repository pour les conversations de la messagerie.
*
* @author UnionFlow Team
- * @version 1.0
- * @since 2026-03-16
+ * @version 4.0
+ * @since 2026-04-13
*/
@ApplicationScoped
public class ConversationRepository implements PanacheRepositoryBase {
/**
- * Trouve toutes les conversations d'un membre
+ * Trouve une conversation par son ID avec Optional.
*/
- public List findByParticipant(UUID membreId, boolean includeArchived) {
- String query = """
- SELECT DISTINCT c FROM Conversation c
- JOIN c.participants p
- WHERE p.id = :membreId
- AND (c.actif IS NULL OR c.actif = true)
- """;
-
- if (!includeArchived) {
- query += " AND c.isArchived = false";
- }
-
- query += " ORDER BY c.updatedAt DESC NULLS LAST, c.dateCreation DESC";
-
- return getEntityManager()
- .createQuery(query, Conversation.class)
- .setParameter("membreId", membreId)
- .getResultList();
+ public Optional findConversationById(UUID id) {
+ return find("id", id).firstResultOptional();
}
/**
- * Trouve une conversation par ID et vérifie que le membre en fait partie
+ * Liste toutes les conversations d'un membre (via les participants).
+ * Triées par date du dernier message décroissante.
*/
- public Optional findByIdAndParticipant(UUID conversationId, UUID membreId) {
- String query = """
- SELECT DISTINCT c FROM Conversation c
- JOIN c.participants p
- WHERE c.id = :conversationId
- AND p.id = :membreId
- AND (c.actif IS NULL OR c.actif = true)
- """;
-
- return getEntityManager()
- .createQuery(query, Conversation.class)
- .setParameter("conversationId", conversationId)
- .setParameter("membreId", membreId)
- .getResultStream()
- .findFirst();
+ public List findByMembreId(UUID membreId) {
+ return find(
+ "SELECT DISTINCT c FROM Conversation c " +
+ "JOIN c.participants p " +
+ "WHERE p.membre.id = ?1 AND p.actif = true " +
+ "ORDER BY c.dernierMessageAt DESC NULLS LAST",
+ membreId
+ ).list();
}
/**
- * Trouve les conversations d'une organisation
+ * Trouve une conversation directe existante entre deux membres dans une organisation.
*/
- public List findByOrganisation(UUID organisationId) {
- return find("organisation.id = ?1 AND (actif IS NULL OR actif = true) ORDER BY updatedAt DESC NULLS LAST", organisationId)
- .list();
+ public Optional findConversationDirecte(UUID membreAId, UUID membreBId, UUID organisationId) {
+ return find(
+ "SELECT DISTINCT c FROM Conversation c " +
+ "JOIN c.participants p1 " +
+ "JOIN c.participants p2 " +
+ "WHERE c.typeConversation = 'DIRECTE' " +
+ "AND c.organisation.id = ?3 " +
+ "AND p1.membre.id = ?1 AND p1.actif = true " +
+ "AND p2.membre.id = ?2 AND p2.actif = true",
+ membreAId, membreBId, organisationId
+ ).firstResultOptional();
+ }
+
+ /**
+ * Trouve le canal d'un rôle dans une organisation.
+ */
+ public Optional findCanalRole(UUID organisationId, String roleCible) {
+ return find(
+ "organisation.id = ?1 AND roleCible = ?2 AND typeConversation = 'ROLE_CANAL' AND actif = true",
+ organisationId, roleCible
+ ).firstResultOptional();
+ }
+
+ /**
+ * Liste les conversations actives d'un membre.
+ */
+ public List findActivesByMembre(UUID membreId) {
+ return find(
+ "SELECT DISTINCT c FROM Conversation c " +
+ "JOIN c.participants p " +
+ "WHERE p.membre.id = ?1 AND p.actif = true AND c.statut = ?2 " +
+ "ORDER BY c.dernierMessageAt DESC NULLS LAST",
+ membreId, StatutConversation.ACTIVE
+ ).list();
}
}
diff --git a/src/main/java/dev/lions/unionflow/server/repository/MemberBlockRepository.java b/src/main/java/dev/lions/unionflow/server/repository/MemberBlockRepository.java
new file mode 100644
index 0000000..70f92ae
--- /dev/null
+++ b/src/main/java/dev/lions/unionflow/server/repository/MemberBlockRepository.java
@@ -0,0 +1,51 @@
+package dev.lions.unionflow.server.repository;
+
+import dev.lions.unionflow.server.entity.MemberBlock;
+import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
+import jakarta.enterprise.context.ApplicationScoped;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * Repository pour les blocages entre membres.
+ *
+ * @author UnionFlow Team
+ * @version 4.0
+ * @since 2026-04-13
+ */
+@ApplicationScoped
+public class MemberBlockRepository implements PanacheRepositoryBase {
+
+ /**
+ * Vérifie si un membre en a bloqué un autre dans une organisation.
+ */
+ public boolean estBloque(UUID bloqueurId, UUID bloqueId, UUID organisationId) {
+ return count("bloqueur.id = ?1 AND bloque.id = ?2 AND organisation.id = ?3 AND actif = true",
+ bloqueurId, bloqueId, organisationId) > 0;
+ }
+
+ /**
+ * Trouve le blocage entre deux membres dans une organisation.
+ */
+ public Optional findBlocage(UUID bloqueurId, UUID bloqueId, UUID organisationId) {
+ return find("bloqueur.id = ?1 AND bloque.id = ?2 AND organisation.id = ?3 AND actif = true",
+ bloqueurId, bloqueId, organisationId
+ ).firstResultOptional();
+ }
+
+ /**
+ * Liste tous les membres bloqués par un membre dans toutes ses organisations.
+ */
+ public List findByBloqueur(UUID bloqueurId) {
+ return find("bloqueur.id = ?1 AND actif = true", bloqueurId).list();
+ }
+
+ /**
+ * Liste les blocages actifs d'un membre dans une organisation spécifique.
+ */
+ public List findByBloqueurEtOrganisation(UUID bloqueurId, UUID organisationId) {
+ return find("bloqueur.id = ?1 AND organisation.id = ?2 AND actif = true",
+ bloqueurId, organisationId).list();
+ }
+}
diff --git a/src/main/java/dev/lions/unionflow/server/repository/MessageRepository.java b/src/main/java/dev/lions/unionflow/server/repository/MessageRepository.java
index 7adf34d..75e0f72 100644
--- a/src/main/java/dev/lions/unionflow/server/repository/MessageRepository.java
+++ b/src/main/java/dev/lions/unionflow/server/repository/MessageRepository.java
@@ -2,65 +2,77 @@ package dev.lions.unionflow.server.repository;
import dev.lions.unionflow.server.entity.Message;
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
+import io.quarkus.panache.common.Page;
import jakarta.enterprise.context.ApplicationScoped;
-
import java.util.List;
+import java.util.Optional;
import java.util.UUID;
/**
- * Repository pour Message
+ * Repository pour les messages de la messagerie.
*
* @author UnionFlow Team
- * @version 1.0
- * @since 2026-03-16
+ * @version 4.0
+ * @since 2026-04-13
*/
@ApplicationScoped
public class MessageRepository implements PanacheRepositoryBase {
/**
- * Trouve tous les messages d'une conversation (non supprimés)
+ * Trouve un message par son ID avec Optional.
*/
- public List findByConversation(UUID conversationId, int limit) {
- return find(
- "conversation.id = ?1 AND isDeleted = false AND (actif IS NULL OR actif = true) ORDER BY dateCreation DESC",
- conversationId
- )
- .page(0, limit)
- .list();
+ public Optional findMessageById(UUID id) {
+ return find("id", id).firstResultOptional();
}
/**
- * Compte les messages non lus d'une conversation pour un membre
+ * Récupère les messages d'une conversation, paginés, du plus récent au plus ancien.
+ *
+ * @param conversationId ID de la conversation
+ * @param page numéro de page (0-based)
+ * @param size nombre de messages par page
*/
- public long countUnreadByConversationAndMember(UUID conversationId, UUID membreId) {
- // Pour simplifier, on compte les messages SENT/DELIVERED (pas READ)
- // et dont le sender n'est PAS le membre en question
+ public List findByConversationPagine(UUID conversationId, int page, int size) {
+ return find(
+ "conversation.id = ?1 AND actif = true ORDER BY dateCreation DESC",
+ conversationId
+ ).page(Page.of(page, size)).list();
+ }
+
+ /**
+ * Compte les messages non lus dans une conversation pour un membre donné.
+ * Un message est non lu si sa dateCreation est postérieure au luJusqua du participant.
+ */
+ public long countNonLus(UUID conversationId, UUID membreId) {
return count(
- "conversation.id = ?1 AND sender.id != ?2 AND status IN ('SENT', 'DELIVERED') AND isDeleted = false",
- conversationId,
- membreId
+ "SELECT COUNT(m) FROM Message m, ConversationParticipant p " +
+ "WHERE m.conversation.id = ?1 " +
+ "AND p.conversation.id = ?1 " +
+ "AND p.membre.id = ?2 " +
+ "AND m.actif = true " +
+ "AND (p.luJusqua IS NULL OR m.dateCreation > p.luJusqua) " +
+ "AND m.expediteur.id <> ?2",
+ conversationId, membreId
);
}
/**
- * Marque tous les messages d'une conversation comme lus pour un membre
+ * Récupère les messages non supprimés d'une conversation (pour les tests).
*/
- public int markAllAsReadByConversationAndMember(UUID conversationId, UUID membreId) {
- return update(
- "status = 'READ', readAt = CURRENT_TIMESTAMP WHERE conversation.id = ?1 AND sender.id != ?2 AND status != 'READ' AND isDeleted = false",
- conversationId,
- membreId
- );
- }
-
- /**
- * Trouve le dernier message d'une conversation
- */
- public Message findLastByConversation(UUID conversationId) {
+ public List findActifsByConversation(UUID conversationId) {
return find(
- "conversation.id = ?1 AND isDeleted = false ORDER BY dateCreation DESC",
+ "conversation.id = ?1 AND actif = true ORDER BY dateCreation ASC",
conversationId
- )
- .firstResult();
+ ).list();
+ }
+
+ /**
+ * Trouve le dernier message actif d'une conversation.
+ */
+ public Optional findDernierMessage(UUID conversationId) {
+ return find(
+ "conversation.id = ?1 AND actif = true ORDER BY dateCreation DESC",
+ conversationId
+ ).firstResultOptional();
}
}
diff --git a/src/main/java/dev/lions/unionflow/server/resource/ConversationResource.java b/src/main/java/dev/lions/unionflow/server/resource/ConversationResource.java
deleted file mode 100644
index afdba5c..0000000
--- a/src/main/java/dev/lions/unionflow/server/resource/ConversationResource.java
+++ /dev/null
@@ -1,145 +0,0 @@
-package dev.lions.unionflow.server.resource;
-
-import dev.lions.unionflow.server.api.dto.communication.request.CreateConversationRequest;
-import dev.lions.unionflow.server.api.dto.communication.response.ConversationResponse;
-import dev.lions.unionflow.server.service.ConversationService;
-import dev.lions.unionflow.server.service.support.SecuriteHelper;
-import jakarta.annotation.security.RolesAllowed;
-import jakarta.inject.Inject;
-import jakarta.validation.Valid;
-import jakarta.ws.rs.*;
-import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.Response;
-import org.eclipse.microprofile.openapi.annotations.Operation;
-import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
-import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
-import org.eclipse.microprofile.openapi.annotations.tags.Tag;
-import org.jboss.logging.Logger;
-
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-
-/**
- * Resource REST pour la gestion des conversations
- *
- * @author UnionFlow Team
- * @version 1.0
- * @since 2026-03-16
- */
-@Path("/api/conversations")
-@Produces(MediaType.APPLICATION_JSON)
-@Consumes(MediaType.APPLICATION_JSON)
-@Tag(name = "Communication", description = "Gestion des conversations et messages")
-@RolesAllowed({"ADMIN", "SUPER_ADMIN", "MEMBRE", "ADMIN_ORGANISATION"})
-public class ConversationResource {
-
- private static final Logger LOG = Logger.getLogger(ConversationResource.class);
-
- @Inject
- ConversationService conversationService;
-
- @Inject
- SecuriteHelper securiteHelper;
-
- /**
- * Liste les conversations de l'utilisateur connecté
- */
- @GET
- @Operation(summary = "Lister mes conversations")
- @APIResponse(responseCode = "200", description = "Liste des conversations")
- public Response getConversations(
- @Parameter(description = "Inclure conversations archivées")
- @QueryParam("includeArchived") @DefaultValue("false") boolean includeArchived,
- @Parameter(description = "Filtrer par organisation")
- @QueryParam("organisationId") String organisationId
- ) {
- UUID membreId = securiteHelper.resolveMembreId();
- UUID orgId = organisationId != null ? UUID.fromString(organisationId) : null;
-
- List conversations = conversationService.getConversations(membreId, orgId, includeArchived);
- return Response.ok(conversations).build();
- }
-
- /**
- * Récupère une conversation par ID
- */
- @GET
- @Path("/{id}")
- @Operation(summary = "Récupérer une conversation")
- @APIResponse(responseCode = "200", description = "Conversation trouvée")
- @APIResponse(responseCode = "404", description = "Conversation non trouvée")
- public Response getConversationById(@PathParam("id") UUID conversationId) {
- UUID membreId = securiteHelper.resolveMembreId();
- ConversationResponse conversation = conversationService.getConversationById(conversationId, membreId);
- return Response.ok(conversation).build();
- }
-
- /**
- * Crée une nouvelle conversation
- */
- @POST
- @Operation(summary = "Créer une conversation")
- @APIResponse(responseCode = "201", description = "Conversation créée")
- @APIResponse(responseCode = "400", description = "Données invalides")
- public Response createConversation(@Valid CreateConversationRequest request) {
- UUID creatorId = securiteHelper.resolveMembreId();
- ConversationResponse conversation = conversationService.createConversation(request, creatorId);
- return Response.status(Response.Status.CREATED).entity(conversation).build();
- }
-
- /**
- * Archive une conversation
- */
- @PUT
- @Path("/{id}/archive")
- @Operation(summary = "Archiver/désarchiver une conversation")
- @APIResponse(responseCode = "204", description = "Conversation archivée")
- public Response archiveConversation(
- @PathParam("id") UUID conversationId,
- @QueryParam("archive") @DefaultValue("true") boolean archive
- ) {
- UUID membreId = securiteHelper.resolveMembreId();
- conversationService.archiveConversation(conversationId, membreId, archive);
- return Response.noContent().build();
- }
-
- /**
- * Marque une conversation comme lue
- */
- @PUT
- @Path("/{id}/mark-read")
- @Operation(summary = "Marquer conversation comme lue")
- @APIResponse(responseCode = "204", description = "Marquée comme lue")
- public Response markAsRead(@PathParam("id") UUID conversationId) {
- UUID membreId = securiteHelper.resolveMembreId();
- conversationService.markAsRead(conversationId, membreId);
- return Response.noContent().build();
- }
-
- /**
- * Toggle mute conversation
- */
- @PUT
- @Path("/{id}/toggle-mute")
- @Operation(summary = "Activer/désactiver le son")
- @APIResponse(responseCode = "204", description = "Paramètre modifié")
- public Response toggleMute(@PathParam("id") UUID conversationId) {
- UUID membreId = securiteHelper.resolveMembreId();
- conversationService.toggleMute(conversationId, membreId);
- return Response.noContent().build();
- }
-
- /**
- * Toggle pin conversation
- */
- @PUT
- @Path("/{id}/toggle-pin")
- @Operation(summary = "Épingler/désépingler")
- @APIResponse(responseCode = "204", description = "Paramètre modifié")
- public Response togglePin(@PathParam("id") UUID conversationId) {
- UUID membreId = securiteHelper.resolveMembreId();
- conversationService.togglePin(conversationId, membreId);
- return Response.noContent().build();
- }
-}
diff --git a/src/main/java/dev/lions/unionflow/server/resource/MessageResource.java b/src/main/java/dev/lions/unionflow/server/resource/MessageResource.java
deleted file mode 100644
index 4b311ca..0000000
--- a/src/main/java/dev/lions/unionflow/server/resource/MessageResource.java
+++ /dev/null
@@ -1,120 +0,0 @@
-package dev.lions.unionflow.server.resource;
-
-import dev.lions.unionflow.server.api.dto.communication.request.SendMessageRequest;
-import dev.lions.unionflow.server.api.dto.communication.response.MessageResponse;
-import dev.lions.unionflow.server.service.MessageService;
-import dev.lions.unionflow.server.service.support.SecuriteHelper;
-import jakarta.annotation.security.RolesAllowed;
-import jakarta.inject.Inject;
-import jakarta.validation.Valid;
-import jakarta.ws.rs.*;
-import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.Response;
-import org.eclipse.microprofile.openapi.annotations.Operation;
-import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
-import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
-import org.eclipse.microprofile.openapi.annotations.tags.Tag;
-import org.jboss.logging.Logger;
-
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-
-/**
- * Resource REST pour la gestion des messages
- *
- * @author UnionFlow Team
- * @version 1.0
- * @since 2026-03-16
- */
-@Path("/api/messages")
-@Produces(MediaType.APPLICATION_JSON)
-@Consumes(MediaType.APPLICATION_JSON)
-@Tag(name = "Communication", description = "Gestion des conversations et messages")
-@RolesAllowed({"ADMIN", "SUPER_ADMIN", "MEMBRE", "ADMIN_ORGANISATION"})
-public class MessageResource {
-
- private static final Logger LOG = Logger.getLogger(MessageResource.class);
-
- @Inject
- MessageService messageService;
-
- @Inject
- SecuriteHelper securiteHelper;
-
- /**
- * Récupère les messages d'une conversation
- */
- @GET
- @Operation(summary = "Lister les messages d'une conversation")
- @APIResponse(responseCode = "200", description = "Liste des messages")
- @APIResponse(responseCode = "404", description = "Conversation non trouvée")
- public Response getMessages(
- @Parameter(description = "ID de la conversation", required = true)
- @QueryParam("conversationId") UUID conversationId,
- @Parameter(description = "Nombre maximum de messages")
- @QueryParam("limit") @DefaultValue("50") int limit
- ) {
- if (conversationId == null) {
- return Response.status(Response.Status.BAD_REQUEST)
- .entity(Map.of("error", "conversationId requis"))
- .build();
- }
-
- UUID membreId = securiteHelper.resolveMembreId();
- List messages = messageService.getMessages(conversationId, membreId, limit);
- return Response.ok(messages).build();
- }
-
- /**
- * Envoie un message
- */
- @POST
- @Operation(summary = "Envoyer un message")
- @APIResponse(responseCode = "201", description = "Message envoyé")
- @APIResponse(responseCode = "400", description = "Données invalides")
- @APIResponse(responseCode = "404", description = "Conversation non trouvée")
- public Response sendMessage(@Valid SendMessageRequest request) {
- UUID senderId = securiteHelper.resolveMembreId();
- MessageResponse message = messageService.sendMessage(request, senderId);
- return Response.status(Response.Status.CREATED).entity(message).build();
- }
-
- /**
- * Édite un message
- */
- @PUT
- @Path("/{id}")
- @Operation(summary = "Éditer un message")
- @APIResponse(responseCode = "200", description = "Message édité")
- @APIResponse(responseCode = "404", description = "Message non trouvé")
- public Response editMessage(
- @PathParam("id") UUID messageId,
- Map body
- ) {
- String newContent = body.get("content");
- if (newContent == null || newContent.isEmpty()) {
- return Response.status(Response.Status.BAD_REQUEST)
- .entity(Map.of("error", "content requis"))
- .build();
- }
-
- UUID senderId = securiteHelper.resolveMembreId();
- MessageResponse message = messageService.editMessage(messageId, senderId, newContent);
- return Response.ok(message).build();
- }
-
- /**
- * Supprime un message
- */
- @DELETE
- @Path("/{id}")
- @Operation(summary = "Supprimer un message")
- @APIResponse(responseCode = "204", description = "Message supprimé")
- @APIResponse(responseCode = "404", description = "Message non trouvé")
- public Response deleteMessage(@PathParam("id") UUID messageId) {
- UUID senderId = securiteHelper.resolveMembreId();
- messageService.deleteMessage(messageId, senderId);
- return Response.noContent().build();
- }
-}
diff --git a/src/main/java/dev/lions/unionflow/server/resource/MessagingResource.java b/src/main/java/dev/lions/unionflow/server/resource/MessagingResource.java
new file mode 100644
index 0000000..51e9597
--- /dev/null
+++ b/src/main/java/dev/lions/unionflow/server/resource/MessagingResource.java
@@ -0,0 +1,200 @@
+package dev.lions.unionflow.server.resource;
+
+import dev.lions.unionflow.server.api.dto.messagerie.request.BloquerMembreRequest;
+import dev.lions.unionflow.server.api.dto.messagerie.request.DemarrerConversationDirecteRequest;
+import dev.lions.unionflow.server.api.dto.messagerie.request.DemarrerConversationRoleRequest;
+import dev.lions.unionflow.server.api.dto.messagerie.request.EnvoyerMessageRequest;
+import dev.lions.unionflow.server.api.dto.messagerie.request.MettreAJourPolitiqueRequest;
+import dev.lions.unionflow.server.api.dto.messagerie.response.ContactPolicyResponse;
+import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationResponse;
+import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationSummaryResponse;
+import dev.lions.unionflow.server.api.dto.messagerie.response.MessageResponse;
+import dev.lions.unionflow.server.service.MessagingService;
+import jakarta.annotation.security.RolesAllowed;
+import jakarta.inject.Inject;
+import jakarta.validation.Valid;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.DefaultValue;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.util.List;
+import java.util.UUID;
+import org.eclipse.microprofile.openapi.annotations.tags.Tag;
+import org.jboss.logging.Logger;
+
+/**
+ * Resource REST pour la messagerie instantanée.
+ *
+ * Endpoints :
+ *
+ * - POST /api/messagerie/conversations/directe — démarrer conversation 1-1
+ * - POST /api/messagerie/conversations/role — contacter un rôle officiel
+ * - GET /api/messagerie/conversations — mes conversations
+ * - GET /api/messagerie/conversations/{id} — détail + messages
+ * - DELETE /api/messagerie/conversations/{id} — archiver
+ * - POST /api/messagerie/conversations/{id}/messages — envoyer un message
+ * - GET /api/messagerie/conversations/{id}/messages — historique
+ * - PUT /api/messagerie/conversations/{id}/lire — marquer comme lu
+ * - DELETE /api/messagerie/conversations/{cId}/messages/{mId} — supprimer message
+ * - POST /api/messagerie/blocages — bloquer un membre
+ * - DELETE /api/messagerie/blocages/{membreId} — débloquer
+ * - GET /api/messagerie/blocages — mes blocages
+ * - GET /api/messagerie/politique/{orgId} — politique de communication
+ * - PUT /api/messagerie/politique/{orgId} — mettre à jour (ADMIN)
+ *
+ *
+ * @author UnionFlow Team
+ * @version 4.0
+ * @since 2026-04-13
+ */
+@Path("/api/messagerie")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE"})
+@Tag(name = "Messagerie", description = "Messagerie instantanée — conversations, messages, notes vocales")
+public class MessagingResource {
+
+ private static final Logger LOG = Logger.getLogger(MessagingResource.class);
+
+ @Inject
+ MessagingService messagingService;
+
+ // ── Conversations ─────────────────────────────────────────────────────────
+
+ @POST
+ @Path("/conversations/directe")
+ public Response demarrerConversationDirecte(@Valid DemarrerConversationDirecteRequest request) {
+ LOG.infof("POST /api/messagerie/conversations/directe → destinataire: %s", request.destinataireId());
+ ConversationResponse result = messagingService.demarrerConversationDirecte(request);
+ return Response.status(Response.Status.CREATED).entity(result).build();
+ }
+
+ @POST
+ @Path("/conversations/role")
+ public Response demarrerConversationRole(@Valid DemarrerConversationRoleRequest request) {
+ LOG.infof("POST /api/messagerie/conversations/role → rôle: %s, org: %s",
+ request.roleCible(), request.organisationId());
+ ConversationResponse result = messagingService.demarrerConversationRole(request);
+ return Response.status(Response.Status.CREATED).entity(result).build();
+ }
+
+ @GET
+ @Path("/conversations")
+ public Response getMesConversations() {
+ LOG.debug("GET /api/messagerie/conversations");
+ List result = messagingService.getMesConversations();
+ return Response.ok(result).build();
+ }
+
+ @GET
+ @Path("/conversations/{id}")
+ public Response getConversation(@PathParam("id") UUID id) {
+ LOG.infof("GET /api/messagerie/conversations/%s", id);
+ ConversationResponse result = messagingService.getConversation(id);
+ return Response.ok(result).build();
+ }
+
+ @DELETE
+ @Path("/conversations/{id}")
+ public Response archiverConversation(@PathParam("id") UUID id) {
+ LOG.infof("DELETE /api/messagerie/conversations/%s", id);
+ ConversationResponse result = messagingService.archiverConversation(id);
+ return Response.ok(result).build();
+ }
+
+ // ── Messages ──────────────────────────────────────────────────────────────
+
+ @POST
+ @Path("/conversations/{id}/messages")
+ public Response envoyerMessage(
+ @PathParam("id") UUID conversationId,
+ @Valid EnvoyerMessageRequest request) {
+ LOG.infof("POST /api/messagerie/conversations/%s/messages", conversationId);
+ MessageResponse result = messagingService.envoyerMessage(conversationId, request);
+ return Response.status(Response.Status.CREATED).entity(result).build();
+ }
+
+ @GET
+ @Path("/conversations/{id}/messages")
+ public Response getMessages(
+ @PathParam("id") UUID conversationId,
+ @QueryParam("page") @DefaultValue("0") int page) {
+ LOG.infof("GET /api/messagerie/conversations/%s/messages?page=%d", conversationId, page);
+ List result = messagingService.getMessages(conversationId, page);
+ return Response.ok(result).build();
+ }
+
+ @PUT
+ @Path("/conversations/{id}/lire")
+ public Response marquerLu(@PathParam("id") UUID conversationId) {
+ LOG.infof("PUT /api/messagerie/conversations/%s/lire", conversationId);
+ messagingService.marquerConversationLue(conversationId);
+ return Response.noContent().build();
+ }
+
+ @DELETE
+ @Path("/conversations/{conversationId}/messages/{messageId}")
+ public Response supprimerMessage(
+ @PathParam("conversationId") UUID conversationId,
+ @PathParam("messageId") UUID messageId) {
+ LOG.infof("DELETE /api/messagerie/conversations/%s/messages/%s", conversationId, messageId);
+ messagingService.supprimerMessage(conversationId, messageId);
+ return Response.noContent().build();
+ }
+
+ // ── Blocages ──────────────────────────────────────────────────────────────
+
+ @POST
+ @Path("/blocages")
+ public Response bloquerMembre(@Valid BloquerMembreRequest request) {
+ LOG.infof("POST /api/messagerie/blocages → bloquer: %s", request.membreABloquerId());
+ messagingService.bloquerMembre(request);
+ return Response.noContent().build();
+ }
+
+ @DELETE
+ @Path("/blocages/{membreId}")
+ public Response debloquerMembre(
+ @PathParam("membreId") UUID membreId,
+ @QueryParam("organisationId") UUID organisationId) {
+ LOG.infof("DELETE /api/messagerie/blocages/%s", membreId);
+ messagingService.debloquerMembre(membreId, organisationId);
+ return Response.noContent().build();
+ }
+
+ @GET
+ @Path("/blocages")
+ public Response getMesBlocages() {
+ LOG.debug("GET /api/messagerie/blocages");
+ return Response.ok(messagingService.getMesBlocages()).build();
+ }
+
+ // ── Politique de communication ────────────────────────────────────────────
+
+ @GET
+ @Path("/politique/{organisationId}")
+ public Response getPolitique(@PathParam("organisationId") UUID organisationId) {
+ LOG.infof("GET /api/messagerie/politique/%s", organisationId);
+ ContactPolicyResponse result = messagingService.getPolitique(organisationId);
+ return Response.ok(result).build();
+ }
+
+ @PUT
+ @Path("/politique/{organisationId}")
+ @RolesAllowed({"ADMIN", "ADMIN_ORGANISATION"})
+ public Response mettreAJourPolitique(
+ @PathParam("organisationId") UUID organisationId,
+ @Valid MettreAJourPolitiqueRequest request) {
+ LOG.infof("PUT /api/messagerie/politique/%s", organisationId);
+ ContactPolicyResponse result = messagingService.mettreAJourPolitique(organisationId, request);
+ return Response.ok(result).build();
+ }
+}
diff --git a/src/main/java/dev/lions/unionflow/server/service/ConversationService.java b/src/main/java/dev/lions/unionflow/server/service/ConversationService.java
deleted file mode 100644
index ed45840..0000000
--- a/src/main/java/dev/lions/unionflow/server/service/ConversationService.java
+++ /dev/null
@@ -1,208 +0,0 @@
-package dev.lions.unionflow.server.service;
-
-import dev.lions.unionflow.server.api.dto.communication.request.CreateConversationRequest;
-import dev.lions.unionflow.server.api.dto.communication.response.ConversationResponse;
-import dev.lions.unionflow.server.api.dto.communication.response.MessageResponse;
-import dev.lions.unionflow.server.entity.Conversation;
-import dev.lions.unionflow.server.entity.Membre;
-import dev.lions.unionflow.server.entity.Message;
-import dev.lions.unionflow.server.entity.Organisation;
-import dev.lions.unionflow.server.repository.ConversationRepository;
-import dev.lions.unionflow.server.repository.MembreRepository;
-import dev.lions.unionflow.server.repository.MessageRepository;
-import dev.lions.unionflow.server.repository.OrganisationRepository;
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.inject.Inject;
-import jakarta.transaction.Transactional;
-import jakarta.ws.rs.NotFoundException;
-import org.jboss.logging.Logger;
-
-import java.time.LocalDateTime;
-import java.util.List;
-import java.util.UUID;
-import java.util.stream.Collectors;
-
-/**
- * Service de gestion des conversations
- *
- * @author UnionFlow Team
- * @version 1.0
- * @since 2026-03-16
- */
-@ApplicationScoped
-public class ConversationService {
-
- private static final Logger LOG = Logger.getLogger(ConversationService.class);
-
- @Inject
- ConversationRepository conversationRepository;
-
- @Inject
- MessageRepository messageRepository;
-
- @Inject
- MembreRepository membreRepository;
-
- @Inject
- OrganisationRepository organisationRepository;
-
- /**
- * Liste les conversations d'un membre
- */
- public List getConversations(UUID membreId, UUID organisationId, boolean includeArchived) {
- LOG.infof("Récupération conversations pour membre %s", membreId);
-
- List conversations;
- if (organisationId != null) {
- conversations = conversationRepository.findByOrganisation(organisationId);
- } else {
- conversations = conversationRepository.findByParticipant(membreId, includeArchived);
- }
-
- return conversations.stream()
- .map(c -> convertToResponse(c, membreId))
- .collect(Collectors.toList());
- }
-
- /**
- * Récupère une conversation par ID
- */
- public ConversationResponse getConversationById(UUID conversationId, UUID membreId) {
- Conversation conversation = conversationRepository.findByIdAndParticipant(conversationId, membreId)
- .orElseThrow(() -> new NotFoundException("Conversation non trouvée ou accès refusé"));
-
- return convertToResponse(conversation, membreId);
- }
-
- /**
- * Crée une nouvelle conversation
- */
- @Transactional
- public ConversationResponse createConversation(CreateConversationRequest request, UUID creatorId) {
- LOG.infof("Création conversation: %s (type: %s)", request.name(), request.type());
-
- Conversation conversation = new Conversation();
- conversation.setName(request.name());
- conversation.setDescription(request.description());
- conversation.setType(request.type());
-
- // Ajouter les participants
- List participants = request.participantIds().stream()
- .map(id -> membreRepository.findById(id))
- .filter(membre -> membre != null)
- .collect(Collectors.toList());
-
- // Ajouter le créateur s'il n'est pas dans la liste
- Membre creator = membreRepository.findById(creatorId);
- if (creator != null && !participants.contains(creator)) {
- participants.add(creator);
- }
-
- conversation.setParticipants(participants);
-
- // Organisation
- if (request.organisationId() != null) {
- Organisation org = organisationRepository.findById(request.organisationId());
- conversation.setOrganisation(org);
- }
-
- conversation.setUpdatedAt(LocalDateTime.now());
- conversationRepository.persist(conversation);
-
- return convertToResponse(conversation, creatorId);
- }
-
- /**
- * Archive/désarchive une conversation
- */
- @Transactional
- public void archiveConversation(UUID conversationId, UUID membreId, boolean archive) {
- Conversation conversation = conversationRepository.findByIdAndParticipant(conversationId, membreId)
- .orElseThrow(() -> new NotFoundException("Conversation non trouvée"));
-
- conversation.setIsArchived(archive);
- conversation.setUpdatedAt(LocalDateTime.now());
- conversationRepository.persist(conversation);
- }
-
- /**
- * Marque une conversation comme lue
- */
- @Transactional
- public void markAsRead(UUID conversationId, UUID membreId) {
- // Vérifier accès
- conversationRepository.findByIdAndParticipant(conversationId, membreId)
- .orElseThrow(() -> new NotFoundException("Conversation non trouvée"));
-
- messageRepository.markAllAsReadByConversationAndMember(conversationId, membreId);
- }
-
- /**
- * Toggle mute
- */
- @Transactional
- public void toggleMute(UUID conversationId, UUID membreId) {
- Conversation conversation = conversationRepository.findByIdAndParticipant(conversationId, membreId)
- .orElseThrow(() -> new NotFoundException("Conversation non trouvée"));
-
- conversation.setIsMuted(!conversation.getIsMuted());
- conversation.setUpdatedAt(LocalDateTime.now());
- }
-
- /**
- * Toggle pin
- */
- @Transactional
- public void togglePin(UUID conversationId, UUID membreId) {
- Conversation conversation = conversationRepository.findByIdAndParticipant(conversationId, membreId)
- .orElseThrow(() -> new NotFoundException("Conversation non trouvée"));
-
- conversation.setIsPinned(!conversation.getIsPinned());
- conversation.setUpdatedAt(LocalDateTime.now());
- }
-
- /**
- * Convertit Conversation en DTO
- */
- private ConversationResponse convertToResponse(Conversation c, UUID currentUserId) {
- Message lastMsg = messageRepository.findLastByConversation(c.getId());
- long unreadCount = messageRepository.countUnreadByConversationAndMember(c.getId(), currentUserId);
-
- return ConversationResponse.builder()
- .id(c.getId())
- .name(c.getName())
- .description(c.getDescription())
- .type(c.getType())
- .participantIds(c.getParticipants().stream().map(Membre::getId).collect(Collectors.toList()))
- .organisationId(c.getOrganisation() != null ? c.getOrganisation().getId() : null)
- .lastMessage(lastMsg != null ? convertMessageToResponse(lastMsg) : null)
- .unreadCount((int) unreadCount)
- .muted(Boolean.TRUE.equals(c.getIsMuted()))
- .pinned(Boolean.TRUE.equals(c.getIsPinned()))
- .archived(Boolean.TRUE.equals(c.getIsArchived()))
- .createdAt(c.getDateCreation())
- .updatedAt(c.getUpdatedAt())
- .avatarUrl(c.getAvatarUrl())
- .build();
- }
-
- /**
- * Convertit Message en DTO simple
- */
- private MessageResponse convertMessageToResponse(Message m) {
- return MessageResponse.builder()
- .id(m.getId())
- .conversationId(m.getConversation().getId())
- .senderId(m.getSender().getId())
- .senderName(m.getSenderName())
- .senderAvatar(m.getSenderAvatar())
- .content(m.getContent())
- .type(m.getType())
- .status(m.getStatus())
- .priority(m.getPriority())
- .createdAt(m.getDateCreation())
- .edited(Boolean.TRUE.equals(m.getIsEdited()))
- .deleted(Boolean.TRUE.equals(m.getIsDeleted()))
- .build();
- }
-}
diff --git a/src/main/java/dev/lions/unionflow/server/service/MessageService.java b/src/main/java/dev/lions/unionflow/server/service/MessageService.java
deleted file mode 100644
index c8f6546..0000000
--- a/src/main/java/dev/lions/unionflow/server/service/MessageService.java
+++ /dev/null
@@ -1,203 +0,0 @@
-package dev.lions.unionflow.server.service;
-
-import dev.lions.unionflow.server.api.dto.communication.request.SendMessageRequest;
-import dev.lions.unionflow.server.api.dto.communication.response.MessageResponse;
-import dev.lions.unionflow.server.api.enums.communication.MessagePriority;
-import dev.lions.unionflow.server.api.enums.communication.MessageStatus;
-import dev.lions.unionflow.server.api.enums.communication.MessageType;
-import dev.lions.unionflow.server.entity.Conversation;
-import dev.lions.unionflow.server.entity.Membre;
-import dev.lions.unionflow.server.entity.Message;
-import dev.lions.unionflow.server.repository.ConversationRepository;
-import dev.lions.unionflow.server.repository.MembreRepository;
-import dev.lions.unionflow.server.repository.MessageRepository;
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.inject.Inject;
-import jakarta.transaction.Transactional;
-import jakarta.ws.rs.NotFoundException;
-import org.jboss.logging.Logger;
-
-import java.time.LocalDateTime;
-import java.util.List;
-import java.util.UUID;
-import java.util.stream.Collectors;
-
-/**
- * Service de gestion des messages
- *
- * @author UnionFlow Team
- * @version 1.0
- * @since 2026-03-16
- */
-@ApplicationScoped
-public class MessageService {
-
- private static final Logger LOG = Logger.getLogger(MessageService.class);
-
- @Inject
- MessageRepository messageRepository;
-
- @Inject
- ConversationRepository conversationRepository;
-
- @Inject
- MembreRepository membreRepository;
-
- /**
- * Récupère les messages d'une conversation
- */
- public List getMessages(UUID conversationId, UUID membreId, int limit) {
- LOG.infof("Récupération messages pour conversation %s (limit: %d)", conversationId, limit);
-
- // Vérifier accès
- conversationRepository.findByIdAndParticipant(conversationId, membreId)
- .orElseThrow(() -> new NotFoundException("Conversation non trouvée ou accès refusé"));
-
- List messages = messageRepository.findByConversation(conversationId, limit);
-
- return messages.stream()
- .map(this::convertToResponse)
- .collect(Collectors.toList());
- }
-
- /**
- * Envoie un message
- */
- @Transactional
- public MessageResponse sendMessage(SendMessageRequest request, UUID senderId) {
- LOG.infof("Envoi message dans conversation %s", request.conversationId());
-
- // Vérifier accès conversation
- Conversation conversation = conversationRepository.findByIdAndParticipant(request.conversationId(), senderId)
- .orElseThrow(() -> new NotFoundException("Conversation non trouvée ou accès refusé"));
-
- Membre sender = membreRepository.findById(senderId);
- if (sender == null) {
- throw new NotFoundException("Expéditeur non trouvé");
- }
-
- Message message = new Message();
- message.setConversation(conversation);
- message.setSender(sender);
- message.setSenderName(sender.getPrenom() + " " + sender.getNom());
- message.setSenderAvatar(sender.getPhotoUrl());
- message.setContent(request.content());
- message.setType(request.type() != null ? request.type() : MessageType.INDIVIDUAL);
- message.setStatus(MessageStatus.SENT);
- message.setPriority(request.priority() != null ? request.priority() : MessagePriority.NORMAL);
-
- // Destinataires (pour targeted messages)
- if (request.recipientIds() != null && !request.recipientIds().isEmpty()) {
- message.setRecipientIds(request.recipientIds().stream()
- .map(UUID::toString)
- .collect(Collectors.joining(",")));
- }
-
- // Rôles destinataires
- if (request.recipientRoles() != null && !request.recipientRoles().isEmpty()) {
- message.setRecipientRoles(String.join(",", request.recipientRoles()));
- }
-
- // Pièces jointes
- if (request.attachments() != null && !request.attachments().isEmpty()) {
- message.setAttachments(String.join(",", request.attachments()));
- }
-
- message.setOrganisation(conversation.getOrganisation());
-
- messageRepository.persist(message);
-
- // Mettre à jour la conversation
- conversation.setUpdatedAt(LocalDateTime.now());
- conversationRepository.persist(conversation);
-
- LOG.infof("Message %s créé avec succès", message.getId());
- return convertToResponse(message);
- }
-
- /**
- * Édite un message
- */
- @Transactional
- public MessageResponse editMessage(UUID messageId, UUID senderId, String newContent) {
- Message message = messageRepository.findById(messageId);
- if (message == null) {
- throw new NotFoundException("Message non trouvé");
- }
-
- if (!message.getSender().getId().equals(senderId)) {
- throw new IllegalStateException("Vous ne pouvez éditer que vos propres messages");
- }
-
- message.setContent(newContent);
- message.markAsEdited();
- messageRepository.persist(message);
-
- return convertToResponse(message);
- }
-
- /**
- * Supprime un message (soft delete)
- */
- @Transactional
- public void deleteMessage(UUID messageId, UUID senderId) {
- Message message = messageRepository.findById(messageId);
- if (message == null) {
- throw new NotFoundException("Message non trouvé");
- }
-
- if (!message.getSender().getId().equals(senderId)) {
- throw new IllegalStateException("Vous ne pouvez supprimer que vos propres messages");
- }
-
- message.setIsDeleted(true);
- message.setContent("[Message supprimé]");
- messageRepository.persist(message);
- }
-
- /**
- * Convertit Message en DTO
- */
- private MessageResponse convertToResponse(Message m) {
- // Parser recipient IDs
- List recipientIds = null;
- if (m.getRecipientIds() != null && !m.getRecipientIds().isEmpty()) {
- recipientIds = List.of(m.getRecipientIds().split(",")).stream()
- .map(UUID::fromString)
- .collect(Collectors.toList());
- }
-
- // Parser roles
- List roles = null;
- if (m.getRecipientRoles() != null && !m.getRecipientRoles().isEmpty()) {
- roles = List.of(m.getRecipientRoles().split(","));
- }
-
- // Parser attachments
- List attachments = null;
- if (m.getAttachments() != null && !m.getAttachments().isEmpty()) {
- attachments = List.of(m.getAttachments().split(","));
- }
-
- return MessageResponse.builder()
- .id(m.getId())
- .conversationId(m.getConversation().getId())
- .senderId(m.getSender().getId())
- .senderName(m.getSenderName())
- .senderAvatar(m.getSenderAvatar())
- .content(m.getContent())
- .type(m.getType())
- .status(m.getStatus())
- .priority(m.getPriority())
- .recipientIds(recipientIds)
- .recipientRoles(roles)
- .organisationId(m.getOrganisation() != null ? m.getOrganisation().getId() : null)
- .createdAt(m.getDateCreation())
- .readAt(m.getReadAt())
- .attachments(attachments)
- .edited(Boolean.TRUE.equals(m.getIsEdited()))
- .editedAt(m.getEditedAt())
- .deleted(Boolean.TRUE.equals(m.getIsDeleted()))
- .build();
- }
-}
diff --git a/src/main/java/dev/lions/unionflow/server/service/MessagingService.java b/src/main/java/dev/lions/unionflow/server/service/MessagingService.java
new file mode 100644
index 0000000..db954c7
--- /dev/null
+++ b/src/main/java/dev/lions/unionflow/server/service/MessagingService.java
@@ -0,0 +1,653 @@
+package dev.lions.unionflow.server.service;
+
+import dev.lions.unionflow.server.api.dto.messagerie.request.BloquerMembreRequest;
+import dev.lions.unionflow.server.api.dto.messagerie.request.DemarrerConversationDirecteRequest;
+import dev.lions.unionflow.server.api.dto.messagerie.request.DemarrerConversationRoleRequest;
+import dev.lions.unionflow.server.api.dto.messagerie.request.EnvoyerMessageRequest;
+import dev.lions.unionflow.server.api.dto.messagerie.request.MettreAJourPolitiqueRequest;
+import dev.lions.unionflow.server.api.dto.messagerie.response.ContactPolicyResponse;
+import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationResponse;
+import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationSummaryResponse;
+import dev.lions.unionflow.server.api.dto.messagerie.response.MessageResponse;
+import dev.lions.unionflow.server.api.enums.messagerie.StatutConversation;
+import dev.lions.unionflow.server.api.enums.messagerie.TypeContenu;
+import dev.lions.unionflow.server.api.enums.messagerie.TypeConversation;
+import dev.lions.unionflow.server.api.enums.messagerie.TypePolitiqueCommunication;
+import dev.lions.unionflow.server.entity.ContactPolicy;
+import dev.lions.unionflow.server.entity.Conversation;
+import dev.lions.unionflow.server.entity.ConversationParticipant;
+import dev.lions.unionflow.server.entity.MemberBlock;
+import dev.lions.unionflow.server.entity.Membre;
+import dev.lions.unionflow.server.entity.MembreOrganisation;
+import dev.lions.unionflow.server.entity.Message;
+import dev.lions.unionflow.server.entity.Organisation;
+import dev.lions.unionflow.server.repository.ContactPolicyRepository;
+import dev.lions.unionflow.server.repository.ConversationParticipantRepository;
+import dev.lions.unionflow.server.repository.ConversationRepository;
+import dev.lions.unionflow.server.repository.MemberBlockRepository;
+import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
+import dev.lions.unionflow.server.repository.MembreRepository;
+import dev.lions.unionflow.server.repository.MessageRepository;
+import dev.lions.unionflow.server.messaging.KafkaEventProducer;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jakarta.transaction.Transactional;
+import jakarta.ws.rs.BadRequestException;
+import jakarta.ws.rs.ForbiddenException;
+import jakarta.ws.rs.NotFoundException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import org.jboss.logging.Logger;
+
+/**
+ * Service métier pour la messagerie instantanée.
+ *
+ * Gère les conversations (directes et canaux-rôle), les messages (texte,
+ * vocal, image), les blocages et les politiques de communication.
+ *
+ *
Politique par appartenance : deux membres de la même organisation
+ * peuvent se contacter sans demande d'amitié préalable.
+ * L'adhésion est la relation de confiance.
+ *
+ * @author UnionFlow Team
+ * @version 4.0
+ * @since 2026-04-13
+ */
+@ApplicationScoped
+public class MessagingService {
+
+ private static final Logger LOG = Logger.getLogger(MessagingService.class);
+ private static final int PAGE_SIZE_DEFAULT = 30;
+
+ @Inject ConversationRepository conversationRepository;
+ @Inject ConversationParticipantRepository participantRepository;
+ @Inject MessageRepository messageRepository;
+ @Inject ContactPolicyRepository contactPolicyRepository;
+ @Inject MemberBlockRepository memberBlockRepository;
+ @Inject MembreRepository membreRepository;
+ @Inject MembreOrganisationRepository membreOrganisationRepository;
+ @Inject KafkaEventProducer kafkaEventProducer;
+ @Inject io.quarkus.security.identity.SecurityIdentity securityIdentity;
+
+ // ── Conversations ─────────────────────────────────────────────────────────
+
+ /**
+ * Démarre ou récupère une conversation directe 1-1.
+ * Idempotent : si la conversation existe déjà, elle est retournée.
+ */
+ @Transactional
+ public ConversationResponse demarrerConversationDirecte(DemarrerConversationDirecteRequest request) {
+ Membre moi = getMembreConnecte();
+ Membre destinataire = membreRepository.findById(request.destinataireId());
+ if (destinataire == null) {
+ throw new NotFoundException("Membre destinataire non trouvé : " + request.destinataireId());
+ }
+
+ UUID orgId = request.organisationId();
+ verifierAppartenance(moi.getId(), orgId);
+ verifierAppartenance(request.destinataireId(), orgId);
+ verifierPolitique(moi.getId(), request.destinataireId(), orgId, false);
+
+ // Idempotence : chercher une conversation directe existante
+ return conversationRepository
+ .findConversationDirecte(moi.getId(), request.destinataireId(), orgId)
+ .map(c -> {
+ // Envoyer le message initial si fourni
+ if (request.contenuInitial() != null && !request.contenuInitial().isBlank()) {
+ envoyerMessageDansConversation(c, moi, request.contenuInitial(), TypeContenu.TEXTE, null, null, null);
+ }
+ return toConversationResponse(c, moi.getId());
+ })
+ .orElseGet(() -> {
+ Organisation org = getOrganisation(orgId);
+ Conversation conv = Conversation.builder()
+ .organisation(org)
+ .typeConversation(TypeConversation.DIRECTE)
+ .statut(StatutConversation.ACTIVE)
+ .build();
+ conversationRepository.persist(conv);
+
+ ajouterParticipant(conv, moi, "INITIATEUR");
+ ajouterParticipant(conv, destinataire, "PARTICIPANT");
+
+ if (request.contenuInitial() != null && !request.contenuInitial().isBlank()) {
+ envoyerMessageDansConversation(conv, moi, request.contenuInitial(), TypeContenu.TEXTE, null, null, null);
+ }
+
+ LOG.infof("Conversation directe créée: %s ↔ %s dans org %s",
+ moi.getEmail(), destinataire.getEmail(), orgId);
+ return toConversationResponse(conv, moi.getId());
+ });
+ }
+
+ /**
+ * Démarre ou récupère un canal de rôle.
+ * Le canal est partagé : tous les membres qui contactent "Le Trésorier"
+ * aboutissent dans le même canal.
+ */
+ @Transactional
+ public ConversationResponse demarrerConversationRole(DemarrerConversationRoleRequest request) {
+ Membre moi = getMembreConnecte();
+ UUID orgId = request.organisationId();
+ verifierAppartenance(moi.getId(), orgId);
+ verifierPolitique(moi.getId(), null, orgId, true);
+
+ String roleCible = request.roleCible();
+ List porteurs = trouverPorteursDuRole(orgId, roleCible);
+ if (porteurs.isEmpty()) {
+ throw new NotFoundException("Aucun membre avec le rôle " + roleCible + " dans cette organisation");
+ }
+
+ Organisation org = getOrganisation(orgId);
+ String titreCanal = libelleDuRole(roleCible);
+
+ return conversationRepository.findCanalRole(orgId, roleCible)
+ .map(c -> {
+ // Ajouter l'initiateur s'il n'est pas encore participant
+ if (!participantRepository.estParticipant(c.getId(), moi.getId())) {
+ ajouterParticipant(c, moi, "PARTICIPANT");
+ }
+ envoyerMessageDansConversation(c, moi, request.contenuInitial(), TypeContenu.TEXTE, null, null, null);
+ return toConversationResponse(c, moi.getId());
+ })
+ .orElseGet(() -> {
+ Conversation conv = Conversation.builder()
+ .organisation(org)
+ .typeConversation(TypeConversation.ROLE_CANAL)
+ .roleCible(roleCible)
+ .titre(titreCanal)
+ .statut(StatutConversation.ACTIVE)
+ .build();
+ conversationRepository.persist(conv);
+
+ ajouterParticipant(conv, moi, "INITIATEUR");
+ porteurs.forEach(p -> ajouterParticipant(conv, p, "MODERATEUR"));
+
+ envoyerMessageDansConversation(conv, moi, request.contenuInitial(), TypeContenu.TEXTE, null, null, null);
+
+ LOG.infof("Canal rôle créé: %s dans org %s", roleCible, orgId);
+ return toConversationResponse(conv, moi.getId());
+ });
+ }
+
+ /**
+ * Retourne la liste des conversations du membre connecté.
+ */
+ public List getMesConversations() {
+ Membre moi = getMembreConnecte();
+ return conversationRepository.findByMembreId(moi.getId()).stream()
+ .map(c -> toConversationSummary(c, moi.getId()))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Retourne le détail d'une conversation (avec les derniers messages).
+ */
+ public ConversationResponse getConversation(UUID conversationId) {
+ Membre moi = getMembreConnecte();
+ Conversation conv = conversationRepository.findConversationById(conversationId)
+ .orElseThrow(() -> new NotFoundException("Conversation non trouvée : " + conversationId));
+ verifierParticipant(conv, moi.getId());
+ return toConversationResponse(conv, moi.getId());
+ }
+
+ /**
+ * Archive une conversation.
+ */
+ @Transactional
+ public ConversationResponse archiverConversation(UUID conversationId) {
+ Membre moi = getMembreConnecte();
+ Conversation conv = conversationRepository.findConversationById(conversationId)
+ .orElseThrow(() -> new NotFoundException("Conversation non trouvée : " + conversationId));
+ verifierParticipant(conv, moi.getId());
+ conv.archiver();
+ return toConversationResponse(conv, moi.getId());
+ }
+
+ // ── Messages ──────────────────────────────────────────────────────────────
+
+ /**
+ * Envoie un message dans une conversation existante.
+ */
+ @Transactional
+ public MessageResponse envoyerMessage(UUID conversationId, EnvoyerMessageRequest request) {
+ Membre moi = getMembreConnecte();
+ Conversation conv = conversationRepository.findConversationById(conversationId)
+ .orElseThrow(() -> new NotFoundException("Conversation non trouvée : " + conversationId));
+
+ verifierParticipant(conv, moi.getId());
+ if (!conv.estActive()) {
+ throw new BadRequestException("Cette conversation est archivée");
+ }
+
+ TypeContenu type = parseTypeContenu(request.typeMessage());
+ validerContenuMessage(type, request);
+
+ Message message = envoyerMessageDansConversation(
+ conv, moi,
+ request.contenu(), type,
+ request.urlFichier(), request.dureeAudio(),
+ request.messageParentId()
+ );
+ return toMessageResponse(message);
+ }
+
+ /**
+ * Récupère les messages d'une conversation (paginés).
+ */
+ public List getMessages(UUID conversationId, int page) {
+ Membre moi = getMembreConnecte();
+ Conversation conv = conversationRepository.findConversationById(conversationId)
+ .orElseThrow(() -> new NotFoundException("Conversation non trouvée : " + conversationId));
+ verifierParticipant(conv, moi.getId());
+
+ return messageRepository.findByConversationPagine(conversationId, page, PAGE_SIZE_DEFAULT)
+ .stream()
+ .map(this::toMessageResponse)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Marque tous les messages d'une conversation comme lus.
+ */
+ @Transactional
+ public void marquerConversationLue(UUID conversationId) {
+ Membre moi = getMembreConnecte();
+ participantRepository.findParticipant(conversationId, moi.getId())
+ .ifPresent(p -> {
+ p.marquerLu();
+ LOG.debugf("Conversation %s marquée lue par %s", conversationId, moi.getEmail());
+ });
+ }
+
+ /**
+ * Supprime un message (soft delete — contenu remplacé par "[Message supprimé]").
+ */
+ @Transactional
+ public void supprimerMessage(UUID conversationId, UUID messageId) {
+ Membre moi = getMembreConnecte();
+ Message message = messageRepository.findMessageById(messageId)
+ .orElseThrow(() -> new NotFoundException("Message non trouvé : " + messageId));
+
+ if (!message.getConversation().getId().equals(conversationId)) {
+ throw new NotFoundException("Message non trouvé dans cette conversation");
+ }
+ if (!message.getExpediteur().getId().equals(moi.getId())) {
+ throw new ForbiddenException("Vous ne pouvez supprimer que vos propres messages");
+ }
+ message.supprimer();
+ }
+
+ // ── Blocages ──────────────────────────────────────────────────────────────
+
+ /**
+ * Bloque un membre dans une organisation.
+ */
+ @Transactional
+ public void bloquerMembre(BloquerMembreRequest request) {
+ Membre moi = getMembreConnecte();
+ UUID membreABloquerId = request.membreABloquerId();
+ UUID orgId = request.organisationId();
+
+ if (moi.getId().equals(membreABloquerId)) {
+ throw new BadRequestException("Vous ne pouvez pas vous bloquer vous-même");
+ }
+
+ Membre aBloquer = membreRepository.findById(membreABloquerId);
+ if (aBloquer == null) {
+ throw new NotFoundException("Membre non trouvé : " + membreABloquerId);
+ }
+
+ if (memberBlockRepository.estBloque(moi.getId(), membreABloquerId, orgId)) {
+ throw new BadRequestException("Ce membre est déjà bloqué");
+ }
+
+ Organisation org = getOrganisation(orgId);
+ MemberBlock block = MemberBlock.builder()
+ .bloqueur(moi)
+ .bloque(aBloquer)
+ .organisation(org)
+ .build();
+ memberBlockRepository.persist(block);
+ LOG.infof("%s a bloqué %s dans org %s", moi.getEmail(), aBloquer.getEmail(), orgId);
+ }
+
+ /**
+ * Débloque un membre dans une organisation.
+ */
+ @Transactional
+ public void debloquerMembre(UUID membreId, UUID organisationId) {
+ Membre moi = getMembreConnecte();
+ MemberBlock block = memberBlockRepository.findBlocage(moi.getId(), membreId, organisationId)
+ .orElseThrow(() -> new NotFoundException("Aucun blocage trouvé pour ce membre"));
+ block.setActif(false);
+ }
+
+ /**
+ * Retourne la liste des membres bloqués par le membre connecté.
+ */
+ public List getMesBlocages() {
+ Membre moi = getMembreConnecte();
+ return memberBlockRepository.findByBloqueur(moi.getId());
+ }
+
+ // ── Politique de communication ────────────────────────────────────────────
+
+ /**
+ * Retourne la politique de communication d'une organisation.
+ * Crée une politique par défaut si elle n'existe pas encore.
+ */
+ @Transactional
+ public ContactPolicyResponse getPolitique(UUID organisationId) {
+ ContactPolicy policy = contactPolicyRepository.findByOrganisationId(organisationId)
+ .orElseGet(() -> creerPolitiqueParDefaut(organisationId));
+ return toContactPolicyResponse(policy);
+ }
+
+ /**
+ * Met à jour la politique de communication.
+ * Réservé aux ADMIN et ADMIN_ORGANISATION.
+ */
+ @Transactional
+ public ContactPolicyResponse mettreAJourPolitique(UUID organisationId, MettreAJourPolitiqueRequest request) {
+ ContactPolicy policy = contactPolicyRepository.findByOrganisationId(organisationId)
+ .orElseGet(() -> creerPolitiqueParDefaut(organisationId));
+
+ if (request.typePolitique() != null) {
+ policy.setTypePolitique(TypePolitiqueCommunication.valueOf(request.typePolitique()));
+ }
+ if (request.autoriserMembreVersMembre() != null) {
+ policy.setAutoriserMembreVersMembre(request.autoriserMembreVersMembre());
+ }
+ if (request.autoriserMembreVersRole() != null) {
+ policy.setAutoriserMembreVersRole(request.autoriserMembreVersRole());
+ }
+ if (request.autoriserNotesVocales() != null) {
+ policy.setAutoriserNotesVocales(request.autoriserNotesVocales());
+ }
+ return toContactPolicyResponse(policy);
+ }
+
+ // ── Méthodes privées ──────────────────────────────────────────────────────
+
+ private Message envoyerMessageDansConversation(
+ Conversation conv, Membre expediteur,
+ String contenu, TypeContenu type,
+ String urlFichier, Integer dureeAudio,
+ UUID messageParentId) {
+
+ Message.MessageBuilder builder = Message.builder()
+ .conversation(conv)
+ .expediteur(expediteur)
+ .typeMessage(type)
+ .contenu(contenu)
+ .urlFichier(urlFichier)
+ .dureeAudio(dureeAudio);
+
+ if (messageParentId != null) {
+ messageRepository.findMessageById(messageParentId)
+ .ifPresent(builder::messageParent);
+ }
+
+ Message message = builder.build();
+ messageRepository.persist(message);
+ conv.enregistrerNouveauMessage();
+
+ // Notifier via Kafka → WebSocket
+ try {
+ java.util.Map data = new java.util.HashMap<>();
+ data.put("conversationId", conv.getId().toString());
+ data.put("messageId", message.getId() != null ? message.getId().toString() : "");
+ data.put("expediteurId", expediteur.getId().toString());
+ data.put("typeMessage", type.name());
+ kafkaEventProducer.publishNouveauMessage(conv.getId(), conv.getOrganisation().getId().toString(), data);
+ } catch (Exception e) {
+ LOG.warnf("Impossible de publier l'event Kafka pour le message: %s", e.getMessage());
+ }
+
+ return message;
+ }
+
+ private void ajouterParticipant(Conversation conv, Membre membre, String role) {
+ if (!participantRepository.estParticipant(conv.getId(), membre.getId())) {
+ ConversationParticipant participant = ConversationParticipant.builder()
+ .conversation(conv)
+ .membre(membre)
+ .roleDansConversation(role)
+ .notifier(true)
+ .build();
+ participantRepository.persist(participant);
+ }
+ }
+
+ private void verifierAppartenance(UUID membreId, UUID organisationId) {
+ boolean appartient = membreOrganisationRepository
+ .count("membre.id = ?1 AND organisation.id = ?2 AND actif = true", membreId, organisationId) > 0;
+ if (!appartient) {
+ throw new ForbiddenException("Le membre n'appartient pas à cette organisation");
+ }
+ }
+
+ private void verifierPolitique(UUID expediteurId, UUID destinataireId, UUID orgId, boolean versRole) {
+ contactPolicyRepository.findByOrganisationId(orgId).ifPresent(policy -> {
+ if (versRole && !policy.getAutoriserMembreVersRole()) {
+ throw new ForbiddenException("La politique de cette organisation n'autorise pas les contacts vers les rôles");
+ }
+ if (!versRole && !policy.getAutoriserMembreVersMembre()) {
+ throw new ForbiddenException("La politique de cette organisation n'autorise pas les contacts entre membres");
+ }
+ });
+
+ // Vérifier le blocage
+ if (destinataireId != null && memberBlockRepository.estBloque(destinataireId, expediteurId, orgId)) {
+ throw new ForbiddenException("Vous ne pouvez pas contacter ce membre");
+ }
+ }
+
+ private void verifierParticipant(Conversation conv, UUID membreId) {
+ if (!participantRepository.estParticipant(conv.getId(), membreId)) {
+ throw new ForbiddenException("Vous n'êtes pas participant à cette conversation");
+ }
+ }
+
+ private List trouverPorteursDuRole(UUID orgId, String role) {
+ List membresOrg = membreOrganisationRepository
+ .find("organisation.id = ?1 AND roleOrg = ?2 AND actif = true", orgId, role)
+ .list();
+ return membresOrg.stream()
+ .map(MembreOrganisation::getMembre)
+ .collect(Collectors.toList());
+ }
+
+ private ContactPolicy creerPolitiqueParDefaut(UUID organisationId) {
+ Organisation org = getOrganisation(organisationId);
+ ContactPolicy policy = ContactPolicy.builder()
+ .organisation(org)
+ .typePolitique(TypePolitiqueCommunication.OUVERT)
+ .autoriserMembreVersMembre(true)
+ .autoriserMembreVersRole(true)
+ .autoriserNotesVocales(true)
+ .build();
+ contactPolicyRepository.persist(policy);
+ return policy;
+ }
+
+ private Membre getMembreConnecte() {
+ String email = securityIdentity.getPrincipal().getName();
+ return membreRepository.find("email", email).firstResult();
+ }
+
+ private Organisation getOrganisation(UUID orgId) {
+ return (Organisation) dev.lions.unionflow.server.entity.Organisation.findById(orgId);
+ }
+
+ private TypeContenu parseTypeContenu(String type) {
+ if (type == null || type.isBlank()) return TypeContenu.TEXTE;
+ try {
+ return TypeContenu.valueOf(type.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ return TypeContenu.TEXTE;
+ }
+ }
+
+ private void validerContenuMessage(TypeContenu type, EnvoyerMessageRequest req) {
+ switch (type) {
+ case TEXTE:
+ if (req.contenu() == null || req.contenu().isBlank()) {
+ throw new BadRequestException("Le contenu est obligatoire pour un message texte");
+ }
+ break;
+ case VOCAL:
+ if (req.urlFichier() == null || req.urlFichier().isBlank()) {
+ throw new BadRequestException("L'URL du fichier audio est obligatoire pour une note vocale");
+ }
+ if (req.dureeAudio() == null) {
+ throw new BadRequestException("La durée audio est obligatoire pour une note vocale");
+ }
+ break;
+ case IMAGE:
+ if (req.urlFichier() == null || req.urlFichier().isBlank()) {
+ throw new BadRequestException("L'URL de l'image est obligatoire");
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ private String libelleDuRole(String role) {
+ return switch (role) {
+ case "PRESIDENT" -> "Président";
+ case "TRESORIER" -> "Trésorier";
+ case "SECRETAIRE" -> "Secrétaire";
+ case "VICE_PRESIDENT" -> "Vice-Président";
+ case "ADMIN" -> "Administrateur";
+ case "ADMIN_ORGANISATION" -> "Administrateur";
+ default -> role;
+ };
+ }
+
+ // ── Conversions DTO ───────────────────────────────────────────────────────
+
+ private ConversationResponse toConversationResponse(Conversation conv, UUID membreConnecteId) {
+ List msgs = messageRepository
+ .findByConversationPagine(conv.getId(), 0, PAGE_SIZE_DEFAULT)
+ .stream().map(this::toMessageResponse).collect(Collectors.toList());
+
+ List parts =
+ participantRepository.findByConversation(conv.getId()).stream()
+ .map(p -> ConversationResponse.ParticipantResponse.builder()
+ .membreId(p.getMembre().getId())
+ .prenom(p.getMembre().getPrenom())
+ .nom(p.getMembre().getNom())
+ .roleDansConversation(p.getRoleDansConversation())
+ .luJusqua(p.getLuJusqua())
+ .build())
+ .collect(Collectors.toList());
+
+ long nonLus = messageRepository.countNonLus(conv.getId(), membreConnecteId);
+
+ return ConversationResponse.builder()
+ .id(conv.getId())
+ .typeConversation(conv.getTypeConversation().name())
+ .titre(resolverTitre(conv, membreConnecteId))
+ .statut(conv.getStatut().name())
+ .roleCible(conv.getRoleCible())
+ .organisationId(conv.getOrganisation().getId())
+ .organisationNom(conv.getOrganisation().getNom())
+ .dateCreation(conv.getDateCreation())
+ .dernierMessageAt(conv.getDernierMessageAt())
+ .nombreMessages(conv.getNombreMessages())
+ .participants(parts)
+ .messages(msgs)
+ .nonLus(nonLus)
+ .build();
+ }
+
+ private ConversationSummaryResponse toConversationSummary(Conversation conv, UUID membreConnecteId) {
+ String apercu = messageRepository.findDernierMessage(conv.getId())
+ .map(m -> {
+ if (TypeContenu.VOCAL.equals(m.getTypeMessage())) return "🎤 Note vocale";
+ if (TypeContenu.IMAGE.equals(m.getTypeMessage())) return "📷 Image";
+ String c = m.getContenu();
+ return c != null && c.length() > 100 ? c.substring(0, 97) + "..." : c;
+ })
+ .orElse(null);
+
+ String dernierType = messageRepository.findDernierMessage(conv.getId())
+ .map(m -> m.getTypeMessage().name()).orElse(null);
+
+ long nonLus = messageRepository.countNonLus(conv.getId(), membreConnecteId);
+
+ return ConversationSummaryResponse.builder()
+ .id(conv.getId())
+ .typeConversation(conv.getTypeConversation().name())
+ .titre(resolverTitre(conv, membreConnecteId))
+ .statut(conv.getStatut().name())
+ .dernierMessageApercu(apercu)
+ .dernierMessageType(dernierType)
+ .dernierMessageAt(conv.getDernierMessageAt())
+ .nonLus(nonLus)
+ .organisationId(conv.getOrganisation().getId())
+ .build();
+ }
+
+ private MessageResponse toMessageResponse(Message message) {
+ String contenuAffiche = message.estSupprime()
+ ? "[Message supprimé]"
+ : message.getContenu();
+
+ String parentApercu = null;
+ UUID parentId = null;
+ if (message.getMessageParent() != null) {
+ parentId = message.getMessageParent().getId();
+ String pc = message.getMessageParent().getContenu();
+ parentApercu = pc != null && pc.length() > 100 ? pc.substring(0, 97) + "..." : pc;
+ }
+
+ return MessageResponse.builder()
+ .id(message.getId())
+ .typeMessage(message.getTypeMessage().name())
+ .contenu(contenuAffiche)
+ .urlFichier(message.estSupprime() ? null : message.getUrlFichier())
+ .dureeAudio(message.getDureeAudio())
+ .supprime(message.estSupprime())
+ .expediteurId(message.getExpediteur().getId())
+ .expediteurNom(message.getExpediteur().getNom())
+ .expediteurPrenom(message.getExpediteur().getPrenom())
+ .messageParentId(parentId)
+ .messageParentApercu(parentApercu)
+ .dateEnvoi(message.getDateCreation())
+ .build();
+ }
+
+ private ContactPolicyResponse toContactPolicyResponse(ContactPolicy policy) {
+ return ContactPolicyResponse.builder()
+ .id(policy.getId())
+ .organisationId(policy.getOrganisation().getId())
+ .typePolitique(policy.getTypePolitique().name())
+ .autoriserMembreVersMembre(Boolean.TRUE.equals(policy.getAutoriserMembreVersMembre()))
+ .autoriserMembreVersRole(Boolean.TRUE.equals(policy.getAutoriserMembreVersRole()))
+ .autoriserNotesVocales(Boolean.TRUE.equals(policy.getAutoriserNotesVocales()))
+ .build();
+ }
+
+ /**
+ * Résout le titre affiché pour une conversation.
+ * Pour DIRECTE : "Prénom Nom" de l'autre participant.
+ * Pour ROLE_CANAL : le titre du canal.
+ */
+ private String resolverTitre(Conversation conv, UUID membreConnecteId) {
+ if (conv.getTitre() != null) return conv.getTitre();
+ if (TypeConversation.DIRECTE.equals(conv.getTypeConversation())) {
+ return participantRepository.findByConversation(conv.getId()).stream()
+ .filter(p -> !p.getMembre().getId().equals(membreConnecteId))
+ .findFirst()
+ .map(p -> p.getMembre().getPrenom() + " " + p.getMembre().getNom())
+ .orElse("Conversation");
+ }
+ return conv.getRoleCible();
+ }
+}
diff --git a/src/main/resources/db/migration/V28__Create_Messagerie_Tables.sql b/src/main/resources/db/migration/V28__Create_Messagerie_Tables.sql
new file mode 100644
index 0000000..6906df3
--- /dev/null
+++ b/src/main/resources/db/migration/V28__Create_Messagerie_Tables.sql
@@ -0,0 +1,237 @@
+-- ============================================================================
+-- V28 — Messagerie temps réel entre membres
+--
+-- Crée les tables nécessaires pour la messagerie instantanée :
+-- - contact_policies : politique de communication par organisation
+-- - member_blocks : blocages unilatéraux entre membres
+-- - conversations : fil de discussion (directe ou canal-rôle)
+-- - conversation_participants : membres d'une conversation
+-- - messages : messages texte, vocal ou image
+--
+-- Les notes vocales sont stockées comme messages de type VOCAL avec
+-- url_fichier + duree_audio. Aucune transcription en V1.
+--
+-- Auteur : UnionFlow Team
+-- Version : 4.0
+-- Date : 2026-04-13
+-- ============================================================================
+
+-- ── Politique de communication par organisation ───────────────────────────────
+CREATE TABLE IF NOT EXISTS contact_policies (
+ id UUID NOT NULL PRIMARY KEY,
+ date_creation TIMESTAMP NOT NULL,
+ date_modification TIMESTAMP,
+ cree_par VARCHAR(255),
+ modifie_par VARCHAR(255),
+ version BIGINT,
+ actif BOOLEAN NOT NULL DEFAULT TRUE,
+
+ organisation_id UUID NOT NULL REFERENCES organisations(id),
+ type_politique VARCHAR(30) NOT NULL DEFAULT 'OUVERT',
+ autoriser_membre_vers_membre BOOLEAN NOT NULL DEFAULT TRUE,
+ autoriser_membre_vers_role BOOLEAN NOT NULL DEFAULT TRUE,
+ autoriser_notes_vocales BOOLEAN NOT NULL DEFAULT TRUE,
+
+ CONSTRAINT uk_contact_policy_org UNIQUE (organisation_id)
+);
+
+-- ── Blocages unilatéraux ──────────────────────────────────────────────────────
+CREATE TABLE IF NOT EXISTS member_blocks (
+ id UUID NOT NULL PRIMARY KEY,
+ date_creation TIMESTAMP NOT NULL,
+ date_modification TIMESTAMP,
+ cree_par VARCHAR(255),
+ modifie_par VARCHAR(255),
+ version BIGINT,
+ actif BOOLEAN NOT NULL DEFAULT TRUE,
+
+ bloqueur_id UUID NOT NULL REFERENCES utilisateurs(id),
+ bloque_id UUID NOT NULL REFERENCES utilisateurs(id),
+ organisation_id UUID NOT NULL REFERENCES organisations(id),
+
+ CONSTRAINT uk_member_block UNIQUE (bloqueur_id, bloque_id, organisation_id),
+ CONSTRAINT chk_block_self CHECK (bloqueur_id <> bloque_id)
+);
+
+-- ── Conversations ─────────────────────────────────────────────────────────────
+CREATE TABLE IF NOT EXISTS conversations (
+ id UUID NOT NULL PRIMARY KEY,
+ date_creation TIMESTAMP NOT NULL,
+ date_modification TIMESTAMP,
+ cree_par VARCHAR(255),
+ modifie_par VARCHAR(255),
+ version BIGINT,
+ actif BOOLEAN NOT NULL DEFAULT TRUE,
+
+ organisation_id UUID NOT NULL REFERENCES organisations(id),
+ -- DIRECTE | ROLE_CANAL | GROUPE
+ type_conversation VARCHAR(30) NOT NULL,
+ -- Pour ROLE_CANAL : PRESIDENT, TRESORIER, SECRETAIRE, etc.
+ role_cible VARCHAR(50),
+ -- Titre affiché (nom du rôle ou du groupe)
+ titre VARCHAR(200),
+ -- ACTIVE | ARCHIVEE
+ statut VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
+ dernier_message_at TIMESTAMP,
+ nombre_messages INTEGER NOT NULL DEFAULT 0
+);
+
+-- ── Participants aux conversations ────────────────────────────────────────────
+CREATE TABLE IF NOT EXISTS conversation_participants (
+ id UUID NOT NULL PRIMARY KEY,
+ date_creation TIMESTAMP NOT NULL,
+ date_modification TIMESTAMP,
+ cree_par VARCHAR(255),
+ modifie_par VARCHAR(255),
+ version BIGINT,
+ actif BOOLEAN NOT NULL DEFAULT TRUE,
+
+ conversation_id UUID NOT NULL REFERENCES conversations(id),
+ membre_id UUID NOT NULL REFERENCES utilisateurs(id),
+ -- INITIATEUR | PARTICIPANT | MODERATEUR
+ role_dans_conversation VARCHAR(50) DEFAULT 'PARTICIPANT',
+ -- Dernier message lu par ce participant
+ lu_jusqu_a TIMESTAMP,
+ notifier BOOLEAN NOT NULL DEFAULT TRUE,
+
+ CONSTRAINT uk_conv_participant UNIQUE (conversation_id, membre_id)
+);
+
+-- ── Messages ──────────────────────────────────────────────────────────────────
+CREATE TABLE IF NOT EXISTS messages (
+ id UUID NOT NULL PRIMARY KEY,
+ date_creation TIMESTAMP NOT NULL,
+ date_modification TIMESTAMP,
+ cree_par VARCHAR(255),
+ modifie_par VARCHAR(255),
+ version BIGINT,
+ actif BOOLEAN NOT NULL DEFAULT TRUE,
+
+ conversation_id UUID NOT NULL REFERENCES conversations(id),
+ expediteur_id UUID NOT NULL REFERENCES utilisateurs(id),
+ -- TEXTE | VOCAL | IMAGE | SYSTEME
+ type_message VARCHAR(20) NOT NULL DEFAULT 'TEXTE',
+ -- Contenu textuel (null pour les vocaux/images)
+ contenu TEXT,
+ -- URL du fichier audio ou image (stocké sur object storage)
+ url_fichier VARCHAR(500),
+ -- Durée en secondes pour les notes vocales
+ duree_audio INTEGER,
+ -- Transcription automatique V2 (null en V1)
+ transcription TEXT,
+ -- Réponse à un message (threading léger)
+ message_parent_id UUID REFERENCES messages(id),
+ -- Suppression douce (null = non supprimé)
+ supprime_le TIMESTAMP
+);
+
+-- ── Ajout colonnes v4 manquantes sur tables existantes ───────────────────────
+-- (les tables ont pu être créées par une migration antérieure avec le schéma v1)
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'conversations' AND column_name = 'type_conversation') THEN
+ ALTER TABLE conversations ADD COLUMN type_conversation VARCHAR(30) NOT NULL DEFAULT 'DIRECTE';
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'conversations' AND column_name = 'role_cible') THEN
+ ALTER TABLE conversations ADD COLUMN role_cible VARCHAR(50);
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'conversations' AND column_name = 'titre') THEN
+ ALTER TABLE conversations ADD COLUMN titre VARCHAR(200);
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'conversations' AND column_name = 'statut') THEN
+ ALTER TABLE conversations ADD COLUMN statut VARCHAR(20) NOT NULL DEFAULT 'ACTIVE';
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'conversations' AND column_name = 'dernier_message_at') THEN
+ ALTER TABLE conversations ADD COLUMN dernier_message_at TIMESTAMP;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'conversations' AND column_name = 'nombre_messages') THEN
+ ALTER TABLE conversations ADD COLUMN nombre_messages INTEGER NOT NULL DEFAULT 0;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'conversations' AND column_name = 'organisation_id') THEN
+ ALTER TABLE conversations ADD COLUMN organisation_id UUID REFERENCES organisations(id);
+ END IF;
+ -- Messages : colonnes FK v4 (nécessaires pour les index)
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'messages' AND column_name = 'conversation_id') THEN
+ ALTER TABLE messages ADD COLUMN conversation_id UUID REFERENCES conversations(id);
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'messages' AND column_name = 'expediteur_id') THEN
+ ALTER TABLE messages ADD COLUMN expediteur_id UUID REFERENCES utilisateurs(id);
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'messages' AND column_name = 'message_parent_id') THEN
+ ALTER TABLE messages ADD COLUMN message_parent_id UUID REFERENCES messages(id);
+ END IF;
+ -- Messages : colonnes métier v4
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'messages' AND column_name = 'type_message') THEN
+ ALTER TABLE messages ADD COLUMN type_message VARCHAR(20) NOT NULL DEFAULT 'TEXTE';
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'messages' AND column_name = 'contenu') THEN
+ ALTER TABLE messages ADD COLUMN contenu TEXT;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'messages' AND column_name = 'url_fichier') THEN
+ ALTER TABLE messages ADD COLUMN url_fichier VARCHAR(500);
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'messages' AND column_name = 'duree_audio') THEN
+ ALTER TABLE messages ADD COLUMN duree_audio INTEGER;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'messages' AND column_name = 'transcription') THEN
+ ALTER TABLE messages ADD COLUMN transcription TEXT;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'messages' AND column_name = 'supprime_le') THEN
+ ALTER TABLE messages ADD COLUMN supprime_le TIMESTAMP;
+ END IF;
+ -- ConversationParticipants : colonnes FK v4
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'conversation_participants' AND column_name = 'conversation_id') THEN
+ ALTER TABLE conversation_participants ADD COLUMN conversation_id UUID REFERENCES conversations(id);
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'conversation_participants' AND column_name = 'membre_id') THEN
+ ALTER TABLE conversation_participants ADD COLUMN membre_id UUID REFERENCES utilisateurs(id);
+ END IF;
+ -- ConversationParticipants : colonnes métier v4
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'conversation_participants' AND column_name = 'role_dans_conversation') THEN
+ ALTER TABLE conversation_participants ADD COLUMN role_dans_conversation VARCHAR(50) DEFAULT 'PARTICIPANT';
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'conversation_participants' AND column_name = 'lu_jusqu_a') THEN
+ ALTER TABLE conversation_participants ADD COLUMN lu_jusqu_a TIMESTAMP;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'conversation_participants' AND column_name = 'notifier') THEN
+ ALTER TABLE conversation_participants ADD COLUMN notifier BOOLEAN NOT NULL DEFAULT TRUE;
+ END IF;
+END $$;
+
+-- ── Index de performance ──────────────────────────────────────────────────────
+CREATE INDEX IF NOT EXISTS idx_conversations_organisation ON conversations(organisation_id);
+CREATE INDEX IF NOT EXISTS idx_conversations_statut ON conversations(statut);
+CREATE INDEX IF NOT EXISTS idx_conversations_dernier_msg ON conversations(dernier_message_at DESC NULLS LAST);
+
+CREATE INDEX IF NOT EXISTS idx_conv_part_conversation ON conversation_participants(conversation_id);
+CREATE INDEX IF NOT EXISTS idx_conv_part_membre ON conversation_participants(membre_id);
+
+CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id);
+CREATE INDEX IF NOT EXISTS idx_messages_expediteur ON messages(expediteur_id);
+CREATE INDEX IF NOT EXISTS idx_messages_date_creation ON messages(date_creation DESC);
+CREATE INDEX IF NOT EXISTS idx_messages_parent ON messages(message_parent_id);
+
+CREATE INDEX IF NOT EXISTS idx_contact_policies_org ON contact_policies(organisation_id);
+CREATE INDEX IF NOT EXISTS idx_member_blocks_bloqueur ON member_blocks(bloqueur_id);
+CREATE INDEX IF NOT EXISTS idx_member_blocks_bloque ON member_blocks(bloque_id, organisation_id);
diff --git a/src/test/java/dev/lions/unionflow/server/entity/ConversationTest.java b/src/test/java/dev/lions/unionflow/server/entity/ConversationTest.java
index c294a7d..ce0794b 100644
--- a/src/test/java/dev/lions/unionflow/server/entity/ConversationTest.java
+++ b/src/test/java/dev/lions/unionflow/server/entity/ConversationTest.java
@@ -1,95 +1,166 @@
package dev.lions.unionflow.server.entity;
-import dev.lions.unionflow.server.api.enums.communication.ConversationType;
+import dev.lions.unionflow.server.api.enums.messagerie.StatutConversation;
+import dev.lions.unionflow.server.api.enums.messagerie.TypeConversation;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
-import java.lang.reflect.Method;
-import java.time.LocalDateTime;
+import java.time.LocalDate;
+import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatCode;
@DisplayName("Conversation")
class ConversationTest {
+ private static Membre newMembre() {
+ Membre m = new Membre();
+ m.setId(UUID.randomUUID());
+ m.setNumeroMembre("M1");
+ m.setPrenom("Alpha");
+ m.setNom("Diallo");
+ m.setEmail("alpha@test.com");
+ m.setDateNaissance(LocalDate.now());
+ return m;
+ }
+
+ private static Organisation newOrg() {
+ Organisation org = new Organisation();
+ org.setId(UUID.randomUUID());
+ org.setNom("Tontine Test");
+ return org;
+ }
+
@Test
- @DisplayName("getters/setters de base")
+ @DisplayName("getters/setters — tous les champs")
void gettersSetters() {
- Conversation c = new Conversation();
- c.setName("Groupe Test");
- c.setDescription("Description groupe");
- c.setType(ConversationType.GROUP);
- c.setIsMuted(false);
- c.setIsPinned(true);
- c.setIsArchived(false);
+ Conversation conv = new Conversation();
+ Organisation org = newOrg();
+ conv.setOrganisation(org);
+ conv.setTypeConversation(TypeConversation.DIRECTE);
+ conv.setStatut(StatutConversation.ACTIVE);
+ conv.setTitre("Discussion");
+ conv.setNombreMessages(5);
- assertThat(c.getName()).isEqualTo("Groupe Test");
- assertThat(c.getDescription()).isEqualTo("Description groupe");
- assertThat(c.getType()).isEqualTo(ConversationType.GROUP);
- assertThat(c.getIsMuted()).isFalse();
- assertThat(c.getIsPinned()).isTrue();
- assertThat(c.getIsArchived()).isFalse();
+ assertThat(conv.getOrganisation()).isEqualTo(org);
+ assertThat(conv.getTypeConversation()).isEqualTo(TypeConversation.DIRECTE);
+ assertThat(conv.getStatut()).isEqualTo(StatutConversation.ACTIVE);
+ assertThat(conv.getTitre()).isEqualTo("Discussion");
+ assertThat(conv.getNombreMessages()).isEqualTo(5);
}
@Test
- @DisplayName("onUpdate (PreUpdate) - met à jour updatedAt via réflexion")
- void onUpdate_setsUpdatedAt() throws Exception {
- Conversation c = new Conversation();
- assertThat(c.getUpdatedAt()).isNull();
-
- Method onUpdate = Conversation.class.getDeclaredMethod("onUpdate");
- onUpdate.setAccessible(true);
-
- LocalDateTime before = LocalDateTime.now().minusSeconds(1);
- onUpdate.invoke(c);
- LocalDateTime after = LocalDateTime.now().plusSeconds(1);
-
- assertThat(c.getUpdatedAt()).isNotNull();
- assertThat(c.getUpdatedAt()).isAfter(before);
- assertThat(c.getUpdatedAt()).isBefore(after);
+ @DisplayName("estActive — ACTIVE → true")
+ void estActive_active() {
+ Conversation conv = buildMinimal();
+ conv.setStatut(StatutConversation.ACTIVE);
+ assertThat(conv.estActive()).isTrue();
}
@Test
- @DisplayName("onUpdate appelé deux fois met à jour updatedAt à chaque fois")
- void onUpdate_calledTwice_updatesEachTime() throws Exception {
- Conversation c = new Conversation();
-
- Method onUpdate = Conversation.class.getDeclaredMethod("onUpdate");
- onUpdate.setAccessible(true);
-
- onUpdate.invoke(c);
- LocalDateTime first = c.getUpdatedAt();
-
- // petit délai pour différencier les timestamps
- Thread.sleep(5);
-
- onUpdate.invoke(c);
- LocalDateTime second = c.getUpdatedAt();
-
- assertThat(second).isAfterOrEqualTo(first);
+ @DisplayName("estActive — ARCHIVEE → false")
+ void estActive_archivee() {
+ Conversation conv = buildMinimal();
+ conv.setStatut(StatutConversation.ARCHIVEE);
+ assertThat(conv.estActive()).isFalse();
}
@Test
- @DisplayName("participants initialisé à liste vide")
+ @DisplayName("archiver — passe le statut à ARCHIVEE")
+ void archiver() {
+ Conversation conv = buildMinimal();
+ conv.setStatut(StatutConversation.ACTIVE);
+ conv.archiver();
+ assertThat(conv.getStatut()).isEqualTo(StatutConversation.ARCHIVEE);
+ assertThat(conv.estActive()).isFalse();
+ }
+
+ @Test
+ @DisplayName("enregistrerNouveauMessage — incrémente le compteur et met à jour la date")
+ void enregistrerNouveauMessage() {
+ Conversation conv = buildMinimal();
+ conv.setNombreMessages(3);
+ conv.enregistrerNouveauMessage();
+ assertThat(conv.getNombreMessages()).isEqualTo(4);
+ assertThat(conv.getDernierMessageAt()).isNotNull();
+ }
+
+ @Test
+ @DisplayName("enregistrerNouveauMessage — part de null")
+ void enregistrerNouveauMessage_partDeNull() {
+ Conversation conv = buildMinimal();
+ conv.setNombreMessages(null);
+ conv.enregistrerNouveauMessage();
+ assertThat(conv.getNombreMessages()).isEqualTo(1);
+ }
+
+ @Test
+ @DisplayName("onCreate — initialise statut et nombreMessages si null")
+ void onCreate_initDefaults() {
+ Conversation conv = new Conversation();
+ conv.setOrganisation(newOrg());
+ conv.setTypeConversation(TypeConversation.DIRECTE);
+
+ conv.onCreate();
+
+ assertThat(conv.getStatut()).isEqualTo(StatutConversation.ACTIVE);
+ assertThat(conv.getNombreMessages()).isEqualTo(0);
+ assertThat(conv.getDateCreation()).isNotNull();
+ assertThat(conv.getActif()).isTrue();
+ }
+
+ @Test
+ @DisplayName("ROLE_CANAL — roleCible renseigné")
+ void roleCanalType() {
+ Conversation conv = buildMinimal();
+ conv.setTypeConversation(TypeConversation.ROLE_CANAL);
+ conv.setRoleCible("TRESORIER");
+ conv.setTitre("Trésorier");
+
+ assertThat(conv.getTypeConversation()).isEqualTo(TypeConversation.ROLE_CANAL);
+ assertThat(conv.getRoleCible()).isEqualTo("TRESORIER");
+ }
+
+ @Test
+ @DisplayName("participants initialisé à liste vide (builder default)")
void participants_initializedEmpty() {
- Conversation c = new Conversation();
- assertThat(c.getParticipants()).isNotNull().isEmpty();
+ Conversation conv = Conversation.builder()
+ .organisation(newOrg())
+ .typeConversation(TypeConversation.DIRECTE)
+ .build();
+ assertThat(conv.getParticipants()).isNotNull().isEmpty();
+ assertThat(conv.getMessages()).isNotNull().isEmpty();
}
@Test
- @DisplayName("messages initialisé à liste vide")
- void messages_initializedEmpty() {
- Conversation c = new Conversation();
- assertThat(c.getMessages()).isNotNull().isEmpty();
+ @DisplayName("equals et hashCode")
+ void equalsHashCode() {
+ UUID id = UUID.randomUUID();
+ Organisation org = newOrg();
+
+ Conversation a = buildMinimal();
+ a.setId(id);
+ a.setOrganisation(org);
+ Conversation b = buildMinimal();
+ b.setId(id);
+ b.setOrganisation(org);
+
+ assertThat(a).isEqualTo(b);
+ assertThat(a.hashCode()).isEqualTo(b.hashCode());
}
@Test
- @DisplayName("isMuted et isPinned et isArchived défaut false")
- void defaultFlags_areFalse() {
- Conversation c = new Conversation();
- assertThat(c.getIsMuted()).isFalse();
- assertThat(c.getIsPinned()).isFalse();
- assertThat(c.getIsArchived()).isFalse();
+ @DisplayName("toString non null")
+ void toString_nonNull() {
+ assertThat(buildMinimal().toString()).isNotNull().isNotEmpty();
+ }
+
+ private Conversation buildMinimal() {
+ Conversation conv = new Conversation();
+ conv.setOrganisation(newOrg());
+ conv.setTypeConversation(TypeConversation.DIRECTE);
+ conv.setStatut(StatutConversation.ACTIVE);
+ conv.setNombreMessages(0);
+ return conv;
}
}
diff --git a/src/test/java/dev/lions/unionflow/server/entity/MessageTest.java b/src/test/java/dev/lions/unionflow/server/entity/MessageTest.java
index de8480d..0fd405a 100644
--- a/src/test/java/dev/lions/unionflow/server/entity/MessageTest.java
+++ b/src/test/java/dev/lions/unionflow/server/entity/MessageTest.java
@@ -1,37 +1,173 @@
package dev.lions.unionflow.server.entity;
-import dev.lions.unionflow.server.api.enums.communication.MessageStatus;
+import dev.lions.unionflow.server.api.enums.messagerie.TypeContenu;
+import dev.lions.unionflow.server.api.enums.messagerie.TypeConversation;
+import dev.lions.unionflow.server.api.enums.messagerie.StatutConversation;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
+import java.time.LocalDate;
+import java.util.UUID;
+
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("Message")
class MessageTest {
- @Test
- @DisplayName("markAsRead sets status to READ and sets readAt")
- void markAsRead_setsStatusAndReadAt() {
- Message m = new Message();
- m.setStatus(MessageStatus.SENT);
- assertThat(m.getReadAt()).isNull();
+ private static Membre newMembre() {
+ Membre m = new Membre();
+ m.setId(UUID.randomUUID());
+ m.setNumeroMembre("M1");
+ m.setPrenom("Alpha");
+ m.setNom("Diallo");
+ m.setEmail("alpha@test.com");
+ m.setDateNaissance(LocalDate.now());
+ return m;
+ }
- m.markAsRead();
-
- assertThat(m.getStatus()).isEqualTo(MessageStatus.READ);
- assertThat(m.getReadAt()).isNotNull();
+ private static Conversation newConversation() {
+ Organisation org = new Organisation();
+ org.setId(UUID.randomUUID());
+ org.setNom("Org Test");
+ Conversation c = new Conversation();
+ c.setId(UUID.randomUUID());
+ c.setOrganisation(org);
+ c.setTypeConversation(TypeConversation.DIRECTE);
+ c.setStatut(StatutConversation.ACTIVE);
+ c.setNombreMessages(0);
+ return c;
}
@Test
- @DisplayName("markAsEdited sets isEdited true and sets editedAt")
- void markAsEdited_setsIsEditedAndEditedAt() {
- Message m = new Message();
- assertThat(m.getIsEdited()).isFalse();
- assertThat(m.getEditedAt()).isNull();
+ @DisplayName("getters/setters — message texte")
+ void gettersSetters_texte() {
+ Message msg = new Message();
+ Conversation conv = newConversation();
+ Membre expediteur = newMembre();
+ msg.setConversation(conv);
+ msg.setExpediteur(expediteur);
+ msg.setTypeMessage(TypeContenu.TEXTE);
+ msg.setContenu("Bonjour tout le monde !");
- m.markAsEdited();
+ assertThat(msg.getConversation()).isEqualTo(conv);
+ assertThat(msg.getExpediteur()).isEqualTo(expediteur);
+ assertThat(msg.getTypeMessage()).isEqualTo(TypeContenu.TEXTE);
+ assertThat(msg.getContenu()).isEqualTo("Bonjour tout le monde !");
+ }
- assertThat(m.getIsEdited()).isTrue();
- assertThat(m.getEditedAt()).isNotNull();
+ @Test
+ @DisplayName("getters/setters — note vocale")
+ void gettersSetters_vocal() {
+ Message msg = new Message();
+ msg.setConversation(newConversation());
+ msg.setExpediteur(newMembre());
+ msg.setTypeMessage(TypeContenu.VOCAL);
+ msg.setUrlFichier("https://storage.example.com/audio.opus");
+ msg.setDureeAudio(45);
+
+ assertThat(msg.getTypeMessage()).isEqualTo(TypeContenu.VOCAL);
+ assertThat(msg.getUrlFichier()).isEqualTo("https://storage.example.com/audio.opus");
+ assertThat(msg.getDureeAudio()).isEqualTo(45);
+ }
+
+ @Test
+ @DisplayName("estTextuel — TEXTE → true")
+ void estTextuel_true() {
+ Message msg = buildMinimal(TypeContenu.TEXTE);
+ assertThat(msg.estTextuel()).isTrue();
+ assertThat(msg.estVocal()).isFalse();
+ }
+
+ @Test
+ @DisplayName("estVocal — VOCAL → true")
+ void estVocal_true() {
+ Message msg = buildMinimal(TypeContenu.VOCAL);
+ assertThat(msg.estVocal()).isTrue();
+ assertThat(msg.estTextuel()).isFalse();
+ }
+
+ @Test
+ @DisplayName("estSupprime — null → false")
+ void estSupprime_nullFalse() {
+ Message msg = buildMinimal(TypeContenu.TEXTE);
+ msg.setSupprimeLe(null);
+ assertThat(msg.estSupprime()).isFalse();
+ }
+
+ @Test
+ @DisplayName("supprimer — contenu remplacé par marqueur, urlFichier null")
+ void supprimer() {
+ Message msg = buildMinimal(TypeContenu.TEXTE);
+ msg.setContenu("Message secret");
+ msg.setUrlFichier("https://example.com/file");
+
+ msg.supprimer();
+
+ assertThat(msg.estSupprime()).isTrue();
+ assertThat(msg.getContenu()).isEqualTo("[Message supprimé]");
+ assertThat(msg.getUrlFichier()).isNull();
+ assertThat(msg.getSupprimeLe()).isNotNull();
+ }
+
+ @Test
+ @DisplayName("onCreate — initialise typeMessage si null")
+ void onCreate_initDefaults() {
+ Message msg = new Message();
+ msg.setConversation(newConversation());
+ msg.setExpediteur(newMembre());
+ msg.setContenu("Test");
+
+ msg.onCreate();
+
+ assertThat(msg.getTypeMessage()).isEqualTo(TypeContenu.TEXTE);
+ assertThat(msg.getDateCreation()).isNotNull();
+ assertThat(msg.getActif()).isTrue();
+ }
+
+ @Test
+ @DisplayName("onCreate — préserve typeMessage existant")
+ void onCreate_preservesType() {
+ Message msg = new Message();
+ msg.setConversation(newConversation());
+ msg.setExpediteur(newMembre());
+ msg.setTypeMessage(TypeContenu.VOCAL);
+ msg.setUrlFichier("url");
+ msg.setDureeAudio(30);
+
+ msg.onCreate();
+
+ assertThat(msg.getTypeMessage()).isEqualTo(TypeContenu.VOCAL);
+ }
+
+ @Test
+ @DisplayName("equals et hashCode")
+ void equalsHashCode() {
+ UUID id = UUID.randomUUID();
+ Message a = buildMinimal(TypeContenu.TEXTE);
+ a.setId(id);
+ Message b = buildMinimal(TypeContenu.TEXTE);
+ b.setId(id);
+ assertThat(a).isEqualTo(b);
+ assertThat(a.hashCode()).isEqualTo(b.hashCode());
+ }
+
+ @Test
+ @DisplayName("toString non null")
+ void toString_nonNull() {
+ assertThat(buildMinimal(TypeContenu.TEXTE).toString()).isNotNull().isNotEmpty();
+ }
+
+ private Message buildMinimal(TypeContenu type) {
+ Message msg = new Message();
+ msg.setConversation(newConversation());
+ msg.setExpediteur(newMembre());
+ msg.setTypeMessage(type);
+ if (TypeContenu.TEXTE.equals(type)) {
+ msg.setContenu("Texte test");
+ } else if (TypeContenu.VOCAL.equals(type)) {
+ msg.setUrlFichier("https://example.com/audio.opus");
+ msg.setDureeAudio(30);
+ }
+ return msg;
}
}
diff --git a/src/test/java/dev/lions/unionflow/server/repository/ConversationRepositoryTest.java b/src/test/java/dev/lions/unionflow/server/repository/ConversationRepositoryTest.java
index ce619cb..8294ed5 100644
--- a/src/test/java/dev/lions/unionflow/server/repository/ConversationRepositoryTest.java
+++ b/src/test/java/dev/lions/unionflow/server/repository/ConversationRepositoryTest.java
@@ -1,149 +1,85 @@
package dev.lions.unionflow.server.repository;
-import dev.lions.unionflow.server.api.enums.communication.ConversationType;
+import dev.lions.unionflow.server.api.enums.messagerie.TypeConversation;
import dev.lions.unionflow.server.entity.Conversation;
-import dev.lions.unionflow.server.entity.Organisation;
import io.quarkus.test.TestTransaction;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
-import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
-/**
- * Tests d'intégration pour {@link ConversationRepository}.
- * Couvre les 3 méthodes : findByParticipant, findByIdAndParticipant, findByOrganisation.
- */
@QuarkusTest
+@DisplayName("ConversationRepository")
class ConversationRepositoryTest {
@Inject
ConversationRepository conversationRepository;
- @Inject
- OrganisationRepository organisationRepository;
-
- private Organisation createOrganisation() {
- Organisation o = new Organisation();
- o.setNom("Org Conversation");
- o.setTypeOrganisation("ASSOCIATION");
- o.setStatut("ACTIVE");
- o.setEmail("conv-" + UUID.randomUUID() + "@test.com");
- o.setActif(true);
- o.setDateCreation(LocalDateTime.now());
- organisationRepository.persist(o);
- return o;
- }
-
- private Conversation createConversation(String name, Organisation org) {
- Conversation c = new Conversation();
- c.setName(name);
- c.setType(ConversationType.GROUP);
- c.setOrganisation(org);
- c.setActif(true);
- c.setDateCreation(LocalDateTime.now());
- conversationRepository.persist(c);
- return c;
- }
-
- // =========================================================================
- // findByParticipant — branches avec includeArchived=true et false
- // =========================================================================
-
@Test
@TestTransaction
- @DisplayName("findByParticipant avec includeArchived=true retourne liste vide si aucune conversation")
- void findByParticipant_noConversations_returnsEmptyList() {
- UUID randomMembre = UUID.randomUUID();
-
- List result = conversationRepository.findByParticipant(randomMembre, true);
-
- assertThat(result).isNotNull();
- assertThat(result).isEmpty();
+ @DisplayName("findById retourne null pour UUID inexistant")
+ void findById_inexistant_returnsNull() {
+ assertThat(conversationRepository.findById(UUID.randomUUID())).isNull();
}
@Test
@TestTransaction
- @DisplayName("findByParticipant avec includeArchived=false retourne liste vide si aucune conversation")
- void findByParticipant_excludeArchived_returnsEmptyList() {
- UUID randomMembre = UUID.randomUUID();
-
- List result = conversationRepository.findByParticipant(randomMembre, false);
-
- assertThat(result).isNotNull();
- assertThat(result).isEmpty();
- }
-
- // =========================================================================
- // findByIdAndParticipant
- // =========================================================================
-
- @Test
- @TestTransaction
- @DisplayName("findByIdAndParticipant retourne empty pour ID et membreId inconnus")
- void findByIdAndParticipant_unknownIds_returnsEmpty() {
- Optional result = conversationRepository.findByIdAndParticipant(
- UUID.randomUUID(), UUID.randomUUID());
-
- assertThat(result).isEmpty();
+ @DisplayName("findConversationById retourne empty pour UUID inexistant")
+ void findConversationById_inexistant_returnsEmpty() {
+ Optional opt = conversationRepository.findConversationById(UUID.randomUUID());
+ assertThat(opt).isEmpty();
}
@Test
@TestTransaction
- @DisplayName("findByIdAndParticipant retourne empty pour conversationId inexistant")
- void findByIdAndParticipant_nonExistentConversation_returnsEmpty() {
- UUID nonExistentId = UUID.randomUUID();
- UUID membreId = UUID.randomUUID();
-
- Optional result = conversationRepository.findByIdAndParticipant(
- nonExistentId, membreId);
-
- assertThat(result).isEmpty();
- }
-
- // =========================================================================
- // findByOrganisation
- // =========================================================================
-
- @Test
- @TestTransaction
- @DisplayName("findByOrganisation retourne liste vide si organisation sans conversation")
- void findByOrganisation_noConversations_returnsEmptyList() {
- Organisation org = createOrganisation();
-
- List result = conversationRepository.findByOrganisation(org.getId());
-
- assertThat(result).isNotNull();
- assertThat(result).isEmpty();
+ @DisplayName("findByMembreId retourne liste non nulle pour membre inexistant")
+ void findByMembreId_inconnu_returnsEmpty() {
+ List list = conversationRepository.findByMembreId(UUID.randomUUID());
+ assertThat(list).isNotNull();
}
@Test
@TestTransaction
- @DisplayName("findByOrganisation retourne les conversations de l'organisation persistées")
- void findByOrganisation_withConversations_returnsList() {
- Organisation org = createOrganisation();
- createConversation("Conv1-" + UUID.randomUUID(), org);
- createConversation("Conv2-" + UUID.randomUUID(), org);
-
- List result = conversationRepository.findByOrganisation(org.getId());
-
- assertThat(result).isNotNull();
- assertThat(result).hasSize(2);
+ @DisplayName("findConversationDirecte retourne empty si aucune conversation")
+ void findConversationDirecte_inconnu_returnsEmpty() {
+ Optional opt = conversationRepository.findConversationDirecte(
+ UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID());
+ assertThat(opt).isEmpty();
}
@Test
@TestTransaction
- @DisplayName("findByOrganisation retourne liste vide pour organisationId inexistant")
- void findByOrganisation_unknownOrganisationId_returnsEmptyList() {
- List result = conversationRepository.findByOrganisation(UUID.randomUUID());
+ @DisplayName("findCanalRole retourne empty si aucun canal")
+ void findCanalRole_inconnu_returnsEmpty() {
+ Optional opt = conversationRepository.findCanalRole(UUID.randomUUID(), "TRESORIER");
+ assertThat(opt).isEmpty();
+ }
- assertThat(result).isNotNull();
- assertThat(result).isEmpty();
+ @Test
+ @TestTransaction
+ @DisplayName("findActivesByMembre retourne liste non nulle")
+ void findActivesByMembre_returnsNonNull() {
+ List list = conversationRepository.findActivesByMembre(UUID.randomUUID());
+ assertThat(list).isNotNull();
+ }
+
+ @Test
+ @TestTransaction
+ @DisplayName("count retourne un nombre >= 0")
+ void count_returnsNonNegative() {
+ assertThat(conversationRepository.count()).isGreaterThanOrEqualTo(0);
+ }
+
+ @Test
+ @TestTransaction
+ @DisplayName("listAll retourne une liste non nulle")
+ void listAll_returnsNonNull() {
+ assertThat(conversationRepository.listAll()).isNotNull();
}
}
diff --git a/src/test/java/dev/lions/unionflow/server/repository/MessageRepositoryTest.java b/src/test/java/dev/lions/unionflow/server/repository/MessageRepositoryTest.java
index 29e8bda..6148889 100644
--- a/src/test/java/dev/lions/unionflow/server/repository/MessageRepositoryTest.java
+++ b/src/test/java/dev/lions/unionflow/server/repository/MessageRepositoryTest.java
@@ -1,256 +1,69 @@
package dev.lions.unionflow.server.repository;
-import dev.lions.unionflow.server.api.enums.communication.ConversationType;
-import dev.lions.unionflow.server.api.enums.communication.MessagePriority;
-import dev.lions.unionflow.server.api.enums.communication.MessageStatus;
-import dev.lions.unionflow.server.api.enums.communication.MessageType;
-import dev.lions.unionflow.server.entity.Conversation;
-import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Message;
import io.quarkus.test.TestTransaction;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
-import jakarta.persistence.EntityManager;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
-import java.time.LocalDate;
import java.util.List;
+import java.util.Optional;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
@QuarkusTest
-@DisplayName("MessageRepository — tests des méthodes de requête")
+@DisplayName("MessageRepository")
class MessageRepositoryTest {
@Inject
MessageRepository messageRepository;
- @Inject
- EntityManager em;
-
- // ─── Helpers ─────────────────────────────────────────────────────────────
-
- private Membre persistMembre() {
- Membre m = new Membre();
- m.setNumeroMembre("MSG-MEM-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase());
- m.setPrenom("Jean");
- m.setNom("Messagerie");
- m.setEmail("msg." + UUID.randomUUID() + "@test.com");
- m.setDateNaissance(LocalDate.of(1990, 1, 1));
- m.setActif(true);
- em.persist(m);
- em.flush();
- return m;
- }
-
- private Conversation persistConversation() {
- Conversation conv = new Conversation();
- conv.setName("Conv Test " + UUID.randomUUID().toString().substring(0, 8));
- conv.setType(ConversationType.GROUP);
- conv.setActif(true);
- em.persist(conv);
- em.flush();
- return conv;
- }
-
- private Message persistMessage(Conversation conv, Membre sender, MessageStatus status) {
- Message msg = new Message();
- msg.setConversation(conv);
- msg.setSender(sender);
- msg.setSenderName(sender.getPrenom() + " " + sender.getNom());
- msg.setContent("Contenu test " + UUID.randomUUID());
- msg.setType(MessageType.INDIVIDUAL);
- msg.setStatus(status);
- msg.setPriority(MessagePriority.NORMAL);
- msg.setIsEdited(false);
- msg.setIsDeleted(false);
- msg.setActif(true);
- em.persist(msg);
- em.flush();
- return msg;
- }
-
- // ─── Tests ───────────────────────────────────────────────────────────────
-
@Test
@TestTransaction
- @DisplayName("findByConversation retourne les messages actifs non supprimés")
- void findByConversation_retourneMessagesActifs() {
- Membre sender = persistMembre();
- Conversation conv = persistConversation();
- persistMessage(conv, sender, MessageStatus.SENT);
- persistMessage(conv, sender, MessageStatus.READ);
-
- List messages = messageRepository.findByConversation(conv.getId(), 10);
-
- assertThat(messages).isNotNull();
- assertThat(messages).hasSizeGreaterThanOrEqualTo(2);
- messages.forEach(m -> assertThat(m.getIsDeleted()).isFalse());
+ @DisplayName("findMessageById retourne empty pour UUID inexistant")
+ void findMessageById_inexistant_returnsEmpty() {
+ Optional opt = messageRepository.findMessageById(UUID.randomUUID());
+ assertThat(opt).isEmpty();
}
@Test
@TestTransaction
- @DisplayName("findByConversation respecte la limite de pagination")
- void findByConversation_respecteLimite() {
- Membre sender = persistMembre();
- Conversation conv = persistConversation();
- for (int i = 0; i < 5; i++) {
- persistMessage(conv, sender, MessageStatus.SENT);
- }
-
- List messages = messageRepository.findByConversation(conv.getId(), 3);
-
- assertThat(messages).hasSizeLessThanOrEqualTo(3);
+ @DisplayName("findByConversationPagine retourne liste vide pour conversation inexistante")
+ void findByConversationPagine_returnsEmpty() {
+ List list = messageRepository.findByConversationPagine(UUID.randomUUID(), 0, 20);
+ assertThat(list).isNotNull().isEmpty();
}
@Test
@TestTransaction
- @DisplayName("findByConversation retourne liste vide pour conversation sans messages")
- void findByConversation_conversationVide_retourneListe() {
- Conversation conv = persistConversation();
-
- List messages = messageRepository.findByConversation(conv.getId(), 10);
-
- assertThat(messages).isNotNull();
- assertThat(messages).isEmpty();
+ @DisplayName("countNonLus retourne >= 0 pour conversation inexistante")
+ void countNonLus_returnsNonNegative() {
+ long count = messageRepository.countNonLus(UUID.randomUUID(), UUID.randomUUID());
+ assertThat(count).isGreaterThanOrEqualTo(0);
}
@Test
@TestTransaction
- @DisplayName("findByConversation exclut les messages supprimés")
- void findByConversation_exclutMessagesSupprimés() {
- Membre sender = persistMembre();
- Conversation conv = persistConversation();
- // Message supprimé
- Message msgSupprime = persistMessage(conv, sender, MessageStatus.SENT);
- msgSupprime.setIsDeleted(true);
- em.flush();
-
- List messages = messageRepository.findByConversation(conv.getId(), 10);
-
- assertThat(messages).noneMatch(m -> m.getId().equals(msgSupprime.getId()));
+ @DisplayName("findActifsByConversation retourne liste vide pour conversation inexistante")
+ void findActifsByConversation_returnsEmpty() {
+ List list = messageRepository.findActifsByConversation(UUID.randomUUID());
+ assertThat(list).isNotNull().isEmpty();
}
@Test
@TestTransaction
- @DisplayName("countUnreadByConversationAndMember compte les messages SENT et DELIVERED d'autres membres")
- void countUnreadByConversationAndMember_compteMsgNonLusAutresMembres() {
- Membre sender = persistMembre();
- Membre reader = persistMembre();
- Conversation conv = persistConversation();
- persistMessage(conv, sender, MessageStatus.SENT);
- persistMessage(conv, sender, MessageStatus.DELIVERED);
- persistMessage(conv, sender, MessageStatus.READ); // déjà lu → exclu
-
- long count = messageRepository.countUnreadByConversationAndMember(conv.getId(), reader.getId());
-
- assertThat(count).isGreaterThanOrEqualTo(2);
+ @DisplayName("findDernierMessage retourne empty pour conversation inexistante")
+ void findDernierMessage_returnsEmpty() {
+ Optional opt = messageRepository.findDernierMessage(UUID.randomUUID());
+ assertThat(opt).isEmpty();
}
@Test
@TestTransaction
- @DisplayName("countUnreadByConversationAndMember exclut les messages du membre lui-même")
- void countUnreadByConversationAndMember_excluMessagesPropresMembre() {
- Membre sender = persistMembre();
- Conversation conv = persistConversation();
- persistMessage(conv, sender, MessageStatus.SENT);
-
- // Le sender lui-même : ses propres messages ne sont pas comptés comme non lus
- long count = messageRepository.countUnreadByConversationAndMember(conv.getId(), sender.getId());
-
- assertThat(count).isEqualTo(0);
- }
-
- @Test
- @TestTransaction
- @DisplayName("countUnreadByConversationAndMember retourne 0 pour conversation vide")
- void countUnreadByConversationAndMember_conversationVide_retourneZero() {
- Conversation conv = persistConversation();
- UUID membreId = UUID.randomUUID();
-
- long count = messageRepository.countUnreadByConversationAndMember(conv.getId(), membreId);
-
- assertThat(count).isEqualTo(0);
- }
-
- @Test
- @TestTransaction
- @DisplayName("markAllAsReadByConversationAndMember marque les messages SENT et DELIVERED en READ")
- void markAllAsReadByConversationAndMember_marqueLesMessagesEnRead() {
- Membre sender = persistMembre();
- Membre reader = persistMembre();
- Conversation conv = persistConversation();
- persistMessage(conv, sender, MessageStatus.SENT);
- persistMessage(conv, sender, MessageStatus.DELIVERED);
-
- int updated = messageRepository.markAllAsReadByConversationAndMember(conv.getId(), reader.getId());
-
- assertThat(updated).isGreaterThanOrEqualTo(2);
- }
-
- @Test
- @TestTransaction
- @DisplayName("markAllAsReadByConversationAndMember ne touche pas les messages déjà READ")
- void markAllAsReadByConversationAndMember_neTouchePasMsgDejaRead() {
- Membre sender = persistMembre();
- Membre reader = persistMembre();
- Conversation conv = persistConversation();
- persistMessage(conv, sender, MessageStatus.READ); // déjà lu
-
- int updated = messageRepository.markAllAsReadByConversationAndMember(conv.getId(), reader.getId());
-
- assertThat(updated).isEqualTo(0);
- }
-
- @Test
- @TestTransaction
- @DisplayName("findLastByConversation retourne le dernier message")
- void findLastByConversation_retourneDernierMessage() {
- Membre sender = persistMembre();
- Conversation conv = persistConversation();
- // Set explicit dateCreation to guarantee ordering (PrePersist skips if non-null)
- Message premierMsg = new Message();
- premierMsg.setConversation(conv);
- premierMsg.setSender(sender);
- premierMsg.setSenderName(sender.getPrenom() + " " + sender.getNom());
- premierMsg.setContent("Premier message");
- premierMsg.setType(dev.lions.unionflow.server.api.enums.communication.MessageType.INDIVIDUAL);
- premierMsg.setStatus(MessageStatus.SENT);
- premierMsg.setPriority(dev.lions.unionflow.server.api.enums.communication.MessagePriority.NORMAL);
- premierMsg.setIsEdited(false);
- premierMsg.setIsDeleted(false);
- premierMsg.setActif(true);
- premierMsg.setDateCreation(java.time.LocalDateTime.now().minusSeconds(10));
- em.persist(premierMsg);
- em.flush();
-
- Message dernierMsg = persistMessage(conv, sender, MessageStatus.DELIVERED);
-
- Message last = messageRepository.findLastByConversation(conv.getId());
-
- assertThat(last).isNotNull();
- assertThat(last.getId()).isEqualTo(dernierMsg.getId());
- }
-
- @Test
- @TestTransaction
- @DisplayName("findLastByConversation retourne null pour conversation vide")
- void findLastByConversation_conversationVide_retourneNull() {
- Conversation conv = persistConversation();
-
- Message last = messageRepository.findLastByConversation(conv.getId());
-
- assertThat(last).isNull();
- }
-
- @Test
- @TestTransaction
- @DisplayName("findLastByConversation retourne null pour conversation inexistante")
- void findLastByConversation_conversationInexistante_retourneNull() {
- Message last = messageRepository.findLastByConversation(UUID.randomUUID());
-
- assertThat(last).isNull();
+ @DisplayName("count retourne un nombre >= 0")
+ void count_returnsNonNegative() {
+ assertThat(messageRepository.count()).isGreaterThanOrEqualTo(0);
}
}
diff --git a/src/test/java/dev/lions/unionflow/server/resource/ConversationResourceTest.java b/src/test/java/dev/lions/unionflow/server/resource/ConversationResourceTest.java
deleted file mode 100644
index bc92dff..0000000
--- a/src/test/java/dev/lions/unionflow/server/resource/ConversationResourceTest.java
+++ /dev/null
@@ -1,377 +0,0 @@
-package dev.lions.unionflow.server.resource;
-
-import static io.restassured.RestAssured.given;
-import static org.hamcrest.Matchers.notNullValue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.when;
-
-import dev.lions.unionflow.server.api.dto.communication.request.CreateConversationRequest;
-import dev.lions.unionflow.server.api.dto.communication.response.ConversationResponse;
-import dev.lions.unionflow.server.api.enums.communication.ConversationType;
-import dev.lions.unionflow.server.service.ConversationService;
-import dev.lions.unionflow.server.service.support.SecuriteHelper;
-import io.quarkus.test.InjectMock;
-import io.quarkus.test.junit.QuarkusTest;
-import io.quarkus.test.security.TestSecurity;
-import io.restassured.http.ContentType;
-import jakarta.ws.rs.NotFoundException;
-
-import java.time.LocalDateTime;
-import java.util.Collections;
-import java.util.List;
-import java.util.UUID;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-
-/**
- * Tests d'intégration REST pour ConversationResource.
- *
- * @author UnionFlow Team
- * @version 1.0
- * @since 2026-03-21
- */
-@QuarkusTest
-class ConversationResourceTest {
-
- private static final String BASE_PATH = "/api/conversations";
- private static final String MEMBRE_ID = "00000000-0000-0000-0000-000000000010";
- private static final String CONVERSATION_ID = "00000000-0000-0000-0000-000000000011";
- private static final String ORG_ID = "00000000-0000-0000-0000-000000000012";
-
- @InjectMock
- ConversationService conversationService;
-
- @InjectMock
- SecuriteHelper securiteHelper;
-
- @BeforeEach
- void setup() {
- when(securiteHelper.resolveMembreId()).thenReturn(UUID.fromString(MEMBRE_ID));
- }
-
- private ConversationResponse buildConversationResponse() {
- return ConversationResponse.builder()
- .id(UUID.fromString(CONVERSATION_ID))
- .name("Discussion générale")
- .description("Conversation test")
- .type(ConversationType.GROUP)
- .participantIds(List.of(UUID.fromString(MEMBRE_ID)))
- .unreadCount(0)
- .muted(false)
- .pinned(false)
- .archived(false)
- .createdAt(LocalDateTime.now())
- .updatedAt(LocalDateTime.now())
- .build();
- }
-
- // -------------------------------------------------------------------------
- // GET /api/conversations
- // -------------------------------------------------------------------------
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("getConversations retourne 200 avec liste vide")
- void getConversations_returnsEmptyList_200() {
- when(conversationService.getConversations(any(), any(), anyBoolean()))
- .thenReturn(Collections.emptyList());
-
- given()
- .when()
- .get(BASE_PATH)
- .then()
- .statusCode(200)
- .contentType(ContentType.JSON)
- .body("$", notNullValue());
- }
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("getConversations avec includeArchived=true retourne 200")
- void getConversations_withIncludeArchived_returns200() {
- when(conversationService.getConversations(any(), any(), eq(true)))
- .thenReturn(List.of(buildConversationResponse()));
-
- given()
- .queryParam("includeArchived", true)
- .when()
- .get(BASE_PATH)
- .then()
- .statusCode(200)
- .contentType(ContentType.JSON);
- }
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("getConversations avec organisationId filtre par organisation")
- void getConversations_withOrganisationId_returns200() {
- when(conversationService.getConversations(any(), eq(UUID.fromString(ORG_ID)), anyBoolean()))
- .thenReturn(List.of(buildConversationResponse()));
-
- given()
- .queryParam("organisationId", ORG_ID)
- .when()
- .get(BASE_PATH)
- .then()
- .statusCode(200)
- .contentType(ContentType.JSON);
- }
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("getConversations retourne 200 avec liste non vide")
- void getConversations_withResults_returns200() {
- when(conversationService.getConversations(any(), any(), anyBoolean()))
- .thenReturn(List.of(buildConversationResponse()));
-
- given()
- .when()
- .get(BASE_PATH)
- .then()
- .statusCode(200)
- .contentType(ContentType.JSON);
- }
-
- // -------------------------------------------------------------------------
- // GET /api/conversations/{id}
- // -------------------------------------------------------------------------
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("getConversationById retourne 200 quand conversation trouvée")
- void getConversationById_found_returns200() {
- when(conversationService.getConversationById(eq(UUID.fromString(CONVERSATION_ID)), any()))
- .thenReturn(buildConversationResponse());
-
- given()
- .when()
- .get(BASE_PATH + "/{id}", CONVERSATION_ID)
- .then()
- .statusCode(200)
- .contentType(ContentType.JSON)
- .body("id", notNullValue());
- }
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("getConversationById retourne 404 quand conversation non trouvée")
- void getConversationById_notFound_returns404() {
- when(conversationService.getConversationById(any(), any()))
- .thenThrow(new NotFoundException("Conversation non trouvée ou accès refusé"));
-
- given()
- .when()
- .get(BASE_PATH + "/{id}", UUID.randomUUID().toString())
- .then()
- .statusCode(404);
- }
-
- // -------------------------------------------------------------------------
- // POST /api/conversations
- // -------------------------------------------------------------------------
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("createConversation retourne 201 avec body valide")
- void createConversation_validRequest_returns201() {
- when(conversationService.createConversation(any(CreateConversationRequest.class), any()))
- .thenReturn(buildConversationResponse());
-
- String body = """
- {
- "name": "Nouveau groupe",
- "description": "Description du groupe",
- "type": "GROUP",
- "participantIds": ["%s"]
- }
- """.formatted(MEMBRE_ID);
-
- given()
- .contentType(ContentType.JSON)
- .body(body)
- .when()
- .post(BASE_PATH)
- .then()
- .statusCode(201)
- .contentType(ContentType.JSON);
- }
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("createConversation avec body invalide (sans name) retourne 400")
- void createConversation_invalidRequest_returns400() {
- String body = """
- {
- "description": "Sans nom",
- "type": "GROUP",
- "participantIds": ["%s"]
- }
- """.formatted(MEMBRE_ID);
-
- given()
- .contentType(ContentType.JSON)
- .body(body)
- .when()
- .post(BASE_PATH)
- .then()
- .statusCode(400);
- }
-
- // -------------------------------------------------------------------------
- // PUT /api/conversations/{id}/archive
- // -------------------------------------------------------------------------
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("archiveConversation retourne 204 quand succès")
- void archiveConversation_success_returns204() {
- doNothing().when(conversationService).archiveConversation(any(), any(), anyBoolean());
-
- given()
- .when()
- .put(BASE_PATH + "/{id}/archive", CONVERSATION_ID)
- .then()
- .statusCode(204);
- }
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("archiveConversation avec archive=false retourne 204")
- void archiveConversation_unarchive_returns204() {
- doNothing().when(conversationService).archiveConversation(any(), any(), eq(false));
-
- given()
- .queryParam("archive", false)
- .when()
- .put(BASE_PATH + "/{id}/archive", CONVERSATION_ID)
- .then()
- .statusCode(204);
- }
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("archiveConversation non trouvée retourne 404")
- void archiveConversation_notFound_returns404() {
- doThrow(new NotFoundException("Conversation non trouvée"))
- .when(conversationService).archiveConversation(any(), any(), anyBoolean());
-
- given()
- .when()
- .put(BASE_PATH + "/{id}/archive", UUID.randomUUID().toString())
- .then()
- .statusCode(404);
- }
-
- // -------------------------------------------------------------------------
- // PUT /api/conversations/{id}/mark-read
- // -------------------------------------------------------------------------
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("markAsRead retourne 204 quand succès")
- void markAsRead_success_returns204() {
- doNothing().when(conversationService).markAsRead(any(), any());
-
- given()
- .when()
- .put(BASE_PATH + "/{id}/mark-read", CONVERSATION_ID)
- .then()
- .statusCode(204);
- }
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("markAsRead conversation non trouvée retourne 404")
- void markAsRead_notFound_returns404() {
- doThrow(new NotFoundException("Conversation non trouvée"))
- .when(conversationService).markAsRead(any(), any());
-
- given()
- .when()
- .put(BASE_PATH + "/{id}/mark-read", UUID.randomUUID().toString())
- .then()
- .statusCode(404);
- }
-
- // -------------------------------------------------------------------------
- // PUT /api/conversations/{id}/toggle-mute
- // -------------------------------------------------------------------------
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("toggleMute retourne 204 quand succès")
- void toggleMute_success_returns204() {
- doNothing().when(conversationService).toggleMute(any(), any());
-
- given()
- .when()
- .put(BASE_PATH + "/{id}/toggle-mute", CONVERSATION_ID)
- .then()
- .statusCode(204);
- }
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("toggleMute conversation non trouvée retourne 404")
- void toggleMute_notFound_returns404() {
- doThrow(new NotFoundException("Conversation non trouvée"))
- .when(conversationService).toggleMute(any(), any());
-
- given()
- .when()
- .put(BASE_PATH + "/{id}/toggle-mute", UUID.randomUUID().toString())
- .then()
- .statusCode(404);
- }
-
- // -------------------------------------------------------------------------
- // PUT /api/conversations/{id}/toggle-pin
- // -------------------------------------------------------------------------
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("togglePin retourne 204 quand succès")
- void togglePin_success_returns204() {
- doNothing().when(conversationService).togglePin(any(), any());
-
- given()
- .when()
- .put(BASE_PATH + "/{id}/toggle-pin", CONVERSATION_ID)
- .then()
- .statusCode(204);
- }
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("togglePin conversation non trouvée retourne 404")
- void togglePin_notFound_returns404() {
- doThrow(new NotFoundException("Conversation non trouvée"))
- .when(conversationService).togglePin(any(), any());
-
- given()
- .when()
- .put(BASE_PATH + "/{id}/toggle-pin", UUID.randomUUID().toString())
- .then()
- .statusCode(404);
- }
-
- // -------------------------------------------------------------------------
- // Sécurité — non authentifié
- // -------------------------------------------------------------------------
-
- @Test
- @DisplayName("getConversations sans authentification retourne 401")
- void getConversations_unauthenticated_returns401() {
- given()
- .when()
- .get(BASE_PATH)
- .then()
- .statusCode(401);
- }
-}
diff --git a/src/test/java/dev/lions/unionflow/server/resource/MessageResourceTest.java b/src/test/java/dev/lions/unionflow/server/resource/MessageResourceTest.java
deleted file mode 100644
index d5a2793..0000000
--- a/src/test/java/dev/lions/unionflow/server/resource/MessageResourceTest.java
+++ /dev/null
@@ -1,374 +0,0 @@
-package dev.lions.unionflow.server.resource;
-
-import static io.restassured.RestAssured.given;
-import static org.hamcrest.Matchers.notNullValue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.when;
-
-import dev.lions.unionflow.server.api.dto.communication.request.SendMessageRequest;
-import dev.lions.unionflow.server.api.dto.communication.response.MessageResponse;
-import dev.lions.unionflow.server.api.enums.communication.MessagePriority;
-import dev.lions.unionflow.server.api.enums.communication.MessageStatus;
-import dev.lions.unionflow.server.api.enums.communication.MessageType;
-import dev.lions.unionflow.server.service.MessageService;
-import dev.lions.unionflow.server.service.support.SecuriteHelper;
-import io.quarkus.test.InjectMock;
-import io.quarkus.test.junit.QuarkusTest;
-import io.quarkus.test.security.TestSecurity;
-import io.restassured.http.ContentType;
-import jakarta.ws.rs.NotFoundException;
-
-import java.time.LocalDateTime;
-import java.util.Collections;
-import java.util.List;
-import java.util.UUID;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-
-/**
- * Tests d'intégration REST pour MessageResource.
- *
- * @author UnionFlow Team
- * @version 1.0
- * @since 2026-03-21
- */
-@QuarkusTest
-class MessageResourceTest {
-
- private static final String BASE_PATH = "/api/messages";
- private static final String MEMBRE_ID = "00000000-0000-0000-0000-000000000020";
- private static final String MESSAGE_ID = "00000000-0000-0000-0000-000000000021";
- private static final String CONVERSATION_ID = "00000000-0000-0000-0000-000000000022";
-
- @InjectMock
- MessageService messageService;
-
- @InjectMock
- SecuriteHelper securiteHelper;
-
- @BeforeEach
- void setup() {
- when(securiteHelper.resolveMembreId()).thenReturn(UUID.fromString(MEMBRE_ID));
- }
-
- private MessageResponse buildMessageResponse() {
- return MessageResponse.builder()
- .id(UUID.fromString(MESSAGE_ID))
- .conversationId(UUID.fromString(CONVERSATION_ID))
- .senderId(UUID.fromString(MEMBRE_ID))
- .senderName("Alice Martin")
- .content("Bonjour !")
- .type(MessageType.INDIVIDUAL)
- .status(MessageStatus.SENT)
- .priority(MessagePriority.NORMAL)
- .edited(false)
- .deleted(false)
- .createdAt(LocalDateTime.now())
- .build();
- }
-
- // -------------------------------------------------------------------------
- // GET /api/messages
- // -------------------------------------------------------------------------
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("getMessages sans conversationId retourne 400")
- void getMessages_missingConversationId_returns400() {
- given()
- .when()
- .get(BASE_PATH)
- .then()
- .statusCode(400);
- }
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("getMessages avec conversationId valide retourne 200")
- void getMessages_withConversationId_returns200() {
- when(messageService.getMessages(eq(UUID.fromString(CONVERSATION_ID)), any(), anyInt()))
- .thenReturn(List.of(buildMessageResponse()));
-
- given()
- .queryParam("conversationId", CONVERSATION_ID)
- .when()
- .get(BASE_PATH)
- .then()
- .statusCode(200)
- .contentType(ContentType.JSON)
- .body("$", notNullValue());
- }
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("getMessages avec conversationId valide retourne liste vide")
- void getMessages_emptyConversation_returns200() {
- when(messageService.getMessages(any(), any(), anyInt()))
- .thenReturn(Collections.emptyList());
-
- given()
- .queryParam("conversationId", CONVERSATION_ID)
- .when()
- .get(BASE_PATH)
- .then()
- .statusCode(200)
- .contentType(ContentType.JSON);
- }
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("getMessages avec limit personnalisé retourne 200")
- void getMessages_withLimit_returns200() {
- when(messageService.getMessages(any(), any(), eq(10)))
- .thenReturn(List.of(buildMessageResponse()));
-
- given()
- .queryParam("conversationId", CONVERSATION_ID)
- .queryParam("limit", 10)
- .when()
- .get(BASE_PATH)
- .then()
- .statusCode(200)
- .contentType(ContentType.JSON);
- }
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("getMessages conversation non trouvée retourne 404")
- void getMessages_conversationNotFound_returns404() {
- when(messageService.getMessages(any(), any(), anyInt()))
- .thenThrow(new NotFoundException("Conversation non trouvée ou accès refusé"));
-
- given()
- .queryParam("conversationId", CONVERSATION_ID)
- .when()
- .get(BASE_PATH)
- .then()
- .statusCode(404);
- }
-
- // -------------------------------------------------------------------------
- // POST /api/messages
- // -------------------------------------------------------------------------
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("sendMessage avec body valide retourne 201")
- void sendMessage_validRequest_returns201() {
- when(messageService.sendMessage(any(SendMessageRequest.class), any()))
- .thenReturn(buildMessageResponse());
-
- String body = """
- {
- "conversationId": "%s",
- "content": "Bonjour à tous !"
- }
- """.formatted(CONVERSATION_ID);
-
- given()
- .contentType(ContentType.JSON)
- .body(body)
- .when()
- .post(BASE_PATH)
- .then()
- .statusCode(201)
- .contentType(ContentType.JSON)
- .body("id", notNullValue());
- }
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("sendMessage sans conversationId retourne 400")
- void sendMessage_missingConversationId_returns400() {
- String body = """
- {
- "content": "Message sans conversation"
- }
- """;
-
- given()
- .contentType(ContentType.JSON)
- .body(body)
- .when()
- .post(BASE_PATH)
- .then()
- .statusCode(400);
- }
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("sendMessage sans contenu retourne 400")
- void sendMessage_missingContent_returns400() {
- String body = """
- {
- "conversationId": "%s"
- }
- """.formatted(CONVERSATION_ID);
-
- given()
- .contentType(ContentType.JSON)
- .body(body)
- .when()
- .post(BASE_PATH)
- .then()
- .statusCode(400);
- }
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("sendMessage conversation non trouvée retourne 404")
- void sendMessage_conversationNotFound_returns404() {
- when(messageService.sendMessage(any(), any()))
- .thenThrow(new NotFoundException("Conversation non trouvée ou accès refusé"));
-
- String body = """
- {
- "conversationId": "%s",
- "content": "Test"
- }
- """.formatted(CONVERSATION_ID);
-
- given()
- .contentType(ContentType.JSON)
- .body(body)
- .when()
- .post(BASE_PATH)
- .then()
- .statusCode(404);
- }
-
- // -------------------------------------------------------------------------
- // PUT /api/messages/{id}
- // -------------------------------------------------------------------------
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("editMessage avec body valide retourne 200")
- void editMessage_validRequest_returns200() {
- when(messageService.editMessage(any(), any(), anyString()))
- .thenReturn(buildMessageResponse());
-
- given()
- .contentType(ContentType.JSON)
- .body("{\"content\": \"Message modifié\"}")
- .when()
- .put(BASE_PATH + "/{id}", MESSAGE_ID)
- .then()
- .statusCode(200)
- .contentType(ContentType.JSON);
- }
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("editMessage sans contenu retourne 400")
- void editMessage_missingContent_returns400() {
- given()
- .contentType(ContentType.JSON)
- .body("{}")
- .when()
- .put(BASE_PATH + "/{id}", MESSAGE_ID)
- .then()
- .statusCode(400);
- }
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("editMessage avec contenu vide retourne 400")
- void editMessage_emptyContent_returns400() {
- given()
- .contentType(ContentType.JSON)
- .body("{\"content\": \"\"}")
- .when()
- .put(BASE_PATH + "/{id}", MESSAGE_ID)
- .then()
- .statusCode(400);
- }
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("editMessage message non trouvé retourne 404")
- void editMessage_notFound_returns404() {
- when(messageService.editMessage(any(), any(), anyString()))
- .thenThrow(new NotFoundException("Message non trouvé"));
-
- given()
- .contentType(ContentType.JSON)
- .body("{\"content\": \"Contenu modifié\"}")
- .when()
- .put(BASE_PATH + "/{id}", UUID.randomUUID().toString())
- .then()
- .statusCode(404);
- }
-
- // -------------------------------------------------------------------------
- // DELETE /api/messages/{id}
- // -------------------------------------------------------------------------
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("deleteMessage retourne 204 quand succès")
- void deleteMessage_success_returns204() {
- doNothing().when(messageService).deleteMessage(any(), any());
-
- given()
- .when()
- .delete(BASE_PATH + "/{id}", MESSAGE_ID)
- .then()
- .statusCode(204);
- }
-
- @Test
- @TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
- @DisplayName("deleteMessage message non trouvé retourne 404")
- void deleteMessage_notFound_returns404() {
- doThrow(new NotFoundException("Message non trouvé"))
- .when(messageService).deleteMessage(any(), any());
-
- given()
- .when()
- .delete(BASE_PATH + "/{id}", UUID.randomUUID().toString())
- .then()
- .statusCode(404);
- }
-
- // -------------------------------------------------------------------------
- // Sécurité — non authentifié
- // -------------------------------------------------------------------------
-
- @Test
- @DisplayName("getMessages sans authentification retourne 401")
- void getMessages_unauthenticated_returns401() {
- given()
- .queryParam("conversationId", CONVERSATION_ID)
- .when()
- .get(BASE_PATH)
- .then()
- .statusCode(401);
- }
-
- @Test
- @DisplayName("sendMessage sans authentification retourne 401")
- void sendMessage_unauthenticated_returns401() {
- String body = """
- {
- "conversationId": "%s",
- "content": "Message non auth"
- }
- """.formatted(CONVERSATION_ID);
-
- given()
- .contentType(ContentType.JSON)
- .body(body)
- .when()
- .post(BASE_PATH)
- .then()
- .statusCode(401);
- }
-}
diff --git a/src/test/java/dev/lions/unionflow/server/service/ConversationServiceTest.java b/src/test/java/dev/lions/unionflow/server/service/ConversationServiceTest.java
deleted file mode 100644
index fe1d93d..0000000
--- a/src/test/java/dev/lions/unionflow/server/service/ConversationServiceTest.java
+++ /dev/null
@@ -1,562 +0,0 @@
-package dev.lions.unionflow.server.service;
-
-import dev.lions.unionflow.server.api.dto.communication.request.CreateConversationRequest;
-import dev.lions.unionflow.server.api.dto.communication.response.ConversationResponse;
-import dev.lions.unionflow.server.api.enums.communication.ConversationType;
-import dev.lions.unionflow.server.api.enums.communication.MessageStatus;
-import dev.lions.unionflow.server.api.enums.communication.MessageType;
-import dev.lions.unionflow.server.entity.Conversation;
-import dev.lions.unionflow.server.entity.Membre;
-import dev.lions.unionflow.server.entity.Message;
-import dev.lions.unionflow.server.entity.Organisation;
-import dev.lions.unionflow.server.repository.ConversationRepository;
-import dev.lions.unionflow.server.repository.MembreRepository;
-import dev.lions.unionflow.server.repository.MessageRepository;
-import dev.lions.unionflow.server.repository.OrganisationRepository;
-import io.quarkus.test.InjectMock;
-import io.quarkus.test.junit.QuarkusTest;
-import io.quarkus.test.junit.mockito.InjectSpy;
-import jakarta.inject.Inject;
-import jakarta.persistence.EntityManager;
-import jakarta.ws.rs.NotFoundException;
-import org.junit.jupiter.api.*;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-import java.util.UUID;
-
-import static org.assertj.core.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
-
-/**
- * Tests unitaires pour ConversationService
- *
- * @author UnionFlow Team
- * @version 1.0
- * @since 2026-03-20
- */
-@QuarkusTest
-@TestMethodOrder(MethodOrderer.DisplayName.class)
-class ConversationServiceTest {
-
- @Inject
- ConversationService conversationService;
-
- @InjectMock
- ConversationRepository conversationRepository;
-
- @InjectMock
- MessageRepository messageRepository;
-
- @InjectSpy
- MembreRepository membreRepository;
-
- @InjectSpy
- OrganisationRepository organisationRepository;
-
- @InjectMock
- EntityManager entityManager;
-
- // -------------------------------------------------------------------------
- // Helpers
- // -------------------------------------------------------------------------
-
- private Conversation mockConversation() {
- Conversation c = new Conversation();
- c.setId(UUID.randomUUID());
- c.setName("Test Conv");
- c.setIsMuted(false);
- c.setIsPinned(false);
- c.setIsArchived(false);
- c.setParticipants(new ArrayList<>());
- return c;
- }
-
- private Message mockMessage(Conversation conv) {
- Message msg = new Message();
- msg.setId(UUID.randomUUID());
- msg.setConversation(conv);
- Membre sender = new Membre();
- sender.setId(UUID.randomUUID());
- msg.setSender(sender);
- msg.setSenderName("Test Sender");
- msg.setContent("Hello");
- msg.setType(MessageType.INDIVIDUAL);
- msg.setStatus(MessageStatus.SENT);
- msg.setIsEdited(false);
- msg.setIsDeleted(false);
- return msg;
- }
-
- // -------------------------------------------------------------------------
- // getConversations
- // -------------------------------------------------------------------------
-
- @Test
- @DisplayName("getConversations_withOrgId_callsByOrganisation")
- void getConversations_withOrgId_callsByOrganisation() {
- UUID membreId = UUID.randomUUID();
- UUID orgId = UUID.randomUUID();
- Conversation conv = mockConversation();
-
- when(conversationRepository.findByOrganisation(orgId)).thenReturn(List.of(conv));
- when(messageRepository.findLastByConversation(conv.getId())).thenReturn(null);
- when(messageRepository.countUnreadByConversationAndMember(conv.getId(), membreId)).thenReturn(0L);
-
- List result = conversationService.getConversations(membreId, orgId, false);
-
- assertThat(result).hasSize(1);
- verify(conversationRepository).findByOrganisation(orgId);
- verify(conversationRepository, never()).findByParticipant(any(), anyBoolean());
- }
-
- @Test
- @DisplayName("getConversations_withoutOrgId_callsByParticipant")
- void getConversations_withoutOrgId_callsByParticipant() {
- UUID membreId = UUID.randomUUID();
- Conversation conv = mockConversation();
-
- when(conversationRepository.findByParticipant(membreId, false)).thenReturn(List.of(conv));
- when(messageRepository.findLastByConversation(conv.getId())).thenReturn(null);
- when(messageRepository.countUnreadByConversationAndMember(conv.getId(), membreId)).thenReturn(0L);
-
- List result = conversationService.getConversations(membreId, null, false);
-
- assertThat(result).hasSize(1);
- verify(conversationRepository).findByParticipant(membreId, false);
- verify(conversationRepository, never()).findByOrganisation(any());
- }
-
- @Test
- @DisplayName("getConversations_includesLastMessageAndUnread")
- void getConversations_includesLastMessageAndUnread() {
- UUID membreId = UUID.randomUUID();
- Conversation conv = mockConversation();
- Message lastMsg = mockMessage(conv);
-
- when(conversationRepository.findByParticipant(membreId, true)).thenReturn(List.of(conv));
- when(messageRepository.findLastByConversation(conv.getId())).thenReturn(lastMsg);
- when(messageRepository.countUnreadByConversationAndMember(conv.getId(), membreId)).thenReturn(3L);
-
- List result = conversationService.getConversations(membreId, null, true);
-
- assertThat(result).hasSize(1);
- ConversationResponse response = result.get(0);
- assertThat(response.getLastMessage()).isNotNull();
- assertThat(response.getLastMessage().getContent()).isEqualTo("Hello");
- assertThat(response.getUnreadCount()).isEqualTo(3);
- }
-
- // -------------------------------------------------------------------------
- // getConversationById
- // -------------------------------------------------------------------------
-
- @Test
- @DisplayName("getConversationById_notFound_throwsNotFound")
- void getConversationById_notFound_throwsNotFound() {
- UUID convId = UUID.randomUUID();
- UUID membreId = UUID.randomUUID();
-
- when(conversationRepository.findByIdAndParticipant(convId, membreId)).thenReturn(Optional.empty());
-
- assertThatThrownBy(() -> conversationService.getConversationById(convId, membreId))
- .isInstanceOf(NotFoundException.class)
- .hasMessageContaining("Conversation non trouvée");
- }
-
- @Test
- @DisplayName("getConversationById_found_returnsResponse")
- void getConversationById_found_returnsResponse() {
- UUID membreId = UUID.randomUUID();
- Conversation conv = mockConversation();
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
- when(messageRepository.findLastByConversation(conv.getId())).thenReturn(null);
- when(messageRepository.countUnreadByConversationAndMember(conv.getId(), membreId)).thenReturn(0L);
-
- ConversationResponse response = conversationService.getConversationById(conv.getId(), membreId);
-
- assertThat(response).isNotNull();
- assertThat(response.getId()).isEqualTo(conv.getId());
- assertThat(response.getName()).isEqualTo("Test Conv");
- }
-
- // -------------------------------------------------------------------------
- // createConversation
- // -------------------------------------------------------------------------
-
- @Test
- @DisplayName("createConversation_withoutOrg_success")
- void createConversation_withoutOrg_success() {
- UUID creatorId = UUID.randomUUID();
- Membre creator = new Membre();
- creator.setId(creatorId);
-
- CreateConversationRequest request = CreateConversationRequest.builder()
- .name("New Conv")
- .description("desc")
- .type(ConversationType.GROUP)
- .participantIds(new ArrayList<>())
- .organisationId(null)
- .build();
-
- when(entityManager.find(Membre.class, creatorId)).thenReturn(creator);
- when(messageRepository.findLastByConversation(any())).thenReturn(null);
- when(messageRepository.countUnreadByConversationAndMember(any(), eq(creatorId))).thenReturn(0L);
-
- ConversationResponse response = conversationService.createConversation(request, creatorId);
-
- assertThat(response).isNotNull();
- assertThat(response.getName()).isEqualTo("New Conv");
- verify(conversationRepository).persist(any(Conversation.class));
- verify(entityManager, never()).find(eq(Organisation.class), any());
- }
-
- @Test
- @DisplayName("createConversation_withOrg_setsOrganisation")
- void createConversation_withOrg_setsOrganisation() {
- UUID creatorId = UUID.randomUUID();
- UUID orgId = UUID.randomUUID();
- Membre creator = new Membre();
- creator.setId(creatorId);
- Organisation org = new Organisation();
- org.setId(orgId);
-
- CreateConversationRequest request = CreateConversationRequest.builder()
- .name("Org Conv")
- .description(null)
- .type(ConversationType.BROADCAST)
- .participantIds(new ArrayList<>())
- .organisationId(orgId)
- .build();
-
- when(entityManager.find(Membre.class, creatorId)).thenReturn(creator);
- when(entityManager.find(Organisation.class, orgId)).thenReturn(org);
- when(messageRepository.findLastByConversation(any())).thenReturn(null);
- when(messageRepository.countUnreadByConversationAndMember(any(), eq(creatorId))).thenReturn(0L);
-
- ConversationResponse response = conversationService.createConversation(request, creatorId);
-
- assertThat(response).isNotNull();
- assertThat(response.getOrganisationId()).isEqualTo(orgId);
- verify(entityManager).find(Organisation.class, orgId);
- }
-
- @Test
- @DisplayName("createConversation_creatorNotInList_addsCreator")
- void createConversation_creatorNotInList_addsCreator() {
- UUID creatorId = UUID.randomUUID();
- UUID participant1Id = UUID.randomUUID();
- Membre creator = new Membre();
- creator.setId(creatorId);
- Membre participant1 = new Membre();
- participant1.setId(participant1Id);
-
- CreateConversationRequest request = CreateConversationRequest.builder()
- .name("Conv")
- .description(null)
- .type(ConversationType.INDIVIDUAL)
- .participantIds(new ArrayList<>(List.of(participant1Id)))
- .organisationId(null)
- .build();
-
- when(entityManager.find(Membre.class, participant1Id)).thenReturn(participant1);
- when(entityManager.find(Membre.class, creatorId)).thenReturn(creator);
- when(messageRepository.findLastByConversation(any())).thenReturn(null);
- when(messageRepository.countUnreadByConversationAndMember(any(), eq(creatorId))).thenReturn(0L);
-
- ConversationResponse response = conversationService.createConversation(request, creatorId);
-
- // Creator + participant1 = 2 participants
- assertThat(response.getParticipantIds()).hasSize(2);
- assertThat(response.getParticipantIds()).contains(creatorId, participant1Id);
- }
-
- @Test
- @DisplayName("createConversation_creatorAlreadyInList_doesNotDuplicate")
- void createConversation_creatorAlreadyInList_doesNotDuplicate() {
- UUID creatorId = UUID.randomUUID();
- Membre creator = new Membre();
- creator.setId(creatorId);
-
- CreateConversationRequest request = CreateConversationRequest.builder()
- .name("Conv")
- .description(null)
- .type(ConversationType.INDIVIDUAL)
- .participantIds(new ArrayList<>(List.of(creatorId)))
- .organisationId(null)
- .build();
-
- // findById(creatorId) appelé 2 fois: une pour le participant, une pour le créateur
- when(entityManager.find(Membre.class, creatorId)).thenReturn(creator);
- when(messageRepository.findLastByConversation(any())).thenReturn(null);
- when(messageRepository.countUnreadByConversationAndMember(any(), eq(creatorId))).thenReturn(0L);
-
- ConversationResponse response = conversationService.createConversation(request, creatorId);
-
- // Le créateur ne doit pas être dupliqué
- assertThat(response.getParticipantIds()).hasSize(1);
- assertThat(response.getParticipantIds()).containsExactly(creatorId);
- }
-
- @Test
- @DisplayName("createConversation_participantNotFound_filtersNull")
- void createConversation_participantNotFound_filtersNull() {
- UUID creatorId = UUID.randomUUID();
- UUID unknownId = UUID.randomUUID();
- Membre creator = new Membre();
- creator.setId(creatorId);
-
- CreateConversationRequest request = CreateConversationRequest.builder()
- .name("Conv")
- .description(null)
- .type(ConversationType.GROUP)
- .participantIds(new ArrayList<>(List.of(unknownId)))
- .organisationId(null)
- .build();
-
- when(entityManager.find(Membre.class, creatorId)).thenReturn(creator);
- when(messageRepository.findLastByConversation(any())).thenReturn(null);
- when(messageRepository.countUnreadByConversationAndMember(any(), eq(creatorId))).thenReturn(0L);
-
- ConversationResponse response = conversationService.createConversation(request, creatorId);
-
- // unknownId est filtré, seul le créateur reste
- assertThat(response.getParticipantIds()).hasSize(1);
- assertThat(response.getParticipantIds()).containsExactly(creatorId);
- }
-
- @Test
- @DisplayName("createConversation_creatorNotFound_doesNotAddCreator (creator == null → L97 false)")
- void createConversation_creatorNull_doesNotAddCreator() {
- UUID creatorId = UUID.randomUUID();
-
- CreateConversationRequest request = CreateConversationRequest.builder()
- .name("No Creator Conv")
- .description(null)
- .type(ConversationType.GROUP)
- .participantIds(new ArrayList<>())
- .organisationId(null)
- .build();
-
- // creator == null → condition L97: creator != null = false → pas d'ajout du créateur
- when(entityManager.find(Membre.class, creatorId)).thenReturn(null);
- when(messageRepository.findLastByConversation(any())).thenReturn(null);
- when(messageRepository.countUnreadByConversationAndMember(any(), eq(creatorId))).thenReturn(0L);
-
- ConversationResponse response = conversationService.createConversation(request, creatorId);
-
- assertThat(response).isNotNull();
- // Aucun participant car creator introuvable et liste vide
- assertThat(response.getParticipantIds()).isEmpty();
- }
-
- // -------------------------------------------------------------------------
- // archiveConversation
- // -------------------------------------------------------------------------
-
- @Test
- @DisplayName("archiveConversation_notFound_throwsNotFound")
- void archiveConversation_notFound_throwsNotFound() {
- UUID convId = UUID.randomUUID();
- UUID membreId = UUID.randomUUID();
-
- when(conversationRepository.findByIdAndParticipant(convId, membreId)).thenReturn(Optional.empty());
-
- assertThatThrownBy(() -> conversationService.archiveConversation(convId, membreId, true))
- .isInstanceOf(NotFoundException.class)
- .hasMessageContaining("Conversation non trouvée");
- }
-
- @Test
- @DisplayName("archiveConversation_archive_setsTrue")
- void archiveConversation_archive_setsTrue() {
- UUID membreId = UUID.randomUUID();
- Conversation conv = mockConversation();
- conv.setIsArchived(false);
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
-
- conversationService.archiveConversation(conv.getId(), membreId, true);
-
- assertThat(conv.getIsArchived()).isTrue();
- verify(conversationRepository).persist(conv);
- }
-
- @Test
- @DisplayName("archiveConversation_unarchive_setsFalse")
- void archiveConversation_unarchive_setsFalse() {
- UUID membreId = UUID.randomUUID();
- Conversation conv = mockConversation();
- conv.setIsArchived(true);
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
-
- conversationService.archiveConversation(conv.getId(), membreId, false);
-
- assertThat(conv.getIsArchived()).isFalse();
- verify(conversationRepository).persist(conv);
- }
-
- // -------------------------------------------------------------------------
- // markAsRead
- // -------------------------------------------------------------------------
-
- @Test
- @DisplayName("markAsRead_notFound_throwsNotFound")
- void markAsRead_notFound_throwsNotFound() {
- UUID convId = UUID.randomUUID();
- UUID membreId = UUID.randomUUID();
-
- when(conversationRepository.findByIdAndParticipant(convId, membreId)).thenReturn(Optional.empty());
-
- assertThatThrownBy(() -> conversationService.markAsRead(convId, membreId))
- .isInstanceOf(NotFoundException.class)
- .hasMessageContaining("Conversation non trouvée");
- }
-
- @Test
- @DisplayName("markAsRead_success_callsRepo")
- void markAsRead_success_callsRepo() {
- UUID membreId = UUID.randomUUID();
- Conversation conv = mockConversation();
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
- when(messageRepository.markAllAsReadByConversationAndMember(conv.getId(), membreId)).thenReturn(5);
-
- conversationService.markAsRead(conv.getId(), membreId);
-
- verify(messageRepository).markAllAsReadByConversationAndMember(conv.getId(), membreId);
- }
-
- // -------------------------------------------------------------------------
- // toggleMute
- // -------------------------------------------------------------------------
-
- @Test
- @DisplayName("toggleMute_notFound_throwsNotFound")
- void toggleMute_notFound_throwsNotFound() {
- UUID convId = UUID.randomUUID();
- UUID membreId = UUID.randomUUID();
-
- when(conversationRepository.findByIdAndParticipant(convId, membreId)).thenReturn(Optional.empty());
-
- assertThatThrownBy(() -> conversationService.toggleMute(convId, membreId))
- .isInstanceOf(NotFoundException.class)
- .hasMessageContaining("Conversation non trouvée");
- }
-
- @Test
- @DisplayName("toggleMute_false_setsTrue")
- void toggleMute_false_setsTrue() {
- UUID membreId = UUID.randomUUID();
- Conversation conv = mockConversation();
- conv.setIsMuted(false);
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
-
- conversationService.toggleMute(conv.getId(), membreId);
-
- assertThat(conv.getIsMuted()).isTrue();
- }
-
- @Test
- @DisplayName("toggleMute_true_setsFalse")
- void toggleMute_true_setsFalse() {
- UUID membreId = UUID.randomUUID();
- Conversation conv = mockConversation();
- conv.setIsMuted(true);
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
-
- conversationService.toggleMute(conv.getId(), membreId);
-
- assertThat(conv.getIsMuted()).isFalse();
- }
-
- // -------------------------------------------------------------------------
- // togglePin
- // -------------------------------------------------------------------------
-
- @Test
- @DisplayName("togglePin_notFound_throwsNotFound")
- void togglePin_notFound_throwsNotFound() {
- UUID convId = UUID.randomUUID();
- UUID membreId = UUID.randomUUID();
-
- when(conversationRepository.findByIdAndParticipant(convId, membreId)).thenReturn(Optional.empty());
-
- assertThatThrownBy(() -> conversationService.togglePin(convId, membreId))
- .isInstanceOf(NotFoundException.class)
- .hasMessageContaining("Conversation non trouvée");
- }
-
- @Test
- @DisplayName("togglePin_false_setsTrue")
- void togglePin_false_setsTrue() {
- UUID membreId = UUID.randomUUID();
- Conversation conv = mockConversation();
- conv.setIsPinned(false);
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
-
- conversationService.togglePin(conv.getId(), membreId);
-
- assertThat(conv.getIsPinned()).isTrue();
- }
-
- @Test
- @DisplayName("togglePin_true_setsFalse")
- void togglePin_true_setsFalse() {
- UUID membreId = UUID.randomUUID();
- Conversation conv = mockConversation();
- conv.setIsPinned(true);
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
-
- conversationService.togglePin(conv.getId(), membreId);
-
- assertThat(conv.getIsPinned()).isFalse();
- }
-
- // -------------------------------------------------------------------------
- // convertToResponse (via getConversationById)
- // -------------------------------------------------------------------------
-
- @Test
- @DisplayName("convertToResponse_withLastMessage_includesMessage")
- void convertToResponse_withLastMessage_includesMessage() {
- UUID membreId = UUID.randomUUID();
- Conversation conv = mockConversation();
- Message lastMsg = mockMessage(conv);
- lastMsg.setContent("Dernier message");
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
- when(messageRepository.findLastByConversation(conv.getId())).thenReturn(lastMsg);
- when(messageRepository.countUnreadByConversationAndMember(conv.getId(), membreId)).thenReturn(0L);
-
- ConversationResponse response = conversationService.getConversationById(conv.getId(), membreId);
-
- assertThat(response.getLastMessage()).isNotNull();
- assertThat(response.getLastMessage().getContent()).isEqualTo("Dernier message");
- assertThat(response.getLastMessage().getId()).isEqualTo(lastMsg.getId());
- assertThat(response.getLastMessage().getSenderId()).isEqualTo(lastMsg.getSender().getId());
- }
-
- @Test
- @DisplayName("convertToResponse_noLastMessage_nullLastMessage")
- void convertToResponse_noLastMessage_nullLastMessage() {
- UUID membreId = UUID.randomUUID();
- Conversation conv = mockConversation();
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
- when(messageRepository.findLastByConversation(conv.getId())).thenReturn(null);
- when(messageRepository.countUnreadByConversationAndMember(conv.getId(), membreId)).thenReturn(0L);
-
- ConversationResponse response = conversationService.getConversationById(conv.getId(), membreId);
-
- assertThat(response.getLastMessage()).isNull();
- }
-}
diff --git a/src/test/java/dev/lions/unionflow/server/service/MessageServiceTest.java b/src/test/java/dev/lions/unionflow/server/service/MessageServiceTest.java
deleted file mode 100644
index c934f91..0000000
--- a/src/test/java/dev/lions/unionflow/server/service/MessageServiceTest.java
+++ /dev/null
@@ -1,621 +0,0 @@
-package dev.lions.unionflow.server.service;
-
-import dev.lions.unionflow.server.api.dto.communication.request.SendMessageRequest;
-import dev.lions.unionflow.server.api.dto.communication.response.MessageResponse;
-import dev.lions.unionflow.server.api.enums.communication.MessagePriority;
-import dev.lions.unionflow.server.api.enums.communication.MessageStatus;
-import dev.lions.unionflow.server.api.enums.communication.MessageType;
-import dev.lions.unionflow.server.entity.Conversation;
-import dev.lions.unionflow.server.entity.Membre;
-import dev.lions.unionflow.server.entity.Message;
-import dev.lions.unionflow.server.entity.Organisation;
-import dev.lions.unionflow.server.repository.ConversationRepository;
-import dev.lions.unionflow.server.repository.MembreRepository;
-import dev.lions.unionflow.server.repository.MessageRepository;
-import io.quarkus.test.InjectMock;
-import io.quarkus.test.junit.QuarkusTest;
-import io.quarkus.test.junit.mockito.InjectSpy;
-import jakarta.inject.Inject;
-import jakarta.persistence.EntityManager;
-import jakarta.ws.rs.NotFoundException;
-import org.junit.jupiter.api.*;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-import java.util.UUID;
-
-import static org.assertj.core.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
-
-/**
- * Tests unitaires pour MessageService
- *
- * @author UnionFlow Team
- * @version 1.0
- * @since 2026-03-20
- */
-@QuarkusTest
-@TestMethodOrder(MethodOrderer.DisplayName.class)
-class MessageServiceTest {
-
- @Inject
- MessageService messageService;
-
- @InjectSpy
- MessageRepository messageRepository;
-
- @InjectMock
- ConversationRepository conversationRepository;
-
- @InjectSpy
- MembreRepository membreRepository;
-
- @InjectMock
- EntityManager entityManager;
-
- // -------------------------------------------------------------------------
- // Helpers
- // -------------------------------------------------------------------------
-
- private Conversation mockConversation() {
- Conversation c = new Conversation();
- c.setId(UUID.randomUUID());
- c.setParticipants(new ArrayList<>());
- return c;
- }
-
- private Membre mockMembre(UUID id) {
- Membre m = new Membre();
- m.setId(id);
- m.setPrenom("Jean");
- m.setNom("Dupont");
- return m;
- }
-
- private Message mockMessage(UUID senderId) {
- Message msg = new Message();
- msg.setId(UUID.randomUUID());
- Conversation conv = mockConversation();
- msg.setConversation(conv);
- Membre sender = mockMembre(senderId);
- msg.setSender(sender);
- msg.setSenderName("Jean Dupont");
- msg.setContent("Hello");
- msg.setType(MessageType.INDIVIDUAL);
- msg.setStatus(MessageStatus.SENT);
- msg.setPriority(MessagePriority.NORMAL);
- msg.setIsEdited(false);
- msg.setIsDeleted(false);
- return msg;
- }
-
- // -------------------------------------------------------------------------
- // getMessages
- // -------------------------------------------------------------------------
-
- @Test
- @DisplayName("getMessages_conversationNotFound_throwsNotFound")
- void getMessages_conversationNotFound_throwsNotFound() {
- UUID convId = UUID.randomUUID();
- UUID membreId = UUID.randomUUID();
-
- when(conversationRepository.findByIdAndParticipant(convId, membreId)).thenReturn(Optional.empty());
-
- assertThatThrownBy(() -> messageService.getMessages(convId, membreId, 20))
- .isInstanceOf(NotFoundException.class)
- .hasMessageContaining("Conversation non trouvée");
- }
-
- @Test
- @DisplayName("getMessages_found_returnsList")
- void getMessages_found_returnsList() {
- UUID membreId = UUID.randomUUID();
- Conversation conv = mockConversation();
- Message msg = mockMessage(UUID.randomUUID());
- msg.setConversation(conv);
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
- doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);
-
- List result = messageService.getMessages(conv.getId(), membreId, 20);
-
- assertThat(result).hasSize(1);
- assertThat(result.get(0).getContent()).isEqualTo("Hello");
- }
-
- // -------------------------------------------------------------------------
- // sendMessage
- // -------------------------------------------------------------------------
-
- @Test
- @DisplayName("sendMessage_conversationNotFound_throwsNotFound")
- void sendMessage_conversationNotFound_throwsNotFound() {
- UUID senderId = UUID.randomUUID();
- UUID convId = UUID.randomUUID();
-
- SendMessageRequest request = SendMessageRequest.builder()
- .conversationId(convId)
- .content("test")
- .build();
-
- when(conversationRepository.findByIdAndParticipant(convId, senderId)).thenReturn(Optional.empty());
-
- assertThatThrownBy(() -> messageService.sendMessage(request, senderId))
- .isInstanceOf(NotFoundException.class)
- .hasMessageContaining("Conversation non trouvée");
- }
-
- @Test
- @DisplayName("sendMessage_senderNotFound_throwsNotFound")
- void sendMessage_senderNotFound_throwsNotFound() {
- UUID senderId = UUID.randomUUID();
- Conversation conv = mockConversation();
-
- SendMessageRequest request = SendMessageRequest.builder()
- .conversationId(conv.getId())
- .content("test")
- .build();
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), senderId)).thenReturn(Optional.of(conv));
- // entityManager.find returns null by default → findById(senderId) returns null → service throws
-
- assertThatThrownBy(() -> messageService.sendMessage(request, senderId))
- .isInstanceOf(NotFoundException.class)
- .hasMessageContaining("Expéditeur non trouvé");
- }
-
- @Test
- @DisplayName("sendMessage_withDefaultTypeAndPriority_success")
- void sendMessage_withDefaultTypeAndPriority_success() {
- UUID senderId = UUID.randomUUID();
- Conversation conv = mockConversation();
- Membre sender = mockMembre(senderId);
-
- SendMessageRequest request = SendMessageRequest.builder()
- .conversationId(conv.getId())
- .content("Bonjour")
- .type(null)
- .priority(null)
- .recipientIds(null)
- .recipientRoles(null)
- .attachments(null)
- .build();
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), senderId)).thenReturn(Optional.of(conv));
- when(entityManager.find(Membre.class, senderId)).thenReturn(sender);
-
- MessageResponse response = messageService.sendMessage(request, senderId);
-
- assertThat(response).isNotNull();
- assertThat(response.getContent()).isEqualTo("Bonjour");
- assertThat(response.getType()).isEqualTo(MessageType.INDIVIDUAL);
- assertThat(response.getPriority()).isEqualTo(MessagePriority.NORMAL);
- assertThat(response.getSenderName()).isEqualTo("Jean Dupont");
- verify(messageRepository).persist(any(Message.class));
- verify(conversationRepository).persist(conv);
- }
-
- @Test
- @DisplayName("sendMessage_withExplicitType_usesType")
- void sendMessage_withExplicitType_usesType() {
- UUID senderId = UUID.randomUUID();
- Conversation conv = mockConversation();
- Membre sender = mockMembre(senderId);
-
- SendMessageRequest request = SendMessageRequest.builder()
- .conversationId(conv.getId())
- .content("Broadcast!")
- .type(MessageType.BROADCAST)
- .priority(MessagePriority.HIGH)
- .recipientIds(null)
- .recipientRoles(null)
- .attachments(null)
- .build();
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), senderId)).thenReturn(Optional.of(conv));
- when(entityManager.find(Membre.class, senderId)).thenReturn(sender);
-
- MessageResponse response = messageService.sendMessage(request, senderId);
-
- assertThat(response.getType()).isEqualTo(MessageType.BROADCAST);
- assertThat(response.getPriority()).isEqualTo(MessagePriority.HIGH);
- }
-
- @Test
- @DisplayName("sendMessage_withRecipientIds_setsCSV")
- void sendMessage_withRecipientIds_setsCSV() {
- UUID senderId = UUID.randomUUID();
- UUID recipient1 = UUID.randomUUID();
- UUID recipient2 = UUID.randomUUID();
- Conversation conv = mockConversation();
- Membre sender = mockMembre(senderId);
-
- SendMessageRequest request = SendMessageRequest.builder()
- .conversationId(conv.getId())
- .content("Targeted")
- .type(MessageType.TARGETED)
- .priority(null)
- .recipientIds(List.of(recipient1, recipient2))
- .recipientRoles(null)
- .attachments(null)
- .build();
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), senderId)).thenReturn(Optional.of(conv));
- when(entityManager.find(Membre.class, senderId)).thenReturn(sender);
-
- MessageResponse response = messageService.sendMessage(request, senderId);
-
- assertThat(response.getRecipientIds()).isNotNull();
- assertThat(response.getRecipientIds()).containsExactlyInAnyOrder(recipient1, recipient2);
- }
-
- @Test
- @DisplayName("sendMessage_withRecipientRoles_setsCSV")
- void sendMessage_withRecipientRoles_setsCSV() {
- UUID senderId = UUID.randomUUID();
- Conversation conv = mockConversation();
- Membre sender = mockMembre(senderId);
-
- SendMessageRequest request = SendMessageRequest.builder()
- .conversationId(conv.getId())
- .content("Role msg")
- .type(null)
- .priority(null)
- .recipientIds(null)
- .recipientRoles(List.of("ADMIN", "TRESORIER"))
- .attachments(null)
- .build();
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), senderId)).thenReturn(Optional.of(conv));
- when(entityManager.find(Membre.class, senderId)).thenReturn(sender);
-
- MessageResponse response = messageService.sendMessage(request, senderId);
-
- assertThat(response.getRecipientRoles()).isNotNull();
- assertThat(response.getRecipientRoles()).containsExactly("ADMIN", "TRESORIER");
- }
-
- @Test
- @DisplayName("sendMessage_withAttachments_setsCSV")
- void sendMessage_withAttachments_setsCSV() {
- UUID senderId = UUID.randomUUID();
- Conversation conv = mockConversation();
- Membre sender = mockMembre(senderId);
-
- SendMessageRequest request = SendMessageRequest.builder()
- .conversationId(conv.getId())
- .content("Msg avec PJ")
- .type(null)
- .priority(null)
- .recipientIds(null)
- .recipientRoles(null)
- .attachments(List.of("https://cdn.example.com/doc1.pdf", "https://cdn.example.com/img1.png"))
- .build();
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), senderId)).thenReturn(Optional.of(conv));
- when(entityManager.find(Membre.class, senderId)).thenReturn(sender);
-
- MessageResponse response = messageService.sendMessage(request, senderId);
-
- assertThat(response.getAttachments()).isNotNull();
- assertThat(response.getAttachments()).containsExactly(
- "https://cdn.example.com/doc1.pdf",
- "https://cdn.example.com/img1.png"
- );
- }
-
- @Test
- @DisplayName("sendMessage_noRecipientsNoRolesNoAttachments_noCSV")
- void sendMessage_noRecipientsNoRolesNoAttachments_noCSV() {
- UUID senderId = UUID.randomUUID();
- Conversation conv = mockConversation();
- Membre sender = mockMembre(senderId);
-
- SendMessageRequest request = SendMessageRequest.builder()
- .conversationId(conv.getId())
- .content("Simple")
- .type(null)
- .priority(null)
- .recipientIds(new ArrayList<>())
- .recipientRoles(new ArrayList<>())
- .attachments(new ArrayList<>())
- .build();
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), senderId)).thenReturn(Optional.of(conv));
- when(entityManager.find(Membre.class, senderId)).thenReturn(sender);
-
- MessageResponse response = messageService.sendMessage(request, senderId);
-
- assertThat(response.getRecipientIds()).isNull();
- assertThat(response.getRecipientRoles()).isNull();
- assertThat(response.getAttachments()).isNull();
- }
-
- // -------------------------------------------------------------------------
- // editMessage
- // -------------------------------------------------------------------------
-
- @Test
- @DisplayName("editMessage_notFound_throwsNotFound")
- void editMessage_notFound_throwsNotFound() {
- UUID messageId = UUID.randomUUID();
- UUID senderId = UUID.randomUUID();
-
- // entityManager.find returns null by default → findById(messageId) returns null → service throws
-
- assertThatThrownBy(() -> messageService.editMessage(messageId, senderId, "nouveau contenu"))
- .isInstanceOf(NotFoundException.class)
- .hasMessageContaining("Message non trouvé");
- }
-
- @Test
- @DisplayName("editMessage_wrongSender_throwsIllegalState")
- void editMessage_wrongSender_throwsIllegalState() {
- UUID realSenderId = UUID.randomUUID();
- UUID wrongSenderId = UUID.randomUUID();
- Message msg = mockMessage(realSenderId);
-
- when(entityManager.find(Message.class, msg.getId())).thenReturn(msg);
-
- assertThatThrownBy(() -> messageService.editMessage(msg.getId(), wrongSenderId, "contenu modifié"))
- .isInstanceOf(IllegalStateException.class)
- .hasMessageContaining("propres messages");
- }
-
- @Test
- @DisplayName("editMessage_success_updatesContent")
- void editMessage_success_updatesContent() {
- UUID senderId = UUID.randomUUID();
- Message msg = mockMessage(senderId);
- msg.setIsEdited(false);
-
- when(entityManager.find(Message.class, msg.getId())).thenReturn(msg);
-
- MessageResponse response = messageService.editMessage(msg.getId(), senderId, "Contenu édité");
-
- assertThat(msg.getContent()).isEqualTo("Contenu édité");
- assertThat(msg.getIsEdited()).isTrue();
- assertThat(msg.getEditedAt()).isNotNull();
- verify(messageRepository).persist(msg);
- assertThat(response.getContent()).isEqualTo("Contenu édité");
- assertThat(response.isEdited()).isTrue();
- }
-
- // -------------------------------------------------------------------------
- // deleteMessage
- // -------------------------------------------------------------------------
-
- @Test
- @DisplayName("deleteMessage_notFound_throwsNotFound")
- void deleteMessage_notFound_throwsNotFound() {
- UUID messageId = UUID.randomUUID();
- UUID senderId = UUID.randomUUID();
-
- // entityManager.find returns null by default → findById(messageId) returns null → service throws
-
- assertThatThrownBy(() -> messageService.deleteMessage(messageId, senderId))
- .isInstanceOf(NotFoundException.class)
- .hasMessageContaining("Message non trouvé");
- }
-
- @Test
- @DisplayName("deleteMessage_wrongSender_throwsIllegalState")
- void deleteMessage_wrongSender_throwsIllegalState() {
- UUID realSenderId = UUID.randomUUID();
- UUID wrongSenderId = UUID.randomUUID();
- Message msg = mockMessage(realSenderId);
-
- when(entityManager.find(Message.class, msg.getId())).thenReturn(msg);
-
- assertThatThrownBy(() -> messageService.deleteMessage(msg.getId(), wrongSenderId))
- .isInstanceOf(IllegalStateException.class)
- .hasMessageContaining("propres messages");
- }
-
- @Test
- @DisplayName("deleteMessage_success_softDeletes")
- void deleteMessage_success_softDeletes() {
- UUID senderId = UUID.randomUUID();
- Message msg = mockMessage(senderId);
- msg.setIsDeleted(false);
- msg.setContent("Contenu original");
-
- when(entityManager.find(Message.class, msg.getId())).thenReturn(msg);
-
- messageService.deleteMessage(msg.getId(), senderId);
-
- assertThat(msg.getIsDeleted()).isTrue();
- assertThat(msg.getContent()).isEqualTo("[Message supprimé]");
- verify(messageRepository).persist(msg);
- }
-
- // -------------------------------------------------------------------------
- // convertToResponse (via getMessages)
- // -------------------------------------------------------------------------
-
- @Test
- @DisplayName("convertToResponse_withRecipientIds_parsesCsv")
- void convertToResponse_withRecipientIds_parsesCsv() {
- UUID membreId = UUID.randomUUID();
- Conversation conv = mockConversation();
- UUID r1 = UUID.randomUUID();
- UUID r2 = UUID.randomUUID();
-
- Message msg = mockMessage(UUID.randomUUID());
- msg.setConversation(conv);
- msg.setRecipientIds(r1 + "," + r2);
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
- doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);
-
- List result = messageService.getMessages(conv.getId(), membreId, 20);
-
- assertThat(result).hasSize(1);
- assertThat(result.get(0).getRecipientIds()).containsExactlyInAnyOrder(r1, r2);
- }
-
- @Test
- @DisplayName("convertToResponse_withRecipientRoles_parsesCsv")
- void convertToResponse_withRecipientRoles_parsesCsv() {
- UUID membreId = UUID.randomUUID();
- Conversation conv = mockConversation();
-
- Message msg = mockMessage(UUID.randomUUID());
- msg.setConversation(conv);
- msg.setRecipientRoles("ADMIN,SECRETAIRE");
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
- doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);
-
- List result = messageService.getMessages(conv.getId(), membreId, 20);
-
- assertThat(result.get(0).getRecipientRoles()).containsExactly("ADMIN", "SECRETAIRE");
- }
-
- @Test
- @DisplayName("convertToResponse_withAttachments_parsesCsv")
- void convertToResponse_withAttachments_parsesCsv() {
- UUID membreId = UUID.randomUUID();
- Conversation conv = mockConversation();
-
- Message msg = mockMessage(UUID.randomUUID());
- msg.setConversation(conv);
- msg.setAttachments("https://cdn.example.com/a.pdf,https://cdn.example.com/b.png");
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
- doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);
-
- List result = messageService.getMessages(conv.getId(), membreId, 20);
-
- assertThat(result.get(0).getAttachments()).containsExactly(
- "https://cdn.example.com/a.pdf",
- "https://cdn.example.com/b.png"
- );
- }
-
- @Test
- @DisplayName("convertToResponse_noRecipients_nullFields")
- void convertToResponse_noRecipients_nullFields() {
- UUID membreId = UUID.randomUUID();
- Conversation conv = mockConversation();
-
- Message msg = mockMessage(UUID.randomUUID());
- msg.setConversation(conv);
- // recipientIds, recipientRoles et attachments sont null par défaut
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
- doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);
-
- List result = messageService.getMessages(conv.getId(), membreId, 20);
-
- MessageResponse response = result.get(0);
- assertThat(response.getRecipientIds()).isNull();
- assertThat(response.getRecipientRoles()).isNull();
- assertThat(response.getAttachments()).isNull();
- }
-
- @Test
- @DisplayName("convertToResponse_withOrganisation_setsOrgId")
- void convertToResponse_withOrganisation_setsOrgId() {
- UUID membreId = UUID.randomUUID();
- UUID orgId = UUID.randomUUID();
- Conversation conv = mockConversation();
- Organisation org = new Organisation();
- org.setId(orgId);
- conv.setOrganisation(org);
-
- Message msg = mockMessage(UUID.randomUUID());
- msg.setConversation(conv);
- msg.setOrganisation(org);
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
- doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);
-
- List result = messageService.getMessages(conv.getId(), membreId, 20);
-
- assertThat(result.get(0).getOrganisationId()).isEqualTo(orgId);
- }
-
- @Test
- @DisplayName("convertToResponse_noOrganisation_nullOrgId")
- void convertToResponse_noOrganisation_nullOrgId() {
- UUID membreId = UUID.randomUUID();
- Conversation conv = mockConversation();
- // organisation est null
-
- Message msg = mockMessage(UUID.randomUUID());
- msg.setConversation(conv);
- msg.setOrganisation(null);
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
- doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);
-
- List result = messageService.getMessages(conv.getId(), membreId, 20);
-
- assertThat(result.get(0).getOrganisationId()).isNull();
- }
-
- // -------------------------------------------------------------------------
- // convertToResponse — branches isEmpty() (non-null mais vide)
- // L163: recipientIds != null && !isEmpty() → false (empty string → isEmpty = true)
- // L172: recipientRoles != null && !isEmpty() → false
- // L178: attachments != null && !isEmpty() → false
- // -------------------------------------------------------------------------
-
- @Test
- @DisplayName("convertToResponse_recipientIdsEmptyString_returnsNullRecipientIds (L163 false)")
- void convertToResponse_recipientIdsEmptyString_returnsNullRecipientIds() {
- UUID membreId = UUID.randomUUID();
- Conversation conv = mockConversation();
-
- Message msg = mockMessage(UUID.randomUUID());
- msg.setConversation(conv);
- // non-null mais vide → L163: isEmpty() = true → condition false → recipientIds = null
- msg.setRecipientIds("");
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
- doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);
-
- List result = messageService.getMessages(conv.getId(), membreId, 20);
-
- assertThat(result.get(0).getRecipientIds()).isNull();
- }
-
- @Test
- @DisplayName("convertToResponse_recipientRolesEmptyString_returnsNullRoles (L172 false)")
- void convertToResponse_recipientRolesEmptyString_returnsNullRoles() {
- UUID membreId = UUID.randomUUID();
- Conversation conv = mockConversation();
-
- Message msg = mockMessage(UUID.randomUUID());
- msg.setConversation(conv);
- msg.setRecipientRoles(""); // non-null mais vide → L172 false → roles = null
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
- doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);
-
- List result = messageService.getMessages(conv.getId(), membreId, 20);
-
- assertThat(result.get(0).getRecipientRoles()).isNull();
- }
-
- @Test
- @DisplayName("convertToResponse_attachmentsEmptyString_returnsNullAttachments (L178 false)")
- void convertToResponse_attachmentsEmptyString_returnsNullAttachments() {
- UUID membreId = UUID.randomUUID();
- Conversation conv = mockConversation();
-
- Message msg = mockMessage(UUID.randomUUID());
- msg.setConversation(conv);
- msg.setAttachments(""); // non-null mais vide → L178 false → attachments = null
-
- when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
- doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);
-
- List result = messageService.getMessages(conv.getId(), membreId, 20);
-
- assertThat(result.get(0).getAttachments()).isNull();
- }
-}