Files
dsh-worktable/04_test/preview-files.test.mjs
T

108 lines
5.6 KiB
JavaScript

import assert from 'node:assert/strict'
import { test, after, afterEach } from 'node:test'
import { createRequire } from 'node:module'
import { mkdtempSync, writeFileSync, mkdirSync, rmSync, symlinkSync, renameSync, truncateSync, unlinkSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { pathToFileURL, fileURLToPath } from 'node:url'
const require=createRequire(new URL('../01_content/package.json',import.meta.url))
const {build}=require('esbuild')
const temp=mkdtempSync(join(tmpdir(),'wt-preview-test-'))
after(()=>rmSync(temp,{recursive:true,force:true}))
// Instrument filesystem scheduling only in the test bundle; production has no hooks.
afterEach(()=>{ delete globalThis.__wtPreviewHooks })
await build({
entryPoints:[fileURLToPath(new URL('../01_content/src/previewFiles.ts',import.meta.url))],
bundle:true,platform:'node',format:'esm',outfile:join(temp,'preview.mjs'),
plugins:[{name:'filesystem-race-fixture',setup(build){
build.onResolve({filter:/^node:fs\/promises$/},args=>args.namespace==='race-fixture'?undefined:{path:args.path,namespace:'race-fixture'})
build.onLoad({filter:/.*/,namespace:'race-fixture'},()=>({loader:'js',contents:`
import * as fs from 'node:fs/promises';
export const realpath=fs.realpath, stat=fs.stat;
export async function open(...args) {
await globalThis.__wtPreviewHooks?.beforeOpen?.();
const h=await fs.open(...args);
await globalThis.__wtPreviewHooks?.afterOpen?.();
const read=h.read.bind(h);
h.read=async(...args)=>{await globalThis.__wtPreviewHooks?.beforeRead?.();return read(...args)};
return h;
}`}))
}}],
})
const {createPreviewFiles,PREVIEW_MAX_BYTES}=await import(pathToFileURL(join(temp,'preview.mjs')).href)
const root=join(temp,'root'),outside=join(temp,'outside');mkdirSync(root);mkdirSync(outside)
writeFileSync(join(root,'index.html'),'fixture');writeFileSync(join(outside,'outside.txt'),'outside')
test('Preview reads only its canonical root, including directory indexes',async()=>{
const store=createPreviewFiles(),token=await store.mount(root)
assert.equal((await store.file(token,[])).path,join(root,'index.html'))
assert.equal(await store.file('invented-token',['index.html']),null)
for(const parts of [['..','outside','outside.txt'],['..\\outside\\outside.txt'],['/outside'],['bad\0']]) assert.equal(await store.file(token,parts),null)
symlinkSync(outside,join(root,'escape'),process.platform==='win32'?'junction':'dir')
assert.equal(await store.file(token,['escape','outside.txt']),null)
rmSync(join(root,'escape'))
})
test('Capabilities expire after idle time; revocation and disposal remove them',async()=>{
let now=0;const store=createPreviewFiles(()=>now),a=await store.mount(root)
now=29*60*1000;assert.ok(await store.file(a,['index.html']))
now+=30*60*1000;assert.equal(await store.file(a,['index.html']),null)
const b=await store.mount(root);store.revoke(b);assert.equal(await store.file(b,[]),null)
const c=await store.mount(root);store.clear();assert.equal(await store.file(c,[]),null)
})
test('Reject relative roots and bound the live capability table',async()=>{
const store=createPreviewFiles();await assert.rejects(store.mount('.'),/absolute/)
for(let i=0;i<128;i++)await store.mount(root)
await assert.rejects(store.mount(root),/Too many/)
})
test('Read content through the checked handle; HEAD omits the body and enforces the limit',async()=>{
const store=createPreviewFiles(),token=await store.mount(root)
assert.equal((await store.file(token,['index.html'])).data.toString(),'fixture')
const head=await store.file(token,['index.html'],true)
assert.equal(head.size,7);assert.equal(head.data,undefined)
const large=join(root,'large.bin');writeFileSync(large,'x');truncateSync(large,PREVIEW_MAX_BYTES+1)
await assert.rejects(store.file(token,['large.bin'],true),e=>e.status===413)
})
test('A directory replaced by a junction after path checking cannot return outside content',async()=>{
const slot=join(root,'slot'),moved=join(root,'slot-original')
mkdirSync(slot);writeFileSync(join(slot,'outside.txt'),'inside')
const store=createPreviewFiles(),token=await store.mount(root)
globalThis.__wtPreviewHooks={beforeOpen(){
delete globalThis.__wtPreviewHooks
renameSync(slot,moved);symlinkSync(outside,slot,process.platform==='win32'?'junction':'dir')
}}
try {assert.equal(await store.file(token,['slot','outside.txt']),null)}
finally {unlinkSync(slot);renameSync(moved,slot)}
})
test('Replacing the named file after open rejects the response, even with an open old handle',async()=>{
const name=join(root,'replace.txt'),old=join(root,'replace-original.txt')
writeFileSync(name,'original')
const store=createPreviewFiles(),token=await store.mount(root)
globalThis.__wtPreviewHooks={afterOpen(){
delete globalThis.__wtPreviewHooks
renameSync(name,old);writeFileSync(name,'replacement')
}}
assert.equal(await store.file(token,['replace.txt']),null)
})
test('Growth after stat cannot produce a response over 40 MiB',async()=>{
const name=join(root,'growing.bin');writeFileSync(name,'small')
const store=createPreviewFiles(),token=await store.mount(root)
globalThis.__wtPreviewHooks={beforeRead(){
delete globalThis.__wtPreviewHooks
truncateSync(name,PREVIEW_MAX_BYTES+1)
}}
await assert.rejects(store.file(token,['growing.bin']),e=>e.status===413)
})
test('Revocation during a pending read prevents the response',async()=>{
const store=createPreviewFiles(),token=await store.mount(root)
globalThis.__wtPreviewHooks={beforeRead(){
delete globalThis.__wtPreviewHooks
store.revoke(token)
}}
assert.equal(await store.file(token,['index.html']),null)
})