Files
auth-portal/apps/api/test/config.test.ts
T

139 lines
4.5 KiB
TypeScript

import { afterEach, describe, expect, it } from 'vitest'
import { Writable } from 'node:stream'
import Fastify from 'fastify'
import { assertBootstrapPassword, loadConfig } from '../src/config.js'
import { buildApp } from '../src/app.js'
const base = { JWT_SECRET: 'a-sufficiently-long-secret-123' }
describe('loadConfig', () => {
it('значения по умолчанию для новых параметров', () => {
const c = loadConfig({ ...base, NODE_ENV: 'test' })
expect(c.shutdownTimeoutMs).toBe(8000)
expect(c.sessionRetentionDays).toBe(7)
expect(c.argon2Concurrency).toBe(2)
expect(c.backupKeep).toBe(7)
expect(c.backupIntervalHours).toBe(0)
expect(c.corsOrigins).toBe('')
expect(c.trustProxy).toContain('10.0.0.0/8')
})
it('читает переопределения из env', () => {
const c = loadConfig({
...base,
NODE_ENV: 'test',
SHUTDOWN_TIMEOUT_MS: '1500',
SESSION_RETENTION_DAYS: '30',
ARGON2_CONCURRENCY: '4',
BACKUP_KEEP: '3',
BACKUP_INTERVAL_HOURS: '12',
CORS_ORIGINS: 'https://a.example',
TRUST_PROXY: 'false',
})
expect(c).toMatchObject({
shutdownTimeoutMs: 1500,
sessionRetentionDays: 30,
argon2Concurrency: 4,
backupKeep: 3,
backupIntervalHours: 12,
corsOrigins: 'https://a.example',
trustProxy: 'false',
})
})
it.each([
['SHUTDOWN_TIMEOUT_MS', '0'],
['ARGON2_CONCURRENCY', '-1'],
['BACKUP_KEEP', 'abc'],
['BACKUP_INTERVAL_HOURS', '-5'],
])('невалидный %s=%s → ошибка', (key, value) => {
expect(() => loadConfig({ ...base, NODE_ENV: 'test', [key]: value })).toThrow()
})
})
describe('production guard', () => {
it.each(['change-me-in-production', 'dev-secret-change-me', 'short-secret'])(
'слабый JWT_SECRET "%s" отклоняется',
(secret) => {
expect(() =>
loadConfig({ NODE_ENV: 'production', JWT_SECRET: secret }),
).toThrow(/JWT_SECRET/)
},
)
it('нормальный секрет в production принимается; в test слабый разрешён', () => {
expect(() =>
loadConfig({ NODE_ENV: 'production', JWT_SECRET: base.JWT_SECRET }),
).not.toThrow()
expect(() =>
loadConfig({ NODE_ENV: 'test', JWT_SECRET: 'short-secret' }),
).not.toThrow()
})
it('пароль первого админа: слабый запрещён только в production', () => {
const prod = (pw?: string) =>
loadConfig({ NODE_ENV: 'production', ...base, ADMIN_PASSWORD: pw })
expect(() => assertBootstrapPassword(prod('admin'))).toThrow(/ADMIN_PASSWORD/)
expect(() => assertBootstrapPassword(prod(''))).toThrow(/ADMIN_PASSWORD/)
expect(() => assertBootstrapPassword(prod('short'))).toThrow(/ADMIN_PASSWORD/)
expect(() => assertBootstrapPassword(prod('long-enough-pw'))).not.toThrow()
expect(() =>
assertBootstrapPassword(loadConfig({ NODE_ENV: 'test', ...base })),
).not.toThrow()
})
it('buildApp в production без нормального пароля падает при создании админа', async () => {
await expect(
buildApp({
config: loadConfig({
NODE_ENV: 'production',
...base,
ADMIN_PASSWORD: 'admin',
DATABASE_URL: 'sqlite::memory:',
}),
databaseUrl: 'sqlite::memory:',
}),
).rejects.toThrow(/ADMIN_PASSWORD/)
})
})
describe('redact в логах', () => {
let app: ReturnType<typeof Fastify> | undefined
afterEach(async () => {
await app?.close()
})
it('authorization и cookie не попадают в лог', async () => {
const lines: string[] = []
const stream = new Writable({
write(chunk, _enc, cb) {
lines.push(String(chunk))
cb()
},
})
// Тот же набор путей, что и в buildApp.
app = Fastify({
logger: {
level: 'info',
stream,
redact: {
paths: ['req.headers.authorization', 'req.headers.cookie'],
censor: '[redacted]',
},
serializers: {
req: (req) => ({ method: req.method, headers: req.headers }),
},
},
})
app.get('/x', async () => ({ ok: true }))
await app.inject({
method: 'GET',
url: '/x',
headers: { authorization: 'Bearer SECRET', cookie: 'refresh_token=SECRET' },
})
const joined = lines.join('')
expect(joined).toContain('[redacted]')
expect(joined).not.toContain('SECRET')
})
})