feat: add account deletion, announcements and subscription renewal

Support personal records and persistent smoking visibility. Add upload quota preflight, file cleanup, privacy entry, image caching and revision-based page refresh across the backend, admin and mini program.
This commit is contained in:
2026-11-03 22:06:47 +08:00
parent 4c9708b562
commit 8be6d9b1ab
98 changed files with 1679 additions and 372 deletions
+40
View File
@@ -0,0 +1,40 @@
const test=require('node:test');const assert=require('node:assert/strict');const path=require('node:path');const loader=require('./load-module.cjs');
function page(name,api,wxExtra={},app={}){let definition;const storage=new Map([['userId','user']]);const dialogs=[],toasts=[];const wx={getStorageSync:k=>storage.get(k),setStorageSync:(k,v)=>storage.set(k,v),removeStorageSync:k=>storage.delete(k),showModal:o=>dialogs.push(o),showToast:o=>toasts.push(o),showLoading(){},hideLoading(){},reLaunch(){},...wxExtra};loader({Page:p=>definition=p,wx,getApp:()=>app,console,Date,Promise,setTimeout,clearTimeout},{'../../utils/request':{api,getImageUrl:v=>v},'../../utils/page-cache':{withDataCache:p=>p},'../../utils/layout':{getLayout:()=>({})}})(path.join(__dirname,'../pages',name));return {p:{...definition,data:JSON.parse(JSON.stringify(definition.data)),setData(v){Object.assign(this.data,v);}},storage,dialogs,toasts};}
const flush=async()=>{for(let i=0;i<30;i++)await Promise.resolve();};
test('smoking preference is restored from server on a newly created page, even without local storage',async()=>{
let visible=0;const api={checkAuth:async()=>({user:{smoking_card_visible:visible}}),smokingCardVisible:async d=>{visible=d.visible;return {card_visible:visible};}};
const first=page('punch/punch.js',api);await first.p.loadSmokingPreference();assert.equal(first.p.data.smokingVisible,false);await first.p.toggleSmoking({detail:{value:true}});assert.equal(visible,1);
const installed=page('punch/punch.js',api);await installed.p.loadSmokingPreference();assert.equal(installed.p.data.smokingVisible,true);assert.equal(installed.storage.has('smokingVisible'),false);
});
test('failed smoking preference save retains the last server state',async()=>{
const e=page('punch/punch.js',{checkAuth:async()=>({user:{smoking_card_visible:0}}),smokingCardVisible:async()=>{throw {error:'offline'};}});await e.p.loadSmokingPreference();await e.p.toggleSmoking({detail:{value:true}});assert.equal(e.p.data.smokingVisible,false);assert.equal(e.p.data.smokingSaving,false);
});
test('announcement is dismissible per revision and server disable immediately hides it',async()=>{
let data={enabled:true,title:'公告',content:'Hello',revision:'v1'};const e=page('index/index.js',{announcement:async()=>data});await e.p.loadAnnouncement();assert.equal(e.p.data.showAnnouncement,true);e.p.closeAnnouncement();await e.p.loadAnnouncement();assert.equal(e.p.data.showAnnouncement,false);data={...data,content:'Updated',revision:'v2'};await e.p.loadAnnouncement();assert.equal(e.p.data.showAnnouncement,true);data={...data,enabled:false};await e.p.loadAnnouncement();assert.equal(e.p.data.showAnnouncement,false);
});
test('account deletion does not submit when either confirmation is cancelled',async()=>{
for(const cancelAt of [0,1]){let deleted=0;const e=page('settings/settings.js',{accountDeletionPreview:async()=>({summary:{counts:{files:2,photos:1}},confirmation_token:'proof'}),accountDelete:async()=>{deleted++;}});const pending=e.p.deleteAccount();await flush();assert.equal(e.dialogs.length,1);e.dialogs[0].success({confirm:cancelAt!==0});await flush();if(cancelAt===1)e.dialogs[1].success({confirm:false});await pending;assert.equal(deleted,0);assert.equal(e.p._deletingAccount,false);}
});
test('account deletion submits the server proof only after both confirmations and clears the session',async()=>{
let payload,cleared=false;const e=page('settings/settings.js',{accountDeletionPreview:async()=>({summary:{counts:{files:2,photos:1}},confirmation_token:'proof'}),accountDelete:async d=>{payload=d;return {success:true};}},{},{finishAccountDeletion(){cleared=true;}});const pending=e.p.deleteAccount();await flush();e.dialogs[0].success({confirm:true});await flush();assert.equal(payload,undefined);e.dialogs[1].success({confirm:true});await pending;assert.equal(payload.confirmed,true);assert.equal(payload.confirmation_token,'proof');assert.equal(cleared,true);
});
test('announcement login-once mode resets only with a new runtime session and isolates accounts',()=>{
const create=()=>loader({}) (path.join(__dirname,'../utils/announcement-session.js'));
const session=create(),notice={enabled:true,mode:'login_once',content:'Hello',revision:'v1'};
assert.equal(session.shouldShow(notice,'a'),true);session.markShown(notice,'a');assert.equal(session.shouldShow(notice,'a'),false);assert.equal(session.shouldShow(notice,'b'),true);assert.equal(create().shouldShow(notice,'a'),true);
session.clear();assert.equal(session.shouldShow(notice,'a'),true);
});
test('always mode returns on each homepage entry while off and blank content never display',async()=>{
let data={enabled:true,mode:'always',content:'Hello',revision:'v1'};const e=page('index/index.js',{announcement:async()=>data});
await e.p.loadAnnouncement();assert.equal(e.p.data.showAnnouncement,true);e.p.closeAnnouncement();await e.p.loadAnnouncement();assert.equal(e.p.data.showAnnouncement,true);
data={...data,mode:'off'};await e.p.loadAnnouncement();assert.equal(e.p.data.showAnnouncement,false);
data={...data,mode:'always',content:' \n '};await e.p.loadAnnouncement();assert.equal(e.p.data.showAnnouncement,false);
});
test('concurrent homepage notice reads share a request and do not dismiss a fresh popup',async()=>{
let done,calls=0;const e=page('index/index.js',{announcement:()=>{calls++;return new Promise(resolve=>done=resolve);}});const a=e.p.loadAnnouncement(),b=e.p.loadAnnouncement();assert.equal(calls,1);done({mode:'login_once',enabled:true,content:'hello',revision:'v1'});await Promise.all([a,b]);assert.equal(e.p.data.showAnnouncement,true);
});
test('a hidden homepage cannot consume a once-per-login notification',async()=>{
let done;const e=page('index/index.js',{announcement:()=>new Promise(resolve=>done=resolve)});const a=e.p.loadAnnouncement();e.p.onHide();done({mode:'login_once',enabled:true,content:'hello',revision:'v1'});await a;assert.equal(e.p.data.showAnnouncement,false);e.p._announcementActive=true;const b=e.p.loadAnnouncement();done({mode:'login_once',enabled:true,content:'hello',revision:'v1'});await b;assert.equal(e.p.data.showAnnouncement,true);
});