import fs from 'node:fs'; import {test,expect} from '@playwright/test'; test('cloud reads and password login recover from empty proxy responses without replaying writes',async({page})=>{ await page.route('**/index.html',r=>r.fulfill({contentType:'text/html',body:''})); await page.goto('/index.html');await page.addScriptTag({url:'/core/cloud-transport.js'}); const result=await page.evaluate(async()=>{ const calls=[],upstream='https://backend.example.invalid',proxy='https://proxy.example.invalid';let scenario='read'; window.fetch=async request=>{calls.push({url:request.url,body:await request.text(),auth:request.headers.get('authorization')});if(scenario==='denied')return new Response('{"error":"denied"}',{status:401});if(scenario==='write')return new Response('',{status:503});return request.url.startsWith(proxy)?new Response('',{headers:{'content-type':'text/html'}}):new Response('[{"id":"company"}]',{headers:{'content-type':'application/json'}})}; const send=window.CateriumCloudTransport.create({upstream,proxy}); const read=await (await send(upstream+'/rest/v1/rpc/sun_my_workspaces',{method:'POST',headers:{Authorization:'Bearer test-token'},body:'{}'})).json(); const readCalls=calls.splice(0); await send(upstream+'/auth/v1/token?grant_type=password',{method:'POST',body:'{"email":"test@example.invalid","password":"test"}'});const authCalls=calls.splice(0); scenario='write';const write=await send(upstream+'/rest/v1/rpc/sun_save_app_state_v17',{method:'POST',body:'{}'});const writeCalls=calls.splice(0); scenario='empty-write';let writeError='';try{await send(upstream+'/rest/v1/rpc/sun_save_app_state_v17',{method:'POST',body:'{}'})}catch(e){writeError=e.message}const emptyWriteCalls=calls.splice(0); scenario='denied';const denied=await send(upstream+'/auth/v1/token?grant_type=password',{method:'POST',body:'{}'});const deniedCalls=calls.splice(0); return {read,readCalls,authCalls,write:write.status,writeCalls,writeError,emptyWriteCalls,denied:denied.status,deniedCalls}; }); expect(result.read).toEqual([{id:'company'}]);expect(result.readCalls.map(c=>c.url)).toEqual(['https://proxy.example.invalid/rest/v1/rpc/sun_my_workspaces','https://backend.example.invalid/rest/v1/rpc/sun_my_workspaces']); expect(result.readCalls.every(c=>c.auth==='Bearer test-token'&&c.body==='{}')).toBe(true); expect(result.authCalls).toHaveLength(2);expect(result.authCalls[0].body).toBe(result.authCalls[1].body); expect(result.write).toBe(503);expect(result.writeCalls).toHaveLength(1);expect(result.writeError).toContain('пустой');expect(result.emptyWriteCalls).toHaveLength(1); expect(result.denied).toBe(401);expect(result.deniedCalls).toHaveLength(1); }); test('membership loading is shared and an invalid response remains an error until retry succeeds',async({page})=>{ await page.route('**/index.html',r=>r.fulfill({contentType:'text/html',body:''}));await page.goto('/index.html'); await page.addScriptTag({url:'/core/sun-safe.js'});await page.addScriptTag({url:'/core/cloud-transport.js'}); const runtime=fs.readFileSync('public/app-runtime.js','utf8');let cloud=runtime.slice(runtime.indexOf('/* ===== MODULE: cloud-sync-v2.js'),runtime.indexOf('/* ===== MODULE: admin-rbac-v3.js')); cloud=cloud.replace('async function boot(){','async function boot(){return;').replace('window.SunCloudV2={',`window.SunCloudV2={testLoad:loadMemberships,testInit:c=>{client=c;session={user:{id:'test-user'}};config.tenantStorageReady=true;config.localWorkspaceId='company';config.workspaceId='company'},`); await page.addScriptTag({content:'var orders=[];var boxes=[];'+cloud}); const result=await page.evaluate(async()=>{ let calls=0,resolve;const c=window.SunCloudV2;c.testInit({rpc:()=>{calls++;return new Promise(r=>resolve=r)}}); const a=c.testLoad(),b=c.testLoad();resolve({data:'',error:null});await Promise.all([a,b]);const failed=c.status(); const retry=c.testLoad();resolve({data:[{id:'company',name:'Test',role:'admin',is_active:true}],error:null});await retry; return {calls,failed,ready:c.status()}; }); expect(result.calls).toBe(2);expect(result.failed.membershipsLoading).toBe(false);expect(result.failed.membershipError).toContain('список компаний');expect(result.failed.workspace).toBeNull(); expect(result.ready.membershipError).toBe('');expect(result.ready.workspace.id).toBe('company');expect(result.ready.membershipsLoading).toBe(false); }); test('a failed company load replaces the stale login form with an actionable retry screen',async({page})=>{ await page.route('**://api.caterium.ru/**',r=>r.abort());await page.route('**://*.supabase.co/**',r=>r.abort()); await page.goto('/index.html',{waitUntil:'domcontentloaded'});await expect(page.locator('#sunGateEmailV3')).toBeVisible(); await page.evaluate(()=>{ window.loginState={connected:true,signedIn:true,membershipsLoading:true,membershipsLoaded:false};window.retryCount=0; window.SunCloudV2={getSession:()=>({user:{id:'test',email:'test@example.invalid'}}),getWorkspace:()=>null,getClient:()=>null,status:()=>window.loginState,hasPermission:()=>false,reloadMemberships:async()=>{window.retryCount++;return null}}; window.dispatchEvent(new Event('sun:cloud-permissions-changed')); }); await expect(page.locator('#sunGateEmailV3')).toHaveCount(0); await page.evaluate(()=>{window.loginState={...window.loginState,membershipsLoading:false,membershipsLoaded:true,membershipError:'Ответ сервера не получен'};window.dispatchEvent(new Event('sun:cloud-permissions-changed'))}); await expect(page.getByRole('heading',{name:'Не удалось загрузить рабочую базу'})).toBeVisible();await expect(page.locator('#sunGateErrorV3')).toHaveText('Ответ сервера не получен'); await page.getByRole('button',{name:'Повторить загрузку',exact:true}).click();expect(await page.evaluate(()=>window.retryCount)).toBe(1); await expect(page.getByRole('button',{name:'Повторить загрузку',exact:true})).toBeEnabled();await expect(page.locator('body > header')).toBeHidden(); }); test('real SDK login opens the ordinary app when the proxy returns empty successful responses',async({page})=>{ const userId='11111111-1111-4111-8111-111111111111',workspaceId='22222222-2222-4222-8222-222222222222',expires=Math.floor(Date.now()/1000)+3600; const user={id:userId,aud:'authenticated',role:'authenticated',email:'test@example.invalid',email_confirmed_at:new Date().toISOString(),app_metadata:{provider:'email'},user_metadata:{}}; const token=[{alg:'HS256',typ:'JWT'},{sub:userId,role:'authenticated',aud:'authenticated',exp:expires,iat:expires-3600,aal:'aal1'},'test'].map(x=>typeof x==='string'?x:Buffer.from(JSON.stringify(x)).toString('base64url')).join('.'); const seen=[],warnings=[];page.on('console',m=>{if(m.type()==='warning')warnings.push(m.text())}); const handle=async route=>{ const request=route.request(),url=new URL(request.url());seen.push(url.host+url.pathname); const headers={'access-control-allow-origin':'*'}; if(request.method()==='OPTIONS')return route.fulfill({status:204,headers}); if(url.host==='api.caterium.ru')return route.fulfill({status:200,contentType:'text/html',body:'',headers}); let body=null; if(url.pathname==='/auth/v1/token')body={access_token:token,refresh_token:'test-refresh',token_type:'bearer',expires_in:3600,expires_at:expires,user}; else if(url.pathname==='/auth/v1/user')body=user; else if(url.pathname.endsWith('/sun_my_workspaces'))body=[{id:workspaceId,name:'Test workspace',role:'admin',is_active:true,permissions:{}}]; else if(url.pathname.endsWith('/sun_is_platform_admin'))body=true; return route.fulfill({status:200,contentType:'application/json',body:JSON.stringify(body),headers}); }; await page.route('**://api.caterium.ru/**',handle);await page.route('**://*.supabase.co/**',handle); await page.goto('/index.html',{waitUntil:'domcontentloaded'});await page.waitForFunction(()=>window.CateriumAuthSecurityV1774&&window.SunCloudV2?.getClient()); await page.locator('#sunGateEmailV3').fill(user.email);await page.locator('#sunGatePasswordV3').fill('test-password');await page.locator('#sunGateSubmitV3').click(); await expect(page.locator('#sunCloudAuthGateV3')).toHaveCount(0,{timeout:20000});await expect(page.locator('body > header')).toBeVisible(); expect(await page.evaluate(()=>window.SunCloudV2.getWorkspace()?.id)).toBe(workspaceId); expect(seen.some(s=>s==='api.caterium.ru/rest/v1/rpc/sun_my_workspaces')).toBe(true); expect(seen.some(s=>s.endsWith('.supabase.co/rest/v1/rpc/sun_my_workspaces'))).toBe(true); expect(warnings.some(s=>s.includes('Multiple GoTrueClient'))).toBe(false); });