48 lines
4.2 KiB
JavaScript
48 lines
4.2 KiB
JavaScript
|
|
import {test,afterEach} from 'node:test';
|
||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import http from 'node:http';
|
||
|
|
import {Readable,Writable} from 'node:stream';
|
||
|
|
import {build} from 'esbuild';
|
||
|
|
import {fileURLToPath} from 'node:url';
|
||
|
|
const result=await build({entryPoints:[fileURLToPath(new URL('../firDevProxy.ts',import.meta.url))],bundle:true,write:false,format:'esm',platform:'node'});
|
||
|
|
const {firDevProxy}=await import('data:text/javascript;base64,'+Buffer.from(result.outputFiles[0].text).toString('base64'));
|
||
|
|
const original=http.request;afterEach(()=>http.request=original);
|
||
|
|
function middleware(){let handler;firDevProxy().configureServer({middlewares:{use:fn=>handler=fn}});return handler;}
|
||
|
|
function response(){const chunks=[];const res=new Writable({write(chunk,_enc,done){chunks.push(chunk);done();}});res.setHeader=()=>{};return {res,chunks};}
|
||
|
|
test('development relay preserves both binary payloads and exact device channel paths',async()=>{
|
||
|
|
for(const side of ['left','right']) {
|
||
|
|
let options;const chunks=[];http.request=(opts,callback)=>{
|
||
|
|
options=opts;const upstream=new Writable({write(chunk,_enc,done){chunks.push(chunk);done();}});
|
||
|
|
upstream.setTimeout=()=>{};upstream.on('finish',()=>{const reply=Readable.from(['OK']);reply.statusCode=200;reply.headers={};callback(reply);});return upstream;
|
||
|
|
};
|
||
|
|
const data=Buffer.alloc(32826,side==='left'?0x12:0x34);const req=Readable.from([data]);req.url='/fir-device/192.168.9.133/'+side;req.method='POST';req.headers={'content-length':'32826'};
|
||
|
|
const {res}=response();const finished=new Promise(resolve=>res.on('finish',resolve));middleware()(req,res,()=>assert.fail('unexpected fallthrough'));await finished;
|
||
|
|
assert.equal(options.agent,false);assert.equal(options.hostname,'192.168.9.133');assert.equal(options.path,'/dev/info.cgi?action=fir&channel='+side);assert.equal(options.headers['Content-Type'],'audio/wav');assert.deepEqual(Buffer.concat(chunks),data);assert.equal(res.statusCode,200);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
test('relay refuses public hosts, bad channels, methods and payload sizes',()=>{
|
||
|
|
http.request=()=>assert.fail('invalid request forwarded');
|
||
|
|
for(const [url,method,length,status] of [['/fir-device/8.8.8.8/left','POST',32826,400],['/fir-device/192.168.9.133/both','POST',32826,400],['/fir-device/192.168.9.133/left','DELETE',32826,400],['/fir-device/192.168.9.133/right','POST',8,422]]) {
|
||
|
|
const {res}=response();middleware()({url,method,headers:{'content-length':String(length)}},res,()=>{});assert.equal(res.statusCode,status);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('GET relay reads original device binary with exact ear path and no upload headers',async()=>{
|
||
|
|
let options; http.request=(opts,callback)=>{options=opts; const upstream=new Writable({write(_c,_e,done){done();}});upstream.setTimeout=()=>{};upstream.on('finish',()=>{const reply=Readable.from([Buffer.alloc(32826,1)]);reply.statusCode=200;reply.headers={'content-type':'audio/wav'};callback(reply);});return upstream;};
|
||
|
|
for(const side of ['left','right']){const req=Readable.from([]);req.url='/fir-device/192.168.9.133/'+side;req.method='GET';req.headers={};const {res,chunks}=response();const finished=new Promise(resolve=>res.on('finish',resolve));middleware()(req,res,()=>assert.fail('fallthrough'));await finished;assert.equal(options.agent,false);assert.equal(options.method,'GET');assert.equal(options.path,'/dev/info.cgi?action=fir&channel='+side);assert.deepEqual(options.headers,{});assert.equal(Buffer.concat(chunks).length,32826);}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('complete firmware response survives an invalid trailing HTTP tail', async () => {
|
||
|
|
http.request=(_opts,callback)=>{
|
||
|
|
const upstream=new Writable({write(_c,_e,done){done();}});upstream.setTimeout=()=>{};
|
||
|
|
upstream.on('finish',()=>{
|
||
|
|
const reply=Readable.from([Buffer.alloc(32826,7)]);reply.statusCode=200;reply.headers={'content-type':'audio/wav'};reply.complete=true;
|
||
|
|
callback(reply);upstream.emit('error',new Error('Parse Error: Expected HTTP/'));
|
||
|
|
});return upstream;
|
||
|
|
};
|
||
|
|
const req=Readable.from([]);req.url='/fir-device/192.168.9.133/left';req.method='GET';req.headers={};
|
||
|
|
const {res,chunks}=response();const finished=new Promise(resolve=>res.on('finish',resolve));
|
||
|
|
middleware()(req,res,()=>assert.fail('fallthrough'));await finished;
|
||
|
|
assert.equal(res.statusCode,200);assert.equal(Buffer.concat(chunks).length,32826);
|
||
|
|
});
|