整体UI修改
This commit is contained in:
+21
-114
@@ -11,34 +11,17 @@ async function moduleUrl(file,replace=code=>code){
|
||||
}
|
||||
const protocolUrl=await moduleUrl('../src/lib/api/protocol.ts');
|
||||
const authUrl=await moduleUrl('../src/lib/client/auth.ts');
|
||||
const {authFormPayload}=await import(authUrl);
|
||||
let sequence=0;
|
||||
async function client(){
|
||||
const url=await moduleUrl('../src/lib/client/api.ts',code=>code.replace('"../api/protocol"',JSON.stringify(protocolUrl)).replace('"./auth"',JSON.stringify(authUrl+'#'+sequence++)));
|
||||
return new (await import(url)).ApiClient();
|
||||
}
|
||||
const guest={userId:7,type:0,isRegister:false};
|
||||
const member={userId:8,type:1,isRegister:true};
|
||||
const other={userId:8,type:0,isRegister:false};
|
||||
const response=(data,code=200,headers={})=>new Response(JSON.stringify({code,data,msg:code===200?'ok':'session expired'}),{status:code===1000?401:200,headers:{'Content-Type':'application/json',...headers}});
|
||||
const deferred=()=>{let resolve;const promise=new Promise(done=>resolve=done);return {promise,resolve};};
|
||||
async function withFetch(fetcher,work){const old=globalThis.fetch;globalThis.fetch=fetcher;try{await work();}finally{globalThis.fetch=old;}}
|
||||
|
||||
test('login accepts account/email, trims identifiers, and preserves password bytes',()=>{
|
||||
assert.deepEqual(authFormPayload('login',{account:' test ',password:' 1234 '}),{account:'test',password:' 1234 '});
|
||||
assert.deepEqual(authFormPayload('login',{account:'me@example.com',password:'1234'}),{account:'me@example.com',password:'1234'});
|
||||
assert.throws(()=>authFormPayload('login',{account:'bad@',password:'1234'}),/有效邮箱/);
|
||||
assert.throws(()=>authFormPayload('login',{account:'test',password:'123'}),/密码/);
|
||||
const account='abcdefghijklmnopqrst',password='SafePassword12345678';
|
||||
assert.equal(authFormPayload('login',{account,password}).account,account);
|
||||
});
|
||||
|
||||
test('guest binding validates confirmation and agreement without requiring an email',()=>{
|
||||
const values={account:' player01 ',password:'Secure123',passwordConfirm:'Secure123',agree:'on'};
|
||||
assert.deepEqual(authFormPayload('register',values),{accountNo:'player01',password:'Secure123'});
|
||||
for(const change of [{account:'a_b'},{password:'abcdefgh',passwordConfirm:'abcdefgh'},{passwordConfirm:'Wrong123'},{agree:''}])assert.throws(()=>authFormPayload('register',{...values,...change}));
|
||||
assert.deepEqual(authFormPayload('resetPassword',{account:' player01 ',email:' me@example.com ',password:'NewPass123',passwordConfirm:'NewPass123'}),{accountNo:'player01',email:'me@example.com',newPassword:'NewPass123'});
|
||||
});
|
||||
|
||||
test('device identity, first channel and invite survive page navigation without persisting credentials',async()=>{
|
||||
const originals=Object.fromEntries(['document','localStorage','location'].map(key=>[key,Object.getOwnPropertyDescriptor(globalThis,key)]));
|
||||
const storage=new Map([['uuid','old-device']]);
|
||||
@@ -66,7 +49,7 @@ test('concurrent private requests share one guest initialization and public data
|
||||
if(url.endsWith('/auth/guest')){await gate.promise;return response(guest);}
|
||||
return response({});
|
||||
},async()=>{
|
||||
const first=api.get('user/center/data'),second=api.get('playlet/collect/list');
|
||||
const first=api.get('comment/list'),second=api.get('playlet/detail');
|
||||
await Promise.resolve();assert.equal(calls.length,1);gate.resolve();await Promise.all([first,second]);
|
||||
assert.equal(calls.filter(call=>call.url.endsWith('/auth/guest')).length,1);
|
||||
assert.ok(calls[0].body.devID);assert.equal(api.user.userId,7);
|
||||
@@ -74,14 +57,26 @@ test('concurrent private requests share one guest initialization and public data
|
||||
});
|
||||
});
|
||||
|
||||
test('history restoration revalidates current cookies, respects logout and cannot overwrite a newer login',async()=>{
|
||||
test('history restoration revalidates current cookies and cannot overwrite a newer session',async()=>{
|
||||
const api=await client();api.seedSession(guest);
|
||||
await withFetch(async(_url,options)=>{assert.equal(JSON.parse(options.body).automatic,true);return response(null);},async()=>{
|
||||
await api.revalidateSession();assert.equal(api.user,null);assert.equal(await api.initializeSession(),null);
|
||||
});
|
||||
await withFetch(async()=>response(null),async()=>{await api.revalidateSession();assert.equal(api.user,null);});
|
||||
const gate=deferred();api.seedSession(guest);
|
||||
await withFetch(async()=>{await gate.promise;return response(guest);},async()=>{
|
||||
const refresh=api.revalidateSession();await Promise.resolve();api.setUser(member);gate.resolve();await refresh;assert.equal(api.user.userId,member.userId);
|
||||
const refresh=api.revalidateSession();await Promise.resolve();api.setUser(other);gate.resolve();await refresh;assert.equal(api.user.userId,other.userId);
|
||||
});
|
||||
});
|
||||
|
||||
test('an expired session is replaced by a fresh guest on the next private request and never prompts for login',async()=>{
|
||||
const api=await client();api.seedSession(guest);const calls=[];let expired=true;
|
||||
await withFetch(async url=>{
|
||||
calls.push(url);
|
||||
if(url.endsWith('/auth/guest'))return response(other);
|
||||
if(expired){expired=false;return response(null,1000);}
|
||||
return response({list:[]});
|
||||
},async()=>{
|
||||
await assert.rejects(api.get('comment/list',{objectId:1}));assert.equal(api.user,null);
|
||||
await api.get('comment/list',{objectId:1});assert.equal(api.user.userId,other.userId);
|
||||
assert.deepEqual(calls.map(url=>url.split('?')[0]),['/api/comment/list','/api/auth/guest','/api/comment/list']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -100,96 +95,8 @@ test('search and suggestions wait for a shared guest session before requesting p
|
||||
});
|
||||
});
|
||||
|
||||
test('slow guest restoration finishes before login and binding waits for a guest identity',async()=>{
|
||||
const api=await client(),gate=deferred(),calls=[];
|
||||
await withFetch(async(url,options)=>{
|
||||
calls.push({url,body:JSON.parse(options.body)});
|
||||
if(url.endsWith('/auth/guest')){await gate.promise;return response(guest);}
|
||||
return response(member);
|
||||
},async()=>{
|
||||
const restore=api.initializeSession(),login=api.post('auth/login',{account:'player',password:'Secure123'});
|
||||
await Promise.resolve();assert.equal(calls.length,1);gate.resolve();await Promise.all([restore,login]);
|
||||
assert.equal(calls[1].body.devID,calls[0].body.devID);assert.equal(api.user.userId,8);
|
||||
const binding=await client();await binding.post('auth/register',{accountNo:'player',password:'Secure123'});
|
||||
assert.deepEqual(calls.slice(2).map(call=>call.url),['/api/auth/guest','/api/auth/register']);
|
||||
assert.equal(binding.user.userId,8);
|
||||
});
|
||||
});
|
||||
|
||||
test('invalid credentials preserve the current identity; late expired requests cannot clear a new login',async()=>{
|
||||
const api=await client();api.setUser(guest);
|
||||
const gate=deferred();let invalid=true;
|
||||
await withFetch(async url=>{
|
||||
if(url.endsWith('/auth/login'))return invalid?response(null,1000):response(member);
|
||||
await gate.promise;return response(null,1000);
|
||||
},async()=>{
|
||||
await assert.rejects(api.post('auth/login',{account:'player',password:'bad'}));assert.equal(api.user.userId,7);
|
||||
const pending=api.get('user/center/data');await Promise.resolve();invalid=false;
|
||||
await api.post('auth/login',{account:'player',password:'Secure123'});gate.resolve();await assert.rejects(pending);
|
||||
assert.equal(api.user.userId,8);
|
||||
});
|
||||
});
|
||||
|
||||
test('registered expiry prompts once and blocks automatic guest downgrade',async()=>{
|
||||
const api=await client();api.setUser(member);let prompts=0,calls=0;api.addEventListener('expired',()=>prompts++);
|
||||
await withFetch(async()=>{calls++;return response(null,1000,{'X-Auth-State':'login-required'});},async()=>{
|
||||
await assert.rejects(api.get('user/center/data'));assert.equal(api.user,null);assert.equal(prompts,1);
|
||||
assert.equal(await api.initializeSession(),null);
|
||||
await assert.rejects(api.ensureSession());await assert.rejects(api.ensureSession());
|
||||
assert.equal(calls,1);assert.equal(prompts,1);
|
||||
});
|
||||
});
|
||||
|
||||
test('manual logout and the following skipped initialization stay signed out',async()=>{
|
||||
const api=await client();api.setUser(member);const calls=[];
|
||||
await withFetch(async url=>{calls.push(url);return response(null);},async()=>{
|
||||
await api.post('auth/logout',{});assert.equal(api.user,null);
|
||||
assert.equal(await api.initializeSession(),null);assert.deepEqual(calls,['/api/auth/logout']);
|
||||
const refreshed=await client();assert.equal(await refreshed.initializeSession(),null);
|
||||
await assert.rejects(refreshed.ensureSession());assert.deepEqual(calls,['/api/auth/logout','/api/auth/guest']);
|
||||
});
|
||||
});
|
||||
|
||||
test('restoring an expired registered cookie prompts for login even before a user is loaded',async()=>{
|
||||
const api=await client();let prompts=0;api.addEventListener('expired',()=>prompts++);
|
||||
await withFetch(async()=>response(null,1000,{'X-Auth-State':'login-required'}),async()=>{
|
||||
await assert.rejects(api.initializeSession());await assert.rejects(api.ensureSession());
|
||||
assert.equal(prompts,1);assert.equal(api.user,null);
|
||||
});
|
||||
});
|
||||
|
||||
test('auth dialog renders binding confirmation, blocks duplicate submission, and clears revealed passwords on close',async()=>{
|
||||
const renderUrl=await moduleUrl('../src/lib/client/render.ts',code=>code.replace('"../api/protocol"',JSON.stringify(protocolUrl)));
|
||||
const accountUrl=await moduleUrl('../src/lib/client/account.ts',code=>code.replace('"../api/protocol"',JSON.stringify(protocolUrl)).replace('"./render"',JSON.stringify(renderUrl)).replace("'./auth'",JSON.stringify(authUrl)));
|
||||
const {bindAccountUI,openAuth}=await import(accountUrl);
|
||||
const oldDocument=globalThis.document,OldFormData=globalThis.FormData;
|
||||
const listeners={},error={textContent:''},button={textContent:'绑定账号',disabled:false};
|
||||
const password={value:'Secure123',type:'password'},confirmation={value:'Secure123',type:'password'};
|
||||
const form={dataset:{mode:'register'},isConnected:true,reportValidity:()=>true,
|
||||
querySelector:selector=>selector==='#auth-error'?error:button,querySelectorAll:()=>[password,confirmation]};
|
||||
const dialog={id:'member-auth-dialog',innerHTML:'',open:false,classList:{toggle(){}},querySelector:selector=>selector==='#auth-account'?{focus(){}}:selector==='#auth-password'?password:null,
|
||||
querySelectorAll:()=>[password,confirmation],addEventListener:(name,handler)=>listeners[name]=handler,showModal(){this.open=true;},close(){this.open=false;listeners.close();}};
|
||||
const gate=deferred();let submitted=0,routed=0;
|
||||
const app={api:{post:async(path,payload)=>{submitted++;assert.equal(path,'auth/register');assert.equal(payload.accountNo,'player01');await gate.promise;}},notify(){},async route(){routed++;}};
|
||||
globalThis.document={getElementById:id=>id==='member-auth-dialog'?dialog:null};
|
||||
globalThis.FormData=class extends OldFormData{constructor(){super();for(const [key,value] of Object.entries({account:'player01',password:'Secure123',passwordConfirm:'Secure123',agree:'on'}))this.append(key,value);}};
|
||||
try{
|
||||
bindAccountUI(app);openAuth('bind');assert.ok(dialog.open);
|
||||
assert.ok(dialog.innerHTML.includes('name="passwordConfirm"'));assert.ok(!dialog.innerHTML.includes('name="email"'));
|
||||
const toggle={dataset:{authPasswordToggle:'auth-password'},setAttribute(){}};
|
||||
listeners.click({target:{closest:()=>toggle}});assert.equal(password.type,'text');
|
||||
const event={target:form,preventDefault(){}};listeners.submit(event);listeners.submit(event);
|
||||
assert.equal(submitted,1);assert.equal(button.disabled,true);
|
||||
gate.resolve();await new Promise(resolve=>setImmediate(resolve));
|
||||
assert.equal(routed,1);assert.equal(button.disabled,false);assert.equal(password.value,'');assert.equal(confirmation.value,'');
|
||||
openAuth('switch');assert.ok(dialog.innerHTML.includes('切换账号'));
|
||||
}finally{globalThis.document=oldDocument;globalThis.FormData=OldFormData;}
|
||||
});
|
||||
|
||||
|
||||
test('a server-prepared session is reused without another guest handshake and a sign-in snapshot blocks automatic downgrade',async()=>{
|
||||
test('a server-prepared session is reused without another guest handshake',async()=>{
|
||||
await withFetch(()=>assert.fail('prepared sessions must not refetch auth'),async()=>{
|
||||
const ready=await client();ready.seedSession(member);assert.equal((await ready.initializeSession()).userId,member.userId);
|
||||
const signedOut=await client();signedOut.seedSession(null);assert.equal(await signedOut.initializeSession(),null);
|
||||
const ready=await client();ready.seedSession(other);assert.equal((await ready.initializeSession()).userId,other.userId);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user