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({}); }); });