Files
flyer-crawler.projectium.com/src/config/env.test.ts
Torben Sorensen a42ee5a461
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 15m11s
unit tests - wheeee! Claude is the mvp
2026-01-09 21:59:09 -08:00

433 lines
12 KiB
TypeScript

// src/config/env.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
describe('env config', () => {
const originalEnv = process.env;
beforeEach(() => {
vi.resetModules();
process.env = { ...originalEnv };
});
afterEach(() => {
process.env = originalEnv;
});
/**
* Sets up minimal valid environment variables for config parsing.
*/
function setValidEnv(overrides: Record<string, string> = {}) {
process.env = {
NODE_ENV: 'test',
// Database (required)
DB_HOST: 'localhost',
DB_PORT: '5432',
DB_USER: 'testuser',
DB_PASSWORD: 'testpass',
DB_NAME: 'testdb',
// Redis (required)
REDIS_URL: 'redis://localhost:6379',
// Auth (required - min 32 chars)
JWT_SECRET: 'this-is-a-test-secret-that-is-at-least-32-characters-long',
...overrides,
};
}
describe('successful config parsing', () => {
it('should parse valid configuration with all required fields', async () => {
setValidEnv();
const { config } = await import('./env');
expect(config.database.host).toBe('localhost');
expect(config.database.port).toBe(5432);
expect(config.database.user).toBe('testuser');
expect(config.database.password).toBe('testpass');
expect(config.database.name).toBe('testdb');
expect(config.redis.url).toBe('redis://localhost:6379');
expect(config.auth.jwtSecret).toBe(
'this-is-a-test-secret-that-is-at-least-32-characters-long',
);
});
it('should use default values for optional fields', async () => {
setValidEnv();
const { config } = await import('./env');
// Worker defaults
expect(config.worker.concurrency).toBe(1);
expect(config.worker.lockDuration).toBe(30000);
expect(config.worker.emailConcurrency).toBe(10);
expect(config.worker.analyticsConcurrency).toBe(1);
expect(config.worker.cleanupConcurrency).toBe(10);
expect(config.worker.weeklyAnalyticsConcurrency).toBe(1);
// Server defaults
expect(config.server.port).toBe(3001);
expect(config.server.nodeEnv).toBe('test');
expect(config.server.storagePath).toBe('/var/www/flyer-crawler.projectium.com/flyer-images');
// AI defaults
expect(config.ai.geminiRpm).toBe(5);
expect(config.ai.priceQualityThreshold).toBe(0.5);
// SMTP defaults
expect(config.smtp.port).toBe(587);
expect(config.smtp.secure).toBe(false);
});
it('should parse custom port values', async () => {
setValidEnv({
DB_PORT: '5433',
PORT: '4000',
SMTP_PORT: '465',
});
const { config } = await import('./env');
expect(config.database.port).toBe(5433);
expect(config.server.port).toBe(4000);
expect(config.smtp.port).toBe(465);
});
it('should parse boolean SMTP_SECURE correctly', async () => {
setValidEnv({
SMTP_SECURE: 'true',
});
const { config } = await import('./env');
expect(config.smtp.secure).toBe(true);
});
it('should parse false for SMTP_SECURE when set to false', async () => {
setValidEnv({
SMTP_SECURE: 'false',
});
const { config } = await import('./env');
expect(config.smtp.secure).toBe(false);
});
it('should parse worker concurrency values', async () => {
setValidEnv({
WORKER_CONCURRENCY: '5',
WORKER_LOCK_DURATION: '60000',
EMAIL_WORKER_CONCURRENCY: '20',
ANALYTICS_WORKER_CONCURRENCY: '3',
CLEANUP_WORKER_CONCURRENCY: '15',
WEEKLY_ANALYTICS_WORKER_CONCURRENCY: '2',
});
const { config } = await import('./env');
expect(config.worker.concurrency).toBe(5);
expect(config.worker.lockDuration).toBe(60000);
expect(config.worker.emailConcurrency).toBe(20);
expect(config.worker.analyticsConcurrency).toBe(3);
expect(config.worker.cleanupConcurrency).toBe(15);
expect(config.worker.weeklyAnalyticsConcurrency).toBe(2);
});
it('should parse AI configuration values', async () => {
setValidEnv({
GEMINI_API_KEY: 'test-gemini-key',
GEMINI_RPM: '10',
AI_PRICE_QUALITY_THRESHOLD: '0.75',
});
const { config } = await import('./env');
expect(config.ai.geminiApiKey).toBe('test-gemini-key');
expect(config.ai.geminiRpm).toBe(10);
expect(config.ai.priceQualityThreshold).toBe(0.75);
});
it('should parse Google configuration values', async () => {
setValidEnv({
GOOGLE_MAPS_API_KEY: 'test-maps-key',
GOOGLE_CLIENT_ID: 'test-client-id',
GOOGLE_CLIENT_SECRET: 'test-client-secret',
});
const { config } = await import('./env');
expect(config.google.mapsApiKey).toBe('test-maps-key');
expect(config.google.clientId).toBe('test-client-id');
expect(config.google.clientSecret).toBe('test-client-secret');
});
it('should parse optional SMTP configuration', async () => {
setValidEnv({
SMTP_HOST: 'smtp.example.com',
SMTP_USER: 'smtp-user',
SMTP_PASS: 'smtp-pass',
SMTP_FROM_EMAIL: 'noreply@example.com',
});
const { config } = await import('./env');
expect(config.smtp.host).toBe('smtp.example.com');
expect(config.smtp.user).toBe('smtp-user');
expect(config.smtp.pass).toBe('smtp-pass');
expect(config.smtp.fromEmail).toBe('noreply@example.com');
});
it('should parse optional JWT_SECRET_PREVIOUS for rotation', async () => {
setValidEnv({
JWT_SECRET_PREVIOUS: 'old-secret-that-is-at-least-32-characters-long',
});
const { config } = await import('./env');
expect(config.auth.jwtSecretPrevious).toBe('old-secret-that-is-at-least-32-characters-long');
});
it('should handle empty string values as undefined for optional int fields', async () => {
setValidEnv({
GEMINI_RPM: '',
AI_PRICE_QUALITY_THRESHOLD: ' ',
});
const { config } = await import('./env');
// Should use defaults when empty
expect(config.ai.geminiRpm).toBe(5);
expect(config.ai.priceQualityThreshold).toBe(0.5);
});
});
describe('convenience helpers', () => {
it('should export isProduction as false in test env', async () => {
setValidEnv({ NODE_ENV: 'test' });
const { isProduction } = await import('./env');
expect(isProduction).toBe(false);
});
it('should export isTest as true in test env', async () => {
setValidEnv({ NODE_ENV: 'test' });
const { isTest } = await import('./env');
expect(isTest).toBe(true);
});
it('should export isDevelopment as false in test env', async () => {
setValidEnv({ NODE_ENV: 'test' });
const { isDevelopment } = await import('./env');
expect(isDevelopment).toBe(false);
});
it('should export isSmtpConfigured as false when SMTP not configured', async () => {
setValidEnv();
const { isSmtpConfigured } = await import('./env');
expect(isSmtpConfigured).toBe(false);
});
it('should export isSmtpConfigured as true when all SMTP fields present', async () => {
setValidEnv({
SMTP_HOST: 'smtp.example.com',
SMTP_USER: 'user',
SMTP_PASS: 'pass',
SMTP_FROM_EMAIL: 'noreply@example.com',
});
const { isSmtpConfigured } = await import('./env');
expect(isSmtpConfigured).toBe(true);
});
it('should export isAiConfigured as false when Gemini not configured', async () => {
setValidEnv();
const { isAiConfigured } = await import('./env');
expect(isAiConfigured).toBe(false);
});
it('should export isAiConfigured as true when Gemini key present', async () => {
setValidEnv({
GEMINI_API_KEY: 'test-key',
});
const { isAiConfigured } = await import('./env');
expect(isAiConfigured).toBe(true);
});
it('should export isGoogleMapsConfigured as false when not configured', async () => {
setValidEnv();
const { isGoogleMapsConfigured } = await import('./env');
expect(isGoogleMapsConfigured).toBe(false);
});
it('should export isGoogleMapsConfigured as true when Maps key present', async () => {
setValidEnv({
GOOGLE_MAPS_API_KEY: 'test-maps-key',
});
const { isGoogleMapsConfigured } = await import('./env');
expect(isGoogleMapsConfigured).toBe(true);
});
});
describe('validation errors', () => {
it('should throw error when DB_HOST is missing', async () => {
setValidEnv();
delete process.env.DB_HOST;
await expect(import('./env')).rejects.toThrow('CONFIGURATION ERROR');
});
it('should throw error when DB_USER is missing', async () => {
setValidEnv();
delete process.env.DB_USER;
await expect(import('./env')).rejects.toThrow('CONFIGURATION ERROR');
});
it('should throw error when DB_PASSWORD is missing', async () => {
setValidEnv();
delete process.env.DB_PASSWORD;
await expect(import('./env')).rejects.toThrow('CONFIGURATION ERROR');
});
it('should throw error when DB_NAME is missing', async () => {
setValidEnv();
delete process.env.DB_NAME;
await expect(import('./env')).rejects.toThrow('CONFIGURATION ERROR');
});
it('should throw error when REDIS_URL is missing', async () => {
setValidEnv();
delete process.env.REDIS_URL;
await expect(import('./env')).rejects.toThrow('CONFIGURATION ERROR');
});
it('should throw error when REDIS_URL is invalid', async () => {
setValidEnv({
REDIS_URL: 'not-a-valid-url',
});
await expect(import('./env')).rejects.toThrow('CONFIGURATION ERROR');
});
it('should throw error when JWT_SECRET is missing', async () => {
setValidEnv();
delete process.env.JWT_SECRET;
await expect(import('./env')).rejects.toThrow('CONFIGURATION ERROR');
});
it('should throw error when JWT_SECRET is too short', async () => {
setValidEnv({
JWT_SECRET: 'short',
});
await expect(import('./env')).rejects.toThrow('CONFIGURATION ERROR');
});
it('should include field path in error message', async () => {
setValidEnv();
delete process.env.DB_HOST;
await expect(import('./env')).rejects.toThrow('database.host');
});
});
describe('environment modes', () => {
it('should set nodeEnv to development by default', async () => {
setValidEnv();
delete process.env.NODE_ENV;
const { config } = await import('./env');
expect(config.server.nodeEnv).toBe('development');
});
it('should accept production as NODE_ENV', async () => {
setValidEnv({
NODE_ENV: 'production',
});
const { config, isProduction, isDevelopment, isTest } = await import('./env');
expect(config.server.nodeEnv).toBe('production');
expect(isProduction).toBe(true);
expect(isDevelopment).toBe(false);
expect(isTest).toBe(false);
});
it('should accept development as NODE_ENV', async () => {
setValidEnv({
NODE_ENV: 'development',
});
const { config, isProduction, isDevelopment, isTest } = await import('./env');
expect(config.server.nodeEnv).toBe('development');
expect(isProduction).toBe(false);
expect(isDevelopment).toBe(true);
expect(isTest).toBe(false);
});
});
describe('server configuration', () => {
it('should parse FRONTEND_URL when provided', async () => {
setValidEnv({
FRONTEND_URL: 'https://example.com',
});
const { config } = await import('./env');
expect(config.server.frontendUrl).toBe('https://example.com');
});
it('should parse BASE_URL when provided', async () => {
setValidEnv({
BASE_URL: '/api/v1',
});
const { config } = await import('./env');
expect(config.server.baseUrl).toBe('/api/v1');
});
it('should parse STORAGE_PATH when provided', async () => {
setValidEnv({
STORAGE_PATH: '/custom/storage/path',
});
const { config } = await import('./env');
expect(config.server.storagePath).toBe('/custom/storage/path');
});
});
describe('Redis configuration', () => {
it('should parse REDIS_PASSWORD when provided', async () => {
setValidEnv({
REDIS_PASSWORD: 'redis-secret',
});
const { config } = await import('./env');
expect(config.redis.password).toBe('redis-secret');
});
});
});