diff --git a/src/main/java/dev/lions/user/manager/resource/UserResource.java b/src/main/java/dev/lions/user/manager/resource/UserResource.java index bf51fa6..5f82741 100644 --- a/src/main/java/dev/lions/user/manager/resource/UserResource.java +++ b/src/main/java/dev/lions/user/manager/resource/UserResource.java @@ -22,6 +22,7 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import java.time.LocalDateTime; import java.util.List; /** @@ -407,6 +408,86 @@ public class UserResource { } } + /** + * Exporter les utilisateurs en CSV + */ + @GET + @Path("/export/csv") + @Operation(summary = "Exporter les utilisateurs en CSV") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Fichier CSV généré avec succès"), + @APIResponse(responseCode = "400", description = "Realm manquant ou invalide"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "user_manager", "user_viewer"}) + @Produces(MediaType.TEXT_PLAIN) + public Response exportUsersToCSV(@QueryParam("realm") @NotBlank String realmName) { + log.info("GET /api/users/export/csv - realm: {}", realmName); + + try { + UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder() + .realmName(realmName) + .pageSize(10000) // Export complet sans pagination + .page(0) + .build(); + + String csvContent = userService.exportUsersToCSV(criteria); + + String filename = "users_export_" + + LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HHmmss")) + + ".csv"; + + return Response.ok(csvContent) + .header("Content-Disposition", "attachment; filename=\"" + filename + "\"") + .build(); + } catch (Exception e) { + log.error("Erreur lors de l'export CSV des utilisateurs", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + /** + * Importer des utilisateurs depuis CSV avec rapport détaillé + */ + @POST + @Path("/import/csv") + @Operation(summary = "Importer des utilisateurs depuis un fichier CSV") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Import terminé avec rapport détaillé"), + @APIResponse(responseCode = "400", description = "Fichier CSV vide ou invalide"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "user_manager"}) + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.APPLICATION_JSON) + public Response importUsersFromCSV( + @QueryParam("realm") @NotBlank String realmName, + String csvContent) { + log.info("POST /api/users/import/csv - realm: {}", realmName); + + try { + if (csvContent == null || csvContent.trim().isEmpty()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Le contenu CSV est vide")) + .build(); + } + + dev.lions.user.manager.dto.importexport.ImportResultDTO result = userService.importUsersFromCSV(csvContent, realmName); + + log.info("{} utilisateur(s) importé(s) dans le realm {} ({} erreur(s))", + result.getSuccessCount(), realmName, result.getErrorCount()); + + return Response.ok(result).build(); + } catch (Exception e) { + log.error("Erreur lors de l'import CSV des utilisateurs", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + // ==================== DTOs internes ==================== @Schema(description = "Requête de réinitialisation de mot de passe") diff --git a/src/main/java/dev/lions/user/manager/service/impl/AuditServiceImpl.java b/src/main/java/dev/lions/user/manager/service/impl/AuditServiceImpl.java index 26a2827..2924283 100644 --- a/src/main/java/dev/lions/user/manager/service/impl/AuditServiceImpl.java +++ b/src/main/java/dev/lions/user/manager/service/impl/AuditServiceImpl.java @@ -2,8 +2,12 @@ package dev.lions.user.manager.service.impl; import dev.lions.user.manager.dto.audit.AuditLogDTO; import dev.lions.user.manager.enums.audit.TypeActionAudit; +import dev.lions.user.manager.server.impl.entity.AuditLogEntity; +import dev.lions.user.manager.server.impl.mapper.AuditLogMapper; import dev.lions.user.manager.service.AuditService; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -19,19 +23,49 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; /** - * Implémentation du service d'audit + * Implémentation du service d'audit avec support de la persistance PostgreSQL. * - * NOTES: - * - Cette implémentation utilise un stockage en mémoire pour le développement - * - En production, il faudrait utiliser une base de données (PostgreSQL avec Panache) - * - Les logs sont également écrits via SLF4J pour être capturés par les systèmes de logging centralisés + *

Architecture Hybride:

+ * + * + *

Configuration:

+ * + * + *

Modes de Fonctionnement:

+ *
+ * Mode DEV (logToDatabase=false):
+ *   - Stockage en mémoire uniquement
+ *   - Logs perdus au redémarrage
+ *   - Performances maximales
+ *
+ * Mode PROD (logToDatabase=true):
+ *   - Persistance PostgreSQL
+ *   - Cache mémoire pour requêtes fréquentes
+ *   - Historique complet préservé
+ * 
+ * + * @author Lions Development Team + * @version 2.0.0 + * @since 2026-01-02 */ @ApplicationScoped @Slf4j public class AuditServiceImpl implements AuditService { - // Stockage en mémoire (à remplacer par une DB en production) - private final Map auditLogs = new ConcurrentHashMap<>(); + // ==================== DÉPENDANCES ==================== + + @Inject + AuditLogMapper auditLogMapper; + + // ==================== CONFIGURATION ==================== @ConfigProperty(name = "lions.audit.enabled", defaultValue = "true") boolean auditEnabled; @@ -39,7 +73,24 @@ public class AuditServiceImpl implements AuditService { @ConfigProperty(name = "lions.audit.log-to-database", defaultValue = "false") boolean logToDatabase; + @ConfigProperty(name = "lions.audit.cache-size", defaultValue = "10000") + int cacheSize; + + @ConfigProperty(name = "lions.audit.retention-days", defaultValue = "365") + int retentionDays; + + // ==================== STOCKAGE ==================== + + /** + * Cache en mémoire pour les logs récents. + *

Limité à {@code cacheSize} entrées. Les plus anciens sont supprimés automatiquement.

+ */ + private final Map auditLogsCache = new ConcurrentHashMap<>(); + + // ==================== MÉTHODES PRINCIPALES ==================== + @Override + @Transactional public AuditLogDTO logAction(@Valid @NotNull AuditLogDTO auditLog) { if (!auditEnabled) { log.debug("Audit désactivé, log ignoré"); @@ -56,7 +107,7 @@ public class AuditServiceImpl implements AuditService { auditLog.setDateAction(LocalDateTime.now()); } - // Log structuré pour les systèmes de logging (Graylog, Elasticsearch, etc.) + // Log structuré pour les systèmes de logging externes (Graylog, Elasticsearch, etc.) log.info("AUDIT | Type: {} | Acteur: {} | Ressource: {} | Succès: {} | IP: {} | Détails: {}", auditLog.getTypeAction(), auditLog.getActeurUsername(), @@ -65,15 +116,30 @@ public class AuditServiceImpl implements AuditService { auditLog.getIpAddress(), auditLog.getDescription()); - // Stocker en mémoire - auditLogs.put(auditLog.getId(), auditLog); + // Stocker en base de données si activé + if (logToDatabase) { + try { + AuditLogEntity entity = auditLogMapper.toEntity(auditLog); + // Le mapper s'occupe du mapping automatique via @Mapping annotations + // Ajout des champs additionnels non mappés automatiquement + entity.setRealmName(auditLog.getRealmName()); - // TODO: Si logToDatabase = true, persister dans PostgreSQL via Panache - // Exemple: - // if (logToDatabase) { - // AuditLogEntity entity = AuditLogMapper.toEntity(auditLog); - // entity.persist(); - // } + entity.persist(); + + log.debug("Log d'audit persisté en base de données avec ID: {}", entity.id); + } catch (Exception e) { + log.error("Erreur lors de la persistance du log d'audit en base de données", e); + // On ne lance pas d'exception pour ne pas bloquer le processus métier + } + } + + // Ajouter au cache mémoire (pour performances) + auditLogsCache.put(auditLog.getId(), auditLog); + + // Nettoyer le cache si trop grand + if (auditLogsCache.size() > cacheSize) { + cleanOldestCacheEntries(); + } return auditLog; } @@ -88,7 +154,7 @@ public class AuditServiceImpl implements AuditService { String description) { AuditLogDTO auditLog = AuditLogDTO.builder() .acteurUserId(acteurUserId) - .acteurUsername(acteurUserId) // Utiliser acteurUserId comme username pour l'instant + .acteurUsername(acteurUserId) .typeAction(typeAction) .ressourceType(ressourceType) .ressourceId(ressourceId != null ? ressourceId : "") @@ -111,7 +177,7 @@ public class AuditServiceImpl implements AuditService { String errorMessage) { AuditLogDTO auditLog = AuditLogDTO.builder() .acteurUserId(acteurUserId) - .acteurUsername(acteurUserId) // Utiliser acteurUserId comme username pour l'instant + .acteurUsername(acteurUserId) .typeAction(typeAction) .ressourceType(ressourceType) .ressourceId(ressourceId != null ? ressourceId : "") @@ -123,13 +189,18 @@ public class AuditServiceImpl implements AuditService { logAction(auditLog); } + // ==================== MÉTHODES DE RECHERCHE ==================== + @Override public List findByActeur(@NotBlank String acteurUserId, LocalDateTime dateDebut, LocalDateTime dateFin, int page, int pageSize) { - return searchLogs(acteurUserId, dateDebut, dateFin, null, null, null, page, pageSize); + if (logToDatabase) { + return searchLogsFromDatabase(acteurUserId, dateDebut, dateFin, null, null, null, page, pageSize); + } + return searchLogsFromCache(acteurUserId, dateDebut, dateFin, null, null, null, page, pageSize); } @Override @@ -139,7 +210,13 @@ public class AuditServiceImpl implements AuditService { LocalDateTime dateFin, int page, int pageSize) { - return searchLogs(null, dateDebut, dateFin, null, ressourceType, null, page, pageSize) + if (logToDatabase) { + return searchLogsFromDatabase(null, dateDebut, dateFin, null, ressourceType, null, page, pageSize) + .stream() + .filter(log -> ressourceId.equals(log.getRessourceId())) + .collect(Collectors.toList()); + } + return searchLogsFromCache(null, dateDebut, dateFin, null, ressourceType, null, page, pageSize) .stream() .filter(log -> ressourceId.equals(log.getRessourceId())) .collect(Collectors.toList()); @@ -152,7 +229,10 @@ public class AuditServiceImpl implements AuditService { LocalDateTime dateFin, int page, int pageSize) { - return searchLogs(null, dateDebut, dateFin, typeAction, null, null, page, pageSize); + if (logToDatabase) { + return searchLogsFromDatabase(null, dateDebut, dateFin, typeAction, null, null, page, pageSize); + } + return searchLogsFromCache(null, dateDebut, dateFin, typeAction, null, null, page, pageSize); } @Override @@ -161,8 +241,11 @@ public class AuditServiceImpl implements AuditService { LocalDateTime dateFin, int page, int pageSize) { - // Pour l'instant, on retourne tous les logs car on n'a pas de champ realmName dans AuditLogDTO - return searchLogs(null, dateDebut, dateFin, null, null, null, page, pageSize); + if (logToDatabase) { + List entities = AuditLogEntity.findByRealm(realmName); + return auditLogMapper.toDTOList(entities); + } + return searchLogsFromCache(null, dateDebut, dateFin, null, null, null, page, pageSize); } @Override @@ -171,7 +254,10 @@ public class AuditServiceImpl implements AuditService { LocalDateTime dateFin, int page, int pageSize) { - return searchLogs(null, dateDebut, dateFin, null, null, false, page, pageSize); + if (logToDatabase) { + return searchLogsFromDatabase(null, dateDebut, dateFin, null, null, false, page, pageSize); + } + return searchLogsFromCache(null, dateDebut, dateFin, null, null, false, page, pageSize); } @Override @@ -180,29 +266,22 @@ public class AuditServiceImpl implements AuditService { LocalDateTime dateFin, int page, int pageSize) { - // Les actions critiques sont USER_DELETE, ROLE_DELETE, etc. - return auditLogs.values().stream() + List allLogs = logToDatabase ? + searchLogsFromDatabase(null, dateDebut, dateFin, null, null, null, page, pageSize) : + searchLogsFromCache(null, dateDebut, dateFin, null, null, null, page, pageSize); + + return allLogs.stream() .filter(log -> { TypeActionAudit type = log.getTypeAction(); - return type == TypeActionAudit.USER_DELETE || + return type == TypeActionAudit.USER_DELETE || type == TypeActionAudit.ROLE_DELETE || type == TypeActionAudit.SESSION_REVOKE_ALL; }) - .filter(log -> { - if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { - return false; - } - if (dateFin != null && log.getDateAction().isAfter(dateFin)) { - return false; - } - return true; - }) - .sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction())) - .skip((long) page * pageSize) - .limit(pageSize) .collect(Collectors.toList()); } + // ==================== MÉTHODES STATISTIQUES ==================== + @Override public Map countByActionType(@NotBlank String realmName, LocalDateTime dateDebut, @@ -223,13 +302,43 @@ public class AuditServiceImpl implements AuditService { LocalDateTime dateFin) { long successCount = getSuccessCount(dateDebut, dateFin); long failureCount = getFailureCount(dateDebut, dateFin); - + Map result = new java.util.HashMap<>(); result.put("success", successCount); result.put("failure", failureCount); return result; } + @Override + public Map getAuditStatistics(@NotBlank String realmName, + LocalDateTime dateDebut, + LocalDateTime dateFin) { + Map stats = new java.util.HashMap<>(); + + long total = logToDatabase ? + AuditLogEntity.findByPeriod(dateDebut, dateFin).size() : + auditLogsCache.values().stream() + .filter(log -> { + if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { + return false; + } + if (dateFin != null && log.getDateAction().isAfter(dateFin)) { + return false; + } + return true; + }) + .count(); + + stats.put("total", total); + stats.put("success", getSuccessCount(dateDebut, dateFin)); + stats.put("failure", getFailureCount(dateDebut, dateFin)); + stats.put("byActionType", countByActionType(realmName, dateDebut, dateFin)); + stats.put("byActeur", countByActeur(realmName, dateDebut, dateFin)); + return stats; + } + + // ==================== EXPORT / PURGE ==================== + @Override public String exportToCSV(@NotBlank String realmName, LocalDateTime dateDebut, @@ -239,159 +348,192 @@ public class AuditServiceImpl implements AuditService { } @Override + @Transactional public long purgeOldLogs(@NotNull LocalDateTime dateLimite) { - long beforeCount = auditLogs.size(); - auditLogs.entrySet().removeIf(entry -> + long purgedCount = 0; + + // Purge en base de données si activé + if (logToDatabase) { + purgedCount = AuditLogEntity.deleteOlderThan(dateLimite); + log.info("Supprimé {} logs d'audit de la base de données avant {}", purgedCount, dateLimite); + } + + // Purge du cache mémoire + long beforeCacheCount = auditLogsCache.size(); + auditLogsCache.entrySet().removeIf(entry -> entry.getValue().getDateAction().isBefore(dateLimite) ); - long afterCount = auditLogs.size(); - return beforeCount - afterCount; + long cacheRemoved = beforeCacheCount - auditLogsCache.size(); + + log.info("Supprimé {} logs du cache mémoire avant {}", cacheRemoved, dateLimite); + + return purgedCount + cacheRemoved; } - @Override - public Map getAuditStatistics(@NotBlank String realmName, - LocalDateTime dateDebut, - LocalDateTime dateFin) { - Map stats = new java.util.HashMap<>(); - stats.put("total", auditLogs.values().stream() - .filter(log -> { - if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { - return false; - } - if (dateFin != null && log.getDateAction().isAfter(dateFin)) { - return false; - } - return true; - }) - .count()); - stats.put("success", getSuccessCount(dateDebut, dateFin)); - stats.put("failure", getFailureCount(dateDebut, dateFin)); - stats.put("byActionType", countByActionType(realmName, dateDebut, dateFin)); - stats.put("byActeur", countByActeur(realmName, dateDebut, dateFin)); - return stats; - } + // ==================== MÉTHODES PRIVÉES ==================== - // Méthode privée helper pour la recherche - private List searchLogs(String acteurUsername, LocalDateTime dateDebut, - LocalDateTime dateFin, TypeActionAudit typeAction, - String ressourceType, Boolean succes, - int page, int pageSize) { - log.debug("Recherche de logs d'audit: acteur={}, dateDebut={}, dateFin={}, typeAction={}, succes={}", - acteurUsername, dateDebut, dateFin, typeAction, succes); + /** + * Recherche les logs depuis le cache mémoire. + */ + private List searchLogsFromCache(String acteurUsername, LocalDateTime dateDebut, + LocalDateTime dateFin, TypeActionAudit typeAction, + String ressourceType, Boolean succes, + int page, int pageSize) { + log.debug("Recherche logs depuis cache mémoire"); - return auditLogs.values().stream() - .filter(log -> { - // Filtre par acteur (si spécifié et non "*") - if (acteurUsername != null && !"*".equals(acteurUsername) && - !acteurUsername.equals(log.getActeurUsername())) { - return false; - } - - // Filtre par date début - if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { - return false; - } - - // Filtre par date fin - if (dateFin != null && log.getDateAction().isAfter(dateFin)) { - return false; - } - - // Filtre par type d'action - if (typeAction != null && !typeAction.equals(log.getTypeAction())) { - return false; - } - - // Filtre par type de ressource - if (ressourceType != null && !ressourceType.equals(log.getRessourceType())) { - return false; - } - - // Filtre par succès/échec - if (succes != null && succes != log.isSuccessful()) { - return false; - } - - return true; - }) - .sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction())) // Tri décroissant par date + return auditLogsCache.values().stream() + .filter(log -> applyFilters(log, acteurUsername, dateDebut, dateFin, typeAction, ressourceType, succes)) + .sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction())) .skip((long) page * pageSize) .limit(pageSize) .collect(Collectors.toList()); } - // Méthodes privées helper - private Map getActionStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) { - log.debug("Calcul des statistiques d'actions entre {} et {}", dateDebut, dateFin); + /** + * Recherche les logs depuis la base de données PostgreSQL. + */ + private List searchLogsFromDatabase(String acteurUsername, LocalDateTime dateDebut, + LocalDateTime dateFin, TypeActionAudit typeAction, + String ressourceType, Boolean succes, + int page, int pageSize) { + log.debug("Recherche logs depuis base de données"); - return auditLogs.values().stream() + List entities; + + // Optimisation: utiliser les requêtes spécialisées si possible + if (acteurUsername != null && typeAction == null && ressourceType == null) { + entities = AuditLogEntity.findByAuteur(acteurUsername); + } else if (typeAction != null && acteurUsername == null && ressourceType == null) { + entities = AuditLogEntity.findByAction(typeAction); + } else if (dateDebut != null && dateFin != null) { + entities = AuditLogEntity.findByPeriod(dateDebut, dateFin); + } else { + entities = AuditLogEntity.listAll(); + } + + return entities.stream() + .map(auditLogMapper::toDTO) + .filter(log -> applyFilters(log, acteurUsername, dateDebut, dateFin, typeAction, ressourceType, succes)) + .sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction())) + .skip((long) page * pageSize) + .limit(pageSize) + .collect(Collectors.toList()); + } + + /** + * Applique les filtres de recherche à un log. + */ + private boolean applyFilters(AuditLogDTO log, String acteurUsername, LocalDateTime dateDebut, + LocalDateTime dateFin, TypeActionAudit typeAction, + String ressourceType, Boolean succes) { + if (acteurUsername != null && !"*".equals(acteurUsername) && + !acteurUsername.equals(log.getActeurUsername())) { + return false; + } + + if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { + return false; + } + + if (dateFin != null && log.getDateAction().isAfter(dateFin)) { + return false; + } + + if (typeAction != null && !typeAction.equals(log.getTypeAction())) { + return false; + } + + if (ressourceType != null && !ressourceType.equals(log.getRessourceType())) { + return false; + } + + if (succes != null && succes != log.isSuccessful()) { + return false; + } + + return true; + } + + /** + * Nettoie les entrées les plus anciennes du cache. + */ + private void cleanOldestCacheEntries() { + int toRemove = auditLogsCache.size() - (cacheSize * 90 / 100); // Garder 90% + + if (toRemove > 0) { + List oldestKeys = auditLogsCache.entrySet().stream() + .sorted((a, b) -> a.getValue().getDateAction().compareTo(b.getValue().getDateAction())) + .limit(toRemove) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + oldestKeys.forEach(auditLogsCache::remove); + log.debug("Nettoyé {} entrées du cache d'audit", oldestKeys.size()); + } + } + + // Méthodes helpers (statistiques) + private Map getActionStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) { + if (logToDatabase) { + List entities = AuditLogEntity.findByPeriod(dateDebut, dateFin); + return entities.stream() + .collect(Collectors.groupingBy(AuditLogEntity::getAction, Collectors.counting())); + } + + return auditLogsCache.values().stream() .filter(log -> { - if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { - return false; - } - if (dateFin != null && log.getDateAction().isAfter(dateFin)) { - return false; - } + if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) return false; + if (dateFin != null && log.getDateAction().isAfter(dateFin)) return false; return true; }) - .collect(Collectors.groupingBy( - AuditLogDTO::getTypeAction, - Collectors.counting() - )); + .collect(Collectors.groupingBy(AuditLogDTO::getTypeAction, Collectors.counting())); } private Map getUserActivityStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) { - log.debug("Calcul des statistiques d'activité utilisateurs entre {} et {}", dateDebut, dateFin); + if (logToDatabase) { + List entities = AuditLogEntity.findByPeriod(dateDebut, dateFin); + return entities.stream() + .collect(Collectors.groupingBy(AuditLogEntity::getAuteurAction, Collectors.counting())); + } - return auditLogs.values().stream() + return auditLogsCache.values().stream() .filter(log -> { - if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { - return false; - } - if (dateFin != null && log.getDateAction().isAfter(dateFin)) { - return false; - } + if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) return false; + if (dateFin != null && log.getDateAction().isAfter(dateFin)) return false; return true; }) - .collect(Collectors.groupingBy( - AuditLogDTO::getActeurUsername, - Collectors.counting() - )); + .collect(Collectors.groupingBy(AuditLogDTO::getActeurUsername, Collectors.counting())); } private long getFailureCount(LocalDateTime dateDebut, LocalDateTime dateFin) { - log.debug("Comptage des échecs entre {} et {}", dateDebut, dateFin); + if (logToDatabase) { + return AuditLogEntity.findByPeriod(dateDebut, dateFin).stream() + .filter(e -> !e.getSuccess()) + .count(); + } - return auditLogs.values().stream() + return auditLogsCache.values().stream() + .filter(log -> !log.isSuccessful()) .filter(log -> { - if (log.isSuccessful()) { - return false; // On ne compte que les échecs - } - if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { - return false; - } - if (dateFin != null && log.getDateAction().isAfter(dateFin)) { - return false; - } + if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) return false; + if (dateFin != null && log.getDateAction().isAfter(dateFin)) return false; return true; }) .count(); } private long getSuccessCount(LocalDateTime dateDebut, LocalDateTime dateFin) { - log.debug("Comptage des succès entre {} et {}", dateDebut, dateFin); + if (logToDatabase) { + return AuditLogEntity.findByPeriod(dateDebut, dateFin).stream() + .filter(AuditLogEntity::getSuccess) + .count(); + } - return auditLogs.values().stream() + return auditLogsCache.values().stream() + .filter(AuditLogDTO::isSuccessful) .filter(log -> { - if (!log.isSuccessful()) { - return false; // On ne compte que les succès - } - if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { - return false; - } - if (dateFin != null && log.getDateAction().isAfter(dateFin)) { - return false; - } + if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) return false; + if (dateFin != null && log.getDateAction().isAfter(dateFin)) return false; return true; }) .count(); @@ -401,56 +543,68 @@ public class AuditServiceImpl implements AuditService { log.info("Export CSV des logs d'audit entre {} et {}", dateDebut, dateFin); List csvLines = new ArrayList<>(); - - // En-tête CSV csvLines.add("ID,Date Action,Acteur,Type Action,Ressource Type,Ressource ID,Succès,Adresse IP,Détails,Message Erreur"); - // Données - auditLogs.values().stream() - .filter(log -> { - if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { - return false; - } - if (dateFin != null && log.getDateAction().isAfter(dateFin)) { - return false; - } - return true; - }) - .sorted((a, b) -> a.getDateAction().compareTo(b.getDateAction())) - .forEach(log -> { - String csvLine = String.format("%s,%s,%s,%s,%s,%s,%s,%s,\"%s\",\"%s\"", - log.getId(), - log.getDateAction(), - log.getActeurUsername(), - log.getTypeAction(), - log.getRessourceType(), - log.getRessourceId(), - log.isSuccessful(), - log.getIpAddress() != null ? log.getIpAddress() : "", - log.getDescription() != null ? log.getDescription().replace("\"", "\"\"") : "", - log.getErrorMessage() != null ? log.getErrorMessage().replace("\"", "\"\"") : "" - ); - csvLines.add(csvLine); - }); + List logs; + if (logToDatabase) { + List entities = AuditLogEntity.findByPeriod(dateDebut, dateFin); + logs = auditLogMapper.toDTOList(entities); + } else { + logs = auditLogsCache.values().stream() + .filter(log -> { + if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) return false; + if (dateFin != null && log.getDateAction().isAfter(dateFin)) return false; + return true; + }) + .sorted((a, b) -> a.getDateAction().compareTo(b.getDateAction())) + .collect(Collectors.toList()); + } + + logs.forEach(log -> { + String csvLine = String.format("%s,%s,%s,%s,%s,%s,%s,%s,\"%s\",\"%s\"", + log.getId(), + log.getDateAction(), + log.getActeurUsername(), + log.getTypeAction(), + log.getRessourceType(), + log.getRessourceId(), + log.isSuccessful(), + log.getIpAddress() != null ? log.getIpAddress() : "", + log.getDescription() != null ? log.getDescription().replace("\"", "\"\"") : "", + log.getErrorMessage() != null ? log.getErrorMessage().replace("\"", "\"\"") : "" + ); + csvLines.add(csvLine); + }); log.info("Export CSV terminé: {} lignes", csvLines.size() - 1); return csvLines; } - // ==================== Méthodes utilitaires ==================== + // ==================== MÉTHODES UTILITAIRES ==================== /** - * Retourne le nombre total de logs en mémoire + * Retourne le nombre total de logs (cache + DB). */ public long getTotalCount() { - return auditLogs.size(); + if (logToDatabase) { + return AuditLogEntity.count(); + } + return auditLogsCache.size(); } /** - * Vide tous les logs (ATTENTION: à utiliser uniquement en développement) + * Vide tous les logs (ATTENTION: à utiliser uniquement en développement). */ + @Transactional public void clearAll() { - log.warn("ATTENTION: Suppression de tous les logs d'audit en mémoire"); - auditLogs.clear(); + log.warn("ATTENTION: Suppression de tous les logs d'audit"); + + if (logToDatabase) { + AuditLogEntity.deleteAll(); + log.warn("Supprimé tous les logs de la base de données"); + } + + auditLogsCache.clear(); + log.warn("Vidé le cache mémoire"); } } diff --git a/src/main/java/dev/lions/user/manager/service/impl/CsvValidationHelper.java b/src/main/java/dev/lions/user/manager/service/impl/CsvValidationHelper.java new file mode 100644 index 0000000..a0c4ac3 --- /dev/null +++ b/src/main/java/dev/lions/user/manager/service/impl/CsvValidationHelper.java @@ -0,0 +1,176 @@ +package dev.lions.user.manager.service.impl; + +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; + +import java.util.regex.Pattern; + +/** + * Classe utilitaire pour la validation des données CSV lors de l'import d'utilisateurs + * + * @author Lions Development Team + * @version 1.0.0 + * @since 2026-01-02 + */ +@Slf4j +@UtilityClass +public class CsvValidationHelper { + + /** + * Pattern pour valider le format d'email selon RFC 5322 (simplifié) + */ + private static final Pattern EMAIL_PATTERN = Pattern.compile( + "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$" + ); + + /** + * Pattern pour valider le username (alphanumérique, tirets, underscores, points) + */ + private static final Pattern USERNAME_PATTERN = Pattern.compile( + "^[a-zA-Z0-9._-]{2,255}$" + ); + + /** + * Longueur minimale pour un username + */ + private static final int USERNAME_MIN_LENGTH = 2; + + /** + * Longueur maximale pour un username + */ + private static final int USERNAME_MAX_LENGTH = 255; + + /** + * Longueur maximale pour un nom ou prénom + */ + private static final int NAME_MAX_LENGTH = 255; + + /** + * Valide le format d'un email + * + * @param email Email à valider + * @return true si l'email est valide, false sinon + */ + public static boolean isValidEmail(String email) { + if (email == null || email.isBlank()) { + return false; + } + return EMAIL_PATTERN.matcher(email.trim()).matches(); + } + + /** + * Valide un username + * + * @param username Username à valider + * @return Message d'erreur si invalide, null si valide + */ + public static String validateUsername(String username) { + if (username == null || username.isBlank()) { + return "Username obligatoire"; + } + + String trimmed = username.trim(); + + if (trimmed.length() < USERNAME_MIN_LENGTH) { + return String.format("Username trop court (minimum %d caractères)", USERNAME_MIN_LENGTH); + } + + if (trimmed.length() > USERNAME_MAX_LENGTH) { + return String.format("Username trop long (maximum %d caractères)", USERNAME_MAX_LENGTH); + } + + if (!USERNAME_PATTERN.matcher(trimmed).matches()) { + return "Username invalide (autorisé: lettres, chiffres, .-_)"; + } + + return null; // Valide + } + + /** + * Valide un email (peut être vide) + * + * @param email Email à valider + * @return Message d'erreur si invalide, null si valide ou vide + */ + public static String validateEmail(String email) { + if (email == null || email.isBlank()) { + return null; // Email optionnel + } + + if (!isValidEmail(email)) { + return "Format d'email invalide"; + } + + return null; // Valide + } + + /** + * Valide un nom ou prénom + * + * @param name Nom à valider + * @param fieldName Nom du champ pour les messages d'erreur + * @return Message d'erreur si invalide, null si valide + */ + public static String validateName(String name, String fieldName) { + if (name == null || name.isBlank()) { + return null; // Nom optionnel + } + + String trimmed = name.trim(); + + if (trimmed.length() > NAME_MAX_LENGTH) { + return String.format("%s trop long (maximum %d caractères)", fieldName, NAME_MAX_LENGTH); + } + + return null; // Valide + } + + /** + * Valide une valeur boolean + * + * @param value Valeur à valider + * @return Message d'erreur si invalide, null si valide + */ + public static String validateBoolean(String value) { + if (value == null || value.isBlank()) { + return null; // Optionnel, défaut à false + } + + String trimmed = value.trim().toLowerCase(); + if (!trimmed.equals("true") && !trimmed.equals("false") && + !trimmed.equals("1") && !trimmed.equals("0") && + !trimmed.equals("yes") && !trimmed.equals("no")) { + return "Valeur boolean invalide (attendu: true/false, 1/0, yes/no)"; + } + + return null; // Valide + } + + /** + * Convertit une chaîne en boolean + * + * @param value Valeur à convertir + * @return boolean correspondant + */ + public static boolean parseBoolean(String value) { + if (value == null || value.isBlank()) { + return false; + } + + String trimmed = value.trim().toLowerCase(); + return trimmed.equals("true") || trimmed.equals("1") || trimmed.equals("yes"); + } + + /** + * Nettoie une chaîne (trim et null si vide) + * + * @param value Valeur à nettoyer + * @return Valeur nettoyée ou null + */ + public static String clean(String value) { + if (value == null || value.isBlank()) { + return null; + } + return value.trim(); + } +} diff --git a/src/main/java/dev/lions/user/manager/service/impl/UserServiceImpl.java b/src/main/java/dev/lions/user/manager/service/impl/UserServiceImpl.java index 1dd03bf..335e37f 100644 --- a/src/main/java/dev/lions/user/manager/service/impl/UserServiceImpl.java +++ b/src/main/java/dev/lions/user/manager/service/impl/UserServiceImpl.java @@ -608,11 +608,17 @@ public class UserServiceImpl implements UserService { } @Override - public int importUsersFromCSV(@NotBlank String csvContent, @NotBlank String realmName) { + public dev.lions.user.manager.dto.importexport.ImportResultDTO importUsersFromCSV(@NotBlank String csvContent, @NotBlank String realmName) { log.info("Import des utilisateurs depuis CSV pour le realm {}", realmName); + dev.lions.user.manager.dto.importexport.ImportResultDTO result = dev.lions.user.manager.dto.importexport.ImportResultDTO.builder() + .totalLines(0) + .successCount(0) + .errorCount(0) + .errors(new java.util.ArrayList<>()) + .build(); + String[] lines = csvContent.split("\\r?\\n"); - int count = 0; int startIndex = 0; // Skip header if present @@ -620,48 +626,159 @@ public class UserServiceImpl implements UserService { startIndex = 1; } + result.setTotalLines(lines.length - startIndex); + for (int i = startIndex; i < lines.length; i++) { + int lineNumber = i + 1; String line = lines[i].trim(); - if (line.isEmpty()) - continue; + + if (line.isEmpty()) { + continue; // Ignore empty lines + } try { + // Parse CSV line String[] parts = parseCSVLine(line); if (parts.length < 5) { - log.warn("Ligne CSV invalide ignorée (pas assez de colonnes): {}", line); + result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder() + .lineNumber(lineNumber) + .lineContent(line) + .errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.INVALID_FORMAT) + .message("Nombre de colonnes insuffisant (attendu: 5, trouvé: " + parts.length + ")") + .build()); continue; } - String username = parts[0]; - String email = parts[1]; - String firstName = parts[2]; - String lastName = parts[3]; - boolean enabled = Boolean.parseBoolean(parts[4]); + String username = CsvValidationHelper.clean(parts[0]); + String email = CsvValidationHelper.clean(parts[1]); + String firstName = CsvValidationHelper.clean(parts[2]); + String lastName = CsvValidationHelper.clean(parts[3]); + String enabledStr = CsvValidationHelper.clean(parts[4]); - if (username == null || username.isBlank()) { - log.warn("Username manquant à la ligne {}", i + 1); + // Validate username + String usernameError = CsvValidationHelper.validateUsername(username); + if (usernameError != null) { + result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder() + .lineNumber(lineNumber) + .lineContent(line) + .errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.VALIDATION_ERROR) + .field("username") + .message(usernameError) + .build()); continue; } + // Validate email + String emailError = CsvValidationHelper.validateEmail(email); + if (emailError != null) { + result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder() + .lineNumber(lineNumber) + .lineContent(line) + .errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.VALIDATION_ERROR) + .field("email") + .message(emailError) + .build()); + continue; + } + + // Validate firstName + String firstNameError = CsvValidationHelper.validateName(firstName, "Prénom"); + if (firstNameError != null) { + result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder() + .lineNumber(lineNumber) + .lineContent(line) + .errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.VALIDATION_ERROR) + .field("firstName") + .message(firstNameError) + .build()); + continue; + } + + // Validate lastName + String lastNameError = CsvValidationHelper.validateName(lastName, "Nom"); + if (lastNameError != null) { + result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder() + .lineNumber(lineNumber) + .lineContent(line) + .errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.VALIDATION_ERROR) + .field("lastName") + .message(lastNameError) + .build()); + continue; + } + + // Validate enabled + String enabledError = CsvValidationHelper.validateBoolean(enabledStr); + if (enabledError != null) { + result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder() + .lineNumber(lineNumber) + .lineContent(line) + .errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.VALIDATION_ERROR) + .field("enabled") + .message(enabledError) + .build()); + continue; + } + + boolean enabled = CsvValidationHelper.parseBoolean(enabledStr); + + // Check if user already exists + try { + java.util.Optional existingUser = getUserByUsername(username, realmName); + if (existingUser.isPresent()) { + result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder() + .lineNumber(lineNumber) + .lineContent(line) + .errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.DUPLICATE_USER) + .field("username") + .message("Utilisateur déjà existant: " + username) + .build()); + continue; + } + } catch (Exception e) { + // User doesn't exist, continue with creation + } + + // Create user UserDTO userDTO = UserDTO.builder() - .username(username) - .email(email.isBlank() ? null : email) - .prenom(firstName.isBlank() ? null : firstName) - .nom(lastName.isBlank() ? null : lastName) - .enabled(enabled) - .build(); + .username(username) + .email(email) + .prenom(firstName) + .nom(lastName) + .enabled(enabled) + .build(); - createUser(userDTO, realmName); - count++; + try { + createUser(userDTO, realmName); + result.setSuccessCount(result.getSuccessCount() + 1); + log.debug("✅ Utilisateur créé: {} (ligne {})", username, lineNumber); + } catch (Exception e) { + result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder() + .lineNumber(lineNumber) + .lineContent(line) + .errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.CREATION_ERROR) + .message("Erreur lors de la création de l'utilisateur") + .details(e.getMessage()) + .build()); + } } catch (Exception e) { - log.error("Erreur lors de l'import de la ligne {}: {}", i + 1, e.getMessage()); - // Continue with next line + log.error("Erreur inattendue lors du traitement de la ligne {}: {}", lineNumber, e.getMessage(), e); + result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder() + .lineNumber(lineNumber) + .lineContent(line) + .errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.SYSTEM_ERROR) + .message("Erreur système") + .details(e.getMessage()) + .build()); } } - log.info("✅ {} utilisateurs importés avec succès", count); - return count; + // Generate summary message + result.generateMessage(); + log.info(result.getMessage()); + + return result; } private String escape(String data) { diff --git a/src/main/resources/db/migration/V1.0.0__Create_audit_logs_table.sql b/src/main/resources/db/migration/V1.0.0__Create_audit_logs_table.sql new file mode 100644 index 0000000..351acae --- /dev/null +++ b/src/main/resources/db/migration/V1.0.0__Create_audit_logs_table.sql @@ -0,0 +1,175 @@ +-- ============================================================================= +-- Migration Flyway V1.0.0 - Création de la table audit_logs +-- ============================================================================= +-- Description: Création de la table pour la persistance des logs d'audit +-- des actions effectuées sur le système de gestion des utilisateurs +-- +-- Auteur: Lions Development Team +-- Date: 2026-01-02 +-- Version: 1.0.0 +-- ============================================================================= + +-- Création de la table audit_logs +CREATE TABLE IF NOT EXISTS audit_logs ( + -- Clé primaire générée automatiquement + id BIGSERIAL PRIMARY KEY, + + -- Informations sur l'utilisateur concerné + user_id VARCHAR(255), + + -- Type d'action effectuée + action VARCHAR(100) NOT NULL, + + -- Détails de l'action + details TEXT, + + -- Informations sur l'auteur de l'action + auteur_action VARCHAR(255) NOT NULL, + + -- Timestamp de l'action + timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + -- Informations de traçabilité réseau + ip_address VARCHAR(45), + user_agent VARCHAR(500), + + -- Informations multi-tenant + realm_name VARCHAR(255), + + -- Statut de l'action + success BOOLEAN NOT NULL DEFAULT TRUE, + error_message TEXT, + + -- Métadonnées + CONSTRAINT chk_audit_action CHECK (action IN ( + -- Actions utilisateurs + 'CREATION_UTILISATEUR', + 'MODIFICATION_UTILISATEUR', + 'SUPPRESSION_UTILISATEUR', + 'ACTIVATION_UTILISATEUR', + 'DESACTIVATION_UTILISATEUR', + 'VERROUILLAGE_UTILISATEUR', + 'DEVERROUILLAGE_UTILISATEUR', + + -- Actions mot de passe + 'RESET_PASSWORD', + 'CHANGE_PASSWORD', + 'FORCE_PASSWORD_RESET', + + -- Actions sessions + 'LOGOUT_UTILISATEUR', + 'LOGOUT_ALL_SESSIONS', + 'SESSION_EXPIREE', + + -- Actions rôles + 'ATTRIBUTION_ROLE', + 'REVOCATION_ROLE', + 'CREATION_ROLE', + 'MODIFICATION_ROLE', + 'SUPPRESSION_ROLE', + + -- Actions groupes + 'AJOUT_GROUPE', + 'RETRAIT_GROUPE', + + -- Actions realms + 'ATTRIBUTION_REALM', + 'REVOCATION_REALM', + + -- Actions synchronisation + 'SYNC_MANUEL', + 'SYNC_AUTO', + 'SYNC_ERREUR', + + -- Actions import/export + 'EXPORT_CSV', + 'IMPORT_CSV', + + -- Actions système + 'CONNEXION_REUSSIE', + 'CONNEXION_ECHOUEE', + 'TENTATIVE_ACCES_NON_AUTORISE', + 'ERREUR_SYSTEME', + 'CONFIGURATION_MODIFIEE' + )) +); + +-- ============================================================================= +-- INDEX pour optimiser les requêtes +-- ============================================================================= + +-- Index sur user_id pour recherches rapides par utilisateur +CREATE INDEX idx_audit_user_id ON audit_logs(user_id) + WHERE user_id IS NOT NULL; + +-- Index sur action pour filtrer par type d'action +CREATE INDEX idx_audit_action ON audit_logs(action); + +-- Index sur timestamp pour recherches chronologiques et tri +CREATE INDEX idx_audit_timestamp ON audit_logs(timestamp DESC); + +-- Index sur auteur_action pour tracer les actions d'un administrateur +CREATE INDEX idx_audit_auteur ON audit_logs(auteur_action); + +-- Index sur realm_name pour isolation multi-tenant +CREATE INDEX idx_audit_realm ON audit_logs(realm_name) + WHERE realm_name IS NOT NULL; + +-- Index composite pour recherches fréquentes +CREATE INDEX idx_audit_user_timestamp ON audit_logs(user_id, timestamp DESC) + WHERE user_id IS NOT NULL; + +-- Index sur success pour identifier rapidement les échecs +CREATE INDEX idx_audit_failures ON audit_logs(success, timestamp DESC) + WHERE success = FALSE; + +-- ============================================================================= +-- COMMENTAIRES sur les colonnes +-- ============================================================================= + +COMMENT ON TABLE audit_logs IS 'Table de persistance des logs d''audit pour traçabilité complète'; + +COMMENT ON COLUMN audit_logs.id IS 'Identifiant unique auto-incrémenté du log'; +COMMENT ON COLUMN audit_logs.user_id IS 'ID de l''utilisateur concerné par l''action (null pour actions système)'; +COMMENT ON COLUMN audit_logs.action IS 'Type d''action effectuée (enum TypeActionAudit)'; +COMMENT ON COLUMN audit_logs.details IS 'Détails complémentaires sur l''action'; +COMMENT ON COLUMN audit_logs.auteur_action IS 'Identifiant de l''utilisateur ayant effectué l''action'; +COMMENT ON COLUMN audit_logs.timestamp IS 'Date et heure précise de l''action'; +COMMENT ON COLUMN audit_logs.ip_address IS 'Adresse IP du client ayant effectué l''action'; +COMMENT ON COLUMN audit_logs.user_agent IS 'User-Agent du navigateur/client'; +COMMENT ON COLUMN audit_logs.realm_name IS 'Nom du realm Keycloak concerné (multi-tenant)'; +COMMENT ON COLUMN audit_logs.success IS 'Indique si l''action a réussi (true) ou échoué (false)'; +COMMENT ON COLUMN audit_logs.error_message IS 'Message d''erreur en cas d''échec (null si success=true)'; + +-- ============================================================================= +-- POLITIQUE DE RÉTENTION (optionnel - à activer selon besoins) +-- ============================================================================= + +-- Fonction pour nettoyer automatiquement les vieux logs +-- Décommenter et adapter la période de rétention selon les besoins + +/* +CREATE OR REPLACE FUNCTION cleanup_old_audit_logs() RETURNS void AS $$ +BEGIN + -- Supprime les logs de plus de 365 jours (configurable) + DELETE FROM audit_logs + WHERE timestamp < CURRENT_TIMESTAMP - INTERVAL '365 days'; + + RAISE NOTICE 'Logs d''audit plus anciens que 365 jours supprimés'; +END; +$$ LANGUAGE plpgsql; + +-- Créer un job CRON (nécessite extension pg_cron) +-- SELECT cron.schedule('cleanup-audit-logs', '0 2 * * 0', 'SELECT cleanup_old_audit_logs()'); +*/ + +-- ============================================================================= +-- GRANTS (à adapter selon les rôles de votre base de données) +-- ============================================================================= + +-- GRANT SELECT, INSERT ON audit_logs TO lions_app_user; +-- GRANT USAGE, SELECT ON SEQUENCE audit_logs_id_seq TO lions_app_user; + +-- ============================================================================= +-- FIN DE LA MIGRATION +-- ============================================================================= diff --git a/src/test/java/dev/lions/user/manager/service/impl/UserServiceImplTest.java b/src/test/java/dev/lions/user/manager/service/impl/UserServiceImplTest.java index e315dca..f089927 100644 --- a/src/test/java/dev/lions/user/manager/service/impl/UserServiceImplTest.java +++ b/src/test/java/dev/lions/user/manager/service/impl/UserServiceImplTest.java @@ -220,12 +220,15 @@ class UserServiceImplTest { lenient().doNothing().when(userResource) .resetPassword(any(org.keycloak.representations.idm.CredentialRepresentation.class)); - String csvContent = "username,email,firstName,lastName,enabled\n" + - "imported,imp@test.com,Imp,Orter,true"; + String csvContent = "username,prenom,nom,email\n" + + "imported,Imp,Orter,imp@test.com"; - int count = userService.importUsersFromCSV(csvContent, REALM); + dev.lions.user.manager.dto.importexport.ImportResultDTO result = + userService.importUsersFromCSV(csvContent, REALM); - assertEquals(1, count); + assertNotNull(result); + assertEquals(1, result.getSuccessCount()); + assertEquals(0, result.getErrorCount()); verify(usersResource, atLeastOnce()).create(argThat(u -> u.getUsername().equals("imported"))); } }