update server
This commit is contained in:
@@ -0,0 +1,665 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const crypto = require("node:crypto");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { after, before, test } = require("node:test");
|
||||
|
||||
const { createApp } = require("../src/app");
|
||||
const { JsonStore } = require("../src/store");
|
||||
|
||||
let baseUrl;
|
||||
let dataDirectory;
|
||||
let server;
|
||||
let licensePrivateKey;
|
||||
let licensePublicKeyPath;
|
||||
|
||||
async function post(payload) {
|
||||
const response = await fetch(`${baseUrl}/api`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
assert.equal(response.status, 200);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function signLicense(payload) {
|
||||
const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString("base64");
|
||||
const signature = crypto.sign("sha256", Buffer.from(payloadBase64), {
|
||||
key: licensePrivateKey,
|
||||
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
|
||||
saltLength: crypto.constants.RSA_PSS_SALTLEN_MAX
|
||||
});
|
||||
return `${payloadBase64}|${signature.toString("base64")}`;
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
dataDirectory = await fs.promises.mkdtemp(path.join(os.tmpdir(), "reinloop-server-"));
|
||||
const keyPair = crypto.generateKeyPairSync("rsa", { modulusLength: 2048 });
|
||||
licensePrivateKey = keyPair.privateKey;
|
||||
licensePublicKeyPath = path.join(dataDirectory, "license-public.pem");
|
||||
await fs.promises.writeFile(licensePublicKeyPath, keyPair.publicKey.export({ type: "spki", format: "pem" }));
|
||||
const store = new JsonStore(dataDirectory);
|
||||
await store.initialize();
|
||||
const app = createApp({ store, adminToken: "test-token", licensePublicKeyPath });
|
||||
await new Promise((resolve) => {
|
||||
server = app.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
baseUrl = `http://127.0.0.1:${server.address().port}`;
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
||||
await fs.promises.rm(dataDirectory, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("health endpoint reports ready", async () => {
|
||||
const response = await fetch(`${baseUrl}/health`);
|
||||
assert.deepEqual(await response.json(), { success: true, service: "reinloop-server" });
|
||||
});
|
||||
|
||||
test("root endpoint accepts API requests without the /api suffix", async () => {
|
||||
const response = await fetch(baseUrl, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ type: "listOrganizations", adminToken: "test-token" })
|
||||
});
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(await response.json(), { success: true, companies: [] });
|
||||
});
|
||||
|
||||
test("legacy data_record endpoint and uploadUserInfo type are unavailable", async () => {
|
||||
const legacyRoute = await fetch(`${baseUrl}/data_record`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ type: "listOrganizations", adminToken: "test-token" })
|
||||
});
|
||||
assert.equal(legacyRoute.status, 404);
|
||||
|
||||
const legacyType = await post({
|
||||
type: "uploadUserInfo", _id: "legacy", username: "legacy",
|
||||
issued: "2026-07-25 12:00", expiry: "2027-07-25 12:00", license: "legacy"
|
||||
});
|
||||
assert.equal(legacyType.success, false);
|
||||
assert.match(legacyType.errMsg, /无效/);
|
||||
});
|
||||
|
||||
test("existing two-step client flow uploads, lists, and downloads a file", async () => {
|
||||
const issued = await post({ type: "uploadDataFile", fileName: "result.csv", folder: "customer/line-1/ind_data" });
|
||||
assert.equal(issued.success, true);
|
||||
assert.ok(issued.uploadMetadata.authorization);
|
||||
|
||||
const form = new FormData();
|
||||
form.append("key", issued.uploadMetadata.cosFileId);
|
||||
form.append("Signature", issued.uploadMetadata.authorization);
|
||||
form.append("x-cos-security-token", issued.uploadMetadata.token);
|
||||
form.append("x-cos-meta-fileid", issued.uploadMetadata.fileId);
|
||||
form.append("file", new Blob(["time,pressure\n0,10\n"], { type: "text/csv" }), "result.csv");
|
||||
const uploaded = await fetch(issued.uploadMetadata.url, { method: "POST", body: form });
|
||||
assert.equal(uploaded.status, 204);
|
||||
|
||||
const listed = await post({ type: "listModels", folder: "customer/line-1/ind_data" });
|
||||
assert.deepEqual(listed.files, ["result.csv"]);
|
||||
assert.equal(listed.fileList[0].fileID, issued.fileID);
|
||||
|
||||
const directDownload = await fetch(`${baseUrl}/files/${encodeURIComponent(issued.fileID)}`);
|
||||
assert.equal(directDownload.status, 403);
|
||||
const rejectedDownload = await post({ type: "downloadModel", fileID: issued.fileID });
|
||||
assert.equal(rejectedDownload.success, false);
|
||||
const download = await post({
|
||||
type: "downloadModel", fileID: issued.fileID, adminToken: "test-token"
|
||||
});
|
||||
const downloaded = await fetch(download.url);
|
||||
assert.equal(await downloaded.text(), "time,pressure\n0,10\n");
|
||||
});
|
||||
|
||||
test("admin lists, downloads, and deletes only the selected device control data", async () => {
|
||||
async function uploadControlData(deviceId, fileName, content) {
|
||||
const issued = await post({
|
||||
type: "uploadDataFile", fileName, folder: `${deviceId}/data_record/run-1`
|
||||
});
|
||||
assert.equal(issued.success, true);
|
||||
const form = new FormData();
|
||||
form.append("file", new Blob([content]), fileName);
|
||||
assert.equal((await fetch(issued.uploadMetadata.url, { method: "POST", body: form })).status, 204);
|
||||
return issued;
|
||||
}
|
||||
|
||||
const deviceId = "control-co/line-1";
|
||||
const first = await uploadControlData(deviceId, "episode_part1.pkl", "part-1");
|
||||
const second = await uploadControlData(deviceId, "episode_manifest.json", '{"parts":1}');
|
||||
await uploadControlData("other-co/line-2", "other.pkl", "other");
|
||||
|
||||
const rejectedList = await post({ type: "listControlFiles", deviceId });
|
||||
assert.equal(rejectedList.success, false);
|
||||
const listed = await post({
|
||||
type: "listControlFiles", deviceId, page: 1, pageSize: 500, adminToken: "test-token"
|
||||
});
|
||||
assert.equal(listed.total, 2);
|
||||
assert.equal(listed.pageSize, 100);
|
||||
assert.deepEqual(new Set(listed.files.map((item) => item.fileID)), new Set([first.fileID, second.fileID]));
|
||||
|
||||
const forbiddenDownload = await post({
|
||||
type: "getControlFileDownload", fileID: "local://ReinLoop_GUI/other-co/line-2/ind_data/result.csv",
|
||||
adminToken: "test-token"
|
||||
});
|
||||
assert.equal(forbiddenDownload.success, false);
|
||||
const download = await post({
|
||||
type: "getControlFileDownload", fileID: second.fileID, adminToken: "test-token"
|
||||
});
|
||||
assert.equal(download.success, true);
|
||||
assert.equal(download.size, Buffer.byteLength('{"parts":1}'));
|
||||
assert.match(download.url, /expires=.*token=/);
|
||||
assert.equal(await (await fetch(download.url)).text(), '{"parts":1}');
|
||||
|
||||
const forbiddenDelete = await post({
|
||||
type: "deleteControlFile", fileID: "model://control-co/line-1/controller.bin", adminToken: "test-token"
|
||||
});
|
||||
assert.equal(forbiddenDelete.success, false);
|
||||
const deleted = await post({ type: "deleteControlFile", fileID: first.fileID, adminToken: "test-token" });
|
||||
assert.deepEqual(deleted, { success: true, deletedCount: 1 });
|
||||
assert.equal((await post({
|
||||
type: "deleteControlFile", fileID: first.fileID, adminToken: "test-token"
|
||||
})).success, false);
|
||||
});
|
||||
|
||||
test("model uploads are stored under data/models/company/line", async () => {
|
||||
const issued = await post({
|
||||
type: "issueModelUpload", deviceId: "company-a/line-1",
|
||||
fileName: "controller.bin", adminToken: "test-token"
|
||||
});
|
||||
assert.equal(issued.fileID, "model://company-a/line-1/controller.bin");
|
||||
|
||||
const form = new FormData();
|
||||
form.append("file", new Blob(["model-content"]), "controller.bin");
|
||||
assert.equal((await fetch(issued.uploadMetadata.url, { method: "POST", body: form })).status, 204);
|
||||
assert.equal(
|
||||
await fs.promises.readFile(path.join(dataDirectory, "models", "company-a", "line-1", "controller.bin"), "utf8"),
|
||||
"model-content"
|
||||
);
|
||||
|
||||
const listed = await post({ type: "listModels", folder: "company-a/line-1/model_config" });
|
||||
assert.equal(listed.fileList[0].fileID, issued.fileID);
|
||||
});
|
||||
|
||||
test("model upload supports renaming while preserving the original file name", async () => {
|
||||
const issued = await post({
|
||||
type: "issueModelUpload", deviceId: "company-a/rename-line",
|
||||
fileName: "controller-original.bin", modelName: "pressure-controller-v2.bin",
|
||||
adminToken: "test-token"
|
||||
});
|
||||
assert.equal(issued.success, true);
|
||||
assert.equal(issued.fileName, "pressure-controller-v2.bin");
|
||||
assert.equal(issued.originalFileName, "controller-original.bin");
|
||||
assert.equal(issued.fileID, "model://company-a/rename-line/pressure-controller-v2.bin");
|
||||
|
||||
const form = new FormData();
|
||||
form.append("file", new Blob(["renamed-model"]), "controller-original.bin");
|
||||
assert.equal((await fetch(issued.uploadMetadata.url, { method: "POST", body: form })).status, 204);
|
||||
|
||||
const listed = await post({
|
||||
type: "listModels", folder: "company-a/rename-line/model_config"
|
||||
});
|
||||
assert.deepEqual(listed.files, ["pressure-controller-v2.bin"]);
|
||||
assert.equal(listed.fileList[0].fileName, "pressure-controller-v2.bin");
|
||||
assert.equal(listed.fileList[0].originalFileName, "controller-original.bin");
|
||||
assert.equal(
|
||||
await fs.promises.readFile(path.join(dataDirectory, "models", "company-a", "rename-line", "pressure-controller-v2.bin"), "utf8"),
|
||||
"renamed-model"
|
||||
);
|
||||
|
||||
const invalid = await post({
|
||||
type: "issueModelUpload", deviceId: "company-a/rename-line",
|
||||
fileName: "controller.bin", modelName: "controller.zip",
|
||||
adminToken: "test-token"
|
||||
});
|
||||
assert.equal(invalid.success, false);
|
||||
assert.match(invalid.errMsg, /扩展名/);
|
||||
});
|
||||
|
||||
test("model upload requires explicit overwrite for an existing file", async () => {
|
||||
const request = {
|
||||
type: "issueModelUpload", deviceId: "company-a/overwrite-line",
|
||||
fileName: "controller.bin", adminToken: "test-token"
|
||||
};
|
||||
const issued = await post(request);
|
||||
const firstForm = new FormData();
|
||||
firstForm.append("file", new Blob(["first-version"]), "controller.bin");
|
||||
assert.equal((await fetch(issued.uploadMetadata.url, { method: "POST", body: firstForm })).status, 204);
|
||||
|
||||
const conflict = await post(request);
|
||||
assert.equal(conflict.success, false);
|
||||
assert.equal(conflict.conflict, true);
|
||||
assert.equal(conflict.existing.fileID, issued.fileID);
|
||||
|
||||
const replacement = await post({ ...request, overwrite: true });
|
||||
assert.equal(replacement.success, true);
|
||||
const replacementForm = new FormData();
|
||||
replacementForm.append("file", new Blob(["second-version"]), "controller.bin");
|
||||
assert.equal((await fetch(replacement.uploadMetadata.url, {
|
||||
method: "POST", body: replacementForm
|
||||
})).status, 204);
|
||||
assert.equal(
|
||||
await fs.promises.readFile(path.join(dataDirectory, "models", "company-a", "overwrite-line", "controller.bin"), "utf8"),
|
||||
"second-version"
|
||||
);
|
||||
});
|
||||
|
||||
test("panel consumes uploaded device files from an inbox without scanning folders", async () => {
|
||||
const deviceId = "panel-company/panel-line";
|
||||
const issued = await post({
|
||||
type: "uploadDataFile", fileName: "result_20260724_120000.csv",
|
||||
folder: `${deviceId}/ind_data`
|
||||
});
|
||||
const form = new FormData();
|
||||
form.append("file", new Blob(["t,u,p\n0,10,20\n"], { type: "text/csv" }), issued.fileID);
|
||||
assert.equal((await fetch(issued.uploadMetadata.url, { method: "POST", body: form })).status, 204);
|
||||
|
||||
const beforeRegistration = await post({
|
||||
type: "getPendingPanelFile", deviceId, adminToken: "test-token"
|
||||
});
|
||||
assert.equal(beforeRegistration.pending, false);
|
||||
|
||||
await post({
|
||||
type: "registerIdentificationResult", deviceId,
|
||||
runId: "result_20260724_120000.csv"
|
||||
});
|
||||
const pending = await post({
|
||||
type: "getPendingPanelFile", deviceId, adminToken: "test-token"
|
||||
});
|
||||
assert.equal(pending.pending, true);
|
||||
assert.equal(pending.fileName, "result_20260724_120000.csv");
|
||||
assert.equal(await (await fetch(pending.url)).text(), "t,u,p\n0,10,20\n");
|
||||
|
||||
assert.equal((await post({
|
||||
type: "ackPanelFile", deviceId, fileID: pending.fileID,
|
||||
adminToken: "test-token"
|
||||
})).deleted, 1);
|
||||
assert.equal((await post({
|
||||
type: "getPendingPanelFile", deviceId, adminToken: "test-token"
|
||||
})).pending, false);
|
||||
|
||||
const jsonIssued = await post({
|
||||
type: "uploadDataFile", fileName: "travel_stability_pressures_20260724_120000.json",
|
||||
folder: `${deviceId}/ind_data`
|
||||
});
|
||||
const jsonForm = new FormData();
|
||||
jsonForm.append("file", new Blob(['{"stable_pressures":[]}'], {
|
||||
type: "application/json"
|
||||
}), jsonIssued.fileID);
|
||||
assert.equal((await fetch(jsonIssued.uploadMetadata.url, {
|
||||
method: "POST", body: jsonForm
|
||||
})).status, 204);
|
||||
const jsonPending = await post({
|
||||
type: "getPendingPanelFile", deviceId, adminToken: "test-token"
|
||||
});
|
||||
assert.equal(jsonPending.pending, true);
|
||||
assert.equal(jsonPending.mediaType, "json");
|
||||
|
||||
const rejectedHistory = await post({ type: "listIdentificationFiles", deviceId });
|
||||
assert.equal(rejectedHistory.success, false);
|
||||
const history = await post({
|
||||
type: "listIdentificationFiles", deviceId, adminToken: "test-token"
|
||||
});
|
||||
assert.equal(history.total, 2);
|
||||
const csvHistory = history.files.find((file) => file.fileID === pending.fileID);
|
||||
assert.equal(csvHistory.status, "processed");
|
||||
assert.ok(csvHistory.processedAt);
|
||||
assert.ok(csvHistory.expiresAt);
|
||||
|
||||
const rejectedHistoryDownload = await post({
|
||||
type: "getIdentificationFileDownload", fileID: pending.fileID
|
||||
});
|
||||
assert.equal(rejectedHistoryDownload.success, false);
|
||||
const historyDownload = await post({
|
||||
type: "getIdentificationFileDownload", fileID: pending.fileID,
|
||||
adminToken: "test-token"
|
||||
});
|
||||
assert.equal(await (await fetch(historyDownload.url)).text(), "t,u,p\n0,10,20\n");
|
||||
|
||||
const rejectedDelete = await post({
|
||||
type: "deleteIdentificationFile", fileID: jsonPending.fileID
|
||||
});
|
||||
assert.equal(rejectedDelete.success, false);
|
||||
const deleted = await post({
|
||||
type: "deleteIdentificationFile", fileID: jsonPending.fileID,
|
||||
adminToken: "test-token"
|
||||
});
|
||||
assert.equal(deleted.deletedCount, 1);
|
||||
assert.equal((await fetch(jsonPending.url)).status, 404);
|
||||
});
|
||||
|
||||
test("expired processed identification files are purged without affecting pending files", async () => {
|
||||
const deviceId = "retention-company/retention-line";
|
||||
const processedIssued = await post({
|
||||
type: "uploadDataFile", fileName: "processed.csv", folder: `${deviceId}/ind_data`
|
||||
});
|
||||
const processedForm = new FormData();
|
||||
processedForm.append("file", new Blob(["processed"]), "processed.csv");
|
||||
assert.equal((await fetch(processedIssued.uploadMetadata.url, {
|
||||
method: "POST", body: processedForm
|
||||
})).status, 204);
|
||||
await post({
|
||||
type: "ackPanelFile", deviceId, fileID: processedIssued.fileID,
|
||||
adminToken: "test-token"
|
||||
});
|
||||
|
||||
const pendingIssued = await post({
|
||||
type: "uploadDataFile", fileName: "pending.json", folder: `${deviceId}/ind_data`
|
||||
});
|
||||
const pendingForm = new FormData();
|
||||
pendingForm.append("file", new Blob(["{}"]), "pending.json");
|
||||
assert.equal((await fetch(pendingIssued.uploadMetadata.url, {
|
||||
method: "POST", body: pendingForm
|
||||
})).status, 204);
|
||||
|
||||
const store = new JsonStore(dataDirectory);
|
||||
await store.update((database) => {
|
||||
const record = database.identificationFiles.find((item) => item.fileID === processedIssued.fileID);
|
||||
record.expiresAt = new Date(Date.now() - 1000).toISOString();
|
||||
});
|
||||
const history = await post({
|
||||
type: "listIdentificationFiles", deviceId, adminToken: "test-token"
|
||||
});
|
||||
assert.deepEqual(history.files.map((file) => file.fileID), [pendingIssued.fileID]);
|
||||
assert.equal(history.files[0].status, "pending");
|
||||
assert.equal(await fs.promises.access(
|
||||
path.join(dataDirectory, "files", "ReinLoop_GUI", deviceId, "ind_data", "processed.csv")
|
||||
).then(() => true, () => false), false);
|
||||
});
|
||||
|
||||
test("identification feedback supports register, publish, poll, and acknowledge", async () => {
|
||||
assert.equal((await post({
|
||||
type: "registerIdentificationResult", deviceId: "customer-a/line-a", runId: "run-1"
|
||||
})).success, true);
|
||||
assert.deepEqual(await post({
|
||||
type: "getIdentificationFeedback", deviceId: "customer-a/line-a", runId: "run-1"
|
||||
}), { success: true, ready: false });
|
||||
|
||||
const published = await post({
|
||||
type: "setIdentificationFeedback", deviceId: "customer-a/line-a", runId: "run-1",
|
||||
result: 1, adminToken: "test-token"
|
||||
});
|
||||
assert.equal(published.result, 1);
|
||||
assert.equal((await post({
|
||||
type: "getIdentificationFeedback", deviceId: "customer-a/line-a", runId: "run-1"
|
||||
})).result, 1);
|
||||
assert.equal((await post({
|
||||
type: "ackIdentificationFeedback", deviceId: "customer-a/line-a", runId: "run-1"
|
||||
})).deleted, 1);
|
||||
});
|
||||
|
||||
test("control panel publishes the device identification CSV consumed by ReinLoop", async () => {
|
||||
const parameters = {
|
||||
q_in_val: 91, dt: 0.1, n_order: 8, t_c: 2.5,
|
||||
levels: [10, 20, 30, 40, 50, 60, 70, 80],
|
||||
dead_area: 0, xa_full: 1000, V_val: 1, repeat: 2
|
||||
};
|
||||
const issued = await post({
|
||||
type: "publishIdentificationConfig", deviceId: "customer-a/line-a",
|
||||
parameters, adminToken: "test-token"
|
||||
});
|
||||
assert.equal(issued.success, true);
|
||||
|
||||
const invalid = await post({
|
||||
type: "publishIdentificationConfig", deviceId: "customer-a/line-a",
|
||||
parameters: { ...parameters, levels: [1000, 900, 800] },
|
||||
adminToken: "test-token"
|
||||
});
|
||||
assert.equal(invalid.success, false);
|
||||
assert.match(invalid.errMsg, /levels/);
|
||||
|
||||
const csv = [
|
||||
"parameter,value",
|
||||
"q_in_val,91",
|
||||
"dt,0.1",
|
||||
"n_order,8",
|
||||
"t_c,2.5",
|
||||
'levels,"10,20,30,40,50,60,70,80"',
|
||||
"dead_area,0",
|
||||
"xa_full,1000",
|
||||
"V_val,1",
|
||||
"repeat,2",
|
||||
""
|
||||
].join("\n");
|
||||
const form = new FormData();
|
||||
form.append("file", new Blob([csv], { type: "text/csv" }), "identification_config.csv");
|
||||
assert.equal((await fetch(issued.uploadMetadata.url, {
|
||||
method: "POST", body: form
|
||||
})).status, 204);
|
||||
|
||||
const available = await post({
|
||||
type: "getIdentificationConfig", deviceId: "customer-a/line-a"
|
||||
});
|
||||
assert.equal(available.success, true);
|
||||
assert.equal(
|
||||
available.cloudPath,
|
||||
"ReinLoop_GUI/customer-a/line-a/identification_config/identification_config.csv"
|
||||
);
|
||||
assert.equal(await (await fetch(available.url)).text(), csv);
|
||||
});
|
||||
|
||||
test("volume publishing becomes readable only after the file upload completes", async () => {
|
||||
const parameters = {
|
||||
q_in_val: 91, dt: 0.1, xa_full: 1000, p_max: 200,
|
||||
fit_low: 50, fit_high: 200, T_delta: 30, num_runs: 6
|
||||
};
|
||||
const issued = await post({
|
||||
type: "publishVolumeConfig", parameters, adminToken: "test-token"
|
||||
});
|
||||
const beforeUpload = await post({ type: "getFunctionConfig", configType: "volume" });
|
||||
assert.equal(beforeUpload.notFound, true);
|
||||
|
||||
const form = new FormData();
|
||||
form.append("file", new Blob([JSON.stringify(parameters)], {
|
||||
type: "application/json"
|
||||
}), "volume_config.json");
|
||||
assert.equal((await fetch(issued.uploadMetadata.url, {
|
||||
method: "POST", body: form
|
||||
})).status, 204);
|
||||
|
||||
const published = await post({ type: "getFunctionConfig", configType: "volume" });
|
||||
assert.equal(published.success, true);
|
||||
assert.deepEqual(published.parameters, parameters);
|
||||
});
|
||||
|
||||
test("volume request accepts only the file uploaded for that request", async () => {
|
||||
const request = await post({ type: "createVolumeConfigRequest", deviceId: "customer-a/line-a" });
|
||||
const folder = `customer-a/line-a/volume_config_requests/${request.requestId}`;
|
||||
const issued = await post({ type: "uploadDataFile", fileName: "volume_measurement.json", folder });
|
||||
const form = new FormData();
|
||||
form.append("file", new Blob(["{\"num_runs\":2}"], { type: "application/json" }), "volume_measurement.json");
|
||||
assert.equal((await fetch(issued.uploadMetadata.url, { method: "POST", body: form })).status, 204);
|
||||
|
||||
const submitted = await post({
|
||||
type: "submitVolumeConfigFile", deviceId: "customer-a/line-a", requestId: request.requestId,
|
||||
fileID: issued.fileID, fileName: "volume_measurement.json"
|
||||
});
|
||||
assert.equal(submitted.success, true);
|
||||
const ready = await post({
|
||||
type: "getVolumeConfigRequest", deviceId: "customer-a/line-a", requestId: request.requestId
|
||||
});
|
||||
assert.equal(ready.ready, true);
|
||||
assert.equal(
|
||||
ready.cloudPath,
|
||||
`ReinLoop_GUI/customer-a/line-a/volume_config_requests/${request.requestId}/volume_measurement.json`
|
||||
);
|
||||
assert.deepEqual(await (await fetch(ready.url)).json(), { num_runs: 2 });
|
||||
});
|
||||
|
||||
test("admin manages companies and production lines with a stable device id", async () => {
|
||||
const unauthorized = await post({
|
||||
type: "createCompany", name: "未授权公司", code: "blocked"
|
||||
});
|
||||
assert.equal(unauthorized.success, false);
|
||||
|
||||
const company = await post({
|
||||
type: "createCompany", name: "示例公司", code: "sample-co",
|
||||
adminToken: "test-token"
|
||||
});
|
||||
assert.equal(company.success, true);
|
||||
assert.equal(company.company.code, "sample-co");
|
||||
|
||||
const line = await post({
|
||||
type: "createProductionLine", companyId: company.company.id,
|
||||
name: "一号产线", code: "line-1", adminToken: "test-token"
|
||||
});
|
||||
assert.equal(line.success, true);
|
||||
assert.equal(line.productionLine.deviceId, "sample-co/line-1");
|
||||
|
||||
const organizations = await post({
|
||||
type: "listOrganizations", adminToken: "test-token"
|
||||
});
|
||||
assert.equal(organizations.companies.length, 1);
|
||||
const listedLine = organizations.companies[0].productionLines[0];
|
||||
assert.equal(listedLine.id, line.productionLine.id);
|
||||
assert.equal(listedLine.online, false);
|
||||
assert.equal(listedLine.lastSeenAt, null);
|
||||
|
||||
const heartbeat = await post({ type: "deviceHeartbeat", deviceId: line.productionLine.deviceId });
|
||||
assert.equal(heartbeat.success, true);
|
||||
assert.equal(heartbeat.deviceId, line.productionLine.deviceId);
|
||||
assert.ok(heartbeat.lastSeenAt);
|
||||
|
||||
const onlineOrganizations = await post({ type: "listOrganizations", adminToken: "test-token" });
|
||||
const onlineLine = onlineOrganizations.companies[0].productionLines[0];
|
||||
assert.equal(onlineLine.online, true);
|
||||
assert.equal(onlineLine.lastSeenAt, heartbeat.lastSeenAt);
|
||||
|
||||
const store = new JsonStore(dataDirectory);
|
||||
await store.update((database) => {
|
||||
const storedLine = database.productionLines.find((item) => item.id === line.productionLine.id);
|
||||
storedLine.lastSeenAt = new Date(Date.now() - 30_001).toISOString();
|
||||
});
|
||||
const offlineOrganizations = await post({ type: "listOrganizations", adminToken: "test-token" });
|
||||
assert.equal(offlineOrganizations.companies[0].productionLines[0].online, false);
|
||||
|
||||
const unknownDevice = await post({ type: "deviceHeartbeat", deviceId: "unknown-co/unknown-line" });
|
||||
assert.equal(unknownDevice.success, false);
|
||||
});
|
||||
|
||||
test("model write operations require an admin token", async () => {
|
||||
const rejectedUpload = await post({
|
||||
type: "uploadDataFile", fileName: "model.bin", folder: "company-a/line-a/model_config"
|
||||
});
|
||||
assert.equal(rejectedUpload.success, false);
|
||||
|
||||
const issued = await post({
|
||||
type: "issueModelUpload", deviceId: "company-a/line-a", fileName: "model.bin",
|
||||
adminToken: "test-token"
|
||||
});
|
||||
assert.equal(issued.success, true);
|
||||
const form = new FormData();
|
||||
form.append("file", new Blob(["model"], { type: "application/octet-stream" }), "model.bin");
|
||||
assert.equal((await fetch(issued.uploadMetadata.url, { method: "POST", body: form })).status, 204);
|
||||
|
||||
const rejectedDelete = await post({ type: "deleteFile", fileID: issued.fileID });
|
||||
assert.equal(rejectedDelete.success, false);
|
||||
const deleted = await post({ type: "deleteModel", fileID: issued.fileID, adminToken: "test-token" });
|
||||
assert.equal(deleted.deletedCount, 1);
|
||||
});
|
||||
|
||||
test("issued licenses can be listed, validated, and revoked", async () => {
|
||||
const company = await post({
|
||||
type: "createCompany", name: "许可证公司", code: "licensed-co",
|
||||
adminToken: "test-token"
|
||||
});
|
||||
const line = await post({
|
||||
type: "createProductionLine", companyId: company.company.id,
|
||||
name: "测试线", code: "test-line", adminToken: "test-token"
|
||||
});
|
||||
const licenseId = crypto.randomUUID();
|
||||
const licensePayload = {
|
||||
license_id: licenseId,
|
||||
company_id: company.company.id,
|
||||
production_line_id: line.productionLine.id,
|
||||
customer: "许可证公司",
|
||||
device_id: "licensed-co/test-line",
|
||||
issued: "2026-07-25 12:00",
|
||||
expiry: "2027-07-25 12:00",
|
||||
features: "*"
|
||||
};
|
||||
const issued = await post({
|
||||
type: "createLicense", licenseId,
|
||||
companyId: company.company.id, productionLineId: line.productionLine.id,
|
||||
customer: "许可证公司", issued: "2026-07-25 12:00",
|
||||
expiry: "2027-07-25 12:00", features: "*",
|
||||
license: signLicense(licensePayload), adminToken: "test-token"
|
||||
});
|
||||
assert.equal(issued.success, true);
|
||||
assert.equal(issued.license.status, "active");
|
||||
assert.equal(issued.license.deviceId, "licensed-co/test-line");
|
||||
assert.equal("license" in issued.license, false);
|
||||
|
||||
const validation = await post({
|
||||
type: "validateLicense", licenseId,
|
||||
deviceId: "licensed-co/test-line"
|
||||
});
|
||||
assert.deepEqual(validation, {
|
||||
success: true, valid: true, status: "active", licenseId
|
||||
});
|
||||
|
||||
const listed = await post({ type: "listLicenses", adminToken: "test-token" });
|
||||
assert.equal(listed.licenses.length, 1);
|
||||
assert.equal(listed.licenses[0].customer, "许可证公司");
|
||||
|
||||
const revoked = await post({
|
||||
type: "revokeLicense", licenseId,
|
||||
reason: "合同终止", adminToken: "test-token"
|
||||
});
|
||||
assert.equal(revoked.license.status, "revoked");
|
||||
assert.equal(revoked.license.revocationReason, "合同终止");
|
||||
|
||||
const rejected = await post({
|
||||
type: "validateLicense", licenseId,
|
||||
deviceId: "licensed-co/test-line"
|
||||
});
|
||||
assert.equal(rejected.valid, false);
|
||||
assert.equal(rejected.status, "revoked");
|
||||
});
|
||||
|
||||
test("license creation verifies the signed payload and validates expiry in real time", async () => {
|
||||
const company = await post({
|
||||
type: "createCompany", name: "安全校验公司", code: "security-co", adminToken: "test-token"
|
||||
});
|
||||
const line = await post({
|
||||
type: "createProductionLine", companyId: company.company.id, name: "安全线", code: "secure-line",
|
||||
adminToken: "test-token"
|
||||
});
|
||||
const licenseId = crypto.randomUUID();
|
||||
const payload = {
|
||||
license_id: licenseId, company_id: company.company.id, production_line_id: line.productionLine.id,
|
||||
customer: "安全校验公司", device_id: line.productionLine.deviceId,
|
||||
issued: "2026-07-25 01:00", expiry: "2027-07-25 01:00", features: "*"
|
||||
};
|
||||
const request = {
|
||||
type: "createLicense", licenseId, companyId: company.company.id, productionLineId: line.productionLine.id,
|
||||
customer: payload.customer, issued: payload.issued, expiry: payload.expiry, features: "*",
|
||||
license: signLicense(payload), adminToken: "test-token"
|
||||
};
|
||||
assert.equal((await post(request)).success, true);
|
||||
assert.equal((await post(request)).idempotent, true);
|
||||
|
||||
const changed = await post({ ...request, customer: "已篡改客户" });
|
||||
assert.equal(changed.success, false);
|
||||
assert.match(changed.errMsg, /不一致/);
|
||||
|
||||
const invalidSignature = await post({
|
||||
...request, licenseId: crypto.randomUUID(), license: `${request.license}x`
|
||||
});
|
||||
assert.equal(invalidSignature.success, false);
|
||||
|
||||
const expiredId = crypto.randomUUID();
|
||||
const expiredPayload = {
|
||||
...payload, license_id: expiredId, issued: "2020-01-01 00:00", expiry: "2020-01-02 00:00"
|
||||
};
|
||||
const expired = await post({
|
||||
...request, licenseId: expiredId, issued: expiredPayload.issued, expiry: expiredPayload.expiry,
|
||||
license: signLicense(expiredPayload)
|
||||
});
|
||||
assert.equal(expired.success, true);
|
||||
assert.deepEqual(await post({
|
||||
type: "validateLicense", licenseId: expiredId, deviceId: line.productionLine.deviceId
|
||||
}), { success: true, valid: false, status: "expired", licenseId: expiredId });
|
||||
});
|
||||
Reference in New Issue
Block a user