legacy-enrollment-reconciliation.test.mjs 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. import { test } from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import { readFileSync } from 'node:fs';
  4. import { PGlite } from '@electric-sql/pglite';
  5. import vm from 'node:vm';
  6. const migration = new URL('../sql/legacy-enrollment-reconciliation.sql', import.meta.url);
  7. const sql = readFileSync(migration, 'utf8');
  8. const gateway = readFileSync(new URL('../cloud/member-enrollment.js', import.meta.url), 'utf8');
  9. async function fixture(run) {
  10. const db = new PGlite();
  11. try {
  12. await db.exec(`
  13. CREATE TABLE "_User" (company text,"legacyUserId" numeric,"identityType" text,"isDisabled" boolean,"isDeleted" boolean,"legacyUserData" jsonb);
  14. CREATE TABLE "Node" (company text,"nodeId" numeric,zstatus numeric,"nodeName" text);
  15. CREATE TABLE "CourseTeachingProfile" (company text,"courseId" numeric,"categoryKey" text,confirmed boolean);
  16. CREATE TABLE "CommonModel" (company text,"modelId" numeric,"itemId" numeric,status numeric,"objectId" text,"sourceKey" text);
  17. CREATE TABLE "CourseBinding" (company text,"objectId" text PRIMARY KEY,id numeric,yhid text,kcid text,"categoryKey" text,"categoryName" text,"enrollmentStatus" text,"sourceKey" text,yxx numeric,cksl numeric,syjd text,"updatedAt" timestamptz);
  18. CREATE UNIQUE INDEX enrollment_active_identity ON "CourseBinding" (company,yhid,"categoryKey",kcid) WHERE "enrollmentStatus"='active';
  19. INSERT INTO "_User" VALUES ('co',20,'member',false,false,'{"Purse":10}');
  20. INSERT INTO "Node" VALUES ('co',100,99,'词表');
  21. INSERT INTO "CourseTeachingProfile" VALUES ('co',100,'word',true);
  22. `);
  23. const binding = async (id, status = 'pending', category = 'word', source = `legacy:model:58:general:${id}`) => db.query(
  24. `INSERT INTO "CourseBinding" VALUES ('co',$1,$2,'20','100',$3,'单词课',$4,$5,123,500,'{"101":"101@第一本@1"}',now())`,
  25. [String(id),id,category,status,source]);
  26. const common = async (id, status = 99) => db.query(`INSERT INTO "CommonModel" VALUES ('co',58,$1,$2,$3,$4)`, [id,status,'c'+id,`legacy:model:58:general:${id}`]);
  27. const get = async id => (await db.query('SELECT * FROM "CourseBinding" WHERE "objectId"=$1',[String(id)])).rows[0];
  28. const install = () => db.exec(sql);
  29. await run({db,binding,common,get,install});
  30. } finally { await db.close(); }
  31. }
  32. test('历史待确认开课恢复后进入真实排课查询,保留编号、进度和余额,重复迁移无变化', () => fixture(async ({db,binding,common,get,install}) => {
  33. await binding(1); await common(1); const before = await get(1);
  34. await install();
  35. const after = await get(1);
  36. assert.equal(after.enrollmentStatus, 'active');
  37. for (const key of ['objectId','id','sourceKey','yxx','cksl','syjd']) assert.equal(after[key],before[key]);
  38. const c = { Psql: { query: async (s,p) => (await db.query(s,p)).rows }, companyIdOf: () => 'co', text: x => String(x ?? ''), number: Number, TEACHING_CATEGORY_LABELS: new Map([['word','单词课']]) };
  39. vm.createContext(c); vm.runInContext(gateway,c);
  40. const courses = await c.enrollmentBindings({}, {},20);
  41. assert.equal(courses.length,1); assert.equal(courses[0].bindingId,'1');
  42. await install(); assert.deepEqual(await get(1),after);
  43. assert.deepEqual((await db.query('SELECT "legacyUserData" FROM "_User"')).rows[0].legacyUserData,{Purse:10});
  44. }));
  45. for (const order of ['binding-first','common-first']) test(`未来同步两张表按${order}顺序写入时自动激活`, () => fixture(async ({binding,common,get,install}) => {
  46. await install();
  47. if(order==='binding-first'){await binding(1,null,null);assert.equal((await get(1)).enrollmentStatus,null);await common(1);}
  48. else {await common(1);await binding(1,null,null);}
  49. assert.equal((await get(1)).enrollmentStatus,'active'); assert.equal((await get(1)).categoryKey,'word');
  50. }));
  51. test('停用学生保留原状,重新启用后无需删除重开;恢复删除的学生同样生效', () => fixture(async ({db,binding,common,get,install}) => {
  52. await db.exec('UPDATE "_User" SET "isDisabled"=true,"isDeleted"=true');
  53. await binding(1);await common(1);await install();
  54. assert.equal((await get(1)).enrollmentStatus,'pending');
  55. await db.exec('UPDATE "_User" SET "isDisabled"=false');assert.equal((await get(1)).enrollmentStatus,'pending');
  56. await db.exec('UPDATE "_User" SET "isDeleted"=false');assert.equal((await get(1)).enrollmentStatus,'active');
  57. }));
  58. test('后续补入学生资料、确认映射和恢复内容时补齐开课', () => fixture(async ({db,binding,common,get,install}) => {
  59. await db.exec('DELETE FROM "_User";UPDATE "CourseTeachingProfile" SET confirmed=false;UPDATE "Node" SET zstatus=0');
  60. await binding(1,null,null);await common(1);await install();
  61. await db.exec(`INSERT INTO "_User" VALUES ('co',20,'member',false,false,'{}');UPDATE "CourseTeachingProfile" SET confirmed=true`);
  62. assert.equal((await get(1)).enrollmentStatus,null);
  63. await db.exec('UPDATE "Node" SET zstatus=99');assert.equal((await get(1)).enrollmentStatus,'active');
  64. }));
  65. test('重复历史开课、已存在有效开课、回收记录及新系统记录保持原状', () => fixture(async ({db,binding,common,get,install}) => {
  66. await binding(1);await common(1);await binding(2);await common(2);await install();
  67. assert.equal((await get(1)).enrollmentStatus,'pending');assert.equal((await get(2)).enrollmentStatus,'pending');
  68. await db.exec(`UPDATE "CourseBinding" SET "enrollmentStatus"='active' WHERE "objectId"='2'`);
  69. assert.equal((await get(1)).enrollmentStatus,'pending');
  70. await binding(3,'recycled');await common(3,-2);assert.equal((await get(3)).enrollmentStatus,'recycled');
  71. await db.exec('DELETE FROM "CourseBinding";DELETE FROM "CommonModel"');
  72. await binding(4,'pending','word','enroll:4');await common(4);assert.equal((await get(4)).enrollmentStatus,'pending');
  73. }));
  74. for(const kind of ['unconfirmed','conflicting-profile','explicit-other-category','deleted-common','foreign-tenant']) test(`${kind}不自动授权`, () => fixture(async ({db,binding,common,get,install}) => {
  75. if(kind==='unconfirmed')await db.exec('UPDATE "CourseTeachingProfile" SET confirmed=false');
  76. if(kind==='conflicting-profile')await db.exec(`INSERT INTO "CourseTeachingProfile" VALUES ('co',100,'trial',true)`);
  77. if(kind==='foreign-tenant')await db.exec(`UPDATE "CourseTeachingProfile" SET company='other'`);
  78. await binding(1,'pending',kind==='explicit-other-category'?'trial':null);await common(1,kind==='deleted-common'?-2:99);await install();
  79. assert.equal((await get(1)).enrollmentStatus,'pending');
  80. }));