feat(device-plane): add fail-closed deploy foundation
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
DEVICE_DISCOVERY_SIGNAL_SCHEMA,
|
||||
} from "../../../packages/device-protocol-contract/src/index.mjs";
|
||||
import { createControlCoreApp } from "../src/app.mjs";
|
||||
|
||||
const gatewayToken = "test-only-gateway-token-with-32-bytes";
|
||||
const identifierPepper = "test-only-identifier-pepper-with-32-bytes";
|
||||
const fakeImei = "000000000000001";
|
||||
|
||||
test("health reports database readiness and disabled command transport", async () => {
|
||||
const runtime = await startTestServer({
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await fetch(`${runtime.baseUrl}/healthz`);
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(await response.json(), {
|
||||
ok: true,
|
||||
service: "nodedc-device-control-core",
|
||||
database: "ready",
|
||||
discoveryIngest: "disabled",
|
||||
commandTransport: "disabled",
|
||||
});
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("discovery ingest is closed by default", async () => {
|
||||
const runtime = await startTestServer({
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/device-discoveries:observe`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "{}",
|
||||
},
|
||||
);
|
||||
assert.equal(response.status, 404);
|
||||
assert.equal(
|
||||
(await response.json()).error,
|
||||
"device_discovery_ingest_disabled",
|
||||
);
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("authenticated ingest stores only digest and returns a masked view", async () => {
|
||||
let stored;
|
||||
const runtime = await startTestServer({
|
||||
discoveryIngestEnabled: true,
|
||||
gatewayToken,
|
||||
identifierPepper,
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
upsertQuarantineDiscovery: async (value) => {
|
||||
stored = value;
|
||||
return {
|
||||
created: true,
|
||||
value: {
|
||||
...value.safeView,
|
||||
discoveryRef: "discovery:test-001",
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const unauthorized = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/device-discoveries:observe`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(fakeSignal()),
|
||||
},
|
||||
);
|
||||
assert.equal(unauthorized.status, 401);
|
||||
|
||||
const response = await fetch(
|
||||
`${runtime.baseUrl}/internal/v1/device-discoveries:observe`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${gatewayToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(fakeSignal()),
|
||||
},
|
||||
);
|
||||
assert.equal(response.status, 201);
|
||||
const body = await response.json();
|
||||
const serialized = JSON.stringify(body);
|
||||
assert.equal(serialized.includes(fakeImei), false);
|
||||
assert.equal(body.discovery.identifier.masked, "***********0001");
|
||||
assert.match(stored.identifierDigest, /^hmac-sha256:[a-f0-9]{64}$/);
|
||||
assert.equal(JSON.stringify(stored).includes(fakeImei), false);
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
function fakeSignal() {
|
||||
return {
|
||||
schemaVersion: DEVICE_DISCOVERY_SIGNAL_SCHEMA,
|
||||
sessionRef: "session:test-001",
|
||||
modelProfileRef: "arusnavi.b2.internal.v1",
|
||||
protocol: "INTERNAL",
|
||||
observedAt: "2026-07-25T00:00:00.000Z",
|
||||
identifier: { kind: "imei", value: fakeImei },
|
||||
evidence: {
|
||||
transport: "tcp",
|
||||
bytesObserved: 128,
|
||||
framingStatus: "verified",
|
||||
specificationRef: "arusnavi.internal.framing.test-v1",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function startTestServer(options) {
|
||||
const server = createControlCoreApp(options);
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
const address = server.address();
|
||||
return {
|
||||
baseUrl: `http://127.0.0.1:${address.port}`,
|
||||
close: () => new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { resolveDeviceDatabaseUrl } from "../src/database-config.mjs";
|
||||
|
||||
test("builds the database URL from a file-backed password", async () => {
|
||||
const password = "test-only-database-password-with-32-bytes";
|
||||
const url = await resolveDeviceDatabaseUrl(
|
||||
{
|
||||
DEVICE_DATABASE_HOST: "device-postgres",
|
||||
DEVICE_DATABASE_PORT: "5432",
|
||||
DEVICE_DATABASE_NAME: "device_plane",
|
||||
DEVICE_DATABASE_USER: "device_plane",
|
||||
DEVICE_DATABASE_PASSWORD_FILE: "/run/test/postgres-password",
|
||||
},
|
||||
async (path, encoding) => {
|
||||
assert.equal(path, "/run/test/postgres-password");
|
||||
assert.equal(encoding, "utf8");
|
||||
return `${password}\n`;
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
url,
|
||||
`postgresql://device_plane:${encodeURIComponent(password)}@device-postgres:5432/device_plane?sslmode=disable`,
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects a short file-backed database password", async () => {
|
||||
await assert.rejects(
|
||||
resolveDeviceDatabaseUrl(
|
||||
{
|
||||
DEVICE_DATABASE_HOST: "device-postgres",
|
||||
DEVICE_DATABASE_NAME: "device_plane",
|
||||
DEVICE_DATABASE_USER: "device_plane",
|
||||
DEVICE_DATABASE_PASSWORD_FILE: "/run/test/postgres-password",
|
||||
},
|
||||
async () => "too-short",
|
||||
),
|
||||
/device_database_password_invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
test("keeps an explicit database URL as a compatibility-only boundary", async () => {
|
||||
const explicit = "postgresql://local:test@127.0.0.1:5432/device_plane";
|
||||
assert.equal(
|
||||
await resolveDeviceDatabaseUrl({ DEVICE_DATABASE_URL: explicit }),
|
||||
explicit,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const migrationUrl = new URL(
|
||||
"../migrations/001_device_plane_foundation.sql",
|
||||
import.meta.url,
|
||||
);
|
||||
|
||||
test("foundation migration keeps restricted identifiers hashed and DB private", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
assert.match(sql, /identifier_digest text not null/);
|
||||
assert.match(sql, /identifier_masked text not null/);
|
||||
assert.doesNotMatch(sql, /imei\s+text/i);
|
||||
assert.doesNotMatch(sql, /password\s+text/i);
|
||||
assert.doesNotMatch(sql, /raw_packet/i);
|
||||
});
|
||||
|
||||
test("foundation migration has quarantine, contour, binding and audit tables", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
for (const table of [
|
||||
"device_model_profiles",
|
||||
"device_contours",
|
||||
"device_discoveries",
|
||||
"device_instances",
|
||||
"device_bindings",
|
||||
"device_audit_events",
|
||||
]) {
|
||||
assert.match(sql, new RegExp(`create table if not exists ${table}`));
|
||||
}
|
||||
assert.match(sql, /default 'quarantine'/);
|
||||
});
|
||||
Reference in New Issue
Block a user