diff --git a/src/infra/http/env.ts b/src/infra/http/env.ts new file mode 100644 index 0000000..3a3fb68 --- /dev/null +++ b/src/infra/http/env.ts @@ -0,0 +1,23 @@ +import { z } from 'zod' + +export const schemaEnv = z.object({ + PORTA: z.coerce.number().int().min(1).max(65535).default(3000), + NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), + JWT_SECRETO: z.string().min(32, 'JWT_SECRETO deve ter no mínimo 32 caracteres.'), + JWT_EXPIRACAO_ACCESS: z.string().default('1h'), + MONGO_URI: z.string().min(1, 'MONGO_URI não definido.'), + REDIS_URL: z.string().min(1, 'REDIS_URL não definido.'), +}) + +export type Env = z.infer + +/** + * Resolve a porta a partir de PORT (Render/cloud) ou PORTA (local/Docker). + * PORT tem precedência para compatibilidade com plataformas cloud. + */ +export function resolverEnv(vars: NodeJS.ProcessEnv): ReturnType { + return schemaEnv.safeParse({ + ...vars, + PORTA: vars.PORT ?? vars.PORTA, + }) +} diff --git a/src/infra/http/servidor.ts b/src/infra/http/servidor.ts index 855830e..aed0676 100644 --- a/src/infra/http/servidor.ts +++ b/src/infra/http/servidor.ts @@ -1,6 +1,6 @@ import { randomBytes, createHash } from 'crypto' -import { z } from 'zod' import { criarAplicacao } from '@/infra/http/aplicacao' +import { resolverEnv } from '@/infra/http/env' import { conectarMongoDB, desconectarMongoDB } from '@/infra/bd/conexao-mongodb' import { obterRedis, desconectarRedis } from '@/infra/cache/conexao-redis' import { RepositorioUsuarioMongo } from '@/modulos/usuarios/infra/repositorios/repositorio-usuario-mongo' @@ -33,17 +33,8 @@ import { BuscarHistoricoVisitante } from '@/modulos/visitantes/aplicacao/casos-d import { ControladorVisitante } from '@/modulos/visitantes/apresentacao/controladores/controlador-visitante' import { rotasVisitante } from '@/modulos/visitantes/apresentacao/rotas/rotas-visitante' -const schemaEnv = z.object({ - PORTA: z.coerce.number().int().min(1).max(65535).default(3000), - NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), - JWT_SECRETO: z.string().min(32, 'JWT_SECRETO deve ter no mínimo 32 caracteres.'), - JWT_EXPIRACAO_ACCESS: z.string().default('1h'), - MONGO_URI: z.string().min(1, 'MONGO_URI não definido.'), - REDIS_URL: z.string().min(1, 'REDIS_URL não definido.'), -}) - async function iniciar(): Promise { - const resultado = schemaEnv.safeParse(process.env) + const resultado = resolverEnv(process.env) if (!resultado.success) { process.stderr.write(`Configuração inválida:\n${resultado.error.message}\n`) process.exit(1) diff --git a/src/testes/unitarios/infra/env.test.ts b/src/testes/unitarios/infra/env.test.ts new file mode 100644 index 0000000..7c8fa43 --- /dev/null +++ b/src/testes/unitarios/infra/env.test.ts @@ -0,0 +1,92 @@ +import { describe, it, expect } from 'vitest' +import { resolverEnv } from '@/infra/http/env' + +const envBase = { + JWT_SECRETO: 'segredo-de-teste-com-pelo-menos-32-caracteres', + MONGO_URI: 'mongodb://localhost:27017/bem-vindo', + REDIS_URL: 'redis://localhost:6379', +} + +describe('resolverEnv', () => { + describe('resolução de porta', () => { + it('deve usar PORTA quando apenas PORTA está definida', () => { + const resultado = resolverEnv({ ...envBase, PORTA: '4000' }) + + expect(resultado.success).toBe(true) + if (resultado.success) { + expect(resultado.data.PORTA).toBe(4000) + } + }) + + it('deve usar PORT (Render) quando PORT está definida e PORTA não', () => { + const resultado = resolverEnv({ ...envBase, PORT: '10000' }) + + expect(resultado.success).toBe(true) + if (resultado.success) { + expect(resultado.data.PORTA).toBe(10000) + } + }) + + it('deve dar precedência a PORT sobre PORTA quando ambas estão definidas', () => { + const resultado = resolverEnv({ ...envBase, PORT: '10000', PORTA: '3000' }) + + expect(resultado.success).toBe(true) + if (resultado.success) { + expect(resultado.data.PORTA).toBe(10000) + } + }) + + it('deve usar porta padrão 3000 quando nem PORT nem PORTA estão definidas', () => { + const resultado = resolverEnv({ ...envBase }) + + expect(resultado.success).toBe(true) + if (resultado.success) { + expect(resultado.data.PORTA).toBe(3000) + } + }) + }) + + describe('validação de variáveis obrigatórias', () => { + it('deve falhar quando JWT_SECRETO está ausente', () => { + const resultado = resolverEnv({ MONGO_URI: envBase.MONGO_URI, REDIS_URL: envBase.REDIS_URL }) + + expect(resultado.success).toBe(false) + }) + + it('deve falhar quando JWT_SECRETO tem menos de 32 caracteres', () => { + const resultado = resolverEnv({ ...envBase, JWT_SECRETO: 'curto' }) + + expect(resultado.success).toBe(false) + }) + + it('deve falhar quando MONGO_URI está ausente', () => { + const resultado = resolverEnv({ JWT_SECRETO: envBase.JWT_SECRETO, REDIS_URL: envBase.REDIS_URL }) + + expect(resultado.success).toBe(false) + }) + + it('deve falhar quando REDIS_URL está ausente', () => { + const resultado = resolverEnv({ JWT_SECRETO: envBase.JWT_SECRETO, MONGO_URI: envBase.MONGO_URI }) + + expect(resultado.success).toBe(false) + }) + + it('deve usar NODE_ENV padrão development quando não definido', () => { + const resultado = resolverEnv({ ...envBase }) + + expect(resultado.success).toBe(true) + if (resultado.success) { + expect(resultado.data.NODE_ENV).toBe('development') + } + }) + + it('deve aceitar NODE_ENV production', () => { + const resultado = resolverEnv({ ...envBase, NODE_ENV: 'production' }) + + expect(resultado.success).toBe(true) + if (resultado.success) { + expect(resultado.data.NODE_ENV).toBe('production') + } + }) + }) +})