Files
NODEDC_DEVICE_CORE/services/device-control-core/test/control-resource-migration.test.mjs
T

105 lines
3.8 KiB
JavaScript

import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
const schemaUrl = new URL(
"../migrations/010_device_control_resources.sql",
import.meta.url,
);
const commandsUrl = new URL(
"../migrations/011_device_control_resource_commands.sql",
import.meta.url,
);
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
const appUrl = new URL("../src/app.mjs", import.meta.url);
test("control resource schema separates bindings, revisions and current state", async () => {
const sql = await readFile(schemaUrl, "utf8");
assert.match(sql, /create table if not exists device_resource_bindings/);
assert.match(sql, /pending_external_approval/);
assert.match(sql, /external_approval_digest/);
assert.match(sql, /device_binding_source_scope_mismatch/);
assert.match(sql, /create table if not exists device_configuration_revisions/);
assert.match(sql, /create table if not exists device_configuration_state/);
assert.match(sql, /device_configuration_revision_scope_mismatch/);
assert.match(sql, /unique \(device_id, revision_number\)/);
});
test("command ledger has honest ordered states without a transport API", async () => {
const sql = await readFile(schemaUrl, "utf8");
const app = await readFile(appUrl, "utf8");
assert.match(sql, /create table if not exists device_commands/);
assert.match(sql, /create table if not exists device_command_events/);
for (const state of [
"draft",
"planned",
"awaiting_confirmation",
"queued",
"dispatched",
"acknowledged",
"verified",
"failed",
"expired",
"unknown",
]) {
assert.match(sql, new RegExp(`'${state}'`));
}
assert.match(sql, /device_command_initial_event_invalid/);
assert.match(sql, /device_command_event_sequence_invalid/);
assert.match(sql, /device_command_event_transition_invalid/);
assert.match(sql, /device_command_event_projection_mismatch/);
assert.match(sql, /device_command_events_current_projection_guard/);
assert.match(sql, /device_command_current_projection_mismatch/);
assert.doesNotMatch(app, /device-commands:(?:plan|confirm|dispatch)/);
});
test("configuration, command history and audit are append-only", async () => {
const sql = await readFile(schemaUrl, "utf8");
for (const table of [
"device_configuration_revisions",
"device_command_events",
"device_audit_events",
]) {
assert.match(
sql,
new RegExp(`${table}_immutable_guard[\\s\\S]*before update or delete or truncate`),
);
}
assert.match(sql, /device_immutable_record_mutation_forbidden/);
assert.match(sql, /device_transfer_active_resource_binding/);
assert.match(sql, /device_transfer_applied_configuration/);
assert.match(sql, /device_transfer_nonterminal_command/);
});
test("control resource schema contains no seeded device or raw secret material", async () => {
const sql = await readFile(schemaUrl, "utf8");
assert.doesNotMatch(sql, /insert\s+into/i);
assert.doesNotMatch(sql, /dcctouch|arusnavi|gelios|\bb2\b|imei/i);
assert.doesNotMatch(sql, /password\s+text|token\s+text|secret\s+text|raw_command|raw_packet/i);
});
test("commands extend receipts only after their schema", async () => {
const commands = await readFile(commandsUrl, "utf8");
const repository = await readFile(repositoryUrl, "utf8");
for (const kind of [
"device_binding.ensure",
"device_binding.revoke",
"device_configuration_revision.create",
"device_configuration_desired.set",
]) {
assert.match(commands, new RegExp(`'${kind.replace(".", "\\.")}'`));
}
const schemaIndex = repository.indexOf("010_device_control_resources.sql");
const commandsIndex = repository.indexOf(
"011_device_control_resource_commands.sql",
);
assert.notEqual(schemaIndex, -1);
assert.notEqual(commandsIndex, -1);
assert.ok(schemaIndex < commandsIndex);
});