import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; // admin.config calls dotenv.config(), which would read the repo's own .env and // quietly reintroduce NODE_ENV=development - the exact value several of these // cases exist to remove. Stub it so the tests see only what they set. vi.mock('dotenv', () => ({config: vi.fn()})); /** * admin.config reads the environment once at import, so every case here has to * reset the module registry and re-import it. The two things worth pinning are * the ones that are silent when wrong: which client-IP header is trusted, and * whether an unset NODE_ENV counts as production. */ const ORIGINAL_ENV = {...process.env}; const loadConfig = async () => { vi.resetModules(); return import('../../src/models/admin/admin.config.js'); }; beforeEach(() => { process.env = {...ORIGINAL_ENV}; // dotenv.config() in admin.config does not overwrite what is already set, // so setting these here is enough to keep the local .env out of the test. process.env.NODE_ENV = 'test'; delete process.env.CLIENT_IP_HEADERS; delete process.env.TRUSTED_PROXY_IPS; }); afterEach(() => { process.env = {...ORIGINAL_ENV}; }); describe('CLIENT_IP_HEADERS', () => { it('defaults to the single header Plesk nginx sets', async () => { const config = await loadConfig(); expect(config.CLIENT_IP_HEADERS).toEqual(['x-real-ip']); expect(config.TRUST_NO_CLIENT_IP_HEADER).toBe(false); }); it('reads a comma-separated list', async () => { process.env.CLIENT_IP_HEADERS = 'x-real-ip, cf-connecting-ip'; const config = await loadConfig(); expect(config.CLIENT_IP_HEADERS).toEqual(['x-real-ip', 'cf-connecting-ip']); }); it('trusts nothing when set to "none"', async () => { // The escape hatch. An empty list is what better-auth reads as "no // headers" - it only falls back to its own default when the option is // absent - so this really does stop any header being believed. process.env.CLIENT_IP_HEADERS = 'none'; const config = await loadConfig(); expect(config.CLIENT_IP_HEADERS).toEqual([]); expect(config.TRUST_NO_CLIENT_IP_HEADER).toBe(true); }); it('accepts the hatch case-insensitively and with stray whitespace', async () => { process.env.CLIENT_IP_HEADERS = ' NONE '; const config = await loadConfig(); expect(config.CLIENT_IP_HEADERS).toEqual([]); }); it('treats an empty value as "use the default", not as the hatch', async () => { // A blank line in a .env must not silently change how requests are // bucketed - only the explicit word does that. process.env.CLIENT_IP_HEADERS = ''; const config = await loadConfig(); expect(config.CLIENT_IP_HEADERS).toEqual(['x-real-ip']); expect(config.TRUST_NO_CLIENT_IP_HEADER).toBe(false); }); it('does not mistake a header actually named none-ish for the hatch', async () => { process.env.CLIENT_IP_HEADERS = 'x-none'; const config = await loadConfig(); expect(config.CLIENT_IP_HEADERS).toEqual(['x-none']); expect(config.TRUST_NO_CLIENT_IP_HEADER).toBe(false); }); }); describe('isProd', () => { it('is false only for the explicit relaxed environments', async () => { process.env.NODE_ENV = 'development'; expect((await loadConfig()).isProd).toBe(false); process.env.NODE_ENV = 'test'; expect((await loadConfig()).isProd).toBe(false); }); it('treats an unset NODE_ENV as production, which is what a bare vhost gives', async () => { delete process.env.NODE_ENV; // Strict mode refuses to boot without these; supply them so the import // gets far enough to answer the question being asked. process.env.BETTER_AUTH_SECRET = 'x'.repeat(48); process.env.API_BASE_URL = 'https://api.nachklang.art'; process.env.ADMIN_APP_URL = 'https://admin.nachklang.art'; expect((await loadConfig()).isProd).toBe(true); }); it('refuses to start without a signing key outside development', async () => { delete process.env.NODE_ENV; delete process.env.BETTER_AUTH_SECRET; process.env.API_BASE_URL = 'https://api.nachklang.art'; process.env.ADMIN_APP_URL = 'https://admin.nachklang.art'; await expect(loadConfig()).rejects.toThrow(/BETTER_AUTH_SECRET/); }); });