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 = '
';
expect(rewriteDomain(html, 'https://data.example.com')).toBe(
'
',
);
});
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');
});
});