member-profile-save.test.mjs 3.7 KB

123456789101112
  1. import {test} from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import vm from 'node:vm';import {PGlite} from '@electric-sql/pglite';
  2. const source=fs.readFileSync(new URL('../deploy-admin-functions.mjs',import.meta.url),'utf8');const extract=name=>{const a=source.indexOf('function '+name+'('),start=source.slice(a-6,a)==='async '?a-6:a;assert.ok(a>=0);return source.slice(start,source.indexOf('\n',a));};
  3. async function fixture(run){const db=new PGlite();try{await db.exec(`CREATE TABLE "_User"("objectId" text PRIMARY KEY,company text,mobile text,phone text,email text,"realName" text,nickname text,gender text,grade text,"registeredAt" timestamptz,"legacyUserData" jsonb);CREATE UNIQUE INDEX "_User_unique_email" ON "_User"(email);INSERT INTO "_User" VALUES('member','co','13800000000',NULL,NULL,'本地学员','本地学员',NULL,NULL,now(),'{"UserPoint":12,"SilverCoin":12,"Purse":0,"UserExp":0}');INSERT INTO "_User"("objectId",company,email) VALUES('blank-owner','co','');`);
  4. const before=(await db.query('SELECT * FROM "_User" WHERE "objectId"=\'member\'')).rows[0],values={...before},audit=[];let calls=0;
  5. const target={id:'member',updatedAt:new Date('2026-11-15T00:00:00Z'),get:k=>k==='company'?{id:'co'}:values[k],set(k,v){values[k]=v;},unset(k){delete values[k];},toJSON:()=>({...values}),async save(){calls++;await db.query('UPDATE "_User" SET email=$1,gender=$2,grade=$3,"realName"=$4,nickname=$5,mobile=$6,"legacyUserData"=$7 WHERE "objectId"=\'member\'',[values.email??null,values.gender,values.grade,values.realName,values.nickname,values.mobile,JSON.stringify(values.legacyUserData)]);}};
  6. const ctx=vm.createContext({Date,text:v=>String(v??'').trim(),number:(v,d=0)=>Number.isFinite(Number(v))?Number(v):d,fail:(status,message)=>{const e=Error(message);e.status=status;throw e;},requireRole(){},reasonOf:()=> '完善',memberTarget:async()=>target,assertVersion(){},displayName:u=>u.realName,mobileOf:u=>u.mobile,maskMobile:()=> '138****0000',userByLegacyId:async()=>({}),pointerId:p=>p?.id,dateIso:d=>d.toISOString(),audit:async(...a)=>audit.push(a),Psql:{oneOrNone:async(s,p)=>(await db.query(s,p)).rows[0]}});
  7. vm.runInContext(extract('memberProfilePayload')+'\n'+extract('updateMemberProfile'),ctx);
  8. const save=email=>ctx.updateMemberProfile({company:{id:'co'}},{targetId:'member',reason:'完善'},{displayName:'本地学员',nickname:'本地学员',mobile:'13800000000',email,gender:'女',grade:'赣州初二',parentUserId:73});await run({db,before,save,audit,get calls(){return calls;}});
  9. }finally{await db.close();}}
  10. test('blank optional email does not collide with another blank email while saving gender/grade and retaining all credit values',()=>fixture(async f=>{await f.save('');const after=(await f.db.query('SELECT * FROM "_User" WHERE "objectId"=\'member\'')).rows[0];assert.equal(after.email,null);assert.equal(after.gender,'女');assert.equal(after.grade,'赣州初二');for(const key of ['UserPoint','SilverCoin','Purse','UserExp'])assert.equal(after.legacyUserData[key],f.before.legacyUserData[key]);assert.equal(f.audit.length,1);}));
  11. test('non-empty email is saved and explicitly clearing it removes the value rather than storing an empty unique key',()=>fixture(async f=>{await f.save('local@example.com');assert.equal((await f.db.query('SELECT email FROM "_User" WHERE "objectId"=\'member\'')).rows[0].email,'local@example.com');await f.save('');assert.equal((await f.db.query('SELECT email FROM "_User" WHERE "objectId"=\'member\'')).rows[0].email,null);}));
  12. test('invalid email fails before saving or auditing',()=>fixture(async f=>{await assert.rejects(f.save('bad-address'),/邮箱格式不正确/);assert.equal(f.calls,0);assert.equal(f.audit.length,0);}));