93 lines
7.0 KiB
JavaScript
93 lines
7.0 KiB
JavaScript
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import http from 'node:http'
|
|
import { mkdtemp, mkdir, writeFile, readFile, symlink, rm, stat, chmod } from 'node:fs/promises'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
import { createApp } from './app.mjs'
|
|
const password='test-only-password-123456'
|
|
async function fixture(t) {
|
|
const dir=await mkdtemp(path.join(os.tmpdir(),'deskweb-test-'))
|
|
await mkdir(path.join(dir,'dist'));await writeFile(path.join(dir,'dist/index.html'),'<html>DeskWEB test</html>')
|
|
await mkdir(path.join(dir,'output'))
|
|
const requests=[]
|
|
const upstream=http.createServer(async(req,res)=>{
|
|
const chunks=[];for await(const chunk of req)chunks.push(chunk)
|
|
requests.push({url:req.url,method:req.method,authorization:req.headers.authorization,body:Buffer.concat(chunks).toString()})
|
|
res.setHeader('Content-Type','application/json');res.setHeader('Set-Cookie','upstream-secret=hidden')
|
|
if(req.url==='/api/fail'){res.writeHead(422);res.end('{"detail":"native validation"}');return}
|
|
res.end('{"status":"ok"}')
|
|
})
|
|
await new Promise(r=>upstream.listen(0,'127.0.0.1',r))
|
|
const app=await createApp({password,backendUrl:`http://127.0.0.1:${upstream.address().port}`,backendToken:'test-upstream-token',dist:path.join(dir,'dist'),roots:[{local:path.join(dir,'input'),backend:'/ltx/inputs',writable:true},{local:path.join(dir,'output'),backend:'/ltx/outputs'}],uploadLimit:1024})
|
|
await new Promise(r=>app.listen(0,'127.0.0.1',r))
|
|
t.after(async()=>{app.closeAllConnections();upstream.closeAllConnections();await Promise.all([new Promise(r=>app.close(r)),new Promise(r=>upstream.close(r))]);await rm(dir,{recursive:true,force:true})})
|
|
const base=`http://127.0.0.1:${app.address().port}`
|
|
const login=await fetch(base+'/web/login',{method:'POST',headers:{'X-Deskweb-Request':'1'},body:JSON.stringify({password})})
|
|
assert.equal(login.status,200)
|
|
const cookie=login.headers.get('set-cookie').split(';')[0]
|
|
const call=(route,init={})=>fetch(base+route,{...init,headers:{Cookie:cookie,'X-Deskweb-Request':'1',...init.headers}})
|
|
return {dir,base,call,requests,cookie}
|
|
}
|
|
test('sign-in required; secret never returned; logout invalidates session',async t=>{
|
|
const {base,call}=await fixture(t)
|
|
assert.equal((await fetch(base+'/web/session')).status,401)
|
|
const session=await call('/web/session');assert.equal(session.status,200);assert.ok(!(await session.text()).includes('test-upstream-token'))
|
|
assert.equal((await call('/web/logout',{method:'POST'})).status,200)
|
|
assert.equal((await call('/web/session')).status,401)
|
|
})
|
|
test('native API bytes, query, method and validation errors pass through; browser token replaced',async t=>{
|
|
const {call,requests}=await fixture(t)
|
|
const body='{"prompt":"synthetic test","width":768,"custom_native_field":true}'
|
|
const response=await call('/ltx/api/generate?native=1',{method:'POST',headers:{Authorization:'Bearer browser-value','Content-Type':'application/json'},body})
|
|
assert.equal(response.status,200);assert.equal(response.headers.get('set-cookie'),null)
|
|
assert.deepEqual(requests[0],{url:'/api/generate?native=1',method:'POST',authorization:'Bearer test-upstream-token',body})
|
|
const invalid=await call('/ltx/api/fail');assert.equal(invalid.status,422);assert.equal(await invalid.text(),'{"detail":"native validation"}')
|
|
})
|
|
test('reject cross-site and arbitrary upstream routes',async t=>{
|
|
const {call}=await fixture(t)
|
|
assert.equal((await call('/ltx/api/generate',{method:'POST',headers:{Origin:'https://evil.example'}})).status,403)
|
|
assert.equal((await call('/web/media?path=/ltx/outputs/a.mp4',{headers:{'Sec-Fetch-Site':'cross-site'}})).status,403)
|
|
assert.equal((await call('/ltx//evil.example/api/generate')).status,404)
|
|
})
|
|
test('upload, backend path, range playback and existence check',async t=>{
|
|
const {call,dir}=await fixture(t)
|
|
const upload=await call('/web/upload?name=clip.mp4',{method:'POST',body:'0123456789'})
|
|
assert.equal(upload.status,201);const result=await upload.json();assert.ok(result.path.startsWith('/ltx/inputs/'))
|
|
assert.equal((await stat(path.join(dir,'input',path.basename(result.path)))).mode & 0o777,0o640)
|
|
assert.equal(await readFile(path.join(dir,'input',path.basename(result.path)),'utf8'),'0123456789')
|
|
const media=await call('/web/media?path='+encodeURIComponent(result.path),{headers:{Range:'bytes=2-5'}})
|
|
assert.equal(media.status,206);assert.equal(media.headers.get('content-range'),'bytes 2-5/10');assert.equal(await media.text(),'2345')
|
|
const exists=await call('/web/files',{method:'POST',body:JSON.stringify({action:'exists',paths:[result.path,'/etc/passwd']})})
|
|
assert.deepEqual(await exists.json(),{[result.path]:true,'/etc/passwd':false})
|
|
})
|
|
test('prevent file traversal, symlink escape, active file uploads, oversized uploads',async t=>{
|
|
const {call,dir}=await fixture(t)
|
|
await writeFile(path.join(dir,'secret.mp4'),'hidden');await symlink(path.join(dir,'secret.mp4'),path.join(dir,'output','link.mp4'))
|
|
for(const p of ['/etc/passwd','/ltx/outputs/../../secret.mp4','/ltx/outputs/link.mp4'])assert.equal((await call('/web/media?path='+encodeURIComponent(p))).status,403)
|
|
assert.equal((await call('/web/upload?name=script.html',{method:'POST',body:'<script/>'})).status,400)
|
|
assert.equal((await call('/web/upload?name=big.mp4',{method:'POST',body:'x'.repeat(1025)})).status,413)
|
|
})
|
|
test('login rate limit',async t=>{
|
|
const {base}=await fixture(t)
|
|
for(let i=0;i<10;i++)assert.equal((await fetch(base+'/web/login',{method:'POST',headers:{'X-Deskweb-Request':'1'},body:JSON.stringify({password:'wrong'})})).status,401)
|
|
assert.equal((await fetch(base+'/web/login',{method:'POST',headers:{'X-Deskweb-Request':'1'},body:JSON.stringify({password})})).status,429)
|
|
})
|
|
test('shared media can be inspected, copied and thumbnailed without an inference runtime',async t=>{
|
|
const {call,dir}=await fixture(t)
|
|
// Deliberately tiny synthetic PPM pixels; ffprobe detects content, not extension.
|
|
await writeFile(path.join(dir,'output','test.png'),Buffer.concat([Buffer.from('P6\n2 2\n255\n'),Buffer.from([0,0,255,0,0,255,0,0,255,0,0,255])]))
|
|
await chmod(path.join(dir,'output','test.png'),0o600)
|
|
const invoke=async action=>{
|
|
const response=await call('/web/files',{method:'POST',body:JSON.stringify({action,path:'/ltx/outputs/test.png'})})
|
|
assert.equal(response.status,200);return response.json()
|
|
}
|
|
const metadata=await invoke('metadata');assert.equal(metadata.width,2);assert.equal(metadata.height,2)
|
|
const copy=await invoke('copy');assert.ok(copy.path.startsWith('/ltx/inputs/'))
|
|
assert.equal((await stat(path.join(dir,'input',path.basename(copy.path)))).mode & 0o777,0o640)
|
|
const thumbnail=await invoke('thumbnail');assert.ok(thumbnail.path.endsWith('.jpg'))
|
|
assert.equal((await stat(path.join(dir,'input',path.basename(thumbnail.path)))).mode & 0o777,0o640)
|
|
const response=await call('/web/media?path='+encodeURIComponent(thumbnail.path));assert.equal(response.status,200)
|
|
assert.equal(response.headers.get('content-type'),'image/jpeg');assert.ok((await response.arrayBuffer()).byteLength>100)
|
|
})
|