'use strict'; /** * backend 最小可跑单测集(P1 ㉚a) * ================================================================ * 背景:`backend/package.json` 声明了 `test-local: egg-bin test`,但 `backend/test/` * 长期是**空目录** ⇒ 「配置存在、用例为零」——跑 `npm run test-local` 恒绿(0 个用例), * 属 R95 根因①「最危险假绿族」。本文件填充**真实、无外部依赖(无 DB / 无网络 / * 无 egg 启动)、有判别力**的最小单测集,使该 script 不再空转。 * * 取材原则(为什么选这三个纯函数模块): * ① 业务核心:aggregationKey = 铁律13/14 归集键 SoT;idCard = 导入通道判据 SoT; * filename = R108g 上传落盘名清洗。 * ② **判别力**:三者的关键判据都有「退化后必然出错」的形态—— * · aggregationKey 若退回「无条件双键归集」(历史 bug)⇒ 跨编号空间重叠用例必红; * · idCard 若校验位算法改动 ⇒ 已知公开有效号 `11010519491231002X` 用例必红; * · filename 若漏控制字符 / 路径分隔符清洗 ⇒ 穿越与 ENOENT 用例必红。 * ③ 零副作用:全部为纯函数,不触盘、不连库。 * * ⚠️ 运行:`cd backend && node_modules/.bin/egg-bin test`(或裸 mocha 直跑本文件)。 */ const assert = require('assert'); const aggKey = require('../app/lib/aggregationKey'); const idCard = require('../app/lib/idCard'); const { sanitizeUploadFileName } = require('../app/utils/filename'); describe('app/lib/aggregationKey —— 归集键 SoT(铁律13/14)', () => { it('考试组:主键 = contestant.id,兜底键 = 真实聚合 uid', () => { assert.strictEqual(aggKey.primaryKeyOf('gaozhi', 5, 9), '5'); assert.strictEqual(aggKey.primaryKeyOf('zhongzhi', 5, 9), '5'); assert.strictEqual(aggKey.fallbackKeyOf('gaozhi', 5, 9), '9'); }); it('考试组:缺真实聚合 uid ⇒ 无兜底键(null),而非拼一个假键', () => { assert.strictEqual(aggKey.fallbackKeyOf('gaozhi', 5, null), null); assert.strictEqual(aggKey.fallbackKeyOf('gaozhi', 5, ''), null); }); it('作品组:主键 = 真实聚合 uid,兜底键 = cid', () => { assert.strictEqual(aggKey.primaryKeyOf('benke', 5, 9), '9'); assert.strictEqual(aggKey.fallbackKeyOf('benke', 5, 9), '5'); }); it('作品组:缺真实聚合 uid ⇒ 主键退化为 cid(不再引入第二个键)', () => { assert.strictEqual(aggKey.primaryKeyOf('benke', 5, null), '5'); assert.strictEqual(aggKey.primaryKeyOf('benke', 5, ''), '5'); }); it('isExamGroup 只认 zhongzhi / gaozhi', () => { assert.strictEqual(aggKey.isExamGroup('zhongzhi'), true); assert.strictEqual(aggKey.isExamGroup('gaozhi'), true); assert.strictEqual(aggKey.isExamGroup('benke'), false); assert.strictEqual(aggKey.isExamGroup('jiaoshi'), false); }); // ---- 判别力核心:跨编号空间重叠 ---- // 生产实测(2026-10-08)283 对碰撞的根因就是「无条件双键归集」。 it('buildAggregationKeyMap:兜底键撞上同组他人主键 ⇒ 必须被丢弃', () => { // 考试组:cid=1 的真实聚合 uid = 2,而 cid=2 的主键恰为 '2' ⇒ 二者重叠 const contestants = [{ id: 1 }, { id: 2 }]; const realAgg = { 1: 2 }; const map = aggKey.buildAggregationKeyMap('gaozhi', contestants, realAgg); assert.deepStrictEqual(map['1'], ['1'], 'cid=1 的兜底键 2 会命中 cid=2 ⇒ 必须丢弃'); assert.deepStrictEqual(map['2'], ['2']); }); it('buildAggregationKeyMap:安全兜底键须保留(不因噎废食)', () => { const contestants = [{ id: 1 }, { id: 2 }]; const realAgg = { 1: 7 }; // 7 未被任何人认领主键 const map = aggKey.buildAggregationKeyMap('gaozhi', contestants, realAgg); assert.deepStrictEqual(map['1'], ['1', '7']); assert.deepStrictEqual(map['2'], ['2']); }); it('buildAggregationKeyMap:空输入 / 非数组不抛错', () => { assert.deepStrictEqual(aggKey.buildAggregationKeyMap('gaozhi', null, null), {}); assert.deepStrictEqual(aggKey.buildAggregationKeyMap('gaozhi', [], {}), {}); }); it('buildAggregationKeyMap:首元素恒为主键且键不重复', () => { const contestants = [{ id: 3 }]; const map = aggKey.buildAggregationKeyMap('benke', contestants, { 3: 3 }); // 兜底=cid=主键 assert.deepStrictEqual(map['3'], ['3'], '主键与兜底相等时不得重复'); }); }); describe('app/lib/idCard —— 导入通道判据 SoT', () => { it('normalizeIdNumber:trim + 末位 x 归一为大写 X', () => { assert.strictEqual(idCard.normalizeIdNumber(' 11010519491231002x '), '11010519491231002X'); assert.strictEqual(idCard.normalizeIdNumber(null), ''); assert.strictEqual(idCard.normalizeIdNumber(undefined), ''); }); it('expectedCheckCode:对 GB11643 公开样例号算出正确校验位(独立已知答案)', () => { // `11010519491231002X` 为公开引用的有效样例;校验位算法若被改动此处必红。 assert.strictEqual(idCard.expectedCheckCode('11010519491231002'), 'X'); // 入参不足 17 位或含非数字 ⇒ 返回 '' assert.strictEqual(idCard.expectedCheckCode('123'), ''); assert.strictEqual(idCard.expectedCheckCode('1101051949123100A'), ''); }); it('inspectIdNumber:合法号 ⇒ importable=true 且 level=null', () => { const r = idCard.inspectIdNumber('11010519491231002X'); assert.strictEqual(r.importable, true); assert.strictEqual(r.level, null); }); it('inspectIdNumber:空 ⇒ error 且不可导入', () => { const r = idCard.inspectIdNumber(''); assert.strictEqual(r.importable, false); assert.strictEqual(r.level, 'error'); }); it('inspectIdNumber:长度≠18 ⇒ error 且不可导入(模型层硬约束前置为可读错误)', () => { const r = idCard.inspectIdNumber('11010519491231002'); // 17 位 assert.strictEqual(r.importable, false); assert.strictEqual(r.level, 'error'); assert.ok(r.message.includes('18')); }); it('inspectIdNumber:18 位但校验位不符 ⇒ warn(不阻断,防误伤在册考生)', () => { const r = idCard.inspectIdNumber('110105194912310020'); // 末位应为 X assert.strictEqual(r.importable, true); assert.strictEqual(r.level, 'warn'); assert.ok(r.message.includes('校验位')); }); it('inspectIdNumber:18 位但含非法字符 ⇒ warn(模型只查长度)', () => { const r = idCard.inspectIdNumber('1101051949123100AX'); assert.strictEqual(r.importable, true); assert.strictEqual(r.level, 'warn'); }); }); describe('app/utils/filename —— 上传落盘名清洗(R108g)', () => { it('去除控制字符(防 stat 失败 / ENOENT)', () => { assert.strictEqual(sanitizeUploadFileName('a\x00b\x1fc.txt'), 'abc.txt'); }); it('路径分隔符替换为下划线(防目录穿越)', () => { assert.strictEqual(sanitizeUploadFileName('a/b\\c.txt'), 'a_b_c.txt'); }); it('折叠多余空白并 trim(修「前导空格 + 乱码」类 ENOENT)', () => { assert.strictEqual(sanitizeUploadFileName(' a b .txt'), 'a b .txt'); }); it('去除首尾点(防 .name / .. 类)', () => { assert.strictEqual(sanitizeUploadFileName('.hidden'), 'hidden'); }); it('超长名保留扩展名并截断到上限(120)', () => { const out = sanitizeUploadFileName('a'.repeat(200) + '.txt'); assert.strictEqual(out.length, 120); assert.ok(out.endsWith('.txt'), '扩展名必须保留'); }); it('客户端预编码(%XX)先解码', () => { assert.strictEqual(sanitizeUploadFileName('%E5%89%8D.txt'), '前.txt'); }); it('空值 / 非字符串 ⇒ 返回空串(调用方回退时间戳名)', () => { assert.strictEqual(sanitizeUploadFileName(''), ''); assert.strictEqual(sanitizeUploadFileName(null), ''); assert.strictEqual(sanitizeUploadFileName(123), ''); }); });