mirror of
https://github.com/logos-storage/logos-storage-nim.git
synced 2026-05-12 14:29:39 +00:00
267 lines
9.4 KiB
JavaScript
267 lines
9.4 KiB
JavaScript
/**
|
|
* two-node-test.js — Layer 1.5: full pipeline two-node upload/download test
|
|
*
|
|
* Spins up two in-process libstorage nodes, connects them via bootstrap SPR,
|
|
* uploads content on node A, and verifies it can be downloaded from node B
|
|
* over the p2p network.
|
|
*
|
|
* This is the test that fills the TODO in tests/cbindings/storage.c:
|
|
* "// TODO: implement check_fetch — requires two nodes connected together"
|
|
*
|
|
* Usage:
|
|
* cd tests/js && npm install && npm test
|
|
*/
|
|
|
|
import { StorageNode } from './harness.js';
|
|
import { rmSync, mkdirSync } from 'fs';
|
|
import { tmpdir } from 'os';
|
|
import path from 'path';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Config helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const TMP = tmpdir();
|
|
|
|
function nodeConfig(label, config) {// { listenPort, discPort, bootstrapSpr = null }) {
|
|
if (!config["data-dir"]) {
|
|
config["data-dir"] = path.join(TMP, `libstorage-test-${label}`);
|
|
}
|
|
|
|
// Clean up any leftover state from a previous run
|
|
rmSync(config["data-dir"], { recursive: true, force: true });
|
|
mkdirSync(config["data-dir"], { recursive: true });
|
|
|
|
if (!config["log-level"]) {
|
|
config["log-level"] = 'ERROR';
|
|
}
|
|
|
|
console.debug(`Node '${label}' config:`, config);
|
|
|
|
return JSON.stringify(config);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Minimal test runner
|
|
// ---------------------------------------------------------------------------
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
async function test(name, fn) {
|
|
process.stdout.write(` ${name} ... `);
|
|
try {
|
|
await fn();
|
|
console.log('PASS');
|
|
passed++;
|
|
} catch (err) {
|
|
console.log(`FAIL\n ${err.message}`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
function assert(condition, message) {
|
|
if (!condition) throw new Error(message ?? 'assertion failed');
|
|
}
|
|
|
|
function assertEqual(actual, expected, label = '') {
|
|
if (actual !== expected) {
|
|
throw new Error(
|
|
`${label ? label + ': ' : ''}expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`
|
|
);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test suite
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function main() {
|
|
console.log('\nlogos-storage two-node p2p test\n');
|
|
|
|
const nodeA = new StorageNode();
|
|
const nodeB = new StorageNode();
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Setup: start node A, get its SPR, start node B bootstrapped to node A
|
|
// -------------------------------------------------------------------------
|
|
|
|
console.log('Setting up nodes...');
|
|
|
|
await nodeA.create(nodeConfig('a', {
|
|
"listen-port": 8792,
|
|
"disc-port": 8794,
|
|
"nat": "extip:127.0.0.1",
|
|
// "log-level": 'TRACE'
|
|
}));
|
|
await nodeA.start();
|
|
console.log(` node A version: ${nodeA.version()}`);
|
|
|
|
const sprA = await nodeA.spr();
|
|
console.log(` node A SPR: ${sprA.slice(0, 40)}...`);
|
|
|
|
await nodeB.create(nodeConfig('b', {
|
|
"listen-port": 8793,
|
|
"disc-port": 8795,
|
|
"nat": "extip:127.0.0.1",
|
|
// "log-level": 'TRACE',
|
|
"bootstrap-node": [sprA]
|
|
}));
|
|
await nodeB.start();
|
|
console.log(` node B version: ${nodeB.version()}`);
|
|
|
|
// Allow time for the bootstrap connection to establish
|
|
// console.log('\nWaiting for p2p connection...');
|
|
// await new Promise(r => setTimeout(r, 3000));
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Single-node tests (mirrors tests/cbindings/storage.c single-node suite)
|
|
// -------------------------------------------------------------------------
|
|
|
|
console.log('\nSingle-node tests (node A):');
|
|
|
|
await test('version is non-empty', () => {
|
|
assert(nodeA.version().length > 0, 'version should be non-empty');
|
|
});
|
|
|
|
await test('peer ID is a non-empty string', async () => {
|
|
const pid = await nodeA.peerId();
|
|
assert(pid && pid.length > 0, 'peer ID should be non-empty');
|
|
});
|
|
|
|
await test('SPR contains "spr"', async () => {
|
|
const spr = await nodeA.spr();
|
|
assert(spr.includes('spr'), `SPR should contain "spr", got: ${spr}`);
|
|
});
|
|
|
|
await test('debug info has id and addrs', async () => {
|
|
const debugJson = await nodeA.debug();
|
|
const info = JSON.parse(debugJson);
|
|
assert(info.id && info.id.length > 0, 'debug.id should be present');
|
|
assert(Array.isArray(info.addrs), 'debug.addrs should be an array');
|
|
});
|
|
|
|
const CONTENT = 'Hello from node A — p2p test content!';
|
|
let cid;
|
|
|
|
await test('upload content on node A', async () => {
|
|
cid = await nodeA.uploadContent(CONTENT);
|
|
assert(cid && cid.length > 0, 'CID should be a non-empty string');
|
|
console.log(`\n CID: ${cid}`);
|
|
});
|
|
|
|
await test('manifest has expected fields', async () => {
|
|
const manifestJson = await nodeA.downloadManifest(cid);
|
|
const m = JSON.parse(manifestJson);
|
|
assert(m.treeCid && m.treeCid.length > 0, 'manifest.treeCid should be present');
|
|
assert(typeof m.datasetSize === 'number', 'manifest.datasetSize should be a number');
|
|
assert(m.filename === 'test.txt', `manifest.filename should be "test.txt", got "${m.filename}"`);
|
|
});
|
|
|
|
await test('list includes uploaded CID manifest', async () => {
|
|
const listJson = await nodeA.list();
|
|
assert(listJson.includes(cid), `list should include CID ${cid}`);
|
|
});
|
|
|
|
await test('space info has totalBlocks', async () => {
|
|
const spaceJson = await nodeA.space();
|
|
const s = JSON.parse(spaceJson);
|
|
assert(typeof s.totalBlocks === 'number', 'space.totalBlocks should be a number');
|
|
});
|
|
|
|
await test('content exists locally on node A', async () => {
|
|
const exists = await nodeA.exists(cid);
|
|
assert(exists === true, 'exists should return true for uploaded CID');
|
|
});
|
|
|
|
await test('content is downloadable locally on node A', async () => {
|
|
const downloaded = await nodeA.downloadContent(cid, true /* local only */);
|
|
assertEqual(downloaded, CONTENT, 'local download on node A');
|
|
});
|
|
|
|
// Upload a second piece of content for delete/fetch tests to leave CONTENT available
|
|
let cid2;
|
|
await test('upload second content for delete test', async () => {
|
|
cid2 = await nodeA.uploadContent('ephemeral content for delete test', 'ephemeral.txt');
|
|
assert(cid2 && cid2.length > 0, 'CID2 should be non-empty');
|
|
});
|
|
|
|
await test('delete removes content from local store', async () => {
|
|
await nodeA.delete(cid2);
|
|
const exists = await nodeA.exists(cid2);
|
|
assert(exists === false, 'exists should return false after delete');
|
|
});
|
|
|
|
await test('cancel download does not crash', async () => {
|
|
// Init a download then immediately cancel it — verifies the cancel path is wired correctly
|
|
await nodeA.downloadContent(cid, true); // ensure blocks are cached
|
|
await nodeA.downloadCancel(cid);
|
|
// If we get here without exception, the cancel path is working
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Two-node tests
|
|
// -------------------------------------------------------------------------
|
|
|
|
console.log('\nTwo-node tests:');
|
|
|
|
await test('node B can download content from node A over p2p', async () => {
|
|
const downloaded = await nodeB.downloadContent(cid, false /* remote */);
|
|
assertEqual(downloaded, CONTENT, 'remote download on node B');
|
|
});
|
|
|
|
await test('storage_fetch: node B fetches into local store, then exists locally', async () => {
|
|
// Upload fresh content on A so B definitely doesn't have it yet
|
|
const fetchContent = 'Content for storage_fetch test';
|
|
const fetchCid = await nodeA.uploadContent(fetchContent, 'fetch-test.txt');
|
|
|
|
// Verify B doesn't have it locally yet
|
|
const beforeFetch = await nodeB.exists(fetchCid);
|
|
assert(beforeFetch === false, 'B should not have the CID locally before fetch');
|
|
|
|
// Fetch into B's local store
|
|
await nodeB.fetch(fetchCid);
|
|
|
|
// Now it should exist locally on B
|
|
const afterFetch = await nodeB.exists(fetchCid);
|
|
assert(afterFetch === true, 'B should have the CID locally after fetch');
|
|
|
|
// And be downloadable locally
|
|
const downloaded = await nodeB.downloadContent(fetchCid, true /* local only */);
|
|
assertEqual(downloaded, fetchContent, 'fetched content should match');
|
|
});
|
|
|
|
await test('node B peer ID is non-empty', async () => {
|
|
const pid = await nodeB.peerId();
|
|
assert(pid && pid.length > 0, 'node B peer ID should be non-empty');
|
|
});
|
|
|
|
await test('node A debug info shows node B in routing table', async () => {
|
|
const debugJson = await nodeA.debug();
|
|
const info = JSON.parse(debugJson);
|
|
// After 3s connection, node B should appear in node A's routing table
|
|
assert(Array.isArray(info.table?.nodes), 'debug.table.nodes should be an array');
|
|
assert(info.table.nodes.length > 0, 'routing table should contain at least one peer (node B)');
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Teardown
|
|
// -------------------------------------------------------------------------
|
|
|
|
console.log('\nShutting down nodes...');
|
|
await nodeA.shutdown();
|
|
await nodeB.shutdown();
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Summary
|
|
// -------------------------------------------------------------------------
|
|
|
|
console.log(`\n${passed + failed} tests: ${passed} passed, ${failed} failed\n`);
|
|
if (failed > 0) process.exit(1);
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error('\nFatal error:', err);
|
|
process.exit(1);
|
|
});
|