355 lines
11 KiB
JavaScript
355 lines
11 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { connect } from "node:net";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
DEVICE_ADAPTER_CATALOG,
|
|
} from "../../../packages/device-adapter-catalog/src/index.mjs";
|
|
import { createDeviceGatewayRuntime } from "../src/runtime.mjs";
|
|
|
|
const specificationHeader = Buffer.from(
|
|
"FF23E9EF782DE7120300",
|
|
"hex",
|
|
);
|
|
const specificationPackage = Buffer.from(
|
|
"5B01010000FBDEC251EC5D",
|
|
"hex",
|
|
);
|
|
|
|
test("baseline health exposes no public ingress and no command transport", async () => {
|
|
const runtime = createDeviceGatewayRuntime({
|
|
healthPort: 0,
|
|
listenEnabled: false,
|
|
});
|
|
const addresses = await runtime.start();
|
|
try {
|
|
const response = await fetch(
|
|
`http://127.0.0.1:${addresses.healthAddress.port}/healthz`,
|
|
);
|
|
assert.equal(response.status, 200);
|
|
const body = await response.json();
|
|
assert.equal(body.publicIngress, "disabled");
|
|
assert.equal(body.commandTransport, "disabled");
|
|
assert.equal(body.tcpListener, "disabled");
|
|
assert.equal(addresses.tcpAddress, null);
|
|
} finally {
|
|
await runtime.stop();
|
|
}
|
|
});
|
|
|
|
test("telemetry ingress persists HEADER2 before acknowledging packages", async () => {
|
|
const captured = [];
|
|
const runtime = createDeviceGatewayRuntime({
|
|
healthPort: 0,
|
|
tcpHost: "0.0.0.0",
|
|
tcpPort: 0,
|
|
listenEnabled: true,
|
|
publicIngressEnabled: true,
|
|
coreChannelAuthenticated: true,
|
|
now: () => new Date(0x52db95de * 1000),
|
|
...gatewayAdapterOptions(),
|
|
onDiscovery: async (value) => {
|
|
captured.push(value);
|
|
return { lifecycleState: "quarantine" };
|
|
},
|
|
onMessage: async (message) => acceptanceFor(message),
|
|
});
|
|
const addresses = await runtime.start();
|
|
const client = await connectAndCollect(addresses.tcpAddress.port);
|
|
try {
|
|
client.socket.write(specificationHeader.subarray(0, 4));
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.equal(client.bytes().length, 0);
|
|
|
|
client.socket.write(specificationHeader.subarray(4));
|
|
await client.waitForBytes(9);
|
|
assert.equal(
|
|
client.bytes().subarray(0, 9).toString("hex").toUpperCase(),
|
|
"7B0400A0DE95DB527D",
|
|
);
|
|
assert.equal(captured.length, 1);
|
|
assert.equal(captured[0].identifier.value, "865209039777769");
|
|
assert.equal(captured[0].evidence.framingStatus, "verified");
|
|
assert.equal(captured[0].commandTransport, undefined);
|
|
|
|
client.socket.write(specificationPackage);
|
|
await client.waitForBytes(13);
|
|
assert.equal(
|
|
client.bytes().subarray(9).toString("hex").toUpperCase(),
|
|
"7B00017D",
|
|
);
|
|
assert.equal(runtime.status().totalDiscoveries, 1);
|
|
assert.equal(runtime.status().totalMessagesAccepted, 1);
|
|
assert.equal(runtime.status().totalPackagesAcknowledged, 1);
|
|
assert.equal(runtime.status().commandTransport, "disabled");
|
|
assert.equal(runtime.status().publicIngress, "telemetry-ingest");
|
|
|
|
const response = await fetch(
|
|
`http://127.0.0.1:${addresses.healthAddress.port}/healthz`,
|
|
);
|
|
const body = await response.json();
|
|
assert.equal(body.framing, "verified-read-only");
|
|
assert.equal(body.tcpListener, "telemetry-ingest");
|
|
assert.equal(body.publicIngress, "telemetry-ingest");
|
|
assert.equal(body.commandTransport, "disabled");
|
|
} finally {
|
|
client.socket.destroy();
|
|
await runtime.stop();
|
|
}
|
|
});
|
|
|
|
test("does not acknowledge malformed or unverified initial bytes", async () => {
|
|
const captured = [];
|
|
const runtime = createDeviceGatewayRuntime({
|
|
healthPort: 0,
|
|
tcpPort: 0,
|
|
listenEnabled: true,
|
|
...gatewayAdapterOptions(),
|
|
onDiscovery: async (value) => {
|
|
captured.push(value);
|
|
return { lifecycleState: "quarantine" };
|
|
},
|
|
onMessage: async (message) => acceptanceFor(message),
|
|
});
|
|
const addresses = await runtime.start();
|
|
try {
|
|
const received = await sendAndCollect(
|
|
addresses.tcpAddress.port,
|
|
Buffer.from("not-a-b2-header", "utf8"),
|
|
);
|
|
assert.equal(received.length, 0);
|
|
assert.equal(captured.length, 0);
|
|
assert.equal(runtime.status().totalRejected, 1);
|
|
} finally {
|
|
await runtime.stop();
|
|
}
|
|
});
|
|
|
|
test("does not acknowledge a header when Core rejects discovery", async () => {
|
|
const runtime = createDeviceGatewayRuntime({
|
|
healthPort: 0,
|
|
tcpPort: 0,
|
|
listenEnabled: true,
|
|
...gatewayAdapterOptions(),
|
|
onDiscovery: async () => {
|
|
throw new Error("core_unavailable");
|
|
},
|
|
onMessage: async (message) => acceptanceFor(message),
|
|
});
|
|
const addresses = await runtime.start();
|
|
try {
|
|
const received = await sendAndCollect(
|
|
addresses.tcpAddress.port,
|
|
specificationHeader,
|
|
);
|
|
assert.equal(received.length, 0);
|
|
assert.equal(runtime.status().totalDiscoveries, 0);
|
|
assert.equal(runtime.status().totalRejected, 1);
|
|
} finally {
|
|
await runtime.stop();
|
|
}
|
|
});
|
|
|
|
test("does not acknowledge a package when durable Core acceptance fails", async () => {
|
|
const runtime = createDeviceGatewayRuntime({
|
|
healthPort: 0,
|
|
tcpPort: 0,
|
|
listenEnabled: true,
|
|
now: () => new Date(0x52db95de * 1000),
|
|
...gatewayAdapterOptions(),
|
|
onDiscovery: async () => ({ lifecycleState: "quarantine" }),
|
|
onMessage: async () => {
|
|
throw new Error("core_commit_failed");
|
|
},
|
|
});
|
|
const addresses = await runtime.start();
|
|
try {
|
|
const received = await sendAndCollect(
|
|
addresses.tcpAddress.port,
|
|
Buffer.concat([specificationHeader, specificationPackage]),
|
|
);
|
|
assert.equal(
|
|
received.toString("hex").toUpperCase(),
|
|
"7B0400A0DE95DB527D",
|
|
);
|
|
assert.equal(runtime.status().totalMessagesAccepted, 0);
|
|
assert.equal(runtime.status().totalPackagesAcknowledged, 0);
|
|
assert.equal(runtime.status().totalRejected, 1);
|
|
} finally {
|
|
await runtime.stop();
|
|
}
|
|
});
|
|
|
|
test("acknowledges an idempotent Core replay as accepted delivery", async () => {
|
|
const runtime = createDeviceGatewayRuntime({
|
|
healthPort: 0,
|
|
tcpPort: 0,
|
|
listenEnabled: true,
|
|
now: () => new Date(0x52db95de * 1000),
|
|
...gatewayAdapterOptions(),
|
|
onDiscovery: async () => ({ lifecycleState: "quarantine" }),
|
|
onMessage: async (message) => acceptanceFor(message, true),
|
|
});
|
|
const addresses = await runtime.start();
|
|
try {
|
|
const received = await sendAndCollect(
|
|
addresses.tcpAddress.port,
|
|
Buffer.concat([specificationHeader, specificationPackage]),
|
|
);
|
|
assert.equal(
|
|
received.toString("hex").toUpperCase(),
|
|
"7B0400A0DE95DB527D7B00017D",
|
|
);
|
|
assert.equal(runtime.status().totalMessagesAccepted, 1);
|
|
assert.equal(runtime.status().totalPackagesAcknowledged, 1);
|
|
} finally {
|
|
await runtime.stop();
|
|
}
|
|
});
|
|
|
|
test("closes an oversized tracker buffer and releases its aggregate budget", async () => {
|
|
const runtime = createDeviceGatewayRuntime({
|
|
healthPort: 0,
|
|
tcpPort: 0,
|
|
listenEnabled: true,
|
|
maxBufferedBytes: 1024,
|
|
maxAggregateBufferedBytes: 1024,
|
|
...gatewayAdapterOptions(),
|
|
onDiscovery: async () => ({ lifecycleState: "quarantine" }),
|
|
onMessage: async (message) => acceptanceFor(message),
|
|
});
|
|
const addresses = await runtime.start();
|
|
try {
|
|
const received = await sendAndCollect(
|
|
addresses.tcpAddress.port,
|
|
Buffer.alloc(1025, 0x01),
|
|
);
|
|
assert.equal(received.length, 0);
|
|
assert.equal(runtime.status().totalRejected, 1);
|
|
assert.equal(runtime.status().totalBufferedBytes, 0);
|
|
assert.equal(runtime.status().activeSessions, 0);
|
|
} finally {
|
|
await runtime.stop();
|
|
}
|
|
});
|
|
|
|
test("public ingress requires both discovery and durable message acceptance sinks", () => {
|
|
assert.throws(
|
|
() => createDeviceGatewayRuntime({
|
|
listenEnabled: true,
|
|
publicIngressEnabled: true,
|
|
coreChannelAuthenticated: true,
|
|
tcpHost: "0.0.0.0",
|
|
}),
|
|
/device_gateway_core_acceptance_sink_required/,
|
|
);
|
|
});
|
|
|
|
test("public ingress cannot start on the legacy bearer HTTP Core client", () => {
|
|
assert.throws(
|
|
() => createDeviceGatewayRuntime({
|
|
listenEnabled: true,
|
|
publicIngressEnabled: true,
|
|
tcpHost: "0.0.0.0",
|
|
...gatewayAdapterOptions(),
|
|
onDiscovery: async () => ({ lifecycleState: "quarantine" }),
|
|
onMessage: async (message) => acceptanceFor(message),
|
|
}),
|
|
/device_gateway_authenticated_core_channel_required/,
|
|
);
|
|
});
|
|
|
|
test("baseline rejects non-loopback binding", () => {
|
|
assert.throws(
|
|
() => createDeviceGatewayRuntime({
|
|
listenEnabled: true,
|
|
tcpHost: "0.0.0.0",
|
|
...gatewayAdapterOptions(),
|
|
onDiscovery: async () => ({ lifecycleState: "quarantine" }),
|
|
onMessage: async (message) => acceptanceFor(message),
|
|
}),
|
|
/device_gateway_baseline_loopback_only/,
|
|
);
|
|
});
|
|
|
|
test("container health may bind all interfaces while TCP stays disabled", async () => {
|
|
const runtime = createDeviceGatewayRuntime({
|
|
healthHost: "0.0.0.0",
|
|
healthPort: 0,
|
|
listenEnabled: false,
|
|
});
|
|
const addresses = await runtime.start();
|
|
try {
|
|
assert.equal(addresses.healthAddress.address, "0.0.0.0");
|
|
assert.equal(addresses.tcpAddress, null);
|
|
assert.equal(runtime.status().publicIngress, "disabled");
|
|
} finally {
|
|
await runtime.stop();
|
|
}
|
|
});
|
|
|
|
function connectAndCollect(port) {
|
|
return new Promise((resolve, reject) => {
|
|
const chunks = [];
|
|
let byteLength = 0;
|
|
const waiters = [];
|
|
const socket = connect({ host: "127.0.0.1", port }, () => {
|
|
resolve({
|
|
socket,
|
|
bytes: () => Buffer.concat(chunks, byteLength),
|
|
waitForBytes: (minimum) => {
|
|
if (byteLength >= minimum) return Promise.resolve();
|
|
return new Promise((waitResolve, waitReject) => {
|
|
waiters.push({ minimum, waitResolve, waitReject });
|
|
});
|
|
},
|
|
});
|
|
});
|
|
socket.on("data", (chunk) => {
|
|
chunks.push(chunk);
|
|
byteLength += chunk.length;
|
|
for (let index = waiters.length - 1; index >= 0; index -= 1) {
|
|
if (byteLength >= waiters[index].minimum) {
|
|
waiters[index].waitResolve();
|
|
waiters.splice(index, 1);
|
|
}
|
|
}
|
|
});
|
|
socket.on("error", (error) => {
|
|
for (const waiter of waiters.splice(0)) waiter.waitReject(error);
|
|
reject(error);
|
|
});
|
|
});
|
|
}
|
|
|
|
function sendAndCollect(port, payload) {
|
|
return new Promise((resolve, reject) => {
|
|
const chunks = [];
|
|
const socket = connect({ host: "127.0.0.1", port }, () => {
|
|
socket.end(payload);
|
|
});
|
|
socket.on("data", (chunk) => chunks.push(chunk));
|
|
socket.on("close", () => resolve(Buffer.concat(chunks)));
|
|
socket.on("error", reject);
|
|
});
|
|
}
|
|
|
|
function gatewayAdapterOptions() {
|
|
return {
|
|
adapterRegistry: DEVICE_ADAPTER_CATALOG.registry,
|
|
protocolProfileRef: DEVICE_ADAPTER_CATALOG.defaultProfileRef,
|
|
edgeRef: "edge:test-001",
|
|
};
|
|
}
|
|
|
|
function acceptanceFor(message, replayed = false) {
|
|
return {
|
|
schemaVersion: "nodedc.device-adapter-acceptance.v1",
|
|
acceptanceRef: "acceptance:test-001",
|
|
idempotencyKey: message.idempotencyKey,
|
|
status: "accepted",
|
|
replayed,
|
|
acceptedAt: "2026-08-11T12:00:00.000Z",
|
|
};
|
|
}
|