From f9c0bc076893eb040fdc59e95a9cbd7c08df5da8 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Mon, 18 May 2026 06:58:55 +0700 Subject: [PATCH] feat: add environment variables validation Co-Authored-By: Claude Opus 4.7 --- src/env.js | 32 ++++++++++++++++++++++++++++++++ test/env.test.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 src/env.js create mode 100644 test/env.test.js diff --git a/src/env.js b/src/env.js new file mode 100644 index 0000000..4d20676 --- /dev/null +++ b/src/env.js @@ -0,0 +1,32 @@ +import logger from './utils/logger.js'; + +const requiredEnv = { + BOT_TOKEN: process.env.BOT_TOKEN, + STORAGE_CHANNEL_ID: process.env.STORAGE_CHANNEL_ID, + BASE_URL: process.env.BASE_URL, + DATABASE_URL: process.env.DATABASE_URL, + PORT: process.env.PORT +}; + +const missing = Object.entries(requiredEnv) + .filter(([_, value]) => value === undefined || value === '') + .map(([key]) => key); + +if (missing.length > 0) { + logger.error('Missing required environment variables:', missing); + throw new Error(`Missing environment variables: ${missing.join(', ')}`); +} + +export const config = { + botToken: process.env.BOT_TOKEN, + storageChatId: parseInt(process.env.STORAGE_CHANNEL_ID, 10), + baseUrl: process.env.BASE_URL, + databaseUrl: process.env.DATABASE_URL, + port: parseInt(process.env.PORT, 10) || 3000, + nodeEnv: process.env.NODE_ENV || 'development', + logLevel: process.env.LOG_LEVEL || 'info', + rateLimitWindowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) || 60000, + rateLimitMaxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS, 10) || 30 +}; + +logger.info('Environment variables loaded', { config: { ...config, botToken: config.botToken?.substring(0, 10) + '...' } }); diff --git a/test/env.test.js b/test/env.test.js new file mode 100644 index 0000000..e9f267c --- /dev/null +++ b/test/env.test.js @@ -0,0 +1,45 @@ +import { describe, it, expect, beforeAll } from "bun:test"; +import { config } from "../src/env.js"; + +describe("Environment Variables Validation", () => { + it("config should have all required fields", () => { + expect(config).toHaveProperty("botToken"); + expect(config).toHaveProperty("storageChatId"); + expect(config).toHaveProperty("baseUrl"); + expect(config).toHaveProperty("databaseUrl"); + expect(config).toHaveProperty("port"); + expect(config).toHaveProperty("nodeEnv"); + expect(config).toHaveProperty("logLevel"); + expect(config).toHaveProperty("rateLimitWindowMs"); + expect(config).toHaveProperty("rateLimitMaxRequests"); + }); + + it("config.botToken should return BOT_TOKEN from process.env", () => { + expect(config.botToken).toBe(process.env.BOT_TOKEN); + }); + + it("config.storageChatId should be parsed as integer from STORAGE_CHANNEL_ID", () => { + expect(typeof config.storageChatId).toBe("number"); + expect(config.storageChatId).toBe(parseInt(process.env.STORAGE_CHANNEL_ID, 10)); + }); + + it("config.port should default to 3000 when not specified", () => { + expect(typeof config.port).toBe("number"); + }); + + it("nodeEnv should be 'test' or 'development'", () => { + expect(["test", "development"]).toContain(config.nodeEnv); + }); + + it("logLevel should default to 'info'", () => { + expect(config.logLevel).toBe("info"); + }); + + it("rateLimitWindowMs should default to 60000 when not specified", () => { + expect(config.rateLimitWindowMs).toBe(60000); + }); + + it("rateLimitMaxRequests should default to 30 when not specified", () => { + expect(config.rateLimitMaxRequests).toBe(30); + }); +});