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, { 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", 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 /api endpoint is unavailable", async () => { const response = await fetch(`${baseUrl}/api`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ type: "listOrganizations", adminToken: "test-token" }) }); assert.equal(response.status, 404); }); 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"); assert.equal((await fetch(download.url)).status, 403); }); 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, /\/downloads\//); assert.equal(await (await fetch(download.url)).text(), '{"parts":1}'); assert.equal((await fetch(download.url)).status, 403); 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" ); const deleted = await post({ type: "deleteModel", fileID: replacement.fileID, adminToken: "test-token" }); assert.equal(deleted.success, true); assert.equal(deleted.deletedCount, 1); const listed = await post({ type: "listModels", folder: "company-a/overwrite-line/model_config" }); assert.deepEqual(listed.fileList, []); await assert.rejects(fs.promises.access( path.join(dataDirectory, "models", "company-a", "overwrite-line", "controller.bin") ), { code: "ENOENT" }); }); 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("panel notifications and volume configuration stay isolated by device", async () => { const deviceId = "notify-co/line-1"; const otherDeviceId = "notify-co/line-2"; const request = await post({ type: "createVolumeConfigRequest", deviceId }); const notification = await post({ type: "getPendingPanelNotification", deviceId, adminToken: "test-token" }); assert.equal(notification.pending, true); assert.equal(notification.notification.type, "volume_request_started"); assert.equal(notification.notification.requestId, request.requestId); assert.equal((await post({ type: "getPendingPanelNotification", deviceId: otherDeviceId, adminToken: "test-token" })).pending, false); const folder = `${deviceId}/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); assert.equal((await post({ type: "submitVolumeConfigFile", deviceId, requestId: request.requestId, fileID: issued.fileID, fileName: "volume_measurement.json" })).success, true); const config = await post({ type: "getVolumeConfigFile", deviceId, adminToken: "test-token" }); assert.equal(config.found, true); assert.equal(config.fileID, issued.fileID); assert.equal((await post({ type: "getVolumeConfigFile", deviceId: otherDeviceId, adminToken: "test-token" })).found, false); assert.equal((await post({ type: "ackPanelNotification", deviceId, notificationId: notification.notification.notificationId, adminToken: "test-token" })).success, true); async function uploadResult(folderName, fileName, content) { const result = await post({ type: "uploadDataFile", fileName, folder: `${deviceId}/${folderName}` }); const uploadForm = new FormData(); uploadForm.append("file", new Blob([content]), fileName); assert.equal((await fetch(result.uploadMetadata.url, { method: "POST", body: uploadForm })).status, 204); } await uploadResult("V_config", "volume_result.json", '{"volume_L":1.2}'); const volumeResult = await post({ type: "getPendingPanelNotification", deviceId, adminToken: "test-token" }); assert.equal(volumeResult.notification.type, "volume_result_ready"); assert.ok(volumeResult.notification.fileID); await post({ type: "ackPanelNotification", deviceId, notificationId: volumeResult.notification.notificationId, adminToken: "test-token" }); await uploadResult("ind_data", "identification_result.json", '{"stable_pressures":[]}'); const identificationResult = await post({ type: "getPendingPanelNotification", deviceId, adminToken: "test-token" }); assert.equal(identificationResult.notification.type, "identification_result_ready"); }); 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("revokeLicense returns actionable errors and accepts legacy request fields", async () => { const invalidToken = await post({ type: "revokeLicense", licenseId: crypto.randomUUID(), adminToken: "wrong-token" }); assert.deepEqual(invalidToken, { success: false, errMsg: "B端管理令牌无效", errCode: "ADMIN_TOKEN_INVALID" }); const missingLicenseId = await post({ type: "revokeLicense", licenseId: " ", adminToken: "test-token" }); assert.deepEqual(missingLicenseId, { success: false, errMsg: "licenseId 不能为空", errCode: "LICENSE_ID_REQUIRED" }); const notFound = await post({ type: "revokeLicense", licenseId: crypto.randomUUID(), adminToken: "test-token" }); assert.deepEqual(notFound, { success: false, errMsg: "许可证不存在", errCode: "LICENSE_NOT_FOUND" }); const legacy = await post({ type: "revoke_license", license_id: " ", admin_token: "test-token" }); assert.deepEqual(legacy, { success: false, errMsg: "licenseId 不能为空", errCode: "LICENSE_ID_REQUIRED" }); }); 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 }); }); test("deleteLicense requires prior revocation", async () => { const suffix = crypto.randomUUID().slice(0, 8); const company = await post({ type: "createCompany", name: `删除许可证公司-${suffix}`, code: `del-lic-${suffix}`, adminToken: "test-token" }); const line = await post({ type: "createProductionLine", companyId: company.company.id, name: "许可证线", code: "line-1", adminToken: "test-token" }); const licenseId = crypto.randomUUID(); const payload = { license_id: licenseId, company_id: company.company.id, production_line_id: line.productionLine.id, customer: company.company.name, device_id: line.productionLine.deviceId, issued: "2026-08-01 10:00", expiry: "2028-08-01 10:00", features: "*" }; const created = await post({ type: "createLicense", licenseId, companyId: company.company.id, productionLineId: line.productionLine.id, customer: payload.customer, issued: payload.issued, expiry: payload.expiry, features: payload.features, license: signLicense(payload), adminToken: "test-token" }); assert.equal(created.success, true); const activeDelete = await post({ type: "deleteLicense", licenseId, adminToken: "test-token" }); assert.equal(activeDelete.success, false); assert.equal(activeDelete.errCode, "LICENSE_ACTIVE"); const revoked = await post({ type: "revokeLicense", licenseId, reason: "测试删除", adminToken: "test-token" }); assert.equal(revoked.success, true); const deleted = await post({ type: "deleteLicense", licenseId, adminToken: "test-token" }); assert.deepEqual(deleted, { success: true, deletedLicenseId: licenseId }); const missing = await post({ type: "getLicense", licenseId, adminToken: "test-token" }); assert.equal(missing.success, false); }); test("deleteProductionLine blocks active licenses and removes revoked data", async () => { const suffix = crypto.randomUUID().slice(0, 8); const company = await post({ type: "createCompany", name: `删除产线公司-${suffix}`, code: `del-line-${suffix}`, adminToken: "test-token" }); const line = await post({ type: "createProductionLine", companyId: company.company.id, name: "待删产线", code: "line-1", adminToken: "test-token" }); const modelIssued = await post({ type: "issueModelUpload", deviceId: line.productionLine.deviceId, fileName: "controller.bin", adminToken: "test-token" }); const modelForm = new FormData(); modelForm.append("file", new Blob(["model-bytes"]), "controller.bin"); assert.equal((await fetch(modelIssued.uploadMetadata.url, { method: "POST", body: modelForm })).status, 204); const licenseId = crypto.randomUUID(); const payload = { license_id: licenseId, company_id: company.company.id, production_line_id: line.productionLine.id, customer: company.company.name, device_id: line.productionLine.deviceId, issued: "2026-08-01 10:00", expiry: "2028-08-01 10:00", features: "*" }; await post({ type: "createLicense", licenseId, companyId: company.company.id, productionLineId: line.productionLine.id, customer: payload.customer, issued: payload.issued, expiry: payload.expiry, features: payload.features, license: signLicense(payload), adminToken: "test-token" }); const blocked = await post({ type: "deleteProductionLine", companyId: company.company.id, productionLineId: line.productionLine.id, adminToken: "test-token" }); assert.equal(blocked.success, false); assert.equal(blocked.errCode, "ACTIVE_LICENSES_PRESENT"); await post({ type: "revokeLicense", licenseId, reason: "产线删除", adminToken: "test-token" }); const orphanModelDir = path.join(dataDirectory, "models", line.productionLine.deviceId); const orphanFileDir = path.join(dataDirectory, "files", "ReinLoop_GUI", line.productionLine.deviceId); await fs.promises.mkdir(orphanModelDir, { recursive: true }); await fs.promises.mkdir(orphanFileDir, { recursive: true }); await fs.promises.writeFile(path.join(orphanModelDir, "orphan.bin"), "orphan-model"); await fs.promises.writeFile(path.join(orphanFileDir, "orphan.json"), "orphan-file"); const deleted = await post({ type: "deleteProductionLine", companyId: company.company.id, productionLineId: line.productionLine.id, adminToken: "test-token" }); assert.equal(deleted.success, true); assert.ok(deleted.deletedFiles >= 1); assert.ok(deleted.deletedLicenses >= 1); assert.ok(deleted.deletedDirectories >= 1); await assert.rejects(fs.promises.access(orphanModelDir)); await assert.rejects(fs.promises.access(orphanFileDir)); const organizations = await post({ type: "listOrganizations", adminToken: "test-token" }); const listedCompany = organizations.companies.find((item) => item.id === company.company.id); assert.ok(listedCompany); assert.equal(listedCompany.productionLines.some((item) => item.id === line.productionLine.id), false); const models = await post({ type: "listModels", folder: `${line.productionLine.deviceId}/model_config` }); assert.deepEqual(models.fileList, []); }); test("deleteCompany requires no child lines and no remaining licenses", async () => { const suffix = crypto.randomUUID().slice(0, 8); const company = await post({ type: "createCompany", name: `删除公司-${suffix}`, code: `del-co-${suffix}`, adminToken: "test-token" }); const line = await post({ type: "createProductionLine", companyId: company.company.id, name: "子产线", code: "line-1", adminToken: "test-token" }); const blocked = await post({ type: "deleteCompany", companyId: company.company.id, adminToken: "test-token" }); assert.equal(blocked.success, false); assert.equal(blocked.errCode, "PRODUCTION_LINES_PRESENT"); const lineDeleted = await post({ type: "deleteProductionLine", companyId: company.company.id, productionLineId: line.productionLine.id, adminToken: "test-token" }); assert.equal(lineDeleted.success, true); const deleted = await post({ type: "deleteCompany", companyId: company.company.id, adminToken: "test-token" }); assert.deepEqual(deleted, { success: true, deletedCompanyId: company.company.id }); const organizations = await post({ type: "listOrganizations", adminToken: "test-token" }); assert.equal(organizations.companies.some((item) => item.id === company.company.id), false); });