219 lines
7.5 KiB
TypeScript
219 lines
7.5 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import {
|
|
validateProviderConfig,
|
|
normalizeVoiceCallConfig,
|
|
resolveVoiceCallConfig,
|
|
type VoiceCallConfig,
|
|
} from "./config.js";
|
|
import { createVoiceCallBaseConfig } from "./test-fixtures.js";
|
|
|
|
function createBaseConfig(provider: "telnyx" | "twilio" | "plivo" | "mock"): VoiceCallConfig {
|
|
return createVoiceCallBaseConfig({ provider });
|
|
}
|
|
|
|
describe("validateProviderConfig", () => {
|
|
const originalEnv = { ...process.env };
|
|
const clearProviderEnv = () => {
|
|
delete process.env.TWILIO_ACCOUNT_SID;
|
|
delete process.env.TWILIO_AUTH_TOKEN;
|
|
delete process.env.TELNYX_API_KEY;
|
|
delete process.env.TELNYX_CONNECTION_ID;
|
|
delete process.env.TELNYX_PUBLIC_KEY;
|
|
delete process.env.PLIVO_AUTH_ID;
|
|
delete process.env.PLIVO_AUTH_TOKEN;
|
|
};
|
|
|
|
beforeEach(() => {
|
|
clearProviderEnv();
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Restore original env
|
|
process.env = { ...originalEnv };
|
|
});
|
|
|
|
describe("provider credential sources", () => {
|
|
it("passes validation when credentials come from config or environment", () => {
|
|
for (const provider of ["twilio", "telnyx", "plivo"] as const) {
|
|
clearProviderEnv();
|
|
const fromConfig = createBaseConfig(provider);
|
|
if (provider === "twilio") {
|
|
fromConfig.twilio = { accountSid: "AC123", authToken: "secret" };
|
|
} else if (provider === "telnyx") {
|
|
fromConfig.telnyx = {
|
|
apiKey: "KEY123",
|
|
connectionId: "CONN456",
|
|
publicKey: "public-key",
|
|
};
|
|
} else {
|
|
fromConfig.plivo = { authId: "MA123", authToken: "secret" };
|
|
}
|
|
expect(validateProviderConfig(fromConfig)).toMatchObject({ valid: true, errors: [] });
|
|
|
|
clearProviderEnv();
|
|
if (provider === "twilio") {
|
|
process.env.TWILIO_ACCOUNT_SID = "AC123";
|
|
process.env.TWILIO_AUTH_TOKEN = "secret";
|
|
} else if (provider === "telnyx") {
|
|
process.env.TELNYX_API_KEY = "KEY123";
|
|
process.env.TELNYX_CONNECTION_ID = "CONN456";
|
|
process.env.TELNYX_PUBLIC_KEY = "public-key";
|
|
} else {
|
|
process.env.PLIVO_AUTH_ID = "MA123";
|
|
process.env.PLIVO_AUTH_TOKEN = "secret";
|
|
}
|
|
const fromEnv = resolveVoiceCallConfig(createBaseConfig(provider));
|
|
expect(validateProviderConfig(fromEnv)).toMatchObject({ valid: true, errors: [] });
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("twilio provider", () => {
|
|
it("passes validation with mixed config and env vars", () => {
|
|
process.env.TWILIO_AUTH_TOKEN = "secret";
|
|
let config = createBaseConfig("twilio");
|
|
config.twilio = { accountSid: "AC123" };
|
|
config = resolveVoiceCallConfig(config);
|
|
|
|
const result = validateProviderConfig(config);
|
|
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).toEqual([]);
|
|
});
|
|
|
|
it("fails validation when required twilio credentials are missing", () => {
|
|
process.env.TWILIO_AUTH_TOKEN = "secret";
|
|
const missingSid = validateProviderConfig(resolveVoiceCallConfig(createBaseConfig("twilio")));
|
|
expect(missingSid.valid).toBe(false);
|
|
expect(missingSid.errors).toContain(
|
|
"plugins.entries.voice-call.config.twilio.accountSid is required (or set TWILIO_ACCOUNT_SID env)",
|
|
);
|
|
|
|
delete process.env.TWILIO_AUTH_TOKEN;
|
|
process.env.TWILIO_ACCOUNT_SID = "AC123";
|
|
const missingToken = validateProviderConfig(
|
|
resolveVoiceCallConfig(createBaseConfig("twilio")),
|
|
);
|
|
expect(missingToken.valid).toBe(false);
|
|
expect(missingToken.errors).toContain(
|
|
"plugins.entries.voice-call.config.twilio.authToken is required (or set TWILIO_AUTH_TOKEN env)",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("telnyx provider", () => {
|
|
it("fails validation when apiKey is missing everywhere", () => {
|
|
process.env.TELNYX_CONNECTION_ID = "CONN456";
|
|
let config = createBaseConfig("telnyx");
|
|
config = resolveVoiceCallConfig(config);
|
|
|
|
const result = validateProviderConfig(config);
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain(
|
|
"plugins.entries.voice-call.config.telnyx.apiKey is required (or set TELNYX_API_KEY env)",
|
|
);
|
|
});
|
|
|
|
it("requires a public key unless signature verification is skipped", () => {
|
|
const missingPublicKey = createBaseConfig("telnyx");
|
|
missingPublicKey.inboundPolicy = "allowlist";
|
|
missingPublicKey.telnyx = { apiKey: "KEY123", connectionId: "CONN456" };
|
|
const missingPublicKeyResult = validateProviderConfig(missingPublicKey);
|
|
expect(missingPublicKeyResult.valid).toBe(false);
|
|
expect(missingPublicKeyResult.errors).toContain(
|
|
"plugins.entries.voice-call.config.telnyx.publicKey is required (or set TELNYX_PUBLIC_KEY env)",
|
|
);
|
|
|
|
const withPublicKey = createBaseConfig("telnyx");
|
|
withPublicKey.inboundPolicy = "allowlist";
|
|
withPublicKey.telnyx = {
|
|
apiKey: "KEY123",
|
|
connectionId: "CONN456",
|
|
publicKey: "public-key",
|
|
};
|
|
expect(validateProviderConfig(withPublicKey)).toMatchObject({ valid: true, errors: [] });
|
|
|
|
const skippedVerification = createBaseConfig("telnyx");
|
|
skippedVerification.skipSignatureVerification = true;
|
|
skippedVerification.telnyx = { apiKey: "KEY123", connectionId: "CONN456" };
|
|
expect(validateProviderConfig(skippedVerification)).toMatchObject({
|
|
valid: true,
|
|
errors: [],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("plivo provider", () => {
|
|
it("fails validation when authId is missing everywhere", () => {
|
|
process.env.PLIVO_AUTH_TOKEN = "secret";
|
|
let config = createBaseConfig("plivo");
|
|
config = resolveVoiceCallConfig(config);
|
|
|
|
const result = validateProviderConfig(config);
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain(
|
|
"plugins.entries.voice-call.config.plivo.authId is required (or set PLIVO_AUTH_ID env)",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("disabled config", () => {
|
|
it("skips validation when enabled is false", () => {
|
|
const config = createBaseConfig("twilio");
|
|
config.enabled = false;
|
|
|
|
const result = validateProviderConfig(config);
|
|
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).toEqual([]);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("normalizeVoiceCallConfig", () => {
|
|
it("fills nested runtime defaults from a partial config boundary", () => {
|
|
const normalized = normalizeVoiceCallConfig({
|
|
enabled: true,
|
|
provider: "mock",
|
|
streaming: {
|
|
enabled: true,
|
|
streamPath: "/custom-stream",
|
|
},
|
|
});
|
|
|
|
expect(normalized.serve.path).toBe("/voice/webhook");
|
|
expect(normalized.streaming.streamPath).toBe("/custom-stream");
|
|
expect(normalized.streaming.sttModel).toBe("gpt-4o-transcribe");
|
|
expect(normalized.tunnel.provider).toBe("none");
|
|
expect(normalized.webhookSecurity.allowedHosts).toEqual([]);
|
|
});
|
|
|
|
it("accepts partial nested TTS overrides and preserves nested objects", () => {
|
|
const normalized = normalizeVoiceCallConfig({
|
|
tts: {
|
|
provider: "elevenlabs",
|
|
elevenlabs: {
|
|
apiKey: {
|
|
source: "env",
|
|
provider: "elevenlabs",
|
|
id: "ELEVENLABS_API_KEY",
|
|
},
|
|
voiceSettings: {
|
|
speed: 1.1,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(normalized.tts?.provider).toBe("elevenlabs");
|
|
expect(normalized.tts?.elevenlabs?.apiKey).toEqual({
|
|
source: "env",
|
|
provider: "elevenlabs",
|
|
id: "ELEVENLABS_API_KEY",
|
|
});
|
|
expect(normalized.tts?.elevenlabs?.voiceSettings).toEqual({ speed: 1.1 });
|
|
});
|
|
});
|