Files
wxserver/server/test/reminder.test.js
T
yuming 57470ade4f 补充发送侧分组与错误分支的测试覆盖,保留43101原始错误信息的修复
- reminder.js:43101 跳过日志改为记录微信原始错误信息,便于和「额度耗尽」的
  兜底文案区分排查(前一实施者已验证,此次原样带入提交)
- reminder.test.js:新增 runOnce 按 openid 分组结算用例,验证多用户额度互不
  串号;将「非额度类错误」用例扩到 2 条数据,证明该分支不会误中止后续发送
- 新建 wx.test.js:验证 sendSubscribeMessage 在微信返回非 0 errcode 时,
  抛出的 Error 对象确实带有正确的 errcode 属性

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 09:07:46 +08:00

179 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const test = require('node:test')
const assert = require('node:assert')
const { useTempDb } = require('./helper')
useTempDb()
const db = require('../src/db')
const wx = require('../src/wx')
const quota = require('../src/quota')
const reminder = require('../src/reminder')
// 用「今天」为基准造数据,保证 daysUntil 落在期望值上
const TODAY = new Date()
const inDays = (n) => new Date(TODAY.getTime() + n * 86400000)
function makeAnniv(id, openid, personName, importance, offsetDays, remindDays) {
const d = inDays(offsetDays)
return {
id, openid, personId: 'p', personName, type: 'birthday',
isLunar: 0, solarYear: d.getFullYear(), solarMonth: d.getMonth() + 1, solarDay: d.getDate(),
importance, remindEnabled: 1, remindDays
}
}
function resetLogs() {
db.prepare('DELETE FROM remind_logs').run()
}
test('额度够时全部发出', async () => {
resetLogs()
quota.grant('uA', 5)
const sent = []
const restore = wx.sendSubscribeMessage
wx.sendSubscribeMessage = async (p) => { sent.push(p.data.name1.value); return { errcode: 0 } }
const items = [
makeAnniv('A1', 'uA', '甲', 'low', 0, 3),
makeAnniv('A2', 'uA', '乙', 'high', 0, 3)
]
const r = await reminder.runForUser('uA', items, TODAY)
wx.sendSubscribeMessage = restore
assert.strictEqual(r.ok, 2)
assert.strictEqual(r.skipped, 0)
assert.strictEqual(quota.getBalance('uA'), 3)
})
test('额度不足时当天优先、同级按重要程度', async () => {
resetLogs()
quota.grant('uB', 1)
const sent = []
const restore = wx.sendSubscribeMessage
wx.sendSubscribeMessage = async (p) => { sent.push(p.data.name1.value); return { errcode: 0 } }
const items = [
makeAnniv('B1', 'uB', '提前的', 'high', 3, 3), // 提前事件(daysUntil=3=remindDays
makeAnniv('B2', 'uB', '当天低', 'low', 0, 3), // 当天事件
makeAnniv('B3', 'uB', '当天高', 'high', 0, 3) // 当天事件,重要程度更高
]
const r = await reminder.runForUser('uB', items, TODAY)
wx.sendSubscribeMessage = restore
assert.strictEqual(r.ok, 1)
assert.strictEqual(r.skipped, 2)
assert.deepStrictEqual(sent, ['当天高'], '应当只发出当天且最重要的那条')
})
test('余额为 0 时不发任何请求', async () => {
resetLogs()
let called = 0
const restore = wx.sendSubscribeMessage
wx.sendSubscribeMessage = async () => { called++; return { errcode: 0 } }
const items = [makeAnniv('C1', 'uC', '丙', 'high', 0, 3)]
const r = await reminder.runForUser('uC', items, TODAY)
wx.sendSubscribeMessage = restore
assert.strictEqual(called, 0, '没额度就不该打请求')
assert.strictEqual(r.skipped, 1)
})
test('遇到 43101 立即归零并中止本用户剩余发送', async () => {
resetLogs()
quota.grant('uD', 9)
let called = 0
const restore = wx.sendSubscribeMessage
wx.sendSubscribeMessage = async () => {
called++
const e = new Error('发送订阅消息失败')
e.errcode = 43101
throw e
}
const items = [
makeAnniv('D1', 'uD', '甲', 'high', 0, 3),
makeAnniv('D2', 'uD', '乙', 'high', 0, 3),
makeAnniv('D3', 'uD', '丙', 'high', 0, 3)
]
const r = await reminder.runForUser('uD', items, TODAY)
wx.sendSubscribeMessage = restore
assert.strictEqual(called, 1, '失败一次就该停,不该继续打请求')
assert.strictEqual(quota.getBalance('uD'), 0, '余额应被归零校正')
assert.strictEqual(r.skipped, 3, '含失败那条在内全部计为 skipped')
})
test('非额度类错误只记 failed,不中止后续发送,也不动余额', async () => {
resetLogs()
quota.grant('uE', 4)
let called = 0
const restore = wx.sendSubscribeMessage
wx.sendSubscribeMessage = async () => {
called++
const e = new Error('网络炸了')
e.errcode = 40003
throw e
}
// 至少 2 条数据:如果有人误把 halted = true 加进非额度错误分支,
// 第二条就不会被尝试,called 会停在 1,这条断言就能抓到
const items = [
makeAnniv('E1', 'uE', '甲', 'high', 0, 3),
makeAnniv('E2', 'uE', '乙', 'high', 0, 3)
]
const r = await reminder.runForUser('uE', items, TODAY)
wx.sendSubscribeMessage = restore
assert.strictEqual(called, 2, '第二条也应被尝试,证明非额度错误不会误中止后续发送')
assert.strictEqual(r.fail, 2)
assert.strictEqual(quota.getBalance('uE'), 4, '配置/网络错误不该动余额')
})
test('skipped 会写入 remind_logs 便于排查', async () => {
resetLogs()
const items = [makeAnniv('F1', 'uF', '甲', 'high', 0, 3)]
await reminder.runForUser('uF', items, TODAY)
const row = db.prepare("SELECT * FROM remind_logs WHERE status = 'skipped'").get()
assert.ok(row, '应写入 skipped 日志')
assert.strictEqual(row.error, 'quota_exhausted')
})
// runOnce 是从数据库读数据的(SELECT * FROM anniversaries WHERE remindEnabled = 1),
// 不能像 runForUser 那样直接传数组,必须真的往临时库插行
function insertAnniv(id, openid, personName, offsetDays, remindDays) {
const d = inDays(offsetDays)
db.prepare(`
INSERT INTO anniversaries
(id, openid, personId, personName, type, isLunar, solarYear, solarMonth, solarDay,
importance, remindEnabled, remindDays, createTime, updateTime)
VALUES (?, ?, 'p', ?, 'birthday', 0, ?, ?, ?, 'high', 1, ?, 0, 0)
`).run(id, openid, personName, d.getFullYear(), d.getMonth() + 1, d.getDate(), remindDays)
}
test('runOnce 按 openid 分组结算,各用户额度互不影响', async () => {
resetLogs()
// 会读到库里所有 remindEnabled=1 的行,先清空避免受其它用例残留数据干扰
db.prepare('DELETE FROM anniversaries').run()
quota.grant('uG', 5) // uG 有额度
// uH 不 grant,余额保持 0
insertAnniv('G1', 'uG', '甲', 0, 3) // 今天到期
insertAnniv('H1', 'uH', '乙', 0, 3) // 今天到期
let called = 0
const restore = wx.sendSubscribeMessage
wx.sendSubscribeMessage = async () => { called++; return { errcode: 0 } }
const r = await reminder.runOnce()
wx.sendSubscribeMessage = restore
assert.strictEqual(r.total, 2)
assert.strictEqual(r.ok, 1, '有额度的 uG 应正常发出')
assert.strictEqual(r.skipped, 1, '没额度的 uH 应被跳过')
assert.strictEqual(r.fail, 0)
assert.strictEqual(called, 1, '没额度的用户不该真的发起请求,两用户不能互相借额度')
assert.strictEqual(quota.getBalance('uG'), 4, 'uG 消费后余额正确减少')
assert.strictEqual(quota.getBalance('uH'), 0, 'uH 的余额不受 uG 影响')
})