test(voice-call): cover twilio and reaper helpers

This commit is contained in:
Vincent Koc
2026-03-22 19:49:21 -07:00
parent 32fdd21c80
commit f1bff0b9d6
2 changed files with 168 additions and 0 deletions

View 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");
});
});

View 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?.();
});
});