Files
multi-simadmin/apps/web/src/instances/notifications-module.test.tsx
T

240 lines
8.6 KiB
TypeScript

// @vitest-environment jsdom
import { cleanup, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { InstanceContext } from '../app-shell.js';
import {
NotificationsModule,
type NotificationsDataSource,
type NotificationsSnapshot,
} from './notifications-module.js';
afterEach(cleanup);
const owner: InstanceContext = {
id: 'alpha',
name: 'Alpha',
origin: 'https://alpha.example',
status: 'online',
authentication: 'authenticated',
freshness: 'fresh',
};
const snapshot: NotificationsSnapshot = {
observedAt: '2026-07-17T12:00:00Z',
channels: {
status: 'healthy',
total: 5,
enabled: 4,
disabled: 1,
healthy: 3,
degraded: 1,
failed: 0,
endpoint: 'https://private.example/hook',
destination: '+15550000123',
token: 'channel-secret',
config: { provider: 'private-provider' },
},
queue: {
status: 'processing',
total: 18,
pending: 4,
processing: 2,
delivered: 11,
failed: 1,
payload: 'private queue payload',
destination: 'private@example.test',
},
logs: {
status: 'available',
total: 30,
info: 20,
warning: 7,
error: 3,
rawLogs: ['private raw log line'],
phoneNumber: '+15550000456',
},
};
function deferredSource() {
let resolve!: (value: unknown) => void;
let reject!: (reason: unknown) => void;
const load = vi.fn<NotificationsDataSource['load']>(
(_instanceId, _signal) =>
new Promise((done, fail) => {
void _instanceId;
void _signal;
resolve = done;
reject = fail;
}),
);
return {
source: { load },
load,
resolve: (value: unknown) => resolve(value),
reject: (reason: unknown) => reject(reason),
};
}
describe('Notifications isolated safe read module', () => {
it('reads only through the injected exact-owner source and renders allowlisted aggregates', async () => {
const pending = deferredSource();
render(<NotificationsModule instance={owner} dataSource={pending.source} />);
expect(screen.getByRole('status', { name: '通知加载状态' })).toBeTruthy();
expect(pending.load).toHaveBeenCalledWith('alpha', expect.any(AbortSignal));
pending.resolve(snapshot);
const channels = await screen.findByRole('region', { name: '渠道汇总' });
expect(within(channels).getByText('正常', { selector: 'dd' })).toBeTruthy();
expect(within(channels).getByText('5')).toBeTruthy();
expect(within(screen.getByRole('region', { name: '队列汇总' })).getByText('18')).toBeTruthy();
expect(within(screen.getByRole('region', { name: '日志汇总' })).getByText('30')).toBeTruthy();
expect(screen.getByText(/观测时间:2026-07-17T12:00:00Z/)).toBeTruthy();
});
it('never exposes polymorphic config, endpoints, destinations, tokens, phone numbers, payloads, raw logs, or actions', async () => {
render(<NotificationsModule instance={owner} dataSource={{ load: async () => snapshot }} />);
expect(await screen.findByText(/仅显示汇总计数和状态/i)).toBeTruthy();
const text = document.body.textContent ?? '';
for (const secret of [
'https://private.example/hook',
'+15550000123',
'channel-secret',
'private-provider',
'private queue payload',
'private@example.test',
'private raw log line',
'+15550000456',
])
expect(text).not.toContain(secret);
expect(
screen.queryByText(/endpoint|destination|token|phone number|payload|raw log/i),
).toBeNull();
expect(screen.queryByRole('button')).toBeNull();
expect(screen.queryByRole('link')).toBeNull();
});
it('fails closed without an injected source or authenticated owner', async () => {
const load = vi.fn<NotificationsDataSource['load']>().mockResolvedValue(snapshot);
const { rerender } = render(<NotificationsModule instance={owner} />);
expect(screen.getByRole('status', { name: '通知不可用' }).textContent).toMatch(
/没有可用的安全通知只读数据源.*未签订契约的生产端点/i,
);
rerender(
<NotificationsModule
instance={{ ...owner, authentication: 'auth-required' }}
dataSource={{ load }}
/>,
);
expect(screen.getByRole('alert').textContent).toMatch(/需要先完成认证/i);
expect(load).not.toHaveBeenCalled();
});
it('clears prior-owner data, aborts replaced reads, and fences late responses', async () => {
const alpha = deferredSource();
const bravo = deferredSource();
const load = vi.fn<NotificationsDataSource['load']>((id, signal) =>
id === 'alpha' ? alpha.source.load(id, signal) : bravo.source.load(id, signal),
);
const source = { load };
const { rerender } = render(<NotificationsModule instance={owner} dataSource={source} />);
const alphaSignal = load.mock.calls[0]?.[1];
rerender(
<NotificationsModule
instance={{ ...owner, id: 'bravo', name: 'Bravo' }}
dataSource={source}
/>,
);
expect(alphaSignal?.aborted).toBe(true);
alpha.resolve(snapshot);
bravo.resolve({
...snapshot,
channels: { ...snapshot.channels, status: 'degraded', total: 9 },
});
expect(await screen.findByText('功能受限', { selector: 'dd' })).toBeTruthy();
expect(screen.queryByText('正常', { selector: 'dd' })).toBeNull();
});
it('uses fixed errors and retains only same-owner last-good data across refresh failure and retry', async () => {
const user = userEvent.setup();
const load = vi
.fn<NotificationsDataSource['load']>()
.mockResolvedValueOnce(snapshot)
.mockRejectedValueOnce(new Error('secret endpoint and token'))
.mockResolvedValueOnce({ ...snapshot, channels: { status: 'available', total: 6 } });
const source = { load };
const { rerender } = render(
<NotificationsModule instance={owner} dataSource={source} refreshSignal={0} />,
);
expect(await screen.findByText('正常', { selector: 'dd' })).toBeTruthy();
rerender(<NotificationsModule instance={owner} dataSource={source} refreshSignal={1} />);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toMatch(/刷新失败,正在显示上次已知的通知数据/i);
expect(alert.textContent).not.toContain('secret endpoint');
expect(screen.getByText('正常', { selector: 'dd' })).toBeTruthy();
await user.click(screen.getByRole('button', { name: '重试加载通知数据' }));
expect(
await within(screen.getByRole('region', { name: '渠道汇总' })).findByText('可用'),
).toBeTruthy();
});
it('copies a bounded safe snapshot and drops malformed nested data and nominal-field secrets', async () => {
const unsafe = {
observedAt: 'api-token@example.test',
channels: {
status: 'channel-secret',
total: -1,
enabled: 1.5,
disabled: 2,
healthy: Number.POSITIVE_INFINITY,
},
queue: ['private payload'],
logs: { status: { token: 'nested-secret' }, total: Number.MAX_SAFE_INTEGER + 1, error: 3 },
};
render(<NotificationsModule instance={owner} dataSource={{ load: async () => unsafe }} />);
const logs = await screen.findByRole('region', { name: '日志汇总' });
expect(within(logs).getByText('3')).toBeTruthy();
expect(within(screen.getByRole('region', { name: '渠道汇总' })).getByText('2')).toBeTruthy();
expect(document.body.textContent).not.toMatch(
/api-token|channel-secret|private payload|nested-secret|Infinity|1\.5|-1/,
);
expect(screen.queryByText(/^观测时间:/)).toBeNull();
unsafe.channels.disabled = 99;
expect(screen.queryByText('99')).toBeNull();
});
it('uses a fixed initial-load error without rejection details', async () => {
render(
<NotificationsModule
instance={owner}
dataSource={{ load: async () => Promise.reject(new Error('raw log and private token')) }}
/>,
);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toContain('无法加载通知数据。');
expect(alert.textContent).not.toContain('private token');
});
it('turns a synchronous source throw into the fixed initial-load error', async () => {
render(
<NotificationsModule
instance={owner}
dataSource={{
load: () => {
throw new Error('private notification token');
},
}}
/>,
);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toContain('无法加载通知数据。');
expect(alert.textContent).not.toContain('private notification token');
});
});