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;