init
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
// Local-only end-to-end smoke test. Requires a built gateway and Chrome/Edge.
|
||||
// Run: go build -o .cache/gateway-smoke.exe ./cmd/gateway
|
||||
// node scripts/browser-smoke.cjs
|
||||
const fs=require('fs'),path=require('path'),net=require('net'),{spawn}=require('child_process');
|
||||
const wait=ms=>new Promise(r=>setTimeout(r,ms));
|
||||
async function port(){const s=net.createServer();await new Promise(r=>s.listen(0,'127.0.0.1',r));const p=s.address().port;await new Promise(r=>s.close(r));return p}
|
||||
async function until(fn,label){for(let i=0;i<100;i++){try{const value=await fn();if(value)return value}catch{}await wait(100)}throw Error('Timeout: '+label)}
|
||||
let gateway,browser,ws,browserLog='';const errors=[];const pending=new Map();let sequence=0;
|
||||
function call(method,params={}){return new Promise((resolve,reject)=>{const id=++sequence;pending.set(id,{resolve,reject});ws.send(JSON.stringify({id,method,params}))})}
|
||||
async function evaluate(expression){const r=await call('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});if(r.exceptionDetails)throw Error(r.exceptionDetails.text+': '+JSON.stringify(r.exceptionDetails.exception));return r.result?.value}
|
||||
(async()=>{
|
||||
const httpPort=await port(),debugPort=await port();const dir=fs.mkdtempSync(path.resolve('.cache/ui-smoke-'));
|
||||
const cfg=JSON.parse(fs.readFileSync('config.example.json','utf8'));cfg.server.listen='127.0.0.1:'+httpPort;cfg.server.session_secret='';cfg.server.admin_password_hash='';
|
||||
cfg.outbounds=cfg.outbounds.filter(o=>['discord','webhook'].includes(o.provider));cfg.ingress.mail=[];cfg.ingress.discord={enabled:false};
|
||||
fs.writeFileSync(path.join(dir,'config.json'),JSON.stringify(cfg));
|
||||
gateway=spawn(path.resolve('.cache/gateway-smoke.exe'),['-config',path.join(dir,'config.json')],{windowsHide:true,env:{...process.env,GATEWAY_ADMIN_PASSWORD:'local-smoke-password'},stdio:'ignore'});
|
||||
const base='http://127.0.0.1:'+httpPort;
|
||||
await until(async()=>{const r=await fetch(base+'/readyz');return r.ok},'gateway readiness');
|
||||
const executable=process.env.BROWSER_PATH||['C:/Program Files/Google/Chrome/Application/chrome.exe','C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'].find(p=>fs.existsSync(p));if(!executable)throw Error('Set BROWSER_PATH');
|
||||
browser=spawn(executable,['--headless=new','--disable-gpu','--no-first-run','--no-default-browser-check','--remote-debugging-port='+debugPort,'--user-data-dir='+path.join(dir,'browser'),'about:blank'],{windowsHide:true,stdio:['ignore','ignore','pipe']});
|
||||
browser.stderr.on('data',b=>{browserLog=(browserLog+b).slice(-4000)});
|
||||
const pages=await until(async()=>{const r=await fetch('http://127.0.0.1:'+debugPort+'/json/list');const j=await r.json();return j.find(p=>p.type==='page')},'browser');
|
||||
ws=new WebSocket(pages.webSocketDebuggerUrl);await new Promise((resolve,reject)=>{ws.onopen=resolve;ws.onerror=reject});
|
||||
ws.onmessage=e=>{const m=JSON.parse(e.data);if(m.id){const p=pending.get(m.id);if(p){pending.delete(m.id);m.error?p.reject(Error(m.error.message)):p.resolve(m.result)}}else if(m.method==='Runtime.exceptionThrown'){errors.push(m.params.exceptionDetails.text)}else if(m.method==='Log.entryAdded'&&m.params.entry.level==='error'){errors.push(m.params.entry.text)}};
|
||||
await call('Runtime.enable');await call('Log.enable');await call('Page.enable');
|
||||
await call('Page.navigate',{url:base+'/login'});await until(()=>evaluate("!!document.querySelector('[name=username]')"),'login form');
|
||||
await evaluate("document.querySelector('[name=username]').value='admin';document.querySelector('[name=password]').value='local-smoke-password';document.querySelector('form').requestSubmit()");
|
||||
await until(()=>evaluate("typeof cfg!=='undefined'&&cfg!==null"),'loaded admin config');
|
||||
await evaluate(`showPage('outbounds');document.querySelector('#addOutbound').click();var card=document.querySelector('[data-output="2"]');var select=card.querySelector('[data-field=provider]');select.value='smtp';select.dispatchEvent(new Event('input'));`);
|
||||
await evaluate(`var card=document.querySelector('[data-output="2"]');for(var [key,value] of Object.entries({smtp_host:'smtp.example.invalid',from:'sender@example.org',to:'receiver@example.org'})){var el=card.querySelector('[data-field='+key+']');el.value=value;el.dispatchEvent(new Event('input'))}showPage('ingress');document.querySelector('#addMail').click();`);
|
||||
await evaluate(`showPage('mappings');document.querySelector('#addMapping').click();document.querySelector('#mName').value='Browser smoke';document.querySelector('#mId').value='browser-smoke';document.querySelector('#mSource').value='webhook';document.querySelector('#mTarget').value='smtp';document.querySelector('#mTarget').dispatchEvent(new Event('change'));document.querySelector('#mOutbound').value=cfg.outbounds[2].id;document.querySelector('#pSource').value='webhook';document.querySelector('#runPreview').click();`);
|
||||
await until(()=>evaluate("document.querySelector('#previewOut').textContent.includes('Rauchentwicklung')"),'SMTP preview');
|
||||
await evaluate("document.querySelector('#applyMapping').click();document.querySelector('#saveAll').click()");
|
||||
await until(()=>evaluate("dirty===false&&cfg.mappings.some(m=>m.id==='browser-smoke')"),'save configuration with CSRF');
|
||||
const accepted=await fetch(base+'/in/webhook/default',{method:'POST',headers:{Authorization:'Bearer replace-me','Content-Type':'application/json','Idempotency-Key':'browser-smoke'},body:JSON.stringify({title:'Test',message:'Smoke test'})});if(accepted.status!==202)throw Error('Ingress: '+accepted.status+' '+await accepted.text());
|
||||
await until(async()=>{await evaluate("showPage('history');loadHistory()");return evaluate("document.querySelector('#historyRows').textContent.includes('Dry-Run')")},'history dry-run result');
|
||||
await evaluate("document.querySelector('[data-attempts]').click()");await until(()=>evaluate("document.querySelector('#attemptHistory').textContent.includes('dry_run')"),'attempt details');
|
||||
const screenshot=await call('Page.captureScreenshot',{format:'png'});fs.writeFileSync(path.join(dir,'history.png'),Buffer.from(screenshot.data,'base64'));
|
||||
if(errors.length)throw Error(errors.join('\n'));
|
||||
console.log('Browser smoke passed: login, SMTP/mail forms, mapping, preview, CSRF save, queued ingress, history.');console.log('Screenshot: '+path.join(dir,'history.png'));
|
||||
await call('Browser.close');
|
||||
})().catch(async e=>{console.error(e.stack);if(errors.length)console.error(errors.join('\n'));if(ws?.readyState===WebSocket.OPEN){try{console.error(await evaluate('JSON.stringify({url:location.href,text:document.body.innerText.slice(0,500)})'))}catch{}}if(browserLog)console.error(browserLog);process.exitCode=1}).finally(()=>{if(ws)ws.close();if(browser)browser.kill();if(gateway)gateway.kill()});
|
||||
Reference in New Issue
Block a user