Files
multi-simadmin/packages/contracts/src/notifications.test.ts
T
chick f11877f13e feat(contracts): share notification, organization and device capability models
Move the Hub channel table, organization tags and device capability definitions
into the workspace packages so the control plane and the console validate the
same contract, and refresh the frozen upstream evidence for the new paths.
2026-09-05 18:52:54 +08:00

164 lines
5.1 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
NOTIFICATION_CHANNEL_SPECS,
NotificationChannelConfigError,
isNotificationChannelType,
notificationChannelDefaults,
notificationChannelFields,
notificationChannelLabel,
notificationChannelSecretKeys,
notificationChannelSpec,
parseKeyValueField,
splitNotificationChannelConfig,
} from './notifications.js';
describe('notification channel specs', () => {
it('describes every Hub channel type exactly once', () => {
const types = NOTIFICATION_CHANNEL_SPECS.map((spec) => spec.type);
expect(new Set(types).size).toBe(types.length);
expect(types).toEqual([
'webhook',
'bark',
'pushplus',
'wecom_app',
'wecom_robot',
'dingtalk_robot',
'dingtalk_app',
'feishu_robot',
'telegram',
'email',
'serverchan',
]);
expect(NOTIFICATION_CHANNEL_SPECS.every((spec) => spec.summary.length > 0)).toBe(true);
});
it('keeps field keys unique and every field kind renderable', () => {
for (const spec of NOTIFICATION_CHANNEL_SPECS) {
const keys = spec.fields.map((field) => field.key);
expect(new Set(keys).size).toBe(keys.length);
for (const field of spec.fields) {
expect([
'text',
'url',
'secret',
'number',
'select',
'toggle',
'key-values',
] as const).toContain(field.kind);
expect(field.label.length).toBeGreaterThan(0);
if (field.kind === 'select') expect((field.options ?? []).length).toBeGreaterThan(0);
}
}
});
it('looks specs up by type and tolerates unknown input', () => {
expect(notificationChannelSpec('telegram')?.label).toBe('Telegram');
expect(notificationChannelSpec('nope')).toBeUndefined();
expect(isNotificationChannelType('bark')).toBe(true);
expect(isNotificationChannelType('Bark')).toBe(false);
expect(notificationChannelLabel('nope')).toBe('nope');
expect(notificationChannelFields('nope')).toEqual([]);
});
});
describe('splitNotificationChannelConfig', () => {
it('fills defaults and separates secrets from plain config', () => {
const { config, secrets } = splitNotificationChannelConfig('bark', {
server_url: 'https://bark.example.test',
device_key: ' key-1 ',
group: 'Island',
});
expect(config).toEqual({
server_url: 'https://bark.example.test',
group: 'Island',
sound: '',
level: '',
icon: '',
auto_copy: true,
save_history: true,
});
expect(secrets).toEqual({ device_key: 'key-1' });
expect(notificationChannelSecretKeys('bark')).toEqual(['device_key']);
});
it('carries a stored secret forward when the editor leaves it blank', () => {
const { config, secrets } = splitNotificationChannelConfig(
'telegram',
{ bot_token: '', chat_id: '42' },
{ bot_token: 'stored-token' },
);
expect(secrets).toEqual({ bot_token: 'stored-token' });
expect(config['chat_id']).toBe('42');
});
it('names the missing required fields in the local language', () => {
expect(() => splitNotificationChannelConfig('serverchan', {})).toThrowError(
NotificationChannelConfigError,
);
try {
splitNotificationChannelConfig('serverchan', {});
} catch (error) {
const failure = error as NotificationChannelConfigError;
expect(failure.fields).toEqual(['send_key']);
expect(failure.message).toContain('SendKey');
}
});
it('validates numeric ranges and select membership', () => {
const relay = {
smtp_host: 'smtp.example.test',
sender_address: 'a@example.test',
receiver_addresses: 'b@example.test',
};
expect(() => splitNotificationChannelConfig('email', { ...relay, smtp_port: 99_999 })).toThrow(
/SMTP 端口/u,
);
expect(() =>
splitNotificationChannelConfig('bark', { device_key: 'k', level: 'loud' }),
).toThrow(/推送级别/u);
expect(
splitNotificationChannelConfig('email', {
...relay,
smtp_port: '587',
smtp_security: 'starttls',
}).config['smtp_port'],
).toBe(587);
});
it('rejects unknown channel types instead of guessing a schema', () => {
expect(() => splitNotificationChannelConfig('sms' as never, {} as never)).toThrowError(
NotificationChannelConfigError,
);
});
it('keeps declared defaults for untouched fields', () => {
expect(notificationChannelDefaults('webhook')).toEqual({
url: '',
http_method: 'POST',
headers: '',
});
expect(notificationChannelFields('webhook').map((field) => field.kind)).toEqual([
'url',
'select',
'secret',
'key-values',
]);
});
});
describe('parseKeyValueField', () => {
it('reads colon lines and ignores anything malformed', () => {
expect(parseKeyValueField('X-A: 1\n\nno-separator\nX-B: a/b \n:c-empty')).toEqual({
'X-A': '1',
'X-B': 'a/b',
});
});
it('returns nothing for non-text input', () => {
expect(parseKeyValueField(undefined)).toEqual({});
expect(parseKeyValueField({})).toEqual({});
});
});