feat(sprint-15 backend 2026-04-25): Live Activity Feed — endpoint /api/audit-trail/recent + tests
Some checks failed
CI/CD Pipeline / pipeline (push) Failing after 3m50s
Some checks failed
CI/CD Pipeline / pipeline (push) Failing after 3m50s
Transparency opérationnelle UnionFlow — opérations sensibles consultables en temps réel selon scope. Repository - findRecent(limit) : N opérations les plus récentes, toutes orgs - findRecentByOrganisation(orgId, limit) : pour scope ORG - findRecentByUser(userId, limit) : pour scope SELF - Tous clamps limit à [1, 500] (sécurité DoS) Service AuditTrailQueryService - listerRecentes(scope, orgId, userId, limit) : dispatcher SELF/ORG/ALL avec fallback liste vide si UUID requis manquant ; insensible à la casse Resource REST AuditTrailOperationResource - GET /api/audit-trail/recent?scope=SELF&orgId=...&userId=...&limit=50 - Default scope=SELF, limit=50 - @RolesAllowed étendu (MEMBRE inclus pour scope SELF) — un membre peut voir SES propres opérations Tests (7 nouveaux, 11/11 cumulé service) - recentes_All / Null defaults to ALL / Org / OrgWithoutId / Self / SelfWithoutUser / CaseInsensitive
This commit is contained in:
@@ -47,4 +47,21 @@ public class AuditTrailOperationRepository
|
||||
+ "ORDER BY operationAt DESC",
|
||||
organisationId, from, to);
|
||||
}
|
||||
|
||||
/** N opérations les plus récentes — toutes organisations confondues (Live Feed). */
|
||||
public List<AuditTrailOperation> findRecent(int limit) {
|
||||
return find("ORDER BY operationAt DESC").page(0, Math.max(1, Math.min(limit, 500))).list();
|
||||
}
|
||||
|
||||
/** N opérations les plus récentes pour une organisation. */
|
||||
public List<AuditTrailOperation> findRecentByOrganisation(UUID organisationId, int limit) {
|
||||
return find("organisationActiveId = ?1 ORDER BY operationAt DESC", organisationId)
|
||||
.page(0, Math.max(1, Math.min(limit, 500))).list();
|
||||
}
|
||||
|
||||
/** N opérations les plus récentes d'un utilisateur. */
|
||||
public List<AuditTrailOperation> findRecentByUser(UUID userId, int limit) {
|
||||
return find("userId = ?1 ORDER BY operationAt DESC", userId)
|
||||
.page(0, Math.max(1, Math.min(limit, 500))).list();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,30 @@ public class AuditTrailOperationResource {
|
||||
return queryService.operationsFinancieres(orgId, fromDt, toDt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Live Activity Feed (Sprint 15) — N opérations les plus récentes (transparency).
|
||||
*
|
||||
* <p>Scopes :
|
||||
* <ul>
|
||||
* <li>ALL — toutes orgs (restreint compliance/contrôleurs/super-admin)</li>
|
||||
* <li>ORG — organisation indiquée (admin org / président / officers)</li>
|
||||
* <li>SELF (défaut) — opérations de l'utilisateur indiqué (n'importe quel rôle peut voir les siennes)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Limit clampé à [1, 500] côté repository (sécurité contre DoS).
|
||||
*/
|
||||
@GET
|
||||
@Path("/recent")
|
||||
@RolesAllowed({"COMPLIANCE_OFFICER", "CONTROLEUR_INTERNE", "ADMIN_ORGANISATION",
|
||||
"PRESIDENT", "TRESORIER", "MEMBRE", "SUPER_ADMIN"})
|
||||
public List<AuditTrailOperationResponse> recent(
|
||||
@QueryParam("scope") @jakarta.ws.rs.DefaultValue("SELF") String scope,
|
||||
@QueryParam("orgId") UUID orgId,
|
||||
@QueryParam("userId") UUID userId,
|
||||
@QueryParam("limit") @jakarta.ws.rs.DefaultValue("50") int limit) {
|
||||
return queryService.listerRecentes(scope, orgId, userId, limit);
|
||||
}
|
||||
|
||||
private LocalDateTime parseDateTime(String input, LocalDateTime fallback) {
|
||||
if (input == null || input.isBlank()) return fallback;
|
||||
try {
|
||||
|
||||
@@ -52,6 +52,32 @@ public class AuditTrailQueryService {
|
||||
.map(this::toResponse).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Live Feed (Sprint 15) — N opérations les plus récentes selon le scope demandé.
|
||||
*
|
||||
* @param scope ALL (toutes orgs — restreint COMPLIANCE_OFFICER/CONTROLEUR_INTERNE/SUPER_ADMIN),
|
||||
* ORG (organisation passée), SELF (utilisateur passé)
|
||||
* @param organisationId requis pour scope=ORG
|
||||
* @param userId requis pour scope=SELF
|
||||
* @param limit clampé à [1, 500]
|
||||
*/
|
||||
public List<AuditTrailOperationResponse> listerRecentes(
|
||||
String scope, UUID organisationId, UUID userId, int limit) {
|
||||
String s = scope == null ? "ALL" : scope.toUpperCase();
|
||||
return switch (s) {
|
||||
case "ORG" -> organisationId == null
|
||||
? java.util.List.of()
|
||||
: repository.findRecentByOrganisation(organisationId, limit)
|
||||
.stream().map(this::toResponse).toList();
|
||||
case "SELF" -> userId == null
|
||||
? java.util.List.of()
|
||||
: repository.findRecentByUser(userId, limit)
|
||||
.stream().map(this::toResponse).toList();
|
||||
default -> repository.findRecent(limit)
|
||||
.stream().map(this::toResponse).toList();
|
||||
};
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Mapping Entity ↔ DTO
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -97,4 +97,65 @@ class AuditTrailQueryServiceTest {
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.get(0).entityType()).isEqualTo("Membre");
|
||||
}
|
||||
|
||||
// ── Sprint 15 : Live Activity Feed (listerRecentes) ─────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("listerRecentes scope=ALL appelle findRecent")
|
||||
void recentes_All() {
|
||||
when(repository.findRecent(50))
|
||||
.thenReturn(List.of(op("Membre", "CREATE", true), op("Cotisation", "UPDATE", true)));
|
||||
var result = service.listerRecentes("ALL", null, null, 50);
|
||||
assertThat(result).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("listerRecentes scope=null → ALL par défaut")
|
||||
void recentes_NullScopeDefaultsToAll() {
|
||||
when(repository.findRecent(20)).thenReturn(List.of(op("X", "CREATE", true)));
|
||||
var result = service.listerRecentes(null, null, null, 20);
|
||||
assertThat(result).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("listerRecentes scope=ORG appelle findRecentByOrganisation")
|
||||
void recentes_Org() {
|
||||
UUID orgId = UUID.randomUUID();
|
||||
when(repository.findRecentByOrganisation(orgId, 30))
|
||||
.thenReturn(List.of(op("Cotisation", "VALIDATE", true)));
|
||||
var result = service.listerRecentes("ORG", orgId, null, 30);
|
||||
assertThat(result).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("listerRecentes scope=ORG sans orgId → liste vide")
|
||||
void recentes_OrgWithoutId() {
|
||||
var result = service.listerRecentes("ORG", null, null, 30);
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("listerRecentes scope=SELF appelle findRecentByUser")
|
||||
void recentes_Self() {
|
||||
UUID userId = UUID.randomUUID();
|
||||
when(repository.findRecentByUser(userId, 25))
|
||||
.thenReturn(List.of(op("Profil", "UPDATE", true)));
|
||||
var result = service.listerRecentes("SELF", null, userId, 25);
|
||||
assertThat(result).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("listerRecentes scope=SELF sans userId → liste vide")
|
||||
void recentes_SelfWithoutUser() {
|
||||
var result = service.listerRecentes("SELF", null, null, 25);
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("listerRecentes scope insensible à la casse")
|
||||
void recentes_CaseInsensitive() {
|
||||
when(repository.findRecent(10)).thenReturn(List.of(op("X", "CREATE", true)));
|
||||
var result = service.listerRecentes("all", null, null, 10);
|
||||
assertThat(result).hasSize(1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user