Updates
This commit is contained in:
+11
-11
@@ -3,35 +3,35 @@ import { AppError, ErrorType, createErrorResponse, sanitizeErrorMessage } from '
|
||||
|
||||
test('AppError - creates error with type and code', (t) => {
|
||||
const error = new AppError('Test error', ErrorType.VALIDATION, 'TEST_CODE');
|
||||
t.equal(error.message, 'Test error');
|
||||
t.equal(error.type, ErrorType.VALIDATION);
|
||||
t.equal(error.code, 'TEST_CODE');
|
||||
t.is(error.message, 'Test error');
|
||||
t.is(error.type, ErrorType.VALIDATION);
|
||||
t.is(error.code, 'TEST_CODE');
|
||||
t.ok(error.timestamp);
|
||||
});
|
||||
|
||||
test('AppError - toJSON serialization', (t) => {
|
||||
const error = new AppError('Test error', ErrorType.NETWORK);
|
||||
const json = error.toJSON();
|
||||
t.equal(json.error, 'Test error');
|
||||
t.equal(json.type, ErrorType.NETWORK);
|
||||
t.is(json.error, 'Test error');
|
||||
t.is(json.type, ErrorType.NETWORK);
|
||||
t.ok(json.timestamp);
|
||||
});
|
||||
|
||||
test('createErrorResponse - AppError', (t) => {
|
||||
const error = new AppError('Test error', ErrorType.VALIDATION, 'TEST_CODE');
|
||||
const response = createErrorResponse(error);
|
||||
t.equal(response.error, 'Test error');
|
||||
t.equal(response.code, 'TEST_CODE');
|
||||
t.equal(response.type, ErrorType.VALIDATION);
|
||||
t.is(response.error, 'Test error');
|
||||
t.is(response.code, 'TEST_CODE');
|
||||
t.is(response.type, ErrorType.VALIDATION);
|
||||
});
|
||||
|
||||
test('createErrorResponse - standard Error', (t) => {
|
||||
const error = new Error('Standard error');
|
||||
error.code = 'ETIMEDOUT';
|
||||
const response = createErrorResponse(error);
|
||||
t.equal(response.error, 'Standard error');
|
||||
t.equal(response.code, 'ETIMEDOUT');
|
||||
t.equal(response.type, ErrorType.NETWORK);
|
||||
t.is(response.error, 'Standard error');
|
||||
t.is(response.code, 'ETIMEDOUT');
|
||||
t.is(response.type, ErrorType.NETWORK);
|
||||
});
|
||||
|
||||
test('sanitizeErrorMessage - removes sensitive info', (t) => {
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Integration: HyperDHT + protomux-rpc protocol (no Docker required).
|
||||
*/
|
||||
import test from 'brittle'
|
||||
import DHT from 'hyperdht'
|
||||
import createTestnet from 'hyperdht/testnet.js'
|
||||
import ProtomuxRPC from 'protomux-rpc'
|
||||
import { PROTOCOL, Methods, Pushes } from '../shared/protocol.js'
|
||||
import { encodings } from '../shared/encodings.js'
|
||||
|
||||
test('protomux-rpc over HyperDHT: request + push', async (t) => {
|
||||
t.plan(5)
|
||||
|
||||
const testnet = await createTestnet()
|
||||
const { bootstrap } = testnet
|
||||
|
||||
const serverDht = new DHT({ bootstrap })
|
||||
const keyPair = DHT.keyPair()
|
||||
const server = serverDht.createServer()
|
||||
|
||||
let pushed = false
|
||||
|
||||
server.on('connection', (socket) => {
|
||||
const rpc = new ProtomuxRPC(socket, {
|
||||
id: keyPair.publicKey,
|
||||
protocol: PROTOCOL,
|
||||
...encodings,
|
||||
})
|
||||
|
||||
rpc.respond(Methods.ping, encodings, () => ({ success: true, pong: Date.now() }))
|
||||
rpc.respond(Methods.listContainers, encodings, () => ({
|
||||
type: 'containers',
|
||||
data: [{ Id: 'abc', Names: ['/demo'] }],
|
||||
}))
|
||||
|
||||
// After client is up, push stats (simulates server broadcast)
|
||||
setTimeout(() => {
|
||||
rpc.event(Pushes.allStats, {
|
||||
type: 'allStats',
|
||||
data: [{ id: 'abc', cpu: 1.5, memory: 1024 }],
|
||||
}, encodings)
|
||||
}, 50)
|
||||
})
|
||||
|
||||
await server.listen(keyPair)
|
||||
|
||||
const clientDht = new DHT({ bootstrap })
|
||||
const socket = clientDht.connect(keyPair.publicKey)
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error('connect timeout')), 15000)
|
||||
const done = () => {
|
||||
clearTimeout(timer)
|
||||
resolve()
|
||||
}
|
||||
socket.once('connect', done)
|
||||
socket.once('open', done)
|
||||
socket.once('error', (err) => {
|
||||
clearTimeout(timer)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
|
||||
const rpc = new ProtomuxRPC(socket, {
|
||||
id: keyPair.publicKey,
|
||||
protocol: PROTOCOL,
|
||||
...encodings,
|
||||
})
|
||||
|
||||
rpc.respond(Pushes.allStats, encodings, (payload) => {
|
||||
pushed = true
|
||||
t.is(payload.type, 'allStats')
|
||||
t.is(payload.data[0].id, 'abc')
|
||||
return null
|
||||
})
|
||||
|
||||
const pong = await rpc.request(Methods.ping, {}, encodings)
|
||||
t.ok(pong.success)
|
||||
|
||||
const list = await rpc.request(Methods.listContainers, {}, encodings)
|
||||
t.is(list.type, 'containers')
|
||||
|
||||
// Wait for push
|
||||
await new Promise((r) => setTimeout(r, 200))
|
||||
t.ok(pushed)
|
||||
|
||||
socket.destroy()
|
||||
await server.close()
|
||||
await serverDht.destroy()
|
||||
await clientDht.destroy()
|
||||
await testnet.destroy()
|
||||
})
|
||||
|
||||
test('protocol method names are stable strings', (t) => {
|
||||
t.is(Methods.listContainers, 'listContainers')
|
||||
t.is(Methods.startTerminal, 'startTerminal')
|
||||
t.is(Pushes.containers, 'push:containers')
|
||||
t.is(PROTOCOL, 'peardock/rpc')
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Full PeerSession + handler registration over HyperDHT.
|
||||
*/
|
||||
import test from 'brittle'
|
||||
import DHT from 'hyperdht'
|
||||
import createTestnet from 'hyperdht/testnet.js'
|
||||
import { PeerSession } from '../server/rpc/session.js'
|
||||
import { registerAllHandlers } from '../server/rpc/register.js'
|
||||
import { Methods } from '../shared/protocol.js'
|
||||
|
||||
test('PearDockConnection + PeerSession ping round-trip', async (t) => {
|
||||
const testnet = await createTestnet()
|
||||
const { bootstrap } = testnet
|
||||
|
||||
const serverDht = new DHT({ bootstrap })
|
||||
const keyPair = DHT.keyPair()
|
||||
const server = serverDht.createServer()
|
||||
|
||||
server.on('connection', (socket) => {
|
||||
const session = new PeerSession(socket, {
|
||||
serverPublicKey: keyPair.publicKey,
|
||||
})
|
||||
registerAllHandlers(session)
|
||||
})
|
||||
|
||||
await server.listen(keyPair)
|
||||
const publicKeyHex = Buffer.from(keyPair.publicKey).toString('hex')
|
||||
|
||||
// Inject bootstrap into HyperDHT by temporarily using a custom connect path:
|
||||
// PearDockConnection creates its own DHT; for testnet we connect manually.
|
||||
const clientDht = new DHT({ bootstrap })
|
||||
const socket = clientDht.connect(keyPair.publicKey)
|
||||
await new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error('timeout')), 15000)
|
||||
const done = () => {
|
||||
clearTimeout(timer)
|
||||
resolve()
|
||||
}
|
||||
socket.once('connect', done)
|
||||
socket.once('open', done)
|
||||
socket.once('error', (e) => {
|
||||
clearTimeout(timer)
|
||||
reject(e)
|
||||
})
|
||||
})
|
||||
|
||||
// Use raw protomux via a temporary connection object by reusing PearDockConnection
|
||||
// after monkey-patching connect to use testnet DHT is heavy — call session methods via
|
||||
// a lightweight client instead:
|
||||
const { default: ProtomuxRPC } = await import('protomux-rpc')
|
||||
const { PROTOCOL } = await import('../shared/protocol.js')
|
||||
const { encodings } = await import('../shared/encodings.js')
|
||||
|
||||
const rpc = new ProtomuxRPC(socket, {
|
||||
id: keyPair.publicKey,
|
||||
protocol: PROTOCOL,
|
||||
...encodings,
|
||||
})
|
||||
|
||||
const pong = await rpc.request(Methods.ping, {}, encodings)
|
||||
t.ok(pong.success)
|
||||
t.ok(pong.pong)
|
||||
|
||||
// browseDirectory should work without Docker
|
||||
const browse = await rpc.request(Methods.browseDirectory, { path: '/' }, encodings)
|
||||
t.ok(browse.success)
|
||||
t.ok(Array.isArray(browse.contents))
|
||||
|
||||
socket.destroy()
|
||||
await server.close()
|
||||
await serverDht.destroy()
|
||||
await clientDht.destroy()
|
||||
await testnet.destroy()
|
||||
})
|
||||
+12
-12
@@ -51,27 +51,27 @@ test('isValidVolumeMount - invalid volumes', (t) => {
|
||||
});
|
||||
|
||||
test('sanitizeEnvVarName - valid names', (t) => {
|
||||
t.equal(validation.sanitizeEnvVarName('MY_VAR'), 'MY_VAR');
|
||||
t.equal(validation.sanitizeEnvVarName('_PRIVATE'), '_PRIVATE');
|
||||
t.equal(validation.sanitizeEnvVarName('var123'), 'var123');
|
||||
t.is(validation.sanitizeEnvVarName('MY_VAR'), 'MY_VAR');
|
||||
t.is(validation.sanitizeEnvVarName('_PRIVATE'), '_PRIVATE');
|
||||
t.is(validation.sanitizeEnvVarName('var123'), 'var123');
|
||||
});
|
||||
|
||||
test('sanitizeEnvVarName - invalid names', (t) => {
|
||||
t.equal(validation.sanitizeEnvVarName('123VAR'), null);
|
||||
t.equal(validation.sanitizeEnvVarName('var-with-dash'), null);
|
||||
t.equal(validation.sanitizeEnvVarName(''), null);
|
||||
t.is(validation.sanitizeEnvVarName('123VAR'), null);
|
||||
t.is(validation.sanitizeEnvVarName('var-with-dash'), null);
|
||||
t.is(validation.sanitizeEnvVarName(''), null);
|
||||
});
|
||||
|
||||
test('validateNumber - valid numbers', (t) => {
|
||||
t.equal(validation.validateNumber(5, 0, 10), 5);
|
||||
t.equal(validation.validateNumber('5', 0, 10), 5);
|
||||
t.equal(validation.validateNumber(0, 0, 10), 0);
|
||||
t.is(validation.validateNumber(5, 0, 10), 5);
|
||||
t.is(validation.validateNumber('5', 0, 10), 5);
|
||||
t.is(validation.validateNumber(0, 0, 10), 0);
|
||||
});
|
||||
|
||||
test('validateNumber - invalid numbers', (t) => {
|
||||
t.equal(validation.validateNumber(15, 0, 10), null);
|
||||
t.equal(validation.validateNumber('invalid', 0, 10), null);
|
||||
t.equal(validation.validateNumber(null, 0, 10), null);
|
||||
t.is(validation.validateNumber(15, 0, 10), null);
|
||||
t.is(validation.validateNumber('invalid', 0, 10), null);
|
||||
t.is(validation.validateNumber(null, 0, 10), null);
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user