Initial Commit
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
|
||||
describe("network-libp2p-server dep-graph", () => {
|
||||
it("must not have circular dependencies", { timeout: 5000 }, async () => {
|
||||
const mod = await import("../src/index.js");
|
||||
expect(mod).toBeDefined();
|
||||
expect(mod.Libp2pServer).toBeDefined();
|
||||
expect(mod.NET_LIBP2P_SERVER_CODES).toBeDefined();
|
||||
expect(mod.NetLibp2pServerError).toBeDefined();
|
||||
expect(mod.Libp2pServerConfigSchema).toBeDefined();
|
||||
expect(mod.handleHealth).toBeDefined();
|
||||
expect(mod.classifyHealthRequest).toBeDefined();
|
||||
expect(mod.buildHealthResponse).toBeDefined();
|
||||
expect(mod.HEALTH_PATHS).toBeDefined();
|
||||
expect(mod.recordConnection).toBeDefined();
|
||||
expect(mod.recordRelayedStream).toBeDefined();
|
||||
expect(mod.adjustActivePeers).toBeDefined();
|
||||
expect(mod.withStartSpan).toBeDefined();
|
||||
expect(mod.withStopSpan).toBeDefined();
|
||||
expect(mod.listenHttp).toBeDefined();
|
||||
expect(mod.wirePeerEvents).toBeDefined();
|
||||
expect(mod.unwirePeerEvents).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { Libp2pServer } from "../src/server/server.js";
|
||||
import {
|
||||
FakeLibp2pHub,
|
||||
FakeLibp2pServerNode,
|
||||
makeFakeServerFactory,
|
||||
} from "./setup/fake-libp2p-server.js";
|
||||
|
||||
async function fetchJson(
|
||||
url: string
|
||||
): Promise<{ readonly status: number; readonly body: unknown }> {
|
||||
const res = await fetch(url);
|
||||
const body = await res.json();
|
||||
return { status: res.status, body };
|
||||
}
|
||||
|
||||
describe("integration: real HTTP boot + health probes", () => {
|
||||
it("health/ready/live respond with 200 while running", {
|
||||
timeout: 10_000,
|
||||
}, async () => {
|
||||
const hub = new FakeLibp2pHub();
|
||||
const { factory } = makeFakeServerFactory(hub);
|
||||
const server = new Libp2pServer({
|
||||
libp2pFactory: factory,
|
||||
listen: ["/ip4/0.0.0.0/tcp/0"],
|
||||
healthPort: 0,
|
||||
healthHost: "127.0.0.1",
|
||||
});
|
||||
const { healthAddress, healthPort } = await server.start();
|
||||
const base = `http://${
|
||||
healthAddress === "::" || healthAddress === "0.0.0.0"
|
||||
? "127.0.0.1"
|
||||
: healthAddress
|
||||
}:${healthPort}`;
|
||||
try {
|
||||
const health = await fetchJson(`${base}/health`);
|
||||
expect(health.status).toBe(200);
|
||||
expect((health.body as { status: string }).status).toBe("ok");
|
||||
|
||||
const ready = await fetchJson(`${base}/ready`);
|
||||
expect(ready.status).toBe(200);
|
||||
|
||||
const live = await fetchJson(`${base}/live`);
|
||||
expect(live.status).toBe(200);
|
||||
|
||||
// Non-health route falls through to the plaintext marker.
|
||||
const fall = await fetch(`${base}/something-else`);
|
||||
expect(fall.status).toBe(200);
|
||||
expect(await fall.text()).toContain("libp2p");
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("a client dial is recorded as peer-connected end-to-end", {
|
||||
timeout: 10_000,
|
||||
}, async () => {
|
||||
const hub = new FakeLibp2pHub();
|
||||
const { factory, instances } = makeFakeServerFactory(hub);
|
||||
const server = new Libp2pServer({
|
||||
libp2pFactory: factory,
|
||||
listen: ["/ip4/0.0.0.0/tcp/0"],
|
||||
healthPort: 0,
|
||||
healthHost: "127.0.0.1",
|
||||
});
|
||||
await server.start();
|
||||
try {
|
||||
const serverNode = instances[0];
|
||||
if (!(serverNode instanceof FakeLibp2pServerNode)) {
|
||||
throw new Error("expected fake node");
|
||||
}
|
||||
// A second fake node in the same hub simulates a browser-tier
|
||||
// client dialling the server. The hub wires both ends' peer
|
||||
// events deterministically.
|
||||
const client = new FakeLibp2pServerNode(
|
||||
hub,
|
||||
{
|
||||
listen: ["/ip4/0.0.0.0/tcp/0"],
|
||||
bootstrap: [],
|
||||
autoRelay: { enabled: false, maxConnections: 100 },
|
||||
maxConnections: 100,
|
||||
protocolId: "/suaveplan/network-server/1.0.0",
|
||||
},
|
||||
"client-1"
|
||||
);
|
||||
hub.connectPair(serverNode, client);
|
||||
expect(server.peers()).toContain("client-1");
|
||||
expect(server.activePeers).toBe(1);
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("server relays a stream observation across two clients", {
|
||||
timeout: 10_000,
|
||||
}, async () => {
|
||||
const hub = new FakeLibp2pHub();
|
||||
const { factory, instances } = makeFakeServerFactory(hub);
|
||||
const server = new Libp2pServer({
|
||||
libp2pFactory: factory,
|
||||
listen: ["/ip4/0.0.0.0/tcp/0"],
|
||||
healthPort: 0,
|
||||
healthHost: "127.0.0.1",
|
||||
autoRelay: { enabled: true, maxConnections: 50 },
|
||||
});
|
||||
await server.start();
|
||||
try {
|
||||
const serverNode = instances[0];
|
||||
if (!(serverNode instanceof FakeLibp2pServerNode)) {
|
||||
throw new Error("expected fake node");
|
||||
}
|
||||
// Two clients sit behind NAT; both connect to the server.
|
||||
const a = new FakeLibp2pServerNode(
|
||||
hub,
|
||||
{
|
||||
listen: ["/ip4/0.0.0.0/tcp/0"],
|
||||
bootstrap: [],
|
||||
autoRelay: { enabled: false, maxConnections: 100 },
|
||||
maxConnections: 100,
|
||||
protocolId: "/suaveplan/network-server/1.0.0",
|
||||
},
|
||||
"client-A"
|
||||
);
|
||||
const b = new FakeLibp2pServerNode(
|
||||
hub,
|
||||
{
|
||||
listen: ["/ip4/0.0.0.0/tcp/0"],
|
||||
bootstrap: [],
|
||||
autoRelay: { enabled: false, maxConnections: 100 },
|
||||
maxConnections: 100,
|
||||
protocolId: "/suaveplan/network-server/1.0.0",
|
||||
},
|
||||
"client-B"
|
||||
);
|
||||
hub.connectPair(serverNode, a);
|
||||
hub.connectPair(serverNode, b);
|
||||
expect(server.peers()).toContain("client-A");
|
||||
expect(server.peers()).toContain("client-B");
|
||||
// Daemon observes a relayed stream between the two clients.
|
||||
server.observeRelayedStream("client-A->client-B");
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("graceful stop closes the inline health listener", {
|
||||
timeout: 10_000,
|
||||
}, async () => {
|
||||
const hub = new FakeLibp2pHub();
|
||||
const { factory } = makeFakeServerFactory(hub);
|
||||
const server = new Libp2pServer({
|
||||
libp2pFactory: factory,
|
||||
listen: ["/ip4/0.0.0.0/tcp/0"],
|
||||
healthPort: 0,
|
||||
healthHost: "127.0.0.1",
|
||||
});
|
||||
const { healthAddress, healthPort } = await server.start();
|
||||
const base = `http://${
|
||||
healthAddress === "::" || healthAddress === "0.0.0.0"
|
||||
? "127.0.0.1"
|
||||
: healthAddress
|
||||
}:${healthPort}`;
|
||||
await server.stop();
|
||||
await expect(fetch(`${base}/health`)).rejects.toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,202 @@
|
||||
import type {
|
||||
Libp2pServerFactory,
|
||||
Libp2pServerFactoryConfig,
|
||||
Libp2pServerNodeLike,
|
||||
} from "../../src/types/types.js";
|
||||
|
||||
let nextNodeNum = 1;
|
||||
|
||||
interface NodeListeners {
|
||||
readonly connect: Set<(event: { detail: unknown }) => void>;
|
||||
readonly disconnect: Set<(event: { detail: unknown }) => void>;
|
||||
}
|
||||
|
||||
/** Deterministic peer-id object. */
|
||||
export class FakePeerId {
|
||||
public constructor(private readonly id: string) {}
|
||||
public toString(): string {
|
||||
return this.id;
|
||||
}
|
||||
}
|
||||
|
||||
/** Deterministic multiaddr value object. */
|
||||
export class FakeMultiaddr {
|
||||
public constructor(private readonly value: string) {}
|
||||
public toString(): string {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Real in-memory `Libp2pServerNodeLike` for tests. Not a mock — every
|
||||
* method does real work over an internal peer Set + listener registry.
|
||||
* The `FakeLibp2pHub` coordinator wires `peer:connect` /
|
||||
* `peer:disconnect` events between paired nodes so the daemon's
|
||||
* `wirePeerEvents` bridging can be exercised end-to-end without
|
||||
* pulling the real `libp2p` module into the test surface.
|
||||
*/
|
||||
export class FakeLibp2pServerNode implements Libp2pServerNodeLike {
|
||||
public readonly localId: string;
|
||||
public readonly peerId: FakePeerId;
|
||||
readonly #hub: FakeLibp2pHub;
|
||||
readonly #peers = new Set<string>();
|
||||
readonly #multiaddrs: FakeMultiaddr[];
|
||||
readonly #listeners: NodeListeners = {
|
||||
connect: new Set(),
|
||||
disconnect: new Set(),
|
||||
};
|
||||
#started = false;
|
||||
#stopped = false;
|
||||
public startThrows = false;
|
||||
public stopThrows = false;
|
||||
|
||||
public constructor(
|
||||
hub: FakeLibp2pHub,
|
||||
config: Libp2pServerFactoryConfig,
|
||||
id?: string
|
||||
) {
|
||||
this.localId = id ?? `peer-${nextNodeNum++}`;
|
||||
this.peerId = new FakePeerId(this.localId);
|
||||
this.#hub = hub;
|
||||
this.#multiaddrs = config.listen.map(
|
||||
(a) => new FakeMultiaddr(`${a}/p2p/${this.localId}`)
|
||||
);
|
||||
hub.register(this);
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
if (this.startThrows) {
|
||||
throw new Error("fake-libp2p-server.start-failed");
|
||||
}
|
||||
this.#started = true;
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
if (this.stopThrows) {
|
||||
throw new Error("fake-libp2p-server.stop-failed");
|
||||
}
|
||||
if (this.#stopped) return;
|
||||
this.#stopped = true;
|
||||
this.#started = false;
|
||||
for (const peerId of Array.from(this.#peers)) {
|
||||
this.dropPeer(peerId);
|
||||
}
|
||||
this.#hub.unregister(this);
|
||||
}
|
||||
|
||||
public getMultiaddrs(): readonly { toString(): string }[] {
|
||||
return this.#multiaddrs;
|
||||
}
|
||||
|
||||
public getPeers(): readonly { toString(): string }[] {
|
||||
return Array.from(this.#peers).map((id) => new FakePeerId(id));
|
||||
}
|
||||
|
||||
public addEventListener(
|
||||
type: string,
|
||||
listener: (event: { detail: unknown }) => void
|
||||
): void {
|
||||
if (type === "peer:connect") this.#listeners.connect.add(listener);
|
||||
else if (type === "peer:disconnect") {
|
||||
this.#listeners.disconnect.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public removeEventListener(
|
||||
type: string,
|
||||
listener: (event: { detail: unknown }) => void
|
||||
): void {
|
||||
if (type === "peer:connect") this.#listeners.connect.delete(listener);
|
||||
else if (type === "peer:disconnect") {
|
||||
this.#listeners.disconnect.delete(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/** Hub uses this to fire peer:connect for both ends of a pairing. */
|
||||
public markConnected(peerId: string): void {
|
||||
if (this.#peers.has(peerId)) return;
|
||||
this.#peers.add(peerId);
|
||||
for (const listener of this.#listeners.connect) {
|
||||
listener({ detail: new FakePeerId(peerId) });
|
||||
}
|
||||
}
|
||||
|
||||
/** Hub uses this to fire peer:disconnect from either end. */
|
||||
public dropPeer(peerId: string): void {
|
||||
if (!this.#peers.has(peerId)) return;
|
||||
this.#peers.delete(peerId);
|
||||
for (const listener of this.#listeners.disconnect) {
|
||||
listener({ detail: new FakePeerId(peerId) });
|
||||
}
|
||||
}
|
||||
|
||||
/** Test-only: re-fire a peer:connect event bypassing dedup. */
|
||||
public forceEmitConnect(peerId: string): void {
|
||||
for (const listener of this.#listeners.connect) {
|
||||
listener({ detail: new FakePeerId(peerId) });
|
||||
}
|
||||
}
|
||||
|
||||
/** Test-only: re-fire a peer:disconnect event bypassing membership. */
|
||||
public forceEmitDisconnect(peerId: string): void {
|
||||
for (const listener of this.#listeners.disconnect) {
|
||||
listener({ detail: new FakePeerId(peerId) });
|
||||
}
|
||||
}
|
||||
|
||||
public get started(): boolean {
|
||||
return this.#started;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hub coordinating multiple `FakeLibp2pServerNode` instances. Tests
|
||||
* call `connectPair(a, b)` to wire each side's `peer:connect` event.
|
||||
*/
|
||||
export class FakeLibp2pHub {
|
||||
readonly #nodes = new Map<string, FakeLibp2pServerNode>();
|
||||
|
||||
public register(node: FakeLibp2pServerNode): void {
|
||||
this.#nodes.set(node.localId, node);
|
||||
}
|
||||
|
||||
public unregister(node: FakeLibp2pServerNode): void {
|
||||
this.#nodes.delete(node.localId);
|
||||
for (const other of this.#nodes.values()) {
|
||||
other.dropPeer(node.localId);
|
||||
}
|
||||
}
|
||||
|
||||
public get(id: string): FakeLibp2pServerNode | undefined {
|
||||
return this.#nodes.get(id);
|
||||
}
|
||||
|
||||
public connectPair(a: FakeLibp2pServerNode, b: FakeLibp2pServerNode): void {
|
||||
a.markConnected(b.localId);
|
||||
b.markConnected(a.localId);
|
||||
}
|
||||
}
|
||||
|
||||
/** Build a `Libp2pServerFactory` bound to a hub. */
|
||||
export function makeFakeServerFactory(
|
||||
hub: FakeLibp2pHub,
|
||||
opts?: { readonly idPrefix?: string; readonly startThrows?: boolean }
|
||||
): {
|
||||
readonly factory: Libp2pServerFactory;
|
||||
readonly instances: FakeLibp2pServerNode[];
|
||||
} {
|
||||
const instances: FakeLibp2pServerNode[] = [];
|
||||
let counter = 0;
|
||||
const factory: Libp2pServerFactory = async (
|
||||
config: Libp2pServerFactoryConfig
|
||||
) => {
|
||||
const id = opts?.idPrefix ? `${opts.idPrefix}-${++counter}` : undefined;
|
||||
const node = new FakeLibp2pServerNode(hub, config, id);
|
||||
if (opts?.startThrows === true) {
|
||||
node.startThrows = true;
|
||||
}
|
||||
instances.push(node);
|
||||
return node;
|
||||
};
|
||||
return { factory, instances };
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { webcrypto } from "node:crypto";
|
||||
|
||||
type WritableGlobal = { crypto?: Crypto };
|
||||
const g = globalThis as WritableGlobal;
|
||||
if (g.crypto === undefined) {
|
||||
g.crypto = webcrypto as unknown as Crypto;
|
||||
}
|
||||
Reference in New Issue
Block a user