-
Notifications
You must be signed in to change notification settings - Fork 2
feat: implementa leitura de eventos #541
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 532-cadastro-expedicoes
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import { BuscarEventoPorIdUseCase } from '@/domain/evento/BuscarEventoPorIdUseCase' | ||
| 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 { | ||
| buscarEventoPorIdUseCase: BuscarEventoPorIdUseCase | ||
| } | ||
|
|
||
| export class BuscarEventoController implements RequestHandler { | ||
| private readonly buscarEventoPorIdUseCase: BuscarEventoPorIdUseCase | ||
|
|
||
| constructor(dependencies: Dependencies) { | ||
| this.buscarEventoPorIdUseCase = dependencies.buscarEventoPorIdUseCase | ||
| } | ||
|
|
||
| async handle(request: HttpRequest, _next: NextHandler): Promise<HttpResponse | HttpError> { | ||
| const { eventoId } = request.params as { eventoId?: string } | ||
|
|
||
| if ( | ||
| eventoId === undefined | ||
| || eventoId === null | ||
| || eventoId === '' | ||
| || !/^\d+$/.test(eventoId) | ||
| ) { | ||
| return new BadRequestError({ message: 'eventoId inválido' }) | ||
| } | ||
|
|
||
| const result = await this.buscarEventoPorIdUseCase.execute({ | ||
| id: Number(eventoId) | ||
| }) | ||
|
|
||
| if (result.left()) { | ||
| return new InternalServerError({ message: result.value.message }) | ||
| } | ||
|
|
||
| if (!result.value) { | ||
| return new NotFoundError({ message: 'Evento não encontrado' }) | ||
| } | ||
|
|
||
| return { | ||
| statusCode: StatusCode.Ok, | ||
| body: result.value | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { ListaEventosUseCase } from '@/domain/evento/ListaEventosUseCase' | ||
| 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 { | ||
| listaEventosUseCase: ListaEventosUseCase | ||
| } | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Standards] Sem cobertura de integração. Só há teste unitário com use case mockado ( |
||
| export class ListaEventosController implements RequestHandler { | ||
| private readonly listaEventosUseCase: ListaEventosUseCase | ||
|
|
||
| constructor(dependencies: Dependencies) { | ||
| this.listaEventosUseCase = dependencies.listaEventosUseCase | ||
| } | ||
|
|
||
| async handle(request: HttpRequest, _next: NextHandler): Promise<HttpResponse | HttpError> { | ||
| const { expedicaoId } = request.params as { expedicaoId?: string } | ||
|
|
||
| if ( | ||
| expedicaoId === undefined | ||
| || expedicaoId === null | ||
| || expedicaoId === '' | ||
| || !/^\d+$/.test(expedicaoId) | ||
| ) { | ||
| return new BadRequestError({ message: 'expedicaoId inválido' }) | ||
| } | ||
|
|
||
| const result = await this.listaEventosUseCase.execute({ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Spec] Filtros, paginação e ordenação estável não chegam até aqui. |
||
| expedicao_id: Number(expedicaoId) | ||
| }) | ||
|
|
||
| if (result.left()) { | ||
| return new InternalServerError({ message: result.value.message }) | ||
| } | ||
|
|
||
| return { | ||
| statusCode: StatusCode.Ok, | ||
| body: result.value | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { type Knex } from 'knex' | ||
|
|
||
| import { BuscarEventoPorIdUseCase } from '@/domain/evento/BuscarEventoPorIdUseCase' | ||
| import { ListaEventosUseCase } from '@/domain/evento/ListaEventosUseCase' | ||
| import { EventoCollectionKnexAdapter } from '@/infrastructure/EventoCollectionKnexAdapter' | ||
| import { Method } from '@/library/http/common' | ||
| import { Route } from '@/library/http/Router' | ||
|
|
||
| import { BuscarEventoController } from './BuscarEventoController' | ||
| import { ListaEventosController } from './ListaEventosController' | ||
|
|
||
| export function routes(knex: Knex): Route[] { | ||
| const eventoCollection = new EventoCollectionKnexAdapter({ knex }) | ||
|
|
||
| // TODO: adicionar autenticação quando a camada HTTP de autenticação estiver disponível. | ||
|
|
||
| return [ | ||
| { | ||
| handlers: [ | ||
| new ListaEventosController({ | ||
| listaEventosUseCase: new ListaEventosUseCase({ eventoCollection }) | ||
| }) | ||
| ], | ||
| method: Method.Get, | ||
| path: '/v2/expedicoes/:expedicaoId/eventos' | ||
| }, | ||
| { | ||
| handlers: [ | ||
| new BuscarEventoController({ | ||
| buscarEventoPorIdUseCase: new BuscarEventoPorIdUseCase({ eventoCollection }) | ||
| }) | ||
| ], | ||
| method: Method.Get, | ||
| path: '/v2/eventos/:eventoId' | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { Either } from '@/library/either/Either' | ||
|
|
||
| import { Attributes } from './Evento' | ||
| import { EventoCollection } from './EventoCollection' | ||
|
|
||
| interface Dependencies { | ||
| eventoCollection: EventoCollection | ||
| } | ||
|
|
||
| export class BuscarEventoPorIdUseCase { | ||
| private readonly eventoCollection: EventoCollection | ||
|
|
||
| constructor(dependencies: Dependencies) { | ||
| this.eventoCollection = dependencies.eventoCollection | ||
| } | ||
|
|
||
| execute({ id }: { id: number }): Promise<Either<Error, Attributes | null>> { | ||
| return this.eventoCollection.findById(id) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { Either } from '@/library/either/Either' | ||
|
|
||
| import { Attributes } from './Evento' | ||
| import { EventoCollection, EventoFilters } from './EventoCollection' | ||
|
|
||
| interface Dependencies { | ||
| eventoCollection: EventoCollection | ||
| } | ||
|
|
||
| export class ListaEventosUseCase { | ||
| private readonly eventoCollection: EventoCollection | ||
|
|
||
| constructor(dependencies: Dependencies) { | ||
| this.eventoCollection = dependencies.eventoCollection | ||
| } | ||
|
|
||
| execute(filters: EventoFilters): Promise<Either<Error, Attributes[]>> { | ||
| return this.eventoCollection.findAll(filters) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Standards] Speculative Generality (smell). |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| import { | ||
| describe, expect, test, vi | ||
| } from 'vitest' | ||
|
|
||
| import { BuscarEventoController } from '@/application/evento/BuscarEventoController' | ||
| import { BuscarEventoPorIdUseCase } from '@/domain/evento/BuscarEventoPorIdUseCase' | ||
| import { Either } from '@/library/either/Either' | ||
| import { | ||
| Headers, HttpRequest, Method, StatusCode | ||
| } from '@/library/http/common' | ||
| import { BadRequestError } from '@/library/http/error/BadRequestError' | ||
| import { NotFoundError } from '@/library/http/error/NotFoundError' | ||
|
|
||
| describe('BuscarEventoController', () => { | ||
| const headers = {} as Headers | ||
|
|
||
| test('returns 200 with body when use case finds event', async () => { | ||
| const evento = { | ||
| id: 1, | ||
| expedicao_id: 10, | ||
| tipo: 'DIARIO' as const, | ||
| capturado_em: new Date('2026-09-15T10:00:00.000Z'), | ||
| latitude: null, | ||
| longitude: null, | ||
| altitude: null, | ||
| observacoes: 'Observação', | ||
| coleta: null, | ||
| created_at: new Date('2026-09-15T10:00:00.000Z'), | ||
| updated_at: new Date('2026-09-15T10:00:00.000Z'), | ||
| created_by: null, | ||
| updated_by: null | ||
| } | ||
|
|
||
| const buscarEventoPorIdUseCase = { | ||
| execute: vi.fn().mockResolvedValue(Either.right(evento)) | ||
| } as unknown as BuscarEventoPorIdUseCase | ||
|
|
||
| const controller = new BuscarEventoController({ | ||
| buscarEventoPorIdUseCase | ||
| }) | ||
|
|
||
| const request = { | ||
| body: {}, | ||
| headers, | ||
| method: Method.Get, | ||
| params: { eventoId: '1' }, | ||
| path: '/eventos/1' | ||
| } satisfies HttpRequest | ||
|
|
||
| const response = await controller.handle(request, vi.fn()) | ||
|
|
||
| expect(buscarEventoPorIdUseCase.execute).toHaveBeenCalledWith({ | ||
| id: 1 | ||
| }) | ||
| expect('statusCode' in response ? response.statusCode : undefined).toBe(StatusCode.Ok) | ||
| expect('body' in response ? response.body : undefined).toEqual(evento) | ||
| }) | ||
|
|
||
| test('returns BadRequest when eventoId is invalid', async () => { | ||
| const buscarEventoPorIdUseCase = { | ||
| execute: vi.fn() | ||
| } as unknown as BuscarEventoPorIdUseCase | ||
|
|
||
| const controller = new BuscarEventoController({ | ||
| buscarEventoPorIdUseCase | ||
| }) | ||
|
|
||
| const request = { | ||
| body: {}, | ||
| headers, | ||
| method: Method.Get, | ||
| params: { eventoId: 'abc' }, | ||
| path: '/eventos/abc' | ||
| } satisfies HttpRequest | ||
|
|
||
| const response = await controller.handle(request, vi.fn()) | ||
|
|
||
| expect(buscarEventoPorIdUseCase.execute).not.toHaveBeenCalled() | ||
| expect(response).toBeInstanceOf(BadRequestError) | ||
| }) | ||
|
|
||
| test('returns NotFound when event does not exist', async () => { | ||
| const buscarEventoPorIdUseCase = { | ||
| execute: vi.fn().mockResolvedValue(Either.right(null)) | ||
| } as unknown as BuscarEventoPorIdUseCase | ||
|
|
||
| const controller = new BuscarEventoController({ | ||
| buscarEventoPorIdUseCase | ||
| }) | ||
|
|
||
| const request = { | ||
| body: {}, | ||
| headers, | ||
| method: Method.Get, | ||
| params: { eventoId: '1' }, | ||
| path: '/eventos/1' | ||
| } satisfies HttpRequest | ||
|
|
||
| const response = await controller.handle(request, vi.fn()) | ||
|
|
||
| expect(response).toBeInstanceOf(NotFoundError) | ||
| }) | ||
|
|
||
| test('returns InternalServerError when use case fails', async () => { | ||
| const buscarEventoPorIdUseCase = { | ||
| execute: vi.fn().mockResolvedValue(Either.left(new Error('boom'))) | ||
| } as unknown as BuscarEventoPorIdUseCase | ||
|
|
||
| const controller = new BuscarEventoController({ | ||
| buscarEventoPorIdUseCase | ||
| }) | ||
|
|
||
| const request = { | ||
| body: {}, | ||
| headers, | ||
| method: Method.Get, | ||
| params: { eventoId: '1' }, | ||
| path: '/eventos/1' | ||
| } satisfies HttpRequest | ||
|
|
||
| const response = await controller.handle(request, vi.fn()) | ||
|
|
||
| expect((response as Error).name).toBe('InternalServerError') | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Standards] Duplicated Code (smell). Este bloco de validação de id (
undefined/null/''/regex) é idêntico ao deListaEventosController.ts:22-27, só muda o nome do campo e a mensagem. Com duas ocorrências ainda não acho que compense extrair, mas vale ficar de olho se aparecer um terceiro controller.