Files
MikrotikManager/backend/scripts/build-database-operator.test.mjs
T

71 lines
8.3 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import {readFile,writeFile,mkdir,copyFile,mkdtemp,rename} from 'node:fs/promises'
import {spawnSync} from 'node:child_process'
import path from 'node:path'
import {fileURLToPath,pathToFileURL} from 'node:url'
import {buildDatabaseOperator,compileOperatorModule,moduleSpecifiers,OPERATOR_SOURCES} from './build-database-operator.mjs'
const backend=process.env.OPERATOR_BUILD_TEST_BACKEND??path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..')
test('AST rewrite changes static/export/dynamic module specifiers and preserves SQL/string values',()=>{
const code=compileOperatorModule(`import {x} from '../src/db/x.js'; export {y} from '../../src/db/y.js'; const module=()=>import('../src/db/z.js'); export const sql="SELECT '../src/db/x.js'";`,'fixture.mts')
assert.deepEqual(moduleSpecifiers(code,'fixture.mjs'),['../dist/db/x.js','../../dist/db/y.js','../dist/db/z.js'])
assert(code.includes(`SELECT '../src/db/x.js'`));assert.throws(()=>compileOperatorModule('const load=name=>import(name)','fixture.mts'),/fixed literal/)
})
test('fixed artifact is reproducible, resolves the compiled backend graph, and runs with plain Node22',async()=>{
assert.equal(Number(process.versions.node.split('.')[0]),22,'Run this acceptance with the production Node22 major')
const temporaryParent=process.env.OPERATOR_BUILD_TEST_TMP??path.resolve(backend,'../tmp');await mkdir(temporaryParent,{recursive:true})
const temporary=await mkdtemp(path.join(temporaryParent,'db-operator-build-')),projectBackend=path.join(temporary,'backend'),output=path.join(projectBackend,'dist-operator')
const packageJson=JSON.parse(await readFile(path.join(backend,'package.json'),'utf8'))
await mkdir(projectBackend,{recursive:true});await writeFile(path.join(projectBackend,'package.json'),JSON.stringify({type:'module',dependencies:packageJson.dependencies}))
const first=await buildDatabaseOperator({backendDirectory:backend,outputDirectory:output}),second=await buildDatabaseOperator({backendDirectory:backend,outputDirectory:output})
assert.deepEqual(first,second);assert.equal(first.modules.length,OPERATOR_SOURCES.length);assert.equal(first.modules.length,19);assert.deepEqual(first.runtimeDependencies,['dotenv','pg','zod']);assert.equal(first.backendDistModules.length,12)
const copied=new Set()
async function copyDist(relative){
if(copied.has(relative))return;copied.add(relative)
const original=path.join(backend,'dist',relative),target=path.join(projectBackend,'dist',relative),source=await readFile(original,'utf8')
await mkdir(path.dirname(target),{recursive:true});await copyFile(original,target)
for(const specifier of moduleSpecifiers(source,relative)){
assert(!specifier.includes('/src/'));assert(!/\.(?:ts|mts)$/.test(specifier))
if(specifier.startsWith('.')){const dependency=path.posix.normalize(path.posix.join(path.posix.dirname(relative),specifier));assert(!dependency.startsWith('../'));await copyDist(dependency)}
else if(!specifier.startsWith('node:')){const name=specifier.startsWith('@')?specifier.split('/').slice(0,2).join('/'):specifier.split('/')[0];assert(Object.hasOwn(packageJson.dependencies,name),'Compiled helper has undeclared production dependency '+name)}
}
}
for(const relative of first.backendDistImports)await copyDist(relative)
assert.deepEqual([...copied].sort(),first.backendDistModules.map(item=>item.file).sort())
// Exercise the real build entry point with its default backend/output paths.
for(const file of OPERATOR_SOURCES){const target=path.join(projectBackend,'scripts',file);await mkdir(path.dirname(target),{recursive:true});await copyFile(path.join(backend,'scripts',file),target)}
const buildScript=path.join(projectBackend,'scripts','build-database-operator.mjs');await copyFile(fileURLToPath(new URL('./build-database-operator.mjs',import.meta.url)),buildScript)
const built=spawnSync(process.execPath,[buildScript],{cwd:temporary,env:{...process.env,NODE_OPTIONS:''},encoding:'utf8',timeout:30000})
assert.equal(built.status,0,built.stderr);assert.equal(JSON.parse(built.stdout.trim()).modules,19)
assert.deepEqual(JSON.parse(await readFile(path.join(output,'build-manifest.json'),'utf8')),first)
const configFile=path.join(projectBackend,'dist','config.js'),originalConfig=await readFile(configFile,'utf8')
for(const injected of ["import 'tsx';","import '../src/config.js';","import '../outside.js';"]){
await writeFile(configFile,originalConfig+'\n'+injected)
await assert.rejects(buildDatabaseOperator({backendDirectory:projectBackend,outputDirectory:output}),/production|Source runtime import|escaped backend\/dist/)
}
await writeFile(configFile,originalConfig)
await writeFile(path.join(output,'unexpected-private.pem'),'not a key; stale-output test')
await assert.rejects(buildDatabaseOperator({backendDirectory:backend,outputDirectory:output}),/Unexpected operator output entry/)
const {unlink}=await import('node:fs/promises');await unlink(path.join(output,'unexpected-private.pem'))
await rename(path.join(projectBackend,'scripts'),path.join(temporary,'build-inputs'))
const guard=path.join(temporary,'runtime-guard.mjs'),telemetry=path.join(temporary,'telemetry.json')
const denied=[...new Set([...Object.keys(packageJson.devDependencies),'typescript','tsx'])]
// Block development imports and connections in the actual plain-Node process.
await writeFile(guard,`import {registerHooks,syncBuiltinESMExports} from 'node:module';import net from 'node:net';import tls from 'node:tls';import {writeFileSync} from 'node:fs';const denied=new Set(${JSON.stringify(denied)});let connections=0;const refuse=()=>{connections++;throw Error('Unexpected operator network connection')};net.Socket.prototype.connect=refuse;tls.connect=refuse;syncBuiltinESMExports();registerHooks({resolve(specifier,context,next){const name=specifier.startsWith('@')?specifier.split('/').slice(0,2).join('/'):specifier.split('/')[0];if(denied.has(name)||/\\.(?:ts|mts)$/.test(specifier)||specifier.includes('/backend/src/'))throw Error('Nonproduction import '+specifier);return next(specifier,context)}});process.on('exit',()=>writeFileSync(${JSON.stringify(telemetry)},JSON.stringify({connections})));`)
const env={...process.env,NODE_ENV:'production',NODE_OPTIONS:'',AUTH_REQUIRED:'false',DOTENV_CONFIG_QUIET:'true'}
// The workspace tmp projection resolves installed packages from its ancestor
// node_modules; the runtime hook rejects every development dependency.
const modulesToImport=first.modules.map(item=>path.join(output,item.file))
const importer=path.join(temporary,'import-all.mjs');await writeFile(importer,`import {pathToFileURL} from 'node:url';for(const file of ${JSON.stringify(modulesToImport)})await import(pathToFileURL(file).href);console.log(JSON.stringify({imported:${modulesToImport.length},readyAuthorized:false}));`)
const imported=spawnSync(process.execPath,['--import',pathToFileURL(guard).href,importer],{cwd:temporary,env,encoding:'utf8',timeout:30000})
assert.equal(imported.status,0,imported.stderr);assert.equal(JSON.parse(imported.stdout.trim()).imported,19);assert.equal(JSON.parse(await readFile(telemetry,'utf8')).connections,0)
const refusals=[]
for(const [entry,event,args] of [['database-writer-bootstrap-operator.mjs','application-writer-bootstrap-refused',['invalid']],['database-writer-profile-operator.mjs','application-writer-profile-publication-refused',['invalid']],['database-writer-profile-operator.mjs','database-maintenance-command-refused',['maintenance-inspect']],['database-writer-profile-operator.mjs','database-maintenance-command-refused',['maintenance-resolve']]]){
const child=spawnSync(process.execPath,['--import',pathToFileURL(guard).href,path.join(output,entry),...args],{cwd:temporary,env,encoding:'utf8',timeout:30000})
assert.equal(child.status,1,child.stderr);assert.equal(child.stdout,'');assert.equal(JSON.parse(child.stderr.trim()).event,event);assert.equal(JSON.parse(await readFile(telemetry,'utf8')).connections,0);refusals.push(event)
}
const receipt={node:process.version,projection:temporary,modules:first.modules.length,compiledBackendModules:copied.size,buildCliExitCode:built.status,importedModules:modulesToImport.length,invalidArgumentsRefused:refusals,developmentDependenciesBlocked:denied,networkConnections:0,productionActivated:false,readyAuthorized:false}
await writeFile(path.join(temporary,'acceptance-receipt.json'),JSON.stringify(receipt,null,2)+'\n');console.log(JSON.stringify(receipt))
})