Files
story/test/e2e/customer-sms-login.mjs
T

102 lines
9.2 KiB
JavaScript
Raw Normal View History

// Real portal + auth hook + API client, synthetic SMS/HTTP responses. Never sends an SMS.
import assert from 'node:assert/strict';
import {createRequire} from 'node:module';
import {mkdir,writeFile} from 'node:fs/promises';
import {resolve} from 'node:path';
import {pathToFileURL} from 'node:url';
const require=createRequire(process.env.PLAYWRIGHT_REQUIRE_FROM??import.meta.url);
const {chromium}=require('playwright');
const {createServer}=await import(pathToFileURL(resolve('web/node_modules/vite/dist/node/index.js')));
const output=resolve('.design-evidence/customer-sms-login');await mkdir(output,{recursive:true});
const server=await createServer({configFile:false,root:resolve('web'),oxc:{jsx:{runtime:'automatic'}},logLevel:'error',server:{host:'127.0.0.1',port:5322,strictPort:true}});
await server.listen();
let browser;
const checks=[],problems=[];
try {
browser=await chromium.launch({headless:true,executablePath:process.env.CHROME_PATH??'C:/Program Files/Google/Chrome/Application/chrome.exe'});
const page=await browser.newPage({viewport:{width:1280,height:1080}});
page.on('pageerror',e=>problems.push(e.message));
await page.clock.install();
let signedIn=false, smsEnabled=true, expireMe=false, sendCount=0, bootstraps=0, lastRequestID, memoryWrites=0;
await page.route('**/api/v1/**',async route=>{
const r=route.request(), path=new URL(r.url()).pathname, body=r.method()==='POST'?r.postDataJSON():undefined;
let status=200,code='OK',data={};
if(path==='/api/v1/web/account/session') {
if(signedIn)data={authenticated:true,csrf_token:'account-csrf'};else{status=401;code='LOGIN_REQUIRED';}
} else if(path==='/api/v1/web/login/session') {bootstraps++;data={sms_enabled:smsEnabled,signup_enabled:true,csrf_token:'login-csrf'};}
else if (['/api/v1/web/login/captcha','/api/v1/web/auth/captcha'].includes(path)) { assert.equal(r.headers()['x-csrf-token'],path.includes('/auth/')?'setup-csrf':'login-csrf'); data={captcha_id:'captcha',image:'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=',expires_at_ms:Date.now()+120000}; }
else if(path==='/api/v1/web/login/sms/send') {
assert.equal(r.headers()['x-csrf-token'],'login-csrf');assert.equal(r.headers().authorization,undefined);
assert.equal(body.captcha_id,'captcha');assert.equal(body.captcha_code,'654321');assert.equal(body.purpose,'LOGIN');assert.match(body.phone_e164,/^\+8613[0-9]{9}$/);
sendCount++;lastRequestID=r.headers()['x-request-id'];assert.match(lastRequestID,/^[a-f0-9-]{36}$/);
data={sms_challenge_id:`challenge-${sendCount}`,resend_after_ms:60000,expires_at_ms:Date.now()+300000};
} else if(path==='/api/v1/web/login/sms/verify') {
assert.equal(r.headers()['x-csrf-token'],'login-csrf');assert.equal(body.sms_challenge_id,`challenge-${sendCount}`);
if(body.code!=='654321'){status=401;code='SMS_CODE_INVALID';}else{signedIn=true;data={csrf_token:'account-csrf',data:{authenticated:true}};}
} else if(path==='/api/v1/me') {
assert.equal(r.headers().authorization,undefined);
if(!signedIn||expireMe){signedIn=false;status=401;code='LOGIN_REQUIRED';}
else data={identity:{issuer:'urn:platform:onboarding:sms',subject:'synthetic',display_name:'短信验收用户',preferred_username:'短信验收用户'},accounts:[]};
} else if(path==='/api/v1/memories/query') {
assert.equal(r.headers().authorization,undefined);assert.equal(r.headers()['x-csrf-token'],'account-csrf');memoryWrites++;data={items:[]};
} else if(path==='/api/v1/content-models'){status=401;code='AUTHENTICATION_REQUIRED';}
else if(path==='/api/v1/web/auth/logout'){signedIn=false;data={signed_out:true};}
else if(path==='/api/v1/web/account/password') data={available:false};
else {throw new Error(`Unexpected API: ${path}`);}
await route.fulfill({status,contentType:'application/json',body:JSON.stringify({ok:status===200,code,message:code==='SMS_CODE_INVALID'?'验证码错误或已失效':code,data})});
});
await page.goto('http://127.0.0.1:5322/?section=profile');
await page.getByRole('heading',{name:'手机号登录'}).waitFor();
await page.waitForFunction(()=>!document.querySelector('#customer-phone').disabled);
assert.equal(bootstraps,1);assert.equal(await page.locator('input[type=password]').count(),0);
await page.waitForTimeout(400);await page.screenshot({path:resolve(output,'login-desktop.png'),fullPage:true});
checks.push('SMS is the default login; StrictMode creates one bootstrap request');
assert.equal(new URL(page.url()).pathname,'/login');
assert.equal(new URL(page.url()).searchParams.get('returnTo'),'/?section=profile');
assert.equal(await page.locator('.customer-layout,.maintenance-layout').count(),0);
checks.push('protected page redirects to standalone login with no business navigation');
const phone=page.getByLabel('手机号',{exact:true}), otp=page.getByLabel('短信验证码',{exact:true}), form=page.locator('.sms-login-form');
assert(await form.getByRole('button',{name:'获取验证码'}).isDisabled());
await phone.fill('13800000000');assert(await form.getByRole('button',{name:'获取验证码'}).isDisabled());await page.getByLabel('图片验证码',{exact:true}).fill('654321');await form.getByRole('button',{name:'获取验证码'}).click();
await page.getByText(/验证码已发送至 138\*\*\*\*0000/).waitFor();
assert.equal(sendCount,1);assert(await form.getByRole('button',{name:/秒后重发/}).isDisabled());
checks.push('send uses normalized phone, CSRF, unique request ID and a resend countdown');
await otp.fill('654321');await phone.fill('13900000000');
assert(await form.getByRole('button',{name:/^登\s*录$/}).isDisabled());
checks.push('changing phone clears the old challenge and code');
await page.clock.fastForward(61000);await page.getByLabel('图片验证码',{exact:true}).fill('654321');await form.getByRole('button',{name:'获取验证码'}).click();
await page.getByText(/验证码已发送至 139\*\*\*\*0000/).waitFor();
await otp.fill('000000');await form.getByRole('button',{name:/^登\s*录$/}).click();
await page.getByText('验证码错误或已失效',{exact:true}).waitFor();
assert.equal(await page.getByRole('heading',{name:'手机号登录'}).count(),1);
checks.push('incorrect OTP stays on the login form and does not trigger a global session failure');
await page.waitForTimeout(400);await page.screenshot({path:resolve(output,'desktop.png'),fullPage:true});
await page.setViewportSize({width:375,height:920});await page.waitForTimeout(400);await page.screenshot({path:resolve(output,'mobile.png'),fullPage:true});
assert(await page.evaluate(()=>document.documentElement.scrollWidth<=document.documentElement.clientWidth+2));
checks.push('375px mobile page has no horizontal overflow');
await otp.fill('654321');await form.getByRole('button',{name:/^登\s*录$/}).click();
await page.getByRole('button',{name:'打开账户菜单'}).waitFor();assert.equal(await page.getByRole('heading',{name:'手机号登录'}).count(),0);
assert.equal(new URL(page.url()).search,'?section=profile');
await page.reload();await page.getByRole('button',{name:'打开账户菜单'}).waitFor();
assert.equal(await page.evaluate(()=>Object.keys(sessionStorage).filter(k=>k.startsWith('oidc.user:')).length),0);
checks.push('successful SMS login restores the customer workspace after reload without an OIDC token');
await page.evaluate(async()=>{const api=await import('/src/api/client.ts');await api.authenticatedJSON('POST','/api/v1/memories/query','',undefined,{elder_id:'synthetic'});});
assert.equal(memoryWrites,1);checks.push('customer API writes send account CSRF with cookie authentication, without an empty Bearer header');
await page.evaluate(async()=>{const {platformApi}=await import('/src/api/client.ts');await platformApi.contentModels('synthetic','').catch(()=>{});});
await page.waitForTimeout(250);assert.equal(new URL(page.url()).pathname,'/');await page.getByRole('button',{name:'打开账户菜单'}).waitFor();
await page.evaluate(async()=>{const {userManager}=await import('/src/auth/oidc.ts');await userManager.events._silentRenewError.raise(new Error('old OIDC error'));});
await page.waitForTimeout(100);assert.equal(new URL(page.url()).pathname,'/');
checks.push('resource 401 and legacy renewal failure cannot discard a valid phone session');
expireMe=true;
await page.evaluate(async()=>{const {platformApi}=await import('/src/api/client.ts');await platformApi.currentUser('').catch(()=>{});});
await page.getByRole('heading',{name:'手机号登录'}).waitFor();assert.equal(await page.getByText('短信验收用户',{exact:true}).count(),0);
assert.equal(new URL(page.url()).searchParams.get('returnTo'),'/?section=profile');
checks.push('an expired customer session removes prior private account content');
smsEnabled=false;await page.reload();await page.getByText('短信登录暂未开放,请使用密码登录',{exact:true}).waitFor();
await phone.fill('13800000000');assert(await form.getByRole('button',{name:'获取验证码'}).isDisabled());
checks.push('an unconfigured SMS service is shown as unavailable, never as a successful send');
assert.deepEqual(problems,[]);
await writeFile(resolve(output,'result.json'),JSON.stringify({checks,problems,boundary:'Synthetic SMS and API responses; no real SMS sent.'},null,2));
console.log(JSON.stringify({checks,problems}));
} finally {await browser?.close();await server.close();}