- 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>
71 lines
2.6 KiB
TypeScript
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/);
|
|
});
|
|
});
|