vocabulary-import.test.mjs 4.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
  1. import { test } from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import { readFileSync } from 'node:fs';
  4. import vm from 'node:vm';
  5. const source = readFileSync(new URL('../deploy-admin-functions.mjs', import.meta.url), 'utf8');
  6. const functions = source.slice(source.indexOf('function importCell('), source.indexOf('async function commitVocabularyImport('));
  7. function importer(nodes = [{ nodeId: 42, nodeName: '第一单元', bookName: '真实教材', stageName: '小学' }], existing = null) {
  8. const queries = [];
  9. class Batch { constructor() { this.id = 'batch'; this.createdAt = new Date(); } set() {} async save() {} }
  10. const context = vm.createContext({
  11. Parse: { Object: Batch }, Psql: { query: async () => nodes, oneOrNone: async (sql, args) => { queries.push({ sql, args }); return existing; } },
  12. text: value => String(value ?? '').trim(), number: value => Number(value) || 0,
  13. requireRole() {}, reasonOf: () => 'test', companyIdOf: () => 'tenant', companyPointer: () => ({}),
  14. fail: (_status, message) => { throw new Error(message); }, randomObjectId: () => 'random', audit: async () => {},
  15. dateIso: value => value.toISOString(), legacyVocabularyMedia: value => String(value ?? '').replace('https://a018.2018.z01.com/UploadFiles/', ''),
  16. });
  17. vm.runInContext(functions, context);
  18. return { queries, run: async (rows, targetNodeId) => JSON.parse(JSON.stringify(await context.previewVocabularyImport({ current: { id: 'operator' } }, {}, { rows, ...(targetNodeId === undefined ? {} : { targetNodeId }) }))) };
  19. }
  20. test('old template headers preserve GeneralID and exported audio URLs', async () => {
  21. const { run, queries } = importer(undefined, { objectId: 'word', title: 'time', nodeId: 42, sy: '时间', yp: 'time.mp3' });
  22. const result = await run([{ ' GeneralID(可选) ': '123', 单元ID: '42', 单词: 'time', 释义: '时间', 音频URL: 'https://a018.2018.z01.com/UploadFiles/time.mp3' }]);
  23. assert.equal(result.summary.unchanged, 1);
  24. assert.equal(queries[0].args[1], '123');
  25. assert.match(queries[0].sql, /generalId/);
  26. });
  27. test('old sample paths fail with actionable guidance, explicit chosen target resolves them', async () => {
  28. const { run } = importer(), rows = [{ 学段: '小学', 教材: '示例教材', 单元: '第一单元', 单词: 'time' }];
  29. const invalid = await run(rows);
  30. assert.equal(invalid.summary.error, 1); assert.match(invalid.items[0].errors[0], /示例教材.*单元ID.*页面选择/);
  31. const valid = await run(rows, 42);
  32. assert.equal(valid.summary.create, 1); assert.equal(valid.items[0].nodeId, 42); assert.equal(valid.summary.missingMedia, 1);
  33. });
  34. test('explicit invalid, recycled or cross-tenant IDs never fall back to matching names', async () => {
  35. const { run } = importer();
  36. const result = await run([{ 单元ID: '999', 学段: '小学', 教材: '真实教材', 单元: '第一单元', 单词: 'time' }]);
  37. assert.equal(result.summary.error, 1); assert.match(result.items[0].errors[0], /单元ID不存在/);
  38. await assert.rejects(run([{ 单词: 'time' }], 999), /有效的目标/);
  39. });
  40. test('duplicate names require ID rather than choosing the last matching directory', async () => {
  41. const { run } = importer([42, 43].map(nodeId => ({ nodeId, nodeName: '第一单元', bookName: '真实教材', stageName: '小学' })));
  42. assert.match((await run([{ 学段: '小学', 教材: '真实教材', 单元: '第一单元', 单词: 'time' }])).items[0].errors[0], /不唯一/);
  43. });
  44. test('duplicate rows identify the original Excel row; different units remain separate', async () => {
  45. const { run } = importer([42, 43].map(nodeId => ({ nodeId })));
  46. const result = await run([{ 单元ID: 42, 单词: 'time' }, { 单元ID: 42, 单词: 'TIME' }, { 单元ID: 43, 单词: 'time' }]);
  47. assert.equal(result.summary.duplicate, 1); assert.equal(result.summary.create, 2);
  48. assert.match(result.items[1].errors[0], /第 2 行/);
  49. });
  50. test('unknown GeneralID blocks import rather than creating an unexpected word', async () => {
  51. const result = await importer().run([{ 'GeneralID(可选)': 999, 单元ID: 42, 单词: 'time' }]);
  52. assert.equal(result.summary.error, 1); assert.match(result.items[0].errors[0], /GeneralID.*不存在/);
  53. });