mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
test(voice-call): cover verification key and header helpers
This commit is contained in:
16
extensions/voice-call/src/http-headers.test.ts
Normal file
16
extensions/voice-call/src/http-headers.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getHeader } from "./http-headers.js";
|
||||
|
||||
describe("getHeader", () => {
|
||||
it("returns first value when header is an array", () => {
|
||||
expect(getHeader({ "x-test": ["first", "second"] }, "x-test")).toBe("first");
|
||||
});
|
||||
|
||||
it("matches headers case-insensitively", () => {
|
||||
expect(getHeader({ "X-Twilio-Signature": "sig-1" }, "x-twilio-signature")).toBe("sig-1");
|
||||
});
|
||||
|
||||
it("returns undefined for missing header", () => {
|
||||
expect(getHeader({ host: "example.com" }, "x-missing")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -203,6 +203,22 @@ describe("verifyPlivoWebhook", () => {
|
||||
expect(second.isReplay).toBe(true);
|
||||
expect(second.verifiedRequestKey).toBe(first.verifiedRequestKey);
|
||||
});
|
||||
|
||||
it("returns a stable request key when verification is skipped", () => {
|
||||
const ctx = {
|
||||
headers: {},
|
||||
rawBody: "CallUUID=uuid&CallStatus=in-progress",
|
||||
url: "https://example.com/voice/webhook",
|
||||
method: "POST" as const,
|
||||
};
|
||||
const first = verifyPlivoWebhook(ctx, "token", { skipVerification: true });
|
||||
const second = verifyPlivoWebhook(ctx, "token", { skipVerification: true });
|
||||
|
||||
expect(first.ok).toBe(true);
|
||||
expect(first.verifiedRequestKey).toMatch(/^plivo:skip:/);
|
||||
expect(second.verifiedRequestKey).toBe(first.verifiedRequestKey);
|
||||
expect(second.isReplay).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("verifyTelnyxWebhook", () => {
|
||||
@@ -236,6 +252,22 @@ describe("verifyTelnyxWebhook", () => {
|
||||
expect(second.isReplay).toBe(true);
|
||||
expect(second.verifiedRequestKey).toBe(first.verifiedRequestKey);
|
||||
});
|
||||
|
||||
it("returns a stable request key when verification is skipped", () => {
|
||||
const ctx = {
|
||||
headers: {},
|
||||
rawBody: JSON.stringify({ data: { event_type: "call.initiated" } }),
|
||||
url: "https://example.com/voice/webhook",
|
||||
method: "POST" as const,
|
||||
};
|
||||
const first = verifyTelnyxWebhook(ctx, undefined, { skipVerification: true });
|
||||
const second = verifyTelnyxWebhook(ctx, undefined, { skipVerification: true });
|
||||
|
||||
expect(first.ok).toBe(true);
|
||||
expect(first.verifiedRequestKey).toMatch(/^telnyx:skip:/);
|
||||
expect(second.verifiedRequestKey).toBe(first.verifiedRequestKey);
|
||||
expect(second.isReplay).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("verifyTwilioWebhook", () => {
|
||||
@@ -571,4 +603,20 @@ describe("verifyTwilioWebhook", () => {
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.verificationUrl).toBe("https://legitimate.example.com/voice/webhook");
|
||||
});
|
||||
|
||||
it("returns a stable request key when verification is skipped", () => {
|
||||
const ctx = {
|
||||
headers: {},
|
||||
rawBody: "CallSid=CS123&CallStatus=completed",
|
||||
url: "https://example.com/voice/webhook",
|
||||
method: "POST" as const,
|
||||
};
|
||||
const first = verifyTwilioWebhook(ctx, "token", { skipVerification: true });
|
||||
const second = verifyTwilioWebhook(ctx, "token", { skipVerification: true });
|
||||
|
||||
expect(first.ok).toBe(true);
|
||||
expect(first.verifiedRequestKey).toMatch(/^twilio:skip:/);
|
||||
expect(second.verifiedRequestKey).toBe(first.verifiedRequestKey);
|
||||
expect(second.isReplay).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import { VoiceCallWebhookServer } from "./webhook.js";
|
||||
|
||||
const provider: VoiceCallProvider = {
|
||||
name: "mock",
|
||||
verifyWebhook: () => ({ ok: true }),
|
||||
verifyWebhook: () => ({ ok: true, verifiedRequestKey: "mock:req:base" }),
|
||||
parseWebhookEvent: () => ({ events: [] }),
|
||||
initiateCall: async () => ({ providerCallId: "provider-call", status: "initiated" }),
|
||||
hangupCall: async () => {},
|
||||
@@ -123,7 +123,7 @@ describe("VoiceCallWebhookServer replay handling", () => {
|
||||
it("acknowledges replayed webhook requests and skips event side effects", async () => {
|
||||
const replayProvider: VoiceCallProvider = {
|
||||
...provider,
|
||||
verifyWebhook: () => ({ ok: true, isReplay: true }),
|
||||
verifyWebhook: () => ({ ok: true, isReplay: true, verifiedRequestKey: "mock:req:replay" }),
|
||||
parseWebhookEvent: () => ({
|
||||
events: [
|
||||
{
|
||||
@@ -217,4 +217,37 @@ describe("VoiceCallWebhookServer replay handling", () => {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects requests when verification succeeds without a request key", async () => {
|
||||
const parseWebhookEvent = vi.fn(() => ({ events: [], statusCode: 200 }));
|
||||
const badProvider: VoiceCallProvider = {
|
||||
...provider,
|
||||
verifyWebhook: () => ({ ok: true }),
|
||||
parseWebhookEvent,
|
||||
};
|
||||
const { manager } = createManager([]);
|
||||
const config = createConfig({ serve: { port: 0, bind: "127.0.0.1", path: "/voice/webhook" } });
|
||||
const server = new VoiceCallWebhookServer(config, manager, badProvider);
|
||||
|
||||
try {
|
||||
const baseUrl = await server.start();
|
||||
const address = (
|
||||
server as unknown as { server?: { address?: () => unknown } }
|
||||
).server?.address?.();
|
||||
const requestUrl = new URL(baseUrl);
|
||||
if (address && typeof address === "object" && "port" in address && address.port) {
|
||||
requestUrl.port = String(address.port);
|
||||
}
|
||||
const response = await fetch(requestUrl.toString(), {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/x-www-form-urlencoded" },
|
||||
body: "CallSid=CA123&SpeechResult=hello",
|
||||
});
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(parseWebhookEvent).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user