feat: add cloud api runtime smoke
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user