All checks were successful
Deploy to Homelab / deploy (push) Successful in 29s
Lays the foundation for brute-force defense: per-username attempt tracking table, configurable lockout/rate-limit thresholds, and a memoized dummy bcrypt hash so unknown-user paths can be timed identically to wrong-password paths in a later step. Adds @fastify/rate-limit dependency for upcoming per-IP rate-limit on login routes.
101 lines
3.5 KiB
TypeScript
101 lines
3.5 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
|
|
describe('config', () => {
|
|
const originalEnv = process.env;
|
|
|
|
beforeEach(() => {
|
|
process.env = { ...originalEnv };
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env = originalEnv;
|
|
});
|
|
|
|
it('returns defaults when env vars are not set', async () => {
|
|
process.env.JWT_SECRET = 'test-secret';
|
|
delete process.env.PORT;
|
|
delete process.env.HOST;
|
|
delete process.env.JWT_EXPIRY;
|
|
delete process.env.DB_PATH;
|
|
delete process.env.UPLOAD_DIR;
|
|
delete process.env.LOG_FILE;
|
|
delete process.env.MAX_FILE_SIZE;
|
|
delete process.env.BASE_URL;
|
|
delete process.env.COOKIE_SECURE;
|
|
delete process.env.TRUST_PROXY;
|
|
delete process.env.LOCKOUT_THRESHOLD;
|
|
delete process.env.LOCKOUT_BASE_SECONDS;
|
|
delete process.env.LOCKOUT_MAX_SECONDS;
|
|
delete process.env.LOGIN_MIN_RESPONSE_MS;
|
|
delete process.env.LOGIN_RATE_LIMIT_MAX;
|
|
delete process.env.LOGIN_RATE_LIMIT_WINDOW_SECONDS;
|
|
|
|
const { loadConfig } = await import('../../src/config.ts');
|
|
const config = loadConfig();
|
|
|
|
expect(config.port).toBe(3000);
|
|
expect(config.host).toBe('0.0.0.0');
|
|
expect(config.jwtExpiry).toBe('7d');
|
|
expect(config.dbPath).toBe('./data/nanodrop.db');
|
|
expect(config.uploadDir).toBe('./data/uploads');
|
|
expect(config.logFile).toBe('./data/nanodrop.log');
|
|
expect(config.maxFileSize).toBe(104857600);
|
|
expect(config.baseUrl).toBe('http://localhost:3000');
|
|
expect(config.cookieSecure).toBe(false);
|
|
expect(config.trustProxy).toBe(false);
|
|
expect(config.lockoutThreshold).toBe(5);
|
|
expect(config.lockoutBaseSeconds).toBe(30);
|
|
expect(config.lockoutMaxSeconds).toBe(3600);
|
|
expect(config.loginMinResponseMs).toBe(350);
|
|
expect(config.loginRateLimitMax).toBe(10);
|
|
expect(config.loginRateLimitWindowSeconds).toBe(60);
|
|
});
|
|
|
|
it('reads values from env vars', async () => {
|
|
process.env.JWT_SECRET = 'my-secret';
|
|
process.env.PORT = '4000';
|
|
process.env.HOST = '127.0.0.1';
|
|
process.env.JWT_EXPIRY = '1d';
|
|
process.env.COOKIE_SECURE = 'true';
|
|
process.env.TRUST_PROXY = 'true';
|
|
process.env.MAX_FILE_SIZE = '52428800';
|
|
|
|
const { loadConfig } = await import('../../src/config.ts');
|
|
const config = loadConfig();
|
|
|
|
expect(config.port).toBe(4000);
|
|
expect(config.host).toBe('127.0.0.1');
|
|
expect(config.jwtSecret).toBe('my-secret');
|
|
expect(config.jwtExpiry).toBe('1d');
|
|
expect(config.cookieSecure).toBe(true);
|
|
expect(config.trustProxy).toBe(true);
|
|
expect(config.maxFileSize).toBe(52428800);
|
|
});
|
|
|
|
it('reads lockout and rate-limit values from env vars', async () => {
|
|
process.env.JWT_SECRET = 'my-secret';
|
|
process.env.LOCKOUT_THRESHOLD = '3';
|
|
process.env.LOCKOUT_BASE_SECONDS = '15';
|
|
process.env.LOCKOUT_MAX_SECONDS = '900';
|
|
process.env.LOGIN_MIN_RESPONSE_MS = '50';
|
|
process.env.LOGIN_RATE_LIMIT_MAX = '20';
|
|
process.env.LOGIN_RATE_LIMIT_WINDOW_SECONDS = '120';
|
|
|
|
const { loadConfig } = await import('../../src/config.ts');
|
|
const config = loadConfig();
|
|
|
|
expect(config.lockoutThreshold).toBe(3);
|
|
expect(config.lockoutBaseSeconds).toBe(15);
|
|
expect(config.lockoutMaxSeconds).toBe(900);
|
|
expect(config.loginMinResponseMs).toBe(50);
|
|
expect(config.loginRateLimitMax).toBe(20);
|
|
expect(config.loginRateLimitWindowSeconds).toBe(120);
|
|
});
|
|
|
|
it('throws when JWT_SECRET is missing', async () => {
|
|
delete process.env.JWT_SECRET;
|
|
const { loadConfig } = await import('../../src/config.ts');
|
|
expect(() => loadConfig()).toThrow('JWT_SECRET');
|
|
});
|
|
});
|