Files
hertzbeat/web-next/lib/api-facade.test.ts
T
Logic 2de48d2caa feat: complete HertzBeat 2.0 novice workflows
Refine entity, monitor, alert, settings, observability, and shared operator workflows; add scale guards, route compatibility, release validation, and Product Design coverage.

AI assistance: used for draft implementation and test iteration.\nHuman validation: Node 22 production build; 426 frontend files and 3,398 tests; 59-route matrix; release budget 7.84 MiB / 8.00 MiB; focused backend tests and Browser QA.\nRisk notes: local scale proof covers 2,005 entities; sustained concurrent-ingestion SLA remains deployment-specific.
2026-07-10 14:54:43 +08:00

996 lines
38 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import { api } from './api-facade';
const fetchMock = vi.fn<typeof fetch>();
function mockApiMessagePayload(data: unknown) {
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ code: 0, data }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
);
}
describe('api facade', () => {
afterEach(() => {
fetchMock.mockReset();
vi.unstubAllGlobals();
});
it('keeps monitor list reads behind the BFF message client', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ content: [] });
await expect(api.monitors.list({ pageIndex: 0, pageSize: 8, status: '2' })).resolves.toEqual({ content: [] });
expect(fetchMock).toHaveBeenCalledWith(
'/api/monitors/manage?pageIndex=0&pageSize=8&status=2',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps monitor page reads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ content: [{ id: 42 }], totalElements: 1 });
await expect(
api.monitors.page({
search: ' mysql ',
app: 'mysql',
labels: '',
status: '2',
pageIndex: '0',
pageSize: '8',
entityId: '',
entityName: '',
returnTo: ''
})
).resolves.toEqual({ content: [{ id: 42 }], totalElements: 1 });
expect(fetchMock).toHaveBeenCalledWith(
'/api/monitors?pageIndex=0&pageSize=8&search=mysql&app=mysql&status=2',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps monitor detail reads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ id: 42, app: 'website' });
await expect(api.monitors.detail(42)).resolves.toEqual({ id: 42, app: 'website' });
expect(fetchMock).toHaveBeenCalledWith(
'/api/monitor/42',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps monitor editor draft reads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ monitor: { id: 42, app: 'mysql' }, params: [], collector: null });
mockApiMessagePayload({ content: [{ collector: { name: 'edge-a' } }] });
mockApiMessagePayload([{ field: 'host', type: 'text' }]);
await expect(api.monitors.editorDetail(42)).resolves.toEqual({
monitor: { id: 42, app: 'mysql' },
params: [],
collector: null
});
await expect(api.monitors.editorCollectors()).resolves.toEqual({ content: [{ collector: { name: 'edge-a' } }] });
await expect(api.monitors.editorParamDefines('mysql')).resolves.toEqual([{ field: 'host', type: 'text' }]);
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/monitor/42',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/collector',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
3,
'/api/apps/mysql/params',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps monitor optional detail reads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload(['cpu']);
mockApiMessagePayload({ enabled: true, url: 'https://grafana.example/d/abc' });
await expect(api.monitors.favoriteMetrics(42)).resolves.toEqual(['cpu']);
await expect(api.monitors.grafanaDashboard(42)).resolves.toEqual({ enabled: true, url: 'https://grafana.example/d/abc' });
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/metrics/favorite/42',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/grafana/dashboard?monitorId=42',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps monitor detail favorite and Grafana mutations through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ ok: true });
mockApiMessagePayload({ ok: true });
mockApiMessagePayload({ ok: true });
await expect(api.monitors.addFavoriteMetric(42, 'cpu.usage')).resolves.toEqual({ ok: true });
await expect(api.monitors.removeFavoriteMetric(42, 'cpu.usage')).resolves.toEqual({ ok: true });
await expect(api.monitors.deleteGrafanaDashboard(42)).resolves.toEqual({ ok: true });
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/metrics/favorite/42/cpu.usage',
expect.objectContaining({
method: 'POST',
credentials: 'same-origin',
body: 'null'
})
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/metrics/favorite/42/cpu.usage',
expect.objectContaining({
method: 'DELETE',
credentials: 'same-origin'
})
);
expect(fetchMock).toHaveBeenNthCalledWith(
3,
'/api/grafana/dashboard?monitorId=42',
expect.objectContaining({
method: 'DELETE',
credentials: 'same-origin'
})
);
});
it('maps monitor realtime metric reads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ values: [{ field: 'usage' }] });
await expect(api.monitors.realtimeMetric(42, 'cpu')).resolves.toEqual({ values: [{ field: 'usage' }] });
expect(fetchMock).toHaveBeenCalledWith(
'/api/monitor/42/metrics/cpu',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps monitor history catalog reads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ available: true });
mockApiMessagePayload({ metrics: [{ name: 'cpu', fields: [{ type: 0, field: 'usage' }] }] });
await expect(api.monitors.warehouseStorageStatus()).resolves.toEqual({ available: true });
await expect(
api.monitors.historyMetricCatalogDefine({ id: 42, app: 'prometheus', scrape: 'prometheus', name: 'node-exporter' } as any)
).resolves.toEqual({ metrics: [{ name: 'cpu', fields: [{ type: 0, field: 'usage' }] }] });
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/warehouse/storage/status',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/apps/42/define/dynamic',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps monitor history metric reads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ values: { usage: [{ origin: '72' }] } });
await expect(
api.monitors.historyMetric(
{ id: 42, instance: '127.0.0.1:8080', app: 'prometheus', name: 'node-exporter' } as any,
{ metrics: 'cpu', metric: 'usage' },
{ history: '1h', interval: true }
)
).resolves.toEqual({ values: { usage: [{ origin: '72' }] } });
expect(fetchMock).toHaveBeenCalledWith(
'/api/monitor/127.0.0.1%3A8080/metric/_prometheus_node-exporter.cpu.usage?history=1h&interval=true',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps monitor editor mutations through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ id: 42 });
mockApiMessagePayload({ id: 42 });
mockApiMessagePayload({ available: true });
const savePayload = { monitor: { app: 'website' }, params: [], grafanaDashboard: { enabled: false } };
const detectPayload = { monitor: { app: 'website' }, params: [] };
await expect(api.monitors.create(savePayload)).resolves.toEqual({ id: 42 });
await expect(api.monitors.update(savePayload)).resolves.toEqual({ id: 42 });
await expect(api.monitors.detect(detectPayload)).resolves.toEqual({ available: true });
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/monitor',
expect.objectContaining({
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify(savePayload)
})
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/monitor',
expect.objectContaining({
method: 'PUT',
credentials: 'same-origin',
body: JSON.stringify(savePayload)
})
);
expect(fetchMock).toHaveBeenNthCalledWith(
3,
'/api/monitor/detect',
expect.objectContaining({
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify(detectPayload)
})
);
});
it('maps monitor import uploads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ imported: 1 });
const body = new FormData();
body.append('file', 'name: mysql');
await expect(api.monitors.import(body)).resolves.toEqual({ imported: 1 });
expect(fetchMock).toHaveBeenCalledWith(
'/api/monitors/import',
expect.objectContaining({
method: 'POST',
credentials: 'same-origin',
body
})
);
expect((fetchMock.mock.calls[0]?.[1]?.headers as Record<string, string> | undefined)?.['Content-Type']).toBeUndefined();
});
it('maps monitor export downloads through the domain facade while preserving response headers', async () => {
vi.stubGlobal('fetch', fetchMock);
fetchMock
.mockResolvedValueOnce(
new Response(new Blob(['{}'], { type: 'application/json' }), {
status: 200,
headers: {
'Content-Disposition': 'attachment; filename=monitors.json',
'Content-Type': 'application/octet-stream'
}
})
)
.mockResolvedValueOnce(
new Response(new Blob(['id,name'], { type: 'text/csv' }), {
status: 200,
headers: {
'Content-Disposition': 'attachment; filename=monitors-all.xlsx',
'Content-Type': 'application/octet-stream'
}
})
);
await expect(api.monitors.exportResponse([1, 2], 'JSON')).resolves.toBeInstanceOf(Response);
await expect(api.monitors.exportAllResponse('EXCEL')).resolves.toBeInstanceOf(Response);
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/monitors/export?ids=1&ids=2&type=JSON',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/monitors/export/all?type=EXCEL',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps topology graph reads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ nodes: [], edges: [], apiBacked: true });
await expect(api.topology.graph({ entityId: '501', sourceKind: 'otlp-trace-call' })).resolves.toMatchObject({
apiBacked: true
});
expect(String(fetchMock.mock.calls[0]?.[0])).toContain('/api/topology?focusEntityId=501');
expect(String(fetchMock.mock.calls[0]?.[0])).toContain('sourceKind=otlp-trace-call');
expect(String(fetchMock.mock.calls[0]?.[0])).toContain('relationType=trace-call');
});
it('maps entity detail reads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ entity: { entity: { id: 123 } } });
mockApiMessagePayload({ content: [{ id: 9, labels: { service: 'checkout' } }], totalElements: 1 });
await expect(api.entities.detail(123)).resolves.toEqual({ entity: { entity: { id: 123 } } });
await expect(api.entities.alerts(123, { pageIndex: 0, pageSize: 20, status: 'firing' })).resolves.toEqual({
content: [{ id: 9, labels: { service: 'checkout' } }],
totalElements: 1
});
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/entities/123/detail',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/entities/123/alerts?pageIndex=0&pageSize=20&status=firing',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps entity editor draft reads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ entity: { id: 123 } });
await expect(api.entities.editorEntity(123)).resolves.toEqual({ entity: { id: 123 } });
expect(fetchMock).toHaveBeenCalledWith(
'/api/entities/123',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps entity list reads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ content: [], totalElements: 0 });
await expect(api.entities.list({ search: ' checkout ', type: 'service', status: 'healthy', pageIndex: '2', pageSize: '20' })).resolves.toEqual({
content: [],
totalElements: 0
});
expect(fetchMock).toHaveBeenCalledWith(
'/api/entities?pageIndex=2&pageSize=20&sort=gmtUpdate&order=desc&search=checkout&type=service&status=healthy',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps entity editor catalog suggestions through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ owners: ['platform'] });
await expect(api.entities.catalogSuggestions()).resolves.toEqual({ owners: ['platform'] });
expect(fetchMock).toHaveBeenCalledWith(
'/api/entities/catalog-suggestions?limit=120',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps entity definition workspace reads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload('kind: Entity');
mockApiMessagePayload([{ id: 1, summary: 'updated definition' }]);
mockApiMessagePayload([{ id: 'base-template', name: 'Base template' }]);
await expect(api.entities.definition(42, 'yaml')).resolves.toBe('kind: Entity');
await expect(api.entities.definitionActivities(42)).resolves.toEqual([{ id: 1, summary: 'updated definition' }]);
await expect(api.entities.definitionTemplates()).resolves.toEqual([{ id: 'base-template', name: 'Base template' }]);
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/entities/42/definition?format=yaml',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/entities/definition-activities?entityId=42&limit=8',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
3,
'/api/entities/definition/templates?limit=8',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps entity import workspace reads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload([{ id: 'base-template', name: 'Base template' }]);
mockApiMessagePayload([{ id: 1, summary: 'imported service' }]);
await expect(api.entities.importTemplates()).resolves.toEqual([{ id: 'base-template', name: 'Base template' }]);
await expect(api.entities.importActivities()).resolves.toEqual([{ id: 1, summary: 'imported service' }]);
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/entities/definition/templates?limit=8',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/entities/definition-activities?limit=8',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps entity discovery workspace reads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload([{ id: 'preset-a', name: 'Default preset' }]);
mockApiMessagePayload([{ id: 'activity-a', summary: 'synced preset' }]);
await expect(api.entities.discoveryGovernancePresets()).resolves.toEqual([{ id: 'preset-a', name: 'Default preset' }]);
await expect(api.entities.discoveryGovernanceActivities()).resolves.toEqual([{ id: 'activity-a', summary: 'synced preset' }]);
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/entities/discovery/governance-presets?limit=8',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/entities/discovery/governance-activities?limit=8',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps overview console reads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ apps: [{ app: 'website', size: 2 }] });
mockApiMessagePayload({ content: [{ id: 1, content: 'latency spike' }] });
await expect(api.overview.summary()).resolves.toEqual({ apps: [{ app: 'website', size: 2 }] });
await expect(api.overview.alerts({ pageIndex: 0, pageSize: 6, sort: 'gmtUpdate', order: 'desc' })).resolves.toEqual({
content: [{ id: 1, content: 'latency spike' }]
});
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/summary',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/alerts?pageIndex=0&pageSize=6&sort=gmtUpdate&order=desc',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps alert center group mutations through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload(undefined);
mockApiMessagePayload(undefined);
await expect(api.alerts.groupStatus('acknowledged', [7, 8])).resolves.toBeUndefined();
await expect(api.alerts.groupClose(7)).resolves.toBeUndefined();
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/alerts/group/status/acknowledged?ids=7&ids=8',
expect.objectContaining({
method: 'PUT',
credentials: 'same-origin',
body: 'null'
})
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/alerts/group?ids=7',
expect.objectContaining({
method: 'DELETE',
credentials: 'same-origin'
})
);
});
it('maps alert center first-screen reads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ total: 4, priorityCriticalNum: 1 });
mockApiMessagePayload({ content: [{ id: 9, content: 'checkout down' }], totalElements: 1, pageIndex: 0, pageSize: 8 });
await expect(api.alerts.summary()).resolves.toEqual({ total: 4, priorityCriticalNum: 1 });
await expect(
api.alerts.groupAlerts({
search: ' checkout ',
status: ' firing ',
severity: ' critical ',
pageIndex: 0,
pageSize: 8,
entityId: '',
entityName: '',
returnTo: ''
})
).resolves.toEqual({
content: [{ id: 9, content: 'checkout down' }],
totalElements: 1,
pageIndex: 0,
pageSize: 8
});
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/alerts/summary',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/alerts/group?pageIndex=0&pageSize=8&sort=gmtUpdate&order=desc&search=checkout&status=firing&severity=critical',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps alert notice first-screen reads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ content: [{ id: 7, name: 'Ops email' }], totalElements: 1 });
mockApiMessagePayload({ content: [{ id: 8, name: 'All receiver' }], totalElements: 1 });
mockApiMessagePayload({ content: [{ id: 5, name: 'Critical rule' }], totalElements: 1 });
mockApiMessagePayload({ content: [{ id: 9, name: 'Email template', preset: false }], totalElements: 1 });
mockApiMessagePayload({ content: [{ id: 10, name: 'Default template', preset: true }], totalElements: 1 });
mockApiMessagePayload({ content: [{ id: 11, name: 'Custom template', preset: false }], totalElements: 1 });
await expect(api.alertNotice.receivers.list({ search: 'ops', pageIndex: 0, pageSize: 8 })).resolves.toEqual({
content: [{ id: 7, name: 'Ops email' }],
totalElements: 1
});
await expect(api.alertNotice.receivers.options()).resolves.toEqual({
content: [{ id: 8, name: 'All receiver' }],
totalElements: 1
});
await expect(api.alertNotice.rules.list({ search: 'critical', pageIndex: 0, pageSize: 8 })).resolves.toEqual({
content: [{ id: 5, name: 'Critical rule' }],
totalElements: 1
});
await expect(api.alertNotice.templates.list({ search: 'email', preset: false, pageIndex: 0, pageSize: 8 })).resolves.toEqual({
content: [{ id: 9, name: 'Email template', preset: false }],
totalElements: 1
});
await expect(api.alertNotice.templates.options()).resolves.toEqual({
content: [
{ id: 10, name: 'Default template', preset: true },
{ id: 11, name: 'Custom template', preset: false }
],
totalElements: 2,
pageIndex: 0,
pageSize: 2
});
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/notice/receivers?pageIndex=0&pageSize=8&name=ops',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/notice/receivers?pageIndex=0&pageSize=1000',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
3,
'/api/notice/rules?pageIndex=0&pageSize=8&name=critical',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
4,
'/api/notice/templates?pageIndex=0&pageSize=8&name=email&preset=false',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
5,
'/api/notice/templates?pageIndex=0&pageSize=1000&preset=true',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
6,
'/api/notice/templates?pageIndex=0&pageSize=1000&preset=false',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps alert notice detail and mutations through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ id: 7, name: 'Ops email' });
mockApiMessagePayload({ ok: true });
mockApiMessagePayload(undefined);
mockApiMessagePayload(undefined);
mockApiMessagePayload(undefined);
mockApiMessagePayload({ id: 5, name: 'Critical rule' });
mockApiMessagePayload({ ok: true });
mockApiMessagePayload(undefined);
mockApiMessagePayload(undefined);
mockApiMessagePayload({ id: 9, name: 'Email template' });
mockApiMessagePayload({ ok: true });
mockApiMessagePayload(undefined);
mockApiMessagePayload(undefined);
const receiverDraft = { id: 7, name: ' Ops email ', type: '1', email: ' ops@example.com ', hookAuthType: 'None' } as any;
const ruleDraft = {
id: 5,
name: ' Critical rule ',
receiverIdsText: '7',
templateId: '9',
enable: true,
filterAll: true,
labelsText: '',
daysText: '1,2,3,4,5,6,7',
periodStart: '09:00',
periodEnd: '18:00'
} as any;
const templateDraft = { id: 9, name: ' Email template ', type: '1', preset: false, content: 'body' } as any;
await expect(api.alertNotice.receivers.detail(7)).resolves.toEqual({ id: 7, name: 'Ops email' });
await expect(api.alertNotice.receivers.create(receiverDraft)).resolves.toEqual({ ok: true });
await expect(api.alertNotice.receivers.update(receiverDraft)).resolves.toBeUndefined();
await expect(api.alertNotice.receivers.sendTest(receiverDraft)).resolves.toBeUndefined();
await expect(api.alertNotice.receivers.delete(7)).resolves.toBeUndefined();
await expect(api.alertNotice.rules.detail(5)).resolves.toEqual({ id: 5, name: 'Critical rule' });
await expect(api.alertNotice.rules.create(ruleDraft, { receiverName: ['Ops email'], templateName: 'Email template' })).resolves.toEqual({ ok: true });
await expect(api.alertNotice.rules.update(ruleDraft)).resolves.toBeUndefined();
await expect(api.alertNotice.rules.delete(5)).resolves.toBeUndefined();
await expect(api.alertNotice.templates.detail(9)).resolves.toEqual({ id: 9, name: 'Email template' });
await expect(api.alertNotice.templates.create(templateDraft)).resolves.toEqual({ ok: true });
await expect(api.alertNotice.templates.update(templateDraft)).resolves.toBeUndefined();
await expect(api.alertNotice.templates.delete(9)).resolves.toBeUndefined();
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/notice/receiver/7',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/notice/receiver',
expect.objectContaining({
method: 'POST',
credentials: 'same-origin',
body: expect.stringContaining('"email":"ops@example.com"')
})
);
expect(fetchMock).toHaveBeenNthCalledWith(
3,
'/api/notice/receiver',
expect.objectContaining({ method: 'PUT', credentials: 'same-origin' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
4,
'/api/notice/receiver/send-test-msg',
expect.objectContaining({ method: 'POST', credentials: 'same-origin' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
5,
'/api/notice/receiver/7',
expect.objectContaining({ method: 'DELETE', credentials: 'same-origin' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
6,
'/api/notice/rule/5',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
7,
'/api/notice/rule',
expect.objectContaining({
method: 'POST',
credentials: 'same-origin',
body: expect.stringContaining('"receiverName":["Ops email"]')
})
);
expect(fetchMock).toHaveBeenNthCalledWith(
8,
'/api/notice/rule',
expect.objectContaining({ method: 'PUT', credentials: 'same-origin' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
9,
'/api/notice/rule/5',
expect.objectContaining({ method: 'DELETE', credentials: 'same-origin' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
10,
'/api/notice/template/9',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
11,
'/api/notice/template',
expect.objectContaining({
method: 'POST',
credentials: 'same-origin',
body: expect.stringContaining('"preset":false')
})
);
expect(fetchMock).toHaveBeenNthCalledWith(
12,
'/api/notice/template',
expect.objectContaining({ method: 'PUT', credentials: 'same-origin' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
13,
'/api/notice/template/9',
expect.objectContaining({ method: 'DELETE', credentials: 'same-origin' })
);
});
it('maps alert setting first-screen reads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
const databaseLabel = String.fromCodePoint(0x6570, 0x636e, 0x5e93);
const mysqlDatabaseLabel = `MySQL${databaseLabel}`;
mockApiMessagePayload({ mysql: mysqlDatabaseLabel });
mockApiMessagePayload({ content: [{ id: 7, name: 'cpu threshold' }], totalElements: 1 });
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ code: 0, data: { promql: true } }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
);
await expect(api.alertSettings.appDefines('zh_CN')).resolves.toEqual({ mysql: mysqlDatabaseLabel });
await expect(api.alertSettings.list(databaseLabel, 2, 15, [{ key: 'mysql', value: mysqlDatabaseLabel }])).resolves.toEqual({
content: [{ id: 7, name: 'cpu threshold' }],
totalElements: 1
});
await expect(api.alertSettings.datasourceStatus()).resolves.toEqual({ code: 0, data: { promql: true } });
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/apps/defines?lang=zh_CN',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/alert/defines?pageIndex=2&pageSize=15&sort=id&order=desc&search=%5B%22mysql%22%5D',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
3,
'/api/alert/define/datasource/status',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps alert setting detail and mutations through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ id: 7, name: 'cpu threshold' });
mockApiMessagePayload({ id: 8 });
mockApiMessagePayload(undefined);
mockApiMessagePayload(undefined);
await expect(api.alertSettings.detail(7)).resolves.toEqual({ id: 7, name: 'cpu threshold' });
await expect(api.alertSettings.create({ name: 'memory threshold' })).resolves.toEqual({ id: 8 });
await expect(api.alertSettings.update({ id: 7, enable: false })).resolves.toBeUndefined();
await expect(api.alertSettings.delete([7, 8])).resolves.toBeUndefined();
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/alert/define/7',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/alert/define',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ name: 'memory threshold' })
})
);
expect(fetchMock).toHaveBeenNthCalledWith(
3,
'/api/alert/define',
expect.objectContaining({
method: 'PUT',
body: JSON.stringify({ id: 7, enable: false })
})
);
expect(fetchMock).toHaveBeenNthCalledWith(
4,
'/api/alert/defines?ids=7&ids=8',
expect.objectContaining({ method: 'DELETE' })
);
});
it('maps alert group first-screen reads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ content: [{ id: 7, name: 'ops group' }], totalElements: 1 });
mockApiMessagePayload({ content: [{ id: 3, name: 'severity', tagValue: 'critical' }], totalElements: 1 });
await expect(api.alertGroups.list({ search: 'ops', pageIndex: 0, pageSize: 8 })).resolves.toEqual({
content: [{ id: 7, name: 'ops group' }],
totalElements: 1
});
await expect(api.alertLabels.list()).resolves.toEqual({
content: [{ id: 3, name: 'severity', tagValue: 'critical' }],
totalElements: 1
});
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/alert/groups?pageIndex=0&pageSize=8&sort=id&order=desc&search=ops',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/label?pageIndex=0&pageSize=9999',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps alert group detail and mutations through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ id: 7, name: 'ops group' });
mockApiMessagePayload({ id: 8 });
mockApiMessagePayload(undefined);
mockApiMessagePayload(undefined);
await expect(api.alertGroups.detail(7)).resolves.toEqual({ id: 7, name: 'ops group' });
await expect(api.alertGroups.create({ name: 'new group' })).resolves.toEqual({ id: 8 });
await expect(api.alertGroups.update({ id: 7, enable: false })).resolves.toBeUndefined();
await expect(api.alertGroups.delete([7, 8])).resolves.toBeUndefined();
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/alert/group/7',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/alert/group',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ name: 'new group' })
})
);
expect(fetchMock).toHaveBeenNthCalledWith(
3,
'/api/alert/group',
expect.objectContaining({
method: 'PUT',
body: JSON.stringify({ id: 7, enable: false })
})
);
expect(fetchMock).toHaveBeenNthCalledWith(
4,
'/api/alert/groups?ids=7&ids=8',
expect.objectContaining({ method: 'DELETE' })
);
});
it('maps alert inhibit first-screen and matched-detail reads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ content: [{ id: 7, name: 'db inhibit' }], totalElements: 1 });
mockApiMessagePayload({ id: 7, name: 'db inhibit' });
await expect(api.alertInhibits.list({ search: 'db', pageIndex: 0, pageSize: 8 })).resolves.toEqual({
content: [{ id: 7, name: 'db inhibit' }],
totalElements: 1
});
await expect(api.alertInhibits.detail(7)).resolves.toEqual({ id: 7, name: 'db inhibit' });
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/alert/inhibits?pageIndex=0&pageSize=8&sort=id&order=desc&search=db',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/alert/inhibit/7',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps alert inhibit mutations through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ id: 8 });
mockApiMessagePayload(undefined);
mockApiMessagePayload(undefined);
await expect(api.alertInhibits.create({ name: 'new inhibit' })).resolves.toEqual({ id: 8 });
await expect(api.alertInhibits.update({ id: 7, enable: false })).resolves.toBeUndefined();
await expect(api.alertInhibits.delete([7, 8])).resolves.toBeUndefined();
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/alert/inhibit',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ name: 'new inhibit' })
})
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/alert/inhibit',
expect.objectContaining({
method: 'PUT',
body: JSON.stringify({ id: 7, enable: false })
})
);
expect(fetchMock).toHaveBeenNthCalledWith(
3,
'/api/alert/inhibits?ids=7&ids=8',
expect.objectContaining({ method: 'DELETE' })
);
});
it('maps alert silence first-screen and matched-detail reads through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ content: [{ id: 7, name: 'weekday silence' }], totalElements: 1 });
mockApiMessagePayload({ id: 7, name: 'weekday silence' });
await expect(api.alertSilences.list({ search: 'weekday', pageIndex: 0, pageSize: 8 })).resolves.toEqual({
content: [{ id: 7, name: 'weekday silence' }],
totalElements: 1
});
await expect(api.alertSilences.detail(7)).resolves.toEqual({ id: 7, name: 'weekday silence' });
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/alert/silences?pageIndex=0&pageSize=8&sort=id&order=desc&search=weekday',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/alert/silence/7',
expect.objectContaining({ credentials: 'same-origin', cache: 'no-store' })
);
});
it('maps alert silence mutations through the domain facade', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ id: 8 });
mockApiMessagePayload(undefined);
mockApiMessagePayload(undefined);
await expect(api.alertSilences.create({ name: 'new silence' })).resolves.toEqual({ id: 8 });
await expect(api.alertSilences.update({ id: 7, enable: false })).resolves.toBeUndefined();
await expect(api.alertSilences.delete([7, 8])).resolves.toBeUndefined();
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/alert/silence',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ name: 'new silence' })
})
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/alert/silence',
expect.objectContaining({
method: 'PUT',
body: JSON.stringify({ id: 7, enable: false })
})
);
expect(fetchMock).toHaveBeenNthCalledWith(
3,
'/api/alert/silences?ids=7&ids=8',
expect.objectContaining({ method: 'DELETE' })
);
});
it('keeps legacy write semantics available through scoped operations', async () => {
vi.stubGlobal('fetch', fetchMock);
mockApiMessagePayload({ ok: true });
await expect(api.monitors.copy(42)).resolves.toEqual({ ok: true });
expect(fetchMock).toHaveBeenCalledWith(
'/api/monitor/copy/42',
expect.objectContaining({
method: 'POST',
body: 'null'
})
);
});
});