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

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


The following commit(s) were added to refs/heads/master by this push:
     new 8fb31f6365 SYNCOPE-1744: restore notification template context after 
user delete (#1352)
8fb31f6365 is described below

commit 8fb31f63654e0bca9b292cb3bfbe7a0342a20e13
Author: Oleg Zimakov <[email protected]>
AuthorDate: Tue Apr 21 01:08:50 2026 -0700

    SYNCOPE-1744: restore notification template context after user delete 
(#1352)
---
 .../notification/DefaultNotificationManager.java   |  39 ++-
 .../DefaultNotificationManagerTest.java            | 287 +++++++++++++++++++++
 2 files changed, 314 insertions(+), 12 deletions(-)

diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/DefaultNotificationManager.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/DefaultNotificationManager.java
index 47486db9ea..330a2b4828 100644
--- 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/DefaultNotificationManager.java
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/DefaultNotificationManager.java
@@ -343,18 +343,33 @@ public class DefaultNotificationManager implements 
NotificationManager {
                     jexlVars.put("output", output);
                     jexlVars.put("input", input);
 
-                    any.ifPresent(a -> {
-                        switch (a) {
-                            case User user ->
-                                jexlVars.put("user", 
userDataBinder.getUserTO(user, true));
-                            case Group group ->
-                                jexlVars.put("group", 
groupDataBinder.getGroupTO(group, true));
-                            case AnyObject anyObject ->
-                                jexlVars.put("anyObject", 
anyObjectDataBinder.getAnyObjectTO(anyObject, true));
-                            default -> {
-                            }
-                        }
-                    });
+                    any.ifPresentOrElse(
+                            a -> {
+                                switch (a) {
+                                    case User user ->
+                                        jexlVars.put("user", 
userDataBinder.getUserTO(user, true));
+                                    case Group group ->
+                                        jexlVars.put("group", 
groupDataBinder.getGroupTO(group, true));
+                                    case AnyObject anyObject ->
+                                        jexlVars.put("anyObject", 
anyObjectDataBinder.getAnyObjectTO(anyObject, true));
+                                    default -> {
+                                    }
+                                }
+                            },
+                            () -> {
+                                switch (before) {
+                                    case null -> {
+                                    }
+                                    case UserTO userTO ->
+                                        jexlVars.put("user", userTO);
+                                    case GroupTO groupTO ->
+                                        jexlVars.put("group", groupTO);
+                                    case AnyObjectTO anyObjectTO ->
+                                        jexlVars.put("anyObject", anyObjectTO);
+                                    default -> {
+                                    }
+                                }
+                            });
 
                     NotificationTask notificationTask = 
getNotificationTask(notification, any.orElse(null), jexlVars);
                     notificationTask = taskDAO.save(notificationTask);
diff --git 
a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/notification/DefaultNotificationManagerTest.java
 
b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/notification/DefaultNotificationManagerTest.java
new file mode 100644
index 0000000000..8efdedc5eb
--- /dev/null
+++ 
b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/notification/DefaultNotificationManagerTest.java
@@ -0,0 +1,287 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.provisioning.java.notification;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.commons.jexl3.JexlBuilder;
+import org.apache.commons.jexl3.JexlEngine;
+import org.apache.commons.jexl3.MapContext;
+import org.apache.commons.jexl3.introspection.JexlPermissions;
+import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
+import org.apache.syncope.common.lib.Attr;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.lib.types.OpEvent;
+import org.apache.syncope.common.lib.types.TraceLevel;
+import org.apache.syncope.core.persistence.api.dao.AnyMatchDAO;
+import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
+import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
+import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.GroupDAO;
+import org.apache.syncope.core.persistence.api.dao.NotificationDAO;
+import org.apache.syncope.core.persistence.api.dao.RelationshipTypeDAO;
+import org.apache.syncope.core.persistence.api.dao.TaskDAO;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.MailTemplate;
+import org.apache.syncope.core.persistence.api.entity.Notification;
+import org.apache.syncope.core.persistence.api.entity.task.NotificationTask;
+import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor;
+import org.apache.syncope.core.provisioning.api.DerAttrHandler;
+import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
+import org.apache.syncope.core.provisioning.api.data.AnyObjectDataBinder;
+import org.apache.syncope.core.provisioning.api.data.GroupDataBinder;
+import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
+import org.apache.syncope.core.provisioning.api.jexl.EmptyClassLoader;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
+import org.apache.syncope.core.provisioning.api.jexl.SyncopeJexlFunctions;
+import org.apache.syncope.core.spring.security.AuthContextUtils;
+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 org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+public class DefaultNotificationManagerTest {
+
+    private static final String DELETE_SUCCESS = OpEvent.toString(
+            OpEvent.CategoryType.LOGIC, "UserLogic", null, "delete", 
OpEvent.Outcome.SUCCESS);
+
+    @Mock
+    private DerSchemaDAO derSchemaDAO;
+
+    @Mock
+    private NotificationDAO notificationDAO;
+
+    @Mock
+    private AnyObjectDAO anyObjectDAO;
+
+    @Mock
+    private UserDAO userDAO;
+
+    @Mock
+    private GroupDAO groupDAO;
+
+    @Mock
+    private AnySearchDAO anySearchDAO;
+
+    @Mock
+    private AnyMatchDAO anyMatchDAO;
+
+    @Mock
+    private TaskDAO taskDAO;
+
+    @Mock
+    private RelationshipTypeDAO relationshipTypeDAO;
+
+    @Mock
+    private DerAttrHandler derAttrHandler;
+
+    @Mock
+    private UserDataBinder userDataBinder;
+
+    @Mock
+    private GroupDataBinder groupDataBinder;
+
+    @Mock
+    private AnyObjectDataBinder anyObjectDataBinder;
+
+    @Mock
+    private ConfParamOps confParamOps;
+
+    @Mock
+    private EntityFactory entityFactory;
+
+    @Mock
+    private IntAttrNameParser intAttrNameParser;
+
+    @Mock
+    private AnySearchCondVisitor searchCondVisitor;
+
+    private JexlTools jexlTools;
+
+    private DefaultNotificationManager manager;
+
+    @BeforeEach
+    void init() {
+        JexlEngine jexlEngine = new JexlBuilder().
+                loader(new EmptyClassLoader()).
+                permissions(JexlPermissions.RESTRICTED.compose("java.time.*", 
"org.apache.syncope.*")).
+                namespaces(Map.of("syncope", new SyncopeJexlFunctions())).
+                cache(512).
+                silent(false).
+                strict(false).
+                create();
+        jexlTools = new JexlTools(jexlEngine);
+        manager = new DefaultNotificationManager(
+                derSchemaDAO,
+                notificationDAO,
+                anyObjectDAO,
+                userDAO,
+                groupDAO,
+                anySearchDAO,
+                anyMatchDAO,
+                taskDAO,
+                relationshipTypeDAO,
+                derAttrHandler,
+                userDataBinder,
+                groupDataBinder,
+                anyObjectDataBinder,
+                confParamOps,
+                entityFactory,
+                intAttrNameParser,
+                searchCondVisitor,
+                jexlTools);
+    }
+
+    @Test
+    void jxltResolvesWhoAndUserInMapContext() {
+        Map<String, Object> ctx = new HashMap<>();
+        ctx.put("who", "admin");
+        UserTO user = new UserTO();
+        user.setUsername("deleted-user");
+        ctx.put("user", user);
+        String out = jexlTools.evaluateTemplate("${who} / ${user.username}", 
new MapContext(ctx));
+        assertFalse(out.contains("${"), out);
+        assertEquals("admin / deleted-user", out);
+    }
+
+    /**
+     * After user deletion the entity is no longer loadable, but {@code 
before} still holds the
+     * {@link UserTO} captured by {@code LogicInvocationHandler}. Notification 
templates must resolve
+     * against that snapshot (SYNCOPE-1744).
+     */
+    @Test
+    void deleteSuccessUsesBeforeUserWhenEntityRemoved() {
+        UserTO beforeDelete = new UserTO();
+        beforeDelete.setKey("c3b7107b-8886-4b1d-b0e3-2d6bfa6b1f9d");
+        beforeDelete.setUsername("deleted-user");
+        beforeDelete.getPlainAttrs().add(new 
Attr.Builder("u_email").value("[email protected]").build());
+
+        
when(userDAO.findById(beforeDelete.getKey())).thenReturn(Optional.empty());
+
+        Notification notification = mock(Notification.class);
+        
doReturn(Collections.singletonList(notification)).when(notificationDAO).findAll();
+        when(notification.isActive()).thenReturn(true);
+        when(notification.getEvents()).thenReturn(List.of(DELETE_SUCCESS));
+        when(notification.getRecipientsFIQL()).thenReturn(null);
+        when(notification.getStaticRecipients()).thenReturn(null);
+        when(notification.getRecipientsProvider()).thenReturn(null);
+        when(notification.getRecipientAttrName()).thenReturn("email");
+        when(notification.getTraceLevel()).thenReturn(TraceLevel.NONE);
+        when(notification.getSender()).thenReturn("[email protected]");
+        when(notification.getSubject()).thenReturn("User deleted");
+
+        MailTemplate mailTemplate = mock(MailTemplate.class);
+        
when(mailTemplate.getTextTemplate()).thenReturn("${user.getPlainAttr(\"u_email\").get().values[0]}");
+        when(mailTemplate.getHTMLTemplate()).thenReturn(null);
+        when(notification.getTemplate()).thenReturn(mailTemplate);
+
+        when(confParamOps.list(anyString())).thenReturn(Map.of());
+
+        NotificationTask task = mock(NotificationTask.class);
+        when(entityFactory.newEntity(NotificationTask.class)).thenReturn(task);
+        when(taskDAO.save(any(NotificationTask.class))).thenAnswer(invocation 
-> invocation.getArgument(0));
+
+        try (var auth = mockStatic(AuthContextUtils.class)) {
+            
auth.when(AuthContextUtils::getDomain).thenReturn(SyncopeConstants.MASTER_DOMAIN);
+
+            manager.createTasks(
+                    "admin",
+                    OpEvent.CategoryType.LOGIC,
+                    "UserLogic",
+                    null,
+                    "delete",
+                    OpEvent.Outcome.SUCCESS,
+                    beforeDelete,
+                    null);
+        }
+
+        ArgumentCaptor<String> textBody = 
ArgumentCaptor.forClass(String.class);
+        verify(task).setTextBody(textBody.capture());
+        assertEquals("[email protected]", textBody.getValue());
+    }
+
+    /**
+     * When {@code before} is {@code null} and the entity is not found, the 
empty branch of
+     * {@code ifPresentOrElse} must not throw {@link NullPointerException} 
(SYNCOPE-1744).
+     */
+    @Test
+    void nullBeforeWithMissingEntityDoesNotThrow() {
+        Notification notification = mock(Notification.class);
+        
doReturn(Collections.singletonList(notification)).when(notificationDAO).findAll();
+        when(notification.isActive()).thenReturn(true);
+        when(notification.getEvents()).thenReturn(List.of(DELETE_SUCCESS));
+        when(notification.getRecipientsFIQL()).thenReturn(null);
+        when(notification.getStaticRecipients()).thenReturn(null);
+        when(notification.getRecipientsProvider()).thenReturn(null);
+        when(notification.getRecipientAttrName()).thenReturn("email");
+        when(notification.getTraceLevel()).thenReturn(TraceLevel.NONE);
+        when(notification.getSender()).thenReturn("[email protected]");
+        when(notification.getSubject()).thenReturn("User deleted");
+
+        MailTemplate mailTemplate = mock(MailTemplate.class);
+        when(mailTemplate.getTextTemplate()).thenReturn("${who}");
+        when(mailTemplate.getHTMLTemplate()).thenReturn(null);
+        when(notification.getTemplate()).thenReturn(mailTemplate);
+
+        when(confParamOps.list(anyString())).thenReturn(Map.of());
+
+        NotificationTask task = mock(NotificationTask.class);
+        when(entityFactory.newEntity(NotificationTask.class)).thenReturn(task);
+        when(taskDAO.save(any(NotificationTask.class))).thenAnswer(invocation 
-> invocation.getArgument(0));
+
+        try (var auth = mockStatic(AuthContextUtils.class)) {
+            
auth.when(AuthContextUtils::getDomain).thenReturn(SyncopeConstants.MASTER_DOMAIN);
+
+            manager.createTasks(
+                    "admin",
+                    OpEvent.CategoryType.LOGIC,
+                    "UserLogic",
+                    null,
+                    "delete",
+                    OpEvent.Outcome.SUCCESS,
+                    null,   // before is null
+                    null);
+        }
+
+        ArgumentCaptor<String> textBody = 
ArgumentCaptor.forClass(String.class);
+        verify(task).setTextBody(textBody.capture());
+        assertEquals("admin", textBody.getValue());
+    }
+}

Reply via email to