'use strict'; const test = require('node:test'); const assert = require('node:assert'); const { Notifier } = require('../src/core/notifier'); const { defaultConfig } = require('../src/config/expansionConfig'); /** 造一个记录请求的假 fetch */ function setup(options) { const opts = options || {}; const config = defaultConfig(); Object.assign(config.notify, opts.notify || {}); const calls = []; const fetchImpl = opts.fetchImpl || (async (url, init) => { calls.push({ url, init }); return { ok: true, status: 200 }; }); const notifier = new Notifier(config, null, { fetchImpl, minIntervalMs: opts.minIntervalMs }); return { notifier, config, calls }; } test('默认关闭:没启用或没配地址时什么都不发', async () => { const off = setup(); assert.strictEqual(off.notifier.isEnabled(), false); assert.strictEqual(await off.notifier.notify('hi'), false); assert.strictEqual(off.calls.length, 0); const noUrl = setup({ notify: { enabled: true } }); assert.strictEqual(noUrl.notifier.isEnabled(), false, '没有地址等同于没启用'); assert.strictEqual(await noUrl.notifier.notify('hi'), false); }); test('启用后按飞书自定义机器人的文本格式 POST', async () => { const { notifier, calls } = setup({ notify: { enabled: true, webhookUrl: 'https://open.feishu.cn/open-apis/bot/v2/hook/abc' } }); assert.strictEqual(notifier.isEnabled(), true); assert.strictEqual(await notifier.notify('需要验证码'), true); assert.strictEqual(calls.length, 1); assert.strictEqual(calls[0].url, 'https://open.feishu.cn/open-apis/bot/v2/hook/abc'); const body = JSON.parse(calls[0].init.body); assert.strictEqual(body.msgtype, 'text'); assert.strictEqual(body.content.text, '需要验证码'); assert.strictEqual(notifier.sent, 1); }); test('同一个去重 key 在最小间隔内只发一次(避免反复打扰)', async () => { const { notifier, calls } = setup({ notify: { enabled: true, webhookUrl: 'https://example.com/hook' }, minIntervalMs: 60000 }); assert.strictEqual(await notifier.notify('a', { key: 'verification:BotA' }), true); assert.strictEqual(await notifier.notify('a', { key: 'verification:BotA' }), false, '同一问题不重复提醒'); assert.strictEqual(calls.length, 1); // 不同 key 互不影响 assert.strictEqual(await notifier.notify('b', { key: 'rescue-failed:BotA' }), true); assert.strictEqual(calls.length, 2); // force 可以跳过去重(手动重发用) assert.strictEqual(await notifier.notify('a', { key: 'verification:BotA', force: true }), true); assert.strictEqual(calls.length, 3); }); test('Webhook 地址可回退环境变量 NOTIFY_WEBHOOK_URL', async () => { const previous = process.env.NOTIFY_WEBHOOK_URL; process.env.NOTIFY_WEBHOOK_URL = 'https://example.com/from-env'; try { const { notifier, calls } = setup({ notify: { enabled: true } }); assert.strictEqual(notifier.isEnabled(), true, '配置里没填地址时应回退环境变量'); await notifier.notify('hi'); assert.strictEqual(calls[0].url, 'https://example.com/from-env'); } finally { if (previous === undefined) delete process.env.NOTIFY_WEBHOOK_URL; else process.env.NOTIFY_WEBHOOK_URL = previous; } }); test('发送失败与异常都被记下来,不会抛出', async () => { const { notifier } = setup({ notify: { enabled: true, webhookUrl: 'https://example.com/hook' }, fetchImpl: async () => ({ ok: false, status: 500 }) }); assert.strictEqual(await notifier.notify('hi'), false); assert.strictEqual(notifier.failed, 1); assert.strictEqual(notifier.lastError, 'HTTP 500'); const throwing = setup({ notify: { enabled: true, webhookUrl: 'https://example.com/hook' }, fetchImpl: async () => { throw new Error('网络不通'); } }); assert.strictEqual(await throwing.notifier.notify('hi'), false, '异常必须被吞掉'); assert.strictEqual(throwing.notifier.failed, 1); assert.ok(throwing.notifier.lastError.includes('网络不通')); }); test('snapshot 只暴露域名,不泄露带凭证的完整地址', async () => { const { notifier } = setup({ notify: { enabled: true, webhookUrl: 'https://open.feishu.cn/open-apis/bot/v2/hook/secret-token' } }); await notifier.notify('hi'); const snap = notifier.snapshot(); assert.strictEqual(snap.enabled, true); assert.strictEqual(snap.configured, true); assert.strictEqual(snap.host, 'open.feishu.cn'); assert.strictEqual(snap.sent, 1); assert.ok(!JSON.stringify(snap).includes('secret-token'), '完整地址不能出现在接口响应里'); });