Files
BYW-VoiceHub/test/wechatSubscribeRemote.test.mjs

193 lines
7.5 KiB
JavaScript

/**
* `server/utils/wechatSubscribeRemote.ts` 的单测:把微信模板原文解析成关键词表,
* 再按类型/中文标签把我们的变量名对应到微信**真实分配**的关键词编号。
*
* 背景:公众平台创建模板时,关键词编号(thing1 / date2 …)是微信自动打的,猜不出来。
* 猜错的表现就是整条消息 47003,而报错点名的字段往往不是真凶 —— 所以解析和映射必须能单独测。
*
* 运行:node --test test/wechatSubscribeRemote.test.mjs
*/
import { test } from 'node:test'
import assert from 'node:assert/strict'
import {
keywordTypeOf,
parseTemplateContent,
resolveFieldsFromKeywords
} from '../server/utils/wechatSubscribeRemote.ts'
/** 微信「获取已有模板列表」返回里 content 的真实形状 */
const SONG_SELECTED_CONTENT =
'歌曲名称:{{thing1.DATA}}\n播放日期:{{date2.DATA}}\n播放时间:{{time3.DATA}}\n序号:{{character_string4.DATA}}\n状态:{{phrase5.DATA}}\n'
const SONG_REJECTED_CONTENT = '审核内容:{{thing6.DATA}}\n驳回原因:{{thing4.DATA}}\n'
test('keywordTypeOf:去掉尾部数字得到关键词类型,认不出的记为 unknown', () => {
assert.equal(keywordTypeOf('thing1'), 'thing')
assert.equal(keywordTypeOf('character_string3'), 'character_string')
assert.equal(keywordTypeOf('phone_number2'), 'phone_number')
assert.equal(keywordTypeOf('foo2'), 'unknown')
assert.equal(keywordTypeOf(''), 'unknown')
})
test('parseTemplateContent:按出现顺序解析出 code / type / 中文标签', () => {
const kws = parseTemplateContent(SONG_SELECTED_CONTENT)
assert.deepEqual(
kws.map((k) => k.code),
['thing1', 'date2', 'time3', 'character_string4', 'phrase5']
)
assert.deepEqual(
kws.map((k) => k.type),
['thing', 'date', 'time', 'character_string', 'phrase']
)
assert.deepEqual(
kws.map((k) => k.label),
['歌曲名称', '播放日期', '播放时间', '序号', '状态']
)
assert.deepEqual(
kws.map((k) => k.index),
[0, 1, 2, 3, 4]
)
})
test('parseTemplateContent:没有占位符就返回空表,不会抛', () => {
assert.deepEqual(parseTemplateContent(''), [])
assert.deepEqual(parseTemplateContent('随便一段没有占位符的文字'), [])
assert.deepEqual(parseTemplateContent(undefined), [])
})
test('resolveFieldsFromKeywords:配置的编号和微信一致时保持 config 以外的等价结果', () => {
const configured = {
thing1: 'songTitle',
date2: 'playDate',
time3: 'playTime',
character_string4: 'sequence',
phrase5: 'status'
}
const r = resolveFieldsFromKeywords(parseTemplateContent(SONG_SELECTED_CONTENT), configured)
assert.equal(r.source, 'wechat-api')
assert.deepEqual(r.fields, configured)
assert.deepEqual(r.invalidConfiguredCodes, [])
assert.match(r.reason, /一致/)
})
test('resolveFieldsFromKeywords:配置编号猜错了也能按类型/标签纠正回真实编号', () => {
// 我们原来猜的是 thing2/date4/time1/character_string3/phrase5,实际是 thing1/date2/time3/character_string4/phrase5
const configured = {
thing2: 'songTitle',
date4: 'playDate',
time1: 'playTime',
character_string3: 'sequence',
phrase5: 'status'
}
const r = resolveFieldsFromKeywords(parseTemplateContent(SONG_SELECTED_CONTENT), configured)
assert.equal(r.source, 'wechat-api')
assert.deepEqual(r.fields, {
thing1: 'songTitle',
date2: 'playDate',
time3: 'playTime',
character_string4: 'sequence',
phrase5: 'status'
})
// 配置里 4 个编号在微信模板里根本不存在 —— 这就是 47003 的直接证据
assert.deepEqual(r.invalidConfiguredCodes.sort(), [
'character_string3',
'date4',
'thing2',
'time1'
])
assert.match(r.reason, /校准/)
})
test('resolveFieldsFromKeywords:两个同类型关键词必须靠中文标签区分(驳回模板)', () => {
const configured = { thing6: 'songTitle', thing4: 'reason' }
const r = resolveFieldsFromKeywords(parseTemplateContent(SONG_REJECTED_CONTENT), configured)
assert.equal(r.source, 'wechat-api')
assert.deepEqual(r.fields, { thing6: 'songTitle', thing4: 'reason' })
assert.deepEqual(r.invalidConfiguredCodes, [])
})
test('resolveFieldsFromKeywords:两个 thing 但标签认不出来时,不冒险互换,沿用配置', () => {
const content = '{{thing1.DATA}}\n{{thing2.DATA}}\n'
const configured = { thing6: 'songTitle', thing4: 'reason' }
const r = resolveFieldsFromKeywords(parseTemplateContent(content), configured)
assert.equal(r.source, 'config')
assert.deepEqual(r.fields, configured)
assert.match(r.reason, /标签不足以区分/)
})
test('resolveFieldsFromKeywords:拿不到关键词表时一律沿用配置', () => {
const configured = { thing2: 'songTitle' }
const r = resolveFieldsFromKeywords([], configured)
assert.equal(r.source, 'config')
assert.deepEqual(r.fields, configured)
})
/**
* 回归:线上真实踩过的坑。
*
* 旧配置是「按常见命名猜的」thing2/date4/time1/character_string3/phrase5,五个编号全错,
* 而且模板里**根本没有日期关键词**。当时按变量顺序贪心认领,playDate(首选 date,模板里
* 没有 date)先把 character_string4 抢走,轮到 sequence 就没位置了,整张模板退回旧配置,
* 发出去就是 `47003 data.thing1.value is empty`。
*/
const REAL_SONG_SELECTED_CONTENT =
'歌曲名称:{{thing1.DATA}}\n排期状态:{{phrase2.DATA}}\n播放时间:{{time3.DATA}}\n排期序号:{{character_string4.DATA}}\n'
test('回归:真实排期模板 + 猜错的旧配置 → 校准成正确的四个编号,并丢掉模板里没有的 playDate', () => {
const wrongConfigured = {
thing2: 'songTitle',
date4: 'playDate',
time1: 'playTime',
character_string3: 'sequence',
phrase5: 'status'
}
const r = resolveFieldsFromKeywords(
parseTemplateContent(REAL_SONG_SELECTED_CONTENT),
wrongConfigured
)
assert.equal(r.source, 'wechat-api')
assert.deepEqual(r.fields, {
thing1: 'songTitle',
phrase2: 'status',
time3: 'playTime',
character_string4: 'sequence'
})
// 模板里没有日期关键词 → playDate 被忽略(发了微信也不认)
assert.deepEqual(r.unresolvedVariables, ['playDate'])
assert.deepEqual(r.unclaimedCodes, [], '四个关键词必须全部填上,否则微信会报 value is empty')
assert.match(r.reason, /已忽略/)
})
test('回归:真实排期模板 + 已修正的内置配置 → 一致,不做任何改动', () => {
const good = {
thing1: 'songTitle',
phrase2: 'status',
time3: 'playTime',
character_string4: 'sequence'
}
const r = resolveFieldsFromKeywords(parseTemplateContent(REAL_SONG_SELECTED_CONTENT), good)
assert.equal(r.source, 'wechat-api')
assert.deepEqual(r.fields, good)
assert.deepEqual(r.invalidConfiguredCodes, [])
assert.deepEqual(r.unclaimedCodes, [])
assert.equal(r.reason, '配置与微信真实模板一致')
})
test('unclaimedCodes:模板里有、配置里没有的关键词要被点名(这是 value is empty 的来源)', () => {
const r = resolveFieldsFromKeywords(parseTemplateContent(REAL_SONG_SELECTED_CONTENT), {
thing1: 'songTitle'
})
assert.deepEqual(r.unclaimedCodes.sort(), ['character_string4', 'phrase2', 'time3'])
// 填不满模板时不动配置
assert.equal(r.source, 'config')
})
test('resolveFieldsFromKeywords:变量类型未知时不做自动映射', () => {
const r = resolveFieldsFromKeywords(parseTemplateContent(SONG_SELECTED_CONTENT), {
thing1: 'whateverCustomVar'
})
assert.equal(r.source, 'config')
assert.match(r.reason, /类型未知/)
})