Keep slow app sessions alive and defer automatic companion prompts
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
// Real served dashboard, isolated delayed app document; no app mutation or signing.
|
||||
const fs = require('fs');
|
||||
const { chromium, expect } = require(process.env.PLAYWRIGHT_MODULE || '@playwright/test');
|
||||
(async () => {
|
||||
const origin = process.env.QUALIFICATION_ORIGIN;
|
||||
const cookieFile = process.env.QUALIFICATION_COOKIES;
|
||||
const node = process.env.QUALIFICATION_LABEL || 'node';
|
||||
if (!origin || !cookieFile) throw new Error('Set private node origin and cookie file');
|
||||
const url = new URL(origin), bytes = url.hostname.split('.').map(Number);
|
||||
const privateHost = url.hostname === 'localhost' || (bytes.length === 4 && bytes.every(n => Number.isInteger(n) && n >= 0 && n <= 255) &&
|
||||
(bytes[0] === 127 || bytes[0] === 10 || (bytes[0] === 192 && bytes[1] === 168) || (bytes[0] === 172 && bytes[1] >= 16 && bytes[1] <= 31) || (bytes[0] === 100 && bytes[1] >= 64 && bytes[1] <= 127)));
|
||||
if (!privateHost || !['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.pathname !== '/' || url.search || url.hash) throw new Error('Refusing non-private node origin');
|
||||
const browser = await chromium.connectOverCDP(process.env.BROWSER_CDP || 'http://127.0.0.1:32911');
|
||||
for (const width of [390, 1440]) {
|
||||
const context = await browser.newContext({ viewport: { width, height: 900 }, serviceWorkers: 'block' });
|
||||
let releaseDocument;
|
||||
const heldDocument = new Promise(resolve => { releaseDocument = resolve; });
|
||||
let documentRequests = 0;
|
||||
try {
|
||||
const cookies = JSON.parse(fs.readFileSync(cookieFile, 'utf8'));
|
||||
await context.addCookies(Object.entries(cookies).map(([name, value]) => ({ name, value, url: origin, httpOnly: name !== 'csrf_token' })));
|
||||
await context.addInitScript(() => {
|
||||
localStorage.setItem('neode-auth', 'true');
|
||||
localStorage.setItem('lnd-seed-backup-prompt-snooze-until', String(Date.now() + 3600000));
|
||||
});
|
||||
await context.route('**:7778/**', async route => {
|
||||
if (route.request().resourceType() !== 'document') return route.abort();
|
||||
documentRequests++;
|
||||
await heldDocument;
|
||||
await route.fulfill({ contentType: 'text/html', body: '<!doctype html><html><body><h1 id="ready">Delayed app ready</h1></body></html>' });
|
||||
});
|
||||
// Explicitly deny signing in this loading-only fixture.
|
||||
await context.route('**/rpc/v1', async route => {
|
||||
let r; try { r = route.request().postDataJSON(); } catch { return route.continue(); }
|
||||
if (/nostr-sign|identity.sign|\.(pay|send|spend|melt|withdraw)(-|$)/.test(r?.method || '')) return route.abort();
|
||||
return route.continue();
|
||||
});
|
||||
const page = await context.newPage();
|
||||
await page.goto(origin + '/dashboard/app-session/indeedhub', { waitUntil: 'domcontentloaded' });
|
||||
const frame = page.locator('iframe[src*="7778"]');
|
||||
await expect(frame).toHaveCount(1, { timeout: 30000 });
|
||||
await frame.evaluate(el => { window.__qualificationFrame = el; });
|
||||
const notice = page.getByRole('status').filter({ hasText: 'is taking longer to load' });
|
||||
await expect(notice).toBeVisible({ timeout: 20000 });
|
||||
expect(await frame.evaluate(el => el === window.__qualificationFrame)).toBe(true);
|
||||
await expect(page.getByRole('button', { name: 'Close companion modal', exact: true })).toHaveCount(0);
|
||||
releaseDocument();
|
||||
await expect(page.frameLocator('iframe[src*="7778"]').locator('#ready')).toHaveText('Delayed app ready', { timeout: 15000 });
|
||||
await expect(notice).toHaveCount(0);
|
||||
expect(await frame.evaluate(el => el === window.__qualificationFrame)).toBe(true);
|
||||
// Modify readiness only in this isolated browser's store, never the node.
|
||||
for (const ready of [false, true]) {
|
||||
await page.evaluate(ready => {
|
||||
const app = document.querySelector('#app').__vue_app__.config.globalProperties.$pinia._s.get('app');
|
||||
app.data['package-data'].indeedhub['ui-ready'] = ready;
|
||||
}, ready);
|
||||
await page.waitForTimeout(300);
|
||||
await expect(frame).toHaveCount(1);
|
||||
expect(await frame.evaluate(el => el === window.__qualificationFrame)).toBe(true);
|
||||
}
|
||||
expect(documentRequests).toBe(1);
|
||||
console.log(JSON.stringify({ node, width, result: 'PASS', delayedBeyondDeadline: true, sameIframeRetained: true, readinessFlapRetained: true, companionOfferDeferred: true, documentRequests }));
|
||||
} finally {
|
||||
releaseDocument();
|
||||
await context.close();
|
||||
}
|
||||
}
|
||||
process.exit(0);
|
||||
})().catch(e => { console.error(String(e.message).split('Call log:')[0]); process.exit(1); });
|
||||
@@ -0,0 +1,62 @@
|
||||
// Real, operator-authorized local IndeeHub login. Only NIP-98 session signatures are allowed.
|
||||
// Uses isolated browser contexts, does not publish events or perform wallet operations.
|
||||
const fs=require('fs');const {chromium,expect}=require(process.env.PLAYWRIGHT_MODULE || '@playwright/test');
|
||||
(async()=>{
|
||||
if(process.env.ALLOW_REAL_AUTH_SIGNING!=='1')throw new Error('Set ALLOW_REAL_AUTH_SIGNING=1 only for an authorized real IndeeHub login check');
|
||||
const origin=process.env.QUALIFICATION_ORIGIN,cookieFile=process.env.QUALIFICATION_COOKIES,node=process.env.QUALIFICATION_LABEL||'node';
|
||||
if(!origin||!cookieFile)throw new Error('Set private node origin and cookie file');
|
||||
const u=new URL(origin),octets=u.hostname.split('.').map(Number);
|
||||
const privateHost=u.hostname==='localhost'||(octets.length===4&&octets.every(n=>Number.isInteger(n)&&n>=0&&n<=255)&&(octets[0]===127||octets[0]===10||(octets[0]===192&&octets[1]===168)||(octets[0]===172&&octets[1]>=16&&octets[1]<=31)||(octets[0]===100&&octets[1]>=64&&octets[1]<=127)));
|
||||
if(!privateHost||!['http:','https:'].includes(u.protocol)||u.username||u.password||u.pathname!=='/'||u.search||u.hash)throw new Error('Refusing non-private node origin');
|
||||
u.port='7778';const appOrigin=u.origin;
|
||||
const b=await chromium.connectOverCDP(process.env.BROWSER_CDP||'http://127.0.0.1:32911');
|
||||
for(const width of [390,1440]){
|
||||
const c=await b.newContext({viewport:{width,height:900},serviceWorkers:'block'});
|
||||
let page;
|
||||
const proof={node,width,stage:'setup',realLogin:true,sessionResponses:[],profileResponses:[],signedAuthRequests:0,blockedUnexpectedRequest:false};
|
||||
try{
|
||||
const cookies=JSON.parse(fs.readFileSync(cookieFile,'utf8'));await c.addCookies(Object.entries(cookies).map(([name,value])=>({name,value,url:origin,httpOnly:name!=='csrf_token'})));
|
||||
await c.addInitScript(()=>{localStorage.setItem('neode-auth','true');localStorage.setItem('lnd-seed-backup-prompt-snooze-until',String(Date.now()+3600000));sessionStorage.setItem('indeedhub_splash_shown','true');});
|
||||
await c.route('**/rpc/v1',async route=>{
|
||||
let r;try{r=route.request().postDataJSON()}catch{return route.continue()}
|
||||
if(['identity.nostr-sign','node.nostr-sign'].includes(r?.method)){
|
||||
const e=r.params?.event;let safe=false;try{const u=new URL(e.tags.find(t=>t[0]==='u')[1]);safe=e.kind===27235&&u.origin===appOrigin&&u.pathname==='/api/auth/nostr/session'&&e.tags.some(t=>t[0]==='method'&&t[1]==='POST')}catch{}
|
||||
if(!safe){proof.blockedUnexpectedRequest=true;return route.abort()}
|
||||
proof.signedAuthRequests++;
|
||||
} else if(r?.method==='identity.sign') {
|
||||
if(typeof r.params?.message!=='string'||!/^archipelago-identity:\d+$/.test(r.params.message)){proof.blockedUnexpectedRequest=true;return route.abort()}
|
||||
} else if(/nostr-(encrypt|decrypt)|\.(pay|send|spend|melt|withdraw)(-|$)/.test(r?.method||'')){
|
||||
proof.blockedUnexpectedRequest=true;return route.abort();
|
||||
}
|
||||
return route.continue();
|
||||
});
|
||||
const p=await c.newPage();page=p;p.on('response',r=>{const u=new URL(r.url());if(u.origin!==appOrigin)return;if(u.pathname.endsWith('/auth/nostr/session'))proof.sessionResponses.push(r.status());if(u.pathname.endsWith('/auth/me'))proof.profileResponses.push(r.status())});
|
||||
await p.goto(origin+'/dashboard/app-session/indeedhub',{waitUntil:'domcontentloaded'});
|
||||
proof.stage='identity selection';
|
||||
const authenticate=p.getByRole('button',{name:'Authenticate',exact:true});await expect(authenticate).toBeEnabled({timeout:30000});await authenticate.click();
|
||||
proof.stage='open app sign-in';
|
||||
const frame=p.frameLocator('iframe[src*="7778"]');
|
||||
const signIn=frame.getByRole('button',{name:'Sign In',exact:true});await expect(signIn).toBeVisible({timeout:30000});await signIn.click();
|
||||
proof.stage='choose native signer';
|
||||
const extension=frame.getByRole('button',{name:'Extension',exact:true});
|
||||
try{await expect(extension).toBeVisible({timeout:10000});await extension.click()}catch(e){console.log(JSON.stringify({...proof,stage:'auth choices',buttons:await frame.getByRole('button').allTextContents()}));throw e}
|
||||
proof.stage='approve login consent';
|
||||
const deadline=Date.now()+45000;let approved=0;
|
||||
while(Date.now()<deadline){
|
||||
if(proof.blockedUnexpectedRequest)throw new Error('An unexpected signing/payment request was blocked');
|
||||
if(proof.sessionResponses.some(s=>s===200||s===201)&&proof.profileResponses.includes(200))break;
|
||||
const approve=p.getByRole('button',{name:'Approve',exact:true});
|
||||
if(await approve.isVisible()){if(++approved>6)throw new Error('Unexpected repeated consent');await approve.click();await p.waitForTimeout(800)}else await p.waitForTimeout(250);
|
||||
}
|
||||
expect(proof.signedAuthRequests).toBeGreaterThan(0);expect(proof.sessionResponses.some(s=>s===200||s===201)).toBe(true);expect(proof.profileResponses).toContain(200);
|
||||
await expect(signIn).toHaveCount(0,{timeout:10000});
|
||||
proof.stage='reload authenticated app';
|
||||
const profilesBeforeReload=proof.profileResponses.filter(s=>s===200).length;
|
||||
await p.reload({waitUntil:'domcontentloaded'});
|
||||
await expect.poll(()=>proof.profileResponses.filter(s=>s===200).length,{timeout:30000}).toBeGreaterThan(profilesBeforeReload);
|
||||
await expect(frame.getByRole('button',{name:'Sign In',exact:true})).toHaveCount(0,{timeout:10000});
|
||||
proof.reloadRetainedLogin=true;proof.result='PASS';console.log(JSON.stringify(proof));
|
||||
}catch(e){console.log(JSON.stringify({...proof,result:'FAIL',path:page?new URL(page.url()).pathname:null,buttons:page?await page.getByRole('button').allTextContents():[]}));if(page)await page.screenshot({path:'/tmp/archy-indeehub-login-'+node+'-'+width+'-failure.png'});throw e}finally{await c.close()}
|
||||
}
|
||||
process.exit(0)
|
||||
})().catch(e=>{console.error(String(e.message).split('Call log:')[0]);process.exit(1)});
|
||||
Reference in New Issue
Block a user