Files
MikrotikManager/backend/scripts/test-flow-settings-read-profiles-owned.mjs

61 lines
5.2 KiB
JavaScript

import EmbeddedPostgres from 'embedded-postgres'
import { build,stop } from 'esbuild'
import { mkdtemp,readFile,rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { resolve,join,dirname } from 'node:path'
import { pathToFileURL } from 'node:url'
import { randomBytes } from 'node:crypto'
import { createServer } from 'node:net'
import { spawn } from 'node:child_process'
import assert from 'node:assert/strict'
if(process.argv.length!==2)throw Error('Profile settings tests accept no database or target arguments')
const terminal=child=>child.exitCode!==null||child.signalCode!==null||child.pid===undefined
async function stopOwnedChild(child,graceful=false){
if(!child||terminal(child))return
await new Promise((done,reject)=>{
let timer,forceTimer
const finish=()=>{clearTimeout(timer);clearTimeout(forceTimer);child.removeListener('exit',finish);done()}
child.once('exit',finish)
const force=()=>{
if(terminal(child)){finish();return}
child.kill('SIGKILL')
forceTimer=setTimeout(()=>{if(terminal(child))finish();else{child.removeListener('exit',finish);reject(Error('Owned child failed to reach confirmed terminal state: '+child.pid))}},10000)
}
timer=setTimeout(force,graceful?5000:10000)
if(graceful&&child.connected)child.send({type:'shutdown'},error=>{if(error&&!terminal(child))child.kill()})
else child.kill()
if(terminal(child))finish()
})
}
const repo=resolve(import.meta.dirname,'../..'),bundle=await mkdtemp(join(repo,'tmp','mm-profile-cold-bundle-'))
try{
const compiled=join(bundle,'case.mjs'),driverFile=resolve(repo,'backend/scripts/flow-read-profile-http-client.mjs')
await build({entryPoints:[resolve(repo,'backend/src/services/traffic-flow-settings-read-profile.owned.test.ts')],outfile:compiled,platform:'node',format:'esm',bundle:true,packages:'external',banner:{js:"import{createRequire as __req}from'node:module';const require=__req(import.meta.url);"},plugins:[{name:'current-source',setup(b){b.onResolve({filter:/^@mmapp\/contracts(?:\/.*)?$/},a=>({path:resolve(repo,'packages/contracts/src',a.path.replace(/^@mmapp\/contracts\/?/,'')||'index')+'.ts'}));b.onLoad({filter:/\.ts$/},async a=>({contents:(await readFile(a.path,'utf8')).replaceAll('import.meta.url',JSON.stringify(pathToFileURL(a.path).href)),loader:'ts'}))}}]})
for(const profile of ['standard','constrained']){
const directory=await mkdtemp(join(tmpdir(),'mm-flow-profile-cold-owned-')),listener=createServer()
await new Promise(resolve=>listener.listen(0,'127.0.0.1',resolve));const port=listener.address().port;await new Promise(resolve=>listener.close(resolve))
const password=randomBytes(24).toString('hex'),postgres=new EmbeddedPostgres({databaseDir:join(directory,'pg'),port,user:'mcp_test',password,persistent:true,createPostgresUser:false,initdbFlags:['--encoding=UTF8'],postgresFlags:['-h','127.0.0.1','-c','shared_buffers=64MB','-c','work_mem=2MB'],onLog:()=>{},onError:()=>{}})
let started=false,client,app
try{
await postgres.initialise();await postgres.start();started=true
await new Promise((done,reject)=>{client=spawn(process.execPath,[driverFile],{cwd:directory,windowsHide:true,stdio:['ignore','inherit','inherit','ipc'],env:{...process.env,NODE_OPTIONS:'--max-old-space-size=192 --max-semi-space-size=16'}});client.on('message',message=>{if(message.type==='client-ready')done();if((message.type==='client-result'||message.type==='client-progress')&&app?.connected)app.send(message)});client.once('error',reject);client.once('exit',code=>{if(!app)reject(Error('Client startup exit '+code))})})
const outcome=await new Promise((done,reject)=>{
app=spawn(process.execPath,[compiled],{cwd:directory,windowsHide:true,stdio:['ignore','inherit','inherit','ipc'],env:{...process.env,NODE_OPTIONS:'--max-old-space-size=192 --max-semi-space-size=16',MM_FLOW_READ_RESOURCE_PROFILE:profile,MMAPP_OWN_FLOW_PROFILE_TEST:'true',DATABASE_URL:`postgresql://mcp_test:${password}@127.0.0.1:${port}/postgres`,DATABASE_PATH:join(directory,'unused.sqlite'),MCP_STATE_DIR:join(directory,'state'),AUTH_REQUIRED:'true',AUTH_ISSUER:'https://auth.test.local',AUTH_JWT_SECRET:'owned-profile-only',MCP_PUBLIC_URL:'http://localhost:8000/mcp',NODE_ENV:'test'}})
const timeout=setTimeout(()=>reject(Error('Profile cold test timed out')),120000)
app.on('message',message=>{if(message.type==='client-command')client.send(message)})
app.once('error',error=>{clearTimeout(timeout);reject(error)});app.once('exit',code=>{clearTimeout(timeout);done(code??1)})
})
assert.equal(outcome,0,profile+' public/settings profile failed')
}finally{
const stopped=await Promise.allSettled([stopOwnedChild(app),stopOwnedChild(client,true)])
for(const result of stopped)if(result.status==='rejected')throw result.reason
if(started)await postgres.stop()
assert.equal(dirname(resolve(directory)),resolve(tmpdir()));assert.match(directory,/mm-flow-profile-cold-owned-/);await rm(directory,{recursive:true,force:true,maxRetries:20,retryDelay:100})
}
}
}finally{stop();assert.equal(dirname(resolve(bundle)),resolve(repo,'tmp'));await rm(bundle,{recursive:true,force:true,maxRetries:20,retryDelay:100})}
// All owned children and files are confirmed closed above; avoid library ambient handles keeping the CLI alive.
console.log('PASS both owned profile fixtures and confirmed child cleanup')
process.exit(0)