All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 15m11s
433 lines
12 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|