|
1 | | -import { readFile, writeFile } from "node:fs/promises"; |
| 1 | +import { mkdir, readFile, writeFile } from "node:fs/promises"; |
2 | 2 | import { afterAll, beforeAll, describe, expect, test } from "vitest"; |
3 | 3 | import { PAIRING_SETUP_BOOTSTRAP_PROFILE } from "../shared/device-bootstrap-profile.js"; |
4 | 4 | import { createSuiteTempRootTracker } from "../test-helpers/temp-dir.js"; |
@@ -161,6 +161,42 @@ describe("device pairing tokens", () => { |
161 | 161 | expect(second.request.requestId).toBe(first.request.requestId); |
162 | 162 | }); |
163 | 163 |
|
| 164 | + test("recovers when pairing state files were written as arrays", async () => { |
| 165 | + const baseDir = await makeDevicePairingDir(); |
| 166 | + const paths = resolvePairingPaths(baseDir, "devices"); |
| 167 | + await mkdir(paths.dir, { recursive: true }); |
| 168 | + await writeFile(paths.pendingPath, "[]", "utf8"); |
| 169 | + await writeFile(paths.pairedPath, "[]", "utf8"); |
| 170 | + |
| 171 | + const pending = await requestDevicePairing( |
| 172 | + { |
| 173 | + deviceId: "device-array-state", |
| 174 | + publicKey: "public-key-array-state", |
| 175 | + role: "operator", |
| 176 | + scopes: ["operator.read"], |
| 177 | + }, |
| 178 | + baseDir, |
| 179 | + ); |
| 180 | + const approved = await approveDevicePairing( |
| 181 | + pending.request.requestId, |
| 182 | + { callerScopes: ["operator.read"] }, |
| 183 | + baseDir, |
| 184 | + ); |
| 185 | + |
| 186 | + expect(approved).toEqual( |
| 187 | + expect.objectContaining({ |
| 188 | + status: "approved", |
| 189 | + device: expect.objectContaining({ deviceId: "device-array-state" }), |
| 190 | + }), |
| 191 | + ); |
| 192 | + expect(Array.isArray(JSON.parse(await readFile(paths.pendingPath, "utf8")))).toBe(false); |
| 193 | + expect(JSON.parse(await readFile(paths.pairedPath, "utf8"))).toEqual( |
| 194 | + expect.objectContaining({ |
| 195 | + "device-array-state": expect.objectContaining({ deviceId: "device-array-state" }), |
| 196 | + }), |
| 197 | + ); |
| 198 | + }); |
| 199 | + |
164 | 200 | test("re-requesting with identical params preserves the original ts to prevent queue-jumping", async () => { |
165 | 201 | // Regression: refreshPendingDevicePairingRequest must not bump ts to Date.now(). |
166 | 202 | // An attacker who reconnects with the same key/role/scopes could otherwise |
|
0 commit comments