Files
YelayMaths/yelaymaths_api/tools/legacy/transform.test.ts
T
2026-09-29 19:34:24 +08:00

156 lines
5.8 KiB
TypeScript
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.
import { describe, expect, it } from 'vitest';
import { buildDataset } from './build';
import { DEFAULT_DUMP_PATH, loadLegacyDump } from './load';
import {
HOME_COURSE_TITLE_MAP,
SETTINGS_KEY_MAP,
SOURCE_LEGACY,
parseLegacyDate,
rewriteDomain,
transformCourse,
transformSetting,
} from './transform';
describe('parseLegacyDate', () => {
it('把旧库的本地墙钟时间按 +08:00 换算为 UTC', () => {
// 2024-04-11 00:00:39(北京时间)= 2024-04-10 16:00:39 UTC
expect(parseLegacyDate('2024-04-11 00:00:39')?.toISOString()).toBe('2024-04-10T16:00:39.000Z');
});
it('空值与零日期返回 null', () => {
expect(parseLegacyDate(null)).toBeNull();
expect(parseLegacyDate('')).toBeNull();
expect(parseLegacyDate('0000-00-00 00:00:00')).toBeNull();
});
it('只有日期没有时间的旧值也能解析(旧库确实存在)', () => {
expect(parseLegacyDate('2024-02-01')?.toISOString()).toBe('2024-01-31T16:00:00.000Z');
});
it('无法解析的值直接抛错,而不是静默变成 Invalid Date', () => {
expect(() => parseLegacyDate('不是时间')).toThrow();
});
});
describe('transformCourse', () => {
it('清理标题首尾空白(旧数据存在带 \\r\\n 的课程名)', () => {
const out = transformCourse({
课程id: 'a'.repeat(64),
课程名称: '7年级同步课程\r\n',
课程介绍: '夯实数学基础有妙招!',
});
expect(out.title).toBe('7年级同步课程');
});
});
describe('rewriteDomain', () => {
it('把旧域名换成新静态域名(http 与 https 都覆盖)', () => {
const html = '<img src="https://api.yeomai.site/Images/a.png"/><img src="http://api.yeomai.site/b.png"/>';
expect(rewriteDomain(html, 'https://data.example.com')).toBe(
'<img src="https://data.example.com/Images/a.png"/><img src="https://data.example.com/b.png"/>',
);
});
it('新域名结尾的斜杠不会产生双斜杠', () => {
expect(rewriteDomain('https://api.yeomai.site/x', 'https://data.example.com/')).toBe(
'https://data.example.com/x',
);
});
it('null 原样返回', () => {
expect(rewriteDomain(null, 'https://data.example.com')).toBeNull();
});
});
describe('transformSetting', () => {
it('按映射表转换 key', () => {
expect(transformSetting({ ID: 2, value: '付费课程', 注释: '标题' })).toEqual({
key: 'home.title.paid',
value: '付费课程',
remark: '标题',
});
});
it('无意义占位(ID 0)被丢弃', () => {
expect(transformSetting({ ID: 0, value: '占空', 注释: '占空' })).toBeNull();
});
it('映射表覆盖 cloudconfig 的全部有效 ID', () => {
expect(Object.keys(SETTINGS_KEY_MAP).map(Number).sort((a, b) => a - b)).toEqual([
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
]);
});
});
// ── 以下用真实旧库 dump 做端到端校验(迁移逻辑的落点检查)──────────────
describe('buildDataset(真实旧库数据)', () => {
const ds = buildDataset(loadLegacyDump(DEFAULT_DUMP_PATH), 'https://data.example.com');
it('行数与旧库一致', () => {
expect(ds.users).toHaveLength(189);
expect(ds.courses).toHaveLength(10);
expect(ds.lessons).toHaveLength(841);
expect(ds.entitlements).toHaveLength(48);
expect(ds.homeCourses).toHaveLength(5);
expect(ds.freeLessons).toHaveLength(12);
expect(ds.homeFreeCourses).toHaveLength(3);
expect(ds.banners).toHaveLength(3);
expect(ds.articles).toHaveLength(2);
expect(ds.courseTips).toHaveLength(2);
expect(ds.settings).toHaveLength(12); // cloudconfig 13 行,ID 0 占位被丢弃
});
it('没有任何数据被静默丢弃', () => {
expect(ds.diagnostics.skippedLessonOrphans).toEqual([]);
expect(ds.diagnostics.duplicateEntitlements).toEqual([]);
expect(ds.diagnostics.unmappedHomeCourses).toEqual([]);
expect(ds.diagnostics.droppedEntitlements).toEqual([]);
});
it('过滤掉旧库混入的攻击载荷,只保留真实用户', () => {
// 旧接口对 openid 无校验,用户表被写入了 95 条安全扫描探针(SQL 注入、Log4Shell 等)
expect(ds.diagnostics.skippedUsers).toHaveLength(95);
expect(ds.users).toHaveLength(189);
for (const u of ds.users) {
expect(u.openid).toMatch(/^[A-Za-z0-9_-]{20,64}$/);
}
});
it('过滤不影响真实用户:购买记录一条不少且都能对应到用户', () => {
expect(ds.entitlements).toHaveLength(48);
const userIds = new Set(ds.users.map((_, i) => i + 1));
for (const e of ds.entitlements) {
expect(userIds.has(e.user_id)).toBe(true);
}
});
it('课程 code 是 64 位十六进制且无重复(视频路径依赖它)', () => {
const codes = ds.courses.map((c) => c.code);
expect(new Set(codes).size).toBe(codes.length);
for (const code of codes) expect(code).toMatch(/^[0-9a-f]{64}$/);
});
it('课程标题已清理首尾空白', () => {
for (const c of ds.courses) expect(c.title).toBe(c.title.trim());
expect(ds.courses.map((c) => c.title)).toContain('7年级同步课程');
});
it('权益无重复(满足 entitlements UNIQUE(user_id, course_id))', () => {
const keys = ds.entitlements.map((e) => `${e.user_id}::${e.course_id}`);
expect(new Set(keys).size).toBe(keys.length);
for (const e of ds.entitlements) expect(e.source).toBe(SOURCE_LEGACY);
});
it('首页 5 条长图全部关联到可购买课程', () => {
expect(Object.keys(HOME_COURSE_TITLE_MAP)).toHaveLength(5);
for (const h of ds.homeCourses) expect(h.course_id).not.toBeNull();
});
it('内容字段中的旧域名已全部改写', () => {
const all = [...ds.articles, ...ds.homeCourses].map((r) => r.content ?? '').join('');
expect(all).not.toContain('api.yeomai.site');
expect(all).toContain('https://data.example.com');
});
});