Initial Commit
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
|
||||
describe("network-gundb-server dep-graph", () => {
|
||||
it("must not have circular dependencies", { timeout: 5000 }, async () => {
|
||||
const mod = await import("../src/index.js");
|
||||
expect(mod).toBeDefined();
|
||||
expect(mod.GunDBServer).toBeDefined();
|
||||
expect(mod.NET_GUNDB_SERVER_CODES).toBeDefined();
|
||||
expect(mod.NetGunDBServerError).toBeDefined();
|
||||
expect(mod.GunDBServerConfigSchema).toBeDefined();
|
||||
expect(mod.validateInboundAnnounce).toBeDefined();
|
||||
expect(mod.handleHealth).toBeDefined();
|
||||
expect(mod.classifyHealthRequest).toBeDefined();
|
||||
expect(mod.buildHealthResponse).toBeDefined();
|
||||
expect(mod.HEALTH_PATHS).toBeDefined();
|
||||
expect(mod.recordConnection).toBeDefined();
|
||||
expect(mod.adjustActiveConnections).toBeDefined();
|
||||
expect(mod.withStartSpan).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,174 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { GunDBServer } from "../src/server/server.js";
|
||||
import { FakeGun, makeFakeGunFactory } from "./setup/fake-gun.js";
|
||||
|
||||
const VALID_ANNOUNCE = {
|
||||
kind: "peer-announce" as const,
|
||||
peerId: "client-1",
|
||||
addresses: ["ws://1.2.3.4:80"],
|
||||
metadata: { tag: "alpha" },
|
||||
capabilities: ["chat"],
|
||||
lastSeen: 1_700_000_000_000,
|
||||
signaturePublicKey: "c".repeat(64),
|
||||
signature: "d".repeat(128),
|
||||
};
|
||||
|
||||
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 { factory } = makeFakeGunFactory();
|
||||
const server = new GunDBServer({
|
||||
port: 0,
|
||||
host: "127.0.0.1",
|
||||
gunFactory: factory,
|
||||
});
|
||||
const { address, port } = await server.start();
|
||||
const base = `http://${address === "::" || address === "0.0.0.0" ? "127.0.0.1" : address}:${port}`;
|
||||
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 relay 200/OK marker.
|
||||
const fall = await fetch(`${base}/gun`);
|
||||
expect(fall.status).toBe(200);
|
||||
await fall.text();
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("two browser-tier clients relay a signed announce via shared graph", {
|
||||
timeout: 10_000,
|
||||
}, async () => {
|
||||
// The injected FakeGun shares storage across calls; both
|
||||
// "clients" inject the same announce-room namespace and the
|
||||
// server observes the inbound frame deterministically. This
|
||||
// demonstrates the wire path without pulling the real `gun`
|
||||
// runtime into the test env.
|
||||
const sharedStore = new Map<string, unknown>();
|
||||
const sharedListeners = new Map<
|
||||
string,
|
||||
Array<(data: unknown, key: string) => void>
|
||||
>();
|
||||
const serverFactory = (cfg: {
|
||||
readonly peers: readonly string[];
|
||||
}): FakeGun =>
|
||||
new FakeGun(
|
||||
{ web: undefined, peers: cfg.peers },
|
||||
sharedStore,
|
||||
sharedListeners
|
||||
);
|
||||
const server = new GunDBServer({
|
||||
port: 0,
|
||||
host: "127.0.0.1",
|
||||
gunFactory: serverFactory,
|
||||
});
|
||||
await server.start();
|
||||
try {
|
||||
// Two distinct "clients" inject into the same shared graph
|
||||
// — the server observes each one as a separate peer.
|
||||
const clientA = new FakeGun(
|
||||
{ web: undefined, peers: [] },
|
||||
sharedStore,
|
||||
sharedListeners
|
||||
);
|
||||
const clientB = new FakeGun(
|
||||
{ web: undefined, peers: [] },
|
||||
sharedStore,
|
||||
sharedListeners
|
||||
);
|
||||
clientA.inject("suaveplan-discovery", "client-1", {
|
||||
...VALID_ANNOUNCE,
|
||||
peerId: "client-1",
|
||||
});
|
||||
clientB.inject("suaveplan-discovery", "client-2", {
|
||||
...VALID_ANNOUNCE,
|
||||
peerId: "client-2",
|
||||
});
|
||||
expect(server.activeConnections).toBe(2);
|
||||
const peerIds = server
|
||||
.peers()
|
||||
.map((p) => p.id)
|
||||
.sort();
|
||||
expect(peerIds).toEqual(["client-1", "client-2"]);
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("malformed payload from a misbehaving client is dropped (counter only)", {
|
||||
timeout: 10_000,
|
||||
}, async () => {
|
||||
const sharedStore = new Map<string, unknown>();
|
||||
const sharedListeners = new Map<
|
||||
string,
|
||||
Array<(data: unknown, key: string) => void>
|
||||
>();
|
||||
const serverFactory = (cfg: {
|
||||
readonly peers: readonly string[];
|
||||
}): FakeGun =>
|
||||
new FakeGun(
|
||||
{ web: undefined, peers: cfg.peers },
|
||||
sharedStore,
|
||||
sharedListeners
|
||||
);
|
||||
const server = new GunDBServer({
|
||||
port: 0,
|
||||
host: "127.0.0.1",
|
||||
gunFactory: serverFactory,
|
||||
});
|
||||
await server.start();
|
||||
try {
|
||||
const badClient = new FakeGun(
|
||||
{ web: undefined, peers: [] },
|
||||
sharedStore,
|
||||
sharedListeners
|
||||
);
|
||||
badClient.inject("suaveplan-discovery", "evil-1", {
|
||||
kind: "wrong",
|
||||
junk: true,
|
||||
});
|
||||
badClient.inject("suaveplan-discovery", "evil-2", null);
|
||||
badClient.inject("suaveplan-discovery", "evil-3", {
|
||||
...VALID_ANNOUNCE,
|
||||
signature: undefined,
|
||||
});
|
||||
expect(server.activeConnections).toBe(0);
|
||||
expect(server.peers()).toHaveLength(0);
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("graceful stop closes the HTTP listener", {
|
||||
timeout: 10_000,
|
||||
}, async () => {
|
||||
const { factory } = makeFakeGunFactory();
|
||||
const server = new GunDBServer({
|
||||
port: 0,
|
||||
host: "127.0.0.1",
|
||||
gunFactory: factory,
|
||||
});
|
||||
const { address, port } = await server.start();
|
||||
const base = `http://${address === "::" || address === "0.0.0.0" ? "127.0.0.1" : address}:${port}`;
|
||||
await server.stop();
|
||||
// After stop, the socket is closed — connection refused.
|
||||
await expect(fetch(`${base}/health`)).rejects.toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { GunFactoryConfig, GunNodeLike } from "../../src/types/types.js";
|
||||
|
||||
/**
|
||||
* In-memory deterministic implementation of the `GunNodeLike` surface
|
||||
* used by the relay daemon. The real `gun` runtime is never pulled
|
||||
* into the test environment — it has heavy side-effects and requires
|
||||
* browser globals — so every test wires this fake via the injected
|
||||
* factory.
|
||||
*
|
||||
* Behavioural model:
|
||||
*
|
||||
* - The "graph" is a `Map<string, unknown>`. `get(path)` returns a
|
||||
* node-handle scoped to that path; chained `get(child)` extends the
|
||||
* path.
|
||||
* - `map().on(handler)` subscribes the current scope's children. The
|
||||
* handler is invoked once for every existing child at subscription
|
||||
* time, then again on every subsequent injected `put`.
|
||||
* - `off()` detaches every subscription created on the current scope.
|
||||
* - `inject(scope, key, value)` is a test affordance that fires the
|
||||
* subscribed handlers as if a remote peer had `put` the value.
|
||||
*/
|
||||
export class FakeGun implements GunNodeLike {
|
||||
public readonly trackers: readonly string[];
|
||||
public disposed = false;
|
||||
|
||||
private readonly store: Map<string, unknown>;
|
||||
private readonly listeners: Map<
|
||||
string,
|
||||
Array<(data: unknown, key: string) => void>
|
||||
>;
|
||||
private readonly path: string;
|
||||
|
||||
public constructor(
|
||||
config: GunFactoryConfig,
|
||||
store?: Map<string, unknown>,
|
||||
listeners?: Map<string, Array<(data: unknown, key: string) => void>>,
|
||||
path = ""
|
||||
) {
|
||||
this.trackers = config.peers;
|
||||
this.store = store ?? new Map();
|
||||
this.listeners = listeners ?? new Map();
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public get(segment: string): GunNodeLike {
|
||||
const next = this.path === "" ? segment : `${this.path}/${segment}`;
|
||||
return new FakeGun(
|
||||
{ web: undefined, peers: this.trackers },
|
||||
this.store,
|
||||
this.listeners,
|
||||
next
|
||||
);
|
||||
}
|
||||
|
||||
public map(): GunNodeLike {
|
||||
return this;
|
||||
}
|
||||
|
||||
public on(handler: (data: unknown, key: string) => void): GunNodeLike {
|
||||
const list = this.listeners.get(this.path) ?? [];
|
||||
list.push(handler);
|
||||
this.listeners.set(this.path, list);
|
||||
// Replay existing children at subscription time.
|
||||
const prefix = this.path === "" ? "" : `${this.path}/`;
|
||||
for (const [k, v] of this.store.entries()) {
|
||||
if (!k.startsWith(prefix)) continue;
|
||||
const rest = k.slice(prefix.length);
|
||||
if (rest === "" || rest.includes("/")) continue;
|
||||
handler(v, rest);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public off(): GunNodeLike {
|
||||
this.listeners.delete(this.path);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Test affordance: simulate a remote peer's `put` at the given path. */
|
||||
public inject(scope: string, key: string, value: unknown): void {
|
||||
const fullPath = scope === "" ? key : `${scope}/${key}`;
|
||||
this.store.set(fullPath, value);
|
||||
const handlers = this.listeners.get(scope);
|
||||
if (handlers === undefined) return;
|
||||
for (const h of [...handlers]) {
|
||||
h(value, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory helper producing `FakeGun` instances and recording every
|
||||
* call so tests can introspect or drive the captured reference.
|
||||
*/
|
||||
export function makeFakeGunFactory(): {
|
||||
readonly instances: FakeGun[];
|
||||
readonly factory: (config: GunFactoryConfig) => FakeGun;
|
||||
} {
|
||||
const instances: FakeGun[] = [];
|
||||
return {
|
||||
instances,
|
||||
factory: (config) => {
|
||||
const inst = new FakeGun(config);
|
||||
instances.push(inst);
|
||||
return inst;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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