test: share devices cli fixtures

This commit is contained in:
Peter Steinberger
2026-04-20 16:08:15 +01:00
parent decdb92f34
commit 3381e4c375

View File

@@ -69,6 +69,55 @@ function readRuntimeCallText(call: unknown[] | undefined): string {
return typeof value === "string" ? value : "";
}
function readRuntimeOutput(): string {
return runtime.log.mock.calls.map((entry) => readRuntimeCallText(entry)).join("\n");
}
function pendingDevice(overrides: Record<string, unknown> = {}) {
return {
requestId: "req-1",
deviceId: "device-1",
displayName: "Device One",
role: "operator",
scopes: ["operator.admin"],
ts: 1,
...overrides,
};
}
function pairedDevice(overrides: Record<string, unknown> = {}) {
return {
deviceId: "device-1",
displayName: "Device One",
roles: ["operator"],
scopes: ["operator.read"],
...overrides,
};
}
function mockGatewayPairingList(
pendingOverrides: Record<string, unknown> = {},
pairedOverrides: Record<string, unknown> = {},
) {
callGateway.mockResolvedValueOnce({
pending: [pendingDevice(pendingOverrides)],
paired: [pairedDevice(pairedOverrides)],
});
}
function rejectGatewayForLocalFallback(message = "gateway closed (1008): pairing required") {
callGateway.mockRejectedValueOnce(new Error(message));
}
function mockLocalPairingFallback(message?: string) {
rejectGatewayForLocalFallback(message);
listDevicePairing.mockResolvedValueOnce({
pending: [{ requestId: "req-1", deviceId: "device-1", publicKey: "pk", ts: 1 }],
paired: [],
});
summarizeDeviceTokens.mockReturnValue(undefined);
}
describe("devices cli approve", () => {
it("approves an explicit request id without listing", async () => {
callGateway.mockResolvedValueOnce({ device: { deviceId: "device-1" } });
@@ -401,12 +450,7 @@ describe("devices cli local fallback", () => {
const fallbackNotice = "Direct scope access failed; using local fallback.";
it("falls back to local pairing list when gateway returns pairing required on loopback", async () => {
callGateway.mockRejectedValueOnce(new Error("gateway closed (1008): pairing required"));
listDevicePairing.mockResolvedValueOnce({
pending: [{ requestId: "req-1", deviceId: "device-1", publicKey: "pk", ts: 1 }],
paired: [],
});
summarizeDeviceTokens.mockReturnValue(undefined);
mockLocalPairingFallback();
await runDevicesCommand(["list"]);
@@ -418,7 +462,7 @@ describe("devices cli local fallback", () => {
});
it("falls back to local approve when gateway returns pairing required on loopback", async () => {
callGateway.mockRejectedValueOnce(new Error("gateway closed (1008): pairing required"));
rejectGatewayForLocalFallback();
approveDevicePairing.mockResolvedValueOnce({
requestId: "req-latest",
device: {
@@ -440,14 +484,7 @@ describe("devices cli local fallback", () => {
});
it("falls back to local pairing list when gateway returns a scope upgrade message on loopback", async () => {
callGateway.mockRejectedValueOnce(
new Error("scope upgrade pending approval (requestId: req-123)"),
);
listDevicePairing.mockResolvedValueOnce({
pending: [{ requestId: "req-1", deviceId: "device-1", publicKey: "pk", ts: 1 }],
paired: [],
});
summarizeDeviceTokens.mockReturnValue(undefined);
mockLocalPairingFallback("scope upgrade pending approval (requestId: req-123)");
await runDevicesCommand(["list"]);
@@ -456,7 +493,7 @@ describe("devices cli local fallback", () => {
});
it("does not use local fallback when an explicit --url is provided", async () => {
callGateway.mockRejectedValueOnce(new Error("gateway closed (1008): pairing required"));
rejectGatewayForLocalFallback();
await expect(
runDevicesCommand(["list", "--json", "--url", "ws://127.0.0.1:18789"]),
@@ -467,30 +504,11 @@ describe("devices cli local fallback", () => {
describe("devices cli list", () => {
it("renders requested versus approved access for pending upgrades", async () => {
callGateway.mockResolvedValueOnce({
pending: [
{
requestId: "req-1",
deviceId: "device-1",
displayName: "Device One",
role: "operator",
scopes: ["operator.admin", "operator.read"],
ts: 1,
},
],
paired: [
{
deviceId: "device-1",
displayName: "Device One",
roles: ["operator"],
scopes: ["operator.read"],
},
],
});
mockGatewayPairingList({ scopes: ["operator.admin", "operator.read"] });
await runDevicesCommand(["list"]);
const output = runtime.log.mock.calls.map((entry) => readRuntimeCallText(entry)).join("\n");
const output = readRuntimeOutput();
expect(output).toContain("Requested");
expect(output).toContain("Approved");
expect(output).toContain("operator.write");
@@ -499,61 +517,21 @@ describe("devices cli list", () => {
});
it("normalizes pending device ids before matching paired approvals", async () => {
callGateway.mockResolvedValueOnce({
pending: [
{
requestId: "req-1",
deviceId: " device-1 ",
displayName: "Device One",
role: "operator",
scopes: ["operator.admin"],
ts: 1,
},
],
paired: [
{
deviceId: "device-1",
displayName: "Device One",
roles: ["operator"],
scopes: ["operator.read"],
},
],
});
mockGatewayPairingList({ deviceId: " device-1 " });
await runDevicesCommand(["list"]);
const output = runtime.log.mock.calls.map((entry) => readRuntimeCallText(entry)).join("\n");
const output = readRuntimeOutput();
expect(output).toContain("scope upgrade");
expect(output).toContain("operator.read");
});
it("does not show upgrade context for key-mismatched pending requests", async () => {
callGateway.mockResolvedValueOnce({
pending: [
{
requestId: "req-1",
deviceId: "device-1",
publicKey: "new-key",
displayName: "Device One",
role: "operator",
scopes: ["operator.admin"],
ts: 1,
},
],
paired: [
{
deviceId: "device-1",
publicKey: "old-key",
displayName: "Device One",
roles: ["operator"],
scopes: ["operator.read"],
},
],
});
mockGatewayPairingList({ publicKey: "new-key" }, { publicKey: "old-key" });
await runDevicesCommand(["list"]);
const output = runtime.log.mock.calls.map((entry) => readRuntimeCallText(entry)).join("\n");
const output = readRuntimeOutput();
expect(output).toContain("new pairing");
expect(output).not.toContain("scope upgrade");
expect(output).not.toContain("roles: operator; scopes: operator.read");
@@ -585,7 +563,7 @@ describe("devices cli list", () => {
await runDevicesCommand(["list"]);
const output = runtime.log.mock.calls.map((entry) => readRuntimeCallText(entry)).join("\n");
const output = readRuntimeOutput();
expect(output).not.toContain("\u001b");
expect(output).not.toContain("\r");
expect(output).toContain("BadName");