From 79881c7386f78d3c9f7b04c0f9c2e8d6459e0e97 Mon Sep 17 00:00:00 2001 From: josuemc Date: Wed, 2 Sep 2026 17:12:14 -0300 Subject: [PATCH 1/5] =?UTF-8?q?Feat:=20Implementa=C3=A7=C3=A3o=20listar=20?= =?UTF-8?q?e=20buscar=20fases=20sucessionais=20Api.v2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/application/create-app.ts | 4 +- .../BuscarFaseSucessionalController.ts | 42 ++++++ .../ListaFasesSucessionaisController.ts | 65 +++++++++ src/application/fase-sucessional/index.ts | 35 +++++ .../BuscarFaseSucessionalUseCase.ts | 20 +++ src/domain/faseSucessional/FaseSucessional.ts | 24 ++++ .../FaseSucessionalCollection.ts | 16 +++ .../ListaFasesSucessionaisUseCase.ts | 20 +++ .../FaseSucessionalCollectionKnexAdapter.ts | 53 +++++++ .../lista-fases-sucessionais.test.ts | 131 ++++++++++++++++++ test/integration/setup/global-setup.ts | 3 +- 11 files changed, 411 insertions(+), 2 deletions(-) create mode 100644 src/application/fase-sucessional/BuscarFaseSucessionalController.ts create mode 100644 src/application/fase-sucessional/ListaFasesSucessionaisController.ts create mode 100644 src/application/fase-sucessional/index.ts create mode 100644 src/domain/faseSucessional/BuscarFaseSucessionalUseCase.ts create mode 100644 src/domain/faseSucessional/FaseSucessional.ts create mode 100644 src/domain/faseSucessional/FaseSucessionalCollection.ts create mode 100644 src/domain/faseSucessional/ListaFasesSucessionaisUseCase.ts create mode 100644 src/infrastructure/FaseSucessionalCollectionKnexAdapter.ts create mode 100644 test/integration/fase-sucessional/lista-fases-sucessionais.test.ts diff --git a/src/application/create-app.ts b/src/application/create-app.ts index 0f53e372..96d84f95 100644 --- a/src/application/create-app.ts +++ b/src/application/create-app.ts @@ -13,6 +13,7 @@ import { assets, upload } from '../config/directory' import legacyErrors from '../middlewares/erros-middleware' import { generatePreview, reportPreview } from '../reports/controller' import { routes as createEstadoRoutes } from './estado' +import { routes as createFaseSucessionalRoutes } from './fase-sucessional' import { routes as createPaisRoutes } from './pais' interface CorsParameters { @@ -55,7 +56,8 @@ export function createApp({ }: Parameters) { const routes: Route[] = [ ...createPaisRoutes(knex), - ...createEstadoRoutes(knex) + ...createEstadoRoutes(knex), + ...createFaseSucessionalRoutes(knex) ] const application = new ExpressApplication({ logger }) diff --git a/src/application/fase-sucessional/BuscarFaseSucessionalController.ts b/src/application/fase-sucessional/BuscarFaseSucessionalController.ts new file mode 100644 index 00000000..969998f9 --- /dev/null +++ b/src/application/fase-sucessional/BuscarFaseSucessionalController.ts @@ -0,0 +1,42 @@ +import { BuscarFaseSucessionalUseCase } from '@/domain/faseSucessional/BuscarFaseSucessionalUseCase' +import { + HttpRequest, HttpResponse, StatusCode +} from '@/library/http/common' +import { BadRequestError } from '@/library/http/error/BadRequestError' +import { HttpError } from '@/library/http/error/HttpError' +import { InternalServerError } from '@/library/http/error/InternalServerError' +import { NotFoundError } from '@/library/http/error/NotFoundError' +import { NextHandler, RequestHandler } from '@/library/http/Server' + +interface Dependencies { + buscarFaseSucessionalUseCase: BuscarFaseSucessionalUseCase +} + +export class BuscarFaseSucessionalController implements RequestHandler { + private readonly buscarFaseSucessionalUseCase: BuscarFaseSucessionalUseCase + + constructor(dependencies: Dependencies) { + this.buscarFaseSucessionalUseCase = dependencies.buscarFaseSucessionalUseCase + } + + async handle(request: HttpRequest, _next: NextHandler): Promise { + const { faseSucessionalId } = request.params as { faseSucessionalId?: string } + + if (!faseSucessionalId || Number.isNaN(Number(faseSucessionalId)) || Number(faseSucessionalId) <= 0) { + return new BadRequestError({ message: 'faseSucessionalId inválido' }) + } + + const id = Number(faseSucessionalId) + const result = await this.buscarFaseSucessionalUseCase.execute({ id }) + + if (result.left()) { + return new InternalServerError({ message: result.value.message }) + } + + if (!result.value) { + return new NotFoundError({ message: 'Fase sucessional não encontrada' }) + } + + return { statusCode: StatusCode.Ok, body: result.value } + } +} diff --git a/src/application/fase-sucessional/ListaFasesSucessionaisController.ts b/src/application/fase-sucessional/ListaFasesSucessionaisController.ts new file mode 100644 index 00000000..b5ed1dbd --- /dev/null +++ b/src/application/fase-sucessional/ListaFasesSucessionaisController.ts @@ -0,0 +1,65 @@ +import { ListaFasesSucessionaisUseCase } from '@/domain/faseSucessional/ListaFasesSucessionaisUseCase' +import { + HttpRequest, HttpResponse, StatusCode +} from '@/library/http/common' +import { BadRequestError } from '@/library/http/error/BadRequestError' +import { HttpError } from '@/library/http/error/HttpError' +import { InternalServerError } from '@/library/http/error/InternalServerError' +import { NextHandler, RequestHandler } from '@/library/http/Server' + +interface Dependencies { + listaFasesSucessionaisUseCase: ListaFasesSucessionaisUseCase +} + +export class ListaFasesSucessionaisController implements RequestHandler { + private readonly listaFasesSucessionaisUseCase: ListaFasesSucessionaisUseCase + + constructor(dependencies: Dependencies) { + this.listaFasesSucessionaisUseCase = dependencies.listaFasesSucessionaisUseCase + } + + async handle(request: HttpRequest, _next: NextHandler): Promise { + const { nome, order } = request.params as { + nome?: string + order?: string + } + + const parsedOrder = parseOrder(order) + if (parsedOrder instanceof Error) { + return new BadRequestError({ message: parsedOrder.message }) + } + + const result = await this.listaFasesSucessionaisUseCase.execute({ + nome, + order: parsedOrder + }) + + if (result.left()) { + return new InternalServerError({ message: result.value.message }) + } + + return { statusCode: StatusCode.Ok, body: result.value } + } +} + +function parseOrder(order?: string): { column: 'id' | 'nome'; direction: 'asc' | 'desc' } | Error | undefined { + if (!order) return undefined + + const pieces = order.split(':') + if (pieces.length !== 2) { + return new Error('order inválido. Use o formato "id:asc", "id:desc", "nome:asc" ou "nome:desc"') + } + + const [rawColumn, rawDirection] = pieces + const column = rawColumn.trim().toLowerCase() + const direction = rawDirection.trim().toLowerCase() + + if ((column !== 'id' && column !== 'nome') || (direction !== 'asc' && direction !== 'desc')) { + return new Error('order inválido. Use o formato "id:asc", "id:desc", "nome:asc" ou "nome:desc"') + } + + return { + column, + direction + } +} diff --git a/src/application/fase-sucessional/index.ts b/src/application/fase-sucessional/index.ts new file mode 100644 index 00000000..9f12cfd8 --- /dev/null +++ b/src/application/fase-sucessional/index.ts @@ -0,0 +1,35 @@ +import { type Knex } from 'knex' + +import { BuscarFaseSucessionalUseCase } from '@/domain/faseSucessional/BuscarFaseSucessionalUseCase' +import { ListaFasesSucessionaisUseCase } from '@/domain/faseSucessional/ListaFasesSucessionaisUseCase' +import { FaseSucessionalCollectionKnexAdapter } from '@/infrastructure/FaseSucessionalCollectionKnexAdapter' +import { Method } from '@/library/http/common' +import { Route } from '@/library/http/Router' + +import { BuscarFaseSucessionalController } from './BuscarFaseSucessionalController' +import { ListaFasesSucessionaisController } from './ListaFasesSucessionaisController' + +export function routes(knex: Knex): Route[] { + const faseSucessionalCollection = new FaseSucessionalCollectionKnexAdapter({ knex }) + + return [ + { + handlers: [ + new ListaFasesSucessionaisController({ + listaFasesSucessionaisUseCase: new ListaFasesSucessionaisUseCase({ faseSucessionalCollection }) + }) + ], + method: Method.Get, + path: '/v2/fases-sucessionais' + }, + { + handlers: [ + new BuscarFaseSucessionalController({ + buscarFaseSucessionalUseCase: new BuscarFaseSucessionalUseCase({ faseSucessionalCollection }) + }) + ], + method: Method.Get, + path: '/v2/fases-sucessionais/:faseSucessionalId' + } + ] +} diff --git a/src/domain/faseSucessional/BuscarFaseSucessionalUseCase.ts b/src/domain/faseSucessional/BuscarFaseSucessionalUseCase.ts new file mode 100644 index 00000000..76ee6405 --- /dev/null +++ b/src/domain/faseSucessional/BuscarFaseSucessionalUseCase.ts @@ -0,0 +1,20 @@ +import { Either } from '@/library/either/Either' + +import { Attributes } from './FaseSucessional' +import { FaseSucessionalCollection } from './FaseSucessionalCollection' + +interface Dependencies { + faseSucessionalCollection: FaseSucessionalCollection +} + +export class BuscarFaseSucessionalUseCase { + private readonly faseSucessionalCollection: FaseSucessionalCollection + + constructor(dependencies: Dependencies) { + this.faseSucessionalCollection = dependencies.faseSucessionalCollection + } + + async execute({ id }: { id: number }): Promise> { + return this.faseSucessionalCollection.findById(id) + } +} diff --git a/src/domain/faseSucessional/FaseSucessional.ts b/src/domain/faseSucessional/FaseSucessional.ts new file mode 100644 index 00000000..97ddc29a --- /dev/null +++ b/src/domain/faseSucessional/FaseSucessional.ts @@ -0,0 +1,24 @@ +import { Either } from '@/library/either/Either' + +export interface Attributes { + id: number + nome: string +} + +export class FaseSucessional { + readonly id: number + readonly nome: string + + private constructor(attributes: Attributes) { + this.id = attributes.id + this.nome = attributes.nome + } + + static create(attributes: Attributes): Either { + if (!attributes.nome.trim()) { + return Either.left(new Error('Nome da fase sucessional não pode ser vazio')) + } + + return Either.right(new FaseSucessional(attributes)) + } +} diff --git a/src/domain/faseSucessional/FaseSucessionalCollection.ts b/src/domain/faseSucessional/FaseSucessionalCollection.ts new file mode 100644 index 00000000..351a7cfd --- /dev/null +++ b/src/domain/faseSucessional/FaseSucessionalCollection.ts @@ -0,0 +1,16 @@ +import { Either } from '@/library/either/Either' + +import { Attributes } from './FaseSucessional' + +export interface FaseSucessionalFilters { + nome?: string + order?: { + column: 'id' | 'nome' + direction: 'asc' | 'desc' + } +} + +export interface FaseSucessionalCollection { + findAll(filters: FaseSucessionalFilters): Promise> + findById(id: number): Promise> +} diff --git a/src/domain/faseSucessional/ListaFasesSucessionaisUseCase.ts b/src/domain/faseSucessional/ListaFasesSucessionaisUseCase.ts new file mode 100644 index 00000000..ccd78084 --- /dev/null +++ b/src/domain/faseSucessional/ListaFasesSucessionaisUseCase.ts @@ -0,0 +1,20 @@ +import { Either } from '@/library/either/Either' + +import { Attributes } from './FaseSucessional' +import { FaseSucessionalCollection, FaseSucessionalFilters } from './FaseSucessionalCollection' + +interface Dependencies { + faseSucessionalCollection: FaseSucessionalCollection +} + +export class ListaFasesSucessionaisUseCase { + private readonly faseSucessionalCollection: FaseSucessionalCollection + + constructor(dependencies: Dependencies) { + this.faseSucessionalCollection = dependencies.faseSucessionalCollection + } + + async execute(filters: FaseSucessionalFilters): Promise> { + return this.faseSucessionalCollection.findAll(filters) + } +} diff --git a/src/infrastructure/FaseSucessionalCollectionKnexAdapter.ts b/src/infrastructure/FaseSucessionalCollectionKnexAdapter.ts new file mode 100644 index 00000000..0b2103e0 --- /dev/null +++ b/src/infrastructure/FaseSucessionalCollectionKnexAdapter.ts @@ -0,0 +1,53 @@ +import { Knex } from 'knex' + +import { Attributes } from '@/domain/faseSucessional/FaseSucessional' +import { FaseSucessionalCollection, FaseSucessionalFilters } from '@/domain/faseSucessional/FaseSucessionalCollection' +import { Either } from '@/library/either/Either' + +import { CollectionError } from './error/CollectionError' + +interface Dependencies { + knex: Knex +} + +export class FaseSucessionalCollectionKnexAdapter implements FaseSucessionalCollection { + private readonly knex: Knex + + constructor(dependencies: Dependencies) { + this.knex = dependencies.knex + } + + async findAll(filters: FaseSucessionalFilters): Promise> { + try { + const query = this.knex('fase_sucessional') + .select(['id', 'nome']) + + if (filters.nome) { + query.whereILike('nome', `%${filters.nome}%`) + } + + if (filters.order) { + query.orderBy(filters.order.column, filters.order.direction) + } else { + query.orderBy('id', 'desc') + } + + return Either.right(await query) + } catch (error) { + return Either.left(new CollectionError({ message: 'Failed to list fases sucessionais', cause: error })) + } + } + + async findById(id: number): Promise> { + try { + const faseSucessional = await this.knex('fase_sucessional') + .select(['id', 'nome']) + .where({ id }) + .first() + + return Either.right(faseSucessional ?? null) + } catch (error) { + return Either.left(new CollectionError({ message: 'Failed to find fase sucessional by id', cause: error })) + } + } +} diff --git a/test/integration/fase-sucessional/lista-fases-sucessionais.test.ts b/test/integration/fase-sucessional/lista-fases-sucessionais.test.ts new file mode 100644 index 00000000..0a251e4a --- /dev/null +++ b/test/integration/fase-sucessional/lista-fases-sucessionais.test.ts @@ -0,0 +1,131 @@ +import { + afterAll, describe, expect, test +} from 'vitest' + +import { createTestApp } from '../setup/app-factory' + +type FaseSucessional = { id: number; nome: string } + +const returning = ['id', 'nome'] as const + +describe('GET /api/v2/fases-sucessionais', () => { + const { agent, knex } = createTestApp() + + afterAll(() => knex.destroy()) + + test('retorna a lista ordenada por id decrescente como padrão dentro do prefixo do teste', async () => { + const prefix = 'XFAS' + const nomes = [ + `${prefix} Inicial`, + `${prefix} Médio`, + `${prefix} Avançado` + ] + + const inserted = await knex('fase_sucessional') + .insert(nomes.map(nome => ({ nome }))) + .returning(returning) + + try { + const response = await agent.get(`/api/v2/fases-sucessionais?nome=${prefix}`).expect(200) + const expected = [...inserted].sort((a, b) => b.id - a.id) + expect(response.body).toEqual(expected) + } finally { + await knex('fase_sucessional').whereIn('nome', nomes).delete() + } + }) + + test('filtra por nome sem diferenciar maiúsculas e minúsculas', async () => { + const prefix = 'XFAS' + const nomes = [ + `${prefix} Inicial`, + `${prefix} Médio`, + `${prefix} Avançado` + ] + + const inserted = await knex('fase_sucessional') + .insert(nomes.map(nome => ({ nome }))) + .returning(returning) + + try { + const response = await agent.get(`/api/v2/fases-sucessionais?nome=${prefix} inicial`).expect(200) + expect(response.body).toEqual(inserted.filter(item => item.nome === `${prefix} Inicial`)) + } finally { + await knex('fase_sucessional').whereIn('nome', nomes).delete() + } + }) + + test('aceita ordenação customizada por nome e id', async () => { + const prefix = 'XFAS' + const nomes = [ + `${prefix} Z`, + `${prefix} A`, + `${prefix} M` + ] + + const inserted = await knex('fase_sucessional') + .insert(nomes.map(nome => ({ nome }))) + .returning(returning) + + try { + const byNameAsc = await agent.get(`/api/v2/fases-sucessionais?nome=${prefix}&order=nome:asc`).expect(200) + expect(byNameAsc.body).toEqual([...inserted].sort((a, b) => a.nome.localeCompare(b.nome))) + + const byIdAsc = await agent.get(`/api/v2/fases-sucessionais?nome=${prefix}&order=id:asc`).expect(200) + expect(byIdAsc.body).toEqual([...inserted].sort((a, b) => a.id - b.id)) + } finally { + await knex('fase_sucessional').whereIn('nome', nomes).delete() + } + }) + + test('retorna 400 quando a ordenação é inválida', async () => { + const prefix = 'XFAS' + const nomes = [ + `${prefix} Z`, + `${prefix} A`, + `${prefix} M` + ] + + await knex('fase_sucessional').insert(nomes.map(nome => ({ nome }))) + + try { + const response = await agent.get(`/api/v2/fases-sucessionais?nome=${prefix}&order=foo:bar`).expect(400) + const body = response.body as { error: { message: string } } + expect(body.error.message).toMatch(/inválido|invalid/i) + } finally { + await knex('fase_sucessional').whereIn('nome', nomes).delete() + } + }) +}) + +describe('GET /api/v2/fases-sucessionais/:faseSucessionalId', () => { + const { agent, knex } = createTestApp() + + afterAll(() => knex.destroy()) + + test('retorna o registro encontrado', async () => { + const [faseSucessional] = await knex('fase_sucessional') + .insert({ nome: 'XFAS Fase Encontrada' }) + .returning(returning) + + try { + const response = await agent.get(`/api/v2/fases-sucessionais/${faseSucessional.id}`).expect(200) + expect(response.body).toEqual({ id: faseSucessional.id, nome: faseSucessional.nome }) + } finally { + await knex('fase_sucessional').where({ id: faseSucessional.id }).delete() + } + }) + + test('retorna 404 para id inexistente', async () => { + const response = await agent.get('/api/v2/fases-sucessionais/999999').expect(404) + const body = response.body as { error: { message: string } } + + expect(body.error.message).toMatch(/não encontrad[ao]|not found/i) + }) + + test('retorna 400 para id inválido', async () => { + const response = await agent.get('/api/v2/fases-sucessionais/abc').expect(400) + const body = response.body as { error: { message: string } } + + expect(body.error.message).toMatch(/inválido|invalid/i) + }) +}) diff --git a/test/integration/setup/global-setup.ts b/test/integration/setup/global-setup.ts index b9cb050d..12b00421 100644 --- a/test/integration/setup/global-setup.ts +++ b/test/integration/setup/global-setup.ts @@ -9,7 +9,8 @@ try { const TABLES = [ 'estados', - 'paises' + 'paises', + 'fase_sucessional' ] async function truncateTables(knex: Knex): Promise { From 91c880e2a5988e43196ff8fcf14b6e7d9b2fc0fb Mon Sep 17 00:00:00 2001 From: josuemc Date: Fri, 4 Sep 2026 12:21:11 -0300 Subject: [PATCH 2/5] =?UTF-8?q?Fix:=20Corre=C3=A7=C3=A3o=20da=20Busca?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BuscaFaseSucessionalController.ts | 42 ++++++++++++++++ src/application/fase-sucessional/index.ts | 8 +-- .../BuscaFaseSucessionalUseCase.ts | 20 ++++++++ .../busca-fases-sucessionais.test.ts | 49 +++++++++++++++++++ .../lista-fases-sucessionais.test.ts | 33 ------------- 5 files changed, 115 insertions(+), 37 deletions(-) create mode 100644 src/application/fase-sucessional/BuscaFaseSucessionalController.ts create mode 100644 src/domain/faseSucessional/BuscaFaseSucessionalUseCase.ts create mode 100644 test/integration/fase-sucessional/busca-fases-sucessionais.test.ts diff --git a/src/application/fase-sucessional/BuscaFaseSucessionalController.ts b/src/application/fase-sucessional/BuscaFaseSucessionalController.ts new file mode 100644 index 00000000..c7065fdb --- /dev/null +++ b/src/application/fase-sucessional/BuscaFaseSucessionalController.ts @@ -0,0 +1,42 @@ +import { BuscaFaseSucessionalUseCase } from '@/domain/faseSucessional/BuscaFaseSucessionalUseCase' +import { + HttpRequest, HttpResponse, StatusCode +} from '@/library/http/common' +import { BadRequestError } from '@/library/http/error/BadRequestError' +import { HttpError } from '@/library/http/error/HttpError' +import { InternalServerError } from '@/library/http/error/InternalServerError' +import { NotFoundError } from '@/library/http/error/NotFoundError' +import { NextHandler, RequestHandler } from '@/library/http/Server' + +interface Dependencies { + buscaFaseSucessionalUseCase: BuscaFaseSucessionalUseCase +} + +export class BuscaFaseSucessionalController implements RequestHandler { + private readonly buscaFaseSucessionalUseCase: BuscaFaseSucessionalUseCase + + constructor(dependencies: Dependencies) { + this.buscaFaseSucessionalUseCase = dependencies.buscaFaseSucessionalUseCase + } + + async handle(request: HttpRequest, _next: NextHandler): Promise { + const { faseSucessionalId } = request.params as { faseSucessionalId?: string } + + if (!faseSucessionalId || Number.isNaN(Number(faseSucessionalId)) || Number(faseSucessionalId) <= 0) { + return new BadRequestError({ message: 'faseSucessionalId inválido' }) + } + + const id = Number(faseSucessionalId) + const result = await this.buscaFaseSucessionalUseCase.execute({ id }) + + if (result.left()) { + return new InternalServerError({ message: result.value.message }) + } + + if (!result.value) { + return new NotFoundError({ message: 'Fase sucessional não encontrada' }) + } + + return { statusCode: StatusCode.Ok, body: result.value } + } +} diff --git a/src/application/fase-sucessional/index.ts b/src/application/fase-sucessional/index.ts index 9f12cfd8..ea9274f5 100644 --- a/src/application/fase-sucessional/index.ts +++ b/src/application/fase-sucessional/index.ts @@ -1,12 +1,12 @@ import { type Knex } from 'knex' -import { BuscarFaseSucessionalUseCase } from '@/domain/faseSucessional/BuscarFaseSucessionalUseCase' +import { BuscaFaseSucessionalUseCase } from '@/domain/faseSucessional/BuscaFaseSucessionalUseCase' import { ListaFasesSucessionaisUseCase } from '@/domain/faseSucessional/ListaFasesSucessionaisUseCase' import { FaseSucessionalCollectionKnexAdapter } from '@/infrastructure/FaseSucessionalCollectionKnexAdapter' import { Method } from '@/library/http/common' import { Route } from '@/library/http/Router' -import { BuscarFaseSucessionalController } from './BuscarFaseSucessionalController' +import { BuscaFaseSucessionalController } from './BuscaFaseSucessionalController' import { ListaFasesSucessionaisController } from './ListaFasesSucessionaisController' export function routes(knex: Knex): Route[] { @@ -24,8 +24,8 @@ export function routes(knex: Knex): Route[] { }, { handlers: [ - new BuscarFaseSucessionalController({ - buscarFaseSucessionalUseCase: new BuscarFaseSucessionalUseCase({ faseSucessionalCollection }) + new BuscaFaseSucessionalController({ + buscaFaseSucessionalUseCase: new BuscaFaseSucessionalUseCase({ faseSucessionalCollection }) }) ], method: Method.Get, diff --git a/src/domain/faseSucessional/BuscaFaseSucessionalUseCase.ts b/src/domain/faseSucessional/BuscaFaseSucessionalUseCase.ts new file mode 100644 index 00000000..6f88101a --- /dev/null +++ b/src/domain/faseSucessional/BuscaFaseSucessionalUseCase.ts @@ -0,0 +1,20 @@ +import { Either } from '@/library/either/Either' + +import { Attributes } from './FaseSucessional' +import { FaseSucessionalCollection } from './FaseSucessionalCollection' + +interface Dependencies { + faseSucessionalCollection: FaseSucessionalCollection +} + +export class BuscaFaseSucessionalUseCase { + private readonly faseSucessionalCollection: FaseSucessionalCollection + + constructor(dependencies: Dependencies) { + this.faseSucessionalCollection = dependencies.faseSucessionalCollection + } + + async execute({ id }: { id: number }): Promise> { + return this.faseSucessionalCollection.findById(id) + } +} diff --git a/test/integration/fase-sucessional/busca-fases-sucessionais.test.ts b/test/integration/fase-sucessional/busca-fases-sucessionais.test.ts new file mode 100644 index 00000000..d4cc8c28 --- /dev/null +++ b/test/integration/fase-sucessional/busca-fases-sucessionais.test.ts @@ -0,0 +1,49 @@ +import { + afterAll, describe, expect, test +} from 'vitest' + +import { createTestApp } from '../setup/app-factory' + +type FaseSucessional = { id: number; nome: string } + +const returning = ['id', 'nome'] as const + +describe('GET /api/v2/fases-sucessionais/:faseSucessionalId', () => { + const { agent, knex } = createTestApp() + + afterAll(() => knex.destroy()) + + test('retorna o registro encontrado', async () => { + const [faseSucessional] = await knex('fase_sucessional') + .insert({ nome: 'XFAS Fase Encontrada' }) + .returning(returning) + + try { + const response = await agent.get(`/api/v2/fases-sucessionais/${faseSucessional.id}`).expect(200) + expect(response.body).toEqual({ id: faseSucessional.id, nome: faseSucessional.nome }) + } finally { + await knex('fase_sucessional').where({ id: faseSucessional.id }).delete() + } + }) + + test('retorna 404 para id inexistente', async () => { + const response = await agent.get('/api/v2/fases-sucessionais/999999').expect(404) + const body = response.body as { error: { message: string } } + + expect(body.error.message).toMatch(/não encontrad[ao]|not found/i) + }) + + test('retorna 400 para id inválido', async () => { + const response = await agent.get('/api/v2/fases-sucessionais/abc').expect(400) + const body = response.body as { error: { message: string } } + + expect(body.error.message).toMatch(/inválido|invalid/i) + }) + + test('retorna 400 para id inválido', async () => { + const response = await agent.get('/api/v2/fases-sucessionais/-12').expect(400) + const body = response.body as { error: { message: string } } + + expect(body.error.message).toMatch(/inválido|invalid/i) + }) +}) diff --git a/test/integration/fase-sucessional/lista-fases-sucessionais.test.ts b/test/integration/fase-sucessional/lista-fases-sucessionais.test.ts index 0a251e4a..95cb28ad 100644 --- a/test/integration/fase-sucessional/lista-fases-sucessionais.test.ts +++ b/test/integration/fase-sucessional/lista-fases-sucessionais.test.ts @@ -96,36 +96,3 @@ describe('GET /api/v2/fases-sucessionais', () => { } }) }) - -describe('GET /api/v2/fases-sucessionais/:faseSucessionalId', () => { - const { agent, knex } = createTestApp() - - afterAll(() => knex.destroy()) - - test('retorna o registro encontrado', async () => { - const [faseSucessional] = await knex('fase_sucessional') - .insert({ nome: 'XFAS Fase Encontrada' }) - .returning(returning) - - try { - const response = await agent.get(`/api/v2/fases-sucessionais/${faseSucessional.id}`).expect(200) - expect(response.body).toEqual({ id: faseSucessional.id, nome: faseSucessional.nome }) - } finally { - await knex('fase_sucessional').where({ id: faseSucessional.id }).delete() - } - }) - - test('retorna 404 para id inexistente', async () => { - const response = await agent.get('/api/v2/fases-sucessionais/999999').expect(404) - const body = response.body as { error: { message: string } } - - expect(body.error.message).toMatch(/não encontrad[ao]|not found/i) - }) - - test('retorna 400 para id inválido', async () => { - const response = await agent.get('/api/v2/fases-sucessionais/abc').expect(400) - const body = response.body as { error: { message: string } } - - expect(body.error.message).toMatch(/inválido|invalid/i) - }) -}) From 6c02d802434be986731e06bbe959e7746c637ce2 Mon Sep 17 00:00:00 2001 From: josuemc Date: Wed, 9 Sep 2026 17:05:53 -0300 Subject: [PATCH 3/5] =?UTF-8?q?fix;=20corre=C3=A7=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/application/create-app.ts | 1 - .../fase-sucessional/busca-fases-sucessionais.test.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/application/create-app.ts b/src/application/create-app.ts index cd6b6b8e..a3b3901c 100644 --- a/src/application/create-app.ts +++ b/src/application/create-app.ts @@ -59,7 +59,6 @@ export function createApp({ ...createPaisRoutes(knex), ...createEstadoRoutes(knex), ...createFaseSucessionalRoutes(knex), - ...createFaseSucessionalRoutes(knex), ...createVegetacaoRoutes(knex) ] const application = new ExpressApplication({ logger }) diff --git a/test/integration/fase-sucessional/busca-fases-sucessionais.test.ts b/test/integration/fase-sucessional/busca-fases-sucessionais.test.ts index d4cc8c28..9e8f0433 100644 --- a/test/integration/fase-sucessional/busca-fases-sucessionais.test.ts +++ b/test/integration/fase-sucessional/busca-fases-sucessionais.test.ts @@ -40,7 +40,7 @@ describe('GET /api/v2/fases-sucessionais/:faseSucessionalId', () => { expect(body.error.message).toMatch(/inválido|invalid/i) }) - test('retorna 400 para id inválido', async () => { + test('retorna 400 para id negativo', async () => { const response = await agent.get('/api/v2/fases-sucessionais/-12').expect(400) const body = response.body as { error: { message: string } } From ff927d2d5f4d3155521a254d5a162f44a79a3735 Mon Sep 17 00:00:00 2001 From: josuemc Date: Wed, 9 Sep 2026 17:08:52 -0300 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20remo=C3=A7=C3=A3o=20de=20arquivos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BuscarFaseSucessionalController.ts | 42 ------------------- .../BuscarFaseSucessionalUseCase.ts | 20 --------- 2 files changed, 62 deletions(-) delete mode 100644 src/application/fase-sucessional/BuscarFaseSucessionalController.ts delete mode 100644 src/domain/faseSucessional/BuscarFaseSucessionalUseCase.ts diff --git a/src/application/fase-sucessional/BuscarFaseSucessionalController.ts b/src/application/fase-sucessional/BuscarFaseSucessionalController.ts deleted file mode 100644 index 969998f9..00000000 --- a/src/application/fase-sucessional/BuscarFaseSucessionalController.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { BuscarFaseSucessionalUseCase } from '@/domain/faseSucessional/BuscarFaseSucessionalUseCase' -import { - HttpRequest, HttpResponse, StatusCode -} from '@/library/http/common' -import { BadRequestError } from '@/library/http/error/BadRequestError' -import { HttpError } from '@/library/http/error/HttpError' -import { InternalServerError } from '@/library/http/error/InternalServerError' -import { NotFoundError } from '@/library/http/error/NotFoundError' -import { NextHandler, RequestHandler } from '@/library/http/Server' - -interface Dependencies { - buscarFaseSucessionalUseCase: BuscarFaseSucessionalUseCase -} - -export class BuscarFaseSucessionalController implements RequestHandler { - private readonly buscarFaseSucessionalUseCase: BuscarFaseSucessionalUseCase - - constructor(dependencies: Dependencies) { - this.buscarFaseSucessionalUseCase = dependencies.buscarFaseSucessionalUseCase - } - - async handle(request: HttpRequest, _next: NextHandler): Promise { - const { faseSucessionalId } = request.params as { faseSucessionalId?: string } - - if (!faseSucessionalId || Number.isNaN(Number(faseSucessionalId)) || Number(faseSucessionalId) <= 0) { - return new BadRequestError({ message: 'faseSucessionalId inválido' }) - } - - const id = Number(faseSucessionalId) - const result = await this.buscarFaseSucessionalUseCase.execute({ id }) - - if (result.left()) { - return new InternalServerError({ message: result.value.message }) - } - - if (!result.value) { - return new NotFoundError({ message: 'Fase sucessional não encontrada' }) - } - - return { statusCode: StatusCode.Ok, body: result.value } - } -} diff --git a/src/domain/faseSucessional/BuscarFaseSucessionalUseCase.ts b/src/domain/faseSucessional/BuscarFaseSucessionalUseCase.ts deleted file mode 100644 index 76ee6405..00000000 --- a/src/domain/faseSucessional/BuscarFaseSucessionalUseCase.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Either } from '@/library/either/Either' - -import { Attributes } from './FaseSucessional' -import { FaseSucessionalCollection } from './FaseSucessionalCollection' - -interface Dependencies { - faseSucessionalCollection: FaseSucessionalCollection -} - -export class BuscarFaseSucessionalUseCase { - private readonly faseSucessionalCollection: FaseSucessionalCollection - - constructor(dependencies: Dependencies) { - this.faseSucessionalCollection = dependencies.faseSucessionalCollection - } - - async execute({ id }: { id: number }): Promise> { - return this.faseSucessionalCollection.findById(id) - } -} From 36e9fa205eac9ec255eeed8de94c80f7e7aa88ab Mon Sep 17 00:00:00 2001 From: josuemc Date: Wed, 9 Sep 2026 17:15:56 -0300 Subject: [PATCH 5/5] =?UTF-8?q?corre=C3=A7=C3=A3o=20dos=20testes=20para=20?= =?UTF-8?q?nao=20ocorrer=20contamina=C3=A7=C3=A3o=20dos=20dados?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fase-sucessional/busca-fases-sucessionais.test.ts | 3 ++- .../fase-sucessional/lista-fases-sucessionais.test.ts | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/test/integration/fase-sucessional/busca-fases-sucessionais.test.ts b/test/integration/fase-sucessional/busca-fases-sucessionais.test.ts index 9e8f0433..420f447e 100644 --- a/test/integration/fase-sucessional/busca-fases-sucessionais.test.ts +++ b/test/integration/fase-sucessional/busca-fases-sucessionais.test.ts @@ -14,8 +14,9 @@ describe('GET /api/v2/fases-sucessionais/:faseSucessionalId', () => { afterAll(() => knex.destroy()) test('retorna o registro encontrado', async () => { + const nome = `XFAS_BUSCA_${Date.now()}` const [faseSucessional] = await knex('fase_sucessional') - .insert({ nome: 'XFAS Fase Encontrada' }) + .insert({ nome }) .returning(returning) try { diff --git a/test/integration/fase-sucessional/lista-fases-sucessionais.test.ts b/test/integration/fase-sucessional/lista-fases-sucessionais.test.ts index 95cb28ad..a9603577 100644 --- a/test/integration/fase-sucessional/lista-fases-sucessionais.test.ts +++ b/test/integration/fase-sucessional/lista-fases-sucessionais.test.ts @@ -14,7 +14,7 @@ describe('GET /api/v2/fases-sucessionais', () => { afterAll(() => knex.destroy()) test('retorna a lista ordenada por id decrescente como padrão dentro do prefixo do teste', async () => { - const prefix = 'XFAS' + const prefix = `XFAS_LISTA_${Date.now()}` const nomes = [ `${prefix} Inicial`, `${prefix} Médio`, @@ -35,7 +35,7 @@ describe('GET /api/v2/fases-sucessionais', () => { }) test('filtra por nome sem diferenciar maiúsculas e minúsculas', async () => { - const prefix = 'XFAS' + const prefix = `XFAS_FILTRO_${Date.now()}` const nomes = [ `${prefix} Inicial`, `${prefix} Médio`, @@ -55,7 +55,7 @@ describe('GET /api/v2/fases-sucessionais', () => { }) test('aceita ordenação customizada por nome e id', async () => { - const prefix = 'XFAS' + const prefix = `XFAS_ORDEM_${Date.now()}` const nomes = [ `${prefix} Z`, `${prefix} A`, @@ -78,7 +78,7 @@ describe('GET /api/v2/fases-sucessionais', () => { }) test('retorna 400 quando a ordenação é inválida', async () => { - const prefix = 'XFAS' + const prefix = `XFAS_ORDEM_INVALIDA_${Date.now()}` const nomes = [ `${prefix} Z`, `${prefix} A`,