diff --git a/extensions/voice-call/src/config.test.ts b/extensions/voice-call/src/config.test.ts new file mode 100644 index 000000000..3a4311c8a --- /dev/null +++ b/extensions/voice-call/src/config.test.ts @@ -0,0 +1,196 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { validateProviderConfig, type VoiceCallConfig } from "./config.js"; + +function createBaseConfig( + provider: "telnyx" | "twilio" | "plivo" | "mock", +): VoiceCallConfig { + return { + enabled: true, + provider, + fromNumber: "+15550001234", + inboundPolicy: "disabled", + allowFrom: [], + outbound: { defaultMode: "notify", notifyHangupDelaySec: 3 }, + maxDurationSeconds: 300, + silenceTimeoutMs: 800, + transcriptTimeoutMs: 180000, + ringTimeoutMs: 30000, + maxConcurrentCalls: 1, + serve: { port: 3334, bind: "127.0.0.1", path: "/voice/webhook" }, + tailscale: { mode: "off", path: "/voice/webhook" }, + tunnel: { provider: "none", allowNgrokFreeTier: true }, + streaming: { + enabled: false, + sttProvider: "openai-realtime", + sttModel: "gpt-4o-transcribe", + silenceDurationMs: 800, + vadThreshold: 0.5, + streamPath: "/voice/stream", + }, + skipSignatureVerification: false, + stt: { provider: "openai", model: "whisper-1" }, + tts: { provider: "openai", model: "gpt-4o-mini-tts", voice: "coral" }, + responseModel: "openai/gpt-4o-mini", + responseTimeoutMs: 30000, + }; +} + +describe("validateProviderConfig", () => { + const originalEnv = { ...process.env }; + + beforeEach(() => { + // Clear all relevant env vars before each test + 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.PLIVO_AUTH_ID; + delete process.env.PLIVO_AUTH_TOKEN; + }); + + afterEach(() => { + // Restore original env + process.env = { ...originalEnv }; + }); + + describe("twilio provider", () => { + it("passes validation when credentials are in config", () => { + const config = createBaseConfig("twilio"); + config.twilio = { accountSid: "AC123", authToken: "secret" }; + + const result = validateProviderConfig(config); + + expect(result.valid).toBe(true); + expect(result.errors).toEqual([]); + }); + + it("passes validation when credentials are in environment variables", () => { + process.env.TWILIO_ACCOUNT_SID = "AC123"; + process.env.TWILIO_AUTH_TOKEN = "secret"; + const config = createBaseConfig("twilio"); + + const result = validateProviderConfig(config); + + expect(result.valid).toBe(true); + expect(result.errors).toEqual([]); + }); + + it("passes validation with mixed config and env vars", () => { + process.env.TWILIO_AUTH_TOKEN = "secret"; + const config = createBaseConfig("twilio"); + config.twilio = { accountSid: "AC123" }; + + const result = validateProviderConfig(config); + + expect(result.valid).toBe(true); + expect(result.errors).toEqual([]); + }); + + it("fails validation when accountSid is missing everywhere", () => { + process.env.TWILIO_AUTH_TOKEN = "secret"; + const config = createBaseConfig("twilio"); + + const result = validateProviderConfig(config); + + expect(result.valid).toBe(false); + expect(result.errors).toContain( + "plugins.entries.voice-call.config.twilio.accountSid is required (or set TWILIO_ACCOUNT_SID env)", + ); + }); + + it("fails validation when authToken is missing everywhere", () => { + process.env.TWILIO_ACCOUNT_SID = "AC123"; + const config = createBaseConfig("twilio"); + + const result = validateProviderConfig(config); + + expect(result.valid).toBe(false); + expect(result.errors).toContain( + "plugins.entries.voice-call.config.twilio.authToken is required (or set TWILIO_AUTH_TOKEN env)", + ); + }); + }); + + describe("telnyx provider", () => { + it("passes validation when credentials are in config", () => { + const config = createBaseConfig("telnyx"); + config.telnyx = { apiKey: "KEY123", connectionId: "CONN456" }; + + const result = validateProviderConfig(config); + + expect(result.valid).toBe(true); + expect(result.errors).toEqual([]); + }); + + it("passes validation when credentials are in environment variables", () => { + process.env.TELNYX_API_KEY = "KEY123"; + process.env.TELNYX_CONNECTION_ID = "CONN456"; + const config = createBaseConfig("telnyx"); + + const result = validateProviderConfig(config); + + expect(result.valid).toBe(true); + expect(result.errors).toEqual([]); + }); + + it("fails validation when apiKey is missing everywhere", () => { + process.env.TELNYX_CONNECTION_ID = "CONN456"; + const config = createBaseConfig("telnyx"); + + 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)", + ); + }); + }); + + describe("plivo provider", () => { + it("passes validation when credentials are in config", () => { + const config = createBaseConfig("plivo"); + config.plivo = { authId: "MA123", authToken: "secret" }; + + const result = validateProviderConfig(config); + + expect(result.valid).toBe(true); + expect(result.errors).toEqual([]); + }); + + it("passes validation when credentials are in environment variables", () => { + process.env.PLIVO_AUTH_ID = "MA123"; + process.env.PLIVO_AUTH_TOKEN = "secret"; + const config = createBaseConfig("plivo"); + + const result = validateProviderConfig(config); + + expect(result.valid).toBe(true); + expect(result.errors).toEqual([]); + }); + + it("fails validation when authId is missing everywhere", () => { + process.env.PLIVO_AUTH_TOKEN = "secret"; + const config = createBaseConfig("plivo"); + + 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([]); + }); + }); +}); diff --git a/extensions/voice-call/src/config.ts b/extensions/voice-call/src/config.ts index 832e692ca..403a2eb89 100644 --- a/extensions/voice-call/src/config.ts +++ b/extensions/voice-call/src/config.ts @@ -352,12 +352,12 @@ export function validateProviderConfig(config: VoiceCallConfig): { } if (config.provider === "telnyx") { - if (!config.telnyx?.apiKey) { + if (!config.telnyx?.apiKey && !process.env.TELNYX_API_KEY) { errors.push( "plugins.entries.voice-call.config.telnyx.apiKey is required (or set TELNYX_API_KEY env)", ); } - if (!config.telnyx?.connectionId) { + if (!config.telnyx?.connectionId && !process.env.TELNYX_CONNECTION_ID) { errors.push( "plugins.entries.voice-call.config.telnyx.connectionId is required (or set TELNYX_CONNECTION_ID env)", ); @@ -365,12 +365,12 @@ export function validateProviderConfig(config: VoiceCallConfig): { } if (config.provider === "twilio") { - if (!config.twilio?.accountSid) { + if (!config.twilio?.accountSid && !process.env.TWILIO_ACCOUNT_SID) { errors.push( "plugins.entries.voice-call.config.twilio.accountSid is required (or set TWILIO_ACCOUNT_SID env)", ); } - if (!config.twilio?.authToken) { + if (!config.twilio?.authToken && !process.env.TWILIO_AUTH_TOKEN) { errors.push( "plugins.entries.voice-call.config.twilio.authToken is required (or set TWILIO_AUTH_TOKEN env)", ); @@ -378,12 +378,12 @@ export function validateProviderConfig(config: VoiceCallConfig): { } if (config.provider === "plivo") { - if (!config.plivo?.authId) { + if (!config.plivo?.authId && !process.env.PLIVO_AUTH_ID) { errors.push( "plugins.entries.voice-call.config.plivo.authId is required (or set PLIVO_AUTH_ID env)", ); } - if (!config.plivo?.authToken) { + if (!config.plivo?.authToken && !process.env.PLIVO_AUTH_TOKEN) { errors.push( "plugins.entries.voice-call.config.plivo.authToken is required (or set PLIVO_AUTH_TOKEN env)", );