fix: harden bootstrap and transport ready coverage
This commit is contained in:
@@ -91,6 +91,24 @@ describe("device bootstrap tokens", () => {
|
||||
expect(raw).toContain(issued.token);
|
||||
});
|
||||
|
||||
it("accepts trimmed bootstrap tokens and still consumes them once", async () => {
|
||||
const baseDir = await createTempDir();
|
||||
const issued = await issueDeviceBootstrapToken({ baseDir });
|
||||
|
||||
await expect(
|
||||
verifyDeviceBootstrapToken({
|
||||
token: ` ${issued.token} `,
|
||||
deviceId: "device-123",
|
||||
publicKey: "public-key-123",
|
||||
role: "operator.admin",
|
||||
scopes: ["operator.admin"],
|
||||
baseDir,
|
||||
}),
|
||||
).resolves.toEqual({ ok: true });
|
||||
|
||||
await expect(fs.readFile(resolveBootstrapPath(baseDir), "utf8")).resolves.toBe("{}");
|
||||
});
|
||||
|
||||
it("rejects blank or unknown tokens", async () => {
|
||||
const baseDir = await createTempDir();
|
||||
await issueDeviceBootstrapToken({ baseDir });
|
||||
@@ -118,6 +136,23 @@ describe("device bootstrap tokens", () => {
|
||||
).resolves.toEqual({ ok: false, reason: "bootstrap_token_invalid" });
|
||||
});
|
||||
|
||||
it("repairs malformed persisted state when issuing a new token", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-03-14T12:00:00Z"));
|
||||
|
||||
const baseDir = await createTempDir();
|
||||
const bootstrapPath = resolveBootstrapPath(baseDir);
|
||||
await fs.mkdir(path.dirname(bootstrapPath), { recursive: true });
|
||||
await fs.writeFile(bootstrapPath, "[1,2,3]\n", "utf8");
|
||||
|
||||
const issued = await issueDeviceBootstrapToken({ baseDir });
|
||||
const raw = await fs.readFile(bootstrapPath, "utf8");
|
||||
const parsed = JSON.parse(raw) as Record<string, { token: string }>;
|
||||
|
||||
expect(Object.keys(parsed)).toEqual([issued.token]);
|
||||
expect(parsed[issued.token]?.token).toBe(issued.token);
|
||||
});
|
||||
|
||||
it("accepts legacy records that only stored issuedAtMs and prunes expired tokens", async () => {
|
||||
vi.useFakeTimers();
|
||||
const baseDir = await createTempDir();
|
||||
|
||||
@@ -31,11 +31,25 @@ function resolveBootstrapPath(baseDir?: string): string {
|
||||
|
||||
async function loadState(baseDir?: string): Promise<DeviceBootstrapStateFile> {
|
||||
const bootstrapPath = resolveBootstrapPath(baseDir);
|
||||
const state = (await readJsonFile<DeviceBootstrapStateFile>(bootstrapPath)) ?? {};
|
||||
for (const entry of Object.values(state)) {
|
||||
if (typeof entry.ts !== "number") {
|
||||
entry.ts = entry.issuedAtMs;
|
||||
const rawState = (await readJsonFile<DeviceBootstrapStateFile>(bootstrapPath)) ?? {};
|
||||
const state: DeviceBootstrapStateFile = {};
|
||||
if (!rawState || typeof rawState !== "object" || Array.isArray(rawState)) {
|
||||
return state;
|
||||
}
|
||||
for (const [tokenKey, entry] of Object.entries(rawState)) {
|
||||
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
||||
continue;
|
||||
}
|
||||
const record = entry as Partial<DeviceBootstrapTokenRecord>;
|
||||
const token =
|
||||
typeof record.token === "string" && record.token.trim().length > 0 ? record.token : tokenKey;
|
||||
const issuedAtMs = typeof record.issuedAtMs === "number" ? record.issuedAtMs : 0;
|
||||
state[tokenKey] = {
|
||||
...record,
|
||||
token,
|
||||
issuedAtMs,
|
||||
ts: typeof record.ts === "number" ? record.ts : issuedAtMs,
|
||||
};
|
||||
}
|
||||
pruneExpiredPending(state, Date.now(), DEVICE_BOOTSTRAP_TOKEN_TTL_MS);
|
||||
return state;
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { waitForTransportReady } from "./transport-ready.js";
|
||||
|
||||
let injectedSleepError: Error | null = null;
|
||||
|
||||
// Perf: `sleepWithAbort` uses `node:timers/promises` which isn't controlled by fake timers.
|
||||
// Route sleeps through global `setTimeout` so tests can advance time deterministically.
|
||||
vi.mock("./backoff.js", () => ({
|
||||
sleepWithAbort: async (ms: number, signal?: AbortSignal) => {
|
||||
if (injectedSleepError) {
|
||||
throw injectedSleepError;
|
||||
}
|
||||
if (signal?.aborted) {
|
||||
throw new Error("aborted");
|
||||
}
|
||||
@@ -37,6 +42,7 @@ describe("waitForTransportReady", () => {
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
injectedSleepError = null;
|
||||
});
|
||||
|
||||
it("returns when the check succeeds and logs after the delay", async () => {
|
||||
@@ -143,4 +149,21 @@ describe("waitForTransportReady", () => {
|
||||
expect(runtime.error.mock.calls.at(0)?.[0]).toContain("unknown error");
|
||||
expect(runtime.error.mock.calls.at(-1)?.[0]).toContain("not ready after 120ms");
|
||||
});
|
||||
|
||||
it("rethrows non-abort sleep failures", async () => {
|
||||
const runtime = createRuntime();
|
||||
injectedSleepError = new Error("sleep exploded");
|
||||
|
||||
await expect(
|
||||
waitForTransportReady({
|
||||
label: "test transport",
|
||||
timeoutMs: 500,
|
||||
pollIntervalMs: 50,
|
||||
runtime,
|
||||
check: async () => ({ ok: false, error: "still down" }),
|
||||
}),
|
||||
).rejects.toThrow("sleep exploded");
|
||||
|
||||
expect(runtime.error).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user