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