156 lines
5.8 KiB
TypeScript
156 lines
5.8 KiB
TypeScript
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');
|
||
});
|
||
});
|