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 6d78d8964 fix(ops): reject mock alert silences with an invalid time 
range (#7117)
6d78d8964 is described below

commit 6d78d89644564978b193ea5b13a669e486f72ab1
Author: ASUKA <[email protected]>
AuthorDate: Mon Oct 26 16:55:41 2026 +0800

    fix(ops): reject mock alert silences with an invalid time range (#7117)
    
    * fix(ops): reject mock alert silences with an invalid time range
    The Mock branch of createAlertSilence stored any window it was given, while
    the REST path rejects a silence whose end is not after its start with
    "Silence end time must be after start time". Demo mode could therefore 
report
    a maintenance window the deployed service would never create.
    Enforce the same strictly-after ordering before the Mock inventory is 
mutated,
    so a rejected request leaves it unchanged. Valid windows still resolve and
    store exactly as before.
    * fix(ops): mirror the recurrence-end rule in the mock silence create
    The mock only checked that the window ends after it starts, so a recurring 
silence
    with a missing or too-early recurrenceUntil was accepted locally and then 
rejected
    by AlertSilenceService.validateRecurrence the moment it met a real backend. 
Mirror
    both branches and cover them with one case.
---
 web/src/services/opsService.test.ts | 61 +++++++++++++++++++++++++++++++++++++
 web/src/services/opsService.ts      | 15 +++++++++
 2 files changed, 76 insertions(+)

diff --git a/web/src/services/opsService.test.ts 
b/web/src/services/opsService.test.ts
index 157c4ae9c..9ad25e365 100644
--- a/web/src/services/opsService.test.ts
+++ b/web/src/services/opsService.test.ts
@@ -21,13 +21,16 @@ import { mockAuditRecords } from '../mock/audit';
 import {
   acknowledgeAlert,
   createAlertRule,
+  createAlertSilence,
   deleteAlertRule,
+  deleteAlertSilence,
   exportAlertRulesTransfer,
   exportAuditLogs,
   getAuditFilterOptions,
   importAlertRulesTransfer,
   listAlertRules,
   listAlertRulesPage,
+  listAlertSilences,
   listAuditRecords,
   listSystemAlerts,
   listSystemAlertsPage,
@@ -363,4 +366,62 @@ describe('ops service mock data', () => {
         '"prod-cn","removed ""topic"", safely","SUCCESS","\'=denied"',
     );
   });
+
+  it('rejects a mock alert silence whose end is not after its start', async () 
=> {
+    const before = await listAlertSilences();
+
+    for (const endsAt of ['2026-10-20T11:00:00Z', '2026-10-20T12:00:00Z']) {
+      await expect(
+        createAlertSilence({
+          domain: 'BUSINESS',
+          startsAt: '2026-10-20T12:00:00Z',
+          endsAt,
+          recurrence: 'ONCE',
+          reason: 'invalid-order-probe',
+        }),
+      ).rejects.toThrow('Silence end time must be after start time');
+    }
+
+    expect(await listAlertSilences()).toHaveLength(before.length);
+  });
+
+  it('rejects a mock recurring alert silence whose recurrence end is missing 
or too early', async () => {
+    const before = await listAlertSilences();
+
+    // AlertSilenceService.validateRecurrence requires recurrenceUntil for a 
recurring silence and
+    // rejects one that ends before the first window does; the mock must not 
accept what the server
+    // would refuse, or a form built against the mock breaks the moment it 
meets a real backend.
+    for (const recurrenceUntil of [undefined, '2026-10-20T11:30:00Z']) {
+      await expect(
+        createAlertSilence({
+          domain: 'BUSINESS',
+          startsAt: '2026-10-20T11:00:00Z',
+          endsAt: '2026-10-20T12:00:00Z',
+          recurrence: 'DAILY',
+          timeZone: 'UTC',
+          recurrenceUntil,
+          reason: 'recurrence-until-probe',
+        }),
+      ).rejects.toThrow(/Recurrence end time/);
+    }
+
+    expect(await listAlertSilences()).toHaveLength(before.length);
+  });
+
+  it('keeps accepting a mock alert silence whose end is after its start', 
async () => {
+    const created = await createAlertSilence({
+      domain: 'BUSINESS',
+      startsAt: '2026-10-20T11:00:00Z',
+      endsAt: '2026-10-20T12:00:00Z',
+      recurrence: 'ONCE',
+      reason: 'valid-order-probe',
+    });
+
+    try {
+      expect(created.startsAt).toBe('2026-10-20T11:00:00Z');
+      expect(await listAlertSilences()).toContainEqual(created);
+    } finally {
+      await deleteAlertSilence(created.id);
+    }
+  });
 });
diff --git a/web/src/services/opsService.ts b/web/src/services/opsService.ts
index 69da52f64..390742a71 100644
--- a/web/src/services/opsService.ts
+++ b/web/src/services/opsService.ts
@@ -476,6 +476,21 @@ export async function listAlertSilencesPage(
 
 export async function createAlertSilence(data: CreateAlertSilence): 
Promise<AlertSilence> {
   if (!isMockMode()) return opsApi.createAlertSilence(data);
+  // Match AlertSilenceService.create: the end must be strictly after the 
start, and a
+  // rejected request must not mutate the Mock inventory.
+  if (!(new Date(data.endsAt).getTime() > new Date(data.startsAt).getTime())) {
+    throw new Error('Silence end time must be after start time');
+  }
+  // Same service, recurring branch: a recurring silence carries the end of 
its recurrence, and
+  // that end must not precede the first window's end.
+  if (data.recurrence && data.recurrence !== 'ONCE') {
+    if (!data.recurrenceUntil) {
+      throw new Error('Recurrence end time is required for recurring 
silences');
+    }
+    if (new Date(data.recurrenceUntil).getTime() < new 
Date(data.endsAt).getTime()) {
+      throw new Error('Recurrence end time must not be before the first window 
ends');
+    }
+  }
   const silence = { ...data, id: Date.now(), createdBy: 'admin' } as 
AlertSilence;
   alertSilencesState = [silence, ...alertSilencesState];
   return silence;

Reply via email to