caterium-app/tests/recovery/smoke.mjs
pavlov346346-source 1e644e6dfb
Fix employee sync and expose mobile profile/logout (#37)
Keep notification-read state personal to user/company, preserve server read-only sections during staff synchronization, and respect separate order-create/edit/delete rights. Add visible mobile header session actions and a sticky logout that survives profile RPC failure; scope asynchronous profile/branding to user and workspace. Targeted browser suites and isolated SQL recovery tests passed in run 35506145405, iPhone screenshots reviewed. Full main QA remains required before production promotion. Workspace branding RPC migration is included but has NOT been applied to production Supabase; older servers retain safe owner-only fallback. No live membership/business-data repair is claimed without identifying the reported employee.
2026-09-20 13:55:56 +03:00

157 lines
16 KiB
JavaScript

import assert from 'node:assert/strict';
export async function run(db){
const q=(s,p=[])=>db.query(s,p);const one=async(s,p)=>(await q(s,p)).rows[0];
const ids=['00000000-0000-4000-8000-000000000001','00000000-0000-4000-8000-000000000002','00000000-0000-4000-8000-000000000003','00000000-0000-4000-8000-000000000004'];
async function actor(id,aal='aal1'){await db.exec('reset role');await q("select set_config('request.jwt.claim.sub',$1,false),set_config('request.jwt.claims',$2,false)",[id,JSON.stringify({sub:id,aal,role:'authenticated'})]);await db.exec('set role authenticated');}
async function denied(s,p,re){await assert.rejects(q(s,p),re);}
await db.exec(`create function realtime.send(jsonb,text,text,boolean) returns void language plpgsql as $$begin end$$;`);
for(let i=0;i<ids.length;i++)await q("insert into auth.users(id,email,email_confirmed_at) values($1,$2,now())",[ids[i],`test${i}@example.invalid`]);
await q('insert into public.sun_platform_admins(user_id) values($1)',[ids[0]]);
await q('insert into public.caterium_sidebar_brand_assignment(user_id) values($1)',[ids[1]]);
await denied('insert into public.caterium_sidebar_brand_assignment(user_id) values($1)',[ids[2]],/duplicate key/);
await actor(ids[1]);
assert.equal((await one('select public.caterium_my_sidebar_brand() as b')).b.variant,'solnce');
await denied('select * from public.caterium_sidebar_brand_assignment',[],/permission denied/);
await denied('update public.caterium_sidebar_brand_assignment set user_id=$1',[ids[2]],/permission denied/);
await db.exec('reset role');
await q('update auth.users set email_confirmed_at=null where id=$1',[ids[1]]);
await actor(ids[1]);assert.equal((await one('select public.caterium_my_sidebar_brand() as b')).b.variant,'caterium');
await db.exec('reset role');await q('update auth.users set email_confirmed_at=now() where id=$1',[ids[1]]);
await actor(ids[2]);assert.equal((await one('select public.caterium_my_sidebar_brand() as b')).b.variant,'caterium');
await db.exec('reset role; set role anon');await denied('select public.caterium_my_sidebar_brand()',[],/permission denied/);
await actor(ids[0]);await denied('select public.sun_dev_dashboard()',[],/AAL2/);
await actor(ids[0],'aal2');
for(let i=1;i<=2;i++)await q('select public.sun_dev_create_trial_promo($1,$2,14,7,1,\'full\',null)',[`RECOVERY-TEST-${i}`,`test${i}@example.invalid`]);
await db.exec('reset role');
for(let i=1;i<=2;i++)await q("update auth.users set raw_user_meta_data=jsonb_build_object('promo_code',$2::text) where id=$1",[ids[i],`RECOVERY-TEST-${i}`]);
await actor(ids[1]);const ws=(await one('select public.sun_create_workspace() as id')).id;
assert.equal((await q('select * from public.sun_my_workspaces()')).rows.length,1);
await denied('select public.sun_create_workspace()',[],/уже относится/);
let row=await one('select * from public.sun_fetch_app_state($1)',[ws]);
assert.deepEqual(row.payload.storage.sunBoxes?.v||[],[]);
let payload=row.payload;payload.storage.sunOrders={t:'j',v:[{id:1,client:'Test customer',date:'2099-01-01',time:'12:00',total:5000,status:'Новый'}]};payload.storage.sunBoxes={t:'j',v:[{id:'box-1',name:'Test box',price:500}]};
row=await one('select * from public.sun_save_app_state_v17($1,$2,$3,$4)',[ws,payload,'recovery-test',row.revision]);
await denied('select * from public.sun_save_app_state_v17($1,$2,$3,$4)',[ws,payload,'stale-client',row.revision-1],e=>e.code==='PT409'&&/SUN_CONFLICT/.test(e.message));
assert.equal((await one('select * from public.sun_fetch_app_state($1)',[ws])).revision,row.revision);
const snap=(await one('select public.sun_v17_entity_snapshot($1) as s',[ws])).s;
assert.equal(snap.orders.length,1);assert.equal(snap.catalog.length,1);
const client=await one('select * from public.sun_v17_save_client_v1772($1,$2,$3,null,$4)',[ws,'p:79990000000',{identity:{name:'Test client',phone:'+79990000000'}},'recovery-test']);
await denied('select * from public.sun_v17_save_client_v1772($1,$2,$3,$4,$5)',[ws,'p:79990000000',{identity:{name:'Stale overwrite'}},client.version-1,'stale-client'],e=>e.code==='PT409'&&/SUN_CLIENT_CONFLICT/.test(e.message));
const unchangedClient=await one('select * from public.sun_v17_clients_snapshot_v1773($1) where client_key=$2',[ws,'p:79990000000']);
assert.equal(unchangedClient.version,client.version);assert.equal(unchangedClient.name,'Test client');
payload.storage.sunPdfBrandNameV1='Company A';
payload.storage.sunPdfBrandLogoV1='data:image/png;base64,dGVzdA==';
row=await one('select * from public.sun_save_app_state_v17($1,$2,$3,$4)',[ws,payload,'brand-owner',row.revision]);
assert.equal((await one('select * from public.sun_fetch_app_state($1)',[ws])).payload.storage.sunPdfBrandNameV1,'Company A');
assert.equal(client.version,1);
await q('select * from public.sun_v17_clients_snapshot_v1773($1)',[ws]);
await q("select public.sun_v17_create_backup($1,'daily','test')",[ws]);
assert.equal((await q('select * from public.sun_v17_list_backups($1)',[ws])).rows.length,1);
await q('select public.sun_v17_prune_backups($1)',[ws]);
payload.storage.sunOrders.v.push({id:77,date:'2025-01-01',time:'12:00',status:'Новый',total:7500,prepayment:2000,balance:5500,autoCompletionDisabled:true});
row=await one('select * from public.sun_save_app_state_v17($1,$2,$3,$4)',[ws,payload,'import-test',row.revision]);
await q('select public.sun_run_order_automation($1)',[ws]);
const historical=(await one('select public.sun_v17_entity_snapshot($1) as s',[ws])).s.orders.find(o=>String(o.id)==='77').data;
assert.equal(historical.prepayment,2000);assert.equal(historical.status,'Новый');assert.equal(historical.balance,5500);
await q('select public.caterium_account_snapshot($1)',[ws]);
await q('select * from public.sun_list_workspace_members($1)',[ws]);
await q("select public.sun_employee_prepare_v28($1,'test3@example.invalid','Viewer','viewer')",[ws]);
await q("select public.sun_employee_finalize_v28($1,$2,'Viewer','viewer')",[ws,ids[3]]);
await actor(ids[2]);const foreign=(await one('select public.sun_create_workspace() as id')).id;
assert.equal((await one('select * from public.sun_fetch_app_state($1)',[foreign])).payload.storage.sunPdfBrandLogoV1,undefined);
const demoRow=await one('select * from public.sun_fetch_app_state($1)',[foreign]);
const training=demoRow.payload.storage;
assert.equal(training.sunBoxes.v.length,44);assert.equal(training.sunStock.v.length,63);assert.equal(training.sunSuppliers.v.length,5);
assert.deepEqual([0,1,2,3,4,5,6].map(c=>training.sunBoxes.v.filter(b=>b.category===c).length),[10,4,3,4,2,3,18]);
assert.equal(training.sunBoxes.v.filter(b=>b.category===6).length,18);assert.equal(training.sunTrialDemoV1.v.banquetVersion,1);
assert.equal(training.sunOrders,undefined);assert.equal(training.sunTrialDemoV1.v.version,1);
assert.equal((await one('select public.sun_v17_entity_snapshot($1) as s',[foreign])).s.catalog.length,44);
assert.ok(training.sunStockMoves.v.length>0);assert.ok(training.sunStock.v.some(p=>p.qty===0));
for(const b of training.sunBoxes.v){for(const [name,qty,unit] of b.ingredients){assert.ok(qty>0);assert.ok(training.sunStock.v.some(p=>p.name===name&&p.unit===unit));}if(!b.ttk)continue;assert.ok(b.ttk.ingredientCost>0);assert.equal(b.ttk.outputGrams,Math.round(b.ttk.rows.filter(r=>r.unit==='кг').reduce((s,r)=>s+r.net*1000,b.ttk.waterGrams||0)));for(const r of b.ttk.rows){assert.ok(r.gross>=r.net);assert.ok(training.sunStock.v.some(p=>p.id===r.productId&&p.name===r.name&&p.unit===r.unit&&p.cost===r.unitCost));}}
await q('select public.caterium_install_trial_demo($1)',[foreign]);
assert.equal((await one('select * from public.sun_fetch_app_state($1)',[foreign])).revision,demoRow.revision,'idempotent demo install does not overwrite');
// Model an already used v1 trial: no banquet yet, edited prices, real balances and an order.
const legacy=structuredClone(demoRow.payload);
legacy.storage.sunBoxes.v=legacy.storage.sunBoxes.v.filter(b=>b.category===0);
legacy.storage.sunBoxes.v[0].price=4321;
legacy.storage.sunStock.v=legacy.storage.sunStock.v.filter(p=>p.id.startsWith('demo-v1-stock-'));
legacy.storage.sunStock.v[0].qty=7.123;
delete legacy.storage.sunTrialDemoV1.v.banquetVersion;
delete legacy.storage.sunTrialDemoV1.v.extrasVersion;
legacy.storage.sunOrders={t:'j',v:[{id:91,event:'Keep my order',date:'2099-12-20',time:'12:00',status:'Новый',lines:[],total:4321,prepayment:100,balance:4221,autoCompletionDisabled:true}]};
let upgraded=await one('select * from public.sun_save_app_state_v17($1,$2,$3,$4)',[foreign,legacy,'legacy-trial-fixture',demoRow.revision]);
assert.equal((await one('select public.caterium_trial_demo_status($1) as s',[foreign])).s.canUpgrade,true);
await q('select public.caterium_install_trial_demo($1)',[foreign]);
upgraded=await one('select * from public.sun_fetch_app_state($1)',[foreign]);
assert.deepEqual(upgraded.payload.storage.sunBoxes.v.slice(0,10),legacy.storage.sunBoxes.v);
assert.deepEqual(upgraded.payload.storage.sunStock.v.slice(0,43),legacy.storage.sunStock.v);
for(const key of ['sunOrders','sunStockMoves','sunSuppliers'])assert.deepEqual(upgraded.payload.storage[key],legacy.storage[key]);
assert.equal(upgraded.payload.storage.sunBoxes.v.length,44);
await q('select public.caterium_install_trial_banquet($1)',[foreign]);
assert.equal((await one('select * from public.sun_fetch_app_state($1)',[foreign])).revision,upgraded.revision);
// Existing custom drink/ware tabs must remain untouched, with no unused demo stock.
const partial=structuredClone(upgraded.payload);
partial.storage.sunBoxes.v=partial.storage.sunBoxes.v.filter(b=>!b.id.startsWith('demo-extras-'));
partial.storage.sunStock.v=partial.storage.sunStock.v.filter(p=>!p.id.startsWith('demo-extras-'));
delete partial.storage.sunTrialDemoV1.v.extrasVersion;
const custom=[{id:'own-drink',name:'My drink',category:3,price:123,ingredients:[]},{id:'own-plate',name:'My plate',category:1,price:456,ingredients:[]}];
partial.storage.sunBoxes.v.push(...custom);
await q('select * from public.sun_save_app_state_v17($1,$2,$3,$4)',[foreign,partial,'partly-filled-trial',upgraded.revision]);
await q('select public.caterium_install_trial_demo($1)',[foreign]);
upgraded=await one('select * from public.sun_fetch_app_state($1)',[foreign]);
assert.equal(upgraded.payload.storage.sunBoxes.v.length,38);
assert.deepEqual(upgraded.payload.storage.sunBoxes.v.filter(b=>b.category===1||b.category===3),custom);
assert.equal(upgraded.payload.storage.sunStock.v.length,55);
assert.deepEqual(upgraded.payload.storage.sunBoxes.v.slice(0,30),partial.storage.sunBoxes.v);
assert.deepEqual(upgraded.payload.storage.sunStock.v.slice(0,52),partial.storage.sunStock.v);
assert.equal(upgraded.payload.storage.sunTrialDemoV1.v.banquetInstalledAt,partial.storage.sunTrialDemoV1.v.banquetInstalledAt,'already upgraded banquet untouched');
await q('select public.caterium_install_trial_extras($1)',[foreign]);
assert.equal((await one('select * from public.sun_fetch_app_state($1)',[foreign])).revision,upgraded.revision);
const filled=structuredClone(upgraded.payload);delete filled.storage.sunTrialDemoV1;
await q('select * from public.sun_save_app_state_v17($1,$2,$3,$4)',[foreign,filled,'test-existing-catalog',upgraded.revision]);
await denied('select public.caterium_install_trial_banquet($1)',[foreign],/Сначала установите/);
await denied('select public.caterium_install_trial_extras($1)',[foreign],/Сначала установите/);
await denied('select public.caterium_install_trial_demo($1)',[foreign],/уже заполнены/);
assert.equal((await one('select public.caterium_my_sidebar_brand() as b')).b.variant,'caterium');
await denied('select * from public.sun_fetch_app_state($1)',[ws],/Access denied/);
await denied('select public.caterium_trial_demo_status($1)',[ws],/Access denied/);
await denied('select public.caterium_install_trial_demo($1)',[ws],/Administrator membership/);
await denied('select public.caterium_install_trial_banquet($1)',[ws],/Administrator membership/);
await denied('select public.caterium_install_trial_extras($1)',[ws],/Administrator membership/);
await denied('select * from public.sun_v17_clients_snapshot_v1773($1)',[ws],/Access denied/);
await denied('select public.sun_dev_dashboard()',[],/administrator/);
await actor(ids[3]);
await denied('select public.caterium_install_trial_demo($1)',[ws],/Administrator membership/);
assert.equal((await one('select public.caterium_my_sidebar_brand() as b')).b.variant,'caterium');
await denied('select * from public.sun_save_app_state_v17($1,$2,$3,$4)',[ws,{...payload,storage:{...payload.storage,sunPdfBrandNameV1:'Hijacked'}},'viewer-brand',row.revision],/Нет права/);
await denied('select * from public.sun_save_app_state_v17($1,$2,$3,$4)',[ws,{...payload,storage:{...payload.storage,sunOrders:{t:'j',v:[{id:2}]}}},'viewer',row.revision],/Нет права/);
await actor(ids[0],'aal2');
await denied('select public.caterium_install_trial_demo($1)',[foreign],/Administrator membership/);
for(const f of ['sun_dev_dashboard','sun_dev_list_companies','sun_dev_list_companies_v1768','sun_dev_list_users','sun_dev_list_errors','sun_dev_list_activity','sun_dev_list_trial_promos'])await q(`select * from public.${f}()`);
for(const f of ['sun_dev_workspace_diagnostics','sun_dev_list_workspace_features','sun_dev_support_snapshot'])await q(`select * from public.${f}($1)`,[ws]);
await q("select public.sun_dev_create_company('Recovery test company','test3@example.invalid','full',30,'[]','test')");
await actor(ids[1]);
assert.equal((await one('select public.caterium_trial_demo_status($1) as s',[ws])).s.eligible,false,'Solnce company excluded');
await denied('select public.caterium_install_trial_demo($1)',[ws],/Демонстрационная база доступна/);
await denied('select public.caterium_install_trial_banquet($1)',[ws],/Демонстрационная база доступна/);
await denied('select public.caterium_install_trial_extras($1)',[ws],/Демонстрационная база доступна/);
await db.exec('reset role');
const devWs=(await one("insert into public.sun_workspaces(name,created_by) values('Own developer test',$1) returning id",[ids[0]])).id;
await q("insert into public.sun_workspace_members(workspace_id,user_id,role,is_active,permissions) values($1,$2,'admin',true,public.sun_role_default_permissions('admin'))",[devWs,ids[0]]);
await q("insert into public.sun_workspace_subscriptions(workspace_id,plan_id,status,current_period_end) values($1,'full','active',now()+interval '30 days')",[devWs]);
const preserved={format:'sun-cloud-v2',version:2,storage:{sunCompanyProfileV1:{t:'j',v:{name:'Own company'}},sunClientLoyaltyV1:{t:'j',v:{fixture:'preserve me'}}}};
await q("insert into public.sun_app_state(workspace_id,payload,client_id) values($1,$2,'fixture')",[devWs,preserved]);
await actor(ids[0]);
await q('select public.caterium_install_trial_demo($1)',[devWs]);
const seeded=(await one('select * from public.sun_fetch_app_state($1)',[devWs])).payload.storage;
assert.deepEqual(seeded.sunClientLoyaltyV1,preserved.storage.sunClientLoyaltyV1);assert.deepEqual(seeded.sunCompanyProfileV1,preserved.storage.sunCompanyProfileV1);
assert.equal(seeded.sunBoxes.v.length,44,'developer own company works without developer-panel MFA');
await db.exec('reset role; set role anon');await denied('select public.caterium_install_trial_extras($1)',[devWs],/permission denied/);await denied('select public.caterium_trial_extras_seed_v1()',[],/permission denied/);
await db.exec('reset role; set role anon');await denied('select public.caterium_install_trial_banquet($1)',[devWs],/permission denied/);await denied('select public.caterium_trial_banquet_seed_v1()',[],/permission denied/);
await db.exec('reset role; set role anon');await denied('select public.caterium_install_trial_demo($1)',[devWs],/permission denied/);await denied('select public.caterium_trial_demo_seed_v1()',[],/permission denied/);
console.log('PASS demo: trial auto seed, 10 box + 18 banquet TTKs, stock links, additive upgrade preserving orders/prices/balances, idempotency, populated catalog guard, Solnce exclusion, own developer account and preserved clients');
console.log('PASS smoke: owner onboarding, save/read, optimistic conflict, normalized catalog/orders/clients, backups, employees, cross-tenant isolation, viewer write denial, AAL2, developer RPCs');
await (await import('./employee-session.mjs')).run(db,{owner:ids[1],employee:ids[3],workspace:ws,foreign});
await db.close();
}