chore(quarkus-327): bump to Quarkus 3.27.3 LTS, make pom autonomous, fix UserServiceImpl tests (search → searchByUsername), rename deprecated config keys
Some checks failed
CI/CD Pipeline / pipeline (push) Failing after 1m57s

This commit is contained in:
2026-04-23 14:47:26 +00:00
parent 16240fedc1
commit 41d87451c9
84 changed files with 12016 additions and 11970 deletions

188
.gitignore vendored
View File

@@ -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/

View File

@@ -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

324
README.md
View File

@@ -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

View File

@@ -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

57
pom.xml
View File

@@ -4,11 +4,58 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.lions.user.manager</groupId>
<artifactId>lions-user-manager-parent</artifactId>
<version>1.0.0</version>
</parent>
<groupId>dev.lions.user.manager</groupId>
<version>1.1.0</version>
<properties>
<java.version>21</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<quarkus.platform.version>3.27.3</quarkus.platform.version>
<lombok.version>1.18.38</lombok.version>
<mapstruct.version>1.6.3</mapstruct.version>
<!-- Overrides BOM : Docker Desktop 29.x compat (bundled TC 1.21.3 / docker-java 3.4.2 OK) -->
<testcontainers.version>1.21.4</testcontainers.version>
<docker-java.version>3.4.2</docker-java.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus.platform</groupId>
<artifactId>quarkus-bom</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>${testcontainers.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>dev.lions.user.manager</groupId>
<artifactId>lions-user-manager-server-api</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Lombok : pas dans Quarkus BOM -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!-- MapStruct : pas dans Quarkus BOM -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<artifactId>lions-user-manager-server-impl-quarkus</artifactId>
<packaging>jar</packaging>

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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"]

View File

@@ -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<String> getAllRealms();
/**
* Récupère la liste des clientId d'un realm
* @param realmName nom du realm
* @return Liste des clientId
*/
java.util.List<String> 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<String> getAllRealms();
/**
* Récupère la liste des clientId d'un realm
* @param realmName nom du realm
* @return Liste des clientId
*/
java.util.List<String> getRealmClients(String realmName);
/**
* Ferme la connexion Keycloak
*/
void close();
/**
* Force la reconnexion
*/
void reconnect();
}

View File

@@ -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<String> 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<String> 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<Map<String, Object>> realmMaps = mapper.readValue(
response.body(), new TypeReference<>() {});
List<String> 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<String> 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<String> 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<Map<String, Object>> clientMaps = mapper.readValue(
response.body(), new TypeReference<>() {});
List<String> 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<String> 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<String> 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<Map<String, Object>> realmMaps = mapper.readValue(
response.body(), new TypeReference<>() {});
List<String> 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<String> 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<String> 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<Map<String, Object>> clientMaps = mapper.readValue(
response.body(), new TypeReference<>() {});
List<String> 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
}
}

View File

@@ -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);
}
}

View File

@@ -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 {
}
}

View File

@@ -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<String> 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<UserRepresentation> 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<RoleRepresentation> 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<String> 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<UserRepresentation> 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<RoleRepresentation> 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());
}
}

View File

@@ -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<RoleDTO> toDTOList(List<RoleRepresentation> 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<RoleRepresentation> toRepresentationList(List<RoleDTO> 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<RoleDTO> toDTOList(List<RoleRepresentation> 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<RoleRepresentation> toRepresentationList(List<RoleDTO> roleDTOs) {
if (roleDTOs == null) {
return List.of();
}
return roleDTOs.stream()
.map(RoleMapper::toRepresentation)
.collect(Collectors.toList());
}
}

View File

@@ -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<String, List<String>> 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<UserDTO> toDTOList(List<UserRepresentation> 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<String> 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<String, List<String>> 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<UserDTO> toDTOList(List<UserRepresentation> 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<String> 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()
);
}
}

View File

@@ -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<AuditLogDTO> 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<AuditLogDTO> 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<AuditLogDTO> 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<AuditLogDTO> 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<AuditLogDTO> 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<TypeActionAudit, Long> 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<String, Long> 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<String, Long> 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<String, Long> 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<AuditLogDTO> 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<AuditLogDTO> 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<AuditLogDTO> 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<AuditLogDTO> 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<AuditLogDTO> 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<TypeActionAudit, Long> 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<String, Long> 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<String, Long> 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<String, Long> 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);
}
}

View File

@@ -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<String, Object> getKeycloakHealth() {
Map<String, Object> 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<String, Object> getServiceStatus() {
Map<String, Object> 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<String, Object> getKeycloakHealth() {
Map<String, Object> 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<String, Object> getServiceStatus() {
Map<String, Object> 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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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<RealmAssignmentDTO> getAllAssignments() {
log.info("GET /api/realm-assignments - Récupération de toutes les affectations");
return realmAuthorizationService.getAllAssignments();
}
@Override
@RolesAllowed({ "admin", "user_manager" })
public List<RealmAssignmentDTO> getAssignmentsByUser(String userId) {
log.info("GET /api/realm-assignments/user/{}", userId);
return realmAuthorizationService.getAssignmentsByUser(userId);
}
@Override
@RolesAllowed({ "admin" })
public List<RealmAssignmentDTO> 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<String> 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<RealmAssignmentDTO> getAllAssignments() {
log.info("GET /api/realm-assignments - Récupération de toutes les affectations");
return realmAuthorizationService.getAllAssignments();
}
@Override
@RolesAllowed({ "admin", "user_manager" })
public List<RealmAssignmentDTO> getAssignmentsByUser(String userId) {
log.info("GET /api/realm-assignments/user/{}", userId);
return realmAuthorizationService.getAssignmentsByUser(userId);
}
@Override
@RolesAllowed({ "admin" })
public List<RealmAssignmentDTO> 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<String> 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);
}
}

View File

@@ -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<String> getAllRealms() {
log.info("GET /api/realms/list");
try {
List<String> 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<String> getRealmClients(String realmName) {
log.info("GET /api/realms/{}/clients", realmName);
try {
List<String> 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<String> getAllRealms() {
log.info("GET /api/realms/list");
try {
List<String> 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<String> getRealmClients(String realmName) {
log.info("GET /api/realms/{}/clients", realmName);
try {
List<String> 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);
}
}
}

View File

@@ -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<RoleDTO> 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<RoleDTO> 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<RoleDTO> 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<RoleDTO> 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<RoleDTO> 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<RoleDTO> 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<RoleDTO> 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<RoleDTO> parentRole = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null);
if (parentRole.isEmpty()) {
throw new RuntimeException("Rôle parent non trouvé");
}
List<String> childRoleIds = request.getRoleNames().stream()
.map(name -> {
Optional<RoleDTO> 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<RoleDTO> getComposites(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName) {
log.info("GET /api/roles/composite/{} - realm: {}", roleName, realmName);
Optional<RoleDTO> 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<RoleDTO> 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<RoleDTO> 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<RoleDTO> 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<RoleDTO> 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<RoleDTO> 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<RoleDTO> 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<RoleDTO> 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<RoleDTO> parentRole = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null);
if (parentRole.isEmpty()) {
throw new RuntimeException("Rôle parent non trouvé");
}
List<String> childRoleIds = request.getRoleNames().stream()
.map(name -> {
Optional<RoleDTO> 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<RoleDTO> getComposites(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName) {
log.info("GET /api/roles/composite/{} - realm: {}", roleName, realmName);
Optional<RoleDTO> 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);
}
}

View File

@@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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();
}
}
}

View File

@@ -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<UserRepresentation> 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<UserRepresentation> 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);
}
}
}

View File

@@ -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<String> 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<String> 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);
}
}

View File

@@ -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<SecurityIdentity> 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<SecurityIdentity> 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);
}
}

View File

@@ -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";
}
}
}

View File

@@ -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.
*
* <p>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.).</p>
*
* <p><b>Utilisation:</b></p>
* <pre>
* 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();
* </pre>
*
* @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.
* <p>Peut être null pour les actions système qui ne concernent pas un utilisateur spécifique.</p>
*/
@Column(name = "user_id", length = 255)
private String userId;
/**
* Type d'action effectuée (CREATION_UTILISATEUR, MODIFICATION_UTILISATEUR, etc.).
* <p>Stocké en tant que STRING pour faciliter la lecture en base de données.</p>
*/
@Column(name = "action", nullable = false, length = 100)
@Enumerated(EnumType.STRING)
private TypeActionAudit action;
/**
* Détails complémentaires sur l'action effectuée.
* <p>Peut contenir des informations contextuelles comme les champs modifiés,
* les raisons d'une action, ou des messages d'erreur.</p>
*/
@Column(name = "details", columnDefinition = "TEXT")
private String details;
/**
* Identifiant de l'utilisateur qui a effectué l'action.
* <p>Généralement l'username ou l'ID de l'administrateur/utilisateur connecté.</p>
*/
@Column(name = "auteur_action", nullable = false, length = 255)
private String auteurAction;
/**
* Timestamp précis de l'action.
* <p>Utilisé pour l'ordre chronologique des logs et le filtrage temporel.</p>
*/
@Column(name = "timestamp", nullable = false)
private LocalDateTime timestamp;
/**
* Adresse IP de l'auteur de l'action.
* <p>Utile pour la traçabilité et la détection d'anomalies.</p>
*/
@Column(name = "ip_address", length = 45)
private String ipAddress;
/**
* User-Agent du client (navigateur, application, etc.).
* <p>Permet d'identifier le type de client utilisé pour l'action.</p>
*/
@Column(name = "user_agent", length = 500)
private String userAgent;
/**
* Nom du realm Keycloak concerné.
* <p>Important dans un environnement multi-tenant pour isoler les logs par realm.</p>
*/
@Column(name = "realm_name", length = 255)
private String realmName;
/**
* Indique si l'action a réussi ou échoué.
* <p>Permet de filtrer facilement les actions en erreur pour analyse.</p>
*/
@Column(name = "success", nullable = false)
private Boolean success = true;
/**
* Message d'erreur en cas d'échec de l'action.
* <p>Null si success = true.</p>
*/
@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<AuditLogEntity> 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<AuditLogEntity> 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<AuditLogEntity> 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<AuditLogEntity> 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<AuditLogEntity> 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.
* <p>Utile pour la maintenance et le respect des politiques de rétention.</p>
*
* @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é.
* <p>Utile pour détecter des problèmes récurrents.</p>
*
* @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.
*
* <p>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.).</p>
*
* <p><b>Utilisation:</b></p>
* <pre>
* 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();
* </pre>
*
* @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.
* <p>Peut être null pour les actions système qui ne concernent pas un utilisateur spécifique.</p>
*/
@Column(name = "user_id", length = 255)
private String userId;
/**
* Type d'action effectuée (CREATION_UTILISATEUR, MODIFICATION_UTILISATEUR, etc.).
* <p>Stocké en tant que STRING pour faciliter la lecture en base de données.</p>
*/
@Column(name = "action", nullable = false, length = 100)
@Enumerated(EnumType.STRING)
private TypeActionAudit action;
/**
* Détails complémentaires sur l'action effectuée.
* <p>Peut contenir des informations contextuelles comme les champs modifiés,
* les raisons d'une action, ou des messages d'erreur.</p>
*/
@Column(name = "details", columnDefinition = "TEXT")
private String details;
/**
* Identifiant de l'utilisateur qui a effectué l'action.
* <p>Généralement l'username ou l'ID de l'administrateur/utilisateur connecté.</p>
*/
@Column(name = "auteur_action", nullable = false, length = 255)
private String auteurAction;
/**
* Timestamp précis de l'action.
* <p>Utilisé pour l'ordre chronologique des logs et le filtrage temporel.</p>
*/
@Column(name = "timestamp", nullable = false)
private LocalDateTime timestamp;
/**
* Adresse IP de l'auteur de l'action.
* <p>Utile pour la traçabilité et la détection d'anomalies.</p>
*/
@Column(name = "ip_address", length = 45)
private String ipAddress;
/**
* User-Agent du client (navigateur, application, etc.).
* <p>Permet d'identifier le type de client utilisé pour l'action.</p>
*/
@Column(name = "user_agent", length = 500)
private String userAgent;
/**
* Nom du realm Keycloak concerné.
* <p>Important dans un environnement multi-tenant pour isoler les logs par realm.</p>
*/
@Column(name = "realm_name", length = 255)
private String realmName;
/**
* Indique si l'action a réussi ou échoué.
* <p>Permet de filtrer facilement les actions en erreur pour analyse.</p>
*/
@Column(name = "success", nullable = false)
private Boolean success = true;
/**
* Message d'erreur en cas d'échec de l'action.
* <p>Null si success = true.</p>
*/
@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<AuditLogEntity> 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<AuditLogEntity> 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<AuditLogEntity> 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<AuditLogEntity> 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<AuditLogEntity> 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.
* <p>Utile pour la maintenance et le respect des politiques de rétention.</p>
*
* @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é.
* <p>Utile pour détecter des problèmes récurrents.</p>
*
* @param userId ID de l'utilisateur
* @return Nombre d'échecs
*/
public static long countFailuresByUserId(String userId) {
return count("userId = ?1 and success = false", userId);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}
}

View File

@@ -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 "";
}

View File

@@ -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).
*
* <p>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.</p>
*
* <p><b>Fonctionnalités:</b></p>
* <ul>
* <li>Conversion Entity → DTO pour lecture/API</li>
* <li>Conversion DTO → Entity pour persistance</li>
* <li>Mapping de listes pour opérations bulk</li>
* <li>Gestion automatique des types LocalDateTime</li>
* <li>Mapping des enums (TypeActionAudit)</li>
* </ul>
*
* <p><b>Utilisation:</b></p>
* <pre>
* {@literal @}Inject
* AuditLogMapper mapper;
*
* // Entity → DTO
* AuditLogDTO dto = mapper.toDTO(entity);
*
* // DTO → Entity
* AuditLogEntity entity = mapper.toEntity(dto);
*
* // Liste Entity → Liste DTO
* List&lt;AuditLogDTO&gt; dtos = mapper.toDTOList(entities);
* </pre>
*
* @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.
*
* <p>Mapping des champs Entity → DTO:</p>
* <ul>
* <li>id (Long) → id (String)</li>
* <li>userId → ressourceId</li>
* <li>action → typeAction</li>
* <li>details → description</li>
* <li>auteurAction → acteurUsername</li>
* <li>timestamp → dateAction</li>
* <li>ipAddress → ipAddress</li>
* <li>userAgent → userAgent</li>
* <li>realmName → realmName</li>
* <li>success → success</li>
* <li>errorMessage → errorMessage</li>
* </ul>
*
* @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.
*
* <p>Utilisé pour créer une nouvelle entité à persister depuis les données API.</p>
*
* <p><b>Note:</b> L'ID de l'entité sera null (auto-généré par la DB),
* même si l'ID du DTO est renseigné.</p>
*
* @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.
*
* <p>Utile pour les recherches qui retournent plusieurs résultats.</p>
*
* @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<AuditLogDTO> toDTOList(List<AuditLogEntity> entities);
/**
* Convertit une liste de DTOs en liste d'entités.
*
* <p>Utile pour les opérations d'import ou de création en masse.</p>
*
* @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<AuditLogEntity> toEntityList(List<AuditLogDTO> dtos);
/**
* Met à jour une entité existante avec les données d'un DTO.
*
* <p>Préserve l'ID de l'entité et ne met à jour que les champs
* présents dans le DTO.</p>
*
* <p><b>Utilisation:</b></p>
* <pre>
* AuditLogEntity existingEntity = AuditLogEntity.findById(id);
* mapper.updateEntityFromDTO(dto, existingEntity);
* existingEntity.persist();
* </pre>
*
* @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).
*
* <p>MapStruct appelle automatiquement cette méthode pour le mapping de l'ID.</p>
*
* @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é).
*
* <p>Utilisé lors de la conversion DTO → Entity si nécessaire.</p>
*
* @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).
*
* <p>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.</p>
*
* <p><b>Fonctionnalités:</b></p>
* <ul>
* <li>Conversion Entity → DTO pour lecture/API</li>
* <li>Conversion DTO → Entity pour persistance</li>
* <li>Mapping de listes pour opérations bulk</li>
* <li>Gestion automatique des types LocalDateTime</li>
* <li>Mapping des enums (TypeActionAudit)</li>
* </ul>
*
* <p><b>Utilisation:</b></p>
* <pre>
* {@literal @}Inject
* AuditLogMapper mapper;
*
* // Entity → DTO
* AuditLogDTO dto = mapper.toDTO(entity);
*
* // DTO → Entity
* AuditLogEntity entity = mapper.toEntity(dto);
*
* // Liste Entity → Liste DTO
* List&lt;AuditLogDTO&gt; dtos = mapper.toDTOList(entities);
* </pre>
*
* @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.
*
* <p>Mapping des champs Entity → DTO:</p>
* <ul>
* <li>id (Long) → id (String)</li>
* <li>userId → ressourceId</li>
* <li>action → typeAction</li>
* <li>details → description</li>
* <li>auteurAction → acteurUsername</li>
* <li>timestamp → dateAction</li>
* <li>ipAddress → ipAddress</li>
* <li>userAgent → userAgent</li>
* <li>realmName → realmName</li>
* <li>success → success</li>
* <li>errorMessage → errorMessage</li>
* </ul>
*
* @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.
*
* <p>Utilisé pour créer une nouvelle entité à persister depuis les données API.</p>
*
* <p><b>Note:</b> L'ID de l'entité sera null (auto-généré par la DB),
* même si l'ID du DTO est renseigné.</p>
*
* @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.
*
* <p>Utile pour les recherches qui retournent plusieurs résultats.</p>
*
* @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<AuditLogDTO> toDTOList(List<AuditLogEntity> entities);
/**
* Convertit une liste de DTOs en liste d'entités.
*
* <p>Utile pour les opérations d'import ou de création en masse.</p>
*
* @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<AuditLogEntity> toEntityList(List<AuditLogDTO> dtos);
/**
* Met à jour une entité existante avec les données d'un DTO.
*
* <p>Préserve l'ID de l'entité et ne met à jour que les champs
* présents dans le DTO.</p>
*
* <p><b>Utilisation:</b></p>
* <pre>
* AuditLogEntity existingEntity = AuditLogEntity.findById(id);
* mapper.updateEntityFromDTO(dto, existingEntity);
* existingEntity.persist();
* </pre>
*
* @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).
*
* <p>MapStruct appelle automatiquement cette méthode pour le mapping de l'ID.</p>
*
* @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é).
*
* <p>Utilisé lors de la conversion DTO → Entity si nécessaire.</p>
*
* @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;
}
}
}

View File

@@ -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<SyncHistoryDTO> toDTOList(List<SyncHistoryEntity> 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<SyncHistoryDTO> toDTOList(List<SyncHistoryEntity> entities);
@Named("longToString")
default String longToString(Long id) {
return id != null ? id.toString() : null;
}
}

View File

@@ -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<AuditLogEntity> {
public List<AuditLogEntity> search(String realmName,
String auteurAction,
LocalDateTime dateDebut,
LocalDateTime dateFin,
String typeAction,
Boolean success,
int page,
int pageSize) {
StringBuilder query = new StringBuilder("1=1");
Map<String, Object> 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<AuditLogEntity> {
public List<AuditLogEntity> search(String realmName,
String auteurAction,
LocalDateTime dateDebut,
LocalDateTime dateFin,
String typeAction,
Boolean success,
int page,
int pageSize) {
StringBuilder query = new StringBuilder("1=1");
Map<String, Object> 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();
}
}

View File

@@ -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<SyncHistoryEntity> {
public List<SyncHistoryEntity> 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<SyncHistoryEntity> {
public List<SyncHistoryEntity> findLatestByRealm(String realmName, int limit) {
return find("realmName = ?1 ORDER BY syncDate DESC", realmName)
.page(0, limit)
.list();
}
}

View File

@@ -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<SyncedRoleEntity> {
/**
* Remplace l'ensemble des snapshots de rôles pour un realm donné.
*/
public void replaceForRealm(String realmName, List<SyncedRoleEntity> 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<SyncedRoleEntity> {
/**
* Remplace l'ensemble des snapshots de rôles pour un realm donné.
*/
public void replaceForRealm(String realmName, List<SyncedRoleEntity> roles) {
delete("realmName", realmName);
persist(roles);
}
}

View File

@@ -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<SyncedUserEntity> {
/**
* Remplace l'ensemble des snapshots d'utilisateurs pour un realm donné.
*/
public void replaceForRealm(String realmName, List<SyncedUserEntity> 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<SyncedUserEntity> {
/**
* Remplace l'ensemble des snapshots d'utilisateurs pour un realm donné.
*/
public void replaceForRealm(String realmName, List<SyncedUserEntity> users) {
delete("realmName", realmName);
persist(users);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<AuditLogDTO> 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<AuditLogEntity> entities = auditLogRepository.search(null, acteurUserId, dateDebut, dateFin, null, null,
page,
pageSize);
return auditLogMapper.toDTOList(entities);
}
@Override
public List<AuditLogDTO> 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<AuditLogEntity> q = auditLogRepository.find("userId = ?1 or details like ?2", ressourceId, filter);
return auditLogMapper.toDTOList(q.page(page, pageSize).list());
}
@Override
public List<AuditLogDTO> findByTypeAction(@NotNull TypeActionAudit typeAction,
@NotBlank String realmName,
LocalDateTime dateDebut,
LocalDateTime dateFin,
int page,
int pageSize) {
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin,
typeAction.name(), null, page,
pageSize);
return auditLogMapper.toDTOList(entities);
}
@Override
public List<AuditLogDTO> findByRealm(@NotBlank String realmName,
LocalDateTime dateDebut,
LocalDateTime dateFin,
int page,
int pageSize) {
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, null, page,
pageSize);
return auditLogMapper.toDTOList(entities);
}
@Override
public List<AuditLogDTO> findFailures(@NotBlank String realmName,
LocalDateTime dateDebut,
LocalDateTime dateFin,
int page,
int pageSize) {
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, false,
page,
pageSize);
return auditLogMapper.toDTOList(entities);
}
@Override
public List<AuditLogDTO> findCriticalActions(@NotBlank String realmName,
LocalDateTime dateDebut,
LocalDateTime dateFin,
int page,
int pageSize) {
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, false,
page, pageSize);
return auditLogMapper.toDTOList(entities);
}
@Override
@SuppressWarnings("unchecked")
public Map<TypeActionAudit, Long> 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<Object[]> rows = query.getResultList();
Map<TypeActionAudit, Long> 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<String, Long> 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<Object[]> rows = query.getResultList();
Map<String, Long> result = new HashMap<>();
for (Object[] row : rows) {
result.put((String) row[0], ((Number) row[1]).longValue());
}
return result;
}
@Override
@SuppressWarnings("unchecked")
public Map<String, Long> 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<Object[]> rows = query.getResultList();
Map<String, Long> 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<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, null, 0, Integer.MAX_VALUE);
List<AuditLogDTO> 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<String, Object> getAuditStatistics(@NotBlank String realmName,
LocalDateTime dateDebut,
LocalDateTime dateFin) {
Map<String, Object> 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<AuditLogDTO> 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<AuditLogEntity> entities = auditLogRepository.search(null, acteurUserId, dateDebut, dateFin, null, null,
page,
pageSize);
return auditLogMapper.toDTOList(entities);
}
@Override
public List<AuditLogDTO> 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<AuditLogEntity> q = auditLogRepository.find("userId = ?1 or details like ?2", ressourceId, filter);
return auditLogMapper.toDTOList(q.page(page, pageSize).list());
}
@Override
public List<AuditLogDTO> findByTypeAction(@NotNull TypeActionAudit typeAction,
@NotBlank String realmName,
LocalDateTime dateDebut,
LocalDateTime dateFin,
int page,
int pageSize) {
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin,
typeAction.name(), null, page,
pageSize);
return auditLogMapper.toDTOList(entities);
}
@Override
public List<AuditLogDTO> findByRealm(@NotBlank String realmName,
LocalDateTime dateDebut,
LocalDateTime dateFin,
int page,
int pageSize) {
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, null, page,
pageSize);
return auditLogMapper.toDTOList(entities);
}
@Override
public List<AuditLogDTO> findFailures(@NotBlank String realmName,
LocalDateTime dateDebut,
LocalDateTime dateFin,
int page,
int pageSize) {
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, false,
page,
pageSize);
return auditLogMapper.toDTOList(entities);
}
@Override
public List<AuditLogDTO> findCriticalActions(@NotBlank String realmName,
LocalDateTime dateDebut,
LocalDateTime dateFin,
int page,
int pageSize) {
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, false,
page, pageSize);
return auditLogMapper.toDTOList(entities);
}
@Override
@SuppressWarnings("unchecked")
public Map<TypeActionAudit, Long> 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<Object[]> rows = query.getResultList();
Map<TypeActionAudit, Long> 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<String, Long> 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<Object[]> rows = query.getResultList();
Map<String, Long> result = new HashMap<>();
for (Object[] row : rows) {
result.put((String) row[0], ((Number) row[1]).longValue());
}
return result;
}
@Override
@SuppressWarnings("unchecked")
public Map<String, Long> 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<Object[]> rows = query.getResultList();
Map<String, Long> 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<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, null, 0, Integer.MAX_VALUE);
List<AuditLogDTO> 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<String, Object> getAuditStatistics(@NotBlank String realmName,
LocalDateTime dateDebut,
LocalDateTime dateFin) {
Map<String, Object> 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();
}
}

View File

@@ -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();
}
}

View File

@@ -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<String, RealmAssignmentDTO> assignmentsById = new ConcurrentHashMap<>();
private final Map<String, Set<String>> userToRealms = new ConcurrentHashMap<>();
private final Map<String, Set<String>> realmToUsers = new ConcurrentHashMap<>();
private final Set<String> superAdmins = ConcurrentHashMap.newKeySet();
@Override
public List<RealmAssignmentDTO> getAllAssignments() {
log.debug("Récupération de toutes les assignations de realms");
return new ArrayList<>(assignmentsById.values());
}
@Override
public List<RealmAssignmentDTO> 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<RealmAssignmentDTO> 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<RealmAssignmentDTO> 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<String> 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<RealmAssignmentDTO> 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<String> realms = userToRealms.get(userId);
if (realms != null) {
realms.remove(realmName);
if (realms.isEmpty()) {
userToRealms.remove(userId);
}
}
Set<String> 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<RealmAssignmentDTO> 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<RealmAssignmentDTO> 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<String, RealmAssignmentDTO> assignmentsById = new ConcurrentHashMap<>();
private final Map<String, Set<String>> userToRealms = new ConcurrentHashMap<>();
private final Map<String, Set<String>> realmToUsers = new ConcurrentHashMap<>();
private final Set<String> superAdmins = ConcurrentHashMap.newKeySet();
@Override
public List<RealmAssignmentDTO> getAllAssignments() {
log.debug("Récupération de toutes les assignations de realms");
return new ArrayList<>(assignmentsById.values());
}
@Override
public List<RealmAssignmentDTO> 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<RealmAssignmentDTO> 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<RealmAssignmentDTO> 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<String> 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<RealmAssignmentDTO> 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<String> realms = userToRealms.get(userId);
if (realms != null) {
realms.remove(realmName);
if (realms.isEmpty()) {
userToRealms.remove(userId);
}
}
Set<String> 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<RealmAssignmentDTO> 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<RealmAssignmentDTO> 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()
);
}
}

View File

@@ -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<UserRepresentation> 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<SyncedUserEntity> 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<RoleRepresentation> 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<SyncedRoleEntity> 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<String, Integer> syncAllRealms() {
Map<String, Integer> 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<String> 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<String, Object> checkDataConsistency(@NotBlank String realmName) {
Map<String, Object> report = new HashMap<>();
report.put("realmName", realmName);
try {
// Données actuelles dans Keycloak
List<UserRepresentation> kcUsers = keycloak.realm(realmName).users().list();
List<RoleRepresentation> kcRoles = keycloak.realm(realmName).roles().list();
// Snapshots locaux
List<SyncedUserEntity> localUsers = syncedUserRepository.list("realmName", realmName);
List<SyncedRoleEntity> localRoles = syncedRoleRepository.list("realmName", realmName);
// Comparaison exacte des identifiants utilisateurs
Set<String> kcUserIds = kcUsers.stream()
.map(UserRepresentation::getId)
.filter(id -> id != null && !id.isBlank())
.collect(java.util.stream.Collectors.toSet());
Set<String> localUserIds = localUsers.stream()
.map(SyncedUserEntity::getKeycloakId)
.filter(id -> id != null && !id.isBlank())
.collect(java.util.stream.Collectors.toSet());
Set<String> missingUsersInLocal = new HashSet<>(kcUserIds);
missingUsersInLocal.removeAll(localUserIds);
Set<String> missingUsersInKeycloak = new HashSet<>(localUserIds);
missingUsersInKeycloak.removeAll(kcUserIds);
// Comparaison exacte des noms de rôles
Set<String> kcRoleNames = kcRoles.stream()
.map(RoleRepresentation::getName)
.filter(name -> name != null && !name.isBlank())
.collect(java.util.stream.Collectors.toSet());
Set<String> localRoleNames = localRoles.stream()
.map(SyncedRoleEntity::getRoleName)
.filter(name -> name != null && !name.isBlank())
.collect(java.util.stream.Collectors.toSet());
Set<String> missingRolesInLocal = new HashSet<>(kcRoleNames);
missingRolesInLocal.removeAll(localRoleNames);
Set<String> 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<String, Object> forceSyncRealm(@NotBlank String realmName) {
Map<String, Object> 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<String, Object> getLastSyncStatus(@NotBlank String realmName) {
List<SyncHistoryEntity> history = syncHistoryRepository.findLatestByRealm(realmName, 1);
if (history.isEmpty()) {
return Collections.singletonMap("status", "NEVER_SYNCED");
}
SyncHistoryEntity lastSync = history.get(0);
Map<String, Object> 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<String, Object> getKeycloakHealthInfo() {
Map<String, Object> 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<String, Object> 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<String> 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<UserRepresentation> 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<SyncedUserEntity> 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<RoleRepresentation> 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<SyncedRoleEntity> 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<String, Integer> syncAllRealms() {
Map<String, Integer> 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<String> 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<String, Object> checkDataConsistency(@NotBlank String realmName) {
Map<String, Object> report = new HashMap<>();
report.put("realmName", realmName);
try {
// Données actuelles dans Keycloak
List<UserRepresentation> kcUsers = keycloak.realm(realmName).users().list();
List<RoleRepresentation> kcRoles = keycloak.realm(realmName).roles().list();
// Snapshots locaux
List<SyncedUserEntity> localUsers = syncedUserRepository.list("realmName", realmName);
List<SyncedRoleEntity> localRoles = syncedRoleRepository.list("realmName", realmName);
// Comparaison exacte des identifiants utilisateurs
Set<String> kcUserIds = kcUsers.stream()
.map(UserRepresentation::getId)
.filter(id -> id != null && !id.isBlank())
.collect(java.util.stream.Collectors.toSet());
Set<String> localUserIds = localUsers.stream()
.map(SyncedUserEntity::getKeycloakId)
.filter(id -> id != null && !id.isBlank())
.collect(java.util.stream.Collectors.toSet());
Set<String> missingUsersInLocal = new HashSet<>(kcUserIds);
missingUsersInLocal.removeAll(localUserIds);
Set<String> missingUsersInKeycloak = new HashSet<>(localUserIds);
missingUsersInKeycloak.removeAll(kcUserIds);
// Comparaison exacte des noms de rôles
Set<String> kcRoleNames = kcRoles.stream()
.map(RoleRepresentation::getName)
.filter(name -> name != null && !name.isBlank())
.collect(java.util.stream.Collectors.toSet());
Set<String> localRoleNames = localRoles.stream()
.map(SyncedRoleEntity::getRoleName)
.filter(name -> name != null && !name.isBlank())
.collect(java.util.stream.Collectors.toSet());
Set<String> missingRolesInLocal = new HashSet<>(kcRoleNames);
missingRolesInLocal.removeAll(localRoleNames);
Set<String> 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<String, Object> forceSyncRealm(@NotBlank String realmName) {
Map<String, Object> 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<String, Object> getLastSyncStatus(@NotBlank String realmName) {
List<SyncHistoryEntity> history = syncHistoryRepository.findLatestByRealm(realmName, 1);
if (history.isEmpty()) {
return Collections.singletonMap("status", "NEVER_SYNCED");
}
SyncHistoryEntity lastSync = history.get(0);
Map<String, Object> 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<String, Object> getKeycloakHealthInfo() {
Map<String, Object> 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<String, Object> 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<String> 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);
}
}
}

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://jakarta.ee/xml/ns/jakartajsf"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartajsf https://jakarta.ee/xml/ns/jakartajsf/beans_3_0.xsd"
bean-discovery-mode="annotated">
</beans>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://jakarta.ee/xml/ns/jakartajsf"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartajsf https://jakarta.ee/xml/ns/jakartajsf/beans_3_0.xsd"
bean-discovery-mode="annotated">
</beans>

View File

@@ -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
}
]

View File

@@ -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

View File

@@ -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
# ============================================

View File

@@ -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=*

View File

@@ -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
-- =============================================================================

View File

@@ -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
-- =============================================================================

View File

@@ -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<String> 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<String> 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<String> 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<String> 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"));
}
}

View File

@@ -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());
}
}

View File

@@ -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));
}
}

View File

@@ -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
}
}

View File

@@ -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<RoleDTO> 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<RoleDTO> 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<RoleDTO> 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<RoleDTO> 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);
}
}

View File

@@ -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<RoleRepresentation> reps = Collections.singletonList(rep);
List<RoleDTO> 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<RoleDTO> dtos = Collections.singletonList(dto);
List<RoleRepresentation> 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<RoleRepresentation> reps = Collections.singletonList(rep);
List<RoleDTO> 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<RoleDTO> dtos = Collections.singletonList(dto);
List<RoleRepresentation> reps = RoleMapper.toRepresentationList(dtos);
assertEquals(1, reps.size());
assertEquals("role", reps.get(0).getName());
assertTrue(RoleMapper.toRepresentationList(null).isEmpty());
}
}

View File

@@ -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<String, List<String>> 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<UserRepresentation> reps = Collections.singletonList(rep);
List<UserDTO> dtos = UserMapper.toDTOList(reps, "realm");
assertEquals(1, dtos.size());
assertTrue(UserMapper.toDTOList(null, "realm").isEmpty());
}
@Test
void testPrivateConstructor() throws Exception {
java.lang.reflect.Constructor<UserMapper> 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<String, List<String>> 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<UserRepresentation> reps = Collections.singletonList(rep);
List<UserDTO> dtos = UserMapper.toDTOList(reps, "realm");
assertEquals(1, dtos.size());
assertTrue(UserMapper.toDTOList(null, "realm").isEmpty());
}
@Test
void testPrivateConstructor() throws Exception {
java.lang.reflect.Constructor<UserMapper> constructor = UserMapper.class.getDeclaredConstructor();
assertTrue(java.lang.reflect.Modifier.isPrivate(constructor.getModifiers()));
constructor.setAccessible(true);
assertNotNull(constructor.newInstance());
}
}

View File

@@ -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<AuditLogDTO> 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<AuditLogDTO> result = auditResource.searchLogs("admin", null, null, null, null, null, 0, 50);
assertEquals(logs, result);
}
@Test
void testGetLogsByActor() {
List<AuditLogDTO> logs = Collections.singletonList(
AuditLogDTO.builder().acteurUsername("admin").build());
when(auditService.findByActeur(eq("admin"), isNull(), isNull(), eq(0), eq(100))).thenReturn(logs);
List<AuditLogDTO> result = auditResource.getLogsByActor("admin", 100);
assertEquals(logs, result);
}
@Test
void testGetLogsByResource() {
List<AuditLogDTO> logs = Collections.emptyList();
when(auditService.findByRessource(eq("USER"), eq("1"), any(), any(), eq(0), eq(100)))
.thenReturn(logs);
List<AuditLogDTO> result = auditResource.getLogsByResource("USER", "1", 100);
assertEquals(logs, result);
}
@Test
void testGetLogsByAction() {
List<AuditLogDTO> logs = Collections.emptyList();
when(auditService.findByTypeAction(eq(TypeActionAudit.USER_CREATE), eq("master"), any(), any(), eq(0), eq(100)))
.thenReturn(logs);
List<AuditLogDTO> result = auditResource.getLogsByAction(TypeActionAudit.USER_CREATE, null, null, 100);
assertEquals(logs, result);
}
@Test
void testGetActionStatistics() {
Map<TypeActionAudit, Long> stats = Map.of(TypeActionAudit.USER_CREATE, 10L);
when(auditService.countByActionType(eq("master"), any(), any())).thenReturn(stats);
Map<TypeActionAudit, Long> result = auditResource.getActionStatistics(null, null);
assertEquals(stats, result);
}
@Test
void testGetUserActivityStatistics() {
Map<String, Long> stats = Map.of("admin", 100L);
when(auditService.countByActeur(eq("master"), any(), any())).thenReturn(stats);
Map<String, Long> result = auditResource.getUserActivityStatistics(null, null);
assertEquals(stats, result);
}
@Test
void testGetFailureCount() {
Map<String, Long> 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<String, Long> 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<AuditLogDTO> 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<AuditLogDTO> 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<AuditLogDTO> logs = Collections.emptyList();
when(auditService.findByRealm(eq("master"), any(), any(), eq(0), eq(20))).thenReturn(logs);
List<AuditLogDTO> 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<AuditLogDTO> 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<AuditLogDTO> 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<AuditLogDTO> 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<AuditLogDTO> 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<AuditLogDTO> result = auditResource.searchLogs("admin", null, null, null, null, null, 0, 50);
assertEquals(logs, result);
}
@Test
void testGetLogsByActor() {
List<AuditLogDTO> logs = Collections.singletonList(
AuditLogDTO.builder().acteurUsername("admin").build());
when(auditService.findByActeur(eq("admin"), isNull(), isNull(), eq(0), eq(100))).thenReturn(logs);
List<AuditLogDTO> result = auditResource.getLogsByActor("admin", 100);
assertEquals(logs, result);
}
@Test
void testGetLogsByResource() {
List<AuditLogDTO> logs = Collections.emptyList();
when(auditService.findByRessource(eq("USER"), eq("1"), any(), any(), eq(0), eq(100)))
.thenReturn(logs);
List<AuditLogDTO> result = auditResource.getLogsByResource("USER", "1", 100);
assertEquals(logs, result);
}
@Test
void testGetLogsByAction() {
List<AuditLogDTO> logs = Collections.emptyList();
when(auditService.findByTypeAction(eq(TypeActionAudit.USER_CREATE), eq("master"), any(), any(), eq(0), eq(100)))
.thenReturn(logs);
List<AuditLogDTO> result = auditResource.getLogsByAction(TypeActionAudit.USER_CREATE, null, null, 100);
assertEquals(logs, result);
}
@Test
void testGetActionStatistics() {
Map<TypeActionAudit, Long> stats = Map.of(TypeActionAudit.USER_CREATE, 10L);
when(auditService.countByActionType(eq("master"), any(), any())).thenReturn(stats);
Map<TypeActionAudit, Long> result = auditResource.getActionStatistics(null, null);
assertEquals(stats, result);
}
@Test
void testGetUserActivityStatistics() {
Map<String, Long> stats = Map.of("admin", 100L);
when(auditService.countByActeur(eq("master"), any(), any())).thenReturn(stats);
Map<String, Long> result = auditResource.getUserActivityStatistics(null, null);
assertEquals(stats, result);
}
@Test
void testGetFailureCount() {
Map<String, Long> 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<String, Long> 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<AuditLogDTO> 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<AuditLogDTO> 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<AuditLogDTO> logs = Collections.emptyList();
when(auditService.findByRealm(eq("master"), any(), any(), eq(0), eq(20))).thenReturn(logs);
List<AuditLogDTO> 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<AuditLogDTO> 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<AuditLogDTO> 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<AuditLogDTO> 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));
}
}

View File

@@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> result = healthResourceEndpoint.getServiceStatus();
assertNotNull(result);
assertEquals("UP", result.get("status"));
assertEquals("ERROR", result.get("keycloak"));
assertEquals("Error", result.get("keycloakError"));
}
}

View File

@@ -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<RealmAssignmentDTO> assignments = Arrays.asList(assignment);
when(realmAuthorizationService.getAllAssignments()).thenReturn(assignments);
List<RealmAssignmentDTO> result = realmAssignmentResource.getAllAssignments();
assertEquals(1, result.size());
}
@Test
void testGetAssignmentsByUser_Success() {
List<RealmAssignmentDTO> assignments = Arrays.asList(assignment);
when(realmAuthorizationService.getAssignmentsByUser("user-1")).thenReturn(assignments);
List<RealmAssignmentDTO> result = realmAssignmentResource.getAssignmentsByUser("user-1");
assertEquals(1, result.size());
}
@Test
void testGetAssignmentsByRealm_Success() {
List<RealmAssignmentDTO> assignments = Arrays.asList(assignment);
when(realmAuthorizationService.getAssignmentsByRealm("realm1")).thenReturn(assignments);
List<RealmAssignmentDTO> 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<String> 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<RealmAssignmentDTO> assignments = Arrays.asList(assignment);
when(realmAuthorizationService.getAllAssignments()).thenReturn(assignments);
List<RealmAssignmentDTO> result = realmAssignmentResource.getAllAssignments();
assertEquals(1, result.size());
}
@Test
void testGetAssignmentsByUser_Success() {
List<RealmAssignmentDTO> assignments = Arrays.asList(assignment);
when(realmAuthorizationService.getAssignmentsByUser("user-1")).thenReturn(assignments);
List<RealmAssignmentDTO> result = realmAssignmentResource.getAssignmentsByUser("user-1");
assertEquals(1, result.size());
}
@Test
void testGetAssignmentsByRealm_Success() {
List<RealmAssignmentDTO> assignments = Arrays.asList(assignment);
when(realmAuthorizationService.getAssignmentsByRealm("realm1")).thenReturn(assignments);
List<RealmAssignmentDTO> 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<String> 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));
}
}

View File

@@ -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<String> realms = Arrays.asList("master", "lions-user-manager", "test-realm");
when(keycloakAdminClient.getAllRealms()).thenReturn(realms);
List<String> result = realmResource.getAllRealms();
assertNotNull(result);
assertEquals(3, result.size());
}
@Test
void testGetAllRealms_Empty() {
when(keycloakAdminClient.getAllRealms()).thenReturn(List.of());
List<String> 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<String> clients = Arrays.asList("admin-cli", "account", "lions-app");
when(keycloakAdminClient.getRealmClients("test-realm")).thenReturn(clients);
List<String> 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<String> 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<String> realms = Arrays.asList("master", "lions-user-manager", "test-realm");
when(keycloakAdminClient.getAllRealms()).thenReturn(realms);
List<String> result = realmResource.getAllRealms();
assertNotNull(result);
assertEquals(3, result.size());
}
@Test
void testGetAllRealms_Empty() {
when(keycloakAdminClient.getAllRealms()).thenReturn(List.of());
List<String> 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<String> clients = Arrays.asList("admin-cli", "account", "lions-app");
when(keycloakAdminClient.getRealmClients("test-realm")).thenReturn(clients);
List<String> 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<String> 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"));
}
}

View File

@@ -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<String> realms = Arrays.asList("master", "lions-user-manager", "btpxpress");
when(keycloakAdminClient.getAllRealms()).thenReturn(realms);
List<String> 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<String> 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<String> realms = Arrays.asList("master", "lions-user-manager", "btpxpress");
when(keycloakAdminClient.getAllRealms()).thenReturn(realms);
List<String> 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<String> 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());
}
}

View File

@@ -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<RoleDTO> roles = Collections.singletonList(RoleDTO.builder().name("role").build());
when(roleService.getAllRealmRoles(REALM)).thenReturn(roles);
List<RoleDTO> 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<RoleDTO> roles = Collections.singletonList(RoleDTO.builder().name("role").build());
when(roleService.getAllClientRoles(REALM, CLIENT_ID)).thenReturn(roles);
List<RoleDTO> 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<RoleDTO> roles = Collections.singletonList(RoleDTO.builder().name("role").build());
when(roleService.getUserRealmRoles("user1", REALM)).thenReturn(roles);
List<RoleDTO> result = roleResource.getUserRealmRoles("user1", REALM);
assertEquals(roles, result);
}
@Test
void testGetUserClientRoles() {
List<RoleDTO> roles = Collections.singletonList(RoleDTO.builder().name("role").build());
when(roleService.getUserClientRoles("user1", CLIENT_ID, REALM)).thenReturn(roles);
List<RoleDTO> 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<RoleDTO> 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<RoleDTO> 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<RoleDTO> roles = Collections.singletonList(RoleDTO.builder().name("role").build());
when(roleService.getAllRealmRoles(REALM)).thenReturn(roles);
List<RoleDTO> 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<RoleDTO> roles = Collections.singletonList(RoleDTO.builder().name("role").build());
when(roleService.getAllClientRoles(REALM, CLIENT_ID)).thenReturn(roles);
List<RoleDTO> 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<RoleDTO> roles = Collections.singletonList(RoleDTO.builder().name("role").build());
when(roleService.getUserRealmRoles("user1", REALM)).thenReturn(roles);
List<RoleDTO> result = roleResource.getUserRealmRoles("user1", REALM);
assertEquals(roles, result);
}
@Test
void testGetUserClientRoles() {
List<RoleDTO> roles = Collections.singletonList(RoleDTO.builder().name("role").build());
when(roleService.getUserClientRoles("user1", CLIENT_ID, REALM)).thenReturn(roles);
List<RoleDTO> 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<RoleDTO> 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<RoleDTO> 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));
}
}

View File

@@ -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());
}
}

View File

@@ -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"));
}
}

View File

@@ -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<String> 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<String> 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());
}
}

View File

@@ -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<SecurityContext> 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<SecurityContext> 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<SecurityContext> 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<SecurityContext> 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<SecurityContext> 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<SecurityContext> 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<SecurityContext> 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<SecurityContext> 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<SecurityContext> 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<SecurityContext> captor = ArgumentCaptor.forClass(SecurityContext.class);
producer.filter(requestContext);
verify(requestContext).setSecurityContext(captor.capture());
SecurityContext devCtx = captor.getValue();
assertEquals("DEV", devCtx.getAuthenticationScheme());
}
}

View File

@@ -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<AuditLogDTO> 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<AuditLogDTO> 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<AuditLogDTO> 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<AuditLogDTO> 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<AuditLogDTO> 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<AuditLogDTO> byType = auditService.findByTypeAction(TypeActionAudit.ROLE_CREATE, "realm", null, null, 0,
10);
assertNotNull(byType);
}
@Test
void testClearAll() {
auditService.clearAll();
verify(auditLogRepository).deleteAll();
}
}

View File

@@ -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<RealmAssignmentDTO> assignments = realmAuthorizationService.getAllAssignments();
assertTrue(assignments.isEmpty());
}
@Test
void testGetAllAssignments_WithAssignments() {
realmAuthorizationService.assignRealmToUser(assignment);
List<RealmAssignmentDTO> assignments = realmAuthorizationService.getAllAssignments();
assertEquals(1, assignments.size());
assertEquals("assignment-1", assignments.get(0).getId());
}
@Test
void testGetAssignmentsByUser_Success() {
realmAuthorizationService.assignRealmToUser(assignment);
List<RealmAssignmentDTO> assignments = realmAuthorizationService.getAssignmentsByUser("user-1");
assertEquals(1, assignments.size());
}
@Test
void testGetAssignmentsByUser_Empty() {
List<RealmAssignmentDTO> assignments = realmAuthorizationService.getAssignmentsByUser("user-1");
assertTrue(assignments.isEmpty());
}
@Test
void testGetAssignmentsByRealm_Success() {
realmAuthorizationService.assignRealmToUser(assignment);
List<RealmAssignmentDTO> assignments = realmAuthorizationService.getAssignmentsByRealm("realm1");
assertEquals(1, assignments.size());
}
@Test
void testGetAssignmentById_Success() {
realmAuthorizationService.assignRealmToUser(assignment);
Optional<RealmAssignmentDTO> found = realmAuthorizationService.getAssignmentById("assignment-1");
assertTrue(found.isPresent());
assertEquals("assignment-1", found.get().getId());
}
@Test
void testGetAssignmentById_NotFound() {
Optional<RealmAssignmentDTO> 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<String> realms = realmAuthorizationService.getAuthorizedRealms("user-1");
assertTrue(realms.isEmpty()); // Super admin retourne liste vide
}
@Test
void testGetAuthorizedRealms_WithAssignments() {
realmAuthorizationService.assignRealmToUser(assignment);
List<String> 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<RealmAssignmentDTO> 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<RealmAssignmentDTO> 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<RealmAssignmentDTO> assignments = realmAuthorizationService.getAllAssignments();
assertTrue(assignments.isEmpty());
}
@Test
void testGetAllAssignments_WithAssignments() {
realmAuthorizationService.assignRealmToUser(assignment);
List<RealmAssignmentDTO> assignments = realmAuthorizationService.getAllAssignments();
assertEquals(1, assignments.size());
assertEquals("assignment-1", assignments.get(0).getId());
}
@Test
void testGetAssignmentsByUser_Success() {
realmAuthorizationService.assignRealmToUser(assignment);
List<RealmAssignmentDTO> assignments = realmAuthorizationService.getAssignmentsByUser("user-1");
assertEquals(1, assignments.size());
}
@Test
void testGetAssignmentsByUser_Empty() {
List<RealmAssignmentDTO> assignments = realmAuthorizationService.getAssignmentsByUser("user-1");
assertTrue(assignments.isEmpty());
}
@Test
void testGetAssignmentsByRealm_Success() {
realmAuthorizationService.assignRealmToUser(assignment);
List<RealmAssignmentDTO> assignments = realmAuthorizationService.getAssignmentsByRealm("realm1");
assertEquals(1, assignments.size());
}
@Test
void testGetAssignmentById_Success() {
realmAuthorizationService.assignRealmToUser(assignment);
Optional<RealmAssignmentDTO> found = realmAuthorizationService.getAssignmentById("assignment-1");
assertTrue(found.isPresent());
assertEquals("assignment-1", found.get().getId());
}
@Test
void testGetAssignmentById_NotFound() {
Optional<RealmAssignmentDTO> 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<String> realms = realmAuthorizationService.getAuthorizedRealms("user-1");
assertTrue(realms.isEmpty()); // Super admin retourne liste vide
}
@Test
void testGetAuthorizedRealms_WithAssignments() {
realmAuthorizationService.assignRealmToUser(assignment);
List<String> 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<RealmAssignmentDTO> 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<RealmAssignmentDTO> 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"));
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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<String, Integer> 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<String, Object> 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<String, Integer> 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<String, Integer> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Integer> 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<String, Object> 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<String, Integer> 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<String, Integer> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> status = syncService.getLastSyncStatus("realm");
assertEquals("completed", status.get("status"));
assertNotNull(status.get("lastSyncDate"));
}
}

View File

@@ -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);

View File

@@ -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")

View File

@@ -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);

View File

@@ -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

View File

@@ -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)