refactor: share exec approval iOS push fixtures

This commit is contained in:
Vincent Koc
2026-06-02 06:02:15 +02:00
parent 1e7a0d8987
commit 75bc80bb42

View File

@@ -1,4 +1,5 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { ExecApprovalRequest, ExecApprovalResolved } from "../infra/exec-approvals.js";
const listDevicePairingMock = vi.fn();
const loadApnsRegistrationMock = vi.fn();
@@ -28,31 +29,93 @@ function createDeferred<T>(): Deferred<T> {
return { promise, resolve, reject };
}
function mockPairedIosOperator(scopes: string[]) {
function apnsRegistration(nodeId = "ios-device-1") {
return {
nodeId,
transport: "direct",
token: "apns-token",
topic: "ai.openclaw.ios.test",
environment: "sandbox",
updatedAtMs: 1,
};
}
function successfulApnsPushResult() {
return {
ok: true,
status: 200,
environment: "sandbox",
topic: "ai.openclaw.ios.test",
tokenSuffix: "token",
transport: "direct",
};
}
function resolvedApnsAuthConfig() {
return {
ok: true,
value: { teamId: "team", keyId: "key", privateKey: "private-key" },
};
}
function approvalRequest(id: string): ExecApprovalRequest {
return {
id,
request: { command: "echo ok", host: "gateway", allowedDecisions: ["allow-once"] },
createdAtMs: 1,
expiresAtMs: 2,
};
}
function approvalResolved(id: string): ExecApprovalResolved {
return {
id,
decision: "allow-once",
ts: 1,
};
}
function pairedIosOperator(options: {
deviceId?: string;
publicKey?: string;
platform?: string;
approvedAtMs?: number;
scopes: string[];
approvedScopes?: string[];
token?: string;
}) {
const deviceId = options.deviceId ?? "ios-device-1";
return {
deviceId,
publicKey: options.publicKey ?? "pub",
platform: options.platform ?? "iOS 18",
role: "operator",
roles: ["operator"],
approvedScopes: options.approvedScopes,
createdAtMs: 1,
approvedAtMs: options.approvedAtMs ?? 1,
tokens: {
operator: {
token: options.token ?? "operator-token",
role: "operator",
scopes: options.scopes,
createdAtMs: 1,
},
},
};
}
function mockPairedIosOperators(...paired: ReturnType<typeof pairedIosOperator>[]) {
listDevicePairingMock.mockResolvedValue({
pending: [],
paired: [
{
deviceId: "ios-device-1",
publicKey: "pub",
platform: "iOS 18",
role: "operator",
roles: ["operator"],
createdAtMs: 1,
approvedAtMs: 1,
tokens: {
operator: {
token: "operator-token",
role: "operator",
scopes,
createdAtMs: 1,
},
},
},
],
paired,
});
}
function mockPairedIosOperator(scopes: string[]) {
mockPairedIosOperators(pairedIosOperator({ scopes }));
}
vi.mock("../config/config.js", () => ({
getRuntimeConfig: () => ({ gateway: {} }),
}));
@@ -86,14 +149,7 @@ describe("createExecApprovalIosPushDelivery", () => {
beforeEach(() => {
vi.clearAllMocks();
listDevicePairingMock.mockResolvedValue({ pending: [], paired: [] });
loadApnsRegistrationMock.mockResolvedValue({
nodeId: "ios-device-1",
transport: "direct",
token: "apns-token",
topic: "ai.openclaw.ios.test",
environment: "sandbox",
updatedAtMs: 1,
});
loadApnsRegistrationMock.mockResolvedValue(apnsRegistration());
loadApnsRegistrationsMock.mockImplementation(async (nodeIds: readonly string[]) => {
const registrations = [];
for (const nodeId of nodeIds) {
@@ -104,62 +160,23 @@ describe("createExecApprovalIosPushDelivery", () => {
}
return registrations;
});
resolveApnsAuthConfigFromEnvMock.mockResolvedValue({
ok: true,
value: { teamId: "team", keyId: "key", privateKey: "private-key" },
});
resolveApnsAuthConfigFromEnvMock.mockResolvedValue(resolvedApnsAuthConfig());
resolveApnsRelayConfigFromEnvMock.mockReturnValue({ ok: false, error: "unused" });
sendApnsExecApprovalAlertMock.mockResolvedValue({
ok: true,
status: 200,
environment: "sandbox",
topic: "ai.openclaw.ios.test",
tokenSuffix: "token",
transport: "direct",
});
sendApnsExecApprovalResolvedWakeMock.mockResolvedValue({
ok: true,
status: 200,
environment: "sandbox",
topic: "ai.openclaw.ios.test",
tokenSuffix: "token",
transport: "direct",
});
sendApnsExecApprovalAlertMock.mockResolvedValue(successfulApnsPushResult());
sendApnsExecApprovalResolvedWakeMock.mockResolvedValue(successfulApnsPushResult());
});
it("does not target iOS devices whose active operator token lacks operator.approvals", async () => {
listDevicePairingMock.mockResolvedValue({
pending: [],
paired: [
{
deviceId: "ios-device-1",
publicKey: "pub",
platform: "iOS 18",
role: "operator",
roles: ["operator"],
approvedScopes: ["operator.approvals"],
createdAtMs: 1,
approvedAtMs: 1,
tokens: {
operator: {
token: "operator-token",
role: "operator",
scopes: ["operator.read"],
createdAtMs: 1,
},
},
},
],
});
mockPairedIosOperators(
pairedIosOperator({
scopes: ["operator.read"],
approvedScopes: ["operator.approvals"],
}),
);
const delivery = createExecApprovalIosPushDelivery({ log: {} });
const accepted = await delivery.handleRequested({
id: "approval-1",
request: { command: "echo ok", host: "gateway", allowedDecisions: ["allow-once"] },
createdAtMs: 1,
expiresAtMs: 2,
});
const accepted = await delivery.handleRequested(approvalRequest("approval-1"));
expect(accepted).toBe(false);
expect(loadApnsRegistrationsMock).not.toHaveBeenCalled();
@@ -171,12 +188,7 @@ describe("createExecApprovalIosPushDelivery", () => {
const delivery = createExecApprovalIosPushDelivery({ log: {} });
const accepted = await delivery.handleRequested({
id: "approval-2",
request: { command: "echo ok", host: "gateway", allowedDecisions: ["allow-once"] },
createdAtMs: 1,
expiresAtMs: 2,
});
const accepted = await delivery.handleRequested(approvalRequest("approval-2"));
expect(accepted).toBe(true);
expect(loadApnsRegistrationsMock).toHaveBeenCalledWith(["ios-device-1"]);
@@ -184,54 +196,26 @@ describe("createExecApprovalIosPushDelivery", () => {
});
it("loads APNs registrations in one bulk read for all visible iOS operators", async () => {
listDevicePairingMock.mockResolvedValue({
pending: [],
paired: [
{
deviceId: "ios-device-1",
publicKey: "pub-1",
platform: "iOS 18",
role: "operator",
roles: ["operator"],
createdAtMs: 1,
approvedAtMs: 1,
tokens: {
operator: {
token: "operator-token-1",
role: "operator",
scopes: ["operator.approvals"],
createdAtMs: 1,
},
},
},
{
deviceId: "ios-device-2",
publicKey: "pub-2",
platform: "iPadOS 18",
role: "operator",
roles: ["operator"],
createdAtMs: 1,
approvedAtMs: 2,
tokens: {
operator: {
token: "operator-token-2",
role: "operator",
scopes: ["operator.approvals"],
createdAtMs: 1,
},
},
},
],
});
mockPairedIosOperators(
pairedIosOperator({
deviceId: "ios-device-1",
publicKey: "pub-1",
scopes: ["operator.approvals"],
token: "operator-token-1",
}),
pairedIosOperator({
deviceId: "ios-device-2",
publicKey: "pub-2",
platform: "iPadOS 18",
approvedAtMs: 2,
scopes: ["operator.approvals"],
token: "operator-token-2",
}),
);
const delivery = createExecApprovalIosPushDelivery({ log: {} });
await delivery.handleRequested({
id: "approval-bulk-load",
request: { command: "echo ok", host: "gateway", allowedDecisions: ["allow-once"] },
createdAtMs: 1,
expiresAtMs: 2,
});
await delivery.handleRequested(approvalRequest("approval-bulk-load"));
expect(loadApnsRegistrationsMock).toHaveBeenCalledTimes(1);
expect(loadApnsRegistrationsMock).toHaveBeenCalledWith(["ios-device-1", "ios-device-2"]);
@@ -243,15 +227,9 @@ describe("createExecApprovalIosPushDelivery", () => {
const delivery = createExecApprovalIosPushDelivery({ log: {} });
const accepted = await delivery.handleRequested(
{
id: "approval-filtered",
request: { command: "echo ok", host: "gateway", allowedDecisions: ["allow-once"] },
createdAtMs: 1,
expiresAtMs: 2,
},
{ isTargetVisible },
);
const accepted = await delivery.handleRequested(approvalRequest("approval-filtered"), {
isTargetVisible,
});
expect(accepted).toBe(false);
expect(isTargetVisible).toHaveBeenCalledWith({
@@ -277,12 +255,7 @@ describe("createExecApprovalIosPushDelivery", () => {
const delivery = createExecApprovalIosPushDelivery({ log: { warn } });
const accepted = await delivery.handleRequested({
id: "approval-dead-route",
request: { command: "echo ok", host: "gateway", allowedDecisions: ["allow-once"] },
createdAtMs: 1,
expiresAtMs: 2,
});
const accepted = await delivery.handleRequested(approvalRequest("approval-dead-route"));
expect(accepted).toBe(false);
expect(sendApnsExecApprovalAlertMock).toHaveBeenCalledTimes(1);
@@ -308,29 +281,13 @@ describe("createExecApprovalIosPushDelivery", () => {
const delivery = createExecApprovalIosPushDelivery({ log: {} });
const requested = delivery.handleRequested({
id: "approval-ordered-cleanup",
request: { command: "echo ok", host: "gateway", allowedDecisions: ["allow-once"] },
createdAtMs: 1,
expiresAtMs: 2,
});
const resolved = delivery.handleResolved({
id: "approval-ordered-cleanup",
decision: "allow-once",
ts: 1,
});
const requested = delivery.handleRequested(approvalRequest("approval-ordered-cleanup"));
const resolved = delivery.handleResolved(approvalResolved("approval-ordered-cleanup"));
await Promise.resolve();
expect(sendApnsExecApprovalResolvedWakeMock).not.toHaveBeenCalled();
requestedPush.resolve({
ok: true,
status: 200,
environment: "sandbox",
topic: "ai.openclaw.ios.test",
tokenSuffix: "token",
transport: "direct",
});
requestedPush.resolve(successfulApnsPushResult());
await requested;
await resolved;
@@ -341,11 +298,7 @@ describe("createExecApprovalIosPushDelivery", () => {
const debug = vi.fn();
const delivery = createExecApprovalIosPushDelivery({ log: { debug } });
await delivery.handleResolved({
id: "approval-missing-targets",
decision: "allow-once",
ts: 1,
});
await delivery.handleResolved(approvalResolved("approval-missing-targets"));
expect(debug).toHaveBeenCalledWith(
"exec approvals: iOS cleanup push skipped approvalId=approval-missing-targets reason=missing-targets",
@@ -360,31 +313,12 @@ describe("createExecApprovalIosPushDelivery", () => {
const delivery = createExecApprovalIosPushDelivery({ log: {} });
await delivery.handleRequested({
id: "approval-cleanup",
request: { command: "echo ok", host: "gateway", allowedDecisions: ["allow-once"] },
createdAtMs: 1,
expiresAtMs: 2,
});
await delivery.handleRequested(approvalRequest("approval-cleanup"));
vi.clearAllMocks();
loadApnsRegistrationMock.mockResolvedValue({
nodeId: "ios-device-1",
transport: "direct",
token: "apns-token",
topic: "ai.openclaw.ios.test",
environment: "sandbox",
updatedAtMs: 1,
});
resolveApnsAuthConfigFromEnvMock.mockResolvedValue({
ok: true,
value: { teamId: "team", keyId: "key", privateKey: "private-key" },
});
loadApnsRegistrationMock.mockResolvedValue(apnsRegistration());
resolveApnsAuthConfigFromEnvMock.mockResolvedValue(resolvedApnsAuthConfig());
await delivery.handleResolved({
id: "approval-cleanup",
decision: "allow-once",
ts: 1,
});
await delivery.handleResolved(approvalResolved("approval-cleanup"));
expect(listDevicePairingMock).not.toHaveBeenCalled();
expect(loadApnsRegistrationsMock).toHaveBeenCalledWith(["ios-device-1"]);