This is an automated email from the ASF dual-hosted git repository.

ilgrosso pushed a commit to branch 4_1_X
in repository https://gitbox.apache.org/repos/asf/syncope.git


The following commit(s) were added to refs/heads/4_1_X by this push:
     new 74ca766cb2 [SYNCOPE-1978] Search audit events by username (#1443)
74ca766cb2 is described below

commit 74ca766cb28bedb8409ee64aec2181b31740cd7c
Author: Oleg Zimakov <[email protected]>
AuthorDate: Sat Jun 27 00:04:24 2026 -0700

    [SYNCOPE-1978] Search audit events by username (#1443)
---
 .../syncope/common/rest/api/beans/AuditQuery.java  | 20 ++++++
 .../org/apache/syncope/core/logic/AuditLogic.java  |  6 +-
 .../core/rest/cxf/service/AuditServiceImpl.java    |  1 +
 .../core/persistence/api/dao/AuditEventDAO.java    |  2 +
 .../core/persistence/jpa/dao/JPAAuditEventDAO.java | 29 ++++++++
 .../persistence/neo4j/dao/Neo4jAuditEventDAO.java  | 14 ++++
 .../dao/ElasticsearchAuditEventDAO.java            | 18 ++++-
 .../opensearch/dao/OpenSearchAuditEventDAO.java    | 18 ++++-
 .../org/apache/syncope/fit/core/AuditITCase.java   | 81 ++++++++++++++++++++++
 9 files changed, 183 insertions(+), 6 deletions(-)

diff --git 
a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AuditQuery.java
 
b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AuditQuery.java
index 9d82c912a0..631057691e 100644
--- 
a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AuditQuery.java
+++ 
b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AuditQuery.java
@@ -43,6 +43,11 @@ public class AuditQuery extends AbstractTimeframeQuery {
             return this;
         }
 
+        public Builder username(final String username) {
+            getInstance().getUsername().add(username);
+            return this;
+        }
+
         public Builder who(final String who) {
             getInstance().getWho().add(who);
             return this;
@@ -76,6 +81,8 @@ public class AuditQuery extends AbstractTimeframeQuery {
 
     private String entityKey;
 
+    private Set<String> username = new HashSet<>();
+
     private Set<String> who = new HashSet<>();
 
     private OpEvent.CategoryType type;
@@ -99,6 +106,19 @@ public class AuditQuery extends AbstractTimeframeQuery {
         this.entityKey = entityKey;
     }
 
+    @Parameter(name = "username", description = "username(s) embedded in the 
audited payload to match "
+            + "(the affected entity, as opposed to the 'who' author); "
+            + "may be repeated to match any of the given values", array =
+            @ArraySchema(schema = @Schema(implementation = String.class)))
+    public Set<String> getUsername() {
+        return username;
+    }
+
+    @QueryParam("username")
+    public void setUsername(final Set<String> username) {
+        this.username = username;
+    }
+
     @Parameter(name = "who", description = "audit event author(s) (username) 
to match; "
             + "may be repeated to match any of the given values", array =
             @ArraySchema(schema = @Schema(implementation = String.class)))
diff --git 
a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AuditLogic.java 
b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AuditLogic.java
index 291c5291e6..cff41724a4 100644
--- 
a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AuditLogic.java
+++ 
b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AuditLogic.java
@@ -275,6 +275,7 @@ public class AuditLogic extends 
AbstractTransactionalLogic<AuditConfTO> {
     @Transactional(readOnly = true)
     public Page<AuditEventTO> search(
             final String entityKey,
+            final Set<String> username,
             final Set<String> who,
             final OpEvent.CategoryType type,
             final String category,
@@ -285,10 +286,11 @@ public class AuditLogic extends 
AbstractTransactionalLogic<AuditConfTO> {
             final OffsetDateTime after,
             final Pageable pageable) {
 
-        long count = auditEventDAO.count(entityKey, who, type, category, 
subcategory, op, result, before, after);
+        long count = auditEventDAO.count(
+                entityKey, username, who, type, category, subcategory, op, 
result, before, after);
 
         List<AuditEventTO> matching = auditEventDAO.search(
-                entityKey, who, type, category, subcategory, op, result, 
before, after, pageable);
+                entityKey, username, who, type, category, subcategory, op, 
result, before, after, pageable);
 
         return new SyncopePage<>(matching, pageable, count);
     }
diff --git 
a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AuditServiceImpl.java
 
b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AuditServiceImpl.java
index 468830cf5e..0580f0953a 100644
--- 
a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AuditServiceImpl.java
+++ 
b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AuditServiceImpl.java
@@ -70,6 +70,7 @@ public class AuditServiceImpl extends AbstractService 
implements AuditService {
     public PagedResult<AuditEventTO> search(final AuditQuery auditQuery) {
         Page<AuditEventTO> result = logic.search(
                 auditQuery.getEntityKey(),
+                auditQuery.getUsername(),
                 auditQuery.getWho(),
                 auditQuery.getType(),
                 auditQuery.getCategory(),
diff --git 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AuditEventDAO.java
 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AuditEventDAO.java
index a2f9e2a107..5e83015b95 100644
--- 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AuditEventDAO.java
+++ 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AuditEventDAO.java
@@ -32,6 +32,7 @@ public interface AuditEventDAO {
 
     long count(
             String entityKey,
+            Set<String> username,
             Set<String> who,
             OpEvent.CategoryType type,
             String category,
@@ -56,6 +57,7 @@ public interface AuditEventDAO {
 
     List<AuditEventTO> search(
             String entityKey,
+            Set<String> username,
             Set<String> who,
             OpEvent.CategoryType type,
             String category,
diff --git 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAuditEventDAO.java
 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAuditEventDAO.java
index c9d9d8780c..e09e70286f 100644
--- 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAuditEventDAO.java
+++ 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAuditEventDAO.java
@@ -63,6 +63,31 @@ public class JPAAuditEventDAO extends AbstractAuditEventDAO 
implements AuditEven
             return this;
         }
 
+        // Unlike the entityKey predicate above (a constrained UUID, 
concatenated), the free-form username
+        // is bound as a query parameter and its LIKE metacharacters are 
escaped, so '%'/'_' in a username
+        // match literally and there is no injection surface.
+        // '#' is used as the LIKE escape char (instead of '\') and applied 
uniformly: a single '\' in
+        // native SQL is mishandled by MySQL/MariaDB and Oracle has no default 
LIKE escape, whereas '#'
+        // works across every supported database (this DAO is not subclassed 
per-database).
+        protected static String escapeForLike(final String value) {
+            return value.replace("#", "##").replace("%", "#%").replace("_", 
"#_");
+        }
+
+        public AuditEventCriteriaBuilder username(final Set<String> username, 
final List<Object> parameters) {
+            if (!CollectionUtils.isEmpty(username)) {
+                query.append(andIfNeeded()).append("(").
+                        append(username.stream().map(value -> {
+                            String pattern = "%\"username\":\"" + 
escapeForLike(value) + "\"%";
+                            return "(beforeValue LIKE ?" + 
setParameter(parameters, pattern) + " ESCAPE '#'"
+                                    + " OR inputs LIKE ?" + 
setParameter(parameters, pattern) + " ESCAPE '#'"
+                                    + " OR output LIKE ?" + 
setParameter(parameters, pattern) + " ESCAPE '#'"
+                                    + " OR throwable LIKE ?" + 
setParameter(parameters, pattern) + " ESCAPE '#')";
+                        }).collect(Collectors.joining(" OR "))).
+                        append(")");
+            }
+            return this;
+        }
+
         public AuditEventCriteriaBuilder who(final Set<String> who, final 
List<Object> parameters) {
             if (!CollectionUtils.isEmpty(who)) {
                 query.append(andIfNeeded()).append("who IN (").
@@ -140,6 +165,7 @@ public class JPAAuditEventDAO extends AbstractAuditEventDAO 
implements AuditEven
     @Override
     public long count(
             final String entityKey,
+            final Set<String> username,
             final Set<String> who,
             final OpEvent.CategoryType type,
             final String category,
@@ -153,6 +179,7 @@ public class JPAAuditEventDAO extends AbstractAuditEventDAO 
implements AuditEven
         String queryString = "SELECT COUNT(0)"
                 + " FROM " + JPAAuditEvent.TABLE
                 + " WHERE" + criteriaBuilder(entityKey).
+                        username(username, parameters).
                         who(who, parameters).
                         opEvent(type, category, subcategory, op, outcome).
                         before(before, parameters).
@@ -168,6 +195,7 @@ public class JPAAuditEventDAO extends AbstractAuditEventDAO 
implements AuditEven
     @Override
     public List<AuditEventTO> search(
             final String entityKey,
+            final Set<String> username,
             final Set<String> who,
             final OpEvent.CategoryType type,
             final String category,
@@ -182,6 +210,7 @@ public class JPAAuditEventDAO extends AbstractAuditEventDAO 
implements AuditEven
         String queryString = "SELECT id"
                 + " FROM " + JPAAuditEvent.TABLE
                 + " WHERE" + criteriaBuilder(entityKey).
+                        username(username, parameters).
                         who(who, parameters).
                         opEvent(type, category, subcategory, op, outcome).
                         before(before, parameters).
diff --git 
a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAuditEventDAO.java
 
b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAuditEventDAO.java
index 2c59bdbf0a..b976c3a162 100644
--- 
a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAuditEventDAO.java
+++ 
b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAuditEventDAO.java
@@ -60,6 +60,16 @@ public class Neo4jAuditEventDAO extends 
AbstractAuditEventDAO implements AuditEv
             return this;
         }
 
+        public AuditEventCriteriaBuilder username(final Set<String> username, 
final Map<String, Object> parameters) {
+            if (!CollectionUtils.isEmpty(username)) {
+                parameters.put("usernames", username.stream().map(value -> 
"\"username\":\"" + value + "\"").toList());
+                query.append(andIfNeeded()).
+                        append("ANY(u IN $usernames WHERE n.before CONTAINS u 
OR n.inputs CONTAINS u "
+                                + "OR n.output CONTAINS u OR n.throwable 
CONTAINS u)");
+            }
+            return this;
+        }
+
         public AuditEventCriteriaBuilder who(final Set<String> who, final 
Map<String, Object> parameters) {
             if (!CollectionUtils.isEmpty(who)) {
                 parameters.put("who", List.copyOf(who));
@@ -135,6 +145,7 @@ public class Neo4jAuditEventDAO extends 
AbstractAuditEventDAO implements AuditEv
     @Override
     public long count(
             final String entityKey,
+            final Set<String> username,
             final Set<String> who,
             final OpEvent.CategoryType type,
             final String category,
@@ -147,6 +158,7 @@ public class Neo4jAuditEventDAO extends 
AbstractAuditEventDAO implements AuditEv
         Map<String, Object> parameters = new HashMap<>();
         String query = "MATCH (n:" + Neo4jAuditEvent.NODE + ") "
                 + " WHERE " + criteriaBuilder(entityKey).
+                        username(username, parameters).
                         who(who, parameters).
                         opEvent(type, category, subcategory, op, outcome).
                         before(before, parameters).
@@ -160,6 +172,7 @@ public class Neo4jAuditEventDAO extends 
AbstractAuditEventDAO implements AuditEv
     @Override
     public List<AuditEventTO> search(
             final String entityKey,
+            final Set<String> username,
             final Set<String> who,
             final OpEvent.CategoryType type,
             final String category,
@@ -174,6 +187,7 @@ public class Neo4jAuditEventDAO extends 
AbstractAuditEventDAO implements AuditEv
 
         StringBuilder query = new StringBuilder("MATCH (n:" + 
Neo4jAuditEvent.NODE + ") "
                 + "WHERE " + criteriaBuilder(entityKey).
+                        username(username, parameters).
                         who(who, parameters).
                         opEvent(type, category, subcategory, op, outcome).
                         before(before, parameters).
diff --git 
a/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAuditEventDAO.java
 
b/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAuditEventDAO.java
index 2798489ee5..de1c2307d7 100644
--- 
a/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAuditEventDAO.java
+++ 
b/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAuditEventDAO.java
@@ -82,6 +82,7 @@ public class ElasticsearchAuditEventDAO implements 
AuditEventDAO {
 
     protected Query getQuery(
             final String entityKey,
+            final Set<String> username,
             final Set<String> who,
             final OpEvent.CategoryType type,
             final String category,
@@ -101,6 +102,17 @@ public class ElasticsearchAuditEventDAO implements 
AuditEventDAO {
                             query("\"key\":\"" + entityKey + 
"\"").build()).build());
         }
 
+        if (!CollectionUtils.isEmpty(username)) {
+            List<Query> usernameQueries = username.stream().map(value -> new 
Query.Builder().
+                    multiMatch(QueryBuilders.multiMatch().
+                            fields("before", "inputs", "output", "throwable").
+                            type(TextQueryType.Phrase).
+                            query("\"username\":\"" + value + 
"\"").build()).build()).
+                    toList();
+            queries.add(new Query.Builder().
+                    
bool(QueryBuilders.bool().should(usernameQueries).minimumShouldMatch("1").build()).build());
+        }
+
         if (!CollectionUtils.isEmpty(who)) {
             List<Query> whoQueries = who.stream().map(value -> new 
Query.Builder().
                     
term(QueryBuilders.term().field("who").value(value).build()).build()).
@@ -137,6 +149,7 @@ public class ElasticsearchAuditEventDAO implements 
AuditEventDAO {
     @Override
     public long count(
             final String entityKey,
+            final Set<String> username,
             final Set<String> who,
             final OpEvent.CategoryType type,
             final String category,
@@ -148,7 +161,7 @@ public class ElasticsearchAuditEventDAO implements 
AuditEventDAO {
 
         CountRequest request = new CountRequest.Builder().
                 
index(ElasticsearchUtils.getAuditIndex(AuthContextUtils.getDomain())).
-                query(getQuery(entityKey, who, type, category, subcategory, 
op, outcome, before, after)).
+                query(getQuery(entityKey, username, who, type, category, 
subcategory, op, outcome, before, after)).
                 build();
         LOG.debug("Count request: {}", request);
 
@@ -172,6 +185,7 @@ public class ElasticsearchAuditEventDAO implements 
AuditEventDAO {
     @Override
     public List<AuditEventTO> search(
             final String entityKey,
+            final Set<String> username,
             final Set<String> who,
             final OpEvent.CategoryType type,
             final String category,
@@ -185,7 +199,7 @@ public class ElasticsearchAuditEventDAO implements 
AuditEventDAO {
         SearchRequest request = new SearchRequest.Builder().
                 
index(ElasticsearchUtils.getAuditIndex(AuthContextUtils.getDomain())).
                 searchType(SearchType.QueryThenFetch).
-                query(getQuery(entityKey, who, type, category, subcategory, 
op, outcome, before, after)).
+                query(getQuery(entityKey, username, who, type, category, 
subcategory, op, outcome, before, after)).
                 from(pageable.isUnpaged() ? 0 : pageable.getPageSize() * 
pageable.getPageNumber()).
                 size(pageable.isUnpaged() ? indexMaxResultWindow : 
pageable.getPageSize()).
                 sort(sortBuilders(pageable.getSort().get())).
diff --git 
a/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAuditEventDAO.java
 
b/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAuditEventDAO.java
index 02bf3be892..eb5d63feaf 100644
--- 
a/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAuditEventDAO.java
+++ 
b/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAuditEventDAO.java
@@ -81,6 +81,7 @@ public class OpenSearchAuditEventDAO implements AuditEventDAO 
{
 
     protected Query getQuery(
             final String entityKey,
+            final Set<String> username,
             final Set<String> who,
             final OpEvent.CategoryType type,
             final String category,
@@ -100,6 +101,17 @@ public class OpenSearchAuditEventDAO implements 
AuditEventDAO {
                             query("\"key\":\"" + entityKey + 
"\"").build()).build());
         }
 
+        if (!CollectionUtils.isEmpty(username)) {
+            List<Query> usernameQueries = username.stream().map(value -> new 
Query.Builder().
+                    multiMatch(QueryBuilders.multiMatch().
+                            fields("before", "inputs", "output", "throwable").
+                            type(TextQueryType.Phrase).
+                            query("\"username\":\"" + value + 
"\"").build()).build()).
+                    toList();
+            queries.add(new Query.Builder().
+                    
bool(QueryBuilders.bool().should(usernameQueries).minimumShouldMatch("1").build()).build());
+        }
+
         if (!CollectionUtils.isEmpty(who)) {
             List<Query> whoQueries = who.stream().map(value -> new 
Query.Builder().
                     term(QueryBuilders.term().field("who").value(v -> 
v.stringValue(value)).build()).build()).
@@ -136,6 +148,7 @@ public class OpenSearchAuditEventDAO implements 
AuditEventDAO {
     @Override
     public long count(
             final String entityKey,
+            final Set<String> username,
             final Set<String> who,
             final OpEvent.CategoryType type,
             final String category,
@@ -147,7 +160,7 @@ public class OpenSearchAuditEventDAO implements 
AuditEventDAO {
 
         CountRequest request = new CountRequest.Builder().
                 
index(OpenSearchUtils.getAuditIndex(AuthContextUtils.getDomain())).
-                query(getQuery(entityKey, who, type, category, subcategory, 
op, outcome, before, after)).
+                query(getQuery(entityKey, username, who, type, category, 
subcategory, op, outcome, before, after)).
                 build();
         LOG.debug("Count request: {}", request);
 
@@ -171,6 +184,7 @@ public class OpenSearchAuditEventDAO implements 
AuditEventDAO {
     @Override
     public List<AuditEventTO> search(
             final String entityKey,
+            final Set<String> username,
             final Set<String> who,
             final OpEvent.CategoryType type,
             final String category,
@@ -184,7 +198,7 @@ public class OpenSearchAuditEventDAO implements 
AuditEventDAO {
         SearchRequest request = new SearchRequest.Builder().
                 
index(OpenSearchUtils.getAuditIndex(AuthContextUtils.getDomain())).
                 searchType(SearchType.QueryThenFetch).
-                query(getQuery(entityKey, who, type, category, subcategory, 
op, outcome, before, after)).
+                query(getQuery(entityKey, username, who, type, category, 
subcategory, op, outcome, before, after)).
                 from(pageable.isUnpaged() ? 0 : pageable.getPageSize() * 
pageable.getPageNumber()).
                 size(pageable.isUnpaged() ? indexMaxResultWindow : 
pageable.getPageSize()).
                 sort(sortBuilders(pageable.getSort().get())).
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuditITCase.java 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuditITCase.java
index 084914a9e2..d7565a51d9 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuditITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuditITCase.java
@@ -234,6 +234,87 @@ public class AuditITCase extends AbstractITCase {
         USER_SERVICE.delete(userTO.getKey());
     }
 
+    @Test
+    public void findByUsername() {
+        UserTO userTO = 
createUser(UserITCase.getUniqueSample("[email protected]")).getEntity();
+        assertNotNull(userTO.getKey());
+
+        // exact match on the username whose payload was logged for the create 
event
+        AuditQuery exact = new AuditQuery.Builder().
+                username(userTO.getUsername()).
+                type(OpEvent.CategoryType.LOGIC).
+                category(UserLogic.class.getSimpleName()).
+                op("create").
+                outcome(OpEvent.Outcome.SUCCESS).
+                build();
+        List<AuditEventTO> exactResult = query(exact, MAX_WAIT_SECONDS);
+        assertFalse(exactResult.isEmpty());
+        // the matched event genuinely carries this username in its payload 
(not matched by chance)
+        assertTrue(exactResult.stream().anyMatch(event -> (event.getOutput() 
!= null
+                && event.getOutput().contains("\"username\":\"" + 
userTO.getUsername() + "\""))
+                || (event.getBefore() != null
+                && event.getBefore().contains("\"username\":\"" + 
userTO.getUsername() + "\""))));
+
+        // multiple username values are OR'ed
+        AuditQuery multiple = new AuditQuery.Builder().
+                username(userTO.getUsername()).
+                username("non-existent-" + UUID.randomUUID()).
+                type(OpEvent.CategoryType.LOGIC).
+                category(UserLogic.class.getSimpleName()).
+                op("create").
+                outcome(OpEvent.Outcome.SUCCESS).
+                build();
+        assertFalse(AUDIT_SERVICE.search(multiple).getResult().isEmpty());
+
+        // a non-matching username excludes the event
+        AuditQuery noMatch = new AuditQuery.Builder().
+                username("non-existent-" + UUID.randomUUID()).
+                type(OpEvent.CategoryType.LOGIC).
+                category(UserLogic.class.getSimpleName()).
+                op("create").
+                outcome(OpEvent.Outcome.SUCCESS).
+                build();
+        assertTrue(AUDIT_SERVICE.search(noMatch).getResult().isEmpty());
+
+        // the match is exact: a strict prefix of the username does not match
+        AuditQuery prefix = new AuditQuery.Builder().
+                username(userTO.getUsername().substring(0, 
userTO.getUsername().length() - 1)).
+                type(OpEvent.CategoryType.LOGIC).
+                category(UserLogic.class.getSimpleName()).
+                op("create").
+                outcome(OpEvent.Outcome.SUCCESS).
+                build();
+        assertTrue(AUDIT_SERVICE.search(prefix).getResult().isEmpty());
+
+        USER_SERVICE.delete(userTO.getKey());
+    }
+
+    @Test
+    public void findDeletedUserByUsername() {
+        UserTO userTO = 
createUser(UserITCase.getUniqueSample("[email protected]")).getEntity();
+        String username = userTO.getUsername();
+        assertNotNull(userTO.getKey());
+
+        AuditQuery deleteQuery = new AuditQuery.Builder().
+                username(username).
+                type(OpEvent.CategoryType.LOGIC).
+                category(UserLogic.class.getSimpleName()).
+                op("delete").
+                outcome(OpEvent.Outcome.SUCCESS).
+                build();
+        try {
+            
AUDIT_SERVICE.setConf(buildAuditConf("[LOGIC]:[UserLogic]:[]:[delete]:[SUCCESS]",
 true));
+
+            USER_SERVICE.delete(userTO.getKey());
+
+            // the user is gone (its key can no longer be resolved), but the 
delete event's "before"
+            // snapshot still carries the username, so it remains searchable 
by username
+            assertFalse(query(deleteQuery, MAX_WAIT_SECONDS).isEmpty());
+        } finally {
+            
AUDIT_SERVICE.setConf(buildAuditConf("[LOGIC]:[UserLogic]:[]:[delete]:[SUCCESS]",
 false));
+        }
+    }
+
     @Test
     public void findByGroup() {
         GroupTO groupTO = 
createGroup(GroupITCase.getBasicSample("AuditGroup")).getEntity();

Reply via email to