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(284); 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([]); }); 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'); }); });