fix: harden bootstrap and transport ready coverage

This commit is contained in:
Peter Steinberger
2026-03-14 00:22:13 +00:00
parent 7a53eb7ea8
commit 3c70e50af5
3 changed files with 76 additions and 4 deletions

View File

@@ -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();

View File

@@ -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;

View File

@@ -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();
});
});