Files
BYW-VoiceHub/test/wechatSubscribeFields.test.mjs
T

177 lines
7.7 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.
/**
* 微信订阅消息字段裁剪规则的单元测试(`npm test` 跑)。
*
* 为什么值得单独测:微信对订阅消息的字段校验是**整条消息级别**的 —— 一个字段超长、
* 格式不对或为空,整条消息就被拒(47003),而且报错里点名的字段不一定是真凶。
* 这些规则一旦写错,线上表现就是「通知发不出去」,却看不出哪里错。
*/
import { test } from 'node:test'
import assert from 'node:assert/strict'
import {
beijingNow,
buildWechatTimeValue,
clampField
} from '../server/utils/wechatSubscribeFields.ts'
const len = (s) => Array.from(s).length
test('thing 型:20 字以内原样,超了截 19 字 + 省略号(正好 20)', () => {
assert.equal(clampField('thing2', '晴天'), '晴天')
const twenty = '一二三四五六七八九十一二三四五六七八九十'
assert.equal(clampField('thing2', twenty), twenty)
assert.equal(len(clampField('thing2', twenty)), 20)
const long = twenty + '崩坏的世界'
const clamped = clampField('thing2', long)
assert.equal(len(clamped), 20)
assert.ok(clamped.endsWith('…'))
assert.ok(long.startsWith(clamped.slice(0, 19)))
})
test('phrase 型:5 个以内纯汉字——“已排期”会被补成全角 5 字,不是原样发出去', () => {
const padded = clampField('phrase5', '已排期')
assert.equal(len(padded), 5)
assert.ok(padded.startsWith('已排期'))
// 刚好 5 个汉字原样保留
assert.equal(clampField('phrase5', '已排期播放'), '已排期播放')
// 超了截到 5 个
assert.equal(len(clampField('phrase5', '已排期播放中啦啦啦')), 5)
})
test('time 型:必须是 YYYY-MM-DD HH:mm 且不超过 24 字', () => {
const w = (v) => ({ value: clampField('time1', v) })
assert.deepEqual(w(buildWechatTimeValue(new Date('2026-02-14T00:00:00Z'), '12:30')), {
value: '2026-02-14 12:30'
})
assert.match(clampField('time1', '2026-02-14 12:30'), /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/)
assert.equal(len(clampField('time1', '2026-02-14 12:30')), 16)
})
test('buildWechatTimeValue:取时段开始时间,兼容各种写法,兜底当天 00:00', () => {
const day = new Date('2026-02-14T00:00:00Z')
assert.equal(buildWechatTimeValue(day, '12:30-13:00'), '2026-02-14 12:30')
assert.equal(buildWechatTimeValue(day, '12:30~13:00'), '2026-02-14 12:30')
assert.equal(buildWechatTimeValue(day, '12:30'), '2026-02-14 12:30')
assert.equal(buildWechatTimeValue(day, '9:05'), '2026-02-14 09:05')
assert.equal(buildWechatTimeValue(day, '13:00'), '2026-02-14 13:00')
// time 型字段不能为空,解析不出来也要给个合法值
assert.equal(buildWechatTimeValue(day, ''), '2026-02-14 00:00')
assert.equal(buildWechatTimeValue(day, null), '2026-02-14 00:00')
assert.equal(buildWechatTimeValue(day, '午间'), '2026-02-14 00:00')
})
test('buildWechatTimeValue:以北京时间取日期(UTC 16:00 已是次日)', () => {
assert.equal(buildWechatTimeValue(new Date('2026-02-13T16:00:00Z'), '12:30'), '2026-02-14 12:30')
assert.equal(buildWechatTimeValue(new Date('2026-02-13T15:59:00Z'), '12:30'), '2026-02-13 12:30')
})
test('character_string 型:序号原样发(不补零、不加字)', () => {
assert.equal(clampField('character_string3', 1), '1')
assert.equal(clampField('character_string3', '12'), '12')
assert.equal(clampField('character_string3', 0), '0')
})
test('date 型:YYYY-MM-DD 原样,脏值截到 10 字', () => {
assert.equal(clampField('date4', '2026-02-14'), '2026-02-14')
assert.equal(clampField('date4', '2026-02-14 12:30:00'), '2026-02-14')
})
test('空白与换行会被压成一行(微信不接受换行)', () => {
assert.equal(clampField('thing1', ' 晴天\n 周杰伦 '), '晴天 周杰伦')
assert.equal(clampField('thing1', null), '')
assert.equal(clampField('thing1', undefined), '')
})
test('SONG_SELECTED 的内置字段映射:每个字段名后缀都要跟发出去的值类型匹配', () => {
// 与 server/services/wechatSubscribeService.ts 的 DEFAULT_TEMPLATES.SONG_SELECTED 保持一致
const fields = {
thing2: 'songTitle',
date4: 'playDate',
time1: 'playTime',
character_string3: 'sequence',
phrase5: 'status'
}
const vars = {
songTitle: '晴天',
playDate: '2026-02-14',
playTime: '2026-02-14 12:30',
sequence: '1',
status: '已排期'
}
const out = {}
for (const key of Object.keys(fields)) out[key] = clampField(key, vars[fields[key]])
assert.equal(out.thing2, '晴天')
assert.equal(out.date4, '2026-02-14')
assert.match(out.time1, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/)
assert.equal(out.character_string3, '1')
assert.equal(len(out.phrase5), 5) // “已排期” 3 字 → 补齐到 5 字,避开 47003
// 不能有任何字段是空值:空值同样会被微信拒
for (const key of Object.keys(out)) assert.ok(out[key].length > 0, key + ' 不能为空')
})
test('SONG_REJECTED 的内置字段映射:长驳回原因必须裁进 thing 的 20 字内', () => {
/*
* 模板字段(站长在公众平台选好的):
* 审核内容 = thing6 → songTitle
* 驳回原因 = thing4 → reason
* 驳回原因是管理员手填的自由文本,经常远超 20 字;thing 超长是整条消息 47003 的头号原因,
* 所以这里专门盯住「长原因不会被原样发出去」。
*/
const fields = { thing6: 'songTitle', thing4: 'reason' }
const vars = {
songTitle: '晴天 - 周杰伦',
reason:
'已有同名单曲,暂不重复收录;另外上传的音频码率过低,建议重新投稿 320kbps 版本后再试一次'
}
const out = {}
for (const key of Object.keys(fields)) out[key] = clampField(key, vars[fields[key]])
assert.equal(out.thing6, '晴天 - 周杰伦')
assert.ok(len(out.thing4) <= 20)
assert.ok(out.thing4.endsWith('…'))
for (const key of Object.keys(out)) assert.ok(out[key].length > 0, key + ' 不能为空')
})
test('内置模板与线上代码一致(直接读源码,防止映射被改坏没人发现)', async () => {
/*
* 为什么不 import:wechatSubscribeService.ts 顶部 import 了 `~~/server/...` 这类 Nuxt 别名,
* 纯 `node --test` 解析不了。这里只校验「模板 ID 和字段映射」这类静态事实,读文本足够,
* 而且不需要跑起来就能在 CI 里拦住「手滑改了模板 ID / 字段名」。
*/
const { readFileSync } = await import('node:fs')
const { fileURLToPath } = await import('node:url')
const src = readFileSync(
fileURLToPath(new URL('../server/services/wechatSubscribeService.ts', import.meta.url)),
'utf8'
)
const block = (kind) => {
const m = src.match(new RegExp(kind + ':\\s*\\{([\\s\\S]*?)\\n \\}'))
assert.ok(m, '源码里找不到 ' + kind + ' 的内置模板')
return m[1]
}
const rejected = block('SONG_REJECTED')
assert.match(rejected, /erAK99M38ZeD-F1mOIgN2M7iAPVTU0YXo-adlkSwg_c/)
assert.match(rejected, /thing6:\s*'songTitle'/)
assert.match(rejected, /thing4:\s*'reason'/)
const selected = block('SONG_SELECTED')
assert.match(selected, /oLTAefO92xYYlGRYmcsm5AfUUlUI5IKFfXhIVNrItSk/)
for (const f of ['thing2', 'date4', 'time1', 'character_string3', 'phrase5']) {
assert.match(selected, new RegExp(f + ":\\s*'"), f + ' 的映射丢了')
}
})
test('beijingNow 是合法的 time 型值,且与北京时间一致', () => {
assert.match(beijingNow(), /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/)
// beijingNow() 内部按 UTC+8 取值,用同一算法反推对比
const t = new Date(Date.now() + 8 * 3600 * 1000)
const p = (n) => (n < 10 ? '0' + n : String(n))
assert.equal(
beijingNow(),
`${t.getUTCFullYear()}-${p(t.getUTCMonth() + 1)}-${p(t.getUTCDate())} ${p(t.getUTCHours())}:${p(t.getUTCMinutes())}`
)
})