// 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();}