import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync,lstatSync,readdirSync,chmodSync,symlinkSync,mkdirSync,rmSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { startTesterSocket, testerSocketPath } from '../src/tester-socket.mjs'; import { testerRequest } from '../../cli/src/tester-http.mjs'; async function refused(options, pattern) { const result=await startTesterSocket(options).catch(e=>e); if(!(result instanceof Error)) await new Promise(r=>result.close(r)); assert.ok(result instanceof Error,'unsafe tester bind must refuse');assert.match(result.message,pattern); } test('private tester HTTP serves on 0600 socket in 0700 parent and fixed origin',async t=>{ const root=mkdtempSync(join(tmpdir(),'g196-w-'));t.after(()=>rmSync(root,{recursive:true,force:true})); const web=await startTesterSocket({dataRoot:root,board:'http://127.0.0.1:9'});t.after(()=>new Promise(r=>web.close(r))); assert.equal(web.address(),testerSocketPath(root)); assert.equal(lstatSync(join(root,'console')).mode&0o777,0o700); assert.equal(lstatSync(web.address()).mode&0o777,0o600); assert.equal((await testerRequest({dataRoot:root,path:'/healthz'})).status,200); for(const headers of [{host:'elsewhere.invalid'},{authorization:'Bearer fixture'},{cookie:'__Host-mosaic_session=fixture'},{origin:'http://elsewhere.invalid'}]) assert.equal((await testerRequest({dataRoot:root,path:'/api/config',headers})).status,403); assert.equal((await testerRequest({dataRoot:root,path:'/auth/login'})).status,404); assert.equal((await testerRequest({dataRoot:root,path:'/api/settings',method:'POST',body:{}})).status,403); await refused({dataRoot:root},/tester-socket-exists/); }); test('private tester refuses symlink, unsafe parent and long path without repairs',async t=>{ const root=mkdtempSync(join(tmpdir(),'g196-w-'));t.after(()=>rmSync(root,{recursive:true,force:true})); mkdirSync(join(root,'console'),{mode:0o755}); await refused({dataRoot:root},/tester-socket-unsafe/); chmodSync(join(root,'console'),0o700);rmSync(join(root,'console'),{recursive:true}); symlinkSync(root,join(root,'console')); await refused({dataRoot:root},/tester-socket-unsafe/); await refused({dataRoot:'/tmp/'+ 'a'.repeat(108)},/tester-socket-path/); }); test('tester cleanup must leave a replacement inode untouched',async t=>{ const { createServer }=await import('node:http'); const { writeFileSync,existsSync }=await import('node:fs'); const { bindTesterSocket }=await import('../src/tester-socket.mjs'); const root=mkdtempSync(join(tmpdir(),'g196-clean-'));t.after(()=>rmSync(root,{recursive:true,force:true})); const path=testerSocketPath(root); const server=await bindTesterSocket(createServer(),path); assert.deepEqual(readdirSync(join(root,'console')),['tester.sock'],'temporary name is absent after bind'); rmSync(path);writeFileSync(path,'replacement',{mode:0o600}); await new Promise(r=>server.close(r)); assert.equal(existsSync(path),true,'closing the listener must not unlink a replacement inode'); assert.deepEqual(readdirSync(join(root,'console')),['tester.sock'],'temporary name is absent after close'); });