monerod get_info
This commit is contained in:
Executable
+93
@@ -0,0 +1,93 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { loadMoneroDaemonProbeConfig, probeMoneroDaemon, type MoneroDaemonProbeTransport } from '../server/market/monero-daemon-probe.ts';
|
||||
|
||||
const config = { network: 'mainnet' as const, url: 'http://10.1.2.3:18081/json_rpc', allowPrivateHttp: true };
|
||||
function info(overrides: Record<string, unknown> = {}) {
|
||||
return { status: 'OK', nettype: 'mainnet', mainnet: true, stagenet: false, testnet: false,
|
||||
synchronized: true, busy_syncing: false, offline: false, untrusted: false,
|
||||
height: 3_000_000, height_without_bootstrap: 3_000_000, target_height: 3_000_000,
|
||||
version: '0.18.4.2-release', bootstrap_daemon_address: '', ...overrides };
|
||||
}
|
||||
function transportFor(result: Record<string, unknown>, options: { status?: number; type?: string; redirected?: boolean } = {}) {
|
||||
let request: { method?: string; redirect?: RequestRedirect; headers?: HeadersInit; body?: string } | undefined;
|
||||
const transport: MoneroDaemonProbeTransport = async (url, init) => {
|
||||
assert.equal(url, config.url);
|
||||
request = { method: init.method, redirect: init.redirect, headers: init.headers, body: init.body as string };
|
||||
const body = JSON.parse(init.body as string) as { id: string };
|
||||
return new Response(JSON.stringify({ jsonrpc: '2.0', id: body.id, result }), {
|
||||
status: options.status ?? 200, headers: { 'content-type': options.type ?? 'application/json' },
|
||||
...((options.redirected === undefined) ? {} : { statusText: options.redirected ? 'Moved' : 'OK' }),
|
||||
});
|
||||
};
|
||||
return { transport, getRequest: () => request };
|
||||
}
|
||||
|
||||
test('daemon probe calls only read-only get_info and accepts a synchronized local Mainnet chain', async () => {
|
||||
const fixture = transportFor(info());
|
||||
const result = await probeMoneroDaemon(config, fixture.transport, Date.UTC(2026, 9, 22));
|
||||
assert.deepEqual(result, { network: 'mainnet', height: 3_000_000, version: '0.18.4.2-release',
|
||||
synchronized: true, bootstrapProxyUsed: false, checkedAt: '2026-10-22T00:00:00.000Z' });
|
||||
const request = fixture.getRequest()!;
|
||||
assert.equal(request.method, 'POST');
|
||||
assert.equal(request.redirect, 'error');
|
||||
assert.equal(JSON.parse(request.body!).method, 'get_info');
|
||||
assert.equal(new Headers(request.headers).has('authorization'), false);
|
||||
});
|
||||
|
||||
test('daemon probe rejects network mismatch, syncing, offline, bootstrap and untrusted replies', async () => {
|
||||
for (const override of [
|
||||
{ nettype: 'stagenet', mainnet: false, stagenet: true },
|
||||
{ synchronized: false }, { busy_syncing: true }, { offline: true }, { untrusted: true },
|
||||
{ height_without_bootstrap: 2_999_999 }, { bootstrap_daemon_address: 'node.example:18081' },
|
||||
{ testnet: true },
|
||||
]) {
|
||||
const fixture = transportFor(info(override));
|
||||
await assert.rejects(probeMoneroDaemon(config, fixture.transport), /network or synchronization/);
|
||||
}
|
||||
});
|
||||
|
||||
test('daemon probe rejects malformed, oversized, redirected and non-JSON responses', async () => {
|
||||
const badJson: MoneroDaemonProbeTransport = async (_url, init) => {
|
||||
const body = JSON.parse(init.body as string) as { id: string };
|
||||
return new Response(JSON.stringify({ jsonrpc: '2.0', id: body.id, result: { ...info(), height: 'bad' } }),
|
||||
{ headers: { 'content-type': 'application/json' } });
|
||||
};
|
||||
await assert.rejects(probeMoneroDaemon(config, badJson), /Invalid Monero daemon RPC response/);
|
||||
const tooLarge: MoneroDaemonProbeTransport = async () => new Response('x'.repeat(70 * 1024),
|
||||
{ headers: { 'content-type': 'application/json' } });
|
||||
await assert.rejects(probeMoneroDaemon(config, tooLarge), /Invalid Monero daemon RPC response/);
|
||||
const redirected = transportFor(info(), { status: 302 });
|
||||
await assert.rejects(probeMoneroDaemon(config, redirected.transport), /response rejected/);
|
||||
const wrongType = transportFor(info(), { type: 'text/html' });
|
||||
await assert.rejects(probeMoneroDaemon(config, wrongType.transport), /response rejected/);
|
||||
const mismatchedId: MoneroDaemonProbeTransport = async () => new Response(JSON.stringify({
|
||||
jsonrpc: '2.0', id: '00000000-0000-4000-8000-000000000000', result: info(),
|
||||
}), { headers: { 'content-type': 'application/json' } });
|
||||
await assert.rejects(probeMoneroDaemon(config, mismatchedId), /response ID mismatch/);
|
||||
});
|
||||
|
||||
test('daemon probe requires explicit private literal endpoint and HTTP boundary acknowledgement', async () => {
|
||||
await assert.rejects(probeMoneroDaemon({ ...config, allowPrivateHttp: false }, async () => new Response()), /explicit private-network acknowledgement/);
|
||||
await assert.rejects(probeMoneroDaemon({ ...config, url: 'http://daemon.example:18081/json_rpc' }, async () => new Response()), /private literal IP/);
|
||||
await assert.rejects(probeMoneroDaemon({ ...config, url: 'http://8.8.8.8:18081/json_rpc' }, async () => new Response()), /private literal IP/);
|
||||
await assert.rejects(probeMoneroDaemon({ ...config, url: 'https://user:password@10.1.2.3:18081/json_rpc' }, async () => new Response()), /bare \/json_rpc/);
|
||||
await assert.rejects(probeMoneroDaemon({ ...config, url: 'http://10.1.2.3:18081/' }, async () => new Response()), /bare \/json_rpc/);
|
||||
});
|
||||
|
||||
test('daemon probe config loader requires a protected file and rejects unsafe configuration', async () => {
|
||||
await assert.rejects(loadMoneroDaemonProbeConfig({}), /config file is required/);
|
||||
const directory = await mkdtemp(join(tmpdir(), 'wikideck-monero-daemon-'));
|
||||
const path = join(directory, 'daemon.json');
|
||||
try {
|
||||
await writeFile(path, JSON.stringify(config), { mode: 0o600 });
|
||||
if (process.platform !== 'win32') await chmod(path, 0o600);
|
||||
const loaded = await loadMoneroDaemonProbeConfig({ WIKIDECK_MONERO_DAEMON_CONFIG_FILE: path });
|
||||
assert.deepEqual(loaded, config);
|
||||
await writeFile(path, JSON.stringify({ ...config, url: 'http://daemon.example:18081/json_rpc' }), { mode: 0o600 });
|
||||
await assert.rejects(loadMoneroDaemonProbeConfig({ WIKIDECK_MONERO_DAEMON_CONFIG_FILE: path }), /Invalid or inaccessible/);
|
||||
} finally { await rm(directory, { recursive: true, force: true }); }
|
||||
});
|
||||
@@ -15,6 +15,10 @@ test('private pilot Compose stack is loopback-bound, isolated, and mock-only', a
|
||||
assert.match(compose, /pg-runtime:\/secrets\/runtime:ro/);
|
||||
assert.doesNotMatch(compose, /POSTGRES_PASSWORD:\s|5432:5432/i);
|
||||
assert.doesNotMatch(compose, /^\s+(?:monero|wallet-rpc|observer):/im);
|
||||
assert.match(compose, /daemon-check:\n\s+image: wikideck:private-pilot/);
|
||||
assert.match(compose, /profiles: \["monero-check"\]/);
|
||||
assert.match(compose, /scripts\/monero-daemon-check\.ts/);
|
||||
assert.match(compose, /target: \/run\/monero\/daemon\.json\n\s+read_only: true/);
|
||||
const database = compose.split(' database:')[1].split(' migrate:')[0];
|
||||
assert.doesNotMatch(database, /ports:/);
|
||||
const app = compose.split(' wikideck:')[1].split('networks:\n financial:')[0];
|
||||
|
||||
Reference in New Issue
Block a user