miravo 0.6.0 → 0.6.2
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/LICENSE +199 -0
- package/README.md +29 -59
- package/bin/miravo.js +84 -0
- package/package.json +21 -37
- package/console-dist/assets/activity-DsWeRq4h.js +0 -1
- package/console-dist/assets/api-4AqKvsm9.js +0 -1
- package/console-dist/assets/arrow-right-Ea_UhTNi.js +0 -1
- package/console-dist/assets/arrow-up-down-DFSjzzl2.js +0 -1
- package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +0 -1
- package/console-dist/assets/box-C1WaoOMF.js +0 -1
- package/console-dist/assets/card-BjeP32PN.js +0 -1
- package/console-dist/assets/catalog-BJzyzbLR.js +0 -1
- package/console-dist/assets/catalog-DKjEm-c4.js +0 -1
- package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +0 -1
- package/console-dist/assets/checkbox-DZlJXTE4.js +0 -1
- package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +0 -3
- package/console-dist/assets/combobox-BGnuCYxO.js +0 -1
- package/console-dist/assets/control-CPhvwx9o.js +0 -1
- package/console-dist/assets/control-Cg2up9NA.js +0 -1
- package/console-dist/assets/copy-button-WkbNkMcd.js +0 -1
- package/console-dist/assets/createLucideIcon-Bwxw859K.js +0 -1
- package/console-dist/assets/deploy-tab-JeWDwPoZ.js +0 -1
- package/console-dist/assets/dist-BhbDML0a.js +0 -1
- package/console-dist/assets/dist-Cy8XTNFT.js +0 -2
- package/console-dist/assets/download-DQ6NH16U.js +0 -1
- package/console-dist/assets/engine-store-CrLnz692.js +0 -1
- package/console-dist/assets/events-CXm29Ok4.js +0 -1
- package/console-dist/assets/events-D01IxhnP.js +0 -1
- package/console-dist/assets/fira-code-cyrillic-ext-wght-normal-DhYMMuQd.woff2 +0 -0
- package/console-dist/assets/fira-code-cyrillic-wght-normal-Y3u8pIsh.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-ext-wght-normal-wWus70Ix.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-wght-normal-B2SviObF.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-ext-wght-normal-Dvfvaomy.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
- package/console-dist/assets/fira-code-symbols2-wght-normal-CE6EOz_n.woff2 +0 -0
- package/console-dist/assets/fleet-CfxbKXt9.js +0 -1
- package/console-dist/assets/fleet-lotMGVqe.js +0 -1
- package/console-dist/assets/format-kXTonpDJ.js +0 -1
- package/console-dist/assets/gauge-Ci8zNZlk.js +0 -1
- package/console-dist/assets/get-error-message-3Q5Q-2G5.js +0 -1
- package/console-dist/assets/index-CVPKSDvJ.js +0 -12
- package/console-dist/assets/index-DJkgDeun.css +0 -2
- package/console-dist/assets/input-BWEQFptx.js +0 -1
- package/console-dist/assets/instance-detail-C5IVwuh3.js +0 -1
- package/console-dist/assets/karla-latin-ext-wght-normal-Dq0rrjgs.woff2 +0 -0
- package/console-dist/assets/karla-latin-wght-normal-C3-ma4ov.woff2 +0 -0
- package/console-dist/assets/label-uephX5ZG.js +0 -1
- package/console-dist/assets/layers-CUsLQ1fq.js +0 -1
- package/console-dist/assets/login-C3RJli0m.js +0 -126
- package/console-dist/assets/models-B_UioWQ5.js +0 -1
- package/console-dist/assets/models-_IPlPiZ7.js +0 -1
- package/console-dist/assets/not-found-B8gAm08S.js +0 -1
- package/console-dist/assets/platform-targets-R3T-4eJ7.js +0 -1
- package/console-dist/assets/platforms-BWh0BQ3Y.js +0 -1
- package/console-dist/assets/platforms-DStNbtVc.js +0 -1
- package/console-dist/assets/proxy-CsnYqnPp.js +0 -9
- package/console-dist/assets/query-keys-BvtE7BMs.js +0 -1
- package/console-dist/assets/radio-CoHegOv5.js +0 -1
- package/console-dist/assets/rolldown-runtime-COnpUsM8.js +0 -1
- package/console-dist/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
- package/console-dist/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
- package/console-dist/assets/select-CeO4vP0G.js +0 -1
- package/console-dist/assets/select-display-items-CIzY8953.js +0 -1
- package/console-dist/assets/server-D9Kfn8fQ.js +0 -1
- package/console-dist/assets/shallow-6lGjQhtJ.js +0 -1
- package/console-dist/assets/tags-CiXOIHtY.js +0 -4
- package/console-dist/assets/tags-FQCXIxrP.js +0 -1
- package/console-dist/assets/tooltip-HGLF366Q.js +0 -1
- package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +0 -1
- package/console-dist/assets/transition-types-CPlGtM8w.js +0 -1
- package/console-dist/assets/triangle-alert-adNk_sea.js +0 -1
- package/console-dist/assets/use-platform-stream-DGbK1k2L.js +0 -2
- package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +0 -2
- package/console-dist/assets/validation-Blvud9_O.js +0 -1
- package/console-dist/assets/vendor-charts-CAZdaPlq.js +0 -60
- package/console-dist/assets/vendor-table-wd8JuMmT.js +0 -4
- package/console-dist/assets/vendor-ui-ByahnaY0.js +0 -1
- package/console-dist/favicon.svg +0 -37
- package/console-dist/index.html +0 -82
- package/console-dist/miravo-logo.png +0 -0
- package/src/__tests__/integration/executable-methods-runner.ts +0 -262
- package/src/__tests__/integration/executable-methods.integration.ts +0 -2348
- package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +0 -314
- package/src/commands/adapter.ts +0 -249
- package/src/commands/apply.test.ts +0 -62
- package/src/commands/apply.ts +0 -216
- package/src/commands/cleanup.test.ts +0 -34
- package/src/commands/cleanup.ts +0 -201
- package/src/commands/cli-test-setup.ts +0 -91
- package/src/commands/export.test.ts +0 -51
- package/src/commands/export.ts +0 -404
- package/src/commands/fork.ts +0 -219
- package/src/commands/inject.test.ts +0 -57
- package/src/commands/inject.ts +0 -212
- package/src/commands/load.ts +0 -167
- package/src/commands/logs.ts +0 -173
- package/src/commands/models.test.ts +0 -162
- package/src/commands/models.ts +0 -338
- package/src/commands/pause.ts +0 -67
- package/src/commands/platforms.test.ts +0 -186
- package/src/commands/platforms.ts +0 -371
- package/src/commands/ps.ts +0 -116
- package/src/commands/remove.ts +0 -38
- package/src/commands/reset.ts +0 -44
- package/src/commands/rm.ts +0 -97
- package/src/commands/spawn.ts +0 -45
- package/src/commands/speed.ts +0 -40
- package/src/commands/start/adapter-config.ts +0 -157
- package/src/commands/start/catalog-le-dt.test.ts +0 -50
- package/src/commands/start/catalog-le-dt.ts +0 -187
- package/src/commands/start/catalog-model-schema.ts +0 -89
- package/src/commands/start/catalog-template-detail.ts +0 -54
- package/src/commands/start/daemon.ts +0 -255
- package/src/commands/start/display.ts +0 -150
- package/src/commands/start/engine-setup.ts +0 -247
- package/src/commands/start/events.ts +0 -126
- package/src/commands/start/index.ts +0 -897
- package/src/commands/start/protocol-plans.ts +0 -239
- package/src/commands/start/template-loader.ts +0 -197
- package/src/commands/start/template-resolver.ts +0 -185
- package/src/commands/start/types.ts +0 -73
- package/src/commands/start.test.ts +0 -480
- package/src/commands/status.test.ts +0 -20
- package/src/commands/status.ts +0 -470
- package/src/commands/stop.test.ts +0 -84
- package/src/commands/stop.ts +0 -169
- package/src/commands/templates.test.ts +0 -58
- package/src/commands/templates.ts +0 -277
- package/src/index.ts +0 -166
- package/src/lib/admin-auth.ts +0 -157
- package/src/lib/admin-client.test.ts +0 -51
- package/src/lib/admin-client.ts +0 -326
- package/src/lib/admin-platform-routes.ts +0 -1017
- package/src/lib/admin-platform-stream.test.ts +0 -434
- package/src/lib/admin-platform-stream.ts +0 -788
- package/src/lib/admin-responses.ts +0 -211
- package/src/lib/admin-server.test.ts +0 -781
- package/src/lib/admin-server.ts +0 -797
- package/src/lib/auth-store.ts +0 -87
- package/src/lib/daemon-registry.test.ts +0 -429
- package/src/lib/daemon-registry.ts +0 -343
- package/src/lib/le/le-apply.test.ts +0 -591
- package/src/lib/le/le-apply.ts +0 -832
- package/src/lib/le/le-auth.ts +0 -89
- package/src/lib/le/le-cleanup.test.ts +0 -168
- package/src/lib/le/le-cleanup.ts +0 -399
- package/src/lib/le/le-cli-helpers.ts +0 -77
- package/src/lib/le/le-client.ts +0 -184
- package/src/lib/le/le-concurrency.test.ts +0 -157
- package/src/lib/le/le-concurrency.ts +0 -78
- package/src/lib/le/le-connection-test.ts +0 -42
- package/src/lib/le/le-devicehub-properties.test.ts +0 -53
- package/src/lib/le/le-devicehub-properties.ts +0 -33
- package/src/lib/le/le-devicehub.ts +0 -213
- package/src/lib/le/le-digital-twins.ts +0 -285
- package/src/lib/le/le-drivers.ts +0 -23
- package/src/lib/le/le-endpoint.ts +0 -40
- package/src/lib/le/le-environments.test.ts +0 -73
- package/src/lib/le/le-environments.ts +0 -156
- package/src/lib/le/le-markers.ts +0 -87
- package/src/lib/le/le-plan.test.ts +0 -26
- package/src/lib/le/le-plan.ts +0 -109
- package/src/lib/le/le-platform-operations.test.ts +0 -198
- package/src/lib/le/le-platform-operations.ts +0 -340
- package/src/lib/le/le-static-model-schema.ts +0 -78
- package/src/lib/le/le-static-planner.test.ts +0 -255
- package/src/lib/le/le-static-planner.ts +0 -442
- package/src/lib/le/le-template-exporter.test.ts +0 -527
- package/src/lib/le/le-template-exporter.ts +0 -459
- package/src/lib/le/le-value-types.ts +0 -23
- package/src/lib/mqtt-client.ts +0 -115
- package/src/lib/parse.test.ts +0 -123
- package/src/lib/parse.ts +0 -87
- package/src/lib/resolve-simulation.test.ts +0 -118
- package/src/lib/resolve-simulation.ts +0 -181
- package/src/lib/session-store.ts +0 -110
- package/src/lib/shutdown.test.ts +0 -562
- package/src/lib/shutdown.ts +0 -621
- package/src/lib/template-category.test.ts +0 -71
- package/src/lib/template-category.ts +0 -72
- package/src/lib/template-locator.ts +0 -55
- package/src/lib/template-utils.ts +0 -45
- package/src/lib/ws-hub.ts +0 -281
- package/src/logger.test.ts +0 -172
- package/src/logger.ts +0 -402
- package/src/output.test.ts +0 -64
- package/src/output.ts +0 -160
|
@@ -1,781 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import type { EventEmitter } from "node:events";
|
|
3
|
-
import { createServer } from "node:net";
|
|
4
|
-
import type { EngineCoreMetrics, EngineStateSnapshot } from "@miravo/core";
|
|
5
|
-
import {
|
|
6
|
-
type CommandResult,
|
|
7
|
-
ConsoleBootstrapResponseSchema,
|
|
8
|
-
type EngineCommand,
|
|
9
|
-
} from "@miravo/shared";
|
|
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
|
-
}
|
|
34
|
-
|
|
35
|
-
async function canBindLocalTcp(): Promise<boolean> {
|
|
36
|
-
const probe = createServer();
|
|
37
|
-
return new Promise((resolve) => {
|
|
38
|
-
const cleanup = () => {
|
|
39
|
-
try {
|
|
40
|
-
probe.close();
|
|
41
|
-
} catch {
|
|
42
|
-
// Ignore close races in test cleanup.
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
(probe as unknown as EventEmitter).once("error", () => {
|
|
47
|
-
cleanup();
|
|
48
|
-
resolve(false);
|
|
49
|
-
});
|
|
50
|
-
(probe as unknown as EventEmitter).once("listening", () => {
|
|
51
|
-
cleanup();
|
|
52
|
-
resolve(true);
|
|
53
|
-
});
|
|
54
|
-
probe.listen(0, "127.0.0.1");
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function reservePort(): Promise<number> {
|
|
59
|
-
const probe = createServer();
|
|
60
|
-
return new Promise((resolve, reject) => {
|
|
61
|
-
const cleanup = () => {
|
|
62
|
-
try {
|
|
63
|
-
probe.close();
|
|
64
|
-
} catch {
|
|
65
|
-
// Ignore close races in test cleanup.
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
(probe as unknown as EventEmitter).once("error", (error: Error) => {
|
|
70
|
-
cleanup();
|
|
71
|
-
reject(error);
|
|
72
|
-
});
|
|
73
|
-
(probe as unknown as EventEmitter).once("listening", () => {
|
|
74
|
-
const address = probe.address();
|
|
75
|
-
if (!address || typeof address === "string") {
|
|
76
|
-
cleanup();
|
|
77
|
-
reject(new Error("Failed to reserve a local TCP port for the admin server test."));
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const { port } = address;
|
|
82
|
-
probe.close((error) => {
|
|
83
|
-
if (error) {
|
|
84
|
-
reject(error);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
resolve(port);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
probe.listen(0, "127.0.0.1");
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const TCP_AVAILABLE = await canBindLocalTcp();
|
|
95
|
-
const describeIfTcp = TCP_AVAILABLE ? describe : describe.skip;
|
|
96
|
-
|
|
97
|
-
function createMockAdminEngine(
|
|
98
|
-
initialState: EngineCoreMetrics["state"],
|
|
99
|
-
tickIntervalMs: number,
|
|
100
|
-
connectedAdapters: string[] = [],
|
|
101
|
-
enabledAdapters: string[] = [],
|
|
102
|
-
) {
|
|
103
|
-
const runtime = {
|
|
104
|
-
state: initialState as EngineCoreMetrics["state"],
|
|
105
|
-
tickCount: 0,
|
|
106
|
-
lastTickProgressAt: performance.now(),
|
|
107
|
-
executedCommands: [] as EngineCommand[],
|
|
108
|
-
connectedAdapters: [...connectedAdapters],
|
|
109
|
-
enabledAdapters: [...enabledAdapters],
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const snapshot: EngineStateSnapshot = {
|
|
113
|
-
simulationName: "test-sim",
|
|
114
|
-
models: [],
|
|
115
|
-
instances: [],
|
|
116
|
-
config: {
|
|
117
|
-
seed: 1,
|
|
118
|
-
tickIntervalMs,
|
|
119
|
-
timeMultiplier: 1,
|
|
120
|
-
adapters: [],
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
engine: {
|
|
126
|
-
getMetrics: () => ({
|
|
127
|
-
simulationName: snapshot.simulationName,
|
|
128
|
-
core: {
|
|
129
|
-
state: runtime.state,
|
|
130
|
-
tickCount: runtime.tickCount,
|
|
131
|
-
simulationTimeSeconds: 0,
|
|
132
|
-
instanceCount: 1,
|
|
133
|
-
memberValueCount: 1,
|
|
134
|
-
quarantinedInstanceCount: 0,
|
|
135
|
-
engineErrorsCount: 0,
|
|
136
|
-
tickOverrunsCount: 0,
|
|
137
|
-
circuitBreakerTripsCount: 0,
|
|
138
|
-
uptimeMs: 0,
|
|
139
|
-
actualTicksPerSecond: 1,
|
|
140
|
-
},
|
|
141
|
-
fleet: {
|
|
142
|
-
modelBreakdown: [{ model: "test-sensor", count: 1 }],
|
|
143
|
-
lifecycleBreakdown: { running: 1 },
|
|
144
|
-
activeFaultCount: 0,
|
|
145
|
-
faultBreakdown: [],
|
|
146
|
-
},
|
|
147
|
-
adapters: runtime.connectedAdapters.map((name) => ({
|
|
148
|
-
name,
|
|
149
|
-
metrics: { connected: true },
|
|
150
|
-
})),
|
|
151
|
-
}),
|
|
152
|
-
getState: () => ({
|
|
153
|
-
...snapshot,
|
|
154
|
-
availableAdapters: runtime.enabledAdapters.map((name) => ({
|
|
155
|
-
name,
|
|
156
|
-
enabled: true,
|
|
157
|
-
pending: false,
|
|
158
|
-
config: {},
|
|
159
|
-
})),
|
|
160
|
-
}),
|
|
161
|
-
getAvailableAdapters: () =>
|
|
162
|
-
runtime.enabledAdapters.map((name) => ({
|
|
163
|
-
name,
|
|
164
|
-
enabled: true,
|
|
165
|
-
})),
|
|
166
|
-
getInstanceDetail: (_id: string) => undefined,
|
|
167
|
-
getModelSchema: (_name: string) => undefined,
|
|
168
|
-
getEventLog: () => ({ events: [], nextCursor: null, hasMore: false }),
|
|
169
|
-
getRunningTemplates: () => [],
|
|
170
|
-
getManifest: () => ({
|
|
171
|
-
generatedAt: new Date().toISOString(),
|
|
172
|
-
protocols: {},
|
|
173
|
-
tags: [],
|
|
174
|
-
summary: { totalTags: 0, totalInstances: 0, enabledProtocols: [] },
|
|
175
|
-
}),
|
|
176
|
-
execute: async (command: EngineCommand): Promise<CommandResult> => {
|
|
177
|
-
runtime.executedCommands.push(command);
|
|
178
|
-
return {
|
|
179
|
-
success: true,
|
|
180
|
-
command: command.type,
|
|
181
|
-
correlationId: command.correlationId,
|
|
182
|
-
message: `Executed ${command.type}`,
|
|
183
|
-
};
|
|
184
|
-
},
|
|
185
|
-
},
|
|
186
|
-
setState(state: EngineCoreMetrics["state"]) {
|
|
187
|
-
runtime.state = state;
|
|
188
|
-
},
|
|
189
|
-
setTickCount(tickCount: number) {
|
|
190
|
-
runtime.tickCount = tickCount;
|
|
191
|
-
},
|
|
192
|
-
setLastTickProgressAt(timestamp: number) {
|
|
193
|
-
runtime.lastTickProgressAt = timestamp;
|
|
194
|
-
},
|
|
195
|
-
getLastTickProgressAt() {
|
|
196
|
-
return runtime.lastTickProgressAt;
|
|
197
|
-
},
|
|
198
|
-
getExecutedCommands() {
|
|
199
|
-
return [...runtime.executedCommands];
|
|
200
|
-
},
|
|
201
|
-
setConnectedAdapters(adapterNames: string[]) {
|
|
202
|
-
runtime.connectedAdapters = [...adapterNames];
|
|
203
|
-
},
|
|
204
|
-
setEnabledAdapters(adapterNames: string[]) {
|
|
205
|
-
runtime.enabledAdapters = [...adapterNames];
|
|
206
|
-
},
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function serverBaseUrl(endpoint: string): string {
|
|
211
|
-
const url = new URL(endpoint);
|
|
212
|
-
return `http://127.0.0.1:${url.port}`;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
interface StartServerOptions {
|
|
216
|
-
readonly state: EngineCoreMetrics["state"];
|
|
217
|
-
readonly tickIntervalMs?: number;
|
|
218
|
-
readonly host?: string;
|
|
219
|
-
/** Adapter names that should report as connected in mock metrics. */
|
|
220
|
-
readonly connectedAdapters?: string[];
|
|
221
|
-
/** Adapter names that should report as enabled in the runtime state. */
|
|
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;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async function startTestServer(options: StartServerOptions, handles: Array<{ stop: () => void }>) {
|
|
232
|
-
const port = await reservePort();
|
|
233
|
-
const mock = createMockAdminEngine(
|
|
234
|
-
options.state,
|
|
235
|
-
options.tickIntervalMs ?? 1000,
|
|
236
|
-
options.connectedAdapters,
|
|
237
|
-
options.enabledAdapters,
|
|
238
|
-
);
|
|
239
|
-
const handle = startAdminServer({
|
|
240
|
-
engine: mock.engine,
|
|
241
|
-
network: {
|
|
242
|
-
host: options.host ?? "127.0.0.1",
|
|
243
|
-
port,
|
|
244
|
-
tickIntervalMs: options.tickIntervalMs ?? 1000,
|
|
245
|
-
},
|
|
246
|
-
protocols: {
|
|
247
|
-
getLastTickProgressAt: () => mock.getLastTickProgressAt(),
|
|
248
|
-
},
|
|
249
|
-
auth: testAuth(),
|
|
250
|
-
...(options.getLeExport ? { getLeExport: options.getLeExport } : {}),
|
|
251
|
-
});
|
|
252
|
-
handles.push(handle);
|
|
253
|
-
const baseUrl = serverBaseUrl(handle.endpoint);
|
|
254
|
-
return { mock, baseUrl };
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async function startShutdownTestServer(handles: Array<{ stop: () => void }>): Promise<{
|
|
258
|
-
baseUrl: string;
|
|
259
|
-
getShutdownReason: () => string;
|
|
260
|
-
}> {
|
|
261
|
-
const port = await reservePort();
|
|
262
|
-
const mock = createMockAdminEngine("running", 1000);
|
|
263
|
-
let shutdownReason = "";
|
|
264
|
-
const handle = startAdminServer({
|
|
265
|
-
engine: mock.engine,
|
|
266
|
-
network: { host: "127.0.0.1", port, tickIntervalMs: 1000 },
|
|
267
|
-
protocols: { getLastTickProgressAt: () => mock.getLastTickProgressAt() },
|
|
268
|
-
auth: testAuth(),
|
|
269
|
-
requestShutdown: (reason) => {
|
|
270
|
-
shutdownReason = reason;
|
|
271
|
-
},
|
|
272
|
-
});
|
|
273
|
-
handles.push(handle);
|
|
274
|
-
return { baseUrl: serverBaseUrl(handle.endpoint), getShutdownReason: () => shutdownReason };
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
describeIfTcp("admin server", () => {
|
|
278
|
-
const handles: Array<{ stop: () => void }> = [];
|
|
279
|
-
|
|
280
|
-
afterEach(() => {
|
|
281
|
-
for (const handle of handles) {
|
|
282
|
-
handle.stop();
|
|
283
|
-
}
|
|
284
|
-
handles.length = 0;
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
// --- Health endpoints (unauthenticated) ---
|
|
288
|
-
|
|
289
|
-
test.each([
|
|
290
|
-
{
|
|
291
|
-
label: "200 when running and mqtt connected",
|
|
292
|
-
state: "running" as const,
|
|
293
|
-
healthStatus: 200,
|
|
294
|
-
readyStatus: 200,
|
|
295
|
-
connectedAdapters: ["mqtt"] as string[],
|
|
296
|
-
},
|
|
297
|
-
{ label: "503 when paused", state: "paused" as const, healthStatus: 200, readyStatus: 503 },
|
|
298
|
-
])("/readyz returns $label", async ({ state, healthStatus, readyStatus, connectedAdapters }) => {
|
|
299
|
-
const { baseUrl } = await startTestServer(
|
|
300
|
-
{
|
|
301
|
-
state,
|
|
302
|
-
enabledAdapters: ["mqtt"],
|
|
303
|
-
connectedAdapters: connectedAdapters ?? [],
|
|
304
|
-
},
|
|
305
|
-
handles,
|
|
306
|
-
);
|
|
307
|
-
|
|
308
|
-
const health = await fetch(`${baseUrl}/healthz`);
|
|
309
|
-
const ready = await fetch(`${baseUrl}/readyz`);
|
|
310
|
-
|
|
311
|
-
expect(health.status).toBe(healthStatus);
|
|
312
|
-
expect(ready.status).toBe(readyStatus);
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
test("/readyz returns 503 when mqtt is enabled but disconnected", async () => {
|
|
316
|
-
const { baseUrl } = await startTestServer(
|
|
317
|
-
{ state: "running", enabledAdapters: ["mqtt"] },
|
|
318
|
-
handles,
|
|
319
|
-
);
|
|
320
|
-
|
|
321
|
-
const ready = await fetch(`${baseUrl}/readyz`);
|
|
322
|
-
expect(ready.status).toBe(503);
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
test("/readyz returns 503 when opcua is enabled but disconnected", async () => {
|
|
326
|
-
const { baseUrl } = await startTestServer(
|
|
327
|
-
{
|
|
328
|
-
state: "running",
|
|
329
|
-
enabledAdapters: ["opcua"],
|
|
330
|
-
},
|
|
331
|
-
handles,
|
|
332
|
-
);
|
|
333
|
-
|
|
334
|
-
const ready = await fetch(`${baseUrl}/readyz`);
|
|
335
|
-
expect(ready.status).toBe(503);
|
|
336
|
-
const body = (await ready.json()) as { reason?: string };
|
|
337
|
-
expect(body.reason).toBe("opcua disconnected");
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
test("/readyz follows live adapter enablement instead of boot-time flags", async () => {
|
|
341
|
-
const { mock, baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
342
|
-
|
|
343
|
-
const initiallyReady = await fetch(`${baseUrl}/readyz`);
|
|
344
|
-
expect(initiallyReady.status).toBe(200);
|
|
345
|
-
|
|
346
|
-
mock.setEnabledAdapters(["mqtt"]);
|
|
347
|
-
const disconnected = await fetch(`${baseUrl}/readyz`);
|
|
348
|
-
expect(disconnected.status).toBe(503);
|
|
349
|
-
|
|
350
|
-
mock.setConnectedAdapters(["mqtt"]);
|
|
351
|
-
const connected = await fetch(`${baseUrl}/readyz`);
|
|
352
|
-
expect(connected.status).toBe(200);
|
|
353
|
-
|
|
354
|
-
mock.setEnabledAdapters([]);
|
|
355
|
-
mock.setConnectedAdapters([]);
|
|
356
|
-
const disabledAgain = await fetch(`${baseUrl}/readyz`);
|
|
357
|
-
expect(disabledAgain.status).toBe(200);
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
test("/healthz returns 503 on the first probe after running tick loop stalls", async () => {
|
|
361
|
-
const { mock, baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
362
|
-
mock.setLastTickProgressAt(performance.now() - 3_500);
|
|
363
|
-
|
|
364
|
-
const stalled = await fetch(`${baseUrl}/healthz`);
|
|
365
|
-
expect(stalled.status).toBe(503);
|
|
366
|
-
|
|
367
|
-
mock.setLastTickProgressAt(performance.now());
|
|
368
|
-
mock.setTickCount(1);
|
|
369
|
-
const recovered = await fetch(`${baseUrl}/healthz`);
|
|
370
|
-
expect(recovered.status).toBe(200);
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
// --- Protected endpoints (require session cookie) ---
|
|
374
|
-
|
|
375
|
-
test("GET /metrics returns the engine metrics snapshot", async () => {
|
|
376
|
-
const { mock, baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
377
|
-
mock.setTickCount(12);
|
|
378
|
-
const cookie = await loginAndGetCookie(baseUrl);
|
|
379
|
-
|
|
380
|
-
const response = await fetch(`${baseUrl}/metrics`, {
|
|
381
|
-
headers: { cookie },
|
|
382
|
-
});
|
|
383
|
-
const body = (await response.json()) as {
|
|
384
|
-
core: { tickCount: number; state: EngineCoreMetrics["state"] };
|
|
385
|
-
simulationName: string;
|
|
386
|
-
};
|
|
387
|
-
|
|
388
|
-
expect(response.status).toBe(200);
|
|
389
|
-
expect(body.simulationName).toBe("test-sim");
|
|
390
|
-
expect(body.core.tickCount).toBe(12);
|
|
391
|
-
expect(body.core.state).toBe("running");
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
test("GET /state returns the engine state snapshot", async () => {
|
|
395
|
-
const { baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
396
|
-
const cookie = await loginAndGetCookie(baseUrl);
|
|
397
|
-
|
|
398
|
-
const response = await fetch(`${baseUrl}/state`, {
|
|
399
|
-
headers: { cookie },
|
|
400
|
-
});
|
|
401
|
-
const body = (await response.json()) as EngineStateSnapshot;
|
|
402
|
-
|
|
403
|
-
expect(response.status).toBe(200);
|
|
404
|
-
expect(body.simulationName).toBe("test-sim");
|
|
405
|
-
expect(body.config.tickIntervalMs).toBe(1000);
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
test("POST /commands validates and dispatches engine commands", async () => {
|
|
409
|
-
const { mock, baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
410
|
-
const cookie = await loginAndGetCookie(baseUrl);
|
|
411
|
-
|
|
412
|
-
const response = await fetch(`${baseUrl}/commands`, {
|
|
413
|
-
method: "POST",
|
|
414
|
-
headers: { "content-type": "application/json", cookie },
|
|
415
|
-
body: JSON.stringify({
|
|
416
|
-
type: "pause",
|
|
417
|
-
correlationId: "req-1",
|
|
418
|
-
}),
|
|
419
|
-
});
|
|
420
|
-
const body = (await response.json()) as CommandResult;
|
|
421
|
-
|
|
422
|
-
expect(response.status).toBe(200);
|
|
423
|
-
expect(body.success).toBe(true);
|
|
424
|
-
expect(body.command).toBe("pause");
|
|
425
|
-
expect(mock.getExecutedCommands()).toEqual([{ type: "pause", correlationId: "req-1" }]);
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
test("POST /commands requires session auth", async () => {
|
|
429
|
-
const { baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
430
|
-
// Without session cookie
|
|
431
|
-
const response = await fetch(`${baseUrl}/commands`, {
|
|
432
|
-
method: "POST",
|
|
433
|
-
headers: { "content-type": "application/json" },
|
|
434
|
-
body: JSON.stringify({ type: "pause", correlationId: "req-1" }),
|
|
435
|
-
});
|
|
436
|
-
expect(response.status).toBe(401);
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
test("POST /commands returns 400 for invalid JSON", async () => {
|
|
440
|
-
const { baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
441
|
-
const cookie = await loginAndGetCookie(baseUrl);
|
|
442
|
-
|
|
443
|
-
const response = await fetch(`${baseUrl}/commands`, {
|
|
444
|
-
method: "POST",
|
|
445
|
-
headers: { "content-type": "application/json", cookie },
|
|
446
|
-
body: "{bad json",
|
|
447
|
-
});
|
|
448
|
-
const body = (await response.json()) as { message: string };
|
|
449
|
-
|
|
450
|
-
expect(response.status).toBe(400);
|
|
451
|
-
expect(body.message).toBe("Invalid JSON in command request body.");
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
test("POST /commands returns 400 for invalid command shape", async () => {
|
|
455
|
-
const { baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
456
|
-
const cookie = await loginAndGetCookie(baseUrl);
|
|
457
|
-
|
|
458
|
-
const response = await fetch(`${baseUrl}/commands`, {
|
|
459
|
-
method: "POST",
|
|
460
|
-
headers: { "content-type": "application/json", cookie },
|
|
461
|
-
body: JSON.stringify({ type: "setSpeed" }),
|
|
462
|
-
});
|
|
463
|
-
const body = (await response.json()) as CommandResult;
|
|
464
|
-
|
|
465
|
-
expect(response.status).toBe(400);
|
|
466
|
-
expect(body.success).toBe(false);
|
|
467
|
-
expect(body.command).toBe("setSpeed");
|
|
468
|
-
});
|
|
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
|
-
|
|
524
|
-
// --- Daemon mode endpoints ---
|
|
525
|
-
|
|
526
|
-
test("/readyz includes simulationName and runId when provided", async () => {
|
|
527
|
-
const port = await reservePort();
|
|
528
|
-
const mock = createMockAdminEngine("running", 1000);
|
|
529
|
-
const handle = startAdminServer({
|
|
530
|
-
engine: mock.engine,
|
|
531
|
-
network: { host: "127.0.0.1", port, tickIntervalMs: 1000 },
|
|
532
|
-
protocols: { getLastTickProgressAt: () => mock.getLastTickProgressAt() },
|
|
533
|
-
auth: testAuth(),
|
|
534
|
-
simulationName: "test-daemon",
|
|
535
|
-
runId: "run-abc-123",
|
|
536
|
-
});
|
|
537
|
-
handles.push(handle);
|
|
538
|
-
const baseUrl = serverBaseUrl(handle.endpoint);
|
|
539
|
-
|
|
540
|
-
const response = await fetch(`${baseUrl}/readyz`);
|
|
541
|
-
const body = (await response.json()) as Record<string, unknown>;
|
|
542
|
-
|
|
543
|
-
expect(response.status).toBe(200);
|
|
544
|
-
expect(body["simulationName"]).toBe("test-daemon");
|
|
545
|
-
expect(body["runId"]).toBe("run-abc-123");
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
test("/readyz omits simulationName and runId when not provided", async () => {
|
|
549
|
-
const { baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
550
|
-
|
|
551
|
-
const response = await fetch(`${baseUrl}/readyz`);
|
|
552
|
-
const body = (await response.json()) as Record<string, unknown>;
|
|
553
|
-
|
|
554
|
-
expect(response.status).toBe(200);
|
|
555
|
-
expect(body["simulationName"]).toBeUndefined();
|
|
556
|
-
expect(body["runId"]).toBeUndefined();
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
test("POST /shutdown calls requestShutdown callback and returns 200", async () => {
|
|
560
|
-
const { baseUrl, getShutdownReason } = await startShutdownTestServer(handles);
|
|
561
|
-
const cookie = await loginAndGetCookie(baseUrl);
|
|
562
|
-
|
|
563
|
-
const response = await fetch(`${baseUrl}/shutdown`, {
|
|
564
|
-
method: "POST",
|
|
565
|
-
headers: { "content-type": "application/json", cookie },
|
|
566
|
-
body: JSON.stringify({ reason: "miravo stop" }),
|
|
567
|
-
});
|
|
568
|
-
const body = (await response.json()) as Record<string, unknown>;
|
|
569
|
-
|
|
570
|
-
expect(response.status).toBe(200);
|
|
571
|
-
expect(body["status"]).toBe("shutting_down");
|
|
572
|
-
expect(getShutdownReason()).toBe("miravo stop");
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
test("POST /shutdown uses default reason when body is empty", async () => {
|
|
576
|
-
const { baseUrl, getShutdownReason } = await startShutdownTestServer(handles);
|
|
577
|
-
const cookie = await loginAndGetCookie(baseUrl);
|
|
578
|
-
|
|
579
|
-
const response = await fetch(`${baseUrl}/shutdown`, {
|
|
580
|
-
method: "POST",
|
|
581
|
-
headers: { "content-type": "application/json", cookie },
|
|
582
|
-
});
|
|
583
|
-
expect(response.status).toBe(200);
|
|
584
|
-
expect(getShutdownReason()).toBe("remote shutdown request");
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
test("GET /shutdown returns 405", async () => {
|
|
588
|
-
const port = await reservePort();
|
|
589
|
-
const mock = createMockAdminEngine("running", 1000);
|
|
590
|
-
const handle = startAdminServer({
|
|
591
|
-
engine: mock.engine,
|
|
592
|
-
network: { host: "127.0.0.1", port, tickIntervalMs: 1000 },
|
|
593
|
-
protocols: { getLastTickProgressAt: () => mock.getLastTickProgressAt() },
|
|
594
|
-
auth: testAuth(),
|
|
595
|
-
requestShutdown: () => {},
|
|
596
|
-
});
|
|
597
|
-
handles.push(handle);
|
|
598
|
-
const baseUrl = serverBaseUrl(handle.endpoint);
|
|
599
|
-
|
|
600
|
-
const response = await fetch(`${baseUrl}/shutdown`);
|
|
601
|
-
expect(response.status).toBe(405);
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
test("POST /shutdown returns 501 when requestShutdown not provided", async () => {
|
|
605
|
-
const { baseUrl } = await startTestServer({ state: "running" }, handles);
|
|
606
|
-
const cookie = await loginAndGetCookie(baseUrl);
|
|
607
|
-
|
|
608
|
-
const response = await fetch(`${baseUrl}/shutdown`, {
|
|
609
|
-
method: "POST",
|
|
610
|
-
headers: { cookie },
|
|
611
|
-
});
|
|
612
|
-
expect(response.status).toBe(501);
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
test("POST /shutdown requires session auth", async () => {
|
|
616
|
-
const { baseUrl } = await startShutdownTestServer(handles);
|
|
617
|
-
// Without session cookie
|
|
618
|
-
const noAuth = await fetch(`${baseUrl}/shutdown`, {
|
|
619
|
-
method: "POST",
|
|
620
|
-
headers: { "content-type": "application/json" },
|
|
621
|
-
});
|
|
622
|
-
expect(noAuth.status).toBe(401);
|
|
623
|
-
// With session cookie
|
|
624
|
-
const cookie = await loginAndGetCookie(baseUrl);
|
|
625
|
-
const withAuth = await fetch(`${baseUrl}/shutdown`, {
|
|
626
|
-
method: "POST",
|
|
627
|
-
headers: { "content-type": "application/json", cookie },
|
|
628
|
-
});
|
|
629
|
-
expect(withAuth.status).toBe(200);
|
|
630
|
-
});
|
|
631
|
-
|
|
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() {
|
|
729
|
-
const port = await reservePort();
|
|
730
|
-
const mock = createMockAdminEngine("running", 1000);
|
|
731
|
-
const handle = startAdminServer({
|
|
732
|
-
engine: mock.engine,
|
|
733
|
-
network: { host: "127.0.0.1", port, tickIntervalMs: 1000 },
|
|
734
|
-
protocols: { getLastTickProgressAt: () => mock.getLastTickProgressAt() },
|
|
735
|
-
auth: { ...testAuth(), anonymousMode: true },
|
|
736
|
-
});
|
|
737
|
-
handles.push(handle);
|
|
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
|
-
});
|
|
753
|
-
|
|
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`, {
|
|
763
|
-
method: "POST",
|
|
764
|
-
headers: { "content-type": "application/json" },
|
|
765
|
-
body: JSON.stringify({ username: "whoever", password: "anything" }),
|
|
766
|
-
});
|
|
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
|
-
});
|
|
772
|
-
|
|
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");
|
|
780
|
-
});
|
|
781
|
-
});
|