mirror of https://github.com/openclaw/openclaw.git
test: extract apns store coverage
This commit is contained in:
parent
d4f36fe0be
commit
e268e7a726
|
|
@ -0,0 +1,308 @@
|
|||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
clearApnsRegistration,
|
||||
clearApnsRegistrationIfCurrent,
|
||||
loadApnsRegistration,
|
||||
registerApnsRegistration,
|
||||
registerApnsToken,
|
||||
} from "./push-apns.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
async function makeTempDir(): Promise<string> {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-push-apns-store-test-"));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
while (tempDirs.length > 0) {
|
||||
const dir = tempDirs.pop();
|
||||
if (dir) {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("push APNs registration store", () => {
|
||||
it("stores and reloads direct APNs registrations", async () => {
|
||||
const baseDir = await makeTempDir();
|
||||
const saved = await registerApnsToken({
|
||||
nodeId: "ios-node-1",
|
||||
token: "ABCD1234ABCD1234ABCD1234ABCD1234",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "sandbox",
|
||||
baseDir,
|
||||
});
|
||||
|
||||
const loaded = await loadApnsRegistration("ios-node-1", baseDir);
|
||||
expect(loaded).toMatchObject({
|
||||
nodeId: "ios-node-1",
|
||||
transport: "direct",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "sandbox",
|
||||
updatedAtMs: saved.updatedAtMs,
|
||||
});
|
||||
expect(loaded && loaded.transport === "direct" ? loaded.token : null).toBe(
|
||||
"abcd1234abcd1234abcd1234abcd1234",
|
||||
);
|
||||
});
|
||||
|
||||
it("stores relay-backed registrations without a raw token", async () => {
|
||||
const baseDir = await makeTempDir();
|
||||
const saved = await registerApnsRegistration({
|
||||
nodeId: "ios-node-relay",
|
||||
transport: "relay",
|
||||
relayHandle: "relay-handle-123",
|
||||
sendGrant: "send-grant-123",
|
||||
installationId: "install-123",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "production",
|
||||
distribution: "official",
|
||||
tokenDebugSuffix: " abcd-1234 ",
|
||||
baseDir,
|
||||
});
|
||||
|
||||
const loaded = await loadApnsRegistration("ios-node-relay", baseDir);
|
||||
expect(saved.transport).toBe("relay");
|
||||
expect(loaded).toMatchObject({
|
||||
nodeId: "ios-node-relay",
|
||||
transport: "relay",
|
||||
relayHandle: "relay-handle-123",
|
||||
sendGrant: "send-grant-123",
|
||||
installationId: "install-123",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "production",
|
||||
distribution: "official",
|
||||
tokenDebugSuffix: "abcd1234",
|
||||
});
|
||||
expect(loaded && "token" in loaded).toBe(false);
|
||||
});
|
||||
|
||||
it("normalizes legacy direct records from disk and ignores invalid entries", async () => {
|
||||
const baseDir = await makeTempDir();
|
||||
const statePath = path.join(baseDir, "push", "apns-registrations.json");
|
||||
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
statePath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
registrationsByNodeId: {
|
||||
" ios-node-legacy ": {
|
||||
nodeId: " ios-node-legacy ",
|
||||
token: "<ABCD1234ABCD1234ABCD1234ABCD1234>",
|
||||
topic: " ai.openclaw.ios ",
|
||||
environment: " PRODUCTION ",
|
||||
updatedAtMs: 3,
|
||||
},
|
||||
" ": {
|
||||
nodeId: " ios-node-fallback ",
|
||||
token: "<ABCD1234ABCD1234ABCD1234ABCD1234>",
|
||||
topic: " ai.openclaw.ios ",
|
||||
updatedAtMs: 2,
|
||||
},
|
||||
"ios-node-bad-relay": {
|
||||
transport: "relay",
|
||||
nodeId: "ios-node-bad-relay",
|
||||
relayHandle: "relay-handle-123",
|
||||
sendGrant: "send-grant-123",
|
||||
installationId: "install-123",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "production",
|
||||
distribution: "beta",
|
||||
updatedAtMs: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
await expect(loadApnsRegistration("ios-node-legacy", baseDir)).resolves.toMatchObject({
|
||||
nodeId: "ios-node-legacy",
|
||||
transport: "direct",
|
||||
token: "abcd1234abcd1234abcd1234abcd1234",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "production",
|
||||
updatedAtMs: 3,
|
||||
});
|
||||
await expect(loadApnsRegistration("ios-node-fallback", baseDir)).resolves.toMatchObject({
|
||||
nodeId: "ios-node-fallback",
|
||||
transport: "direct",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "sandbox",
|
||||
updatedAtMs: 2,
|
||||
});
|
||||
await expect(loadApnsRegistration("ios-node-bad-relay", baseDir)).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("falls back cleanly for malformed or missing registration state", async () => {
|
||||
const baseDir = await makeTempDir();
|
||||
const statePath = path.join(baseDir, "push", "apns-registrations.json");
|
||||
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
||||
await fs.writeFile(statePath, "[]", "utf8");
|
||||
|
||||
await expect(loadApnsRegistration("ios-node-missing", baseDir)).resolves.toBeNull();
|
||||
await expect(loadApnsRegistration(" ", baseDir)).resolves.toBeNull();
|
||||
await expect(clearApnsRegistration(" ", baseDir)).resolves.toBe(false);
|
||||
await expect(clearApnsRegistration("ios-node-missing", baseDir)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("rejects invalid direct and relay registration inputs", async () => {
|
||||
const baseDir = await makeTempDir();
|
||||
const oversized = "x".repeat(257);
|
||||
|
||||
await expect(
|
||||
registerApnsToken({
|
||||
nodeId: "ios-node-1",
|
||||
token: "not-a-token",
|
||||
topic: "ai.openclaw.ios",
|
||||
baseDir,
|
||||
}),
|
||||
).rejects.toThrow("invalid APNs token");
|
||||
await expect(
|
||||
registerApnsToken({
|
||||
nodeId: "n".repeat(257),
|
||||
token: "ABCD1234ABCD1234ABCD1234ABCD1234",
|
||||
topic: "ai.openclaw.ios",
|
||||
baseDir,
|
||||
}),
|
||||
).rejects.toThrow("nodeId required");
|
||||
await expect(
|
||||
registerApnsToken({
|
||||
nodeId: "ios-node-1",
|
||||
token: "A".repeat(513),
|
||||
topic: "ai.openclaw.ios",
|
||||
baseDir,
|
||||
}),
|
||||
).rejects.toThrow("invalid APNs token");
|
||||
await expect(
|
||||
registerApnsToken({
|
||||
nodeId: "ios-node-1",
|
||||
token: "ABCD1234ABCD1234ABCD1234ABCD1234",
|
||||
topic: "a".repeat(256),
|
||||
baseDir,
|
||||
}),
|
||||
).rejects.toThrow("topic required");
|
||||
await expect(
|
||||
registerApnsRegistration({
|
||||
nodeId: "ios-node-relay",
|
||||
transport: "relay",
|
||||
relayHandle: "relay-handle-123",
|
||||
sendGrant: "send-grant-123",
|
||||
installationId: "install-123",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "staging",
|
||||
distribution: "official",
|
||||
baseDir,
|
||||
}),
|
||||
).rejects.toThrow("relay registrations must use production environment");
|
||||
await expect(
|
||||
registerApnsRegistration({
|
||||
nodeId: "ios-node-relay",
|
||||
transport: "relay",
|
||||
relayHandle: "relay-handle-123",
|
||||
sendGrant: "send-grant-123",
|
||||
installationId: "install-123",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "production",
|
||||
distribution: "beta",
|
||||
baseDir,
|
||||
}),
|
||||
).rejects.toThrow("relay registrations must use official distribution");
|
||||
await expect(
|
||||
registerApnsRegistration({
|
||||
nodeId: "ios-node-relay",
|
||||
transport: "relay",
|
||||
relayHandle: oversized,
|
||||
sendGrant: "send-grant-123",
|
||||
installationId: "install-123",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "production",
|
||||
distribution: "official",
|
||||
baseDir,
|
||||
}),
|
||||
).rejects.toThrow("relayHandle too long");
|
||||
await expect(
|
||||
registerApnsRegistration({
|
||||
nodeId: "ios-node-relay",
|
||||
transport: "relay",
|
||||
relayHandle: "relay-handle-123",
|
||||
sendGrant: "send-grant-123",
|
||||
installationId: oversized,
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "production",
|
||||
distribution: "official",
|
||||
baseDir,
|
||||
}),
|
||||
).rejects.toThrow("installationId too long");
|
||||
await expect(
|
||||
registerApnsRegistration({
|
||||
nodeId: "ios-node-relay",
|
||||
transport: "relay",
|
||||
relayHandle: "relay-handle-123",
|
||||
sendGrant: "x".repeat(1025),
|
||||
installationId: "install-123",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "production",
|
||||
distribution: "official",
|
||||
baseDir,
|
||||
}),
|
||||
).rejects.toThrow("sendGrant too long");
|
||||
});
|
||||
|
||||
it("persists with a trailing newline and clears registrations", async () => {
|
||||
const baseDir = await makeTempDir();
|
||||
await registerApnsToken({
|
||||
nodeId: "ios-node-1",
|
||||
token: "ABCD1234ABCD1234ABCD1234ABCD1234",
|
||||
topic: "ai.openclaw.ios",
|
||||
baseDir,
|
||||
});
|
||||
|
||||
const statePath = path.join(baseDir, "push", "apns-registrations.json");
|
||||
await expect(fs.readFile(statePath, "utf8")).resolves.toMatch(/\n$/);
|
||||
await expect(clearApnsRegistration("ios-node-1", baseDir)).resolves.toBe(true);
|
||||
await expect(loadApnsRegistration("ios-node-1", baseDir)).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("only clears a registration when the stored entry still matches", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const baseDir = await makeTempDir();
|
||||
vi.setSystemTime(new Date("2026-03-11T00:00:00Z"));
|
||||
const stale = await registerApnsToken({
|
||||
nodeId: "ios-node-1",
|
||||
token: "ABCD1234ABCD1234ABCD1234ABCD1234",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "sandbox",
|
||||
baseDir,
|
||||
});
|
||||
|
||||
vi.setSystemTime(new Date("2026-03-11T00:00:01Z"));
|
||||
const fresh = await registerApnsToken({
|
||||
nodeId: "ios-node-1",
|
||||
token: "ABCD1234ABCD1234ABCD1234ABCD1234",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "sandbox",
|
||||
baseDir,
|
||||
});
|
||||
|
||||
await expect(
|
||||
clearApnsRegistrationIfCurrent({
|
||||
nodeId: "ios-node-1",
|
||||
registration: stale,
|
||||
baseDir,
|
||||
}),
|
||||
).resolves.toBe(false);
|
||||
await expect(loadApnsRegistration("ios-node-1", baseDir)).resolves.toEqual(fresh);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,27 +1,10 @@
|
|||
import { generateKeyPairSync } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
clearApnsRegistration,
|
||||
clearApnsRegistrationIfCurrent,
|
||||
loadApnsRegistration,
|
||||
registerApnsRegistration,
|
||||
registerApnsToken,
|
||||
sendApnsAlert,
|
||||
sendApnsBackgroundWake,
|
||||
} from "./push-apns.js";
|
||||
import { sendApnsAlert, sendApnsBackgroundWake } from "./push-apns.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
const testAuthPrivateKey = generateKeyPairSync("ec", { namedCurve: "prime256v1" })
|
||||
.privateKey.export({ format: "pem", type: "pkcs8" })
|
||||
.toString();
|
||||
async function makeTempDir(): Promise<string> {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-push-apns-test-"));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
function createDirectApnsSendFixture(params: {
|
||||
nodeId: string;
|
||||
|
|
@ -48,229 +31,6 @@ function createDirectApnsSendFixture(params: {
|
|||
|
||||
afterEach(async () => {
|
||||
vi.unstubAllGlobals();
|
||||
while (tempDirs.length > 0) {
|
||||
const dir = tempDirs.pop();
|
||||
if (dir) {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("push APNs registration store", () => {
|
||||
it("stores and reloads node APNs registration", async () => {
|
||||
const baseDir = await makeTempDir();
|
||||
const saved = await registerApnsToken({
|
||||
nodeId: "ios-node-1",
|
||||
token: "ABCD1234ABCD1234ABCD1234ABCD1234",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "sandbox",
|
||||
baseDir,
|
||||
});
|
||||
|
||||
const loaded = await loadApnsRegistration("ios-node-1", baseDir);
|
||||
expect(loaded).not.toBeNull();
|
||||
expect(loaded?.nodeId).toBe("ios-node-1");
|
||||
expect(loaded?.transport).toBe("direct");
|
||||
expect(loaded && loaded.transport === "direct" ? loaded.token : null).toBe(
|
||||
"abcd1234abcd1234abcd1234abcd1234",
|
||||
);
|
||||
expect(loaded?.topic).toBe("ai.openclaw.ios");
|
||||
expect(loaded?.environment).toBe("sandbox");
|
||||
expect(loaded?.updatedAtMs).toBe(saved.updatedAtMs);
|
||||
});
|
||||
|
||||
it("stores and reloads relay-backed APNs registrations without a raw token", async () => {
|
||||
const baseDir = await makeTempDir();
|
||||
const saved = await registerApnsRegistration({
|
||||
nodeId: "ios-node-relay",
|
||||
transport: "relay",
|
||||
relayHandle: "relay-handle-123",
|
||||
sendGrant: "send-grant-123",
|
||||
installationId: "install-123",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "production",
|
||||
distribution: "official",
|
||||
tokenDebugSuffix: "abcd1234",
|
||||
baseDir,
|
||||
});
|
||||
|
||||
const loaded = await loadApnsRegistration("ios-node-relay", baseDir);
|
||||
expect(saved.transport).toBe("relay");
|
||||
expect(loaded).toMatchObject({
|
||||
nodeId: "ios-node-relay",
|
||||
transport: "relay",
|
||||
relayHandle: "relay-handle-123",
|
||||
sendGrant: "send-grant-123",
|
||||
installationId: "install-123",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "production",
|
||||
distribution: "official",
|
||||
tokenDebugSuffix: "abcd1234",
|
||||
});
|
||||
expect(loaded && "token" in loaded).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects invalid APNs tokens", async () => {
|
||||
const baseDir = await makeTempDir();
|
||||
await expect(
|
||||
registerApnsToken({
|
||||
nodeId: "ios-node-1",
|
||||
token: "not-a-token",
|
||||
topic: "ai.openclaw.ios",
|
||||
baseDir,
|
||||
}),
|
||||
).rejects.toThrow("invalid APNs token");
|
||||
});
|
||||
|
||||
it("rejects oversized direct APNs registration fields", async () => {
|
||||
const baseDir = await makeTempDir();
|
||||
await expect(
|
||||
registerApnsToken({
|
||||
nodeId: "n".repeat(257),
|
||||
token: "ABCD1234ABCD1234ABCD1234ABCD1234",
|
||||
topic: "ai.openclaw.ios",
|
||||
baseDir,
|
||||
}),
|
||||
).rejects.toThrow("nodeId required");
|
||||
await expect(
|
||||
registerApnsToken({
|
||||
nodeId: "ios-node-1",
|
||||
token: "A".repeat(513),
|
||||
topic: "ai.openclaw.ios",
|
||||
baseDir,
|
||||
}),
|
||||
).rejects.toThrow("invalid APNs token");
|
||||
await expect(
|
||||
registerApnsToken({
|
||||
nodeId: "ios-node-1",
|
||||
token: "ABCD1234ABCD1234ABCD1234ABCD1234",
|
||||
topic: "a".repeat(256),
|
||||
baseDir,
|
||||
}),
|
||||
).rejects.toThrow("topic required");
|
||||
});
|
||||
|
||||
it("rejects relay registrations that do not use production/official values", async () => {
|
||||
const baseDir = await makeTempDir();
|
||||
await expect(
|
||||
registerApnsRegistration({
|
||||
nodeId: "ios-node-relay",
|
||||
transport: "relay",
|
||||
relayHandle: "relay-handle-123",
|
||||
sendGrant: "send-grant-123",
|
||||
installationId: "install-123",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "staging",
|
||||
distribution: "official",
|
||||
baseDir,
|
||||
}),
|
||||
).rejects.toThrow("relay registrations must use production environment");
|
||||
await expect(
|
||||
registerApnsRegistration({
|
||||
nodeId: "ios-node-relay",
|
||||
transport: "relay",
|
||||
relayHandle: "relay-handle-123",
|
||||
sendGrant: "send-grant-123",
|
||||
installationId: "install-123",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "production",
|
||||
distribution: "beta",
|
||||
baseDir,
|
||||
}),
|
||||
).rejects.toThrow("relay registrations must use official distribution");
|
||||
});
|
||||
|
||||
it("rejects oversized relay registration identifiers", async () => {
|
||||
const baseDir = await makeTempDir();
|
||||
const oversized = "x".repeat(257);
|
||||
await expect(
|
||||
registerApnsRegistration({
|
||||
nodeId: "ios-node-relay",
|
||||
transport: "relay",
|
||||
relayHandle: oversized,
|
||||
sendGrant: "send-grant-123",
|
||||
installationId: "install-123",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "production",
|
||||
distribution: "official",
|
||||
baseDir,
|
||||
}),
|
||||
).rejects.toThrow("relayHandle too long");
|
||||
await expect(
|
||||
registerApnsRegistration({
|
||||
nodeId: "ios-node-relay",
|
||||
transport: "relay",
|
||||
relayHandle: "relay-handle-123",
|
||||
sendGrant: "send-grant-123",
|
||||
installationId: oversized,
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "production",
|
||||
distribution: "official",
|
||||
baseDir,
|
||||
}),
|
||||
).rejects.toThrow("installationId too long");
|
||||
await expect(
|
||||
registerApnsRegistration({
|
||||
nodeId: "ios-node-relay",
|
||||
transport: "relay",
|
||||
relayHandle: "relay-handle-123",
|
||||
sendGrant: "x".repeat(1025),
|
||||
installationId: "install-123",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "production",
|
||||
distribution: "official",
|
||||
baseDir,
|
||||
}),
|
||||
).rejects.toThrow("sendGrant too long");
|
||||
});
|
||||
|
||||
it("clears registrations", async () => {
|
||||
const baseDir = await makeTempDir();
|
||||
await registerApnsToken({
|
||||
nodeId: "ios-node-1",
|
||||
token: "ABCD1234ABCD1234ABCD1234ABCD1234",
|
||||
topic: "ai.openclaw.ios",
|
||||
baseDir,
|
||||
});
|
||||
|
||||
await expect(clearApnsRegistration("ios-node-1", baseDir)).resolves.toBe(true);
|
||||
await expect(loadApnsRegistration("ios-node-1", baseDir)).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("only clears a registration when the stored entry still matches", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const baseDir = await makeTempDir();
|
||||
vi.setSystemTime(new Date("2026-03-11T00:00:00Z"));
|
||||
const stale = await registerApnsToken({
|
||||
nodeId: "ios-node-1",
|
||||
token: "ABCD1234ABCD1234ABCD1234ABCD1234",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "sandbox",
|
||||
baseDir,
|
||||
});
|
||||
|
||||
vi.setSystemTime(new Date("2026-03-11T00:00:01Z"));
|
||||
const fresh = await registerApnsToken({
|
||||
nodeId: "ios-node-1",
|
||||
token: "ABCD1234ABCD1234ABCD1234ABCD1234",
|
||||
topic: "ai.openclaw.ios",
|
||||
environment: "sandbox",
|
||||
baseDir,
|
||||
});
|
||||
|
||||
await expect(
|
||||
clearApnsRegistrationIfCurrent({
|
||||
nodeId: "ios-node-1",
|
||||
registration: stale,
|
||||
baseDir,
|
||||
}),
|
||||
).resolves.toBe(false);
|
||||
await expect(loadApnsRegistration("ios-node-1", baseDir)).resolves.toEqual(fresh);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("push APNs send semantics", () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue