Skip to content

Feat: Cadastrar, Renomear e Remover tipos de vegetações - #525

Open
JosueModesto wants to merge 5 commits into
developmentfrom
490-cadastrar-renomear-remover-tipos-vegetacoes
Open

JosueModesto wants to merge 5 commits into
developmentfrom
490-cadastrar-renomear-remover-tipos-vegetacoes

Conversation

@JosueModesto

@JosueModesto JosueModesto commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Close #490

O que foi feito

Foi implementado o módulo de cadastro, atualização e remoção de tipos de vegetação na API v2, preservando a API legada sem alterações e seguindo o padrão de autenticação já utilizado pelo projeto.

Alterações principais

  • Implementação dos endpoints de vegetação na API v2:
    • POST /api/v2/vegetacoes
    • PUT /api/v2/vegetacoes/:vegetacaoId
    • DELETE /api/v2/vegetacoes/:vegetacaoId
  • Validação de acesso de escrita com middleware específico para vegetações, respeitando o segredo JWT do projeto e os tipos de usuário permitidos.
  • Validação de nome vazio com retorno 400.
  • Validação de duplicidade de nome com retorno 409.
  • Validação de vegetacaoId inválido com retorno 400.
  • Retorno 404 quando o registro não existe.
  • Tratamento de conflito ao remover uma vegetação em uso em tombos com retorno 409.
  • Garantia de integridade no banco por meio de index único em LOWER(nome), além de tratamento compatível de erros de duplicidade e dependência em diferentes motores de banco.
  • Manutenção da API legacy /api sem alterações.

Cobertura de testes

  • Cadastro com sucesso
  • Cadastro com nome vazio
  • Cadastro com nome duplicado
  • Atualização de vegetação existente
  • Atualização com id inexistente
  • Atualização com id inválido
  • Atualização com nome duplicado
  • Remoção com sucesso
  • Remoção com vegetação em uso
  • Rejeição de token assinado com segredo inválido quando JWT_SECRET não está configurado

@JosueModesto
JosueModesto requested review from edvaldoszy and a lite review from Copilot and removed request for Copilot September 4, 2026 19:37

return [
{
handlers: [

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

POST, PUT e DELETE montam só o controller em handlers. O legado POST /vegetacoes exige tokensMiddleware([CURADOR, OPERADOR]). País/estado v2 são só GET públicos e não justificam mutação anônima do catálogo. Os testes de escrita passam sem Authorization.


if (result.left()) {
const message = result.value.message.toLowerCase()
if (message.includes('duplicate') || message.includes('unique') || message.includes('already') || message.includes('exist')) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

message.includes('exist') trata como conflito qualquer falha cujo texto contenha exist (por exemplo relation "vegetacoes" does not exist). O else devolve InternalServerError com result.value.message; no adapter o catch usa cause.message cru. O mesmo padrão está em RenomeiaVegetacaoController e RemoveVegetacaoController.

}
})

test('retorna 404 para id inexistente', async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O PR pede nome vazio → 400, duplicidade → 409, vegetacaoId inválido → 400 e 404. Este arquivo só cobre sucesso, 404 e id abc. DELETE só cobre 204 e 409 em uso. No PUT, nome duplicado vira BadRequestError 400, não 409.

if (result.left()) {
const message = result.value.message.toLowerCase()
if (message.includes('duplicate') || message.includes('unique') || message.includes('already') || message.includes('exist')) {
return new BadRequestError({ message: 'Já existe uma vegetação com esse nome' })

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicidade no PUT retorna BadRequestError (400). O POST mapeia o mesmo caso para ConflictError (409). Vale alinhar os dois para 409.

return Either.left(new CollectionError({ message: cause.message, cause }))
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unicidade é findByNome e depois insert/update. A tabela vegetacoes só tem PK, então duas requisições concorrentes com o mesmo nome passam no select e as duas gravam. O catch de unique/23505 não dispara sem constraint.

expect(body.error.message).toMatch(/vazio|empty|obrigat/i)
})

test('retorna 400 para nome duplicado', async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O título do teste diz 400, mas o expect na linha 49 é 409.

}

try {
const payload = jwt.verify(token, process.env.JWT_SECRET ?? 'test-secret') as { tipo_usuario_id?: unknown }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jwt.verify usa process.env.JWT_SECRET ?? 'test-secret'. O legado verifica com o secret de src/config/security.js, sem fallback. Sem JWT_SECRET, qualquer token assinado com test-secret passa. Os testes de integração assinam com o mesmo fallback; o secret de teste deve ficar no setup, não no handler de produção.

updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
);

CREATE UNIQUE INDEX vegetacoes_nome_unique

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O índice único em LOWER(nome) existe só no schema de integração. Não há migration em src/database e o model Sequelize de vegetacoes.nome não é unique. Em MySQL de produção o check-then-write continua sujeito a corrida e o 23505 do adapter não dispara.

const message = typeof error === 'object' && error !== null && 'message' in error ? String((error as { message?: unknown }).message) : ''
const constraint = typeof error === 'object' && error !== null && 'constraint' in error ? String((error as { constraint?: unknown }).constraint) : ''

return code === '23505'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unique e FK estão mapeados com códigos/textos Postgres (23505, 23503, violates foreign key constraint). O app default é MySQL (ER_DUP_ENTRY 1062, ER_ROW_IS_REFERENCED_2 1451, foreign key constraint fails). Delete em uso no MySQL pode virar 500 em vez de 409 se error.constraint não vier preenchido.


if (!authorization || typeof authorization !== 'string' || !authorization.startsWith('Bearer ')) {
return new ForbiddenError({ message: 'Token de autenticação obrigatório' })
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Token ausente ou vazio retorna 403, papel diferente de 1|2 retorna 403, expirado ou inválido retorna 401. Os testes de POST/PUT/DELETE só enviam token de curador no caminho feliz; esses ramos novos não estão cobertos.


const ALLOWED_TIPOS_USUARIOS = new Set([1, 2])

export class RequireVegetacaoWriteAccess implements RequestHandler {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RequireVegetacaoWriteAccess está no infinitivo inglês. Tipos novos devem usar 3ª pessoa do singular (RequiresVegetacaoWriteAccess) ou um verbo finito em português, sem misturar idiomas no nome.

}

const cause = error instanceof Error ? error : new Error(String(error))
return Either.left(new CollectionError({ message: cause.message, cause }))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O catch genérico devolve cause.message. Essa string chega no InternalServerError do controller e pode expor SQL no JSON de 500.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cadastrar, renomear e remover tipos de vegetação

2 participants