diff --git a/.gitignore b/.gitignore index 6f9eb04..6c4ab0b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,94 +1,94 @@ -# ============================================ -# Quarkus Backend .gitignore -# ============================================ - -# Maven -target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties -.mvn/wrapper/maven-wrapper.jar - -# Quarkus -.quarkus/ -quarkus.log - -# IDE -.idea/ -*.iml -*.ipr -*.iws -.vscode/ -.classpath -.project -.settings/ -.factorypath -.apt_generated/ -.apt_generated_tests/ - -# Eclipse -.metadata -bin/ -tmp/ -*.tmp -*.bak -*.swp -*~.nib -local.properties -.loadpath -.recommenders - -# IntelliJ -out/ -.idea_modules/ - -# Logs -*.log -*.log.* -logs/ - -# OS -.DS_Store -Thumbs.db -*.pid - -# Java -*.class -*.jar -!.mvn/wrapper/maven-wrapper.jar -*.war -*.ear -hs_err_pid* - -# Application secrets -*.jks -*.p12 -*.pem -*.key -*-secret.properties -application-local.properties -application-dev-override.properties -.env - -# Docker -.dockerignore -docker-compose.override.yml - -# Database -*.db -*.sqlite -*.h2.db - -# Test -test-output/ -.gradle/ -build/ - -# Temporary -.tmp/ -temp/ +# ============================================ +# Quarkus Backend .gitignore +# ============================================ + +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +# Quarkus +.quarkus/ +quarkus.log + +# IDE +.idea/ +*.iml +*.ipr +*.iws +.vscode/ +.classpath +.project +.settings/ +.factorypath +.apt_generated/ +.apt_generated_tests/ + +# Eclipse +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.loadpath +.recommenders + +# IntelliJ +out/ +.idea_modules/ + +# Logs +*.log +*.log.* +logs/ + +# OS +.DS_Store +Thumbs.db +*.pid + +# Java +*.class +*.jar +!.mvn/wrapper/maven-wrapper.jar +*.war +*.ear +hs_err_pid* + +# Application secrets +*.jks +*.p12 +*.pem +*.key +*-secret.properties +application-local.properties +application-dev-override.properties +.env + +# Docker +.dockerignore +docker-compose.override.yml + +# Database +*.db +*.sqlite +*.h2.db + +# Test +test-output/ +.gradle/ +build/ + +# Temporary +.tmp/ +temp/ diff --git a/Dockerfile.prod b/Dockerfile.prod index 9c19cd2..836511b 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -1,91 +1,91 @@ -#### -# Dockerfile de production pour Lions User Manager Server (Backend) -# Multi-stage build optimisé avec sécurité renforcée -# Basé sur la structure de btpxpress-server -#### - -## Stage 1 : Build avec Maven -FROM maven:3.9.6-eclipse-temurin-17 AS builder - -WORKDIR /app - -# Copier pom.xml et télécharger les dépendances (cache Docker) -COPY pom.xml . -RUN mvn dependency:go-offline -B - -# Copier le code source -COPY src ./src - -# Construire l'application avec profil production -RUN mvn clean package -DskipTests -B -Dquarkus.profile=prod -Dquarkus.http.root-path=/lions-user-manager - -## Stage 2 : Image de production optimisée -FROM registry.access.redhat.com/ubi8/openjdk-17:1.18 - -ENV LANGUAGE='en_US:en' - -# Configuration des variables d'environnement pour production -ENV QUARKUS_PROFILE=prod -ENV DB_HOST=postgresql-service.postgresql.svc.cluster.local -ENV DB_PORT=5432 -ENV DB_NAME=lions_user_manager -ENV DB_USERNAME=lionsuser -ENV DB_PASSWORD=LionsUser2025! -ENV SERVER_PORT=8080 - -# Configuration Keycloak/OIDC (production) -ENV QUARKUS_OIDC_AUTH_SERVER_URL=https://security.lions.dev/realms/lions-user-manager -ENV QUARKUS_OIDC_CLIENT_ID=lions-user-manager -ENV KEYCLOAK_CLIENT_SECRET=oGCivOdgbNHroNsHS1MRBZJXX8VpRGk3 -ENV QUARKUS_OIDC_TLS_VERIFICATION=required - -# Configuration Keycloak Admin Client -ENV LIONS_KEYCLOAK_SERVER_URL=https://security.lions.dev -ENV KEYCLOAK_SERVER_URL=https://security.lions.dev -ENV LIONS_KEYCLOAK_ADMIN_REALM=master -ENV LIONS_KEYCLOAK_ADMIN_CLIENT_ID=admin-cli -ENV LIONS_KEYCLOAK_ADMIN_USERNAME=admin -ENV KEYCLOAK_ADMIN_USERNAME=admin -ENV LIONS_KEYCLOAK_ADMIN_PASSWORD=KeycloakAdmin2025! -ENV KEYCLOAK_ADMIN_PASSWORD=KeycloakAdmin2025! - -# Configuration CORS pour production -ENV CORS_ORIGINS=https://users.lions.dev,https://btpxpress.lions.dev,https://admin.lions.dev -ENV QUARKUS_HTTP_CORS_ALLOW_CREDENTIALS=true - -# Installer curl pour les health checks -USER root -RUN microdnf install curl -y && microdnf clean all -RUN mkdir -p /app/logs && chown -R 185:185 /app/logs -USER 185 - -# Copier l'application depuis le builder -COPY --from=builder --chown=185 /app/target/quarkus-app/lib/ /deployments/lib/ -COPY --from=builder --chown=185 /app/target/quarkus-app/*.jar /deployments/ -COPY --from=builder --chown=185 /app/target/quarkus-app/app/ /deployments/app/ -COPY --from=builder --chown=185 /app/target/quarkus-app/quarkus/ /deployments/quarkus/ - -# Exposer le port -EXPOSE 8080 - -# Variables JVM optimisées pour production avec sécurité -ENV JAVA_OPTS="-Xmx1g -Xms512m \ - -XX:+UseG1GC \ - -XX:MaxGCPauseMillis=200 \ - -XX:+UseStringDeduplication \ - -XX:+ParallelRefProcEnabled \ - -XX:+HeapDumpOnOutOfMemoryError \ - -XX:HeapDumpPath=/app/logs/heapdump.hprof \ - -Djava.security.egd=file:/dev/./urandom \ - -Djava.awt.headless=true \ - -Dfile.encoding=UTF-8 \ - -Djava.util.logging.manager=org.jboss.logmanager.LogManager \ - -Dquarkus.profile=${QUARKUS_PROFILE}" - -# Point d'entrée avec profil production -ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /deployments/quarkus-run.jar"] - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ - CMD curl -f http://localhost:8080/q/health/ready || exit 1 - +#### +# Dockerfile de production pour Lions User Manager Server (Backend) +# Multi-stage build optimisé avec sécurité renforcée +# Basé sur la structure de btpxpress-server +#### + +## Stage 1 : Build avec Maven +FROM maven:3.9.6-eclipse-temurin-17 AS builder + +WORKDIR /app + +# Copier pom.xml et télécharger les dépendances (cache Docker) +COPY pom.xml . +RUN mvn dependency:go-offline -B + +# Copier le code source +COPY src ./src + +# Construire l'application avec profil production +RUN mvn clean package -DskipTests -B -Dquarkus.profile=prod -Dquarkus.http.root-path=/lions-user-manager + +## Stage 2 : Image de production optimisée +FROM registry.access.redhat.com/ubi8/openjdk-17:1.18 + +ENV LANGUAGE='en_US:en' + +# Configuration des variables d'environnement pour production +ENV QUARKUS_PROFILE=prod +ENV DB_HOST=postgresql-service.postgresql.svc.cluster.local +ENV DB_PORT=5432 +ENV DB_NAME=lions_user_manager +ENV DB_USERNAME=lionsuser +ENV DB_PASSWORD=LionsUser2025! +ENV SERVER_PORT=8080 + +# Configuration Keycloak/OIDC (production) +ENV QUARKUS_OIDC_AUTH_SERVER_URL=https://security.lions.dev/realms/lions-user-manager +ENV QUARKUS_OIDC_CLIENT_ID=lions-user-manager +ENV KEYCLOAK_CLIENT_SECRET=oGCivOdgbNHroNsHS1MRBZJXX8VpRGk3 +ENV QUARKUS_OIDC_TLS_VERIFICATION=required + +# Configuration Keycloak Admin Client +ENV LIONS_KEYCLOAK_SERVER_URL=https://security.lions.dev +ENV KEYCLOAK_SERVER_URL=https://security.lions.dev +ENV LIONS_KEYCLOAK_ADMIN_REALM=master +ENV LIONS_KEYCLOAK_ADMIN_CLIENT_ID=admin-cli +ENV LIONS_KEYCLOAK_ADMIN_USERNAME=admin +ENV KEYCLOAK_ADMIN_USERNAME=admin +ENV LIONS_KEYCLOAK_ADMIN_PASSWORD=KeycloakAdmin2025! +ENV KEYCLOAK_ADMIN_PASSWORD=KeycloakAdmin2025! + +# Configuration CORS pour production +ENV CORS_ORIGINS=https://users.lions.dev,https://btpxpress.lions.dev,https://admin.lions.dev +ENV QUARKUS_HTTP_CORS_ALLOW_CREDENTIALS=true + +# Installer curl pour les health checks +USER root +RUN microdnf install curl -y && microdnf clean all +RUN mkdir -p /app/logs && chown -R 185:185 /app/logs +USER 185 + +# Copier l'application depuis le builder +COPY --from=builder --chown=185 /app/target/quarkus-app/lib/ /deployments/lib/ +COPY --from=builder --chown=185 /app/target/quarkus-app/*.jar /deployments/ +COPY --from=builder --chown=185 /app/target/quarkus-app/app/ /deployments/app/ +COPY --from=builder --chown=185 /app/target/quarkus-app/quarkus/ /deployments/quarkus/ + +# Exposer le port +EXPOSE 8080 + +# Variables JVM optimisées pour production avec sécurité +ENV JAVA_OPTS="-Xmx1g -Xms512m \ + -XX:+UseG1GC \ + -XX:MaxGCPauseMillis=200 \ + -XX:+UseStringDeduplication \ + -XX:+ParallelRefProcEnabled \ + -XX:+HeapDumpOnOutOfMemoryError \ + -XX:HeapDumpPath=/app/logs/heapdump.hprof \ + -Djava.security.egd=file:/dev/./urandom \ + -Djava.awt.headless=true \ + -Dfile.encoding=UTF-8 \ + -Djava.util.logging.manager=org.jboss.logmanager.LogManager \ + -Dquarkus.profile=${QUARKUS_PROFILE}" + +# Point d'entrée avec profil production +ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /deployments/quarkus-run.jar"] + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD curl -f http://localhost:8080/q/health/ready || exit 1 + diff --git a/README.md b/README.md index 6e924d0..2ca2960 100644 --- a/README.md +++ b/README.md @@ -1,162 +1,162 @@ -# lions-user-manager-server-impl-quarkus - -> Backend REST Quarkus — Gestion des utilisateurs, rôles et realms via Keycloak Admin API - -## Dépôt Git - -`https://git.lions.dev/lionsdev/lions-user-manager-server-impl-quarkus` - ---- - -## Responsabilités - -- Exposition d'une API REST sécurisée (OIDC) -- Gestion CRUD des utilisateurs et rôles Keycloak via Admin API -- Synchronisation des données entre Keycloak et PostgreSQL -- Audit complet des actions (traçabilité en base) -- Export / import CSV -- Health checks, métriques Prometheus, Swagger UI - ---- - -## API REST - -| Ressource | Path | Description | -|-----------|------|-------------| -| Utilisateurs | `GET/POST/PUT/DELETE /api/users` | CRUD utilisateurs | -| Export CSV | `GET /api/users/export/csv` | Export utilisateurs au format CSV | -| Import CSV | `POST /api/users/import/csv` | Import en masse | -| Rôles | `GET/POST/DELETE /api/roles` | Gestion des rôles par realm | -| Audit | `GET /api/audit` | Consultation des logs | -| Analytics | `GET /api/audit/analytics/*` | Statistiques d'activité | -| Sync | `POST /api/sync` | Déclencher une synchronisation | -| Sync status | `GET /api/sync/status` | Dernier statut de sync | -| Sync check | `GET /api/sync/consistency` | Vérification de cohérence | -| Realms | `GET /api/realms` | Liste des realms autorisés | -| Assignments | `GET/POST/DELETE /api/realm-assignments` | Assignation realm/utilisateur | - -Documentation complète : `https://api.lions.dev/lions-user-manager/q/swagger-ui` - ---- - -## Stack - -| Composant | Technologie | -|-----------|-------------| -| Framework | Quarkus 3.17.8 | -| API | Quarkus REST (RESTEasy Reactive) + Jackson | -| Auth | `quarkus-oidc` (Keycloak) | -| Admin | `quarkus-keycloak-admin-rest-client` | -| ORM | Hibernate ORM Panache | -| Base de données | PostgreSQL 15 | -| Migration | Flyway | -| Health | SmallRye Health | -| Métriques | Micrometer + Prometheus | -| Docs | SmallRye OpenAPI (Swagger UI) | - ---- - -## Développement local - -### Prérequis - -- Java 17+, Maven 3.9+ -- Keycloak sur `localhost:8180` -- PostgreSQL sur `localhost:5432` (DB : `lions_user_manager`) - -### Démarrage - -```bash -mvn quarkus:dev -``` - -Swagger UI disponible sur : `http://localhost:8081/q/swagger-ui` - -### Configuration dev - -Fichier : `src/main/resources/application-dev.properties` - -```properties -quarkus.http.port=8081 -quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/lions_user_manager -quarkus.oidc.auth-server-url=http://localhost:8180/realms/lions-user-manager -``` - ---- - -## Configuration production - -Fichier : `src/main/resources/application-prod.properties` - -Toutes les valeurs sensibles sont passées via variables d'environnement : - -| Variable | Description | -|----------|-------------| -| `DB_HOST` | Hôte PostgreSQL | -| `DB_PORT` | Port (défaut : 5432) | -| `DB_NAME` | Nom de la base (défaut : lions_user_manager) | -| `DB_USERNAME` | Utilisateur PostgreSQL | -| `DB_PASSWORD` | Mot de passe PostgreSQL | -| `KEYCLOAK_AUTH_SERVER_URL` | URL du realm Keycloak | -| `KEYCLOAK_SERVER_URL` | URL base Keycloak | -| `KEYCLOAK_ADMIN_USERNAME` | Admin Keycloak | -| `KEYCLOAK_ADMIN_PASSWORD` | Mot de passe admin Keycloak | -| `CORS_ORIGINS` | Origines CORS autorisées | - ---- - -## Build - -```bash -# Build standard (développement) -mvn clean package -DskipTests - -# Build production -mvn clean package -P prod -DskipTests -``` - ---- - -## Déploiement (lionsctl) - -```bash -lionsctl pipeline \ - -u https://git.lions.dev/lionsdev/lions-user-manager-server-impl-quarkus \ - -b main -j 17 -e production -c k1 -p prod -``` - -**Pipeline** : clone → `mvn package -P prod` → `docker build -f Dockerfile.prod` → push `registry.lions.dev` → `kubectl apply` → health check - -**URL prod** : `https://api.lions.dev/lions-user-manager` - ---- - -## Tests - -```bash -# Unitaires -mvn test - -# Intégration (Testcontainers) -mvn verify -``` - ---- - -## Structure - -``` -src/main/java/dev/lions/user/manager/server/impl/ -├── entity/ # Entités JPA (AuditLogEntity, SyncHistoryEntity, ...) -├── mapper/ # MapStruct mappers -├── repository/ # Repositories Panache -├── resource/ # Resources JAX-RS (UserResource, RoleResource, ...) -└── service/ - └── impl/ # Implémentations des services -``` - ---- - -## Licence - -Propriétaire — Lions Dev © 2025 +# lions-user-manager-server-impl-quarkus + +> Backend REST Quarkus — Gestion des utilisateurs, rôles et realms via Keycloak Admin API + +## Dépôt Git + +`https://git.lions.dev/lionsdev/lions-user-manager-server-impl-quarkus` + +--- + +## Responsabilités + +- Exposition d'une API REST sécurisée (OIDC) +- Gestion CRUD des utilisateurs et rôles Keycloak via Admin API +- Synchronisation des données entre Keycloak et PostgreSQL +- Audit complet des actions (traçabilité en base) +- Export / import CSV +- Health checks, métriques Prometheus, Swagger UI + +--- + +## API REST + +| Ressource | Path | Description | +|-----------|------|-------------| +| Utilisateurs | `GET/POST/PUT/DELETE /api/users` | CRUD utilisateurs | +| Export CSV | `GET /api/users/export/csv` | Export utilisateurs au format CSV | +| Import CSV | `POST /api/users/import/csv` | Import en masse | +| Rôles | `GET/POST/DELETE /api/roles` | Gestion des rôles par realm | +| Audit | `GET /api/audit` | Consultation des logs | +| Analytics | `GET /api/audit/analytics/*` | Statistiques d'activité | +| Sync | `POST /api/sync` | Déclencher une synchronisation | +| Sync status | `GET /api/sync/status` | Dernier statut de sync | +| Sync check | `GET /api/sync/consistency` | Vérification de cohérence | +| Realms | `GET /api/realms` | Liste des realms autorisés | +| Assignments | `GET/POST/DELETE /api/realm-assignments` | Assignation realm/utilisateur | + +Documentation complète : `https://api.lions.dev/lions-user-manager/q/swagger-ui` + +--- + +## Stack + +| Composant | Technologie | +|-----------|-------------| +| Framework | Quarkus 3.17.8 | +| API | Quarkus REST (RESTEasy Reactive) + Jackson | +| Auth | `quarkus-oidc` (Keycloak) | +| Admin | `quarkus-keycloak-admin-rest-client` | +| ORM | Hibernate ORM Panache | +| Base de données | PostgreSQL 15 | +| Migration | Flyway | +| Health | SmallRye Health | +| Métriques | Micrometer + Prometheus | +| Docs | SmallRye OpenAPI (Swagger UI) | + +--- + +## Développement local + +### Prérequis + +- Java 17+, Maven 3.9+ +- Keycloak sur `localhost:8180` +- PostgreSQL sur `localhost:5432` (DB : `lions_user_manager`) + +### Démarrage + +```bash +mvn quarkus:dev +``` + +Swagger UI disponible sur : `http://localhost:8081/q/swagger-ui` + +### Configuration dev + +Fichier : `src/main/resources/application-dev.properties` + +```properties +quarkus.http.port=8081 +quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/lions_user_manager +quarkus.oidc.auth-server-url=http://localhost:8180/realms/lions-user-manager +``` + +--- + +## Configuration production + +Fichier : `src/main/resources/application-prod.properties` + +Toutes les valeurs sensibles sont passées via variables d'environnement : + +| Variable | Description | +|----------|-------------| +| `DB_HOST` | Hôte PostgreSQL | +| `DB_PORT` | Port (défaut : 5432) | +| `DB_NAME` | Nom de la base (défaut : lions_user_manager) | +| `DB_USERNAME` | Utilisateur PostgreSQL | +| `DB_PASSWORD` | Mot de passe PostgreSQL | +| `KEYCLOAK_AUTH_SERVER_URL` | URL du realm Keycloak | +| `KEYCLOAK_SERVER_URL` | URL base Keycloak | +| `KEYCLOAK_ADMIN_USERNAME` | Admin Keycloak | +| `KEYCLOAK_ADMIN_PASSWORD` | Mot de passe admin Keycloak | +| `CORS_ORIGINS` | Origines CORS autorisées | + +--- + +## Build + +```bash +# Build standard (développement) +mvn clean package -DskipTests + +# Build production +mvn clean package -P prod -DskipTests +``` + +--- + +## Déploiement (lionsctl) + +```bash +lionsctl pipeline \ + -u https://git.lions.dev/lionsdev/lions-user-manager-server-impl-quarkus \ + -b main -j 17 -e production -c k1 -p prod +``` + +**Pipeline** : clone → `mvn package -P prod` → `docker build -f Dockerfile.prod` → push `registry.lions.dev` → `kubectl apply` → health check + +**URL prod** : `https://api.lions.dev/lions-user-manager` + +--- + +## Tests + +```bash +# Unitaires +mvn test + +# Intégration (Testcontainers) +mvn verify +``` + +--- + +## Structure + +``` +src/main/java/dev/lions/user/manager/server/impl/ +├── entity/ # Entités JPA (AuditLogEntity, SyncHistoryEntity, ...) +├── mapper/ # MapStruct mappers +├── repository/ # Repositories Panache +├── resource/ # Resources JAX-RS (UserResource, RoleResource, ...) +└── service/ + └── impl/ # Implémentations des services +``` + +--- + +## Licence + +Propriétaire — Lions Dev © 2025 diff --git a/lombok.config b/lombok.config index 2b45606..b4edc26 100644 --- a/lombok.config +++ b/lombok.config @@ -1,6 +1,6 @@ -# This file configures Lombok for the project -# See https://projectlombok.org/features/configuration - -# Add @Generated annotation to all generated code -# This allows JaCoCo to automatically exclude Lombok-generated code from coverage -lombok.addLombokGeneratedAnnotation = true +# This file configures Lombok for the project +# See https://projectlombok.org/features/configuration + +# Add @Generated annotation to all generated code +# This allows JaCoCo to automatically exclude Lombok-generated code from coverage +lombok.addLombokGeneratedAnnotation = true diff --git a/pom.xml b/pom.xml index c515dfb..6c9a0cc 100644 --- a/pom.xml +++ b/pom.xml @@ -4,11 +4,58 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - - dev.lions.user.manager - lions-user-manager-parent - 1.0.0 - + dev.lions.user.manager + 1.1.0 + + + 21 + ${java.version} + ${java.version} + UTF-8 + 3.27.3 + 1.18.38 + 1.6.3 + + 1.21.4 + 3.4.2 + + + + + + io.quarkus.platform + quarkus-bom + ${quarkus.platform.version} + pom + import + + + org.testcontainers + testcontainers-bom + ${testcontainers.version} + pom + import + + + dev.lions.user.manager + lions-user-manager-server-api + ${project.version} + + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + lions-user-manager-server-impl-quarkus jar diff --git a/script/docker/.env.example b/script/docker/.env.example index ca3d012..dbcd743 100644 --- a/script/docker/.env.example +++ b/script/docker/.env.example @@ -1,17 +1,17 @@ -# Base de données -DB_NAME=lions_user_manager -DB_USER=lions -DB_PASSWORD=lions -DB_PORT=5432 - -# Keycloak (Docker Compose) -KC_ADMIN=admin -KC_ADMIN_PASSWORD=admin -KC_PORT=8180 - -# Keycloak Admin Client (application-dev.properties) -KEYCLOAK_ADMIN_USERNAME=admin -KEYCLOAK_ADMIN_PASSWORD=admin - -# Serveur -SERVER_PORT=8080 +# Base de données +DB_NAME=lions_user_manager +DB_USER=lions +DB_PASSWORD=lions +DB_PORT=5432 + +# Keycloak (Docker Compose) +KC_ADMIN=admin +KC_ADMIN_PASSWORD=admin +KC_PORT=8180 + +# Keycloak Admin Client (application-dev.properties) +KEYCLOAK_ADMIN_USERNAME=admin +KEYCLOAK_ADMIN_PASSWORD=admin + +# Serveur +SERVER_PORT=8080 diff --git a/script/docker/dependencies-docker-compose.yml b/script/docker/dependencies-docker-compose.yml index ff7dbf6..79d1183 100644 --- a/script/docker/dependencies-docker-compose.yml +++ b/script/docker/dependencies-docker-compose.yml @@ -1,35 +1,35 @@ -services: - postgres: - image: postgres:15 - environment: - POSTGRES_DB: ${DB_NAME:-lions_user_manager} - POSTGRES_USER: ${DB_USER:-lions} - POSTGRES_PASSWORD: ${DB_PASSWORD:-lions} - ports: - - "${DB_PORT:-5432}:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-lions} -d ${DB_NAME:-lions_user_manager}"] - interval: 5s - timeout: 5s - retries: 5 - - keycloak: - image: quay.io/keycloak/keycloak:26.3.3 - command: start-dev - environment: - KC_DB: postgres - KC_DB_URL: jdbc:postgresql://postgres:5432/${DB_NAME:-lions_user_manager} - KC_DB_USERNAME: ${DB_USER:-lions} - KC_DB_PASSWORD: ${DB_PASSWORD:-lions} - KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_ADMIN:-admin} - KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD:-admin} - ports: - - "${KC_PORT:-8180}:8080" - depends_on: - postgres: - condition: service_healthy - -volumes: - postgres_data: +services: + postgres: + image: postgres:15 + environment: + POSTGRES_DB: ${DB_NAME:-lions_user_manager} + POSTGRES_USER: ${DB_USER:-lions} + POSTGRES_PASSWORD: ${DB_PASSWORD:-lions} + ports: + - "${DB_PORT:-5432}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-lions} -d ${DB_NAME:-lions_user_manager}"] + interval: 5s + timeout: 5s + retries: 5 + + keycloak: + image: quay.io/keycloak/keycloak:26.3.3 + command: start-dev + environment: + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://postgres:5432/${DB_NAME:-lions_user_manager} + KC_DB_USERNAME: ${DB_USER:-lions} + KC_DB_PASSWORD: ${DB_PASSWORD:-lions} + KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_ADMIN:-admin} + KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD:-admin} + ports: + - "${KC_PORT:-8180}:8080" + depends_on: + postgres: + condition: service_healthy + +volumes: + postgres_data: diff --git a/script/docker/docker-compose.yml b/script/docker/docker-compose.yml index fdc0a14..6c88936 100644 --- a/script/docker/docker-compose.yml +++ b/script/docker/docker-compose.yml @@ -1,52 +1,52 @@ -services: - postgres: - image: postgres:15 - environment: - POSTGRES_DB: ${DB_NAME:-lions_user_manager} - POSTGRES_USER: ${DB_USER:-lions} - POSTGRES_PASSWORD: ${DB_PASSWORD:-lions} - ports: - - "${DB_PORT:-5432}:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-lions} -d ${DB_NAME:-lions_user_manager}"] - interval: 5s - timeout: 5s - retries: 5 - - keycloak: - image: quay.io/keycloak/keycloak:26.3.3 - command: start-dev - environment: - KC_DB: postgres - KC_DB_URL: jdbc:postgresql://postgres:5432/${DB_NAME:-lions_user_manager} - KC_DB_USERNAME: ${DB_USER:-lions} - KC_DB_PASSWORD: ${DB_PASSWORD:-lions} - KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_ADMIN:-admin} - KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD:-admin} - ports: - - "${KC_PORT:-8180}:8080" - depends_on: - postgres: - condition: service_healthy - - lions-user-manager-server: - build: - context: ../.. - dockerfile: src/main/docker/Dockerfile.jvm - ports: - - "${SERVER_PORT:-8080}:8080" - environment: - QUARKUS_DATASOURCE_JDBC_URL: jdbc:postgresql://postgres:5432/${DB_NAME:-lions_user_manager} - QUARKUS_DATASOURCE_USERNAME: ${DB_USER:-lions} - QUARKUS_DATASOURCE_PASSWORD: ${DB_PASSWORD:-lions} - KEYCLOAK_SERVER_URL: http://keycloak:8080 - depends_on: - postgres: - condition: service_healthy - keycloak: - condition: service_started - -volumes: - postgres_data: +services: + postgres: + image: postgres:15 + environment: + POSTGRES_DB: ${DB_NAME:-lions_user_manager} + POSTGRES_USER: ${DB_USER:-lions} + POSTGRES_PASSWORD: ${DB_PASSWORD:-lions} + ports: + - "${DB_PORT:-5432}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-lions} -d ${DB_NAME:-lions_user_manager}"] + interval: 5s + timeout: 5s + retries: 5 + + keycloak: + image: quay.io/keycloak/keycloak:26.3.3 + command: start-dev + environment: + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://postgres:5432/${DB_NAME:-lions_user_manager} + KC_DB_USERNAME: ${DB_USER:-lions} + KC_DB_PASSWORD: ${DB_PASSWORD:-lions} + KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_ADMIN:-admin} + KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD:-admin} + ports: + - "${KC_PORT:-8180}:8080" + depends_on: + postgres: + condition: service_healthy + + lions-user-manager-server: + build: + context: ../.. + dockerfile: src/main/docker/Dockerfile.jvm + ports: + - "${SERVER_PORT:-8080}:8080" + environment: + QUARKUS_DATASOURCE_JDBC_URL: jdbc:postgresql://postgres:5432/${DB_NAME:-lions_user_manager} + QUARKUS_DATASOURCE_USERNAME: ${DB_USER:-lions} + QUARKUS_DATASOURCE_PASSWORD: ${DB_PASSWORD:-lions} + KEYCLOAK_SERVER_URL: http://keycloak:8080 + depends_on: + postgres: + condition: service_healthy + keycloak: + condition: service_started + +volumes: + postgres_data: diff --git a/script/docker/run-dev.bat b/script/docker/run-dev.bat index c2695f7..1bf8bdf 100644 --- a/script/docker/run-dev.bat +++ b/script/docker/run-dev.bat @@ -1,5 +1,5 @@ -@echo off -REM Demarre les dependances (postgres + keycloak) puis le serveur en mode dev (mvn quarkus:dev -P dev) -cd /d "%~dp0\..\.." -docker-compose -f script/docker/dependencies-docker-compose.yml up -d -mvn quarkus:dev -P dev +@echo off +REM Demarre les dependances (postgres + keycloak) puis le serveur en mode dev (mvn quarkus:dev -P dev) +cd /d "%~dp0\..\.." +docker-compose -f script/docker/dependencies-docker-compose.yml up -d +mvn quarkus:dev -P dev diff --git a/script/docker/run-dev.sh b/script/docker/run-dev.sh index a15508d..9ad8383 100644 --- a/script/docker/run-dev.sh +++ b/script/docker/run-dev.sh @@ -1,7 +1,7 @@ -#!/usr/bin/env bash -# Démarre les dépendances (postgres + keycloak) puis le serveur en mode dev (mvn quarkus:dev -P dev) -set -e -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR/../.." -docker-compose -f script/docker/dependencies-docker-compose.yml up -d -mvn quarkus:dev -P dev +#!/usr/bin/env bash +# Démarre les dépendances (postgres + keycloak) puis le serveur en mode dev (mvn quarkus:dev -P dev) +set -e +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR/../.." +docker-compose -f script/docker/dependencies-docker-compose.yml up -d +mvn quarkus:dev -P dev diff --git a/src/main/docker/Dockerfile.jvm b/src/main/docker/Dockerfile.jvm index e207a44..1ba3334 100644 --- a/src/main/docker/Dockerfile.jvm +++ b/src/main/docker/Dockerfile.jvm @@ -1,20 +1,20 @@ -FROM registry.access.redhat.com/ubi8/openjdk-17:1.20 - -ENV LANGUAGE='en_US:en' - -# Copy files with correct ownership for user 1001 -COPY --chown=1001:1001 target/quarkus-app/lib/ /deployments/lib/ -COPY --chown=1001:1001 target/quarkus-app/*.jar /deployments/ -COPY --chown=1001:1001 target/quarkus-app/app/ /deployments/app/ -COPY --chown=1001:1001 target/quarkus-app/quarkus/ /deployments/quarkus/ - -EXPOSE 8080 - -# Use user 1001 (compatible with K8s securityContext) -USER 1001 - -ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" -ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" - -# Use java command with proper Quarkus options for fast-jar -ENTRYPOINT ["java", "-Dquarkus.http.host=0.0.0.0", "-Djava.util.logging.manager=org.jboss.logmanager.LogManager", "-jar", "/deployments/quarkus-run.jar"] +FROM registry.access.redhat.com/ubi8/openjdk-17:1.20 + +ENV LANGUAGE='en_US:en' + +# Copy files with correct ownership for user 1001 +COPY --chown=1001:1001 target/quarkus-app/lib/ /deployments/lib/ +COPY --chown=1001:1001 target/quarkus-app/*.jar /deployments/ +COPY --chown=1001:1001 target/quarkus-app/app/ /deployments/app/ +COPY --chown=1001:1001 target/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 + +# Use user 1001 (compatible with K8s securityContext) +USER 1001 + +ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" + +# Use java command with proper Quarkus options for fast-jar +ENTRYPOINT ["java", "-Dquarkus.http.host=0.0.0.0", "-Djava.util.logging.manager=org.jboss.logmanager.LogManager", "-jar", "/deployments/quarkus-run.jar"] diff --git a/src/main/java/dev/lions/user/manager/client/KeycloakAdminClient.java b/src/main/java/dev/lions/user/manager/client/KeycloakAdminClient.java index d4e7bde..22dd208 100644 --- a/src/main/java/dev/lions/user/manager/client/KeycloakAdminClient.java +++ b/src/main/java/dev/lions/user/manager/client/KeycloakAdminClient.java @@ -1,76 +1,76 @@ -package dev.lions.user.manager.client; - -import org.keycloak.admin.client.Keycloak; -import org.keycloak.admin.client.resource.RealmResource; -import org.keycloak.admin.client.resource.UsersResource; -import org.keycloak.admin.client.resource.RolesResource; - -/** - * Interface pour le client Keycloak Admin - * Abstraction pour faciliter les tests et la gestion du cycle de vie - */ -public interface KeycloakAdminClient { - - /** - * Récupère l'instance Keycloak - * @return instance Keycloak - */ - Keycloak getInstance(); - - /** - * Récupère une ressource Realm - * @param realmName nom du realm - * @return RealmResource - */ - RealmResource getRealm(String realmName); - - /** - * Récupère la ressource Users d'un realm - * @param realmName nom du realm - * @return UsersResource - */ - UsersResource getUsers(String realmName); - - /** - * Récupère la ressource Roles d'un realm - * @param realmName nom du realm - * @return RolesResource - */ - RolesResource getRoles(String realmName); - - /** - * Vérifie si la connexion à Keycloak est active - * @return true si connecté - */ - boolean isConnected(); - - /** - * Vérifie si un realm existe - * @param realmName nom du realm - * @return true si le realm existe - */ - boolean realmExists(String realmName); - - /** - * Récupère la liste de tous les realms - * @return Liste des noms de realms - */ - java.util.List getAllRealms(); - - /** - * Récupère la liste des clientId d'un realm - * @param realmName nom du realm - * @return Liste des clientId - */ - java.util.List getRealmClients(String realmName); - - /** - * Ferme la connexion Keycloak - */ - void close(); - - /** - * Force la reconnexion - */ - void reconnect(); -} +package dev.lions.user.manager.client; + +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.admin.client.resource.RolesResource; + +/** + * Interface pour le client Keycloak Admin + * Abstraction pour faciliter les tests et la gestion du cycle de vie + */ +public interface KeycloakAdminClient { + + /** + * Récupère l'instance Keycloak + * @return instance Keycloak + */ + Keycloak getInstance(); + + /** + * Récupère une ressource Realm + * @param realmName nom du realm + * @return RealmResource + */ + RealmResource getRealm(String realmName); + + /** + * Récupère la ressource Users d'un realm + * @param realmName nom du realm + * @return UsersResource + */ + UsersResource getUsers(String realmName); + + /** + * Récupère la ressource Roles d'un realm + * @param realmName nom du realm + * @return RolesResource + */ + RolesResource getRoles(String realmName); + + /** + * Vérifie si la connexion à Keycloak est active + * @return true si connecté + */ + boolean isConnected(); + + /** + * Vérifie si un realm existe + * @param realmName nom du realm + * @return true si le realm existe + */ + boolean realmExists(String realmName); + + /** + * Récupère la liste de tous les realms + * @return Liste des noms de realms + */ + java.util.List getAllRealms(); + + /** + * Récupère la liste des clientId d'un realm + * @param realmName nom du realm + * @return Liste des clientId + */ + java.util.List getRealmClients(String realmName); + + /** + * Ferme la connexion Keycloak + */ + void close(); + + /** + * Force la reconnexion + */ + void reconnect(); +} diff --git a/src/main/java/dev/lions/user/manager/client/KeycloakAdminClientImpl.java b/src/main/java/dev/lions/user/manager/client/KeycloakAdminClientImpl.java index 6a35cf5..af11ccf 100644 --- a/src/main/java/dev/lions/user/manager/client/KeycloakAdminClientImpl.java +++ b/src/main/java/dev/lions/user/manager/client/KeycloakAdminClientImpl.java @@ -1,233 +1,233 @@ -package dev.lions.user.manager.client; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.quarkus.runtime.Startup; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.eclipse.microprofile.faulttolerance.CircuitBreaker; -import org.eclipse.microprofile.faulttolerance.Retry; -import org.eclipse.microprofile.faulttolerance.Timeout; -import org.keycloak.admin.client.Keycloak; -import org.keycloak.admin.client.resource.RealmResource; -import org.keycloak.admin.client.resource.RolesResource; -import org.keycloak.admin.client.resource.UsersResource; - -import jakarta.ws.rs.NotFoundException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.temporal.ChronoUnit; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * Implémentation du client Keycloak Admin - * Utilise le bean Keycloak géré par Quarkus (quarkus-keycloak-admin-rest-client) - * qui respecte la configuration Jackson (fail-on-unknown-properties=false) - * Utilise Circuit Breaker, Retry et Timeout pour la résilience - */ -@ApplicationScoped -@Startup -@Slf4j -public class KeycloakAdminClientImpl implements KeycloakAdminClient { - - @Inject - Keycloak keycloak; - - @ConfigProperty(name = "lions.keycloak.server-url") - String serverUrl; - - @ConfigProperty(name = "lions.keycloak.admin-realm") - String adminRealm; - - @ConfigProperty(name = "lions.keycloak.admin-client-id") - String adminClientId; - - @ConfigProperty(name = "lions.keycloak.admin-username") - String adminUsername; - - @PostConstruct - void init() { - log.info("========================================"); - log.info("Initialisation du client Keycloak Admin"); - log.info("========================================"); - log.info("Server URL: {}", serverUrl); - log.info("Admin Realm: {}", adminRealm); - log.info("Admin Client ID: {}", adminClientId); - log.info("Admin Username: {}", adminUsername); - log.info("✅ Client Keycloak initialisé via Quarkus CDI (connexion lazy)"); - log.info("La connexion sera établie lors de la première requête API"); - } - - @Override - @Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS) - @Timeout(value = 30, unit = ChronoUnit.SECONDS) - @CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000) - public Keycloak getInstance() { - return keycloak; - } - - @Override - @Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS) - @Timeout(value = 30, unit = ChronoUnit.SECONDS) - @CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000) - public RealmResource getRealm(String realmName) { - try { - return keycloak.realm(realmName); - } catch (Exception e) { - log.error("Erreur lors de la récupération du realm {}: {}", realmName, e.getMessage()); - throw new RuntimeException("Impossible de récupérer le realm: " + realmName, e); - } - } - - @Override - @Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS) - @Timeout(value = 30, unit = ChronoUnit.SECONDS) - @CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000) - public UsersResource getUsers(String realmName) { - return getRealm(realmName).users(); - } - - @Override - @Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS) - @Timeout(value = 30, unit = ChronoUnit.SECONDS) - @CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000) - public RolesResource getRoles(String realmName) { - return getRealm(realmName).roles(); - } - - @Override - public boolean isConnected() { - try { - // getAccessTokenString() n'implique pas la désérialisation de ServerInfoRepresentation - // (qui échoue sur le champ inconnu "cpuInfo" avec Keycloak 26+) - keycloak.tokenManager().getAccessTokenString(); - return true; - } catch (Exception e) { - log.warn("Keycloak non connecté: {}", e.getMessage()); - return false; - } - } - - @Override - public boolean realmExists(String realmName) { - try { - getRealm(realmName).roles().list(); - return true; - } catch (NotFoundException e) { - log.debug("Realm {} n'existe pas", realmName); - return false; - } catch (Exception e) { - log.debug("Erreur lors de la vérification du realm {} (probablement il existe): {}", - realmName, e.getMessage()); - return true; - } - } - - @Override - @Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS) - @Timeout(value = 30, unit = ChronoUnit.SECONDS) - @CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000) - public List getAllRealms() { - try { - log.debug("Récupération de tous les realms depuis Keycloak"); - // Appel HTTP direct pour éviter l'erreur de désérialisation de RealmRepresentation - // (champ bruteForceStrategy inconnu dans la version de la librairie cliente) - String token = keycloak.tokenManager().getAccessTokenString(); - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(serverUrl + "/admin/realms")) - .header("Authorization", "Bearer " + token) - .header("Accept", "application/json") - .GET() - .build(); - - HttpResponse response = HttpClient.newHttpClient() - .send(request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() != 200) { - throw new RuntimeException("Keycloak returned HTTP " + response.statusCode()); - } - - ObjectMapper mapper = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - - List> realmMaps = mapper.readValue( - response.body(), new TypeReference<>() {}); - - List realms = realmMaps.stream() - .map(r -> (String) r.get("realm")) - .filter(r -> r != null) - .collect(Collectors.toList()); - - log.debug("Realms récupérés: {}", realms); - return realms; - } catch (Exception e) { - log.error("Erreur lors de la récupération de tous les realms: {}", e.getMessage()); - throw new RuntimeException("Impossible de récupérer la liste des realms", e); - } - } - - @Override - @Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS) - @Timeout(value = 30, unit = ChronoUnit.SECONDS) - @CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000) - public List getRealmClients(String realmName) { - try { - log.debug("Récupération des clients du realm {}", realmName); - String token = keycloak.tokenManager().getAccessTokenString(); - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(serverUrl + "/admin/realms/" + realmName + "/clients")) - .header("Authorization", "Bearer " + token) - .header("Accept", "application/json") - .GET() - .build(); - - HttpResponse response = HttpClient.newHttpClient() - .send(request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() != 200) { - throw new RuntimeException("Keycloak returned HTTP " + response.statusCode()); - } - - ObjectMapper mapper = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - - List> clientMaps = mapper.readValue( - response.body(), new TypeReference<>() {}); - - List clients = clientMaps.stream() - .map(c -> (String) c.get("clientId")) - .filter(c -> c != null) - .collect(Collectors.toList()); - - log.debug("Clients récupérés pour {}: {}", realmName, clients); - return clients; - } catch (Exception e) { - log.error("Erreur lors de la récupération des clients du realm {}: {}", realmName, e.getMessage()); - throw new RuntimeException("Impossible de récupérer les clients du realm: " + realmName, e); - } - } - - @PreDestroy - @Override - public void close() { - log.info("Fermeture de la connexion Keycloak..."); - // Le cycle de vie est géré par Quarkus CDI - } - - @Override - public void reconnect() { - log.info("Reconnexion à Keycloak... (géré par Quarkus CDI)"); - // Le bean Keycloak est géré par Quarkus, pas de reconnexion manuelle nécessaire - } -} +package dev.lions.user.manager.client; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.runtime.Startup; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.faulttolerance.CircuitBreaker; +import org.eclipse.microprofile.faulttolerance.Retry; +import org.eclipse.microprofile.faulttolerance.Timeout; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.RolesResource; +import org.keycloak.admin.client.resource.UsersResource; + +import jakarta.ws.rs.NotFoundException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Implémentation du client Keycloak Admin + * Utilise le bean Keycloak géré par Quarkus (quarkus-keycloak-admin-rest-client) + * qui respecte la configuration Jackson (fail-on-unknown-properties=false) + * Utilise Circuit Breaker, Retry et Timeout pour la résilience + */ +@ApplicationScoped +@Startup +@Slf4j +public class KeycloakAdminClientImpl implements KeycloakAdminClient { + + @Inject + Keycloak keycloak; + + @ConfigProperty(name = "lions.keycloak.server-url") + String serverUrl; + + @ConfigProperty(name = "lions.keycloak.admin-realm") + String adminRealm; + + @ConfigProperty(name = "lions.keycloak.admin-client-id") + String adminClientId; + + @ConfigProperty(name = "lions.keycloak.admin-username") + String adminUsername; + + @PostConstruct + void init() { + log.info("========================================"); + log.info("Initialisation du client Keycloak Admin"); + log.info("========================================"); + log.info("Server URL: {}", serverUrl); + log.info("Admin Realm: {}", adminRealm); + log.info("Admin Client ID: {}", adminClientId); + log.info("Admin Username: {}", adminUsername); + log.info("✅ Client Keycloak initialisé via Quarkus CDI (connexion lazy)"); + log.info("La connexion sera établie lors de la première requête API"); + } + + @Override + @Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS) + @Timeout(value = 30, unit = ChronoUnit.SECONDS) + @CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000) + public Keycloak getInstance() { + return keycloak; + } + + @Override + @Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS) + @Timeout(value = 30, unit = ChronoUnit.SECONDS) + @CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000) + public RealmResource getRealm(String realmName) { + try { + return keycloak.realm(realmName); + } catch (Exception e) { + log.error("Erreur lors de la récupération du realm {}: {}", realmName, e.getMessage()); + throw new RuntimeException("Impossible de récupérer le realm: " + realmName, e); + } + } + + @Override + @Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS) + @Timeout(value = 30, unit = ChronoUnit.SECONDS) + @CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000) + public UsersResource getUsers(String realmName) { + return getRealm(realmName).users(); + } + + @Override + @Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS) + @Timeout(value = 30, unit = ChronoUnit.SECONDS) + @CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000) + public RolesResource getRoles(String realmName) { + return getRealm(realmName).roles(); + } + + @Override + public boolean isConnected() { + try { + // getAccessTokenString() n'implique pas la désérialisation de ServerInfoRepresentation + // (qui échoue sur le champ inconnu "cpuInfo" avec Keycloak 26+) + keycloak.tokenManager().getAccessTokenString(); + return true; + } catch (Exception e) { + log.warn("Keycloak non connecté: {}", e.getMessage()); + return false; + } + } + + @Override + public boolean realmExists(String realmName) { + try { + getRealm(realmName).roles().list(); + return true; + } catch (NotFoundException e) { + log.debug("Realm {} n'existe pas", realmName); + return false; + } catch (Exception e) { + log.debug("Erreur lors de la vérification du realm {} (probablement il existe): {}", + realmName, e.getMessage()); + return true; + } + } + + @Override + @Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS) + @Timeout(value = 30, unit = ChronoUnit.SECONDS) + @CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000) + public List getAllRealms() { + try { + log.debug("Récupération de tous les realms depuis Keycloak"); + // Appel HTTP direct pour éviter l'erreur de désérialisation de RealmRepresentation + // (champ bruteForceStrategy inconnu dans la version de la librairie cliente) + String token = keycloak.tokenManager().getAccessTokenString(); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(serverUrl + "/admin/realms")) + .header("Authorization", "Bearer " + token) + .header("Accept", "application/json") + .GET() + .build(); + + HttpResponse response = HttpClient.newHttpClient() + .send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new RuntimeException("Keycloak returned HTTP " + response.statusCode()); + } + + ObjectMapper mapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + List> realmMaps = mapper.readValue( + response.body(), new TypeReference<>() {}); + + List realms = realmMaps.stream() + .map(r -> (String) r.get("realm")) + .filter(r -> r != null) + .collect(Collectors.toList()); + + log.debug("Realms récupérés: {}", realms); + return realms; + } catch (Exception e) { + log.error("Erreur lors de la récupération de tous les realms: {}", e.getMessage()); + throw new RuntimeException("Impossible de récupérer la liste des realms", e); + } + } + + @Override + @Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS) + @Timeout(value = 30, unit = ChronoUnit.SECONDS) + @CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000) + public List getRealmClients(String realmName) { + try { + log.debug("Récupération des clients du realm {}", realmName); + String token = keycloak.tokenManager().getAccessTokenString(); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(serverUrl + "/admin/realms/" + realmName + "/clients")) + .header("Authorization", "Bearer " + token) + .header("Accept", "application/json") + .GET() + .build(); + + HttpResponse response = HttpClient.newHttpClient() + .send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new RuntimeException("Keycloak returned HTTP " + response.statusCode()); + } + + ObjectMapper mapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + List> clientMaps = mapper.readValue( + response.body(), new TypeReference<>() {}); + + List clients = clientMaps.stream() + .map(c -> (String) c.get("clientId")) + .filter(c -> c != null) + .collect(Collectors.toList()); + + log.debug("Clients récupérés pour {}: {}", realmName, clients); + return clients; + } catch (Exception e) { + log.error("Erreur lors de la récupération des clients du realm {}: {}", realmName, e.getMessage()); + throw new RuntimeException("Impossible de récupérer les clients du realm: " + realmName, e); + } + } + + @PreDestroy + @Override + public void close() { + log.info("Fermeture de la connexion Keycloak..."); + // Le cycle de vie est géré par Quarkus CDI + } + + @Override + public void reconnect() { + log.info("Reconnexion à Keycloak... (géré par Quarkus CDI)"); + // Le bean Keycloak est géré par Quarkus, pas de reconnexion manuelle nécessaire + } +} diff --git a/src/main/java/dev/lions/user/manager/config/JacksonConfig.java b/src/main/java/dev/lions/user/manager/config/JacksonConfig.java index 3cefc7d..63c73f8 100644 --- a/src/main/java/dev/lions/user/manager/config/JacksonConfig.java +++ b/src/main/java/dev/lions/user/manager/config/JacksonConfig.java @@ -1,22 +1,22 @@ -package dev.lions.user.manager.config; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.quarkus.jackson.ObjectMapperCustomizer; -import jakarta.inject.Singleton; -import lombok.extern.slf4j.Slf4j; - -/** - * Configure Jackson globally to ignore unknown JSON properties. - * This is required for forward compatibility with newer Keycloak versions (e.g. cpuInfo field). - */ -@Singleton -@Slf4j -public class JacksonConfig implements ObjectMapperCustomizer { - - @Override - public void customize(ObjectMapper objectMapper) { - log.info("### LIONS: Applying Jackson configuration for Keycloak compatibility ###"); - objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - } -} +package dev.lions.user.manager.config; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.jackson.ObjectMapperCustomizer; +import jakarta.inject.Singleton; +import lombok.extern.slf4j.Slf4j; + +/** + * Configure Jackson globally to ignore unknown JSON properties. + * This is required for forward compatibility with newer Keycloak versions (e.g. cpuInfo field). + */ +@Singleton +@Slf4j +public class JacksonConfig implements ObjectMapperCustomizer { + + @Override + public void customize(ObjectMapper objectMapper) { + log.info("### LIONS: Applying Jackson configuration for Keycloak compatibility ###"); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } +} diff --git a/src/main/java/dev/lions/user/manager/config/KeycloakJacksonCustomizer.java b/src/main/java/dev/lions/user/manager/config/KeycloakJacksonCustomizer.java index 99b30e8..aecd606 100644 --- a/src/main/java/dev/lions/user/manager/config/KeycloakJacksonCustomizer.java +++ b/src/main/java/dev/lions/user/manager/config/KeycloakJacksonCustomizer.java @@ -1,33 +1,33 @@ -package dev.lions.user.manager.config; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.quarkus.jackson.ObjectMapperCustomizer; -import jakarta.inject.Singleton; -import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.representations.idm.RoleRepresentation; - -/** - * Customizer pour Jackson afin d'ignorer les propriétés inconnues dans les - * représentations Keycloak. - * Cela évite les erreurs de désérialisation (comme bruteForceStrategy) lorsque - * le serveur Keycloak - * est plus récent que les bibliothèques clients. - */ -@Singleton -public class KeycloakJacksonCustomizer implements ObjectMapperCustomizer { - - @Override - public void customize(ObjectMapper objectMapper) { - // En plus de la configuration globale, on force les Mix-ins pour les classes - // Keycloak critiques - objectMapper.addMixIn(RealmRepresentation.class, IgnoreUnknownMixin.class); - objectMapper.addMixIn(UserRepresentation.class, IgnoreUnknownMixin.class); - objectMapper.addMixIn(RoleRepresentation.class, IgnoreUnknownMixin.class); - } - - @JsonIgnoreProperties(ignoreUnknown = true) - abstract static class IgnoreUnknownMixin { - } -} +package dev.lions.user.manager.config; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.jackson.ObjectMapperCustomizer; +import jakarta.inject.Singleton; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; + +/** + * Customizer pour Jackson afin d'ignorer les propriétés inconnues dans les + * représentations Keycloak. + * Cela évite les erreurs de désérialisation (comme bruteForceStrategy) lorsque + * le serveur Keycloak + * est plus récent que les bibliothèques clients. + */ +@Singleton +public class KeycloakJacksonCustomizer implements ObjectMapperCustomizer { + + @Override + public void customize(ObjectMapper objectMapper) { + // En plus de la configuration globale, on force les Mix-ins pour les classes + // Keycloak critiques + objectMapper.addMixIn(RealmRepresentation.class, IgnoreUnknownMixin.class); + objectMapper.addMixIn(UserRepresentation.class, IgnoreUnknownMixin.class); + objectMapper.addMixIn(RoleRepresentation.class, IgnoreUnknownMixin.class); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + abstract static class IgnoreUnknownMixin { + } +} diff --git a/src/main/java/dev/lions/user/manager/config/KeycloakTestUserConfig.java b/src/main/java/dev/lions/user/manager/config/KeycloakTestUserConfig.java index 02c6ba1..0004720 100644 --- a/src/main/java/dev/lions/user/manager/config/KeycloakTestUserConfig.java +++ b/src/main/java/dev/lions/user/manager/config/KeycloakTestUserConfig.java @@ -1,281 +1,281 @@ -package dev.lions.user.manager.config; - -import io.quarkus.arc.profile.IfBuildProfile; -import io.quarkus.runtime.StartupEvent; -import jakarta.enterprise.event.Observes; -import jakarta.inject.Inject; -import jakarta.inject.Singleton; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.keycloak.admin.client.Keycloak; -import org.keycloak.admin.client.KeycloakBuilder; -import org.keycloak.representations.idm.CredentialRepresentation; -import org.keycloak.representations.idm.ProtocolMapperRepresentation; -import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.representations.idm.RoleRepresentation; -import org.keycloak.representations.idm.UserRepresentation; - -import java.util.*; - -/** - * Configuration automatique de Keycloak pour l'utilisateur de test - * S'exécute au démarrage de l'application en mode dev - */ -@Singleton -@IfBuildProfile("dev") -@Slf4j -public class KeycloakTestUserConfig { - - @Inject - @ConfigProperty(name = "quarkus.profile", defaultValue = "prod") - String profile; - - @Inject - @ConfigProperty(name = "lions.keycloak.server-url") - String keycloakServerUrl; - - @Inject - @ConfigProperty(name = "lions.keycloak.admin-realm", defaultValue = "master") - String adminRealm; - - @Inject - @ConfigProperty(name = "lions.keycloak.admin-username", defaultValue = "admin") - String adminUsername; - - @Inject - @ConfigProperty(name = "lions.keycloak.admin-password", defaultValue = "admin") - String adminPassword; - - @Inject - @ConfigProperty(name = "lions.keycloak.authorized-realms") - String authorizedRealms; - - private static final String TEST_REALM = "lions-user-manager"; - private static final String TEST_USER = "test-user"; - private static final String TEST_PASSWORD = "test123"; - private static final String TEST_EMAIL = "test@lions.dev"; - private static final String CLIENT_ID = "lions-user-manager-client"; - - private static final List REQUIRED_ROLES = Arrays.asList( - "admin", "user_manager", "user_viewer", - "role_manager", "role_viewer", "auditor", "sync_manager" - ); - - void onStart(@Observes StartupEvent ev) { - // DÉSACTIVÉ: Configuration manuelle via script create-roles-and-assign.sh - // Cette configuration automatique cause des erreurs de compatibilité Keycloak - // (bruteForceStrategy, cpuInfo non reconnus par la version Keycloak client) - log.info("Configuration automatique de Keycloak DÉSACTIVÉE"); - log.info("Utiliser le script create-roles-and-assign.sh pour configurer Keycloak manuellement"); - return; - - /* ANCIEN CODE DÉSACTIVÉ - // Ne s'exécuter qu'en mode dev - if (!"dev".equals(profile) && !"development".equals(profile)) { - log.debug("Mode non-dev détecté ({}), configuration Keycloak ignorée", profile); - return; - } - - log.info("Configuration automatique de Keycloak pour l'utilisateur de test..."); - - Keycloak adminClient = null; - try { - // Connexion en tant qu'admin - adminClient = KeycloakBuilder.builder() - .serverUrl(keycloakServerUrl) - .realm(adminRealm) - .username(adminUsername) - .password(adminPassword) - .clientId("admin-cli") - .build(); - - // 1. Vérifier/Créer le realm - ensureRealmExists(adminClient); - - // 2. Créer les rôles - ensureRolesExist(adminClient); - - // 3. Créer l'utilisateur de test - String userId = ensureTestUserExists(adminClient); - - // 4. Assigner les rôles - assignRolesToUser(adminClient, userId); - - // 5. Vérifier/Créer le client et le mapper - ensureClientAndMapper(adminClient); - - log.info("✓ Configuration Keycloak terminée avec succès"); - log.info(" Utilisateur de test: {} / {}", TEST_USER, TEST_PASSWORD); - log.info(" Rôles assignés: {}", String.join(", ", REQUIRED_ROLES)); - - } catch (Exception e) { - log.error("Erreur lors de la configuration Keycloak: {}", e.getMessage(), e); - } finally { - if (adminClient != null) { - adminClient.close(); - } - } - */ - } - - private void ensureRealmExists(Keycloak adminClient) { - try { - adminClient.realms().realm(TEST_REALM).toRepresentation(); - log.debug("Realm '{}' existe déjà", TEST_REALM); - } catch (jakarta.ws.rs.NotFoundException e) { - log.info("Création du realm '{}'...", TEST_REALM); - RealmRepresentation realm = new RealmRepresentation(); - realm.setRealm(TEST_REALM); - realm.setEnabled(true); - adminClient.realms().create(realm); - log.info("✓ Realm '{}' créé", TEST_REALM); - } - } - - private void ensureRolesExist(Keycloak adminClient) { - var rolesResource = adminClient.realms().realm(TEST_REALM).roles(); - - for (String roleName : REQUIRED_ROLES) { - try { - rolesResource.get(roleName).toRepresentation(); - log.debug("Rôle '{}' existe déjà", roleName); - } catch (jakarta.ws.rs.NotFoundException e) { - log.info("Création du rôle '{}'...", roleName); - RoleRepresentation role = new RoleRepresentation(); - role.setName(roleName); - role.setDescription("Rôle " + roleName + " pour lions-user-manager"); - rolesResource.create(role); - log.info("✓ Rôle '{}' créé", roleName); - } - } - } - - private String ensureTestUserExists(Keycloak adminClient) { - var usersResource = adminClient.realms().realm(TEST_REALM).users(); - - // Chercher l'utilisateur - List users = usersResource.search(TEST_USER, true); - - String userId; - if (users != null && !users.isEmpty()) { - userId = users.get(0).getId(); - log.debug("Utilisateur '{}' existe déjà (ID: {})", TEST_USER, userId); - } else { - log.info("Création de l'utilisateur '{}'...", TEST_USER); - UserRepresentation user = new UserRepresentation(); - user.setUsername(TEST_USER); - user.setEmail(TEST_EMAIL); - user.setFirstName("Test"); - user.setLastName("User"); - user.setEnabled(true); - user.setEmailVerified(true); - - jakarta.ws.rs.core.Response response = usersResource.create(user); - userId = getCreatedId(response); - - // Définir le mot de passe - CredentialRepresentation credential = new CredentialRepresentation(); - credential.setType(CredentialRepresentation.PASSWORD); - credential.setValue(TEST_PASSWORD); - credential.setTemporary(false); - usersResource.get(userId).resetPassword(credential); - - log.info("✓ Utilisateur '{}' créé (ID: {})", TEST_USER, userId); - } - - return userId; - } - - private void assignRolesToUser(Keycloak adminClient, String userId) { - var usersResource = adminClient.realms().realm(TEST_REALM).users(); - var rolesResource = adminClient.realms().realm(TEST_REALM).roles(); - - List rolesToAssign = new ArrayList<>(); - for (String roleName : REQUIRED_ROLES) { - RoleRepresentation role = rolesResource.get(roleName).toRepresentation(); - rolesToAssign.add(role); - } - - usersResource.get(userId).roles().realmLevel().add(rolesToAssign); - log.info("✓ {} rôles assignés à l'utilisateur", rolesToAssign.size()); - } - - private void ensureClientAndMapper(Keycloak adminClient) { - try { - var clientsResource = adminClient.realms().realm(TEST_REALM).clients(); - var clients = clientsResource.findByClientId(CLIENT_ID); - - String clientId; - if (clients == null || clients.isEmpty()) { - log.info("Création du client '{}'...", CLIENT_ID); - org.keycloak.representations.idm.ClientRepresentation client = new org.keycloak.representations.idm.ClientRepresentation(); - client.setClientId(CLIENT_ID); - client.setName(CLIENT_ID); - client.setDescription("Client OIDC pour lions-user-manager"); - client.setEnabled(true); - client.setPublicClient(false); - client.setStandardFlowEnabled(true); - client.setDirectAccessGrantsEnabled(true); - client.setFullScopeAllowed(true); // IMPORTANT: Permet d'inclure tous les rôles dans le token - client.setRedirectUris(java.util.Arrays.asList( - "http://localhost:8080/*", - "http://localhost:8080/auth/callback" - )); - client.setWebOrigins(java.util.Arrays.asList("http://localhost:8080")); - client.setSecret("NTuaQpk5E6qiMqAWTFrCOcIkOABzZzKO"); - - jakarta.ws.rs.core.Response response = clientsResource.create(client); - clientId = getCreatedId(response); - log.info("✓ Client '{}' créé (ID: {})", CLIENT_ID, clientId); - } else { - clientId = clients.get(0).getId(); - log.debug("Client '{}' existe déjà (ID: {})", CLIENT_ID, clientId); - } - - // Ajouter le scope "roles" par défaut au client - try { - var clientScopesResource = adminClient.realms().realm(TEST_REALM).clientScopes(); - var defaultClientScopes = clientScopesResource.findAll(); - var rolesScope = defaultClientScopes.stream() - .filter(s -> "roles".equals(s.getName())) - .findFirst(); - - if (rolesScope.isPresent()) { - var clientResource = clientsResource.get(clientId); - var defaultScopes = clientResource.getDefaultClientScopes(); - boolean hasRolesScope = defaultScopes.stream() - .anyMatch(s -> "roles".equals(s.getName())); - - if (!hasRolesScope) { - log.info("Ajout du scope 'roles' au client..."); - clientResource.addDefaultClientScope(rolesScope.get().getId()); - log.info("✓ Scope 'roles' ajouté au client"); - } else { - log.debug("Scope 'roles' déjà présent sur le client"); - } - } else { - log.warn("Scope 'roles' non trouvé dans les scopes par défaut du realm"); - } - } catch (Exception e) { - log.warn("Erreur lors de l'ajout du scope 'roles': {}", e.getMessage()); - } - - // Le scope "roles" de Keycloak crée automatiquement realm_access.roles - // Pas besoin de mapper personnalisé si on utilise realm_access.roles - // Le mapper personnalisé peut créer des conflits (comme dans unionflow) - log.debug("Le scope 'roles' est utilisé pour créer realm_access.roles automatiquement"); - } catch (Exception e) { - log.warn("Erreur lors de la vérification/création du client: {}", e.getMessage(), e); - } - } - - private String getCreatedId(jakarta.ws.rs.core.Response response) { - jakarta.ws.rs.core.Response.StatusType statusInfo = response.getStatusInfo(); - if (statusInfo.equals(jakarta.ws.rs.core.Response.Status.CREATED)) { - String location = response.getLocation().getPath(); - return location.substring(location.lastIndexOf('/') + 1); - } - throw new RuntimeException("Erreur lors de la création: " + statusInfo.getStatusCode()); - } -} - +package dev.lions.user.manager.config; + +import io.quarkus.arc.profile.IfBuildProfile; +import io.quarkus.runtime.StartupEvent; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.KeycloakBuilder; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.representations.idm.ProtocolMapperRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; + +import java.util.*; + +/** + * Configuration automatique de Keycloak pour l'utilisateur de test + * S'exécute au démarrage de l'application en mode dev + */ +@Singleton +@IfBuildProfile("dev") +@Slf4j +public class KeycloakTestUserConfig { + + @Inject + @ConfigProperty(name = "quarkus.profile", defaultValue = "prod") + String profile; + + @Inject + @ConfigProperty(name = "lions.keycloak.server-url") + String keycloakServerUrl; + + @Inject + @ConfigProperty(name = "lions.keycloak.admin-realm", defaultValue = "master") + String adminRealm; + + @Inject + @ConfigProperty(name = "lions.keycloak.admin-username", defaultValue = "admin") + String adminUsername; + + @Inject + @ConfigProperty(name = "lions.keycloak.admin-password", defaultValue = "admin") + String adminPassword; + + @Inject + @ConfigProperty(name = "lions.keycloak.authorized-realms") + String authorizedRealms; + + private static final String TEST_REALM = "lions-user-manager"; + private static final String TEST_USER = "test-user"; + private static final String TEST_PASSWORD = "test123"; + private static final String TEST_EMAIL = "test@lions.dev"; + private static final String CLIENT_ID = "lions-user-manager-client"; + + private static final List REQUIRED_ROLES = Arrays.asList( + "admin", "user_manager", "user_viewer", + "role_manager", "role_viewer", "auditor", "sync_manager" + ); + + void onStart(@Observes StartupEvent ev) { + // DÉSACTIVÉ: Configuration manuelle via script create-roles-and-assign.sh + // Cette configuration automatique cause des erreurs de compatibilité Keycloak + // (bruteForceStrategy, cpuInfo non reconnus par la version Keycloak client) + log.info("Configuration automatique de Keycloak DÉSACTIVÉE"); + log.info("Utiliser le script create-roles-and-assign.sh pour configurer Keycloak manuellement"); + return; + + /* ANCIEN CODE DÉSACTIVÉ + // Ne s'exécuter qu'en mode dev + if (!"dev".equals(profile) && !"development".equals(profile)) { + log.debug("Mode non-dev détecté ({}), configuration Keycloak ignorée", profile); + return; + } + + log.info("Configuration automatique de Keycloak pour l'utilisateur de test..."); + + Keycloak adminClient = null; + try { + // Connexion en tant qu'admin + adminClient = KeycloakBuilder.builder() + .serverUrl(keycloakServerUrl) + .realm(adminRealm) + .username(adminUsername) + .password(adminPassword) + .clientId("admin-cli") + .build(); + + // 1. Vérifier/Créer le realm + ensureRealmExists(adminClient); + + // 2. Créer les rôles + ensureRolesExist(adminClient); + + // 3. Créer l'utilisateur de test + String userId = ensureTestUserExists(adminClient); + + // 4. Assigner les rôles + assignRolesToUser(adminClient, userId); + + // 5. Vérifier/Créer le client et le mapper + ensureClientAndMapper(adminClient); + + log.info("✓ Configuration Keycloak terminée avec succès"); + log.info(" Utilisateur de test: {} / {}", TEST_USER, TEST_PASSWORD); + log.info(" Rôles assignés: {}", String.join(", ", REQUIRED_ROLES)); + + } catch (Exception e) { + log.error("Erreur lors de la configuration Keycloak: {}", e.getMessage(), e); + } finally { + if (adminClient != null) { + adminClient.close(); + } + } + */ + } + + private void ensureRealmExists(Keycloak adminClient) { + try { + adminClient.realms().realm(TEST_REALM).toRepresentation(); + log.debug("Realm '{}' existe déjà", TEST_REALM); + } catch (jakarta.ws.rs.NotFoundException e) { + log.info("Création du realm '{}'...", TEST_REALM); + RealmRepresentation realm = new RealmRepresentation(); + realm.setRealm(TEST_REALM); + realm.setEnabled(true); + adminClient.realms().create(realm); + log.info("✓ Realm '{}' créé", TEST_REALM); + } + } + + private void ensureRolesExist(Keycloak adminClient) { + var rolesResource = adminClient.realms().realm(TEST_REALM).roles(); + + for (String roleName : REQUIRED_ROLES) { + try { + rolesResource.get(roleName).toRepresentation(); + log.debug("Rôle '{}' existe déjà", roleName); + } catch (jakarta.ws.rs.NotFoundException e) { + log.info("Création du rôle '{}'...", roleName); + RoleRepresentation role = new RoleRepresentation(); + role.setName(roleName); + role.setDescription("Rôle " + roleName + " pour lions-user-manager"); + rolesResource.create(role); + log.info("✓ Rôle '{}' créé", roleName); + } + } + } + + private String ensureTestUserExists(Keycloak adminClient) { + var usersResource = adminClient.realms().realm(TEST_REALM).users(); + + // Chercher l'utilisateur + List users = usersResource.search(TEST_USER, true); + + String userId; + if (users != null && !users.isEmpty()) { + userId = users.get(0).getId(); + log.debug("Utilisateur '{}' existe déjà (ID: {})", TEST_USER, userId); + } else { + log.info("Création de l'utilisateur '{}'...", TEST_USER); + UserRepresentation user = new UserRepresentation(); + user.setUsername(TEST_USER); + user.setEmail(TEST_EMAIL); + user.setFirstName("Test"); + user.setLastName("User"); + user.setEnabled(true); + user.setEmailVerified(true); + + jakarta.ws.rs.core.Response response = usersResource.create(user); + userId = getCreatedId(response); + + // Définir le mot de passe + CredentialRepresentation credential = new CredentialRepresentation(); + credential.setType(CredentialRepresentation.PASSWORD); + credential.setValue(TEST_PASSWORD); + credential.setTemporary(false); + usersResource.get(userId).resetPassword(credential); + + log.info("✓ Utilisateur '{}' créé (ID: {})", TEST_USER, userId); + } + + return userId; + } + + private void assignRolesToUser(Keycloak adminClient, String userId) { + var usersResource = adminClient.realms().realm(TEST_REALM).users(); + var rolesResource = adminClient.realms().realm(TEST_REALM).roles(); + + List rolesToAssign = new ArrayList<>(); + for (String roleName : REQUIRED_ROLES) { + RoleRepresentation role = rolesResource.get(roleName).toRepresentation(); + rolesToAssign.add(role); + } + + usersResource.get(userId).roles().realmLevel().add(rolesToAssign); + log.info("✓ {} rôles assignés à l'utilisateur", rolesToAssign.size()); + } + + private void ensureClientAndMapper(Keycloak adminClient) { + try { + var clientsResource = adminClient.realms().realm(TEST_REALM).clients(); + var clients = clientsResource.findByClientId(CLIENT_ID); + + String clientId; + if (clients == null || clients.isEmpty()) { + log.info("Création du client '{}'...", CLIENT_ID); + org.keycloak.representations.idm.ClientRepresentation client = new org.keycloak.representations.idm.ClientRepresentation(); + client.setClientId(CLIENT_ID); + client.setName(CLIENT_ID); + client.setDescription("Client OIDC pour lions-user-manager"); + client.setEnabled(true); + client.setPublicClient(false); + client.setStandardFlowEnabled(true); + client.setDirectAccessGrantsEnabled(true); + client.setFullScopeAllowed(true); // IMPORTANT: Permet d'inclure tous les rôles dans le token + client.setRedirectUris(java.util.Arrays.asList( + "http://localhost:8080/*", + "http://localhost:8080/auth/callback" + )); + client.setWebOrigins(java.util.Arrays.asList("http://localhost:8080")); + client.setSecret("NTuaQpk5E6qiMqAWTFrCOcIkOABzZzKO"); + + jakarta.ws.rs.core.Response response = clientsResource.create(client); + clientId = getCreatedId(response); + log.info("✓ Client '{}' créé (ID: {})", CLIENT_ID, clientId); + } else { + clientId = clients.get(0).getId(); + log.debug("Client '{}' existe déjà (ID: {})", CLIENT_ID, clientId); + } + + // Ajouter le scope "roles" par défaut au client + try { + var clientScopesResource = adminClient.realms().realm(TEST_REALM).clientScopes(); + var defaultClientScopes = clientScopesResource.findAll(); + var rolesScope = defaultClientScopes.stream() + .filter(s -> "roles".equals(s.getName())) + .findFirst(); + + if (rolesScope.isPresent()) { + var clientResource = clientsResource.get(clientId); + var defaultScopes = clientResource.getDefaultClientScopes(); + boolean hasRolesScope = defaultScopes.stream() + .anyMatch(s -> "roles".equals(s.getName())); + + if (!hasRolesScope) { + log.info("Ajout du scope 'roles' au client..."); + clientResource.addDefaultClientScope(rolesScope.get().getId()); + log.info("✓ Scope 'roles' ajouté au client"); + } else { + log.debug("Scope 'roles' déjà présent sur le client"); + } + } else { + log.warn("Scope 'roles' non trouvé dans les scopes par défaut du realm"); + } + } catch (Exception e) { + log.warn("Erreur lors de l'ajout du scope 'roles': {}", e.getMessage()); + } + + // Le scope "roles" de Keycloak crée automatiquement realm_access.roles + // Pas besoin de mapper personnalisé si on utilise realm_access.roles + // Le mapper personnalisé peut créer des conflits (comme dans unionflow) + log.debug("Le scope 'roles' est utilisé pour créer realm_access.roles automatiquement"); + } catch (Exception e) { + log.warn("Erreur lors de la vérification/création du client: {}", e.getMessage(), e); + } + } + + private String getCreatedId(jakarta.ws.rs.core.Response response) { + jakarta.ws.rs.core.Response.StatusType statusInfo = response.getStatusInfo(); + if (statusInfo.equals(jakarta.ws.rs.core.Response.Status.CREATED)) { + String location = response.getLocation().getPath(); + return location.substring(location.lastIndexOf('/') + 1); + } + throw new RuntimeException("Erreur lors de la création: " + statusInfo.getStatusCode()); + } +} + diff --git a/src/main/java/dev/lions/user/manager/mapper/RoleMapper.java b/src/main/java/dev/lions/user/manager/mapper/RoleMapper.java index 46385ea..55bd560 100644 --- a/src/main/java/dev/lions/user/manager/mapper/RoleMapper.java +++ b/src/main/java/dev/lions/user/manager/mapper/RoleMapper.java @@ -1,76 +1,76 @@ -package dev.lions.user.manager.mapper; - -import dev.lions.user.manager.dto.role.RoleDTO; -import dev.lions.user.manager.enums.role.TypeRole; -import org.keycloak.representations.idm.RoleRepresentation; - -import java.util.List; -import java.util.stream.Collectors; - -/** - * Mapper pour convertir entre RoleDTO et Keycloak RoleRepresentation - */ -public class RoleMapper { - - /** - * Convertit une RoleRepresentation Keycloak en RoleDTO - */ - public static RoleDTO toDTO(RoleRepresentation roleRep, String realmName, TypeRole typeRole) { - if (roleRep == null) { - return null; - } - - return RoleDTO.builder() - .id(roleRep.getId()) - .name(roleRep.getName()) - .description(roleRep.getDescription()) - .typeRole(typeRole) - .realmName(realmName) - .composite(roleRep.isComposite()) - .build(); - } - - /** - * Convertit un RoleDTO en RoleRepresentation Keycloak - */ - public static RoleRepresentation toRepresentation(RoleDTO roleDTO) { - if (roleDTO == null) { - return null; - } - - RoleRepresentation roleRep = new RoleRepresentation(); - roleRep.setId(roleDTO.getId()); - roleRep.setName(roleDTO.getName()); - roleRep.setDescription(roleDTO.getDescription()); - roleRep.setComposite(roleDTO.isComposite()); - roleRep.setClientRole(roleDTO.getTypeRole() == TypeRole.CLIENT_ROLE); - - return roleRep; - } - - /** - * Convertit une liste de RoleRepresentation en liste de RoleDTO - */ - public static List toDTOList(List roleReps, String realmName, TypeRole typeRole) { - if (roleReps == null) { - return List.of(); - } - - return roleReps.stream() - .map(roleRep -> toDTO(roleRep, realmName, typeRole)) - .collect(Collectors.toList()); - } - - /** - * Convertit une liste de RoleDTO en liste de RoleRepresentation - */ - public static List toRepresentationList(List roleDTOs) { - if (roleDTOs == null) { - return List.of(); - } - - return roleDTOs.stream() - .map(RoleMapper::toRepresentation) - .collect(Collectors.toList()); - } -} +package dev.lions.user.manager.mapper; + +import dev.lions.user.manager.dto.role.RoleDTO; +import dev.lions.user.manager.enums.role.TypeRole; +import org.keycloak.representations.idm.RoleRepresentation; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Mapper pour convertir entre RoleDTO et Keycloak RoleRepresentation + */ +public class RoleMapper { + + /** + * Convertit une RoleRepresentation Keycloak en RoleDTO + */ + public static RoleDTO toDTO(RoleRepresentation roleRep, String realmName, TypeRole typeRole) { + if (roleRep == null) { + return null; + } + + return RoleDTO.builder() + .id(roleRep.getId()) + .name(roleRep.getName()) + .description(roleRep.getDescription()) + .typeRole(typeRole) + .realmName(realmName) + .composite(roleRep.isComposite()) + .build(); + } + + /** + * Convertit un RoleDTO en RoleRepresentation Keycloak + */ + public static RoleRepresentation toRepresentation(RoleDTO roleDTO) { + if (roleDTO == null) { + return null; + } + + RoleRepresentation roleRep = new RoleRepresentation(); + roleRep.setId(roleDTO.getId()); + roleRep.setName(roleDTO.getName()); + roleRep.setDescription(roleDTO.getDescription()); + roleRep.setComposite(roleDTO.isComposite()); + roleRep.setClientRole(roleDTO.getTypeRole() == TypeRole.CLIENT_ROLE); + + return roleRep; + } + + /** + * Convertit une liste de RoleRepresentation en liste de RoleDTO + */ + public static List toDTOList(List roleReps, String realmName, TypeRole typeRole) { + if (roleReps == null) { + return List.of(); + } + + return roleReps.stream() + .map(roleRep -> toDTO(roleRep, realmName, typeRole)) + .collect(Collectors.toList()); + } + + /** + * Convertit une liste de RoleDTO en liste de RoleRepresentation + */ + public static List toRepresentationList(List roleDTOs) { + if (roleDTOs == null) { + return List.of(); + } + + return roleDTOs.stream() + .map(RoleMapper::toRepresentation) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/dev/lions/user/manager/mapper/UserMapper.java b/src/main/java/dev/lions/user/manager/mapper/UserMapper.java index a09b734..86d84cb 100644 --- a/src/main/java/dev/lions/user/manager/mapper/UserMapper.java +++ b/src/main/java/dev/lions/user/manager/mapper/UserMapper.java @@ -1,173 +1,173 @@ -package dev.lions.user.manager.mapper; - -import dev.lions.user.manager.dto.user.UserDTO; -import dev.lions.user.manager.enums.user.StatutUser; -import org.keycloak.representations.idm.UserRepresentation; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * Mapper pour convertir UserRepresentation (Keycloak) -> UserDTO - * Utilisé pour transformer les objets de l'API Keycloak vers nos DTOs - */ -public class UserMapper { - - private UserMapper() { - // Classe utilitaire - } - - /** - * Convertit UserRepresentation vers UserDTO - * @param userRep UserRepresentation de Keycloak - * @param realmName nom du realm - * @return UserDTO - */ - public static UserDTO toDTO(UserRepresentation userRep, String realmName) { - if (userRep == null) { - return null; - } - - return UserDTO.builder() - .id(userRep.getId()) - .username(userRep.getUsername()) - .email(userRep.getEmail()) - .emailVerified(userRep.isEmailVerified()) - .prenom(userRep.getFirstName()) - .nom(userRep.getLastName()) - .statut(StatutUser.fromEnabled(userRep.isEnabled())) - .enabled(userRep.isEnabled()) - .realmName(realmName) - .attributes(userRep.getAttributes()) - .requiredActions(userRep.getRequiredActions()) - .dateCreation(convertTimestamp(userRep.getCreatedTimestamp())) - .telephone(getAttributeValue(userRep, "phone_number")) - .organisation(getAttributeValue(userRep, "organization")) - .departement(getAttributeValue(userRep, "department")) - .fonction(getAttributeValue(userRep, "job_title")) - .pays(getAttributeValue(userRep, "country")) - .ville(getAttributeValue(userRep, "city")) - .langue(getAttributeValue(userRep, "locale")) - .timezone(getAttributeValue(userRep, "timezone")) - .build(); - } - - /** - * Convertit UserDTO vers UserRepresentation - * @param userDTO UserDTO - * @return UserRepresentation - */ - public static UserRepresentation toRepresentation(UserDTO userDTO) { - if (userDTO == null) { - return null; - } - - UserRepresentation userRep = new UserRepresentation(); - userRep.setId(userDTO.getId()); - userRep.setUsername(userDTO.getUsername()); - userRep.setEmail(userDTO.getEmail()); - userRep.setEmailVerified(userDTO.getEmailVerified()); - userRep.setFirstName(userDTO.getPrenom()); - userRep.setLastName(userDTO.getNom()); - userRep.setEnabled(userDTO.getEnabled() != null ? userDTO.getEnabled() : true); - - // Attributs personnalisés - Map> attributes = new HashMap<>(); - - if (userDTO.getTelephone() != null) { - attributes.put("phone_number", List.of(userDTO.getTelephone())); - } - if (userDTO.getOrganisation() != null) { - attributes.put("organization", List.of(userDTO.getOrganisation())); - } - if (userDTO.getDepartement() != null) { - attributes.put("department", List.of(userDTO.getDepartement())); - } - if (userDTO.getFonction() != null) { - attributes.put("job_title", List.of(userDTO.getFonction())); - } - if (userDTO.getPays() != null) { - attributes.put("country", List.of(userDTO.getPays())); - } - if (userDTO.getVille() != null) { - attributes.put("city", List.of(userDTO.getVille())); - } - if (userDTO.getLangue() != null) { - attributes.put("locale", List.of(userDTO.getLangue())); - } - if (userDTO.getTimezone() != null) { - attributes.put("timezone", List.of(userDTO.getTimezone())); - } - - // Ajouter les attributs existants du DTO - if (userDTO.getAttributes() != null) { - attributes.putAll(userDTO.getAttributes()); - } - - userRep.setAttributes(attributes); - - // Actions requises - if (userDTO.getRequiredActions() != null) { - userRep.setRequiredActions(userDTO.getRequiredActions()); - } - - return userRep; - } - - /** - * Convertit une liste de UserRepresentation vers UserDTO - * @param userReps liste de UserRepresentation - * @param realmName nom du realm - * @return liste de UserDTO - */ - public static List toDTOList(List userReps, String realmName) { - if (userReps == null) { - return new ArrayList<>(); - } - - return userReps.stream() - .map(userRep -> toDTO(userRep, realmName)) - .collect(Collectors.toList()); - } - - /** - * Récupère la valeur d'un attribut Keycloak - * @param userRep UserRepresentation - * @param attributeName nom de l'attribut - * @return valeur de l'attribut ou null - */ - private static String getAttributeValue(UserRepresentation userRep, String attributeName) { - if (userRep.getAttributes() == null) { - return null; - } - - List values = userRep.getAttributes().get(attributeName); - if (values == null || values.isEmpty()) { - return null; - } - - return values.get(0); - } - - /** - * Convertit un timestamp (millisecondes) vers LocalDateTime - * @param timestamp timestamp en millisecondes - * @return LocalDateTime ou null - */ - private static LocalDateTime convertTimestamp(Long timestamp) { - if (timestamp == null) { - return null; - } - - return LocalDateTime.ofInstant( - Instant.ofEpochMilli(timestamp), - ZoneId.systemDefault() - ); - } -} +package dev.lions.user.manager.mapper; + +import dev.lions.user.manager.dto.user.UserDTO; +import dev.lions.user.manager.enums.user.StatutUser; +import org.keycloak.representations.idm.UserRepresentation; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Mapper pour convertir UserRepresentation (Keycloak) -> UserDTO + * Utilisé pour transformer les objets de l'API Keycloak vers nos DTOs + */ +public class UserMapper { + + private UserMapper() { + // Classe utilitaire + } + + /** + * Convertit UserRepresentation vers UserDTO + * @param userRep UserRepresentation de Keycloak + * @param realmName nom du realm + * @return UserDTO + */ + public static UserDTO toDTO(UserRepresentation userRep, String realmName) { + if (userRep == null) { + return null; + } + + return UserDTO.builder() + .id(userRep.getId()) + .username(userRep.getUsername()) + .email(userRep.getEmail()) + .emailVerified(userRep.isEmailVerified()) + .prenom(userRep.getFirstName()) + .nom(userRep.getLastName()) + .statut(StatutUser.fromEnabled(userRep.isEnabled())) + .enabled(userRep.isEnabled()) + .realmName(realmName) + .attributes(userRep.getAttributes()) + .requiredActions(userRep.getRequiredActions()) + .dateCreation(convertTimestamp(userRep.getCreatedTimestamp())) + .telephone(getAttributeValue(userRep, "phone_number")) + .organisation(getAttributeValue(userRep, "organization")) + .departement(getAttributeValue(userRep, "department")) + .fonction(getAttributeValue(userRep, "job_title")) + .pays(getAttributeValue(userRep, "country")) + .ville(getAttributeValue(userRep, "city")) + .langue(getAttributeValue(userRep, "locale")) + .timezone(getAttributeValue(userRep, "timezone")) + .build(); + } + + /** + * Convertit UserDTO vers UserRepresentation + * @param userDTO UserDTO + * @return UserRepresentation + */ + public static UserRepresentation toRepresentation(UserDTO userDTO) { + if (userDTO == null) { + return null; + } + + UserRepresentation userRep = new UserRepresentation(); + userRep.setId(userDTO.getId()); + userRep.setUsername(userDTO.getUsername()); + userRep.setEmail(userDTO.getEmail()); + userRep.setEmailVerified(userDTO.getEmailVerified()); + userRep.setFirstName(userDTO.getPrenom()); + userRep.setLastName(userDTO.getNom()); + userRep.setEnabled(userDTO.getEnabled() != null ? userDTO.getEnabled() : true); + + // Attributs personnalisés + Map> attributes = new HashMap<>(); + + if (userDTO.getTelephone() != null) { + attributes.put("phone_number", List.of(userDTO.getTelephone())); + } + if (userDTO.getOrganisation() != null) { + attributes.put("organization", List.of(userDTO.getOrganisation())); + } + if (userDTO.getDepartement() != null) { + attributes.put("department", List.of(userDTO.getDepartement())); + } + if (userDTO.getFonction() != null) { + attributes.put("job_title", List.of(userDTO.getFonction())); + } + if (userDTO.getPays() != null) { + attributes.put("country", List.of(userDTO.getPays())); + } + if (userDTO.getVille() != null) { + attributes.put("city", List.of(userDTO.getVille())); + } + if (userDTO.getLangue() != null) { + attributes.put("locale", List.of(userDTO.getLangue())); + } + if (userDTO.getTimezone() != null) { + attributes.put("timezone", List.of(userDTO.getTimezone())); + } + + // Ajouter les attributs existants du DTO + if (userDTO.getAttributes() != null) { + attributes.putAll(userDTO.getAttributes()); + } + + userRep.setAttributes(attributes); + + // Actions requises + if (userDTO.getRequiredActions() != null) { + userRep.setRequiredActions(userDTO.getRequiredActions()); + } + + return userRep; + } + + /** + * Convertit une liste de UserRepresentation vers UserDTO + * @param userReps liste de UserRepresentation + * @param realmName nom du realm + * @return liste de UserDTO + */ + public static List toDTOList(List userReps, String realmName) { + if (userReps == null) { + return new ArrayList<>(); + } + + return userReps.stream() + .map(userRep -> toDTO(userRep, realmName)) + .collect(Collectors.toList()); + } + + /** + * Récupère la valeur d'un attribut Keycloak + * @param userRep UserRepresentation + * @param attributeName nom de l'attribut + * @return valeur de l'attribut ou null + */ + private static String getAttributeValue(UserRepresentation userRep, String attributeName) { + if (userRep.getAttributes() == null) { + return null; + } + + List values = userRep.getAttributes().get(attributeName); + if (values == null || values.isEmpty()) { + return null; + } + + return values.get(0); + } + + /** + * Convertit un timestamp (millisecondes) vers LocalDateTime + * @param timestamp timestamp en millisecondes + * @return LocalDateTime ou null + */ + private static LocalDateTime convertTimestamp(Long timestamp) { + if (timestamp == null) { + return null; + } + + return LocalDateTime.ofInstant( + Instant.ofEpochMilli(timestamp), + ZoneId.systemDefault() + ); + } +} diff --git a/src/main/java/dev/lions/user/manager/resource/AuditResource.java b/src/main/java/dev/lions/user/manager/resource/AuditResource.java index b9094a5..d7e43f0 100644 --- a/src/main/java/dev/lions/user/manager/resource/AuditResource.java +++ b/src/main/java/dev/lions/user/manager/resource/AuditResource.java @@ -1,171 +1,171 @@ -package dev.lions.user.manager.resource; - -import dev.lions.user.manager.api.AuditResourceApi; -import dev.lions.user.manager.dto.audit.AuditLogDTO; -import dev.lions.user.manager.dto.common.CountDTO; -import dev.lions.user.manager.enums.audit.TypeActionAudit; -import dev.lions.user.manager.service.AuditService; -import jakarta.annotation.security.RolesAllowed; -import jakarta.inject.Inject; -import jakarta.ws.rs.core.Response; -import org.eclipse.microprofile.config.inject.ConfigProperty; -import lombok.extern.slf4j.Slf4j; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * REST Resource pour l'audit et la consultation des logs - * Implémente l'interface API commune. - */ -@Slf4j -@jakarta.enterprise.context.ApplicationScoped -@jakarta.ws.rs.Path("/api/audit") -public class AuditResource implements AuditResourceApi { - - private static final String DEFAULT_REALM_VALUE = "master"; - - @Inject - AuditService auditService; - - @ConfigProperty(name = "lions.keycloak.admin-realm", defaultValue = DEFAULT_REALM_VALUE) - String defaultRealm; - - @Override - @RolesAllowed({ "admin", "auditor" }) - public List searchLogs( - String acteurUsername, - String dateDebutStr, - String dateFinStr, - TypeActionAudit typeAction, - String ressourceType, - Boolean succes, - int page, - int pageSize) { - log.info("POST /api/audit/search - Recherche de logs"); - - LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null; - LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null; - - // Utiliser findByActeur si acteurUsername est fourni, sinon findByRealm - List logs; - if (acteurUsername != null && !acteurUsername.isBlank()) { - logs = auditService.findByActeur(acteurUsername, dateDebut, dateFin, page, pageSize); - } else { - // Pour une recherche générale, utiliser findByRealm (on utilise defaultRealm par - // défaut) - logs = auditService.findByRealm(defaultRealm, dateDebut, dateFin, page, pageSize); - } - - // Filtrer par typeAction, ressourceType et succes si fournis - if (typeAction != null || ressourceType != null || succes != null) { - logs = logs.stream() - .filter(log -> typeAction == null || typeAction.equals(log.getTypeAction())) - .filter(log -> ressourceType == null || ressourceType.equals(log.getRessourceType())) - .filter(log -> succes == null || succes == log.isSuccessful()) - .collect(Collectors.toList()); - } - - return logs; - } - - @Override - @RolesAllowed({ "admin", "auditor" }) - public List getLogsByActor(String acteurUsername, int limit) { - log.info("GET /api/audit/actor/{} - Limite: {}", acteurUsername, limit); - return auditService.findByActeur(acteurUsername, null, null, 0, limit); - } - - @Override - @RolesAllowed({ "admin", "auditor" }) - public List getLogsByResource(String ressourceType, String ressourceId, int limit) { - log.info("GET /api/audit/resource/{}/{} - Limite: {}", ressourceType, ressourceId, limit); - return auditService.findByRessource(ressourceType, ressourceId, null, null, 0, limit); - } - - @Override - @RolesAllowed({ "admin", "auditor" }) - public List getLogsByAction(TypeActionAudit typeAction, String dateDebutStr, String dateFinStr, - int limit) { - log.info("GET /api/audit/action/{} - Limite: {}", typeAction, limit); - LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null; - LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null; - - return auditService.findByTypeAction(typeAction, defaultRealm, dateDebut, dateFin, 0, limit); - } - - @Override - @RolesAllowed({ "admin", "auditor" }) - public Map getActionStatistics(String dateDebutStr, String dateFinStr) { - log.info("GET /api/audit/stats/actions - Période: {} à {}", dateDebutStr, dateFinStr); - LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null; - LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null; - - return auditService.countByActionType(defaultRealm, dateDebut, dateFin); - } - - @Override - @RolesAllowed({ "admin", "auditor" }) - public Map getUserActivityStatistics(String dateDebutStr, String dateFinStr) { - log.info("GET /api/audit/stats/users - Période: {} à {}", dateDebutStr, dateFinStr); - LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null; - LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null; - - return auditService.countByActeur(defaultRealm, dateDebut, dateFin); - } - - @Override - @RolesAllowed({ "admin", "auditor" }) - public CountDTO getFailureCount(String dateDebutStr, String dateFinStr) { - log.info("GET /api/audit/stats/failures - Période: {} à {}", dateDebutStr, dateFinStr); - LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null; - LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null; - - Map successVsFailure = auditService.countSuccessVsFailure(defaultRealm, dateDebut, dateFin); - long count = successVsFailure.getOrDefault("failure", 0L); - return new CountDTO(count); - } - - @Override - @RolesAllowed({ "admin", "auditor" }) - public CountDTO getSuccessCount(String dateDebutStr, String dateFinStr) { - log.info("GET /api/audit/stats/success - Période: {} à {}", dateDebutStr, dateFinStr); - LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null; - LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null; - - Map successVsFailure = auditService.countSuccessVsFailure(defaultRealm, dateDebut, dateFin); - long count = successVsFailure.getOrDefault("success", 0L); - return new CountDTO(count); - } - - @Override - @RolesAllowed({ "admin", "auditor" }) - public Response exportLogsToCSV(String dateDebutStr, String dateFinStr) { - log.info("GET /api/audit/export/csv - Période: {} à {}", dateDebutStr, dateFinStr); - - try { - LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null; - LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null; - - String csvContent = auditService.exportToCSV(defaultRealm, dateDebut, dateFin); - - return Response.ok(csvContent) - .header("Content-Disposition", "attachment; filename=\"audit-logs-" + - LocalDateTime.now().toString().replace(":", "-") + ".csv\"") - .build(); - } catch (Exception e) { - log.error("Erreur lors de l'export CSV des logs", e); - throw new RuntimeException(e); - } - } - - @Override - @RolesAllowed({ "admin" }) - public void purgeOldLogs(int joursAnciennete) { - log.info("DELETE /api/audit/purge - Suppression des logs de plus de {} jours", joursAnciennete); - LocalDateTime dateLimite = LocalDateTime.now().minusDays(joursAnciennete); - auditService.purgeOldLogs(dateLimite); - } -} +package dev.lions.user.manager.resource; + +import dev.lions.user.manager.api.AuditResourceApi; +import dev.lions.user.manager.dto.audit.AuditLogDTO; +import dev.lions.user.manager.dto.common.CountDTO; +import dev.lions.user.manager.enums.audit.TypeActionAudit; +import dev.lions.user.manager.service.AuditService; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * REST Resource pour l'audit et la consultation des logs + * Implémente l'interface API commune. + */ +@Slf4j +@jakarta.enterprise.context.ApplicationScoped +@jakarta.ws.rs.Path("/api/audit") +public class AuditResource implements AuditResourceApi { + + private static final String DEFAULT_REALM_VALUE = "master"; + + @Inject + AuditService auditService; + + @ConfigProperty(name = "lions.keycloak.admin-realm", defaultValue = DEFAULT_REALM_VALUE) + String defaultRealm; + + @Override + @RolesAllowed({ "admin", "auditor" }) + public List searchLogs( + String acteurUsername, + String dateDebutStr, + String dateFinStr, + TypeActionAudit typeAction, + String ressourceType, + Boolean succes, + int page, + int pageSize) { + log.info("POST /api/audit/search - Recherche de logs"); + + LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null; + LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null; + + // Utiliser findByActeur si acteurUsername est fourni, sinon findByRealm + List logs; + if (acteurUsername != null && !acteurUsername.isBlank()) { + logs = auditService.findByActeur(acteurUsername, dateDebut, dateFin, page, pageSize); + } else { + // Pour une recherche générale, utiliser findByRealm (on utilise defaultRealm par + // défaut) + logs = auditService.findByRealm(defaultRealm, dateDebut, dateFin, page, pageSize); + } + + // Filtrer par typeAction, ressourceType et succes si fournis + if (typeAction != null || ressourceType != null || succes != null) { + logs = logs.stream() + .filter(log -> typeAction == null || typeAction.equals(log.getTypeAction())) + .filter(log -> ressourceType == null || ressourceType.equals(log.getRessourceType())) + .filter(log -> succes == null || succes == log.isSuccessful()) + .collect(Collectors.toList()); + } + + return logs; + } + + @Override + @RolesAllowed({ "admin", "auditor" }) + public List getLogsByActor(String acteurUsername, int limit) { + log.info("GET /api/audit/actor/{} - Limite: {}", acteurUsername, limit); + return auditService.findByActeur(acteurUsername, null, null, 0, limit); + } + + @Override + @RolesAllowed({ "admin", "auditor" }) + public List getLogsByResource(String ressourceType, String ressourceId, int limit) { + log.info("GET /api/audit/resource/{}/{} - Limite: {}", ressourceType, ressourceId, limit); + return auditService.findByRessource(ressourceType, ressourceId, null, null, 0, limit); + } + + @Override + @RolesAllowed({ "admin", "auditor" }) + public List getLogsByAction(TypeActionAudit typeAction, String dateDebutStr, String dateFinStr, + int limit) { + log.info("GET /api/audit/action/{} - Limite: {}", typeAction, limit); + LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null; + LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null; + + return auditService.findByTypeAction(typeAction, defaultRealm, dateDebut, dateFin, 0, limit); + } + + @Override + @RolesAllowed({ "admin", "auditor" }) + public Map getActionStatistics(String dateDebutStr, String dateFinStr) { + log.info("GET /api/audit/stats/actions - Période: {} à {}", dateDebutStr, dateFinStr); + LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null; + LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null; + + return auditService.countByActionType(defaultRealm, dateDebut, dateFin); + } + + @Override + @RolesAllowed({ "admin", "auditor" }) + public Map getUserActivityStatistics(String dateDebutStr, String dateFinStr) { + log.info("GET /api/audit/stats/users - Période: {} à {}", dateDebutStr, dateFinStr); + LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null; + LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null; + + return auditService.countByActeur(defaultRealm, dateDebut, dateFin); + } + + @Override + @RolesAllowed({ "admin", "auditor" }) + public CountDTO getFailureCount(String dateDebutStr, String dateFinStr) { + log.info("GET /api/audit/stats/failures - Période: {} à {}", dateDebutStr, dateFinStr); + LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null; + LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null; + + Map successVsFailure = auditService.countSuccessVsFailure(defaultRealm, dateDebut, dateFin); + long count = successVsFailure.getOrDefault("failure", 0L); + return new CountDTO(count); + } + + @Override + @RolesAllowed({ "admin", "auditor" }) + public CountDTO getSuccessCount(String dateDebutStr, String dateFinStr) { + log.info("GET /api/audit/stats/success - Période: {} à {}", dateDebutStr, dateFinStr); + LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null; + LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null; + + Map successVsFailure = auditService.countSuccessVsFailure(defaultRealm, dateDebut, dateFin); + long count = successVsFailure.getOrDefault("success", 0L); + return new CountDTO(count); + } + + @Override + @RolesAllowed({ "admin", "auditor" }) + public Response exportLogsToCSV(String dateDebutStr, String dateFinStr) { + log.info("GET /api/audit/export/csv - Période: {} à {}", dateDebutStr, dateFinStr); + + try { + LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null; + LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null; + + String csvContent = auditService.exportToCSV(defaultRealm, dateDebut, dateFin); + + return Response.ok(csvContent) + .header("Content-Disposition", "attachment; filename=\"audit-logs-" + + LocalDateTime.now().toString().replace(":", "-") + ".csv\"") + .build(); + } catch (Exception e) { + log.error("Erreur lors de l'export CSV des logs", e); + throw new RuntimeException(e); + } + } + + @Override + @RolesAllowed({ "admin" }) + public void purgeOldLogs(int joursAnciennete) { + log.info("DELETE /api/audit/purge - Suppression des logs de plus de {} jours", joursAnciennete); + LocalDateTime dateLimite = LocalDateTime.now().minusDays(joursAnciennete); + auditService.purgeOldLogs(dateLimite); + } +} diff --git a/src/main/java/dev/lions/user/manager/resource/HealthResourceEndpoint.java b/src/main/java/dev/lions/user/manager/resource/HealthResourceEndpoint.java index e4691e0..3881179 100644 --- a/src/main/java/dev/lions/user/manager/resource/HealthResourceEndpoint.java +++ b/src/main/java/dev/lions/user/manager/resource/HealthResourceEndpoint.java @@ -1,68 +1,68 @@ -package dev.lions.user.manager.resource; - -import dev.lions.user.manager.client.KeycloakAdminClient; -import jakarta.inject.Inject; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -import lombok.extern.slf4j.Slf4j; - -import java.util.HashMap; -import java.util.Map; - -/** - * Resource REST pour health et readiness - */ -@Path("/api/health") -@Produces(MediaType.APPLICATION_JSON) -@Slf4j -public class HealthResourceEndpoint { - - @Inject - KeycloakAdminClient keycloakAdminClient; - - @GET - @Path("/keycloak") - public Map getKeycloakHealth() { - Map health = new HashMap<>(); - - try { - // Vérifier simplement que le client est initialisé (pas d'appel réel à Keycloak) - boolean initialized = keycloakAdminClient.getInstance() != null; - health.put("status", initialized ? "UP" : "DOWN"); - health.put("connected", initialized); - health.put("message", initialized ? "Client Keycloak initialisé" : "Client non initialisé"); - health.put("timestamp", System.currentTimeMillis()); - } catch (Exception e) { - log.error("Erreur health check Keycloak", e); - health.put("status", "ERROR"); - health.put("connected", false); - health.put("error", e.getMessage()); - health.put("timestamp", System.currentTimeMillis()); - } - - return health; - } - - @GET - @Path("/status") - public Map getServiceStatus() { - Map status = new HashMap<>(); - status.put("service", "lions-user-manager-server"); - status.put("version", "1.0.0"); - status.put("status", "UP"); - status.put("timestamp", System.currentTimeMillis()); - - // Health Keycloak - try { - boolean keycloakConnected = keycloakAdminClient.isConnected(); - status.put("keycloak", keycloakConnected ? "CONNECTED" : "DISCONNECTED"); - } catch (Exception e) { - status.put("keycloak", "ERROR"); - status.put("keycloakError", e.getMessage()); - } - - return status; - } -} +package dev.lions.user.manager.resource; + +import dev.lions.user.manager.client.KeycloakAdminClient; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.Map; + +/** + * Resource REST pour health et readiness + */ +@Path("/api/health") +@Produces(MediaType.APPLICATION_JSON) +@Slf4j +public class HealthResourceEndpoint { + + @Inject + KeycloakAdminClient keycloakAdminClient; + + @GET + @Path("/keycloak") + public Map getKeycloakHealth() { + Map health = new HashMap<>(); + + try { + // Vérifier simplement que le client est initialisé (pas d'appel réel à Keycloak) + boolean initialized = keycloakAdminClient.getInstance() != null; + health.put("status", initialized ? "UP" : "DOWN"); + health.put("connected", initialized); + health.put("message", initialized ? "Client Keycloak initialisé" : "Client non initialisé"); + health.put("timestamp", System.currentTimeMillis()); + } catch (Exception e) { + log.error("Erreur health check Keycloak", e); + health.put("status", "ERROR"); + health.put("connected", false); + health.put("error", e.getMessage()); + health.put("timestamp", System.currentTimeMillis()); + } + + return health; + } + + @GET + @Path("/status") + public Map getServiceStatus() { + Map status = new HashMap<>(); + status.put("service", "lions-user-manager-server"); + status.put("version", "1.0.0"); + status.put("status", "UP"); + status.put("timestamp", System.currentTimeMillis()); + + // Health Keycloak + try { + boolean keycloakConnected = keycloakAdminClient.isConnected(); + status.put("keycloak", keycloakConnected ? "CONNECTED" : "DISCONNECTED"); + } catch (Exception e) { + status.put("keycloak", "ERROR"); + status.put("keycloakError", e.getMessage()); + } + + return status; + } +} diff --git a/src/main/java/dev/lions/user/manager/resource/KeycloakHealthCheck.java b/src/main/java/dev/lions/user/manager/resource/KeycloakHealthCheck.java index a78ef63..e7a9664 100644 --- a/src/main/java/dev/lions/user/manager/resource/KeycloakHealthCheck.java +++ b/src/main/java/dev/lions/user/manager/resource/KeycloakHealthCheck.java @@ -1,50 +1,50 @@ -package dev.lions.user.manager.resource; - -import dev.lions.user.manager.client.KeycloakAdminClient; -import jakarta.inject.Inject; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.microprofile.health.HealthCheck; -import org.eclipse.microprofile.health.HealthCheckResponse; -import org.eclipse.microprofile.health.Readiness; - -/** - * Health check pour Keycloak - */ -@Readiness -@Slf4j -public class KeycloakHealthCheck implements HealthCheck { - - @Inject - KeycloakAdminClient keycloakAdminClient; - - @Override - public HealthCheckResponse call() { - try { - boolean connected = keycloakAdminClient.isConnected(); - - if (connected) { - return HealthCheckResponse.builder() - .name("keycloak-connection") - .up() - .withData("status", "connected") - .withData("message", "Keycloak est disponible") - .build(); - } else { - return HealthCheckResponse.builder() - .name("keycloak-connection") - .down() - .withData("status", "disconnected") - .withData("message", "Keycloak n'est pas disponible") - .build(); - } - } catch (Exception e) { - log.error("Erreur lors du health check Keycloak", e); - return HealthCheckResponse.builder() - .name("keycloak-connection") - .down() - .withData("status", "error") - .withData("message", e.getMessage()) - .build(); - } - } -} +package dev.lions.user.manager.resource; + +import dev.lions.user.manager.client.KeycloakAdminClient; +import jakarta.inject.Inject; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.Readiness; + +/** + * Health check pour Keycloak + */ +@Readiness +@Slf4j +public class KeycloakHealthCheck implements HealthCheck { + + @Inject + KeycloakAdminClient keycloakAdminClient; + + @Override + public HealthCheckResponse call() { + try { + boolean connected = keycloakAdminClient.isConnected(); + + if (connected) { + return HealthCheckResponse.builder() + .name("keycloak-connection") + .up() + .withData("status", "connected") + .withData("message", "Keycloak est disponible") + .build(); + } else { + return HealthCheckResponse.builder() + .name("keycloak-connection") + .down() + .withData("status", "disconnected") + .withData("message", "Keycloak n'est pas disponible") + .build(); + } + } catch (Exception e) { + log.error("Erreur lors du health check Keycloak", e); + return HealthCheckResponse.builder() + .name("keycloak-connection") + .down() + .withData("status", "error") + .withData("message", e.getMessage()) + .build(); + } + } +} diff --git a/src/main/java/dev/lions/user/manager/resource/RealmAssignmentResource.java b/src/main/java/dev/lions/user/manager/resource/RealmAssignmentResource.java index c2fdb32..ca48017 100644 --- a/src/main/java/dev/lions/user/manager/resource/RealmAssignmentResource.java +++ b/src/main/java/dev/lions/user/manager/resource/RealmAssignmentResource.java @@ -1,141 +1,141 @@ -package dev.lions.user.manager.resource; - -import dev.lions.user.manager.api.RealmAssignmentResourceApi; -import dev.lions.user.manager.dto.realm.AuthorizedRealmsDTO; -import dev.lions.user.manager.dto.realm.RealmAccessCheckDTO; -import dev.lions.user.manager.dto.realm.RealmAssignmentDTO; -import dev.lions.user.manager.service.RealmAuthorizationService; -import jakarta.annotation.security.RolesAllowed; -import jakarta.inject.Inject; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.SecurityContext; -import lombok.extern.slf4j.Slf4j; - -import java.util.List; - -/** - * REST Resource pour la gestion des affectations de realms aux utilisateurs - * Implémente l'interface API commune. - */ -@Slf4j -@jakarta.enterprise.context.ApplicationScoped -@jakarta.ws.rs.Path("/api/realm-assignments") -public class RealmAssignmentResource implements RealmAssignmentResourceApi { - - @Inject - RealmAuthorizationService realmAuthorizationService; - - @Context - SecurityContext securityContext; - - @Override - @RolesAllowed({ "admin" }) - public List getAllAssignments() { - log.info("GET /api/realm-assignments - Récupération de toutes les affectations"); - return realmAuthorizationService.getAllAssignments(); - } - - @Override - @RolesAllowed({ "admin", "user_manager" }) - public List getAssignmentsByUser(String userId) { - log.info("GET /api/realm-assignments/user/{}", userId); - return realmAuthorizationService.getAssignmentsByUser(userId); - } - - @Override - @RolesAllowed({ "admin" }) - public List getAssignmentsByRealm(String realmName) { - log.info("GET /api/realm-assignments/realm/{}", realmName); - return realmAuthorizationService.getAssignmentsByRealm(realmName); - } - - @Override - @RolesAllowed({ "admin" }) - public RealmAssignmentDTO getAssignmentById(String assignmentId) { - log.info("GET /api/realm-assignments/{}", assignmentId); - return realmAuthorizationService.getAssignmentById(assignmentId) - .orElseThrow(() -> new RuntimeException("Affectation non trouvée")); // ExceptionMapper should - // handle/map to 404 - } - - @Override - @RolesAllowed({ "admin", "user_manager" }) - public RealmAccessCheckDTO canManageRealm(String userId, String realmName) { - log.info("GET /api/realm-assignments/check - userId: {}, realmName: {}", userId, realmName); - boolean canManage = realmAuthorizationService.canManageRealm(userId, realmName); - return new RealmAccessCheckDTO(canManage, userId, realmName); - } - - @Override - @RolesAllowed({ "admin", "user_manager" }) - public AuthorizedRealmsDTO getAuthorizedRealms(String userId) { - log.info("GET /api/realm-assignments/authorized-realms/{}", userId); - List realms = realmAuthorizationService.getAuthorizedRealms(userId); - boolean isSuperAdmin = realmAuthorizationService.isSuperAdmin(userId); - return new AuthorizedRealmsDTO(realms, isSuperAdmin); - } - - @Override - @RolesAllowed({ "admin" }) - public Response assignRealmToUser(@Valid @NotNull RealmAssignmentDTO assignment) { - log.info("POST /api/realm-assignments - Assignation du realm {} à l'utilisateur {}", - assignment.getRealmName(), assignment.getUserId()); - - try { - // Ajouter l'utilisateur qui fait l'assignation - if (securityContext.getUserPrincipal() != null) { - assignment.setAssignedBy(securityContext.getUserPrincipal().getName()); - } - - RealmAssignmentDTO createdAssignment = realmAuthorizationService.assignRealmToUser(assignment); - return Response.status(Response.Status.CREATED).entity(createdAssignment).build(); - } catch (IllegalArgumentException e) { - log.warn("Données invalides lors de l'assignation: {}", e.getMessage()); - // Need to return 409 or 400 manually since this method returns Response - return Response.status(Response.Status.CONFLICT) - .entity(new dev.lions.user.manager.dto.common.ApiErrorDTO(e.getMessage())) - .build(); - } catch (Exception e) { - log.error("Erreur lors de l'assignation du realm", e); - throw new RuntimeException(e); - } - } - - @Override - @RolesAllowed({ "admin" }) - public void revokeRealmFromUser(String userId, String realmName) { - log.info("DELETE /api/realm-assignments/user/{}/realm/{}", userId, realmName); - realmAuthorizationService.revokeRealmFromUser(userId, realmName); - } - - @Override - @RolesAllowed({ "admin" }) - public void revokeAllRealmsFromUser(String userId) { - log.info("DELETE /api/realm-assignments/user/{}", userId); - realmAuthorizationService.revokeAllRealmsFromUser(userId); - } - - @Override - @RolesAllowed({ "admin" }) - public void deactivateAssignment(String assignmentId) { - log.info("PUT /api/realm-assignments/{}/deactivate", assignmentId); - realmAuthorizationService.deactivateAssignment(assignmentId); - } - - @Override - @RolesAllowed({ "admin" }) - public void activateAssignment(String assignmentId) { - log.info("PUT /api/realm-assignments/{}/activate", assignmentId); - realmAuthorizationService.activateAssignment(assignmentId); - } - - @Override - @RolesAllowed({ "admin" }) - public void setSuperAdmin(String userId, @NotNull Boolean superAdmin) { - log.info("PUT /api/realm-assignments/super-admin/{} - superAdmin: {}", userId, superAdmin); - realmAuthorizationService.setSuperAdmin(userId, superAdmin); - } -} +package dev.lions.user.manager.resource; + +import dev.lions.user.manager.api.RealmAssignmentResourceApi; +import dev.lions.user.manager.dto.realm.AuthorizedRealmsDTO; +import dev.lions.user.manager.dto.realm.RealmAccessCheckDTO; +import dev.lions.user.manager.dto.realm.RealmAssignmentDTO; +import dev.lions.user.manager.service.RealmAuthorizationService; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +/** + * REST Resource pour la gestion des affectations de realms aux utilisateurs + * Implémente l'interface API commune. + */ +@Slf4j +@jakarta.enterprise.context.ApplicationScoped +@jakarta.ws.rs.Path("/api/realm-assignments") +public class RealmAssignmentResource implements RealmAssignmentResourceApi { + + @Inject + RealmAuthorizationService realmAuthorizationService; + + @Context + SecurityContext securityContext; + + @Override + @RolesAllowed({ "admin" }) + public List getAllAssignments() { + log.info("GET /api/realm-assignments - Récupération de toutes les affectations"); + return realmAuthorizationService.getAllAssignments(); + } + + @Override + @RolesAllowed({ "admin", "user_manager" }) + public List getAssignmentsByUser(String userId) { + log.info("GET /api/realm-assignments/user/{}", userId); + return realmAuthorizationService.getAssignmentsByUser(userId); + } + + @Override + @RolesAllowed({ "admin" }) + public List getAssignmentsByRealm(String realmName) { + log.info("GET /api/realm-assignments/realm/{}", realmName); + return realmAuthorizationService.getAssignmentsByRealm(realmName); + } + + @Override + @RolesAllowed({ "admin" }) + public RealmAssignmentDTO getAssignmentById(String assignmentId) { + log.info("GET /api/realm-assignments/{}", assignmentId); + return realmAuthorizationService.getAssignmentById(assignmentId) + .orElseThrow(() -> new RuntimeException("Affectation non trouvée")); // ExceptionMapper should + // handle/map to 404 + } + + @Override + @RolesAllowed({ "admin", "user_manager" }) + public RealmAccessCheckDTO canManageRealm(String userId, String realmName) { + log.info("GET /api/realm-assignments/check - userId: {}, realmName: {}", userId, realmName); + boolean canManage = realmAuthorizationService.canManageRealm(userId, realmName); + return new RealmAccessCheckDTO(canManage, userId, realmName); + } + + @Override + @RolesAllowed({ "admin", "user_manager" }) + public AuthorizedRealmsDTO getAuthorizedRealms(String userId) { + log.info("GET /api/realm-assignments/authorized-realms/{}", userId); + List realms = realmAuthorizationService.getAuthorizedRealms(userId); + boolean isSuperAdmin = realmAuthorizationService.isSuperAdmin(userId); + return new AuthorizedRealmsDTO(realms, isSuperAdmin); + } + + @Override + @RolesAllowed({ "admin" }) + public Response assignRealmToUser(@Valid @NotNull RealmAssignmentDTO assignment) { + log.info("POST /api/realm-assignments - Assignation du realm {} à l'utilisateur {}", + assignment.getRealmName(), assignment.getUserId()); + + try { + // Ajouter l'utilisateur qui fait l'assignation + if (securityContext.getUserPrincipal() != null) { + assignment.setAssignedBy(securityContext.getUserPrincipal().getName()); + } + + RealmAssignmentDTO createdAssignment = realmAuthorizationService.assignRealmToUser(assignment); + return Response.status(Response.Status.CREATED).entity(createdAssignment).build(); + } catch (IllegalArgumentException e) { + log.warn("Données invalides lors de l'assignation: {}", e.getMessage()); + // Need to return 409 or 400 manually since this method returns Response + return Response.status(Response.Status.CONFLICT) + .entity(new dev.lions.user.manager.dto.common.ApiErrorDTO(e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de l'assignation du realm", e); + throw new RuntimeException(e); + } + } + + @Override + @RolesAllowed({ "admin" }) + public void revokeRealmFromUser(String userId, String realmName) { + log.info("DELETE /api/realm-assignments/user/{}/realm/{}", userId, realmName); + realmAuthorizationService.revokeRealmFromUser(userId, realmName); + } + + @Override + @RolesAllowed({ "admin" }) + public void revokeAllRealmsFromUser(String userId) { + log.info("DELETE /api/realm-assignments/user/{}", userId); + realmAuthorizationService.revokeAllRealmsFromUser(userId); + } + + @Override + @RolesAllowed({ "admin" }) + public void deactivateAssignment(String assignmentId) { + log.info("PUT /api/realm-assignments/{}/deactivate", assignmentId); + realmAuthorizationService.deactivateAssignment(assignmentId); + } + + @Override + @RolesAllowed({ "admin" }) + public void activateAssignment(String assignmentId) { + log.info("PUT /api/realm-assignments/{}/activate", assignmentId); + realmAuthorizationService.activateAssignment(assignmentId); + } + + @Override + @RolesAllowed({ "admin" }) + public void setSuperAdmin(String userId, @NotNull Boolean superAdmin) { + log.info("PUT /api/realm-assignments/super-admin/{} - superAdmin: {}", userId, superAdmin); + realmAuthorizationService.setSuperAdmin(userId, superAdmin); + } +} diff --git a/src/main/java/dev/lions/user/manager/resource/RealmResource.java b/src/main/java/dev/lions/user/manager/resource/RealmResource.java index 3bbb2fc..bd85d27 100644 --- a/src/main/java/dev/lions/user/manager/resource/RealmResource.java +++ b/src/main/java/dev/lions/user/manager/resource/RealmResource.java @@ -1,56 +1,56 @@ -package dev.lions.user.manager.resource; - -import dev.lions.user.manager.api.RealmResourceApi; -import dev.lions.user.manager.client.KeycloakAdminClient; -import io.quarkus.security.identity.SecurityIdentity; -import jakarta.annotation.security.RolesAllowed; -import jakarta.inject.Inject; -import lombok.extern.slf4j.Slf4j; - -import java.util.List; - -/** - * Ressource REST pour la gestion des realms Keycloak - * Implémente l'interface API commune. - */ -@Slf4j -@jakarta.enterprise.context.ApplicationScoped -@jakarta.ws.rs.Path("/api/realms") -public class RealmResource implements RealmResourceApi { - - @Inject - KeycloakAdminClient keycloakAdminClient; - - @Inject - SecurityIdentity securityIdentity; - - @Override - @RolesAllowed({ "admin", "user_manager", "user_viewer", "role_manager", "role_viewer" }) - public List getAllRealms() { - log.info("GET /api/realms/list"); - - try { - List realms = keycloakAdminClient.getAllRealms(); - log.info("Récupération réussie: {} realms trouvés", realms.size()); - return realms; - } catch (Exception e) { - log.error("Erreur lors de la récupération des realms", e); - throw new RuntimeException("Erreur lors de la récupération des realms: " + e.getMessage(), e); - } - } - - @Override - @RolesAllowed({ "admin", "user_manager", "role_manager", "role_viewer" }) - public List getRealmClients(String realmName) { - log.info("GET /api/realms/{}/clients", realmName); - - try { - List clients = keycloakAdminClient.getRealmClients(realmName); - log.info("Récupération réussie: {} clients trouvés pour le realm {}", clients.size(), realmName); - return clients; - } catch (Exception e) { - log.error("Erreur lors de la récupération des clients du realm {}", realmName, e); - throw new RuntimeException("Erreur lors de la récupération des clients: " + e.getMessage(), e); - } - } -} +package dev.lions.user.manager.resource; + +import dev.lions.user.manager.api.RealmResourceApi; +import dev.lions.user.manager.client.KeycloakAdminClient; +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +/** + * Ressource REST pour la gestion des realms Keycloak + * Implémente l'interface API commune. + */ +@Slf4j +@jakarta.enterprise.context.ApplicationScoped +@jakarta.ws.rs.Path("/api/realms") +public class RealmResource implements RealmResourceApi { + + @Inject + KeycloakAdminClient keycloakAdminClient; + + @Inject + SecurityIdentity securityIdentity; + + @Override + @RolesAllowed({ "admin", "user_manager", "user_viewer", "role_manager", "role_viewer" }) + public List getAllRealms() { + log.info("GET /api/realms/list"); + + try { + List realms = keycloakAdminClient.getAllRealms(); + log.info("Récupération réussie: {} realms trouvés", realms.size()); + return realms; + } catch (Exception e) { + log.error("Erreur lors de la récupération des realms", e); + throw new RuntimeException("Erreur lors de la récupération des realms: " + e.getMessage(), e); + } + } + + @Override + @RolesAllowed({ "admin", "user_manager", "role_manager", "role_viewer" }) + public List getRealmClients(String realmName) { + log.info("GET /api/realms/{}/clients", realmName); + + try { + List clients = keycloakAdminClient.getRealmClients(realmName); + log.info("Récupération réussie: {} clients trouvés pour le realm {}", clients.size(), realmName); + return clients; + } catch (Exception e) { + log.error("Erreur lors de la récupération des clients du realm {}", realmName, e); + throw new RuntimeException("Erreur lors de la récupération des clients: " + e.getMessage(), e); + } + } +} diff --git a/src/main/java/dev/lions/user/manager/resource/RoleResource.java b/src/main/java/dev/lions/user/manager/resource/RoleResource.java index 47dffe0..606a8a0 100644 --- a/src/main/java/dev/lions/user/manager/resource/RoleResource.java +++ b/src/main/java/dev/lions/user/manager/resource/RoleResource.java @@ -1,290 +1,290 @@ -package dev.lions.user.manager.resource; - -import dev.lions.user.manager.api.RoleResourceApi; -import dev.lions.user.manager.dto.common.ApiErrorDTO; -import dev.lions.user.manager.dto.role.RoleAssignmentDTO; -import dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO; -import dev.lions.user.manager.dto.role.RoleDTO; -import dev.lions.user.manager.enums.role.TypeRole; -import dev.lions.user.manager.service.RoleService; -import jakarta.annotation.security.RolesAllowed; -import jakarta.inject.Inject; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import lombok.extern.slf4j.Slf4j; - -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -/** - * REST Resource pour la gestion des rôles Keycloak - * Implémente l'interface API commune. - * Annotation explicite des méthodes pour éviter les problèmes d'héritage JAX-RS - * dans Quarkus. - */ -@Slf4j -@jakarta.enterprise.context.ApplicationScoped -@Path("/api/roles") -public class RoleResource implements RoleResourceApi { - - @Inject - RoleService roleService; - - // ==================== Endpoints Realm Roles ==================== - - @Override - @POST - @Path("/realm") - @RolesAllowed({ "admin", "role_manager", "ADMIN", "SUPER_ADMIN" }) - public Response createRealmRole( - @Valid @NotNull RoleDTO roleDTO, - @QueryParam("realm") String realmName) { - log.info("POST /api/roles/realm - Création du rôle realm: {} dans le realm: {}", - roleDTO.getName(), realmName); - - try { - RoleDTO createdRole = roleService.createRealmRole(roleDTO, realmName); - return Response.status(Response.Status.CREATED).entity(createdRole).build(); - } catch (IllegalArgumentException e) { - log.warn("Données invalides lors de la création du rôle: {}", e.getMessage()); - return Response.status(Response.Status.CONFLICT) - .entity(new ApiErrorDTO(e.getMessage())) - .build(); - } catch (Exception e) { - log.error("Erreur lors de la création du rôle realm", e); - throw new RuntimeException(e); - } - } - - @Override - @GET - @Path("/realm/{roleName}") - @RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" }) - public RoleDTO getRealmRole(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName) { - log.info("GET /api/roles/realm/{} - realm: {}", roleName, realmName); - return roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null) - .orElseThrow(() -> new RuntimeException("Rôle non trouvé")); - } - - @Override - @GET - @Path("/realm") - @RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" }) - public List getAllRealmRoles(@QueryParam("realm") String realmName) { - log.info("GET /api/roles/realm - realm: {}", realmName); - return roleService.getAllRealmRoles(realmName); - } - - @Override - @PUT - @Path("/realm/{roleName}") - @RolesAllowed({ "admin", "role_manager", "ADMIN", "SUPER_ADMIN" }) - public RoleDTO updateRealmRole(@PathParam("roleName") String roleName, @Valid @NotNull RoleDTO roleDTO, - @QueryParam("realm") String realmName) { - log.info("PUT /api/roles/realm/{} - realm: {}", roleName, realmName); - - Optional existingRole = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null); - if (existingRole.isEmpty()) { - throw new RuntimeException("Rôle non trouvé"); - } - - return roleService.updateRole(existingRole.get().getId(), roleDTO, realmName, TypeRole.REALM_ROLE, null); - } - - @Override - @DELETE - @Path("/realm/{roleName}") - @RolesAllowed({ "admin" }) - public void deleteRealmRole(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName) { - log.info("DELETE /api/roles/realm/{} - realm: {}", roleName, realmName); - - Optional existingRole = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null); - if (existingRole.isEmpty()) { - throw new RuntimeException("Rôle non trouvé"); - } - - roleService.deleteRole(existingRole.get().getId(), realmName, TypeRole.REALM_ROLE, null); - } - - // ==================== Endpoints Client Roles ==================== - - @Override - @POST - @Path("/client/{clientId}") - @RolesAllowed({ "admin", "role_manager", "ADMIN", "SUPER_ADMIN" }) - public Response createClientRole(@PathParam("clientId") String clientId, @Valid @NotNull RoleDTO roleDTO, - @QueryParam("realm") String realmName) { - log.info("POST /api/roles/client/{} - Création du rôle client dans le realm: {}", - clientId, realmName); - - try { - RoleDTO createdRole = roleService.createClientRole(roleDTO, clientId, realmName); - return Response.status(Response.Status.CREATED).entity(createdRole).build(); - } catch (IllegalArgumentException e) { - log.warn("Données invalides lors de la création du rôle client: {}", e.getMessage()); - return Response.status(Response.Status.CONFLICT) - .entity(new ApiErrorDTO(e.getMessage())) - .build(); - } catch (Exception e) { - log.error("Erreur lors de la création du rôle client", e); - throw new RuntimeException(e); - } - } - - @Override - @GET - @Path("/client/{clientId}/{roleName}") - @RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" }) - public RoleDTO getClientRole(@PathParam("clientId") String clientId, @PathParam("roleName") String roleName, - @QueryParam("realm") String realmName) { - log.info("GET /api/roles/client/{}/{} - realm: {}", clientId, roleName, realmName); - return roleService.getRoleByName(roleName, realmName, TypeRole.CLIENT_ROLE, clientId) - .orElseThrow(() -> new RuntimeException("Rôle client non trouvé")); - } - - @Override - @GET - @Path("/client/{clientId}") - @RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" }) - public List getAllClientRoles(@PathParam("clientId") String clientId, - @QueryParam("realm") String realmName) { - log.info("GET /api/roles/client/{} - realm: {}", clientId, realmName); - return roleService.getAllClientRoles(realmName, clientId); - } - - @Override - @DELETE - @Path("/client/{clientId}/{roleName}") - @RolesAllowed({ "admin" }) - public void deleteClientRole(@PathParam("clientId") String clientId, @PathParam("roleName") String roleName, - @QueryParam("realm") String realmName) { - log.info("DELETE /api/roles/client/{}/{} - realm: {}", clientId, roleName, realmName); - - Optional existingRole = roleService.getRoleByName(roleName, realmName, TypeRole.CLIENT_ROLE, clientId); - if (existingRole.isEmpty()) { - throw new RuntimeException("Rôle client non trouvé"); - } - - roleService.deleteRole(existingRole.get().getId(), realmName, TypeRole.CLIENT_ROLE, clientId); - } - - // ==================== Endpoints Attribution de rôles ==================== - - @Override - @POST - @Path("/assign/realm/{userId}") - @RolesAllowed({ "admin", "role_manager", "user_manager", "ADMIN", "SUPER_ADMIN" }) - public void assignRealmRoles(@PathParam("userId") String userId, @QueryParam("realm") String realmName, - @NotNull RoleAssignmentRequestDTO request) { - log.info("POST /api/roles/assign/realm/{} - Attribution de {} rôles", userId, request.getRoleNames().size()); - - RoleAssignmentDTO assignment = RoleAssignmentDTO.builder() - .userId(userId) - .roleNames(request.getRoleNames()) - .typeRole(TypeRole.REALM_ROLE) - .realmName(realmName) - .build(); - roleService.assignRolesToUser(assignment); - } - - @Override - @POST - @Path("/revoke/realm/{userId}") - @RolesAllowed({ "admin", "role_manager", "user_manager", "ADMIN", "SUPER_ADMIN" }) - public void revokeRealmRoles(@PathParam("userId") String userId, @QueryParam("realm") String realmName, - @NotNull RoleAssignmentRequestDTO request) { - log.info("POST /api/roles/revoke/realm/{} - Révocation de {} rôles", userId, request.getRoleNames().size()); - - RoleAssignmentDTO assignment = RoleAssignmentDTO.builder() - .userId(userId) - .roleNames(request.getRoleNames()) - .typeRole(TypeRole.REALM_ROLE) - .realmName(realmName) - .build(); - roleService.revokeRolesFromUser(assignment); - } - - @Override - @POST - @Path("/assign/client/{clientId}/{userId}") - @RolesAllowed({ "admin", "role_manager", "ADMIN", "SUPER_ADMIN" }) - public void assignClientRoles(@PathParam("clientId") String clientId, @PathParam("userId") String userId, - @QueryParam("realm") String realmName, - @NotNull RoleAssignmentRequestDTO request) { - log.info("POST /api/roles/assign/client/{}/{} - Attribution de {} rôles client", - clientId, userId, request.getRoleNames().size()); - - RoleAssignmentDTO assignment = RoleAssignmentDTO.builder() - .userId(userId) - .roleNames(request.getRoleNames()) - .typeRole(TypeRole.CLIENT_ROLE) - .realmName(realmName) - .clientName(clientId) - .build(); - roleService.assignRolesToUser(assignment); - } - - @Override - @GET - @Path("/user/realm/{userId}") - @RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" }) - public List getUserRealmRoles(@PathParam("userId") String userId, @QueryParam("realm") String realmName) { - log.info("GET /api/roles/user/realm/{} - realm: {}", userId, realmName); - return roleService.getUserRealmRoles(userId, realmName); - } - - @Override - @GET - @Path("/user/client/{clientId}/{userId}") - @RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" }) - public List getUserClientRoles(@PathParam("clientId") String clientId, @PathParam("userId") String userId, - @QueryParam("realm") String realmName) { - log.info("GET /api/roles/user/client/{}/{} - realm: {}", clientId, userId, realmName); - return roleService.getUserClientRoles(userId, clientId, realmName); - } - - // ==================== Endpoints Rôles composites ==================== - - @Override - @POST - @Path("/composite/{roleName}/add") - @RolesAllowed({ "admin", "role_manager", "ADMIN", "SUPER_ADMIN" }) - public void addComposites(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName, - @NotNull RoleAssignmentRequestDTO request) { - log.info("POST /api/roles/composite/{}/add - Ajout de {} composites", roleName, request.getRoleNames().size()); - - Optional parentRole = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null); - if (parentRole.isEmpty()) { - throw new RuntimeException("Rôle parent non trouvé"); - } - - List childRoleIds = request.getRoleNames().stream() - .map(name -> { - Optional role = roleService.getRoleByName(name, realmName, TypeRole.REALM_ROLE, null); - return role.map(RoleDTO::getId).orElse(null); - }) - .filter(id -> id != null) - .collect(Collectors.toList()); - - roleService.addCompositeRoles(parentRole.get().getId(), childRoleIds, realmName, TypeRole.REALM_ROLE, null); - } - - @Override - @GET - @Path("/composite/{roleName}") - @RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" }) - public List getComposites(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName) { - log.info("GET /api/roles/composite/{} - realm: {}", roleName, realmName); - - Optional role = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null); - if (role.isEmpty()) { - throw new RuntimeException("Rôle non trouvé"); - } - - return roleService.getCompositeRoles(role.get().getId(), realmName, TypeRole.REALM_ROLE, null); - } -} +package dev.lions.user.manager.resource; + +import dev.lions.user.manager.api.RoleResourceApi; +import dev.lions.user.manager.dto.common.ApiErrorDTO; +import dev.lions.user.manager.dto.role.RoleAssignmentDTO; +import dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO; +import dev.lions.user.manager.dto.role.RoleDTO; +import dev.lions.user.manager.enums.role.TypeRole; +import dev.lions.user.manager.service.RoleService; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * REST Resource pour la gestion des rôles Keycloak + * Implémente l'interface API commune. + * Annotation explicite des méthodes pour éviter les problèmes d'héritage JAX-RS + * dans Quarkus. + */ +@Slf4j +@jakarta.enterprise.context.ApplicationScoped +@Path("/api/roles") +public class RoleResource implements RoleResourceApi { + + @Inject + RoleService roleService; + + // ==================== Endpoints Realm Roles ==================== + + @Override + @POST + @Path("/realm") + @RolesAllowed({ "admin", "role_manager", "ADMIN", "SUPER_ADMIN" }) + public Response createRealmRole( + @Valid @NotNull RoleDTO roleDTO, + @QueryParam("realm") String realmName) { + log.info("POST /api/roles/realm - Création du rôle realm: {} dans le realm: {}", + roleDTO.getName(), realmName); + + try { + RoleDTO createdRole = roleService.createRealmRole(roleDTO, realmName); + return Response.status(Response.Status.CREATED).entity(createdRole).build(); + } catch (IllegalArgumentException e) { + log.warn("Données invalides lors de la création du rôle: {}", e.getMessage()); + return Response.status(Response.Status.CONFLICT) + .entity(new ApiErrorDTO(e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la création du rôle realm", e); + throw new RuntimeException(e); + } + } + + @Override + @GET + @Path("/realm/{roleName}") + @RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" }) + public RoleDTO getRealmRole(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName) { + log.info("GET /api/roles/realm/{} - realm: {}", roleName, realmName); + return roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null) + .orElseThrow(() -> new RuntimeException("Rôle non trouvé")); + } + + @Override + @GET + @Path("/realm") + @RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" }) + public List getAllRealmRoles(@QueryParam("realm") String realmName) { + log.info("GET /api/roles/realm - realm: {}", realmName); + return roleService.getAllRealmRoles(realmName); + } + + @Override + @PUT + @Path("/realm/{roleName}") + @RolesAllowed({ "admin", "role_manager", "ADMIN", "SUPER_ADMIN" }) + public RoleDTO updateRealmRole(@PathParam("roleName") String roleName, @Valid @NotNull RoleDTO roleDTO, + @QueryParam("realm") String realmName) { + log.info("PUT /api/roles/realm/{} - realm: {}", roleName, realmName); + + Optional existingRole = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null); + if (existingRole.isEmpty()) { + throw new RuntimeException("Rôle non trouvé"); + } + + return roleService.updateRole(existingRole.get().getId(), roleDTO, realmName, TypeRole.REALM_ROLE, null); + } + + @Override + @DELETE + @Path("/realm/{roleName}") + @RolesAllowed({ "admin" }) + public void deleteRealmRole(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName) { + log.info("DELETE /api/roles/realm/{} - realm: {}", roleName, realmName); + + Optional existingRole = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null); + if (existingRole.isEmpty()) { + throw new RuntimeException("Rôle non trouvé"); + } + + roleService.deleteRole(existingRole.get().getId(), realmName, TypeRole.REALM_ROLE, null); + } + + // ==================== Endpoints Client Roles ==================== + + @Override + @POST + @Path("/client/{clientId}") + @RolesAllowed({ "admin", "role_manager", "ADMIN", "SUPER_ADMIN" }) + public Response createClientRole(@PathParam("clientId") String clientId, @Valid @NotNull RoleDTO roleDTO, + @QueryParam("realm") String realmName) { + log.info("POST /api/roles/client/{} - Création du rôle client dans le realm: {}", + clientId, realmName); + + try { + RoleDTO createdRole = roleService.createClientRole(roleDTO, clientId, realmName); + return Response.status(Response.Status.CREATED).entity(createdRole).build(); + } catch (IllegalArgumentException e) { + log.warn("Données invalides lors de la création du rôle client: {}", e.getMessage()); + return Response.status(Response.Status.CONFLICT) + .entity(new ApiErrorDTO(e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la création du rôle client", e); + throw new RuntimeException(e); + } + } + + @Override + @GET + @Path("/client/{clientId}/{roleName}") + @RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" }) + public RoleDTO getClientRole(@PathParam("clientId") String clientId, @PathParam("roleName") String roleName, + @QueryParam("realm") String realmName) { + log.info("GET /api/roles/client/{}/{} - realm: {}", clientId, roleName, realmName); + return roleService.getRoleByName(roleName, realmName, TypeRole.CLIENT_ROLE, clientId) + .orElseThrow(() -> new RuntimeException("Rôle client non trouvé")); + } + + @Override + @GET + @Path("/client/{clientId}") + @RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" }) + public List getAllClientRoles(@PathParam("clientId") String clientId, + @QueryParam("realm") String realmName) { + log.info("GET /api/roles/client/{} - realm: {}", clientId, realmName); + return roleService.getAllClientRoles(realmName, clientId); + } + + @Override + @DELETE + @Path("/client/{clientId}/{roleName}") + @RolesAllowed({ "admin" }) + public void deleteClientRole(@PathParam("clientId") String clientId, @PathParam("roleName") String roleName, + @QueryParam("realm") String realmName) { + log.info("DELETE /api/roles/client/{}/{} - realm: {}", clientId, roleName, realmName); + + Optional existingRole = roleService.getRoleByName(roleName, realmName, TypeRole.CLIENT_ROLE, clientId); + if (existingRole.isEmpty()) { + throw new RuntimeException("Rôle client non trouvé"); + } + + roleService.deleteRole(existingRole.get().getId(), realmName, TypeRole.CLIENT_ROLE, clientId); + } + + // ==================== Endpoints Attribution de rôles ==================== + + @Override + @POST + @Path("/assign/realm/{userId}") + @RolesAllowed({ "admin", "role_manager", "user_manager", "ADMIN", "SUPER_ADMIN" }) + public void assignRealmRoles(@PathParam("userId") String userId, @QueryParam("realm") String realmName, + @NotNull RoleAssignmentRequestDTO request) { + log.info("POST /api/roles/assign/realm/{} - Attribution de {} rôles", userId, request.getRoleNames().size()); + + RoleAssignmentDTO assignment = RoleAssignmentDTO.builder() + .userId(userId) + .roleNames(request.getRoleNames()) + .typeRole(TypeRole.REALM_ROLE) + .realmName(realmName) + .build(); + roleService.assignRolesToUser(assignment); + } + + @Override + @POST + @Path("/revoke/realm/{userId}") + @RolesAllowed({ "admin", "role_manager", "user_manager", "ADMIN", "SUPER_ADMIN" }) + public void revokeRealmRoles(@PathParam("userId") String userId, @QueryParam("realm") String realmName, + @NotNull RoleAssignmentRequestDTO request) { + log.info("POST /api/roles/revoke/realm/{} - Révocation de {} rôles", userId, request.getRoleNames().size()); + + RoleAssignmentDTO assignment = RoleAssignmentDTO.builder() + .userId(userId) + .roleNames(request.getRoleNames()) + .typeRole(TypeRole.REALM_ROLE) + .realmName(realmName) + .build(); + roleService.revokeRolesFromUser(assignment); + } + + @Override + @POST + @Path("/assign/client/{clientId}/{userId}") + @RolesAllowed({ "admin", "role_manager", "ADMIN", "SUPER_ADMIN" }) + public void assignClientRoles(@PathParam("clientId") String clientId, @PathParam("userId") String userId, + @QueryParam("realm") String realmName, + @NotNull RoleAssignmentRequestDTO request) { + log.info("POST /api/roles/assign/client/{}/{} - Attribution de {} rôles client", + clientId, userId, request.getRoleNames().size()); + + RoleAssignmentDTO assignment = RoleAssignmentDTO.builder() + .userId(userId) + .roleNames(request.getRoleNames()) + .typeRole(TypeRole.CLIENT_ROLE) + .realmName(realmName) + .clientName(clientId) + .build(); + roleService.assignRolesToUser(assignment); + } + + @Override + @GET + @Path("/user/realm/{userId}") + @RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" }) + public List getUserRealmRoles(@PathParam("userId") String userId, @QueryParam("realm") String realmName) { + log.info("GET /api/roles/user/realm/{} - realm: {}", userId, realmName); + return roleService.getUserRealmRoles(userId, realmName); + } + + @Override + @GET + @Path("/user/client/{clientId}/{userId}") + @RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" }) + public List getUserClientRoles(@PathParam("clientId") String clientId, @PathParam("userId") String userId, + @QueryParam("realm") String realmName) { + log.info("GET /api/roles/user/client/{}/{} - realm: {}", clientId, userId, realmName); + return roleService.getUserClientRoles(userId, clientId, realmName); + } + + // ==================== Endpoints Rôles composites ==================== + + @Override + @POST + @Path("/composite/{roleName}/add") + @RolesAllowed({ "admin", "role_manager", "ADMIN", "SUPER_ADMIN" }) + public void addComposites(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName, + @NotNull RoleAssignmentRequestDTO request) { + log.info("POST /api/roles/composite/{}/add - Ajout de {} composites", roleName, request.getRoleNames().size()); + + Optional parentRole = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null); + if (parentRole.isEmpty()) { + throw new RuntimeException("Rôle parent non trouvé"); + } + + List childRoleIds = request.getRoleNames().stream() + .map(name -> { + Optional role = roleService.getRoleByName(name, realmName, TypeRole.REALM_ROLE, null); + return role.map(RoleDTO::getId).orElse(null); + }) + .filter(id -> id != null) + .collect(Collectors.toList()); + + roleService.addCompositeRoles(parentRole.get().getId(), childRoleIds, realmName, TypeRole.REALM_ROLE, null); + } + + @Override + @GET + @Path("/composite/{roleName}") + @RolesAllowed({ "admin", "role_manager", "role_viewer", "ADMIN", "SUPER_ADMIN", "USER" }) + public List getComposites(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName) { + log.info("GET /api/roles/composite/{} - realm: {}", roleName, realmName); + + Optional role = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null); + if (role.isEmpty()) { + throw new RuntimeException("Rôle non trouvé"); + } + + return roleService.getCompositeRoles(role.get().getId(), realmName, TypeRole.REALM_ROLE, null); + } +} diff --git a/src/main/java/dev/lions/user/manager/resource/SyncResource.java b/src/main/java/dev/lions/user/manager/resource/SyncResource.java index ffa0832..5e1b4cf 100644 --- a/src/main/java/dev/lions/user/manager/resource/SyncResource.java +++ b/src/main/java/dev/lions/user/manager/resource/SyncResource.java @@ -1,166 +1,166 @@ -package dev.lions.user.manager.resource; - -import dev.lions.user.manager.api.SyncResourceApi; -import dev.lions.user.manager.dto.sync.HealthStatusDTO; -import dev.lions.user.manager.dto.sync.SyncConsistencyDTO; -import dev.lions.user.manager.dto.sync.SyncHistoryDTO; -import dev.lions.user.manager.dto.sync.SyncResultDTO; -import dev.lions.user.manager.service.SyncService; -import jakarta.annotation.security.PermitAll; -import jakarta.annotation.security.RolesAllowed; -import jakarta.inject.Inject; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import lombok.extern.slf4j.Slf4j; - -import java.util.Map; - -/** - * REST Resource pour la synchronisation avec Keycloak. - * Suit le même pattern que AuditResource : les annotations JAX-RS des méthodes - * héritées de l'interface ne sont PAS répétées ici (conformité RESTEasy Reactive). - */ -@Slf4j -@jakarta.enterprise.context.ApplicationScoped -@jakarta.ws.rs.Path("/api/sync") -@jakarta.ws.rs.Produces(jakarta.ws.rs.core.MediaType.APPLICATION_JSON) -@jakarta.ws.rs.Consumes(jakarta.ws.rs.core.MediaType.APPLICATION_JSON) -public class SyncResource implements SyncResourceApi { - - @Inject - SyncService syncService; - - @GET - @Path("/ping") - @PermitAll - public String ping() { - return "{\"status\":\"pong\",\"resource\":\"SyncResource\"}"; - } - - @Override - @PermitAll - public HealthStatusDTO checkKeycloakHealth() { - log.info("REST: checkKeycloakHealth sur /api/sync/health/keycloak"); - try { - boolean available = syncService.isKeycloakAvailable(); - Map details = syncService.getKeycloakHealthInfo(); - return HealthStatusDTO.builder() - .keycloakAccessible(available) - .overallHealthy(available) - .keycloakVersion((String) details.getOrDefault("version", "Unknown")) - .timestamp(System.currentTimeMillis()) - .build(); - } catch (Exception e) { - log.error("Erreur lors du check health keycloak", e); - return HealthStatusDTO.builder() - .overallHealthy(false) - .errorMessage("Erreur: " + e.getMessage()) - .timestamp(System.currentTimeMillis()) - .build(); - } - } - - @Override - @RolesAllowed({ "admin", "sync_manager" }) - public SyncResultDTO syncUsers(String realmName) { - log.info("REST: syncUsers pour le realm: {}", realmName); - long start = System.currentTimeMillis(); - try { - int count = syncService.syncUsersFromRealm(realmName); - return SyncResultDTO.builder() - .success(true) - .usersCount(count) - .realmName(realmName) - .startTime(start) - .endTime(System.currentTimeMillis()) - .build(); - } catch (Exception e) { - log.error("Erreur lors de la synchro users realm {}", realmName, e); - return SyncResultDTO.builder() - .success(false) - .errorMessage(e.getMessage()) - .realmName(realmName) - .startTime(start) - .endTime(System.currentTimeMillis()) - .build(); - } - } - - @Override - @RolesAllowed({ "admin", "sync_manager" }) - public SyncResultDTO syncRoles(String realmName, String clientName) { - log.info("REST: syncRoles pour le realm: {}, client: {}", realmName, clientName); - long start = System.currentTimeMillis(); - try { - int count = syncService.syncRolesFromRealm(realmName); - return SyncResultDTO.builder() - .success(true) - .realmRolesCount(count) - .realmName(realmName) - .startTime(start) - .endTime(System.currentTimeMillis()) - .build(); - } catch (Exception e) { - log.error("Erreur lors de la synchro roles realm {}", realmName, e); - return SyncResultDTO.builder() - .success(false) - .errorMessage(e.getMessage()) - .realmName(realmName) - .startTime(start) - .endTime(System.currentTimeMillis()) - .build(); - } - } - - @Override - @RolesAllowed({ "admin", "sync_manager" }) - public SyncConsistencyDTO checkDataConsistency(String realmName) { - log.info("REST: checkDataConsistency pour realm: {}", realmName); - try { - Map report = syncService.checkDataConsistency(realmName); - return SyncConsistencyDTO.builder() - .realmName((String) report.get("realmName")) - .status((String) report.get("status")) - .usersKeycloakCount((Integer) report.get("usersKeycloakCount")) - .usersLocalCount((Integer) report.get("usersLocalCount")) - .error((String) report.get("error")) - .build(); - } catch (Exception e) { - log.error("Erreur checkDataConsistency realm {}", realmName, e); - return SyncConsistencyDTO.builder() - .realmName(realmName) - .status("ERROR") - .error(e.getMessage()) - .build(); - } - } - - @Override - @RolesAllowed({ "admin", "sync_manager", "user_viewer" }) - public SyncHistoryDTO getLastSyncStatus(String realmName) { - log.info("REST: getLastSyncStatus pour realm: {}", realmName); - return SyncHistoryDTO.builder() - .realmName(realmName) - .status("NEVER_SYNCED") - .build(); - } - - @Override - @RolesAllowed({ "admin", "sync_manager" }) - public SyncHistoryDTO forceSyncRealm(String realmName) { - log.info("REST: forceSyncRealm pour realm: {}", realmName); - try { - syncService.forceSyncRealm(realmName); - return SyncHistoryDTO.builder() - .realmName(realmName) - .status("SUCCESS") - .build(); - } catch (Exception e) { - log.error("Erreur forceSyncRealm realm {}", realmName, e); - return SyncHistoryDTO.builder() - .realmName(realmName) - .status("FAILED") - .build(); - } - } -} +package dev.lions.user.manager.resource; + +import dev.lions.user.manager.api.SyncResourceApi; +import dev.lions.user.manager.dto.sync.HealthStatusDTO; +import dev.lions.user.manager.dto.sync.SyncConsistencyDTO; +import dev.lions.user.manager.dto.sync.SyncHistoryDTO; +import dev.lions.user.manager.dto.sync.SyncResultDTO; +import dev.lions.user.manager.service.SyncService; +import jakarta.annotation.security.PermitAll; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +/** + * REST Resource pour la synchronisation avec Keycloak. + * Suit le même pattern que AuditResource : les annotations JAX-RS des méthodes + * héritées de l'interface ne sont PAS répétées ici (conformité RESTEasy Reactive). + */ +@Slf4j +@jakarta.enterprise.context.ApplicationScoped +@jakarta.ws.rs.Path("/api/sync") +@jakarta.ws.rs.Produces(jakarta.ws.rs.core.MediaType.APPLICATION_JSON) +@jakarta.ws.rs.Consumes(jakarta.ws.rs.core.MediaType.APPLICATION_JSON) +public class SyncResource implements SyncResourceApi { + + @Inject + SyncService syncService; + + @GET + @Path("/ping") + @PermitAll + public String ping() { + return "{\"status\":\"pong\",\"resource\":\"SyncResource\"}"; + } + + @Override + @PermitAll + public HealthStatusDTO checkKeycloakHealth() { + log.info("REST: checkKeycloakHealth sur /api/sync/health/keycloak"); + try { + boolean available = syncService.isKeycloakAvailable(); + Map details = syncService.getKeycloakHealthInfo(); + return HealthStatusDTO.builder() + .keycloakAccessible(available) + .overallHealthy(available) + .keycloakVersion((String) details.getOrDefault("version", "Unknown")) + .timestamp(System.currentTimeMillis()) + .build(); + } catch (Exception e) { + log.error("Erreur lors du check health keycloak", e); + return HealthStatusDTO.builder() + .overallHealthy(false) + .errorMessage("Erreur: " + e.getMessage()) + .timestamp(System.currentTimeMillis()) + .build(); + } + } + + @Override + @RolesAllowed({ "admin", "sync_manager" }) + public SyncResultDTO syncUsers(String realmName) { + log.info("REST: syncUsers pour le realm: {}", realmName); + long start = System.currentTimeMillis(); + try { + int count = syncService.syncUsersFromRealm(realmName); + return SyncResultDTO.builder() + .success(true) + .usersCount(count) + .realmName(realmName) + .startTime(start) + .endTime(System.currentTimeMillis()) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la synchro users realm {}", realmName, e); + return SyncResultDTO.builder() + .success(false) + .errorMessage(e.getMessage()) + .realmName(realmName) + .startTime(start) + .endTime(System.currentTimeMillis()) + .build(); + } + } + + @Override + @RolesAllowed({ "admin", "sync_manager" }) + public SyncResultDTO syncRoles(String realmName, String clientName) { + log.info("REST: syncRoles pour le realm: {}, client: {}", realmName, clientName); + long start = System.currentTimeMillis(); + try { + int count = syncService.syncRolesFromRealm(realmName); + return SyncResultDTO.builder() + .success(true) + .realmRolesCount(count) + .realmName(realmName) + .startTime(start) + .endTime(System.currentTimeMillis()) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la synchro roles realm {}", realmName, e); + return SyncResultDTO.builder() + .success(false) + .errorMessage(e.getMessage()) + .realmName(realmName) + .startTime(start) + .endTime(System.currentTimeMillis()) + .build(); + } + } + + @Override + @RolesAllowed({ "admin", "sync_manager" }) + public SyncConsistencyDTO checkDataConsistency(String realmName) { + log.info("REST: checkDataConsistency pour realm: {}", realmName); + try { + Map report = syncService.checkDataConsistency(realmName); + return SyncConsistencyDTO.builder() + .realmName((String) report.get("realmName")) + .status((String) report.get("status")) + .usersKeycloakCount((Integer) report.get("usersKeycloakCount")) + .usersLocalCount((Integer) report.get("usersLocalCount")) + .error((String) report.get("error")) + .build(); + } catch (Exception e) { + log.error("Erreur checkDataConsistency realm {}", realmName, e); + return SyncConsistencyDTO.builder() + .realmName(realmName) + .status("ERROR") + .error(e.getMessage()) + .build(); + } + } + + @Override + @RolesAllowed({ "admin", "sync_manager", "user_viewer" }) + public SyncHistoryDTO getLastSyncStatus(String realmName) { + log.info("REST: getLastSyncStatus pour realm: {}", realmName); + return SyncHistoryDTO.builder() + .realmName(realmName) + .status("NEVER_SYNCED") + .build(); + } + + @Override + @RolesAllowed({ "admin", "sync_manager" }) + public SyncHistoryDTO forceSyncRealm(String realmName) { + log.info("REST: forceSyncRealm pour realm: {}", realmName); + try { + syncService.forceSyncRealm(realmName); + return SyncHistoryDTO.builder() + .realmName(realmName) + .status("SUCCESS") + .build(); + } catch (Exception e) { + log.error("Erreur forceSyncRealm realm {}", realmName, e); + return SyncHistoryDTO.builder() + .realmName(realmName) + .status("FAILED") + .build(); + } + } +} diff --git a/src/main/java/dev/lions/user/manager/resource/UserMetricsResource.java b/src/main/java/dev/lions/user/manager/resource/UserMetricsResource.java index a82d6c4..130c150 100644 --- a/src/main/java/dev/lions/user/manager/resource/UserMetricsResource.java +++ b/src/main/java/dev/lions/user/manager/resource/UserMetricsResource.java @@ -1,73 +1,73 @@ -package dev.lions.user.manager.resource; - -import dev.lions.user.manager.api.UserMetricsResourceApi; -import dev.lions.user.manager.client.KeycloakAdminClient; -import dev.lions.user.manager.dto.common.UserSessionStatsDTO; -import jakarta.annotation.security.RolesAllowed; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.ws.rs.Path; -import lombok.extern.slf4j.Slf4j; -import org.keycloak.admin.client.resource.RealmResource; -import org.keycloak.admin.client.resource.UserResource; -import org.keycloak.admin.client.resource.UsersResource; -import org.keycloak.representations.idm.UserRepresentation; - -import java.util.List; - -/** - * Ressource REST fournissant des métriques agrégées sur les utilisateurs. - * Implémente l'interface API commune. - * - * Toutes les valeurs sont calculées en temps réel à partir de Keycloak - * (aucune approximation ni cache local). - */ -@Slf4j -@ApplicationScoped -@Path("/api/metrics/users") -public class UserMetricsResource implements UserMetricsResourceApi { - - @Inject - KeycloakAdminClient keycloakAdminClient; - - @Override - @RolesAllowed({ "admin", "user_manager", "auditor" }) - public UserSessionStatsDTO getUserSessionStats(String realmName) { - String effectiveRealm = (realmName == null || realmName.isBlank()) ? "master" : realmName; - log.info("GET /api/metrics/users/sessions - realm={}", effectiveRealm); - - try { - RealmResource realm = keycloakAdminClient.getRealm(effectiveRealm); - UsersResource usersResource = realm.users(); - - // Liste complète des utilisateurs du realm (source de vérité Keycloak) - List users = usersResource.list(); - long totalUsers = users.size(); - - long activeSessions = 0L; - long onlineUsers = 0L; - - for (UserRepresentation user : users) { - UserResource userResource = usersResource.get(user.getId()); - int sessionsForUser = userResource.getUserSessions().size(); - - activeSessions += sessionsForUser; - if (sessionsForUser > 0) { - onlineUsers++; - } - } - - return UserSessionStatsDTO.builder() - .realmName(effectiveRealm) - .totalUsers(totalUsers) - .activeSessions(activeSessions) - .onlineUsers(onlineUsers) - .build(); - } catch (Exception e) { - log.error("Erreur lors du calcul des statistiques de sessions pour le realm {}", effectiveRealm, e); - // On laisse l'exception remonter pour signaler une vraie erreur (pas de valeur approximative) - throw new RuntimeException("Impossible de calculer les statistiques de sessions en temps réel", e); - } - } -} - +package dev.lions.user.manager.resource; + +import dev.lions.user.manager.api.UserMetricsResourceApi; +import dev.lions.user.manager.client.KeycloakAdminClient; +import dev.lions.user.manager.dto.common.UserSessionStatsDTO; +import jakarta.annotation.security.RolesAllowed; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Path; +import lombok.extern.slf4j.Slf4j; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.representations.idm.UserRepresentation; + +import java.util.List; + +/** + * Ressource REST fournissant des métriques agrégées sur les utilisateurs. + * Implémente l'interface API commune. + * + * Toutes les valeurs sont calculées en temps réel à partir de Keycloak + * (aucune approximation ni cache local). + */ +@Slf4j +@ApplicationScoped +@Path("/api/metrics/users") +public class UserMetricsResource implements UserMetricsResourceApi { + + @Inject + KeycloakAdminClient keycloakAdminClient; + + @Override + @RolesAllowed({ "admin", "user_manager", "auditor" }) + public UserSessionStatsDTO getUserSessionStats(String realmName) { + String effectiveRealm = (realmName == null || realmName.isBlank()) ? "master" : realmName; + log.info("GET /api/metrics/users/sessions - realm={}", effectiveRealm); + + try { + RealmResource realm = keycloakAdminClient.getRealm(effectiveRealm); + UsersResource usersResource = realm.users(); + + // Liste complète des utilisateurs du realm (source de vérité Keycloak) + List users = usersResource.list(); + long totalUsers = users.size(); + + long activeSessions = 0L; + long onlineUsers = 0L; + + for (UserRepresentation user : users) { + UserResource userResource = usersResource.get(user.getId()); + int sessionsForUser = userResource.getUserSessions().size(); + + activeSessions += sessionsForUser; + if (sessionsForUser > 0) { + onlineUsers++; + } + } + + return UserSessionStatsDTO.builder() + .realmName(effectiveRealm) + .totalUsers(totalUsers) + .activeSessions(activeSessions) + .onlineUsers(onlineUsers) + .build(); + } catch (Exception e) { + log.error("Erreur lors du calcul des statistiques de sessions pour le realm {}", effectiveRealm, e); + // On laisse l'exception remonter pour signaler une vraie erreur (pas de valeur approximative) + throw new RuntimeException("Impossible de calculer les statistiques de sessions en temps réel", e); + } + } +} + 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 96d6bfc..d0742dd 100644 --- a/src/main/java/dev/lions/user/manager/resource/UserResource.java +++ b/src/main/java/dev/lions/user/manager/resource/UserResource.java @@ -1,162 +1,161 @@ -package dev.lions.user.manager.resource; - -import dev.lions.user.manager.api.UserResourceApi; -import dev.lions.user.manager.dto.common.ApiErrorDTO; -import dev.lions.user.manager.dto.importexport.ImportResultDTO; -import dev.lions.user.manager.dto.user.*; -import dev.lions.user.manager.service.UserService; -import jakarta.annotation.security.RolesAllowed; -import jakarta.inject.Inject; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import lombok.extern.slf4j.Slf4j; - -import java.util.List; - -/** - * REST Resource pour la gestion des utilisateurs - * Implémente l'interface API commune. - */ -@Slf4j -@jakarta.enterprise.context.ApplicationScoped -@jakarta.ws.rs.Path("/api/users") -public class UserResource implements UserResourceApi { - - @Inject - UserService userService; - - @Override - @RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" }) - public UserSearchResultDTO searchUsers(@Valid @NotNull UserSearchCriteriaDTO criteria) { - log.info("POST /api/users/search - Recherche d'utilisateurs"); - return userService.searchUsers(criteria); - } - - @Override - @RolesAllowed({ "admin", "user_manager", "user_viewer", "ADMIN", "SUPER_ADMIN", "USER" }) - public UserDTO getUserById(String userId, String realmName) { - log.info("GET /api/users/{} - realm: {}", userId, realmName); - return userService.getUserById(userId, realmName) - .orElseThrow(() -> new RuntimeException("Utilisateur non trouvé")); // ExceptionMapper should handle/map - // to 404 - } - - @Override - @RolesAllowed({ "admin", "user_manager", "user_viewer", "ADMIN", "SUPER_ADMIN", "USER" }) - public UserSearchResultDTO getAllUsers(String realmName, int page, int pageSize) { - log.info("GET /api/users - realm: {}, page: {}, pageSize: {}", realmName, page, pageSize); - return userService.getAllUsers(realmName, page, pageSize); - } - - @Override - @RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" }) - public Response createUser(@Valid @NotNull UserDTO user, String realmName) { - log.info("POST /api/users - Création d'un utilisateur: {}", user.getUsername()); - - try { - UserDTO createdUser = userService.createUser(user, realmName); - return Response.status(Response.Status.CREATED).entity(createdUser).build(); - } catch (IllegalArgumentException e) { - log.warn("Données invalides lors de la création: {}", e.getMessage()); - return Response.status(Response.Status.CONFLICT) - .entity(new ApiErrorDTO(e.getMessage())) - .build(); - } catch (Exception e) { - log.error("Erreur lors de la création de l'utilisateur", e); - throw new RuntimeException(e); - } - } - - @Override - @RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" }) - public UserDTO updateUser(String userId, @Valid @NotNull UserDTO user, String realmName) { - log.info("PUT /api/users/{} - Mise à jour", userId); - return userService.updateUser(userId, user, realmName); - } - - @Override - @RolesAllowed({ "admin", "ADMIN", "SUPER_ADMIN" }) - public void deleteUser(String userId, String realmName, boolean hardDelete) { - log.info("DELETE /api/users/{} - realm: {}, hardDelete: {}", userId, realmName, hardDelete); - userService.deleteUser(userId, realmName, hardDelete); - } - - @Override - @RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" }) - public void activateUser(String userId, String realmName) { - log.info("POST /api/users/{}/activate", userId); - userService.activateUser(userId, realmName); - } - - @Override - @RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" }) - public void deactivateUser(String userId, String realmName, String raison) { - log.info("POST /api/users/{}/deactivate - raison: {}", userId, raison); - userService.deactivateUser(userId, realmName, raison); - } - - @Override - @RolesAllowed({ "admin", "user_manager" }) - public void resetPassword(String userId, String realmName, @NotNull PasswordResetRequestDTO request) { - log.info("POST /api/users/{}/reset-password - temporary: {}", userId, request.isTemporary()); - userService.resetPassword(userId, realmName, request.getPassword(), request.isTemporary()); - } - - @Override - @RolesAllowed({ "admin", "user_manager" }) - public Response sendVerificationEmail(String userId, String realmName) { - log.info("POST /api/users/{}/send-verification-email", userId); - userService.sendVerificationEmail(userId, realmName); - return Response.accepted().build(); - } - - @Override - @RolesAllowed({ "admin", "user_manager" }) - public SessionsRevokedDTO logoutAllSessions(String userId, String realmName) { - log.info("POST /api/users/{}/logout-sessions", userId); - int count = userService.logoutAllSessions(userId, realmName); - return new SessionsRevokedDTO(count); - } - - @Override - @RolesAllowed({ "admin", "user_manager", "user_viewer" }) - public List getActiveSessions(String userId, String realmName) { - log.info("GET /api/users/{}/sessions", userId); - return userService.getActiveSessions(userId, realmName); - } - - @Override - @GET - @jakarta.ws.rs.Path("/export/csv") - @jakarta.ws.rs.Produces("text/csv") - @RolesAllowed({ "admin", "user_manager" }) - public Response exportUsersToCSV(@QueryParam("realm") String realmName) { - log.info("GET /api/users/export/csv - realm: {}", realmName); - UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder() - .realmName(realmName) - .page(0) - .pageSize(10_000) - .build(); - String csv = userService.exportUsersToCSV(criteria); - return Response.ok(csv) - .type(MediaType.valueOf("text/csv")) - .header("Content-Disposition", "attachment; filename=\"users-" + (realmName != null ? realmName : "export") + ".csv\"") - .build(); - } - - @Override - @POST - @jakarta.ws.rs.Path("/import/csv") - @jakarta.ws.rs.Consumes(MediaType.TEXT_PLAIN) - @RolesAllowed({ "admin", "user_manager" }) - public ImportResultDTO importUsersFromCSV(@QueryParam("realm") String realmName, String csvContent) { - log.info("POST /api/users/import/csv - realm: {}", realmName); - return userService.importUsersFromCSV(csvContent, realmName); - } -} +package dev.lions.user.manager.resource; + +import dev.lions.user.manager.api.UserResourceApi; +import dev.lions.user.manager.dto.common.ApiErrorDTO; +import dev.lions.user.manager.dto.importexport.ImportResultDTO; +import dev.lions.user.manager.dto.user.*; +import dev.lions.user.manager.service.UserService; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +/** + * REST Resource pour la gestion des utilisateurs + * Implémente l'interface API commune. + */ +@Slf4j +@jakarta.enterprise.context.ApplicationScoped +@jakarta.ws.rs.Path("/api/users") +public class UserResource implements UserResourceApi { + + @Inject + UserService userService; + + @Override + @RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" }) + public UserSearchResultDTO searchUsers(@Valid @NotNull UserSearchCriteriaDTO criteria) { + log.info("POST /api/users/search - Recherche d'utilisateurs"); + return userService.searchUsers(criteria); + } + + @Override + @RolesAllowed({ "admin", "user_manager", "user_viewer", "ADMIN", "SUPER_ADMIN", "USER" }) + public UserDTO getUserById(String userId, String realmName) { + log.info("GET /api/users/{} - realm: {}", userId, realmName); + return userService.getUserById(userId, realmName) + .orElseThrow(() -> new jakarta.ws.rs.NotFoundException("Utilisateur non trouvé")); + } + + @Override + @RolesAllowed({ "admin", "user_manager", "user_viewer", "ADMIN", "SUPER_ADMIN", "USER" }) + public UserSearchResultDTO getAllUsers(String realmName, int page, int pageSize) { + log.info("GET /api/users - realm: {}, page: {}, pageSize: {}", realmName, page, pageSize); + return userService.getAllUsers(realmName, page, pageSize); + } + + @Override + @RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" }) + public Response createUser(@Valid @NotNull UserDTO user, String realmName) { + log.info("POST /api/users - Création d'un utilisateur: {}", user.getUsername()); + + try { + UserDTO createdUser = userService.createUser(user, realmName); + return Response.status(Response.Status.CREATED).entity(createdUser).build(); + } catch (IllegalArgumentException e) { + log.warn("Données invalides lors de la création: {}", e.getMessage()); + return Response.status(Response.Status.CONFLICT) + .entity(new ApiErrorDTO(e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la création de l'utilisateur", e); + throw new RuntimeException(e); + } + } + + @Override + @RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" }) + public UserDTO updateUser(String userId, @Valid @NotNull UserDTO user, String realmName) { + log.info("PUT /api/users/{} - Mise à jour", userId); + return userService.updateUser(userId, user, realmName); + } + + @Override + @RolesAllowed({ "admin", "ADMIN", "SUPER_ADMIN" }) + public void deleteUser(String userId, String realmName, boolean hardDelete) { + log.info("DELETE /api/users/{} - realm: {}, hardDelete: {}", userId, realmName, hardDelete); + userService.deleteUser(userId, realmName, hardDelete); + } + + @Override + @RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" }) + public void activateUser(String userId, String realmName) { + log.info("POST /api/users/{}/activate", userId); + userService.activateUser(userId, realmName); + } + + @Override + @RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" }) + public void deactivateUser(String userId, String realmName, String raison) { + log.info("POST /api/users/{}/deactivate - raison: {}", userId, raison); + userService.deactivateUser(userId, realmName, raison); + } + + @Override + @RolesAllowed({ "admin", "user_manager" }) + public void resetPassword(String userId, String realmName, @NotNull PasswordResetRequestDTO request) { + log.info("POST /api/users/{}/reset-password - temporary: {}", userId, request.isTemporary()); + userService.resetPassword(userId, realmName, request.getPassword(), request.isTemporary()); + } + + @Override + @RolesAllowed({ "admin", "user_manager" }) + public Response sendVerificationEmail(String userId, String realmName) { + log.info("POST /api/users/{}/send-verification-email", userId); + userService.sendVerificationEmail(userId, realmName); + return Response.accepted().build(); + } + + @Override + @RolesAllowed({ "admin", "user_manager" }) + public SessionsRevokedDTO logoutAllSessions(String userId, String realmName) { + log.info("POST /api/users/{}/logout-sessions", userId); + int count = userService.logoutAllSessions(userId, realmName); + return new SessionsRevokedDTO(count); + } + + @Override + @RolesAllowed({ "admin", "user_manager", "user_viewer" }) + public List getActiveSessions(String userId, String realmName) { + log.info("GET /api/users/{}/sessions", userId); + return userService.getActiveSessions(userId, realmName); + } + + @Override + @GET + @jakarta.ws.rs.Path("/export/csv") + @jakarta.ws.rs.Produces("text/csv") + @RolesAllowed({ "admin", "user_manager" }) + public Response exportUsersToCSV(@QueryParam("realm") String realmName) { + log.info("GET /api/users/export/csv - realm: {}", realmName); + UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder() + .realmName(realmName) + .page(0) + .pageSize(10_000) + .build(); + String csv = userService.exportUsersToCSV(criteria); + return Response.ok(csv) + .type(MediaType.valueOf("text/csv")) + .header("Content-Disposition", "attachment; filename=\"users-" + (realmName != null ? realmName : "export") + ".csv\"") + .build(); + } + + @Override + @POST + @jakarta.ws.rs.Path("/import/csv") + @jakarta.ws.rs.Consumes(MediaType.TEXT_PLAIN) + @RolesAllowed({ "admin", "user_manager" }) + public ImportResultDTO importUsersFromCSV(@QueryParam("realm") String realmName, String csvContent) { + log.info("POST /api/users/import/csv - realm: {}", realmName); + return userService.importUsersFromCSV(csvContent, realmName); + } +} diff --git a/src/main/java/dev/lions/user/manager/security/DevModeSecurityAugmentor.java b/src/main/java/dev/lions/user/manager/security/DevModeSecurityAugmentor.java index c19bde4..40aabe4 100644 --- a/src/main/java/dev/lions/user/manager/security/DevModeSecurityAugmentor.java +++ b/src/main/java/dev/lions/user/manager/security/DevModeSecurityAugmentor.java @@ -1,38 +1,38 @@ -package dev.lions.user.manager.security; - -import io.quarkus.security.identity.AuthenticationRequestContext; -import io.quarkus.security.identity.SecurityIdentity; -import io.quarkus.security.identity.SecurityIdentityAugmentor; -import io.quarkus.security.runtime.QuarkusSecurityIdentity; -import io.quarkus.arc.profile.IfBuildProfile; -import io.smallrye.mutiny.Uni; -import jakarta.enterprise.context.ApplicationScoped; -import org.eclipse.microprofile.config.inject.ConfigProperty; - -import java.util.Set; - -/** - * Augmenteur de sécurité pour le mode DEV - * Ajoute automatiquement les rôles admin et user_manager à toutes les requêtes - * Permet de tester l'API sans authentification Keycloak - */ -@ApplicationScoped -@IfBuildProfile("dev") -public class DevModeSecurityAugmentor implements SecurityIdentityAugmentor { - - @ConfigProperty(name = "quarkus.oidc.enabled", defaultValue = "true") - boolean oidcEnabled; - - @Override - public Uni augment(SecurityIdentity identity, AuthenticationRequestContext context) { - // Seulement actif si OIDC est désactivé (mode DEV) - if (!oidcEnabled && identity.isAnonymous()) { - // Créer une identité avec les rôles nécessaires pour DEV - return Uni.createFrom().item(QuarkusSecurityIdentity.builder(identity) - .setPrincipal(() -> "dev-user") - .addRoles(Set.of("admin", "user_manager", "user_viewer")) - .build()); - } - return Uni.createFrom().item(identity); - } -} +package dev.lions.user.manager.security; + +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.quarkus.arc.profile.IfBuildProfile; +import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.ApplicationScoped; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import java.util.Set; + +/** + * Augmenteur de sécurité pour le mode DEV + * Ajoute automatiquement les rôles admin et user_manager à toutes les requêtes + * Permet de tester l'API sans authentification Keycloak + */ +@ApplicationScoped +@IfBuildProfile("dev") +public class DevModeSecurityAugmentor implements SecurityIdentityAugmentor { + + @ConfigProperty(name = "quarkus.oidc.enabled", defaultValue = "true") + boolean oidcEnabled; + + @Override + public Uni augment(SecurityIdentity identity, AuthenticationRequestContext context) { + // Seulement actif si OIDC est désactivé (mode DEV) + if (!oidcEnabled && identity.isAnonymous()) { + // Créer une identité avec les rôles nécessaires pour DEV + return Uni.createFrom().item(QuarkusSecurityIdentity.builder(identity) + .setPrincipal(() -> "dev-user") + .addRoles(Set.of("admin", "user_manager", "user_viewer")) + .build()); + } + return Uni.createFrom().item(identity); + } +} diff --git a/src/main/java/dev/lions/user/manager/security/DevSecurityContextProducer.java b/src/main/java/dev/lions/user/manager/security/DevSecurityContextProducer.java index 9c86fdb..7507e71 100644 --- a/src/main/java/dev/lions/user/manager/security/DevSecurityContextProducer.java +++ b/src/main/java/dev/lions/user/manager/security/DevSecurityContextProducer.java @@ -1,94 +1,94 @@ -package dev.lions.user.manager.security; - -import jakarta.annotation.Priority; -import jakarta.inject.Inject; -import jakarta.ws.rs.Priorities; -import jakarta.ws.rs.container.ContainerRequestContext; -import jakarta.ws.rs.container.ContainerRequestFilter; -import jakarta.ws.rs.core.SecurityContext; -import jakarta.ws.rs.ext.Provider; -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.jboss.logging.Logger; - -import java.security.Principal; - -/** - * Filtre JAX-RS pour remplacer le SecurityContext en mode développement - * En dev, remplace le SecurityContext par un mock qui autorise tous les rôles - * En prod, laisse le SecurityContext réel de Quarkus - */ -@Provider -@Priority(Priorities.AUTHENTICATION - 10) // S'exécute très tôt, avant l'authentification -public class DevSecurityContextProducer implements ContainerRequestFilter { - - private static final Logger LOG = Logger.getLogger(DevSecurityContextProducer.class); - - @Inject - @ConfigProperty(name = "quarkus.profile", defaultValue = "prod") - String profile; - - @Inject - @ConfigProperty(name = "quarkus.oidc.enabled", defaultValue = "true") - boolean oidcEnabled; - - @Override - public void filter(ContainerRequestContext requestContext) { - // Détecter le mode dev : si OIDC est désactivé, on est probablement en dev - // ou si le profil est explicitement "dev" ou "development" - boolean isDevMode = !oidcEnabled || "dev".equals(profile) || "development".equals(profile); - - if (isDevMode) { - String path = requestContext.getUriInfo().getPath(); - LOG.infof("Mode dev détecté (profile=%s, oidc.enabled=%s): remplacement du SecurityContext pour le chemin %s", - profile, oidcEnabled, path); - SecurityContext original = requestContext.getSecurityContext(); - requestContext.setSecurityContext(new DevSecurityContext(original)); - LOG.debugf("SecurityContext remplacé - isUserInRole('admin')=%s, isUserInRole('user_manager')=%s", - new DevSecurityContext(original).isUserInRole("admin"), - new DevSecurityContext(original).isUserInRole("user_manager")); - } else { - LOG.debugf("Mode prod - SecurityContext original conservé (profile=%s, oidc.enabled=%s)", profile, oidcEnabled); - } - } - - /** - * SecurityContext mock pour le mode développement - * Simule un utilisateur avec tous les rôles nécessaires - */ - private static class DevSecurityContext implements SecurityContext { - - private final SecurityContext original; - private final Principal principal = new Principal() { - @Override - public String getName() { - return "dev-user"; - } - }; - - public DevSecurityContext(SecurityContext original) { - this.original = original; - } - - @Override - public Principal getUserPrincipal() { - return principal; - } - - @Override - public boolean isUserInRole(String role) { - // En dev, autoriser tous les rôles - return true; - } - - @Override - public boolean isSecure() { - return original != null ? original.isSecure() : false; - } - - @Override - public String getAuthenticationScheme() { - return "DEV"; - } - } -} - +package dev.lions.user.manager.security; + +import jakarta.annotation.Priority; +import jakarta.inject.Inject; +import jakarta.ws.rs.Priorities; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.core.SecurityContext; +import jakarta.ws.rs.ext.Provider; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; + +import java.security.Principal; + +/** + * Filtre JAX-RS pour remplacer le SecurityContext en mode développement + * En dev, remplace le SecurityContext par un mock qui autorise tous les rôles + * En prod, laisse le SecurityContext réel de Quarkus + */ +@Provider +@Priority(Priorities.AUTHENTICATION - 10) // S'exécute très tôt, avant l'authentification +public class DevSecurityContextProducer implements ContainerRequestFilter { + + private static final Logger LOG = Logger.getLogger(DevSecurityContextProducer.class); + + @Inject + @ConfigProperty(name = "quarkus.profile", defaultValue = "prod") + String profile; + + @Inject + @ConfigProperty(name = "quarkus.oidc.enabled", defaultValue = "true") + boolean oidcEnabled; + + @Override + public void filter(ContainerRequestContext requestContext) { + // Détecter le mode dev : si OIDC est désactivé, on est probablement en dev + // ou si le profil est explicitement "dev" ou "development" + boolean isDevMode = !oidcEnabled || "dev".equals(profile) || "development".equals(profile); + + if (isDevMode) { + String path = requestContext.getUriInfo().getPath(); + LOG.infof("Mode dev détecté (profile=%s, oidc.enabled=%s): remplacement du SecurityContext pour le chemin %s", + profile, oidcEnabled, path); + SecurityContext original = requestContext.getSecurityContext(); + requestContext.setSecurityContext(new DevSecurityContext(original)); + LOG.debugf("SecurityContext remplacé - isUserInRole('admin')=%s, isUserInRole('user_manager')=%s", + new DevSecurityContext(original).isUserInRole("admin"), + new DevSecurityContext(original).isUserInRole("user_manager")); + } else { + LOG.debugf("Mode prod - SecurityContext original conservé (profile=%s, oidc.enabled=%s)", profile, oidcEnabled); + } + } + + /** + * SecurityContext mock pour le mode développement + * Simule un utilisateur avec tous les rôles nécessaires + */ + private static class DevSecurityContext implements SecurityContext { + + private final SecurityContext original; + private final Principal principal = new Principal() { + @Override + public String getName() { + return "dev-user"; + } + }; + + public DevSecurityContext(SecurityContext original) { + this.original = original; + } + + @Override + public Principal getUserPrincipal() { + return principal; + } + + @Override + public boolean isUserInRole(String role) { + // En dev, autoriser tous les rôles + return true; + } + + @Override + public boolean isSecure() { + return original != null ? original.isSecure() : false; + } + + @Override + public String getAuthenticationScheme() { + return "DEV"; + } + } +} + diff --git a/src/main/java/dev/lions/user/manager/server/impl/entity/AuditLogEntity.java b/src/main/java/dev/lions/user/manager/server/impl/entity/AuditLogEntity.java index b984608..0b7a018 100644 --- a/src/main/java/dev/lions/user/manager/server/impl/entity/AuditLogEntity.java +++ b/src/main/java/dev/lions/user/manager/server/impl/entity/AuditLogEntity.java @@ -1,209 +1,209 @@ -package dev.lions.user.manager.server.impl.entity; - -import dev.lions.user.manager.enums.audit.TypeActionAudit; -import io.quarkus.hibernate.orm.panache.PanacheEntity; -import jakarta.persistence.*; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.time.LocalDateTime; - -/** - * Entité JPA pour la persistance des logs d'audit en base de données PostgreSQL. - * - *

Cette entité représente un enregistrement d'audit qui track toutes les actions - * effectuées sur les utilisateurs du système (création, modification, suppression, etc.).

- * - *

Utilisation:

- *
- * AuditLogEntity auditLog = new AuditLogEntity();
- * auditLog.setUserId("user-123");
- * auditLog.setAction(TypeActionAudit.CREATION_UTILISATEUR);
- * auditLog.setDetails("Utilisateur créé avec succès");
- * auditLog.setAuteurAction("admin");
- * auditLog.setTimestamp(LocalDateTime.now());
- * auditLog.persist();
- * 
- * - * @see dev.lions.user.manager.server.api.dto.AuditLogDTO - * @see TypeActionAudit - * @author Lions Development Team - * @version 1.0.0 - * @since 2026-01-02 - */ -@Entity -@Table( - name = "audit_logs", - indexes = { - @Index(name = "idx_audit_user_id", columnList = "user_id"), - @Index(name = "idx_audit_action", columnList = "action"), - @Index(name = "idx_audit_timestamp", columnList = "timestamp"), - @Index(name = "idx_audit_auteur", columnList = "auteur_action") - } -) -@Data -@EqualsAndHashCode(callSuper = true) -public class AuditLogEntity extends PanacheEntity { - - /** - * ID de l'utilisateur concerné par l'action. - *

Peut être null pour les actions système qui ne concernent pas un utilisateur spécifique.

- */ - @Column(name = "user_id", length = 255) - private String userId; - - /** - * Type d'action effectuée (CREATION_UTILISATEUR, MODIFICATION_UTILISATEUR, etc.). - *

Stocké en tant que STRING pour faciliter la lecture en base de données.

- */ - @Column(name = "action", nullable = false, length = 100) - @Enumerated(EnumType.STRING) - private TypeActionAudit action; - - /** - * Détails complémentaires sur l'action effectuée. - *

Peut contenir des informations contextuelles comme les champs modifiés, - * les raisons d'une action, ou des messages d'erreur.

- */ - @Column(name = "details", columnDefinition = "TEXT") - private String details; - - /** - * Identifiant de l'utilisateur qui a effectué l'action. - *

Généralement l'username ou l'ID de l'administrateur/utilisateur connecté.

- */ - @Column(name = "auteur_action", nullable = false, length = 255) - private String auteurAction; - - /** - * Timestamp précis de l'action. - *

Utilisé pour l'ordre chronologique des logs et le filtrage temporel.

- */ - @Column(name = "timestamp", nullable = false) - private LocalDateTime timestamp; - - /** - * Adresse IP de l'auteur de l'action. - *

Utile pour la traçabilité et la détection d'anomalies.

- */ - @Column(name = "ip_address", length = 45) - private String ipAddress; - - /** - * User-Agent du client (navigateur, application, etc.). - *

Permet d'identifier le type de client utilisé pour l'action.

- */ - @Column(name = "user_agent", length = 500) - private String userAgent; - - /** - * Nom du realm Keycloak concerné. - *

Important dans un environnement multi-tenant pour isoler les logs par realm.

- */ - @Column(name = "realm_name", length = 255) - private String realmName; - - /** - * Indique si l'action a réussi ou échoué. - *

Permet de filtrer facilement les actions en erreur pour analyse.

- */ - @Column(name = "success", nullable = false) - private Boolean success = true; - - /** - * Message d'erreur en cas d'échec de l'action. - *

Null si success = true.

- */ - @Column(name = "error_message", columnDefinition = "TEXT") - private String errorMessage; - - /** - * Constructeur par défaut requis par JPA. - */ - public AuditLogEntity() { - this.timestamp = LocalDateTime.now(); - } - - /** - * Recherche tous les logs d'audit pour un utilisateur donné. - * - * @param userId ID de l'utilisateur - * @return Liste des logs triés par timestamp décroissant - */ - public static java.util.List findByUserId(String userId) { - return list("userId = ?1 order by timestamp desc", userId); - } - - /** - * Recherche tous les logs d'audit d'un type d'action donné. - * - * @param action Type d'action - * @return Liste des logs triés par timestamp décroissant - */ - public static java.util.List findByAction(TypeActionAudit action) { - return list("action = ?1 order by timestamp desc", action); - } - - /** - * Recherche tous les logs d'audit pour un auteur donné. - * - * @param auteurAction Identifiant de l'auteur - * @return Liste des logs triés par timestamp décroissant - */ - public static java.util.List findByAuteur(String auteurAction) { - return list("auteurAction = ?1 order by timestamp desc", auteurAction); - } - - /** - * Recherche tous les logs d'audit dans une période donnée. - * - * @param startDate Date de début (inclusive) - * @param endDate Date de fin (inclusive) - * @return Liste des logs dans la période, triés par timestamp décroissant - */ - public static java.util.List findByPeriod(LocalDateTime startDate, LocalDateTime endDate) { - return list("timestamp >= ?1 and timestamp <= ?2 order by timestamp desc", startDate, endDate); - } - - /** - * Recherche tous les logs d'audit pour un realm donné. - * - * @param realmName Nom du realm - * @return Liste des logs triés par timestamp décroissant - */ - public static java.util.List findByRealm(String realmName) { - return list("realmName = ?1 order by timestamp desc", realmName); - } - - /** - * Supprime tous les logs d'audit plus anciens qu'une date donnée. - *

Utile pour la maintenance et le respect des politiques de rétention.

- * - * @param beforeDate Date limite (les logs avant cette date seront supprimés) - * @return Nombre de logs supprimés - */ - public static long deleteOlderThan(LocalDateTime beforeDate) { - return delete("timestamp < ?1", beforeDate); - } - - /** - * Compte le nombre d'actions effectuées par un auteur donné. - * - * @param auteurAction Identifiant de l'auteur - * @return Nombre d'actions - */ - public static long countByAuteur(String auteurAction) { - return count("auteurAction = ?1", auteurAction); - } - - /** - * Compte le nombre d'échecs pour un utilisateur donné. - *

Utile pour détecter des problèmes récurrents.

- * - * @param userId ID de l'utilisateur - * @return Nombre d'échecs - */ - public static long countFailuresByUserId(String userId) { - return count("userId = ?1 and success = false", userId); - } -} +package dev.lions.user.manager.server.impl.entity; + +import dev.lions.user.manager.enums.audit.TypeActionAudit; +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import jakarta.persistence.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * Entité JPA pour la persistance des logs d'audit en base de données PostgreSQL. + * + *

Cette entité représente un enregistrement d'audit qui track toutes les actions + * effectuées sur les utilisateurs du système (création, modification, suppression, etc.).

+ * + *

Utilisation:

+ *
+ * AuditLogEntity auditLog = new AuditLogEntity();
+ * auditLog.setUserId("user-123");
+ * auditLog.setAction(TypeActionAudit.CREATION_UTILISATEUR);
+ * auditLog.setDetails("Utilisateur créé avec succès");
+ * auditLog.setAuteurAction("admin");
+ * auditLog.setTimestamp(LocalDateTime.now());
+ * auditLog.persist();
+ * 
+ * + * @see dev.lions.user.manager.server.api.dto.AuditLogDTO + * @see TypeActionAudit + * @author Lions Development Team + * @version 1.0.0 + * @since 2026-01-02 + */ +@Entity +@Table( + name = "audit_logs", + indexes = { + @Index(name = "idx_audit_user_id", columnList = "user_id"), + @Index(name = "idx_audit_action", columnList = "action"), + @Index(name = "idx_audit_timestamp", columnList = "timestamp"), + @Index(name = "idx_audit_auteur", columnList = "auteur_action") + } +) +@Data +@EqualsAndHashCode(callSuper = true) +public class AuditLogEntity extends PanacheEntity { + + /** + * ID de l'utilisateur concerné par l'action. + *

Peut être null pour les actions système qui ne concernent pas un utilisateur spécifique.

+ */ + @Column(name = "user_id", length = 255) + private String userId; + + /** + * Type d'action effectuée (CREATION_UTILISATEUR, MODIFICATION_UTILISATEUR, etc.). + *

Stocké en tant que STRING pour faciliter la lecture en base de données.

+ */ + @Column(name = "action", nullable = false, length = 100) + @Enumerated(EnumType.STRING) + private TypeActionAudit action; + + /** + * Détails complémentaires sur l'action effectuée. + *

Peut contenir des informations contextuelles comme les champs modifiés, + * les raisons d'une action, ou des messages d'erreur.

+ */ + @Column(name = "details", columnDefinition = "TEXT") + private String details; + + /** + * Identifiant de l'utilisateur qui a effectué l'action. + *

Généralement l'username ou l'ID de l'administrateur/utilisateur connecté.

+ */ + @Column(name = "auteur_action", nullable = false, length = 255) + private String auteurAction; + + /** + * Timestamp précis de l'action. + *

Utilisé pour l'ordre chronologique des logs et le filtrage temporel.

+ */ + @Column(name = "timestamp", nullable = false) + private LocalDateTime timestamp; + + /** + * Adresse IP de l'auteur de l'action. + *

Utile pour la traçabilité et la détection d'anomalies.

+ */ + @Column(name = "ip_address", length = 45) + private String ipAddress; + + /** + * User-Agent du client (navigateur, application, etc.). + *

Permet d'identifier le type de client utilisé pour l'action.

+ */ + @Column(name = "user_agent", length = 500) + private String userAgent; + + /** + * Nom du realm Keycloak concerné. + *

Important dans un environnement multi-tenant pour isoler les logs par realm.

+ */ + @Column(name = "realm_name", length = 255) + private String realmName; + + /** + * Indique si l'action a réussi ou échoué. + *

Permet de filtrer facilement les actions en erreur pour analyse.

+ */ + @Column(name = "success", nullable = false) + private Boolean success = true; + + /** + * Message d'erreur en cas d'échec de l'action. + *

Null si success = true.

+ */ + @Column(name = "error_message", columnDefinition = "TEXT") + private String errorMessage; + + /** + * Constructeur par défaut requis par JPA. + */ + public AuditLogEntity() { + this.timestamp = LocalDateTime.now(); + } + + /** + * Recherche tous les logs d'audit pour un utilisateur donné. + * + * @param userId ID de l'utilisateur + * @return Liste des logs triés par timestamp décroissant + */ + public static java.util.List findByUserId(String userId) { + return list("userId = ?1 order by timestamp desc", userId); + } + + /** + * Recherche tous les logs d'audit d'un type d'action donné. + * + * @param action Type d'action + * @return Liste des logs triés par timestamp décroissant + */ + public static java.util.List findByAction(TypeActionAudit action) { + return list("action = ?1 order by timestamp desc", action); + } + + /** + * Recherche tous les logs d'audit pour un auteur donné. + * + * @param auteurAction Identifiant de l'auteur + * @return Liste des logs triés par timestamp décroissant + */ + public static java.util.List findByAuteur(String auteurAction) { + return list("auteurAction = ?1 order by timestamp desc", auteurAction); + } + + /** + * Recherche tous les logs d'audit dans une période donnée. + * + * @param startDate Date de début (inclusive) + * @param endDate Date de fin (inclusive) + * @return Liste des logs dans la période, triés par timestamp décroissant + */ + public static java.util.List findByPeriod(LocalDateTime startDate, LocalDateTime endDate) { + return list("timestamp >= ?1 and timestamp <= ?2 order by timestamp desc", startDate, endDate); + } + + /** + * Recherche tous les logs d'audit pour un realm donné. + * + * @param realmName Nom du realm + * @return Liste des logs triés par timestamp décroissant + */ + public static java.util.List findByRealm(String realmName) { + return list("realmName = ?1 order by timestamp desc", realmName); + } + + /** + * Supprime tous les logs d'audit plus anciens qu'une date donnée. + *

Utile pour la maintenance et le respect des politiques de rétention.

+ * + * @param beforeDate Date limite (les logs avant cette date seront supprimés) + * @return Nombre de logs supprimés + */ + public static long deleteOlderThan(LocalDateTime beforeDate) { + return delete("timestamp < ?1", beforeDate); + } + + /** + * Compte le nombre d'actions effectuées par un auteur donné. + * + * @param auteurAction Identifiant de l'auteur + * @return Nombre d'actions + */ + public static long countByAuteur(String auteurAction) { + return count("auteurAction = ?1", auteurAction); + } + + /** + * Compte le nombre d'échecs pour un utilisateur donné. + *

Utile pour détecter des problèmes récurrents.

+ * + * @param userId ID de l'utilisateur + * @return Nombre d'échecs + */ + public static long countFailuresByUserId(String userId) { + return count("userId = ?1 and success = false", userId); + } +} diff --git a/src/main/java/dev/lions/user/manager/server/impl/entity/SyncHistoryEntity.java b/src/main/java/dev/lions/user/manager/server/impl/entity/SyncHistoryEntity.java index c11764f..50ab452 100644 --- a/src/main/java/dev/lions/user/manager/server/impl/entity/SyncHistoryEntity.java +++ b/src/main/java/dev/lions/user/manager/server/impl/entity/SyncHistoryEntity.java @@ -1,50 +1,50 @@ -package dev.lions.user.manager.server.impl.entity; - -import io.quarkus.hibernate.orm.panache.PanacheEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import jakarta.persistence.Index; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.time.LocalDateTime; - -/** - * Entité représentant l'historique des synchronisations avec Keycloak. - */ -@Entity -@Table(name = "sync_history", indexes = { - @Index(name = "idx_sync_realm", columnList = "realm_name"), - @Index(name = "idx_sync_date", columnList = "sync_date") -}) -@Data -@EqualsAndHashCode(callSuper = true) -public class SyncHistoryEntity extends PanacheEntity { - - @Column(name = "realm_name", nullable = false) - private String realmName; - - @Column(name = "sync_date", nullable = false) - private LocalDateTime syncDate; - - // USER ou ROLE - @Column(name = "sync_type", nullable = false) - private String syncType; - - @Column(name = "status", nullable = false) // SUCCESS, FAILURE - private String status; - - @Column(name = "items_processed") - private Integer itemsProcessed; - - @Column(name = "duration_ms") - private Long durationMs; - - @Column(name = "error_message", columnDefinition = "TEXT") - private String errorMessage; - - public SyncHistoryEntity() { - this.syncDate = LocalDateTime.now(); - } -} +package dev.lions.user.manager.server.impl.entity; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.persistence.Index; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * Entité représentant l'historique des synchronisations avec Keycloak. + */ +@Entity +@Table(name = "sync_history", indexes = { + @Index(name = "idx_sync_realm", columnList = "realm_name"), + @Index(name = "idx_sync_date", columnList = "sync_date") +}) +@Data +@EqualsAndHashCode(callSuper = true) +public class SyncHistoryEntity extends PanacheEntity { + + @Column(name = "realm_name", nullable = false) + private String realmName; + + @Column(name = "sync_date", nullable = false) + private LocalDateTime syncDate; + + // USER ou ROLE + @Column(name = "sync_type", nullable = false) + private String syncType; + + @Column(name = "status", nullable = false) // SUCCESS, FAILURE + private String status; + + @Column(name = "items_processed") + private Integer itemsProcessed; + + @Column(name = "duration_ms") + private Long durationMs; + + @Column(name = "error_message", columnDefinition = "TEXT") + private String errorMessage; + + public SyncHistoryEntity() { + this.syncDate = LocalDateTime.now(); + } +} diff --git a/src/main/java/dev/lions/user/manager/server/impl/entity/SyncedRoleEntity.java b/src/main/java/dev/lions/user/manager/server/impl/entity/SyncedRoleEntity.java index 311631d..6595df4 100644 --- a/src/main/java/dev/lions/user/manager/server/impl/entity/SyncedRoleEntity.java +++ b/src/main/java/dev/lions/user/manager/server/impl/entity/SyncedRoleEntity.java @@ -1,32 +1,32 @@ -package dev.lions.user.manager.server.impl.entity; - -import io.quarkus.hibernate.orm.panache.PanacheEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Index; -import jakarta.persistence.Table; -import lombok.Data; -import lombok.EqualsAndHashCode; - -/** - * Snapshot local d'un rôle Keycloak synchronisé. - */ -@Entity -@Table(name = "synced_role", indexes = { - @Index(name = "idx_synced_role_realm", columnList = "realm_name"), - @Index(name = "idx_synced_role_realm_name", columnList = "realm_name,role_name", unique = true) -}) -@Data -@EqualsAndHashCode(callSuper = true) -public class SyncedRoleEntity extends PanacheEntity { - - @Column(name = "realm_name", nullable = false) - private String realmName; - - @Column(name = "role_name", nullable = false) - private String roleName; - - @Column(name = "description") - private String description; -} - +package dev.lions.user.manager.server.impl.entity; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * Snapshot local d'un rôle Keycloak synchronisé. + */ +@Entity +@Table(name = "synced_role", indexes = { + @Index(name = "idx_synced_role_realm", columnList = "realm_name"), + @Index(name = "idx_synced_role_realm_name", columnList = "realm_name,role_name", unique = true) +}) +@Data +@EqualsAndHashCode(callSuper = true) +public class SyncedRoleEntity extends PanacheEntity { + + @Column(name = "realm_name", nullable = false) + private String realmName; + + @Column(name = "role_name", nullable = false) + private String roleName; + + @Column(name = "description") + private String description; +} + diff --git a/src/main/java/dev/lions/user/manager/server/impl/entity/SyncedUserEntity.java b/src/main/java/dev/lions/user/manager/server/impl/entity/SyncedUserEntity.java index 843a914..4270726 100644 --- a/src/main/java/dev/lions/user/manager/server/impl/entity/SyncedUserEntity.java +++ b/src/main/java/dev/lions/user/manager/server/impl/entity/SyncedUserEntity.java @@ -1,47 +1,47 @@ -package dev.lions.user.manager.server.impl.entity; - -import io.quarkus.hibernate.orm.panache.PanacheEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Index; -import jakarta.persistence.Table; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.time.LocalDateTime; - -/** - * Snapshot local d'un utilisateur Keycloak synchronisé. - * Permet de conserver un état minimal pour des rapports ou vérifications de cohérence. - */ -@Entity -@Table(name = "synced_user", indexes = { - @Index(name = "idx_synced_user_realm", columnList = "realm_name"), - @Index(name = "idx_synced_user_realm_kc_id", columnList = "realm_name,keycloak_id", unique = true) -}) -@Data -@EqualsAndHashCode(callSuper = true) -public class SyncedUserEntity extends PanacheEntity { - - @Column(name = "realm_name", nullable = false) - private String realmName; - - @Column(name = "keycloak_id", nullable = false) - private String keycloakId; - - @Column(name = "username", nullable = false) - private String username; - - @Column(name = "email") - private String email; - - @Column(name = "enabled") - private Boolean enabled; - - @Column(name = "email_verified") - private Boolean emailVerified; - - @Column(name = "created_at") - private LocalDateTime createdAt; -} - +package dev.lions.user.manager.server.impl.entity; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * Snapshot local d'un utilisateur Keycloak synchronisé. + * Permet de conserver un état minimal pour des rapports ou vérifications de cohérence. + */ +@Entity +@Table(name = "synced_user", indexes = { + @Index(name = "idx_synced_user_realm", columnList = "realm_name"), + @Index(name = "idx_synced_user_realm_kc_id", columnList = "realm_name,keycloak_id", unique = true) +}) +@Data +@EqualsAndHashCode(callSuper = true) +public class SyncedUserEntity extends PanacheEntity { + + @Column(name = "realm_name", nullable = false) + private String realmName; + + @Column(name = "keycloak_id", nullable = false) + private String keycloakId; + + @Column(name = "username", nullable = false) + private String username; + + @Column(name = "email") + private String email; + + @Column(name = "enabled") + private Boolean enabled; + + @Column(name = "email_verified") + private Boolean emailVerified; + + @Column(name = "created_at") + private LocalDateTime createdAt; +} + diff --git a/src/main/java/dev/lions/user/manager/server/impl/interceptor/AuditInterceptor.java b/src/main/java/dev/lions/user/manager/server/impl/interceptor/AuditInterceptor.java index e968735..dadc32c 100644 --- a/src/main/java/dev/lions/user/manager/server/impl/interceptor/AuditInterceptor.java +++ b/src/main/java/dev/lions/user/manager/server/impl/interceptor/AuditInterceptor.java @@ -1,93 +1,93 @@ -package dev.lions.user.manager.server.impl.interceptor; - -import dev.lions.user.manager.dto.audit.AuditLogDTO; -import dev.lions.user.manager.enums.audit.TypeActionAudit; -import dev.lions.user.manager.service.AuditService; -import io.quarkus.security.identity.SecurityIdentity; -import jakarta.annotation.Priority; -import jakarta.inject.Inject; -import jakarta.interceptor.AroundInvoke; -import jakarta.interceptor.Interceptor; -import jakarta.interceptor.InvocationContext; -import lombok.extern.slf4j.Slf4j; - -import java.time.LocalDateTime; - -@Logged -@Interceptor -@Priority(Interceptor.Priority.APPLICATION) -@Slf4j -public class AuditInterceptor { - - @Inject - AuditService auditService; - - @Inject - SecurityIdentity securityIdentity; - - @AroundInvoke - public Object auditMethod(InvocationContext context) throws Exception { - Logged annotation = context.getMethod().getAnnotation(Logged.class); - if (annotation == null) { - annotation = context.getTarget().getClass().getAnnotation(Logged.class); - } - - String actionStr = annotation != null ? annotation.action() : "UNKNOWN"; - String resourceType = annotation != null ? annotation.resource() : "UNKNOWN"; - String username = securityIdentity.isAnonymous() ? "anonymous" : securityIdentity.getPrincipal().getName(); - - // Extraction du realm depuis l'issuer JWT (ex: http://keycloak/realms/lions-user-manager) - String realmName = "unknown"; - if (!securityIdentity.isAnonymous() - && securityIdentity.getPrincipal() instanceof org.eclipse.microprofile.jwt.JsonWebToken jwt) { - String issuer = jwt.getIssuer(); - if (issuer != null && issuer.contains("/realms/")) { - realmName = issuer.substring(issuer.lastIndexOf("/realms/") + 8); - } - } - - // Tentative d'extraction de l'ID de la ressource (1er argument String) - String resourceId = ""; - if (context.getParameters().length > 0 && context.getParameters()[0] instanceof String) { - resourceId = (String) context.getParameters()[0]; - } - - try { - Object result = context.proceed(); - - // Log Success - try { - TypeActionAudit action = TypeActionAudit.valueOf(actionStr); - auditService.logSuccess( - action, - resourceType, - resourceId, - null, - realmName, - username, - "Action réussie via AOP"); - } catch (IllegalArgumentException e) { - log.warn("Type d'action audit inconnu: {}", actionStr); - } - - return result; - } catch (Exception e) { - // Log Failure - try { - TypeActionAudit action = TypeActionAudit.valueOf(actionStr); - auditService.logFailure( - action, - resourceType, - resourceId, - null, - realmName, - username, - "ERROR", - e.getMessage()); - } catch (IllegalArgumentException ex) { - log.warn("Type d'action audit inconnu: {}", actionStr); - } - throw e; - } - } -} +package dev.lions.user.manager.server.impl.interceptor; + +import dev.lions.user.manager.dto.audit.AuditLogDTO; +import dev.lions.user.manager.enums.audit.TypeActionAudit; +import dev.lions.user.manager.service.AuditService; +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.annotation.Priority; +import jakarta.inject.Inject; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InvocationContext; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; + +@Logged +@Interceptor +@Priority(Interceptor.Priority.APPLICATION) +@Slf4j +public class AuditInterceptor { + + @Inject + AuditService auditService; + + @Inject + SecurityIdentity securityIdentity; + + @AroundInvoke + public Object auditMethod(InvocationContext context) throws Exception { + Logged annotation = context.getMethod().getAnnotation(Logged.class); + if (annotation == null) { + annotation = context.getTarget().getClass().getAnnotation(Logged.class); + } + + String actionStr = annotation != null ? annotation.action() : "UNKNOWN"; + String resourceType = annotation != null ? annotation.resource() : "UNKNOWN"; + String username = securityIdentity.isAnonymous() ? "anonymous" : securityIdentity.getPrincipal().getName(); + + // Extraction du realm depuis l'issuer JWT (ex: http://keycloak/realms/lions-user-manager) + String realmName = "unknown"; + if (!securityIdentity.isAnonymous() + && securityIdentity.getPrincipal() instanceof org.eclipse.microprofile.jwt.JsonWebToken jwt) { + String issuer = jwt.getIssuer(); + if (issuer != null && issuer.contains("/realms/")) { + realmName = issuer.substring(issuer.lastIndexOf("/realms/") + 8); + } + } + + // Tentative d'extraction de l'ID de la ressource (1er argument String) + String resourceId = ""; + if (context.getParameters().length > 0 && context.getParameters()[0] instanceof String) { + resourceId = (String) context.getParameters()[0]; + } + + try { + Object result = context.proceed(); + + // Log Success + try { + TypeActionAudit action = TypeActionAudit.valueOf(actionStr); + auditService.logSuccess( + action, + resourceType, + resourceId, + null, + realmName, + username, + "Action réussie via AOP"); + } catch (IllegalArgumentException e) { + log.warn("Type d'action audit inconnu: {}", actionStr); + } + + return result; + } catch (Exception e) { + // Log Failure + try { + TypeActionAudit action = TypeActionAudit.valueOf(actionStr); + auditService.logFailure( + action, + resourceType, + resourceId, + null, + realmName, + username, + "ERROR", + e.getMessage()); + } catch (IllegalArgumentException ex) { + log.warn("Type d'action audit inconnu: {}", actionStr); + } + throw e; + } + } +} diff --git a/src/main/java/dev/lions/user/manager/server/impl/interceptor/Logged.java b/src/main/java/dev/lions/user/manager/server/impl/interceptor/Logged.java index 728ed43..d4833f7 100644 --- a/src/main/java/dev/lions/user/manager/server/impl/interceptor/Logged.java +++ b/src/main/java/dev/lions/user/manager/server/impl/interceptor/Logged.java @@ -1,26 +1,26 @@ -package dev.lions.user.manager.server.impl.interceptor; - -import jakarta.interceptor.InterceptorBinding; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Annotation pour auditer automatiquement l'exécution d'une méthode. - */ -@InterceptorBinding -@Target({ ElementType.METHOD, ElementType.TYPE }) -@Retention(RetentionPolicy.RUNTIME) -public @interface Logged { - - /** - * Type d'action d'audit (ex: UPDATE_USER). - */ - String action() default ""; - - /** - * Type de ressource concernée (ex: USER). - */ - String resource() default ""; -} +package dev.lions.user.manager.server.impl.interceptor; + +import jakarta.interceptor.InterceptorBinding; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation pour auditer automatiquement l'exécution d'une méthode. + */ +@InterceptorBinding +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface Logged { + + /** + * Type d'action d'audit (ex: UPDATE_USER). + */ + String action() default ""; + + /** + * Type de ressource concernée (ex: USER). + */ + String resource() default ""; +} diff --git a/src/main/java/dev/lions/user/manager/server/impl/mapper/AuditLogMapper.java b/src/main/java/dev/lions/user/manager/server/impl/mapper/AuditLogMapper.java index 2685c21..d504bfa 100644 --- a/src/main/java/dev/lions/user/manager/server/impl/mapper/AuditLogMapper.java +++ b/src/main/java/dev/lions/user/manager/server/impl/mapper/AuditLogMapper.java @@ -1,179 +1,179 @@ -package dev.lions.user.manager.server.impl.mapper; - -import dev.lions.user.manager.dto.audit.AuditLogDTO; -import dev.lions.user.manager.server.impl.entity.AuditLogEntity; -import org.mapstruct.*; - -import java.util.List; - -/** - * Mapper MapStruct pour la conversion entre AuditLogEntity (JPA) et AuditLogDTO (API). - * - *

Ce mapper gère la transformation bidirectionnelle entre l'entité de persistance - * et le DTO exposé via l'API REST, avec mapping automatique des champs compatibles.

- * - *

Fonctionnalités:

- *
    - *
  • Conversion Entity → DTO pour lecture/API
  • - *
  • Conversion DTO → Entity pour persistance
  • - *
  • Mapping de listes pour opérations bulk
  • - *
  • Gestion automatique des types LocalDateTime
  • - *
  • Mapping des enums (TypeActionAudit)
  • - *
- * - *

Utilisation:

- *
- * {@literal @}Inject
- * AuditLogMapper mapper;
- *
- * // Entity → DTO
- * AuditLogDTO dto = mapper.toDTO(entity);
- *
- * // DTO → Entity
- * AuditLogEntity entity = mapper.toEntity(dto);
- *
- * // Liste Entity → Liste DTO
- * List<AuditLogDTO> dtos = mapper.toDTOList(entities);
- * 
- * - * @see AuditLogEntity - * @see AuditLogDTO - * @author Lions Development Team - * @version 1.0.0 - * @since 2026-01-02 - */ -@Mapper( - componentModel = MappingConstants.ComponentModel.JAKARTA_CDI, - injectionStrategy = InjectionStrategy.CONSTRUCTOR, - unmappedTargetPolicy = ReportingPolicy.IGNORE -) -public interface AuditLogMapper { - - /** - * Convertit une entité AuditLogEntity en DTO AuditLogDTO. - * - *

Mapping des champs Entity → DTO:

- *
    - *
  • id (Long) → id (String)
  • - *
  • userId → ressourceId
  • - *
  • action → typeAction
  • - *
  • details → description
  • - *
  • auteurAction → acteurUsername
  • - *
  • timestamp → dateAction
  • - *
  • ipAddress → ipAddress
  • - *
  • userAgent → userAgent
  • - *
  • realmName → realmName
  • - *
  • success → success
  • - *
  • errorMessage → errorMessage
  • - *
- * - * @param entity L'entité JPA à convertir (peut être null) - * @return Le DTO correspondant, ou null si l'entité est null - */ - @Mapping(target = "id", source = "id", qualifiedByName = "longToString") - @Mapping(target = "ressourceId", source = "userId") - @Mapping(target = "typeAction", source = "action") - @Mapping(target = "description", source = "details") - @Mapping(target = "acteurUsername", source = "auteurAction") - @Mapping(target = "dateAction", source = "timestamp") - AuditLogDTO toDTO(AuditLogEntity entity); - - /** - * Convertit un DTO AuditLogDTO en entité AuditLogEntity. - * - *

Utilisé pour créer une nouvelle entité à persister depuis les données API.

- * - *

Note: L'ID de l'entité sera null (auto-généré par la DB), - * même si l'ID du DTO est renseigné.

- * - * @param dto Le DTO à convertir (peut être null) - * @return L'entité JPA correspondante, ou null si le DTO est null - */ - @Mapping(target = "id", ignore = true) // L'ID sera généré par la DB - @Mapping(target = "userId", source = "ressourceId") - @Mapping(target = "action", source = "typeAction") - @Mapping(target = "details", source = "description") - @Mapping(target = "auteurAction", source = "acteurUsername") - @Mapping(target = "timestamp", source = "dateAction") - AuditLogEntity toEntity(AuditLogDTO dto); - - /** - * Convertit une liste d'entités en liste de DTOs. - * - *

Utile pour les recherches qui retournent plusieurs résultats.

- * - * @param entities Liste des entités à convertir (peut être null ou vide) - * @return Liste des DTOs correspondants, ou liste vide si entities est null/vide - */ - List toDTOList(List entities); - - /** - * Convertit une liste de DTOs en liste d'entités. - * - *

Utile pour les opérations d'import ou de création en masse.

- * - * @param dtos Liste des DTOs à convertir (peut être null ou vide) - * @return Liste des entités correspondantes, ou liste vide si dtos est null/vide - */ - List toEntityList(List dtos); - - /** - * Met à jour une entité existante avec les données d'un DTO. - * - *

Préserve l'ID de l'entité et ne met à jour que les champs - * présents dans le DTO.

- * - *

Utilisation:

- *
-     * AuditLogEntity existingEntity = AuditLogEntity.findById(id);
-     * mapper.updateEntityFromDTO(dto, existingEntity);
-     * existingEntity.persist();
-     * 
- * - * @param dto Le DTO source contenant les nouvelles valeurs - * @param entity L'entité cible à mettre à jour - */ - @Mapping(target = "id", ignore = true) // Préserve l'ID existant - @Mapping(target = "userId", source = "ressourceId") - @Mapping(target = "action", source = "typeAction") - @Mapping(target = "details", source = "description") - @Mapping(target = "auteurAction", source = "acteurUsername") - @Mapping(target = "timestamp", source = "dateAction") - @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) - void updateEntityFromDTO(AuditLogDTO dto, @MappingTarget AuditLogEntity entity); - - /** - * Convertit un Long (ID de l'entité) en String (ID du DTO). - * - *

MapStruct appelle automatiquement cette méthode pour le mapping de l'ID.

- * - * @param id L'ID de type Long (peut être null) - * @return L'ID converti en String, ou null si l'input est null - */ - @Named("longToString") - default String longToString(Long id) { - return id != null ? id.toString() : null; - } - - /** - * Convertit un String (ID du DTO) en Long (ID de l'entité). - * - *

Utilisé lors de la conversion DTO → Entity si nécessaire.

- * - * @param id L'ID de type String (peut être null) - * @return L'ID converti en Long, ou null si l'input est null ou invalide - */ - @Named("stringToLong") - default Long stringToLong(String id) { - if (id == null || id.isBlank()) { - return null; - } - try { - return Long.parseLong(id); - } catch (NumberFormatException e) { - // Log warning et retourne null en cas de format invalide - System.err.println("WARN: Invalid ID format for conversion to Long: " + id); - return null; - } - } -} +package dev.lions.user.manager.server.impl.mapper; + +import dev.lions.user.manager.dto.audit.AuditLogDTO; +import dev.lions.user.manager.server.impl.entity.AuditLogEntity; +import org.mapstruct.*; + +import java.util.List; + +/** + * Mapper MapStruct pour la conversion entre AuditLogEntity (JPA) et AuditLogDTO (API). + * + *

Ce mapper gère la transformation bidirectionnelle entre l'entité de persistance + * et le DTO exposé via l'API REST, avec mapping automatique des champs compatibles.

+ * + *

Fonctionnalités:

+ *
    + *
  • Conversion Entity → DTO pour lecture/API
  • + *
  • Conversion DTO → Entity pour persistance
  • + *
  • Mapping de listes pour opérations bulk
  • + *
  • Gestion automatique des types LocalDateTime
  • + *
  • Mapping des enums (TypeActionAudit)
  • + *
+ * + *

Utilisation:

+ *
+ * {@literal @}Inject
+ * AuditLogMapper mapper;
+ *
+ * // Entity → DTO
+ * AuditLogDTO dto = mapper.toDTO(entity);
+ *
+ * // DTO → Entity
+ * AuditLogEntity entity = mapper.toEntity(dto);
+ *
+ * // Liste Entity → Liste DTO
+ * List<AuditLogDTO> dtos = mapper.toDTOList(entities);
+ * 
+ * + * @see AuditLogEntity + * @see AuditLogDTO + * @author Lions Development Team + * @version 1.0.0 + * @since 2026-01-02 + */ +@Mapper( + componentModel = MappingConstants.ComponentModel.JAKARTA_CDI, + injectionStrategy = InjectionStrategy.CONSTRUCTOR, + unmappedTargetPolicy = ReportingPolicy.IGNORE +) +public interface AuditLogMapper { + + /** + * Convertit une entité AuditLogEntity en DTO AuditLogDTO. + * + *

Mapping des champs Entity → DTO:

+ *
    + *
  • id (Long) → id (String)
  • + *
  • userId → ressourceId
  • + *
  • action → typeAction
  • + *
  • details → description
  • + *
  • auteurAction → acteurUsername
  • + *
  • timestamp → dateAction
  • + *
  • ipAddress → ipAddress
  • + *
  • userAgent → userAgent
  • + *
  • realmName → realmName
  • + *
  • success → success
  • + *
  • errorMessage → errorMessage
  • + *
+ * + * @param entity L'entité JPA à convertir (peut être null) + * @return Le DTO correspondant, ou null si l'entité est null + */ + @Mapping(target = "id", source = "id", qualifiedByName = "longToString") + @Mapping(target = "ressourceId", source = "userId") + @Mapping(target = "typeAction", source = "action") + @Mapping(target = "description", source = "details") + @Mapping(target = "acteurUsername", source = "auteurAction") + @Mapping(target = "dateAction", source = "timestamp") + AuditLogDTO toDTO(AuditLogEntity entity); + + /** + * Convertit un DTO AuditLogDTO en entité AuditLogEntity. + * + *

Utilisé pour créer une nouvelle entité à persister depuis les données API.

+ * + *

Note: L'ID de l'entité sera null (auto-généré par la DB), + * même si l'ID du DTO est renseigné.

+ * + * @param dto Le DTO à convertir (peut être null) + * @return L'entité JPA correspondante, ou null si le DTO est null + */ + @Mapping(target = "id", ignore = true) // L'ID sera généré par la DB + @Mapping(target = "userId", source = "ressourceId") + @Mapping(target = "action", source = "typeAction") + @Mapping(target = "details", source = "description") + @Mapping(target = "auteurAction", source = "acteurUsername") + @Mapping(target = "timestamp", source = "dateAction") + AuditLogEntity toEntity(AuditLogDTO dto); + + /** + * Convertit une liste d'entités en liste de DTOs. + * + *

Utile pour les recherches qui retournent plusieurs résultats.

+ * + * @param entities Liste des entités à convertir (peut être null ou vide) + * @return Liste des DTOs correspondants, ou liste vide si entities est null/vide + */ + List toDTOList(List entities); + + /** + * Convertit une liste de DTOs en liste d'entités. + * + *

Utile pour les opérations d'import ou de création en masse.

+ * + * @param dtos Liste des DTOs à convertir (peut être null ou vide) + * @return Liste des entités correspondantes, ou liste vide si dtos est null/vide + */ + List toEntityList(List dtos); + + /** + * Met à jour une entité existante avec les données d'un DTO. + * + *

Préserve l'ID de l'entité et ne met à jour que les champs + * présents dans le DTO.

+ * + *

Utilisation:

+ *
+     * AuditLogEntity existingEntity = AuditLogEntity.findById(id);
+     * mapper.updateEntityFromDTO(dto, existingEntity);
+     * existingEntity.persist();
+     * 
+ * + * @param dto Le DTO source contenant les nouvelles valeurs + * @param entity L'entité cible à mettre à jour + */ + @Mapping(target = "id", ignore = true) // Préserve l'ID existant + @Mapping(target = "userId", source = "ressourceId") + @Mapping(target = "action", source = "typeAction") + @Mapping(target = "details", source = "description") + @Mapping(target = "auteurAction", source = "acteurUsername") + @Mapping(target = "timestamp", source = "dateAction") + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + void updateEntityFromDTO(AuditLogDTO dto, @MappingTarget AuditLogEntity entity); + + /** + * Convertit un Long (ID de l'entité) en String (ID du DTO). + * + *

MapStruct appelle automatiquement cette méthode pour le mapping de l'ID.

+ * + * @param id L'ID de type Long (peut être null) + * @return L'ID converti en String, ou null si l'input est null + */ + @Named("longToString") + default String longToString(Long id) { + return id != null ? id.toString() : null; + } + + /** + * Convertit un String (ID du DTO) en Long (ID de l'entité). + * + *

Utilisé lors de la conversion DTO → Entity si nécessaire.

+ * + * @param id L'ID de type String (peut être null) + * @return L'ID converti en Long, ou null si l'input est null ou invalide + */ + @Named("stringToLong") + default Long stringToLong(String id) { + if (id == null || id.isBlank()) { + return null; + } + try { + return Long.parseLong(id); + } catch (NumberFormatException e) { + // Log warning et retourne null en cas de format invalide + System.err.println("WARN: Invalid ID format for conversion to Long: " + id); + return null; + } + } +} diff --git a/src/main/java/dev/lions/user/manager/server/impl/mapper/SyncHistoryMapper.java b/src/main/java/dev/lions/user/manager/server/impl/mapper/SyncHistoryMapper.java index 48beff6..495eeb6 100644 --- a/src/main/java/dev/lions/user/manager/server/impl/mapper/SyncHistoryMapper.java +++ b/src/main/java/dev/lions/user/manager/server/impl/mapper/SyncHistoryMapper.java @@ -1,21 +1,21 @@ -package dev.lions.user.manager.server.impl.mapper; - -import dev.lions.user.manager.dto.sync.SyncHistoryDTO; -import dev.lions.user.manager.server.impl.entity.SyncHistoryEntity; -import org.mapstruct.*; - -import java.util.List; - -@Mapper(componentModel = MappingConstants.ComponentModel.JAKARTA_CDI, injectionStrategy = InjectionStrategy.CONSTRUCTOR, unmappedTargetPolicy = ReportingPolicy.IGNORE) -public interface SyncHistoryMapper { - - @Mapping(target = "id", source = "id", qualifiedByName = "longToString") - SyncHistoryDTO toDTO(SyncHistoryEntity entity); - - List toDTOList(List entities); - - @Named("longToString") - default String longToString(Long id) { - return id != null ? id.toString() : null; - } -} +package dev.lions.user.manager.server.impl.mapper; + +import dev.lions.user.manager.dto.sync.SyncHistoryDTO; +import dev.lions.user.manager.server.impl.entity.SyncHistoryEntity; +import org.mapstruct.*; + +import java.util.List; + +@Mapper(componentModel = MappingConstants.ComponentModel.JAKARTA_CDI, injectionStrategy = InjectionStrategy.CONSTRUCTOR, unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface SyncHistoryMapper { + + @Mapping(target = "id", source = "id", qualifiedByName = "longToString") + SyncHistoryDTO toDTO(SyncHistoryEntity entity); + + List toDTOList(List entities); + + @Named("longToString") + default String longToString(Long id) { + return id != null ? id.toString() : null; + } +} diff --git a/src/main/java/dev/lions/user/manager/server/impl/repository/AuditLogRepository.java b/src/main/java/dev/lions/user/manager/server/impl/repository/AuditLogRepository.java index 95d56ea..7a715a2 100644 --- a/src/main/java/dev/lions/user/manager/server/impl/repository/AuditLogRepository.java +++ b/src/main/java/dev/lions/user/manager/server/impl/repository/AuditLogRepository.java @@ -1,62 +1,62 @@ -package dev.lions.user.manager.server.impl.repository; - -import dev.lions.user.manager.enums.audit.TypeActionAudit; -import dev.lions.user.manager.server.impl.entity.AuditLogEntity; -import io.quarkus.hibernate.orm.panache.PanacheRepository; -import jakarta.enterprise.context.ApplicationScoped; - -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@ApplicationScoped -public class AuditLogRepository implements PanacheRepository { - - public List search(String realmName, - String auteurAction, - LocalDateTime dateDebut, - LocalDateTime dateFin, - String typeAction, - Boolean success, - int page, - int pageSize) { - - StringBuilder query = new StringBuilder("1=1"); - Map params = new HashMap<>(); - - // Construction dynamique de la requête - if (realmName != null && !realmName.isEmpty()) { - query.append(" AND realmName = :realmName"); - params.put("realmName", realmName); - } - if (auteurAction != null && !auteurAction.isEmpty()) { - query.append(" AND auteurAction = :auteurAction"); - params.put("auteurAction", auteurAction); - } - if (dateDebut != null) { - query.append(" AND timestamp >= :dateDebut"); - params.put("dateDebut", dateDebut); - } - if (dateFin != null) { - query.append(" AND timestamp <= :dateFin"); - params.put("dateFin", dateFin); - } - if (typeAction != null && !typeAction.isEmpty()) { - try { - TypeActionAudit actionEnum = TypeActionAudit.valueOf(typeAction); - query.append(" AND action = :actionEnum"); - params.put("actionEnum", actionEnum); - } catch (IllegalArgumentException e) { - // Ignore invalid enum value filter - } - } - if (success != null) { - query.append(" AND success = :success"); - params.put("success", success); - } - - query.append(" ORDER BY timestamp DESC"); - return find(query.toString(), params).page(page, pageSize).list(); - } -} +package dev.lions.user.manager.server.impl.repository; + +import dev.lions.user.manager.enums.audit.TypeActionAudit; +import dev.lions.user.manager.server.impl.entity.AuditLogEntity; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@ApplicationScoped +public class AuditLogRepository implements PanacheRepository { + + public List search(String realmName, + String auteurAction, + LocalDateTime dateDebut, + LocalDateTime dateFin, + String typeAction, + Boolean success, + int page, + int pageSize) { + + StringBuilder query = new StringBuilder("1=1"); + Map params = new HashMap<>(); + + // Construction dynamique de la requête + if (realmName != null && !realmName.isEmpty()) { + query.append(" AND realmName = :realmName"); + params.put("realmName", realmName); + } + if (auteurAction != null && !auteurAction.isEmpty()) { + query.append(" AND auteurAction = :auteurAction"); + params.put("auteurAction", auteurAction); + } + if (dateDebut != null) { + query.append(" AND timestamp >= :dateDebut"); + params.put("dateDebut", dateDebut); + } + if (dateFin != null) { + query.append(" AND timestamp <= :dateFin"); + params.put("dateFin", dateFin); + } + if (typeAction != null && !typeAction.isEmpty()) { + try { + TypeActionAudit actionEnum = TypeActionAudit.valueOf(typeAction); + query.append(" AND action = :actionEnum"); + params.put("actionEnum", actionEnum); + } catch (IllegalArgumentException e) { + // Ignore invalid enum value filter + } + } + if (success != null) { + query.append(" AND success = :success"); + params.put("success", success); + } + + query.append(" ORDER BY timestamp DESC"); + return find(query.toString(), params).page(page, pageSize).list(); + } +} diff --git a/src/main/java/dev/lions/user/manager/server/impl/repository/SyncHistoryRepository.java b/src/main/java/dev/lions/user/manager/server/impl/repository/SyncHistoryRepository.java index 7aa00bc..dbe8348 100644 --- a/src/main/java/dev/lions/user/manager/server/impl/repository/SyncHistoryRepository.java +++ b/src/main/java/dev/lions/user/manager/server/impl/repository/SyncHistoryRepository.java @@ -1,17 +1,17 @@ -package dev.lions.user.manager.server.impl.repository; - -import dev.lions.user.manager.server.impl.entity.SyncHistoryEntity; -import io.quarkus.hibernate.orm.panache.PanacheRepository; -import jakarta.enterprise.context.ApplicationScoped; - -import java.util.List; - -@ApplicationScoped -public class SyncHistoryRepository implements PanacheRepository { - - public List findLatestByRealm(String realmName, int limit) { - return find("realmName = ?1 ORDER BY syncDate DESC", realmName) - .page(0, limit) - .list(); - } -} +package dev.lions.user.manager.server.impl.repository; + +import dev.lions.user.manager.server.impl.entity.SyncHistoryEntity; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.List; + +@ApplicationScoped +public class SyncHistoryRepository implements PanacheRepository { + + public List findLatestByRealm(String realmName, int limit) { + return find("realmName = ?1 ORDER BY syncDate DESC", realmName) + .page(0, limit) + .list(); + } +} diff --git a/src/main/java/dev/lions/user/manager/server/impl/repository/SyncedRoleRepository.java b/src/main/java/dev/lions/user/manager/server/impl/repository/SyncedRoleRepository.java index 228ab4e..25599d4 100644 --- a/src/main/java/dev/lions/user/manager/server/impl/repository/SyncedRoleRepository.java +++ b/src/main/java/dev/lions/user/manager/server/impl/repository/SyncedRoleRepository.java @@ -1,20 +1,20 @@ -package dev.lions.user.manager.server.impl.repository; - -import dev.lions.user.manager.server.impl.entity.SyncedRoleEntity; -import io.quarkus.hibernate.orm.panache.PanacheRepository; -import jakarta.enterprise.context.ApplicationScoped; - -import java.util.List; - -@ApplicationScoped -public class SyncedRoleRepository implements PanacheRepository { - - /** - * Remplace l'ensemble des snapshots de rôles pour un realm donné. - */ - public void replaceForRealm(String realmName, List roles) { - delete("realmName", realmName); - persist(roles); - } -} - +package dev.lions.user.manager.server.impl.repository; + +import dev.lions.user.manager.server.impl.entity.SyncedRoleEntity; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.List; + +@ApplicationScoped +public class SyncedRoleRepository implements PanacheRepository { + + /** + * Remplace l'ensemble des snapshots de rôles pour un realm donné. + */ + public void replaceForRealm(String realmName, List roles) { + delete("realmName", realmName); + persist(roles); + } +} + diff --git a/src/main/java/dev/lions/user/manager/server/impl/repository/SyncedUserRepository.java b/src/main/java/dev/lions/user/manager/server/impl/repository/SyncedUserRepository.java index 806bb2f..a74f24e 100644 --- a/src/main/java/dev/lions/user/manager/server/impl/repository/SyncedUserRepository.java +++ b/src/main/java/dev/lions/user/manager/server/impl/repository/SyncedUserRepository.java @@ -1,20 +1,20 @@ -package dev.lions.user.manager.server.impl.repository; - -import dev.lions.user.manager.server.impl.entity.SyncedUserEntity; -import io.quarkus.hibernate.orm.panache.PanacheRepository; -import jakarta.enterprise.context.ApplicationScoped; - -import java.util.List; - -@ApplicationScoped -public class SyncedUserRepository implements PanacheRepository { - - /** - * Remplace l'ensemble des snapshots d'utilisateurs pour un realm donné. - */ - public void replaceForRealm(String realmName, List users) { - delete("realmName", realmName); - persist(users); - } -} - +package dev.lions.user.manager.server.impl.repository; + +import dev.lions.user.manager.server.impl.entity.SyncedUserEntity; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.List; + +@ApplicationScoped +public class SyncedUserRepository implements PanacheRepository { + + /** + * Remplace l'ensemble des snapshots d'utilisateurs pour un realm donné. + */ + public void replaceForRealm(String realmName, List users) { + delete("realmName", realmName); + persist(users); + } +} + diff --git a/src/main/java/dev/lions/user/manager/service/exception/KeycloakServiceException.java b/src/main/java/dev/lions/user/manager/service/exception/KeycloakServiceException.java index b95786f..c161767 100644 --- a/src/main/java/dev/lions/user/manager/service/exception/KeycloakServiceException.java +++ b/src/main/java/dev/lions/user/manager/service/exception/KeycloakServiceException.java @@ -1,72 +1,72 @@ -package dev.lions.user.manager.service.exception; - -/** - * Exception levée lorsqu'une erreur survient lors de l'appel au service Keycloak. - * - * @author Lions User Manager Team - * @version 1.0 - */ -public class KeycloakServiceException extends RuntimeException { - - private final int httpStatus; - private final String serviceName; - - public KeycloakServiceException(String message) { - super(message); - this.httpStatus = 0; - this.serviceName = "Keycloak"; - } - - public KeycloakServiceException(String message, Throwable cause) { - super(message, cause); - this.httpStatus = 0; - this.serviceName = "Keycloak"; - } - - public KeycloakServiceException(String message, int httpStatus) { - super(message); - this.httpStatus = httpStatus; - this.serviceName = "Keycloak"; - } - - public KeycloakServiceException(String message, int httpStatus, Throwable cause) { - super(message, cause); - this.httpStatus = httpStatus; - this.serviceName = "Keycloak"; - } - - public int getHttpStatus() { - return httpStatus; - } - - public String getServiceName() { - return serviceName; - } - - /** - * Exception spécifique pour les erreurs de connexion (service indisponible) - */ - public static class ServiceUnavailableException extends KeycloakServiceException { - public ServiceUnavailableException(String message) { - super("Service Keycloak indisponible: " + message); - } - - public ServiceUnavailableException(String message, Throwable cause) { - super("Service Keycloak indisponible: " + message, cause); - } - } - - /** - * Exception spécifique pour les erreurs de timeout - */ - public static class TimeoutException extends KeycloakServiceException { - public TimeoutException(String message) { - super("Timeout lors de l'appel au service Keycloak: " + message); - } - - public TimeoutException(String message, Throwable cause) { - super("Timeout lors de l'appel au service Keycloak: " + message, cause); - } - } -} - +package dev.lions.user.manager.service.exception; + +/** + * Exception levée lorsqu'une erreur survient lors de l'appel au service Keycloak. + * + * @author Lions User Manager Team + * @version 1.0 + */ +public class KeycloakServiceException extends RuntimeException { + + private final int httpStatus; + private final String serviceName; + + public KeycloakServiceException(String message) { + super(message); + this.httpStatus = 0; + this.serviceName = "Keycloak"; + } + + public KeycloakServiceException(String message, Throwable cause) { + super(message, cause); + this.httpStatus = 0; + this.serviceName = "Keycloak"; + } + + public KeycloakServiceException(String message, int httpStatus) { + super(message); + this.httpStatus = httpStatus; + this.serviceName = "Keycloak"; + } + + public KeycloakServiceException(String message, int httpStatus, Throwable cause) { + super(message, cause); + this.httpStatus = httpStatus; + this.serviceName = "Keycloak"; + } + + public int getHttpStatus() { + return httpStatus; + } + + public String getServiceName() { + return serviceName; + } + + /** + * Exception spécifique pour les erreurs de connexion (service indisponible) + */ + public static class ServiceUnavailableException extends KeycloakServiceException { + public ServiceUnavailableException(String message) { + super("Service Keycloak indisponible: " + message); + } + + public ServiceUnavailableException(String message, Throwable cause) { + super("Service Keycloak indisponible: " + message, cause); + } + } + + /** + * Exception spécifique pour les erreurs de timeout + */ + public static class TimeoutException extends KeycloakServiceException { + public TimeoutException(String message) { + super("Timeout lors de l'appel au service Keycloak: " + message); + } + + public TimeoutException(String message, Throwable cause) { + super("Timeout lors de l'appel au service Keycloak: " + message, cause); + } + } +} + 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 a856519..e92b3d4 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 @@ -1,362 +1,362 @@ -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.mapper.AuditLogMapper; // DELETE - Wrong package -import dev.lions.user.manager.server.impl.mapper.AuditLogMapper; // ADD - Correct package -import dev.lions.user.manager.server.impl.entity.AuditLogEntity; -import dev.lions.user.manager.server.impl.repository.AuditLogRepository; -import dev.lions.user.manager.service.AuditService; -import io.quarkus.hibernate.orm.panache.PanacheQuery; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.persistence.EntityManager; -import jakarta.transaction.Transactional; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.microprofile.config.inject.ConfigProperty; - -import java.time.LocalDateTime; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -@ApplicationScoped -@Slf4j -public class AuditServiceImpl implements AuditService { - - @Inject - AuditLogRepository auditLogRepository; - - @Inject - AuditLogMapper auditLogMapper; - - @Inject - EntityManager entityManager; - - @ConfigProperty(name = "lions.audit.enabled", defaultValue = "true") - boolean auditEnabled; - - @ConfigProperty(name = "lions.audit.log-to-database", defaultValue = "true") - boolean logToDatabase; - - @Override - @Transactional(Transactional.TxType.REQUIRES_NEW) - public AuditLogDTO logAction(@Valid @NotNull AuditLogDTO auditLog) { - if (!auditEnabled) { - log.debug("Audit désactivé, action ignorée: {}", auditLog.getTypeAction()); - return auditLog; - } - - log.info("AUDIT: [{}] {} - user:{} - ressource:{}/{} - status:{}", - auditLog.getRealmName(), - auditLog.getTypeAction(), - auditLog.getActeurUsername(), // ou getActeurUserId() - auditLog.getRessourceType(), - auditLog.getRessourceId(), - auditLog.getSuccess() != null && auditLog.getSuccess() ? "SUCCESS" : "FAILURE"); - - if (logToDatabase) { - try { - // Ensure dateAction is set - if (auditLog.getDateAction() == null) { - auditLog.setDateAction(LocalDateTime.now()); - } - - AuditLogEntity entity = auditLogMapper.toEntity(auditLog); - auditLogRepository.persist(entity); - - // Mettre à jour l'ID du DTO avec l'ID généré par la base - if (entity.id != null) { - auditLog.setId(entity.id.toString()); - } - } catch (Exception e) { - log.error("Erreur lors de la persistance du log d'audit", e); - // On ne bloque pas l'action métier si l'audit échoue (sauf exigence contraire) - } - } - - return auditLog; - } - - @Override - @Transactional(Transactional.TxType.REQUIRES_NEW) - public void logSuccess(@NotNull TypeActionAudit typeAction, - @NotBlank String ressourceType, - String ressourceId, - String ressourceName, - @NotBlank String realmName, - @NotBlank String acteurUserId, - String description) { - - AuditLogDTO log = AuditLogDTO.builder() - .typeAction(typeAction) - .ressourceType(ressourceType) - .ressourceId(ressourceId) - .ressourceName(ressourceName) - .realmName(realmName) - .acteurUserId(acteurUserId) - .acteurUsername(acteurUserId) // On map aussi le username pour la persistence Entity - .description(description) - .dateAction(LocalDateTime.now()) - .success(true) - .build(); - - logAction(log); - } - - @Override - @Transactional(Transactional.TxType.REQUIRES_NEW) - public void logFailure(@NotNull TypeActionAudit typeAction, - @NotBlank String ressourceType, - String ressourceId, - String ressourceName, - @NotBlank String realmName, - @NotBlank String acteurUserId, - String errorCode, - String errorMessage) { - - AuditLogDTO log = AuditLogDTO.builder() - .typeAction(typeAction) - .ressourceType(ressourceType) - .ressourceId(ressourceId) - .ressourceName(ressourceName) - .realmName(realmName) - .acteurUserId(acteurUserId) - .acteurUsername(acteurUserId) - .description("Echec: " + errorCode) - .errorMessage(errorMessage) - .dateAction(LocalDateTime.now()) - .success(false) - .build(); - - logAction(log); - } - - @Override - public List findByActeur(@NotBlank String acteurUserId, - LocalDateTime dateDebut, - LocalDateTime dateFin, - int page, - int pageSize) { - // Le repository cherche par auteurAction, qui est mappé sur acteurUsername dans - // le DTO - List entities = auditLogRepository.search(null, acteurUserId, dateDebut, dateFin, null, null, - page, - pageSize); - return auditLogMapper.toDTOList(entities); - } - - @Override - public List findByRessource(@NotBlank String ressourceType, - @NotBlank String ressourceId, - LocalDateTime dateDebut, - LocalDateTime dateFin, - int page, - int pageSize) { - - // Utilisation de Panache query directe car le repo search générique est limité - // On cherche dans 'details' (description) ou 'userId' (ressourceId) - String filter = "%" + ressourceId + "%"; - // Correction: userId est le nom du champ dans l'entité qui mappe ressourceId - PanacheQuery q = auditLogRepository.find("userId = ?1 or details like ?2", ressourceId, filter); - - return auditLogMapper.toDTOList(q.page(page, pageSize).list()); - } - - @Override - public List findByTypeAction(@NotNull TypeActionAudit typeAction, - @NotBlank String realmName, - LocalDateTime dateDebut, - LocalDateTime dateFin, - int page, - int pageSize) { - List entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, - typeAction.name(), null, page, - pageSize); - return auditLogMapper.toDTOList(entities); - } - - @Override - public List findByRealm(@NotBlank String realmName, - LocalDateTime dateDebut, - LocalDateTime dateFin, - int page, - int pageSize) { - List entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, null, page, - pageSize); - return auditLogMapper.toDTOList(entities); - } - - @Override - public List findFailures(@NotBlank String realmName, - LocalDateTime dateDebut, - LocalDateTime dateFin, - int page, - int pageSize) { - List entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, false, - page, - pageSize); - return auditLogMapper.toDTOList(entities); - } - - @Override - public List findCriticalActions(@NotBlank String realmName, - LocalDateTime dateDebut, - LocalDateTime dateFin, - int page, - int pageSize) { - List entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, false, - page, pageSize); - return auditLogMapper.toDTOList(entities); - } - - @Override - @SuppressWarnings("unchecked") - public Map countByActionType(@NotBlank String realmName, - LocalDateTime dateDebut, - LocalDateTime dateFin) { - StringBuilder sql = new StringBuilder("SELECT action, COUNT(*) AS cnt FROM audit_logs WHERE realm_name = :realmName"); - if (dateDebut != null) sql.append(" AND timestamp >= :dateDebut"); - if (dateFin != null) sql.append(" AND timestamp <= :dateFin"); - sql.append(" GROUP BY action"); - var query = entityManager.createNativeQuery(sql.toString()) - .setParameter("realmName", realmName); - if (dateDebut != null) query.setParameter("dateDebut", dateDebut); - if (dateFin != null) query.setParameter("dateFin", dateFin); - List rows = query.getResultList(); - Map result = new HashMap<>(); - for (Object[] row : rows) { - String actionStr = (String) row[0]; - Long count = ((Number) row[1]).longValue(); - try { - result.put(TypeActionAudit.valueOf(actionStr), count); - } catch (IllegalArgumentException e) { - log.debug("TypeActionAudit inconnu ignoré: {}", actionStr); - } - } - return result; - } - - @Override - @SuppressWarnings("unchecked") - public Map countByActeur(@NotBlank String realmName, - LocalDateTime dateDebut, - LocalDateTime dateFin) { - StringBuilder sql = new StringBuilder("SELECT auteur_action, COUNT(*) AS cnt FROM audit_logs WHERE realm_name = :realmName"); - if (dateDebut != null) sql.append(" AND timestamp >= :dateDebut"); - if (dateFin != null) sql.append(" AND timestamp <= :dateFin"); - sql.append(" GROUP BY auteur_action ORDER BY COUNT(*) DESC LIMIT 10"); - var query = entityManager.createNativeQuery(sql.toString()) - .setParameter("realmName", realmName); - if (dateDebut != null) query.setParameter("dateDebut", dateDebut); - if (dateFin != null) query.setParameter("dateFin", dateFin); - List rows = query.getResultList(); - Map result = new HashMap<>(); - for (Object[] row : rows) { - result.put((String) row[0], ((Number) row[1]).longValue()); - } - return result; - } - - @Override - @SuppressWarnings("unchecked") - public Map countSuccessVsFailure(@NotBlank String realmName, - LocalDateTime dateDebut, - LocalDateTime dateFin) { - StringBuilder sql = new StringBuilder("SELECT success, COUNT(*) AS cnt FROM audit_logs WHERE realm_name = :realmName"); - if (dateDebut != null) sql.append(" AND timestamp >= :dateDebut"); - if (dateFin != null) sql.append(" AND timestamp <= :dateFin"); - sql.append(" GROUP BY success"); - var query = entityManager.createNativeQuery(sql.toString()) - .setParameter("realmName", realmName); - if (dateDebut != null) query.setParameter("dateDebut", dateDebut); - if (dateFin != null) query.setParameter("dateFin", dateFin); - List rows = query.getResultList(); - Map result = new HashMap<>(); - result.put("success", 0L); - result.put("failure", 0L); - for (Object[] row : rows) { - Boolean success = (Boolean) row[0]; - Long count = ((Number) row[1]).longValue(); - result.put(Boolean.TRUE.equals(success) ? "success" : "failure", count); - } - return result; - } - - @Override - public String exportToCSV(@NotBlank String realmName, - LocalDateTime dateDebut, - LocalDateTime dateFin) { - List entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, null, 0, Integer.MAX_VALUE); - List logs = auditLogMapper.toDTOList(entities); - StringBuilder csv = new StringBuilder(); - csv.append("id;typeAction;acteur;realmName;ressourceType;ressourceId;succes;dateAction;message\n"); - for (AuditLogDTO dto : logs) { - csv.append(escapeCsv(dto.getId())); - csv.append(";"); - csv.append(escapeCsv(dto.getTypeAction() != null ? dto.getTypeAction().name() : "")); - csv.append(";"); - csv.append(escapeCsv(dto.getActeurUsername())); - csv.append(";"); - csv.append(escapeCsv(dto.getRealmName())); - csv.append(";"); - csv.append(escapeCsv(dto.getRessourceType())); - csv.append(";"); - csv.append(escapeCsv(dto.getRessourceId())); - csv.append(";"); - csv.append(dto.getSuccess() != null && dto.getSuccess() ? "true" : "false"); - csv.append(";"); - csv.append(dto.getDateAction() != null ? dto.getDateAction().toString() : ""); - csv.append(";"); - csv.append(escapeCsv(dto.getErrorMessage() != null ? dto.getErrorMessage() : (dto.getDescription() != null ? dto.getDescription() : ""))); - csv.append("\n"); - } - return csv.toString(); - } - - private static String escapeCsv(String value) { - if (value == null) return ""; - if (value.contains(";") || value.contains("\"") || value.contains("\n")) { - return "\"" + value.replace("\"", "\"\"") + "\""; - } - return value; - } - - @Override - @Transactional - public long purgeOldLogs(@NotNull LocalDateTime dateLimite) { - return auditLogRepository.delete("timestamp < ?1", dateLimite); - } - - @Override - public Map getAuditStatistics(@NotBlank String realmName, - LocalDateTime dateDebut, - LocalDateTime dateFin) { - Map stats = new java.util.HashMap<>(); - stats.put("total", auditLogRepository.count("realmName", realmName)); - return stats; - } - - // ==================== Méthodes utilitaires ==================== - - /** - * Retourne le nombre total de logs (Utilisé par les tests) - */ - public long getTotalCount() { - return auditLogRepository.count(); - } - - /** - * Vide tous les logs (Utilisé par les tests) - */ - @Transactional - public void clearAll() { - log.warn("ATTENTION: Suppression de tous les logs d'audit en base"); - auditLogRepository.deleteAll(); - } -} +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.mapper.AuditLogMapper; // DELETE - Wrong package +import dev.lions.user.manager.server.impl.mapper.AuditLogMapper; // ADD - Correct package +import dev.lions.user.manager.server.impl.entity.AuditLogEntity; +import dev.lions.user.manager.server.impl.repository.AuditLogRepository; +import dev.lions.user.manager.service.AuditService; +import io.quarkus.hibernate.orm.panache.PanacheQuery; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@ApplicationScoped +@Slf4j +public class AuditServiceImpl implements AuditService { + + @Inject + AuditLogRepository auditLogRepository; + + @Inject + AuditLogMapper auditLogMapper; + + @Inject + EntityManager entityManager; + + @ConfigProperty(name = "lions.audit.enabled", defaultValue = "true") + boolean auditEnabled; + + @ConfigProperty(name = "lions.audit.log-to-database", defaultValue = "true") + boolean logToDatabase; + + @Override + @Transactional(Transactional.TxType.REQUIRES_NEW) + public AuditLogDTO logAction(@Valid @NotNull AuditLogDTO auditLog) { + if (!auditEnabled) { + log.debug("Audit désactivé, action ignorée: {}", auditLog.getTypeAction()); + return auditLog; + } + + log.info("AUDIT: [{}] {} - user:{} - ressource:{}/{} - status:{}", + auditLog.getRealmName(), + auditLog.getTypeAction(), + auditLog.getActeurUsername(), // ou getActeurUserId() + auditLog.getRessourceType(), + auditLog.getRessourceId(), + auditLog.getSuccess() != null && auditLog.getSuccess() ? "SUCCESS" : "FAILURE"); + + if (logToDatabase) { + try { + // Ensure dateAction is set + if (auditLog.getDateAction() == null) { + auditLog.setDateAction(LocalDateTime.now()); + } + + AuditLogEntity entity = auditLogMapper.toEntity(auditLog); + auditLogRepository.persist(entity); + + // Mettre à jour l'ID du DTO avec l'ID généré par la base + if (entity.id != null) { + auditLog.setId(entity.id.toString()); + } + } catch (Exception e) { + log.error("Erreur lors de la persistance du log d'audit", e); + // On ne bloque pas l'action métier si l'audit échoue (sauf exigence contraire) + } + } + + return auditLog; + } + + @Override + @Transactional(Transactional.TxType.REQUIRES_NEW) + public void logSuccess(@NotNull TypeActionAudit typeAction, + @NotBlank String ressourceType, + String ressourceId, + String ressourceName, + @NotBlank String realmName, + @NotBlank String acteurUserId, + String description) { + + AuditLogDTO log = AuditLogDTO.builder() + .typeAction(typeAction) + .ressourceType(ressourceType) + .ressourceId(ressourceId) + .ressourceName(ressourceName) + .realmName(realmName) + .acteurUserId(acteurUserId) + .acteurUsername(acteurUserId) // On map aussi le username pour la persistence Entity + .description(description) + .dateAction(LocalDateTime.now()) + .success(true) + .build(); + + logAction(log); + } + + @Override + @Transactional(Transactional.TxType.REQUIRES_NEW) + public void logFailure(@NotNull TypeActionAudit typeAction, + @NotBlank String ressourceType, + String ressourceId, + String ressourceName, + @NotBlank String realmName, + @NotBlank String acteurUserId, + String errorCode, + String errorMessage) { + + AuditLogDTO log = AuditLogDTO.builder() + .typeAction(typeAction) + .ressourceType(ressourceType) + .ressourceId(ressourceId) + .ressourceName(ressourceName) + .realmName(realmName) + .acteurUserId(acteurUserId) + .acteurUsername(acteurUserId) + .description("Echec: " + errorCode) + .errorMessage(errorMessage) + .dateAction(LocalDateTime.now()) + .success(false) + .build(); + + logAction(log); + } + + @Override + public List findByActeur(@NotBlank String acteurUserId, + LocalDateTime dateDebut, + LocalDateTime dateFin, + int page, + int pageSize) { + // Le repository cherche par auteurAction, qui est mappé sur acteurUsername dans + // le DTO + List entities = auditLogRepository.search(null, acteurUserId, dateDebut, dateFin, null, null, + page, + pageSize); + return auditLogMapper.toDTOList(entities); + } + + @Override + public List findByRessource(@NotBlank String ressourceType, + @NotBlank String ressourceId, + LocalDateTime dateDebut, + LocalDateTime dateFin, + int page, + int pageSize) { + + // Utilisation de Panache query directe car le repo search générique est limité + // On cherche dans 'details' (description) ou 'userId' (ressourceId) + String filter = "%" + ressourceId + "%"; + // Correction: userId est le nom du champ dans l'entité qui mappe ressourceId + PanacheQuery q = auditLogRepository.find("userId = ?1 or details like ?2", ressourceId, filter); + + return auditLogMapper.toDTOList(q.page(page, pageSize).list()); + } + + @Override + public List findByTypeAction(@NotNull TypeActionAudit typeAction, + @NotBlank String realmName, + LocalDateTime dateDebut, + LocalDateTime dateFin, + int page, + int pageSize) { + List entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, + typeAction.name(), null, page, + pageSize); + return auditLogMapper.toDTOList(entities); + } + + @Override + public List findByRealm(@NotBlank String realmName, + LocalDateTime dateDebut, + LocalDateTime dateFin, + int page, + int pageSize) { + List entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, null, page, + pageSize); + return auditLogMapper.toDTOList(entities); + } + + @Override + public List findFailures(@NotBlank String realmName, + LocalDateTime dateDebut, + LocalDateTime dateFin, + int page, + int pageSize) { + List entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, false, + page, + pageSize); + return auditLogMapper.toDTOList(entities); + } + + @Override + public List findCriticalActions(@NotBlank String realmName, + LocalDateTime dateDebut, + LocalDateTime dateFin, + int page, + int pageSize) { + List entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, false, + page, pageSize); + return auditLogMapper.toDTOList(entities); + } + + @Override + @SuppressWarnings("unchecked") + public Map countByActionType(@NotBlank String realmName, + LocalDateTime dateDebut, + LocalDateTime dateFin) { + StringBuilder sql = new StringBuilder("SELECT action, COUNT(*) AS cnt FROM audit_logs WHERE realm_name = :realmName"); + if (dateDebut != null) sql.append(" AND timestamp >= :dateDebut"); + if (dateFin != null) sql.append(" AND timestamp <= :dateFin"); + sql.append(" GROUP BY action"); + var query = entityManager.createNativeQuery(sql.toString()) + .setParameter("realmName", realmName); + if (dateDebut != null) query.setParameter("dateDebut", dateDebut); + if (dateFin != null) query.setParameter("dateFin", dateFin); + List rows = query.getResultList(); + Map result = new HashMap<>(); + for (Object[] row : rows) { + String actionStr = (String) row[0]; + Long count = ((Number) row[1]).longValue(); + try { + result.put(TypeActionAudit.valueOf(actionStr), count); + } catch (IllegalArgumentException e) { + log.debug("TypeActionAudit inconnu ignoré: {}", actionStr); + } + } + return result; + } + + @Override + @SuppressWarnings("unchecked") + public Map countByActeur(@NotBlank String realmName, + LocalDateTime dateDebut, + LocalDateTime dateFin) { + StringBuilder sql = new StringBuilder("SELECT auteur_action, COUNT(*) AS cnt FROM audit_logs WHERE realm_name = :realmName"); + if (dateDebut != null) sql.append(" AND timestamp >= :dateDebut"); + if (dateFin != null) sql.append(" AND timestamp <= :dateFin"); + sql.append(" GROUP BY auteur_action ORDER BY COUNT(*) DESC LIMIT 10"); + var query = entityManager.createNativeQuery(sql.toString()) + .setParameter("realmName", realmName); + if (dateDebut != null) query.setParameter("dateDebut", dateDebut); + if (dateFin != null) query.setParameter("dateFin", dateFin); + List rows = query.getResultList(); + Map result = new HashMap<>(); + for (Object[] row : rows) { + result.put((String) row[0], ((Number) row[1]).longValue()); + } + return result; + } + + @Override + @SuppressWarnings("unchecked") + public Map countSuccessVsFailure(@NotBlank String realmName, + LocalDateTime dateDebut, + LocalDateTime dateFin) { + StringBuilder sql = new StringBuilder("SELECT success, COUNT(*) AS cnt FROM audit_logs WHERE realm_name = :realmName"); + if (dateDebut != null) sql.append(" AND timestamp >= :dateDebut"); + if (dateFin != null) sql.append(" AND timestamp <= :dateFin"); + sql.append(" GROUP BY success"); + var query = entityManager.createNativeQuery(sql.toString()) + .setParameter("realmName", realmName); + if (dateDebut != null) query.setParameter("dateDebut", dateDebut); + if (dateFin != null) query.setParameter("dateFin", dateFin); + List rows = query.getResultList(); + Map result = new HashMap<>(); + result.put("success", 0L); + result.put("failure", 0L); + for (Object[] row : rows) { + Boolean success = (Boolean) row[0]; + Long count = ((Number) row[1]).longValue(); + result.put(Boolean.TRUE.equals(success) ? "success" : "failure", count); + } + return result; + } + + @Override + public String exportToCSV(@NotBlank String realmName, + LocalDateTime dateDebut, + LocalDateTime dateFin) { + List entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, null, 0, Integer.MAX_VALUE); + List logs = auditLogMapper.toDTOList(entities); + StringBuilder csv = new StringBuilder(); + csv.append("id;typeAction;acteur;realmName;ressourceType;ressourceId;succes;dateAction;message\n"); + for (AuditLogDTO dto : logs) { + csv.append(escapeCsv(dto.getId())); + csv.append(";"); + csv.append(escapeCsv(dto.getTypeAction() != null ? dto.getTypeAction().name() : "")); + csv.append(";"); + csv.append(escapeCsv(dto.getActeurUsername())); + csv.append(";"); + csv.append(escapeCsv(dto.getRealmName())); + csv.append(";"); + csv.append(escapeCsv(dto.getRessourceType())); + csv.append(";"); + csv.append(escapeCsv(dto.getRessourceId())); + csv.append(";"); + csv.append(dto.getSuccess() != null && dto.getSuccess() ? "true" : "false"); + csv.append(";"); + csv.append(dto.getDateAction() != null ? dto.getDateAction().toString() : ""); + csv.append(";"); + csv.append(escapeCsv(dto.getErrorMessage() != null ? dto.getErrorMessage() : (dto.getDescription() != null ? dto.getDescription() : ""))); + csv.append("\n"); + } + return csv.toString(); + } + + private static String escapeCsv(String value) { + if (value == null) return ""; + if (value.contains(";") || value.contains("\"") || value.contains("\n")) { + return "\"" + value.replace("\"", "\"\"") + "\""; + } + return value; + } + + @Override + @Transactional + public long purgeOldLogs(@NotNull LocalDateTime dateLimite) { + return auditLogRepository.delete("timestamp < ?1", dateLimite); + } + + @Override + public Map getAuditStatistics(@NotBlank String realmName, + LocalDateTime dateDebut, + LocalDateTime dateFin) { + Map stats = new java.util.HashMap<>(); + stats.put("total", auditLogRepository.count("realmName", realmName)); + return stats; + } + + // ==================== Méthodes utilitaires ==================== + + /** + * Retourne le nombre total de logs (Utilisé par les tests) + */ + public long getTotalCount() { + return auditLogRepository.count(); + } + + /** + * Vide tous les logs (Utilisé par les tests) + */ + @Transactional + public void clearAll() { + log.warn("ATTENTION: Suppression de tous les logs d'audit en base"); + auditLogRepository.deleteAll(); + } +} 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 index a0c4ac3..d4fa1c4 100644 --- a/src/main/java/dev/lions/user/manager/service/impl/CsvValidationHelper.java +++ b/src/main/java/dev/lions/user/manager/service/impl/CsvValidationHelper.java @@ -1,176 +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(); - } -} +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/RealmAuthorizationServiceImpl.java b/src/main/java/dev/lions/user/manager/service/impl/RealmAuthorizationServiceImpl.java index 97d849c..84a27d2 100644 --- a/src/main/java/dev/lions/user/manager/service/impl/RealmAuthorizationServiceImpl.java +++ b/src/main/java/dev/lions/user/manager/service/impl/RealmAuthorizationServiceImpl.java @@ -1,346 +1,346 @@ -package dev.lions.user.manager.service.impl; - -import dev.lions.user.manager.dto.realm.RealmAssignmentDTO; -import dev.lions.user.manager.enums.audit.TypeActionAudit; -import dev.lions.user.manager.service.AuditService; -import dev.lions.user.manager.service.RealmAuthorizationService; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.extern.slf4j.Slf4j; - -import java.time.LocalDateTime; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -/** - * Implémentation du service d'autorisation multi-tenant par realm - * - * NOTE: Cette implémentation utilise un stockage en mémoire (ConcurrentHashMap) - * Pour la production, migrer vers une base de données PostgreSQL - */ -@ApplicationScoped -@Slf4j -public class RealmAuthorizationServiceImpl implements RealmAuthorizationService { - - @Inject - AuditService auditService; - - // Stockage temporaire en mémoire (à remplacer par BD en production) - private final Map assignmentsById = new ConcurrentHashMap<>(); - private final Map> userToRealms = new ConcurrentHashMap<>(); - private final Map> realmToUsers = new ConcurrentHashMap<>(); - private final Set superAdmins = ConcurrentHashMap.newKeySet(); - - @Override - public List getAllAssignments() { - log.debug("Récupération de toutes les assignations de realms"); - return new ArrayList<>(assignmentsById.values()); - } - - @Override - public List getAssignmentsByUser(@NotBlank String userId) { - log.debug("Récupération des assignations pour l'utilisateur: {}", userId); - - return assignmentsById.values().stream() - .filter(assignment -> assignment.getUserId().equals(userId)) - .filter(RealmAssignmentDTO::isActive) - .filter(assignment -> !assignment.isExpired()) - .collect(Collectors.toList()); - } - - @Override - public List getAssignmentsByRealm(@NotBlank String realmName) { - log.debug("Récupération des assignations pour le realm: {}", realmName); - - return assignmentsById.values().stream() - .filter(assignment -> assignment.getRealmName().equals(realmName)) - .filter(RealmAssignmentDTO::isActive) - .filter(assignment -> !assignment.isExpired()) - .collect(Collectors.toList()); - } - - @Override - public Optional getAssignmentById(@NotBlank String assignmentId) { - log.debug("Récupération de l'assignation: {}", assignmentId); - return Optional.ofNullable(assignmentsById.get(assignmentId)); - } - - @Override - public boolean canManageRealm(@NotBlank String userId, @NotBlank String realmName) { - log.debug("Vérification si {} peut gérer le realm {}", userId, realmName); - - // Super admin peut tout gérer - if (isSuperAdmin(userId)) { - return true; - } - - // Vérifier les assignations actives et non expirées - return assignmentsById.values().stream() - .anyMatch(assignment -> - assignment.getUserId().equals(userId) && - assignment.getRealmName().equals(realmName) && - assignment.isActive() && - !assignment.isExpired() - ); - } - - @Override - public boolean isSuperAdmin(@NotBlank String userId) { - return superAdmins.contains(userId); - } - - @Override - public List getAuthorizedRealms(@NotBlank String userId) { - log.debug("Récupération des realms autorisés pour: {}", userId); - - // Super admin retourne liste vide (convention: peut tout gérer) - if (isSuperAdmin(userId)) { - return Collections.emptyList(); - } - - // Retourner les realms assignés actifs et non expirés - return assignmentsById.values().stream() - .filter(assignment -> assignment.getUserId().equals(userId)) - .filter(RealmAssignmentDTO::isActive) - .filter(assignment -> !assignment.isExpired()) - .map(RealmAssignmentDTO::getRealmName) - .distinct() - .collect(Collectors.toList()); - } - - @Override - public RealmAssignmentDTO assignRealmToUser(@Valid @NotNull RealmAssignmentDTO assignment) { - log.info("Assignation du realm {} à l'utilisateur {}", - assignment.getRealmName(), assignment.getUserId()); - - // Validation - if (assignment.getUserId() == null || assignment.getUserId().isBlank()) { - throw new IllegalArgumentException("L'ID utilisateur est obligatoire"); - } - if (assignment.getRealmName() == null || assignment.getRealmName().isBlank()) { - throw new IllegalArgumentException("Le nom du realm est obligatoire"); - } - - // Vérifier si l'assignation existe déjà - if (assignmentExists(assignment.getUserId(), assignment.getRealmName())) { - throw new IllegalArgumentException( - String.format("L'utilisateur %s a déjà accès au realm %s", - assignment.getUserId(), assignment.getRealmName()) - ); - } - - // Générer ID si absent - if (assignment.getId() == null) { - assignment.setId(UUID.randomUUID().toString()); - } - - // Compléter les métadonnées - assignment.setAssignedAt(LocalDateTime.now()); - assignment.setActive(true); - assignment.setDateCreation(LocalDateTime.now()); - - // Stocker l'assignation - assignmentsById.put(assignment.getId(), assignment); - - // Mettre à jour les index - userToRealms.computeIfAbsent(assignment.getUserId(), k -> ConcurrentHashMap.newKeySet()) - .add(assignment.getRealmName()); - realmToUsers.computeIfAbsent(assignment.getRealmName(), k -> ConcurrentHashMap.newKeySet()) - .add(assignment.getUserId()); - - // Audit - auditService.logSuccess( - TypeActionAudit.REALM_ASSIGN, - "REALM_ASSIGNMENT", - assignment.getId(), - assignment.getUsername(), - assignment.getRealmName(), - assignment.getAssignedBy() != null ? assignment.getAssignedBy() : "system", - String.format("Assignation du realm %s à %s", assignment.getRealmName(), assignment.getUsername()) - ); - - log.info("Realm {} assigné avec succès à {}", assignment.getRealmName(), assignment.getUserId()); - return assignment; - } - - @Override - public void revokeRealmFromUser(@NotBlank String userId, @NotBlank String realmName) { - log.info("Révocation du realm {} pour l'utilisateur {}", realmName, userId); - - // Trouver et supprimer l'assignation - Optional assignment = assignmentsById.values().stream() - .filter(a -> a.getUserId().equals(userId) && a.getRealmName().equals(realmName)) - .findFirst(); - - if (assignment.isEmpty()) { - log.warn("Aucune assignation trouvée pour {} / {}", userId, realmName); - return; - } - - RealmAssignmentDTO assignmentToRemove = assignment.get(); - assignmentsById.remove(assignmentToRemove.getId()); - - // Mettre à jour les index - Set realms = userToRealms.get(userId); - if (realms != null) { - realms.remove(realmName); - if (realms.isEmpty()) { - userToRealms.remove(userId); - } - } - - Set users = realmToUsers.get(realmName); - if (users != null) { - users.remove(userId); - if (users.isEmpty()) { - realmToUsers.remove(realmName); - } - } - - // Audit - auditService.logSuccess( - TypeActionAudit.REALM_REVOKE, - "REALM_ASSIGNMENT", - assignmentToRemove.getId(), - assignmentToRemove.getUsername(), - realmName, - "system", - String.format("Révocation du realm %s pour %s", realmName, assignmentToRemove.getUsername()) - ); - - log.info("Realm {} révoqué avec succès pour {}", realmName, userId); - } - - @Override - public void revokeAllRealmsFromUser(@NotBlank String userId) { - log.info("Révocation de tous les realms pour l'utilisateur {}", userId); - - List userAssignments = getAssignmentsByUser(userId); - userAssignments.forEach(assignment -> - revokeRealmFromUser(userId, assignment.getRealmName()) - ); - - log.info("{} realm(s) révoqué(s) pour {}", userAssignments.size(), userId); - } - - @Override - public void revokeAllUsersFromRealm(@NotBlank String realmName) { - log.info("Révocation de tous les utilisateurs du realm {}", realmName); - - List realmAssignments = getAssignmentsByRealm(realmName); - realmAssignments.forEach(assignment -> - revokeRealmFromUser(assignment.getUserId(), realmName) - ); - - log.info("{} utilisateur(s) révoqué(s) du realm {}", realmAssignments.size(), realmName); - } - - @Override - public void setSuperAdmin(@NotBlank String userId, boolean superAdmin) { - log.info("Définition de {} comme super admin: {}", userId, superAdmin); - - if (superAdmin) { - superAdmins.add(userId); - auditService.logSuccess( - TypeActionAudit.REALM_SET_SUPER_ADMIN, - "USER", - userId, - userId, - "lions-user-manager", - "system", - String.format("Utilisateur %s défini comme super admin", userId) - ); - } else { - superAdmins.remove(userId); - auditService.logSuccess( - TypeActionAudit.REALM_SET_SUPER_ADMIN, - "USER", - userId, - userId, - "lions-user-manager", - "system", - String.format("Privilèges super admin retirés pour %s", userId) - ); - } - } - - @Override - public void deactivateAssignment(@NotBlank String assignmentId) { - log.info("Désactivation de l'assignation {}", assignmentId); - - RealmAssignmentDTO assignment = assignmentsById.get(assignmentId); - if (assignment == null) { - throw new IllegalArgumentException("Assignation non trouvée: " + assignmentId); - } - - assignment.setActive(false); - assignment.setDateModification(LocalDateTime.now()); - - auditService.logSuccess( - TypeActionAudit.REALM_DEACTIVATE, - "REALM_ASSIGNMENT", - assignment.getId(), - assignment.getUsername(), - assignment.getRealmName(), - "system", - String.format("Désactivation de l'assignation %s", assignmentId) - ); - } - - @Override - public void activateAssignment(@NotBlank String assignmentId) { - log.info("Activation de l'assignation {}", assignmentId); - - RealmAssignmentDTO assignment = assignmentsById.get(assignmentId); - if (assignment == null) { - throw new IllegalArgumentException("Assignation non trouvée: " + assignmentId); - } - - assignment.setActive(true); - assignment.setDateModification(LocalDateTime.now()); - - auditService.logSuccess( - TypeActionAudit.REALM_ACTIVATE, - "REALM_ASSIGNMENT", - assignment.getId(), - assignment.getUsername(), - assignment.getRealmName(), - "system", - String.format("Activation de l'assignation %s", assignmentId) - ); - } - - @Override - public long countAssignmentsByUser(@NotBlank String userId) { - return assignmentsById.values().stream() - .filter(assignment -> assignment.getUserId().equals(userId)) - .filter(RealmAssignmentDTO::isActive) - .filter(assignment -> !assignment.isExpired()) - .count(); - } - - @Override - public long countUsersByRealm(@NotBlank String realmName) { - return assignmentsById.values().stream() - .filter(assignment -> assignment.getRealmName().equals(realmName)) - .filter(RealmAssignmentDTO::isActive) - .filter(assignment -> !assignment.isExpired()) - .map(RealmAssignmentDTO::getUserId) - .distinct() - .count(); - } - - @Override - public boolean assignmentExists(@NotBlank String userId, @NotBlank String realmName) { - return assignmentsById.values().stream() - .anyMatch(assignment -> - assignment.getUserId().equals(userId) && - assignment.getRealmName().equals(realmName) && - assignment.isActive() - ); - } -} +package dev.lions.user.manager.service.impl; + +import dev.lions.user.manager.dto.realm.RealmAssignmentDTO; +import dev.lions.user.manager.enums.audit.TypeActionAudit; +import dev.lions.user.manager.service.AuditService; +import dev.lions.user.manager.service.RealmAuthorizationService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * Implémentation du service d'autorisation multi-tenant par realm + * + * NOTE: Cette implémentation utilise un stockage en mémoire (ConcurrentHashMap) + * Pour la production, migrer vers une base de données PostgreSQL + */ +@ApplicationScoped +@Slf4j +public class RealmAuthorizationServiceImpl implements RealmAuthorizationService { + + @Inject + AuditService auditService; + + // Stockage temporaire en mémoire (à remplacer par BD en production) + private final Map assignmentsById = new ConcurrentHashMap<>(); + private final Map> userToRealms = new ConcurrentHashMap<>(); + private final Map> realmToUsers = new ConcurrentHashMap<>(); + private final Set superAdmins = ConcurrentHashMap.newKeySet(); + + @Override + public List getAllAssignments() { + log.debug("Récupération de toutes les assignations de realms"); + return new ArrayList<>(assignmentsById.values()); + } + + @Override + public List getAssignmentsByUser(@NotBlank String userId) { + log.debug("Récupération des assignations pour l'utilisateur: {}", userId); + + return assignmentsById.values().stream() + .filter(assignment -> assignment.getUserId().equals(userId)) + .filter(RealmAssignmentDTO::isActive) + .filter(assignment -> !assignment.isExpired()) + .collect(Collectors.toList()); + } + + @Override + public List getAssignmentsByRealm(@NotBlank String realmName) { + log.debug("Récupération des assignations pour le realm: {}", realmName); + + return assignmentsById.values().stream() + .filter(assignment -> assignment.getRealmName().equals(realmName)) + .filter(RealmAssignmentDTO::isActive) + .filter(assignment -> !assignment.isExpired()) + .collect(Collectors.toList()); + } + + @Override + public Optional getAssignmentById(@NotBlank String assignmentId) { + log.debug("Récupération de l'assignation: {}", assignmentId); + return Optional.ofNullable(assignmentsById.get(assignmentId)); + } + + @Override + public boolean canManageRealm(@NotBlank String userId, @NotBlank String realmName) { + log.debug("Vérification si {} peut gérer le realm {}", userId, realmName); + + // Super admin peut tout gérer + if (isSuperAdmin(userId)) { + return true; + } + + // Vérifier les assignations actives et non expirées + return assignmentsById.values().stream() + .anyMatch(assignment -> + assignment.getUserId().equals(userId) && + assignment.getRealmName().equals(realmName) && + assignment.isActive() && + !assignment.isExpired() + ); + } + + @Override + public boolean isSuperAdmin(@NotBlank String userId) { + return superAdmins.contains(userId); + } + + @Override + public List getAuthorizedRealms(@NotBlank String userId) { + log.debug("Récupération des realms autorisés pour: {}", userId); + + // Super admin retourne liste vide (convention: peut tout gérer) + if (isSuperAdmin(userId)) { + return Collections.emptyList(); + } + + // Retourner les realms assignés actifs et non expirés + return assignmentsById.values().stream() + .filter(assignment -> assignment.getUserId().equals(userId)) + .filter(RealmAssignmentDTO::isActive) + .filter(assignment -> !assignment.isExpired()) + .map(RealmAssignmentDTO::getRealmName) + .distinct() + .collect(Collectors.toList()); + } + + @Override + public RealmAssignmentDTO assignRealmToUser(@Valid @NotNull RealmAssignmentDTO assignment) { + log.info("Assignation du realm {} à l'utilisateur {}", + assignment.getRealmName(), assignment.getUserId()); + + // Validation + if (assignment.getUserId() == null || assignment.getUserId().isBlank()) { + throw new IllegalArgumentException("L'ID utilisateur est obligatoire"); + } + if (assignment.getRealmName() == null || assignment.getRealmName().isBlank()) { + throw new IllegalArgumentException("Le nom du realm est obligatoire"); + } + + // Vérifier si l'assignation existe déjà + if (assignmentExists(assignment.getUserId(), assignment.getRealmName())) { + throw new IllegalArgumentException( + String.format("L'utilisateur %s a déjà accès au realm %s", + assignment.getUserId(), assignment.getRealmName()) + ); + } + + // Générer ID si absent + if (assignment.getId() == null) { + assignment.setId(UUID.randomUUID().toString()); + } + + // Compléter les métadonnées + assignment.setAssignedAt(LocalDateTime.now()); + assignment.setActive(true); + assignment.setDateCreation(LocalDateTime.now()); + + // Stocker l'assignation + assignmentsById.put(assignment.getId(), assignment); + + // Mettre à jour les index + userToRealms.computeIfAbsent(assignment.getUserId(), k -> ConcurrentHashMap.newKeySet()) + .add(assignment.getRealmName()); + realmToUsers.computeIfAbsent(assignment.getRealmName(), k -> ConcurrentHashMap.newKeySet()) + .add(assignment.getUserId()); + + // Audit + auditService.logSuccess( + TypeActionAudit.REALM_ASSIGN, + "REALM_ASSIGNMENT", + assignment.getId(), + assignment.getUsername(), + assignment.getRealmName(), + assignment.getAssignedBy() != null ? assignment.getAssignedBy() : "system", + String.format("Assignation du realm %s à %s", assignment.getRealmName(), assignment.getUsername()) + ); + + log.info("Realm {} assigné avec succès à {}", assignment.getRealmName(), assignment.getUserId()); + return assignment; + } + + @Override + public void revokeRealmFromUser(@NotBlank String userId, @NotBlank String realmName) { + log.info("Révocation du realm {} pour l'utilisateur {}", realmName, userId); + + // Trouver et supprimer l'assignation + Optional assignment = assignmentsById.values().stream() + .filter(a -> a.getUserId().equals(userId) && a.getRealmName().equals(realmName)) + .findFirst(); + + if (assignment.isEmpty()) { + log.warn("Aucune assignation trouvée pour {} / {}", userId, realmName); + return; + } + + RealmAssignmentDTO assignmentToRemove = assignment.get(); + assignmentsById.remove(assignmentToRemove.getId()); + + // Mettre à jour les index + Set realms = userToRealms.get(userId); + if (realms != null) { + realms.remove(realmName); + if (realms.isEmpty()) { + userToRealms.remove(userId); + } + } + + Set users = realmToUsers.get(realmName); + if (users != null) { + users.remove(userId); + if (users.isEmpty()) { + realmToUsers.remove(realmName); + } + } + + // Audit + auditService.logSuccess( + TypeActionAudit.REALM_REVOKE, + "REALM_ASSIGNMENT", + assignmentToRemove.getId(), + assignmentToRemove.getUsername(), + realmName, + "system", + String.format("Révocation du realm %s pour %s", realmName, assignmentToRemove.getUsername()) + ); + + log.info("Realm {} révoqué avec succès pour {}", realmName, userId); + } + + @Override + public void revokeAllRealmsFromUser(@NotBlank String userId) { + log.info("Révocation de tous les realms pour l'utilisateur {}", userId); + + List userAssignments = getAssignmentsByUser(userId); + userAssignments.forEach(assignment -> + revokeRealmFromUser(userId, assignment.getRealmName()) + ); + + log.info("{} realm(s) révoqué(s) pour {}", userAssignments.size(), userId); + } + + @Override + public void revokeAllUsersFromRealm(@NotBlank String realmName) { + log.info("Révocation de tous les utilisateurs du realm {}", realmName); + + List realmAssignments = getAssignmentsByRealm(realmName); + realmAssignments.forEach(assignment -> + revokeRealmFromUser(assignment.getUserId(), realmName) + ); + + log.info("{} utilisateur(s) révoqué(s) du realm {}", realmAssignments.size(), realmName); + } + + @Override + public void setSuperAdmin(@NotBlank String userId, boolean superAdmin) { + log.info("Définition de {} comme super admin: {}", userId, superAdmin); + + if (superAdmin) { + superAdmins.add(userId); + auditService.logSuccess( + TypeActionAudit.REALM_SET_SUPER_ADMIN, + "USER", + userId, + userId, + "lions-user-manager", + "system", + String.format("Utilisateur %s défini comme super admin", userId) + ); + } else { + superAdmins.remove(userId); + auditService.logSuccess( + TypeActionAudit.REALM_SET_SUPER_ADMIN, + "USER", + userId, + userId, + "lions-user-manager", + "system", + String.format("Privilèges super admin retirés pour %s", userId) + ); + } + } + + @Override + public void deactivateAssignment(@NotBlank String assignmentId) { + log.info("Désactivation de l'assignation {}", assignmentId); + + RealmAssignmentDTO assignment = assignmentsById.get(assignmentId); + if (assignment == null) { + throw new IllegalArgumentException("Assignation non trouvée: " + assignmentId); + } + + assignment.setActive(false); + assignment.setDateModification(LocalDateTime.now()); + + auditService.logSuccess( + TypeActionAudit.REALM_DEACTIVATE, + "REALM_ASSIGNMENT", + assignment.getId(), + assignment.getUsername(), + assignment.getRealmName(), + "system", + String.format("Désactivation de l'assignation %s", assignmentId) + ); + } + + @Override + public void activateAssignment(@NotBlank String assignmentId) { + log.info("Activation de l'assignation {}", assignmentId); + + RealmAssignmentDTO assignment = assignmentsById.get(assignmentId); + if (assignment == null) { + throw new IllegalArgumentException("Assignation non trouvée: " + assignmentId); + } + + assignment.setActive(true); + assignment.setDateModification(LocalDateTime.now()); + + auditService.logSuccess( + TypeActionAudit.REALM_ACTIVATE, + "REALM_ASSIGNMENT", + assignment.getId(), + assignment.getUsername(), + assignment.getRealmName(), + "system", + String.format("Activation de l'assignation %s", assignmentId) + ); + } + + @Override + public long countAssignmentsByUser(@NotBlank String userId) { + return assignmentsById.values().stream() + .filter(assignment -> assignment.getUserId().equals(userId)) + .filter(RealmAssignmentDTO::isActive) + .filter(assignment -> !assignment.isExpired()) + .count(); + } + + @Override + public long countUsersByRealm(@NotBlank String realmName) { + return assignmentsById.values().stream() + .filter(assignment -> assignment.getRealmName().equals(realmName)) + .filter(RealmAssignmentDTO::isActive) + .filter(assignment -> !assignment.isExpired()) + .map(RealmAssignmentDTO::getUserId) + .distinct() + .count(); + } + + @Override + public boolean assignmentExists(@NotBlank String userId, @NotBlank String realmName) { + return assignmentsById.values().stream() + .anyMatch(assignment -> + assignment.getUserId().equals(userId) && + assignment.getRealmName().equals(realmName) && + assignment.isActive() + ); + } +} diff --git a/src/main/java/dev/lions/user/manager/service/impl/RoleServiceImpl.java b/src/main/java/dev/lions/user/manager/service/impl/RoleServiceImpl.java index d0d53a1..7ddc849 100644 --- a/src/main/java/dev/lions/user/manager/service/impl/RoleServiceImpl.java +++ b/src/main/java/dev/lions/user/manager/service/impl/RoleServiceImpl.java @@ -1,979 +1,979 @@ -package dev.lions.user.manager.service.impl; - -import dev.lions.user.manager.client.KeycloakAdminClient; -import dev.lions.user.manager.dto.role.RoleAssignmentDTO; -import dev.lions.user.manager.dto.role.RoleDTO; -import dev.lions.user.manager.enums.role.TypeRole; -import dev.lions.user.manager.mapper.RoleMapper; -import dev.lions.user.manager.service.RoleService; -import org.keycloak.admin.client.resource.UserResource; -import org.keycloak.representations.idm.UserRepresentation; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.ws.rs.NotFoundException; -import lombok.extern.slf4j.Slf4j; -import org.keycloak.admin.client.resource.*; -import org.keycloak.representations.idm.RoleRepresentation; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -/** - * Implémentation du service de gestion des rôles Keycloak - */ -@ApplicationScoped -@Slf4j -public class RoleServiceImpl implements RoleService { - - @Inject - KeycloakAdminClient keycloakAdminClient; - - // ==================== CRUD Realm Roles ==================== - - @Override - public RoleDTO createRealmRole(@Valid @NotNull RoleDTO roleDTO, @NotBlank String realmName) { - log.info("Création du rôle realm: {} dans le realm: {}", roleDTO.getName(), realmName); - - RolesResource rolesResource = keycloakAdminClient.getInstance() - .realm(realmName) - .roles(); - - // Vérifier si le rôle existe déjà - try { - rolesResource.get(roleDTO.getName()).toRepresentation(); - throw new IllegalArgumentException("Le rôle " + roleDTO.getName() + " existe déjà"); - } catch (NotFoundException e) { - // OK, le rôle n'existe pas - } - - RoleRepresentation roleRep = RoleMapper.toRepresentation(roleDTO); - rolesResource.create(roleRep); - - // Récupérer le rôle créé avec son ID - RoleRepresentation createdRole = rolesResource.get(roleDTO.getName()).toRepresentation(); - return RoleMapper.toDTO(createdRole, realmName, TypeRole.REALM_ROLE); - } - - // Méthodes privées helper pour utilisation interne - private Optional getRealmRoleById(String roleId, String realmName) { - log.debug("Récupération du rôle realm par ID: {} dans le realm: {}", roleId, realmName); - - try { - RolesResource rolesResource = keycloakAdminClient.getInstance() - .realm(realmName) - .roles(); - - // Keycloak ne permet pas de récupérer un rôle par ID directement, on doit lister tous les rôles - List roles = rolesResource.list(); - return roles.stream() - .filter(r -> r.getId().equals(roleId)) - .findFirst() - .map(r -> RoleMapper.toDTO(r, realmName, TypeRole.REALM_ROLE)); - } catch (Exception e) { - log.error("Erreur lors de la récupération du rôle realm {}", roleId, e); - return Optional.empty(); - } - } - - private Optional getRealmRoleByName(String roleName, String realmName) { - log.debug("Récupération du rôle realm par nom: {} dans le realm: {}", roleName, realmName); - - try { - RoleRepresentation roleRep = keycloakAdminClient.getInstance() - .realm(realmName) - .roles() - .get(roleName) - .toRepresentation(); - - return Optional.of(RoleMapper.toDTO(roleRep, realmName, TypeRole.REALM_ROLE)); - } catch (NotFoundException e) { - log.warn("Rôle realm {} non trouvé dans le realm {}", roleName, realmName); - return Optional.empty(); - } - } - - @Override - public RoleDTO updateRole(@NotBlank String roleId, - @Valid @NotNull RoleDTO role, - @NotBlank String realmName, - @NotNull TypeRole typeRole, - String clientName) { - log.info("Mise à jour du rôle {} (type: {}) dans le realm: {}", roleId, typeRole, realmName); - - if (typeRole == TypeRole.REALM_ROLE) { - // Trouver le nom du rôle par son ID - Optional existingRole = getRealmRoleById(roleId, realmName); - if (existingRole.isEmpty()) { - throw new jakarta.ws.rs.NotFoundException("Rôle non trouvé: " + roleId); - } - String roleName = existingRole.get().getName(); - - RoleResource roleResource = keycloakAdminClient.getInstance() - .realm(realmName) - .roles() - .get(roleName); - - RoleRepresentation roleRep = roleResource.toRepresentation(); - - // Mettre à jour uniquement les champs modifiables - if (role.getDescription() != null) { - roleRep.setDescription(role.getDescription()); - } - - roleResource.update(roleRep); - - // Retourner le rôle mis à jour - return RoleMapper.toDTO(roleResource.toRepresentation(), realmName, TypeRole.REALM_ROLE); - } else if (typeRole == TypeRole.CLIENT_ROLE && clientName != null) { - // Pour les rôles client, trouver le nom par ID puis mettre à jour - Optional existingRole = getRoleById(roleId, realmName, typeRole, clientName); - if (existingRole.isEmpty()) { - throw new jakarta.ws.rs.NotFoundException("Rôle non trouvé: " + roleId); - } - String roleName = existingRole.get().getName(); - - ClientsResource clientsResource = keycloakAdminClient.getInstance() - .realm(realmName) - .clients(); - - List clients = - clientsResource.findByClientId(clientName); - - if (clients.isEmpty()) { - throw new IllegalArgumentException("Client " + clientName + " non trouvé"); - } - - String internalClientId = clients.get(0).getId(); - RoleResource roleResource = clientsResource.get(internalClientId) - .roles() - .get(roleName); - - RoleRepresentation roleRep = roleResource.toRepresentation(); - - if (role.getDescription() != null) { - roleRep.setDescription(role.getDescription()); - } - - roleResource.update(roleRep); - - RoleDTO result = RoleMapper.toDTO(roleResource.toRepresentation(), realmName, TypeRole.CLIENT_ROLE); - result.setClientId(clientName); - return result; - } - - throw new IllegalArgumentException("Type de rôle non supporté pour la mise à jour: " + typeRole); - } - - @Override - public void deleteRole(@NotBlank String roleId, - @NotBlank String realmName, - @NotNull TypeRole typeRole, - String clientName) { - log.info("Suppression du rôle {} (type: {}) dans le realm: {}", roleId, typeRole, realmName); - - if (typeRole == TypeRole.REALM_ROLE) { - // Trouver le nom du rôle par son ID - Optional existingRole = getRealmRoleById(roleId, realmName); - if (existingRole.isEmpty()) { - throw new jakarta.ws.rs.NotFoundException("Rôle non trouvé: " + roleId); - } - String roleName = existingRole.get().getName(); - - keycloakAdminClient.getInstance() - .realm(realmName) - .roles() - .deleteRole(roleName); - } else if (typeRole == TypeRole.CLIENT_ROLE && clientName != null) { - // Trouver le nom du rôle par son ID - Optional existingRole = getRoleById(roleId, realmName, typeRole, clientName); - if (existingRole.isEmpty()) { - throw new jakarta.ws.rs.NotFoundException("Rôle non trouvé: " + roleId); - } - String roleName = existingRole.get().getName(); - - ClientsResource clientsResource = keycloakAdminClient.getInstance() - .realm(realmName) - .clients(); - - List clients = - clientsResource.findByClientId(clientName); - - if (clients.isEmpty()) { - throw new IllegalArgumentException("Client " + clientName + " non trouvé"); - } - - String internalClientId = clients.get(0).getId(); - clientsResource.get(internalClientId).roles().deleteRole(roleName); - } else { - throw new IllegalArgumentException("Type de rôle non supporté pour la suppression: " + typeRole); - } - } - - @Override - public List getAllRealmRoles(@NotBlank String realmName) { - log.info("Récupération de tous les rôles realm du realm: {}", realmName); - - try { - // Vérifier que le realm existe - if (!keycloakAdminClient.realmExists(realmName)) { - log.error("Le realm {} n'existe pas", realmName); - throw new IllegalArgumentException("Le realm '" + realmName + "' n'existe pas"); - } - - List roleReps = keycloakAdminClient.getInstance() - .realm(realmName) - .roles() - .list(); - - log.info("Récupération réussie: {} rôles trouvés dans le realm {}", roleReps.size(), realmName); - return RoleMapper.toDTOList(roleReps, realmName, TypeRole.REALM_ROLE); - } catch (Exception e) { - log.error("Erreur lors de la récupération des rôles realm du realm {}: {}", realmName, e.getMessage(), e); - String msg = e.getMessage() != null ? e.getMessage().toLowerCase() : ""; - if (msg.contains("not found") || msg.contains("404")) { - throw new IllegalArgumentException("Realm '" + realmName + "' introuvable: " + e.getMessage(), e); - } - throw new RuntimeException("Erreur lors de la récupération des rôles realm: " + e.getMessage(), e); - } - } - - // ==================== CRUD Client Roles ==================== - - @Override - public RoleDTO createClientRole(@Valid @NotNull RoleDTO roleDTO, @NotBlank String realmName, - @NotBlank String clientName) { - log.info("Création du rôle client: {} pour le client: {} dans le realm: {}", - roleDTO.getName(), clientName, realmName); - - ClientsResource clientsResource = keycloakAdminClient.getInstance() - .realm(realmName) - .clients(); - - // Trouver le client par clientId (on utilise clientName comme clientId) - List clients = - clientsResource.findByClientId(clientName); - - if (clients.isEmpty()) { - throw new IllegalArgumentException("Client " + clientName + " non trouvé"); - } - - String internalClientId = clients.get(0).getId(); - RolesResource rolesResource = clientsResource.get(internalClientId).roles(); - - // Vérifier si le rôle existe déjà - try { - rolesResource.get(roleDTO.getName()).toRepresentation(); - throw new IllegalArgumentException("Le rôle " + roleDTO.getName() + " existe déjà pour ce client"); - } catch (NotFoundException e) { - // OK, le rôle n'existe pas - } - - RoleRepresentation roleRep = RoleMapper.toRepresentation(roleDTO); - rolesResource.create(roleRep); - - // Récupérer le rôle créé - RoleRepresentation createdRole = rolesResource.get(roleDTO.getName()).toRepresentation(); - RoleDTO result = RoleMapper.toDTO(createdRole, realmName, TypeRole.CLIENT_ROLE); - result.setClientId(clientName); - - return result; - } - - // Méthode privée helper pour utilisation interne - private Optional getClientRoleByName(String roleName, String clientId, - String realmName) { - log.debug("Récupération du rôle client: {} pour le client: {} dans le realm: {}", - roleName, clientId, realmName); - - try { - ClientsResource clientsResource = keycloakAdminClient.getInstance() - .realm(realmName) - .clients(); - - List clients = - clientsResource.findByClientId(clientId); - - if (clients.isEmpty()) { - return Optional.empty(); - } - - String internalClientId = clients.get(0).getId(); - RoleRepresentation roleRep = clientsResource.get(internalClientId) - .roles() - .get(roleName) - .toRepresentation(); - - RoleDTO roleDTO = RoleMapper.toDTO(roleRep, realmName, TypeRole.CLIENT_ROLE); - roleDTO.setClientId(clientId); - - return Optional.of(roleDTO); - } catch (NotFoundException e) { - log.warn("Rôle client {} non trouvé pour le client {} dans le realm {}", - roleName, clientId, realmName); - return Optional.empty(); - } - } - - - @Override - public List getAllClientRoles(@NotBlank String realmName, @NotBlank String clientName) { - log.debug("Récupération de tous les rôles du client: {} dans le realm: {}", clientName, realmName); - - ClientsResource clientsResource = keycloakAdminClient.getInstance() - .realm(realmName) - .clients(); - - List clients = - clientsResource.findByClientId(clientName); - - if (clients.isEmpty()) { - return List.of(); - } - - String internalClientId = clients.get(0).getId(); - List roleReps = clientsResource.get(internalClientId) - .roles() - .list(); - - List roles = RoleMapper.toDTOList(roleReps, realmName, TypeRole.CLIENT_ROLE); - roles.forEach(role -> role.setClientId(clientName)); - - return roles; - } - - @Override - public Optional getRoleById(@NotBlank String roleId, - @NotBlank String realmName, - @NotNull TypeRole typeRole, - String clientName) { - log.debug("Récupération du rôle par ID: {} (type: {}) dans le realm: {}", roleId, typeRole, realmName); - - if (typeRole == TypeRole.REALM_ROLE) { - return getRealmRoleById(roleId, realmName); - } else if (typeRole == TypeRole.CLIENT_ROLE && clientName != null) { - // Pour les rôles client, on doit lister tous les rôles du client et trouver par ID - try { - ClientsResource clientsResource = keycloakAdminClient.getInstance() - .realm(realmName) - .clients(); - - List clients = - clientsResource.findByClientId(clientName); - - if (clients.isEmpty()) { - return Optional.empty(); - } - - String internalClientId = clients.get(0).getId(); - List roles = clientsResource.get(internalClientId) - .roles() - .list(); - - return roles.stream() - .filter(r -> r.getId().equals(roleId)) - .findFirst() - .map(r -> { - RoleDTO roleDTO = RoleMapper.toDTO(r, realmName, TypeRole.CLIENT_ROLE); - roleDTO.setClientId(clientName); - return roleDTO; - }); - } catch (Exception e) { - log.error("Erreur lors de la récupération du rôle client {}", roleId, e); - return Optional.empty(); - } - } - return Optional.empty(); - } - - @Override - public Optional getRoleByName(@NotBlank String roleName, - @NotBlank String realmName, - @NotNull TypeRole typeRole, - String clientName) { - log.debug("Récupération du rôle par nom: {} (type: {}) dans le realm: {}", roleName, typeRole, realmName); - - if (typeRole == TypeRole.REALM_ROLE) { - return getRealmRoleByName(roleName, realmName); - } else if (typeRole == TypeRole.CLIENT_ROLE && clientName != null) { - return getClientRoleByName(roleName, clientName, realmName); - } - return Optional.empty(); - } - - // ==================== Attribution de rôles ==================== - - @Override - public void assignRolesToUser(@Valid @NotNull RoleAssignmentDTO assignment) { - log.info("Attribution de {} rôles {} à l'utilisateur {} dans le realm {}", - assignment.getRoleNames().size(), assignment.getTypeRole(), assignment.getUserId(), assignment.getRealmName()); - - if (assignment.getTypeRole() == TypeRole.REALM_ROLE) { - assignRealmRolesToUser(assignment.getUserId(), assignment.getRoleNames(), assignment.getRealmName()); - } else if (assignment.getTypeRole() == TypeRole.CLIENT_ROLE && assignment.getClientName() != null) { - assignClientRolesToUser(assignment.getUserId(), assignment.getClientName(), assignment.getRoleNames(), assignment.getRealmName()); - } else { - throw new IllegalArgumentException("Données d'attribution invalides pour le type de rôle: " + assignment.getTypeRole()); - } - } - - @Override - public void revokeRolesFromUser(@Valid @NotNull RoleAssignmentDTO assignment) { - log.info("Révocation de {} rôles {} pour l'utilisateur {} dans le realm {}", - assignment.getRoleNames().size(), assignment.getTypeRole(), assignment.getUserId(), assignment.getRealmName()); - - if (assignment.getTypeRole() == TypeRole.REALM_ROLE) { - revokeRealmRolesFromUser(assignment.getUserId(), assignment.getRoleNames(), assignment.getRealmName()); - } else if (assignment.getTypeRole() == TypeRole.CLIENT_ROLE && assignment.getClientName() != null) { - revokeClientRolesFromUser(assignment.getUserId(), assignment.getClientName(), assignment.getRoleNames(), assignment.getRealmName()); - } else { - throw new IllegalArgumentException("Données de révocation invalides pour le type de rôle: " + assignment.getTypeRole()); - } - } - - private void assignRealmRolesToUser(String userId, List roleNames, - String realmName) { - log.info("Attribution de {} rôles realm à l'utilisateur {} dans le realm {}", - roleNames.size(), userId, realmName); - - UserResource userResource = keycloakAdminClient.getInstance() - .realm(realmName) - .users() - .get(userId); - - RolesResource rolesResource = keycloakAdminClient.getInstance() - .realm(realmName) - .roles(); - - List rolesToAssign = roleNames.stream() - .map(roleName -> { - try { - return rolesResource.get(roleName).toRepresentation(); - } catch (NotFoundException e) { - log.warn("Rôle {} non trouvé, ignoré", roleName); - return null; - } - }) - .filter(role -> role != null) - .collect(Collectors.toList()); - - if (!rolesToAssign.isEmpty()) { - userResource.roles().realmLevel().add(rolesToAssign); - } - } - - private void revokeRealmRolesFromUser(String userId, List roleNames, - String realmName) { - log.info("Révocation de {} rôles realm pour l'utilisateur {} dans le realm {}", - roleNames.size(), userId, realmName); - - UserResource userResource = keycloakAdminClient.getInstance() - .realm(realmName) - .users() - .get(userId); - - RolesResource rolesResource = keycloakAdminClient.getInstance() - .realm(realmName) - .roles(); - - List rolesToRevoke = roleNames.stream() - .map(roleName -> { - try { - return rolesResource.get(roleName).toRepresentation(); - } catch (NotFoundException e) { - log.warn("Rôle {} non trouvé, ignoré", roleName); - return null; - } - }) - .filter(role -> role != null) - .collect(Collectors.toList()); - - if (!rolesToRevoke.isEmpty()) { - userResource.roles().realmLevel().remove(rolesToRevoke); - } - } - - private void assignClientRolesToUser(String userId, String clientId, - List roleNames, String realmName) { - log.info("Attribution de {} rôles du client {} à l'utilisateur {} dans le realm {}", - roleNames.size(), clientId, userId, realmName); - - UserResource userResource = keycloakAdminClient.getInstance() - .realm(realmName) - .users() - .get(userId); - - ClientsResource clientsResource = keycloakAdminClient.getInstance() - .realm(realmName) - .clients(); - - List clients = - clientsResource.findByClientId(clientId); - - if (clients.isEmpty()) { - throw new IllegalArgumentException("Client " + clientId + " non trouvé"); - } - - String internalClientId = clients.get(0).getId(); - RolesResource rolesResource = clientsResource.get(internalClientId).roles(); - - List rolesToAssign = roleNames.stream() - .map(roleName -> { - try { - return rolesResource.get(roleName).toRepresentation(); - } catch (NotFoundException e) { - log.warn("Rôle client {} non trouvé, ignoré", roleName); - return null; - } - }) - .filter(role -> role != null) - .collect(Collectors.toList()); - - if (!rolesToAssign.isEmpty()) { - userResource.roles().clientLevel(internalClientId).add(rolesToAssign); - } - } - - private void revokeClientRolesFromUser(String userId, String clientId, - List roleNames, String realmName) { - log.info("Révocation de {} rôles du client {} pour l'utilisateur {} dans le realm {}", - roleNames.size(), clientId, userId, realmName); - - UserResource userResource = keycloakAdminClient.getInstance() - .realm(realmName) - .users() - .get(userId); - - ClientsResource clientsResource = keycloakAdminClient.getInstance() - .realm(realmName) - .clients(); - - List clients = - clientsResource.findByClientId(clientId); - - if (clients.isEmpty()) { - throw new IllegalArgumentException("Client " + clientId + " non trouvé"); - } - - String internalClientId = clients.get(0).getId(); - RolesResource rolesResource = clientsResource.get(internalClientId).roles(); - - List rolesToRevoke = roleNames.stream() - .map(roleName -> { - try { - return rolesResource.get(roleName).toRepresentation(); - } catch (NotFoundException e) { - log.warn("Rôle client {} non trouvé, ignoré", roleName); - return null; - } - }) - .filter(role -> role != null) - .collect(Collectors.toList()); - - if (!rolesToRevoke.isEmpty()) { - userResource.roles().clientLevel(internalClientId).remove(rolesToRevoke); - } - } - - @Override - public List getUserRealmRoles(@NotBlank String userId, @NotBlank String realmName) { - log.debug("Récupération des rôles realm de l'utilisateur {} dans le realm {}", userId, realmName); - - List roleReps = keycloakAdminClient.getInstance() - .realm(realmName) - .users() - .get(userId) - .roles() - .realmLevel() - .listAll(); - - return RoleMapper.toDTOList(roleReps, realmName, TypeRole.REALM_ROLE); - } - - @Override - public List getUserClientRoles(@NotBlank String userId, - @NotBlank String realmName, - @NotBlank String clientName) { - log.debug("Récupération des rôles du client {} pour l'utilisateur {} dans le realm {}", - clientName, userId, realmName); - - ClientsResource clientsResource = keycloakAdminClient.getInstance() - .realm(realmName) - .clients(); - - List clients = - clientsResource.findByClientId(clientName); - - if (clients.isEmpty()) { - return List.of(); - } - - String internalClientId = clients.get(0).getId(); - List roleReps = keycloakAdminClient.getInstance() - .realm(realmName) - .users() - .get(userId) - .roles() - .clientLevel(internalClientId) - .listAll(); - - List roles = RoleMapper.toDTOList(roleReps, realmName, TypeRole.CLIENT_ROLE); - roles.forEach(role -> role.setClientId(clientName)); - - return roles; - } - - @Override - public List getAllUserRoles(@NotBlank String userId, @NotBlank String realmName) { - log.debug("Récupération de tous les rôles de l'utilisateur {} dans le realm {}", userId, realmName); - - List allRoles = new ArrayList<>(); - - // Ajouter les rôles realm - allRoles.addAll(getUserRealmRoles(userId, realmName)); - - // Ajouter les rôles client pour tous les clients - try { - var clientsResource = keycloakAdminClient.getInstance() - .realm(realmName) - .clients(); - - List clients = clientsResource.findAll(); - - for (org.keycloak.representations.idm.ClientRepresentation client : clients) { - String clientId = client.getClientId(); - allRoles.addAll(getUserClientRoles(userId, realmName, clientId)); - } - } catch (Exception e) { - log.warn("Erreur lors de la récupération des rôles client pour l'utilisateur {}", userId, e); - } - - return allRoles; - } - - // ==================== Rôles composites ==================== - - @Override - public void addCompositeRoles(@NotBlank String parentRoleId, - @NotNull List childRoleIds, - @NotBlank String realmName, - @NotNull TypeRole typeRole, - String clientName) { - log.info("Ajout de {} rôles composites au rôle {} (type: {}) dans le realm {}", - childRoleIds.size(), parentRoleId, typeRole, realmName); - - // Trouver le nom du rôle parent par son ID - Optional parentRole = getRoleById(parentRoleId, realmName, typeRole, clientName); - if (parentRole.isEmpty()) { - throw new jakarta.ws.rs.NotFoundException("Rôle parent non trouvé: " + parentRoleId); - } - String parentRoleName = parentRole.get().getName(); - - RolesResource rolesResource = keycloakAdminClient.getInstance() - .realm(realmName) - .roles(); - - if (typeRole == TypeRole.REALM_ROLE) { - RoleResource roleResource = rolesResource.get(parentRoleName); - - // Convertir les IDs en noms de rôles - List childRoleNames = childRoleIds.stream() - .map(childRoleId -> { - Optional childRole = getRealmRoleById(childRoleId, realmName); - return childRole.map(RoleDTO::getName).orElse(null); - }) - .filter(name -> name != null) - .collect(Collectors.toList()); - - List compositesToAdd = childRoleNames.stream() - .map(compositeName -> { - try { - return rolesResource.get(compositeName).toRepresentation(); - } catch (NotFoundException e) { - log.warn("Rôle composite {} non trouvé, ignoré", compositeName); - return null; - } - }) - .filter(role -> role != null) - .collect(Collectors.toList()); - - if (!compositesToAdd.isEmpty()) { - roleResource.addComposites(compositesToAdd); - } - } else if (typeRole == TypeRole.CLIENT_ROLE && clientName != null) { - // Pour les rôles client, utiliser le client - ClientsResource clientsResource = keycloakAdminClient.getInstance() - .realm(realmName) - .clients(); - - List clients = - clientsResource.findByClientId(clientName); - - if (clients.isEmpty()) { - throw new IllegalArgumentException("Client " + clientName + " non trouvé"); - } - - String internalClientId = clients.get(0).getId(); - RolesResource clientRolesResource = clientsResource.get(internalClientId).roles(); - RoleResource roleResource = clientRolesResource.get(parentRoleName); - - // Convertir les IDs en noms de rôles - List childRoleNames = childRoleIds.stream() - .map(childRoleId -> { - Optional childRole = getRoleById(childRoleId, realmName, typeRole, clientName); - return childRole.map(RoleDTO::getName).orElse(null); - }) - .filter(name -> name != null) - .collect(Collectors.toList()); - - List compositesToAdd = childRoleNames.stream() - .map(compositeName -> { - try { - return clientRolesResource.get(compositeName).toRepresentation(); - } catch (NotFoundException e) { - log.warn("Rôle composite {} non trouvé, ignoré", compositeName); - return null; - } - }) - .filter(role -> role != null) - .collect(Collectors.toList()); - - if (!compositesToAdd.isEmpty()) { - roleResource.addComposites(compositesToAdd); - } - } - } - - @Override - public void removeCompositeRoles(@NotBlank String parentRoleId, - @NotNull List childRoleIds, - @NotBlank String realmName, - @NotNull TypeRole typeRole, - String clientName) { - log.info("Suppression de {} rôles composites du rôle {} (type: {}) dans le realm {}", - childRoleIds.size(), parentRoleId, typeRole, realmName); - - // Trouver le nom du rôle parent par son ID - Optional parentRole = getRoleById(parentRoleId, realmName, typeRole, clientName); - if (parentRole.isEmpty()) { - throw new jakarta.ws.rs.NotFoundException("Rôle parent non trouvé: " + parentRoleId); - } - String parentRoleName = parentRole.get().getName(); - - RolesResource rolesResource = keycloakAdminClient.getInstance() - .realm(realmName) - .roles(); - - if (typeRole == TypeRole.REALM_ROLE) { - RoleResource roleResource = rolesResource.get(parentRoleName); - - // Convertir les IDs en noms de rôles - List childRoleNames = childRoleIds.stream() - .map(childRoleId -> { - Optional childRole = getRealmRoleById(childRoleId, realmName); - return childRole.map(RoleDTO::getName).orElse(null); - }) - .filter(name -> name != null) - .collect(Collectors.toList()); - - List compositesToRemove = childRoleNames.stream() - .map(compositeName -> { - try { - return rolesResource.get(compositeName).toRepresentation(); - } catch (NotFoundException e) { - log.warn("Rôle composite {} non trouvé, ignoré", compositeName); - return null; - } - }) - .filter(role -> role != null) - .collect(Collectors.toList()); - - if (!compositesToRemove.isEmpty()) { - roleResource.deleteComposites(compositesToRemove); - } - } else if (typeRole == TypeRole.CLIENT_ROLE && clientName != null) { - ClientsResource clientsResource = keycloakAdminClient.getInstance() - .realm(realmName) - .clients(); - - List clients = - clientsResource.findByClientId(clientName); - - if (clients.isEmpty()) { - throw new IllegalArgumentException("Client " + clientName + " non trouvé"); - } - - String internalClientId = clients.get(0).getId(); - RolesResource clientRolesResource = clientsResource.get(internalClientId).roles(); - RoleResource roleResource = clientRolesResource.get(parentRoleName); - - // Convertir les IDs en noms de rôles - List childRoleNames = childRoleIds.stream() - .map(childRoleId -> { - Optional childRole = getRoleById(childRoleId, realmName, typeRole, clientName); - return childRole.map(RoleDTO::getName).orElse(null); - }) - .filter(name -> name != null) - .collect(Collectors.toList()); - - List compositesToRemove = childRoleNames.stream() - .map(compositeName -> { - try { - return clientRolesResource.get(compositeName).toRepresentation(); - } catch (NotFoundException e) { - log.warn("Rôle composite {} non trouvé, ignoré", compositeName); - return null; - } - }) - .filter(role -> role != null) - .collect(Collectors.toList()); - - if (!compositesToRemove.isEmpty()) { - roleResource.deleteComposites(compositesToRemove); - } - } - } - - - @Override - public List getCompositeRoles(@NotBlank String roleId, - @NotBlank String realmName, - @NotNull TypeRole typeRole, - String clientName) { - log.debug("Récupération des rôles composites du rôle {} dans le realm {}", roleId, realmName); - - // Pour récupérer par ID, on doit d'abord trouver le nom du rôle - // Comme Keycloak ne permet pas de récupérer directement par ID, on doit lister et trouver - RolesResource rolesResource = keycloakAdminClient.getInstance() - .realm(realmName) - .roles(); - - RoleRepresentation roleRep = rolesResource.list().stream() - .filter(r -> r.getId().equals(roleId)) - .findFirst() - .orElseThrow(() -> new jakarta.ws.rs.NotFoundException("Rôle non trouvé: " + roleId)); - - java.util.Set compositesSet = rolesResource - .get(roleRep.getName()) - .getRoleComposites(); - - List composites = new ArrayList<>(compositesSet); - - return RoleMapper.toDTOList(composites, realmName, TypeRole.COMPOSITE_ROLE); - } - - // ==================== Vérification de permissions ==================== - - @Override - public boolean userHasRole(@NotBlank String userId, - @NotBlank String roleName, - @NotBlank String realmName, - @NotNull TypeRole typeRole, - String clientName) { - log.debug("Vérification si l'utilisateur {} a le rôle {} (type: {}) dans le realm {}", - userId, roleName, typeRole, realmName); - - if (typeRole == TypeRole.REALM_ROLE) { - List userRoles = keycloakAdminClient.getInstance() - .realm(realmName) - .users() - .get(userId) - .roles() - .realmLevel() - .listEffective(); // Incluant les rôles hérités via composites - - return userRoles.stream() - .anyMatch(role -> role.getName().equals(roleName)); - } else if (typeRole == TypeRole.CLIENT_ROLE && clientName != null) { - ClientsResource clientsResource = keycloakAdminClient.getInstance() - .realm(realmName) - .clients(); - - List clients = - clientsResource.findByClientId(clientName); - - if (clients.isEmpty()) { - return false; - } - - String internalClientId = clients.get(0).getId(); - List userClientRoles = keycloakAdminClient.getInstance() - .realm(realmName) - .users() - .get(userId) - .roles() - .clientLevel(internalClientId) - .listEffective(); - - return userClientRoles.stream() - .anyMatch(role -> role.getName().equals(roleName)); - } - - return false; - } - - @Override - public boolean roleExists(@NotBlank String roleName, - @NotBlank String realmName, - @NotNull TypeRole typeRole, - String clientName) { - log.debug("Vérification de l'existence du rôle {} (type: {}) dans le realm {}", - roleName, typeRole, realmName); - - return getRoleByName(roleName, realmName, typeRole, clientName).isPresent(); - } - - @Override - public long countUsersWithRole(@NotBlank String roleId, - @NotBlank String realmName, - @NotNull TypeRole typeRole, - String clientName) { - log.debug("Comptage des utilisateurs ayant le rôle {} (type: {}) dans le realm {}", - roleId, typeRole, realmName); - - // Trouver le nom du rôle par son ID - Optional role = getRoleById(roleId, realmName, typeRole, clientName); - if (role.isEmpty()) { - return 0; - } - - String roleName = role.get().getName(); - - try { - // Keycloak ne fournit pas directement cette fonctionnalité via l'API Admin - // On doit lister tous les utilisateurs et vérifier leurs rôles - // C'est coûteux mais nécessaire - List users = keycloakAdminClient.getInstance() - .realm(realmName) - .users() - .list(); - - long count = 0; - for (UserRepresentation user : users) { - if (userHasRole(user.getId(), roleName, realmName, typeRole, clientName)) { - count++; - } - } - - return count; - } catch (Exception e) { - log.error("Erreur lors du comptage des utilisateurs avec le rôle {}", roleId, e); - return 0; - } - } - - // Méthodes privées pour compatibilité interne (utilisées par les nouvelles méthodes publiques) - private boolean userHasRealmRole(String userId, String roleName, - String realmName) { - return userHasRole(userId, roleName, realmName, TypeRole.REALM_ROLE, null); - } - - private boolean userHasClientRole(String userId, String clientId, - String roleName, String realmName) { - return userHasRole(userId, roleName, realmName, TypeRole.CLIENT_ROLE, clientId); - } -} +package dev.lions.user.manager.service.impl; + +import dev.lions.user.manager.client.KeycloakAdminClient; +import dev.lions.user.manager.dto.role.RoleAssignmentDTO; +import dev.lions.user.manager.dto.role.RoleDTO; +import dev.lions.user.manager.enums.role.TypeRole; +import dev.lions.user.manager.mapper.RoleMapper; +import dev.lions.user.manager.service.RoleService; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.representations.idm.UserRepresentation; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.NotFoundException; +import lombok.extern.slf4j.Slf4j; +import org.keycloak.admin.client.resource.*; +import org.keycloak.representations.idm.RoleRepresentation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Implémentation du service de gestion des rôles Keycloak + */ +@ApplicationScoped +@Slf4j +public class RoleServiceImpl implements RoleService { + + @Inject + KeycloakAdminClient keycloakAdminClient; + + // ==================== CRUD Realm Roles ==================== + + @Override + public RoleDTO createRealmRole(@Valid @NotNull RoleDTO roleDTO, @NotBlank String realmName) { + log.info("Création du rôle realm: {} dans le realm: {}", roleDTO.getName(), realmName); + + RolesResource rolesResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles(); + + // Vérifier si le rôle existe déjà + try { + rolesResource.get(roleDTO.getName()).toRepresentation(); + throw new IllegalArgumentException("Le rôle " + roleDTO.getName() + " existe déjà"); + } catch (NotFoundException e) { + // OK, le rôle n'existe pas + } + + RoleRepresentation roleRep = RoleMapper.toRepresentation(roleDTO); + rolesResource.create(roleRep); + + // Récupérer le rôle créé avec son ID + RoleRepresentation createdRole = rolesResource.get(roleDTO.getName()).toRepresentation(); + return RoleMapper.toDTO(createdRole, realmName, TypeRole.REALM_ROLE); + } + + // Méthodes privées helper pour utilisation interne + private Optional getRealmRoleById(String roleId, String realmName) { + log.debug("Récupération du rôle realm par ID: {} dans le realm: {}", roleId, realmName); + + try { + RolesResource rolesResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles(); + + // Keycloak ne permet pas de récupérer un rôle par ID directement, on doit lister tous les rôles + List roles = rolesResource.list(); + return roles.stream() + .filter(r -> r.getId().equals(roleId)) + .findFirst() + .map(r -> RoleMapper.toDTO(r, realmName, TypeRole.REALM_ROLE)); + } catch (Exception e) { + log.error("Erreur lors de la récupération du rôle realm {}", roleId, e); + return Optional.empty(); + } + } + + private Optional getRealmRoleByName(String roleName, String realmName) { + log.debug("Récupération du rôle realm par nom: {} dans le realm: {}", roleName, realmName); + + try { + RoleRepresentation roleRep = keycloakAdminClient.getInstance() + .realm(realmName) + .roles() + .get(roleName) + .toRepresentation(); + + return Optional.of(RoleMapper.toDTO(roleRep, realmName, TypeRole.REALM_ROLE)); + } catch (NotFoundException e) { + log.warn("Rôle realm {} non trouvé dans le realm {}", roleName, realmName); + return Optional.empty(); + } + } + + @Override + public RoleDTO updateRole(@NotBlank String roleId, + @Valid @NotNull RoleDTO role, + @NotBlank String realmName, + @NotNull TypeRole typeRole, + String clientName) { + log.info("Mise à jour du rôle {} (type: {}) dans le realm: {}", roleId, typeRole, realmName); + + if (typeRole == TypeRole.REALM_ROLE) { + // Trouver le nom du rôle par son ID + Optional existingRole = getRealmRoleById(roleId, realmName); + if (existingRole.isEmpty()) { + throw new jakarta.ws.rs.NotFoundException("Rôle non trouvé: " + roleId); + } + String roleName = existingRole.get().getName(); + + RoleResource roleResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles() + .get(roleName); + + RoleRepresentation roleRep = roleResource.toRepresentation(); + + // Mettre à jour uniquement les champs modifiables + if (role.getDescription() != null) { + roleRep.setDescription(role.getDescription()); + } + + roleResource.update(roleRep); + + // Retourner le rôle mis à jour + return RoleMapper.toDTO(roleResource.toRepresentation(), realmName, TypeRole.REALM_ROLE); + } else if (typeRole == TypeRole.CLIENT_ROLE && clientName != null) { + // Pour les rôles client, trouver le nom par ID puis mettre à jour + Optional existingRole = getRoleById(roleId, realmName, typeRole, clientName); + if (existingRole.isEmpty()) { + throw new jakarta.ws.rs.NotFoundException("Rôle non trouvé: " + roleId); + } + String roleName = existingRole.get().getName(); + + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientName); + + if (clients.isEmpty()) { + throw new IllegalArgumentException("Client " + clientName + " non trouvé"); + } + + String internalClientId = clients.get(0).getId(); + RoleResource roleResource = clientsResource.get(internalClientId) + .roles() + .get(roleName); + + RoleRepresentation roleRep = roleResource.toRepresentation(); + + if (role.getDescription() != null) { + roleRep.setDescription(role.getDescription()); + } + + roleResource.update(roleRep); + + RoleDTO result = RoleMapper.toDTO(roleResource.toRepresentation(), realmName, TypeRole.CLIENT_ROLE); + result.setClientId(clientName); + return result; + } + + throw new IllegalArgumentException("Type de rôle non supporté pour la mise à jour: " + typeRole); + } + + @Override + public void deleteRole(@NotBlank String roleId, + @NotBlank String realmName, + @NotNull TypeRole typeRole, + String clientName) { + log.info("Suppression du rôle {} (type: {}) dans le realm: {}", roleId, typeRole, realmName); + + if (typeRole == TypeRole.REALM_ROLE) { + // Trouver le nom du rôle par son ID + Optional existingRole = getRealmRoleById(roleId, realmName); + if (existingRole.isEmpty()) { + throw new jakarta.ws.rs.NotFoundException("Rôle non trouvé: " + roleId); + } + String roleName = existingRole.get().getName(); + + keycloakAdminClient.getInstance() + .realm(realmName) + .roles() + .deleteRole(roleName); + } else if (typeRole == TypeRole.CLIENT_ROLE && clientName != null) { + // Trouver le nom du rôle par son ID + Optional existingRole = getRoleById(roleId, realmName, typeRole, clientName); + if (existingRole.isEmpty()) { + throw new jakarta.ws.rs.NotFoundException("Rôle non trouvé: " + roleId); + } + String roleName = existingRole.get().getName(); + + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientName); + + if (clients.isEmpty()) { + throw new IllegalArgumentException("Client " + clientName + " non trouvé"); + } + + String internalClientId = clients.get(0).getId(); + clientsResource.get(internalClientId).roles().deleteRole(roleName); + } else { + throw new IllegalArgumentException("Type de rôle non supporté pour la suppression: " + typeRole); + } + } + + @Override + public List getAllRealmRoles(@NotBlank String realmName) { + log.info("Récupération de tous les rôles realm du realm: {}", realmName); + + try { + // Vérifier que le realm existe + if (!keycloakAdminClient.realmExists(realmName)) { + log.error("Le realm {} n'existe pas", realmName); + throw new IllegalArgumentException("Le realm '" + realmName + "' n'existe pas"); + } + + List roleReps = keycloakAdminClient.getInstance() + .realm(realmName) + .roles() + .list(); + + log.info("Récupération réussie: {} rôles trouvés dans le realm {}", roleReps.size(), realmName); + return RoleMapper.toDTOList(roleReps, realmName, TypeRole.REALM_ROLE); + } catch (Exception e) { + log.error("Erreur lors de la récupération des rôles realm du realm {}: {}", realmName, e.getMessage(), e); + String msg = e.getMessage() != null ? e.getMessage().toLowerCase() : ""; + if (msg.contains("not found") || msg.contains("404")) { + throw new IllegalArgumentException("Realm '" + realmName + "' introuvable: " + e.getMessage(), e); + } + throw new RuntimeException("Erreur lors de la récupération des rôles realm: " + e.getMessage(), e); + } + } + + // ==================== CRUD Client Roles ==================== + + @Override + public RoleDTO createClientRole(@Valid @NotNull RoleDTO roleDTO, @NotBlank String realmName, + @NotBlank String clientName) { + log.info("Création du rôle client: {} pour le client: {} dans le realm: {}", + roleDTO.getName(), clientName, realmName); + + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + // Trouver le client par clientId (on utilise clientName comme clientId) + List clients = + clientsResource.findByClientId(clientName); + + if (clients.isEmpty()) { + throw new IllegalArgumentException("Client " + clientName + " non trouvé"); + } + + String internalClientId = clients.get(0).getId(); + RolesResource rolesResource = clientsResource.get(internalClientId).roles(); + + // Vérifier si le rôle existe déjà + try { + rolesResource.get(roleDTO.getName()).toRepresentation(); + throw new IllegalArgumentException("Le rôle " + roleDTO.getName() + " existe déjà pour ce client"); + } catch (NotFoundException e) { + // OK, le rôle n'existe pas + } + + RoleRepresentation roleRep = RoleMapper.toRepresentation(roleDTO); + rolesResource.create(roleRep); + + // Récupérer le rôle créé + RoleRepresentation createdRole = rolesResource.get(roleDTO.getName()).toRepresentation(); + RoleDTO result = RoleMapper.toDTO(createdRole, realmName, TypeRole.CLIENT_ROLE); + result.setClientId(clientName); + + return result; + } + + // Méthode privée helper pour utilisation interne + private Optional getClientRoleByName(String roleName, String clientId, + String realmName) { + log.debug("Récupération du rôle client: {} pour le client: {} dans le realm: {}", + roleName, clientId, realmName); + + try { + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientId); + + if (clients.isEmpty()) { + return Optional.empty(); + } + + String internalClientId = clients.get(0).getId(); + RoleRepresentation roleRep = clientsResource.get(internalClientId) + .roles() + .get(roleName) + .toRepresentation(); + + RoleDTO roleDTO = RoleMapper.toDTO(roleRep, realmName, TypeRole.CLIENT_ROLE); + roleDTO.setClientId(clientId); + + return Optional.of(roleDTO); + } catch (NotFoundException e) { + log.warn("Rôle client {} non trouvé pour le client {} dans le realm {}", + roleName, clientId, realmName); + return Optional.empty(); + } + } + + + @Override + public List getAllClientRoles(@NotBlank String realmName, @NotBlank String clientName) { + log.debug("Récupération de tous les rôles du client: {} dans le realm: {}", clientName, realmName); + + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientName); + + if (clients.isEmpty()) { + return List.of(); + } + + String internalClientId = clients.get(0).getId(); + List roleReps = clientsResource.get(internalClientId) + .roles() + .list(); + + List roles = RoleMapper.toDTOList(roleReps, realmName, TypeRole.CLIENT_ROLE); + roles.forEach(role -> role.setClientId(clientName)); + + return roles; + } + + @Override + public Optional getRoleById(@NotBlank String roleId, + @NotBlank String realmName, + @NotNull TypeRole typeRole, + String clientName) { + log.debug("Récupération du rôle par ID: {} (type: {}) dans le realm: {}", roleId, typeRole, realmName); + + if (typeRole == TypeRole.REALM_ROLE) { + return getRealmRoleById(roleId, realmName); + } else if (typeRole == TypeRole.CLIENT_ROLE && clientName != null) { + // Pour les rôles client, on doit lister tous les rôles du client et trouver par ID + try { + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientName); + + if (clients.isEmpty()) { + return Optional.empty(); + } + + String internalClientId = clients.get(0).getId(); + List roles = clientsResource.get(internalClientId) + .roles() + .list(); + + return roles.stream() + .filter(r -> r.getId().equals(roleId)) + .findFirst() + .map(r -> { + RoleDTO roleDTO = RoleMapper.toDTO(r, realmName, TypeRole.CLIENT_ROLE); + roleDTO.setClientId(clientName); + return roleDTO; + }); + } catch (Exception e) { + log.error("Erreur lors de la récupération du rôle client {}", roleId, e); + return Optional.empty(); + } + } + return Optional.empty(); + } + + @Override + public Optional getRoleByName(@NotBlank String roleName, + @NotBlank String realmName, + @NotNull TypeRole typeRole, + String clientName) { + log.debug("Récupération du rôle par nom: {} (type: {}) dans le realm: {}", roleName, typeRole, realmName); + + if (typeRole == TypeRole.REALM_ROLE) { + return getRealmRoleByName(roleName, realmName); + } else if (typeRole == TypeRole.CLIENT_ROLE && clientName != null) { + return getClientRoleByName(roleName, clientName, realmName); + } + return Optional.empty(); + } + + // ==================== Attribution de rôles ==================== + + @Override + public void assignRolesToUser(@Valid @NotNull RoleAssignmentDTO assignment) { + log.info("Attribution de {} rôles {} à l'utilisateur {} dans le realm {}", + assignment.getRoleNames().size(), assignment.getTypeRole(), assignment.getUserId(), assignment.getRealmName()); + + if (assignment.getTypeRole() == TypeRole.REALM_ROLE) { + assignRealmRolesToUser(assignment.getUserId(), assignment.getRoleNames(), assignment.getRealmName()); + } else if (assignment.getTypeRole() == TypeRole.CLIENT_ROLE && assignment.getClientName() != null) { + assignClientRolesToUser(assignment.getUserId(), assignment.getClientName(), assignment.getRoleNames(), assignment.getRealmName()); + } else { + throw new IllegalArgumentException("Données d'attribution invalides pour le type de rôle: " + assignment.getTypeRole()); + } + } + + @Override + public void revokeRolesFromUser(@Valid @NotNull RoleAssignmentDTO assignment) { + log.info("Révocation de {} rôles {} pour l'utilisateur {} dans le realm {}", + assignment.getRoleNames().size(), assignment.getTypeRole(), assignment.getUserId(), assignment.getRealmName()); + + if (assignment.getTypeRole() == TypeRole.REALM_ROLE) { + revokeRealmRolesFromUser(assignment.getUserId(), assignment.getRoleNames(), assignment.getRealmName()); + } else if (assignment.getTypeRole() == TypeRole.CLIENT_ROLE && assignment.getClientName() != null) { + revokeClientRolesFromUser(assignment.getUserId(), assignment.getClientName(), assignment.getRoleNames(), assignment.getRealmName()); + } else { + throw new IllegalArgumentException("Données de révocation invalides pour le type de rôle: " + assignment.getTypeRole()); + } + } + + private void assignRealmRolesToUser(String userId, List roleNames, + String realmName) { + log.info("Attribution de {} rôles realm à l'utilisateur {} dans le realm {}", + roleNames.size(), userId, realmName); + + UserResource userResource = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId); + + RolesResource rolesResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles(); + + List rolesToAssign = roleNames.stream() + .map(roleName -> { + try { + return rolesResource.get(roleName).toRepresentation(); + } catch (NotFoundException e) { + log.warn("Rôle {} non trouvé, ignoré", roleName); + return null; + } + }) + .filter(role -> role != null) + .collect(Collectors.toList()); + + if (!rolesToAssign.isEmpty()) { + userResource.roles().realmLevel().add(rolesToAssign); + } + } + + private void revokeRealmRolesFromUser(String userId, List roleNames, + String realmName) { + log.info("Révocation de {} rôles realm pour l'utilisateur {} dans le realm {}", + roleNames.size(), userId, realmName); + + UserResource userResource = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId); + + RolesResource rolesResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles(); + + List rolesToRevoke = roleNames.stream() + .map(roleName -> { + try { + return rolesResource.get(roleName).toRepresentation(); + } catch (NotFoundException e) { + log.warn("Rôle {} non trouvé, ignoré", roleName); + return null; + } + }) + .filter(role -> role != null) + .collect(Collectors.toList()); + + if (!rolesToRevoke.isEmpty()) { + userResource.roles().realmLevel().remove(rolesToRevoke); + } + } + + private void assignClientRolesToUser(String userId, String clientId, + List roleNames, String realmName) { + log.info("Attribution de {} rôles du client {} à l'utilisateur {} dans le realm {}", + roleNames.size(), clientId, userId, realmName); + + UserResource userResource = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId); + + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientId); + + if (clients.isEmpty()) { + throw new IllegalArgumentException("Client " + clientId + " non trouvé"); + } + + String internalClientId = clients.get(0).getId(); + RolesResource rolesResource = clientsResource.get(internalClientId).roles(); + + List rolesToAssign = roleNames.stream() + .map(roleName -> { + try { + return rolesResource.get(roleName).toRepresentation(); + } catch (NotFoundException e) { + log.warn("Rôle client {} non trouvé, ignoré", roleName); + return null; + } + }) + .filter(role -> role != null) + .collect(Collectors.toList()); + + if (!rolesToAssign.isEmpty()) { + userResource.roles().clientLevel(internalClientId).add(rolesToAssign); + } + } + + private void revokeClientRolesFromUser(String userId, String clientId, + List roleNames, String realmName) { + log.info("Révocation de {} rôles du client {} pour l'utilisateur {} dans le realm {}", + roleNames.size(), clientId, userId, realmName); + + UserResource userResource = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId); + + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientId); + + if (clients.isEmpty()) { + throw new IllegalArgumentException("Client " + clientId + " non trouvé"); + } + + String internalClientId = clients.get(0).getId(); + RolesResource rolesResource = clientsResource.get(internalClientId).roles(); + + List rolesToRevoke = roleNames.stream() + .map(roleName -> { + try { + return rolesResource.get(roleName).toRepresentation(); + } catch (NotFoundException e) { + log.warn("Rôle client {} non trouvé, ignoré", roleName); + return null; + } + }) + .filter(role -> role != null) + .collect(Collectors.toList()); + + if (!rolesToRevoke.isEmpty()) { + userResource.roles().clientLevel(internalClientId).remove(rolesToRevoke); + } + } + + @Override + public List getUserRealmRoles(@NotBlank String userId, @NotBlank String realmName) { + log.debug("Récupération des rôles realm de l'utilisateur {} dans le realm {}", userId, realmName); + + List roleReps = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId) + .roles() + .realmLevel() + .listAll(); + + return RoleMapper.toDTOList(roleReps, realmName, TypeRole.REALM_ROLE); + } + + @Override + public List getUserClientRoles(@NotBlank String userId, + @NotBlank String realmName, + @NotBlank String clientName) { + log.debug("Récupération des rôles du client {} pour l'utilisateur {} dans le realm {}", + clientName, userId, realmName); + + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientName); + + if (clients.isEmpty()) { + return List.of(); + } + + String internalClientId = clients.get(0).getId(); + List roleReps = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId) + .roles() + .clientLevel(internalClientId) + .listAll(); + + List roles = RoleMapper.toDTOList(roleReps, realmName, TypeRole.CLIENT_ROLE); + roles.forEach(role -> role.setClientId(clientName)); + + return roles; + } + + @Override + public List getAllUserRoles(@NotBlank String userId, @NotBlank String realmName) { + log.debug("Récupération de tous les rôles de l'utilisateur {} dans le realm {}", userId, realmName); + + List allRoles = new ArrayList<>(); + + // Ajouter les rôles realm + allRoles.addAll(getUserRealmRoles(userId, realmName)); + + // Ajouter les rôles client pour tous les clients + try { + var clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = clientsResource.findAll(); + + for (org.keycloak.representations.idm.ClientRepresentation client : clients) { + String clientId = client.getClientId(); + allRoles.addAll(getUserClientRoles(userId, realmName, clientId)); + } + } catch (Exception e) { + log.warn("Erreur lors de la récupération des rôles client pour l'utilisateur {}", userId, e); + } + + return allRoles; + } + + // ==================== Rôles composites ==================== + + @Override + public void addCompositeRoles(@NotBlank String parentRoleId, + @NotNull List childRoleIds, + @NotBlank String realmName, + @NotNull TypeRole typeRole, + String clientName) { + log.info("Ajout de {} rôles composites au rôle {} (type: {}) dans le realm {}", + childRoleIds.size(), parentRoleId, typeRole, realmName); + + // Trouver le nom du rôle parent par son ID + Optional parentRole = getRoleById(parentRoleId, realmName, typeRole, clientName); + if (parentRole.isEmpty()) { + throw new jakarta.ws.rs.NotFoundException("Rôle parent non trouvé: " + parentRoleId); + } + String parentRoleName = parentRole.get().getName(); + + RolesResource rolesResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles(); + + if (typeRole == TypeRole.REALM_ROLE) { + RoleResource roleResource = rolesResource.get(parentRoleName); + + // Convertir les IDs en noms de rôles + List childRoleNames = childRoleIds.stream() + .map(childRoleId -> { + Optional childRole = getRealmRoleById(childRoleId, realmName); + return childRole.map(RoleDTO::getName).orElse(null); + }) + .filter(name -> name != null) + .collect(Collectors.toList()); + + List compositesToAdd = childRoleNames.stream() + .map(compositeName -> { + try { + return rolesResource.get(compositeName).toRepresentation(); + } catch (NotFoundException e) { + log.warn("Rôle composite {} non trouvé, ignoré", compositeName); + return null; + } + }) + .filter(role -> role != null) + .collect(Collectors.toList()); + + if (!compositesToAdd.isEmpty()) { + roleResource.addComposites(compositesToAdd); + } + } else if (typeRole == TypeRole.CLIENT_ROLE && clientName != null) { + // Pour les rôles client, utiliser le client + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientName); + + if (clients.isEmpty()) { + throw new IllegalArgumentException("Client " + clientName + " non trouvé"); + } + + String internalClientId = clients.get(0).getId(); + RolesResource clientRolesResource = clientsResource.get(internalClientId).roles(); + RoleResource roleResource = clientRolesResource.get(parentRoleName); + + // Convertir les IDs en noms de rôles + List childRoleNames = childRoleIds.stream() + .map(childRoleId -> { + Optional childRole = getRoleById(childRoleId, realmName, typeRole, clientName); + return childRole.map(RoleDTO::getName).orElse(null); + }) + .filter(name -> name != null) + .collect(Collectors.toList()); + + List compositesToAdd = childRoleNames.stream() + .map(compositeName -> { + try { + return clientRolesResource.get(compositeName).toRepresentation(); + } catch (NotFoundException e) { + log.warn("Rôle composite {} non trouvé, ignoré", compositeName); + return null; + } + }) + .filter(role -> role != null) + .collect(Collectors.toList()); + + if (!compositesToAdd.isEmpty()) { + roleResource.addComposites(compositesToAdd); + } + } + } + + @Override + public void removeCompositeRoles(@NotBlank String parentRoleId, + @NotNull List childRoleIds, + @NotBlank String realmName, + @NotNull TypeRole typeRole, + String clientName) { + log.info("Suppression de {} rôles composites du rôle {} (type: {}) dans le realm {}", + childRoleIds.size(), parentRoleId, typeRole, realmName); + + // Trouver le nom du rôle parent par son ID + Optional parentRole = getRoleById(parentRoleId, realmName, typeRole, clientName); + if (parentRole.isEmpty()) { + throw new jakarta.ws.rs.NotFoundException("Rôle parent non trouvé: " + parentRoleId); + } + String parentRoleName = parentRole.get().getName(); + + RolesResource rolesResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles(); + + if (typeRole == TypeRole.REALM_ROLE) { + RoleResource roleResource = rolesResource.get(parentRoleName); + + // Convertir les IDs en noms de rôles + List childRoleNames = childRoleIds.stream() + .map(childRoleId -> { + Optional childRole = getRealmRoleById(childRoleId, realmName); + return childRole.map(RoleDTO::getName).orElse(null); + }) + .filter(name -> name != null) + .collect(Collectors.toList()); + + List compositesToRemove = childRoleNames.stream() + .map(compositeName -> { + try { + return rolesResource.get(compositeName).toRepresentation(); + } catch (NotFoundException e) { + log.warn("Rôle composite {} non trouvé, ignoré", compositeName); + return null; + } + }) + .filter(role -> role != null) + .collect(Collectors.toList()); + + if (!compositesToRemove.isEmpty()) { + roleResource.deleteComposites(compositesToRemove); + } + } else if (typeRole == TypeRole.CLIENT_ROLE && clientName != null) { + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientName); + + if (clients.isEmpty()) { + throw new IllegalArgumentException("Client " + clientName + " non trouvé"); + } + + String internalClientId = clients.get(0).getId(); + RolesResource clientRolesResource = clientsResource.get(internalClientId).roles(); + RoleResource roleResource = clientRolesResource.get(parentRoleName); + + // Convertir les IDs en noms de rôles + List childRoleNames = childRoleIds.stream() + .map(childRoleId -> { + Optional childRole = getRoleById(childRoleId, realmName, typeRole, clientName); + return childRole.map(RoleDTO::getName).orElse(null); + }) + .filter(name -> name != null) + .collect(Collectors.toList()); + + List compositesToRemove = childRoleNames.stream() + .map(compositeName -> { + try { + return clientRolesResource.get(compositeName).toRepresentation(); + } catch (NotFoundException e) { + log.warn("Rôle composite {} non trouvé, ignoré", compositeName); + return null; + } + }) + .filter(role -> role != null) + .collect(Collectors.toList()); + + if (!compositesToRemove.isEmpty()) { + roleResource.deleteComposites(compositesToRemove); + } + } + } + + + @Override + public List getCompositeRoles(@NotBlank String roleId, + @NotBlank String realmName, + @NotNull TypeRole typeRole, + String clientName) { + log.debug("Récupération des rôles composites du rôle {} dans le realm {}", roleId, realmName); + + // Pour récupérer par ID, on doit d'abord trouver le nom du rôle + // Comme Keycloak ne permet pas de récupérer directement par ID, on doit lister et trouver + RolesResource rolesResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles(); + + RoleRepresentation roleRep = rolesResource.list().stream() + .filter(r -> r.getId().equals(roleId)) + .findFirst() + .orElseThrow(() -> new jakarta.ws.rs.NotFoundException("Rôle non trouvé: " + roleId)); + + java.util.Set compositesSet = rolesResource + .get(roleRep.getName()) + .getRoleComposites(); + + List composites = new ArrayList<>(compositesSet); + + return RoleMapper.toDTOList(composites, realmName, TypeRole.COMPOSITE_ROLE); + } + + // ==================== Vérification de permissions ==================== + + @Override + public boolean userHasRole(@NotBlank String userId, + @NotBlank String roleName, + @NotBlank String realmName, + @NotNull TypeRole typeRole, + String clientName) { + log.debug("Vérification si l'utilisateur {} a le rôle {} (type: {}) dans le realm {}", + userId, roleName, typeRole, realmName); + + if (typeRole == TypeRole.REALM_ROLE) { + List userRoles = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId) + .roles() + .realmLevel() + .listEffective(); // Incluant les rôles hérités via composites + + return userRoles.stream() + .anyMatch(role -> role.getName().equals(roleName)); + } else if (typeRole == TypeRole.CLIENT_ROLE && clientName != null) { + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientName); + + if (clients.isEmpty()) { + return false; + } + + String internalClientId = clients.get(0).getId(); + List userClientRoles = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId) + .roles() + .clientLevel(internalClientId) + .listEffective(); + + return userClientRoles.stream() + .anyMatch(role -> role.getName().equals(roleName)); + } + + return false; + } + + @Override + public boolean roleExists(@NotBlank String roleName, + @NotBlank String realmName, + @NotNull TypeRole typeRole, + String clientName) { + log.debug("Vérification de l'existence du rôle {} (type: {}) dans le realm {}", + roleName, typeRole, realmName); + + return getRoleByName(roleName, realmName, typeRole, clientName).isPresent(); + } + + @Override + public long countUsersWithRole(@NotBlank String roleId, + @NotBlank String realmName, + @NotNull TypeRole typeRole, + String clientName) { + log.debug("Comptage des utilisateurs ayant le rôle {} (type: {}) dans le realm {}", + roleId, typeRole, realmName); + + // Trouver le nom du rôle par son ID + Optional role = getRoleById(roleId, realmName, typeRole, clientName); + if (role.isEmpty()) { + return 0; + } + + String roleName = role.get().getName(); + + try { + // Keycloak ne fournit pas directement cette fonctionnalité via l'API Admin + // On doit lister tous les utilisateurs et vérifier leurs rôles + // C'est coûteux mais nécessaire + List users = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .list(); + + long count = 0; + for (UserRepresentation user : users) { + if (userHasRole(user.getId(), roleName, realmName, typeRole, clientName)) { + count++; + } + } + + return count; + } catch (Exception e) { + log.error("Erreur lors du comptage des utilisateurs avec le rôle {}", roleId, e); + return 0; + } + } + + // Méthodes privées pour compatibilité interne (utilisées par les nouvelles méthodes publiques) + private boolean userHasRealmRole(String userId, String roleName, + String realmName) { + return userHasRole(userId, roleName, realmName, TypeRole.REALM_ROLE, null); + } + + private boolean userHasClientRole(String userId, String clientId, + String roleName, String realmName) { + return userHasRole(userId, roleName, realmName, TypeRole.CLIENT_ROLE, clientId); + } +} diff --git a/src/main/java/dev/lions/user/manager/service/impl/SyncServiceImpl.java b/src/main/java/dev/lions/user/manager/service/impl/SyncServiceImpl.java index 52960e3..e5be13b 100644 --- a/src/main/java/dev/lions/user/manager/service/impl/SyncServiceImpl.java +++ b/src/main/java/dev/lions/user/manager/service/impl/SyncServiceImpl.java @@ -1,389 +1,389 @@ -package dev.lions.user.manager.service.impl; - -import dev.lions.user.manager.server.impl.entity.SyncHistoryEntity; -import dev.lions.user.manager.server.impl.entity.SyncedRoleEntity; -import dev.lions.user.manager.server.impl.entity.SyncedUserEntity; -import dev.lions.user.manager.server.impl.interceptor.Logged; -import dev.lions.user.manager.server.impl.repository.SyncHistoryRepository; -import dev.lions.user.manager.server.impl.repository.SyncedRoleRepository; -import dev.lions.user.manager.server.impl.repository.SyncedUserRepository; -import dev.lions.user.manager.service.SyncService; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.transaction.Transactional; -import jakarta.validation.constraints.NotBlank; -import lombok.extern.slf4j.Slf4j; -import dev.lions.user.manager.client.KeycloakAdminClient; -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.keycloak.admin.client.Keycloak; -import org.keycloak.representations.idm.RoleRepresentation; -import org.keycloak.representations.idm.UserRepresentation; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.time.temporal.ChronoUnit; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -@ApplicationScoped -@Slf4j -public class SyncServiceImpl implements SyncService { - - @Inject - Keycloak keycloak; - - @Inject - KeycloakAdminClient keycloakAdminClient; - - @Inject - SyncHistoryRepository syncHistoryRepository; - - // Repositories optionnels pour la persistance locale des snapshots. - // Ils sont marqués @Inject mais l'utilisation dans le code est protégée - // par des checks null pour ne pas casser les tests existants. - @Inject - SyncedUserRepository syncedUserRepository; - - @Inject - SyncedRoleRepository syncedRoleRepository; - - @ConfigProperty(name = "lions.keycloak.server-url") - String keycloakServerUrl; - - @Override - @Transactional - @Logged(action = "SYNC_USERS", resource = "REALM") - public int syncUsersFromRealm(@NotBlank String realmName) { - log.info("Synchronisation des utilisateurs depuis le realm: {}", realmName); - LocalDateTime start = LocalDateTime.now(); - int count = 0; - String status = "SUCCESS"; - String errorMessage = null; - - try { - List users = keycloak.realm(realmName).users().list(); - count = users.size(); - - // Persister un snapshot minimal des utilisateurs dans la base locale si le - // repository est disponible. - if (syncedUserRepository != null && !users.isEmpty()) { - List snapshots = users.stream() - .map(user -> { - SyncedUserEntity entity = new SyncedUserEntity(); - entity.setRealmName(realmName); - entity.setKeycloakId(user.getId()); - entity.setUsername(user.getUsername()); - entity.setEmail(user.getEmail()); - entity.setEnabled(user.isEnabled()); - entity.setEmailVerified(user.isEmailVerified()); - - if (user.getCreatedTimestamp() != null) { - LocalDateTime createdAt = LocalDateTime.ofInstant( - Instant.ofEpochMilli(user.getCreatedTimestamp()), - ZoneOffset.UTC); - entity.setCreatedAt(createdAt); - } - return entity; - }) - .toList(); - - syncedUserRepository.replaceForRealm(realmName, snapshots); - log.info("Persisted {} synced user snapshots for realm {}", snapshots.size(), realmName); - } - - log.info("✅ {} utilisateurs synchronisés depuis le realm {}", count, realmName); - } catch (Exception e) { - log.error("❌ Erreur lors de la synchronisation des utilisateurs depuis le realm {}", realmName, e); - status = "FAILURE"; - errorMessage = e.getMessage(); - throw new RuntimeException("Erreur de synchronisation utilisateurs: " + e.getMessage(), e); - } finally { - recordSyncHistory(realmName, "USER", status, count, start, errorMessage); - } - return count; - } - - @Override - @Transactional - @Logged(action = "SYNC_ROLES", resource = "REALM") - public int syncRolesFromRealm(@NotBlank String realmName) { - log.info("Synchronisation des rôles depuis le realm: {}", realmName); - LocalDateTime start = LocalDateTime.now(); - int count = 0; - String status = "SUCCESS"; - String errorMessage = null; - - try { - List roles = keycloak.realm(realmName).roles().list(); - count = roles.size(); - - // Persister un snapshot minimal des rôles dans la base locale si le repository - // est disponible. - if (syncedRoleRepository != null && !roles.isEmpty()) { - List snapshots = roles.stream() - .map(role -> { - SyncedRoleEntity entity = new SyncedRoleEntity(); - entity.setRealmName(realmName); - entity.setRoleName(role.getName()); - entity.setDescription(role.getDescription()); - return entity; - }) - .toList(); - - syncedRoleRepository.replaceForRealm(realmName, snapshots); - log.info("Persisted {} synced role snapshots for realm {}", snapshots.size(), realmName); - } - - log.info("✅ {} rôles synchronisés depuis le realm {}", count, realmName); - } catch (Exception e) { - log.error("❌ Erreur lors de la synchronisation des rôles depuis le realm {}", realmName, e); - status = "FAILURE"; - errorMessage = e.getMessage(); - throw new RuntimeException("Erreur de synchronisation rôles: " + e.getMessage(), e); - } finally { - recordSyncHistory(realmName, "ROLE", status, count, start, errorMessage); - } - return count; - } - - @Override - @Transactional - @Logged(action = "REALM_SYNC", resource = "SYSTEM") - public Map syncAllRealms() { - Map result = new HashMap<>(); - try { - // getAllRealms() utilise un HttpClient raw avec ObjectMapper(FAIL_ON_UNKNOWN_PROPERTIES=false) - // pour éviter les erreurs de désérialisation de RealmRepresentation avec Keycloak 26+ - List realmNames = keycloakAdminClient.getAllRealms(); - - for (String realmName : realmNames) { - if (realmName == null || realmName.isBlank()) { - continue; - } - - log.info("Synchronisation complète du realm {}", realmName); - int totalForRealm = 0; - try { - int users = syncUsersFromRealm(realmName); - int roles = syncRolesFromRealm(realmName); - totalForRealm = users + roles; - log.info("✅ Realm {} synchronisé (users={}, roles={})", realmName, users, roles); - } catch (Exception e) { - log.error("❌ Erreur lors de la synchronisation du realm {}", realmName, e); - // On enregistre quand même le realm dans le résultat avec 0 éléments traités - totalForRealm = 0; - } - result.put(realmName, totalForRealm); - } - } catch (Exception e) { - log.error("❌ Erreur lors de la récupération de la liste des realms pour synchronisation globale", e); - // En cas d'erreur globale, on retourne simplement une map vide (aucune - // approximation) - } - return result; - } - - @Override - public Map checkDataConsistency(@NotBlank String realmName) { - Map report = new HashMap<>(); - report.put("realmName", realmName); - - try { - // Données actuelles dans Keycloak - List kcUsers = keycloak.realm(realmName).users().list(); - List kcRoles = keycloak.realm(realmName).roles().list(); - - // Snapshots locaux - List localUsers = syncedUserRepository.list("realmName", realmName); - List localRoles = syncedRoleRepository.list("realmName", realmName); - - // Comparaison exacte des identifiants utilisateurs - Set kcUserIds = kcUsers.stream() - .map(UserRepresentation::getId) - .filter(id -> id != null && !id.isBlank()) - .collect(java.util.stream.Collectors.toSet()); - - Set localUserIds = localUsers.stream() - .map(SyncedUserEntity::getKeycloakId) - .filter(id -> id != null && !id.isBlank()) - .collect(java.util.stream.Collectors.toSet()); - - Set missingUsersInLocal = new HashSet<>(kcUserIds); - missingUsersInLocal.removeAll(localUserIds); - - Set missingUsersInKeycloak = new HashSet<>(localUserIds); - missingUsersInKeycloak.removeAll(kcUserIds); - - // Comparaison exacte des noms de rôles - Set kcRoleNames = kcRoles.stream() - .map(RoleRepresentation::getName) - .filter(name -> name != null && !name.isBlank()) - .collect(java.util.stream.Collectors.toSet()); - - Set localRoleNames = localRoles.stream() - .map(SyncedRoleEntity::getRoleName) - .filter(name -> name != null && !name.isBlank()) - .collect(java.util.stream.Collectors.toSet()); - - Set missingRolesInLocal = new HashSet<>(kcRoleNames); - missingRolesInLocal.removeAll(localRoleNames); - - Set missingRolesInKeycloak = new HashSet<>(localRoleNames); - missingRolesInKeycloak.removeAll(kcRoleNames); - - boolean usersOk = missingUsersInLocal.isEmpty() && missingUsersInKeycloak.isEmpty(); - boolean rolesOk = missingRolesInLocal.isEmpty() && missingRolesInKeycloak.isEmpty(); - - report.put("status", (usersOk && rolesOk) ? "OK" : "MISMATCH"); - - report.put("usersKeycloakCount", kcUserIds.size()); - report.put("usersLocalCount", localUserIds.size()); - report.put("missingUsersInLocal", missingUsersInLocal); - report.put("missingUsersInKeycloak", missingUsersInKeycloak); - - report.put("rolesKeycloakCount", kcRoleNames.size()); - report.put("rolesLocalCount", localRoleNames.size()); - report.put("missingRolesInLocal", missingRolesInLocal); - report.put("missingRolesInKeycloak", missingRolesInKeycloak); - } catch (Exception e) { - log.error("❌ Erreur lors du contrôle de cohérence des données pour le realm {}", realmName, e); - report.put("status", "ERROR"); - report.put("error", e.getMessage()); - } - - return report; - } - - @Override - @Transactional - public Map forceSyncRealm(@NotBlank String realmName) { - Map result = new HashMap<>(); - try { - int users = syncUsersFromRealm(realmName); - int roles = syncRolesFromRealm(realmName); - result.put("usersSynced", users); - result.put("rolesSynced", roles); - result.put("status", "SUCCESS"); - } catch (Exception e) { - result.put("status", "FAILURE"); - result.put("error", e.getMessage()); - } - return result; - } - - @Override - public Map getLastSyncStatus(@NotBlank String realmName) { - List history = syncHistoryRepository.findLatestByRealm(realmName, 1); - if (history.isEmpty()) { - return Collections.singletonMap("status", "NEVER_SYNCED"); - } - SyncHistoryEntity lastSync = history.get(0); - - Map statusMap = new HashMap<>(); // Utilisation de HashMap pour permettre nulls si besoin - statusMap.put("lastSyncDate", lastSync.getSyncDate()); - statusMap.put("status", lastSync.getStatus()); - statusMap.put("type", lastSync.getSyncType()); - statusMap.put("itemsProcessed", lastSync.getItemsProcessed()); - return statusMap; - } - - @Override - public boolean isKeycloakAvailable() { - try { - // getAllRealms() utilise un HttpClient raw : pas de désérialisation de RealmRepresentation - // donc pas d'erreur UnrecognizedPropertyException avec Keycloak 26+ - keycloakAdminClient.getAllRealms(); - return true; - } catch (Exception e) { - log.warn("Keycloak availability check failed: {}", e.getMessage()); - return false; - } - } - - @Override - public Map getKeycloakHealthInfo() { - Map health = new HashMap<>(); - try { - var info = keycloak.serverInfo().getInfo(); - health.put("status", "UP"); - health.put("version", info.getSystemInfo().getVersion()); - health.put("serverTime", info.getSystemInfo().getServerTime()); - } catch (Exception e) { - log.debug("serverInfo().getInfo() failed, trying raw HTTP fallback: {}", e.getMessage()); - fetchVersionViaHttp(health); - } - return health; - } - - private void fetchVersionViaHttp(Map health) { - try { - String token = keycloak.tokenManager().getAccessTokenString(); - var client = java.net.http.HttpClient.newHttpClient(); - var request = java.net.http.HttpRequest.newBuilder() - .uri(java.net.URI.create(keycloakServerUrl + "/admin/serverinfo")) - .header("Authorization", "Bearer " + token) - .header("Accept", "application/json") - .GET().build(); - var response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() == 200) { - String body = response.body(); - health.put("status", "UP"); - int sysInfoIdx = body.indexOf("\"systemInfo\""); - if (sysInfoIdx >= 0) { - extractJsonStringField(body, "version", sysInfoIdx) - .ifPresent(v -> health.put("version", v)); - extractJsonStringField(body, "serverTime", sysInfoIdx) - .ifPresent(v -> health.put("serverTime", v)); - } - if (!health.containsKey("version")) { - health.put("version", "UP (version non parsée)"); - } - } else { - health.put("status", "UP"); - health.put("version", "UP (serverinfo HTTP " + response.statusCode() + ")"); - } - } catch (Exception ex) { - log.error("Fallback HTTP serverinfo also failed: {}", ex.getMessage()); - health.put("status", "DOWN"); - health.put("error", ex.getMessage()); - } - } - - private java.util.Optional extractJsonStringField(String json, String field, int searchFrom) { - String pattern = "\"" + field + "\""; - int idx = json.indexOf(pattern, searchFrom); - if (idx < 0) return java.util.Optional.empty(); - int colonIdx = json.indexOf(':', idx + pattern.length()); - if (colonIdx < 0) return java.util.Optional.empty(); - int startQuote = json.indexOf('"', colonIdx + 1); - if (startQuote < 0) return java.util.Optional.empty(); - int endQuote = json.indexOf('"', startQuote + 1); - if (endQuote < 0) return java.util.Optional.empty(); - return java.util.Optional.of(json.substring(startQuote + 1, endQuote)); - } - - // Helper method to record history - private void recordSyncHistory(String realmName, String type, String status, int count, LocalDateTime start, - String errorMessage) { - try { - SyncHistoryEntity history = new SyncHistoryEntity(); - history.setRealmName(realmName); - history.setSyncType(type); - history.setStatus(status); - history.setItemsProcessed(count); - history.setSyncDate(LocalDateTime.now()); - history.setDurationMs(ChronoUnit.MILLIS.between(start, LocalDateTime.now())); - history.setErrorMessage(errorMessage); - - // Persist the history entity - syncHistoryRepository.persist(history); - } catch (Exception e) { - log.error("Failed to record sync history", e); - } - } -} +package dev.lions.user.manager.service.impl; + +import dev.lions.user.manager.server.impl.entity.SyncHistoryEntity; +import dev.lions.user.manager.server.impl.entity.SyncedRoleEntity; +import dev.lions.user.manager.server.impl.entity.SyncedUserEntity; +import dev.lions.user.manager.server.impl.interceptor.Logged; +import dev.lions.user.manager.server.impl.repository.SyncHistoryRepository; +import dev.lions.user.manager.server.impl.repository.SyncedRoleRepository; +import dev.lions.user.manager.server.impl.repository.SyncedUserRepository; +import dev.lions.user.manager.service.SyncService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.validation.constraints.NotBlank; +import lombok.extern.slf4j.Slf4j; +import dev.lions.user.manager.client.KeycloakAdminClient; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@ApplicationScoped +@Slf4j +public class SyncServiceImpl implements SyncService { + + @Inject + Keycloak keycloak; + + @Inject + KeycloakAdminClient keycloakAdminClient; + + @Inject + SyncHistoryRepository syncHistoryRepository; + + // Repositories optionnels pour la persistance locale des snapshots. + // Ils sont marqués @Inject mais l'utilisation dans le code est protégée + // par des checks null pour ne pas casser les tests existants. + @Inject + SyncedUserRepository syncedUserRepository; + + @Inject + SyncedRoleRepository syncedRoleRepository; + + @ConfigProperty(name = "lions.keycloak.server-url") + String keycloakServerUrl; + + @Override + @Transactional + @Logged(action = "SYNC_USERS", resource = "REALM") + public int syncUsersFromRealm(@NotBlank String realmName) { + log.info("Synchronisation des utilisateurs depuis le realm: {}", realmName); + LocalDateTime start = LocalDateTime.now(); + int count = 0; + String status = "SUCCESS"; + String errorMessage = null; + + try { + List users = keycloak.realm(realmName).users().list(); + count = users.size(); + + // Persister un snapshot minimal des utilisateurs dans la base locale si le + // repository est disponible. + if (syncedUserRepository != null && !users.isEmpty()) { + List snapshots = users.stream() + .map(user -> { + SyncedUserEntity entity = new SyncedUserEntity(); + entity.setRealmName(realmName); + entity.setKeycloakId(user.getId()); + entity.setUsername(user.getUsername()); + entity.setEmail(user.getEmail()); + entity.setEnabled(user.isEnabled()); + entity.setEmailVerified(user.isEmailVerified()); + + if (user.getCreatedTimestamp() != null) { + LocalDateTime createdAt = LocalDateTime.ofInstant( + Instant.ofEpochMilli(user.getCreatedTimestamp()), + ZoneOffset.UTC); + entity.setCreatedAt(createdAt); + } + return entity; + }) + .toList(); + + syncedUserRepository.replaceForRealm(realmName, snapshots); + log.info("Persisted {} synced user snapshots for realm {}", snapshots.size(), realmName); + } + + log.info("✅ {} utilisateurs synchronisés depuis le realm {}", count, realmName); + } catch (Exception e) { + log.error("❌ Erreur lors de la synchronisation des utilisateurs depuis le realm {}", realmName, e); + status = "FAILURE"; + errorMessage = e.getMessage(); + throw new RuntimeException("Erreur de synchronisation utilisateurs: " + e.getMessage(), e); + } finally { + recordSyncHistory(realmName, "USER", status, count, start, errorMessage); + } + return count; + } + + @Override + @Transactional + @Logged(action = "SYNC_ROLES", resource = "REALM") + public int syncRolesFromRealm(@NotBlank String realmName) { + log.info("Synchronisation des rôles depuis le realm: {}", realmName); + LocalDateTime start = LocalDateTime.now(); + int count = 0; + String status = "SUCCESS"; + String errorMessage = null; + + try { + List roles = keycloak.realm(realmName).roles().list(); + count = roles.size(); + + // Persister un snapshot minimal des rôles dans la base locale si le repository + // est disponible. + if (syncedRoleRepository != null && !roles.isEmpty()) { + List snapshots = roles.stream() + .map(role -> { + SyncedRoleEntity entity = new SyncedRoleEntity(); + entity.setRealmName(realmName); + entity.setRoleName(role.getName()); + entity.setDescription(role.getDescription()); + return entity; + }) + .toList(); + + syncedRoleRepository.replaceForRealm(realmName, snapshots); + log.info("Persisted {} synced role snapshots for realm {}", snapshots.size(), realmName); + } + + log.info("✅ {} rôles synchronisés depuis le realm {}", count, realmName); + } catch (Exception e) { + log.error("❌ Erreur lors de la synchronisation des rôles depuis le realm {}", realmName, e); + status = "FAILURE"; + errorMessage = e.getMessage(); + throw new RuntimeException("Erreur de synchronisation rôles: " + e.getMessage(), e); + } finally { + recordSyncHistory(realmName, "ROLE", status, count, start, errorMessage); + } + return count; + } + + @Override + @Transactional + @Logged(action = "REALM_SYNC", resource = "SYSTEM") + public Map syncAllRealms() { + Map result = new HashMap<>(); + try { + // getAllRealms() utilise un HttpClient raw avec ObjectMapper(FAIL_ON_UNKNOWN_PROPERTIES=false) + // pour éviter les erreurs de désérialisation de RealmRepresentation avec Keycloak 26+ + List realmNames = keycloakAdminClient.getAllRealms(); + + for (String realmName : realmNames) { + if (realmName == null || realmName.isBlank()) { + continue; + } + + log.info("Synchronisation complète du realm {}", realmName); + int totalForRealm = 0; + try { + int users = syncUsersFromRealm(realmName); + int roles = syncRolesFromRealm(realmName); + totalForRealm = users + roles; + log.info("✅ Realm {} synchronisé (users={}, roles={})", realmName, users, roles); + } catch (Exception e) { + log.error("❌ Erreur lors de la synchronisation du realm {}", realmName, e); + // On enregistre quand même le realm dans le résultat avec 0 éléments traités + totalForRealm = 0; + } + result.put(realmName, totalForRealm); + } + } catch (Exception e) { + log.error("❌ Erreur lors de la récupération de la liste des realms pour synchronisation globale", e); + // En cas d'erreur globale, on retourne simplement une map vide (aucune + // approximation) + } + return result; + } + + @Override + public Map checkDataConsistency(@NotBlank String realmName) { + Map report = new HashMap<>(); + report.put("realmName", realmName); + + try { + // Données actuelles dans Keycloak + List kcUsers = keycloak.realm(realmName).users().list(); + List kcRoles = keycloak.realm(realmName).roles().list(); + + // Snapshots locaux + List localUsers = syncedUserRepository.list("realmName", realmName); + List localRoles = syncedRoleRepository.list("realmName", realmName); + + // Comparaison exacte des identifiants utilisateurs + Set kcUserIds = kcUsers.stream() + .map(UserRepresentation::getId) + .filter(id -> id != null && !id.isBlank()) + .collect(java.util.stream.Collectors.toSet()); + + Set localUserIds = localUsers.stream() + .map(SyncedUserEntity::getKeycloakId) + .filter(id -> id != null && !id.isBlank()) + .collect(java.util.stream.Collectors.toSet()); + + Set missingUsersInLocal = new HashSet<>(kcUserIds); + missingUsersInLocal.removeAll(localUserIds); + + Set missingUsersInKeycloak = new HashSet<>(localUserIds); + missingUsersInKeycloak.removeAll(kcUserIds); + + // Comparaison exacte des noms de rôles + Set kcRoleNames = kcRoles.stream() + .map(RoleRepresentation::getName) + .filter(name -> name != null && !name.isBlank()) + .collect(java.util.stream.Collectors.toSet()); + + Set localRoleNames = localRoles.stream() + .map(SyncedRoleEntity::getRoleName) + .filter(name -> name != null && !name.isBlank()) + .collect(java.util.stream.Collectors.toSet()); + + Set missingRolesInLocal = new HashSet<>(kcRoleNames); + missingRolesInLocal.removeAll(localRoleNames); + + Set missingRolesInKeycloak = new HashSet<>(localRoleNames); + missingRolesInKeycloak.removeAll(kcRoleNames); + + boolean usersOk = missingUsersInLocal.isEmpty() && missingUsersInKeycloak.isEmpty(); + boolean rolesOk = missingRolesInLocal.isEmpty() && missingRolesInKeycloak.isEmpty(); + + report.put("status", (usersOk && rolesOk) ? "OK" : "MISMATCH"); + + report.put("usersKeycloakCount", kcUserIds.size()); + report.put("usersLocalCount", localUserIds.size()); + report.put("missingUsersInLocal", missingUsersInLocal); + report.put("missingUsersInKeycloak", missingUsersInKeycloak); + + report.put("rolesKeycloakCount", kcRoleNames.size()); + report.put("rolesLocalCount", localRoleNames.size()); + report.put("missingRolesInLocal", missingRolesInLocal); + report.put("missingRolesInKeycloak", missingRolesInKeycloak); + } catch (Exception e) { + log.error("❌ Erreur lors du contrôle de cohérence des données pour le realm {}", realmName, e); + report.put("status", "ERROR"); + report.put("error", e.getMessage()); + } + + return report; + } + + @Override + @Transactional + public Map forceSyncRealm(@NotBlank String realmName) { + Map result = new HashMap<>(); + try { + int users = syncUsersFromRealm(realmName); + int roles = syncRolesFromRealm(realmName); + result.put("usersSynced", users); + result.put("rolesSynced", roles); + result.put("status", "SUCCESS"); + } catch (Exception e) { + result.put("status", "FAILURE"); + result.put("error", e.getMessage()); + } + return result; + } + + @Override + public Map getLastSyncStatus(@NotBlank String realmName) { + List history = syncHistoryRepository.findLatestByRealm(realmName, 1); + if (history.isEmpty()) { + return Collections.singletonMap("status", "NEVER_SYNCED"); + } + SyncHistoryEntity lastSync = history.get(0); + + Map statusMap = new HashMap<>(); // Utilisation de HashMap pour permettre nulls si besoin + statusMap.put("lastSyncDate", lastSync.getSyncDate()); + statusMap.put("status", lastSync.getStatus()); + statusMap.put("type", lastSync.getSyncType()); + statusMap.put("itemsProcessed", lastSync.getItemsProcessed()); + return statusMap; + } + + @Override + public boolean isKeycloakAvailable() { + try { + // getAllRealms() utilise un HttpClient raw : pas de désérialisation de RealmRepresentation + // donc pas d'erreur UnrecognizedPropertyException avec Keycloak 26+ + keycloakAdminClient.getAllRealms(); + return true; + } catch (Exception e) { + log.warn("Keycloak availability check failed: {}", e.getMessage()); + return false; + } + } + + @Override + public Map getKeycloakHealthInfo() { + Map health = new HashMap<>(); + try { + var info = keycloak.serverInfo().getInfo(); + health.put("status", "UP"); + health.put("version", info.getSystemInfo().getVersion()); + health.put("serverTime", info.getSystemInfo().getServerTime()); + } catch (Exception e) { + log.debug("serverInfo().getInfo() failed, trying raw HTTP fallback: {}", e.getMessage()); + fetchVersionViaHttp(health); + } + return health; + } + + private void fetchVersionViaHttp(Map health) { + try { + String token = keycloak.tokenManager().getAccessTokenString(); + var client = java.net.http.HttpClient.newHttpClient(); + var request = java.net.http.HttpRequest.newBuilder() + .uri(java.net.URI.create(keycloakServerUrl + "/admin/serverinfo")) + .header("Authorization", "Bearer " + token) + .header("Accept", "application/json") + .GET().build(); + var response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + String body = response.body(); + health.put("status", "UP"); + int sysInfoIdx = body.indexOf("\"systemInfo\""); + if (sysInfoIdx >= 0) { + extractJsonStringField(body, "version", sysInfoIdx) + .ifPresent(v -> health.put("version", v)); + extractJsonStringField(body, "serverTime", sysInfoIdx) + .ifPresent(v -> health.put("serverTime", v)); + } + if (!health.containsKey("version")) { + health.put("version", "UP (version non parsée)"); + } + } else { + health.put("status", "UP"); + health.put("version", "UP (serverinfo HTTP " + response.statusCode() + ")"); + } + } catch (Exception ex) { + log.error("Fallback HTTP serverinfo also failed: {}", ex.getMessage()); + health.put("status", "DOWN"); + health.put("error", ex.getMessage()); + } + } + + private java.util.Optional extractJsonStringField(String json, String field, int searchFrom) { + String pattern = "\"" + field + "\""; + int idx = json.indexOf(pattern, searchFrom); + if (idx < 0) return java.util.Optional.empty(); + int colonIdx = json.indexOf(':', idx + pattern.length()); + if (colonIdx < 0) return java.util.Optional.empty(); + int startQuote = json.indexOf('"', colonIdx + 1); + if (startQuote < 0) return java.util.Optional.empty(); + int endQuote = json.indexOf('"', startQuote + 1); + if (endQuote < 0) return java.util.Optional.empty(); + return java.util.Optional.of(json.substring(startQuote + 1, endQuote)); + } + + // Helper method to record history + private void recordSyncHistory(String realmName, String type, String status, int count, LocalDateTime start, + String errorMessage) { + try { + SyncHistoryEntity history = new SyncHistoryEntity(); + history.setRealmName(realmName); + history.setSyncType(type); + history.setStatus(status); + history.setItemsProcessed(count); + history.setSyncDate(LocalDateTime.now()); + history.setDurationMs(ChronoUnit.MILLIS.between(start, LocalDateTime.now())); + history.setErrorMessage(errorMessage); + + // Persist the history entity + syncHistoryRepository.persist(history); + } catch (Exception e) { + log.error("Failed to record sync history", e); + } + } +} 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 ac19c09..50ba082 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 @@ -1,900 +1,900 @@ -package dev.lions.user.manager.service.impl; - -import dev.lions.user.manager.server.impl.interceptor.Logged; - -import dev.lions.user.manager.client.KeycloakAdminClient; -import dev.lions.user.manager.dto.importexport.ImportResultDTO; -import dev.lions.user.manager.dto.user.UserDTO; -import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO; -import dev.lions.user.manager.dto.user.UserSearchResultDTO; -import dev.lions.user.manager.enums.user.StatutUser; -import dev.lions.user.manager.mapper.UserMapper; -import dev.lions.user.manager.service.UserService; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.ws.rs.NotFoundException; -import lombok.extern.slf4j.Slf4j; -import org.keycloak.admin.client.resource.UserResource; -import org.keycloak.admin.client.resource.UsersResource; -import org.keycloak.representations.idm.CredentialRepresentation; -import org.keycloak.representations.idm.RoleRepresentation; -import org.keycloak.representations.idm.UserRepresentation; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.stream.Collectors; - -/** - * Implémentation du service de gestion des utilisateurs - * Utilise UNIQUEMENT l'API Admin Keycloak (ZERO accès direct à la DB Keycloak) - */ -@ApplicationScoped -@Slf4j -public class UserServiceImpl implements UserService { - - @Inject - KeycloakAdminClient keycloakAdminClient; - - @Override - public UserSearchResultDTO searchUsers(@Valid @NotNull UserSearchCriteriaDTO criteria) { - log.info("Recherche d'utilisateurs avec critères: {}", criteria); - - try { - String realmName = criteria.getRealmName(); - UsersResource usersResource = keycloakAdminClient.getUsers(realmName); - - // Construire la requête de recherche - List users; - - if (criteria.getSearchTerm() != null && !criteria.getSearchTerm().isBlank()) { - // Recherche globale - users = usersResource.search( - criteria.getSearchTerm(), - criteria.getOffset(), - criteria.getPageSize()); - } else if (criteria.getUsername() != null) { - // Recherche par username exact - users = usersResource.search( - criteria.getUsername(), - criteria.getOffset(), - criteria.getPageSize(), - true // exact match - ); - } else if (criteria.getEmail() != null) { - // Recherche par email - users = usersResource.searchByEmail( - criteria.getEmail(), - true // exact match - ); - } else { - // Liste tous les utilisateurs - users = usersResource.list( - criteria.getOffset(), - criteria.getPageSize()); - } - - // Filtrer selon les critères supplémentaires - users = filterUsers(users, criteria); - - // Convertir en DTOs - List userDTOs = UserMapper.toDTOList(users, realmName); - - // Enrichir avec les rôles si demandé - if (Boolean.TRUE.equals(criteria.getIncludeRoles())) { - for (UserDTO dto : userDTOs) { - try { - List realmRoles = usersResource.get(dto.getId()) - .roles().realmLevel().listAll(); - if (realmRoles != null) { - dto.setRealmRoles(realmRoles.stream() - .map(RoleRepresentation::getName) - .collect(Collectors.toList())); - } - } catch (Exception e) { - log.warn("Impossible de charger les rôles pour l'utilisateur {}: {}", dto.getId(), e.getMessage()); - } - } - } - - // Compter le total - long totalCount = usersResource.count(); - - // Construire le résultat paginé - return UserSearchResultDTO.of(userDTOs, criteria, totalCount); - - } catch (Exception e) { - log.error("Erreur lors de la recherche d'utilisateurs", e); - throw new RuntimeException("Impossible de rechercher les utilisateurs", e); - } - } - - @Override - public Optional getUserById(@NotBlank String userId, @NotBlank String realmName) { - log.info("Récupération de l'utilisateur {} dans le realm {}", userId, realmName); - - try { - UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); - UserRepresentation userRep = userResource.toRepresentation(); - UserDTO userDTO = UserMapper.toDTO(userRep, realmName); - - // Récupérer les rôles realm de l'utilisateur - try { - List realmRoles = userResource.roles().realmLevel().listAll(); - if (realmRoles != null && !realmRoles.isEmpty()) { - List roleNames = realmRoles.stream() - .map(RoleRepresentation::getName) - .collect(Collectors.toList()); - userDTO.setRealmRoles(roleNames); - } - } catch (Exception e) { - log.warn("Erreur lors de la récupération des rôles realm pour l'utilisateur {}: {}", userId, - e.getMessage()); - // Ne pas échouer si les rôles ne peuvent pas être récupérés - } - - return Optional.of(userDTO); - } catch (NotFoundException e) { - log.warn("Utilisateur {} non trouvé dans le realm {}", userId, realmName); - return Optional.empty(); - } catch (Exception e) { - // Vérifier si l'exception contient un message indiquant un 404 - String errorMessage = e.getMessage(); - if (errorMessage != null && (errorMessage.contains("404") || - errorMessage.contains("Server response is: 404") || - errorMessage.contains("Received: 'Server response is: 404'"))) { - log.warn("Utilisateur {} non trouvé dans le realm {} (404 détecté dans l'exception)", userId, - realmName); - return Optional.empty(); - } - log.error("Erreur lors de la récupération de l'utilisateur {}", userId, e); - throw new RuntimeException("Impossible de récupérer l'utilisateur", e); - } - } - - @Override - public Optional getUserByUsername(@NotBlank String username, @NotBlank String realmName) { - log.info("Récupération de l'utilisateur par username {} dans le realm {}", username, realmName); - - try { - List users = keycloakAdminClient.getUsers(realmName) - .search(username, 0, 1, true); - - if (users.isEmpty()) { - return Optional.empty(); - } - - return Optional.of(UserMapper.toDTO(users.get(0), realmName)); - } catch (Exception e) { - log.error("Erreur lors de la récupération de l'utilisateur par username {}", username, e); - throw new RuntimeException("Impossible de récupérer l'utilisateur", e); - } - } - - @Override - public Optional getUserByEmail(@NotBlank String email, @NotBlank String realmName) { - log.info("Récupération de l'utilisateur par email {} dans le realm {}", email, realmName); - - try { - List users = keycloakAdminClient.getUsers(realmName) - .searchByEmail(email, true); - - if (users.isEmpty()) { - return Optional.empty(); - } - - return Optional.of(UserMapper.toDTO(users.get(0), realmName)); - } catch (Exception e) { - log.error("Erreur lors de la récupération de l'utilisateur par email {}", email, e); - throw new RuntimeException("Impossible de récupérer l'utilisateur", e); - } - } - - @Override - @Logged(action = "USER_CREATE", resource = "USER") - public UserDTO createUser(@Valid @NotNull UserDTO user, @NotBlank String realmName) { - log.info("Création de l'utilisateur {} dans le realm {}", user.getUsername(), realmName); - - try { - // Vérifier si l'utilisateur existe déjà - if (usernameExists(user.getUsername(), realmName)) { - throw new IllegalArgumentException("Le nom d'utilisateur existe déjà: " + user.getUsername()); - } - - if (user.getEmail() != null && emailExists(user.getEmail(), realmName)) { - throw new IllegalArgumentException("L'email existe déjà: " + user.getEmail()); - } - - // Convertir DTO vers UserRepresentation - UserRepresentation userRep = UserMapper.toRepresentation(user); - - // Créer l'utilisateur - UsersResource usersResource = keycloakAdminClient.getUsers(realmName); - var response = usersResource.create(userRep); - - if (response.getStatus() == 409) { - // Utilisateur déjà existant — le récupérer par email ou username - log.warn("Utilisateur {} déjà existant dans Keycloak (409), récupération...", user.getUsername()); - if (user.getEmail() != null) { - List existing = usersResource.searchByEmail(user.getEmail(), true); - if (!existing.isEmpty()) { - log.info("Utilisateur récupéré par email: {}", user.getEmail()); - return UserMapper.toDTO(existing.get(0), realmName); - } - } - List existing = usersResource.searchByUsername(user.getUsername(), true); - if (!existing.isEmpty()) { - log.info("Utilisateur récupéré par username: {}", user.getUsername()); - return UserMapper.toDTO(existing.get(0), realmName); - } - throw new RuntimeException("Utilisateur en conflit mais introuvable: " + user.getUsername()); - } - if (response.getStatus() != 201) { - throw new RuntimeException("Échec de la création de l'utilisateur (HTTP " + response.getStatus() + " " + response.getStatusInfo().getReasonPhrase() + ")"); - } - - // Récupérer l'ID de l'utilisateur créé - String userId = response.getLocation().getPath().replaceAll(".*/([^/]+)$", "$1"); - - // Définir le mot de passe si fourni - if (user.getTemporaryPassword() != null) { - setPassword(userId, realmName, user.getTemporaryPassword(), - user.getTemporaryPasswordFlag() != null && user.getTemporaryPasswordFlag()); - } - - // Re-appliquer les required actions après la définition du mot de passe. - // resetPassword(temporary=false) retire UPDATE_PASSWORD des required actions dans Keycloak. - if (user.getRequiredActions() != null && !user.getRequiredActions().isEmpty()) { - UserResource userResForActions = usersResource.get(userId); - UserRepresentation repForActions = userResForActions.toRepresentation(); - repForActions.setRequiredActions(user.getRequiredActions()); - userResForActions.update(repForActions); - log.info("✅ Required actions re-appliquées pour {} : {}", userId, user.getRequiredActions()); - } - - // Récupérer l'utilisateur créé - UserResource userResource = usersResource.get(userId); - UserRepresentation createdUser = userResource.toRepresentation(); - - log.info("✅ Utilisateur créé avec succès: {} (ID: {})", user.getUsername(), userId); - return UserMapper.toDTO(createdUser, realmName); - - } catch (Exception e) { - log.error("❌ Erreur lors de la création de l'utilisateur {}", user.getUsername(), e); - throw new RuntimeException("Impossible de créer l'utilisateur", e); - } - } - - @Override - @Logged(action = "USER_UPDATE", resource = "USER") - public UserDTO updateUser(@NotBlank String userId, @Valid @NotNull UserDTO user, @NotBlank String realmName) { - log.info("Mise à jour de l'utilisateur {} dans le realm {}", userId, realmName); - - try { - UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); - - // Récupérer l'utilisateur existant - UserRepresentation existingUser = userResource.toRepresentation(); - - // Mettre à jour les champs - if (user.getEmail() != null) { - existingUser.setEmail(user.getEmail()); - } - if (user.getPrenom() != null) { - existingUser.setFirstName(user.getPrenom()); - } - if (user.getNom() != null) { - existingUser.setLastName(user.getNom()); - } - if (user.getEnabled() != null) { - existingUser.setEnabled(user.getEnabled()); - } - if (user.getEmailVerified() != null) { - existingUser.setEmailVerified(user.getEmailVerified()); - } - if (user.getAttributes() != null) { - existingUser.setAttributes(user.getAttributes()); - } - if (user.getRequiredActions() != null) { - existingUser.setRequiredActions(user.getRequiredActions()); - } - - // Envoyer la mise à jour - userResource.update(existingUser); - - // Récupérer l'utilisateur mis à jour - UserRepresentation updatedUser = userResource.toRepresentation(); - - log.info("✅ Utilisateur mis à jour avec succès: {}", userId); - return UserMapper.toDTO(updatedUser, realmName); - - } catch (NotFoundException e) { - log.error("❌ Utilisateur {} non trouvé", userId); - throw new RuntimeException("Utilisateur non trouvé", e); - } catch (Exception e) { - log.error("❌ Erreur lors de la mise à jour de l'utilisateur {}", userId, e); - throw new RuntimeException("Impossible de mettre à jour l'utilisateur", e); - } - } - - @Override - @Logged(action = "USER_DELETE", resource = "USER") - public void deleteUser(@NotBlank String userId, @NotBlank String realmName, boolean hardDelete) { - log.info("Suppression de l'utilisateur {} dans le realm {} (hard: {})", userId, realmName, hardDelete); - - try { - UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); - - if (hardDelete) { - // Suppression définitive - userResource.remove(); - log.info("✅ Utilisateur supprimé définitivement: {}", userId); - } else { - // Soft delete: désactiver l'utilisateur - UserRepresentation user = userResource.toRepresentation(); - user.setEnabled(false); - userResource.update(user); - log.info("✅ Utilisateur désactivé (soft delete): {}", userId); - } - - } catch (NotFoundException e) { - log.error("❌ Utilisateur {} non trouvé", userId); - throw new RuntimeException("Utilisateur non trouvé", e); - } catch (Exception e) { - log.error("❌ Erreur lors de la suppression de l'utilisateur {}", userId, e); - throw new RuntimeException("Impossible de supprimer l'utilisateur", e); - } - } - - @Override - @Logged(action = "USER_ACTIVATE", resource = "USER") - public void activateUser(@NotBlank String userId, @NotBlank String realmName) { - log.info("Activation de l'utilisateur {} dans le realm {}", userId, realmName); - - try { - UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); - UserRepresentation user = userResource.toRepresentation(); - user.setEnabled(true); - userResource.update(user); - - log.info("✅ Utilisateur activé: {}", userId); - } catch (Exception e) { - log.error("❌ Erreur lors de l'activation de l'utilisateur {}", userId, e); - throw new RuntimeException("Impossible d'activer l'utilisateur", e); - } - } - - @Override - @Logged(action = "USER_DEACTIVATE", resource = "USER") - public void deactivateUser(@NotBlank String userId, @NotBlank String realmName, String raison) { - log.info("Désactivation de l'utilisateur {} dans le realm {} (raison: {})", userId, realmName, raison); - - try { - UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); - UserRepresentation user = userResource.toRepresentation(); - user.setEnabled(false); - userResource.update(user); - - log.info("✅ Utilisateur désactivé: {}", userId); - } catch (Exception e) { - log.error("❌ Erreur lors de la désactivation de l'utilisateur {}", userId, e); - throw new RuntimeException("Impossible de désactiver l'utilisateur", e); - } - } - - @Override - public void suspendUser(@NotBlank String userId, @NotBlank String realmName, String raison, int duree) { - log.info("Suspension de l'utilisateur {} dans le realm {} (raison: {}, durée: {} jours)", - userId, realmName, raison, duree); - - deactivateUser(userId, realmName, raison); - } - - @Override - @Logged(action = "USER_UNLOCK", resource = "USER") - public void unlockUser(@NotBlank String userId, @NotBlank String realmName) { - log.info("Déverrouillage de l'utilisateur {} dans le realm {}", userId, realmName); - activateUser(userId, realmName); - } - - @Override - @Logged(action = "USER_PASSWORD_RESET", resource = "USER") - public void resetPassword(@NotBlank String userId, @NotBlank String realmName, - @NotBlank String temporaryPassword, boolean temporary) { - log.info("Réinitialisation du mot de passe pour l'utilisateur {} (temporaire: {})", userId, temporary); - - setPassword(userId, realmName, temporaryPassword, temporary); - } - - @Override - public void sendVerificationEmail(@NotBlank String userId, @NotBlank String realmName) { - log.info("Envoi d'un email de vérification à l'utilisateur {}", userId); - - try { - UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); - userResource.sendVerifyEmail(); - - log.info("✅ Email de vérification envoyé: {}", userId); - } catch (Exception e) { - log.warn("⚠️ Impossible d'envoyer l'email de vérification pour {} (SMTP peut-être non configuré ou Keycloak indisponible): {}", userId, e.getMessage()); - // L'envoi d'email est best-effort : on ne bloque pas le flux appelant - } - } - - @Override - @Logged(action = "USER_FORCE_LOGOUT", resource = "USER") - public int logoutAllSessions(@NotBlank String userId, @NotBlank String realmName) { - log.info("Déconnexion de toutes les sessions pour l'utilisateur {}", userId); - - try { - UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); - int sessionsCount = userResource.getUserSessions().size(); - userResource.logout(); - - log.info("✅ {} sessions révoquées pour l'utilisateur {}", sessionsCount, userId); - return sessionsCount; - } catch (Exception e) { - log.error("❌ Erreur lors de la déconnexion des sessions pour {}", userId, e); - throw new RuntimeException("Impossible de déconnecter les sessions", e); - } - } - - @Override - public List getActiveSessions(@NotBlank String userId, @NotBlank String realmName) { - log.info("Récupération des sessions actives pour l'utilisateur {}", userId); - - try { - UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); - return userResource.getUserSessions().stream() - .map(session -> session.getId()) - .collect(Collectors.toList()); - } catch (Exception e) { - log.error("❌ Erreur lors de la récupération des sessions pour {}", userId, e); - return Collections.emptyList(); - } - } - - @Override - public long countUsers(@NotNull UserSearchCriteriaDTO criteria) { - try { - return keycloakAdminClient.getUsers(criteria.getRealmName()).count(); - } catch (Exception e) { - log.error("Erreur lors du comptage des utilisateurs", e); - return 0; - } - } - - @Override - public UserSearchResultDTO getAllUsers(@NotBlank String realmName, int page, int pageSize) { - UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder() - .realmName(realmName) - .page(page) - .pageSize(pageSize) - .build(); - - return searchUsers(criteria); - } - - @Override - public boolean usernameExists(@NotBlank String username, @NotBlank String realmName) { - try { - List users = keycloakAdminClient.getUsers(realmName) - .searchByUsername(username, true); - return !users.isEmpty(); - } catch (Exception e) { - log.error("Erreur lors de la vérification de l'existence du username {}", username, e); - return false; - } - } - - @Override - public boolean emailExists(@NotBlank String email, @NotBlank String realmName) { - try { - List users = keycloakAdminClient.getUsers(realmName) - .searchByEmail(email, true); - return !users.isEmpty(); - } catch (Exception e) { - log.error("Erreur lors de la vérification de l'existence de l'email {}", email, e); - return false; - } - } - - @Override - public String exportUsersToCSV(@NotNull UserSearchCriteriaDTO criteria) { - log.info("Export CSV des utilisateurs avec critères: {}", criteria); - - try { - // Récupérer tous les utilisateurs correspondant aux critères - UserSearchResultDTO searchResult = searchUsers(criteria); - List users = searchResult.getUsers(); - - if (users == null || users.isEmpty()) { - log.warn("Aucun utilisateur trouvé pour l'export CSV"); - return generateCSVHeader(); - } - - // Générer le CSV - StringBuilder csv = new StringBuilder(); - csv.append(generateCSVHeader()); - csv.append("\n"); - - for (UserDTO user : users) { - csv.append(escapeCSVField(user.getUsername() != null ? user.getUsername() : "")); - csv.append(","); - csv.append(escapeCSVField(user.getEmail() != null ? user.getEmail() : "")); - csv.append(","); - csv.append(escapeCSVField(user.getPrenom() != null ? user.getPrenom() : "")); - csv.append(","); - csv.append(escapeCSVField(user.getNom() != null ? user.getNom() : "")); - csv.append(","); - csv.append(escapeCSVField(user.getTelephone() != null ? user.getTelephone() : "")); - csv.append(","); - csv.append(user.getEnabled() != null && user.getEnabled() ? "true" : "false"); - csv.append(","); - csv.append(user.getEmailVerified() != null && user.getEmailVerified() ? "true" : "false"); - csv.append(","); - csv.append(escapeCSVField(user.getStatut() != null ? user.getStatut().name() : "")); - csv.append(","); - csv.append(escapeCSVField(user.getOrganisation() != null ? user.getOrganisation() : "")); - csv.append(","); - csv.append(escapeCSVField(user.getDepartement() != null ? user.getDepartement() : "")); - csv.append(","); - csv.append(escapeCSVField(user.getFonction() != null ? user.getFonction() : "")); - csv.append(","); - csv.append(escapeCSVField(user.getPays() != null ? user.getPays() : "")); - csv.append(","); - csv.append(escapeCSVField(user.getVille() != null ? user.getVille() : "")); - csv.append(","); - csv.append(escapeCSVField(user.getDateCreation() != null - ? user.getDateCreation().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) - : "")); - csv.append("\n"); - } - - log.info("✅ Export CSV réussi: {} utilisateur(s) exporté(s)", users.size()); - return csv.toString(); - - } catch (Exception e) { - log.error("❌ Erreur lors de l'export CSV", e); - throw new RuntimeException("Impossible d'exporter les utilisateurs en CSV", e); - } - } - - private String generateCSVHeader() { - return "username,email,prenom,nom,telephone,enabled,emailVerified,statut,organisation,departement,fonction,pays,ville,dateCreation"; - } - - private String escapeCSVField(String field) { - if (field == null) { - return ""; - } - // Si le champ contient une virgule, des guillemets ou un saut de ligne, - // l'entourer de guillemets - if (field.contains(",") || field.contains("\"") || field.contains("\n")) { - // Échapper les guillemets en les doublant - return "\"" + field.replace("\"", "\"\"") + "\""; - } - return field; - } - - @Override - @Logged(action = "USER_IMPORT", resource = "USER") - public ImportResultDTO importUsersFromCSV(@NotBlank String csvContent, @NotBlank String realmName) { - log.info("Import CSV de {} lignes pour le realm {}", csvContent.split("\n").length, realmName); - - ImportResultDTO.ImportResultDTOBuilder resultBuilder = ImportResultDTO.builder(); - List errors = new ArrayList<>(); - int successCount = 0; - int lineNumber = 0; - - String[] lines = csvContent.split("\n"); - resultBuilder.totalLines(lines.length); - - // Ignorer la première ligne si c'est l'en-tête - int startLine = 0; - if (lines.length > 0 && lines[0].toLowerCase().contains("username")) { - startLine = 1; - } - - for (int i = startLine; i < lines.length; i++) { - lineNumber = i + 1; - String line = lines[i].trim(); - - if (line.isEmpty()) { - continue; - } - - try { - // Parser la ligne CSV - String[] fields = parseCSVLine(line); - - if (fields.length < 4) { - errors.add(ImportResultDTO.ImportErrorDTO.builder() - .lineNumber(lineNumber) - .lineContent(line) - .errorType(ImportResultDTO.ErrorType.INVALID_FORMAT) - .message("Nombre de colonnes insuffisant (minimum 4: username, email, prenom, nom)") - .build()); - continue; - } - - // Créer l'utilisateur - String username = fields[0].trim(); - String email = fields[1].trim(); - String prenom = fields.length > 2 ? fields[2].trim() : ""; - String nom = fields.length > 3 ? fields[3].trim() : ""; - String telephone = fields.length > 4 ? fields[4].trim() : null; - Boolean enabled = fields.length > 5 ? Boolean.parseBoolean(fields[5].trim()) : true; - Boolean emailVerified = fields.length > 6 ? Boolean.parseBoolean(fields[6].trim()) : false; - - // Valider les champs obligatoires - if (username.isBlank() || email.isBlank()) { - errors.add(ImportResultDTO.ImportErrorDTO.builder() - .lineNumber(lineNumber) - .lineContent(line) - .errorType(ImportResultDTO.ErrorType.VALIDATION_ERROR) - .field(username.isBlank() ? "username" : "email") - .message("Le username et l'email sont obligatoires") - .build()); - continue; - } - - // Vérifier si l'utilisateur existe déjà - if (emailExists(email, realmName)) { - errors.add(ImportResultDTO.ImportErrorDTO.builder() - .lineNumber(lineNumber) - .lineContent(line) - .errorType(ImportResultDTO.ErrorType.DUPLICATE_USER) - .field("email") - .message("Un utilisateur avec cet email existe déjà") - .build()); - continue; - } - - // Créer l'utilisateur - UserDTO newUser = UserDTO.builder() - .username(username) - .email(email) - .prenom(prenom) - .nom(nom) - .telephone(telephone) - .enabled(enabled) - .emailVerified(emailVerified) - .build(); - - createUser(newUser, realmName); - successCount++; - - } catch (Exception e) { - log.error("Erreur lors de l'import de la ligne {}: {}", lineNumber, e.getMessage()); - errors.add(ImportResultDTO.ImportErrorDTO.builder() - .lineNumber(lineNumber) - .lineContent(line) - .errorType(ImportResultDTO.ErrorType.CREATION_ERROR) - .message("Erreur lors de la création: " + e.getMessage()) - .details(e.getClass().getSimpleName()) - .build()); - } - } - - resultBuilder.successCount(successCount); - resultBuilder.errors(errors); - resultBuilder.errorCount(errors.size()); - - ImportResultDTO result = resultBuilder.build(); - result.generateMessage(); - - log.info("✅ Import CSV terminé: {} succès, {} erreurs", successCount, errors.size()); - return result; - } - - private String[] parseCSVLine(String line) { - List fields = new ArrayList<>(); - boolean inQuotes = false; - StringBuilder currentField = new StringBuilder(); - - for (int i = 0; i < line.length(); i++) { - char c = line.charAt(i); - - if (c == '"') { - if (inQuotes && i + 1 < line.length() && line.charAt(i + 1) == '"') { - // Guillemet échappé - currentField.append('"'); - i++; // Passer le guillemet suivant - } else { - // Toggle inQuotes - inQuotes = !inQuotes; - } - } else if (c == ',' && !inQuotes) { - // Fin du champ - fields.add(currentField.toString()); - currentField = new StringBuilder(); - } else { - currentField.append(c); - } - } - - // Ajouter le dernier champ - fields.add(currentField.toString()); - - return fields.toArray(new String[0]); - } - - // ==================== Méthodes privées ==================== - - private void setPassword(String userId, String realmName, String password, boolean temporary) { - try { - UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); - - CredentialRepresentation credential = new CredentialRepresentation(); - credential.setType(CredentialRepresentation.PASSWORD); - credential.setValue(password); - credential.setTemporary(temporary); - - userResource.resetPassword(credential); - - log.info("✅ Mot de passe défini pour l'utilisateur {} (temporaire: {})", userId, temporary); - } catch (Exception e) { - log.error("❌ Erreur lors de la définition du mot de passe pour {}", userId, e); - throw new RuntimeException("Impossible de définir le mot de passe", e); - } - } - - private List filterUsers(List users, UserSearchCriteriaDTO criteria) { - return users.stream() - .filter(user -> { - // Filtrer par enabled - if (criteria.getEnabled() != null && !criteria.getEnabled().equals(user.isEnabled())) { - return false; - } - - // Filtrer par emailVerified - if (criteria.getEmailVerified() != null - && !criteria.getEmailVerified().equals(user.isEmailVerified())) { - return false; - } - - // Filtrer par username - if (criteria.getUsername() != null - && !criteria.getUsername().equalsIgnoreCase(user.getUsername())) { - return false; - } - - // Filtrer par email - if (criteria.getEmail() != null && !criteria.getEmail().equalsIgnoreCase(user.getEmail())) { - return false; - } - - // Filtrer par prénom - if (criteria.getPrenom() != null) { - String prenom = user.getFirstName(); - if (prenom == null || !prenom.toLowerCase().contains(criteria.getPrenom().toLowerCase())) { - return false; - } - } - - // Filtrer par nom - if (criteria.getNom() != null) { - String nom = user.getLastName(); - if (nom == null || !nom.toLowerCase().contains(criteria.getNom().toLowerCase())) { - return false; - } - } - - // Filtrer par téléphone - if (criteria.getTelephone() != null) { - String phone = user.getAttributes() != null - ? user.getAttributes().getOrDefault("phone_number", Collections.emptyList()).stream() - .findFirst().orElse(null) - : null; - if (phone == null || !phone.contains(criteria.getTelephone())) { - return false; - } - } - - // Filtrer par statut (basé sur enabled et emailVerified) - if (criteria.getStatut() != null) { - StatutUser userStatut = determineUserStatut(user); - if (!criteria.getStatut().equals(userStatut)) { - return false; - } - } - - // Filtrer par organisation - if (criteria.getOrganisation() != null) { - String org = user.getAttributes() != null - ? user.getAttributes().getOrDefault("organization", Collections.emptyList()).stream() - .findFirst().orElse(null) - : null; - if (org == null || !org.toLowerCase().contains(criteria.getOrganisation().toLowerCase())) { - return false; - } - } - - // Filtrer par département - if (criteria.getDepartement() != null) { - String dept = user.getAttributes() != null - ? user.getAttributes().getOrDefault("department", Collections.emptyList()).stream() - .findFirst().orElse(null) - : null; - if (dept == null || !dept.toLowerCase().contains(criteria.getDepartement().toLowerCase())) { - return false; - } - } - - // Filtrer par fonction - if (criteria.getFonction() != null) { - String fonction = user.getAttributes() != null - ? user.getAttributes().getOrDefault("job_title", Collections.emptyList()).stream() - .findFirst().orElse(null) - : null; - if (fonction == null - || !fonction.toLowerCase().contains(criteria.getFonction().toLowerCase())) { - return false; - } - } - - // Filtrer par pays - if (criteria.getPays() != null) { - String pays = user.getAttributes() != null - ? user.getAttributes().getOrDefault("country", Collections.emptyList()).stream() - .findFirst().orElse(null) - : null; - if (pays == null || !pays.toLowerCase().contains(criteria.getPays().toLowerCase())) { - return false; - } - } - - // Filtrer par ville - if (criteria.getVille() != null) { - String ville = user.getAttributes() != null - ? user.getAttributes().getOrDefault("city", Collections.emptyList()).stream() - .findFirst().orElse(null) - : null; - if (ville == null || !ville.toLowerCase().contains(criteria.getVille().toLowerCase())) { - return false; - } - } - - // Filtrer par date de création - if (criteria.getDateCreationMin() != null || criteria.getDateCreationMax() != null) { - Long createdTimestamp = user.getCreatedTimestamp(); - if (createdTimestamp != null) { - LocalDateTime createdDate = LocalDateTime.ofEpochSecond( - createdTimestamp / 1000, 0, - java.time.ZoneOffset.UTC); - - if (criteria.getDateCreationMin() != null && - createdDate.isBefore(criteria.getDateCreationMin())) { - return false; - } - - if (criteria.getDateCreationMax() != null && - createdDate.isAfter(criteria.getDateCreationMax())) { - return false; - } - } else { - // Si pas de date de création et qu'un filtre min est défini, exclure - if (criteria.getDateCreationMin() != null) { - return false; - } - } - } - - return true; - }) - .collect(Collectors.toList()); - } - - private StatutUser determineUserStatut(UserRepresentation user) { - if (!user.isEnabled()) { - return StatutUser.INACTIF; - } - // Si enabled mais email non vérifié, on considère toujours comme ACTIF - // car l'email non vérifié n'empêche pas l'activation du compte - return StatutUser.ACTIF; - } -} +package dev.lions.user.manager.service.impl; + +import dev.lions.user.manager.server.impl.interceptor.Logged; + +import dev.lions.user.manager.client.KeycloakAdminClient; +import dev.lions.user.manager.dto.importexport.ImportResultDTO; +import dev.lions.user.manager.dto.user.UserDTO; +import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO; +import dev.lions.user.manager.dto.user.UserSearchResultDTO; +import dev.lions.user.manager.enums.user.StatutUser; +import dev.lions.user.manager.mapper.UserMapper; +import dev.lions.user.manager.service.UserService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.NotFoundException; +import lombok.extern.slf4j.Slf4j; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Implémentation du service de gestion des utilisateurs + * Utilise UNIQUEMENT l'API Admin Keycloak (ZERO accès direct à la DB Keycloak) + */ +@ApplicationScoped +@Slf4j +public class UserServiceImpl implements UserService { + + @Inject + KeycloakAdminClient keycloakAdminClient; + + @Override + public UserSearchResultDTO searchUsers(@Valid @NotNull UserSearchCriteriaDTO criteria) { + log.info("Recherche d'utilisateurs avec critères: {}", criteria); + + try { + String realmName = criteria.getRealmName(); + UsersResource usersResource = keycloakAdminClient.getUsers(realmName); + + // Construire la requête de recherche + List users; + + if (criteria.getSearchTerm() != null && !criteria.getSearchTerm().isBlank()) { + // Recherche globale + users = usersResource.search( + criteria.getSearchTerm(), + criteria.getOffset(), + criteria.getPageSize()); + } else if (criteria.getUsername() != null) { + // Recherche par username exact + users = usersResource.search( + criteria.getUsername(), + criteria.getOffset(), + criteria.getPageSize(), + true // exact match + ); + } else if (criteria.getEmail() != null) { + // Recherche par email + users = usersResource.searchByEmail( + criteria.getEmail(), + true // exact match + ); + } else { + // Liste tous les utilisateurs + users = usersResource.list( + criteria.getOffset(), + criteria.getPageSize()); + } + + // Filtrer selon les critères supplémentaires + users = filterUsers(users, criteria); + + // Convertir en DTOs + List userDTOs = UserMapper.toDTOList(users, realmName); + + // Enrichir avec les rôles si demandé + if (Boolean.TRUE.equals(criteria.getIncludeRoles())) { + for (UserDTO dto : userDTOs) { + try { + List realmRoles = usersResource.get(dto.getId()) + .roles().realmLevel().listAll(); + if (realmRoles != null) { + dto.setRealmRoles(realmRoles.stream() + .map(RoleRepresentation::getName) + .collect(Collectors.toList())); + } + } catch (Exception e) { + log.warn("Impossible de charger les rôles pour l'utilisateur {}: {}", dto.getId(), e.getMessage()); + } + } + } + + // Compter le total + long totalCount = usersResource.count(); + + // Construire le résultat paginé + return UserSearchResultDTO.of(userDTOs, criteria, totalCount); + + } catch (Exception e) { + log.error("Erreur lors de la recherche d'utilisateurs", e); + throw new RuntimeException("Impossible de rechercher les utilisateurs", e); + } + } + + @Override + public Optional getUserById(@NotBlank String userId, @NotBlank String realmName) { + log.info("Récupération de l'utilisateur {} dans le realm {}", userId, realmName); + + try { + UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); + UserRepresentation userRep = userResource.toRepresentation(); + UserDTO userDTO = UserMapper.toDTO(userRep, realmName); + + // Récupérer les rôles realm de l'utilisateur + try { + List realmRoles = userResource.roles().realmLevel().listAll(); + if (realmRoles != null && !realmRoles.isEmpty()) { + List roleNames = realmRoles.stream() + .map(RoleRepresentation::getName) + .collect(Collectors.toList()); + userDTO.setRealmRoles(roleNames); + } + } catch (Exception e) { + log.warn("Erreur lors de la récupération des rôles realm pour l'utilisateur {}: {}", userId, + e.getMessage()); + // Ne pas échouer si les rôles ne peuvent pas être récupérés + } + + return Optional.of(userDTO); + } catch (NotFoundException e) { + log.warn("Utilisateur {} non trouvé dans le realm {}", userId, realmName); + return Optional.empty(); + } catch (Exception e) { + // Vérifier si l'exception contient un message indiquant un 404 + String errorMessage = e.getMessage(); + if (errorMessage != null && (errorMessage.contains("404") || + errorMessage.contains("Server response is: 404") || + errorMessage.contains("Received: 'Server response is: 404'"))) { + log.warn("Utilisateur {} non trouvé dans le realm {} (404 détecté dans l'exception)", userId, + realmName); + return Optional.empty(); + } + log.error("Erreur lors de la récupération de l'utilisateur {}", userId, e); + throw new RuntimeException("Impossible de récupérer l'utilisateur", e); + } + } + + @Override + public Optional getUserByUsername(@NotBlank String username, @NotBlank String realmName) { + log.info("Récupération de l'utilisateur par username {} dans le realm {}", username, realmName); + + try { + List users = keycloakAdminClient.getUsers(realmName) + .search(username, 0, 1, true); + + if (users.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(UserMapper.toDTO(users.get(0), realmName)); + } catch (Exception e) { + log.error("Erreur lors de la récupération de l'utilisateur par username {}", username, e); + throw new RuntimeException("Impossible de récupérer l'utilisateur", e); + } + } + + @Override + public Optional getUserByEmail(@NotBlank String email, @NotBlank String realmName) { + log.info("Récupération de l'utilisateur par email {} dans le realm {}", email, realmName); + + try { + List users = keycloakAdminClient.getUsers(realmName) + .searchByEmail(email, true); + + if (users.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(UserMapper.toDTO(users.get(0), realmName)); + } catch (Exception e) { + log.error("Erreur lors de la récupération de l'utilisateur par email {}", email, e); + throw new RuntimeException("Impossible de récupérer l'utilisateur", e); + } + } + + @Override + @Logged(action = "USER_CREATE", resource = "USER") + public UserDTO createUser(@Valid @NotNull UserDTO user, @NotBlank String realmName) { + log.info("Création de l'utilisateur {} dans le realm {}", user.getUsername(), realmName); + + try { + // Vérifier si l'utilisateur existe déjà + if (usernameExists(user.getUsername(), realmName)) { + throw new IllegalArgumentException("Le nom d'utilisateur existe déjà: " + user.getUsername()); + } + + if (user.getEmail() != null && emailExists(user.getEmail(), realmName)) { + throw new IllegalArgumentException("L'email existe déjà: " + user.getEmail()); + } + + // Convertir DTO vers UserRepresentation + UserRepresentation userRep = UserMapper.toRepresentation(user); + + // Créer l'utilisateur + UsersResource usersResource = keycloakAdminClient.getUsers(realmName); + var response = usersResource.create(userRep); + + if (response.getStatus() == 409) { + // Utilisateur déjà existant — le récupérer par email ou username + log.warn("Utilisateur {} déjà existant dans Keycloak (409), récupération...", user.getUsername()); + if (user.getEmail() != null) { + List existing = usersResource.searchByEmail(user.getEmail(), true); + if (!existing.isEmpty()) { + log.info("Utilisateur récupéré par email: {}", user.getEmail()); + return UserMapper.toDTO(existing.get(0), realmName); + } + } + List existing = usersResource.searchByUsername(user.getUsername(), true); + if (!existing.isEmpty()) { + log.info("Utilisateur récupéré par username: {}", user.getUsername()); + return UserMapper.toDTO(existing.get(0), realmName); + } + throw new RuntimeException("Utilisateur en conflit mais introuvable: " + user.getUsername()); + } + if (response.getStatus() != 201) { + throw new RuntimeException("Échec de la création de l'utilisateur (HTTP " + response.getStatus() + " " + response.getStatusInfo().getReasonPhrase() + ")"); + } + + // Récupérer l'ID de l'utilisateur créé + String userId = response.getLocation().getPath().replaceAll(".*/([^/]+)$", "$1"); + + // Définir le mot de passe si fourni + if (user.getTemporaryPassword() != null) { + setPassword(userId, realmName, user.getTemporaryPassword(), + user.getTemporaryPasswordFlag() != null && user.getTemporaryPasswordFlag()); + } + + // Re-appliquer les required actions après la définition du mot de passe. + // resetPassword(temporary=false) retire UPDATE_PASSWORD des required actions dans Keycloak. + if (user.getRequiredActions() != null && !user.getRequiredActions().isEmpty()) { + UserResource userResForActions = usersResource.get(userId); + UserRepresentation repForActions = userResForActions.toRepresentation(); + repForActions.setRequiredActions(user.getRequiredActions()); + userResForActions.update(repForActions); + log.info("✅ Required actions re-appliquées pour {} : {}", userId, user.getRequiredActions()); + } + + // Récupérer l'utilisateur créé + UserResource userResource = usersResource.get(userId); + UserRepresentation createdUser = userResource.toRepresentation(); + + log.info("✅ Utilisateur créé avec succès: {} (ID: {})", user.getUsername(), userId); + return UserMapper.toDTO(createdUser, realmName); + + } catch (Exception e) { + log.error("❌ Erreur lors de la création de l'utilisateur {}", user.getUsername(), e); + throw new RuntimeException("Impossible de créer l'utilisateur", e); + } + } + + @Override + @Logged(action = "USER_UPDATE", resource = "USER") + public UserDTO updateUser(@NotBlank String userId, @Valid @NotNull UserDTO user, @NotBlank String realmName) { + log.info("Mise à jour de l'utilisateur {} dans le realm {}", userId, realmName); + + try { + UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); + + // Récupérer l'utilisateur existant + UserRepresentation existingUser = userResource.toRepresentation(); + + // Mettre à jour les champs + if (user.getEmail() != null) { + existingUser.setEmail(user.getEmail()); + } + if (user.getPrenom() != null) { + existingUser.setFirstName(user.getPrenom()); + } + if (user.getNom() != null) { + existingUser.setLastName(user.getNom()); + } + if (user.getEnabled() != null) { + existingUser.setEnabled(user.getEnabled()); + } + if (user.getEmailVerified() != null) { + existingUser.setEmailVerified(user.getEmailVerified()); + } + if (user.getAttributes() != null) { + existingUser.setAttributes(user.getAttributes()); + } + if (user.getRequiredActions() != null) { + existingUser.setRequiredActions(user.getRequiredActions()); + } + + // Envoyer la mise à jour + userResource.update(existingUser); + + // Récupérer l'utilisateur mis à jour + UserRepresentation updatedUser = userResource.toRepresentation(); + + log.info("✅ Utilisateur mis à jour avec succès: {}", userId); + return UserMapper.toDTO(updatedUser, realmName); + + } catch (NotFoundException e) { + log.error("❌ Utilisateur {} non trouvé", userId); + throw new RuntimeException("Utilisateur non trouvé", e); + } catch (Exception e) { + log.error("❌ Erreur lors de la mise à jour de l'utilisateur {}", userId, e); + throw new RuntimeException("Impossible de mettre à jour l'utilisateur", e); + } + } + + @Override + @Logged(action = "USER_DELETE", resource = "USER") + public void deleteUser(@NotBlank String userId, @NotBlank String realmName, boolean hardDelete) { + log.info("Suppression de l'utilisateur {} dans le realm {} (hard: {})", userId, realmName, hardDelete); + + try { + UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); + + if (hardDelete) { + // Suppression définitive + userResource.remove(); + log.info("✅ Utilisateur supprimé définitivement: {}", userId); + } else { + // Soft delete: désactiver l'utilisateur + UserRepresentation user = userResource.toRepresentation(); + user.setEnabled(false); + userResource.update(user); + log.info("✅ Utilisateur désactivé (soft delete): {}", userId); + } + + } catch (NotFoundException e) { + log.error("❌ Utilisateur {} non trouvé", userId); + throw new RuntimeException("Utilisateur non trouvé", e); + } catch (Exception e) { + log.error("❌ Erreur lors de la suppression de l'utilisateur {}", userId, e); + throw new RuntimeException("Impossible de supprimer l'utilisateur", e); + } + } + + @Override + @Logged(action = "USER_ACTIVATE", resource = "USER") + public void activateUser(@NotBlank String userId, @NotBlank String realmName) { + log.info("Activation de l'utilisateur {} dans le realm {}", userId, realmName); + + try { + UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); + UserRepresentation user = userResource.toRepresentation(); + user.setEnabled(true); + userResource.update(user); + + log.info("✅ Utilisateur activé: {}", userId); + } catch (Exception e) { + log.error("❌ Erreur lors de l'activation de l'utilisateur {}", userId, e); + throw new RuntimeException("Impossible d'activer l'utilisateur", e); + } + } + + @Override + @Logged(action = "USER_DEACTIVATE", resource = "USER") + public void deactivateUser(@NotBlank String userId, @NotBlank String realmName, String raison) { + log.info("Désactivation de l'utilisateur {} dans le realm {} (raison: {})", userId, realmName, raison); + + try { + UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); + UserRepresentation user = userResource.toRepresentation(); + user.setEnabled(false); + userResource.update(user); + + log.info("✅ Utilisateur désactivé: {}", userId); + } catch (Exception e) { + log.error("❌ Erreur lors de la désactivation de l'utilisateur {}", userId, e); + throw new RuntimeException("Impossible de désactiver l'utilisateur", e); + } + } + + @Override + public void suspendUser(@NotBlank String userId, @NotBlank String realmName, String raison, int duree) { + log.info("Suspension de l'utilisateur {} dans le realm {} (raison: {}, durée: {} jours)", + userId, realmName, raison, duree); + + deactivateUser(userId, realmName, raison); + } + + @Override + @Logged(action = "USER_UNLOCK", resource = "USER") + public void unlockUser(@NotBlank String userId, @NotBlank String realmName) { + log.info("Déverrouillage de l'utilisateur {} dans le realm {}", userId, realmName); + activateUser(userId, realmName); + } + + @Override + @Logged(action = "USER_PASSWORD_RESET", resource = "USER") + public void resetPassword(@NotBlank String userId, @NotBlank String realmName, + @NotBlank String temporaryPassword, boolean temporary) { + log.info("Réinitialisation du mot de passe pour l'utilisateur {} (temporaire: {})", userId, temporary); + + setPassword(userId, realmName, temporaryPassword, temporary); + } + + @Override + public void sendVerificationEmail(@NotBlank String userId, @NotBlank String realmName) { + log.info("Envoi d'un email de vérification à l'utilisateur {}", userId); + + try { + UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); + userResource.sendVerifyEmail(); + + log.info("✅ Email de vérification envoyé: {}", userId); + } catch (Exception e) { + log.warn("⚠️ Impossible d'envoyer l'email de vérification pour {} (SMTP peut-être non configuré ou Keycloak indisponible): {}", userId, e.getMessage()); + // L'envoi d'email est best-effort : on ne bloque pas le flux appelant + } + } + + @Override + @Logged(action = "USER_FORCE_LOGOUT", resource = "USER") + public int logoutAllSessions(@NotBlank String userId, @NotBlank String realmName) { + log.info("Déconnexion de toutes les sessions pour l'utilisateur {}", userId); + + try { + UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); + int sessionsCount = userResource.getUserSessions().size(); + userResource.logout(); + + log.info("✅ {} sessions révoquées pour l'utilisateur {}", sessionsCount, userId); + return sessionsCount; + } catch (Exception e) { + log.error("❌ Erreur lors de la déconnexion des sessions pour {}", userId, e); + throw new RuntimeException("Impossible de déconnecter les sessions", e); + } + } + + @Override + public List getActiveSessions(@NotBlank String userId, @NotBlank String realmName) { + log.info("Récupération des sessions actives pour l'utilisateur {}", userId); + + try { + UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); + return userResource.getUserSessions().stream() + .map(session -> session.getId()) + .collect(Collectors.toList()); + } catch (Exception e) { + log.error("❌ Erreur lors de la récupération des sessions pour {}", userId, e); + return Collections.emptyList(); + } + } + + @Override + public long countUsers(@NotNull UserSearchCriteriaDTO criteria) { + try { + return keycloakAdminClient.getUsers(criteria.getRealmName()).count(); + } catch (Exception e) { + log.error("Erreur lors du comptage des utilisateurs", e); + return 0; + } + } + + @Override + public UserSearchResultDTO getAllUsers(@NotBlank String realmName, int page, int pageSize) { + UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder() + .realmName(realmName) + .page(page) + .pageSize(pageSize) + .build(); + + return searchUsers(criteria); + } + + @Override + public boolean usernameExists(@NotBlank String username, @NotBlank String realmName) { + try { + List users = keycloakAdminClient.getUsers(realmName) + .searchByUsername(username, true); + return !users.isEmpty(); + } catch (Exception e) { + log.error("Erreur lors de la vérification de l'existence du username {}", username, e); + return false; + } + } + + @Override + public boolean emailExists(@NotBlank String email, @NotBlank String realmName) { + try { + List users = keycloakAdminClient.getUsers(realmName) + .searchByEmail(email, true); + return !users.isEmpty(); + } catch (Exception e) { + log.error("Erreur lors de la vérification de l'existence de l'email {}", email, e); + return false; + } + } + + @Override + public String exportUsersToCSV(@NotNull UserSearchCriteriaDTO criteria) { + log.info("Export CSV des utilisateurs avec critères: {}", criteria); + + try { + // Récupérer tous les utilisateurs correspondant aux critères + UserSearchResultDTO searchResult = searchUsers(criteria); + List users = searchResult.getUsers(); + + if (users == null || users.isEmpty()) { + log.warn("Aucun utilisateur trouvé pour l'export CSV"); + return generateCSVHeader(); + } + + // Générer le CSV + StringBuilder csv = new StringBuilder(); + csv.append(generateCSVHeader()); + csv.append("\n"); + + for (UserDTO user : users) { + csv.append(escapeCSVField(user.getUsername() != null ? user.getUsername() : "")); + csv.append(","); + csv.append(escapeCSVField(user.getEmail() != null ? user.getEmail() : "")); + csv.append(","); + csv.append(escapeCSVField(user.getPrenom() != null ? user.getPrenom() : "")); + csv.append(","); + csv.append(escapeCSVField(user.getNom() != null ? user.getNom() : "")); + csv.append(","); + csv.append(escapeCSVField(user.getTelephone() != null ? user.getTelephone() : "")); + csv.append(","); + csv.append(user.getEnabled() != null && user.getEnabled() ? "true" : "false"); + csv.append(","); + csv.append(user.getEmailVerified() != null && user.getEmailVerified() ? "true" : "false"); + csv.append(","); + csv.append(escapeCSVField(user.getStatut() != null ? user.getStatut().name() : "")); + csv.append(","); + csv.append(escapeCSVField(user.getOrganisation() != null ? user.getOrganisation() : "")); + csv.append(","); + csv.append(escapeCSVField(user.getDepartement() != null ? user.getDepartement() : "")); + csv.append(","); + csv.append(escapeCSVField(user.getFonction() != null ? user.getFonction() : "")); + csv.append(","); + csv.append(escapeCSVField(user.getPays() != null ? user.getPays() : "")); + csv.append(","); + csv.append(escapeCSVField(user.getVille() != null ? user.getVille() : "")); + csv.append(","); + csv.append(escapeCSVField(user.getDateCreation() != null + ? user.getDateCreation().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + : "")); + csv.append("\n"); + } + + log.info("✅ Export CSV réussi: {} utilisateur(s) exporté(s)", users.size()); + return csv.toString(); + + } catch (Exception e) { + log.error("❌ Erreur lors de l'export CSV", e); + throw new RuntimeException("Impossible d'exporter les utilisateurs en CSV", e); + } + } + + private String generateCSVHeader() { + return "username,email,prenom,nom,telephone,enabled,emailVerified,statut,organisation,departement,fonction,pays,ville,dateCreation"; + } + + private String escapeCSVField(String field) { + if (field == null) { + return ""; + } + // Si le champ contient une virgule, des guillemets ou un saut de ligne, + // l'entourer de guillemets + if (field.contains(",") || field.contains("\"") || field.contains("\n")) { + // Échapper les guillemets en les doublant + return "\"" + field.replace("\"", "\"\"") + "\""; + } + return field; + } + + @Override + @Logged(action = "USER_IMPORT", resource = "USER") + public ImportResultDTO importUsersFromCSV(@NotBlank String csvContent, @NotBlank String realmName) { + log.info("Import CSV de {} lignes pour le realm {}", csvContent.split("\n").length, realmName); + + ImportResultDTO.ImportResultDTOBuilder resultBuilder = ImportResultDTO.builder(); + List errors = new ArrayList<>(); + int successCount = 0; + int lineNumber = 0; + + String[] lines = csvContent.split("\n"); + resultBuilder.totalLines(lines.length); + + // Ignorer la première ligne si c'est l'en-tête + int startLine = 0; + if (lines.length > 0 && lines[0].toLowerCase().contains("username")) { + startLine = 1; + } + + for (int i = startLine; i < lines.length; i++) { + lineNumber = i + 1; + String line = lines[i].trim(); + + if (line.isEmpty()) { + continue; + } + + try { + // Parser la ligne CSV + String[] fields = parseCSVLine(line); + + if (fields.length < 4) { + errors.add(ImportResultDTO.ImportErrorDTO.builder() + .lineNumber(lineNumber) + .lineContent(line) + .errorType(ImportResultDTO.ErrorType.INVALID_FORMAT) + .message("Nombre de colonnes insuffisant (minimum 4: username, email, prenom, nom)") + .build()); + continue; + } + + // Créer l'utilisateur + String username = fields[0].trim(); + String email = fields[1].trim(); + String prenom = fields.length > 2 ? fields[2].trim() : ""; + String nom = fields.length > 3 ? fields[3].trim() : ""; + String telephone = fields.length > 4 ? fields[4].trim() : null; + Boolean enabled = fields.length > 5 ? Boolean.parseBoolean(fields[5].trim()) : true; + Boolean emailVerified = fields.length > 6 ? Boolean.parseBoolean(fields[6].trim()) : false; + + // Valider les champs obligatoires + if (username.isBlank() || email.isBlank()) { + errors.add(ImportResultDTO.ImportErrorDTO.builder() + .lineNumber(lineNumber) + .lineContent(line) + .errorType(ImportResultDTO.ErrorType.VALIDATION_ERROR) + .field(username.isBlank() ? "username" : "email") + .message("Le username et l'email sont obligatoires") + .build()); + continue; + } + + // Vérifier si l'utilisateur existe déjà + if (emailExists(email, realmName)) { + errors.add(ImportResultDTO.ImportErrorDTO.builder() + .lineNumber(lineNumber) + .lineContent(line) + .errorType(ImportResultDTO.ErrorType.DUPLICATE_USER) + .field("email") + .message("Un utilisateur avec cet email existe déjà") + .build()); + continue; + } + + // Créer l'utilisateur + UserDTO newUser = UserDTO.builder() + .username(username) + .email(email) + .prenom(prenom) + .nom(nom) + .telephone(telephone) + .enabled(enabled) + .emailVerified(emailVerified) + .build(); + + createUser(newUser, realmName); + successCount++; + + } catch (Exception e) { + log.error("Erreur lors de l'import de la ligne {}: {}", lineNumber, e.getMessage()); + errors.add(ImportResultDTO.ImportErrorDTO.builder() + .lineNumber(lineNumber) + .lineContent(line) + .errorType(ImportResultDTO.ErrorType.CREATION_ERROR) + .message("Erreur lors de la création: " + e.getMessage()) + .details(e.getClass().getSimpleName()) + .build()); + } + } + + resultBuilder.successCount(successCount); + resultBuilder.errors(errors); + resultBuilder.errorCount(errors.size()); + + ImportResultDTO result = resultBuilder.build(); + result.generateMessage(); + + log.info("✅ Import CSV terminé: {} succès, {} erreurs", successCount, errors.size()); + return result; + } + + private String[] parseCSVLine(String line) { + List fields = new ArrayList<>(); + boolean inQuotes = false; + StringBuilder currentField = new StringBuilder(); + + for (int i = 0; i < line.length(); i++) { + char c = line.charAt(i); + + if (c == '"') { + if (inQuotes && i + 1 < line.length() && line.charAt(i + 1) == '"') { + // Guillemet échappé + currentField.append('"'); + i++; // Passer le guillemet suivant + } else { + // Toggle inQuotes + inQuotes = !inQuotes; + } + } else if (c == ',' && !inQuotes) { + // Fin du champ + fields.add(currentField.toString()); + currentField = new StringBuilder(); + } else { + currentField.append(c); + } + } + + // Ajouter le dernier champ + fields.add(currentField.toString()); + + return fields.toArray(new String[0]); + } + + // ==================== Méthodes privées ==================== + + private void setPassword(String userId, String realmName, String password, boolean temporary) { + try { + UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); + + CredentialRepresentation credential = new CredentialRepresentation(); + credential.setType(CredentialRepresentation.PASSWORD); + credential.setValue(password); + credential.setTemporary(temporary); + + userResource.resetPassword(credential); + + log.info("✅ Mot de passe défini pour l'utilisateur {} (temporaire: {})", userId, temporary); + } catch (Exception e) { + log.error("❌ Erreur lors de la définition du mot de passe pour {}", userId, e); + throw new RuntimeException("Impossible de définir le mot de passe", e); + } + } + + private List filterUsers(List users, UserSearchCriteriaDTO criteria) { + return users.stream() + .filter(user -> { + // Filtrer par enabled + if (criteria.getEnabled() != null && !criteria.getEnabled().equals(user.isEnabled())) { + return false; + } + + // Filtrer par emailVerified + if (criteria.getEmailVerified() != null + && !criteria.getEmailVerified().equals(user.isEmailVerified())) { + return false; + } + + // Filtrer par username + if (criteria.getUsername() != null + && !criteria.getUsername().equalsIgnoreCase(user.getUsername())) { + return false; + } + + // Filtrer par email + if (criteria.getEmail() != null && !criteria.getEmail().equalsIgnoreCase(user.getEmail())) { + return false; + } + + // Filtrer par prénom + if (criteria.getPrenom() != null) { + String prenom = user.getFirstName(); + if (prenom == null || !prenom.toLowerCase().contains(criteria.getPrenom().toLowerCase())) { + return false; + } + } + + // Filtrer par nom + if (criteria.getNom() != null) { + String nom = user.getLastName(); + if (nom == null || !nom.toLowerCase().contains(criteria.getNom().toLowerCase())) { + return false; + } + } + + // Filtrer par téléphone + if (criteria.getTelephone() != null) { + String phone = user.getAttributes() != null + ? user.getAttributes().getOrDefault("phone_number", Collections.emptyList()).stream() + .findFirst().orElse(null) + : null; + if (phone == null || !phone.contains(criteria.getTelephone())) { + return false; + } + } + + // Filtrer par statut (basé sur enabled et emailVerified) + if (criteria.getStatut() != null) { + StatutUser userStatut = determineUserStatut(user); + if (!criteria.getStatut().equals(userStatut)) { + return false; + } + } + + // Filtrer par organisation + if (criteria.getOrganisation() != null) { + String org = user.getAttributes() != null + ? user.getAttributes().getOrDefault("organization", Collections.emptyList()).stream() + .findFirst().orElse(null) + : null; + if (org == null || !org.toLowerCase().contains(criteria.getOrganisation().toLowerCase())) { + return false; + } + } + + // Filtrer par département + if (criteria.getDepartement() != null) { + String dept = user.getAttributes() != null + ? user.getAttributes().getOrDefault("department", Collections.emptyList()).stream() + .findFirst().orElse(null) + : null; + if (dept == null || !dept.toLowerCase().contains(criteria.getDepartement().toLowerCase())) { + return false; + } + } + + // Filtrer par fonction + if (criteria.getFonction() != null) { + String fonction = user.getAttributes() != null + ? user.getAttributes().getOrDefault("job_title", Collections.emptyList()).stream() + .findFirst().orElse(null) + : null; + if (fonction == null + || !fonction.toLowerCase().contains(criteria.getFonction().toLowerCase())) { + return false; + } + } + + // Filtrer par pays + if (criteria.getPays() != null) { + String pays = user.getAttributes() != null + ? user.getAttributes().getOrDefault("country", Collections.emptyList()).stream() + .findFirst().orElse(null) + : null; + if (pays == null || !pays.toLowerCase().contains(criteria.getPays().toLowerCase())) { + return false; + } + } + + // Filtrer par ville + if (criteria.getVille() != null) { + String ville = user.getAttributes() != null + ? user.getAttributes().getOrDefault("city", Collections.emptyList()).stream() + .findFirst().orElse(null) + : null; + if (ville == null || !ville.toLowerCase().contains(criteria.getVille().toLowerCase())) { + return false; + } + } + + // Filtrer par date de création + if (criteria.getDateCreationMin() != null || criteria.getDateCreationMax() != null) { + Long createdTimestamp = user.getCreatedTimestamp(); + if (createdTimestamp != null) { + LocalDateTime createdDate = LocalDateTime.ofEpochSecond( + createdTimestamp / 1000, 0, + java.time.ZoneOffset.UTC); + + if (criteria.getDateCreationMin() != null && + createdDate.isBefore(criteria.getDateCreationMin())) { + return false; + } + + if (criteria.getDateCreationMax() != null && + createdDate.isAfter(criteria.getDateCreationMax())) { + return false; + } + } else { + // Si pas de date de création et qu'un filtre min est défini, exclure + if (criteria.getDateCreationMin() != null) { + return false; + } + } + } + + return true; + }) + .collect(Collectors.toList()); + } + + private StatutUser determineUserStatut(UserRepresentation user) { + if (!user.isEnabled()) { + return StatutUser.INACTIF; + } + // Si enabled mais email non vérifié, on considère toujours comme ACTIF + // car l'email non vérifié n'empêche pas l'activation du compte + return StatutUser.ACTIF; + } +} diff --git a/src/main/resources/META-INF/beans.xml b/src/main/resources/META-INF/beans.xml index 5e4c686..0a9276d 100644 --- a/src/main/resources/META-INF/beans.xml +++ b/src/main/resources/META-INF/beans.xml @@ -1,6 +1,6 @@ - - - + + + diff --git a/src/main/resources/META-INF/reflection-config.json b/src/main/resources/META-INF/reflection-config.json index d64bffe..67d1228 100644 --- a/src/main/resources/META-INF/reflection-config.json +++ b/src/main/resources/META-INF/reflection-config.json @@ -1,33 +1,33 @@ -[ - { - "name": "dev.lions.user.manager.dto.realm.RealmAssignmentDTO", - "allDeclaredFields": true, - "allDeclaredMethods": true, - "allDeclaredConstructors": true - }, - { - "name": "dev.lions.user.manager.dto.role.RoleDTO", - "allDeclaredFields": true, - "allDeclaredMethods": true, - "allDeclaredConstructors": true - }, - { - "name": "dev.lions.user.manager.dto.role.RoleDTO$RoleCompositeDTO", - "allDeclaredFields": true, - "allDeclaredMethods": true, - "allDeclaredConstructors": true - }, - { - "name": "dev.lions.user.manager.dto.user.UserDTO", - "allDeclaredFields": true, - "allDeclaredMethods": true, - "allDeclaredConstructors": true - }, - { - "name": "dev.lions.user.manager.dto.user.UserSearchResultDTO", - "allDeclaredFields": true, - "allDeclaredMethods": true, - "allDeclaredConstructors": true - } -] - +[ + { + "name": "dev.lions.user.manager.dto.realm.RealmAssignmentDTO", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "dev.lions.user.manager.dto.role.RoleDTO", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "dev.lions.user.manager.dto.role.RoleDTO$RoleCompositeDTO", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "dev.lions.user.manager.dto.user.UserDTO", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "dev.lions.user.manager.dto.user.UserSearchResultDTO", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + } +] + diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 8ec123b..fe1da4f 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -68,7 +68,7 @@ quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:543 # ============================================ # Hibernate ORM Configuration DEV # ============================================ -quarkus.hibernate-orm.database.generation=update +quarkus.hibernate-orm.schema-management.strategy=update quarkus.hibernate-orm.log.sql=true # ============================================ @@ -89,11 +89,11 @@ quarkus.log.category."io.quarkus.oidc.runtime".level=DEBUG quarkus.log.category."io.quarkus.security".level=DEBUG quarkus.log.category."io.quarkus.security.runtime".level=DEBUG -quarkus.log.console.enable=true +quarkus.log.console.enabled=true quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n # File Logging pour Audit DEV -quarkus.log.file.enable=true +quarkus.log.file.enabled=true quarkus.log.file.path=logs/dev/lions-user-manager.log quarkus.log.file.rotation.max-file-size=10M quarkus.log.file.rotation.max-backup-index=3 @@ -102,7 +102,7 @@ quarkus.log.file.rotation.max-backup-index=3 # OpenAPI/Swagger Configuration DEV # ============================================ quarkus.swagger-ui.always-include=true -quarkus.swagger-ui.enable=true +quarkus.swagger-ui.enabled=true # ============================================ # Dev Services DEV diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index 2e77d4d..90035f5 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -75,7 +75,7 @@ quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST}:${DB_PORT:5432}/${DB_NA # ============================================ # Hibernate ORM Configuration PROD # ============================================ -quarkus.hibernate-orm.database.generation=none +quarkus.hibernate-orm.schema-management.strategy=none quarkus.hibernate-orm.log.sql=false # ============================================ @@ -91,17 +91,17 @@ quarkus.log.category."dev.lions.user.manager".level=INFO quarkus.log.category."org.keycloak".level=WARN quarkus.log.category."io.quarkus".level=INFO -quarkus.log.console.enable=true +quarkus.log.console.enabled=true quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n # File Logging désactivé en PROD (logs centralisés via Kubernetes) -quarkus.log.file.enable=false +quarkus.log.file.enabled=false # ============================================ # OpenAPI/Swagger Configuration PROD # ============================================ quarkus.swagger-ui.always-include=true -quarkus.swagger-ui.enable=true +quarkus.swagger-ui.enabled=true quarkus.swagger-ui.urls.default=/lions-user-manager/q/openapi # ============================================ diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7bfd689..952f35e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -15,7 +15,7 @@ quarkus.application.version=1.0.0 # HTTP Configuration (COMMUNE) # ============================================ quarkus.http.host=0.0.0.0 -quarkus.http.cors=true +quarkus.http.cors.enabled=true quarkus.http.cors.methods=GET,POST,PUT,DELETE,PATCH,OPTIONS quarkus.http.cors.headers=* 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 index 351acae..b88fa0a 100644 --- 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 @@ -1,175 +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 --- ============================================================================= +-- ============================================================================= +-- 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/main/resources/db/migration/V2.0.0__Create_sync_tables.sql b/src/main/resources/db/migration/V2.0.0__Create_sync_tables.sql index c234d62..dc08f25 100644 --- a/src/main/resources/db/migration/V2.0.0__Create_sync_tables.sql +++ b/src/main/resources/db/migration/V2.0.0__Create_sync_tables.sql @@ -1,85 +1,85 @@ --- ============================================================================= --- Migration Flyway V2.0.0 - Création des tables de synchronisation Keycloak --- ============================================================================= --- Description: Tables pour la persistance des snapshots et de l'historique --- des synchronisations entre l'application et Keycloak. --- --- Entités correspondantes: --- SyncHistoryEntity → sync_history --- SyncedUserEntity → synced_user --- SyncedRoleEntity → synced_role --- --- Auteur: Lions Development Team --- Date: 2026-02-17 --- Version: 2.0.0 --- ============================================================================= - - --- ============================================================================= --- TABLE sync_history : historique des opérations de synchronisation --- ============================================================================= -CREATE TABLE IF NOT EXISTS sync_history ( - id BIGSERIAL PRIMARY KEY, - realm_name VARCHAR(255) NOT NULL, - sync_date TIMESTAMP NOT NULL, - sync_type VARCHAR(50) NOT NULL, -- 'USER' ou 'ROLE' - status VARCHAR(50) NOT NULL, -- 'SUCCESS' ou 'FAILURE' - items_processed INTEGER, - duration_ms BIGINT, - error_message TEXT -); - -CREATE INDEX IF NOT EXISTS idx_sync_realm ON sync_history(realm_name); -CREATE INDEX IF NOT EXISTS idx_sync_date ON sync_history(sync_date DESC); - -COMMENT ON TABLE sync_history IS 'Historique des synchronisations Keycloak (users et rôles)'; -COMMENT ON COLUMN sync_history.sync_type IS 'Type de synchronisation : USER ou ROLE'; -COMMENT ON COLUMN sync_history.status IS 'Résultat : SUCCESS ou FAILURE'; - - --- ============================================================================= --- TABLE synced_user : snapshot local des utilisateurs Keycloak synchronisés --- ============================================================================= -CREATE TABLE IF NOT EXISTS synced_user ( - id BIGSERIAL PRIMARY KEY, - realm_name VARCHAR(255) NOT NULL, - keycloak_id VARCHAR(255) NOT NULL, - username VARCHAR(255) NOT NULL, - email VARCHAR(255), - enabled BOOLEAN, - email_verified BOOLEAN, - created_at TIMESTAMP, - CONSTRAINT uq_synced_user_realm_kc UNIQUE (realm_name, keycloak_id) -); - -CREATE INDEX IF NOT EXISTS idx_synced_user_realm - ON synced_user(realm_name); -CREATE UNIQUE INDEX IF NOT EXISTS idx_synced_user_realm_kc_id - ON synced_user(realm_name, keycloak_id); - -COMMENT ON TABLE synced_user IS 'Snapshot local des utilisateurs Keycloak pour rapports et vérifications'; -COMMENT ON COLUMN synced_user.keycloak_id IS 'UUID Keycloak de l''utilisateur'; - - --- ============================================================================= --- TABLE synced_role : snapshot local des rôles Keycloak synchronisés --- ============================================================================= -CREATE TABLE IF NOT EXISTS synced_role ( - id BIGSERIAL PRIMARY KEY, - realm_name VARCHAR(255) NOT NULL, - role_name VARCHAR(255) NOT NULL, - description VARCHAR(500), - CONSTRAINT uq_synced_role_realm_name UNIQUE (realm_name, role_name) -); - -CREATE INDEX IF NOT EXISTS idx_synced_role_realm - ON synced_role(realm_name); -CREATE UNIQUE INDEX IF NOT EXISTS idx_synced_role_realm_name - ON synced_role(realm_name, role_name); - -COMMENT ON TABLE synced_role IS 'Snapshot local des rôles Keycloak pour rapports et vérifications'; -COMMENT ON COLUMN synced_role.role_name IS 'Nom du rôle realm dans Keycloak'; - --- ============================================================================= --- FIN DE LA MIGRATION --- ============================================================================= +-- ============================================================================= +-- Migration Flyway V2.0.0 - Création des tables de synchronisation Keycloak +-- ============================================================================= +-- Description: Tables pour la persistance des snapshots et de l'historique +-- des synchronisations entre l'application et Keycloak. +-- +-- Entités correspondantes: +-- SyncHistoryEntity → sync_history +-- SyncedUserEntity → synced_user +-- SyncedRoleEntity → synced_role +-- +-- Auteur: Lions Development Team +-- Date: 2026-02-17 +-- Version: 2.0.0 +-- ============================================================================= + + +-- ============================================================================= +-- TABLE sync_history : historique des opérations de synchronisation +-- ============================================================================= +CREATE TABLE IF NOT EXISTS sync_history ( + id BIGSERIAL PRIMARY KEY, + realm_name VARCHAR(255) NOT NULL, + sync_date TIMESTAMP NOT NULL, + sync_type VARCHAR(50) NOT NULL, -- 'USER' ou 'ROLE' + status VARCHAR(50) NOT NULL, -- 'SUCCESS' ou 'FAILURE' + items_processed INTEGER, + duration_ms BIGINT, + error_message TEXT +); + +CREATE INDEX IF NOT EXISTS idx_sync_realm ON sync_history(realm_name); +CREATE INDEX IF NOT EXISTS idx_sync_date ON sync_history(sync_date DESC); + +COMMENT ON TABLE sync_history IS 'Historique des synchronisations Keycloak (users et rôles)'; +COMMENT ON COLUMN sync_history.sync_type IS 'Type de synchronisation : USER ou ROLE'; +COMMENT ON COLUMN sync_history.status IS 'Résultat : SUCCESS ou FAILURE'; + + +-- ============================================================================= +-- TABLE synced_user : snapshot local des utilisateurs Keycloak synchronisés +-- ============================================================================= +CREATE TABLE IF NOT EXISTS synced_user ( + id BIGSERIAL PRIMARY KEY, + realm_name VARCHAR(255) NOT NULL, + keycloak_id VARCHAR(255) NOT NULL, + username VARCHAR(255) NOT NULL, + email VARCHAR(255), + enabled BOOLEAN, + email_verified BOOLEAN, + created_at TIMESTAMP, + CONSTRAINT uq_synced_user_realm_kc UNIQUE (realm_name, keycloak_id) +); + +CREATE INDEX IF NOT EXISTS idx_synced_user_realm + ON synced_user(realm_name); +CREATE UNIQUE INDEX IF NOT EXISTS idx_synced_user_realm_kc_id + ON synced_user(realm_name, keycloak_id); + +COMMENT ON TABLE synced_user IS 'Snapshot local des utilisateurs Keycloak pour rapports et vérifications'; +COMMENT ON COLUMN synced_user.keycloak_id IS 'UUID Keycloak de l''utilisateur'; + + +-- ============================================================================= +-- TABLE synced_role : snapshot local des rôles Keycloak synchronisés +-- ============================================================================= +CREATE TABLE IF NOT EXISTS synced_role ( + id BIGSERIAL PRIMARY KEY, + realm_name VARCHAR(255) NOT NULL, + role_name VARCHAR(255) NOT NULL, + description VARCHAR(500), + CONSTRAINT uq_synced_role_realm_name UNIQUE (realm_name, role_name) +); + +CREATE INDEX IF NOT EXISTS idx_synced_role_realm + ON synced_role(realm_name); +CREATE UNIQUE INDEX IF NOT EXISTS idx_synced_role_realm_name + ON synced_role(realm_name, role_name); + +COMMENT ON TABLE synced_role IS 'Snapshot local des rôles Keycloak pour rapports et vérifications'; +COMMENT ON COLUMN synced_role.role_name IS 'Nom du rôle realm dans Keycloak'; + +-- ============================================================================= +-- FIN DE LA MIGRATION +-- ============================================================================= diff --git a/src/test/java/dev/lions/user/manager/client/KeycloakAdminClientImplCompleteTest.java b/src/test/java/dev/lions/user/manager/client/KeycloakAdminClientImplCompleteTest.java index 29a07a1..fdc6172 100644 --- a/src/test/java/dev/lions/user/manager/client/KeycloakAdminClientImplCompleteTest.java +++ b/src/test/java/dev/lions/user/manager/client/KeycloakAdminClientImplCompleteTest.java @@ -1,265 +1,265 @@ -package dev.lions.user.manager.client; - -import com.sun.net.httpserver.HttpServer; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.keycloak.admin.client.Keycloak; -import org.keycloak.admin.client.resource.RealmResource; -import org.keycloak.admin.client.resource.RolesResource; -import org.keycloak.admin.client.resource.UsersResource; -import org.keycloak.admin.client.token.TokenManager; -import org.keycloak.representations.info.ServerInfoRepresentation; -import org.keycloak.admin.client.resource.ServerInfoResource; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import jakarta.ws.rs.NotFoundException; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.net.InetSocketAddress; -import java.nio.charset.StandardCharsets; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -/** - * Tests complets pour KeycloakAdminClientImpl - */ -@ExtendWith(MockitoExtension.class) -class KeycloakAdminClientImplCompleteTest { - - @Mock - Keycloak mockKeycloak; - - @Mock - TokenManager mockTokenManager; - - @InjectMocks - KeycloakAdminClientImpl client; - - private HttpServer localServer; - private int localPort; - - private void setField(String fieldName, Object value) throws Exception { - Field field = KeycloakAdminClientImpl.class.getDeclaredField(fieldName); - field.setAccessible(true); - field.set(client, value); - } - - @BeforeEach - void setUp() throws Exception { - setField("serverUrl", "http://localhost:8180"); - setField("adminRealm", "master"); - setField("adminClientId", "admin-cli"); - setField("adminUsername", "admin"); - } - - @AfterEach - void tearDown() { - if (localServer != null) { - localServer.stop(0); - localServer = null; - } - } - - private int startLocalServer(String path, String responseBody, int statusCode) throws Exception { - localServer = HttpServer.create(new InetSocketAddress(0), 0); - localServer.createContext(path, exchange -> { - byte[] bytes = responseBody.getBytes(StandardCharsets.UTF_8); - exchange.sendResponseHeaders(statusCode, bytes.length); - exchange.getResponseBody().write(bytes); - exchange.getResponseBody().close(); - }); - localServer.start(); - return localServer.getAddress().getPort(); - } - - @Test - void testGetInstance() { - Keycloak result = client.getInstance(); - assertSame(mockKeycloak, result); - } - - @Test - void testGetRealm_Success() { - RealmResource mockRealmResource = mock(RealmResource.class); - when(mockKeycloak.realm("test-realm")).thenReturn(mockRealmResource); - - RealmResource result = client.getRealm("test-realm"); - assertSame(mockRealmResource, result); - } - - @Test - void testGetRealm_Exception() { - when(mockKeycloak.realm("bad-realm")).thenThrow(new RuntimeException("Connection error")); - - assertThrows(RuntimeException.class, () -> client.getRealm("bad-realm")); - } - - @Test - void testGetUsers() { - RealmResource mockRealmResource = mock(RealmResource.class); - UsersResource mockUsersResource = mock(UsersResource.class); - when(mockKeycloak.realm("test-realm")).thenReturn(mockRealmResource); - when(mockRealmResource.users()).thenReturn(mockUsersResource); - - UsersResource result = client.getUsers("test-realm"); - assertSame(mockUsersResource, result); - } - - @Test - void testGetRoles() { - RealmResource mockRealmResource = mock(RealmResource.class); - RolesResource mockRolesResource = mock(RolesResource.class); - when(mockKeycloak.realm("test-realm")).thenReturn(mockRealmResource); - when(mockRealmResource.roles()).thenReturn(mockRolesResource); - - RolesResource result = client.getRoles("test-realm"); - assertSame(mockRolesResource, result); - } - - @Test - void testIsConnected_True() { - when(mockKeycloak.tokenManager()).thenReturn(mockTokenManager); - when(mockTokenManager.getAccessTokenString()).thenReturn("fake-token"); - - assertTrue(client.isConnected()); - } - - @Test - void testIsConnected_False() { - when(mockKeycloak.tokenManager()).thenThrow(new RuntimeException("Connection refused")); - - assertFalse(client.isConnected()); - } - - @Test - void testRealmExists_True() { - RealmResource mockRealmResource = mock(RealmResource.class); - RolesResource mockRolesResource = mock(RolesResource.class); - when(mockKeycloak.realm("test-realm")).thenReturn(mockRealmResource); - when(mockRealmResource.roles()).thenReturn(mockRolesResource); - when(mockRolesResource.list()).thenReturn(List.of()); - - assertTrue(client.realmExists("test-realm")); - } - - @Test - void testRealmExists_NotFound() { - RealmResource mockRealmResource = mock(RealmResource.class); - RolesResource mockRolesResource = mock(RolesResource.class); - when(mockKeycloak.realm("missing")).thenReturn(mockRealmResource); - when(mockRealmResource.roles()).thenReturn(mockRolesResource); - when(mockRolesResource.list()).thenThrow(new NotFoundException("Not found")); - - assertFalse(client.realmExists("missing")); - } - - @Test - void testRealmExists_OtherException() { - when(mockKeycloak.realm("error-realm")).thenThrow(new RuntimeException("Other error")); - - assertTrue(client.realmExists("error-realm")); - } - - @Test - void testGetAllRealms_TokenError() { - // When token retrieval fails, getAllRealms should throw - when(mockKeycloak.tokenManager()).thenThrow(new RuntimeException("Token error")); - - assertThrows(RuntimeException.class, () -> client.getAllRealms()); - } - - @Test - void testGetAllRealms_NullTokenManager() { - when(mockKeycloak.tokenManager()).thenReturn(null); - - assertThrows(RuntimeException.class, () -> client.getAllRealms()); - } - - @Test - void testClose() { - assertDoesNotThrow(() -> client.close()); - } - - @Test - void testReconnect() { - assertDoesNotThrow(() -> client.reconnect()); - } - - @Test - void testInit() throws Exception { - // init() est appelé @PostConstruct — l'appeler via réflexion pour couvrir la méthode - Method initMethod = KeycloakAdminClientImpl.class.getDeclaredMethod("init"); - initMethod.setAccessible(true); - assertDoesNotThrow(() -> initMethod.invoke(client)); - } - - @Test - void testGetAllRealms_Success() throws Exception { - // Démarrer un serveur HTTP local qui retourne une liste de realms JSON - String realmsJson = "[{\"realm\":\"master\",\"id\":\"1\"},{\"realm\":\"lions\",\"id\":\"2\"}]"; - localPort = startLocalServer("/admin/realms", realmsJson, 200); - setField("serverUrl", "http://localhost:" + localPort); - - when(mockKeycloak.tokenManager()).thenReturn(mockTokenManager); - when(mockTokenManager.getAccessTokenString()).thenReturn("fake-token"); - - List realms = client.getAllRealms(); - - assertNotNull(realms); - assertEquals(2, realms.size()); - assertTrue(realms.contains("master")); - assertTrue(realms.contains("lions")); - } - - @Test - void testGetAllRealms_NonOkResponse() throws Exception { - localPort = startLocalServer("/admin/realms", "Forbidden", 403); - setField("serverUrl", "http://localhost:" + localPort); - - when(mockKeycloak.tokenManager()).thenReturn(mockTokenManager); - when(mockTokenManager.getAccessTokenString()).thenReturn("fake-token"); - - assertThrows(RuntimeException.class, () -> client.getAllRealms()); - } - - @Test - void testGetRealmClients_Success() throws Exception { - String clientsJson = "[{\"clientId\":\"admin-cli\",\"id\":\"1\"},{\"clientId\":\"account\",\"id\":\"2\"}]"; - localPort = startLocalServer("/admin/realms/master/clients", clientsJson, 200); - setField("serverUrl", "http://localhost:" + localPort); - - when(mockKeycloak.tokenManager()).thenReturn(mockTokenManager); - when(mockTokenManager.getAccessTokenString()).thenReturn("fake-token"); - - List clients = client.getRealmClients("master"); - - assertNotNull(clients); - assertEquals(2, clients.size()); - assertTrue(clients.contains("admin-cli")); - assertTrue(clients.contains("account")); - } - - @Test - void testGetRealmClients_NonOkResponse() throws Exception { - localPort = startLocalServer("/admin/realms/bad/clients", "Not Found", 404); - setField("serverUrl", "http://localhost:" + localPort); - - when(mockKeycloak.tokenManager()).thenReturn(mockTokenManager); - when(mockTokenManager.getAccessTokenString()).thenReturn("fake-token"); - - assertThrows(RuntimeException.class, () -> client.getRealmClients("bad")); - } - - @Test - void testGetRealmClients_TokenError() { - when(mockKeycloak.tokenManager()).thenThrow(new RuntimeException("Token error")); - - assertThrows(RuntimeException.class, () -> client.getRealmClients("master")); - } -} +package dev.lions.user.manager.client; + +import com.sun.net.httpserver.HttpServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.RolesResource; +import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.admin.client.token.TokenManager; +import org.keycloak.representations.info.ServerInfoRepresentation; +import org.keycloak.admin.client.resource.ServerInfoResource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import jakarta.ws.rs.NotFoundException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Tests complets pour KeycloakAdminClientImpl + */ +@ExtendWith(MockitoExtension.class) +class KeycloakAdminClientImplCompleteTest { + + @Mock + Keycloak mockKeycloak; + + @Mock + TokenManager mockTokenManager; + + @InjectMocks + KeycloakAdminClientImpl client; + + private HttpServer localServer; + private int localPort; + + private void setField(String fieldName, Object value) throws Exception { + Field field = KeycloakAdminClientImpl.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(client, value); + } + + @BeforeEach + void setUp() throws Exception { + setField("serverUrl", "http://localhost:8180"); + setField("adminRealm", "master"); + setField("adminClientId", "admin-cli"); + setField("adminUsername", "admin"); + } + + @AfterEach + void tearDown() { + if (localServer != null) { + localServer.stop(0); + localServer = null; + } + } + + private int startLocalServer(String path, String responseBody, int statusCode) throws Exception { + localServer = HttpServer.create(new InetSocketAddress(0), 0); + localServer.createContext(path, exchange -> { + byte[] bytes = responseBody.getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(statusCode, bytes.length); + exchange.getResponseBody().write(bytes); + exchange.getResponseBody().close(); + }); + localServer.start(); + return localServer.getAddress().getPort(); + } + + @Test + void testGetInstance() { + Keycloak result = client.getInstance(); + assertSame(mockKeycloak, result); + } + + @Test + void testGetRealm_Success() { + RealmResource mockRealmResource = mock(RealmResource.class); + when(mockKeycloak.realm("test-realm")).thenReturn(mockRealmResource); + + RealmResource result = client.getRealm("test-realm"); + assertSame(mockRealmResource, result); + } + + @Test + void testGetRealm_Exception() { + when(mockKeycloak.realm("bad-realm")).thenThrow(new RuntimeException("Connection error")); + + assertThrows(RuntimeException.class, () -> client.getRealm("bad-realm")); + } + + @Test + void testGetUsers() { + RealmResource mockRealmResource = mock(RealmResource.class); + UsersResource mockUsersResource = mock(UsersResource.class); + when(mockKeycloak.realm("test-realm")).thenReturn(mockRealmResource); + when(mockRealmResource.users()).thenReturn(mockUsersResource); + + UsersResource result = client.getUsers("test-realm"); + assertSame(mockUsersResource, result); + } + + @Test + void testGetRoles() { + RealmResource mockRealmResource = mock(RealmResource.class); + RolesResource mockRolesResource = mock(RolesResource.class); + when(mockKeycloak.realm("test-realm")).thenReturn(mockRealmResource); + when(mockRealmResource.roles()).thenReturn(mockRolesResource); + + RolesResource result = client.getRoles("test-realm"); + assertSame(mockRolesResource, result); + } + + @Test + void testIsConnected_True() { + when(mockKeycloak.tokenManager()).thenReturn(mockTokenManager); + when(mockTokenManager.getAccessTokenString()).thenReturn("fake-token"); + + assertTrue(client.isConnected()); + } + + @Test + void testIsConnected_False() { + when(mockKeycloak.tokenManager()).thenThrow(new RuntimeException("Connection refused")); + + assertFalse(client.isConnected()); + } + + @Test + void testRealmExists_True() { + RealmResource mockRealmResource = mock(RealmResource.class); + RolesResource mockRolesResource = mock(RolesResource.class); + when(mockKeycloak.realm("test-realm")).thenReturn(mockRealmResource); + when(mockRealmResource.roles()).thenReturn(mockRolesResource); + when(mockRolesResource.list()).thenReturn(List.of()); + + assertTrue(client.realmExists("test-realm")); + } + + @Test + void testRealmExists_NotFound() { + RealmResource mockRealmResource = mock(RealmResource.class); + RolesResource mockRolesResource = mock(RolesResource.class); + when(mockKeycloak.realm("missing")).thenReturn(mockRealmResource); + when(mockRealmResource.roles()).thenReturn(mockRolesResource); + when(mockRolesResource.list()).thenThrow(new NotFoundException("Not found")); + + assertFalse(client.realmExists("missing")); + } + + @Test + void testRealmExists_OtherException() { + when(mockKeycloak.realm("error-realm")).thenThrow(new RuntimeException("Other error")); + + assertTrue(client.realmExists("error-realm")); + } + + @Test + void testGetAllRealms_TokenError() { + // When token retrieval fails, getAllRealms should throw + when(mockKeycloak.tokenManager()).thenThrow(new RuntimeException("Token error")); + + assertThrows(RuntimeException.class, () -> client.getAllRealms()); + } + + @Test + void testGetAllRealms_NullTokenManager() { + when(mockKeycloak.tokenManager()).thenReturn(null); + + assertThrows(RuntimeException.class, () -> client.getAllRealms()); + } + + @Test + void testClose() { + assertDoesNotThrow(() -> client.close()); + } + + @Test + void testReconnect() { + assertDoesNotThrow(() -> client.reconnect()); + } + + @Test + void testInit() throws Exception { + // init() est appelé @PostConstruct — l'appeler via réflexion pour couvrir la méthode + Method initMethod = KeycloakAdminClientImpl.class.getDeclaredMethod("init"); + initMethod.setAccessible(true); + assertDoesNotThrow(() -> initMethod.invoke(client)); + } + + @Test + void testGetAllRealms_Success() throws Exception { + // Démarrer un serveur HTTP local qui retourne une liste de realms JSON + String realmsJson = "[{\"realm\":\"master\",\"id\":\"1\"},{\"realm\":\"lions\",\"id\":\"2\"}]"; + localPort = startLocalServer("/admin/realms", realmsJson, 200); + setField("serverUrl", "http://localhost:" + localPort); + + when(mockKeycloak.tokenManager()).thenReturn(mockTokenManager); + when(mockTokenManager.getAccessTokenString()).thenReturn("fake-token"); + + List realms = client.getAllRealms(); + + assertNotNull(realms); + assertEquals(2, realms.size()); + assertTrue(realms.contains("master")); + assertTrue(realms.contains("lions")); + } + + @Test + void testGetAllRealms_NonOkResponse() throws Exception { + localPort = startLocalServer("/admin/realms", "Forbidden", 403); + setField("serverUrl", "http://localhost:" + localPort); + + when(mockKeycloak.tokenManager()).thenReturn(mockTokenManager); + when(mockTokenManager.getAccessTokenString()).thenReturn("fake-token"); + + assertThrows(RuntimeException.class, () -> client.getAllRealms()); + } + + @Test + void testGetRealmClients_Success() throws Exception { + String clientsJson = "[{\"clientId\":\"admin-cli\",\"id\":\"1\"},{\"clientId\":\"account\",\"id\":\"2\"}]"; + localPort = startLocalServer("/admin/realms/master/clients", clientsJson, 200); + setField("serverUrl", "http://localhost:" + localPort); + + when(mockKeycloak.tokenManager()).thenReturn(mockTokenManager); + when(mockTokenManager.getAccessTokenString()).thenReturn("fake-token"); + + List clients = client.getRealmClients("master"); + + assertNotNull(clients); + assertEquals(2, clients.size()); + assertTrue(clients.contains("admin-cli")); + assertTrue(clients.contains("account")); + } + + @Test + void testGetRealmClients_NonOkResponse() throws Exception { + localPort = startLocalServer("/admin/realms/bad/clients", "Not Found", 404); + setField("serverUrl", "http://localhost:" + localPort); + + when(mockKeycloak.tokenManager()).thenReturn(mockTokenManager); + when(mockTokenManager.getAccessTokenString()).thenReturn("fake-token"); + + assertThrows(RuntimeException.class, () -> client.getRealmClients("bad")); + } + + @Test + void testGetRealmClients_TokenError() { + when(mockKeycloak.tokenManager()).thenThrow(new RuntimeException("Token error")); + + assertThrows(RuntimeException.class, () -> client.getRealmClients("master")); + } +} diff --git a/src/test/java/dev/lions/user/manager/client/KeycloakAdminClientImplTest.java b/src/test/java/dev/lions/user/manager/client/KeycloakAdminClientImplTest.java index 63f2299..5eb419d 100644 --- a/src/test/java/dev/lions/user/manager/client/KeycloakAdminClientImplTest.java +++ b/src/test/java/dev/lions/user/manager/client/KeycloakAdminClientImplTest.java @@ -1,158 +1,158 @@ -package dev.lions.user.manager.client; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.keycloak.admin.client.Keycloak; -import org.keycloak.admin.client.resource.RealmResource; -import org.keycloak.admin.client.resource.RolesResource; -import org.keycloak.admin.client.resource.ServerInfoResource; -import org.keycloak.admin.client.resource.UsersResource; -import org.keycloak.admin.client.token.TokenManager; -import org.keycloak.representations.info.ServerInfoRepresentation; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import jakarta.ws.rs.NotFoundException; -import java.lang.reflect.Field; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class KeycloakAdminClientImplTest { - - @InjectMocks - KeycloakAdminClientImpl client; - - @Mock - Keycloak keycloak; - - @Mock - RealmResource realmResource; - - @Mock - UsersResource usersResource; - - @Mock - RolesResource rolesResource; - - @Mock - ServerInfoResource serverInfoResource; - - @Mock - TokenManager tokenManager; - - private void setField(Object target, String fieldName, Object value) throws Exception { - Field field = target.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - field.set(target, value); - } - - @BeforeEach - void setUp() throws Exception { - setField(client, "serverUrl", "http://localhost:8180"); - setField(client, "adminRealm", "master"); - setField(client, "adminClientId", "admin-cli"); - setField(client, "adminUsername", "admin"); - } - - @Test - void testGetInstance() { - Keycloak result = client.getInstance(); - assertNotNull(result); - assertEquals(keycloak, result); - } - - @Test - void testGetRealm() { - when(keycloak.realm("test-realm")).thenReturn(realmResource); - - RealmResource result = client.getRealm("test-realm"); - - assertNotNull(result); - assertEquals(realmResource, result); - } - - @Test - void testGetRealmThrowsException() { - when(keycloak.realm("bad-realm")).thenThrow(new RuntimeException("Connection failed")); - - assertThrows(RuntimeException.class, () -> client.getRealm("bad-realm")); - } - - @Test - void testGetUsers() { - when(keycloak.realm("test-realm")).thenReturn(realmResource); - when(realmResource.users()).thenReturn(usersResource); - - UsersResource result = client.getUsers("test-realm"); - - assertNotNull(result); - assertEquals(usersResource, result); - } - - @Test - void testGetRoles() { - when(keycloak.realm("test-realm")).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - - RolesResource result = client.getRoles("test-realm"); - - assertNotNull(result); - assertEquals(rolesResource, result); - } - - @Test - void testIsConnected_true() { - when(keycloak.tokenManager()).thenReturn(tokenManager); - when(tokenManager.getAccessTokenString()).thenReturn("fake-token"); - - assertTrue(client.isConnected()); - } - - @Test - void testIsConnected_false_exception() { - when(keycloak.tokenManager()).thenThrow(new RuntimeException("Connection refused")); - - assertFalse(client.isConnected()); - } - - @Test - void testRealmExists_true() { - when(keycloak.realm("test-realm")).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.list()).thenReturn(java.util.Collections.emptyList()); - - assertTrue(client.realmExists("test-realm")); - } - - @Test - void testRealmExists_notFound() { - when(keycloak.realm("missing-realm")).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.list()).thenThrow(new NotFoundException("Realm not found")); - - assertFalse(client.realmExists("missing-realm")); - } - - @Test - void testRealmExists_otherException() { - when(keycloak.realm("problem-realm")).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.list()).thenThrow(new RuntimeException("Some other error")); - - assertTrue(client.realmExists("problem-realm")); - } - - @Test - void testClose() { - assertDoesNotThrow(() -> client.close()); - } - - @Test - void testReconnect() { - assertDoesNotThrow(() -> client.reconnect()); - } -} +package dev.lions.user.manager.client; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.RolesResource; +import org.keycloak.admin.client.resource.ServerInfoResource; +import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.admin.client.token.TokenManager; +import org.keycloak.representations.info.ServerInfoRepresentation; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import jakarta.ws.rs.NotFoundException; +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class KeycloakAdminClientImplTest { + + @InjectMocks + KeycloakAdminClientImpl client; + + @Mock + Keycloak keycloak; + + @Mock + RealmResource realmResource; + + @Mock + UsersResource usersResource; + + @Mock + RolesResource rolesResource; + + @Mock + ServerInfoResource serverInfoResource; + + @Mock + TokenManager tokenManager; + + private void setField(Object target, String fieldName, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } + + @BeforeEach + void setUp() throws Exception { + setField(client, "serverUrl", "http://localhost:8180"); + setField(client, "adminRealm", "master"); + setField(client, "adminClientId", "admin-cli"); + setField(client, "adminUsername", "admin"); + } + + @Test + void testGetInstance() { + Keycloak result = client.getInstance(); + assertNotNull(result); + assertEquals(keycloak, result); + } + + @Test + void testGetRealm() { + when(keycloak.realm("test-realm")).thenReturn(realmResource); + + RealmResource result = client.getRealm("test-realm"); + + assertNotNull(result); + assertEquals(realmResource, result); + } + + @Test + void testGetRealmThrowsException() { + when(keycloak.realm("bad-realm")).thenThrow(new RuntimeException("Connection failed")); + + assertThrows(RuntimeException.class, () -> client.getRealm("bad-realm")); + } + + @Test + void testGetUsers() { + when(keycloak.realm("test-realm")).thenReturn(realmResource); + when(realmResource.users()).thenReturn(usersResource); + + UsersResource result = client.getUsers("test-realm"); + + assertNotNull(result); + assertEquals(usersResource, result); + } + + @Test + void testGetRoles() { + when(keycloak.realm("test-realm")).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + + RolesResource result = client.getRoles("test-realm"); + + assertNotNull(result); + assertEquals(rolesResource, result); + } + + @Test + void testIsConnected_true() { + when(keycloak.tokenManager()).thenReturn(tokenManager); + when(tokenManager.getAccessTokenString()).thenReturn("fake-token"); + + assertTrue(client.isConnected()); + } + + @Test + void testIsConnected_false_exception() { + when(keycloak.tokenManager()).thenThrow(new RuntimeException("Connection refused")); + + assertFalse(client.isConnected()); + } + + @Test + void testRealmExists_true() { + when(keycloak.realm("test-realm")).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.list()).thenReturn(java.util.Collections.emptyList()); + + assertTrue(client.realmExists("test-realm")); + } + + @Test + void testRealmExists_notFound() { + when(keycloak.realm("missing-realm")).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.list()).thenThrow(new NotFoundException("Realm not found")); + + assertFalse(client.realmExists("missing-realm")); + } + + @Test + void testRealmExists_otherException() { + when(keycloak.realm("problem-realm")).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.list()).thenThrow(new RuntimeException("Some other error")); + + assertTrue(client.realmExists("problem-realm")); + } + + @Test + void testClose() { + assertDoesNotThrow(() -> client.close()); + } + + @Test + void testReconnect() { + assertDoesNotThrow(() -> client.reconnect()); + } +} diff --git a/src/test/java/dev/lions/user/manager/config/KeycloakTestUserConfigCompleteTest.java b/src/test/java/dev/lions/user/manager/config/KeycloakTestUserConfigCompleteTest.java index 9702f5e..d2e5ee5 100644 --- a/src/test/java/dev/lions/user/manager/config/KeycloakTestUserConfigCompleteTest.java +++ b/src/test/java/dev/lions/user/manager/config/KeycloakTestUserConfigCompleteTest.java @@ -1,422 +1,422 @@ -package dev.lions.user.manager.config; - -import io.quarkus.runtime.StartupEvent; -import jakarta.ws.rs.NotFoundException; -import jakarta.ws.rs.core.Response; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.keycloak.admin.client.Keycloak; -import org.keycloak.admin.client.KeycloakBuilder; -import org.keycloak.admin.client.resource.*; -import org.keycloak.representations.idm.*; -import org.mockito.MockedStatic; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.lang.reflect.Method; -import java.net.URI; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -/** - * Tests complets pour KeycloakTestUserConfig pour atteindre 100% de couverture - * Teste toutes les méthodes privées via la méthode publique onStart - */ -@ExtendWith(MockitoExtension.class) -class KeycloakTestUserConfigCompleteTest { - - private KeycloakTestUserConfig config; - private Keycloak adminClient; - private RealmsResource realmsResource; - private RealmResource realmResource; - private RolesResource rolesResource; - private RoleResource roleResource; - private UsersResource usersResource; - private UserResource userResource; - private ClientsResource clientsResource; - private ClientResource clientResource; - private ClientScopesResource clientScopesResource; - private ClientScopeResource clientScopeResource; - - @BeforeEach - void setUp() throws Exception { - config = new KeycloakTestUserConfig(); - - // Injecter les valeurs via reflection - setField("profile", "dev"); - setField("keycloakServerUrl", "http://localhost:8080"); - setField("adminRealm", "master"); - setField("adminUsername", "admin"); - setField("adminPassword", "admin"); - setField("authorizedRealms", "lions-user-manager"); - - // Mocks pour Keycloak - adminClient = mock(Keycloak.class); - realmsResource = mock(RealmsResource.class); - realmResource = mock(RealmResource.class); - rolesResource = mock(RolesResource.class); - roleResource = mock(RoleResource.class); - usersResource = mock(UsersResource.class); - userResource = mock(UserResource.class); - clientsResource = mock(ClientsResource.class); - clientResource = mock(ClientResource.class); - clientScopesResource = mock(ClientScopesResource.class); - clientScopeResource = mock(ClientScopeResource.class); - } - - private void setField(String fieldName, Object value) throws Exception { - java.lang.reflect.Field field = KeycloakTestUserConfig.class.getDeclaredField(fieldName); - field.setAccessible(true); - field.set(config, value); - } - - @Test - void testOnStart_DevMode() { - // Le code est désactivé, donc onStart devrait juste logger et retourner - StartupEvent event = mock(StartupEvent.class); - - // Ne devrait pas lancer d'exception - assertDoesNotThrow(() -> config.onStart(event)); - } - - @Test - void testEnsureRealmExists_RealmExists() throws Exception { - when(adminClient.realms()).thenReturn(realmsResource); - when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); - when(realmResource.toRepresentation()).thenReturn(new RealmRepresentation()); - - Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureRealmExists", Keycloak.class); - method.setAccessible(true); - - assertDoesNotThrow(() -> method.invoke(config, adminClient)); - verify(realmResource).toRepresentation(); - } - - @Test - void testEnsureRealmExists_RealmNotFound() throws Exception { - when(adminClient.realms()).thenReturn(realmsResource); - when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); - when(realmResource.toRepresentation()).thenThrow(new NotFoundException()); - doNothing().when(realmsResource).create(any(RealmRepresentation.class)); - - Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureRealmExists", Keycloak.class); - method.setAccessible(true); - - assertDoesNotThrow(() -> method.invoke(config, adminClient)); - verify(realmResource).toRepresentation(); - } - - @Test - void testEnsureRolesExist_AllRolesExist() throws Exception { - when(adminClient.realms()).thenReturn(realmsResource); - when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.get(anyString())).thenReturn(roleResource); - when(roleResource.toRepresentation()).thenReturn(new RoleRepresentation()); - - Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureRolesExist", Keycloak.class); - method.setAccessible(true); - - assertDoesNotThrow(() -> method.invoke(config, adminClient)); - } - - @Test - void testEnsureRolesExist_RoleNotFound() throws Exception { - when(adminClient.realms()).thenReturn(realmsResource); - when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.get(anyString())).thenReturn(roleResource); - when(roleResource.toRepresentation()) - .thenThrow(new NotFoundException()) - .thenReturn(new RoleRepresentation()); - doNothing().when(rolesResource).create(any(RoleRepresentation.class)); - - Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureRolesExist", Keycloak.class); - method.setAccessible(true); - - assertDoesNotThrow(() -> method.invoke(config, adminClient)); - } - - @Test - void testEnsureTestUserExists_UserExists() throws Exception { - when(adminClient.realms()).thenReturn(realmsResource); - when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); - when(realmResource.users()).thenReturn(usersResource); - - UserRepresentation existingUser = new UserRepresentation(); - existingUser.setId("user-id-123"); - when(usersResource.search("test-user", true)).thenReturn(List.of(existingUser)); - - Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureTestUserExists", Keycloak.class); - method.setAccessible(true); - - String userId = (String) method.invoke(config, adminClient); - assertEquals("user-id-123", userId); - } - - @Test - void testEnsureTestUserExists_UserNotFound() throws Exception { - when(adminClient.realms()).thenReturn(realmsResource); - when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); - when(realmResource.users()).thenReturn(usersResource); - when(usersResource.search("test-user", true)).thenReturn(Collections.emptyList()); - - Response response = mock(Response.class); - when(response.getStatusInfo()).thenReturn(Response.Status.CREATED); - when(response.getLocation()).thenReturn(URI.create("http://localhost/users/user-id-123")); - when(usersResource.create(any(UserRepresentation.class))).thenReturn(response); - when(usersResource.get("user-id-123")).thenReturn(userResource); - - CredentialRepresentation credential = new CredentialRepresentation(); - doNothing().when(userResource).resetPassword(any(CredentialRepresentation.class)); - - Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureTestUserExists", Keycloak.class); - method.setAccessible(true); - - String userId = (String) method.invoke(config, adminClient); - assertEquals("user-id-123", userId); - verify(usersResource).create(any(UserRepresentation.class)); - verify(userResource).resetPassword(any(CredentialRepresentation.class)); - } - - @Test - void testAssignRolesToUser() throws Exception { - when(adminClient.realms()).thenReturn(realmsResource); - when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); - when(realmResource.users()).thenReturn(usersResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.get(anyString())).thenReturn(roleResource); - - RoleRepresentation role = new RoleRepresentation(); - role.setName("admin"); - when(roleResource.toRepresentation()).thenReturn(role); - - when(usersResource.get("user-id")).thenReturn(userResource); - RoleMappingResource roleMappingResource = mock(RoleMappingResource.class); - RoleScopeResource roleScopeResource = mock(RoleScopeResource.class); - when(userResource.roles()).thenReturn(roleMappingResource); - when(roleMappingResource.realmLevel()).thenReturn(roleScopeResource); - doNothing().when(roleScopeResource).add(anyList()); - - Method method = KeycloakTestUserConfig.class.getDeclaredMethod("assignRolesToUser", Keycloak.class, String.class); - method.setAccessible(true); - - assertDoesNotThrow(() -> method.invoke(config, adminClient, "user-id")); - verify(roleScopeResource).add(anyList()); - } - - @Test - void testEnsureClientAndMapper_ClientExists() throws Exception { - when(adminClient.realms()).thenReturn(realmsResource); - when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); - when(realmResource.clients()).thenReturn(clientsResource); - - ClientRepresentation existingClient = new ClientRepresentation(); - existingClient.setId("client-id-123"); - when(clientsResource.findByClientId("lions-user-manager-client")).thenReturn(List.of(existingClient)); - - when(realmResource.clientScopes()).thenReturn(clientScopesResource); - ClientScopeRepresentation rolesScope = new ClientScopeRepresentation(); - rolesScope.setId("scope-id"); - rolesScope.setName("roles"); - when(clientScopesResource.findAll()).thenReturn(List.of(rolesScope)); - - when(clientsResource.get("client-id-123")).thenReturn(clientResource); - when(clientResource.getDefaultClientScopes()).thenReturn(List.of(rolesScope)); - - Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureClientAndMapper", Keycloak.class); - method.setAccessible(true); - - assertDoesNotThrow(() -> method.invoke(config, adminClient)); - } - - @Test - void testEnsureClientAndMapper_ClientNotFound() throws Exception { - when(adminClient.realms()).thenReturn(realmsResource); - when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); - when(realmResource.clients()).thenReturn(clientsResource); - when(clientsResource.findByClientId("lions-user-manager-client")).thenReturn(Collections.emptyList()); - - Response response = mock(Response.class); - when(response.getStatusInfo()).thenReturn(Response.Status.CREATED); - when(response.getLocation()).thenReturn(URI.create("http://localhost/clients/client-id-123")); - when(clientsResource.create(any(ClientRepresentation.class))).thenReturn(response); - - when(realmResource.clientScopes()).thenReturn(clientScopesResource); - ClientScopeRepresentation rolesScope = new ClientScopeRepresentation(); - rolesScope.setId("scope-id"); - rolesScope.setName("roles"); - when(clientScopesResource.findAll()).thenReturn(List.of(rolesScope)); - - when(clientsResource.get("client-id-123")).thenReturn(clientResource); - when(clientResource.getDefaultClientScopes()).thenReturn(List.of(rolesScope)); - - Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureClientAndMapper", Keycloak.class); - method.setAccessible(true); - - assertDoesNotThrow(() -> method.invoke(config, adminClient)); - verify(clientsResource).create(any(ClientRepresentation.class)); - } - - @Test - void testEnsureClientAndMapper_ClientNotFound_NoRolesScope() throws Exception { - when(adminClient.realms()).thenReturn(realmsResource); - when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); - when(realmResource.clients()).thenReturn(clientsResource); - when(clientsResource.findByClientId("lions-user-manager-client")).thenReturn(Collections.emptyList()); - - Response response = mock(Response.class); - when(response.getStatusInfo()).thenReturn(Response.Status.CREATED); - when(response.getLocation()).thenReturn(URI.create("http://localhost/clients/client-id-123")); - when(clientsResource.create(any(ClientRepresentation.class))).thenReturn(response); - - when(realmResource.clientScopes()).thenReturn(clientScopesResource); - when(clientScopesResource.findAll()).thenReturn(Collections.emptyList()); - - Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureClientAndMapper", Keycloak.class); - method.setAccessible(true); - - assertDoesNotThrow(() -> method.invoke(config, adminClient)); - } - - @Test - void testEnsureClientAndMapper_ClientNotFound_RolesScopeAlreadyPresent() throws Exception { - when(adminClient.realms()).thenReturn(realmsResource); - when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); - when(realmResource.clients()).thenReturn(clientsResource); - when(clientsResource.findByClientId("lions-user-manager-client")).thenReturn(Collections.emptyList()); - - Response response = mock(Response.class); - when(response.getStatusInfo()).thenReturn(Response.Status.CREATED); - when(response.getLocation()).thenReturn(URI.create("http://localhost/clients/client-id-123")); - when(clientsResource.create(any(ClientRepresentation.class))).thenReturn(response); - - when(realmResource.clientScopes()).thenReturn(clientScopesResource); - ClientScopeRepresentation rolesScope = new ClientScopeRepresentation(); - rolesScope.setId("scope-id"); - rolesScope.setName("roles"); - when(clientScopesResource.findAll()).thenReturn(List.of(rolesScope)); - - when(clientsResource.get("client-id-123")).thenReturn(clientResource); - // Simuler que le scope "roles" est déjà présent - when(clientResource.getDefaultClientScopes()).thenReturn(List.of(rolesScope)); - - Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureClientAndMapper", Keycloak.class); - method.setAccessible(true); - - assertDoesNotThrow(() -> method.invoke(config, adminClient)); - } - - @Test - void testEnsureClientAndMapper_Exception() throws Exception { - when(adminClient.realms()).thenReturn(realmsResource); - when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); - when(realmResource.clients()).thenReturn(clientsResource); - when(clientsResource.findByClientId("lions-user-manager-client")).thenThrow(new RuntimeException("Connection error")); - - Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureClientAndMapper", Keycloak.class); - method.setAccessible(true); - - assertDoesNotThrow(() -> method.invoke(config, adminClient)); - } - - @Test - void testGetCreatedId_Success() throws Exception { - Response response = mock(Response.class); - when(response.getStatusInfo()).thenReturn(Response.Status.CREATED); - when(response.getLocation()).thenReturn(URI.create("http://localhost/users/user-id-123")); - - Method method = KeycloakTestUserConfig.class.getDeclaredMethod("getCreatedId", Response.class); - method.setAccessible(true); - - String id = (String) method.invoke(config, response); - assertEquals("user-id-123", id); - } - - @Test - void testGetCreatedId_Error() throws Exception { - Response response = mock(Response.class); - // Utiliser Response.Status.BAD_REQUEST directement - when(response.getStatusInfo()).thenReturn(Response.Status.BAD_REQUEST); - - Method method = KeycloakTestUserConfig.class.getDeclaredMethod("getCreatedId", Response.class); - method.setAccessible(true); - - Exception exception = assertThrows(Exception.class, () -> method.invoke(config, response)); - assertTrue(exception.getCause() instanceof RuntimeException); - assertTrue(exception.getCause().getMessage().contains("Erreur lors de la création")); - } - - /** - * Couvre L250-252 : le scope "roles" n'est pas encore dans les scopes du client, - * donc addDefaultClientScope est appelé. - */ - @Test - void testEnsureClientAndMapper_ClientExists_RolesScopeNotYetPresent() throws Exception { - when(adminClient.realms()).thenReturn(realmsResource); - when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); - when(realmResource.clients()).thenReturn(clientsResource); - - ClientRepresentation existingClient = new ClientRepresentation(); - existingClient.setId("client-id-456"); - when(clientsResource.findByClientId("lions-user-manager-client")).thenReturn(List.of(existingClient)); - - // Le scope "roles" existe dans le realm - when(realmResource.clientScopes()).thenReturn(clientScopesResource); - ClientScopeRepresentation rolesScope = new ClientScopeRepresentation(); - rolesScope.setId("scope-roles-id"); - rolesScope.setName("roles"); - when(clientScopesResource.findAll()).thenReturn(List.of(rolesScope)); - - // Mais il N'EST PAS encore dans les scopes par défaut du client (liste vide) - when(clientsResource.get("client-id-456")).thenReturn(clientResource); - when(clientResource.getDefaultClientScopes()).thenReturn(Collections.emptyList()); - doNothing().when(clientResource).addDefaultClientScope(anyString()); - - Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureClientAndMapper", Keycloak.class); - method.setAccessible(true); - - assertDoesNotThrow(() -> method.invoke(config, adminClient)); - // Vérifie que addDefaultClientScope a été appelé avec l'ID du scope - verify(clientResource).addDefaultClientScope("scope-roles-id"); - } - - /** - * Couvre L259-260 : addDefaultClientScope lève une exception → catch warn. - */ - @Test - void testEnsureClientAndMapper_ClientExists_AddScopeThrowsException() throws Exception { - when(adminClient.realms()).thenReturn(realmsResource); - when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); - when(realmResource.clients()).thenReturn(clientsResource); - - ClientRepresentation existingClient = new ClientRepresentation(); - existingClient.setId("client-id-789"); - when(clientsResource.findByClientId("lions-user-manager-client")).thenReturn(List.of(existingClient)); - - when(realmResource.clientScopes()).thenReturn(clientScopesResource); - ClientScopeRepresentation rolesScope = new ClientScopeRepresentation(); - rolesScope.setId("scope-roles-id-2"); - rolesScope.setName("roles"); - when(clientScopesResource.findAll()).thenReturn(List.of(rolesScope)); - - // Scope pas encore présent - when(clientsResource.get("client-id-789")).thenReturn(clientResource); - when(clientResource.getDefaultClientScopes()).thenReturn(Collections.emptyList()); - // addDefaultClientScope lève une exception → couvre le catch L259-260 - doThrow(new RuntimeException("Forbidden")).when(clientResource).addDefaultClientScope(anyString()); - - Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureClientAndMapper", Keycloak.class); - method.setAccessible(true); - - // La méthode ne doit pas propager l'exception (catch + warn) - assertDoesNotThrow(() -> method.invoke(config, adminClient)); - } -} - +package dev.lions.user.manager.config; + +import io.quarkus.runtime.StartupEvent; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.KeycloakBuilder; +import org.keycloak.admin.client.resource.*; +import org.keycloak.representations.idm.*; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * Tests complets pour KeycloakTestUserConfig pour atteindre 100% de couverture + * Teste toutes les méthodes privées via la méthode publique onStart + */ +@ExtendWith(MockitoExtension.class) +class KeycloakTestUserConfigCompleteTest { + + private KeycloakTestUserConfig config; + private Keycloak adminClient; + private RealmsResource realmsResource; + private RealmResource realmResource; + private RolesResource rolesResource; + private RoleResource roleResource; + private UsersResource usersResource; + private UserResource userResource; + private ClientsResource clientsResource; + private ClientResource clientResource; + private ClientScopesResource clientScopesResource; + private ClientScopeResource clientScopeResource; + + @BeforeEach + void setUp() throws Exception { + config = new KeycloakTestUserConfig(); + + // Injecter les valeurs via reflection + setField("profile", "dev"); + setField("keycloakServerUrl", "http://localhost:8080"); + setField("adminRealm", "master"); + setField("adminUsername", "admin"); + setField("adminPassword", "admin"); + setField("authorizedRealms", "lions-user-manager"); + + // Mocks pour Keycloak + adminClient = mock(Keycloak.class); + realmsResource = mock(RealmsResource.class); + realmResource = mock(RealmResource.class); + rolesResource = mock(RolesResource.class); + roleResource = mock(RoleResource.class); + usersResource = mock(UsersResource.class); + userResource = mock(UserResource.class); + clientsResource = mock(ClientsResource.class); + clientResource = mock(ClientResource.class); + clientScopesResource = mock(ClientScopesResource.class); + clientScopeResource = mock(ClientScopeResource.class); + } + + private void setField(String fieldName, Object value) throws Exception { + java.lang.reflect.Field field = KeycloakTestUserConfig.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(config, value); + } + + @Test + void testOnStart_DevMode() { + // Le code est désactivé, donc onStart devrait juste logger et retourner + StartupEvent event = mock(StartupEvent.class); + + // Ne devrait pas lancer d'exception + assertDoesNotThrow(() -> config.onStart(event)); + } + + @Test + void testEnsureRealmExists_RealmExists() throws Exception { + when(adminClient.realms()).thenReturn(realmsResource); + when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); + when(realmResource.toRepresentation()).thenReturn(new RealmRepresentation()); + + Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureRealmExists", Keycloak.class); + method.setAccessible(true); + + assertDoesNotThrow(() -> method.invoke(config, adminClient)); + verify(realmResource).toRepresentation(); + } + + @Test + void testEnsureRealmExists_RealmNotFound() throws Exception { + when(adminClient.realms()).thenReturn(realmsResource); + when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); + when(realmResource.toRepresentation()).thenThrow(new NotFoundException()); + doNothing().when(realmsResource).create(any(RealmRepresentation.class)); + + Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureRealmExists", Keycloak.class); + method.setAccessible(true); + + assertDoesNotThrow(() -> method.invoke(config, adminClient)); + verify(realmResource).toRepresentation(); + } + + @Test + void testEnsureRolesExist_AllRolesExist() throws Exception { + when(adminClient.realms()).thenReturn(realmsResource); + when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.get(anyString())).thenReturn(roleResource); + when(roleResource.toRepresentation()).thenReturn(new RoleRepresentation()); + + Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureRolesExist", Keycloak.class); + method.setAccessible(true); + + assertDoesNotThrow(() -> method.invoke(config, adminClient)); + } + + @Test + void testEnsureRolesExist_RoleNotFound() throws Exception { + when(adminClient.realms()).thenReturn(realmsResource); + when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.get(anyString())).thenReturn(roleResource); + when(roleResource.toRepresentation()) + .thenThrow(new NotFoundException()) + .thenReturn(new RoleRepresentation()); + doNothing().when(rolesResource).create(any(RoleRepresentation.class)); + + Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureRolesExist", Keycloak.class); + method.setAccessible(true); + + assertDoesNotThrow(() -> method.invoke(config, adminClient)); + } + + @Test + void testEnsureTestUserExists_UserExists() throws Exception { + when(adminClient.realms()).thenReturn(realmsResource); + when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); + when(realmResource.users()).thenReturn(usersResource); + + UserRepresentation existingUser = new UserRepresentation(); + existingUser.setId("user-id-123"); + when(usersResource.search("test-user", true)).thenReturn(List.of(existingUser)); + + Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureTestUserExists", Keycloak.class); + method.setAccessible(true); + + String userId = (String) method.invoke(config, adminClient); + assertEquals("user-id-123", userId); + } + + @Test + void testEnsureTestUserExists_UserNotFound() throws Exception { + when(adminClient.realms()).thenReturn(realmsResource); + when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); + when(realmResource.users()).thenReturn(usersResource); + when(usersResource.search("test-user", true)).thenReturn(Collections.emptyList()); + + Response response = mock(Response.class); + when(response.getStatusInfo()).thenReturn(Response.Status.CREATED); + when(response.getLocation()).thenReturn(URI.create("http://localhost/users/user-id-123")); + when(usersResource.create(any(UserRepresentation.class))).thenReturn(response); + when(usersResource.get("user-id-123")).thenReturn(userResource); + + CredentialRepresentation credential = new CredentialRepresentation(); + doNothing().when(userResource).resetPassword(any(CredentialRepresentation.class)); + + Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureTestUserExists", Keycloak.class); + method.setAccessible(true); + + String userId = (String) method.invoke(config, adminClient); + assertEquals("user-id-123", userId); + verify(usersResource).create(any(UserRepresentation.class)); + verify(userResource).resetPassword(any(CredentialRepresentation.class)); + } + + @Test + void testAssignRolesToUser() throws Exception { + when(adminClient.realms()).thenReturn(realmsResource); + when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); + when(realmResource.users()).thenReturn(usersResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.get(anyString())).thenReturn(roleResource); + + RoleRepresentation role = new RoleRepresentation(); + role.setName("admin"); + when(roleResource.toRepresentation()).thenReturn(role); + + when(usersResource.get("user-id")).thenReturn(userResource); + RoleMappingResource roleMappingResource = mock(RoleMappingResource.class); + RoleScopeResource roleScopeResource = mock(RoleScopeResource.class); + when(userResource.roles()).thenReturn(roleMappingResource); + when(roleMappingResource.realmLevel()).thenReturn(roleScopeResource); + doNothing().when(roleScopeResource).add(anyList()); + + Method method = KeycloakTestUserConfig.class.getDeclaredMethod("assignRolesToUser", Keycloak.class, String.class); + method.setAccessible(true); + + assertDoesNotThrow(() -> method.invoke(config, adminClient, "user-id")); + verify(roleScopeResource).add(anyList()); + } + + @Test + void testEnsureClientAndMapper_ClientExists() throws Exception { + when(adminClient.realms()).thenReturn(realmsResource); + when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); + when(realmResource.clients()).thenReturn(clientsResource); + + ClientRepresentation existingClient = new ClientRepresentation(); + existingClient.setId("client-id-123"); + when(clientsResource.findByClientId("lions-user-manager-client")).thenReturn(List.of(existingClient)); + + when(realmResource.clientScopes()).thenReturn(clientScopesResource); + ClientScopeRepresentation rolesScope = new ClientScopeRepresentation(); + rolesScope.setId("scope-id"); + rolesScope.setName("roles"); + when(clientScopesResource.findAll()).thenReturn(List.of(rolesScope)); + + when(clientsResource.get("client-id-123")).thenReturn(clientResource); + when(clientResource.getDefaultClientScopes()).thenReturn(List.of(rolesScope)); + + Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureClientAndMapper", Keycloak.class); + method.setAccessible(true); + + assertDoesNotThrow(() -> method.invoke(config, adminClient)); + } + + @Test + void testEnsureClientAndMapper_ClientNotFound() throws Exception { + when(adminClient.realms()).thenReturn(realmsResource); + when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); + when(realmResource.clients()).thenReturn(clientsResource); + when(clientsResource.findByClientId("lions-user-manager-client")).thenReturn(Collections.emptyList()); + + Response response = mock(Response.class); + when(response.getStatusInfo()).thenReturn(Response.Status.CREATED); + when(response.getLocation()).thenReturn(URI.create("http://localhost/clients/client-id-123")); + when(clientsResource.create(any(ClientRepresentation.class))).thenReturn(response); + + when(realmResource.clientScopes()).thenReturn(clientScopesResource); + ClientScopeRepresentation rolesScope = new ClientScopeRepresentation(); + rolesScope.setId("scope-id"); + rolesScope.setName("roles"); + when(clientScopesResource.findAll()).thenReturn(List.of(rolesScope)); + + when(clientsResource.get("client-id-123")).thenReturn(clientResource); + when(clientResource.getDefaultClientScopes()).thenReturn(List.of(rolesScope)); + + Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureClientAndMapper", Keycloak.class); + method.setAccessible(true); + + assertDoesNotThrow(() -> method.invoke(config, adminClient)); + verify(clientsResource).create(any(ClientRepresentation.class)); + } + + @Test + void testEnsureClientAndMapper_ClientNotFound_NoRolesScope() throws Exception { + when(adminClient.realms()).thenReturn(realmsResource); + when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); + when(realmResource.clients()).thenReturn(clientsResource); + when(clientsResource.findByClientId("lions-user-manager-client")).thenReturn(Collections.emptyList()); + + Response response = mock(Response.class); + when(response.getStatusInfo()).thenReturn(Response.Status.CREATED); + when(response.getLocation()).thenReturn(URI.create("http://localhost/clients/client-id-123")); + when(clientsResource.create(any(ClientRepresentation.class))).thenReturn(response); + + when(realmResource.clientScopes()).thenReturn(clientScopesResource); + when(clientScopesResource.findAll()).thenReturn(Collections.emptyList()); + + Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureClientAndMapper", Keycloak.class); + method.setAccessible(true); + + assertDoesNotThrow(() -> method.invoke(config, adminClient)); + } + + @Test + void testEnsureClientAndMapper_ClientNotFound_RolesScopeAlreadyPresent() throws Exception { + when(adminClient.realms()).thenReturn(realmsResource); + when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); + when(realmResource.clients()).thenReturn(clientsResource); + when(clientsResource.findByClientId("lions-user-manager-client")).thenReturn(Collections.emptyList()); + + Response response = mock(Response.class); + when(response.getStatusInfo()).thenReturn(Response.Status.CREATED); + when(response.getLocation()).thenReturn(URI.create("http://localhost/clients/client-id-123")); + when(clientsResource.create(any(ClientRepresentation.class))).thenReturn(response); + + when(realmResource.clientScopes()).thenReturn(clientScopesResource); + ClientScopeRepresentation rolesScope = new ClientScopeRepresentation(); + rolesScope.setId("scope-id"); + rolesScope.setName("roles"); + when(clientScopesResource.findAll()).thenReturn(List.of(rolesScope)); + + when(clientsResource.get("client-id-123")).thenReturn(clientResource); + // Simuler que le scope "roles" est déjà présent + when(clientResource.getDefaultClientScopes()).thenReturn(List.of(rolesScope)); + + Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureClientAndMapper", Keycloak.class); + method.setAccessible(true); + + assertDoesNotThrow(() -> method.invoke(config, adminClient)); + } + + @Test + void testEnsureClientAndMapper_Exception() throws Exception { + when(adminClient.realms()).thenReturn(realmsResource); + when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); + when(realmResource.clients()).thenReturn(clientsResource); + when(clientsResource.findByClientId("lions-user-manager-client")).thenThrow(new RuntimeException("Connection error")); + + Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureClientAndMapper", Keycloak.class); + method.setAccessible(true); + + assertDoesNotThrow(() -> method.invoke(config, adminClient)); + } + + @Test + void testGetCreatedId_Success() throws Exception { + Response response = mock(Response.class); + when(response.getStatusInfo()).thenReturn(Response.Status.CREATED); + when(response.getLocation()).thenReturn(URI.create("http://localhost/users/user-id-123")); + + Method method = KeycloakTestUserConfig.class.getDeclaredMethod("getCreatedId", Response.class); + method.setAccessible(true); + + String id = (String) method.invoke(config, response); + assertEquals("user-id-123", id); + } + + @Test + void testGetCreatedId_Error() throws Exception { + Response response = mock(Response.class); + // Utiliser Response.Status.BAD_REQUEST directement + when(response.getStatusInfo()).thenReturn(Response.Status.BAD_REQUEST); + + Method method = KeycloakTestUserConfig.class.getDeclaredMethod("getCreatedId", Response.class); + method.setAccessible(true); + + Exception exception = assertThrows(Exception.class, () -> method.invoke(config, response)); + assertTrue(exception.getCause() instanceof RuntimeException); + assertTrue(exception.getCause().getMessage().contains("Erreur lors de la création")); + } + + /** + * Couvre L250-252 : le scope "roles" n'est pas encore dans les scopes du client, + * donc addDefaultClientScope est appelé. + */ + @Test + void testEnsureClientAndMapper_ClientExists_RolesScopeNotYetPresent() throws Exception { + when(adminClient.realms()).thenReturn(realmsResource); + when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); + when(realmResource.clients()).thenReturn(clientsResource); + + ClientRepresentation existingClient = new ClientRepresentation(); + existingClient.setId("client-id-456"); + when(clientsResource.findByClientId("lions-user-manager-client")).thenReturn(List.of(existingClient)); + + // Le scope "roles" existe dans le realm + when(realmResource.clientScopes()).thenReturn(clientScopesResource); + ClientScopeRepresentation rolesScope = new ClientScopeRepresentation(); + rolesScope.setId("scope-roles-id"); + rolesScope.setName("roles"); + when(clientScopesResource.findAll()).thenReturn(List.of(rolesScope)); + + // Mais il N'EST PAS encore dans les scopes par défaut du client (liste vide) + when(clientsResource.get("client-id-456")).thenReturn(clientResource); + when(clientResource.getDefaultClientScopes()).thenReturn(Collections.emptyList()); + doNothing().when(clientResource).addDefaultClientScope(anyString()); + + Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureClientAndMapper", Keycloak.class); + method.setAccessible(true); + + assertDoesNotThrow(() -> method.invoke(config, adminClient)); + // Vérifie que addDefaultClientScope a été appelé avec l'ID du scope + verify(clientResource).addDefaultClientScope("scope-roles-id"); + } + + /** + * Couvre L259-260 : addDefaultClientScope lève une exception → catch warn. + */ + @Test + void testEnsureClientAndMapper_ClientExists_AddScopeThrowsException() throws Exception { + when(adminClient.realms()).thenReturn(realmsResource); + when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource); + when(realmResource.clients()).thenReturn(clientsResource); + + ClientRepresentation existingClient = new ClientRepresentation(); + existingClient.setId("client-id-789"); + when(clientsResource.findByClientId("lions-user-manager-client")).thenReturn(List.of(existingClient)); + + when(realmResource.clientScopes()).thenReturn(clientScopesResource); + ClientScopeRepresentation rolesScope = new ClientScopeRepresentation(); + rolesScope.setId("scope-roles-id-2"); + rolesScope.setName("roles"); + when(clientScopesResource.findAll()).thenReturn(List.of(rolesScope)); + + // Scope pas encore présent + when(clientsResource.get("client-id-789")).thenReturn(clientResource); + when(clientResource.getDefaultClientScopes()).thenReturn(Collections.emptyList()); + // addDefaultClientScope lève une exception → couvre le catch L259-260 + doThrow(new RuntimeException("Forbidden")).when(clientResource).addDefaultClientScope(anyString()); + + Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureClientAndMapper", Keycloak.class); + method.setAccessible(true); + + // La méthode ne doit pas propager l'exception (catch + warn) + assertDoesNotThrow(() -> method.invoke(config, adminClient)); + } +} + diff --git a/src/test/java/dev/lions/user/manager/config/KeycloakTestUserConfigTest.java b/src/test/java/dev/lions/user/manager/config/KeycloakTestUserConfigTest.java index ebe362e..16b2369 100644 --- a/src/test/java/dev/lions/user/manager/config/KeycloakTestUserConfigTest.java +++ b/src/test/java/dev/lions/user/manager/config/KeycloakTestUserConfigTest.java @@ -1,65 +1,65 @@ -package dev.lions.user.manager.config; - -import io.quarkus.runtime.StartupEvent; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.lang.reflect.Field; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Tests unitaires pour KeycloakTestUserConfig - */ -@ExtendWith(MockitoExtension.class) -class KeycloakTestUserConfigTest { - - @InjectMocks - private KeycloakTestUserConfig config; - - @BeforeEach - void setUp() throws Exception { - // Injecter les propriétés via reflection - setField("profile", "dev"); - setField("keycloakServerUrl", "http://localhost:8180"); - setField("adminRealm", "master"); - setField("adminUsername", "admin"); - setField("adminPassword", "admin"); - setField("authorizedRealms", "lions-user-manager"); - } - - private void setField(String fieldName, Object value) throws Exception { - Field field = KeycloakTestUserConfig.class.getDeclaredField(fieldName); - field.setAccessible(true); - field.set(config, value); - } - - @Test - void testOnStart_DevMode() { - // La méthode onStart est désactivée, elle devrait juste logger et retourner - assertDoesNotThrow(() -> { - config.onStart(new StartupEvent()); - }); - } - - @Test - void testOnStart_ProdMode() throws Exception { - setField("profile", "prod"); - - // En prod, la méthode devrait retourner immédiatement - assertDoesNotThrow(() -> { - config.onStart(new StartupEvent()); - }); - } - - @Test - void testConstants() { - // Vérifier que les constantes sont définies - assertNotNull(KeycloakTestUserConfig.class); - // Les constantes sont privées, on ne peut pas les tester directement - // mais on peut vérifier que la classe se charge correctement - } -} +package dev.lions.user.manager.config; + +import io.quarkus.runtime.StartupEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests unitaires pour KeycloakTestUserConfig + */ +@ExtendWith(MockitoExtension.class) +class KeycloakTestUserConfigTest { + + @InjectMocks + private KeycloakTestUserConfig config; + + @BeforeEach + void setUp() throws Exception { + // Injecter les propriétés via reflection + setField("profile", "dev"); + setField("keycloakServerUrl", "http://localhost:8180"); + setField("adminRealm", "master"); + setField("adminUsername", "admin"); + setField("adminPassword", "admin"); + setField("authorizedRealms", "lions-user-manager"); + } + + private void setField(String fieldName, Object value) throws Exception { + Field field = KeycloakTestUserConfig.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(config, value); + } + + @Test + void testOnStart_DevMode() { + // La méthode onStart est désactivée, elle devrait juste logger et retourner + assertDoesNotThrow(() -> { + config.onStart(new StartupEvent()); + }); + } + + @Test + void testOnStart_ProdMode() throws Exception { + setField("profile", "prod"); + + // En prod, la méthode devrait retourner immédiatement + assertDoesNotThrow(() -> { + config.onStart(new StartupEvent()); + }); + } + + @Test + void testConstants() { + // Vérifier que les constantes sont définies + assertNotNull(KeycloakTestUserConfig.class); + // Les constantes sont privées, on ne peut pas les tester directement + // mais on peut vérifier que la classe se charge correctement + } +} diff --git a/src/test/java/dev/lions/user/manager/mapper/RoleMapperAdditionalTest.java b/src/test/java/dev/lions/user/manager/mapper/RoleMapperAdditionalTest.java index 6c61985..ecb45e1 100644 --- a/src/test/java/dev/lions/user/manager/mapper/RoleMapperAdditionalTest.java +++ b/src/test/java/dev/lions/user/manager/mapper/RoleMapperAdditionalTest.java @@ -1,91 +1,91 @@ -package dev.lions.user.manager.mapper; - -import dev.lions.user.manager.dto.role.RoleDTO; -import dev.lions.user.manager.enums.role.TypeRole; -import org.junit.jupiter.api.Test; -import org.keycloak.representations.idm.RoleRepresentation; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Tests supplémentaires pour RoleMapper pour améliorer la couverture - */ -class RoleMapperAdditionalTest { - - @Test - void testToDTO_WithAllFields() { - RoleRepresentation roleRep = new RoleRepresentation(); - roleRep.setId("role-123"); - roleRep.setName("admin"); - roleRep.setDescription("Administrator role"); - roleRep.setComposite(false); - - RoleDTO dto = RoleMapper.toDTO(roleRep, "test-realm", TypeRole.REALM_ROLE); - - assertNotNull(dto); - assertEquals("role-123", dto.getId()); - assertEquals("admin", dto.getName()); - assertEquals("Administrator role", dto.getDescription()); - assertEquals(TypeRole.REALM_ROLE, dto.getTypeRole()); - assertFalse(dto.getComposite()); - } - - @Test - void testToDTO_WithNullFields() { - RoleRepresentation roleRep = new RoleRepresentation(); - roleRep.setId("role-123"); - roleRep.setName("user"); - - RoleDTO dto = RoleMapper.toDTO(roleRep, "test-realm", TypeRole.REALM_ROLE); - - assertNotNull(dto); - assertEquals("role-123", dto.getId()); - assertEquals("user", dto.getName()); - assertNull(dto.getDescription()); - } - - @Test - void testToDTOList_Empty() { - List dtos = RoleMapper.toDTOList(Collections.emptyList(), "test-realm", TypeRole.REALM_ROLE); - - assertNotNull(dtos); - assertTrue(dtos.isEmpty()); - } - - @Test - void testToDTOList_WithRoles() { - RoleRepresentation role1 = new RoleRepresentation(); - role1.setId("role-1"); - role1.setName("admin"); - RoleRepresentation role2 = new RoleRepresentation(); - role2.setId("role-2"); - role2.setName("user"); - - List dtos = RoleMapper.toDTOList(Arrays.asList(role1, role2), "test-realm", TypeRole.REALM_ROLE); - - assertNotNull(dtos); - assertEquals(2, dtos.size()); - assertEquals("admin", dtos.get(0).getName()); - assertEquals("user", dtos.get(1).getName()); - } - - // La méthode toKeycloak() n'existe pas dans RoleMapper - // Ces tests sont supprimés car la méthode n'est pas disponible - - /** - * Couvre RoleMapper.java L13 : le constructeur par défaut implicite de la classe utilitaire. - * JaCoCo (counter=LINE) marque la déclaration de classe comme non couverte si aucune instance - * n'est jamais créée. - */ - @Test - void testRoleMapperInstantiation() { - // Instantie la classe pour couvrir le constructeur par défaut (L13) - RoleMapper mapper = new RoleMapper(); - assertNotNull(mapper); - } -} - +package dev.lions.user.manager.mapper; + +import dev.lions.user.manager.dto.role.RoleDTO; +import dev.lions.user.manager.enums.role.TypeRole; +import org.junit.jupiter.api.Test; +import org.keycloak.representations.idm.RoleRepresentation; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests supplémentaires pour RoleMapper pour améliorer la couverture + */ +class RoleMapperAdditionalTest { + + @Test + void testToDTO_WithAllFields() { + RoleRepresentation roleRep = new RoleRepresentation(); + roleRep.setId("role-123"); + roleRep.setName("admin"); + roleRep.setDescription("Administrator role"); + roleRep.setComposite(false); + + RoleDTO dto = RoleMapper.toDTO(roleRep, "test-realm", TypeRole.REALM_ROLE); + + assertNotNull(dto); + assertEquals("role-123", dto.getId()); + assertEquals("admin", dto.getName()); + assertEquals("Administrator role", dto.getDescription()); + assertEquals(TypeRole.REALM_ROLE, dto.getTypeRole()); + assertFalse(dto.getComposite()); + } + + @Test + void testToDTO_WithNullFields() { + RoleRepresentation roleRep = new RoleRepresentation(); + roleRep.setId("role-123"); + roleRep.setName("user"); + + RoleDTO dto = RoleMapper.toDTO(roleRep, "test-realm", TypeRole.REALM_ROLE); + + assertNotNull(dto); + assertEquals("role-123", dto.getId()); + assertEquals("user", dto.getName()); + assertNull(dto.getDescription()); + } + + @Test + void testToDTOList_Empty() { + List dtos = RoleMapper.toDTOList(Collections.emptyList(), "test-realm", TypeRole.REALM_ROLE); + + assertNotNull(dtos); + assertTrue(dtos.isEmpty()); + } + + @Test + void testToDTOList_WithRoles() { + RoleRepresentation role1 = new RoleRepresentation(); + role1.setId("role-1"); + role1.setName("admin"); + RoleRepresentation role2 = new RoleRepresentation(); + role2.setId("role-2"); + role2.setName("user"); + + List dtos = RoleMapper.toDTOList(Arrays.asList(role1, role2), "test-realm", TypeRole.REALM_ROLE); + + assertNotNull(dtos); + assertEquals(2, dtos.size()); + assertEquals("admin", dtos.get(0).getName()); + assertEquals("user", dtos.get(1).getName()); + } + + // La méthode toKeycloak() n'existe pas dans RoleMapper + // Ces tests sont supprimés car la méthode n'est pas disponible + + /** + * Couvre RoleMapper.java L13 : le constructeur par défaut implicite de la classe utilitaire. + * JaCoCo (counter=LINE) marque la déclaration de classe comme non couverte si aucune instance + * n'est jamais créée. + */ + @Test + void testRoleMapperInstantiation() { + // Instantie la classe pour couvrir le constructeur par défaut (L13) + RoleMapper mapper = new RoleMapper(); + assertNotNull(mapper); + } +} + diff --git a/src/test/java/dev/lions/user/manager/mapper/RoleMapperTest.java b/src/test/java/dev/lions/user/manager/mapper/RoleMapperTest.java index b993856..44a6fc7 100644 --- a/src/test/java/dev/lions/user/manager/mapper/RoleMapperTest.java +++ b/src/test/java/dev/lions/user/manager/mapper/RoleMapperTest.java @@ -1,91 +1,91 @@ -package dev.lions.user.manager.mapper; - -import dev.lions.user.manager.dto.role.RoleDTO; -import dev.lions.user.manager.enums.role.TypeRole; -import org.junit.jupiter.api.Test; -import org.keycloak.representations.idm.RoleRepresentation; -import java.util.Collections; -import java.util.List; -import static org.junit.jupiter.api.Assertions.*; - -class RoleMapperTest { - - @Test - void testToDTO() { - RoleRepresentation rep = new RoleRepresentation(); - rep.setId("1"); - rep.setName("role"); - rep.setDescription("desc"); - rep.setComposite(true); - - RoleDTO dto = RoleMapper.toDTO(rep, "realm", TypeRole.REALM_ROLE); - - assertNotNull(dto); - assertEquals("1", dto.getId()); - assertEquals("role", dto.getName()); - assertEquals("desc", dto.getDescription()); - assertEquals(TypeRole.REALM_ROLE, dto.getTypeRole()); - assertEquals("realm", dto.getRealmName()); - assertTrue(dto.getComposite()); - - assertNull(RoleMapper.toDTO(null, "realm", TypeRole.REALM_ROLE)); - } - - @Test - void testToRepresentation() { - RoleDTO dto = RoleDTO.builder() - .id("1") - .name("role") - .description("desc") - .composite(true) - .compositeRoles(Collections.singletonList("subrole")) - .typeRole(TypeRole.CLIENT_ROLE) // Should setClientRole(true) - .build(); - - RoleRepresentation rep = RoleMapper.toRepresentation(dto); - - assertNotNull(rep); - assertEquals("1", rep.getId()); - assertEquals("role", rep.getName()); - assertEquals("desc", rep.getDescription()); - assertTrue(rep.isComposite()); - assertTrue(rep.getClientRole()); - - assertNull(RoleMapper.toRepresentation(null)); - } - - // New test case to cover full branch logic - @Test - void testToRepresentationRealmRole() { - RoleDTO dto = RoleDTO.builder() - .typeRole(TypeRole.REALM_ROLE) - .build(); - RoleRepresentation rep = RoleMapper.toRepresentation(dto); - assertFalse(rep.getClientRole()); - } - - @Test - void testToDTOList() { - RoleRepresentation rep = new RoleRepresentation(); - rep.setName("role"); - List reps = Collections.singletonList(rep); - - List dtos = RoleMapper.toDTOList(reps, "realm", TypeRole.REALM_ROLE); - assertEquals(1, dtos.size()); - assertEquals("role", dtos.get(0).getName()); - - assertTrue(RoleMapper.toDTOList(null, "realm", TypeRole.REALM_ROLE).isEmpty()); - } - - @Test - void testToRepresentationList() { - RoleDTO dto = RoleDTO.builder().name("role").typeRole(TypeRole.REALM_ROLE).build(); - List dtos = Collections.singletonList(dto); - - List reps = RoleMapper.toRepresentationList(dtos); - assertEquals(1, reps.size()); - assertEquals("role", reps.get(0).getName()); - - assertTrue(RoleMapper.toRepresentationList(null).isEmpty()); - } -} +package dev.lions.user.manager.mapper; + +import dev.lions.user.manager.dto.role.RoleDTO; +import dev.lions.user.manager.enums.role.TypeRole; +import org.junit.jupiter.api.Test; +import org.keycloak.representations.idm.RoleRepresentation; +import java.util.Collections; +import java.util.List; +import static org.junit.jupiter.api.Assertions.*; + +class RoleMapperTest { + + @Test + void testToDTO() { + RoleRepresentation rep = new RoleRepresentation(); + rep.setId("1"); + rep.setName("role"); + rep.setDescription("desc"); + rep.setComposite(true); + + RoleDTO dto = RoleMapper.toDTO(rep, "realm", TypeRole.REALM_ROLE); + + assertNotNull(dto); + assertEquals("1", dto.getId()); + assertEquals("role", dto.getName()); + assertEquals("desc", dto.getDescription()); + assertEquals(TypeRole.REALM_ROLE, dto.getTypeRole()); + assertEquals("realm", dto.getRealmName()); + assertTrue(dto.getComposite()); + + assertNull(RoleMapper.toDTO(null, "realm", TypeRole.REALM_ROLE)); + } + + @Test + void testToRepresentation() { + RoleDTO dto = RoleDTO.builder() + .id("1") + .name("role") + .description("desc") + .composite(true) + .compositeRoles(Collections.singletonList("subrole")) + .typeRole(TypeRole.CLIENT_ROLE) // Should setClientRole(true) + .build(); + + RoleRepresentation rep = RoleMapper.toRepresentation(dto); + + assertNotNull(rep); + assertEquals("1", rep.getId()); + assertEquals("role", rep.getName()); + assertEquals("desc", rep.getDescription()); + assertTrue(rep.isComposite()); + assertTrue(rep.getClientRole()); + + assertNull(RoleMapper.toRepresentation(null)); + } + + // New test case to cover full branch logic + @Test + void testToRepresentationRealmRole() { + RoleDTO dto = RoleDTO.builder() + .typeRole(TypeRole.REALM_ROLE) + .build(); + RoleRepresentation rep = RoleMapper.toRepresentation(dto); + assertFalse(rep.getClientRole()); + } + + @Test + void testToDTOList() { + RoleRepresentation rep = new RoleRepresentation(); + rep.setName("role"); + List reps = Collections.singletonList(rep); + + List dtos = RoleMapper.toDTOList(reps, "realm", TypeRole.REALM_ROLE); + assertEquals(1, dtos.size()); + assertEquals("role", dtos.get(0).getName()); + + assertTrue(RoleMapper.toDTOList(null, "realm", TypeRole.REALM_ROLE).isEmpty()); + } + + @Test + void testToRepresentationList() { + RoleDTO dto = RoleDTO.builder().name("role").typeRole(TypeRole.REALM_ROLE).build(); + List dtos = Collections.singletonList(dto); + + List reps = RoleMapper.toRepresentationList(dtos); + assertEquals(1, reps.size()); + assertEquals("role", reps.get(0).getName()); + + assertTrue(RoleMapper.toRepresentationList(null).isEmpty()); + } +} diff --git a/src/test/java/dev/lions/user/manager/mapper/UserMapperTest.java b/src/test/java/dev/lions/user/manager/mapper/UserMapperTest.java index d895949..ea06b1d 100644 --- a/src/test/java/dev/lions/user/manager/mapper/UserMapperTest.java +++ b/src/test/java/dev/lions/user/manager/mapper/UserMapperTest.java @@ -1,150 +1,150 @@ -package dev.lions.user.manager.mapper; - -import dev.lions.user.manager.dto.user.UserDTO; -import dev.lions.user.manager.enums.user.StatutUser; -import org.junit.jupiter.api.Test; -import org.keycloak.representations.idm.UserRepresentation; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; - -class UserMapperTest { - - @Test - void testToDTO() { - UserRepresentation rep = new UserRepresentation(); - rep.setId("1"); - rep.setUsername("jdoe"); - rep.setEmail("jdoe@example.com"); - rep.setEmailVerified(true); - rep.setFirstName("John"); - rep.setLastName("Doe"); - rep.setEnabled(true); - rep.setCreatedTimestamp(System.currentTimeMillis()); - - Map> attrs = Map.of( - "phone_number", List.of("123"), - "organization", List.of("Lions"), - "department", List.of("IT"), - "job_title", List.of("Dev"), - "country", List.of("CI"), - "city", List.of("Abidjan"), - "locale", List.of("fr"), - "timezone", List.of("UTC")); - rep.setAttributes(attrs); - - UserDTO dto = UserMapper.toDTO(rep, "realm"); - - assertNotNull(dto); - assertEquals("1", dto.getId()); - assertEquals("jdoe", dto.getUsername()); - assertEquals("jdoe@example.com", dto.getEmail()); - assertTrue(dto.getEmailVerified()); - assertEquals("John", dto.getPrenom()); - assertEquals("Doe", dto.getNom()); - assertEquals(StatutUser.ACTIF, dto.getStatut()); - assertEquals("realm", dto.getRealmName()); - assertEquals("123", dto.getTelephone()); - assertEquals("Lions", dto.getOrganisation()); - assertEquals("IT", dto.getDepartement()); - assertEquals("Dev", dto.getFonction()); - assertEquals("CI", dto.getPays()); - assertEquals("Abidjan", dto.getVille()); - assertEquals("fr", dto.getLangue()); - assertEquals("UTC", dto.getTimezone()); - assertNotNull(dto.getDateCreation()); - - assertNull(UserMapper.toDTO(null, "realm")); - } - - @Test - void testToDTOWithNullAttributes() { - UserRepresentation rep = new UserRepresentation(); - rep.setId("1"); - rep.setEnabled(true); - UserDTO dto = UserMapper.toDTO(rep, "realm"); - assertNotNull(dto); - assertNull(dto.getTelephone()); // Attribute missing - } - - @Test - void testToDTOWithEmptyAttributes() { - UserRepresentation rep = new UserRepresentation(); - rep.setEnabled(true); - rep.setAttributes(Collections.emptyMap()); - UserDTO dto = UserMapper.toDTO(rep, "realm"); - assertNotNull(dto); - assertNull(dto.getTelephone()); - } - - @Test - void testToRepresentation() { - UserDTO dto = UserDTO.builder() - .id("1") - .username("jdoe") - .email("jdoe@example.com") - .emailVerified(true) - .prenom("John") - .nom("Doe") - .enabled(true) - .telephone("123") - .organisation("Lions") - .departement("IT") - .fonction("Dev") - .pays("CI") - .ville("Abidjan") - .langue("fr") - .timezone("UTC") - .requiredActions(Collections.singletonList("UPDATE_PASSWORD")) - .attributes(Map.of("custom", List.of("value"))) - .build(); - - UserRepresentation rep = UserMapper.toRepresentation(dto); - - assertNotNull(rep); - assertEquals("1", rep.getId()); - assertEquals("jdoe", rep.getUsername()); - assertEquals("jdoe@example.com", rep.getEmail()); - assertTrue(rep.isEmailVerified()); - assertEquals("John", rep.getFirstName()); - assertEquals("Doe", rep.getLastName()); - assertTrue(rep.isEnabled()); - - assertNotNull(rep.getAttributes()); - assertEquals(List.of("123"), rep.getAttributes().get("phone_number")); - assertEquals(List.of("Lions"), rep.getAttributes().get("organization")); - assertEquals(List.of("value"), rep.getAttributes().get("custom")); - - assertNotNull(rep.getRequiredActions()); - assertTrue(rep.getRequiredActions().contains("UPDATE_PASSWORD")); - - assertNull(UserMapper.toRepresentation(null)); - } - - @Test - void testToRepresentationValuesNull() { - UserDTO dto = UserDTO.builder().username("jdoe").enabled(null).build(); - UserRepresentation rep = UserMapper.toRepresentation(dto); - assertTrue(rep.isEnabled()); // Defaults to true in mapper - } - - @Test - void testToDTOList() { - UserRepresentation rep = new UserRepresentation(); - rep.setEnabled(true); - List reps = Collections.singletonList(rep); - List dtos = UserMapper.toDTOList(reps, "realm"); - assertEquals(1, dtos.size()); - - assertTrue(UserMapper.toDTOList(null, "realm").isEmpty()); - } - - @Test - void testPrivateConstructor() throws Exception { - java.lang.reflect.Constructor constructor = UserMapper.class.getDeclaredConstructor(); - assertTrue(java.lang.reflect.Modifier.isPrivate(constructor.getModifiers())); - constructor.setAccessible(true); - assertNotNull(constructor.newInstance()); - } -} +package dev.lions.user.manager.mapper; + +import dev.lions.user.manager.dto.user.UserDTO; +import dev.lions.user.manager.enums.user.StatutUser; +import org.junit.jupiter.api.Test; +import org.keycloak.representations.idm.UserRepresentation; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import static org.junit.jupiter.api.Assertions.*; + +class UserMapperTest { + + @Test + void testToDTO() { + UserRepresentation rep = new UserRepresentation(); + rep.setId("1"); + rep.setUsername("jdoe"); + rep.setEmail("jdoe@example.com"); + rep.setEmailVerified(true); + rep.setFirstName("John"); + rep.setLastName("Doe"); + rep.setEnabled(true); + rep.setCreatedTimestamp(System.currentTimeMillis()); + + Map> attrs = Map.of( + "phone_number", List.of("123"), + "organization", List.of("Lions"), + "department", List.of("IT"), + "job_title", List.of("Dev"), + "country", List.of("CI"), + "city", List.of("Abidjan"), + "locale", List.of("fr"), + "timezone", List.of("UTC")); + rep.setAttributes(attrs); + + UserDTO dto = UserMapper.toDTO(rep, "realm"); + + assertNotNull(dto); + assertEquals("1", dto.getId()); + assertEquals("jdoe", dto.getUsername()); + assertEquals("jdoe@example.com", dto.getEmail()); + assertTrue(dto.getEmailVerified()); + assertEquals("John", dto.getPrenom()); + assertEquals("Doe", dto.getNom()); + assertEquals(StatutUser.ACTIF, dto.getStatut()); + assertEquals("realm", dto.getRealmName()); + assertEquals("123", dto.getTelephone()); + assertEquals("Lions", dto.getOrganisation()); + assertEquals("IT", dto.getDepartement()); + assertEquals("Dev", dto.getFonction()); + assertEquals("CI", dto.getPays()); + assertEquals("Abidjan", dto.getVille()); + assertEquals("fr", dto.getLangue()); + assertEquals("UTC", dto.getTimezone()); + assertNotNull(dto.getDateCreation()); + + assertNull(UserMapper.toDTO(null, "realm")); + } + + @Test + void testToDTOWithNullAttributes() { + UserRepresentation rep = new UserRepresentation(); + rep.setId("1"); + rep.setEnabled(true); + UserDTO dto = UserMapper.toDTO(rep, "realm"); + assertNotNull(dto); + assertNull(dto.getTelephone()); // Attribute missing + } + + @Test + void testToDTOWithEmptyAttributes() { + UserRepresentation rep = new UserRepresentation(); + rep.setEnabled(true); + rep.setAttributes(Collections.emptyMap()); + UserDTO dto = UserMapper.toDTO(rep, "realm"); + assertNotNull(dto); + assertNull(dto.getTelephone()); + } + + @Test + void testToRepresentation() { + UserDTO dto = UserDTO.builder() + .id("1") + .username("jdoe") + .email("jdoe@example.com") + .emailVerified(true) + .prenom("John") + .nom("Doe") + .enabled(true) + .telephone("123") + .organisation("Lions") + .departement("IT") + .fonction("Dev") + .pays("CI") + .ville("Abidjan") + .langue("fr") + .timezone("UTC") + .requiredActions(Collections.singletonList("UPDATE_PASSWORD")) + .attributes(Map.of("custom", List.of("value"))) + .build(); + + UserRepresentation rep = UserMapper.toRepresentation(dto); + + assertNotNull(rep); + assertEquals("1", rep.getId()); + assertEquals("jdoe", rep.getUsername()); + assertEquals("jdoe@example.com", rep.getEmail()); + assertTrue(rep.isEmailVerified()); + assertEquals("John", rep.getFirstName()); + assertEquals("Doe", rep.getLastName()); + assertTrue(rep.isEnabled()); + + assertNotNull(rep.getAttributes()); + assertEquals(List.of("123"), rep.getAttributes().get("phone_number")); + assertEquals(List.of("Lions"), rep.getAttributes().get("organization")); + assertEquals(List.of("value"), rep.getAttributes().get("custom")); + + assertNotNull(rep.getRequiredActions()); + assertTrue(rep.getRequiredActions().contains("UPDATE_PASSWORD")); + + assertNull(UserMapper.toRepresentation(null)); + } + + @Test + void testToRepresentationValuesNull() { + UserDTO dto = UserDTO.builder().username("jdoe").enabled(null).build(); + UserRepresentation rep = UserMapper.toRepresentation(dto); + assertTrue(rep.isEnabled()); // Defaults to true in mapper + } + + @Test + void testToDTOList() { + UserRepresentation rep = new UserRepresentation(); + rep.setEnabled(true); + List reps = Collections.singletonList(rep); + List dtos = UserMapper.toDTOList(reps, "realm"); + assertEquals(1, dtos.size()); + + assertTrue(UserMapper.toDTOList(null, "realm").isEmpty()); + } + + @Test + void testPrivateConstructor() throws Exception { + java.lang.reflect.Constructor constructor = UserMapper.class.getDeclaredConstructor(); + assertTrue(java.lang.reflect.Modifier.isPrivate(constructor.getModifiers())); + constructor.setAccessible(true); + assertNotNull(constructor.newInstance()); + } +} diff --git a/src/test/java/dev/lions/user/manager/resource/AuditResourceTest.java b/src/test/java/dev/lions/user/manager/resource/AuditResourceTest.java index 30749f4..e9e0051 100644 --- a/src/test/java/dev/lions/user/manager/resource/AuditResourceTest.java +++ b/src/test/java/dev/lions/user/manager/resource/AuditResourceTest.java @@ -1,233 +1,233 @@ -package dev.lions.user.manager.resource; - -import dev.lions.user.manager.dto.audit.AuditLogDTO; -import dev.lions.user.manager.dto.common.CountDTO; -import dev.lions.user.manager.enums.audit.TypeActionAudit; -import dev.lions.user.manager.service.AuditService; -import jakarta.ws.rs.core.Response; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class AuditResourceTest { - - @Mock - AuditService auditService; - - @InjectMocks - AuditResource auditResource; - - @org.junit.jupiter.api.BeforeEach - void setUp() { - auditResource.defaultRealm = "master"; - } - - @Test - void testSearchLogs() { - List logs = Collections.singletonList( - AuditLogDTO.builder().acteurUsername("admin").typeAction(TypeActionAudit.USER_CREATE).build()); - when(auditService.findByActeur(eq("admin"), any(), any(), eq(0), eq(50))).thenReturn(logs); - - List result = auditResource.searchLogs("admin", null, null, null, null, null, 0, 50); - - assertEquals(logs, result); - } - - @Test - void testGetLogsByActor() { - List logs = Collections.singletonList( - AuditLogDTO.builder().acteurUsername("admin").build()); - when(auditService.findByActeur(eq("admin"), isNull(), isNull(), eq(0), eq(100))).thenReturn(logs); - - List result = auditResource.getLogsByActor("admin", 100); - - assertEquals(logs, result); - } - - @Test - void testGetLogsByResource() { - List logs = Collections.emptyList(); - when(auditService.findByRessource(eq("USER"), eq("1"), any(), any(), eq(0), eq(100))) - .thenReturn(logs); - - List result = auditResource.getLogsByResource("USER", "1", 100); - - assertEquals(logs, result); - } - - @Test - void testGetLogsByAction() { - List logs = Collections.emptyList(); - when(auditService.findByTypeAction(eq(TypeActionAudit.USER_CREATE), eq("master"), any(), any(), eq(0), eq(100))) - .thenReturn(logs); - - List result = auditResource.getLogsByAction(TypeActionAudit.USER_CREATE, null, null, 100); - - assertEquals(logs, result); - } - - @Test - void testGetActionStatistics() { - Map stats = Map.of(TypeActionAudit.USER_CREATE, 10L); - when(auditService.countByActionType(eq("master"), any(), any())).thenReturn(stats); - - Map result = auditResource.getActionStatistics(null, null); - - assertEquals(stats, result); - } - - @Test - void testGetUserActivityStatistics() { - Map stats = Map.of("admin", 100L); - when(auditService.countByActeur(eq("master"), any(), any())).thenReturn(stats); - - Map result = auditResource.getUserActivityStatistics(null, null); - - assertEquals(stats, result); - } - - @Test - void testGetFailureCount() { - Map successVsFailure = Map.of("failure", 5L, "success", 100L); - when(auditService.countSuccessVsFailure(eq("master"), any(), any())).thenReturn(successVsFailure); - - CountDTO result = auditResource.getFailureCount(null, null); - - assertEquals(5L, result.getCount()); - } - - @Test - void testGetSuccessCount() { - Map successVsFailure = Map.of("failure", 5L, "success", 100L); - when(auditService.countSuccessVsFailure(eq("master"), any(), any())).thenReturn(successVsFailure); - - CountDTO result = auditResource.getSuccessCount(null, null); - - assertEquals(100L, result.getCount()); - } - - @Test - void testExportLogsToCSV() { - when(auditService.exportToCSV(eq("master"), any(), any())).thenReturn("csv,data"); - - Response response = auditResource.exportLogsToCSV(null, null); - - assertEquals(200, response.getStatus()); - assertEquals("csv,data", response.getEntity()); - } - - @Test - void testPurgeOldLogs() { - when(auditService.purgeOldLogs(any())).thenReturn(0L); - - auditResource.purgeOldLogs(90); - - verify(auditService).purgeOldLogs(any()); - } - - @Test - void testSearchLogs_NullActeur_UsesRealm() { - List logs = Collections.singletonList( - AuditLogDTO.builder().acteurUsername("admin").typeAction(TypeActionAudit.USER_CREATE).build()); - when(auditService.findByRealm(eq("master"), any(), any(), eq(0), eq(50))).thenReturn(logs); - - List result = auditResource.searchLogs(null, null, null, null, null, null, 0, 50); - - assertEquals(logs, result); - verify(auditService).findByRealm(eq("master"), any(), any(), eq(0), eq(50)); - } - - @Test - void testSearchLogs_BlankActeur_UsesRealm() { - List logs = Collections.emptyList(); - when(auditService.findByRealm(eq("master"), any(), any(), eq(0), eq(20))).thenReturn(logs); - - List result = auditResource.searchLogs(" ", null, null, null, null, null, 0, 20); - - assertEquals(logs, result); - verify(auditService).findByRealm(eq("master"), any(), any(), eq(0), eq(20)); - } - - @Test - void testSearchLogs_WithPostFilter_TypeAction() { - AuditLogDTO matchLog = AuditLogDTO.builder() - .acteurUsername("admin") - .typeAction(TypeActionAudit.USER_CREATE) - .build(); - AuditLogDTO otherLog = AuditLogDTO.builder() - .acteurUsername("admin") - .typeAction(TypeActionAudit.USER_DELETE) - .build(); - when(auditService.findByActeur(eq("admin"), any(), any(), eq(0), eq(50))) - .thenReturn(List.of(matchLog, otherLog)); - - List result = auditResource.searchLogs("admin", null, null, - TypeActionAudit.USER_CREATE, null, null, 0, 50); - - assertEquals(1, result.size()); - assertEquals(TypeActionAudit.USER_CREATE, result.get(0).getTypeAction()); - } - - @Test - void testSearchLogs_WithPostFilter_RessourceType() { - AuditLogDTO matchLog = AuditLogDTO.builder() - .acteurUsername("admin") - .typeAction(TypeActionAudit.USER_CREATE) - .ressourceType("USER") - .build(); - AuditLogDTO otherLog = AuditLogDTO.builder() - .acteurUsername("admin") - .typeAction(TypeActionAudit.USER_CREATE) - .ressourceType("ROLE") - .build(); - when(auditService.findByActeur(eq("admin"), any(), any(), eq(0), eq(50))) - .thenReturn(List.of(matchLog, otherLog)); - - List result = auditResource.searchLogs("admin", null, null, - null, "USER", null, 0, 50); - - assertEquals(1, result.size()); - assertEquals("USER", result.get(0).getRessourceType()); - } - - @Test - void testSearchLogs_WithPostFilter_Succes() { - AuditLogDTO successLog = AuditLogDTO.builder() - .acteurUsername("admin") - .typeAction(TypeActionAudit.USER_CREATE) - .success(true) - .build(); - AuditLogDTO failLog = AuditLogDTO.builder() - .acteurUsername("admin") - .typeAction(TypeActionAudit.USER_CREATE) - .success(false) - .build(); - when(auditService.findByActeur(eq("admin"), any(), any(), eq(0), eq(50))) - .thenReturn(List.of(successLog, failLog)); - - List result = auditResource.searchLogs("admin", null, null, - null, null, Boolean.TRUE, 0, 50); - - assertEquals(1, result.size()); - assertTrue(result.get(0).isSuccessful()); - } - - @Test - void testExportLogsToCSV_Exception() { - when(auditService.exportToCSV(eq("master"), any(), any())) - .thenThrow(new RuntimeException("Export failed")); - - assertThrows(RuntimeException.class, () -> auditResource.exportLogsToCSV(null, null)); - } -} +package dev.lions.user.manager.resource; + +import dev.lions.user.manager.dto.audit.AuditLogDTO; +import dev.lions.user.manager.dto.common.CountDTO; +import dev.lions.user.manager.enums.audit.TypeActionAudit; +import dev.lions.user.manager.service.AuditService; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class AuditResourceTest { + + @Mock + AuditService auditService; + + @InjectMocks + AuditResource auditResource; + + @org.junit.jupiter.api.BeforeEach + void setUp() { + auditResource.defaultRealm = "master"; + } + + @Test + void testSearchLogs() { + List logs = Collections.singletonList( + AuditLogDTO.builder().acteurUsername("admin").typeAction(TypeActionAudit.USER_CREATE).build()); + when(auditService.findByActeur(eq("admin"), any(), any(), eq(0), eq(50))).thenReturn(logs); + + List result = auditResource.searchLogs("admin", null, null, null, null, null, 0, 50); + + assertEquals(logs, result); + } + + @Test + void testGetLogsByActor() { + List logs = Collections.singletonList( + AuditLogDTO.builder().acteurUsername("admin").build()); + when(auditService.findByActeur(eq("admin"), isNull(), isNull(), eq(0), eq(100))).thenReturn(logs); + + List result = auditResource.getLogsByActor("admin", 100); + + assertEquals(logs, result); + } + + @Test + void testGetLogsByResource() { + List logs = Collections.emptyList(); + when(auditService.findByRessource(eq("USER"), eq("1"), any(), any(), eq(0), eq(100))) + .thenReturn(logs); + + List result = auditResource.getLogsByResource("USER", "1", 100); + + assertEquals(logs, result); + } + + @Test + void testGetLogsByAction() { + List logs = Collections.emptyList(); + when(auditService.findByTypeAction(eq(TypeActionAudit.USER_CREATE), eq("master"), any(), any(), eq(0), eq(100))) + .thenReturn(logs); + + List result = auditResource.getLogsByAction(TypeActionAudit.USER_CREATE, null, null, 100); + + assertEquals(logs, result); + } + + @Test + void testGetActionStatistics() { + Map stats = Map.of(TypeActionAudit.USER_CREATE, 10L); + when(auditService.countByActionType(eq("master"), any(), any())).thenReturn(stats); + + Map result = auditResource.getActionStatistics(null, null); + + assertEquals(stats, result); + } + + @Test + void testGetUserActivityStatistics() { + Map stats = Map.of("admin", 100L); + when(auditService.countByActeur(eq("master"), any(), any())).thenReturn(stats); + + Map result = auditResource.getUserActivityStatistics(null, null); + + assertEquals(stats, result); + } + + @Test + void testGetFailureCount() { + Map successVsFailure = Map.of("failure", 5L, "success", 100L); + when(auditService.countSuccessVsFailure(eq("master"), any(), any())).thenReturn(successVsFailure); + + CountDTO result = auditResource.getFailureCount(null, null); + + assertEquals(5L, result.getCount()); + } + + @Test + void testGetSuccessCount() { + Map successVsFailure = Map.of("failure", 5L, "success", 100L); + when(auditService.countSuccessVsFailure(eq("master"), any(), any())).thenReturn(successVsFailure); + + CountDTO result = auditResource.getSuccessCount(null, null); + + assertEquals(100L, result.getCount()); + } + + @Test + void testExportLogsToCSV() { + when(auditService.exportToCSV(eq("master"), any(), any())).thenReturn("csv,data"); + + Response response = auditResource.exportLogsToCSV(null, null); + + assertEquals(200, response.getStatus()); + assertEquals("csv,data", response.getEntity()); + } + + @Test + void testPurgeOldLogs() { + when(auditService.purgeOldLogs(any())).thenReturn(0L); + + auditResource.purgeOldLogs(90); + + verify(auditService).purgeOldLogs(any()); + } + + @Test + void testSearchLogs_NullActeur_UsesRealm() { + List logs = Collections.singletonList( + AuditLogDTO.builder().acteurUsername("admin").typeAction(TypeActionAudit.USER_CREATE).build()); + when(auditService.findByRealm(eq("master"), any(), any(), eq(0), eq(50))).thenReturn(logs); + + List result = auditResource.searchLogs(null, null, null, null, null, null, 0, 50); + + assertEquals(logs, result); + verify(auditService).findByRealm(eq("master"), any(), any(), eq(0), eq(50)); + } + + @Test + void testSearchLogs_BlankActeur_UsesRealm() { + List logs = Collections.emptyList(); + when(auditService.findByRealm(eq("master"), any(), any(), eq(0), eq(20))).thenReturn(logs); + + List result = auditResource.searchLogs(" ", null, null, null, null, null, 0, 20); + + assertEquals(logs, result); + verify(auditService).findByRealm(eq("master"), any(), any(), eq(0), eq(20)); + } + + @Test + void testSearchLogs_WithPostFilter_TypeAction() { + AuditLogDTO matchLog = AuditLogDTO.builder() + .acteurUsername("admin") + .typeAction(TypeActionAudit.USER_CREATE) + .build(); + AuditLogDTO otherLog = AuditLogDTO.builder() + .acteurUsername("admin") + .typeAction(TypeActionAudit.USER_DELETE) + .build(); + when(auditService.findByActeur(eq("admin"), any(), any(), eq(0), eq(50))) + .thenReturn(List.of(matchLog, otherLog)); + + List result = auditResource.searchLogs("admin", null, null, + TypeActionAudit.USER_CREATE, null, null, 0, 50); + + assertEquals(1, result.size()); + assertEquals(TypeActionAudit.USER_CREATE, result.get(0).getTypeAction()); + } + + @Test + void testSearchLogs_WithPostFilter_RessourceType() { + AuditLogDTO matchLog = AuditLogDTO.builder() + .acteurUsername("admin") + .typeAction(TypeActionAudit.USER_CREATE) + .ressourceType("USER") + .build(); + AuditLogDTO otherLog = AuditLogDTO.builder() + .acteurUsername("admin") + .typeAction(TypeActionAudit.USER_CREATE) + .ressourceType("ROLE") + .build(); + when(auditService.findByActeur(eq("admin"), any(), any(), eq(0), eq(50))) + .thenReturn(List.of(matchLog, otherLog)); + + List result = auditResource.searchLogs("admin", null, null, + null, "USER", null, 0, 50); + + assertEquals(1, result.size()); + assertEquals("USER", result.get(0).getRessourceType()); + } + + @Test + void testSearchLogs_WithPostFilter_Succes() { + AuditLogDTO successLog = AuditLogDTO.builder() + .acteurUsername("admin") + .typeAction(TypeActionAudit.USER_CREATE) + .success(true) + .build(); + AuditLogDTO failLog = AuditLogDTO.builder() + .acteurUsername("admin") + .typeAction(TypeActionAudit.USER_CREATE) + .success(false) + .build(); + when(auditService.findByActeur(eq("admin"), any(), any(), eq(0), eq(50))) + .thenReturn(List.of(successLog, failLog)); + + List result = auditResource.searchLogs("admin", null, null, + null, null, Boolean.TRUE, 0, 50); + + assertEquals(1, result.size()); + assertTrue(result.get(0).isSuccessful()); + } + + @Test + void testExportLogsToCSV_Exception() { + when(auditService.exportToCSV(eq("master"), any(), any())) + .thenThrow(new RuntimeException("Export failed")); + + assertThrows(RuntimeException.class, () -> auditResource.exportLogsToCSV(null, null)); + } +} diff --git a/src/test/java/dev/lions/user/manager/resource/HealthResourceEndpointTest.java b/src/test/java/dev/lions/user/manager/resource/HealthResourceEndpointTest.java index 5604e89..5d6e41d 100644 --- a/src/test/java/dev/lions/user/manager/resource/HealthResourceEndpointTest.java +++ b/src/test/java/dev/lions/user/manager/resource/HealthResourceEndpointTest.java @@ -1,99 +1,99 @@ -package dev.lions.user.manager.resource; - -import dev.lions.user.manager.client.KeycloakAdminClient; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.keycloak.admin.client.Keycloak; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class HealthResourceEndpointTest { - - @Mock - KeycloakAdminClient keycloakAdminClient; - - @Mock - Keycloak keycloak; - - @InjectMocks - HealthResourceEndpoint healthResourceEndpoint; - - @Test - void testGetKeycloakHealthConnected() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloak); - - Map result = healthResourceEndpoint.getKeycloakHealth(); - - assertNotNull(result); - assertEquals("UP", result.get("status")); - assertEquals(true, result.get("connected")); - assertNotNull(result.get("timestamp")); - } - - @Test - void testGetKeycloakHealthDisconnected() { - when(keycloakAdminClient.getInstance()).thenReturn(null); - - Map result = healthResourceEndpoint.getKeycloakHealth(); - - assertNotNull(result); - assertEquals("DOWN", result.get("status")); - assertEquals(false, result.get("connected")); - } - - @Test - void testGetKeycloakHealthError() { - when(keycloakAdminClient.getInstance()).thenThrow(new RuntimeException("Connection error")); - - Map result = healthResourceEndpoint.getKeycloakHealth(); - - assertNotNull(result); - assertEquals("ERROR", result.get("status")); - assertEquals(false, result.get("connected")); - assertEquals("Connection error", result.get("error")); - } - - @Test - void testGetServiceStatusConnected() { - when(keycloakAdminClient.isConnected()).thenReturn(true); - - Map result = healthResourceEndpoint.getServiceStatus(); - - assertNotNull(result); - assertEquals("lions-user-manager-server", result.get("service")); - assertEquals("1.0.0", result.get("version")); - assertEquals("UP", result.get("status")); - assertEquals("CONNECTED", result.get("keycloak")); - assertNotNull(result.get("timestamp")); - } - - @Test - void testGetServiceStatusDisconnected() { - when(keycloakAdminClient.isConnected()).thenReturn(false); - - Map result = healthResourceEndpoint.getServiceStatus(); - - assertNotNull(result); - assertEquals("UP", result.get("status")); - assertEquals("DISCONNECTED", result.get("keycloak")); - } - - @Test - void testGetServiceStatusKeycloakError() { - when(keycloakAdminClient.isConnected()).thenThrow(new RuntimeException("Error")); - - Map result = healthResourceEndpoint.getServiceStatus(); - - assertNotNull(result); - assertEquals("UP", result.get("status")); - assertEquals("ERROR", result.get("keycloak")); - assertEquals("Error", result.get("keycloakError")); - } -} +package dev.lions.user.manager.resource; + +import dev.lions.user.manager.client.KeycloakAdminClient; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.keycloak.admin.client.Keycloak; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class HealthResourceEndpointTest { + + @Mock + KeycloakAdminClient keycloakAdminClient; + + @Mock + Keycloak keycloak; + + @InjectMocks + HealthResourceEndpoint healthResourceEndpoint; + + @Test + void testGetKeycloakHealthConnected() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloak); + + Map result = healthResourceEndpoint.getKeycloakHealth(); + + assertNotNull(result); + assertEquals("UP", result.get("status")); + assertEquals(true, result.get("connected")); + assertNotNull(result.get("timestamp")); + } + + @Test + void testGetKeycloakHealthDisconnected() { + when(keycloakAdminClient.getInstance()).thenReturn(null); + + Map result = healthResourceEndpoint.getKeycloakHealth(); + + assertNotNull(result); + assertEquals("DOWN", result.get("status")); + assertEquals(false, result.get("connected")); + } + + @Test + void testGetKeycloakHealthError() { + when(keycloakAdminClient.getInstance()).thenThrow(new RuntimeException("Connection error")); + + Map result = healthResourceEndpoint.getKeycloakHealth(); + + assertNotNull(result); + assertEquals("ERROR", result.get("status")); + assertEquals(false, result.get("connected")); + assertEquals("Connection error", result.get("error")); + } + + @Test + void testGetServiceStatusConnected() { + when(keycloakAdminClient.isConnected()).thenReturn(true); + + Map result = healthResourceEndpoint.getServiceStatus(); + + assertNotNull(result); + assertEquals("lions-user-manager-server", result.get("service")); + assertEquals("1.0.0", result.get("version")); + assertEquals("UP", result.get("status")); + assertEquals("CONNECTED", result.get("keycloak")); + assertNotNull(result.get("timestamp")); + } + + @Test + void testGetServiceStatusDisconnected() { + when(keycloakAdminClient.isConnected()).thenReturn(false); + + Map result = healthResourceEndpoint.getServiceStatus(); + + assertNotNull(result); + assertEquals("UP", result.get("status")); + assertEquals("DISCONNECTED", result.get("keycloak")); + } + + @Test + void testGetServiceStatusKeycloakError() { + when(keycloakAdminClient.isConnected()).thenThrow(new RuntimeException("Error")); + + Map result = healthResourceEndpoint.getServiceStatus(); + + assertNotNull(result); + assertEquals("UP", result.get("status")); + assertEquals("ERROR", result.get("keycloak")); + assertEquals("Error", result.get("keycloakError")); + } +} diff --git a/src/test/java/dev/lions/user/manager/resource/RealmAssignmentResourceTest.java b/src/test/java/dev/lions/user/manager/resource/RealmAssignmentResourceTest.java index eb46bc4..e8b7f6d 100644 --- a/src/test/java/dev/lions/user/manager/resource/RealmAssignmentResourceTest.java +++ b/src/test/java/dev/lions/user/manager/resource/RealmAssignmentResourceTest.java @@ -1,222 +1,222 @@ -package dev.lions.user.manager.resource; - -import dev.lions.user.manager.dto.realm.AuthorizedRealmsDTO; -import dev.lions.user.manager.dto.realm.RealmAccessCheckDTO; -import dev.lions.user.manager.dto.realm.RealmAssignmentDTO; -import dev.lions.user.manager.service.RealmAuthorizationService; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.SecurityContext; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.security.Principal; -import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -/** - * Tests unitaires pour RealmAssignmentResource - */ -@ExtendWith(MockitoExtension.class) -class RealmAssignmentResourceTest { - - @Mock - private RealmAuthorizationService realmAuthorizationService; - - @Mock - private SecurityContext securityContext; - - @Mock - private Principal principal; - - @InjectMocks - private RealmAssignmentResource realmAssignmentResource; - - private RealmAssignmentDTO assignment; - - @BeforeEach - void setUp() { - assignment = RealmAssignmentDTO.builder() - .id("assignment-1") - .userId("user-1") - .username("testuser") - .email("test@example.com") - .realmName("realm1") - .isSuperAdmin(false) - .active(true) - .assignedAt(LocalDateTime.now()) - .assignedBy("admin") - .build(); - } - - @Test - void testGetAllAssignments_Success() { - List assignments = Arrays.asList(assignment); - when(realmAuthorizationService.getAllAssignments()).thenReturn(assignments); - - List result = realmAssignmentResource.getAllAssignments(); - - assertEquals(1, result.size()); - } - - @Test - void testGetAssignmentsByUser_Success() { - List assignments = Arrays.asList(assignment); - when(realmAuthorizationService.getAssignmentsByUser("user-1")).thenReturn(assignments); - - List result = realmAssignmentResource.getAssignmentsByUser("user-1"); - - assertEquals(1, result.size()); - } - - @Test - void testGetAssignmentsByRealm_Success() { - List assignments = Arrays.asList(assignment); - when(realmAuthorizationService.getAssignmentsByRealm("realm1")).thenReturn(assignments); - - List result = realmAssignmentResource.getAssignmentsByRealm("realm1"); - - assertEquals(1, result.size()); - } - - @Test - void testGetAssignmentById_Success() { - when(realmAuthorizationService.getAssignmentById("assignment-1")).thenReturn(Optional.of(assignment)); - - RealmAssignmentDTO result = realmAssignmentResource.getAssignmentById("assignment-1"); - - assertNotNull(result); - assertEquals("assignment-1", result.getId()); - } - - @Test - void testGetAssignmentById_NotFound() { - when(realmAuthorizationService.getAssignmentById("non-existent")).thenReturn(Optional.empty()); - - assertThrows(RuntimeException.class, () -> realmAssignmentResource.getAssignmentById("non-existent")); - } - - @Test - void testCanManageRealm_Success() { - when(realmAuthorizationService.canManageRealm("user-1", "realm1")).thenReturn(true); - - RealmAccessCheckDTO result = realmAssignmentResource.canManageRealm("user-1", "realm1"); - - assertTrue(result.isCanManage()); - } - - @Test - void testGetAuthorizedRealms_Success() { - List realms = Arrays.asList("realm1", "realm2"); - when(realmAuthorizationService.getAuthorizedRealms("user-1")).thenReturn(realms); - when(realmAuthorizationService.isSuperAdmin("user-1")).thenReturn(false); - - AuthorizedRealmsDTO result = realmAssignmentResource.getAuthorizedRealms("user-1"); - - assertEquals(2, result.getRealms().size()); - assertFalse(result.isSuperAdmin()); - } - - @Test - void testAssignRealmToUser_Success() { - // En Quarkus, @Context SecurityContext injecté peut être simulé via Mockito - // Mais dans RealmAssignmentResource, l'admin est récupéré du SecurityContext. - // Puisque c'est un test unitaire @ExtendWith(MockitoExtension.class), - // @Inject SecurityContext securityContext est mocké. - - when(securityContext.getUserPrincipal()).thenReturn(principal); - when(principal.getName()).thenReturn("admin"); - when(realmAuthorizationService.assignRealmToUser(any(RealmAssignmentDTO.class))).thenReturn(assignment); - - Response response = realmAssignmentResource.assignRealmToUser(assignment); - - assertEquals(201, response.getStatus()); - } - - @Test - void testRevokeRealmFromUser_Success() { - doNothing().when(realmAuthorizationService).revokeRealmFromUser("user-1", "realm1"); - - realmAssignmentResource.revokeRealmFromUser("user-1", "realm1"); - - verify(realmAuthorizationService).revokeRealmFromUser("user-1", "realm1"); - } - - @Test - void testRevokeAllRealmsFromUser_Success() { - doNothing().when(realmAuthorizationService).revokeAllRealmsFromUser("user-1"); - - realmAssignmentResource.revokeAllRealmsFromUser("user-1"); - - verify(realmAuthorizationService).revokeAllRealmsFromUser("user-1"); - } - - @Test - void testDeactivateAssignment_Success() { - doNothing().when(realmAuthorizationService).deactivateAssignment("assignment-1"); - - realmAssignmentResource.deactivateAssignment("assignment-1"); - - verify(realmAuthorizationService).deactivateAssignment("assignment-1"); - } - - @Test - void testActivateAssignment_Success() { - doNothing().when(realmAuthorizationService).activateAssignment("assignment-1"); - - realmAssignmentResource.activateAssignment("assignment-1"); - - verify(realmAuthorizationService).activateAssignment("assignment-1"); - } - - @Test - void testSetSuperAdmin_Success() { - doNothing().when(realmAuthorizationService).setSuperAdmin("user-1", true); - - realmAssignmentResource.setSuperAdmin("user-1", true); - - verify(realmAuthorizationService).setSuperAdmin("user-1", true); - } - - @Test - void testAssignRealmToUser_NullPrincipal() { - when(securityContext.getUserPrincipal()).thenReturn(null); - when(realmAuthorizationService.assignRealmToUser(any(RealmAssignmentDTO.class))).thenReturn(assignment); - - Response response = realmAssignmentResource.assignRealmToUser(assignment); - - assertEquals(201, response.getStatus()); - // assignedBy n'est pas modifié car le principal est null - } - - @Test - void testAssignRealmToUser_IllegalArgumentException() { - when(securityContext.getUserPrincipal()).thenReturn(principal); - when(principal.getName()).thenReturn("admin"); - when(realmAuthorizationService.assignRealmToUser(any(RealmAssignmentDTO.class))) - .thenThrow(new IllegalArgumentException("Affectation déjà existante")); - - Response response = realmAssignmentResource.assignRealmToUser(assignment); - - assertEquals(409, response.getStatus()); - } - - @Test - void testAssignRealmToUser_GenericException() { - when(securityContext.getUserPrincipal()).thenReturn(principal); - when(principal.getName()).thenReturn("admin"); - when(realmAuthorizationService.assignRealmToUser(any(RealmAssignmentDTO.class))) - .thenThrow(new RuntimeException("Erreur inattendue")); - - assertThrows(RuntimeException.class, () -> realmAssignmentResource.assignRealmToUser(assignment)); - } -} +package dev.lions.user.manager.resource; + +import dev.lions.user.manager.dto.realm.AuthorizedRealmsDTO; +import dev.lions.user.manager.dto.realm.RealmAccessCheckDTO; +import dev.lions.user.manager.dto.realm.RealmAssignmentDTO; +import dev.lions.user.manager.service.RealmAuthorizationService; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.security.Principal; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * Tests unitaires pour RealmAssignmentResource + */ +@ExtendWith(MockitoExtension.class) +class RealmAssignmentResourceTest { + + @Mock + private RealmAuthorizationService realmAuthorizationService; + + @Mock + private SecurityContext securityContext; + + @Mock + private Principal principal; + + @InjectMocks + private RealmAssignmentResource realmAssignmentResource; + + private RealmAssignmentDTO assignment; + + @BeforeEach + void setUp() { + assignment = RealmAssignmentDTO.builder() + .id("assignment-1") + .userId("user-1") + .username("testuser") + .email("test@example.com") + .realmName("realm1") + .isSuperAdmin(false) + .active(true) + .assignedAt(LocalDateTime.now()) + .assignedBy("admin") + .build(); + } + + @Test + void testGetAllAssignments_Success() { + List assignments = Arrays.asList(assignment); + when(realmAuthorizationService.getAllAssignments()).thenReturn(assignments); + + List result = realmAssignmentResource.getAllAssignments(); + + assertEquals(1, result.size()); + } + + @Test + void testGetAssignmentsByUser_Success() { + List assignments = Arrays.asList(assignment); + when(realmAuthorizationService.getAssignmentsByUser("user-1")).thenReturn(assignments); + + List result = realmAssignmentResource.getAssignmentsByUser("user-1"); + + assertEquals(1, result.size()); + } + + @Test + void testGetAssignmentsByRealm_Success() { + List assignments = Arrays.asList(assignment); + when(realmAuthorizationService.getAssignmentsByRealm("realm1")).thenReturn(assignments); + + List result = realmAssignmentResource.getAssignmentsByRealm("realm1"); + + assertEquals(1, result.size()); + } + + @Test + void testGetAssignmentById_Success() { + when(realmAuthorizationService.getAssignmentById("assignment-1")).thenReturn(Optional.of(assignment)); + + RealmAssignmentDTO result = realmAssignmentResource.getAssignmentById("assignment-1"); + + assertNotNull(result); + assertEquals("assignment-1", result.getId()); + } + + @Test + void testGetAssignmentById_NotFound() { + when(realmAuthorizationService.getAssignmentById("non-existent")).thenReturn(Optional.empty()); + + assertThrows(RuntimeException.class, () -> realmAssignmentResource.getAssignmentById("non-existent")); + } + + @Test + void testCanManageRealm_Success() { + when(realmAuthorizationService.canManageRealm("user-1", "realm1")).thenReturn(true); + + RealmAccessCheckDTO result = realmAssignmentResource.canManageRealm("user-1", "realm1"); + + assertTrue(result.isCanManage()); + } + + @Test + void testGetAuthorizedRealms_Success() { + List realms = Arrays.asList("realm1", "realm2"); + when(realmAuthorizationService.getAuthorizedRealms("user-1")).thenReturn(realms); + when(realmAuthorizationService.isSuperAdmin("user-1")).thenReturn(false); + + AuthorizedRealmsDTO result = realmAssignmentResource.getAuthorizedRealms("user-1"); + + assertEquals(2, result.getRealms().size()); + assertFalse(result.isSuperAdmin()); + } + + @Test + void testAssignRealmToUser_Success() { + // En Quarkus, @Context SecurityContext injecté peut être simulé via Mockito + // Mais dans RealmAssignmentResource, l'admin est récupéré du SecurityContext. + // Puisque c'est un test unitaire @ExtendWith(MockitoExtension.class), + // @Inject SecurityContext securityContext est mocké. + + when(securityContext.getUserPrincipal()).thenReturn(principal); + when(principal.getName()).thenReturn("admin"); + when(realmAuthorizationService.assignRealmToUser(any(RealmAssignmentDTO.class))).thenReturn(assignment); + + Response response = realmAssignmentResource.assignRealmToUser(assignment); + + assertEquals(201, response.getStatus()); + } + + @Test + void testRevokeRealmFromUser_Success() { + doNothing().when(realmAuthorizationService).revokeRealmFromUser("user-1", "realm1"); + + realmAssignmentResource.revokeRealmFromUser("user-1", "realm1"); + + verify(realmAuthorizationService).revokeRealmFromUser("user-1", "realm1"); + } + + @Test + void testRevokeAllRealmsFromUser_Success() { + doNothing().when(realmAuthorizationService).revokeAllRealmsFromUser("user-1"); + + realmAssignmentResource.revokeAllRealmsFromUser("user-1"); + + verify(realmAuthorizationService).revokeAllRealmsFromUser("user-1"); + } + + @Test + void testDeactivateAssignment_Success() { + doNothing().when(realmAuthorizationService).deactivateAssignment("assignment-1"); + + realmAssignmentResource.deactivateAssignment("assignment-1"); + + verify(realmAuthorizationService).deactivateAssignment("assignment-1"); + } + + @Test + void testActivateAssignment_Success() { + doNothing().when(realmAuthorizationService).activateAssignment("assignment-1"); + + realmAssignmentResource.activateAssignment("assignment-1"); + + verify(realmAuthorizationService).activateAssignment("assignment-1"); + } + + @Test + void testSetSuperAdmin_Success() { + doNothing().when(realmAuthorizationService).setSuperAdmin("user-1", true); + + realmAssignmentResource.setSuperAdmin("user-1", true); + + verify(realmAuthorizationService).setSuperAdmin("user-1", true); + } + + @Test + void testAssignRealmToUser_NullPrincipal() { + when(securityContext.getUserPrincipal()).thenReturn(null); + when(realmAuthorizationService.assignRealmToUser(any(RealmAssignmentDTO.class))).thenReturn(assignment); + + Response response = realmAssignmentResource.assignRealmToUser(assignment); + + assertEquals(201, response.getStatus()); + // assignedBy n'est pas modifié car le principal est null + } + + @Test + void testAssignRealmToUser_IllegalArgumentException() { + when(securityContext.getUserPrincipal()).thenReturn(principal); + when(principal.getName()).thenReturn("admin"); + when(realmAuthorizationService.assignRealmToUser(any(RealmAssignmentDTO.class))) + .thenThrow(new IllegalArgumentException("Affectation déjà existante")); + + Response response = realmAssignmentResource.assignRealmToUser(assignment); + + assertEquals(409, response.getStatus()); + } + + @Test + void testAssignRealmToUser_GenericException() { + when(securityContext.getUserPrincipal()).thenReturn(principal); + when(principal.getName()).thenReturn("admin"); + when(realmAuthorizationService.assignRealmToUser(any(RealmAssignmentDTO.class))) + .thenThrow(new RuntimeException("Erreur inattendue")); + + assertThrows(RuntimeException.class, () -> realmAssignmentResource.assignRealmToUser(assignment)); + } +} diff --git a/src/test/java/dev/lions/user/manager/resource/RealmResourceAdditionalTest.java b/src/test/java/dev/lions/user/manager/resource/RealmResourceAdditionalTest.java index 9e38773..b74ee6d 100644 --- a/src/test/java/dev/lions/user/manager/resource/RealmResourceAdditionalTest.java +++ b/src/test/java/dev/lions/user/manager/resource/RealmResourceAdditionalTest.java @@ -1,88 +1,88 @@ -package dev.lions.user.manager.resource; - -import dev.lions.user.manager.client.KeycloakAdminClient; -import io.quarkus.security.identity.SecurityIdentity; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Arrays; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -/** - * Tests supplémentaires pour RealmResource pour améliorer la couverture - */ -@ExtendWith(MockitoExtension.class) -class RealmResourceAdditionalTest { - - @Mock - private KeycloakAdminClient keycloakAdminClient; - - @Mock - private SecurityIdentity securityIdentity; - - @InjectMocks - private RealmResource realmResource; - - @Test - void testGetAllRealms_Success() { - List realms = Arrays.asList("master", "lions-user-manager", "test-realm"); - when(keycloakAdminClient.getAllRealms()).thenReturn(realms); - - List result = realmResource.getAllRealms(); - - assertNotNull(result); - assertEquals(3, result.size()); - } - - @Test - void testGetAllRealms_Empty() { - when(keycloakAdminClient.getAllRealms()).thenReturn(List.of()); - - List result = realmResource.getAllRealms(); - - assertNotNull(result); - assertTrue(result.isEmpty()); - } - - @Test - void testGetAllRealms_Exception() { - when(keycloakAdminClient.getAllRealms()).thenThrow(new RuntimeException("Connection error")); - - assertThrows(RuntimeException.class, () -> realmResource.getAllRealms()); - } - - @Test - void testGetRealmClients_Success() { - List clients = Arrays.asList("admin-cli", "account", "lions-app"); - when(keycloakAdminClient.getRealmClients("test-realm")).thenReturn(clients); - - List result = realmResource.getRealmClients("test-realm"); - - assertNotNull(result); - assertEquals(3, result.size()); - assertTrue(result.contains("admin-cli")); - } - - @Test - void testGetRealmClients_Empty() { - when(keycloakAdminClient.getRealmClients("test-realm")).thenReturn(List.of()); - - List result = realmResource.getRealmClients("test-realm"); - - assertNotNull(result); - assertTrue(result.isEmpty()); - } - - @Test - void testGetRealmClients_Exception() { - when(keycloakAdminClient.getRealmClients("bad-realm")).thenThrow(new RuntimeException("Not found")); - - assertThrows(RuntimeException.class, () -> realmResource.getRealmClients("bad-realm")); - } -} +package dev.lions.user.manager.resource; + +import dev.lions.user.manager.client.KeycloakAdminClient; +import io.quarkus.security.identity.SecurityIdentity; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Tests supplémentaires pour RealmResource pour améliorer la couverture + */ +@ExtendWith(MockitoExtension.class) +class RealmResourceAdditionalTest { + + @Mock + private KeycloakAdminClient keycloakAdminClient; + + @Mock + private SecurityIdentity securityIdentity; + + @InjectMocks + private RealmResource realmResource; + + @Test + void testGetAllRealms_Success() { + List realms = Arrays.asList("master", "lions-user-manager", "test-realm"); + when(keycloakAdminClient.getAllRealms()).thenReturn(realms); + + List result = realmResource.getAllRealms(); + + assertNotNull(result); + assertEquals(3, result.size()); + } + + @Test + void testGetAllRealms_Empty() { + when(keycloakAdminClient.getAllRealms()).thenReturn(List.of()); + + List result = realmResource.getAllRealms(); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + void testGetAllRealms_Exception() { + when(keycloakAdminClient.getAllRealms()).thenThrow(new RuntimeException("Connection error")); + + assertThrows(RuntimeException.class, () -> realmResource.getAllRealms()); + } + + @Test + void testGetRealmClients_Success() { + List clients = Arrays.asList("admin-cli", "account", "lions-app"); + when(keycloakAdminClient.getRealmClients("test-realm")).thenReturn(clients); + + List result = realmResource.getRealmClients("test-realm"); + + assertNotNull(result); + assertEquals(3, result.size()); + assertTrue(result.contains("admin-cli")); + } + + @Test + void testGetRealmClients_Empty() { + when(keycloakAdminClient.getRealmClients("test-realm")).thenReturn(List.of()); + + List result = realmResource.getRealmClients("test-realm"); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + void testGetRealmClients_Exception() { + when(keycloakAdminClient.getRealmClients("bad-realm")).thenThrow(new RuntimeException("Not found")); + + assertThrows(RuntimeException.class, () -> realmResource.getRealmClients("bad-realm")); + } +} diff --git a/src/test/java/dev/lions/user/manager/resource/RealmResourceTest.java b/src/test/java/dev/lions/user/manager/resource/RealmResourceTest.java index d5a0dbf..4376cab 100644 --- a/src/test/java/dev/lions/user/manager/resource/RealmResourceTest.java +++ b/src/test/java/dev/lions/user/manager/resource/RealmResourceTest.java @@ -1,62 +1,62 @@ -package dev.lions.user.manager.resource; - -import dev.lions.user.manager.client.KeycloakAdminClient; -import io.quarkus.security.identity.SecurityIdentity; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -/** - * Tests unitaires pour RealmResource - */ -@ExtendWith(MockitoExtension.class) -class RealmResourceTest { - - @Mock - private KeycloakAdminClient keycloakAdminClient; - - @Mock - private SecurityIdentity securityIdentity; - - @InjectMocks - private RealmResource realmResource; - - @Test - void testGetAllRealms_Success() { - List realms = Arrays.asList("master", "lions-user-manager", "btpxpress"); - when(keycloakAdminClient.getAllRealms()).thenReturn(realms); - - List result = realmResource.getAllRealms(); - - assertNotNull(result); - assertEquals(3, result.size()); - assertEquals("master", result.get(0)); - verify(keycloakAdminClient).getAllRealms(); - } - - @Test - void testGetAllRealms_EmptyList() { - when(keycloakAdminClient.getAllRealms()).thenReturn(Collections.emptyList()); - - List result = realmResource.getAllRealms(); - - assertNotNull(result); - assertTrue(result.isEmpty()); - } - - @Test - void testGetAllRealms_Exception() { - when(keycloakAdminClient.getAllRealms()).thenThrow(new RuntimeException("Keycloak connection error")); - - assertThrows(RuntimeException.class, () -> realmResource.getAllRealms()); - } -} +package dev.lions.user.manager.resource; + +import dev.lions.user.manager.client.KeycloakAdminClient; +import io.quarkus.security.identity.SecurityIdentity; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Tests unitaires pour RealmResource + */ +@ExtendWith(MockitoExtension.class) +class RealmResourceTest { + + @Mock + private KeycloakAdminClient keycloakAdminClient; + + @Mock + private SecurityIdentity securityIdentity; + + @InjectMocks + private RealmResource realmResource; + + @Test + void testGetAllRealms_Success() { + List realms = Arrays.asList("master", "lions-user-manager", "btpxpress"); + when(keycloakAdminClient.getAllRealms()).thenReturn(realms); + + List result = realmResource.getAllRealms(); + + assertNotNull(result); + assertEquals(3, result.size()); + assertEquals("master", result.get(0)); + verify(keycloakAdminClient).getAllRealms(); + } + + @Test + void testGetAllRealms_EmptyList() { + when(keycloakAdminClient.getAllRealms()).thenReturn(Collections.emptyList()); + + List result = realmResource.getAllRealms(); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + void testGetAllRealms_Exception() { + when(keycloakAdminClient.getAllRealms()).thenThrow(new RuntimeException("Keycloak connection error")); + + assertThrows(RuntimeException.class, () -> realmResource.getAllRealms()); + } +} diff --git a/src/test/java/dev/lions/user/manager/resource/RoleResourceTest.java b/src/test/java/dev/lions/user/manager/resource/RoleResourceTest.java index 1714963..d0efb39 100644 --- a/src/test/java/dev/lions/user/manager/resource/RoleResourceTest.java +++ b/src/test/java/dev/lions/user/manager/resource/RoleResourceTest.java @@ -1,370 +1,370 @@ -package dev.lions.user.manager.resource; - -import dev.lions.user.manager.dto.role.RoleAssignmentDTO; -import dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO; -import dev.lions.user.manager.dto.role.RoleDTO; -import dev.lions.user.manager.enums.role.TypeRole; -import dev.lions.user.manager.service.RoleService; -import jakarta.ws.rs.core.Response; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class RoleResourceTest { - - @Mock - RoleService roleService; - - @InjectMocks - RoleResource roleResource; - - private static final String REALM = "test-realm"; - private static final String CLIENT_ID = "test-client"; - - // ============== Realm Role Tests ============== - - @Test - void testCreateRealmRole() { - RoleDTO input = RoleDTO.builder().name("role").description("desc").build(); - RoleDTO created = RoleDTO.builder().id("1").name("role").description("desc").build(); - - when(roleService.createRealmRole(any(), eq(REALM))).thenReturn(created); - - Response response = roleResource.createRealmRole(input, REALM); - - assertEquals(201, response.getStatus()); - assertEquals(created, response.getEntity()); - } - - @Test - void testCreateRealmRoleConflict() { - RoleDTO input = RoleDTO.builder().name("role").build(); - - when(roleService.createRealmRole(any(), eq(REALM))) - .thenThrow(new IllegalArgumentException("Role already exists")); - - Response response = roleResource.createRealmRole(input, REALM); - - assertEquals(409, response.getStatus()); - } - - @Test - void testGetRealmRole() { - RoleDTO role = RoleDTO.builder().id("1").name("role").build(); - when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) - .thenReturn(Optional.of(role)); - - RoleDTO result = roleResource.getRealmRole("role", REALM); - - assertEquals(role, result); - } - - @Test - void testGetRealmRoleNotFound() { - when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) - .thenReturn(Optional.empty()); - - assertThrows(RuntimeException.class, () -> roleResource.getRealmRole("role", REALM)); - } - - @Test - void testGetAllRealmRoles() { - List roles = Collections.singletonList(RoleDTO.builder().name("role").build()); - when(roleService.getAllRealmRoles(REALM)).thenReturn(roles); - - List result = roleResource.getAllRealmRoles(REALM); - - assertEquals(roles, result); - } - - @Test - void testUpdateRealmRole() { - RoleDTO existingRole = RoleDTO.builder().id("1").name("role").build(); - RoleDTO input = RoleDTO.builder().description("updated").build(); - RoleDTO updated = RoleDTO.builder().id("1").name("role").description("updated").build(); - - when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) - .thenReturn(Optional.of(existingRole)); - when(roleService.updateRole(eq("1"), any(), eq(REALM), eq(TypeRole.REALM_ROLE), isNull())) - .thenReturn(updated); - - RoleDTO result = roleResource.updateRealmRole("role", input, REALM); - - assertEquals(updated, result); - } - - @Test - void testDeleteRealmRole() { - RoleDTO existingRole = RoleDTO.builder().id("1").name("role").build(); - when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) - .thenReturn(Optional.of(existingRole)); - doNothing().when(roleService).deleteRole(eq("1"), eq(REALM), eq(TypeRole.REALM_ROLE), isNull()); - - roleResource.deleteRealmRole("role", REALM); - - verify(roleService).deleteRole(eq("1"), eq(REALM), eq(TypeRole.REALM_ROLE), isNull()); - } - - // ============== Client Role Tests ============== - - @Test - void testCreateClientRole() { - RoleDTO input = RoleDTO.builder().name("role").build(); - RoleDTO created = RoleDTO.builder().id("1").name("role").build(); - - when(roleService.createClientRole(any(RoleDTO.class), eq(CLIENT_ID), eq(REALM))).thenReturn(created); - - Response response = roleResource.createClientRole(CLIENT_ID, input, REALM); - - assertEquals(201, response.getStatus()); - assertEquals(created, response.getEntity()); - } - - @Test - void testGetClientRole() { - RoleDTO role = RoleDTO.builder().id("1").name("role").build(); - when(roleService.getRoleByName("role", REALM, TypeRole.CLIENT_ROLE, CLIENT_ID)) - .thenReturn(Optional.of(role)); - - RoleDTO result = roleResource.getClientRole(CLIENT_ID, "role", REALM); - - assertEquals(role, result); - } - - @Test - void testGetAllClientRoles() { - List roles = Collections.singletonList(RoleDTO.builder().name("role").build()); - when(roleService.getAllClientRoles(REALM, CLIENT_ID)).thenReturn(roles); - - List result = roleResource.getAllClientRoles(CLIENT_ID, REALM); - - assertEquals(roles, result); - } - - @Test - void testDeleteClientRole() { - RoleDTO existingRole = RoleDTO.builder().id("1").name("role").build(); - when(roleService.getRoleByName("role", REALM, TypeRole.CLIENT_ROLE, CLIENT_ID)) - .thenReturn(Optional.of(existingRole)); - doNothing().when(roleService).deleteRole(eq("1"), eq(REALM), eq(TypeRole.CLIENT_ROLE), eq(CLIENT_ID)); - - roleResource.deleteClientRole(CLIENT_ID, "role", REALM); - - verify(roleService).deleteRole(eq("1"), eq(REALM), eq(TypeRole.CLIENT_ROLE), eq(CLIENT_ID)); - } - - // ============== Role Assignment Tests ============== - - @Test - void testAssignRealmRoles() { - doNothing().when(roleService).assignRolesToUser(any(RoleAssignmentDTO.class)); - - RoleAssignmentRequestDTO request = RoleAssignmentRequestDTO.builder() - .roleNames(Collections.singletonList("role")) - .build(); - - roleResource.assignRealmRoles("user1", REALM, request); - - verify(roleService).assignRolesToUser(any(RoleAssignmentDTO.class)); - } - - @Test - void testRevokeRealmRoles() { - doNothing().when(roleService).revokeRolesFromUser(any(RoleAssignmentDTO.class)); - - RoleAssignmentRequestDTO request = RoleAssignmentRequestDTO.builder() - .roleNames(Collections.singletonList("role")) - .build(); - - roleResource.revokeRealmRoles("user1", REALM, request); - - verify(roleService).revokeRolesFromUser(any(RoleAssignmentDTO.class)); - } - - @Test - void testAssignClientRoles() { - doNothing().when(roleService).assignRolesToUser(any(RoleAssignmentDTO.class)); - - RoleAssignmentRequestDTO request = RoleAssignmentRequestDTO.builder() - .roleNames(Collections.singletonList("role")) - .build(); - - roleResource.assignClientRoles(CLIENT_ID, "user1", REALM, request); - - verify(roleService).assignRolesToUser(any(RoleAssignmentDTO.class)); - } - - @Test - void testGetUserRealmRoles() { - List roles = Collections.singletonList(RoleDTO.builder().name("role").build()); - when(roleService.getUserRealmRoles("user1", REALM)).thenReturn(roles); - - List result = roleResource.getUserRealmRoles("user1", REALM); - - assertEquals(roles, result); - } - - @Test - void testGetUserClientRoles() { - List roles = Collections.singletonList(RoleDTO.builder().name("role").build()); - when(roleService.getUserClientRoles("user1", CLIENT_ID, REALM)).thenReturn(roles); - - List result = roleResource.getUserClientRoles(CLIENT_ID, "user1", REALM); - - assertEquals(roles, result); - } - - // ============== Composite Role Tests ============== - - @Test - void testAddComposites() { - RoleDTO parentRole = RoleDTO.builder().id("parent-1").name("role").build(); - RoleDTO childRole = RoleDTO.builder().id("child-1").name("composite").build(); - - when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) - .thenReturn(Optional.of(parentRole)); - when(roleService.getRoleByName("composite", REALM, TypeRole.REALM_ROLE, null)) - .thenReturn(Optional.of(childRole)); - doNothing().when(roleService).addCompositeRoles(eq("parent-1"), anyList(), eq(REALM), - eq(TypeRole.REALM_ROLE), isNull()); - - RoleAssignmentRequestDTO request = RoleAssignmentRequestDTO.builder() - .roleNames(Collections.singletonList("composite")) - .build(); - - roleResource.addComposites("role", REALM, request); - - verify(roleService).addCompositeRoles(eq("parent-1"), anyList(), eq(REALM), - eq(TypeRole.REALM_ROLE), isNull()); - } - - @Test - void testGetComposites() { - RoleDTO role = RoleDTO.builder().id("1").name("role").build(); - List composites = Collections.singletonList(RoleDTO.builder().name("composite").build()); - - when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) - .thenReturn(Optional.of(role)); - when(roleService.getCompositeRoles("1", REALM, TypeRole.REALM_ROLE, null)) - .thenReturn(composites); - - List result = roleResource.getComposites("role", REALM); - - assertEquals(composites, result); - } - - @Test - void testCreateRealmRole_GenericException() { - RoleDTO input = RoleDTO.builder().name("role").build(); - when(roleService.createRealmRole(any(), eq(REALM))) - .thenThrow(new RuntimeException("Internal error")); - - assertThrows(RuntimeException.class, () -> roleResource.createRealmRole(input, REALM)); - } - - @Test - void testUpdateRealmRole_NotFound() { - when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) - .thenReturn(Optional.empty()); - - RoleDTO input = RoleDTO.builder().description("updated").build(); - assertThrows(RuntimeException.class, () -> roleResource.updateRealmRole("role", input, REALM)); - } - - @Test - void testDeleteRealmRole_NotFound() { - when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) - .thenReturn(Optional.empty()); - - assertThrows(RuntimeException.class, () -> roleResource.deleteRealmRole("role", REALM)); - } - - @Test - void testCreateClientRole_IllegalArgumentException() { - RoleDTO input = RoleDTO.builder().name("role").build(); - when(roleService.createClientRole(any(RoleDTO.class), eq(CLIENT_ID), eq(REALM))) - .thenThrow(new IllegalArgumentException("Conflict")); - - Response response = roleResource.createClientRole(CLIENT_ID, input, REALM); - - assertEquals(409, response.getStatus()); - } - - @Test - void testCreateClientRole_GenericException() { - RoleDTO input = RoleDTO.builder().name("role").build(); - when(roleService.createClientRole(any(RoleDTO.class), eq(CLIENT_ID), eq(REALM))) - .thenThrow(new RuntimeException("Internal error")); - - assertThrows(RuntimeException.class, () -> roleResource.createClientRole(CLIENT_ID, input, REALM)); - } - - @Test - void testGetClientRole_NotFound() { - when(roleService.getRoleByName("role", REALM, TypeRole.CLIENT_ROLE, CLIENT_ID)) - .thenReturn(Optional.empty()); - - assertThrows(RuntimeException.class, () -> roleResource.getClientRole(CLIENT_ID, "role", REALM)); - } - - @Test - void testDeleteClientRole_NotFound() { - when(roleService.getRoleByName("role", REALM, TypeRole.CLIENT_ROLE, CLIENT_ID)) - .thenReturn(Optional.empty()); - - assertThrows(RuntimeException.class, () -> roleResource.deleteClientRole(CLIENT_ID, "role", REALM)); - } - - @Test - void testAddComposites_ParentNotFound() { - when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) - .thenReturn(Optional.empty()); - - RoleAssignmentRequestDTO request = RoleAssignmentRequestDTO.builder() - .roleNames(Collections.singletonList("composite")) - .build(); - - assertThrows(RuntimeException.class, () -> roleResource.addComposites("role", REALM, request)); - } - - @Test - void testAddComposites_ChildNotFound_FilteredOut() { - RoleDTO parentRole = RoleDTO.builder().id("parent-1").name("role").build(); - - when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) - .thenReturn(Optional.of(parentRole)); - when(roleService.getRoleByName("nonexistent", REALM, TypeRole.REALM_ROLE, null)) - .thenReturn(Optional.empty()); // will be filtered out (null id) - doNothing().when(roleService).addCompositeRoles(eq("parent-1"), anyList(), eq(REALM), - eq(TypeRole.REALM_ROLE), isNull()); - - RoleAssignmentRequestDTO request = RoleAssignmentRequestDTO.builder() - .roleNames(Collections.singletonList("nonexistent")) - .build(); - - roleResource.addComposites("role", REALM, request); - - // addCompositeRoles called with empty list (filtered out) - verify(roleService).addCompositeRoles(eq("parent-1"), eq(Collections.emptyList()), eq(REALM), - eq(TypeRole.REALM_ROLE), isNull()); - } - - @Test - void testGetComposites_RoleNotFound() { - when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) - .thenReturn(Optional.empty()); - - assertThrows(RuntimeException.class, () -> roleResource.getComposites("role", REALM)); - } -} +package dev.lions.user.manager.resource; + +import dev.lions.user.manager.dto.role.RoleAssignmentDTO; +import dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO; +import dev.lions.user.manager.dto.role.RoleDTO; +import dev.lions.user.manager.enums.role.TypeRole; +import dev.lions.user.manager.service.RoleService; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class RoleResourceTest { + + @Mock + RoleService roleService; + + @InjectMocks + RoleResource roleResource; + + private static final String REALM = "test-realm"; + private static final String CLIENT_ID = "test-client"; + + // ============== Realm Role Tests ============== + + @Test + void testCreateRealmRole() { + RoleDTO input = RoleDTO.builder().name("role").description("desc").build(); + RoleDTO created = RoleDTO.builder().id("1").name("role").description("desc").build(); + + when(roleService.createRealmRole(any(), eq(REALM))).thenReturn(created); + + Response response = roleResource.createRealmRole(input, REALM); + + assertEquals(201, response.getStatus()); + assertEquals(created, response.getEntity()); + } + + @Test + void testCreateRealmRoleConflict() { + RoleDTO input = RoleDTO.builder().name("role").build(); + + when(roleService.createRealmRole(any(), eq(REALM))) + .thenThrow(new IllegalArgumentException("Role already exists")); + + Response response = roleResource.createRealmRole(input, REALM); + + assertEquals(409, response.getStatus()); + } + + @Test + void testGetRealmRole() { + RoleDTO role = RoleDTO.builder().id("1").name("role").build(); + when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) + .thenReturn(Optional.of(role)); + + RoleDTO result = roleResource.getRealmRole("role", REALM); + + assertEquals(role, result); + } + + @Test + void testGetRealmRoleNotFound() { + when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) + .thenReturn(Optional.empty()); + + assertThrows(RuntimeException.class, () -> roleResource.getRealmRole("role", REALM)); + } + + @Test + void testGetAllRealmRoles() { + List roles = Collections.singletonList(RoleDTO.builder().name("role").build()); + when(roleService.getAllRealmRoles(REALM)).thenReturn(roles); + + List result = roleResource.getAllRealmRoles(REALM); + + assertEquals(roles, result); + } + + @Test + void testUpdateRealmRole() { + RoleDTO existingRole = RoleDTO.builder().id("1").name("role").build(); + RoleDTO input = RoleDTO.builder().description("updated").build(); + RoleDTO updated = RoleDTO.builder().id("1").name("role").description("updated").build(); + + when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) + .thenReturn(Optional.of(existingRole)); + when(roleService.updateRole(eq("1"), any(), eq(REALM), eq(TypeRole.REALM_ROLE), isNull())) + .thenReturn(updated); + + RoleDTO result = roleResource.updateRealmRole("role", input, REALM); + + assertEquals(updated, result); + } + + @Test + void testDeleteRealmRole() { + RoleDTO existingRole = RoleDTO.builder().id("1").name("role").build(); + when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) + .thenReturn(Optional.of(existingRole)); + doNothing().when(roleService).deleteRole(eq("1"), eq(REALM), eq(TypeRole.REALM_ROLE), isNull()); + + roleResource.deleteRealmRole("role", REALM); + + verify(roleService).deleteRole(eq("1"), eq(REALM), eq(TypeRole.REALM_ROLE), isNull()); + } + + // ============== Client Role Tests ============== + + @Test + void testCreateClientRole() { + RoleDTO input = RoleDTO.builder().name("role").build(); + RoleDTO created = RoleDTO.builder().id("1").name("role").build(); + + when(roleService.createClientRole(any(RoleDTO.class), eq(CLIENT_ID), eq(REALM))).thenReturn(created); + + Response response = roleResource.createClientRole(CLIENT_ID, input, REALM); + + assertEquals(201, response.getStatus()); + assertEquals(created, response.getEntity()); + } + + @Test + void testGetClientRole() { + RoleDTO role = RoleDTO.builder().id("1").name("role").build(); + when(roleService.getRoleByName("role", REALM, TypeRole.CLIENT_ROLE, CLIENT_ID)) + .thenReturn(Optional.of(role)); + + RoleDTO result = roleResource.getClientRole(CLIENT_ID, "role", REALM); + + assertEquals(role, result); + } + + @Test + void testGetAllClientRoles() { + List roles = Collections.singletonList(RoleDTO.builder().name("role").build()); + when(roleService.getAllClientRoles(REALM, CLIENT_ID)).thenReturn(roles); + + List result = roleResource.getAllClientRoles(CLIENT_ID, REALM); + + assertEquals(roles, result); + } + + @Test + void testDeleteClientRole() { + RoleDTO existingRole = RoleDTO.builder().id("1").name("role").build(); + when(roleService.getRoleByName("role", REALM, TypeRole.CLIENT_ROLE, CLIENT_ID)) + .thenReturn(Optional.of(existingRole)); + doNothing().when(roleService).deleteRole(eq("1"), eq(REALM), eq(TypeRole.CLIENT_ROLE), eq(CLIENT_ID)); + + roleResource.deleteClientRole(CLIENT_ID, "role", REALM); + + verify(roleService).deleteRole(eq("1"), eq(REALM), eq(TypeRole.CLIENT_ROLE), eq(CLIENT_ID)); + } + + // ============== Role Assignment Tests ============== + + @Test + void testAssignRealmRoles() { + doNothing().when(roleService).assignRolesToUser(any(RoleAssignmentDTO.class)); + + RoleAssignmentRequestDTO request = RoleAssignmentRequestDTO.builder() + .roleNames(Collections.singletonList("role")) + .build(); + + roleResource.assignRealmRoles("user1", REALM, request); + + verify(roleService).assignRolesToUser(any(RoleAssignmentDTO.class)); + } + + @Test + void testRevokeRealmRoles() { + doNothing().when(roleService).revokeRolesFromUser(any(RoleAssignmentDTO.class)); + + RoleAssignmentRequestDTO request = RoleAssignmentRequestDTO.builder() + .roleNames(Collections.singletonList("role")) + .build(); + + roleResource.revokeRealmRoles("user1", REALM, request); + + verify(roleService).revokeRolesFromUser(any(RoleAssignmentDTO.class)); + } + + @Test + void testAssignClientRoles() { + doNothing().when(roleService).assignRolesToUser(any(RoleAssignmentDTO.class)); + + RoleAssignmentRequestDTO request = RoleAssignmentRequestDTO.builder() + .roleNames(Collections.singletonList("role")) + .build(); + + roleResource.assignClientRoles(CLIENT_ID, "user1", REALM, request); + + verify(roleService).assignRolesToUser(any(RoleAssignmentDTO.class)); + } + + @Test + void testGetUserRealmRoles() { + List roles = Collections.singletonList(RoleDTO.builder().name("role").build()); + when(roleService.getUserRealmRoles("user1", REALM)).thenReturn(roles); + + List result = roleResource.getUserRealmRoles("user1", REALM); + + assertEquals(roles, result); + } + + @Test + void testGetUserClientRoles() { + List roles = Collections.singletonList(RoleDTO.builder().name("role").build()); + when(roleService.getUserClientRoles("user1", CLIENT_ID, REALM)).thenReturn(roles); + + List result = roleResource.getUserClientRoles(CLIENT_ID, "user1", REALM); + + assertEquals(roles, result); + } + + // ============== Composite Role Tests ============== + + @Test + void testAddComposites() { + RoleDTO parentRole = RoleDTO.builder().id("parent-1").name("role").build(); + RoleDTO childRole = RoleDTO.builder().id("child-1").name("composite").build(); + + when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) + .thenReturn(Optional.of(parentRole)); + when(roleService.getRoleByName("composite", REALM, TypeRole.REALM_ROLE, null)) + .thenReturn(Optional.of(childRole)); + doNothing().when(roleService).addCompositeRoles(eq("parent-1"), anyList(), eq(REALM), + eq(TypeRole.REALM_ROLE), isNull()); + + RoleAssignmentRequestDTO request = RoleAssignmentRequestDTO.builder() + .roleNames(Collections.singletonList("composite")) + .build(); + + roleResource.addComposites("role", REALM, request); + + verify(roleService).addCompositeRoles(eq("parent-1"), anyList(), eq(REALM), + eq(TypeRole.REALM_ROLE), isNull()); + } + + @Test + void testGetComposites() { + RoleDTO role = RoleDTO.builder().id("1").name("role").build(); + List composites = Collections.singletonList(RoleDTO.builder().name("composite").build()); + + when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) + .thenReturn(Optional.of(role)); + when(roleService.getCompositeRoles("1", REALM, TypeRole.REALM_ROLE, null)) + .thenReturn(composites); + + List result = roleResource.getComposites("role", REALM); + + assertEquals(composites, result); + } + + @Test + void testCreateRealmRole_GenericException() { + RoleDTO input = RoleDTO.builder().name("role").build(); + when(roleService.createRealmRole(any(), eq(REALM))) + .thenThrow(new RuntimeException("Internal error")); + + assertThrows(RuntimeException.class, () -> roleResource.createRealmRole(input, REALM)); + } + + @Test + void testUpdateRealmRole_NotFound() { + when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) + .thenReturn(Optional.empty()); + + RoleDTO input = RoleDTO.builder().description("updated").build(); + assertThrows(RuntimeException.class, () -> roleResource.updateRealmRole("role", input, REALM)); + } + + @Test + void testDeleteRealmRole_NotFound() { + when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) + .thenReturn(Optional.empty()); + + assertThrows(RuntimeException.class, () -> roleResource.deleteRealmRole("role", REALM)); + } + + @Test + void testCreateClientRole_IllegalArgumentException() { + RoleDTO input = RoleDTO.builder().name("role").build(); + when(roleService.createClientRole(any(RoleDTO.class), eq(CLIENT_ID), eq(REALM))) + .thenThrow(new IllegalArgumentException("Conflict")); + + Response response = roleResource.createClientRole(CLIENT_ID, input, REALM); + + assertEquals(409, response.getStatus()); + } + + @Test + void testCreateClientRole_GenericException() { + RoleDTO input = RoleDTO.builder().name("role").build(); + when(roleService.createClientRole(any(RoleDTO.class), eq(CLIENT_ID), eq(REALM))) + .thenThrow(new RuntimeException("Internal error")); + + assertThrows(RuntimeException.class, () -> roleResource.createClientRole(CLIENT_ID, input, REALM)); + } + + @Test + void testGetClientRole_NotFound() { + when(roleService.getRoleByName("role", REALM, TypeRole.CLIENT_ROLE, CLIENT_ID)) + .thenReturn(Optional.empty()); + + assertThrows(RuntimeException.class, () -> roleResource.getClientRole(CLIENT_ID, "role", REALM)); + } + + @Test + void testDeleteClientRole_NotFound() { + when(roleService.getRoleByName("role", REALM, TypeRole.CLIENT_ROLE, CLIENT_ID)) + .thenReturn(Optional.empty()); + + assertThrows(RuntimeException.class, () -> roleResource.deleteClientRole(CLIENT_ID, "role", REALM)); + } + + @Test + void testAddComposites_ParentNotFound() { + when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) + .thenReturn(Optional.empty()); + + RoleAssignmentRequestDTO request = RoleAssignmentRequestDTO.builder() + .roleNames(Collections.singletonList("composite")) + .build(); + + assertThrows(RuntimeException.class, () -> roleResource.addComposites("role", REALM, request)); + } + + @Test + void testAddComposites_ChildNotFound_FilteredOut() { + RoleDTO parentRole = RoleDTO.builder().id("parent-1").name("role").build(); + + when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) + .thenReturn(Optional.of(parentRole)); + when(roleService.getRoleByName("nonexistent", REALM, TypeRole.REALM_ROLE, null)) + .thenReturn(Optional.empty()); // will be filtered out (null id) + doNothing().when(roleService).addCompositeRoles(eq("parent-1"), anyList(), eq(REALM), + eq(TypeRole.REALM_ROLE), isNull()); + + RoleAssignmentRequestDTO request = RoleAssignmentRequestDTO.builder() + .roleNames(Collections.singletonList("nonexistent")) + .build(); + + roleResource.addComposites("role", REALM, request); + + // addCompositeRoles called with empty list (filtered out) + verify(roleService).addCompositeRoles(eq("parent-1"), eq(Collections.emptyList()), eq(REALM), + eq(TypeRole.REALM_ROLE), isNull()); + } + + @Test + void testGetComposites_RoleNotFound() { + when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null)) + .thenReturn(Optional.empty()); + + assertThrows(RuntimeException.class, () -> roleResource.getComposites("role", REALM)); + } +} diff --git a/src/test/java/dev/lions/user/manager/resource/SyncResourceTest.java b/src/test/java/dev/lions/user/manager/resource/SyncResourceTest.java index fa14527..adb57f4 100644 --- a/src/test/java/dev/lions/user/manager/resource/SyncResourceTest.java +++ b/src/test/java/dev/lions/user/manager/resource/SyncResourceTest.java @@ -1,163 +1,163 @@ -package dev.lions.user.manager.resource; - -import dev.lions.user.manager.api.SyncResourceApi; -import dev.lions.user.manager.dto.sync.HealthStatusDTO; -import dev.lions.user.manager.dto.sync.SyncConsistencyDTO; -import dev.lions.user.manager.dto.sync.SyncHistoryDTO; -import dev.lions.user.manager.dto.sync.SyncResultDTO; -import dev.lions.user.manager.service.SyncService; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class SyncResourceTest { - - @Mock - SyncService syncService; - - @InjectMocks - SyncResource syncResource; - - private static final String REALM = "test-realm"; - - @Test - void testCheckKeycloakHealth() { - when(syncService.isKeycloakAvailable()).thenReturn(true); - when(syncService.getKeycloakHealthInfo()).thenReturn(Map.of("version", "23.0.0")); - - HealthStatusDTO status = syncResource.checkKeycloakHealth(); - - assertTrue(status.isKeycloakAccessible()); - assertTrue(status.isOverallHealthy()); - assertEquals("23.0.0", status.getKeycloakVersion()); - } - - @Test - void testCheckKeycloakHealthError() { - when(syncService.isKeycloakAvailable()).thenThrow(new RuntimeException("Connection refused")); - - HealthStatusDTO status = syncResource.checkKeycloakHealth(); - - assertFalse(status.isOverallHealthy()); - assertTrue(status.getErrorMessage().contains("Connection refused")); - } - - @Test - void testSyncUsers() { - when(syncService.syncUsersFromRealm(REALM)).thenReturn(10); - - SyncResultDTO result = syncResource.syncUsers(REALM); - - assertTrue(result.isSuccess()); - assertEquals(10, result.getUsersCount()); - assertEquals(REALM, result.getRealmName()); - } - - @Test - void testSyncUsersError() { - when(syncService.syncUsersFromRealm(REALM)).thenThrow(new RuntimeException("Sync failed")); - - SyncResultDTO result = syncResource.syncUsers(REALM); - - assertFalse(result.isSuccess()); - assertEquals("Sync failed", result.getErrorMessage()); - } - - @Test - void testSyncRoles() { - when(syncService.syncRolesFromRealm(REALM)).thenReturn(5); - - SyncResultDTO result = syncResource.syncRoles(REALM, null); - - assertTrue(result.isSuccess()); - assertEquals(5, result.getRealmRolesCount()); - } - - @Test - void testSyncRolesError() { - when(syncService.syncRolesFromRealm(REALM)).thenThrow(new RuntimeException("Roles sync failed")); - - SyncResultDTO result = syncResource.syncRoles(REALM, null); - - assertFalse(result.isSuccess()); - assertEquals("Roles sync failed", result.getErrorMessage()); - } - - @Test - void testPing() { - String response = syncResource.ping(); - - assertNotNull(response); - assertTrue(response.contains("pong")); - assertTrue(response.contains("SyncResource")); - } - - @Test - void testCheckDataConsistency_Success() { - when(syncService.checkDataConsistency(REALM)).thenReturn(Map.of( - "realmName", REALM, - "status", "OK", - "usersKeycloakCount", 10, - "usersLocalCount", 10 - )); - - var result = syncResource.checkDataConsistency(REALM); - - assertNotNull(result); - assertEquals(REALM, result.getRealmName()); - assertEquals("OK", result.getStatus()); - assertEquals(10, result.getUsersKeycloakCount()); - } - - @Test - void testCheckDataConsistency_Exception() { - when(syncService.checkDataConsistency(REALM)).thenThrow(new RuntimeException("DB error")); - - var result = syncResource.checkDataConsistency(REALM); - - assertNotNull(result); - assertEquals("ERROR", result.getStatus()); - assertEquals(REALM, result.getRealmName()); - assertEquals("DB error", result.getError()); - } - - @Test - void testGetLastSyncStatus() { - var result = syncResource.getLastSyncStatus(REALM); - - assertNotNull(result); - assertEquals(REALM, result.getRealmName()); - assertEquals("NEVER_SYNCED", result.getStatus()); - } - - @Test - void testForceSyncRealm_Success() { - when(syncService.forceSyncRealm(REALM)).thenReturn(Map.of()); - - var result = syncResource.forceSyncRealm(REALM); - - assertNotNull(result); - assertEquals("SUCCESS", result.getStatus()); - assertEquals(REALM, result.getRealmName()); - } - - @Test - void testForceSyncRealm_Exception() { - doThrow(new RuntimeException("Force sync failed")).when(syncService).forceSyncRealm(REALM); - - var result = syncResource.forceSyncRealm(REALM); - - assertNotNull(result); - assertEquals("FAILED", result.getStatus()); - assertEquals(REALM, result.getRealmName()); - } -} +package dev.lions.user.manager.resource; + +import dev.lions.user.manager.api.SyncResourceApi; +import dev.lions.user.manager.dto.sync.HealthStatusDTO; +import dev.lions.user.manager.dto.sync.SyncConsistencyDTO; +import dev.lions.user.manager.dto.sync.SyncHistoryDTO; +import dev.lions.user.manager.dto.sync.SyncResultDTO; +import dev.lions.user.manager.service.SyncService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SyncResourceTest { + + @Mock + SyncService syncService; + + @InjectMocks + SyncResource syncResource; + + private static final String REALM = "test-realm"; + + @Test + void testCheckKeycloakHealth() { + when(syncService.isKeycloakAvailable()).thenReturn(true); + when(syncService.getKeycloakHealthInfo()).thenReturn(Map.of("version", "23.0.0")); + + HealthStatusDTO status = syncResource.checkKeycloakHealth(); + + assertTrue(status.isKeycloakAccessible()); + assertTrue(status.isOverallHealthy()); + assertEquals("23.0.0", status.getKeycloakVersion()); + } + + @Test + void testCheckKeycloakHealthError() { + when(syncService.isKeycloakAvailable()).thenThrow(new RuntimeException("Connection refused")); + + HealthStatusDTO status = syncResource.checkKeycloakHealth(); + + assertFalse(status.isOverallHealthy()); + assertTrue(status.getErrorMessage().contains("Connection refused")); + } + + @Test + void testSyncUsers() { + when(syncService.syncUsersFromRealm(REALM)).thenReturn(10); + + SyncResultDTO result = syncResource.syncUsers(REALM); + + assertTrue(result.isSuccess()); + assertEquals(10, result.getUsersCount()); + assertEquals(REALM, result.getRealmName()); + } + + @Test + void testSyncUsersError() { + when(syncService.syncUsersFromRealm(REALM)).thenThrow(new RuntimeException("Sync failed")); + + SyncResultDTO result = syncResource.syncUsers(REALM); + + assertFalse(result.isSuccess()); + assertEquals("Sync failed", result.getErrorMessage()); + } + + @Test + void testSyncRoles() { + when(syncService.syncRolesFromRealm(REALM)).thenReturn(5); + + SyncResultDTO result = syncResource.syncRoles(REALM, null); + + assertTrue(result.isSuccess()); + assertEquals(5, result.getRealmRolesCount()); + } + + @Test + void testSyncRolesError() { + when(syncService.syncRolesFromRealm(REALM)).thenThrow(new RuntimeException("Roles sync failed")); + + SyncResultDTO result = syncResource.syncRoles(REALM, null); + + assertFalse(result.isSuccess()); + assertEquals("Roles sync failed", result.getErrorMessage()); + } + + @Test + void testPing() { + String response = syncResource.ping(); + + assertNotNull(response); + assertTrue(response.contains("pong")); + assertTrue(response.contains("SyncResource")); + } + + @Test + void testCheckDataConsistency_Success() { + when(syncService.checkDataConsistency(REALM)).thenReturn(Map.of( + "realmName", REALM, + "status", "OK", + "usersKeycloakCount", 10, + "usersLocalCount", 10 + )); + + var result = syncResource.checkDataConsistency(REALM); + + assertNotNull(result); + assertEquals(REALM, result.getRealmName()); + assertEquals("OK", result.getStatus()); + assertEquals(10, result.getUsersKeycloakCount()); + } + + @Test + void testCheckDataConsistency_Exception() { + when(syncService.checkDataConsistency(REALM)).thenThrow(new RuntimeException("DB error")); + + var result = syncResource.checkDataConsistency(REALM); + + assertNotNull(result); + assertEquals("ERROR", result.getStatus()); + assertEquals(REALM, result.getRealmName()); + assertEquals("DB error", result.getError()); + } + + @Test + void testGetLastSyncStatus() { + var result = syncResource.getLastSyncStatus(REALM); + + assertNotNull(result); + assertEquals(REALM, result.getRealmName()); + assertEquals("NEVER_SYNCED", result.getStatus()); + } + + @Test + void testForceSyncRealm_Success() { + when(syncService.forceSyncRealm(REALM)).thenReturn(Map.of()); + + var result = syncResource.forceSyncRealm(REALM); + + assertNotNull(result); + assertEquals("SUCCESS", result.getStatus()); + assertEquals(REALM, result.getRealmName()); + } + + @Test + void testForceSyncRealm_Exception() { + doThrow(new RuntimeException("Force sync failed")).when(syncService).forceSyncRealm(REALM); + + var result = syncResource.forceSyncRealm(REALM); + + assertNotNull(result); + assertEquals("FAILED", result.getStatus()); + assertEquals(REALM, result.getRealmName()); + } +} diff --git a/src/test/java/dev/lions/user/manager/resource/UserMetricsResourceTest.java b/src/test/java/dev/lions/user/manager/resource/UserMetricsResourceTest.java index 3e62f10..bdcb07b 100644 --- a/src/test/java/dev/lions/user/manager/resource/UserMetricsResourceTest.java +++ b/src/test/java/dev/lions/user/manager/resource/UserMetricsResourceTest.java @@ -1,95 +1,95 @@ -package dev.lions.user.manager.resource; - -import dev.lions.user.manager.client.KeycloakAdminClient; -import dev.lions.user.manager.dto.common.UserSessionStatsDTO; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.keycloak.admin.client.resource.RealmResource; -import org.keycloak.admin.client.resource.UserResource; -import org.keycloak.admin.client.resource.UsersResource; -import org.keycloak.representations.idm.UserRepresentation; -import org.junit.jupiter.api.Assertions; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class UserMetricsResourceTest { - - @Mock - KeycloakAdminClient keycloakAdminClient; - - @Mock - RealmResource realmResource; - - @Mock - UsersResource usersResource; - - @Mock - UserResource userResource1; - - @Mock - UserResource userResource2; - - @InjectMocks - UserMetricsResource userMetricsResource; - - @Test - void testGetUserSessionStats() { - // Préparer deux utilisateurs avec des sessions différentes - UserRepresentation u1 = new UserRepresentation(); - u1.setId("u1"); - UserRepresentation u2 = new UserRepresentation(); - u2.setId("u2"); - - when(keycloakAdminClient.getRealm("test-realm")).thenReturn(realmResource); - when(realmResource.users()).thenReturn(usersResource); - when(usersResource.list()).thenReturn(List.of(u1, u2)); - - // u1 a 2 sessions, u2 en a 0 - when(usersResource.get("u1")).thenReturn(userResource1); - when(usersResource.get("u2")).thenReturn(userResource2); - when(userResource1.getUserSessions()).thenReturn(List.of(new org.keycloak.representations.idm.UserSessionRepresentation(), - new org.keycloak.representations.idm.UserSessionRepresentation())); - when(userResource2.getUserSessions()).thenReturn(List.of()); - - UserSessionStatsDTO stats = userMetricsResource.getUserSessionStats("test-realm"); - - assertNotNull(stats); - assertEquals("test-realm", stats.getRealmName()); - assertEquals(2L, stats.getTotalUsers()); - assertEquals(2L, stats.getActiveSessions()); // 2 sessions au total - assertEquals(1L, stats.getOnlineUsers()); // 1 utilisateur avec au moins une session - } - - @Test - void testGetUserSessionStats_DefaultRealm() { - when(keycloakAdminClient.getRealm("master")).thenReturn(realmResource); - when(realmResource.users()).thenReturn(usersResource); - when(usersResource.list()).thenReturn(List.of()); - - UserSessionStatsDTO stats = userMetricsResource.getUserSessionStats(null); - - assertNotNull(stats); - assertEquals("master", stats.getRealmName()); - assertEquals(0L, stats.getTotalUsers()); - } - - @Test - void testGetUserSessionStats_OnError() { - when(keycloakAdminClient.getRealm(anyString())) - .thenThrow(new RuntimeException("KC error")); - - Assertions.assertThrows(RuntimeException.class, - () -> userMetricsResource.getUserSessionStats("realm")); - } -} - - +package dev.lions.user.manager.resource; + +import dev.lions.user.manager.client.KeycloakAdminClient; +import dev.lions.user.manager.dto.common.UserSessionStatsDTO; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.representations.idm.UserRepresentation; +import org.junit.jupiter.api.Assertions; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class UserMetricsResourceTest { + + @Mock + KeycloakAdminClient keycloakAdminClient; + + @Mock + RealmResource realmResource; + + @Mock + UsersResource usersResource; + + @Mock + UserResource userResource1; + + @Mock + UserResource userResource2; + + @InjectMocks + UserMetricsResource userMetricsResource; + + @Test + void testGetUserSessionStats() { + // Préparer deux utilisateurs avec des sessions différentes + UserRepresentation u1 = new UserRepresentation(); + u1.setId("u1"); + UserRepresentation u2 = new UserRepresentation(); + u2.setId("u2"); + + when(keycloakAdminClient.getRealm("test-realm")).thenReturn(realmResource); + when(realmResource.users()).thenReturn(usersResource); + when(usersResource.list()).thenReturn(List.of(u1, u2)); + + // u1 a 2 sessions, u2 en a 0 + when(usersResource.get("u1")).thenReturn(userResource1); + when(usersResource.get("u2")).thenReturn(userResource2); + when(userResource1.getUserSessions()).thenReturn(List.of(new org.keycloak.representations.idm.UserSessionRepresentation(), + new org.keycloak.representations.idm.UserSessionRepresentation())); + when(userResource2.getUserSessions()).thenReturn(List.of()); + + UserSessionStatsDTO stats = userMetricsResource.getUserSessionStats("test-realm"); + + assertNotNull(stats); + assertEquals("test-realm", stats.getRealmName()); + assertEquals(2L, stats.getTotalUsers()); + assertEquals(2L, stats.getActiveSessions()); // 2 sessions au total + assertEquals(1L, stats.getOnlineUsers()); // 1 utilisateur avec au moins une session + } + + @Test + void testGetUserSessionStats_DefaultRealm() { + when(keycloakAdminClient.getRealm("master")).thenReturn(realmResource); + when(realmResource.users()).thenReturn(usersResource); + when(usersResource.list()).thenReturn(List.of()); + + UserSessionStatsDTO stats = userMetricsResource.getUserSessionStats(null); + + assertNotNull(stats); + assertEquals("master", stats.getRealmName()); + assertEquals(0L, stats.getTotalUsers()); + } + + @Test + void testGetUserSessionStats_OnError() { + when(keycloakAdminClient.getRealm(anyString())) + .thenThrow(new RuntimeException("KC error")); + + Assertions.assertThrows(RuntimeException.class, + () -> userMetricsResource.getUserSessionStats("realm")); + } +} + + diff --git a/src/test/java/dev/lions/user/manager/resource/UserResourceTest.java b/src/test/java/dev/lions/user/manager/resource/UserResourceTest.java index 7f1c3c0..4b3ce6f 100644 --- a/src/test/java/dev/lions/user/manager/resource/UserResourceTest.java +++ b/src/test/java/dev/lions/user/manager/resource/UserResourceTest.java @@ -1,243 +1,243 @@ -package dev.lions.user.manager.resource; - -import dev.lions.user.manager.dto.importexport.ImportResultDTO; -import dev.lions.user.manager.dto.user.*; -import dev.lions.user.manager.service.UserService; -import jakarta.ws.rs.core.Response; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class UserResourceTest { - - @Mock - UserService userService; - - @InjectMocks - UserResource userResource; - - private static final String REALM = "test-realm"; - - @Test - void testSearchUsers() { - UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder() - .realmName(REALM) - .searchTerm("test") - .page(0) - .pageSize(20) - .build(); - - UserSearchResultDTO mockResult = UserSearchResultDTO.builder() - .users(Collections.singletonList(UserDTO.builder().username("test").build())) - .totalCount(1L) - .build(); - - when(userService.searchUsers(any())).thenReturn(mockResult); - - UserSearchResultDTO result = userResource.searchUsers(criteria); - - assertNotNull(result); - assertEquals(1, result.getTotalCount()); - } - - @Test - void testGetUserById() { - UserDTO user = UserDTO.builder().id("1").username("testuser").build(); - when(userService.getUserById("1", REALM)).thenReturn(Optional.of(user)); - - UserDTO result = userResource.getUserById("1", REALM); - - assertNotNull(result); - assertEquals(user, result); - } - - @Test - void testGetUserByIdNotFound() { - when(userService.getUserById("1", REALM)).thenReturn(Optional.empty()); - - assertThrows(RuntimeException.class, () -> userResource.getUserById("1", REALM)); - } - - @Test - void testGetAllUsers() { - UserSearchResultDTO mockResult = UserSearchResultDTO.builder() - .users(Collections.emptyList()) - .totalCount(0L) - .build(); - when(userService.getAllUsers(REALM, 0, 20)).thenReturn(mockResult); - - UserSearchResultDTO result = userResource.getAllUsers(REALM, 0, 20); - - assertNotNull(result); - assertEquals(0, result.getTotalCount()); - } - - @Test - void testCreateUser() { - UserDTO newUser = UserDTO.builder().username("newuser").email("new@test.com").build(); - UserDTO createdUser = UserDTO.builder().id("123").username("newuser").email("new@test.com").build(); - - when(userService.createUser(any(), eq(REALM))).thenReturn(createdUser); - - Response response = userResource.createUser(newUser, REALM); - - assertEquals(201, response.getStatus()); - assertEquals(createdUser, response.getEntity()); - } - - @Test - void testUpdateUser() { - UserDTO updateUser = UserDTO.builder() - .username("updated") - .prenom("John") - .nom("Doe") - .email("john.doe@test.com") - .build(); - UserDTO updatedUser = UserDTO.builder() - .id("1") - .username("updated") - .prenom("John") - .nom("Doe") - .email("john.doe@test.com") - .build(); - - when(userService.updateUser(eq("1"), any(), eq(REALM))).thenReturn(updatedUser); - - UserDTO result = userResource.updateUser("1", updateUser, REALM); - - assertNotNull(result); - assertEquals(updatedUser, result); - } - - @Test - void testDeleteUser() { - doNothing().when(userService).deleteUser("1", REALM, false); - - userResource.deleteUser("1", REALM, false); - - verify(userService).deleteUser("1", REALM, false); - } - - @Test - void testActivateUser() { - doNothing().when(userService).activateUser("1", REALM); - - userResource.activateUser("1", REALM); - - verify(userService).activateUser("1", REALM); - } - - @Test - void testDeactivateUser() { - doNothing().when(userService).deactivateUser("1", REALM, "reason"); - - userResource.deactivateUser("1", REALM, "reason"); - - verify(userService).deactivateUser("1", REALM, "reason"); - } - - @Test - void testResetPassword() { - doNothing().when(userService).resetPassword("1", REALM, "newpassword", true); - - PasswordResetRequestDTO request = PasswordResetRequestDTO.builder() - .password("newpassword") - .temporary(true) - .build(); - - userResource.resetPassword("1", REALM, request); - - verify(userService).resetPassword("1", REALM, "newpassword", true); - } - - @Test - void testSendVerificationEmail() { - doNothing().when(userService).sendVerificationEmail("1", REALM); - - Response response = userResource.sendVerificationEmail("1", REALM); - - verify(userService).sendVerificationEmail("1", REALM); - assertNotNull(response); - assertEquals(202, response.getStatus()); - } - - @Test - void testLogoutAllSessions() { - when(userService.logoutAllSessions("1", REALM)).thenReturn(5); - - SessionsRevokedDTO result = userResource.logoutAllSessions("1", REALM); - - assertNotNull(result); - assertEquals(5, result.getCount()); - } - - @Test - void testGetActiveSessions() { - when(userService.getActiveSessions("1", REALM)).thenReturn(Collections.singletonList("session-1")); - - List result = userResource.getActiveSessions("1", REALM); - - assertNotNull(result); - assertEquals(1, result.size()); - assertEquals("session-1", result.get(0)); - } - - @Test - void testCreateUser_IllegalArgumentException() { - UserDTO newUser = UserDTO.builder().username("existinguser").email("existing@test.com").build(); - when(userService.createUser(any(), eq(REALM))).thenThrow(new IllegalArgumentException("Username exists")); - - Response response = userResource.createUser(newUser, REALM); - - assertEquals(409, response.getStatus()); - } - - @Test - void testCreateUser_RuntimeException() { - UserDTO newUser = UserDTO.builder().username("user").email("user@test.com").build(); - when(userService.createUser(any(), eq(REALM))).thenThrow(new RuntimeException("Connection error")); - - assertThrows(RuntimeException.class, () -> userResource.createUser(newUser, REALM)); - } - - @Test - void testExportUsersToCSV() { - String csvContent = "username,email,prenom,nom\ntest,test@test.com,Test,User"; - when(userService.exportUsersToCSV(any())).thenReturn(csvContent); - - Response response = userResource.exportUsersToCSV(REALM); - - assertEquals(200, response.getStatus()); - assertEquals(csvContent, response.getEntity()); - } - - @Test - void testImportUsersFromCSV() { - String csvContent = "username,email,prenom,nom\ntest,test@test.com,Test,User"; - ImportResultDTO importResult = ImportResultDTO.builder() - .successCount(1) - .errorCount(0) - .totalLines(2) - .errors(Collections.emptyList()) - .build(); - when(userService.importUsersFromCSV(csvContent, REALM)).thenReturn(importResult); - - ImportResultDTO result = userResource.importUsersFromCSV(REALM, csvContent); - - assertNotNull(result); - assertEquals(1, result.getSuccessCount()); - assertEquals(0, result.getErrorCount()); - } -} +package dev.lions.user.manager.resource; + +import dev.lions.user.manager.dto.importexport.ImportResultDTO; +import dev.lions.user.manager.dto.user.*; +import dev.lions.user.manager.service.UserService; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class UserResourceTest { + + @Mock + UserService userService; + + @InjectMocks + UserResource userResource; + + private static final String REALM = "test-realm"; + + @Test + void testSearchUsers() { + UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder() + .realmName(REALM) + .searchTerm("test") + .page(0) + .pageSize(20) + .build(); + + UserSearchResultDTO mockResult = UserSearchResultDTO.builder() + .users(Collections.singletonList(UserDTO.builder().username("test").build())) + .totalCount(1L) + .build(); + + when(userService.searchUsers(any())).thenReturn(mockResult); + + UserSearchResultDTO result = userResource.searchUsers(criteria); + + assertNotNull(result); + assertEquals(1, result.getTotalCount()); + } + + @Test + void testGetUserById() { + UserDTO user = UserDTO.builder().id("1").username("testuser").build(); + when(userService.getUserById("1", REALM)).thenReturn(Optional.of(user)); + + UserDTO result = userResource.getUserById("1", REALM); + + assertNotNull(result); + assertEquals(user, result); + } + + @Test + void testGetUserByIdNotFound() { + when(userService.getUserById("1", REALM)).thenReturn(Optional.empty()); + + assertThrows(RuntimeException.class, () -> userResource.getUserById("1", REALM)); + } + + @Test + void testGetAllUsers() { + UserSearchResultDTO mockResult = UserSearchResultDTO.builder() + .users(Collections.emptyList()) + .totalCount(0L) + .build(); + when(userService.getAllUsers(REALM, 0, 20)).thenReturn(mockResult); + + UserSearchResultDTO result = userResource.getAllUsers(REALM, 0, 20); + + assertNotNull(result); + assertEquals(0, result.getTotalCount()); + } + + @Test + void testCreateUser() { + UserDTO newUser = UserDTO.builder().username("newuser").email("new@test.com").build(); + UserDTO createdUser = UserDTO.builder().id("123").username("newuser").email("new@test.com").build(); + + when(userService.createUser(any(), eq(REALM))).thenReturn(createdUser); + + Response response = userResource.createUser(newUser, REALM); + + assertEquals(201, response.getStatus()); + assertEquals(createdUser, response.getEntity()); + } + + @Test + void testUpdateUser() { + UserDTO updateUser = UserDTO.builder() + .username("updated") + .prenom("John") + .nom("Doe") + .email("john.doe@test.com") + .build(); + UserDTO updatedUser = UserDTO.builder() + .id("1") + .username("updated") + .prenom("John") + .nom("Doe") + .email("john.doe@test.com") + .build(); + + when(userService.updateUser(eq("1"), any(), eq(REALM))).thenReturn(updatedUser); + + UserDTO result = userResource.updateUser("1", updateUser, REALM); + + assertNotNull(result); + assertEquals(updatedUser, result); + } + + @Test + void testDeleteUser() { + doNothing().when(userService).deleteUser("1", REALM, false); + + userResource.deleteUser("1", REALM, false); + + verify(userService).deleteUser("1", REALM, false); + } + + @Test + void testActivateUser() { + doNothing().when(userService).activateUser("1", REALM); + + userResource.activateUser("1", REALM); + + verify(userService).activateUser("1", REALM); + } + + @Test + void testDeactivateUser() { + doNothing().when(userService).deactivateUser("1", REALM, "reason"); + + userResource.deactivateUser("1", REALM, "reason"); + + verify(userService).deactivateUser("1", REALM, "reason"); + } + + @Test + void testResetPassword() { + doNothing().when(userService).resetPassword("1", REALM, "newpassword", true); + + PasswordResetRequestDTO request = PasswordResetRequestDTO.builder() + .password("newpassword") + .temporary(true) + .build(); + + userResource.resetPassword("1", REALM, request); + + verify(userService).resetPassword("1", REALM, "newpassword", true); + } + + @Test + void testSendVerificationEmail() { + doNothing().when(userService).sendVerificationEmail("1", REALM); + + Response response = userResource.sendVerificationEmail("1", REALM); + + verify(userService).sendVerificationEmail("1", REALM); + assertNotNull(response); + assertEquals(202, response.getStatus()); + } + + @Test + void testLogoutAllSessions() { + when(userService.logoutAllSessions("1", REALM)).thenReturn(5); + + SessionsRevokedDTO result = userResource.logoutAllSessions("1", REALM); + + assertNotNull(result); + assertEquals(5, result.getCount()); + } + + @Test + void testGetActiveSessions() { + when(userService.getActiveSessions("1", REALM)).thenReturn(Collections.singletonList("session-1")); + + List result = userResource.getActiveSessions("1", REALM); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("session-1", result.get(0)); + } + + @Test + void testCreateUser_IllegalArgumentException() { + UserDTO newUser = UserDTO.builder().username("existinguser").email("existing@test.com").build(); + when(userService.createUser(any(), eq(REALM))).thenThrow(new IllegalArgumentException("Username exists")); + + Response response = userResource.createUser(newUser, REALM); + + assertEquals(409, response.getStatus()); + } + + @Test + void testCreateUser_RuntimeException() { + UserDTO newUser = UserDTO.builder().username("user").email("user@test.com").build(); + when(userService.createUser(any(), eq(REALM))).thenThrow(new RuntimeException("Connection error")); + + assertThrows(RuntimeException.class, () -> userResource.createUser(newUser, REALM)); + } + + @Test + void testExportUsersToCSV() { + String csvContent = "username,email,prenom,nom\ntest,test@test.com,Test,User"; + when(userService.exportUsersToCSV(any())).thenReturn(csvContent); + + Response response = userResource.exportUsersToCSV(REALM); + + assertEquals(200, response.getStatus()); + assertEquals(csvContent, response.getEntity()); + } + + @Test + void testImportUsersFromCSV() { + String csvContent = "username,email,prenom,nom\ntest,test@test.com,Test,User"; + ImportResultDTO importResult = ImportResultDTO.builder() + .successCount(1) + .errorCount(0) + .totalLines(2) + .errors(Collections.emptyList()) + .build(); + when(userService.importUsersFromCSV(csvContent, REALM)).thenReturn(importResult); + + ImportResultDTO result = userResource.importUsersFromCSV(REALM, csvContent); + + assertNotNull(result); + assertEquals(1, result.getSuccessCount()); + assertEquals(0, result.getErrorCount()); + } +} diff --git a/src/test/java/dev/lions/user/manager/security/DevSecurityContextProducerTest.java b/src/test/java/dev/lions/user/manager/security/DevSecurityContextProducerTest.java index 5a38921..cd62b17 100644 --- a/src/test/java/dev/lions/user/manager/security/DevSecurityContextProducerTest.java +++ b/src/test/java/dev/lions/user/manager/security/DevSecurityContextProducerTest.java @@ -1,178 +1,178 @@ -package dev.lions.user.manager.security; - -import jakarta.ws.rs.container.ContainerRequestContext; -import jakarta.ws.rs.core.SecurityContext; -import jakarta.ws.rs.core.UriInfo; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.lang.reflect.Field; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -/** - * Tests unitaires pour DevSecurityContextProducer - */ -@ExtendWith(MockitoExtension.class) -class DevSecurityContextProducerTest { - - @Mock - private ContainerRequestContext requestContext; - - @Mock - private UriInfo uriInfo; - - @Mock - private SecurityContext originalSecurityContext; - - private DevSecurityContextProducer producer; - - @BeforeEach - void setUp() throws Exception { - producer = new DevSecurityContextProducer(); - - // Injecter les propriétés via reflection - setField("profile", "dev"); - setField("oidcEnabled", false); - } - - private void setField(String fieldName, Object value) throws Exception { - Field field = DevSecurityContextProducer.class.getDeclaredField(fieldName); - field.setAccessible(true); - field.set(producer, value); - } - - @Test - void testFilter_DevMode() throws Exception { - setField("profile", "dev"); - setField("oidcEnabled", true); - - when(requestContext.getUriInfo()).thenReturn(uriInfo); - when(uriInfo.getPath()).thenReturn("/api/users"); - when(requestContext.getSecurityContext()).thenReturn(originalSecurityContext); - - producer.filter(requestContext); - - verify(requestContext, times(1)).setSecurityContext(any(SecurityContext.class)); - } - - @Test - void testFilter_ProdMode() throws Exception { - setField("profile", "prod"); - setField("oidcEnabled", true); - - // En mode prod, on n'a pas besoin de mocker getUriInfo car le code ne l'utilise pas - producer.filter(requestContext); - - verify(requestContext, never()).setSecurityContext(any(SecurityContext.class)); - } - - @Test - void testFilter_OidcDisabled() throws Exception { - setField("profile", "prod"); - setField("oidcEnabled", false); - - when(requestContext.getUriInfo()).thenReturn(uriInfo); - when(uriInfo.getPath()).thenReturn("/api/users"); - when(requestContext.getSecurityContext()).thenReturn(originalSecurityContext); - - producer.filter(requestContext); - - verify(requestContext, times(1)).setSecurityContext(any(SecurityContext.class)); - } - - @Test - void testDevSecurityContext_GetUserPrincipal() throws Exception { - setField("profile", "dev"); - setField("oidcEnabled", false); - - when(requestContext.getUriInfo()).thenReturn(uriInfo); - when(uriInfo.getPath()).thenReturn("/api/test"); - when(requestContext.getSecurityContext()).thenReturn(originalSecurityContext); - - ArgumentCaptor captor = ArgumentCaptor.forClass(SecurityContext.class); - producer.filter(requestContext); - verify(requestContext).setSecurityContext(captor.capture()); - - SecurityContext devCtx = captor.getValue(); - assertNotNull(devCtx.getUserPrincipal()); - assertEquals("dev-user", devCtx.getUserPrincipal().getName()); - } - - @Test - void testDevSecurityContext_IsUserInRole() throws Exception { - setField("profile", "dev"); - setField("oidcEnabled", false); - - when(requestContext.getUriInfo()).thenReturn(uriInfo); - when(uriInfo.getPath()).thenReturn("/api/test"); - when(requestContext.getSecurityContext()).thenReturn(originalSecurityContext); - - ArgumentCaptor captor = ArgumentCaptor.forClass(SecurityContext.class); - producer.filter(requestContext); - verify(requestContext).setSecurityContext(captor.capture()); - - SecurityContext devCtx = captor.getValue(); - assertTrue(devCtx.isUserInRole("admin")); - assertTrue(devCtx.isUserInRole("user_manager")); - assertTrue(devCtx.isUserInRole("any_role")); - } - - @Test - void testDevSecurityContext_IsSecure_WithOriginal() throws Exception { - setField("profile", "dev"); - setField("oidcEnabled", false); - - when(requestContext.getUriInfo()).thenReturn(uriInfo); - when(uriInfo.getPath()).thenReturn("/api/test"); - when(requestContext.getSecurityContext()).thenReturn(originalSecurityContext); - when(originalSecurityContext.isSecure()).thenReturn(true); - - ArgumentCaptor captor = ArgumentCaptor.forClass(SecurityContext.class); - producer.filter(requestContext); - verify(requestContext).setSecurityContext(captor.capture()); - - SecurityContext devCtx = captor.getValue(); - assertTrue(devCtx.isSecure()); // delegates to original which returns true - } - - @Test - void testDevSecurityContext_IsSecure_WithNullOriginal() throws Exception { - setField("profile", "dev"); - setField("oidcEnabled", false); - - when(requestContext.getUriInfo()).thenReturn(uriInfo); - when(uriInfo.getPath()).thenReturn("/api/test"); - when(requestContext.getSecurityContext()).thenReturn(null); // null original - - ArgumentCaptor captor = ArgumentCaptor.forClass(SecurityContext.class); - producer.filter(requestContext); - verify(requestContext).setSecurityContext(captor.capture()); - - SecurityContext devCtx = captor.getValue(); - assertFalse(devCtx.isSecure()); // original is null → returns false - } - - @Test - void testDevSecurityContext_GetAuthenticationScheme() throws Exception { - setField("profile", "dev"); - setField("oidcEnabled", false); - - when(requestContext.getUriInfo()).thenReturn(uriInfo); - when(uriInfo.getPath()).thenReturn("/api/test"); - when(requestContext.getSecurityContext()).thenReturn(originalSecurityContext); - - ArgumentCaptor captor = ArgumentCaptor.forClass(SecurityContext.class); - producer.filter(requestContext); - verify(requestContext).setSecurityContext(captor.capture()); - - SecurityContext devCtx = captor.getValue(); - assertEquals("DEV", devCtx.getAuthenticationScheme()); - } -} - +package dev.lions.user.manager.security; + +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.core.SecurityContext; +import jakarta.ws.rs.core.UriInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Tests unitaires pour DevSecurityContextProducer + */ +@ExtendWith(MockitoExtension.class) +class DevSecurityContextProducerTest { + + @Mock + private ContainerRequestContext requestContext; + + @Mock + private UriInfo uriInfo; + + @Mock + private SecurityContext originalSecurityContext; + + private DevSecurityContextProducer producer; + + @BeforeEach + void setUp() throws Exception { + producer = new DevSecurityContextProducer(); + + // Injecter les propriétés via reflection + setField("profile", "dev"); + setField("oidcEnabled", false); + } + + private void setField(String fieldName, Object value) throws Exception { + Field field = DevSecurityContextProducer.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(producer, value); + } + + @Test + void testFilter_DevMode() throws Exception { + setField("profile", "dev"); + setField("oidcEnabled", true); + + when(requestContext.getUriInfo()).thenReturn(uriInfo); + when(uriInfo.getPath()).thenReturn("/api/users"); + when(requestContext.getSecurityContext()).thenReturn(originalSecurityContext); + + producer.filter(requestContext); + + verify(requestContext, times(1)).setSecurityContext(any(SecurityContext.class)); + } + + @Test + void testFilter_ProdMode() throws Exception { + setField("profile", "prod"); + setField("oidcEnabled", true); + + // En mode prod, on n'a pas besoin de mocker getUriInfo car le code ne l'utilise pas + producer.filter(requestContext); + + verify(requestContext, never()).setSecurityContext(any(SecurityContext.class)); + } + + @Test + void testFilter_OidcDisabled() throws Exception { + setField("profile", "prod"); + setField("oidcEnabled", false); + + when(requestContext.getUriInfo()).thenReturn(uriInfo); + when(uriInfo.getPath()).thenReturn("/api/users"); + when(requestContext.getSecurityContext()).thenReturn(originalSecurityContext); + + producer.filter(requestContext); + + verify(requestContext, times(1)).setSecurityContext(any(SecurityContext.class)); + } + + @Test + void testDevSecurityContext_GetUserPrincipal() throws Exception { + setField("profile", "dev"); + setField("oidcEnabled", false); + + when(requestContext.getUriInfo()).thenReturn(uriInfo); + when(uriInfo.getPath()).thenReturn("/api/test"); + when(requestContext.getSecurityContext()).thenReturn(originalSecurityContext); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SecurityContext.class); + producer.filter(requestContext); + verify(requestContext).setSecurityContext(captor.capture()); + + SecurityContext devCtx = captor.getValue(); + assertNotNull(devCtx.getUserPrincipal()); + assertEquals("dev-user", devCtx.getUserPrincipal().getName()); + } + + @Test + void testDevSecurityContext_IsUserInRole() throws Exception { + setField("profile", "dev"); + setField("oidcEnabled", false); + + when(requestContext.getUriInfo()).thenReturn(uriInfo); + when(uriInfo.getPath()).thenReturn("/api/test"); + when(requestContext.getSecurityContext()).thenReturn(originalSecurityContext); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SecurityContext.class); + producer.filter(requestContext); + verify(requestContext).setSecurityContext(captor.capture()); + + SecurityContext devCtx = captor.getValue(); + assertTrue(devCtx.isUserInRole("admin")); + assertTrue(devCtx.isUserInRole("user_manager")); + assertTrue(devCtx.isUserInRole("any_role")); + } + + @Test + void testDevSecurityContext_IsSecure_WithOriginal() throws Exception { + setField("profile", "dev"); + setField("oidcEnabled", false); + + when(requestContext.getUriInfo()).thenReturn(uriInfo); + when(uriInfo.getPath()).thenReturn("/api/test"); + when(requestContext.getSecurityContext()).thenReturn(originalSecurityContext); + when(originalSecurityContext.isSecure()).thenReturn(true); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SecurityContext.class); + producer.filter(requestContext); + verify(requestContext).setSecurityContext(captor.capture()); + + SecurityContext devCtx = captor.getValue(); + assertTrue(devCtx.isSecure()); // delegates to original which returns true + } + + @Test + void testDevSecurityContext_IsSecure_WithNullOriginal() throws Exception { + setField("profile", "dev"); + setField("oidcEnabled", false); + + when(requestContext.getUriInfo()).thenReturn(uriInfo); + when(uriInfo.getPath()).thenReturn("/api/test"); + when(requestContext.getSecurityContext()).thenReturn(null); // null original + + ArgumentCaptor captor = ArgumentCaptor.forClass(SecurityContext.class); + producer.filter(requestContext); + verify(requestContext).setSecurityContext(captor.capture()); + + SecurityContext devCtx = captor.getValue(); + assertFalse(devCtx.isSecure()); // original is null → returns false + } + + @Test + void testDevSecurityContext_GetAuthenticationScheme() throws Exception { + setField("profile", "dev"); + setField("oidcEnabled", false); + + when(requestContext.getUriInfo()).thenReturn(uriInfo); + when(uriInfo.getPath()).thenReturn("/api/test"); + when(requestContext.getSecurityContext()).thenReturn(originalSecurityContext); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SecurityContext.class); + producer.filter(requestContext); + verify(requestContext).setSecurityContext(captor.capture()); + + SecurityContext devCtx = captor.getValue(); + assertEquals("DEV", devCtx.getAuthenticationScheme()); + } +} + diff --git a/src/test/java/dev/lions/user/manager/service/impl/AuditServiceImplTest.java b/src/test/java/dev/lions/user/manager/service/impl/AuditServiceImplTest.java index e35bd39..9f98ad3 100644 --- a/src/test/java/dev/lions/user/manager/service/impl/AuditServiceImplTest.java +++ b/src/test/java/dev/lions/user/manager/service/impl/AuditServiceImplTest.java @@ -1,118 +1,118 @@ -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.server.impl.repository.AuditLogRepository; -import io.quarkus.hibernate.orm.panache.PanacheQuery; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; - -import java.time.LocalDateTime; -import java.util.Collections; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) -class AuditServiceImplTest { - - @Mock - AuditLogRepository auditLogRepository; - - @Mock - AuditLogMapper auditLogMapper; - - @InjectMocks - AuditServiceImpl auditService; - - @BeforeEach - void setUp() { - auditService.auditEnabled = true; - auditService.logToDatabase = true; - } - - @Test - void testLogAction() { - AuditLogDTO log = new AuditLogDTO(); - log.setTypeAction(TypeActionAudit.USER_CREATE); - log.setActeurUsername("admin"); - - when(auditLogMapper.toEntity(any(AuditLogDTO.class))).thenReturn(new AuditLogEntity()); - - auditService.logAction(log); - - verify(auditLogRepository).persist(any(AuditLogEntity.class)); - } - - @Test - void testLogDisabled() { - auditService.auditEnabled = false; - AuditLogDTO log = new AuditLogDTO(); - - auditService.logAction(log); - - verify(auditLogRepository, never()).persist(any(AuditLogEntity.class)); - } - - @Test - void testLogSuccess() { - when(auditLogMapper.toEntity(any(AuditLogDTO.class))).thenReturn(new AuditLogEntity()); - - auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", "1", "user", "realm", "admin", "desc"); - - verify(auditLogRepository).persist(any(AuditLogEntity.class)); - } - - @Test - void testLogFailure() { - when(auditLogMapper.toEntity(any(AuditLogDTO.class))).thenReturn(new AuditLogEntity()); - - auditService.logFailure(TypeActionAudit.USER_CREATE, "USER", "1", "user", "realm", "admin", "ERR", "Error"); - - verify(auditLogRepository).persist(any(AuditLogEntity.class)); - - // Test findFailures mock logic - when(auditLogRepository.search(anyString(), any(), any(), any(), any(), eq(false), anyInt(), anyInt())) - .thenReturn(Collections.singletonList(new AuditLogEntity())); - when(auditLogMapper.toDTOList(anyList())).thenReturn(Collections.singletonList(new AuditLogDTO())); - - List failures = auditService.findFailures("realm", null, null, 0, 10); - assertEquals(1, failures.size()); - } - - @Test - void testSearchLogs() { - // Mocking repo results - when(auditLogRepository.search(any(), anyString(), any(), any(), any(), any(), anyInt(), anyInt())) - .thenReturn(Collections.singletonList(new AuditLogEntity())); - when(auditLogMapper.toDTOList(anyList())).thenReturn(Collections.singletonList(new AuditLogDTO())); - - List byActeur = auditService.findByActeur("admin1", null, null, 0, 10); - assertNotNull(byActeur); - assertFalse(byActeur.isEmpty()); - - when(auditLogRepository.search(anyString(), any(), any(), any(), anyString(), any(), anyInt(), anyInt())) - .thenReturn(Collections.singletonList(new AuditLogEntity())); - - List byType = auditService.findByTypeAction(TypeActionAudit.ROLE_CREATE, "realm", null, null, 0, - 10); - assertNotNull(byType); - } - - @Test - void testClearAll() { - auditService.clearAll(); - verify(auditLogRepository).deleteAll(); - } -} +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.server.impl.repository.AuditLogRepository; +import io.quarkus.hibernate.orm.panache.PanacheQuery; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class AuditServiceImplTest { + + @Mock + AuditLogRepository auditLogRepository; + + @Mock + AuditLogMapper auditLogMapper; + + @InjectMocks + AuditServiceImpl auditService; + + @BeforeEach + void setUp() { + auditService.auditEnabled = true; + auditService.logToDatabase = true; + } + + @Test + void testLogAction() { + AuditLogDTO log = new AuditLogDTO(); + log.setTypeAction(TypeActionAudit.USER_CREATE); + log.setActeurUsername("admin"); + + when(auditLogMapper.toEntity(any(AuditLogDTO.class))).thenReturn(new AuditLogEntity()); + + auditService.logAction(log); + + verify(auditLogRepository).persist(any(AuditLogEntity.class)); + } + + @Test + void testLogDisabled() { + auditService.auditEnabled = false; + AuditLogDTO log = new AuditLogDTO(); + + auditService.logAction(log); + + verify(auditLogRepository, never()).persist(any(AuditLogEntity.class)); + } + + @Test + void testLogSuccess() { + when(auditLogMapper.toEntity(any(AuditLogDTO.class))).thenReturn(new AuditLogEntity()); + + auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", "1", "user", "realm", "admin", "desc"); + + verify(auditLogRepository).persist(any(AuditLogEntity.class)); + } + + @Test + void testLogFailure() { + when(auditLogMapper.toEntity(any(AuditLogDTO.class))).thenReturn(new AuditLogEntity()); + + auditService.logFailure(TypeActionAudit.USER_CREATE, "USER", "1", "user", "realm", "admin", "ERR", "Error"); + + verify(auditLogRepository).persist(any(AuditLogEntity.class)); + + // Test findFailures mock logic + when(auditLogRepository.search(anyString(), any(), any(), any(), any(), eq(false), anyInt(), anyInt())) + .thenReturn(Collections.singletonList(new AuditLogEntity())); + when(auditLogMapper.toDTOList(anyList())).thenReturn(Collections.singletonList(new AuditLogDTO())); + + List failures = auditService.findFailures("realm", null, null, 0, 10); + assertEquals(1, failures.size()); + } + + @Test + void testSearchLogs() { + // Mocking repo results + when(auditLogRepository.search(any(), anyString(), any(), any(), any(), any(), anyInt(), anyInt())) + .thenReturn(Collections.singletonList(new AuditLogEntity())); + when(auditLogMapper.toDTOList(anyList())).thenReturn(Collections.singletonList(new AuditLogDTO())); + + List byActeur = auditService.findByActeur("admin1", null, null, 0, 10); + assertNotNull(byActeur); + assertFalse(byActeur.isEmpty()); + + when(auditLogRepository.search(anyString(), any(), any(), any(), anyString(), any(), anyInt(), anyInt())) + .thenReturn(Collections.singletonList(new AuditLogEntity())); + + List byType = auditService.findByTypeAction(TypeActionAudit.ROLE_CREATE, "realm", null, null, 0, + 10); + assertNotNull(byType); + } + + @Test + void testClearAll() { + auditService.clearAll(); + verify(auditLogRepository).deleteAll(); + } +} diff --git a/src/test/java/dev/lions/user/manager/service/impl/RealmAuthorizationServiceImplTest.java b/src/test/java/dev/lions/user/manager/service/impl/RealmAuthorizationServiceImplTest.java index e8eea10..6667ba7 100644 --- a/src/test/java/dev/lions/user/manager/service/impl/RealmAuthorizationServiceImplTest.java +++ b/src/test/java/dev/lions/user/manager/service/impl/RealmAuthorizationServiceImplTest.java @@ -1,280 +1,280 @@ -package dev.lions.user.manager.service.impl; - -import dev.lions.user.manager.dto.realm.RealmAssignmentDTO; -import dev.lions.user.manager.enums.audit.TypeActionAudit; -import dev.lions.user.manager.service.AuditService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -/** - * Tests unitaires pour RealmAuthorizationServiceImpl - */ -@ExtendWith(MockitoExtension.class) -class RealmAuthorizationServiceImplTest { - - @Mock - private AuditService auditService; - - @InjectMocks - private RealmAuthorizationServiceImpl realmAuthorizationService; - - private RealmAssignmentDTO assignment; - - @BeforeEach - void setUp() { - assignment = RealmAssignmentDTO.builder() - .id("assignment-1") - .userId("user-1") - .username("testuser") - .email("test@example.com") - .realmName("realm1") - .isSuperAdmin(false) - .active(true) - .assignedAt(LocalDateTime.now()) - .assignedBy("admin") - .build(); - } - - @Test - void testGetAllAssignments_Empty() { - List assignments = realmAuthorizationService.getAllAssignments(); - assertTrue(assignments.isEmpty()); - } - - @Test - void testGetAllAssignments_WithAssignments() { - realmAuthorizationService.assignRealmToUser(assignment); - List assignments = realmAuthorizationService.getAllAssignments(); - assertEquals(1, assignments.size()); - assertEquals("assignment-1", assignments.get(0).getId()); - } - - @Test - void testGetAssignmentsByUser_Success() { - realmAuthorizationService.assignRealmToUser(assignment); - List assignments = realmAuthorizationService.getAssignmentsByUser("user-1"); - assertEquals(1, assignments.size()); - } - - @Test - void testGetAssignmentsByUser_Empty() { - List assignments = realmAuthorizationService.getAssignmentsByUser("user-1"); - assertTrue(assignments.isEmpty()); - } - - @Test - void testGetAssignmentsByRealm_Success() { - realmAuthorizationService.assignRealmToUser(assignment); - List assignments = realmAuthorizationService.getAssignmentsByRealm("realm1"); - assertEquals(1, assignments.size()); - } - - @Test - void testGetAssignmentById_Success() { - realmAuthorizationService.assignRealmToUser(assignment); - Optional found = realmAuthorizationService.getAssignmentById("assignment-1"); - assertTrue(found.isPresent()); - assertEquals("assignment-1", found.get().getId()); - } - - @Test - void testGetAssignmentById_NotFound() { - Optional found = realmAuthorizationService.getAssignmentById("non-existent"); - assertFalse(found.isPresent()); - } - - @Test - void testCanManageRealm_SuperAdmin() { - realmAuthorizationService.setSuperAdmin("user-1", true); - assertTrue(realmAuthorizationService.canManageRealm("user-1", "any-realm")); - } - - @Test - void testCanManageRealm_WithAssignment() { - realmAuthorizationService.assignRealmToUser(assignment); - assertTrue(realmAuthorizationService.canManageRealm("user-1", "realm1")); - } - - @Test - void testCanManageRealm_NoAccess() { - assertFalse(realmAuthorizationService.canManageRealm("user-1", "realm1")); - } - - @Test - void testIsSuperAdmin_True() { - realmAuthorizationService.setSuperAdmin("user-1", true); - assertTrue(realmAuthorizationService.isSuperAdmin("user-1")); - } - - @Test - void testIsSuperAdmin_False() { - assertFalse(realmAuthorizationService.isSuperAdmin("user-1")); - } - - @Test - void testGetAuthorizedRealms_SuperAdmin() { - realmAuthorizationService.setSuperAdmin("user-1", true); - List realms = realmAuthorizationService.getAuthorizedRealms("user-1"); - assertTrue(realms.isEmpty()); // Super admin retourne liste vide - } - - @Test - void testGetAuthorizedRealms_WithAssignments() { - realmAuthorizationService.assignRealmToUser(assignment); - List realms = realmAuthorizationService.getAuthorizedRealms("user-1"); - assertEquals(1, realms.size()); - assertEquals("realm1", realms.get(0)); - } - - @Test - void testAssignRealmToUser_Success() { - doNothing().when(auditService).logSuccess( - any(TypeActionAudit.class), - anyString(), - anyString(), - anyString(), - anyString(), - anyString(), - anyString() - ); - - RealmAssignmentDTO result = realmAuthorizationService.assignRealmToUser(assignment); - assertNotNull(result); - assertNotNull(result.getId()); - assertTrue(result.isActive()); - assertNotNull(result.getAssignedAt()); - } - - @Test - void testAssignRealmToUser_NoUserId() { - assignment.setUserId(null); - assertThrows(IllegalArgumentException.class, () -> { - realmAuthorizationService.assignRealmToUser(assignment); - }); - } - - @Test - void testAssignRealmToUser_NoRealmName() { - assignment.setRealmName(null); - assertThrows(IllegalArgumentException.class, () -> { - realmAuthorizationService.assignRealmToUser(assignment); - }); - } - - @Test - void testAssignRealmToUser_Duplicate() { - doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); - realmAuthorizationService.assignRealmToUser(assignment); - assertThrows(IllegalArgumentException.class, () -> { - realmAuthorizationService.assignRealmToUser(assignment); - }); - } - - @Test - void testRevokeRealmFromUser_Success() { - doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); - realmAuthorizationService.assignRealmToUser(assignment); - realmAuthorizationService.revokeRealmFromUser("user-1", "realm1"); - assertFalse(realmAuthorizationService.canManageRealm("user-1", "realm1")); - } - - @Test - void testRevokeRealmFromUser_NotExists() { - // Ne doit pas lever d'exception si l'assignation n'existe pas - assertDoesNotThrow(() -> { - realmAuthorizationService.revokeRealmFromUser("user-1", "realm1"); - }); - } - - @Test - void testRevokeAllRealmsFromUser() { - doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); - realmAuthorizationService.assignRealmToUser(assignment); - realmAuthorizationService.revokeAllRealmsFromUser("user-1"); - assertTrue(realmAuthorizationService.getAssignmentsByUser("user-1").isEmpty()); - } - - @Test - void testSetSuperAdmin_True() { - doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); - realmAuthorizationService.setSuperAdmin("user-1", true); - assertTrue(realmAuthorizationService.isSuperAdmin("user-1")); - } - - @Test - void testSetSuperAdmin_False() { - doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); - realmAuthorizationService.setSuperAdmin("user-1", true); - realmAuthorizationService.setSuperAdmin("user-1", false); - assertFalse(realmAuthorizationService.isSuperAdmin("user-1")); - } - - @Test - void testDeactivateAssignment_Success() { - doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); - realmAuthorizationService.assignRealmToUser(assignment); - realmAuthorizationService.deactivateAssignment(assignment.getId()); - Optional found = realmAuthorizationService.getAssignmentById(assignment.getId()); - assertTrue(found.isPresent()); - assertFalse(found.get().isActive()); - } - - @Test - void testDeactivateAssignment_NotFound() { - assertThrows(IllegalArgumentException.class, () -> { - realmAuthorizationService.deactivateAssignment("non-existent"); - }); - } - - @Test - void testActivateAssignment_Success() { - doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); - realmAuthorizationService.assignRealmToUser(assignment); - realmAuthorizationService.deactivateAssignment(assignment.getId()); - realmAuthorizationService.activateAssignment(assignment.getId()); - Optional found = realmAuthorizationService.getAssignmentById(assignment.getId()); - assertTrue(found.isPresent()); - assertTrue(found.get().isActive()); - } - - @Test - void testCountAssignmentsByUser() { - doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); - realmAuthorizationService.assignRealmToUser(assignment); - long count = realmAuthorizationService.countAssignmentsByUser("user-1"); - assertEquals(1, count); - } - - @Test - void testCountUsersByRealm() { - doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); - realmAuthorizationService.assignRealmToUser(assignment); - long count = realmAuthorizationService.countUsersByRealm("realm1"); - assertEquals(1, count); - } - - @Test - void testAssignmentExists_True() { - doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); - realmAuthorizationService.assignRealmToUser(assignment); - assertTrue(realmAuthorizationService.assignmentExists("user-1", "realm1")); - } - - @Test - void testAssignmentExists_False() { - assertFalse(realmAuthorizationService.assignmentExists("user-1", "realm1")); - } -} - +package dev.lions.user.manager.service.impl; + +import dev.lions.user.manager.dto.realm.RealmAssignmentDTO; +import dev.lions.user.manager.enums.audit.TypeActionAudit; +import dev.lions.user.manager.service.AuditService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * Tests unitaires pour RealmAuthorizationServiceImpl + */ +@ExtendWith(MockitoExtension.class) +class RealmAuthorizationServiceImplTest { + + @Mock + private AuditService auditService; + + @InjectMocks + private RealmAuthorizationServiceImpl realmAuthorizationService; + + private RealmAssignmentDTO assignment; + + @BeforeEach + void setUp() { + assignment = RealmAssignmentDTO.builder() + .id("assignment-1") + .userId("user-1") + .username("testuser") + .email("test@example.com") + .realmName("realm1") + .isSuperAdmin(false) + .active(true) + .assignedAt(LocalDateTime.now()) + .assignedBy("admin") + .build(); + } + + @Test + void testGetAllAssignments_Empty() { + List assignments = realmAuthorizationService.getAllAssignments(); + assertTrue(assignments.isEmpty()); + } + + @Test + void testGetAllAssignments_WithAssignments() { + realmAuthorizationService.assignRealmToUser(assignment); + List assignments = realmAuthorizationService.getAllAssignments(); + assertEquals(1, assignments.size()); + assertEquals("assignment-1", assignments.get(0).getId()); + } + + @Test + void testGetAssignmentsByUser_Success() { + realmAuthorizationService.assignRealmToUser(assignment); + List assignments = realmAuthorizationService.getAssignmentsByUser("user-1"); + assertEquals(1, assignments.size()); + } + + @Test + void testGetAssignmentsByUser_Empty() { + List assignments = realmAuthorizationService.getAssignmentsByUser("user-1"); + assertTrue(assignments.isEmpty()); + } + + @Test + void testGetAssignmentsByRealm_Success() { + realmAuthorizationService.assignRealmToUser(assignment); + List assignments = realmAuthorizationService.getAssignmentsByRealm("realm1"); + assertEquals(1, assignments.size()); + } + + @Test + void testGetAssignmentById_Success() { + realmAuthorizationService.assignRealmToUser(assignment); + Optional found = realmAuthorizationService.getAssignmentById("assignment-1"); + assertTrue(found.isPresent()); + assertEquals("assignment-1", found.get().getId()); + } + + @Test + void testGetAssignmentById_NotFound() { + Optional found = realmAuthorizationService.getAssignmentById("non-existent"); + assertFalse(found.isPresent()); + } + + @Test + void testCanManageRealm_SuperAdmin() { + realmAuthorizationService.setSuperAdmin("user-1", true); + assertTrue(realmAuthorizationService.canManageRealm("user-1", "any-realm")); + } + + @Test + void testCanManageRealm_WithAssignment() { + realmAuthorizationService.assignRealmToUser(assignment); + assertTrue(realmAuthorizationService.canManageRealm("user-1", "realm1")); + } + + @Test + void testCanManageRealm_NoAccess() { + assertFalse(realmAuthorizationService.canManageRealm("user-1", "realm1")); + } + + @Test + void testIsSuperAdmin_True() { + realmAuthorizationService.setSuperAdmin("user-1", true); + assertTrue(realmAuthorizationService.isSuperAdmin("user-1")); + } + + @Test + void testIsSuperAdmin_False() { + assertFalse(realmAuthorizationService.isSuperAdmin("user-1")); + } + + @Test + void testGetAuthorizedRealms_SuperAdmin() { + realmAuthorizationService.setSuperAdmin("user-1", true); + List realms = realmAuthorizationService.getAuthorizedRealms("user-1"); + assertTrue(realms.isEmpty()); // Super admin retourne liste vide + } + + @Test + void testGetAuthorizedRealms_WithAssignments() { + realmAuthorizationService.assignRealmToUser(assignment); + List realms = realmAuthorizationService.getAuthorizedRealms("user-1"); + assertEquals(1, realms.size()); + assertEquals("realm1", realms.get(0)); + } + + @Test + void testAssignRealmToUser_Success() { + doNothing().when(auditService).logSuccess( + any(TypeActionAudit.class), + anyString(), + anyString(), + anyString(), + anyString(), + anyString(), + anyString() + ); + + RealmAssignmentDTO result = realmAuthorizationService.assignRealmToUser(assignment); + assertNotNull(result); + assertNotNull(result.getId()); + assertTrue(result.isActive()); + assertNotNull(result.getAssignedAt()); + } + + @Test + void testAssignRealmToUser_NoUserId() { + assignment.setUserId(null); + assertThrows(IllegalArgumentException.class, () -> { + realmAuthorizationService.assignRealmToUser(assignment); + }); + } + + @Test + void testAssignRealmToUser_NoRealmName() { + assignment.setRealmName(null); + assertThrows(IllegalArgumentException.class, () -> { + realmAuthorizationService.assignRealmToUser(assignment); + }); + } + + @Test + void testAssignRealmToUser_Duplicate() { + doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); + realmAuthorizationService.assignRealmToUser(assignment); + assertThrows(IllegalArgumentException.class, () -> { + realmAuthorizationService.assignRealmToUser(assignment); + }); + } + + @Test + void testRevokeRealmFromUser_Success() { + doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); + realmAuthorizationService.assignRealmToUser(assignment); + realmAuthorizationService.revokeRealmFromUser("user-1", "realm1"); + assertFalse(realmAuthorizationService.canManageRealm("user-1", "realm1")); + } + + @Test + void testRevokeRealmFromUser_NotExists() { + // Ne doit pas lever d'exception si l'assignation n'existe pas + assertDoesNotThrow(() -> { + realmAuthorizationService.revokeRealmFromUser("user-1", "realm1"); + }); + } + + @Test + void testRevokeAllRealmsFromUser() { + doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); + realmAuthorizationService.assignRealmToUser(assignment); + realmAuthorizationService.revokeAllRealmsFromUser("user-1"); + assertTrue(realmAuthorizationService.getAssignmentsByUser("user-1").isEmpty()); + } + + @Test + void testSetSuperAdmin_True() { + doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); + realmAuthorizationService.setSuperAdmin("user-1", true); + assertTrue(realmAuthorizationService.isSuperAdmin("user-1")); + } + + @Test + void testSetSuperAdmin_False() { + doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); + realmAuthorizationService.setSuperAdmin("user-1", true); + realmAuthorizationService.setSuperAdmin("user-1", false); + assertFalse(realmAuthorizationService.isSuperAdmin("user-1")); + } + + @Test + void testDeactivateAssignment_Success() { + doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); + realmAuthorizationService.assignRealmToUser(assignment); + realmAuthorizationService.deactivateAssignment(assignment.getId()); + Optional found = realmAuthorizationService.getAssignmentById(assignment.getId()); + assertTrue(found.isPresent()); + assertFalse(found.get().isActive()); + } + + @Test + void testDeactivateAssignment_NotFound() { + assertThrows(IllegalArgumentException.class, () -> { + realmAuthorizationService.deactivateAssignment("non-existent"); + }); + } + + @Test + void testActivateAssignment_Success() { + doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); + realmAuthorizationService.assignRealmToUser(assignment); + realmAuthorizationService.deactivateAssignment(assignment.getId()); + realmAuthorizationService.activateAssignment(assignment.getId()); + Optional found = realmAuthorizationService.getAssignmentById(assignment.getId()); + assertTrue(found.isPresent()); + assertTrue(found.get().isActive()); + } + + @Test + void testCountAssignmentsByUser() { + doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); + realmAuthorizationService.assignRealmToUser(assignment); + long count = realmAuthorizationService.countAssignmentsByUser("user-1"); + assertEquals(1, count); + } + + @Test + void testCountUsersByRealm() { + doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); + realmAuthorizationService.assignRealmToUser(assignment); + long count = realmAuthorizationService.countUsersByRealm("realm1"); + assertEquals(1, count); + } + + @Test + void testAssignmentExists_True() { + doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); + realmAuthorizationService.assignRealmToUser(assignment); + assertTrue(realmAuthorizationService.assignmentExists("user-1", "realm1")); + } + + @Test + void testAssignmentExists_False() { + assertFalse(realmAuthorizationService.assignmentExists("user-1", "realm1")); + } +} + diff --git a/src/test/java/dev/lions/user/manager/service/impl/RoleServiceImplCompleteTest.java b/src/test/java/dev/lions/user/manager/service/impl/RoleServiceImplCompleteTest.java index 2113328..6307ef5 100644 --- a/src/test/java/dev/lions/user/manager/service/impl/RoleServiceImplCompleteTest.java +++ b/src/test/java/dev/lions/user/manager/service/impl/RoleServiceImplCompleteTest.java @@ -1,350 +1,350 @@ -package dev.lions.user.manager.service.impl; - -import dev.lions.user.manager.client.KeycloakAdminClient; -import dev.lions.user.manager.dto.role.RoleDTO; -import dev.lions.user.manager.enums.role.TypeRole; -import dev.lions.user.manager.mapper.RoleMapper; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.keycloak.admin.client.Keycloak; -import org.keycloak.admin.client.resource.*; -import org.keycloak.representations.idm.ClientRepresentation; -import org.keycloak.representations.idm.RoleRepresentation; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Collections; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -/** - * Tests complets pour RoleServiceImpl pour atteindre 100% de couverture - * Couvre updateRole, deleteRole pour CLIENT_ROLE, createRealmRole avec rôle existant, etc. - */ -@ExtendWith(MockitoExtension.class) -class RoleServiceImplCompleteTest { - - @Mock - private KeycloakAdminClient keycloakAdminClient; - - @Mock - private Keycloak keycloakInstance; - - @Mock - private RealmResource realmResource; - - @Mock - private RolesResource rolesResource; - - @Mock - private RoleResource roleResource; - - @Mock - private ClientsResource clientsResource; - - @Mock - private ClientResource clientResource; - - @InjectMocks - private RoleServiceImpl roleService; - - private static final String REALM = "test-realm"; - private static final String ROLE_ID = "role-123"; - private static final String ROLE_NAME = "test-role"; - private static final String CLIENT_NAME = "test-client"; - private static final String INTERNAL_CLIENT_ID = "internal-client-id"; - - @Test - void testCreateRealmRole_RoleAlreadyExists() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); - - RoleRepresentation existingRole = new RoleRepresentation(); - existingRole.setName(ROLE_NAME); - when(roleResource.toRepresentation()).thenReturn(existingRole); - - RoleDTO roleDTO = RoleDTO.builder() - .name(ROLE_NAME) - .description("Test role") - .build(); - - assertThrows(IllegalArgumentException.class, () -> - roleService.createRealmRole(roleDTO, REALM)); - } - - @Test - void testUpdateRole_RealmRole_Success() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - - // Mock getRealmRoleById - RoleRepresentation roleRep = new RoleRepresentation(); - roleRep.setId(ROLE_ID); - roleRep.setName(ROLE_NAME); - when(rolesResource.list()).thenReturn(List.of(roleRep)); - when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); - when(roleResource.toRepresentation()).thenReturn(roleRep); - - RoleDTO roleDTO = RoleDTO.builder() - .id(ROLE_ID) - .name(ROLE_NAME) - .description("Updated description") - .build(); - - RoleDTO result = roleService.updateRole(ROLE_ID, roleDTO, REALM, TypeRole.REALM_ROLE, null); - - assertNotNull(result); - verify(roleResource).update(any(RoleRepresentation.class)); - } - - @Test - void testUpdateRole_RealmRole_NotFound() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.list()).thenReturn(Collections.emptyList()); - - RoleDTO roleDTO = RoleDTO.builder() - .id(ROLE_ID) - .name(ROLE_NAME) - .build(); - - assertThrows(jakarta.ws.rs.NotFoundException.class, () -> - roleService.updateRole(ROLE_ID, roleDTO, REALM, TypeRole.REALM_ROLE, null)); - } - - @Test - void testUpdateRole_RealmRole_NoDescription() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - - RoleRepresentation roleRep = new RoleRepresentation(); - roleRep.setId(ROLE_ID); - roleRep.setName(ROLE_NAME); - when(rolesResource.list()).thenReturn(List.of(roleRep)); - when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); - when(roleResource.toRepresentation()).thenReturn(roleRep); - - RoleDTO roleDTO = RoleDTO.builder() - .id(ROLE_ID) - .name(ROLE_NAME) - .description(null) // No description - .build(); - - RoleDTO result = roleService.updateRole(ROLE_ID, roleDTO, REALM, TypeRole.REALM_ROLE, null); - - assertNotNull(result); - verify(roleResource).update(any(RoleRepresentation.class)); - } - - @Test - void testUpdateRole_ClientRole_Success() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.clients()).thenReturn(clientsResource); - - ClientRepresentation client = new ClientRepresentation(); - client.setId(INTERNAL_CLIENT_ID); - when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client)); - when(clientsResource.get(INTERNAL_CLIENT_ID)).thenReturn(clientResource); - when(clientResource.roles()).thenReturn(rolesResource); - - // Mock getRoleById - RoleRepresentation roleRep = new RoleRepresentation(); - roleRep.setId(ROLE_ID); - roleRep.setName(ROLE_NAME); - when(rolesResource.list()).thenReturn(List.of(roleRep)); - when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); - when(roleResource.toRepresentation()).thenReturn(roleRep); - - RoleDTO roleDTO = RoleDTO.builder() - .id(ROLE_ID) - .name(ROLE_NAME) - .description("Updated description") - .build(); - - RoleDTO result = roleService.updateRole(ROLE_ID, roleDTO, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME); - - assertNotNull(result); - assertEquals(CLIENT_NAME, result.getClientId()); - verify(roleResource).update(any(RoleRepresentation.class)); - } - - @Test - void testUpdateRole_ClientRole_ClientNotFound() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.clients()).thenReturn(clientsResource); - when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList()); - - RoleDTO roleDTO = RoleDTO.builder() - .id(ROLE_ID) - .name(ROLE_NAME) - .build(); - - // getRoleById is called first, which will throw NotFoundException when client is not found - // Actually, getRoleById returns Optional.empty() when client is not found - // So it will throw NotFoundException for role not found - assertThrows(jakarta.ws.rs.NotFoundException.class, () -> - roleService.updateRole(ROLE_ID, roleDTO, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME)); - } - - @Test - void testUpdateRole_ClientRole_NotFound() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.clients()).thenReturn(clientsResource); - - ClientRepresentation client = new ClientRepresentation(); - client.setId(INTERNAL_CLIENT_ID); - when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client)); - when(clientsResource.get(INTERNAL_CLIENT_ID)).thenReturn(clientResource); - when(clientResource.roles()).thenReturn(rolesResource); - when(rolesResource.list()).thenReturn(Collections.emptyList()); - - RoleDTO roleDTO = RoleDTO.builder() - .id(ROLE_ID) - .name(ROLE_NAME) - .build(); - - assertThrows(jakarta.ws.rs.NotFoundException.class, () -> - roleService.updateRole(ROLE_ID, roleDTO, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME)); - } - - @Test - void testUpdateRole_UnsupportedType() { - RoleDTO roleDTO = RoleDTO.builder() - .id(ROLE_ID) - .name(ROLE_NAME) - .build(); - - assertThrows(IllegalArgumentException.class, () -> - roleService.updateRole(ROLE_ID, roleDTO, REALM, null, null)); - } - - @Test - void testDeleteRole_ClientRole_Success() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.clients()).thenReturn(clientsResource); - - ClientRepresentation client = new ClientRepresentation(); - client.setId(INTERNAL_CLIENT_ID); - when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client)); - when(clientsResource.get(INTERNAL_CLIENT_ID)).thenReturn(clientResource); - when(clientResource.roles()).thenReturn(rolesResource); - - // Mock getRoleById - getRoleById for CLIENT_ROLE only uses rolesResource.list() - RoleRepresentation roleRep = new RoleRepresentation(); - roleRep.setId(ROLE_ID); - roleRep.setName(ROLE_NAME); - when(rolesResource.list()).thenReturn(List.of(roleRep)); - - roleService.deleteRole(ROLE_ID, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME); - - verify(rolesResource).deleteRole(ROLE_NAME); - } - - @Test - void testDeleteRole_ClientRole_ClientNotFound() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.clients()).thenReturn(clientsResource); - when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList()); - - // getRoleById is called first, which returns Optional.empty() when client is not found - // So it will throw NotFoundException for role not found - assertThrows(jakarta.ws.rs.NotFoundException.class, () -> - roleService.deleteRole(ROLE_ID, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME)); - } - - @Test - void testDeleteRole_ClientRole_NotFound() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.clients()).thenReturn(clientsResource); - - ClientRepresentation client = new ClientRepresentation(); - client.setId(INTERNAL_CLIENT_ID); - when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client)); - when(clientsResource.get(INTERNAL_CLIENT_ID)).thenReturn(clientResource); - when(clientResource.roles()).thenReturn(rolesResource); - when(rolesResource.list()).thenReturn(Collections.emptyList()); - - assertThrows(jakarta.ws.rs.NotFoundException.class, () -> - roleService.deleteRole(ROLE_ID, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME)); - } - - @Test - void testDeleteRole_UnsupportedType() { - assertThrows(IllegalArgumentException.class, () -> - roleService.deleteRole(ROLE_ID, REALM, null, null)); - } - - // Note: getRealmRoleById is private, so we test it indirectly through updateRole - // The exception path is tested via updateRole_RealmRole_NotFound - - @Test - void testGetAllRealmRoles_Success() { - when(keycloakAdminClient.realmExists(REALM)).thenReturn(true); - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - - RoleRepresentation role1 = new RoleRepresentation(); - role1.setName("role1"); - RoleRepresentation role2 = new RoleRepresentation(); - role2.setName("role2"); - when(rolesResource.list()).thenReturn(List.of(role1, role2)); - - var result = roleService.getAllRealmRoles(REALM); - - assertNotNull(result); - assertEquals(2, result.size()); - } - - @Test - void testGetAllRealmRoles_With404InMessage() { - when(keycloakAdminClient.realmExists(REALM)).thenReturn(true); - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.list()).thenThrow(new RuntimeException("Server response is: 404")); - - assertThrows(IllegalArgumentException.class, () -> - roleService.getAllRealmRoles(REALM)); - } - - @Test - void testGetAllRealmRoles_WithNotInMessage() { - when(keycloakAdminClient.realmExists(REALM)).thenReturn(true); - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.list()).thenThrow(new RuntimeException("Not Found")); - - assertThrows(IllegalArgumentException.class, () -> - roleService.getAllRealmRoles(REALM)); - } - - @Test - void testGetAllRealmRoles_WithOtherException() { - when(keycloakAdminClient.realmExists(REALM)).thenReturn(true); - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.list()).thenThrow(new RuntimeException("Connection error")); - - assertThrows(RuntimeException.class, () -> - roleService.getAllRealmRoles(REALM)); - } -} - +package dev.lions.user.manager.service.impl; + +import dev.lions.user.manager.client.KeycloakAdminClient; +import dev.lions.user.manager.dto.role.RoleDTO; +import dev.lions.user.manager.enums.role.TypeRole; +import dev.lions.user.manager.mapper.RoleMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.*; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * Tests complets pour RoleServiceImpl pour atteindre 100% de couverture + * Couvre updateRole, deleteRole pour CLIENT_ROLE, createRealmRole avec rôle existant, etc. + */ +@ExtendWith(MockitoExtension.class) +class RoleServiceImplCompleteTest { + + @Mock + private KeycloakAdminClient keycloakAdminClient; + + @Mock + private Keycloak keycloakInstance; + + @Mock + private RealmResource realmResource; + + @Mock + private RolesResource rolesResource; + + @Mock + private RoleResource roleResource; + + @Mock + private ClientsResource clientsResource; + + @Mock + private ClientResource clientResource; + + @InjectMocks + private RoleServiceImpl roleService; + + private static final String REALM = "test-realm"; + private static final String ROLE_ID = "role-123"; + private static final String ROLE_NAME = "test-role"; + private static final String CLIENT_NAME = "test-client"; + private static final String INTERNAL_CLIENT_ID = "internal-client-id"; + + @Test + void testCreateRealmRole_RoleAlreadyExists() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); + + RoleRepresentation existingRole = new RoleRepresentation(); + existingRole.setName(ROLE_NAME); + when(roleResource.toRepresentation()).thenReturn(existingRole); + + RoleDTO roleDTO = RoleDTO.builder() + .name(ROLE_NAME) + .description("Test role") + .build(); + + assertThrows(IllegalArgumentException.class, () -> + roleService.createRealmRole(roleDTO, REALM)); + } + + @Test + void testUpdateRole_RealmRole_Success() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + + // Mock getRealmRoleById + RoleRepresentation roleRep = new RoleRepresentation(); + roleRep.setId(ROLE_ID); + roleRep.setName(ROLE_NAME); + when(rolesResource.list()).thenReturn(List.of(roleRep)); + when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); + when(roleResource.toRepresentation()).thenReturn(roleRep); + + RoleDTO roleDTO = RoleDTO.builder() + .id(ROLE_ID) + .name(ROLE_NAME) + .description("Updated description") + .build(); + + RoleDTO result = roleService.updateRole(ROLE_ID, roleDTO, REALM, TypeRole.REALM_ROLE, null); + + assertNotNull(result); + verify(roleResource).update(any(RoleRepresentation.class)); + } + + @Test + void testUpdateRole_RealmRole_NotFound() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.list()).thenReturn(Collections.emptyList()); + + RoleDTO roleDTO = RoleDTO.builder() + .id(ROLE_ID) + .name(ROLE_NAME) + .build(); + + assertThrows(jakarta.ws.rs.NotFoundException.class, () -> + roleService.updateRole(ROLE_ID, roleDTO, REALM, TypeRole.REALM_ROLE, null)); + } + + @Test + void testUpdateRole_RealmRole_NoDescription() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + + RoleRepresentation roleRep = new RoleRepresentation(); + roleRep.setId(ROLE_ID); + roleRep.setName(ROLE_NAME); + when(rolesResource.list()).thenReturn(List.of(roleRep)); + when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); + when(roleResource.toRepresentation()).thenReturn(roleRep); + + RoleDTO roleDTO = RoleDTO.builder() + .id(ROLE_ID) + .name(ROLE_NAME) + .description(null) // No description + .build(); + + RoleDTO result = roleService.updateRole(ROLE_ID, roleDTO, REALM, TypeRole.REALM_ROLE, null); + + assertNotNull(result); + verify(roleResource).update(any(RoleRepresentation.class)); + } + + @Test + void testUpdateRole_ClientRole_Success() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.clients()).thenReturn(clientsResource); + + ClientRepresentation client = new ClientRepresentation(); + client.setId(INTERNAL_CLIENT_ID); + when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client)); + when(clientsResource.get(INTERNAL_CLIENT_ID)).thenReturn(clientResource); + when(clientResource.roles()).thenReturn(rolesResource); + + // Mock getRoleById + RoleRepresentation roleRep = new RoleRepresentation(); + roleRep.setId(ROLE_ID); + roleRep.setName(ROLE_NAME); + when(rolesResource.list()).thenReturn(List.of(roleRep)); + when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); + when(roleResource.toRepresentation()).thenReturn(roleRep); + + RoleDTO roleDTO = RoleDTO.builder() + .id(ROLE_ID) + .name(ROLE_NAME) + .description("Updated description") + .build(); + + RoleDTO result = roleService.updateRole(ROLE_ID, roleDTO, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME); + + assertNotNull(result); + assertEquals(CLIENT_NAME, result.getClientId()); + verify(roleResource).update(any(RoleRepresentation.class)); + } + + @Test + void testUpdateRole_ClientRole_ClientNotFound() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.clients()).thenReturn(clientsResource); + when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList()); + + RoleDTO roleDTO = RoleDTO.builder() + .id(ROLE_ID) + .name(ROLE_NAME) + .build(); + + // getRoleById is called first, which will throw NotFoundException when client is not found + // Actually, getRoleById returns Optional.empty() when client is not found + // So it will throw NotFoundException for role not found + assertThrows(jakarta.ws.rs.NotFoundException.class, () -> + roleService.updateRole(ROLE_ID, roleDTO, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME)); + } + + @Test + void testUpdateRole_ClientRole_NotFound() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.clients()).thenReturn(clientsResource); + + ClientRepresentation client = new ClientRepresentation(); + client.setId(INTERNAL_CLIENT_ID); + when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client)); + when(clientsResource.get(INTERNAL_CLIENT_ID)).thenReturn(clientResource); + when(clientResource.roles()).thenReturn(rolesResource); + when(rolesResource.list()).thenReturn(Collections.emptyList()); + + RoleDTO roleDTO = RoleDTO.builder() + .id(ROLE_ID) + .name(ROLE_NAME) + .build(); + + assertThrows(jakarta.ws.rs.NotFoundException.class, () -> + roleService.updateRole(ROLE_ID, roleDTO, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME)); + } + + @Test + void testUpdateRole_UnsupportedType() { + RoleDTO roleDTO = RoleDTO.builder() + .id(ROLE_ID) + .name(ROLE_NAME) + .build(); + + assertThrows(IllegalArgumentException.class, () -> + roleService.updateRole(ROLE_ID, roleDTO, REALM, null, null)); + } + + @Test + void testDeleteRole_ClientRole_Success() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.clients()).thenReturn(clientsResource); + + ClientRepresentation client = new ClientRepresentation(); + client.setId(INTERNAL_CLIENT_ID); + when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client)); + when(clientsResource.get(INTERNAL_CLIENT_ID)).thenReturn(clientResource); + when(clientResource.roles()).thenReturn(rolesResource); + + // Mock getRoleById - getRoleById for CLIENT_ROLE only uses rolesResource.list() + RoleRepresentation roleRep = new RoleRepresentation(); + roleRep.setId(ROLE_ID); + roleRep.setName(ROLE_NAME); + when(rolesResource.list()).thenReturn(List.of(roleRep)); + + roleService.deleteRole(ROLE_ID, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME); + + verify(rolesResource).deleteRole(ROLE_NAME); + } + + @Test + void testDeleteRole_ClientRole_ClientNotFound() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.clients()).thenReturn(clientsResource); + when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList()); + + // getRoleById is called first, which returns Optional.empty() when client is not found + // So it will throw NotFoundException for role not found + assertThrows(jakarta.ws.rs.NotFoundException.class, () -> + roleService.deleteRole(ROLE_ID, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME)); + } + + @Test + void testDeleteRole_ClientRole_NotFound() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.clients()).thenReturn(clientsResource); + + ClientRepresentation client = new ClientRepresentation(); + client.setId(INTERNAL_CLIENT_ID); + when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client)); + when(clientsResource.get(INTERNAL_CLIENT_ID)).thenReturn(clientResource); + when(clientResource.roles()).thenReturn(rolesResource); + when(rolesResource.list()).thenReturn(Collections.emptyList()); + + assertThrows(jakarta.ws.rs.NotFoundException.class, () -> + roleService.deleteRole(ROLE_ID, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME)); + } + + @Test + void testDeleteRole_UnsupportedType() { + assertThrows(IllegalArgumentException.class, () -> + roleService.deleteRole(ROLE_ID, REALM, null, null)); + } + + // Note: getRealmRoleById is private, so we test it indirectly through updateRole + // The exception path is tested via updateRole_RealmRole_NotFound + + @Test + void testGetAllRealmRoles_Success() { + when(keycloakAdminClient.realmExists(REALM)).thenReturn(true); + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + + RoleRepresentation role1 = new RoleRepresentation(); + role1.setName("role1"); + RoleRepresentation role2 = new RoleRepresentation(); + role2.setName("role2"); + when(rolesResource.list()).thenReturn(List.of(role1, role2)); + + var result = roleService.getAllRealmRoles(REALM); + + assertNotNull(result); + assertEquals(2, result.size()); + } + + @Test + void testGetAllRealmRoles_With404InMessage() { + when(keycloakAdminClient.realmExists(REALM)).thenReturn(true); + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.list()).thenThrow(new RuntimeException("Server response is: 404")); + + assertThrows(IllegalArgumentException.class, () -> + roleService.getAllRealmRoles(REALM)); + } + + @Test + void testGetAllRealmRoles_WithNotInMessage() { + when(keycloakAdminClient.realmExists(REALM)).thenReturn(true); + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.list()).thenThrow(new RuntimeException("Not Found")); + + assertThrows(IllegalArgumentException.class, () -> + roleService.getAllRealmRoles(REALM)); + } + + @Test + void testGetAllRealmRoles_WithOtherException() { + when(keycloakAdminClient.realmExists(REALM)).thenReturn(true); + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.list()).thenThrow(new RuntimeException("Connection error")); + + assertThrows(RuntimeException.class, () -> + roleService.getAllRealmRoles(REALM)); + } +} + diff --git a/src/test/java/dev/lions/user/manager/service/impl/RoleServiceImplExtendedTest.java b/src/test/java/dev/lions/user/manager/service/impl/RoleServiceImplExtendedTest.java index 6522f87..4b68042 100644 --- a/src/test/java/dev/lions/user/manager/service/impl/RoleServiceImplExtendedTest.java +++ b/src/test/java/dev/lions/user/manager/service/impl/RoleServiceImplExtendedTest.java @@ -1,245 +1,245 @@ -package dev.lions.user.manager.service.impl; - -import dev.lions.user.manager.client.KeycloakAdminClient; -import dev.lions.user.manager.dto.role.RoleDTO; -import dev.lions.user.manager.enums.role.TypeRole; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.keycloak.admin.client.Keycloak; -import org.keycloak.admin.client.resource.*; -import org.keycloak.representations.idm.ClientRepresentation; -import org.keycloak.representations.idm.RoleRepresentation; -import org.keycloak.representations.idm.UserRepresentation; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -/** - * Tests supplémentaires pour RoleServiceImpl pour améliorer la couverture - * Couvre les méthodes : userHasRole, roleExists, countUsersWithRole - */ -@ExtendWith(MockitoExtension.class) -class RoleServiceImplExtendedTest { - - @Mock - private KeycloakAdminClient keycloakAdminClient; - - @Mock - private Keycloak keycloakInstance; - - @Mock - private RealmResource realmResource; - - @Mock - private RolesResource rolesResource; - - @Mock - private RoleResource roleResource; - - @Mock - private UsersResource usersResource; - - @Mock - private UserResource userResource; - - @Mock - private RoleMappingResource roleMappingResource; - - @Mock - private RoleScopeResource realmLevelRoleScopeResource; - - @Mock - private RoleScopeResource clientLevelRoleScopeResource; - - @Mock - private ClientsResource clientsResource; - - @InjectMocks - private RoleServiceImpl roleService; - - private static final String REALM = "test-realm"; - private static final String USER_ID = "user-123"; - private static final String ROLE_NAME = "admin"; - private static final String CLIENT_NAME = "test-client"; - - @Test - void testUserHasRole_RealmRole_True() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.users()).thenReturn(usersResource); - when(usersResource.get(USER_ID)).thenReturn(userResource); - when(userResource.roles()).thenReturn(roleMappingResource); - when(roleMappingResource.realmLevel()).thenReturn(realmLevelRoleScopeResource); - - RoleRepresentation role = new RoleRepresentation(); - role.setName(ROLE_NAME); - when(realmLevelRoleScopeResource.listEffective()).thenReturn(List.of(role)); - - boolean result = roleService.userHasRole(USER_ID, ROLE_NAME, REALM, TypeRole.REALM_ROLE, null); - - assertTrue(result); - } - - @Test - void testUserHasRole_RealmRole_False() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.users()).thenReturn(usersResource); - when(usersResource.get(USER_ID)).thenReturn(userResource); - when(userResource.roles()).thenReturn(roleMappingResource); - when(roleMappingResource.realmLevel()).thenReturn(realmLevelRoleScopeResource); - when(realmLevelRoleScopeResource.listEffective()).thenReturn(Collections.emptyList()); - - boolean result = roleService.userHasRole(USER_ID, ROLE_NAME, REALM, TypeRole.REALM_ROLE, null); - - assertFalse(result); - } - - @Test - void testUserHasRole_ClientRole_True() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.clients()).thenReturn(clientsResource); - when(realmResource.users()).thenReturn(usersResource); - when(usersResource.get(USER_ID)).thenReturn(userResource); - when(userResource.roles()).thenReturn(roleMappingResource); - - ClientRepresentation client = new ClientRepresentation(); - client.setId("client-123"); - when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client)); - when(roleMappingResource.clientLevel("client-123")).thenReturn(clientLevelRoleScopeResource); - - RoleRepresentation role = new RoleRepresentation(); - role.setName(ROLE_NAME); - when(clientLevelRoleScopeResource.listEffective()).thenReturn(List.of(role)); - - boolean result = roleService.userHasRole(USER_ID, ROLE_NAME, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME); - - assertTrue(result); - } - - @Test - void testUserHasRole_ClientRole_ClientNotFound() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.clients()).thenReturn(clientsResource); - when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList()); - - boolean result = roleService.userHasRole(USER_ID, ROLE_NAME, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME); - - assertFalse(result); - } - - @Test - void testUserHasRole_ClientRole_NullClientName() { - boolean result = roleService.userHasRole(USER_ID, ROLE_NAME, REALM, TypeRole.CLIENT_ROLE, null); - - assertFalse(result); - } - - @Test - void testRoleExists_RealmRole_True() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); - - RoleRepresentation role = new RoleRepresentation(); - role.setName(ROLE_NAME); - when(roleResource.toRepresentation()).thenReturn(role); - - boolean result = roleService.roleExists(ROLE_NAME, REALM, TypeRole.REALM_ROLE, null); - - assertTrue(result); - } - - @Test - void testRoleExists_RealmRole_False() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); - when(roleResource.toRepresentation()).thenThrow(new jakarta.ws.rs.NotFoundException()); - - boolean result = roleService.roleExists(ROLE_NAME, REALM, TypeRole.REALM_ROLE, null); - - assertFalse(result); - } - - @Test - void testCountUsersWithRole_Success() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(realmResource.users()).thenReturn(usersResource); - - // Mock getRoleById - RoleRepresentation roleRep = new RoleRepresentation(); - roleRep.setId("role-123"); - roleRep.setName(ROLE_NAME); - when(rolesResource.list()).thenReturn(List.of(roleRep)); - - // Mock user list - UserRepresentation user1 = new UserRepresentation(); - user1.setId("user-1"); - UserRepresentation user2 = new UserRepresentation(); - user2.setId("user-2"); - when(usersResource.list()).thenReturn(List.of(user1, user2)); - - // Mock userHasRole for each user - when(usersResource.get("user-1")).thenReturn(userResource); - when(usersResource.get("user-2")).thenReturn(userResource); - when(userResource.roles()).thenReturn(roleMappingResource); - when(roleMappingResource.realmLevel()).thenReturn(realmLevelRoleScopeResource); - - RoleRepresentation role = new RoleRepresentation(); - role.setName(ROLE_NAME); - // User 1 has role, user 2 doesn't - when(realmLevelRoleScopeResource.listEffective()) - .thenReturn(List.of(role)) // user-1 - .thenReturn(Collections.emptyList()); // user-2 - - long count = roleService.countUsersWithRole("role-123", REALM, TypeRole.REALM_ROLE, null); - - assertEquals(1, count); - } - - @Test - void testCountUsersWithRole_RoleNotFound() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.list()).thenReturn(Collections.emptyList()); - - long count = roleService.countUsersWithRole("non-existent-role", REALM, TypeRole.REALM_ROLE, null); - - assertEquals(0, count); - } - - @Test - void testCountUsersWithRole_Exception() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(realmResource.users()).thenReturn(usersResource); - - RoleRepresentation roleRep = new RoleRepresentation(); - roleRep.setId("role-123"); - roleRep.setName(ROLE_NAME); - when(rolesResource.list()).thenReturn(List.of(roleRep)); - when(usersResource.list()).thenThrow(new RuntimeException("Error")); - - long count = roleService.countUsersWithRole("role-123", REALM, TypeRole.REALM_ROLE, null); - - assertEquals(0, count); - } -} - +package dev.lions.user.manager.service.impl; + +import dev.lions.user.manager.client.KeycloakAdminClient; +import dev.lions.user.manager.dto.role.RoleDTO; +import dev.lions.user.manager.enums.role.TypeRole; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.*; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * Tests supplémentaires pour RoleServiceImpl pour améliorer la couverture + * Couvre les méthodes : userHasRole, roleExists, countUsersWithRole + */ +@ExtendWith(MockitoExtension.class) +class RoleServiceImplExtendedTest { + + @Mock + private KeycloakAdminClient keycloakAdminClient; + + @Mock + private Keycloak keycloakInstance; + + @Mock + private RealmResource realmResource; + + @Mock + private RolesResource rolesResource; + + @Mock + private RoleResource roleResource; + + @Mock + private UsersResource usersResource; + + @Mock + private UserResource userResource; + + @Mock + private RoleMappingResource roleMappingResource; + + @Mock + private RoleScopeResource realmLevelRoleScopeResource; + + @Mock + private RoleScopeResource clientLevelRoleScopeResource; + + @Mock + private ClientsResource clientsResource; + + @InjectMocks + private RoleServiceImpl roleService; + + private static final String REALM = "test-realm"; + private static final String USER_ID = "user-123"; + private static final String ROLE_NAME = "admin"; + private static final String CLIENT_NAME = "test-client"; + + @Test + void testUserHasRole_RealmRole_True() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.users()).thenReturn(usersResource); + when(usersResource.get(USER_ID)).thenReturn(userResource); + when(userResource.roles()).thenReturn(roleMappingResource); + when(roleMappingResource.realmLevel()).thenReturn(realmLevelRoleScopeResource); + + RoleRepresentation role = new RoleRepresentation(); + role.setName(ROLE_NAME); + when(realmLevelRoleScopeResource.listEffective()).thenReturn(List.of(role)); + + boolean result = roleService.userHasRole(USER_ID, ROLE_NAME, REALM, TypeRole.REALM_ROLE, null); + + assertTrue(result); + } + + @Test + void testUserHasRole_RealmRole_False() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.users()).thenReturn(usersResource); + when(usersResource.get(USER_ID)).thenReturn(userResource); + when(userResource.roles()).thenReturn(roleMappingResource); + when(roleMappingResource.realmLevel()).thenReturn(realmLevelRoleScopeResource); + when(realmLevelRoleScopeResource.listEffective()).thenReturn(Collections.emptyList()); + + boolean result = roleService.userHasRole(USER_ID, ROLE_NAME, REALM, TypeRole.REALM_ROLE, null); + + assertFalse(result); + } + + @Test + void testUserHasRole_ClientRole_True() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.clients()).thenReturn(clientsResource); + when(realmResource.users()).thenReturn(usersResource); + when(usersResource.get(USER_ID)).thenReturn(userResource); + when(userResource.roles()).thenReturn(roleMappingResource); + + ClientRepresentation client = new ClientRepresentation(); + client.setId("client-123"); + when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client)); + when(roleMappingResource.clientLevel("client-123")).thenReturn(clientLevelRoleScopeResource); + + RoleRepresentation role = new RoleRepresentation(); + role.setName(ROLE_NAME); + when(clientLevelRoleScopeResource.listEffective()).thenReturn(List.of(role)); + + boolean result = roleService.userHasRole(USER_ID, ROLE_NAME, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME); + + assertTrue(result); + } + + @Test + void testUserHasRole_ClientRole_ClientNotFound() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.clients()).thenReturn(clientsResource); + when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList()); + + boolean result = roleService.userHasRole(USER_ID, ROLE_NAME, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME); + + assertFalse(result); + } + + @Test + void testUserHasRole_ClientRole_NullClientName() { + boolean result = roleService.userHasRole(USER_ID, ROLE_NAME, REALM, TypeRole.CLIENT_ROLE, null); + + assertFalse(result); + } + + @Test + void testRoleExists_RealmRole_True() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); + + RoleRepresentation role = new RoleRepresentation(); + role.setName(ROLE_NAME); + when(roleResource.toRepresentation()).thenReturn(role); + + boolean result = roleService.roleExists(ROLE_NAME, REALM, TypeRole.REALM_ROLE, null); + + assertTrue(result); + } + + @Test + void testRoleExists_RealmRole_False() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); + when(roleResource.toRepresentation()).thenThrow(new jakarta.ws.rs.NotFoundException()); + + boolean result = roleService.roleExists(ROLE_NAME, REALM, TypeRole.REALM_ROLE, null); + + assertFalse(result); + } + + @Test + void testCountUsersWithRole_Success() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(realmResource.users()).thenReturn(usersResource); + + // Mock getRoleById + RoleRepresentation roleRep = new RoleRepresentation(); + roleRep.setId("role-123"); + roleRep.setName(ROLE_NAME); + when(rolesResource.list()).thenReturn(List.of(roleRep)); + + // Mock user list + UserRepresentation user1 = new UserRepresentation(); + user1.setId("user-1"); + UserRepresentation user2 = new UserRepresentation(); + user2.setId("user-2"); + when(usersResource.list()).thenReturn(List.of(user1, user2)); + + // Mock userHasRole for each user + when(usersResource.get("user-1")).thenReturn(userResource); + when(usersResource.get("user-2")).thenReturn(userResource); + when(userResource.roles()).thenReturn(roleMappingResource); + when(roleMappingResource.realmLevel()).thenReturn(realmLevelRoleScopeResource); + + RoleRepresentation role = new RoleRepresentation(); + role.setName(ROLE_NAME); + // User 1 has role, user 2 doesn't + when(realmLevelRoleScopeResource.listEffective()) + .thenReturn(List.of(role)) // user-1 + .thenReturn(Collections.emptyList()); // user-2 + + long count = roleService.countUsersWithRole("role-123", REALM, TypeRole.REALM_ROLE, null); + + assertEquals(1, count); + } + + @Test + void testCountUsersWithRole_RoleNotFound() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.list()).thenReturn(Collections.emptyList()); + + long count = roleService.countUsersWithRole("non-existent-role", REALM, TypeRole.REALM_ROLE, null); + + assertEquals(0, count); + } + + @Test + void testCountUsersWithRole_Exception() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(realmResource.users()).thenReturn(usersResource); + + RoleRepresentation roleRep = new RoleRepresentation(); + roleRep.setId("role-123"); + roleRep.setName(ROLE_NAME); + when(rolesResource.list()).thenReturn(List.of(roleRep)); + when(usersResource.list()).thenThrow(new RuntimeException("Error")); + + long count = roleService.countUsersWithRole("role-123", REALM, TypeRole.REALM_ROLE, null); + + assertEquals(0, count); + } +} + diff --git a/src/test/java/dev/lions/user/manager/service/impl/RoleServiceImplIntegrationTest.java b/src/test/java/dev/lions/user/manager/service/impl/RoleServiceImplIntegrationTest.java index ef725d2..a3deaca 100644 --- a/src/test/java/dev/lions/user/manager/service/impl/RoleServiceImplIntegrationTest.java +++ b/src/test/java/dev/lions/user/manager/service/impl/RoleServiceImplIntegrationTest.java @@ -1,589 +1,589 @@ -package dev.lions.user.manager.service.impl; - -import dev.lions.user.manager.client.KeycloakAdminClient; -import dev.lions.user.manager.dto.role.RoleAssignmentDTO; -import dev.lions.user.manager.dto.role.RoleDTO; -import dev.lions.user.manager.enums.role.TypeRole; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.keycloak.admin.client.Keycloak; -import org.keycloak.admin.client.resource.*; -import org.keycloak.representations.idm.ClientRepresentation; -import org.keycloak.representations.idm.RoleRepresentation; -import org.keycloak.representations.idm.UserRepresentation; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -/** - * Tests d'intégration pour RoleServiceImpl - Cas limites et branches conditionnelles complexes - */ -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) -class RoleServiceImplIntegrationTest { - - @Mock - private KeycloakAdminClient keycloakAdminClient; - - @Mock - private Keycloak keycloakInstance; - - @Mock - private RealmResource realmResource; - - @Mock - private RolesResource rolesResource; - - @Mock - private RoleResource roleResource; - - @Mock - private UsersResource usersResource; - - @Mock - private UserResource userResource; - - @Mock - private RoleMappingResource roleMappingResource; - - @Mock - private RoleScopeResource roleScopeResource; - - @Mock - private ClientsResource clientsResource; - - @Mock - private ClientResource clientResource; - - @InjectMocks - private RoleServiceImpl roleService; - - private static final String REALM = "test-realm"; - private static final String USER_ID = "user-123"; - private static final String ROLE_NAME = "admin"; - private static final String CLIENT_NAME = "test-client"; - private static final String ROLE_ID = "role-123"; - - // ==================== Tests getRoleByName - Cas limites ==================== - - @Test - void testGetRoleByName_RealmRole_Success() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); - - RoleRepresentation roleRep = new RoleRepresentation(); - roleRep.setName(ROLE_NAME); - roleRep.setId(ROLE_ID); - when(roleResource.toRepresentation()).thenReturn(roleRep); - - Optional result = roleService.getRoleByName(ROLE_NAME, REALM, TypeRole.REALM_ROLE, null); - - assertTrue(result.isPresent()); - assertEquals(ROLE_NAME, result.get().getName()); - } - - @Test - void testGetRoleByName_RealmRole_NotFound() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); - when(roleResource.toRepresentation()).thenThrow(new jakarta.ws.rs.NotFoundException()); - - Optional result = roleService.getRoleByName(ROLE_NAME, REALM, TypeRole.REALM_ROLE, null); - - assertFalse(result.isPresent()); - } - - @Test - void testGetRoleByName_ClientRole_Success() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.clients()).thenReturn(clientsResource); - - ClientRepresentation client = new ClientRepresentation(); - client.setId("client-123"); - client.setClientId(CLIENT_NAME); - when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client)); - when(clientsResource.get("client-123")).thenReturn(clientResource); - when(clientResource.roles()).thenReturn(rolesResource); - when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); - - RoleRepresentation roleRep = new RoleRepresentation(); - roleRep.setName(ROLE_NAME); - roleRep.setId(ROLE_ID); - when(roleResource.toRepresentation()).thenReturn(roleRep); - - Optional result = roleService.getRoleByName(ROLE_NAME, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME); - - assertTrue(result.isPresent()); - assertEquals(ROLE_NAME, result.get().getName()); - } - - @Test - void testGetRoleByName_ClientRole_ClientNotFound() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.clients()).thenReturn(clientsResource); - when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList()); - - Optional result = roleService.getRoleByName(ROLE_NAME, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME); - - assertFalse(result.isPresent()); - } - - @Test - void testGetRoleByName_ClientRole_NullClientName() { - Optional result = roleService.getRoleByName(ROLE_NAME, REALM, TypeRole.CLIENT_ROLE, null); - - assertFalse(result.isPresent()); - } - - // ==================== Tests assignRolesToUser - Cas limites ==================== - - @Test - void testAssignRolesToUser_RealmRole_Success() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(realmResource.users()).thenReturn(usersResource); - when(usersResource.get(USER_ID)).thenReturn(userResource); - when(userResource.roles()).thenReturn(roleMappingResource); - when(roleMappingResource.realmLevel()).thenReturn(roleScopeResource); - - RoleRepresentation roleRep = new RoleRepresentation(); - roleRep.setName(ROLE_NAME); - when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); - when(roleResource.toRepresentation()).thenReturn(roleRep); - - RoleAssignmentDTO assignment = RoleAssignmentDTO.builder() - .userId(USER_ID) - .realmName(REALM) - .typeRole(TypeRole.REALM_ROLE) - .roleNames(List.of(ROLE_NAME)) - .build(); - - roleService.assignRolesToUser(assignment); - - verify(roleScopeResource).add(anyList()); - } - - @Test - void testAssignRolesToUser_ClientRole_Success() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.clients()).thenReturn(clientsResource); - when(realmResource.users()).thenReturn(usersResource); - when(usersResource.get(USER_ID)).thenReturn(userResource); - when(userResource.roles()).thenReturn(roleMappingResource); - - ClientRepresentation client = new ClientRepresentation(); - client.setId("client-123"); - when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client)); - when(clientsResource.get("client-123")).thenReturn(clientResource); - when(clientResource.roles()).thenReturn(rolesResource); - when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); - when(roleMappingResource.clientLevel("client-123")).thenReturn(roleScopeResource); - - RoleRepresentation roleRep = new RoleRepresentation(); - roleRep.setName(ROLE_NAME); - when(roleResource.toRepresentation()).thenReturn(roleRep); - - RoleAssignmentDTO assignment = RoleAssignmentDTO.builder() - .userId(USER_ID) - .realmName(REALM) - .typeRole(TypeRole.CLIENT_ROLE) - .clientName(CLIENT_NAME) - .roleNames(List.of(ROLE_NAME)) - .build(); - - roleService.assignRolesToUser(assignment); - - verify(roleScopeResource).add(anyList()); - } - - @Test - void testAssignRolesToUser_ClientRole_NullClientName() { - RoleAssignmentDTO assignment = RoleAssignmentDTO.builder() - .userId(USER_ID) - .realmName(REALM) - .typeRole(TypeRole.CLIENT_ROLE) - .clientName(null) - .roleNames(List.of(ROLE_NAME)) - .build(); - - assertThrows(IllegalArgumentException.class, () -> roleService.assignRolesToUser(assignment)); - } - - // ==================== Tests revokeRolesFromUser - Cas limites ==================== - - @Test - void testRevokeRolesFromUser_RealmRole_Success() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(realmResource.users()).thenReturn(usersResource); - when(usersResource.get(USER_ID)).thenReturn(userResource); - when(userResource.roles()).thenReturn(roleMappingResource); - when(roleMappingResource.realmLevel()).thenReturn(roleScopeResource); - - RoleRepresentation roleRep = new RoleRepresentation(); - roleRep.setName(ROLE_NAME); - when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); - when(roleResource.toRepresentation()).thenReturn(roleRep); - - RoleAssignmentDTO assignment = RoleAssignmentDTO.builder() - .userId(USER_ID) - .realmName(REALM) - .typeRole(TypeRole.REALM_ROLE) - .roleNames(List.of(ROLE_NAME)) - .build(); - - roleService.revokeRolesFromUser(assignment); - - verify(roleScopeResource).remove(anyList()); - } - - @Test - void testRevokeRolesFromUser_ClientRole_NullClientName() { - RoleAssignmentDTO assignment = RoleAssignmentDTO.builder() - .userId(USER_ID) - .realmName(REALM) - .typeRole(TypeRole.CLIENT_ROLE) - .clientName(null) - .roleNames(List.of(ROLE_NAME)) - .build(); - - assertThrows(IllegalArgumentException.class, () -> roleService.revokeRolesFromUser(assignment)); - } - - // ==================== Tests getAllUserRoles - Cas limites ==================== - - @Test - void testGetAllUserRoles_WithRealmAndClientRoles() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.users()).thenReturn(usersResource); - when(usersResource.get(USER_ID)).thenReturn(userResource); - when(userResource.roles()).thenReturn(roleMappingResource); - when(realmResource.clients()).thenReturn(clientsResource); - - // Mock realm roles - getUserRealmRoles is called first - RoleScopeResource realmRoleScope = mock(RoleScopeResource.class); - RoleRepresentation realmRole = new RoleRepresentation(); - realmRole.setName("realm-role"); - when(roleMappingResource.realmLevel()).thenReturn(realmRoleScope); - when(realmRoleScope.listAll()).thenReturn(List.of(realmRole)); - - // Mock client roles - getAllUserRoles calls getUserClientRoles for each client - // getAllUserRoles calls getUserClientRoles with client.getClientId() (CLIENT_NAME) - // getUserClientRoles then finds the client by clientId and uses the internal ID - ClientRepresentation client = new ClientRepresentation(); - client.setId("client-123"); // Internal ID - client.setClientId(CLIENT_NAME); // Client ID - when(clientsResource.findAll()).thenReturn(List.of(client)); - - // getUserClientRoles finds client by clientId - when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client)); - - // getUserClientRoles uses internal ID for clientLevel - RoleScopeResource clientRoleScope = mock(RoleScopeResource.class); - when(roleMappingResource.clientLevel("client-123")).thenReturn(clientRoleScope); - RoleRepresentation clientRole = new RoleRepresentation(); - clientRole.setName("client-role"); - when(clientRoleScope.listAll()).thenReturn(List.of(clientRole)); - - List result = roleService.getAllUserRoles(USER_ID, REALM); - - assertNotNull(result); - assertTrue(result.size() >= 1); - } - - @Test - void testGetAllUserRoles_WithExceptionInClientRoles() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.users()).thenReturn(usersResource); - when(usersResource.get(USER_ID)).thenReturn(userResource); - when(userResource.roles()).thenReturn(roleMappingResource); - when(roleMappingResource.realmLevel()).thenReturn(roleScopeResource); - when(realmResource.clients()).thenReturn(clientsResource); - - // Mock realm roles - RoleRepresentation realmRole = new RoleRepresentation(); - realmRole.setName("realm-role"); - when(roleScopeResource.listAll()).thenReturn(List.of(realmRole)); - - // Exception when getting clients - when(clientsResource.findAll()).thenThrow(new RuntimeException("Error")); - - // Should not throw, just log warning - List result = roleService.getAllUserRoles(USER_ID, REALM); - - assertNotNull(result); - assertEquals(1, result.size()); // Only realm roles - } - - // ==================== Tests addCompositeRoles - Cas limites ==================== - - @Test - void testAddCompositeRoles_RealmRole_ParentNotFound() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.list()).thenReturn(Collections.emptyList()); - - // getRoleById returns Optional.empty() when role not found, which causes NotFoundException - assertThrows(jakarta.ws.rs.NotFoundException.class, () -> - roleService.addCompositeRoles(ROLE_ID, List.of("child-1"), REALM, TypeRole.REALM_ROLE, null)); - } - - @Test - void testAddCompositeRoles_RealmRole_ChildNotFound() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - - RoleRepresentation parentRole = new RoleRepresentation(); - parentRole.setId(ROLE_ID); - parentRole.setName("parent"); - // Mock getRoleById to return parent role - when(rolesResource.list()).thenReturn(List.of(parentRole)); - when(rolesResource.get("parent")).thenReturn(roleResource); - when(roleResource.toRepresentation()).thenReturn(parentRole); - - // Child role not found - getRealmRoleById returns empty for child - // This means childRoleNames will be empty, so addComposites won't be called - // Should not throw, just log warning and skip - roleService.addCompositeRoles(ROLE_ID, List.of("child-id"), REALM, TypeRole.REALM_ROLE, null); - - // Verify that get was called for parent role - use lenient to avoid unnecessary stubbing - verify(rolesResource, atLeastOnce()).list(); - } - - @Test - void testAddCompositeRoles_ClientRole_ClientNotFound() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(realmResource.clients()).thenReturn(clientsResource); - - // Mock getRoleById to return a role - RoleRepresentation parentRole = new RoleRepresentation(); - parentRole.setId(ROLE_ID); - parentRole.setName("parent"); - when(rolesResource.list()).thenReturn(List.of(parentRole)); - when(rolesResource.get("parent")).thenReturn(roleResource); - when(roleResource.toRepresentation()).thenReturn(parentRole); - - when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList()); - - // When client not found, it throws IllegalArgumentException in removeCompositeRoles - // But in addCompositeRoles, it first checks getRoleById which may throw NotFoundException - // Actually, looking at the code, if client is not found, it throws IllegalArgumentException - // But getRoleById might throw NotFoundException first - assertThrows(jakarta.ws.rs.NotFoundException.class, () -> - roleService.addCompositeRoles(ROLE_ID, List.of("child-1"), REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME)); - } - - // ==================== Tests removeCompositeRoles - Cas limites ==================== - - @Test - void testRemoveCompositeRoles_RealmRole_ChildNotFound() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - - RoleRepresentation parentRole = new RoleRepresentation(); - parentRole.setId(ROLE_ID); - parentRole.setName("parent"); - when(rolesResource.list()).thenReturn(List.of(parentRole)); - - // Child role not found - getRealmRoleById returns empty, so childRoleNames will be empty - // Should not throw, just log warning and skip - roleService.removeCompositeRoles(ROLE_ID, List.of("child-id"), REALM, TypeRole.REALM_ROLE, null); - - // Verify that list was called - verify(rolesResource, atLeastOnce()).list(); - } - - @Test - void testRemoveCompositeRoles_ClientRole_ClientNotFound() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(realmResource.clients()).thenReturn(clientsResource); - - // Mock getRoleById to return a role - RoleRepresentation parentRole = new RoleRepresentation(); - parentRole.setId(ROLE_ID); - parentRole.setName("parent"); - when(rolesResource.list()).thenReturn(List.of(parentRole)); - when(rolesResource.get("parent")).thenReturn(roleResource); - when(roleResource.toRepresentation()).thenReturn(parentRole); - - when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList()); - - // When client not found, it throws IllegalArgumentException - // But getRoleById might throw NotFoundException first - assertThrows(jakarta.ws.rs.NotFoundException.class, () -> - roleService.removeCompositeRoles(ROLE_ID, List.of("child-1"), REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME)); - } - - // ==================== Tests getAllRealmRoles - Cas limites ==================== - - @Test - void testGetAllRealmRoles_RealmNotFound() { - // realmExists returns false, so it throws IllegalArgumentException - when(keycloakAdminClient.realmExists(REALM)).thenReturn(false); - - // But if realmExists throws an exception, it might be wrapped - // Let's test both cases - try { - roleService.getAllRealmRoles(REALM); - fail("Should have thrown an exception"); - } catch (IllegalArgumentException e) { - // Expected when realmExists returns false - assertTrue(e.getMessage().contains("n'existe pas")); - } catch (RuntimeException e) { - // Also possible if realmExists throws - assertTrue(e.getMessage().contains("n'existe pas") || - e.getMessage().contains("récupération des rôles realm")); - } - } - - @Test - void testGetAllRealmRoles_NotFoundException() { - when(keycloakAdminClient.realmExists(REALM)).thenReturn(true); - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.list()).thenThrow(new jakarta.ws.rs.NotFoundException()); - - assertThrows(IllegalArgumentException.class, () -> - roleService.getAllRealmRoles(REALM)); - } - - @Test - void testGetAllRealmRoles_ExceptionWith404() { - when(keycloakAdminClient.realmExists(REALM)).thenReturn(true); - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.list()).thenThrow(new RuntimeException("Server response is: 404")); - - assertThrows(IllegalArgumentException.class, () -> - roleService.getAllRealmRoles(REALM)); - } - - @Test - void testGetAllRealmRoles_ExceptionWithNotFound() { - when(keycloakAdminClient.realmExists(REALM)).thenReturn(true); - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.list()).thenThrow(new RuntimeException("Not Found")); - - assertThrows(IllegalArgumentException.class, () -> - roleService.getAllRealmRoles(REALM)); - } - - // ==================== Tests getAllClientRoles - Cas limites ==================== - - @Test - void testGetAllClientRoles_ClientNotFound() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.clients()).thenReturn(clientsResource); - when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList()); - - List result = roleService.getAllClientRoles(REALM, CLIENT_NAME); - - assertNotNull(result); - assertTrue(result.isEmpty()); - } - - // ==================== Tests createClientRole - Cas limites ==================== - - @Test - void testCreateClientRole_ClientNotFound() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.clients()).thenReturn(clientsResource); - when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList()); - - RoleDTO roleDTO = RoleDTO.builder() - .name(ROLE_NAME) - .build(); - - assertThrows(IllegalArgumentException.class, () -> - roleService.createClientRole(roleDTO, REALM, CLIENT_NAME)); - } - - @Test - void testCreateClientRole_RoleAlreadyExists() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.clients()).thenReturn(clientsResource); - - ClientRepresentation client = new ClientRepresentation(); - client.setId("client-123"); - when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client)); - when(clientsResource.get("client-123")).thenReturn(clientResource); - when(clientResource.roles()).thenReturn(rolesResource); - when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); - when(roleResource.toRepresentation()).thenReturn(new RoleRepresentation()); - - RoleDTO roleDTO = RoleDTO.builder() - .name(ROLE_NAME) - .build(); - - assertThrows(IllegalArgumentException.class, () -> - roleService.createClientRole(roleDTO, REALM, CLIENT_NAME)); - } - - // ==================== Tests countUsersWithRole - Cas limites ==================== - - @Test - void testCountUsersWithRole_RoleNotFound() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.list()).thenReturn(Collections.emptyList()); - - long count = roleService.countUsersWithRole(ROLE_ID, REALM, TypeRole.REALM_ROLE, null); - - assertEquals(0, count); - } - - @Test - void testCountUsersWithRole_Exception() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(realmResource.users()).thenReturn(usersResource); - - RoleRepresentation roleRep = new RoleRepresentation(); - roleRep.setId(ROLE_ID); - roleRep.setName(ROLE_NAME); - when(rolesResource.list()).thenReturn(List.of(roleRep)); - when(usersResource.list()).thenThrow(new RuntimeException("Error")); - - long count = roleService.countUsersWithRole(ROLE_ID, REALM, TypeRole.REALM_ROLE, null); - - assertEquals(0, count); // Should return 0 on exception - } -} - +package dev.lions.user.manager.service.impl; + +import dev.lions.user.manager.client.KeycloakAdminClient; +import dev.lions.user.manager.dto.role.RoleAssignmentDTO; +import dev.lions.user.manager.dto.role.RoleDTO; +import dev.lions.user.manager.enums.role.TypeRole; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.*; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * Tests d'intégration pour RoleServiceImpl - Cas limites et branches conditionnelles complexes + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class RoleServiceImplIntegrationTest { + + @Mock + private KeycloakAdminClient keycloakAdminClient; + + @Mock + private Keycloak keycloakInstance; + + @Mock + private RealmResource realmResource; + + @Mock + private RolesResource rolesResource; + + @Mock + private RoleResource roleResource; + + @Mock + private UsersResource usersResource; + + @Mock + private UserResource userResource; + + @Mock + private RoleMappingResource roleMappingResource; + + @Mock + private RoleScopeResource roleScopeResource; + + @Mock + private ClientsResource clientsResource; + + @Mock + private ClientResource clientResource; + + @InjectMocks + private RoleServiceImpl roleService; + + private static final String REALM = "test-realm"; + private static final String USER_ID = "user-123"; + private static final String ROLE_NAME = "admin"; + private static final String CLIENT_NAME = "test-client"; + private static final String ROLE_ID = "role-123"; + + // ==================== Tests getRoleByName - Cas limites ==================== + + @Test + void testGetRoleByName_RealmRole_Success() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); + + RoleRepresentation roleRep = new RoleRepresentation(); + roleRep.setName(ROLE_NAME); + roleRep.setId(ROLE_ID); + when(roleResource.toRepresentation()).thenReturn(roleRep); + + Optional result = roleService.getRoleByName(ROLE_NAME, REALM, TypeRole.REALM_ROLE, null); + + assertTrue(result.isPresent()); + assertEquals(ROLE_NAME, result.get().getName()); + } + + @Test + void testGetRoleByName_RealmRole_NotFound() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); + when(roleResource.toRepresentation()).thenThrow(new jakarta.ws.rs.NotFoundException()); + + Optional result = roleService.getRoleByName(ROLE_NAME, REALM, TypeRole.REALM_ROLE, null); + + assertFalse(result.isPresent()); + } + + @Test + void testGetRoleByName_ClientRole_Success() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.clients()).thenReturn(clientsResource); + + ClientRepresentation client = new ClientRepresentation(); + client.setId("client-123"); + client.setClientId(CLIENT_NAME); + when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client)); + when(clientsResource.get("client-123")).thenReturn(clientResource); + when(clientResource.roles()).thenReturn(rolesResource); + when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); + + RoleRepresentation roleRep = new RoleRepresentation(); + roleRep.setName(ROLE_NAME); + roleRep.setId(ROLE_ID); + when(roleResource.toRepresentation()).thenReturn(roleRep); + + Optional result = roleService.getRoleByName(ROLE_NAME, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME); + + assertTrue(result.isPresent()); + assertEquals(ROLE_NAME, result.get().getName()); + } + + @Test + void testGetRoleByName_ClientRole_ClientNotFound() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.clients()).thenReturn(clientsResource); + when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList()); + + Optional result = roleService.getRoleByName(ROLE_NAME, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME); + + assertFalse(result.isPresent()); + } + + @Test + void testGetRoleByName_ClientRole_NullClientName() { + Optional result = roleService.getRoleByName(ROLE_NAME, REALM, TypeRole.CLIENT_ROLE, null); + + assertFalse(result.isPresent()); + } + + // ==================== Tests assignRolesToUser - Cas limites ==================== + + @Test + void testAssignRolesToUser_RealmRole_Success() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(realmResource.users()).thenReturn(usersResource); + when(usersResource.get(USER_ID)).thenReturn(userResource); + when(userResource.roles()).thenReturn(roleMappingResource); + when(roleMappingResource.realmLevel()).thenReturn(roleScopeResource); + + RoleRepresentation roleRep = new RoleRepresentation(); + roleRep.setName(ROLE_NAME); + when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); + when(roleResource.toRepresentation()).thenReturn(roleRep); + + RoleAssignmentDTO assignment = RoleAssignmentDTO.builder() + .userId(USER_ID) + .realmName(REALM) + .typeRole(TypeRole.REALM_ROLE) + .roleNames(List.of(ROLE_NAME)) + .build(); + + roleService.assignRolesToUser(assignment); + + verify(roleScopeResource).add(anyList()); + } + + @Test + void testAssignRolesToUser_ClientRole_Success() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.clients()).thenReturn(clientsResource); + when(realmResource.users()).thenReturn(usersResource); + when(usersResource.get(USER_ID)).thenReturn(userResource); + when(userResource.roles()).thenReturn(roleMappingResource); + + ClientRepresentation client = new ClientRepresentation(); + client.setId("client-123"); + when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client)); + when(clientsResource.get("client-123")).thenReturn(clientResource); + when(clientResource.roles()).thenReturn(rolesResource); + when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); + when(roleMappingResource.clientLevel("client-123")).thenReturn(roleScopeResource); + + RoleRepresentation roleRep = new RoleRepresentation(); + roleRep.setName(ROLE_NAME); + when(roleResource.toRepresentation()).thenReturn(roleRep); + + RoleAssignmentDTO assignment = RoleAssignmentDTO.builder() + .userId(USER_ID) + .realmName(REALM) + .typeRole(TypeRole.CLIENT_ROLE) + .clientName(CLIENT_NAME) + .roleNames(List.of(ROLE_NAME)) + .build(); + + roleService.assignRolesToUser(assignment); + + verify(roleScopeResource).add(anyList()); + } + + @Test + void testAssignRolesToUser_ClientRole_NullClientName() { + RoleAssignmentDTO assignment = RoleAssignmentDTO.builder() + .userId(USER_ID) + .realmName(REALM) + .typeRole(TypeRole.CLIENT_ROLE) + .clientName(null) + .roleNames(List.of(ROLE_NAME)) + .build(); + + assertThrows(IllegalArgumentException.class, () -> roleService.assignRolesToUser(assignment)); + } + + // ==================== Tests revokeRolesFromUser - Cas limites ==================== + + @Test + void testRevokeRolesFromUser_RealmRole_Success() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(realmResource.users()).thenReturn(usersResource); + when(usersResource.get(USER_ID)).thenReturn(userResource); + when(userResource.roles()).thenReturn(roleMappingResource); + when(roleMappingResource.realmLevel()).thenReturn(roleScopeResource); + + RoleRepresentation roleRep = new RoleRepresentation(); + roleRep.setName(ROLE_NAME); + when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); + when(roleResource.toRepresentation()).thenReturn(roleRep); + + RoleAssignmentDTO assignment = RoleAssignmentDTO.builder() + .userId(USER_ID) + .realmName(REALM) + .typeRole(TypeRole.REALM_ROLE) + .roleNames(List.of(ROLE_NAME)) + .build(); + + roleService.revokeRolesFromUser(assignment); + + verify(roleScopeResource).remove(anyList()); + } + + @Test + void testRevokeRolesFromUser_ClientRole_NullClientName() { + RoleAssignmentDTO assignment = RoleAssignmentDTO.builder() + .userId(USER_ID) + .realmName(REALM) + .typeRole(TypeRole.CLIENT_ROLE) + .clientName(null) + .roleNames(List.of(ROLE_NAME)) + .build(); + + assertThrows(IllegalArgumentException.class, () -> roleService.revokeRolesFromUser(assignment)); + } + + // ==================== Tests getAllUserRoles - Cas limites ==================== + + @Test + void testGetAllUserRoles_WithRealmAndClientRoles() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.users()).thenReturn(usersResource); + when(usersResource.get(USER_ID)).thenReturn(userResource); + when(userResource.roles()).thenReturn(roleMappingResource); + when(realmResource.clients()).thenReturn(clientsResource); + + // Mock realm roles - getUserRealmRoles is called first + RoleScopeResource realmRoleScope = mock(RoleScopeResource.class); + RoleRepresentation realmRole = new RoleRepresentation(); + realmRole.setName("realm-role"); + when(roleMappingResource.realmLevel()).thenReturn(realmRoleScope); + when(realmRoleScope.listAll()).thenReturn(List.of(realmRole)); + + // Mock client roles - getAllUserRoles calls getUserClientRoles for each client + // getAllUserRoles calls getUserClientRoles with client.getClientId() (CLIENT_NAME) + // getUserClientRoles then finds the client by clientId and uses the internal ID + ClientRepresentation client = new ClientRepresentation(); + client.setId("client-123"); // Internal ID + client.setClientId(CLIENT_NAME); // Client ID + when(clientsResource.findAll()).thenReturn(List.of(client)); + + // getUserClientRoles finds client by clientId + when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client)); + + // getUserClientRoles uses internal ID for clientLevel + RoleScopeResource clientRoleScope = mock(RoleScopeResource.class); + when(roleMappingResource.clientLevel("client-123")).thenReturn(clientRoleScope); + RoleRepresentation clientRole = new RoleRepresentation(); + clientRole.setName("client-role"); + when(clientRoleScope.listAll()).thenReturn(List.of(clientRole)); + + List result = roleService.getAllUserRoles(USER_ID, REALM); + + assertNotNull(result); + assertTrue(result.size() >= 1); + } + + @Test + void testGetAllUserRoles_WithExceptionInClientRoles() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.users()).thenReturn(usersResource); + when(usersResource.get(USER_ID)).thenReturn(userResource); + when(userResource.roles()).thenReturn(roleMappingResource); + when(roleMappingResource.realmLevel()).thenReturn(roleScopeResource); + when(realmResource.clients()).thenReturn(clientsResource); + + // Mock realm roles + RoleRepresentation realmRole = new RoleRepresentation(); + realmRole.setName("realm-role"); + when(roleScopeResource.listAll()).thenReturn(List.of(realmRole)); + + // Exception when getting clients + when(clientsResource.findAll()).thenThrow(new RuntimeException("Error")); + + // Should not throw, just log warning + List result = roleService.getAllUserRoles(USER_ID, REALM); + + assertNotNull(result); + assertEquals(1, result.size()); // Only realm roles + } + + // ==================== Tests addCompositeRoles - Cas limites ==================== + + @Test + void testAddCompositeRoles_RealmRole_ParentNotFound() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.list()).thenReturn(Collections.emptyList()); + + // getRoleById returns Optional.empty() when role not found, which causes NotFoundException + assertThrows(jakarta.ws.rs.NotFoundException.class, () -> + roleService.addCompositeRoles(ROLE_ID, List.of("child-1"), REALM, TypeRole.REALM_ROLE, null)); + } + + @Test + void testAddCompositeRoles_RealmRole_ChildNotFound() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + + RoleRepresentation parentRole = new RoleRepresentation(); + parentRole.setId(ROLE_ID); + parentRole.setName("parent"); + // Mock getRoleById to return parent role + when(rolesResource.list()).thenReturn(List.of(parentRole)); + when(rolesResource.get("parent")).thenReturn(roleResource); + when(roleResource.toRepresentation()).thenReturn(parentRole); + + // Child role not found - getRealmRoleById returns empty for child + // This means childRoleNames will be empty, so addComposites won't be called + // Should not throw, just log warning and skip + roleService.addCompositeRoles(ROLE_ID, List.of("child-id"), REALM, TypeRole.REALM_ROLE, null); + + // Verify that get was called for parent role - use lenient to avoid unnecessary stubbing + verify(rolesResource, atLeastOnce()).list(); + } + + @Test + void testAddCompositeRoles_ClientRole_ClientNotFound() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(realmResource.clients()).thenReturn(clientsResource); + + // Mock getRoleById to return a role + RoleRepresentation parentRole = new RoleRepresentation(); + parentRole.setId(ROLE_ID); + parentRole.setName("parent"); + when(rolesResource.list()).thenReturn(List.of(parentRole)); + when(rolesResource.get("parent")).thenReturn(roleResource); + when(roleResource.toRepresentation()).thenReturn(parentRole); + + when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList()); + + // When client not found, it throws IllegalArgumentException in removeCompositeRoles + // But in addCompositeRoles, it first checks getRoleById which may throw NotFoundException + // Actually, looking at the code, if client is not found, it throws IllegalArgumentException + // But getRoleById might throw NotFoundException first + assertThrows(jakarta.ws.rs.NotFoundException.class, () -> + roleService.addCompositeRoles(ROLE_ID, List.of("child-1"), REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME)); + } + + // ==================== Tests removeCompositeRoles - Cas limites ==================== + + @Test + void testRemoveCompositeRoles_RealmRole_ChildNotFound() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + + RoleRepresentation parentRole = new RoleRepresentation(); + parentRole.setId(ROLE_ID); + parentRole.setName("parent"); + when(rolesResource.list()).thenReturn(List.of(parentRole)); + + // Child role not found - getRealmRoleById returns empty, so childRoleNames will be empty + // Should not throw, just log warning and skip + roleService.removeCompositeRoles(ROLE_ID, List.of("child-id"), REALM, TypeRole.REALM_ROLE, null); + + // Verify that list was called + verify(rolesResource, atLeastOnce()).list(); + } + + @Test + void testRemoveCompositeRoles_ClientRole_ClientNotFound() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(realmResource.clients()).thenReturn(clientsResource); + + // Mock getRoleById to return a role + RoleRepresentation parentRole = new RoleRepresentation(); + parentRole.setId(ROLE_ID); + parentRole.setName("parent"); + when(rolesResource.list()).thenReturn(List.of(parentRole)); + when(rolesResource.get("parent")).thenReturn(roleResource); + when(roleResource.toRepresentation()).thenReturn(parentRole); + + when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList()); + + // When client not found, it throws IllegalArgumentException + // But getRoleById might throw NotFoundException first + assertThrows(jakarta.ws.rs.NotFoundException.class, () -> + roleService.removeCompositeRoles(ROLE_ID, List.of("child-1"), REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME)); + } + + // ==================== Tests getAllRealmRoles - Cas limites ==================== + + @Test + void testGetAllRealmRoles_RealmNotFound() { + // realmExists returns false, so it throws IllegalArgumentException + when(keycloakAdminClient.realmExists(REALM)).thenReturn(false); + + // But if realmExists throws an exception, it might be wrapped + // Let's test both cases + try { + roleService.getAllRealmRoles(REALM); + fail("Should have thrown an exception"); + } catch (IllegalArgumentException e) { + // Expected when realmExists returns false + assertTrue(e.getMessage().contains("n'existe pas")); + } catch (RuntimeException e) { + // Also possible if realmExists throws + assertTrue(e.getMessage().contains("n'existe pas") || + e.getMessage().contains("récupération des rôles realm")); + } + } + + @Test + void testGetAllRealmRoles_NotFoundException() { + when(keycloakAdminClient.realmExists(REALM)).thenReturn(true); + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.list()).thenThrow(new jakarta.ws.rs.NotFoundException()); + + assertThrows(IllegalArgumentException.class, () -> + roleService.getAllRealmRoles(REALM)); + } + + @Test + void testGetAllRealmRoles_ExceptionWith404() { + when(keycloakAdminClient.realmExists(REALM)).thenReturn(true); + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.list()).thenThrow(new RuntimeException("Server response is: 404")); + + assertThrows(IllegalArgumentException.class, () -> + roleService.getAllRealmRoles(REALM)); + } + + @Test + void testGetAllRealmRoles_ExceptionWithNotFound() { + when(keycloakAdminClient.realmExists(REALM)).thenReturn(true); + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.list()).thenThrow(new RuntimeException("Not Found")); + + assertThrows(IllegalArgumentException.class, () -> + roleService.getAllRealmRoles(REALM)); + } + + // ==================== Tests getAllClientRoles - Cas limites ==================== + + @Test + void testGetAllClientRoles_ClientNotFound() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.clients()).thenReturn(clientsResource); + when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList()); + + List result = roleService.getAllClientRoles(REALM, CLIENT_NAME); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + // ==================== Tests createClientRole - Cas limites ==================== + + @Test + void testCreateClientRole_ClientNotFound() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.clients()).thenReturn(clientsResource); + when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList()); + + RoleDTO roleDTO = RoleDTO.builder() + .name(ROLE_NAME) + .build(); + + assertThrows(IllegalArgumentException.class, () -> + roleService.createClientRole(roleDTO, REALM, CLIENT_NAME)); + } + + @Test + void testCreateClientRole_RoleAlreadyExists() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.clients()).thenReturn(clientsResource); + + ClientRepresentation client = new ClientRepresentation(); + client.setId("client-123"); + when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client)); + when(clientsResource.get("client-123")).thenReturn(clientResource); + when(clientResource.roles()).thenReturn(rolesResource); + when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource); + when(roleResource.toRepresentation()).thenReturn(new RoleRepresentation()); + + RoleDTO roleDTO = RoleDTO.builder() + .name(ROLE_NAME) + .build(); + + assertThrows(IllegalArgumentException.class, () -> + roleService.createClientRole(roleDTO, REALM, CLIENT_NAME)); + } + + // ==================== Tests countUsersWithRole - Cas limites ==================== + + @Test + void testCountUsersWithRole_RoleNotFound() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.list()).thenReturn(Collections.emptyList()); + + long count = roleService.countUsersWithRole(ROLE_ID, REALM, TypeRole.REALM_ROLE, null); + + assertEquals(0, count); + } + + @Test + void testCountUsersWithRole_Exception() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(realmResource.users()).thenReturn(usersResource); + + RoleRepresentation roleRep = new RoleRepresentation(); + roleRep.setId(ROLE_ID); + roleRep.setName(ROLE_NAME); + when(rolesResource.list()).thenReturn(List.of(roleRep)); + when(usersResource.list()).thenThrow(new RuntimeException("Error")); + + long count = roleService.countUsersWithRole(ROLE_ID, REALM, TypeRole.REALM_ROLE, null); + + assertEquals(0, count); // Should return 0 on exception + } +} + diff --git a/src/test/java/dev/lions/user/manager/service/impl/RoleServiceImplTest.java b/src/test/java/dev/lions/user/manager/service/impl/RoleServiceImplTest.java index 22a5809..15edccd 100644 --- a/src/test/java/dev/lions/user/manager/service/impl/RoleServiceImplTest.java +++ b/src/test/java/dev/lions/user/manager/service/impl/RoleServiceImplTest.java @@ -1,128 +1,128 @@ -package dev.lions.user.manager.service.impl; - -import dev.lions.user.manager.client.KeycloakAdminClient; -import dev.lions.user.manager.dto.role.RoleAssignmentDTO; -import dev.lions.user.manager.dto.role.RoleDTO; -import dev.lions.user.manager.enums.role.TypeRole; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.keycloak.admin.client.Keycloak; -import org.keycloak.admin.client.resource.*; -import org.keycloak.representations.idm.RoleRepresentation; -import org.keycloak.representations.idm.UserRepresentation; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Collections; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class RoleServiceImplTest { - - @Mock - KeycloakAdminClient keycloakAdminClient; - - @Mock - Keycloak keycloakInstance; - - @Mock - RealmResource realmResource; - - @Mock - RolesResource rolesResource; - - @Mock - RoleResource roleResource; - - @Mock - UsersResource usersResource; - - @Mock - UserResource userResource; - - @Mock - RoleMappingResource roleMappingResource; - - @Mock - RoleScopeResource roleScopeResource; - - @InjectMocks - RoleServiceImpl roleService; - - private static final String REALM = "test-realm"; - - @Test - void testCreateRealmRole() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - - // Check not found initially, then return created role - RoleRepresentation createdRep = new RoleRepresentation(); - createdRep.setName("role"); - createdRep.setId("1"); - when(rolesResource.get("role")).thenReturn(roleResource); - when(roleResource.toRepresentation()).thenThrow(new jakarta.ws.rs.NotFoundException()) - .thenReturn(createdRep); - - // Mock create - doNothing().when(rolesResource).create(any(RoleRepresentation.class)); - - RoleDTO input = RoleDTO.builder().name("role").description("desc").build(); - - RoleDTO result = roleService.createRealmRole(input, REALM); - - assertNotNull(result); - assertEquals("role", result.getName()); - } - - @Test - void testDeleteRole() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - - // find by id logic uses list() - RoleRepresentation rep = new RoleRepresentation(); - rep.setId("1"); - rep.setName("role"); - when(rolesResource.list()).thenReturn(Collections.singletonList(rep)); - - roleService.deleteRole("1", REALM, TypeRole.REALM_ROLE, null); - - verify(rolesResource).deleteRole("role"); - } - - @Test - void testAssignRolesToUser() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm(REALM)).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(realmResource.users()).thenReturn(usersResource); - when(usersResource.get("u1")).thenReturn(userResource); - when(userResource.roles()).thenReturn(roleMappingResource); - when(roleMappingResource.realmLevel()).thenReturn(roleScopeResource); - - RoleRepresentation roleRep = new RoleRepresentation(); - roleRep.setName("role1"); - when(rolesResource.get("role1")).thenReturn(roleResource); - when(roleResource.toRepresentation()).thenReturn(roleRep); - - RoleAssignmentDTO assignment = RoleAssignmentDTO.builder() - .userId("u1") - .realmName(REALM) - .typeRole(TypeRole.REALM_ROLE) - .roleNames(Collections.singletonList("role1")) - .build(); - - roleService.assignRolesToUser(assignment); - - verify(roleScopeResource).add(anyList()); - } -} +package dev.lions.user.manager.service.impl; + +import dev.lions.user.manager.client.KeycloakAdminClient; +import dev.lions.user.manager.dto.role.RoleAssignmentDTO; +import dev.lions.user.manager.dto.role.RoleDTO; +import dev.lions.user.manager.enums.role.TypeRole; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.*; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class RoleServiceImplTest { + + @Mock + KeycloakAdminClient keycloakAdminClient; + + @Mock + Keycloak keycloakInstance; + + @Mock + RealmResource realmResource; + + @Mock + RolesResource rolesResource; + + @Mock + RoleResource roleResource; + + @Mock + UsersResource usersResource; + + @Mock + UserResource userResource; + + @Mock + RoleMappingResource roleMappingResource; + + @Mock + RoleScopeResource roleScopeResource; + + @InjectMocks + RoleServiceImpl roleService; + + private static final String REALM = "test-realm"; + + @Test + void testCreateRealmRole() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + + // Check not found initially, then return created role + RoleRepresentation createdRep = new RoleRepresentation(); + createdRep.setName("role"); + createdRep.setId("1"); + when(rolesResource.get("role")).thenReturn(roleResource); + when(roleResource.toRepresentation()).thenThrow(new jakarta.ws.rs.NotFoundException()) + .thenReturn(createdRep); + + // Mock create + doNothing().when(rolesResource).create(any(RoleRepresentation.class)); + + RoleDTO input = RoleDTO.builder().name("role").description("desc").build(); + + RoleDTO result = roleService.createRealmRole(input, REALM); + + assertNotNull(result); + assertEquals("role", result.getName()); + } + + @Test + void testDeleteRole() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + + // find by id logic uses list() + RoleRepresentation rep = new RoleRepresentation(); + rep.setId("1"); + rep.setName("role"); + when(rolesResource.list()).thenReturn(Collections.singletonList(rep)); + + roleService.deleteRole("1", REALM, TypeRole.REALM_ROLE, null); + + verify(rolesResource).deleteRole("role"); + } + + @Test + void testAssignRolesToUser() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm(REALM)).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(realmResource.users()).thenReturn(usersResource); + when(usersResource.get("u1")).thenReturn(userResource); + when(userResource.roles()).thenReturn(roleMappingResource); + when(roleMappingResource.realmLevel()).thenReturn(roleScopeResource); + + RoleRepresentation roleRep = new RoleRepresentation(); + roleRep.setName("role1"); + when(rolesResource.get("role1")).thenReturn(roleResource); + when(roleResource.toRepresentation()).thenReturn(roleRep); + + RoleAssignmentDTO assignment = RoleAssignmentDTO.builder() + .userId("u1") + .realmName(REALM) + .typeRole(TypeRole.REALM_ROLE) + .roleNames(Collections.singletonList("role1")) + .build(); + + roleService.assignRolesToUser(assignment); + + verify(roleScopeResource).add(anyList()); + } +} diff --git a/src/test/java/dev/lions/user/manager/service/impl/SyncServiceImplTest.java b/src/test/java/dev/lions/user/manager/service/impl/SyncServiceImplTest.java index 4206eec..d4e22a3 100644 --- a/src/test/java/dev/lions/user/manager/service/impl/SyncServiceImplTest.java +++ b/src/test/java/dev/lions/user/manager/service/impl/SyncServiceImplTest.java @@ -1,244 +1,244 @@ -package dev.lions.user.manager.service.impl; - -import dev.lions.user.manager.client.KeycloakAdminClient; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.keycloak.admin.client.Keycloak; -import org.keycloak.admin.client.resource.*; -import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.representations.idm.RoleRepresentation; -import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.representations.info.ServerInfoRepresentation; -import org.keycloak.representations.info.SystemInfoRepresentation; // Correct import -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -import org.mockito.quality.Strictness; -import org.mockito.junit.jupiter.MockitoSettings; - -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) -class SyncServiceImplTest { - - @Mock - KeycloakAdminClient keycloakAdminClient; - - @Mock - Keycloak keycloakInstance; - - @Mock - RealmsResource realmsResource; - - @Mock - RealmResource realmResource; - - @Mock - UsersResource usersResource; - - @Mock - RolesResource rolesResource; - - @Mock - ServerInfoResource serverInfoResource; - - @Mock - dev.lions.user.manager.server.impl.repository.SyncHistoryRepository syncHistoryRepository; - - @Mock - dev.lions.user.manager.server.impl.repository.SyncedUserRepository syncedUserRepository; - - @Mock - dev.lions.user.manager.server.impl.repository.SyncedRoleRepository syncedRoleRepository; - - @InjectMocks - SyncServiceImpl syncService; - - // Correcting inner class usage if needed, but assuming standard Keycloak - // representations - // ServerInfoRepresentation contains SystemInfoRepresentation - - @Test - void testSyncUsersFromRealm() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm("realm")).thenReturn(realmResource); - when(realmResource.users()).thenReturn(usersResource); - when(usersResource.list()).thenReturn(Collections.singletonList(new UserRepresentation())); - - int count = syncService.syncUsersFromRealm("realm"); - assertEquals(1, count); - } - - @Test - void testSyncRolesFromRealm() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm("realm")).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.list()).thenReturn(Collections.singletonList(new RoleRepresentation())); - - int count = syncService.syncRolesFromRealm("realm"); - assertEquals(1, count); - } - - @Test - void testSyncAllRealms() { - when(keycloakAdminClient.getAllRealms()).thenReturn(Collections.singletonList("realm1")); - - when(keycloakInstance.realm("realm1")).thenReturn(realmResource); - when(realmResource.users()).thenReturn(usersResource); - when(usersResource.list()).thenReturn(Collections.emptyList()); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.list()).thenReturn(Collections.emptyList()); - - Map result = syncService.syncAllRealms(); - assertTrue(result.containsKey("realm1")); - assertEquals(0, result.get("realm1")); - } - - @Test - void testIsKeycloakAvailable() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.serverInfo()).thenReturn(serverInfoResource); - when(serverInfoResource.getInfo()).thenReturn(new ServerInfoRepresentation()); - - assertTrue(syncService.isKeycloakAvailable()); - } - - @Test - void testGetKeycloakHealthInfo() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.serverInfo()).thenReturn(serverInfoResource); - - ServerInfoRepresentation info = new ServerInfoRepresentation(); - SystemInfoRepresentation systemInfo = new SystemInfoRepresentation(); - systemInfo.setVersion("1.0"); - info.setSystemInfo(systemInfo); - - when(serverInfoResource.getInfo()).thenReturn(info); - - Map health = syncService.getKeycloakHealthInfo(); - assertEquals("UP", health.get("status")); - assertEquals("1.0", health.get("version")); - } - - @Test - void testSyncUsersFromRealm_Exception() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm("realm")).thenReturn(realmResource); - when(realmResource.users()).thenReturn(usersResource); - when(usersResource.list()).thenThrow(new RuntimeException("Connection error")); - - assertThrows(RuntimeException.class, () -> syncService.syncUsersFromRealm("realm")); - } - - @Test - void testSyncRolesFromRealm_Exception() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm("realm")).thenReturn(realmResource); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.list()).thenThrow(new RuntimeException("Connection error")); - - assertThrows(RuntimeException.class, () -> syncService.syncRolesFromRealm("realm")); - } - - @Test - void testSyncAllRealms_WithException() { - when(keycloakAdminClient.getAllRealms()).thenReturn(Collections.singletonList("realm1")); - - when(keycloakInstance.realm("realm1")).thenReturn(realmResource); - when(realmResource.users()).thenReturn(usersResource); - when(usersResource.list()).thenThrow(new RuntimeException("Sync error")); - - Map result = syncService.syncAllRealms(); - assertTrue(result.containsKey("realm1")); - assertEquals(0, result.get("realm1")); // Should be 0 on error - } - - @Test - void testSyncAllRealms_ExceptionInFindAll() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realms()).thenReturn(realmsResource); - when(realmsResource.findAll()).thenThrow(new RuntimeException("Connection error")); - - Map result = syncService.syncAllRealms(); - assertTrue(result.isEmpty()); - } - - // Note: checkDataConsistency doesn't actually throw exceptions in the current - // implementation - // The try-catch block is there for future use, but currently always succeeds - // So we test the success path in testCheckDataConsistency_Success - - @Test - void testForceSyncRealm_Exception() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm("realm")).thenReturn(realmResource); - when(realmResource.users()).thenReturn(usersResource); - when(usersResource.list()).thenThrow(new RuntimeException("Sync error")); - - Map stats = syncService.forceSyncRealm("realm"); - assertEquals("FAILURE", stats.get("status")); - assertNotNull(stats.get("error")); - } - - @Test - void testIsKeycloakAvailable_Exception() { - when(keycloakAdminClient.getAllRealms()).thenThrow(new RuntimeException("Connection refused")); - - assertFalse(syncService.isKeycloakAvailable()); - } - - @Test - void testGetKeycloakHealthInfo_Exception() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.serverInfo()).thenReturn(serverInfoResource); - when(serverInfoResource.getInfo()).thenThrow(new RuntimeException("Connection error")); - - Map health = syncService.getKeycloakHealthInfo(); - assertNotNull(health); - // Either status=DOWN (HTTP fallback also fails) or status=UP (HTTP fallback succeeds) - assertNotNull(health.get("status")); - } - - @Test - void testCheckDataConsistency_Success() { - when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); - when(keycloakInstance.realm("realm")).thenReturn(realmResource); - when(realmResource.users()).thenReturn(usersResource); - when(usersResource.list()).thenReturn(Collections.emptyList()); - when(realmResource.roles()).thenReturn(rolesResource); - when(rolesResource.list()).thenReturn(Collections.emptyList()); - - when(syncedUserRepository.list(anyString(), anyString())).thenReturn(Collections.emptyList()); - when(syncedRoleRepository.list(anyString(), anyString())).thenReturn(Collections.emptyList()); - - Map report = syncService.checkDataConsistency("realm"); - - assertEquals("realm", report.get("realmName")); - assertEquals("OK", report.get("status")); - } - - @Test - void testGetLastSyncStatus() { - dev.lions.user.manager.server.impl.entity.SyncHistoryEntity entity = - new dev.lions.user.manager.server.impl.entity.SyncHistoryEntity(); - entity.setStatus("completed"); - entity.setSyncType("USER"); - entity.setItemsProcessed(5); - entity.setSyncDate(java.time.LocalDateTime.now()); - when(syncHistoryRepository.findLatestByRealm(eq("realm"), eq(1))) - .thenReturn(Collections.singletonList(entity)); - - Map status = syncService.getLastSyncStatus("realm"); - assertEquals("completed", status.get("status")); - assertNotNull(status.get("lastSyncDate")); - } -} +package dev.lions.user.manager.service.impl; + +import dev.lions.user.manager.client.KeycloakAdminClient; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.*; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.representations.info.ServerInfoRepresentation; +import org.keycloak.representations.info.SystemInfoRepresentation; // Correct import +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import org.mockito.quality.Strictness; +import org.mockito.junit.jupiter.MockitoSettings; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class SyncServiceImplTest { + + @Mock + KeycloakAdminClient keycloakAdminClient; + + @Mock + Keycloak keycloakInstance; + + @Mock + RealmsResource realmsResource; + + @Mock + RealmResource realmResource; + + @Mock + UsersResource usersResource; + + @Mock + RolesResource rolesResource; + + @Mock + ServerInfoResource serverInfoResource; + + @Mock + dev.lions.user.manager.server.impl.repository.SyncHistoryRepository syncHistoryRepository; + + @Mock + dev.lions.user.manager.server.impl.repository.SyncedUserRepository syncedUserRepository; + + @Mock + dev.lions.user.manager.server.impl.repository.SyncedRoleRepository syncedRoleRepository; + + @InjectMocks + SyncServiceImpl syncService; + + // Correcting inner class usage if needed, but assuming standard Keycloak + // representations + // ServerInfoRepresentation contains SystemInfoRepresentation + + @Test + void testSyncUsersFromRealm() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm("realm")).thenReturn(realmResource); + when(realmResource.users()).thenReturn(usersResource); + when(usersResource.list()).thenReturn(Collections.singletonList(new UserRepresentation())); + + int count = syncService.syncUsersFromRealm("realm"); + assertEquals(1, count); + } + + @Test + void testSyncRolesFromRealm() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm("realm")).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.list()).thenReturn(Collections.singletonList(new RoleRepresentation())); + + int count = syncService.syncRolesFromRealm("realm"); + assertEquals(1, count); + } + + @Test + void testSyncAllRealms() { + when(keycloakAdminClient.getAllRealms()).thenReturn(Collections.singletonList("realm1")); + + when(keycloakInstance.realm("realm1")).thenReturn(realmResource); + when(realmResource.users()).thenReturn(usersResource); + when(usersResource.list()).thenReturn(Collections.emptyList()); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.list()).thenReturn(Collections.emptyList()); + + Map result = syncService.syncAllRealms(); + assertTrue(result.containsKey("realm1")); + assertEquals(0, result.get("realm1")); + } + + @Test + void testIsKeycloakAvailable() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.serverInfo()).thenReturn(serverInfoResource); + when(serverInfoResource.getInfo()).thenReturn(new ServerInfoRepresentation()); + + assertTrue(syncService.isKeycloakAvailable()); + } + + @Test + void testGetKeycloakHealthInfo() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.serverInfo()).thenReturn(serverInfoResource); + + ServerInfoRepresentation info = new ServerInfoRepresentation(); + SystemInfoRepresentation systemInfo = new SystemInfoRepresentation(); + systemInfo.setVersion("1.0"); + info.setSystemInfo(systemInfo); + + when(serverInfoResource.getInfo()).thenReturn(info); + + Map health = syncService.getKeycloakHealthInfo(); + assertEquals("UP", health.get("status")); + assertEquals("1.0", health.get("version")); + } + + @Test + void testSyncUsersFromRealm_Exception() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm("realm")).thenReturn(realmResource); + when(realmResource.users()).thenReturn(usersResource); + when(usersResource.list()).thenThrow(new RuntimeException("Connection error")); + + assertThrows(RuntimeException.class, () -> syncService.syncUsersFromRealm("realm")); + } + + @Test + void testSyncRolesFromRealm_Exception() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm("realm")).thenReturn(realmResource); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.list()).thenThrow(new RuntimeException("Connection error")); + + assertThrows(RuntimeException.class, () -> syncService.syncRolesFromRealm("realm")); + } + + @Test + void testSyncAllRealms_WithException() { + when(keycloakAdminClient.getAllRealms()).thenReturn(Collections.singletonList("realm1")); + + when(keycloakInstance.realm("realm1")).thenReturn(realmResource); + when(realmResource.users()).thenReturn(usersResource); + when(usersResource.list()).thenThrow(new RuntimeException("Sync error")); + + Map result = syncService.syncAllRealms(); + assertTrue(result.containsKey("realm1")); + assertEquals(0, result.get("realm1")); // Should be 0 on error + } + + @Test + void testSyncAllRealms_ExceptionInFindAll() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realms()).thenReturn(realmsResource); + when(realmsResource.findAll()).thenThrow(new RuntimeException("Connection error")); + + Map result = syncService.syncAllRealms(); + assertTrue(result.isEmpty()); + } + + // Note: checkDataConsistency doesn't actually throw exceptions in the current + // implementation + // The try-catch block is there for future use, but currently always succeeds + // So we test the success path in testCheckDataConsistency_Success + + @Test + void testForceSyncRealm_Exception() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm("realm")).thenReturn(realmResource); + when(realmResource.users()).thenReturn(usersResource); + when(usersResource.list()).thenThrow(new RuntimeException("Sync error")); + + Map stats = syncService.forceSyncRealm("realm"); + assertEquals("FAILURE", stats.get("status")); + assertNotNull(stats.get("error")); + } + + @Test + void testIsKeycloakAvailable_Exception() { + when(keycloakAdminClient.getAllRealms()).thenThrow(new RuntimeException("Connection refused")); + + assertFalse(syncService.isKeycloakAvailable()); + } + + @Test + void testGetKeycloakHealthInfo_Exception() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.serverInfo()).thenReturn(serverInfoResource); + when(serverInfoResource.getInfo()).thenThrow(new RuntimeException("Connection error")); + + Map health = syncService.getKeycloakHealthInfo(); + assertNotNull(health); + // Either status=DOWN (HTTP fallback also fails) or status=UP (HTTP fallback succeeds) + assertNotNull(health.get("status")); + } + + @Test + void testCheckDataConsistency_Success() { + when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance); + when(keycloakInstance.realm("realm")).thenReturn(realmResource); + when(realmResource.users()).thenReturn(usersResource); + when(usersResource.list()).thenReturn(Collections.emptyList()); + when(realmResource.roles()).thenReturn(rolesResource); + when(rolesResource.list()).thenReturn(Collections.emptyList()); + + when(syncedUserRepository.list(anyString(), anyString())).thenReturn(Collections.emptyList()); + when(syncedRoleRepository.list(anyString(), anyString())).thenReturn(Collections.emptyList()); + + Map report = syncService.checkDataConsistency("realm"); + + assertEquals("realm", report.get("realmName")); + assertEquals("OK", report.get("status")); + } + + @Test + void testGetLastSyncStatus() { + dev.lions.user.manager.server.impl.entity.SyncHistoryEntity entity = + new dev.lions.user.manager.server.impl.entity.SyncHistoryEntity(); + entity.setStatus("completed"); + entity.setSyncType("USER"); + entity.setItemsProcessed(5); + entity.setSyncDate(java.time.LocalDateTime.now()); + when(syncHistoryRepository.findLatestByRealm(eq("realm"), eq(1))) + .thenReturn(Collections.singletonList(entity)); + + Map status = syncService.getLastSyncStatus("realm"); + assertEquals("completed", status.get("status")); + assertNotNull(status.get("lastSyncDate")); + } +} diff --git a/src/test/java/dev/lions/user/manager/service/impl/UserServiceImplCompleteTest.java b/src/test/java/dev/lions/user/manager/service/impl/UserServiceImplCompleteTest.java index d6e514b..ae3aa73 100644 --- a/src/test/java/dev/lions/user/manager/service/impl/UserServiceImplCompleteTest.java +++ b/src/test/java/dev/lions/user/manager/service/impl/UserServiceImplCompleteTest.java @@ -756,7 +756,7 @@ class UserServiceImplCompleteTest { when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource); UserRepresentation existing = new UserRepresentation(); existing.setUsername("existinguser"); - when(usersResource.search("existinguser", 0, 1, true)).thenReturn(List.of(existing)); + when(usersResource.searchByUsername("existinguser", true)).thenReturn(List.of(existing)); assertTrue(userService.usernameExists("existinguser", REALM)); } @@ -764,7 +764,7 @@ class UserServiceImplCompleteTest { @Test void testUsernameExists_False() { when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource); - when(usersResource.search("newuser", 0, 1, true)).thenReturn(Collections.emptyList()); + when(usersResource.searchByUsername("newuser", true)).thenReturn(Collections.emptyList()); assertFalse(userService.usernameExists("newuser", REALM)); } @@ -772,7 +772,7 @@ class UserServiceImplCompleteTest { @Test void testUsernameExists_Exception() { when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource); - when(usersResource.search("user", 0, 1, true)).thenThrow(new RuntimeException("error")); + when(usersResource.searchByUsername("user", true)).thenThrow(new RuntimeException("error")); assertFalse(userService.usernameExists("user", REALM)); // returns false on exception } @@ -969,7 +969,7 @@ class UserServiceImplCompleteTest { when(usersResource.searchByEmail("john@test.com", true)).thenReturn(Collections.emptyList()); // Username doesn't exist when(usersResource.search("\"john\"", 0, 1, true)).thenReturn(Collections.emptyList()); - when(usersResource.search("john", 0, 1, true)).thenReturn(Collections.emptyList()); + when(usersResource.searchByUsername("john", true)).thenReturn(Collections.emptyList()); // Mock create response jakarta.ws.rs.core.Response response = mock(jakarta.ws.rs.core.Response.class); @@ -998,7 +998,7 @@ class UserServiceImplCompleteTest { when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource); // Email doesn't exist when(usersResource.searchByEmail("john@test.com", true)).thenReturn(Collections.emptyList()); - when(usersResource.search("john", 0, 1, true)).thenReturn(Collections.emptyList()); + when(usersResource.searchByUsername("john", true)).thenReturn(Collections.emptyList()); jakarta.ws.rs.core.Response response = mock(jakarta.ws.rs.core.Response.class); when(response.getStatus()).thenReturn(201); diff --git a/src/test/java/dev/lions/user/manager/service/impl/UserServiceImplExtendedTest.java b/src/test/java/dev/lions/user/manager/service/impl/UserServiceImplExtendedTest.java index 4437887..fd56874 100644 --- a/src/test/java/dev/lions/user/manager/service/impl/UserServiceImplExtendedTest.java +++ b/src/test/java/dev/lions/user/manager/service/impl/UserServiceImplExtendedTest.java @@ -219,7 +219,7 @@ class UserServiceImplExtendedTest { when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource); when(usersResource.search("testuser", 0, 1, true)).thenThrow(new RuntimeException("Connection error")); - assertThrows(RuntimeException.class, () -> + assertThrows(RuntimeException.class, () -> userService.getUserByUsername("testuser", REALM)); } @@ -265,7 +265,7 @@ class UserServiceImplExtendedTest { UserRepresentation existingUser = new UserRepresentation(); existingUser.setUsername("existinguser"); existingUser.setEnabled(true); - when(usersResource.search("existinguser", 0, 1, true)).thenReturn(List.of(existingUser)); + when(usersResource.searchByUsername("existinguser", true)).thenReturn(List.of(existingUser)); UserDTO userDTO = UserDTO.builder() .username("existinguser") @@ -282,7 +282,7 @@ class UserServiceImplExtendedTest { @Test void testCreateUser_EmailExists() { when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource); - when(usersResource.search("newuser", 0, 1, true)).thenReturn(Collections.emptyList()); + when(usersResource.searchByUsername("newuser", true)).thenReturn(Collections.emptyList()); // emailExists calls searchByEmail which should return a non-empty list UserRepresentation existingUser = new UserRepresentation(); existingUser.setEmail("existing@example.com"); @@ -304,7 +304,7 @@ class UserServiceImplExtendedTest { @Test void testCreateUser_StatusNot201() { when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource); - when(usersResource.search("newuser", 0, 1, true)).thenReturn(Collections.emptyList()); + when(usersResource.searchByUsername("newuser", true)).thenReturn(Collections.emptyList()); UserDTO userDTO = UserDTO.builder() .username("newuser") @@ -323,7 +323,7 @@ class UserServiceImplExtendedTest { @Test void testCreateUser_WithTemporaryPassword() { when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource); - when(usersResource.search("newuser", 0, 1, true)).thenReturn(Collections.emptyList()); + when(usersResource.searchByUsername("newuser", true)).thenReturn(Collections.emptyList()); UserDTO userDTO = UserDTO.builder() .username("newuser") @@ -354,7 +354,7 @@ class UserServiceImplExtendedTest { @Test void testCreateUser_Exception() { when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource); - when(usersResource.search("newuser", 0, 1, true)).thenThrow(new RuntimeException("Connection error")); + when(usersResource.searchByUsername("newuser", true)).thenThrow(new RuntimeException("Connection error")); UserDTO userDTO = UserDTO.builder() .username("newuser") diff --git a/src/test/java/dev/lions/user/manager/service/impl/UserServiceImplIntegrationTest.java b/src/test/java/dev/lions/user/manager/service/impl/UserServiceImplIntegrationTest.java index 156e593..aa69ec2 100644 --- a/src/test/java/dev/lions/user/manager/service/impl/UserServiceImplIntegrationTest.java +++ b/src/test/java/dev/lions/user/manager/service/impl/UserServiceImplIntegrationTest.java @@ -460,7 +460,7 @@ class UserServiceImplIntegrationTest { UserRepresentation user = new UserRepresentation(); user.setUsername("existinguser"); - when(usersResource.search("existinguser", 0, 1, true)).thenReturn(List.of(user)); + when(usersResource.searchByUsername("existinguser", true)).thenReturn(List.of(user)); boolean exists = userService.usernameExists("existinguser", REALM); @@ -470,7 +470,7 @@ class UserServiceImplIntegrationTest { @Test void testUsernameExists_False() { when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource); - when(usersResource.search("nonexistent", 0, 1, true)).thenReturn(Collections.emptyList()); + when(usersResource.searchByUsername("nonexistent", true)).thenReturn(Collections.emptyList()); boolean exists = userService.usernameExists("nonexistent", REALM); @@ -480,7 +480,7 @@ class UserServiceImplIntegrationTest { @Test void testUsernameExists_Exception() { when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource); - when(usersResource.search("erroruser", 0, 1, true)).thenThrow(new RuntimeException("Error")); + when(usersResource.searchByUsername("erroruser", true)).thenThrow(new RuntimeException("Error")); boolean exists = userService.usernameExists("erroruser", REALM); 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 06f2620..dd5348e 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 @@ -106,7 +106,7 @@ class UserServiceImplTest { UserDTO newUser = UserDTO.builder().username("newuser").email("new@example.com").build(); // Check exists - when(usersResource.search("newuser", 0, 1, true)).thenReturn(Collections.emptyList()); + when(usersResource.searchByUsername("newuser", true)).thenReturn(Collections.emptyList()); when(usersResource.searchByEmail("new@example.com", true)).thenReturn(Collections.emptyList()); // Mock creation response diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index f7993d8..f6610ef 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -28,7 +28,7 @@ quarkus.log.category."dev.lions.user.manager".level=WARN # Base de données H2 pour @QuarkusTest (pas de Docker requis) quarkus.datasource.db-kind=h2 quarkus.datasource.jdbc.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=PostgreSQL -quarkus.hibernate-orm.database.generation=drop-and-create +quarkus.hibernate-orm.schema-management.strategy=drop-and-create quarkus.flyway.enabled=false # Désactiver tous les DevServices (Docker non disponible en local)