Files
dinggadingga-backend/src/config/env.validation.spec.ts
MastersGame 3dd5369175 feat(security): share rate limits across instances, drain syncs on shutdown
- Rate limiting counters live in PostgreSQL (throttler_records), shared by
  every instance, one atomic upsert per request; fails open.
- On shutdown, new syncs are refused (503) and the running one is waited for
  up to SYNC_SHUTDOWN_TIMEOUT; Prisma disconnects last.
- Hiboutik calls never follow redirects and are capped at 50 MB.
- Prisma errors no longer print query arguments (errorFormat minimal).
- In production, DATABASE_URL must require TLS.
- The sync lock connection times out after 10 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 12:21:15 +01:00

71 lines
2.6 KiB
TypeScript

import 'reflect-metadata';
import { validateEnv } from './env.validation';
describe('validateEnv', () => {
const valid = {
DATABASE_URL: 'postgresql://user:secret@localhost:5432/db',
HIBOUTIK_BASE_URL: 'https://shop.hiboutik.com/api',
HIBOUTIK_USERNAME: 'user',
HIBOUTIK_PASSWORD: 'secret-password',
};
it('accepts a valid configuration and keeps every variable', () => {
const env = { ...valid, PORT: '3000', CORS_ORIGINS: 'https://shop.example.com', TRUST_PROXY: '1', OTHER: 'x' };
expect(validateEnv(env)).toBe(env);
});
it('rejects a missing required variable, naming it', () => {
expect(() => validateEnv({ ...valid, HIBOUTIK_PASSWORD: undefined })).toThrow(/HIBOUTIK_PASSWORD/);
});
it('rejects a non-HTTPS Hiboutik URL', () => {
expect(() => validateEnv({ ...valid, HIBOUTIK_BASE_URL: 'http://shop.hiboutik.com/api' })).toThrow(/HIBOUTIK_BASE_URL/);
});
it('rejects an invalid port', () => {
expect(() => validateEnv({ ...valid, PORT: 'abc' })).toThrow(/PORT/);
expect(() => validateEnv({ ...valid, PORT: '70000' })).toThrow(/PORT/);
});
it('never puts secret values in the error message', () => {
expect.assertions(2);
try {
validateEnv({ ...valid, HIBOUTIK_BASE_URL: 'http://insecure', DATABASE_URL: '' });
} catch (error) {
expect((error as Error).message).not.toContain('secret-password');
expect((error as Error).message).not.toContain('secret');
}
});
describe('in production', () => {
const production = { ...valid, NODE_ENV: 'production' };
it('requires an encrypted database connection', () => {
expect(() => validateEnv(production)).toThrow(/DATABASE_URL: sslmode/);
expect(() => validateEnv({ ...production, DATABASE_URL: `${valid.DATABASE_URL}?sslmode=disable` })).toThrow(/DATABASE_URL/);
});
it.each(['require', 'verify-ca', 'verify-full'])('accepts sslmode=%s', (mode) => {
expect(() => validateEnv({ ...production, DATABASE_URL: `${valid.DATABASE_URL}?sslmode=${mode}` })).not.toThrow();
});
it('never puts the connection string in the error message', () => {
expect.assertions(2);
try {
validateEnv(production);
} catch (error) {
expect((error as Error).message).toContain('DATABASE_URL');
expect((error as Error).message).not.toContain('secret');
}
});
});
it('does not require TLS outside production', () => {
expect(() => validateEnv({ ...valid, NODE_ENV: 'development' })).not.toThrow();
});
it('rejects an unknown NODE_ENV', () => {
expect(() => validateEnv({ ...valid, NODE_ENV: 'prod' })).toThrow(/NODE_ENV/);
});
});