caterium-app/tests/login-recovery.spec.mjs

83 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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:'<!doctype html><html><body></body></html>'}));
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:'<!doctype html><html><body></body></html>'}));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);
});