52 lines
5.1 KiB
JavaScript
52 lines
5.1 KiB
JavaScript
/** Compile and exercise the actual Next route in a disposable frontend project. */
|
|
import assert from 'node:assert/strict';
|
|
import { createServer } from 'node:http';
|
|
import { mkdtemp, mkdir, copyFile, writeFile, symlink, rm } from 'node:fs/promises';
|
|
import { join, resolve } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { spawn } from 'node:child_process';
|
|
import { setTimeout as pause } from 'node:timers/promises';
|
|
|
|
const source=resolve(import.meta.dirname,'../..'),directory=await mkdtemp(join(tmpdir(),'endoleap-proxy-check-')),received=[];
|
|
const account='10000000-0000-4000-8000-000000000004',key='90000000-0000-4000-8000-000000000002';
|
|
const responseBytes=' { "protocol_version":2, "request_id":"'+key+'", "status":"not-applied", "terminal":true, "error":"fixture_rejection", "extension":{"retained":true} }\n';
|
|
const upstream=createServer(async(request,response)=>{
|
|
const chunks=[];for await(const chunk of request)chunks.push(chunk);
|
|
received.push({path:request.url,body:Buffer.concat(chunks),headers:request.headers});
|
|
response.writeHead(request.url.endsWith('/model/operations')?409:200,{'Content-Type':'application/json'});
|
|
response.end(request.url.endsWith('/model/operations')?responseBytes:'{"id":"fixture"}');
|
|
});
|
|
await new Promise(resolve=>upstream.listen(0,'127.0.0.1',resolve));
|
|
const reservation=createServer();await new Promise(resolve=>reservation.listen(0,'127.0.0.1',resolve));const port=reservation.address().port;await new Promise(resolve=>reservation.close(resolve));
|
|
let next,logs='',launchError;
|
|
try{
|
|
for(const filename of ['package.json','tsconfig.json','src/app/api/foundry/[...path]/route.ts','src/lib/canvas-response.ts','src/lib/session-cookie.ts']){
|
|
const target=join(directory,filename);await mkdir(resolve(target,'..'),{recursive:true});await copyFile(join(source,filename),target);
|
|
}
|
|
await symlink(join(source,'node_modules'),join(directory,'node_modules'));
|
|
await writeFile(join(directory,'src/app/layout.tsx'),'export default function Layout({children}:{children:React.ReactNode}){return <html><body>{children}</body></html>}');
|
|
await writeFile(join(directory,'src/app/page.tsx'),'export default function Page(){return <main>Proxy verification fixture</main>}');
|
|
next=spawn(process.execPath,[join(source,'node_modules/next/dist/bin/next'),'dev','--webpack','--hostname','127.0.0.1','--port',String(port)],{cwd:directory,detached:true,env:{...process.env,NEXT_TELEMETRY_DISABLED:'1',FOUNDRY_API_ORIGIN:`http://127.0.0.1:${upstream.address().port}`,NEXT_PUBLIC_FOUNDRY_PROJECT:'fixture'},stdio:['ignore','pipe','pipe']});
|
|
next.stdout.on('data',chunk=>{logs=(logs+chunk).slice(-12000)});next.stderr.on('data',chunk=>{logs=(logs+chunk).slice(-12000)});next.on('error',error=>{launchError=error});
|
|
let ready=false;
|
|
for(let attempt=0;attempt<90;attempt++){
|
|
if(launchError)throw launchError;if(next.exitCode!==null)throw new Error('Next exited during startup:\n'+logs);
|
|
try{const response=await fetch(`http://127.0.0.1:${port}/api/foundry/profile`,{signal:AbortSignal.timeout(1500)});await response.text();if(response.status===200){ready=true;break}}catch{}
|
|
await pause(250);
|
|
}
|
|
if(!ready)throw new Error('Next did not become ready:\n'+logs);
|
|
const path='workspaces/10000000-0000-4000-8000-000000000003/materials/10000000-0000-4000-8000-000000000001/canvas/model/operations';
|
|
// More than the generic 1 MiB proxy cap, below the canonical 5 MiB cap.
|
|
const body=' {\n "protocol_version":2,"request_id":"'+key+'","fixture":"'+ 'x'.repeat(1100000)+'"\n} ';
|
|
const response=await fetch(`http://127.0.0.1:${port}/api/foundry/${path}`,{method:'POST',body,headers:{'Content-Type':'application/json','Idempotency-Key':key,'X-Expected-Account':account,Cookie:'foundry_session=fixture-session',Origin:`http://127.0.0.1:${port}`},signal:AbortSignal.timeout(20000)});
|
|
assert.equal(response.status,409);assert.equal(await response.text(),responseBytes);
|
|
const request=received.find(item=>item.path.endsWith('/model/operations'));
|
|
assert.ok(request);assert.equal(request.body.toString(),body);assert.equal(request.headers['idempotency-key'],key);assert.equal(request.headers['x-expected-account'],account);assert.equal(request.headers.authorization,'Bearer fixture-session');
|
|
const before=received.length,invalid=await fetch(`http://127.0.0.1:${port}/api/foundry/${path}/unexpected/extra`,{signal:AbortSignal.timeout(10000)});await invalid.text();assert.equal(invalid.status,404);assert.equal(received.length,before);
|
|
console.log('PASS actual Next proxy preserves frozen request bytes/key/account and raw terminal receipt above 1 MiB');
|
|
console.log('PASS canonical route allowlist rejects unexpected paths without contacting the upstream');
|
|
}finally{
|
|
if(next?.pid){const exited=new Promise(resolve=>{if(next.exitCode!==null)resolve();else next.once('exit',resolve)});try{process.kill(-next.pid,'SIGTERM')}catch{}await Promise.race([exited,pause(3000)]);if(next.exitCode===null){try{process.kill(-next.pid,'SIGKILL')}catch{}}}
|
|
upstream.closeAllConnections();await new Promise(resolve=>upstream.close(resolve));await rm(directory,{recursive:true,force:true,maxRetries:5});
|
|
}
|