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

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


The following commit(s) were added to refs/heads/master by this push:
     new de93952d9f fix(alert): guard against null alert/dateCreated in email 
alert template (#6457)
de93952d9f is described below

commit de93952d9fc5d2105bd110cb584e622e53d2263e
Author: wy471x <[email protected]>
AuthorDate: Mon Oct 19 09:39:24 2026 +0800

    fix(alert): guard against null alert/dateCreated in email alert template 
(#6457)
    
    * fix(alert): guard against null alert/dateCreated in email alert template
    
    * fix(alert): move validation from shared AlarmContent to controller-layer 
DTO
    
    Remove @NotBlank from AlarmContent (shared DTO used by gateway AlarmSender)
    and add a separate AlertReportRequest DTO at the controller layer instead.
    This prevents gateway-generated alerts with null content (e.g. from
    IllegalArgumentException with no message) from being silently dropped by
    Spring @Valid rejection before dispatch.
    
    Co-Authored-By: Claude Opus 4.7 <[email protected]>
    
    * fix(alert): drop unreachable null-alert guard, add controller validation 
test
    
    Remove the dead alert==null check in EmailAlertNotifyStrategy since
    DispatchTask.run() already guarantees non-null alerts upstream.
    Add AlertReportControllerTest covering @NotBlank validation rejections
    and correct AlertReportRequest→AlarmContent mapping.
    
    Co-Authored-By: Claude Opus 4.7 <[email protected]>
    
    * fix(common): remove whitespace in the pom file
    
    * fix(alert): resolve checkstyle declaration order violation in 
EmailAlertNotifyStrategyTest
    
    ---------
    
    Co-authored-by: aias00 <[email protected]>
    Co-authored-by: Claude Opus 4.7 <[email protected]>
---
 .../admin/controller/AlertReportController.java    |  14 ++-
 .../shenyu/admin/model/dto/AlertReportRequest.java | 101 +++++++++++++++++
 .../controller/AlertReportControllerTest.java      | 120 +++++++++++++++++++++
 .../alert/strategy/EmailAlertNotifyStrategy.java   |   5 +-
 .../strategy/EmailAlertNotifyStrategyTest.java     |  65 ++++++++++-
 .../apache/shenyu/common/dto/AlarmContentTest.java |  49 +++++++++
 6 files changed, 346 insertions(+), 8 deletions(-)

diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/AlertReportController.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/AlertReportController.java
index 0f5a19d1d6..704fc06482 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/AlertReportController.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/AlertReportController.java
@@ -18,6 +18,7 @@
 package org.apache.shenyu.admin.controller;
 
 import org.apache.shenyu.admin.aspect.annotation.RestApi;
+import org.apache.shenyu.admin.model.dto.AlertReportRequest;
 import org.apache.shenyu.admin.model.result.ShenyuAdminResult;
 import org.apache.shenyu.admin.service.AlertDispatchService;
 import org.apache.shenyu.admin.utils.ShenyuResultMessage;
@@ -40,11 +41,20 @@ public class AlertReportController {
     /**
      * report new alert content.
      *
-     * @param alarmContent AlertContentDTO
+     * @param request alert report request
      * @return row int
      */
     @PostMapping
-    public ShenyuAdminResult reportAlert(@Valid @RequestBody final 
AlarmContent alarmContent) {
+    public ShenyuAdminResult reportAlert(@Valid @RequestBody final 
AlertReportRequest request) {
+        AlarmContent alarmContent = new AlarmContent.Builder()
+                .title(request.getTitle())
+                .content(request.getContent())
+                .level(request.getLevel())
+                .labels(request.getLabels())
+                .namespaceId(request.getNamespaceId())
+                .dateCreated(request.getDateCreated())
+                .dateUpdated(request.getDateUpdated())
+                .build();
         alertDispatchService.dispatchAlert(alarmContent);
         return ShenyuAdminResult.success(ShenyuResultMessage.CREATE_SUCCESS);
     }
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/AlertReportRequest.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/AlertReportRequest.java
new file mode 100644
index 0000000000..4b486819c6
--- /dev/null
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/dto/AlertReportRequest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.shenyu.admin.model.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * Request DTO for /alert/report endpoint.
+ * Validation constraints live here so internal callers using AlarmContent 
directly are unaffected.
+ */
+public class AlertReportRequest {
+
+    @NotBlank
+    private String title;
+
+    private byte level;
+
+    private Map<String, String> labels;
+
+    @NotBlank
+    private String content;
+
+    private String namespaceId;
+
+    private Date dateCreated;
+
+    private Date dateUpdated;
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(final String title) {
+        this.title = title;
+    }
+
+    public byte getLevel() {
+        return level;
+    }
+
+    public void setLevel(final byte level) {
+        this.level = level;
+    }
+
+    public Map<String, String> getLabels() {
+        return labels;
+    }
+
+    public void setLabels(final Map<String, String> labels) {
+        this.labels = labels;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(final String content) {
+        this.content = content;
+    }
+
+    public String getNamespaceId() {
+        return namespaceId;
+    }
+
+    public void setNamespaceId(final String namespaceId) {
+        this.namespaceId = namespaceId;
+    }
+
+    public Date getDateCreated() {
+        return dateCreated;
+    }
+
+    public void setDateCreated(final Date dateCreated) {
+        this.dateCreated = dateCreated;
+    }
+
+    public Date getDateUpdated() {
+        return dateUpdated;
+    }
+
+    public void setDateUpdated(final Date dateUpdated) {
+        this.dateUpdated = dateUpdated;
+    }
+}
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/AlertReportControllerTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/AlertReportControllerTest.java
new file mode 100644
index 0000000000..ac2803a911
--- /dev/null
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/AlertReportControllerTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.shenyu.admin.controller;
+
+import org.apache.shenyu.admin.service.AlertDispatchService;
+import org.apache.shenyu.common.dto.AlarmContent;
+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.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.verify;
+import static 
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Test case for AlertReportController.
+ */
+@ExtendWith(MockitoExtension.class)
+public class AlertReportControllerTest {
+
+    private MockMvc mockMvc;
+
+    @InjectMocks
+    private AlertReportController alertReportController;
+
+    @Mock
+    private AlertDispatchService alertDispatchService;
+
+    @BeforeEach
+    public void setUp() {
+        LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
+        validator.afterPropertiesSet();
+        this.mockMvc = MockMvcBuilders.standaloneSetup(alertReportController)
+                .setValidator(validator)
+                .build();
+    }
+
+    @Test
+    public void testBlankTitleReturns400() throws Exception {
+        String body = "{\"title\":\"\",\"content\":\"test 
content\",\"level\":1}";
+
+        this.mockMvc.perform(post("/alert/report")
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(body))
+                .andExpect(status().isBadRequest());
+    }
+
+    @Test
+    public void testNullTitleReturns400() throws Exception {
+        String body = "{\"content\":\"test content\",\"level\":1}";
+
+        this.mockMvc.perform(post("/alert/report")
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(body))
+                .andExpect(status().isBadRequest());
+    }
+
+    @Test
+    public void testBlankContentReturns400() throws Exception {
+        String body = "{\"title\":\"test title\",\"content\":\"   
\",\"level\":1}";
+
+        this.mockMvc.perform(post("/alert/report")
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(body))
+                .andExpect(status().isBadRequest());
+    }
+
+    @Test
+    public void testNullContentReturns400() throws Exception {
+        String body = "{\"title\":\"test title\",\"level\":1}";
+
+        this.mockMvc.perform(post("/alert/report")
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(body))
+                .andExpect(status().isBadRequest());
+    }
+
+    @Test
+    public void testValidRequestDispatchesCorrectly() throws Exception {
+        String body = "{\"title\":\"test title\",\"content\":\"test 
content\",\"level\":1,\"namespaceId\":\"ns-1\",\"labels\":{\"key\":\"value\"}}";
+
+        this.mockMvc.perform(post("/alert/report")
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(body))
+                .andExpect(status().isOk());
+
+        ArgumentCaptor<AlarmContent> captor = 
ArgumentCaptor.forClass(AlarmContent.class);
+        verify(alertDispatchService).dispatchAlert(captor.capture());
+        AlarmContent dispatched = captor.getValue();
+        assertEquals("test title", dispatched.getTitle());
+        assertEquals("test content", dispatched.getContent());
+        assertEquals((byte) 1, dispatched.getLevel());
+        assertEquals("ns-1", dispatched.getNamespaceId());
+        assertEquals("value", dispatched.getLabels().get("key"));
+    }
+}
diff --git 
a/shenyu-alert/src/main/java/org/apache/shenyu/alert/strategy/EmailAlertNotifyStrategy.java
 
b/shenyu-alert/src/main/java/org/apache/shenyu/alert/strategy/EmailAlertNotifyStrategy.java
index 55fc1cbee4..99209707f8 100644
--- 
a/shenyu-alert/src/main/java/org/apache/shenyu/alert/strategy/EmailAlertNotifyStrategy.java
+++ 
b/shenyu-alert/src/main/java/org/apache/shenyu/alert/strategy/EmailAlertNotifyStrategy.java
@@ -80,10 +80,7 @@ final class EmailAlertNotifyStrategy implements 
AlertNotifyHandler {
         context.setVariable("nameContent", "Alarm Content");
         context.setVariable("content", alert.getContent());
         SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd 
HH:mm:ss");
-        Date alertTime = alert.getDateCreated();
-        if (Objects.isNull(alertTime)) {
-            alertTime = new Date();
-        }
+        Date alertTime = Objects.isNull(alert.getDateCreated()) ? new Date() : 
alert.getDateCreated();
         String alarmTime = simpleDateFormat.format(alertTime);
         context.setVariable("lastTriggerTime", alarmTime);
         return templateEngine.process("mailAlarm", context);
diff --git 
a/shenyu-alert/src/test/java/org/apache/shenyu/alert/strategy/EmailAlertNotifyStrategyTest.java
 
b/shenyu-alert/src/test/java/org/apache/shenyu/alert/strategy/EmailAlertNotifyStrategyTest.java
index 091dd38ad7..1d808baa21 100644
--- 
a/shenyu-alert/src/test/java/org/apache/shenyu/alert/strategy/EmailAlertNotifyStrategyTest.java
+++ 
b/shenyu-alert/src/test/java/org/apache/shenyu/alert/strategy/EmailAlertNotifyStrategyTest.java
@@ -20,6 +20,7 @@ package org.apache.shenyu.alert.strategy;
 import org.apache.shenyu.alert.model.AlertReceiverDTO;
 import org.apache.shenyu.common.dto.AlarmContent;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mock;
@@ -27,6 +28,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
 import org.mockito.junit.jupiter.MockitoSettings;
 import org.mockito.quality.Strictness;
 import org.springframework.mail.javamail.JavaMailSender;
+import org.mockito.Mockito;
 import org.thymeleaf.TemplateEngine;
 import org.thymeleaf.context.Context;
 
@@ -34,15 +36,19 @@ import jakarta.mail.Session;
 import jakarta.mail.internet.MimeMessage;
 import java.lang.reflect.Field;
 import java.text.SimpleDateFormat;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.util.Date;
 import java.util.concurrent.atomic.AtomicReference;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.verify;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.when;
 
 /**
@@ -60,6 +66,10 @@ public class EmailAlertNotifyStrategyTest {
 
     private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
 
+    private static Method buildAlertHtmlTemplateMethod;
+
+    private static EmailAlertNotifyStrategy strategy;
+
     @Mock
     private JavaMailSender javaMailSender;
 
@@ -69,8 +79,6 @@ public class EmailAlertNotifyStrategyTest {
     @Mock
     private AlertReceiverDTO receiver;
 
-    private EmailAlertNotifyStrategy strategy;
-
     private final AtomicReference<Context> capturedContext = new 
AtomicReference<>();
 
     @BeforeEach
@@ -112,4 +120,57 @@ public class EmailAlertNotifyStrategyTest {
         assertNotNull(context);
         assertEquals("2026-08-21 10:00:00", 
context.getVariable("lastTriggerTime"));
     }
+
+    @BeforeAll
+    public static void setUpAll() throws Exception {
+        TemplateEngine mockEngine = Mockito.mock(TemplateEngine.class);
+        when(mockEngine.process(eq("mailAlarm"), 
any(org.thymeleaf.context.IContext.class)))
+                .thenReturn("<html>Rendered mailAlarm template</html>");
+
+        strategy = new EmailAlertNotifyStrategy(mockEngine, null);
+        buildAlertHtmlTemplateMethod = EmailAlertNotifyStrategy.class
+                .getDeclaredMethod("buildAlertHtmlTemplate", 
AlarmContent.class);
+        buildAlertHtmlTemplateMethod.setAccessible(true);
+    }
+
+    private String invokeBuildAlertHtmlTemplate(final AlarmContent alert) {
+        try {
+            return (String) buildAlertHtmlTemplateMethod.invoke(strategy, 
alert);
+        } catch (IllegalAccessException | InvocationTargetException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Test
+    public void testNullDateCreatedShouldNotThrowNpe() {
+        AlarmContent alert = new AlarmContent.Builder()
+                .title("test title")
+                .content("test content")
+                .dateCreated(null)
+                .build();
+
+        assertDoesNotThrow(() -> invokeBuildAlertHtmlTemplate(alert));
+    }
+
+    @Test
+    public void testValidAlertShouldNotThrow() {
+        AlarmContent alert = new AlarmContent.Builder()
+                .title("test title")
+                .content("test content")
+                .dateCreated(new Date())
+                .build();
+
+        assertDoesNotThrow(() -> invokeBuildAlertHtmlTemplate(alert));
+    }
+
+    @Test
+    public void testNullContentShouldNotThrow() {
+        AlarmContent alert = new AlarmContent.Builder()
+                .title("test title")
+                .content(null)
+                .dateCreated(new Date())
+                .build();
+
+        assertDoesNotThrow(() -> invokeBuildAlertHtmlTemplate(alert));
+    }
 }
diff --git 
a/shenyu-common/src/test/java/org/apache/shenyu/common/dto/AlarmContentTest.java
 
b/shenyu-common/src/test/java/org/apache/shenyu/common/dto/AlarmContentTest.java
new file mode 100644
index 0000000000..8be103714b
--- /dev/null
+++ 
b/shenyu-common/src/test/java/org/apache/shenyu/common/dto/AlarmContentTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.shenyu.common.dto;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Test case for AlarmContent.
+ */
+public class AlarmContentTest {
+
+    @Test
+    public void testBuilderAndGetterSetter() {
+        AlarmContent alarmContent = new AlarmContent.Builder()
+                .title("test title")
+                .content("test content")
+                .level((byte) 1)
+                .build();
+
+        assertTrue("test title".equals(alarmContent.getTitle()));
+        assertTrue("test content".equals(alarmContent.getContent()));
+        assertTrue(alarmContent.getLevel() == 1);
+
+        alarmContent.setTitle("new title");
+        alarmContent.setContent("new content");
+        alarmContent.setLevel((byte) 2);
+
+        assertTrue("new title".equals(alarmContent.getTitle()));
+        assertTrue("new content".equals(alarmContent.getContent()));
+        assertTrue(alarmContent.getLevel() == 2);
+    }
+}

Reply via email to