Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/infra/http/env.ts
Original file line number Diff line number Diff line change
@@ -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<typeof schemaEnv>

/**
* 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<typeof schemaEnv.safeParse> {
return schemaEnv.safeParse({
...vars,
PORTA: vars.PORT ?? vars.PORTA,
})
}
13 changes: 2 additions & 11 deletions src/infra/http/servidor.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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<void> {
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)
Expand Down
92 changes: 92 additions & 0 deletions src/testes/unitarios/infra/env.test.ts
Original file line number Diff line number Diff line change
@@ -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')
}
})
})
})
Loading