miravo 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/console-dist/assets/activity-DsWeRq4h.js +1 -0
- package/console-dist/assets/api-4AqKvsm9.js +1 -0
- package/console-dist/assets/arrow-right-Ea_UhTNi.js +1 -0
- package/console-dist/assets/arrow-up-down-DFSjzzl2.js +1 -0
- package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +1 -0
- package/console-dist/assets/box-C1WaoOMF.js +1 -0
- package/console-dist/assets/card-BjeP32PN.js +1 -0
- package/console-dist/assets/catalog-BJzyzbLR.js +1 -0
- package/console-dist/assets/catalog-DKjEm-c4.js +1 -0
- package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +1 -0
- package/console-dist/assets/checkbox-DZlJXTE4.js +1 -0
- package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +3 -0
- package/console-dist/assets/combobox-BGnuCYxO.js +1 -0
- package/console-dist/assets/control-CPhvwx9o.js +1 -0
- package/console-dist/assets/control-Cg2up9NA.js +1 -0
- package/console-dist/assets/copy-button-WkbNkMcd.js +1 -0
- package/console-dist/assets/createLucideIcon-Bwxw859K.js +1 -0
- package/console-dist/assets/deploy-tab-JeWDwPoZ.js +1 -0
- package/console-dist/assets/dist-BhbDML0a.js +1 -0
- package/console-dist/assets/dist-Cy8XTNFT.js +2 -0
- package/console-dist/assets/download-DQ6NH16U.js +1 -0
- package/console-dist/assets/engine-store-CrLnz692.js +1 -0
- package/console-dist/assets/events-CXm29Ok4.js +1 -0
- package/console-dist/assets/events-D01IxhnP.js +1 -0
- package/console-dist/assets/fleet-CfxbKXt9.js +1 -0
- package/console-dist/assets/fleet-lotMGVqe.js +1 -0
- package/console-dist/assets/format-kXTonpDJ.js +1 -0
- package/console-dist/assets/gauge-Ci8zNZlk.js +1 -0
- package/console-dist/assets/get-error-message-3Q5Q-2G5.js +1 -0
- package/console-dist/assets/index-CVPKSDvJ.js +12 -0
- package/console-dist/assets/index-DJkgDeun.css +2 -0
- package/console-dist/assets/input-BWEQFptx.js +1 -0
- package/console-dist/assets/instance-detail-C5IVwuh3.js +1 -0
- package/console-dist/assets/label-uephX5ZG.js +1 -0
- package/console-dist/assets/layers-CUsLQ1fq.js +1 -0
- package/console-dist/assets/login-C3RJli0m.js +126 -0
- package/console-dist/assets/models-B_UioWQ5.js +1 -0
- package/console-dist/assets/models-_IPlPiZ7.js +1 -0
- package/console-dist/assets/not-found-B8gAm08S.js +1 -0
- package/console-dist/assets/platform-targets-R3T-4eJ7.js +1 -0
- package/console-dist/assets/platforms-BWh0BQ3Y.js +1 -0
- package/console-dist/assets/platforms-DStNbtVc.js +1 -0
- package/console-dist/assets/proxy-CsnYqnPp.js +9 -0
- package/console-dist/assets/query-keys-BvtE7BMs.js +1 -0
- package/console-dist/assets/radio-CoHegOv5.js +1 -0
- package/console-dist/assets/rolldown-runtime-COnpUsM8.js +1 -0
- package/console-dist/assets/select-CeO4vP0G.js +1 -0
- package/console-dist/assets/select-display-items-CIzY8953.js +1 -0
- package/console-dist/assets/server-D9Kfn8fQ.js +1 -0
- package/console-dist/assets/shallow-6lGjQhtJ.js +1 -0
- package/console-dist/assets/tags-CiXOIHtY.js +4 -0
- package/console-dist/assets/tags-FQCXIxrP.js +1 -0
- package/console-dist/assets/tooltip-HGLF366Q.js +1 -0
- package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +1 -0
- package/console-dist/assets/transition-types-CPlGtM8w.js +1 -0
- package/console-dist/assets/triangle-alert-adNk_sea.js +1 -0
- package/console-dist/assets/use-platform-stream-DGbK1k2L.js +2 -0
- package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +2 -0
- package/console-dist/assets/validation-Blvud9_O.js +1 -0
- package/console-dist/assets/vendor-charts-CAZdaPlq.js +60 -0
- package/console-dist/assets/vendor-table-wd8JuMmT.js +4 -0
- package/console-dist/assets/vendor-ui-ByahnaY0.js +1 -0
- package/console-dist/index.html +70 -2
- package/package.json +9 -8
- package/src/__tests__/integration/executable-methods-runner.ts +7 -6
- package/src/__tests__/integration/executable-methods.integration.ts +214 -122
- package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +314 -0
- package/src/commands/adapter.ts +180 -7
- package/src/commands/apply.test.ts +62 -0
- package/src/commands/apply.ts +216 -0
- package/src/commands/cleanup.test.ts +34 -0
- package/src/commands/cleanup.ts +201 -0
- package/src/commands/export.test.ts +51 -0
- package/src/commands/export.ts +404 -0
- package/src/commands/fork.ts +2 -2
- package/src/commands/inject.ts +2 -2
- package/src/commands/load.ts +1 -1
- package/src/commands/logs.ts +13 -15
- package/src/commands/models.test.ts +1 -1
- package/src/commands/models.ts +1 -1
- package/src/commands/pause.ts +3 -3
- package/src/commands/platforms.test.ts +186 -0
- package/src/commands/platforms.ts +371 -0
- package/src/commands/remove.ts +1 -1
- package/src/commands/reset.ts +2 -2
- package/src/commands/spawn.ts +2 -2
- package/src/commands/speed.ts +2 -2
- package/src/commands/start/adapter-config.ts +157 -0
- package/src/commands/start/catalog-le-dt.test.ts +50 -0
- package/src/commands/start/catalog-le-dt.ts +187 -0
- package/src/commands/start/catalog-model-schema.ts +89 -0
- package/src/commands/start/catalog-template-detail.ts +54 -0
- package/src/commands/start/daemon.ts +255 -0
- package/src/commands/start/display.ts +150 -0
- package/src/commands/start/engine-setup.ts +247 -0
- package/src/commands/start/events.ts +126 -0
- package/src/commands/start/index.ts +897 -0
- package/src/commands/start/protocol-plans.ts +239 -0
- package/src/commands/start/template-loader.ts +197 -0
- package/src/commands/start/template-resolver.ts +185 -0
- package/src/commands/start/types.ts +73 -0
- package/src/commands/start.test.ts +137 -16
- package/src/commands/status.ts +1 -1
- package/src/commands/stop.ts +19 -8
- package/src/commands/templates.test.ts +1 -1
- package/src/commands/templates.ts +1 -3
- package/src/index.ts +11 -3
- package/src/lib/admin-auth.ts +157 -0
- package/src/lib/admin-client.test.ts +31 -15
- package/src/lib/admin-client.ts +106 -6
- package/src/lib/admin-platform-routes.ts +1017 -0
- package/src/lib/admin-platform-stream.test.ts +434 -0
- package/src/lib/admin-platform-stream.ts +788 -0
- package/src/lib/admin-responses.ts +211 -0
- package/src/lib/admin-server.test.ts +274 -55
- package/src/lib/admin-server.ts +387 -248
- package/src/lib/auth-store.ts +87 -0
- package/src/lib/daemon-registry.ts +12 -14
- package/src/lib/le/le-apply.test.ts +591 -0
- package/src/lib/le/le-apply.ts +832 -0
- package/src/lib/le/le-auth.ts +89 -0
- package/src/lib/le/le-cleanup.test.ts +168 -0
- package/src/lib/le/le-cleanup.ts +399 -0
- package/src/lib/le/le-cli-helpers.ts +77 -0
- package/src/lib/le/le-client.ts +184 -0
- package/src/lib/le/le-concurrency.test.ts +157 -0
- package/src/lib/le/le-concurrency.ts +78 -0
- package/src/lib/le/le-connection-test.ts +42 -0
- package/src/lib/le/le-devicehub-properties.test.ts +53 -0
- package/src/lib/le/le-devicehub-properties.ts +33 -0
- package/src/lib/le/le-devicehub.ts +213 -0
- package/src/lib/le/le-digital-twins.ts +285 -0
- package/src/lib/le/le-drivers.ts +23 -0
- package/src/lib/le/le-endpoint.ts +40 -0
- package/src/lib/le/le-environments.test.ts +73 -0
- package/src/lib/le/le-environments.ts +156 -0
- package/src/lib/le/le-markers.ts +87 -0
- package/src/lib/le/le-plan.test.ts +26 -0
- package/src/lib/le/le-plan.ts +109 -0
- package/src/lib/le/le-platform-operations.test.ts +198 -0
- package/src/lib/le/le-platform-operations.ts +340 -0
- package/src/lib/le/le-static-model-schema.ts +78 -0
- package/src/lib/le/le-static-planner.test.ts +255 -0
- package/src/lib/le/le-static-planner.ts +442 -0
- package/src/lib/le/le-template-exporter.test.ts +527 -0
- package/src/lib/le/le-template-exporter.ts +459 -0
- package/src/lib/le/le-value-types.ts +23 -0
- package/src/lib/mqtt-client.ts +1 -2
- package/src/lib/resolve-simulation.ts +1 -6
- package/src/lib/session-store.ts +110 -0
- package/src/lib/shutdown.test.ts +184 -2
- package/src/lib/shutdown.ts +104 -34
- package/src/lib/template-category.ts +3 -3
- package/src/lib/template-locator.ts +8 -2
- package/src/lib/ws-hub.ts +72 -46
- package/src/logger.ts +39 -14
- package/src/output.ts +3 -0
- package/console-dist/assets/index-C3XMYq26.css +0 -2
- package/console-dist/assets/index-hUJSpKtB.js +0 -119
- package/src/commands/start.ts +0 -2023
|
@@ -2,8 +2,35 @@ import { afterEach, describe, expect, test } from "bun:test";
|
|
|
2
2
|
import type { EventEmitter } from "node:events";
|
|
3
3
|
import { createServer } from "node:net";
|
|
4
4
|
import type { EngineCoreMetrics, EngineStateSnapshot } from "@miravo/core";
|
|
5
|
-
import
|
|
5
|
+
import {
|
|
6
|
+
type CommandResult,
|
|
7
|
+
ConsoleBootstrapResponseSchema,
|
|
8
|
+
type EngineCommand,
|
|
9
|
+
} from "@miravo/shared";
|
|
6
10
|
import { startAdminServer } from "./admin-server.ts";
|
|
11
|
+
import { createSessionStore } from "./session-store.ts";
|
|
12
|
+
|
|
13
|
+
/** Default test auth config: password "admin", not default. */
|
|
14
|
+
function testAuth() {
|
|
15
|
+
return {
|
|
16
|
+
verifyPassword: async (pw: string) => pw === "testpass",
|
|
17
|
+
isDefaultPassword: false,
|
|
18
|
+
sessionStore: createSessionStore(),
|
|
19
|
+
changePassword: async (_old: string, _new: string) => {},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Login to the test server and return the session cookie string for use in headers. */
|
|
24
|
+
async function loginAndGetCookie(baseUrl: string, password = "testpass"): Promise<string> {
|
|
25
|
+
const res = await fetch(`${baseUrl}/auth/login`, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: { "content-type": "application/json" },
|
|
28
|
+
body: JSON.stringify({ username: "admin", password }),
|
|
29
|
+
});
|
|
30
|
+
if (!res.ok) throw new Error(`Login failed: ${res.status}`);
|
|
31
|
+
const setCookie = res.headers.get("set-cookie") ?? "";
|
|
32
|
+
return setCookie.split(";")[0] ?? "";
|
|
33
|
+
}
|
|
7
34
|
|
|
8
35
|
async function canBindLocalTcp(): Promise<boolean> {
|
|
9
36
|
const probe = createServer();
|
|
@@ -140,6 +167,12 @@ function createMockAdminEngine(
|
|
|
140
167
|
getModelSchema: (_name: string) => undefined,
|
|
141
168
|
getEventLog: () => ({ events: [], nextCursor: null, hasMore: false }),
|
|
142
169
|
getRunningTemplates: () => [],
|
|
170
|
+
getManifest: () => ({
|
|
171
|
+
generatedAt: new Date().toISOString(),
|
|
172
|
+
protocols: {},
|
|
173
|
+
tags: [],
|
|
174
|
+
summary: { totalTags: 0, totalInstances: 0, enabledProtocols: [] },
|
|
175
|
+
}),
|
|
143
176
|
execute: async (command: EngineCommand): Promise<CommandResult> => {
|
|
144
177
|
runtime.executedCommands.push(command);
|
|
145
178
|
return {
|
|
@@ -187,6 +220,12 @@ interface StartServerOptions {
|
|
|
187
220
|
readonly connectedAdapters?: string[];
|
|
188
221
|
/** Adapter names that should report as enabled in the runtime state. */
|
|
189
222
|
readonly enabledAdapters?: string[];
|
|
223
|
+
readonly getLeExport?:
|
|
224
|
+
| ((
|
|
225
|
+
id: string,
|
|
226
|
+
opts: { host: string; protocol: import("@miravo/shared").ProtocolName | undefined },
|
|
227
|
+
) => Promise<{ templateJson: string; tagsCSV: string } | null>)
|
|
228
|
+
| undefined;
|
|
190
229
|
}
|
|
191
230
|
|
|
192
231
|
async function startTestServer(options: StartServerOptions, handles: Array<{ stop: () => void }>) {
|
|
@@ -207,6 +246,8 @@ async function startTestServer(options: StartServerOptions, handles: Array<{ sto
|
|
|
207
246
|
protocols: {
|
|
208
247
|
getLastTickProgressAt: () => mock.getLastTickProgressAt(),
|
|
209
248
|
},
|
|
249
|
+
auth: testAuth(),
|
|
250
|
+
...(options.getLeExport ? { getLeExport: options.getLeExport } : {}),
|
|
210
251
|
});
|
|
211
252
|
handles.push(handle);
|
|
212
253
|
const baseUrl = serverBaseUrl(handle.endpoint);
|
|
@@ -224,6 +265,7 @@ async function startShutdownTestServer(handles: Array<{ stop: () => void }>): Pr
|
|
|
224
265
|
engine: mock.engine,
|
|
225
266
|
network: { host: "127.0.0.1", port, tickIntervalMs: 1000 },
|
|
226
267
|
protocols: { getLastTickProgressAt: () => mock.getLastTickProgressAt() },
|
|
268
|
+
auth: testAuth(),
|
|
227
269
|
requestShutdown: (reason) => {
|
|
228
270
|
shutdownReason = reason;
|
|
229
271
|
},
|
|
@@ -242,6 +284,8 @@ describeIfTcp("admin server", () => {
|
|
|
242
284
|
handles.length = 0;
|
|
243
285
|
});
|
|
244
286
|
|
|
287
|
+
// --- Health endpoints (unauthenticated) ---
|
|
288
|
+
|
|
245
289
|
test.each([
|
|
246
290
|
{
|
|
247
291
|
label: "200 when running and mqtt connected",
|
|
@@ -326,11 +370,16 @@ describeIfTcp("admin server", () => {
|
|
|
326
370
|
expect(recovered.status).toBe(200);
|
|
327
371
|
});
|
|
328
372
|
|
|
373
|
+
// --- Protected endpoints (require session cookie) ---
|
|
374
|
+
|
|
329
375
|
test("GET /metrics returns the engine metrics snapshot", async () => {
|
|
330
376
|
const { mock, baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
331
377
|
mock.setTickCount(12);
|
|
378
|
+
const cookie = await loginAndGetCookie(baseUrl);
|
|
332
379
|
|
|
333
|
-
const response = await fetch(`${baseUrl}/metrics
|
|
380
|
+
const response = await fetch(`${baseUrl}/metrics`, {
|
|
381
|
+
headers: { cookie },
|
|
382
|
+
});
|
|
334
383
|
const body = (await response.json()) as {
|
|
335
384
|
core: { tickCount: number; state: EngineCoreMetrics["state"] };
|
|
336
385
|
simulationName: string;
|
|
@@ -344,8 +393,11 @@ describeIfTcp("admin server", () => {
|
|
|
344
393
|
|
|
345
394
|
test("GET /state returns the engine state snapshot", async () => {
|
|
346
395
|
const { baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
396
|
+
const cookie = await loginAndGetCookie(baseUrl);
|
|
347
397
|
|
|
348
|
-
const response = await fetch(`${baseUrl}/state
|
|
398
|
+
const response = await fetch(`${baseUrl}/state`, {
|
|
399
|
+
headers: { cookie },
|
|
400
|
+
});
|
|
349
401
|
const body = (await response.json()) as EngineStateSnapshot;
|
|
350
402
|
|
|
351
403
|
expect(response.status).toBe(200);
|
|
@@ -355,10 +407,11 @@ describeIfTcp("admin server", () => {
|
|
|
355
407
|
|
|
356
408
|
test("POST /commands validates and dispatches engine commands", async () => {
|
|
357
409
|
const { mock, baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
410
|
+
const cookie = await loginAndGetCookie(baseUrl);
|
|
358
411
|
|
|
359
412
|
const response = await fetch(`${baseUrl}/commands`, {
|
|
360
413
|
method: "POST",
|
|
361
|
-
headers: { "content-type": "application/json" },
|
|
414
|
+
headers: { "content-type": "application/json", cookie },
|
|
362
415
|
body: JSON.stringify({
|
|
363
416
|
type: "pause",
|
|
364
417
|
correlationId: "req-1",
|
|
@@ -372,30 +425,24 @@ describeIfTcp("admin server", () => {
|
|
|
372
425
|
expect(mock.getExecutedCommands()).toEqual([{ type: "pause", correlationId: "req-1" }]);
|
|
373
426
|
});
|
|
374
427
|
|
|
375
|
-
test("POST /commands requires auth
|
|
376
|
-
const { baseUrl } = await startTestServer({ state: "running"
|
|
377
|
-
|
|
428
|
+
test("POST /commands requires session auth", async () => {
|
|
429
|
+
const { baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
430
|
+
// Without session cookie
|
|
378
431
|
const response = await fetch(`${baseUrl}/commands`, {
|
|
379
432
|
method: "POST",
|
|
380
433
|
headers: { "content-type": "application/json" },
|
|
381
|
-
body: JSON.stringify({
|
|
382
|
-
type: "pause",
|
|
383
|
-
correlationId: "req-1",
|
|
384
|
-
}),
|
|
434
|
+
body: JSON.stringify({ type: "pause", correlationId: "req-1" }),
|
|
385
435
|
});
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
expect(response.status).toBe(403);
|
|
389
|
-
expect(body.message).toBe("Mutation endpoints require authentication.");
|
|
390
|
-
expect(body.error).toBe("Set MIRAVO_ADMIN_TOKEN when binding to a non-loopback address.");
|
|
436
|
+
expect(response.status).toBe(401);
|
|
391
437
|
});
|
|
392
438
|
|
|
393
439
|
test("POST /commands returns 400 for invalid JSON", async () => {
|
|
394
440
|
const { baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
441
|
+
const cookie = await loginAndGetCookie(baseUrl);
|
|
395
442
|
|
|
396
443
|
const response = await fetch(`${baseUrl}/commands`, {
|
|
397
444
|
method: "POST",
|
|
398
|
-
headers: { "content-type": "application/json" },
|
|
445
|
+
headers: { "content-type": "application/json", cookie },
|
|
399
446
|
body: "{bad json",
|
|
400
447
|
});
|
|
401
448
|
const body = (await response.json()) as { message: string };
|
|
@@ -406,10 +453,11 @@ describeIfTcp("admin server", () => {
|
|
|
406
453
|
|
|
407
454
|
test("POST /commands returns 400 for invalid command shape", async () => {
|
|
408
455
|
const { baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
456
|
+
const cookie = await loginAndGetCookie(baseUrl);
|
|
409
457
|
|
|
410
458
|
const response = await fetch(`${baseUrl}/commands`, {
|
|
411
459
|
method: "POST",
|
|
412
|
-
headers: { "content-type": "application/json" },
|
|
460
|
+
headers: { "content-type": "application/json", cookie },
|
|
413
461
|
body: JSON.stringify({ type: "setSpeed" }),
|
|
414
462
|
});
|
|
415
463
|
const body = (await response.json()) as CommandResult;
|
|
@@ -419,6 +467,60 @@ describeIfTcp("admin server", () => {
|
|
|
419
467
|
expect(body.command).toBe("setSpeed");
|
|
420
468
|
});
|
|
421
469
|
|
|
470
|
+
test.each([
|
|
471
|
+
{ label: "missing host", suffix: "" },
|
|
472
|
+
{ label: "blank host", suffix: "?host=%20%20%20" },
|
|
473
|
+
])("GET /catalog/templates/:id/le-template.json returns 400 for $label", async ({ suffix }) => {
|
|
474
|
+
const { baseUrl } = await startTestServer(
|
|
475
|
+
{
|
|
476
|
+
state: "running",
|
|
477
|
+
getLeExport: async () => ({
|
|
478
|
+
templateJson: JSON.stringify({ ok: true }),
|
|
479
|
+
tagsCSV: "name,value\n",
|
|
480
|
+
}),
|
|
481
|
+
},
|
|
482
|
+
handles,
|
|
483
|
+
);
|
|
484
|
+
const cookie = await loginAndGetCookie(baseUrl);
|
|
485
|
+
|
|
486
|
+
const response = await fetch(
|
|
487
|
+
`${baseUrl}/catalog/templates/smart-factory/le-template.json${suffix}`,
|
|
488
|
+
{
|
|
489
|
+
headers: { cookie },
|
|
490
|
+
},
|
|
491
|
+
);
|
|
492
|
+
const body = (await response.json()) as { message: string; error: string };
|
|
493
|
+
|
|
494
|
+
expect(response.status).toBe(400);
|
|
495
|
+
expect(body.message).toBe("Missing host query parameter.");
|
|
496
|
+
expect(body.error).toContain("?host=<hostname-or-ip>");
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test("GET /catalog/templates/:id/le-template.json accepts a non-empty host", async () => {
|
|
500
|
+
const { baseUrl } = await startTestServer(
|
|
501
|
+
{
|
|
502
|
+
state: "running",
|
|
503
|
+
getLeExport: async (_id, { host }) => ({
|
|
504
|
+
templateJson: JSON.stringify({ host }),
|
|
505
|
+
tagsCSV: "name,value\n",
|
|
506
|
+
}),
|
|
507
|
+
},
|
|
508
|
+
handles,
|
|
509
|
+
);
|
|
510
|
+
const cookie = await loginAndGetCookie(baseUrl);
|
|
511
|
+
|
|
512
|
+
const response = await fetch(
|
|
513
|
+
`${baseUrl}/catalog/templates/smart-factory/le-template.json?host=172.17.0.1`,
|
|
514
|
+
{
|
|
515
|
+
headers: { cookie },
|
|
516
|
+
},
|
|
517
|
+
);
|
|
518
|
+
const body = (await response.json()) as { host: string };
|
|
519
|
+
|
|
520
|
+
expect(response.status).toBe(200);
|
|
521
|
+
expect(body.host).toBe("172.17.0.1");
|
|
522
|
+
});
|
|
523
|
+
|
|
422
524
|
// --- Daemon mode endpoints ---
|
|
423
525
|
|
|
424
526
|
test("/readyz includes simulationName and runId when provided", async () => {
|
|
@@ -428,6 +530,7 @@ describeIfTcp("admin server", () => {
|
|
|
428
530
|
engine: mock.engine,
|
|
429
531
|
network: { host: "127.0.0.1", port, tickIntervalMs: 1000 },
|
|
430
532
|
protocols: { getLastTickProgressAt: () => mock.getLastTickProgressAt() },
|
|
533
|
+
auth: testAuth(),
|
|
431
534
|
simulationName: "test-daemon",
|
|
432
535
|
runId: "run-abc-123",
|
|
433
536
|
});
|
|
@@ -455,10 +558,11 @@ describeIfTcp("admin server", () => {
|
|
|
455
558
|
|
|
456
559
|
test("POST /shutdown calls requestShutdown callback and returns 200", async () => {
|
|
457
560
|
const { baseUrl, getShutdownReason } = await startShutdownTestServer(handles);
|
|
561
|
+
const cookie = await loginAndGetCookie(baseUrl);
|
|
458
562
|
|
|
459
563
|
const response = await fetch(`${baseUrl}/shutdown`, {
|
|
460
564
|
method: "POST",
|
|
461
|
-
headers: { "content-type": "application/json" },
|
|
565
|
+
headers: { "content-type": "application/json", cookie },
|
|
462
566
|
body: JSON.stringify({ reason: "miravo stop" }),
|
|
463
567
|
});
|
|
464
568
|
const body = (await response.json()) as Record<string, unknown>;
|
|
@@ -470,10 +574,11 @@ describeIfTcp("admin server", () => {
|
|
|
470
574
|
|
|
471
575
|
test("POST /shutdown uses default reason when body is empty", async () => {
|
|
472
576
|
const { baseUrl, getShutdownReason } = await startShutdownTestServer(handles);
|
|
577
|
+
const cookie = await loginAndGetCookie(baseUrl);
|
|
473
578
|
|
|
474
579
|
const response = await fetch(`${baseUrl}/shutdown`, {
|
|
475
580
|
method: "POST",
|
|
476
|
-
headers: { "content-type": "application/json" },
|
|
581
|
+
headers: { "content-type": "application/json", cookie },
|
|
477
582
|
});
|
|
478
583
|
expect(response.status).toBe(200);
|
|
479
584
|
expect(getShutdownReason()).toBe("remote shutdown request");
|
|
@@ -486,6 +591,7 @@ describeIfTcp("admin server", () => {
|
|
|
486
591
|
engine: mock.engine,
|
|
487
592
|
network: { host: "127.0.0.1", port, tickIntervalMs: 1000 },
|
|
488
593
|
protocols: { getLastTickProgressAt: () => mock.getLastTickProgressAt() },
|
|
594
|
+
auth: testAuth(),
|
|
489
595
|
requestShutdown: () => {},
|
|
490
596
|
});
|
|
491
597
|
handles.push(handle);
|
|
@@ -497,66 +603,179 @@ describeIfTcp("admin server", () => {
|
|
|
497
603
|
|
|
498
604
|
test("POST /shutdown returns 501 when requestShutdown not provided", async () => {
|
|
499
605
|
const { baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
606
|
+
const cookie = await loginAndGetCookie(baseUrl);
|
|
500
607
|
|
|
501
|
-
const response = await fetch(`${baseUrl}/shutdown`, {
|
|
608
|
+
const response = await fetch(`${baseUrl}/shutdown`, {
|
|
609
|
+
method: "POST",
|
|
610
|
+
headers: { cookie },
|
|
611
|
+
});
|
|
502
612
|
expect(response.status).toBe(501);
|
|
503
613
|
});
|
|
504
614
|
|
|
505
|
-
test("POST /shutdown requires auth
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
const handle = startAdminServer({
|
|
509
|
-
engine: mock.engine,
|
|
510
|
-
network: { host: "127.0.0.1", port, tickIntervalMs: 1000 },
|
|
511
|
-
protocols: { getLastTickProgressAt: () => mock.getLastTickProgressAt() },
|
|
512
|
-
requestShutdown: () => {},
|
|
513
|
-
adminToken: "secret-token",
|
|
514
|
-
});
|
|
515
|
-
handles.push(handle);
|
|
516
|
-
const baseUrl = serverBaseUrl(handle.endpoint);
|
|
517
|
-
|
|
518
|
-
// Without token
|
|
615
|
+
test("POST /shutdown requires session auth", async () => {
|
|
616
|
+
const { baseUrl } = await startShutdownTestServer(handles);
|
|
617
|
+
// Without session cookie
|
|
519
618
|
const noAuth = await fetch(`${baseUrl}/shutdown`, {
|
|
520
619
|
method: "POST",
|
|
521
620
|
headers: { "content-type": "application/json" },
|
|
522
621
|
});
|
|
523
622
|
expect(noAuth.status).toBe(401);
|
|
524
|
-
|
|
525
|
-
|
|
623
|
+
// With session cookie
|
|
624
|
+
const cookie = await loginAndGetCookie(baseUrl);
|
|
526
625
|
const withAuth = await fetch(`${baseUrl}/shutdown`, {
|
|
527
626
|
method: "POST",
|
|
528
|
-
headers: { "content-type": "application/json",
|
|
627
|
+
headers: { "content-type": "application/json", cookie },
|
|
529
628
|
});
|
|
530
629
|
expect(withAuth.status).toBe(200);
|
|
531
630
|
});
|
|
532
631
|
|
|
533
|
-
test("POST /commands accepts
|
|
632
|
+
test("POST /commands accepts session auth", async () => {
|
|
633
|
+
const { mock, baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
634
|
+
const cookie = await loginAndGetCookie(baseUrl);
|
|
635
|
+
const response = await fetch(`${baseUrl}/commands`, {
|
|
636
|
+
method: "POST",
|
|
637
|
+
headers: { "content-type": "application/json", cookie },
|
|
638
|
+
body: JSON.stringify({ type: "pause", correlationId: "req-2" }),
|
|
639
|
+
});
|
|
640
|
+
const body = (await response.json()) as CommandResult;
|
|
641
|
+
expect(response.status).toBe(200);
|
|
642
|
+
expect(body.success).toBe(true);
|
|
643
|
+
expect(mock.getExecutedCommands()).toEqual([{ type: "pause", correlationId: "req-2" }]);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
// --- Auth endpoint tests ---
|
|
647
|
+
|
|
648
|
+
test("POST /auth/login returns 200 with valid credentials", async () => {
|
|
649
|
+
const { baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
650
|
+
const res = await fetch(`${baseUrl}/auth/login`, {
|
|
651
|
+
method: "POST",
|
|
652
|
+
headers: { "content-type": "application/json" },
|
|
653
|
+
body: JSON.stringify({ username: "admin", password: "testpass" }),
|
|
654
|
+
});
|
|
655
|
+
expect(res.status).toBe(200);
|
|
656
|
+
const body = (await res.json()) as { ok: boolean; changePassword: boolean };
|
|
657
|
+
expect(body.ok).toBe(true);
|
|
658
|
+
expect(body.changePassword).toBe(false);
|
|
659
|
+
expect(res.headers.get("set-cookie")).toContain("miravo_session=");
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test("POST /auth/login returns 401 with invalid credentials", async () => {
|
|
663
|
+
const { baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
664
|
+
const res = await fetch(`${baseUrl}/auth/login`, {
|
|
665
|
+
method: "POST",
|
|
666
|
+
headers: { "content-type": "application/json" },
|
|
667
|
+
body: JSON.stringify({ username: "admin", password: "wrong" }),
|
|
668
|
+
});
|
|
669
|
+
expect(res.status).toBe(401);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test("GET /auth/status returns authenticated with valid session", async () => {
|
|
673
|
+
const { baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
674
|
+
const cookie = await loginAndGetCookie(baseUrl);
|
|
675
|
+
const res = await fetch(`${baseUrl}/auth/status`, { headers: { cookie } });
|
|
676
|
+
const body = (await res.json()) as { authenticated: boolean; username?: string };
|
|
677
|
+
expect(body.authenticated).toBe(true);
|
|
678
|
+
expect(body.username).toBe("admin");
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
test("GET /auth/status returns unauthenticated without session", async () => {
|
|
682
|
+
const { baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
683
|
+
const res = await fetch(`${baseUrl}/auth/status`);
|
|
684
|
+
const body = (await res.json()) as { authenticated: boolean };
|
|
685
|
+
expect(body.authenticated).toBe(false);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test("GET /bootstrap returns unauthenticated without session", async () => {
|
|
689
|
+
const { baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
690
|
+
const res = await fetch(`${baseUrl}/bootstrap`);
|
|
691
|
+
expect(res.status).toBe(200);
|
|
692
|
+
const parsed = ConsoleBootstrapResponseSchema.safeParse(await res.json());
|
|
693
|
+
expect(parsed.success).toBe(true);
|
|
694
|
+
expect(parsed.data).toEqual({ authenticated: false });
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
test("GET /bootstrap returns auth plus canonical runtime snapshot with valid session", async () => {
|
|
698
|
+
const { baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
699
|
+
const cookie = await loginAndGetCookie(baseUrl);
|
|
700
|
+
const res = await fetch(`${baseUrl}/bootstrap`, { headers: { cookie } });
|
|
701
|
+
expect(res.status).toBe(200);
|
|
702
|
+
const parsed = ConsoleBootstrapResponseSchema.safeParse(await res.json());
|
|
703
|
+
expect(parsed.success).toBe(true);
|
|
704
|
+
if (!parsed.success || !parsed.data.authenticated) {
|
|
705
|
+
throw new Error("Expected authenticated bootstrap payload.");
|
|
706
|
+
}
|
|
707
|
+
expect(parsed.data.username).toBe("admin");
|
|
708
|
+
expect(parsed.data.runtime.state.instances.length).toBeGreaterThanOrEqual(0);
|
|
709
|
+
expect(parsed.data.runtime.metrics.core.state).toBe("running");
|
|
710
|
+
expect(Array.isArray(parsed.data.runtime.runningTemplates)).toBe(true);
|
|
711
|
+
expect(typeof parsed.data.runtime.revision).toBe("number");
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
test("GET /state requires session auth", async () => {
|
|
715
|
+
const { baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
716
|
+
const res = await fetch(`${baseUrl}/state`);
|
|
717
|
+
expect(res.status).toBe(401);
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
describeIfTcp("anonymous mode (--no-auth)", () => {
|
|
722
|
+
const handles: Array<{ stop: () => void }> = [];
|
|
723
|
+
afterEach(() => {
|
|
724
|
+
for (const h of handles) h.stop();
|
|
725
|
+
handles.length = 0;
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
async function startAnonServer() {
|
|
534
729
|
const port = await reservePort();
|
|
535
730
|
const mock = createMockAdminEngine("running", 1000);
|
|
536
731
|
const handle = startAdminServer({
|
|
537
732
|
engine: mock.engine,
|
|
538
|
-
network: { host: "
|
|
733
|
+
network: { host: "127.0.0.1", port, tickIntervalMs: 1000 },
|
|
539
734
|
protocols: { getLastTickProgressAt: () => mock.getLastTickProgressAt() },
|
|
540
|
-
|
|
735
|
+
auth: { ...testAuth(), anonymousMode: true },
|
|
541
736
|
});
|
|
542
737
|
handles.push(handle);
|
|
543
|
-
|
|
738
|
+
return { baseUrl: serverBaseUrl(handle.endpoint) };
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
test("GET /bootstrap returns authenticated:true without a session cookie", async () => {
|
|
742
|
+
const { baseUrl } = await startAnonServer();
|
|
743
|
+
const res = await fetch(`${baseUrl}/bootstrap`);
|
|
744
|
+
expect(res.status).toBe(200);
|
|
745
|
+
const parsed = ConsoleBootstrapResponseSchema.safeParse(await res.json());
|
|
746
|
+
expect(parsed.success).toBe(true);
|
|
747
|
+
if (!parsed.success || !parsed.data.authenticated) {
|
|
748
|
+
throw new Error("Expected authenticated bootstrap payload.");
|
|
749
|
+
}
|
|
750
|
+
expect(parsed.data.username).toBe("anonymous");
|
|
751
|
+
expect(parsed.data.runtime.metrics.core.state).toBe("running");
|
|
752
|
+
});
|
|
544
753
|
|
|
545
|
-
|
|
754
|
+
test("GET /state succeeds without a session cookie", async () => {
|
|
755
|
+
const { baseUrl } = await startAnonServer();
|
|
756
|
+
const res = await fetch(`${baseUrl}/state`);
|
|
757
|
+
expect(res.status).toBe(200);
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
test("POST /auth/login succeeds with any credentials", async () => {
|
|
761
|
+
const { baseUrl } = await startAnonServer();
|
|
762
|
+
const res = await fetch(`${baseUrl}/auth/login`, {
|
|
546
763
|
method: "POST",
|
|
547
|
-
headers: {
|
|
548
|
-
|
|
549
|
-
authorization: "Bearer secret-token",
|
|
550
|
-
},
|
|
551
|
-
body: JSON.stringify({
|
|
552
|
-
type: "pause",
|
|
553
|
-
correlationId: "req-2",
|
|
554
|
-
}),
|
|
764
|
+
headers: { "content-type": "application/json" },
|
|
765
|
+
body: JSON.stringify({ username: "whoever", password: "anything" }),
|
|
555
766
|
});
|
|
556
|
-
|
|
767
|
+
expect(res.status).toBe(200);
|
|
768
|
+
const body = (await res.json()) as { ok: boolean; changePassword: boolean };
|
|
769
|
+
expect(body.ok).toBe(true);
|
|
770
|
+
expect(body.changePassword).toBe(false);
|
|
771
|
+
});
|
|
557
772
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
773
|
+
test("GET /auth/status returns authenticated:true without a session cookie", async () => {
|
|
774
|
+
const { baseUrl } = await startAnonServer();
|
|
775
|
+
const res = await fetch(`${baseUrl}/auth/status`);
|
|
776
|
+
expect(res.status).toBe(200);
|
|
777
|
+
const body = (await res.json()) as { authenticated: boolean; username?: string };
|
|
778
|
+
expect(body.authenticated).toBe(true);
|
|
779
|
+
expect(body.username).toBe("anonymous");
|
|
561
780
|
});
|
|
562
781
|
});
|