mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 04:20:43 +00:00
test(voice-call): cover twilio and reaper helpers
This commit is contained in:
85
extensions/voice-call/src/providers/twilio/api.test.ts
Normal file
85
extensions/voice-call/src/providers/twilio/api.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { twilioApiRequest } from "./api.js";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
describe("twilioApiRequest", () => {
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it("posts form bodies with basic auth and parses json", async () => {
|
||||
globalThis.fetch = vi.fn(async () => {
|
||||
return new Response(JSON.stringify({ sid: "CA123" }), { status: 200 });
|
||||
}) as typeof fetch;
|
||||
|
||||
await expect(
|
||||
twilioApiRequest({
|
||||
baseUrl: "https://api.twilio.com",
|
||||
accountSid: "AC123",
|
||||
authToken: "secret",
|
||||
endpoint: "/Calls.json",
|
||||
body: {
|
||||
To: "+14155550123",
|
||||
StatusCallbackEvent: ["initiated", "completed"],
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual({ sid: "CA123" });
|
||||
|
||||
const [url, init] = vi.mocked(globalThis.fetch).mock.calls[0] ?? [];
|
||||
expect(url).toBe("https://api.twilio.com/Calls.json");
|
||||
expect(init).toEqual(
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from("AC123:secret").toString("base64")}`,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(String(init?.body)).toBe("To=%2B14155550123&StatusCallbackEvent=initiated&StatusCallbackEvent=completed");
|
||||
});
|
||||
|
||||
it("passes through URLSearchParams, allows 404s, and returns undefined for empty bodies", async () => {
|
||||
const responses = [
|
||||
new Response(null, { status: 204 }),
|
||||
new Response("missing", { status: 404 }),
|
||||
];
|
||||
globalThis.fetch = vi.fn(async () => responses.shift()!) as typeof fetch;
|
||||
|
||||
await expect(
|
||||
twilioApiRequest({
|
||||
baseUrl: "https://api.twilio.com",
|
||||
accountSid: "AC123",
|
||||
authToken: "secret",
|
||||
endpoint: "/Calls.json",
|
||||
body: new URLSearchParams({ To: "+14155550123" }),
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
await expect(
|
||||
twilioApiRequest({
|
||||
baseUrl: "https://api.twilio.com",
|
||||
accountSid: "AC123",
|
||||
authToken: "secret",
|
||||
endpoint: "/Calls/missing.json",
|
||||
body: {},
|
||||
allowNotFound: true,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("throws twilio api errors for non-ok responses", async () => {
|
||||
globalThis.fetch = vi.fn(async () => new Response("bad request", { status: 400 })) as typeof fetch;
|
||||
|
||||
await expect(
|
||||
twilioApiRequest({
|
||||
baseUrl: "https://api.twilio.com",
|
||||
accountSid: "AC123",
|
||||
authToken: "secret",
|
||||
endpoint: "/Calls.json",
|
||||
body: {},
|
||||
}),
|
||||
).rejects.toThrow("Twilio API error: 400 bad request");
|
||||
});
|
||||
});
|
||||
83
extensions/voice-call/src/webhook/stale-call-reaper.test.ts
Normal file
83
extensions/voice-call/src/webhook/stale-call-reaper.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { startStaleCallReaper } from "./stale-call-reaper.js";
|
||||
|
||||
describe("startStaleCallReaper", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-03-22T12:00:00.000Z"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("returns null when disabled or non-positive", () => {
|
||||
const manager = {
|
||||
getActiveCalls: vi.fn(() => []),
|
||||
endCall: vi.fn(),
|
||||
};
|
||||
|
||||
expect(startStaleCallReaper({ manager: manager as never })).toBeNull();
|
||||
expect(startStaleCallReaper({ manager: manager as never, staleCallReaperSeconds: 0 })).toBeNull();
|
||||
});
|
||||
|
||||
it("reaps stale calls and ignores fresh ones", async () => {
|
||||
const endCall = vi.fn(async () => {});
|
||||
const manager = {
|
||||
getActiveCalls: vi.fn(() => [
|
||||
{
|
||||
callId: "call-stale",
|
||||
startedAt: Date.now() - 61_000,
|
||||
state: "active",
|
||||
},
|
||||
{
|
||||
callId: "call-fresh",
|
||||
startedAt: Date.now() - 10_000,
|
||||
state: "active",
|
||||
},
|
||||
]),
|
||||
endCall,
|
||||
};
|
||||
|
||||
const stop = startStaleCallReaper({
|
||||
manager: manager as never,
|
||||
staleCallReaperSeconds: 60,
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(30_000);
|
||||
|
||||
expect(endCall).toHaveBeenCalledTimes(1);
|
||||
expect(endCall).toHaveBeenCalledWith("call-stale");
|
||||
|
||||
stop?.();
|
||||
});
|
||||
|
||||
it("logs and swallows endCall failures", async () => {
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const manager = {
|
||||
getActiveCalls: vi.fn(() => [
|
||||
{
|
||||
callId: "call-stale",
|
||||
startedAt: Date.now() - 61_000,
|
||||
state: "active",
|
||||
},
|
||||
]),
|
||||
endCall: vi.fn(async () => {
|
||||
throw new Error("network");
|
||||
}),
|
||||
};
|
||||
|
||||
const stop = startStaleCallReaper({
|
||||
manager: manager as never,
|
||||
staleCallReaperSeconds: 60,
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(30_000);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(warn).toHaveBeenCalledWith("[voice-call] Reaper failed to end call call-stale:", expect.any(Error));
|
||||
|
||||
stop?.();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user