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:
@@ -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);
|
||||
});
|
||||
@@ -47,11 +47,11 @@ test('blank edit address uses current user location; a supplied address wins',as
|
||||
p.openPhotoEditor(event({batch:0}));p.onEditPhotoInput(event({field:'location'},' 手填地址 '));await p.savePhotoInfo();assert.equal(e.calls[1].location,'手填地址');assert.equal(e.calls[1].lat,0);
|
||||
});
|
||||
|
||||
function uploadSetup(auth) {
|
||||
function uploadSetup(auth, uploadError) {
|
||||
let definition;const calls=[],modals=[],toasts=[];
|
||||
const wx={getStorageSync:()=>'',showToast:v=>toasts.push(v),showModal:v=>modals.push(v),showLoading(){},hideLoading(){},switchTab:v=>calls.push(['switchTab',v]),redirectTo:v=>calls.push(['redirectTo',v]),chooseImage:()=>calls.push(['picker'])};
|
||||
const api={checkAuth:async()=>auth(),storageInfo:async()=>({is_unlimited:true}),albumList:async()=>({items:[]}),albumAdd:async()=>{calls.push(['albumAdd']);return {id:'c224bc8c-0848-442d-abf6-350fab34d669'};},photoAdd:async data=>{calls.push(['photoAdd',data]);return {success:true};}};
|
||||
const load=createLoader({wx,console,Date,Promise,Page:p=>definition=p},{'../../utils/request':{api,getImageUrl:v=>v,API_BASE:'https://we.sundynix.cn/api',uploadFileCompat:async()=>{calls.push(['upload']);return {file_id:'6487966a-28ac-41d1-97b6-26f279b11fa9'};}},'../../utils/user-location':{currentUserLocation:async()=>({location:'',lat:0,lng:0})}});
|
||||
const load=createLoader({wx,console,Date,Promise,Page:p=>definition=p},{'../../utils/request':{api,getImageUrl:v=>v,API_BASE:'https://we.sundynix.cn/api',uploadFileCompat:async()=>{calls.push(['upload']);if(uploadError)throw uploadError;return {file_id:'6487966a-28ac-41d1-97b6-26f279b11fa9'};}},'../../utils/user-location':{currentUserLocation:async()=>({location:'',lat:0,lng:0})}});
|
||||
load(path.join(__dirname,'../pages/album/album.js'));
|
||||
const page={...definition,data:{...definition.data,albumId:'c224bc8c-0848-442d-abf6-350fab34d669',uploadFilePaths:['photo.jpg'],photoFormList:[{}]},setData(v){Object.assign(this.data,v);},loadData(){calls.push(['reload']);}};
|
||||
return {page,calls,modals,toasts};
|
||||
@@ -69,3 +69,11 @@ test('stale demo album is replaced with a real personal album before transferrin
|
||||
const e=uploadSetup(()=>({logged_in:true,user:{couple_id:null}}));e.page.data.albumId=-1;await e.page.uploadAll();
|
||||
assert.equal(e.calls[0][0],'albumAdd');assert.equal(e.calls[1][0],'upload');assert.equal(e.calls[2][0],'photoAdd');assert.equal(e.calls[2][1].album_id,'c224bc8c-0848-442d-abf6-350fab34d669');
|
||||
});
|
||||
|
||||
test('quota rejection stops an album batch without another upload or a toast covering the upgrade dialog',async()=>{
|
||||
const e=uploadSetup(()=>({logged_in:true}),{code:10001,error:'空间不足'});
|
||||
e.page.data.uploadFilePaths=['one.jpg','two.jpg','three.jpg'];e.page.data.photoFormList=[{},{},{}];
|
||||
await e.page.uploadAll();
|
||||
assert.equal(e.calls.filter(c=>c[0]==='upload').length,1);assert.equal(e.calls.filter(c=>c[0]==='photoAdd').length,0);
|
||||
assert.equal(e.page.data.uploading,false);assert.equal(e.toasts.length,0);assert.equal(e.page.data.uploadFilePaths.length,3);
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ function setup() {
|
||||
let app;
|
||||
const wx = {
|
||||
getStorageSync: key => storage.get(key), setStorageSync: (key,value) => storage.set(key,value),
|
||||
removeStorageSync: key => storage.delete(key), getStorageInfoSync: () => ({keys:[...storage.keys()]}),
|
||||
clearStorageSync:()=>storage.clear(), removeStorageSync: key => storage.delete(key), getStorageInfoSync: () => ({keys:[...storage.keys()]}),
|
||||
login: options => logins.push(options), request: options => requests.push(options),
|
||||
getNetworkType: options => options.success({networkType:'wifi'}), onNetworkStatusChange(){},
|
||||
hideLoading(){}, showToast(){}
|
||||
@@ -19,7 +19,7 @@ function setup() {
|
||||
module:{exports:{}}, require: name => loadModule(path.join(__dirname,'../utils', name + '.js'))};
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../utils/request.js'),'utf8'),ctx);
|
||||
const request = ctx.module.exports;
|
||||
ctx.require = () => request;
|
||||
ctx.require = name => name === './utils/request' ? request : loadModule(path.join(__dirname,'..', name + '.js'));
|
||||
ctx.App = definition => {app=definition;};
|
||||
vm.runInNewContext('(function(){' + fs.readFileSync(path.join(__dirname,'../app.js'),'utf8') + '\n})()',ctx);
|
||||
return {app,wx,storage,logins,requests,api:request.api};
|
||||
@@ -70,3 +70,7 @@ test('parallel 401 responses share token refresh and retry at most once',async()
|
||||
retried[0].success({statusCode:401,data:{error:'still unauthorized'}});retried[1].success({statusCode:200,data:[]});
|
||||
await failed;await two;assert.equal(env.logins.length,2);
|
||||
});
|
||||
|
||||
test('account deletion clears drafts and credentials and prevents automatic account recreation',async()=>{
|
||||
const env=setup();env.storage.set('token','private');env.storage.set('publishDraft','private draft');env.app.finishAccountDeletion();assert.equal(env.storage.has('publishDraft'),false);assert.equal(env.storage.has('token'),false);assert.equal(env.storage.get('accountDeleted'),true);assert.equal(env.app.globalData.authState,'error');await assert.rejects(env.app.refreshAuth('private'));assert.equal(env.logins.length,0);
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ test('identical reads share a request, return independent objects and expire', a
|
||||
a.items[0].title='formatted';assert.equal(b.items[0].title,'old');
|
||||
assert.equal((await e.api.diaryList({page:1,page_size:20})).items[0].title,'old');assert.equal(e.requests.length,1);
|
||||
const different=e.api.diaryList({page:2,page_size:20});await e.flush();assert.equal(e.requests.length,2);e.respond(1,{items:[]});await different;
|
||||
e.advance(45001);const expired=e.api.diaryList({page:1,page_size:20});await e.flush();assert.equal(e.requests.length,3);e.respond(2,{items:[]});await expired;
|
||||
e.advance(e.cache.TTL+1);const expired=e.api.diaryList({page:1,page_size:20});await e.flush();assert.equal(e.requests.length,3);e.respond(2,{items:[]});await expired;
|
||||
});
|
||||
test('successful mutations invalidate related reads; unrelated and failed writes retain cache',async()=>{
|
||||
const e=setup();const diary=e.api.diaryList(),album=e.api.albumList();await e.flush();e.respond(0,{items:[]});e.respond(1,[]);await Promise.all([diary,album]);
|
||||
@@ -50,7 +50,7 @@ test('each upper page observes changes independently and pull refresh bypasses c
|
||||
await Promise.all([a.loadData(),b.loadData()]);assert.equal(loads,2);
|
||||
e.cache.mutation('diaryUpdate',{success:true});pending=Promise.all([a.loadData(),b.loadData()]);await e.flush();assert.equal(e.requests.length,2);e.respond(1,{items:[{id:1}]});await pending;assert.equal(loads,4);
|
||||
pending=a.onPullDownRefresh();await e.flush();assert.equal(e.requests.length,3);e.respond(2,{items:[]});await pending;
|
||||
e.advance(45001);pending=b.loadData();await e.flush();assert.equal(e.requests.length,4);e.respond(3,{items:[]});await pending;
|
||||
e.advance(e.cache.TTL+1);pending=b.loadData();await e.flush();assert.equal(e.requests.length,4);e.respond(3,{items:[]});await pending;
|
||||
});
|
||||
test('permission reads remain live and longer profile TTL does not extend list TTL',async()=>{
|
||||
const e=setup();let p=e.api.periodList();await e.flush();e.respond(0,{cycles:[]});await p;
|
||||
@@ -99,3 +99,19 @@ test('photo metadata updates invalidate album and photo caches',async()=>{
|
||||
pending=e.api.photoUpdate({id:1,taken_at:'',location:'公园',activity:'散步'});await e.flush();e.respond(2,{success:true});await pending;
|
||||
pending=Promise.all([e.api.photos(7),e.api.albumList()]);await e.flush();assert.equal(e.requests.length,5);e.respond(3,[]);e.respond(4,[]);await pending;
|
||||
});
|
||||
|
||||
test('storage reads are reused and upload or confirmed payment invalidates their usage',async()=>{
|
||||
const e=setup();let p=e.api.storageInfo();await e.flush();e.respond(0,{used_bytes:10});await p;
|
||||
await e.api.storageInfo();assert.equal(e.requests.length,1);
|
||||
e.cache.mutation('upload',{success:true});p=e.api.storageInfo();await e.flush();assert.equal(e.requests.length,2);e.respond(1,{used_bytes:20});await p;
|
||||
e.cache.mutation('storageOrderStatus',{status:'paid'});p=e.api.storageInfo();await e.flush();assert.equal(e.requests.length,3);e.respond(2,{quota_bytes:1024});await p;
|
||||
});
|
||||
test('remote versions retain unchanged data and invalidate every page after partner or admin changes',async()=>{
|
||||
const e=setup();e.cache.applyRemoteRevision('initial');let loads=0;
|
||||
const page=e.withDataCache({data:{},async load(){loads++;this.data.items=await e.api.diaryList();}},{load:['diaryList']});
|
||||
let p=page.load();await e.flush();e.respond(0,{items:[]});await p;
|
||||
assert.equal(e.cache.applyRemoteRevision('initial'),false);await page.load();assert.equal(e.requests.length,1);
|
||||
assert.equal(e.cache.applyRemoteRevision('partner-change'),true);p=page.onDataChanged();await e.flush();e.respond(1,{items:[{id:1}]});await p;assert.equal(loads,2);assert.equal(page.data.items.items[0].id,1);
|
||||
const stale=e.api.photos(7);await e.flush();e.cache.applyRemoteRevision('admin-change');e.respond(2,{items:[{id:'old'}]});await stale;
|
||||
p=e.api.photos(7);await e.flush();assert.equal(e.requests.length,4);e.respond(3,{items:[{id:'new'}]});await p;
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ function client(extra = {}) {
|
||||
const calls = [];
|
||||
const load = createLoader({console, Promise, Date, wx: {
|
||||
getStorageSync: () => 'token', hideLoading() {},
|
||||
getFileInfo(o){o.success({size:100,digest:'a'.repeat(32)});},
|
||||
request(o) { calls.push(o); o.success({statusCode:200, data:{success:true}}); }, ...extra
|
||||
}});
|
||||
return {...load(path.join(__dirname, '../utils/request.js')), calls};
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
const test=require('node:test');
|
||||
const assert=require('node:assert/strict');
|
||||
const path=require('node:path');
|
||||
function setup(){
|
||||
const id='K_iQMwE_jre0j6QDVL78DZz7wiR1SKVY5h2YJCK6uPU';
|
||||
const storage={userId:'user',coupleId:'couple',token:'token'},reads=[],prompts=[],saves=[],ui=[];
|
||||
const app={globalData:{authState:'ready'}};
|
||||
const load=require('./load-module.cjs')({console:{warn(){},error(){}},Promise,Date,getApp:()=>app,wx:{
|
||||
getStorageSync:key=>storage[key],setStorageSync:(key,value)=>storage[key]=value,
|
||||
getSetting:o=>reads.push(o),requestSubscribeMessage:o=>prompts.push(o),
|
||||
showToast:o=>ui.push(o),showModal:o=>ui.push(o)
|
||||
}},{'./request':{api:{subscribeTemplate:async(data,options)=>{saves.push({data,options});return {success:true}}}}});
|
||||
const helper=load(path.join(__dirname,'../utils/subscribe.js'));
|
||||
const ready=(settings={mainSwitch:true,itemSettings:{[id]:'accept'}})=>reads.at(-1).success({subscriptionsSetting:settings});
|
||||
return {id,storage,reads,prompts,saves,ui,app,helper,ready};
|
||||
}
|
||||
test('login only reads remembered settings; first real tap renews synchronously once per cold session',async()=>{
|
||||
const e=setup();e.helper.resetLoginRenewal();e.helper.prepareLoginRenewal();
|
||||
assert.equal(e.reads[0].withSubscriptions,true);assert.equal(e.prompts.length,0);assert.equal(e.helper.tryLoginRenewal(),undefined);
|
||||
e.ready();const pending=e.helper.tryLoginRenewal();assert.equal(e.prompts.length,1);
|
||||
assert.equal(e.helper.tryLoginRenewal(),undefined);
|
||||
e.prompts[0].success({[e.id]:'accept'});await pending;
|
||||
assert.equal(e.saves[0].data.template_type,'notify');assert.equal(e.saves[0].options.silent,true);assert.equal(e.ui.length,0);
|
||||
e.ready();e.helper.prepareLoginRenewal();e.ready();e.helper.tryLoginRenewal();assert.equal(e.prompts.length,1);
|
||||
e.helper.resetLoginRenewal();e.helper.prepareLoginRenewal();e.ready();e.helper.tryLoginRenewal();assert.equal(e.prompts.length,2);
|
||||
});
|
||||
test('unknown permission, not remembered, rejected, banned and disabled main switch never request subscription',()=>{
|
||||
for(const settings of [{},{mainSwitch:true},{mainSwitch:true,itemSettings:{}},{mainSwitch:true,itemSettings:{bad:'accept'}},{mainSwitch:true,itemSettings:{K_iQMwE_jre0j6QDVL78DZz7wiR1SKVY5h2YJCK6uPU:'reject'}},{mainSwitch:false,itemSettings:{K_iQMwE_jre0j6QDVL78DZz7wiR1SKVY5h2YJCK6uPU:'accept'}}]){
|
||||
const e=setup();e.helper.prepareLoginRenewal();e.ready(settings);e.helper.tryLoginRenewal();assert.equal(e.prompts.length,0);assert.equal(e.ui.length,0);
|
||||
}
|
||||
});
|
||||
test('unbound, locally disabled or unauthenticated users never auto-renew',()=>{
|
||||
for(const change of [e=>e.storage.coupleId='',e=>e.storage.subscribed_notify=false,e=>e.app.globalData.authState='loading']){
|
||||
const e=setup();e.helper.prepareLoginRenewal();e.ready();change(e);e.helper.tryLoginRenewal();assert.equal(e.prompts.length,0);
|
||||
}
|
||||
});
|
||||
test('settings failures and stale callbacks fail closed',()=>{
|
||||
const e=setup();e.helper.prepareLoginRenewal();e.reads[0].fail();e.helper.tryLoginRenewal();assert.equal(e.prompts.length,0);
|
||||
e.helper.prepareLoginRenewal();const stale=e.reads.at(-1);e.helper.resetLoginRenewal();stale.success({subscriptionsSetting:{mainSwitch:true,itemSettings:{[e.id]:'accept'}}});e.helper.tryLoginRenewal();assert.equal(e.prompts.length,0);
|
||||
e.helper.prepareLoginRenewal();e.ready();e.helper.prepareLoginRenewal();e.helper.tryLoginRenewal();assert.equal(e.prompts.length,0);
|
||||
});
|
||||
test('renewal failures remain silent and do not retry repeatedly during the same session',async()=>{
|
||||
const e=setup();e.helper.prepareLoginRenewal();e.ready();const pending=e.helper.tryLoginRenewal();e.prompts[0].fail({errCode:20001});await pending;
|
||||
assert.equal(e.ui.length,0);e.ready();e.helper.tryLoginRenewal();assert.equal(e.prompts.length,1);
|
||||
});
|
||||
@@ -25,7 +25,7 @@ test('native custom tabs retain the three main pages; pages never mount a duplic
|
||||
test('component selects the main page, hides on child routes and avoids reloading its current tab',()=>{
|
||||
let definition,route='pages/profile/profile',pageData={};const navigations=[];
|
||||
vm.runInNewContext(fs.readFileSync(path.join(root,'custom-tab-bar/index.js'),'utf8'),{
|
||||
Component:c=>definition=c,require:()=>({tabForPage,paths}),getCurrentPages:()=>[{route,data:pageData}],wx:{switchTab:o=>{navigations.push(o.url);o.complete();},reLaunch(){assert.fail('normal tab switching must retain pages');}}
|
||||
Component:c=>definition=c,require:name=>name.includes('subscribe')?{tryLoginRenewal(){}}:({tabForPage,paths}),getCurrentPages:()=>[{route,data:pageData}],wx:{switchTab:o=>{navigations.push(o.url);o.complete();},reLaunch(){assert.fail('normal tab switching must retain pages');}}
|
||||
});
|
||||
const component={...definition.methods,data:{...definition.data},setData(v){Object.assign(this.data,v);}};
|
||||
definition.lifetimes.attached.call(component);assert.equal(component.data.active,'profile');
|
||||
|
||||
@@ -19,7 +19,7 @@ test('GET business parameters and POST bodies use clean paths with auth', async
|
||||
});
|
||||
test('multipart upload retains action and token when Android omits headers',async()=>{
|
||||
let upload;
|
||||
const {uploadFileCompat}=load('utils/request.js',{getStorageSync:()=> 'test-token',compressImage(o){o.fail();},uploadFile(o){upload=o;o.success({statusCode:200,data:'{"success":true,"path":"uploads/test.jpg"}'});} });
|
||||
const {uploadFileCompat}=load('utils/request.js',{getStorageSync:()=> 'test-token',getFileInfo(o){o.success({size:100,digest:'a'.repeat(32)});},request(o){o.success({statusCode:200,data:{success:true}});},compressImage(o){o.fail();},uploadFile(o){upload=o;o.success({statusCode:200,data:'{"success":true,"path":"uploads/test.jpg"}'});} });
|
||||
await uploadFileCompat({url:'https://we.sundynix.cn/api/uploadPartnerAvatar',filePath:'test.jpg',name:'file',formData:{partner_id:87}});
|
||||
assert.equal(upload.url,'https://we.sundynix.cn/api/uploadPartnerAvatar?token=test-token');
|
||||
assert.equal(upload.formData.action,'uploadPartnerAvatar');
|
||||
@@ -59,3 +59,27 @@ test('unbound newcomer loads homepage records without requesting couple-only pet
|
||||
assert.equal(page.data.homeError,false);assert.equal(page.data.statsError,false);assert.equal(page.data.statsLoading,false);
|
||||
assert.equal(page.data.recentDiary.length,0);assert.equal(page.data.stats.pet,0);
|
||||
});
|
||||
|
||||
test('smoking visibility page uses the real request adapter and restores the persisted response',async()=>{
|
||||
let definition,visible=1;const calls=[];
|
||||
const wx={getStorageSync:()=> 'test-token',showToast(){},request(o){
|
||||
calls.push(o);
|
||||
if(o.url.endsWith('/checkAuth'))o.success({statusCode:200,data:{user:{smokingCardVisible:visible}}});
|
||||
else if(o.url.endsWith('/smokingCardVisible')){visible=o.data.visible;o.success({statusCode:200,data:{success:true,cardVisible:visible}});}
|
||||
else throw new Error('Unexpected request: '+o.url);
|
||||
}};
|
||||
require('./load-module.cjs')({wx,console,Date,Promise,Page:p=>definition=p},{'../../utils/page-cache':{withDataCache:p=>p}})(path.join(__dirname,'../pages/punch/punch.js'));
|
||||
const makePage=()=>({...definition,data:JSON.parse(JSON.stringify(definition.data)),setData(p){Object.assign(this.data,p);}});
|
||||
const page=makePage();await page.loadSmokingPreference();assert.equal(page.data.smokingVisible,true);
|
||||
await page.toggleSmoking({detail:{value:false}});assert.equal(visible,0);assert.equal(page.data.smokingVisible,false);
|
||||
const save=calls.find(c=>c.url.endsWith('/smokingCardVisible'));assert.equal(save.method,'POST');assert.equal(save.data.visible,0);assert.equal(save.header.Authorization,'Bearer test-token');
|
||||
const reopened=makePage();await reopened.loadSmokingPreference();assert.equal(reopened.data.smokingVisible,false);
|
||||
await reopened.toggleSmoking({detail:{value:true}});assert.equal(visible,1);assert.equal(reopened.data.smokingVisible,true);
|
||||
});
|
||||
|
||||
test('silent subscription persistence never hides a business spinner or shows a network toast',async()=>{
|
||||
const ui=[];
|
||||
const {api}=load('utils/request.js',{getStorageSync:()=> 'token',hideLoading:()=>ui.push('hide'),showToast:()=>ui.push('toast'),request:o=>o.fail({errMsg:'offline'})});
|
||||
await assert.rejects(api.subscribeTemplate({template_type:'notify',template_id:'template'},{silent:true}));
|
||||
assert.equal(ui.length,0);
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ test('finite paid space uses its quota, zero is respected, unlimited is explicit
|
||||
test('storage page renders 1-cent plans and custom month/capacity values',async()=>{
|
||||
let definition;
|
||||
const info={freeBytes:200*1024*1024,quotaBytes:1024*1024*1024,usedBytes:512*1024*1024,isVip:true,isUnlimited:false,vipExpireAt:'2026-12-02',plans:[{key:'custom',name:'测试套餐',priceCents:1,quotaMb:1024,months:3}]};
|
||||
const load=loader({Page(p){definition=p},wx:{},console},{'../../utils/request':{api:{storageInfo:async()=>info}}});
|
||||
const load=loader({Page(p){definition=p},wx:{getStorageSync:()=> 'user'},console},{'../../utils/request':{api:{storageInfo:async()=>info}}});
|
||||
load(path.join(__dirname,'../pages/storage/storage.js'));
|
||||
const page={...definition,data:{...definition.data},setData(d){Object.assign(this.data,d)}};
|
||||
await page.loadInfo();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const test=require('node:test');
|
||||
const assert=require('node:assert/strict');
|
||||
const path=require('node:path');
|
||||
const template='tOaqM_Z6pNSih2pVpaUmP86MgwnDT5ZlwOqd0lgmXP8';
|
||||
const template='K_iQMwE_jre0j6QDVL78DZz7wiR1SKVY5h2YJCK6uPU';
|
||||
function setup(save){
|
||||
const storage={coupleId:10},calls=[];
|
||||
const wx={getStorageSync:key=>storage[key],setStorageSync:(key,v)=>storage[key]=v,requestSubscribeMessage:o=>calls.push(o)};
|
||||
@@ -24,18 +24,6 @@ test('failed persistence never reports successful subscription and a later tap c
|
||||
fail=false;pending=e.helper.requestSubscribe('notify');assert.equal(e.calls.length,2);e.calls[1].success({[template]:'accept'});
|
||||
assert.equal((await pending).notify,true);
|
||||
});
|
||||
test('each subsequent user action can replenish authorization; denial does not prevent later attempts',async()=>{
|
||||
const e=setup(async()=>({success:true}));
|
||||
let pending=e.helper.requestSubscribeAfterAction('diary');assert.equal(e.calls.length,1);e.calls[0].success({[template]:'reject'});await pending;
|
||||
pending=e.helper.requestSubscribeAfterAction('photo');assert.equal(e.calls.length,2);e.calls[1].success({[template]:'accept'});await pending;
|
||||
pending=e.helper.requestSubscribeAfterAction('checkin');assert.equal(e.calls.length,3);e.calls[2].fail(Error('cancelled'));await pending;
|
||||
pending=e.helper.requestSubscribeAfterAction('checkin');assert.equal(e.calls.length,4);e.calls[3].success({[template]:'accept'});await pending;
|
||||
});
|
||||
test('explicitly disabled notifications and unbound users do not prompt during business actions',async()=>{
|
||||
const e=setup(async()=>({success:true}));e.storage.subscribed_notify=false;await e.helper.requestSubscribeAfterAction('diary');assert.equal(e.calls.length,0);
|
||||
delete e.storage.subscribed_notify;delete e.storage.coupleId;await e.helper.requestSubscribeAfterAction('diary');assert.equal(e.calls.length,0);
|
||||
});
|
||||
|
||||
test('manual renewal requests authorization again without disabling existing notifications',async()=>{
|
||||
let page, calls=0, cancelled=0, complete;
|
||||
const loader=require('./load-module.cjs')({Page:p=>page=p,wx:{showToast(){}}},{
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
const test=require('node:test');
|
||||
const assert=require('node:assert/strict');
|
||||
const path=require('node:path');
|
||||
function setup({preflightQuota=false,uploadQuota=false,networkRetry=false,metadataFailure=false}={}){
|
||||
const checks=[],uploads=[],modals=[],navigation=[];
|
||||
const load=require('./load-module.cjs')({console,Date,Promise,wx:{
|
||||
getStorageSync:()=> 'token',hideLoading(){},showLoading(){},showToast(){},
|
||||
compressImage(o){o.success({tempFilePath:'compressed.jpg'});},
|
||||
getFileInfo(o){if(metadataFailure)return o.fail();o.success({size:o.filePath==='compressed.jpg'?100:200,digest:'a'.repeat(32)});},
|
||||
request(o){checks.push(o);o.success({statusCode:preflightQuota?403:200,data:preflightQuota?{error:'空间不足',code:10001}:{success:true}});},
|
||||
uploadFile(o){uploads.push(o);if(networkRetry&&uploads.length===1)return o.fail({errMsg:'network'});o.success({statusCode:uploadQuota?403:200,data:JSON.stringify(uploadQuota?{error:'空间不足',code:10001}:{success:true,fileId:'file-id'})});},
|
||||
showModal(o){modals.push(o);},navigateTo(o){navigation.push(o.url);}
|
||||
}});
|
||||
const client=load(path.join(__dirname,'../utils/request.js'));
|
||||
return {...client,checks,uploads,modals,navigation};
|
||||
}
|
||||
const options={url:'https://we.sundynix.cn/api/upload',filePath:'original.jpg'};
|
||||
test('preflight checks the compressed bytes before any upload',async()=>{
|
||||
const c=setup();await c.uploadFileCompat(options);
|
||||
assert.equal(c.checks[0].url,'https://we.sundynix.cn/api/uploadPreflight');
|
||||
assert.equal(c.checks[0].data.size,100);assert.equal(c.checks[0].data.md5,'a'.repeat(32));
|
||||
assert.equal(c.uploads[0].filePath,'compressed.jpg');
|
||||
});
|
||||
test('preflight quota rejection blocks bytes, prompts once and offers purchase',async()=>{
|
||||
const c=setup({preflightQuota:true});
|
||||
await assert.rejects(c.uploadFileCompat(options),e=>e.code===10001);
|
||||
assert.equal(c.uploads.length,0);assert.equal(c.checks.length,1);assert.equal(c.modals.length,1);
|
||||
assert.equal(c.modals[0].title,'存储空间不足');c.modals[0].success({confirm:true});
|
||||
assert.deepEqual(c.navigation,['/pages/storage/storage']);
|
||||
});
|
||||
test('authoritative backend quota rejection after a successful preflight also prompts',async()=>{
|
||||
const c=setup({uploadQuota:true});await assert.rejects(c.uploadFileCompat(options),e=>e.code===10001);
|
||||
assert.equal(c.uploads.length,1);assert.equal(c.modals.length,1);
|
||||
c.modals[0].success({confirm:false});assert.equal(c.navigation.length,0);
|
||||
});
|
||||
test('avatar, partner avatar and album uploads all use quota preflight and show one modal on rejection',async()=>{
|
||||
for(const run of [c=>c.api.uploadAvatar('original.jpg'),c=>c.api.uploadPartnerAvatar('original.jpg','partner'),c=>c.api.photoUpload('original.jpg','album')]){
|
||||
const c=setup({uploadQuota:true});await assert.rejects(run(c),e=>e.code===10001);
|
||||
assert.equal(c.checks.length,1);assert.equal(c.uploads.length,1);assert.equal(c.modals.length,1);
|
||||
}
|
||||
});
|
||||
test('network fallback rechecks the original file size; unreadable files never upload',async()=>{
|
||||
const c=setup({networkRetry:true});await c.uploadFileCompat(options);
|
||||
assert.deepEqual(c.checks.map(o=>o.data.size),[100,200]);assert.equal(c.uploads[1].filePath,'original.jpg');
|
||||
const bad=setup({metadataFailure:true});await assert.rejects(bad.uploadFileCompat(options));assert.equal(bad.uploads.length,0);
|
||||
});
|
||||
Reference in New Issue
Block a user