72 lines
6.2 KiB
TypeScript
72 lines
6.2 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import EmbeddedPostgres from 'embedded-postgres'
|
|
import pg from 'pg'
|
|
import { mkdtemp, rm, readFile } from 'node:fs/promises'
|
|
import ts from 'typescript'
|
|
import { createRequire } from 'node:module'
|
|
import { getTableColumns } from 'drizzle-orm'
|
|
import { drizzle } from 'drizzle-orm/node-postgres'
|
|
import { servers } from '../src/db/schema.js'
|
|
import { tmpdir } from 'node:os'
|
|
import { join, resolve, dirname } from 'node:path'
|
|
import { createServer } from 'node:net'
|
|
import { randomBytes } from 'node:crypto'
|
|
import { readBoundedFlowSql } from '../src/services/traffic-flow-bounded-sql.js'
|
|
|
|
if(process.argv.length!==2)throw Error('Read source acceptance requires its own disposable cluster and no arguments')
|
|
const directory=await mkdtemp(join(tmpdir(),'mm-flow-read-owned-'))
|
|
const listener=createServer();await new Promise<void>(resolve=>listener.listen(0,'127.0.0.1',resolve))
|
|
const address=listener.address();assert.ok(address&&typeof address!=='string');const port=address.port
|
|
await new Promise<void>(resolve=>listener.close(()=>resolve()))
|
|
const password=randomBytes(24).toString('hex')
|
|
const 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','work_mem=4MB','-c','statement_timeout=10000'],onLog:()=>{},onError:()=>{}})
|
|
let pool:pg.Pool|undefined,started=false
|
|
try{
|
|
await postgres.initialise();await postgres.start();started=true
|
|
pool=new pg.Pool({host:'127.0.0.1',port,user:'mcp_test',password,database:'postgres',max:2})
|
|
const max={maxRows:3,maxEncodedBytes:1024}
|
|
const query='SELECT i AS id,$1::text AS text FROM generate_series(1,3) i ORDER BY i'
|
|
const normal=await pool.query(query,['я'])
|
|
assert.deepEqual(await readBoundedFlowSql(pool,query,['я'],max),normal.rows)
|
|
assert.deepEqual(await readBoundedFlowSql(pool,'SELECT 1 id WHERE false',[],max),[])
|
|
await assert.rejects(readBoundedFlowSql(pool,'SELECT i FROM generate_series(1,4) i',[],max),/превышает/)
|
|
await assert.rejects(readBoundedFlowSql(pool,'SELECT repeat(\'я\',10000) text',[],max),/превышает/)
|
|
await assert.rejects(readBoundedFlowSql(pool,'SELECT repeat(\'a\',600) text FROM generate_series(1,2)',[],max),/превышает/)
|
|
const large='SELECT 9223372036854775807::bigint AS counter,\'2026-10-29T12:00:00Z\'::timestamptz AS at,123456789012345678901234567890::numeric AS exact'
|
|
assert.deepEqual(await readBoundedFlowSql(pool,large,[],max),(await pool.query(large)).rows,'bigint/numeric/timestamp parser behavior is preserved')
|
|
for(const [name,value] of [['__flowRowCount',"'1'::text"],['__flowEncodedBytes',"'1'::text"],['__flowPresent','true']])await assert.rejects(readBoundedFlowSql(pool,`SELECT ${value} AS "${name}"`,[],max),/Reserved NetFlow source column collision/)
|
|
let overflowPayloads=0
|
|
const watched={query:async(sql:string,values:unknown[])=>{const result=await pool!.query(sql,values);overflowPayloads=result.rows.filter(row=>row.__flowPresent===true).length;return result}}
|
|
await assert.rejects(readBoundedFlowSql(watched,'SELECT repeat(\'a\',1000000) text FROM generate_series(1,20)',[],max),/превышает/)
|
|
assert.equal(overflowPayloads,0,'20 MiB source must send only a null sentinel row; no source payload is decoded in Node')
|
|
await pool.query('CREATE TABLE flow_read_growth(text text);INSERT INTO flow_read_growth VALUES(\'a\')')
|
|
let calls=0,growthDecodedRows=0
|
|
const growing={query:async(sql:string,values:unknown[])=>{
|
|
const result=await pool!.query(sql,values);calls++
|
|
if(calls===1)await pool!.query('UPDATE flow_read_growth SET text=repeat(\'a\',100000)')
|
|
if(calls===2)growthDecodedRows=result.rows.filter(row=>row.__flowPresent===true).length
|
|
return result
|
|
}}
|
|
await assert.rejects(readBoundedFlowSql(growing,'SELECT text FROM flow_read_growth',[],max),/превышает/)
|
|
assert.equal(calls,2);assert.equal(growthDecodedRows,0,'Concurrent growth cannot bypass the preflight reservation')
|
|
const columns=Object.entries(getTableColumns(servers)),quote=(s:string)=>'"'+s.replaceAll('"','""')+'"'
|
|
await pool.query('CREATE TABLE servers('+columns.map(([,column])=>quote(column.name)+' '+column.getSQLType()).join(',')+')')
|
|
await pool.query('INSERT INTO servers(id,name,host,created_at,updated_at) VALUES(1,\'selected\',\'192.0.2.1\',\'2026-10-29T00:00:00Z\',\'2026-10-29T00:00:00Z\')')
|
|
const code=ts.transpileModule(await readFile(new URL('../src/services/traffic-flow-server-source.ts',import.meta.url),'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText
|
|
const serverModule={exports:{} as {readBoundedFlowServerRows:(ids:number[])=>Promise<unknown[]>}},localRequire=createRequire(import.meta.url)
|
|
const mocks:Record<string,unknown>={'drizzle-orm':localRequire('drizzle-orm'),'../db/schema.js':{servers},'../db/index.js':{pool},'./traffic-flow-bounded-sql.js':{readBoundedFlowSql}}
|
|
new Function('require','module','exports',code)((id:string)=>{if(!(id in mocks))throw Error('Unexpected dependency '+id);return mocks[id]},serverModule,serverModule.exports)
|
|
assert.deepEqual(await serverModule.exports.readBoundedFlowServerRows([1]),await drizzle(pool).select().from(servers),'Full server rows preserve Drizzle fingerprint inputs including timestamp strings')
|
|
await pool.query('UPDATE servers SET password=repeat(\'x\',5*1024*1024)')
|
|
await assert.rejects(serverModule.exports.readBoundedFlowServerRows([1]),/превышает/)
|
|
console.log('PASS actual owned PostgreSQL: complete rows, empty result, count refusal, UTF-8 byte refusal, cumulative source refusal, parser parity, oversize no-decode sentinel and concurrent-growth fence')
|
|
console.log('PASS actual bounded server source versus Drizzle mapping parity; 5 MiB credential/config text refused before source decode')
|
|
}finally{
|
|
await pool?.end();if(started)await postgres.stop()
|
|
// Only the exact private mkdtemp child is eligible; never shell deletion.
|
|
assert.equal(dirname(resolve(directory)),resolve(tmpdir()))
|
|
assert.equal(directory.includes('mm-flow-read-owned-'),true)
|
|
await rm(directory,{recursive:true,force:true,maxRetries:20,retryDelay:100})
|
|
console.log('Owned source SQL cluster stopped and temporary resources removed')
|
|
}
|