600 lines
18 KiB
JavaScript
600 lines
18 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
DEVICE_ADAPTER_MESSAGE_SCHEMA,
|
|
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 managementToken = "test-only-management-token-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",
|
|
managementApi: "disabled",
|
|
edgeChannels: {
|
|
enabled: false,
|
|
configured: 0,
|
|
accepted: 0,
|
|
degraded: 0,
|
|
},
|
|
commandTransport: "disabled",
|
|
});
|
|
} finally {
|
|
await runtime.close();
|
|
}
|
|
});
|
|
|
|
test("management API is closed by default", async () => {
|
|
const runtime = await startTestServer({
|
|
repository: {
|
|
health: async () => "ready",
|
|
},
|
|
});
|
|
try {
|
|
const response = await fetch(
|
|
`${runtime.baseUrl}/internal/v1/management/owner-scopes:ensure`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: "{}",
|
|
},
|
|
);
|
|
assert.equal(response.status, 404);
|
|
assert.equal((await response.json()).error, "device_management_api_disabled");
|
|
} finally {
|
|
await runtime.close();
|
|
}
|
|
});
|
|
|
|
test("management API cannot start without its repository boundary and strong token", () => {
|
|
assert.throws(
|
|
() => createControlCoreApp({
|
|
managementApiEnabled: true,
|
|
managementToken,
|
|
repository: { health: async () => "ready" },
|
|
}),
|
|
/device_management_repository_required/,
|
|
);
|
|
assert.throws(
|
|
() => createControlCoreApp({
|
|
managementApiEnabled: true,
|
|
managementToken: "short",
|
|
repository: {
|
|
health: async () => "ready",
|
|
executeManagementCommand: async () => ({}),
|
|
},
|
|
}),
|
|
/device_management_token_invalid/,
|
|
);
|
|
assert.throws(
|
|
() => createControlCoreApp({
|
|
managementApiEnabled: true,
|
|
managementToken,
|
|
repository: {
|
|
health: async () => "ready",
|
|
executeManagementCommand: async () => ({}),
|
|
},
|
|
}),
|
|
/device_identifier_pepper_invalid/,
|
|
);
|
|
});
|
|
|
|
test("management API requires service auth and an idempotency key", async () => {
|
|
const runtime = await startTestServer({
|
|
managementApiEnabled: true,
|
|
managementToken,
|
|
repository: {
|
|
health: async () => "ready",
|
|
executeManagementCommand: async () => {
|
|
throw new Error("must_not_execute");
|
|
},
|
|
},
|
|
});
|
|
try {
|
|
const unauthorized = await fetch(
|
|
`${runtime.baseUrl}/internal/v1/management/owner-scopes:ensure`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: "{}",
|
|
},
|
|
);
|
|
assert.equal(unauthorized.status, 401);
|
|
|
|
const missingIdempotency = await fetch(
|
|
`${runtime.baseUrl}/internal/v1/management/owner-scopes:ensure`,
|
|
{
|
|
method: "POST",
|
|
headers: managementHeaders({ includeIdempotency: false }),
|
|
body: JSON.stringify(ownerScopeCommand()),
|
|
},
|
|
);
|
|
assert.equal(missingIdempotency.status, 400);
|
|
assert.equal(
|
|
(await missingIdempotency.json()).error,
|
|
"device_management_header_required",
|
|
);
|
|
} finally {
|
|
await runtime.close();
|
|
}
|
|
});
|
|
|
|
test("management API forwards only normalized actor and command data", async () => {
|
|
let executed;
|
|
const runtime = await startTestServer({
|
|
managementApiEnabled: true,
|
|
managementToken,
|
|
repository: {
|
|
health: async () => "ready",
|
|
executeManagementCommand: async (value) => {
|
|
executed = value;
|
|
return {
|
|
replayed: false,
|
|
result: {
|
|
created: true,
|
|
ownerScope: {
|
|
ownerScopeRef: "owner-scope:11111111-1111-4111-8111-111111111111",
|
|
scopeKind: "company",
|
|
ownerRef: "client:example",
|
|
displayName: "Example Company",
|
|
lifecycleState: "active",
|
|
},
|
|
},
|
|
};
|
|
},
|
|
},
|
|
});
|
|
try {
|
|
const response = await fetch(
|
|
`${runtime.baseUrl}/internal/v1/management/owner-scopes:ensure`,
|
|
{
|
|
method: "POST",
|
|
headers: managementHeaders(),
|
|
body: JSON.stringify(ownerScopeCommand()),
|
|
},
|
|
);
|
|
assert.equal(response.status, 200);
|
|
assert.equal(response.headers.get("idempotency-key"), "phase2-test-0001");
|
|
assert.equal(response.headers.get("idempotency-replayed"), "false");
|
|
assert.match(executed.requestDigest, /^sha256:[a-f0-9]{64}$/);
|
|
assert.equal(executed.commandKind, "owner_scope.ensure");
|
|
assert.deepEqual(executed.command, ownerScopeCommand());
|
|
assert.deepEqual(executed.actor, {
|
|
userRef: "user:engineer",
|
|
hubRole: "admin",
|
|
groupRefs: ["group:engineers", "group:operators"],
|
|
ownerScopes: [{ scopeKind: "company", ownerRef: "client:example" }],
|
|
});
|
|
assert.equal((await response.json()).result.created, true);
|
|
} finally {
|
|
await runtime.close();
|
|
}
|
|
});
|
|
|
|
test("management API rejects unexpected fields before repository execution", async () => {
|
|
let executions = 0;
|
|
const runtime = await startTestServer({
|
|
managementApiEnabled: true,
|
|
managementToken,
|
|
repository: {
|
|
health: async () => "ready",
|
|
executeManagementCommand: async () => {
|
|
executions += 1;
|
|
return { replayed: false, result: {} };
|
|
},
|
|
},
|
|
});
|
|
try {
|
|
const response = await fetch(
|
|
`${runtime.baseUrl}/internal/v1/management/owner-scopes:ensure`,
|
|
{
|
|
method: "POST",
|
|
headers: managementHeaders(),
|
|
body: JSON.stringify({
|
|
...ownerScopeCommand(),
|
|
credential: "must-never-cross-boundary",
|
|
}),
|
|
},
|
|
);
|
|
assert.equal(response.status, 400);
|
|
assert.match(
|
|
(await response.json()).error,
|
|
/device_management_command_field_unexpected:credential/,
|
|
);
|
|
assert.equal(executions, 0);
|
|
} finally {
|
|
await runtime.close();
|
|
}
|
|
});
|
|
|
|
test("management API exposes a repository idempotency conflict without retrying", async () => {
|
|
let executions = 0;
|
|
const runtime = await startTestServer({
|
|
managementApiEnabled: true,
|
|
managementToken,
|
|
repository: {
|
|
health: async () => "ready",
|
|
executeManagementCommand: async () => {
|
|
executions += 1;
|
|
const error = new Error("device_idempotency_key_conflict");
|
|
error.statusCode = 409;
|
|
throw error;
|
|
},
|
|
},
|
|
});
|
|
try {
|
|
const response = await fetch(
|
|
`${runtime.baseUrl}/internal/v1/management/owner-scopes:ensure`,
|
|
{
|
|
method: "POST",
|
|
headers: managementHeaders(),
|
|
body: JSON.stringify(ownerScopeCommand()),
|
|
},
|
|
);
|
|
assert.equal(response.status, 409);
|
|
assert.equal((await response.json()).error, "device_idempotency_key_conflict");
|
|
assert.equal(executions, 1);
|
|
} finally {
|
|
await runtime.close();
|
|
}
|
|
});
|
|
|
|
test("typed service ping accepts a transient access code and never echoes it", async () => {
|
|
const projectRef = "project:11111111-1111-4111-8111-111111111111";
|
|
const deviceRef = "device:22222222-2222-4222-8222-222222222222";
|
|
let planned;
|
|
const runtime = await startTestServer({
|
|
managementApiEnabled: true,
|
|
managementToken,
|
|
repository: {
|
|
health: async () => "ready",
|
|
executeManagementCommand: async () => ({ replayed: false, result: {} }),
|
|
},
|
|
typedCommandRuntime: {
|
|
status: () => ({ commandTransport: "typed-service-ping-v1" }),
|
|
planServicePing: async (value) => {
|
|
planned = value;
|
|
return {
|
|
replayed: false,
|
|
command: {
|
|
commandRef: "command:33333333-3333-4333-8333-333333333333",
|
|
deviceRef,
|
|
commandType: "service.ping",
|
|
lifecycleState: "queued",
|
|
},
|
|
};
|
|
},
|
|
},
|
|
});
|
|
try {
|
|
const response = await fetch(
|
|
`${runtime.baseUrl}/internal/v1/commands:service-ping`,
|
|
{
|
|
method: "POST",
|
|
headers: managementHeaders(),
|
|
body: JSON.stringify({
|
|
projectRef,
|
|
deviceRef,
|
|
accessCode: "654321",
|
|
expiresInSeconds: 300,
|
|
}),
|
|
},
|
|
);
|
|
assert.equal(response.status, 200);
|
|
const body = await response.json();
|
|
assert.equal(body.result.lifecycleState, "queued");
|
|
assert.equal(JSON.stringify(body).includes("654321"), false);
|
|
assert.equal(planned.input.accessCode, "654321");
|
|
assert.equal(planned.idempotencyKey, "phase2-test-0001");
|
|
} finally {
|
|
await runtime.close();
|
|
}
|
|
});
|
|
|
|
test("project query is service-authenticated and forwards only the trusted actor", async () => {
|
|
let queriedActor;
|
|
const runtime = await startTestServer({
|
|
managementApiEnabled: true,
|
|
managementToken,
|
|
repository: {
|
|
health: async () => "ready",
|
|
executeManagementCommand: async () => ({ replayed: false, result: {} }),
|
|
listAccessibleProjects: async (actor) => {
|
|
queriedActor = actor;
|
|
return [{ projectRef: "project:11111111-1111-4111-8111-111111111111" }];
|
|
},
|
|
},
|
|
});
|
|
try {
|
|
const unauthorized = await fetch(
|
|
`${runtime.baseUrl}/internal/v1/query/projects`,
|
|
);
|
|
assert.equal(unauthorized.status, 401);
|
|
|
|
const response = await fetch(
|
|
`${runtime.baseUrl}/internal/v1/query/projects`,
|
|
{ headers: managementHeaders() },
|
|
);
|
|
assert.equal(response.status, 200);
|
|
assert.equal((await response.json()).projects.length, 1);
|
|
assert.deepEqual(queriedActor, {
|
|
userRef: "user:engineer",
|
|
hubRole: "admin",
|
|
groupRefs: ["group:engineers", "group:operators"],
|
|
ownerScopes: [{ scopeKind: "company", ownerRef: "client:example" }],
|
|
});
|
|
} finally {
|
|
await runtime.close();
|
|
}
|
|
});
|
|
|
|
test("project workspace query accepts only a canonical project path", async () => {
|
|
const projectId = "11111111-1111-4111-8111-111111111111";
|
|
let queried;
|
|
const edgeChannels = {
|
|
enabled: true,
|
|
edges: [{
|
|
edgeRegistrationId: "edge:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
|
|
channel: "accepted",
|
|
lastErrorCode: null,
|
|
}],
|
|
};
|
|
const runtime = await startTestServer({
|
|
managementApiEnabled: true,
|
|
managementToken,
|
|
edgeChannelStatusProvider: () => edgeChannels,
|
|
repository: {
|
|
health: async () => "ready",
|
|
executeManagementCommand: async () => ({ replayed: false, result: {} }),
|
|
getProjectWorkspace: async (actor, id, options) => {
|
|
queried = { actor, id, options };
|
|
return { project: { projectRef: `project:${id}` }, devices: [] };
|
|
},
|
|
},
|
|
});
|
|
try {
|
|
const response = await fetch(
|
|
`${runtime.baseUrl}/internal/v1/query/projects/${projectId}/workspace`,
|
|
{ headers: managementHeaders() },
|
|
);
|
|
assert.equal(response.status, 200);
|
|
assert.equal((await response.json()).workspace.devices.length, 0);
|
|
assert.equal(queried.id, projectId);
|
|
assert.equal(queried.options.edgeChannelStatus, edgeChannels);
|
|
|
|
const invalid = await fetch(
|
|
`${runtime.baseUrl}/internal/v1/query/projects/not-a-project/workspace`,
|
|
{ headers: managementHeaders() },
|
|
);
|
|
assert.equal(invalid.status, 404);
|
|
} 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",
|
|
},
|
|
};
|
|
},
|
|
acceptAdapterMessage: async () => {
|
|
throw new Error("must_not_accept_message");
|
|
},
|
|
},
|
|
});
|
|
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(stored.sessionRef, "session:test-001");
|
|
assert.equal(stored.routeRef, null);
|
|
assert.equal(JSON.stringify(stored).includes(fakeImei), false);
|
|
} finally {
|
|
await runtime.close();
|
|
}
|
|
});
|
|
|
|
test("gateway message endpoint returns acceptance only after repository commit", async () => {
|
|
let stored;
|
|
const runtime = await startTestServer({
|
|
discoveryIngestEnabled: true,
|
|
gatewayToken,
|
|
identifierPepper,
|
|
repository: {
|
|
health: async () => "ready",
|
|
upsertQuarantineDiscovery: async () => {
|
|
throw new Error("must_not_observe_discovery");
|
|
},
|
|
acceptAdapterMessage: async (value) => {
|
|
stored = value;
|
|
return {
|
|
acceptance: {
|
|
schemaVersion: "nodedc.device-adapter-acceptance.v1",
|
|
acceptanceRef: "acceptance:test-001",
|
|
idempotencyKey: value.safeView.idempotencyKey,
|
|
status: "accepted",
|
|
replayed: false,
|
|
acceptedAt: "2026-08-11T12:00:00.000Z",
|
|
},
|
|
claimedDeviceRef: "device:11111111-1111-4111-8111-111111111111",
|
|
};
|
|
},
|
|
},
|
|
});
|
|
try {
|
|
const response = await fetch(
|
|
`${runtime.baseUrl}/internal/v1/gateway/messages:accept`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${gatewayToken}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(fakeAdapterMessage()),
|
|
},
|
|
);
|
|
assert.equal(response.status, 201);
|
|
const body = await response.json();
|
|
assert.equal(body.acceptance.status, "accepted");
|
|
assert.match(stored.identifierDigest, /^hmac-sha256:[a-f0-9]{64}$/);
|
|
assert.match(stored.requestDigest, /^sha256:[a-f0-9]{64}$/);
|
|
assert.equal(stored.safeView.identifier.masked, "***********0001");
|
|
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",
|
|
},
|
|
};
|
|
}
|
|
|
|
function fakeAdapterMessage() {
|
|
return {
|
|
schemaVersion: DEVICE_ADAPTER_MESSAGE_SCHEMA,
|
|
edgeRef: "edge:test-001",
|
|
adapterRef: "arusnavi-b2",
|
|
protocolProfileRef: "arusnavi.b2.internal.v1",
|
|
protocol: "INTERNAL",
|
|
sessionRef: "session:test-001",
|
|
messageRef: "package:1:test",
|
|
messageType: "telemetry.package",
|
|
sequence: 1,
|
|
observedAt: "2026-08-11T12:00:00.000Z",
|
|
idempotencyKey: `sha256:${"a".repeat(64)}`,
|
|
identifier: { kind: "imei", value: fakeImei },
|
|
payloadSchemaRef: "arusnavi.internal.package-metadata.v1",
|
|
payload: {
|
|
packageNumber: 1,
|
|
packetCount: 1,
|
|
packageDigest: `sha256:${"b".repeat(64)}`,
|
|
},
|
|
};
|
|
}
|
|
|
|
function managementHeaders({ includeIdempotency = true } = {}) {
|
|
return {
|
|
Authorization: `Bearer ${managementToken}`,
|
|
"Content-Type": "application/json",
|
|
...(includeIdempotency ? { "Idempotency-Key": "phase2-test-0001" } : {}),
|
|
"X-NODEDC-User-Ref": "user:engineer",
|
|
"X-NODEDC-Hub-Role": "admin",
|
|
"X-NODEDC-Group-Refs": "group:operators,group:engineers",
|
|
"X-NODEDC-Owner-Scopes": "company=client:example",
|
|
};
|
|
}
|
|
|
|
function ownerScopeCommand() {
|
|
return {
|
|
scopeKind: "company",
|
|
ownerRef: "client:example",
|
|
displayName: "Example Company",
|
|
};
|
|
}
|
|
|
|
async function startTestServer(options) {
|
|
const server = createControlCoreApp({ identifierPepper, ...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()));
|
|
}),
|
|
};
|
|
}
|