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

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 0d83a567d fix(alert): preserve silence label conditions during 
normalization (#6430)
0d83a567d is described below

commit 0d83a567d5f8299be3a6471929b23079f4757217
Author: Loyal-Young <[email protected]>
AuthorDate: Thu Oct 15 18:12:32 2026 +0800

    fix(alert): preserve silence label conditions during normalization (#6430)
---
 .../studio/ops/alert/AlertSilenceService.java      |  3 ++
 .../studio/ops/alert/AlertSilenceServiceTest.java  | 26 ++++++++++++++
 .../pages/ops/__tests__/SystemAlertsPage.test.tsx  | 42 ++++++++++++++++++++++
 web/src/pages/ops/systemAlerts.tsx                 | 10 +++---
 4 files changed, 77 insertions(+), 4 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceService.java
index 726eccb9e..c09036ccf 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceService.java
@@ -198,6 +198,9 @@ public class AlertSilenceService {
                     || normalizedValue.length() > 512) {
                 throw new BusinessException(400, "Silence labels must have 
non-empty bounded keys and values");
             }
+            if (normalized.containsKey(normalizedKey)) {
+                throw new BusinessException(400, "Silence label keys must be 
unique after trimming");
+            }
             normalized.put(normalizedKey, normalizedValue);
         });
         return Map.copyOf(normalized);
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceServiceTest.java
index 05c2676fb..c0b35af86 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceServiceTest.java
@@ -35,6 +35,8 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.verifyNoInteractions;
 
 @ExtendWith(MockitoExtension.class)
 class AlertSilenceServiceTest {
@@ -262,6 +264,30 @@ class AlertSilenceServiceTest {
                 .hasMessage("Weekly silence windows must not exceed 7 days");
     }
 
+    @Test
+    void rejectsLabelKeysThatCollideAfterTrimmingBeforePersistenceTest() {
+        AlertSilenceService service = new AlertSilenceService(repository, 
operationAuditService);
+        CreateAlertSilenceDTO request = 
recurringRequest(AlertSilenceRecurrence.DAILY);
+        request.setLabels(Map.of("brokerName", "broker-a", " brokerName ", 
"broker-b"));
+        lenient().when(repository.save(any())).thenAnswer(invocation -> 
invocation.getArgument(0));
+
+        assertThatThrownBy(() -> service.create(request))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("Silence label keys must be unique after 
trimming");
+        verifyNoInteractions(repository, operationAuditService);
+    }
+
+    @Test
+    void preservesDistinctLabelsAndNormalizesTheirWhitespaceTest() {
+        AlertSilenceService service = new AlertSilenceService(repository, 
operationAuditService);
+        CreateAlertSilenceDTO request = 
recurringRequest(AlertSilenceRecurrence.DAILY);
+        request.setLabels(Map.of(" brokerName ", " broker-a ", "__proto__", 
"rack=one"));
+        when(repository.save(any())).thenAnswer(invocation -> 
invocation.getArgument(0));
+
+        assertThat(service.create(request).getLabels())
+                .containsExactlyInAnyOrderEntriesOf(Map.of("brokerName", 
"broker-a", "__proto__", "rack=one"));
+    }
+
     private static CreateAlertSilenceDTO 
recurringRequest(AlertSilenceRecurrence recurrence) {
         CreateAlertSilenceDTO request = new CreateAlertSilenceDTO();
         
request.setStartsAt(java.time.OffsetDateTime.parse("2026-09-01T10:00:00Z"));
diff --git a/web/src/pages/ops/__tests__/SystemAlertsPage.test.tsx 
b/web/src/pages/ops/__tests__/SystemAlertsPage.test.tsx
index 3d5397d00..a75028db2 100644
--- a/web/src/pages/ops/__tests__/SystemAlertsPage.test.tsx
+++ b/web/src/pages/ops/__tests__/SystemAlertsPage.test.tsx
@@ -744,6 +744,48 @@ describe('SystemAlertsPage', () => {
     expect(payload.labels).toBeUndefined();
   });
 
+  it.each(['brokerName=a, brokerName=b', ' =broker-a'])(
+    'rejectsSilenceLabelsWithoutDroppingConditionsTest: %s',
+    async (labels) => {
+      const user = userEvent.setup();
+      renderPage();
+      await user.click(await screen.findByRole('button', { name: '维护窗口' }));
+      fireEvent.change(screen.getByLabelText('标签范围'), { target: { value: 
labels } });
+      fireEvent.change(screen.getByLabelText('开始时间'), {
+        target: { value: '2026-08-11T01:00' },
+      });
+      fireEvent.change(screen.getByLabelText('结束时间'), {
+        target: { value: '2026-08-11T02:00' },
+      });
+      await user.click(screen.getByRole('button', { name: /创\s*建/ }));
+      expect(await screen.findByText('标签格式应为 
key=value,并以逗号分隔')).toBeInTheDocument();
+      expect(createAlertSilence).not.toHaveBeenCalled();
+    },
+  );
+
+  it('preservesPrototypeNamedSilenceLabelsInTheRequestTest', async () => {
+    vi.mocked(createAlertSilence).mockResolvedValue({
+      id: 10,
+      startsAt: '2026-08-11T01:00',
+      endsAt: '2026-08-11T02:00',
+      createdBy: 'admin',
+    });
+    const user = userEvent.setup();
+    renderPage();
+    await user.click(await screen.findByRole('button', { name: '维护窗口' }));
+    fireEvent.change(screen.getByLabelText('标签范围'), {
+      target: { value: ' __proto__ 
=broker-a,constructor=rack=one,toString=orders' },
+    });
+    fireEvent.change(screen.getByLabelText('开始时间'), { target: { value: 
'2026-08-11T01:00' } });
+    fireEvent.change(screen.getByLabelText('结束时间'), { target: { value: 
'2026-08-11T02:00' } });
+    await user.click(screen.getByRole('button', { name: /创\s*建/ }));
+    await waitFor(() => expect(createAlertSilence).toHaveBeenCalledTimes(1));
+    const labels = vi.mocked(createAlertSilence).mock.calls[0][0].labels;
+    expect(JSON.parse(JSON.stringify(labels))).toEqual(
+      
JSON.parse('{"__proto__":"broker-a","constructor":"rack=one","toString":"orders"}'),
+    );
+  });
+
   it('surfaces the label validation message when the silence labels are 
invalid', async () => {
     vi.mocked(listAlertSilencesPage).mockResolvedValue({
       items: [],
diff --git a/web/src/pages/ops/systemAlerts.tsx 
b/web/src/pages/ops/systemAlerts.tsx
index 5d826741c..db4480a20 100644
--- a/web/src/pages/ops/systemAlerts.tsx
+++ b/web/src/pages/ops/systemAlerts.tsx
@@ -96,15 +96,17 @@ const parseSilenceLabels = (
   invalidMessage: string,
 ): Record<string, string> | undefined => {
   if (!value?.trim()) return undefined;
-  const labels: Record<string, string> = {};
+  const labels = new Map<string, string>();
   for (const pair of value.split(',')) {
     const separator = pair.indexOf('=');
-    if (separator <= 0 || !pair.slice(separator + 1).trim()) {
+    const key = pair.slice(0, separator).trim();
+    const labelValue = pair.slice(separator + 1).trim();
+    if (separator <= 0 || !key || !labelValue || labels.has(key)) {
       throw new Error(invalidMessage);
     }
-    labels[pair.slice(0, separator).trim()] = pair.slice(separator + 1).trim();
+    labels.set(key, labelValue);
   }
-  return labels;
+  return Object.fromEntries(labels);
 };
 
 const localDateTimeToUtc = (value: string) => new 
Date(`${value}:00`).toISOString();

Reply via email to