/**
* Tests for environment configuration validation
* Tests Zod schema validation for all environment variables
*/
import { z } from 'zod';
// Note: Zod's coerce.boolean() converts any truthy value to true and falsy to false
// This means "false" (a non-empty string) is truthy and becomes true
// To handle "false" string properly, we'd need a custom transformer
// These tests verify the actual behavior of the schema in env.ts
const envSchema = z.object({
TWILIO_ACCOUNT_SID: z.string().startsWith('AC', 'Account SID must start with AC'),
TWILIO_AUTH_TOKEN: z.string().min(32, 'Auth token must be at least 32 characters'),
TWILIO_PHONE_NUMBER: z.string().regex(
/^\+[1-9]\d{1,14}$/,
'Phone number must be in E.164 format (+1234567890)'
),
WEBHOOK_PORT: z.coerce.number().min(1).max(65535).default(3000),
WEBHOOK_BASE_URL: z.string().url('Webhook base URL must be a valid URL'),
DATABASE_PATH: z.string().default('./data/twilio.db'),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
AUTO_CREATE_CONVERSATIONS: z.coerce.boolean().default(true),
ENABLE_AI_CONTEXT: z.coerce.boolean().default(true),
ENABLE_MMS: z.coerce.boolean().default(true),
});
describe('Environment Configuration Schema', () => {
const validEnv = {
TWILIO_ACCOUNT_SID: 'ACtest12345678901234567890123456',
TWILIO_AUTH_TOKEN: 'test_auth_token_12345678901234567890',
TWILIO_PHONE_NUMBER: '+15551234567',
WEBHOOK_PORT: '3000',
WEBHOOK_BASE_URL: 'https://test.example.com',
DATABASE_PATH: './data/twilio.db',
LOG_LEVEL: 'info',
AUTO_CREATE_CONVERSATIONS: 'true',
ENABLE_AI_CONTEXT: 'true',
ENABLE_MMS: 'true',
};
describe('TWILIO_ACCOUNT_SID', () => {
it('should accept valid Account SID starting with AC', () => {
const result = envSchema.safeParse(validEnv);
expect(result.success).toBe(true);
});
it('should reject Account SID not starting with AC', () => {
const result = envSchema.safeParse({
...validEnv,
TWILIO_ACCOUNT_SID: 'XXtest12345678901234567890123456',
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.errors[0].message).toContain('Account SID must start with AC');
}
});
it('should reject empty Account SID', () => {
const result = envSchema.safeParse({
...validEnv,
TWILIO_ACCOUNT_SID: '',
});
expect(result.success).toBe(false);
});
});
describe('TWILIO_AUTH_TOKEN', () => {
it('should accept valid auth token with 32+ characters', () => {
const result = envSchema.safeParse(validEnv);
expect(result.success).toBe(true);
});
it('should reject auth token shorter than 32 characters', () => {
const result = envSchema.safeParse({
...validEnv,
TWILIO_AUTH_TOKEN: 'short_token',
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.errors[0].message).toContain('at least 32 characters');
}
});
});
describe('TWILIO_PHONE_NUMBER', () => {
it('should accept valid E.164 format phone number', () => {
const result = envSchema.safeParse(validEnv);
expect(result.success).toBe(true);
});
it('should accept international phone numbers', () => {
const result = envSchema.safeParse({
...validEnv,
TWILIO_PHONE_NUMBER: '+447911123456',
});
expect(result.success).toBe(true);
});
it('should reject phone number without + prefix', () => {
const result = envSchema.safeParse({
...validEnv,
TWILIO_PHONE_NUMBER: '15551234567',
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.errors[0].message).toContain('E.164 format');
}
});
it('should reject phone number starting with +0', () => {
const result = envSchema.safeParse({
...validEnv,
TWILIO_PHONE_NUMBER: '+05551234567',
});
expect(result.success).toBe(false);
});
it('should reject phone number with non-digit characters', () => {
const result = envSchema.safeParse({
...validEnv,
TWILIO_PHONE_NUMBER: '+1555-123-4567',
});
expect(result.success).toBe(false);
});
});
describe('WEBHOOK_PORT', () => {
it('should accept valid port number', () => {
const result = envSchema.parse(validEnv);
expect(result.WEBHOOK_PORT).toBe(3000);
});
it('should coerce string port to number', () => {
const result = envSchema.parse({
...validEnv,
WEBHOOK_PORT: '8080',
});
expect(result.WEBHOOK_PORT).toBe(8080);
});
it('should default to 3000 when not provided', () => {
const { WEBHOOK_PORT, ...envWithoutPort } = validEnv;
const result = envSchema.parse(envWithoutPort);
expect(result.WEBHOOK_PORT).toBe(3000);
});
it('should reject port below 1', () => {
const result = envSchema.safeParse({
...validEnv,
WEBHOOK_PORT: '0',
});
expect(result.success).toBe(false);
});
it('should reject port above 65535', () => {
const result = envSchema.safeParse({
...validEnv,
WEBHOOK_PORT: '70000',
});
expect(result.success).toBe(false);
});
});
describe('WEBHOOK_BASE_URL', () => {
it('should accept valid HTTPS URL', () => {
const result = envSchema.safeParse(validEnv);
expect(result.success).toBe(true);
});
it('should accept valid HTTP URL', () => {
const result = envSchema.safeParse({
...validEnv,
WEBHOOK_BASE_URL: 'http://localhost:3000',
});
expect(result.success).toBe(true);
});
it('should reject invalid URL', () => {
const result = envSchema.safeParse({
...validEnv,
WEBHOOK_BASE_URL: 'not-a-url',
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.errors[0].message).toContain('valid URL');
}
});
});
describe('DATABASE_PATH', () => {
it('should accept custom database path', () => {
const result = envSchema.parse({
...validEnv,
DATABASE_PATH: '/custom/path/db.sqlite',
});
expect(result.DATABASE_PATH).toBe('/custom/path/db.sqlite');
});
it('should default to ./data/twilio.db', () => {
const { DATABASE_PATH, ...envWithoutPath } = validEnv;
const result = envSchema.parse(envWithoutPath);
expect(result.DATABASE_PATH).toBe('./data/twilio.db');
});
});
describe('LOG_LEVEL', () => {
it('should accept valid log levels', () => {
const levels = ['debug', 'info', 'warn', 'error'];
for (const level of levels) {
const result = envSchema.safeParse({
...validEnv,
LOG_LEVEL: level,
});
expect(result.success).toBe(true);
}
});
it('should reject invalid log level', () => {
const result = envSchema.safeParse({
...validEnv,
LOG_LEVEL: 'verbose',
});
expect(result.success).toBe(false);
});
it('should default to info', () => {
const { LOG_LEVEL, ...envWithoutLevel } = validEnv;
const result = envSchema.parse(envWithoutLevel);
expect(result.LOG_LEVEL).toBe('info');
});
});
describe('AUTO_CREATE_CONVERSATIONS', () => {
it('should coerce string "true" to boolean true', () => {
const result = envSchema.parse(validEnv);
expect(result.AUTO_CREATE_CONVERSATIONS).toBe(true);
});
// Note: Zod's coerce.boolean() treats any truthy value (including non-empty strings like "false") as true
// This is the actual behavior of the schema - "false" string is truthy
it('should coerce non-empty string to true (Zod coerce behavior)', () => {
const result = envSchema.parse({
...validEnv,
AUTO_CREATE_CONVERSATIONS: 'false',
});
// "false" is a non-empty string, so it's truthy and coerces to true
expect(result.AUTO_CREATE_CONVERSATIONS).toBe(true);
});
it('should coerce empty string to false', () => {
const result = envSchema.parse({
...validEnv,
AUTO_CREATE_CONVERSATIONS: '',
});
expect(result.AUTO_CREATE_CONVERSATIONS).toBe(false);
});
it('should default to true', () => {
const { AUTO_CREATE_CONVERSATIONS, ...envWithout } = validEnv;
const result = envSchema.parse(envWithout);
expect(result.AUTO_CREATE_CONVERSATIONS).toBe(true);
});
});
describe('ENABLE_AI_CONTEXT', () => {
it('should coerce truthy value to true', () => {
const result = envSchema.parse({
...validEnv,
ENABLE_AI_CONTEXT: '1',
});
expect(result.ENABLE_AI_CONTEXT).toBe(true);
});
it('should coerce empty string to false', () => {
const result = envSchema.parse({
...validEnv,
ENABLE_AI_CONTEXT: '',
});
expect(result.ENABLE_AI_CONTEXT).toBe(false);
});
it('should default to true', () => {
const { ENABLE_AI_CONTEXT, ...envWithout } = validEnv;
const result = envSchema.parse(envWithout);
expect(result.ENABLE_AI_CONTEXT).toBe(true);
});
});
describe('ENABLE_MMS', () => {
it('should coerce truthy value to true', () => {
const result = envSchema.parse({
...validEnv,
ENABLE_MMS: 'yes',
});
expect(result.ENABLE_MMS).toBe(true);
});
it('should coerce empty string to false', () => {
const result = envSchema.parse({
...validEnv,
ENABLE_MMS: '',
});
expect(result.ENABLE_MMS).toBe(false);
});
it('should default to true', () => {
const { ENABLE_MMS, ...envWithout } = validEnv;
const result = envSchema.parse(envWithout);
expect(result.ENABLE_MMS).toBe(true);
});
});
describe('Missing required fields', () => {
it('should fail when TWILIO_ACCOUNT_SID is missing', () => {
const { TWILIO_ACCOUNT_SID, ...envWithout } = validEnv;
const result = envSchema.safeParse(envWithout);
expect(result.success).toBe(false);
});
it('should fail when TWILIO_AUTH_TOKEN is missing', () => {
const { TWILIO_AUTH_TOKEN, ...envWithout } = validEnv;
const result = envSchema.safeParse(envWithout);
expect(result.success).toBe(false);
});
it('should fail when TWILIO_PHONE_NUMBER is missing', () => {
const { TWILIO_PHONE_NUMBER, ...envWithout } = validEnv;
const result = envSchema.safeParse(envWithout);
expect(result.success).toBe(false);
});
it('should fail when WEBHOOK_BASE_URL is missing', () => {
const { WEBHOOK_BASE_URL, ...envWithout } = validEnv;
const result = envSchema.safeParse(envWithout);
expect(result.success).toBe(false);
});
});
});