mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-12 18:02:56 +00:00
refactor: share exec approval iOS push fixtures
This commit is contained in:
@@ -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"]);
|
||||
|
||||
Reference in New Issue
Block a user