209 lines
6.9 KiB
JavaScript
209 lines
6.9 KiB
JavaScript
#!/usr/bin/env node
|
|
import assert from "node:assert/strict";
|
|
import { createServer } from "node:net";
|
|
|
|
import { createCloudApiServer } from "../internal/cloud/server.mjs";
|
|
import { ENVIRONMENT_DEV } from "../internal/protocol/index.mjs";
|
|
|
|
const previousEnv = {
|
|
HWLAB_CLOUD_DB_URL: process.env.HWLAB_CLOUD_DB_URL,
|
|
HWLAB_CLOUD_DB_SSL_MODE: process.env.HWLAB_CLOUD_DB_SSL_MODE
|
|
};
|
|
|
|
function logOk(name) {
|
|
process.stdout.write(`[cloud-api-runtime-smoke] ok ${name}\n`);
|
|
}
|
|
|
|
async function freePort() {
|
|
const server = createServer();
|
|
await new Promise((resolve, reject) => {
|
|
server.once("error", reject);
|
|
server.listen(0, "127.0.0.1", resolve);
|
|
});
|
|
const address = server.address();
|
|
const port = typeof address === "object" && address ? address.port : null;
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
return port;
|
|
}
|
|
|
|
async function requestJson(url, options = {}) {
|
|
const response = await fetch(url, {
|
|
...options,
|
|
headers: {
|
|
"content-type": "application/json",
|
|
...(options.headers ?? {})
|
|
}
|
|
});
|
|
const text = await response.text();
|
|
return {
|
|
response,
|
|
body: text ? JSON.parse(text) : null
|
|
};
|
|
}
|
|
|
|
function rpcBody(id, method, params = {}) {
|
|
return JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id,
|
|
method,
|
|
params,
|
|
meta: {
|
|
traceId: `trc_${id}`,
|
|
actorId: "usr_cloud_api_runtime_smoke",
|
|
serviceId: "hwlab-cli",
|
|
environment: ENVIRONMENT_DEV
|
|
}
|
|
});
|
|
}
|
|
|
|
async function rpc(baseUrl, id, method, params = {}) {
|
|
const { response, body } = await requestJson(`${baseUrl}/rpc`, {
|
|
method: "POST",
|
|
body: rpcBody(id, method, params)
|
|
});
|
|
assert.equal(response.status, 200, `${method} http status`);
|
|
assert.equal(body.jsonrpc, "2.0", `${method} JSON-RPC version`);
|
|
assert.equal(Object.hasOwn(body, "error"), false, body.error?.message);
|
|
return body.result;
|
|
}
|
|
|
|
async function run() {
|
|
delete process.env.HWLAB_CLOUD_DB_URL;
|
|
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
|
|
|
|
const port = await freePort();
|
|
const server = createCloudApiServer();
|
|
await new Promise((resolve) => server.listen(port, "127.0.0.1", resolve));
|
|
const baseUrl = `http://127.0.0.1:${port}`;
|
|
|
|
try {
|
|
const health = await requestJson(`${baseUrl}/health`);
|
|
assert.equal(health.response.status, 200);
|
|
assert.equal(health.body.serviceId, "hwlab-cloud-api");
|
|
assert.equal(health.body.environment, ENVIRONMENT_DEV);
|
|
assert.equal(health.body.status, "degraded");
|
|
assert.equal(health.body.db.status, "blocked");
|
|
assert.equal(health.body.db.connected, false);
|
|
assert.equal(health.body.db.ready, false);
|
|
assert.deepEqual(health.body.db.missingEnv, ["HWLAB_CLOUD_DB_URL", "HWLAB_CLOUD_DB_SSL_MODE"]);
|
|
assert.equal(health.body.runtime.durable, false);
|
|
assert.equal(health.body.runtime.status, "degraded");
|
|
logOk("health degraded DB contract");
|
|
|
|
const v1 = await requestJson(`${baseUrl}/v1`);
|
|
assert.equal(v1.response.status, 200);
|
|
assert.equal(v1.body.status, "degraded");
|
|
for (const method of [
|
|
"gateway.session.register",
|
|
"box.resource.register",
|
|
"box.capability.report",
|
|
"hardware.operation.request",
|
|
"hardware.invoke.shell",
|
|
"audit.event.query",
|
|
"evidence.record.query"
|
|
]) {
|
|
assert.ok(v1.body.methods.includes(method), `missing ${method}`);
|
|
}
|
|
logOk("REST adapter method contract");
|
|
|
|
const projectId = "prj_cloud-api-runtime-smoke";
|
|
const gatewaySessionId = "gws_cloud-api-runtime-smoke";
|
|
const resourceId = "res_cloud-api-runtime-smoke";
|
|
const capabilityId = "cap_cloud-api-runtime-smoke";
|
|
|
|
const gateway = await rpc(baseUrl, "req_cloud_api_runtime_gateway", "gateway.session.register", {
|
|
projectId,
|
|
gatewaySessionId,
|
|
gatewayId: "gtw_cloud-api-runtime-smoke",
|
|
serviceId: "hwlab-gateway-simu",
|
|
endpoint: "http://127.0.0.1:7101"
|
|
});
|
|
assert.equal(gateway.registered, true);
|
|
assert.equal(gateway.gatewaySession.gatewaySessionId, gatewaySessionId);
|
|
assert.equal(gateway.auditEvent.action, "gateway.session.register");
|
|
|
|
const resource = await rpc(baseUrl, "req_cloud_api_runtime_box", "box.resource.register", {
|
|
projectId,
|
|
gatewaySessionId,
|
|
resourceId,
|
|
boxId: "box_cloud-api-runtime-smoke",
|
|
resourceType: "board",
|
|
state: "available",
|
|
metadata: {
|
|
ports: ["shell0"]
|
|
}
|
|
});
|
|
assert.equal(resource.registered, true);
|
|
assert.equal(resource.resource.resourceId, resourceId);
|
|
|
|
const capability = await rpc(baseUrl, "req_cloud_api_runtime_capability", "box.capability.report", {
|
|
capabilityId,
|
|
resourceId,
|
|
projectId,
|
|
name: "shell.exec",
|
|
direction: "bidirectional",
|
|
valueType: "object",
|
|
mutatesState: true
|
|
});
|
|
assert.equal(capability.reported, true);
|
|
assert.equal(capability.capabilities[0].capabilityId, capabilityId);
|
|
logOk("gateway/box/capability write path");
|
|
|
|
const invoke = await rpc(baseUrl, "req_cloud_api_runtime_shell", "hardware.invoke.shell", {
|
|
projectId,
|
|
gatewaySessionId,
|
|
resourceId,
|
|
capabilityId,
|
|
input: {
|
|
command: "echo hwlab"
|
|
}
|
|
});
|
|
assert.equal(invoke.accepted, true);
|
|
assert.equal(invoke.operation.status, "accepted");
|
|
assert.equal(invoke.dispatch.shellExecuted, false);
|
|
assert.equal(invoke.dispatch.dispatchStatus, "not_connected");
|
|
assert.equal(invoke.evidenceRecord.kind, "trace");
|
|
assert.equal(invoke.evidenceRecord.serviceId, "hwlab-cloud-api");
|
|
assert.match(invoke.evidenceRecord.sha256, /^[a-f0-9]{64}$/);
|
|
logOk("hardware invoke shell degraded dispatch with evidence");
|
|
|
|
const audit = await rpc(baseUrl, "req_cloud_api_runtime_audit_query", "audit.event.query", {
|
|
projectId
|
|
});
|
|
assert.ok(audit.count >= 5);
|
|
assert.ok(audit.events.every((event) => event.serviceId === "hwlab-cloud-api"));
|
|
assert.ok(audit.events.every((event) => event.environment === ENVIRONMENT_DEV));
|
|
assert.ok(audit.events.some((event) => event.action === "hardware.operation.request"));
|
|
assert.ok(audit.events.some((event) => event.action === "hardware.invoke.shell"));
|
|
|
|
const evidence = await rpc(baseUrl, "req_cloud_api_runtime_evidence_query", "evidence.record.query", {
|
|
projectId
|
|
});
|
|
assert.equal(evidence.count, 1);
|
|
assert.equal(evidence.records[0].operationId, invoke.operationId);
|
|
assert.equal(evidence.records[0].metadata.dispatchStatus, "not_connected");
|
|
logOk("audit/evidence query shape");
|
|
|
|
process.stdout.write("[cloud-api-runtime-smoke] passed\n");
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
restoreEnv();
|
|
}
|
|
}
|
|
|
|
function restoreEnv() {
|
|
for (const [name, value] of Object.entries(previousEnv)) {
|
|
if (value === undefined) {
|
|
delete process.env[name];
|
|
} else {
|
|
process.env[name] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
await run();
|