Files
MikrotikManager/backend/scripts/mcp-external-process/bridge-vlan-fixture.ts
T

85 lines
7.2 KiB
TypeScript

import assert from 'node:assert/strict'
import { spawn } from 'node:child_process'
import { createServer } from 'node:http'
import { writeFile } from 'node:fs/promises'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { isAbsolute, resolve } from 'node:path'
assert.equal(process.env.OWN_EXTERNAL_PROCESS_PG, 'true')
assert.equal(process.env.NODE_ENV, 'test')
assert(isAbsolute(process.env.OWN_EXTERNAL_SOURCE_ROOT!))
assert(isAbsolute(process.env.OWN_EXTERNAL_RESULT_FILE!))
const database = new URL(process.env.DATABASE_URL!)
assert.equal(database.hostname, '127.0.0.1'); assert.equal(database.username, 'mcp_test')
assert.equal(database.pathname, '/postgres'); assert.notEqual(database.port, '5432')
const source = (path: string) => pathToFileURL(resolve(process.env.OWN_EXTERNAL_SOURCE_ROOT!, 'backend/src', path)).href
const rows: Record<string, Record<string, string>[]> = {
'/interface/ethernet': [{ '.id': '*2', name: 'ether2', disabled: 'false' }, { '.id': '*3', name: 'ether3', disabled: 'false' }, { '.id': '*4', name: 'ether-mgmt', disabled: 'false' }],
'/interface/bridge': [{ '.id': '*1', name: 'br-users', pvid: '1', 'vlan-filtering': 'false', 'frame-types': 'admit-all', 'ingress-filtering': 'true', mvrp: 'false', disabled: 'false' }],
'/interface/bridge/port': [{ '.id': '*10', bridge: 'br-users', interface: 'ether2', pvid: '10', 'ingress-filtering': 'true', 'frame-types': 'admit-all', disabled: 'false' }],
'/interface/bridge/vlan': [{ '.id': '*11', bridge: 'br-users', 'vlan-ids': '10', tagged: 'br-users', untagged: 'ether2', disabled: 'false', dynamic: 'false' }],
'/interface/vlan': [], '/interface/gre': [], '/interface/wireguard': [], '/interface/vxlan': [],
'/ip/address': [{ '.id': '*20', interface: 'ether-mgmt', address: '127.0.0.1/8', disabled: 'false', dynamic: 'false' }], '/ipv6/address': [],
}
const requests: string[] = []
let writeAttempts = 0
const router = createServer((req, res) => {
res.setHeader('content-type', 'application/json')
const path = decodeURIComponent(new URL(req.url!, 'http://127.0.0.1').pathname.replace(/^\/rest/, ''))
requests.push(req.method + ' ' + path)
if (req.method !== 'GET') { writeAttempts++; res.writeHead(405); res.end('{}'); return }
if (req.headers.authorization !== 'Basic ' + Buffer.from('fixture:fixture-only').toString('base64')) { res.writeHead(401); res.end('{}'); return }
if (!Object.hasOwn(rows, path)) { res.writeHead(404); res.end('{}'); return }
res.end(JSON.stringify(rows[path]))
})
await new Promise<void>(done => router.listen(0, '127.0.0.1', done))
const { pool, closePool } = await import(source('db/index.js'))
const { applySqlMigrations } = await import(source('db/migrate.js'))
const { mcpConnectionCreateSchema } = await import('@mmapp/contracts/mcp')
const { createMcpConnection, updateMcpSettings } = await import(source('modules/mcp/store.js'))
const { buildApp } = await import(source('index.js'))
const actor = { id: 'owned-bridge-vlan-sdk-owner', email: 'bridge-sdk@example.invalid', name: 'Owned bridge VLAN SDK owner', apps: ['mm'], permissions: ['mm:settings:admin', 'mm:mcp:admin', 'mm:network:admin', 'mm:servers:admin'], expiresAt: Date.now() + 3600000, issuedAt: Date.now() - 1000 }
let app: Awaited<ReturnType<typeof buildApp>> | undefined
let child: ReturnType<typeof spawn> | undefined
let childExit: Promise<number> | undefined
try {
await applySqlMigrations(pool)
const serverId = (await pool.query("INSERT INTO servers(name,host,port,use_ssl,enabled,username,password) VALUES('owned bridge VLAN SDK','127.0.0.1',$1,false,true,'fixture','fixture-only') RETURNING id", [(router.address() as { port: number }).port])).rows[0].id
await updateMcpSettings({ enabled: true }, actor)
app = await buildApp({ startScheduler: false, logger: false })
await app.listen({ host: '127.0.0.1', port: 0 })
const endpoint = `http://127.0.0.1:${(app.server.address() as { port: number }).port}/mcp`
const issued = await createMcpConnection(mcpConnectionCreateSchema.parse({ name: 'Owned bridge VLAN external SDK', mode: 'autonomous',
expiresAt: new Date(Date.now() + 1200000).toISOString(), permissions: ['mm:network:write'],
scope: { serverIds: [serverId], operationIds: ['network.bridge-vlan.read', 'network.bridge-vlan.configure', 'changes.apply'], allowGlobal: false },
limits: { callsPerMinute: 120, mutationsPerHour: 100 } }), actor)
let result: unknown, stdout = '', stderr = ''
child = spawn(process.execPath, [fileURLToPath(new URL('./bridge-vlan-client.mjs', import.meta.url))], { cwd: process.cwd(), windowsHide: true,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'], env: { ...Object.fromEntries(['SystemRoot', 'WINDIR', 'TEMP', 'TMP', 'PATH', 'Path'].filter(k => process.env[k]).map(k => [k, process.env[k]!])),
OWN_EXTERNAL_CLIENT: 'true', OWN_MCP_ENDPOINT: endpoint, OWN_MCP_TOKEN: issued.token, OWN_SERVER_PID: String(process.pid), OWN_ALLOWED_SERVER: String(serverId) } })
child.on('message', (message: { kind: string; result?: unknown }) => { if (message.kind === 'result') result = message.result })
child.stdout?.on('data', chunk => { stdout += String(chunk); if (stdout.length > 1048576) child?.kill() })
child.stderr?.on('data', chunk => { stderr += String(chunk); if (stderr.length > 1048576) child?.kill() })
childExit = new Promise<number>((done, reject) => { child!.once('exit', code => done(code ?? 1)); child!.once('error', reject) })
const timeout = setTimeout(() => child?.kill(), 60000)
const code = await childExit; clearTimeout(timeout)
assert.equal(code, 0, stderr.slice(0, 3000)); assert(result)
assert.equal(writeAttempts, 0)
assert.equal((await pool.query('SELECT count(*)::int AS count FROM mcp_operations')).rows[0].count, 0)
const plans = (await pool.query('SELECT operation_id,state,approved_by FROM mcp_plans')).rows
assert.equal(plans.length, 1); assert.equal(plans[0].operation_id, 'network.bridge-vlan.configure')
assert.equal(plans[0].state, 'awaiting-approval'); assert.equal(plans[0].approved_by, null)
const audit = (await pool.query('SELECT tool,status,error_code FROM mcp_requests ORDER BY created_at')).rows
assert(audit.some(row => row.error_code === 'APPROVAL_REQUIRED'))
const serialized = JSON.stringify({ result, stdout, stderr, audit })
assert(!serialized.includes(issued.token)); assert(!serialized.includes('fixture-only'))
const final = { ...(result as object), actualOwnPg: true, actualProductionHttpServer: true, serverFixturePid: process.pid,
connectionMode: 'autonomous', plans, audit, nativeReadRequests: requests, nativeWriteAttempts: writeAttempts,
appliedOperations: 0, syntheticRouterOnly: true, productionDatabaseTouched: false, portalContacted: false }
await writeFile(process.env.OWN_EXTERNAL_RESULT_FILE!, JSON.stringify(final, null, 2))
console.log('Owned bridge VLAN SDK passed:', JSON.stringify({ listedTools: 2, planState: plans[0].state, nativeReadRequests: requests.length, nativeWriteAttempts: writeAttempts }))
} finally {
if (child && child.exitCode === null && child.signalCode === null) { child.kill(); await childExit }
try { await app?.close() } finally { try { await closePool() } finally { router.closeIdleConnections(); await new Promise<void>((done, reject) => router.close(error => error ? reject(error) : done())) } }
}